A deep dive into a CLI tool I built to give AI agents full situational awareness
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.
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:
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.
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.
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").
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.
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.
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.
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.
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.
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***.
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.
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.
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.
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.
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.
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.
/issues endpoint silently returns PRs — you have to filter them out by checking for the pull_request field on each item/pulls endpoint does not support a since query param, so date-filtering for merged PRs has to happen client-sidenoEscape: true compilation option exists precisely so you can handle escaping yourself per-helper rather than globally — crucial for the XML style[👍👎]) does not reliably match all representations of those emoji — Unicode supplementary plane characters need /gu flags and their own regex passesA 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