Conversation
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>
There was a problem hiding this comment.
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
| `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL | ||
| ); | ||
| --> statement-breakpoint | ||
| CREATE TABLE `topics` ( |
There was a problem hiding this comment.
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>
| } | ||
| function selectProject(next: string) { | ||
| setEditing(null); setEditingProject(null); setError(""); | ||
| setProjectId(next); |
There was a problem hiding this comment.
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>
| setProjectId(next); | |
| setDream(""); setSavedDream(""); setTopics([]); | |
| setProjectId(next); |
| 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; |
There was a problem hiding this comment.
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>
| const projectId = meta.projectId ?? process.env.AGENCY_PROJECT; | |
| const projectId = meta.projectId || process.env.AGENCY_PROJECT; |
| 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>>(); |
There was a problem hiding this comment.
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>
| 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(); |
There was a problem hiding this comment.
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>
| 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(); |
| 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 }); |
There was a problem hiding this comment.
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>
| 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); | |
| } |
| 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(); |
There was a problem hiding this comment.
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>
| ( | ||
| 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')) |
There was a problem hiding this comment.
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>
| 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), | ||
| ]); |
There was a problem hiding this comment.
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>
| 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), | |
| ]); |
| 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 }); |
There was a problem hiding this comment.
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>
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.mdkeeps the user's voice and preferences for all of them.Changes
projectstable,project_idoncontextsandideas, and topics rebuilt with a(project_id, id)key. Existing dreams, cards and topics move into adefaultproject on the first request. Drizzle migration0007is included.GET/POST/DELETE /api/projects. Delete only removes empty projects other thandefault.?project=<id>and writes takeprojectId. A missing project meansdefault; 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 itsprojectId.sync-me.mjs --project <id>syncsprojects/<id>/me.md, which is Git-ignored.push-cardhonorsprojectIdorAGENCY_PROJECT.SKILL.mdand the README describe projects, per-project profiles and the API changes.Testing
npm run lintshows only the one warning that was already there.npm run buildpasses.sync-me.Notes
default.node scripts/sync-me.mjs --project default --app-to-fileonce, then keep only voice and preferences inme.md.app/agency.tsxandapp/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
defaultproject automatically via Drizzle migration0007.default.node scripts/sync-me.mjs --project default --app-to-fileonce, then leave only voice and preferences inme.md.Behavior
GET/POST/DELETE /api/projects; delete only removes empty projects other thandefault.?project=<id>and writes takeprojectId; missing meansdefault, unknown returns 404.sync-me.mjs --project <id>syncs a project'sprojects/<id>/me.md(Git-ignored);push-cardhonorsprojectIdorAGENCY_PROJECT.Written for commit fff7737. Summary will update on new commits.