Skip to content

fix(list): skip unresolvable entries when dating changes - #1866

Merged
clay-good merged 2 commits into
Fission-AI:mainfrom
dwin-gharibi:fix-list-dangling-symlink
Sep 16, 2026
Merged

clay-good merged 2 commits into
Fission-AI:mainfrom
dwin-gharibi:fix-list-dangling-symlink

Conversation

@dwin-gharibi

@dwin-gharibi dwin-gharibi commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1865.

Why

openspec list sorts changes by recency. To do that, getLastModified
(src/core/list.ts:44-71) walks every entry of each change, dotfiles included,
and calls fs.stat on each non-directory (:54) with no error handling.
fs.stat follows symlinks, so any entry that no longer resolves throws, and the
whole command fails. Such entries include:

  • a dangling symlink. Emacs creates one by default beside every file with
    unsaved changes: .#tasks.md, pointing at user@host.PID:TIMESTAMP.
  • a symlink loop (ELOOP)
  • a file removed between readdir and stat, such as an editor's temp file

With openspec/changes/add-thing/.#tasks.md dangling:

$ openspec list
✖ Error: ENOENT: no such file or directory, stat '.../openspec/changes/add-thing/.#tasks.md'
$ openspec list --json          # exit 1
{"changes":[],"root":null,"status":[{"severity":"error","code":"list_error","message":"ENOENT: ..."}]}

list --json is how agents discover changes, and it reports none at all while
the file is open with unsaved edits. view, status and validate walk the
same tree without failing; only list broke.

Verified on a build of main @ 9d4e597 and on the published
@fission-ai/openspec@1.13.0.

What Changes

  • If reading a subdirectory or statting an entry fails with ENOENT or ELOOP,
    that one entry is skipped when computing the change's last-modified time. A
    new isUnresolvableEntryError helper sits next to the existing
    isMissingPathError.

Unchanged:

  • Valid symlinks are still followed and dated as before.
  • Symlinked directories are still not walked. Dirent reports them as
    symlinks, not directories.
  • Any other error, such as EACCES, still fails the listing, as before.
  • A change with nothing datable still falls back to the directory's own mtime.

Testing

test/core/list.test.ts gains 5 tests under "entries that cannot be stat-ed". I
wrote them first. On main, 4 failed: ENOENT twice, ELOOP once, and CLI exit

  1. The valid-symlink control passed. All pass after the fix.
  • a dangling lock symlink at a change's top level, with a second change listed
    alongside it and its task counts intact
  • a dangling symlink in a nested directory
  • a symlink loop
  • control: a change holding a valid symlink still lists, and is dated by its
    newest file
  • end to end: openspec list --json through the built CLI exits 0 and lists
    the change

The symlink tests use it.skipIf(process.platform === 'win32'), the repo's
existing convention, because creating symlinks on Windows needs elevated
rights. The fix itself is platform-independent. The existing ListCommand
tests pass unchanged.

CI parity: I ran the CI steps in a clean Linux container under Node 20.19.0 (the
CI version), after pnpm install --frozen-lockfile. pnpm run build,
pnpm exec tsc --noEmit and pnpm lint pass. The container was shared with
other test runs (load average 12–18 on 6 CPUs). That load made the repo's
CLI-spawning tests time out under the default 10-second testTimeout, so I ran
the full suite with --testTimeout=120000 --hookTimeout=120000. It ran 4568
tests: 4567 passed and 1 failed.

The one failure was environmental: an npm source installation test whose
npm subprocess hit its 30-second spawnSync timeout (ETIMEDOUT). I reran
that file on its own, under the suite lock with --maxWorkers=2. It passes on
this branch, and it passes the same way on a pristine main @ 9d4e597.

CI's validate-changesets job runs on Node 24, and
@changesets/cli 3 needs Node 22 or newer because it calls
module.enableCompileCache. So I ran pnpm exec changeset status under
Node 22; it lists the patch bump for @fission-ai/openspec.

Changeset

.changeset/list-skips-unresolvable-entries.md (patch).

Summary by CodeRabbit

  • Bug Fixes
    • Fixed openspec list failing when a change contains dangling or looping symlinks, or files removed during scanning.
    • Listings now skip unresolvable entries while continuing to display valid changes.
    • Valid symlinks continue to retain their correct last-modified dates.
    • Other errors, such as permission failures, still report normally.

To sort changes by recency, list stats every file inside each change,
and any entry it could not stat failed the whole command. A dangling
symlink, such as the .#file lock Emacs keeps beside every file with
unsaved edits, or a symlink loop made list exit 1 and list --json report
no changes at all.

Skip an entry that no longer resolves (ENOENT or ELOOP) when computing a
change's last-modified time. Valid symlinks are dated as before, and any
other error still fails the listing.
Copilot AI lite review requested due to automatic review settings September 12, 2026 16:15
@dwin-gharibi
dwin-gharibi requested a review from a team as a code owner September 12, 2026 16:15
@dwin-gharibi
dwin-gharibi requested review from clay-good and removed request for a team September 12, 2026 16:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Warning

Review limit reached

Next included review available in 53 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

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

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: f4315229-1d12-4ff1-b13d-a4b7a5fc2fbb

📥 Commits

Reviewing files that changed from the base of the PR and between 5bdcefb and 27e59c5.

📒 Files selected for processing (1)
  • test/core/list.test.ts

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 86f7525f-779d-4236-b554-b0c19adb209c

📥 Commits

Reviewing files that changed from the base of the PR and between 9d4e597 and 5bdcefb.

📒 Files selected for processing (3)
  • .changeset/list-skips-unresolvable-entries.md
  • src/core/list.ts
  • test/core/list.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 4 remain after this review.


📝 Walkthrough

Walkthrough

openspec list now skips entries that fail with ENOENT or ELOOP during timestamp calculation. Valid symlinks retain their timestamps, while other errors still fail the listing. Tests cover direct, nested, looping, valid, and CLI scenarios.

Changes

List resilience

Layer / File(s) Summary
Resilient timestamp walk
src/core/list.ts
The walk detects ENOENT and ELOOP, skips those entries, and rethrows other errors.
Coverage and release declaration
test/core/list.test.ts, .changeset/list-skips-unresolvable-entries.md
Tests cover dangling symlinks, nested entries, symlink loops, valid symlinks, and CLI output. The changeset declares a patch release.

Priority: ➖ Normal

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

Change: Bug fix · Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 5bdce

The listing resilience change is covered for dangling links, loops, valid symlinks, nested entries, and CLI JSON output. No merge-blocking risk was identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 2 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: updating list behavior to skip unresolvable entries when calculating change dates.
Linked Issues check ✅ Passed The implementation meets issue #1865. getLastModified catches ENOENT and ELOOP for individual entries, including recursive entries, and continues processing the change. Other errors remain failu…
Out of Scope Changes check ✅ Passed The changes stay within issue #1865. The source change implements tolerant recency calculation. The tests verify the required listing and symlink behavior. The patch changeset documents the fix and do…
Full details: Docstring Coverage

Explanation

Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 2 files. (1 skipped: 1 unsupported.)

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@clay-good

Copy link
Copy Markdown
Collaborator

Hardening pushed for merge:

  • 27e59c5 test(list): pins that a permission error (EACCES on an unreadable directory inside a change) still fails the listing, so skipping ENOENT/ELOOP cannot hide real errors.

Verified: reproduced list --json exiting 1 with a dangling .#tasks.md symlink on main via the real CLI; exits 0 and lists the change on this branch. list.test.ts 16/16; the dangling, nested and loop tests fail on main. Sorting for normal changes is unchanged because the stat path is identical outside the catch.

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The recency walk now skips only entries that disappeared or cannot resolve, while retaining valid symlink dating and surfacing other filesystem errors. Focused list and CLI tests pass.

@clay-good
clay-good added this pull request to the merge queue Sep 16, 2026
Merged via the queue into Fission-AI:main with commit 8146be5 Sep 16, 2026
14 checks passed
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.

One dangling symlink in a change makes openspec list fail for the whole project

4 participants