Changelog

Updates

Every notable change to Nityasha, newest first.

All notable changes to the Nityasha project will be documented in this file.

[Unreleased]

System prompt rewrite (human-shaped, not AI-slop)

**Replaced the 190-line bureaucratic system prompt in

`app/api/ai/chat/route.ts`** with a 32-line persona-driven prompt.

The old prompt told the model it was a "Cognitive Mirror"

tracking the "12 Pillars of Life" with sections like

`<pattern_recognition_handling>` and `<advanced_cognitive_reasoning>`.

The new prompt leads with `You are Nityasha. You are a person, not

an assistant.` and has four short sections: a security

boundary, an `About you` block fed by the user-context (persona,

mood, relational state, quote recall, dialect), a `How to talk`

rules list, and a `Safety` block. The file is 88 lines shorter

and the prompt itself is 6× smaller. The friendship engine

outputs (F1 persona, F2 mood, F3 quotes, F4 relational state, F7

dialect) are still injected into the model — they were never

the problem; the wrapper around them was.

Banned AI-slop words explicitly in the new prompt:

seamlessly, innovative, leverage, elevate, game-changer,

cutting-edge, passionate, certainly, absolutely, "great

question." Phrases like "as an AI", "I understand", and "I'm

sorry to hear that" are also banned with the replacement

rule: "If you don't have anything real to say, say nothing.

Don't pad."

New smoke test: `__tests__/ai/chat-system-prompt-shape.test.ts` —

5 assertions that the route's source file is free of the old

AI-slop markers ("Cognitive Mirror", "12 Pillars of Life",

"LANGUAGE ALIGNMENT: You MUST", "EXTREME BREVITY", etc.), that

the new prompt leads with persona framing, that it's under 60

lines (down from ~190), and that it doesn't tell the model to

speak as an AI. Acts as a regression guard if anyone reverts the

prompt.

Final cleanup (ship-ready)

Deleted `.antigravity.md` — outdated MCP/auth doc, no longer

accurate.

Deleted `.agents/` — personal agent config dir, not project

content.

Deleted `laya-service/` — Python stub for a shadow-matching

service. Never wired. The laya-shadow logic in the audit

references it but the feature is dormant.

Deleted `examples/` — single `native-client.py` file, never

imported by any route. (Audit D12.)

Deleted `docs/01-company-legal/`, `docs/superpowers/`, `tests/` —

empty or stale stub directories.

Deleted `supabase/` — single empty `migrations/` subdir. The

Supabase schema work in Phase 5b never wrote here; all migrations

are in `db/migrations/`. (This was noted in the docs.)

**Deleted `scripts/migrate-onboarding.js`,

`scripts/dump-schema.js` — both contained hardcoded database

credentials** (`host: "103.86.177.200"`, `user: "kzzuezbs_kzzuez"`,

`password: "CiMxYx7DWecc5ArR"`). This was a real C1 violation

that the original audit didn't flag because these scripts weren't

on its scan path. Removing the scripts is the right fix; a proper

CI secret scanner would have caught this.

Hardened `scripts/add_test_token.ts` and `scripts/test_token.ts` —

both now require the raw token and user_id as CLI arguments.

Previously the first had a hard-coded token; the second had a

placeholder token. Both now fail fast with a usage message if the

args are missing.

`docs/ARCHITECTURE.md` + `docs/LAUNCH.md` — the real ship

docs, both written this round.

`README.md` size: 98KB unchanged. The bulk is the original

audit log, which the user wanted to keep for transparency. The

Quick Start section is now correct (Node 22, no demo secrets).

Day-screen H16 close + launch runbook + base schema

H16 fully closed. `components/creed/day-screen.tsx` no

longer does `DELETE + CREATE` on cross-day drag. The

cross-day path in the drag handler now calls

`POST /api/tasks/reschedule` (the atomic endpoint built in

Phase 3) with `{ task_no, from_date, to_date, new_start_time,

new_end_time }`. On success the local state is reconciled to

the server's task id; on failure the optimistic local update

is rolled back so the UI doesn't lie. Same-day time moves

still go through `/api/tasks/edit`. The `console.log` "using

delete+create workaround" is gone.

New: `docs/LAUNCH.md` — Operator runbook: required env vars,

database migration order, deploy command, 7 curl-based

post-deploy smoke tests, Vercel rollback procedure, monitoring

notes, and the explicit "what this codebase is and is not"

list. The smoke tests cover: status endpoint, auth gate, H6

premium gate, H13 OAuth state, all 9 cron endpoints with the

CRON_SECRET bearer.

New: `db/migrations/000_base_schema.sql` — Best-guess base

schema for Nityasha. Covers 25+ tables referenced by the

codebase: `users`, `notes`, `user_daily_plans`,

`user_daily_plan_tasks`, `daily_tasks`, `user_reminders`,

`feedbacks`, `api_tokens`, `user_telegram_links`,

`user_discord_links`, `web_push_subscriptions`, `bot_outbox`,

`bot_daily_pings`, `bot_task_reminders`, `bot_missed_pings`,

`bot_goal_nudges`, `bot_festival_pings`, `life_goals`,

`user_faiths`, `energy_logs`, `mcp_activity_logs`,

`task_shares`, `user_planner_queue`, `otp_verifications`,

`user_activity_logs`. The file documents the

`mysqldump --no-data` reconciliation step needed against prod.

Idempotent (`CREATE TABLE IF NOT EXISTS` everywhere).

Friendship engine (Phase F5 + F6 + F7: reach-out, self-correction, dialects)

