Repository navigation
feat(server): threads persist linked GitHub issues - #1440
Merged
Merged
Conversation
Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no
donjor
force-pushed
the
t3code/thread-issue-links
branch
from
September 30, 2026 00:20
ec45ad5 to
ca3b407
Compare
10 tasks done
donjor
added a commit
that referenced
this pull request
Sep 30, 2026
## What Changed Agents link GitHub issues to their own thread over MCP, the way they link pull requests. | Tool | Does | | --- | --- | | `link_issue` | links by URL, or repository and number, as `agent` | | `unlink_issue` | removes a link; reports whether one existed | | `list_thread_issues` | the thread's links with last known state | - **Thread:** always the calling token's own thread - **Host default:** the project's host when only a number is given - **Repeat link:** `alreadyLinked`, no error - **Absent unlink:** `wasLinked: false`, no error - **Other rejections:** stay failures - **Deleted thread:** not found, never a no-op - **Stored URL:** canonical issue URL only - **Hosts:** github.com or the project's host - **Capability:** rides the pull request tools' gate - **Instruction:** one line beside the pull request linking line Upstream files change only by `fork-hook` lines in `McpHttpServer.ts` and `RuntimeInstructions.ts`. ## Why Agents already register the pull requests they open; the issues they work on stayed invisible to the thread. This is layer 3 of #1430, on the link model from #1440. ## Checks ```text vp fmt --check (changed files) → exit 0 vp lint (changed files) → exit 0 tsc --noEmit (apps/server) → exit 0 vp test run issues toolkit + instructions → exit 0 (13 tests) vp run fork:ci → exit 0 ``` ## Evidence A real Claude thread on this branch's dev server, told to use only its MCP tools: 1. `link_issue` the issue URL 2. `link_issue` it again 3. `list_thread_issues` 4. `unlink_issue` it 5. `unlink_issue` it again 6. `list_thread_issues`  ## Close conditions Closes #1433 - [x] link_issue: links by URL as agent - [x] unlink_issue: removes the link - [x] list_thread_issues: lists the thread's links - [x] Idempotent: a repeat link changes nothing - [x] Scope: a call reaches only its own thread - [x] Missing thread: fails, never "already linked" | Condition | Evidence | | --- | --- | | link_issue | "links by URL with source agent on the token's thread" | | unlink_issue | "unlinks a linked issue and reports a missing one as wasLinked=false" | | list_thread_issues | "lists the shell's links, with unknown state until the first read" | | Idempotent | "treats a duplicate link as alreadyLinked rather than an error" | | Scope | the thread comes from the invocation token, never the input | | Missing thread | "fails cleanly when the token's thread no longer exists"; "reports a thread deleted between lookup and dispatch as not found" | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the capture above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
This was referenced Sep 30, 2026
donjor
added a commit
that referenced
this pull request
Sep 30, 2026
## What Changed A thread stores its linked GitHub issues as orchestration state, modeled on its pull request links. ### Model - **Link:** host, repository, number, url - **Record:** source (`manual`, `handoff`, `agent`), `linkedAt` - **Snapshot:** title, state, `syncedAt` - **Commands:** `thread.issue.link`, `thread.issue.unlink` - **Sync:** internal `thread.issue-link.sync` - **Capability:** `threadIssues`, missing means unsupported - **Reverse lookup:** RPC for the threads on one issue ### Storage and reads - **Table:** idempotent `ForkSchema.ts` pass, never a numbered migration - **Thread and shell:** optional `issues`, omitted when empty - **Live:** shell subscribers refetch on a link change - **Detail stream:** carries no issue events - **Sync order:** an older snapshot never wins - **Deletion:** a re-created thread has no links Upstream files change only by one-line `fork-hook` lines; logic lives in `*.fork.ts`. ## Why Threads link pull requests but not the issues they work on. This is layer 1 of #1430: the web controls, MCP tools, sync and mobile build on it. ### Version skew | Client | Server | Result | | --- | --- | --- | | Old | New | decodes thread, shell and every stream item | | New | Old | missing `issues` reads as none; capability off | A server from before this change cannot reopen a database holding link events; `fork-delta.md` records that downgrade limit. ## Checks ```text tsc --noEmit (contracts, server, web, client-runtime, mobile) → exit 0 vp test run src/server.test.ts → exit 0 (211 tests) vp test run threadIssues + orchestration (contracts) → exit 0 (68 tests) vp test run 10 server files → exit 1 (146/147) vp run fork:ci → exit 0 ``` The one failure is `ServerEnvironment.fork.test` on this machine: the local service launcher started a different t3 version. CI is the authority for it. ## Evidence A second client holds a live shell subscription on a dev server running a snapshot copy of real data. The driver speaks the WebSocket RPC the clients use. 1. Link issues 1430 and 1431 on a real thread 2. Live shell update carries both links 3. Reverse lookup returns the thread 4. Duplicate link: rejected, no event, no update 5. Unlink 1430: live update drops it 6. Read-only database: projection row and events 7. Unlink 1431: `issues` key gone  <details> <summary>Stills</summary>   </details> ## Close conditions Refs #1431 - [x] Replay: events rebuild the same projection - [x] Stale sync: older than linkedAt is ignored - [x] Duplicate: linking twice is a no-op - [x] Re-link: works after an unlink - [x] Authorization: only readable threads return - [x] Deletion: deleted thread leaves the lookup - [ ] Gate: no link control without capability | Condition | Evidence | | --- | --- | | Replay, re-link, deletion | "link, sync, unlink, re-link and delete agree across every projection" | | Stale sync | "both projectors drop a sync for a removed link, an earlier link, or an older read" | | Duplicate | decider rejects it, no event; state unchanged | | Authorization | orchestration read scope, beside the pull request lookup | | Gate | server advertises `threadIssues`; the control lands with #1432, so this PR refs, not closes | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the recording above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Sep 30, 2026
## What Changed Agents link GitHub issues to their own thread over MCP, the way they link pull requests. | Tool | Does | | --- | --- | | `link_issue` | links by URL, or repository and number, as `agent` | | `unlink_issue` | removes a link; reports whether one existed | | `list_thread_issues` | the thread's links with last known state | - **Thread:** always the calling token's own thread - **Host default:** the project's host when only a number is given - **Repeat link:** `alreadyLinked`, no error - **Absent unlink:** `wasLinked: false`, no error - **Other rejections:** stay failures - **Deleted thread:** not found, never a no-op - **Stored URL:** canonical issue URL only - **Hosts:** github.com or the project's host - **Capability:** rides the pull request tools' gate - **Instruction:** one line beside the pull request linking line Upstream files change only by `fork-hook` lines in `McpHttpServer.ts` and `RuntimeInstructions.ts`. ## Why Agents already register the pull requests they open; the issues they work on stayed invisible to the thread. This is layer 3 of #1430, on the link model from #1440. ## Checks ```text vp fmt --check (changed files) → exit 0 vp lint (changed files) → exit 0 tsc --noEmit (apps/server) → exit 0 vp test run issues toolkit + instructions → exit 0 (13 tests) vp run fork:ci → exit 0 ``` ## Evidence A real Claude thread on this branch's dev server, told to use only its MCP tools: 1. `link_issue` the issue URL 2. `link_issue` it again 3. `list_thread_issues` 4. `unlink_issue` it 5. `unlink_issue` it again 6. `list_thread_issues`  ## Close conditions Closes #1433 - [x] link_issue: links by URL as agent - [x] unlink_issue: removes the link - [x] list_thread_issues: lists the thread's links - [x] Idempotent: a repeat link changes nothing - [x] Scope: a call reaches only its own thread - [x] Missing thread: fails, never "already linked" | Condition | Evidence | | --- | --- | | link_issue | "links by URL with source agent on the token's thread" | | unlink_issue | "unlinks a linked issue and reports a missing one as wasLinked=false" | | list_thread_issues | "lists the shell's links, with unknown state until the first read" | | Idempotent | "treats a duplicate link as alreadyLinked rather than an error" | | Scope | the thread comes from the invocation token, never the input | | Missing thread | "fails cleanly when the token's thread no longer exists"; "reports a thread deleted between lookup and dispatch as not found" | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the capture above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Sep 30, 2026
…, and palette (#1442) ## What Changed The web client links GitHub issues to threads from both sides, and **Work on this issue** links the issue at the draft's first send. | Entry point | Does | | --- | --- | | Thread issues panel | "Linked to this thread" list, **Link**, unlink | | Issue detail | "Linked threads" list, open, unlink, **Link to a thread** | | Issue actions menu | link or unlink the active thread | | Command palette | "Link GitHub issue to thread" | | Work on this issue | links as `handoff` after the turn starts | | Rule | Behavior | | --- | --- | | Handoff store | keyed by draft, in `localStorage` | | Taken | only after a turn start succeeds | | Multi-model send | links every started thread | | Failed send | entry kept for the retry | | Ask, Explain | mention the issue, never record it | | Gate | every control needs `threadIssues` | | Keybinding | none by default | | Title, state | unknown until #1434 | Upstream files change only by `fork-hook` lines in `ChatView.tsx` and `CommandPalette.tsx`. ## Why **Work on this issue** only seeded a draft, so no thread existed to link until the first send. This is layer 2 of #1430, on the link model from #1440. ## Checks ```text tsc --noEmit (apps/web) → exit 0 tsc --noEmit (packages/client-runtime) → exit 0 vp test run githubIssue + CommandPalette → exit 0 (13 files, 95 tests) vp run knip:check → exit 0 vp run fork:ci → exit 0 ``` ## Evidence This branch's dev server on a copy of real data, in Chrome. ### Thread side 1. Empty panel  2. **Link** dialog resolves a bare number  3. Linked; state unknown until a read  ### Issue side 4. The same link under "Linked threads"  5. After unlink from the issue  ### Palette  ### Handoff **Work on this issue** on this issue, then one send; the new thread links it.  Stored row: `source: handoff`, number 1432; the handoff entry was consumed. ## Close conditions Closes #1432 Closes #1431 ### Handoff - [x] **First send:** links the issue as `handoff` - [x] **Draft reuse:** the link survives - [x] **Promotion:** the link survives - [x] **Background send:** links the issue - [x] **Failed send:** a retry links it once ### Surfaces - [x] **Issue panel:** lists and opens linked threads - [x] **Entry points:** issue panel, thread, palette - [x] **Ask, Explain:** create no link ### Access (#1431) - [x] **Gate:** no link control without capability | Condition | Evidence | | --- | --- | | First send | capture 7 and the stored row; "links the draft's issue as a handoff at the first send, once" | | Draft reuse | "survives a reload and a reused draft, where the latest issue wins" | | Promotion | the entry is keyed by draft and taken after the promoted thread's turn starts | | Background send | the link runs on the shared path after `startPromise`, foreground or background | | Failed send | "keeps the issue through a failed send so the retry on a re-minted thread links it" | | Issue panel | captures 4 and 5 | | Entry points | captures 2, 4, and 6 | | Ask, Explain | only **Work on this issue** records a handoff | | Gate | panel, dialog, menu, palette, and handoff each check `threadIssues` | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes Capture 1 is the before state; no motion changed. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Sep 30, 2026
## What Changed A thread stores its linked GitHub issues as orchestration state, modeled on its pull request links. ### Model - **Link:** host, repository, number, url - **Record:** source (`manual`, `handoff`, `agent`), `linkedAt` - **Snapshot:** title, state, `syncedAt` - **Commands:** `thread.issue.link`, `thread.issue.unlink` - **Sync:** internal `thread.issue-link.sync` - **Capability:** `threadIssues`, missing means unsupported - **Reverse lookup:** RPC for the threads on one issue ### Storage and reads - **Table:** idempotent `ForkSchema.ts` pass, never a numbered migration - **Thread and shell:** optional `issues`, omitted when empty - **Live:** shell subscribers refetch on a link change - **Detail stream:** carries no issue events - **Sync order:** an older snapshot never wins - **Deletion:** a re-created thread has no links Upstream files change only by one-line `fork-hook` lines; logic lives in `*.fork.ts`. ## Why Threads link pull requests but not the issues they work on. This is layer 1 of #1430: the web controls, MCP tools, sync and mobile build on it. ### Version skew | Client | Server | Result | | --- | --- | --- | | Old | New | decodes thread, shell and every stream item | | New | Old | missing `issues` reads as none; capability off | A server from before this change cannot reopen a database holding link events; `fork-delta.md` records that downgrade limit. ## Checks ```text tsc --noEmit (contracts, server, web, client-runtime, mobile) → exit 0 vp test run src/server.test.ts → exit 0 (211 tests) vp test run threadIssues + orchestration (contracts) → exit 0 (68 tests) vp test run 10 server files → exit 1 (146/147) vp run fork:ci → exit 0 ``` The one failure is `ServerEnvironment.fork.test` on this machine: the local service launcher started a different t3 version. CI is the authority for it. ## Evidence A second client holds a live shell subscription on a dev server running a snapshot copy of real data. The driver speaks the WebSocket RPC the clients use. 1. Link issues 1430 and 1431 on a real thread 2. Live shell update carries both links 3. Reverse lookup returns the thread 4. Duplicate link: rejected, no event, no update 5. Unlink 1430: live update drops it 6. Read-only database: projection row and events 7. Unlink 1431: `issues` key gone  <details> <summary>Stills</summary>   </details> ## Close conditions Refs #1431 - [x] Replay: events rebuild the same projection - [x] Stale sync: older than linkedAt is ignored - [x] Duplicate: linking twice is a no-op - [x] Re-link: works after an unlink - [x] Authorization: only readable threads return - [x] Deletion: deleted thread leaves the lookup - [ ] Gate: no link control without capability | Condition | Evidence | | --- | --- | | Replay, re-link, deletion | "link, sync, unlink, re-link and delete agree across every projection" | | Stale sync | "both projectors drop a sync for a removed link, an earlier link, or an older read" | | Duplicate | decider rejects it, no event; state unchanged | | Authorization | orchestration read scope, beside the pull request lookup | | Gate | server advertises `threadIssues`; the control lands with #1432, so this PR refs, not closes | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the recording above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Sep 30, 2026
## What Changed Agents link GitHub issues to their own thread over MCP, the way they link pull requests. | Tool | Does | | --- | --- | | `link_issue` | links by URL, or repository and number, as `agent` | | `unlink_issue` | removes a link; reports whether one existed | | `list_thread_issues` | the thread's links with last known state | - **Thread:** always the calling token's own thread - **Host default:** the project's host when only a number is given - **Repeat link:** `alreadyLinked`, no error - **Absent unlink:** `wasLinked: false`, no error - **Other rejections:** stay failures - **Deleted thread:** not found, never a no-op - **Stored URL:** canonical issue URL only - **Hosts:** github.com or the project's host - **Capability:** rides the pull request tools' gate - **Instruction:** one line beside the pull request linking line Upstream files change only by `fork-hook` lines in `McpHttpServer.ts` and `RuntimeInstructions.ts`. ## Why Agents already register the pull requests they open; the issues they work on stayed invisible to the thread. This is layer 3 of #1430, on the link model from #1440. ## Checks ```text vp fmt --check (changed files) → exit 0 vp lint (changed files) → exit 0 tsc --noEmit (apps/server) → exit 0 vp test run issues toolkit + instructions → exit 0 (13 tests) vp run fork:ci → exit 0 ``` ## Evidence A real Claude thread on this branch's dev server, told to use only its MCP tools: 1. `link_issue` the issue URL 2. `link_issue` it again 3. `list_thread_issues` 4. `unlink_issue` it 5. `unlink_issue` it again 6. `list_thread_issues`  ## Close conditions Closes #1433 - [x] link_issue: links by URL as agent - [x] unlink_issue: removes the link - [x] list_thread_issues: lists the thread's links - [x] Idempotent: a repeat link changes nothing - [x] Scope: a call reaches only its own thread - [x] Missing thread: fails, never "already linked" | Condition | Evidence | | --- | --- | | link_issue | "links by URL with source agent on the token's thread" | | unlink_issue | "unlinks a linked issue and reports a missing one as wasLinked=false" | | list_thread_issues | "lists the shell's links, with unknown state until the first read" | | Idempotent | "treats a duplicate link as alreadyLinked rather than an error" | | Scope | the thread comes from the invocation token, never the input | | Missing thread | "fails cleanly when the token's thread no longer exists"; "reports a thread deleted between lookup and dispatch as not found" | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the capture above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Sep 30, 2026
…, and palette (#1442) ## What Changed The web client links GitHub issues to threads from both sides, and **Work on this issue** links the issue at the draft's first send. | Entry point | Does | | --- | --- | | Thread issues panel | "Linked to this thread" list, **Link**, unlink | | Issue detail | "Linked threads" list, open, unlink, **Link to a thread** | | Issue actions menu | link or unlink the active thread | | Command palette | "Link GitHub issue to thread" | | Work on this issue | links as `handoff` after the turn starts | | Rule | Behavior | | --- | --- | | Handoff store | keyed by draft, in `localStorage` | | Taken | only after a turn start succeeds | | Multi-model send | links every started thread | | Failed send | entry kept for the retry | | Ask, Explain | mention the issue, never record it | | Gate | every control needs `threadIssues` | | Keybinding | none by default | | Title, state | unknown until #1434 | Upstream files change only by `fork-hook` lines in `ChatView.tsx` and `CommandPalette.tsx`. ## Why **Work on this issue** only seeded a draft, so no thread existed to link until the first send. This is layer 2 of #1430, on the link model from #1440. ## Checks ```text tsc --noEmit (apps/web) → exit 0 tsc --noEmit (packages/client-runtime) → exit 0 vp test run githubIssue + CommandPalette → exit 0 (13 files, 95 tests) vp run knip:check → exit 0 vp run fork:ci → exit 0 ``` ## Evidence This branch's dev server on a copy of real data, in Chrome. ### Thread side 1. Empty panel  2. **Link** dialog resolves a bare number  3. Linked; state unknown until a read  ### Issue side 4. The same link under "Linked threads"  5. After unlink from the issue  ### Palette  ### Handoff **Work on this issue** on this issue, then one send; the new thread links it.  Stored row: `source: handoff`, number 1432; the handoff entry was consumed. ## Close conditions Closes #1432 Closes #1431 ### Handoff - [x] **First send:** links the issue as `handoff` - [x] **Draft reuse:** the link survives - [x] **Promotion:** the link survives - [x] **Background send:** links the issue - [x] **Failed send:** a retry links it once ### Surfaces - [x] **Issue panel:** lists and opens linked threads - [x] **Entry points:** issue panel, thread, palette - [x] **Ask, Explain:** create no link ### Access (#1431) - [x] **Gate:** no link control without capability | Condition | Evidence | | --- | --- | | First send | capture 7 and the stored row; "links the draft's issue as a handoff at the first send, once" | | Draft reuse | "survives a reload and a reused draft, where the latest issue wins" | | Promotion | the entry is keyed by draft and taken after the promoted thread's turn starts | | Background send | the link runs on the shared path after `startPromise`, foreground or background | | Failed send | "keeps the issue through a failed send so the retry on a re-minted thread links it" | | Issue panel | captures 4 and 5 | | Entry points | captures 2, 4, and 6 | | Ask, Explain | only **Work on this issue** records a handoff | | Gate | panel, dialog, menu, palette, and handoff each check `threadIssues` | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes Capture 1 is the before state; no motion changed. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Oct 1, 2026
## What Changed A thread stores its linked GitHub issues as orchestration state, modeled on its pull request links. ### Model - **Link:** host, repository, number, url - **Record:** source (`manual`, `handoff`, `agent`), `linkedAt` - **Snapshot:** title, state, `syncedAt` - **Commands:** `thread.issue.link`, `thread.issue.unlink` - **Sync:** internal `thread.issue-link.sync` - **Capability:** `threadIssues`, missing means unsupported - **Reverse lookup:** RPC for the threads on one issue ### Storage and reads - **Table:** idempotent `ForkSchema.ts` pass, never a numbered migration - **Thread and shell:** optional `issues`, omitted when empty - **Live:** shell subscribers refetch on a link change - **Detail stream:** carries no issue events - **Sync order:** an older snapshot never wins - **Deletion:** a re-created thread has no links Upstream files change only by one-line `fork-hook` lines; logic lives in `*.fork.ts`. ## Why Threads link pull requests but not the issues they work on. This is layer 1 of #1430: the web controls, MCP tools, sync and mobile build on it. ### Version skew | Client | Server | Result | | --- | --- | --- | | Old | New | decodes thread, shell and every stream item | | New | Old | missing `issues` reads as none; capability off | A server from before this change cannot reopen a database holding link events; `fork-delta.md` records that downgrade limit. ## Checks ```text tsc --noEmit (contracts, server, web, client-runtime, mobile) → exit 0 vp test run src/server.test.ts → exit 0 (211 tests) vp test run threadIssues + orchestration (contracts) → exit 0 (68 tests) vp test run 10 server files → exit 1 (146/147) vp run fork:ci → exit 0 ``` The one failure is `ServerEnvironment.fork.test` on this machine: the local service launcher started a different t3 version. CI is the authority for it. ## Evidence A second client holds a live shell subscription on a dev server running a snapshot copy of real data. The driver speaks the WebSocket RPC the clients use. 1. Link issues 1430 and 1431 on a real thread 2. Live shell update carries both links 3. Reverse lookup returns the thread 4. Duplicate link: rejected, no event, no update 5. Unlink 1430: live update drops it 6. Read-only database: projection row and events 7. Unlink 1431: `issues` key gone  <details> <summary>Stills</summary>   </details> ## Close conditions Refs #1431 - [x] Replay: events rebuild the same projection - [x] Stale sync: older than linkedAt is ignored - [x] Duplicate: linking twice is a no-op - [x] Re-link: works after an unlink - [x] Authorization: only readable threads return - [x] Deletion: deleted thread leaves the lookup - [ ] Gate: no link control without capability | Condition | Evidence | | --- | --- | | Replay, re-link, deletion | "link, sync, unlink, re-link and delete agree across every projection" | | Stale sync | "both projectors drop a sync for a removed link, an earlier link, or an older read" | | Duplicate | decider rejects it, no event; state unchanged | | Authorization | orchestration read scope, beside the pull request lookup | | Gate | server advertises `threadIssues`; the control lands with #1432, so this PR refs, not closes | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the recording above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Oct 1, 2026
## What Changed Agents link GitHub issues to their own thread over MCP, the way they link pull requests. | Tool | Does | | --- | --- | | `link_issue` | links by URL, or repository and number, as `agent` | | `unlink_issue` | removes a link; reports whether one existed | | `list_thread_issues` | the thread's links with last known state | - **Thread:** always the calling token's own thread - **Host default:** the project's host when only a number is given - **Repeat link:** `alreadyLinked`, no error - **Absent unlink:** `wasLinked: false`, no error - **Other rejections:** stay failures - **Deleted thread:** not found, never a no-op - **Stored URL:** canonical issue URL only - **Hosts:** github.com or the project's host - **Capability:** rides the pull request tools' gate - **Instruction:** one line beside the pull request linking line Upstream files change only by `fork-hook` lines in `McpHttpServer.ts` and `RuntimeInstructions.ts`. ## Why Agents already register the pull requests they open; the issues they work on stayed invisible to the thread. This is layer 3 of #1430, on the link model from #1440. ## Checks ```text vp fmt --check (changed files) → exit 0 vp lint (changed files) → exit 0 tsc --noEmit (apps/server) → exit 0 vp test run issues toolkit + instructions → exit 0 (13 tests) vp run fork:ci → exit 0 ``` ## Evidence A real Claude thread on this branch's dev server, told to use only its MCP tools: 1. `link_issue` the issue URL 2. `link_issue` it again 3. `list_thread_issues` 4. `unlink_issue` it 5. `unlink_issue` it again 6. `list_thread_issues`  ## Close conditions Closes #1433 - [x] link_issue: links by URL as agent - [x] unlink_issue: removes the link - [x] list_thread_issues: lists the thread's links - [x] Idempotent: a repeat link changes nothing - [x] Scope: a call reaches only its own thread - [x] Missing thread: fails, never "already linked" | Condition | Evidence | | --- | --- | | link_issue | "links by URL with source agent on the token's thread" | | unlink_issue | "unlinks a linked issue and reports a missing one as wasLinked=false" | | list_thread_issues | "lists the shell's links, with unknown state until the first read" | | Idempotent | "treats a duplicate link as alreadyLinked rather than an error" | | Scope | the thread comes from the invocation token, never the input | | Missing thread | "fails cleanly when the token's thread no longer exists"; "reports a thread deleted between lookup and dispatch as not found" | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the capture above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Oct 1, 2026
…, and palette (#1442) ## What Changed The web client links GitHub issues to threads from both sides, and **Work on this issue** links the issue at the draft's first send. | Entry point | Does | | --- | --- | | Thread issues panel | "Linked to this thread" list, **Link**, unlink | | Issue detail | "Linked threads" list, open, unlink, **Link to a thread** | | Issue actions menu | link or unlink the active thread | | Command palette | "Link GitHub issue to thread" | | Work on this issue | links as `handoff` after the turn starts | | Rule | Behavior | | --- | --- | | Handoff store | keyed by draft, in `localStorage` | | Taken | only after a turn start succeeds | | Multi-model send | links every started thread | | Failed send | entry kept for the retry | | Ask, Explain | mention the issue, never record it | | Gate | every control needs `threadIssues` | | Keybinding | none by default | | Title, state | unknown until #1434 | Upstream files change only by `fork-hook` lines in `ChatView.tsx` and `CommandPalette.tsx`. ## Why **Work on this issue** only seeded a draft, so no thread existed to link until the first send. This is layer 2 of #1430, on the link model from #1440. ## Checks ```text tsc --noEmit (apps/web) → exit 0 tsc --noEmit (packages/client-runtime) → exit 0 vp test run githubIssue + CommandPalette → exit 0 (13 files, 95 tests) vp run knip:check → exit 0 vp run fork:ci → exit 0 ``` ## Evidence This branch's dev server on a copy of real data, in Chrome. ### Thread side 1. Empty panel  2. **Link** dialog resolves a bare number  3. Linked; state unknown until a read  ### Issue side 4. The same link under "Linked threads"  5. After unlink from the issue  ### Palette  ### Handoff **Work on this issue** on this issue, then one send; the new thread links it.  Stored row: `source: handoff`, number 1432; the handoff entry was consumed. ## Close conditions Closes #1432 Closes #1431 ### Handoff - [x] **First send:** links the issue as `handoff` - [x] **Draft reuse:** the link survives - [x] **Promotion:** the link survives - [x] **Background send:** links the issue - [x] **Failed send:** a retry links it once ### Surfaces - [x] **Issue panel:** lists and opens linked threads - [x] **Entry points:** issue panel, thread, palette - [x] **Ask, Explain:** create no link ### Access (#1431) - [x] **Gate:** no link control without capability | Condition | Evidence | | --- | --- | | First send | capture 7 and the stored row; "links the draft's issue as a handoff at the first send, once" | | Draft reuse | "survives a reload and a reused draft, where the latest issue wins" | | Promotion | the entry is keyed by draft and taken after the promoted thread's turn starts | | Background send | the link runs on the shared path after `startPromise`, foreground or background | | Failed send | "keeps the issue through a failed send so the retry on a re-minted thread links it" | | Issue panel | captures 4 and 5 | | Entry points | captures 2, 4, and 6 | | Ask, Explain | only **Work on this issue** records a handoff | | Gate | panel, dialog, menu, palette, and handoff each check `threadIssues` | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes Capture 1 is the before state; no motion changed. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Oct 1, 2026
## What Changed A thread stores its linked GitHub issues as orchestration state, modeled on its pull request links. ### Model - **Link:** host, repository, number, url - **Record:** source (`manual`, `handoff`, `agent`), `linkedAt` - **Snapshot:** title, state, `syncedAt` - **Commands:** `thread.issue.link`, `thread.issue.unlink` - **Sync:** internal `thread.issue-link.sync` - **Capability:** `threadIssues`, missing means unsupported - **Reverse lookup:** RPC for the threads on one issue ### Storage and reads - **Table:** idempotent `ForkSchema.ts` pass, never a numbered migration - **Thread and shell:** optional `issues`, omitted when empty - **Live:** shell subscribers refetch on a link change - **Detail stream:** carries no issue events - **Sync order:** an older snapshot never wins - **Deletion:** a re-created thread has no links Upstream files change only by one-line `fork-hook` lines; logic lives in `*.fork.ts`. ## Why Threads link pull requests but not the issues they work on. This is layer 1 of #1430: the web controls, MCP tools, sync and mobile build on it. ### Version skew | Client | Server | Result | | --- | --- | --- | | Old | New | decodes thread, shell and every stream item | | New | Old | missing `issues` reads as none; capability off | A server from before this change cannot reopen a database holding link events; `fork-delta.md` records that downgrade limit. ## Checks ```text tsc --noEmit (contracts, server, web, client-runtime, mobile) → exit 0 vp test run src/server.test.ts → exit 0 (211 tests) vp test run threadIssues + orchestration (contracts) → exit 0 (68 tests) vp test run 10 server files → exit 1 (146/147) vp run fork:ci → exit 0 ``` The one failure is `ServerEnvironment.fork.test` on this machine: the local service launcher started a different t3 version. CI is the authority for it. ## Evidence A second client holds a live shell subscription on a dev server running a snapshot copy of real data. The driver speaks the WebSocket RPC the clients use. 1. Link issues 1430 and 1431 on a real thread 2. Live shell update carries both links 3. Reverse lookup returns the thread 4. Duplicate link: rejected, no event, no update 5. Unlink 1430: live update drops it 6. Read-only database: projection row and events 7. Unlink 1431: `issues` key gone  <details> <summary>Stills</summary>   </details> ## Close conditions Refs #1431 - [x] Replay: events rebuild the same projection - [x] Stale sync: older than linkedAt is ignored - [x] Duplicate: linking twice is a no-op - [x] Re-link: works after an unlink - [x] Authorization: only readable threads return - [x] Deletion: deleted thread leaves the lookup - [ ] Gate: no link control without capability | Condition | Evidence | | --- | --- | | Replay, re-link, deletion | "link, sync, unlink, re-link and delete agree across every projection" | | Stale sync | "both projectors drop a sync for a removed link, an earlier link, or an older read" | | Duplicate | decider rejects it, no event; state unchanged | | Authorization | orchestration read scope, beside the pull request lookup | | Gate | server advertises `threadIssues`; the control lands with #1432, so this PR refs, not closes | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the recording above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Oct 1, 2026
## What Changed Agents link GitHub issues to their own thread over MCP, the way they link pull requests. | Tool | Does | | --- | --- | | `link_issue` | links by URL, or repository and number, as `agent` | | `unlink_issue` | removes a link; reports whether one existed | | `list_thread_issues` | the thread's links with last known state | - **Thread:** always the calling token's own thread - **Host default:** the project's host when only a number is given - **Repeat link:** `alreadyLinked`, no error - **Absent unlink:** `wasLinked: false`, no error - **Other rejections:** stay failures - **Deleted thread:** not found, never a no-op - **Stored URL:** canonical issue URL only - **Hosts:** github.com or the project's host - **Capability:** rides the pull request tools' gate - **Instruction:** one line beside the pull request linking line Upstream files change only by `fork-hook` lines in `McpHttpServer.ts` and `RuntimeInstructions.ts`. ## Why Agents already register the pull requests they open; the issues they work on stayed invisible to the thread. This is layer 3 of #1430, on the link model from #1440. ## Checks ```text vp fmt --check (changed files) → exit 0 vp lint (changed files) → exit 0 tsc --noEmit (apps/server) → exit 0 vp test run issues toolkit + instructions → exit 0 (13 tests) vp run fork:ci → exit 0 ``` ## Evidence A real Claude thread on this branch's dev server, told to use only its MCP tools: 1. `link_issue` the issue URL 2. `link_issue` it again 3. `list_thread_issues` 4. `unlink_issue` it 5. `unlink_issue` it again 6. `list_thread_issues`  ## Close conditions Closes #1433 - [x] link_issue: links by URL as agent - [x] unlink_issue: removes the link - [x] list_thread_issues: lists the thread's links - [x] Idempotent: a repeat link changes nothing - [x] Scope: a call reaches only its own thread - [x] Missing thread: fails, never "already linked" | Condition | Evidence | | --- | --- | | link_issue | "links by URL with source agent on the token's thread" | | unlink_issue | "unlinks a linked issue and reports a missing one as wasLinked=false" | | list_thread_issues | "lists the shell's links, with unknown state until the first read" | | Idempotent | "treats a duplicate link as alreadyLinked rather than an error" | | Scope | the thread comes from the invocation token, never the input | | Missing thread | "fails cleanly when the token's thread no longer exists"; "reports a thread deleted between lookup and dispatch as not found" | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the capture above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Oct 1, 2026
…, and palette (#1442) ## What Changed The web client links GitHub issues to threads from both sides, and **Work on this issue** links the issue at the draft's first send. | Entry point | Does | | --- | --- | | Thread issues panel | "Linked to this thread" list, **Link**, unlink | | Issue detail | "Linked threads" list, open, unlink, **Link to a thread** | | Issue actions menu | link or unlink the active thread | | Command palette | "Link GitHub issue to thread" | | Work on this issue | links as `handoff` after the turn starts | | Rule | Behavior | | --- | --- | | Handoff store | keyed by draft, in `localStorage` | | Taken | only after a turn start succeeds | | Multi-model send | links every started thread | | Failed send | entry kept for the retry | | Ask, Explain | mention the issue, never record it | | Gate | every control needs `threadIssues` | | Keybinding | none by default | | Title, state | unknown until #1434 | Upstream files change only by `fork-hook` lines in `ChatView.tsx` and `CommandPalette.tsx`. ## Why **Work on this issue** only seeded a draft, so no thread existed to link until the first send. This is layer 2 of #1430, on the link model from #1440. ## Checks ```text tsc --noEmit (apps/web) → exit 0 tsc --noEmit (packages/client-runtime) → exit 0 vp test run githubIssue + CommandPalette → exit 0 (13 files, 95 tests) vp run knip:check → exit 0 vp run fork:ci → exit 0 ``` ## Evidence This branch's dev server on a copy of real data, in Chrome. ### Thread side 1. Empty panel  2. **Link** dialog resolves a bare number  3. Linked; state unknown until a read  ### Issue side 4. The same link under "Linked threads"  5. After unlink from the issue  ### Palette  ### Handoff **Work on this issue** on this issue, then one send; the new thread links it.  Stored row: `source: handoff`, number 1432; the handoff entry was consumed. ## Close conditions Closes #1432 Closes #1431 ### Handoff - [x] **First send:** links the issue as `handoff` - [x] **Draft reuse:** the link survives - [x] **Promotion:** the link survives - [x] **Background send:** links the issue - [x] **Failed send:** a retry links it once ### Surfaces - [x] **Issue panel:** lists and opens linked threads - [x] **Entry points:** issue panel, thread, palette - [x] **Ask, Explain:** create no link ### Access (#1431) - [x] **Gate:** no link control without capability | Condition | Evidence | | --- | --- | | First send | capture 7 and the stored row; "links the draft's issue as a handoff at the first send, once" | | Draft reuse | "survives a reload and a reused draft, where the latest issue wins" | | Promotion | the entry is keyed by draft and taken after the promoted thread's turn starts | | Background send | the link runs on the shared path after `startPromise`, foreground or background | | Failed send | "keeps the issue through a failed send so the retry on a re-minted thread links it" | | Issue panel | captures 4 and 5 | | Entry points | captures 2, 4, and 6 | | Ask, Explain | only **Work on this issue** records a handoff | | Gate | panel, dialog, menu, palette, and handoff each check `threadIssues` | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes Capture 1 is the before state; no motion changed. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
pushed a commit
that referenced
this pull request
Oct 2, 2026
## What Changed A thread stores its linked GitHub issues as orchestration state, modeled on its pull request links. ### Model - **Link:** host, repository, number, url - **Record:** source (`manual`, `handoff`, `agent`), `linkedAt` - **Snapshot:** title, state, `syncedAt` - **Commands:** `thread.issue.link`, `thread.issue.unlink` - **Sync:** internal `thread.issue-link.sync` - **Capability:** `threadIssues`, missing means unsupported - **Reverse lookup:** RPC for the threads on one issue ### Storage and reads - **Table:** idempotent `ForkSchema.ts` pass, never a numbered migration - **Thread and shell:** optional `issues`, omitted when empty - **Live:** shell subscribers refetch on a link change - **Detail stream:** carries no issue events - **Sync order:** an older snapshot never wins - **Deletion:** a re-created thread has no links Upstream files change only by one-line `fork-hook` lines; logic lives in `*.fork.ts`. ## Why Threads link pull requests but not the issues they work on. This is layer 1 of #1430: the web controls, MCP tools, sync and mobile build on it. ### Version skew | Client | Server | Result | | --- | --- | --- | | Old | New | decodes thread, shell and every stream item | | New | Old | missing `issues` reads as none; capability off | A server from before this change cannot reopen a database holding link events; `fork-delta.md` records that downgrade limit. ## Checks ```text tsc --noEmit (contracts, server, web, client-runtime, mobile) → exit 0 vp test run src/server.test.ts → exit 0 (211 tests) vp test run threadIssues + orchestration (contracts) → exit 0 (68 tests) vp test run 10 server files → exit 1 (146/147) vp run fork:ci → exit 0 ``` The one failure is `ServerEnvironment.fork.test` on this machine: the local service launcher started a different t3 version. CI is the authority for it. ## Evidence A second client holds a live shell subscription on a dev server running a snapshot copy of real data. The driver speaks the WebSocket RPC the clients use. 1. Link issues 1430 and 1431 on a real thread 2. Live shell update carries both links 3. Reverse lookup returns the thread 4. Duplicate link: rejected, no event, no update 5. Unlink 1430: live update drops it 6. Read-only database: projection row and events 7. Unlink 1431: `issues` key gone  <details> <summary>Stills</summary>   </details> ## Close conditions Refs #1431 - [x] Replay: events rebuild the same projection - [x] Stale sync: older than linkedAt is ignored - [x] Duplicate: linking twice is a no-op - [x] Re-link: works after an unlink - [x] Authorization: only readable threads return - [x] Deletion: deleted thread leaves the lookup - [ ] Gate: no link control without capability | Condition | Evidence | | --- | --- | | Replay, re-link, deletion | "link, sync, unlink, re-link and delete agree across every projection" | | Stale sync | "both projectors drop a sync for a removed link, an earlier link, or an older read" | | Duplicate | decider rejects it, no event; state unchanged | | Authorization | orchestration read scope, beside the pull request lookup | | Gate | server advertises `threadIssues`; the control lands with #1432, so this PR refs, not closes | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the recording above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
pushed a commit
that referenced
this pull request
Oct 2, 2026
## What Changed Agents link GitHub issues to their own thread over MCP, the way they link pull requests. | Tool | Does | | --- | --- | | `link_issue` | links by URL, or repository and number, as `agent` | | `unlink_issue` | removes a link; reports whether one existed | | `list_thread_issues` | the thread's links with last known state | - **Thread:** always the calling token's own thread - **Host default:** the project's host when only a number is given - **Repeat link:** `alreadyLinked`, no error - **Absent unlink:** `wasLinked: false`, no error - **Other rejections:** stay failures - **Deleted thread:** not found, never a no-op - **Stored URL:** canonical issue URL only - **Hosts:** github.com or the project's host - **Capability:** rides the pull request tools' gate - **Instruction:** one line beside the pull request linking line Upstream files change only by `fork-hook` lines in `McpHttpServer.ts` and `RuntimeInstructions.ts`. ## Why Agents already register the pull requests they open; the issues they work on stayed invisible to the thread. This is layer 3 of #1430, on the link model from #1440. ## Checks ```text vp fmt --check (changed files) → exit 0 vp lint (changed files) → exit 0 tsc --noEmit (apps/server) → exit 0 vp test run issues toolkit + instructions → exit 0 (13 tests) vp run fork:ci → exit 0 ``` ## Evidence A real Claude thread on this branch's dev server, told to use only its MCP tools: 1. `link_issue` the issue URL 2. `link_issue` it again 3. `list_thread_issues` 4. `unlink_issue` it 5. `unlink_issue` it again 6. `list_thread_issues`  ## Close conditions Closes #1433 - [x] link_issue: links by URL as agent - [x] unlink_issue: removes the link - [x] list_thread_issues: lists the thread's links - [x] Idempotent: a repeat link changes nothing - [x] Scope: a call reaches only its own thread - [x] Missing thread: fails, never "already linked" | Condition | Evidence | | --- | --- | | link_issue | "links by URL with source agent on the token's thread" | | unlink_issue | "unlinks a linked issue and reports a missing one as wasLinked=false" | | list_thread_issues | "lists the shell's links, with unknown state until the first read" | | Idempotent | "treats a duplicate link as alreadyLinked rather than an error" | | Scope | the thread comes from the invocation token, never the input | | Missing thread | "fails cleanly when the token's thread no longer exists"; "reports a thread deleted between lookup and dispatch as not found" | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the capture above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
pushed a commit
that referenced
this pull request
Oct 2, 2026
…, and palette (#1442) ## What Changed The web client links GitHub issues to threads from both sides, and **Work on this issue** links the issue at the draft's first send. | Entry point | Does | | --- | --- | | Thread issues panel | "Linked to this thread" list, **Link**, unlink | | Issue detail | "Linked threads" list, open, unlink, **Link to a thread** | | Issue actions menu | link or unlink the active thread | | Command palette | "Link GitHub issue to thread" | | Work on this issue | links as `handoff` after the turn starts | | Rule | Behavior | | --- | --- | | Handoff store | keyed by draft, in `localStorage` | | Taken | only after a turn start succeeds | | Multi-model send | links every started thread | | Failed send | entry kept for the retry | | Ask, Explain | mention the issue, never record it | | Gate | every control needs `threadIssues` | | Keybinding | none by default | | Title, state | unknown until #1434 | Upstream files change only by `fork-hook` lines in `ChatView.tsx` and `CommandPalette.tsx`. ## Why **Work on this issue** only seeded a draft, so no thread existed to link until the first send. This is layer 2 of #1430, on the link model from #1440. ## Checks ```text tsc --noEmit (apps/web) → exit 0 tsc --noEmit (packages/client-runtime) → exit 0 vp test run githubIssue + CommandPalette → exit 0 (13 files, 95 tests) vp run knip:check → exit 0 vp run fork:ci → exit 0 ``` ## Evidence This branch's dev server on a copy of real data, in Chrome. ### Thread side 1. Empty panel  2. **Link** dialog resolves a bare number  3. Linked; state unknown until a read  ### Issue side 4. The same link under "Linked threads"  5. After unlink from the issue  ### Palette  ### Handoff **Work on this issue** on this issue, then one send; the new thread links it.  Stored row: `source: handoff`, number 1432; the handoff entry was consumed. ## Close conditions Closes #1432 Closes #1431 ### Handoff - [x] **First send:** links the issue as `handoff` - [x] **Draft reuse:** the link survives - [x] **Promotion:** the link survives - [x] **Background send:** links the issue - [x] **Failed send:** a retry links it once ### Surfaces - [x] **Issue panel:** lists and opens linked threads - [x] **Entry points:** issue panel, thread, palette - [x] **Ask, Explain:** create no link ### Access (#1431) - [x] **Gate:** no link control without capability | Condition | Evidence | | --- | --- | | First send | capture 7 and the stored row; "links the draft's issue as a handoff at the first send, once" | | Draft reuse | "survives a reload and a reused draft, where the latest issue wins" | | Promotion | the entry is keyed by draft and taken after the promoted thread's turn starts | | Background send | the link runs on the shared path after `startPromise`, foreground or background | | Failed send | "keeps the issue through a failed send so the retry on a re-minted thread links it" | | Issue panel | captures 4 and 5 | | Entry points | captures 2, 4, and 6 | | Ask, Explain | only **Work on this issue** records a handoff | | Gate | panel, dialog, menu, palette, and handoff each check `threadIssues` | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes Capture 1 is the before state; no motion changed. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Oct 2, 2026
## What Changed A thread stores its linked GitHub issues as orchestration state, modeled on its pull request links. ### Model - **Link:** host, repository, number, url - **Record:** source (`manual`, `handoff`, `agent`), `linkedAt` - **Snapshot:** title, state, `syncedAt` - **Commands:** `thread.issue.link`, `thread.issue.unlink` - **Sync:** internal `thread.issue-link.sync` - **Capability:** `threadIssues`, missing means unsupported - **Reverse lookup:** RPC for the threads on one issue ### Storage and reads - **Table:** idempotent `ForkSchema.ts` pass, never a numbered migration - **Thread and shell:** optional `issues`, omitted when empty - **Live:** shell subscribers refetch on a link change - **Detail stream:** carries no issue events - **Sync order:** an older snapshot never wins - **Deletion:** a re-created thread has no links Upstream files change only by one-line `fork-hook` lines; logic lives in `*.fork.ts`. ## Why Threads link pull requests but not the issues they work on. This is layer 1 of #1430: the web controls, MCP tools, sync and mobile build on it. ### Version skew | Client | Server | Result | | --- | --- | --- | | Old | New | decodes thread, shell and every stream item | | New | Old | missing `issues` reads as none; capability off | A server from before this change cannot reopen a database holding link events; `fork-delta.md` records that downgrade limit. ## Checks ```text tsc --noEmit (contracts, server, web, client-runtime, mobile) → exit 0 vp test run src/server.test.ts → exit 0 (211 tests) vp test run threadIssues + orchestration (contracts) → exit 0 (68 tests) vp test run 10 server files → exit 1 (146/147) vp run fork:ci → exit 0 ``` The one failure is `ServerEnvironment.fork.test` on this machine: the local service launcher started a different t3 version. CI is the authority for it. ## Evidence A second client holds a live shell subscription on a dev server running a snapshot copy of real data. The driver speaks the WebSocket RPC the clients use. 1. Link issues 1430 and 1431 on a real thread 2. Live shell update carries both links 3. Reverse lookup returns the thread 4. Duplicate link: rejected, no event, no update 5. Unlink 1430: live update drops it 6. Read-only database: projection row and events 7. Unlink 1431: `issues` key gone  <details> <summary>Stills</summary>   </details> ## Close conditions Refs #1431 - [x] Replay: events rebuild the same projection - [x] Stale sync: older than linkedAt is ignored - [x] Duplicate: linking twice is a no-op - [x] Re-link: works after an unlink - [x] Authorization: only readable threads return - [x] Deletion: deleted thread leaves the lookup - [ ] Gate: no link control without capability | Condition | Evidence | | --- | --- | | Replay, re-link, deletion | "link, sync, unlink, re-link and delete agree across every projection" | | Stale sync | "both projectors drop a sync for a removed link, an earlier link, or an older read" | | Duplicate | decider rejects it, no event; state unchanged | | Authorization | orchestration read scope, beside the pull request lookup | | Gate | server advertises `threadIssues`; the control lands with #1432, so this PR refs, not closes | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the recording above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Oct 2, 2026
## What Changed Agents link GitHub issues to their own thread over MCP, the way they link pull requests. | Tool | Does | | --- | --- | | `link_issue` | links by URL, or repository and number, as `agent` | | `unlink_issue` | removes a link; reports whether one existed | | `list_thread_issues` | the thread's links with last known state | - **Thread:** always the calling token's own thread - **Host default:** the project's host when only a number is given - **Repeat link:** `alreadyLinked`, no error - **Absent unlink:** `wasLinked: false`, no error - **Other rejections:** stay failures - **Deleted thread:** not found, never a no-op - **Stored URL:** canonical issue URL only - **Hosts:** github.com or the project's host - **Capability:** rides the pull request tools' gate - **Instruction:** one line beside the pull request linking line Upstream files change only by `fork-hook` lines in `McpHttpServer.ts` and `RuntimeInstructions.ts`. ## Why Agents already register the pull requests they open; the issues they work on stayed invisible to the thread. This is layer 3 of #1430, on the link model from #1440. ## Checks ```text vp fmt --check (changed files) → exit 0 vp lint (changed files) → exit 0 tsc --noEmit (apps/server) → exit 0 vp test run issues toolkit + instructions → exit 0 (13 tests) vp run fork:ci → exit 0 ``` ## Evidence A real Claude thread on this branch's dev server, told to use only its MCP tools: 1. `link_issue` the issue URL 2. `link_issue` it again 3. `list_thread_issues` 4. `unlink_issue` it 5. `unlink_issue` it again 6. `list_thread_issues`  ## Close conditions Closes #1433 - [x] link_issue: links by URL as agent - [x] unlink_issue: removes the link - [x] list_thread_issues: lists the thread's links - [x] Idempotent: a repeat link changes nothing - [x] Scope: a call reaches only its own thread - [x] Missing thread: fails, never "already linked" | Condition | Evidence | | --- | --- | | link_issue | "links by URL with source agent on the token's thread" | | unlink_issue | "unlinks a linked issue and reports a missing one as wasLinked=false" | | list_thread_issues | "lists the shell's links, with unknown state until the first read" | | Idempotent | "treats a duplicate link as alreadyLinked rather than an error" | | Scope | the thread comes from the invocation token, never the input | | Missing thread | "fails cleanly when the token's thread no longer exists"; "reports a thread deleted between lookup and dispatch as not found" | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the capture above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Oct 2, 2026
…, and palette (#1442) ## What Changed The web client links GitHub issues to threads from both sides, and **Work on this issue** links the issue at the draft's first send. | Entry point | Does | | --- | --- | | Thread issues panel | "Linked to this thread" list, **Link**, unlink | | Issue detail | "Linked threads" list, open, unlink, **Link to a thread** | | Issue actions menu | link or unlink the active thread | | Command palette | "Link GitHub issue to thread" | | Work on this issue | links as `handoff` after the turn starts | | Rule | Behavior | | --- | --- | | Handoff store | keyed by draft, in `localStorage` | | Taken | only after a turn start succeeds | | Multi-model send | links every started thread | | Failed send | entry kept for the retry | | Ask, Explain | mention the issue, never record it | | Gate | every control needs `threadIssues` | | Keybinding | none by default | | Title, state | unknown until #1434 | Upstream files change only by `fork-hook` lines in `ChatView.tsx` and `CommandPalette.tsx`. ## Why **Work on this issue** only seeded a draft, so no thread existed to link until the first send. This is layer 2 of #1430, on the link model from #1440. ## Checks ```text tsc --noEmit (apps/web) → exit 0 tsc --noEmit (packages/client-runtime) → exit 0 vp test run githubIssue + CommandPalette → exit 0 (13 files, 95 tests) vp run knip:check → exit 0 vp run fork:ci → exit 0 ``` ## Evidence This branch's dev server on a copy of real data, in Chrome. ### Thread side 1. Empty panel  2. **Link** dialog resolves a bare number  3. Linked; state unknown until a read  ### Issue side 4. The same link under "Linked threads"  5. After unlink from the issue  ### Palette  ### Handoff **Work on this issue** on this issue, then one send; the new thread links it.  Stored row: `source: handoff`, number 1432; the handoff entry was consumed. ## Close conditions Closes #1432 Closes #1431 ### Handoff - [x] **First send:** links the issue as `handoff` - [x] **Draft reuse:** the link survives - [x] **Promotion:** the link survives - [x] **Background send:** links the issue - [x] **Failed send:** a retry links it once ### Surfaces - [x] **Issue panel:** lists and opens linked threads - [x] **Entry points:** issue panel, thread, palette - [x] **Ask, Explain:** create no link ### Access (#1431) - [x] **Gate:** no link control without capability | Condition | Evidence | | --- | --- | | First send | capture 7 and the stored row; "links the draft's issue as a handoff at the first send, once" | | Draft reuse | "survives a reload and a reused draft, where the latest issue wins" | | Promotion | the entry is keyed by draft and taken after the promoted thread's turn starts | | Background send | the link runs on the shared path after `startPromise`, foreground or background | | Failed send | "keeps the issue through a failed send so the retry on a re-minted thread links it" | | Issue panel | captures 4 and 5 | | Entry points | captures 2, 4, and 6 | | Ask, Explain | only **Work on this issue** records a handoff | | Gate | panel, dialog, menu, palette, and handoff each check `threadIssues` | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes Capture 1 is the before state; no motion changed. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Oct 2, 2026
## What Changed A thread stores its linked GitHub issues as orchestration state, modeled on its pull request links. ### Model - **Link:** host, repository, number, url - **Record:** source (`manual`, `handoff`, `agent`), `linkedAt` - **Snapshot:** title, state, `syncedAt` - **Commands:** `thread.issue.link`, `thread.issue.unlink` - **Sync:** internal `thread.issue-link.sync` - **Capability:** `threadIssues`, missing means unsupported - **Reverse lookup:** RPC for the threads on one issue ### Storage and reads - **Table:** idempotent `ForkSchema.ts` pass, never a numbered migration - **Thread and shell:** optional `issues`, omitted when empty - **Live:** shell subscribers refetch on a link change - **Detail stream:** carries no issue events - **Sync order:** an older snapshot never wins - **Deletion:** a re-created thread has no links Upstream files change only by one-line `fork-hook` lines; logic lives in `*.fork.ts`. ## Why Threads link pull requests but not the issues they work on. This is layer 1 of #1430: the web controls, MCP tools, sync and mobile build on it. ### Version skew | Client | Server | Result | | --- | --- | --- | | Old | New | decodes thread, shell and every stream item | | New | Old | missing `issues` reads as none; capability off | A server from before this change cannot reopen a database holding link events; `fork-delta.md` records that downgrade limit. ## Checks ```text tsc --noEmit (contracts, server, web, client-runtime, mobile) → exit 0 vp test run src/server.test.ts → exit 0 (211 tests) vp test run threadIssues + orchestration (contracts) → exit 0 (68 tests) vp test run 10 server files → exit 1 (146/147) vp run fork:ci → exit 0 ``` The one failure is `ServerEnvironment.fork.test` on this machine: the local service launcher started a different t3 version. CI is the authority for it. ## Evidence A second client holds a live shell subscription on a dev server running a snapshot copy of real data. The driver speaks the WebSocket RPC the clients use. 1. Link issues 1430 and 1431 on a real thread 2. Live shell update carries both links 3. Reverse lookup returns the thread 4. Duplicate link: rejected, no event, no update 5. Unlink 1430: live update drops it 6. Read-only database: projection row and events 7. Unlink 1431: `issues` key gone  <details> <summary>Stills</summary>   </details> ## Close conditions Refs #1431 - [x] Replay: events rebuild the same projection - [x] Stale sync: older than linkedAt is ignored - [x] Duplicate: linking twice is a no-op - [x] Re-link: works after an unlink - [x] Authorization: only readable threads return - [x] Deletion: deleted thread leaves the lookup - [ ] Gate: no link control without capability | Condition | Evidence | | --- | --- | | Replay, re-link, deletion | "link, sync, unlink, re-link and delete agree across every projection" | | Stale sync | "both projectors drop a sync for a removed link, an earlier link, or an older read" | | Duplicate | decider rejects it, no event; state unchanged | | Authorization | orchestration read scope, beside the pull request lookup | | Gate | server advertises `threadIssues`; the control lands with #1432, so this PR refs, not closes | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the recording above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Oct 2, 2026
## What Changed Agents link GitHub issues to their own thread over MCP, the way they link pull requests. | Tool | Does | | --- | --- | | `link_issue` | links by URL, or repository and number, as `agent` | | `unlink_issue` | removes a link; reports whether one existed | | `list_thread_issues` | the thread's links with last known state | - **Thread:** always the calling token's own thread - **Host default:** the project's host when only a number is given - **Repeat link:** `alreadyLinked`, no error - **Absent unlink:** `wasLinked: false`, no error - **Other rejections:** stay failures - **Deleted thread:** not found, never a no-op - **Stored URL:** canonical issue URL only - **Hosts:** github.com or the project's host - **Capability:** rides the pull request tools' gate - **Instruction:** one line beside the pull request linking line Upstream files change only by `fork-hook` lines in `McpHttpServer.ts` and `RuntimeInstructions.ts`. ## Why Agents already register the pull requests they open; the issues they work on stayed invisible to the thread. This is layer 3 of #1430, on the link model from #1440. ## Checks ```text vp fmt --check (changed files) → exit 0 vp lint (changed files) → exit 0 tsc --noEmit (apps/server) → exit 0 vp test run issues toolkit + instructions → exit 0 (13 tests) vp run fork:ci → exit 0 ``` ## Evidence A real Claude thread on this branch's dev server, told to use only its MCP tools: 1. `link_issue` the issue URL 2. `link_issue` it again 3. `list_thread_issues` 4. `unlink_issue` it 5. `unlink_issue` it again 6. `list_thread_issues`  ## Close conditions Closes #1433 - [x] link_issue: links by URL as agent - [x] unlink_issue: removes the link - [x] list_thread_issues: lists the thread's links - [x] Idempotent: a repeat link changes nothing - [x] Scope: a call reaches only its own thread - [x] Missing thread: fails, never "already linked" | Condition | Evidence | | --- | --- | | link_issue | "links by URL with source agent on the token's thread" | | unlink_issue | "unlinks a linked issue and reports a missing one as wasLinked=false" | | list_thread_issues | "lists the shell's links, with unknown state until the first read" | | Idempotent | "treats a duplicate link as alreadyLinked rather than an error" | | Scope | the thread comes from the invocation token, never the input | | Missing thread | "fails cleanly when the token's thread no longer exists"; "reports a thread deleted between lookup and dispatch as not found" | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the capture above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
pushed a commit
that referenced
this pull request
Oct 4, 2026
## What Changed A thread stores its linked GitHub issues as orchestration state, modeled on its pull request links. ### Model - **Link:** host, repository, number, url - **Record:** source (`manual`, `handoff`, `agent`), `linkedAt` - **Snapshot:** title, state, `syncedAt` - **Commands:** `thread.issue.link`, `thread.issue.unlink` - **Sync:** internal `thread.issue-link.sync` - **Capability:** `threadIssues`, missing means unsupported - **Reverse lookup:** RPC for the threads on one issue ### Storage and reads - **Table:** idempotent `ForkSchema.ts` pass, never a numbered migration - **Thread and shell:** optional `issues`, omitted when empty - **Live:** shell subscribers refetch on a link change - **Detail stream:** carries no issue events - **Sync order:** an older snapshot never wins - **Deletion:** a re-created thread has no links Upstream files change only by one-line `fork-hook` lines; logic lives in `*.fork.ts`. ## Why Threads link pull requests but not the issues they work on. This is layer 1 of #1430: the web controls, MCP tools, sync and mobile build on it. ### Version skew | Client | Server | Result | | --- | --- | --- | | Old | New | decodes thread, shell and every stream item | | New | Old | missing `issues` reads as none; capability off | A server from before this change cannot reopen a database holding link events; `fork-delta.md` records that downgrade limit. ## Checks ```text tsc --noEmit (contracts, server, web, client-runtime, mobile) → exit 0 vp test run src/server.test.ts → exit 0 (211 tests) vp test run threadIssues + orchestration (contracts) → exit 0 (68 tests) vp test run 10 server files → exit 1 (146/147) vp run fork:ci → exit 0 ``` The one failure is `ServerEnvironment.fork.test` on this machine: the local service launcher started a different t3 version. CI is the authority for it. ## Evidence A second client holds a live shell subscription on a dev server running a snapshot copy of real data. The driver speaks the WebSocket RPC the clients use. 1. Link issues 1430 and 1431 on a real thread 2. Live shell update carries both links 3. Reverse lookup returns the thread 4. Duplicate link: rejected, no event, no update 5. Unlink 1430: live update drops it 6. Read-only database: projection row and events 7. Unlink 1431: `issues` key gone  <details> <summary>Stills</summary>   </details> ## Close conditions Refs #1431 - [x] Replay: events rebuild the same projection - [x] Stale sync: older than linkedAt is ignored - [x] Duplicate: linking twice is a no-op - [x] Re-link: works after an unlink - [x] Authorization: only readable threads return - [x] Deletion: deleted thread leaves the lookup - [ ] Gate: no link control without capability | Condition | Evidence | | --- | --- | | Replay, re-link, deletion | "link, sync, unlink, re-link and delete agree across every projection" | | Stale sync | "both projectors drop a sync for a removed link, an earlier link, or an older read" | | Duplicate | decider rejects it, no event; state unchanged | | Authorization | orchestration read scope, beside the pull request lookup | | Gate | server advertises `threadIssues`; the control lands with #1432, so this PR refs, not closes | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the recording above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Squashes: - 9151ae2 feat(server): threads persist linked GitHub issues (#1440) (#1440) - 2822287 fixup! feat(server): threads persist linked GitHub issues (#1440) (#1584) (#1584) Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no
donjor
added a commit
that referenced
this pull request
Oct 4, 2026
## What Changed Agents link GitHub issues to their own thread over MCP, the way they link pull requests. | Tool | Does | | --- | --- | | `link_issue` | links by URL, or repository and number, as `agent` | | `unlink_issue` | removes a link; reports whether one existed | | `list_thread_issues` | the thread's links with last known state | - **Thread:** always the calling token's own thread - **Host default:** the project's host when only a number is given - **Repeat link:** `alreadyLinked`, no error - **Absent unlink:** `wasLinked: false`, no error - **Other rejections:** stay failures - **Deleted thread:** not found, never a no-op - **Stored URL:** canonical issue URL only - **Hosts:** github.com or the project's host - **Capability:** rides the pull request tools' gate - **Instruction:** one line beside the pull request linking line Upstream files change only by `fork-hook` lines in `McpHttpServer.ts` and `RuntimeInstructions.ts`. ## Why Agents already register the pull requests they open; the issues they work on stayed invisible to the thread. This is layer 3 of #1430, on the link model from #1440. ## Checks ```text vp fmt --check (changed files) → exit 0 vp lint (changed files) → exit 0 tsc --noEmit (apps/server) → exit 0 vp test run issues toolkit + instructions → exit 0 (13 tests) vp run fork:ci → exit 0 ``` ## Evidence A real Claude thread on this branch's dev server, told to use only its MCP tools: 1. `link_issue` the issue URL 2. `link_issue` it again 3. `list_thread_issues` 4. `unlink_issue` it 5. `unlink_issue` it again 6. `list_thread_issues`  ## Close conditions Closes #1433 - [x] link_issue: links by URL as agent - [x] unlink_issue: removes the link - [x] list_thread_issues: lists the thread's links - [x] Idempotent: a repeat link changes nothing - [x] Scope: a call reaches only its own thread - [x] Missing thread: fails, never "already linked" | Condition | Evidence | | --- | --- | | link_issue | "links by URL with source agent on the token's thread" | | unlink_issue | "unlinks a linked issue and reports a missing one as wasLinked=false" | | list_thread_issues | "lists the shell's links, with unknown state until the first read" | | Idempotent | "treats a duplicate link as alreadyLinked rather than an error" | | Scope | the thread comes from the invocation token, never the input | | Missing thread | "fails cleanly when the token's thread no longer exists"; "reports a thread deleted between lookup and dispatch as not found" | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the capture above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
pushed a commit
that referenced
this pull request
Oct 4, 2026
…, and palette (#1442) ## What Changed The web client links GitHub issues to threads from both sides, and **Work on this issue** links the issue at the draft's first send. | Entry point | Does | | --- | --- | | Thread issues panel | "Linked to this thread" list, **Link**, unlink | | Issue detail | "Linked threads" list, open, unlink, **Link to a thread** | | Issue actions menu | link or unlink the active thread | | Command palette | "Link GitHub issue to thread" | | Work on this issue | links as `handoff` after the turn starts | | Rule | Behavior | | --- | --- | | Handoff store | keyed by draft, in `localStorage` | | Taken | only after a turn start succeeds | | Multi-model send | links every started thread | | Failed send | entry kept for the retry | | Ask, Explain | mention the issue, never record it | | Gate | every control needs `threadIssues` | | Keybinding | none by default | | Title, state | unknown until #1434 | Upstream files change only by `fork-hook` lines in `ChatView.tsx` and `CommandPalette.tsx`. ## Why **Work on this issue** only seeded a draft, so no thread existed to link until the first send. This is layer 2 of #1430, on the link model from #1440. ## Checks ```text tsc --noEmit (apps/web) → exit 0 tsc --noEmit (packages/client-runtime) → exit 0 vp test run githubIssue + CommandPalette → exit 0 (13 files, 95 tests) vp run knip:check → exit 0 vp run fork:ci → exit 0 ``` ## Evidence This branch's dev server on a copy of real data, in Chrome. ### Thread side 1. Empty panel  2. **Link** dialog resolves a bare number  3. Linked; state unknown until a read  ### Issue side 4. The same link under "Linked threads"  5. After unlink from the issue  ### Palette  ### Handoff **Work on this issue** on this issue, then one send; the new thread links it.  Stored row: `source: handoff`, number 1432; the handoff entry was consumed. ## Close conditions Closes #1432 Closes #1431 ### Handoff - [x] **First send:** links the issue as `handoff` - [x] **Draft reuse:** the link survives - [x] **Promotion:** the link survives - [x] **Background send:** links the issue - [x] **Failed send:** a retry links it once ### Surfaces - [x] **Issue panel:** lists and opens linked threads - [x] **Entry points:** issue panel, thread, palette - [x] **Ask, Explain:** create no link ### Access (#1431) - [x] **Gate:** no link control without capability | Condition | Evidence | | --- | --- | | First send | capture 7 and the stored row; "links the draft's issue as a handoff at the first send, once" | | Draft reuse | "survives a reload and a reused draft, where the latest issue wins" | | Promotion | the entry is keyed by draft and taken after the promoted thread's turn starts | | Background send | the link runs on the shared path after `startPromise`, foreground or background | | Failed send | "keeps the issue through a failed send so the retry on a re-minted thread links it" | | Issue panel | captures 4 and 5 | | Entry points | captures 2, 4, and 6 | | Ask, Explain | only **Work on this issue** records a handoff | | Gate | panel, dialog, menu, palette, and handoff each check `threadIssues` | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes Capture 1 is the before state; no motion changed. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
pushed a commit
that referenced
this pull request
Oct 4, 2026
## What Changed A thread stores its linked GitHub issues as orchestration state, modeled on its pull request links. ### Model - **Link:** host, repository, number, url - **Record:** source (`manual`, `handoff`, `agent`), `linkedAt` - **Snapshot:** title, state, `syncedAt` - **Commands:** `thread.issue.link`, `thread.issue.unlink` - **Sync:** internal `thread.issue-link.sync` - **Capability:** `threadIssues`, missing means unsupported - **Reverse lookup:** RPC for the threads on one issue ### Storage and reads - **Table:** idempotent `ForkSchema.ts` pass, never a numbered migration - **Thread and shell:** optional `issues`, omitted when empty - **Live:** shell subscribers refetch on a link change - **Detail stream:** carries no issue events - **Sync order:** an older snapshot never wins - **Deletion:** a re-created thread has no links Upstream files change only by one-line `fork-hook` lines; logic lives in `*.fork.ts`. ## Why Threads link pull requests but not the issues they work on. This is layer 1 of #1430: the web controls, MCP tools, sync and mobile build on it. ### Version skew | Client | Server | Result | | --- | --- | --- | | Old | New | decodes thread, shell and every stream item | | New | Old | missing `issues` reads as none; capability off | A server from before this change cannot reopen a database holding link events; `fork-delta.md` records that downgrade limit. ## Checks ```text tsc --noEmit (contracts, server, web, client-runtime, mobile) → exit 0 vp test run src/server.test.ts → exit 0 (211 tests) vp test run threadIssues + orchestration (contracts) → exit 0 (68 tests) vp test run 10 server files → exit 1 (146/147) vp run fork:ci → exit 0 ``` The one failure is `ServerEnvironment.fork.test` on this machine: the local service launcher started a different t3 version. CI is the authority for it. ## Evidence A second client holds a live shell subscription on a dev server running a snapshot copy of real data. The driver speaks the WebSocket RPC the clients use. 1. Link issues 1430 and 1431 on a real thread 2. Live shell update carries both links 3. Reverse lookup returns the thread 4. Duplicate link: rejected, no event, no update 5. Unlink 1430: live update drops it 6. Read-only database: projection row and events 7. Unlink 1431: `issues` key gone  <details> <summary>Stills</summary>   </details> ## Close conditions Refs #1431 - [x] Replay: events rebuild the same projection - [x] Stale sync: older than linkedAt is ignored - [x] Duplicate: linking twice is a no-op - [x] Re-link: works after an unlink - [x] Authorization: only readable threads return - [x] Deletion: deleted thread leaves the lookup - [ ] Gate: no link control without capability | Condition | Evidence | | --- | --- | | Replay, re-link, deletion | "link, sync, unlink, re-link and delete agree across every projection" | | Stale sync | "both projectors drop a sync for a removed link, an earlier link, or an older read" | | Duplicate | decider rejects it, no event; state unchanged | | Authorization | orchestration read scope, beside the pull request lookup | | Gate | server advertises `threadIssues`; the control lands with #1432, so this PR refs, not closes | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the recording above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Squashes: - 9151ae2 feat(server): threads persist linked GitHub issues (#1440) (#1440) - 2822287 fixup! feat(server): threads persist linked GitHub issues (#1440) (#1584) (#1584) Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no
donjor
added a commit
that referenced
this pull request
Oct 4, 2026
## What Changed Agents link GitHub issues to their own thread over MCP, the way they link pull requests. | Tool | Does | | --- | --- | | `link_issue` | links by URL, or repository and number, as `agent` | | `unlink_issue` | removes a link; reports whether one existed | | `list_thread_issues` | the thread's links with last known state | - **Thread:** always the calling token's own thread - **Host default:** the project's host when only a number is given - **Repeat link:** `alreadyLinked`, no error - **Absent unlink:** `wasLinked: false`, no error - **Other rejections:** stay failures - **Deleted thread:** not found, never a no-op - **Stored URL:** canonical issue URL only - **Hosts:** github.com or the project's host - **Capability:** rides the pull request tools' gate - **Instruction:** one line beside the pull request linking line Upstream files change only by `fork-hook` lines in `McpHttpServer.ts` and `RuntimeInstructions.ts`. ## Why Agents already register the pull requests they open; the issues they work on stayed invisible to the thread. This is layer 3 of #1430, on the link model from #1440. ## Checks ```text vp fmt --check (changed files) → exit 0 vp lint (changed files) → exit 0 tsc --noEmit (apps/server) → exit 0 vp test run issues toolkit + instructions → exit 0 (13 tests) vp run fork:ci → exit 0 ``` ## Evidence A real Claude thread on this branch's dev server, told to use only its MCP tools: 1. `link_issue` the issue URL 2. `link_issue` it again 3. `list_thread_issues` 4. `unlink_issue` it 5. `unlink_issue` it again 6. `list_thread_issues`  ## Close conditions Closes #1433 - [x] link_issue: links by URL as agent - [x] unlink_issue: removes the link - [x] list_thread_issues: lists the thread's links - [x] Idempotent: a repeat link changes nothing - [x] Scope: a call reaches only its own thread - [x] Missing thread: fails, never "already linked" | Condition | Evidence | | --- | --- | | link_issue | "links by URL with source agent on the token's thread" | | unlink_issue | "unlinks a linked issue and reports a missing one as wasLinked=false" | | list_thread_issues | "lists the shell's links, with unknown state until the first read" | | Idempotent | "treats a duplicate link as alreadyLinked rather than an error" | | Scope | the thread comes from the invocation token, never the input | | Missing thread | "fails cleanly when the token's thread no longer exists"; "reports a thread deleted between lookup and dispatch as not found" | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the capture above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
pushed a commit
that referenced
this pull request
Oct 4, 2026
…, and palette (#1442) ## What Changed The web client links GitHub issues to threads from both sides, and **Work on this issue** links the issue at the draft's first send. | Entry point | Does | | --- | --- | | Thread issues panel | "Linked to this thread" list, **Link**, unlink | | Issue detail | "Linked threads" list, open, unlink, **Link to a thread** | | Issue actions menu | link or unlink the active thread | | Command palette | "Link GitHub issue to thread" | | Work on this issue | links as `handoff` after the turn starts | | Rule | Behavior | | --- | --- | | Handoff store | keyed by draft, in `localStorage` | | Taken | only after a turn start succeeds | | Multi-model send | links every started thread | | Failed send | entry kept for the retry | | Ask, Explain | mention the issue, never record it | | Gate | every control needs `threadIssues` | | Keybinding | none by default | | Title, state | unknown until #1434 | Upstream files change only by `fork-hook` lines in `ChatView.tsx` and `CommandPalette.tsx`. ## Why **Work on this issue** only seeded a draft, so no thread existed to link until the first send. This is layer 2 of #1430, on the link model from #1440. ## Checks ```text tsc --noEmit (apps/web) → exit 0 tsc --noEmit (packages/client-runtime) → exit 0 vp test run githubIssue + CommandPalette → exit 0 (13 files, 95 tests) vp run knip:check → exit 0 vp run fork:ci → exit 0 ``` ## Evidence This branch's dev server on a copy of real data, in Chrome. ### Thread side 1. Empty panel  2. **Link** dialog resolves a bare number  3. Linked; state unknown until a read  ### Issue side 4. The same link under "Linked threads"  5. After unlink from the issue  ### Palette  ### Handoff **Work on this issue** on this issue, then one send; the new thread links it.  Stored row: `source: handoff`, number 1432; the handoff entry was consumed. ## Close conditions Closes #1432 Closes #1431 ### Handoff - [x] **First send:** links the issue as `handoff` - [x] **Draft reuse:** the link survives - [x] **Promotion:** the link survives - [x] **Background send:** links the issue - [x] **Failed send:** a retry links it once ### Surfaces - [x] **Issue panel:** lists and opens linked threads - [x] **Entry points:** issue panel, thread, palette - [x] **Ask, Explain:** create no link ### Access (#1431) - [x] **Gate:** no link control without capability | Condition | Evidence | | --- | --- | | First send | capture 7 and the stored row; "links the draft's issue as a handoff at the first send, once" | | Draft reuse | "survives a reload and a reused draft, where the latest issue wins" | | Promotion | the entry is keyed by draft and taken after the promoted thread's turn starts | | Background send | the link runs on the shared path after `startPromise`, foreground or background | | Failed send | "keeps the issue through a failed send so the retry on a re-minted thread links it" | | Issue panel | captures 4 and 5 | | Entry points | captures 2, 4, and 6 | | Ask, Explain | only **Work on this issue** records a handoff | | Gate | panel, dialog, menu, palette, and handoff each check `threadIssues` | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes Capture 1 is the before state; no motion changed. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
pushed a commit
that referenced
this pull request
Oct 4, 2026
## What Changed A thread stores its linked GitHub issues as orchestration state, modeled on its pull request links. ### Model - **Link:** host, repository, number, url - **Record:** source (`manual`, `handoff`, `agent`), `linkedAt` - **Snapshot:** title, state, `syncedAt` - **Commands:** `thread.issue.link`, `thread.issue.unlink` - **Sync:** internal `thread.issue-link.sync` - **Capability:** `threadIssues`, missing means unsupported - **Reverse lookup:** RPC for the threads on one issue ### Storage and reads - **Table:** idempotent `ForkSchema.ts` pass, never a numbered migration - **Thread and shell:** optional `issues`, omitted when empty - **Live:** shell subscribers refetch on a link change - **Detail stream:** carries no issue events - **Sync order:** an older snapshot never wins - **Deletion:** a re-created thread has no links Upstream files change only by one-line `fork-hook` lines; logic lives in `*.fork.ts`. ## Why Threads link pull requests but not the issues they work on. This is layer 1 of #1430: the web controls, MCP tools, sync and mobile build on it. ### Version skew | Client | Server | Result | | --- | --- | --- | | Old | New | decodes thread, shell and every stream item | | New | Old | missing `issues` reads as none; capability off | A server from before this change cannot reopen a database holding link events; `fork-delta.md` records that downgrade limit. ## Checks ```text tsc --noEmit (contracts, server, web, client-runtime, mobile) → exit 0 vp test run src/server.test.ts → exit 0 (211 tests) vp test run threadIssues + orchestration (contracts) → exit 0 (68 tests) vp test run 10 server files → exit 1 (146/147) vp run fork:ci → exit 0 ``` The one failure is `ServerEnvironment.fork.test` on this machine: the local service launcher started a different t3 version. CI is the authority for it. ## Evidence A second client holds a live shell subscription on a dev server running a snapshot copy of real data. The driver speaks the WebSocket RPC the clients use. 1. Link issues 1430 and 1431 on a real thread 2. Live shell update carries both links 3. Reverse lookup returns the thread 4. Duplicate link: rejected, no event, no update 5. Unlink 1430: live update drops it 6. Read-only database: projection row and events 7. Unlink 1431: `issues` key gone  <details> <summary>Stills</summary>   </details> ## Close conditions Refs #1431 - [x] Replay: events rebuild the same projection - [x] Stale sync: older than linkedAt is ignored - [x] Duplicate: linking twice is a no-op - [x] Re-link: works after an unlink - [x] Authorization: only readable threads return - [x] Deletion: deleted thread leaves the lookup - [ ] Gate: no link control without capability | Condition | Evidence | | --- | --- | | Replay, re-link, deletion | "link, sync, unlink, re-link and delete agree across every projection" | | Stale sync | "both projectors drop a sync for a removed link, an earlier link, or an older read" | | Duplicate | decider rejects it, no event; state unchanged | | Authorization | orchestration read scope, beside the pull request lookup | | Gate | server advertises `threadIssues`; the control lands with #1432, so this PR refs, not closes | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the recording above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Squashes: - b06fe75 feat(server): threads persist linked GitHub issues (#1440) (#1440) (#1584) - b4d8082 feat(issues): threads keep linked issues on Orchestrator V2 thread state - 0530808 feat(issues): the legacy importer carries V1 issue links onto V2 threads Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no
donjor
added a commit
that referenced
this pull request
Oct 4, 2026
## What Changed Agents link GitHub issues to their own thread over MCP, the way they link pull requests. | Tool | Does | | --- | --- | | `link_issue` | links by URL, or repository and number, as `agent` | | `unlink_issue` | removes a link; reports whether one existed | | `list_thread_issues` | the thread's links with last known state | - **Thread:** always the calling token's own thread - **Host default:** the project's host when only a number is given - **Repeat link:** `alreadyLinked`, no error - **Absent unlink:** `wasLinked: false`, no error - **Other rejections:** stay failures - **Deleted thread:** not found, never a no-op - **Stored URL:** canonical issue URL only - **Hosts:** github.com or the project's host - **Capability:** rides the pull request tools' gate - **Instruction:** one line beside the pull request linking line Upstream files change only by `fork-hook` lines in `McpHttpServer.ts` and `RuntimeInstructions.ts`. ## Why Agents already register the pull requests they open; the issues they work on stayed invisible to the thread. This is layer 3 of #1430, on the link model from #1440. ## Checks ```text vp fmt --check (changed files) → exit 0 vp lint (changed files) → exit 0 tsc --noEmit (apps/server) → exit 0 vp test run issues toolkit + instructions → exit 0 (13 tests) vp run fork:ci → exit 0 ``` ## Evidence A real Claude thread on this branch's dev server, told to use only its MCP tools: 1. `link_issue` the issue URL 2. `link_issue` it again 3. `list_thread_issues` 4. `unlink_issue` it 5. `unlink_issue` it again 6. `list_thread_issues`  ## Close conditions Closes #1433 - [x] link_issue: links by URL as agent - [x] unlink_issue: removes the link - [x] list_thread_issues: lists the thread's links - [x] Idempotent: a repeat link changes nothing - [x] Scope: a call reaches only its own thread - [x] Missing thread: fails, never "already linked" | Condition | Evidence | | --- | --- | | link_issue | "links by URL with source agent on the token's thread" | | unlink_issue | "unlinks a linked issue and reports a missing one as wasLinked=false" | | list_thread_issues | "lists the shell's links, with unknown state until the first read" | | Idempotent | "treats a duplicate link as alreadyLinked rather than an error" | | Scope | the thread comes from the invocation token, never the input | | Missing thread | "fails cleanly when the token's thread no longer exists"; "reports a thread deleted between lookup and dispatch as not found" | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the capture above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Oct 4, 2026
…, and palette (#1442) ## What Changed The web client links GitHub issues to threads from both sides, and **Work on this issue** links the issue at the draft's first send. | Entry point | Does | | --- | --- | | Thread issues panel | "Linked to this thread" list, **Link**, unlink | | Issue detail | "Linked threads" list, open, unlink, **Link to a thread** | | Issue actions menu | link or unlink the active thread | | Command palette | "Link GitHub issue to thread" | | Work on this issue | links as `handoff` after the turn starts | | Rule | Behavior | | --- | --- | | Handoff store | keyed by draft, in `localStorage` | | Taken | only after a turn start succeeds | | Multi-model send | links every started thread | | Failed send | entry kept for the retry | | Ask, Explain | mention the issue, never record it | | Gate | every control needs `threadIssues` | | Keybinding | none by default | | Title, state | unknown until #1434 | Upstream files change only by `fork-hook` lines in `ChatView.tsx` and `CommandPalette.tsx`. ## Why **Work on this issue** only seeded a draft, so no thread existed to link until the first send. This is layer 2 of #1430, on the link model from #1440. ## Checks ```text tsc --noEmit (apps/web) → exit 0 tsc --noEmit (packages/client-runtime) → exit 0 vp test run githubIssue + CommandPalette → exit 0 (13 files, 95 tests) vp run knip:check → exit 0 vp run fork:ci → exit 0 ``` ## Evidence This branch's dev server on a copy of real data, in Chrome. ### Thread side 1. Empty panel  2. **Link** dialog resolves a bare number  3. Linked; state unknown until a read  ### Issue side 4. The same link under "Linked threads"  5. After unlink from the issue  ### Palette  ### Handoff **Work on this issue** on this issue, then one send; the new thread links it.  Stored row: `source: handoff`, number 1432; the handoff entry was consumed. ## Close conditions Closes #1432 Closes #1431 ### Handoff - [x] **First send:** links the issue as `handoff` - [x] **Draft reuse:** the link survives - [x] **Promotion:** the link survives - [x] **Background send:** links the issue - [x] **Failed send:** a retry links it once ### Surfaces - [x] **Issue panel:** lists and opens linked threads - [x] **Entry points:** issue panel, thread, palette - [x] **Ask, Explain:** create no link ### Access (#1431) - [x] **Gate:** no link control without capability | Condition | Evidence | | --- | --- | | First send | capture 7 and the stored row; "links the draft's issue as a handoff at the first send, once" | | Draft reuse | "survives a reload and a reused draft, where the latest issue wins" | | Promotion | the entry is keyed by draft and taken after the promoted thread's turn starts | | Background send | the link runs on the shared path after `startPromise`, foreground or background | | Failed send | "keeps the issue through a failed send so the retry on a re-minted thread links it" | | Issue panel | captures 4 and 5 | | Entry points | captures 2, 4, and 6 | | Ask, Explain | only **Work on this issue** records a handoff | | Gate | panel, dialog, menu, palette, and handoff each check `threadIssues` | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes Capture 1 is the before state; no motion changed. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
pushed a commit
that referenced
this pull request
Oct 4, 2026
## What Changed A thread stores its linked GitHub issues as orchestration state, modeled on its pull request links. ### Model - **Link:** host, repository, number, url - **Record:** source (`manual`, `handoff`, `agent`), `linkedAt` - **Snapshot:** title, state, `syncedAt` - **Commands:** `thread.issue.link`, `thread.issue.unlink` - **Sync:** internal `thread.issue-link.sync` - **Capability:** `threadIssues`, missing means unsupported - **Reverse lookup:** RPC for the threads on one issue ### Storage and reads - **Table:** idempotent `ForkSchema.ts` pass, never a numbered migration - **Thread and shell:** optional `issues`, omitted when empty - **Live:** shell subscribers refetch on a link change - **Detail stream:** carries no issue events - **Sync order:** an older snapshot never wins - **Deletion:** a re-created thread has no links Upstream files change only by one-line `fork-hook` lines; logic lives in `*.fork.ts`. ## Why Threads link pull requests but not the issues they work on. This is layer 1 of #1430: the web controls, MCP tools, sync and mobile build on it. ### Version skew | Client | Server | Result | | --- | --- | --- | | Old | New | decodes thread, shell and every stream item | | New | Old | missing `issues` reads as none; capability off | A server from before this change cannot reopen a database holding link events; `fork-delta.md` records that downgrade limit. ## Checks ```text tsc --noEmit (contracts, server, web, client-runtime, mobile) → exit 0 vp test run src/server.test.ts → exit 0 (211 tests) vp test run threadIssues + orchestration (contracts) → exit 0 (68 tests) vp test run 10 server files → exit 1 (146/147) vp run fork:ci → exit 0 ``` The one failure is `ServerEnvironment.fork.test` on this machine: the local service launcher started a different t3 version. CI is the authority for it. ## Evidence A second client holds a live shell subscription on a dev server running a snapshot copy of real data. The driver speaks the WebSocket RPC the clients use. 1. Link issues 1430 and 1431 on a real thread 2. Live shell update carries both links 3. Reverse lookup returns the thread 4. Duplicate link: rejected, no event, no update 5. Unlink 1430: live update drops it 6. Read-only database: projection row and events 7. Unlink 1431: `issues` key gone  <details> <summary>Stills</summary>   </details> ## Close conditions Refs #1431 - [x] Replay: events rebuild the same projection - [x] Stale sync: older than linkedAt is ignored - [x] Duplicate: linking twice is a no-op - [x] Re-link: works after an unlink - [x] Authorization: only readable threads return - [x] Deletion: deleted thread leaves the lookup - [ ] Gate: no link control without capability | Condition | Evidence | | --- | --- | | Replay, re-link, deletion | "link, sync, unlink, re-link and delete agree across every projection" | | Stale sync | "both projectors drop a sync for a removed link, an earlier link, or an older read" | | Duplicate | decider rejects it, no event; state unchanged | | Authorization | orchestration read scope, beside the pull request lookup | | Gate | server advertises `threadIssues`; the control lands with #1432, so this PR refs, not closes | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the recording above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Squashes: - b06fe75 feat(server): threads persist linked GitHub issues (#1440) (#1440) (#1584) - b4d8082 feat(issues): threads keep linked issues on Orchestrator V2 thread state - 0530808 feat(issues): the legacy importer carries V1 issue links onto V2 threads Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no
donjor
added a commit
that referenced
this pull request
Oct 4, 2026
## What Changed Agents link GitHub issues to their own thread over MCP, the way they link pull requests. | Tool | Does | | --- | --- | | `link_issue` | links by URL, or repository and number, as `agent` | | `unlink_issue` | removes a link; reports whether one existed | | `list_thread_issues` | the thread's links with last known state | - **Thread:** always the calling token's own thread - **Host default:** the project's host when only a number is given - **Repeat link:** `alreadyLinked`, no error - **Absent unlink:** `wasLinked: false`, no error - **Other rejections:** stay failures - **Deleted thread:** not found, never a no-op - **Stored URL:** canonical issue URL only - **Hosts:** github.com or the project's host - **Capability:** rides the pull request tools' gate - **Instruction:** one line beside the pull request linking line Upstream files change only by `fork-hook` lines in `McpHttpServer.ts` and `RuntimeInstructions.ts`. ## Why Agents already register the pull requests they open; the issues they work on stayed invisible to the thread. This is layer 3 of #1430, on the link model from #1440. ## Checks ```text vp fmt --check (changed files) → exit 0 vp lint (changed files) → exit 0 tsc --noEmit (apps/server) → exit 0 vp test run issues toolkit + instructions → exit 0 (13 tests) vp run fork:ci → exit 0 ``` ## Evidence A real Claude thread on this branch's dev server, told to use only its MCP tools: 1. `link_issue` the issue URL 2. `link_issue` it again 3. `list_thread_issues` 4. `unlink_issue` it 5. `unlink_issue` it again 6. `list_thread_issues`  ## Close conditions Closes #1433 - [x] link_issue: links by URL as agent - [x] unlink_issue: removes the link - [x] list_thread_issues: lists the thread's links - [x] Idempotent: a repeat link changes nothing - [x] Scope: a call reaches only its own thread - [x] Missing thread: fails, never "already linked" | Condition | Evidence | | --- | --- | | link_issue | "links by URL with source agent on the token's thread" | | unlink_issue | "unlinks a linked issue and reports a missing one as wasLinked=false" | | list_thread_issues | "lists the shell's links, with unknown state until the first read" | | Idempotent | "treats a duplicate link as alreadyLinked rather than an error" | | Scope | the thread comes from the invocation token, never the input | | Missing thread | "fails cleanly when the token's thread no longer exists"; "reports a thread deleted between lookup and dispatch as not found" | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the capture above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Oct 4, 2026
…, and palette (#1442) ## What Changed The web client links GitHub issues to threads from both sides, and **Work on this issue** links the issue at the draft's first send. | Entry point | Does | | --- | --- | | Thread issues panel | "Linked to this thread" list, **Link**, unlink | | Issue detail | "Linked threads" list, open, unlink, **Link to a thread** | | Issue actions menu | link or unlink the active thread | | Command palette | "Link GitHub issue to thread" | | Work on this issue | links as `handoff` after the turn starts | | Rule | Behavior | | --- | --- | | Handoff store | keyed by draft, in `localStorage` | | Taken | only after a turn start succeeds | | Multi-model send | links every started thread | | Failed send | entry kept for the retry | | Ask, Explain | mention the issue, never record it | | Gate | every control needs `threadIssues` | | Keybinding | none by default | | Title, state | unknown until #1434 | Upstream files change only by `fork-hook` lines in `ChatView.tsx` and `CommandPalette.tsx`. ## Why **Work on this issue** only seeded a draft, so no thread existed to link until the first send. This is layer 2 of #1430, on the link model from #1440. ## Checks ```text tsc --noEmit (apps/web) → exit 0 tsc --noEmit (packages/client-runtime) → exit 0 vp test run githubIssue + CommandPalette → exit 0 (13 files, 95 tests) vp run knip:check → exit 0 vp run fork:ci → exit 0 ``` ## Evidence This branch's dev server on a copy of real data, in Chrome. ### Thread side 1. Empty panel  2. **Link** dialog resolves a bare number  3. Linked; state unknown until a read  ### Issue side 4. The same link under "Linked threads"  5. After unlink from the issue  ### Palette  ### Handoff **Work on this issue** on this issue, then one send; the new thread links it.  Stored row: `source: handoff`, number 1432; the handoff entry was consumed. ## Close conditions Closes #1432 Closes #1431 ### Handoff - [x] **First send:** links the issue as `handoff` - [x] **Draft reuse:** the link survives - [x] **Promotion:** the link survives - [x] **Background send:** links the issue - [x] **Failed send:** a retry links it once ### Surfaces - [x] **Issue panel:** lists and opens linked threads - [x] **Entry points:** issue panel, thread, palette - [x] **Ask, Explain:** create no link ### Access (#1431) - [x] **Gate:** no link control without capability | Condition | Evidence | | --- | --- | | First send | capture 7 and the stored row; "links the draft's issue as a handoff at the first send, once" | | Draft reuse | "survives a reload and a reused draft, where the latest issue wins" | | Promotion | the entry is keyed by draft and taken after the promoted thread's turn starts | | Background send | the link runs on the shared path after `startPromise`, foreground or background | | Failed send | "keeps the issue through a failed send so the retry on a re-minted thread links it" | | Issue panel | captures 4 and 5 | | Entry points | captures 2, 4, and 6 | | Ask, Explain | only **Work on this issue** records a handoff | | Gate | panel, dialog, menu, palette, and handoff each check `threadIssues` | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes Capture 1 is the before state; no motion changed. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
pushed a commit
that referenced
this pull request
Oct 4, 2026
## What Changed A thread stores its linked GitHub issues as orchestration state, modeled on its pull request links. ### Model - **Link:** host, repository, number, url - **Record:** source (`manual`, `handoff`, `agent`), `linkedAt` - **Snapshot:** title, state, `syncedAt` - **Commands:** `thread.issue.link`, `thread.issue.unlink` - **Sync:** internal `thread.issue-link.sync` - **Capability:** `threadIssues`, missing means unsupported - **Reverse lookup:** RPC for the threads on one issue ### Storage and reads - **Table:** idempotent `ForkSchema.ts` pass, never a numbered migration - **Thread and shell:** optional `issues`, omitted when empty - **Live:** shell subscribers refetch on a link change - **Detail stream:** carries no issue events - **Sync order:** an older snapshot never wins - **Deletion:** a re-created thread has no links Upstream files change only by one-line `fork-hook` lines; logic lives in `*.fork.ts`. ## Why Threads link pull requests but not the issues they work on. This is layer 1 of #1430: the web controls, MCP tools, sync and mobile build on it. ### Version skew | Client | Server | Result | | --- | --- | --- | | Old | New | decodes thread, shell and every stream item | | New | Old | missing `issues` reads as none; capability off | A server from before this change cannot reopen a database holding link events; `fork-delta.md` records that downgrade limit. ## Checks ```text tsc --noEmit (contracts, server, web, client-runtime, mobile) → exit 0 vp test run src/server.test.ts → exit 0 (211 tests) vp test run threadIssues + orchestration (contracts) → exit 0 (68 tests) vp test run 10 server files → exit 1 (146/147) vp run fork:ci → exit 0 ``` The one failure is `ServerEnvironment.fork.test` on this machine: the local service launcher started a different t3 version. CI is the authority for it. ## Evidence A second client holds a live shell subscription on a dev server running a snapshot copy of real data. The driver speaks the WebSocket RPC the clients use. 1. Link issues 1430 and 1431 on a real thread 2. Live shell update carries both links 3. Reverse lookup returns the thread 4. Duplicate link: rejected, no event, no update 5. Unlink 1430: live update drops it 6. Read-only database: projection row and events 7. Unlink 1431: `issues` key gone  <details> <summary>Stills</summary>   </details> ## Close conditions Refs #1431 - [x] Replay: events rebuild the same projection - [x] Stale sync: older than linkedAt is ignored - [x] Duplicate: linking twice is a no-op - [x] Re-link: works after an unlink - [x] Authorization: only readable threads return - [x] Deletion: deleted thread leaves the lookup - [ ] Gate: no link control without capability | Condition | Evidence | | --- | --- | | Replay, re-link, deletion | "link, sync, unlink, re-link and delete agree across every projection" | | Stale sync | "both projectors drop a sync for a removed link, an earlier link, or an older read" | | Duplicate | decider rejects it, no event; state unchanged | | Authorization | orchestration read scope, beside the pull request lookup | | Gate | server advertises `threadIssues`; the control lands with #1432, so this PR refs, not closes | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the recording above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Squashes: - b06fe75 feat(server): threads persist linked GitHub issues (#1440) (#1440) (#1584) - b4d8082 feat(issues): threads keep linked issues on Orchestrator V2 thread state - 0530808 feat(issues): the legacy importer carries V1 issue links onto V2 threads Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no
donjor
added a commit
that referenced
this pull request
Oct 4, 2026
## What Changed Agents link GitHub issues to their own thread over MCP, the way they link pull requests. | Tool | Does | | --- | --- | | `link_issue` | links by URL, or repository and number, as `agent` | | `unlink_issue` | removes a link; reports whether one existed | | `list_thread_issues` | the thread's links with last known state | - **Thread:** always the calling token's own thread - **Host default:** the project's host when only a number is given - **Repeat link:** `alreadyLinked`, no error - **Absent unlink:** `wasLinked: false`, no error - **Other rejections:** stay failures - **Deleted thread:** not found, never a no-op - **Stored URL:** canonical issue URL only - **Hosts:** github.com or the project's host - **Capability:** rides the pull request tools' gate - **Instruction:** one line beside the pull request linking line Upstream files change only by `fork-hook` lines in `McpHttpServer.ts` and `RuntimeInstructions.ts`. ## Why Agents already register the pull requests they open; the issues they work on stayed invisible to the thread. This is layer 3 of #1430, on the link model from #1440. ## Checks ```text vp fmt --check (changed files) → exit 0 vp lint (changed files) → exit 0 tsc --noEmit (apps/server) → exit 0 vp test run issues toolkit + instructions → exit 0 (13 tests) vp run fork:ci → exit 0 ``` ## Evidence A real Claude thread on this branch's dev server, told to use only its MCP tools: 1. `link_issue` the issue URL 2. `link_issue` it again 3. `list_thread_issues` 4. `unlink_issue` it 5. `unlink_issue` it again 6. `list_thread_issues`  ## Close conditions Closes #1433 - [x] link_issue: links by URL as agent - [x] unlink_issue: removes the link - [x] list_thread_issues: lists the thread's links - [x] Idempotent: a repeat link changes nothing - [x] Scope: a call reaches only its own thread - [x] Missing thread: fails, never "already linked" | Condition | Evidence | | --- | --- | | link_issue | "links by URL with source agent on the token's thread" | | unlink_issue | "unlinks a linked issue and reports a missing one as wasLinked=false" | | list_thread_issues | "lists the shell's links, with unknown state until the first read" | | Idempotent | "treats a duplicate link as alreadyLinked rather than an error" | | Scope | the thread comes from the invocation token, never the input | | Missing thread | "fails cleanly when the token's thread no longer exists"; "reports a thread deleted between lookup and dispatch as not found" | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the capture above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Oct 4, 2026
…, and palette (#1442) ## What Changed The web client links GitHub issues to threads from both sides, and **Work on this issue** links the issue at the draft's first send. | Entry point | Does | | --- | --- | | Thread issues panel | "Linked to this thread" list, **Link**, unlink | | Issue detail | "Linked threads" list, open, unlink, **Link to a thread** | | Issue actions menu | link or unlink the active thread | | Command palette | "Link GitHub issue to thread" | | Work on this issue | links as `handoff` after the turn starts | | Rule | Behavior | | --- | --- | | Handoff store | keyed by draft, in `localStorage` | | Taken | only after a turn start succeeds | | Multi-model send | links every started thread | | Failed send | entry kept for the retry | | Ask, Explain | mention the issue, never record it | | Gate | every control needs `threadIssues` | | Keybinding | none by default | | Title, state | unknown until #1434 | Upstream files change only by `fork-hook` lines in `ChatView.tsx` and `CommandPalette.tsx`. ## Why **Work on this issue** only seeded a draft, so no thread existed to link until the first send. This is layer 2 of #1430, on the link model from #1440. ## Checks ```text tsc --noEmit (apps/web) → exit 0 tsc --noEmit (packages/client-runtime) → exit 0 vp test run githubIssue + CommandPalette → exit 0 (13 files, 95 tests) vp run knip:check → exit 0 vp run fork:ci → exit 0 ``` ## Evidence This branch's dev server on a copy of real data, in Chrome. ### Thread side 1. Empty panel  2. **Link** dialog resolves a bare number  3. Linked; state unknown until a read  ### Issue side 4. The same link under "Linked threads"  5. After unlink from the issue  ### Palette  ### Handoff **Work on this issue** on this issue, then one send; the new thread links it.  Stored row: `source: handoff`, number 1432; the handoff entry was consumed. ## Close conditions Closes #1432 Closes #1431 ### Handoff - [x] **First send:** links the issue as `handoff` - [x] **Draft reuse:** the link survives - [x] **Promotion:** the link survives - [x] **Background send:** links the issue - [x] **Failed send:** a retry links it once ### Surfaces - [x] **Issue panel:** lists and opens linked threads - [x] **Entry points:** issue panel, thread, palette - [x] **Ask, Explain:** create no link ### Access (#1431) - [x] **Gate:** no link control without capability | Condition | Evidence | | --- | --- | | First send | capture 7 and the stored row; "links the draft's issue as a handoff at the first send, once" | | Draft reuse | "survives a reload and a reused draft, where the latest issue wins" | | Promotion | the entry is keyed by draft and taken after the promoted thread's turn starts | | Background send | the link runs on the shared path after `startPromise`, foreground or background | | Failed send | "keeps the issue through a failed send so the retry on a re-minted thread links it" | | Issue panel | captures 4 and 5 | | Entry points | captures 2, 4, and 6 | | Ask, Explain | only **Work on this issue** records a handoff | | Gate | panel, dialog, menu, palette, and handoff each check `threadIssues` | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes Capture 1 is the before state; no motion changed. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
pushed a commit
that referenced
this pull request
Oct 6, 2026
## What Changed A thread stores its linked GitHub issues as orchestration state, modeled on its pull request links. ### Model - **Link:** host, repository, number, url - **Record:** source (`manual`, `handoff`, `agent`), `linkedAt` - **Snapshot:** title, state, `syncedAt` - **Commands:** `thread.issue.link`, `thread.issue.unlink` - **Sync:** internal `thread.issue-link.sync` - **Capability:** `threadIssues`, missing means unsupported - **Reverse lookup:** RPC for the threads on one issue ### Storage and reads - **Table:** idempotent `ForkSchema.ts` pass, never a numbered migration - **Thread and shell:** optional `issues`, omitted when empty - **Live:** shell subscribers refetch on a link change - **Detail stream:** carries no issue events - **Sync order:** an older snapshot never wins - **Deletion:** a re-created thread has no links Upstream files change only by one-line `fork-hook` lines; logic lives in `*.fork.ts`. ## Why Threads link pull requests but not the issues they work on. This is layer 1 of #1430: the web controls, MCP tools, sync and mobile build on it. ### Version skew | Client | Server | Result | | --- | --- | --- | | Old | New | decodes thread, shell and every stream item | | New | Old | missing `issues` reads as none; capability off | A server from before this change cannot reopen a database holding link events; `fork-delta.md` records that downgrade limit. ## Checks ```text tsc --noEmit (contracts, server, web, client-runtime, mobile) → exit 0 vp test run src/server.test.ts → exit 0 (211 tests) vp test run threadIssues + orchestration (contracts) → exit 0 (68 tests) vp test run 10 server files → exit 1 (146/147) vp run fork:ci → exit 0 ``` The one failure is `ServerEnvironment.fork.test` on this machine: the local service launcher started a different t3 version. CI is the authority for it. ## Evidence A second client holds a live shell subscription on a dev server running a snapshot copy of real data. The driver speaks the WebSocket RPC the clients use. 1. Link issues 1430 and 1431 on a real thread 2. Live shell update carries both links 3. Reverse lookup returns the thread 4. Duplicate link: rejected, no event, no update 5. Unlink 1430: live update drops it 6. Read-only database: projection row and events 7. Unlink 1431: `issues` key gone  <details> <summary>Stills</summary>   </details> ## Close conditions Refs #1431 - [x] Replay: events rebuild the same projection - [x] Stale sync: older than linkedAt is ignored - [x] Duplicate: linking twice is a no-op - [x] Re-link: works after an unlink - [x] Authorization: only readable threads return - [x] Deletion: deleted thread leaves the lookup - [ ] Gate: no link control without capability | Condition | Evidence | | --- | --- | | Replay, re-link, deletion | "link, sync, unlink, re-link and delete agree across every projection" | | Stale sync | "both projectors drop a sync for a removed link, an earlier link, or an older read" | | Duplicate | decider rejects it, no event; state unchanged | | Authorization | orchestration read scope, beside the pull request lookup | | Gate | server advertises `threadIssues`; the control lands with #1432, so this PR refs, not closes | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the recording above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Squashes: - b06fe75 feat(server): threads persist linked GitHub issues (#1440) (#1440) (#1584) - b4d8082 feat(issues): threads keep linked issues on Orchestrator V2 thread state - 0530808 feat(issues): the legacy importer carries V1 issue links onto V2 threads Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no
donjor
added a commit
that referenced
this pull request
Oct 6, 2026
## What Changed Agents link GitHub issues to their own thread over MCP, the way they link pull requests. | Tool | Does | | --- | --- | | `link_issue` | links by URL, or repository and number, as `agent` | | `unlink_issue` | removes a link; reports whether one existed | | `list_thread_issues` | the thread's links with last known state | - **Thread:** always the calling token's own thread - **Host default:** the project's host when only a number is given - **Repeat link:** `alreadyLinked`, no error - **Absent unlink:** `wasLinked: false`, no error - **Other rejections:** stay failures - **Deleted thread:** not found, never a no-op - **Stored URL:** canonical issue URL only - **Hosts:** github.com or the project's host - **Capability:** rides the pull request tools' gate - **Instruction:** one line beside the pull request linking line Upstream files change only by `fork-hook` lines in `McpHttpServer.ts` and `RuntimeInstructions.ts`. ## Why Agents already register the pull requests they open; the issues they work on stayed invisible to the thread. This is layer 3 of #1430, on the link model from #1440. ## Checks ```text vp fmt --check (changed files) → exit 0 vp lint (changed files) → exit 0 tsc --noEmit (apps/server) → exit 0 vp test run issues toolkit + instructions → exit 0 (13 tests) vp run fork:ci → exit 0 ``` ## Evidence A real Claude thread on this branch's dev server, told to use only its MCP tools: 1. `link_issue` the issue URL 2. `link_issue` it again 3. `list_thread_issues` 4. `unlink_issue` it 5. `unlink_issue` it again 6. `list_thread_issues`  ## Close conditions Closes #1433 - [x] link_issue: links by URL as agent - [x] unlink_issue: removes the link - [x] list_thread_issues: lists the thread's links - [x] Idempotent: a repeat link changes nothing - [x] Scope: a call reaches only its own thread - [x] Missing thread: fails, never "already linked" | Condition | Evidence | | --- | --- | | link_issue | "links by URL with source agent on the token's thread" | | unlink_issue | "unlinks a linked issue and reports a missing one as wasLinked=false" | | list_thread_issues | "lists the shell's links, with unknown state until the first read" | | Idempotent | "treats a duplicate link as alreadyLinked rather than an error" | | Scope | the thread comes from the invocation token, never the input | | Missing thread | "fails cleanly when the token's thread no longer exists"; "reports a thread deleted between lookup and dispatch as not found" | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the capture above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
added a commit
that referenced
this pull request
Oct 6, 2026
…, and palette (#1442) ## What Changed The web client links GitHub issues to threads from both sides, and **Work on this issue** links the issue at the draft's first send. | Entry point | Does | | --- | --- | | Thread issues panel | "Linked to this thread" list, **Link**, unlink | | Issue detail | "Linked threads" list, open, unlink, **Link to a thread** | | Issue actions menu | link or unlink the active thread | | Command palette | "Link GitHub issue to thread" | | Work on this issue | links as `handoff` after the turn starts | | Rule | Behavior | | --- | --- | | Handoff store | keyed by draft, in `localStorage` | | Taken | only after a turn start succeeds | | Multi-model send | links every started thread | | Failed send | entry kept for the retry | | Ask, Explain | mention the issue, never record it | | Gate | every control needs `threadIssues` | | Keybinding | none by default | | Title, state | unknown until #1434 | Upstream files change only by `fork-hook` lines in `ChatView.tsx` and `CommandPalette.tsx`. ## Why **Work on this issue** only seeded a draft, so no thread existed to link until the first send. This is layer 2 of #1430, on the link model from #1440. ## Checks ```text tsc --noEmit (apps/web) → exit 0 tsc --noEmit (packages/client-runtime) → exit 0 vp test run githubIssue + CommandPalette → exit 0 (13 files, 95 tests) vp run knip:check → exit 0 vp run fork:ci → exit 0 ``` ## Evidence This branch's dev server on a copy of real data, in Chrome. ### Thread side 1. Empty panel  2. **Link** dialog resolves a bare number  3. Linked; state unknown until a read  ### Issue side 4. The same link under "Linked threads"  5. After unlink from the issue  ### Palette  ### Handoff **Work on this issue** on this issue, then one send; the new thread links it.  Stored row: `source: handoff`, number 1432; the handoff entry was consumed. ## Close conditions Closes #1432 Closes #1431 ### Handoff - [x] **First send:** links the issue as `handoff` - [x] **Draft reuse:** the link survives - [x] **Promotion:** the link survives - [x] **Background send:** links the issue - [x] **Failed send:** a retry links it once ### Surfaces - [x] **Issue panel:** lists and opens linked threads - [x] **Entry points:** issue panel, thread, palette - [x] **Ask, Explain:** create no link ### Access (#1431) - [x] **Gate:** no link control without capability | Condition | Evidence | | --- | --- | | First send | capture 7 and the stored row; "links the draft's issue as a handoff at the first send, once" | | Draft reuse | "survives a reload and a reused draft, where the latest issue wins" | | Promotion | the entry is keyed by draft and taken after the promoted thread's turn starts | | Background send | the link runs on the shared path after `startPromise`, foreground or background | | Failed send | "keeps the issue through a failed send so the retry on a re-minted thread links it" | | Issue panel | captures 4 and 5 | | Entry points | captures 2, 4, and 6 | | Ask, Explain | only **Work on this issue** records a handoff | | Gate | panel, dialog, menu, palette, and handoff each check `threadIssues` | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes Capture 1 is the before state; no motion changed. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
pushed a commit
that referenced
this pull request
Oct 6, 2026
## What Changed A thread stores its linked GitHub issues as orchestration state, modeled on its pull request links. ### Model - **Link:** host, repository, number, url - **Record:** source (`manual`, `handoff`, `agent`), `linkedAt` - **Snapshot:** title, state, `syncedAt` - **Commands:** `thread.issue.link`, `thread.issue.unlink` - **Sync:** internal `thread.issue-link.sync` - **Capability:** `threadIssues`, missing means unsupported - **Reverse lookup:** RPC for the threads on one issue ### Storage and reads - **Table:** idempotent `ForkSchema.ts` pass, never a numbered migration - **Thread and shell:** optional `issues`, omitted when empty - **Live:** shell subscribers refetch on a link change - **Detail stream:** carries no issue events - **Sync order:** an older snapshot never wins - **Deletion:** a re-created thread has no links Upstream files change only by one-line `fork-hook` lines; logic lives in `*.fork.ts`. ## Why Threads link pull requests but not the issues they work on. This is layer 1 of #1430: the web controls, MCP tools, sync and mobile build on it. ### Version skew | Client | Server | Result | | --- | --- | --- | | Old | New | decodes thread, shell and every stream item | | New | Old | missing `issues` reads as none; capability off | A server from before this change cannot reopen a database holding link events; `fork-delta.md` records that downgrade limit. ## Checks ```text tsc --noEmit (contracts, server, web, client-runtime, mobile) → exit 0 vp test run src/server.test.ts → exit 0 (211 tests) vp test run threadIssues + orchestration (contracts) → exit 0 (68 tests) vp test run 10 server files → exit 1 (146/147) vp run fork:ci → exit 0 ``` The one failure is `ServerEnvironment.fork.test` on this machine: the local service launcher started a different t3 version. CI is the authority for it. ## Evidence A second client holds a live shell subscription on a dev server running a snapshot copy of real data. The driver speaks the WebSocket RPC the clients use. 1. Link issues 1430 and 1431 on a real thread 2. Live shell update carries both links 3. Reverse lookup returns the thread 4. Duplicate link: rejected, no event, no update 5. Unlink 1430: live update drops it 6. Read-only database: projection row and events 7. Unlink 1431: `issues` key gone  <details> <summary>Stills</summary>   </details> ## Close conditions Refs #1431 - [x] Replay: events rebuild the same projection - [x] Stale sync: older than linkedAt is ignored - [x] Duplicate: linking twice is a no-op - [x] Re-link: works after an unlink - [x] Authorization: only readable threads return - [x] Deletion: deleted thread leaves the lookup - [ ] Gate: no link control without capability | Condition | Evidence | | --- | --- | | Replay, re-link, deletion | "link, sync, unlink, re-link and delete agree across every projection" | | Stale sync | "both projectors drop a sync for a removed link, an earlier link, or an older read" | | Duplicate | decider rejects it, no event; state unchanged | | Authorization | orchestration read scope, beside the pull request lookup | | Gate | server advertises `threadIssues`; the control lands with #1432, so this PR refs, not closes | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the recording above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Squashes: - b06fe75 feat(server): threads persist linked GitHub issues (#1440) (#1440) (#1584) - b4d8082 feat(issues): threads keep linked issues on Orchestrator V2 thread state - 0530808 feat(issues): the legacy importer carries V1 issue links onto V2 threads Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no
donjor
pushed a commit
that referenced
this pull request
Oct 6, 2026
## What Changed Agents link GitHub issues to their own thread over MCP, the way they link pull requests. | Tool | Does | | --- | --- | | `link_issue` | links by URL, or repository and number, as `agent` | | `unlink_issue` | removes a link; reports whether one existed | | `list_thread_issues` | the thread's links with last known state | - **Thread:** always the calling token's own thread - **Host default:** the project's host when only a number is given - **Repeat link:** `alreadyLinked`, no error - **Absent unlink:** `wasLinked: false`, no error - **Other rejections:** stay failures - **Deleted thread:** not found, never a no-op - **Stored URL:** canonical issue URL only - **Hosts:** github.com or the project's host - **Capability:** rides the pull request tools' gate - **Instruction:** one line beside the pull request linking line Upstream files change only by `fork-hook` lines in `McpHttpServer.ts` and `RuntimeInstructions.ts`. ## Why Agents already register the pull requests they open; the issues they work on stayed invisible to the thread. This is layer 3 of #1430, on the link model from #1440. ## Checks ```text vp fmt --check (changed files) → exit 0 vp lint (changed files) → exit 0 tsc --noEmit (apps/server) → exit 0 vp test run issues toolkit + instructions → exit 0 (13 tests) vp run fork:ci → exit 0 ``` ## Evidence A real Claude thread on this branch's dev server, told to use only its MCP tools: 1. `link_issue` the issue URL 2. `link_issue` it again 3. `list_thread_issues` 4. `unlink_issue` it 5. `unlink_issue` it again 6. `list_thread_issues`  ## Close conditions Closes #1433 - [x] link_issue: links by URL as agent - [x] unlink_issue: removes the link - [x] list_thread_issues: lists the thread's links - [x] Idempotent: a repeat link changes nothing - [x] Scope: a call reaches only its own thread - [x] Missing thread: fails, never "already linked" | Condition | Evidence | | --- | --- | | link_issue | "links by URL with source agent on the token's thread" | | unlink_issue | "unlinks a linked issue and reports a missing one as wasLinked=false" | | list_thread_issues | "lists the shell's links, with unknown state until the first read" | | Idempotent | "treats a duplicate link as alreadyLinked rather than an error" | | Scope | the thread comes from the invocation token, never the input | | Missing thread | "fails cleanly when the token's thread no longer exists"; "reports a thread deleted between lookup and dispatch as not found" | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the capture above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Squashes: - b06df04 feat(server): agents link GitHub issues to their thread over MCP (#1441) (#1441) - ead034c fix(fork): adapt fork seams to v0.0.46-nightly.20261005.2689 Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no
donjor
added a commit
that referenced
this pull request
Oct 6, 2026
…, and palette (#1442) ## What Changed The web client links GitHub issues to threads from both sides, and **Work on this issue** links the issue at the draft's first send. | Entry point | Does | | --- | --- | | Thread issues panel | "Linked to this thread" list, **Link**, unlink | | Issue detail | "Linked threads" list, open, unlink, **Link to a thread** | | Issue actions menu | link or unlink the active thread | | Command palette | "Link GitHub issue to thread" | | Work on this issue | links as `handoff` after the turn starts | | Rule | Behavior | | --- | --- | | Handoff store | keyed by draft, in `localStorage` | | Taken | only after a turn start succeeds | | Multi-model send | links every started thread | | Failed send | entry kept for the retry | | Ask, Explain | mention the issue, never record it | | Gate | every control needs `threadIssues` | | Keybinding | none by default | | Title, state | unknown until #1434 | Upstream files change only by `fork-hook` lines in `ChatView.tsx` and `CommandPalette.tsx`. ## Why **Work on this issue** only seeded a draft, so no thread existed to link until the first send. This is layer 2 of #1430, on the link model from #1440. ## Checks ```text tsc --noEmit (apps/web) → exit 0 tsc --noEmit (packages/client-runtime) → exit 0 vp test run githubIssue + CommandPalette → exit 0 (13 files, 95 tests) vp run knip:check → exit 0 vp run fork:ci → exit 0 ``` ## Evidence This branch's dev server on a copy of real data, in Chrome. ### Thread side 1. Empty panel  2. **Link** dialog resolves a bare number  3. Linked; state unknown until a read  ### Issue side 4. The same link under "Linked threads"  5. After unlink from the issue  ### Palette  ### Handoff **Work on this issue** on this issue, then one send; the new thread links it.  Stored row: `source: handoff`, number 1432; the handoff entry was consumed. ## Close conditions Closes #1432 Closes #1431 ### Handoff - [x] **First send:** links the issue as `handoff` - [x] **Draft reuse:** the link survives - [x] **Promotion:** the link survives - [x] **Background send:** links the issue - [x] **Failed send:** a retry links it once ### Surfaces - [x] **Issue panel:** lists and opens linked threads - [x] **Entry points:** issue panel, thread, palette - [x] **Ask, Explain:** create no link ### Access (#1431) - [x] **Gate:** no link control without capability | Condition | Evidence | | --- | --- | | First send | capture 7 and the stored row; "links the draft's issue as a handoff at the first send, once" | | Draft reuse | "survives a reload and a reused draft, where the latest issue wins" | | Promotion | the entry is keyed by draft and taken after the promoted thread's turn starts | | Background send | the link runs on the shared path after `startPromise`, foreground or background | | Failed send | "keeps the issue through a failed send so the retry on a re-minted thread links it" | | Issue panel | captures 4 and 5 | | Entry points | captures 2, 4, and 6 | | Ask, Explain | only **Work on this issue** records a handoff | | Gate | panel, dialog, menu, palette, and handoff each check `threadIssues` | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes Capture 1 is the before state; no motion changed. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
pushed a commit
that referenced
this pull request
Oct 8, 2026
## What Changed A thread stores its linked GitHub issues as orchestration state, modeled on its pull request links. ### Model - **Link:** host, repository, number, url - **Record:** source (`manual`, `handoff`, `agent`), `linkedAt` - **Snapshot:** title, state, `syncedAt` - **Commands:** `thread.issue.link`, `thread.issue.unlink` - **Sync:** internal `thread.issue-link.sync` - **Capability:** `threadIssues`, missing means unsupported - **Reverse lookup:** RPC for the threads on one issue ### Storage and reads - **Table:** idempotent `ForkSchema.ts` pass, never a numbered migration - **Thread and shell:** optional `issues`, omitted when empty - **Live:** shell subscribers refetch on a link change - **Detail stream:** carries no issue events - **Sync order:** an older snapshot never wins - **Deletion:** a re-created thread has no links Upstream files change only by one-line `fork-hook` lines; logic lives in `*.fork.ts`. ## Why Threads link pull requests but not the issues they work on. This is layer 1 of #1430: the web controls, MCP tools, sync and mobile build on it. ### Version skew | Client | Server | Result | | --- | --- | --- | | Old | New | decodes thread, shell and every stream item | | New | Old | missing `issues` reads as none; capability off | A server from before this change cannot reopen a database holding link events; `fork-delta.md` records that downgrade limit. ## Checks ```text tsc --noEmit (contracts, server, web, client-runtime, mobile) → exit 0 vp test run src/server.test.ts → exit 0 (211 tests) vp test run threadIssues + orchestration (contracts) → exit 0 (68 tests) vp test run 10 server files → exit 1 (146/147) vp run fork:ci → exit 0 ``` The one failure is `ServerEnvironment.fork.test` on this machine: the local service launcher started a different t3 version. CI is the authority for it. ## Evidence A second client holds a live shell subscription on a dev server running a snapshot copy of real data. The driver speaks the WebSocket RPC the clients use. 1. Link issues 1430 and 1431 on a real thread 2. Live shell update carries both links 3. Reverse lookup returns the thread 4. Duplicate link: rejected, no event, no update 5. Unlink 1430: live update drops it 6. Read-only database: projection row and events 7. Unlink 1431: `issues` key gone  <details> <summary>Stills</summary>   </details> ## Close conditions Refs #1431 - [x] Replay: events rebuild the same projection - [x] Stale sync: older than linkedAt is ignored - [x] Duplicate: linking twice is a no-op - [x] Re-link: works after an unlink - [x] Authorization: only readable threads return - [x] Deletion: deleted thread leaves the lookup - [ ] Gate: no link control without capability | Condition | Evidence | | --- | --- | | Replay, re-link, deletion | "link, sync, unlink, re-link and delete agree across every projection" | | Stale sync | "both projectors drop a sync for a removed link, an earlier link, or an older read" | | Duplicate | decider rejects it, no event; state unchanged | | Authorization | orchestration read scope, beside the pull request lookup | | Gate | server advertises `threadIssues`; the control lands with #1432, so this PR refs, not closes | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the recording above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Squashes: - b06fe75 feat(server): threads persist linked GitHub issues (#1440) (#1440) (#1584) - b4d8082 feat(issues): threads keep linked issues on Orchestrator V2 thread state - 0530808 feat(issues): the legacy importer carries V1 issue links onto V2 threads Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no
donjor
pushed a commit
that referenced
this pull request
Oct 8, 2026
## What Changed Agents link GitHub issues to their own thread over MCP, the way they link pull requests. | Tool | Does | | --- | --- | | `link_issue` | links by URL, or repository and number, as `agent` | | `unlink_issue` | removes a link; reports whether one existed | | `list_thread_issues` | the thread's links with last known state | - **Thread:** always the calling token's own thread - **Host default:** the project's host when only a number is given - **Repeat link:** `alreadyLinked`, no error - **Absent unlink:** `wasLinked: false`, no error - **Other rejections:** stay failures - **Deleted thread:** not found, never a no-op - **Stored URL:** canonical issue URL only - **Hosts:** github.com or the project's host - **Capability:** rides the pull request tools' gate - **Instruction:** one line beside the pull request linking line Upstream files change only by `fork-hook` lines in `McpHttpServer.ts` and `RuntimeInstructions.ts`. ## Why Agents already register the pull requests they open; the issues they work on stayed invisible to the thread. This is layer 3 of #1430, on the link model from #1440. ## Checks ```text vp fmt --check (changed files) → exit 0 vp lint (changed files) → exit 0 tsc --noEmit (apps/server) → exit 0 vp test run issues toolkit + instructions → exit 0 (13 tests) vp run fork:ci → exit 0 ``` ## Evidence A real Claude thread on this branch's dev server, told to use only its MCP tools: 1. `link_issue` the issue URL 2. `link_issue` it again 3. `list_thread_issues` 4. `unlink_issue` it 5. `unlink_issue` it again 6. `list_thread_issues`  ## Close conditions Closes #1433 - [x] link_issue: links by URL as agent - [x] unlink_issue: removes the link - [x] list_thread_issues: lists the thread's links - [x] Idempotent: a repeat link changes nothing - [x] Scope: a call reaches only its own thread - [x] Missing thread: fails, never "already linked" | Condition | Evidence | | --- | --- | | link_issue | "links by URL with source agent on the token's thread" | | unlink_issue | "unlinks a linked issue and reports a missing one as wasLinked=false" | | list_thread_issues | "lists the shell's links, with unknown state until the first read" | | Idempotent | "treats a duplicate link as alreadyLinked rather than an error" | | Scope | the thread comes from the invocation token, never the input | | Missing thread | "fails cleanly when the token's thread no longer exists"; "reports a thread deleted between lookup and dispatch as not found" | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the capture above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Squashes: - b06df04 feat(server): agents link GitHub issues to their thread over MCP (#1441) (#1441) - ead034c fix(fork): adapt fork seams to v0.0.46-nightly.20261005.2689 Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no
donjor
added a commit
that referenced
this pull request
Oct 8, 2026
…, and palette (#1442) ## What Changed The web client links GitHub issues to threads from both sides, and **Work on this issue** links the issue at the draft's first send. | Entry point | Does | | --- | --- | | Thread issues panel | "Linked to this thread" list, **Link**, unlink | | Issue detail | "Linked threads" list, open, unlink, **Link to a thread** | | Issue actions menu | link or unlink the active thread | | Command palette | "Link GitHub issue to thread" | | Work on this issue | links as `handoff` after the turn starts | | Rule | Behavior | | --- | --- | | Handoff store | keyed by draft, in `localStorage` | | Taken | only after a turn start succeeds | | Multi-model send | links every started thread | | Failed send | entry kept for the retry | | Ask, Explain | mention the issue, never record it | | Gate | every control needs `threadIssues` | | Keybinding | none by default | | Title, state | unknown until #1434 | Upstream files change only by `fork-hook` lines in `ChatView.tsx` and `CommandPalette.tsx`. ## Why **Work on this issue** only seeded a draft, so no thread existed to link until the first send. This is layer 2 of #1430, on the link model from #1440. ## Checks ```text tsc --noEmit (apps/web) → exit 0 tsc --noEmit (packages/client-runtime) → exit 0 vp test run githubIssue + CommandPalette → exit 0 (13 files, 95 tests) vp run knip:check → exit 0 vp run fork:ci → exit 0 ``` ## Evidence This branch's dev server on a copy of real data, in Chrome. ### Thread side 1. Empty panel  2. **Link** dialog resolves a bare number  3. Linked; state unknown until a read  ### Issue side 4. The same link under "Linked threads"  5. After unlink from the issue  ### Palette  ### Handoff **Work on this issue** on this issue, then one send; the new thread links it.  Stored row: `source: handoff`, number 1432; the handoff entry was consumed. ## Close conditions Closes #1432 Closes #1431 ### Handoff - [x] **First send:** links the issue as `handoff` - [x] **Draft reuse:** the link survives - [x] **Promotion:** the link survives - [x] **Background send:** links the issue - [x] **Failed send:** a retry links it once ### Surfaces - [x] **Issue panel:** lists and opens linked threads - [x] **Entry points:** issue panel, thread, palette - [x] **Ask, Explain:** create no link ### Access (#1431) - [x] **Gate:** no link control without capability | Condition | Evidence | | --- | --- | | First send | capture 7 and the stored row; "links the draft's issue as a handoff at the first send, once" | | Draft reuse | "survives a reload and a reused draft, where the latest issue wins" | | Promotion | the entry is keyed by draft and taken after the promoted thread's turn starts | | Background send | the link runs on the shared path after `startPromise`, foreground or background | | Failed send | "keeps the issue through a failed send so the retry on a re-minted thread links it" | | Issue panel | captures 4 and 5 | | Entry points | captures 2, 4, and 6 | | Ask, Explain | only **Work on this issue** records a handoff | | Gate | panel, dialog, menu, palette, and handoff each check `threadIssues` | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes Capture 1 is the before state; no motion changed. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
pushed a commit
that referenced
this pull request
Oct 10, 2026
## What Changed A thread stores its linked GitHub issues as orchestration state, modeled on its pull request links. ### Model - **Link:** host, repository, number, url - **Record:** source (`manual`, `handoff`, `agent`), `linkedAt` - **Snapshot:** title, state, `syncedAt` - **Commands:** `thread.issue.link`, `thread.issue.unlink` - **Sync:** internal `thread.issue-link.sync` - **Capability:** `threadIssues`, missing means unsupported - **Reverse lookup:** RPC for the threads on one issue ### Storage and reads - **Table:** idempotent `ForkSchema.ts` pass, never a numbered migration - **Thread and shell:** optional `issues`, omitted when empty - **Live:** shell subscribers refetch on a link change - **Detail stream:** carries no issue events - **Sync order:** an older snapshot never wins - **Deletion:** a re-created thread has no links Upstream files change only by one-line `fork-hook` lines; logic lives in `*.fork.ts`. ## Why Threads link pull requests but not the issues they work on. This is layer 1 of #1430: the web controls, MCP tools, sync and mobile build on it. ### Version skew | Client | Server | Result | | --- | --- | --- | | Old | New | decodes thread, shell and every stream item | | New | Old | missing `issues` reads as none; capability off | A server from before this change cannot reopen a database holding link events; `fork-delta.md` records that downgrade limit. ## Checks ```text tsc --noEmit (contracts, server, web, client-runtime, mobile) → exit 0 vp test run src/server.test.ts → exit 0 (211 tests) vp test run threadIssues + orchestration (contracts) → exit 0 (68 tests) vp test run 10 server files → exit 1 (146/147) vp run fork:ci → exit 0 ``` The one failure is `ServerEnvironment.fork.test` on this machine: the local service launcher started a different t3 version. CI is the authority for it. ## Evidence A second client holds a live shell subscription on a dev server running a snapshot copy of real data. The driver speaks the WebSocket RPC the clients use. 1. Link issues 1430 and 1431 on a real thread 2. Live shell update carries both links 3. Reverse lookup returns the thread 4. Duplicate link: rejected, no event, no update 5. Unlink 1430: live update drops it 6. Read-only database: projection row and events 7. Unlink 1431: `issues` key gone  <details> <summary>Stills</summary>   </details> ## Close conditions Refs #1431 - [x] Replay: events rebuild the same projection - [x] Stale sync: older than linkedAt is ignored - [x] Duplicate: linking twice is a no-op - [x] Re-link: works after an unlink - [x] Authorization: only readable threads return - [x] Deletion: deleted thread leaves the lookup - [ ] Gate: no link control without capability | Condition | Evidence | | --- | --- | | Replay, re-link, deletion | "link, sync, unlink, re-link and delete agree across every projection" | | Stale sync | "both projectors drop a sync for a removed link, an earlier link, or an older read" | | Duplicate | decider rejects it, no event; state unchanged | | Authorization | orchestration read scope, beside the pull request lookup | | Gate | server advertises `threadIssues`; the control lands with #1432, so this PR refs, not closes | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the recording above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Squashes: - b06fe75 feat(server): threads persist linked GitHub issues (#1440) (#1440) (#1584) - b4d8082 feat(issues): threads keep linked issues on Orchestrator V2 thread state - 0530808 feat(issues): the legacy importer carries V1 issue links onto V2 threads Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no
donjor
pushed a commit
that referenced
this pull request
Oct 10, 2026
## What Changed Agents link GitHub issues to their own thread over MCP, the way they link pull requests. | Tool | Does | | --- | --- | | `link_issue` | links by URL, or repository and number, as `agent` | | `unlink_issue` | removes a link; reports whether one existed | | `list_thread_issues` | the thread's links with last known state | - **Thread:** always the calling token's own thread - **Host default:** the project's host when only a number is given - **Repeat link:** `alreadyLinked`, no error - **Absent unlink:** `wasLinked: false`, no error - **Other rejections:** stay failures - **Deleted thread:** not found, never a no-op - **Stored URL:** canonical issue URL only - **Hosts:** github.com or the project's host - **Capability:** rides the pull request tools' gate - **Instruction:** one line beside the pull request linking line Upstream files change only by `fork-hook` lines in `McpHttpServer.ts` and `RuntimeInstructions.ts`. ## Why Agents already register the pull requests they open; the issues they work on stayed invisible to the thread. This is layer 3 of #1430, on the link model from #1440. ## Checks ```text vp fmt --check (changed files) → exit 0 vp lint (changed files) → exit 0 tsc --noEmit (apps/server) → exit 0 vp test run issues toolkit + instructions → exit 0 (13 tests) vp run fork:ci → exit 0 ``` ## Evidence A real Claude thread on this branch's dev server, told to use only its MCP tools: 1. `link_issue` the issue URL 2. `link_issue` it again 3. `list_thread_issues` 4. `unlink_issue` it 5. `unlink_issue` it again 6. `list_thread_issues`  ## Close conditions Closes #1433 - [x] link_issue: links by URL as agent - [x] unlink_issue: removes the link - [x] list_thread_issues: lists the thread's links - [x] Idempotent: a repeat link changes nothing - [x] Scope: a call reaches only its own thread - [x] Missing thread: fails, never "already linked" | Condition | Evidence | | --- | --- | | link_issue | "links by URL with source agent on the token's thread" | | unlink_issue | "unlinks a linked issue and reports a missing one as wasLinked=false" | | list_thread_issues | "lists the shell's links, with unknown state until the first read" | | Idempotent | "treats a duplicate link as alreadyLinked rather than an error" | | Scope | the thread comes from the invocation token, never the input | | Missing thread | "fails cleanly when the token's thread no longer exists"; "reports a thread deleted between lookup and dispatch as not found" | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the capture above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Squashes: - b06df04 feat(server): agents link GitHub issues to their thread over MCP (#1441) (#1441) - ead034c fix(fork): adapt fork seams to v0.0.46-nightly.20261005.2689 Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no
donjor
added a commit
that referenced
this pull request
Oct 10, 2026
…, and palette (#1442) ## What Changed The web client links GitHub issues to threads from both sides, and **Work on this issue** links the issue at the draft's first send. | Entry point | Does | | --- | --- | | Thread issues panel | "Linked to this thread" list, **Link**, unlink | | Issue detail | "Linked threads" list, open, unlink, **Link to a thread** | | Issue actions menu | link or unlink the active thread | | Command palette | "Link GitHub issue to thread" | | Work on this issue | links as `handoff` after the turn starts | | Rule | Behavior | | --- | --- | | Handoff store | keyed by draft, in `localStorage` | | Taken | only after a turn start succeeds | | Multi-model send | links every started thread | | Failed send | entry kept for the retry | | Ask, Explain | mention the issue, never record it | | Gate | every control needs `threadIssues` | | Keybinding | none by default | | Title, state | unknown until #1434 | Upstream files change only by `fork-hook` lines in `ChatView.tsx` and `CommandPalette.tsx`. ## Why **Work on this issue** only seeded a draft, so no thread existed to link until the first send. This is layer 2 of #1430, on the link model from #1440. ## Checks ```text tsc --noEmit (apps/web) → exit 0 tsc --noEmit (packages/client-runtime) → exit 0 vp test run githubIssue + CommandPalette → exit 0 (13 files, 95 tests) vp run knip:check → exit 0 vp run fork:ci → exit 0 ``` ## Evidence This branch's dev server on a copy of real data, in Chrome. ### Thread side 1. Empty panel  2. **Link** dialog resolves a bare number  3. Linked; state unknown until a read  ### Issue side 4. The same link under "Linked threads"  5. After unlink from the issue  ### Palette  ### Handoff **Work on this issue** on this issue, then one send; the new thread links it.  Stored row: `source: handoff`, number 1432; the handoff entry was consumed. ## Close conditions Closes #1432 Closes #1431 ### Handoff - [x] **First send:** links the issue as `handoff` - [x] **Draft reuse:** the link survives - [x] **Promotion:** the link survives - [x] **Background send:** links the issue - [x] **Failed send:** a retry links it once ### Surfaces - [x] **Issue panel:** lists and opens linked threads - [x] **Entry points:** issue panel, thread, palette - [x] **Ask, Explain:** create no link ### Access (#1431) - [x] **Gate:** no link control without capability | Condition | Evidence | | --- | --- | | First send | capture 7 and the stored row; "links the draft's issue as a handoff at the first send, once" | | Draft reuse | "survives a reload and a reused draft, where the latest issue wins" | | Promotion | the entry is keyed by draft and taken after the promoted thread's turn starts | | Background send | the link runs on the shared path after `startPromise`, foreground or background | | Failed send | "keeps the issue through a failed send so the retry on a re-minted thread links it" | | Issue panel | captures 4 and 5 | | Entry points | captures 2, 4, and 6 | | Ask, Explain | only **Work on this issue** records a handoff | | Gate | panel, dialog, menu, palette, and handoff each check `threadIssues` | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes Capture 1 is the before state; no motion changed. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no Co-authored-by: donjor <[email protected]>
donjor
pushed a commit
that referenced
this pull request
Oct 10, 2026
## What Changed A thread stores its linked GitHub issues as orchestration state, modeled on its pull request links. ### Model - **Link:** host, repository, number, url - **Record:** source (`manual`, `handoff`, `agent`), `linkedAt` - **Snapshot:** title, state, `syncedAt` - **Commands:** `thread.issue.link`, `thread.issue.unlink` - **Sync:** internal `thread.issue-link.sync` - **Capability:** `threadIssues`, missing means unsupported - **Reverse lookup:** RPC for the threads on one issue ### Storage and reads - **Table:** idempotent `ForkSchema.ts` pass, never a numbered migration - **Thread and shell:** optional `issues`, omitted when empty - **Live:** shell subscribers refetch on a link change - **Detail stream:** carries no issue events - **Sync order:** an older snapshot never wins - **Deletion:** a re-created thread has no links Upstream files change only by one-line `fork-hook` lines; logic lives in `*.fork.ts`. ## Why Threads link pull requests but not the issues they work on. This is layer 1 of #1430: the web controls, MCP tools, sync and mobile build on it. ### Version skew | Client | Server | Result | | --- | --- | --- | | Old | New | decodes thread, shell and every stream item | | New | Old | missing `issues` reads as none; capability off | A server from before this change cannot reopen a database holding link events; `fork-delta.md` records that downgrade limit. ## Checks ```text tsc --noEmit (contracts, server, web, client-runtime, mobile) → exit 0 vp test run src/server.test.ts → exit 0 (211 tests) vp test run threadIssues + orchestration (contracts) → exit 0 (68 tests) vp test run 10 server files → exit 1 (146/147) vp run fork:ci → exit 0 ``` The one failure is `ServerEnvironment.fork.test` on this machine: the local service launcher started a different t3 version. CI is the authority for it. ## Evidence A second client holds a live shell subscription on a dev server running a snapshot copy of real data. The driver speaks the WebSocket RPC the clients use. 1. Link issues 1430 and 1431 on a real thread 2. Live shell update carries both links 3. Reverse lookup returns the thread 4. Duplicate link: rejected, no event, no update 5. Unlink 1430: live update drops it 6. Read-only database: projection row and events 7. Unlink 1431: `issues` key gone  <details> <summary>Stills</summary>   </details> ## Close conditions Refs #1431 - [x] Replay: events rebuild the same projection - [x] Stale sync: older than linkedAt is ignored - [x] Duplicate: linking twice is a no-op - [x] Re-link: works after an unlink - [x] Authorization: only readable threads return - [x] Deletion: deleted thread leaves the lookup - [ ] Gate: no link control without capability | Condition | Evidence | | --- | --- | | Replay, re-link, deletion | "link, sync, unlink, re-link and delete agree across every projection" | | Stale sync | "both projectors drop a sync for a removed link, an earlier link, or an older read" | | Duplicate | decider rejects it, no event; state unchanged | | Authorization | orchestration read scope, beside the pull request lookup | | Gate | server advertises `threadIssues`; the control lands with #1432, so this PR refs, not closes | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the recording above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Squashes: - b06fe75 feat(server): threads persist linked GitHub issues (#1440) (#1440) (#1584) - b4d8082 feat(issues): threads keep linked issues on Orchestrator V2 thread state - 0530808 feat(issues): the legacy importer carries V1 issue links onto V2 threads Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no
donjor
pushed a commit
that referenced
this pull request
Oct 10, 2026
## What Changed Agents link GitHub issues to their own thread over MCP, the way they link pull requests. | Tool | Does | | --- | --- | | `link_issue` | links by URL, or repository and number, as `agent` | | `unlink_issue` | removes a link; reports whether one existed | | `list_thread_issues` | the thread's links with last known state | - **Thread:** always the calling token's own thread - **Host default:** the project's host when only a number is given - **Repeat link:** `alreadyLinked`, no error - **Absent unlink:** `wasLinked: false`, no error - **Other rejections:** stay failures - **Deleted thread:** not found, never a no-op - **Stored URL:** canonical issue URL only - **Hosts:** github.com or the project's host - **Capability:** rides the pull request tools' gate - **Instruction:** one line beside the pull request linking line Upstream files change only by `fork-hook` lines in `McpHttpServer.ts` and `RuntimeInstructions.ts`. ## Why Agents already register the pull requests they open; the issues they work on stayed invisible to the thread. This is layer 3 of #1430, on the link model from #1440. ## Checks ```text vp fmt --check (changed files) → exit 0 vp lint (changed files) → exit 0 tsc --noEmit (apps/server) → exit 0 vp test run issues toolkit + instructions → exit 0 (13 tests) vp run fork:ci → exit 0 ``` ## Evidence A real Claude thread on this branch's dev server, told to use only its MCP tools: 1. `link_issue` the issue URL 2. `link_issue` it again 3. `list_thread_issues` 4. `unlink_issue` it 5. `unlink_issue` it again 6. `list_thread_issues`  ## Close conditions Closes #1433 - [x] link_issue: links by URL as agent - [x] unlink_issue: removes the link - [x] list_thread_issues: lists the thread's links - [x] Idempotent: a repeat link changes nothing - [x] Scope: a call reaches only its own thread - [x] Missing thread: fails, never "already linked" | Condition | Evidence | | --- | --- | | link_issue | "links by URL with source agent on the token's thread" | | unlink_issue | "unlinks a linked issue and reports a missing one as wasLinked=false" | | list_thread_issues | "lists the shell's links, with unknown state until the first read" | | Idempotent | "treats a duplicate link as alreadyLinked rather than an error" | | Scope | the thread comes from the invocation token, never the input | | Missing thread | "fails cleanly when the token's thread no longer exists"; "reports a thread deleted between lookup and dispatch as not found" | ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why - [x] I included before/after screenshots for any UI changes - [x] I included a video for animation/interaction changes No UI changes; the capture above is server evidence. Opus 5.5 via Claude Code in T3 Code. ## Fork trailers Squashes: - 8022f70 feat(server): agents link GitHub issues to their thread over MCP (#1441) (#1441) - 05e302b fixup! feat(server): agents link GitHub issues to their thread over MCP (#1441) (#1441) - dda4b19 fixup! feat(server): agents link GitHub issues to their thread over MCP (#1441) (#1441) - b06df04 feat(server): agents link GitHub issues to their thread over MCP (#1441) (#1441) - ead034c fix(fork): adapt fork seams to v0.0.46-nightly.20261005.2689 Fork-Domain: github-issues Fork-Tier: qol Fork-Upstreamable: no
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What Changed
A thread stores its linked GitHub issues as orchestration state, modeled on its pull request links.
Model
manual,handoff,agent),linkedAtsyncedAtthread.issue.link,thread.issue.unlinkthread.issue-link.syncthreadIssues, missing means unsupportedStorage and reads
ForkSchema.tspass, never a numbered migrationissues, omitted when emptyUpstream files change only by one-line
fork-hooklines; logic lives in*.fork.ts.Why
Threads link pull requests but not the issues they work on.
This is layer 1 of #1430: the web controls, MCP tools, sync and mobile build on it.
Version skew
issuesreads as none; capability offA server from before this change cannot reopen a database holding link events;
fork-delta.mdrecords that downgrade limit.Checks
The one failure is
ServerEnvironment.fork.teston this machine: the local service launcher started a different t3 version.CI is the authority for it.
Evidence
A second client holds a live shell subscription on a dev server running a snapshot copy of real data.
The driver speaks the WebSocket RPC the clients use.
issueskey goneStills
Close conditions
Refs #1431
threadIssues; the control lands with #1432, so this PR refs, not closesChecklist
No UI changes; the recording above is server evidence.
Opus 5.5 via Claude Code in T3 Code.
Fork trailers
Fork-Domain: github-issues
Fork-Tier: qol
Fork-Upstreamable: no