Skip to content

Add projects: separate dream, topics, cards and stats per project - #21

Open
rubysolo wants to merge 1 commit into
browser-use:mainfrom
rubysolo:feat/projects
Open

rubysolo wants to merge 1 commit into
browser-use:mainfrom
rubysolo:feat/projects

Conversation

@rubysolo

@rubysolo rubysolo commented Sep 15, 2026

Copy link
Copy Markdown

Summary

Agency keeps one dream and one card queue, so work for unrelated companies or products lands in the same feed. This adds a projects layer. Each project has its own dream, topics, cards, jobs and stats. The shared me.md keeps the user's voice and preferences for all of them.

Changes

  • Data: new projects table, project_id on contexts and ideas, and topics rebuilt with a (project_id, id) key. Existing dreams, cards and topics move into a default project on the first request. Drizzle migration 0007 is included.
  • API: GET/POST/DELETE /api/projects. Delete only removes empty projects other than default.
  • Scoping: reads take ?project=<id> and writes take projectId. A missing project means default; an unknown one returns 404. This covers state, topics, context, tasks, ideas and stats. GET /api/agent-jobs?project=<id> narrows the queue, and each job includes its projectId.
  • Dedupe keys: keys stay unique across projects, because the existing inline UNIQUE constraint can't be dropped without a table rebuild. A key owned by another project returns 409 instead of silently moving the card.
  • UI: a project switcher appears in the top bar, on the first-run screen and in Settings. Settings can add, rename and remove projects, and Stats is per project. The active project lives in the URL and localStorage.
  • Scripts: sync-me.mjs --project <id> syncs projects/<id>/me.md, which is Git-ignored. push-card honors projectId or AGENCY_PROJECT.
  • Docs: SKILL.md and the README describe projects, per-project profiles and the API changes.

Testing

  • npm run lint shows only the one warning that was already there. npm run build passes.
  • Ran a scripted end-to-end test against a dev server started on a database from before this change. It covered the migration, creating/renaming/deleting projects, and dreams, topics, cards, tasks, the job queue, points and stats staying within their project. It also covered the cross-project dedupe 409, the blocked-replacement guard and sync-me.
  • Checked the switcher, the first-run screen and Settings in headless Chrome at desktop width and 390px.

Notes

  • Existing agents keep working unchanged; everything they write lands in default.
  • To split an existing profile, run node scripts/sync-me.mjs --project default --app-to-file once, then keep only voice and preferences in me.md.
  • Sanitize card HTML at render and add a Content-Security-Policy #22 (card HTML sanitizer) also changes app/agency.tsx and app/api/ideas/route.ts. The two merge cleanly in either order.

🤖 Generated with Claude Code


Summary by cubic

Adds a projects layer so work for unrelated companies or products no longer lands in a single shared feed. Each project now has its own dream, topics, cards, jobs, and stats.

Migration

  • Existing dreams, cards, and topics move into a default project automatically via Drizzle migration 0007.
  • Existing agents keep working unchanged; writes without a project go to default.
  • To split an existing profile, run node scripts/sync-me.mjs --project default --app-to-file once, then leave only voice and preferences in me.md.

Behavior

  • New GET/POST/DELETE /api/projects; delete only removes empty projects other than default.
  • Reads take ?project=<id> and writes take projectId; missing means default, unknown returns 404.
  • Dedupe keys stay unique across projects; a key owned by another project returns 409.
  • The UI adds a project switcher in the top bar, first-run screen, and Settings; the active project lives in the URL and localStorage.
  • sync-me.mjs --project <id> syncs a project's projects/<id>/me.md (Git-ignored); push-card honors projectId or AGENCY_PROJECT.

Written for commit fff7737. Summary will update on new commits.

Review in cubic

Agency had one dream and one queue. A projects layer now scopes the dream,
topics, cards, jobs and stats, with a switcher in the top bar and project
management in Settings. The shared me.md keeps voice and preferences; each
project's dream syncs with projects/<id>/me.md.

Existing cards, dreams and topics migrate into a "default" project. Reads take
?project=<id>, writes take projectId, and a missing project means default, so
existing agents keep working. Dedupe keys stay unique across projects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

