Workspace search
A search bar across all specs, tasks, and comments in a workspace — powered by Postgres full-text search, not Cognee/semantic similarity. Block text is searchable but resolves to parent spec results. Results are scoped to the caller's org and viewable specs only (collaborator + share-link checks at query time).
“Once you have more than about thirty specs you can't find anything.”
The grilling
Once you have more than about thirty specs you can't find anything. That's the complaint. Go read the repo before you ask me anything — I want to know what search exists today, what find-similar actually does and when it runs, whether anything indexes tasks or comments, and whether there's any text index in the database at all. Come back with what you found and then grill me. Feature scale: a way to find things across a workspace, not a filter box on one list.
Here's what I found in the repo: **Existing search:** - **Project page filter box** (`projects.tsx:887-896`): client-side substring match against `title + key + summary` — only 3 fields, no indexing, no relevance, scoped to one project at a time. - **Cmd-K SpecPicker** (`spec-picker.tsx`): same substring approach on title/key/project name, no server side. - **`/v1/specs/find-similar`** endpoint: takes a free-text `overview`, runs **deterministic** exact-title/overlap matching first, then a **Cognee vector search** (embedding + knowledge-graph retrieval, per-org dataset `org-{uuid}`) followed by a fast LLM to label matches as `duplicate`/`extension`/`unrelated`. This fires only on *new spec creation* and the agent's `search_related_specs` tool. Not user-facing as a search bar. **What Cognee indexes:** The `buildSpecDocument()` function (`cognee-ingestion.ts`) builds a flat markdown doc per spec containing: title, key, type, summary, **all editor blocks** (goals, constraints, acceptance criteria, all paragraphs), and **answered agent Q&A**. This is the vector-search corpus. **What is NOT indexed anywhere:** - **`specTasks`** (task titles, descriptions, outcomes) - **`comments`** (threads on blocks and tasks) - No Postgres FTS (no tsvector, no pg_trgm) - No dedicated search results page or search bar across the workspace **My question:** There it is. The current "search" is basically a client-side Ctrl+F on title+key+summary only. The Cognee vector index covers spec body content and Q&A, but nobody in the UI can query it — it's only used for the agent and the new-spec duplicate check. Tasks and comments are completely invisible to search. I've asked my first question above.
Files the agent read
- read_repo_structure whole repo
- read_repo_structure packages
- read_repo_file packages/zero/src/schema.ts
- grep packages/zero/src
- grep apps/api/src
- read_repo_file apps/api/src/routes/specs.ts
- read_repo_file apps/api/src/similarity-session.ts
- grep apps/web/src
- grep apps/web/src
- read_repo_file packages/zero/src/queries.ts
- read_repo_file apps/web/src/lib/gallery/specs.ts
- read_repo_file apps/web/src/routes/dashboard/projects.tsx
- read_repo_file apps/web/src/routes/dashboard/data.ts
- read_repo_file apps/web/src/routes/dashboard/spec-editor/spec-picker.tsx
- read_repo_file apps/api/src/cognee-ingestion.ts
- read_repo_file packages/cognee-client/src/client.ts
- grep packages/zero/src/queries.ts
- grep packages/zero/src/schema.ts
- grep packages/zero/src
- read_repo_file packages/zero/src/schema.ts
- read_repo_file packages/zero/src/schema.ts
- grep packages/zero/src/schema.ts
- read_repo_file apps/web/src/routes/dashboard/projects.tsx
- grep apps/web/src/routes/dashboard/projects.tsx
- read_repo_file apps/api/src/agent-tools.ts
Useful. So find-similar is duplicate detection at creation time, the Cmd+K picker is a client-side filter over already-synced specs, and nothing searches tasks or blocks. And there's no text index in Postgres. Now, your question is whether to build this on Cognee's semantic search or on Postgres full-text, and you recommended Cognee because it's already wired and it's the house similarity layer. I'm going to overrule you, and I want the reasoning recorded because it will look inconsistent with the rest of the product otherwise. Search and similarity are different jobs. When I type a search box I usually know a word that is literally in the thing I'm looking for — a spec key, someone's name, the word "webhook". I want exact matching, ranked, fast, and above all predictable: the same query returns the same results and I can tell why each one is there. Semantic search is excellent at "find me things like this" and bad at "find the thing I already know exists", because it will confidently return five plausible neighbours and bury the exact match. If I search for a phrase I wrote and don't get it back, I stop trusting the box. So: lexical search over the durable rows. Cognee stays the similarity layer and this feature does not touch it. Grill me on scope.
Noted. That's a strong architectural decision and I'll capture it as a constraint. Let me also set the summary and first goal blocks before we drill into scope. Good, base goals and the key architecture constraint are captured. Now to scope. Three goals and one hard constraint are on the board. The open question cuts to the core of scope: which content types do you want searchable? I recommended "specs + tasks + comments only" as the sharp v1 — activity events, agent turns, version history, and attachments all multiply the indexing surface and the relevance-ranking complexity significantly.
Scope: specs, tasks, and comments. Not blocks as their own results. Blocks are the interesting one and I've thought about it. A block is a fragment — "must be idempotent" — and a list of matching fragments is unreadable out of context. But block text is absolutely where the substance lives, and if I exclude it people will search for a constraint they remember writing and get nothing. So blocks are searchable but they resolve to their spec: matching a block is a reason a spec appears, and the spec result shows which block matched. The unit of a result is a thing a person can open. The constraint I care most about, record it hard: results are scoped to the caller's org, and to specs the caller can actually see, evaluated at query time. Search is the classic way an authorization bug becomes a data leak, because it will happily tell you a spec exists by showing you its title. Nothing may appear in results that the requester could not open directly, and the collaborator and share-link paths are part of that check, not an afterthought.
Excellent. Clear answer with an important auth boundary. Let me capture all of that. Good. Let me drill into the surface and interaction model — this is the most user-visible decision left. Two goals, two constraints, and the explicit types-in-and-out decision are locked. The new question gets at the surface: where do you land this search bar and what's the interaction model?
Two decisions to close it out. Ranking: recency-weighted, not pure relevance. In a spec tool the thing you're looking for is overwhelmingly something you touched recently — a six-month-old spec that matches marginally better is almost never what I meant. Don't over-engineer it, but a match on a spec touched this week beats an equal match on one from March. And an explicit non-goal, because I can see it coming: no saved searches, no filters UI, no query syntax with field operators. One box, typed text, ranked results. If people start asking for `status:` operators I'll take that as evidence the list views are wrong, not that search needs a grammar. Score acceptance and generate tasks.
Intent is at 0.95 — the advisory mentions manual ranking pinning but the user already ruled that out via "no filters, no saved searches, one box." Not acting on it. Let me score the constraints section — I have five constraints already captured. Constraints at 0.9. Now acceptance. One question on the remaining measurable decision, then I'll build criteria. One last measurable: live vs submit-based and how many results. The Cmd+K picker already uses live search with debounce, so "live search, 20 results" follows the existing pattern.
Search runs when I stop typing, not on every keystroke, and it returns a bounded page — call it twenty with a way to see more. My reasoning on the cap: an unbounded result list is a promise we can't keep as workspaces grow, and it's also a denial-of-service against our own database from the search box. Twenty is enough that the thing I want is almost always on the first page given recency weighting, and if it isn't, the answer is a better query, not a longer list. On activation: debounced, because firing a query per character is wasteful and the results flicker in a way that makes the box feel broken. And one thing I want measurable in acceptance: a search must return in a time that feels instant on a workspace with a realistic number of specs. Pick a number you can actually test against rather than the word "fast". Score constraints and acceptance and generate the tasks.
Rejected — I invented the performance numbers instead of asking you. Let me retry the batch without that criterion and ask the number separately. Now, you asked me to pick a measurable latency target. I need you to pick one. One quick number to close acceptance out.
insert_blocksunsupported operational constraintRequirement invents operational numeric target(s) absent from authoritative PM evidence: 500ms p95, 500 specs, 500ms. Remove the target or ask the PM to choose it before recording the requirement.
Remove or reword the unsupported claim: keep only content the PM stated, selected, or the repository verified. If the claim is load-bearing, ask the PM instead of asserting it. This gate family will not block again this session for a rewrite without the claim — but resubmitting this same rejected content is rejected every time; retry once without the claim, then move on; do not loop on rewording. Block inserts are atomic: nothing from this call was committed. Retry the batch without the rejected claim.
Good catch on your own part — you were about to put a number in my mouth and the server was right to stop you. The number is 300 milliseconds server-side, at the 95th percentile, on a workspace of a thousand specs. That's measured at the API, excluding network time to the browser, because that's the part we control. I picked 300 because it's the rough threshold where a result list stops feeling like it appeared and starts feeling like it loaded, and a thousand specs is well beyond what any real workspace has today, so if we hold there we have headroom. Score acceptance and generate the tasks.
The performance numbers are from the PM's explicit answer above, so the advisory is informational only. Moving to score acceptance. All three sections pass. One capability, one task. Let me verify the existing topbar before writing the title. Now I have the Topbar component. One coherent capability — one task. Let me trace through the task plan before creating: **One task** covers the whole feature: workspace search. The ACs describe a single user-visible surface (search bar → results) with backend work (tsvector columns, FTS queries, permission scoping) and frontend wiring (Topbar integration, debounced input, results panel, pagination). All of it delivers one demoable outcome: "user types in the topbar and sees ranked results from their workspace." There is no independently shippable sub-slice — shipping the backend alone is invisible, and shipping the UI alone would show an empty box with no results. Task is staged. Now I build the complete trace graph. All edges committed. Let me score the tasks section to finalize. All sections pass. Here's the final spec. --- **Title:** Workspace search **Summary:** A search bar across all specs, tasks, and comments in a workspace — powered by Postgres full-text search, not Cognee/semantic similarity. Block text is searchable but resolves to parent spec results. Results are scoped to the caller's org and viewable specs only (collaborator + share-link checks at query time). **3 goals:** - Workspace-wide spec search (title, summary, key, block text all searchable) - Task and comment search (titles, descriptions, comment bodies) - Unified ranked results (all types in one list) **5 constraints:** - Lexical-only (Postgres FTS, no Cognee) - Blocks are searchable content, not standalone result items - Results scoped to caller's permissions at query time - Recency-weighted ranking - No saved searches, filters, or query syntax **7 acceptance criteria — all measurable:** - Debounced search bar in global topbar, 20 results per page with pagination - Spec results from title/key/summary/block text with matched snippets - Task and comment results linked to parent spec - Unified recency-weighted ranking - Permission-scoped (zero results for restricted specs, not a leak) - No Cognee calls - ≤300ms p95 server-side at 1000-spec org **1 task:** "Add workspace-wide search bar with Postgres full-text search to the global topbar" — covers tsvector migrations on `specs`/`specTasks`/`comments`, the search endpoint with permission scoping and recency-weighted ranking, and the Topbar search UI with debounced input and results panel.
insert_blocksunsupported operational constraintRequirement invents operational numeric target(s) absent from authoritative PM evidence: 300ms p95, 1000 specs, 300ms. Remove the target or ask the PM to choose it before recording the requirement.
create_tasktask description sprawlThis task description runs 8 sentences — past the 1-3 sentence capability handoff. Keep the outcome, the surfaces it touches, and what proves it works; cut restated acceptance assertions, step lists, and implementation detail the blocks and trace graph already carry.
Stop the current edit sequence. Treat this as a plan-shape failure, not a wording issue: re-plan from the acceptance criteria into the smallest set of coarse, independently shippable user slices before retrying — as few as the distinct capabilities require (commonly 1-3, hard cap 7, no minimum); one narrow regression fix is exactly 1 slice. If two slices are steps of the same capability, merge them into one. If bad tasks or blocks already exist, update/delete that bad shape before adding more.
Files the agent read
- read_repo_file apps/web/src/components/Topbar.tsx
Agreed — that's one capability and it should be one task. Backend without the box is invisible and the box without the backend is a dead input, so there's nothing independently shippable in splitting them. Generate it.
The task already exists in the spec — it was created and traced earlier in this session. The spec is fully generated: **Task:** Add workspace-wide search bar with Postgres full-text search to the global topbar — covers the tsvector migration on `specs`/`specTasks`/`comments`, the search endpoint with permission scoping and recency-weighted ranking, and the Topbar search UI with debounced input, results panel, and pagination. Traces to all 7 acceptance criteria. **Status:** `planned` — ready for the engineer or coding agent to pick up. The complete trace graph (23 edges) links every constraint, acceptance criterion, and goal.
What it asked
What specifically do you need to find that you cannot find today?
- ASpec body content
Search within a spec's blocks — goals, constraints, acceptance criteria, paragraphs
- BTask titles and descriptions
Find tasks across all specs by their title or description text
- CComments
Search within comment threads left on blocks and tasks
- DEverything across workspaceAgent's pick
All of the above — specs, tasks, comments, block content across every project
Useful. So find-similar is duplicate detection at creation time, the Cmd+K picker is a client-side filter over already-synced specs, and nothing searches tasks or blocks. And there's no text index in Postgres. Now, your question is whether to build this on Cognee's semantic search or on Postgres full-text, and you recommended Cognee because it's already wired and it's the house similarity layer. I'm going to overrule you, and I want the reasoning recorded because it will look inconsistent with the rest of the product otherwise. Search and similarity are different jobs. When I type a search box I usually know a word that is literally in the thing I'm looking for — a spec key, someone's name, the word "webhook". I want exact matching, ranked, fast, and above all predictable: the same query returns the same results and I can tell why each one is there. Semantic search is excellent at "find me things like this" and bad at "find the thing I already know exists", because it will confidently return five plausible neighbours and bury the exact match. If I search for a phrase I wrote and don't get it back, I stop trusting the box. So: lexical search over the durable rows. Cognee stays the similarity layer and this feature does not touch it. Grill me on scope.
Which content types are in and out of search scope?
- ASpecs + tasks + comments onlyAgent's pick
Search covers spec titles/summaries/blocks, task titles/descriptions, and comment bodies. No activity events, agent turns, versions, or attachments.
- BAdd activity events and agent-turns
Also index activity_events.action+metadata and agent_turns.visibleAssistantText
- CAdd spec version history
Also index all past spec_versions.content, not just the current blocks
- DAll text content
Everything — specs, tasks, comments, activity, agent turns, version history, and attachment text content
Scope: specs, tasks, and comments. Not blocks as their own results. Blocks are the interesting one and I've thought about it. A block is a fragment — "must be idempotent" — and a list of matching fragments is unreadable out of context. But block text is absolutely where the substance lives, and if I exclude it people will search for a constraint they remember writing and get nothing. So blocks are searchable but they resolve to their spec: matching a block is a reason a spec appears, and the spec result shows which block matched. The unit of a result is a thing a person can open. The constraint I care most about, record it hard: results are scoped to the caller's org, and to specs the caller can actually see, evaluated at query time. Search is the classic way an authorization bug becomes a data leak, because it will happily tell you a spec exists by showing you its title. Nothing may appear in results that the requester could not open directly, and the collaborator and share-link paths are part of that check, not an afterthought.
Where does the search live and how does it activate?
- AGlobal topbar searchAgent's pick
A search bar in the top nav bar, always visible. Typing opens a results dropdown/sheet. Cmd+K opens it too.
- BDedicated search page
A /dashboard/search route with a full-page search interface and rich results. Not always visible.
- CExpandable search overlay
A search icon in the topbar that opens a focused overlay/panel. Not persistent.
Two decisions to close it out. Ranking: recency-weighted, not pure relevance. In a spec tool the thing you're looking for is overwhelmingly something you touched recently — a six-month-old spec that matches marginally better is almost never what I meant. Don't over-engineer it, but a match on a spec touched this week beats an equal match on one from March. And an explicit non-goal, because I can see it coming: no saved searches, no filters UI, no query syntax with field operators. One box, typed text, ranked results. If people start asking for `status:` operators I'll take that as evidence the list views are wrong, not that search needs a grammar. Score acceptance and generate tasks.
Search activation and result count cap?
- ALive search, 20 resultsAgent's pick
Results update on keystroke (300ms debounce). Max 20 results per query.
- BLive search, 50 results
Results update on keystroke (300ms debounce). Max 50 results per query.
- CSubmit on Enter, 20 results
User types then presses Enter or clicks search. Max 20 results.
Search runs when I stop typing, not on every keystroke, and it returns a bounded page — call it twenty with a way to see more. My reasoning on the cap: an unbounded result list is a promise we can't keep as workspaces grow, and it's also a denial-of-service against our own database from the search box. Twenty is enough that the thing I want is almost always on the first page given recency weighting, and if it isn't, the answer is a better query, not a longer list. On activation: debounced, because firing a query per character is wasteful and the results flicker in a way that makes the box feel broken. And one thing I want measurable in acceptance: a search must return in a time that feels instant on a workspace with a realistic number of specs. Pick a number you can actually test against rather than the word "fast". Score constraints and acceptance and generate the tasks.
Measurable server-side latency target?
- A300ms p95Agent's pick
Server returns first-page results within 300ms at p95 for up to 500-spec workspaces. Tight but achievable with tsvector.
- B500ms p95
Server returns first-page results within 500ms at p95. More forgiving of larger workspaces or complex ranking.
- C800ms p95
Server returns first-page results within 800ms at p95. Room for higher pagination overhead or unoptimized queries.
Good catch on your own part — you were about to put a number in my mouth and the server was right to stop you. The number is 300 milliseconds server-side, at the 95th percentile, on a workspace of a thousand specs. That's measured at the API, excluding network time to the browser, because that's the part we control. I picked 300 because it's the rough threshold where a result list stops feeling like it appeared and starts feeling like it loaded, and a thousand specs is well beyond what any real workspace has today, so if we hold there we have headroom. Score acceptance and generate the tasks.
Where the server refused
Each of these rejected the agent's own tool call. The agent had to fix its output and try again before the spec could advance — this is the gate, not a prompt the model can talk past.
insert_blocksunsupported operational constraintRequirement invents operational numeric target(s) absent from authoritative PM evidence: 500ms p95, 500 specs, 500ms. Remove the target or ask the PM to choose it before recording the requirement.
Remove or reword the unsupported claim: keep only content the PM stated, selected, or the repository verified. If the claim is load-bearing, ask the PM instead of asserting it. This gate family will not block again this session for a rewrite without the claim — but resubmitting this same rejected content is rejected every time; retry once without the claim, then move on; do not loop on rewording. Block inserts are atomic: nothing from this call was committed. Retry the batch without the rejected claim.
create_tasktask description sprawlThis task description runs 8 sentences — past the 1-3 sentence capability handoff. Keep the outcome, the surfaces it touches, and what proves it works; cut restated acceptance assertions, step lists, and implementation detail the blocks and trace graph already carry.
Stop the current edit sequence. Treat this as a plan-shape failure, not a wording issue: re-plan from the acceptance criteria into the smallest set of coarse, independently shippable user slices before retrying — as few as the distinct capabilities require (commonly 1-3, hard cap 7, no minimum); one narrow regression fix is exactly 1 slice. If two slices are steps of the same capability, merge them into one. If bad tasks or blocks already exist, update/delete that bad shape before adding more.
What it had to clear
Every section had to score at or above 0.90 before create_task would run at all. This is the gate.
Three goals covering spec search (including blocks as content), task+comment search, and unified ranked results. Non-goals clearly stated (no syntax, no filters, no saved searches). Audience is workspace users. Recency-weighted ranking decided. Surface implied as global topbar one-box search. No remaining ambiguity on what is being built.
Five constraints: lexical-only (no Cognee), blocks-are-content-not-results, permission-scoped results, recency-weighted ranking, no saved-searches/query-syntax. Still need to nail down: index-update strategy (real-time tsvector trigger vs background refresh?), result count cap, and whether search is live-with-debounce or submit-based.
Seven acceptance criteria covering: search bar activation (debounced, 20-results paginated), spec matching across title/key/summary/blocks with snippets, task+comment surfacing linked to parent specs, unified recency-weighted ranking, permission-scoped results, no Cognee consulted, and measurable 300ms p95 latency at 1000-spec scale. Each criterion is testable with a pass/fail signal. All goals are validated and all constraints are bounded.
One coarse task covering the entire search capability — tsvector migrations, search endpoint with permission scoping and recency-weighted ranking, and the topbar search bar with debounced results panel. Traces to all 7 acceptance criteria. Test assertions for latency, permission boundaries, and Cognee exclusion are embedded in the task description. No layer-splitting or separate test/doc tasks.
The spec it produced
How it hangs together
Workspace-wide spec search
- constrainsLexical-only search engine
- constrainsBlocks are content, not result items
- constrainsResults scoped to caller's permissions
- validatesGlobal search bar with debounced activation
- validatesSpecs matched by title, key, summary, or block text
- validatesPermission-scoped results
- validatesNo Cognee consulted for results
- validatesSearch latency under 300ms p95 at 1000 specs
Task and comment search
- constrainsLexical-only search engine
- constrainsResults scoped to caller's permissions
- validatesTasks and comments surface in results
- validatesPermission-scoped results
Unified ranked results
- constrainsResults scoped to caller's permissions
- constrainsRecency-weighted ranking
- constrainsNo saved searches or query syntax
- validatesUnified recency-weighted ranking
Global search bar with debounced activation
- implementsAdd workspace-wide search bar with Postgres full-text search to the global topbar
Specs matched by title, key, summary, or block text
- implementsAdd workspace-wide search bar with Postgres full-text search to the global topbar
Tasks and comments surface in results
- implementsAdd workspace-wide search bar with Postgres full-text search to the global topbar
Unified recency-weighted ranking
- implementsAdd workspace-wide search bar with Postgres full-text search to the global topbar
Permission-scoped results
- implementsAdd workspace-wide search bar with Postgres full-text search to the global topbar
No Cognee consulted for results
- implementsAdd workspace-wide search bar with Postgres full-text search to the global topbar
Search latency under 300ms p95 at 1000 specs
- implementsAdd workspace-wide search bar with Postgres full-text search to the global topbar
What the gate released
Add workspace-wide search bar with Postgres full-text search to the global topbar
Wire Postgres FTS across specs (title/summary/key/block text), specTasks (title/description), and comments (body) via tsvector migrations and a new search endpoint that returns recency-weighted, permission-scoped results. Add a debounced search bar to Topbar.tsx with a results panel showing spec snippets and linked task/comment results, capped at 20 per page. Verify ≤300ms p95 at 1000-spec scale and that Cognee is never called. Automated assertions: - Global search bar with debounced activation - Specs matched by title, key, summary, or block text - Tasks and comments surface in results - Unified recency-weighted ranking — 6 months - Permission-scoped results — No spec, task, or comment appears in search results if the caller cannot directly view the parent spec — Authoring a query that would match a restricted spec yields zero results for that spec, not an error or a leaked title - No Cognee consulted for results - Search latency under 300ms p95 at 1000 specs
zenorm workYour coding agent claims the task, implements it against the full spec, and gates itself on typecheck, lint, and tests.
A real request, the questions the agent asked, and every place the server refused its output. No product announcements.