Skip to content

Make MCP usable for planning: fix the consent dead end, add the ceremony tools - #89

Merged
imshashank merged 18 commits into
mainfrom
mcp-planning
Aug 5, 2026
Merged

Make MCP usable for planning: fix the consent dead end, add the ceremony tools#89
imshashank merged 18 commits into
mainfrom
mcp-planning

Conversation

@imshashank

@imshashank imshashank commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Makes MCP usable for planning, fixes the board bugs that made the tracker feel broken, renames cycles to sprints, adds per-document sharing and Markdown export/import, and closes the security holes an adversarial review reproduced against the database.

The board was dropping cards

Dragging to another column removed the card from the board. The move reached the server and the badges moved, but the card rendered nowhere: each column keeps its own cached list, and the reconciler had been told never to insert a row it had not already seen. belongsInList already prevents a new issue appearing in every column, so that guard only ever suppressed the legitimate case.

The drag-and-drop test covering this was disabled as flaky. It isn't. It now creates its own issue, drags it, and asserts persistence through the API as well as the render.

Also fixed: an in-column reorder showed no movement until the server answered; a cross-column drag on a filtered board repainted the card in the column it had just left; display options did nothing on server-backed columns while the header showed a total that disagreed with the rows; a failing mutation rolled the whole issue cache back, undoing sibling mutations that had already succeeded; a created issue never reached My Issues; and an issue arriving over the socket lost its labels.

Search never searched

⌘K matched command names only. Typing an issue title returned "No matching commands" and never called the server. It searches issues as you type now, debounced.

MCP

Connecting dead-ended: after signing in, the consent screen demanded a passkey on top of the sign-in that had just happened, and when that prompt did not complete the client waited for a redirect that never came. A session that authenticated within five minutes now satisfies the step-up; an older one still needs a passkey; and when consent cannot complete the screen returns the client access_denied rather than stranding it.

Sixteen new tools, because the server had 23 and none touched the ceremony: the whole standup is drivable, sprints can be opened, moved and completed, milestones listed and created.

Scopes are now enforced. The token carries what the user consented to and nothing looked at it, so a client granted orbit.read could create issues, close sprints and invite members. A read-only token now sees 23 read tools instead of 39.

Sprints

Renamed everywhere a person looks, with /sprints and permanent redirects from the old paths. The database column stays cycle, so no migration.

History told a lie. Completing a sprint moves unfinished issues into the next one, so a closed sprint's live rows contain only what landed, and every history query reported near-100% completion. The outcome is written to the progress_snapshot column the schema always carried and never used, captured before the rollover.

Completion also wasn't concurrency safe (two completions both passed the guard, the loser overwrote the winner's outcome), filed unfinished work into already-closed sprints, and minted successors that overlapped existing sprints — a window createCycle itself refuses. All three are fixed and tested. Sprints were unreachable from the web app at all; they have routes now, plus a history list and a per-sprint page.

Docs

Per-document sharing with named people and teams. The permission model was complete in the service layer and unreachable: no route called setDocAccess, nothing listed who a doc was shared with, and a private doc could not be created.

Hardening that came with it: a write grantee could publish someone else's private doc to the open web; the grant list being unknown was treated as empty, so adding one person wiped everybody; two quick edits could restore what the later one removed; revoking a grant left the reader's live subscription in place; and a restricted doc's deltas were addressed at a scope nothing subscribed to.

Plus Markdown export and import, and PDF through a print stylesheet.

Other security fixes

The passkey step-up was bypassable — the hook stamping lastUsedAt ran whether or not the assertion verified, so a deliberately failed attempt satisfied the freshness check. The MCP grant was written before the consent code was validated, and took its client and scopes from the request body rather than the consent record.

Verification

bun run verify green. 11 e2e specs pass, covering cross-column drag, palette search, sprint completion and history, the old cycle URLs, and sharing a private doc with a named person who then gains access. Regression tests were checked against the pre-fix code where the behaviour was subtle.

Not in this PR

Docs folders and tree, anchored comments, the inbox reading pane, the project tabs, and moving tests into a dedicated tree.

Greptile Summary

This PR covers five distinct improvements: fixing the board drag-drop bug that dropped moved cards, wiring issue search into the command palette, repairing the MCP OAuth consent dead-end and adding sixteen ceremony tools, renaming cycles to sprints everywhere the user sees, capturing sprint completion snapshots before rollover so history is accurate, and surfacing the previously-unreachable doc-access model in the UI.

  • Board fix: reconcile now uses admitsNewRows to decide whether a moved issue can enter a previously unseen list; settleFilteredLists handles filtered columns by invalidating and refetching, covering the half that left moved cards invisible.
  • MCP auth fix: signedInWithin allows a session created within the last five minutes to satisfy the passkey step-up, and the consent route now reads clientId/scope from the stored consent record instead of trusting the request body, so a client cannot forge a wider grant.
  • Sprint snapshot: atClose captures all non-archived issues before the rollover runs, then outcomeOf writes the pre-rollover totals into progressSnapshot; the advisory lock on cycle:teamId prevents a double-complete race.

Confidence Score: 5/5

Safe to merge. The auth, board, and sprint completion paths are all correctly implemented and covered by new tests.

The MCP consent flow, board reconciler, sprint snapshot, and doc-access validation are each well-implemented and covered by new tests. The one real-time gap (revoked doc grantees not receiving a cache-invalidation event) is a UX limitation, not a correctness or security issue, because loadReadableDoc enforces access on every server call.

Files Needing Attention: packages/core/src/content/doc-service.ts - the setDocAccess sync broadcast is worth revisiting to include previously-granted scopes so revoked users see an immediate update.

Important Files Changed

Filename Overview
packages/core/src/content/doc-service.ts setDocAccess now validates subjects are in the workspace, deduplicates grants, and publishes a sync action. Real-time broadcast scope does not include previously-granted users whose access is revoked, leaving their cache stale until next navigation.
packages/core/src/work/cycle-service.ts Sprint completion now takes a pre-rollover snapshot, uses a pg_advisory_xact_lock + re-read pattern to prevent double-complete, and finds the next sprint by date rather than number. New pastCycles and getCycleByNumber helpers added.
apps/web/src/app/(auth)/oauth/authorize/decision/route.ts MCP consent route now reads clientId and scope from the stored consent record rather than the request body, closing a potential grant-widening vector; deny path unaffected.
apps/web/src/app/(auth)/oauth/authorize/step-up.ts New helper that guards the passkey step-up window; correctly rejects unparseable or future-dated session timestamps via !Number.isFinite(started) and started <= now.
apps/web/src/app/(auth)/oauth/authorize/consent-form.tsx Adds abandon() path that sends deny to the client with a redirect instead of leaving it stranded; blocked is only set on allow failures, so the Cancel button does not appear alongside Deny on deny errors.
apps/web/src/lib/query/use-issues.ts Board drag fix: reconcile now uses admitsNewRows to admit newly-scoped issues into unfiltered lists; settleFilteredLists invalidates filtered lists that either held the moved issue or now should contain it.
packages/mcp-server/src/tools/scrum.ts Sixteen new tools covering standup ceremony, sprint lifecycle, and milestones; instant refine validates parsability at the tool boundary for date fields.
apps/web/src/lib/realtime/delta-bridge.tsx Adds scopes.user(currentUserId) to the realtime subscription list so user-scoped events reach the client.
packages/core/src/work/standup-service.ts advanceStandup now short-circuits when direction is previous and already at the first turn. setRotation now returns actions and the route publishes them.

Sequence Diagram