22 issues found across 27 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/api/ideas/action/route.ts">

<violation number="1" location="app/api/ideas/action/route.ts:24">
P2: When a card action is submitted from a stale or wrong project view, this query can select a card owned by another project because it does not constrain `project_id`. Accept and validate `projectId`, then include it in this lookup and all action-side effects so project isolation is enforced.</violation>
</file>

<file name="drizzle/0007_projects.sql">

<violation number="1" location="drizzle/0007_projects.sql:8">
P0: On existing installations with the pre-project `topics` table, this migration fails at `CREATE TABLE topics` before the old table can be rebuilt, blocking the schema migration and app startup. Replace this unconditional create with the legacy-table rebuild/rename path (or otherwise make the migration handle the existing table) before creating the composite-key table.</violation>
</file>

<file name="app/api/topics/route.ts">

<violation number="1" location="app/api/topics/route.ts:19">
P2: When `?project=` is a valid but nonexistent ID, this handler returns `200 {topics: []}` instead of the documented 404. Check `findProject(db, projectId)` after `ensureDatabase()` and return `unknownProject(projectId)` before querying.</violation>

<violation number="2" location="app/api/topics/route.ts:51">
P2: When DELETE targets a valid but nonexistent project, this handler returns `200 {ok: true}` and hides the invalid project ID. Check `findProject(db, projectId)` after `ensureDatabase()` and return `unknownProject(projectId)` before deleting.</violation>
</file>

<file name="app/globals.css">

<violation number="1" location="app/globals.css:323">
P2: On screens below 640px, this rule forces the project switcher onto its own row but leaves the queue nav and `.radar-header-right` to auto-place in columns 1 and 2. The scores consequently sit beside the nav rather than at the right edge, and can crowd or overlap the queue on narrow screens. Explicitly place the nav and score group in the remaining columns (or define a dedicated two-item mobile row).</violation>
</file>

<file name="app/api/agent-jobs/route.ts">

<violation number="1" location="app/api/agent-jobs/route.ts:21">
P2: When `?project` contains a valid but nonexistent project ID, this route returns `200 { jobs: [] }` instead of the documented 404, so agent typos silently look like empty queues. Check `findProject` after `ensureDatabase()` and return `unknownProject(projectId)` before querying jobs.</violation>
</file>

<file name="README.md">

<violation number="1" location="README.md:247">
P2: This blanket routing contract is incorrect for documented endpoints. `/api/projects` is global, unscoped `GET /api/agent-jobs` lists every project, and unknown IDs on agent-jobs/topics return empty results rather than 404; qualify the rule or make those routes enforce it.</violation>
</file>

<file name="app/api/tasks/route.ts">

<violation number="1" location="app/api/tasks/route.ts:40">
P2: When an empty project is deleted during task submission, this check can pass before deletion and the later insert can create an orphaned card. Make project validation and card creation atomic, or prevent deletion while a write is in progress.</violation>

<violation number="2" location="app/api/tasks/route.ts:57">
P2: User-created task cards now put the outer project label in `ideas.project`, which the card contract reserves for a product or repository area. Preserve the existing `Agency` area value or provide a separate field for the project label.</violation>
</file>

<file name="app/api/ideas/route.ts">

<violation number="1" location="app/api/ideas/route.ts:85">
P2: When a project is deleted after this check but before the INSERT, this request creates an idea for a nonexistent project, making the card inaccessible. Make project validation and insertion atomic, or enforce a foreign key so deletion cannot race ingestion.</violation>
</file>

<file name="app/stats/page.tsx">

<violation number="1" location="app/stats/page.tsx:81">
P2: When a non-default project is missing, this fallback leaves the URL and remembered project unchanged. Refreshing Stats therefore requests the deleted project again instead of retaining the default fallback; persist the default project and remove the stale `project` query, as the other project-aware pages do.</violation>
</file>

<file name="app/settings/page.tsx">

