Skip to content

Propagate an unresolvable path in the shared script launcher so gates cannot silently skip - #63

Merged
unbraind merged 2 commits into
mainfrom
fix-launcher-entry-guard
Aug 10, 2026
Merged

unbraind merged 2 commits into
mainfrom
fix-launcher-entry-guard

Conversation

@unbraind

@unbraind unbraind commented Aug 10, 2026 •

Copy link
Copy Markdown
Owner

The defect

isMainInvocation in scripts/script-launcher.ts caught a realpathSync failure and
returned false. runIfMain reads false as "not the entry point", so the script it guards
never runs — and npm run docstring exits 0 having scanned nothing. A required release
check reporting success without doing its job is the single failure this gate exists to
prevent. Letting realpathSync throw turns it into a loud non-zero exit.

The docstring argued the right principle and then described the opposite behaviour: it said
"a release script that no-ops without erroring is worse than one that throws" and then used
fail closed to name the branch that no-ops. Here the crash is the safe outcome.

Why this repo was missed by the fleet sweep

The fleet-wide fix for this defect surveyed scripts/docstring-gate.ts for the guard. This
repo had refactored it into a shared script-launcher.ts, with the gate only re-exporting
it, so the survey found nothing and reported the repo clean. Factoring a defect out of the
file people search makes it invisible to the search.

What was deliberately kept

Unlike the copies elsewhere, this variant resolves both sides through realpathSync to
normalise Windows drive-letter casing. That reasoning is sound, so only the catch was
removed — the fix does not reintroduce the casing bug the both-sides comparison prevents.

Verification

  • npm test — 71 tests, all pass
  • npm run coverage — thresholds met, unchanged
  • npm run changelog:check — passes
  • Behavioural probe: isMainInvocation(["node","/nonexistent/x.ts"], ...) now throws ENOENT

pm items

  • pm-linear-lonp — The shared script launcher skipped every gate it guarded when a path could not be resolved

Summary by Sourcery

Ensure the shared script launcher fails loudly when entry paths cannot be resolved so release gates cannot silently skip.

Bug Fixes:

  • Propagate realpath resolution failures from isMainInvocation instead of treating them as non-main invocations that skip guarded scripts.

Enhancements:

  • Clarify isMainInvocation documentation to explain propagation of unresolvable paths and the safety rationale for crashing over silent skips.

Tests:

  • Add a docstring gate test asserting that isMainInvocation throws on an unresolvable entry path instead of silently declining to run the gate.

Chores:

  • Record the pm-linear-lonp incident in the PM agent history and issues metadata.

Summary by cubic

Propagate unresolvable entry paths in the shared script launcher so gates cannot silently skip. Release checks now fail loudly (non-zero) instead of exiting 0 when the entry path can’t be resolved (Linear pm-linear-lonp).

  • Bug Fixes
    • isMainInvocation: let realpathSync errors bubble instead of returning false; still returns false only when paths resolve to different files.
    • Keep both-sides realpathSync comparison to normalize Windows drive-letter casing; add a test that asserts the throw on ENOENT.

Written for commit 773f095. Summary will update on new commits.

Review in cubic

… gate

isMainInvocation caught a realpathSync failure and returned false. runIfMain
reads false as "not the entry point", so the script it guards never runs and
a required release check exits 0 having done nothing — the one failure these
gates exist to prevent. Letting realpathSync throw makes that a loud
non-zero exit instead.

The docstring argued the right principle and then did the opposite: it said
a release script that no-ops without erroring is worse than one that throws,
and used "fail closed" to name the branch that no-ops. Here the crash is the
safe outcome.

The both-sides realpathSync comparison is kept. It exists to normalise
Windows drive-letter casing, which is a separate and correct concern, so
only the catch is removed.

A test now asserts the throw, so the guard cannot regress silently.

@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

@sourcery-ai

