Add first-class issue references (#4214)

## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies.
> - Operators and agents coordinate through company-scoped issues,
comments, documents, and task relationships.
> - Issue text can mention other tickets, but those references were
previously plain markdown/text without durable relationship data.
> - That made it harder to understand related work, surface backlinks,
and keep cross-ticket context visible in the board.
> - This pull request adds first-class issue reference extraction,
storage, API responses, and UI surfaces.
> - The benefit is that issue references become queryable, navigable,
and visible without relying on ad hoc text scanning.

## What Changed

- Added shared issue-reference parsing utilities and exported
reference-related types/constants.
- Added an `issue_reference_mentions` table, idempotent migration DDL,
schema exports, and database documentation.
- Added server-side issue reference services, route integration,
activity summaries, and a backfill command for existing issue content.
- Added UI reference pills, related-work panels, markdown/editor mention
handling, and issue detail/property rendering updates.
- Added focused shared, server, and UI tests for parsing, persistence,
display, and related-work behavior.
- Rebased `PAP-735-first-class-task-references` cleanly onto
`public-gh/master`; no `pnpm-lock.yaml` changes are included.

## Verification

- `pnpm -r typecheck`
- `pnpm test:run packages/shared/src/issue-references.test.ts
server/src/__tests__/issue-references-service.test.ts
ui/src/components/IssueRelatedWorkPanel.test.tsx
ui/src/components/IssueProperties.test.tsx
ui/src/components/MarkdownBody.test.tsx`

## Risks

- Medium risk because this adds a new issue-reference persistence path
that touches shared parsing, database schema, server routes, and UI
rendering.
- Migration risk is mitigated by `CREATE TABLE IF NOT EXISTS`, guarded
foreign-key creation, and `CREATE INDEX IF NOT EXISTS` statements so
users who have applied an older local version of the numbered migration
can re-run safely.
- UI risk is limited by focused component coverage, but reviewers should
still manually inspect issue detail pages containing ticket references
before merge.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5-based coding agent, tool-using shell workflow with
repository inspection, git rebase/push, typecheck, and focused Vitest
verification.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: dotta <dotta@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-04-21 10:02:52 -05:00 committed by GitHub
parent 1954eb3048
commit ab9051b595
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 16100 additions and 28 deletions

View file

@ -123,6 +123,7 @@ import { FilterBar, type FilterValue } from "@/components/FilterBar";
import { InlineEditor } from "@/components/InlineEditor";
import { PageSkeleton } from "@/components/PageSkeleton";
import { Identity } from "@/components/Identity";
import { IssueReferencePill } from "@/components/IssueReferencePill";
/* ------------------------------------------------------------------ */
/* Section wrapper */
@ -466,6 +467,21 @@ export function DesignGuide() {
))}
</div>
</SubSection>
<SubSection title="IssueReferencePill">
<p className="text-xs text-muted-foreground">
Used wherever a task is referenced in markdown, the Related Work tab, and activity summaries.
Pass <code className="font-mono">status</code> to show the target issue&apos;s state at a glance.
Use <code className="font-mono">strikethrough</code> for &quot;removed&quot; contexts.
</p>
<div className="flex items-center gap-2 flex-wrap">
<IssueReferencePill issue={{ id: "demo-1", identifier: "PAP-123", title: "Identifier only — no status yet" }} />
<IssueReferencePill issue={{ id: "demo-2", identifier: "PAP-456", title: "With in_progress status", status: "in_progress" }} />
<IssueReferencePill issue={{ id: "demo-3", identifier: "PAP-789", title: "Done status", status: "done" }} />
<IssueReferencePill issue={{ id: "demo-4", identifier: "PAP-101", title: "Blocked status", status: "blocked" }} />
<IssueReferencePill strikethrough issue={{ id: "demo-5", identifier: "PAP-202", title: "Removed (strikethrough)", status: "todo" }} />
</div>
</SubSection>
</Section>
{/* ============================================================ */}

View file

@ -63,6 +63,8 @@ import { IssueChatThread, type IssueChatComposerHandle } from "../components/Iss
import { IssueContinuationHandoff } from "../components/IssueContinuationHandoff";
import { IssueDocumentsSection } from "../components/IssueDocumentsSection";
import { IssuesList } from "../components/IssuesList";
import { IssueReferenceActivitySummary } from "../components/IssueReferenceActivitySummary";
import { IssueRelatedWorkPanel } from "../components/IssueRelatedWorkPanel";
import { IssueProperties } from "../components/IssueProperties";
import { IssueRunLedger } from "../components/IssueRunLedger";
import { IssueWorkspaceCard } from "../components/IssueWorkspaceCard";
@ -94,6 +96,7 @@ import {
Copy,
EyeOff,
Hexagon,
ListTree,
MessageSquare,
MoreHorizontal,
MoreVertical,
@ -886,10 +889,13 @@ function IssueDetailActivityTab({
) : (
<div className="space-y-1.5">
{activity.slice(0, 20).map((evt) => (
<div key={evt.id} className="flex items-center gap-1.5 text-xs text-muted-foreground">
<ActorIdentity evt={evt} agentMap={agentMap} userProfileMap={userProfileMap} />
<span>{formatIssueActivityAction(evt.action, evt.details, { agentMap, userProfileMap, currentUserId })}</span>
<span className="ml-auto shrink-0">{relativeTime(evt.createdAt)}</span>
<div key={evt.id} className="space-y-1.5 rounded-lg border border-border/60 px-3 py-2 text-xs text-muted-foreground">
<div className="flex items-center gap-1.5">
<ActorIdentity evt={evt} agentMap={agentMap} userProfileMap={userProfileMap} />
<span>{formatIssueActivityAction(evt.action, evt.details, { agentMap, userProfileMap, currentUserId })}</span>
<span className="ml-auto shrink-0">{relativeTime(evt.createdAt)}</span>
</div>
<IssueReferenceActivitySummary event={evt} />
</div>
))}
</div>
@ -2674,6 +2680,10 @@ export function IssueDetail() {
<ActivityIcon className="h-3.5 w-3.5" />
Activity
</TabsTrigger>
<TabsTrigger value="related-work" className="gap-1.5">
<ListTree className="h-3.5 w-3.5" />
Related work
</TabsTrigger>
{issuePluginTabItems.map((item) => (
<TabsTrigger key={item.value} value={item.value}>
{item.label}
@ -2738,6 +2748,10 @@ export function IssueDetail() {
) : null}
</TabsContent>
<TabsContent value="related-work">
<IssueRelatedWorkPanel relatedWork={issue.relatedWork} />
</TabsContent>
{activePluginTab && (
<TabsContent value={activePluginTab.value}>
<PluginSlotMount