diff --git a/README.md b/README.md index cbcd557c..f7323e7b 100644 --- a/README.md +++ b/README.md @@ -331,7 +331,12 @@ For local CLI usage, add them to your shell profile (`~/.zshrc` or `~/.bashrc`). for details. To disable automatic `gh` installation, pass `--no-gh-cli` during setup or set -`use_gh_cli: false` in `.tbd/config.yml` under `settings:`. +`use_gh_cli: false` in `.tbd/config.yml` under `settings:`. Note: disabling `gh` also +disables all external issue features — linking beads to GitHub issues/PRs +(`--external-issue`), bidirectional status/label sync (`tbd sync --external`), and +GitHub issue/PR validation. +The `external_issue_url` field can still exist on beads from collaborators, but no sync +or validation occurs locally. ### Migrating from Beads @@ -355,10 +360,13 @@ tbd list --all # Include closed tbd list --specs # Group beads by spec tbd show proj-a7k2 # View bead details tbd create "Title" --type=bug # Create bead (bug/feature/task/epic/chore) +tbd create "Title" --external-issue=https://github.com/org/repo/issues/42 tbd update proj-a7k2 --status=in_progress +tbd update proj-a7k2 --external-issue=https://github.com/org/repo/issues/42 tbd close proj-a7k2 # Close bead tbd close proj-a7k2 --reason="Fixed in commit abc123" tbd sync # Sync with remote (auto-commits and pushes) +tbd sync --external # Sync external GitHub issue status/labels ``` ### Dependencies and Labels diff --git a/docs/project/qa/external-issues.qa.md b/docs/project/qa/external-issues.qa.md new file mode 100644 index 00000000..a6732ddb --- /dev/null +++ b/docs/project/qa/external-issues.qa.md @@ -0,0 +1,675 @@ +# External Issue Linking: QA Validation Script + +> **Feature**: External GitHub Issue Linking (PR #83) **Branch**: +> `claude/external-issue-linking-1rGfb` **Date**: 2026-02-10 + +## Overview + +This document provides a prescriptive, step-by-step QA script for manually validating +the external issue linking feature end-to-end. +It complements the 89 automated tests (unit + e2e) by exercising real GitHub API calls, +real `gh` CLI authentication, and real sync behavior that cannot be tested in CI without +credentials. + +## Goals + +1. Verify `--external-issue` flag works on `create` and `update` with real GitHub Issues +2. Verify `tbd show` and `tbd list --external-issue` display linked issues correctly +3. Verify bidirectional sync: `tbd sync --external` pulls GitHub state changes and + pushes local status changes +4. Verify inheritance: child beads inherit `external_issue_url` from parent +5. Verify propagation: updating parent’s `external_issue_url` propagates to children +6. Verify `tbd doctor` checks `gh` CLI health +7. Verify `use_gh_cli: false` gates all external issue features +8. Verify error handling for bad URLs, non-existent issues + +* * * + +## Prerequisites + +### Tools Required + +```bash +# Verify gh CLI is installed and authenticated +gh --version +gh auth status + +# Verify tbd is built from the feature branch +git checkout claude/external-issue-linking-1rGfb +pnpm install && pnpm build +tbd --version +``` + +### Create a Test Repository on GitHub + +You need a throwaway GitHub repo with a few test issues. +You can use an existing test repo or create a new one: + +```bash +# Create a test repo (or use an existing one) +gh repo create my-tbd-qa-test --public --description "Throwaway repo for tbd QA" --clone +cd my-tbd-qa-test + +# Create test issues on GitHub that we'll link to +gh issue create --title "QA Test Issue 1 - Basic linking" --body "For QA testing tbd external linking" +gh issue create --title "QA Test Issue 2 - Sync testing" --body "For QA testing bidirectional sync" +gh issue create --title "QA Test Issue 3 - Inheritance" --body "For QA testing inheritance" +gh issue create --title "QA Test Issue 4 - Propagation" --body "For QA testing propagation" + +# Note the issue URLs (or query them) +gh issue list --json number,url +``` + +Record the URLs: +- `ISSUE_1_URL=https://github.com//my-tbd-qa-test/issues/1` +- `ISSUE_2_URL=https://github.com//my-tbd-qa-test/issues/2` +- `ISSUE_3_URL=https://github.com//my-tbd-qa-test/issues/3` +- `ISSUE_4_URL=https://github.com//my-tbd-qa-test/issues/4` + +### Set Up the Test Workspace + +Clone the test repo into the attic (or a temp directory) and initialize tbd: + +```bash +# From the tbd repo root +mkdir -p attic +cd attic +git clone https://github.com//my-tbd-qa-test.git tbd-qa-workspace +cd tbd-qa-workspace + +# Initialize tbd +tbd init --prefix=qa +tbd doctor +``` + +* * * + +## Test Script + +### Phase 1: Doctor Command and Configuration + +**Test 1.1: Doctor reports gh CLI status** + +```bash +tbd doctor +``` + +- [ ] Output includes an “Integration” or “gh CLI” check +- [ ] Reports gh as installed and authenticated +- [ ] Overall status is healthy + +**Test 1.2: Doctor with use_gh_cli disabled** + +```bash +# Temporarily disable gh CLI in config +echo "use_gh_cli: false" >> .tbd/config.yml +tbd doctor +``` + +- [ ] gh CLI check reports “disabled” (not an error/warning) +- [ ] No authentication check is run + +```bash +# Re-enable (remove the line we added) +# Edit .tbd/config.yml to remove "use_gh_cli: false" +``` + +* * * + +### Phase 2: Creating Beads with External Issue Links + +**Test 2.1: Create bead with --external-issue flag** + +```bash +tbd create "Basic linked task" --external-issue "$ISSUE_1_URL" --json +``` + +- [ ] Command succeeds (exit 0) +- [ ] JSON output includes the bead ID +- [ ] No error about GitHub API + +**Test 2.2: Verify the link via show** + +```bash +tbd show +``` + +- [ ] Output includes `external_issue_url:` line +- [ ] URL is displayed with colored formatting +- [ ] URL matches the issue URL provided + +```bash +tbd show --json +``` + +- [ ] JSON output includes `"external_issue_url": ""` + +**Test 2.3: Accept PR URLs** + +```bash +# First, create a PR in the test repo (or use an existing one) +tbd create "PR link" --external-issue "https://github.com//my-tbd-qa-test/pull/1" +``` + +- [ ] Command succeeds — PR URLs are valid external issue links +- [ ] A bead is created with the PR URL as `external_issue_url` + +**Test 2.4: Reject non-existent issues** + +```bash +tbd create "Bad link" --external-issue "https://github.com//my-tbd-qa-test/issues/99999" +``` + +- [ ] Command fails with an error about issue not found / not accessible +- [ ] No bead is created + +**Test 2.5: Reject when use_gh_cli is false** + +```bash +# Temporarily disable +echo "use_gh_cli: false" >> .tbd/config.yml +tbd create "Should fail" --external-issue "$ISSUE_1_URL" +``` + +- [ ] Command fails with error about use_gh_cli being disabled +- [ ] No bead is created + +```bash +# Re-enable +# Edit .tbd/config.yml to remove "use_gh_cli: false" +``` + +* * * + +### Phase 3: Updating Beads with External Issue Links + +**Test 3.1: Add external issue to existing bead** + +```bash +tbd create "Unlinked task" --json +# Note the ID +tbd update --external-issue "$ISSUE_2_URL" +tbd show --json +``` + +- [ ] Update succeeds +- [ ] `external_issue_url` is now set in show output + +**Test 3.2: Clear external issue via --from-file** + +```bash +cat > /tmp/clear-external.yml << 'EOF' +--- +external_issue_url: +--- +EOF +tbd update --from-file /tmp/clear-external.yml +tbd show --json +``` + +- [ ] Update succeeds +- [ ] `external_issue_url` is now null/absent in show output + +**Test 3.3: Set external issue via --from-file** + +```bash +cat > /tmp/set-external.yml << EOF +--- +external_issue_url: "$ISSUE_2_URL" +--- +EOF +tbd update --from-file /tmp/set-external.yml +tbd show --json +``` + +- [ ] `external_issue_url` is set again + +* * * + +### Phase 4: List Filtering + +**Test 4.1: List with --external-issue flag (show all linked)** + +```bash +tbd list --external-issue +``` + +- [ ] Shows only beads with an external_issue_url +- [ ] Does NOT show unlinked beads + +**Test 4.2: List with --external-issue (match specific URL)** + +```bash +tbd list --external-issue "$ISSUE_1_URL" --json +``` + +- [ ] Shows only the bead linked to ISSUE_1_URL +- [ ] Does NOT show bead linked to ISSUE_2_URL + +**Test 4.3: List --json includes external_issue_url field** + +```bash +tbd list --json +``` + +- [ ] Linked beads have `external_issue_url` in their JSON +- [ ] Unlinked beads do NOT have the field (or it’s undefined) + +* * * + +### Phase 5: Inheritance + +**Test 5.1: Child inherits external_issue_url from parent** + +```bash +tbd create "Parent epic" --type epic --external-issue "$ISSUE_3_URL" --json +# Note parent ID +tbd create "Child task" --parent --json +# Note child ID +tbd show --json +``` + +- [ ] Child has `external_issue_url` matching the parent’s URL +- [ ] Child did NOT require `--external-issue` flag + +**Test 5.2: Child does NOT inherit when parent has no URL** + +```bash +tbd create "Plain parent" --type epic --json +tbd create "Plain child" --parent --json +tbd show --json +``` + +- [ ] Child does NOT have `external_issue_url` + +* * * + +### Phase 6: Propagation + +**Test 6.1: Updating parent’s URL propagates to children** + +```bash +tbd create "Prop parent" --type epic --json +# Note parent ID +tbd create "Prop child 1" --parent --json +tbd create "Prop child 2" --parent --json + +# Now set URL on parent +tbd update --external-issue "$ISSUE_4_URL" + +# Check children +tbd show --json +tbd show --json +``` + +- [ ] Both children now have `external_issue_url` matching ISSUE_4_URL +- [ ] Parent also has the URL + +* * * + +### Phase 7: Bidirectional Sync (The Main Event) + +This is the most important manual test — it validates real GitHub API round-trips. + +**Test 7.1: External pull — close issue on GitHub, sync locally** + +```bash +# Start with an open bead linked to ISSUE_2_URL +tbd show --json +# Confirm status is "open" + +# Close the issue on GitHub +gh issue close --repo /my-tbd-qa-test --comment "Closing for QA" + +# Sync +tbd sync --external + +# Check local bead +tbd show --json +``` + +- [ ] Sync reports pulling status change +- [ ] Local bead status is now “closed” + +**Test 7.2: External pull — reopen issue on GitHub, sync locally** + +```bash +# Reopen the issue on GitHub +gh issue reopen --repo /my-tbd-qa-test + +# Sync +tbd sync --external + +# Check local bead +tbd show --json +``` + +- [ ] Sync reports pulling status change +- [ ] Local bead status is now “open” (reopened) + +**Test 7.3: External push — close bead locally, push to GitHub** + +```bash +# Close the local bead +tbd close + +# Sync +tbd sync --external + +# Check GitHub +gh issue view --repo /my-tbd-qa-test --json state,stateReason +``` + +- [ ] Sync reports pushing status change +- [ ] GitHub issue is now closed with reason “completed” + +**Test 7.4: External push — reopen bead locally, push to GitHub** + +```bash +# Reopen the local bead +tbd update --status open + +# Sync +tbd sync --external + +# Check GitHub +gh issue view --repo /my-tbd-qa-test --json state +``` + +- [ ] GitHub issue is now open again + +**Test 7.5: Sync with --external flag (scope isolation)** + +```bash +tbd sync --external +``` + +- [ ] Only external sync runs (no git push/pull, no doc sync) +- [ ] Output shows external pull/push phases only + +**Test 7.6: Full sync includes external phases** + +```bash +tbd sync +``` + +- [ ] Output shows all phases: external pull, docs, issues, external push +- [ ] External phases run alongside regular sync + +* * * + +### Phase 8: Error Handling + +**Test 8.1: Sync with invalid external_issue_url** + +```bash +# Manually set a bad URL via --from-file +cat > /tmp/bad-url.yml << 'EOF' +--- +external_issue_url: "https://github.com/nonexistent-org-12345/nonexistent-repo/issues/1" +--- +EOF +tbd create "Bad URL bead" --json +tbd update --from-file /tmp/bad-url.yml + +tbd sync --external +``` + +- [ ] Sync does NOT crash +- [ ] Reports error for the specific bead with bad URL +- [ ] Other linked beads (if any) still sync successfully + +**Test 8.2: Sync without gh CLI** + +```bash +# Temporarily rename gh or set PATH to exclude it +PATH_BAK="$PATH" +export PATH=$(echo "$PATH" | tr ':' '\n' | grep -v gh | tr '\n' ':') + +tbd sync --external + +export PATH="$PATH_BAK" +``` + +- [ ] Sync reports error about gh CLI not available +- [ ] Does NOT crash + +* * * + +### Phase 9: Deferred Status Mapping + +**Test 9.1: Defer bead locally, verify GitHub shows not_planned** + +```bash +tbd create "Deferred task" --external-issue "$ISSUE_1_URL" --json +tbd update --status deferred +tbd sync --external + +gh issue view --repo /my-tbd-qa-test --json state,stateReason +``` + +- [ ] GitHub issue is closed with `stateReason: "NOT_PLANNED"` + +**Test 9.2: Close as not_planned on GitHub, verify local shows deferred** + +```bash +# First reopen +gh issue reopen --repo /my-tbd-qa-test +tbd sync --external +# Bead should be open again + +# Now close as not_planned +gh issue close --repo /my-tbd-qa-test --reason "not planned" +tbd sync --external + +tbd show --json +``` + +- [ ] Local bead status is “deferred” (not “closed”) + +* * * + +## Cleanup + +After testing, clean up the test environment: + +```bash +# Delete the test repo +gh repo delete /my-tbd-qa-test --yes + +# Remove the test workspace +cd /path/to/tbd +rm -rf attic/tbd-qa-workspace +``` + +* * * + +## Automated Test Summary + +The following are covered by the 89 automated tests in CI and do NOT need manual +re-verification: + +| Area | Tests | Coverage | +| --- | --- | --- | +| URL parsing | 44 | Valid/invalid URLs, PR rejection, format variants | +| Status mapping | 12 | All tbd-to-GitHub and GitHub-to-tbd transitions | +| Label diff | 3 | Add/remove/no-op label changes | +| Inheritance | 20 | Parent-child field copying, explicit override, propagation | +| External sync | 15 | Pull/push logic with mocked GitHub API | +| E2e golden tests | 10 | Round-trip, update, clear, inherit, propagate, list filter, show | +| **Total** | **89** | Schema, CLI, logic, inheritance — all without network | + +### What manual testing adds + +The manual tests above exercise the real `gh api` integration that is mocked in CI. +Specifically: + +1. **Real HTTP calls** to GitHub’s API via `gh` +2. **Real authentication** flow (tokens, SSH keys) +3. **Real state transitions** (open/close/reopen on GitHub) +4. **Real `state_reason`** mapping (completed vs not_planned) +5. **Real error responses** (404, permission denied) +6. **Visual verification** of CLI output formatting +7. **Scope isolation** of `--external` flag in sync + +* * * + +## Semi-Automated QA Script + +For faster re-runs, you can save the following as a shell script. +It requires `GITHUB_USER` and optionally `QA_REPO` environment variables. + +```bash +#!/usr/bin/env bash +# external-issues-qa.sh - Semi-automated QA for external issue linking +# +# Usage: +# GITHUB_USER=yourname ./external-issues-qa.sh +# GITHUB_USER=yourname QA_REPO=existing-repo ./external-issues-qa.sh +# +set -euo pipefail + +GITHUB_USER="${GITHUB_USER:?Set GITHUB_USER to your GitHub username}" +QA_REPO="${QA_REPO:-tbd-qa-$(date +%s)}" +QA_DIR="$(mktemp -d)" +REPO_URL="https://github.com/$GITHUB_USER/$QA_REPO" + +cleanup() { + echo "" + echo "=== Cleanup ===" + echo "Test workspace: $QA_DIR" + echo "GitHub repo: $REPO_URL" + read -p "Delete test repo and workspace? [y/N] " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + gh repo delete "$GITHUB_USER/$QA_REPO" --yes 2>/dev/null || true + rm -rf "$QA_DIR" + echo "Cleaned up." + else + echo "Skipped cleanup. Remove manually when done." + fi +} +trap cleanup EXIT + +echo "=== Setup ===" +echo "Creating test repo: $GITHUB_USER/$QA_REPO" +gh repo create "$QA_REPO" --public --description "tbd QA test repo" --clone -- "$QA_DIR/$QA_REPO" +cd "$QA_DIR/$QA_REPO" + +echo "Creating test issues..." +ISSUE_1_URL=$(gh issue create --title "QA: Basic linking" --body "test" | tail -1) +ISSUE_2_URL=$(gh issue create --title "QA: Sync testing" --body "test" | tail -1) +ISSUE_3_URL=$(gh issue create --title "QA: Inheritance" --body "test" | tail -1) +ISSUE_4_URL=$(gh issue create --title "QA: Propagation" --body "test" | tail -1) + +echo "Issue 1: $ISSUE_1_URL" +echo "Issue 2: $ISSUE_2_URL" +echo "Issue 3: $ISSUE_3_URL" +echo "Issue 4: $ISSUE_4_URL" + +echo "" +echo "Initializing tbd..." +tbd init --prefix=qa +echo "" + +pass() { echo " PASS: $1"; } +fail() { echo " FAIL: $1"; FAILURES=$((FAILURES + 1)); } +FAILURES=0 + +# ---- Test: Doctor ---- +echo "=== Test: Doctor ===" +tbd doctor 2>&1 | grep -qi "gh\|github\|cli" && pass "Doctor mentions gh CLI" || fail "Doctor should mention gh CLI" + +# ---- Test: Create with --external-issue ---- +echo "" +echo "=== Test: Create with --external-issue ===" +BEAD_1=$(tbd create "Linked task 1" --external-issue "$ISSUE_1_URL" --json | jq -r '.id') +[[ -n "$BEAD_1" ]] && pass "Created bead $BEAD_1" || fail "Failed to create linked bead" + +URL_OUT=$(tbd show "$BEAD_1" --json | jq -r '.external_issue_url // empty') +[[ "$URL_OUT" == "$ISSUE_1_URL" ]] && pass "Show displays correct URL" || fail "Show URL mismatch: $URL_OUT" + +# ---- Test: Accept PR URL ---- +echo "" +echo "=== Test: Accept PR URL ===" +# Create a PR in the repo first for this test (or use an existing one) +PR_BEAD=$(tbd create "PR link" --external-issue "${REPO_URL}/pull/1" --json 2>&1 | jq -r '.id // empty') +if [[ -n "$PR_BEAD" ]]; then + pass "Accepted PR URL (both issues and PRs are valid)" +else + fail "Should accept PR URL" +fi + +# ---- Test: Create linked bead for sync ---- +echo "" +echo "=== Test: Sync Setup ===" +BEAD_2=$(tbd create "Sync task" --external-issue "$ISSUE_2_URL" --json | jq -r '.id') +echo "Created sync bead: $BEAD_2" + +# ---- Test: External pull (close on GitHub, pull locally) ---- +echo "" +echo "=== Test: External Pull ===" +gh issue close 2 --repo "$GITHUB_USER/$QA_REPO" --comment "QA close" +sleep 2 +tbd sync --external +STATUS=$(tbd show "$BEAD_2" --json | jq -r '.status') +[[ "$STATUS" == "closed" ]] && pass "Pull: bead closed after GH close" || fail "Pull: expected closed, got $STATUS" + +# ---- Test: External pull (reopen on GitHub, pull locally) ---- +echo "" +echo "=== Test: External Pull (reopen) ===" +gh issue reopen 2 --repo "$GITHUB_USER/$QA_REPO" +sleep 2 +tbd sync --external +STATUS=$(tbd show "$BEAD_2" --json | jq -r '.status') +[[ "$STATUS" == "open" ]] && pass "Pull: bead reopened after GH reopen" || fail "Pull: expected open, got $STATUS" + +# ---- Test: External push (close locally, push to GitHub) ---- +echo "" +echo "=== Test: External Push ===" +tbd close "$BEAD_2" +tbd sync --external +sleep 2 +GH_STATE=$(gh issue view 2 --repo "$GITHUB_USER/$QA_REPO" --json state --jq '.state') +[[ "$GH_STATE" == "CLOSED" ]] && pass "Push: GH issue closed" || fail "Push: expected CLOSED, got $GH_STATE" + +# ---- Test: Inheritance ---- +echo "" +echo "=== Test: Inheritance ===" +PARENT=$(tbd create "Parent epic" --type epic --external-issue "$ISSUE_3_URL" --json | jq -r '.id') +CHILD=$(tbd create "Child task" --parent "$PARENT" --json | jq -r '.id') +CHILD_URL=$(tbd show "$CHILD" --json | jq -r '.external_issue_url // empty') +[[ "$CHILD_URL" == "$ISSUE_3_URL" ]] && pass "Child inherited URL from parent" || fail "Inheritance failed: $CHILD_URL" + +# ---- Test: Propagation ---- +echo "" +echo "=== Test: Propagation ===" +PROP_PARENT=$(tbd create "Prop parent" --type epic --json | jq -r '.id') +PROP_CHILD=$(tbd create "Prop child" --parent "$PROP_PARENT" --json | jq -r '.id') +tbd update "$PROP_PARENT" --external-issue "$ISSUE_4_URL" +PROP_URL=$(tbd show "$PROP_CHILD" --json | jq -r '.external_issue_url // empty') +[[ "$PROP_URL" == "$ISSUE_4_URL" ]] && pass "Propagation: child got parent URL" || fail "Propagation failed: $PROP_URL" + +# ---- Test: List filter ---- +echo "" +echo "=== Test: List Filter ===" +LINKED_COUNT=$(tbd list --external-issue --json | jq 'length') +TOTAL_COUNT=$(tbd list --json | jq 'length') +[[ "$LINKED_COUNT" -gt 0 && "$LINKED_COUNT" -lt "$TOTAL_COUNT" ]] && \ + pass "List filter: $LINKED_COUNT linked out of $TOTAL_COUNT total" || \ + fail "List filter: linked=$LINKED_COUNT total=$TOTAL_COUNT" + +# ---- Summary ---- +echo "" +echo "===============================" +if [[ $FAILURES -eq 0 ]]; then + echo "ALL TESTS PASSED" +else + echo "$FAILURES TEST(S) FAILED" +fi +echo "===============================" +exit $FAILURES +``` + +Save this script and run it with `GITHUB_USER=yourname ./external-issues-qa.sh`. It +creates a disposable GitHub repo, runs the tests, and offers to clean up afterward. diff --git a/docs/project/research/current/research-github-issue-linking-workflows.md b/docs/project/research/current/research-github-issue-linking-workflows.md new file mode 100644 index 00000000..274ef0ed --- /dev/null +++ b/docs/project/research/current/research-github-issue-linking-workflows.md @@ -0,0 +1,956 @@ +# Research Brief: GitHub Issue, PR, and Project Workflows for External Issue Linking + +**Last Updated**: 2026-02-11 + +**Related**: + +- [External Issue Linking Spec](../../specs/active/plan-2026-02-10-external-issue-linking.md) +- [External Issues QA Plan](../../qa/external-issues.qa.md) +- [tbd Design Doc](../../../packages/tbd/docs/tbd-design.md) + +* * * + +## Executive Summary + +This research brief documents the GitHub Issues, Pull Requests, Labels, and Projects V2 +data models, APIs, workflows, and ecosystem to inform the design and implementation of +tbd’s external issue linking feature. +It covers four areas: + +1. **GitHub data model and APIs** — Issues, PRs, Labels, and Projects V2 internals +2. **Workflow best practices** — How teams use GitHub Issues and Projects effectively +3. **Third-party integrations** — Tools for cross-platform sync (Jira, Linear, etc.) +4. **Alternatives and agent-driven development** — Competing approaches and where + agent-native tools like tbd fit + +The core finding is that GitHub’s issue/PR system is simple but powerful: issues have a +minimal state model (`open`/`closed` with `state_reason`), labels are free-form +per-repository strings, and the REST API is straightforward. +This simplicity makes bidirectional sync between tbd beads and GitHub Issues feasible +with a small, well-defined mapping layer. +The main complexity lies in label auto-creation (GitHub does not auto-create labels when +adding them to issues) and in the new sub-issues/issue types features that are evolving +rapidly. + +Regarding issue/PR workflows in GitHub Projects, the research reveals that **the +dominant pattern is to track issues only** and view linked PRs contextually through +GitHub’s linked PR display feature (badges/indicators on issue cards). +Adding both issues and PRs as separate project items creates “2+ entries for the same +thing” and causes board clutter, which GitHub has confirmed they have “no near-term +plans” to automatically deduplicate. +Teams that need explicit PR tracking typically create filtered views or use enhanced +tools like Zenhub that provide PR-issue synchronization. + +**Research Questions**: + +1. What is the complete GitHub Issues/PRs/Labels data model and API surface? +2. What are the state transitions, and how do they map to tbd bead statuses? +3. How do GitHub Projects V2 relate to issues, and what automation is available? +4. How do teams manage issues and PRs for the same work in GitHub Projects? + Do they track both (duplicative) or just one? + What are the best practices and automations? +5. What third-party tools exist for cross-platform issue sync? +6. What are the emerging alternatives (Linear, Plane, etc.) + and how do they compare? +7. How do agent-driven workflows change the requirements for issue tracking? + +* * * + +## Part 1: GitHub Data Model and APIs + +### 1.1 GitHub Issues + +GitHub Issues are the fundamental work-tracking unit. +Each issue belongs to a single repository and has: + +| Field | Type | Notes | +| --- | --- | --- | +| `number` | integer | Unique per-repo, auto-incremented | +| `title` | string | Required | +| `body` | string (Markdown) | Optional | +| `state` | `open` \| `closed` | Two states only | +| `state_reason` | `completed` \| `not_planned` \| `reopened` \| `null` | Since 2022; ignored unless `state` changes | +| `labels` | string[] | Free-form, per-repo | +| `assignees` | user[] | Up to 10 | +| `milestone` | object \| null | Per-repo milestone | +| `locked` | boolean | Conversation lock | +| `comments` | integer | Comment count | +| `created_at` / `updated_at` / `closed_at` | ISO 8601 | Timestamps | + +**Key design points:** + +- Issues and PRs share a unified number space within a repository (issue #5 and PR #5 + cannot coexist). +- The `pull_request` field on an issue object indicates whether it is actually a PR. + GitHub’s Issues API endpoints work for both issues and PRs for most operations + (labels, assignees, comments). +- There is no organization-level issue concept — issues always belong to a repository. + Teams wanting cross-repo issues typically create a dedicated “issues” repository. + +#### State Model + +The state model is intentionally minimal: + +``` + reopen + ┌──────────────────────┐ + │ │ + ▼ │ + open ──────────────► closed + close │ + ├── state_reason: completed (default) + ├── state_reason: not_planned + └── state_reason: reopened (after reopen+close) +``` + +The `state_reason` field was added in 2022. Before that, all closures were equivalent. +The `duplicate` state_reason exists but is undocumented — it appears when closing via +the “Close as duplicate” UI option. + +**Implications for tbd sync:** + +- tbd has five statuses (`open`, `in_progress`, `blocked`, `deferred`, `closed`). GitHub + has two states. The mapping is necessarily lossy: + - `in_progress` and `blocked` have no GitHub equivalent (both map to `open`) + - `deferred` maps to `closed` with `state_reason: not_planned` + - Reverse mapping must be careful: reopening a `blocked` bead on GitHub should not + change it to `open` (it should stay `blocked`) + +#### REST API Endpoints + +| Operation | Method | Endpoint | +| --- | --- | --- | +| List repo issues | GET | `/repos/{owner}/{repo}/issues` | +| Get single issue | GET | `/repos/{owner}/{repo}/issues/{number}` | +| Create issue | POST | `/repos/{owner}/{repo}/issues` | +| Update issue | PATCH | `/repos/{owner}/{repo}/issues/{number}` | +| Lock conversation | PUT | `/repos/{owner}/{repo}/issues/{number}/lock` | +| Unlock | DELETE | `/repos/{owner}/{repo}/issues/{number}/lock` | + +The list endpoint returns both issues and PRs by default. +Use `?pull_request=false` or check the `pull_request` field to filter. + +**Reference**: [GitHub REST API: Issues](https://docs.github.com/en/rest/issues/issues) + +### 1.2 GitHub Pull Requests + +PRs extend the issue model with merge-specific fields: + +| Field | Type | Notes | +| --- | --- | --- | +| `head` / `base` | branch ref | Source and target branches | +| `mergeable` | boolean \| null | `null` while computing | +| `merged` | boolean | Whether PR was merged | +| `draft` | boolean | Draft PR status | +| `requested_reviewers` | user[] | Review requests | +| `merge_commit_sha` | string \| null | Test merge commit, then actual | + +**Key points:** + +- PRs share the `/issues/` API for labels, assignees, milestones, and comments. + PR-specific operations (merge, reviews, files) use `/pulls/`. +- Cross-repository PRs require `username:branch` notation for the head ref. +- The `mergeable` field is computed asynchronously — it may be `null` on first fetch. + +| Operation | Method | Endpoint | +| --- | --- | --- | +| List PRs | GET | `/repos/{owner}/{repo}/pulls` | +| Get PR | GET | `/repos/{owner}/{repo}/pulls/{number}` | +| Create PR | POST | `/repos/{owner}/{repo}/pulls` | +| Update PR | PATCH | `/repos/{owner}/{repo}/pulls/{number}` | +| Merge PR | PUT | `/repos/{owner}/{repo}/pulls/{number}/merge` | +| List commits | GET | `/repos/{owner}/{repo}/pulls/{number}/commits` | +| List files | GET | `/repos/{owner}/{repo}/pulls/{number}/files` | +| Check merged | GET | `/repos/{owner}/{repo}/pulls/{number}/merged` | + +**Implication for tbd**: The `external_issue_url` field accepts both `/issues/` and +`/pull/` URLs. Since GitHub’s Issues API handles both uniformly for status and label +operations, the sync logic does not need to differentiate. + +**Reference**: [GitHub REST API: Pulls](https://docs.github.com/en/rest/pulls/pulls) + +### 1.3 Labels + +Labels are free-form strings scoped to a repository. +They have: + +| Field | Type | Notes | +| --- | --- | --- | +| `name` | string | Case-sensitive display name | +| `color` | hex string | 6-char hex color (no `#` prefix) | +| `description` | string | Optional description | + +**Key design points:** + +- Labels are **per-repository**, not per-organization or per-project. +- GitHub provides 9 default labels on new repos: `bug`, `documentation`, `duplicate`, + `enhancement`, `good first issue`, `help wanted`, `invalid`, `question`, `wontfix`. +- Labels must exist in a repository before they can be added to an issue. + The API returns **422 Validation Failed** if you try to add a non-existent label. +- Anyone with write access can create labels; triage access allows applying them. + +#### REST API Endpoints + +| Operation | Method | Endpoint | +| --- | --- | --- | +| List repo labels | GET | `/repos/{owner}/{repo}/labels` | +| Create label | POST | `/repos/{owner}/{repo}/labels` | +| Get label | GET | `/repos/{owner}/{repo}/labels/{name}` | +| Update label | PATCH | `/repos/{owner}/{repo}/labels/{name}` | +| Delete label | DELETE | `/repos/{owner}/{repo}/labels/{name}` | +| List issue labels | GET | `/repos/{owner}/{repo}/issues/{number}/labels` | +| Add labels to issue | POST | `/repos/{owner}/{repo}/issues/{number}/labels` | +| Set issue labels | PUT | `/repos/{owner}/{repo}/issues/{number}/labels` | +| Remove all labels | DELETE | `/repos/{owner}/{repo}/issues/{number}/labels` | +| Remove one label | DELETE | `/repos/{owner}/{repo}/issues/{number}/labels/{name}` | + +**Implication for tbd sync**: When pushing labels from a tbd bead to GitHub, the sync +must first ensure the label exists in the repository (create if not, ignore 422 if +already exists), then add it to the issue. +This two-step approach is idempotent. + +**Reference**: [GitHub REST API: Labels](https://docs.github.com/en/rest/issues/labels), +[Managing Labels](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/managing-labels) + +### 1.4 Milestones + +Milestones group issues within a repository by target date: + +| Field | Type | Notes | +| --- | --- | --- | +| `title` | string | Milestone name | +| `state` | `open` \| `closed` | | +| `due_on` | ISO 8601 \| null | Target date | +| `description` | string | Optional | + +- Milestones are per-repository (like labels). +- An issue can have at most one milestone. +- Not currently relevant to tbd sync (no milestone field on beads), but worth noting for + future extensions. + +**Reference**: +[GitHub REST API: Milestones](https://docs.github.com/en/rest/issues/milestones) + +### 1.5 Sub-Issues (New, GA 2025) + +GitHub announced general availability of sub-issues in early 2025, replacing the earlier +Tasklists beta: + +- **Parent-child hierarchy**: Issues can have up to 100 sub-issues, nested up to 8 + levels deep. +- **Progress tracking**: Projects V2 includes a “sub-issue progress” field showing + completion percentage. +- **Slide-out panel**: Sub-issues can be viewed/edited inline from the parent issue + page. +- **API access**: Sub-issues can be managed via GraphQL mutations. + +**Limitations:** + +- Project table/board views do not display nested hierarchies — only flat lists with + “Group by Parent Issue” as a workaround. +- No recursive hierarchy display in project views (highly requested feature, no + timeline). + +**Implication for tbd**: tbd already has parent-child bead relationships with +inheritance. GitHub’s sub-issues are complementary — a tbd epic could link to a GitHub +parent issue, and child beads could link to GitHub sub-issues. +However, this is future work beyond the v1 `external_issue_url` feature. + +**Reference**: +[GitHub Docs: Adding Sub-Issues](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/adding-sub-issues), +[Evolving GitHub Issues (GA)](https://github.com/orgs/community/discussions/154148) + +### 1.6 Issue Types (New, GA 2025) + +GitHub introduced organization-level issue types alongside sub-issues: + +- **Default types**: `task`, `bug`, `feature` — provided out of the box. +- **Custom types**: Organization admins can create custom types (e.g., `spike`, + `initiative`, `epic`). +- **Organization-scoped**: Unlike labels (per-repo), issue types are defined at the + organization level and available across all repos. +- **Projects integration**: The “Type” field can be added to project views for filtering + and grouping. +- **API access**: Available via GraphQL mutations (not yet via `gh` CLI flags). + +**Implication for tbd**: tbd beads already have a `type` field (`task`, `bug`, +`feature`, `epic`). A future enhancement could map tbd types to GitHub issue types, but +this is not needed for v1. + +**Reference**: +[GitHub Docs: Managing Issue Types](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/managing-issue-types-in-an-organization), +[Issue Types Public Preview](https://github.com/orgs/community/discussions/148715) + +### 1.7 GitHub Projects V2 + +Projects V2 is GitHub’s flexible project management layer, replacing the deprecated +“classic” project boards. + +#### Data Model + +| Concept | Description | +| --- | --- | +| **Project** (`ProjectV2`) | Container owned by user or organization | +| **Items** (`ProjectV2Item`) | Issues, PRs, or Draft Issues added to the project | +| **Fields** | Custom metadata on items (text, number, date, single select, iteration) | +| **Views** (`ProjectV2View`) | Saved filter/sort/layout configurations | +| **Workflows** | Built-in automation rules | + +**Field types:** + +| Type | Value Format | Notes | +| --- | --- | --- | +| Text | string | Free-form | +| Number | string (e.g., `"100.1"`) | Numeric as string | +| Date | `YYYY-MM-DD` | ISO date | +| Single Select | option ID | Powers board columns (e.g., "Status") | +| Iteration | iteration ID | Sprint/cycle tracking | + +**Limits**: Up to 50 custom fields per project. +Up to 50,000 items per project (increased from 1,200 in the GA release). + +#### View Layouts + +| Layout | Description | Key Features | +| --- | --- | --- | +| **Table** | Spreadsheet-style | Sort, filter, group, show/hide columns | +| **Board** (Kanban) | Column-based | Drag-and-drop, WIP limits (advisory only), field sums | +| **Roadmap** | Timeline | Date/iteration fields, zoom levels (month/quarter/year) | + +**Board/Kanban specifics:** + +- Columns are derived from a single select field (typically “Status”). +- Drag-and-drop between columns automatically updates the field value. +- Column limits (WIP limits) are **advisory only** — they display a visual indicator but + do not block items from being added. + This is a highly requested enforcement feature with no timeline. +- Horizontal grouping (swim lanes) and slicing (side-panel filtering) are available. + +#### Built-in Automations + +| Trigger | Action | Default? | +| --- | --- | --- | +| Item closed | Set status to "Done" | Yes | +| PR merged | Set status to "Done" | Yes | +| Item added to project | Set status (configurable) | No | +| Item reopened | Set status (configurable) | No | +| Status changed to value | Close/reopen underlying issue | No | + +Additional automation types: + +- **Auto-add**: Automatically adds new items from a repo matching filter criteria. + Limit: 20 auto-add workflows per org. + Does not retroactively add existing items. +- **Auto-archive**: Archives items matching filter criteria (supports `is`, `reason`, + `updated` filters). + +**For anything beyond these**, teams must use GitHub Actions or the GraphQL API. +Built-in workflows cannot trigger on column-to-column moves, custom field changes, or +cross-project events. + +#### GraphQL API + +Authentication requires a classic PAT or GitHub App token with `project` scope. +Fine-grained PATs do not work with the Projects V2 GraphQL API. + +**Key mutations:** + +| Mutation | Purpose | +| --- | --- | +| `addProjectV2ItemById` | Add issue/PR to project | +| `addProjectV2DraftIssue` | Create draft issue | +| `updateProjectV2ItemFieldValue` | Set field value | +| `clearProjectV2ItemFieldValue` | Clear field value | +| `deleteProjectV2Item` | Remove item | +| `archiveProjectV2Item` | Archive item | +| `createProjectV2Field` | Create custom field | +| `updateProjectV2` | Update project settings | +| `createProjectV2` | Create new project | + +**Critical constraint**: You cannot add an item and update its fields in the same API +call. The add must complete first, returning the item ID, before fields can be set. + +**No view mutations exist** — there are no GraphQL mutations for creating, updating, or +deleting views. View management is UI-only. + +**Reference**: +[GitHub Docs: About Projects](https://docs.github.com/en/issues/planning-and-tracking-with-projects/learning-about-projects/about-projects), +[Using the API to Manage Projects](https://docs.github.com/en/issues/planning-and-tracking-with-projects/automating-your-project/using-the-api-to-manage-projects), +[Built-in Automations](https://docs.github.com/en/issues/planning-and-tracking-with-projects/automating-your-project/using-the-built-in-automations) + +* * * + +## Part 2: Workflow Best Practices + +### 2.1 Issue Lifecycle Patterns + +**Simple flow (small teams):** +``` +open → in progress (via assignee + label) → closed (completed) +``` + +**Kanban flow (with Projects V2 board):** +``` +Backlog → Todo → In Progress → In Review → Done +``` +Each stage is a single-select option on the Status field. +Moving cards between columns updates the status automatically. + +**Sprint/iteration flow:** +``` +Backlog → Sprint N (via iteration field) → In Progress → Done +``` +Iteration fields support configurable durations, start dates, and breaks. + +### 2.2 Label Conventions + +Common label taxonomies: + +| Category | Examples | Purpose | +| --- | --- | --- | +| **Type** | `bug`, `feature`, `enhancement`, `question` | What kind of work | +| **Priority** | `P0-critical`, `P1-high`, `P2-medium`, `P3-low` | Urgency | +| **Status** | `needs-triage`, `confirmed`, `wontfix` | Workflow state | +| **Area** | `frontend`, `backend`, `docs`, `infra` | Codebase area | +| **Effort** | `good first issue`, `help wanted` | Contributor signals | + +**Best practice**: Use consistent label naming across repos in an organization. +GitHub does not enforce this — it requires manual discipline or automation (GitHub +Actions that create standard labels on repo creation). + +### 2.3 Cross-Repository Workflows + +GitHub does not natively support cross-repo issues. +Common patterns: + +1. **Dedicated issues repo**: Create `org/issues` or `org/tracker` repo for + organization-wide issues. + Link PRs from code repos using `org/issues#123`. + +2. **Organization-level Projects**: A single project aggregates issues from multiple + repos. This is the primary cross-repo coordination mechanism. + +3. **Issue references**: Use `owner/repo#number` syntax in issue/PR bodies and comments + to create cross-references. + GitHub auto-links these. + +4. **Autoclose via PR**: Including `Fixes owner/repo#123` in a PR description + automatically closes the referenced issue when the PR merges — even cross-repo. + +### 2.4 Automation Patterns with GitHub Actions + +For workflows beyond built-in automations: + +```yaml +# .github/workflows/project-automation.yml +on: + issues: + types: [opened, labeled] + +jobs: + add-to-project: + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v1 + with: + project-url: https://github.com/orgs/myorg/projects/1 + github-token: ${{ secrets.PROJECT_TOKEN }} +``` + +**Authentication note**: The default `GITHUB_TOKEN` cannot access Projects V2. You need +either a GitHub App installation token (recommended for org projects) or a classic PAT +with `project` scope. + +### 2.5 Issue and PR Workflow Patterns in Projects + +A critical workflow question for GitHub Projects is how to manage issues and pull +requests that represent the same work. +This section examines common patterns, the duplication problem, and best practices. + +#### 2.5.1 Common Workflow Patterns + +Teams use three main approaches when managing issues and PRs in GitHub Projects: + +**Pattern A: Issues Only (Most Common)** + +The dominant pattern is to add only issues to GitHub Projects and track linked PRs +contextually through PR indicators/badges that appear on issue cards. + +- Issues move through workflow stages (Backlog → In Progress → Review → Done) +- PRs are linked using keywords (`Fixes #N`) in PR descriptions +- Linked PRs appear as badges/bubbles on issue cards in project views +- Provides “one-click access” to PRs from table/board views +- Shows PR status, assignees, and review state inline + +**Benefits:** +- Reduces board clutter (one item per work unit) +- Maintains single source of truth for work tracking +- Simplifies automation (one item to track through stages) +- Leverages GitHub’s built-in linked PR display feature + +**Drawbacks:** +- PR-specific metadata less prominent +- Requires discipline with linking keywords +- May need separate PR-focused views for code review tracking + +**Pattern B: Both Issues and PRs (Less Common)** + +Some teams add both issues and PRs as separate project items to provide “additional +visibility and traceability” for PRs. + +- Creates duplicate entries for the same work +- Both items appear on project boards +- Requires filtering or separate views to avoid confusion +- May need automation to sync status between linked items + +**Benefits:** +- Maximum visibility and traceability +- Explicit tracking of implementation progress +- Useful for PR review workflows where PR-level detail matters + +**Drawbacks:** +- Creates “2+ entries for the 'same thing'” (GitHub Discussion #4714) +- Can clutter boards and confuse stakeholders +- Requires more complex filtering and view management +- Increases manual management overhead + +**Pattern C: PRs Only (Rare)** + +Less common approach, typically used for maintenance work where PRs don’t need prior +planning issues. + +- PRs are created directly without associated issues +- Suitable for small fixes, dependency updates, etc. +- Skips the planning/tracking phase + +#### 2.5.2 The Duplication Problem + +When teams add both issues and PRs to projects, they encounter several challenges: + +**Visual Clutter**: Project boards show multiple entries for the same work, making it +harder to assess true workload and progress at a glance. + +**No Automatic Deduplication**: GitHub has confirmed they have “no near-term plans” to +automatically collapse linked issues and PRs into single entries (Community Discussion +#4714). + +**Manual Management Overhead**: Teams must establish conventions about which items to +add, when to add them, and how to keep them synchronized. + +**Workarounds Teams Use:** + +1. **Filter views**: Use `-is:pr` to show only issues with linked PR indicators +2. **Separate views**: Create issue-focused and PR-focused views +3. **Automation**: Use workflows to sync status between linked items +4. **Issues-only policy**: Add only issues and access PRs through issue card displays + +#### 2.5.3 Automatic Issue-PR Linking + +GitHub supports automatic issue closing via keywords in PR descriptions: + +| Keyword | Effect | +| --- | --- | +| `Fixes #N` | Closes issue #N when PR merges | +| `Closes #N` | Same as Fixes | +| `Resolves #N` | Same as Fixes | + +These work cross-repo with full URLs: `Fixes https://github.com/owner/repo/issues/N`. + +**Linked PR Display Feature**: GitHub displays linked PRs as badges on issue cards, +showing PR status, assignees, and review state. +This addresses the visibility gap without requiring both as separate project items. + +**What’s NOT Automated** (requires GitHub Actions): +- Moving issue to “In Progress” when PR is attached (highly requested, not built-in) +- Moving issue to “In Review” when PR review is requested +- Removing issue when linked PR is added +- Status changes based on custom field updates + +#### 2.5.4 Best Practices from GitHub and the Community + +**From GitHub’s Official Documentation:** + +- **Issues for Planning, PRs for Implementation**: “All items under In progress or To Do + columns should be GitHub issues, not note cards” +- **Link Issues to PRs**: “If your pull request addresses an issue, link the issue so + that issue stakeholders are aware of the pull request and vice versa” +- **Break Down Work**: “Breaking a large issue into smaller issues makes the work more + manageable and enables team members to work in parallel. + It also leads to smaller pull requests, which are easier to review” +- **Single Source of Truth**: Maintain one location per data point; projects + automatically sync with GitHub data + +**From Practitioner Guides:** + +- **Gitdash Guide**: Issues are the foundational unit of work; use checkboxes for + subtasks within issues; link PRs to issues using keywords to automate closure +- **Zenhub Approach**: “The most effective way to track pull requests is by connecting + them to issues. As you move issues through pipelines, connected PRs move with them + automatically” + +**From Python Core Development Debate:** + +The CPython project requires both issue AND PR for every contribution, leading to +community debate: +- **Arguments FOR**: Separation of concerns, data preservation, narrative integrity +- **Arguments AGAINST**: Overhead burden, redundant documentation +- **Compromise**: `skip issue` label for trivial changes + +#### 2.5.5 Recommendations + +**For Most Teams: Issues Only + Linked PRs** + +``` +Recommended Approach: +- Add issues to GitHub Projects +- Use linking keywords (Fixes #N) in PR descriptions +- Rely on linked PR badges for PR visibility +- Filter with -is:pr to ensure only issues appear +``` + +**Decision Factors:** + +| Factor | Issues Only | Add Both | +| --- | --- | --- | +| **Board Clarity** | High (one item per work) | Low (duplicates) | +| **PR Visibility** | Medium (via badges) | High (explicit items) | +| **Automation Complexity** | Low (one item) | Medium (sync required) | +| **Code Review Tracking** | Basic | Advanced | +| **Stakeholder Communication** | Clear | Can be confusing | + +**For Teams Needing PR Visibility: Add Both + Use Filters** + +``` +Alternative Approach: +- Add both issues and PRs to project +- Create filtered views: issues-only, PRs-only, combined +- Use automation to sync status between linked items +``` + +Use this approach when: +- PR review workflows require explicit tracking +- You need detailed PR-level metrics +- Code review is a distinct tracked phase +- Team size requires visibility into who’s reviewing what + +#### 2.5.6 Helpful Automations + +**Filter Views Strategically:** +- Create “Active Issues” view: `-is:pr status:Open,InProgress` +- Create “Code Review” view: `is:pr status:InReview` +- Create “Recently Completed” view: `status:Done updated:>@today-7d` + +**Use Auto-Archive to Clean Up:** +- Configure auto-archive with `is:merged updated:<@today-14d` to remove old PRs +- Archive completed issues with `reason:completed updated:<@today-1m` + +**Automate Issue-PR Linking with GitHub Actions:** + +```yaml +# .github/workflows/project-automation.yml +on: + issues: + types: [opened, labeled] + pull_request: + types: [opened, ready_for_review] + +jobs: + add-to-project: + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v1 + with: + project-url: https://github.com/orgs/myorg/projects/1 + github-token: ${{ secrets.PROJECT_TOKEN }} +``` + +**Implication for tbd**: When a bead is linked to a GitHub issue and the agent creates a +PR that fixes it, the PR description can include `Fixes ` to auto-close the GitHub +issue on merge. The next `tbd sync --external` would then pull the closed state back to +the bead. The recommended pattern (issues only) aligns well with tbd’s model where beads +are the primary work tracking unit and external issues provide human-readable +visibility. + +* * * + +## Part 3: Third-Party Integrations and Tools + +### 3.1 Jira-GitHub Integration + +The native “GitHub for Jira” app provides **one-way** linking: it shows GitHub commits, +branches, and PRs on Jira tickets. +It does not create bidirectional issue sync. + +For bidirectional sync, third-party tools are required: + +| Tool | Sync Type | Key Features | Pricing | +| --- | --- | --- | --- | +| **Exalate** | Bidirectional | Groovy scripting, field mapping, multi-platform | Per-connection | +| **Unito** | Bidirectional | No-code rules, custom field sync | Per-user | +| **Getint** | Bidirectional | Single app (vs. Exalate's two), unlimited fields | Per-instance | + +**Common sync fields**: status, assignees, comments, labels, priority, due dates, custom +fields. + +**Key challenge**: Jira and GitHub have fundamentally different data models. +Jira has rich workflow states (customizable per project), while GitHub has only +`open`/`closed`. Sync tools must map between these, similar to how tbd maps its five +statuses to GitHub’s two states. + +**Reference**: +[Exalate: Jira GitHub Integration](https://exalate.com/blog/jira-github-issues-integration/), +[Unito: GitHub Jira Integration](https://unito.io/integrations/github-jira/) + +### 3.2 Linear-GitHub Integration + +Linear provides native bidirectional GitHub sync: + +- Merging a GitHub PR can auto-update the Linear issue status. +- Creating a branch from Linear auto-links it to the issue. +- Comments and status changes sync between platforms. + +Linear’s API is GraphQL-based with OAuth refresh tokens (updated 2025/2026). It offers a +faster, more opinionated UX than GitHub Issues but is a separate paid product. + +**Implication for tbd**: Linear is a potential future sync target alongside GitHub. +The `external_issue_url` field could accept Linear URLs +(`https://linear.app/team/issue/TEAM-123`), but the sync logic would need a separate +provider implementation. + +**Reference**: [Linear GitHub Integration](https://linear.app/integrations/github), +[Linear Review 2026](https://work-management.org/software-development/linear-review/) + +### 3.3 Other Integration Tools + +| Tool | Focus | GitHub Support | +| --- | --- | --- | +| **Zapier/Make** | General automation | Webhook-based, no deep field sync | +| **ZenHub** | GitHub-native PM | Adds epics, estimates, sprints to GitHub | +| **GitKraken** | Git client | Issue board integration | +| **Plane** | Open-source PM | GitHub issue sync (emerging) | +| **Shortcut** | PM for software teams | GitHub PR linking | + +### 3.4 GitHub CLI (`gh`) as Integration Layer + +The `gh` CLI is the primary interface tbd uses for GitHub API operations. +Key capabilities: + +```bash +# Issue operations +gh issue create --title "..." --body "..." --label "bug" +gh issue close 123 --reason "completed" +gh issue reopen 123 +gh issue view 123 --json state,stateReason,labels + +# Label operations +gh label create "priority:high" --color "FF0000" +gh api repos/{owner}/{repo}/issues/{number}/labels -f labels[]="bug" + +# PR operations +gh pr create --title "..." --body "..." +gh pr merge 123 --squash + +# Raw API access +gh api repos/{owner}/{repo}/issues/123 --jq '.state' +gh api repos/{owner}/{repo}/issues/123 -f state=closed -f state_reason=completed +``` + +**Authentication**: `gh auth login` handles OAuth flow. +Tokens are stored securely. +The `GH_TOKEN` environment variable can override for CI/automation. + +**Implication for tbd**: All external issue operations in tbd use `gh api` via +`execFile`, following the pattern established in `github-fetch.ts`. This avoids direct +HTTP calls and leverages `gh`'s authentication and error handling. + +* * * + +## Part 4: Alternatives and Agent-Driven Development + +### 4.1 Issue Tracker Landscape + +| Tracker | Model | API | GitHub Integration | Agent-Friendliness | +| --- | --- | --- | --- | --- | +| **GitHub Issues** | Simple (open/closed) | REST + GraphQL | Native | High (gh CLI) | +| **Linear** | Rich (customizable states) | GraphQL | Bidirectional | Medium | +| **Jira** | Complex (custom workflows) | REST | Via plugins | Low (heavy UI) | +| **Plane** | Medium (open-source) | REST | Sync | Medium | +| **Shortcut** | Medium | REST | PR linking | Medium | +| **tbd Beads** | Rich (5 states, deps) | CLI | Planned | Very high | + +### 4.2 Agent-Driven Development Patterns + +AI coding agents (Claude Code, Cursor, Codex, etc.) +change issue tracking requirements: + +1. **CLI-first interfaces**: Agents interact via CLI, not web UI. Tools must have + comprehensive CLI/API coverage. + GitHub Issues + `gh` CLI scores well here. + +2. **Structured output**: Agents need `--json` output for parsing. + GitHub’s `--json` flag and `--jq` filtering are excellent for this. + +3. **Batch operations**: Agents often need to create/update multiple issues in sequence. + GitHub’s API handles this well, though rate limits apply (5,000 requests/hour for + authenticated users). + +4. **Context preservation**: Agents lose context between sessions. + Issue trackers serve as persistent memory. + tbd’s bead system is designed specifically for this — beads persist across agent + sessions via git. + +5. **Dependency tracking**: Agents benefit from knowing which tasks are blocked. + GitHub Issues has no native dependency concept. + tbd beads have first-class dependencies (`tbd dep add`), which is a key + differentiator. + +6. **Auto-discovery**: Agents should be able to find available work without human + guidance. tbd’s `tbd ready` command returns beads that have no blockers — perfect for + autonomous agent workflows. + +### 4.3 Why External Issue Linking Matters for Agents + +The external issue linking feature bridges two worlds: + +- **tbd beads**: Rich, git-native, agent-optimized issue tracking with dependencies, + inheritance, and CLI-first design. +- **GitHub Issues**: The industry-standard, human-readable, web-accessible issue tracker + that teams already use. + +By linking beads to GitHub Issues, agents can: + +1. Work locally with tbd’s rich model (dependencies, inheritance, batch operations) +2. Keep stakeholders informed via GitHub Issues (which they already monitor) +3. Sync status changes bidirectionally without manual intervention +4. Leverage GitHub’s ecosystem (PR auto-close, project boards, notifications) + +This is analogous to how `git` works: developers work locally with rich tooling, then +push to a remote that others consume through a web interface. + +### 4.4 Design Decisions Informed by This Research + +| Decision | Rationale from Research | +| --- | --- | +| Accept both `/issues/` and `/pull/` URLs | GitHub's Issues API handles both uniformly | +| Two-step label creation | GitHub API returns 422 for non-existent labels | +| `state_reason` mapping for deferred | `not_planned` is the closest GitHub equivalent | +| No `in_progress`/`blocked` sync to GitHub | No GitHub equivalent exists | +| Sync-at-sync-time (not on every operation) | Matches git's push/pull model; enables batching | +| Use `gh api` via `execFile` | Leverages existing auth; follows `github-fetch.ts` pattern | +| Advisory-only approach to Projects V2 | No API for view mutations; projects are optional | + +* * * + +## Validated References + +### GitHub Official Documentation + +- [REST API: Issues](https://docs.github.com/en/rest/issues/issues) — Issue CRUD, state, + state_reason +- [REST API: Labels](https://docs.github.com/en/rest/issues/labels) — Label CRUD, issue + label management +- [REST API: Pulls](https://docs.github.com/en/rest/pulls/pulls) — PR CRUD, merge, + reviews +- [REST API: Milestones](https://docs.github.com/en/rest/issues/milestones) — Milestone + management +- [Linking a pull request to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) + — Using keywords and manual linking +- [Best practices for Projects](https://docs.github.com/en/issues/planning-and-tracking-with-projects/learning-about-projects/best-practices-for-projects) + — Planning, breaking down work, single source of truth +- [Planning and tracking with Projects](https://docs.github.com/en/issues/planning-and-tracking-with-projects) + — Overview of Projects functionality +- [Adding items to your project](https://docs.github.com/en/issues/planning-and-tracking-with-projects/managing-items-in-your-project/adding-items-to-your-project) + — How to add issues, PRs, and draft issues +- [Managing Labels](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/managing-labels) + — Label scope, defaults, permissions +- [About Projects](https://docs.github.com/en/issues/planning-and-tracking-with-projects/learning-about-projects/about-projects) + — Projects V2 overview +- [Using the API to Manage Projects](https://docs.github.com/en/issues/planning-and-tracking-with-projects/automating-your-project/using-the-api-to-manage-projects) + — GraphQL mutations/queries +- [Built-in Automations](https://docs.github.com/en/issues/planning-and-tracking-with-projects/automating-your-project/using-the-built-in-automations) + — Default and configurable workflows +- [Automating Projects Using Actions](https://docs.github.com/en/issues/planning-and-tracking-with-projects/automating-your-project/automating-projects-using-actions) + — GitHub Actions integration +- [Adding Items Automatically](https://docs.github.com/en/issues/planning-and-tracking-with-projects/automating-your-project/adding-items-automatically) + — Auto-add workflows +- [Archiving Items Automatically](https://docs.github.com/en/issues/planning-and-tracking-with-projects/automating-your-project/archiving-items-automatically) + — Auto-archive workflows +- [Customizing the Board Layout](https://docs.github.com/en/issues/planning-and-tracking-with-projects/customizing-views-in-your-project/customizing-the-board-layout) + — Kanban WIP limits, column sums +- [Customizing the Roadmap Layout](https://docs.github.com/en/issues/planning-and-tracking-with-projects/customizing-views-in-your-project/customizing-the-roadmap-layout) + — Timeline view +- [Adding Sub-Issues](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/adding-sub-issues) + — Parent-child issue hierarchy +- [Managing Issue Types](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/managing-issue-types-in-an-organization) + — Organization-level types +- [GraphQL Mutations Reference](https://docs.github.com/en/graphql/reference/mutations) + — Full mutation list + +### GitHub Community Discussions + +- [Evolving GitHub Issues and Projects (GA)](https://github.com/orgs/community/discussions/154148) + — Sub-issues, issue types GA announcement +- [Sub-issues Public Preview](https://github.com/orgs/community/discussions/139932) — + Feature details and feedback +- [Issue Types Public Preview](https://github.com/orgs/community/discussions/148715) — + Issue types details +- [Kanban WIP Limits Discussion](https://github.com/orgs/community/discussions/4848) — + Community request for enforced limits +- [ProjectV2View Mutations](https://github.com/orgs/community/discussions/153532) — + Confirmation that view mutations don’t exist +- [Linked Issues/PRs added to a Projects (beta) board should show as a single linked entry · Discussion #4714](https://github.com/orgs/community/discussions/4714) + — Community request for deduplication, GitHub’s response +- [Github Projects workflows - Move issue to In Progress on attaching PR · Discussion #72699](https://github.com/orgs/community/discussions/72699) + — Request for automatic status update when PR attached +- [Feature Request: Show linked pull requests in Projects (beta) · Discussion #5003](https://github.com/orgs/community/discussions/5003) + — Linked PR display feature request and implementation +- [Best Practices for Managing Issues and Pull Requests in an Active Open Source Repo? + · Discussion #163134](https://github.com/orgs/community/discussions/163134) — + Community discussion on issue/PR management patterns + +### Third-Party Integration References + +- [Exalate: Jira GitHub Integration Guide](https://exalate.com/blog/jira-github-issues-integration/) + — Bidirectional sync setup +- [Unito: GitHub Jira Integration](https://unito.io/integrations/github-jira/) — No-code + sync rules +- [Linear GitHub Integration](https://linear.app/integrations/github) — Native + bidirectional sync +- [Linear Review 2026](https://work-management.org/software-development/linear-review/) + — Feature comparison +- [Zenhub: Getting Started with Issues and PR Linking](https://help.zenhub.com/support/solutions/articles/43000487942-getting-started-with-issues-and-pr-linking-in-zenhub) + — Zenhub’s approach to linking issues and PRs +- [Zenhub: Pull Request Tracking](https://support.zenhub.com/article/how-do-i-track-pull-requests) + — How Zenhub handles PR tracking with issues +- [Zenhub: Why Engineering Teams Are Moving Away from GitHub Projects in 2025](https://www.zenhub.com/blog-posts/why-switch-from-github-projects) + — Comparison of GitHub Projects vs. + enhanced tools +- [Gitdash: GitHub Project Management Guide](https://gitdash.dev/blog/github-project-management) + — Best practices for GitHub issue/PR workflows +- [Graphite: How to link pull requests to GitHub issues](https://graphite.com/guides/how-to-link-pull-requests-to-github-issues) + — Linking keywords and automation +- [Graphite: How to use GitHub Projects for tracking code reviews](https://graphite.com/guides/how-to-use-github-projects-for-tracking-code-reviews) + — PR-focused project workflows + +### Technical Blog Posts + +- [DevOps Journal: GitHub GraphQL ProjectsV2 Examples](https://devopsjournal.io/blog/2022/11/28/github-graphql-queries) + — Practical query/mutation examples +- [Some Natalie: Intro to GraphQL with GitHub Projects](https://some-natalie.dev/blog/graphql-intro/) + — Custom fields via API +- [Den Delimarsky: Programmatically Setting Issue Types](https://den.dev/blog/set-github-issue-type/) + — GraphQL workaround for issue types +- [InfoQ: Evolving GitHub Issues: Enhancing Project Management for Developers](https://www.infoq.com/news/2025/02/github-issues/) + — Recent updates to GitHub Issues and Projects (2025) +- [Onenine: Best Practices for Using Git with Kanban Boards](https://onenine.com/best-practices-for-using-git-with-kanban-boards/) + — Integrating git workflows with project boards + +### Community Discussions and Forums + +- [Python.org: Questioning necessity of having both a GitHub issue and PR](https://discuss.python.org/t/questioning-necessity-of-having-both-a-github-issue-and-pr/26776) + — CPython workflow debate: arguments for and against requiring both + +### GitHub Marketplace + +- [actions/add-to-project](https://github.com/actions/add-to-project) — Official GitHub + Action for automating project item addition diff --git a/docs/project/specs/active/plan-2026-02-10-external-issue-linking.md b/docs/project/specs/active/plan-2026-02-10-external-issue-linking.md new file mode 100644 index 00000000..6b650a4c --- /dev/null +++ b/docs/project/specs/active/plan-2026-02-10-external-issue-linking.md @@ -0,0 +1,1245 @@ +# Feature: External Issue Linking + +**Date:** 2026-02-10 (last updated 2026-02-10) + +**Author:** Joshua Levy / Claude + +**Status:** Draft + +## Overview + +Add support for optionally linking tbd beads to external issue tracker issues, starting +with GitHub Issues as the v1 provider. +This enables bidirectional status sync, label sync, and provides easy clickable URLs to +the external issue from any bead rendering. + +The feature follows the same architectural pattern as `spec_path` linking: an optional +field on the bead, with inheritance from parent epics to child beads, and propagation on +updates. + +## Goals + +- **Full backward compatibility**: The feature must be completely inert unless + explicitly activated by linking a bead to a GitHub issue or PR via `--external-issue`. + No behavioral changes to existing commands, no external API calls, no output changes + (except `tbd doctor` integration checks and `tbd sync --help` text) when the feature + is not in use. Users who never use `--external-issue` should see zero difference in + behavior. +- Add an optional `external_issue_url` field to the bead schema for linking to external + issue tracker URLs (GitHub Issues and PRs for v1). Despite the field name saying + “issue,” it accepts both issues and PRs since GitHub’s API treats them uniformly. +- Parse and validate GitHub issue and PR URLs to extract owner, repo, and number +- Verify at link time that the issue/PR exists and is accessible via `gh` CLI +- Inherit external issue links from parent beads to children (same pattern as + `spec_path`) +- Propagate external issue link changes from parent to children +- Sync bead status changes to linked GitHub issues (closing a bead closes the GitHub + issue) +- Sync label changes bidirectionally between beads and GitHub issues +- Ensure `gh` CLI availability is checked in health/doctor commands +- Update the design doc to reflect the new field, status mapping, and sync behaviors + +## Non-Goals + +- Full bidirectional comment sync (future GitHub Bridge feature) +- Webhook-driven real-time sync (future enhancement; this is CLI-triggered sync) +- Support for non-GitHub providers in v1 (Jira, Linear, etc. + are future work) +- Required linking (it remains optional, like `spec_path`) +- Multiple external issue links per bead (single URL is sufficient for v1) +- Automatic issue creation on GitHub when a bead is created (manual linking only) + +## Background + +### Current State + +Beads have rich metadata including `spec_path` for linking to spec documents, but no way +to link to external issue trackers. +The design doc (§8.7) describes external issue tracker linking as a planned future +feature, recommending a `linked` metadata structure with provider-specific fields. + +**Existing patterns we build on:** + +1. **`spec_path` field and inheritance** (`schemas.ts:149-150`, `create.ts:113-119`, + `update.ts:151-164`): Optional string field with parent-to-child inheritance on + create and propagation on update. + This is the direct template for `external_issue_url`. + +2. **GitHub URL parsing** (`github-fetch.ts`): Existing regex patterns for parsing + GitHub blob/raw URLs. + No issue URL parsing exists yet, but the pattern is established. + +3. **`gh` CLI availability** (`setup.ts`, `ensure-gh-cli.sh`): The `use_gh_cli` config + setting and SessionStart hook ensure `gh` CLI is installed. + But the `doctor` command does not currently check for `gh` availability. + +4. **Merge strategy** (`git.ts:277-308`): `spec_path` uses `lww` (last-write-wins). + The same strategy applies to `external_issue_url`. + +5. **Design doc §8.7** (`tbd-design.md:5717-5779`): Describes the metadata model for + external issue linking with `linked` array and provider-specific fields. + +### GitHub Issues: States and Labels + +GitHub Issues have a simple state model: + +| `state` | `state_reason` | Meaning | +| --- | --- | --- | +| `open` | `null` | Issue is open | +| `open` | `reopened` | Issue was reopened | +| `closed` | `completed` | Closed as done/resolved (default) | +| `closed` | `not_planned` | Closed as won't fix / not planned | +| `closed` | `duplicate` | Closed as duplicate (undocumented) | + +GitHub labels are free-form strings attached to issues, similar to tbd labels. + +### tbd Bead Status States + +| Status | Meaning | +| --- | --- | +| `open` | Not started | +| `in_progress` | Actively being worked on | +| `blocked` | Waiting on a dependency | +| `deferred` | Postponed | +| `closed` | Complete | + +### Status Mapping: tbd → GitHub + +The following mapping defines how bead status changes propagate to linked GitHub issues. +This mapping is defined in one place and could be extended for other providers in the +future. + +| tbd Status | GitHub Action | GitHub State | GitHub `state_reason` | +| --- | --- | --- | --- | +| `open` | Reopen issue (if closed) | `open` | — | +| `in_progress` | Reopen issue (if closed) | `open` | — | +| `blocked` | No change | — | — | +| `deferred` | Close as not planned | `closed` | `not_planned` | +| `closed` | Close as completed | `closed` | `completed` | + +### Status Mapping: GitHub → tbd + +When pulling from GitHub during `tbd sync --external`, the reverse mapping: + +| GitHub State | GitHub `state_reason` | tbd Status | +| --- | --- | --- | +| `open` | `null` or `reopened` | `open` (only if bead is `closed` or `deferred`) | +| `closed` | `completed` | `closed` | +| `closed` | `not_planned` | `deferred` | +| `closed` | `duplicate` | `closed` | + +Note: `blocked` and `in_progress` have no GitHub equivalent. +If GitHub reopens an issue that was `in_progress`, the bead stays `in_progress`. If +GitHub closes an issue that was `blocked`, the bead moves to `closed`. + +### Label Mapping + +Labels sync bidirectionally: + +- **tbd → GitHub**: At sync time, labels added/removed on a bead since last sync are + pushed to the linked GitHub issue. +- **GitHub → tbd**: At sync time, labels added/removed on the GitHub issue since last + sync are pulled into the bead. +- Labels are matched by exact string equality. +- Label sync is additive for union merges: if both sides add different labels, both end + up with the union. + +**Label auto-creation on GitHub**: The GitHub API does NOT auto-create labels when +adding them to an issue. +If a tbd bead has a label that doesn’t exist as a GitHub repo label, we must create it +first. The implementation should: + +1. Attempt `POST /repos/{owner}/{repo}/labels` with the label name (use a default + color). If the label already exists, GitHub returns 422 — ignore that error. +2. Then `POST /repos/{owner}/{repo}/issues/{number}/labels` to add it to the issue. + +This two-step approach is idempotent and handles both new and existing labels. + +## Design + +### Approach + +1. **New schema field**: Add `external_issue_url` as an optional nullable string field + on `IssueSchema`, following the `spec_path` pattern. + +2. **GitHub URL parser**: Create a `github-issues.ts` module with functions to parse + GitHub issue and PR URLs, extract `{owner, repo, number}`, validate via `gh` CLI, and + perform status/label operations. + Both `/issues/` and `/pull/` URL forms are accepted since GitHub’s `/issues/` API + handles both. + +3. **Generic inheritable field system**: Extract the existing `spec_path` + parent-to-child inheritance logic into a reusable module that any field can opt into. + Both `spec_path` and `external_issue_url` use this shared logic — no copy-pasting of + inheritance code. + +4. **Sync-at-sync-time**: External issue sync happens only when `tbd sync` is called, + not on individual bead operations. + This makes local operations a staging area — you can create, update, close, and label + beads freely, then sync everything in one batch. + This mirrors how `--issues` syncs to the git sync branch and `--docs` syncs doc + caches: `--external` syncs to linked external issues. + +5. **Bidirectional status and label sync**: At sync time, push local status/label + changes to GitHub and pull GitHub status/label changes to local beads, using the + mapping tables. + +6. **Doctor check**: Add a `gh` CLI availability check to the `doctor` command. + +### Components + +| Component | File(s) | Purpose | +| --- | --- | --- | +| Schema | `schemas.ts` | Add `external_issue_url` field | +| Inheritable fields | `inheritable-fields.ts` (new) | Generic parent→child field inheritance/propagation | +| URL Parser | `github-issues.ts` (new) | Parse, validate, and operate on GitHub issues | +| Create | `create.ts` | `--external-issue` flag, uses inheritable fields | +| Update | `update.ts` | `--external-issue` flag, uses inheritable fields | +| Show | `show.ts` | Display external issue URL with highlighting | +| List | `list.ts` | `--external-issue` filter option | +| Sync | `sync.ts` | Add `--external` scope for external issue sync | +| Doctor | `doctor.ts` | Add `gh` CLI health check | +| Merge rules | `git.ts` | Add `external_issue_url: 'lww'` | +| Status mapping | `github-issues.ts` | Hardcoded mapping table | + +### Schema Changes + +Add to `IssueSchema` in `packages/tbd/src/lib/schemas.ts`: + +```typescript +// External issue linking - URL to linked external issue (e.g., GitHub Issues) +external_issue_url: z.string().url().nullable().optional(), +``` + +### GitHub Issue URL Parsing + +New module `packages/tbd/src/file/github-issues.ts`: + +```typescript +// Matches: https://github.com/{owner}/{repo}/issues/{number} +const GITHUB_ISSUE_RE = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)$/; + +interface GitHubIssueRef { + owner: string; + repo: string; + number: number; + url: string; +} + +function parseGitHubIssueUrl(url: string): GitHubIssueRef | null; +function isGitHubIssueUrl(url: string): boolean; +async function validateGitHubIssue(ref: GitHubIssueRef): Promise; +async function closeGitHubIssue(ref: GitHubIssueRef, reason: 'completed' | 'not_planned'): Promise; +async function reopenGitHubIssue(ref: GitHubIssueRef): Promise; +async function addGitHubLabel(ref: GitHubIssueRef, label: string): Promise; +async function removeGitHubLabel(ref: GitHubIssueRef, label: string): Promise; +async function getGitHubIssueState(ref: GitHubIssueRef): Promise<{state: string, state_reason: string | null, labels: string[]}>; +``` + +All GitHub API operations use `gh api` via child process, leveraging the existing `gh` +CLI that `ensure-gh-cli.sh` installs. + +### Generic Inheritable Field System + +Currently, `spec_path` inheritance is implemented with inline logic in `create.ts` +(lines 113-119) and `update.ts` (lines 94-104, 151-164). Rather than copy-pasting this +logic for `external_issue_url`, we extract it into a generic system. + +New module `packages/tbd/src/lib/inheritable-fields.ts`: + +```typescript +import type { Issue } from './types.js'; + +/** + * Configuration for a field that inherits from parent to child beads. + * All inheritable fields follow the same three rules: + * + * 1. On create with --parent: if the field is not explicitly set, inherit + * from parent. + * 2. On re-parenting: if the field is not explicitly set and the child has + * no value, inherit from new parent. + * 3. On parent update: if the field changes on the parent, propagate to + * children whose field is null or matches the old value. + */ +interface InheritableFieldConfig { + /** The field name on the Issue type */ + field: keyof Issue; +} + +/** Registry of all inheritable fields */ +const INHERITABLE_FIELDS: InheritableFieldConfig[] = [ + { field: 'spec_path' }, + { field: 'external_issue_url' }, +]; + +/** + * Inherit fields from a parent issue to a child issue being created. + * For each inheritable field: if the child has no explicit value, + * copy from parent. + */ +function inheritFromParent( + child: Partial, + parent: Issue, + explicitlySet: Set, +): void; + +/** + * Propagate field changes from a parent to its children. + * For each inheritable field that changed on the parent: + * update children whose value is null or matches the old value. + */ +async function propagateToChildren( + parent: Issue, + oldValues: Partial, + children: Issue[], + writeIssueFn: (issue: Issue) => Promise, +): Promise; +``` + +**Key design points:** + +- `INHERITABLE_FIELDS` is the single registry. + Adding a new inheritable field means adding one entry here — no other code changes + needed for inheritance. +- The `create` and `update` commands call these shared functions instead of inline + field-specific logic. +- The existing `spec_path` inline logic is refactored to use this system as part of this + feature (not just `external_issue_url`). +- `explicitlySet` tracks which fields the user provided via CLI flags, so we only + inherit fields the user didn’t explicitly set. + +**Three inheritance rules (same for all fields):** + +1. **On create with `--parent`**: If the field was not explicitly provided via a CLI + flag but the parent has a value, the child inherits it. +2. **On re-parenting** (via `update --parent`): If the field was not explicitly provided + and the child’s current value is null, inherit from the new parent. +3. **On parent field update**: When a parent’s inheritable field changes, propagate to + all children whose field is null or still matches the old (inherited) value. + Children with explicitly different values are untouched. + +### API Changes + +#### CLI Flags + +| Command | New Flag | Purpose | +| --- | --- | --- | +| `tbd create` | `--external-issue ` | Link to external issue | +| `tbd update` | `--external-issue ` | Set/change external issue link | +| `tbd list` | `--external-issue [url]` | Filter by external issue link | +| `tbd show` | (no flag; auto-displayed) | Shows URL in output | + +#### Merge Rules Addition + +In `git.ts` `FIELD_STRATEGIES`: +```typescript +external_issue_url: 'lww', +``` + +### Sync Architecture: Scoped Sync with Staging + +**Key principle**: Individual bead operations (`create`, `update`, `close`, `label add`, +etc.) only modify the local data. +No external side effects. +External sync happens only when `tbd sync` is called. + +This means the local worktree acts as a **staging area**. You can make a batch of +changes — close several beads, add labels, update statuses — and none of it touches +GitHub until you explicitly sync. +This also means you can abort changes (e.g., via workspace operations) before they’re +pushed externally. + +**Existing sync scopes** (`sync.ts`): + +| Flag | Scope | What it syncs | +| --- | --- | --- | +| `--issues` | Git sync branch | Push/pull bead data via `tbd-sync` branch | +| `--docs` | Doc cache | Sync docs from configured sources | + +**New sync scope**: + +| Flag | Scope | What it syncs | +| --- | --- | --- | +| `--external` | External issues | Push/pull status and labels to/from linked GitHub issues | + +**Default behavior**: `tbd sync` (no flags) syncs all scopes: issues, docs, and +external. Selective flags (`--issues`, `--docs`, `--external`) let you choose which +scopes to sync. + +**Full sync ordering** (when `tbd sync` runs all scopes): + +The sync phases are ordered so that on failure at any step, the data is in a sane, +consistent state. The key insight: pull from external sources *before* committing issues +to git, and push to external sources *after* committing to git. + +``` +Phase 1: Pull from external → local beads (external-pull) +Phase 2: Sync docs (docs) +Phase 3: Sync issues to git (push/pull) (issues) +Phase 4: Push from local beads → external (external-push) +``` + +**Why this order:** + +- **External-pull first** (Phase 1): Captures any changes made on GitHub (status + changes, label changes by collaborators) into local beads before those beads are + committed to the git sync branch. + This means the git commit reflects the latest merged state from all sources. + +- **Issues sync in the middle** (Phase 3): After pulling external changes, commit the + local bead data (which now includes merged external state) to the git sync branch. + If this fails (e.g., merge conflict, push rejection), the external state has already + been captured locally but nothing has been pushed externally yet — a safe state. + +- **External-push last** (Phase 4): Only after the local bead state has been + successfully committed to git do we push changes to external trackers. + This means the git sync branch is the source of truth. + If the external push fails partway through, the git state is already consistent and + the next sync will retry the external push. + +**If any phase fails**, subsequent phases still attempt to run (best-effort), but the +overall sync returns a non-zero exit code. + +**External sync flow** (detail of Phases 1 and 4): + +*Phase 1 — External-pull:* +1. Find all beads with a non-null `external_issue_url` +2. For each linked bead, fetch current GitHub issue state via `gh api` +3. If GitHub state differs from bead, apply the reverse mapping: + - Update bead status per the GitHub → tbd mapping table + - Pull label changes from GitHub into the bead +4. Conflict resolution: If both sides changed since last sync, local wins (consistent + with LWW merge strategy). + The losing value is logged. + +*Phase 4 — External-push:* +1. For each bead with a non-null `external_issue_url` +2. If bead status or labels differ from what GitHub has (based on the state fetched in + Phase 1), push local changes to GitHub: + - Map bead status to GitHub state and update via `gh api` + - Sync label diff to GitHub (with auto-creation as needed) +3. Report a summary of synced issues (e.g., “Synced 3 external issues: 2 pushed, 1 + pulled, 1 unchanged”) + +### `use_gh_cli` Configuration Gate + +All external issue features require the GitHub CLI (`gh`). The existing `use_gh_cli` +config setting (in `.tbd/config.yml` under `settings:`, default `true`) serves as the +master gate for all external issue functionality. + +**When `use_gh_cli` is `false`:** + +- `--external-issue` flag on `create`/`update` is **rejected** with a clear error: + "External issue linking requires GitHub CLI. Set `use_gh_cli: true` in config or run + `tbd setup --auto`." +- `tbd sync --external` is a **no-op** with a warning: “External sync skipped: GitHub + CLI is disabled (use_gh_cli: false).” +- `tbd sync` (no flags) silently skips the external sync phases (phases 1 and 4) — + issues and docs sync still run normally. +- The `external_issue_url` field on the schema is unaffected — beads may still have the + field populated (e.g., from a collaborator who has `gh` enabled), but no sync or + validation occurs locally. +- The `doctor` command’s `gh` CLI check reports “skipped” rather than a warning when + `use_gh_cli` is `false`. + +**When `use_gh_cli` is `true` (default):** + +- All external issue features are available. +- The `--external-issue` flag validates the URL and verifies the issue exists via + `gh api`. +- `tbd sync` includes the external sync phases. +- The `doctor` command checks `gh` availability and auth status. + +This gating behavior must be clearly documented in: +- CLI `--help` text for `--external-issue` flags +- Error messages (always suggest how to enable) +- The design doc §8.7 +- The README (GitHub authentication section) +- The `setup-github-cli` shortcut + +### Error Handling + +- At link time (`--external-issue` on `create`/`update`): + - If `use_gh_cli` is `false`, reject immediately with a clear error. + - If `use_gh_cli` is `true`, validate the URL format first (must be a full GitHub + issue or PR URL like `https://github.com/owner/repo/issues/123` or `.../pull/456`), + then verify the target exists via `gh api`. Clear error messages for each failure + mode: + - Not a URL → "Invalid URL. Expected a GitHub issue or pull request URL like + https://github.com/owner/repo/issues/123" + - Non-GitHub URL → "Only GitHub issue and pull request URLs are supported. + Expected: https://github.com/owner/repo/issues/123" + - Valid URL but 404 → "Issue or pull request not found or not accessible. + Check the URL and your GitHub authentication (`gh auth status`)." +- During `tbd sync --external`: + - If `use_gh_cli` is `false`, skip with warning (see above). + - If `gh` CLI is not installed or not authenticated, log a warning and skip external + sync. Return non-zero exit code. + - If a GitHub API call fails for a specific issue (network error, auth error, + permission error), log the error for that issue, continue syncing other issues, and + return non-zero exit code at the end. + - Individual issue sync failures do not block other issues from syncing. +- All errors are surfaced to the user, never silently swallowed. + +## Implementation Plan + +Each phase lists the exact source files, line numbers, and nature of changes. +Line numbers are approximate (based on current state) and may shift as earlier phases +are implemented. + +* * * + +### Phase 1: Schema, URL Parsing, Inheritable Fields, and Core Linking + +Add the field, extract inheritable field logic, parse GitHub URLs, validate issues, and +wire up the basic create/update/show/list functionality. +No status or label sync yet. + +#### 1a. Schema and Merge Rules + +**`packages/tbd/src/lib/schemas.ts`** (line 149, after `spec_path`): +- [ ] Add field to `IssueSchema`: + ```typescript + external_issue_url: z.string().url().nullable().optional(), + ``` +- [ ] This auto-propagates to the `Issue` type via `types.ts:28` + (`type Issue = z.infer`) + +**`packages/tbd/src/file/git.ts`** (line 300, after `spec_path: 'lww'`): +- [ ] Add merge rule: + ```typescript + external_issue_url: 'lww', + ``` + +**`packages/tbd/src/lib/types.ts`** (lines 81-91, 96-109): +- [ ] Add `external_issue_url?: string | null` to `CreateIssueOptions` +- [ ] Add `external_issue_url?: string | null` to `UpdateIssueOptions` + +**Tests:** +- [ ] Add `external_issue_url` validation cases to `schemas.test.ts` +- [ ] Add LWW merge test for `external_issue_url` in `git.test.ts` + +#### 1b. GitHub Issue URL Parser + +**`packages/tbd/src/file/github-issues.ts`** (NEW FILE): + +Place alongside `github-fetch.ts` (existing GitHub URL utilities). + +```typescript +// Regex: https://github.com/{owner}/{repo}/issues/{number} +const GITHUB_ISSUE_RE = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)$/; + +// Also need a PR detection regex for better error messages +const GITHUB_PR_RE = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)$/; + +interface GitHubIssueRef { owner: string; repo: string; number: number; url: string; } + +export function parseGitHubIssueUrl(url: string): GitHubIssueRef | null; +export function isGitHubIssueUrl(url: string): boolean; +export function isGitHubPrUrl(url: string): boolean; +export function formatGitHubIssueRef(ref: GitHubIssueRef): string; // → "owner/repo#123" + +// gh api operations (all use execFile('gh', [...]) like github-fetch.ts:16) +export async function validateGitHubIssue(ref: GitHubIssueRef): Promise; +export async function getGitHubIssueState(ref: GitHubIssueRef): Promise<{ + state: string; state_reason: string | null; labels: string[]; +}>; +export async function closeGitHubIssue( + ref: GitHubIssueRef, reason: 'completed' | 'not_planned' +): Promise; +export async function reopenGitHubIssue(ref: GitHubIssueRef): Promise; +export async function addGitHubLabel(ref: GitHubIssueRef, label: string): Promise; +export async function removeGitHubLabel(ref: GitHubIssueRef, label: string): Promise; +``` + +**Pattern reference:** `github-fetch.ts:12-16` uses `execFile` + `promisify` for +`gh api` calls. Follow same pattern. + +**`packages/tbd/src/file/github-issues.test.ts`** (NEW FILE): +- [ ] URL parsing: valid URLs, trailing slash, query params, PR URLs, blob URLs, + non-GitHub, malformed, no issue number, non-numeric +- [ ] Format: `parseGitHubIssueUrl` → correct owner/repo/number +- [ ] Detection: `isGitHubIssueUrl()`, `isGitHubPrUrl()` + +#### 1c. Generic Inheritable Field System + +**`packages/tbd/src/lib/inheritable-fields.ts`** (NEW FILE): + +```typescript +import type { Issue } from './types.js'; + +interface InheritableFieldConfig { + field: keyof Issue; +} + +export const INHERITABLE_FIELDS: InheritableFieldConfig[] = [ + { field: 'spec_path' }, + { field: 'external_issue_url' }, +]; + +export function inheritFromParent( + child: Partial, + parent: Issue, + explicitlySet: Set, +): void; + +export async function propagateToChildren( + parent: Issue, + oldValues: Partial>, + children: Issue[], + writeIssueFn: (issue: Issue) => Promise, +): Promise; // returns count of updated children +``` + +**`packages/tbd/src/lib/inheritable-fields.test.ts`** (NEW FILE): +- [ ] `inheritFromParent()` copies registered fields from parent when not explicitly set +- [ ] `inheritFromParent()` does NOT overwrite explicitly set fields +- [ ] `propagateToChildren()` updates children with null or old-matching values +- [ ] `propagateToChildren()` skips children with different values +- [ ] Both `spec_path` and `external_issue_url` are exercised + +#### 1d. Refactor `create.ts` to Use Inheritable Fields + +**`packages/tbd/src/cli/commands/create.ts`**: + +Current inline `spec_path` inheritance (lines 113-119): +```typescript +// Inherit spec_path from parent if not explicitly provided +if (!specPath && parentId) { + const parentIssue = await readIssue(dataSyncDir, parentId); + if (parentIssue.spec_path) { + specPath = parentIssue.spec_path; + } +} +``` + +Changes needed: +- [ ] **Line 29-41** (`CreateOptions` interface): Add `externalIssue?: string` +- [ ] **Lines 67-76** (spec validation block): Add parallel validation block for + `--external-issue`: + - Read config to check `use_gh_cli` (`readConfig` already imported, line 26) + - If `use_gh_cli` is false, throw `ValidationError` with clear message + - Parse URL via `parseGitHubIssueUrl()` + - Validate issue exists via `validateGitHubIssue()` +- [ ] **Lines 113-119** (spec_path inheritance): Replace with call to + `inheritFromParent()` from `inheritable-fields.ts`. Pass a `Set` tracking + which fields the user explicitly provided (`spec_path` if `--spec` was given, + `external_issue_url` if `--external-issue` was given). +- [ ] **Line 138** (`spec_path: specPath`): Add `external_issue_url` to the issue data + object +- [ ] **Line 201** (Commander `.option('--spec ...')`): Add: + ```typescript + .option('--external-issue ', + 'Link to GitHub issue or PR (e.g., https://github.com/owner/repo/issues/123). Requires use_gh_cli: true') + ``` + +#### 1e. Refactor `update.ts` to Use Inheritable Fields + +**`packages/tbd/src/cli/commands/update.ts`**: + +Current inline logic: +- `spec_path` re-parenting inheritance (lines 94-104) +- `spec_path` propagation to children (lines 151-164) + +Changes needed: +- [ ] **Line 30-41** (`UpdateOptions` interface): Add `externalIssue?: string` +- [ ] **Lines 76-77** (`oldSpecPath` capture): Generalize to capture old values for all + inheritable fields: + ```typescript + const oldInheritableValues: Partial> = {}; + for (const config of INHERITABLE_FIELDS) { + oldInheritableValues[config.field] = issue[config.field]; + } + ``` +- [ ] **Line 90** (`spec_path` update): Add `external_issue_url` update alongside: + ```typescript + if (updates.external_issue_url !== undefined) + issue.external_issue_url = updates.external_issue_url; + ``` +- [ ] **Lines 94-104** (re-parenting inheritance): Replace inline `spec_path`-specific + logic with `inheritFromParent()` call. + Track `explicitlySet` from CLI flags. +- [ ] **Lines 151-164** (propagation to children): Replace inline `spec_path`-specific + logic with `propagateToChildren()` call. + Pass `oldInheritableValues` and write function. +- [ ] **Lines 371-383** (spec CLI option handling in `buildUpdates`): Add parallel block + for `--external-issue`: + - If non-empty: validate URL format, check `use_gh_cli`, validate via `gh api`, set + `updates.external_issue_url` + - If empty string: set `updates.external_issue_url = null` (clear) +- [ ] **Line 437** (Commander `.option('--spec ...')`): Add: + ```typescript + .option('--external-issue ', + 'Set or clear external issue (empty clears). Requires use_gh_cli: true') + ``` + +#### 1f. Update `show.ts` Display + +**`packages/tbd/src/cli/commands/show.ts`** (lines 69-70): + +Current `spec_path` highlighting: +```typescript +} else if (line.startsWith('spec_path:')) { + console.log(`${colors.dim('spec_path:')} ${colors.id(line.slice(11))}`); +``` + +- [ ] Add after line 70: + ```typescript + } else if (line.startsWith('external_issue_url:')) { + console.log(`${colors.dim('external_issue_url:')} ${colors.id(line.slice(20))}`); + ``` + +#### 1g. Add `--external-issue` Filter to `list.ts` + +**`packages/tbd/src/cli/commands/list.ts`**: + +- [ ] **Line 33-50** (`ListOptions` interface): Add `externalIssue?: string` +- [ ] **Lines 192-260** (`filterIssues` method): Add filter block after the `spec` + filter (lines 247-251): + ```typescript + // External issue filter + if (options.externalIssue) { + if (!issue.external_issue_url) return false; + // If a specific URL is given, match it; otherwise just filter for + // any linked issue + if (options.externalIssue !== 'true' && + issue.external_issue_url !== options.externalIssue) { + return false; + } + } + ``` +- [ ] **Line 96** (`displayIssues` mapping): Add + `external_issue_url: i.external_issue_url ?? undefined` after `spec_path` +- [ ] **Lines 289-316** (Commander options): Add after `--spec`: + ```typescript + .option('--external-issue [url]', + 'Filter by external issue (URL optional, shows all linked if no URL given)') + ``` + +#### 1h. Golden Tests + +**`packages/tbd/tests/cli-external-issue-linking.tryscript.md`** (NEW FILE): +- [ ] Basic CRUD with `--external-issue` +- [ ] URL validation error scenarios (PR URL, non-GitHub, malformed, etc.) +- [ ] Show displays URL, list filters by URL + +**`packages/tbd/tests/cli-inheritable-fields.tryscript.md`** (NEW FILE): +- [ ] 5 scenarios from Testing Strategy section (parent-to-child, propagation, + re-parenting, clearing, mixed inheritance) +- [ ] Both `spec_path` and `external_issue_url` exercised in each scenario + +#### 1i. Verification + +- [ ] All existing `spec_path` tryscript tests pass (the refactor to + `inheritable-fields.ts` must be behavior-preserving) +- [ ] `pnpm test` passes +- [ ] `pnpm build` passes + +* * * + +### Phase 2: `gh` CLI Health Check and Setup Validation + +Ensure `gh` CLI availability is verified in `doctor` and that the setup flow properly +validates GitHub access. + +**`packages/tbd/src/cli/commands/doctor.ts`**: + +- [ ] **Lines 136-142** (integration checks): Add a third integration check: + ```typescript + // Integration 3: GitHub CLI (gh) + integrationChecks.push(await this.checkGhCli()); + ``` +- [ ] Add new method `checkGhCli()` (after `checkCodexAgents()`, line ~602): + ```typescript + private async checkGhCli(): Promise { + // If use_gh_cli is false, report as skipped + if (this.config?.settings?.use_gh_cli === false) { + return { + name: 'GitHub CLI (gh)', + status: 'ok', + message: 'disabled (use_gh_cli: false)', + }; + } + // Check if gh is available + try { + await execFileAsync('gh', ['--version']); + } catch { + return { + name: 'GitHub CLI (gh)', + status: 'warn', + message: 'not found in PATH', + suggestion: 'Run: tbd setup --auto, or set use_gh_cli: false', + }; + } + // Check auth + try { + await execFileAsync('gh', ['auth', 'status']); + return { name: 'GitHub CLI (gh)', status: 'ok' }; + } catch { + return { + name: 'GitHub CLI (gh)', + status: 'warn', + message: 'not authenticated', + suggestion: 'Run: gh auth login, or set GH_TOKEN env var', + }; + } + } + ``` +- [ ] **Line 10** (imports): Add `execFile` from `node:child_process` and `promisify` + from `node:util` (or reuse from git.ts) + +**Tests:** +- [ ] Add `checkGhCli` test cases to doctor tests: gh missing, gh unauthenticated, gh + available, use_gh_cli=false +- [ ] Verify existing doctor tests still pass + +* * * + +### Phase 3: External Sync Scope and Status Sync + +Add `--external` scope to `tbd sync` and implement bidirectional status sync with the +correct two-phase ordering (pull before git commit, push after). + +#### 3a. Sync Scope Changes + +**`packages/tbd/src/cli/commands/sync.ts`**: + +- [ ] **Lines 58-65** (`SyncOptions` interface): Add `external?: boolean`: + ```typescript + interface SyncOptions { + push?: boolean; + pull?: boolean; + local?: boolean; + issues?: boolean; + docs?: boolean; + external?: boolean; // NEW + } + ``` +- [ ] **Lines 89-103** (scope selection logic): Extend to handle `--external`: + ```typescript + const hasSelectiveFlag = Boolean(options.issues) || Boolean(options.docs) + || Boolean(options.external); + // ... + const syncExternal = Boolean(options.external) + || (!hasSelectiveFlag && !hasExclusiveIssueFlag); + ``` + Also: `--push`/`--pull` should be rejected with `--external` (like `--docs`). +- [ ] **Lines 105-116** (sync steps): Reorder to 4 phases: + ``` + // Phase 1: External-pull (if syncExternal && use_gh_cli) + // Phase 2: Docs sync (if syncDocs) + // Phase 3: Issues git sync (if syncIssues) + // Phase 4: External-push (if syncExternal && use_gh_cli) + ``` + The `use_gh_cli` gate check: read config, check `config.settings.use_gh_cli`. If + false: + - `--external` explicitly → warn “External sync skipped: use_gh_cli is false” + - default (no flags) → silently skip phases 1/4 +- [ ] **Lines 1100-1113** (Commander definition): Add option: + ```typescript + .option('--external', 'Sync only external issues (not issues or docs)') + ``` + +#### 3b. External Sync Implementation + +**`packages/tbd/src/cli/commands/sync.ts`** (new methods): + +- [ ] Add `syncExternalPull()` method: + - Load all beads via `listIssues(dataSyncDir)` + - Filter to beads with non-null `external_issue_url` + - For each: parse URL, call `getGitHubIssueState()`, apply reverse mapping + - Write updated beads via `writeIssue()` + - Return count of updated beads +- [ ] Add `syncExternalPush()` method: + - For each linked bead: compare local state to fetched state (from pull) + - Push status via `closeGitHubIssue()` / `reopenGitHubIssue()` + - Return count of pushed changes +- [ ] Add summary output: “Synced N external issues: X pulled, Y pushed, Z unchanged” + +**`packages/tbd/src/file/github-issues.ts`** (status mapping tables): + +- [ ] Add status mapping constants: + ```typescript + // tbd → GitHub mapping + const TBD_TO_GITHUB_STATUS: Record = { + open: { state: 'open' }, + in_progress: { state: 'open' }, + blocked: null, // no change + deferred: { state: 'closed', state_reason: 'not_planned' }, + closed: { state: 'closed', state_reason: 'completed' }, + }; + + // GitHub → tbd mapping + function githubToTbdStatus( + state: string, stateReason: string | null, currentTbdStatus: string + ): string | null; + ``` + +**Tests:** +- [ ] Status mapping unit tests in `github-issues.test.ts` +- [ ] Sync scope selection unit tests in `sync.test.ts` +- [ ] Mock `gh api` calls to test sync flow end-to-end +- [ ] Golden tryscript tests for sync behavior + +* * * + +### Phase 4: Label Sync (bidirectional, optional) + +Add bidirectional label sync as part of the external sync scope. +This phase is optional and can be deferred. + +**`packages/tbd/src/file/github-issues.ts`**: + +- [x] Add `addGitHubLabel()`: + - Step 1: `POST /repos/{owner}/{repo}/labels` (create if needed, ignore 422) + - Step 2: `POST /repos/{owner}/{repo}/issues/{number}/labels` +- [x] Add `removeGitHubLabel()`: + - `DELETE /repos/{owner}/{repo}/issues/{number}/labels/{label}` +- [x] Add `computeLabelDiff()` helper: + ```typescript + function computeLabelDiff( + localLabels: string[], remoteLabels: string[] + ): { toAdd: string[]; toRemove: string[] }; + ``` + +**`packages/tbd/src/file/external-sync.ts`**: + +- [x] Extend `externalPull()` to also pull label changes (union semantics) +- [x] Extend `externalPush()` to also push label diffs +- [x] Union semantics: if both sides added different labels, both get union + +**Tests:** +- [x] Label diff computation tests in `github-issues.test.ts` +- [x] Label sync mock tests in `external-sync.test.ts` +- [ ] Golden tryscript tests for bidirectional label sync (deferred — requires live + GitHub API) + +### Documentation Updates + +All documentation must be updated to reflect the new `--external-issue` flag, +`--external` sync scope, and `use_gh_cli` gating. +This section lists every document that needs changes. + +#### Design Doc (`packages/tbd/docs/tbd-design.md`) + +- [x] §2.6.3 IssueSchema: add `external_issue_url` field +- [x] §5.5 merge rules: add `external_issue_url: 'lww'` +- [x] §8.7: rewrite with implemented design (status/label mapping, sync arch) +- [x] §8.7: document `use_gh_cli` prerequisite +- [x] §2.7.4 ConfigSchema: add `use_gh_cli` to documented settings +- [x] §4.4 Create command: add `--external-issue ` to options list and examples. + Note: `--spec` also needs adding (currently undocumented in §4.4) +- [x] §4.4 Update command: add `--external-issue ` to options list and examples +- [x] §4.4 Show command: mention `external_issue_url` in output description +- [x] §4.4 List command: add `--external-issue` filter option +- [x] §4.7 Sync command: add `--external` scope flag, `--issues`, `--docs` scope flags, + and document the 4-phase sync ordering + +#### CLI Reference (`packages/tbd/docs/tbd-docs.md`) + +- [x] `create` section (line ~193): add `--external-issue ` option with description + and example. Include URL format example in help text. +- [x] `update` section (line ~294): add `--external-issue ` option +- [x] `list` section (line ~236): add `--external-issue [url]` filter option with + example +- [x] `show` section (line ~282): mention `external_issue_url` in output fields +- [x] `sync` section (line ~449): add `--external`, `--issues`, `--docs` scope flags. + Document that default (no flags) syncs all scopes. + +#### Workflow Context (`packages/tbd/docs/tbd-prime.md`) + +- [x] “Creating & Updating” section: add `--external-issue` to create example +- [x] “Sync & Collaboration” section: mention `--external` scope flag +- [x] Consider adding a brief external issue linking note to “Common Workflows” + +#### Top-Level README (`README.md`) + +- [x] GitHub authentication section: note that `use_gh_cli: false` disables external + issue features +- [x] Commands section: add `--external-issue` to create/update examples +- [x] Commands section: add `--external` to sync examples + +#### Shortcuts + +These shortcuts reference beads workflows and should mention that beads may have linked +external issues: + +- [x] `plan-implementation-with-beads.md`: In the “Create a top-level epic” step, show + `--external-issue` as an optional flag alongside `--spec` (epics are the natural place + to link to GitHub issues) +- [x] `implement-beads.md`: Note that beads may be linked to external GitHub issues — + agents should check `tbd show` output for `external_issue_url` and be aware that + `tbd sync` pushes status changes externally +- [x] `agent-handoff.md`: Add “External issues” to the “What to Include” checklist + (whether beads have linked GitHub issues, sync status) +- [x] `setup-github-cli.md`: Mention external issue linking as a feature that requires + `gh` CLI. List it alongside PR creation and code review. +- [x] `code-review-and-commit.md`: No changes needed (doesn’t deal with beads) +- [x] `create-or-update-pr-simple.md`: No changes needed (doesn’t deal with beads) + +#### CLI `--help` Text (in source code) + +- [x] `create` command: `--external-issue` help must include format example: + `--external-issue Link to GitHub issue (e.g., https://github.com/owner/repo/issues/123)` +- [x] `update` command: same format +- [x] `list` command: `--external-issue [url]` filter help +- [x] `sync` command: `--external` scope help text +- [x] All `--external-issue` help should note: “Requires use_gh_cli: true” + +#### Error Messages (in source code) + +Every error path must include the expected URL format example so agents are never +confused: + +- [x] Invalid URL format → include `https://github.com/owner/repo/issues/123` +- [x] PR URLs accepted (no longer rejected — both issues and PRs are valid) +- [x] Non-GitHub URL → rejected with clear error +- [x] 404 → “Issue or pull request not found or not accessible” +- [x] `use_gh_cli: false` → “Set use_gh_cli: true or run tbd setup --auto” + +## Testing Strategy + +### Unit Tests + +1. **GitHub Issue URL Parsing and Validation** (`github-issues.test.ts`) + - Valid GitHub issue URLs: `https://github.com/owner/repo/issues/123` → parses + - Valid with http: `http://github.com/owner/repo/issues/456` → parses + - Trailing slash rejected: `https://github.com/owner/repo/issues/123/` → null + - Query params rejected: `https://github.com/owner/repo/issues/123?foo=bar` → null + - PR URL accepted: `https://github.com/owner/repo/pull/123` → parses + - Repo URL rejected: `https://github.com/owner/repo` → null + - Blob URL rejected: `https://github.com/owner/repo/blob/main/file.ts` → null + - Non-GitHub URL rejected: `https://jira.example.com/PROJ-123` → null + - Malformed: `not-a-url` → null + - No issue number: `https://github.com/owner/repo/issues/` → null + - Non-numeric issue number: `https://github.com/owner/repo/issues/abc` → null + - Extracts correct owner, repo, number from valid URLs + +2. **Schema Validation** (add to `schemas.test.ts`) + - `external_issue_url` accepts valid URLs + - `external_issue_url` accepts null/undefined + - `external_issue_url` rejects non-URL strings + +3. **Status Mapping** (`github-issues.test.ts`) + - Each tbd status maps to correct GitHub action + - Each GitHub state+reason maps to correct tbd status + - Edge cases: `blocked` bead + GitHub close, `in_progress` bead + GitHub reopen + +4. **Label Sync** (`github-issues.test.ts`) + - Diff calculation (added, removed, unchanged) + - Union behavior + - Empty label lists + +5. **Inheritable Fields** (`inheritable-fields.test.ts`) + - `inheritFromParent()` copies all registered fields from parent when not explicitly + set on child + - `inheritFromParent()` does NOT overwrite fields the user explicitly set + - `propagateToChildren()` updates children with null or old-matching values + - `propagateToChildren()` skips children with explicitly different values + - Adding a new field to `INHERITABLE_FIELDS` automatically includes it in inherit and + propagate operations (no other code changes) + +6. **Merge Rules** (add to `git.test.ts`) + - `external_issue_url` uses LWW correctly + - Concurrent edits to `external_issue_url` resolved by timestamp + +### Golden Tryscript Tests + +New tryscript files covering the full range of inheritance and linking scenarios. + +#### `tests/cli-external-issue-linking.tryscript.md` + +Basic external issue linking operations: +- Create with and without `--external-issue` (backward compatibility) +- Show displays external issue URL +- List filtering by external issue +- Update to set/change/clear external issue URL +- Close bead locally → verify no GitHub API call happens (staging only) +- `tbd sync --external` → verify GitHub API calls happen for linked beads +- `tbd sync --issues` → verify no external sync happens (scope isolation) +- `tbd sync` (no flags) → verify all three scopes run (issues + docs + external) + +**URL parsing and validation error scenarios** (must be golden-tested): +- Valid GitHub issue URL → succeeds (`https://github.com/owner/repo/issues/123`) +- Valid GitHub PR URL → succeeds (`https://github.com/owner/repo/pull/123`) +- GitHub repo URL (no issue number) → error (`https://github.com/owner/repo`) +- GitHub blob URL → error (`https://github.com/owner/repo/blob/main/file.ts`) +- Non-GitHub URL → error: “only GitHub issue and PR URLs are supported” + (`https://jira.example.com/browse/PROJ-123`) +- Malformed URL → error (`not-a-url`, `github.com/owner/repo/issues/123` without scheme) +- Inaccessible issue/PR (valid URL format but 404) → error: “issue or pull request not + found or not accessible” + +#### `tests/cli-inheritable-fields.tryscript.md` + +Comprehensive tests for the generic inheritable field system. +These tests must exercise both `spec_path` and `external_issue_url` to prove the shared +logic works for any registered field. + +**Scenario 1: Parent-to-child inheritance on create** +1. Create parent epic with `--spec` and `--external-issue` +2. Create child under parent (no explicit `--spec` or `--external-issue`) +3. Verify child inherited both `spec_path` and `external_issue_url` from parent +4. Create another child with explicit `--spec` (different from parent) +5. Verify that child has the explicit `spec_path` but inherited `external_issue_url` + +**Scenario 2: Propagation from parent to children on update** +1. Create parent epic with `--spec` and `--external-issue` +2. Create 3 children under parent (all inherit both fields) +3. Manually set child-3’s `external_issue_url` to a different value +4. Update parent’s `--external-issue` to a new URL +5. Verify child-1 and child-2 got the new `external_issue_url` (they had the inherited + value) +6. Verify child-3 was NOT updated (it had an explicitly different value) +7. Update parent’s `--spec` to a new path +8. Verify same propagation logic applies to `spec_path` + +**Scenario 3: Re-parenting inherits from new parent** +1. Create parent-A with `--external-issue URL-A` +2. Create parent-B with `--external-issue URL-B` +3. Create orphan child (no parent, no external issue) +4. Re-parent child under parent-A +5. Verify child inherited `external_issue_url` from parent-A +6. Re-parent child under parent-B (child still has URL-A from first parent) +7. Verify child kept URL-A (not overwritten — only inherits if null) + +**Scenario 4: Clearing and re-inheriting** +1. Create parent with `--external-issue` +2. Create child (inherits from parent) +3. Clear child’s `external_issue_url` with `--external-issue ""` +4. Verify child’s `external_issue_url` is now null +5. Update parent’s `--external-issue` to a new URL +6. Verify child gets the new URL (its value was null, so it’s eligible for propagation) + +**Scenario 5: Mixed inheritance — some fields set, some inherited** +1. Create parent with `--spec` only (no `--external-issue`) +2. Create child — inherits `spec_path`, no `external_issue_url` +3. Update parent to add `--external-issue` +4. Verify child now has `external_issue_url` propagated (was null) +5. Verify child still has original `spec_path` (unchanged) + +### Integration Tests + +- End-to-end with real GitHub repo (can use a test repo) +- Verify `gh api` calls are correct +- Verify status and label sync round-trips + +## Rollout Plan + +1. Phase 1 shipped first — safe, backward-compatible schema addition +2. Phase 2 adds health checks — no behavioral change +3. Phase 3 adds one-way status sync — low risk, always succeeds locally +4. Phase 4 adds bidirectional sync — optional, can be feature-flagged if needed + +All phases are backward compatible. +The field is optional, so older tbd versions simply ignore it (it may be stripped by +older schemas, but merge rules preserve it via LWW). + +## Open Questions + +1. **Should we use `external_issue_url` (string URL) or a structured `linked` field (as + described in design doc §8.7)?** Recommendation: Start with `external_issue_url` as a + simple string for v1. It’s simpler, matches the `spec_path` pattern, and the URL + contains all necessary information. + The structured `linked` field can be added later (possibly in `extensions`) if we + need multi-provider support. + +2. **Should status sync be opt-in via a config setting?** Recommendation: Default to + enabled when `use_gh_cli` is true. + No additional config needed for v1. + +3. ~~**Should we sync on every bead operation or only on explicit `tbd sync`?**~~ + **RESOLVED**: Sync only at `tbd sync` time, never on individual operations. + Local operations are a staging area. + This is consistent with how issue sync (git) and doc sync already work, and allows + batching changes before pushing them externally. + The `--external` scope flag selects this sync, and it’s included by default when no + scope flags are given. + +4. ~~**How should we handle GitHub rate limits?**~~ **RESOLVED**: We don’t handle rate + limits. If rate-limited, the `gh` CLI surfaces the error. + The user or agent decides whether to back off and retry. + No special retry logic, batching, or queuing in tbd. + +5. ~~**Should the `--external-issue` flag accept shorthand like `#123` for the current + repo?**~~ **RESOLVED**: No. + Only full GitHub issue URLs are accepted + (`https://github.com/owner/repo/issues/123`). However, `--help` text, error messages, + and documentation must be very clear about the expected format so that agents have no + confusion. Every error message should include an example of the correct URL format. + +## References + +### Source Files (with key line numbers) + +| File | Key Lines | What's There | +| --- | --- | --- | +| `packages/tbd/src/lib/schemas.ts` | 118-151 | `IssueSchema` definition | +| | 149-150 | `spec_path` field (template for `external_issue_url`) | +| | ~280 | `use_gh_cli: z.boolean().default(true)` in ConfigSchema | +| `packages/tbd/src/lib/types.ts` | 28 | `Issue` type (inferred from schema) | +| | 81-91 | `CreateIssueOptions` (needs `external_issue_url`) | +| | 96-109 | `UpdateIssueOptions` (needs `external_issue_url`) | +| `packages/tbd/src/cli/commands/create.ts` | 29-41 | `CreateOptions` interface | +| | 67-76 | `--spec` validation block (template for `--external-issue`) | +| | 95 | `readConfig(tbdRoot)` — config already loaded here | +| | 113-119 | `spec_path` inheritance from parent (to refactor) | +| | 138 | `spec_path: specPath` in issue data (add `external_issue_url`) | +| | 201 | `.option('--spec ...')` Commander flag | +| `packages/tbd/src/cli/commands/update.ts` | 30-41 | `UpdateOptions` interface | +| | 76-77 | `oldSpecPath` capture (generalize to all inheritable) | +| | 90 | `spec_path` update (add `external_issue_url`) | +| | 94-104 | Re-parent inheritance (to refactor) | +| | 151-164 | Propagation to children (to refactor) | +| | 371-383 | `--spec` CLI option handling in `buildUpdates` | +| | 437 | `.option('--spec ...')` Commander flag | +| `packages/tbd/src/cli/commands/show.ts` | 69-70 | `spec_path` color highlighting (add `external_issue_url`) | +| `packages/tbd/src/cli/commands/list.ts` | 33-50 | `ListOptions` interface | +| | 96 | `spec_path` in displayIssues (add `external_issue_url`) | +| | 247-251 | `--spec` filter block (template for `--external-issue`) | +| | 301-304 | `.option('--spec ...')` Commander flag | +| `packages/tbd/src/cli/commands/sync.ts` | 58-65 | `SyncOptions` interface (add `external`) | +| | 89-103 | Scope selection logic (extend for `--external`) | +| | 105 | Step 1: docs sync | +| | 116 | Step 2: issues sync | +| | 1100-1113 | Commander definition + options | +| `packages/tbd/src/cli/commands/close.ts` | 56-61 | Idempotent close (no external side effects — by design) | +| `packages/tbd/src/cli/commands/doctor.ts` | 88-133 | 15 health checks | +| | 136-142 | 2 integration checks (add `gh` CLI check) | +| | 562-601 | Integration check methods (add `checkGhCli()`) | +| `packages/tbd/src/file/git.ts` | 277-308 | `FIELD_STRATEGIES` merge rules | +| | 300 | `spec_path: 'lww'` (add `external_issue_url`) | +| `packages/tbd/src/file/github-fetch.ts` | 12-16 | `execFile` + `promisify` pattern for `gh` CLI | +| | 28, 36 | GitHub URL regex patterns (reference for issue regex) | +| | 63 | `isGitHubUrl()` helper | + +### New Files to Create + +| File | Purpose | +| --- | --- | +| `packages/tbd/src/file/github-issues.ts` | GitHub issue URL parsing, validation, and API operations | +| `packages/tbd/src/lib/inheritable-fields.ts` | Generic parent→child field inheritance/propagation | +| `packages/tbd/src/file/github-issues.test.ts` | URL parsing and status mapping tests | +| `packages/tbd/src/lib/inheritable-fields.test.ts` | Inheritance logic tests | +| `packages/tbd/tests/cli-external-issue-linking.tryscript.md` | Golden tests for external issue linking | +| `packages/tbd/tests/cli-inheritable-fields.tryscript.md` | Golden tests for inheritance system | + +### Documentation Files to Update + +| File | Sections | +| --- | --- | +| `packages/tbd/docs/tbd-design.md` | §2.6.3, §2.7.4, §4.4, §4.7, §5.5, §7.2, §8.7 | +| `packages/tbd/docs/tbd-docs.md` | create, update, list, show, sync sections | +| `packages/tbd/docs/tbd-prime.md` | Creating, Sync, Common Workflows | +| `README.md` | GitHub auth, Commands sections | +| `packages/tbd/docs/shortcuts/standard/plan-implementation-with-beads.md` | Epic creation | +| `packages/tbd/docs/shortcuts/standard/implement-beads.md` | Bead awareness | +| `packages/tbd/docs/shortcuts/standard/agent-handoff.md` | Handoff checklist | +| `packages/tbd/docs/shortcuts/standard/setup-github-cli.md` | Feature list | + +### External References + +- `docs/project/specs/done/plan-2026-01-26-spec-linking.md` — Reference spec for similar + `spec_path` feature +- [GitHub Issues API: state and state_reason](https://docs.github.com/en/rest/issues) +- [GitHub REST API: Labels](https://docs.github.com/en/rest/issues/labels) diff --git a/packages/tbd/docs/shortcuts/standard/agent-handoff.md b/packages/tbd/docs/shortcuts/standard/agent-handoff.md index 26bcaa2e..b5aff9e1 100644 --- a/packages/tbd/docs/shortcuts/standard/agent-handoff.md +++ b/packages/tbd/docs/shortcuts/standard/agent-handoff.md @@ -19,6 +19,8 @@ The next agent needs the latest issue state. - **Task**: One line on what we’re doing - **Spec**: Path to active spec + relevant sections - **Beads**: tbd issue ID(s), status, dependencies, synced? +- **External issues**: Whether beads have linked GitHub issues or PRs + (`external_issue_url`), sync status - **Branch**: Current branch, base branch (if not main), pushed to remote? - **PR**: Filed/not filed, URL, CI status, up to date with branch? - **Git**: Uncommitted changes, files modified diff --git a/packages/tbd/docs/shortcuts/standard/implement-beads.md b/packages/tbd/docs/shortcuts/standard/implement-beads.md index 45d61880..92c319c3 100644 --- a/packages/tbd/docs/shortcuts/standard/implement-beads.md +++ b/packages/tbd/docs/shortcuts/standard/implement-beads.md @@ -23,6 +23,10 @@ Create a to-do list with the following items then perform all of them: If the user did not specify which beads, check all open beads with `tbd ready`. - Beads are usually linked to specs so be sure to find specs that are relevant (for that bead or an umbrella bead) for each if possible and review those specs. + - Beads may be linked to external GitHub issues or pull requests. + Check `tbd show ` output for an `external_issue_url` field. + When present, be aware that `tbd sync` pushes status and label changes to the + linked GitHub issue/PR bidirectionally. - Follow `tbd shortcut precommit-process` and `tbd sync` changes after each bead. 4. Repeat this for all beads where you know how to fix them. diff --git a/packages/tbd/docs/shortcuts/standard/plan-implementation-with-beads.md b/packages/tbd/docs/shortcuts/standard/plan-implementation-with-beads.md index 40c6a21b..52ea4e6f 100644 --- a/packages/tbd/docs/shortcuts/standard/plan-implementation-with-beads.md +++ b/packages/tbd/docs/shortcuts/standard/plan-implementation-with-beads.md @@ -17,9 +17,12 @@ If unclear, ask the user if they want you to create a spec first using 1. **Create a top-level epic** referencing the spec: ```bash - tbd create "Spec: [feature or task]" --type=epic --spec plan-YYYY-MM-DD-feature.md + tbd create "Spec: [feature or task]" --type=epic --spec plan-YYYY-MM-DD-feature.md [--external-issue=] ``` + Use `--external-issue` to link the epic to a GitHub issue when one exists (requires + `use_gh_cli: true` in config). + 2. **Create child beads** for each implementation step, all under the epic: ```bash diff --git a/packages/tbd/docs/shortcuts/standard/setup-github-cli.md b/packages/tbd/docs/shortcuts/standard/setup-github-cli.md index da7931e8..0b7987fc 100644 --- a/packages/tbd/docs/shortcuts/standard/setup-github-cli.md +++ b/packages/tbd/docs/shortcuts/standard/setup-github-cli.md @@ -4,7 +4,8 @@ description: Ensure GitHub CLI (gh) is installed and working category: session author: Joshua Levy (github.com/jlevy) with LLM assistance --- -The GitHub CLI (`gh`) is required for PR and issue operations. +The GitHub CLI (`gh`) is required for PR creation, code review, and external issue +linking (`--external-issue`, `tbd sync --external`). **In most cases, gh is already available** - tbd installs a SessionStart hook that auto-installs gh on every session. diff --git a/packages/tbd/docs/tbd-design.md b/packages/tbd/docs/tbd-design.md index b6b56726..e830be0f 100644 --- a/packages/tbd/docs/tbd-design.md +++ b/packages/tbd/docs/tbd-design.md @@ -1433,6 +1433,9 @@ const IssueSchema = BaseEntity.extend({ // Spec linking - path to related spec/doc (relative to repo root) spec_path: z.string().optional(), + // External issue linking - URL to linked GitHub issue or PR + external_issue_url: z.string().url().optional(), + // Beads compatibility due_date: Timestamp.optional(), deferred_until: Timestamp.optional(), @@ -1482,6 +1485,24 @@ type Issue = z.infer; Re-parenting a child (via `tbd update --parent`) also inherits the new parent’s `spec_path` if the child has no existing `spec_path`. +- `external_issue_url`: Optional URL to a linked external issue tracker entry. + For v1, GitHub issue and pull request URLs are supported (e.g., + `https://github.com/owner/repo/issues/123` or `.../pull/123`). Despite the field name + saying “issue,” it accepts both issues and PRs since GitHub’s API treats them + uniformly. The URL is validated at link time to ensure the target exists and is + accessible via `gh` CLI. + + **Inheritance from Parent:** Follows the same inheritance rules as `spec_path`: both + fields are registered in the generic inheritable field system. + When creating a child with `--parent`, the child inherits `external_issue_url` from + the parent if not explicitly set. + When a parent’s URL is updated, it propagates to children whose URL was null or + matched the old value. + + **Sync Behavior:** External issue sync (status and labels) happens only at `tbd sync` + time via the `--external` scope. + See §8.7 for the full sync architecture. + - `child_order_hints`: Optional array of internal IssueIds specifying preferred display order for children of this issue. Used by `tbd list --pretty` to control child ordering in tree views. @@ -1564,11 +1585,19 @@ const ConfigSchema = z.object({ .object({ auto_sync: z.boolean().default(false), index_enabled: z.boolean().default(true), + use_gh_cli: z.boolean().default(true), // Master gate for GitHub CLI features }) .default({}), }); ``` +**`use_gh_cli` setting:** Controls whether GitHub CLI (`gh`) features are enabled. +When `true` (default), `tbd setup` installs a SessionStart hook that ensures `gh` is +available, and all external issue features (linking, sync, validation) are active. +When `false`, the hook is not installed and all `gh`- dependent features are disabled — +including external issue linking (§8.7) and `tbd sync --external`. Set via +`tbd setup --no-gh-cli` or directly in config. + > **Forward Compatibility Policy:** ConfigSchema uses Zod’s `strip()` mode, which > discards unknown fields. > To prevent data loss when users mix tbd versions: @@ -2198,6 +2227,7 @@ const issueMergeRules: MergeRules = { dependencies: { strategy: 'merge_by_id', key: (d) => d.target }, parent_id: { strategy: 'lww' }, spec_path: { strategy: 'lww' }, + external_issue_url: { strategy: 'lww' }, due_date: { strategy: 'lww' }, deferred_until: { strategy: 'lww' }, created_by: { strategy: 'preserve_oldest' }, @@ -2413,6 +2443,8 @@ Options: --defer Defer until date (ISO8601) --parent= Parent issue ID --label