Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
## Context

`2026-08-26-add-adopt-changesets-template` covers the "propose Changesets
adoption from an OpenSpec change" half of the agreed integration scope.
This change covers the other half: once a project has actually adopted
Changesets, a nudge at the moment a change is archived (the point where
a forgotten changeset is most likely to be discovered too late — after
the code has already landed) that a pending changeset might be missing.

## Goals / Non-Goals

**Goals:**
- A project that has adopted Changesets and archives a change without a
pending changeset sees a single, dismissible reminder with a one-click
way to start `npx changeset`.
- A project that has not adopted Changesets (no `.changeset/config.json`)
sees nothing at all — this feature is invisible until a project opts
in by adopting Changesets in the first place.
- The check is read-only and best-effort: it never blocks, delays, or
can fail the archive operation that already succeeded.

**Non-Goals:**
- Not a visual panel or dedicated view for Changesets state (rejected —
see the 2026-08-26 discussion; the product stays release-tool-agnostic
per `docs/adr/0001-shared-core-two-delivery-targets.md`).
- Not correlating the archived change's specific file diff against which
packages actually need a version bump — that requires a git diff
against a base ref, which the archive command does not currently have
and which would meaningfully raise the false-negative/false-positive
surface for a first version of this feature. The reminder only asks
"is anything pending at all," not "does the right package have a
changeset."
- Not implemented for `webui`/standalone: the action (opening an
integrated terminal) has no equivalent in a browser tab or the VS Code
local-server iframe embed. A future change could add a copy-to-clipboard
or in-page instruction instead, if requested.
- Not auto-running `npx changeset` or auto-installing `@changesets/cli`
— the user explicitly asked whether to auto-install and the answer
here is no: the reminder only appears once a project has already
chosen to adopt Changesets (`.changeset/config.json` exists), so
`@changesets/cli` is already an expected dependency by that point.

## Decisions

### The core function reports facts only, never prompts

`checkChangesetReminder(cwd)` in `packages/core/src/changeset-reminder.ts`
returns `{ changesetsAdopted, pendingChangesetCount }` and does nothing
else — no `vscode` import, no UI. `packages/extension/src/commands.ts`
decides whether and how to surface it. This mirrors
`agent-detection.ts`'s existing shape in the same package (a pure
presence check the host acts on).

### Best-effort, silently swallowed failures

`remindAboutPendingChangeset` wraps its entire body in try/catch with no
error surfaced anywhere — a broken or unreadable `.changeset` directory
should never turn into a scary error message immediately after a
successful archive. `checkChangesetReminder` itself already treats a
missing/unreadable `.changeset` directory as "not adopted," so the
try/catch is a second line of defense against anything unexpected (e.g.
a VS Code API call failing).

## Risks / Trade-offs

- **[Risk]** A project with per-package changesets workflows unrelated
to the archived change (e.g. an already-pending changeset for a
different, older piece of work) suppresses the reminder even though
the archived change itself introduced an uncovered package bump.
→ **Mitigation**: accepted for a first version — see the Non-Goals
above on not correlating against the specific diff. The reminder is a
best-effort nudge, not a gate; `require-changeset` (Gitea/GitHub CI, if
adopted) remains the actual enforcement point.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
## Why

Second and final piece of the narrower Changesets-integration scope the
user confirmed on 2026-08-26 (template + conditional archive-time
reminder, not a full Changesets visual panel baked into the product
core — see `2026-08-26-add-adopt-changesets-template` for the template
half). The product should not assume every project uses Changesets, so
this is a best-effort, silent-by-default nudge: only a project that has
already adopted Changesets (`.changeset/config.json` present) and has
just archived a change without a pending changeset sees anything at
all.

## What Changes

- Add `checkChangesetReminder(cwd)` to `@openspec-ui/core`: reports
whether `.changeset/config.json` exists and, if so, how many pending
`.changeset/*.md` files exist (excluding `README.md`). Pure
filesystem read, no prompting — the host decides what to do with the
result, per `docs/adr/0001-shared-core-two-delivery-targets.md`.
- Wire it into the VS Code extension's `openspec-ui.archiveChange`
command: after a successful archive, if Changesets is adopted and
nothing is pending, show an information message with a "Run npx
changeset" action that opens an integrated terminal and runs it.
Never blocks or delays the archive result itself; any failure in the
check is swallowed silently.
- Scoped to the VS Code extension only, not `webui`/standalone: the
reminder's action (an integrated terminal) has no equivalent in a
browser-hosted or message-bridge iframe context.

## Capabilities

### Modified Capabilities

- `vscode-extension`: adds a new Requirement for the archive-time
Changesets reminder.

## Impact

- `packages/core/src/changeset-reminder.ts` (new)
- `packages/core/src/index.ts`
- `packages/extension/src/commands.ts`
- `openspec/specs/vscode-extension/spec.md`
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
## ADDED Requirements

### Requirement: Archiving a change offers a Changesets reminder when appropriate