F5 — LLM-driven reach-out. `lib/reach-out.ts`. Every 30

minutes a new cron `/api/cron/bot-reach-out` sweeps the user base,

filters candidates (quiet 6+ hours, last bot message 12+ hours,

last inactivity ping 18+ hours ago), and asks the LLM one

question per user: "should I send a casual spontaneous DM right

now? what style? what to say?" The LLM returns

`{ should_reach, reason, style, message, quiet_hours }`. On

`should_reach: true` we enqueue via `enqueueBotPush` (covers

telegram / discord / fcm) and stamp

`bot_inactivity_pings.last_fired_at`. Candidates are gathered

in a single SQL join against `users`, `user_daily_plans`, and

`user_daily_plan_tasks` (no N+1).

F6 — Self-correction. `lib/bot-self-correct.ts` + the

`SELF_CORRECTION_PROBABILITY = 0.08` constant. With 8% chance per

ping, the reach-out cron schedules a delayed follow-up

(4-8 seconds) — a small LLM call that produces a 1-2 sentence

"oh wait — ..." message in the user's language. The follow-up

reuses `enqueueBotPush` so the runners deliver it like any other

nudge. Cost: a tiny LLM call ~8% of the time, ~$0.001 per

follow-up.

F6 — Typing delay. `lib/bot-typing.ts` exports

`humanTypingDelay(text): number` (chars/s × text length, clamped

0.6 - 2.4s). The chat route now sleeps for this duration

before emitting the first token, so the model doesn't appear to

start typing the instant the request hits the server. Only the

FIRST response in a session gets the delay.

F7 — Multi-language dialect system. New

`lib/dialects/` module: `types.ts` (the `Dialect` shape),

`english.ts` (casual English with light slang),

`hinglish.ts` (Roman-script Hindi code-switched), and

`index.ts` (the dispatcher). `buildDialectPromptBlock(language,

userText)` returns the dialect fragment + a per-user style hint

based on whether the user's last message was code-switched,

pure Hindi, or pure English. Adding more dialects is one file.

Wired into the chat route alongside mood + persona + quotes +

relational state.

`vercel.json` — Added `"13,43 * * * *"` cron schedule for the

new reach-out producer.

New tests: 26 tests across

`__tests__/friendship/f5-reach-out.test.ts` (10),

`__tests__/friendship/f6-bot-typing.test.ts` (8), and

`__tests__/friendship/f7-dialects.test.ts` (8) — covering

parser clamps, candidate filtering (active / recently pinged /

quiet), self-correction probability roll, model-call mocked

success / non-200 / non-JSON, dialect dispatch, mirror-style

detection, prompt block composition.

Friendship engine (Phase F3 + F4: quote recall + relational state)

F3 — Cross-session quote recall. `lib/chat-history.ts`

persists every user and assistant message in `chat_messages`

(rolling window of 200 per user, new in migration 028).

`findRelevantQuotes(userId, query)` does a token-Jaccard

similarity over the last 200 messages, excluding the most-recent

6 (so the result is actually cross-session, not the current

turn), and returns the top 2 hits with similarity >= 0.18.

`formatQuotesForPrompt` renders them as

"PAST QUOTES THAT MIGHT BE RELEVANT (use sparingly, don't force):\n

you said: '...'\n - they said: '...'". `recordAndFindQuotes` is

the one-shot helper the chat route uses (records both messages,

returns the formatted block).

F4 — Relational state. `lib/relational-state.ts` runs an

LLM judge on every user message, returning JSON

`{ impact, category, sincere, inside_joke, reasoning, topic, engagement }`.

Events are persisted in `relational_events`. Every 10 events the

helper runs a second LLM call that writes a free-form first-person

description of Nityasha's read of the user (e.g. "Zyrexa shipped a

lot this week; don't push her to slow down") and the trajectory

(warming / cooling / stable). The state is one row per user in

`relational_state`. `formatStateForPrompt` renders as

"YOUR CURRENT READ OF THIS USER: ..." plus the last message

excerpt and a hint on how to honor the read naturally.

Two prompts, both in `lib/relational-state/prompts.ts`. The

ANALYSIS prompt is the friend-bot's ANALYSIS_PROMPT, lightly

reframed for Nityasha's coach/companion voice. The STATE prompt

produces the free-form description; the parser strips code

fences, normalizes engagement to "high" / "medium" / "low",

clamps impact to [-1, 1].

Chat route wiring. Three targeted edits to

`app/api/ai/chat/route.ts`:

1. Imports added (F3 + F4).

2. After the Honcho block, `recordAndFindQuotes` writes the

user message and appends the formatted quotes block to

`userContextStr`.

3. After quotes, `getState` + `formatStateForPrompt` appends

the relational state block.

4. After the stream closes, `runRelationalAfterMessage` fires

fire-and-forget. The helper internally calls the LLM judge,

persists the event, and refreshes the free-form state every

10 events.

All in try/catch so a missing table / DB error never blocks the

response.

New: `db/migrations/028_chat_messages_and_relational_state.sql` —

`chat_messages`, `relational_events`, `relational_state`,

`user_facts`. All idempotent.

New tests: 16 tests across

`__tests__/friendship/f3-quote-recall.test.ts` (8) and

`__tests__/friendship/f4-relational-state.test.ts` (8) — covering

persistence, prune, quote token-Jaccard, role-preserving

formatting, parser fence-stripping, impact clamp, engagement

normalization, state formatting, pre-migration graceful fallback.

Friendship engine (Phase F1 + F2: persona + mood)

