# Agent Operating Model Source: https://docs.tuturuuu.com/build/development-tools/agent-operating-model Roles, the shared subagent budget, publication and review rules, and the security default for autonomous agent work. Autonomous agent work on Tuturuuu runs as a small, bounded company hierarchy. This page explains the model for people. The repository's canonical rules are the [program orchestration reference](https://github.com/tutur3u/platform/blob/main/plugins/tuturuuu/skills/tuturuuu-agent-coordination/references/program-orchestration.md#operating-model-and-budget) and the self-contained [security default](https://github.com/tutur3u/platform/blob/main/plugins/tuturuuu/skills/tuturuuu-agent-coordination/references/security-default.md). The user-local shared `pstack-t3-workflow` skill is an optional convenience. ## Roles | Role | Who | Responsibility | | - | - | - | | Director | The user or Mastermind | Cross-project priorities and admin custody | | Project integration owner | One orchestrator per project | The only writer of main, release and other shared refs; program board; admission and budget reservations | | Team lead | Delegated child | One disjoint workset, its workers and its receipts | | Worker | Delegated child | Its own work branch and PR only, on an isolated worktree | | Reviewer | A child that did not author the change | Independent review receipt | ## Subagent budget Each thread tree may have up to `agent-allocation: 10` subagents, counting every descendant. This replaces the older five-child ceiling. Ten is a ceiling, not a target. * A project declares its allocation as the lowest of: * ten; * the observed runtime limit when scoped `per-thread-tree`; * resource admission; * any explicit lower allocation. * `observedRuntimeLimit.unit` accepts only `per-thread-tree` or `per-parent`. A tree limit caps the allocation; a per-parent limit caps each parent's live direct children, independently of the tree allocation and descendant grants. Do not relabel an ambiguous session limit without evidence. * Reserved, queued, active, waiting-for-children and blocked agents occupy a slot. Only drained `done` and `retired` agents release theirs. * The owner grants each lead an explicit remaining budget, and the lead's children must fit inside it. Nested leads share their parent's grant. They never get ten new slots. * The hierarchy is at most three subagent levels deep, and only leads delegate. > [!WARNING] > > The T3 runtime does not enforce ten, and no live session is claimed to > support ten. The budget is a repository contract plus a checker that > validates declared allocation files. It cannot configure or enforce T3's > runtime limit. Declare a program's live allocation in its ignored `tmp/agent-coordination//allocation.json`. The plugin reference ships an example. Check it with: ```bash theme={null} node scripts/ci/check-agent-budget.js tmp/agent-coordination//allocation.json ``` The checker rejects: * more live agents than the allocation (11 fails); * a file allocation above `AGENTS.md` or an observed tree limit, direct children above a per-parent limit, or a missing/unknown runtime scope; * lead grants that oversubscribe their parent; * excess nesting; * live agents under finished leads; * a reviewer that is, or descends from, its author; * any subagent that claims integration writes. Without arguments, it confirms that `AGENTS.md`, the plugin reference and this page declare the same allocation. `node scripts/docs-audit.js`, which the Documentation and SEO contracts workflow runs, includes that consistency check. The root script test suite runs the checker's tests. A passing check proves the declared structure only. It cannot see what a live thread tree actually ran. Agent count is not resource admission. ttr validation lanes, free disk, CPU and RAM still limit heavy work. Reduce active WIP when queues grow or disk runs low, and never spawn busywork to fill unused slots. ## Publication, review and retirement * Isolated workers and leads commit and push only their own verified work branch, or open a PR. Main, release and other shared refs move only through the project integration owner. * Publishing from the worker's own worktree means the root checkout's index never blocks branch publication. * The reviewer is never the author. * Leads return compact receipts: the PR, full head SHA, tests with results, residual risk, an explicit blocker (or none) and the resume event. * Once a child's result is on a pushed branch, the parent removes the child's worktree. The pushed branch and its receipt are the recovery point. * Add no new permanent controller, timer or wake, and never revive a retired one. Native result and PR watches plus the existing authorized cadence provide wakes. ## Security by default Tuturuuu is open source and a high-value target. * **Every change:** code, dependency, runtime and workflow changes get focused secret, dependency, SAST and trust-boundary checks. * **Independent review:** authentication, RLS and tenant isolation, credentials, storage delivery, CI/actions, runner execution, package publishing and deployment changes get an independent security review before integration. * **CI trust:** * Untrusted fork code never runs with secrets or write permissions. * Actions and artifacts are pinned, and tokens get least privilege. * Privileged follow-ups check actor and head/branch trust. * Mutations fence duplicates, replays and UNKNOWN outcomes. * **Cadence:** both cadences run only through existing admitted mechanisms. Where one is missing, record the exact gap, its owner and its resume event, and never add a T3 timer. * The daily automated audit belongs in trusted CI. As of 2026-10-23 this is a recorded gap: `codeql.yml` runs only on manual dispatch, and the scheduled security workflows are still pending. The gap is owned by the CI security workstream under the project integration owner. Its resume event is that workflow's merge. * The weekly deeper review runs on the integration owner's existing admitted control cadence. The plugin's security-default reference tracks the current status. * **Findings:** * Each finding carries a stable ID, severity, owner, source, current status and remediation receipts. * Open critical and high findings, including unconfirmed findings, hold the release until a verified fix with fresh exact-head evidence, an evidenced false-positive determination, or recorded risk acceptance by the release authority naming the finding ID, scope and expiry. Risk acceptance never waives secret rotation, untrusted-code isolation, or legal and tenant-isolation gates. * Existing negative assertions are preserved. * A scan is never reported as a blanket all-clear. * Raw secrets and customer logs never appear in public PRs, issues or artifacts. # Agent quota routing Source: https://docs.tuturuuu.com/build/development-tools/agent-quota-routing Inspect local agent quota signals and spread delegated work across accounts. `packages/agent-quota` is a read-only local CLI and routing library for Codex, Claude Code, and Antigravity provider instances available to T3 Code. It recommends where to send delegated tasks using provider headroom when available, active assignments, data freshness, reset time, and account reserves. ## Data sources and authority | Source | Signals | Authority and freshness | | - | - | - | | T3 Code `~/.t3/userdata/statev2.sqlite` | Provider instance IDs, latest session timestamps, active subagent counts | Authoritative for T3's locally persisted orchestration metadata. It does not contain provider quota snapshots. Read through a read-only SQLite connection and only query non-content columns. | | T3 provider status cache `~/.t3/caches/.json` | `usageLimits` windows with provider-reported utilization and resets | Authoritative only for each reported window and its model scope at `checkedAt`. A model with no covering window is UNKNOWN; no percentage is inferred. Codex monthly buckets are retained, but their model-gating scope is UNKNOWN. | | Antigravity live catalog | Provider instance ID, supported models and delegation availability | Identifies routable models, not quota. Gemini models without a covering provider-reported usage/reset window remain UNKNOWN. | | Codex local `codex app-server` snapshot (primary), with `~/.codex/sessions` and `~/.codex-t3/sessions` JSONL fallback | Local JSON-RPC `account/rateLimits/read` primary/secondary `usedPercent`, window length and reset time; JSONL token increments | Codex owns authentication and provider communication. The app-server snapshot and provider-reported windows are authoritative at their observation timestamp. Token totals are estimates, limited to recent local rollouts. | | Claude JSONL with an explicit root | Recent input/output token usage; structured `rate_limit_event` utilization and numeric `resetsAt` values | Default root discovery is disabled. Explicit-root callers receive token estimates and provider event windows, never percentages inferred from token totals. | Optional JSONL collectors open each session with `O_NOFOLLOW | O_NONBLOCK`, validate the opened descriptor as a regular file, and read its mtime and bytes through that handle. A session replaced with a symlink after enumeration is ignored. Existing age filters remain in effect; this does not enable collection. `collector-file-race.test.ts` covers replacement for both providers, while `collectors.test.ts` preserves valid usage, timestamp and quota behavior. Codex homes remain `~/.codex` for `codex` and `~/.codex-t3` for `codex_ai4ed`. Its app-server owns authentication and provider communication. Claude default root discovery is disabled; explicit-root collection remains available to library callers. No collector discovers or opens credential files. Active assignments and provider associations come from T3 SQLite metadata. ## Commands ```sh theme={null} bun packages/agent-quota/src/cli.ts status bun packages/agent-quota/src/cli.ts recommend --provider claude --count 3 --json bun packages/agent-quota/src/cli.ts recommend --provider codex --count 2 --json ``` `recommend` returns a ranked list with reasons and a simulated assignment list that spreads the requested task count. A stale or missing quota signal is uncertain and makes the pool ineligible. A fresh reported window at or above 90% is excluded. A reset timestamp already in the past also makes that window uncertain, so it receives no imminent-reset preference. `claude_hehe` defaults to at most one active assignment and a 25% remaining reserve to keep the orchestrator account available. Set `AGENT_QUOTA_RESERVES` to a JSON object keyed by instance ID to override or add per-account `minRemainingPercent`, `maxConcurrent`, or `excluded` values. These policy choices are local routing recommendations; T3 Code dispatch still requires its normal provider instance selection. Production state paths use `process.env.HOME || os.homedir()`. The `AGENT_QUOTA_STATE_ROOT` override is honored only when `VITEST` is set; production commands ignore it. The passwd home is used only by the test write guard. ## Freshness, reservations, and Antigravity The 2026-10-23 routing policy includes every configured Codex, Claude, and Antigravity instance. Availability alone is never quota evidence. Only provider-reported percentages with valid observation and future reset timestamps can establish healthy capacity. Every reported quota window must be current. Codex and Claude signals have a 15-minute TTL; Antigravity has a 5-minute TTL. These are conservative local policy defaults, not provider reset cadences. Output includes `state`, `ageMs` (oldest quota window), `ttlMs`, `activeCount`, `action`, and explanatory reasons. Missing observations have a null age. Expired, missing, malformed, or non-authoritative usage is `UNKNOWN` and cannot be selected. Near means at least 90% used; exhausted means 100%. Both recommend `drain`: finish existing work without assigning new lanes, then require a fresh healthy observation. An expired reset never implies free quota. The tool reads existing `~/.t3/caches/.json` **provider status snapshots**, validating both instance ID and driver. It projects only quota windows, `usageLimits.checkedAt`, and the optional opaque account identity hash. It does not read provider profiles or auth files, invoke Antigravity's probe, or reproduce its authentication flow. `--snapshots-only` disables transcript and Codex app-server collection, making this cache plus orchestration metadata the only usage sources. T3 Nightly source inspection found Antigravity support in provider-core's `makeAntigravityUsageProbe`: provider quota-summary buckets are preferred, with reported model quotas as a fallback. The probe publishes `usageLimits` with `checkedAt`, percentage windows, reported resets, and an opaque `credentialFingerprint`. `ProviderRegistry` persists those status snapshots; the Limits tab deduplicates account identities. No reset cadence is inferred when a provider omits it. Missing or unavailable cached usage produces `UNKNOWN` with `no-usage-signal`. This is a snapshot consumer; refreshing T3's Limits tab remains T3's responsibility. If a normal live collector has windows but the cache is missing, malformed, or unavailable, those windows are retained with zero freshness TTL so the cache failure cannot erase usage evidence. The 2026-10-25 Codex `codex` registry row reported 0% remaining in a monthly window while two `gpt-6-luna` child runs on that instance continued normally for over an hour. This conflicts with treating the monthly bucket as the model's execution gate. The router keeps its percentage, label, duration, and reset visible, but marks model scope UNKNOWN; it cannot establish healthy headroom or model exhaustion. With no separate covering Codex window, the pool remains UNKNOWN and subject to bounded unknown admission. A model-scoped usage-limit receipt still blocks the matching model. Claude's supported provider usage source can publish model-scoped weekly rows, but the five observed account snapshots expose only `seven_day_fable`. Those rows do not cover Opus. Opus weekly status therefore remains UNKNOWN until T3 reports a covering Opus row; Fable usage is never used as Opus headroom. Antigravity's `3p-5h` and `3p-weekly` rows are scoped to Claude/GPT and do not cover Gemini. A completed Gemini run reporting `Usage Limit Reached` with a reset is recorded as a model-scoped receipt. The observed Argon failure with a four-day, 22-hour reset blocks Argon until that reset; it does not establish that Gemini Flash shares the same exhausted scope. An unrecognized provider window kind keeps its reported percentage but is marked non-authoritative. It cannot establish headroom. If it reports a near-limit or exhausted value with a pending reset, the pool stays blocked until reset evidence changes. Models and display names come from `orchestrator_capabilities`, through the supported T3 ACP command transport when available. A caller using native MCP can pipe that tool's JSON response to `--catalog-stdin`. No Antigravity model IDs are baked into the router. The observed live catalog includes Gemini 4 Argon and Gemini 3.8 Flash low/medium/high variants. Without a catalog, known instance IDs remain visible, but requested model availability is unverified. ```sh theme={null} bun packages/agent-quota/src/cli.ts recommend --task-kind implementation --snapshots-only --json bun packages/agent-quota/src/cli.ts recommend --task-kind review --provider antigravity --snapshots-only --json # Pipe a sanitized orchestrator_capabilities response into this command: bun packages/agent-quota/src/cli.ts recommend --task-kind research --catalog-stdin --snapshots-only --json ``` Task kinds share quota scoring; they do not imply invented model-quality scores. A requested `--model` must appear in that instance's catalog. Rankings subtract 30 points per active reservation plus a bounded contention penalty. This is a routing weight, not a usage percentage. Eligible healthy pools rank before UNKNOWN, near, or exhausted pools even when their reservations produce a negative score. Reservations combine active run and subagent projections, including queued, preparing, starting, pending and waiting work. Held queued runs do not reserve capacity. A task and its running child thread count once per instance; a later active child turn still counts after the original task completes. Native subagents without child threads use their provider-thread identity. Reads are advisory snapshots, not atomic admission locks; concurrent callers must still coordinate dispatch. Identity is always `providerInstanceId`, never display name. The crossed Claude labels and the shared `skora` label do not merge pools. When T3 exposes the same opaque account fingerprint under the same provider, instances collapse into one pool with combined reservations and the strictest configured reserve. Otherwise they remain separate and carry an `alias` explanation. Hashes are not printed in CLI output. Regression coverage lives in `tests/signals.test.ts` and the existing collector/policy suites under `packages/agent-quota`. ## Single-shot safeguards for every orchestrator `admit`, `observe`, `switch-lease acquire|release`, `switch-check`, and `handover-check` are opt-in, single-shot JSON hooks. They serve Tuturuuu, Upskii, Mastermind, and other owner threads on the same host. They do not install a controller, daemon, timer, scheduler, automatic switch, or automatic failover. Each reads one JSON object from stdin and writes one JSON result. Invalid input, unreadable state, or contention returns `status: "block"` with a fixed reason; callers must inspect `status`, not just the process exit code. Errors never echo raw input. These commands always use non-credential provider status caches; they never invoke provider probes; receipt scans select matching final provider-limit messages only. Pool selection follows the shared-account contract: identify quota pools by provider identity, deduplicate aliases, apply owner reserves and concurrency caps, and use provider preference only to order otherwise eligible healthy pools. Keep reviewer independence, privacy authorization, queue drainage and writer custody as hard gates. If a same-thread transition is unsafe, preserve the handover packet and workspace binding for one successor thread; never reparent children or replay work. ### Admission and outcome protocol `admit` accepts a stable `requestId`, the requesting `threadId`, its `currentInstanceId`, a fresh `catalog` from `orchestrator_capabilities`, and: ```json theme={null} { "requirements": { "capability": "implementation", "taskClass": "mechanical", "authorizedInstanceIds": ["codex_ai4ed", "antigravity"], "privacyApprovedInstanceIds": ["codex_ai4ed", "antigravity"], "candidates": [ {"instanceId": "codex_ai4ed", "model": "catalog-model-id", "capabilities": ["implementation"]} ] }, "policy": { "orchestratorReservePercent": 30, "nearUsedPercent": 90, "unknownMaxConcurrent": 1, "preferenceWeights": {"codex_ai4ed": 4, "antigravity": 1} } } ``` The numbers above are example policy inputs, not observed quota. Capabilities are `implementation`, `analysis`, `review`, and `research`; reviewers also require `requirements.authorInstanceId`. Candidate task capability and privacy/authorization approval come from the owner, since the live catalog does not establish those permissions. Every selected model must exist in the supplied catalog and its instance must support delegation. Missing approval blocks rather than broadening scope. An author and every verified alias of that account are excluded from review. Same-provider account identity uncertainty blocks bypassing an author's account or the owner's reserve. Output includes the chosen `providerInstanceId` and model, reasons, a durable reservation ID, and each pool's `FRESH`, `STALE`, `UNKNOWN`, or `EXHAUSTED` state, observation age, reset timestamp (or null), and shared reservation count. STALE has `quotaState: "UNKNOWN"`; it never supplies current headroom. Unlike the strict `recommend` command, `admit` permits STALE/UNKNOWN pools below the explicit `unknownMaxConcurrent` bound. The owner's own pool requires fresh headroom strictly above its reserve. Fresh near-limit/exhausted usage and existing per-pool reserve/concurrency gates always win. No eligible pool yields `no-eligible-pool` plus the exact blockers for each pool, with no exhausted fallback. Accounts are shared: multiple threads consume one pool's usage and reservations. Verified aliases collapse once; neither thread count nor alias count multiplies capacity. A file transaction serializes each admission decision and its pending reservation. Reusing a reserved/completed request ID blocks duplicate dispatch; reconcile its original result instead. Pending reservations release on terminal observation or after a 30-minute TTL when no active native binding confirms that work continues. Expired request IDs remain tombstoned for 30 days; reconcile uncertain dispatch rather than replaying it. A heartbeat can reconcile a reservation with its native child-thread/provider-thread ID so native and local records count once. Until that mapping is supplied, counting is conservatively additive. `observe` accepts `eventId`, `instanceId`, `observedAt`, and `outcome` (`success`, `usage-limit`, or `capacity-error`), optionally `resetsAt` and `requestId`. `terminal` defaults to true and releases that matching request reservation. An existing heartbeat must send `terminal: false`; it can also provide `nativeReservationId` to link the admission to a native reservation. No heartbeat is created by this tool. Event IDs are idempotent; conflicting reuse, account mismatch, and outcomes older than the admission are rejected. A usage-limit outcome fences the entire shared account until its reported reset, even without a usage API. Without a reset it remains exhausted until a later fresh covering-window check shows usage below 100%; with a reset it expires at that time unless a newer receipt exists. A no-reset receipt is retained for at most seven days, then expires without inventing a reset. A success receipt alone does not clear it. A capacity error imposes a 30-minute local pacing cooldown, not a quota percentage. After a reported reset, normal fresh/unknown admission gates apply again; reset is not proof of fresh capacity. ### Planning preference and model sizing Default planning weights are Codex 4, Claude 1, and Antigravity 1, configurable per instance through `policy.preferenceWeights`. They reflect the user's report that the ChatGPT allowance is about four times one Claude allowance. **These weights are user planning inputs, not telemetry or measured capacity.** Each shared account gets one weight (the maximum configured member weight), never the sum of aliases. After role and eligibility gates and FRESH/UNKNOWN ordering, an operator's configured preference takes precedence over the unknown-state ordering hint. The emitted `unknownOrderHint` is only a sorting diagnostic: it is the minimum reported remaining percentage among covering numeric windows whose reset is still pending, including non-authoritative windows. It is NOT headroom and never makes a pool eligible. Claude and Antigravity remain available for task fit, parallel work, and reviewer independence. The reported extra ChatGPT reset credit appears only as an unverified planning note. No command triggers, spends, activates, or counts reset credit as capacity. For a selected live-catalog model, `taskClass` suggests low effort for `mechanical`, medium for `standard`, and high for `security-review`, only if the model advertises the matching `effort` or `reasoningEffort` option. The recommendation is not runtime enforcement and never substitutes a weaker model outside the approved candidate list. ### Thread transition and successor gates `switch-lease acquire` takes `instanceId` (target), `threadId`, `attemptId`, owner `pid`, `processStartedAt`, and `ttlMs` (1,000–300,000). **The lease is keyed only by owner thread.** Different targets competing for the same thread cannot both win. Different threads may hold transitions to the same account simultaneously; shared-pool admission governs their capacity. The target is metadata, not an exclusive account lock. Acquisition is atomic, records expired takeovers, and rejects retired attempt IDs to prevent a delayed old release from removing a replacement. Release takes the target instance, thread, attempt, pid, and process start time; it is idempotent and cannot release a different attempt. `switch-check` never acquires or renews a lease. It requires the matching live thread lease and fresh owner-supplied evidence (at most five seconds old): `observedAt`, `threadIdle`, `queue: {threadId, complete, items}`, empty `unfinishedWriterIds` and `backgroundJobIds`, `children: {preserved, ids}`, `watches: {preserved, ids}`, and empty `uncertainMutationIds`. The queue must be a complete, freshly drained `t3_queue_list` result. A queued old model selection blocks with `queued-model-rollback-risk`. Caller attestations are not a native atomic switch fence; recheck at the safe boundary. Only the owner calls `t3_thread_configure(modelSelection)` after the gates pass. `handover-check` additionally takes `currentInstanceId` and a `handover` packet of booleans: `inThreadSwitchSupported`, `oldWriterDrainedOrStopped`, `sourcePreserved`, `dirtyFilesPreserved`, `indexPreserved`, `jobsPreserved`, `childParentsPreserved`, `questionsPreserved`, `watchesPreserved`, `unknownWritesPreserved`, `bindingVerified`, `successorIndependentlyAccepted`, and `acceptanceRecorded`, plus `successorCount`. A healthy current account reports `path: "stay"` with no successor recommendation. Otherwise a safe supported switch reports `in-thread`; an unsafe/unsupported switch reports `successor` and lists missing handover gates. At most one top-level successor is permitted. The old writer must drain or stop, the successor must independently read and accept the packet and supported workspace binding before writing, and acceptance must be recorded before the predecessor settles. True child parents, questions, watches, jobs, index, dirty source and unknown writes remain preserved. The check never launches a thread, reparents a child, replays work, or creates a writer. ### Persistence and native integration limits The host-wide tool ledger is `~/.t3/caches/agent-quota/hooks.json`, separate from T3-owned provider snapshots and its database. A short exclusive transaction file protects atomic write/fsync/rename operations; it is not an account lock. There is no daemon. Symlink ledger files and malformed data fail closed; the ledger has a 2 MB bound. Expired outcomes and inactive reservations are compacted under the retention rules below. A transaction lock with a recorded owner can be reclaimed only after its PID/start identity is confirmed dead or replaced and its inode is unchanged. An empty or partial owner record is retryable for ten seconds, then may be reclaimed under the separate reclamation lock after an inode check. Current telemetry coverage, verified from non-credential caches on 2026-10-23: | Pool | Supported cached usage/reset | Account identity hash exposed | | - | - | - | | `codex_ai4ed` | Yes: one reported window with reset | No | | `codex` | Yes: one reported window with reset | No | | `claudeAgent` | Yes: two reported windows with resets | No | | `claude_mira` | Yes: two reported windows with resets | No | | `claude_kv` | Yes: two reported windows with resets | No | | `claude_hehe` | Yes: two reported windows with resets | No | | `claude_dd` | Yes: two reported windows with resets | No | | `antigravity` | Third-party Claude/GPT only: two windows; no Gemini coverage | Yes | These surfaces do not establish coverage for every model. In particular, Antigravity Gemini remains uncovered by the observed windows. Claude has a session window plus a Fable-scoped weekly window. Coverage is not a claim that a pool is currently fresh or healthy. Cursor, Grok, OpenCode, Muse and Pi were disabled in the catalog; this adapter has no verified quota collector for them. Missing individual snapshots still yield UNKNOWN, and provider identity gaps remain explicit. Native T3 currently exposes no pre-delegation hook parameter on `delegate_task`, no atomic quota reservation/dispatch transaction, no atomic queue-drain-and-switch operation, and no automatic failover API. The owner must wire these one-shot commands into its existing dispatch/completion/heartbeat boundaries, supply live catalog and safety evidence, map native reservation IDs, and retain custody. Cross-host coordination is not provided by this host-local ledger. Independent security review, exact-commit CI, and an owner-driven integration exercise remain activation gates. Offline tests exercise policy, observation, shared reservations, concurrent leases, expiry, queued rollback and handover; they do not claim native hook activation or actual failover. ### Reconcile descendant failures after task completion ```sh theme={null} bun packages/agent-quota/src/cli.ts reconcile-children --parent THREAD_ID --timezone Asia/Ho_Chi_Minh ``` This read-only command traverses the existing T3 lineage and task projections, including nested descendants and provider-native subagents. It projects only run/task status, model/account selection, pending request metadata, delivery receipts, structured terminal errors, and final assistant messages matching provider limit/capacity patterns, including completed runs. It never reads authentication files or credentials, and never mutates T3 state. Each entry distinguishes an app-owned original task, provider-native subagent, or unbound thread-send follow-up. It shows original task status separately from the latest terminal turn; snooze deadline; queued runs with their own model and account selections (including held queues); pending questions/approvals; failure class; the provider's redacted terminal receipt; reported reset; delivery evidence; and the next resume boundary. Repeated root error items collapse to one entry per terminal run/failure. Authentication error text is withheld and recognizable credential syntax is additionally redacted. Other terminal receipt text is preserved; missing receipts remain explicitly unavailable. ISO reset timestamps are used directly. A human reset such as `resets 11:20am` requires the caller's explicit `--timezone`; it is interpreted relative to the receipt time, not the time reconciliation runs. Without a timezone, the reset stays unknown. A snoozed descendant with an unheld queued run reports `auto-resume-already-queued-do-not-duplicate`. All queued runs block switching, because their stored selection can restore the old account. No queue is removed or replayed. With no queued work, `safe-to-switch-account` requires idle run state and fresh owner-attested writer drainage in `--evidence-stdin`, keyed by thread ID as `{ "observedAt": "ISO timestamp", "drained": true }`; without it, `writer-drain-unverified` requires parent action. This disposition is only the run/drain prerequisite: `switch-check` still enforces the lease and custody gates. Delivery is `delivered` when a notification delivery or consumption receipt exists. A delivered event without parent observation reports unknown parent drain; an unbound follow-up without a matching transfer reports `missing-event`. Pending or ambiguous delivery remains `unknown`. Original task acknowledgment is never reused to claim delivery of a later turn. These records survive restarts; they prove recorded observation, not completed recovery. The observed native gap is that queued `t3_thread_send` turns are not bound to a new delegated task, so their later failures do not use the original task's parent notification path. The 2026-10-23 incident was an original task completing, then an unbound follow-up failing on the old account while another old-account run remained queued. An original-task failure did notify its parent. This command is a reconciliation aid, not automatic recovery or failover. The recommended native fix is to deliver **every terminal failure of every descendant turn** to its creating parent, deduplicated by run/failure, carrying account and reset evidence regardless of whether the original task already completed. Every failure has a stable `recoveryAdmissionKey`. To admit recovery, send `admit` a `recovery: {threadId, runId, failure}` object along with the usual task requirements and owner thread ID. The router derives the key itself, ignoring alternate caller request IDs, and atomically records it in the same ledger. Duplicate admission blocks both while reserved and after terminal observation. The CLI rechecks descendants, refuses already queued/superseded recovery and pending input, and requires fresh `recoveryEvidence: {observedAt, drained}` before admission. This is exactly-once local **admission**, not an exactly-once external mutation or automatic task replay. Dispatch outcomes remain the owner's custody; unknown results must be reconciled under the original operation identity. ### Read-only reset reroute planning `agent-quota reroute-plan --parent THREAD_ID` consumes the same JSON admission requirements and policy on stdin, plus `rerouteEvidence` keyed by descendant thread. Evidence includes `observedAt`, `onlyResetBlocked`, a complete `writerPids` list (with `pid` and `state`: `idle-stream-json`, `active`, or `unknown`), `writersComplete`, `dirtyFiles`, `dirtyFilesPreserved`, `custodyPreserved`, and `uncertainMutations`. Evidence expires after five seconds. Idle stream-json is acceptable only with confirmed idle run and drained writer/job custody; an idle process name alone is insufficient. Optional `configured` and `resumed` readbacks carry provider/model; resumed additionally requires task ID, run ID and running status. The planner simulates existing admission on a copy of the ledger, applying the verified limit receipt before selection. It does not reserve capacity. The parent must commit admission and revalidate gates immediately before acting. The recovery key is stable per thread and failed run; repeated admission blocks once reserved or completed. Planning never claims exactly-once external execution: preserve a returned task ID and reconcile unknown send outcomes before retrying. The supported sequence preserves queued resume text, cancels old-account queued runs, verifies an empty queue, commits admission, acquires the thread transition lease, checks switch safety, configures the model, sends saved resume text plus state readback with `t3_thread_send(mode: auto)`, and verifies the returned task via `task_status`. Configuration is reported as `configured`; only a matching running task is `resumed-on-provider`. Preserve dirty files and index throughout. Unsafe switches report the full successor handover gates; exhausted pools never become a fallback. These are instructions for the owner, not automatic tool execution. A native reset-time continuation may remain armed after rerouting. It queues behind a running turn, but may become redundant. Reconciliation identifies server-created `limit-resume:::...` message identities without reading message bodies and recommends preserving text then `t3_queue_cancel` when another provider continuation has started. An armed snooze alone is not evidence that a continuation has queued. No controller, timer, automatic failover, or native pre-delegation hook is installed by this package. For a reset-only wait, the owner may use the read-only reroute plan: preserve queued text and dirty files, cancel old-account queued selections, obtain fresh admission and the thread transition lease, then configure and verify a task-bound running continuation. Configuration alone does not count as resumption. ### Receipt and model-scope admission gates Every admission scans recent provider-limit receipts across all T3 threads, bounded to catalog instances and 30 days, including completed runs whose final assistant message reports a limit. This reflects shared accounts across orchestrators. This catches Antigravity's `Usage Limit Reached` result even when T3 labels the run completed. Relative resets (for example, `6 days, 23 hours`) use the observation time and round duration up to whole seconds. A limit without a reset remains exhausted until a later fresh covering-window check shows usage below 100%, or for at most seven days when no reset is reported. A receipt with a reset expires at that timestamp unless a newer receipt supersedes it. Invalid ledger rows more than five minutes ahead are quarantined per account. A persisted receipt more than five minutes ahead keeps its pool UNKNOWN even when another window is fresh. The quarantine lifts only after a fresh authoritative window observed after the receipt time covers its model, or covers the whole account when its scope is unknown. If the source receipt is gone or malformed, the quarantine is capped at 30 days from its first observation. Once the supported provider cache has a fresh covering window, run `agent-quota admit ` to recheck eligibility; the command prunes an expired quarantine. Do not edit the local hook ledger by hand. An invalid receipt timestamp is replaced with the observation time and treated as account-wide. Cache window IDs, labels and model scopes are preserved. Claude windows are model-scoped only when the label names a recognized model family (`Opus`, `Sonnet`, `Haiku`, or `Fable`); generic labels such as `Weekly limit`, `Weekly usage`, `Weekly total`, and `Current week` apply account-wide. `Current week (Opus)` and `Current week (Sonnet only)` cover only the named family. Antigravity `3p-*` windows cover Claude/GPT, not Gemini; `gemini-*` windows cover Gemini. Models with no covering window are UNKNOWN and subject to bounded admission. Availability and a zero in an unrelated window never establish headroom. When provider identity hashes are missing, distinct per-instance Claude homes or an explicit owner mapping may establish separate accounts. The home identity is the SHA-256 hash of the normalized path from `settings.providerInstances..config.homePath`, not a built-in table. Missing paths remain unknown. A provider identity hash, when present, takes precedence for alias deduplication. Account keys and fingerprints are removed from hook JSON output. Cache reads reject symlinks, non-regular files and files over 2 MB. Reservations without a currently active native binding expire at their recorded expiry, capped at 30 minutes; positional admits use a shorter five-minute provisional reservation. Completion or expiry retains a 30-day request tombstone. A usage-limit receipt expires at its recorded reset unless a newer receipt exists. Expired capacity observations and dead-owner leases are compacted; live or uncertain expired lease owners remain protected until release or confirmed death. Lease history and request tombstones have 30-day retention, so callers must reconcile older recovery operations rather than relying on indefinite idempotency. The transaction mutex records its PID and process start time; takeover requires a confirmed dead or replaced process and an unchanged lock inode, except for an empty or partial record older than the ten-second grace period. Unknown liveness fails closed. Switch lease release also requires the original PID/start identity; an expired lease held by a live or unverified process cannot be taken over. Reroute planning reuses and revalidates its existing reservation rather than reserving twice. Blockers take precedence over configured/resumed status, and resumption must match a running task, provider and model in the T3 snapshot. ### Preventive block ledger and shell-compatible gate Before each delegation, invoke `agent-quota admit POOL MODEL`. Exit **0** means ADMIT; exit **3** means a policy block; exit **2** means malformed input or unverifiable state. JSON on stdout explains the decision. This preflight uses cache-only quota and current reservations. Every successful positional admission records a provisional reservation for five minutes, so concurrent callers share the configured pool limit while the native task binding settles. The reservation is provisional, not a native task reservation. The existing stdin JSON `admit` interface remains the transactional admission path for full requirements. Pass `--reserve-owner` when the target is the orchestrator's own account, or set `AGENT_QUOTA_OWNER_POOL`; matching aliases require fresh headroom above the 30% owner reserve. Uncovered/stale models remain bounded by one concurrent reservation. Both admission interfaces and recommendations ingest `~/.local/share/tuturuuu/orchestration/quota/blocks.json`. Version 1 preserves the interim `blocks` entries: `pool`, `modelScope`, `reason`, `observedAt`, `resetAt` and/or `nextEligibleAt`, plus evidence and existing metadata. Model scopes support `*` and `?` globs. A block applies until the later supplied eligibility timestamp. Any active scope block suppresses all provider probes for that account, including detected aliases; cache-only reads can still admit an unblocked model. A malformed ledger fails closed. No-model recommendations exclude a pool with any active scope. Record a failure once with: ```sh theme={null} agent-quota block --pool antigravity --model-scope 'gemini-4-argon-*' \ --reason 'Usage Limit Reached resets in 6 days, 23 hours' \ --evidence 'thread-id/run-id/terminal-receipt' ``` Optional `--observed-at`, `--reset-at` or `--next-eligible-at` records explicit provider/operator timing. Relative reset text is anchored to observation time; clock-only reset text uses `--timezone` or the machine timezone. Without reset evidence, the default conservative `nextEligibleAt` is five hours later, not a claim that quota resets then. Evidence should identify the terminal run uniquely; repeating the same pool/scope/reason/evidence is idempotent and does not extend the original block. Writes are atomic under a process-verified file mutex and preserve existing interim metadata. Block expiry removes this prohibition; normal fresh quota, owner-reserve and bounded-unknown gates still apply. Enforcement is **orchestrator-invoked**. T3 currently has no native pre-delegation hook or automatic failover. A native `delegate_task` pre-admission callback that can veto dispatch is needed for platform enforcement; provider-reported usage, model scope and reset timestamps in `orchestrator_capabilities` would supply the corresponding telemetry. This change installs neither API nor any controller. An existing interim block without either deadline remains blocked until its evidence is corrected; it does not invalidate unrelated pools. Newly recorded blocks always receive a deadline. ### Usage UI parity, shared receipts and operator commands The Usage view reads `server.getConfig.providers[].usageLimits`. The application also publishes `subscribeServerConfig` snapshots and `providerStatuses` events. These are normal T3 application RPC surfaces; there is currently no narrow MCP/CLI endpoint exporting only quota windows and account home paths to this router. The router reads the existing non-credential `~/.t3/caches/.json` provider status snapshots. It does not refresh a provider, read authentication files, or write SQLite. A fresh RPC response does not refresh an old `usageLimits.checkedAt`. The qualified passive registry reader uses one `server.getConfig` call with an empty input through T3's supported `orchestration:read` WebSocket protocol. It does not call `server.refreshProviders`. Registry memory is the Usage view's source; the cache reader is secondary because startup hydration omits `usageLimits`. Each imported window uses its enclosing snapshot's `checkedAt` as `observedAt` and is limited to a five-minute age. An unchanged `checkedAt`, an RPC response, or a successful provider refresh does not establish fresh quota. Missing `usageLimits`, stale snapshots, or unknown window kinds remain UNKNOWN. The candidate does not claim exact equality between persisted caches and the UI's in-memory snapshot. Each window reports **used percent**; remaining is `100 - usedPercent`. Session, weekly and model-specific windows remain separate. Scope comes from window IDs and labels: Claude `five_hour` is account-wide, `seven_day_fable` is Fable-only; Codex primary/secondary windows cover its account; Antigravity `3p-*` covers Claude/GPT, not Gemini. Uncovered Gemini models are UNKNOWN. Receipt scope follows the corresponding model family, so Argon effort variants share a limit without blocking Flash. The latest reset phrase wins. Every admission scans terminal receipts across all catalog instances and orchestrator trees, limited to 30 days and 5,000 rows per projection; overflow or unreadable state fails closed. An expired receipt is cleared at its recorded reset unless a newer receipt exists. The UI aggregates only after deduplicating accounts by provider driver and normalized email, falling back to the provider identity fingerprint, within a workspace. It rounds the mean of deduplicated remaining percentages; Codex windows group by kind, while other providers group by kind and window ID. The bridge deliberately omits email and fingerprints, so this candidate retains per-instance rows unless a separate supported identity hash is already available; it does not invent a cross-account UI aggregate. Claude's displayed effective session can also reflect an exhausted weekly window, while this router keeps provider windows separate and applies only model-covering windows to the requested model. Quota headroom only makes a target eligible. It does not establish that a provider accepted or started the continuation. Reconciliation records context allowance failures and provider-start failures separately. A reroute is called verified resumed execution only after the resumed run reports the expected provider and model as running; admission or configuration alone is not success. Account identity uses provider metadata when available. Otherwise it is the SHA-256 hash of the normalized, explicitly supplied account home path (including provider kind), **not the display name**. Read the path-only projection of `settings.providerInstances..config.homePath` from the supported, redacted `server.getSettings`/`server.getConfig` response. Do not read raw settings files: they can contain credentials. Missing paths remain unknown; there is no static Claude home table. The current mapping supplied for verification is claudeAgent → claude\_skora, claude\_mira → claude\_mira, claude\_kv → claude\_kv, claude\_hehe → claude\_hehe, claude\_dd → claude\_dd. These names are a comparison receipt, not fallback routing configuration. Identical identities collapse before aggregation, and shared reservations are non-exclusive. Home separation is configuration identity evidence, not proof that two independently configured homes can never contain the same provider login. Run from the repository (the package script exposes the same commands): ```sh theme={null} bun packages/agent-quota/src/cli.ts status --json bun packages/agent-quota/src/cli.ts explain --pool claude_dd --model CLAUDE_MODEL_FROM_LIVE_CATALOG bun packages/agent-quota/src/cli.ts dry-run route --model gpt-6.1-sol bun packages/agent-quota/src/cli.ts admit codex_ai4ed gpt-6.1-sol ``` `status`, `explain` and `dry-run route` are read-only and report individual windows, remaining/used units, source, confidence, age, reset and routing reasons. For a review recommendation, pass `--task-kind review --author-pool INSTANCE`; shared or unverified same-provider identity cannot satisfy reviewer independence. `--catalog-stdin` accepts the current `orchestrator_capabilities` catalog when the ACP transport is unavailable. Positional admission retains exit **0** for admit, **3** for policy/exhaustion block, and **2** for malformed input or unverifiable state. Lock contention is a retryable block (exit 3). A malformed `block` command also exits **2**. Invalid manual observations are skipped individually and listed in `invalidObservations`; valid observations continue to contribute. It records pacing/hysteresis state and a five-minute provisional reservation in the existing hook ledger; it does not switch an active writer. Full JSON admission remains the transactional native-task reservation interface. Manual observations more than five minutes in the future are rejected into `invalidObservations`. A resetless session observation is bounded to five hours. A block reset is inherited only when its scope covers the entire observed window and the block is within that window’s maximum age. The cross-thread terminal receipt scan targets a maximum of 2 seconds for up to 5,000 candidate runs on a local projection; it performs one bounded candidate query and reuses those IDs for receipt reads. Exceeding this target is a performance regression, not a reason to skip receipt checks. For a supported manual observation, pipe a narrow JSON document into `bun packages/agent-quota/src/cli.ts import-usage`. Include the current catalog as `catalog` if needed: ```json theme={null} { "provenance": "operator transcription of the individual T3 Usage window", "observations": [{ "pool": "claude_dd", "measure": "remaining", "units": "percent", "value": 97, "observedAt": "2026-10-23T05:33:00Z", "windowId": "five_hour", "windowLabel": "Session", "provenance": "user screenshot at 12:33 Asia/Saigon" }] } ``` This dated example is evidence, not a default percentage. Supply `resetAt` only when observed. Without a reset or after the TTL, the reading cannot establish fresh headroom. Existing `.observations[]` with `kind: manual-screenshot` and `sessionRemainingPercent` are normalized explicitly to remaining percent. Names and ordering do not identify accounts. For app-side exports, `providers[]` accepts only `instanceId`, `driver`, and `usageLimits` (`checkedAt`, `windows`, `unavailable`); windows accept `id`, `kind`, `label`, `usedPercent`, `resetsAt`, `windowDurationMins`. Optional `settings.providerInstances` accepts only driver and config homePath/shadowHomePath. Never pipe a full config or auth document. Newer provider observations supersede older manual readings for the same window. Provider `checkedAt` may be at most five minutes ahead of import time, and merge uses the same bound. An unavailable provider entry still requires a valid `checkedAt`; otherwise the whole import is rejected with `usage-import-unavailable-checked-at-required` rather than silently skipping it. `configure` takes JSON stdin with `pool`, `policy`, `evidence` and optionally `catalog`. Policy requires `reservePercent` (0–100), `nearUsedPercent` (1–100), `hysteresisPercent` (0–50 and less than near), `pacingMs` (0–86400000), `maxConcurrent` (integer 1–100), and `preference` (0–100). It validates and records a dated receipt with an evidence digest. Near depletion latches until fresh used percent falls to `nearUsedPercent - hysteresisPercent`; pacing limits time between admissions. Aliases use the strictest reserve/concurrency/pacing thresholds and a single preference, never multiplied capacity. Preference cannot override safety gates. Adjusting these values never changes the active thread's model/account. At 05:58:05Z on 2026-10-23, existing provider caches yielded the following individual **session remaining** comparison. Snapshot `checkedAt` values were 05:56:08–05:56:12Z, within the 15-minute Claude TTL; these are cache observations, not a newly invoked provider probe. Drift after the 05:33Z screenshot is expected. | Catalog ID | Screenshot 05:33Z | Snapshot \~05:56Z | Session reset (UTC) | | - | -: | -: | - | | claudeAgent | 0% | 0% | 06:00:00 | | claude\_kv | 12% | 8% | 05:59:59 | | claude\_hehe | 57% | 45% | 08:29:59 | | claude\_dd | 97% | 93% | 09:20:00 | | claude\_mira | 100% | 94% | 10:30:00 | All five had session and Fable weekly windows, with no Opus weekly row. Codex `codex` reported 0% remaining in its primary Monthly window; `codex_ai4ed` reported 75% in primary Weekly. The monthly Codex value is retained as an unverified model-gating scope, not treated as account-wide exhaustion. Antigravity exposed only third-party `3p-5h` and `3p-weekly` windows; those do not prove Gemini headroom. Subsequent Argon usage-limit receipts do block the matching Argon scope. These dated observations are not configuration and expire normally. The native gaps remain a narrow read-only usage/home-path export and a veto-capable pre-admission callback on `delegate_task`. No native automatic account transition or failover is implemented. `Selected model is at capacity` is a capacity event, not a quota event. A `block` command for this receipt requires an exact `--model-scope MODEL` and uses a 30-minute `nextEligibleAt` by default; it rejects `*`/`?` scopes and `--reset-at` for capacity. Other models on the shared pool remain eligible. Likewise, `observe` with `outcome: capacity-error` requires `model`, accepts `nextEligibleAt`, and defaults to a 30-minute model-only cooldown. Capacity expiry does not assert quota headroom; normal admission checks still apply. ### Turn-boundary warning and safe same-thread switch The optional, bounded `watch` command is read-only and is intended to run at an orchestrator turn boundary. It uses the minimum remaining percentage across all fresh authoritative windows covering the owner's model, including weekly and monthly windows, and never probes a provider. Pass `--model` when known; without it, model-scoped windows are included conservatively. An active block or usage-limit receipt forces `switch-now`; stale or missing data returns `warn`. Defaults are `warn` at 25% remaining, `switch-now` at 12%, and a 3-point hysteresis band. Supply the previous verdict to preserve hysteresis across invocations; callers own that small piece of turn state, so the command needs no timer or writable watch ledger: ```sh theme={null} bun packages/agent-quota/src/cli.ts watch --owner claude_hehe --role main \ --previous-verdict warn --warn-percent 25 --critical-percent 12 \ --hysteresis-percent 3 ``` It emits one JSON line with the verdict, signal state, remaining percent and an optional successor. Exit codes are 0 for `ok`, 3 for `warn`, and 4 for `switch-now`. Missing or stale data is `warn`; a successor is selected only from fresh healthy headroom and excludes the owner account, blocked pools, active loads, outstanding reservations, active usage-limit receipts, and model capacity cooldowns. `successorReason` appears only for a non-`ok` verdict. No successor means the caller must stop and escalate rather than choose a fallback. Before a supported same-thread account change, the owner runs a read-only checkpoint against its thread ID and a newline-separated file of watcher PIDs: ```sh theme={null} bun packages/agent-quota/src/cli.ts checkpoint-ready \ --thread THREAD_ID --watcher-pids-file /path/to/watcher-pids.txt ``` It reports `go` or `no-go`, including queue/writer counts, missing watcher process metadata and any in-flight `gh pr merge` process. Writer nodes count only while their associated T3 run is active; the count query is capped at 1,001 nodes and SQLite lock waiting is bounded to one second. A non-empty queue, active T3 writer/run, unknown watcher PID, or merge blocks the configure boundary. The supported operation sequence is exactly: 1. `checkpoint-ready` passes and the owner's queue is empty. 2. The owner calls the supported `t3_thread_configure(modelSelection)` API. 3. The owner reads back the thread configuration and verifies provider/model. 4. The owner continues only after the readback matches. The check does not acquire a lease, cancel queued work, or configure the thread. The owner must preserve and drain any work before retrying it. T3 still has no native pre-delegation admission callback on `delegate_task` and no automatic account failover; these one-shot commands are orchestrator-invoked safeguards. Main orchestrators can pass `--role main` to prefer an eligible, fresh, healthy Claude successor; child/subagent callers use `--role child` (the default) and retain task/provider/model-driven ranking. A main-role non-Claude fallback is blocked unless the caller explicitly opts in with `--allow-non-claude-fallback`; an exhausted, stale, reserved, or otherwise ineligible Claude pool is never promoted by the role preference. The JSON admission request carries the same choice as `policy.orchestratorRole`, with `policy.allowNonClaudeFallback` defaulting to false. Main role changes ordering among eligible pools only and does not bypass quota, block, reserve, capability, privacy, authorization, concurrency, reviewer-independence, or drainage gates. # cf CLI and Void over Wrangler Source: https://docs.tuturuuu.com/build/development-tools/cf-cli-wrangler-parity Per-operation parity matrix, native Worker template, rollback, the Wrangler usage guard and the per-service cutover checklist. ## Decision The target toolchain for active Worker-backed apps and services is **cf + Void**. Adopt it per verified operation. Preserve existing commands and configuration until authentication, target identity, bindings and migrations, emitted artifacts and rollback parity are established. Wrangler remains for recorded gaps with an owner and removal condition. The shrink-only usage guard requires structured admission for expansion, changed Wrangler lines, or changed gap scope. This page refines the cf adoption gates in [Oxc and Cloudflare adoption](/build/development-tools/oxc-cloudflare-adoption). A merged tooling switch is CI-verified, not production-delivered: every deploy target change still needs the gated production workflow, exact-commit evidence and the cutover checklist below. ## Toolchain shape | Surface | Build | Deploy artifact | Publisher | | - | - | - | - | | Native Workers (fetch, scheduled) | Vite+ with `@cloudflare/vite-plugin` reading `cloudflare.config.ts` (`experimental.newConfig.cfBuildOutput`) | cf Build Output Specification, `.cloudflare/output/v0` | `cf deploy --prebuilt` | | Durable Object Workers | Vite+ with the cf config | Same emitted bundle, with retained `v1` migration metadata | Wrangler until a separate rehearsal-backed publication change | | Framework apps (TanStack Start on Void) | Void SDK plugin on Vite+ | Void-emitted Worker config | Wrangler until the Void publication gap closes | | OpenNext apps (Parley, Lettin) | OpenNext Cloudflare adapter | OpenNext Worker | Wrangler (adapter-owned) until the app moves to Start on Void | The Void SDK is not applied to native Workers. Void's supported app types are Void apps, meta-frameworks and static sites, so a native fetch/scheduled/Durable Object Worker would have to be rewritten into Void's routes and crons model. For publication, Void's own Cloudflare deploy converts the emitted config into a cf Build Output and calls `cf deploy --prebuilt`. Native Workers emit that format directly, so they share Void's publication path without its runtime layer. The stable Vite plugin (1.64) already supports `cloudflare.config.ts` and Build Output emission; the 2.0 beta is not required. Loading `cloudflare.config.ts` requires Node.js (not Bun). The cf package requires Node `^22.23 || ^24.22 || ^26.12`. The **cf/Void Wrangler-exit lane** owns the `cf@1.0.0-beta.20` pin. Upgrade through a scoped dependency PR after checking upstream release notes and rerunning config/output parity, credential-free artifact checks, secret handling, settings readback fixtures and real Worker CI tests on the candidate version. DO publication remains Wrangler until a disposable migration rehearsal and a separate reviewed publisher change. Renovate holds cf prerelease pins via the `cf` package rule in `renovate.json`; update this hold only with the qualified upgrade PR. Do not float this beta pin automatically. ## Operation parity matrix Checked against installed cf `1.0.0-beta.20` source/help and `@cloudflare/vite-plugin` 1.64.0. Command availability below does not establish live authentication, resource identity or production acceptance for every caller. The existing native profiles qualify only their declared configuration fields. | Operation | Wrangler | cf / Void | Verdict | | - | - | - | - | | Configuration | `wrangler.jsonc` | `cloudflare.config.ts` (`cf/config`), generated by `cf migrate` | Parity, guarded by a normalized identity diff | | Local development | `wrangler dev` or Vite plugin with `configPath` | `vp dev` with the Vite plugin `newConfig`; `cf dev` also works but delegates to `bunx vite` (stock Vite, not Vite+ core) | Parity | | Local runtime tests | `createTestHarness` from `wrangler` | `scripts/ci/cf-vite-worker.mjs`: Vite dev server (source) or preview (emitted Build Output) in workerd; DO SQL through the Local Explorer API; `restart()` replaces eviction | Parity | | Binding types | `wrangler types` | `cf workers types` requires `cloudflare.config.ts`; writes `.cloudflare/types/index.d.ts` | Qualified for migrated native configs; no JSONC input, output-path or `--env-interface` equivalent | | CI build | Vite plugin emits `dist/*/wrangler.json` | Vite plugin emits `.cloudflare/output/v0` | Parity | | Credential-free artifact check | `wrangler deploy --dry-run` | `cf deploy --prebuilt --mode production --dry-run --no-provision` | Parity | | Deploy the exact artifact | `wrangler deploy -c --message` | `cf deploy --prebuilt --mode production --no-provision --message` | Parity; `--provision` defaults to true, so always pass `--no-provision` | | Secrets with a deploy | `wrangler secret bulk` then `wrangler deploy` | `cf deploy --secrets-file` uploads secrets with the same version | Parity (atomic) | | Bulk secret change | `wrangler secret bulk` | `cf workers secrets bulk --worker --secrets @file` | API available; its map contains secret objects, not the plain string map accepted by deploy `--secrets-file` | | Single secret put | `wrangler secret put` | `cf workers secrets update ` | API available; interactive/stdin and deployment parity unverified; never place secret values in command arguments | | Secret names | `wrangler secret list` | `cf workers secrets list` (API command) | Parity, verify output shape before scripting | | Versions | `wrangler versions list` | `cf workers versions list --worker-id ` | Parity | | Deployments | `wrangler deployments list` | `cf workers deployments list --worker ` | Parity | | Rollback | `wrangler rollback ` | `cf workers deployments create --worker --strategy percentage --versions '[{"version_id":"","percentage":100}]'` | Parity | | Gradual rollout | `wrangler versions upload` / `versions deploy` | `cf workers versions create --prebuilt`, then `cf workers deployments create` | Parity | | Cron, routes, custom domains | top-level `triggers`, `routes` | `worker.triggers`, `worker.domains`; `cf workers triggers deploy` | Parity in config; verify applied settings at cutover | | Durable Object migrations | `migrations` tags | `worker.exports` lifecycle declaration; `cf migrate` does not convert it | Manual conversion; needs a disposable rehearsal before production | | D1 migrations | `wrangler d1 migrations apply` | `cf d1 migrations apply --dir `; remote unless `--local` | Requires UUID, migration directory/table and local-persistence parity; binding names are rejected | | Live logs | `wrangler tail` | Not supported (cf docs) | Gap | | Prebuilt Void deploy | n/a | `void deploy` always builds and provisions; `--skip-build` is static-only (Void 1.1.12 and 1.2.1) | Gap for the exact-artifact contract | | OpenNext publication | `opennextjs-cloudflare deploy` invokes Wrangler | None | Gap until Start on Void | ### Capability limits before replacing a caller `cf workers types --no-include-runtime` skips the runtime declaration generator; its default can start workerd through Miniflare. The command discovers typed config by filename, so replacing a legacy `types --config ` invocation requires config and declaration-consumer migration, including the global interface. `cf dev` can delegate to Wrangler's new-config implementation. Detected Vite projects use the package manager's Vite command; use the owning `vp dev` script to preserve Vite+. `cf build` can delegate too, and `cf deploy --dry-run` can build unless `--prebuilt` is supplied. None is a universal replacement for legacy JSONC. Treat **`cf d1 migrations list` as potentially mutating**: migration preparation can issue `CREATE TABLE IF NOT EXISTS` for its history table before listing it. It is unsuitable for a read-only production inventory. No replacement for every Wrangler SQL file/export operation has been qualified. R2 object and KV key API commands exist, but require explicit bucket/namespace identifiers and verification of byte output, metadata, TTL, pagination and local state. `cf logs query` and `cf o11y telemetry query` do not establish live-tail parity. Keep the existing commands until their exact caller contracts pass. The configuration normalizer compares live DO classes/storage, not historical migration tags or module bytes. Its current binding mappings cover text, JSON, secrets and DOs; assets, AI, R2, D1, KV, services and email triggers need explicit profiles. The retained publication adapter separately enforces the original `v1` history and exact module inventory. Preserve both gates and unknown-field rejection. ### Calendar emitted binding types Calendar's ASSETS-only profile uses `calendar-cf-types.mjs` after its existing emitted-artifact validator. The helper creates a disposable literal typed config and runs the owning pinned `cf workers types --no-include-runtime` on a supported Node executor. It never loads a Worker module, environment file or credential. The self-contained `cloudflare-env.d.ts` comes from an ASSETS-only template; cf output checks that template rather than supplying the declaration text. A new binding requires changing the template and validator together. The cf-inferred binding and template must have the exact required ASSETS key and Fetcher type, with no `any`. During CI qualification the existing producer remains an explicit reference oracle; the same checks apply to its declaration. Additional resources, vars, secrets and named environments remain rejected by the existing artifact profile. Only after declaration parity and unchanged hashes for the bundle, assets, emitted config and deploy pointer does the helper atomically replace `cloudflare-env.d.ts`. Exclusive directory custody serializes supported generators through comparison and replacement; an existing lock fails closed. Detected edits and partial, empty or symlinked producer output preserve prior declarations. This is cooperative publication custody; do not edit the destination during generation. Its disposable config is removed; the output remains self-contained when the sealed artifact is restored. The JSON receipt records source/tool versions and before/after artifact digests. Local `cf:typegen` requires a qualified existing build; it does not build or publish. Exact-head CI must run the real cf CLI and the retained reference producer. Fixture tests exercise failure handling and the real TypeScript proof; they do not establish supported-runtime cf execution on an older local Node. Read declaration and receipt inputs through an opened regular-file descriptor; verify its inode/device against the named file before reading. Path checks alone leave a replacement race. Keep supplier-dependent fixtures and restored copies under `apps/calendar/tmp`. Copying the workspace manifest into repository-level `tmp` does not install its dependencies there; Bun's isolated layout resolves cf and its type imports through the owning Calendar workspace. Test that layout without a root-hoisted cf package. ## Native Worker template Applied to Cron Control, Devbox Control and Coordination. Each migrated Worker has: * `cloudflare.config.ts`, generated by `cf migrate --no-install` and reviewed. Durable Object `migrations` become `exports: { Class: exports.durableObject({ storage: 'sqlite' }) }` for every live class. * `vite.config.ts` with `cloudflare({ experimental: { newConfig: { cfBuildOutput: true } } })`, no `configPath`, explicit preview URL and observability/log settings, loopback dev server settings, `persistState` under a disposable directory for runtime tests (normal dev uses `.cloudflare/state`), and the Worker environment's `build.sourcemap: false` (Wrangler did not upload source maps). * Scripts: `dev: vp dev` (host/port in `vite.config.ts`), `build: vp build`, `type-check: cf workers types && tsc`. `cf` is an exact devDependency; the app-level `wrangler` devDependency is removed. * A parity baseline in `scripts/ci/fixtures/cf-worker-parity/.json`, normalized from the final `wrangler.jsonc`. `scripts/ci/cf-worker-parity.test.mjs` compares `cloudflare.config.ts` with it, and CI compares the emitted Build Output after the build with `node scripts/ci/cf-worker-parity.mjs apps/ scripts/ci/fixtures/cf-worker-parity/.json`. * The workflow validates, runs the runtime checks on source and on the emitted Build Output, dry-runs `cf deploy --prebuilt`, then uploads `.cloudflare/output` as the exact-commit artifact. The gated deploy job downloads that artifact and publishes it without rebuilding. For DO Workers, `cf-wrangler-publication.mjs` validates the same emitted bundle against `wrangler.publish.jsonc`, retains its original `v1` history and emits a Wrangler `no_bundle` publication config. Both credential-free publisher checks run in CI. Production has no cf deploy branch; enabling one needs a separate reviewed change with the disposable migration/data-retention rehearsal receipt. `@cloudflare/vite-plugin` 1.x still depends on the Wrangler library internally. That is a library dependency, not CLI usage; it disappears with plugin 2.x. The DO publication adapter excludes exactly `bundle/.vite/manifest.json` from module inventory. This Vite metadata is not a Worker module. Other undeclared files and symlinked metadata still fail qualification. After building in CI, run `CF_PUBLICATION_TEST_APP=coordination node --test scripts/ci/cf-wrangler-publication.runtime.mjs` (and select `devbox-control` for that app) against the real emitted output. The test requires Vite's manifest, preserves module bytes and applied `v1` migrations, and rejects unexpected metadata and symlinks. `CF_PUBLICATION_TEST_ROOT` can select an extracted exact-commit CI artifact tree; never commit the built bundles. ## Rollback ```bash theme={null} # Identify the previous version (first deployment is the one serving traffic). cf workers deployments list --worker tuturuuu-cron-control # Route 100% of traffic back to it. cf workers deployments create --worker tuturuuu-cron-control --strategy percentage \ --versions '[{"version_id":"","percentage":100}]' ``` Rollback cannot cross a Durable Object lifecycle change. Secret changes can block rollback; inspect the error before using `--bypass-deployment-checks`. Retained CI artifacts (`-worker-`) allow re-publishing an exact earlier build. The local harness rejects `.dev.vars*` and `.env*` files in both the app root and working directory (template `.example` files are allowed). Every declared secret needs a nonempty disposable fixture. Undeclared/control-key fixtures are rejected, and declared text vars are masked from the shell before process-env loading. It restores the first prior environment value after startup failure or close. `cf-vite-worker.test.mjs` covers both source and artifact modes using a disposable Vite fixture; real Worker runtime acceptance remains a CI gate. Each run allocates a private temporary persistence directory, keeps it through Worker restarts and deletes it on close or startup failure. The app Vite configs consume only the harness-owned `CF_VITE_TEST_PERSIST_PATH` override; ordinary development retains its usual state. ## Wrangler usage guard `node scripts/ci/wrangler-usage.js` prints the inventory. The guard scans tracked files, including executables under `plugins/`, `.agents/` and `tmp/`; Markdown, text, lockfiles and binary assets are excluded. Ignored/untracked `tmp/` files are not an inventory: executable files there enter the guard only when tracked. `.md`, `.mdx` and `.txt` are documentation exclusions in every directory; commands embedded in those files need normal review, since the guard does not parse executable documentation examples. It counts mention lines and all Wrangler token occurrences conservatively, including comments and strings. A SHA-256 digest also covers each file's Wrangler lines (all content for config files), so same-count substitutions require admission. Lower or remove counts when usage falls. An `admitted` record must be `{ "decision": "https://github.com/tutur3u/platform/pull/", "reason": "" }`. New entries, growth, changed digests and changed gap scope need a fresh record; reverting a migration follows the same rule. Compatibility references need admission too: cache policies must still recognize legacy declaration producers and scratch directories, and cleanup exclusions must retain local state until its owning toolchain is retired. These references do not invoke the CLI; replacing their names with cf would change their safety coverage. Use the existing `shared-toolchain` gap for scratch/state policy references and `producer-inventory` for declaration-producer detection, with exact counts, digests and the admitting PR decision. Remove each reference only with its covered usage. CI compares the allowlist with its base commit (`HEAD^1` of the pull request merge ref) and fails if that commit is unavailable. Current gaps: `paused-runtime` (retained paused runtimes), `opennext-adapter`, `void-wave1-publication`, `pending-migration` (services awaiting their parity PR), `shared-toolchain` (root CLI provider, trusted-dependency and policy files), `parity-tooling` (the migration parity checker, which reads former Wrangler configs), `do-publication-rehearsal` (Wrangler publication of existing DO migration tags), and `producer-inventory` (native-check declaration-producer detection and its data-only fixture; no CLI invocation). The latter recognizes both legacy and `cf types` producers and is removed when legacy recognition is no longer needed. ## Service inventory | Service | Current publisher | Status | Gap | | - | - | - | - | | Cron Control | cf (after its parity PR) | Template | — | | Devbox Control, Coordination | Wrangler until a rehearsal-backed publisher change | Template with DO `exports`, runtime SQLite namespace checks | — | | Colab | Wrangler bundler with assets and AI | Local Worker preview uses Vite+; release build-output conversion pending | `pending-migration` | | Meet realtime | Wrangler, three SQLite DO classes | Needs DO `exports` and runtime fixtures | `pending-migration` | | Mail email routing | Wrangler config, email handler | Needs the email trigger mapping | `pending-migration` | | Meet, Apps, Calendar, Mail, Shortener | Void build, Wrangler publication | Wave 1 owner | `void-wave1-publication` | | Parley, Lettin | OpenNext, Wrangler | Start on Void migration | `opennext-adapter` | | Backend, TanStack Web | Paused | Excluded | `paused-runtime` | ### Colab local Worker preview Run `bun run --cwd apps/colab preview` to serve the Worker and source UI on `127.0.0.1:8795`. The UI's separate `7835` dev proxy remains available. `vite.worker.config.ts` reuses the UI's React/Tailwind plugins and source alias, replaces its proxy with the local Worker, and reads the existing service configuration through the Cloudflare Vite plugin. It preserves the plugin's default local persistence path, disables remote bindings and leaves the UI build and production publisher intact. HMR stays disabled so React cannot inject an inline refresh preamble rejected by the Worker's production CSP. Cloud AI calls are outside this local-only scope. The Colab workflow exercises the actual preview config after its UI build: health and session responses, signed-out workshop denial and foreign-origin rejection. It also requests HTML, transformed client imports and compiled Tailwind CSS so an API-only pass cannot hide a broken UI. It uses disposable signing/persistence fixtures, refuses local credential files, and restores the process environment and removes only its own state on close. Types, release Build Output, DO migration history and publisher cutover remain separate gates; do not rename those commands based on this development change. ## Settings preflight token permissions The deploy token must cover the pinned account and every zone in that account. The settings snapshots require these Cloudflare API token permissions in addition to the publication permissions: | Resource | Read permission | Snapshot data | | - | - | - | | Target account | Account → Workers Scripts → Read (Write also grants read) | Worker settings, schedules, secret names, deployments and custom domains | | Every target-account zone | Zone → Zone → Read | Zone enumeration | | Every target-account zone | Zone → Workers Routes → Read | Worker route enumeration | Grant zone visibility for **all zones in the target account**. Limiting visibility to the expected hostname's zone can hide unexpected routes and cannot establish route parity. Do not grant unrelated accounts. These requirements apply to `COLAB_CLOUDFLARE_API_TOKEN` and to `COORDINATION_CLOUDFLARE_API_TOKEN` when supplied instead of the fallback. Keep values in GitHub secrets; never attach token values to receipts. Actual token scope remains unverified until the bounded read-only preflight succeeds. Missing access, truncated lists and unsupported account size fail before publication; do not bypass that gate to deploy. Each snapshot phase first logs `Cloudflare API token fp:<12 hex> status ` from `/user/tokens/verify` (or the account-owned token endpoint); this diagnostic cannot fail the job. The fingerprint is the first 12 hex of `sha256(id)`, computed only for a well-formed 32-hex token id (otherwise `fp:`); the raw id is never printed because it is response-derived. A failed read prints its HTTP status, numeric Cloudflare error codes and a request path built only from the pinned account, the Worker name and zone ordinals (`/zones//workers/routes`), so a `403` shows which token and zone lack access. Diagnostics never print response-derived text (error messages, bodies, zone names or IDs, JSON parse errors or assertion diffs of live values). The status prints only when it is a known value, and binding names only when the baseline already declares them (others print as ``). Undeclared text or Durable Object bindings fail before they are recorded; undeclared secret bindings are tolerated because their values are never read. The CLI prints only its own assertion messages and the name of any other error. To identify the token, hash each candidate id from the Cloudflare dashboard token list (`printf %s | shasum -a 256 | cut -c1-12`) and compare before changing the GitHub secret. Cloudflare documents the accepted permissions for [Worker secrets](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/secrets/methods/list/), [deployments](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/deployments/methods/list/), [custom domains](https://developers.cloudflare.com/api/resources/workers/subresources/domains/methods/list/), [zones](https://developers.cloudflare.com/api/resources/zones/methods/list/) and [routes](https://developers.cloudflare.com/api/resources/workers/subresources/routes/methods/list/). ### Secret inventory from settings snapshots Coordination's existing before/after settings capture verifies the required `COORDINATION_TOKEN` name against its recorded identity. Each snapshot already performs one bounded secret-name GET on the pinned account and Worker. Reuse its `secrets` array for the workflow's explicit token gates instead of issuing a second CLI request. Require array shape and membership; keep both checks fatal, keep the after check after publication, and retain the private settings receipts. No secret values are captured. The original migration-tag publisher stays in place. `cf-worker-settings.test.mjs` exercises the actual Coordination profile, one read per phase, and missing-token rejection before and after publication. ## Deployment account variable Set the non-secret **Actions repository variable** `CLOUDFLARE_ACCOUNT_ID` in `tutur3u/platform` (Settings → Secrets and variables → Actions → Variables). Use the account ID recorded in the service's `cloudflare.config.ts` and parity baseline; the deploy job checks it against the emitted artifact. One repository variable supplies `cron-control-production`, `devbox-control-production` and `coordination-production`. If any of those environments defines the same variable, it must have the same value because the environment value takes precedence. Missing or mismatched configuration blocks deployment. Source publication does not provision this variable; the release owner must set it before promotion. ## Cached Build Output The root Turbo `build.outputs` includes `.cloudflare/output/**`, including hidden Vite metadata. Cache hits must restore the complete emitted tree before parity, runtime and publication checks. `.cloudflare/state` remains outside the output scope. `cf-build-output-cache.test.mjs` runs a disposable emit-only task through an actual cache miss and hit, verifies restored bytes and execution count, and checks the old output list as a negative control. It never compiles an app or uses shared/remote cache credentials. ## Production cutover checklist For each service, the orchestrator verifies before promotion: 1. Exact-commit CI on the promoted SHA is green for the service workflow, including: source runtime checks, emitted Build Output parity (`cf-worker-parity.mjs` prints `matches deployment identity`), emitted runtime checks, the `cf deploy --prebuilt --dry-run --no-provision` step, and the uploaded `-worker-` artifact. 2. The production job publishes that artifact with `cf deploy --prebuilt` (no rebuild) and the same `--message` SHA. 3. The deploy job runs `cf-worker-settings.mjs before` before any publication or secret mutation, then `after` before recording success. It checks the pinned account, compatibility, preview URLs, observability/logs, cron schedules and declared secret names; snapshots routes/custom domains and hashes remaining settings without persisting text bindings. It captures the active deployment and version IDs (`cf workers deployments list`) as the rollback target. Also capture `cf workers get `, `cf workers secrets list` and, with the Cloudflare API, the script's cron schedules (`/workers/scripts//schedules`), routes or custom domains, and `workers.dev`/preview URL state. The job captures them again after deployment and compares; unexplained changes fail the job before its success marker. Evidence is retained even on failure. This route-free pilot supports at most 20 account zones and one API list page, uses at most 28 GET requests per snapshot, and fails closed on missing read permissions, truncated results or unsupported scope. It never retries writes. The expected preview setting is explicitly true; this is unverified against current production until the preflight runs. A mismatch stops before mutation. To change crons, text vars or Durable Objects, the parity baseline records a reviewed `transition` (`decision` ID and `fields`, limited to `crons`, `vars`, `durableObjects` and `durableObjectClasses`) from `previousIdentity` (live) to `identity` (the emitted artifact). Every other identity field must be equal and every declared field must change. `before` reads once and must match exactly one stage, verifying text values (digest only, differing names in errors) and Durable Object bindings; unreviewed binding attributes such as `environment` or `dispatch_namespace` fail. A Worker matching neither stage fails closed. `after` must match the target. On the first publication only the settings hash, bindings and schedules may change; once live already matches the target, settings must stay unchanged. Storage classes are verified on the artifact, not read back live. Remove the transition in a follow-up change after production matches the target. 4. Production publication for Devbox Control and Coordination remains Wrangler with their applied `v1` migration metadata. A separate reviewed change must carry the rehearsal receipt before enabling cf publication. For these Workers, rehearse the first cf deploy on a disposable Worker created with the same Wrangler migration history and data, and confirm object data survives the switch to `exports`. Do not promote until that rehearsal passes. 5. Health checks pass on the canonical host, and the deployment marker records the SHA. 6. If a check fails, roll back with `cf workers deployments create` to the captured version, then reconcile before retrying. # ChatGPT and MCP Source: https://docs.tuturuuu.com/build/development-tools/chatgpt-mcp Portable plugins, hosted read workflows and authorization gates. Source audit: October 1, 2026. This package now has a portable root `plugin.json` and `mcp.json`, with `.codex-plugin/plugin.json` and `.mcp.json` retained for compatibility. The portable manifest deliberately omits `extensions.com.openai`, so the existing OpenAI presentation overlay remains active in full. Portable version is omitted; Release Please continues to own the compatibility manifest's version. No legacy `ai-plugin.json` is used. ### Capability and security gates | Capability | Existing source / first slice | Gate before claiming ChatGPT delivery | | - | - | - | | Portable packaging | Root manifests plus existing developer skills | Install and smoke-test the package in each intended host | | Workspace discovery and task summaries | Local opt-in CLI bridge plus hosted user/grant-bound source, tested with synthetic accounts | Reviewed issuer/grant/session authority, private-board/RLS proof and permitted-account delivery | | Tasks / Calendar navigation and Calendar summaries | Canonical links and hosted bounded event projection; existing Calendar permission preserved | Signed-in smoke tests and reviewed Calendar/RLS compatibility; no live event reads yet | | Hosted ChatGPT MCP | Connected official SDK source and real Streamable HTTP protocol tests; no registered endpoint | Public HTTPS deployment, persistent grants, provider/live-session policy, read-only API/RLS and rate admission | | ChatGPT OAuth | Existing CLI browser exchange and external-app token exchange are different protocols | Protected-resource and issuer discovery, resource/audience binding, authorization code + S256 PKCE, exact redirect allowlist, scopes and consent; choose CIMD or supported registration flow | | MCP Apps UI | No widget or UI resource yet | Standard MCP Apps bridge first, output schemas, resource CSP and approved origins; keep credentials server-side | | Sidebar / panels / deep links | Optional OpenAI extension stage | Capability detection and MCP App entrypoints; Free/Go web extensions are documented as coming soon | | Composer mentions | Optional later stage | Desktop only per current docs; workspace-authorized search and IDs, never raw token/file paths | | File viewers / editors | Deferred; no current workspace task need | File access/size/type authorization and user-confirmed writes | | Writes / marketplace | No write tools, public submission, credentials, grants, or production configuration changes | Separate parent approval, reviewed scope/consent, idempotency tied to actor/workspace, stale-state checks; publication requires review materials and remote endpoint | Current OpenAI docs describe portable Agent Plugins as the authoring format, MCP Apps as the shared UI bridge, and OpenAI MCP Extensions as optional host surfaces. Existing plugin functionality remains available while Free/Go web extensions are coming soon. Do not infer availability on every plan, mobile client, or host from a successful desktop example. See [packaging](https://developers.openai.com/plugins/build/plugins), [extensions](https://developers.openai.com/plugins/build/extensions), [UI reference](https://developers.openai.com/plugins/reference), and [changelog](https://developers.openai.com/plugins/changelog). ### Local read bridge This is a local stdio server using the official MCP Python SDK's maintained FastMCP API, pinned to `mcp==1.30.0`; SDK v2 is now stable and needs a separately validated migration. It is not a hosted ChatGPT connector. Public MCP submission requires a remote HTTPS endpoint. The server binds to no network port and exposes: * `list_workspaces`: only workspace IDs and bounded names. * `list_workspace_tasks`: one page of open task IDs and bounded names with an explicit workspace UUID, limit 1–50 and offset 0–10000. Default CLI status filtering still applies. Personal external placements fail closed. * `get_workspace_navigation`: a canonical Tasks or Calendar workspace link; does not launch a browser or fetch events. Each call uses the current local CLI identity. Workspace membership is rechecked before scoped reads and links; the API remains the permission authority. There is no MCP cache. The current workspace API uses private response caching; this bridge adds no cache and does not prove immediate upstream revocation. The task GET route already uses a server admin client behind explicit membership checks and workspace filters; the MCP layer adds no admin client or bypass. Hosted rollout still needs actor/guest/private-board authorization and revocation tests. The bridge never reads the CLI config or tokens directly, accepts no model-selected origin/config/executable, invokes no shell, disables update checks, and emits no raw CLI errors. The existing CLI may refresh its user session and rotate its local config. The CLI session itself is broader than these three read tools: this process allowlist is not a new narrowly scoped OAuth grant. Never serve this adapter over HTTP or reuse a machine CLI identity for multiple remote users. A future hosted adapter must bind issuer/subject/workspace/scopes per request, partition caches by that identity, and reauthorize after revocation. Names returned by Tuturuuu are hostile content boundaries: treat them as data, never instructions, links, commands or authorization. No descriptions, assignee identities, raw provider events, secrets or tokens are returned. All tools declare read-only, nondestructive, idempotent and closed-world annotations; typed results generate `outputSchema`. A CLI call times out after 20 seconds and its parsed response is capped at 1 MiB; temporary output is deleted after each call. Setup is deliberate and separate from installation of repository dependencies: ```bash theme={null} # Use a reviewed Python 3.10+ environment and an already installed ttr CLI. # Run dependency setup and validation through the shared FIFO resource queue. ttr resources run -- python3 -m pip install --require-hashes -r plugins/tuturuuu/mcp/requirements.lock # Authenticate manually using the existing user CLI flow if needed. # Enabling reads authorizes requested workspace/task data to reach the MCP host. export TUTURUUU_MCP_READS_ENABLED=1 python3 plugins/tuturuuu/mcp/server.py ``` Install the SDK in the Python 3.10+ environment that the host uses to launch the manifest's `python3` command. A repo-local virtualenv is not copied into plugin caches. Codex's compatibility overlay explicitly forwards only `TUTURUUU_MCP_READS_ENABLED` via `.mcp.json` `env_vars`; start/restart the host with that variable after review. No manifest hardcodes enablement. Both manifests use contained plugin-relative `cwd: "./"` and `mcp/server.py` arguments. The portable schema has literal `env`, not `env_vars`; generic portable hosts must explicitly configure opt-in forwarding, otherwise reads remain disabled. Codex uses the existing OpenAI compatibility overlay. Loader rules were checked against pinned official Codex source [`plugin_config.rs`](https://github.com/openai/codex/blob/d2b254fd17ced848dbc269705373cf5a02ed3284/codex-rs/codex-mcp/src/plugin_config.rs) and [`agent_plugin_config.rs`](https://github.com/openai/codex/blob/d2b254fd17ced848dbc269705373cf5a02ed3284/codex-rs/codex-mcp/src/agent_plugin_config.rs). The legacy loader joins relative cwd to the package root and does not expand `${PLUGIN_ROOT}` in arguments. Tests launch the actual packaged manifests using these path/forwarding contracts and the real Python SDK; they do not execute the Rust loader or prove live host installation. Local discovery also fails closed while its actor-bound Hidden reader is unavailable: it returns no workspaces. The installed CLI currently has no owner Hidden read command. The trusted integration seam must read the owner-only route with the current CLI `whoami` identity and `expectedActorId`, without token export, admin fallback, or caching. Missing/malformed authority or an actor switch returns no discovery rows. Hidden never removes canonical membership: explicit authorized task reads and navigation still work. Production wiring of that seam remains a gate. Do not place tokens in either manifest or enable reads globally without understanding the host's data handling. Reinstall/refresh the plugin cache after source changes; cached packages do not follow source edits automatically. Offline verification (synthetic responses; does not authenticate or call APIs): ```bash theme={null} ttr resources run -- python3 -m unittest discover -s plugins/tuturuuu/mcp -p 'test_*.py' python3 plugins/tuturuuu/scripts/validate_plugin.py ``` The focused suite checks scope rejection, missing membership, cross-workspace and external rows, bounded pagination/output, sensitive-field projection, safe error handling, opt-in behavior, and actual SDK schemas/annotations. Live user reads, ChatGPT host installation, OAuth, UI, and CI builds are separate evidence gates. ### Smallest staged rollout and owned paths 1. Local read slice: `plugins/tuturuuu/{plugin.json,mcp.json,.mcp.json}`, `.codex-plugin/plugin.json`, `plugins/tuturuuu/mcp/`, the plugin validator, and this existing documentation page, and `.github/workflows/codex-plugin.yaml` for offline protocol/security tests. No mobile, workspace picker, Calendar UI, database, SDK, or live API files change. 2. Hosted read adapter: propose isolated `packages/mcp/` transport/auth modules and `apps/web/src/app/api/mcp/` only after OAuth design review. Keep Rust/API migration registration in step under repository rules. Reuse existing user-authorized API reads; never introduce a service-role bypass. Gate calendar reads on verified per-user/provider visibility and bounded time ranges. 3. MCP App: a workspace/task view built on the standard bridge, then optional sidebar/panel entrypoints and desktop composer mentions when supported. 4. Writes and publication: independent review and approval; use explicit proposed changes and server-enforced confirmation/idempotency, then host testing, privacy/support/terms, test-account and review materials, metadata scan and directory publication. No stage silently enables another. Authentication and review requirements: [OAuth](https://developers.openai.com/plugins/build/auth), [security and privacy](https://developers.openai.com/plugins/guides/security-privacy), [MCP review](https://developers.openai.com/plugins/deploy/app-review), and [submission](https://developers.openai.com/plugins/deploy/submission). MCP itself requires no OpenAI API key or paid model call. ### Corrected hosted boundary plan Reconciled against verified remote main `e819953329392db746bb22e2e918f3c8bbb06579` on October 1, 2026. The local stdio implementation above is Codex/desktop groundwork, not completion of the requested hosted ChatGPT integration. Portable packaging supports local stdio, but public ChatGPT MCP distribution needs a remote HTTPS endpoint and its own authorization contract. No placeholder remote URL or registered connection ID is advertised. Current source boundaries to reuse: * `packages/internal-api/src/workspaces.ts` and `tasks.ts` own typed user-facing read requests; `client.ts` owns API transport. Do not wrap every API operation. * `packages/tasks-api/src/server/tasks/route.ts` owns task visibility and membership checks; the current handler has admin-backed branches. A hosted adapter must not treat a verified MCP token as permission to use those branches without checking the actor, workspace, private board and guest boundaries. * `apps/web/src/lib/api-auth.ts` and `api-auth-audiences.ts` own gateway session, audience, abuse and step-up decisions. Its `allowAppSessionAuth` path explicitly uses an admin-backed client. Do not enable that path for MCP as an RLS substitute. * `packages/auth/src/cli-session.ts` grants broad `cli:access`; it is not the hosted token contract. `app-session.ts` handles internal target-app sessions. Neither token is returned to ChatGPT or accepted as an MCP access token. * `apps/web/src/app/api/v1/auth/app-token/exchange/route.ts` handles external-app secrets, scopes, workspace checks and rotation. Reuse its authorization and refresh principles; do not send ChatGPT a cross-app token or app secret. * `apps/web/src/lib/auth/oauth-providers.ts` configures incoming account sign-in providers. The audit did not find a complete MCP OAuth authorization server in this source. Outbound Google/SePay OAuth is unrelated. Issuer capability, client registration, consent storage and audience binding remain explicit design gates. Proposed hosted dataflow, source design only: 1. ChatGPT discovers the MCP protected-resource document and the approved issuer's discovery document. No account-specific data is available anonymously. 2. Authorization code + S256 PKCE enters the existing Tuturuuu sign-in boundary. A Tuturuuu-owned consent screen chooses one accessible workspace and read scopes for workspace discovery/task summaries/navigation. Preserve required MFA and deny suspended users. Bind client, exact redirect, resource, workspace and granted scopes to a single-use code; never infer consent from sign-in alone. 3. The approved issuer produces a resource-bound OAuth user access token with subject, expiry, client and revisioned MCP grant claims. ChatGPT receives this restricted credential. Ordinary account, CLI, internal-app and refresh tokens are rejected as tool-access credentials and never appear in tool output. 4. A stateless Streamable HTTP request verifies issuer/signature or introspection, audience/resource, expiry, grant revocation, scopes and actor on every POST. Reject browser cookie-only requests, unsafe Origin/Host, oversized JSON/batches and unsupported methods. Missing auth returns the discoverable 401 challenge. Apply shared gateway abuse/rate limits and redacted correlation-only logging. 5. Construct a request-scoped typed read client using `packages/internal-api`. The adapter uses the verified OAuth user JWT on three fixed resource-read GET paths plus the owner-private Hidden GET at canonical platform/Tasks/Calendar origins. Release requires those gateways to enforce the MCP audience, client, domain grant and read-only user authority. Ordinary Supabase `authenticated` audience acceptance is insufficient. If gateway/RLS compatibility cannot be established safely, keep this adapter disabled and implement the narrow actor-aware boundary first. No process CLI identity, workspace API key or service-role client is constructed by MCP. 6. Recheck selected-workspace membership and operation-specific task visibility; return only projected summaries and canonical allowlisted links. No MCP cache in the first hosted slice; later cache keys include issuer/subject/grant/workspace and scopes, and never bypass revocation. Names remain untrusted content. 7. Start without widgets or writes. The prepared Calendar adapter preserves the current `manage_calendar` requirement and projects event ID/title/times only, within a seven-day interval and 100-event cap. It does not weaken Calendar permissions or return guests, descriptions or provider credentials. Add MCP Apps UI only after this read path passes isolation tests; use standard bridge/resource CSP before optional OpenAI sidebar/panel/desktop composer surfaces. ### Prepared hosted source slice The source has one connected composition root and remains unregistered. The shipping web package pins the official `@modelcontextprotocol/sdk@1.31.0` in `apps/web/package.json` and the root `bun.lock`. Package-manager generation added only that workspace dependency and eight new SDK-related lock records; existing package records, other workspaces and lock settings did not change. Existing JOSE, Zod and internal-api dependencies support the source core. A bounded local FIFO run passed all 37 focused tests, including three real official SDK client/server Streamable HTTP tests. It exercised initialize/initialized, typed tools/list, task/navigation tools/call and forged-workspace/cross-tenant rejection through JWT, provider and typed API boundaries. Test identities, signing keys and upstream provider/API responses are synthetic; no live grants, credentials, public endpoint, database or application build was used. This proves the source read workflow, not deployed OAuth/RLS authority or production readiness. Exact owned paths: | Paths | Responsibility / prerequisite | | - | - | | `apps/web/src/lib/mcp/contracts.ts` | Revisioned actor/client/resource/workspace grant, projected outputs and mandatory fresh authority interfaces | | `apps/web/src/lib/mcp/authorization.ts` | Official JOSE signature validation; issuer/audience/expiry/client checks; fresh grant, provider session, account, MFA and admission checks | | `apps/web/src/lib/mcp/provider.ts` and `provider-client.ts` | Installed Auth SDK response contract, fixed issuer identity/grant GETs, public publishable key and mandatory live-session policy; no administrator or refresh session | | `apps/web/src/lib/mcp/read-service.ts` | Visible workspace discovery; bounded task/Calendar summaries and navigation require explicit workspace grants plus current member/operation permission | | `apps/web/src/lib/mcp/internal-api.ts` | Fixed-origin GET-only, no-store, redirect-denying user bearer transport; 10-second timeout, 1 MiB cap, tenant projections and owner Hidden API allowlist | | `apps/web/src/lib/mcp/visibility.ts` | Per-request verified-actor Hidden reader through existing internal-api transport, expected actor check, unavailable route means unknown, no cache | | `apps/web/src/lib/mcp/consent.ts` and `grant-store.ts` | Pure trusted provider/actor/redirect/workspace consent preparation; persistent atomic reserve/activate/revoke contract and owner-bound grant reader; no production store or provider approval | | `apps/web/src/lib/mcp/runtime.ts` | Single provider/grant/session/visibility/admission/internal-api/HTTP composition root; no environment defaults | | `apps/web/src/lib/mcp/http.ts` | Unregistered HTTP factory/discovery payload; stateless request instances, 64 KiB body cap, Origin/Host checks and sanitized errors | | `apps/web/src/lib/mcp/tools.ts` | Four accurate typed output schemas and read-only/idempotent/non-destructive/closed-world annotations; hostile-content instructions | | `apps/web/src/lib/mcp/hosted.test.ts`, `workflow.test.ts` and `workflow-fixtures.ts` | Unit coverage plus composed signed-JOSE/provider-REST/internal-api/task projection/Hidden/consent/store fixtures; passed in the focused suite | | `apps/web/src/lib/mcp/sdk.ts` and `sdk-workflow.ts` | Concrete official SDK Streamable HTTP composition; fresh server/transport per request, JSON responses, structured outputs, no shared sessions; exact official SDK dependency declared | | `plugins/tuturuuu/hosted/sdk-protocol.test.ts` | Real SDK client/server initialize, initialized notification, typed tools/list, task/navigation tools/call and cross-tenant failure tests; all three passed | | `plugins/tuturuuu/hosted/vitest.config.mjs` and `tsconfig.test.json` | Focused Node/one-worker non-build tests, isolated from unrelated sparse project references and application environment loading | | Existing portable manifests, local Python MCP, validator, plugin workflow and this page | Local groundwork preserved separately; no remote endpoint or connection ID advertised | No route, route-override, mobile, workspace selector, existing API, database or Calendar lane file is changed. Route registration and any Rust gateway mirror remain required before deployment, using the repository's normal ownership rules. ### Provider, consent and release gates Current Supabase documentation describes a managed OAuth 2.1/OIDC server with authorization-code PKCE, custom consent and user grant revocation. It is a candidate issuer, not evidence that the Tuturuuu project has enabled it. The provider handles codes, token issuance and refresh; do not implement custom OAuth cryptography. Use its authorization-details and approval/denial APIs from a trusted authenticated consent boundary with existing MFA/account rules, same-origin CSRF protection and exact approved client/redirect checks. Persist actor/client/resource/selected workspaces/read scopes before approval in an atomic revisioned grant store. No consent page, grant store, access-token hook or OAuth configuration was created. [Supabase OAuth flows](https://supabase.com/docs/guides/auth/oauth-server/oauth-flows). Supabase's default audience is `authenticated`; OAuth tokens carry `client_id`. Its OIDC scopes are not Tuturuuu task/Calendar permissions. The prepared `mcp:*:read` values are a local domain grant contract, not a claim that the provider supports arbitrary OAuth scopes. A reviewed provider hook must bind the MCP resource audience and grant ID/revision. Restrictive RLS and gateway checks must prevent this JWT from gaining ordinary write or unrelated workspace access via another API. No RLS, signing key, issuer, hook or gateway security configuration was changed. [Supabase token security](https://supabase.com/docs/guides/auth/oauth-server/token-security). The current user API wrappers can enter administrator proxy paths and some task and Calendar handlers use admin queries after authorization. Their existing membership/private-board gates must be proved for OAuth user tokens; MCP does not enable app-session fallback or construct a service-role client. Raw public-key Supabase identity access is required at the provider seam. The installed `@supabase/auth-js@2.117.1` differs from the public flow page examples: it exposes `oauth.listGrants()` and rows with `client.id`. That SDK method requires a local refreshable session, even with global bearer headers. The prepared stateless provider reader therefore uses its two fixed read endpoints (`/user` and `/user/oauth/grants`) with the verified request JWT and public publishable key; it does not fabricate a refresh session or credential vault. `getUser` and JWKS verification alone do not satisfy the live-session/account/MFA policy contract. The parallel workspace lane defines owner-only `HIDDEN_WORKSPACE = true` rows in `user_workspace_configs`, with absent rows visible and an actor-bound owner GET. The verified base does not yet contain that lane's route. The prepared reader returns `visible`, `hidden` or `unknown`; Hidden/unknown are excluded from discovery and consent choices. Explicit granted reads and authorized navigation preserve canonical membership, matching that lane's documented deep-link rule: hiding is not permission revocation. The route must be integrated and its OAuth user behavior proved before deployment. This lane does not edit or copy its preference writes. There is no MCP data cache or cross-user session cache. Task API responses that include external placements or lack the workspace relation are rejected in full; native-only API query behavior remains a compatibility gate. No OAuth grant, secret, client registration, public endpoint, database/security configuration or marketplace action occurs during this source slice. Before runtime wiring, resolve issuer, consent/grant storage, live session policy, Hidden visibility and gateway/RLS authority; before installs or tests, use bounded FIFO admission. Prepared synthetic tests cover wrong issuer/audience, expired/revoked grants, refresh-as-access, account switching, forged workspace IDs, guest workspace access, Origin/Host confusion and hostile names. Private-board API fixtures, real signature/key-rotation tests, real SDK initialize/list/call tests, provider refresh/revocation, restrictive RLS and gateway compatibility tests remain required. Delivery evidence also needs actual Streamable HTTP host tests and parent-approved permitted-account reads. Writes require separately reviewed confirmation/idempotency; widgets require standard MCP Apps bridge/CSP review; marketplace publication remains a later parent-approved stage. ### Remaining authority design against existing contracts The runtime's required policy/store callbacks are seams, not a deployed authority. No environment loader supplies them and no public route invokes the workflow. Keep `securityReviewComplete` false until these boundaries are implemented and reviewed with the provider and gateway behavior. * `packages/supabase/src/next/auth-session-user.ts` revalidates identity with `getUser()` after checking claims. Reuse that identity principle, but do not equate identity/JWKS success with an authoritative active OAuth session. No existing source contract inspected here proves owner-bound live `session_id` and client revocation together. A narrow reviewed provider/session adapter remains required; never query unrestricted auth tables via an MCP admin client or fabricate a local refresh session to obtain SDK grant methods. * `packages/utils/src/required-mfa-policy.ts` owns the server `app_metadata` policy and verified AMR/session proof helpers: `readRequiredMfaPolicy`, `mfaProofFromVerifiedClaims` and `satisfiesRequiredMfaPolicy`. A future adapter must carry already-verified claims and current server-owned metadata in request context, bind proof to the same session, and preserve recovery/primary-proof cutoffs. The current `(userId, sessionId)` callback is intentionally unwired; enrich its verified context before implementing MFA policy. Refresh `iat`, browser flags and editable `user_metadata` do not establish fresh MFA. * `packages/utils/src/abuse-protection/user-suspension.ts` owns suspension reads. Some existing `apps/web/src/lib/api-auth.ts` branches catch suspension lookup errors and continue. Do not inherit that fail-open behavior for MCP: unavailable policy or unknown account/session state must deny access. * The persistent grant store must uniquely bind user/client/provider authorization ID, resource, redirect, selected workspace IDs and domain read scopes. Reserve consent atomically; activate only after trusted provider approval; the token hook reads only active grants. Never reuse generic editable user configuration as grant authority. Revisioned revoke/change commits must reject old JWTs before returning tool data. Provider revocation is a separate distributed side effect: use durable reconciliation/outbox semantics rather than claiming an atomic database/provider transaction. No schema, RPC, privileged policy or hook is created by this slice. ### Focused validation and SDK provenance From an installed checkout, run the non-build Node tests with one worker: ```bash theme={null} node node_modules/vitest/vitest.mjs run \ --config plugins/tuturuuu/hosted/vitest.config.mjs --configLoader native \ --maxWorkers 1 --no-file-parallelism ``` Locally, wrap that command in `ttr resources run --`. The focused config isolates unrelated TypeScript project references and never loads application environment files. It resolves the shipping SDK from the web package. An explicitly owned isolated validation installation can be selected with `TUTURUUU_MCP_SDK_DIR`; that option changes only test module resolution, not runtime authorization or API origins. `workflow.test.ts` uses an explicitly synthetic dispatcher to isolate the source boundary; `sdk-protocol.test.ts` uses the actual official Client, Streamable HTTP client transport, McpServer and Web Standard Streamable HTTP server transport. Both share real JOSE-signed synthetic identities and the actual internal-api client. No stub dispatcher is used to claim SDK protocol compatibility. The official registry's compatible v1 package `@modelcontextprotocol/sdk@1.31.0` was published September 28, 2026 at 18:59:36 UTC. It exceeded this repository's `minimumReleaseAge = 86400`; MCP has no exemption. The newer generation uses separate v2 packages; this source targets the maintained v1 API. See the [official v1 release](https://github.com/modelcontextprotocol/typescript-sdk/releases/tag/1.31.0). The SDK lock integrity matches the registry: `sha512-UvTMgnNlnIBO/22ob2RcVGDlcvOslQs8T59+FTGdA0L27a39fdGF/EDETNtDVK4DZGpwomlsYpRdA8UXcVL/pw==`. Local SDK installation used an owned isolated directory/cache, exact pin, disabled lifecycle scripts and normal FIFO admission. The shipping manifest/lock were package-manager-generated in a manifest-only workspace copy with unchanged existing patch/vendor inputs, then inspected before export. No donor/global dependency or credential mutation occurred. Focused evidence does not replace exact-commit CI lint/type/build checks, live provider refresh/revocation, private-board/RLS fixtures or permitted-account delivery checks; the parent owns integration and monitoring. Provider consent must prove the authorization transaction was issued for the exact MCP resource before grant reservation. The installed provider SDK currently omits that resource field, so its unaugmented authorization details intentionally fail closed. A reviewed provider resource-binding integration is still required before activation; the source factory does not infer audience from configuration or user input. The packaged local bridge uses `python3` (Python 3.10 or newer), with a generated transitive dependency hash lock and pip cache in CI. A missing trusted Hidden discovery reader returns an empty workspace list without launching the CLI. Local CLI output is bounded while reading the pipe; oversized output terminates the child before it can accumulate in a temporary file. # CI/CD Pipeline Optimization Source: https://docs.tuturuuu.com/build/development-tools/ci-cd-optimization Centralized affected-path gating and smart caching to minimize unnecessary CI/CD runs. > [!NOTE] > > **Prerequisite**: Read the [CI/CD > Pipelines](/build/development-tools/ci-cd-pipelines) page to understand the > basic workflow structure. ## Overview Our CI/CD pipelines avoid wasted builds, tests, and deployments through a **centralized affected-path gate** plus **build caching**. Instead of every workflow re-implementing its own change detection, automatic deployments share the same TypeScript decision function while avoiding a separate configuration runner when the decision can be made inside an already-required job. This page documents how that gate works, how the per-app Vercel workflows are wired, and how to debug skip decisions. The maintained runtime is Next.js Web and its satellite apps. Rust (`apps/backend`) and TanStack Start (`apps/tanstack-web`) are paused and not in use. Docker setup is inactive and the Docker cron runner is retired. Retained migration designs describe a possible future, not live traffic or a current setup requirement. See the [active runtime map](/build/devops/active-runtime). ## Parallel test validation Mobile analysis and four Flutter test shards run independently. The stable `build` check requires analysis and every shard to succeed; a cancelled or unexpectedly skipped shard fails the gate. Flutter's native `--total-shards=4` and zero-based `--shard-index` partition all test files, with two processes per runner. Format, static analysis and Bloc lint run once in the analysis job. TypeScript unit tests and coverage each use four workspace shards. The planner in `scripts/ci/test-shards.js` assigns every workspace with a `test` script (except the paused TanStack app) exactly once, putting larger suites first and balancing by test-file count. Counts only guide scheduling: each workspace still runs its complete configured Vitest suite, preserving isolation, discovery and coverage thresholds. New test workspaces join automatically. Each runner uses at most two Turbo tasks with two Vitest workers per task. Coverage artifacts retain their workspace paths. Only after all shards pass does the coverage gate download the complete set and upload once to Codecov. Shard-specific fallback-cache families prevent concurrent jobs from competing for one cache entry. Main and production runs are never cancelled by a newer push; superseded feature-branch runs are cancelled. Inspect a shard locally with `node scripts/ci/run-test-shard.js 0 4 --plan`. Execute it through `ttr resources run -- node scripts/ci/run-test-shard.js 0 4` (or append `--coverage`). Reproduce a Flutter shard with `flutter test --total-shards=4 --shard-index=0 --concurrency=2` from `apps/mobile`. Use the resource queue locally rather than launching CI-sized parallel runner groups on one machine. Measure optimization using actual Actions step start/end times and aggregate runner minutes. Compare cold runs separately from Turbo cache hits. The prior observed unit and coverage test steps took 15m07s and 16m27s; the mobile test step took 4m04s after analysis. New timings must be measured on the changed workflow; increasing shard count alone is not proof of a speedup. ## The Problem Before centralized gating, workflows ran on every push regardless of: * Whether files relevant to that specific app or package actually changed * Whether the change only touched an unrelated app * Whether build artifacts were already cached With a large monorepo of apps and shared packages, this meant a single commit to one app could trigger dozens of unrelated build and deploy workflows, slowing feedback and congesting the Actions queue. ## How Gating Works Today Vercel affected-app gating does **not** rely on GitHub's native `on.push.paths` filters. Path filters are coarse (they cannot understand workspace dependency graphs), so the Vercel logic stays centralized. A narrow exception is the external-app compatibility build: its contract is simply "`apps/external` or any internal package changed," so native trigger paths can avoid creating a runner at all for unrelated app-only commits. The shared logic is organized in four pieces: 1. **`tuturuuu.ts`** — the source of truth. It exports a `ci` toggle map (each workflow can be globally enabled/disabled), the `vercelWorkflowTargets` table that maps each app to its preview/production workflow and package name, and the `getWorkflowDecision()` function that decides whether a given workflow is affected by a set of changed files. 2. **`scripts/ci/resolve-changed-files.ts`** — resolves the changed-file set for the current event (push payload, pull-request base/head, or git diff against the last successful deployment marker). 3. **`.github/workflows/ci-check.yml`** — the reusable gate for non-Vercel workflows that still need an individual `should_run` output. 4. **`.github/workflows/vercel-production.yaml`** — the single production planner. It resolves Vercel and Cloudflare apps against each app's last successful deployment marker in one runner, then calls affected workflows as reusable jobs in the same commit-associated push run. ### Reusable and inline gates Test and Coverage opt into `check_release_only: true` on the existing `ci-check.yml` job. One full-history sparse checkout at `github.sha` now resolves both the CI switchboard and the release-only comparison; callers use its separate `should_run` and `run_checks` outputs. Other callers retain the default `run_checks=true`. Missing, zero, invalid, or unavailable comparison SHAs and malformed manifests still default to running checks. A failed preflight cannot schedule the heavy jobs; shard aggregates still require every shard to succeed. The shared-preflight change removed three redundant runner allocations per push that creates Test, Coverage, and Type Check, without changing tests, shard counts, aggregate check names, or coverage uploads. The singleton Type Check exception below removes one additional allocation. It does not estimate elapsed time savings. Before removing the `Check release-only changes` jobs, live effective branch rules for `main` and `production` and ruleset `8839175` were checked: they required PR approval, code-owner review, and resolved threads, with no required status-check contexts; legacy branch protection was absent (verified on October 1, 2026). Recheck those rules before future check renames. Deployment-aware callers retain `deployments: read`. Run the focused contracts and decisions with: ```sh theme={null} node --test --test-concurrency=1 \ scripts/ci/ci-preflight-workflow.test.js \ scripts/ci/release-only-changes.test.js \ scripts/ci/check-workflow-config.test.js \ scripts/ci/resolve-changed-files.test.js ``` Type Check is a singleton exception: `type-check.yaml` evaluates the same changed-file, CI configuration, and release-only scripts directly inside its existing `Type Check` job. One full-history checkout pinned to `github.sha` serves preflight and the existing heavy steps. Only when both decisions are `true` do Bun setup, cache restore, install, dependency builds, and type checking run. Missing or uncertain comparisons still run checks; preflight script errors fail the named job before any heavy step. A disabled configuration skips release comparison as well as heavy steps; its absent release output cannot enable work. There is no condition override that continues after a failed step. Keep inline resolve/config/release wiring aligned with `ci-check.yml`; the workflow comment records intentional differences and `type-check-inline-workflow.test.js` compares the actual shell commands to protect parity. A configuration-disabled or release-only run now reports a successful `Type Check` job with heavy steps skipped and an explicit skip summary, instead of a skipped job. It does not claim compilation occurred. Effective GitHub rules for `main` and `production` still had no required status-check contexts when this change was prepared on October 1, 2026. The job name, triggers, runner label, and secrets remain unchanged. The whole singleton job explicitly uses `permissions: { contents: read }`; unspecified token permissions are `none`. This keeps preflight read-only and removes the heavy job's inherited write access. Checkout and Node setup need repository read access. Type Check's changed-file resolver uses push payloads or manual dispatch, not deployment API markers, and does not inject a token into its preflight environment. Bun setup, install, compiler commands, and the skip summary need no GitHub write grant. GitHub cache save/restore uses the Actions runtime token; Turbo remote caching uses its existing separately scoped credential. The workflow has no artifact upload, publishing, deployment, or custom check/status API step, so no privileged job is needed. GitHub Actions generates the named job check. Deployment-aware callers continue using the reusable gate with `deployments: read`. This singleton layout removes one additional runner allocation relative to the shared-preflight layout (two to one), or two relative to the earlier separate CI-config and release-only layout (three to one). It cannot affect runs already queued under older workflow source and does not establish the cause of hosted runner delays. Verify it with `node --test --test-concurrency=1 scripts/ci/type-check-inline-workflow.test.js` alongside the focused contracts above. Non-Vercel gated workflows can declare a `check-ci` job that calls `ci-check.yml`, then gate their real job on the result. Platform preview is different: it must complete on every protected `main` push so `supabase-staging.yaml` receives its workflow-run signal. Its deploy job checks configuration and affected paths inline, then skips install/build/deploy steps when the app is unaffected. This keeps the workflow signal while using one runner instead of a configuration runner followed by a deploy runner. ```yaml theme={null} jobs: Deploy-Preview: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: fetch-depth: 0 - uses: actions/setup-node@v7 with: node-version: 24 - name: Compute changed files id: changed_files run: node --experimental-strip-types scripts/ci/resolve-changed-files.ts - name: Check preview configuration id: check_config run: node --experimental-strip-types scripts/ci/check-workflow-config.ts ``` Internally, `ci-check.yml` uses a sparse checkout (only the manifests and scripts it needs), computes the changed files, and runs the decision script: ```yaml theme={null} - name: Compute changed files id: changed_files env: GITHUB_TOKEN: ${{ github.token }} WORKFLOW_NAME: ${{ inputs.workflow_name }} run: node --experimental-strip-types scripts/ci/resolve-changed-files.ts - name: Check Configuration id: check_config env: CHANGED_FILES_FILE: ${{ steps.changed_files.outputs.changed_files_path }} WORKFLOW_NAME: ${{ inputs.workflow_name }} run: node --experimental-strip-types scripts/ci/check-workflow-config.ts ``` The scripts run with Node's `--experimental-strip-types`, so the `.ts` sources execute directly without a separate build step. Trusted manual satellite preview workflows launch their guarded deploy job directly: manual dispatch already bypasses affected-path gating, so a reusable preflight would only add a second queued runner. These workflows remain protected by the `main` dispatch ref, `TRUSTED_PREVIEW_DEPLOY_ACTORS`, and their GitHub Environment. Preview concurrency is keyed by workflow and `preview_ref`; a repeated request for the same preview cancels stale work, while different preview refs remain independent. ### The production planner A `production` push starts one `plan` job instead of starting one `check-ci` runner per Vercel app. `resolve-production-vercel-targets.ts` reads the workspace graph once, memoizes dependency closures, resolves each target from its own successful deployment marker, and emits a reviewable Actions summary. Static reusable jobs call selected Vercel and Cloudflare workflows, so deployments stay attached to the original push SHA instead of appearing as detached `workflow_dispatch` runs. Per-app production workflows remain independently protected by their GitHub Environment and a static per-app concurrency group. The group must include the app identifier because reusable workflows inherit caller context; a shared caller-derived key would cancel sibling deployments and show red X statuses instead of clean skips. They accept `workflow_call` for commit-driven planner jobs and retain `workflow_dispatch` for deliberate operator reruns. Their `cancel-in-progress` predicate is enabled only for a `production` push, so a `main` commit or manual recovery run cannot cancel an active production deploy. A missing or untrusted marker fails open for that app without forcing every other app's gate open. Colab, Coordination, Lettin, Meet, and Parley use the same per-app marker baseline. Their production push deployments are called by the planner; their `main` and pull request validation runs remain independent. Markers are recorded only after deployment verification, so a failed Cloudflare deploy is selected again on the next production push. Coordination uses its worker and coordination utility paths; the other apps use their workspace dependency closure and any explicitly shared runtime paths. ### The decision function `getWorkflowDecision()` in `tuturuuu.ts` resolves to `should_run` using this order of precedence: 1. **Global toggle.** If the workflow is `false` in the `ci` map, it never runs. 2. **`workflow_dispatch` bypass.** Manual runs always proceed (affected-path gating is skipped), so you can force a deploy from the Actions UI. 3. **Other workflows.** Workflows without an entry in `vercelWorkflowTargets` or `cloudflareProductionTargets` fall back to their static `ci` toggle. 4. **Unavailable change data.** If the changed-file set cannot be resolved, the gate stays **open** (`should_run = true`) to fail safe. 5. **Affected-path matching (deployment workflows).** The workflow runs only if at least one changed file matches one of: * a **global affecting path** (`bun.lock`, `package.json`, `turbo.json`, the shared Bun/remote-cache actions, or Vercel's deterministic metadata generator); * the workflow's **own** workflow file; * the target app's directory (e.g. `apps/web/**` for the platform target); * any package inside the target app's **workspace dependency closure**. The closure is built from `workspace:*` dependencies, so editing a shared package only triggers the apps that actually depend on it. Cloudflare Meet also watches its signaling Worker and meeting API paths. Coordination is a Vite+ workspace. Its worker and coordination utility paths remain explicit so source-level shared contracts also trigger validation. If nothing matches, the decision is `should_run = false` and the heavy job is skipped. ### Vercel target table Every Vercel app is registered once in `vercelWorkflowTargets`. Adding a new app means adding a single entry there (app slug, app path, package name, and the preview/production workflow filenames) — the gating logic and changed-file matching then apply automatically. ```ts theme={null} { app: 'platform', appPath: 'apps/web', packageName: '@tuturuuu/web', previewWorkflow: 'vercel-preview-platform.yaml', productionWorkflow: 'vercel-production-platform.yaml', } ``` **Applied to:** the 24-target production planner, the inline platform preview gate, and other gated CI workflows wired through `ci-check.yml`. The other 23 preview workflows are trusted manual dispatches and therefore do not allocate a standalone configuration runner. ## Build Caching Once a workflow decides it must run, cacheable JavaScript work is invoked from the repository root through `.github/actions/run-with-turbo-remote-cache/action.yml`: ```yaml theme={null} - name: Type check uses: ./.github/actions/run-with-turbo-remote-cache with: command: bun turbo:local run type-check --concurrency=4 token: ${{ secrets.TURBO_TOKEN }} team: ${{ vars.TURBO_TEAM || secrets.TURBO_TEAM }} ``` The composite action exposes credentials only to the wrapped command step. It runs locally when both token and team are absent, rejects a token without a team, and keeps Turbo's local rebuild behavior available if the remote service is unavailable. `TURBO_TOKEN` must never be passed to pull-request or Dependabot jobs. Those jobs restore a task-family-scoped `.turbo` fallback from GitHub's cache without writing shared entries; trusted default-branch jobs seed that fallback. The shared Bun runtime and package-download caches follow the same ownership rule: only trusted `main` jobs save entries. Production, pull-request, Dependabot, and other branch jobs restore the default-branch cache without creating branch-scoped duplicates. This is especially important for Bun's package cache, which can approach 1 GB for the full monorepo lockfile. Turborepo hashes inputs and reuses cached task outputs, so unchanged builds, type checks, and tests resolve to cache hits instead of recompiling. The task graph includes transit-only dependency nodes for tests: dependency source changes invalidate downstream tests without running every dependency's test suite. Build outputs include framework output, coverage, and TypeScript incremental state where those artifacts are deterministic. Output-affecting environment values are hashed, while CPU, heap, and concurrency controls are pass-through values. Every active Vercel project has a checked-in `buildCommand` that invokes its full workspace build through root Turbo. The 48 deployment workflows generate deterministic source metadata and then wrap `vercel build`; they do not run a separate dependency prebuild. This lets an unchanged full app build restore as one task while `.vercel/output` remains an in-job handoff to `vercel deploy --prebuilt`, not an uploaded artifact or generic cache. Docker and Rust verification use the same principle at their native cache layers. Each shared GitHub BuildKit scope has one trusted `main` writer: Docker setup owns web, TanStack, and storage scopes; Rust CI owns the backend scope; and E2E shard 1 owns the small leaf-service scopes. Production, other shards, and migration E2E restore without exporting. Expensive web, TanStack, and backend images use `mode=max`; validation-only development and small leaf images use `mode=min` or restore-only caching. Dockerized E2E jobs explicitly select a `docker-container` Buildx builder so Compose can import and export the GitHub cache backend. Turbo values enter image builds only as BuildKit secrets. Rust verify and deploy jobs share one pinned, toolchain-aware Cargo cache, with writes restricted to protected branches. Flutter workflows cache dependencies and native tool state (pub, Gradle, and CocoaPods), never final APK, AAB, IPA, archive, or application-bundle outputs. Dependency resolution and the platform build always execute so a cache hit cannot skip validation. Managed uv and CodeQL caches remain owned by their upstream actions rather than being duplicated. Automatic CodeQL is owned by GitHub's organization-managed `dynamic/github-code-scanning/codeql` workflow, which scans JavaScript/TypeScript and Python. The checked-in `.github/workflows/codeql.yml` is manual-only: it preserves an explicit fallback and avoids the Security UI's missing-workflow warning without adding push, pull-request, or cron duplicates. `bun git-sync` mirrors the already-scanned `main` SHA to `production`. The full Dockerized E2E graph is created for E2E specs, Playwright and Docker configuration, database fixtures, package manifests, lockfiles, and its own runner scripts. Ordinary application source commits do not create six expensive E2E consumers. There is no cron schedule: automatic E2E runs are commit-driven and path-scoped, while manual dispatch remains available for deliberate validation. E2E uses native push paths and launches its image producer directly, without an extra static switchboard job. Supabase staging and production migrations use the last successful migration deployment marker as their change-range baseline. Each workflow resolves the entire pending range, fails open when no trustworthy marker exists, and runs `supabase db push --include-all` only when `apps/database/**` or migration control files changed. Evaluation and deployment share one serialized job per environment, so no-op workflow-run signals finish quickly while database migrations cannot race. Production still requires the successful same-SHA platform deployment marker and staging migration before applying changes. ## Runner And Storage Policy Tuturuuu is a public repository in a GitHub Free organization. Standard hosted Linux and Windows jobs are budgeted for 4 CPUs and 16 GB RAM; the organization can run 20 jobs concurrently, including at most 5 macOS jobs. CPU-bound Turbo checks therefore default to concurrency 4. Memory-heavy Next.js and Docker stages limit inner build concurrency to 2 and retain an 8 GB Node heap. Keep independent jobs parallel, keep macOS matrices below the platform limit, and do not switch to billable larger runners as a cache optimization. The live repository cache configuration, queried through GitHub's API, is a 10 GB maximum with 7-day retention. The weekly Actions resource report groups cache usage by key prefix and reports informational status at 80%, warning at 90%, and critical status at 100%. It also reports artifact count, bytes, age, and largest workflow families without enforcing a fixed artifact byte cap. Steady-state cache usage should stay below 9 GB to leave eviction headroom. The July 10 audit observed active usage temporarily above the configured limit; that is eviction lag and cache churn, not an additional open-source allowance. Artifacts are retained only as long as their handoff or diagnostic value requires: * E2E and Playwright diagnostics: one failure-only artifact per shard or migration mode, 7 days. * Package-release tarballs: 1 day with compression disabled. * Mobile development deliverables: 7 days; production store deliverables: 14 days. Pre-compressed bundles are archived first and uploaded without redundant compression. * Discord JUnit and Cloudflare smoke diagnostics: failure-only or explicit 7-day retention as applicable. Every artifact upload must declare both retention and missing-file behavior. Required release outputs fail when absent; optional diagnostics may warn. Closed trusted pull requests delete cache entries scoped to their merge ref. Legacy broad Turbo, run-ID Supabase, final Flutter-output, and old Rust cache prefixes are removed manually only after their replacements demonstrate hits; default-branch CodeQL caches are never part of automated cleanup. > [!NOTE] > > Earlier versions of these workflows ran an inline Turborepo `--dry-run=json` > step to *decide* whether to skip a build. That per-workflow skip step has been > removed in favor of the centralized `ci-check` gate above. Turborepo is now > used for caching the build itself, not for the run/skip decision. ## Race-Condition Handling Production deployments serialize each app and branch with GitHub Actions concurrency: ```yaml theme={null} concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'push' && github.ref == 'refs/heads/production' }} ``` When another production commit arrives, GitHub cancels the superseded run and keeps the newest run deployable. Older runs no longer finish green after skipping every deployment step, which makes queued and deployed SHAs observable without allowing stale artifacts to replace newer production code. Commits that exist only on `main` and manual recovery dispatches leave an active production deployment running. Affected-app gating always diffs the newest SHA against the last successful deployment marker. If no trustworthy marker is available, the gate fails open and runs the deployment instead of considering only the newest push payload; this preserves app changes from canceled intermediate commits. ## Edge Case Handling `resolve-changed-files.ts` and the gate are written to fail safe. ### Missing or unresolvable change data If the changed-file set cannot be computed (e.g. an unusual event payload or a missing base commit), `getWorkflowDecision()` returns `should_run = true`. The guiding principle: **build when uncertain** rather than skip incorrectly. ### Initial commits and shallow clones When a parent commit is unavailable (the very first commit, or a shallow clone without history), the changed-file resolver cannot diff against a base, so the gate opens and a full build runs. The workflow that needs deeper history checks out with an appropriate `fetch-depth`, and `ci-check.yml` uses `fetch-depth: 0` with a sparse checkout so it always has enough history to diff. ### Last-successful-deployment markers For push events, the resolver can diff against the SHA of the last successful deployment (recorded as a GitHub deployment marker) rather than only the parent commit. This catches the case where intermediate commits never deployed, so the full diff since the last live deploy is considered. ## Monitoring & Debugging ### Inspect the gate decision `check-workflow-config.ts` logs its decision and the matched paths. Preview `check-ci` logs show the individual decision; production uses the planner's **Production Vercel deployment plan** job summary, including the per-app baseline source and deploy/skip result. Individual logs still look like: ```text theme={null} Workflow: vercel-production-platform.yaml Should run: false Reason: vercel-production-platform.yaml is unaffected by the changed paths ``` or, when it does run: ```text theme={null} Should run: true Reason: vercel-production-platform.yaml is affected by 3 changed path(s) Matched paths: - apps/web/src/app/page.tsx - packages/ui/src/button.tsx ``` ### Reproduce a decision locally You can run the decision script directly to debug why a workflow did or did not run: ```bash theme={null} CHANGED_FILES="apps/web/src/app/page.tsx" \ WORKFLOW_NAME="vercel-production-platform.yaml" \ node --experimental-strip-types scripts/ci/check-workflow-config.ts ``` There are also unit tests covering the gating logic: ```bash theme={null} node --test scripts/ci/check-workflow-config.test.js node --test scripts/ci/resolve-changed-files.test.js node --test scripts/ci/resolve-production-vercel-targets.test.js ``` ### Common Issues #### Issue: A workflow never runs for legitimate changes **Likely causes:** * The app is missing (or misconfigured) in `vercelWorkflowTargets`. * The changed package is not in the target app's workspace dependency closure (the app does not actually `workspace:*`-depend on it). * The workflow is toggled off in the `ci` map in `tuturuuu.ts`. **Fix:** Verify the target entry and the dependency edges, then re-run the local reproduction above to confirm the decision. #### Issue: A workflow always runs **Likely causes:** * A global affecting path (`bun.lock`, `package.json`, `turbo.json`, `tuturuuu.ts`) changed — these intentionally trigger every Vercel target. * The change-data resolver failed, so the gate fell open by design. This is usually correct behavior; only investigate if it persists for changes that clearly do not touch shared roots. #### Issue: Need to force a run Use the app workflow's manual trigger. A direct `workflow_dispatch` starts that app without re-running affected-path gating: ```yaml theme={null} on: workflow_dispatch: ``` ## Performance Impact The exact savings depend on the size of the app set and how localized each commit is, so treat the following as **illustrative** rather than a fixed figure. The monorepo currently registers **24 active Vercel app targets**, each with a preview and a production workflow (**48 Vercel deploy workflows**), out of 91 workflow files. Production planning prevents a single-app change from creating 23 unrelated production runners. Preview workflows create a runner only for the inline platform signal or an explicitly requested satellite preview. For a change that touches only one app: ```text theme={null} 1 production planner workflow + only the affected app deployment workflows + one inline platform preview gate on protected main + direct satellite preview jobs only when explicitly dispatched ``` Each skipped production app now consumes no runner at all; only the shared planner and selected reusable deployment jobs run under the production push. Changes to shared roots (`package.json`, `bun.lock`, `turbo.json`, `tuturuuu.ts`) intentionally fan out to all targets, so headline skip rates will vary with how often those roots change. > [!NOTE] > > To measure real-world impact, inspect the production planner's job summary and > compare its selected workflow count with the 24 registered targets. Platform > preview reports its inline decision in the deploy job; manual satellite > previews have no separate gate job. ## Best Practices ### 1. Keep `tuturuuu.ts` as the source of truth When adding an app or workflow, register Vercel app metadata in `vercelWorkflowTargets` and register automatic switchboard-controlled workflows in the `ci` map. Trusted manual satellite previews and intentionally path-triggered compatibility smokes do not keep unused `ci` toggles. The gate and the tests key off these boundaries. ### 2. Trust the fail-safe When change data is missing, the gate opens and the workflow runs. This is intentional — it is safer to build unnecessarily than to skip a real change. Don't add ad-hoc skip logic that defeats it. ### 3. Model dependencies with `workspace:*` The dependency-closure matching only works because shared packages are wired as `workspace:*` dependencies. If an app consumes shared code without declaring the dependency, the gate will not know to run it when that package changes. ### 4. Use `workflow_dispatch` to debug To bypass gating for a one-off investigation, trigger the workflow manually from the Actions UI rather than touching the gate logic. ### 5. Update tests when changing the gate `getWorkflowDecision()` and the resolver are covered by unit tests under `scripts/ci/`. Update them alongside any change to the matching rules so the behavior stays pinned. ## Further Reading * [CI/CD Pipelines](/build/development-tools/ci-cd-pipelines) * [TanStack + Rust migration](/platform/architecture/tanstack-rust-migration) * [Turborepo: Remote Caching](https://turborepo.com/docs/core-concepts/remote-caching) * [GitHub Actions: Reusing workflows](https://docs.github.com/en/actions/using-workflows/reusing-workflows) ## Summary Our CI/CD optimization centralizes change detection so each workflow stays thin: * Reusable `ci-check.yml` calls remain available where a separate gate is necessary; production planning and platform preview avoid per-app preflight fan-out. * `tuturuuu.ts` decides, per workflow, whether the change is relevant — including workspace dependency-closure awareness for shared packages. * The remote-cache wrapper keeps Turbo credentials step-scoped and gives secretless jobs a GitHub-backed local fallback. * Turborepo caches full app builds, type checks, tests, and release preparation once a run is warranted. * The gate fails safe (builds on uncertainty), and trusted manual satellite previews start their guarded deploy job directly. The result: a one-app change selects only the necessary production deploys in the commit-associated push run, platform preview retains its required staging signal without a second runner, manual previews never queue a redundant preflight, and superseded previews are canceled before they occupy the queue indefinitely. ## Runner Queue Efficiency Translation validation runs on every pull request (including forks and stacked PRs), plus direct pushes to `main` and `production`. Feature-branch pushes no longer duplicate the same four translation checks alongside the pull-request run. Manual dispatch remains available for work without a PR. Superseded feature-branch translation and E2E runs cancel; protected branch runs retain their exact-commit verification evidence. E2E keeps all four general shards and all six isolated invitation cohorts. Only the first general shard writes the shared Supabase image cache on a cold main run. Invitation cohorts also have `shard: 1`, so checking only that field caused seven jobs to serialize and compete to upload the same multi-gigabyte archive. Failure artifacts use the unique matrix ID so separate invitation failures do not collide on the same artifact name. Run `node --test scripts/ci/runner-efficiency.test.js` to verify trigger coverage, the single cache writer, cohort preservation, artifact uniqueness, and protected branch cancellation policy. More matrix jobs are not automatically faster: runner queue time and repeated database/container startup must be considered alongside test duration. # CI/CD Pipelines Source: https://docs.tuturuuu.com/build/development-tools/ci-cd-pipelines High-level CI/CD overview, with current operational runbooks linked from one place. > [!NOTE] > > Current runtime policy: Docker setup and Docker-backed E2E/image CI are paused; > the Docker cron runner is retired. Rust and TanStack Start are not in use and > remain paused for runtime and deployment. The branch-scoped hosted Rust source > qualification below is the authorized exception for this release integration; > it does not activate either migration runtime. Retained Docker and > migration sections below are historical procedures. Current hosted Next.js, > package, Worker, database, and mobile workflows retain their own gates. See the > [active runtime map](/build/devops/active-runtime). This page is the entry point for CI/CD. The day-to-day operational detail now lives in the dedicated DevOps pages below so the docs stay current as workflows change. ## Read Next * [DevOps & Deployment Overview](/build/devops/overview) * [Environments & Release Flow](/build/devops/environments-release-flow) * [Web Docker Deployment](/build/devops/web-docker-deployment) * [GitHub Actions Runbook](/build/devops/github-actions-runbook) * [Secrets & Configuration](/build/devops/secrets-and-configuration) ## Current CI/CD Shape Tuturuuu currently automates: * Vercel preview deployments for non-`production` branches * Vercel production deployments from the `production` branch * Independent Vercel preview and production deployments for satellite apps such as `apps/cms` * Affected-app gating for Vercel deployments before dependency install, build, or deploy work * Supabase staging migrations driven from `main` and production migrations driven from `production` * Docker parity and image validation for the self-hosted web runtime * Modal deployment for `apps/discord` * Mobile build artifact generation * Package publishing to npm * Quality, lint, type, test, and security checks ## Important Repo Conventions * For most authored changes, run the applicable check, lint, test, and build workflows in CI and verify their results on the exact commit. Focused local checks help find problems quickly but do not replace that CI evidence. If a workflow queue is demonstrably stuck, record the pending jobs and local results; resume the exact-commit CI checks before claiming release readiness. * `ci-check.yml` reads `tuturuuu.ts` and can disable an individual workflow centrally. * Vercel workflows use `tuturuuu.ts` app metadata and workspace `workspace:*` dependency closures to run only affected app deploys. * `bun.lock` changes run all Vercel app deploys because lockfile ownership is ambiguous without a source or manifest path. * Manual `workflow_dispatch` runs enabled workflows even when changed paths would otherwise be unaffected. * Production Vercel workflows cancel superseded per-app runs only for newer `production` pushes. A `main` commit or manual recovery run never cancels an active production deployment. * The external-app internal-package build uses the same cancel-superseded policy, so rapid pushes cannot finish as green no-op builds. * Database migrations are not treated the same as app deploys; staging and production each have explicit prerequisite logic, with staging now sourced from `main`. * Docker setup and image-build CI (`docker-setup-check.yaml`) are disabled, including manual dispatch, until explicitly resumed. ## Inline Type Check preflight regressions `type-check.yaml` runs `node --test --test-reporter=tap scripts/ci/type-check-inline-workflow.test.js` on its exact checkout with Node 24, after the existing Setup Bun step and before dependency installation. Bun is required by the workflow YAML test helper; these 21 regression cases need no workspace dependency installation. The step uses the existing configuration and release-relevance gates. A skipped step is not test evidence. Before merging a preflight fixture change, verify the exact-head hosted step succeeded and its TAP summary reports 21 tests, 21 passes, and zero failures, cancellations, skips, or todos. ## Worker validation caching Cron Control and Devbox Control run package build, test and type-check tasks through the existing Turbo remote-cache action with explicit workspace filters. Meet Realtime uses the same action for build and type-check. Worker builds restore the declared `dist/**` outputs before emitted-artifact verification. Cron Control and Devbox Control generate binding types before cached type checks because the generated `.d.ts` file is not a declared type-check output. Lint, source and emitted runtime checks, SQLite and Durable Object verification, and deploy dry-runs still run directly. Remote credentials remain step-scoped and are supplied only to trusted main or production runs, never untrusted PRs. ## Hosted root script qualification The `Test` workflow runs the complete selection exported by `scripts/run-script-tests.js` in a separate `Root script tests (24)` job on the exact checkout. Node 24 runs it with two test-file workers and the TAP reporter; the existing paused-runtime discovery policy remains unchanged. No shell glob or subset replaces the canonical selection. The job installs the frozen dependencies with the existing Bun setup, verifies Ruby and `@playwright/test` resolution, and exposes the owning Web dependency location through `NODE_PATH`. It needs no application secrets, browser install, workspace build, deployment, or production action. The unit-test aggregation requires successful package shards or verified duplicate package evidence, and always requires the fresh root script job to succeed when checks are relevant. Configuration and release-relevance gates still apply. The root script job does not reuse the package-shard duplicate-validation result. A skipped job or green package shards do not prove root script qualification: require the exact-head job, prerequisite results, complete discovery count, and full TAP summary. Historical failures remain unresolved until their actual cases pass on the qualified source; do not infer a fix from a different branch's test bytes. ## Hosted Rust source qualification On `fix/release-5795-reconciliation` and `fix/main-production-reconciliation-20261012`, the existing `Test` workflow runs `Rust source qualification (1.95)` after the shared configuration and release relevance gates. The read-only job checks out the exact workflow commit, records its SHA and Cargo manifest/lock hashes, and uses pinned Rust 1.95.0 with two Cargo build workers. It runs formatting, warning-denying native Clippy, all native library tests with two test threads, and the wasm Worker target check against the locked dependency graph. It needs no application secrets or deployment action. The stable `Unit Tests (24)` aggregate requires this fresh job to succeed on those branches, including when verified package duplicate evidence skips shards. A skipped Rust job or successful root script tests do not qualify Rust: require the exact-head job and its complete Cargo results. Other branches retain their existing package/root gates. The standalone Rust workflows, TanStack migration, and backend deployment remain paused; the backend is a future migration target, and Web continues to serve the live API. ## Cross-app users-core regression tests The canonical approval regressions live outside the package source tree: * `packages/users-core/tests/approvals/get-tenant-boundary.test.ts` * `packages/users-core/tests/approvals/review-reads-tenant-boundary.test.ts` The source type-check project keeps `rootDir: "src"`. Vitest discovers the cross-app tests separately through `packages/users-core/vitest.config.ts`; relocating these tests does not expand the package's source compiler boundary. That config maps the Web-specific imports `@/lib/post-email-queue` and `@/features/reports/report-limits` to their exact `apps/web/src` files before its default `@` alias, which still points to `packages/users-core/src`. The exact `@tuturuuu/satellite/auth` mapping points to `packages/satellite/src/auth.ts`; it keeps the satellite namespace separate from the package-local `@` imports. Run selected tests from `packages/users-core` with that canonical config and the receiver's qualified dependencies. For a source `TS6059` error, check that the cross-app test is outside `src`. For a Vitest module-resolution failure, check the owning cwd, specific alias destination, and installed dependency closure before changing compiler roots or alias scope. A config/source handoff or historical donor test and type results do not prove the current receiver's test, type, CI, or runtime outcome. ## When To Use This Page Use this page for orientation. Use the DevOps pages for the runbook you actually need while operating the system. ### Learn preview build validation The trusted manual `vercel-preview-learn.yaml` workflow builds the checked-out `preview_ref` with a 35-minute timeout and the existing two-task Turbo limit. It records a `build` marker using `git rev-parse HEAD`, rather than the main workflow dispatch SHA. It does not deploy the artifacts. Learn build markers cannot advance the successful deployment baseline; existing Platform marker compatibility remains unchanged. A successful build proves compilation, while actual-device validation and deployment are separate gates. Focused checks: `node --test scripts/ci/learn-preview-build.test.js scripts/ci/github-deployment-markers.test.js scripts/ci/release-workflows.test.js`. ### Generated release inventory Release Please owns platform and independent package version changes. Release normalization derives only `apps/docs/reference/repository/versions.mdx` using the complete documentation inventory generator. Other stale inventory pages fail the job; this phase does not repair unrelated source or navigation changes. Before updating the generated PR, the workflow captures its previous immutable head. A retained versions page must match either the production page, the new complete projection, or the independently verified projection at that captured head. Changed generator/configuration sources and unproven docs bytes fail closed. The amended generated commit keeps its release annotations and is pushed using its exact head lease. Concurrent updates reject that push. Auto-approval verifies the final immutable page before rechecking the PR head and base. `bun git-release-please` also regenerates this page after resolving the platform version during integration and before validation or commit. Run `node scripts/ci/release-docs-inventory.js --verify BASE HEAD` to verify immutable release output. This is derived source evidence, not a publication or deployment receipt; failed release PR checks continue to block auto-integration. # Cleaning Your Tuturuuu Clone Source: https://docs.tuturuuu.com/build/development-tools/cleaning-clone How to reset your local Tuturuuu repository to a clean state. > [!NOTE] > > This guide helps you reset your Tuturuuu repository to a clean state, which is > useful if you encounter dependency issues or want to ensure a fresh > development environment. The current stable pin is Bun `1.4.4` ([official release](https://github.com/oven-sh/bun/releases/tag/bun-v1.4.4)). Keep active workspace metadata and CI pins aligned with the root `packageManager`. Change manifest metadata with `bun pm pkg set packageManager=bun@1.4.4` in each owning workspace. An updated pin does not upgrade an installed host or runner; verify `bun --version` separately. Retained paused runtime files and historical reproduction notes keep their original versions. ## Prerequisites * Ensure you have [bun](https://bun.sh/) installed. The repository pins a specific Bun version via the `packageManager` field in the root `package.json` (currently `bun@1.4.4`) — match that version so your local toolchain stays in sync. * Use the Node.js version pinned in the root `.node-version` (currently `26.12.1`). The root `engines` field keeps a `>=22.13.0` support floor; see [Node.js runtime pin](/build/development-tools/node-runtime). ## Quick Cleanup (Recommended) The repository ships a one-shot root script that performs the full cleanup for you: ```bash theme={null} bun clean ``` This removes `bun.lock`, deletes every `node_modules` directory, and clears all `.next` and `.turbo` build caches across the monorepo (it uses `npkill --delete-all` under the hood). After it finishes, reinstall everything: ```bash theme={null} bun i ``` > [!TIP] > > `bun clean` is the supported shortcut for the manual steps below. Reach for the > manual steps when you want finer control over which folders are removed. ## Manual Cleanup (Granular Alternative) ### 1. Delete the lockfile Remove the `bun.lock` file at the root of your repository (if it exists): ```bash theme={null} rm bun.lock ``` ### 2. Remove all `node_modules` folders Use [npkill](https://github.com/voidcosmos/npkill) to find and remove all `node_modules` directories: ```bash theme={null} bunx npkill ``` * In the `npkill` interface, select and delete all `node_modules` folders found in your repo. > [!TIP] > > If you don't have `npkill` installed, `bunx` will download and run it > automatically. ### 3. Clear build caches (optional) Remove stale `.next` and `.turbo` build caches if you suspect they are causing issues (the `bun clean` shortcut does this automatically): ```bash theme={null} find . -name node_modules -prune -o \( -name .next -o -name .turbo \) -type d -exec rm -rf {} + ``` ### 4. Reinstall dependencies Install all required dependencies using bun: ```bash theme={null} bun i ``` This will install fresh dependencies for all apps and packages in the monorepo. > [!NOTE] > > After completing these steps, your repository should be in a clean state. You > can now continue with the usual development workflow as described in > [Development](/build/development-tools/development). ## Troubleshooting * If you still encounter issues, try restarting your IDE or terminal. * Make sure your bun version is up to date: `bun upgrade`. * If problems persist, check the [Development](/build/development-tools/development) guide or ask for help in the project discussions. # Codex Plugin Source: https://docs.tuturuuu.com/build/development-tools/codex-plugin Use and maintain the local Tuturuuu Codex plugin. The local Tuturuuu Codex plugin lives at `plugins/tuturuuu`. It packages repo-specific operating knowledge as Codex skills so future agent sessions can load focused guidance without copying long instructions into every prompt. ## Maintaining focused agent guidance Tuturuuu skills support a specific operation. Discover the capability from its short description, read its entrypoint when needed, then open only the relevant reference section. CLI task capture, finance reads, CLI implementation, and PR integration have different procedures; using one does not require all the others. This organization follows OpenAI's [Rethinking skills and prompts for GPT-6 Astra](https://developers.openai.com/blog/rethinking-skills-and-prompts-for-gpt-6-astra). The guidance remains model-neutral: preserve repository invariants while letting the agent choose routine implementation details. The maintained source is `plugins/tuturuuu/skills`. Audit all skill descriptions, entrypoints, and `agents/openai.yaml` prompts there. Keep existing short skills when their boundaries are already clear. Update source packages rather than installed plugin caches or third-party skill copies. App `CLAUDE.md` aliases and generated Next.js instruction blocks retain their canonical owners. | Work | Guidance to load | | - | - | | CLI install/auth/implementation | `tuturuuu-cli`, then relevant CLI procedure | | Task capture or templates | `tuturuuu-cli-tasks`, then task or template procedure | | Finance read or mutation | `tuturuuu-cli-finance`, then finance diagnostics/examples | | Rollback or shared API rollout recovery | `tuturuuu-rollout-recovery`, then live graph and recovery evidence | | Declarative database adoption readiness | `tuturuuu-database/references/declarative-schema-adoption.md`, read-only checker and baseline evidence gates | | PR integration | `tuturuuu-pr-merge-sync`, then merge gates and watcher commands | | Skill or prompt audit | `tuturuuu-development-tooling/references/agent-guidance-review.md` | Task prompts should name the desired outcome, relevant constraints, and completion boundary. For example: "Fix the invoice loading failure, verify recovery, open a PR, and merge only after 15 minutes without new review activity, zero unresolved review threads, and green checks on the current head." The duration belongs to that request; it does not change the default for every future PR. Required checks, ownership, production data protections, and migration parity still apply. The default quiet window for PR merge sync and automated Release Please merges is five minutes when a request does not specify a duration. For dependent PR work, the merge skill uses native GitHub stacks as the daily default. Follow [stack operations](/build/development-tools/stacked-pull-requests) for `gh stack` adoption, worktree ownership, cascading fixes, and verified prefix merges. The official upstream skill is installed at `.agents/skills/gh-stack` from `github/gh-stack@v0.2.0`, with installer source metadata. Preserve its upstream content and review updates; repository rules override generic upstream examples. Apply review, quiet-window, and exact-head checks to every included layer; register each PR with the T3 thread when linking tools are available. Manual chains are a documented fallback for concrete limitations. A main-only merge request ends after main verification; production sync still needs production authorization. For authorized parallel work, follow the [autonomy contract](https://github.com/tutur3u/platform/blob/main/plugins/tuturuuu/skills/tuturuuu-agent-coordination/references/program-orchestration.md#autonomy-and-asynchronous-waits). Use useful subagents for independent ready backlog while PR/CI or resource gates run. Arrange child work before arming a native PR watch, then end the turn as the tool requires; native result/watch events resume delivery without a user reminder. Relay the policy, project instructions, current custody overrides and remaining child budget in each brief. The [agent operating model](/build/development-tools/agent-operating-model) allows up to 10 subagents per thread tree, including descendants. That number is a contract plus checker, not a runtime-enforced or observed live-session limit. Leads receive explicit remaining budgets, and lower observed runtime, provider and resource limits still apply. After observed delays or unnecessary handovers, use a bounded child workflow audit at a safe checkpoint. Apply accepted improvements to maintained source, then refresh the plugin through its installer and compare the resolved cache content. Already-running agents need the update in their next substantive brief; new files do not retroactively change a live session. Preserve permissions, exact-head verification, paused predecessors and urgent release priority. Finished repository work includes scoped commits and a normal push without a separate user reminder. Follow the [completion contract](https://github.com/tutur3u/platform/blob/main/plugins/tuturuuu/skills/tuturuuu-agent-coordination/references/program-orchestration.md#completion-and-publication) for branch/review gates, shared-index ownership and blocked-publication receipts. A push does not certify CI, merge or production delivery. A plugin validator proves structural validity, not better model behavior. Review representative read, mutation, implementation, and delivery requests for correct routing. Record any isolated behavioral evaluation separately. Live product AI prompts need their own behavior evaluation before changing their contracts. ## Discovery Codex discovers repo-local plugins through `.agents/plugins/marketplace.json`. The Tuturuuu marketplace is named `tuturuuu`, displays as `Tuturuuu`, and points its local plugin entry at `./plugins/tuturuuu`. After changing marketplace metadata or plugin files, validate the plugin and refresh the repo-local marketplace from the repo root. Do not bump `plugins/tuturuuu/.codex-plugin/plugin.json` `version` for ordinary authored workflow updates unless a release workflow or user request requires it. The repo-local plugin manifest is currently refreshed to `0.2.23`. ```bash theme={null} codex plugin marketplace add ./ ``` Restart Codex after refreshing the marketplace. Then open the plugin directory, choose the `Tuturuuu` marketplace, and install or update the `Tuturuuu` plugin. If Codex does not show the repo marketplace after restart, add the repo root as a local marketplace source from the CLI: ```bash theme={null} codex plugin marketplace add ./ ``` ## skills.sh Discovery The public skills.sh registry discovers skills from GitHub repositories after someone installs the repo with the `skills` CLI. The platform repo exposes the Tuturuuu plugin skills for that flow through two root metadata files: * `.claude-plugin/marketplace.json` points compatible skill installers at `plugins/tuturuuu/skills`. * `skills.sh.json` groups the public repo page so Tuturuuu skills appear ahead of unrelated installed project skills. Do not run the public install trigger before the metadata is pushed to GitHub. After pushing the metadata commit, run the trigger from a disposable directory, not from this checkout: ```bash theme={null} npx skills add tutur3u/platform --agent codex --copy -y \ --skill tuturuuu-agent-coordination \ --skill tuturuuu-browser-vercel-debugging \ --skill tuturuuu-ci-docs \ --skill tuturuuu-cli \ --skill tuturuuu-cli-resources \ --skill tuturuuu-cli-finance \ --skill tuturuuu-cli-tasks \ --skill tuturuuu-cms-studio \ --skill tuturuuu-commit \ --skill tuturuuu-database \ --skill tuturuuu-devbox-ops \ --skill tuturuuu-development-tooling \ --skill tuturuuu-e2e-auth-debugging \ --skill tuturuuu-external-apps \ --skill tuturuuu-mobile-task-board \ --skill tuturuuu-platform \ --skill tuturuuu-pr-merge-sync \ --skill tuturuuu-review-comments \ --skill tuturuuu-rollout-recovery \ --skill tuturuuu-satellite-app-ux \ --skill tuturuuu-validation-offload \ --skill tuturuuu-web-release ``` Then verify the cached page after refresh at `https://skills.sh/tutur3u/platform`. ## Included Skills * `$tuturuuu-rollout-recovery`: mixed-version and Instant Rollback diagnosis, canonical alias/source audits, safe recovery evidence, and bounded CI retries. Read the [critical rollout runbook](../devops/critical-app-rollout) for the maintained staging/probe/promotion contract. Ingress 401 checks do not prove signed-in business recovery; preserve per-app authentication and QA privacy. * `$tuturuuu-platform`: repo-wide workflow guidance for `apps/web`, `packages/*`, `apps/database`, `apps/docs`, translations, navigation, and verification follow-through, including shared-worktree preflight, scoped staging expectations, and Effect adoption guidance for TypeScript server/service orchestration. * `$tuturuuu-browser-vercel-debugging`: authenticated Browser/Chrome and Vercel CLI guidance for reproducing deployed UI/API bugs, correlating runtime and build logs, checking satellite routing/auth, auditing responsive and i18n behavior, investigating performance/cost, and verifying improvements without mutating customer data. * `$tuturuuu-cms-studio`: CMS and external-project content studio guidance for `apps/cms`, branded project adapters, landing-page content editing, content collections, media workflows, preview delivery, and non-technical editor UX. * `$tuturuuu-satellite-app-ux`: standalone app shell guidance for satellite apps such as Mail and CMS, including `@tuturuuu/satellite` auth, app-session routes, i18n, workspace navigation, permissions, and focused operational UX. * `$tuturuuu-external-apps`: external and branded sibling app integration guidance for app-token exchange, first-class workspace invitation decisions, member and role administration, Drive management, refreshable admin sessions, direct signed storage uploads, external-project mutations, delivery fallbacks, and sanitized diagnostics. * `$tuturuuu-e2e-auth-debugging`: local E2E authentication guidance for native Playwright patch iteration, `dev-session`, guest access, onboarding redirects, app-session verification, rate-limit tests, and explicit Docker-parity auth checks. * `$tuturuuu-commit`: scoped commit workflow guidance for explicit commit, commit-and-push, atomic staging, Conventional Commit subjects, shared-worktree path isolation, commit-window claim/wait/release, proof-gated no-verify commit evidence, hook failure handling, and final commit reporting. * `$tuturuuu-agent-coordination`: shared-worktree coordination guidance for dirty checkouts, active ownership notes, archived context, overlapping edits, stale handoffs, explicit write sets, commit-window coordination, and path-scoped staging safety. * `$tuturuuu-cli`: core native `ttr` CLI install, browser login, copy-token login, workspace discovery, scoped help, SDK-backed command surfaces, task template command boundaries, version checks, and release verification guidance. It documents autonomous Bun and CLI installation with `curl -fsSL https://bun.sh/install | bash` on macOS/Linux, `powershell -c "irm bun.sh/install.ps1 | iex"` on Windows, then `bun i -g tuturuuu`. * `$tuturuuu-cli-resources`: opt-in machine resource control through `ttr resources`, shared validation admission across projects/worktrees, independent Turbo and Vitest limits, memory-pressure diagnostics, remote-runner checks, and rollback. * `$tuturuuu-cli-tasks`: focused `ttr tasks` and `ttr task-templates` guidance for task capture, task board/list/label discovery, open-task defaults, compact task tables, task creation, reusable task-template import/export, template instantiation, splitting, movement, completion, closure, and task verification. It documents ttr-first task capture for requests to add, create, template, track, or split Tuturuuu tasks, including label discovery and closing superseded combined tasks after replacements are created. * `$tuturuuu-cli-finance`: focused `ttr finance` guidance for wallet, transaction, category, budget, and recurring CRUD; analytics reads; finance pagination footers; explicit-workspace finance read diagnostics; and wrapped finance response normalization in the SDK client. * `$tuturuuu-development-tooling`: shared guidance for improving Codex skills, plugin behavior, validation scripts, docs runbooks, helper scripts, and durable workflow learnings that should benefit future Tuturuuu contributors and assistants, including Turborepo/Next cache boundaries, local dev-speed diagnostics, release-please merge automation, and package release automation rules. * `$tuturuuu-pr-merge-sync`: PR closeout guidance for quiet-window review watching, efficient GitHub polling, merge/admin-merge follow-through, the hard main-green gate before `bun git-sync`, production sync, and production workflow verification. * `$tuturuuu-devbox-ops`: operational guidance for `ttr box` setup, runner registration, one-shot smoke tests, 24/7 system services, runner-token cleanup, CLI upgrades, and infrastructure devbox observability. * `$tuturuuu-validation-offload`: validation strategy guidance for deciding when to run focused tests, `bun validate`, Supabase, Docker, or browser-heavy workflows through internal devboxes instead of the local agent session. * `$tuturuuu-web-release`: `apps/web` release badge metadata, blue/green runtime snapshot fallback, `PLATFORM_BUILD_*` precedence, and release-please-managed `TUTURUUU_PLATFORM_VERSION` guidance. * `$tuturuuu-mobile-task-board`: Flutter mobile task-board guidance for task dates, overdue behavior, task detail routing, description mode, assignee flows, and release-please-managed mobile version metadata. * `$tuturuuu-database`: Supabase migration, RLS, workspace-scoped API write, generated type, storage, and database verification guidance. Agents never push production migrations manually; the gated production workflow applies them after authorized sync and requires separate exact-SHA verification. * `$tuturuuu-ci-docs`: GitHub Actions, `tuturuuu.ts`, validation script, docs-page, and docs navigation guidance. * `$tuturuuu-review-comments`: GitHub PR review-thread guidance for checking, re-checking, validating, fixing, resolving, and committing unresolved review comments. Addressed review threads are resolved by default after validation; commits and pushes still require an explicit user request. Its bundled `fetch_review_threads.py` helper supports `--active-only --summary` and emits review-thread counts for final re-check reports. ## Translation Key Workflow When a platform change introduces a new translation key, prefer the root helper instead of editing message JSON by hand: ```bash theme={null} bun i18n:add --app web --key common.save --value en=Save --value vi=Lưu ``` Use `--dir apps//messages` for non-standard message directories and `--all` only for shared UI keys that must exist in every detected app message setup. The helper detects locale JSON files in the target setup, requires a value for each locale, blocks unsafe key paths, prevents accidental overwrites unless `--overwrite` is passed, and writes sorted JSON. Manual message edits are reserved for broad copy rewrites or value-only updates; run `bun i18n:sort` after those edits. For larger translation updates, use bulk modes instead of repeatedly editing JSON: ```bash theme={null} bun i18n:add --app web --mode add --entries '{"common.save":{"en":"Save","vi":"Lưu"}}' bun i18n:add --app web --mode remove --entries '["common.old"]' bun i18n:add --app web --mode replace --entries-file ./translations.json ``` Bulk add and replace entries use a JSON object keyed by translation key, with locale values underneath each key. Bulk remove accepts an array of translation keys. Replace and remove fail when a key is missing unless `--ignore-missing` is passed. ## CI Coverage `.github/workflows/codex-plugin.yaml` runs the plugin-local validator and parses the docs navigation JSON. The workflow is enabled through `tuturuuu.ts`, matching the repo's other CI switchboard-controlled checks. ### Native parallel validation pilot After checkout, Python 3.13 setup, plugin validation and the hash-locked MCP dependency install, one native `parallel` group runs the MCP boundary/stdio suite and orchestration-evidence suite. Both read repository source and use separate temporary fixtures; neither consumes the other's output. Each suite retains its original discovery command and applies a five-minute timeout and a 1 GiB per-process virtual memory ceiling. The job has a fifteen-minute timeout and read-only permissions. GitHub's [June 25, 2026 announcement](https://github.blog/changelog/2026-06-25-actions-steps-can-now-be-run-in-parallel/) and [workflow syntax](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idstepsparallel) document `parallel` as background steps followed by an implicit wait. Any suite failure fails that barrier and the job; no step uses `continue-on-error`. Docs navigation validation stays after the barrier. Triggers, switchboard gating and the required check name remain unchanged. Run `node --test scripts/ci/codex-plugin-parallel-workflow.test.js` to check the parsed workflow's prerequisite order, bounded two-suite group and failure gates. This structural check does not execute GitHub's scheduler. Confirm overlapping suite intervals and successful completion in the automatic exact-head hosted run before treating the pilot as qualified. No speed improvement is established; compare equivalent runner, source, dependency/cache and suite inputs before claiming one. Roll back by expanding the two children into sequential steps. The CI check currently verifies: * plugin manifest JSON and interface metadata * natural default prompt examples for the invokable skills, written as short user requests instead of visible `$skill` commands * dedicated prompt coverage for CMS studio and landing-page content management * dedicated prompt coverage for external app integrations, in-app workspace invitations and membership, Drive management, and direct uploads * dedicated prompt coverage for satellite app UX and app-session workflows * dedicated prompt coverage for native local E2E auth and dev-session debugging * dedicated prompt coverage for browser and Vercel production troubleshooting * dedicated prompt coverage for shared-worktree coordination and agent handoff * dedicated prompt coverage for PR quiet-window merge sync with main-green gating before production sync * dedicated prompt coverage for devbox runner operations and validation offload * dedicated prompt coverage for web release badge metadata * durable CI tooling guidance for npm package release auto-recovery * durable CI tooling guidance for Supabase CLI setup retry and GitHub-tokened release lookup * durable CI tooling guidance for bounded Codecov coverage test retries * scoped commit workflow coverage for explicit commit requests * native `ttr` task-template workflow coverage in the CLI task skill * proof-gated no-verify commit evidence coverage * commit-window claim, wait, release, and stale-lock guidance * repo marketplace metadata at `.agents/plugins/marketplace.json` * public skills.sh discovery metadata at `.claude-plugin/marketplace.json` * public skills.sh grouping metadata at `skills.sh.json` * skill folder and frontmatter naming parity * `agents/openai.yaml` defaults for every skill * skill `references/` links * plugin docs page registration in `apps/docs/docs.json` * Codex plugin workflow registration in `tuturuuu.ts` * absence of scaffold `[TODO: ...]` placeholders * absence of machine-specific local paths in plugin text ## Program Ownership And Verification For authorized work spanning multiple owners and delivery wakes, the coordinator owns scope, acceptance, dependency order, source review and delivery evidence. Workers own disjoint implementation lanes. Use the plugin coordination skill's `references/program-orchestration.md` for the brief and current-board format. Completions are reconciled after critical sections and before status reports; an idle worker is resumed only with concrete work, never merely for status. Keep the coordinator-owned current board in ignored `tmp/agent-coordination/` state rather than reconstructing current progress from a growing event history. The read-only checker rejects stale-head delivery claims, conflicting gates and multiple active writers in the same worktree: ```bash theme={null} python3 plugins/tuturuuu/scripts/check_program_state.py tmp/agent-coordination//state.json python3 -m unittest discover -s plugins/tuturuuu/scripts -p 'test_program_state.py' ``` Recorded PASS requires a real receipt. This checker does not query remote systems or validate receipt contents. A merge, green summary, skipped deployment or old store upload cannot establish new-source availability. Each requested delivery stage needs current-source evidence; new heads invalidate prior head receipts. Use native PR wakes and an explicitly authorized cadence instead of polling. For a task-specific harness change, the board may also record bounded baseline and candidate experiments. The checker requires fixed input/context/control pins, a closed paired case census, explicit probe usage, and separate failure/unknown/error outcomes. Adoption requires observed improvement without regressions, a comparison, a scoped lesson and a current candidate regression receipt. See the coordination skill's `references/program-orchestration.md` for the contract and offline example. This adapts [Harness²'s contrastive method](https://github.com/google-research/harness2) without its executor or model calls. Matching pins and unit tests establish record consistency, not semantic guardrail preservation or improved agent quality. Experiment acceptance never replaces a unit's delivery evidence or changes authority. This workflow adapts [pstack's upstream orchestration playbook](https://github.com/cursor/plugins/blob/df581122cde17e6e27686b5a448bde23e4ad4318/pstack/skills/poteto-mode/playbooks/orchestrate.md) to T3's actual child-task tools and Tuturuuu's ownership, resource and production boundaries. It does not require Cursor, Graphite, cloud agents or a new plugin installation. Structural/routing checks are bounded evidence, not a behavioral agent evaluation. Managed plugin caches are updated by their installer. ## Long-Term Request Intake When a user assigns ongoing program ownership, capture each idea, bug, feature, improvement and amendment before dispatch. Use stable IDs, source pointers, checkable acceptance, dependencies and current owners. The coordination skill's `references/request-backlog.md` defines the private SQLite ledger and commands. Intake survives conversation compaction and task-worktree retirement; it is local persistence, not cross-machine synchronization or a guarantee of unlimited recall. The standard store is under `~/.local/share/tuturuuu/orchestration/programs/`. Never commit user dumps, transcripts, credentials or the database. Repeated imports preserve progress, conflicting scopes fail rather than overwrite, and updates retain event history. Read backlog and current-unit evidence at every wake. Keep captured, active, implemented and verified distinct. Native Tuturuuu task creation still uses `ttr` after destination discovery. The requested pstack upstream package can be used alongside this plugin. Its Cursor marketplace does not expose pstack through the Claude marketplace Codex reads, so the local compatibility installation adds a Codex manifest/marketplace without modifying upstream skills. Pin the reviewed upstream commit, retain the license and verify installed/enabled status with the official installer. This adapter does not make Cursor-only tools available or override Tuturuuu policy. Managed caches stay installer-owned. Start a subsequent turn to discover newly installed skills. ## Shared Worktree Coordination Agents should assume that humans or other agents may have uncommitted work in the same checkout. The docs-facing protocol lives in the [Agent Operating Manual](/overview/agent-operating-manual#shared-worktree-coordination), and the agent-facing workflow lives in `$tuturuuu-agent-coordination`. Open Tuturuuu pull requests are always handled in isolated worktrees under `.worktrees/`. Run `bun install` immediately after creating the PR worktree; builds run in CI. Keep the worktree and local task branch while the PR is open, and remove both only after the merge is confirmed on `main` and post-merge verification is complete. This lifecycle is documented in the Agent Operating Manual and enforced as a cross-cutting rule in `AGENTS.md`. For user-authorized continuous integration, the same lifecycle applies to each verified checkpoint: integrate it into current `main`, require the exact main SHA to be fully green, run `bun git-sync`, verify production, then remove only the completed worktree and local task branch. Rust-heavy retained worktrees use `bun rust-cache report` plus bounded `prune`/`auto` cleanup rather than broad filesystem deletion. Use `tmp/agent-coordination/` for lightweight agent-to-agent notes when work may overlap, the worktree is dirty, the task is long-running, or the task changes agent workflow rules. The directory is ignored by git, so it can hold live intent, ownership, status, verification, risk, and handoff messages without polluting commits. Active notes are notes marked `working`, `blocked`, or `handoff`. Treat stale active notes as ownership signals until you have read them and checked the current worktree. Keep active notes as direct files under `tmp/agent-coordination/`; archive completed notes under `tmp/agent-coordination/archive//.md` after they are marked `done` and no longer need top-level visibility. Use `bun git-commit-window` before changing the staged set or creating commits in a shared checkout. `claim` creates an advisory lock under `tmp/agent-coordination/git-commit-window.lock.json`; `wait` sleeps until the current lock is released or expires, then claims it before notifying the waiting agent that it can commit. Claims default to 10 minutes, accept only 5-10-minute TTLs, and should be held only while staging, inspecting, committing, amending, rebasing, or finishing commit-and-push follow-through. Release the lock with `bun git-commit-window release --token ` after the commit operation finishes or aborts, and do not record tokens in coordination notes. Archived notes are historical context for targeted lookup, not locks. Agents should search them by path, feature, or workflow only when previous decisions or verification matter to the current task. If another active note claims the same file set, agents should choose non-overlapping slices, write a response note, or ask the human partner to arbitrate. They should never format, stage, or fix unrelated dirty files simply because a repo-wide check or commit hook reports them. When coordinating subagents, split work into named lanes with disjoint owned paths, excluded paths, validation commands, and a no-stage/no-commit boundary unless the lane explicitly owns a commit. The coordinator owns integration and shared generated artifacts by default, including TanStack route trees, migration manifests, route overrides, OpenAPI snapshots, docs navigation, sorted translation bundles, and Supabase generated types. Workers should report generated drift and hand off exact follow-up instead of silently committing it. When a generator can see dirty or untracked files owned by another lane, regenerate from a clean worktree or an explicit input set and copy back only the intended artifact. Write a lane contract before spawning: owner, mode, owned paths, excluded paths, generated outputs, validation, handoff shape, and commit authority. Read-only lanes return evidence and risks without taking ownership. In Codex, choose either a typed worker/explorer role with explicit lane context in the prompt or a full-history fork without a role override; the harness rejects a spawn request that asks for both. After integrating a slice, checkpoint before dispatching more work: refresh status, close completed subagents, update the parent note with the commit or handoff state, list validation and unrelated blockers, and choose the next lane from the current worktree state. The normal scoped commit path still lets Git hooks run. Agents may use `git commit --no-verify` only when they can produce a proof packet for their exact staged paths: reviewed status and staged diff output, path ownership, separated validation mapped to the relevant `bun validate` components, path-based rationale for skipped components, and `bun check:mobile` coverage when `apps/mobile` is touched. Incomplete proof, unclear ownership, or uncertain check mapping means the hook should run or the risk should be reported instead of bypassed. ## AGENTS.md Split Root `AGENTS.md` is intentionally a compact hard-policy index. Detailed implementation gotchas and composable patterns live in focused Tuturuuu skill references instead: * web/API/shared UI patterns in `$tuturuuu-platform` * release badge/version metadata in `$tuturuuu-web-release` * database/API/storage patterns in `$tuturuuu-database` * CI/root-script/tooling patterns in `$tuturuuu-development-tooling` * Docker blue/green watcher patterns in `$tuturuuu-ci-docs` * mobile task-board and overlay patterns in `$tuturuuu-mobile-task-board` When a session reveals durable knowledge, update the narrowest matching skill reference and docs page. Add to root `AGENTS.md` only when the rule is a cross-cutting hard mandate that agents must see before skill loading. ## Skill Scope And Maintenance Descriptions should identify the capability and its actual trigger in a short sentence. There is no minimum description length. Avoid exhaustive keyword lists, mandatory self-selection, and competing style prescriptions. Keep a small entrypoint and route substantial conditional detail to references; a simple skill needs no extra router. Preserve user intent, existing authorization, and concrete security/ownership/release boundaries. Skills should not fabricate tool execution, force unrelated image generation, require approval after every local step, or stop a requested implementation at a plan. Choose checks for meaningful behavior and repository requirements, not repeated rituals. Audit canonical source files and symlink targets once. Installed plugin caches and runtime system skills remain installer-managed. Keep upstream licenses and metadata; local customizations to imported skills may need reapplying after an upstream reinstall. Personal skills outside this Git repository are maintained and verified separately. The September 2026 audit applied these principles from [Eric Provencher's article](https://x.com/pvncher/status/2095991462416490862). See the [repository audit inventory](https://github.com/tutur3u/platform/blob/main/docs/audits/2026-09-05-skills-agents.md) for per-file disposition and known managed copies. ## Validate The Plugin Run the local validator after changing the plugin manifest or its skills: ```bash theme={null} python3 plugins/tuturuuu/scripts/validate_plugin.py ``` The local validator also checks the root skills.sh metadata files against the actual Tuturuuu skill folders, so add every new public skill to both `.claude-plugin/marketplace.json` and `skills.sh.json`. For individual skills, run the Codex skill validator: ```bash theme={null} python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/tuturuuu/skills/tuturuuu-platform python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/tuturuuu/skills/tuturuuu-cms-studio python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/tuturuuu/skills/tuturuuu-satellite-app-ux python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/tuturuuu/skills/tuturuuu-e2e-auth-debugging python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/tuturuuu/skills/tuturuuu-external-apps python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/tuturuuu/skills/tuturuuu-commit python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/tuturuuu/skills/tuturuuu-agent-coordination python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/tuturuuu/skills/tuturuuu-cli python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/tuturuuu/skills/tuturuuu-cli-tasks python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/tuturuuu/skills/tuturuuu-cli-finance python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/tuturuuu/skills/tuturuuu-development-tooling python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/tuturuuu/skills/tuturuuu-devbox-ops python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/tuturuuu/skills/tuturuuu-validation-offload python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/tuturuuu/skills/tuturuuu-web-release python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/tuturuuu/skills/tuturuuu-mobile-task-board python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/tuturuuu/skills/tuturuuu-database python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/tuturuuu/skills/tuturuuu-ci-docs python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/tuturuuu/skills/tuturuuu-review-comments python3 ~/.codex/skills/.system/skill-creator/scripts/quick_validate.py plugins/tuturuuu/skills/tuturuuu-rollout-recovery ``` The Codex skill validator imports `yaml`, so it requires `PyYAML` in the active Python environment. The plugin-local validator uses only the Python standard library. ## Maintenance Rules * Keep `plugins/tuturuuu/.codex-plugin/plugin.json` aligned with the plugin folder name. Do not bump its `version` for ordinary authored work unless a release workflow or user request requires it; when a user explicitly asks for a plugin refresh, bump the manifest and keep this page current. * Keep each skill folder name aligned with its `SKILL.md` frontmatter `name`. * Keep plugin and skill default prompts short, natural, and action-oriented. Marketplace prompt cards should read like user goals, for example "Clear the unresolved review comments on this PR." * Keep skill text portable across machines. Prefer repo-relative paths, ``, or `~` over user-specific absolute paths. * Put detailed guidance in `references/` and keep `SKILL.md` focused on the workflow that should be loaded immediately. * Keep shared-worktree and commit-window coordination guidance aligned across `AGENTS.md`, the Agent Operating Manual, `$tuturuuu-agent-coordination`, and `$tuturuuu-commit`. * Keep the open-PR `.worktrees/` lifecycle, immediate `bun install` (without local builds), and post-merge worktree/branch cleanup guidance aligned across `AGENTS.md`, the Agent Operating Manual, `$tuturuuu-agent-coordination`, the platform checklist, and `$tuturuuu-pr-merge-sync`. Keep the authorized continuous integration, main-green, `bun git-sync`, production verification, and bounded Rust-cache cleanup rules aligned on those same surfaces. * Keep parallel subagent lane, staged-set ownership, generated-artifact ownership, and hook-failure guidance aligned across the Agent Operating Manual, `$tuturuuu-agent-coordination`, `$tuturuuu-commit`, and Git conventions docs. * Keep platform release/version guidance aligned across `AGENTS.md`, `$tuturuuu-web-release`, and the Web Docker Deployment runbook. * Add marketplace metadata only when deciding where the plugin should be installed from: repo-local `.agents/plugins/marketplace.json` or home-local `~/.agents/plugins/marketplace.json`. * Keep public skills.sh metadata aligned across `.claude-plugin/marketplace.json` and `skills.sh.json`; do not modify `.agents/skills` just to publish the Tuturuuu plugin skills. ## ChatGPT and MCP integration See [ChatGPT and MCP](./chatgpt-mcp) for the portable plugin gap matrix, hosted read-only workflow, capability support, authorization gates and validation. ### Realtime services `$tuturuuu-realtime` implements shared room protocols, collaboration, cursors, private checkpoints and reconnect recovery. It reuses `packages/realtime` and the shared programming workbench. Its focused reference covers admission, capability scope, persistence, egress and service verification; operational configuration lives in [the programming realtime runbook](/build/devops/programming-realtime-runbook). ### Backlog priority and dependent receipts Reprioritize requests through the audited `program_backlog.py update --priority <0-3>` command; reimporting a changed priority is rejected. Reopened prerequisites invalidate current dependent verification transitively while preserving audit history and implemented source. Parent recovery never silently restores dependent delivery receipts. See the coordination skill's request-backlog reference and `test_program_backlog.py` for the contract and regressions. ## Operational peer sync source foundation `packages/sdk/src/operational-sync/` defines a version 1 protocol and an inert coordination/replica fixture adapter. It adds no CLI command, HTTP route, production store, grant or service. The local offline CLI remains independent. Existing runner tokens, resource admission and Git commit windows retain their own scope. Bindings carry principal, workspace, project, program, machine, agent, source store, replica and grant identities. A grant revision binds each request. The future server adapter must authenticate the principal and machine, enforce staff and personal-project permissions, and recheck current consent/revocation. Client identity fields are claims to compare against authority, never authentication. The supplied authority callback in tests is synthetic. A SQLite transaction does not serialize revocation in a separate provider or database; production needs a reviewed atomic grant check or equivalent revocation fence in its server adapter. Planning events use expected outcome revision and prior hash. The coordinator assigns a global cursor and immutable hash chain. Operation IDs dedupe exact requests; persisted receipts must match original proposals and immutable event history. Changed requests fail, and stale proposals retain an explicit conflict alongside the existing history. No clock-based or last-writer-wins resolution is implemented. Completion is `user-confirmed`, never `evidence-verified`. Leases bind the complete owner/grant identity, a monotonically increasing fence and server-clock expiry. Acquire, renew and release participate in outcome CAS. Expired/released/foreign owners cannot mutate. Tombstones remain in event history and block resurrection. Lease expiry never proves that an old process stopped, and a lease never bypasses a host resource queue or local commit window. Replicas validate scope, peer store, audience, contiguous global cursors, per-outcome ancestry, hashes and command/projection semantics. SQLite commits the accepted page, projection and cursor together. Interrupted application rolls back; exact replay is harmless and divergent replay fails. Hashes are integrity checks, not peer authentication. Trusted transport and grant validation remain prerequisites. The fixture adapter requires explicit private paths and initialization, rejects symlinks and insecure permissions, and never defaults to the live backlog. Payloads allow only planning state and identities; no credentials, personal memory, customer archives, arbitrary file bodies or operational-note export is wired. Pages contain at most 500 events; stores cap 1,000 outcomes, 10,000 events, 20,000 operation receipts and 32 MiB of serialized state. Saturation fails without truncation. Regression coverage in `operational-sync/engine.test.ts` uses two persisted local synthetic replicas. It proves local CAS/conflict/fence/tombstone/recovery behavior, not network, provider authentication, consent, private SQL or two-machine delivery. The rehashed-projection regression first fails against hash-only chain validation, then passes with shared transition validation. Activation remains denied pending a reviewed grant/backend/transport adapter, exact-head CI and actual authorized runtime qualification. See the focused repository skill reference `plugins/tuturuuu/skills/tuturuuu-agent-coordination/references/machine-sync.md`. ## Pinned security audit skill The shared `.agents/skills/security-audit` directory vendors Cloudflare's MIT security audit skill. `UPSTREAM.json` pins its commit and file hashes; agent aliases resolve to this single copy. It is manually vendored, outside `skills-lock.json`. Keep upstream files unchanged when updating the pin and retain its license. Repository authority, inactive-runtime rules and resource admission still apply. Audit output stays private and unstaged. Target execution requires the skill's OS-enforced sandbox; missing controls produce validation gaps, not confirmed findings. Parent-owned JSON validators do not establish target security. Remediation, CI, integration and production verification remain separate gates. # Development Source: https://docs.tuturuuu.com/build/development-tools/development Learn how to preview changes locally. > [!NOTE] > > **Prerequisite**: You should have installed [Node.js](https://nodejs.org) > (matching the repository floor of `>=22.13.0`). ## Installation ### Supabase Auth declaration compatibility The repository pins a Bun patch for `@supabase/auth-js@2.117.1`. Its credential interface retains every native `PublicKeyCredential` member except `toJSON`, which the package declares separately with its existing response type. Native DOM JSON uses an encoded large-blob string; the package's existing extension output type uses `ArrayBuffer`. Treat these as distinct contracts. The patch changes declarations only. It does not encode extension data, alter authentication behavior, or qualify browser passkey delivery. Both shipped declaration formats and the package source carry the same correction. Keep `skipLibCheck` disabled when checking this compatibility boundary: ```sh theme={null} ttr resources run -- ./node_modules/.bin/tsgo --project packages/supabase/type-tests/auth-webauthn-compat.tsconfig.json ttr resources run -- bun packages/supabase/type-tests/auth-webauthn-compat.ts ``` Bun installs the patch through `patchedDependencies`; do not edit installed dependencies as a permanent fix. Before updating Auth JS or removing the patch, check the strict public verification types and the negative native-credential and encoded-blob controls against the installed replacement. The finite behavior controls call the installed ESM and CommonJS serialization functions with typed mocks. They check registration and authentication separately. The native `toJSON` branch returns its encoded extension output unchanged; the fallback returns raw extension buffers unchanged. Stringifying a fallback large-blob buffer produces `{}`, so this patch does not prove extension data can be transmitted correctly. Native registration JSON also requires fields that the existing fallback does not emit. The controls retain expected type errors for native DOM JSON and credentials without `toJSON`. The package's serializer signatures still require `toJSON` and advertise the package's raw-extension response type even though the implementation supports both branches. Those signature limitations remain separate from this inheritance correction. The mocks exercise pure serialization, not browser credential acquisition or server verification. Step 1. Install [bun](https://bun.sh/docs/installation) on your machine (if you don't already have it yet), by running the following command: **macOS/Linux:** ```bash theme={null} curl -fsSL https://bun.sh/install | bash ``` **Windows:** ```bash theme={null} powershell -c "irm bun.sh/install.ps1 | iex" ``` ## Install the repository dependencies Use the Bun version pinned by the root `packageManager` field. The [version reference](/reference/repository/versions) is generated from repository manifests and lockfiles, so it records the current pins without relying on outdated prose or a globally installed CLI. ```bash theme={null} bun install --frozen-lockfile ``` The Tiptap Pro registry step is retired; no private registry setup is required. `bun setup` is not a minimal installation command: it can build packages and probe services. Use [development without Docker](/build/development-tools/without-docker) for the supported minimal Next.js path. Rust and TanStack Start are not in use and are not required to work on the active web apps. This repository's Bun configuration enforces a minimum release age of 1 day for new npm package versions. If Bun skips a just-published release during install, wait for the package to age past the gate or choose an older published version. First-party package families that publish coordinated releases together may be explicitly excluded from the age gate in `bunfig.toml`; keep each trusted family's exact-name list aligned with the packages resolved in `bun.lock` and cover the list with a policy test. Discord's Python environment is resolved separately in `apps/discord/uv.lock`. For targeted transitive updates, use `uv lock --upgrade-package pyjwt --upgrade-package urllib3`, then confirm that unrelated package records and the project's Python floor are unchanged. Validate artifact hashes against PyPI and run the owning locked Python CI, including the offline dependency security tests in `tests/test_dependency_security.py`. PyJWT 2.15.1 includes malformed-payload/JWKS hardening and accepts valid trailing Base64URL padding while continuing to reject invalid characters. urllib3 2.8.0 bounds chunk-size lines and fixes HTTPS proxy TLS configuration and Deflate streaming. Proxy-specific trust settings belong in `proxy_ssl_context` and the proxy identity options; destination TLS settings are separate. These dependencies are reached through Supabase Auth/MSAL and Requests, respectively. A locked CI environment does not establish the version in an already deployed Modal image: `app.py` has a separate floating `.pip_install(...)` recipe. Verify that image's resolved versions during the separately authorized deployment process. To complete the initial setup, please restart your IDE so that it can recognize the newly installed dependencies. Additionally, some recommended VS Code Extensions may only work after restarting your IDE. > [!TIP] > > If you're using VS Code, you can install following the recommended extensions > that will help you with the development process: > [Biome](https://marketplace.visualstudio.com/items?itemName=biomejs.biome) > (this repo standardizes on Biome for linting and formatting via `biome.json`, > not ESLint or Prettier), > [Vitest](https://marketplace.visualstudio.com/items?itemName=vitest.explorer), > [Tailwind CSS > IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss), > [Version > Lens](https://marketplace.visualstudio.com/items?itemName=pflannery.vscode-versionlens), > [Error > Lens](https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens), > [Pretty TypeScript > Errors](https://marketplace.visualstudio.com/items?itemName=YoavBls.pretty-ts-errors), > [Material Icon > Theme](https://marketplace.visualstudio.com/items?itemName=PKief.material-icon-theme). ### Zed This monorepo tracks project-level Zed settings in `.zed/settings.json`. Opening the repository root in Zed can otherwise trigger expensive scans across generated output and local dependency trees, especially `node_modules`, `.turbo`, `.next`, package `dist` directories, Flutter build output, Rust `target`, and Python virtualenv/cache folders. Keep large generated directories in `file_scan_exclusions`. Zed's `file_scan_exclusions` setting replaces the default list instead of extending it, so include Zed's default VCS/system exclusions whenever adding project exclusions. The project uses TypeScript 7 RC's `tsc` for compiler checks and a single TypeScript language server path for TypeScript, TSX, and JavaScript; avoid enabling competing TypeScript language servers for the same language unless you are deliberately comparing language server behavior. Every app configuration overrides TypeScript 7's `types` compatibility wildcard with its explicit runtime globals, so editors with an older bundled language service do not report `Cannot find type definition file for '*'` across the app tree. ## Development ### Start Next.js apps To develop all apps and packages (without requiring a local setup), run the following command: ```bash bun theme={null} bun dev ``` > [!NOTE] > > This command will start all Next.js apps in development mode. You can access > the platform app by visiting the following URL: > [https://tuturuuu.localhost](https://tuturuuu.localhost) Local app development uses Portless, so each app gets a stable Tuturuuu subdomain instead of relying on its fallback port. The app package `dev` scripts run the repo's Portless-safe wrapper, and the underlying framework command remains available through `dev:app` for direct port-based debugging. The wrapper only removes stale static aliases for the same app when their default backend port is closed, then starts Portless with the original app config. This fixes old worktree aliases such as `zalo-qr-chat-setup.tuturuuu.localhost` without cleaning certificates, deleting unrelated routes, or touching other apps' active aliases. Package-local `bun dev --force` delegates to `portless run --name --force` so Portless can take over a live route when you explicitly request it. Root app-specific commands such as `bun dev:chat`, `bun dev:calendar`, and `bun dev:edu` reuse app servers that are already running. The launcher checks active Portless routes first, then the app's default localhost port; if it finds a direct localhost listener, it tries to register a matching Portless alias so cross-app auth can still use the `*.tuturuuu.localhost` URL. Pass `--force` or `--no-reuse` when you intentionally want the command to include already-running app workspaces. Force mode starts the package `dev --force` path so Portless can take over the matching route instead of leaving a stale static alias in place. ### Inactive migration tooling Rust (`apps/backend`) and TanStack Start (`apps/tanstack-web`) are paused and not in use. Retained commands and Worker entrypoints are future-resumption references, not steps in the current Next.js setup. Do not start these services or require their migration gates for routine app development. See the [active runtime map](/build/devops/active-runtime). `bun dev:web` is intentionally lean: by default it starts only `apps/web` and does not start the `@tuturuuu/types` or `@tuturuuu/supabase` package watch builds. Use this default for app-route and UI work so the local process tree stays small. When you are actively editing those package sources and need their `dist` output rebuilt live, opt in explicitly: ```bash theme={null} bun dev:web --with-shared-watchers ``` You can also pass `--no-shared-watchers` to other root app launchers when you want the same low-memory posture for a focused app session. `apps/web` does not mount React Query Devtools in its default development provider. Keep the default off for faster cold route compiles and lower Turbopack memory pressure; add a local, temporary devtools mount only while debugging query cache behavior, then remove it before committing. Keep the always-mounted public shell small. The mobile menu drawer, marketing footer body, report-problem dialog, and authenticated dropdown-only affordances should stay behind dynamic imports when possible, and shell icons should use `@tuturuuu/icons/lucide-static` instead of the root `@tuturuuu/icons` entrypoint. This prevents the public `/login` compile graph from pulling large Radix, footer, or full icon-package chunks into every `bun dev:web` session. `bun dev:chat` also manages the local chat realtime sidecar. It reuses `localhost:7817` when a sidecar is already running; otherwise it starts `apps/chat-realtime` so `apps/web` can proxy chat SSE traffic for `apps/chat`. | App | Local URL | | - | - | | Platform | `https://tuturuuu.localhost` | | Calendar | `https://calendar.tuturuuu.localhost` | | Chat | `https://chat.tuturuuu.localhost` | | CMS | `https://cms.tuturuuu.localhost` | | Drive | `https://drive.tuturuuu.localhost` | | External | `https://external.tuturuuu.localhost` | | Finance | `https://finance.tuturuuu.localhost` | | Hive | `https://hive.tuturuuu.localhost` | | Hive Realtime | `https://realtime.hive.tuturuuu.localhost` | | Infra | `https://infra.tuturuuu.localhost` | | Inventory | `https://inventory.tuturuuu.localhost` | | Learn | `https://learn.tuturuuu.localhost` | | Mail | `https://mail.tuturuuu.localhost` | | Meet | `https://meet.tuturuuu.localhost` | | Mind | `https://mind.tuturuuu.localhost` | | Nova | `https://nova.tuturuuu.localhost` | | Playground | `https://playground.tuturuuu.localhost` | | QR | `https://qr.tuturuuu.localhost` | | Rewise | `https://rewise.tuturuuu.localhost` | | Shortener | `https://shortener.tuturuuu.localhost` | | Tasks | `https://tasks.tuturuuu.localhost` | | Teach | `https://teach.tuturuuu.localhost` | | Track | `https://track.tuturuuu.localhost` | When debugging without Portless, run an app's `dev:app` script directly. If auth redirects or absolute links must stay on the raw listener port, set `BASE_URL`, `WEB_APP_URL`, or the app-specific URL environment variable to that legacy fallback port for the direct debug session. Keep runtime defaults, auth redirects, and cross-app links on the Portless origins during local development. Use the shared local app URL helpers instead of hard-coding `localhost` fallbacks in satellite app constants or proxies, so a browser session that starts on `tasks.tuturuuu.localhost` stays under the `tuturuuu.localhost` namespace through login and return URLs. When a satellite proxies a Web-owned API collection through an external rewrite, use an exact collection rule and a separate nonempty `:path+` descendant rule. A `:path*` destination can compile to a regex backreference whose empty value leaves a trailing slash. If Web strips that slash with a relative 308, the satellite can repeat the same request indefinitely. Infrastructure's employee collection regression exercises both installed Next runtime matching and the deployment rewrite converter in `apps/infrastructure/next.config.test.ts`. Preserve satellite session and API guards, and verify the emitted deployment route plus an authenticated request after the authorized rollout; passing source tests alone does not establish production availability. For a production-mode smoke test of `apps/web`, use the preview deployment built by CI for the PR's exact head commit. Do not create a local production build on this machine. Local development still uses the Portless hostname for cross-app login and routing checks. Use `bun dev:edu` when working on the education apps together. It starts Learn, Teach, their shared packages, and the central web app so cross-app login can complete locally. Next.js still prints the internal listener port, for example `http://localhost:4803`, because Portless assigns a free backend port to the app process. App `dev:app` scripts also print a `Portless URL:` line from the `PORTLESS_URL` environment variable; use that URL in the browser: ```text theme={null} Portless URL: https://tuturuuu.localhost ``` If Turbo cannot prompt for the Portless sudo password while starting app dev scripts, run the Portless setup helper first: ```bash theme={null} bun portless:setup ``` The helper starts the Portless HTTPS proxy on port `443` before Turbo launches package `dev` scripts. It no-ops when the proxy is already responding and skips in non-interactive shells or CI. `bun setup` runs this helper after dependency installation so fresh local checkouts are ready before the build step, then invokes Turborepo through the repo-local `turbo:local` helper instead of any globally installed Turbo binary. To install Portless as an OS startup service instead of only starting the current proxy daemon, run: ```bash theme={null} bun portless:setup -- --service ``` ### Diagnose a broken local environment When a local app suddenly cannot reach another app — for example the Inventory landing page throws `InternalApiError: 404` because its server-side call to `https://tuturuuu.localhost/api/...` is not routed — run the doctor: ```bash theme={null} bun doctor ``` It checks the Node runtime, Docker daemon, local Supabase ports, local Redis ports, apps/web Redis env wiring, app-local Supabase env consistency, whether the Portless proxy is responding on port `443`, and the health of every registered Portless route. The report uses colored `OK`, `WARN`, and `FAIL` statuses in interactive terminals. Supabase env mismatch warnings are compact by default so secret-like values stay out of the normal output; use the verbose view when you need redacted per-file fingerprints: ```bash theme={null} bun doctor --verbose ``` The most common failure after overlapping or crashed dev sessions is a **stale routing table**: the proxy still points a live app route at a dead dev-server port, so cross-app internal API calls return `404`. The doctor flags those routes as `DEAD` and exits non-zero. Static aliases for apps you simply are not running are reported as warnings, not failures, and include an exact cleanup command. An initialized but empty Portless route table is treated as healthy because no apps may be running yet. Apply the safe automatic Portless repair (start the proxy, or reset it when routes are stale) and re-run the checks: ```bash theme={null} bun doctor --fix ``` `bun setup` deliberately no-ops when a proxy is already responding, so it does **not** clear a stale routing table on its own. To force a clean restart — stop the proxy, prune orphaned dev servers from crashed sessions, then start a fresh proxy that apps re-register against — run: ```bash theme={null} bun portless:reset ``` After a reset, restart your dev servers (for example `bun dev:inventory`) so each app re-registers its route with the new proxy. If `bun doctor` still warns about an inactive static alias after a reset, that alias was created with `portless alias` and is not deleted by proxy resets. Start the app if you still use the alias, or remove the alias shown in the doctor output: ```bash theme={null} bunx portless alias --remove ``` If the doctor warns that app env files use mismatched Supabase values, choose the intended source app and keep these three keys together: `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`, and `SUPABASE_SECRET_KEY`. For quick local development, copy `apps/web/.env.local` to app-local `.env.local` files: ```bash theme={null} bun dev:sync:apps ``` Preview the target files first with: ```bash theme={null} bun dev:sync:apps:dry-run ``` This sync compares app env files only. The root `.env.local` can intentionally target a different Docker or deployment workflow without keeping the app mismatch warning alive. ### Diagnose web dev memory and cache pressure When `apps/web` feels slow or Activity Monitor reports high Node.js usage, start with the dev diagnostic before moving routes into another app: ```bash theme={null} bun diagnose:dev:web ``` Read the report by pressure type: * `Live Next dev process RSS` is the memory held by the running `next dev` process tree. If this is high, inspect the top processes and the latest trace spans before changing architecture. * `apps/web/.next/dev/cache/turbopack` is generated Turbopack filesystem cache. Multi-GB cache growth can make disk usage look like a live memory leak even when process RSS is modest. * `apps/web/.next/cache` can include the Next 16.3 Turbopack build filesystem cache from local builds. It speeds repeated local `next build` runs, but it is still local artifact state and must stay out of Turborepo task outputs. * `.turbo/cache` is Turborepo task cache. It is separate from the live Next.js dev server and should not be treated as app RSS. * `Build memory policy` reports Docker and `next build` memory budgets. Those caps explain build behavior only; do not compare them directly to `next dev` RSS. * `Likely pressure` calls out whether the current evidence points to live RSS, cache growth, watcher limits, or build-only memory settings. If the report points at Turbopack cache growth, stop the web dev server and preview the targeted cleanup first: ```bash theme={null} bun clean:dev:web --dry-run ``` Then remove the web Next dev output: ```bash theme={null} bun clean:dev:web ``` Use `bun clean:dev:web --all-next-dev --dry-run` when several app dev caches exist and you want to inspect every `apps/*/.next/dev` target before deleting them. Add `--include-turbo-cache` only when the report also calls out `.turbo/cache` growth after branch or dependency graph churn. Avoid the broad root `bun clean` command for this problem because it deletes dependency and build state that is unrelated to the current dev-server slowdown. Then restart the targeted app command you were using, such as `bun dev:web`. Avoid deleting unrelated app output while other agents or local sessions are working in the same checkout. If live RSS is the real pressure, use the latest `.next/dev/trace` spans and the matching process tree to reduce the compiled import graph first. Favor narrow package subpath imports and lazy-loaded settings panels before splitting an app boundary. A dedicated infrastructure app only reduces `apps/web` memory when infrastructure pages leave the web route graph; running `apps/web` and an infrastructure app together can increase total RAM. Native local dev leaves Watchpack polling off unless the environment explicitly sets `WATCHPACK_POLLING`. This keeps file watching lighter on macOS and Linux. If the diagnostic reports watcher `EMFILE` errors or route discovery stops updating on your machine, restart with polling enabled: ```bash theme={null} WATCHPACK_POLLING=true bun dev:web ``` If the doctor warns that apps/web is missing Redis env, configure the local SRH bridge and start the Redis stack in one step: ```bash theme={null} bun redis:setup ``` `bun redis:setup` writes `UPSTASH_REDIS_REST_URL=http://localhost:8079` and the matching local token into `apps/web/.env.local`, then runs `bun redis:start`. Restart `bun dev:web` after setup so the app picks up the new env values. To clear local Redis data while keeping the same local env wiring, run: ```bash theme={null} bun redis:reset ``` This starts the local Redis stack if needed, then runs `FLUSHALL` inside the Redis container. To stop the local dev environment, run: ```bash theme={null} bun stop ``` This stops this repo's Portless-registered Next.js dev servers, prunes Portless routes, removes stale static aliases for this project, stops local Supabase, and finishes by running `bun doctor`. Preview the cleanup first with: ```bash theme={null} bun stop --dry-run ``` ### Start Local Supabase Instance To start a local supabase instance (database), run the following command: ```bash bun theme={null} bun sb:start ``` > [!NOTE] > > This command will start a local supabase instance on your machine. You can > access the supabase instance by visiting the following URL: > [http://localhost:8003](http://localhost:8003) > [!WARNING] > > You need to have Docker installed and running on your machine to start a local > supabase instance. ### Stop Local Supabase Instance To stop the local supabase instance, run the following command: ```bash standard theme={null} bun sb:stop ``` `bun stop` is broader than Supabase. It stops this repo's local dev servers, cleans stale Portless aliases, stops Supabase, and verifies the result with `bun doctor`. ### Better Development Experience In case you want to run all local development servers, run the following command: ```bash bun theme={null} bun devx ``` > [!NOTE] > > Running `devx` will: > > 1. Stop the currently running supabase instance and save current data as backup (if there is any) > 2. Install all dependencies > 3. Start a new supabase instance (using backed up data) > 4. Start all Next.js apps in development mode > > If you want to have the same procedure without the backup, you can run `bun devrs` instead. This will: > > 1. Stop the currently running supabase instance (if there is any) > 2. Install all dependencies > 3. Start a new supabase instance (with clean data from seed.sql) > 4. Start all Next.js apps in development mode > [!TIP] > > In case you don't want to run a local supabase instance, you can run `bun dev` > instead. ### Local development #### Seed accounts There are 5 seed accounts that are already set up for local development: 1. [local@tuturuuu.com](mailto:local@tuturuuu.com) 2. [user1@tuturuuu.com](mailto:user1@tuturuuu.com) 3. [user2@tuturuuu.com](mailto:user2@tuturuuu.com) 4. [user3@tuturuuu.com](mailto:user3@tuturuuu.com) 5. [user4@tuturuuu.com](mailto:user4@tuturuuu.com) You can use any of these accounts to log in to the app and quickly test the functionality of the app, since they are already set up with the necessary data. #### Authentication A local mail server (**InBucket**) is automatically set up by Supabase to handle authentication emails. > [!NOTE] > > You can access the mail server by visiting the following URL: > [http://localhost:8004](http://localhost:8004) ## Build Builds run in CI on this machine's workflow. Open a PR, wait for the relevant app build checks on its exact head commit, and inspect the preview deployment when runtime behavior needs verification. `bun setup` also invokes package builds, so use `bun install` for a new local worktree. Do not run `bun run build` or app-level build scripts locally. Run focused tests locally if needed; `bun validate` can build workspace dependencies through Turbo. Use the applicable test, type-check, lint, and build workflows on the PR's exact head commit instead of running it here. ## Test To run all tests, run the following command: ```bash bun theme={null} bun run test ``` To run the Flutter mobile quality gate, use: ```bash bun theme={null} bun check:mobile ``` `bun check:mobile` runs Dart format, Flutter analyze, and Flutter tests for `apps/mobile`. Flutter test output is streamed by default so slow suites still show progress instead of appearing stuck. ### Verify dependency patches Check the official stable version and publication time before resolving an update. Keep `bunfig.toml`'s release-age policy in force. Resolve a small coherent group in an isolated checkout using the package manager, then compare resolved entries and workspace declarations in the lockfile. A new nested entry can be required when a transitive consumer pins the old version exactly; report that constraint rather than claiming every consumer uses the new version. For example, DOMPurify 3.4.16 fixes in-place raw-text and hook-detachment behavior. Test the real browser and isomorphic packages with the owning app configurations, including safe markup, executable attributes and whole-document forms. An old version that fails the targeted regression provides a useful negative control. The contacts preview uses DOMPurify; Mail uses a separate `sanitize-html` contract, so tests of one sanitizer do not verify the other. Monaco 0.57.0 pins DOMPurify 3.4.15, including in its current stable registry metadata; upgrading compatible consumers does not remove that pin. Queue installs and test stages through `ttr resources run` after fresh resource checks, leaving urgent validation ahead of dependency maintenance. A focused test fixture does not establish full workspace resolution, type-checking or app builds. Require those results from CI on the exact implementation commit. ### Mobile release builds The native build commands verify or hydrate each flavor's ignored Firebase SDK configuration from the `tuturuuu-mobile` Firebase project before compiling. The Firebase CLI must be authenticated with access to that project. ```bash theme={null} # Production Android App Bundle bun build:android # Signed production iOS IPA bun build:ios ``` The default iOS command requires an Apple signing identity and provisioning profile. `bun build:ios:ipa` is an alias for the same signed release build. To compile the production app on a machine without signing credentials, use: ```bash theme={null} bun build:ios:unsigned ``` `bun build:android` only produces signed production bundles. Configure the protected upload keystore through all four `ANDROID_KEYSTORE_*` environment variables or `apps/mobile/android/key.properties`; the command fails before compilation when signing is absent and verifies the resulting AAB with `jarsigner`. The Mobile Store Deployment workflow hydrates these credentials in its protected runner and executes this exact command. Each store upload derives its app version from the major and minor values in `apps/mobile/pubspec.yaml` and the highest existing TestFlight prerelease version for that series. For example, an unused `0.11.0` ships as `0.11.0`; once that version exists in TestFlight, later uploads advance to `0.11.1`, then `0.11.2`. An unused higher source patch is retained rather than incremented. Android and iOS receive the same version, while the unique CI build number remains separate. This does not commit a version bump; Release Please still owns the source version. Development builds remain available through `bun build:android:dev`. TestFlight distribution defaults to all available beta groups. Configure `TESTFLIGHT_BETA_ENABLED=false` to disable automatic distribution, or set `TESTFLIGHT_BETA_GROUPS` to a comma-separated list of group names or IDs to limit it. If Apple is still reviewing another build of the same app version, the new build remains available to internal testers and its external review is deferred. Reviews of older app versions do not block a newer version. The Mobile TestFlight Review Queue workflow checks every five minutes and can be run manually; it submits the newest processed internal build to the selected external groups without falling back to an older build. It never withdraws an earlier submission. Once an approved build is available, the retry also expires superseded builds older than 14 days while retaining the two newest uploads, the two newest approved builds, builds in review, and builds attached to an App Store release. The retry workflow accesses App Store Connect only after its code has been promoted to `production`. Validate the store workflow and review queue locally with `node --test scripts/ci/mobile-store-workflow.test.js scripts/mobile-deployment/verify-store.test.mjs`. To hydrate or validate a native Firebase configuration without compiling, run: ```bash theme={null} bun mobile:firebase:configure --environment production --platform android bun mobile:firebase:configure --environment production --platform ios ``` > [!WARNING] > > Tests are still a work in progress. We're currently working on adding tests to all packages to ensure the best quality possible. ## Git Conventions Tuturuuu uses standardized conventions for Git commits and branch naming. For more information, see the [Git Conventions](/build/development-tools/git-conventions) guide to learn how to format your commits and branch names to align with our workflow. ### Diagnosing a first-attempt Playwright failure `trace: 'on-first-retry'` records a trace only when Playwright runs a retry. A suite with no configured retries therefore produces no trace for its initial failure. For the Inventory / Storefront cache contract, use `trace: 'retain-on-failure'` so the first failed navigation has an uploaded trace without adding automatic reruns. A URL assertion can complete before a streamed route enters its recovery boundary. Capture browser errors and recovery details after the failing content assertion, before deleting the local fixture. Keep the original assertion error, timeout, and shell-persistence checks intact; diagnostic failures must not mask it. The cache contract attaches bounded browser errors with Bearer/JWT values redacted and a failure screenshot. Its trace may contain local fixture request data, so keep it within the existing failure-artifact access and seven-day retention policy; never use production credentials or customer records in this fixture. Inspect the trace and attached errors before deciding whether the cause belongs to the application, fixture, or development runtime. A recovery boundary alone does not establish that a failure is a flake. A push changing this test/config already triggers `e2e-tests.yaml`; use that exact-head run rather than creating a duplicate manual dispatch. Manual dispatch currently selects the full E2E workflow, rather than an individual cache-contract job. ### Validating Undici dependency upgrades The direct Undici 8.x owners are Infrastructure and Web. Update their package declarations through Bun and review the lock diff: a scoped direct upgrade should change those workspace entries and the shared 8.x resolution. A recursive update can also refresh SDK/Discord consumers on maintained 6.x and 7.x lines; validate those as a separate consumer group rather than assuming the direct owners cover them. Preserve the repository's minimum release age and lifecycle-script policy. Run the existing managed-cron network and chat link-preview tests in both owning apps using their actual Vitest configs. These checks preserve public-address DNS validation, private-address rejection and redirect handling with injected transport dependencies. They do not establish real socket, HTTP/2 or Worker transport behavior. Web's `src/lib/undici-compatibility.test.ts` additionally uses the actual library to preserve a literal `__proto__` cookie as an own property of a null-prototype result and retain an epoch expiry when clearing a cookie. Require exact-commit Infrastructure and Web type checks and builds on hosted CI after frozen-lock installation. Do not run local builds. A skipped build job is not verification. Infrastructure's existing preview workflow also deploys; use a CI-only build route when deployment is outside the authorized scope. # Documenting Source: https://docs.tuturuuu.com/build/development-tools/documenting Learn how to contribute to our documentation. ## Preview and validate Install the pinned Bun/Node toolchain and clone the repository. Docker, Rust, and TanStack Start are paused and are not prerequisites for writing docs. ```bash theme={null} bun dev:docs bun dev:docs --port 3333 ``` The local wrapper runs the version pinned in `apps/docs/mintlify-version.json` through `npx` from `apps/docs`. Preview at `http://localhost:3000` (or the selected port), and stop with Ctrl+C. It does not start containers or other apps. Run finite source checks from the repository root: ```bash theme={null} node scripts/generate-docs-inventory.js node scripts/generate-app-seo.js node scripts/docs-audit.js node scripts/generate-docs-inventory.js --check node scripts/generate-app-seo.js --check node scripts/local-docs.js broken-links ``` The inventory covers maintained app routes, packages, toolchains, and declared and locked dependencies. Do not hand-edit generated reference pages. It excludes paused Rust/TanStack source and dependency refreshes. Version references describe checked-in versions, not the newest upstream release or a deployment claim. `docs-seo-check.yaml` runs inventory, navigation/link, SEO-policy tests, and strict Mintlify validation in CI. Strict validation compiles docs; do not run it or app builds on this machine. Require relevant lint, type-check, unit-test, and real Next build workflows for the exact commit when implementation changes. ## Sitemap ownership and Search Console `packages/utils/src/seo-policy.json` is the canonical app-domain and public-page allowlist. Generate satellite assets with `node scripts/generate-app-seo.js` and verify them with `--check`. The generator removes obsolete static sitemaps for private apps and the platform; it leaves unrelated public files untouched. Authenticated workspace pages, login flows, private share links, and checkout pages are not sitemap candidates. A private app needs no sitemap until it has explicitly indexable public content; do not copy the platform sitemap into it. The platform owns `src/app/sitemap.ts` and `src/app/robots.ts`; static files with those names conflict with its publication-aware metadata routes. Satellite sitemaps advertise their own canonical origin, including Nova (`nova.tuturuuu.com`) and Rewise (`rewise.tuturuuu.com`) production domains. The retired `rewise.me` and noncanonical `nova.ai.vn` hosts must not appear in generated discovery links. Do not invent `lastmod` values from build time or add language URLs that redirect to the same canonical page. Dynamic publication paths need an anonymous, publication-aware reader before being enumerated; public route patterns alone do not expose a content catalog. After deployment, verify each advertised sitemap returns XML with successful HTTP status and canonical URLs before submitting it under its matching Search Console property. Repository checks do not prove deployment or crawlability. The invite-only Colab SPA also declares `noindex`; its authenticated workshop URLs are not a public sitemap catalog. Docs hosting generates its own `https://docs.tuturuuu.com/sitemap.xml`. ## Writing Documentation ### File Structure Each documentation file should follow this structure: ```mdx theme={null} --- title: "Page Title" description: "Brief description of the page content" updatedAt: "YYYY-MM-DD" --- # Page Title Content goes here... ``` ### Content Guidelines 1. **Use clear headings** to structure your content 2. **Include code examples** with proper syntax highlighting 3. **Add cross-references** to related documentation 4. **Keep content up-to-date** by updating the `updatedAt` field 5. **Use consistent formatting** throughout ### Product Decisions and Regression Evidence Record intentional customer behavior in the owning feature page alongside the change: scope, defaults, affected web/mobile surfaces, persistence and permission rules, and intentional exceptions. Distinguish implemented behavior from pending stages and installed or deployed behavior. Link the owning regression tests and exact validation evidence; do not convert a user preference into a repo-wide rule. Exercise the real boundary that previously failed. Examples include server-to-client date serialization, cache provenance before membership-based cancellation, and route-to-provider payload propagation. A helper-only mock can pass while those contracts remain broken. Use synthetic fixtures; keep private calendars, mail, credentials and live notifications out of shared test evidence. ### Long-session learning maintenance Finish sustained agent work by updating reusable guidance, not by copying the conversation into documentation. Use the existing feature page for customer behavior, the operational runbook for debugging/delivery, and the source skill reference for a fragile agent procedure. Keep AGENTS.md for rules that apply across tasks; managed plugin caches are not editable source. Record the observed cause, the evidence that distinguishes it from alternatives, the safe repair procedure, relevant regression tests and outstanding runtime proof. Separate source implementation, focused local validation, exact-commit CI, deployment markers, production migrations and authenticated runtime behavior. A merge alone proves none of the later stages. Avoid promoting an unverified inference or a one-off workaround into a permanent rule. Use the [autonomy contract](/build/development-tools/codex-plugin#maintaining-focused-agent-guidance) for useful child work during PR/CI waits and occasional evidence-led workflow audits. Propagate accepted guidance through the maintained plugin and next substantive child/owner briefs. Do not add recurring timers or wake retired controllers as a maintenance side effect. For parallel work, assign one owner per overlapping path and Git publication lane. A child agent being listed is not proof that it is active: check its current state and request a concrete path/check/commit handoff. If a PR branch has another agent's dirty packet, integrate main in a fresh isolated worktree rather than staging or overwriting that packet. After the parent merges, transplant the owned follow-up patch onto current main; do not replay the whole old branch. Keep temporary receipts, exact SHAs, pending gates and scheduler IDs in an unstaged coordination note. Register every worked-on PR with T3 Code, including follow-ups. Use native PR watches for PR news; use an explicitly authorized scheduled task for broader delivery follow-through. Record its cadence and next run, and disable it when delivery is complete. Recheck live refs after each wake. The latest explicit user instruction determines merge/review timing; a speed exception for merging does not silently authorize production promotion or expand the release range. ### Code Examples When including code examples: * Use proper syntax highlighting * Include TypeScript types * Show both server and client examples * Include error handling * Test examples before committing ### Images and Assets * Store images in the `images/` directory * Use descriptive filenames * Optimize images for web * Include alt text for accessibility ## Contributing ### Before You Start 1. Check if documentation already exists 2. Look for similar patterns in existing docs 3. Follow the established structure 4. Read the [Organization Guide](/overview/organization-guide) for structure ### Making Changes 1. **Create a branch** for your documentation changes 2. **Write clear, concise content** that's easy to understand 3. **Include practical examples** that developers can use 4. **Update related documentation** if needed 5. **Test your changes** locally with `bun dev:docs` ### Review Process 1. **Self-review** your changes for clarity and accuracy 2. **Check links** to ensure they work correctly 3. **Verify code examples** are correct and complete 4. **Update the table of contents** if adding new sections 5. **Submit a pull request** with a clear description ## Best Practices ### Writing Style * **Be concise** but comprehensive * **Use active voice** when possible * **Write for your audience** (developers) * **Include context** for complex topics * **Use consistent terminology** throughout ### Organization * **Group related content** together * **Use clear section headings** * **Include a table of contents** for long pages * **Cross-reference** related documentation * **Keep examples practical** and realistic ### Maintenance * **Update documentation** when APIs change * **Remove outdated information** promptly * **Keep examples current** with latest versions * **Review documentation regularly** for accuracy * **Ask for feedback** from other developers ### Mintlify runtime compatibility The docs wrapper pins Node 24.19.0 alongside Mintlify in `apps/docs/mintlify-version.json`, independent of the repository `.node-version` pin (see [Node.js runtime pin](/build/development-tools/node-runtime)). Other host versions use an isolated `npx` Node executable; the host installation is unchanged. Got 12.6.1, used by the pinned CLI's connectivity probe, can schedule a retry after rejection on Node 24.20+ and crash before validation reports an MDX result. The compatibility pin preserves validation and failure exits; it does not retry or ignore errors. The initial tool download needs network access. Reassess the pin when the CLI's Got dependency contains the upstream connection-error fix. The wrapper gives npm an empty temporary `--prefix` while Mintlify still runs from `apps/docs`. This prevents npm from traversing Bun's installed workspace graph: published packages can contain development-only `catalog:` ranges that npm cannot parse when applying the repository's security overrides. Keep those overrides and installed manifests intact. The prefix is removed after success or failure; `scripts/paused-runtimes.test.js` covers working directory, prefix isolation, cleanup and exit propagation. Strict validation remains CI-only. # Employee address migration suppliers Source: https://docs.tuturuuu.com/build/development-tools/employee-address-migration Stable identity SQL, Auth and Web suppliers and their qualification boundaries. The portable protocol and suppliers live in `packages/utils/src/internal-accounts/employee-address-migration*.ts`. Infrastructure retains compatible re-exports. Web owns POST endpoints under `/api/v1/infrastructure/internal-accounts/address-migrations/{inspect,prepare,migrate}`. The Infrastructure internal-accounts page exposes their operator dialog through `@tuturuuu/internal-api/infrastructure`. Runtime availability follows current root administrator permission, valid server configuration and fresh SQL/provider readiness. No optional migration activation environment variable is consulted; an absent or false `EMPLOYEE_ADDRESS_MIGRATION_RUNTIME_ENABLED` does not disable an otherwise authorized operation. CI qualification selections below remain independently opt-in test controls. Account execution still needs the operator's authorized exact tuple and the operation's one-attempt protocol. ## Explicit operator preparation and recovery In Infrastructure's internal-accounts dialog, enter the target UUID and exact legacy source address. For an existing intent, also enter its original operation UUID **before the first action**. Leave the operation field blank only for a new intent. The first inspect or preparation persists and freezes the operation UUID, target, both addresses and revision zero. Denial does not unlock or discard them. The original operation remains available through close/reopen and remount within the same actor/workspace browser session. Unavailable or malformed recovery storage fails closed; do not replace an uncertain operation to bypass recovery. A new session with no stored receipt starts in editing state. Use **Confirm and prepare mailbox once** only as a separately authorized action. Retype the exact target UUID and both addresses. This uses the typed existing prepare POST with the account-action header and unchanged server authorization, origin and permission checks. A preparation receipt means mailbox preparation was acknowledged, never that account migration completed. Inspect the same operation afterwards. Retype both addresses for migration; the session performs another fresh inspection before its sole migration request. Version 2 browser receipts record the exact tuple and an unattempted, prepared, uncertain-inspection, uncertain-preparation or migration-dispatched phase. Legacy bare tuples remain compatible and conservatively inspect-only. An unknown preparation outcome cannot be replayed, including after denied, forbidden or invalid inspections. Only exact authoritative readiness may unlock migration; preparation never becomes eligible again. Before any preparation, a known not-ready/conflict inspection may allow explicit preparation, while a pre-read forbidden/invalid response retains the unknown-inspection fence. A dispatched migration can only be inspected, regardless of later ready/denied replies. Transport errors and malformed acknowledgements do not establish readiness or completion. The UI does not retry either write. Focused coverage lives in Infrastructure's `address-migration-session.test.ts` and `address-migration-dialog.test.tsx`, and internal-api's `employee-address-migration.test.ts`. They cover exact preparation confirmation, known-operation recovery, denied/remounted tuple retention, legacy receipts, unknown outcomes, actor retirement, receipt validation and fresh migration preflight. Synthetic tests do not establish production mailbox delivery or customer identity/data preservation. Run them from their owning app/package using the currently admitted resource route; do not activate or rewrite shared shims to obtain local admission. Exact-commit CI and authorized customer acceptance remain separate evidence. ## Address and identity policy The operation changes a canonical lowercase managed-subdomain address to the same localpart at exactly `tuturuuu.com`. It rejects external domains, root-address sources, a changed localpart, self-action, unknown ownership, conflicting reservations, stale revisions and unverified source identities. It never requests another Auth UUID, account creation/deletion, password, metadata, administrator grants or ban changes. The bounded Google destination transition below is the sole exception to the email-only confirmation policy. The provider port exposes only a read and an email update on the original UUID. A protected journal supplier must reauthorize the confirmed active root administrator and lock/revalidate the original actor, UUID, both addresses and all competing CREATE/RESTORE/migration reservations in every transaction. The supplier must independently observe real Auth/provider identity IDs, the public identity, workspace memberships, grants, saved data and mailbox routes. A caller supplied boolean or fingerprint is not administrator or preservation proof. Fingerprints must use documented canonical snapshots and must exclude only the specific email fields that this operation is allowed to change. ## Persisted operation protocol An original operation UUID binds the actor, target UUID, source/destination, revision, immutable-state fingerprint and provider-identity fingerprint. Reservation must be durably idempotent on that whole binding, with unique unresolved target/address ownership. An atomic attempt claim returns true to exactly one invocation; every replay returns false. Neither lease expiry nor process restart may reissue that claim. Attempt IDs are server-generated UUIDs. The executor requires an independent committed read after reservation, attempt claim and completion. A claimed attempt also needs its one-time acknowledgement; a persisted attempted row alone cannot authorize another provider request. Crashes and lost responses retain the original operation for inspection. Even a reserved replay is inspection-only. No expiry, replacement operation, rollback, compensation or automatic provider retry is part of this contract. A provider email update must use the supported admin update on the original UUID with `{ email }` for the original email-provider path. It must perform one request without transport retry and preserve existing provider identity IDs. Actual GoTrue behavior, verification challenges, secure-email-change settings and concurrent external administrators must be qualified before activating that supplier. An unverified destination or changed identity remains pending. Only the separately qualified Google destination transition may request `email_confirm: true`, backed by the exact verified claim. Inspection can complete a named attempted operation after observing the verified destination and unchanged identity. Completion must independently verify actual Auth state and preservation, then atomically update only approved registry/public email projections and mailbox routing with a revision increment. The final readback must match the original binding, completed phase and exact next revision. A completed replay returns that original receipt without another provider write. ## Evidence and remaining gates `employee-address-migration.test.ts` exercises the real source executor with an explicitly in-memory journal/provider fixture. It covers concurrent callers, strict old/new tuples, lost reserve/claim/provider acknowledgements, false acknowledgements, source/destination verification, identity drift, immutable-state drift, completed replay and inspection without relaunch. This fixture does **not** establish durable persistence, SQL lock/ACL behavior, real email change/verification, mailbox delivery, public profile convergence or UUID/membership/data preservation in a real project. Those remain supplier gates. The additive `20261017010000_employee_address_migrations.sql` supplier implements service-only journal transactions with direct table access revoked, durable unique target/address reservations and a one-time claim. Its scope is deliberately legacy accounts absent from CREATE/RESTORE registries with email-provider-only identities, first revision zero to one. The original email-v1 path requires existing active source and destination mailboxes and matching active domains, with both mailboxes owned exclusively by the target UUID. It does not provision mail, change addresses on mailboxes, set onboarding metadata or convert the account into the CREATE registry. The retained completed migration is its address registry; the existing Auth synchronization trigger owns the public email projection. A missing route or registry conflict fails closed. The immutable snapshot covers Auth password/metadata/ban/role/phone state, the public profile, private details except email projections, workspace memberships, workspace role assignments/definitions/permissions, platform grants, owned mailboxes, mailbox memberships and the two domains. It hashes these protected values without returning the snapshot. Saved application rows remain linked to the unchanged UUID; this is not an exhaustive snapshot of every application table. Migration transactions first try exclusive global address admission without waiting. Busy admission raises SQLSTATE `55P03` before any capability or state change; it must not queue behind a shared writer and block later shared writers. Guarded ordinary writers take shared admission before statement row locks. This avoids the destination-row/advisory inversion without serializing ordinary writers with each other. Both original and destination addresses remain reserved permanently. Both RPCs and guarded writers require READ COMMITTED isolation. The RPC uses NOWAIT for row and authorization-table locks after admission; contention aborts without issuing a claim, releasing admission for existing CREATE/RESTORE/provider transactions that already hold rows. There is no automatic retry. Authorization dependencies use short transaction SHARE locks to fence concurrent permission revocation and absent-row grant changes. Qualification must measure that lock scope before activation. Reservation triggers also fence Auth users and identities, public email profiles, mailboxes, reserved grants and CREATE intents. The identity fingerprint is lowercase SHA-256 of compact UTF-8 JSON tuples `[[identity_id, provider, user_id], ...]`, sorted by canonical UUID `identity_id`. GoTrue JSON `identity_id` maps to SQL `auth.identities.id`; JSON `id` is the provider-specific ID. SQL explicitly constructs compact JSON under C collation. Email fields are excluded so the original identity remains recognizable. The original email-v1 provider uses the installed supported `auth.admin.updateUserById(id, {email})` contract. Its independent verification requires the matching email identity's `email_verified=true`, the confirmed timestamp, no pending email and an active account. Upstream GoTrue's admin email-only path can set identity verification false while retaining the confirmed timestamp. That observation stays pending; there is no confirmation override or automatic retry. See the [GoTrue admin handler](https://github.com/supabase/auth/blob/v2.197.0/internal/api/admin.go#L235-L284) and [supported JavaScript API](https://supabase.com/docs/reference/javascript/auth-admin-updateuserbyid). The disposable SQL qualification runner exercises the actual migration on an isolated PostgreSQL fixture with explicitly reduced prerequisite tables. It is not a full migration-history replay or a real GoTrue provider result. It includes a three-session blocking-exclusive control and all four RPC actions under shared contention, checking `55P03`, unrelated Auth-writer progress and unchanged fixture state. Competing claims are drained without retry; exactly one successful capability must match the durable journal attempt and independent inspection. The loser may return false or the exact owned SQLSTATE `55P03` admission-busy envelope; other errors fail qualification. The 10-assertion composed SQL packet does not run these concurrency cases. Run it only with separate admission for its disposable database lifecycle, via the canonical resource queue: ```sh theme={null} ttr resources run -- node apps/database/scripts/employee-address-migration-qualification.mjs ``` The runner retains the exact observed daemon identity `docker-desktop linux 29.8.2` as a pre-creation admission fence. This identifies the originally targeted local fixture; it is not a PostgreSQL compatibility requirement or approval for another daemon/version. A mismatch requires a separately reviewed target/admission decision, not removal of the check. A separate real-provider runner uses cached GoTrue `v2.197.0` and PostgreSQL `17.11.0.004` images on an internal Docker network without published ports. GoTrue distinguishes user confirmation from identity verification: [admin creation](https://github.com/supabase/auth/blob/v2.197.0/internal/api/admin.go#L473-L528) creates the email identity with the default false verification claim, then [confirms the user](https://github.com/supabase/auth/blob/v2.197.0/internal/models/user.go#L488-L507). Magic-link OTP uses recovery verification, which does not update that identity flag. The installed SDK forwards these fields; it does not repair the mismatch. The Utils transport regression owns an SDK devDependency at the existing locked version. Its strict isolated dependency layout must resolve that import directly; a hoisted root SDK can mask a missing package declaration during local checks. A confirmed timestamp plus correlated email alone therefore does not satisfy this migration's stricter verification contract. The guard remains unchanged. The positive fixture creates an unconfirmed disposable source and uses supported `generateLink({type: 'signup', ...})` followed by signup `verifyOtp`. [Signup verification](https://github.com/supabase/auth/blob/v2.197.0/internal/api/verify.go#L294-L375) sets the matching identity flag through GoTrue. This fixture path is not a repair procedure for already-confirmed legacy accounts; generating a signup link for an already-confirmed user is rejected by the pinned server. The real supplier then issues one email-only PUT on that UUID. The runner drops the successful response to exercise an unknown acknowledgement, reads back the actual destination, and proves protocol replay and a repeated supplier call do not issue another PUT. UUID, identity IDs, password hash, metadata, ban state, confirmation timestamp, sessions and refresh-token rows are compared privately. A separate fixture-owned mail table is also compared; that table does not prove preservation of the full product mail schema or hosted delivery. The journal is an explicit fixture. The destination identity becomes unverified, so the result remains `pending` for inspection; this is positive provider mutation and preservation qualification, not a completed address migration. ```sh theme={null} ttr resources run -- bun apps/database/scripts/employee-address-migration-gotrue-qualification.ts --source=signup ``` The retained negative mode creates the source with admin confirmation and uses magic-link OTP. It must remain unverified under the stricter contract, refuse the supplier change with zero PUTs, and preserve its original snapshot: ```sh theme={null} ttr resources run -- bun apps/database/scripts/employee-address-migration-gotrue-qualification.ts --source=magiclink ``` The full-schema workflow's separate `include_address_migration=true` selection adds `employee-address-migration.sql` with exactly 10 rollback assertions. It qualifies the legacy SQL path and existing profile trigger, without treating SQL simulation as real GoTrue verification. Foundation and journal receipts remain incompatible and keep their original defaults; see the [qualification profiles](./employee-schema-qualification). Before release, apply the full schema in the approved isolated fixture, verify generated types, real GoTrue and Web transport behavior, hosted mail routing, and the exact Web/Infrastructure builds in CI. Existing schema CI guards remain in force; a reduced fixture never replaces that gate. Record a scoped promotion/rollback plan separately. CREATE or RESTORE receipts do not qualify this migration, and production/provider/SQL actions require separate authority. ## Permission-authorized Web and Infrastructure surface The surface currently admits only canonical `@xwf.tuturuuu.com` sources and the same localpart at `@tuturuuu.com`. Its initial expected revision is zero, matching the supplier's legacy-first scope. It cannot adopt CREATE/RESTORE registry rows, repair identity verification, provision mailboxes or bypass ownership readiness. For the original email-v1 path, both approved active mailboxes must already belong to the original user. The separately bounded Google-destination exception is described below. Every invocation uses the existing Infrastructure satellite/request-session root-admin authorizer, with a confirmed active exact root-domain actor and `manage_internal_accounts`. The client never supplies that actor. POSTs require the account-action header, same-origin browser origin/fetch-site checks and strict bounded JSON. Each RPC/provider boundary and successful disclosure rechecks the original actor. Composition and reauthorization have no optional migration activation flag; current permissions remain authoritative. Inspection returns readiness only after fresh committed SQL and verified active provider observations agree on the exact original tuple and fingerprints. It projects no fingerprint to the browser. The dialog requires both exact addresses, then repeats inspection before the single explicit migration invocation. That invocation still performs the supplier's own fresh admission checks. A successful mutation reply alone does not publish completion in the UI: a separate inspection must confirm the original operation, target UUID and destination. Before migration dispatch, the browser retains the original operation UUID, revision and tuple in actor/workspace-scoped session recovery. Closing/reopening or remounting keeps that recovery; switching actor/workspace retires in-flight callbacks and prevents disclosure or another dispatch from the old lifetime. Uncertain replies, unavailable readiness, pending verification and lost acknowledgements remain inspectable. No retry, replacement UUID, password/token entry, provider compensation or fabricated success is offered. Session recovery is local to that browser tab; the server journal remains authoritative. Focused regressions cover real Web adapter/provider/journal composition using synthetic transport observations, typed-client tuple validation and UI/controller lifetime/recovery behavior. They do not qualify hosted sessions, full schema, mail delivery, actual account preservation or production completion. Keep the supplier qualification gates and exact Web/Infrastructure CI builds separate. ## Verified Google destination and explicit mailbox preparation The forward migration `20261019010000_employee_google_destination_migration.sql` adds a narrow legacy compatibility path. The source must be a confirmed, active, nonanonymous UUID with exactly one Google identity whose provider ID equals its nonempty subject, whose email is the exact canonical destination, and whose `email_verified` claim is exactly true. The legacy Auth/public email may still be at the source. A verified source-address Google claim does not qualify this path. No upstream Google account change, identity rewrite or manufactured claim occurs. The administrator first explicitly POSTs the original operation/UUID/address/ revision tuple to `/api/v1/infrastructure/internal-accounts/address-migrations/prepare`, with the same request authentication, origin and account-action header used by migration. This is an administrative mutation, not an inspection operation. It can adopt only an existing canonical personal mailbox that has NULL creator, `disabled` status, no archive timestamp, an active canonical domain and zero members or foreign-key-referenced data/configuration. Quarantined, archived, shared, owned, populated or conflicting mailboxes fail closed. It creates no legacy route and changes no Auth or Google state. NOWAIT admission, actor/target locks, membership-table and referencing-table locks fence concurrent changes; contention is a failure, never an automatic retry. Every existing migration record for the target/address prevents preparation, including another operation UUID. The endpoint requires a separate committed inspection before acknowledging preparation. Unknown preparation outcomes require inspection, not blind retry. After preparation, independently inspect the same original tuple. Only then may migration reserve a fresh persisted snapshot and claim its single attempt. Inspection remains unavailable while the mailbox is inactive or unowned. For this exact Google shape, the provider makes one supported admin request `{ email: destination, email_confirm: true }`. The confirmation comes from the already verified exact Google destination claim, with fresh authorization and source readback; timestamps alone cannot authorize it. The original Google identity ID, provider ID, subject and complete identity data remain fixed. GoTrue may add exactly one verified email identity for the same UUID/destination; no other provider/identity addition, removal or change is accepted. The original binding and snapshot stay immutable. The journal independently observes the actual post identity and state hashes, verifies that removing only that new email identity reconstructs the protected original snapshot, and stores a distinct before/after transition receipt in its completion transaction. The executor independently compares the provider's actual post hash and requires the persisted receipt on final readback. It never treats the original and new hashes as equal. Unknown provider outcomes retain the original attempt for inspection; inspection and replay cannot issue another provider request. The separate rollback packet `employee-google-destination-migration.sql` has exactly 30 assertions and is selected by the existing default-off address-migration qualification profile. The original 10-assertion packet and default foundation172/ journal301 profiles remain intact; opting into both address packets yields 212 or 341 assertions respectively. The real-provider runner has a separate mode: ```sh theme={null} ttr resources run -- bun apps/database/scripts/employee-address-migration-gotrue-qualification.ts --source=google-destination ``` That mode seeds synthetic Google claims in a disposable database and exercises real pinned GoTrue admin behavior with a lost acknowledgement. It does not prove external Google sign-in, hosted mailbox delivery or the application SQL journal. The Web state-flow fixture also uses synthetic SQL observations. Require the new packet on the full isolated schema, real-provider mode, generated types, exact-commit Web/Infrastructure builds, and independent source review before release. This source candidate does not authorize production preparation or Auth writes. Mail delivery and actual customer account preservation remain separate post-release checks owned by the authorized operator. ### Real-provider preservation and the optional CI hook The Google provider mode first signs in the disposable confirmed password source and independently reads its authenticated user, then seeds the synthetic Google claim. Its baseline requires nonempty persisted sessions and refresh tokens, including an unrevoked token, before the single admin email update. Every column of those persisted rows is compared afterward alongside the original Google row, UUID, password, metadata and mail fixture. Four controls delete or mutate observed session/token rows and require the same preservation assertion to fail; they do not modify the fixture database. Empty baselines cannot qualify preservation. The existing `employee-schema-contract.yaml` workflow has a separately default-off `include_google_destination_provider` selection. It requires address migration selection and identical reviewed, published source/control commits. The smallest combined admission uses `source_sha=H`, `include_address_migration=true`, `include_public_consumer=true`, `include_google_destination_provider=true`, with all other optional inputs false. The workflow ref must resolve to that reviewed H; source SHA alone does not select the updated trusted control workflow. This keeps the selected 212 assertions (or 341 when journal is explicitly selected) and generates the full isolated schema once. The existing public compiler fixture covers CREATE/RESTORE imports; it does not replace migration-specific Web/Utils compiler and build gates. The Google hook then uses a separate disposable GoTrue/PostgreSQL fixture on the hosted Linux Docker daemon. It verifies actual Linux process/daemon context, pulls only the two pinned version images with fixed time bounds, records their actual IDs, runs one owned process group, and checks run-bound nonempty preservation/negative-control receipts. The child receives only needed local tool/Docker environment values; no service/project credentials or GitHub token are forwarded. Provider results are recorded in `google-provider.json` in the existing private schema artifact. The receipt binds source/control H, repository/run/attempt, fixture UUID, actual image identities, one PUT, preservation counts, process-group stop and independently observed owned cleanup. Primary or cleanup failure, unknown inspect outcomes, mismatched output, missing baseline or an upstream failure leaves a durable unqualified receipt. Finalization cannot turn a prepared or partially executed receipt into success. No automatic provider retry exists. The helper removes only exact UUID/name/label-owned resources and verifies absent containers, network and their volumes. No unrelated Docker cache/resources are removed. This provider receipt remains distinct from SQL/compiler qualification, external Google login and hosted/customer outcomes. The 30-assertion SQL packet also uses an independent local database connection to hold the precise FK-writer table lock. Preparation must fail NOWAIT with SQLSTATE 55P03, preserve exact mailbox/journal/Auth state, and succeed after that owned connection is drained. Post-preparation readback verifies the table SHARE fence. Related ownership/readiness fields remain exact in a grouped JSON assertion; foundation172, journal301, Google30 and legacy10 counts are unchanged. This uses `dblink` in the disposable full schema, not a production connection. Executable source alone is not a passed contention or provider result. ## Explicit retained-mailbox adoption Empty-only `prepare` keeps rejecting populated placeholders. A separate `retained-inspect` / `adopt-retained` administrator action supports an unowned, disabled, unarchived personal canonical mailbox whose sole verified Google identity belongs to the unchanged legacy account. This action is available in Infrastructure's address migration dialog after the original tuple is frozen, including when historical preparation is unknown. It does not resolve that prior outcome or mint a replacement operation. The administrator must freshly confirm the exact target UUID, both addresses and that every retained content item belongs to that account. This is an explicit entitlement decision for this mailbox snapshot. Missing historical envelope data remains missing; matching addresses or localparts alone never establish ownership. The action preserves Auth, complete Google identity, workspace memberships, mailbox UUID, all retained rows, history and external object references. It makes no provider request, creates no account, unbans nobody and sends no mail. The service-only RPC reuses migration authorization and ordered admission locks, then uses NOWAIT table fences for mail graph DML/DDL and conflicting journals. It validates the fixed direct and reverse FK inventory, denies unexpected or composite FKs, and hashes complete retained rows plus linked raw messages, attachments, recipients and label assignments. Only messages, threads, labels and stored objects may populate direct mailbox dependencies. Credentials, catch-all pointers, jobs, historical events, per-user state, notifications and other access records require separate investigation and block this narrow action. Foreign authors, cross-mailbox graph links, unlinked/deleted objects, pending or shared jobs and conflicting employee journals also block it. Whole-mail-table locks make this a short, bounded administrative transaction, never a bulk import API. Inspection returns only a snapshot digest and tuple-bound eligibility; it writes nothing. Adoption revalidates that digest and authorization under the same locks, then atomically assigns ownership, inserts exactly one owner membership and writes `private.employee_mailbox_adoptions`. That durable receipt binds the actor, original operation, revision, target, both addresses and explicit ownership confirmation. Migration reservation is fenced to that original operation. Before dispatch, the UI persists an unknown-outcome fence scoped to the actor, workspace and operation. A lost acknowledgement permits only inspection of the original receipt. An absent receipt, drift, revoked actor or failed readback never silently re-enables adoption. A confirmed receipt triggers the ordinary migration readiness read; reserve/claim/complete remain separate. Clearing browser storage or choosing another operation is not an unknown-outcome recovery procedure. Regression coverage is in `employee-retained-mailbox-adoption.sql` (41 assertions, rollback-only full-schema packet), the adoption session tests and migration HTTP adapter tests. Run the packet using the canonical isolated Supabase runner with `--test supabase/tests/employee-retained-mailbox-adoption.sql`, with same-database `--typegen` output, on the admitted Linux environment. Preserve Studio priority and cold-restore barriers. Static source checks and mocked HTTP tests do not prove SQL acceptance; require actual isolated migration application, generated types, exact-commit CI and independent review before any authorized account operation. # Employee CLI recovery Source: https://docs.tuturuuu.com/build/development-tools/employee-cli-recovery Operation-scoped employee CREATE recovery with existing CLI authentication. The operation-only Web route is `/api/v1/infrastructure/internal-accounts/employees/operations/{operationId}/cli-recovery`. Its CLI authorization grant is separate from Infrastructure audience acceptance. It verifies the unchanged CLI access bearer, CLI origin, current required-MFA policy/lineage and the existing current employee administrator checks. CLI recovery also requires a valid signed email that canonically matches the freshly read, confirmed Auth email, including when both addresses are internal. Missing or invalid email fails closed for reads and writes. Ordinary Infrastructure authorization keeps its existing behavior. There is no CLI-to-Infra exchange, actor override, service-auth bypass or ordinary CREATE admission. The release owner must independently review this grant and require exact-commit Web build, lint, type-check and test CI before any authorized release. For repository operator code, load the existing CLI configuration through its normal loader and create the existing SDK client. Do not print configuration, headers, passwords or credentials. Recovery requests must use a client without automatic refresh/replay: a final authorization failure can follow a committed adjudication or resume. Refresh authentication separately before starting if needed. ```ts theme={null} import { TuturuuuUserClient } from '@tuturuuu/sdk'; import { inspectEmployeeCliRecovery, observeEmployeeCliRecoveryOutcome, submitEmployeeCliRecovery, } from '@tuturuuu/internal-api/infrastructure'; // Repository operator code uses the existing SDK configuration loader. import { readCliConfig } from '../../packages/sdk/src/cli/config'; const config = await readCliConfig(); if (!config.session?.accessToken || !config.session.expiresAt || config.session.expiresAt * 1000 <= Date.now()) { throw new Error('Authenticate with ttr login before recovery'); } const client = new TuturuuuUserClient({ baseUrl: config.baseUrl, accessToken: config.session.accessToken, // Omit refreshToken: this client must not retry a mutation after a 401. }); const options = client.getClientOptions(); const state = await inspectEmployeeCliRecovery(originalOperationId, options); ``` The caller supplies its retained original operation ID; no production identity is embedded in the adapter. Independently compare the returned actor and complete identity tuple to the authorized retained intent. A `fenced`/unavailable response or an actor mismatch stops the procedure. GET never claims or creates anything. For `eligible` version 2, the original actor must actually establish terminal rejection from retained evidence and explicitly approve every existing attestation. Submit `{ action: 'adjudicate', adjudication }` once with the exact returned tuple, original lease/attempt, retained event ID and evidence SHA-256. A nearby provider 422 and account absence do not establish terminal rejection. After an uncertain response, inspect; do not blindly repeat adjudication. Only after an independently matching `retry_ready` version-3 readback may the operator explicitly submit `{ action: 'resume', expected: state, temporaryPassword }` once through `submitEmployeeCliRecovery`. Obtain the fresh password privately and keep it transient. The route rechecks current administrator authorization, operation/key and every identity/audit field before claiming the original operation. The existing journal reauthorizes its SQL and provider phases. After all awaited resume work, the route freshly checks the CLI token, MFA and current administrator authority before disclosing any result. Authorization loss withholds the result and returns `nextAction: inspect`; the mutation may already have committed. Retain the original operation and evidence, restore authorization separately, and inspect without replaying the mutation. The typed client accepts adjudication only at HTTP 200, and resume only as HTTP 201/created, 202/pending or 409/conflict with the matching original identity. Contradictory HTTP/payload pairs require inspection and never establish completion. An uncertain CREATE returns inspection guidance, never permission for another CREATE or replacement UUID. Use the same unexpired CLI access credential for read-only original-operation outcome observation: ```ts theme={null} const outcome = await observeEmployeeCliRecoveryOutcome({ operationId: originalOperationId, targetUserId: retainedOriginalTargetUserId, email: retainedOriginalEmail, displayName: retainedOriginalDisplayName, }, options); ``` This calls `GET .../cli-recovery?view=outcome` with the original operation key. Default GET remains rejection inspection and fences pending/active phases; `view=outcome` uses the normal observation factory with fresh CLI authorization, original actor/operation binding, independent provider identity, fresh SQL proof and final authorization before disclosure. It never claims, creates, provisions, unbans or replays. It requires no Infrastructure audience or credential exchange. Only HTTP 200/created or stopped and 202/pending are valid outcome pairs; conflict, contradictory statuses, identity mismatches and errors never establish completion. The original target, email and creation name remain bound; when terminal active SQL provenance supplies `creationDisplayName`, that immutable name binds the operation while `displayName` reports the current managed profile name. POST resume still requires the exact `retry_ready` version-3 tuple. Authentication restoration happens separately; do not add SDK refresh/replay to these calls. Synthetic local regressions cover signed credential target/scope/origin, expiry, refresh/signature failures, stale/missing signed or current emails, authorization changes during awaited journal work, the HTTP/outcome matrix, owned/missing/foreign operations, tuple/version changes, lost acknowledgement and no CREATE during adjudication. `employee-cli-outcome-route.test.ts` exercises the actual GET with signed CLI credentials, foreign actor/operation, active/pending/stopped, provider ban, contradictory fresh SQL, stale authority and no mutation dispatch. `employee-cli-outcome.test.ts` covers identity and the strict read-only HTTP matrix with one dispatch. These tests do not prove production operation state, original actor, provider effects or delivery. Parent/release custody retains those actions. # Employee schema exception provenance Source: https://docs.tuturuuu.com/build/development-tools/employee-schema-provenance Diagnostic-only isolated exception provenance for two frozen failures. The employee schema workflow accepts structured\_provenance, default false. Normal mode preserves all 172 rollback assertions and the qualification artifact. Diagnostic mode accepts only source 5c7c08e23a3d410bdbdc066f5c4dd2776fbeabd7. It binds the exact trusted control checkout, all five fixture hashes and the probe hash, applies the full frozen schema to an owned disposable database, and runs only the separate two-statement rollback probe. Original fixtures and assertions stay unchanged. The receipt contains SQLSTATE, fixed provenance categories, booleans and ordinals 22/23. Names resolve against the disposable catalog. Known categories additionally require an exact frozen private relation/constraint pair; unknown provenance remains redacted. Raw SQL, exception messages, details, context, row values and names are never published. Commands suppress raw database output and emit only parsed bounded records. Version 2 adds guardCategory. A known category requires SQLSTATE 23514, empty structured names, an exact whitelisted static message and the innermost matching function frame. Only the fixed category is published. Unknown or oversized diagnostics remain unclassified; contradictory serialized records are rejected. Catalog categories are unchanged. Empty structured names alone establish no catalog binding or particular guard origin. A guard category is a static source-signature match, not a catalog constraint or schema qualification. The probe checks its classifier with synthetic inputs inside the same rollback transaction before collecting the two actual records. Exact owned cleanup must succeed. The separate employee-provenance artifact is diagnostic evidence. Command success means two records and cleanup were collected; schemaQualified and assertionsQualified remain false. It establishes no 172 PASS, native/provider readiness, production application or delivery. Never retry old failed attempts; a new diagnostic requires reviewed new behavior and fresh exact control/source binding. # Employee schema qualification Source: https://docs.tuturuuu.com/build/development-tools/employee-schema-qualification Qualify the employee creation and restoration schema in an exact-source disposable database. Dispatch `employee-schema-contract.yaml` on a published ref whose head is the reviewed creation/restoration stack commit, and set `source_sha` to that same full SHA. The workflow rejects a source that differs from the dispatched (control) commit, so no dispatch input selects code to run in that ref's cache scope, in particular `main`'s. It keeps the control checkout at the workspace root for the shared setup action and checks out the same commit under `source/`. Commands run from that source directory invoke the reviewed control scripts via `../apps` and `../scripts`; they never substitute current main or connects to staging or production. Its installed CLI starts a disposable full-migration Supabase project using the same isolated runner as the Tutoring contract gates. Startup/reset and each fixture are bounded; an always-run cleanup step stops only the owned project, without retaining a database backup. ## Employee public-consumer proof The employee schema workflow's `include_public_consumer` option defaults off and excludes standalone chat and diagnostic provenance. It preserves the selected SQL packets and counts while generating types from that owned database, even on the foundation profile. Before cleanup, the maintained adapter records private employee function definitions, table columns/RLS and the complete applied migration ledger as digests before generation, rechecks them after generation, then compiles the current CREATE aliases and RESTORE public imports against the same artifact. It checks source/control commits, tracked source digests, repository/run/attempt, isolated project, artifact hash and catalog identity again after compilation. No SQL or compiler diagnostic values enter the receipt. The compiler uses `noEmit` with `noCheck: false`, inherits strict repository flags, derives source entries from actual public package export targets and replaces only the generated schema inside a disposable Types source copy with the owning dependency links. Its fixture requires operation identity through both creation names and exercises missing/wrong UUID, revision, private RPC and private table controls. UUID string syntax remains owned by runtime validation; TypeScript checks its string representation. `employeePublicConsumer.sourceCompilerQualified` in `qualification.json` requires successful generation, compilation, unchanged bindings, SQL qualification, compiler scratch cleanup and owned database cleanup. Failure, timeout or a skipped compiler stays false; `emittedPublicPackageQualified` stays false. This source proof does not replace exact-commit real builds or emitted public package/release acceptance. Focused regressions live in `scripts/employee-public-consumer-qualification.test.mjs` and the maintained workflow test. SQL/catalog and same-run generated-schema acceptance require a separately admitted hosted run; checked-in-schema compiler smoke is only fixture validation. The compiler state remains provisional through all optional HTTP/Web runtime work. Immediately before the existing owned database stop, a lifecycle adapter rechecks live catalog identity, current source/control binding and the generated artifact against the original compiler state. A failed check still allows the existing stop and cleanup proof to run once. After cleanup returns, the adapter rechecks only the filesystem artifact and source/control binding; it never queries the destroyed database. Both final fences, SQL/lifecycle success and positive owned cleanup are required before sourceCompilerQualified can be true. Primary final-check and cleanup failures remain available together; no unknown operation is retried. Late-runtime artifact/catalog drift and source/control changes during cleanup are covered by the root proof regression file. The lifecycle check does not change packet selection/counts, optional defaults or the existing cleanup ownership rule. The foundation gate requires all five packets: signup guards (25), administrator (20), finalization (34), identity contract (52), and restoration (41). Each packet must emit one complete, sequential, non-skipped pgTAP plan and close its synthetic transaction with rollback. Missing packets, a different count, failed SQL, truncated output, or unsuccessful cleanup fail the gate. The private command output is not uploaded; the small JSON artifact binds the exact source and fixture hashes to the 172 assertions and cleanup outcome. Verify the helper with `node --test apps/database/scripts/employee-schema-tap.test.mjs`. A green schema result is not native admission/lease/fence qualification, a GoTrue or provider transaction, employee activation, or production SQL application. The feedback layer is a separate, default-off selection. In `employee-schema-contract.yaml`, set `source_sha` to the reviewed clean 40-character commit and `include_feedback=true`; leave `structured_provenance`, `include_journal`, `include_personal_journal`, `include_address_migration`, `include_http`, `include_web_session` and `include_restore` false. The control checkout must also contain the feedback input, selector and runner wiring. This profile runs the original 172 employee assertions, then intake34, staff-inbox41 and staff-eligibility31, for 278 total. Existing profiles retain their counts. Incompatible optional profile combinations fail before database setup. `include_feedback` selects tests, not migration application or feature activation. Every profile still applies the complete tracked migration snapshot to its disposable database, including feedback migrations when present. An older profile's green receipt retains its original count and does not qualify feedback. The legacy profile matrix explicitly sets feedback false in each child; the feedback controls separately test true, including a parent dispatch environment with feedback enabled. Neither control changes the profile incompatibility guard. The maintained runner requires hosted Linux with `GITHUB_ACTIONS=true`, an exact HEAD matching `source_sha` and no tracked source modifications. It stages and byte-compares the complete tracked database snapshot, including all migration history, before applying it to its derived isolated project. Never simulate these hosted guards locally. Packet source hashes, strict sequential TAP, rollback, bounded command deadlines and cleanup of only the exact owned project remain mandatory. The always-run workflow cleanup step checks retained ownership before stopping an interrupted project. A successful feedback packet run generates types from that same database before cleanup; the receipt and artifact retain the digest. A failed packet does not establish type generation or SQL qualification. Eligibility qualification must retain an exact-source hosted BEFORE failure and owned cleanup before changing fixture setup. The observed original fixture failed its no-membership assertion: ordinary Auth signup inserts a public profile and unconditionally bootstraps personal membership. Setting the employee marker earlier alone does not prevent that trigger and also requires a canonical creation intent. The eligibility fixture constructs a synthetic membership-free actor by suppressing only `auto_create_personal_workspace_tr` on `public.users` around its two Auth identity inserts. It first locks `public.users` under the existing SQL lock timeout to prevent concurrent trigger-state changes, then requires the named trigger, canonical function and normal `O` enabled state; unexpected state aborts before suppression. The same block immediately restores `ENABLE TRIGGER` and verifies that exact state before all thirty-one behavioral assertions. A final non-TAP integrity fence checks the restored state before `finish()` and rollback. Setup/integrity exceptions fail the packet; these fences add no pgTAP assertions. Transaction rollback also restores trigger DDL if setup or later SQL fails; the hosted lifecycle must still prove owned cleanup. No other trigger, production function, ACL, RLS or membership is changed/deleted. This constructs an eligibility precondition, not proof that canonical employee signup lacks personal membership. Keep that observed signup behavior in the parent residual audit. Do not weaken staff eligibility, remove assertions or substitute a minimal registry. The candidate preserves the original105 assertions and adds one registry-correlation assertion, for106 feedback assertions; require a fresh exact-source hosted AFTER with foundation172, same-database type generation and positive cleanup. SQL causal controls remain separately admitted proof. Static fixture inspection and pure parser tests do not execute SQL or establish AFTER qualification. Feature activation and production remain separate. ### Optional feedback causal qualification The retained hosted source with eligibility30 passed277 assertions and emitted types. The new correlation assertion31 and causal helper are a new source unit; that prior result does not qualify278 or the causal phase. Correlation coverage uses two existing synthetic Auth identities: temporarily give registry row A actor B's distinct valid corporate email, require B to remain denied, restore A's email and compare the complete registry row. No FK/unique/check constraint is bypassed; all original30 assertions remain exact and in order. `include_feedback_causal` defaults false and requires `include_feedback=true`; all other optional profiles must be false. The legacy matrix and feedback guard children explicitly clear the causal flag. Default GREEN parsing and old employee profiles retain their contracts. Source/fixture/migration pins are checked before DB setup. Causal execution also checks the actual control checkout HEAD against CONTROL\_SHA and requires clean tracked control files; ordinary profiles retain their workflow-metadata control binding. In the existing hosted owned database, the optional phase records the actual function body/definition, owner, ACL, settings, identity, language and named trigger state before mutations. It admits exactly eight immutable single-removal variants: service-role, actor binding, email tuple, active lifecycle, confirmation, ban, Boolean onboarding marker and registry join correlation. Each private payload installs its one replacement definition inside the eligibility transaction and rolls it back. Source hashes bind payloads to the reviewed fixed inventory. A separate RED parser requires SQL exit0, complete ordered plan31, exactly the variant's expected failed ordinals, all other assertions successful, no skips, TODO, bailout, truncation or missing rollback. SQL/setup/permission/process errors are never expected RED. Each failed ordinal must have its immediately adjacent, unique pgTAP diagnostic block: matching fixed assertion description, `caught: no exception`, and wanted SQLSTATE42501 for the service-role assertion or PT403 for feedback denial. Canonical [pgTAP throws\_ok](https://github.com/theory/pgtap/blob/v1.3.0/sql/pgtap.sql.in) emits only the wanted SQLSTATE in its no-exception branch; the expected message is bound through the hash-pinned fixture's description/code/message contract. Caught exceptions, missing/duplicate/malformed or incorrectly bound blocks fail closed. This format comes from canonical source, not a claimed live causal SQL observation. SQL definitions containing dollar quotes must be injected literally using a replacement callback or exact slicing, never a replacement string. Structural controls require the exact injected definition and reconstruct the original fixture byte-exact after removing that one span, including one plan and assertion sequence. Every post-rollback catalog snapshot must exactly match the actual baseline; there is no compensating restore or retry. First seven variants require unmodified eligibility31 GREEN and another catalog readback. The eighth restoration uses the full278 profile, then same-database typegen and positive owned cleanup. Expected RED observations are never reported as passes: 248 expected-RED assertions,217 intermediate GREEN and278 terminal GREEN (743 across23 executions). Causal receipt qualification additionally requires every ordered observation, final catalog equality, typegen and lifecycle/cleanup success. The phase has a monotonic20-minute deadline, zero retries, existing60s packet / 45s statement /3s lock bounds,10s catalog reads and bounded output. Cleanup remains outside the phase deadline with its existing60s bound and always-cleanup ownership checks. An unexpected failure stops the phase and retains partial fixed metadata. Actual timeout/backend drain, catalog restoration and hosted cleanup require the real admitted run; injected unit ports and synthetic TAP prove only contracts. The domain regexp is structurally implied by the constrained canonical registry email plus exact lower(Auth email) matching; it is not a tested mutation or RED. ACL and missing-registry availability guards are not mutated and retain ordinary denial coverage. No private recipe, source implementation or positive artifact activates this optional phase; fresh independent review and parent exact-source / control hosted admission remain required. Feature activation is separate. Select `include_journal=true` on the employee schema workflow to apply the full selected database snapshot and run 324 rollback assertions: the original 172, 48 durable-operation assertions, 61 reserved-preflight assertions, 20 HTTP runtime-observation assertions and 23 rejection-adjudication assertions. The receipt uses the `employee-creation-restoration-journal` scope and fails if any packet, assertion, lifecycle step or cleanup proof is missing. The default remains the 172-assertion foundation profile. This SQL gate does not qualify the fixed worker, provider, production configuration, generated types or live account operations. The independent `include_address_migration=true` selection appends three exact rollback packets, in order: Google destination30, legacy address migration10, and retained mailbox adoption41. The defaults remain foundation172 and journal324; the selected totals are foundation253 or journal405. Personal journal cannot combine with address migration. Its receipt scope adds `-address-migration` and requires every packet exactly once, in order, with the exact count, no skips, TODOs, failed assertions or truncation, successful lifecycle and owned cleanup. The original Google30 and legacy10 packets keep their bytes and ordering. After all selected address packets pass, the runner generates public, private and storage definitions from the same isolated database before cleanup. A missing or malformed generated artifact digest prevents address schema qualification. The artifact includes `generated-types.ts`; verify its digest against `generatedTypesSha256` in `qualification.json` before reconciling canonical types. This does not prove application consumers compile or qualify retained storage access, provider changes, acknowledgement-loss runtime behavior or account actions. For SQL-only qualification, dispatch `employee-schema-contract.yaml` with `include_address_migration=true`, `include_http=false`, and other optional inputs false. Use a published branch or tag resolving to the reviewed commit as `--ref` and set `source_sha` to its full immutable SHA. GitHub rejects a raw SHA as the dispatch ref. Verify the accepted run control SHA equals that source SHA before moving the ref; the workflow also rejects mismatched address source/control SHAs. An older control that omits adoption41 cannot qualify retained adoption, even when its source snapshot applies the new migration. Inspect the run's actual control SHA, exact source SHA, three packet entries and fixture hashes, generated artifact digest, lifecycle exit and cleanup receipt. A queued, skipped or failed run establishes no SQL acceptance. HTTP and RESTORE remain separate transport gates and do not contribute to the SQL count. The journal, personal journal, feedback and public-consumer profiles retain their existing same-database type generation. Default foundation selection stays unchanged. Preserve owned cleanup on generation failure; never substitute source parser tests, a minimal fixture or manual SQL for the full isolated migration run. The retained adoption concurrency fixture must connect and drain its independent writer before the first successful adoption inspection. Those inspections retain ShareRowExclusive table fences for the rollback transaction; starting the writer afterward waits behind the fixture itself rather than exercising NOWAIT admission. The packet asserts no such fence is held before starting the writer, verifies its actual independent lock, then exercises the original denial without relaxing it. Conflict fixtures that insert a logical mailbox must link its `domain_id` to the matching `private.mail_domains` row. The current mailbox trigger rejects an address-only insert before employee preflight runs. Preserve that trigger and the intended conflict assertion; a fixture setup failure does not establish a failure in employee reservation logic. If the schema runner fails after its private receipt directory exists, the receipt includes a fixed `failureDiagnostic`: the first failing phase, packet ordinal, bounded exit status, timeout/signal indicators, and TAP failure flags/counts. It contains no assertion descriptions, SQL, exception messages, container paths, or credentials. A diagnostic is evidence about the failed stage, not a passing packet; verify the same exact source and keep all failed attempts. The bounded failure diagnostic also records numeric `failedAssertionOrdinals` only when the TAP numbers are unique, increasing, within the frozen packet's assertion range, and agree with the failure count. Malformed or oversized evidence sets `assertionOrdinalsValid` to false and emits an empty array. Match valid numbers to the exact frozen fixture offline; descriptions and expected/actual values are never exposed. These numbers locate failures and do not change any passing gate. ### Signup exception contract diagnostics For failed first-packet assertions 22 and 23 only, the receipt can include the actual five-character SQLSTATE and a boolean comparison against the fixed expected message. Caught messages, wanted messages, SQL, identities and TAP descriptions remain private. Unknown or inconsistent pgTAP diagnostic formats emit invalid classification with null values; absence of an exception is not inferred. These diagnostics do not alter the expected 23514 contracts or the strict 172-assertion success and cleanup gates. The previous timing-only fixture change remained unqualified after both assertions failed again. Validated caught records also include a fixed message category for the four owned creation-denial literals: intent missing, pending required, identity tuple mismatch, or registry missing. Any other message becomes `other_redacted`. Unknown or inconsistent diagnostic formats keep the category null. Matching is exact: whitespace, case, continuation lines and runtime values are never normalized into an owned category. These enums identify evidence; they do not change fixture expectations, security checks or schema qualification. A separate `constraintCategory` classifies six exact generated CHECK-message relation/constraint pairs from the frozen creation migration: creation-intent staff email/name and employee staff email/name/lifecycle/recovery. The original `messageCategory` remains `other_redacted`. Generated messages omit schema, so schema identity is not inferred. SQLSTATE must be 23514; unknown pairs or altered text remain `other_redacted`; invalid caught evidence emits null. This diagnostic does not change SQL, the 25 assertions or the strict 172-assertion success gate. ## Disposable HTTP and Auth qualification The optional `include_http=true` input requires `include_journal=true`. It keeps GoTrue, PostgREST and Kong in the same isolated stack, runs all 324 SQL assertions and type generation, then exercises the actual Web journal, RPC adapter and provider adapter through loopback HTTP. It creates synthetic identities only in that disposable project; the entire owned stack is stopped by the existing cleanup path. No production configuration or activation flag is changed. The 30 checks cover effective service/anonymous RPC access, successful CREATE, independent persisted state, stable-UUID replay, payload conflict, a committed claim with a lost acknowledgement and a successful Auth create with a lost acknowledgement. Neither unknown outcome may trigger a second provider create. Credentials enter the child over stdin and never enter its command line or artifact. Valid failed receipts retain only a fixed phase and completed check count; malformed child output retains only the parent `http-runtime` diagnostic. `httpTransportQualified` requires a zero child exit, the complete HTTP receipt, all SQL assertions and successful owned cleanup. It is separate from production proof and does not qualify the live request authentication boundary, deployed project configuration, RESTORE, or legacy-address migration. The runtime flag remains off until the remaining target and request-boundary checks pass. Source tests and a published workflow are not a successful HTTP qualification run. ## Web request and session qualification `include_web_session=true` additionally requires both `include_http=true` and `include_journal=true`. This is an opt-in extension of the same owned disposable Supabase lifecycle. It builds the selected `apps/web` source on the hosted CI runner and starts the real Next server on loopback. The fixture resolves the Next CLI from `apps/web/package.json` so isolated workspace installs do not require a root Next dependency; Web declares the fixture's Supabase SSR dependency. It does not resume the retired Docker Web E2E workflow. A missing build prerequisite or failed proxy, session, SQL or provider check fails this mode; do not replace it with a mocked authorizer or a service-only receipt. The Node support-test transform uses the matching `apps/web/e2e/fixtures/tsconfig.json`, which inherits Web's compiler settings. Web's main config includes application source rather than these fixtures; without a matching config, Vite/Oxc can traverse root project references and fail on an unrelated package's unavailable TypeScript preset before collecting any tests. The support suite reproduces that failure boundary in a disposable nested tree and transforms both real fixture files without installing the unrelated preset. Run `bun x vitest run --config apps/web/e2e/fixtures/vitest.employee-web-session.config.ts` from the selected source checkout. Passing this finite support suite does not qualify the 24 hosted Web session cases or the SQL/HTTP/RESTORE profiles. The fixture sends HTTP requests through the real Web proxy and route handlers. Its 24 named cases cover absent/malformed credentials, a GoTrue password session bearer, Supabase SSR-generated cookies, signed infra app cookies, wrong app, expired and tampered app cookies, canonical actor domain, root membership and permission revocation, actor ban, GoTrue session sign-out, origin/fetch-site and action-header guards, self-action, CREATE, RESTORE and stable-operation replay. App cookies are signed by the production token helper using an ephemeral fixture secret; this does not qualify the browser login or satellite token exchange/refresh flow. The API proxy checks current account assurance before the employee route. A previously issued satellite cookie whose current provider user is banned is rejected there with exactly `401 / UNAUTHENTICATED`, before the route's later `403 / forbidden` actor-policy check. A provider bearer rejected by `auth.getUser(token)` uses the same exact proxy denial. The actor-ban and session-sign-out cases require this response, `Cache-Control: no-store`, and no provider update caused by the denied request; they do not accept interchangeable 401/403 responses or arbitrary error codes. Focused required-MFA runtime tests exercise both credential paths with injected provider responses. Only the hosted fixture proves the disposable provider's actual ban and sign-out behavior. [Supabase getUser](https://supabase.com/docs/reference/javascript/auth-getuser) validates through the Auth server. Sign-out is not proof that an already-issued access JWT has expired; the fixture retains its bearer and requires the current provider session check to reject it. A successful source test does not establish that hosted session-revocation case or all 24 Web cases. Failed self-action cases retain only the fixed case/stage, numeric HTTP status, no-store boolean, and an exact allowlisted error category. Unknown codes become `other`; bodies, identities, headers and tokens are discarded. The strict receipt accepts this metadata only with the exact completed prefix before self-action, never as qualification. The self-action expectation remains exactly `409 / employee_self_action`; the restoration handler regression verifies that self rejection precedes provider reads, updates and SQL transport. If a hosted response differs, inspect the fixed metadata before changing that contract. Passing the injected handler test does not identify an unretained hosted response or qualify the remaining CREATE/RESTORE cases. Use `http://localhost:` consistently for the fixture's Web URL and Origin, while the server continues binding to `127.0.0.1`. Next 16 normalizes loopback IP request URLs to `localhost`; sending an IP Origin alongside that normalized URL fails the unchanged origin guard with `403 / forbidden` before self-action or CREATE. Actual NextRequest handler controls reproduce the mismatch and require 409 for self-action and 201 for CREATE with the matching localhost Origin. A finite transport control verifies that the localhost URL reaches the IPv4-only listener. These controls use injected authorization/journal dependencies and do not replace matching hosted Web24 acceptance. Do not repair this origin mismatch by granting permissions: the fixture creator retains creator permissions once its actual MEMBER membership is restored, independently of default grants. A fixture-only loopback proxy forwards requests to the real owned GoTrue and PostgREST services. After a successful provider unban it can discard exactly one acknowledgement; Web must return pending/inspect and reconcile the original operation without another provider write. A second fault revokes root membership after the durable attempt acknowledgement and before provider dispatch. The fixture checks that the target remains banned and no provider update occurred. These are deliberate network faults around real effects, not mocked auth or RPC successes. Existing service-level SQL acknowledgement-loss cases remain separate. `webSessionQualification` records the selected source SHA, completed case names, assertion count, fixed phase, server-stop evidence and explicit production=false. Only a complete ordered inventory, zero child exit, matching source, stopped server/proxy and successful schema lifecycle cleanup can set `webSessionQualified=true`. Failed or partial cases never establish route proof. Failed receipts preserve the first fixed runtime phase and error category separately from server-stop and proxy-close outcomes. Both cleanup steps are attempted even when the first fails. Build-process diagnostics retain only exit code, a bounded signal category and deadline status; child output, messages and stacks remain discarded. Cleanup failure cannot replace an earlier failure or qualify a run. Compiler stdout/stderr are piped only into two bounded 64-KiB in-memory tails; they are never printed or written to an artifact. Failed build receipts project at most eight fixed compiler categories, fixed Node/TypeScript codes, and paths matched exactly against tracked Web/package source paths. Unknown messages, module names, identities, config keys and secret values are discarded. Tail truncation is explicit. Use a matching reviewed source/control revision so the strict control parser retains these diagnostics. This classification is evidence for the next investigation, not permission to guess a dependency repair. Cleanup error categories separately distinguish not-running, type-error, timeout and unknown; a category never turns failed cleanup into success. The hosted fixture first runs the existing root Turbo dependency graph with `bun turbo:local run build --filter=@tuturuuu/web^... --concurrency=1`, then its unchanged `bun run --cwd apps/web build` real Next compiler. Both phases share one 18-minute deadline and run sequentially. Prerequisite errors stop the run. This follows Web's canonical `^build` dependency ordering without allowing an application cache hit to replace compiler acceptance or filtering the disposable Auth/runtime configuration out of the direct Web build. Clean installs can resolve Bun source exports while Node/Next targets still require emitted workspace files; the non-build export regression demonstrates that boundary without generating artifacts. Only hosted CI may execute this build pipeline. Start the resolved Next CLI with `cwd=apps/web` and no additional `apps/web` argument: `node start --hostname 127.0.0.1 --port `. The TypeScript config loader must resolve config imports in the same app context as the build. A bounded Next 16 config-loader reproduction from the source-root cwd selected the SEO generator outside the installed source checkout and failed with a missing-module error; the app cwd selected the source copy and loaded. The disposable config regression exercises this boundary without an app build, server or outbound request. Do not repair shared dependencies or infer a hosted module identity from startup categories alone; matching hosted acceptance remains required after the launcher correction. Startup readiness still requires exactly HTTP 401 from the anonymous employees GET. It makes at most 60 requests (one-second request timeout, 500 ms interval), never follows redirects, and discards response bodies without buffering them. The receipt retains only a fixed startup outcome and last observation, numeric status (0–599), attempt count, child exit/signal and deadline flag. Connection failure, request timeout, other request error, unexpected status and child exit remain distinct; an unexpected status does not identify an Auth or routing cause. Startup stdout/stderr are immediately drained into separate fixed 16-KiB in-memory prefixes. Only fixed allowlisted categories and a truncation boolean enter `startupOutput`; raw lines, module names, paths, URLs, headers, bodies and credentials are discarded. Classification scans each bounded prefix once. A descendant retaining a writer cannot hold cleanup open: 250 ms after the direct child exits, remaining capture readers close and truncation is recorded. The existing TERM/KILL and outer process-group cleanup still govern owned processes. Truncation or an empty category list cannot clear a possible cause. Loopback controls exercise these distinctions with disposable children, including the pinned Bun runtime; they do not establish readiness of the hosted Web application. The fault proxy starts HTTP server closure before forcing connection closure. In Bun 1.4.1, forcing connections first stops listening and makes a later `close()` reject with `ERR_SERVER_NOT_RUNNING`. The finite actual-Bun control requires successful close, refused connections on the released port, and a rejected repeated close. It emits only fixed phase/error categories on failure. Use the workflow's pinned Bun/Node versions when diagnosing runtime differences; newer local runtime success is not evidence for the pinned runtime. Historical receipts with only `phase=cleanup` cannot identify the earlier cause. The support suite exercises simultaneous runtime/cleanup failures, real child exit and deadline handling, and strict redacted receipt projection. These checks do not replace hosted Web24 acceptance. Receipt/process gates run from the uninstalled immutable control checkout; only `source/` has workspace dependencies installed. Keep process controls on the canonical dependency-free `.mjs` owner rather than importing the Web TypeScript support module there. Dependency-bearing adapter tests run in the installed source checkout. Process controls copy byte-checked canonical modules into a fresh dependency-free directory, use Bun `--no-install`, and retain all marker, exit, port/PID and timeout assertions. A control import failure can precede every build/database/session stage; absent later artifacts do not identify a Web cause. The control runner owns the fixture process group and launches each requested build group itself over a private IPC channel. The fixture cannot detach an unregistered build: missing owner IPC fails closed. Build exit triggers whole-group TERM/KILL cleanup even when a descendant retains stdout/stderr or ignores TERM. Fixture exit, hard kill, timeout and cancellation abort and drain every owned build job before `processGroupStopped` can qualify. Build output is classified at the owner; only the fixed bounded diagnostic returns over IPC, never raw output. Actual finite controls retain leader exit 7, enforce an inner deadline, and verify port/PID disappearance after hard fixture kill and outer timeout. They use the resolved runtime executable so local command shims cannot drop IPC descriptors. These synthetic process controls do not replace hosted compiler/start/session proof. Build/server output, passwords, cookies, tokens, emails and raw provider/SQL errors are discarded. The stack's always-run cleanup removes fixture identities with the disposable database; no real account or deployed activation flag is modified. Source receipt tests prove receipt rejection behavior only. Activation still requires the parent release owner's review of the actual exact-source CI artifact and all independent production gates. ### RESTORE transport profile `include_restore=true` additionally requires both `include_http=true` and `include_journal=true`. It extends the same disposable project, exact-source checkout, bounded child and always-run cleanup. The default 324 SQL and 29 CREATE HTTP checks retain their counts. RESTORE adds 66 checks in seven fixed phases: RPC permissions (18), success/replay/conflict (11), lost committed reservation, attempt and completion acknowledgements (8 each), lost successful GoTrue unban acknowledgement (8), and revoked SQL actor (5). The receipt parser requires every phase count and a zero child exit before cleanup can qualify RESTORE transport. The fixture creates managed employees through the real CREATE journal, bans them through disposable GoTrue, and restores the same user UUID through the retained RESTORE orchestration and five-function PostgREST adapter. Separate HTTP reads verify committed operation phase, revision and provider identity. Anonymous and signed-in outsider credentials cannot execute any of the five RPCs; a service caller naming an outsider or freshly banned administrator is also denied by SQL. Lost-acknowledgement injection discards responses only after real commits. Pending RESTORE and reconciliation retain their operation UUID and never repeat an unban; completed reconciliation replays the receipt without advancing revision again. `restoreTransportQualified` is independent of `httpTransportQualified`. It requires the complete selected SQL/HTTP/RESTORE profile, successful lifecycle and owned cleanup. This profile calls the retained service orchestration directly; it does not qualify Web request authentication, satellite sessions, session revocation, production configuration, hosted races or legacy-address migration. Real GoTrue transport evidence is distinct from SQL fixture evidence. The RESTORE runtime flag remains OFF. Dispatch only from a reviewed control revision with the exact reviewed source commit; a published fixture is not a successful qualification run. ## Personal journal schema qualification Select `include_personal_journal=true` with `include_journal=false`, `include_http=false`, `include_web_session=false`, `include_restore=false` and `include_address_migration=false` on the same workflow. This optional profile keeps the foundation's 172 assertions and adds the 24 `personal-journal.sql` assertions. It requires the complete selected migration snapshot, all 196 assertions, transaction rollback and positive owned-project cleanup, then records the `personal-journal` scope. Mixed optional profiles fail closed. The default foundation and employee-operation profiles keep their existing counts. The workflow regression test exercises all 64 boolean profile combinations against the guarded runner before hosted database setup. The same disposable database generates public, private and storage types before cleanup. Review the exact-source artifact and its digest before adopting generated types. This gate does not qualify a real journal provider, explicit consent, authenticated runtime behavior, memory ingestion or production activation. Before dispatch, keep new, unapplied PR migrations later than the current base migration tip. The pending personal-journal migration is `apps/database/supabase/migrations/20261017000000_personal_journal.sql`. Preserve SQL bytes when correcting only its chronological identity; never rename an applied migration. A path change changes the selected snapshot and source SHA, so earlier SQL/type artifacts remain historical and the new source requires fresh qualification. The isolated runner derives its full snapshot from tracked files, including the renamed migration, rather than a fixed migration-name manifest. ## Opt-in chat initial-settings qualification `include_chat_initial_settings` is a default-false, standalone profile on the existing `employee-schema-contract.yaml` workflow. It selects the unchanged 172 foundation assertions plus exactly 25 assertions from `chat-initial-ai-settings.sql`: 197 total. Existing foundation, journal, personal journal, address, feedback, HTTP, restore and structured-provenance defaults and counts are unchanged. Chat mode rejects every other optional profile, including structured provenance; it never silently skips a missing chat packet. The workflow input maps to `EMPLOYEE_SCHEMA_INCLUDE_CHAT_INITIAL_SETTINGS`. The same disposable database applies all source migrations once. The chat packet is rollback-only. A separate bounded bridge intentionally commits four synthetic AI conversations under the actual `private.chat_create_conversation(uuid,uuid,jsonb)` admission: workspace credits, personal credits, explicit-null credit scope and legacy no-tuple creation. Its actor has create/view permission and lacks manage permission. Readback after COMMIT must match each original RPC receipt exactly; a denied create must leave zero added rows. This does not modify the production RPC, its ACL, settings-edit permissions or SQL serializers. Both chat fixtures reuse the personal workspace and creator membership produced by each synthetic user's normal signup trigger. Personal-credit inputs and the SQL envelope carry that actual UUID. The committed bridge marks all three owned workspaces (shared plus both bootstrap personal workspaces); cleanup checks their creator identities and shape, deletes only that marked set, and proves its absence. Shared-workspace permission defaults explicitly target `MEMBER` using the `(ws_id, permission, member_type)` key. Only that synthetic shared MEMBER admin default is disabled, so explicit view/create grants, manage denial and subsequent create revocation exercise the real permission evaluator without an admin bypass. The explicit Web qualification target consumes those unchanged SQL-produced receipts through the actual conversation POST handler. Only external auth, model admission, realtime and SQL transport ports are synthetic. It records nine successful controls, including malformed-calendar, workspace/tuple mismatch, SQL denial and route denial, with at most one creation transport call and zero provider calls. The actual Web response then enters the public Dart `ChatRepository.createAcknowledgedConversation` path and strict receipt parser. Nine Dart controls cover four successes, malformed-calendar/workspace/actor/tuple rejection and rejection of raw PostgreSQL timestamps without Web normalization. No hand-written JSON artifact can substitute for the SQL-producing phase. Message timestamp transport is **not** qualified by this creation-only bridge. Web fixtures remain under `apps/web/e2e/fixtures` and use the explicit `vitest.chat-initial-settings.config.ts`. Mobile uses the explicit `tool/qualification/chat_initial_settings_bridge_test.dart` target outside ordinary test discovery. Missing SQL/Web envelope environment is a failure, not a skip. CI alone installs the existing Flutter 3.47.x toolchain and restores locked mobile dependencies for this opt-in mode; it never builds a mobile app. Ordinary Web/mobile suites and real app builds remain separate exact-source gates. Before dispatch, the delivery owner must approve a clean published source SHA and reviewed control ref. Retain the resolved run control SHA, never infer it from a moving ref. Source/control checkouts, run ID, migration identity, source fixture hashes and control helper hashes bind the private envelope and phase results. Qualification requires strict 197 TAP, successful actual Web/Dart processes and phase files with exact expected cases/hashes, stopped child process groups, exact-marker committed-fixture cleanup, and finally successful owned database stop plus container absence. Missing phase, ambiguous cleanup, wrong pin or artifact existence alone is HOLD. `chatInitialSettingsQualified` and `schemaQualified` stay false on any failure. Employee HTTP, native backend, provider and production flags cannot be inferred from this profile. The private three-day artifact includes `qualification.json` and bounded `chat-*.json` SQL/Web/Dart envelopes and runner result files. They contain only synthetic fixture identities; keep them out of public PR descriptions. Never reuse this receipt as an upgrade/production-application or provider/device proof. Pure contract regressions are in `apps/database/scripts/chat-initial-settings-qualification.test.mjs`; run them through admitted resource control before claiming execution. Source preparation and static syntax checks do not qualify SQL, Web or Dart behavior. ## Original CREATE response recovery Infrastructure preserves a validated actor/workspace-scoped original CREATE operation reference in session storage before dispatch, without a password. If storage cannot confirm that reference, it does not dispatch CREATE. Pending, malformed or lost responses trigger bounded TanStack observation reads of `GET /api/v1/infrastructure/internal-accounts/employees/operations/:operationId`. This is a CREATE observation contract; employee RESTORE GET/reconcile endpoints must never substitute for it. A normal confirmed CREATE response incurs no extra observation round trip. The GET uses the current Infrastructure session actor and rechecks authorization before disclosure. Its supplier exposes only readiness/inspection and provider identity reads. Existing private `employee_creation_runtime_inspect` performs observation with row locks and no durable writes. Completion requires the exact operation/target/email/name tuple, an independent confirmed active provider identity with `employee_onboarding=true`, and a subsequent fresh SQL inspection of the active account, profiles, personal mailbox/ownership and restricted grants. A stored phase or earlier receipt alone cannot establish completion. Revoked or halted operations stop confirmation; absent or incomplete observations remain unconfirmed. No GET can claim, advance, finalize, activate, reconcile or repeat a provider operation. The client admits at most six reads within 30 seconds of the first confirmation read, with a 500 ms interval and an eight-second client request timeout. Admission counts and start time survive remounts; reopening cannot reset the budget. Conflicts remain conflicts, permission denial stops reads, and results from a retired actor/workspace lifetime are ignored. Unresolved references remain locked against another CREATE after the budget expires. These bounds describe behavior, not measured production latency. Readback cannot finish a provider/SQL step that the original invocation never completed; it can recover confirmation when that invocation completed despite an uncertain response. Regression coverage: Web `employee-creation-observation.test.ts` and operation route tests; internal-api `employee-creation.test.ts`; Infrastructure creation client/dialog/recovery tests. These synthetic transport and DOM checks do not qualify real SQL/Auth, deployed authorization, customer creation latency or production acceptance. The delivery owner must verify exact-commit CI and Web and Infrastructure builds, then separately collect authorized runtime evidence. ### CREATE password admission The shared internal-api `EmployeeCreationPasswordSchema` is used by both the Infrastructure form and Web CREATE boundary. Before readiness or journal claim, it requires 12–72 characters including an ASCII lowercase letter, uppercase letter, digit and printable ASCII punctuation. The punctuation set follows [Supabase password security](https://supabase.com/docs/guides/auth/password-security). The form explains these requirements in English and Vietnamese and disables submission until the shared schema accepts the password. It does not normalize, log or retain the credential in the recovery reference. Boundary regression controls reject each missing character class before readiness, claim or provider admission; the internal-api test additionally proves zero HTTP dispatch. This admission check prevents the observed character-policy mismatch for future requests. It cannot confirm or repair an existing reservation whose Auth creation was rejected. The current provider adapter still classifies non-conflict provider errors, including weak-password rejection, as unknown. Changing that durable outcome mapping or retiring an earlier rejected reservation requires a separately reviewed causal recovery contract; observation must continue to report incomplete state truthfully. Provider-specific rejection (including any additional hosted policy) is not permission to replay CREATE. ## Administrator adjudication of a rejected initial Auth attempt An initial CREATE at `unknown` remains non-retryable by default. The service-only `adjudicate_employee_creation_rejection` RPC is a one-time exception for the original administrator's terminally rejected first attempt, at version 2. It requires explicit human attestation of terminal rejection and no request in flight. Temporal log correlation and account absence are never machine-verified rejection proof. If the administrator cannot make that determination, retain the unknown operation and escalate for investigation. Bind the exact original actor, operation, target UUID, canonical email, name, expired lease and attempted event. Retain the evidence outside the journal and supply its SHA-256 reference digest with the fixed adjudication reason. The RPC freshly authorizes the original administrator, validates both original history events and the retained intent, and independently checks current Auth identity, profiles, registry, mailbox, grants and competing reservations. Revocation, changed identities, later phases, existing repair or conflicting state deny it. Only service execution is granted; direct audit-table writes are denied. Success appends a history event and immutable adjudication record and returns the same operation at reserved/version 3 with an expired replacement fence. It does not create, activate or prove an account ready. A later explicit normal CREATE must acquire a fresh lease and use a newly validated password through the existing journal, provider and persisted completion proof. Never persist credentials, replay an unknown provider request, delete history or allocate another target. A lost repair acknowledgement requires inspection; do not blindly retry it. The journal profile now requires the separate 23-assertion rollback packet `employee-rejection-adjudication.sql` (324 total). The disposable CLI 2.117.0 pins GoTrue v2.196.0, whose administrator CREATE path does not enforce password strength. Both HTTP and authenticated Web session terminal rejection controls share the same invalid-input producer and exact rejection proof. Each control submits mutually exclusive plaintext and hash inputs, requires exactly HTTP 400 with `validation_failed`, and independently verifies the selected Auth UUID remains absent before adjudication. This qualifies terminal rejection recovery, not the hosted production weak-password policy. Production policy is a separate runtime observation. See [the pinned Auth guard](https://github.com/supabase/auth/blob/v2.196.0/internal/api/admin.go) and [the CLI image manifest](https://github.com/supabase/cli/blob/v2.117.0/apps/cli-go/pkg/config/templates/Dockerfile). Failed rejection HTTP qualification retains a fixed substep, primary error category, message hash and bounded assertion value in `httpQualification.rejectionFailure`. Raw SDK errors, response bodies, addresses and passwords are excluded. The top-level assertion counter does not identify a failed substep or prove that prior SQL packets passed; inspect the packet and lifecycle receipts separately. The canonical disposable HTTP fixture additionally rejects a real weak password through GoTrue, adjudicates through PostgREST, verifies the committed reservation, denies replay and finishes normal CREATE with the same UUID. Its strict top-level count is 30. Until that exact-source fixture and real migration application pass, these are authored regressions, not runtime qualification or production acceptance. The Infrastructure stopped-operation panel offers explicit inspection and adjudication. The authenticated Web recovery route reauthorizes the original root administrator, binds the full tuple and workspace, and reads back the committed audit before exposing retry readiness. Session storage retains the original operation plus repair event/digest before dispatch, never a password. An uncertain response stays fenced until explicit matching audit readback. Only then can a user enter a fresh password and explicitly submit normal CREATE with the original operation UUID and locked email/name. Closing or remounting the form does not mint a replacement operation. The optional Web-session profile adds the real authenticated recovery GET/POST and normal CREATE POST to its ordered case inventory, including revoked-session, workspace, actor, attestation, origin, tuple and replay denials. The basic HTTP profile qualifies the lower service/journal boundary; it does not establish Web request authorization. The selected HTTP fixture typecheck maps the `@tuturuuu/supabase/next/user` type-only export to its owning source because the package declaration export otherwise requires unbuilt `dist`. Keep that source-only gate enabled; do not build locally or bypass type errors. ## Managed profile qualification Select `include_managed_profile=true` with `include_journal=true` on the same reviewed control/source commit. The runner selects rollback packets `employee-managed-profile.sql` (53), `employee-admin-profile-usernames.sql` (74), then `employee-unmanaged-profile-usernames.sql` (100), before the committed `employee-managed-profile-concurrency.sql` (10). Types come from the same disposable database. Username packets exercise changed 1/4/33/65/81/1001-character handles, null clear, exact ownership, duplicate/CAS/replay denials, unchanged public 5–32 policy and preserved account data. The legacy packet additionally rejects partial/historical managed evidence, revoked actors and wrong identity/domain; it preserves disabled/unconfirmed target eligibility, nullable preimages and atomic owned old-handle release. Only exact protected contexts admit length exceptions; other generic limits and B-tree physical bounds remain. Optional defaults remain unchanged. `managedProfileQualified` requires all four exact packets, same-run types digest, base packet success and positive owned cleanup. Missing packets and old receipts cannot qualify. With `include_http=true`, the managed receipt remains exactly 47 assertions and one lost-ACK write. Its required `unmanagedProfileQualification` v1 adds 75 assertions, lengths 1/4/33/65/81/1001, six single writes with lost-ACK independent inspection, exact replay, stale-CAS/duplicate/managed-bypass/ordinary-actor and public-policy denials. These SQL/PostgREST checks do not establish authenticated Web route composition, browser or production acceptance. Select the separate default-off `include_profile_route=true` with journal, HTTP and managed profile enabled; `include_web_session` is not required and its existing creation/restoration matrix is unchanged. The hosted fixture builds and starts exact Infrastructure source on loopback under the owned process/deadline helpers. Anonymous PATCH readiness must return 401; GET 405 is not readiness. Real GoTrue sessions traverse the Infrastructure proxy, required-MFA guard, route authorization, service and managed/null-fallback SQL writers. Each managed and unmanaged target already owns `hehe`: a name-only PATCH must return 200 with SQL readback and exactly one correctly bound writer RPC; a competing target must return 409 without SQL state changes. Retained credentials after root revocation or ban, AAL1 after policy activation, and an invalidated real TOTP proof must be denied without writer calls; real TOTP verification must pass. Anonymous and malformed Bearer requests cannot borrow a valid cookie. Private SQL snapshots compare identity, credentials, access, mailbox ownership and creation provenance; receipts publish only fixed diagnostics. `profileRouteQualification` is independent of the 47/75 transport counts and requires exact source, all 18 cases, process-group drainage and owned database cleanup. Its `routeAuthorizationProof=true` remains disposable route evidence; `productionProof=false` always. Direct PATCH imports and mocked authentication cannot qualify this receipt. See the [Infrastructure contract](/platform/applications/infrastructure#managed-employee-profile-edits). # Git Conventions Source: https://docs.tuturuuu.com/build/development-tools/git-conventions Learn how Tuturuuu uses Conventional Commits and Branch naming to improve development workflow. > [!NOTE] > > **Prerequisite**: You should be familiar with basic Git operations and > understand the [Monorepo Architecture](/build/development-tools/monorepo-architecture) of our > codebase. ## Overview Tuturuuu follows standardized conventions for Git commits and branch naming to improve collaboration, automate releases, and maintain a clean, navigable repository history. We use two main specifications: 1. [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for structured commit messages 2. [Conventional Branch](https://conventional-branch.github.io/) for consistent branch naming These conventions help automate our CI/CD workflows, generate changelogs, and make our development process more efficient. ## Conventional Commits ### What are Conventional Commits? Conventional Commits is a specification for adding human and machine-readable meaning to commit messages. It provides a set of rules for creating an explicit commit history, making it easier to write automated tools on top of. The basic structure of a conventional commit is: ``` [optional scope]: [optional body] [optional footer(s)] ``` ### Types Tuturuuu uses the following commit types: | Type | Description | Example | | - | - | - | | `feat` | New feature or enhancement | `feat: add dark mode support` | | `fix` | Bug fix | `fix: prevent crash when user data is undefined` | | `docs` | Documentation changes | `docs: update installation instructions` | | `style` | Code style changes (formatting, no code change) | `style: format code with prettier` | | `refactor` | Code refactoring | `refactor: simplify authentication logic` | | `perf` | Performance improvements | `perf: optimize database queries` | | `test` | Add or fix tests | `test: add unit tests for auth middleware` | | `build` | Changes affecting build system or dependencies | `build: update dependency to fix security issue` | | `ci` | Changes to CI configuration files and scripts | `ci: simplify workflow branch filters` | | `chore` | Routine tasks, maintenance | `chore: update package.json metadata` | ### Scopes Scopes provide additional context about which part of the codebase is affected. In our monorepo structure, we often use package or app names as scopes: ``` feat(web): add new dashboard layout fix(ui): correct button alignment in mobile view chore(types): update supabase database types ``` ### Breaking Changes Breaking changes must be indicated by adding a `!` after the type/scope or by using a `BREAKING CHANGE:` footer: ``` feat(api)!: require authentication for all endpoints BREAKING CHANGE: All API endpoints now require authentication tokens. ``` ### Examples Here are some examples of conventional commits used in our codebase: ``` feat(web): add new calendar integration fix(supabase): correct permission issue in auth policy docs(readme): update development setup instructions style: apply consistent formatting with prettier refactor(utils): simplify date formatting functions perf(queries): optimize workspace user loading test(auth): add tests for token verification build(deps): update Next.js to v14 ci(vercel): add automatic preview deployments chore(release): publish packages ``` ## Conventional Branches ### What are Conventional Branches? Conventional Branch refers to a structured naming convention for Git branches that makes it easier to identify branches by type and purpose. The basic structure is: ``` / ``` ### Branch Types Tuturuuu uses the following branch types: | Type | Description | Example | | - | - | - | | `main` | Main development branch | `main` | | `feature` | For new features | `feature/user-dashboard` | | `feat` | Short feature prefix also accepted | `feat/invite-link-permissions` | | `fix` | Short bug-fix prefix also accepted | `fix/invite-membership-check` | | `bugfix` | For bug fixes | `bugfix/login-error` | | `hotfix` | For urgent fixes | `hotfix/security-vulnerability` | | `release` | For preparing releases | `release/v1.2.0` | | `chore` | For maintenance tasks | `chore/update-dependencies` | | `docs` | For documentation-only work | `docs/git-conventions-refresh` | | `style` | For formatting or stylistic cleanups | `style/biome-pass` | | `refactor` | For internal code restructuring | `refactor/task-query-shell` | | `perf` | For performance-focused changes | `perf/workspace-user-search` | | `dependabot` | Automated dependency update branches | `dependabot/npm_and_yarn/next` | | `claude` | AI-assisted scratch or experimental PR | `claude/rate-limit-audit` | Release Please bot branches are also accepted with the `release-please--branches--` prefix. The exact automated image-optimization branch `imgbot` is also allowed; similarly named branches still require a conventional prefix. Do not create human-authored branches with that prefix. ### Naming Rules 1. Use lowercase alphanumeric characters and hyphens 2. Keep names concise but descriptive 3. Include ticket/issue numbers when applicable 4. Avoid special characters (except hyphens) ### Examples ``` feature/workspace-sharing feat/public-course-sharing fix/file-upload-error bugfix/auth-vulnerability release/v2.5.0 chore/update-react-18 docs/git-conventions-refresh feature/issue-123-user-profile ``` ### Merge Commit Exception Authored commits should follow Conventional Commits. The common exception is a Git-generated merge commit when syncing a long-lived branch, for example: ```bash theme={null} Merge branch 'main' into feat/studying-platform ``` Keep that merge summary when the goal is to preserve the branch's merge history. Use a conventional commit message again for the next authored commit after the merge. ## How These Conventions Improve Our Workflow ### 1. Automated Changelog Generation Our conventional commits are used to automatically generate changelogs for releases. Different commit types are categorized accordingly: ```markdown theme={null} # Changelog ## Features - add dark mode support (#123) - add calendar integration (#124) ## Bug Fixes - prevent crash when user data is undefined (#125) ## Documentation - update installation instructions (#126) ``` ### 2. Semantic Versioning Conventional commits help determine the next semantic version for packages: * `fix:` commits trigger a PATCH increment (1.0.0 → 1.0.1) * `feat:` commits trigger a MINOR increment (1.0.0 → 1.1.0) * Commits with `BREAKING CHANGE` trigger a MAJOR increment (1.0.0 → 2.0.0) ### 3. Automated Release PRs And Package Publishing Release Please reads Conventional Commits from `production`, updates `release-please-config.json` packages through `.release-please-manifest.json`, and opens one combined release PR with package versions and changelogs. Package publish workflows do not generate versions; they publish only after a release-please version bump lands on `production`. Local manifests keep Tuturuuu workspace dependencies as `workspace:*`; npm release workflows rewrite the checked-out package manifest with `scripts/ci/prepare-npm-package-manifest.js` immediately before `npm pack` so published artifacts contain installable npm version ranges. Package-included `file:` tarball dependencies, such as `@tuturuuu/ui`'s vendored SheetJS tarball, must not leak a consumer-relative `file:` range into the npm manifest. Package preparation expands the checked-in archive into the artifact, redirects the package export to those immutable bytes, and removes the install-time dependency edge. Do not rewrite the vetted archive to a mutable external tarball URL. Before any build or pack work starts, package workflows run `scripts/ci/package-release-readiness.js gate-package-release packages/`. The gate checks publishable Tuturuuu workspace dependency versions on npm once, dispatches missing dependency workflows, and exits green without occupying a runner while dependencies are pending. If the related dependency workflow for the same production SHA failed, completed successfully without npm visibility, or cannot be inspected, the gate fails immediately. Publish jobs still wait for their own version to become visible after `npm publish`, then a separate non-OIDC job dispatches direct dependent package workflows. Workflow-published package manifests must carry provenance-compatible `repository` metadata for `tutur3u/platform`; otherwise npm rejects trusted publishes with `E422`. For example, `release-types-package.yaml` triggers from production changes to `packages/types/package.json`: When bringing the generated release PR branch back to `main`, run `bun git-release-please` from a clean `main` checkout. The helper fetches the latest `release-please--branches--production` branch, merges it without committing, syncs `platform-version.txt` into the platform badge constant and test expectation, runs `bun ff`, stages the resolved merge, then runs `bun validate` directly before the merge commit lands. When the staged release merge includes `apps/mobile` paths, the helper also runs `bun check:mobile`. If you are already inside a manual merge, run `bun release:sync-platform-version` to resolve the recurring `TUTURUUU_PLATFORM_VERSION` conflict before staging the merge. ```yaml theme={null} on: push: branches: [production] paths: - "packages/types/package.json" # ... jobs: check-version-bump: if: github.ref == 'refs/heads/production' && needs.check-ci.outputs.should_run == 'true' # ... ``` ### 4. Better Code Reviews With conventional commits and branches, it's easier to understand the purpose of a pull request at a glance: * A PR from `feature/user-dashboard` with commits like `feat: add user stats widget` clearly indicates a new feature * A PR from `bugfix/auth-issue` with commits like `fix: prevent token expiration error` indicates a bug fix ### 5. Simplifying Navigation Conventional branch names make it easier to navigate the repository history and find specific changes. For example: ```bash theme={null} # Find all feature branches git branch --list "feature/*" # Find branches related to authentication git branch --list "*auth*" ``` ## Tools and Enforcement We use a dedicated CI/CD check to enforce our Git conventions: ### Branch Naming Check We use a GitHub Action workflow to verify that branch names follow our convention: ```yaml theme={null} name: Branch Name Check on: push: branches-ignore: - main - production jobs: check-branch-name: name: Check branch name runs-on: ubuntu-latest steps: - name: Check branch name run: | BRANCH_NAME=${GITHUB_REF#refs/heads/} if ! [[ $BRANCH_NAME =~ ^(feature|feat|fix|bugfix|hotfix|release|chore|docs|style|refactor|perf|dependabot|claude)/.+|^release-please--branches--.+|^imgbot$ ]]; then echo "❌ Branch name '$BRANCH_NAME' doesn't follow the conventional branch format." echo "Branch name should be in format: type/description" echo "Allowed types: feature, feat, fix, bugfix, hotfix, release, chore, docs, style, refactor, perf, dependabot, claude" echo "Bot branches are also allowed with the release-please--branches-- prefix or the exact name imgbot." exit 1 else echo "✅ Branch name follows convention: $BRANCH_NAME" fi ``` ## Best Practices ### Writing Good Commit Messages 1. Use the imperative mood ("add" not "added" or "adds") 2. Keep the description in lowercase to match the repository's observed convention (for example, `feat(backend): migrate nova team read`) 3. Do not end the description with a period 4. Keep the description under 72 characters 5. Use the body to explain the what and why, not the how Example of a well-formatted commit: ``` feat(auth): add multi-factor authentication support Implement TOTP-based multi-factor authentication to improve security. The implementation follows the RFC 6238 standard. Closes #123 ``` ### Coordinating Commits In Shared Checkouts When multiple agents or humans may commit in the same checkout, use the commit window before changing the staged set. The lock is advisory and lives under the ignored `tmp/agent-coordination/` directory, so it protects the Git index and commit operation without becoming part of a commit. Claims default to 10 minutes and may only be 5-10 minutes, so claim only when ready to stage, inspect, and commit. This coordination is **per-checkout** and **harness-agnostic**. The commit window and the `tmp/agent-coordination/` notes both live in the working directory, so they coordinate every agent or human sharing one checkout — parallel Codex or Claude Code sessions, background tasks, and same-directory subagents — regardless of which tool they run under. A separate `git worktree` has its own lock file, its own notes, and its own Git index, so the window does not span worktrees; separate worktrees stay isolated by being on different branches and integrate through the shared remote instead. In a hot shared checkout, commit your owned paths promptly in small scoped commits: a large unstaged set can be discarded by a concurrent rebase, `reset --hard`, `checkout`, or `stash` from any agent or human. (`bun git-sync` is isolated and safe; manual destructive Git is not.) ```bash theme={null} bun git-commit-window status bun git-commit-window claim --owner "" --scope "type(scope): subject" git add path/to/file-a path/to/file-b git diff --cached --stat git diff --cached --name-only git commit -m "type(scope): subject" bun git-commit-window release --token ``` If another agent owns the window and it is appropriate to wait, use `wait`. The command sleeps until the active lock is released or expires, then claims the window before reporting that it is safe to proceed. The waiting period can be longer than the claim TTL, but the claimed window remains capped at 10 minutes: ```bash theme={null} bun git-commit-window wait --owner "" --scope "type(scope): subject" ``` Use `--allow-staged` only after inspecting existing staged files. The commit window does not grant ownership of files or permission to stage unrelated paths. Existing staged files are owned by the staging agent or coordinator until explicitly reassigned. If a file appears as `MM`, both its staged and unstaged diffs need owner review before commit. Let commit hooks run by default. This is a proof-gated no-verify path: use `git commit --no-verify` only when the current agent can prove its exact staged paths would pass the checks normally covered by `bun validate`. The proof packet should include reviewed `git status --short`, `git diff --cached --stat`, and `git diff --cached --name-only` output; the touched files or narrow path group; the separated checks that covered each affected `bun validate` component; any skipped components with path-based rationale; unrelated dirty files excluded from the claim; and `bun check:mobile` coverage when `apps/mobile` is touched. If ownership is unclear, proof is incomplete, or the check mapping is uncertain, do not bypass the hook. Commit hooks and repo checks can read the whole worktree, including files owned by other agents. If they fail on unrelated dirty files, release the commit window and report the blocker instead of formatting, fixing, staging, or committing those files for convenience. Only the staged-set owner may use the proof-gated no-verify path, and only with exact-path evidence for the staged files. ### Branch Management 1. Create branches from the latest `main` branch 2. Keep branches focused on a single task or issue 3. Regularly rebase long-lived branches on `main` to avoid merge conflicts 4. Delete branches after they've been merged ### Synchronizing Release Branches Use the root command below when `production` should point at the same commit as `main`: ```bash theme={null} bun git-sync ``` The command fetches `origin`, creates a temporary detached worktree, refreshes `main`, and fast-forwards `production` to the current `main` commit from that temporary worktree before pushing those branches. It finishes by fetching again and verifying that local and remote `main` and `production` both resolve to the same commit. The checkout you started from is left untouched, so uncommitted work on another branch can keep running while the release refs are synchronized. If a branch that needs to move is already checked out in another worktree, Git will refuse to force-update that branch. Switch that worktree away from the branch or update it manually, then rerun `bun git-sync`. `bun git-sync` does not create commits for you. Commit all intended changes on `main` first; if `production` contains commits that are not already in `main`, reconcile the branch manually before rerunning the command. Use `--only-branch` to update one active sync branch while leaving the other untouched: ```bash theme={null} bun git-sync --only-branch production ``` `main` is still refreshed locally as the source commit, but only `production` is fast-forwarded and pushed. Use `--only-branch main` when you only want to pull and push `main`. The retired `staging` branch is no longer a supported `--only-branch` target. The Supabase staging environment still exists, but its workflow is driven from `main`. Use `--current-branch` or `-c` when `main` and `production` should move to the latest commit on the branch you currently have checked out instead of latest `origin/main`: ```bash theme={null} bun git-sync --current-branch bun git-sync -c ``` Current-branch mode still uses a temporary detached worktree, leaves the checkout you started from untouched, and only moves the selected sync branches. It does not push the source branch unless the source branch is itself one of the selected sync branches, such as running from `main`. Combine `-c` with `--only-branch` when only one long-lived branch should move to the current checkout's commit: ```bash theme={null} bun git-sync -c --only-branch production ``` Use `--no-push` for local-only synchronization: ```bash theme={null} bun git-sync --no-push bun git-sync --only-branch production --no-push bun git-sync -c --no-push ``` With `--no-push`, the command fetches, pulls with `--ff-only`, and fast-forwards the selected local branches, but it never pushes to `origin`. Final verification checks local refs only. ### Pull Request Workflow 1. Create a branch with the appropriate type based on the work 2. Make commits using conventional commit messages 3. Push the branch and create a pull request 4. Use conventional commit style for the PR title 5. After approval and merge, delete the branch ## Conclusion Following these Git conventions helps us maintain a clean, understandable repository history, automate release processes, and improve collaboration across the team. By standardizing both commit messages and branch names, we create a more efficient development workflow. When a branch grows large enough that one pull request cannot be reviewed comfortably, split it into a chain of dependent PRs — see [Stacked Pull Requests](/build/development-tools/stacked-pull-requests). For more information, refer to the official documentation for [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) and [Conventional Branch](https://conventional-branch.github.io/). # Local Supabase Development Source: https://docs.tuturuuu.com/build/development-tools/local-supabase-development Learn how to work with Supabase locally in the Tuturuuu development workflow. > [!NOTE] > > **Prerequisite**: Install [Docker](https://www.docker.com/products/docker-desktop/) and followed the > [Development](/build/development-tools/development) setup guide. ## CI image pulls Contract startup and type generation retry registry throttling and transient image-pull transport failures up to seven attempts, waiting 10/20/40/80/120/120 seconds plus 0-25% jitter. SQL tests, resets and pushes are never replayed. Authentication, missing-image and schema failures fail immediately; exhausted pulls remain failures. Type generation retains bounded private diagnostics and publishes only successful output. The E2E baseline keeps its existing service set, excluding only Edge Runtime and Functions. SQL-only contracts keep their existing narrower service sets. CI uses `public.ecr.aws`; no image archive cache is introduced. Regression coverage: `TMPDIR=/private/tmp node --test apps/database/scripts/run-supabase-image-retry.test.js`. Real cold/warm/corrupt baseline and contract acceptance runs in exact-commit CI. ## Basic Commands ### Starting Supabase ```bash theme={null} bun sb:start ``` Local ports are pinned in `apps/database/supabase/config.toml`: | Service | URL / Port | | - | - | | REST/Auth API | [http://localhost:8001](http://localhost:8001) | | Postgres database | `postgresql://postgres:postgres@localhost:8002/postgres` | | Studio (dashboard) | [http://localhost:8003](http://localhost:8003) | | InBucket (test emails) | [http://localhost:8004](http://localhost:8004) | > [!NOTE] > > Run `bun sb:status` at any time to print the live URLs, keys, and the DB > connection string for your running instance. Prefer reading these values from > `bun sb:status` over hard-coding them, since they are the source of truth for > the current container. ### Stopping Supabase To stop the local Supabase instance: ```bash theme={null} bun sb:stop ``` ### Checking Status To view the current URLs and status of your local Supabase instance: ```bash theme={null} bun sb:status ``` ## Development Workflow ### Recommended Startup When developing for Tuturuuu, you have several options to start your environment: 1. **Standard Approach**: Start Next.js apps and Supabase separately. ```bash theme={null} bun dev # Starts all Next.js apps bun sb:start # Starts Supabase ``` 2. **Enhanced Development Experience**: Use the `devx` command for a streamlined setup. ```bash theme={null} bun devx ``` This command: * Stops any running Supabase instance and saves current data as backup * Installs all dependencies * Starts a new Supabase instance (using backed up data) * Starts all Next.js apps in development mode 3. **Fresh Database Setup**: When switching branches with potential schema changes. ```bash theme={null} bun devrs ``` This command (`bun sb:stop && bun sb:start && bun sb:reset && bun dev`): * Stops any running Supabase instance (without backup) * Starts a new Supabase instance * Resets the database to use the latest schema and seed data * Starts all Next.js apps in development mode App-scoped variants also exist (for example `bun devrs:web`, `bun devrs:tasks`) that reset Supabase and then start only that app's dev server. > [!WARNING] > > Use `bun devrs` when switching between branches that might have different > database migrations to ensure your local database schema matches the branch > you're working on. ### Syncing With Schema Changes If you're keeping your Next.js server running but need to reset your database to match the current branch's schema: ```bash theme={null} bun sb:reset ``` This command will reset your local database to use the latest schema definitions and automatically regenerate TypeScript types. After the reset, the database package also runs the local AI credits bootstrap, which syncs gateway models through the local Supabase REST API and retries the short PostgREST schema-cache warmup window that can happen immediately after containers restart. ## Database Schema Management ### Making Schema Changes There are two approaches to modifying the database schema: #### 1. Using the Supabase UI 1. Navigate to your local Supabase Studio at [http://localhost:8003](http://localhost:8003) 2. Make your changes through the UI 3. Generate a migration file: ```bash theme={null} bun sb:diff ``` This creates a migration file based on the differences between your current schema and the previous state. > [!WARNING] > > Be cautious when using `sb:diff` for schema changes. If you rename columns or > tables, the migration will drop the old ones and create new ones, which can > result in data loss in production environments. #### 2. Creating Manual Migrations For more control, you can create empty migration files and populate them manually: ```bash theme={null} bun sb:new add_custom_function ``` This creates a new empty migration file in `apps/database/supabase/migrations` using the provided migration name. If you omit the name, the helper uses `new_migration`. ### Applying Migrations After creating a migration, apply it to your local database: ```bash theme={null} bun sb:up ``` > [!NOTE] > > This same process is used to keep our production database up-to-date with the > schema defined in the `production` branch. ### Validating an Exact Worktree in Isolation Database plans pinned to an older commit must not reset or reuse the ordinary `tuturuuu` stack when its migration history belongs to another checkout. Run the disposable validator from the repository root instead: ```bash theme={null} bun --cwd apps/database sb:validate:isolated ``` For a focused pgTAP file, pass its path relative to `apps/database`: ```bash theme={null} bun --cwd apps/database sb:validate:isolated --test supabase/tests/workspace-creator-membership.sql ``` To replace the checked-in database types only after the disposable reset and focused pgTAP test both succeed, add the exact approved output path: ```bash theme={null} bun --cwd apps/database sb:validate:isolated --typegen packages/types/src/supabase.ts --test supabase/tests/habit-tracker-write-rls.sql ``` The type generator uses the same pinned Supabase binary and disposable project, includes the `public`, `private`, and `storage` schemas, buffers its output, and atomically replaces `packages/types/src/supabase.ts` only after a successful, non-empty result. Reset, pgTAP, type generation, or output-write failure keeps the existing generated file unchanged. No other output path is accepted. The runner derives a Docker-safe identity from the exact worktree path and commit, selects an available deterministic eight-port block, and prints both. It copies only tracked `apps/database/supabase` files into an owned `tuturuuu-supabase-*` temporary directory, changing its project id and ports without editing tracked config. Staging failure removes only its partial project and preserves the original error. Secondary cleanup failures emit only `Secondary staging cleanup failure.`, without paths or exception details. `node --test scripts/run-supabase-isolated-staging-cleanup.test.mjs` checks error identity, retained partial files and scoped removal without Docker. The lifecycle starts the copied project, resets its exact-base migrations, runs full or focused pgTAP, and stops only that project with `--no-backup`. It never uses `supabase stop --all` or the ordinary `sb:stop`/`sb:reset` aliases. SIGINT and SIGTERM request scoped cleanup while preserving the command or signal exit code. Hard termination can retain `.tuturuuu-isolated-supabase.json`, recording the project id, worktree commit, ports and last complete lifecycle step. Resume or clean the recorded directory without guessing its Docker identity: ```bash theme={null} bun --cwd apps/database sb:validate:isolated --resume /tmp/tuturuuu-supabase-EXAMPLE bun --cwd apps/database sb:validate:isolated --cleanup /tmp/tuturuuu-supabase-EXAMPLE ``` Use the printed disposable path; its temporary parent varies by operating system. Cleanup refuses paths outside the owned pattern; failed Docker cleanup retains metadata for a safe retry. Recovery metadata updates atomically replace the prior regular file with mode `0600`; temporary creation stays exclusive and symlink targets are rejected. Fault tests interrupt descriptor writes, then verify partial bytes and the prior record. Run the retained proposal's suite separately from the repository root: ```bash theme={null} TMPDIR=/private/tmp ttr resources run -- node --test --test-concurrency=1 apps/database/tests/programming-hosted-typegen-review/metadata.node-test.mjs ``` This `.node-test.mjs` suite is absent from root script discovery and active CI; `apps/database/scripts/*.test.*` results do not include it. ### Generating TypeScript Types After schema changes, regenerate the TypeScript types to keep your code in sync with the database schema: ```bash theme={null} bun sb:typegen ``` The generator includes the `public`, `private`, and `storage` schemas. Keep server-owned/private tables out of Supabase REST exposure, but still regenerate types after migrations so server-only helpers and storage paths can import accurate generated row shapes. Alternatively, you can use the shorthand: ```bash theme={null} bun typegen ``` > [!NOTE] > > This step is automatically performed when running `bun sb:reset`, making it > useful when catching up with a new branch's schema. ### Using Generated TypeScript Types The Supabase-generated TypeScript types are available at `packages/types/src/supabase.ts`. These types are accessible to all apps that have the `@tuturuuu/types` package installed. You can use these types to ensure type safety when working with Supabase data: With supabase-js v2, table types flow from the client itself rather than a generic on `.from()`. The Tuturuuu Supabase helpers in `@tuturuuu/supabase` are already typed with the generated `Database` type, so queries are type-safe out of the box. Reach for the `Database` type directly when you need a specific row shape: ```typescript theme={null} import type { Database } from '@tuturuuu/types/supabase'; type WorkspaceMemberRow = Database['public']['Tables']['workspace_members']['Row']; // Type-safe access to tables (the client already knows the row shapes) const { data, error } = await supabase .from('workspace_members') .select('*') .eq('ws_id', workspaceId); // data is typed as WorkspaceMemberRow[] | null // Type-safe access to specific columns const { data: workspace } = await supabase .from('workspaces') .select('id, name, handle') .eq('id', workspaceId) .single(); // TypeScript knows the structure of 'workspace' with proper types const workspaceName: string | undefined = workspace?.name; ``` ### Short-hand Type Access For more convenient access to common table types, Tuturuuu also provides short-hand type definitions in `packages/types/src/db.ts`. These are easier to use and remember than the full database type paths: ```typescript theme={null} import type { WorkspaceCourse, WorkspaceRole } from '@tuturuuu/types/db'; // Use short-hand types directly const { data: roles } = await supabase .from('workspace_roles') .select('*') .eq('ws_id', workspaceId); // Type is now WorkspaceRole[] roles?.forEach((role: WorkspaceRole) => { console.log(role.name, role.permissions); }); // Short-hand types can also include extended properties const course: WorkspaceCourse = { id: 'course-id', ws_id: 'workspace-id', name: 'Course Name', created_at: new Date().toISOString(), updated_at: new Date().toISOString(), href: '/courses/course-id', // Extended property not in the database }; ``` You can add your own short-hand types to `db.ts` for tables you frequently work with. This is especially useful for tables that have complex structures or need additional client-side properties. This ensures that your code correctly interacts with the database schema, reducing runtime errors and improving development experience. ## Migration Files All migration files are stored in `apps/database/supabase/migrations`. These files: * Contain SQL commands that create and modify the database schema * Are executed in order based on the timestamp prefix in their filenames * Include descriptive names after the timestamp to help developers understand their purpose > [!WARNING] > > When contributing new migrations in a Pull Request, always add them after the > latest migration file from the `main` branch. This maintains the correct > execution order and prevents issues when syncing the production database. ## Local Authentication A local mail server (**InBucket**) is automatically set up by Supabase to handle authentication emails. You can access it at [http://localhost:8004](http://localhost:8004). With InBucket, you can: * Receive all authentication emails sent by your local Supabase instance * Test any email combination without needing actual mail delivery * View password reset links, confirmation emails, and other authentication flows * Troubleshoot email templates and content This makes it easy to test different authentication scenarios without configuring a real email service or waiting for actual email delivery. Five seed accounts are pre-configured for local development: 1. [local@tuturuuu.com](mailto:local@tuturuuu.com) 2. [user1@tuturuuu.com](mailto:user1@tuturuuu.com) 3. [user2@tuturuuu.com](mailto:user2@tuturuuu.com) 4. [user3@tuturuuu.com](mailto:user3@tuturuuu.com) 5. [user4@tuturuuu.com](mailto:user4@tuturuuu.com) These accounts are already set up with the necessary data, allowing you to quickly test the app's functionality. However, you can register any new email address and the authentication emails will be captured by InBucket for you to inspect. > [!WARNING] > > OTP send limits include email-scoped cooldowns in the web process. Playwright > tests that only need to assert the OTP stage should use a dedicated throwaway > email address instead of a seed login account. Reserve seed accounts for tests > that must complete authentication. ### Playwright E2E Safety Web E2E runs must use the local Supabase stack. For local debugging and patch iteration, run the web app and Playwright on the native machine so code changes are picked up quickly and server logs stay visible. Avoid `bun test:e2e`, `bun --cwd apps/web test:e2e`, and `bun test:e2e:web:docker` as the first local debugging path. Those commands route through `scripts/run-web-e2e-docker.js`, which writes `tmp/e2e/web.env`, resets local Supabase, builds and runs the production-style Docker web stack, and then runs Playwright. Reserve that Dockerized path for explicit CI-parity checks or production-Docker runtime bugs. For the native workflow, start or reset local Supabase with the database package scripts, start `apps/web` natively with the local E2E environment, and run the focused Playwright spec directly from `apps/web`. Native web server Supabase URLs should resolve to `http://127.0.0.1:8001`; `host.docker.internal` is only for the Dockerized web stack. Do not point E2E at a cloud Supabase project. The Playwright global setup fails fast if `BASE_URL` is not local or if `NEXT_PUBLIC_SUPABASE_URL`, `SUPABASE_SERVER_URL`, or `SUPABASE_URL` resolves outside the local Supabase origins on port `8001`. ## Further Information For more details about Supabase CLI usage, refer to the [Supabase CLI documentation](https://supabase.com/docs/guides/local-development/cli/getting-started). ## Row Level Security (RLS) Row Level Security (RLS) is a powerful Postgres feature that allows you to control access to rows in a database table based on the user making the request. In Tuturuuu, we use RLS extensively to ensure data security. ### Enabling RLS RLS should be enabled on all tables in exposed schemas (like `public`). When creating tables through the Supabase UI, RLS is enabled by default. For tables created using SQL, you need to explicitly enable RLS: ```sql theme={null} alter table . enable row level security; ``` ### Creating RLS Policies Policies define the conditions under which users can access or modify data. Here are some common patterns used in Tuturuuu: #### Organization-based Access In Tuturuuu, workspace-scoped resource tables use `ws_id`, and membership rows use `workspace_members.ws_id` plus the `workspace_member_type` enum. Use `MEMBER` when an example requires a full member rather than a guest. For application-specific mutations, pair membership with the maintained permission helper and confirm the permission name in the generated enum. The canonical [RLS policy reference](/reference/database/rls-policies) explains when a different membership or permission rule is appropriate. ```sql theme={null} -- Allow users to select data from their organizations create policy "Users can view data from their organizations" on public.table_name for select to authenticated using ( exists ( select 1 from public.workspace_members workspace_member where workspace_member.ws_id = table_name.ws_id and workspace_member.user_id = (select auth.uid()) and workspace_member.type = 'MEMBER' ) ); -- Allow members with manage_projects to insert data create policy "Project managers can insert data" on public.table_name for insert to authenticated with check ( exists ( select 1 from public.workspace_members workspace_member where workspace_member.ws_id = table_name.ws_id and workspace_member.user_id = (select auth.uid()) and workspace_member.type = 'MEMBER' ) and public.has_workspace_permission( table_name.ws_id, (select auth.uid()), 'manage_projects' ) ); -- UPDATE needs USING for the existing row and WITH CHECK for the new row create policy "Project managers can update data" on public.table_name for update to authenticated using ( exists ( select 1 from public.workspace_members workspace_member where workspace_member.ws_id = table_name.ws_id and workspace_member.user_id = (select auth.uid()) and workspace_member.type = 'MEMBER' ) and public.has_workspace_permission( table_name.ws_id, (select auth.uid()), 'manage_projects' ) ) with check ( exists ( select 1 from public.workspace_members workspace_member where workspace_member.ws_id = table_name.ws_id and workspace_member.user_id = (select auth.uid()) and workspace_member.type = 'MEMBER' ) and public.has_workspace_permission( table_name.ws_id, (select auth.uid()), 'manage_projects' ) ); -- DELETE uses USING because there is no new row to check create policy "Project managers can delete data" on public.table_name for delete to authenticated using ( exists ( select 1 from public.workspace_members workspace_member where workspace_member.ws_id = table_name.ws_id and workspace_member.user_id = (select auth.uid()) and workspace_member.type = 'MEMBER' ) and public.has_workspace_permission( table_name.ws_id, (select auth.uid()), 'manage_projects' ) ); ``` #### Role-based Access Use `public.has_workspace_permission(p_ws_id, p_user_id, p_permission)` rather than joining retired member-role columns. Permission checks should still be paired with membership when a workspace-wide default permission must not admit non-members. If a policy needs a recursion-free membership lookup, keep the helper in a non-exposed schema, derive identity from the request, and harden its execution boundary before referencing it from a policy: ```sql theme={null} create or replace function private.can_access_workspace(p_ws_id uuid) returns boolean language plpgsql security definer stable set search_path = '' as $$ declare v_caller_id uuid := (select auth.uid()); begin if coalesce((select auth.jwt() ->> 'role'), '') = 'service_role' then return true; end if; if v_caller_id is null then return false; end if; return exists ( select 1 from public.workspace_members workspace_member where workspace_member.ws_id = p_ws_id and workspace_member.user_id = v_caller_id and workspace_member.type = 'MEMBER' ); end; $$; revoke execute on function private.can_access_workspace(uuid) from public, anon, authenticated, service_role; grant execute on function private.can_access_workspace(uuid) to authenticated, service_role; ``` Because `private` is not a Data API schema, this narrowly granted helper cannot be invoked as an exposed RPC. A public Data API RPC needs its own internal workspace permission check; a page or route check is not enough. Revoke `public` and `anon`, grant only the required caller role, and cover the grant matrix and allowed/denied calls with pgTAP. See [Workspace-Scoped Security Definer RPCs](/reference/database/rls-policies#pattern-9-workspace-scoped-security-definer-rpcs). ```sql theme={null} -- Allow full members with manage_projects to access a resource create policy "Project managers can view workspace data" on public.table_name for select to authenticated using ( exists ( select 1 from public.workspace_members workspace_member where workspace_member.ws_id = table_name.ws_id and workspace_member.user_id = (select auth.uid()) and workspace_member.type = 'MEMBER' ) and public.has_workspace_permission( table_name.ws_id, (select auth.uid()), 'manage_projects' ) ); ``` ### Performance Optimization for RLS For better performance in your RLS policies: 1. **Wrap function calls in subqueries**: ```sql theme={null} -- Instead of this using (auth.uid() = user_id); -- Use this using ((select auth.uid()) = user_id); ``` 2. **Use hardened security-definer helpers only when needed**. The `private.can_access_workspace` recipe above shows the required caller, service-role, search-path, and grant controls. Wrap stable helper calls in a scalar subquery so Postgres can evaluate them once per statement: ```sql theme={null} create policy "Users can access workspace data" on public.table_name for select to authenticated using ((select private.can_access_workspace(table_name.ws_id))); ``` 3. **Add explicit filters** in your queries even when you have RLS: ```typescript theme={null} // Even though RLS will filter by workspace_id, adding the filter explicitly improves performance const { data } = await supabase .from('projects') .select() .eq('workspace_id', workspaceId); ``` ### Testing RLS Policies Tuturuuu ships a large suite of database tests in `apps/database/supabase/tests/`. These are [pgTAP](https://pgtap.org/) test files (each one wraps its assertions in a transaction and rolls back), and they run against the local Supabase instance through the Supabase CLI. To run the database test suite locally: 1. Make sure a local Supabase instance is running (`bun sb:start`). 2. Add or edit a `*.sql` test file in `apps/database/supabase/tests/`. 3. Run the tests through the database workspace's Supabase CLI wrapper: ```bash theme={null} # From the repo root bun --cwd apps/database scripts/run-supabase.js test db ``` `run-supabase.js` resolves the pinned local Supabase binary and invokes `supabase test db`, which executes every `tests/*.sql` file against the local database. There is no dedicated `bun sb:test` alias — the test runner is the Supabase CLI itself. Each test file begins by enabling pgTAP, then uses helpers like `plan()`, `is()`, and `throws_ok()` to assert behavior. A trimmed example: ```sql theme={null} begin; create extension if not exists pgtap with schema extensions; set local search_path = public, extensions; select plan(2); -- Impersonate an authenticated user set local role authenticated; select set_config( 'request.jwt.claims', '{"sub":"00000000-0000-0000-0000-000000000001","role":"authenticated"}', true ); -- Should return data the user is allowed to see select isnt_empty( $$ select 1 from public.workspaces where id = '00000000-0000-0000-0000-000000000000' $$, 'User can see a workspace they belong to' ); -- Should be blocked by RLS for an unrelated workspace select is_empty( $$ select 1 from public.workspaces where id = '11111111-1111-1111-1111-111111111111' $$, 'User cannot see a workspace they do not belong to' ); select * from finish(); rollback; ``` > [!NOTE] > > Look through the existing files in `apps/database/supabase/tests/` (for > example `user-profile-rls.sql` and the `private-schema-*` suites) to follow the > established pgTAP patterns for impersonating roles and asserting RLS behavior. ## Database Triggers Triggers in Postgres allow you to automatically execute a function when a specified database event occurs (INSERT, UPDATE, DELETE). In Tuturuuu, we use triggers for various purposes like: * Maintaining audit logs * Syncing data between tables * Enforcing complex business rules ### Creating Triggers Here's how to create a trigger in your Tuturuuu development workflow: 1. First, create a trigger function: ```sql theme={null} create or replace function public.handle_new_user() returns trigger language plpgsql security definer set search_path = '' as $$ declare v_workspace_id uuid := gen_random_uuid(); begin -- Create a personal workspace for the new user insert into public.workspaces (id, name, personal, creator_id) values (v_workspace_id, new.email || '''s workspace', true, new.id); -- Record the creator as a full member without re-querying the workspace insert into public.workspace_members (ws_id, user_id, type) values (v_workspace_id, new.id, 'MEMBER'); return new; end; $$; revoke execute on function public.handle_new_user() from public, anon, authenticated, service_role; grant execute on function public.handle_new_user() to supabase_auth_admin; ``` 2. Then, create the trigger: ```sql theme={null} create trigger on_auth_user_created after insert on auth.users for each row execute function public.handle_new_user(); ``` ### Common Triggers in Tuturuuu #### Audit Logging ```sql theme={null} create or replace function private.audit_log_changes() returns trigger language plpgsql security definer as $$ begin insert into public.audit_logs ( table_name, record_id, action, old_data, new_data, performed_by ) values ( TG_TABLE_NAME, coalesce(new.id, old.id), TG_OP, case when TG_OP = 'DELETE' or TG_OP = 'UPDATE' then row_to_json(old) else null end, case when TG_OP = 'INSERT' or TG_OP = 'UPDATE' then row_to_json(new) else null end, coalesce(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid) ); return coalesce(new, old); end; $$; -- Apply this trigger to a table create trigger projects_audit_trigger after insert or update or delete on public.projects for each row execute function private.audit_log_changes(); ``` #### Automated Timestamps ```sql theme={null} create or replace function public.update_timestamp() returns trigger language plpgsql as $$ begin new.updated_at = now(); return new; end; $$; -- Apply to a table create trigger update_projects_timestamp before update on public.projects for each row execute function public.update_timestamp(); ``` ### Testing Triggers You can test triggers by running SQL commands in the local Supabase instance and verifying the results: ```sql theme={null} -- Insert a test user insert into auth.users (id, email) values ('test-uuid', 'test@example.com'); -- Verify the trigger created a workspace select * from public.workspaces where creator_id = 'test-uuid'; -- Verify the creator is a full member of the workspace select * from public.workspace_members where user_id = 'test-uuid' and type = 'MEMBER'; ``` ## Seeding Your Database Database seeding is the process of populating your database with initial data. In Tuturuuu, we use seeding to: 1. Create test users and workspaces for local development 2. Initialize lookup tables with standard values 3. Ensure a consistent starting point for all developers ### Seed Files Location In Tuturuuu, seed files are stored in `apps/database/supabase/seed.sql`. This file is automatically executed when you run `bun sb:reset` or start a fresh Supabase instance. ### Real Examples from Tuturuuu's Seed File Let's look at some real examples from Tuturuuu's seed.sql file: #### 1. Authentication Users The seed file creates five default test users with pre-set passwords: ```sql theme={null} -- Populate auth users INSERT INTO "auth"."users" ( "instance_id", "id", "aud", "role", "email", "encrypted_password", "email_confirmed_at", /* other fields... */ ) VALUES ( '00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001', 'authenticated', 'authenticated', 'local@tuturuuu.com', crypt('password123', gen_salt('bf')), '2023-02-18 23:31:13.017218+00', /* other values... */ ), /* additional users... */ ``` All seed users have the same password: `password123`, making it easy to log in for testing. #### 2. Workspaces The seed creates several workspaces for testing different scenarios: ```sql theme={null} -- Populate workspaces insert into public.workspaces (id, name, handle, creator_id) values ( '00000000-0000-0000-0000-000000000000', 'Tuturuuu', 'tuturuuu', '00000000-0000-0000-0000-000000000001' ), ( '00000000-0000-0000-0000-000000000001', 'Prototype All', 'prototype-all', null ), /* additional workspaces... */ ``` #### 3. Workspace Members and Roles The seed also sets up relationships between users and workspaces with different roles: ```sql theme={null} -- Populate workspace_members insert into public.workspace_members (user_id, ws_id, role) values ( '00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000000', 'OWNER' ), ( '00000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000000', 'ADMIN' ), ( '00000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000000', 'MEMBER' ), /* additional members... */ ``` #### 4. Workspace Features Configuration The seed file configures workspace features using secrets: ```sql theme={null} -- Populate workspace_secrets insert into public.workspace_secrets (ws_id, name, value) values ( '00000000-0000-0000-0000-000000000000', 'ENABLE_CHAT', 'true' ), ( '00000000-0000-0000-0000-000000000000', 'ENABLE_EDUCATION', 'true' ), /* additional features... */ ``` #### 5. Domain-specific Data The seed includes domain-specific data for different workspace types. For example, user group metric data: ```sql theme={null} -- Populate user group metrics insert into public.user_group_metrics (id, ws_id, name, unit, is_weighted) values ( '00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Nhiệt độ', '°C', true ), ( '00000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Chiều cao', 'cm', false ), /* additional metrics... */ ``` `user_group_metrics.is_weighted = false` keeps a metric visible for entry and category views without counting it toward report totals or averages. Metric categories live in `user_group_metric_categories` and are linked through `user_group_metric_category_links`. When renaming or recreating these metric tables, keep authenticated CRUD grants and service-role grants in the same migration as the table change. The group indicators dashboard reads the renamed tables through protected API/server paths after workspace permission checks, and missing grants can surface as `permission denied for table user_group_metric_categories`. When code uses `createAdminClient()` for these protected reads, name the local client `sbAdmin`. Do not name a service-role client `supabase`; reserve that name for request-scoped or user-scoped clients so route authorization is easy to audit. ### Creating Seed Data Here's how to create and modify seed data: 1. Edit the `apps/database/supabase/seed.sql` file 2. Add SQL statements to insert your data 3. Run `bun sb:reset` to apply the seed data Example seed data format: ```sql theme={null} -- Create a new user group INSERT INTO public.workspace_user_groups (id, name, ws_id) VALUES ('your-uuid-here', 'New Group', 'workspace-uuid-here') ON CONFLICT (id) DO NOTHING; -- Add a new workspace feature INSERT INTO public.workspace_secrets (ws_id, name, value) VALUES ('workspace-uuid-here', 'ENABLE_NEW_FEATURE', 'true') ON CONFLICT (ws_id, name) DO UPDATE SET value = EXCLUDED.value; ``` ### Creating a Custom Seed File Sometimes you might want to create a custom seed file for specific testing scenarios: 1. Create a new SQL file in the `apps/database/supabase` directory 2. Add your custom seed data 3. Run it with the Supabase CLI: ```bash theme={null} bun supabase db reset --db-url=postgresql://postgres:postgres@localhost:54322/postgres psql postgresql://postgres:postgres@localhost:54322/postgres -f apps/database/supabase/my_custom_seed.sql ``` ### Exporting Current Data as Seed You can also export your current database data to use as seed data: ```bash theme={null} # Export only data (not schema) to seed.sql bun supabase db dump --db-url=postgresql://postgres:postgres@localhost:54322/postgres --data-only > apps/database/supabase/new_seed.sql ``` This is helpful when you've set up data manually and want to preserve it for future development environments. ### Recommended Seeding Workflow For Tuturuuu development, we recommend: 1. Start with a fresh database: `bun sb:reset` 2. Make changes through the UI or your app 3. When you're satisfied, export the data: `bun supabase db dump --data-only > apps/database/supabase/new_seed.sql` 4. Edit the generated SQL to keep only what you need 5. Update the main `seed.sql` file with your changes 6. Test by running `bun sb:reset` again ## AI Integration with Vercel AI SDK Tuturuuu uses Vercel's AI SDK for its AI features, utilizing structured data generation capabilities that integrate with Supabase. This section covers how to work with AI features in the development workflow. ### Overview of AI SDK in Tuturuuu The AI SDK standardizes integrating various AI models across supported providers into Tuturuuu applications. It enables structured data generation, tool calling, and streaming responses to create rich AI-powered features. The main libraries used are: * `ai` - Core Vercel AI SDK package * `@ai-sdk/google` - Provider-specific integration for Google models * `@tuturuuu/supabase` - Supabase client with Tuturuuu-specific utilities ### Generating Structured Data Tuturuuu uses the AI SDK's structured data generation capabilities to create typed responses from AI models. This approach ensures type safety and consistent data structures for features like: * Flashcards generation * Quiz generation * Learning plans * Task management #### Example: Flashcard Generation The structured data pattern used in Tuturuuu follows this workflow: 1. Define a schema using Zod 2. Connect to Supabase for authentication and workspace validation 3. Generate structured data using the AI SDK 4. Stream the response to the client Here's an example from Tuturuuu's codebase: ```typescript theme={null} // 1. Define the schema import { z } from 'zod'; export const flashcardSchema = z.object({ flashcards: z.array( z.object({ front: z.string().describe('Question. Do not use emojis or links.'), back: z.string().describe('Answer. Do not use emojis or links.'), }) ), }); // 2. Setup API endpoint export async function POST(req: Request) { const sbAdmin = await createAdminClient(); const { wsId, context } = await req.json(); // Validate user and workspace permissions const { data: { user }, } = await supabase.auth.getUser(); if (!user) return new Response('Unauthorized', { status: 401 }); // Check workspace feature flag const { count, error } = await sbAdmin .from('workspace_secrets') .select('*', { count: 'exact', head: true }) .eq('ws_id', wsId) .eq('name', 'ENABLE_CHAT') .eq('value', 'true'); if (error) return new Response(error.message, { status: 500 }); if (count === 0) return new Response('You are not allowed to use this feature.', { status: 401, }); // 3. Generate structured data using AI SDK const result = streamObject({ model: google('gemini-2.0-flash-001', { safetySettings: [ // Safety settings configuration... ], }), prompt: `Generate 10 flashcards with the following context: ${context}`, schema: flashcardSchema, }); // 4. Stream the response to the client return result.toTextStreamResponse(); } ``` ### Available Models Tuturuuu supports multiple AI models through Vercel AI SDK. You can define which models are available in your application by updating the `models.ts` file in the `packages/ai` directory: ```typescript theme={null} export const models = [ { value: 'gemini-2.0-flash-001', label: 'gemini-2.0-flash', provider: 'Google', description: 'Gemini 2.0 Flash delivers next-gen features...', context: 1000000, }, // Add other models here... ]; export const defaultModel = models.find( (model) => model.value === 'gemini-2.0-flash-001' && model.provider === 'Google Vertex' ); ``` ### Creating Custom Schema Types To create new structured data types for AI generation, add your schema definition to the `packages/ai/src/object/types.ts` file: ```typescript theme={null} export const myNewSchema = z.object({ items: z.array( z.object({ name: z.string().describe('Name of the item'), description: z.string().describe('Description of the item'), priority: z.enum(['high', 'medium', 'low']).describe('Priority level'), }) ), }); ``` ### Integration with Supabase Tuturuuu's AI features leverage Supabase for: 1. **Authentication** - Validating users before making AI requests 2. **Authorization** - Checking workspace permissions via `workspace_secrets` 3. **Feature Flags** - Using `workspace_secrets` to enable/disable AI features per workspace 4. **Storage** - Storing AI-generated content for later use To enable AI features for a workspace, ensure the appropriate flags are set in the `workspace_secrets` table: ```sql theme={null} -- Enable AI chat features for a workspace INSERT INTO public.workspace_secrets (ws_id, name, value) VALUES ('your-workspace-id', 'ENABLE_CHAT', 'true'); -- Enable AI document features for a workspace INSERT INTO public.workspace_secrets (ws_id, name, value) VALUES ('your-workspace-id', 'ENABLE_DOCS', 'true'); ``` ### Testing AI Features Locally When testing AI features in your local environment: 1. Ensure you have the required API keys set in your `.env.local` file: ``` GOOGLE_GENERATIVE_AI_API_KEY=your-api-key ``` 2. Verify the workspace has the necessary feature flags enabled in your local database ```sql theme={null} SELECT * FROM workspace_secrets WHERE ws_id = 'your-workspace-id' AND name = 'ENABLE_CHAT'; ``` 3. Use the AI-enabled accounts from the seed data (`local@tuturuuu.com`) as they often have additional permissions ### Error Handling When integrating AI features, implement proper error handling to account for: 1. Missing API keys 2. Model unavailability 3. Invalid user input 4. Exceeded token limits Example error handling pattern used in Tuturuuu: ```typescript theme={null} try { // AI SDK code here } catch (error) { console.log(error); return NextResponse.json( { message: `## Edge API Failure\nCould not complete the request. Please view the **Stack trace** below.\n\`\`\`bash\n${(error as Error)?.stack || 'No stack trace available'}`, }, { status: 200, } ); } ``` CI pulls Supabase images anonymously from `public.ecr.aws` through `SUPABASE_INTERNAL_IMAGE_REGISTRY`; no registry login or package-read permission is needed. Seven attempts allow at most 487.5 seconds of backoff, excluding command runtime. Throttling (`toomanyrequests`, `Rate exceeded` or HTTP 429) and classified transient pull transport errors retry; SQL never replays. The baseline fingerprint includes the registry, so a registry change invalidates existing snapshots once. # Machine Storage Health Source: https://docs.tuturuuu.com/build/development-tools/machine-storage-health Measure, triage and safely reclaim disk space on Macs, Linux devboxes and self-hosted runners with ttr storage. Every Tuturuuu machine (the M4 Mac, Linux devboxes and VMs, self-hosted CI runners) uses one tool for storage triage: `ttr storage`. It reports headroom, ranks consumers by the bytes deleting them would actually free, suggests exact cleanup commands, and applies only safe, bounded cleanup on request. ## Daily triage ```bash theme={null} ttr storage status # free/used/pressure and inodes per volume (exit 2 when critical) ttr storage analyze # ranked consumers by category (add --json) ttr storage suggest # actions: reclaimable bytes, risk, exact command ttr storage prune # dry run of the default safe categories ttr storage prune --apply # re-verifies every fence, then applies the plan ttr storage reclaim # automatic-reclaim plan, lease holder and last run on this host ttr storage fleet # disk headroom reported by every devbox runner ``` `analyze` and `suggest` walk large trees (Bun cache, stale `node_modules`, every worktree) and take a few minutes on a busy Mac; run them through `ttr resources run --` when other lanes are validating. `suggest` caches its top actions in `~/.tuturuuu/storage/last-suggest.json`. ## Measure real bytes, not apparent bytes | Platform | Method | Why | | - | - | - | | macOS (APFS) | `getattrlist` `ATTR_CMNEXT_PRIVATESIZE` per file, through the system Python | Bun's `node_modules` are APFS clones of `~/.bun/install/cache`; `du` counts shared blocks once per clone. | | Linux | Allocated blocks (`st_blocks`), hard links counted once per run | Bun's Linux backend hard-links from its cache. | Never use `du -L`: it follows Bun's workspace symlinks and reported 300 GiB for `apps/web` alone. A fresh worktree costs about 0.3 GiB unique (source only) and about 250k inodes, not the 4–9 GiB `du` shows. `bun worktree:footprint` reports the per-worktree breakdown. ## Thresholds | Pressure | Rule (any one) | | - | - | | `critical` | free \< `TTR_STORAGE_MIN_FREE` (15 GiB), free \< `TTR_STORAGE_MIN_FREE_PERCENT` (5%), or free inodes \< `TTR_STORAGE_MIN_FREE_INODE_PERCENT` (5%) | | `low` | within twice any threshold | | `ok` | otherwise | Free space is what an unprivileged user can allocate (`statfs` `bavail`). On APFS every volume shares its container's free space and inode tables grow on demand, so the inode percentage only matters on ext4/xfs. Invalid threshold values fall back to the default with a warning. Heavy admission uses hysteresis on top of these thresholds. A volume enters `low` below the `critical` thresholds above and leaves it only at the separate recover thresholds: * `TTR_STORAGE_RECOVER_FREE` * `TTR_STORAGE_RECOVER_FREE_PERCENT` * `TTR_STORAGE_RECOVER_FREE_INODE_PERCENT` Each defaults to twice its low threshold (30 GiB / 10% / 10%). A recover value below its low value is raised to the low value. These are configurable starting points, not measured fleet values; tune them per host. ## Disk-aware admission `ttr resources` checks the volume holding a heavy command's working directory before queueing it. Once queued, the lane queue re-checks free disk and inodes at dequeue (`TTR_STORAGE_MIN_FREE`, `TTR_STORAGE_MIN_FREE_PERCENT`, and `TTR_STORAGE_MIN_FREE_INODE_PERCENT`) and holds the job, failing closed, until headroom returns. * **`low`:** while the volume is in the hysteresis `low` state, automatic reclaim runs first (see below). If that does not bring the volume back to the recover thresholds, the command is refused with exit code 75 and a blocker report. The report gives: * what reclaim removed and its receipt file; * the eligible items it skipped, with their owners and reasons; * the largest non-high manual actions from the last `ttr storage suggest`, with their hold owners; * the exact free space needed to resume. * **`ok`:** when the volume is `ok` but below the recover thresholds, the command warns and continues. * **Alerts:** warn, low and recovered alerts print once per storage authority (this OS user on this host), volume and level. The state lives in `~/.tuturuuu/storage/pressure-state.json`. * **Probe failure:** a failed probe is UNKNOWN. It never triggers reclaim and the command is refused with the probe error; only the override below admits it. * `--ignore-disk` (or `TTR_RESOURCES_IGNORE_DISK=1`) overrides both checks once. Prefer reclaiming space. * `--hosts` dispatch skips the queue-time refusal; it applies its own local disk gate per placement, including inode pressure. Both placement and dequeue probe the command directory selected by `--cwd`, `--dir` or `-C`. * With `--offload auto`, a critical local disk counts as saturated, so the exact pushed commit runs on a devbox runner when one is eligible. * Commands nested inside an admitted job are not checked again. ## Fleet headroom Each devbox agent adds a bucketed disk report to its existing heartbeat: `resources.disk = { freeGiB, freePct, inodeFreePct? }`. Values are conservative lower bounds (GiB steps 0, 1, 2, 5, 10, 15, 20, 30, 50, …; percentages in 5% steps), about 35 bytes against the 4 KiB heartbeat cap. The probed path is the agent's working directory, or `TUTURUUU_DEVBOX_STORAGE_PATH`. The web route (zod `.strict()`) and the devbox-control Worker (`typeof` checks, no coercion) both reject malformed reports. ```bash theme={null} ttr storage fleet # every registered runner, critical hosts first (exit 2 if any) ttr resources offload # blockedBy=low_disk when every trusted runner is critical ``` Offload selection skips a trusted runner that reports critical disk. Runners that do not report disk yet (older agents) stay eligible and show as `unreported`. No new timer or controller is involved: the heartbeat is the transport. The authenticated heartbeat of a registered runner is the only fleet input. `ttr storage fleet` reports a runner as `unknown` when any of these hold: * it is offline; * its heartbeat is disabled; * it has never reported; * its last heartbeat is older than the 60-second freshness window. `unknown` is never healthy, and `fleet` exits 2 when any runner is `unknown`. Nothing connects to a host to measure or clean it; a host's own heartbeat or admission triggers its own reclaim. ## Automatic reclaim Automatic reclaim is **off by default**. It runs only after an explicit opt-in for this storage authority: `TTR_STORAGE_AUTO_RECLAIM=1` (or `true`, `on`, `yes`), or the sentinel file `~/.tuturuuu/storage/auto-reclaim` containing one of those values. Once opted in, when a trigger finds a volume `low`, reclaim removes explicitly regenerable space on its own. A human can always run `ttr storage reclaim --apply` without opting in. There are two triggers, and neither is a schedule: * heavy `ttr resources` admission; * a devbox agent heartbeat, which starts one detached `ttr storage reclaim --apply --auto --trigger heartbeat`. This follows the shared [cross-orchestrator quota and storage coordination](#cross-orchestrator-contract) contract. **Scope: one storage authority.** Every guarantee below holds for one storage authority: one OS user's storage home (`TTR_STORAGE_HOME`, default `~/.tuturuuu/storage`) on one host. Every orchestrator and project that user runs (Tuturuuu, Upskii, Mastermind) shares it. Another OS user on the same host has a separate home, lease and receipts; nothing here coordinates across OS users, so this is not a host-wide guarantee. * **One lease per storage authority.** The lease is `~/.tuturuuu/storage/reclaim//cleanup.lease`, keyed by host identity (`TTR_STORAGE_HOST_ID`, default the hostname) inside the user's home. It records: * a monotonic fence number (from a durable counter, `fence`); * TTL (15 minutes, renewed before each item); * owner pid and start time; * trigger; * the caller's `TTR_ORCHESTRATOR_ID`. Acquisition, takeover, renewal and release all run under an O\_EXCL mutation lock, so they are serialized. A second caller does not reclaim in parallel; its admission refusal names the holder. An expired or dead-pid lease is taken over with a larger fence. Holders record their process start time, so a reused pid counts as a dead holder. Breaking a dead holder's mutation lock is itself serialized behind a separate O\_EXCL break lock, and the lock is re-read under it before removal, so a fresh lock is never broken. * **Fenced at the destructive boundary.** Each removal runs inside the lease's guard: under the mutation lock, the holder's token, fence and expiry are re-validated, and the removal runs while the lock is still held. A holder that paused past its TTL and was taken over never removes anything, and no takeover can overlap a removal. * **Eligible items only.** Only path-bounded rules, removed as one verified directory tree, so the deleted set is exactly the checked set: * idle Xcode DerivedData folders, never while Xcode or `xcodebuild` runs; * ignored, untracked build artifacts inside merged, clean, unheld worktrees idle at least a day. Command-based cleanups (Homebrew, npm cache, Docker, simulators) are never automatic; they stay manual suggestions. `TTR_STORAGE_AUTO_RECLAIM_RULES` narrows the list (comma-separated rule ids). The idle age is `TTR_STORAGE_RECLAIM_OLDER_THAN_DAYS` (7), measured over the whole tree. Only candidates on the low volume's device count, checked when planning and again right before each item. A volume reading that is not usable (no device, non-positive total, non-finite or inconsistent values) stops the run with nothing removed. * **Re-checked right before each item, fail closed.** In this order: 1. the directory identity (device and inode chain from the rule root, no symlinks, same device) is captured; 2. holds from every file in every holds directory (an invalid line stops); 3. the rule's own re-check (for worktree artifacts, a fresh merged-clean classification at the same `HEAD`); 4. a process `cwd` scan covering every live process of this user; 5. `lsof +D` open files. A check that fails or errors skips the item. Hidden processes of other users (including root) are tolerated only with [private-tree evidence](#process-visibility-on-macos-private-tree-evidence) for the item, proven at or above its rule root. Receipts record the evidence level (`full-visibility` or `private-tree`). * **Durable pre-receipt.** Before acting, an `intent` line is appended and fsynced (new directories and the new file's directory entry are fsynced too) to `~/.tuturuuu/storage/reclaim//receipts/*.jsonl` (mode 0600). If the append or any fsync fails, the item is not removed. After acting, an `outcome` line records the result. Each line records path and bytes, rule, the checks that passed, the lease fence, outcome and freed bytes, host, orchestrator, trigger and time. * **Bounded by the goal.** Free space is re-measured under the lease before each item, and reclaim stops at the recover thresholds. If space recovered meanwhile, nothing is removed. * **Not enough safe reclaim.** Heavy admission stays closed and prints the blocker report. Reclaim never widens its scope to close the gap. After an unsuccessful run, triggers wait `TTR_STORAGE_RECLAIM_COOLDOWN_MINUTES` (30) before planning again. `ttr storage reclaim --apply` ignores only the cooldown. * **Hard kill switch.** A false value (`0`, `false`, `off`, `no`) in `TTR_STORAGE_AUTO_RECLAIM` (process or passed environment) or in the `auto-reclaim` sentinel file disables both triggers and the manual `ttr storage reclaim --apply`, and wins over any opt-in. A sentinel with any other content, or one that cannot be read, also kills. The engine checks it itself before the lease, before every item and inside the lease guard right before each removal, so no caller or flag bypasses it. * **Trusted re-exec.** The heartbeat starts the child only through a verified absolute path: the absolute runtime plus the real path of an existing script entry, or a packaged executable named `ttr`. It never resolves `ttr` through `PATH`; otherwise it spawns nothing. Set `TTR_ORCHESTRATOR_ID` in each orchestrator's environment so leases and receipts name it. Without it they record an unidentified caller. T3 does not inject a thread id into child processes today. ### What stays manual Automatic reclaim never touches any of these: * worktrees themselves; * Homebrew, npm, Bun and pub caches, Docker and simulators; * `node_modules`; * tmp, evidence and scratch; * held paths; * databases and Supabase or Docker volumes; * Docker images; * VM disks, Trash, archives and AVDs; * production assets; * files another process has open. A human owns those decisions: run `ttr storage suggest` and `ttr storage prune`, and ask hold owners to release their holds. The blocker report names the largest such items and their owners. ### Activation These hooks ship in the `ttr` CLI. Until a host runs a released CLI that contains them, they are not active there. A heartbeat trigger also needs that host's devbox agent to be upgraded. Record per-host activation evidence (version, a receipt or a refusal) before claiming coverage. ## Categories | Category | What | Risk | `prune` | | - | - | - | - | | `worktrees` | Linked worktrees, classified `dirty`, `unmerged`, `merged-clean`, `orphaned`, with owner and hold status | safe for merged-clean | named only | | `node-modules-stale` | Bun stores with far more `.bun` entries than `bun.lock` packages (diverged private clones) | moderate | named, one `--path` | | `build-artifacts` | `.turbo`, `.next`, `.open-next`, `dist`, `build`, `.dart_tool` in checkouts | safe | default, via `bun worktree:prune-artifacts` | | `rust-target` | `apps/backend/target` | safe | default, via `bun rust-cache prune` | | `bun-cache`, `npm-cache`, `pub-cache` | Package manager caches. The npm cache root is resolved once (`npm_config_cache`, then `npm config get cache`, then `~/.npm`); `prune` measures that root's `_cacache` and runs `npm cache clean --force --cache ` only on a directory owned by you | moderate / safe | named only | | `xcode-derived-data` | DerivedData project folders idle past `--older-than` | safe | default | | `xcode-simulators` | Simulators for uninstalled runtimes, each deleted by its own UDID (`xcrun simctl delete `, never `delete unavailable`) | safe | default | | `homebrew-cache` | `brew cleanup --prune=` (also removes old formula versions) | safe | named only | | `docker` | Build cache older than the age bound; unused images. Verified local daemon only | safe / moderate | named only | | `supabase-volumes` | Supabase CLI local volumes | high | only `--name `, never attached | | `tmp-old` | Identified tool scratch dirs (mktemp, `tmp-*`, Vitest, Turbo, Bun, `ttr-*` and similar) directly under a tmp root, idle past the age bound, at least 25 MiB, with no git checkout, linked worktree or bare repository inside (checked again by the deleting walk itself, which refuses the whole removal if one appears) | moderate | named only | | `xcode-device-support`, `gradle`, `playwright` | Rebuildable but costly | moderate | manual command only | | `trash`, `xcode-archives`, `android-sdk` AVDs, `vm-disks` | User data, signed builds, emulator state, VM images | high | never | Defaults: `--older-than 7` days, `--max-remove 20G` total per run. ## What is never removed automatically * Dirty, unmerged, locked, busy (a process `cwd` inside) or current worktrees, and the primary checkout. * Worktrees active in the last 24 hours. * Any path covered by a hold file. Every file in `~/.t3/cleanup/holds.d` counts, except dotfiles and editor swap or backup files (`~`, `.swp`), which are skipped with a warning. That includes each project's file (`tuturuuu.txt`, `upskii.txt`, `mastermind.txt`, …) whatever its extension. `--holds-dir` and `TTR_STORAGE_HOLDS_DIRS` add directories; the shared default is always read. Each file holds one absolute path or glob per line, with `#` comments. A hold covers its path, everything below it and everything above it. An unreadable holds directory, or any non-comment line that is not an absolute path or glob, stops the run with the file and line; fix or comment out the line. * Any path a process may be using. Right before each removal, every process's working directory (`lsof -d cwd`, checked against the full `ps` table) and every open file under the path (`lsof +D`) are re-checked. A failed scan, any `lsof` diagnostic, or an uninspectable live process of this user means use is not proven absent, and the step is skipped. Uninspectable processes of other users (including root) are tolerated only under the private-tree rule below. `ps` and `lsof` run by absolute path and scripts by the absolute runtime, so a shim directory on `PATH` cannot answer for them. * Worktrees whose ignored files are not rebuildable. `git worktree remove` deletes ignored files without `--force`, so a worktree is kept when `git ls-files -o -i --exclude-standard --directory` lists anything outside build artifacts, `node_modules` and `target` (for example `.env.local` or `tmp/agent-coordination/`); the dry run names those paths. * Trees with entries this user does not own, or with a group- or world-writable directory (`mode & 0o022`), checked in the pre-scan and again before every unlink. Running as root removes only fully root-owned trees. * Anything reached through a symlink or across a mount. Trees are removed by directory identity (device and inode) captured before the checks and re-verified before every unlink; a swapped ancestor or a nested mount stops the removal, and entries created after the pre-scan are left in place. * Docker on a remote or unknown engine. Docker steps run only when `DOCKER_HOST` is unset and the current context's endpoint is an existing local unix socket; the command is pinned to that socket with `-H`. * `preserve/*` branches and every other ref. Merged-clean worktrees are removed with plain `git worktree remove` (no `--force`) and their branch with `git branch -d`; git keeps a squash-merged branch, which is reported for a human to delete after checking. * Repositories or linked worktrees nested inside a reclaim candidate, including ignored output and bare repositories. Planning rejects the candidate; the deletion walk rechecks repository markers before every unlink and rmdir. * Source, Trash, VM disk images, Xcode archives, AVDs and Supabase volumes not named explicitly. "Merged" means the exact local `HEAD` reached the base: either it is an ancestor of `origin/main`, or a merged pull request's head is that `HEAD` (squash merges). An open PR, a closed-unmerged PR or local commits after the merged head keep the worktree. Every apply step re-reads holds, process working directories, open files and worktree state immediately before acting. Idle ages use the newest modification or change time anywhere in a tree (a deep, recently written file keeps the whole tree); trees over 500,000 entries or unreadable ones count as recent. ## Process visibility on macOS: private-tree evidence Decision (orchestrator, 2026-10-23): a non-root user's `lsof` cannot inspect other users' or root's processes, so strict visibility would skip every removal on macOS. Removal (manual `prune` and automatic reclaim alike) may proceed with `private-tree` evidence only when all of these hold: 1. Every live process of the effective user was inspected; only processes of other uids may be hidden. 2. The item has a private-tree proof: some directory at or above the rule root is owned by the effective user, has `mode & 0o077 == 0` and no ACL; every directory from there down to the item is owned by the effective user and not group- or other-writable; all on one device. 3. The open-file scan is scoped to the effective user (`lsof -a -u `). 4. Never when running as root, and only on macOS. Linux stays fail-closed until a container-mount check exists. Receipts and step checks record the evidence level (`private-tree` or `full-visibility`). Root's processes remain uninspectable under this rule; that residual is accepted only for proven-private trees. Idle Xcode DerivedData usually qualifies through `~/Library/Developer` (0700, no ACL). Its repository-refusal exemption applies only to the literal `~/Library/Developer/Xcode/DerivedData` root when its canonical path matches and every component through the user's home is a real directory owned by the effective user. Symlinked roots or ancestors and nonstandard report roots are skipped, and the chain is checked again immediately before removal. This keeps checkouts reached through a redirected DerivedData path intact; see `packages/sdk/src/cli/storage-derived-data.test.ts`. Worktrees and their artifacts under `~/.t3` do not qualify unless that tree is 0700; ttr never changes permissions to make something qualify. Regression coverage: `packages/sdk/src/cli/storage-privacy.test.ts` (scan rules per platform and uid, the proof's failure modes, scoped `lsof`, manual prune wiring), `storage-liveness.test.ts` (strict scan parsing) and `storage-reclaim.test.ts` (automatic reclaim records `private-tree` evidence in its receipts). ## Stale `node_modules` `bun install` never removes old `node_modules/.bun/` entries. On APFS the old entries become private copies once the cache is refreshed; the main checkout held about 10 GiB this way. The fix is a reinstall, queued through `ttr resources`: ```bash theme={null} ttr storage prune --category node-modules-stale --path # dry run ttr storage prune --category node-modules-stale --path --apply ``` This removes `node_modules` and runs `ttr resources run -- bun install --frozen-lockfile`. It refuses while another process works inside the checkout. A shared primary checkout needs its owner's approval first. ## Per-OS commands ### macOS (APFS) ```bash theme={null} diskutil apfs list | grep -E 'Capacity (In Use|Not Allocated)' df -h /System/Volumes/Data # the Data volume holds user files xcrun simctl list devices booted xcrun simctl shutdown all # a booted simulator mounts ~220 GB of phantom DeviceFS size tmutil listlocalsnapshots / # local snapshots pin deleted blocks ``` ### Linux devbox or runner ```bash theme={null} df -h / /var/lib/docker df -i / # inodes; ext4 tables are fixed at mkfs docker system df journalctl --disk-usage ``` Self-hosted runner jobs remove their own `start-source--*` checkout in an `if: always()` step; sweep leftovers only from completed runs. ## Ownership Run `ttr storage prune --apply` only on a machine you own. Devbox VMs and CI runner hosts belong to their host owner; report suggestions to that owner instead. On the M4 Mac the Mastermind daily cleanup task uses the same hold files and busy/PR-state rules, so a hold written for one tool protects the path from both. Automatic reclaim acts only for the storage authority (OS user and host) that triggered it, under that authority's lease. ### Cross-orchestrator contract Every orchestrator (Tuturuuu, Upskii, Mastermind) follows the shared contract in `~/.codex/skills/pstack-t3-workflow/references/t3-orchestration.md#cross-orchestrator-quota-and-storage-coordination`. Under that contract: * missing telemetry stays UNKNOWN; * one cleanup lease owns each host's reclaim (implemented here per storage authority, one OS user's home; a host-wide authority shared across OS users is not implemented yet); * automatic reclaim is limited to eligible regenerable caches and completed, unowned outputs, with receipts; * insufficient reclaim pauses heavy admission or offloads it rather than deleting more. The Tuturuuu `ttr storage` lane is the single implementation owner of these hooks; other projects supply requirements and receipts. # Mobile design language Source: https://docs.tuturuuu.com/build/development-tools/mobile-design-language How redesigned mobile mini apps look and behave: type, surfaces, colour roles, motion, page anatomy, patterns, responsive rules and per-app identity, built on the shared composition kit. This page is the visual and interaction contract for the mobile app redesign. The [design system](/build/development-tools/mobile-design-system) owns the layout primitives, tokens, components and overlays; this page says how to compose them so every mini app feels like one product without looking like a copy of the next one. The language grew out of the Settings redesign: translucent grouped cards over the appearance theme, soft tinted icon tiles, generous rounding and muted captions. Home and Notifications extended it with tones, icon tiles, grouped rows and shared states. Mini apps now adopt the same vocabulary through the composition kit in `package:mobile/core/ui/kit/kit.dart`. ## Principles 1. **Lead with the answer.** A root page opens with what matters now (a balance, the next meeting, today's tasks), not with a toolbar or a list header. 2. **One surface family.** Content sits on grouped cards, panels and stat tiles that share radii, borders and translucency, so appearance themes (solid, gradient, blobs) show through consistently. 3. **Theme roles only.** Every colour comes from `AppColorTokens` through an `AppTone`. No hex values, `Colors.*` hues or per-feature palettes. 4. **Cheap by default.** Static fills and borders, no blur behind scrolling content, no shadows on list items, no layers for opacity. 5. **Adaptive, not stretched.** Wider bodies get more columns, side panels and two panes, never a phone layout scaled up. 6. **Accessible without exceptions.** 48dp targets, readable contrast in every theme, unclamped text scaling and complete semantics. ## Typography Use `AppTokens.of(context).typography` (`context.tokens.typography`). Styles carry no colour; apply a role from the colour tokens. | Role | Style | Use | | - | - | - | | Hero figure | `x3Large` w700, tabular figures, -0.4 tracking | `AppSummaryHero` headline | | Stat value | `x2Large` w700, tabular figures | `AppStatTile` value | | Section title | `large` w600 | `AppSectionHeader` | | Panel and row title | `base` w600 | `AppPanel`, `AppRichRow` | | Grouped row title | `base` w500 | `AppGroupedRow` | | Body and supporting | `small`, `mutedForeground`, 1.35–1.4 height | subtitles, messages | | Meta and captions | `xSmall` w500, `mutedForeground` | `AppMetaItem`, trends | | Grouped caption | `labelLarge` w600 | `AppSectionCaption` | * Numbers that update or line up (amounts, counts, times) use `FontFeature.tabularFigures()`. * Titles wrap to two lines before truncating. Never clamp `MediaQuery.textScaler`; components grow vertically instead. * The shell's top navbar owns the page title. Do not repeat it as a heading in the body; the hero or the first section header introduces the content. ## Spacing, radius and elevation Spacing uses the 4pt `AppSpacing` scale and the page tokens in `AppLayout`. Composition rules on top of those tokens: | Between | Gap | | - | - | | Hero and first section | `AppLayout.sectionGap` (24) | | Sections | `AppLayout.sectionGap` (24) | | Section header and its content | 12 (built into `AppSectionHeader`) | | Tiles in a grid, stacked cards | `AppLayout.itemGap` (12) | | Rows inside a card | hairline divider, no gap | | Surface | Radius | Elevation | | - | - | - | | Grouped card, panel, hero | `AppRadius.card` (22) | none | | Stat tile, grid tile | 20 | none | | Filter pill, badge | pill | none | | Sheet | `AppRadius.sheet` (24) | overlay shadow from the host | | Dock | `AppRadius.dock` (26) | owned by the shell | Elevation is reserved for floating layers (dock, sheets, dialogs, toasts). Cards in lists separate with a border and the surface tint, never with a shadow. Legacy `FinancePanel` adds a 28px blurred shadow to every card; new code uses `AppPanel` instead. ## Surfaces | Surface | Fill | Border | When | | - | - | - | - | | Page | appearance background (solid, gradient or blobs) | none | owned by the shell | | Grouped card (`AppGroupedCard`) | `surface` at 72% | `outlineVariant` 40% | lists of rows | | Panel (`AppPanel`) | `card` at 86% | `border` 70% | charts, breakdowns, short lists, inline forms | | Stat tile (`AppStatTile`) | `card` at 86% | `border` 70% | key figures | | Hero (`AppSummaryHero`) | tone fill blended into `card`, linear gradient | tone at 18% | the opening summary | All of them become solid when the platform requests high contrast (`MediaQuery.highContrastOf`). ## Colour roles Colours come from `AppColorTokens`, which the appearance theme resolves for every theme, accent and brightness. Components choose an `AppTone`, and the tone resolves a foreground and a quiet fill: | Tone | Foreground role | Fill | Meaning | | - | - | - | - | | `neutral` | `mutedForeground` | `muted` | default, inactive | | `primary` | `primary` (follows the accent) | primary at 12% (20% dark) | the app's main object | | `info` | `info` | info tint | time, schedule, links | | `success` | `success` | success tint | money in, done, healthy | | `warning` | `warning` | warning tint | due soon, attention | | `destructive` | `destructive` | destructive tint | overdue, failed, money out over budget | * Text that carries meaning (values, titles) stays in `foreground`. Tones colour icons, meters, trend labels and small accents, which keeps every theme above WCAG AA for text. * Tone tints are fills behind icons, not backgrounds behind body text. * `primary` follows the user's accent. Do not assume it is blue; in Classic it is near-black in light mode and near-white in dark mode. * Per-module hex palettes (the Apps hub cards, Drive's green, finance positive/negative literals) are migration debt; map them to tones. ## Iconography * Use the generated Lucide icons (`LucideIcons`, `LucideLabIcons`, `AppIcons`) so mobile matches the web glyph for glyph. Material `Icons.*` are legacy. * Icons sit in an `AppIconTile` when they identify an object (a row, a stat, an eyebrow) and stand alone when they decorate metadata (`AppMetaItem`, 14dp) or controls (16–20dp). * Row tiles are 32dp in grouped rows and 40dp in rich rows. Stat and hero eyebrow tiles are 28dp. * Icons never carry meaning alone. Pair them with a label or a semantics label, and give icon buttons a tooltip. ## Motion Motion explains a change; it never decorates a resting screen. | Use | Duration | Curve | | - | - | - | | Press, toggle, selection | `AppMotion.fast` (120ms) | `standard` | | Expand, swap content, sheet content | `AppMotion.normal` (220ms) | `standard` | | Page-level or large layout change | `AppMotion.slow` (320ms) | `standard` | **Cheap-animation policy.** These rules keep the kit fast on mid-range Android devices; the Android jank lane may add a user-facing "reduce effects" switch on top of them. * Animate transforms and colours of small subtrees. Do not animate `Opacity` over large subtrees; use `FadeTransition` or an animated colour. * No live `BackdropFilter` behind scrolling content. Frosted chrome goes through the shell's glass policy, which draws near-opaque fills on Android. * No `ShaderMask`, `ColorFiltered` or `Opacity` wrappers that force a `saveLayer` in lists. Fade edges with a painted gradient or not at all. * No shadows on list items or tiles. `AppElevation.raised` is for single floating cards only. * Wrap independently repainting regions (a ticking timer, a live meter, a chart) in a `RepaintBoundary`. `SliverAppAdaptiveGrid` adds one per tile. * Skeletons are static shapes. Do not add shimmer. * Respect `MediaQuery.disableAnimationsOf`: skip to the end state. ## Page anatomy A redesigned mini-app root page reads top to bottom: 1. **Shell header.** Title and actions from the shell. No body AppBar. 2. **Summary hero** (`AppSummaryHero`) or a **stat row** (`AppAdaptiveGrid` of `AppStatTile`, `equalHeight: true`). Pick one per page, not both stacked at the same weight. 3. **Filter bar** (`AppFilterBar`) when the list has 2–6 views. Search is the shell search action. More than six filters belong in a sheet. 4. **Sections.** `AppSectionHeader` plus content: grouped rows, rich rows, panels or a grid. 5. **End clearance.** Handled by the page primitives. Detail pages drop the hero, open with the object's title block and use `AppPanel` with `AppKeyValueRow`s for properties. Forms use `AppGroupedSection`s and open with `showAppSheet` or as a full-screen route above the shell. ## Patterns ### Section headers `AppSectionHeader(title:, count:, subtitle:, actionLabel:, onAction:)` heads content sections (lists, grids, panels). The quieter `AppSectionCaption` (via `AppGroupedSection.caption`) heads settings-style groups. Use one style per page level; never stack a caption under a section header. ### Lists and rows | Need | Component | | - | - | | Settings, navigation, simple items | `AppGroupedRow` in an `AppGroupedCard` | | Entities with metadata (transactions, files, customers, products, courses) | `AppRichRow` with `AppMetaItem`s | | Long homogeneous collections | `AppPageListView.builder` of rich rows wrapped in `AppGroupedSlice`s; group by date or status with section headers | * Rich rows put a value or a status `AppBadge` on the trailing edge and a unit or time in `trailingCaption`. * `selected: true` marks the open item when a list drives a detail pane. * Show at most three `AppMetaItem`s. Overdue or failing facts take the `destructive` role colour, everything else stays muted. * Group long lists by meaning (Today / Yesterday / Earlier, Overdue / Next / Later) instead of one undifferentiated column. ### Cards and grids * `AppAdaptiveGrid(minTileWidth:, maxColumns:)` lays out summaries and small collections; `SliverAppAdaptiveGrid` lays out long collections lazily with a fixed tile height scaled by text size. * Default stat tiles use `minTileWidth: 160` (two per phone, four on a tablet). Catalogues and drives use 120–180 depending on thumbnail size. * `AppPanel(title:, subtitle:, actionLabel:)` frames a chart, breakdown or short list. Set `padded: false` when the child brings row insets. ### Stat tiles and charts * A stat tile shows one label, one value and at most one context line (`trend` or `caption`) plus an optional `progress` meter. * `AppStatTrend(positive:)` decides the colour, not the direction: a falling expense is good news. * `AppMeter` is the static progress bar for budgets, goals, quotas and streaks. * Charts live in an `AppPanel`, use tone foregrounds for series (at most four series per chart), label axes in `xSmall` muted text and keep values in tabular figures. Paint charts with `CustomPainter` inside a `RepaintBoundary`; do not animate them on scroll. ### Filters, search and sort * `AppFilterBar` is a single-select pill row for views of the same list (All / Open / Done). Counts sit inside the pill. * `AppSegmentedControl` switches between two to four modes of the same content (Day / Week / Month, Self / Team). It is not a filter: use it for view modes and scopes, never for more than four choices. * Sorting and scopes (a collection, a wallet) use an `AppFilterButton` that names the current choice, opens an `AppOptionTile` sheet and shows `active` while it narrows the list. * Search uses the shell search action (`AppSheetSearch` in sheets). Do not put an inline search field above a list inside the shell. * Multi-select filters (labels, people, ranges) open a sheet and show their active state as a count on a filter button. ### Empty, loading and error states * First load: `AppSkeletonRows` inside the same card the rows will occupy, or static tile shapes for grids. No spinners in the middle of a page. * Empty: `AppStateMessage` with an icon, a title that says what is missing, a message that says what will appear here and one action that fixes it. * Error with no data: `AppStateMessage(tone: AppTone.destructive)` with a retry action. * Error with cached data: keep the data and show `AppStateMessage.inline` above it ("Could not refresh, showing data from 10:24"). * Load more: load automatically at the end of the list. Show a skeleton row, not a "Load more" button. ### Forms and sheets * Open forms with `showAppSheet` and lay them out with `AppSheetScaffold` and `AppGroupedSection`s of `AppTextField`, `AppSelect`, `AppSwitch`, `AppDatePicker` and `AppTimePicker`. Do not use `AlertDialog`, `SwitchListTile`, `showDatePicker` or raw `showModalBottomSheet`. * Confirmations use `showAppDialog` with a destructive `AppButton` for destructive actions. * One primary action per sheet, in the sheet's action area. ## Responsive rules Decisions use the width the page body receives (after the shell rail), via `DeviceClass` and the page primitives. | Body width | Navigation | Layout | | - | - | - | | compact (\< 600) | dock | one column; hero full width; stat grid 2 columns; lists full width | | medium (600–839) | dock | hero facts beside the headline; stat grid 3–4 columns; grids 3–4 columns; list/detail stays push-based unless the app needs a split | | expanded (≥ 840) | rail | `AppPageKind.root` pages use 2-column section layouts (list plus side panel); entity lists split into list/detail with `AppMasterDetail`; grids 4–6 columns | * Use the shared list/detail primitive, `AppMasterDetail` (`core/layout/master_detail.dart`), for two-pane screens. Do not build a per-feature split. * Side panels (filters, an inspector, a summary) sit on the trailing side at 320–360dp and scroll independently. * Pointer devices get hover states from `InkWell` and tooltips on icon buttons; keyboard users get focus rings from the core controls. ## Visual identity per app "Not repetitive" means each app has a recognisable opening and rhythm while sharing the same parts. Identity comes from four levers, in this order: 1. **The hero question.** Each root answers its own question in the hero: Finance "How much do I have?", Calendar "What is next?", Habits "Am I on streak?". Never reuse the same eyebrow and layout across apps. 2. **The app tone.** One `AppTone` per app for the hero, its icon tiles and meters. Other tones only appear for status. 3. **The dominant pattern.** Pick the layout that fits the data: a timeline for Calendar, a board for Tasks, a ledger of rich rows for Finance, a catalogue grid for Inventory, a gallery for Drive. 4. **Signature detail.** One small, cheap flourish per app, such as a streak meter in Habits, a live dot on the next meeting in Meet or a storage meter in Drive. | App | Tone | Hero question | Dominant pattern | Signature detail | | - | - | - | - | - | | Tasks | `primary` | What should I do today? | grouped rich rows, board on wide | due-state meta in role colours | | Calendar | `info` | What is next? | agenda timeline | now-line and next-event card | | Time tracker | `info` | What am I working on? | running timer hero, session list | single repainting ticker | | Habits | `success` | Am I on streak? | tracker cards with meters | streak meter | | Notes | `warning` | What was I thinking? | masonry cards | pinned strip | | Finance | `success` | How much do I have? | ledger of rich rows | budget meters | | Inventory | `primary` | What needs restocking? | catalogue grid | low-stock badges | | CRM | `info` | Who needs attention? | people rich rows | relationship meta | | Education | `primary` | What do I teach next? | course cards grid | progress meters | | Drive | `info` | Where is my file? | file gallery and list | storage meter | | Documents | `neutral` | What was I writing? | recent strip and list | reading-width editor | | CMS | `info` | What is ready to publish? | pipeline queues and a status-toned library | per-collection publish meters | | Chat | `primary` | Who is waiting for me? | conversations list, two-pane | unread emphasis | | Meet | `success` | When is my next call? | up-next hero and grid | live dot and Join | | Apps hub | per app tone | Where do I go? | launcher grid by category | live badges | The hub's tiles take each app's tone, so the launcher previews the identity users meet inside the app. ## Composition kit `package:mobile/core/ui/kit/kit.dart` exports: | Widget | Purpose | | - | - | | `AppSummaryHero`, `AppHeroFact` | opening summary with tone, headline, facts and actions | | `AppSectionHeader` | content section title with count, subtitle and action | | `AppStatTile`, `AppStatTrend` | key figure with icon, trend, caption and meter | | `AppMeter` | static progress bar | | `AppPanel` | titled content surface | | `AppKeyValueRow` | label and value on detail screens | | `AppFilterBar`, `AppFilterOption`, `AppFilterButton` | single-select views, and the trailing sort or scope control | | `AppSegmentedControl`, `AppSegment` | two to four equal-width modes of the same content | | `AppGroupedSlice`, `AppSlicePosition` | one row's share of a grouped card in a lazy builder list | | `AppRichRow`, `AppMetaItem` | entity row with metadata, trailing value and selection | | `AppAdaptiveGrid`, `SliverAppAdaptiveGrid` | width-driven columns for summaries and collections | Rows, tones, icon tiles and states (`AppGroupedRow`, `AppTone`, `AppIconTile`, `AppSkeletonRows`, `AppStateMessage`) live in `package:mobile/core/layout/layout.dart`. ### Migrating from `finance_ui.dart` `features/finance/widgets/finance_ui.dart` grew into an informal shared kit used by about 30 files outside Finance. Replace it as each app is redesigned: | Legacy | Kit | | - | - | | `FinancePanel` | `AppPanel` (no shadow) or `AppGroupedCard` | | `FinanceSectionHeader` | `AppSectionHeader` | | `FinanceStatChip` | `AppStatTile` or `AppHeroFact` | | `FinanceEmptyState` | `AppStateMessage` | | `FinanceSkeletonBlock` | `AppSkeletonRows` | | `FinanceKeyValueRow` | `AppKeyValueRow` | | `FinancePalette` colours | `AppTone` roles | ## Pilot: CMS The CMS mini app is the first app rebuilt entirely on the kit (`features/cms`). Use it as the reference when redesigning another app. * **Overview.** `AppSummaryHero` answers "what is ready to publish?" with the published share, drafts and scheduled counts, three facts and the two create actions. The three queues (needs attention, scheduled soon, archived backlog) are section headers over grouped cards of `AppRichRow`s and sit side by side on wide bodies through `AppAdaptiveGrid`. An `AppPanel` shows publish meters per collection. Tapping a queued item opens its entry. * **Library.** A lazy list of `AppGroupedSlice`s: collections first, then entries with an `AppFilterBar` of statuses (with counts) and an `AppFilterButton` that picks the collection. Rows open their editor; delete moved from inline icon buttons into the editor. * **States.** First load shows a static skeleton shaped like the overview. A denied workspace shows an `AppStateMessage`; a failed first load shows a destructive message with Retry; a failed refresh keeps the content under an inline notice. Empty queues and empty filters use inline messages. * **Editors.** `AppSheetScaffold` forms with captioned sections, `AppTextField`, `AppSelect` and `AppToggleRow`. Save is the primary action and shows progress; Delete appears for existing items and asks for confirmation in an `AppDialog`. The collection Enabled switch appears only when editing, because creation always enables a collection. An entry keeps its collection after creation. Drafts and inline errors survive a failed save, as before. * **Status language.** Every entry status has one tone and icon: draft `warning`, scheduled `info`, published `success`, archived `neutral`. `test/features/cms/view/cms_render_test.dart` renders the overview and the library at every review size and theme with the same guidelines as the kit gallery. Feature render tests should use the shared harness in `test/helpers/design_render_harness.dart` (`DesignRender`, `DesignRenderApp`). ## Regression coverage * `test/core/ui/kit/kit_components_test.dart` covers grid columns, filter and segment selection semantics, section header actions, trend colours, selected rows, slice positions, meter clamping, the hero's wide layout and high-contrast surfaces. * `test/core/ui/kit/kit_gallery_test.dart` renders every component on three boards at 390x844, 820x1180 and 1440x900 in light, dark and Aurora blobs. Each render must lay out without exceptions and pass the tap-target, labelled-target and text-contrast guidelines. Set `DESIGN_RENDER_DIR` (and optionally `DESIGN_RENDER_PIXEL_RATIO`) to write the PNGs for review. # Mobile design system Source: https://docs.tuturuuu.com/build/development-tools/mobile-design-system Spacing tokens, page primitives, components, sheets and dialogs, the full-bleed allowlist, breakpoints, and the adaptive shell for apps/mobile. The Flutter app uses one layout foundation so every screen shares the same gutters, section rhythm, dock clearance, and wide-screen behavior. Import it from `package:mobile/core/layout/layout.dart`. ## Rule Build page bodies with the page primitives. Use the full-bleed variant only for the screen types in the [allowlist](#full-bleed-allowlist). Do not hand-write page gutters (`EdgeInsets.fromLTRB(16, …)`), wrap a scrollable in `ResponsiveWrapper`, or add `N + MediaQuery.paddingOf(context).bottom` for the scroll end in new or substantially edited screens. ## Breakpoints `DeviceClass` follows the Material 3 window-size classes (`core/responsive/breakpoints.dart`): | Class | Width | Typical devices | | - | - | - | | compact | \< 600dp | phones (portrait and most landscape) | | medium | 600–839dp | portrait tablets, foldables | | expanded | >= 840dp | landscape tablets, laptops, desktops | Page primitives resolve the class from the width the page actually receives, so a page beside the shell rail uses the gutters for its own available width. ## Tokens `layout_tokens.dart` defines the only spacing values a page should need. | Token | Value | | - | - | | `AppSpacing` | 2, 4, 8, 12, 16, 20, 24, 32, 40 (`xxs` … `huge`) | | `AppLayout.gutter` | 16 compact, 24 medium, 32 expanded | | `AppLayout.pageTop` | 8 below the shell header | | `AppLayout.scrollEndGap` / `boxEndGap` | 24 / 16 after dock clearance | | `AppLayout.sectionGap` | 24 between grouped sections | | `AppLayout.sectionCaptionGap` / `sectionCaptionInset` | 8 / 16 | | `AppLayout.itemGap` | 12 between stacked cards | | `AppLayout.rowMinHeight` / `rowCompactMinHeight` | 56 / 48 | | `AppLayout.rowHorizontalPadding`, `rowLeadingSize`, `rowLeadingGap` | 16, 32, 12 | | `AppLayout.rowDividerInset` | 60 (aligns dividers with row text) | | `AppRadius` | 8, 12, 16, card 22, sheet 24, dock 26, pill | Max content width (gutters included) per `AppPageKind`: | Kind | Use | medium | expanded | | - | - | - | - | | `root` | root destinations and dashboards | 1120 | 1440 | | `detail` (default) | mini-app lists and detail screens | 600 | 720 | | `form` | auth, create and edit forms | 480 | 480 | | `reading` | documents, notes, long text | 640 | 680 | Compact screens never constrain width. The max width is the content box, so the usable field width is the max width minus both gutters: a `form` at 480dp gives 432dp fields on medium bodies and 416dp on expanded bodies. Auth screens (`AuthScaffold`) intentionally use that 416–432dp field width. `ResponsivePadding` is a compatibility layer that derives from these tokens; its names and values are unchanged. ## Page primitives | Widget | Use | | - | - | | `AppPageListView(children:)` / `.builder` | Standard scrolling page. | | `AppPageScrollView(slivers:)` | Sliver page; all slivers share one set of insets. | | `SliverAppPage(sliver:)` | One padded sliver inside a hand-built `CustomScrollView` (pinned headers, `SliverFillRemaining`). Use `includeTop` / `includeBottom` for middle slivers. | | `AppPageBody(child:)` | Non-scrolling body, such as an editor or a pane layout. | | `AppPageBody.fullBleed(child:)` | Allowlisted canvases only. | | `AppPageInsets.resolve` / `.of` | Pure resolver behind all of the above. | Every primitive: * applies the device-class gutter plus horizontal safe areas; * centers content at the page kind's max width by **padding**, so the scrollable, scrollbar, and drag area stay full width; * adds `AppLayout.pageTop` plus an optional `topInset`. Root pages pass `floatingShellHeaderInset(context)`; * reserves the shell's dock clearance (`MediaQuery.padding.bottom`, published by `FloatingShellDock`) plus the end gap. Pass `consumeBottomClearance: false` when an inner scrollable should run beneath the dock and read the clearance itself. Every primitive also removes the insets it consumes (horizontal safe areas, and the bottom clearance when consumed) from its children's `MediaQuery`. A legacy `SafeArea` or `MediaQuery.paddingOf(context).bottom` inside a migrated page therefore adds nothing instead of counting the dock clearance twice. `SliverAppPage(includeBottom: false)` leaves the bottom clearance for the sliver that consumes it. ## Grouped sections `AppGroupedSection(caption:, footer:, children:)` renders a muted caption, a heavily rounded translucent `AppGroupedCard`, inset hairline dividers, and an optional muted footer. Use `AppGroupedSection.spaced` in a list to add `AppLayout.sectionGap` after the section. The card only groups and divides rows. Row layout belongs to the owning feature (for example `SettingsTile`). Cards use theme `surface` / `outlineVariant` and become solid in high-contrast mode. ### Rows, tiles and states `core/layout/grouped_rows.dart` and `core/layout/state_message.dart` hold the shared row vocabulary for grouped cards. Feature screens compose these instead of drawing their own tinted tiles, list tiles or empty-state columns. | Widget | Use | | - | - | | `AppTone` | Semantic tint: `neutral`, `primary`, `info`, `success`, `warning`, `destructive`. `foreground` and `fill` resolve from `AppColorTokens`, so every appearance theme, accent and brightness applies. Never pick a hue in feature code. | | `AppIconTile(icon:, tone:, size:, badge:)` | Rounded leading tile. `badge: true` adds the primary unread dot. Default size is `AppLayout.rowLeadingSize` (32). | | `AppGroupedRow(title:, subtitle:, leading:, trailing:, onTap:, showChevron:)` | Row inside an `AppGroupedCard`: at least `AppLayout.rowMinHeight` tall, row padding from the tokens, and the whole row is the tap target and a semantic button. | | `AppSkeletonRows(count:, leadingSize:)` | First-load placeholder rows inside a card. Use it instead of a centred spinner. | | `AppStateMessage` / `.inline` | Empty, error and unavailable states. The default constructor centres a 56dp tile, title, message and optional outline action for a page or pane. `.inline` lays the same content out as a card row. Destructive messages are live regions. | When a row's leading tile is larger than 32dp, pass the matching `dividerInset` to the card (padding + tile + gap) so dividers align with the text column. ## Icons Mobile icons match the web's Lucide icons in glyph and name. `LucideIcons` (base) and `LucideLabIcons` (lab) are generated from the exact `lucide-react` and `@lucide/lab` versions in `bun.lock`. Import them through `package:mobile/core/ui/ui.dart` or `core/ui/icons/app_icons.dart`. | Web | Mobile | | - | - | | `` (`arrow-right`) | `Icon(LucideIcons.arrowRight)` | | ``, an alias of `house` | `LucideIcons.home`, the same glyph as `LucideIcons.house` | | `@lucide/lab` `appleCore` | `Icon(LucideLabIcons.appleCore)` | * **Coverage.** Every icon at the pinned versions: 1,903 base icons with 264 web aliases, and 357 lab icons. Names are the web names in lowerCamelCase. Dart reserved words get an `Icon` suffix (`switchIcon`). * **Glyphs.** Each glyph is the 2px round-stroked outline the web renders at 24px. `scripts/mobile-lucide/generate.mjs` strokes the pinned SVG nodes with Skia (CanvasKit), the engine Flutter draws with, and unions any filled shapes. Contours are re-oriented for the nonzero fill rule fonts use, so holes stay open. The written font is parsed back, and every serialized glyph is rasterised against the stroked original; any glyph whose visible ink differs by more than 1% fails the run. The worst glyphs differ by 0.12% (base) and 0.08% (lab). A font is used because the app passes icons as `IconData` (`Icon`, `IconButton`, shell actions). Stroke width is therefore fixed at the web default of 2. * **Brand marks.** Lucide no longer ships brand icons. `AppIcons` maps the few the app used to neutral glyphs. * **Licences.** Lucide and Lucide Lab are ISC licensed. Their notices ship as `assets/fonts/lucide/LICENSE-*.txt` and appear on the app's licences page. The fonts and Dart classes are marked as generated. ### Regenerating Generation is deterministic: sorted names and fixed font timestamps. Run it on a remote validation lane, not on an agent laptop: ```bash theme={null} cd scripts/mobile-lucide npm ci node generate.mjs --repo ../.. ``` The generator reads the versions and integrity hashes from `bun.lock`, downloads the tarballs, and refuses a tarball whose hash differs. It then rewrites the fonts, the generated Dart, the licence copies, the manifest and the parity fixture. `scripts/check-mobile-lucide-sync.js` runs in Mobile Analysis, in `bun check:changed` and as `bun check:mobile-lucide`. It fails when the web's `lucide-react` or `@lucide/lab` version changes without regeneration, or when a generated file differs from its manifest hash. `test/core/ui/lucide_icons_test.dart` checks the manifest counts. It also renders sample base and lab icons through the real `Icon` widget and compares them with the web SVG drawn by `flutter_svg`. Set `DESIGN_RENDER_DIR` to write the light and dark parity grids (`lucide-parity-light.png`, `lucide-parity-dark.png`). ## Components and overlays `package:mobile/core/ui/ui.dart` is the app-owned design system that replaces `shadcn_flutter`. New and migrated code imports it. A CI ratchet blocks new shadcn imports. See the [migration plan](/build/development-tools/mobile-shadcn-migration). **Tokens.** `AppTokens.of(context)` (or `context.tokens`) exposes `colorScheme` (`AppColorTokens`), `typography` (`AppTypography`), radii and brightness. The appearance theme publishes `AppColorTokens` as a Material `ThemeExtension` derived from the same scheme as the selected appearance, so every theme, accent and light/dark mode applies without extra wiring. Role and type names match the legacy shadcn names. `AppElevation` and `AppMotion` hold shadows and durations. Spacing and radii stay in `AppSpacing` / `AppRadius`. **Components.** `AppButton` (`primary`, `secondary`, `outline`, `ghost`, `destructive`; densities, sizes, `loading`), `AppIconButton` (required `tooltip`), `AppTextField`, `AppSearchField`, `AppCard`, `AppBadge`, `AppAlert`, `AppDivider`, `AppSwitch`, `AppCheckbox`, `AppSelect`, `AppOptionTile`, `AppTooltip`, `showAppToast`, `AppGap`, `AppScaffold` and `AppTopBar`. Icons are the generated web-parity `LucideIcons` and `LucideLabIcons` (see [Icons](#icons)), with `AppIcons` for semantic aliases and brand fallbacks. **Sheets and dialogs.** Open every overlay with `showAppSheet` (compact: bottom sheet; wider: centered dialog) or `showAppDialog`. Lay out the body with `AppSheetScaffold` or `AppDialog`. * **Back closes the topmost overlay first.** `AppModalBinding` registers a *modal* entry in `ShellChromeActionsCubit`. While it exists, `backForLocation` returns the overlay's close action, so system Back (`ShellPage.didPopRoute`), the floating-dock Back and the Android back dispatcher close the sheet or dialog and never pop the page beneath. A dialog above a sheet owns Back until it closes. `showAdaptiveSheet` uses the same binding. * **The shell chrome stays live.** Sheets open on the nearest (shell) navigator, below the shell's top navbar and floating dock. While an overlay is open, modal registrations replace the page's chrome actions for every location. * **Dynamic search.** Pass `search: AppSheetSearch(...)` to `AppSheetScaffold`. Inside the shell, it registers a dock search action; tapping it turns the top navbar into the shell search field bound to the sheet's controller. The action is removed with the sheet. Outside the shell (no `ShellChromeActionsCubit`), an inline `AppSearchField` is rendered. * **Sizes.** `AppSheetSize.fit`, `medium` (60% with a drag detent to 94%), `large` (94%) and `full`. The drag handle and drag-to-dismiss are on unless `enableDrag: false`. * **Insets.** The body ends above the larger of the dock clearance and the keyboard. When the keyboard or large text leaves less than 280dp (scaled with text), the header, search and actions scroll with the body. * **Accessibility.** Overlays scope and name their route for screen readers, keep focus inside the modal route, use live regions for alerts and toasts, and never clamp text scaling. * Explicit full-screen forms and system-owned flows still open above the shell with `useRootNavigator: true` and `AppScaffold(headers: [AppTopBar(...)])`. The Personal and Workspace timezone pickers (`features/settings/view/timezone_picker_sheet.dart`) are the reference implementation. ## Full-bleed allowlist Only these screen types may use `AppPageBody.fullBleed` or skip the gutters: * calendar timelines, month, and year grids; * task board kanban, board timeline, and planner/gantt canvases; * chat and Assistant conversation threads, composers, and Assistant Live; * Meet call stages and other live video surfaces; * media, document-page, and map viewers, plus full-canvas editors. Full-bleed still honors horizontal safe areas. Unless the canvas reads the dock clearance itself (`consumeBottomClearance: false`), it also stops above the dock. Headers, toolbars, and empty/error states inside a full-bleed screen should still use the gutter token. ## Adaptive shell `shellNavigationModeFor(size)` in `features/shell/view/shell_navigation_rail.dart` picks the navigation mode: * **Dock**: phones, including landscape phones, and portrait tablets. The floating bottom dock owns destinations and in-app Back. * **Rail**: width >= 840dp and shortest side >= 600dp (landscape tablets, laptops, desktops). `ShellNavigationRail` shows the same destinations on the leading edge, including the mini-app Back item. The dock keeps only the Assistant/Notes composer and page actions; an idle dock paints no surface. Scroll content reserves dock clearance only while the dock shows a composer slot or dock actions for the current route. Rail destinations show their existing localized label as a hover-only tooltip on pointer devices. Both modes keep a single top navbar title with no Back button, system Back and deep links (handled by `ShellPage` independently of the layout), and scroll clearance. `AdaptiveShellFrame` builds the same tree in both modes: a `Stack` whose first, keyed child holds the body. Rotation (for example an iPad going from 820x1180 to 1180x820), a window resize across either threshold, or an immersive/fullscreen toggle only adds or removes the rail, so the dock and every root tab keep their state. Routed pages survive through GoRouter's navigator key, but offstage root tabs do not, so regressions must assert on a visited root tab. The rail paints after the body because routed pages can contain `BlockSemantics`, which would otherwise drop the rail from the accessibility tree. ## List/detail layouts `AppMasterDetail` (`core/layout/master_detail.dart`) is the shared list/detail primitive. It decides from the width its body receives (after the shell rail), never the window: | Body width | Layout | | - | - | | \< 720dp | List only. The feature keeps its pushed detail route. | | >= 720dp | List pane beside the detail pane, split by a 1dp divider. | The list pane is about a third of the body, clamped to 320–400dp. Portrait tablets (about 768–834dp) therefore split as well. Each pane resolves its own gutters and page-kind width, so a 360dp list uses compact gutters and the detail pane centres `reading` content at its own width. Contract for features: * **The feature owns the selection.** Keep it in the screen's state, not in the layout, so it survives rotation and resizing. Pass the selected detail as `detail` and its identity as `detailKey`. Without a selection, the pane shows `emptyDetail`; use `AppMasterDetailEmpty` with localized strings. * **Crossing the split width moves the selection.** `onSplitChanged` fires after the first layout and on every change. On split → compact, push the usual detail route for the open selection. A covered route is not laid out, so wrap the pushed route's content in `AppMasterDetailRouteBody`; its `onSplit` fires when that route becomes wide enough, and the feature then removes the route (`Navigator.removeRoute`) and shows the selection in the pane. * **Deep links** open the selection through the same entry point as a tap, so they land in the pane on split layouts and in the pushed route on phones. * **Back.** On phones the detail is a route, so Back pops it. On split layouts the first Back clears the selection: register the list owner's `ShellChromeActions.onBack` while a pane selection is open, and add a `PopScope` for system Back. The next Back leaves the app as usual. * **Chrome.** The shell navbar and dock keep the list's title, mini-nav and actions. The detail pane carries the item's title and item actions in a pane header (`AppMasterDetail.inDetailPaneOf(context)`), plus a close button. This is not a second app bar: it belongs to the pane, and phones never show it. Route the pane's child routes (reply, attachment preview) through the owner's push helper, so the list chrome yields while they are open. * **Keyboard and focus.** Each pane is a `FocusTraversalGroup`. A new `detailKey` moves focus into the detail pane, and Escape there calls `onCloseDetail`. Mail is the first adopter; see `mail_master_detail_test.dart` and `core/layout/master_detail_test.dart`. ## Layout audit (October 2026) Before this foundation: * 179 feature files hard-coded 16/20/24dp page insets. 103 of them set horizontal gutters without `ResponsivePadding`, so tablets and desktops got phone gutters. Phones mixed 16 and 20dp (for example the Documents list versus the document editor). * Scroll ends used at least eight different constants (16, 20, 24, 28, 32, 40, 96, 108) on top of `MediaQuery.paddingOf(context).bottom`. * 38 files wrapped scrollables in `ResponsiveWrapper`, which narrowed the scroll and drag area on wide screens. * Device classes came from the window width, which is wrong beside a rail. * `ShellPage` documented a rail and sidebar, but it always rendered the floating dock. * Root pages added `floatingShellHeaderInset` with ad-hoc +8/+10 offsets. ## Page kinds by screen Feature screens use the primitives with these page kinds: | Kind | Screens | | - | - | | `root` | Home dashboard, Profile overview, Apps hub, Assistant transcript and start state, Drive grid, Meet lobby, Chat surface (two panes), Notes (list and editor), CMS overview, Mobile versions, board timeline, calendar year grid | | `detail` | Notifications inbox and archive, Profile timeline, Drive list, task lists, boards, planning, portfolio, estimates, habits, time tracker tabs and requests; finance, inventory and storefront lists and overviews; education, CMS library and CRM; mail organization; Meet ended review; workspace select, accounts, security, reminders | | `form` | Auth screens (`AuthScaffold`), inventory editors (`InventoryFormScaffold`), mail composer and settings, product feedback, MFA request, required-update gate, Assistant memory and personality editors | | `reading` | Documents editor, mail reader | Full-bleed screens: calendar day/multi-day/month views, the task board kanban canvas, chat threads and composers, Assistant Live, the Meet call grid, and media/attachment viewers. Their headers, empty states and loading states still use the gutter. When a page passes the dock clearance through (`consumeBottomClearance: false`), a scrolling canvas pads its own scroll end. The calendar day, 3-day and week timelines pad their vertical scroll end with `MediaQuery.paddingOf(context).bottom`, so the last hour scrolls clear of the dock. A fixed, non-scrolling canvas such as the month grid must stop above the dock by reserving the same clearance below itself. Shell detail routes that are not mini-app nav items, such as a portfolio project, register `ShellTitleOverride` and a `ShellMiniNav` with a `deepLinkBackRoute`, so the dock or rail Back returns to their parent list. Documented exceptions: * Modal sheets and dialogs keep their own internal padding; they are not page bodies. * Mail swipe rows stay edge to edge on phones and centre at the detail width on wider windows. The mail reader keeps a larger named end gap so long messages clear the Mail dock. * Standalone (non-shell) forms with their own footer reserve the system inset in that footer instead of consuming dock clearance. * Settings, Home, Notifications and Profile migrate in their own lanes. The full-screen task detail view keeps its own inset math until it is reworked. The Mail and Assistant settings hubs move to the shared insets with their settings stacks. * Onboarding is a full-screen flow above the shell with paged slides. Its slides and footer keep their own horizontal padding and system insets. Layout widths should come from the constraints a widget receives (`LayoutBuilder`), not `MediaQuery.sizeOf(context)` or `context.isCompact`. Beside the rail, the window is wider than the body. Use `AppPageInsets.gutterOf(context, width: constraints.maxWidth)` for a gutter outside the page primitives, as the Meet and Assistant Live canvases do. Overlays such as `showAdaptiveDrawer` keep the window class, because they open above the shell rather than inside the body. ## Regression coverage * `test/core/layout/layout_tokens_test.dart`: token derivation, `ResponsivePadding` compatibility, and the inset resolver. * `test/core/layout/app_page_test.dart`: gutters per device class, max-width centering, dock clearance, full-bleed opt-out, slivers, and grouped sections. `app_page_media_query_test.dart` covers the consumed-inset removal. * `test/features/documents/view/documents_layout_test.dart`: the migrated Documents list and editor. * `test/features/shell/view/shell_navigation_rail_test.dart` and `shell_rail_navigation_test.dart`: rail geometry, RTL, Back reachability, system Back, deep links, landscape phones, and body-width gutters. Set `DESIGN_RENDER_DIR` to write phone, tablet, and desktop PNG captures. * `shell_rail_layout_test.dart`: no remount across rotation, resize, or immersive toggles, rail-mode clearance, RTL through `ShellPage`, and hover-only tooltips. * `shell_viewport_checks.dart`: the dock/rail viewport matrix, including the root page's first content one `pageTop` gap below the floating header. * `test/core/ui/app_components_test.dart`: token resolution from the appearance extension, `AppGap` axes, every component in light and dark at 2x text, and toast placement above the dock. * `test/core/ui/app_sheet_host_test.dart`: modal chrome registration, Back from the dock or shell closing only the sheet, dialog-over-sheet ordering, and the inline search fallback outside the shell. * `settings_timezone_picker_checks.dart` (in `settings_compact_navigation_test.dart`): the timezone pilot inside the real shell. It covers dock search, system and dock Back, the pinned current zone and Automatic, and keyboard insets. `SETTINGS_RENDER_DIR` with `SETTINGS_RENDER_PIXEL_RATIO=3` writes the 390x844\@3x renders. * `test/core/layout/page_padding_matrix_test.dart`: representative detail and form screens at phone, portrait/landscape tablet and desktop widths. It checks that the page list uses the resolved insets, the scrollable stays full width, and the content width equals the body or max width minus both gutters. A rail-narrowed case checks that an expanded window whose body is medium resolves medium insets. * `test/features/calendar/widgets/timeline_dock_clearance_test.dart` and `month_view_dock_clearance_test.dart`: the timelines scroll their last hour clear of the dock, and the month grid stops above it. * `test/features/drive/drive_page_test.dart`: the Drive list keeps the detail width on a landscape tablet while the grid uses the root width. `DESIGN_RENDER_DIR` writes PNG captures. ### Custom selection and date controls AppSwitch and AppCheckbox draw their own token-based surfaces on every platform. They retain 48dp touch targets, keyboard activation, focus indicators, disabled states and screen-reader state. Use AppToggleRow or AppCheckboxRow for labeled settings. AppChip covers selectable filters and short actions. AppSlider previews through onChanged and commits through onChangeEnd. Use showAppDatePicker and showAppTimePicker for in-app date/time editing. Both use the modal host so Back dismisses the picker before its parent editor. Date bounds, leap days and 12/24-hour input are preserved; year selection supports the shell search action. Existing framework localizations supply English and Vietnamese picker labels. System-owned permission dialogs remain OS-owned. Regression coverage: test/core/ui/app\_selection\_controls\_test.dart and test/core/ui/app\_date\_time\_picker\_test.dart. Selection rows respond to taps on their labels as well as the control. Toast handles become closed when their overlay is disposed, canceling their dismissal timer. Text inputs retain the controller, selection, context menu and multiline geometry when used by a composer or form. Scoped settings editors forward account/workspace providers into nested sheets and retain their original cancellation fences; see the mobile skill reference for migration details. AppListTile provides token-based action and metadata rows. AppDisclosure owns expand/collapse semantics and keyboard activation without platform tile chrome. Use AppSheetSearch for searchable modal content and a location-scoped shell action for route search. Nested record browsers open their own sheet instead of adding competing inline search fields to a page. Custom button frames establish a semantics boundary, so a nested visibility or row action cannot merge with its parent launch action. The existing `apps_reorder_grid_accessibility_test.dart` checks that those nodes remain separate and independently actionable. Text-field labels stay above the input border and retain the editable's accessible name. Chip avatars use an 18dp swatch inside the shared 48dp touch target. `app_field_presentation_test.dart` verifies both presentation contracts. Short dialogs keep their actions outside the content scroll region. Validation errors and enlarged text must not push Save or Retry off the visible surface; see `app_dialog_layout_test.dart` and the unchanged Assistant rename retry tests. `AppChip.selected` is null for display/action chips and a bool for selectable choices. Disabled choices retain their selected and disabled accessibility states. Dialog and fit-sheet hosts consume keyboard insets once. Their content receives those consumed insets so an AppSheetScaffold cannot add the same clearance twice. Phone regression coverage in `app_phone_picker_test.dart` exercises picker results, system Back and tappable OK/Save actions at 390×844 with a 300dp keyboard. Chip activation callbacks are exclusive: onSelected requires a boolean selected; onPressed requires selected to be null. Single-choice groups set inMutuallyExclusiveGroup. Selected choices show a check on a tonal surface. AppTextFormField synchronizes controller edits with validation and restores the initial text on Form.reset; `app_form_field_lifecycle_test.dart` covers both. Scoped settings cancellation removes only the invalidated editor route. A pop animation can remain paused when an ancestor covers its nested navigator, so privacy and account fences cannot depend on that animation completing. The covered account/workspace ABA checks in `settings_assistant_shell_checks.dart` retain their no-write and covering-route assertions. # Mobile shadcn_flutter migration Source: https://docs.tuturuuu.com/build/development-tools/mobile-shadcn-migration Component mapping, batch order, codemod and ratchet for removing shadcn_flutter from apps/mobile. `apps/mobile` is replacing `shadcn_flutter` with the app-owned design system in `lib/core/ui/` (see [Mobile design system](/build/development-tools/mobile-design-system#components-and-overlays)). The migration runs in reviewable batches. A CI ratchet stops new imports, and the dependency is removed once nothing imports it. ## Ratchet `scripts/check-mobile-shadcn-imports.js` scans `apps/mobile/lib` and `apps/mobile/test`. It treats a direct `package:shadcn_flutter` import, or an import of an in-repo shim, as a shadcn import. The shims are `shadcn_flutter_compat.dart`, `shadcn_material_bridge.dart`, `shadcn_localizations_fallback.dart` and `mobile_shad_theme.dart`. Each such file must appear in `apps/mobile/tool/shadcn_import_allowlist.txt`. * A new importer fails with a pointer to `package:mobile/core/ui/ui.dart`. * Every URI of every `import`/`export` directive is checked. A small Dart tokenizer (`scripts/dart-directive-uris.js`) skips comments and strings, so comments inside a directive, several directives on one line, conditional `if (dart.library.x)` alternatives and adjacent string literals are all read. Dart escapes (`\uXXXX`, `\u{...}`, `\xHH`, simple escapes) are decoded, and a URI with a malformed escape or interpolation fails closed. Relative and `package:mobile/` paths are normalised before matching a shim. * An allowlisted file that no longer imports shadcn also fails. Run `node scripts/check-mobile-shadcn-imports.js --prune` to remove migrated entries. The flag never adds entries. * Entries missing from the allowlist at the base revision fail, so a PR cannot add an importer together with an allowlist line. CI passes the PR base (or the previous `main` commit, else `main`) with `--base`. Locally the base defaults to the merge base with `origin/main`. When no base resolves, the check fails closed and asks for `--base`. * When the list is empty, the check fails until `flutter pub remove shadcn_flutter` has run. The check runs in the **Mobile Analysis** CI job, in `bun check:changed` whenever mobile Dart or the allowlist changes, and as `bun check:mobile-shadcn`. Part files (`part of`) inherit their library's import. A library is migrated only when neither it nor any of its parts uses the prefix. The initial allowlist (October 2026) has **325** files: 253 under `lib/` and 72 under `test/`. Counting the 139 part files, **464** files use `shad.` identifiers. ## Component mapping | shadcn\_flutter | core/ui replacement | Notes | | - | - | - | | `shad.Theme.of(context)` | `AppTokens.of(context)` / `context.tokens` | Same `colorScheme.*` and `typography.*` names; `radiusSm…Xl`, `brightness`. Codemod. | | `shad.ColorScheme` roles | `AppColorTokens` | Adds `success`, `warning`, `info`. Published as a `ThemeExtension` by the appearance theme. | | `shad.Typography` | `AppTypography` | Same sizes and weights (`small` 14, `textSmall` 14/500, `h4` 18/600…). | | `shad.Gap(n)` | `AppGap(n)` | Follows the parent `Flex` or scroll axis. Codemod. | | `shad.LucideIcons.x` | `LucideIcons.x` (generated at the web's `lucide-react` version) | Same member names; lab icons are `LucideLabIcons`. The 17 brand marks and `railSymbol` that Lucide dropped map to `AppIcons.*`. Codemod. | | `shad.PrimaryButton` / `SecondaryButton` / `OutlineButton` / `GhostButton` / `DestructiveButton` | `AppButton.primary` / `.secondary` / `.outline` / `.ghost` / `.destructive` | Keeps `child`, `onPressed`, `leading`, `trailing`, `density`, `size`, `shape`, `enabled`, `alignment`. Adds `loading`. | | `shad.ButtonDensity.*`, `ButtonSize.*`, `ButtonShape.*` | `AppButtonDensity`, `AppButtonSize`, `AppButtonShape` | `icon`, `iconDense`, `dense`, `compact`, `comfortable` keep their meaning. | | `shad.IconButton` | `AppIconButton(tooltip:)` | `tooltip` is required and is the accessible name. | | `shad.TextField` / `shad.TextArea` | `AppTextField` (`maxLines: null` for areas) | `placeholder` is a `String`. `InputFeature.leading/trailing` become `leading`/`trailing`. Keep `contextMenuBuilder` call sites on `PlatformTextContextMenu`. | | Search inputs | `AppSearchField`, or `AppSheetSearch` in sheets | Shell pages and sheets register dynamic search instead of inline fields. | | `shad.Card` / `shad.SurfaceCard` | `AppCard` (`elevated: true`) | Use `AppGroupedSection` for settings-style rows. | | `shad.OutlineBadge` / `PrimaryBadge` / `SecondaryBadge` / `DestructiveBadge` | `AppBadge(variant:)` / `AppBadge.outline` | Adds `success`/`warning`. | | `shad.Alert(destructive:)` | `AppAlert(variant:)` / `AppAlert.destructive` | Live region; `info`, `success`, `warning` variants. | | `shad.Divider` / `VerticalDivider` | `AppDivider` / `AppDivider.vertical` | | | `shad.Switch` | `AppSwitch` | Adaptive; pass `semanticLabel` when no visible label is adjacent. | | `shad.Checkbox` + `CheckboxState` | `AppCheckbox(value: bool?)` | `checked` → `true`, `unchecked` → `false`, `indeterminate` → `null` with `tristate`. | | `shad.Select`, `DropdownMenu` (single choice) | `AppSelect` | Opens an `AppOptionTile` list in `showAppSheet`. | | `shad.Tooltip` | `AppTooltip` | | | `shad.showToast` / `ToastOverlay` | `showAppToast` | Sits above the dock clearance; one at a time. | | `shad.AlertDialog`, `compat.showDialog` | `showAppDialog` + `AppDialog`, `showAppConfirmDialog` | Correct Back semantics. | | `openDrawer`, `showAdaptiveDrawer`, `showAdaptiveSheet`, `AppDialogScaffold` | `showAppSheet` + `AppSheetScaffold` | `showAdaptiveSheet` already uses `AppModalBinding` and remains a compatibility path. | | `shad.Scaffold` | `AppScaffold` | Transparent inside the shell. `headers` only for full-screen flows above it. | | `shad.AppBar` (outside shell chrome) | `AppTopBar` | Inside the shell, register title and actions with the shell instead. | | `shad.NavigationItem`, `NavigationBar`, `Tabs`/`TabItem` | Shell dock (`MorphingNavigationBar`), mini-nav, Material `TabBar` styled with tokens | Shell batch. | | `shad.FormKey`, `FormField`, `MenuButton`, `DatePicker`, `CalendarView`, `Slider`, `Toggle`, `LinearProgressIndicator` | Material equivalents themed with `AppTokens`, wrapped as needed | Low counts (≤15 each); migrate in their feature batch. | | `shad.ShadcnApp.router`, `ShadcnLocalizations`, `ThemeData`, `ColorSchemes`, `ThemeMode`, `ScaffoldTheme`, `ComponentTheme` | `MaterialApp.router` with `AppearanceTheme.materialTheme`, a local `ThemeMode` | Final batch, with `app.dart`, theme cubit, appearance scope and test helpers. | ## Codemod `scripts/mobile-shadcn-codemod.js` performs the mechanical renames on whole libraries, including their parts: ```bash theme={null} node scripts/mobile-shadcn-codemod.js --dry-run apps/mobile/lib/features/finance node scripts/mobile-shadcn-codemod.js apps/mobile/lib/features/finance apps/mobile/test/features/finance cd apps/mobile && dart format lib test # scoped paths in practice node ../../scripts/check-mobile-shadcn-imports.js --prune ``` It rewrites `.Gap(` → `AppGap(`, `.Theme.of(` → `AppTokens.of(`, and `.LucideIcons.*` → `LucideIcons.*` or `AppIcons.*`. It adds `package:mobile/core/ui/ui.dart` and drops the shadcn import once neither the library nor its parts use the prefix. The codemod covers 1,705 icons, 1,282 gaps and 770 theme reads, about 3,750 of the 5,270 prefixed uses. A dry run over the whole app touches 322 files. Afterwards, 263 files still import shadcn (62 freed). After running it, fix values passed into APIs typed `shad.ThemeData`, then run `flutter analyze` and the feature's tests on the remote validation lane. Replace the remaining \~1,500 widget uses by hand with the table above. ## Batch order Each batch is one PR, or one layer of a native stack, owned by one lane. Run the codemod for its folders, migrate the remaining widgets, prune the allowlist, and attach renders for changed screens. LOC and counts cover every file that uses `shad.`, parts included. | Phase | Scope | Files | LOC | Icons / Gap / Theme | Manual uses | | - | - | - | - | - | - | | 1 | Codemod sweep across all folders (mechanical only, no widget swaps) | 322 | \~148k touched | 1,705 / 1,282 / 770 | 0 | | 2 | `core`, `widgets`, `app_version`, `mobile_versions`, `desktop_update`, `onboarding`, `counter` and their tests: shared surfaces first | 52 | 12.1k | 1,520 / 42 / 28 | 165 | | 3 | `settings`, `profile`, `security`, `auth` and tests (coordinate with the settings lane) | 92 | 23.0k | 75 / 193 / 120 | 224 | | 4 | `finance`, `inventory`, `storefront` and tests | 61 | 26.1k | 0 / 378 / 160 | 226 | | 5 | `tasks_boards`, `task_portfolio`, `tasks_estimates`, `tasks`, `task_planning` and tests | 75 | 28.0k | 21 / 349 / 216 | 394 | | 6 | `time_tracker`, `habits`, `calendar`, `reminders` and tests | 69 | 23.9k | 58 / 251 / 119 | 254 | | 7 | `assistant`, `chat`, `mail`, `meet`, `notes`, `drive`, `documents`, `education`, `crm`, `cms`, `apps`, `dashboard`, `notifications`, `workspace` and tests | 82 | 24.3k | 31 / 42 / 102 | 133 | | 8 | `shell`, `app.dart`, `bootstrap`, theme plumbing, `test/shell`, `test/helpers`, `test/app` | 33 | 10.8k | 0 / 27 / 25 | 118 | | 9 | Removal: delete the shims, `flutter pub remove shadcn_flutter`, drop the allowlist file and its CI step | — | small | — | — | Phases 2–7 can run in parallel after phase 1 merges, because they touch disjoint folders. Phase 8 waits for 2–7. Large features (`tasks_boards`, `time_tracker`, `finance`) should be split by sub-folder into stack layers of about 10–15 files. ## Final removal 1. Allowlist empty (`--prune` leaves only the header). 2. Delete `core/widgets/shadcn_flutter_compat.dart`, `shadcn_material_bridge.dart`, `shadcn_localizations_fallback.dart` and `core/theme/mobile_shad_theme.dart`. Fold `AppearanceTheme.colorScheme` into `AppColorTokens` directly. 3. `flutter pub remove shadcn_flutter`, then `flutter pub get`, then analyze and test on the validation lane. 4. Remove the ratchet step from `.github/workflows/mobile.yaml`, the `check-changed` plan entry, the script, its test and the allowlist file. 5. Update this page and the mobile skill reference to mark the migration complete. # Mobile state and lifecycle regressions Source: https://docs.tuturuuu.com/build/development-tools/mobile-state-regressions Diagnose retained state, owned overlays, and asynchronous mobile failures with real host coverage. ## Separate repeated push handling from duplicate Mail events Mobile startup initializes push handling before a restored auth session starts. Both entry paths must await the same initialization so one foreground delivery has one receiver. Fetch the cold-start message before attaching listeners; a failed lookup must not leave a receiver behind when setup retries. `push_notification_initialization_test.dart` exercises overlapping setup, retry after failure, disposal, and distinct notifications through the service. Duplicates on both Web and mobile lists require a separate producer diagnosis. Mail receipts prevent re-enqueue of the same message for the same recipient; forwarding can create distinct messages in different mailboxes. Compare redacted notification, message, mailbox, receipt and batch identities before changing that policy. A client display fix does not prove a shared producer issue resolved. Web notification type labels interpolate runtime types, which the static key scanner cannot enumerate. `notification-type-translations.test.ts` checks the incoming Mail label in both owning Web bundles, including a key absent from both. ## Remove the route owned by the departed session An account or workspace switch can happen while an editor is covered by another modal. A generic navigator pop can dismiss the covering route and leave the old editor alive. Capture the editor's own route and remove that route after the frame, checking that it is still active and the navigator is mounted. If the UI is settled, call `WidgetsBinding.instance.ensureVisualUpdate()` when scheduling that cleanup: adding a post-frame callback alone does not request a frame. `HabitsOwnedOverlay` implements this ownership pattern. The CMS and Education workspace regressions also cover an editor beneath an unrelated modal and assert that the previous actor's content is gone without dismissing the unrelated route. See `cms_workspace_test.dart`, `education_workspace_test.dart`, and `habits_host_scope_test.dart` under `apps/mobile/test/features`. ## Test the real entry path, not only the destination widget A review sheet can render correctly in isolation while the page never exposes its action. Mount the actual host with its shell action publisher, invoke the published action, and assert the resulting widget and side effects. Inject a bounded repository or cubit result to avoid accidental provider calls, but keep the real host admission and navigation code in the test. Notes voice review distinguishes `canReview` from `canSave`: completed silence can explain the result, while Save stays disabled and no analysis is replayed. `notes/voice/notes_voice_host_review_test.dart` covers the published dock action; `notes_voice_widgets_test.dart` covers the review widget itself. Both are useful, but the latter alone does not establish reachability. For Assistant first-turn fixtures, establish new-conversation authority through the real history/New conversation confirmation path. A repository mock's outer creation flag does not acknowledge the page adapter's persisted unknown-creation hold. Keep setup free of creation POSTs and preserve actor/workspace admission; then exercise the actual page send and authoritative creation receipt. ## Cached display data is not a fresh authorization result A read-through repository can return a previously authorized snapshot while its network revalidation continues. That is appropriate for stale-while-revalidate rendering. It cannot authorize publication into a second cache or reauthorize content after a durable denial marker has been written. For an authorization-sensitive publication, request the repository's explicit fresh path and disable awaited transport fallback. Habits uses `requireFresh` for this purpose; its repository passes `forceRefresh: true` and `allowAwaitedTransportFallback: false` to `readThroughJson`. Preserve the actor, workspace, request token, and denial revision checks around reads and writes. A fresh read failure must not silently promote an older cached result. `habits/cubit/habits_snapshot_access_test.dart` covers transport fallback, denial serialization, and scoped publication. The product policy is documented in [Mobile workspace tools](/platform/features/mobile-workspace-tools). ## Distinguish verification from definitive denial An HTTP status alone does not distinguish an MFA challenge from revoked access. Check both the typed verification flag and the `MFA_REQUIRED` error code when that endpoint's contract supports them. Keep the existing scoped view during a challenge and show its message; a later definitive denial still clears retained rows and selection. Do not treat retaining the view as permission to bypass the challenge for a mutation or a new request. The Drive page regression exercises both challenge forms through actual refresh and then verifies clearing on a definitive 403. See `drive/drive_page_test.dart` and the [Drive policy](/platform/applications/drive). ## Attach handlers to independent startup tasks together Independent prerequisites can run concurrently, but sequentially awaiting already started futures can leave the second failure unhandled while the first is still pending. Attach both to a shared `Future.wait` before awaiting either. Its default settle-all behavior keeps the owned attempt pending until both finish. Connect only after both succeed, then recheck the actor/session/request generation. Mira Live binds history and initializes native audio this way before opening the socket. `assistant/assistant_live_startup_phases_test.dart` verifies overlap, settle-all failure handling, and scope cancellation. Duration-only diagnostics show completed phases; concurrent phase durations must not be summed as a total. ## Keep validation claims scoped Focused host, state, and cache regressions establish these source behaviors. Run the required full mobile checks for implementation changes, then use CI for native builds. A mocked provider result or a passing widget suite does not prove authenticated device behavior, provider billing, installed model inference, or production delivery. Record those missing gates separately from the code fix. ## Close offline queue admission before awaiting cleanup Disposing an offline mutation queue permanently closes that instance. Mark it closed before awaiting subscription cancellation, and check closure after connectivity, initialization, and dependency reads before starting another request or refreshing cached resources. Disposal must not await an unresolved connectivity future. A delayed old queue must not refresh resources registered by a replacement queue, including when the same actor returns after logout. An already dispatched write retains acknowledgment reconciliation so disposal cannot make an accepted write replay. Reconcile only its owned pending record; skip subsequent dispatch, retry timers, revalidation, and visible refresh after closure. A new queue remains able to synchronize the current actor's resources. `offline_queue_disposal_regression_test.dart` covers held connectivity, actor ABA, disposal before/during initialization, replacement queues, and admitted ordinary and inventory acknowledgments using a real scoped temporary cache. ## Await owned Assistant stream cancellation Workspace retirement, explicit Stop and Cubit closure must capture and detach the intended current subscription and admission before awaiting any cleanup. Track pending cleanup per operation: joining an older reply's cancellation does not cancel a replacement reply. A caller joins only the operations captured at its entry; it must not rescan after an await and cancel a later replacement. Concurrent callers without a replacement still join the same pending cleanup. A pre-dispatch rejection can surface through the cancellation future rather than its detached listener. Handle only a typed definitely-not-dispatched failure whose admission is identical to that subscription's captured admission; retain unexpected failures and ambiguous dispatched outcomes. Cancellation does not supply a saved-USER acknowledgment or authorize a replacement request. `assistant_native_dispatch_v116_test.dart` covers held credentials across actor retirement and concurrent Stop calls, zero retired POSTs and a fresh next turn. Its public-history replacement controls hold A cleanup, restore and start B, then verify Stop/close owns B cleanup while the older A caller cannot cancel B or wait for B's later cleanup. Those cleanup ports are synthetic streams, not live HTTP cancellation proof. Selection fixtures must use uploaded attachment paths owned by the selected chat. `assistant_chat_creation_selection_test.dart` checks the actual outbound attachment tuple and saved-USER proof before clearing the retained composer. Native history attachments use the Chat wire response keys `storagePath`, `filename`, `contentType` and `sizeBytes`. Do not substitute the Assistant stored-file `path`/`name`/`type`/`size` shape: native restore reads `filename` and otherwise uses its fallback label. Keep the original filename, storage path and tuple assertions. These controls require actual mobile CI execution; source preparation alone is not a passing test result. Byte-backed `XFile.fromData` fixtures need a nonempty `path` as well as `name` when testing filenames on the IO implementation. Assert the uploaded name and outbound summary rather than accepting an empty attachment label. ## Keep asynchronous Assistant fixtures in their owning zone Widget callbacks run in fake async, while a fixture may use real Hive/file I/O. Locate the actual blocked await before changing scheduling. When real queue writes must progress independently of fake microtasks, capture `Zone.current` eagerly inside the existing `tester.runAsync` creation scope and admit those writes there. Keep this fixture-only and opt-in, delegating the same record, scope callback and real queue implementation. Preserve close/disposal waits and all persistence assertions; wrapping only the later close does not drain an earlier fake-owned write. A reduced Hive/zone control establishes a possible mechanism, not the hosted writer's identity or complete widget acceptance. Capture scheduling ownership before entering fake callbacks; a lazy zone initializer can capture the wrong zone on its first read. Awaiting an already-completed fake-owned Future from `runAsync` does not necessarily advance its source-zone continuations. A reduced cross-zone probe must attach its caller-owned completion bridge while the source is still pending and record explicit final completion; an empty exit-zero process is not proof that its awaited work finished. Page workspace bootstrap may disconnect a supplied Live socket. Open a controlled socket after that owned cleanup and assert its setup frame before testing sink admission. A seeded connected Cubit state alone does not establish an open sink. Create real file/cache operations and socket cancellation inside `runAsync`; passing a previously created fake-zone Future into it does not move its work. Keep the original dispatch, filename, ACK and retirement assertions. When advancing reconnect and readiness timers in a widget test, construct the Cubit inside that widget body. Ordinary `setUp` can put socket listeners and reconnect timers in a real zone while the watchdog runs in fake time. Hold and observe the actual replacement preparation so the clocks cannot conceal request supersession. `assistant_live_typed_dispatch_test.dart` covers this boundary. A retired send must leave shared setup in a terminal state, or under a newer preparation owner. Assert that state after abort and fence cleanup publication across native awaits. Count preparation attempts before retry or screen teardown awaits, so abandoned cleanup preserves a newer attempt before request admission. The latest rejected attempt settles inherited setup; teardown rechecks that ownership after native awaits. Notice guards must use the requested selection while a restore is pending; the previous loaded chat snapshot is not selection proof. `assistant_live_preparation_cases.dart` retains both same-chat recovery and no-notice controls for another conversation. Manual End needs a monotonic retirement fence independent of automatic recovery; resetting a connection flag must not revive a cancelled send during replacement preparation. Actor-ABA stream controls must establish that the actual response body is subscribed before retiring the actor/epoch. Complete an explicit handshake inside the existing `onListen`, after its prior callback, and await it before the actor change. Request creation alone does not prove subscription. Keep the original request tuple, receipt/body assertions, zero forbidden effects and cleanup after retirement. Shell fixtures must provide the getters used by their real page consumers. `MockCubit` does not supply a valid `isClosed` value automatically: stub an open AuthCubit explicitly when mounting Assistant. Preserve navigation and geometry assertions instead of ignoring the resulting page-build exception. For auth transitions, assert immediate revocation and await the actual source notification before requiring restored session identity. ## Measure the Mira composer after layout Mira text entry grows from one to five lines. Enter inserts a newline; the side primary action sends the draft. Keep the existing controller, focus, attachment, and restoring-state admission when adjusting this widget. The composer reports its rendered height to the assistant page. Use that height for transcript padding and scroll-to-bottom clearance, including the shell surface border. A one-line text estimate cannot cover wrapped drafts or enlarged text inside the published shell slot. Keep the system safe area separate from an IME inset already consumed by the shell Scaffold. The page publishes its dock slot after its frame; the shell consumes that slot in a subsequent build. Reopening a collapsed composer through the real FAB therefore has separate tap, page-visibility, slot-publication and mounted-dock boundaries. Assert that the launcher is hit-testable, observe the public composer state, then observe the published composing slot before driving its consumer frame and requiring exactly one dock. Derive frames from those boundaries, not arbitrary delays or a pump-until loop. A failed boundary needs evidence, not more pumping. Preserve the held request, draft controller and attachments; do not reach unmounted input State. Run `assistant_composer_multiline_test.dart` and `assistant_composer_geometry_test.dart` under `apps/mobile/test/features/assistant/widgets` before `bun check:mobile`. The geometry regression renders the real composer in `PersistentShellDock` and checks multiline growth, large text, keyboard and physical safe-area variants. These fixtures do not establish authenticated native-device behavior. ## Recover a hosted Dart formatting failure The existing Mobile Analysis job runs the configured hosted Flutter SDK after its app-root `flutter pub get --enforce-lockfile` dependency step. The app's lock and path overrides resolve vendor libraries; quality does not independently run pub-get inside vendor packages or their examples. Standalone vendor resolution can change a tracked vendor lock before formatting, invalidating preimages. A lock mismatch must fail dependency installation; do not restore changed locks, ignore dirty paths or relax capture guards. Other mobile jobs keep their own install/validation contracts. A failed `dart format --set-exit-if-changed lib test` still fails quality and prevents analysis. Its three-day `mobile-format` failure artifact contains only a bounded tracked-Dart patch and a digest/version receipt; it does not contain raw package configuration, environment or authentication data. No patch is committed or applied automatically, and artifact existence is not a format, analyzer, test or runtime pass. Before using a patch, verify its GitHub run/job and reviewed workflow identity, then require `producerValid=true`, an ordinary formatter exit1 with no bound failure, stopped owned processes and the expected source/context identities. The tested checkout commit may be a synthetic PR merge: compare `testedCommit`, `eventCommit` and the separate `prHeadCommit` with the actual run metadata rather than treating them as interchangeable. A missing/invalid receipt is a hold. Schema1 retains class/phase `captureErrors`; optional `captureErrorCodes` adds only exact allowlisted producer constants with their phase. Unknown exceptions retain class/phase only. Use a captured safe code to diagnose a failure; never bypass validation or infer its cause from a generic class alone. `preformatIdentityMatches` separates checkout identity from dirty-source evidence. `preformatTrackedChanges` contains only a capped count and SHA256 hashes of raw NUL-delimited relative tracked path bytes, never names or diffs. Match hashes offline against the public tested Git tree. Incomplete/invalid snapshots remain a hold; `countTruncated=true` means more than 256 paths and no hashes are emitted. Check patch byte length/SHA256, every changed path and full old/new blob IDs. Only existing tracked regular `.dart` files under `apps/mobile/lib` or `apps/mobile/test` are allowed. Compare each old SHA256/blob/mode with the actual application target before any separately authorized `git apply --check`; reject wrong-source, tampered, out-of-scope, new/deleted/renamed or mode-changing input. A matching PR number or commit title does not establish preimage equality. The receipt's digest is not an independent signature: retrieve it through the trusted exact run and verify the producer workflow/context, not only its claims. The producer bounds input to 4096 tracked Dart files / 32 MiB (2 MiB per file), changed paths to 256, patch to 4 MiB, receipt/output captures to 1 MiB, and the actual formatter to 180 seconds. Bounds or capture failures retain the original formatting failure and cannot qualify a patch; missing capture/upload requires another admitted source-bound run. Context records hashes of source/lock/analysis/package context, resolved analysis includes and actual tool identities, never their raw contents. Keep a local formatter result separate when its SDK/package context differs. When an owned checkout lacks package configuration, derive its language version from `apps/mobile/pubspec.yaml` and pass it explicitly to the formatter, such as `dart format --language-version=3.10` for the current package. Limit formatting to the owned path manifest; still require analysis and tests in the resolved project context before accepting the change. After separately reviewed application, require clean exact-head format and analysis CI; retain independent compiler, behavior, SQL and delivery gates. ## Keep widget fixtures current Flutter verifies semantics handles and debug platform overrides before registered `addTearDown` callbacks run. Dispose handles and restore overrides in a `try/finally` inside the test body, including when an assertion fails. Keep the accessibility and platform-specific assertions intact. Transport changes also affect widget fixtures. Exercise the current operation receipt and read shape rather than stubbing a retired method: a mock that never receives the request can leave a held-response test waiting indefinitely. Assert that the expected request entered before awaiting its completion, and retain stable operation identity, uncertainty and duplicate-write checks. The Assistant message-action and transcript review regressions cover these boundaries. ## Prepare chat qualification declarations The opt-in chat SQL-to-Web-to-Dart qualification compiles the actual Web route and its transitive imports. In CI, the selected source builds the existing `@tuturuuu/types` and `@tuturuuu/supabase` workspace declarations before the unchanged explicit bridge `tsc --noEmit` step. The maintained Turbo build graph orders dependencies with `^build`; the chat-only step uses one worker and emits normal ignored `dist` outputs. Other profiles keep their existing setup. Missing public declaration artifacts are a setup failure before SQL, not SQL qualification. Build success still requires the full compiler gate; remaining type errors must be diagnosed independently. Do not replace exports with source aliases or declaration stubs, weaken compiler checks, or treat absent phase artifacts or an empty cleanup as a completed database qualification. The Web bridge selects its existing `tsconfig.chat-initial-settings.json` explicitly through Vite’s top-level `tsconfig` option. This keeps transforms under the same owning configuration as the explicit compiler, including when the installed source checkout sits beneath a separate control checkout. A missing-config transform error before test collection is a Web setup failure; it does not qualify SQL or either consumer. Preserve the actual route imports, full compiler checks, and all nine Web and nine Dart cases, and require the source-bound hosted qualification after correcting configuration ownership. ## Web bridge process failures after passing tests A successful nine-test JSON report does not qualify a child that aborts or fails to stop cleanly. The bridge records the actual status, signal, timeout and group cleanup before enforcing its existing exit gate. Its separate process-output summary contains only fixed panic, assertion, memory, fatal or unknown indicators, per-stream byte counts and SHA256 digests, with 64-KiB tails and explicit truncation. Counts saturate at 1 GiB per stream; a saturation flag preserves that limit while streaming hashes cover the received bytes. No stdout, stderr, paths, environment, SQL payloads or arbitrary exception text are retained. Existing build diagnostics keep their prior category/code/path projection. The bridge validates the summary before accepting a successful Web phase. Missing or malformed summary cannot qualify. Fixed indicators are evidence hints, not causal attribution; an unknown signal remains unknown without further evidence. Preserve source/control/run binding, all exit/cleanup gates and the independent SQL, Web and Dart phases. Do not substitute a passing report for exit zero, change runtimes or IPC, or retry qualification blindly. Require reviewed source and a separately admitted exact-source hosted attempt for actual process evidence. ## Opt in to a chat process comparison without qualifying a retry `diagnose_chat_process` defaults to false and is accepted only with standalone `include_chat_initial_settings=true`, with all other optional profiles and structured provenance false. It does not change the authoritative Bunx attempt. Only a stopped, non-timeout original SIGABRT with nine valid passing Web controls, matching actual process identities and the unchanged SQL-produced envelope may launch one sequential direct setup-Node24 comparison of the locked source Vitest entry. Both arms retain the same config, controls, one-worker policy, owned process-group cleanup and 120-second bound. They use separate report/result and identity artifacts, with no second SQL producer or database lifecycle. Config and fixture code record actual runtime versions, executable and Vitest-bin digests, actual per-role entry digests, bounded process ancestry and only NODE\_OPTIONS presence/digest. The config entry must be the source-owned Vitest bin, and each role’s entry digest must agree across arms. Identity records are owned 0600 regular nonlink files, source/control/run/envelope-bound, and bounded to 2 KiB each; the fixed diagnostic receipt is bounded to 8 KiB. No raw arguments, paths, environment, output, SQL or arbitrary errors are emitted. Missing or unequal identity, timeout, malformed artifacts or cleanup uncertainty makes the comparison invalid. Command spelling does not identify the interpreter. The original abort remains failed, with Web0/Dart0 and qualification false even if the comparison passes. No Dart follows the original failure, no comparison is a fallback or causal repair, and no further launch is allowed. A diagnostic result only narrows the launcher/interpreter/teardown hypothesis; the signal source remains unknown until separately proven. Require reviewed clean source and control pins and an independently admitted hosted attempt. The synthetic `chat-process-diagnostic.test.mjs` ports are orchestration regression coverage, not actual SQL, Vitest, runtime or provider qualification. ### Diagnose the selected Dart bridge without exposing its output Only the opt-in chat bridge's single Flutter child uses the JSON test reporter and bounded event projection. The runner attaches its summary before checking the actual exit, including a failed exit. Native stream byte counts, hashes and truncation remain available. Other process callers retain their existing behavior. The projection retains the nine source-owned case labels, fixed assertion, non-assertion, compiler-diagnostic or load-diagnostic classifications only when observed in an error event, and bounded error/stack byte counts and digests. Raw errors, stacks, unknown test names, paths, prints, environment and SQL are omitted. A compiler/load label describes observed text, not a proven causal phase. Each stream tail is limited to 64 KiB; the projection accepts at most 256 events, 32 test IDs, 4 suite IDs, 4 error digests and 8 KiB of serialized diagnostics. Unknown, malformed, overflow, truncated, late/interleaved and incomplete events retain explicit evidence limits. Missing/reordered events do not establish setup or teardown attribution. A passing JSON stream never replaces child exit zero, source/control/run/envelope binding, the actual exact-nine result or cleanup. Diagnostic capture is evidence repair; only a separately admitted exact-source hosted attempt can establish the Dart failure or strict 197/9/9 qualification. # Monorepo Architecture Source: https://docs.tuturuuu.com/build/development-tools/monorepo-architecture Understanding Tuturuuu monorepo architecture and the benefits of using Turborepo. > [!NOTE] > > **Prerequisite**: You should have followed the > [Development](/build/development-tools/development) setup guide to understand > the basic structure of the codebase. ## What is a Monorepo? A monorepo is a single repository containing multiple distinct projects with well-defined relationships. This approach differs from a polyrepo strategy where each project has its own separate repository. > "A monorepo is a single repository containing multiple distinct projects, with well-defined relationships." > — [monorepo.tools](https://monorepo.tools/) It's important to note that a monorepo is not the same as a monolith. A monolith is a single, tightly coupled application, while a monorepo can contain many independent applications, libraries, and tools that can be deployed separately. ## Why Tuturuuu Uses a Monorepo Tuturuuu's platform is built as a monorepo (`@tutur3u/platform`) for several key reasons: ### 1. Code Sharing Without Overhead Our monorepo structure makes it easy to share code across different applications and services without the overhead of publishing packages. Core components, utilities, and business logic can be shared across multiple applications without duplicating code. ``` platform/ ├── apps/ │ ├── web/ # Main web application │ ├── tanstack-web/ # TanStack Start migration frontend │ ├── backend/ # Rust API runtime for migrated web APIs │ ├── cms/ # Dedicated Tuturuuu CMS satellite app │ ├── docs/ # Documentation site (Mintlify) │ ├── rewise/ # Rewise application │ ├── nova/ # Nova application │ ├── calendar/ # Standalone calendar application │ └── ... # Other applications └── packages/ ├── ai/ # Shared AI functions & configurations ├── ui/ # Shared UI components ├── types/ # Shared TypeScript types ├── utils/ # Shared utilities ├── supabase/ # Supabase client and utilities └── ... # Other shared packages ``` ### 2. Atomic Changes Across Projects When making changes that affect multiple parts of the platform, we can make those changes in a single commit. This ensures that all parts of the system remain compatible with each other, reducing integration issues. For example, when updating a database schema: 1. We can update the Supabase schema in `apps/database/supabase/migrations` 2. Update the TypeScript types in `packages/types/src/supabase.ts` 3. Update all affected applications in the same pull request ### 3. Consistent Developer Experience A monorepo allows us to standardize tooling, testing, and deployment processes across all projects. This creates a consistent development experience regardless of which part of the platform a developer is working on. All developers use the same: * Package manager (bun) * Build system (Turborepo) * Code style guidelines (enforced by Biome) * Testing framework * CI/CD pipelines ### 4. Simplified Dependency Management Managing dependencies in a monorepo is simpler because we can ensure all projects use the same versions of shared dependencies, avoiding version conflicts. ## Why We Chose Turborepo Tuturuuu uses [Turborepo](https://turborepo.org/) as its build system for several reasons: ### 1. Incremental Builds with Caching Turborepo provides intelligent caching of build artifacts. This means that if a file hasn't changed, Turborepo will use the cached result instead of rebuilding it, significantly reducing build times. ```bash theme={null} # Only builds what changed since the last build bun run build ``` ### 2. Parallel Task Execution Turborepo automatically parallelizes tasks, maximizing the use of available CPU cores. ```bash theme={null} # Runs lint across all packages in parallel bun lint ``` ### 3. Task Orchestration Turborepo understands dependencies between packages and ensures tasks are run in the correct order. ```json theme={null} // In turbo.json (Turbo 2.x uses "tasks", not the older "pipeline") { "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**", "!.next/cache/**", "!.next/dev/**"] } } } ``` Keep local Next development state out of Turbo build outputs. The `.next/dev` tree belongs to `next dev`; archiving it can capture multi-GB Turbopack caches and make `bun dev:web` slower. When local `apps/web` compilation feels slow, run the dev-speed diagnostic before changing code: ```bash theme={null} bun diagnose:dev:web ``` It reports generated cache sizes, Next slow-filesystem warnings, and the slowest `.next/dev/trace` spans from the previous `next dev` session. It also flags watcher `EMFILE` errors; the web dev launcher raises the child process open-file limit to `65536` by default, and unusual local shells can override that with `TUTURUUU_DEV_MAX_OPEN_FILES`. Native local dev leaves `WATCHPACK_POLLING` unset by default for lower watcher overhead; set `WATCHPACK_POLLING=true` only when a local filesystem or container workflow needs polling to keep route discovery reliable. ### 4. Remote Caching For CI/CD pipelines, Turborepo supports remote caching, which means build artifacts can be shared across different environments. ## Monorepo Structure Our monorepo is organized into several main directories: ### apps/ Contains all deployable applications. Each application is a standalone project that can be deployed independently. ``` apps/ ├── web/ # Main web application and central API host ├── tanstack-web/ # TanStack Start frontend replacing apps/web ├── backend/ # Dedicated Rust API runtime for migrated web APIs ├── cms/ # Dedicated Tuturuuu CMS frontend ├── docs/ # Documentation site (Mintlify) ├── rewise/ # Rewise application ├── nova/ # Nova application └── database/ # Database migrations and scripts ``` `apps/tanstack-web` and `apps/backend` are the migration pair for replacing the legacy `apps/web` runtime. The TanStack app owns migrated frontend routes and Start server functions; the Rust backend owns migrated API, job, cron, and private/admin call paths. Both apps can run in Docker for local and blue/green rehearsals, and both have Cloudflare Worker preview entrypoints: `apps/tanstack-web/wrangler.jsonc` and `apps/backend/wrangler.jsonc`. Cloudflare preview is incremental, not the production cutover. Bind `BACKEND_INTERNAL_TOKEN`, `BACKEND_PUBLIC_ORIGIN`, and `BACKEND_INTERNAL_URL` through Wrangler or Cloudflare secrets, run `bun check:cloudflare` before preview deployment, and keep `apps/web` serving production until the TanStack/Rust route manifest, Docker E2E compare report, benchmark report, and migration gates pass. ### packages/ Contains shared libraries and utilities that are used by multiple applications. ``` packages/ ├── ui/ # Shared UI components ├── types/ # Shared TypeScript types including Supabase types ├── utils/ # Shared utilities ├── ai/ # AI-related utilities └── supabase/ # Supabase client and utilities ``` ## Development Workflow When working in our monorepo, you'll typically: 1. Clone the repository: `git clone https://github.com/tutur3u/platform.git` 2. Install dependencies: `bun install` 3. Start the development servers: `bun dev` or `bun devx` (with Supabase) Satellite apps can also be started with app-specific scripts when you only need one frontend plus the shared `web` API host. For example, `bun dev:cms` runs the CMS app together with the shared packages and `apps/web`. For more detailed instructions on development workflow, see the [Development](/build/development-tools/development) and [Local Supabase Development](/build/development-tools/local-supabase-development) guides. ## Best Practices When working in our monorepo, follow these best practices: ### 1. Keep Projects Modular Even though all code lives in one repository, maintain clear boundaries between projects. Each package or app should have a well-defined purpose and API. ### 2. Use Workspace References When one project depends on another within the monorepo, use workspace references: ```json theme={null} { "dependencies": { "@tuturuuu/ui": "workspace:*", "@tuturuuu/types": "workspace:*" } } ``` ### 3. Think About Build Order Be mindful of dependencies between packages. If package B depends on package A, ensure that changes to package A trigger rebuilds of package B. ### 4. Use Correct Scope for Changes * For changes that affect a single application, focus your changes there * For changes that affect multiple applications, consider extracting common code to a shared package * For global changes (like tooling updates), ensure you test across all affected projects ## Common Challenges and Solutions ### Challenge: Long Build Times **Solution:** Turborepo's caching mechanism helps, but also: * Be selective about what you build during development * Use `bun --filter` to focus on specific packages ```bash theme={null} # Only build the web app and its dependencies bun --filter web... build ``` ### Challenge: Dependency Management **Solution:** * Regularly update dependencies * Bun automatically detects workspace packages from the root `package.json` workspaces field * Consider using `bun dedupe` to eliminate duplicate dependencies ### Challenge: CI/CD Pipeline Complexity **Solution:** * Use GitHub Actions workflows that are specific to the affected parts of the codebase * Leverage Turborepo's `--filter` and `--since` flags to only build what's changed ## Further Resources * [monorepo.tools](https://monorepo.tools/): Comprehensive information about monorepos * [Turborepo Documentation](https://turbo.build/repo/docs): Official Turborepo documentation * [Bun Workspace](https://bun.sh/docs/install/workspaces): How bun handles monorepos # Node.js Runtime Pin Source: https://docs.tuturuuu.com/build/development-tools/node-runtime Single-source Node.js version, where it applies, and the platforms that intentionally run a different version. The repository pins Node.js **26.12.1** in the root `.node-version` file. It is the 2026-10-21 security release of the Node 26 Current line, which is not LTS. Treat `.node-version` as the source of the requested version. Installing that version and activating it are separate steps. Version managers need the setup below; a plain `node` command can still resolve an older installation. The workflow migration and its guard are pending in [PR #6496](https://github.com/tutur3u/platform/pull/6496). Until that layer lands, this pin alone does not change CI runtime selection. The CI behavior and receipts described below are the intended acceptance criteria for that layer. The `engines.node` ranges are support floors, not runtime pins. The root `>=22.13.0` floor stays because Vercel maps `>=22` to its newest supported major. The `>=18.0.0` ranges in published packages are consumer contracts. Raising either range to 26 would break Vercel deployments and package consumers without changing the CI runtime. ## Local setup On a personal machine, install the pinned version side by side, activate it, and verify the version in the same shell that will run your commands. For [fnm](https://github.com/Schniz/fnm#shell-setup), initialize the shell and select the version explicitly: ```bash theme={null} eval "$(fnm env --use-on-cd --shell zsh)" # use --shell bash for Bash fnm install "$(cat .node-version)" fnm use "$(cat .node-version)" node --version # v26.12.1 ``` The `--use-on-cd` hook switches to `.node-version` when entering the repository. Installing a version without selecting it or enabling that hook is insufficient. [mise version-file support](https://mise.jdx.dev/lang/node.html#version-files) is opt-in. On your personal machine, enable Node version files with `mise settings add idiomatic_version_file_enable_tools node` (this changes mise's global configuration). With mise's shell integration active, it then selects the repository's `.node-version`. Explicit execution works without relying on shell activation: ```bash theme={null} mise install node@"$(cat .node-version)" mise exec node@"$(cat .node-version)" -- node --version ``` nvm does not automatically read `.node-version`. On a personal machine, run `nvm install "$(cat .node-version)"`, then `nvm use "$(cat .node-version)"`, and check `node --version`; automatic directory switching requires a separate hook. [Volta project pins](https://docs.volta.sh/guide/understanding#pinning-node-engines) use the `volta` field in `package.json`, which this repository does not supply. Do not assume Volta reads `.node-version`. Use an explicit one-command override, `volta run --node "$(cat .node-version)" node --version`, or one of the managers above instead of adding a second repository pin. The devbox bootstrap remains distribution/LTS-based; it does not activate `.node-version`. Select the private toolchain explicitly when validating on a devbox, just as on other shared hosts. ### Shared agent hosts Several projects share agent hosts, so do not use `nvm install` there. Placing a build under `~/.nvm/versions/node` changes what nvm's major alias (for example `26`) resolves to for every new shell, even when the alias file stays the same. Instead: 1. Download `node-v-.tar.xz`, `SHASUMS256.txt` and `SHASUMS256.txt.sig` from `https://nodejs.org/dist/v/` into a new empty directory. 2. Check the SHA-256 of the tarball against `SHASUMS256.txt`. Verify the signature with the [Node.js release keys](https://github.com/nodejs/release-keys) in a temporary `GNUPGHOME`. 3. Extract the tarball to `~/.local/share/tuturuuu/toolchains/node-v-/`. 4. Call that `bin/node` by absolute path, or prepend its `bin` to `PATH` for your own commands only. For queued work, use `ttr resources run -- /bin/node …`. Do not change any of the following, which other sessions and projects share: * nvm's aliases or `~/.nvm`; * the `ttr resources` shims; * Homebrew or `/usr/local` Node; * runner tool caches; * shared setup scripts or shell profiles. If a change really needs one of these, stop and get the owner's approval. `bun doctor` still enforces the Node 22 floor rather than the exact pin. ## Where 26.12.1 applies after #6496 | Surface | Version | Source | | - | - | - | | GitHub Actions `setup-node` steps | 26.12.1 | `node-version-file: .node-version` | | Root script tests, SDK and package tests, Turbo tasks in CI | 26.12.1 | inherits the setup step | | Desktop beta and store packaging jobs (Flutter desktop plus Node TypeScript scripts) | 26.12.1 | inherits the setup step | | Dev container | 26.12.1 | `.devcontainer/devcontainer.json` node feature | | Local development | 26.12.1 | `.node-version` | A job reads `node-version-file` only when its checkout is the workflow's own commit (no `ref`, or `${{ github.sha }}`) and is not sparse. The file then comes from the same tree as the workflow. Other jobs pin the literal `26.12.1`: * jobs that set up Node before any checkout, such as package publish and dispatch; * sparse duplicate-validation checkouts; * jobs that check out a PR head, the `production` branch, a workflow-run head or an input SHA, because that tree may predate `.node-version`. The guard proposed in #6496 (`scripts/ci/node-version-pin.test.js`) requires every literal to equal `.node-version` and every exception to be listed. It also requires each `test "$(node --version)" = v…` workflow assertion to name the pin. Each setup step logs the resolved version. Two receipts bind `node --version` to the exact source SHA: * the SDK qualification receipt (`runtimeVersions.node` with `sourceSha`); * the Calendar native artifact receipt (`node` with `sha`). ## Intentional exceptions | Surface | Runs | Why | Patched fallback | | - | - | - | - | | `employee-schema-contract.yaml` | Node 24.22.1 | It qualifies an input source SHA whose chat qualification binds a Node 24 runtime identity (`chat-initial-settings-qualification.mjs`). | 24.22.1 is the latest patched 24.x (2026-10-20 security release). | | `supabase-staging.yaml` | Node 24 (floating patch) | `supabase-production.yaml` admits staging completions only from allowlisted git blob SHAs of this file. #6496 preserves its bytes; any later setup-step change needs a coordinated gate update. | A floating `24` can use a cached patch; pinning 24.22.1 or adding `check-latest: true` needs that coordinated update. | | Vercel Functions and the `vercel build` / `vercel deploy` workflows | Node 24.x | [Vercel supports only 24.x (default) and 22.x](https://vercel.com/docs/functions/runtimes/node-js/node-js-versions). The CI build should match the function runtime it produces. | The current floating `24` may use a cached patch. #6496 pins Vercel CI jobs to 24.22.1; Vercel patches its function runtime independently. | | Cloudflare Workers | workerd, not Node | Workers implement Node APIs through the `nodejs_compat` flag and `compatibility_date`. That date is set in each `wrangler.jsonc`; the Node version is not configurable. | Advance `compatibility_date` deliberately with each Worker's acceptance tests. | | Supabase Edge Functions | Deno | The repository ships no Edge Functions today. Supabase Edge would run Deno, not Node. | Not applicable. | | JavaScript actions (`runs.using`) | Runner-embedded Node 24 | [Action metadata accepts only `node20` and `node24`](https://docs.github.com/en/actions/reference/workflows-and-actions/metadata-syntax). `setup-node` does not change it. | GitHub maintains the runner-embedded runtime. | | Local docs (`bun dev:docs`, docs validation) | Node 24.19.0 through `npx` | Mintlify CLI 4.2.520 rejects Node 25+, and its Got 12.6.1 connectivity probe crashes on Node 24.20+ (see [Mintlify runtime compatibility](/build/development-tools/documenting#mintlify-runtime-compatibility)). The host can run 26.12.1; the wrapper launches an isolated Node 24.19.0. | Upgrade Mintlify to a CLI that accepts Node 26 and reassess the Got defect, then move the pin. | | Docker images (`apps/web`, `apps/hive`, `apps/docs`, `apps/web/docker/native-runner.Dockerfile`) | `node:24-bookworm-slim` | The Docker runtime is paused (see [active runtime policy](/build/devops/active-runtime)), and `scripts/check-docker-web.js` enforces the retained builder stage. | On resumption, use `node:26.12.1-bookworm-slim`, digest `sha256:28147155d47dc31abe0a46e0d12b59b2fb3d0a2dcc5dc65fce76a074180b16fe` as of 2026-10-21. | | `apps/tanstack-web`, `apps/backend` | Unchanged | These runtimes are paused and their retained source is not updated. | Not applicable. | | Flutter (`apps/mobile`) | Flutter/Dart SDK | Flutter tooling does not use Node. Only the Node scripts around it follow the pin. | Not applicable. | | Electron | Not used | No workspace depends on Electron; desktop builds use Flutter. | Not applicable. | | `.idx/dev.nix` | `nodejs_20` | This Firebase Studio template depends on the nixpkgs channel that template pins. | Update it when that channel ships a Node 26 attribute. | A floating `node-version: 24` does not guarantee the newest patch: [setup-node defaults `check-latest` to false](https://github.com/actions/setup-node/blob/main/docs/advanced-usage.md#check-latest-version) and first accepts a matching cached release. An exact patched version gives a reproducible CI selection; `check-latest: true` instead requests the newest matching release. Neither proves which patch a deployed function runs. ## Native addons Node 26 uses `NODE_MODULE_VERSION` 147 and N-API 10. sharp, bcrypt, better-sqlite3, esbuild, lightningcss, `@swc/core`, `@napi-rs/canvas`, `@napi-rs/keyring` and `node:sqlite` all load and run on 26.12.1. better-sqlite3 is the only one tied to a V8 ABI, so reinstall it after a major Node change. ## Updating the pin 1. Read the target from `https://nodejs.org/dist/index.json`, including its `security` and `lts` fields. 2. Change `.node-version`, plus any pre-checkout `node-version` literals the workflow contract tests list. 3. Run the root script tests and the affected package tests with that exact binary through `ttr resources run`. 4. Check the CI setup logs and the SDK qualification receipt for the new `node --version` on the exact commit. 5. Re-check every exception above against its upstream documentation. # Task notification email freshness Source: https://docs.tuturuuu.com/build/development-tools/notification-email-freshness Current due-date eligibility for immediate and digest task emails. Task notification emails are skipped when the current task due date is strictly more than 24 hours before the delivery check. Exactly 24 hours overdue, future due dates and tasks without a due date remain eligible under the existing recipient, notification-age and preference checks. This is an instant-based cutoff, not a calendar-day or workspace-timezone comparison. The dispatcher loads the current task through its task list and workspace board, binding both the notification task ID and workspace ID. It does not use the queued due-date snapshot for eligibility. A task rescheduled into the future is therefore not skipped because its notification contains an older due date. Immediate and digest dispatchers recheck every email item before rendering. Mixed digests omit overdue items and build their subject and content from the remaining items. An all-overdue batch completes with skipped delivery logs and makes no email provider request. The skip reason is `skipped: task_due_older_than_1_day`. A tenant-scoped lookup that succeeds with an explicitly null task row is a known unavailable task (for example, deleted or moved). It is skipped with `skipped: task_unavailable`, so other eligible items in a mixed batch can proceed. Undefined or malformed lookup results remain unknown and fail closed. Skip and settlement writes must succeed before the dispatcher continues. If an email provider was already attempted, a later settlement error remains an unknown outcome for reconciliation; the digest is not marked retryable or automatically resent. A provider `success: false` result can represent a transport timeout and does not prove rejection. Existing explicit suppression reasons still settle as skipped; other unsuccessful or thrown provider outcomes require reconciliation. The same settlement protection applies to push delivery because the write helpers are shared: accepted or ambiguous push attempts remain in processing for reconciliation. Only zero deliveries with every device rejected as invalid proves no push was accepted. Push freshness eligibility is unchanged. Missing task/workspace bindings, conflicting payload task IDs, tenant mismatch, lookup errors and invalid current due dates fail before delivery. They are not recorded as a proven overdue skip. Existing delivery-attempt and ambiguous provider-result handling remain in place; freshness is not reevaluated after a provider attempt. Push and non-task notifications retain their existing checks. For source validation, run the focused notification freshness and dispatcher tests, then `bun validate` and a real web app build through the resource queue. The digest route is first-class under `apps/web/src/app/api/cron/`; after route registration changes, run `node scripts/tanstack-migration-manifest.js generate` followed by `node scripts/tanstack-migration-manifest.js check` and `bun web:api-routes:check`. The package command `bun migration:tanstack:manifest` remains disabled while the alternate runtime is paused; source inventory generation does not resume that runtime. Rust route parity remains backlog and does not mean production traffic has moved. Source checks do not prove production activation or delivered email. No migration or manual notification replay is required by this change. ## Retained batch reconciliation The digest persists `delivery_in_flight` in the processing batch before calling a provider. The write must return the exact processing batch ID; zero updated rows or database errors prevent provider contact. A settlement or ambiguous attempt error writes the fixed `reconciliation_required` marker while keeping status `processing`. If that write fails, the cron returns an error; the earlier in-flight marker remains the durable unknown-outcome signal. Neither marker is a provider acceptance receipt. Each authorized digest cron also performs a bounded read-only inspection of up to 50 marked processing batches in the root workspace, with a separate two-second discovery budget capped by the overall processing deadline. The budget is checked before each query; an in-flight query may exceed it. It reports `reconciliation` entries with batch IDs, persisted log counts and `terminal_log_snapshot`. At most 1001 logs are read per batch; empty, oversized, pending, failed, missing-date or ambiguous-error snapshots never qualify. Null-workspace batches and older unmarked processing rows require separate tenant-bound operator inspection. This inspection never sends a notification, updates logs, resets a batch or automatically marks a batch sent. A true snapshot only means every observed log is already `sent` with a valid settlement timestamp and either no error or an explicit `skipped: ` reason. It is not provider acceptance or inbox arrival proof, and it cannot exclude a concurrently started queue insert. For an authorized reconciliation, first select the exact processing batch by ID and its marker through an authenticated, tenant-bound read-only client. Capture all attached delivery log IDs, statuses, timestamps and skip reasons. Resolve any active queue/dispatcher ownership before treating the snapshot as complete. Only when every log is already terminal sent/skipped, the complete batch membership is settled, and the corresponding outcome is known may an explicitly authorized operator prepare a separately reviewed batch-only status update, guarded by its ID, current processing status and unchanged marker. Use the complete log count; do not change delivery logs or contact a provider during that operation. The current helper has no conditional reconciliation guard; do not call it as a reconciliation shortcut. The cron does not provide that mutation capability. Pending logs after an attempted provider call require correlated provider outcome receipts and a separately reviewed settlement operation. Unknown/accepted outcomes remain held: do not use Retry, mark failed/pending, clear the marker, or resend. If receipts or complete membership are unavailable, retain processing and record the unresolved batch ID. This change adds no automatic unknown-outcome recovery, new schema or production migration. # Development Tools Source: https://docs.tuturuuu.com/build/development-tools/overview Tools and guides for developing on the Tuturuuu platform This section contains tools, guides, and best practices for developing on the Tuturuuu platform. ## Getting Started * [Development Setup](/build/development-tools/development) - Set up your development environment * [Monorepo Architecture](/build/development-tools/monorepo-architecture) - Understand the project structure * [Git Conventions](/build/development-tools/git-conventions) - Code contribution guidelines ## Development Workflow * [Local Supabase Development](/build/development-tools/local-supabase-development) - Database development setup * [CI/CD Pipelines](/build/development-tools/ci-cd-pipelines) - Continuous integration and deployment * [DevOps & Deployment](/build/devops/overview) - Operational runbooks for environments, workflows, Docker, and secrets * [Cleaning Clone](/build/development-tools/cleaning-clone) - Reset your local environment ## Documentation * [Documenting](/build/development-tools/documenting) - How to write and maintain documentation ## Development Guidelines ### Code Quality * Formatting and linting are handled by [Biome](https://biomejs.dev/) (`biome.json`), not ESLint or Prettier. Use `bun ff` to format, lint, and check in one pass. * Run focused tests, then the build-free `bun check:changed` gate before every push. `bun validate` is not run on agent machines because its Turbo graph can build workspace dependencies; it and app builds run in CI for the exact commit (see [pre-push checks](/build/development-tools/pre-push-checks)). * Default to Server Components in `apps/web`; add `'use client'` only for state, browser APIs, or interactivity. * Prefer typed expected errors and dependency services via `@tuturuuu/utils/effect` for new server/service orchestration. ### Git Workflow * Use [Conventional Commits](/build/development-tools/git-conventions) for commit messages and branch names. * The branch checker only accepts these prefixes: `feature/`, `feat/`, `fix/`, `bugfix/`, `hotfix/`, `release/`, `chore/`, `docs/`, `style/`, `refactor/`, `perf/`, `dependabot/`, and `claude/`. * Keep commits atomic and write descriptive PR descriptions. * Run `bun check:changed` before every push; it maps changed paths to the cheap CI gates without builds. See [pre-push checks](/build/development-tools/pre-push-checks). * Do not manually bump `TUTURUUU_PLATFORM_VERSION`, package versions, or changelogs — Release Please owns version updates. ### Testing * Tests run with Vitest. Use `bun test` for the full suite or filter to a single workspace (for example `bun --filter @tuturuuu/web test`). * Write unit tests for utilities and cover error scenarios. * Run focused tests before the full `bun validate` gate. ### Performance * Default to server-side rendering and Server Components where possible. * Use TanStack Query for client-side fetching and caching; avoid `useEffect` for data fetching. * Minimize bundle size and profile performance regularly. ## Tools and Scripts ### Package Management Dependencies are always installed from inside the owning workspace — never by hand-editing the root `package.json` and never with a `--workspace` flag. ```bash theme={null} # Install all dependencies bun install # Add a dependency to an app (e.g. apps/web) cd apps/web && bun add # Add a dependency to a shared package (e.g. packages/ui) cd packages/ui && bun add # Update dependencies within their current ranges bun update-all # Upgrade workspace dependencies to the latest releases, including major versions bun update --latest --recursive ``` For a broad refresh, also update Rust (`cargo update` in `apps/backend`), Python (`uv lock --upgrade` in `apps/discord`), and Flutter (`flutter pub upgrade --major-versions` in `apps/mobile`). Keep native CocoaPods locks aligned with Flutter plugins and retain stable versions when a registry's `latest` tag points at a prerelease. The configured npm release-age policy still applies. Run `bun check:changed` locally and require `bun validate` and affected app builds to pass in CI for the exact commit. Keep React and React DOM aligned at 19.2.8 while React Three Fiber 9.7.0 requires versions below 19.3. The root overrides also prevent transitive peer dependencies from selecting a second runtime; revisit both overrides and Hive workspace constraints when Fiber supports the next React minor. Keep `@types/react` and `@types/react-dom` on the matching runtime minor as well. Other packages retain compatible consumer ranges so the repository overrides do not force a second React copy into published-package consumers. For a security patch, save the initial `bun audit --json` result and update direct dependencies with `bun update @` from the owning workspace. For transitive fixes, manage the root `overrides` with `bun pm pkg set --json 'overrides='`, preserving existing entries. Prefer selectors restricted to vulnerable versions, with the minimum fixed release on each major; test the consumer before crossing a major version. Re-run the audit after resolution because an initial fix can itself have a newer advisory. Verify the resulting lock with `bun install --frozen-lockfile`, run focused tests through the resource queue, and require applicable exact-commit CI. Builds remain CI-only, and paused runtimes remain excluded. Keep Vite+ and its core alias on the same published version; check the upstream release's bundled Vite version separately from plain `vite` dependencies. Audit Python locks, Flutter/Pub advisories, and pinned Actions separately from Bun. An advisory without a published fix remains an explicit blocker, even when other packages are clean. Do not remove a used dependency or suppress an advisory merely to reduce the reported count. React-PDF 11 requires Node.js 22.13 or newer; CI uses Node.js 24. Its default Suspense behavior differs from version 10, so the shared PDF viewer explicitly keeps its existing loading and error handling with `suspense={false}`. Mermaid 12 also changes visual defaults; the chat renderers set `layout: 'dagre'` and `look: 'classic'` to preserve existing diagrams. ### Development Commands ```bash theme={null} # Start all apps bun dev # Start a specific app bun dev:web # Run tests (Vitest) bun test # Run the full verification gate before a PR bun validate ``` ### Database Commands ```bash theme={null} # Start the local Supabase stack bun sb:start # Create a new migration bun sb:new # Apply pending migrations locally bun sb:up # Generate TypeScript types from the local schema bun sb:typegen # Reset the local database (re-runs all migrations + seeds) bun sb:reset ``` > [!WARNING] > > `bun sb:push` and `bun sb:linkpush` push migrations to the **remote** > Supabase project and are reserved for the maintainer applying production > changes. Do not run them — prepare migrations locally with `bun sb:up` and let > the user apply production changes. ## Troubleshooting ### Common Issues 1. **Build / Check Failures**: Run `bun check:changed` to surface TypeScript and Biome errors in changed files, and confirm dependencies are installed with `bun install` 2. **Database Issues**: Ensure the local Supabase stack is running (`bun sb:start`) and migrations are applied (`bun sb:up`) 3. **Test Failures**: Check environment setup and re-run the focused Vitest workspace before `bun check:changed`; the full `bun validate` gate runs in CI 4. **Performance Issues**: Use React DevTools and bundle analyzers ### Getting Help * Check existing documentation * Search GitHub issues * Ask in team channels * Create detailed bug reports ## Contributing When contributing to the platform: 1. Read the relevant documentation 2. Follow the established patterns 3. Write tests for new features 4. Update documentation 5. Submit a pull request with a clear description ### Task-owned temporary files for check stages When an admitted runner has a constrained `/tmp` user quota, prepare a unique, existing current-user directory with mode `0700` on a filesystem with freshly verified quota and free-space capacity. Set `TUTURUUU_CHECK_CHILD_TMPDIR` to its absolute real path when invoking normal `bun validate` through `ttr resources run`. The option changes `TMPDIR`, `TMP`, and `TEMP` only for validation children. Keep the check runner's own `TMPDIR`, `HOME`, and `TTR_RESOURCES_HOME` unchanged; both shared queues remain in their original locations. Directory validation alone does not establish capacity or authorize cleanup. Invalid directories fail without falling back to `/tmp`. Child processes carry a sentinel preserved through Turbo strict mode that rejects a nested `bun validate` CLI before it creates a queue; imports of the check module remain allowed. Run another broad check separately through the original queues. This option does not skip checks, change resource limits, or turn an incomplete or failing check into a pass. ## Aggregate validation and native Bun type checking `bun validate` is the repository aggregate (formerly the custom `bun check` script). `bun validate:now` retains the explicit `--force-now` queue behavior. Both invoke the same `scripts/check.js`; flags, fail-fast handling, mandatory Biome follow-through, queues and child-temp isolation are unchanged. The legacy `tuturuuu-bun-check` queue directory and environment names stay stable so old and new runner processes coordinate during the rename. The aggregate still covers workspace tests, Turbo type-checking, blocking Biome format/lint, source size, public SEO, server console, internal-app-auth, satellite cookie auth, offline Worker wiring, multi-account vault ownership, legacy API wrappers, mobile iOS settings and API mappings, script tests, next-intl setup, translation validity/key parity/sorting/namespaces and migration timestamps. Platform release alignment runs for release files; Discord Python validation runs for Discord changes. No component was replaced with a type-only command. `bun check:changed` remains the separate build-free pre-push gate; other `check:*` commands retain their roles. `ttr resources run -- bun check --no-script -p ` explicitly invokes Bun's native type-only checker. A package script named `check` takes precedence over native checking; `scripts/validate-command.test.js` rejects that name in the root and every workspace, including retained inactive workspaces. The test is discovered by the existing script-test gate. Native checking is not a substitute for `bun validate`, app builds, guards, tests, or declaration generation. The ban on local aggregate/build execution applies to `bun validate`; native qualification still requires resource admission and a bounded thread count. The resource wrapper injects `--threads=` when absent and clamps explicit values to that lane limit, preserving smaller requests. The aggregate rejects unknown arguments, including `--help` and `--dry-run`, before starting work, prints one usage line to stderr and exits 1; neither flag provides an aggregate preview. The inventory recognizes both `wrangler types` and `cf types` as declaration generators, including scripts with custom names. It reads package manifests without executing generators. The legacy detector and its data-only regression fixture have an explicit `producer-inventory` Wrangler-gap admission. ### Native checker qualification The required gate remains `tsc` 7.0.2, including `run-tsc-build.js` and its cache invalidation. PR #6468 supplies optional tsrs 7.1-dev shadow/advisory pre-push comparison; it is not the required CI baseline. Keep both until a replacement proves all existing semantics. Do not remove TypeScript: declaration producers and editor tooling still require it. Bun 1.4.4 reports TypeScript 7.0.2 through `bun -p process.versions.typescript`, matching the repository compiler version. It embeds its own compiler and standard library; matching versions alone is not proof of diagnostic or build equivalence. The repository Bun pin remains owned by the separate Bun upgrade/security lane; this change does not update it. Native execution requires Bun 1.4.4, while `validate` works with the existing pin. See [Bun's native checker](https://bun.sh/docs/runtime/check). There are 89 root/workspace manifests; only the root defined `check` before the rename. Active scripts invoke `tsc` from TypeScript 7.0.2, often through `run-tsc-build.js`. No active manifest invokes a separate `tsgo` binary; that name is recognized by the changed-files project resolver. tsrs is installed externally for shadow/pre-push use, not as a workspace dependency. Run `node scripts/ci/bun-check-inventory.js` to inventory root/workspace scripts, resolved type-check projects, inactive workspaces and custom emit/generation producers. Turbo's `type-check` still depends on `^build` and `^type-check`. Meet has a custom declaration producer; Calendar/Mail split app, boundary and Worker configs; several services generate Wrangler ambient declarations before checking. Those steps must survive any future switch. The opt-in Linux harness is `scripts/ci/bun-check-qualification.js --out --threads 6 --runs 5 [--tsrs ]`. Invoke it with `node` inside the existing fenced exact-SHA runner after frozen dependency installation. It does not create another CI gate. Each report records source SHA, lock hash, versions, host/load, exact commands, resolved configs/references, file closures and hashes, full diagnostics, exit status, wall time and peak RSS. `--all` disables native diagnostic grouping. The known-invalid control adds a fresh `TS2322` assignment under `packages/types/src`, requires every checker to report it, and removes the file in `finally`; the mutation and source hash are recorded separately. This harness compares the same **no-emit projection** used by the existing shadow: `noEmit=true`, `incremental=false`, `composite=false`. This alone does not prove the original build/reference/emit semantics. First runs are process cold; subsequent runs use fresh processes with warm OS caches. It does not evict shared host caches or claim cold filesystem results. Alternate checker order, report all five observations and errors, and compare diagnostic multisets before comparing performance. Real diagnostic differences or unverified generated closures prohibit gate promotion. Preserve declaration/emit tasks even after future diagnostic equivalence is established. ### Bun 1.4.4 evaluation: initial declaration closure The first exact-source run at `b56c8fde67b487ee07961004feb92930f121b945` used `sokora-ubuntu` (Linux x86\_64, 20 CPUs), the fenced `early-merge-validate.sh` runner, Bun 1.4.4 and Node 24.22.1. Both Bun's embedded compiler and the baseline reported TypeScript 7.0.2; the tsrs pin reported 7.1.0-dev.20260930.4. The frozen `bun.lock` SHA256 was `c4687dadb08c5aa33a132d860f8d44a0b7ee2c032ec11c09e5724a4d864999e2`. All checkers used a six-thread budget (`--threads 6`, `GOMAXPROCS=6`, `RAYON_NUM_THREADS=6` as applicable). The four standard Types/Supabase/Internal API/Masonry declaration prerequisites were built first. Each paired comparison used identical source/dependencies and effective no-emit flags; all 11 recorded input closure hashes stayed unchanged. There was one process-cold pass and four warm passes per checker, with alternating order, plus five error-control runs. The host was shared (initial load averages 9.57/34.93/31.40); these are observations, not an adoption or speedup claim. Source-loading inventory had already warmed filesystem caches before the first timed compiler process. | Checker | Process-cold total, s | Warm total median \[range], s | Error median \[range], s | Peak RSS, MiB | | - | - | - | - | - | | tsc | 38.295 | 37.336 \[37.112-37.599] | 0.231 \[0.227-0.240] | 7855.8 | | bun | 7.337 | 7.346 \[7.206-7.493] | 0.071 \[0.067-0.072] | 2159.9 | | tsrs | 6.520 | 6.216 \[6.100-6.310] | 0.066 \[0.063-0.072] | 2418.1 | Totals above sum the 11 project checks sequentially. Peak RSS is the maximum single checker process, not summed memory. Warm values summarize four totals; error values summarize five runs of the injected Types control only. tsrs and Bun did not perform identical validation work on this initial closure, so the timing columns must not be used as an equivalence-adjusted performance ratio. | Project | tsc / Bun / tsrs diagnostics | Bun exact parity | | - | - | - | | `packages/types/tsconfig.build.json` | 0 / 0 / 0 | yes | | `packages/utils/tsconfig.typecheck.json` | 0 / 0 / 0 | yes | | `packages/ui/tsconfig.typecheck.json` | 0 / 0 / 20 | yes | | `packages/supabase/tsconfig.typecheck.json` | 0 / 0 / 0 | yes | | `packages/internal-api/tsconfig.typecheck.json` | 0 / 0 / 0 | yes | | `apps/tasks/tsconfig.json` | 0 / 0 / 0 | yes | | `apps/calendar/tsconfig.json` | 0 / 0 / 0 | yes | | `apps/calendar/tsconfig.worker.json` | 1 / 1 / 1 | yes | | `apps/meet/tsconfig.app.json` | 0 / 0 / 0 | yes | | `apps/meet/tsconfig.cloudflare.json` | 2 / 0 / 2 | NO | | `apps/web/tsconfig.json` | 2 / 2 / 20 | yes | Every checker reported the injected `TS2322` at the same file, line, column and message in all five runs; the control was removed afterward. Existing real checkout diagnostics also matched exactly: Calendar Worker reported `TS2307` for the absent generated `#calendar-handler` alias, and Web plus SDK reported `TS2307` for absent `@tuturuuu/devbox` declarations. These are genuine checkout diagnostics, but they are missing generated inputs rather than evidence of a fully prepared production type gate. Meet's Worker config has `disableSourceOfProjectReferenceRedirect: true` and references its app declaration producer. With those declarations absent, tsc reported two `TS6305` diagnostics for `token.d.ts` and `handler.d.ts`; Bun reported none. This is consistent with Bun's documented use of referenced source instead of prebuilt declarations. Removing the current gate at this point would lose an existing generated-output assertion. tsrs also differed from the 7.0.2 baseline (20 UI diagnostics and 20 Web diagnostics versus 0 and 2). **Native gate promotion remains unqualified.** Retain the authoritative producers, emit/declaration validation and existing baseline. The follow-up uses `--all-projects --runs 1` after running the existing complete Turbo type gate, including Meet/Calendar boundary producers, to distinguish missing-output differences from source diagnostic differences. This mode covers all active inventoried projects and expands Meet's solution root into its two concrete configs; inactive TanStack remains excluded. It is coverage evidence, not a second performance benchmark or an automatic rollout decision. ### Prepared-closure coverage result At `612429d21223b91018e632ebd84b6044c19d00ec`, the unchanged authoritative Turbo type graph passed on the same Linux host (72 seconds, concurrency 2, `GOMAXPROCS=6`). Its declaration and boundary producers prepared the complete closure. The subsequent one-pass comparison covered **86 concrete configs from 79 active workspaces**: all had exact diagnostic parity, zero diagnostics and unchanged input-closure hashes. The additional TS2322 negative control also matched exactly and failed in both checkers as intended. The lock hash and compiler versions matched the earlier run. Inactive TanStack was not executed. This qualifies the observed **prepared no-emit projection** of those configs. It does not establish drop-in equivalence for the complete current gate: `composite`/incremental behavior was disabled for the paired measurements, Bun intentionally does not enforce the same missing-reference-output behavior, and it does not provide emit or the existing producer/cache receipts. Keep the TypeScript gate and tsrs shadow; no checker dependency was removed. Before a switch, compare unmodified config semantics and preserve the producer/output assertions explicitly, including a missing/stale-output regression. Do not interpret the prepared all-green result as permission to discard those checks. The complete build-free pre-push gate passed on Linux for the implementation commit, including the changed SDK/Devbox tests and type checks; focused Node coverage passed 92 tests on the Mac through resource admission. The installed Mac shim and nested workspace CLI had incompatible admission behavior, so that owned full-gate invocation was stopped and the remaining checks ran on Linux. No installed scheduler was modified. Exact-head hosted CI and independent review remain separate delivery gates; no merge or production sync is part of this change. # Oxc and Cloudflare adoption Source: https://docs.tuturuuu.com/build/development-tools/oxc-cloudflare-adoption Incremental Oxlint/Oxfmt tooling, portable first-party libraries, and Vite-native Worker migration boundaries. ## Decision and current state Gradually adopt **Oxlint and Oxfmt over Biome**, and move application hosting toward **Cloudflare**. These are independent changes: Oxc can lint and format Next.js source without changing its runtime. Vite+ consolidates tooling; it does not translate Next.js routes, server components, or server actions. The foundation adds root-pinned Oxlint/Oxfmt, shared configuration and explicit-file commands. Existing Biome scripts, CI, editor defaults, import organization and release/generated-file formatting remain authoritative. No workspace has completed the switch merely because these tools are installed. Lettin and Parley already have Next.js/OpenNext Worker deployments. Their next architecture milestone is a Vite-native runtime preserving existing contracts. React Router Framework Mode with the Cloudflare Vite plugin is a candidate requiring a scoped compatibility proof. The retained `apps/tanstack-web` remains paused. ## Platform-wide target and daily checkpoints The ultimate target is **every active application using Vite+, hosted on Cloudflare, with Oxc linting/formatting**. Work proceeds incrementally alongside the product backlog; the initial Worker inventory below is the first wave, not a limit on the program. Retained paused applications remain excluded until resumed. Mobile continues to use its native Flutter toolchain; shared services and web surfaces follow the platform target without implying a rewrite of native UI. Keep per-app ownership, prerequisites and exact-head receipts in the private request backlog and coordination board. Checkpoint reviewable PRs as each unit finishes. Separate implemented, CI-verified, runtime-accepted and production-delivered states. A merge does not establish hosting cutover. Capture new Worker/app scope and compatibility findings before dispatching the next unit; avoid competing migration and product owners in the same files. ## Complete Worker migration scope The migration includes every active Worker-backed app/service, not only Lettin and Parley. Framework replacement applies to Next/OpenNext web apps; native Workers retain their fetch/email/scheduled/RPC entrypoints while adopting Vite+ and Oxc tooling. A backend service does not need React or a router to qualify. | Active surface | Current boundary | Migration acceptance | | - | - | - | | Meet | Next/OpenNext web Worker and shared Meet services | Vite-native web routes; preserve meeting/session APIs, AI entrypoints, media and Durable Object classes | | Parley | Next/OpenNext web Worker | Vite-native studio and participant routes; preserve private reviews, snapshots and Meet bindings | | Lettin | Next/OpenNext web Worker with D1/R2 | Vite-native routes; preserve publication, ownership, D1 session and R2 contracts | | Colab | Vite React assets plus native Worker | Adopt Vite+ without changing auth, sponsorship, room Durable Objects or SPA routing | | Meet realtime | Native Worker and Durable Objects | Adopt shared tooling; preserve signalling, TURN/SFU contracts and realtime health checks | | Coordination | Native Worker service | Preserve coordination protocol, authentication, object identity and storage | | Devbox control | Native Worker service | Preserve control authentication, lease/state transitions and deployment identity | | Cron control | Scheduled/native Worker | Preserve scheduler ownership, deduplication, recovery RPC and authenticated forwarding | | Mail email routing | Native email Worker within Mail | Preserve MIME ingestion, R2 permissions and the owning Mail webhook contract | This inventory follows active checked-in Wrangler configurations. Existing `apps/backend` and `apps/tanstack-web` Wrangler files are retained paused artifacts, not migration targets. New active Worker configurations must join the inventory. Deployment existence and canonical health still require live evidence. Begin with the portable session core and the already Vite-native Colab toolchain, then use the same verified conventions for native service workers. Meet, Parley and Lettin require the shared framework-adapter work before full web replacement. Each app retains its own exact-commit test/type/lint/build and Worker acceptance receipts; one green app cannot establish another app's readiness. ## cf CLI adoption alongside Vite+ Prefer Cloudflare's **cf CLI** over Wrangler as each operation proves compatible. The per-operation verdicts, native Worker template, guard and cutover checklist live in [cf CLI and Void over Wrangler](/build/development-tools/cf-cli-wrangler-parity). This is a phased tooling decision, separate from framework migration and production promotion. Existing Wrangler commands/configurations remain authoritative until that scope passes its replacement gates. [Cloudflare's launch announcement](https://blog.cloudflare.com/cloudflare-cf-cli-launch/) introduces cf in open beta, with JSON output and a generated API command surface. Vite-built Workers can migrate to typed `cloudflare.config.ts`; configuration uses Vite modes, whose semantics differ from Wrangler environments. cf still delegates some development/deployment paths to Wrangler. Recheck upstream support before implementing a scope; the announcement is not evidence of repository compatibility. 1. **Read-only discovery first.** Pin a reviewed cf version through the owning workspace's package manager. Verify help/schema, authentication and selected account independently; existing Wrangler login does not establish cf access. Exercise bounded inventory/observability queries and record supported command mappings. Filter JSON to required fields and keep raw logs/credentials private. 2. **Pilot a Vite-native Worker.** Coordination or Colab is a candidate after its Vite+ artifact gates pass. Review migration output in an isolated worktree; do not run a repository-wide migrator. Verify compatibility with the pinned Vite+ core, Cloudflare plugin and test runner before adopting typed configuration. 3. **Prove configuration and artifact parity.** Compare resolved settings for every used mode: account/name, routes/domains, compatibility date/flags, assets, service bindings, Durable Object names/classes/migrations, storage identifiers, triggers, secrets and observability. Preserve resource identity; never infer that a similarly named mode selects the same deployed environment. Select one authoritative configuration per scope, with an explicit fallback procedure. 4. **Replace operations separately.** Map local development, type generation, tests, CI bundling, validation upload, tail, version inspection, deployment and rollback individually. Unsupported or unverified operations retain Wrangler. CI must exercise the final emitted artifact and preserve exact-SHA and target identity checks; a CLI rename or dry run alone cannot establish runtime health. 5. **Roll out per owner.** Update scripts, workflow permissions, runbooks and skill references together after focused tests and exact-commit CI pass. Production commands change only with separately authorized promotion, bounded scope and verified rollback. Keep paused runtimes excluded. ### Discovery runtime and authentication Use Node.js 22.18 or later to execute cf. Installing a dependency with Bun does not mean the CLI should run on Bun: Cloudflare documents that commands loading `cloudflare.config.ts` fail on the Bun runtime. Verify the executor separately from the package manager. See [installation and authentication](https://developers.cloudflare.com/cf/get-started/). For capability discovery before project adoption, run a reviewed, exact cf version through `npm exec --yes --package=cf@ -- cf --help` from a private scratch directory outside app configuration and `.env` ancestry. This avoids a global install or repository dependency change. Use an anonymous `cf cli search` query describing the operation, then inspect that command's help/schema. Discovery proves command availability, not authenticated operation or Worker artifact parity. Parse `cf auth whoami` JSON and require `authenticated === true`; exit status zero alone does not establish authentication. An unauthenticated result can return `authenticated: false` and an error with a successful process exit. Preserve that failure instead of proceeding to API calls. cf uses separate credentials from Wrangler; do not copy credential files or assume a prior Wrangler login applies. API commands can remember selected accounts in project-local cache files. Use explicit target identity and isolate discovery before touching app worktrees; review any resulting local files. Neither command discovery nor `--dry-run` authorizes migration, configuration mutation, resource creation or deployment. Cron Control, Devbox Control and Coordination pin `cf@1.0.0-beta.20` and use `cloudflare.config.ts`, `vp dev` and `cf workers types`. Their existing per-service CI checks qualify configuration and emitted output; this does not qualify legacy Void or OpenNext configuration consumers. See the operation matrix for retained publication gaps. The package pin requires Node `^22.23 || ^24.22 || ^26.12`; the older Node 22.18 discovery minimum does not satisfy that package's engines. Do not replace `wrangler types --config ` with a bare cf command: type generation must read the same Worker identity and preserve the declarations consumed by the app. Retain legacy local dev, D1 migration and tail commands until their exact configuration, persistence and output contracts pass independently. Calendar is the first emitted-config type-generation profile: its existing validator admits only ASSETS, and the adapter runs the pinned cf CLI on a supported Node executor. CI compares exact binding keys/requiredness/types against the existing producer, verifies unchanged artifact bytes and retains a receipt. Its self-contained declaration does not require shipping a generated config. Other resource and framework profiles still require separate qualification. Track CLI capability gaps independently from app migration so an unsupported operation does not block portable libraries or Oxc adoption. ## Qualify dependency resolution Reconcile workspace requests with Bun's generated lockfile using the repository's pinned Bun version. A manifest-only update can leave frozen installation blocked before any type checking runs. Keep lifecycle scripts disabled during local dependency inspection; require the normal exact-commit CI installation and builds. Review effective tool dependencies as well as changed lock rows. Oxlint and Oxfmt have optional Vite+ peers, so a Vite+ update can change their selected peer without changing either tool's own row. Static scoped placements and a successful frozen lockfile check do not prove the installer uses those placements. Resolve packages from each owning app and tool after installation, record intentional peer changes, and retain CI and emitted-runtime gates for the affected apps. ## Use the tooling foundation From the repository root, pass **explicit files you own**: ```bash theme={null} bun oxc:lint scripts/oxc.js scripts/oxc.test.js bun oxc:format scripts/oxc.js scripts/oxc.test.js bun oxc:write scripts/oxc.js node --test scripts/oxc.test.js ``` Lint fails on warnings/errors. Format checks do not write; `oxc:write` explicitly applies formatting. The wrapper rejects empty selections, flags, globs, directories, symlinks resolving outside the permitted source, build/dependency trees and paused runtimes. Installed tools run with two threads, without Turbo or builds. Tests cover scope rejection, native-dialog linting, read-only formatting and idempotence. The baseline uses correctness rules, native-dialog prohibition, TypeScript/React/ accessibility plugins, and two-space, 80-column, single-quote, semicolon, LF, ES5-trailing-comma formatting. This is **not Biome rule parity**. Manifest sorting is disabled; import and Tailwind sorting are not enabled. ## Move one workspace at a time 1. Inventory Biome rules/suppressions, file types, editors, Turbo/CI tasks and generated-file producers. Compare read-only Oxc output on owned files. 2. Map rules by behavior, including accessibility, hooks, unused imports and intentional exceptions. Record gaps and compensating validators; preserve CSS/Tailwind validation and import organization. 3. Isolate formatting from product logic. Preserve generated/vendor ownership; translate suppressions only when their semantics match. 4. Select one formatter per migrated scope and update CI, editors, generators and docs together. Require applicable exact-commit test/type/lint/build evidence. 5. Remove Biome only once every remaining scope and release tool has equivalent coverage. Preserve auth, i18n, source-size, import-boundary and runtime guards. Vite+'s monorepo migrator operates at the workspace root and rewrites shared catalogs/lockfiles. Do not run it for a single-app task. Review bounded dependency and configuration changes instead; use package-manager commands for dependencies. Verify Vite+/Vite/Vitest version compatibility before changing shared test runners. Use the package-manager-produced graph and its provenance to retain the minimal complete closure: workspace aliases plus required transitive and peer-context records. Keeping only Bun's workspace block while preserving the packages section can omit that closure. Preserve the root provider and every unchanged consumer; avoid wholesale unrelated lockfile churn. Change dependency manifests only through package-manager commands, never by hand. A frozen restore exiting zero without rewriting the lockfile does not prove the effective package identity. After an admitted restore, use `createRequire` from the owning app, CLI and plugin to resolve package JSON files; record their canonical paths and name/version identities, including required transitives. Distinguish an alias package's release version from its embedded Vite API version. Check plugin peer compatibility against that API; do not force a plain-Vite peer from a textual lockfile key alone. Preserve cold-restore admission, heavy-work holds, resource gates and CI-only builds. Require the owning app's actual exact-commit CI build. ## First-party portability contract Maintain domain rules and application services as ordinary TypeScript modules. Reusable React components may depend on React and framework-independent UI primitives; they receive navigation, translated labels and data through typed props or a small explicitly scoped provider. React Server Components are an optional adapter surface, never a prerequisite for consuming the domain package. | Layer | Owns | Avoids | | - | - | - | | Domain | Pure decisions, validation, immutable snapshots, permission predicates | Framework imports, request globals, storage drivers | | Service | Workflows with injected actor, clock, repositories and capabilities | Reading ambient cookies/env or assuming trusted clients | | Portable React UI | Semantic tokens, presentation, interaction state, accessibility | Next/router/translation hooks and server-only imports | | Framework adapter | Route loaders/actions, HTTP mapping, navigation, localization, request credentials | Duplicating domain rules or bypassing policy | | Runtime adapter | D1/R2/Supabase clients, Worker bindings and scheduling | Leaking credentials or runtime-specific types into portable exports | Prefer existing first-party packages and narrow public subpath exports over a new universal framework wrapper. Keep Next-specific compatibility entrypoints stable while adding portable ones. Do not call an entire package framework-agnostic just because one file is pure: consumers must resolve its entrypoint without transitively loading Next/OpenNext or Cloudflare. Validate import graphs and consumer builds in CI. Runtime-specific optional adapters must have explicit entrypoints/dependencies. Use standard `Request`, `Response`, `Headers`, URLs and `AbortSignal` at HTTP boundaries where appropriate. Pass request context per invocation; never store actors or bindings in process-wide mutable state. Inject verified identity rather than accepting browser-provided user IDs as authority. Preserve audience, expiry, MFA, consent, membership and revocation checks in reusable policy services. Current seams include Lettin's Actor/Store context and SQL permission fences, with bindings/identity/HTTP as framework adapters. Parley/Meet domain contracts can remain shared while routes, session lookup and meeting UI adapters are extracted. A package can support Next.js, React Router or other React hosts by implementing these small adapters; support for every framework is a goal requiring consumer verification, not an automatic guarantee. ## Vite-native Worker acceptance gates Compare emitted compatibility flags against each service’s source configuration; Devbox Control currently has no flags, so its verifier must not require `nodejs_compat` simply because another Worker uses it. | Boundary | Required preservation and evidence | | - | - | | Routes | Public/private/API inventory, bilingual paths, redirects/rewrites, metadata, noindex and deep links | | Identity | Satellite app-session audience/expiry, allowlists, MFA, workspace membership, owner consent and actor attribution | | API | Existing internal-api contracts and TanStack Query; injected request credentials | | UI | Navigation/translation/server adapters extracted without changing semantic tokens | | Lettin | D1 session behavior, R2 permissions, schemas/data, draft/publication snapshots and writeAccess fences | | Parley/Meet | Immutable scenarios, private facilitator reviews, participant invitations, signalling/media, Durable Objects and AI bindings | | Runtime | SSR, assets, auth denial/redirects, cache, errors, cancellation and bindings in real local Workers with isolated fixtures | | Delivery | Exact-commit CI build, staging runtime acceptance, then authorized canonical promotion with rollback and product checks | Prove a small authenticated vertical slice before replacing the complete app. Document fixture boundaries and hosted dependencies: local Durable Objects do not validate Cloudflare SFU/TURN. Hosting adoption does not imply migrating every store to D1 or replacing Supabase. Database changes, schedules, secrets and canonical traffic each need a bounded plan. Pin the promotion range, retain a rollback artifact and verify schema/binding compatibility and health before promotion. ## Runaway-work and cost acceptance Every migration or change that adds or amplifies billable work must include a cost-safety review. The [reported runaway Durable Object alarm incident](https://serverlesshorrors.com/all/cloudflare-108k/) is a warning about self-sustaining work, not an independently verified account of provider usage. Do not rely on future refunds, billing notifications or a per-invocation CPU limit to cap total storage, requests, retries or downstream spend. [Durable Object alarms](https://developers.cloudflare.com/durable-objects/api/alarms/) are at-least-once and automatically retry thrown errors. Explicit rescheduling can create another chain beyond those retries. Ingress rate limits cannot stop work that an alarm, queue consumer or cron trigger already owns. | Control | Acceptance evidence | | - | - | | Work bounds | Bound operations, scanned rows/pages, payload size, fan-out and concurrency per wake; bound attempts and elapsed age for finite jobs. Persistent failures or no progress exhaust a durable budget rather than resetting on wake or restart. | | Scheduling | Empty/completed/expired/cancelled jobs do not reschedule. A valid next deadline is finite and in the future; retries use capped backoff. Recurring activity requires an explicit owner and renewable lease/expiry, with a documented stop path. | | Retry identity | Duplicate delivery and restarts preserve job identity, attempts, cursor and idempotency fences. A consumer cannot re-enter its own enqueue path indefinitely. Checkpoint progress without multiplying writes per item. | | Durable shutdown | Check a persisted stop/lease fence before expensive work and before rescheduling, including racing callbacks. Test a scoped authenticated stop path that preserves resumable data. Removing public routes or stopping ingress is insufficient. | | Storage amplification | Use indexed/bounded reads; avoid unbounded list/scan fallback in hot paths. Batch writes where useful, then measure the actual billing unit: batching does not necessarily reduce billed keys/rows/messages. | | Cost envelope | Record normal and failure-case operation counts per job/session and expected maximum admitted concurrency/volume. Include storage, queue retries, AI/media and logging; calculate with current product pricing and record assumptions. | | Detection and response | Verify which usage/anomaly alerts the account and products support, who receives them and how quickly they can react. Record sampled per-service rates, retry/no-progress counters and a tested independent operator stop procedure. | Require regressions for empty queues, expired state, duplicate execution, restart, clock/deadline boundaries, persistent downstream failure, no progress and stop-versus- reschedule races. Count storage/downstream calls as well as final return values; prove bounds under repeated wakes in isolated fixtures and the real local Worker. Exact-commit CI must exercise the emitted artifact. Keep live account settings, alert delivery and hosted shutdown verification as separate production evidence. Cron Control's default-off checkout Worker uses a 5,000-item combined active and quarantine bound, pre-expiry capacity checks, three durable attempts per item, per-workspace invalidation receipts and a 240-second whole-run deadline. Stop is checked before each expiry batch and invalidation chunk. See the [processor runbook](/build/devops/cloudflare-processor-migration) for counted operation bounds, restart/deadline/poison regressions and remaining hosted stop and cache-freshness evidence. Source and mocked requests do not establish billed usage or crash-atomic database-to-ledger persistence. CPU limits constrain an invocation, not the number of invocations or all billable services. Usage notifications are detection, not an automatic spending cutoff. Choose service-specific limits with measured workloads; do not silently apply an arbitrary global value that breaks realtime or recovery behavior. See current [Workers pricing and custom CPU limits](https://developers.cloudflare.com/workers/platform/pricing/) and [available billing alerts](https://developers.cloudflare.com/notifications/notification-available/). Any automatic circuit breaker must itself perform bounded work and fail safely without deleting original data or retrying indefinitely. Existing tests and code must be inspected per service; this foundation establishes review gates and does not assert that every deployed alarm is protected or that account alerts are configured. ### Wave-1 staging publishers: publication and serving envelope Calendar, Mail and Apps staging publishers require an exact owner-reviewed source/artifact admission and authenticated D1 consumption state. PR GitHub tokens cannot write that staging-account ledger; missing/deleted/invalid state fails closed. Seven bounded D1 queries persist/read back three transitions on a fresh success; there is one upload invocation, no application retry and no automatic admission renewal. The real-entrypoint stop-fence negative control, forged/deleted-state fixtures and SQLite conditional-update races live in `scripts/ci/wave1-native-*.test.js`. Staging configs disable workers.dev and preview URLs and have no public routes, including Meet's prepared profiles. This removes public request amplification; rate limits, CPU caps and the publication flag alone would not cap serving costs. The owner must verify hosted topology before enablement. Disable publication and delete/disable only the staging Worker to stop unexpected execution, preserving consumption evidence. The [publisher runbook](../devops/wave1-native-publishers.mdx) owns provisioning, HMAC/token rotation, finite counts and the stop procedure. Hosted credential scope, private runtime acceptance and actual serving state remain separate release evidence. Production publication stays held for all four apps. ## Incident evidence Correlate scoped failing requests with authenticated Worker tail exceptions and the deployed revision. A hung-request cancellation establishes a runtime failure; nearby framework warnings are clues, not root-cause proof. Offline artifact replay can isolate middleware/server boundaries, but missing modules or post-build config patches do not establish a valid replacement release. Keep raw logs private. Build success alone does not establish request health. Follow [active runtime ownership](/build/devops/active-runtime) and the [delivery runbook](/build/devops/github-actions-runbook). Release builds remain in CI; a migration direction does not authorize production cutover. ## Upstream references * [Oxlint configuration](https://oxc.rs/docs/guide/usage/linter/config.html) * [Oxfmt configuration](https://oxc.rs/docs/guide/usage/formatter/config.html) * [Vite+ migration](https://viteplus.dev/guide/migrate) * [React Router on Cloudflare](https://developers.cloudflare.com/workers/framework-guides/web-apps/react-router/) # Pre-push checks Source: https://docs.tuturuuu.com/build/development-tools/pre-push-checks Run the build-free changed-files gate before pushing so cheap CI failures are caught locally in seconds. ## Why this gate exists Most avoidable CI failures come from cheap checks skipped before pushing: Biome formatting, `flutter analyze` infos (fatal in CI), TypeScript errors such as `TS6059` (a package importing app source outside its `rootDir`), stale generated inventories and branch names the checker rejects. `bun validate` is too heavy for agent machines because its Turbo graph can build workspace dependencies, so it is not run locally. `bun check:changed` is the replacement before every push. ```bash theme={null} bun check:changed # working tree vs merge base with origin/main bun check:changed --dry-run # print the planned steps only bun check:changed --only biome # one step (ids or prefixes such as type-check) bun check:changed --skip vitest # skip a step family bun check:changed --committed # committed changes only (the hook's mode) bun check:changed --files a.ts b.ts ``` It never runs builds, `bun validate` or Turbo. Small repository scripts run directly; Vitest, type-check and `flutter analyze` are queued through `ttr resources run` when resource control is installed and enabled (`--no-ttr` opts out). Without the `~/.bun/bin/ttr` directory entry, the gate warns and runs sequentially with Vitest capped at two workers. An invalid installed global CLI or unsafe ancestor bin alias still fails closed; a workspace binary is never used as a fallback. A dangling global launcher symlink or missing package bin target is a broken installation, not an absent CLI: reinstall with `bun add -g tuturuuu` before retrying. ## What runs for which paths | Changed paths | Steps | | - | - | | Any | Branch name (same pattern as `branch-name-check.yaml`), source-size ceiling, docs inventory and app SEO `--check` | | JS/TS/JSON/CSS outside `apps/mobile` | Biome `check --error-on-warnings` on those files (pinned 2.5.13); Oxlint on JS/TS files as an **advisory** step | | `apps/*` or `packages/*` source | App-session, satellite cookie and server-console guards | | A workspace's source, `tsconfig*.json` or `package.json` | Type-check of the projects its `type-check` script checks | | A workspace's related tests | Changed and colocated tests use `node --test` when they import `node:test` or their package's `test` script uses `node --test`; explicit Vitest imports retain Vitest. Other related tests use the owning workspace's Vitest config. Each file runs under one runner. | | `scripts/`, `.github/actions/`, `plugins/` | Changed and sibling `node --test` files | | `.github/workflows/` or `.github/actions/` | Workflow concurrency, Bun pin and workflow-config policy tests | | `apps/mobile/` | `flutter gen-l10n` first when ARB files changed, `dart format` on changed Dart files, then `flutter analyze lib test` exactly as `mobile.yaml` runs it | | `apps/*/messages/*.json` | i18n sort and en/vi key parity | | Migrations, docs, web API routes | Migration timestamps, docs audit, legacy route wrappers, mobile API mappings | Oxlint stays advisory until a scope records Biome parity under the [Oxc adoption rules](/build/development-tools/oxc-cloudflare-adoption); Biome remains the blocking formatter and linter. Paused runtimes are excluded. In hook mode, every non-deletion `refs/heads/*` destination from Git's pre-push stdin is validated, including additional branches whose commits differ from HEAD. Tracking cannot override an invalid destination such as `tmp/agent`. Branch deletions and tag names do not undergo branch-name validation. Without push refs, local `tmp/` branches with a configured tracking branch validate that tracked branch name and print both names. Untracked `tmp/` branches still fail, and other local branch names always validate themselves. This keeps the check blocking while allowing temporary agent worktrees that track a valid publication branch. The manual label `branch name fix/routing (tracking for tmp/agent)` validates only the configured upstream name, which may name a local or remote branch. It does not certify a future push destination. For example, `git push origin tmp/agent` is still rejected by the opted-in hook even when the upstream is `fix/routing`. CI continues to validate the actual published branch name. Ancestor bin refusals can be caused by stale links in the main checkout even when the current worktree is healthy. `--no-ttr` does not bypass that safety check. Inspect the listed ancestor's manifest and links: refresh declared workspace dependencies with `~/.bun/bin/ttr resources run -- bun install --force`, keeping legitimate package links. Remove links only after proving they are undeclared orphan symlinks; follow the conditional [orphan alias recovery procedure](/reference/packages/sdk#orphan-workspace-aliases). ## Type-checking without built declarations CI builds `@tuturuuu/types`, `supabase`, `internal-api` and `masonry` before type-checking. Local checkouts usually lack those `dist` declarations, which produces thousands of missing-module errors and implicit-`any` cascades. The gate runs `-p --noEmit --incremental false --composite false` per project and classifies diagnostics: * With every workspace declaration present, all diagnostics block. * When declarations are cold, missing-module errors citing an unbuilt workspace package are ignored. Configuration errors (`TS5xxx`/`TS6xxx`, including `TS6059`) and errors in changed files that do not import an unbuilt package block. A changed file importing a missing package still blocks on real type errors such as TS2322; only implicit-any follow-ons (7005/7006/7031/7034) in that file are advisory. Diagnostics in unchanged files remain advisory. Shared-package changes also select transitive reverse dependents from workspace manifests. Root/shared configuration changes (`package.json`, `bun.lock`, root `tsconfig.json`, `biome.json`, `turbo.json`, `bunfig.toml`, `.npmrc`, root `vitest.config.ts`, `.oxlintrc.json`, `.oxfmtrc.json`, and `packages/typescript-config`) print **not covered; CI only** because this focused gate cannot prove their full repository effect. CI type-checking remains authoritative. ### Optional tsrs shadow `--checker auto` and `--checker tsc` use tsc only. Existing clean-source qualification does not establish that tsrs catches seeded errors, so a tsrs pass cannot replace tsc. `--checker tsrs` requests advisory comparisons, always followed by tsc regardless of success, failure or crash. Only tsc can block. The resolver runs only if at least one planned project is in the recorded qualification set; unqualified-only plans fall back to tsc. Hook mode uses only an existing `TSRS_BIN`, with no network fetch. Outside hooks, pinned npm resolution disables install lifecycle scripts. See the [qualification limits](/build/development-tools/tsrs-shadow-type-check). ## Optional pre-push hook `bun install` already points `core.hooksPath` at `.githooks`. The pre-push hook is inert until you opt in: ```bash theme={null} git config tuturuuu.prePush true # every push in this clone and its worktrees TUTURUUU_PRE_PUSH=1 git push # a single push TUTURUUU_PRE_PUSH=0 git push # skip once while opted in ``` The hook refuses tracked staged or unstaged edits so it can check the exact pushed HEAD. It also refuses untracked files in changed workspaces and selected reverse dependents because those files can affect type-checking and test discovery. When script tests are selected, the fence covers `scripts/`, `plugins/` and `.github/actions/` as well as the tests' own parent directories, so nested tests cannot depend on untracked helpers in a parent or sibling directory. Ignored files are excluded from this check. NUL-delimited Git paths preserve whitespace; only option-like repository/workspace-relative prefixes are rejected before file tools run. Internal dash directories, including Git app routes, are valid. The hook checks committed changes only and skips deletions or pushes of refs other than `HEAD`. Agents should run `bun check:changed` explicitly before pushing whether or not the hook is enabled. ### Bun upgrade and lock normalization Until the Bun 1.4.4 upgrade in #6427 lands, a normal `bun install` with installed Bun 1.4.4 can normalize the 1.4.3-era `bun.lock`. The resulting tracked change makes the opted-in hook refuse a push. Prefer `bun install --frozen-lockfile` for the existing lock or the repository-pinned Bun version; do not commit incidental normalization into an unrelated PR. If drift already exists, preserve its provenance and use a clean isolated checkout. #6427 owns the normalized lock; a passing frozen install alone does not prove a normal install leaves it stable. ## Regression coverage `scripts/check-changed.test.js`, `scripts/check-changed/plan.test.js`, `scripts/check-changed/type-check.test.js`, `scripts/check-changed/tsrs.test.js` and `scripts/ts-diagnostics.test.js` cover path mapping, the branch pattern's parity with the workflow, hook opt-in, ttr queueing, cold-declaration classification and tsc confirmation of tsrs failures. Related native `node:test` files, including database script `.test.mjs` siblings, run through `node --test` even in packages whose default is Vitest. An explicit Vitest import selects Vitest when a file also imports Node test helpers or types. This runner partition avoids false “No test suite found” failures. # Public SEO and Sitemap Maintenance Source: https://docs.tuturuuu.com/build/development-tools/public-seo How public pages enter the bilingual sitemap and how published content refreshes without redeploying. ## Public page discovery Public marketing URLs keep English unprefixed and Vietnamese under `/vi`, independent of browser preferences. New root paths must also be public in `apps/web/src/constants/public_paths.ts`; the route tests verify anonymous access for every sitemap-listed path in both locales. `apps/web/src/app/sitemap.ts` owns `/sitemap.xml`; `robots.ts` advertises it at `/robots.txt`. Do not add copies under `apps/web/public`: static assets conflict with the Next.js metadata routes. The public route manifest is generated from concrete pages under `apps/web/src/app/[locale]`, including public pages outside the marketing route group such as `/portfolio`. A page enters the manifest when its page or colocated layout calls a marketing metadata helper with a literal `pathname` matching its actual route. Redirect pages, dynamic segments, and metadata marked `indexable: false` are excluded. Pages without their own matching metadata stay out: inherited homepage canonicals must not qualify private or utility pages. Use `createLocalizedMarketingMetadata` with an English and Vietnamese message namespace containing `title` and `description`. Canonicals use unprefixed English URLs and `/vi` Vietnamese URLs. Both sitemap entries contain reciprocal language alternates and an English `x-default`. The Next config regenerates `public-routes.generated.json` before every dev/build, including Docker builds. Commit a refreshed manifest with the page change: ```bash theme={null} node scripts/generate-public-seo-routes.js node scripts/generate-public-seo-routes.js --check ``` `bun validate` runs the second command and rejects stale manifests or conflicting static metadata assets. Dynamic content requires a publication-aware reader; never crawl arbitrary workspace, user, invitation, or shared-document routes. ## Automatic content updates The sitemap opts into request-time rendering with `connection()`. Published editorial changelog entries are read with an anonymous Supabase client, preserving RLS and preventing a signed-in visitor from exposing privileged content. The reader requires `is_published`, a publication timestamp at or before the refresh time, and a valid single-segment slug. It paginates in a stable order and caches successful results with a one-hour revalidation interval. New entries and publication changes become discoverable on a subsequent request after that interval; deployment is unnecessary. Next may serve a stale result while its refresh completes. The maximum cache lifetime is one day. Query failures reject the refresh instead of caching a partial or empty successful sitemap. `lastmod` uses the later of actual publication and update timestamps. Static pages omit it because a build timestamp is not a reliable content modification date. The sitemap avoids invented freshness and does not depend on Git history being available in a deployment container. The reader stops at 20,000 changelog records to stay below the protocol limit of 50,000 URLs after adding both locales and public pages. Reaching capacity fails explicitly: split the sitemap before increasing that bound. ## Copy and structured data Keep page titles specific and descriptions aligned with visible product behavior. Use the public product pages for feature details; avoid unsupported customer counts, compliance claims, ratings, or automated actions. The homepage FAQ and main editorial sections are present in server-rendered HTML. Interactive demos and repository statistics can remain deferred. The homepage emits Organization, WebSite, WebPage, and WebApplication JSON-LD. The page language and description match its translated metadata. Escape `<` when serializing JSON-LD into HTML. Structured data does not guarantee a rich result. ## Verification and search monitoring Run focused non-build generator, sitemap-reader, structured-data, and locale tests locally through the shared resource queue. Require the applicable repository checks and an actual web app build in CI for the exact source SHA; do not run `bun validate` or builds locally, because the check's Turbo graph can also build dependencies. For a deployed release: * Inspect `/sitemap.xml` and `/robots.txt` for successful XML/text responses. * Check new product routes and published changelog entries in both languages. * Inspect initial HTML for canonical, language alternates, translated titles, descriptions, visible homepage copy, and JSON-LD. * Validate JSON-LD with Schema.org Validator or Google's Rich Results Test. * Submit `https://tuturuuu.com/sitemap.xml` in Google Search Console and Bing Webmaster Tools. Use URL Inspection and indexing reports to track discovery. Search Console ownership and submission are external account operations. Code checks and sitemap delivery prove crawlability, not search rankings or indexing. References: [Google sitemap guidance](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap), [localized pages](https://developers.google.com/search/docs/specialty/international/localized-versions), and [Next.js sitemap metadata routes](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap). # Remote Devboxes Source: https://docs.tuturuuu.com/build/development-tools/remote-devboxes Run heavy Tuturuuu development workflows on self-hosted remote runners. ## Overview Remote devboxes let internal root workspace members offload expensive local work to self-hosted runner machines. A runner connects outbound to `tuturuuu.com`, receives queued work, executes it on its host, streams logs, stores artifacts, and releases the lease automatically for one-off work. The current `ttr box agent` executes claimed commands as child processes on its host. Its command policy and environment filtering are operational controls, not an isolation boundary for untrusted learner code. Do not route Learn coding submissions to ordinary Devbox runs. Judge needs a separate sandbox executor and readiness checks before its capability can be enabled. V1 is intentionally self-hosted. It does not provision cloud machines. Any runner host should have Node.js, Bun, Docker, and Git available. ## Quick Start ```bash theme={null} ttr login ttr box doctor ttr box setup ttr box setup --dir . ttr box setup --dir . --clone-into ./tuturuuu ttr box agent register --name "ci-lab-1" ttr box agent start --token ttr box repair --dir . ttr box upgrade --runner ttr box restart --runner TUTURUUU_DEVBOX_RUNNER_TOKEN= ttr box shutdown ``` `ttr box setup` is the default bootstrap for a runner host. It first checks whether the current directory is already a Tuturuuu platform checkout. If it is, setup reuses that checkout. Otherwise it can clone or reuse `https://github.com/tutur3u/platform.git`, install dependencies with `bun install --frozen-lockfile`, start local Supabase with `bun sb:start`, verify `supabase status -o json`, and write local Supabase connection values into ignored `apps/*/.env.local` files. Secret values are redacted from human and JSON output. Pass `--dir ` for a specific checkout. If the target exists but is not a valid Tuturuuu checkout, interactive setup asks whether it should clone into a nested `tuturuuu` directory. In non-interactive shells, pass `--clone-into ` explicitly: ```bash theme={null} ttr box setup --dir . --clone-into ./tuturuuu ``` Use `--yes` only when the host should install detected missing prerequisites automatically. Runner registration and service installation are separate choices. To register the machine and install a boot-starting runner service in one non-interactive command after login: ```bash theme={null} ttr box setup --agent --service --runner-name "$(hostname)-devbox" --yes ``` For a platform runner that does not need a Tuturuuu checkout or local Supabase, use `ttr box connect --service --runner-name "$(hostname)-devbox"` instead. This registers the machine, writes its runner token to a mode-0600 file, and installs the same boot-starting service. The CLI prints a direct Infrastructure dashboard link for that runner. The dashboard shows the heartbeat's CLI version and machine identity so an administrator can verify the connection before enabling workloads. The setup command stores the runner token in a restricted local env file and installs a system-level runner service on Linux systemd or macOS launchd. The service starts after reboot and restarts automatically if the runner exits. Runner heartbeats report a small observability snapshot, including `ttr`, Bun, Node, Docker, Git, OS, CPU, RAM, load average, bucketed disk headroom, uptime and, when execution-mode reporting is enabled, isolation attestation. Root workspace members can inspect them from Infrastructure > Fleet; feature, heartbeat and token controls stay in Infrastructure > Devboxes. Devbox agent poll and heartbeat traffic is blocked by default at the platform API boundary. Set `TUTURUUU_DEVBOX_AGENT_API_ENABLED=true` on the web app before starting or repairing runners. When the flag is absent, agent poll and heartbeat requests return `403` before runner tokens are authenticated or runner state is updated. ### Cloudflare control plane `ttr box agent start` uses the existing web API until `TUTURUUU_DEVBOX_CONTROL_URL` is set on the runner host. Point this variable at the deployed `tuturuuu-devbox-control` Worker to send heartbeats, job claims, and an authenticated wake connection through Cloudflare. The agent still sends job events and completion to the web API. It polls the Worker every 30 seconds when idle, even with a wake connection, so a missed signal cannot strand work. The Worker checks the runner token, token revocation, runner status, and root workspace membership before each heartbeat or claim. Disabling heartbeats on a runner continues to reject heartbeat requests. Worker traffic requires TLS. Agents treat `401` as fatal, so `401` means only that the credentials were rejected. If token, runner or membership storage fails, the web agent routes return `503` with `Retry-After: 5`, and the Worker returns `503`. Agents then back off and retry instead of exiting and flapping during a database brownout (`agent-auth.test.ts`). The production workflow installs Worker secrets from the repository's `PRODUCTION_SUPABASE_URL`, `PRODUCTION_SUPABASE_SECRET_KEY`, and `DEVBOX_CONTROL_INTERNAL_TOKEN` secrets. It uses `COLAB_CLOUDFLARE_API_TOKEN` for Cloudflare deployment. Set `TUTURUUU_DEVBOX_CONTROL_URL` and `TUTURUUU_DEVBOX_CONTROL_TOKEN` on web and Learn to wake an assigned runner immediately after enqueuing work. Use the same internal token as the Worker secret; never put it in the runner's env file. Missing or failed wake calls do not fail enqueue, and the fallback poll catches the job. Verify `/health`, then a real runner heartbeat, claim, completion, revocation rejection, and Learn submission before moving additional runners. The old web API remains available until all runners are migrated. Without a Worker URL, idle claims back off from 5 to 10, 20, and then 30 seconds. A claimed job or completion resets that delay; heartbeats remain independent at 20 seconds to preserve online detection and leases. Worker wake messages still trigger immediate claims, with a 30-second reconciliation poll for missed wakes. If a host completed `ttr box setup` but does not appear in Infrastructure > Devboxes, confirm it was registered as a runner. Plain setup prepares the checkout, dependencies, local Supabase, and ignored env files only; it does not create a `private.devbox_runners` row. Run setup with `--agent --service`, or register and start the agent manually, then confirm `ttr whoami` on that host is pointing at the same production origin and root workspace user. If the host appears as registered but shows no heartbeat, inspect the installed service on that host: ```bash theme={null} sudo systemctl status --no-pager --full tuturuuu-devbox-runner.service sudo journalctl -u tuturuuu-devbox-runner.service -n 80 --no-pager ``` The runner service reads its token from the local `devbox-runner.env` file. If the journal shows a missing runner token or stale wrapper, upgrade the CLI and repair the service. Repair reuses the existing token file; it does not register a new runner token or create another runner row. ```bash theme={null} ttr upgrade ttr box repair --dir . ``` Use dry-run first when you want to confirm the checkout, token file, service manager, wrapper path, and service path without writing files or calling sudo: ```bash theme={null} ttr box repair --dir . --dry-run ``` Upgrade a runner's global CLI through the same brokered queue: ```bash theme={null} ttr box upgrade --runner ``` The upgrade command queues `bun i -g tuturuuu` for the selected runner, waits for completion, and returns the remote command logs and exit code. After a successful CLI update, the agent exits; a runner installed with `--service` restarts under systemd or launchd and reports its new CLI version on the next heartbeat. A manually started agent must be restarted by its operator. The run completion and the subsequent heartbeat are separate verification steps. ### Platform-owned fleet controls Root members can use the existing Infrastructure authorization boundary through `client.devboxes.fleet.snapshot()` or the CLI: ```bash theme={null} ttr box runners --json ttr box inspect --runner --json ttr box upgrade --runners --dry-run ttr box upgrade --runners --apply --json ttr box restart --all --dry-run ``` Fleet maintenance defaults to a dry run. `--apply` performs sequential, allowlisted maintenance, rechecks fresh heartbeats and unfinished work before each operation, stops on failure, and waits for a subsequent healthy heartbeat. Upgrades additionally verify the advertised CLI version against the registry release. Offline, revoked, busy, or truncated snapshots are skipped. Snapshot limits mean this is a conservative operator convenience, not an atomic host-wide reservation; coordinate a maintenance window when service users share an install. Capabilities, feature switches, and resource limits are available as structured JSON for tooling extensions. These commands do not enable arbitrary shell access or change runner workload permissions. Custom SDK deployments use their own origin rather than forwarding credentials to hosted Infrastructure. ### Runner trust pools and drain Trusted workloads (`run`, `build`, `serve`, `tunnel`) and untrusted Judge or playground work never share a runner identity. Give each trust tier its own runner token, OS user, token file, service unit and Docker socket. `set_devbox_runner_feature` refuses to enable trusted features on a runner with Judge or playground enabled, or one that reports `executionMode: judge-only`, and the reverse for Judge and playground. A reported mode can only narrow what may be enabled; it never grants a feature. The function locks the runner row before checking, so concurrent toggles or a concurrent heartbeat cannot leave a runner with both tiers enabled. Newly registered runners start with every feature off, so an operator must enable features explicitly. This includes runners a non-root user registers with `ttr box register` or `ttr box connect`: they accept no work, not even trusted `run` jobs from their owner, until a root Infrastructure admin enables a feature through the dashboard or the features endpoint below. Existing runners keep their features, including legacy runners that already mix tiers. Draining a runner stops all new claims on it except maintenance (restart or upgrade). New Judge and playground enqueues skip draining runners, and creating a non-maintenance run pinned to one (`--runner`, or a lease on it) fails with `Runner is draining`; a database trigger enforces this for every insert path. Queued Judge work pinned to it is requeued to another Judge runner that enqueue would also choose: it must support the submission's language, report no `trusted` mode, and have no trusted feature enabled. Its lease moves with it. Queued Judge work whose lease has expired, and other queued work pinned to it (explicit `--runner` pins, playground environments), is cancelled with a run event, and its idle non-kept leases are released. In-flight jobs finish normally. Requesting a drain again re-runs these sweeps. Judge work that no other runner can take stays queued until its lease expires. The Infrastructure snapshot (`ttr box runners --json`) reports per-runner `drain_requested_at`, uncapped `activity` counts and `drained`. `active_leases` counts unexpired non-kept leases; `kept_leases` counts persistent leases, which a drain never releases and which do not block `drained`. Release kept leases separately before retiring the runner. `drained` is true once a draining runner has no queued or running work and no active leases. These fields are `null` until the migration is applied. Operators with the dashboard's root secrets or roles permission can call: ```bash theme={null} POST /api/v1/infrastructure/devboxes/runners//features {"feature":"judge","enabled":false} POST /api/v1/infrastructure/devboxes/runners//drain {"draining":true} ``` Both use the same guarded database functions as the dashboard; Fleet's machine page exposes **Drain** and **Resume placement** to the same administrators. Regression coverage lives in `apps/database/supabase/tests/devbox-runner-trust-pools.sql` and `devbox-runner-drain-hardening.sql` (pgTAP), and the Infrastructure route and store tests. ### Several runner identities on one host Keep each trust tier on its own runner identity: a separate runner token, OS user, token file, service unit and Docker socket. Pass `--service-name ` to `ttr box setup`, `ttr box connect` and `ttr box repair` to give an identity its own systemd unit (`tuturuuu-devbox-runner-.service`), launchd label (`com.tuturuuu.devbox-runner.`), token file (`devbox-runner-.env`) and wrapper. Without a name the legacy single-runner paths are used, so existing installations and `repair` keep working. `setup`, `connect` and `repair` refuse to overwrite a token file, wrapper or unit that belongs to a different execution mode (for example adding a Judge runner without a name on a host whose legacy unit is trusted). Use `--service-name` for the new identity; pass `--force` only to deliberately replace the existing one. `repair --execution-mode` still requires `--force` when the installed service belongs to another mode. The agent enforces the tiers locally as well. A `trusted` runner never executes Judge or playground envelopes. A playground pool process (`TUTURUUU_PLAYGROUND_POOL_ID`) accepts only playground envelopes and maintenance. A `judge-only` runner accepts only Judge envelopes and maintenance. Judge hosts therefore run in `judge-only` mode, which `--judge-images` selects automatically. When a trusted runner refuses a Judge or playground envelope it posts `trusted_runner_fence refused a envelope` as the job error and logs `[devbox] trusted_runner_fence` locally. That means untrusted work was routed to a credentialed host: audit run events for it before enabling trusted-mode admission. `ttr box doctor --isolation` prints read-only evidence about the identity it runs as: dedicated user, private token directory, systemd `ProtectHome`, dedicated Docker socket, gVisor default runtime, and no `docker` group membership. Each check is `pass`, `FAIL` or `unknown`. The command exits `1` unless every check the execution mode requires is proven and no other non-advisory check fails. `trusted` requires a dedicated user and a private token directory; `judge-only` also requires a dedicated Docker socket, gVisor by default and no `docker` group. An unset `DOCKER_HOST` with a reachable daemon counts as the general socket. `protectedService` is advisory: it is self-reported and an unreadable `/home` is reported as `unknown`, never a pass. The mode comes from `--execution-mode` or `TUTURUUU_DEVBOX_EXECUTION_MODE`. Add `--other-user ` to prove that user cannot reach this runner's secrets. It fails when that user is root or in `docker`, `sudo`, `wheel`, `admin`, `root` or `lxd`; when the credential file or directory is a symlink, or grants it owner, group or other access. It checks `$HOME`, the checkout (`--checkout-dir`, default cwd), checkout `.env*` files other than examples, and every ancestor using modes and ACLs (`getfacl` on Linux, `ls -le` on macOS). Effective extended ACL grants conservatively fail even for unrelated identities; missing tools or indeterminate ACLs are required `unknown` failures. Agents report `executionMode` and the isolation summary in heartbeats only when `TUTURUUU_DEVBOX_REPORT_EXECUTION_MODE=true` is set. Enable it only after both heartbeat handlers accept those fields. ### Running an exact commit Trusted runners accept a typed `__ttr_commit_run_v1__` envelope that carries only a full commit SHA, a clean relative directory, and an argv. The runner checks the argv against the execution policy before any git work, then fetches `origin` branches (`refs/heads/*` into `refs/remotes/origin/*`) and requires the SHA to be reachable from one of them. A commit that GitHub would serve by SHA from elsewhere in a fork network is rejected. The detached worktree is created inside a fresh `mkdtemp` directory (mode `0700`) under the runner-private `commit-runs` directory beside the CLI config, never in shared `/tmp`. The argv then runs from that directory through the normal execution policy, process-group timeout and stop handling, and the worktree is removed on every exit path. Agent startup removes any `ttr-commit-run-*` worktree a crashed agent left behind and prunes git's records. Local edits on the requesting machine and on the runner never reach the run. Unknown commits and missing directories fail before anything runs. Git error output is not forwarded, because remote URLs can contain credentials. `judge-only` runners and playground pools reject the envelope. `ttr resources run --offload` uses it. A runner advertises the envelope versions it decodes as `capabilities.protocols.commitRun` (currently `[1]`). The advertisement ships in the same build as the decoder and is sent only by opted-in (`TUTURUUU_DEVBOX_REPORT_EXECUTION_MODE=true`) `trusted` runners. `ttr resources run --offload` submits a commit run only to a runner that advertises the client's version. Otherwise the runner is `blockedBy=capability_missing`: `auto` queues locally and an explicit runner ID fails. This closes the gap where an older trusted runner would execute the envelope as a command. Deploy the heartbeat handlers that accept `protocols` before upgrading trusted runners, because both handlers reject unknown keys. The server enforces the same rule, so direct submissions and capability changes after selection cannot bypass it (migration `20261024030400_devbox_commit_run_protocol.sql`): * Both heartbeat handlers drop `protocols` unless the report says `executionMode: trusted`. A database trigger also drops it for any runner with Judge or playground enabled. * A run whose command is the envelope must be pinned, through `runnerId` and an automatic lease, to a runner that supports the protocol now. The create handler returns 400 or 409, and the `require_devbox_commit_run_runner` trigger enforces the same rule against races. * If a claim would hand the envelope to an unsupported runner (for example after re-pinning), the `refuse_unsupported_devbox_commit_run_claim` trigger skips it. It also expires the run's lease so the run is no longer offered, and records a refusal event. Stop the run and submit it again. * When a runner stops advertising support, the `cancel_unsupported_devbox_commit_runs` trigger cancels its queued envelope runs. `claim_next_devbox_run` is not redefined, so concurrent changes to its body stay intact. `apps/database/supabase/tests/devbox-commit-run-protocol.sql` covers these rules against the real claim function. ### Protecting hosts with existing critical files Use `--execution-mode judge-only` with `box connect`, `box setup`, or `box repair`. Supplying Judge images during registration defaults to Judge-only; general development runners retain trusted execution. Manual Judge-only setup restricts accepted jobs but does not receive protected-service host isolation; use the protected Linux service setup for hosts with critical files. `--control-url` persists the HTTPS Cloudflare Worker URL during registration. Protected service setup requires Linux systemd, a pre-existing non-login service user with its private home under `/var/lib`, a private service-owned token directory, and checkout and CLI outside personal homes. The dedicated root-owned Unix Docker socket must be mode `0660` and restricted to that user's group; the user must not belong to the general `docker` group. Preflight verifies the daemon defaults to `runsc` with cgroup limits before registering a credential. It does not create OS users, move host files, or reconfigure a shared Docker daemon automatically. ```bash theme={null} ttr box connect --service --service-manager systemd --service-user tuturuuu-judge \ --execution-mode judge-only --docker-host unix:///run/tuturuuu-judge-docker/docker.sock \ --token-file /var/lib/tuturuuu-judge/config/runner.env --judge-images "$TUTURUUU_JUDGE_IMAGES" ``` Run protected setup and repair as root from the prepared runner directory, using an authenticated operator CLI context and a CLI installed outside personal homes. The installer deliberately fails before registration when invoked by a non-root operator: it does not broaden private directory permissions or implicitly copy operator credentials into the service account. Prepare the token directory with mode `0700`; group and other access are rejected. Protected systemd units hide personal homes, make the host filesystem read-only except runner directories, remove capabilities, restrict devices, namespaces and kernel access, and set a private umask. Repair preserves an existing Judge-only policy and refuses to weaken it; `--dry-run` performs the same preflight before service writes. Setup persists the dedicated service user for subsequent repair. Older private token files need `--service-user` once; repair then retains it without rotating the runner token. The wrapper prefers the service user's global `ttr` executable so upgrades affect the running service. Before starting a protected agent, it requires `box agent policy` to succeed on that executable; an older CLI lacking policy support fails closed rather than launching. Launchd lacks these Linux protections; use a dedicated Linux VM for protected Judge setup on macOS. Local policy enforcement requires the CLI release containing this change. Judge-only servers must keep Run, Build, Preview, and Tunnel disabled in Infrastructure. Set `TUTURUUU_DEVBOX_EXECUTION_MODE=judge-only` in the private runner environment after installing the release containing this local policy. It rejects host commands, environment files, and environment overrides on maintenance jobs even if a remote workload switch is accidentally enabled. Unknown modes fail closed. The exact CLI-update and supervised-restart commands remain root-member-authorized maintenance operations. Judge submissions use digest-pinned images, gVisor `runsc`, an unprivileged container user, no network, a read-only root filesystem, no host bind mounts, dropped capabilities, no privilege escalation, and CPU/memory/PID/time limits. Never expose the Docker socket to submissions. The runner and dedicated Docker daemon remain trusted infrastructure: access to a rootful Docker socket is a privileged control plane, even when its default runtime is gVisor. Keep the service user out of the general Docker group, use a dedicated Unix socket, hide personal home directories, and do not use this host for arbitrary trusted devbox commands. Stronger isolation for additional workloads requires a separate VM or host; a shell-command denylist is not a security boundary. ### Operator-owned automatic upgrades For a fleet whose service users do not have CLI user sessions, schedule upgrades from an authenticated operator machine with an explicit runner allowlist: ```bash theme={null} bun scripts/devbox-cli-upgrades.ts --runner --runner bun scripts/devbox-cli-upgrades.ts --runner --runner --apply bun scripts/setup-devbox-cli-upgrades.ts --runner --runner --dry-run bun scripts/setup-devbox-cli-upgrades.ts --runner --runner ``` The installer supports macOS launchd, checks at login and every 24 hours while the operator machine is running, and stores its portable script, lock helper, and private logs under `~/.tuturuuu/devbox-upgrades`. It uses the existing CLI session and never copies credentials into its plist. `launchctl print gui/$(id -u)/com.tuturuuu.devbox-cli-upgrades` shows scheduler status. A full snapshot, an active lease, stale heartbeat, or unfinished job prevents upgrade; an unsuccessful run or version heartbeat stops the remaining operations. Keep runner self-upgrades disabled when this scheduler owns maintenance. Ensure the service's executable points at the same global installation that `bun i -g tuturuuu` updates for its OS user. A successful install into a different path does not update a service launching an older private copy. After changing a systemd path, drain work, reload the unit, restart sequentially, and verify each runner's heartbeat and CLI version. ### Automatic CLI upgrades New or repaired managed services default `TUTURUUU_DEVBOX_AUTO_UPGRADE=false`. Enable `TUTURUUU_DEVBOX_AUTO_UPGRADE=true` in the private runner environment only after selecting an upgrade owner and maintenance policy. Existing services need one repair after the release containing this capability is installed: ```bash theme={null} ttr box repair --dir . --dry-run ttr box repair --dir . ``` An idle agent checks the official npm registry at most once per day, installs only a newer stable `tuturuuu` release pinned to its exact version, verifies `--version`, and exits for its service manager to restart. It pauses new claims during installation and keeps heartbeating. A private host lock serializes installs from runners sharing an OS user home directory, and persisted check state prevents restart retry loops. Cached install state is scoped to the verified CLI command, and a failed cached-version verification resumes its registry check. Other agents using that shared install restart after verifying the installed version. Lock contention schedules another attempt after 30 seconds, at the next idle claim, rather than delaying a stale process for a full day. Registry checks remain limited by the shared daily state. Each agent drains its own jobs; for hosts with different OS service users or unrelated CLI consumers, coordinate a host maintenance window or opt out instead of assuming this lock covers them. Installer subprocesses receive host path/runtime variables, never the runner token. An unverified install attempts to restore the previous release. A failed rollback stops the agent with a repair error; inspect service logs and repair the host installation. Locks record process ownership and identity; dead owners are reclaimed under a separate reclamation guard, while live installers are never evicted by age. An unknown legacy lock requires operator inspection. Reclaim guards also record process ownership; an interrupted guard reports a repair-required error rather than being deleted automatically or retrying silently. Never remove a lock held by a running installer. Set `TUTURUUU_DEVBOX_AUTO_UPGRADE=false` in `devbox-runner.env` to opt out. Manually started and source-checkout agents do not upgrade automatically unless explicitly configured, and source `.ts` entry points never mutate the global CLI. Infrastructure's **Update CLI** remains the bootstrap for existing runners; confirm both its successful run and the subsequent version heartbeat. Source changes take effect only after the normal tested CLI release is published. Regression coverage: `devbox-auto-upgrade.test.ts` covers stable version validation, pinned installation, shared-host locking, restart throttling, verification and rollback. `devbox-agent.test.ts` covers claim backoff, independent heartbeats, concurrency, draining and supervised restart; `devbox-setup.test.ts` verifies the service wrapper preserves the explicit opt-in. Infrastructure > Devboxes lets root administrators enable or disable command, build, preview, and tunnel workloads per runner. New runners default to those four workload types enabled. Disabling a type stops new matching jobs from being claimed on that runner; it does not interrupt a running job or act as a security permission for root workspace members who can submit generic commands. The dashboard keeps switches unavailable until the `20260928130000_devbox_runner_features.sql` migration is applied. Its Update CLI action queues an update for an online runner, then the agent restarts as above. The Restart agent action and `ttr box restart` queue a typed maintenance job that exits the agent after successful acknowledgement; the installed service manager starts it again. These actions restart the agent process, not the operating system. Judge defaults off. Enabling it requires a recent heartbeat reporting a verified sandbox runtime and at least one pinned, cached language image. Existing Devbox host commands remain separate from Judge submissions. The same dashboard stores a runner's Judge capacity budget. Defaults reserve at most 50% of host CPU and 25% of host memory, with one sandbox and one instance at a time. Each sandbox defaults to 1 GB, 30 seconds, and 64 processes. Administrators can adjust these limits per machine. These settings are enforced on each Judge container by Docker cgroups, with a conservative free-memory check before each case. The platform claim function caps concurrent Judge runs on a runner at the lower of its sandbox and instance maxima. Each container receives a share of the host CPU and memory budget divided by that ceiling. A managed agent can dispatch independent Judge jobs concurrently within the configured process-wide sandbox budget. These settings do not limit the current trusted host commands, builds, previews, or tunnels. ## Judge setup The Judge executor requires a Docker engine with the `runsc` gVisor runtime registered, working CPU/memory/process cgroup limits, and locally cached OCI images pinned to SHA-256 digests. The CLI fails readiness when these are absent or the Docker engine is rootless with resource enforcement not verified. gVisor currently requires a Linux host or Linux VM. Do not enable Judge on a machine that has only the default Docker runtime. Configure the languages you want with a JSON map of language names to pinned image references. Supported adapters are Python, JavaScript, TypeScript, C, C++, Java, Rust, Go, Ruby, and PHP. The chosen image must contain that language's runtime or compiler plus a POSIX shell and `base64` for compiled languages. Pull each digest on the host before connecting. Then run: ```bash theme={null} export TUTURUUU_JUDGE_IMAGES='{"python":"python@sha256:","cpp":"gcc@sha256:"}' ttr box judge doctor --images "$TUTURUUU_JUDGE_IMAGES" ttr box connect --service --judge-images "$TUTURUUU_JUDGE_IMAGES" ``` Replace the placeholder digests with full verified 64-character values. The connect command saves the map with the runner token in the restricted local environment file used by the service. Its dashboard link opens the exact runner row in Infrastructure. After the heartbeat reports Judge readiness, review the host budget and enable Judge. Workspaces consume the platform Judge capacity through Learn; they do not register or control the physical runner. The executor runs one case at a time without network access or host mounts, with a read-only image, dropped capabilities, no new privileges, a non-root user, bounded temporary storage, and per-case CPU, memory, process, time, and output limits. Compiled languages compile inside the same sandbox. Source and hidden cases are stored in the private platform schema; learner results expose only the aggregate pass count and visible case outcomes. A failed or missing sandbox readiness check leaves Judge unavailable. ### Recover a Lima Judge VM after disk errors On macOS, a full host disk can leave a Lima guest's ext4 root filesystem read-only. If writes fail with `Read-only file system` or the VM stops before SSH is ready, restore host free space first. Keep the VM disk and runner token files. Check `df -h` on the host and remove only known rebuildable caches or clean, merged worktrees. If the guest remains read-only, stop the VM before checking its disk. On an APFS host, make a copy-on-write backup, then attach the raw disk without mounting it: ```bash theme={null} judge_instance=ttr-judge limactl stop --force "$judge_instance" judge_disk="$HOME/.lima/$judge_instance/disk" cp -c "$judge_disk" "$judge_disk.pre-fsck" diskutil image attach --noMount "$judge_disk" ``` Find the ext4 root partition in the `diskutil` output (for example, `/dev/disk4s1`). Install the bottled `e2fsprogs` package if needed. Run the read-only check first, then repair **that partition** with the block device path, not the raw `/dev/rdisk*` path: ```bash theme={null} brew install --force-bottle e2fsprogs e2fsck_bin="$(brew --prefix e2fsprogs)/sbin/e2fsck" "$e2fsck_bin" -n /dev/disk4s1 "$e2fsck_bin" -y /dev/disk4s1 "$e2fsck_bin" -fn /dev/disk4s1 diskutil eject /dev/disk4 limactl start "$judge_instance" ``` Replace the example disk identifiers with those from your attach output. The final check must report a clean filesystem. Verify guest writes and the runner service before deleting the backup. If the host fills again, repair the host space problem before restarting the VM; repeated restarts can corrupt more guest data. See [Lima's offline fsck guidance](https://github.com/lima-vm/lima/discussions/2104) for the underlying disk-image procedure. Remove a runner from the cluster from the runner host with its runner token: ```bash theme={null} TUTURUUU_DEVBOX_RUNNER_TOKEN= ttr box shutdown ``` Shutdown deletes the runner's server-side token rows and marks the runner revoked. If the host installed a systemd or launchd runner service, stop and disable that service as well so the OS does not keep restarting an invalid agent process. Run heavy commands remotely: ```bash theme={null} ttr box run -- bun validate ttr box run -- bun sb:reset ttr box run -- bun test:e2e ttr box build --cwd apps/web ``` By default, `ttr box run` creates an auto lease, waits for an authenticated runner to claim the job, streams recorded logs after completion, and releases the lease after one-off work. Use `--keep` for repeated sync/run work: ```bash theme={null} ttr box run --keep --preview-port 7803 -- bun test:e2e ttr box sync --lease --watch ttr box preview --lease --port 7803 ttr box release ``` ## Build, Serve, And Tunnel Use `ttr box build` for common build workloads without spelling the full remote command every time: ```bash theme={null} ttr box build ttr box build --cwd apps/web ttr box build --build-command "bun turbo:local run build -F @tuturuuu/web" ``` Use `ttr box serve` when the remote devbox should build an app, start the server, and keep the run alive. It defaults to `apps/web` on port `7803`, stores the preview port on the run, and returns immediately unless `--wait` is passed: ```bash theme={null} ttr box serve --cwd apps/web --port 7803 ttr box preview --lease --port 7803 ttr box logs ttr box stop ``` `ttr box stop`, `ttr box logs`, and lease release act only on runs and leases created by the signed-in user; another member's ids return 404. Stopping a queued run cancels it immediately, a running run moves to `cancel_requested` for its agent, and finished runs are never changed. Repeating an owner stop on a `cancel_requested` or `cancelled` run returns its current cancellation without rewriting timestamps; retries cannot postpone stale-run reaping. Foreign ids still return 404, including cancelled or finished runs. Owner stops that race completion return the observed final status without a write. Infrastructure administrators use the same stable retry and final-status behavior and can stop any active run from the dashboard. Regression coverage lives in `apps/web/src/lib/devboxes/store-scoping.test.ts`. `ttr box build`, `ttr box serve`, and `ttr box tunnel` do not set a remote command timeout by default. Pass `--timeout ` only when the run should be killed automatically. To expose a served app through a Cloudflare tunnel, keep the token in a local environment variable and pass the variable name to the devbox. The queued command references `$CLOUDFLARED_TOKEN`; the raw token is delivered only as run-scoped env and is redacted from logs. ```bash theme={null} export CLOUDFLARED_TOKEN= ttr box serve --cloudflared --cloudflared-token-env CLOUDFLARED_TOKEN ttr box tunnel --cloudflared-token-env CLOUDFLARED_TOKEN ``` `ttr box tunnel` runs a dockerized `cloudflare/cloudflared` container with host networking on the runner. The command policy blocks inline `cloudflared --token ...` arguments, so operators should use `--cloudflared-token-env` or another local environment variable indirection rather than pasting raw tokens into commands. ## Container Boundary The current agent starts brokered commands directly on the host. The command policy blocks several destructive command forms, but is not a sandbox. These machines must be operated as trusted infrastructure. Docker-backed commands create their own containers only when the requested command does so. Allowed remote workflows include: * `bun validate`, `bun test`, and package-local Bun commands * `bun test:e2e` and Docker-backed Playwright workflows * `bun sb:start`, `bun sb:reset`, `bun sb:up`, and `bun sb:typegen` Blocked defaults include privileged Docker containers, Docker prune-all style commands, absolute host mounts, `sudo`, and host-destructive filesystem operations. ## Env And Secrets Remote env is explicit. Local `.env*` files are never synced automatically. The setup command only prepares ignored app env files inside the runner checkout so local Supabase-backed apps and E2E workflows can use that runner's own Supabase stack. ```bash theme={null} ttr box run --env DATABASE_URL=postgres://remote -- bun validate ttr box run --database-url-env DEVBOX_DATABASE_URL -- bun validate ttr box serve --database-url-env DEVBOX_DATABASE_URL ttr box run --env-file .env.remote -- bun test:e2e ttr box env set --lease API_TOKEN=value ttr box env unset --lease API_TOKEN ``` Brokered env values are scoped to the run or lease, delivered only to the claimed runner, and redacted from command logs. For split-resource development, run Supabase or another database on one devbox, publish its reachable URL as `DEVBOX_DATABASE_URL` on the operator machine, and queue the app devbox with `--database-url-env DEVBOX_DATABASE_URL`. This lets a database-heavy runner and a Next.js runner share work while the current machine only brokers commands and opens previews. ## Runner scheduling and hosted environments Agents keep heartbeats on a 20-second cadence while dispatching independent jobs concurrently, with at most eight active jobs. Judge cases share a process-wide sandbox budget and always use fresh isolated containers. Each case reserves `1 / parallelLimit` of the host budget, so mixed jobs cannot each claim the whole memory or CPU allocation. Eligible smaller reservations can pass a waiting full-budget case. Infrastructure failures cancel queued cases and wait for already admitted commands and container cleanup before reporting failure. Regression coverage lives in `devbox-sandbox-slots.test.ts` and `devbox-judge-case-batch.test.ts`. Maintenance and CLI restart jobs drain active work before changing the agent. An execution failure stops new dispatch and drains already active work before the agent exits. Heartbeats continue during that drain, and local completions wake idle claim polling. The parent scheduling client omits Playground capabilities until both heartbeat contracts support them; `devbox-agent-capabilities.test.ts` exercises its emitted body against both current handlers. Both heartbeat handlers (`apps/web` and the `devbox-control` Worker) also accept an optional `executionMode` (`trusted` or `judge-only`), an optional `protocols.commitRun` list of envelope versions (integers 1-100), and an optional `isolation` summary: `checkedAt` plus the boolean checks `dedicatedUser`, `privateTokenDirectory`, `protectedService`, `dedicatedDockerSocket`, `gvisorDefault` and `noGeneralDockerGroup`. Unknown keys and wrong types are still rejected. These fields are self-reported and advisory: they are stored for operators and later claim matching, never used to grant features or authorize a runner. Agents send them only after both handlers are deployed. Judge readiness probes are cached for ten minutes and refreshed in the background; execution still fails closed if required isolation or language images are unavailable. Agent HTTP calls are bounded. Heartbeat, claim, event, completion and shutdown requests time out after 15 seconds (playground file sync after 30 seconds), and the timeout also covers reading the response body. Network errors, timeouts, `408`, `425`, `429` and `5xx` responses are transient: heartbeats retry with jittered exponential backoff (never slower than the 20-second heartbeat interval), and claims back off up to 60 seconds without crashing. Event and completion uploads retry up to three times. A replayed completion that finds the run already finished (`404` on a retry) counts as delivered. Rejected tokens and other `4xx` responses still stop the agent for operator action. If no heartbeat succeeds for six intervals (two minutes), the agent drains active jobs and exits non-zero. A separate timer exits the process with code `75` after twice that window if an await stays wedged, so the service manager's `Restart=always` recovers the agent instead of leaving it hung with a stale heartbeat. Regression coverage, including a real socket that never answers, lives in `devbox-agent-network.test.ts`. Trusted jobs run as the leader of their own process group. A timeout (`--timeout`) or a remote stop (`ttr box stop `) sends `SIGTERM` to the whole group: shells, Docker CLIs and test workers, not only the direct child. Anything still running after a 10-second grace period gets `SIGKILL`. The claiming agent observes a stop through the events endpoint, which returns `cancelRequested` to the runner that owns the run, about every 10 seconds per job. A stopped run completes as `cancelled` with exit `143`; a timeout completes as `failed` with exit `124`. On `SIGTERM` or `SIGINT` the agent claims nothing new, finishes its active jobs while heartbeating, and exits `0`. A second signal also stops active trusted jobs through their process groups. Generated systemd units use `KillMode=mixed` (only the agent receives the stop signal) and `TimeoutStopSec=600`; launchd plists use `ExitTimeOut` 600. Re-render existing units with `ttr box repair` to pick these up. Regression coverage lives in `devbox-job-process.test.ts` and `devbox-agent-stop.test.ts`. Signals only ever target the job's process group. Once the group is gone the agent does not signal the bare PID, because the operating system may have reused it for an unrelated process. Judge sandboxes carry a `dev.tuturuuu.devbox.judge=1` label and an owner label derived from a hash of the runner token. Docker keeps a container running after the agent dies (watchdog exit, `SIGKILL`, crash). So when an agent with `TUTURUUU_JUDGE_IMAGES` configured starts, it removes up to 100 of its own runner's labelled containers before claiming work. Dedicated playground pool processes skip this step, and other runners on the same host keep their containers. Regression coverage lives in `devbox-judge-containers.test.ts`. Platform-managed playground workers opt in through operator configuration. They require gVisor and pinned language image digests, run unprivileged without host credentials, and disable network access by default. Project environments retain dependencies while warm. A configured `TUTURUUU_PLAYGROUND_POOL_ID` dedicates the agent process to Playground: Judge readiness is false and Judge execution fails before Docker admission, so warm environments cannot double the Judge host budget. Idle/max-age eviction waits for an active project run and holds the pool lock; near-expiry containers are replaced before admitting another bounded run. Failed starts remove their exact owned name, retaining quarantined capacity when Docker cannot confirm removal. Environments expire after fifteen idle minutes and have a two-hour maximum lifetime. Environment count is bounded by the runner's resource limits. A failed container removal retains its capacity reservation until a successful removal or healthy Docker inventory confirms the container is absent. Failed startup orphan cleanup blocks new environments and is retried on the next admission. Creation and stop removal share the same pool lock. Same-project runs serialize synchronization, execution and saves; stop invalidates queued runs without waiting for a running command. Stopping waits for admitted creation, with a fifteen-second wait bound; a timeout reports an unconfirmed stop and retains capacity ownership. It does not wait for the arbitrary project command to finish. Python helpers use the pinned image PATH, including images that install Python under `/usr/local/bin`. A failed background save emits a fixed diagnostic and makes the run report incomplete persistence rather than silently claiming success. These lifecycle rules are covered by `devbox-playground-sandbox.test.ts`. File inventories and changed bytes are exported separately; credentials and runner metadata never enter project files. File/directory conflicts are checked against every path ancestor, and preview paths reject traversal before URL normalization. Export transport permits the worst-case JSON escaping of a valid two-MiB text project; unsupported paths or oversized files report the offending file instead of failing with an opaque schema error. The schema and Python export contracts cover these rules. The platform must provide its signed file-save callback before enabling the hosted playground workload. The scheduling primitives alone reject the reserved hosted Playground envelope without running a host command; its HTTP adapter ships together with the canonical file-save API. The typed-adapter regression in `devbox-runner-judge.test.ts` verifies hosted execution uses the sandbox and canonical authenticated callback, while malformed envelopes never fall through to host execution. Use focused SDK agent, Judge and sandbox-slot tests for scheduling changes. A passing mock test does not establish real sandbox startup latency; measure cold and warm execution on a configured gVisor runner before advertising timings. Do not fall back to ordinary Docker isolation when gVisor is missing. ## Resource queue offload cancellation `ttr resources run --offload auto -- ` may queue locally only before remote creation starts, or after a definite server rejection. A lost response, malformed success acknowledgement, or unexpected error after creation starts exits nonzero and never repeats the command locally. When the acknowledgement contains a run ID, the CLI requests cancellation of that same run. Each offload sends a fresh client operation ID as `idempotencyKey`. The create handler dedupes on (actor, key), so a retried create returns the original run; reusing a key for a different command returns 409. When creation is interrupted, times out or fails without a definite rejection, the CLI calls `POST /api/v1/devboxes/runs/stop-by-key`, which is scoped to the caller's runs. A 404 can mean the create is still in flight, so the CLI retries up to three times within a 10-second budget. If cancellation is still unconfirmed, it prints the operation key; find the run with `ttr box list` and stop it with `ttr box stop `. A stop request alone does not prove the remote process has exited. Keyed creates are accepted only once key storage is confirmed: `private.devbox_run_idempotency_ready()` reports the unique (actor, key) index as valid, and the schema cache knows the column. Until then, the handler returns 503 with `code: DEVBOX_IDEMPOTENCY_UNAVAILABLE` before writing anything. The CLI treats that coded 503 as a definite rejection, so `--offload auto` queues locally. Any other 503 stays ambiguous. Unkeyed creates never consult key storage. The proxy exempts trusted CLI identities from its general rate limit, so keyed creates and `runs/stop-by-key` each have a per-actor limit of 30 requests per minute (`apps/web/src/lib/devboxes/rate-limits.ts`). A 429 is a definite rejection. SIGINT and SIGTERM remain terminal through every creation outcome, including a later rejection, and return 130 and 143 respectively. Cancellation or the overall deadline is checked before each log page and status request. Fleet, creation, status, log and stop requests each have a 15-second bound; follow requests also respect the remaining overall deadline. Repeated control-plane failures request a remote stop and exit nonzero. A stalled stop request cannot hold the CLI open indefinitely. The focused `resources-offload-interruption.test.ts` suite covers rejected and malformed creation after interruption, ambiguous acknowledgements, stalled log pages, cancellation between full pages, and SDK transport abort signals. `resources-offload-operation-key.test.ts` covers an accepted create whose acknowledgement is lost after Ctrl-C, an in-flight create, and the unconfirmed notice. `store-run-idempotency.test.ts` covers server-side dedupe and key races. These fixtures do not establish hosted runner acceptance. ### Run log cursor Run events carry a dense per-run `seq`. A `BEFORE INSERT` trigger allocates it by updating, or creating, the run's `devbox_run_event_cursors` row. That row lock is held until the writer commits, so commit order equals sequence order for every writer (web, infrastructure and SQL functions). The logs route pages `seq > offset` and returns the last `seq` as `next`. Because sequences start at 1, older clients that send raw-row offsets keep working. Paging by a `created_at` offset could repeat one line and skip another that committed late with an earlier timestamp. Events written before the migration keep positions `1..n` in the `(created_at, id)` order readers already used, and later events start at `n + 1`. The cursor row records `legacy_rows` and `legacy_numbered`. The route keeps the legacy `created_at` paging for a run until the two match (and when the cursor table is missing), then switches to `seq` with consistent offsets. #### Rollout without a long table lock The migrations are split so the run events table is never exclusively locked for longer than a catalog change: | Migration | Step | Lock | | - | - | - | | `20261024030000` | nullable `seq`, cursor table, triggers, functions, `NOT VALID` constraints | brief `ACCESS EXCLUSIVE`, `lock_timeout` 5 s | | `030100`, `030150`, `030200` | unique `(run_id, seq)`, partial unsequenced and unique `(actor_id, idempotency_key)` indexes | `CREATE INDEX CONCURRENTLY`, one statement per file | | `030300` | backfill of at most 50 000 events in 5 000-row calls | row locks; same-run appends wait | | `030350` | `VALIDATE CONSTRAINT` once no event is unsequenced | `SHARE UPDATE EXCLUSIVE` | The migration runner wraps a multi-statement file in one transaction, and `CREATE INDEX CONCURRENTLY` cannot run inside one. Each concurrent index is therefore the only statement in its file. If a concurrent build fails, drop the invalid index before re-running, because `if not exists` would skip it. Check production size before applying (read-only): ```sql theme={null} select count(*) as events, pg_size_pretty(pg_total_relation_size('private.devbox_run_events')) as total_size, pg_size_pretty(pg_relation_size('private.devbox_run_events')) as heap_size, (select count(distinct run_id) from private.devbox_run_events) as runs from private.devbox_run_events; ``` If more than 50 000 events exist, `030300` leaves the rest for this one-time job. Run it with the service role or as the database owner, one call per transaction, until it returns 0, and then validate: ```sql theme={null} select private.backfill_devbox_run_event_seq(5000); -- repeat until 0 alter table private.devbox_run_events validate constraint devbox_run_events_seq_present; ``` Each call numbers at most 5 000 events and locks only the cursor row of the run it is numbering. `store-run-logs.test.ts` models a late commit and the legacy-to-sequence switch. `apps/database/supabase/tests/devbox-run-log-cursor.sql` checks density, forged sequences, validation, readiness and key constraints. To reproduce the concurrency boundary itself, use two sessions on a private socket-only Postgres cluster (`initdb`, then `pg_ctl -o "-c listen_addresses='' -k "`): 1. Session A runs `begin`, inserts an event for a run and sleeps before `commit`. 2. Session B inserts an event for the same run. 3. A reader pages `seq > cursor` before and after both commit. Before the migration the `created_at` reader sees `B,B`. After it, B blocks until A commits, and the reader sees `A,B`. ## Cache Policy Runners use named cache volumes for Bun installs, Turbo, Playwright browsers, Supabase Docker state, package manager cache, and optional `node_modules`. Cache keys include the repo fingerprint, lockfile hash, runtime image digest, platform, command profile, Bun/Node versions, and cache schema version. Cleanup runs on runner startup, after runs, and during heartbeat maintenance. The runner evicts incompatible caches first, including legacy Bun install caches when the Bun version, lockfile hash, package profile, or cache schema changes. It then enforces cache budgets with least-recently-used eviction while keeping a small set of recent compatible caches to avoid thrashing. Operators can inspect and prune cache metadata: ```bash theme={null} ttr box cache list ttr box cache doctor ttr box cache prune ``` ## Access Remote devbox API routes require a Tuturuuu CLI/app session and root workspace membership with `workspace_members.type = 'MEMBER'`. Guests and non-root workspace users cannot create runs, leases, previews, or runner tokens. ## Local Executable Verification For local source CLI development, use an isolated CLI config and disposable resource home so production CLI and scheduler state stay separate. `bun ttr` invokes the trusted global CLI; it does not run source. Use the explicit source entry below: ```bash theme={null} bun sb:status docker exec supabase_db_tuturuuu psql -U postgres -d postgres -tAc "select to_regclass('private.devbox_leases'), exists (select 1 from supabase_migrations.schema_migrations where version = '20260603171600')" TUTURUUU_CONFIG=/tmp/ttr-devbox-local-config.json TTR_RESOURCES_HOME=/tmp/ttr-devbox-local-resources bun packages/sdk/src/cli/index.ts login --base-url http://localhost:7803 TUTURUUU_CONFIG=/tmp/ttr-devbox-local-config.json TTR_RESOURCES_HOME=/tmp/ttr-devbox-local-resources bun packages/sdk/src/cli/index.ts box agent register --name local-devbox TUTURUUU_CONFIG=/tmp/ttr-devbox-local-config.json TTR_RESOURCES_HOME=/tmp/ttr-devbox-local-resources bun packages/sdk/src/cli/index.ts box agent start --token TUTURUUU_CONFIG=/tmp/ttr-devbox-local-config.json TTR_RESOURCES_HOME=/tmp/ttr-devbox-local-resources bun packages/sdk/src/cli/index.ts box run -- bun --version ``` The final command should print the remote command logs and exit with the remote command's exit code. In local Supabase, `private.devbox_runs.status` should end as `succeeded` with `exit_code = 0`. ## Production Readiness The production API needs migration `20260603171600_create_private_devboxes.sql` before any `ttr box` command can create leases or runs. If the CLI prints a schema-cache error such as `private.devbox_leases` not being found, first confirm the production migration workflow ran for the deployed SHA. If the table exists but PostgREST still returns the schema-cache error, refresh the schema cache from Supabase SQL Editor: ```sql theme={null} select to_regclass('private.devbox_leases'); select version from supabase_migrations.schema_migrations where version = '20260603171600'; notify pgrst, 'reload schema'; ``` Fleet selection rejects combining `--all` with explicit runner IDs. Maintenance is deferred when a runner does not report its hostname. Fleet upgrades honor `--timeout`; a timed-out run requests cancellation and reports whether that request succeeded. Verify its run and lease before retrying or touching another runner, since a stop request does not prove immediate process termination. ## Runtime Acceptance Diagnostics Judge capacity discovery runs a strict four-second `docker info` metadata probe before allocating a sandbox. A failure reports a fixed timeout, spawn (not found, permission denied, or other), numeric exit, signal, or missing-exit classification. These diagnostics omit Docker hosts, command arguments, raw output, and provider error messages. A timeout is not proof that the engine is absent: inspect the owning CI job and runner lifecycle before repairing or retrying. Successful discovery still requires registered gVisor, enforced cgroups, and reported CPU/memory capacity. Mocked process tests verify the classification and failure boundary; they do not verify a runner's Docker engine. The Lettin persistence browser regression separately reports project-navigation confirmation and the notebook-button action, retaining their existing deadlines and assertions. Phase failures emit an allowlisted error name and a fixed message summary rather than authenticated URLs or response bodies. A runner shutdown without a final assertion report leaves the underlying cause unproven. Preserve allocated fixtures until successful final cleanup or independently verified resource absence, and use the next exact-source CI result for runtime evidence. ## Trusted command execution boundary General devbox jobs intentionally execute caller-selected binaries and argv on trusted infrastructure. Only verified CLI app sessions or authenticated Supabase sessions with `MEMBER` membership in the root workspace can submit them through `/api/v1/devboxes/runs`. Membership in another workspace, root guest membership, and pull-request content alone do not authorize submission. Root members are trusted to execute code on enabled fleet runners; command-policy deny rules are operational guardrails, not a sandbox or an executable allowlist. The API persists the verified actor with the command in `private.devbox_runs`. Private tables have RLS enabled, and the claim RPC is restricted to `service_role`. Both the Web polling endpoint and the control Worker authenticate a hashed, non-revoked runner token and recheck its owner's root membership. The Worker supports heartbeat, poll, connect and internal wake notifications, not arbitrary job submission. Runner feature selection, active leases and trust-pool constraints limit claims. Host-side execution fences reject Judge and playground envelopes on ordinary trusted runners, and generic commands on Judge-only runners. Learner source reaches the dedicated Judge/playground paths, not generic host execution. Keep those pools and their credentials separate. The exact-commit envelope validates a full SHA and clean relative directory, fetches origin branches and requires the SHA to be reachable from an origin branch before making a private detached worktree. This proves provenance, not that project code or a branch is safe: root members must review what they submit. An explicit interpreter such as `sh -c` remains supported for these trusted users. The process launcher always uses `shell: false` and a separate argv array. It rejects control characters and shell metacharacters in the executable and NUL in arguments. Shell syntax in ordinary arguments remains literal; these checks do not restrict what an explicitly selected interpreter can execute. Regression coverage is in `packages/sdk/src/cli/devbox-job-process.test.ts`, `devbox-runner-trust.test.ts`, and `devbox-commit-run.test.ts`; API authorization coverage is in the devbox runs route tests. CodeQL command-line-injection alerts at this launcher require review of the authenticated execution boundary rather than treating `shell: false` alone as proof of safety. Source review does not verify deployed grants, runner configuration, or token custody. # SEO strategy across Tuturuuu Source: https://docs.tuturuuu.com/build/development-tools/seo-strategy Search intent, canonical ownership, product and docs content, privacy, localization, and measurable outcomes. ## Goals and audience Help someone discover the appropriate product, understand a concrete workflow, verify the implementation contract, and start an eligible workspace experience. Keep private content out of search. Measure qualified discovery and successful onboarding rather than treating raw indexed URL count as the goal. | Search intent | Canonical content | Next useful destination | | - | - | - | | Tuturuuu identity and capabilities | Web homepage, about, product directory | Product page and docs getting started | | Tasks/calendar/time planning | Web Tasks/Calendar/Track pages | App guide, scheduling rules, private app entry | | CRM/forms/business operations | Web CRM/Forms/Finance/Inventory pages | App guide, permission and publishing details | | Shopping for a published product | Owning Storefront product page | Store/cart; private checkout stays unindexed | | Learning a subject or tool | Nova public lessons, Tools pages, docs learning guides | Relevant practice or authenticated product | | Creating and discovering worlds | Tulletin landing, public explorer, published world | Creator content and authorized private studio | | Integrating or operating Tuturuuu | Docs SDK/API/package/runtime guides | Source-derived route/version references | ## Own one public URL per purpose Web owns broad product and solution descriptions. The satellite owns the usable public tool or published content when it serves one. Docs owns learning and integration contracts. Link these together with meaningful labels rather than copying the same landing page onto every host. Preserve redirect aliases when moving a public page; update canonicals, internal links, and sitemap ownership together. Every indexable page needs a specific title, an accurate description, a self-consistent canonical, and a useful server-rendered introduction. Metadata must describe visible behavior and actual availability. Avoid unsupported customer counts, ratings, compliance claims, AI autonomy, or inferred features from a retained route. A keyword tag does not replace useful content. ## Build content around actual workflows A product guide explains purpose, access, the common workflow, outcomes, limitations, and related products. Feature pages record scope, defaults, exceptions, and regression evidence. Tutorials teach one reproducible task. Reference pages identify contracts and versions. Troubleshooting pages map a symptom to the owning boundary and observable evidence. Use the [product directory](/platform/applications/overview), [getting started](/platform/guides/getting-started), [workflows](/platform/guides/workflows), and [repository inventory](/reference/repository/catalog) as the content hubs. Prefer substantive first-party explanations over automatically creating thin pages for every keyword, API handler, query variant, or unsupported use case. Generated route references locate source; they do not replace product tutorials. ## Localization and social discovery Web and Nova public pages use unprefixed English and `/vi` Vietnamese URLs. Distinct language pages need reciprocal hreflang and an English `x-default`. Canonical URLs exclude tracking/search parameters. Public apps that use one stable URL with locale preferences do not advertise fictitious translations. Maintain user-visible metadata copy in both message bundles when adding strings. Use the actual page/product title and description in Open Graph and Twitter cards. Use valid absolute images and meaningful alt text. Private record names, bearer tokens, emails, submitted answers, and hidden-case data must not leak into public schemas, titles, or discovery feeds. ## Technical requirements * Apply the [shared indexing policy](/platform/features/search-indexing) to every Next app and new route; default private routes to noindex. * Keep sitemap URLs on the owning host. Include only canonical, indexable, successful content. Do not publish auth, order-token, cart, or checkout URLs. * Use a publication-aware anonymous reader for dynamic discovery. An admin query or signed-in session must not expand a public sitemap. * Use real publication/update timestamps for `lastmod`; omit it when no reliable timestamp exists. Do not mark every static page fresh on each request/build. * Keep redirects and noindex responses crawlable so search engines can see them. * Serve real 404s for missing published content, maintain redirect continuity for moved public pages, and avoid soft-404 success pages in sitemaps. * Keep important copy and links in server HTML. Deferred interactive demos must not be the only explanation of a product. * Use structured data only for actual visible entities. Organization/WebSite/ WebPage/WebApplication can describe the platform; product, article, or other schemas require the corresponding real content and data. Do not fabricate reviews or expect a rich-result guarantee. * Noindex preview/staging and inactive migration shells. Authentication and authorization remain independent of SEO. ## Measurement and review For each verified release, inspect served headers, HTML, canonical/hreflang, sitemap and robots responses, and publication/access edge cases. Submit the owning public sitemaps through authorized Search Console/Bing accounts. Track: | Measure | Interpretation | | - | - | | Canonical indexed public pages | Compare declared eligible pages with Google's selected canonical/status | | Excluded private pages | Investigate any private route indexed or eligible unexpectedly | | Impressions and clicks by intent/page | Discover gaps in copy, topics, and public page usefulness | | Qualified app opens/onboarding | Connect discovery to an actual useful next step, with privacy-aware analytics | | Documentation task completion | Use feedback/search failures to prioritize missing instructions | | Crawl errors, redirects, and stale sitemaps | Find implementation/ownership drift | Establish a baseline before interpreting changes. Compare the same window and intent/page groups, accounting for release timing and seasonality. Search indexing and ranking are external outcomes; passing code checks does not prove either. Search Console account access, submission, and production deployment require their own authorized delivery scope. References: [Google developer SEO](https://developers.google.com/search/docs/fundamentals/get-started-developers), [localized versions](https://developers.google.com/search/docs/specialty/international/localized-versions), and [sitemap guidance](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap). # Stacked Pull Requests Source: https://docs.tuturuuu.com/build/development-tools/stacked-pull-requests Use native GitHub stacks for daily dependent PR work, cascading updates, and verified merges. ## Daily operating policy Use [native GitHub stacked pull requests](https://gh.io/stacks-overview) whenever reviewable changes depend on one another. This is our day-to-day workflow; no repository pilot or separate adoption approval is required. Split large dependent work into focused layers and continue on the next layer while earlier layers are reviewed. Keep independent changes on separate branches from `main`, and keep a single focused change in one PR. GitHub's upstream documentation currently labels the feature public preview. That upstream label does not make our adoption a pilot. Follow the current [GitHub requirements](https://docs.github.com/en/pull-requests/reference/stacked-pull-requests) when capabilities change. A stack has a trunk (normally `main`), a bottom PR targeting the trunk, and higher PRs targeting the branch immediately below. Each layer shows its own diff. Put foundations below their consumers; every layer must include the coverage and supporting changes needed to review it. Do not separate regression tests merely to leave an earlier layer unverified. ## Native stacks and fallback chains | Workflow | When to use it | Merge behavior | | - | - | - | | Native GitHub stack | Default for dependent PRs with branches in the same repository | GitHub tracks membership and merges a contiguous prefix; remaining work is rebased/retargeted | | Manual base chain | Cross-fork work or a concrete native-feature/tooling limitation | Merge parents individually with merge commits, verify retargeting, and rerun child gates | | Independent PRs | Changes with no dependency | Each targets `main` and can land independently | Native stacks require same-repository branches and fully linear history. Cross-fork stacks and GitHub Desktop stack support are unavailable. A missing CLI extension alone is not a reason to use manual chains: install it or create/link the stack on GitHub. Record the concrete fallback reason in the PR and coordination note. A non-main base or a stack label in a PR body does not establish native membership. ## Tooling and worktree ownership Install the official extension and inspect its supported commands: ```bash theme={null} gh extension install github/gh-stack gh stack --help ``` Use `gh extension upgrade stack` for an existing installation when an operation needs a newer capability. Inspect its help and release notes before relying on worktree support; do not mix extension versions during a paused operation. The upstream agent skill is installed at `.agents/skills/gh-stack`, with source tracking metadata for `github/gh-stack@v0.2.0`. Install or refresh it explicitly: ```bash theme={null} gh skill install github/gh-stack gh-stack --agent codex --scope project gh skill update --help ``` A bare `gh skill install github/gh-stack` lists available skills in a non-interactive terminal; supplying `gh-stack` installs it. Review upstream changes before updates. Keep the installed files as upstream content; repository ownership, branch naming, review gates, and delivery rules take precedence over generic skill examples. Every open PR has an isolated owning worktree under `.worktrees/`. Run `bun install` immediately after creating it. Never run `bun setup`, local builds, or local `bun validate`; use focused non-build checks and exact-head CI. Current upstream `gh-stack` supports branches distributed across linked worktrees. Its catalog and recovery journals live in Git's common directory, so mutations can affect more than the invoking checkout. Inspect active coordination notes, `git worktree list`, and each affected owner's status before `rebase`, `sync`, or `modify`. Coordinate with those owners and keep affected worktrees idle. The extension's mutation lock does not serialize ordinary Git, editors, or agents. Never use cascading commands to rewrite another owner's work without coordination. Use each branch's owning directory for edits and commits. Navigation commands such as `gh stack checkout` do not move a branch already checked out elsewhere; use the reported owning path (or `--print-path` where supported). Do not switch the shared main checkout or steal another worktree's branch. Claim the repository commit window before staging or committing; stage exact owned paths, not the extension's broad staging shortcuts. See the upstream [worktree and command reference](https://github.com/github/gh-stack) for version-specific behavior and recovery. ### Repository corrections to upstream examples Read these constraints together with the installed v0.2.0 skill; keep its vendored files unchanged so source tracking remains accurate: * A failed `gh stack sync` restores the cascade's stack branches on conflict, but does not undo completed fetches or trunk fast-forwards. Inspect trunk and affected branch refs/worktrees after failure; recovery is not proof that the entire invocation left the clone unchanged. * A push rejection can come from remote movement, permissions, or server-side rules. Inspect the reported error and all affected remote heads, resolve its actual cause, and only then retry. Do not treat every rejection as a lease race or assume a multi-branch push completed atomically. * `unstack` and `init` rebuild grouping metadata; they do not reorder existing Git ancestry. Preserve old boundary SHAs and deliberately reorder/rebase the commits first, then rebuild membership and rerun every affected gate. ### Recover unexpected replay after a parent merge A remaining child can have a valid scoped diff while the local catalog still tracks a merged parent. A cascading rebase can unexpectedly replay already landed commits and report conflicts in unrelated features. A stale parent tip is one possible boundary problem, but passing ancestry checks alone does not prove that the replay selected the correct range. Stop that replay rather than resolving those unrelated files into the child. 1. Abort the native rebase. Inspect every affected worktree and branch ref; completed fetches can remain after the abort. 2. Recover the authoritative GitHub stack membership and base chain. Preserve the pre-operation heads and local catalog boundaries. Check whether the tracked parent tip and saved child base are ancestors of the child with `git merge-base --is-ancestor `. Also inspect `git log origin/main..` and its diff, and compare the attempted replay with GitHub's actual layer diff. Both boundaries can be ancestors while the attempted replay still includes unrelated landed commits. Confirm the parent is merged and the remaining range contains only owned unmerged changes; record the observed boundary mismatch without inventing a root cause. 3. Coordinate every affected owner and require clean, idle worktrees. When that incorrect local replay range is confirmed, run `gh stack unstack --local` and rebuild local tracking with `gh stack init --base main ... ` in bottom-up order. This changes the local catalog; preserve the remote native group and its merge requirements. Do not remove server membership or rebuild ancestry as a shortcut around a native gate. 4. Inspect `gh stack view`, then use the normal native rebase and push. Review only the intended conflicts, compare the resulting source with the preserved heads, and rerun focused regressions. 5. Read GitHub membership, bases and immutable heads again. Reconcile any partial push and rerun exact-head CI, reviews, the quiet window and contiguous-prefix gates before merging. This recovery applies after a parent merge when the preserved child range and GitHub diff prove that the attempted replay includes unrelated landed commits. An unexpected conflict alone does not establish that diagnosis. Unknown ownership, dirty worktrees, unmerged parents or unexplained source differences block the metadata repair until their cause is understood. ## Create and submit a native stack Create dependent branches in their owning worktrees, then adopt them in bottom-up order. Run from the bottom layer's worktree: ```bash theme={null} git fetch origin git worktree add -b feat/slice-one .worktrees/slice-one origin/main (cd .worktrees/slice-one && bun install) # Implement, check, and commit the first layer before creating the child. git worktree add -b feat/slice-two .worktrees/slice-two feat/slice-one (cd .worktrees/slice-two && bun install) # Implement, check, and commit the second layer in its own worktree. (cd .worktrees/slice-one && gh stack init --base main feat/slice-one feat/slice-two) (cd .worktrees/slice-one && gh stack submit --auto) ``` `init` can adopt existing branches without switching their owning worktrees. For another dependent layer, create its worktree from the current top, install, and adopt that existing branch with `gh stack add feat/slice-three` from the top owner's directory. Confirm this behavior in the installed version's help. `gh stack submit --auto` creates draft PRs; use `--open` when ready for review. Inspect and edit every generated title/body to describe its focused change, validation, and dependencies. If PRs already exist, link them in bottom-up order without rewriting their branches: ```bash theme={null} gh stack link --base main gh stack view --json ``` `link` can push branches, create PRs, and correct their bases; inspect the intended membership and ownership first. Alternatively use GitHub's **Create stack** recommendation for an existing chain, or **Add to stack** for a new top layer. Do not rewrite green published branches merely to register native membership. Verify the stack map on GitHub or the PR REST resource's `stack` metadata. See [creating stacks](https://docs.github.com/en/pull-requests/how-tos/create-pull-requests/creating-stacked-pull-requests). When running in T3 Code, register every created or worked-on PR with `link_pull_request`, including each layer; `gh stack submit` and `link` do not register them with the thread. Before closeout, reconcile `list_thread_pull_requests`. GitHub's stack map supplies ordering; still explain any dependency important to reviewers in the PR body. ## Iterate and review Fix feedback in the layer that owns the behavior. Commit in its owning worktree, then cascade the change through higher layers: ```bash theme={null} gh stack rebase --upstack gh stack push ``` Use `gh stack rebase` to refresh the full stack from the trunk. These operations rewrite affected histories; `push` uses force-with-lease. Inspect affected diffs and record new heads, then rerun their focused checks, exact-head CI, and review quiet windows. Do not merge `main` into native stack branches: restore linear history with a cascading rebase. On conflict, resolve only the reported files in the reported owning worktree, stage them under its commit window, and run `gh stack rebase --continue`. Use `--abort` to restore the pre-operation state when appropriate. Do not delete or manually edit recovery journals. Server-side rebase creates unsigned commits; use local signing when signed commits are required. Humans can use the interactive `gh stack modify` for deliberate restructuring after checking clean owners, linear history, and absence of queued merges. Dropping work or rewriting another owner's branch needs explicit scope/coordination. Agents must not launch its TUI. For non-interactive restructuring, preserve the old boundary SHAs, coordinate owners, and deliberately rebuild ancestry and membership with `unstack` and `init`; do not drop work outside the authorized scope. Submit the resulting composition again and review new diffs and heads. Unstacking removes native membership while retaining bases; it changes the workflow to manual chaining, not independent PRs. See [stack maintenance](https://docs.github.com/en/pull-requests/how-tos/create-pull-requests/managing-stacked-pull-requests). ## CI and merge gates Native members use the trunk's protections, required checks, CODEOWNERS, and pull-request workflow selection, even when their immediate base is another layer. A workflow filtering PRs to `main` runs for native members whose trunk is `main`. Custom automation must distinguish immediate base from stack trunk and handle absent `github.event.pull_request.stack` for ordinary PRs. Do not disable a layer's required validation just because a higher layer is green. See the [Actions runbook](/build/devops/github-actions-runbook#validation-and-delivery-evidence). Every selected layer needs its own resolved review threads, required reviews, applicable successful exact-head checks, and five-minute review quiet window (unless the user specified another duration). New pushes or review activity reset the affected layer's window. Record the complete selected prefix and all immutable heads immediately before merging; stack CLI state alone is not a gate verifier. ## Merge a verified prefix Choose the highest PR in the authorized, fully verified contiguous prefix starting at the lowest unmerged layer. Landing the bottom alone remains useful when later layers need work. Landing a higher PR includes every unmerged PR below it; those PRs must all be within the authorized scope and pass the gates. | Selected PR | Included work | | - | - | | Lowest unmerged | One layer | | Middle | All unmerged layers through that PR | | Top | All remaining layers | For a native stack, use the official stack merge operation with an explicit PR and merge method: ```bash theme={null} gh stack merge --merge --yes ``` Do not use a bare non-interactive `gh stack merge`: it can select the entire stack. Native merges support merge, squash, and rebase; `--merge` remains our normal choice. The extension checks only basic PR state before asking GitHub to merge and has no equivalent to `gh pr merge --match-head-commit`. Installed `gh stack` v0.2.1 also omits `bypass_rules`; its help's unsupported-bypass claim is a client limitation, not an API limitation. Re-read all selected heads and gates immediately before invoking it; changed pins require validation again. Do not apply the ordinary single-PR admin merge endpoint to native stacks. ### Authorized native administrator merge Normal atomic merging of a verified prefix remains the default. For an explicitly user-authorized administrator merge blocked only by repository policy, prefer the supported [asynchronous REST merge API](https://docs.github.com/en/rest/pulls/pulls#merge-a-pull-request-asynchronously) with boolean `bypass_rules: true` over dissolving native membership. GitHub's [stack merge requirements](https://docs.github.com/en/pull-requests/reference/stacked-pull-requests#merge-requirements) restrict bypass to the bottom PR; it cannot merge a higher prefix or whole stack. The OpenAPI's general downstack description does not remove this restriction. Confirm the authenticated actor has permission to bypass the applicable rules. Authorization and actor permission are both required; this is not permission to bypass CI or change rulesets/security settings. Retain the complete prefix's scope, successful applicable exact-head CI, review resolution and required-review evidence, and quiet windows before proceeding. Immediately re-read native membership, every selected head/base, trunk SHA and all gates against the retained pins. The API's `sha` guards only the selected target head, not each lower head or the trunk; any movement requires reassessment. Select only the current bottom unmerged PR with its pinned head: ```bash theme={null} gh api --method PUT repos/tutur3u/platform/pulls//merge-async \ -H 'Accept: application/vnd.github+json' \ -H 'X-GitHub-Api-Version: 2026-03-10' \ -f sha= -f merge_method=merge -f merge_action=direct_merge \ -F bypass_rules=true ``` `-F` sends a boolean; `-f bypass_rules=true` would send a string. Retain the HTTP status/body and any operation UUID. HTTP 200 can mean already merged or already queued and may omit the UUID: inspect the body and actual PR state. Only when a UUID is present, read that exact operation with `GET /repos/tutur3u/platform/pulls//merge-async/`. HTTP 202, pending or enqueued means acceptance, not completion. Record the actual terminal result, verify the bottom PR merged and record the resulting trunk SHA before advancing. Re-read the remaining native group, heads, bases, diffs, exact-head CI, reviews and quiet windows before each successor; automatic rebase/retargeting can change pins. Repeat bypass only for a newly verified bottom PR within explicit authorization. On HTTP 409, inspect the existing UUID/options and actual server state without blind retry. Unknown or rejected outcomes remain unresolved. API schema support alone does not prove a successful merge. Merge queues may enqueue the selected group and split it across consecutive merge groups. Queued is not merged; wait for every selected PR's merged state and record the resulting trunk SHA. Native stack auto-merge is not supported. See [GitHub merge requirements](https://docs.github.com/en/pull-requests/how-tos/merge-and-close-pull-requests/merging-stacked-pull-requests). After a partial merge, confirm remaining membership, retargeting, diffs and heads. Run `gh stack sync` only after coordinating affected worktrees: it can fetch, fast-forward trunk, rebase remaining branches, and push. Rerun gates for changed heads. Avoid `--prune` until each branch/worktree is eligible for cleanup; do not update another session's main checkout or resolve divergence by discarding its work. Verify exact merged-main CI for the requested integration. Production sync and its deployment/migration verification require production authorization; a request to merge into main alone does not authorize `bun git-sync`. ## Manual base-chain fallback Without native membership, merge one PR at a time, parents first. Use a merge commit (`gh pr merge --merge --match-head-commit `) to keep parent ancestry reachable from `main`. Squash/rebase merges can make child diffs repeat already landed work; repair the child deliberately and rerun its gates. Confirm each child's `baseRefName`, diff, and checks after the parent merges. Retarget with `gh pr edit --base main` if needed. A base edit may not trigger the configured workflows, so verify actual runs rather than assuming CI restarted. Do not delete remote parent branches outside the PR merge flow: dependent PRs can close instead of retargeting. ## Cleanup After the PR is confirmed merged into `main` and the requested delivery gates pass, remove only its completed clean owning worktree and local task branch. Keep dirty, blocked, unmerged, active, or other-owned work. When production delivery was requested, retain the worktree through production verification. Archive your completed coordination note; never commit it. ```bash theme={null} git worktree remove .worktrees/slice-one git branch -d feat/slice-one ``` # Thanh Local End-to-End Environment Source: https://docs.tuturuuu.com/build/development-tools/thanh-local-e2e Run the isolated Supabase stack, web, Playwright and mobile simulator/emulator end-to-end tests on the Thanh Mac Studio. The Thanh Mac Studio (`ssh skr-studio`) hosts a repeatable local end-to-end environment: an isolated Supabase project with migrations and seed data, the `apps/web` dev server, Playwright, and the mobile app on a dedicated iOS Simulator and Android Emulator. Use it to validate changes against real simulator/emulator runs instead of host-only widget tests. The entry point is `scripts/thanh-e2e/thanh-e2e.mjs`. It works on any macOS host with the same toolchain; Thanh defaults are baked in and overridable by environment variables. > [!WARNING] > > Use Thanh only for this lane. Queue heavy steps (`up`, `seed`, `web`, > `web-e2e`, `mobile-e2e`) with `ttr resources run --` from a checkout under > `/Users/thanh/ci-lanes/tuturuuu-*`, and never stop Upskii or other lanes' > containers, emulators or queue owners. Android's dummy Firebase input is created exclusively: an existing file or symlink is preserved, including a dangling symlink. PID state is opened without following symlinks and classified and read through the same descriptor; malformed PID values are ignored, while unsafe file types fail explicitly. Run the focused filesystem regressions with `node --test scripts/thanh-e2e/*.test.mjs`; no simulator or database setup is required. ## Toolchain Everything Tuturuuu-specific lives in user space under `/Users/thanh/ci-lanes/tuturuuu-tools` (`$T`). Source `$T/mobile-env.sh` first; it sets `DEVELOPER_DIR`, `JAVA_HOME`, `ANDROID_HOME`/`ANDROID_AVD_HOME`, `PUB_CACHE`, `GRADLE_USER_HOME`, an isolated `XDG_CONFIG_HOME` for Flutter settings, and `PATH` (Flutter 3.47.7, Temurin 17, Android SDK tools, Bun, Docker). | Component | Location | | - | - | | Xcode 26.5 + iOS 26.5 runtime | `/Applications/Xcode.app` | | Flutter 3.47.7 | `$T/flutter-3.47.7-stable-arm64/flutter` | | JDK 17 (Temurin) | `$T/jdk` | | Android SDK (platforms 36 + 37, NDK 28.2, API 36 arm64 image) | `$T/android-sdk` | | AVDs | `$T/android-avd` | | E2E state (Supabase workdir, env files, logs, screenshots) | `$T/e2e/` | `xcode-select` on Thanh still points at the Command Line Tools. Dart native-asset build hooks run with a filtered environment that keeps `PATH` but drops `DEVELOPER_DIR`, so `$T/bin/xcrun` pins Xcode. Without it, iOS builds fail in `objective_c/hook/build.dart` with `Bad state: No element`. Remove the shim after an administrator runs `sudo xcode-select -s /Applications/Xcode.app/Contents/Developer`. ## Isolation | Resource | Value | Override | | - | - | - | | Supabase project id | `tuturuuu-e2e` | `TUTURUUU_E2E_PROJECT_ID` | | Supabase ports | `8000`-`8007` (API `8001`, DB `8002`, Studio `8003`, mail `8004`) | `TUTURUUU_E2E_SUPABASE_BASE_PORT` | | Web dev server | `http://127.0.0.1:7803` | `TUTURUUU_E2E_WEB_PORT` | | Satellites | Calendar on `http://localhost:7806` | `TUTURUUU_E2E_SATELLITES` (`calendar`, `none`) | | iOS Simulator | `tuturuuu-ios-iphone17pro` | `TUTURUUU_E2E_IOS_SIM` | | Android Emulator | `tuturuuu-android-36-pixel9pro` on `emulator-5580` | `TUTURUUU_E2E_AVD`, `TUTURUUU_E2E_AVD_PORT` | The ports stay canonical on purpose: the web local-E2E auth and captcha bypass (`apps/web/src/lib/auth/local-e2e.ts`) and the Playwright safety guard (`apps/web/e2e/helpers/environment.ts`) only trust Supabase `:8001` and web `:7803` on loopback. With other ports, mobile password sign-in fails with `Turnstile is not configured`. Isolation comes from the project id instead: the script refuses the default `tuturuuu` id and `up` fails fast when any port in the block is already listening, so it never adopts another lane's stack. Only one stack on the canonical ports can run on a host at a time; stop it with `down` when you finish. The Supabase config is staged from tracked `apps/database/supabase` files and rewritten with the isolated helpers from `apps/database/scripts/run-supabase-isolated.js`. Docker over SSH cannot use the macOS keychain (`credsStore: desktop`). The script points `DOCKER_CONFIG` at a credential-free directory in the state dir and `DOCKER_HOST` at the Docker Desktop socket, so public image pulls work anonymously and the keychain is never touched. ## Run it ```bash theme={null} ssh skr-studio . ~/ci-lanes/tuturuuu-tools/mobile-env.sh cd ~/ci-lanes/tuturuuu-e2e # any checkout under ci-lanes/tuturuuu-* bun install # once per checkout E2E=scripts/thanh-e2e/thanh-e2e.mjs node $E2E doctor ttr resources run -- node $E2E up # start + migrations + seed ttr resources run -- node $E2E web # build types/supabase, start web + satellites node $E2E devices # boot simulator + emulator headless ttr resources run -- node $E2E web-e2e --install -- e2e/auth.noauth.spec.ts ttr resources run -- node $E2E mobile-e2e ios ttr resources run -- node $E2E mobile-e2e android ttr resources run -- node $E2E mobile-e2e android --mode=profile node $E2E status node $E2E down # stop web + Supabase; --web-only, --purge ``` `seed` re-applies migrations and seed data. `env` rewrites `$T/e2e//env/web.env` and `mobile.json` from `supabase status`. `web.log` and other logs live in the state dir's `logs/`. ## Test accounts The seed creates `local@tuturuuu.com` (root admin) and `user1`-`user4@tuturuuu.com` with the public local-only seed password from `apps/database/supabase/seed.sql`. The mobile flow defaults to `local@tuturuuu.com`; override with `--dart-define=E2E_EMAIL=...` only for other seeded accounts. ## Satellites Web proxies some owner APIs to satellite apps; Calendar's event API is proxied to the Calendar app, so without it Agenda shows `ApiException(500): Request failed` and web logs `Failed to proxy https://calendar.tuturuuu.localhost/... ECONNREFUSED`. `web` therefore also starts each satellite in `TUTURUUU_E2E_SATELLITES` (default `calendar`) on its default port with the same local Supabase env and `WEB_APP_URL`/`INTERNAL_WEB_API_ORIGIN` pointing back at web; web gets `CALENDAR_APP_URL=http://localhost:7806`. `down` stops web and satellites. `web` also builds `@tuturuuu/internal-api`, which Calendar imports from `dist/`. Satellites run on their Next.js runtime (`next dev`), not Vite+ `dev:app`. Vite+ dev runs Calendar inside workerd, which loads linked workspace packages unbundled; `@tuturuuu/internal-api` builds as CommonJS (`NodeNext` without `"type": "module"`), so startup fails with `ReferenceError: exports is not defined`, or `Failed to resolve entry for package "@tuturuuu/internal-api"` before it is built. The Next runtime serves the same route handlers. ### Native Worker imports Workerd rejects random generation during module initialization. Meet and Parley's shared chat initializes its unguessable mention marker on first rendering use, then reuses it for that module instance. Keep the marker lazy; a fixed marker would let authored links impersonate generated mentions. The import and lifetime regressions are in `packages/meet-core/src/features/call/lib/assistant-identity-import.test.ts`. An authenticated native preview returning 500 is not a valid performance sample. ## Mobile wiring `mobile.json` is passed with `--dart-define-from-file` and routes `NEXT_PUBLIC_SUPABASE_URL` and every `*_API_BASE_URL` owner to loopback `127.0.0.1`. Android permits cleartext only to `127.0.0.1`/`localhost`, and the app rewrites `localhost` to `10.0.2.2`, so the script uses `127.0.0.1` and runs `adb reverse` for the web and Supabase ports. All satellite API owners point at the local web server, which owns the live API handlers. `apps/mobile/integration_test/e2e_flows_test.dart` signs in with the seeded account, opens Agenda, opens Settings → Preferences, changes the personal timezone (default `Pacific/Niue`, override with `--dart-define=E2E_TIMEZONE=...`; pick a zone near the top of the picker, whose list is lazy and lives in a draggable sheet) and returns to Agenda. It fails if an `ErrorWidget` renders at any step. At each checkpoint it prints an `E2E_SHOT ` marker and holds the frame; the runner streams `flutter drive` output and captures the device with `xcrun simctl io ... screenshot` or `adb exec-out screencap`, so screenshots work in debug, profile and release builds. Shots and `flutter-drive.log` land in `$T/e2e//shots/--/`. (On Android, `IntegrationTestWidgetsFlutterBinding.takeScreenshot` after `convertFlutterSurfaceToImage` captured only the first frame.) Before each run the script prepares the gitignored Firebase inputs the same way mobile CI does: `scripts/mobile-deployment/firebase-service-config.py` copies the tracked development plist on iOS, and Android gets the synthetic `google-services.json` CI writes. iOS builds run `pod install`, which can leave `ios/Podfile.lock` and `project.pbxproj` modified in the E2E checkout; do not commit those side effects. `flutter test -d test/...` does not run on the device: files outside `integration_test/` run on the host VM. Widget tests that resize `tester.view` also fail under the live binding, so on-device coverage belongs in `integration_test/`. ## Release-mode gray screen Release builds replace the debug red error box with a gray `ErrorWidget`. iOS simulators run debug builds only (Flutter refuses `--profile` and `--release`), so reproduce release-only rendering on the Android emulator with `mobile-e2e android --mode=profile` or `--mode=release`. The same `ErrorWidget` assertion reports the failing screen, and the screenshots show it. ## Web Playwright `web` and `web-e2e` set `BASE_URL=http://127.0.0.1:7803`, the local Supabase keys and `TUTURUUU_LOCAL_E2E_AUTH_BYPASS=1`, the same local-only opt-in CI web E2E uses. `local-e2e.ts` honors it only for the loopback `:7803`/`:8001` origins; without it the `/api/auth/dev-session` rate-limit reset returns 403 and most auth specs fail in `beforeEach`. `--install` installs the Playwright Chromium build first. Pass spec paths or Playwright flags after `--`. ## Gotchas * Browsers must open web at `http://localhost:7803`, not `127.0.0.1`: Next dev serves its dev resources (HMR, hydration) only to `localhost`, so a `127.0.0.1` page stays unhydrated and the login form remains disabled. The script uses `localhost` for `BASE_URL`/`NEXT_PUBLIC_WEB_APP_URL` and keeps `127.0.0.1` for mobile, which Android cleartext requires. * `bootstrap()` installs the app's `FlutterError.onError`. The integration test restores the test binding's handler after `app.main()`; otherwise a framework error surfaces only as `A test overrode FlutterError.onError ...` and the real failure is hidden. * Mobile login ignores "Continue with email" while OTP availability resolves (a short grace period); the test retries the tap until the password stage renders. * Every wait in the flow captures `timeout-