Skip to content

fix(import): raise the pm read buffer past 1 MiB and preview updates in non-atomic --dry-run - #13

Merged
unbraind merged 6 commits into
mainfrom
fix/maxbuffer-and-dryrun-update-preview
Jul 24, 2026
Merged

unbraind merged 6 commits into
mainfrom
fix/maxbuffer-and-dryrun-update-preview

Conversation

@unbraind

@unbraind unbraind commented Jul 24, 2026 •

Copy link
Copy Markdown
Owner

Two defects, both found by running the importer against a real 443-item workspace instead of fixtures

Both fail — or mislead — precisely when a tracker is mature enough for the integration to matter.

1. Import dies on any tracker over 1 MiB, with nothing to diagnose

readPmItems() spawned pm list-all --full --include-body without a maxBuffer, so Node's 1 MiB spawnSync default applied. The real workspace used for testing dumps 1,052,859 bytes — 4 KiB over the cap — so the child was killed and the result came back as:

status: null   error.code: ENOBUFS   stderr: ""

readPmItems() only checks result.status !== 0 and reports result.stderr || "pm list-all failed", so every import, atomic plan and sync against that workspace failed with a bare pm list-all failed and no cause. Nothing in the message hints at a buffer limit, so the natural next step is to go hunting for a broken tracker.

Fixed by capping at 64 MiB — the same cap pm-changelog, pm-context and pm-brief already settled on — applied to readPmItems(), the central pmRun() spawner (a large pm show --json has the same exposure) and the afterCommand hook's pm show. A buffer overrun is now named explicitly and suggests narrowing the import (--labels, --since) instead of surfacing as an unexplained failure.

2. Non-atomic --dry-run previewed every issue as a create

const shouldMatchExisting = !opts.dryRun || opts.atomic;   // ← skipped the index on a plain dry run

With the provenance index never built, match was always undefined, so the preview counted every issue as an import:

[dry-run] Would import 29, skip 0.        ← before
[dry-run] Would import 24, update 5, skip 0.   ← after (matches the real plan)

A preview that overstates creates reads as "this will duplicate my whole tracker", which is the single thing --dry-run exists to rule out. --atomic --dry-run built the index and reported the split correctly, so the two paths disagreed about the same plan.

The index is now built for dry runs too; the non-atomic preview labels each issue import/update and returns wouldUpdate alongside wouldImport/wouldSkip, matching the atomic shape. Note the coupling: fixing (2) makes dry runs read the tracker, so (1) had to be fixed in the same change or previews would newly hit ENOBUFS.

Verification (same real workspace that exposed the bugs)

command before after
pm github import unbraind/pm-cli --state open --dry-run Would import 29, skip 0 Would import 24, update 5, skip 0
… --dry-run --atomic Error: pm list-all failed (ENOBUFS) wouldImport 24, wouldUpdate 5, wouldSkip 0

Both paths now agree exactly. The 5 updates are real: 5 upstream issues in that workspace already carry gh:unbraind/pm-cli#N provenance tags, and a real run would update rather than duplicate them — which the old preview could not tell you.

168/168 tests pass, including two new tests: one asserting the non-atomic preview labels an already-linked issue as an update, one asserting the atomic and non-atomic previews agree on the split.

pm items

item status
pm-github-tn92 — Import dies with an unexplained pm list-all failed on any tracker over 1 MiB, and non-atomic --dry-run previews every issue as a create closed

Related fleet context: the same unguarded-maxBuffer pattern exists in 7 sibling packages (pm-beads, pm-jira, pm-todos, pm-starter, pm-gantt-chart, pm-ts-starter, and one call in pm-csv); those are tracked separately.

🤖 Generated with Claude Code


Summary by cubic