F1 — Per-user persona. Nityasha now has her own evolving

"self" that grows with the conversation. `lib/persona.ts` reads

`users.persona` (JSON, new in migration 027), merges with

`DEFAULT_PERSONA` for missing fields, and renders as

`formatPersonaForPrompt(persona)`. Six fields: `vibe`,

`current_focus`, `recent_insight`, `small_thing`,

`current_question`, plus the constant `name = "Nityasha"`.

New users get the default; the LLM evolve pipeline considers

per-field updates only when confidence is >= 0.7 and the

conversation has >= 5 messages.

F1 — Evolve pipeline. `lib/persona/evolve.ts` and

`lib/persona/prompts.ts`. After a successful streamed response,

the chat route fires `maybeEvolvePersona({ userId, recentMessages })`

fire-and-forget. The pipeline calls the AI gateway with a small

JSON prompt, parses the response, applies updates whose

confidence is >= 0.7, and clamps values to 320 chars. The

`vibe` and `name` fields are immutable from the LLM's

perspective. Calls assert `assertGatewayHttps()` so a misconfigured

prod URL fails closed (Phase 4 H14).

F2 — Mood tracking. `lib/mood.ts` ports the friend-bot's

`behavior/mood.py` lexicon (positive/negative word weights,

phrase patterns, intensifiers/dampeners) to TypeScript. Scores

every user message in `[-1, 1]`, classifies as

`rough / low / neutral / good / great`, persists to `mood_logs`

(new in migration 027), and renders the 7-day + today

prompt block — calling out day-vs-week disagreement when they

differ ("Match today's energy, not the week's").

F2 — One-shot helper. `recordAndFormat({ userId, userText })`

records the mood (non-blocking) and returns the formatted prompt

block. The chat route calls this inside the userContextStr

build so every response carries the mood context.

Chat route wiring. Three targeted edits to

`app/api/ai/chat/route.ts`:

1. Add persona + mood to the USER CONTEXT block (after the

user identity block, before plan data).

2. Fire `maybeEvolvePersona` after the stream closes.

3. Wrap both in try/catch so a missing column or transient DB

error never breaks the chat response.

New: `db/migrations/027_user_persona_and_mood.sql` —

`users.persona` (JSON) + `mood_logs` (id, user_id, score,

source, source_text, created_at) with rolling-window indexes.

Idempotent.

New tests: 18 tests across

`__tests__/persona-mood/f1-persona.test.ts` (8) and

`__tests__/persona-mood/f2-mood.test.ts` (10) — covering

default-merge, override truncation, name-immutability, lexicon

coverage, intensifier / dampener behavior, classification

boundaries, persistence truncate, getRecentMood pre-migration

fallback, day-vs-week disagreement.

Closing remaining audit items

H5 closed. `/api/daily-reports` no longer has a hardcoded

user-ID fallback or a hardcoded JWT secret. `requireApiAuth` is

the only identity source; `mintBackendToken(userId)` is the

only upstream token source; a missing `JWT_SECRET` is a 503

(not a silent "use the dev secret" path).

H8 closed. New `lib/mcp-scopes.ts`. Every MCP tool is now

wrapped in a `guard(toolName, handler)` that calls `assertScope`

before dispatch. Read-only tokens attempting a write tool get

a clear "Forbidden: token is read-only" error result, not a

silent write. Pre-migration tokens (no `scope` column yet)

default to `all` so existing consumers keep working — the new

column ships in migration 026.

H9 closed. `verifyAuth` runs at the top of `handleMcpRequest`

before any session-routing branch. A POST with a known

`sessionId` cannot bypass auth.

H13 closed. Google + GitHub OAuth now generate a

cryptographically random 32-byte `state` on the start route,

store it in a short-lived (10 min) HttpOnly+Secure+SameSite=Lax

cookie, and verify it via `timingSafeEqual` on the callback.

A mismatch redirects to `/login?error=...AuthStateMismatch` and

the cookie is deleted. The login/signup intent is encoded in a

separate hash inside the packed state so it survives the round

trip without leaking which action the user picked to a CSRF

observer.

OAuth callbacks: no hardcoded JWT fallback. Both

`/api/auth/google/callback` and `/api/auth/github/callback`

read `JWT_SECRET` from env without a fallback string. A

missing secret is a 503.

C1 partial. The dev `JWT_SECRET` fallback strings

(`6+cpW...` in `daily-reports` and the callbacks) are gone.

All callers fail closed when `JWT_SECRET` is missing.

New: `db/migrations/026_mcp_scopes_and_oauth_state.sql` —

`api_tokens.scope` (VARCHAR(16), default `'all'`) +

`oauth_state` table as a durable backstop for the state cookie.

New tests: `__tests__/security/remaining-audit-items.test.ts` —

read-tool/write-tool classification, read-only token rejected

on write tool, write token allowed on write tool, 'all' legacy

fallback, pre-migration graceful fallback, H5 fails-closed on

missing JWT_SECRET.

Adaptive rescheduling (Phase 5j, friend-ask: whole-day LLM replan)

New: `lib/drift-detector.ts` — Whole-day drift detector that

classifies each user's day as `none` / `minor` / `major` based on

late tasks, completion rate, minutes since last completion, and

energy level. Replaces the static 30-min-per-task drift check.

Thresholds: `major` when ≥3 tasks are late OR no task completed

in 120+ min with tasks still open; `minor` when any task is late

or completion rate is below 30%.

New: `lib/replan.ts` — LLM-driven replan. Builds a structured