<violation number="1" location="app/settings/page.tsx:63">
P1: Switching projects leaves the previous project's dream and topics editable until the new requests finish. A quick edit or removal can copy that dream or delete a same-ID topic in the newly selected project; clear or disable project-specific controls until loading completes.</violation>

<violation number="2" location="app/settings/page.tsx:114">
P2: When Settings opens on a non-default project, clicking Back before the project list loads returns to the default project. Build the link from `projectId` rather than falling back to `DEFAULT_PROJECT_ID` while the list is still loading.</violation>
</file>

<file name="scripts/push-card.mjs">

<violation number="1" location="scripts/push-card.mjs:19">
P1: When metadata contains an empty `projectId` and `AGENCY_PROJECT` is set, `??` suppresses the environment fallback and the API interprets the empty value as `default`, silently misrouting the card. Treat blank IDs as missing before falling back to `AGENCY_PROJECT`.</violation>
</file>

<file name="app/agency.tsx">

<violation number="1" location="app/agency.tsx:773">
P2: When switching projects, the hidden composer drafts remain from the previous project. Reopening it can display or submit that text into the new project; clear both drafts when switching.</violation>
</file>

<file name="lib/project.ts">

<violation number="1" location="lib/project.ts:17">
P2: When a non-ASCII or punctuation-only project name is entered, `projectSlug` returns an empty ID and the add-project flow rejects it. Generate a valid unique fallback or validate these labels explicitly before submission.</violation>
</file>

<file name="scripts/sync-me.mjs">

<violation number="1" location="scripts/sync-me.mjs:19">
P2: If `--project` is supplied without a value, this fallback silently selects `default`, so a malformed command can sync the wrong profile. Validate the value before applying the default.</violation>
</file>

<file name="app/api/state/route.ts">

<violation number="1" location="app/api/state/route.ts:56">
P2: Each project visited after its 30-second TTL leaves its decision rows in `decisionCache` forever. Prune expired entries (or use a bounded TTL/LRU cache) when refreshing this cache so project switching cannot grow worker memory without bound.</violation>
</file>

<file name="app/api/context/route.ts">

<violation number="1" location="app/api/context/route.ts:20">
P2: When a context save races deletion of a cardless project, this check can pass before the project is deleted, after which the insert succeeds for a nonexistent project or is removed while the client receives 201. Coordinate the context write with project deletion so the project cannot be removed between validation and insertion, preventing orphaned or lost context data.</violation>
</file>

<file name="db/projects.ts">

<violation number="1" location="db/projects.ts:14">
P2: When an idea has an older queued/running job and a newer terminal job, this `open` count omits it even though the New lane counts it. Check only the latest job status so the project switcher count matches the New lane.</violation>
</file>

<file name="app/api/projects/route.ts">

<violation number="1" location="app/api/projects/route.ts:53">
P3: When `DELETE /api/projects?id=missing` is called, this returns 200 even though the project API contract says unknown projects return 404. Return a 404 response so stale or mistyped IDs are not reported as successfully removed.</violation>

<violation number="2" location="app/api/projects/route.ts:57">
P2: When a card is inserted after `findProject` but before this batch, the guarded project delete is skipped while the unconditional cleanup deletes that project's topics and dream. Apply the same empty-project predicate to the cleanup statements or make the check and cleanup atomic.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread drizzle/0007_projects.sql
`created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL
);
--> statement-breakpoint
CREATE TABLE `topics` (

@cubic-dev-ai cubic-dev-ai Bot Sep 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0: On existing installations with the pre-project topics table, this migration fails at CREATE TABLE topics before the old table can be rebuilt, blocking the schema migration and app startup. Replace this unconditional create with the legacy-table rebuild/rename path (or otherwise make the migration handle the existing table) before creating the composite-key table.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At drizzle/0007_projects.sql, line 8:

<comment>On existing installations with the pre-project `topics` table, this migration fails at `CREATE TABLE topics` before the old table can be rebuilt, blocking the schema migration and app startup. Replace this unconditional create with the legacy-table rebuild/rename path (or otherwise make the migration handle the existing table) before creating the composite-key table.</comment>

<file context>
@@ -0,0 +1,19 @@
+	`created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE `topics` (
+	`project_id` text DEFAULT 'default' NOT NULL,
+	`id` text NOT NULL,
</file context>
Fix with cubic

Comment thread app/settings/page.tsx
}
function selectProject(next: string) {
setEditing(null); setEditingProject(null); setError("");
setProjectId(next);

@cubic-dev-ai cubic-dev-ai Bot Sep 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Switching projects leaves the previous project's dream and topics editable until the new requests finish. A quick edit or removal can copy that dream or delete a same-ID topic in the newly selected project; clear or disable project-specific controls until loading completes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/settings/page.tsx, line 63:

<comment>Switching projects leaves the previous project's dream and topics editable until the new requests finish. A quick edit or removal can copy that dream or delete a same-ID topic in the newly selected project; clear or disable project-specific controls until loading completes.</comment>

<file context>
@@ -3,60 +3,134 @@
+  }
+  function selectProject(next: string) {
+    setEditing(null); setEditingProject(null); setError("");
+    setProjectId(next);
   }