Raise the pm read buffer to 64 MiB (overridable via PM_JSON_MAX_BUFFER) and fix non-atomic --dry-run so previews correctly show imports vs updates. The cap is resolved per call and rejects malformed overrides, removing unexplained pm list-all failed errors and making dry-run output match real plans.

  • Bug Fixes
    • Apply the cap to readPmItems, central pmRun, and afterCommand pm show; resolve it per call. Support PM_JSON_MAX_BUFFER, ignoring invalid values (e.g. "64MiB"); ENOBUFS now reports the effective limit with tips to narrow via --labels/--since.
    • Non-atomic --dry-run builds the provenance index, labels each issue import/update, logs "Would import X, update Y, skip Z", and returns wouldUpdate; tests verify the split and parity with --atomic --dry-run.

Written for commit 9d2070b. Summary will update on new commits.

Review in cubic

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @unbraind, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@unbraind

Copy link
Copy Markdown
Owner Author

@greptileai review

/gemini review

@coderabbitai full review

@sourcery-ai review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@unbraind, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 9 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0d95174f-993c-406b-a933-3db9e8d0b12b

📥 Commits

Reviewing files that changed from the base of the PR and between ca23a7d and 9d2070b.

📒 Files selected for processing (5)
  • .agents/pm/history/pm-github-tn92.jsonl
  • .agents/pm/issues/pm-github-tn92.toon
  • CHANGELOG.md
  • index.ts
  • test/dryrun-preview.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/maxbuffer-and-dryrun-update-preview

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

@unbraind, I’ll perform a complete review of PR #13, including the buffer-limit handling and dry-run preview behavior.

✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 43 minutes.

@unbraind

Copy link
Copy Markdown
Owner Author

Bot-feedback triage — round 1

CodeRabbit — check SUCCESS, no findings. Extra context so a re-review can be sharper: this is a one-constant change with a measured failure behind it, not a defensive tweak. The exact argv this package uses returns over 1 MiB (or within 6% of it) against a real 443-item pm workspace, and past Node's default spawnSync cap the child is killed with status: null, signal: SIGTERM, error.code: ENOBUFS and empty stderr — so the pre-existing if (result.status !== 0) throw result.stderr || "...failed" reported an unexplained failure. At larger sizes stdout truncates mid-JSON (forcing a 100 KiB cap on the same workspace yields Unterminated string in JSON at position 130829). The highest-value thing to look for here is any other unguarded child-process read of pm output in this package that the audit's grep missed.

Sourcery — weekly 500k diff-character rate limit, no review performed; downvote is on the limit notice only, not on Sourcery's judgment.

Gemini — sunset notice, no review; downvoted as non-actionable.

cubic — no findings surfaced.

@greptileai review

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Changed files =='
git diff --name-status "$(git merge-base HEAD origin/main)" HEAD || git diff --name-status HEAD~1 HEAD

printf '%s\n' '== Candidate package/source files =='
fd -t f -E node_modules -E dist -E build | sed -n '1,240p'