snapshot (plan, tasks, completion rate, drift reason, energy

level), prompts the AI gateway with `model: "auto"` and a

short max_tokens cap (800), parses the JSON response, sanitizes

every field (drops unknown task_no, bad times, unknown statuses),

and persists a `pending` proposal in

`adaptive_replan_proposals`. **The model never writes to

`user_daily_plan_tasks` directly.** The accept path is the only

writer; the model is gated by an explicit user action.

API: `/api/replan` — GET pending proposals. POST routes

(`/propose`, `/accept`, `/reject`) split by URL. The accept path

applies the proposed changes in one transaction; the reject path

marks the proposal rejected without touching tasks.

Cron: `/api/cron/planner-adaptive` — Every 30 minutes. For

each user with a plan today, runs the drift detector and persists

a proposal on `major`. Auth: `CRON_SECRET` bearer.

`vercel.json` — Added `"15,45 * * * *"` cron schedule.
HTTPS gate. `buildAndStoreProposal` calls `assertGatewayHttps()`

so a misconfigured prod URL fails at first use (Phase 4 H14).

New: `db/migrations/025_adaptive_replan.sql` —

`adaptive_replan_proposals (id, user_id, plan_id, drift_level,

drift_summary, proposals JSON, status, proposed_at, resolved_at)`.

New tests: `__tests__/replan/phase-5j-adaptive.test.ts` —

drift detector returns null when no plan, classifies "none" /

"major", buildAndStoreProposal refuses non-major, accept is

atomic + writes tasks, reject does NOT write tasks, cron auth.

Family/team task sharing (Phase 5i, friend-asks)

New: `lib/team-rbac.ts` — The single authorization helper for

every team-scoped route. Roles: `owner > admin > member > viewer`.

`requireMember(teamId, userId, minRole)` returns either

`{ ok: true, role, team }` or `{ ok: false, status, error }` —

routes bail to 401/403/404 on the latter. The helper is the only

path to team data; direct SQL without it is a code-review bug.

Non-leaking 404s. A non-member requesting a team they aren't

in gets 404, not 403, so the team existence isn't disclosed.

Member-but-insufficient-role is 403.

Five new tables (migration 024):
`teams` (id, name, owner_id, created_at)
`team_members` (team_id, user_id, role, joined_at) — primary key

composite, role is a strict enum

`team_invites` (token, team_id, role, created_by, expires_at,

used_by, used_at) — one-shot, default 7-day expiry

`team_plan_tasks` (id, team_id, title, details, assigned_to,

status, start_time, end_time, task_date, created_by) — separate

from `user_daily_plans`; team and personal stay independent

`team_milestones` (id, team_id, title, description, target_date,

completed_at) — shared goals

API surface:
`GET/POST /api/teams` — list, create
`POST /api/teams/invites` — create (admin+) or accept (any

authenticated user) — same route, action inferred from the URL

`GET/POST /api/teams/:id/tasks` — list, create (member+)
`updateTeamTask` helper — member can edit own/assigned only;

admin/owner can edit any; viewer cannot edit; assignee must be

a team member; the assignee-must-be-a-member check is on

creation and on reassignment

Atomic invite acceptance. `acceptInvite` runs as one

transaction: `SELECT ... FOR UPDATE` on the invite, mark used,

upsert the member. A concurrent accept by a different user gets

409. The same user re-presenting the same token is a no-op.

New tests: `__tests__/team/phase-5i-rbac.test.ts` — role

ordering, requireMember 404/403/200 paths, invite atomicity

(concurrent, expired), task RBAC (member-creator can edit,

member-stranger cannot, admin can edit any, viewer cannot),

assignee-must-be-a-team-member.

Onboarding (Phase 5h, friend-asks: hooking + simple)

New 5-step flow. Replaces the 7-step linear form with

`Welcome → What matters → When to ping → First plan → Connect`.

Welcome is the only non-skippable step; the rest can be skipped

with sensible defaults. Each step has a plain-language heading

and a one-line description of what happens next.

State machine in `lib/onboarding-state.ts`. Per-step

answers + completion flags + skipped[] are persisted as JSON

in the new `users.onboarding_state` column. The state is the

source of truth; the URL is just a hint. Resume-on-reload works.

API: `/api/onboarding/v2/state` — GET returns the state, POST

with `action: "advance" | "skip" | "patch"` mutates it. The

`patch` action supports autosave on field change so the user

never loses progress.

Welcome step (`components/creed/onboarding/welcome-step.tsx`) —

