cachegate 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,554 +1,855 @@
1
- # Open-Source Release Roadmap — 20 Steps
2
-
3
- **What this is:** the concrete execution plan for `ROADMAP.md`'s Phase 5
4
- ("open-source release"). That phase has existed as a single line —
5
- "Public GitHub repo → npm package → Docker image → announce" — since
6
- 2026-08-23; this is what actually has to happen to make each of those
7
- four words true, broken into 20 numbered steps with the sub-work under
8
- each, so progress is trackable the same way the embedded build itself
9
- was.
10
-
11
- **Scope, precisely** (see `ROADMAP.md` section 6 and the 2026-08-29
12
- open-source-scope discussion in the Decision Journal): **only the
13
- embedded engine gets open-sourced** — routing, caching, metrics,
14
- providers, the dashboard. Login, payment, multi-tenancy, and anything
15
- else that makes the **standalone** hosted product ($ Phase 6) are never
16
- part of this repo, this package, or this Docker image. If a step below
17
- ever seems to call for adding one of those things, that's a sign the
18
- step has drifted out of scope, not a sign the scope needs expanding.
19
-
20
- **Status legend:** ⬜ not started · 🟨 in progress · ✅ done · ⏸ blocked/waiting
21
-
22
- **Lesson applied from the embedded build's own history (see
23
- `ROADMAP.md`'s "Note on an older numbering"):** that 20-step list was
24
- never written down as one document and step 18's content couldn't be
25
- reconstructed months later. This one is written down as one document,
26
- here, on purpose — update status inline as steps close, don't let this
27
- become a second undocumented list.
28
-
29
- ---
30
-
31
- ## Group A — Decide before building anything (steps 1-5)
32
-
33
- ### 1. ✅ Decide the extraction method — **decided 2026-08-29: fresh, curated history**
34
- - **Checked directly** (`git log --format="%an <%ae>" --all -- 210_apps/001_model_router/`):
35
- the real commit history contains a real person's full name and
36
- personal Gmail address in the author field, across 12+ commits — not
37
- a hypothetical risk, a confirmed one. That alone settles the
38
- decision: **do not preserve the original git history** when
39
- extracting. A `git subtree split`/`filter-repo` approach would carry
40
- that identity into a permanently public, unrescindable record the
41
- moment the repo goes live.
42
- - No leaked API keys or other secrets found in the history's actual
43
- diffs (checked separately — see step 3) — the ONE real exposure is
44
- the author metadata itself, which a fresh/squashed history sidesteps
45
- entirely along with everything else.
46
- - **Decision: fresh, curated history** — squash to a clean set of
47
- milestone commits authored under a project/org identity, not
48
- personal ones, when step 16 actually pushes this public.
49
-
50
- ### 2. ✅ Pick a license — **decided 2026-08-29: MIT**
51
- - **Decision: MIT**, not a BSL-style source-available license, for this
52
- first release. Reasoning, weighed against the alternative:
53
- - Phase 5's whole stated purpose is trust/adoption ("top-of-funnel"
54
- per section 3's own monetization table) - the two launch
55
- communities already planned for (Hacker News, r/selfhosted) react
56
- badly to source-available licenses on a first release from an
57
- unknown project. BSL here risks damaging the exact thing Phase 5
58
- exists to build.
59
- - The competing-hosted-clone risk BSL would guard against is
60
- theoretical today, not real: it requires someone to find this
61
- project among much bigger established names (LiteLLM, Portkey,
62
- OpenRouter), choose to build a hosted competitor on it instead of
63
- those, and out-compete our own hosted tier on the same code with
64
- none of our head start. Low-probability chain to defend against
65
- before there's any traction worth defending.
66
- - Real precedent (Elastic, MongoDB, Sentry) all started fully
67
- permissive and only moved to a restrictive license YEARS later,
68
- once a specific, material competitor was demonstrably siphoning
69
- revenue - never before there was traction. Code already released
70
- under MIT stays MIT forever, but nothing stops a FUTURE version
71
- from adopting different terms later if a real competing clone
72
- actually appears - so nothing is lost by not defending against a
73
- threat that doesn't exist yet.
74
- - MIT over Apache-2.0 specifically: LiteLLM, our closest named
75
- competitor, already ships MIT - matching it lowers the evaluation
76
- friction for the exact audience both projects are fishing from.
77
- - Revisit ONLY if a real competing hosted clone materializes later -
78
- that would apply to future versions/contributions, not retroactively.
79
- - **Done:** `LICENSE` file added (MIT, "MemoCode" as copyright holder
80
- as a placeholder - confirm the real legal entity name before step 15
81
- actually makes the repo public).
82
-
83
- ### 3. ✅ Secrets and credential audit — **done 2026-08-29, clean**
84
- - Grepped both the working tree AND the full commit history's diffs for
85
- API key patterns (`sk-...`, `AIza...`, `xai-...`) and email addresses
86
- — **no leaked keys found anywhere**, and the only email match was the
87
- git author metadata already handled by step 1's decision, not
88
- anything embedded in file content.
89
- - `.env.example` confirmed to hold only placeholder values
90
- (`sk-ant-api03-...` truncated, `your-random-internal-key`, etc.), no
91
- real credentials.
92
- - `MODEL_ROUTER_OWNER_EMAILS`-style references in the code/docs are the
93
- env var NAME only, never a real value.
94
- - Nothing to rotate. Re-run this same check once more right before
95
- step 15 actually makes the repo public, as a final gate — a clean
96
- result today doesn't exempt a later commit from the same scrutiny.
97
-
98
- ### 4. ✅ Strip MemoCode-specific coupling — **name decided 2026-08-29: cachegate**
99
- - **Checked directly**: grepped all `.js` code (not docs) for
100
- "memocode"/"MemoCode" — found only comments explaining context and
101
- one branded startup log line (`🚀 MemoCode Model Router listening...`
102
- in `server.js`), **zero functional coupling** — no hardcoded MemoCode
103
- URLs, no assumption that only a MemoCode caller exists. The "drop-in
104
- module" design claim holds up under inspection, not just assumption.
105
- - **Naming — a real open decision, CEO's call (taste/brand, unlike the
106
- license), not decided here.** Checked npm registry availability for
107
- three neutral candidates: `cachegate` ✅ available, `routecache` ✅
108
- available, `llm-routecache` ✅ available (`llm-relay` was tried first
109
- and is already taken by an unrelated, similarly-scoped package).
110
- - **Case for a neutral name** (not "MemoCode ___"): "MemoCode" is a
111
- mnemonic/note-taking app name with no connection to "LLM gateway" —
112
- a stranger evaluating self-hosted LLM routers on Hacker News or
113
- r/selfhosted would find a "MemoCode Model Router" confusing (why
114
- does a notes app also make infrastructure?), which works against
115
- Phase 5's own adoption/trust goal. The eventual Phase 6 standalone
116
- product will also want its own identity as an "OpenRouter/LiteLLM
117
- competitor" (`ROADMAP.md` section 6's own framing) — easier to
118
- establish that now than rename after people have already
119
- starred/installed something.
120
- - **Case for keeping MemoCode branding:** funnel value back to the
121
- parent product; zero rename work.
122
- - **Decided: `cachegate`.** Applied everywhere: `package.json`'s
123
- `name`, `package-lock.json` regenerated, `README.md`'s title, and
124
- the startup log line in `server.js` (was
125
- `🚀 MemoCode Model Router listening...`, now `🚀 cachegate
126
- listening...`). Confirmed clean afterward - grepped for any
127
- remaining "memocode"/"MemoCode" reference in code or README, none
128
- found. Full suite re-run after the rename: 96/96 passing.
129
-
130
- ### 5. ✅ Rewrite the README for a stranger, not a teammate — done 2026-08-29
131
- - Existing `README.md` was already thorough and honest (features,
132
- streaming caveats, dashboard limitations, all stated plainly) - most
133
- of it needed no change. What was missing for an external reader:
134
- - A "Why this instead of LiteLLM/Portkey/OpenRouter" section, pulling
135
- in the honest positioning already drafted in `ROADMAP.md` section 2
136
- (self-hosted-first, semantic cache with the LiteLLM caveat stated
137
- plainly, Node.js-native, embeddable, the real 20-45%/47-90% savings
138
- range instead of an inflated 86-95%).
139
- - A "What this is NOT" section stating the scope boundary up front
140
- (no hosted service, no login/billing/multi-tenancy, no 140-provider
141
- reach, no vector-indexed semantic cache yet) - added, per this
142
- doc's own step-14 principle, so nobody files an issue asking for
143
- something never in scope.
144
- - The quickstart assumed the MemoCode monorepo was already cloned
145
- (`cd 210_apps/001_model_router` as step one) - now gives both a
146
- standalone `git clone` path and an "embedded in your own app" path.
147
- - One paragraph named MemoCode's own internal file
148
- (`210_apps/000_backend/router-owner.mjs`) as the dashboard-gating
149
- example - generalized to describe the PATTERN (front the dashboard
150
- with your own app's login) without a path that won't exist in the
151
- extracted repo.
152
- - Title left as a placeholder pending step 4's naming decision.
153
-
154
- ---
155
-
156
- ## Group B — Make the code shippable outside this monorepo (steps 6-10)
157
-
158
- ### 6. 🟨 Package it as a real, standalone npm module — mostly done 2026-08-29
159
- - `package.json`: `name` → `cachegate`, `description` rewritten to be
160
- accurate standalone (no "for MemoCode" framing), `license: "MIT"`
161
- added (matches step 2 - npm shows this on the package page without
162
- needing to open `LICENSE`), `engines: {"node": ">=18.0.0"}` added.
163
- Honest caveat: `>=18.0.0` is a reasonable floor given the
164
- dependencies (Express 4, `redis` v4, `pg` v8 all support it), NOT a
165
- claim this has been cross-version tested — only run against Node 22
166
- in this sandbox.
167
- - **`bin` entry added**: `"cachegate": "./server.js"`, plus a
168
- `#!/usr/bin/env node` shebang line added to `server.js` (it had none)
169
- and the file's execute bit set - both required for `npx cachegate` to
170
- actually work once published (step 17). README's "Run it" section now
171
- documents this as the zero-clone path.
172
- - **Deferred on purpose, not forgotten:** the `0.1.0` → `1.0.0` semver
173
- bump. Bumping it now, before the repo is actually public (step 16),
174
- would sit oddly in the internal monorepo and communicate a stability
175
- claim nothing has tested yet. Bump this at step 16/17, right when
176
- it's actually true.
177
- - Dependencies unchanged and already appropriate for a standalone
178
- package - nothing in `package.json` assumed shared monorepo tooling
179
- to begin with, verified by a fresh `npm install` + full test run in
180
- an isolated worktree: 96/96 passing.
181
-
182
- ### 7. ✅ Standard OSS repo hygiene — done 2026-08-29
183
- - `CONTRIBUTING.md` — local setup, how to run tests (including the two
184
- real test-suite conventions a contributor needs to know: no test
185
- calls a live provider, and metrics-writing tests need to isolate
186
- their own state), and the scope boundary repeated up front (a PR
187
- adding login/billing/multi-tenancy gets closed regardless of
188
- quality — that's a separate product, not this engine).
189
- - `CODE_OF_CONDUCT.md` — Contributor Covenant v2.1, adopted verbatim
190
- (that's its actual intended use, not something to rewrite).
191
- - `SECURITY.md` — points to GitHub's private vulnerability reporting
192
- (Security tab → Report a vulnerability) rather than a fabricated
193
- contact email; states concretely what counts as a security issue
194
- for THIS project specifically (auth bypass, cross-deployment data
195
- leak, a resource-exhaustion path rate limiting doesn't cover) versus
196
- a regular bug. **Needs "Private vulnerability reporting" turned on**
197
- in the repo's Settings once step 15 creates it — flagged inline in
198
- the file itself as a maintainer note to remove once live.
199
- - `.github/ISSUE_TEMPLATE/bug_report.md` + `feature_request.md` (the
200
- feature template repeats the scope boundary up front too) +
201
- `config.yml` (points to Security Advisories instead of a public
202
- issue for vulnerabilities — has a placeholder `OWNER` in its URL,
203
- flagged inline, to fill in at step 15).
204
- - `.github/PULL_REQUEST_TEMPLATE.md` — a scope-boundary checkbox
205
- reviewers can actually check against, plus what testing was done.
206
-
207
- ### 8. ✅ CI for the public repo — done 2026-08-29
208
- - `.github/workflows/test.yml` added: runs on every push and PR
209
- (deliberately NOT copying this monorepo's own root
210
- `.github/workflows/e2e.yml`, which is `workflow_dispatch`-only — a
211
- public OSS repo needs the automatic trigger, that's the whole point
212
- of the trust signal this step exists for).
213
- - **Real finding, not assumed:** `npm test` needs more than just Node.
214
- `semanticCache.test.js` spawns its own throwaway `redis-server`
215
- process directly (the binary isn't on `ubuntu-latest` by default —
216
- added an install step); `metrics-postgres.test.js` needs a real
217
- reachable Postgres. The workflow adds a Postgres service container
218
- matching that test file's own default connection string exactly.
219
- - **Second real finding, caught by actually stopping Postgres and
220
- re-running the suite rather than trusting the code's own comment**:
221
- `metrics-postgres.test.js`'s header comment claimed it "fails loudly
222
- with ECONNREFUSED... rather than silently skipping" — checked
223
- directly, and that's not what the code does. It probes connectivity
224
- once and skips gracefully with a warning if Postgres isn't reachable,
225
- and **every test still reports as passing either way** (96/96,
226
- whether or not Postgres was running). Fixed the comment to describe
227
- the real behavior, and noted the real consequence: without this
228
- workflow's Postgres service, CI would report green while silently
229
- never exercising that file at all — the service container isn't a
230
- nice-to-have here, it's what makes the badge mean what it's supposed
231
- to mean.
232
- - Matrix across Node `18.x`/`20.x`/`22.x` — `package.json` declares
233
- `engines: {"node": ">=18.0.0"}`; running against all three actually
234
- backs that claim instead of leaving it asserted but untested.
235
- - **Verified for real, not just written**: ran the actual suite locally
236
- with both a live local Postgres (started, password/db created to
237
- match the test's default string) and the pre-installed `redis-server`
238
- binary present — 96/96 passing, ~11s (vs. ~1.3s when Postgres is
239
- down and that file's tests silently skip) — confirms the difference
240
- is real exercise, not a no-op.
241
- - Added a CI-status badge to `README.md` (next to the license badge) —
242
- has the same `OWNER` placeholder as `.github/ISSUE_TEMPLATE/config.yml`,
243
- flagged inline, to fill in at step 15.
244
-
245
- ### 9. 🟨 Docker image — improved and documented; build NOT verified here, real limitation
246
- - **Honest limitation, not glossed over**: this sandbox's network
247
- egress policy blocks Docker Hub entirely (confirmed - `docker build`
248
- fails pulling `node:20-slim` with a 403 policy denial from
249
- `production.cloudfront.docker.com`, and the proxy status explicitly
250
- lists it as a policy denial, not a transient failure worth retrying).
251
- **The actual `docker build` + `docker run` + hit-`/health` verification
252
- this step calls for could NOT be done from here** - that needs
253
- whoever has real internet access (the CEO's local machine / DeepSeek's
254
- lane) to run it for real before step 16 ships. Said plainly rather
255
- than assumed to be fine.
256
- - What WAS done, based on direct static review of the existing
257
- `Dockerfile` (not a guess):
258
- - **Added `.dockerignore`** - none existed. Without it, `COPY . .`
259
- would copy a HOST-machine `node_modules` (if one happens to exist
260
- at build time) straight over the image's own freshly-`npm ci`'d
261
- one - a classic, easy-to-hit Docker footgun, especially likely
262
- once this is a real repo other contributors build locally. Also
263
- excludes `.env`, `.git`, `test/`, `.github/` - none of it belongs
264
- in a runtime image.
265
- - **Added a non-root `USER node`** - the official Node image already
266
- ships this user (uid 1000); the Dockerfile just never used it,
267
- running as root by default. Standard hardening for a public image.
268
- - **Added a real `HEALTHCHECK`** - calls the existing `GET /health`
269
- endpoint via Node's own `http` module (the slim base image has no
270
- curl/wget), so `docker ps` actually reports `healthy`/`unhealthy`
271
- instead of only "running."
272
- - **Publish target decided: GitHub Container Registry (GHCR)**, not
273
- Docker Hub - ties to the same repo/`GITHUB_TOKEN` already used for
274
- CI with no separate account or credential to manage, and the image
275
- inherits the repo's own visibility settings. Docker Hub has better
276
- discoverability for someone specifically browsing Docker Hub's own
277
- search, but the realistic audience here is someone already reading
278
- this GitHub repo's README, not browsing Docker Hub cold - GHCR fits
279
- that path better. Not closed off permanently: worth adding a Docker
280
- Hub mirror later if search-driven discovery turns out to matter.
281
- - README's "Run it" section gained a Docker subsection: `docker build`
282
- + `docker run` with `--env-file`, a note on what `HEALTHCHECK`
283
- reports, and an explicit statement that Redis is NOT bundled in the
284
- image (point `REDIS_URL` at an external instance; missing it disables
285
- caching cleanly rather than failing to start).
286
- - **Remaining, real, not done here:** the actual build-and-run
287
- verification. Flagging this explicitly as an open item for whoever
288
- picks this up next with real network access - do not treat this
289
- step as fully closed until that verification actually happens.
290
-
291
- ### 10. ✅ Config and environment documentation — done 2026-08-29
292
- - **Checked directly, not assumed**: grepped every `.js` file for
293
- `process.env.` to build the actual, complete list of env vars the
294
- code reads, then diffed it against `.env.example`.
295
- - **Real finding**: `.env.example` (and the README's own `.env`
296
- snippet) documented `ANTHROPIC_MODEL=claude-sonnet-4-5-20250929` as a
297
- config value - **grepped for it and it's read NOWHERE in the code**.
298
- The model is named per-request in the API call's own `model` field,
299
- not configured via env at all. A new user setting this would see it
300
- silently do nothing. Removed from both files, replaced with an
301
- explicit note that the model is per-request, not env-configured.
302
- - **Missing from `.env.example` entirely, now added**:
303
- `ALLOW_INSECURE_LOCAL_DEV`, `EMBEDDING_MODEL` (which OpenAI embedding
304
- model the semantic cache uses), `METRICS_LOG_PATH` (JSONL storage
305
- location), `MEMOCODE_ROUTER_DATABASE_URL` (an alternate to
306
- `DATABASE_URL` that takes priority when set - useful when embedding
307
- this inside an app that already has its own `DATABASE_URL`), and
308
- `ROUTER_TIERS_JSON` (override the default routing tiers).
309
- - **Genuinely tested the quickstart, not just written it**: ran
310
- `npm install`, copied `.env.example` to `.env`, filled in a test
311
- internal key and a fake Anthropic key, ran `npm start` - boots clean,
312
- logs "🚀 cachegate listening on port 4000." Hit `GET /health` (real
313
- response, correctly reflecting config) and `POST
314
- /v1/chat/completions` (reached Anthropic for real, got back a genuine
315
- 401 "API key is invalid" - proving the full pipeline works end to
316
- end; only the placeholder key is fake, exactly what a real follower
317
- would see before adding their own). Also confirmed `.env` and the
318
- metrics `data/` directory stay gitignored, as claimed.
319
- - README's `.env` snippet now points to `.env.example` for the full
320
- option list instead of duplicating a partial, now-corrected copy of it.
321
-
322
- ---
323
-
324
- ## Group C — Keep embedded and public in sync, and safe (steps 11-14)
325
-
326
- ### 11. ✅ Decide the concrete sync mechanism — **decided 2026-08-29**
327
- - `ROADMAP.md` section 6 states the *principle* ("wrap it, don't fork
328
- it - same core logic, two thin deployment shells") but never names
329
- an actual mechanism. Two directions were on the table:
330
- - (a) the public repo becomes the source of truth, MemoCode vendors
331
- it in via a script/subtree pull.
332
- - (b) this monorepo directory stays the source of truth, a script
333
- pushes/mirrors it out to the public repo on release.
334
- - **Decided: (b) — this monorepo directory stays the source of truth.**
335
- Reasoning:
336
- - It matches reality, not a fresh ideal: every real day of
337
- development on this router - the original 20-step build, the
338
- failover feature, all ten OSS-prep steps so far - happened inside
339
- this monorepo, through its own task-branch/PR/gate workflow
340
- (`AGENTS.md`). Moving day-to-day development to the public repo
341
- would mean either running two parallel workflows or abandoning the
342
- one that's actually proven itself this week - neither is worth it
343
- to satisfy a "public repo is canonical" ideal nobody needs yet.
344
- - Option (a) would put every internal-only concern (this team's own
345
- coordination-board conventions, anything MemoCode-embedding-
346
- specific) through public PR review before it could land internally
347
- - backwards for a team that needs to move fast on its own repo.
348
- - This is also the well-established pattern for exactly this
349
- situation, not a novel one: several real companies develop OSS
350
- projects inside a private monorepo and mirror them out
351
- (contributions flow back in via manual review + reapplication, not
352
- automatic two-way merge) rather than developing directly in the
353
- public repo.
354
- - **The real gap this creates, named rather than ignored:** once
355
- external contributors exist (post step 19's launch), their PRs land
356
- on the PUBLIC repo first - there is no automatic path back into this
357
- monorepo. Concrete resolution: an accepted external PR gets manually
358
- reapplied to `210_apps/001_model_router/` as its own normal task
359
- branch here (same `AGENTS.md` workflow as any other change - it goes
360
- through this project's own gate even though it originated externally,
361
- on purpose, not as an oversight: an external diff doesn't get to skip
362
- this codebase's own verification standard just because GitHub already
363
- approved it). This creates a real but bounded divergence window
364
- between the two repos - acceptable, disclosed, and the discipline is
365
- keeping that window short (reapply promptly), not eliminating it.
366
- - **The actual repeatable command** (implemented for real in step 12,
367
- sketched here so the decision isn't just prose): a single script,
368
- `sync-oss-release.sh`, invoked as `./sync-oss-release.sh <path-to-public-repo-checkout>`,
369
- that does, in order: (1) copy this directory's tracked files into the
370
- target checkout, excluding anything `.gitignore`d; (2) run the same
371
- secrets grep step 3 already established, failing loudly rather than
372
- publishing on a hit; (3) bump `package.json`'s version per step 6's
373
- deferred semver plan; (4) commit and leave the push to a human/CI
374
- step, never auto-pushed. One command, not a remembered sequence of
375
- manual copy-paste steps.
376
-
377
- ### 12. ✅ Build the actual sync script/workflow — done and genuinely tested 2026-08-29
378
- - `sync-oss-release.sh` built per step 11's decision: mirrors this
379
- directory's git-tracked files into a target checkout (removing
380
- everything else there first, except its own `.git/`), scans for
381
- secrets before touching anything, optionally bumps `package.json`'s
382
- version (`--version X.Y.Z`), and commits in the TARGET repo without
383
- pushing. Refuses to run against a path that isn't a git repository,
384
- specifically so pointing it at the wrong path can't wipe something
385
- unrelated.
386
- - **Actually tested end-to-end, not just written** - a real target git
387
- repo was created in scratch space and run through every real
388
- scenario: a clean sync with `--version 0.9.0` (files copied
389
- correctly, version bumped, a stale target-only file correctly
390
- removed, real commit created); a resync with no version flag
391
- (correctly left the version as whatever's currently in this
392
- directory); an identical third run (correctly reported "nothing
393
- changed," no empty commit); the non-git-directory safety guard
394
- (correctly refused); and the secrets-scan abort path (a real fake
395
- key was planted in `README.md` and the script was run against it).
396
- - **Real bug found by that last test, not assumed to work**: the
397
- secrets-scan regex FAILED to catch the planted key on the first
398
- attempt. Root cause: the character class `[a-zA-Z0-9]{20,}` used
399
- after `sk-`/`xai-` doesn't allow hyphens - and a real Anthropic key
400
- looks like `sk-ant-api03-<random>`, where the hyphens immediately
401
- after `sk-` broke the match after only 3 characters. Fixed to
402
- `[a-zA-Z0-9_-]{20,}` (matching `AIza`'s pattern, which already had
403
- this right), re-tested, and confirmed it now correctly aborts.
404
- - **This is the exact same pattern used in steps 1, 3, and 8's manual
405
- audits** - meaning those "clean" results were reached with the same
406
- blind spot. Re-ran the full audit (working tree + entire commit
407
- history) with the CORRECTED pattern before concluding anything:
408
- still genuinely clean. The earlier conclusion holds, but only because
409
- it was actually re-verified just now, not assumed to still be valid
410
- once the flaw in the method was found.
411
- - No separate "checklist for what needs re-verifying after a sync" was
412
- needed beyond what the script itself already does (secrets scan is
413
- automatic on every run) - a maintainer still reviews the target
414
- repo's diff before pushing (the script's own final message says so),
415
- which covers README drift and anything else worth a human glance.
416
-
417
- ### 13. ✅ Security review pass, specifically for "now public" risk — done 2026-08-29
418
- - **The one real, serious finding, confirmed live not assumed**: sent
419
- an actual oversized POST body during this review and got back a raw
420
- HTML page containing a FULL STACK TRACE with this server's own
421
- absolute filesystem paths - Express's own default error handler,
422
- reached because nothing here ever caught an error raised before a
423
- route's own try/catch (the concrete trigger: `express.json()`
424
- rejecting a too-large body). Fixed with a catch-all JSON error
425
- handler registered last, re-tested with the identical request:
426
- `{"error":"Request body too large."}`, 413, no stack trace. This is
427
- exactly the class of thing "worked fine as an internal tool nobody
428
- attacked" and would not have survived being public.
429
- - **Second real finding**: `express.json({ limit: '50mb' })` was
430
- applied GLOBALLY and BEFORE the `/v1` auth check - an anonymous
431
- caller could force up to 50MB of JSON parsing per request before
432
- ever being rejected with 401. Fixed three ways: scoped to `/v1` only
433
- (the sole route that reads a body - checked directly, every other
434
- route is a body-less GET); moved after `requireInternalKey` and the
435
- rate limiter in the middleware chain, so a request is rejected by a
436
- cheap check before any parsing happens; limit dropped from 50mb to a
437
- configurable 2mb default (`JSON_BODY_LIMIT`) - checked that this
438
- router has no image/multimodal support, so even a very long text
439
- conversation fits comfortably under that. Live-verified: an
440
- unauthenticated 3MB POST now gets rejected in 16ms with a 401,
441
- vs. previously being fully parsed first.
442
- - **Third finding, lower severity**: `/stats` and `/dashboard/data`
443
- had `requireInternalKey` but no rate limit at all, unlike `/v1`.
444
- Added a separate, more generous `readEndpointLimiter`
445
- (`READ_RATE_LIMIT_MAX`, default 120/window) - lower stakes than `/v1`
446
- (no provider spend on the line) but still real server work a
447
- leaked/shared key shouldn't be able to hammer unbounded.
448
- - Free hardening added while in here: `app.disable('x-powered-by')` -
449
- no reason to hand a public-facing service's framework fingerprint to
450
- every caller.
451
- - **Confirmed clean, no fix needed** (checked directly, not assumed):
452
- no `.stack` or raw error object is ever sent to a client anywhere in
453
- the existing route handlers - every error path already used
454
- `err.message` only. `ALLOW_INSECURE_LOCAL_DEV` already refuses to
455
- start without either a real key or this explicit opt-in, AND already
456
- prints a loud runtime `console.warn` when active - no doc-only
457
- warning to strengthen, the code itself already enforces it. No CORS
458
- middleware exists, which is the CORRECT default here (an admin/data
459
- API with bearer-token auth has no reason to allow arbitrary
460
- cross-origin browser access) - not a gap to fix.
461
- - Also clarified in `server.js`'s own rate-limiter comment: the
462
- "shared ceiling across all callers" caveat is specific to a
463
- single-caller EMBEDDED deployment - a standalone self-hoster with
464
- distinct per-caller IPs gets real per-caller separation from the
465
- same default, not the same limitation.
466
- - All fixes verified live (server started, real HTTP requests sent and
467
- checked) in addition to the automated suite - 96/96 passing
468
- throughout, confirming none of this changed any existing behavior
469
- other than the three things it was meant to fix.
470
-
471
- ### 14. ✅ Explicit non-goals, written down where a stranger will read them — done 2026-08-29
472
- - Most of the literal content already existed from step 5 (the README's
473
- "What this is NOT" section) and step 7 (`CONTRIBUTING.md`,
474
- `.github/ISSUE_TEMPLATE/feature_request.md`) - checked all three
475
- directly rather than assuming step 14 was redundant with them.
476
- - **Real gap found in two of the three, not assumed fine**: both the
477
- README's closing line and `CONTRIBUTING.md`'s scope section said, in
478
- effect, "if you want a hosted service, fork it" - which actively
479
- *invites* the exact outcome this section (and step 2's license
480
- reasoning) exists to discourage. Technically true under MIT, but
481
- saying it right next to "this is not a hosted service" undermines
482
- the whole point of drawing the boundary. Fixed both: still honest
483
- that MIT permits it (never claimed otherwise), but reframed as "not
484
- a licensing restriction, a project-scope one" - this repo specifically
485
- isn't going to grow into a hosted competitor to its own paid product,
486
- full stop, rather than a soft invitation to go build one.
487
- `.github/ISSUE_TEMPLATE/feature_request.md` already had this right -
488
- checked, no change needed there.
489
- - README's closing line also now explicitly separates the two real,
490
- legitimate PR-worthy gaps (140-provider reach, vector-indexed
491
- semantic cache) from the one boundary that isn't a gap at all,
492
- pointing to `CONTRIBUTING.md` before someone opens a PR for it.
493
-
494
- ---
495
-
496
- ## Group D — Ship it (steps 15-18)
497
-
498
- ### 15. Create the real public GitHub repository
499
- - Public repo under whichever account/org is decided, real description,
500
- topics/tags for discoverability (`llm-gateway`, `semantic-cache`,
501
- `self-hosted`, `openai-compatible`, `llm-proxy`).
502
- - Branch protection on the default branch (require CI green + review
503
- before merge - the repo's own credibility depends on this from day
504
- one, unlike an internal task branch nobody outside the team sees).
505
-
506
- ### 16. Push the extracted, cleaned codebase
507
- - First real push/release using whatever step 1 decided (curated
508
- history or fresh start).
509
- - Tag it `v1.0.0` (or whatever step 6 decided) - this is the actual
510
- "it's out" moment everything before this step has been preparing for.
511
-
512
- ### 17. Publish the npm package
513
- - `npm publish` under the name step 4 chose, confirming it installs
514
- cleanly in a throwaway empty directory (not just "works from this
515
- monorepo").
516
-
517
- ### 18. Publish the Docker image
518
- - Push to the registry step 9 chose, confirm `docker pull` + `docker
519
- run` works cold, from a machine that has never seen this project's
520
- source at all.
521
-
522
- ---
523
-
524
- ## Group E Tell people, then keep it alive (steps 19-20)
525
-
526
- ### 19. Announce
527
- - Per `ROADMAP.md`'s existing plan: Hacker News (Show HN post),
528
- r/LocalLLaMA, r/selfhosted, Dev.to - each gets its own framing (Show
529
- HN wants the honest savings number and the self-hosted angle;
530
- r/selfhosted wants the Docker one-liner front and center; Dev.to
531
- supports a longer "why we built this" writeup).
532
- - Write the honest version of "why this exists" - the real story (an
533
- internal tool for MemoCode that turned out to be worth its own
534
- release), not a manufactured origin story.
535
-
536
- ### 20. Post-launch triage plan
537
- - Decide who watches the new public repo's issues/PRs day-to-day (the
538
- same "one agent holds the standing watch" model already used for
539
- this team's own PRs, per `AGENTS.md`) - a public repo with no
540
- visible maintainer activity in the first weeks reads as abandoned.
541
- - Define what "Phase 5 done" actually means so it doesn't stay
542
- open-ended forever: e.g., shipped + stable for some real window with
543
- no critical issues, or a first external contribution merged -
544
- pick a concrete bar, not a vibe.
545
- - This is also the moment Phase 6 (the standalone/hosted, NOT
546
- open-sourced) becomes buildable for real - it depends on this
547
- engine being proven stable under outside use, not just internal
548
- dogfooding.
549
-
550
- ---
551
-
552
- *Written 2026-08-29. Cross-referenced from `ROADMAP.md`'s Phase 5. Update
553
- status markers inline as each step closes - this document IS the tracker,
554
- not a one-time plan to be superseded by a better one later.*
1
+ # Open-Source Release Roadmap — 20 Steps
2
+
3
+ **What this is:** the concrete execution plan for `ROADMAP.md`'s Phase 5
4
+ ("open-source release"). That phase has existed as a single line —
5
+ "Public GitHub repo → npm package → Docker image → announce" — since
6
+ 2026-08-23; this is what actually has to happen to make each of those
7
+ four words true, broken into 20 numbered steps with the sub-work under
8
+ each, so progress is trackable the same way the embedded build itself
9
+ was.
10
+
11
+ **Scope, precisely** (see `ROADMAP.md` section 6 and the 2026-08-29
12
+ open-source-scope discussion in the Decision Journal): **only the
13
+ embedded engine gets open-sourced** — routing, caching, metrics,
14
+ providers, the dashboard. Login, payment, multi-tenancy, and anything
15
+ else that makes the **standalone** hosted product ($ Phase 6) are never
16
+ part of this repo, this package, or this Docker image. If a step below
17
+ ever seems to call for adding one of those things, that's a sign the
18
+ step has drifted out of scope, not a sign the scope needs expanding.
19
+
20
+ **Status legend:** ⬜ not started · 🟨 in progress · ✅ done · ⏸ blocked/waiting
21
+
22
+ **Lesson applied from the embedded build's own history (see
23
+ `ROADMAP.md`'s "Note on an older numbering"):** that 20-step list was
24
+ never written down as one document and step 18's content couldn't be
25
+ reconstructed months later. This one is written down as one document,
26
+ here, on purpose — update status inline as steps close, don't let this
27
+ become a second undocumented list.
28
+
29
+ ---
30
+
31
+ ## Group A — Decide before building anything (steps 1-5)
32
+
33
+ ### 1. ✅ Decide the extraction method — **decided 2026-08-29: fresh, curated history**
34
+ - **Checked directly** (`git log --format="%an <%ae>" --all -- 210_apps/001_model_router/`):
35
+ the real commit history contains a real person's full name and
36
+ personal Gmail address in the author field, across 12+ commits — not
37
+ a hypothetical risk, a confirmed one. That alone settles the
38
+ decision: **do not preserve the original git history** when
39
+ extracting. A `git subtree split`/`filter-repo` approach would carry
40
+ that identity into a permanently public, unrescindable record the
41
+ moment the repo goes live.
42
+ - No leaked API keys or other secrets found in the history's actual
43
+ diffs (checked separately — see step 3) — the ONE real exposure is
44
+ the author metadata itself, which a fresh/squashed history sidesteps
45
+ entirely along with everything else.
46
+ - **Decision: fresh, curated history** — squash to a clean set of
47
+ milestone commits authored under a project/org identity, not
48
+ personal ones, when step 16 actually pushes this public.
49
+
50
+ ### 2. ✅ Pick a license — **decided 2026-08-29: MIT**
51
+ - **Decision: MIT**, not a BSL-style source-available license, for this
52
+ first release. Reasoning, weighed against the alternative:
53
+ - Phase 5's whole stated purpose is trust/adoption ("top-of-funnel"
54
+ per section 3's own monetization table) - the two launch
55
+ communities already planned for (Hacker News, r/selfhosted) react
56
+ badly to source-available licenses on a first release from an
57
+ unknown project. BSL here risks damaging the exact thing Phase 5
58
+ exists to build.
59
+ - The competing-hosted-clone risk BSL would guard against is
60
+ theoretical today, not real: it requires someone to find this
61
+ project among much bigger established names (LiteLLM, Portkey,
62
+ OpenRouter), choose to build a hosted competitor on it instead of
63
+ those, and out-compete our own hosted tier on the same code with
64
+ none of our head start. Low-probability chain to defend against
65
+ before there's any traction worth defending.
66
+ - Real precedent (Elastic, MongoDB, Sentry) all started fully
67
+ permissive and only moved to a restrictive license YEARS later,
68
+ once a specific, material competitor was demonstrably siphoning
69
+ revenue - never before there was traction. Code already released
70
+ under MIT stays MIT forever, but nothing stops a FUTURE version
71
+ from adopting different terms later if a real competing clone
72
+ actually appears - so nothing is lost by not defending against a
73
+ threat that doesn't exist yet.
74
+ - MIT over Apache-2.0 specifically: LiteLLM, our closest named
75
+ competitor, already ships MIT - matching it lowers the evaluation
76
+ friction for the exact audience both projects are fishing from.
77
+ - Revisit ONLY if a real competing hosted clone materializes later -
78
+ that would apply to future versions/contributions, not retroactively.
79
+ - **Done:** `LICENSE` file added (MIT, "MemoCode" as copyright holder
80
+ as a placeholder - confirm the real legal entity name before step 15
81
+ actually makes the repo public).
82
+
83
+ ### 3. ✅ Secrets and credential audit — **done 2026-08-29, clean**
84
+ - Grepped both the working tree AND the full commit history's diffs for
85
+ API key patterns (`sk-...`, `AIza...`, `xai-...`) and email addresses
86
+ — **no leaked keys found anywhere**, and the only email match was the
87
+ git author metadata already handled by step 1's decision, not
88
+ anything embedded in file content.
89
+ - `.env.example` confirmed to hold only placeholder values
90
+ (`sk-ant-api03-...` truncated, `your-random-internal-key`, etc.), no
91
+ real credentials.
92
+ - `MODEL_ROUTER_OWNER_EMAILS`-style references in the code/docs are the
93
+ env var NAME only, never a real value.
94
+ - Nothing to rotate. Re-run this same check once more right before
95
+ step 15 actually makes the repo public, as a final gate — a clean
96
+ result today doesn't exempt a later commit from the same scrutiny.
97
+
98
+ ### 4. ✅ Strip MemoCode-specific coupling — **name decided 2026-08-29: cachegate**
99
+ - **Checked directly**: grepped all `.js` code (not docs) for
100
+ "memocode"/"MemoCode" — found only comments explaining context and
101
+ one branded startup log line (`🚀 MemoCode Model Router listening...`
102
+ in `server.js`), **zero functional coupling** — no hardcoded MemoCode
103
+ URLs, no assumption that only a MemoCode caller exists. The "drop-in
104
+ module" design claim holds up under inspection, not just assumption.
105
+ - **Naming — a real open decision, CEO's call (taste/brand, unlike the
106
+ license), not decided here.** Checked npm registry availability for
107
+ three neutral candidates: `cachegate` ✅ available, `routecache` ✅
108
+ available, `llm-routecache` ✅ available (`llm-relay` was tried first
109
+ and is already taken by an unrelated, similarly-scoped package).
110
+ - **Case for a neutral name** (not "MemoCode ___"): "MemoCode" is a
111
+ mnemonic/note-taking app name with no connection to "LLM gateway" —
112
+ a stranger evaluating self-hosted LLM routers on Hacker News or
113
+ r/selfhosted would find a "MemoCode Model Router" confusing (why
114
+ does a notes app also make infrastructure?), which works against
115
+ Phase 5's own adoption/trust goal. The eventual Phase 6 standalone
116
+ product will also want its own identity as an "OpenRouter/LiteLLM
117
+ competitor" (`ROADMAP.md` section 6's own framing) — easier to
118
+ establish that now than rename after people have already
119
+ starred/installed something.
120
+ - **Case for keeping MemoCode branding:** funnel value back to the
121
+ parent product; zero rename work.
122
+ - **Decided: `cachegate`.** Applied everywhere: `package.json`'s
123
+ `name`, `package-lock.json` regenerated, `README.md`'s title, and
124
+ the startup log line in `server.js` (was
125
+ `🚀 MemoCode Model Router listening...`, now `🚀 cachegate
126
+ listening...`). Confirmed clean afterward - grepped for any
127
+ remaining "memocode"/"MemoCode" reference in code or README, none
128
+ found. Full suite re-run after the rename: 96/96 passing.
129
+
130
+ ### 5. ✅ Rewrite the README for a stranger, not a teammate — done 2026-08-29
131
+ - Existing `README.md` was already thorough and honest (features,
132
+ streaming caveats, dashboard limitations, all stated plainly) - most
133
+ of it needed no change. What was missing for an external reader:
134
+ - A "Why this instead of LiteLLM/Portkey/OpenRouter" section, pulling
135
+ in the honest positioning already drafted in `ROADMAP.md` section 2
136
+ (self-hosted-first, semantic cache with the LiteLLM caveat stated
137
+ plainly, Node.js-native, embeddable, the real 20-45%/47-90% savings
138
+ range instead of an inflated 86-95%).
139
+ - A "What this is NOT" section stating the scope boundary up front
140
+ (no hosted service, no login/billing/multi-tenancy, no 140-provider
141
+ reach, no vector-indexed semantic cache yet) - added, per this
142
+ doc's own step-14 principle, so nobody files an issue asking for
143
+ something never in scope.
144
+ - The quickstart assumed the MemoCode monorepo was already cloned
145
+ (`cd 210_apps/001_model_router` as step one) - now gives both a
146
+ standalone `git clone` path and an "embedded in your own app" path.
147
+ - One paragraph named MemoCode's own internal file
148
+ (`210_apps/000_backend/router-owner.mjs`) as the dashboard-gating
149
+ example - generalized to describe the PATTERN (front the dashboard
150
+ with your own app's login) without a path that won't exist in the
151
+ extracted repo.
152
+ - Title left as a placeholder pending step 4's naming decision.
153
+
154
+ ---
155
+
156
+ ## Group B — Make the code shippable outside this monorepo (steps 6-10)
157
+
158
+ ### 6. 🟨 Package it as a real, standalone npm module — mostly done 2026-08-29
159
+ - `package.json`: `name` → `cachegate`, `description` rewritten to be
160
+ accurate standalone (no "for MemoCode" framing), `license: "MIT"`
161
+ added (matches step 2 - npm shows this on the package page without
162
+ needing to open `LICENSE`), `engines: {"node": ">=18.0.0"}` added.
163
+ Honest caveat: `>=18.0.0` is a reasonable floor given the
164
+ dependencies (Express 4, `redis` v4, `pg` v8 all support it), NOT a
165
+ claim this has been cross-version tested — only run against Node 22
166
+ in this sandbox.
167
+ - **`bin` entry added**: `"cachegate": "./server.js"`, plus a
168
+ `#!/usr/bin/env node` shebang line added to `server.js` (it had none)
169
+ and the file's execute bit set - both required for `npx cachegate` to
170
+ actually work once published (step 17). README's "Run it" section now
171
+ documents this as the zero-clone path.
172
+ - **Deferred on purpose, not forgotten:** the `0.1.0` → `1.0.0` semver
173
+ bump. Bumping it now, before the repo is actually public (step 16),
174
+ would sit oddly in the internal monorepo and communicate a stability
175
+ claim nothing has tested yet. Bump this at step 16/17, right when
176
+ it's actually true.
177
+ - Dependencies unchanged and already appropriate for a standalone
178
+ package - nothing in `package.json` assumed shared monorepo tooling
179
+ to begin with, verified by a fresh `npm install` + full test run in
180
+ an isolated worktree: 96/96 passing.
181
+
182
+ ### 7. ✅ Standard OSS repo hygiene — done 2026-08-29
183
+ - `CONTRIBUTING.md` — local setup, how to run tests (including the two
184
+ real test-suite conventions a contributor needs to know: no test
185
+ calls a live provider, and metrics-writing tests need to isolate
186
+ their own state), and the scope boundary repeated up front (a PR
187
+ adding login/billing/multi-tenancy gets closed regardless of
188
+ quality — that's a separate product, not this engine).
189
+ - `CODE_OF_CONDUCT.md` — Contributor Covenant v2.1, adopted verbatim
190
+ (that's its actual intended use, not something to rewrite).
191
+ - `SECURITY.md` — points to GitHub's private vulnerability reporting
192
+ (Security tab → Report a vulnerability) rather than a fabricated
193
+ contact email; states concretely what counts as a security issue
194
+ for THIS project specifically (auth bypass, cross-deployment data
195
+ leak, a resource-exhaustion path rate limiting doesn't cover) versus
196
+ a regular bug. **Needs "Private vulnerability reporting" turned on**
197
+ in the repo's Settings once step 15 creates it — flagged inline in
198
+ the file itself as a maintainer note to remove once live.
199
+ - `.github/ISSUE_TEMPLATE/bug_report.md` + `feature_request.md` (the
200
+ feature template repeats the scope boundary up front too) +
201
+ `config.yml` (points to Security Advisories instead of a public
202
+ issue for vulnerabilities — has a placeholder `OWNER` in its URL,
203
+ flagged inline, to fill in at step 15).
204
+ - `.github/PULL_REQUEST_TEMPLATE.md` — a scope-boundary checkbox
205
+ reviewers can actually check against, plus what testing was done.
206
+
207
+ ### 8. ✅ CI for the public repo — done 2026-08-29
208
+ - `.github/workflows/test.yml` added: runs on every push and PR
209
+ (deliberately NOT copying this monorepo's own root
210
+ `.github/workflows/e2e.yml`, which is `workflow_dispatch`-only — a
211
+ public OSS repo needs the automatic trigger, that's the whole point
212
+ of the trust signal this step exists for).
213
+ - **Real finding, not assumed:** `npm test` needs more than just Node.
214
+ `semanticCache.test.js` spawns its own throwaway `redis-server`
215
+ process directly (the binary isn't on `ubuntu-latest` by default —
216
+ added an install step); `metrics-postgres.test.js` needs a real
217
+ reachable Postgres. The workflow adds a Postgres service container
218
+ matching that test file's own default connection string exactly.
219
+ - **Second real finding, caught by actually stopping Postgres and
220
+ re-running the suite rather than trusting the code's own comment**:
221
+ `metrics-postgres.test.js`'s header comment claimed it "fails loudly
222
+ with ECONNREFUSED... rather than silently skipping" — checked
223
+ directly, and that's not what the code does. It probes connectivity
224
+ once and skips gracefully with a warning if Postgres isn't reachable,
225
+ and **every test still reports as passing either way** (96/96,
226
+ whether or not Postgres was running). Fixed the comment to describe
227
+ the real behavior, and noted the real consequence: without this
228
+ workflow's Postgres service, CI would report green while silently
229
+ never exercising that file at all — the service container isn't a
230
+ nice-to-have here, it's what makes the badge mean what it's supposed
231
+ to mean.
232
+ - Matrix across Node `18.x`/`20.x`/`22.x` — `package.json` declares
233
+ `engines: {"node": ">=18.0.0"}`; running against all three actually
234
+ backs that claim instead of leaving it asserted but untested.
235
+ - **Verified for real, not just written**: ran the actual suite locally
236
+ with both a live local Postgres (started, password/db created to
237
+ match the test's default string) and the pre-installed `redis-server`
238
+ binary present — 96/96 passing, ~11s (vs. ~1.3s when Postgres is
239
+ down and that file's tests silently skip) — confirms the difference
240
+ is real exercise, not a no-op.
241
+ - Added a CI-status badge to `README.md` (next to the license badge) —
242
+ has the same `OWNER` placeholder as `.github/ISSUE_TEMPLATE/config.yml`,
243
+ flagged inline, to fill in at step 15.
244
+
245
+ ### 9. 🟨 Docker image — improved and documented; build NOT verified here, real limitation
246
+ - **Honest limitation, not glossed over**: this sandbox's network
247
+ egress policy blocks Docker Hub entirely (confirmed - `docker build`
248
+ fails pulling `node:20-slim` with a 403 policy denial from
249
+ `production.cloudfront.docker.com`, and the proxy status explicitly
250
+ lists it as a policy denial, not a transient failure worth retrying).
251
+ **The actual `docker build` + `docker run` + hit-`/health` verification
252
+ this step calls for could NOT be done from here** - that needs
253
+ whoever has real internet access (the CEO's local machine / DeepSeek's
254
+ lane) to run it for real before step 16 ships. Said plainly rather
255
+ than assumed to be fine.
256
+ - What WAS done, based on direct static review of the existing
257
+ `Dockerfile` (not a guess):
258
+ - **Added `.dockerignore`** - none existed. Without it, `COPY . .`
259
+ would copy a HOST-machine `node_modules` (if one happens to exist
260
+ at build time) straight over the image's own freshly-`npm ci`'d
261
+ one - a classic, easy-to-hit Docker footgun, especially likely
262
+ once this is a real repo other contributors build locally. Also
263
+ excludes `.env`, `.git`, `test/`, `.github/` - none of it belongs
264
+ in a runtime image.
265
+ - **Added a non-root `USER node`** - the official Node image already
266
+ ships this user (uid 1000); the Dockerfile just never used it,
267
+ running as root by default. Standard hardening for a public image.
268
+ - **Added a real `HEALTHCHECK`** - calls the existing `GET /health`
269
+ endpoint via Node's own `http` module (the slim base image has no
270
+ curl/wget), so `docker ps` actually reports `healthy`/`unhealthy`
271
+ instead of only "running."
272
+ - **Publish target decided: GitHub Container Registry (GHCR)**, not
273
+ Docker Hub - ties to the same repo/`GITHUB_TOKEN` already used for
274
+ CI with no separate account or credential to manage, and the image
275
+ inherits the repo's own visibility settings. Docker Hub has better
276
+ discoverability for someone specifically browsing Docker Hub's own
277
+ search, but the realistic audience here is someone already reading
278
+ this GitHub repo's README, not browsing Docker Hub cold - GHCR fits
279
+ that path better. Not closed off permanently: worth adding a Docker
280
+ Hub mirror later if search-driven discovery turns out to matter.
281
+ - README's "Run it" section gained a Docker subsection: `docker build`
282
+ + `docker run` with `--env-file`, a note on what `HEALTHCHECK`
283
+ reports, and an explicit statement that Redis is NOT bundled in the
284
+ image (point `REDIS_URL` at an external instance; missing it disables
285
+ caching cleanly rather than failing to start).
286
+ - **Remaining, real, not done here:** the actual build-and-run
287
+ verification. Flagging this explicitly as an open item for whoever
288
+ picks this up next with real network access - do not treat this
289
+ step as fully closed until that verification actually happens.
290
+
291
+ ### 10. ✅ Config and environment documentation — done 2026-08-29
292
+ - **Checked directly, not assumed**: grepped every `.js` file for
293
+ `process.env.` to build the actual, complete list of env vars the
294
+ code reads, then diffed it against `.env.example`.
295
+ - **Real finding**: `.env.example` (and the README's own `.env`
296
+ snippet) documented `ANTHROPIC_MODEL=claude-sonnet-4-5-20250929` as a
297
+ config value - **grepped for it and it's read NOWHERE in the code**.
298
+ The model is named per-request in the API call's own `model` field,
299
+ not configured via env at all. A new user setting this would see it
300
+ silently do nothing. Removed from both files, replaced with an
301
+ explicit note that the model is per-request, not env-configured.
302
+ - **Missing from `.env.example` entirely, now added**:
303
+ `ALLOW_INSECURE_LOCAL_DEV`, `EMBEDDING_MODEL` (which OpenAI embedding
304
+ model the semantic cache uses), `METRICS_LOG_PATH` (JSONL storage
305
+ location), `MEMOCODE_ROUTER_DATABASE_URL` (an alternate to
306
+ `DATABASE_URL` that takes priority when set - useful when embedding
307
+ this inside an app that already has its own `DATABASE_URL`), and
308
+ `ROUTER_TIERS_JSON` (override the default routing tiers).
309
+ - **Genuinely tested the quickstart, not just written it**: ran
310
+ `npm install`, copied `.env.example` to `.env`, filled in a test
311
+ internal key and a fake Anthropic key, ran `npm start` - boots clean,
312
+ logs "🚀 cachegate listening on port 4000." Hit `GET /health` (real
313
+ response, correctly reflecting config) and `POST
314
+ /v1/chat/completions` (reached Anthropic for real, got back a genuine
315
+ 401 "API key is invalid" - proving the full pipeline works end to
316
+ end; only the placeholder key is fake, exactly what a real follower
317
+ would see before adding their own). Also confirmed `.env` and the
318
+ metrics `data/` directory stay gitignored, as claimed.
319
+ - README's `.env` snippet now points to `.env.example` for the full
320
+ option list instead of duplicating a partial, now-corrected copy of it.
321
+
322
+ ---
323
+
324
+ ## Group C — Keep embedded and public in sync, and safe (steps 11-14)
325
+
326
+ ### 11. ✅ Decide the concrete sync mechanism — **decided 2026-08-29**
327
+ - `ROADMAP.md` section 6 states the *principle* ("wrap it, don't fork
328
+ it - same core logic, two thin deployment shells") but never names
329
+ an actual mechanism. Two directions were on the table:
330
+ - (a) the public repo becomes the source of truth, MemoCode vendors
331
+ it in via a script/subtree pull.
332
+ - (b) this monorepo directory stays the source of truth, a script
333
+ pushes/mirrors it out to the public repo on release.
334
+ - **Decided: (b) — this monorepo directory stays the source of truth.**
335
+ Reasoning:
336
+ - It matches reality, not a fresh ideal: every real day of
337
+ development on this router - the original 20-step build, the
338
+ failover feature, all ten OSS-prep steps so far - happened inside
339
+ this monorepo, through its own task-branch/PR/gate workflow
340
+ (`AGENTS.md`). Moving day-to-day development to the public repo
341
+ would mean either running two parallel workflows or abandoning the
342
+ one that's actually proven itself this week - neither is worth it
343
+ to satisfy a "public repo is canonical" ideal nobody needs yet.
344
+ - Option (a) would put every internal-only concern (this team's own
345
+ coordination-board conventions, anything MemoCode-embedding-
346
+ specific) through public PR review before it could land internally
347
+ - backwards for a team that needs to move fast on its own repo.
348
+ - This is also the well-established pattern for exactly this
349
+ situation, not a novel one: several real companies develop OSS
350
+ projects inside a private monorepo and mirror them out
351
+ (contributions flow back in via manual review + reapplication, not
352
+ automatic two-way merge) rather than developing directly in the
353
+ public repo.
354
+ - **The real gap this creates, named rather than ignored:** once
355
+ external contributors exist (post step 19's launch), their PRs land
356
+ on the PUBLIC repo first - there is no automatic path back into this
357
+ monorepo. Concrete resolution: an accepted external PR gets manually
358
+ reapplied to `210_apps/001_model_router/` as its own normal task
359
+ branch here (same `AGENTS.md` workflow as any other change - it goes
360
+ through this project's own gate even though it originated externally,
361
+ on purpose, not as an oversight: an external diff doesn't get to skip
362
+ this codebase's own verification standard just because GitHub already
363
+ approved it). This creates a real but bounded divergence window
364
+ between the two repos - acceptable, disclosed, and the discipline is
365
+ keeping that window short (reapply promptly), not eliminating it.
366
+ - **The actual repeatable command** (implemented for real in step 12,
367
+ sketched here so the decision isn't just prose): a single script,
368
+ `sync-oss-release.sh`, invoked as `./sync-oss-release.sh <path-to-public-repo-checkout>`,
369
+ that does, in order: (1) copy this directory's tracked files into the
370
+ target checkout, excluding anything `.gitignore`d; (2) run the same
371
+ secrets grep step 3 already established, failing loudly rather than
372
+ publishing on a hit; (3) bump `package.json`'s version per step 6's
373
+ deferred semver plan; (4) commit and leave the push to a human/CI
374
+ step, never auto-pushed. One command, not a remembered sequence of
375
+ manual copy-paste steps.
376
+
377
+ ### 12. ✅ Build the actual sync script/workflow — done and genuinely tested 2026-08-29
378
+ - `sync-oss-release.sh` built per step 11's decision: mirrors this
379
+ directory's git-tracked files into a target checkout (removing
380
+ everything else there first, except its own `.git/`), scans for
381
+ secrets before touching anything, optionally bumps `package.json`'s
382
+ version (`--version X.Y.Z`), and commits in the TARGET repo without
383
+ pushing. Refuses to run against a path that isn't a git repository,
384
+ specifically so pointing it at the wrong path can't wipe something
385
+ unrelated.
386
+ - **Actually tested end-to-end, not just written** - a real target git
387
+ repo was created in scratch space and run through every real
388
+ scenario: a clean sync with `--version 0.9.0` (files copied
389
+ correctly, version bumped, a stale target-only file correctly
390
+ removed, real commit created); a resync with no version flag
391
+ (correctly left the version as whatever's currently in this
392
+ directory); an identical third run (correctly reported "nothing
393
+ changed," no empty commit); the non-git-directory safety guard
394
+ (correctly refused); and the secrets-scan abort path (a real fake
395
+ key was planted in `README.md` and the script was run against it).
396
+ - **Real bug found by that last test, not assumed to work**: the
397
+ secrets-scan regex FAILED to catch the planted key on the first
398
+ attempt. Root cause: the character class `[a-zA-Z0-9]{20,}` used
399
+ after `sk-`/`xai-` doesn't allow hyphens - and a real Anthropic key
400
+ looks like `sk-ant-api03-<random>`, where the hyphens immediately
401
+ after `sk-` broke the match after only 3 characters. Fixed to
402
+ `[a-zA-Z0-9_-]{20,}` (matching `AIza`'s pattern, which already had
403
+ this right), re-tested, and confirmed it now correctly aborts.
404
+ - **This is the exact same pattern used in steps 1, 3, and 8's manual
405
+ audits** - meaning those "clean" results were reached with the same
406
+ blind spot. Re-ran the full audit (working tree + entire commit
407
+ history) with the CORRECTED pattern before concluding anything:
408
+ still genuinely clean. The earlier conclusion holds, but only because
409
+ it was actually re-verified just now, not assumed to still be valid
410
+ once the flaw in the method was found.
411
+ - No separate "checklist for what needs re-verifying after a sync" was
412
+ needed beyond what the script itself already does (secrets scan is
413
+ automatic on every run) - a maintainer still reviews the target
414
+ repo's diff before pushing (the script's own final message says so),
415
+ which covers README drift and anything else worth a human glance.
416
+
417
+ ### 13. ✅ Security review pass, specifically for "now public" risk — done 2026-08-29
418
+ - **The one real, serious finding, confirmed live not assumed**: sent
419
+ an actual oversized POST body during this review and got back a raw
420
+ HTML page containing a FULL STACK TRACE with this server's own
421
+ absolute filesystem paths - Express's own default error handler,
422
+ reached because nothing here ever caught an error raised before a
423
+ route's own try/catch (the concrete trigger: `express.json()`
424
+ rejecting a too-large body). Fixed with a catch-all JSON error
425
+ handler registered last, re-tested with the identical request:
426
+ `{"error":"Request body too large."}`, 413, no stack trace. This is
427
+ exactly the class of thing "worked fine as an internal tool nobody
428
+ attacked" and would not have survived being public.
429
+ - **Second real finding**: `express.json({ limit: '50mb' })` was
430
+ applied GLOBALLY and BEFORE the `/v1` auth check - an anonymous
431
+ caller could force up to 50MB of JSON parsing per request before
432
+ ever being rejected with 401. Fixed three ways: scoped to `/v1` only
433
+ (the sole route that reads a body - checked directly, every other
434
+ route is a body-less GET); moved after `requireInternalKey` and the
435
+ rate limiter in the middleware chain, so a request is rejected by a
436
+ cheap check before any parsing happens; limit dropped from 50mb to a
437
+ configurable 2mb default (`JSON_BODY_LIMIT`) - checked that this
438
+ router has no image/multimodal support, so even a very long text
439
+ conversation fits comfortably under that. Live-verified: an
440
+ unauthenticated 3MB POST now gets rejected in 16ms with a 401,
441
+ vs. previously being fully parsed first.
442
+ - **Third finding, lower severity**: `/stats` and `/dashboard/data`
443
+ had `requireInternalKey` but no rate limit at all, unlike `/v1`.
444
+ Added a separate, more generous `readEndpointLimiter`
445
+ (`READ_RATE_LIMIT_MAX`, default 120/window) - lower stakes than `/v1`
446
+ (no provider spend on the line) but still real server work a
447
+ leaked/shared key shouldn't be able to hammer unbounded.
448
+ - Free hardening added while in here: `app.disable('x-powered-by')` -
449
+ no reason to hand a public-facing service's framework fingerprint to
450
+ every caller.
451
+ - **Confirmed clean, no fix needed** (checked directly, not assumed):
452
+ no `.stack` or raw error object is ever sent to a client anywhere in
453
+ the existing route handlers - every error path already used
454
+ `err.message` only. `ALLOW_INSECURE_LOCAL_DEV` already refuses to
455
+ start without either a real key or this explicit opt-in, AND already
456
+ prints a loud runtime `console.warn` when active - no doc-only
457
+ warning to strengthen, the code itself already enforces it. No CORS
458
+ middleware exists, which is the CORRECT default here (an admin/data
459
+ API with bearer-token auth has no reason to allow arbitrary
460
+ cross-origin browser access) - not a gap to fix.
461
+ - Also clarified in `server.js`'s own rate-limiter comment: the
462
+ "shared ceiling across all callers" caveat is specific to a
463
+ single-caller EMBEDDED deployment - a standalone self-hoster with
464
+ distinct per-caller IPs gets real per-caller separation from the
465
+ same default, not the same limitation.
466
+ - All fixes verified live (server started, real HTTP requests sent and
467
+ checked) in addition to the automated suite - 96/96 passing
468
+ throughout, confirming none of this changed any existing behavior
469
+ other than the three things it was meant to fix.
470
+
471
+ ### 14. ✅ Explicit non-goals, written down where a stranger will read them — done 2026-08-29
472
+ - Most of the literal content already existed from step 5 (the README's
473
+ "What this is NOT" section) and step 7 (`CONTRIBUTING.md`,
474
+ `.github/ISSUE_TEMPLATE/feature_request.md`) - checked all three
475
+ directly rather than assuming step 14 was redundant with them.
476
+ - **Real gap found in two of the three, not assumed fine**: both the
477
+ README's closing line and `CONTRIBUTING.md`'s scope section said, in
478
+ effect, "if you want a hosted service, fork it" - which actively
479
+ *invites* the exact outcome this section (and step 2's license
480
+ reasoning) exists to discourage. Technically true under MIT, but
481
+ saying it right next to "this is not a hosted service" undermines
482
+ the whole point of drawing the boundary. Fixed both: still honest
483
+ that MIT permits it (never claimed otherwise), but reframed as "not
484
+ a licensing restriction, a project-scope one" - this repo specifically
485
+ isn't going to grow into a hosted competitor to its own paid product,
486
+ full stop, rather than a soft invitation to go build one.
487
+ `.github/ISSUE_TEMPLATE/feature_request.md` already had this right -
488
+ checked, no change needed there.
489
+ - README's closing line also now explicitly separates the two real,
490
+ legitimate PR-worthy gaps (140-provider reach, vector-indexed
491
+ semantic cache) from the one boundary that isn't a gap at all,
492
+ pointing to `CONTRIBUTING.md` before someone opens a PR for it.
493
+
494
+ ---
495
+
496
+ ## Group D — Ship it (steps 15-18)
497
+
498
+ ### 15. Create the real public GitHub repository — live 2026-08-29: https://github.com/iDebunk/cachegate
499
+ - **Owner decided: `iDebunk`** (the same GitHub account already used for
500
+ this monorepo's own repos - confirmed via `get_me` that it's a user
501
+ account, not a separate org, so "existing org" and "personal account"
502
+ were never actually two different choices here). Reasoning: matches
503
+ the common real-world pattern of hosting OSS directly under a
504
+ company's own account; a personal-account or brand-new-org
505
+ alternative would either tie a strategic asset to one individual or
506
+ invent a naming decision with no present need - and a repo can be
507
+ transferred to a different owner later at zero cost if that ever
508
+ changes, so this wasn't a one-way door.
509
+ - **Real limitation hit while executing this**: this session's own
510
+ `mcp__github__create_repository` call failed with a 403 ("Resource
511
+ not accessible by integration") - the connected GitHub App lacks
512
+ repository-creation scope, confirmed not a transient error. The CEO
513
+ created the repo manually via github.com/new instead (public, no
514
+ auto-init) - noted here so a future session doesn't waste a retry
515
+ loop on the same wall.
516
+ - **Real finding on branch protection's status-check picker**: the CI
517
+ workflow's `strategy: matrix` (Node 18.x/20.x/22.x, added in step 8)
518
+ means GitHub creates THREE separate status checks - `test (18.x)`,
519
+ `test (20.x)`, `test (22.x)` - not one called "Tests" as the
520
+ workflow's own top-level `name:` field might suggest. Searching
521
+ "Tests" in the branch-protection UI finds nothing; searching "test"
522
+ (the job id) finds all three. All three were required, matching the
523
+ actual reason the matrix exists (a single required check would let
524
+ the other two silently break without ever blocking a merge).
525
+ - **Also decided**: "Require branches to be up to date before merging"
526
+ left OFF for now - real value only once there's enough concurrent PR
527
+ traffic for it to matter, and it's a single checkbox to enable later
528
+ when that's true. "Require approvals: 1" kept ON, with admin bypass
529
+ intact (default), so external PRs need real review without locking
530
+ the maintainer out of merging their own work.
531
+ - Topics added via the repo's "About" gear (only appears once the repo
532
+ has content - see step 16): `llm-gateway`, `semantic-cache`,
533
+ `self-hosted`, `openai-compatible`, `llm-proxy`.
534
+
535
+ ### 16. ✅ Push the extracted, cleaned codebase — done 2026-08-29
536
+ - Pushed from the CEO's own local machine using `sync-oss-release.sh`
537
+ (this session's own repo-scope restrictions block direct git push
538
+ from here too, same category of limitation as step 15's repo
539
+ creation) - the script itself worked exactly as tested: clean
540
+ secrets scan, version bumped to `1.0.0` (the real first-release
541
+ moment step 6's deferred plan was waiting for), 40 files mirrored,
542
+ one real commit.
543
+ - **Two real, Windows-specific findings, worth keeping for next time**:
544
+ (1) the script's shebang/`set -euo pipefail` line broke with
545
+ `$'\r': command not found` - Git on Windows had checked the file out
546
+ with CRLF line endings, which no bash interpreter handles in a
547
+ script. Fixed locally with `sed -i 's/\r$//' sync-oss-release.sh`
548
+ before running; added `.gitattributes` (forcing LF on `*.sh` and text
549
+ files generally) to this directory so a future Windows checkout gets
550
+ LF from git itself rather than needing the same manual `sed` fix
551
+ again. (2) `cp`-based file mirroring on
552
+ Windows/NTFS loses the Unix executable bit - `server.js` (needed for
553
+ its `npx cachegate` shebang) and `sync-oss-release.sh` itself both
554
+ landed as `644` in the initial commit. Fixed via
555
+ `git update-index --chmod=+x <file>` before pushing, which sets the
556
+ bit at the git level regardless of the filesystem - confirmed via
557
+ `git ls-files -s` showing `100755` before the push went out.
558
+ - First CI run on the real public repo passed for real (all three
559
+ Node-version matrix jobs green, ~1 minute) - genuine external
560
+ validation of step 8's workflow, not just this sandbox's own testing.
561
+ - Commit message rewritten before pushing to read as an actual first
562
+ release ("Initial public release of cachegate") rather than the
563
+ script's own generic ongoing-sync message, which is what every
564
+ *later* sync should look like, not this one-time moment.
565
+
566
+ ### 17. ✅ Publish the npm package — live 2026-08-29: `npm install cachegate`
567
+ - Published from the CEO's own machine (this session has no npm
568
+ credentials, same category of limitation as steps 15-16's GitHub
569
+ access - noted once there, applies here too without repeating it).
570
+ - **Real wall hit**: npm's registry now requires either account 2FA or
571
+ a Granular Access Token with "Bypass two-factor authentication"
572
+ checked before it accepts a publish (403 otherwise) - npm's own
573
+ token-creation UI has also changed since older guides describe it
574
+ (no more separate "Automation" token type; it's a checkbox on the
575
+ one Granular Access Token form now, under Packages and scopes →
576
+ Permissions → Read and write → All packages, since an unpublished
577
+ package can't be individually selected yet).
578
+ - **A `"bin[cachegate]" script name was cleaned` warning appeared on
579
+ every publish attempt - checked directly rather than assumed
580
+ cosmetic**: queried the live registry after publishing
581
+ (`registry.npmjs.org/cachegate/1.0.0`) and confirmed the published
582
+ `bin` field is `{"cachegate":"server.js"}` - npm just stripped the
583
+ `./` prefix from `"./server.js"` as routine normalization. Not
584
+ corruption, not related to the earlier Windows CRLF finding from
585
+ step 16 - a real, if noisy, false alarm worth having actually
586
+ checked rather than left as an assumption either way.
587
+ - **Verified for real, not just published-and-assumed-fine**: installed
588
+ in a brand new, unrelated directory (`npm init -y` + `npm install
589
+ cachegate`) and confirmed `require('cachegate')` resolves and exposes
590
+ `{ app, isAuthConfigured }` - proves it installs and loads correctly
591
+ from the real registry, not just "works in the folder it was built
592
+ in."
593
+ - **Follow-up gap, caught by DeepSeek's read-only review and fixed
594
+ 2026-08-29**: `sync-oss-release.sh --version X.Y.Z` bumps THIS
595
+ directory's own `package.json` (the monorepo source) in place before
596
+ copying, by design (see the script's own header comment) - but that
597
+ bump, from the original `1.0.0` publish, was never committed back
598
+ here. The monorepo's committed `package.json` sat at the stale
599
+ `0.1.0` while npm had `1.0.0` live, and the CEO's local
600
+ `memocode-fresh` checkout carried the real bump only as an
601
+ uncommitted working-tree change. Fixed by committing `package.json`
602
+ (and regenerating `package-lock.json` to match) at `1.0.0` here, so
603
+ the source of truth matches what's actually published instead of
604
+ lagging behind it silently.
605
+
606
+ ### 18. ✅ Publish the Docker image — live 2026-08-29: `ghcr.io/idebunk/cachegate:latest`
607
+ - Pushed from the CEO's own machine (same credential/scope limitation
608
+ as steps 15-17, not repeated here). Built with `docker build`, tagged
609
+ both `:1.0.0` and `:latest`, pushed to GHCR with `docker login
610
+ ghcr.io` using a classic GitHub PAT (`write:packages` scope) as the
611
+ password prompt - a separate credential from the npm token in step
612
+ 17. Real digest for both tags:
613
+ `sha256:c657784e882ceecd427974c4fb0b72a6953cf45da8fab66a3ae4cc4e2cd4e99f`.
614
+ - **Real wall hit**: GHCR images pushed under a personal account (not
615
+ an org) default to **Private** visibility even though the linked
616
+ repo is Public, and are not auto-linked to that repo's sidebar or
617
+ the pretty `/pkgs/container/<name>` URL (which 404s until linked).
618
+ Fixed the load-bearing part - visibility - via the package's own
619
+ Danger Zone (`github.com/<user>?tab=packages` → click the package
620
+ name → Package settings). The cosmetic repo-link
621
+ (`/pkgs/container/cachegate` resolving, sidebar showing the package)
622
+ was left as-is after confirming it has no effect on public
623
+ pullability - GitHub's package-listing UI showed inconsistent counts
624
+ between `?tab=packages` and a `?repo_name=` filtered variant of the
625
+ same page even after a refresh, which looks like a stale/broken
626
+ query-string filter on GitHub's own UI rather than a real state
627
+ problem, and wasn't worth chasing further.
628
+ - **Update 2026-08-30 - actually closed, not just deprioritized**: used
629
+ the package's own "Connect Repository" button (`Package settings` →
630
+ `Link this package to a repository`) and connected it to
631
+ `iDebunk/cachegate` specifically - worth naming plainly that the repo
632
+ picker also offered the private `2000_1010_memocode01` monorepo (same
633
+ owner, so GHCR's "must be the same owner" rule doesn't rule it out on
634
+ its own), which would have been a real mistake to pick, not just a
635
+ wrong label - a public package's page pointing at a private internal
636
+ repo defeats the entire point of the sync-out design. Verified after
637
+ connecting: the package page now shows `cachegate`'s actual README
638
+ content, confirming the correct repo, not the monorepo's.
639
+ - **Verified for real, cold, from a machine that has never built this
640
+ project**: `docker logout ghcr.io` (drops all local credentials),
641
+ then `docker rmi` both local tags, then `docker pull
642
+ ghcr.io/idebunk/cachegate:latest` - succeeded with zero credentials,
643
+ proving the image is genuinely public. First `docker run` +
644
+ `curl /health` attempt failed (`Exited (1)`, then later a timing-
645
+ related empty reply) - checked `docker logs` rather than guessing,
646
+ and found the real cause: the app's own security hardening from step
647
+ 13 correctly refuses to start with an open `/v1` endpoint unless
648
+ `MODEL_ROUTER_INTERNAL_KEY` is set (working as designed, not a
649
+ packaging bug). Reran with that env var set, `docker ps` showed
650
+ `(healthy)`, and `curl http://localhost:4001/health` returned
651
+ `{"status":"healthy","redis_connected":false,...}` - a genuine cold
652
+ pull-run-verify cycle, not published-and-assumed-fine.
653
+
654
+ ---
655
+
656
+ ## Group E — Tell people, then keep it alive (steps 19-20)
657
+
658
+ ### 19. ✅ Announce — posted live 2026-08-31
659
+ - All four posts drafted with real, distinct framing (see the original
660
+ drafting notes below), delivered as a published Artifact ("launch
661
+ kit") plus a standalone downloadable `.html` file.
662
+ - **Posted for real, live, from the CEO's own accounts** - the outward-
663
+ facing, hard-to-reverse action this session genuinely can't do
664
+ itself (no browser session or credentials on any of these
665
+ platforms). Confirmed done by the CEO directly; URLs weren't
666
+ collected/independently verified this pass, noted plainly rather
667
+ than implied otherwise.
668
+ - **HN title had to be trimmed live**: the original draft title (~98
669
+ chars) exceeded Hacker News's 80-character limit - shortened to
670
+ `Show HN: cachegate – self-hosted, cost-aware caching proxy for LLMs`
671
+ (67 chars) on the spot, same core pitch intact.
672
+ - **r/selfhosted was correctly skipped, not posted to**: its submit
673
+ flow required a mandatory flair, and one option was literally
674
+ "Release (No AI)" - cachegate was built with substantial AI
675
+ assistance (this whole roadmap's own execution is direct evidence),
676
+ so that flair genuinely didn't apply. Rather than force an
677
+ ill-fitting flair or imply something untrue, the CEO skipped that
678
+ specific subreddit outright - the right call, not a shortcut.
679
+ - **r/SelfHostedAI posted instead** - a different, more narrowly-
680
+ focused subreddit (self-hosted AI tooling specifically) with no such
681
+ flair conflict, reached mid-session while navigating Reddit's submit
682
+ flow. A reasonable, arguably better-targeted substitution for the
683
+ fourth platform, not the originally-planned r/selfhosted.
684
+ - **A near-miss caught before it mattered**: an early attempt landed on
685
+ `reddit.com/r/Art/submit` (a real subreddit for visual artwork, not
686
+ self-hosted software) - caught immediately from the page's own
687
+ "Title, Artist Name, Medium, Year" format requirement before
688
+ anything was submitted there.
689
+ - Final four actually posted: **Hacker News** (Show HN), **r/LocalLLaMA**,
690
+ **r/SelfHostedAI** (substituted for r/selfhosted), **Dev.to**.
691
+
692
+ Original drafting notes (why each framing was chosen): Show HN leads
693
+ with the honest savings range and the "what it doesn't do yet" list up
694
+ front (that crowd asks about gaps in the first three comments
695
+ regardless); r/LocalLLaMA leads with the one objection that sub raises
696
+ immediately - no local-model backend yet - framed as an open
697
+ contribution gap, not a hidden limitation; the self-hosted-targeted
698
+ post puts the `docker run` one-liner and the healthcheck/no-telemetry
699
+ details before any explanation; Dev.to is the one long-form piece with
700
+ the real origin story (an internal MemoCode cost-control tool that
701
+ turned out worth its own release) and the fullest explanation of the
702
+ exact/semantic cache distinction.
703
+
704
+ ### 20. ✅ Post-launch triage plan — decided 2026-08-29
705
+
706
+ **Who watches, and how, stated plainly instead of assumed:**
707
+ `AGENTS.md`'s "one agent holds the standing watch" model (a Claude
708
+ session subscribed to every open PR via `subscribe_pr_activity`) is
709
+ what this team already uses internally - but it does not extend to
710
+ `cachegate` automatically. This session's own repo access is scoped to
711
+ a fixed list of repos for the environment it runs in, `cachegate`
712
+ is not on that list, and adding it hit a real, already-documented
713
+ limitation earlier in this same roadmap (`add_repo` on
714
+ `iDebunk/cachegate` failing with an unresolved approval prompt, back
715
+ in steps 15-16). So, honestly, for now:
716
+ - **The CEO is the standing watch**, via GitHub's own native "Watch →
717
+ All Activity" on the repo plus notifications, not a Claude session -
718
+ that's the real, working mechanism today, not an aspiration.
719
+ - **Response SLA**: first reply to any new issue or PR within 48
720
+ hours, even if just "looked at this, need more detail" or "queued,
721
+ will get to the actual fix by \<date\>" - a fast acknowledgment is
722
+ what prevents "reads as abandoned," not a fast fix.
723
+ - **Revisit if the environment's repo scope ever includes
724
+ `iDebunk/cachegate`** (an access-grant change outside this session's
725
+ own control, not something to keep retrying) - at that point, a
726
+ Claude session picking up the exact same standing-watch model used
727
+ internally becomes possible for real, and is the right thing to
728
+ switch to.
729
+
730
+ **"Phase 5 done" - one concrete bar, not a vibe:**
731
+ Phase 5 (this whole roadmap) is done when **either** of these is true,
732
+ whichever comes first, not both required:
733
+ - 4 consecutive weeks live with no open critical issue (a security
734
+ vulnerability, data loss, or the router silently returning wrong
735
+ answers) at any point during that window, **or**
736
+ - the first external (non-`iDebunk`) pull request is merged.
737
+ A quiet repo with zero critical bugs for a month is success on its own
738
+ terms; so is a total stranger trusting the code enough to send a real
739
+ patch before that clock runs out. Either is real evidence the release
740
+ worked - waiting for both would just be moving the goalpost.
741
+
742
+ **Phase 6 unlock:** per `ROADMAP.md`, Phase 6 (a standalone/hosted
743
+ product built on this same engine, explicitly NOT open-sourced itself)
744
+ becomes buildable once the bar above is actually met - not before, and
745
+ not automatically the moment it is either. Meeting the bar makes
746
+ Phase 6 *plannable* for real, using evidence from actual outside use
747
+ instead of internal dogfooding alone; starting to build it is still a
748
+ separate, deliberate decision when that time comes.
749
+
750
+ ---
751
+
752
+ ## Follow-ups discovered after the original 20 steps closed
753
+
754
+ Not renumbered into the list above - the original 20 are a closed,
755
+ honest historical record (see the scaffold-first rule in `AGENTS.md`).
756
+ New real work discovered later gets its own dated entry here instead.
757
+
758
+ ### 21. ✅ Docker Hub mirror — live 2026-08-30: `docker.io/shipman/cachegate`
759
+
760
+ Step 9 originally decided GHCR over Docker Hub, explicitly leaving the
761
+ door open: *"Not closed off permanently: worth adding a Docker Hub
762
+ mirror later if search-driven discovery turns out to matter."* Revisited
763
+ because GHCR's own download counter turned out to be unreliable for
764
+ this purpose (checked directly - the live package page shows "Total
765
+ downloads: 0" even after step 18's own confirmed cold pull, so either
766
+ anonymous pulls aren't counted or there's a reporting lag; not
767
+ confirmed which) and Docker Hub remains a real, separate discovery
768
+ surface people specifically search.
769
+
770
+ - **Real wall hit, caught before it caused real confusion**: the plan
771
+ going in assumed the CEO's Docker Hub username was `cachegate`, matching
772
+ the project name. It isn't - `docker login` reported the real account
773
+ as `shipman`. First push to `cachegate/cachegate` correctly failed
774
+ (`insufficient_scope: authorization failed` - `shipman` has no write
775
+ access to a `cachegate` namespace). **Target corrected to
776
+ `docker.io/shipman/cachegate`** - Docker Hub auto-created the repo on
777
+ first push to that personal namespace, no separate creation step
778
+ needed.
779
+ - **Also fixed while in here**: added standard OCI labels to the
780
+ Dockerfile (`org.opencontainers.image.source`, `.description`,
781
+ `.licenses`). `image.source` specifically is what GHCR reads to link
782
+ a package to its repo automatically on every future push - the
783
+ durable alternative to clicking "Connect Repository" by hand once,
784
+ which doesn't survive a re-publish. Same three-line cost, fixes the
785
+ cosmetic repo-link gap left open back at step 18 for good. GHCR's
786
+ already-published `:1.0.0`/`:latest` tags were deliberately left
787
+ untouched - not worth rewriting an immutable version tag for a
788
+ cosmetic label; it reaches GHCR naturally on the next real version
789
+ bump.
790
+ - **Login was actually via Docker's device-code browser flow**, not a
791
+ pasted token at a password prompt - `docker login docker.io` opened
792
+ a one-time code + browser confirmation instead. Functionally
793
+ equivalent (an access token was still created and available as a
794
+ fallback), just a different, newer CLI flow than GHCR's or npm's.
795
+ - **Not a CI automation, on purpose, matching GHCR's own precedent**:
796
+ pushed manually from the CEO's own machine, same credential/scope
797
+ category as steps 15-18 - not something this session can do directly.
798
+ - **Verified for real, cold, same rigor as step 18**: `docker logout
799
+ docker.io` (drops all local credentials), `docker rmi` all local
800
+ tags, `docker pull docker.io/shipman/cachegate:latest` - succeeded
801
+ with zero credentials. Real digest:
802
+ `sha256:4d8548dda952ec1dceff6ff6a9c69e1c86d55193150395465f387c8da171a55e`.
803
+ First `curl /health` attempt hit the same timing gap seen at step 18
804
+ (container 1 second old, still `health: starting`) - not re-guessed
805
+ as a real bug this time, waited for `docker ps` to show `(healthy)`,
806
+ then `curl http://localhost:4002/health` returned
807
+ `{"status":"healthy","redis_connected":false,...}` - a genuine cold
808
+ pull-run-verify cycle on a second, independent registry.
809
+
810
+ ### 22. 🟨 `--env-path` flag - built 2026-08-31, not yet republished
811
+
812
+ Decided after a design discussion with the CEO: the cwd-only `.env`
813
+ lookup (`npx cachegate` reads `.env` from wherever you happen to be
814
+ standing, not a fixed path) was a real, repeated point of confusion -
815
+ see step 21's own README work on "Wiring this into your app." Weighed
816
+ against a second candidate (per-app keys / usage tracked separately per
817
+ caller) and deliberately NOT building that one: the README already
818
+ states this is "a single-operator, self-hosted admin tool, not a
819
+ multi-tenant product," and real per-app key custody is explicitly
820
+ named in "What this is NOT" as reserved for the separate, closed,
821
+ hosted product this engine may one day sit under - building it into
822
+ the free engine now would cannibalize that product's own future
823
+ differentiation, not just cost more effort.
824
+
825
+ - **What was built**: an optional `--env-path <file>` /
826
+ `--env-path=<file>` flag, parsed from `process.argv` before
827
+ `dotenv.config()` runs. Backward compatible by construction - no flag
828
+ given behaves exactly as before (reads `.env` from the current
829
+ directory); flag given reads from wherever it points instead,
830
+ resolved relative to the invoking directory or absolute as given.
831
+ `resolveEnvPathFromArgv` extracted as its own small, pure, exported
832
+ function specifically so this could be unit-tested directly rather
833
+ than through a heavier process-spawning test.
834
+ - **Verified twice, not once**: 6 new unit tests
835
+ (`test/env-path.test.js`) covering both flag syntaxes
836
+ (`--env-path X` and `--env-path=X`), absolute paths, the flag missing
837
+ entirely, the flag present with no value after it, and other
838
+ unrelated flags not interfering. Full suite: 105/105 passing. Then a
839
+ real, live smoke test on top of the unit tests - not just trusting
840
+ them - starting `node server.js --env-path <path>` from a directory
841
+ with no `.env` at all, pointing at a real `.env` (with a distinct
842
+ `PORT=4099`) in a completely different location, and confirming via
843
+ `curl /health` that it actually started on the port from that other
844
+ file, not the default.
845
+ - **`README.md` updated** in both places that described the old
846
+ cwd-only behavior as absolute ("there's no separate config path for
847
+ this one") - both now correctly describe the flag as an option.
848
+ - **Version bumped to `1.1.0`** (`package.json` + regenerated
849
+ `package-lock.json`) - a real, backward-compatible feature addition,
850
+ not a patch.
851
+ - **Not done yet, on purpose**: this needs republishing to all three
852
+ registries (npm, GHCR, Docker Hub) before it's real for anyone
853
+ outside this monorepo, same cost and rigor as every other publish
854
+ step in this document - not marking this ✅ until that's actually
855
+ done and cold-verified again.