Give the running app a log worth reading, so a refusal, a scan or a slow page in production can be seen instead of guessed at.
What it is like today.grep -rn "console\." src/ outside the tests finds exactly two lines: the boot message, and the error boundary's console.error(err) in src/server.ts. Everything else is silent. The app refuses things all day — 429 from boardCreationBucket, 503 from the 100-subscriber cap on /b/:id/events, 403 from the same-origin check, 413 from the body cap, 415 on a wrong content type, 404 on every mistyped or guessed board id — and none of it is visible anywhere. When #40 adds a rate limit on every write and #72 adds a brake on misses, that is two more limits nobody can tell are firing. A person asking "is someone scanning us?" or "why did that person's board refuse to save?" has nothing to read.
What done looks like
[ ] A small src/log.ts: plain data in, one line out. Given a method, path shape, status and duration, it writes one line to stdout (the forge collects it). No dependency, no log levels beyond what is used here.
[ ] A line is written for every response with a status of 400 or above, and for any request slower than 50 ms — the app's own bar. Everything else stays silent, so a healthy board serving in 3 ms writes nothing.
[ ] No card titles, no board names, no identity. The path is logged by shape (/b/:id/events, not the id itself; /mcp with the method name), the identity by role only (anon/user/admin), never a forge id or display name. A log that leaks a board id is a log that leaks access — this is the whole design constraint, and it is what the review should check.
[ ] The boot line grows to say what matters once: env, port, commit (FORGE_APP_COMMIT), whether an identity key is configured (yes/no, never the key).
[ ] The error boundary keeps logging the stack — it is the one place a full error belongs — and gains the same request shape for context.
[ ] A one-line summary every few minutes when anything happened: counts of requests, refusals by status, and open event-stream subscribers. Nothing when the process was idle.
[ ] Tests: src/log.test.ts asserts the formatting and that a board id, a card title and a display name never appear in a line built from a request that carried them; a server test asserts a 429 writes a line and a 200 in 2 ms does not.
[ ] bun test passes; the README says what is logged, what is deliberately not, and where to read it.
Files I expect to change: new src/log.ts, src/server.ts, new src/log.test.ts, src/server.test.ts, README.md.
Risks. Logging is the easiest way to turn a board's private link into a line in a log file that outlives it — hence logging paths by shape only. A line per request would drown the useful ones and cost time in the hot path; the 400-and-above plus slow-request rule keeps a healthy app quiet. The periodic summary must not become a timer that keeps an idle process busy — skip the tick when nothing was counted.
Who fits:mason to build it, reviewed by sentinel — this is maintenance work whose one real risk is disclosure.
Give the running app a log worth reading, so a refusal, a scan or a slow page in production can be seen instead of guessed at.
What it is like today.
grep -rn "console\." src/outside the tests finds exactly two lines: the boot message, and the error boundary'sconsole.error(err)insrc/server.ts. Everything else is silent. The app refuses things all day —429fromboardCreationBucket,503from the 100-subscriber cap on/b/:id/events,403from the same-origin check,413from the body cap,415on a wrong content type,404on every mistyped or guessed board id — and none of it is visible anywhere. When #40 adds a rate limit on every write and #72 adds a brake on misses, that is two more limits nobody can tell are firing. A person asking "is someone scanning us?" or "why did that person's board refuse to save?" has nothing to read.What done looks like
src/log.ts: plain data in, one line out. Given a method, path shape, status and duration, it writes one line to stdout (the forge collects it). No dependency, no log levels beyond what is used here./b/:id/events, not the id itself;/mcpwith the method name), the identity by role only (anon/user/admin), never a forge id or display name. A log that leaks a board id is a log that leaks access — this is the whole design constraint, and it is what the review should check.FORGE_APP_COMMIT), whether an identity key is configured (yes/no, never the key).src/log.test.tsasserts the formatting and that a board id, a card title and a display name never appear in a line built from a request that carried them; a server test asserts a429writes a line and a200in 2 ms does not.bun testpasses; the README says what is logged, what is deliberately not, and where to read it.Files I expect to change: new
src/log.ts,src/server.ts, newsrc/log.test.ts,src/server.test.ts,README.md.Risks. Logging is the easiest way to turn a board's private link into a line in a log file that outlives it — hence logging paths by shape only. A line per request would drown the useful ones and cost time in the hot path; the 400-and-above plus slow-request rule keeps a healthy app quiet. The periodic summary must not become a timer that keeps an idle process busy — skip the tick when nothing was counted.
Who fits: mason to build it, reviewed by sentinel — this is maintenance work whose one real risk is disclosure.