For a workspace that has adopted Changesets (`.changeset/config.json`
exists), the extension SHALL check, after a successful archive, whether
any changeset is currently pending, and SHALL offer to start `npx
changeset` in an integrated terminal when none is. A workspace that has
not adopted Changesets SHALL see no such reminder. The check SHALL NOT
block, delay, or affect the outcome of the archive operation.

#### Scenario: Archiving with Changesets adopted and nothing pending

- **WHEN** a change is archived in a workspace with
`.changeset/config.json` and no pending `.changeset/*.md` file
- **THEN** the extension shows an information message offering to run
`npx changeset`
- **AND** choosing that action opens an integrated terminal and runs
`npx changeset`

#### Scenario: Archiving with a changeset already pending

- **WHEN** a change is archived in a workspace with
`.changeset/config.json` and at least one pending `.changeset/*.md`
file
- **THEN** no reminder is shown

#### Scenario: Archiving in a workspace that has not adopted Changesets

- **WHEN** a change is archived in a workspace with no
`.changeset/config.json`
- **THEN** no reminder is shown

#### Scenario: The reminder check fails

- **WHEN** the Changesets presence/pending check throws or the
filesystem is unreadable
- **THEN** the archive operation's own success result is unaffected
- **AND** no error is surfaced for the failed check
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
## 1. Core: report Changesets adoption/pending state

- [x] 1.1 Add `checkChangesetReminder(cwd)` to
`packages/core/src/changeset-reminder.ts`: `changesetsAdopted` (does
`.changeset/config.json` exist), `pendingChangesetCount` (count of
`.changeset/*.md` files, excluding `README.md`). Never throws.
- [x] 1.2 Export it from `packages/core/src/index.ts`.
- [x] 1.3 Add `packages/core/src/changeset-reminder.test.ts` covering:
no `.changeset` directory, `.changeset` present without
`config.json`, adopted with zero pending, adopted with pending
changesets counted correctly (README.md excluded).

## 2. Extension: archive-time reminder

- [x] 2.1 Add `remindAboutPendingChangeset(workspaceRoot)` to
`packages/extension/src/commands.ts`: calls `checkChangesetReminder`;
if adopted and nothing pending, shows an information message with a
"Run npx changeset" action that opens an integrated terminal
(`vscode.window.createTerminal`) and sends `npx changeset`. Wrapped in
try/catch with no surfaced error.
- [x] 2.2 Call it (fire-and-forget, not awaited) from
`openspec-ui.archiveChange` after the archive succeeds and trees
refresh, so it never delays or can fail the archive result itself.
- [x] 2.3 Add tests to `packages/extension/src/commands.test.ts`: the
reminder appears and opens a terminal with `npx changeset` when
adopted with nothing pending and the user picks the action; no
terminal opens when a changeset is already pending.

## 3. Spec

- [x] 3.1 Add the `ADDED Requirements` delta to
`openspec/specs/vscode-extension/spec.md` via
`specs/vscode-extension/spec.md` in this change.

## 4. Verification

- [x] 4.1 `npm run typecheck` passes workspace-wide.
- [x] 4.2 `npm run lint` (including `lint:english`) passes workspace-wide.
- [x] 4.3 `npm run test` passes workspace-wide, including the new test
files.
- [x] 4.4 Rebuild the VSIX (`npm run package --workspace
openspec-ui-vscode`) and confirm it packages without error.
- [x] 4.5 Propose a changeset (`npx changeset`) for `openspec-ui-vscode`
and `@openspec-ui/core` (both minor: new capability, no breaking
change) instead of hand-editing `version`/`CHANGELOG.md`; apply it via
`npx changeset version`.
- [x] 4.6 Run `openspec change validate --strict add-changeset-archive-reminder`.
38 changes: 38 additions & 0 deletions openspec/specs/vscode-extension/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,41 @@ report that instead of prompting for confirmation.
rollback-eligible processes
- **THEN** the system reports this without showing a confirmation dialog

### Requirement: Archiving a change offers a Changesets reminder when appropriate

For a workspace that has adopted Changesets (`.changeset/config.json`
exists), the extension SHALL check, after a successful archive, whether
any changeset is currently pending, and SHALL offer to start `npx
changeset` in an integrated terminal when none is. A workspace that has
not adopted Changesets SHALL see no such reminder. The check SHALL NOT
block, delay, or affect the outcome of the archive operation.

#### Scenario: Archiving with Changesets adopted and nothing pending

- **WHEN** a change is archived in a workspace with
`.changeset/config.json` and no pending `.changeset/*.md` file
- **THEN** the extension shows an information message offering to run
`npx changeset`
- **AND** choosing that action opens an integrated terminal and runs
`npx changeset`

#### Scenario: Archiving with a changeset already pending

- **WHEN** a change is archived in a workspace with
`.changeset/config.json` and at least one pending `.changeset/*.md`
file
- **THEN** no reminder is shown

#### Scenario: Archiving in a workspace that has not adopted Changesets

- **WHEN** a change is archived in a workspace with no
`.changeset/config.json`
- **THEN** no reminder is shown

