Foreground still skips the heavy overview stack, but overlaps a timed
gateway so official evidence can merge when it finishes in budget.
Co-authored-by: Cursor <cursoragent@cursor.com>
Web consult already computed Yoga, Arudha, KP, and related layers, then
stripped them before the spoken model. Keep those executed structures in
toModelOutput so answers cannot invent degrees from parametric knowledge.
Co-authored-by: Cursor <cursoragent@cursor.com>
The activity-row assertion still expected a one-line ChatMessageContent call, which cancelled the staging gate.
Co-authored-by: Cursor <cursoragent@cursor.com>
The homepage was waiting on a full chart plus four extra engines, so the card timed out and showed the failure copy on every visit.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep comparative tables in chat, but hide the long audit behind a collapsed control so the reply stays readable.
Co-authored-by: Cursor <cursoragent@cursor.com>
Adjacent minutes still cannot confirm a unique birth minute. A scored
representative time can now be accepted, and the copy must say the minute
is not unique.
Co-authored-by: Cursor <cursoragent@cursor.com>
Manual SKILL.md updates were blocked by registry sha256 and a byte-equal versions/ gate. Consult now loads the operator-maintained tree; rectification and personal-report stay hashed.
Co-authored-by: Cursor <cursoragent@cursor.com>
Web was round-robinning missing domains and waiting to score until the user said they had no more events. Server follow-up now uses the eight-method plan, rescored snapshots stay candidates, and D9/D10 observations never become user labels.
Co-authored-by: Cursor <cursoragent@cursor.com>
SQL kinds now match the TypeScript ledger so batch ingest can confirm dated events. Confirm no longer burns the opening focus, recap uses server date labels, and the Agent sees indistinguishable width instead of a fake unique minute.
Co-authored-by: Cursor <cursoragent@cursor.com>
Web answers were thinner than a local Agent calling yinduzhanxing-skill:
theme-subset vargas, no visible audit table, and a prompt that dropped the
invocation contract. Bind the commercial method, compute D1–D60 plus Western
layers, and deliver the same Full-Spectrum checklist.
Co-authored-by: Cursor <cursoragent@cursor.com>
Activating the skill answered with the entrypoint plus a flat listing of every
file in the package, and nothing carried over between requests: the agent is
rebuilt per request, no thread memory is configured, and the replayed history is
plain question/answer text. So the model re-activated on every turn and paid for
that listing every time, twice more whenever a retry opened a fresh model loop.
The method is now read from the hash-verified package and bound into the
instructions, and the activation tools are withdrawn while skill_read stays. The
agents also stopped loading from the working-tree view, whose reference listing
no hash covered - only SKILL.md was compared - so what the model sees is finally
what the registry pinned.
Withdrawing an activation the model could forget also removes the failure it
mostly produced: the contract no longer waits on a model action for method, and
the no-birth-time path has no contract left to repair.
Measured against this package: activation 118,352 bytes, bound method 47,289.
Co-authored-by: Cursor <cursoragent@cursor.com>
The evidence field is optional, so reading the mahadasha through it yields
undefined as well as null, which the lookup did not accept. staging's typecheck
was failing on this before this branch touched it.
Co-authored-by: Cursor <cursoragent@cursor.com>
Activating the skill returned 129,651 bytes, of which 99KB was a flat list of
1,592 undifferentiated file paths against 30KB of actual method. The one line
telling the model to open the strict-workflow router sat inside that method,
so no reference was ever opened and every answer was composed from the model's
own background knowledge over server evidence.
The route is already decided server-side and the skill already states which
checklist each route requires, so the selection needs no model turn: read the
mandated sections from the hash-pinned package and hand them to the model with
the evidence they apply to. A route the router declares no checklist for is
reported as such rather than filled in with another route's.
The receipt now reports delivered sections separately from model-initiated
reads, because only one of those is under the model's control.
Co-authored-by: Cursor <cursoragent@cursor.com>
Homepage visits were failing closed whenever the 30s generation missed. Return an evidence-backed card immediately, polish in the background, and remember the day's card locally.
Co-authored-by: Cursor <cursoragent@cursor.com>
xiaoxin's 20-core npm test opened one Docker network per database file and exhausted default address pools. Limit concurrent fixtures and remove unused jyotisha-postgres networks before the gate.
Co-authored-by: Cursor <cursoragent@cursor.com>
The model packet read chart.modules.dasha_boundaries, a key the engine never
wrote, so no answer ever had sub-period boundaries while the receipt still
reported precise timing as allowed. The server now cuts the running mahadasha
into antardashas out of the periods the packet already shows, exposes them as
their own evidence section, and precise timing requires that section.
A run whose calculation succeeded and whose model then wrote nothing was
answered with a fixed apology and billed as completed. It now asks once more
against the cached calculation, and fails with empty_answer—no charge—if that
attempt is silent too.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep skill truth boundaries, but stop asking the model to announce skipped domains or parrot tool JSON.
Co-authored-by: Cursor <cursoragent@cursor.com>
A staging run answered "which year?" with nothing but the model explaining that
its own tool calls had failed. Two causes, one upstream of the other.
The domains parameter accepted any string, so it stated no vocabulary at all
while the skill's methodology names strict-workflow checklists the tool has
never accepted. The model followed the skill, the schema took it, and the call
died in the registry two steps later. Enumerating the accepted values puts the
vocabulary where the model reads it. Aliases stay in the enum: they are a
promise the instructions make and a test pins.
Mastra reports an input-schema rejection by resolving with a validation
envelope rather than throwing, so enumerating alone would have turned those
rejections into tool.completed for calls that never ran. The stream now reads
that envelope for what it is.
Text written before the runtime contract is ready was held rather than dropped,
so a later successful call released the model's narration of its own failures as
the entire visible answer. Dropping it means a run that cannot answer says so.
Co-authored-by: Cursor <cursoragent@cursor.com>
Sending cleared the draft and stored a request id before the server reserved usage, so a refresh left a sent question with no agent run.
Co-authored-by: Cursor <cursoragent@cursor.com>
Report pages now scroll inside the chat shell lock, and admin product save forks a draft or retires a published plan instead of rejecting with a generic constraint error.
Co-authored-by: Cursor <cursoragent@cursor.com>
The rebase also applied staging's fix-version edit to the wrong record,
because every record ends with the same boilerplate line; the follow-up
chip record is back to an uncommitted candidate.
Co-authored-by: Cursor <cursoragent@cursor.com>
Measured use of the three chips above the composer was negligible. They
were also not what they appeared to be: the server looked up a fixed
triplet by session theme and passed it as metadata that overrode
anything the model produced, so the same ten hardcoded sets served every
user regardless of question or chart. That is a plausible reason nobody
pressed them.
Both copies of the per-theme table are gone, reply metadata narrows to
the session title, and the two parse entry points collapse into one now
that they return the same shape. The write schema still tolerates a
suggestions field so a client on the previous bundle does not lose its
message mid-deploy, and stored answers containing the legacy hidden
block are still stripped rather than shown raw.
Co-authored-by: Cursor <cursoragent@cursor.com>
The home screen renders all ten domains from the consultation registry,
but the Agent only ever wrote three of them; the other seven were static
registry prompts dressed up as personalized starting points. The payload
now has to cover every domain in registry order, validated as a set
rather than per item, so a short or misordered answer is rejected whole
instead of silently leaving cards on static copy.
The greeting went the other way. Nothing has rendered it since the hero
note was removed, so it leaves the schema, the prompt, and the client
contract rather than costing tokens for text no one reads.
Ten questions take much longer to generate than three, so the route,
the server generation budget, and the client request deadline all grow
together, and the cache version bump forces existing payloads to be
regenerated once under the new shape.
Co-authored-by: Cursor <cursoragent@cursor.com>
The hero carried three lines saying the same thing: a salutation, a
question, and an Agent greeting that repeated both. Removing the third
line would also have removed the only place where accounts without a
usable birth minute are told that unsupported claims will be disclosed,
so that sentence moves to the topic section subheading, where the reader
is about to pick a question.
The starter loading copy claimed three starting points while the page
renders every domain in the registry, only three of which the Agent
writes.
Co-authored-by: Cursor <cursoragent@cursor.com>
BUG-270 and BUG-271 went out as 5caf47a4 and b5bcbaed. Both said
"本地未提交候选", which is the state that makes the ledger useless for
answering "is this on staging yet".
Co-authored-by: Cursor <cursoragent@cursor.com>
The daily starlanguage card sat on "正在结合你的星盘写今天的星语。" forever for
every account whose birth time was usable. Its effect bailed out on
birthTimeDisplayState(profile), which returns a value precisely when the
birth time is candidate, accepted or confirmed, so the request went out
only for accounts that had nothing to read. The guard predates the Agent
rewrite and was masked by the written-in client fallback that rewrite
deleted. It now gates on personalChartAvailable, the same fact the card
uses to render personal content, and retries once before admitting that
today has no card.
The route stops letting one engine call take the whole card down
silently: /api/chart fails into a named reason like the other four
layers, and the engine and agent budgets leave room for a cold chart and
an observed 30s generation inside the 60s ceiling.
The home also had three greeting implementations. The hero heading drew
from a static pool while the time-aware greeting lived elsewhere and the
Agent's own greeting was overwritten client-side into a field nothing
rendered. createStartGreeting now exposes its salutation and question
halves, the hero uses both, and the served greeting reaches the hero note.
Co-authored-by: Cursor <cursoragent@cursor.com>
The fixture removes the container as soon as startup fails, so every CI
failure reduced to one line: the container is unhealthy. The server's own
account of the refusal is deleted with it, which leaves a reproducible
staging-gate failure undiagnosable from the log alone.
Print the container log before the teardown runs.
Co-authored-by: Cursor <cursoragent@cursor.com>
The daily starlanguage card claimed to be personal but was a four-card
rotation picked by hashing the date and birth place, with the same pool
duplicated as a client fallback. It now collects chart, Vimshottari and
Narayana dasha, D9/D10 and today's transits, hands that evidence to a
dedicated Agent, and keeps the result per account per day in process. A
failed generation says so instead of printing generic advice.
The starter heading is drawn from a pool on each visit, and the
rectification card drops its fine print.
Co-authored-by: Cursor <cursoragent@cursor.com>
A zero-uncertainty exact declaration is accepted server-side as the active
minute, but the account write only answered {ok:true}. Every save path then
kept the draft it submitted, so the first consultation after initialization
asked for unverified_birth_time against an accepted profile and was rejected
with mode_changed before billing.
The account route now returns the status and active minute it derived, and
every profile save adopts that result instead of its own local guess.
Co-authored-by: Cursor <cursoragent@cursor.com>
The button was a sticky child of the scroll container, which reserves
composer-sized bottom padding. Sticky boxes are clamped by their
containing block's content box, so the 12px offset never applied and the
button parked 160px above the composer, on top of the transcript.
Anchor it to the composer wrapper's own top edge instead, so the offset
no longer depends on the --composer-reserve constant.
Co-authored-by: Cursor <cursoragent@cursor.com>
page.tsx changed on staging while this batch waited to land, so the line
numbers in the record no longer resolve. State the base explicitly and tell
future readers to locate Home by name rather than by line.
Co-authored-by: Cursor <cursoragent@cursor.com>
Second collision in one delivery: origin/staging advanced from c8d9ec64 to
e1db5762 while the test suite was running and claimed 256-259, so the three
records in this batch move to 260, 261 and 262 with their cross-references
updated. Titles differ from every remote record, so BUG-254's rule assigns new
numbers rather than merging.
BUG-260's related-records field now states the limit this exposes: searching
for the highest number before appending only holds at the moment of writing
and cannot survive the remote advancing before the push.
Co-authored-by: Cursor <cursoragent@cursor.com>
BUG-257 documents the skills/ COPY collision fixed in the previous commit, as
the workflow requires a fix to land together with its record. It notes the
missing automated regression instead of hiding it: reproducing the defect needs
a real image build, so the entry states the gap and proposes a structural guard.
BUG-258 records that node:22-alpine resolves to Node v22.15.0 while
posthog-node@5.41.0 declares ^20.20.0 || >=22.22.0. Filed as investigating,
not resolved: the version range genuinely does not match and it predates the
Next upgrade, but whether the package actually calls Node 22.22 APIs is
unconfirmed, and both remedies are out of scope for this batch.
Renumbered the React Compiler record from BUG-255 to BUG-256 while rebasing:
origin/staging had taken 254 and 255, and its 255 is an unrelated bug, so
BUG-254's rule applies and a distinct number was assigned rather than merging.
Co-authored-by: Cursor <cursoragent@cursor.com>
Next's output file tracing pulls skills/ into .next/standalone with symlinks
resolved into real directories. The final stage copies the standalone bundle
into /app first, so the later explicit `COPY skills /app/skills` tries to put
the tracked symlink skills/jyotish-vedic-astrology/assets -> ../../assets on
top of a real directory. Whether that is fatal depends on the BuildKit
snapshotter: GitHub's runners tolerate it, Docker 29.1.3 with
containerd-overlayfs refuses with "cannot replace to directory ... with file".
Dropping the traced copy after the build lets the explicit COPY own that tree
deterministically. /app/assets still comes from the standalone bundle, which
outputFileTracingIncludes guarantees, so the symlink resolves.
Verified by building the image and inspecting it: skills/ complete at 1208
files, assets still a symlink that resolves, container boots on Next.js
16.3.1 and serves / with HTTP 200. Reproduced the original failure on the
pre-upgrade commit too, so it predates the Next 16.3.1 upgrade.
Co-authored-by: Cursor <cursoragent@cursor.com>
Diagnosis was unblocked by temporarily installing
babel-plugin-react-compiler and reading its logger events, which the
Rust port swallows. Every failure in Home comes from the compiler, not
from this repository's code: 24 errors under the stable 1.0.0, all
prefixed Todo: (the compiler's own marker for unimplemented syntax) --
13 for try/finally, 9 for throw inside try/catch, 2 for a
non-reorderable MemberExpression. All 13 finally blocks do real cleanup
(clearing a timeout, releasing in-flight guards, resetting loading
flags), so deleting them to please the compiler would trade correctness
for speculative memoization.
Two paths are now closed by measurement rather than assumption. Newer
compiler builds fix the try/finally gap but Home then hits two
consecutive internal Invariant panics, which are compiler bugs. And
project-wide the version makes no difference at all: across 373 files
both versions compile exactly 134 functions, with the same 21 files
failing, so switching implementations buys nothing.
The diagnostic dependency is therefore removed and the lockfile
restored via npm ci. Deliverable is unchanged: config still rolled
back, frontend/src untouched.
Co-authored-by: Cursor <cursoragent@cursor.com>
Decision is to stay rolled back: the round's target was Home, and the
44 functions the compiler does optimize have no measured render benefit
to justify the +2.50% first-load JS. Reopening this should start by
benchmarking those 44, not by flipping the config.
Co-authored-by: Cursor <cursoragent@cursor.com>
The Rust React Compiler in Next 16.3.1 refuses to compile the 2730-line
Home component in frontend/src/app/page.tsx and reports nothing at all:
the build prints the option as enabled, exits 0, and tests stay green
while zero memoization is applied. Six configurations were tried; the
compiler optimized 44 functions elsewhere (including 2 smaller ones in
page.tsx itself) but never Home, and panicThreshold: "all_errors" is a
no-op on the Rust port, so the failing construct could not be located.
The reactCompiler config is therefore rolled back per the task's
stop-loss clause while the Next 16.3.1 upgrade is kept. BUG-255 records
the silent-bailout trap so a future attempt does not mistake "option
enabled and build green" for "compiler actually working". BLOCKED.md
records the two paths out, both of which need authorization: adding
babel-plugin-react-compiler to get diagnostics, or splitting Home.
No business code, test, or CI file was touched.
Co-authored-by: Cursor <cursoragent@cursor.com>
Prerequisite for the Rust React Compiler, which only ships from Next
16.3 onward. package.json changes one line; the 490 lockfile lines are
next itself plus the transitive versions npm re-resolved (sharp,
@img/*, @swc/helpers, and postcss hoisted out of @tailwindcss/postcss).
Verified on this commit: tsc --noEmit clean, next build exit 0, and the
199-file suite at 1592 pass / 0 fail / 0 skipped / 0 todo.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit contains zero hand edits. Running `npm install` in a fresh
worktree rewrites package-lock.json into npm's canonical form, which
produces ~8.6k changed lines of pure noise:
- all 961 `packages` keys re-sorted into npm's lexicographic order
(945 were previously out of spec order)
- 6 entries lose `dev: true` because npm recomputed their dev
reachability: @types/react-dom, escape-string-regexp, js-tokens,
loose-envify, prop-types, react-is
Verified mechanically: 961 packages before and after, no additions, no
removals, and no change to any `version`, `resolved` or `integrity`
field. Isolated into its own commit so the real dependency change that
follows is reviewable instead of being buried in the reordering.
Co-authored-by: Cursor <cursoragent@cursor.com>
The single search field asked people to type a place name and then judge
which of several near-identical results was theirs, which is the one thing
they cannot verify about their own birth record. Province, city and district
are now chosen from the dataset the app already ships, so there is nothing to
type and nothing to disambiguate.
Levels that offer no choice collapse: municipalities show one level, and
prefecture cities without districts stop at the city. A district can be left
as the city centre, which is accurate enough because the chart only needs
coordinates and a timezone.
The timezone is resolved once for the chosen place rather than on every
keystroke, through a dedicated route that both this picker and the Geoapify
path share.
Co-authored-by: Cursor <cursoragent@cursor.com>
A staging consultation asked one question about two domains, career and
wealth. The model planned both, and all four attempts failed identically with
calculation_failed. The server resolves the workflow route from the question
text — an explicit-timing check, then keyword domain_tokens, falling back to
the themes argument only when the text yields nothing — while the frontend
declares strict_workflow_route from the domain it chose. The plan contract then
requires the text-derived route to equal the declared one, and each
RouteContract allows exactly one, so the mismatch became
ConsultationPlanContractError, BadRequest, HTTP 400, workflow_bad_request.
Here "事业" is in the career token list and "财运" is not in the wealth one, so
both calls resolved to career and the wealth call was rejected every time.
Widening the token list would only move the contradiction to the next
phrasing. The frontend sends one Python call per domain with the same question
text, so text routing can agree with at most one domain of a multi-domain plan
and every other domain is refused by construction. Now that 1955ba8c caps the
plan at three domains and merges the per-domain packets into one top-level
contract, two- and three-domain plans are expected to work end to end and this
is what stops them.
Make the server-issued declaration authoritative. declared_workflow_route()
returns the route a complete, version-supported, allowlisted plan declares, and
resolve_route() honours it instead of reading the text; a caller that sends no
plan metadata keeps the text heuristics verbatim, so the MCP strict_workflow
tool and the research callers behave exactly as before. The route packet
records which rule decided, because routing now has two legitimate sources.
This is not a way to silence the 400: the whole packet comes from the declared
domain's RouteDefinition, so the sync steps, the consumer_context required
layers, the evidence packet and the frontend's themes[primary_theme] lookup all
land on the domain that was declared. A call declaring wealth can no longer
execute career and label career evidence as wealth. The contract stays
fail-closed — a route off the allowlist is refused before execution, and
themes, layers, boundary, domains, categories, depth, horizon and precision are
still checked one by one. The surviving resolved_routes check changes meaning
rather than going away: it now asserts the workflow executed what was declared.
The timing prefix "应期与阶段问题:" existed only to inject 应期 so the text
router would agree with the declared route for one domain out of ten. With the
declaration authoritative it fixes nothing and still rewrites the question the
model's answer derives from, so it goes. It influences no other server
behaviour: it matches none of the consumer-context domain regexes, and the
timing route already sets precise_timing_requested.
test_consultation_workflow_domains.py deliberately sent no plan metadata, which
is why this was never caught — its per-domain question happened to route to its
own domain. It now sends the real plan for all ten canonical domains behind one
question whose text routes to career; nine of them fail without this change.
Refs BUG-259.
Co-authored-by: Cursor <cursoragent@cursor.com>
A staging consultation submitted a three-domain plan, calculated all three
successfully in 62.9s, and returned nothing but the ensureFinalResponseText
fallback. The step budget was barely touched, so this is not the exhaustion
c8d9ec64 fixed. toModelDomainPlanContext returns two different shapes: a single
domain flattens the evidence packet to the top level, several domains return only
success, domains and consultations. Every hard output rule in jyotishInstructions
is written against those top-level paths — evidence_contract.answer_policy,
hard_blockers, rectification.boundary, status. None of them resolve in the
multi-domain shape, and under a policy that forbids stating anything the server
evidence does not support, silence is what the instructions ask for.
Merge the packets into one top-level contract shaped exactly like the single
domain one. Merging may only restrict: status takes the worst of ready >
degraded > blocked, hard_blockers and missing_route_layers take the union,
permission booleans need every domain to agree while limitation booleans need
only one, and a field the domains genuinely disagree on is reported as
unresolved rather than decided. available_layers is the one permission-shaped
union, because a layer really was computed for some domain and denying it would
deny real evidence. The natal projection is the same chart for every domain, so
it is hoisted to one copy when the domains agree and left per-domain when they
do not.
The domain cap was six, advertised as six, and could never be paid for. Domains
run sequentially at ~21s each against a cumulative 110s abort signal, so six is
~126s and four leaves nothing to write the answer with. Concurrency is not
available: the Python API is a single GIL-bound ThreadingHTTPServer whose async
work already sits behind a two-worker bounded queue that answers 503 when full.
Derive the cap from the clock instead of choosing it — 110s minus a 45s answer
reserve, divided by 21s, is three — and let the model-facing schema carry that
bound so an unpayable plan is unrepresentable. A caller that builds a plan
without that schema is truncated rather than refused, the loop stops early when
the measured pace says the next domain will not fit, and either way the dropped
domains are disclosed through omitted_domains and the receipt while status
degrades, so a partial answer cannot be presented as complete.
run.failed carried a code and nothing else, so the step durations, step budget
and workflow route recorded by c8d9ec64 were unavailable exactly when a run
needed explaining. Send the same allowlisted receipt run.completed sends,
built through publicConsultationRuntimeSteps so the internal failure code and
model loop diagnostics stay server-side, and never let building it replace the
failure event with a silent close. An agentic run that fails before
streamAgentResponse exists never reached the settle-and-log path either, so the
request-level catch now goes through the same entry point.
Refs BUG-256, BUG-257, BUG-258.
Co-authored-by: Cursor <cursoragent@cursor.com>
A staging consultation calculated the chart and then returned nothing but the
ensureFinalResponseText fallback. The model had made four calls to
run-jyotish-consultation, and two of them never reached a calculation: they set
both domains and theme, which canonicalDomainPlan rejects at execution. The
schema declared those two fields as independent optionals, the description never
mentioned the constraint, and the instructions actively told the model to use
theme for a single-domain retry. Each attempt therefore bought a rule the
contract never stated, and because the throw happens before the step-recording
try/catch, it left no trace in the receipt either.
Make the constraint unrepresentable instead of enforced. The model-facing schema
keeps only question and domains, so Mastra refuses the pair before the tool body
runs; the description states the single-array contract, and the instruction that
advertised theme is gone. canonicalDomainPlan still resolves the single-value
form for callers that build a plan without that schema, and is now exported so
that path has its own tests.
maxSteps and the abort timeout bound the same run but were hard-coded apart. One
calculation takes about 20s against a 110s budget, so time is the binding
constraint and three failed calculations exhaust it whatever the step count. The
budget only has to cover the longest useful shape, so it moves to 8 beside the
timeout with that reasoning recorded, and the recorded step list is sized to
match so an exhausted run cannot truncate its own evidence.
Step exhaustion was only ever inferable by counting events, since finishReason
was recorded nowhere and progressive-disclosure reads never reach the public
stream. Capture it as a closed enum plus a step count, normalizing anything
unrecognized, and log both as controlled fields. Neither may enter the client
receipt, whose step schema is strict and would fail a successful run.
Co-authored-by: Cursor <cursoragent@cursor.com>
A merge on 2026-07-25 (3ca30ed7) left the heading sequence and the body
sequence offset by one, so looking up a bug returned another bug's
symptom and root cause — worse than a miss, because it reads as normal.
The last body in the run was appended to BUG-043, giving that record two.
Rebind by content, not position: every reassignment was confirmed by
checking the body's 用户现象/根因 against the heading. Also drop 11
duplicate tail blocks (7 verbatim-equivalent, 4 strict subsets of the
surviving copy) and fold BUG-221~234 into BUG-020~035, which were the
same bugs recorded twice under two numbering schemes.
Structurally incomplete records 22 -> 0; records 253 -> 240 as 14
duplicates retire. Verified no body line was lost.
Co-authored-by: Cursor <cursoragent@cursor.com>
Second batch from the staging UX audit (BUG-248..253).
- css: expose the 32 palette tokens through @theme. Seven utilities
including text-ink, text-danger and text-warning compiled to no CSS
at all, so 30 call sites had been silently inert (BUG-248)
- chat: move the composer into its own component behind a draft store,
so a keystroke no longer re-renders a 2723-line component, and
persist the draft across reloads (BUG-249)
- chat: load gsap, react-markdown and thinking-orbs on demand. First
Load JS for / drops 549.5 kB to 476.3 kB gzipped (BUG-250)
- chat: route the five in-app destinations through router.push, and
keep the five auth redirects and the bootstrap retry as hard loads
on purpose (BUG-251)
- a11y: announce reply completion, and move the live region out of the
aria-busy subtree that was likely suppressing even the start
announcement (BUG-252)
- docs: give the 27 collided bug ids unique numbers and repair their
inbound references; require search rather than a full read of a
3690-line file (BUG-253)
Verified: tsc, eslint, next build, and 1592 assertions across the 199
non-database test files.
Co-authored-by: Cursor <cursoragent@cursor.com>