sequenceDiagram
    participant MCP as MCP Client
    participant Web as /oauth/authorize
    participant Decision as /decision route
    participant DB as Postgres

    MCP->>Web: "GET /authorize?client_id=..."
    Web->>MCP: Render consent screen
    MCP->>Decision: "POST {decision:allow, consentCode, organizationId}"
    Decision->>DB: read session.createdAt
    alt session created within 5 min
        Decision->>DB: finalizeMcpConsent(accept:true)
        DB-->>Decision: "{redirectUri, clientId, scope}"
        Decision->>DB: recordMcpGrant(clientId, scope from consent record)
        Decision->>MCP: "{redirectUri}"
    else older session + passkey required
        Decision->>MCP: "{status:passkey_required}"
        MCP->>Web: trigger passkey prompt
        MCP->>Decision: retry POST
        Decision->>DB: finalizeMcpConsent + recordMcpGrant
        Decision->>MCP: "{redirectUri}"
    end
    alt allow fails
        Web->>Web: setBlocked(message), show Cancel button
        MCP->>Decision: "POST {decision:deny}"
        Decision->>DB: finalizeMcpConsent(accept:false)
        Decision->>MCP: "{redirectUri with access_denied}"
    end
Loading

Reviews (11): Last reviewed commit: "fix(sprints,docs): announce a deleted sp..." | Re-trigger Greptile

Context used:

  • Context used - CLAUDE.md (source)

Connecting an MCP client dead ended. After signing in, the consent screen
demanded a passkey step up on top of the sign in that had just happened, and
when that second prompt did not complete there was nothing to do: the decision
endpoint kept answering passkey_required, the form raised a toast, and the
client was left waiting for a redirect that never came.

A session that authenticated within the last five minutes now satisfies the step
up on its own, because that sign in already proved possession. An older session
still has to present a passkey, which is the case the step up was written for.
The window is closed rather than open at the edge, an unparseable timestamp
never counts as fresh, and neither does one stamped in the future, so a skewed
clock cannot skip the check.

When consent cannot be completed for any reason, the screen now offers to return
to the client with access_denied instead of stranding it, so the client reports
a refused connection rather than hanging.

Discovery also advertised only the four default scopes while the server issues
orbit.read and orbit.write, so a client that trusts the metadata never asked for
the permissions the consent screen describes.

Verified end to end against a registered client: authorize, consent, code
exchange, then tools/list over the issued token.
The MCP server offered 23 tools and none of them touched the ceremony the team
actually runs. A standup could not be opened, walked, recorded, or reviewed from
a client, sprints could only be read rather than opened or closed, and milestones
were absent entirely, so planning through MCP stopped at the issue list.

Sixteen tools close that gap. The full ceremony is drivable: open the room, start
it, walk it forwards and backwards, record what each person said with their
attendance, raise a blocker against a turn and clear it later, read the open
blockers a team is carrying, and see who is overloaded before the meeting. The
rotation that decides the speaking order and the scrum master seat can be read
and set. Sprints can be opened, moved, and completed with unfinished work rolling
into the next one. Milestones can be listed and created against a project.

Every tool goes through the same service layer the web app uses, so the team and
workspace rules that apply in the UI apply here too, including the rule that
everyone in a standup has to be on the team.

Verified against a live client over the issued OAuth token: 39 tools listed, a
room opened and started through the protocol.
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
orbit Ready Ready Preview Aug 5, 2026 8:54am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@imshashank, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: db1117d9-c03d-45af-8cc9-bbc6a470d6a4

📥 Commits

Reviewing files that changed from the base of the PR and between 9f5ed92 and 09330ce.

📒 Files selected for processing (19)
  • apps/web/src/app/(auth)/oauth/authorize/decision/route.ts
  • apps/web/src/app/globals.css
  • apps/web/src/features/docs/doc-export-menu.test.tsx
  • apps/web/src/features/docs/doc-import.test.tsx
  • apps/web/src/features/docs/doc-import.tsx
  • apps/web/src/features/inbox/data.ts
  • apps/web/src/features/inbox/inbox-deltas.test.ts
  • apps/web/src/features/inbox/inbox-view.tsx
  • apps/web/src/features/issues/board.tsx
  • apps/web/src/features/issues/team-view.tsx
  • apps/web/src/lib/query/use-issues.ts
  • apps/web/src/lib/realtime/delta-bridge.tsx
  • packages/core/src/auth/mcp-token.ts
  • packages/core/src/content/doc-service.test.ts
  • packages/core/src/content/doc-service.ts
  • packages/core/src/work/cycle-service.test.ts
  • packages/core/src/work/cycle-service.ts
  • packages/core/src/work/issue-service.ts
  • packages/realtime-server/src/hub.ts
📝 Walkthrough

Walkthrough

The pull request adds OAuth step-up handling, issue search, Scrum MCP tools, sprint lifecycle pages and APIs, document access and transfer workflows, realtime synchronization, query reconciliation, and Playwright coverage.

Changes

OAuth consent and session verification

Layer / File(s) Summary
OAuth consent flow
apps/web/src/app/(auth)/oauth/..., packages/core/src/auth/mcp-token.ts
OAuth consent exposes scopes, applies fresh-session and passkey checks, handles approval failures, and returns approved client and scope data.

Issue search and reconciliation

Layer / File(s) Summary
Issue search and optimistic updates
apps/web/src/components/command-palette.tsx, apps/web/src/lib/query/use-issue-search.ts, apps/web/src/lib/query/use-issues.ts
The command palette searches issues with debounce. Issue mutations reconcile filtered query lists without full snapshots.

Sprint lifecycle

Layer / File(s) Summary
Sprint pages, APIs, history, and labels
apps/web/src/app/(app)/sprints/..., apps/web/src/app/api/cycles/..., apps/web/src/features/cycles/..., packages/core/src/work/cycle-service.ts
Sprint routes and APIs load, complete, list, and display sprint history with validated outcomes. Legacy cycle routes redirect to sprint routes.
Sprint terminology
apps/web/src/lib/navigation.ts, apps/web/src/features/analytics/*, packages/shared/src/utils/index.ts, packages/mcp-server/src/tools/planning.ts
User-facing cycle labels and fallbacks now use sprint terminology and shared sprintLabel formatting.

Document access and transfer

Layer / File(s) Summary
Document access control
packages/core/src/content/doc-service.ts, packages/shared/src/validators/doc.ts, apps/web/src/features/docs/doc-people-access.tsx, apps/web/src/lib/query/use-doc-access.ts
Document grants are validated, deduplicated, workspace-scoped, persisted, and synchronized. The UI manages user and team read/write access.
Document sharing and transfer
apps/web/src/features/docs/doc-share-menu.tsx, apps/web/src/features/docs/doc-transfer.ts, apps/web/src/features/docs/doc-import.tsx, apps/web/src/features/docs/doc-export-menu.tsx
Documents support private and restricted sharing, Markdown import/export, and browser print export.

Scrum MCP and synchronization

Layer / File(s) Summary
Scrum MCP tools and write scopes
packages/mcp-server/src/tools/scrum.ts, packages/mcp-server/src/server.ts, packages/mcp-server/src/tools/support.ts
The MCP server registers standup, sprint, and milestone tools. Write tools require orbit.write.
Standup rotation synchronization
packages/core/src/work/standup-service.ts, packages/core/src/realtime/backfill.ts, apps/web/src/app/api/standups/rotation/route.ts
Rotation changes return and publish team-scoped synchronization actions. Backfill reloads complete affected rotations.

End-to-end and test support

Layer / File(s) Summary
Browser workflow coverage
apps/web/e2e/*
Playwright tests cover board dragging, issue search, document sharing, sprint completion, and legacy route redirects.
Shared test rendering and timeouts
apps/web/src/test/render.tsx, apps/web/src/components/*.test.tsx, packages/*/package.json
Web tests use a shared provider-aware render helper. Bun test commands use explicit 20-second timeouts.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • Noveum/orbit#4: Extends cycle and issue domain services introduced by the earlier PR.
  • Noveum/orbit#9: Extends the MCP server with Scrum tools and OAuth write-scope handling.
  • Noveum/orbit#63: Shares the OAuth consent, authorization, metadata, and MCP token flow.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the primary MCP planning changes: fixing the OAuth consent dead end and adding ceremony tools.