printf '%s\n' '== Child-process imports and invocations =='
rg -n -i -C 4 \
  '(from\s+["'\''](?:node:)?child_process["'\'']|require\(["'\''](?:node:)?child_process["'\'']\)|\b(?:spawnSync|execSync|execFileSync|spawn|exec|execFile)\s*\()' \
  -g '!node_modules/**' -g '!dist/**' -g '!build/**' .

printf '%s\n' '== pm command construction / output reads =='
rg -n -i -C 4 \
  '(\bpm\s+(?:list-all|show|[a-z-]+)|["'\'']pm["'\'']|\.stdout\b|JSON\.parse\s*\()' \
  -g '!node_modules/**' -g '!dist/**' -g '!build/**' .

Length of output: 179718


🏁 Script executed:

#!/bin/bash
set -euo pipefail

base="$(git merge-base HEAD origin/main 2>/dev/null || git rev-parse HEAD~1)"

printf '%s\n' '== PR diff: index.ts and dry-run tests =='
git diff --unified=80 "$base" HEAD -- index.ts test/dryrun-preview.test.ts

printf '%s\n' '== Import-flow symbols =='
ast-grep outline index.ts --items all --type function --match 'runImport|readPmItems|runAtomicImport|pmRun'

printf '%s\n' '== Import execution and result paths =='
rg -n -C 24 \
  '(shouldMatchExisting|opts\.dryRun|wouldImport|wouldUpdate|wouldSkip|indexByProvenance|readPmItems\(|runAtomicImport\()' \
  index.ts test/dryrun-preview.test.ts

printf '%s\n' '== All production spawnSync call sites with surrounding source =='
rg -n -C 10 '\bspawnSync\s*\(' index.ts scripts/prepare-merge-driver.mjs

Length of output: 87916


@unbraind Full re-review complete — no findings.

Focused audit results:

  • All production pm output reads are now guarded by the shared 64 MiB buffer: readPmItems(), pmRun(), and the afterCommand pm show hook.
  • The remaining child-process uses either invoke gh, inherit stdio (pm merge install), or are test-only fixture helpers; no additional runtime pm reader was missed.
  • Non-atomic dry runs now build the provenance index, report per-item import/update, return wouldUpdate, and match the atomic preview split. The new tests cover both the linked-item regression and atomic/non-atomic parity.

I did not execute the test suite because repository code execution is unavailable in this review sandbox.

✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 26 minutes.

unbraind and others added 6 commits July 24, 2026 22:46
…review updates

Two defects found by running the importer against a real 443-item workspace
rather than fixtures. Both make the integration fail or mislead exactly when a
tracker is mature enough to matter.

1. `readPmItems()` spawned `pm list-all --full --include-body` without a
   `maxBuffer`, so Node's 1 MiB default applied. That workspace's dump is
   1,052,859 bytes — 4 KiB over the cap — so the child was killed with
   `status: null`, `error.code: ENOBUFS` and EMPTY stderr, and the code reported
   a bare `pm list-all failed` with nothing to diagnose. Every import, atomic
   plan and sync against such a workspace was dead with no actionable message.

   Now capped at 64 MiB (matching pm-changelog / pm-context / pm-brief, which
   already learned this), applied to `readPmItems()`, the central `pmRun()`
   spawner and the `afterCommand` hook's `pm show`. A buffer overrun is now
   named explicitly — the message says the output exceeded the read buffer and
   suggests narrowing the import (`--labels`, `--since`) — instead of being
   reported as an unexplained failure.

2. The non-atomic `--dry-run` path deliberately skipped building the provenance
   index (`shouldMatchExisting = !dryRun || atomic`), so every issue was
   previewed as a create: "Would import 29, skip 0" where the real run performs
   updates for already-linked issues. A preview that overstates creates reads as
   "this will duplicate my whole tracker" — the single thing `--dry-run` exists
   to rule out. `--atomic --dry-run` reported the split correctly, so the two
   paths disagreed about the same plan.

   The index is now built for dry runs too, and the non-atomic preview labels
   each issue `import`/`update` and reports "Would import X, update Y, skip Z",
   returning `wouldUpdate` alongside `wouldImport`/`wouldSkip` like the atomic
   path. Note this also means dry runs now read the tracker — which is why (1)
   had to be fixed in the same change, or previews would newly hit ENOBUFS.

Verified against the same real workspace: non-atomic reports
"Would import 24, update 5, skip 0"; `--atomic --dry-run`, which previously died
with ENOBUFS, now completes and agrees exactly (24/5/0). 168/168 tests pass,
including two new tests covering the preview split and the two paths' agreement.

Items: pm-github-<pending> (see PR)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…fects

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Greptile review findings, all confirmed:

- **P2 — the ENOBUFS diagnostic prescribed an impossible remedy.** It told the
  reader to raise the cap, but the cap was a module-private compile-time
  constant with no override, so a published-package consumer could only follow
  that advice by patching and rebuilding. The cap now reads an env var
  (`PM_JSON_MAX_BUFFER` / `PM_LIST_MAX_BUFFER`, in bytes), falling back to the
  default on any invalid or non-positive value so the guard cannot be silently
  disabled.
- **P2 (pm-starter) — inserting the constant between the existing JSDoc and
  `readPmItems` orphaned the doc comment**, dropping the "never throws / safe
  read pattern" contract from the consumer-facing `dist/index.d.ts`. Verified in
  the generated output, then moved the block above the JSDoc; the contract is
  back in `dist/index.d.ts`.
- **P1 (starters) — a genuine nonzero `pm` exit was still silent.** Only the
  `result.error` branch was hardened, so an unreadable or invalid workspace still
  returned an empty result with stderr discarded — leaving exactly the failure
  mode this change set out to remove: silence that looks like "no items" /
  "no matches". Both never-throw paths now log the exit code and stderr while
  keeping their empty-result contract.

Typecheck and full test suite green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to the Greptile round-2 findings.

- The cap was resolved once at module load, so the `PM_JSON_MAX_BUFFER` override
  only applied if the env var was set before the module was imported — an
  import-order dependency that also made the branch untestable. Now resolved per
  call (one integer parse against a process spawn: free), and the ENOBUFS message
  reports the limit actually in effect rather than a constant's name.
- Added coverage for the failure contract in the starter packages: a real pm
  workspace read with the cap forced to 64 bytes must return the documented empty
  result AND report the overrun on stderr. Without a test, "a read failure must
  not look like an empty workspace" was an assertion in a commit message rather
  than something the suite enforces.

Typecheck and the full test suite green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eric prefix

Greptile P1: `Number.parseInt("64MiB", 10)` returns 64, so that value silently
imposed a **64-BYTE** cap — breaking every ordinary read while appearing to honor
the documented invalid-value fallback. The plausible-looking typo was the worst
case: it turned a safety guard into a guaranteed failure.

Switched to `Number()`, which rejects the whole string, plus
`Number.isSafeInteger(raw) && raw > 0`. Covered in the starter suite across
"64MiB", "64 MB", "abc", "-1", "0", "6.5" and "", each of which must fall back to
the default, while a valid explicit value is still honored.

Also narrowed the starters' overrun message: those reads are always the full
workspace, so "narrow the operation" was a dead instruction — the message now
names only the lever the reader has.

Typecheck and full test suites green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CHANGELOG.md is generated, so the rebase conflict against the merged
changelog-hygiene PR is resolved by regeneration rather than by hand-picking
hunks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@unbraind
unbraind force-pushed the fix/maxbuffer-and-dryrun-update-preview branch from 9af7cd8 to 9d2070b Compare July 24, 2026 20:47
@unbraind

Copy link
Copy Markdown
Owner Author

Force-pushed: rebased onto main after #12 merged. The only conflict was CHANGELOG.md, which is generated by pm-changelog from the pm tracker — resolved by regenerating rather than hand-picking hunks, so the result is exactly what npm run changelog:full produces. changelog:check and 168/168 tests green on the new head.

Also carried in from the Greptile round on the sibling packages in this sweep (same code shape, same defects):

  • the read cap is now env-overridable (PM_JSON_MAX_BUFFER, in bytes) and resolved per call, so the ENOBUFS message's advice is actually followable — it previously named a module-private compile-time constant;
  • the override is parsed with Number() + Number.isSafeInteger, not parseInt: parseInt("64MiB") returns 64, which would have imposed a 64-byte cap and broken every read while appearing to honor the documented invalid-value fallback.

@greptileai review

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Rate Limit Exceeded

@unbraind have exceeded the limit for the number of chat messages per hour. Please wait 10 minutes and 23 seconds before sending another message.

@unbraind
unbraind merged commit 0dc2aa2 into main Jul 24, 2026
5 checks passed
@unbraind
unbraind deleted the fix/maxbuffer-and-dryrun-update-preview branch July 24, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant