Skip to content

fix(status): exit gracefully when no changes exist#759

Open
fsilvaortiz wants to merge 2 commits intoFission-AI:mainfrom
fsilvaortiz:fix/714-graceful-status-no-changes
Open

fix(status): exit gracefully when no changes exist#759
fsilvaortiz wants to merge 2 commits intoFission-AI:mainfrom
fsilvaortiz:fix/714-graceful-status-no-changes

Conversation

@fsilvaortiz
Copy link

@fsilvaortiz fsilvaortiz commented Feb 25, 2026

Summary

Changes

File Change
src/commands/workflow/shared.ts Extract getAvailableChanges as public export
src/commands/workflow/status.ts Early check + graceful exit when no changes
test/commands/artifact-workflow.test.ts 2 new tests (text + JSON graceful exit)
.changeset/graceful-status-no-changes.md Patch changeset
openspec/changes/graceful-status-no-changes/ OpenSpec change artifacts (proposal, design, specs, tasks)

Test plan

  • openspec status with no changes → friendly message, exit 0
  • openspec status --json with no changes → valid JSON, exit 0
  • openspec status with changes but no --change → error listing changes (preserved)
  • openspec status --change non-existent → error "not found" (preserved)
  • Full onboard scenario: initstatusnew changestatus --change works end-to-end
  • All existing tests pass (18 pre-existing zsh-installer failures unrelated)

🤖 Generated with Claude Code using claude-opus-4-6.

Summary by CodeRabbit

  • Bug Fixes

    • Status now exits gracefully with code 0 when no active changes exist: shows a friendly hint in text mode and returns {"changes": [], "message":"No active changes."} in JSON mode instead of failing.
  • Tests

    • Added tests verifying graceful status behavior for both text and JSON outputs when no changes are present.

@fsilvaortiz fsilvaortiz requested a review from TabishB as a code owner February 25, 2026 18:06
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 738d6a1 and 60c5849.

📒 Files selected for processing (2)
  • openspec/changes/graceful-status-no-changes/design.md
  • openspec/changes/graceful-status-no-changes/proposal.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • openspec/changes/graceful-status-no-changes/design.md

📝 Walkthrough

Walkthrough

Adds graceful handling for openspec status when no changes exist by exporting getAvailableChanges and having statusCommand check for empty changes first; it now exits 0 with a friendly text message or JSON {"changes":[],"message":"No active changes."}. Existing validation behavior remains unchanged.

Changes

Cohort / File(s) Summary
Changelog & OpenSpec docs
\.changeset/graceful-status-no-changes.md, openspec/changes/graceful-status-no-changes/.openspec.yaml, openspec/changes/graceful-status-no-changes/*
Added changeset, OpenSpec config, design, proposal, spec, and tasks describing the new graceful status behavior and rationale.
Specs
openspec/changes/graceful-status-no-changes/specs/graceful-status-empty/spec.md
New spec describing expected text and JSON outputs and scenarios for status when no changes exist.
Core Implementation
src/commands/workflow/shared.ts, src/commands/workflow/status.ts
Exported getAvailableChanges(projectRoot) (previously internal) and updated statusCommand to call it before validation; early-return with exit 0 and appropriate text/JSON when no changes found; preserved validateChangeExists semantics for other flows.
Tests
test/commands/artifact-workflow.test.ts
Added tests asserting openspec status exits 0 with the friendly text and JSON {"changes":[],"message":"No active changes."} when no changes exist.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as Client (openspec)
  participant Status as statusCommand
  participant Shared as getAvailableChanges
  participant FS as FileSystem

  CLI->>Status: run "openspec status" [no --change]
  Status->>Shared: getAvailableChanges(projectRoot)
  Shared->>FS: read openspec/changes directory
  FS-->>Shared: list of change dirs (empty / non-empty)

  alt no changes found
    Status->>Status: stop spinner
    Status-->>CLI: output text "No active changes..." or JSON {"changes":[],"message":"No active changes."}
    Status-->>CLI: exit 0
  else changes exist
    Status->>Status: proceed to validateChangeExists (existing behavior)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 I sniffed the changes, none in sight,
So I twitched my nose and made it right.
JSON or text, no crash or fright,
Exit zero — hop on, delight! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix(status): exit gracefully when no changes exist' directly and clearly describes the main change—making the status command exit gracefully instead of fatally when no changes exist, which is the core objective of this PR.
Linked Issues check ✅ Passed The PR fully addresses issue #714 by implementing graceful handling for 'openspec status' with no changes, supporting both text and JSON outputs with exit code 0, and providing user-friendly messages instead of fatal errors.
Out of Scope Changes check ✅ Passed All changes are directly related to issue #714: extracting getAvailableChanges, updating statusCommand with graceful no-changes handling, adding supporting documentation, and implementing tests. No unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

@greptile-apps
Copy link

greptile-apps bot commented Feb 25, 2026

Greptile Summary

This PR fixes issue #714 by making openspec status exit gracefully (exit code 0) when no changes exist, instead of throwing a fatal error. The implementation extracts getAvailableChanges as a public function from shared.ts and adds early checking in statusCommand to handle the no-changes case before calling validateChangeExists. Both text and JSON output modes are supported.

Key changes:

  • Extracted getAvailableChanges as a public utility function in shared.ts (refactoring, no behavior change)
  • Added early check in status.ts to gracefully handle no-changes case with appropriate messages for text and JSON modes
  • Added comprehensive tests covering both output modes with exit code 0 verification
  • Other commands (apply, show, instructions) remain unaffected and continue using strict validation

The implementation is clean, well-tested, and maintains backward compatibility. No issues found.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The change is a focused bug fix with clear scope. The refactoring (extracting getAvailableChanges) doesn't alter existing behavior, just makes the function reusable. The new graceful handling in statusCommand is well-isolated and properly tested with both text and JSON output modes. All existing validation paths remain unchanged, ensuring other commands are unaffected. Test coverage is comprehensive.
  • No files require special attention

Important Files Changed

Filename Overview
src/commands/workflow/shared.ts Extracted getAvailableChanges as public export; refactored existing logic without changing behavior
src/commands/workflow/status.ts Added graceful handling for no-changes case; supports both text and JSON output modes with exit code 0
test/commands/artifact-workflow.test.ts Added two tests for graceful exit behavior (text and JSON modes); both verify exit code 0 and appropriate output

Last reviewed commit: eca0298

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/commands/workflow/shared.ts (1)

89-109: ⚠️ Potential issue | 🟡 Minor

Orphaned JSDoc comment belongs on validateChangeExists, not above getAvailableChanges.

The block at lines 89-92 was the original JSDoc for validateChangeExists. Inserting getAvailableChanges (with its own JSDoc at lines 93-96) immediately after it leaves the old comment orphaned — TypeScript tooling will silently discard it, and validateChangeExists now has no JSDoc at all.

♻️ Proposed fix
-/**
- * Validates that a change exists and returns available changes if not.
- * Checks directory existence directly to support scaffolded changes (without proposal.md).
- */
-/**
+/**
  * Returns the list of available change directory names under openspec/changes/.
  * Excludes the archive directory and hidden directories.
  */
 export async function getAvailableChanges(projectRoot: string): Promise<string[]> {
   ...
 }

