Skip to content

fix(change): resolve changes by directory instead of requiring proposal.md - #1433

Merged
clay-good merged 4 commits into
mainfrom
fix/show-resolves-proposalless-changes
Jul 23, 2026
Merged

fix(change): resolve changes by directory instead of requiring proposal.md#1433
clay-good merged 4 commits into
mainfrom
fix/show-resolves-proposalless-changes

Conversation

@clay-good

@clay-good clay-good commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Status

LGTM. No behavior change for a change that already has a proposal.md; the full suite (2234 tests) passes on Linux, macOS, and Windows.

What was wrong

Change lookup used two different rules. openspec list, status, instructions, and validate resolve a change by its directory. openspec show, openspec change list, and shell completion instead required openspec/changes/<name>/proposal.md.

Those rules disagree the moment you create a change, because openspec new change writes only .openspec.yaml:

$ openspec new change my-feature
Created change 'my-feature' at openspec/changes/my-feature/
Next: openspec status --change my-feature

$ openspec list
Changes:
  my-feature     No tasks      just now

$ openspec show my-feature
Unknown item 'my-feature'.

So openspec list hands you a name that openspec show denies. Tab-completion had the same blind spot. openspec change list — whose deprecation warning says "Use openspec list" — reported a different set than openspec list. And for a custom schema that defines no proposal artifact, the change was never resolvable at all: the first defect listed in #1161.

How it was fixed

getActiveChangeIds / getArchivedChangeIds resolve a change by its directory, the rule getAvailableChanges already applies. Archive and hidden directories are still excluded. openspec change list moves onto the same helper, so the deprecated alias now matches the command it points at.

Reporting is honest about what is actually on disk:

$ openspec show my-feature
Error: Change "my-feature" has no proposal.md yet.
Run "openspec status --change my-feature" to see which artifact comes next.

$ openspec change list --long
my-feature: (no proposal.md yet) [tasks 1/2]

This finishes a direction the repo already took: #1182 moved validate off the proposal.md gate onto directory resolution, and #1375 relaxed --change lookup to accept any name that exists on disk. No schema, workflow, or artifact-graph behavior changes.

Replication / proof

Reproduce on main:

mkdir -p repro/openspec/specs && cd repro
printf 'schema: spec-driven\n' > openspec/config.yaml
openspec new change my-feature
openspec list              # lists my-feature
openspec show my-feature   # Unknown item 'my-feature'.
openspec change list       # empty, despite `openspec list` listing it

After this branch both commands agree and show explains what is missing.

Tests:

  • test/utils/item-discovery.test.ts (new) — scaffolded change, proposal-less schema, archive/hidden exclusion, missing directory.
  • test/commands/show.test.ts — end-to-end: show resolves a scaffolded change; change show with no name offers it.
  • test/core/commands/change-command.list.test.tschange list lists it, keeps [tasks 1/2], and labels it (no proposal.md yet) rather than (unable to read).
  • test/core/commands/change-command.show-validate.test.ts — a stray file, a traversing name, and a nested name are not reported as changes; the traversal case writes the reachable target file so it fails without the containment guard, and builds the input with path.join('..', '..') for Windows separators.

eslint src/ clean. No template changes, so the golden parity hashes are untouched.

Review findings addressed

Three adversarial passes over the first two commits (blast radius, edge cases, consumer contracts) found four defects, all fixed in e48e4fd:

  1. change list kept a private proposal-gated scan, disagreeing with openspec list and with the show selector directly above it. Unified; the orphaned helper and its ARCHIVE_DIR constant are gone.
  2. Task counts were computed inside the proposal try block, so a change with tasks but no proposal reported 0/0.
  3. --long/--json labelled a not-yet-written proposal (unable to read)/Unknown. Missing vs. unreadable is now decided by testing existence rather than sniffing error codes.
  4. show reported a stray file under changes/ as a change awaiting its proposal — pointing at a status --change call that cannot work. It now requires a directory.
  5. fs.access failing with EACCES was read as "no proposal.md yet", hiding a real read failure. Only ENOENT now counts as absent.
  6. Pre-existing traversal, now closed. openspec change show ../.. resolved openspec/changes/../../proposal.md and printed a file from outside the changes directory. Verified reproducing identically on origin/main, so it predates this PR; since this PR already added containment for the error message, show now rejects any name that is not a direct child of changes/ before touching the filesystem. Both new tests fail with the guard removed.

Verified unchanged: getActiveChangeIds/getArchivedChangeIds are not part of the package's public export surface (src/index.ts exports cli and core only); ChangeCommand.validate needs only the change directory, so widening its selector cannot crash; every store root derives changesDir as <root>/openspec/changes (makeRoot), so getActiveChangeIds(root.path) stays correct under --store; and an unreadable changes/ directory still degrades to an empty list.

Notes / nits

  • Deliberate behavior change: if a directory under changes/ shares a name with a spec, openspec show <name> now reports the existing "Ambiguous item — pass --type change|spec" error even before the change has a proposal. That is the correct answer once the change is real to list/status, and it was already the behavior the moment a proposal existed.
  • Scope: this addresses the first defect in Custom schemas without proposal are only partially supported #1161 (change discovery). That issue also asks about workflow guidance and the overall compatibility contract for proposal-less schemas, so this PR references it rather than closing it.
  • Pre-existing nit, left alone: when a name matches both a change and a spec, show's not-found path can suggest it twice (Did you mean: auth, auth?). It is in the suggestion logic, unrelated to discovery.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • openspec show <change> now resolves changes by directory name (including scaffolded/proposal-less changes without proposal.md).
    • Improved proposal-less behavior across CLI output: shows (no proposal.md yet) and guidance to openspec status --change <name>, avoids “Unknown item”, and keeps task counts accurate.
    • Archived-change discovery and shell completion now include proposal-less entries.
  • Tests
    • Added coverage for scaffolded proposal-less changes, proper handling of stray/hidden entries, traversal safety, and archived listing.