Description check ✅ Passed The description is detailed and directly covers the MCP, board, sprint, document-sharing, search, and security changes in the pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mcp-planning

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread packages/mcp-server/src/tools/scrum.ts Outdated
Comment thread apps/web/src/app/(auth)/oauth/authorize/consent-form.tsx
…est timeout where bun reads it

A sprint date arrived as a bare string, so a client that produced "next Monday"
got an error raised deep inside the cycle service rather than a clear rejection
naming the field. Both sprint tools now check the value parses as a date and say
so at the boundary, matching the precedent heldOn already set.

The consent screen also raised its blocked notice on any failure, including a
failed deny, which left two buttons that did the same thing. It now only appears
when approving is what failed, and clears when a new attempt starts.

Separately, packages/core carried timeout = 20000 under [test] in bunfig.toml,
which bun does not read: the suite kept timing out at the 5000ms default and an
earlier run of three clean passes was luck rather than proof. The timeout moves
to the CLI flag bun actually honours, and the other database backed packages get
it too, since they share the same contention. Three consecutive full runs of the
core suite now pass.
…make search find issues

Dragging a card to another column removed it from the board. The move reached
the server and the badges moved, but the card itself rendered nowhere: the
destination column keeps its own cached list, and the reconciler was told never
to insert a row it had not already seen. That guard was added to stop a newly
created issue appearing in all seven columns at once, but belongsInList already
prevents that on its own by matching the state the list is scoped to, so the
flag only ever suppressed the legitimate case.

A list now admits a row whose scope it can verify. When a list carries an
encoded filter group the client cannot evaluate, it asks the server to settle
instead of guessing, which is the same rule the realtime path already follows.

The board drag and drop test that covered this was disabled as flaky under
synthetic mouse events. Driving the pointer through a dozen intermediate moves
turns out to be perfectly stable, so the behaviour is covered again for real:
the card lands in the target column, leaves the old one, and is still there
after a reload. The test fails against the previous reconciler.

Search was a separate gap. The palette bound to the sidebar search box only ever
matched command names, so typing an issue title returned "No matching commands"
and never called the server, even though the issue list has supported a text
query all along. It now searches issues as you type, debounced, and opens the
one you pick, while still matching commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 6

🧹 Nitpick comments (2)
packages/mcp-server/src/tools/scrum.ts (1)

464-464: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Constrain targetDate to the documented format.

The description states YYYY-MM-DD, but the schema accepts any non-empty string. An invalid value fails inside createMilestone instead of at the tool boundary, so the caller receives a less specific error. The heldOn schema at lines 39-42 already defines this pattern.

Reuse that pattern here.

