Building repissue — Packing GitHub Issues & PRs into an AI-Ready Context File

A deep dive into a CLI tool I built to give AI agents full situational awareness

repissue banner
The Problem

I use AI coding agents every day — Claude, Cursor, you name it. And one thing kept bothering me: the agents always know everything about the code, but absolutely nothing about the live state of the project.

Tools like Repomix solve the code side beautifully — they pack your entire codebase into one structured file you can drop into any LLM context window. But what about the 47 open bugs? The 12 PRs in review? The discussion in issue #312 that explains why a particular design decision was made?

Without that, the agent is flying blind on the live project. It might suggest a fix for a bug already being worked on in an open PR, or miss the thread where your team already rejected the approach it's recommending. I wanted to fix that.

What repissue Does

repissue is a CLI tool that fetches all open issues and pull requests from any GitHub repository and packs them into a single, structured, signal-dense file — ready to be fed into any LLM alongside a Repomix code snapshot.

One command. One file. Complete situational awareness.

npx repissue facebook/react
# → repissue-output.md (~28,400 tokens of structured context)

The idea is dead simple:

  • Repomix output → everything the code is
  • repissue output → everything that needs to change
The 9-Stage Pipeline

The core of repissue is a pack() function in src/core/packager.ts that runs every call through 9 clearly separated stages. I deliberately kept them sequential and explicit — each stage does one thing, and its output is the next stage's input.

1. Fetch

Parallel requests to the GitHub REST API for issues and PRs using a custom fetchPage()abstraction that handles pagination, rate limiting, and retries. Issues and PRs are fetched concurrently via Promise.all(), then comments are batched in groups of 5 using a bounded concurrency queue I wrote from scratch — no semaphore libraries, just workers draining a shared queue array.

The rate limit handler reads GitHub's X-RateLimit-Reset header directly and waits the exact right amount of time, with a random jitter of up to 2 seconds to avoid thundering herd issues. It retries up to 3 times before surfacing a clean, human-readable error.

2. Filter

Raw GitHub data is extremely noisy. Bot accounts (dependabot, renovate, github-actions) flood comment threads with automated messages. Reaction comments — just "+1" or a thumbs up emoji — carry zero information. I wrote an isReactionOnly() function that strips all recognised emoji tokens and reaction shorthands and checks if anything substantive remains. It handles Unicode supplementary plane emoji, skin-tone modifiers, and variation selectors properly without putting + or - inside a character class (which would silently filter real content like "1 + 2").

3. Sort by priority

Issues are sorted by a configurable label priority list — "bug", "security", "P0"]by default. Earlier labels score higher. Ties are broken by comment count descending (more active issues first), then by creation date ascending (oldest first). PRs sort by label score, then draft status (non-draft first), then by last updated descending. The sort is stable and never mutates the input array.

4. Enrich

Each issue and PR gets augmented with its filtered comments and parsed cross-references. The cross-reference parser reads GitHub's documented closing keywords (closes, fixes, resolves, and all their variations) in a case-insensitive pass, handles owner/repo#Ncross-repo references, deduplicates, and separates "closes" (intent to fix) from "mentions" (informational references). The parser runs over the full thread — body plus all comment bodies — and merges the results, promoting a bare mention to "closes" if any comment in the thread uses a closing keyword for the same number.

5. Generate output

Handlebars templates render the enriched data into one of three output styles: markdown (with collapsible <details>blocks, emoji label badges mapped by category, and stripped inline images), plain (ASCII dividers, no markdown interpretation), or xml (properly escaped, structured for programmatic consumption). All Handlebars helpers are registered once via a singleton guard so templates can be compiled on demand without re-registering.

6. Write

The output is written to a file, printed to stdout, appended to an existing file, or split into multiple chunk files. The --append-to mode is particularly interesting for XML: it finds the last closing root tag in the target file using a regex heuristic and inserts the<repissue_append> block before it, keeping the document well-formed. For--split-output, a two-pass algorithm first bins items into groups using a preamble size estimate, then assembles final content strings with correct "Part N of M" preambles once the total file count is known.

