Comments with mentions on blocks and tasks
Add @-mentions to the existing block/task comment threads, and handle the case where an agent rewrite orphans comments against rewritten or deleted block content by preserving the original block text alongside the flagged comment.
“People discuss specs in Slack and none of it ends up anywhere near the spec.”
The grilling
People discuss specs in Slack and none of it ends up anywhere near the spec. Whiteboard-level, that's the ask. Don't ask me anything yet — go read the codebase first and come back with what's actually there. I want to know: whether any commenting exists today and what it can attach to, whether there's any notion of addressing a specific person, and how a spec is shared with someone who isn't the author. Report what you find, then grill me. Feature scale, not a comment box. The interesting question is what a discussion thread means on a document that an agent is concurrently rewriting.
Let me start by exploring the codebase structure and then diving into specifics. Good. Now let me examine the key areas: schema, sharing, collaboration, and whether any commenting exists. Found a `comments` table already in the Drizzle schema. Let me read that section and search for comment-related UI and Zero coverage. There's already a live comments system on tasks. Let me read the Zero schema's comments table, the queries, and see what the task-focus page does with comments — and what's missing for block-level comments and mentions. Here's what I found in the codebase. --- ## Existing Infrastructure ### Comments — fully built at the data + mutator + UI layer - **`comments` table** (Drizzle + Zero schema): `id, orgId, specId, targetType (block|task), targetId, userId, body, parentId (nullable — threaded replies), resolvedAt, timestamps` - **Zero client mutators**: `create`, `update`, `delete`, `resolve`, `unresolve` - **Server-side mutators** in `server-mutators.ts`: every mutation checks `assertCanEditSpec` + org scoping, and writing a comment auto-creates an `activity_events` row with `action: "commented"` - **Block focus page** (`block-focus-page.tsx`): threaded comment UI with reply/resolve/delete already wired for blocks - **Task focus page** (`task-focus-page.tsx`): identical threaded comment UI for tasks - **Activity feed**: `activity_events` table with `"commented"` action type, scoped to the target (block/task), not the comment row itself - **Public share view**: both `publicSpec.comments` and `specs.comments` Zero queries exist — the public view syncs comments (though author attribution is deliberately hidden on the public path) ### What's MISSING from the current UX **A) Inline commenting on the main spec content pane.** Today, comments on a block are only reachable through a separate modal/focus page triggered via the spec editor URL (`/spec/:specId/block/:blockId`). The main `spec-content-pane.tsx` — the rich document view where you see all blocks — has **no comment indicator, no inline reply, no comment badge** on any block. You have to navigate away from the spec to see or write comments. **B) No @-mention / person-tagging anywhere.** Zero grep for `@`, `mention`, `at-mention` across the entire web app returned nothing. You cannot tag a specific collaborator in a comment. **C) No notification infrastructure.** There's no notifications table, no in-app notification bell, no email push, no webhook — nothing that tells a user "someone commented on a block/task you're part of." The only visibility into new comments is the activity feed in the focus page, and you have to already be looking at that specific block/task to see it. **D) The collaboration panel is dead code.** `collaboration-panel.tsx` is defined (invite by email, role management, remove collaborators) but the file has a comment noting it is "never mounted anywhere." The share link dialog is the only shipped sharing UI. ### Sharing model (for context on "who sees what") - **Public share links**: token-based (SHA-256 hashed, one-time reveal), read-only, no account needed. Works. - **Spec collaborators**: `spec_collaborators` table with `owner | editor | viewer` roles, email invites. Data model is solid but the UI panel was built and never shipped. ### The concurrent agent problem The Zero sync layer handles real-time collaboration. `spec_editor_blocks` rows carry a `revision` counter. `db/spec-revision.ts` implements a revision gateway with `stale_revision` / `idempotency_conflict` errors — the same optimistic concurrency that protects block edits would extend to comments naturally (they're both Zero-synced rows). But there's a question: when an agent deletes or restructures a block, what happens to its attached comments? They'd orphan in the DB since `targetId` is a UUID, not a FK with CASCADE (checking the schema — the `comments` table references `specId` FK but `targetId` is a bare UUID, no FK constraint). --- Is this an accurate picture? Fire away — I want to understand which specific gap you're solving first.
Files the agent read
- read_repo_structure .
- ls whole repo
- read_repo_structure apps
- read_repo_structure packages
- read_repo_structure db
- read packages/zero/src/schema.ts
- read apps/api/src/routes/public-share.ts
- read apps/api/src/spec-share-link-store.ts
- read packages/zero/src/schema.ts
- grep db/schema/index.ts
- read apps/web/src/routes/dashboard/spec-editor/share-spec-dialog.tsx
- read db/schema/index.ts
- grep packages/zero/src/schema.ts
- grep apps/web/src
- read packages/zero/src/schema.ts
- grep packages/zero/src/queries.ts
- read apps/web/src/routes/dashboard/spec-editor/task-focus-page.tsx
- grep apps/api/src/zero/server-mutators.ts
- grep apps/web/src
- grep apps/web/src
- grep apps/api/src/zero/server-mutators.ts
- read apps/web/src/routes/dashboard/spec-editor/block-focus-page.tsx
- grep apps/api/src/zero/server-mutators.ts
- read apps/web/src/routes/dashboard/spec-editor/block-focus-page.tsx
- grep packages/zero/src/schema.ts
- grep apps/web/src
- read apps/web/src/routes/dashboard/spec-editor/block-focus-page.tsx
- grep apps/web/src/routes/dashboard/spec-editor/spec-content-pane.tsx
- grep apps/web/src/routes/dashboard/spec-editor
- read apps/web/src/routes/dashboard/spec-editor/collaboration-panel.tsx
- grep apps/web/src/routes/dashboard/spec-editor/spec-content-pane.tsx
- grep apps/web/src/routes/dashboard/spec-editor/spec-editor.tsx
- grep apps/api/src/spec-revision.ts
- grep packages/zero/src/mutators.ts
- grep db/spec-revision.ts
You found the shape of it. So there's a comments table already, it hangs off blocks and tasks, and there's no mention support at all. Good — that changes the ask. You're asking whether to add spec-level comments as a third target type, and you recommended doing it. I'm overruling that. Spec-level comments are where discussion goes to die. If I give people a general comment box on the document, everything lands there — "looks good", "can we talk about this", "what about mobile" — none of it attached to the thing it's about, and the spec ends up with a wall of context-free chatter at the bottom. The existing constraint that a comment must target a block or a task is a good constraint that someone made on purpose, and I want to keep it. If you have something to say, say it about a specific claim. So: no spec-level comments. Mentions are the actual feature. Now grill me on the hard part, which is what happens to a comment when the agent rewrites the block it's attached to.
Good call. The constraint is clean — a block or task is a specific claim or unit of work; a spec-level comment is a void. Keeping that. Now, the rewrite question. There are actually two different scenarios with different answers: **A. Block survives, text changes** — the agent tightens an AC from "response < 2s" to "< 500ms". The comment thread ("2s is too aggressive for our DB") is now commenting on text the author can't even see anymore. The block id is the same, the comment still renders, but its context is gone. **B. Block is deleted or restructured** — agent removes a superseded constraint. The block id vanishes. Comments have a `targetId` with no FK constraint so they'd orphan silently, visible to nobody. Which of these is your actual concern? Or is there a third scenario I'm missing? Let me know which one you're most concerned about for v1.
This is the question I wanted you to ask, and my answer is going to sound harsh: a comment on a block that has materially changed is not resolved and is not deleted. It is orphaned, visibly, with the text of the block as the commenter saw it. Think about what the alternatives do. If the comment silently follows the block, then someone reads "I don't think this is right" attached to a sentence that now says something completely different, and that's actively misleading — it puts words in the commenter's mouth. If the comment is deleted, the product silently destroys a human's objection because an agent edited a paragraph, which is the worst thing this feature could possibly do. So: preserve it, show it against the text it was written about, and mark it as referring to a superseded version. Record that as the central constraint. Nothing in this feature may drop or silently re-target a human comment as a result of an agent edit. And note the honest limitation while you're at it — the comments table stores userId as a raw Clerk id with no foreign key, so resolving a mention to a real person is a lookup this feature has to actually do rather than assume.
This is the right answer. Let me capture it before the next question. One constraint tripped a quality gate — the server wants me to ask you about the deletion boundary explicitly rather than asserting it. Let me drop that constraint from the batch and ask. You said it clearly in chat — comments must not be silently dropped — but the server requires it surfaced as a structured decision so it's locked in the record. That question is now on the spec for you to click.
insert_blocksunsupported scope boundaryRequirement invents high-risk non-goal(s) absent from authoritative PM evidence: deletion/erasure. A primary scenario does not exclude adjacent behavior; remove the boundary or ask the PM to decide it explicitly.
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.
Last thing, on notification, because I can see you circling it: a mention does not send an email. It shows up in the product for the mentioned person and that's all this feature owns. I know that limits it. I'm doing it deliberately, because email delivery is a whole separate feature with its own failure modes, and if I bundle it here I'll end up shipping neither well. Write that as an explicit non-goal so nobody reads the absence as an oversight. Who can be mentioned: anyone with access to that spec, which today means the org plus whoever's in spec_collaborators — including a pending invite, since email-before-user-row is already a case that table handles. If they can't see the spec, they can't be mentioned; mentioning must never become a way to leak spec content to someone who wasn't granted it. Score acceptance and generate the tasks.
Captured. Let me update the spec with these decisions, close the open questions, and score intent. Intent passes at 0.95. Moving to constraints — enough is there from your decisions to score now. Constraints passes. Now acceptance — I can derive most criteria from what you've told me, but there's one decision that defines the whole user experience.
insert_blocksunsupported failure notificationRequirement invents a failure/error notification outcome absent from authoritative PM or repository evidence. Completion/success confirmation does not authorize failure messaging; remove it or confirm the failure recipient and outcome explicitly.
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.
In the product, on the spec — an indicator that something addressed to you happened, and the thread it happened in. Not a separate inbox surface. A notifications centre is a second place to check, and the whole reason people are having these conversations in Slack is that checking things is expensive. If I have to visit an inbox to find out someone asked me something, I'll just keep using Slack. The mention has to be visible where the work is. So: the mentioned person sees that they were mentioned when they look at their specs, and can get to the exact comment. Unread state is per person and clears when they've seen the thread. That's the last open dimension. Score constraints and acceptance and generate the tasks.
That's a clear product answer — the mention lives on the spec, not in an inbox. Let me capture the acceptance criteria and score. All three sections pass. Now generating tasks. Two independently demoable capabilities here: (1) the mention flow itself — type @, see the badge, navigate to the thread — which works without rewrite safety, and (2) the rewrite preservation, which makes comments durable against agent edits. Two tasks. The spec is complete. Here's what was built: --- **Title:** Comments with mentions on blocks and tasks **Summary:** Add @-mentions to the existing block/task comment threads. Mentioned collaborators see an unread badge on the spec (no email). When an agent rewrite changes or deletes a block, comments are preserved against the original block text and flagged as superseded — never silently dropped or re-targeted. **3 goals** — mention a collaborator, preserve comment context through rewrites, preserve comments on deleted blocks. **4 constraints** — no spec-level comments, mentions resolve from Clerk user ID, in-product only, bounded to spec access. **7 acceptance criteria** — mention autocomplete, spec-level unread badge, badge navigates to exact thread, rewrite displays original text with superseded flag, deleted-block comments remain accessible, scope respects access boundaries, no out-of-band delivery. **2 tasks** — one for the full mention flow (autocomplete → storage → badge → navigation → unread state), one for rewrite-safe comment preservation (block text snapshot on write, superseded flagging, orphaned-comments view for deleted blocks). Each maps to its own set of acceptance criteria. Both tasks are implementable independently: mentions work without rewrite safety, and rewrite safety makes existing (and new) comments durable against agent edits.
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.
create_tasktask description sprawlThis task description runs 5 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.
What it asked
Which rewrite hazard matters most for v1?
- ABlock text changesAgent's pick
Block id survives but content is rewritten — old comment body references invisible text
- BBlock is deleted
Block id vanishes, comments orphan with no FK cascade and no UI to find them
- CBoth are in scope
Address both the text-change orphan and the delete-orphan hazard in v1
- DNeither — v1 is mentions only
Rewrite safety is a v2 concern; just build @-mention delivery for v1
This is the question I wanted you to ask, and my answer is going to sound harsh: a comment on a block that has materially changed is not resolved and is not deleted. It is orphaned, visibly, with the text of the block as the commenter saw it. Think about what the alternatives do. If the comment silently follows the block, then someone reads "I don't think this is right" attached to a sentence that now says something completely different, and that's actively misleading — it puts words in the commenter's mouth. If the comment is deleted, the product silently destroys a human's objection because an agent edited a paragraph, which is the worst thing this feature could possibly do. So: preserve it, show it against the text it was written about, and mark it as referring to a superseded version. Record that as the central constraint. Nothing in this feature may drop or silently re-target a human comment as a result of an agent edit. And note the honest limitation while you're at it — the comments table stores userId as a raw Clerk id with no foreign key, so resolving a mention to a real person is a lookup this feature has to actually do rather than assume.
Are comments on deleted blocks visible or gone?
- ANever delete comment rowsAgent's pick
Agent may delete a block but comments survive, visible in an orphaned-comments view
- BCascade delete with block
When a block is deleted, its comments are deleted too (accepts the loss)
- CSoft-orphan with recovery
Comments detach from the block and go to a generic spec-level 'detached comments' panel
Last thing, on notification, because I can see you circling it: a mention does not send an email. It shows up in the product for the mentioned person and that's all this feature owns. I know that limits it. I'm doing it deliberately, because email delivery is a whole separate feature with its own failure modes, and if I bundle it here I'll end up shipping neither well. Write that as an explicit non-goal so nobody reads the absence as an oversight. Who can be mentioned: anyone with access to that spec, which today means the org plus whoever's in spec_collaborators — including a pending invite, since email-before-user-row is already a case that table handles. If they can't see the spec, they can't be mentioned; mentioning must never become a way to leak spec content to someone who wasn't granted it. Score acceptance and generate the tasks.
Where does the mentioned user see the mention?
- ADedicated mentions inboxAgent's pick
A new bell/indicator in the top nav shows unread mention count; clicking opens a list of comments where you were mentioned
- BActivity feed filter
Activity feed on each block shows 'X mentioned you'; no global inbox, user visits the block to see it
- CSpec-level mention badge
Spec card in dashboard shows 'X new mention'; click opens the spec scrolled to that block's comment thread
- DNo new surface — minimal
The mention renders as a styled link in the comment body; user finds it by browsing comments on the spec normally
In the product, on the spec — an indicator that something addressed to you happened, and the thread it happened in. Not a separate inbox surface. A notifications centre is a second place to check, and the whole reason people are having these conversations in Slack is that checking things is expensive. If I have to visit an inbox to find out someone asked me something, I'll just keep using Slack. The mention has to be visible where the work is. So: the mentioned person sees that they were mentioned when they look at their specs, and can get to the exact comment. Unread state is per person and clears when they've seen the thread. That's the last open dimension. Score constraints and 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 scope boundaryRequirement invents high-risk non-goal(s) absent from authoritative PM evidence: deletion/erasure. A primary scenario does not exclude adjacent behavior; remove the boundary or ask the PM to decide it explicitly.
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.
insert_blocksunsupported failure notificationRequirement invents a failure/error notification outcome absent from authoritative PM or repository evidence. Completion/success confirmation does not authorize failure messaging; remove it or confirm the failure recipient and outcome explicitly.
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.
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.
Four goals cover the core user capability (mentions), the rewrite-hazard response (preserve context), the deletion-hazard response (orphaned comments), and the non-goal (in-product only, no email). Four constraints bound the scope: no spec-level comments, mention-accessible-only users, Clerk ID resolution, and no out-of-band delivery. Audience (spec collaborators + org members) and priority (v1 covers mentions + rewrite safety, no email) are locked.
Four constraints: no spec-level comments (preserves existing good constraint), Clerk-ID-to-user resolution (technical), in-product-only delivery (cuts email scope), mention-accessible-only-users (prevents access leak). No unowned topology, lifecycle, or authority changes — comments and Zero sync already handle auth, revision, and persistence.
Seven acceptance criteria cover all user-visible outcomes: mention autocomplete and rendering, unread badge on spec, navigation from badge to exact comment thread, rewrite context preservation with superseded flag, deleted-block comment accessibility, scope enforcement (non-resolving mentions), and the no-email constraint. All are measurable, observable behaviors — no vibes, no test-procedure language.
The spec it produced
How it hangs together
Mention a collaborator in a comment
- constrainsNo spec-level comments
- constrainsMentions resolve from Clerk user ID
- constrainsIn-product mention delivery only
- constrainsMention scope bounded to spec access
- validatesMention autocomplete in comment input
- validatesSpec-level unread mention badge
- validatesBadge navigates to exact comment thread
- validatesMention scope respects spec access
- validatesMentions are in-product only
Preserve comment context through agent rewrites
- validatesRewrite preserves comment with original text
Preserve comments on deleted blocks
- validatesDeleted block comments remain accessible
Mention autocomplete in comment input
- implementsAdd @-mention autocomplete to block/task comment inputs and unread mention badge to spec cards
Spec-level unread mention badge
- implementsAdd @-mention autocomplete to block/task comment inputs and unread mention badge to spec cards
Badge navigates to exact comment thread
- implementsAdd @-mention autocomplete to block/task comment inputs and unread mention badge to spec cards
Mention scope respects spec access
- implementsAdd @-mention autocomplete to block/task comment inputs and unread mention badge to spec cards
Mentions are in-product only
- implementsAdd @-mention autocomplete to block/task comment inputs and unread mention badge to spec cards
Rewrite preserves comment with original text
- implementsCapture block snapshot on comment creation and flag comments as superseded when agent rewrites block text
Deleted block comments remain accessible
- implementsCapture block snapshot on comment creation and flag comments as superseded when agent rewrites block text
What the gate released
Add @-mention autocomplete to block/task comment inputs and unread mention badge to spec cards
Wire @-mention autocomplete into the existing comment textarea in BlockFocusPage and TaskFocusPage. On @ trigger, query the Zero users table filtered by name/email within the spec's access boundary. Selected mention inserts a mention entity (inline mention token referencing userId) into the comment body. On save, the comment row stores the mention. Add an `unread_mentions` counter or derived query on the spec that shows a numeric badge in the dashboard spec card and spec editor header. Clicking the badge navigates to the block/task focus page at the comment thread and marks the mention as read per-user. Zero query + client mutator changes are in scope; no email or push notification. Automated assertions: - Mention autocomplete in comment input - Spec-level unread mention badge - Badge navigates to exact comment thread - Mention scope respects spec access — does not match any user with access to the spec produces no matching results - Mentions are in-product only
Capture block snapshot on comment creation and flag comments as superseded when agent rewrites block text
When a comment is created on a block, snapshot the block's current text into a new `snapshotBlockText` column on the comments table (or a parallel snapshots table). In the agent's block-rewrite path (specEditor.applyBlockChanges or the revision gateway), detect when a block that has comments is being rewritten. If the block text changes, preserve all existing comments unchanged and flag them with a `superseded` boolean. The comment thread UI renders the original snapshot text and a 'superseded — block has changed' banner when that flag is set. For deleted blocks, add an orphaned-comments query that returns comments whose targetId no longer matches any block/task, displayed in a collapsed section at the bottom of the spec editor. Automated assertions: - Rewrite preserves comment with original text — Comment body, author, and timestamps are not altered - Deleted block comments remain accessible — No comment row is deleted or re-targeted as a result of the block deletion
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.