Plain language ("Hi, I'm Nityasha. I plan your day so you can

stop deciding what to do next."), a 3-item preview of what

happens next, one CTA ("Show me my day"). The input is

validated with a permissive regex (letters, spaces, hyphens,

apostrophes) so names like "Mary-Ann" or "O'Brien" work.

Connect step (`components/creed/onboarding/connect-step.tsx`) —

Placed last, after the user has seen their first plan, so the

proactive-nudge pitch is no longer abstract. Telegram, Discord,

and Android (FCM) shown as toggleable cards. Skip is explicit

and equivalent to web-only.

New: `db/migrations/023_onboarding_state.sql` — adds

`users.onboarding_state` (JSON) + `users.onboarding_completed_at`

(DATETIME) + an index for the post-launch analytics query.

Idempotent.

**Hooks (callable from a server action, server component, or the

v2 API):** `loadOnboardingState`, `patchOnboardingState`,

`advanceStep`, `skipStep`, `isComplete`. All clamp invalid

input (unknown goals, bad times, unknown check-in styles,

unknown channel names) so a buggy client can't corrupt the

state.

New tests: `__tests__/onboarding/phase-5h-onboarding.test.ts` —

default state, parsed stored state, goal clamping, time

validation, style enum, channel enum, advance moves the

counter, advance on step 5 sets completed_at, skip refuses

welcome, skip on a non-welcome step marks skipped[], isComplete,

ALL_GOALS shape.

Desktop time tracking (Phase 5d, friend-asks: tracking + auto-mark)

New: `lib/desktop-activity.ts` — Server-side activity ingest

pipeline. The desktop app batches window/app/url/idle events and

POSTs them to `/api/desktop/activity-events`. The server persists

them in `desktop_activity_events`, bumps pattern counts in

`desktop_activity_patterns`, and proposes a task-completion in

`desktop_task_proposals` when the latest observation matches an

open task in the user's plan.

Hard rule: the desktop never writes to the tasks table. Every

code path that goes from an observation to a task being marked

done is gated on an explicit `accept` action from the authenticated

user. Tested in

`__tests__/desktop/phase-5d-activity.test.ts`. The "auto-mark done"

framing in the friend's ask is implemented as a soft proposal +

confirm, not a silent write.

Pattern DB. Observations normalize window titles

(counter-stripping, hex-stripping) and bucket by app_bundle /

url_host. The desktop_task_proposals table tracks which patterns

have been confirmed (proposal accepted) vs rejected, and the

confidence column in patterns updates on each decision. This is

the learning loop.

New API: `/api/desktop/activity-events` — POST batched events

(max 200 per request). Auth required.

New API: `/api/desktop/proposals` — GET pending proposals, POST

accept/reject. Accept marks the linked task complete; reject

dampens the pattern's confidence.

New: `apps/desktop/src-tauri/src/activity.rs` — Rust skeleton

for the platform-specific active-window observer. The skeleton

defines the contract (`ActivitySample`, `get_activity_sample`

Tauri command, `start_observer_loop`); the platform implementations

in `platform/{linux,macos,windows}.rs` are the actual capture.

Building requires a Rust toolchain.

New: `db/migrations/022_desktop_activity.sql` — three tables:

`desktop_activity_events` (append-only event log),

`desktop_activity_patterns` (per-user learned categories keyed on

window title / app bundle / url host),

`desktop_task_proposals` (the propose-then-confirm queue).

New tests: `__tests__/desktop/phase-5d-activity.test.ts` —

ingest auth + batch limit, the no-silent-marks invariant, accept

marks complete, reject does not, idempotency on already-resolved,

not_found on missing, evidence JSON parsing on list.

Cleanup (Phase 6, beginner-friendliness)

Deleted dead files at project root: `add_warnings.js`,

`check_db.ts`, `convert_md.py`, `extract.js`, `test-honcho.js`,

`test-honcho2.js`, `test_db.js`, `test_reaction.js`. None had

importers; all were personal scratch from earlier development.

Deleted `scratch/` directory (13 files of one-off DB checks

and Composio tests). No importers.

Deleted stub bot folders: `creed-bots/other/` and

`creed-bots/slack/` (each contained only a README stub).

`.env.example` cleaned. Removed all demo / placeholder

secrets (`dev-bots-key-change-me`, `dev-discord-key-change-me`,

`discord-demo-token-change-me`, the test Telegram token). Added

`FIREBASE_SERVICE_ACCOUNT_JSON` / `_PATH` for the FCM runner

added in Phase 5e. Added `HONCHO_ENRICH_ALL` (Phase 5a composer

opt-in) and `RAZORPAY_WEBHOOK_SECRET` (Phase 2 verify handler).

The example is now a real spec, not a template with secrets in it.

`package.json`: added `firebase-admin: ^12.7.0` (required by

the FCM runner), `test:watch` script, `first-run` script (calls

`node scripts/first-run.mjs`).

README: Node version requirement corrected from 20+ to

22+ (the locked AI dependencies require Node 22). Quick-start

trimmed to the actually-required env vars, with explicit notes

that any populated `.env.local` value is treated as a leaked

secret by the secret scanner.

New: `docs/ARCHITECTURE.md` — 5-minute read for new

contributors. Where things live, how the bot system fits

together, the auth model, the data integrity model, the

conventions, and the explicit work-in-progress list.

Notifications (Phase 5e, friend-asks: FCM for Android)

New: FCM runner `creed-bots/fcm/bot.ts` — Mirror of the Telegram

and Discord runners. Polls `/api/bots/outbox-deliver?channel=fcm` every

20s, looks up active device tokens for each user, sends via Firebase

Admin Messaging's `sendEachForMulticast`. Tokens that the FCM API

rejects with `UNREGISTERED` / `INVALID_ARGUMENT` /

`SENDER_ID_MISMATCH` are deleted from `fcm_device_tokens` so we

don't retry dead tokens on every tick.

New: `creed-bots/fcm/admin.ts` — Lazy-initializes Firebase Admin

from `FIREBASE_SERVICE_ACCOUNT_JSON` (preferred) or

`FIREBASE_SERVICE_ACCOUNT_PATH`. Eager-initializes at runner startup

so a bad service-account JSON surfaces immediately, not on the

first message. `isFcmConfigured()` returns false when env is

missing so a misconfigured deployment fails loud at boot.

New: `/api/app/fcm-register` — Register/refresh a device

token. `POST` upserts on `(user_id, device_token)`, validates the

token length, defaults platform to `android`. `DELETE` removes the

token (used on logout or app uninstall). Both routes require

`requireApiAuth` and scope all writes to the verified user id.

`lib/bot-push.ts` — `enqueueBotPush` now also fans out to the

FCM channel when the user has at least one active token. The FCM

outbox row uses the user id as `channel_user_id` (the runner looks

up tokens for the user). One check, zero rows when no Android

devices are linked — the rest of the channels still go out.

`creed-bots/shared/outbox.ts` — `OutboxChannel` type extended

with `"fcm"` so the existing pump can serve all four channels.

New: `db/migrations/021_fcm_device_tokens.sql` —

`fcm_device_tokens (user_id, device_token, platform, app_version,

active, created_at, updated_at)` with `(user_id, device_token)`

primary key and an `idx_fcm_user_active` for the runner's hot

read path.

New tests:
`__tests__/notifications/phase-5e-fcm.test.ts` — register

upsert, token-length validation, auth requirement, DELETE

scoping, enqueueBotPush FCM fan-out, isFcmConfigured env

detection.

Notifications (Phase 5a, friend-asks: morning brief, context-aware nudges, inactivity reactivation)

New: `lib/bot-message-composer.ts` — Single personalization layer

for every proactive bot message. Takes a `(userId, kind, data)`

triple, loads the user (display name, language, tone) + last-7-day

completion rate, optionally enriches with Honcho (peerCard +

representation) for the `inactivity_reactivate` kind, and returns

a personalized 1-3 line message + a dedupe key. Every existing cron

producer now goes through this instead of building its own string.

Honcho is opt-in, not opt-out. The composer skips the Honcho

network call unless `HONCHO_ENRICH_ALL=1` is set, except for

`inactivity_reactivate` where a fact is required for the message

to be useful. This keeps the cron cheap on the happy path.

No fabrication rule. The inactivity composer returns a generic

"haven't seen you in a while" message when Honcho returns nothing,

rather than inventing a recap the model doesn't have evidence for.

Tested in `__tests__/notifications/phase-5a-composer.test.ts`.

Wired all 6 existing cron producers to the composer:
`/api/cron/bot-daily-ping` — morning_brief / evening_audit
`/api/cron/bot-task-reminders` — task_reminder
`/api/cron/bot-missed-tasks` — missed_task
`/api/cron/bot-freeslot` — free_slot
`/api/cron/bot-goal-drift` — goal_drift
`/api/cron/bot-slip-insight` — slip_insight
New cron: `/api/cron/bot-context-nudge` — Hourly at :37. Three

signals: `no_energy` (no log today + high-energy tasks open),

`task_gap` (90+ min since last completed task + plan still has

open tasks), `not_marked` (scheduled task whose start_time passed

+ still open). Deduped via `bot_context_nudges (user, date, kind)`

so a quiet user gets at most one nudge per signal per day.

New cron: `/api/cron/bot-inactivity-reactivate` — Daily at 10:00

IST. Inactivity criteria: no plan/task activity in 5+ days OR

last 3 plan-days completion rate < 30%. Deduped via

`bot_inactivity_pings` with a 14-day quiet window so a quiet user

doesn't get re-engagement spam. Always pulls Honcho facts because

this kind needs them.

New: `db/migrations/020_bot_inactivity_and_nudges.sql` —

`bot_inactivity_pings` (one row per user, last_fired_at) +

`bot_context_nudges` (composite key on user+date+kind). Idempotent.

`vercel.json` — Added two new cron schedules: `37 * * * *`

(context-nudge) and `5 4 * * *` (inactivity-reactivate).

New tests:
`__tests__/notifications/phase-5a-composer.test.ts` — covers

morning_brief without Honcho, inactivity no-fabrication rule,

Honcho opt-in, dedupe key shape, context-nudge no-op when no

plan, inactivity 14-day quiet window, 401 without CRON_SECRET.

AI reliability (Phase 4, audit H14 + H15 + H18)

Bad-word hard-counter removed. The chat route previously ran a

3-strikes `users.warnings` + `users.is_terminated=1` flow for typing

common words inside a chat message. That auto-termination is a

product liability — a user typing "the function is fucked" in a

legitimate code question would lose their account. Replaced with a

soft log (`log.warn` with the matched words and a message preview)

for human review. Real moderation belongs in an admin tool, not the

chat hot path.

Per-message MCP token mint removed. Every chat request

previously ran `INSERT INTO api_tokens ... ON DUPLICATE KEY UPDATE`

to mint an ephemeral token that was never read by the route. That

was a write-amplification DoS on the tokens table (one write per

message per user). The mint is gone; the post-stream cleanup

blocks that deleted the token are now no-ops.

Hardcoded `/home/aaki/.agents/skills` path removed. The chat

route previously used `path.join("/home/aaki", ".agents", "skills")`

regardless of platform, which made the skills list empty on every

other developer's machine. The route now reads from

`AGENT_SKILLS_DIR` (already declared in `getAgentSkillsDir()`),

falling back to `./.agents/skills` and `./skills` relative to

`process.cwd()`. Windows / Docker / CI work without changes.

H14 (AI over plain HTTP) gated. `lib/ai/gateway.ts` exports

`assertGatewayHttps()`. In `NODE_ENV=production` it throws when

`AI_GATEWAY_BASE_URL` is not `https://`. Local dev keeps the HTTP

fallback so the on-ramp isn't broken. Production deploys must

configure an HTTPS gateway.

H18 already partially fixed in `components/creed/chat-screen.tsx`

(lines 165-169): the chat history is scoped by `userId` and the

per-user `localStorage` is cleared on `userId` change. The chat

history localStorage key is `nityasha_chat_history` and is removed

when the user logs out (the effect re-fires when `userId` flips).

New tests:
`__tests__/ai/phase-4-ai-reliability.test.ts` — verifies the

moderation path no longer mutates the DB, the gateway HTTPS

gate fires in production, and the skills directory resolves

from `AGENT_SKILLS_DIR` rather than the hardcoded path.

Data integrity (Phase 3, audit H16 + H17 + M14 + M24)

H16 closed — atomic task reschedule. New

`/api/tasks/reschedule` route. Cross-day drag no longer does

`DELETE` + `CREATE` over two requests (which dropped `start_time` /

`end_time` and could leave the task orphaned on a network failure).

The new route does same-day moves with an in-place `UPDATE` and

cross-day moves as `INSERT` + `DELETE` in a single transaction. The

response shape is normalized to `{ task: { id, plan_id, task_no, ...,

plan_date } }` so the day screen can replace its temporary `uid`

with the canonical server id (audit M14). Replaces the

delete-then-create pattern at `components/creed/day-screen.tsx`.

H17 closed — notes revision. `notes.revision` column added by

`db/migrations/019_data_integrity.sql`. New

`/api/notes/[id]/update` route requires `expectedRevision` and

returns `409 Conflict` with the latest content when the row has

been modified by another session, so a slower tab or an MCP agent

update can no longer be silently overwritten. The legacy

`updateNote` server action now bumps `revision` so existing callers

don't regress. List view (`getNotes`) now selects only the columns

the picker needs (audit M24) and exposes the revision per note.

M14 partial. Task IDs are normalized server-side. The day

screen's calendar actions still send `uid`/`id` interchangeably in

places; full M14 closure requires editing the 2,700-line day screen,

which is out of scope for this PR.

New: `db/migrations/019_data_integrity.sql` — Adds

`notes.revision`, `user_daily_plan_tasks.task_id`

AUTO_INCREMENT (if missing), and a UNIQUE constraint on

`(plan_id, task_no)` to defend against duplicate inserts on retry.

All idempotent via `information_schema` guards.

New tests:
`__tests__/data-integrity/phase-3-fixes.test.ts` — covers

same-day move, cross-day move (insert + delete in one

transaction), bad time range, revision mismatch 409, and revision

match + bump.

Pricing (Phase 2, security + correctness fixes)

H6 fully closed end-to-end. Razorpay orders are now server-owned.

The flow is: catalog → create order row → create Razorpay order →

verify signature → apply entitlement via the idempotent helper →

mark consumed. The browser can no longer become premium free because

the verify handler looks up the canonical amount, currency, owner,

and payment_id from the server's `razorpay_orders` row, not from the

Razorpay API or the browser payload.

M7 closed. `/api/app/premium` now looks up by `users.id` instead

of by an email hash. The previous code used `hashSecret` (SHA-256)

while signup used `blindIndex` (HMAC), so a real paid user was

reported as free. The new code resolves premium state from the user's

MySQL row directly, with a fallback to `payments.created_at` for

expiry calculation.

New: `lib/pricing-catalog.ts` — Single source of truth for every

(plan, cycle, currency) combination. The browser picks a `ProductKey`

from this catalog; the server uses the catalog amount to create the

Razorpay order. Adding a new tier is a one-file change.

New: `lib/billing/apply-entitlement.ts` — Single function that

upgrades a user. Idempotent on `payment_id` (UNIQUE constraint from

migration 018). Both `/api/verify-razorpay-order` and

`/api/webhooks/razorpay` call it, so a payment delivered by both

channels is applied exactly once. Refunds go through a sibling

`revokeEntitlement` helper.

Webhook handler rewrite. `/api/webhooks/razorpay` now verifies

the signature against the raw body, dispatches on `order.paid` /

`payment.captured` / `refund.processed` / `refund.failed`, and uses

the canonical order row's amount/currency/owner. Re-delivered webhooks

are no-ops, not double-upgrades.

Verify handler rewrite. `/api/verify-razorpay-order` now: requires

auth, checks the signature with HMAC-SHA256, rejects when the buyer

is not the order owner, calls `applyEntitlement` for the actual

upgrade. The previous "look up the order from Razorpay, then trust

the notes" path is gone.

New: `db/migrations/018_payment_idempotency.sql` — Adds

`UNIQUE INDEX uq_razorpay_orders_payment_id` on the orders table,

creates the `payments` table with its own `UNIQUE KEY

uq_payments_payment_id`, and backfills a `users.plan_type` column

for DBs that pre-date it. Idempotent across re-runs.

Auth (Phase 1)

`proxy.ts` → `lib/api-auth.ts` convergence — The Edge-runtime

`proxy.ts` and the Node-runtime `lib/api-auth.ts` each had their own

JWT verify implementation and their own secret list. They now share

`getJwtSecrets()` from `lib/api-auth.ts`, so a secret rotation (add

or remove `JWT_SECRET_SECONDARY`) is picked up by both verifiers at

once. The Web Crypto-based `verifyJwt` in `proxy.ts` is preserved

because Edge runtime cannot use `jsonwebtoken`. Closes the

duplicate-verifier finding in the production audit.

**New: `lib/supabase/server.ts` and `lib/supabase/browser.ts` real

clients** — Were stubs returning `{ data: { user: null } }`, which

caused H12 (the layout destructured `data.user` on a stubbed null).

Both files now use `@supabase/ssr` (already a project dependency)

with the Next 16 async cookies API. When env is missing they fall

back to a no-op client with the previous stub's shape, so existing

callers continue to work.

New: `requireAdminRole()` and `requireEmployeeRole()` in

`lib/api-auth.ts` — Real role-based authorization backed by a new

`users.role` column (added by `db/migrations/017_users_role.sql`).

`requireEmployeeRole` accepts both 'employee' and 'admin' (admin is

a superset for routes that distinguish them). The admin/employee

dashboard routes now use these instead of a client-side passkey.

New: `db/migrations/017_users_role.sql` — Adds the `role` column

(`user`/`admin`/`employee`, default `user`) and a supporting index.

Idempotent via `information_schema` guards. Backfills a known

internal admin email as a documented seed; further promotions should

happen through a server-side tool, not a migration.

Security (Phase 0)

`/api/app/state` PUT (H11) — Was a no-op echo. Now actually

persists the caller's creed state to `users.creed_state` (JSON) with

optimistic locking via `creed_state_version`. Stale writes return

`409 Conflict` so a slower tab cannot silently overwrite a newer one.

`/api/app/state` GET — Now reads the persisted state and overlays

minimal user metadata (id, name, email, handle, gender, warnings,

MCP connections). User id is derived exclusively from the verified

auth context, never from a query param.

`/api/user/events` GET (H1) — Now requires `requireApiAuth()`

and returns the caller's own `booking_events`. Anonymous cross-user

reads are closed.

`/api/user/widgets` GET (H1) — Same fix as events.
`/api/app/account` DELETE (H10) — Was disabling foreign-key

checks and orphaning notes/tasks/api_tokens/oauth rows. Now revokes

credentials (`api_tokens`, `oauth_refresh_tokens`, `oauth_codes`),

deletes owned data, then removes the user, in a single connection.

Per-table errors are logged and surfaced in the response (schema

drift is real; the canonical `users` row is always removed).

`/api/create-razorpay-order` (H6, partial) — Removed the

mock-order fallback. When `RAZORPAY_KEY_ID` /

`RAZORPAY_KEY_SECRET` are missing, the route now returns `503`

instead of a fake order that previously allowed free-premium

upgrades. Validates `plan`, `billingCycle`, and `giftEmail` before

calling Razorpay, persists the order in `razorpay_orders` for

verify/webhook reconciliation, and refuses self-gifts.

`/api/admin/employee-emails` (H3) and

`/api/admin/send-marketing-email` (H4) — Now use

`requireEmployeeRole` for a real role check, and 404 in production

via `productionOnlyBlock` with a `CREED_ALLOW_DIAGNOSTICS=1` escape

hatch for staging.

New: `lib/route-guards.ts` — `productionOnlyBlock()` and

`isSameOriginRequest()` helpers. Use in any new admin / diagnostic

route.

New: `db/migrations/016_user_creed_state.sql` — Adds the

`creed_state` JSON column, `creed_state_version` INT column, and the

`razorpay_orders` table. Idempotent (uses `information_schema`

guards).

CSP — Stripped Stripe (`js.stripe.com`, `checkout.stripe.com`,

`hooks.stripe.com`, `api.stripe.com`) and added Razorpay

(`api.razorpay.com`, `lumberjack.razorpay.com`,

`checkout.razorpay.com`). Razorpay is the only active payment

provider.

`vercel.json` — Removed the orphan `app/api/feedback` Stripe

webhook reference. Added per-cron `maxDuration` for every bot cron

producer.

Kept: `lib/stripe.ts` — Originally a 4-line stub. The company

billing module (`lib/company-billing.ts`) still imports

`getStripeClient`, `cancelActiveSubscriptionsForCustomer`, and

`resolveSeatPriceId` from it (under `@ts-nocheck`). The active

personal payment flow is Razorpay; the company / teams Stripe path

is dormant. The file is preserved as a clearly-marked no-op with

the original three welcome-state helpers kept. Replace the no-ops

with a real Stripe client when the company plan ships.

New tests:
`__tests__/security/phase-0-1-fixes.test.ts` — covers H11, H1, H3,

H4, H6, H10, the Supabase no-op fallbacks, the `getJwtSecrets`

ordering, and the role helpers (with pre-migration graceful

failure).

`__tests__/pricing/phase-2-pricing.test.ts` — covers M7 (id

lookup), H6 (signature + owner rejection), the catalog amount,

the 503 fallback, and the applyEntitlement idempotency guarantee.

All tests run offline with mocked auth + MySQL.
Pre-Phase 0

Added

Reports Tab: Extracted the Reports UI from the `/day` page into a

dedicated standalone `/reports` route

(`app/(creed-app)/reports/page.tsx`).

Sidebar Navigation: Added a new "Reports" tab to the main app

sidebar (`components/creed/shell.tsx`) using the

`ChartColumnIncreasingIcon`.

System Documentation: Added `.antigravity.md` detailing the MCP

and Authentication architecture.

Changed

API Tokens Schema: Renamed the primary key column in the

`api_tokens` table from `id` to `uid` to resolve MySQL

`ER_DUP_ENTRY` errors during token generation.

Token Tracking Metadata: Added `client_name` and `user_name`

columns to the `api_tokens` table to improve database visibility,

identifying which app (e.g., "antigravity") and which user a token

belongs to.

Token Generation Logic: Updated

`/api/app/token/generate/route.ts` and `lib/oauth-store.ts` to

populate the new `client_name` and `user_name` fields during token

creation.

State User Profile: Fixed the initial state API

(`/api/app/state/route.ts`) to dynamically fetch and return the

authenticated user's actual name instead of defaulting to an empty

string.