feat: bundle Linear issues, harden path boundaries - #10
Merged
Conversation
Bundles could only ever describe code, but the reason a change exists usually lives in a tracker. A bundle may now name `linear`, and each issue becomes a virtual file at `linear/issues/<identifier>.md` — indexed, line counted and excludable like any other entry, so nothing downstream needs to know it wasn't read off disk. Auth is `LINEAR_API_KEY` from the environment rather than config, because config files get committed. A pre-release audit of the surrounding path handling turned up five defects that shared one cause: paths were compared as the strings that name them, and filters were treated as best-effort. That is fine for deciding what goes into a bundle and wrong for deciding what gets deleted. - `outDir: "src"` deleted the sources it was asked to bundle. Emptying is now automatic only for the conventional `.srcpack`. - A symlinked `.srcpack` emptied and wrote to its target. Ownership is decided on the physical path, and a redirected `.srcpack` fails the run. - A symlinked directory was walked and everything under it bundled. - Nested `.gitignore` files were never read, so `packages/app/.env` could land in a bundle the docs promise keeps secrets out. - `--no-uplaod` uploaded. Unknown flags and config keys are now errors. Comparison identity is NFC-normalised and case folded, since a filesystem that folds either one turns two bundles into one silent overwrite. Ownership stays an exact match — folding there could only widen what gets deleted. Bundles are written via temp file and rename, so a symlink at the output path is replaced rather than written through. Each defect was reproduced first, and every fix has a regression test that fails when the fix is reverted. BREAKING CHANGE: a custom `outDir` is no longer emptied automatically; set `emptyOutDir: true` to keep the old behaviour.
Every index and dry-run sample was hand-aligned into columns the formatter never produces, and two showed line ranges starting at L1 — impossible, since content begins below the index header. Replaced with output captured from real runs, which also fixes the entry order: entries sort by path, so the files were listed in an order srcpack would never emit. Adds the -h/-v short forms to the CLI options table.
npm version and downloads answer "is this maintained and used" before someone reads any further; CI and license answer the next two questions. Discord points at the community link the README already carries at the bottom, where nobody arriving from npm will see it.
The bundle index lists paths, and for a code file the path is the summary. For an issue it isn't: forty lines of `linear/issues/SM-*.md` tell a model nothing, so it has to read every body to find the two that matter, and can't answer "what's in progress" at all. Each Linear source now emits `linear/issues.md` ahead of the issues — scope heading, a count per workflow state, and one row per issue with state, priority and title. It sorts first because "." precedes "/", and it's an ordinary entry, so the collision check and `!` exclusions apply unchanged. Rows are ordered by issue number, which the index cannot be: entries sort by path as text, so SM-2 lands between SM-19 and SM-20. Natural-sorting every bundle to fix that would be a far larger claim than this needs.
koistya
force-pushed
the
feat/linear-and-path-hardening
branch
from
August 15, 2026 18:06
fb45172 to
4bec8a5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds Linear as a bundle source, and hardens the path handling around it after a pre-release audit turned up five ways a bundling run could read or delete the wrong thing.
Linear issues as virtual files
A bundle can now name
linearalongside (or instead of) globs. Each issue becomes a virtual file, so it gets an index entry, a line range, and can be filtered with an ordinary!exclusion — nothing downstream needs to know it wasn't read off disk.Each source also emits
linear/issues.mdahead of the issues: scope heading, a count per workflow state, and a row per issue ordered by number. Without it the index is forty lines of opaque identifiers, and issue numbers sort as text (SM-2betweenSM-19andSM-20). Drop it with!linear/issues.md.teamis required: a workspace-wide fetch looks innocuous and can pull thousands of issues into a context window. Auth readsLINEAR_API_KEYfrom the environment rather than config, because config files get committed. Pagination orders bycreatedAt— theupdatedAtdefault is mutable, so an issue edited mid-run reorders the list under the cursor and drops or repeats its neighbours.Path boundaries
Five defects, one cause: paths were compared as the strings that name them, and filters were treated as best-effort. That's fine for deciding what goes into a bundle and wrong for deciding what gets deleted — and
.gitignorehad quietly become the second kind, because the docs promise it keeps secrets out.outDir: "src"deleted the sources it was asked to bundle.srcpack.srcpackemptied and wrote into its target.srcpackfails the run.gitignorefiles were never read--no-uplaoduploadedupload.excludeentries naming no bundle are errorsTwo related holes: two bundles could resolve to one physical file through a symlink or a case/normalisation alias, and a bundle could swallow its own previous output when reached by a different spelling.
Comparison identity is now NFC-normalised and case folded on every platform — a filesystem that folds either one turns two bundles into a silent overwrite, and a config that works in CI but loses a bundle on a laptop is worse than one rejected everywhere. Ownership deliberately stays an exact match, since folding there could only widen what gets deleted.
Writes go through a temp file and
rename, so a symlink sitting at the output path is replaced rather than written through, and each bundle appears whole or not at all.Full rationale in ADR 003 and ADR 004, both added here.
Breaking change
A custom
outDiris no longer emptied automatically — setemptyOutDir: trueto keep the old behaviour. Stale files are a nuisance; deleted sources are not, and 0.x is the time to take that break. Version bumped to 0.3.0.Verification
Each defect was reproduced against the built CLI before being fixed, and every fix is mutation-tested: reverting it alone fails its own regression test. That covers symlink following, nested
.gitignore, outDir ownership, lexical vs physical output identity, NFC and case folding, unknown-flag rejection, collision detection, and temp-file writes.Ran
bun test,bun run check,prettier --check, and the VitePress build. Layered.gitignoreresolution was cross-checked againstgit check-ignore, andorderBy: createdAtagainst the live Linear API.