Known problems, in the order a new engineer should care about them. Every entry names where it is and how to see it. Nothing here is a hunch — a concern without a reproduction does not belong on this list.
The situation. A person in a live room says "also research pricing while you
keep counting". On the hosted (Convex) deployment the model is told that
add_goal is a legal answer, so the room can carry a second workstream. On the
local Node server the same sentence reaches a prompt that never lists add_goal
among the allowed shapes, so the model cannot return it — even though the shared
parser in src/core/steering.ts handles add_goal and agentOsGoalKind
classifies it.
Where. convex/openai.ts:interpretHumanSteer lists
{"kind":"add_goal",…}; src/live/pipeline.ts:interpretHumanSteer does not.
The agent-turn prompts have drifted the same way (the Node one carries a "Rules
of the room" block the Convex one condenses).
Why it is still here. Unifying the prompt text changes what a model says on at least one backend, which is behavior work rather than structural work, and deciding which wording is correct needs a live API key and a measured comparison. Wave 3 does not mix the two kinds of change.
Fix shape. Move both system prompts into a src/core/prompts.ts, pick one
wording, and re-measure with scripts/model-eval.mjs before and after.
.github/workflows/ci.yml runs npm run check and npm run check:client as
separate steps and does not call npm run doctor, so the Convex typecheck added
in Wave 3 runs locally but not on a pull request. Since convex/ now imports
src/core/*, a break in that boundary would reach main before anyone noticed.
One line in the workflow fixes it.
src/core/numberWords.ts:extractNumber answers "what number did this speaker
just say"; src/core/steering.ts:parseLeadingNumberPhrase parses a count
command and reports where the phrase ended. They share the lexicon after Wave
3 but not the implementation, because their contracts differ at the edges: the
command parser accepts "a hundred", the listener accepts "zero". Merging them
changes behavior on both paths to save about twenty lines. Left as two named,
documented functions.
It is the largest file in the repo and holds the whole live-room page: lobby, room header, transcript, state drawer, V3 policy panel and the agent roster. jscpd finds only one 10-line clone inside it, so it is long rather than duplicated, but it is the file a newcomer will find hardest to change. Splitting it is real work with real regression risk and no failing test to anchor it; it was not attempted in a reduction pass.
src/live/roomServer.ts keeps rooms in a module-level Map. Restarting the
server loses every live room, and two server processes do not share rooms. This
is correct for the local demo — the hosted path exists precisely because durable
state needs a database — but it is not a scaling story and should not be read as
one.
promotion/PROMOTION_LOG.md lists D2 (the Invite control is clipped past
the viewport at 390 px, measured right edge 399 against a 390 px viewport), D3
(the "Shared goal" textarea and the join-code input have no programmatic label,
so a screen-reader user hears an unnamed text field twice) and D4 (the
walkthrough takes about 125 s to reach a result the API produces in 13.8 ms).
These belong to the product loop, not to codebase reduction, and Wave 3
deliberately did not touch them.
badgeVariants, buttonVariants, BadgeProps, ButtonProps, InputProps,
SelectProps in src/client/components/ui/. These files are generated by
npx shadcn add; trimming their exports makes the next generation conflict.
playwright is reported as an unlisted dependency for the two capture scripts
and is declared in knip.json ignoreDependencies, because it is installed
--no-save by design.