Path-A task 4/5. The root landing logic in apps/web/app/page.tsx was
redirecting to `/${workspaceId}` (UUID, ugly) and using no ORDER BY
(so two sessions could land on different workspaces). It also looped
zero-workspace users through `/sign-in`.
Changes:
* Inner-join workspaceMembers with workspaces to fetch the slug, not
just the id. Order by membership createdAt ascending so users
consistently hit their oldest workspace.
* Redirect to /{slug} (slug, not UUID).
* Removed the unused `objects` / `and` imports that were lint
warnings.
* Zero-workspace branch redirects to /sign-in?error=no_workspace as a
defensive fallback; documented inline that this is unreachable for
fresh sign-ins post `ensureUserHasWorkspace` in apps/web/lib/auth.ts.
The dashboard at /{slug}/ is no longer a mockup (post commit f64d307
which wired it to objects.stats and objects.listRecent), so landing
there now shows real state.
Filed plans/Plan-daily-driver-finish/Epic-shipping-the-shell/
Task-onboarding-zero-workspace-flow.md (P2) as the follow-up that
turns the defensive fallback into a proper welcome flow with a
shared workspace-provisioning helper.
`pnpm lint && pnpm type-check` clean. Closes
plans/Plan-daily-driver-finish/Epic-shipping-the-shell/
Task-pick-workspace-landing-route.md.
Co-authored-by: Cursor <cursoragent@cursor.com>
Path-A task 3/5. Replaces the setTimeout mock that returned the literal
"Full AI integration is coming soon!" string with a real streaming
provider call.
* apps/web/app/api/chat/route.ts (new): POST handler that runs the
same auth + resolveWorkspace pipeline workspaceProcedure uses, then
streams a response from streamText().toDataStreamResponse(). Maps
resolveWorkspace's TRPCError codes to HTTP status (401/403/404/400).
Returns a structured 503 with a human-readable hint when
OPENAI_API_KEY is unset, so the misconfiguration is surfaced rather
than masked by a fake stream.
* apps/web/app/(app)/[workspaceSlug]/ai/page.tsx: replace the local
message-state + setTimeout placeholder with useChat from
@ai-sdk/react. workspace slug is sent on every request body so the
server can enforce tenant scoping. Adds a ChatErrorBanner that
parses the JSON error body the route emits and renders amber for
the "unavailable" case, destructive for other failures.
* apps/web/package.json: pull in @ai-sdk/react as a direct dep
(previously only transitive via `ai`).
The existing aiRouter.chat tRPC mutation is left intact — it powers
the right-panel command palette via the non-streaming generateText
path, and rebuilding that as streaming was outside the scope of
making the dedicated chat page usable.
Provider selection still flows from env per packages/ai conventions:
OPENAI_API_KEY gates availability, OPENAI_BASE_URL lets operators
route through Ollama on CT 108 transparently, OPENAI_MODEL overrides
the default gpt-4o-mini.
`pnpm lint && pnpm type-check` clean. Closes
plans/Plan-daily-driver-finish/Epic-shipping-the-shell/
Task-wire-ai-chat-to-trpc.md.
Co-authored-by: Cursor <cursoragent@cursor.com>
Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and
hardcoded `recent` list on the workspace-home page with real
workspace-scoped data.
* server/routers/objects.ts: add two new procedures.
- `objects.stats` returns { openTasks, containers }. Open-task count
treats null status as open; only `done` and `closed` (per
packages/shared object-statuses) are terminal. Container count
aggregates project + space + group rows.
- `objects.listRecent({ limit })` returns the N most-recently-updated
rows, descending by updated_at. Excludes archived and excludes
`workspace`/`group` from the activity feed (containers clutter
"what did I just touch" recency).
Both go through workspaceProcedure, so the workspace_id filter
comes from the middleware-resolved ctx.workspace.id rather than
any user input.
* app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new
procedures via @trpc/react-query. Adds:
- Skeleton loading state (no flash of zeros).
- Empty state with a "New task" CTA on workspaces with no objects.
- Real "X ago" labels on the recent feed.
- Click-through links from recent rows to /{slug}/{id}.
- A locally-mounted CreateObjectDialog instance independent of the
global one in AppShell so the empty-state CTA can pre-seed
defaultType="task" without coordinating shared state.
* components/ui/skeleton.tsx: new (standard shadcn pulse skeleton).
Used by the dashboard but reusable across the app.
The scaffolded "Due this week" stat is dropped: `objects` has no
due_at column and the task explicitly preferred dropping a card to
schema-creep.
`pnpm lint && pnpm type-check` clean. Closes
plans/Plan-daily-driver-finish/Epic-shipping-the-shell/
Task-wire-workspace-home-dashboard.md.
Co-authored-by: Cursor <cursoragent@cursor.com>
Path-A first task: get the repo's two repo-wide quality gates passing.
Both were failing from a clean clone in ways that were silently hiding
each other.
Headline fixes:
* apps/web: add an eslint 9 flat config (eslint.config.mjs) using
FlatCompat against next/core-web-vitals + next/typescript, and switch
the `lint` script from `next lint` to `eslint .`. Previously `next
lint` fell into its interactive setup prompt because there was no
config at all in apps/web, which made `pnpm lint` permanently fail
before any rule ever ran.
* packages/shared/src/utils/id.ts: replace `randomUUID` from `node:crypto`
with `globalThis.crypto.randomUUID`. `@tasks/shared` is forbidden from
using Node-only APIs (per AGENTS.md / repo-overview.mdc) because it
has to be importable from the browser bundle.
Adjacent fixes pulled in to make the gates actually green:
* apps/mcp-server/tsconfig.json: drop vestigial rootDir / declaration*
/ outDir / sourceMap (build is via tsup, not tsc emit) and add
allowImportingTsExtensions. The MCP server uses `.ts`-extension
re-export shims (db.ts / schema.ts / shared-types.ts) so tsup can
inline workspace .ts sources into the bundle.
* apps/collab-server/tsconfig.json: same simplification.
* apps/mcp-server/package.json: add @types/node so `process.env` in
packages/database/src/client.ts (transitively pulled into the MCP
server's type-check) resolves.
* apps/web/components/ui/input.tsx: empty `interface InputProps extends
React.InputHTMLAttributes<HTMLInputElement> {}` -> `type` alias.
* apps/web/server/lib/workspace-guard.ts: `from(args.table as any)` ->
`as unknown as PgTable` with a comment. Standard drizzle escape
hatch for structural generic tables.
* apps/web/components/whiteboard/shapes/{document,project,task}-card.tsx:
`BaseBoxShapeUtil<any>` -> `BaseBoxShapeUtil<{Shape}>` plus inline
`declare module "@tldraw/tlschema"` augmentation of
TLGlobalShapePropsMap. Required adding @tldraw/tlschema as a direct
devDep of apps/web so the augmentation target resolves; previously
it was only present transitively under tldraw's own deps.
Result: `pnpm lint && pnpm type-check` exits 0 across all 6 packages.
16 unused-import / exhaustive-deps warnings remain; they're pre-existing
housekeeping and out of scope for this task.
Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/
Task-fix-lint-and-shared-types.md (status: done).
Co-authored-by: Cursor <cursoragent@cursor.com>
Three new plan trees that fill in the gaps surfaced during repo review.
Together they map out what remains between the current scaffold-with-stubs
state and a daily-usable, multitenant, agent-coordinated app.
* Plan-daily-driver-finish (P0): turn stubs into real data. Five tasks
covering the lint/shared-types breakage, hardcoded dashboard mocks,
AI-page setTimeout placeholder, post-signin landing decision, and a
cross-browser collab smoke test against the deployed Hocuspocus
instance.
* Plan-multitenant-saas-hardening (P1): everything multitenant needs
beyond what Plan-multitenant-cursor-sync already covers. Invites and
role management, soft-delete + append-only audit log, rate limits on
the auth + mutation hot paths, and a Vitest + GitHub Actions test
foundation so PRs can't ship red.
* Plan-agent-coordination (P2): the layer that makes a Task-*.md
runnable, not just readable. Adds workflow_prompt with task -> epic
-> plan inheritance, an agent_runs table for auditable sessions, and
two new MCP tools (claim_task / complete_task) that replace the
freeform update_object composition agents do today. Includes an
intentionally-deferred Epic-optional-orchestrator that captures the
Symphony-shaped runner as a decision point rather than an immediate
build.
Each task is bead-scale (one focused Cursor session) with explicit
in-scope, out-of-scope, and anti-goal sections so a future agent can
pick up a single Task-*.md and start without scrollback context.
Co-authored-by: Cursor <cursoragent@cursor.com>
Three pieces of authentication work that need to land together so OAuth
sign-ins produce a usable session.
* `ensureUserIdByEmail` upserts a `users` row on every OAuth sign-in
matched case-insensitively on email, then stamps `token.id` with the
resulting UUID so workspace-scoped tRPC procedures can resolve
membership. Credentials sign-in already returned the DB id from
`authorize`; OAuth now does the equivalent.
* `ensureUserHasWorkspace` mints a personal workspace (and `owner`
member row) on first sign-in for any user that doesn't already
belong to one, so fresh OAuth accounts don't land in the app with
no tenant scope. Idempotent; slug collisions retry with a random
suffix and cap at 5 attempts.
* Migration 0004 adds a `UNIQUE (lower(email))` index on `users` to
match the lookup pattern and prevent two providers from minting
rows that differ only in casing. Existing rows are normalized to
lowercase first; the column-level UNIQUE catches any pre-existing
duplicates so they get resolved by a human rather than silently
merged.
Sign-in / sign-up pages add an Authentik SSO button (gated on
`AUTH_AUTHENTIK_*` env vars). Layout switches to GitHub+Google on top
with Authentik full-width below.
Co-authored-by: Cursor <cursoragent@cursor.com>
Block A of the EchoDo plan. Workspaces used to live as `objects(type='workspace')`,
which made it impossible to put a real RLS-friendly tenant boundary on the schema
or to give each workspace a stable URL slug. This commit:
- Adds a top-level `workspaces` table (slug unique, owner FK, plan_tier hook).
- Migrates the 8 anchor tables (objects, workspace_members, object_type_defs,
property_definitions, templates, forms, markdown_backlog_items,
cursor_sync_mappings) to FK into `workspaces.id` instead of `objects.id`,
with a hand-augmented data-copy migration that preserves IDs and slug-collision-
proofs on backfill.
- Introduces a `workspaceProcedure` tRPC middleware + `resolveWorkspace` helper
that take a UUID-or-slug `workspace` handle and expose `ctx.workspace`. All
tenant-scoped routers (objects, types, properties, templates, forms, search,
ai, relations, favorites) now flow through it.
- Updates the web app to pass `workspace` slugs from the URL (or store) instead
of the old `workspaceId`, including a workspace-sync layer that rewrites
/<UUID>/... links to /<slug>/...
- Updates the MCP tools (list_objects, create_object, search_objects) and the
workspace://{handle}/tree resource to accept either a slug or UUID so existing
agents keep working.
- Adds a Create Workspace dialog and a Workspace Settings page (rename + slug
rename with redirect, owner-only archive).
Verified locally against a fresh Postgres: migration applies cleanly, slug
uniqueness holds, tenant data is isolated by workspace_id, slug↔UUID resolution
works in both directions, and ON DELETE CASCADE cleans up child rows in the
correct workspace only.
Co-authored-by: Cursor <cursoragent@cursor.com>
Introduces a small graph executor under packages/ai/src/graph and a
code-review pipeline (analyze, summarize, compose) wired through new
prompt builders. Re-exported from the package index.
Co-authored-by: Cursor <cursoragent@cursor.com>
Repo-wide agent guide at the root plus path-scoped Cursor rules for
the web app, database/migrations, and the plans/ markdown backlog.
Co-authored-by: Cursor <cursoragent@cursor.com>
The collab server was using createRequire() with hardcoded relative paths
(../../../packages/database/package.json, ../node_modules/@hocuspocus/server/...)
to grab `eq` from drizzle-orm and `Forbidden` from @hocuspocus/common. That
worked under tsx in dev but fell apart in the bundled prod image because
those paths don't exist there and pnpm's symlink topology in the runtime
node_modules wasn't reachable from inside the bundled dist file.
Result at runtime: Cannot find module '@hocuspocus/common' on every
collab restart, infinite crash loop.
Switch both to plain ESM imports — tsup bundles them into dist/index.mjs
directly, no runtime resolution needed. Add @hocuspocus/common and
drizzle-orm as explicit deps so they're properly tracked.
Verified locally: bundle now resolves cleanly with no createRequire calls
(actually shrinks from 304 KB to 102 KB after dead-code elimination).
Made-with: Cursor
CT 107 bumped to 8 GB RAM. Capping the heap below the available memory
makes V8 GC-thrash near the limit and contributes to oom-kills. Set
--max-old-space-size to 5120 MB (~60% of host) so the compile has real
headroom while leaving room for kernel + dockerd + buildkit + tsup workers.
Made-with: Cursor
@tasks/database (and @tasks/shared) export raw .ts files via "main" /
"exports", which works fine for tsx/dev but blows up at runtime in the
production image:
ERR_UNKNOWN_FILE_EXTENSION: Unknown file extension ".ts" for
/app/packages/database/src/client.ts
tsup was leaving those workspace imports as externals in the output, so
the deployed dist/index.mjs still tried to resolve them at startup.
Switch each app to a tsup.config.ts that sets noExternal: [/^@tasks\\//],
which inlines workspace packages into the bundle while keeping
node_modules deps (postgres, drizzle-orm, hocuspocus, mcp-sdk, etc.)
external.
Verified locally: collab-server bundle size goes from 7 KB to 304 KB,
confirming @tasks/database is now compiled in. Also fixed the collab
start script to point at index.mjs (tsup ESM output) instead of .js.
Made-with: Cursor
The 1.5GB heap cap was actually too aggressive for the Next 15 webpack
compile on this monorepo: V8 spirals into GC churn near the cap, runs
the host out of free memory, and oom-killer SIGKILLs the build (exit 255).
With 4GB on the host now:
- Cap V8 heap at 2.5GB (~70% of host RAM) — gives the compile real
headroom while leaving room for kernel + dockerd + concurrent COPY
layers from the collab/mcp runtime stages.
- Add depends_on so the lighter collab + mcp services finish building
before the web target starts its peak-memory compile, instead of
fighting it for memory in parallel.
Made-with: Cursor
next build was OOM-killed (exit 255) on the Coolify host during
'Linting and checking validity of types' — tsc pulls the whole type graph
into memory and pushes the build past available RAM.
Type-checking and linting belong in CI / pre-commit, not in the prod
container build. Skip them via next.config and cap the Node heap to 1.5 GB
so any future memory blow-up surfaces as a JS OOM instead of a SIGKILL.
Made-with: Cursor
The Next.js standalone runtime stage copies apps/web/public into the image,
but the directory was never committed since the project hasn't shipped any
static assets. BuildKit fails with "not found" on the COPY. Adding an empty
.gitkeep guarantees the directory exists at build time and lets future static
assets drop in without further build changes.
Made-with: Cursor
Collapse Dockerfile.web / Dockerfile.collab / Dockerfile.mcp into a single
docker/Dockerfile with shared `deps` stage and `web` / `collab` / `mcp`
runtime targets. BuildKit hashes the deps stage identically for all three
targets, so `pnpm install` runs once instead of three times in parallel —
fixes the OOM kill on the Coolify host (CT 107) during `docker compose build`.
Both docker-compose.yml (local dev) and docker-compose.coolify.yml now
select services via `target:` instead of separate dockerfile paths.
Made-with: Cursor
Two AI components were importing from ../../../../packages/ai/src using
relative paths that escape the workspace root. This worked locally because
all packages are siblings on disk, but failed in Docker where Dockerfile.web
only copies apps/web, packages/database, and packages/shared into the
build context — packages/ai never made it in.
Changes:
- Add @tasks/ai as a workspace dependency in apps/web/package.json
- Switch both imports (command-palette.tsx, ai-block.tsx) to "@tasks/ai"
- Add @tasks/ai to transpilePackages in next.config.ts and the docker variant
- Copy packages/ai into the Docker build context (Dockerfile.web)
- Refresh pnpm-lock.yaml for the new workspace edge
Verified locally: web builds compile cleanly past the previously failing
"Module not found" errors. (Local final step hits an unrelated ENOSPC on
the dev disk; Coolify's volume has plenty of headroom.)
Made-with: Cursor
The previous multi-stage `deps` → `builder` split copied only the root
/app/node_modules from the deps stage, but pnpm workspaces also create
per-package node_modules directories (e.g. apps/web/node_modules) that
contain the .bin symlinks for `next`, `tsup`, etc. Without those, the
builder stage failed with `sh: tsup: not found` and `sh: next: not found`.
Collapse `deps` and `builder` into a single stage so the per-package
node_modules survive intact. pnpm's content-addressable store keeps
re-installs nearly free on cache hits, and Docker layer caching still
short-circuits the install step when only source files change.
For collab and mcp runner stages, also copy the per-app node_modules
so runtime dependency resolution works.
Made-with: Cursor
Coolify invokes `docker compose` with --project-directory set to the
repository root, not this file's directory. Previous `context: ..` was
resolving to /artifacts (one level above the repo) and failing with
`resolve : lstat /artifacts/docker: no such file or directory`.
Switch all three services (web, collab, mcp) to `context: .` so the
build context is the repo root and `dockerfile: docker/Dockerfile.X`
resolves correctly.
Made-with: Cursor