♻️ Proposed change
-        targetDate: z.string().optional().describe('YYYY-MM-DD.'),
+        targetDate: z
+          .string()
+          .regex(/^\d{4}-\d{2}-\d{2}$/)
+          .optional()
+          .describe('The milestone target day, as YYYY-MM-DD.'),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp-server/src/tools/scrum.ts` at line 464, Update the targetDate
schema in the relevant Scrum tool definition to reuse the existing heldOn
YYYY-MM-DD validation pattern, while preserving its optional behavior and
description.
packages/mcp-server/src/tools.test.ts (1)

344-357: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for standup idempotency.

The open_standup description promises that opening the same day twice returns the same room. openStandup implements this with an advisory lock and an existing-row short circuit (packages/core/src/work/standup-service.ts lines 192-215). No test covers it, so a regression that creates a second room for the same day would pass.

Call open_standup twice for one heldOn value and assert that both calls return the same id.

As per coding guidelines: "A feature is not complete until it has tests that would fail if the feature broke."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp-server/src/tools.test.ts` around lines 344 - 357, Add an
idempotency assertion to the existing standup test by calling
admin.result('open_standup') twice with the same team and heldOn value, then
assert both returned standup records have the same id. Keep the existing
get_standup and missing-day assertions unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/e2e/board-drag.spec.ts`:
- Around line 44-75: Update the test case around the card selection and drag
flow to avoid mutating shared board data: create a uniquely identifiable Todo
issue for this test, use it as moving, and delete it in a finally block. Ensure
cleanup runs on assertion or drag failure as well as success, while preserving
the existing reload persistence assertions.

In `@apps/web/src/app/`(auth)/oauth/authorize/consent-form.tsx:
- Line 57: Add a colocated test for the consent form’s blocked approval
transition, covering the approval-error path around the blocked state and
related UI branches. Assert that the blocked panel appears, the passkey notice
is hidden, and selecting cancel follows the deny flow and returns the user
appropriately. Use the existing consent-form test patterns and visible component
symbols rather than changing production behavior.

In `@apps/web/src/lib/query/use-issues.ts`:
- Around line 248-253: Update settleFilteredLists and its caller reconciliation
flow to capture which filtered queries contained each moved issue before
removal, then invalidate a query when either that recorded old membership or
belongsInList(search, issue) indicates membership. Preserve the existing
admitsNewRows behavior and avoid relying solely on the post-reconciliation issue
state.

In `@apps/web/src/test/render.tsx`:
- Around line 7-11: Move the QueryClient creation out of the Providers component
and into the shared render-helper scope, then have Providers reuse that stable
client. Preserve the existing defaultOptions while ensuring rerenders retain the
same QueryClientProvider instance and query cache.

In `@packages/mcp-server/src/tools/scrum.ts`:
- Around line 108-123: The participants array in open_standup and the members
array in set_standup_rotation accept unbounded person references, causing one
concurrent resolveUserId lookup per entry. Add the same appropriate .max()
length bound to both schemas at packages/mcp-server/src/tools/scrum.ts lines
108-123 and 338-346, preserving the existing optional array behavior.
- Around line 348-357: Update the async rotation handler around setRotation to
build and publish the corresponding SyncAction after applying the rotation
changes, then return the rotation result as before. Reuse the existing action
build/publish utilities and ensure publishing occurs before the handler returns
so realtime listeners receive the update.

---

Nitpick comments:
In `@packages/mcp-server/src/tools.test.ts`:
- Around line 344-357: Add an idempotency assertion to the existing standup test
by calling admin.result('open_standup') twice with the same team and heldOn
value, then assert both returned standup records have the same id. Keep the
existing get_standup and missing-day assertions unchanged.

In `@packages/mcp-server/src/tools/scrum.ts`:
- Line 464: Update the targetDate schema in the relevant Scrum tool definition
to reuse the existing heldOn YYYY-MM-DD validation pattern, while preserving its
optional behavior and description.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1ef4b6fa-9e02-4886-9f73-6506672dde9a

📥 Commits

Reviewing files that changed from the base of the PR and between 19827b1 and 295e2bb.

📒 Files selected for processing (23)
  • apps/web/e2e/board-drag.spec.ts
  • apps/web/src/app/(auth)/oauth/authorize/consent-form.tsx
  • apps/web/src/app/(auth)/oauth/authorize/decision/route.ts
  • apps/web/src/app/(auth)/oauth/authorize/step-up.test.ts
  • apps/web/src/app/(auth)/oauth/authorize/step-up.ts
  • apps/web/src/app/.well-known/oauth-authorization-server/route.ts
  • apps/web/src/components/command-palette.test.tsx
  • apps/web/src/components/command-palette.tsx
  • apps/web/src/components/keyboard-hints.test.tsx
  • apps/web/src/components/layout/app-shell.test.tsx
  • apps/web/src/lib/query/keys.ts
  • apps/web/src/lib/query/use-issue-search.ts
  • apps/web/src/lib/query/use-issues.ts
  • apps/web/src/test/render.tsx
  • packages/core/bunfig.toml
  • packages/core/package.json
  • packages/db/package.json
  • packages/mcp-server/package.json
  • packages/mcp-server/src/tools.test.ts
  • packages/mcp-server/src/tools/index.ts
  • packages/mcp-server/src/tools/scrum.ts
  • packages/realtime-server/package.json
  • packages/services/package.json
💤 Files with no reviewable changes (1)
  • packages/core/bunfig.toml

Comment thread apps/web/e2e/board-drag.spec.ts
Comment thread apps/web/src/app/(auth)/oauth/authorize/consent-form.tsx
Comment thread apps/web/src/lib/query/use-issues.ts Outdated
Comment thread apps/web/src/test/render.tsx Outdated
Comment thread packages/mcp-server/src/tools/scrum.ts
Comment thread packages/mcp-server/src/tools/scrum.ts
The team says sprint, the product said cycle. Every label, heading, menu entry,
empty state and MCP tool description now reads sprint, and the two routes move
to /sprints and /team/<key>/sprint/active with permanent redirects from the old
paths so existing links and bookmarks still land.

The database column, the API field and the internal identifiers stay cycle. A
schema rename would have meant a migration, a breaking API change and a rewrite
of every query for no user visible gain, so the change stops at the surface a
person actually reads. Cycle time keeps its name in the analytics panel, because
that is the name of the metric rather than a reference to the sprint.

Also adds the test timeout flag to apps/realtime, which was missed when the other
database backed packages got it, and which was still failing at the 5000ms
default under contention. Three consecutive runs pass.
…ne home

Moving the routes left the breadcrumb table matching the old paths, so /sprints
and the team sprint page were not recognised as navigable and rendered without a
trail. The active sprint page also kept its old metadata title and heading.

Six places separately reconstructed a sprint's display name when the stored name
was blank, each writing "Cycle N". They now share sprintLabel in @orbit/shared,
and new sprints are created named "Sprint N" rather than "Cycle N", so the name a
person sees no longer depends on which screen they are looking at.

Cycle time keeps its name in the analytics panel: it is the started to completed
duration, the sibling of lead time, not a reference to the sprint.
Completing a sprint moves every unfinished issue into the next one, so a closed
sprint's live issue set contains only the work that landed. Every history query
built on those rows reports a shrunken scope and close to a hundred percent
completion, which is the opposite of useful. The outcome is now written into the
progress_snapshot column the schema has always carried and never used: scope,
completed, canceled, rolled over, and points, captured before the rollover runs.
A regression test asserts the recorded scope is larger than what the live query
reports afterwards, which is the whole point.

Sprints were also unreachable from the product. createCycle, updateCycle,
deleteCycle and completeCycle had no callers outside the MCP tools, so a person
using the web app could not open a sprint or close one, and therefore never
produced any history at all. They now have routes under /api/cycles.

On top of that: pastCycles and getCycleByNumber, a history list on the sprints
page showing what each closed sprint shipped, and a per sprint page at
/team/<key>/sprint/<number> so a finished sprint can be opened rather than only
the running one. Sprints closed before this change say so rather than showing
numbers that would be wrong.
The permission model was already complete in the service layer and completely
unreachable from the product: no route called setDocAccess, no screen listed who
a doc was shared with, and the share menu offered only workspace, link and
public, so a private or team doc could not be created at all. Every part of the
feature existed except the part a person could use.

The share menu now offers all five visibilities, and a restricted doc opens a
dialog that searches the workspace by name, grants read or write to a person or
a whole team, and revokes again. Granting reaches the grantee live, because the
action is published on their own scope as well as the doc's.

The service itself needed hardening before it could be exposed. It trusted the
grant array rather than parsing it, never checked that a named subject was even
in the workspace, silently kept duplicate rows for the same person, and
published nothing, so a grant was invisible to every other client including the
sharer's other tabs. It also returned "that doc does not exist" to somebody who
could plainly see the doc, which now reads as the refusal it is.

Restricting a doc no longer demands publish rights. Tightening access from
workspace to private was going through the same permission gate as publishing to
the open web, which had it backwards: only sharing outward needs doc:publish.

A person picked in the dialog was also being sent to the server double encoded,
because apiFetch already serialises the body it is given.
… stop tests sharing state

A filtered list was only invalidated when the moved issue belonged to it after
the move. An issue leaving a filtered list was removed by the reconciler before
that check ran, so the list stayed a row short with stale pagination and nothing
ever refetched it. Membership is now recorded before reconciliation, and a list
is settled when either its old or its new membership changed.

setRotation changed the speaking order and the scrum master seat without
publishing anything, so every other client kept the previous rotation until a
reload. It now returns an action on the team scope, standup_rotation joins the
sync models, and the catch up loader covers it like every other model.

The shared test render helper built a new QueryClient on each render, which
meant a component could lose the cache it had just written to. The client is now
created once per render call.

The board drag test moved whichever issue happened to be first in Todo and left
it there, so repeated runs slowly emptied the column and parallel runs fought
over the same row. It creates its own issue now. It also asserts persistence
through the API rather than relying on a reload repainting within a fixed
window, which is what made it fail on slower CI while passing locally.

Both MCP tools that accept a list of people now bound it, and the consent screen
has the regression test its blocked state was missing: the panel appears only
when approving fails, the passkey notice gives way to it, and the way out
returns the client a denial rather than nothing.
@imshashank

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai 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.

Actionable comments posted: 16

🧹 Nitpick comments (2)
apps/web/src/features/cycles/sprint-history.tsx (1)

62-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared interaction helper.

Replace the direct transition-colors and hover:bg-surface-2 classes with cardHover. This keeps the sprint card aligned with the shared hover, focus, duration, and reduced-motion policy.

As per coding guidelines, use shared interaction tokens for color transitions. Based on learnings, use cardHover for selectable card border and background behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/features/cycles/sprint-history.tsx` around lines 62 - 66, Update
the Link in the sprint history card to use the shared cardHover interaction
helper instead of the direct transition-colors and hover:bg-surface-2 classes,
while preserving the remaining layout and styling classes.

Sources: Coding guidelines, Learnings

apps/web/src/features/docs/doc-people-access.tsx (1)

108-112: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared hover interaction helpers.

Replace the custom transition-colors and hover:bg-surface-2 classes with the applicable helper from @/lib/interaction.ts.

Based on learnings, hover and focus color transitions must use the shared interaction helpers and motion tokens.

Also applies to: 151-154

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/features/docs/doc-people-access.tsx` around lines 108 - 112,
Update the buttons in the doc access entry actions around the button element and
the corresponding block at the second referenced location to replace the custom
transition-colors and hover:bg-surface-2 classes with the applicable shared
hover interaction helper from `@/lib/interaction.ts`, preserving the existing
layout and motion behavior.

Sources: Coding guidelines, Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/e2e/board-drag.spec.ts`:
- Around line 55-69: Update the API calls in the test setup around the bootstrap
lookup and issue creation to validate HTTP success before parsing, then parse
each JSON payload with Zod schemas instead of TypeScript assertions. Ensure the
schemas validate the optional teams/team fields and created issue identifier,
and fail immediately at the API boundary when the ENG team, response status, or
persisted issue data is invalid; apply the same response-validation change to
the code referenced around lines 83–88.

In `@apps/web/e2e/sprints.spec.ts`:
- Around line 22-45: Add response schemas for the /api/bootstrap and /api/cycles
POST envelopes, then parse each response body with its schema before accessing
teams, id, or number in the sprint test. Replace the current type assertions in
the evaluate callbacks while preserving the existing team lookup and
created-cycle behavior.

In `@apps/web/src/app/`(app)/team/[key]/cycle/active/page.tsx:
- Around line 8-9: Validate params.key against teamCreateSchema.key in the
active cycle page before constructing the redirect, and reject invalid keys
rather than redirecting them. Only pass the validated key to permanentRedirect,
preserving the existing legacy route behavior for valid team keys.