`openspec show <change>` and shell completion resolved a change only when
`openspec/changes/<name>/proposal.md` existed. Every sibling command --
`list`, `status`, `instructions`, `validate` -- resolves a change by its
directory (`getAvailableChanges`). The two rules disagree the moment a
change is created: `openspec new change <name>` scaffolds only
`.openspec.yaml`, so `list` showed the change while `show` reported
`Unknown item`. A custom schema that defines no proposal artifact was
never resolvable at all (#1161).

Resolve by directory in `getActiveChangeIds`/`getArchivedChangeIds`, and
report a change that exists without a proposal accurately -- pointing at
`openspec status --change <name>` -- rather than as missing.

The deprecated `openspec change list` keeps its own proposal-backed scan;
its JSON output parses proposal.md per change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Change discovery now resolves active and archived changes by directory rather than requiring proposal.md. openspec show and openspec change list report targeted states for proposal-less changes, preserve task progress, and point to openspec status --change.

Changes

Proposal-less change resolution

Layer / File(s) Summary
Directory-based change discovery
src/utils/item-discovery.ts, test/utils/item-discovery.test.ts
Active and archived change IDs are discovered from non-hidden directories without requiring proposal.md; tests cover scaffolded, archived, hidden, stray, and missing-directory cases.
Change command resolution and output
src/commands/change.ts, test/commands/show.test.ts, test/core/commands/change-command.list.test.ts, test/core/commands/change-command.show-validate.test.ts
show and list resolve directory-based changes, distinguish missing proposals from missing changes, preserve task progress, and validate invalid identifiers.
Behavior documentation
.changeset/show-resolves-proposalless-changes.md
Documents proposal-less resolution and the corresponding CLI messaging.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers: tabishb, alfred-openspec

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: resolving changes by directory instead of requiring proposal.md.
✨ 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/show-resolves-proposalless-changes

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.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/commands/change.ts`:
- Around line 62-80: Update the no-name discovery path in the show command to
enumerate change directories directly, so scaffolded changes without proposal.md
appear in interactive selections and available-ID errors. Keep the deprecated
list command’s proposal-gated getActiveChanges() behavior unchanged, and add a
regression test covering no-name show discovery of a proposal-less change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6d05d12f-08e7-48cf-94d4-8af53234e0ff

📥 Commits

Reviewing files that changed from the base of the PR and between a874d1d and 932da82.

📒 Files selected for processing (5)
  • .changeset/show-resolves-proposalless-changes.md
  • src/commands/change.ts
  • src/utils/item-discovery.ts
  • test/commands/show.test.ts
  • test/utils/item-discovery.test.ts

Comment thread src/commands/change.ts
`ChangeCommand.show` resolved a named proposal-less change but still built
its no-name selector (and the non-interactive "Available IDs" hint) from
the proposal-gated scan, so a scaffolded change could not be picked.

Use directory-based discovery there as well. `list` keeps the local
proposal-backed scan: its --json output parses proposal.md per change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@clay-good

Copy link
Copy Markdown
Collaborator Author

Addressed the review finding on src/commands/change.ts in 67aca4a.

The no-name selector in ChangeCommand.show (and the non-interactive Available IDs hint) now uses getActiveChangeIds(this.rootPath ?? process.cwd()), which resolves to the same root as getChangesPath(). Added a regression test for openspec change show with no name.

list deliberately keeps the local proposal-backed scan: its --json output parses proposal.md for every change it reports, so widening it there would change that output rather than fix it.

@coderabbitai coderabbitai Bot 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.

Caution

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

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

68-85: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve non-missing filesystem errors.

This catch treats permission errors and malformed paths as “no proposal.md yet.” It also considers any existing path a change directory, so a regular file named like the change receives the wrong guidance. Only handle ENOENT as a missing artifact, and verify changeDir is a directory before emitting the proposal-less message; rethrow other filesystem errors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/change.ts` around lines 68 - 85, The proposalPath error handling
must only interpret ENOENT as a missing proposal. Update the catch around
fs.access in the change command to rethrow other filesystem errors, then verify
changeDir is an actual directory (not merely an existing path) before reporting
a proposal-less change; otherwise preserve the not-found error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/commands/change.ts`:
- Around line 68-85: The proposalPath error handling must only interpret ENOENT
as a missing proposal. Update the catch around fs.access in the change command
to rethrow other filesystem errors, then verify changeDir is an actual directory
(not merely an existing path) before reporting a proposal-less change; otherwise
preserve the not-found error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 753740e7-32c2-411e-bfec-42820744b034

📥 Commits

Reviewing files that changed from the base of the PR and between 932da82 and 67aca4a.

📒 Files selected for processing (2)
  • src/commands/change.ts
  • test/commands/show.test.ts

…honestly

Adversarial review of the first two commits surfaced four defects.

`change list` still used a private proposal-gated scan, so the deprecated
alias reported a different set than `openspec list` -- the command its own
deprecation warning tells you to use -- while the `show` selector beside it
offered the wider set. Move it to `getActiveChangeIds` and drop the now
unused helper and its ARCHIVE_DIR constant.

Widening that list exposed three follow-on bugs, all fixed here:

- Task counts were computed inside the proposal try block, so a change with
  tasks but no proposal.md reported 0/0. Task progress is independent of the
  proposal; resolve it first.
- `--long` printed "(unable to read)" and `--json` "Unknown" for a change
  that is simply not written yet. Distinguish a missing proposal from an
  unreadable one by testing existence, not by sniffing error codes.
- `show` reported a stray file under changes/, or a traversing name such as
  `../..`, as a change awaiting its proposal, pointing the user at a
  `status --change` call that cannot work. Require a directory that is a
  direct child of changes/.

Also drops a duplicated proposal.md read in the --long path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@clay-good clay-good changed the title fix(show): resolve changes by directory instead of requiring proposal.md fix(change): resolve changes by directory instead of requiring proposal.md Jul 23, 2026

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

Caution

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

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

67-86: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject non-child change paths before proposal access. The current validation occurs only in the failed-access path, so traversal can read an existing external proposal.md.

  • src/commands/change.ts#L67-L86: validate the resolved direct child with lstat().isDirectory() before accessing proposal.md; reject traversal and symlink aliases.
  • test/core/commands/change-command.show-validate.test.ts#L109-L112: seed the traversed target with tempRoot/proposal.md, use path.join('..', '..'), and add an alias-path regression case.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/change.ts` around lines 67 - 86, In src/commands/change.ts lines
67-86, update the change validation before proposalPath access to resolve the
candidate, verify it is a direct child of changesPath, and use
lstat().isDirectory() so traversal and symlink aliases are rejected before
fs.access(proposalPath); preserve the existing missing-proposal handling for
valid change directories. In
test/core/commands/change-command.show-validate.test.ts lines 109-112, seed
tempRoot/proposal.md, use path.join('..', '..') for the traversal case, and add
regression coverage for an alias path.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/commands/change.ts`:
- Around line 13-17: Update pathExists to return false only when fs.access fails
with an ENOENT error; rethrow or propagate EACCES and other I/O failures so the
existing read-error handling reports an unknown/unreadable proposal instead of
treating it as missing.

In `@test/core/commands/change-command.show-validate.test.ts`:
- Around line 109-112: Update the test around cmd.show in “does not treat a
traversing name as a change” to create tempRoot/proposal.md before asserting
rejection, and construct the traversal input with path.join('..', '..') rather
than a hard-coded separator. Add a separate symlink-alias regression case that
targets the reachable proposal path and verifies traversal aliases are not
accepted, covering Windows path identity behavior.

---

Outside diff comments:
In `@src/commands/change.ts`:
- Around line 67-86: In src/commands/change.ts lines 67-86, update the change
validation before proposalPath access to resolve the candidate, verify it is a
direct child of changesPath, and use lstat().isDirectory() so traversal and
symlink aliases are rejected before fs.access(proposalPath); preserve the
existing missing-proposal handling for valid change directories. In
test/core/commands/change-command.show-validate.test.ts lines 109-112, seed
tempRoot/proposal.md, use path.join('..', '..') for the traversal case, and add
regression coverage for an alias path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cb3f0d44-619b-4ddb-994c-85504be18e62

📥 Commits

Reviewing files that changed from the base of the PR and between 67aca4a and e48e4fd.

📒 Files selected for processing (4)
  • .changeset/show-resolves-proposalless-changes.md
  • src/commands/change.ts
  • test/core/commands/change-command.list.test.ts
  • test/core/commands/change-command.show-validate.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/show-resolves-proposalless-changes.md

Comment thread src/commands/change.ts Outdated
Comment on lines +109 to +112
it('does not treat a traversing name as a change', async () => {
await expect(cmd.show('../..', { json: false })).rejects.toThrow(/not found at/);
await expect(cmd.show('../..', { json: false })).rejects.not.toThrow(/has no proposal\.md yet/);
});

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exercise the reachable traversal target.

Create tempRoot/proposal.md before this assertion; the current code then incorrectly accepts ../... Build the traversal input with path.join('..', '..'), and add a symlink-alias case to cover path identity on Windows. As per coding guidelines, “When touching path behavior, add coverage that would fail on Windows path separators” and “Add an alias-path regression test when touching path identity logic.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/commands/change-command.show-validate.test.ts` around lines 109 -
112, Update the test around cmd.show in “does not treat a traversing name as a
change” to create tempRoot/proposal.md before asserting rejection, and construct
the traversal input with path.join('..', '..') rather than a hard-coded
separator. Add a separate symlink-alias regression case that targets the
reachable proposal path and verifies traversal aliases are not accepted,
covering Windows path identity behavior.

Source: Coding guidelines

@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.

Exact head e48e4fd cleanly unifies change discovery by directory across show, deprecated noun-form list/show/validate, and active/archived completion while preserving archive/hidden exclusions, direct-child containment, task counts, and structured JSON errors. Build, 63 focused discovery/show/list/completion tests, a real proposal-less CLI reproduction, and the full hosted Linux/macOS/Windows/security matrix pass; approved technically, with a real second-human approval still required before merge.

…oposals

Second review round.

`isDefinitelyMissing` replaces the plain existence check: fs.access can fail
with EACCES or an I/O error, and treating that as "no proposal.md yet" hid a
real read failure behind an ordinary-looking state. Only ENOENT counts as
absent; anything else falls through to the existing unreadable handling.

`show` now rejects a name that is not a direct child of changes/ before
touching the filesystem. This closes a pre-existing traversal on main:
`openspec change show ../..` resolved openspec/changes/../../proposal.md and
printed a file from outside the changes directory. Both new tests fail
without the guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@clay-good

Copy link
Copy Markdown
Collaborator Author

Both findings from the second review round are addressed in 66cd50c, and the traversal one turned out to be a genuine (pre-existing) bug.

1. "Do not classify inaccessible proposals as missing" — valid, fixed.
fs.access failing with EACCES (for example an unsearchable parent directory) was being read as "no proposal.md yet". Replaced with isDefinitelyMissing, which treats only ENOENT as absent; every other failure falls through to the existing unreadable handling. Note the common case was already correct — chmod 000 on the file itself still reports (unable to read), because fs.access with F_OK tests existence, not readability.

2. "Exercise the reachable traversal target" — valid, and it exposed a real traversal.
You were right that the old test was vacuous. Creating tempRoot/proposal.md shows that openspec change show ../.. resolves openspec/changes/../../proposal.md and prints a file from outside the changes directory.

I verified this against origin/main before changing anything: it reproduces there identically, so it is pre-existing rather than something this PR introduced. Since this PR already added a containment check for the error message, I hoisted it — show now rejects any name that is not a direct child of changes/ before touching the filesystem. No legitimate change name traverses, and --change lookups elsewhere already reject separators (#1375).

Tests now use path.join('..', '..') for Windows separators, plus a nested-name case. I confirmed both fail with the guard removed and pass with it, so they are real regression tests rather than incidental passes.

I skipped the suggested symlink-alias case: symlink creation is unreliable on Windows CI without elevation, and containment here is decided by path.resolve on the joined name, which never dereferences links.

Full suite: 2234 tests passing; eslint src/ clean.

@coderabbitai coderabbitai Bot 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.

♻️ Duplicate comments (1)
src/commands/change.ts (1)

88-100: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve unreadable-proposal errors.

Line 90 catches EACCES and I/O failures, then Lines 97-105 report “has no proposal.md yet” whenever the directory is readable. Only ENOENT should trigger that message; propagate other access errors after retaining the stray-file directory check.

Suggested fix
-    } catch {
+    } catch (error) {
       // A change can exist without a proposal: `openspec new change` scaffolds
       // only .openspec.yaml, and a custom schema need not define a proposal
       // artifact. Say which of the two cases this is instead of reporting a
@@
       if (isChangeDirectory) {
+        if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
+          throw error;
+        }
         throw new Error(
           `Change "${changeName}" has no proposal.md yet. ` +
             `Run "openspec status --change ${changeName}" to see which artifact comes next.`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/change.ts` around lines 88 - 100, Update the proposalPath access
handling in the change command to treat only an ENOENT error as a missing
proposal; preserve the existing isChangeDirectory stray-file check for that
case. For EACCES and other filesystem failures, rethrow the original error
instead of reporting that proposal.md is absent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/commands/change.ts`:
- Around line 88-100: Update the proposalPath access handling in the change
command to treat only an ENOENT error as a missing proposal; preserve the
existing isChangeDirectory stray-file check for that case. For EACCES and other
filesystem failures, rethrow the original error instead of reporting that
proposal.md is absent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 470ce446-d750-4077-821d-e99c211cb33d

📥 Commits

Reviewing files that changed from the base of the PR and between e48e4fd and 66cd50c.

📒 Files selected for processing (2)
  • src/commands/change.ts
  • test/core/commands/change-command.show-validate.test.ts

@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.

Follow-up exact head 66cd50c closes the remaining safety gaps: change lookup now rejects traversal and nested names before any proposal read, and only labels a proposal missing on ENOENT, preserving unreadable-file reporting. Build, 64 focused discovery/show/list/completion tests including a real outside-proposal traversal regression, a structured JSON CLI reproduction, and the full hosted Linux/macOS/Windows/security matrix pass; approved technically, with a real second-human approval still required before merge.

@clay-good
clay-good added this pull request to the merge queue Jul 23, 2026
Merged via the queue into main with commit 26f009d Jul 23, 2026
16 checks passed
@clay-good
clay-good deleted the fix/show-resolves-proposalless-changes branch July 23, 2026 17:37
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.

2 participants