#### Scenario: The reminder check fails

- **WHEN** the Changesets presence/pending check throws or the
filesystem is unreadable
- **THEN** the archive operation's own success result is unaffected
- **AND** no error is surfaced for the failed check

11 changes: 11 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# @openspec-ui/core

## 0.26.0

### Minor Changes

- Add an archive-time Changesets reminder to the VS Code extension. When a
workspace has adopted Changesets (`.changeset/config.json` exists) and no
changeset is currently pending, archiving a change now offers to run
`npx changeset` in an integrated terminal. Silent for workspaces that
have not adopted Changesets, and never affects the archive operation's
own result.

## 0.25.0

### Minor Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@openspec-ui/core",
"private": true,
"version": "0.25.0",
"version": "0.26.0",
"type": "module",
"main": "src/index.ts",
"exports": {
Expand Down
65 changes: 65 additions & 0 deletions packages/core/src/changeset-reminder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { checkChangesetReminder } from "./changeset-reminder.js";

const temporaryRoots: string[] = [];

async function temporaryRoot(): Promise<string> {
const root = await mkdtemp(path.join(os.tmpdir(), "openspec-changeset-reminder-"));
temporaryRoots.push(root);
return root;
}

afterEach(async () => {
await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
});

describe("checkChangesetReminder", () => {
it("reports not adopted when there is no .changeset directory at all", async () => {
const root = await temporaryRoot();

expect(await checkChangesetReminder(root)).toEqual({
changesetsAdopted: false,
pendingChangesetCount: 0,
});
});

it("reports not adopted when .changeset exists but has no config.json", async () => {
const root = await temporaryRoot();
await mkdir(path.join(root, ".changeset"));
await writeFile(path.join(root, ".changeset", "README.md"), "# Changesets\n");

expect(await checkChangesetReminder(root)).toEqual({
changesetsAdopted: false,
pendingChangesetCount: 0,
});
});

it("reports adopted with zero pending changesets when only config.json and README.md exist", async () => {
const root = await temporaryRoot();
await mkdir(path.join(root, ".changeset"));
await writeFile(path.join(root, ".changeset", "config.json"), "{}");
await writeFile(path.join(root, ".changeset", "README.md"), "# Changesets\n");

expect(await checkChangesetReminder(root)).toEqual({
changesetsAdopted: true,
pendingChangesetCount: 0,
});
});

it("counts pending changeset markdown files, excluding README.md", async () => {
const root = await temporaryRoot();
await mkdir(path.join(root, ".changeset"));
await writeFile(path.join(root, ".changeset", "config.json"), "{}");
await writeFile(path.join(root, ".changeset", "README.md"), "# Changesets\n");
await writeFile(path.join(root, ".changeset", "fuzzy-cats-jump.md"), "---\n---\nSome change.\n");
await writeFile(path.join(root, ".changeset", "brave-lions-run.md"), "---\n---\nAnother change.\n");

expect(await checkChangesetReminder(root)).toEqual({
changesetsAdopted: true,
pendingChangesetCount: 2,
});
});
});
41 changes: 41 additions & 0 deletions packages/core/src/changeset-reminder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Best-effort presence check for whether a workspace has adopted
// Changesets (`.changeset/config.json`) and, if so, whether any changeset
// file is currently pending. A host adapter uses this to decide whether to
// show an archive-time reminder — this module only reports facts, it never
// prompts or writes anything itself (see openspec/changes/
// add-changeset-archive-reminder/design.md).

import { access, readdir } from "node:fs/promises";
import path from "node:path";

export interface ChangesetReminderStatus {
changesetsAdopted: boolean;
pendingChangesetCount: number;
}

const README_NAMES = new Set(["readme.md"]);

/** Returns whether `cwd` has adopted Changesets (a `.changeset/config.json`
* file exists) and, if so, how many pending `.changeset/*.md` changeset
* files exist (excluding `.changeset/README.md`). Never throws — a missing
* or unreadable `.changeset` directory is reported as "not adopted". */
export async function checkChangesetReminder(cwd: string): Promise<ChangesetReminderStatus> {
const changesetDir = path.join(cwd, ".changeset");
try {
await access(path.join(changesetDir, "config.json"));
} catch {
return { changesetsAdopted: false, pendingChangesetCount: 0 };
}

let entries: string[] = [];
try {
entries = await readdir(changesetDir);
} catch {
entries = [];
}
const pendingChangesetCount = entries.filter(
(name) => name.toLowerCase().endsWith(".md") && !README_NAMES.has(name.toLowerCase()),
).length;

return { changesetsAdopted: true, pendingChangesetCount };
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export * from "./checkpoint.js";
export * from "./workbench-run-journal.js";
export * from "./workbench-recovery.js";
export * from "./agent-detection.js";
export * from "./changeset-reminder.js";

export { ClaudeCliAdapter } from "./agents/claude.js";
export { CopilotCliAdapter } from "./agents/copilot.js";
Expand Down
Loading
Loading