-  useEffect(() => { void load(); }, []);
</file context>
Suggested change
setProjectId(next);
setDream(""); setSavedDream(""); setTopics([]);
setProjectId(next);
Fix with cubic

Comment thread scripts/push-card.mjs
const cardHtml = await readFile(resolve(dirname(absoluteMeta), meta.cardHtmlFile), "utf8");
delete meta.cardHtmlFile;
// Cards go to the default project unless the metadata or AGENCY_PROJECT names another.
const projectId = meta.projectId ?? process.env.AGENCY_PROJECT;

@cubic-dev-ai cubic-dev-ai Bot Sep 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When metadata contains an empty projectId and AGENCY_PROJECT is set, ?? suppresses the environment fallback and the API interprets the empty value as default, silently misrouting the card. Treat blank IDs as missing before falling back to AGENCY_PROJECT.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/push-card.mjs, line 19:

<comment>When metadata contains an empty `projectId` and `AGENCY_PROJECT` is set, `??` suppresses the environment fallback and the API interprets the empty value as `default`, silently misrouting the card. Treat blank IDs as missing before falling back to `AGENCY_PROJECT`.</comment>

<file context>
@@ -15,16 +15,18 @@ if (!meta.cardHtmlFile) {
 const cardHtml = await readFile(resolve(dirname(absoluteMeta), meta.cardHtmlFile), "utf8");
 delete meta.cardHtmlFile;
+// Cards go to the default project unless the metadata or AGENCY_PROJECT names another.
+const projectId = meta.projectId ?? process.env.AGENCY_PROJECT;
 
 const baseUrl = process.env.RADAR_URL ?? "http://localhost:3100";
</file context>
Suggested change
const projectId = meta.projectId ?? process.env.AGENCY_PROJECT;
const projectId = meta.projectId || process.env.AGENCY_PROJECT;
Fix with cubic

const activeMs = Math.max(0, Math.min(15_000, Math.round(Number(payload.activeMs ?? 0))));
const db = await ensureDatabase();
const idea = await db.prepare("SELECT id, version, status, project, category, headline, card_html AS cardHtml, agent_context AS agentContext, score, rise_reach AS riseReach, rise_impact AS riseImpact, rise_strategic_fit AS riseStrategicFit, rise_ease AS riseEase, decision_estimate_ms AS decisionEstimateMs, decision_estimate_reason AS decisionEstimateReason, source_label AS sourceLabel, source_url AS sourceUrl, dedupe_key AS dedupeKey FROM ideas WHERE id = ? AND version = ? AND status = ?").bind(payload.id, payload.version, payload.status).first<{ id: number; version: number } & Record<string, unknown>>();
const idea = await db.prepare("SELECT id, version, status, project_id AS projectId, project, category, headline, card_html AS cardHtml, agent_context AS agentContext, score, rise_reach AS riseReach, rise_impact AS riseImpact, rise_strategic_fit AS riseStrategicFit, rise_ease AS riseEase, decision_estimate_ms AS decisionEstimateMs, decision_estimate_reason AS decisionEstimateReason, source_label AS sourceLabel, source_url AS sourceUrl, dedupe_key AS dedupeKey FROM ideas WHERE id = ? AND version = ? AND status = ?").bind(payload.id, payload.version, payload.status).first<{ id: number; version: number } & Record<string, unknown>>();

@cubic-dev-ai cubic-dev-ai Bot Sep 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a card action is submitted from a stale or wrong project view, this query can select a card owned by another project because it does not constrain project_id. Accept and validate projectId, then include it in this lookup and all action-side effects so project isolation is enforced.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/api/ideas/action/route.ts, line 24:

<comment>When a card action is submitted from a stale or wrong project view, this query can select a card owned by another project because it does not constrain `project_id`. Accept and validate `projectId`, then include it in this lookup and all action-side effects so project isolation is enforced.</comment>

<file context>
@@ -21,7 +21,7 @@ export async function POST(request: Request) {
   const activeMs = Math.max(0, Math.min(15_000, Math.round(Number(payload.activeMs ?? 0))));
   const db = await ensureDatabase();
-  const idea = await db.prepare("SELECT id, version, status, project, category, headline, card_html AS cardHtml, agent_context AS agentContext, score, rise_reach AS riseReach, rise_impact AS riseImpact, rise_strategic_fit AS riseStrategicFit, rise_ease AS riseEase, decision_estimate_ms AS decisionEstimateMs, decision_estimate_reason AS decisionEstimateReason, source_label AS sourceLabel, source_url AS sourceUrl, dedupe_key AS dedupeKey FROM ideas WHERE id = ? AND version = ? AND status = ?").bind(payload.id, payload.version, payload.status).first<{ id: number; version: number } & Record<string, unknown>>();
+  const idea = await db.prepare("SELECT id, version, status, project_id AS projectId, project, category, headline, card_html AS cardHtml, agent_context AS agentContext, score, rise_reach AS riseReach, rise_impact AS riseImpact, rise_strategic_fit AS riseStrategicFit, rise_ease AS riseEase, decision_estimate_ms AS decisionEstimateMs, decision_estimate_reason AS decisionEstimateReason, source_label AS sourceLabel, source_url AS sourceUrl, dedupe_key AS dedupeKey FROM ideas WHERE id = ? AND version = ? AND status = ?").bind(payload.id, payload.version, payload.status).first<{ id: number; version: number } & Record<string, unknown>>();
   if (!idea) {
     const current = await db.prepare("SELECT id FROM ideas WHERE id = ?").bind(payload.id).first();
</file context>
Fix with cubic

Comment thread app/api/topics/route.ts
if (!id) return Response.json({ error: "Missing id" }, { status: 400 });
const db = await ensureDatabase();
await db.prepare("DELETE FROM topics WHERE id = ?").bind(id).run();
await db.prepare("DELETE FROM topics WHERE project_id = ? AND id = ?").bind(projectId, id).run();

@cubic-dev-ai cubic-dev-ai Bot Sep 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When DELETE targets a valid but nonexistent project, this handler returns 200 {ok: true} and hides the invalid project ID. Check findProject(db, projectId) after ensureDatabase() and return unknownProject(projectId) before deleting.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/api/topics/route.ts, line 51:

<comment>When DELETE targets a valid but nonexistent project, this handler returns `200 {ok: true}` and hides the invalid project ID. Check `findProject(db, projectId)` after `ensureDatabase()` and return `unknownProject(projectId)` before deleting.</comment>

<file context>
@@ -10,35 +12,42 @@ function slug(label: string) {
   if (!id) return Response.json({ error: "Missing id" }, { status: 400 });
   const db = await ensureDatabase();
-  await db.prepare("DELETE FROM topics WHERE id = ?").bind(id).run();
+  await db.prepare("DELETE FROM topics WHERE project_id = ? AND id = ?").bind(projectId, id).run();
   return Response.json({ ok: true });
 }
</file context>
Suggested change
await db.prepare("DELETE FROM topics WHERE project_id = ? AND id = ?").bind(projectId, id).run();
if (!(await findProject(db, projectId))) return unknownProject(projectId);
await db.prepare("DELETE FROM topics WHERE project_id = ? AND id = ?").bind(projectId, id).run();
Fix with cubic

Comment thread app/api/state/route.ts
const cached = decisionCache.get(projectId);
if (cached && Date.now() - cached.at < DECISION_CACHE_MS) return cached.rows;
const rows = await db.prepare(DECISION_HISTORY_SQL).bind(projectId).all<DecisionHistoryRow>();
decisionCache.set(projectId, { at: Date.now(), rows });

@cubic-dev-ai cubic-dev-ai Bot Sep 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Each project visited after its 30-second TTL leaves its decision rows in decisionCache forever. Prune expired entries (or use a bounded TTL/LRU cache) when refreshing this cache so project switching cannot grow worker memory without bound.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/api/state/route.ts, line 56:

<comment>Each project visited after its 30-second TTL leaves its decision rows in `decisionCache` forever. Prune expired entries (or use a bounded TTL/LRU cache) when refreshing this cache so project switching cannot grow worker memory without bound.</comment>

<file context>
@@ -43,19 +45,24 @@ const DECISION_HISTORY_SQL = `
+  const cached = decisionCache.get(projectId);
+  if (cached && Date.now() - cached.at < DECISION_CACHE_MS) return cached.rows;
+  const rows = await db.prepare(DECISION_HISTORY_SQL).bind(projectId).all<DecisionHistoryRow>();
+  decisionCache.set(projectId, { at: Date.now(), rows });
   return rows;
 }
</file context>
Suggested change
decisionCache.set(projectId, { at: Date.now(), rows });
decisionCache.set(projectId, { at: Date.now(), rows });
for (const [cachedProjectId, entry] of decisionCache) {
if (Date.now() - entry.at >= DECISION_CACHE_MS) decisionCache.delete(cachedProjectId);
}
Fix with cubic

Comment thread app/api/context/route.ts
const db = await ensureDatabase();
await db.prepare("INSERT INTO contexts (text) VALUES (?)").bind(text).run();
if (!(await findProject(db, projectId))) return unknownProject(projectId);
await db.prepare("INSERT INTO contexts (project_id, text) VALUES (?, ?)").bind(projectId, text).run();

@cubic-dev-ai cubic-dev-ai Bot Sep 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a context save races deletion of a cardless project, this check can pass before the project is deleted, after which the insert succeeds for a nonexistent project or is removed while the client receives 201. Coordinate the context write with project deletion so the project cannot be removed between validation and insertion, preventing orphaned or lost context data.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/api/context/route.ts, line 20:

<comment>When a context save races deletion of a cardless project, this check can pass before the project is deleted, after which the insert succeeds for a nonexistent project or is removed while the client receives 201. Coordinate the context write with project deletion so the project cannot be removed between validation and insertion, preventing orphaned or lost context data.</comment>

<file context>
@@ -8,10 +10,13 @@ function isSameOrigin(request: Request) {
   const db = await ensureDatabase();
-  await db.prepare("INSERT INTO contexts (text) VALUES (?)").bind(text).run();
+  if (!(await findProject(db, projectId))) return unknownProject(projectId);
+  await db.prepare("INSERT INTO contexts (project_id, text) VALUES (?, ?)").bind(projectId, text).run();
   return Response.json({ ok: true }, { status: 201 });
 }
</file context>
Fix with cubic

Comment thread db/projects.ts
(
SELECT COUNT(*) FROM ideas i
WHERE i.project_id = p.id AND i.card_html != '' AND i.status = 'new'
AND NOT EXISTS (SELECT 1 FROM agent_jobs j WHERE j.idea_id = i.id AND j.status IN ('queued', 'running'))

@cubic-dev-ai cubic-dev-ai Bot Sep 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When an idea has an older queued/running job and a newer terminal job, this open count omits it even though the New lane counts it. Check only the latest job status so the project switcher count matches the New lane.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At db/projects.ts, line 14:

<comment>When an idea has an older queued/running job and a newer terminal job, this `open` count omits it even though the New lane counts it. Check only the latest job status so the project switcher count matches the New lane.</comment>

<file context>
@@ -0,0 +1,35 @@
+    (
+      SELECT COUNT(*) FROM ideas i
+      WHERE i.project_id = p.id AND i.card_html != '' AND i.status = 'new'
+        AND NOT EXISTS (SELECT 1 FROM agent_jobs j WHERE j.idea_id = i.id AND j.status IN ('queued', 'running'))
+    ) AS open,
+    (SELECT COUNT(*) FROM ideas i WHERE i.project_id = p.id) AS cards
</file context>
Fix with cubic

Comment thread app/api/projects/route.ts
Comment on lines +57 to +61
await db.batch([
db.prepare("DELETE FROM topics WHERE project_id = ?").bind(id),
db.prepare("DELETE FROM contexts WHERE project_id = ?").bind(id),
db.prepare("DELETE FROM projects WHERE id = ? AND NOT EXISTS (SELECT 1 FROM ideas WHERE project_id = ?)").bind(id, id),
]);

@cubic-dev-ai cubic-dev-ai Bot Sep 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a card is inserted after findProject but before this batch, the guarded project delete is skipped while the unconditional cleanup deletes that project's topics and dream. Apply the same empty-project predicate to the cleanup statements or make the check and cleanup atomic.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/api/projects/route.ts, line 57:

<comment>When a card is inserted after `findProject` but before this batch, the guarded project delete is skipped while the unconditional cleanup deletes that project's topics and dream. Apply the same empty-project predicate to the cleanup statements or make the check and cleanup atomic.</comment>

<file context>
@@ -0,0 +1,63 @@
+  if (project.cards > 0) {
+    return Response.json({ error: `This project still has ${project.cards} cards. Only empty projects can be removed.` }, { status: 409 });
+  }
+  await db.batch([
+    db.prepare("DELETE FROM topics WHERE project_id = ?").bind(id),
+    db.prepare("DELETE FROM contexts WHERE project_id = ?").bind(id),
</file context>
Suggested change
await db.batch([
db.prepare("DELETE FROM topics WHERE project_id = ?").bind(id),
db.prepare("DELETE FROM contexts WHERE project_id = ?").bind(id),
db.prepare("DELETE FROM projects WHERE id = ? AND NOT EXISTS (SELECT 1 FROM ideas WHERE project_id = ?)").bind(id, id),
]);
await db.batch([
db.prepare("DELETE FROM topics WHERE project_id = ? AND NOT EXISTS (SELECT 1 FROM ideas WHERE project_id = ?)").bind(id, id),
db.prepare("DELETE FROM contexts WHERE project_id = ? AND NOT EXISTS (SELECT 1 FROM ideas WHERE project_id = ?)").bind(id, id),
db.prepare("DELETE FROM projects WHERE id = ? AND NOT EXISTS (SELECT 1 FROM ideas WHERE project_id = ?)").bind(id, id),
]);
Fix with cubic

Comment thread app/api/projects/route.ts
if (id === DEFAULT_PROJECT_ID) return Response.json({ error: "The default project cannot be removed." }, { status: 400 });
const db = await ensureDatabase();
const project = await findProject(db, id);
if (!project) return Response.json({ ok: true });

@cubic-dev-ai cubic-dev-ai Bot Sep 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: When DELETE /api/projects?id=missing is called, this returns 200 even though the project API contract says unknown projects return 404. Return a 404 response so stale or mistyped IDs are not reported as successfully removed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/api/projects/route.ts, line 53:

<comment>When `DELETE /api/projects?id=missing` is called, this returns 200 even though the project API contract says unknown projects return 404. Return a 404 response so stale or mistyped IDs are not reported as successfully removed.</comment>

<file context>
@@ -0,0 +1,63 @@
+  if (id === DEFAULT_PROJECT_ID) return Response.json({ error: "The default project cannot be removed." }, { status: 400 });
+  const db = await ensureDatabase();
+  const project = await findProject(db, id);
+  if (!project) return Response.json({ ok: true });
+  if (project.cards > 0) {
+    return Response.json({ error: `This project still has ${project.cards} cards. Only empty projects can be removed.` }, { status: 409 });
</file context>
Fix with cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant