@legioncodeinc/rflectr 0.1.0 → 0.1.1

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.
Files changed (34) hide show
  1. package/README.md +1 -5
  2. package/dist/cli.js +1 -1
  3. package/library/README.md +39 -39
  4. package/library/issues/README.md +46 -46
  5. package/library/issues/backlog/README.md +26 -26
  6. package/library/issues/completed/README.md +13 -13
  7. package/library/issues/in-work/README.md +13 -13
  8. package/library/knowledge/README.md +34 -34
  9. package/library/knowledge/private/README.md +40 -40
  10. package/library/knowledge/private/standards/documentation-framework.md +154 -154
  11. package/library/knowledge/public/README.md +49 -49
  12. package/library/notes/README.md +21 -21
  13. package/library/requirements/README.md +51 -51
  14. package/library/requirements/backlog/README.md +30 -30
  15. package/library/requirements/completed/README.md +14 -14
  16. package/library/requirements/completed/prd-002-provider-registry/prd-002-provider-registry-index.md +263 -0
  17. package/library/requirements/completed/prd-003-model-discovery-classification/prd-003-model-discovery-classification-index.md +260 -0
  18. package/library/requirements/completed/prd-004-translation-layer/prd-004-translation-layer-index.md +196 -0
  19. package/library/requirements/completed/prd-005-local-proxy-catalog-routing/prd-005-local-proxy-catalog-routing-index.md +176 -0
  20. package/library/requirements/completed/prd-006-credential-storage/prd-006-credential-storage-index.md +190 -0
  21. package/library/requirements/completed/prd-006-credential-storage/qa/.gitkeep +0 -0
  22. package/library/requirements/completed/prd-007-oauth-device-flows/prd-007-oauth-device-flows-index.md +208 -0
  23. package/library/requirements/completed/prd-008-preferences-tiers-favorites/prd-008-preferences-tiers-favorites-index.md +249 -0
  24. package/library/requirements/completed/prd-008-preferences-tiers-favorites/qa/.gitkeep +0 -0
  25. package/library/requirements/completed/prd-009-codex-integration/prd-009-codex-integration-index.md +212 -0
  26. package/library/requirements/completed/prd-009-codex-integration/qa/.gitkeep +0 -0
  27. package/library/requirements/completed/prd-010-gemini-cli-integration/prd-010-gemini-cli-integration-index.md +211 -0
  28. package/library/requirements/completed/prd-010-gemini-cli-integration/qa/.gitkeep +0 -0
  29. package/library/requirements/completed/prd-011-claude-desktop-integration/prd-011-claude-desktop-integration-index.md +228 -0
  30. package/library/requirements/completed/prd-012-server-gateway/prd-012-server-gateway-index.md +356 -0
  31. package/library/requirements/completed/prd-012-server-gateway/qa/.gitkeep +0 -0
  32. package/library/requirements/in-work/README.md +19 -19
  33. package/library/requirements/reports/README.md +31 -31
  34. package/package.json +1 -1