7. Token count

The rendered output string is passed to a Tinypool worker thread that runs tiktoken'scl100k_base encoder — the same encoding used by GPT-4 and Claude. Running it in a worker keeps the main thread free during WASM initialisation. If the worker fails for any reason (missing native module, WASM error, constrained environment), it falls back to a chars / 4 estimate so the CLI never crashes just because token counting broke.

8. Security scan

An optional pass (off by default, enabled with --security-check) scans the rendered output line-by-line against 14 regex patterns: GitHub tokens (classic and fine-grained), OAuth tokens, AWS access keys, secret access key assignments, PEM headers, OpenAI/Anthropic-style sk- keys, Slack tokens, Stripe keys, SendGrid API keys, bearer tokens in Authorization headers, and generic password/secret assignments. Every hit is redacted before being reported — the first 6 characters are shown, the rest replaced with***.

9. Clipboard

Cross-platform clipboard write using native OS utilities — pbcopy on macOS,clip.exe on Windows, xclip with an xsel fallback on Linux. No external npm dependencies — just spawning the OS-provided tool and piping to its stdin.

Key Design Decisions
Dependency injection for testability

Every function that makes HTTP calls accepts a deps parameter with afetchPage injection point. This means the entire fetch → filter → generate pipeline can be unit tested with zero network calls, no mocking of globals, and no test doubles leaking between tests. The real defaultFetchPage is the default — you only override it in tests.

Zod schemas at every boundary

Every GitHub API response is validated through a Zod schema before it touches application code. The config file is validated on load. The merged config after CLI overrides is validated again. If GitHub changes their response shape, repissue surfaces a clear validation error instead of propagating undefined values silently through the pipeline.

Config layering

The configuration follows a strict priority order: defaults → config file → CLI flags. Only defined CLI flags win — Commander's option values are checked for!== undefined before being spread. This means passing --no-prscorrectly overrides a config file that says includePRs: true, but omitting the flag leaves the config file value intact.

File extension auto-resolution

When you run repissue owner/repo --style xml without specifying --output, the output file is automatically renamed from repissue-output.md torepissue-output.xml. But if you explicitly pass --output my-file.md, your path is left untouched. One small detail that prevents a lot of confusion.

Testing Approach

The test suite covers every layer independently. Fixture factories (makeIssue,makePR, makeComment) produce typed GitHub objects with sensible defaults and per-property overrides — a pattern that keeps test setup lean without hiding what's actually being tested.

Unit tests cover: label scoring and sort stability, noise filter edge cases (Unicode emoji, skin-tone modifiers, null users), cross-reference parsing (all 9 closing keywords, case insensitivity, deduplication, cross-repo references), pagination following, comment concurrency, split file naming, XML append insertion, token counter contracts, and all 14 secret scan patterns.

Integration tests hit the real GitHub API against elysiajs/elysia-jwt — a small, stable repo — and assert on structure rather than exact counts. They share a singlepack() call across all assertions to avoid hammering the API in CI.

What I Learned
  • The GitHub /issues endpoint silently returns PRs — you have to filter them out by checking for the pull_request field on each item
  • The /pulls endpoint does not support a since query param, so date-filtering for merged PRs has to happen client-side
  • Bounded concurrency queues are simple to write and more predictable than generic semaphore libraries for a fixed workload like "fetch comments for N items"
  • Tiktoken's WASM module is not reliable in all environments — graceful fallback is not optional, it's a requirement
  • Handlebars' noEscape: true compilation option exists precisely so you can handle escaping yourself per-helper rather than globally — crucial for the XML style
  • Emoji in character classes ([👍👎]) does not reliably match all representations of those emoji — Unicode supplementary plane characters need /gu flags and their own regex passes
The Result

A single command that gives an AI agent everything it needs to understand the live state of a project — not just the code, but the conversations, priorities, and in-flight work that shape what needs to happen next.

Built in TypeScript, Node.js ≥ 18, tested with Vitest — April 2026