sourcery-ai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR changes the shared script launcher so an unresolvable entry path causes the process to fail loudly instead of silently skipping the guarded gate, and adds a regression test plus pm tracking artifacts documenting the defect.

Sequence diagram for isMainInvocation error propagation in the shared script launcher

sequenceDiagram
  actor Developer
  participant npm
  participant docstring_gate
  participant script_launcher as isMainInvocation
  participant fs as realpathSync

  Developer->>npm: npm run docstring
  npm->>docstring_gate: start
  docstring_gate->>script_launcher: isMainInvocation(argv, moduleUrl)
  alt entry path resolves
    script_launcher->>fs: realpathSync(argv[1])
    fs-->>script_launcher: resolved_entry_path
    script_launcher->>fs: realpathSync(fileURLToPath(moduleUrl))
    fs-->>script_launcher: resolved_module_path
    script_launcher-->>docstring_gate: true or false
    opt true
      docstring_gate->>docstring_gate: run main gate
    end
  else entry path does not resolve (ENOENT)
    script_launcher->>fs: realpathSync(argv[1])
    fs-->>script_launcher: ENOENT error
    script_launcher-->>docstring_gate: propagate ENOENT
    docstring_gate-->>npm: non-zero exit (gate fails loudly)
  end
Loading

File-Level Changes

Change Details Files
Make isMainInvocation propagate realpathSync failures instead of treating them as a non-main invocation.
  • Updated isMainInvocation docstring to clarify propagation semantics and safety rationale.
  • Removed try/catch around realpathSync calls so ENOENT and similar errors bubble up.
  • Kept the realpathSync-on-both-sides comparison to preserve Windows drive-letter and symlink normalization behavior.
scripts/script-launcher.ts
Add regression test ensuring an unresolvable entry path throws instead of silently skipping the docstring gate.
  • Added a test that constructs a temporary gate script and calls isMainInvocation with a non-existent entry path.
  • Asserted that the invocation throws ENOENT and documented why crashing is the safe outcome for release gates.
  • Ensured temporary filesystem artifacts are cleaned up after the test.
test/docstring-gate.test.ts
Record the product-management issue for the docstring gate defect in pm agent artifacts.
  • Added pm-linear-lonp history entry for the shared script launcher defect.
  • Added pm-linear-lonp issue description file to track the gate-skipping behavior.
.agents/pm/history/pm-linear-lonp.jsonl
.agents/pm/issues/pm-linear-lonp.toon

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 40 seconds

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

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: f09a4178-c7c7-4bd7-bd00-7f3ab9c60b2c

📥 Commits

Reviewing files that changed from the base of the PR and between 773f095 and 95a4a76.

📒 Files selected for processing (2)
  • .agents/pm/history/pm-linear-lonp.jsonl
  • .agents/pm/issues/pm-linear-lonp.toon

Summary by CodeRabbit

  • Bug Fixes

    • Direct script launches now correctly surface path-resolution errors instead of being mistaken for imported modules.
    • Required launch checks no longer get silently skipped when the entry path cannot be resolved.
    • Existing behavior for valid paths and Windows path-casing differences remains unchanged.
  • Tests

    • Added regression coverage to verify that unresolved entry paths report an error as expected.

Walkthrough

isMainInvocation now propagates path-resolution errors instead of treating them as imports. A regression test covers unresolved entry paths. Project issue records document the defect, investigation, and fix scope.

Changes

Launcher error propagation

Layer / File(s) Summary
Propagate path-resolution errors and validate direct invocation
scripts/script-launcher.ts, test/docstring-gate.test.ts, .agents/pm/issues/pm-linear-lonp.toon, .agents/pm/history/pm-linear-lonp.jsonl
isMainInvocation now propagates realpathSync failures. Missing or different paths still return false, and matching paths return true. The regression test verifies ENOENT propagation and cleanup. Issue records document the defect and required fix scope.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: propagate unresolved paths so guarded release gates cannot skip silently.
Description check ✅ Passed The description directly explains the defect, implementation, preserved behavior, regression test, and verification results.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-launcher-entry-guard

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.