+/**
+ * Validates that a change exists and returns available changes if not.
+ * Checks directory existence directly to support scaffolded changes (without proposal.md).
+ */
 export async function validateChangeExists(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/workflow/shared.ts` around lines 89 - 109, Move the orphaned
JSDoc so it documents the correct function: remove the stray comment block
currently sitting above getAvailableChanges and place it immediately above the
validateChangeExists function declaration; ensure getAvailableChanges retains
its own JSDoc (the comment that currently documents it should remain directly
above getAvailableChanges) so both getAvailableChanges and validateChangeExists
have appropriate, non-duplicate JSDoc blocks.
🧹 Nitpick comments (1)
src/commands/workflow/status.ts (1)

44-57: Double getAvailableChanges call when --change is omitted and changes exist.

When options.change is undefined and available.length > 0, getAvailableChanges(projectRoot) is called at line 45, then called again inside validateChangeExists at line 57 (which always calls it when changeName is undefined). Two readdir round-trips for the same path in the same request.

♻️ Proposed fix — pass the already-resolved list to `validateChangeExists`

One approach: short-circuit entirely once you have available, skipping the second validateChangeExists call in the no---change branch.

-    if (!options.change) {
-      const available = await getAvailableChanges(projectRoot);
-      if (available.length === 0) {
-        spinner.stop();
-        if (options.json) {
-          console.log(JSON.stringify({ changes: [], message: 'No active changes.' }, null, 2));
-          return;
-        }
-        console.log('No active changes. Create one with: openspec new change <name>');
-        return;
-      }
-    }
-
-    const changeName = await validateChangeExists(options.change, projectRoot);
+    let changeName: string;
+    if (!options.change) {
+      const available = await getAvailableChanges(projectRoot);
+      if (available.length === 0) {
+        spinner.stop();
+        if (options.json) {
+          console.log(JSON.stringify({ changes: [], message: 'No active changes.' }, null, 2));
+          return;
+        }
+        console.log('No active changes. Create one with: openspec new change <name>');
+        return;
+      }
+      // Changes exist but --change not provided — replicate the validateChangeExists error path
+      spinner.stop();
+      throw new Error(
+        `Missing required option --change. Available changes:\n  ${available.join('\n  ')}`
+      );
+    } else {
+      changeName = await validateChangeExists(options.change, projectRoot);
+    }

Alternatively, this is low-impact enough that a comment acknowledging the duplicate is sufficient if you prefer to keep validateChangeExists as the single source of truth for that error message.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/workflow/status.ts` around lines 44 - 57, When options.change is
undefined you call getAvailableChanges(projectRoot) and then
validateChangeExists(...) will call getAvailableChanges again; avoid the
duplicate readdir by extending validateChangeExists to accept an optional
precomputed list (e.g., available: string[] | undefined) and use that list if
provided instead of calling getAvailableChanges internally, then call
validateChangeExists(options.change, projectRoot, available) from status.ts when
you already have the available array; update the validateChangeExists signature
and its callers accordingly so the second disk lookup is skipped.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openspec/changes/graceful-status-no-changes/design.md`:
- Line 37: The risk description in the design text is self-contradictory; update
the sentence referencing getAvailableChanges, validateChangeExists and
statusCommand so it correctly states that the extra filesystem read occurs when
changes do exist (i.e., getAvailableChanges returns results and statusCommand
proceeds to call validateChangeExists), and that when no changes exist
statusCommand returns early and only one read happens; reference
getAvailableChanges, validateChangeExists, and statusCommand in the corrected
sentence for clarity.

---

Outside diff comments:
In `@src/commands/workflow/shared.ts`:
- Around line 89-109: Move the orphaned JSDoc so it documents the correct
function: remove the stray comment block currently sitting above
getAvailableChanges and place it immediately above the validateChangeExists
function declaration; ensure getAvailableChanges retains its own JSDoc (the
comment that currently documents it should remain directly above
getAvailableChanges) so both getAvailableChanges and validateChangeExists have
appropriate, non-duplicate JSDoc blocks.

---

Nitpick comments:
In `@src/commands/workflow/status.ts`:
- Around line 44-57: When options.change is undefined you call
getAvailableChanges(projectRoot) and then validateChangeExists(...) will call
getAvailableChanges again; avoid the duplicate readdir by extending
validateChangeExists to accept an optional precomputed list (e.g., available:
string[] | undefined) and use that list if provided instead of calling
getAvailableChanges internally, then call validateChangeExists(options.change,
projectRoot, available) from status.ts when you already have the available
array; update the validateChangeExists signature and its callers accordingly so
the second disk lookup is skipped.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7d1860 and eca0298.

📒 Files selected for processing (9)
  • .changeset/graceful-status-no-changes.md
  • openspec/changes/graceful-status-no-changes/.openspec.yaml
  • openspec/changes/graceful-status-no-changes/design.md
  • openspec/changes/graceful-status-no-changes/proposal.md
  • openspec/changes/graceful-status-no-changes/specs/graceful-status-empty/spec.md
  • openspec/changes/graceful-status-no-changes/tasks.md
  • src/commands/workflow/shared.ts
  • src/commands/workflow/status.ts
  • test/commands/artifact-workflow.test.ts

Extract `getAvailableChanges` as a public function from `validateChangeExists`
and use it in `statusCommand` to detect the no-changes case early. Returns a
friendly message (text and JSON modes) with exit code 0 instead of a fatal error.

Generated with Claude Code using claude-opus-4-6.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@fsilvaortiz fsilvaortiz force-pushed the fix/714-graceful-status-no-changes branch from eca0298 to 738d6a1 Compare February 25, 2026 18:15
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
openspec/changes/graceful-status-no-changes/design.md (1)

37-37: ⚠️ Potential issue | 🟡 Minor

Self-contradictory risk/mitigation sentence — previous review comment not yet addressed.

The risk clause states the extra filesystem read occurs "when no changes exist", while the mitigation in the same sentence says "the extra read only happens when changes do exist." These two halves directly contradict each other and are still present in the current code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/changes/graceful-status-no-changes/design.md` at line 37, The
sentence describing the risk/mitigation is self-contradictory: clarify whether
the extra filesystem read happens when no changes exist or only when changes do
exist by updating the sentence in design.md; reference the behavior of
getAvailableChanges, validateChangeExists and statusCommand to state precisely
that statusCommand returns early when no --change is provided and no changes
exist (avoiding validateChangeExists and the second read), so the extra read
only occurs when changes exist (because validateChangeExists will call
getAvailableChanges again); rewrite the line to remove the contradiction and
make the condition for the extra read explicit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openspec/changes/graceful-status-no-changes/proposal.md`:
- Around line 17-19: Update the "Modified Capabilities" note to mention that
while behavior is unchanged, the internal implementation of validateChangeExists
was refactored to delegate to the newly exported getAvailableChanges; explicitly
reference statusCommand (unchanged), validateChangeExists (refactored), and
getAvailableChanges so readers know the function was re-implemented not left
untouched.

In `@src/commands/workflow/shared.ts`:
- Around line 100-102: The catch in getAvailableChanges currently swallows all
errors and returns []; change it to only suppress ENOENT (directory not present)
and re-throw any other errors so permission or IO errors surface: inside
getAvailableChanges catch the error, if (err.code === 'ENOENT') return [];
otherwise throw err (or rethrow) so callers like statusCommand see real
filesystem errors.

---

Duplicate comments:
In `@openspec/changes/graceful-status-no-changes/design.md`:
- Line 37: The sentence describing the risk/mitigation is self-contradictory:
clarify whether the extra filesystem read happens when no changes exist or only
when changes do exist by updating the sentence in design.md; reference the
behavior of getAvailableChanges, validateChangeExists and statusCommand to state
precisely that statusCommand returns early when no --change is provided and no
changes exist (avoiding validateChangeExists and the second read), so the extra
read only occurs when changes exist (because validateChangeExists will call
getAvailableChanges again); rewrite the line to remove the contradiction and
make the condition for the extra read explicit.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eca0298 and 738d6a1.

📒 Files selected for processing (9)
  • .changeset/graceful-status-no-changes.md
  • openspec/changes/graceful-status-no-changes/.openspec.yaml
  • openspec/changes/graceful-status-no-changes/design.md
  • openspec/changes/graceful-status-no-changes/proposal.md
  • openspec/changes/graceful-status-no-changes/specs/graceful-status-empty/spec.md
  • openspec/changes/graceful-status-no-changes/tasks.md
  • src/commands/workflow/shared.ts
  • src/commands/workflow/status.ts
  • test/commands/artifact-workflow.test.ts
✅ Files skipped from review due to trivial changes (1)
  • openspec/changes/graceful-status-no-changes/.openspec.yaml
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/commands/workflow/status.ts
  • openspec/changes/graceful-status-no-changes/tasks.md
  • test/commands/artifact-workflow.test.ts
  • .changeset/graceful-status-no-changes.md

Comment on lines +100 to +102
} catch {
return [];
}
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 25, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Silent catch {} masks real filesystem errors (e.g., EACCES).

The catch block absorbs every error, including permission-denied and corrupted-path cases. When the openspec/changes directory exists but is unreadable, getAvailableChanges silently returns [], causing statusCommand to print No active changes. instead of surfacing the real error — a misleading signal.

Consider narrowing the catch to only ENOENT (directory not yet created), or at minimum re-throw unexpected errors:

🛡️ Proposed fix
-  } catch {
-    return [];
-  }
+  } catch (err: unknown) {
+    const code = (err as NodeJS.ErrnoException).code;
+    if (code === 'ENOENT') return [];
+    throw err;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/workflow/shared.ts` around lines 100 - 102, The catch in
getAvailableChanges currently swallows all errors and returns []; change it to
only suppress ENOENT (directory not present) and re-throw any other errors so
permission or IO errors surface: inside getAvailableChanges catch the error, if
(err.code === 'ENOENT') return []; otherwise throw err (or rethrow) so callers
like statusCommand see real filesystem errors.

Copy link
Author

Choose a reason for hiding this comment

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

This catch {} block is pre-existing behavior — it was the private closure inside validateChangeExists before this PR. We only extracted it as a public function without changing its internals. Narrowing the catch to ENOENT is a valid improvement but outside the scope of this fix. Happy to open a separate issue/PR for it if the maintainers agree.

Copy link
Contributor

Choose a reason for hiding this comment

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

@fsilvaortiz, thanks for the clarification! You're absolutely right — keeping behavioral changes separate from refactoring/extraction work is good practice for scope management and makes PRs easier to review.

Since you've confirmed the improvement is valid, would you like me to create a follow-up issue to track narrowing the catch block to only suppress ENOENT errors? I can draft one that references this discussion and includes the suggested fix for visibility.

Address CodeRabbit review feedback:
- Fix contradictory risk description in design.md (double-read happens
  when changes exist, not when they don't)
- Clarify in proposal.md that validateChangeExists was internally
  refactored to delegate to getAvailableChanges

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

Feedback: Onboards fails because openspec status returns : Error: No changes found. Create one with: openspec new change <name>

1 participant