@@ -0,0 +1,228 @@
1
+ # PRD-011: Claude Desktop Integration *(Retroactive)*
2
+
3
+ > **Status:** Shipped
4
+ > **Priority:** —
5
+ > **Effort:** —
6
+ > **Written:** June 2026
7
+ > **Retroactive:** Yes — written after implementation (rflectr v0.2.7).
8
+ > **Source:** `src/claude-desktop/*`, `src/claude-app.ts`
9
+
10
+ ---
11
+
12
+ ## Overview
13
+
14
+ `rflectr claude-app` launches the **Claude Desktop** app in third-party-inference ("3P") mode, pointed at a local rflectr gateway instead of Anthropic's servers. The user picks a provider + model (or a favorites catalog), rflectr starts an in-process gateway server on a random local port, writes a gateway config into Claude Desktop's on-disk 3P config library, and opens (or restarts) the app. On exit, rflectr restores the original config.
15
+
16
+ The defining constraint of this surface: **a desktop app cannot inherit environment variables** the way a launched CLI can. The CLI launchers (`rflectr claude`, `codex`, `gemini`) point the host at the proxy purely through child-process env vars and never touch a config file (see [PRD-001 — CLI Core & Launch Orchestration](../prd-001-cli-core-launch-orchestration/prd-001-cli-core-launch-orchestration-index.md)). Claude Desktop has no such hook. So this is the one surface where rflectr **writes the host application's own config file** — and therefore must back it up and restore it on exit, guarded by a lock file to survive crashes. This is the deliberate exception to rflectr's env-only isolation contract.
17
+
18
+ Entry point: `runClaudeAppCommand` (`src/claude-app.ts:61`). Supported on **macOS and Windows only** (`claudeAppSupported`, `src/claude-desktop/app-launch.ts:11`).
19
+
20
+ > See also: [`harnesses.md`](../../../knowledge/private/integrations/harnesses.md) (private integration notes — the "where each host departs from the pattern" overview) and [`claude-desktop.md`](../../../knowledge/public/guides/claude-desktop.md) (public user-facing setup guide).
21
+
22
+ ---
23
+
24
+ ## What Was Built
25
+
26
+ - A `rflectr claude-app` command that, on macOS/Windows, drives Claude Desktop into 3P mode against a local gateway with no manual config editing.
27
+ - A **config-write** path: a `<uuid>.json` gateway config is written into the Claude Desktop 3P config library, and `_meta.json`'s `appliedId` is repointed at it so the app picks it up on next launch (`writeRflectrConfig`, `src/claude-desktop/app-config.ts:58`).
28
+ - A **backup + restore** path: `_meta.json` is copied to `_meta.json.bak` before the patch, and a `.rflectr.lock` file records the live session. On clean exit, crash recovery, or `--restore`, the backup is restored and the injected config removed (`src/claude-desktop/app-session.ts`).
29
+ - **Session lifecycle** handling: concurrent-session detection, stale-session recovery on startup, SIGINT/SIGTERM-driven shutdown, and a `process.on('exit')` cleanup hook.
30
+ - **Model selection** reusing the Codex provider/model pickers, plus a favorites-catalog mode driven by saved preferences.
31
+ - The gateway itself is the full `server` gateway (`startServer`, see [PRD-012 — Server Gateway](../prd-012-server-gateway/prd-012-server-gateway-index.md)), serving `/anthropic` with a synthetic model catalog — not a bespoke per-protocol proxy.
32
+
33
+ ---
34
+
35
+ ## Goals
36
+
37
+ - Let a user route Claude Desktop's **Cowork** and **Code** tabs at registry providers / OpenCode Zen / Go with one command, no manual Developer-menu config.
38
+ - Never leave Claude Desktop's config in a broken 3P state after the rflectr session ends — restore the prior config on exit and after crashes.
39
+ - Keep the model catalog under the user's control (single selected model, or a favorites-only catalog).
40
+ - Reuse the existing `server` gateway and SDK translation layer rather than building a Claude-Desktop-specific proxy.
41
+
42
+ ## Non-Goals
43
+
44
+ - **Linux support.** macOS and Windows only; `claudeAppSupported()` throws otherwise (`src/claude-desktop/app-launch.ts:11`).
45
+ - **Restoring Claude Desktop to first-party (Anthropic sign-in / Chat tab) mode.** rflectr restores the *pre-session* 3P config state; full 1P revert is a documented manual procedure in the user guide ([`claude-desktop.md`](../../../knowledge/public/guides/claude-desktop.md) → "Restore Claude Desktop to Anthropic's servers").
46
+ - **The Chat tab.** With 3P inference, Claude Desktop offers only Cowork and Code; Chat is an Anthropic product constraint, not an rflectr limitation.
47
+ - **Editing `settings.json`-style host config beyond the 3P config library.** rflectr only writes the `<uuid>.json` and `_meta.json` under `Claude-3p/configLibrary/`.
48
+ - **Mid-session model switching** inside the running app (the Gemini CLI surface has a `.model` switch; Claude Desktop does not).
49
+
50
+ ---
51
+
52
+ ## Features
53
+
54
+ | # | Feature | Implementation |
55
+ |---|---------|----------------|
56
+ | F1 | `rflectr claude-app` command + help/restore/trace flags | `runClaudeAppCommand`, `claudeAppHelpText` (`src/claude-app.ts:27`, `src/claude-app.ts:61`) |
57
+ | F2 | Platform gate (macOS/Windows only) | `claudeAppSupported` (`src/claude-desktop/app-launch.ts:11`) |
58
+ | F3 | App discovery (Claude.app / Claude.exe / Start menu) | `findClaudeApp` (`src/claude-desktop/app-launch.ts:68`) |
59
+ | F4 | Launch or restart the app | `launchOrRestartClaudeApp` (`src/claude-desktop/app-launch.ts:198`) |
60
+ | F5 | Provider + model picker (reuses Codex pickers) | `pickCodexProvider` / `pickCodexModel` via `src/claude-app.ts:127`–`134` |
61
+ | F6 | Favorites-catalog mode | `favorites.length > 0` branch + `filterServerModelsByFavorites` (`src/claude-app.ts:121`, `src/claude-app.ts:153`) |
62
+ | F7 | Gateway config write into 3P config library | `writeRflectrConfig` (`src/claude-desktop/app-config.ts:58`) |
63
+ | F8 | `_meta.json` backup before patch | `backupMetaJson` (`src/claude-desktop/app-session.ts:43`) |
64
+ | F9 | Lock file (pid / startedAt / uuid / proxyPort) | `ClaudeSessionLock`, `writeSessionLock` (`src/claude-desktop/app-session.ts:7`, `:28`) |
65
+ | F10 | Concurrent + stale session detection | `isConcurrentLiveSession`, `hasStaleSession` (`src/claude-desktop/app-session.ts:76`, `:67`) |
66
+ | F11 | Shutdown wait (SIGINT/SIGTERM) + exit-hook cleanup | `waitForShutdown`, `setupExitCleanup` (`src/claude-desktop/app-session.ts:94`, `:119`) |
67
+ | F12 | Restore on exit / crash / `--restore` | `cleanupSession`, `recoverSession` (`src/claude-desktop/app-session.ts:113`, `:82`) |
68
+ | F13 | Local gateway (the `server` gateway, `/anthropic`) | `startServer` + `createGatewayModelCatalog` (`src/claude-app.ts:183`) |
69
+ | F14 | Recent-models persistence per provider | `savePreferences` recent-models update (`src/claude-app.ts:206`–`212`) |
70
+
71
+ ---
72
+
73
+ ## Architecture & Implementation
74
+
75
+ ### Where this surface departs from the env-only rule
76
+
77
+ CLI hosts are pointed at the proxy through child-process environment variables only (PRD-001). A desktop app has no env to inherit, so Claude Desktop reads a **config file** at launch. rflectr therefore writes that config — and to keep the rule "never permanently mutate the host" intact, it pairs every write with a backup and a guaranteed restore.
78
+
79
+ ```mermaid
80
+ flowchart TD
81
+ cli["CLI hosts (claude, codex, gemini)"] -->|child-process env vars| proxy["translating proxy / gateway"]
82
+ app["Desktop app (claude-app)"] -->|"writes &lt;uuid&gt;.json + repoints _meta.json"| proxy
83
+ app -.->|"_meta.json.bak + .rflectr.lock"| restore["restore original config on exit / crash"]
84
+ proxy --> gw["server gateway /anthropic + SDK adapter"]
85
+ ```
86
+
87
+ ### Gateway config shape
88
+
89
+ `buildRflectrConfig(proxyPort)` (`src/claude-desktop/app-config.ts:48`) produces the 3P gateway profile:
90
+
91
+ ```jsonc
92
+ {
93
+ "inferenceProvider": "gateway",
94
+ "inferenceGatewayBaseUrl": "http://127.0.0.1:<port>/anthropic",
95
+ "inferenceGatewayApiKey": "dummy",
96
+ "inferenceGatewayAuthScheme": "bearer",
97
+ "coworkEgressAllowedHosts": ["*"]
98
+ }
99
+ ```
100
+
101
+ - `inferenceGatewayBaseUrl` ends in `/anthropic` with **no `/v1` suffix** — Claude Desktop appends `/v1/models` and `/v1/messages` itself; a `/anthropic/v1` URL would break discovery and inference (documented in [`claude-desktop.md`](../../../knowledge/public/guides/claude-desktop.md)).
102
+ - The API key is the literal `'dummy'` because local-mode gateway has no server password; the server is started with `apiKey: 'dummy'` / `serverPassword: null` (`src/claude-app.ts:186`–`187`).
103
+
104
+ ### Config write + `_meta.json` repointing
105
+
106
+ `writeRflectrConfig(proxyPort)` (`src/claude-desktop/app-config.ts:58`):
107
+
108
+ 1. Generates a `randomUUID()` and writes `buildRflectrConfig(...)` to `configLibrary/<uuid>.json`.
109
+ 2. Reads (or initializes) `_meta.json`, sets `appliedId = uuid`, and appends an `entries` row `{ id: uuid, name: 'Rflectr Gateway' }` if not already present.
110
+ 3. Returns the uuid (used as the session/cleanup key).
111
+
112
+ Config roots, by platform (`getClaudeDesktopHome`, `src/claude-desktop/app-config.ts:8`):
113
+
114
+ | Platform | 3P config root |
115
+ |---|---|
116
+ | macOS | `~/Library/Application Support/Claude-3p/` |
117
+ | Windows | `%LOCALAPPDATA%\Claude-3p/` |
118
+
119
+ The config library and `_meta.json` live under `configLibrary/` within that root (`getConfigLibraryPath`, `getMetaJsonPath`, `src/claude-desktop/app-config.ts:15`, `:19`).
120
+
121
+ ### Backup / restore via lock files
122
+
123
+ The lock and backup are the safety mechanism that makes config-writing reversible:
124
+
125
+ - **Backup:** `backupMetaJson()` copies `_meta.json` → `_meta.json.bak` *before* the patch (`src/claude-desktop/app-session.ts:43`). Called at `src/claude-app.ts:181`, before `startServer` and `writeRflectrConfig`.
126
+ - **Lock:** `writeSessionLock({ pid, startedAt, uuid, proxyPort })` writes `.rflectr.lock` in the 3P home (`src/claude-desktop/app-session.ts:28`; shape `ClaudeSessionLock`, `:7`). Written at `src/claude-app.ts:196` right after the config is applied.
127
+ - **Restore:** `restoreMetaJson()` copies the `.bak` back over `_meta.json` and deletes the backup (`src/claude-desktop/app-session.ts:51`). `removeRflectrConfig(uuid)` deletes the injected `<uuid>.json` (`:60`).
128
+ - **Cleanup entry points:** `cleanupSession(uuid)` (clean exit, `:113`) and `recoverSession()` (crash / `--restore`, `:82`) both run restore + config-removal + lock deletion. `setupExitCleanup(uuid)` registers `cleanupSession` on `process.on('exit')` as a last-resort net (`:119`).
129
+
130
+ ### Session lifecycle
131
+
132
+ ```mermaid
133
+ sequenceDiagram
134
+ participant U as User
135
+ participant R as rflectr claude-app
136
+ participant FS as Claude-3p config
137
+ participant App as Claude Desktop
138
+
139
+ U->>R: rflectr claude-app
140
+ R->>R: claudeAppSupported() + TTY check
141
+ R->>R: isConcurrentLiveSession()? -> abort if live
142
+ R->>R: hasStaleSession()? -> recoverSession()
143
+ R->>R: pick provider + model (or favorites)
144
+ R->>FS: backupMetaJson() (_meta.json -> .bak)
145
+ R->>R: startServer() on 127.0.0.1:0
146
+ R->>FS: writeRflectrConfig(port) (<uuid>.json + _meta.appliedId)
147
+ R->>FS: writeSessionLock({pid,uuid,port})
148
+ R->>R: setupExitCleanup(uuid)
149
+ R->>App: launchOrRestartClaudeApp()
150
+ U-->>R: Ctrl+C (SIGINT)
151
+ R->>R: waitForShutdown() resolves
152
+ R->>FS: cleanupSession(uuid) (restore .bak, rm config, rm lock)
153
+ R->>App: optionally quitClaudeAppGracefully()
154
+ ```
155
+
156
+ Startup guards (`src/claude-app.ts`):
157
+ - **Interactive-terminal required** — non-TTY aborts (`src/claude-app.ts:84`).
158
+ - **Concurrent session** — `isConcurrentLiveSession()` (lock present + pid alive) aborts with a "stop it with Ctrl+C" message (`src/claude-app.ts:90`; `src/claude-desktop/app-session.ts:76`).
159
+ - **Stale session** — `hasStaleSession()` (lock present + pid dead) triggers `recoverSession()` to clean up a prior crash before proceeding (`src/claude-app.ts:96`).
160
+
161
+ Shutdown ordering (`src/claude-app.ts:232`–`245`): `waitForShutdown()` resolves on SIGINT/SIGTERM, then `cleanupSession(uuid)` runs **before** the optional "close Claude Desktop?" prompt — so the config is restored ASAP and a second Ctrl+C during the prompt finds nothing left to undo (per the inline comment at `src/claude-app.ts:235`).
162
+
163
+ ### Model selection
164
+
165
+ - Providers come from `fetchProviderCatalog({ agent: 'codex-app' })`, filtered by `codexCompatibleProviders(..., 'codex-app')` (`src/claude-app.ts:105`, `:113`). The provider's models are narrowed with `routableModelsForProvider(provider, 'codex-app')` (`providerForClaudePicker`, `src/claude-app.ts:57`).
166
+ - **Single-model mode:** the picked model becomes a one-entry `ServerModelInfo[]` carrying `modelFormat`, `npm`, `apiBaseUrl`, `baseUrl`, `completionsUrl`, `upstreamModelId`, `contextWindow`, and the resolved provider `apiKey` (`src/claude-app.ts:157`–`174`). Credential resolved via `activeProvider.apiKey` or `resolveProviderCredential(id, authRef)` (`src/claude-app.ts:138`–`148`).
167
+ - **Favorites mode:** when `prefs.favoriteModels.length > 0`, a `__favorites__` picker option loads all server models and filters them with `filterServerModelsByFavorites(allModels, favorites)` (`src/claude-app.ts:153`–`155`).
168
+ - Either way, the model list is wrapped in `createGatewayModelCatalog(serverModels, { maskGatewayIds: true })` and handed to `startServer` (`src/claude-app.ts:188`). Discovery-id masking is on so Claude Desktop's competitor-name filtering doesn't hide models (rationale in [`claude-desktop.md`](../../../knowledge/public/guides/claude-desktop.md) troubleshooting).
169
+ - Recent models per provider are persisted (single-mode only) via `savePreferences` (`src/claude-app.ts:206`–`212`).
170
+
171
+ ### App discovery & launch (platform specifics)
172
+
173
+ `findClaudeApp()` (`src/claude-desktop/app-launch.ts:68`):
174
+ - **macOS:** checks `/Applications/Claude.app` and `~/Applications/Claude.app`, then falls back to `mdfind` by bundle id `com.anthropic.claudefordesktop`.
175
+ - **Windows:** checks `%LOCALAPPDATA%\Programs\Claude` and `%LOCALAPPDATA%\Claude` (including `app-*` subfolders) for `Claude.exe`, then falls back to `Get-StartApps` returning a `shell:AppsFolder\<AppID>` URI.
176
+
177
+ `launchOrRestartClaudeApp()` (`src/claude-desktop/app-launch.ts:198`): if the app isn't running, it just opens it; if it is running, it prompts to restart (so the new config is read), quits gracefully (`osascript` on macOS, `CloseMainWindow()` on Windows), waits up to 5s, force-quits Windows PIDs if needed, then reopens. "Is it running?" uses `osascript` (macOS) or PowerShell `Get-Process` / matching-PID checks (Windows) (`isClaudeAppRunning`, `:121`).
178
+
179
+ ---
180
+
181
+ ## Acceptance Criteria
182
+
183
+ - [x] `rflectr claude-app` launches Claude Desktop in 3P gateway mode on macOS and Windows (`runClaudeAppCommand`, `src/claude-app.ts:61`; `claudeAppSupported`, `src/claude-desktop/app-launch.ts:11`).
184
+ - [x] A `<uuid>.json` gateway config is written into the 3P config library with `inferenceProvider: 'gateway'`, a `/anthropic` base URL (no `/v1`), `bearer` auth, and a `'dummy'` key (`buildRflectrConfig` / `writeRflectrConfig`, `src/claude-desktop/app-config.ts:48`, `:58`).
185
+ - [x] `_meta.json` `appliedId` is repointed at the new uuid and an `entries` row is added (`src/claude-desktop/app-config.ts:65`–`71`).
186
+ - [x] `_meta.json` is backed up to `_meta.json.bak` before the patch (`backupMetaJson`, `src/claude-desktop/app-session.ts:43`; called `src/claude-app.ts:181`).
187
+ - [x] A `.rflectr.lock` records `pid`, `startedAt`, `uuid`, and `proxyPort` (`ClaudeSessionLock` / `writeSessionLock`, `src/claude-desktop/app-session.ts:7`, `:28`).
188
+ - [x] On clean exit (Ctrl+C), the original `_meta.json` is restored, the injected config removed, and the lock deleted (`cleanupSession`, `src/claude-desktop/app-session.ts:113`; invoked `src/claude-app.ts:237`).
189
+ - [x] After a crash, a stale session is detected and cleaned up on next run, and `--restore` performs the same recovery (`hasStaleSession` + `recoverSession`, `src/claude-desktop/app-session.ts:67`, `:82`; `--restore` at `src/claude-app.ts:67`).
190
+ - [x] A concurrent live session is detected and the second invocation aborts (`isConcurrentLiveSession`, `src/claude-desktop/app-session.ts:76`; `src/claude-app.ts:90`).
191
+ - [x] Exit-hook cleanup is registered so an abrupt `process.exit` still restores config (`setupExitCleanup`, `src/claude-desktop/app-session.ts:119`; `src/claude-app.ts:204`).
192
+ - [x] Both a single selected model and a favorites-only catalog can drive the gateway (`src/claude-app.ts:153`–`174`).
193
+ - [x] The gateway is the shared `server` gateway serving `/anthropic`, not a bespoke proxy (`startServer` + `createGatewayModelCatalog`, `src/claude-app.ts:183`–`192`).
194
+ - [x] Non-interactive terminals are rejected (`src/claude-app.ts:84`).
195
+
196
+ ---
197
+
198
+ ## Files
199
+
200
+ | File | Role |
201
+ |---|---|
202
+ | `src/claude-app.ts` | Command entry: `runClaudeAppCommand`, help text, picker orchestration, server start, session wiring, shutdown |
203
+ | `src/claude-desktop/app-config.ts` | 3P config paths, `buildRflectrConfig`, `writeRflectrConfig`, `_meta.json` read/write |
204
+ | `src/claude-desktop/app-session.ts` | Lock file + backup/restore: `backupMetaJson`, `restoreMetaJson`, `removeRflectrConfig`, lock read/write, stale/concurrent detection, `cleanupSession`, `recoverSession`, `waitForShutdown`, `setupExitCleanup` |
205
+ | `src/claude-desktop/app-launch.ts` | App discovery + launch/restart/quit per platform: `claudeAppSupported`, `findClaudeApp`, `isClaudeAppRunning`, `launchOrRestartClaudeApp`, `quitClaudeAppGracefully` |
206
+
207
+ ---
208
+
209
+ ## Risks & Known Limitations
210
+
211
+ - **Config-editing exception to the env-only rule.** Unlike every CLI launcher, this surface writes the host application's config file. Reversibility depends entirely on the backup (`_meta.json.bak`) + lock (`.rflectr.lock`) machinery. If the backup or lock is lost, manual recovery (the documented 1P-revert procedure) is required.
212
+ - **Backup granularity.** Only `_meta.json` is backed up; the injected `<uuid>.json` is removed by uuid on cleanup. If `_meta.json` is mutated by Claude Desktop itself between backup and restore, restore overwrites those changes with the pre-session snapshot.
213
+ - **`process.on('exit')` constraints.** The exit hook runs synchronous cleanup; it cannot await async work. A hard kill (`SIGKILL`) bypasses both the SIGINT/SIGTERM handler and the exit hook, leaving a stale session for the next-run / `--restore` recovery to clean up.
214
+ - **Linux unsupported.** `claudeAppSupported()` throws on any non-darwin/non-win32 platform.
215
+ - **No mid-session model switch.** The catalog is fixed at launch; changing models means restarting the session.
216
+ - **Chat tab unavailable in 3P mode** (Anthropic product constraint). Claude in Chrome is also incompatible with a gateway. Both documented in [`claude-desktop.md`](../../../knowledge/public/guides/claude-desktop.md).
217
+ - **Full 1P revert is manual.** rflectr restores the pre-session 3P config but does not return Claude Desktop to Anthropic sign-in; the user guide documents the multi-step manual revert.
218
+
219
+ ---
220
+
221
+ ## Related
222
+
223
+ - [`harnesses.md`](../../../knowledge/private/integrations/harnesses.md) — private integration notes (the host-departure pattern, platform-differences table).
224
+ - [`claude-desktop.md`](../../../knowledge/public/guides/claude-desktop.md) — public setup, gateway cheat sheet, restore-to-1P, troubleshooting.
225
+ - [PRD-009 — Codex Integration](../prd-009-codex-integration/prd-009-codex-integration-index.md) — sibling desktop-app surface (`codex-app`) using the same config-patch + backup/restore-via-lock pattern.
226
+ - [PRD-005 — Local Proxy & Catalog Routing](../prd-005-local-proxy-catalog-routing/prd-005-local-proxy-catalog-routing-index.md) — the proxy/translation machinery the gateway builds on.
227
+ - [PRD-012 — Server Gateway](../prd-012-server-gateway/prd-012-server-gateway-index.md) — the `startServer` gateway that serves `/anthropic` for Claude Desktop.
228
+ - [PRD-001 — CLI Core & Launch Orchestration](../prd-001-cli-core-launch-orchestration/prd-001-cli-core-launch-orchestration-index.md) — the env-only isolation contract this surface deliberately departs from.
@@ -0,0 +1,356 @@
1
+ # PRD-012: Server Gateway *(Retroactive)*
2
+
3
+ > **Status:** Shipped
4
+ > **Priority:** —
5
+ > **Effort:** —
6
+ > **Written:** June 2026
7
+ > **Retroactive:** Yes — written after implementation (rflectr v0.2.7).
8
+ > **Source:** `src/server/*`
9
+
10
+ ---
11
+
12
+ ## Overview
13
+
14
+ Every other rflectr command is short-lived: it starts a proxy, spawns a coding
15
+ agent as a child process, and tears everything down on exit. `rflectr server`
16
+ inverts that model. It runs a **long-lived, foreground HTTP gateway** that
17
+ exposes the same model backends — OpenCode Zen, OpenCode Go, every materialized
18
+ local registry provider, or Claude on Google Vertex AI — behind both an
19
+ **Anthropic-compatible** and an **OpenAI-compatible** endpoint on a single port
20
+ (default **17645**).
21
+
22
+ The gateway is the backend for [PRD-011 Claude Desktop Integration](../prd-011-claude-desktop-integration/prd-011-claude-desktop-integration-index.md)
23
+ and for any tool that can be pointed at a base URL (e.g. THE AI Counsel, OpenAI-compatible
24
+ editor extensions). It reuses the same translation core as the CLI proxies
25
+ (PRD-004 / PRD-005): anthropic-format models forward raw to the provider's
26
+ `/v1/messages`; openai-format models route through the shared Vercel AI SDK
27
+ adapter via `createLanguageModel`. There is no second translation path.
28
+
29
+ `runServerCommand(options)` (`src/server/index.ts:377`) drives an interactive
30
+ wizard (which providers to expose, optional favorites-only catalog, discovery-id
31
+ masking, local vs network bind, server password) and then `startServer()`
32
+ (`src/server/router.ts:76`) listens until `Ctrl+C`.
33
+
34
+ ---
35
+
36
+ ## What Was Built
37
+
38
+ - **`rflectr server`** — foreground gateway over Zen/Go cloud models plus every
39
+ local registry provider, served on one port as both Anthropic and OpenAI APIs
40
+ (`src/server/index.ts:377`, `src/server/router.ts:76`).
41
+ - **`rflectr server --vertex`** — Claude on Google Vertex AI using local gcloud
42
+ Application Default Credentials, no OpenCode key required
43
+ (`src/server/index.ts:290`, `src/server/vertex-config.ts`).
44
+ - **Unified model loading** — `loadServerModels()` merges Zen, Go, and local
45
+ provider models into a single `ServerModelInfo[]`, enriched with reasoning
46
+ metadata (`src/server/index.ts:155`).
47
+ - **Per-endpoint routing** — `handleAnthropicMessages` and
48
+ `handleOpenAIChatCompletions` dispatch by `modelFormat`: anthropic → raw
49
+ forward; openai → SDK adapter with a per-`(model × npm × baseURL)`
50
+ `LanguageModel` cache (`src/server/router.ts:155`, `:242`, `:331`).
51
+ - **Auth gate** — Bearer / `x-api-key` comparison against an optional server
52
+ password; `null` password (local mode) allows all callers
53
+ (`src/server/auth.ts:10`).
54
+ - **Discovery-id masking** — self-inverse provider/model-slug reversal so vendor
55
+ names never appear literally in Claude Desktop / Cowork discovery ids
56
+ (`src/server/vendor-mask.ts:14`).
57
+ - **Provider / favorites filtering** — expose a chosen subset of providers, or
58
+ only favorite models (`src/server/catalog-filter.ts`).
59
+ - **Credential hygiene** — `GET /models` strips `apiKey` from every model entry;
60
+ header values are CR/LF-sanitized (`src/server/router.ts:125`, `:397`).
61
+
62
+ ---
63
+
64
+ ## Goals
65
+
66
+ 1. Serve every configured backend (Zen, Go, local registry providers, Vertex)
67
+ behind **one** local HTTP port that speaks **both** Anthropic and OpenAI wire
68
+ formats.
69
+ 2. Reuse the shared SDK translation core (PRD-004) and upstream-forward helpers
70
+ (PRD-005) — no gateway-specific translation logic.
71
+ 3. Make the gateway safe to expose on a LAN: optional server password, network
72
+ bind opt-in, credential stripping in catalog responses.
73
+ 4. Provide a discovery surface Claude Desktop / Cowork can consume, including
74
+ optional vendor-name masking.
75
+ 5. Offer a zero-OpenCode-key path to Claude via Vertex AI using existing gcloud
76
+ ADC.
77
+
78
+ ## Non-Goals
79
+
80
+ - Process management / daemonization — the server runs in the foreground and
81
+ exits on `Ctrl+C` (`waitForShutdown` in `src/server/index.ts:189`). No
82
+ systemd unit, PID file, or background mode is shipped.
83
+ - TLS termination — the gateway listens over plain HTTP; HTTPS is expected to be
84
+ handled by a front proxy if needed.
85
+ - Accurate cost reporting for non-Anthropic models (inherited limitation from
86
+ the translation layer; Claude clients apply their own pricing table).
87
+ - Rate limiting, request quotas, or multi-tenant key management.
88
+ - Live context-window updates on `/model` switch (see Risks).
89
+
90
+ ---
91
+
92
+ ## Features
93
+
94
+ | # | Feature | Where |
95
+ |---|---------|-------|
96
+ | F1 | Foreground gateway on port 17645, dual Anthropic + OpenAI endpoints | `src/server/router.ts:76`, `src/server/index.ts:450` |
97
+ | F2 | Interactive wizard: start mode, favorites-only, exposed providers, masking | `src/server/index.ts:256`, `src/server/prompts.ts` |
98
+ | F3 | Unified model load (Zen + Go + local providers) with reasoning enrichment | `src/server/index.ts:155`, `:176` |
99
+ | F4 | Anthropic Messages relay (raw forward or SDK adapter by format) | `src/server/router.ts:155` |
100
+ | F5 | OpenAI Chat Completions relay (direct relay or SDK adapter) | `src/server/router.ts:242` |
101
+ | F6 | Gateway alias ids + bidirectional catalog lookup | `src/server/models.ts:114`, `:140` |
102
+ | F7 | Discovery-id masking (self-inverse) | `src/server/vendor-mask.ts:14` |
103
+ | F8 | Bearer / `x-api-key` auth with null-password local mode | `src/server/auth.ts:10` |
104
+ | F9 | `apiKey` stripped from `GET /models` | `src/server/router.ts:125` |
105
+ | F10 | Provider-subset and favorites-only filtering | `src/server/catalog-filter.ts:6`, `:15` |
106
+ | F11 | Vertex AI mode via gcloud ADC | `src/server/index.ts:290`, `src/server/vertex-config.ts` |
107
+ | F12 | Server-password save/reuse, network-bind opt-in | `src/server/index.ts:205`, `src/server/prompts.ts:51` |
108
+
109
+ ---
110
+
111
+ ## Architecture & Implementation
112
+
113
+ ### Request flow
114
+
115
+ ```mermaid
116
+ flowchart TD
117
+ client["Claude Desktop / any tool"] --> ep{"method + path"}
118
+ ep -->|"GET /health"| health["{ ok: true }"]
119
+ ep -->|other| auth{"isAuthorized?"}
120
+ auth -->|no| u401["401 Unauthorized"]
121
+ auth -->|yes| route{"path"}
122
+ route -->|"GET /models"| list["catalog.list() — apiKey stripped"]
123
+ route -->|"GET /anthropic/v1/models"| amodels["formatGatewayAnthropicModels (optional mask)"]
124
+ route -->|"GET /openai/v1/models"| omodels["formatOpenAIModels"]
125
+ route -->|"POST /anthropic/v1/messages"| anth["handleAnthropicMessages"]
126
+ route -->|"POST /openai/v1/chat/completions"| oai["handleOpenAIChatCompletions"]
127
+ anth --> fmt{"model.modelFormat"}
128
+ oai --> fmt
129
+ fmt -->|anthropic| raw["raw forward → {baseUrl}/v1/messages"]
130
+ fmt -->|openai| sdk["createLanguageModel + SDK adapter"]
131
+ ```
132
+
133
+ `routeRequest` (`src/server/router.ts:109`) handles `/health` **before** the auth
134
+ check, then gates everything else through `isAuthorized` before dispatching by
135
+ method + path.
136
+
137
+ ### Server model loading
138
+
139
+ `loadServerModels()` (`src/server/index.ts:155`) calls
140
+ `fetchProviderCatalog({ agent: 'server' })` and assembles one `ServerModelInfo[]`:
141
+
142
+ - **Zen** models, filtered by the registry's `subscriptionFilter` (free-only when
143
+ configured) via `filterZenModelsForServer` (`src/server/index.ts:115`), then
144
+ mapped by `zenGoModelsToServerModels` (`src/provider-catalog.ts:259`).
145
+ - **Go** models, filtered to drop `modelFormat === 'unsupported'` by
146
+ `usableGoModels` (`src/server/index.ts:123`), same mapper.
147
+ - **Local registry providers**, mapped by `localProvidersToServerModels`
148
+ (`src/provider-catalog.ts:228`), each carrying `npm`, `apiBaseUrl`, `baseUrl`,
149
+ `completionsUrl`, `apiKey`, `authType`, and `oauthAccountId`.
150
+
151
+ For Zen/Go, openai-format models get `npm = '@ai-sdk/openai-compatible'` and
152
+ `apiBaseUrl = ${backend.baseUrl}/v1`; anthropic-format models stay raw
153
+ passthrough (no `npm`) — matching the CLI catalog's `zenGoModelToRoute`
154
+ (`src/provider-catalog.ts:273`).
155
+
156
+ Every model is then passed through `enrichServerModelReasoning`
157
+ (`src/server/index.ts:176`), which calls `getReasoningCapabilities` and stamps a
158
+ `defaultEffort` fallback for openai-format models that declare one.
159
+
160
+ ### Catalog & gateway aliases (`src/server/models.ts`)
161
+
162
+ `createGatewayModelCatalog(models, opts?)` (`:140`) builds a bidirectional
163
+ lookup keyed by `model.id` **and** by the exposed gateway alias, so Claude
164
+ clients (which only surface `claude-*` / `anthropic-*` ids) can address a model
165
+ by either form.
166
+
167
+ - `gatewayAliasId(model)` (`:114`) → `anthropic-{provider}__{model}` via
168
+ `aliasModelId` (from `src/proxy.ts`).
169
+ - `exposedGatewayAliasId(model, opts?)` (`:118`) → masked alias when
170
+ `opts.maskGatewayIds`.
171
+ - `gatewayDisplayName(model, opts?)` (`:124`) → `"Model Name"`, or
172
+ `"Model Name (Provider Label)"` when masking is on.
173
+ - `upstreamModelId(model)` (`:159`) → strips a trailing `[1m]` context suffix
174
+ for the wire call.
175
+ - `formatGatewayAnthropicModels` / `formatOpenAIModels` (`:129`, `:174`) build
176
+ the endpoint payloads; `formatAnthropicModelEntry` (`:58`) attaches
177
+ `context_window` / `max_input_tokens` via `resolveContextWindow`.
178
+
179
+ ### Routing — Anthropic messages (`src/server/router.ts:155`)
180
+
181
+ 1. Parse JSON body; look up the model in the catalog (`lookupModel`, `:307`).
182
+ 2. **anthropic format** (`:176`): validate `baseUrl` is `http(s)://`, compute
183
+ `{baseUrl}/v1/messages` (or the cloud backend's URL via `backendFor`, `:322`),
184
+ forward the body verbatim — swapping in `upstreamModelId(model)` and relaying
185
+ the inbound `anthropic-beta` header — through `postJsonUpstream`
186
+ (shared with PRD-005's `upstream-forward.ts`).
187
+ 3. **openai format** (`:192`): guard with `isSdkMigratedNpm(model.npm)`; init or
188
+ reuse a cached `LanguageModel` (`getOrInitLanguageModel`, `:331`);
189
+ `sdkTranslateRequest` → `streamAnthropicResponse` (SSE) or
190
+ `generateAnthropicResponse`. The response `model` field is set to the masked
191
+ display name when masking is on (`getResponseModelId`, `:363`) so Claude
192
+ Desktop's status chip shows a human-readable name.
193
+
194
+ ### Routing — OpenAI chat completions (`src/server/router.ts:242`)
195
+
196
+ - `supportsDirectOpenAIChatCompletions(model)` (`src/server/models.ts:165`) is
197
+ true for openai-format models with a `completionsUrl` or a Zen/Go backend — those
198
+ relay raw through `relayAnthropicMessages` to `{completionsUrl|backend}/v1/chat/completions`.
199
+ - Otherwise the request goes through the SDK adapter: `translateOpenAiRequest` →
200
+ `streamOpenAiResponse` / `generateOpenAiResponse` (`src/openai-adapter.ts`).
201
+
202
+ ### LanguageModel cache (`src/server/router.ts:331`)
203
+
204
+ `getOrInitLanguageModel` keys the cache on
205
+ `providerId/sourceBackend ∣ id ∣ upstreamModelId ∣ npm ∣ baseURL` (joined with
206
+ `\x1f`) so a given model is instantiated once per process and reused across
207
+ requests.
208
+
209
+ ### Auth (`src/server/auth.ts`)
210
+
211
+ `isAuthorized(request, serverPassword)` (`:10`) returns `true` immediately when
212
+ `serverPassword === null` (local mode). Otherwise it accepts a `Bearer` token
213
+ (`extractBearerToken`, `:19`) **or** an `x-api-key` header, each passed through
214
+ `sanitizeCredential` (first non-empty line only, `:4`). In **network mode** the
215
+ wizard requires a server password; it is the only gate once the port is reachable
216
+ beyond localhost, so it must be treated as a real secret. Incoming header values
217
+ are CR/LF-stripped in `sanitizeIncomingHeaderValue` (`src/server/router.ts:397`).
218
+
219
+ ### Vendor masking (`src/server/vendor-mask.ts`)
220
+
221
+ `maskGatewayModelId(aliasId)` (`:14`) reverses the provider-slug and
222
+ model-suffix segments of `anthropic-{provider}__{model}`. It is **self-inverse**
223
+ — `unmaskGatewayModelId` (`:24`) calls the same function. The masked catalog
224
+ registers all of `model.id`, the masked alias, and the raw alias so chat
225
+ requests resolve regardless of which id the client sends
226
+ (`createGatewayModelCatalog`, `src/server/models.ts:146`).
227
+
228
+ ### Vertex mode (`src/server/index.ts:290`, `src/server/vertex-config.ts`)
229
+
230
+ `runVertexServerCommand` exposes **Claude on Vertex AI** without an OpenCode key:
231
+
232
+ - `buildVertexRuntimeConfig(env?)` (`vertex-config.ts:107`) resolves project
233
+ (`ANTHROPIC_VERTEX_PROJECT_ID` → `GOOGLE_CLOUD_PROJECT` → `GOOGLE_VERTEX_PROJECT`)
234
+ and location (`GOOGLE_CLOUD_LOCATION` → `CLOUD_ML_REGION` → `GOOGLE_VERTEX_LOCATION`
235
+ → `global`); returns `null` if no project is set.
236
+ - `hasApplicationDefaultCredentials()` (`:66`) checks
237
+ `GOOGLE_APPLICATION_CREDENTIALS` or `~/.config/gcloud/application_default_credentials.json`.
238
+ - `vertexModelsToServerModels(config)` (`:118`) builds `ServerModelInfo[]` routed
239
+ through `@ai-sdk/google-vertex/anthropic` (`VERTEX_ANTHROPIC_NPM`,
240
+ `modelFormat: 'openai'`, `sourceBackend: 'vertex'`).
241
+ - `createVertexModelCatalog(models)` (`:159`) adds short aliases (`sonnet` /
242
+ `haiku` / `opus`) and `[1m]` context variants, resolving client lookups via
243
+ `vertexClientModelLookupCandidates` (`:139`). Defaults: `claude-sonnet-4-6`,
244
+ `claude-opus-4-6`, `claude-haiku-4-5`; overridable at
245
+ `~/.rflectr/vertex-models.json`.
246
+
247
+ The Vertex server starts with `apiKey: 'vertex-local'` and passes a `vertex`
248
+ config (`{ project, location }`) to `startServer`, which threads it into
249
+ `createLanguageModel` (`src/server/router.ts:331`, `:348`).
250
+
251
+ ---
252
+
253
+ ## API Surface
254
+
255
+ Base URLs for clients:
256
+
257
+ - Anthropic: `http://127.0.0.1:17645/anthropic`
258
+ - OpenAI: `http://127.0.0.1:17645/openai/v1`
259
+
260
+ > Do **not** append `/v1` to the Anthropic base URL — the Anthropic SDK adds API
261
+ > paths itself.
262
+
263
+ | Method + path | Purpose | Source |
264
+ |---|---|---|
265
+ | `GET /health` | Liveness `{ ok: true }` (pre-auth) | `src/server/router.ts:114` |
266
+ | `GET /models` | Raw catalog, `apiKey` stripped | `src/server/router.ts:124` |
267
+ | `GET /anthropic/v1/models` | Anthropic-format list (optionally masked) | `src/server/router.ts:129` |
268
+ | `GET /openai/v1/models` | OpenAI-format list | `src/server/router.ts:134` |
269
+ | `POST /anthropic/v1/messages` | Anthropic Messages relay | `src/server/router.ts:139` |
270
+ | `POST /openai/v1/chat/completions` | OpenAI Chat Completions relay | `src/server/router.ts:144` |
271
+
272
+ `POST /anthropic/v1/messages` honors `stream` (SSE when true), supports both
273
+ streaming and non-streaming for anthropic-format (raw forward) and openai-format
274
+ (SDK adapter) models, and relays the inbound `anthropic-beta` header on raw
275
+ forwards. Unknown / unsupported models return `400`; upstream/SDK errors surface
276
+ as `502`.
277
+
278
+ ---
279
+
280
+ ## Acceptance Criteria
281
+
282
+ - [x] `rflectr server` starts a foreground HTTP gateway on port 17645 serving
283
+ both `/anthropic` and `/openai/v1` endpoints (`src/server/index.ts:450`,
284
+ `src/server/router.ts:76`).
285
+ - [x] `loadServerModels()` merges Zen, Go, and local registry provider models
286
+ into one `ServerModelInfo[]` (`src/server/index.ts:155`).
287
+ - [x] Local providers are appended carrying `npm` / `apiBaseUrl` / `baseUrl` /
288
+ `completionsUrl` / `apiKey` (`src/provider-catalog.ts:228`).
289
+ - [x] `handleAnthropicMessages` raw-forwards anthropic-format models to
290
+ `{baseUrl}/v1/messages` (`src/server/router.ts:176`).
291
+ - [x] openai-format models route through the `isSdkMigratedNpm` guard →
292
+ `createLanguageModel` + `streamAnthropicResponse` /
293
+ `generateAnthropicResponse` (`src/server/router.ts:192`).
294
+ - [x] `GET /models` strips `apiKey` from every entry (`src/server/router.ts:125`).
295
+ - [x] Auth accepts `Bearer` or `x-api-key`; `null` password allows all callers
296
+ (`src/server/auth.ts:10`).
297
+ - [x] Discovery-id masking reverses provider/model segments and is self-inverse
298
+ (`src/server/vendor-mask.ts:14`).
299
+ - [x] Provider-subset and favorites-only filtering are available in the wizard
300
+ (`src/server/catalog-filter.ts`, `src/server/index.ts:405`).
301
+ - [x] `rflectr server --vertex` exposes Claude on Vertex AI via gcloud ADC with
302
+ no OpenCode key (`src/server/index.ts:290`, `src/server/vertex-config.ts:66`).
303
+ - [x] Per-`(model × npm × baseURL)` `LanguageModel` cache reuses instances across
304
+ requests (`src/server/router.ts:331`).
305
+ - [x] `/health` is reachable without auth (`src/server/router.ts:114`).
306
+ - [x] Network mode requires a server password before binding to `0.0.0.0`
307
+ (`src/server/index.ts:205`, `:394`).
308
+
309
+ ---
310
+
311
+ ## Files
312
+
313
+ | File | Role |
314
+ |------|------|
315
+ | `src/server/index.ts` | Command entry, wizard, `loadServerModels`, reasoning enrichment, Vertex command, startup output |
316
+ | `src/server/router.ts` | HTTP server, routing, Anthropic + OpenAI handlers, LanguageModel cache, header sanitization |
317
+ | `src/server/models.ts` | `ServerModelInfo`, catalog builders, gateway aliases, display names, endpoint payload formatters |
318
+ | `src/server/auth.ts` | `isAuthorized`, `extractBearerToken`, `sanitizeCredential` |
319
+ | `src/server/catalog-filter.ts` | Provider-subset / favorites filtering, provider summary |
320
+ | `src/server/provider-select.ts` | Interactive exposed-providers picker |
321
+ | `src/server/vendor-mask.ts` | Self-inverse discovery-id masking |
322
+ | `src/server/vertex-config.ts` | Vertex runtime config, ADC detection, Vertex model catalog |
323
+ | `src/server/prompts.ts` | Wizard prompts (start mode, listen mode, password, masking, favorites) |
324
+ | `src/provider-catalog.ts` | `zenGoModelsToServerModels`, `localProvidersToServerModels` (shared) |
325
+ | `src/upstream-forward.ts` | `postJsonUpstream`, `relayAnthropicMessages` (shared with proxy) |
326
+
327
+ ---
328
+
329
+ ## Risks & Known Limitations
330
+
331
+ - **No TLS / no daemonization.** Plain HTTP, foreground only. A LAN deployment
332
+ relies entirely on the server password as its sole access gate.
333
+ - **Server password is the only network gate.** Once bound to `0.0.0.0`, any
334
+ caller with the password reaches every exposed provider's upstream key. Treat
335
+ it as a real secret.
336
+ - **Cost display inaccurate for non-Anthropic models** — Claude clients apply
337
+ their own pricing table; the gateway cannot correct it.
338
+ - **Context window reflects launch state.** Discovery payloads carry a static
339
+ `context_window`; a live `/model` switch in a Claude client does not refresh it.
340
+ - **OAuth-only local providers** with no stored key are skipped upstream of this
341
+ gateway (PRD-002), so they never appear in the catalog.
342
+ - **Vertex auth beyond ADC** (impersonation, workload identity) is not handled —
343
+ only `GOOGLE_APPLICATION_CREDENTIALS` or the default ADC file are detected.
344
+ - **`::ts::` / `[1m]` string conventions** are inherited from the translation and
345
+ alias layers; the same edge-case caveats apply.
346
+
347
+ ---
348
+
349
+ ## Related
350
+
351
+ - [PRD-002: Provider Registry](../prd-002-provider-registry/prd-002-provider-registry-index.md) — local provider discovery feeding `localProvidersToServerModels`.
352
+ - [PRD-003: Model Discovery & Classification](../prd-003-model-discovery-classification/prd-003-model-discovery-classification-index.md) — `ModelInfo` source for `zenGoModelsToServerModels`.
353
+ - [PRD-004: Translation Layer](../prd-004-translation-layer/prd-004-translation-layer-index.md) — the shared SDK adapter (`createLanguageModel`, `streamAnthropicResponse`).
354
+ - [PRD-005: Local Proxy & Catalog Routing](../prd-005-local-proxy-catalog-routing/prd-005-local-proxy-catalog-routing-index.md) — shared `upstream-forward.ts` and `aliasModelId`.
355
+ - [PRD-011: Claude Desktop Integration](../prd-011-claude-desktop-integration/prd-011-claude-desktop-integration-index.md) — primary consumer of this gateway.
356
+ - Knowledge: [Server Gateway (private)](../../../knowledge/private/infrastructure/server-gateway.md) · [API Server guide (public)](../../../knowledge/public/guides/api-server.md)
@@ -1,19 +1,19 @@
1
- ---
2
- ai_description: |
3
- Contains PRD folders actively being implemented. A folder lives here
4
- from the moment implementation begins until the work ships.
5
- Structure inside is identical to backlog/: prd-<###>-<slug>/index + sub-PRDs + qa/.
6
- To promote: move entire prd-<###>-<slug>/ folder to completed/.
7
- Do NOT create new PRD folders here; create them in backlog/ first,
8
- then move to in-work/ when implementation starts.
9
- human_description: |
10
- PRDs currently being implemented. Do not start new PRDs here —
11
- create them in backlog/ and move the folder here when work begins.
12
- When work ships, move the entire folder to completed/.
13
- ---
14
-
15
- # Requirements — In Work
16
-
17
- PRDs currently being implemented. Folder location = lifecycle state.
18
-
19
- Move an entire `prd-<###>-<slug>/` folder **from** `backlog/` → here when implementation starts, and **from** here → `completed/` when the work ships.
1
+ ---
2
+ ai_description: |
3
+ Contains PRD folders actively being implemented. A folder lives here
4
+ from the moment implementation begins until the work ships.
5
+ Structure inside is identical to backlog/: prd-<###>-<slug>/index + sub-PRDs + qa/.
6
+ To promote: move entire prd-<###>-<slug>/ folder to completed/.
7
+ Do NOT create new PRD folders here; create them in backlog/ first,
8
+ then move to in-work/ when implementation starts.
9
+ human_description: |
10
+ PRDs currently being implemented. Do not start new PRDs here —
11
+ create them in backlog/ and move the folder here when work begins.
12
+ When work ships, move the entire folder to completed/.
13
+ ---
14
+
15
+ # Requirements — In Work
16
+
17
+ PRDs currently being implemented. Folder location = lifecycle state.
18
+
19
+ Move an entire `prd-<###>-<slug>/` folder **from** `backlog/` → here when implementation starts, and **from** here → `completed/` when the work ships.