The guard had been refactored into the shared script launcher, so a survey
that searched scripts/docstring-gate.ts found nothing here and reported the
repository clean.
@unbraind

Copy link
Copy Markdown
Owner Author

@coderabbitai full review
@greptileai

Reviewer context — the premise matters more than the diff here:

isMainInvocation caught a realpathSync failure and returned false. runIfMain reads
false as "not the entry point", so the guarded script never runs and a required release
check exits 0 having done nothing. Letting realpathSync throw makes that a loud
non-zero exit. The original docstring argued the right principle — "a release script that
no-ops without erroring is worse than one that throws" — and then used fail closed to name
the branch that no-ops.

Please check specifically:

  1. The both-sides realpathSync comparison is retained on purpose. It normalises Windows
    drive-letter casing; only the catch was removed. Flag it if you think dropping the catch
    changes the casing behaviour in any way.
  2. A genuinely different entry path must still return false — that is how a test import
    declines to run the script. Only an unresolvable path should propagate.
  3. Coverage thresholds are untouched. Please confirm nothing was weakened to accommodate the
    removed branch.
  4. This repo was missed by the earlier fleet sweep because the guard lives in a shared
    script-launcher.ts that the gate only re-exports. If you can see other gate-bearing
    scripts in this repo reachable through the same launcher that I have not covered, say so.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Rate Limit Exceeded

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

@greptile-apps

greptile-apps Bot commented Aug 10, 2026

Copy link
Copy Markdown

Greptile Summary

This PR makes the shared script launcher propagate path-resolution failures so the docstring release gate exits non-zero rather than silently skipping.

  • Retains the both-sides realpathSync comparison, preserving symlink and Windows drive-letter normalization.
  • Preserves false for genuinely different resolvable entry paths.
  • Adds a regression test for an unresolvable entry path.
  • Records the associated PM issue and history.

Confidence Score: 5/5

The PR appears safe to merge because the launcher now fails loudly on unresolvable paths while preserving normalized matching and resolvable non-match behavior.

The only production launcher consumer is the docstring gate, the propagated exception produces the intended non-zero release-check failure, existing tests retain coverage of true and false path comparisons, and coverage thresholds and test inclusion remain unchanged.

Important Files Changed

Filename Overview
scripts/script-launcher.ts Removes the catch that converted path-resolution failures to false while retaining both normalized path resolutions and the distinct-path behavior.
test/docstring-gate.test.ts Adds an included, isolated regression test confirming that ENOENT propagates; existing tests continue to cover matching and different resolvable paths.
.agents/pm/issues/pm-linear-lonp.toon Records the launcher defect, intended failure semantics, and rationale for retaining both-side path normalization.
.agents/pm/history/pm-linear-lonp.jsonl Adds the corresponding issue history without affecting runtime behavior.

Reviews (1): Last reviewed commit: "docs(pm): record why the fleet sweep mis..." | Re-trigger Greptile

@unbraind

Copy link
Copy Markdown
Owner Author

@coderabbitai review

Re-requesting because your last pass on this PR predates the current head (or was cut short by the hourly rate limit), and a green check from a rate-limited run is not a review. Flagging it explicitly rather than reading the silence as approval.

Since your last look, the substantive change is that the symlink regression test now puts the symlink in moduleUrl rather than in argv[1] alone. Your own finding on the sibling PRs is what prompted it: the earlier test passed against both the fixed and the broken implementation, so it did not bind the fix. Verified by reverting the one-line implementation change — 6 pass / 1 fail reverted, 7 pass / 0 fail restored.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== PR diff summary =='
git diff --stat HEAD^ HEAD
echo
echo '== PR diff =='
git diff --find-renames --find-copies HEAD^ HEAD -- \
  scripts/script-launcher.ts \
  test/docstring-gate.test.ts \
  .agents/pm/history/pm-linear-lonp.jsonl \
  .agents/pm/issues/pm-linear-lonp.toon

echo
echo '== Launcher outline =='
ast-grep outline scripts/script-launcher.ts --items all || true
echo
echo '== Launcher source =='
cat -n scripts/script-launcher.ts

echo
echo '== Relevant test outline =='
ast-grep outline test/docstring-gate.test.ts --items all || true
echo
echo '== Relevant test source =='
cat -n test/docstring-gate.test.ts

echo
echo '== Coverage and test configuration candidates =='
fd -HI -t f '^(package\.json|vitest\.config\..*|jest\.config\..*|nyc\.config\..*|\.nycrc.*|c8rc.*|tsconfig.*\.json)$' . \
  -E node_modules -E .git | sort

echo
echo '== Coverage-related configuration =='
rg -n -i -C 4 '(coverage|threshold|lines|branches|functions|statements)' \
  -g 'package.json' \
  -g 'vitest.config.*' \
  -g 'jest.config.*' \
  -g 'nyc.config.*' \
  -g '.nycrc*' \
  -g 'c8rc.*' \
  -g 'tsconfig*.json' . || true

echo
echo '== Shared launcher imports and calls =='
rg -n -P -C 4 '(from\s+[\'"][^\'"]*script-launcher|require\([\'"][^\'"]*script-launcher|(?:runIfMain|isMainInvocation)\s*\()' \
  -g '*.ts' -g '*.tsx' -g '*.mts' -g '*.cts' . \
  -g '!node_modules/**' -g '!coverage/**'

echo
echo '== Changed coverage configuration files, if any =='
git diff --name-status HEAD^ HEAD -- \
  'package.json' 'vitest.config.*' 'jest.config.*' 'nyc.config.*' '.nycrc*' 'c8rc.*' 'tsconfig*.json'

Length of output: 24302


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Repository state and refs =='
git status --short --branch
git remote -v
git branch -a --no-color
echo
echo '== Recent commits =='
git log --oneline --decorate -12

echo
echo '== Candidate base refs and merge bases =='
for ref in origin/main origin/master main master; do
  if git rev-parse --verify --quiet "$ref" >/dev/null; then
    printf '%s: ' "$ref"
    git merge-base HEAD "$ref"
  fi
done

base=''
for ref in origin/main origin/master main master; do
  if git rev-parse --verify --quiet "$ref" >/dev/null; then
    base=$(git merge-base HEAD "$ref")
    printf '\n== Full diff against %s (%s) ==\n' "$ref" "$base"
    git diff --stat "$base" HEAD
    git diff --name-status "$base" HEAD
    break
  fi
done

if [ -z "$base" ]; then
  echo 'No standard base ref exists; inspect GitHub PR metadata.'
  gh pr view 63 --repo unbraind/pm-linear --json baseRefName,baseRefOid,headRefName,headRefOid,commits
else
  echo
  echo '== Relevant full PR diff =='
  git diff --find-renames "$base" HEAD -- scripts/script-launcher.ts test/docstring-gate.test.ts package.json
  echo
  echo '== Coverage configuration changes in full PR =='
  git diff --name-status "$base" HEAD -- package.json tsconfig.json tsconfig.test.json
fi

echo
echo '== Direct imports and calls of the shared launcher =='
rg -n -C 4 -e 'script-launcher' -e 'runIfMain[[:space:]]*\(' -e 'isMainInvocation[[:space:]]*\(' \
  -g '*.ts' -g '*.tsx' -g '*.mts' -g '*.cts' \
  -g '!node_modules/**' -g '!coverage/**' .

echo
echo '== TypeScript files that identify themselves as gates or execute via runIfMain =='
rg -n -i -C 3 -e 'gate' -e 'runIfMain[[:space:]]*\(' \
  -g 'scripts/*.ts' -g '!node_modules/**' scripts test || true

echo
echo '== Symlink-related test code at current HEAD =='
rg -n -i -C 5 'symlink|moduleUrl|pathToFileURL' test scripts \
  -g '*.ts' -g '*.mts' -g '*.cts'