In `@apps/web/src/app/`(app)/team/[key]/sprint/[number]/page.tsx:
- Line 52: Update SprintPage’s CyclePanel usage so an unloaded upcoming-sprint
schedule is not represented by an empty array. Either load and pass the
applicable upcoming list, or add an explicit unavailable state that CyclePanel
uses to hide the “Nothing scheduled after this one” section until data is
available.
- Around line 12-25: Validate both dynamic route parameters with the shared Zod
schemas before any metadata generation or data lookup: in
apps/web/src/app/(app)/team/[key]/sprint/[number]/page.tsx lines 12-25, replace
sprintNumber’s Number-based parsing with the appropriate team-key and digit-only
positive sprint-number schemas, handling invalid values with the existing
notFound behavior; in apps/web/src/app/(app)/team/[key]/sprint/active/page.tsx
lines 12-22, parse the team key schema before metadata generation and team
lookup.

In `@apps/web/src/app/`(auth)/oauth/authorize/consent-form.test.tsx:
- Line 13: Replace the prohibited AI-tooling client name and host references in
the consent-form test fixture, including the clientName and corresponding values
in the nearby fixture entries, with neutral OAuth client names and URLs.
Preserve the fixture structure and test behavior.
- Around line 80-82: Update the denial-request test around the deny button
interaction to first wait for the failed denial request to enter its pending
state, then await the asynchronous rejection/handler completion before asserting
that consent-blocked is absent. Keep the existing assign-not-called assertion
while ensuring the UI assertion cannot run before the failure path finishes.

In `@apps/web/src/app/api/cycles/route.ts`:
- Around line 7-12: Add a shared Zod schema for the GET /api/cycles query
parameters and parse the result of searchParamsOf(request) before accessing
teamId, status, or limit. Use the parsed values in the route’s pastCycles call,
enforcing the intended limit type and range rather than relying on
Number.isFinite or downstream clamping; preserve the existing default behavior
for omitted parameters and empty cycles responses.

In `@apps/web/src/features/cycles/data.ts`:
- Around line 62-68: Update readOutcome to validate the entire external value
with sprintOutcomeSchema.safeParse(), including canceled, rolledOver, points,
and closedAt, and return the parsed SprintOutcome only on success; return null
for any incomplete or invalid snapshot so SprintHistory uses its
unavailable-outcome path. Add a regression test covering a partial sprint
snapshot.

In `@apps/web/src/features/docs/doc-people-access.tsx`:
- Around line 29-40: Update the access controls around useDocAccess and the
grants useMemo so pending or failed queries render an appropriate loading/error
state instead of treating missing data as an empty grant set. Keep controls
disabled or unavailable until access.data contains the current grants, and
ensure the commit path cannot call commit before that data is available.

In `@apps/web/src/features/docs/doc-share-menu.tsx`:
- Around line 65-74: Update DocShareMenuProps and the DocShareMenu component to
accept a separate canPublish permission, then use it to hide or disable the
external visibility options (`link` and `public`) while preserving
canManageAccess for access-management controls. Ensure writers without
doc:publish cannot select those options.

In `@apps/web/src/features/issues/issue-properties.tsx`:
- Around line 226-242: Complete the sprint terminology migration: in
apps/web/src/features/issues/issue-properties.tsx lines 226-242, update the
PropertyRow label to “Sprint”; in
apps/web/src/features/filters/filter-fields.tsx lines 237-247 and
apps/web/src/features/filters/grouping.ts lines 105-114, change “No cycle”
fallbacks to “No sprint”; in apps/web/src/features/analytics/cycle-panel.tsx
lines 192-209, update the selector label, zero state, and velocity entity copy
to “Sprint” while preserving “Cycle time” as the metric name.

In `@apps/web/src/lib/query/use-doc-access.ts`:
- Around line 33-44: Update useSetDocAccess to apply an optimistic grant
snapshot before the mutation, preserve the previous docAccess cache for rollback
on failure, and invalidate or reconcile the query after completion. Also
serialize mutations per docId, or disable overlapping submissions while one is
pending, so an older snapshot cannot overwrite a newer grant set.

In `@packages/core/src/realtime/backfill.ts`:
- Around line 655-673: The standup_rotation backfill currently emits member-row
updates instead of the team-level snapshot produced by setRotation, losing empty
and replacement rotations. Update the standup_rotation loader and persistence
path to retain a team-level rotation revision/tombstone with modelId equal to
the teamId, return the full rotation payload, and apply limit by team syncId
rather than member rows; add coverage for empty and replacement rotation
updates.

In `@packages/core/src/work/cycle-service.test.ts`:
- Around line 343-347: Extend the test around the persisted outcome assertion
for the cycle progress snapshot to create estimated completed and canceled
issues, pass a fixed completion time through the cycle completion flow, and
assert every SprintOutcome field: scope, completed, canceled, points,
rolledOver, and closedAt. Keep the existing assertions and verify the stored
values match the created issues and fixed timestamp.

In `@packages/shared/src/utils/index.ts`:
- Around line 147-149: Update sprintLabel to return the normalized, trimmed
sprint.name when it is non-empty, while retaining the existing Sprint number
fallback for blank names. Add coverage for names with surrounding whitespace to
verify the returned label is trimmed.

---

Nitpick comments:
In `@apps/web/src/features/cycles/sprint-history.tsx`:
- Around line 62-66: Update the Link in the sprint history card to use the
shared cardHover interaction helper instead of the direct transition-colors and
hover:bg-surface-2 classes, while preserving the remaining layout and styling
classes.

In `@apps/web/src/features/docs/doc-people-access.tsx`:
- Around line 108-112: Update the buttons in the doc access entry actions around
the button element and the corresponding block at the second referenced location
to replace the custom transition-colors and hover:bg-surface-2 classes with the
applicable shared hover interaction helper from `@/lib/interaction.ts`, preserving
the existing layout and motion behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 847ff6e8-0769-44c4-adcf-7b006cec7991

📥 Commits

Reviewing files that changed from the base of the PR and between 295e2bb and 25425ce.

📒 Files selected for processing (59)
  • apps/realtime/package.json
  • apps/web/e2e/board-drag.spec.ts
  • apps/web/e2e/doc-sharing.spec.ts
  • apps/web/e2e/sprints.spec.ts
  • apps/web/src/app/(app)/cycles/page.tsx
  • apps/web/src/app/(app)/sprints/loading.tsx
  • apps/web/src/app/(app)/sprints/page.tsx
  • apps/web/src/app/(app)/team/[key]/cycle/active/page.tsx
  • apps/web/src/app/(app)/team/[key]/sprint/[number]/page.tsx
  • apps/web/src/app/(app)/team/[key]/sprint/active/loading.tsx
  • apps/web/src/app/(app)/team/[key]/sprint/active/page.tsx
  • apps/web/src/app/(auth)/oauth/authorize/consent-form.test.tsx
  • apps/web/src/app/(auth)/oauth/authorize/consent-form.tsx
  • apps/web/src/app/api/cycles/[id]/complete/route.ts
  • apps/web/src/app/api/cycles/[id]/route.ts
  • apps/web/src/app/api/cycles/route.ts
  • apps/web/src/app/api/docs/[id]/access/route.ts
  • apps/web/src/components/command-palette.test.tsx
  • apps/web/src/features/analytics/burndown-chart.tsx
  • apps/web/src/features/analytics/cycle-panel.tsx
  • apps/web/src/features/cycles/cycle-board.tsx
  • apps/web/src/features/cycles/data.ts
  • apps/web/src/features/cycles/sprint-history.tsx
  • apps/web/src/features/docs/doc-people-access.tsx
  • apps/web/src/features/docs/doc-share-menu.tsx
  • apps/web/src/features/docs/doc-surface.tsx
  • apps/web/src/features/filters/filter-fields.tsx
  • apps/web/src/features/filters/grouping.ts
  • apps/web/src/features/issues/issue-card.tsx
  • apps/web/src/features/issues/issue-properties.tsx
  • apps/web/src/features/issues/issue-row.tsx
  • apps/web/src/lib/breadcrumbs.ts
  • apps/web/src/lib/navigation.ts
  • apps/web/src/lib/query/fetcher.ts
  • apps/web/src/lib/query/keys.ts
  • apps/web/src/lib/query/use-doc-access.ts
  • apps/web/src/lib/query/use-issues.ts
  • apps/web/src/test/render.tsx
  • packages/core/src/analytics/burndown.ts
  • packages/core/src/analytics/distribution.ts
  • packages/core/src/analytics/snapshot.ts
  • packages/core/src/content/doc-comment-service.test.ts
  • packages/core/src/content/doc-service.test.ts
  • packages/core/src/content/doc-service.ts
  • packages/core/src/org/team-service.ts
  • packages/core/src/realtime/backfill.test.ts
  • packages/core/src/realtime/backfill.ts
  • packages/core/src/work/cycle-service.test.ts
  • packages/core/src/work/cycle-service.ts
  • packages/core/src/work/standup-service.test.ts
  • packages/core/src/work/standup-service.ts
  • packages/mcp-server/src/tools/issues.ts
  • packages/mcp-server/src/tools/planning.ts
  • packages/mcp-server/src/tools/scrum.ts
  • packages/shared/src/constants/onboarding.ts
  • packages/shared/src/events/sync.ts
  • packages/shared/src/filters/index.ts
  • packages/shared/src/utils/index.ts
  • packages/shared/src/validators/doc.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/web/src/components/command-palette.test.tsx
  • apps/web/src/lib/query/use-issues.ts
  • packages/mcp-server/src/tools/scrum.ts
  • apps/web/src/app/(auth)/oauth/authorize/consent-form.tsx

Comment thread apps/web/e2e/board-drag.spec.ts Outdated
Comment thread apps/web/e2e/sprints.spec.ts Outdated
Comment thread apps/web/src/app/(app)/team/[key]/cycle/active/page.tsx
Comment thread apps/web/src/app/(app)/team/[key]/sprint/[number]/page.tsx
Comment thread apps/web/src/app/(app)/team/[key]/sprint/[number]/page.tsx Outdated
Comment thread apps/web/src/features/issues/issue-properties.tsx Outdated
Comment thread apps/web/src/lib/query/use-doc-access.ts
Comment thread packages/core/src/realtime/backfill.ts Outdated
Comment thread packages/core/src/work/cycle-service.test.ts Outdated
Comment thread packages/shared/src/utils/index.ts
…nish the wording

Two ways the share dialog could destroy access. While the grant list was still
loading, or after it had failed, the component treated the unknown set as an
empty one, so adding a single person replaced everybody who already had access.
It now waits for the list, and says so when it cannot load it. Separately, two
quick edits each sent the whole set, so a slow first request could land after a
fast second and restore what the second had just removed. Mutations are
serialized per doc and settle against the server.

A sprint outcome from before this change, or any partial snapshot, was accepted
on the strength of two fields and then dereferenced for a third, which threw
instead of falling back to the unavailable state the UI already has. It parses
with a schema now, and the test covers every persisted field rather than three
of them, including a canceled issue and the point totals.

The rotation catch up loader emitted one entry per member row while the producer
emits one snapshot per team, so a reconnecting client saw a different shape than
a connected one, and a page limit could truncate a rotation mid team. The loader
now rebuilds the same team snapshot.

Writers without publish rights were offered the link and public options and got
a server rejection for taking them. Those options are hidden unless the person
can actually publish.

Route parameters are validated: a sprint number must be digits, the legacy cycle
redirect refuses a key that is not shaped like a team key, and the sprint list
query is parsed rather than coerced by hand. The numbered sprint page also loads
the upcoming schedule instead of passing an empty list, which had it claiming
nothing was scheduled.

Also finishes the wording sweep, which still said cycle in the issue sidebar,
both filter menus, the analytics panel and the marketing copy, and trims the
sprint name so a padded one does not keep its whitespace in every label. The
consent test fixture no longer names a real vendor, and its denial case waits
for the request to settle rather than asserting before it runs.

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

♻️ Duplicate comments (1)
apps/web/src/app/(app)/team/[key]/cycle/active/page.tsx (1)

3-12: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use the shared Zod team-key schema.

Line 11 validates request input with a local regular expression. Parse params.key with the shared team-key schema and redirect with the parsed value. This keeps route validation aligned with team creation.

As per coding guidelines, “validate every external input with Zod.”

#!/bin/bash
set -euo pipefail

ast-grep outline packages/shared/src/validators/team.ts --items all
rg -n -C 3 'teamCreateSchema|teamKey.*Schema|key:.*z\.' packages/shared/src/validators apps/web/src/app
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/`(app)/team/[key]/cycle/active/page.tsx around lines 3 - 12,
Replace the local TEAM_KEY regex validation in ActiveCycleRedirect with the
shared team-key Zod schema from the validators module. Parse params.key using
that schema, use the parsed value in permanentRedirect, and preserve notFound
behavior for invalid keys.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/features/landing/landing-page.tsx`:
- Line 593: Update the visible palette result labels near the “Move to sprint”
span so “Move to Cycle 14” and “Move to Cycle 15” use “sprint” instead, keeping
the mock terminology consistent for the same action.

In `@apps/web/src/lib/query/zz-audit3.test.tsx`:
- Around line 119-140: The tests currently only log state instead of verifying
behavior. In apps/web/src/lib/query/zz-audit3.test.tsx lines 119-140, replace
timed waits and console.log calls with waitFor assertions covering source and
destination column contents during and after the move. In
packages/core/src/content/scratch-escalation.test.ts lines 35-46, add assertions
for public visibility, anonymous document retrieval, expected content, and
inclusion in listPublicDocs().

In `@packages/core/src/realtime/backfill.ts`:
- Around line 656-665: The distinct team query in catchUp must deterministically
prioritize teams by their earliest newer standup rotation syncId before applying
the page limit. Update the query around the standupRotation selection to group
each team’s matching rows, order by the minimum syncId ascending, then limit the
results; add a catchup test covering older and newer changed teams to verify no
team is skipped.

In `@packages/shared/src/validators/cycle.ts`:
- Around line 23-33: Update sprintOutcomeSchema so closedAt uses the existing
instantSchema instead of a nonempty string validator, matching startsAt and
endsAt. Adjust CycleService.outcomeOf if necessary so its returned closedAt type
is Date and remains consistent with the schema.

---

Duplicate comments:
In `@apps/web/src/app/`(app)/team/[key]/cycle/active/page.tsx:
- Around line 3-12: Replace the local TEAM_KEY regex validation in
ActiveCycleRedirect with the shared team-key Zod schema from the validators
module. Parse params.key using that schema, use the parsed value in
permanentRedirect, and preserve notFound behavior for invalid keys.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 515b96a9-140f-4db4-be60-ada1616299fc

📥 Commits

Reviewing files that changed from the base of the PR and between 25425ce and cd63f68.

📒 Files selected for processing (27)
  • apps/web/e2e/api.ts
  • apps/web/e2e/board-drag.spec.ts
  • apps/web/e2e/doc-sharing.spec.ts
  • apps/web/e2e/sprints.spec.ts
  • apps/web/src/app/(app)/team/[key]/cycle/active/page.tsx
  • apps/web/src/app/(app)/team/[key]/sprint/[number]/page.tsx
  • apps/web/src/app/(auth)/oauth/authorize/consent-form.test.tsx
  • apps/web/src/app/api/cycles/route.ts
  • apps/web/src/app/layout.tsx
  • apps/web/src/app/llms.txt/route.ts
  • apps/web/src/features/analytics/cycle-panel.tsx
  • apps/web/src/features/cycles/data.ts
  • apps/web/src/features/docs/doc-people-access.tsx
  • apps/web/src/features/docs/doc-share-menu.tsx
  • apps/web/src/features/docs/doc-surface.tsx
  • apps/web/src/features/filters/filter-fields.tsx
  • apps/web/src/features/filters/grouping.ts
  • apps/web/src/features/issues/issue-properties.tsx
  • apps/web/src/features/landing/landing-meta.ts
  • apps/web/src/features/landing/landing-page.tsx
  • apps/web/src/lib/query/use-doc-access.ts
  • apps/web/src/lib/query/zz-audit3.test.tsx
  • packages/core/src/content/scratch-escalation.test.ts
  • packages/core/src/realtime/backfill.ts
  • packages/core/src/work/cycle-service.test.ts
  • packages/shared/src/utils/index.ts
  • packages/shared/src/validators/cycle.ts
🚧 Files skipped from review as they are similar to previous changes (17)
  • apps/web/src/features/docs/doc-surface.tsx
  • apps/web/src/features/filters/filter-fields.tsx
  • apps/web/src/features/issues/issue-properties.tsx
  • packages/shared/src/utils/index.ts
  • apps/web/src/features/filters/grouping.ts
  • apps/web/e2e/sprints.spec.ts
  • apps/web/e2e/board-drag.spec.ts
  • apps/web/src/app/api/cycles/route.ts
  • apps/web/src/app/(app)/team/[key]/sprint/[number]/page.tsx
  • apps/web/src/features/cycles/data.ts
  • apps/web/src/lib/query/use-doc-access.ts
  • apps/web/e2e/doc-sharing.spec.ts
  • apps/web/src/features/docs/doc-share-menu.tsx
  • apps/web/src/app/(auth)/oauth/authorize/consent-form.test.tsx
  • apps/web/src/features/docs/doc-people-access.tsx
  • apps/web/src/features/analytics/cycle-panel.tsx
  • packages/core/src/work/cycle-service.test.ts

Comment thread apps/web/src/features/landing/landing-page.tsx
Comment thread apps/web/src/lib/query/zz-audit3.test.tsx Outdated
Comment thread packages/core/src/realtime/backfill.ts
Comment thread packages/shared/src/validators/cycle.ts
…ps another

Closing a sprint looked for the next one by number, but numbers are handed out in
creation order while windows sit on a calendar, so the two disagree the moment
somebody backfills an earlier sprint. Completing that earlier sprint then minted
a successor starting where it ended, straight through the middle of a sprint that
already existed. createCycle refuses exactly that window, so completion was
creating state the product will not let you create by hand, and activeCycle
resolves the running sprint by date, so which one is active became arbitrary.

The successor is now the earliest sprint that starts at or after the completed
one ends, and only when there is none does it mint a new one, starting after the
last sprint the team has rather than after the one just closed. Its number sits
above every existing number instead of one above the closed sprint, which was the
other way duplicates could appear.

Found by an adversarial review agent, whose reproduction is now a test asserting
that no two of a team's sprint windows clash after a completion. It fails against
the by-number lookup.

Also adds Markdown export and import and PDF through print. Export rewrites
attachment links to absolute urls so they still resolve outside the app, and adds
the title as a heading unless the body already opens with it. Import reads a
front matter title, falls back to the first heading and lifts it out of the body,
then to the file name, and refuses a file past the content limit rather than
failing at the server. A print stylesheet drops the shell so the page carries the
document rather than the application around it.
…st the database

A write grantee could publish somebody else's private doc to the open web. Making
restriction not require publish rights had left widening gated only on being able
to write, and a write grant is exactly what you hand a collaborator. Widening the
audience of a restricted doc is now the author's or an admin's call, on both the
share and the update path, while narrowing it stays open to any writer because
narrowing takes nothing away from anyone.

The passkey step up could be skipped. The hook stamping last used ran after every
call to the verification endpoint, whether or not the assertion verified, so a
deliberately failed attempt was enough to satisfy the freshness check for the
next two minutes. It only stamps on a verified assertion now.

Completing a sprint was not concurrency safe. Two completions read completed_at
as null at read committed, both passed the guard, and the loser overwrote the
winner's recorded outcome with a snapshot claiming nothing rolled over, which is
the one thing sprint history exists to record. Completion now takes the same
advisory lock the create path already used, re-reads under it, and the closing
update refuses a sprint that is already closed. The successor also has to be a
sprint that is still open, so unfinished work can no longer be filed into a
sprint that was closed weeks ago.

The MCP grant was written before the consent code was validated, so a failed
authorization still left a client connected to a workspace. The grant is written
after, and from the client and scopes recorded on the consent itself rather than
from the request body, which the browser could otherwise choose freely.

A failing mutation rolled the whole issue cache back to a snapshot taken before
it started, undoing any sibling mutation that had landed in between. Each
rollback now restores only the row its own mutation touched. A newly created
issue was also only placed into team-prefixed lists, so it never appeared in My
Issues even though the header count moved.

Also: the standup rotation route published nothing and double wrapped its
response, and pressing Previous on the first speaker marked them done and left
the room with nobody speaking.
…tool

The access token carries the scopes the user consented to, and nothing looked at
them. Consent asked for orbit.read and orbit.write separately, the screen listed
them as different permissions, and then every tool was registered regardless, so
a client granted read only could create issues, move them, open and close
sprints, change the rotation and invite members. The permission screen was
describing a boundary that did not exist.

Tools now register only when the token allows them: defineTool already knows
whether a tool is read only, so a token without orbit.write simply never sees
the write half of the server. A read only client lists 23 read tools rather than
39, every one of them annotated read only, and a write call comes back as an
unknown tool rather than being carried out.

Also orders the standup rotation catch up by the earliest new sync id before
taking a page, since selectDistinct with a limit picks rows arbitrarily and could
skip a team whose rotation changed earlier, and validates the recorded sprint
close time as an instant rather than any non empty string.
… scope on revocation

Display options did nothing on a board whose columns fetch their own rows. Hiding
sub issues or narrowing completed work filtered the flat first page, which is the
only list those options ever reached, while each column rendered the server rows
straight through. Turning an option on visibly changed nothing. The column now
applies the same filter, and its header shows the rendered count rather than the
server total whenever a filter is trimming rows and there is no further page, so
the number above a column matches what is under it.

Revoking a doc grant left the reader's live subscription in place. The socket
authorized a doc scope once, at subscribe time, and never looked again, so
somebody removed from a private doc kept receiving its updates and its comment
bodies until they reloaded. A doc or doc comment action now revalidates that
scope on every connection holding it and drops it when the reader may no longer
read the doc, failing closed if the check itself fails.

@coderabbitai coderabbitai 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.

Actionable comments posted: 7

♻️ Duplicate comments (1)
packages/core/src/realtime/backfill.ts (1)

656-671: ⚠️ Potential issue | 🟠 Major

Preserve catch-up for an empty rotation.

When setRotation removes all rows for a team, touched has no row to select. The loader then returns no standup_rotation action. A client that missed the live Redis action keeps the old rotation after reconnect.

Persist a team-level rotation revision or tombstone, or read a durable team-scoped sync record during catch-up. Add a regression test in packages/core/src/realtime/backfill.test.ts.

This is the same unresolved gap noted in the previous review. Based on learnings, an empty rotation has no standupRotation row that can carry a new syncId; catch-up cannot represent that transition. As per coding guidelines, the feature needs a regression test for this path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/realtime/backfill.ts` around lines 656 - 671, Update the
catch-up flow around the touched query in the backfill loader so teams whose
rotation rows were all removed still produce a standup_rotation action by
reading or persisting a durable team-scoped revision/tombstone, rather than
relying only on standupRotation rows. Preserve existing non-empty rotation
behavior, and add a regression test in backfill.test.ts covering reconnect
catch-up after setRotation empties a team’s rotation.

Sources: Coding guidelines, Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/app/`(auth)/oauth/authorize/decision/route.ts:
- Around line 62-68: Update the approval flow around finalizeMcpConsent and
recordMcpGrant so consent finalization and MCP grant persistence execute within
the same database transaction. Ensure a failure in either operation rolls back
both changes, allowing the consent code to be retried, while preserving the
existing approved client, user, organization, and scope values.

In `@apps/web/src/app/globals.css`:
- Around line 829-832: Replace the hardcoded print colors in the body rule with
semantic CSS custom properties, and define corresponding print background and
foreground variables in the existing theme rules. Preserve the current
light-theme values while providing appropriate values for supported themes.

In `@apps/web/src/features/docs/doc-export-menu.tsx`:
- Around line 44-60: Add colocated Bun behavioral tests for
apps/web/src/features/docs/doc-export-menu.tsx lines 44-60, covering Markdown
download generation and window.print invocation. Add tests for
apps/web/src/features/docs/doc-import.tsx lines 42-73 covering file selection,
create mutation input, successful navigation, busy state, and failed-import
error feedback.

In `@apps/web/src/features/docs/doc-import.tsx`:
- Around line 25-36: Update the take function to validate the selected file
metadata and the parsed title/content with the appropriate xSchema Zod schemas
before calling create.mutateAsync. Keep the existing content-length check, and
ensure mutation receives only schema-validated values.
- Around line 25-29: Update take to validate file.size against a bounded byte
limit before calling file.text(), rejecting oversized files without reading
them; retain the existing text.length check after decoding to enforce the
content limit as well.

In `@packages/core/src/work/cycle-service.test.ts`:
- Around line 392-449: Add a concurrency test alongside the existing completion
tests that invokes completeCycle for the same sprint twice concurrently via
Promise.allSettled. Assert exactly one operation fulfills, the other rejects
with the expected conflict, and querying the team’s cycles confirms only one
successor was created.
- Around line 436-447: Strengthen the test around firstCycle and cycle
completion by storing the preexisting sprint ID, making that sprint ineligible
for successor selection, and recording the IDs returned by listCycles before
completion. After completeCycle, assert closed.nextCycle has a new ID not among
the preexisting cycles, then verify its number exceeds the highest preexisting
sprint number.

---

Duplicate comments:
In `@packages/core/src/realtime/backfill.ts`:
- Around line 656-671: Update the catch-up flow around the touched query in the
backfill loader so teams whose rotation rows were all removed still produce a
standup_rotation action by reading or persisting a durable team-scoped
revision/tombstone, rather than relying only on standupRotation rows. Preserve
existing non-empty rotation behavior, and add a regression test in
backfill.test.ts covering reconnect catch-up after setRotation empties a team’s
rotation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 16ad15ca-4b34-4cc4-aa29-b05aea50e753

📥 Commits

Reviewing files that changed from the base of the PR and between cd63f68 and 9f5ed92.

📒 Files selected for processing (24)
  • apps/web/src/app/(auth)/oauth/authorize/decision/route.ts
  • apps/web/src/app/api/standups/rotation/route.ts
  • apps/web/src/app/globals.css
  • apps/web/src/features/docs/doc-export-menu.tsx
  • apps/web/src/features/docs/doc-import.tsx
  • apps/web/src/features/docs/doc-surface.tsx
  • apps/web/src/features/docs/doc-transfer.test.ts
  • apps/web/src/features/docs/doc-transfer.ts
  • apps/web/src/features/docs/docs-empty-pane.tsx
  • apps/web/src/features/landing/landing-page.tsx
  • apps/web/src/lib/auth/server.ts
  • apps/web/src/lib/query/use-issues.ts
  • packages/core/src/auth/mcp-token.ts
  • packages/core/src/content/doc-service.test.ts
  • packages/core/src/content/doc-service.ts
  • packages/core/src/realtime/backfill.ts
  • packages/core/src/work/cycle-service.test.ts
  • packages/core/src/work/cycle-service.ts
  • packages/core/src/work/standup-service.ts
  • packages/mcp-server/src/server.ts
  • packages/mcp-server/src/test-helpers.ts
  • packages/mcp-server/src/tools.test.ts
  • packages/mcp-server/src/tools/support.ts
  • packages/shared/src/validators/cycle.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/mcp-server/src/tools.test.ts
  • apps/web/src/features/landing/landing-page.tsx
  • packages/shared/src/validators/cycle.ts
  • packages/core/src/work/standup-service.ts

Comment thread apps/web/src/app/(auth)/oauth/authorize/decision/route.ts Outdated
Comment thread apps/web/src/app/globals.css
Comment thread apps/web/src/features/docs/doc-export-menu.tsx
Comment thread apps/web/src/features/docs/doc-import.tsx
Comment thread apps/web/src/features/docs/doc-import.tsx
Comment thread packages/core/src/work/cycle-service.test.ts
Comment thread packages/core/src/work/cycle-service.test.ts
… its labels

Dropping a card within its own column showed nothing until the server answered.
The reconciler replaced the row in place and returned the list unsorted, so the
new sortOrder it had just written had no effect on the order rendered. It sorts
after replacing now, which is what the insert path already did.

A cross column drag on a filtered board also repainted the card in the column it
had just left. Filtered lists were being settled from onMutate, so the refetch
raced the mutation and read pre move state. They settle once the server has
answered instead.

An issue arriving over the socket lost its labels, because the delta carries the
issue row and labels live in a join table, so the card rendered bare until the
next refetch. A create now publishes the labels it was created with.
…c's grantees

Deleting a sprint left every issue in it pointing at a row that no longer exists.
The foreign key nulls the column, but nothing told any client, so open boards
kept the issues filed under a sprint that had gone until somebody reloaded. The
issues are detached explicitly now and each one is announced.

A restricted doc addressed its deltas at the author's own user scope and the doc
scope. Nothing subscribed to either from the shell, so a change to a private doc
reached nobody in real time, and a person the doc had just been shared with was
never told. The shell now subscribes to the reader's own user scope, and a
sharing change is addressed at each grantee, by user or by team.
… the transfer controls

Consuming the consent code and writing the workspace grant were two separate
statements, so a grant that failed to write left the user holding a code that had
already been spent and no way to retry. They land in one transaction now.

Importing a file read the whole thing into a string before checking the limit, so
picking a very large file could take the tab down before any check ran. The size
is checked first, and the parsed title and body go through a schema before they
reach the create call rather than being trusted because the file picker had an
accept attribute.

Print colours come from theme tokens instead of two hardcoded hex values.

Adds the coverage the transfer controls were missing: exporting downloads a file
named after the doc, the PDF action hands off to the browser rather than
rendering anything, an import creates the doc and opens it and carries the
collection it started from, and a refused or oversized import neither creates a
doc nor navigates. Also adds the concurrent completion case, which asserts one
sprint completion wins, the other is refused, and exactly one successor exists.
Selection was an index into the filtered list, so a notification arriving above
the one being read silently moved the pane onto a different item, and a list that
shrank left the index pointing past the end, which took two presses to recover
from. Selection is keyed by notification id now, so what is open stays open
whatever arrives.

The panes also shared the shell's single scroll container, so a long list pushed
the whole page down and the reading pane scrolled away with it. Each pane scrolls
on its own.

The entity a notification points at was read from the database and then dropped
by the mapping layer, so nothing downstream could open the thing the notification
was about. It is carried through now, from both the initial load and the socket.
@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown

Too many files changed for review (103 files, 100 file limit).

Bypass the limit by tagging @greptile-apps to review.

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