Length of output: 40811


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== GitHub PR `#63` metadata =='
gh pr view 63 --repo unbraind/pm-linear \
  --json number,url,state,baseRefName,baseRefOid,headRefName,headRefOid,commits

echo
echo '== GitHub PR `#63` changed files =='
gh api repos/unbraind/pm-linear/pulls/63/files --paginate \
  --jq '.[] | {filename, status, sha, additions, deletions, patch}'

echo
echo '== Symlink API and test-text search across the complete PR range =='
git log --oneline 6e2b02b..HEAD
git log -S'symlink' --oneline 6e2b02b..HEAD -- test/docstring-gate.test.ts scripts/script-launcher.ts || true
rg -n -i -C 3 'symlinkSync|symlink|readlink|moduleUrl' \
  test scripts -g '*.ts' -g '*.mts' -g '*.cts' || true

echo
echo '== Functional commit test diff =='
git show --format=fuller --find-renames 773f095 -- test/docstring-gate.test.ts scripts/script-launcher.ts

echo
echo '== All runIfMain call sites, with definitions excluded =='
rg -n -P -C 3 '\brunIfMain\s*\(' \
  -g '*.ts' -g '*.tsx' -g '*.mts' -g '*.cts' \
  -g '!scripts/script-launcher.ts' -g '!node_modules/**' -g '!coverage/**' .

Length of output: 23929


@unbraind I completed the review of GitHub PR #63 at head 95a4a76.

Finding

  • test/docstring-gate.test.ts Lines 142-159 does not contain the reported symlink regression test. It tests only an unresolvable argv[1] path. The GitHub PR file list and the complete PR range contain no symlinkSync, symlink, or moduleUrl symlink test. Please push the revised test to this PR, or identify the PR that contains it. A moduleUrl symlink case is needed to bind the retained second realpathSync call. Without it, a future change that resolves only argv[1] can reintroduce the Windows path-normalization failure without this test detecting it.

Checks completed

  • scripts/script-launcher.ts retains realpathSync on both paths. Removing the catch does not change the dual-path normalization behavior.
  • A different resolvable entry path still returns false. test/docstring-gate.test.ts Lines 88-90 covers this behavior.
  • The new missing-entry test fails with the prior catch { return false; } implementation. It correctly binds the primary failure-propagation fix.
  • The PR does not modify package.json, tsconfig.json, or tsconfig.test.json. Coverage thresholds remain lines 70, branches 80, and functions 80.
  • scripts/docstring-gate.ts is the only non-test runIfMain consumer in this repository. I found no additional launcher-reachable gate scripts.
  • I did not run repository tests because the review sandbox does not permit execution of repository files.
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@unbraind

Copy link
Copy Markdown
Owner Author

Merging. CodeRabbit's review is current against this head and answered every question I asked, including the one that mattered most — it independently confirmed the revert-proof:

The new missing-entry test fails with the prior catch { return false; } implementation. It correctly binds the primary failure-propagation fix.

That matches what I measured locally, and it is the property worth having: a test that fails when the thing it guards is removed.

It also confirmed the two things I was most worried about breaking — the dual-path realpathSync normalization is retained (only the catch was removed, so the Windows drive-letter casing behaviour is unchanged), and a different resolvable entry path still returns false, which is how a test import declines to run the gate.

One observation from its report worth recording rather than acting on here: it noted this repo's coverage thresholds are lines 70, branches 80, functions 80 — below the fleet's 100/100/100/100 mandate. That is pre-existing and out of scope for this PR, and it is already tracked as a fleet coverage-gap epic. Flagging it so the number in that report is not read as an endorsement of the threshold.

@unbraind
unbraind merged commit b94fca5 into main Aug 10, 2026
8 checks passed
@unbraind
unbraind deleted the fix-launcher-entry-guard branch August 10, 2026 07:39
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