Compare commits

...
Author SHA1 Message Date
Clay GoodandClaude Opus 5.5 239ddaf21f test: give the deprecation notice tests their own anchor in update.test.ts
Each of the four #2022 PRs added its tests before the same describe, so
merging them in sequence conflicted. No test changes.

Part of #2022

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:43:25 -05:00
Clay GoodandClaude Opus 5.5 29a3df1bcb fix(update): only advise switching where it changes something
The notice fired for any tool that gets command files, and told its users
to switch to skills-only delivery. For a tool that keeps its commands
under every delivery, because nothing it runs loads skills, that advice
would point at a switch that cannot help and suggests they lose their
only working surface.

The notice now counts only tools that switching would change: ones that
get command files now and would not under `skills`. On today's tools
every adapter-backed tool qualifies, so behavior is unchanged; it becomes
the right condition as soon as a tool keeps commands regardless.

Part of #2022

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:22:26 -05:00
Clay GoodandClaude Opus 5.5 2563f0f48b docs(update): describe when the deprecation notice shows accurately
The notice shows whenever delivery gives a configured tool command files,
including up-to-date runs that write nothing. The docs callout, changeset
and comments said it showed on runs that write command files.

Part of #2022

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:10:27 -05:00
Clay GoodandClaude Opus 5.5 019acb0011 feat(update): announce that generated command files are deprecated
Generated command files are moving to deprecation in favor of skills, and
users need to hear it from the tool before the next major changes the
default. `openspec update` now ends with a four-line notice whenever it
still writes command files for the project: what is changing, how to
switch now, how to keep commands, and where the plan lives.

The notice is shown on the up-to-date path too, which returns early and
is what most routine runs hit. It stays silent once delivery is `skills`,
and for projects whose tools get no command files at all. It is worded
"by default" so it is true both for users on the implicit default and for
users who set `delivery` explicitly, whose setting the next major keeps.

The delivery guide and the `delivery` reference entry carry the same
message, and a docs-claims test pins them to the CLI notice so the two
cannot drift.

Part of #2022

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:57:30 -05:00
Clay GoodandClaude Opus 5.5 9111a7654d fix(security): accept the unpatched braces advisory in pnpm audit (#2048)
* fix(security): clear the unpatched braces advisory on main

pnpm audit --prod fails on main for GHSA-vfj7-8cjw-p6xm (braces <=3.0.3,
stack exhaustion on deeply nested patterns). braces ships at runtime via
fast-glob > micromatch, and no patched version exists, so no override can
fix it.

Reject artifact output patterns that nest braces more than 16 levels deep
before they reach fast-glob, and record the advisory in auditConfig with
that mitigation and a removal check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(security): keep only the audit exception for the braces advisory

Move the brace-nesting guard to a follow-up PR: it adds a user-visible
limit to schema `generates` that needs a docs-lab contract update and a
spec change. The audit exception alone clears main's Security workflow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(security): record the braces advisory's residual risk accurately

Name both inputs that reach fast-glob (generates and apply.tracks) and
state that a crafted schema can still crash the CLI, instead of relying on
the input cap or a failed local reproduction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(security): drop unsupported claims from the braces risk record

Schemas resolve from the project, user, or package directories, not a
store, and the input-length cap does not prevent stack exhaustion. State
only the accepted risk and the removal check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:21:34 +00:00
9 changed files with 202 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@fission-ai/openspec": patch
---
Generated command files are now marked deprecated. While your delivery setting gives a configured tool command files, `openspec update` ends with a short notice: the next major version installs skills only by default, how to switch now (`openspec config set delivery skills && openspec update`), how to keep commands (`openspec config set delivery both`), and a link to the plan. The notice also appears when every tool is already up to date, and it never appears once delivery is `skills` or for tools that get no command files, such as Codex. Nothing about what OpenSpec installs changes in this release.
+3
View File
@@ -44,6 +44,9 @@ The picker asks what to configure ([delivery](#delivery-skills-commands-or-both)
Delivery is a profile setting that lets you choose to have only skills or only commands installed. The default is `both`. [Set up your project](../start/setup.md#the-workflow-files-skills-and-commands) explains the two forms and why both exist. The field's exact contract is in [CLI settings (config.json)](../reference/configuration/config-json.md#delivery).
> [!IMPORTANT]
> Command files are deprecated. The next major version of OpenSpec installs skills only by default, and `openspec update` says so on every run while your delivery setting gives a configured tool command files, including runs where everything is already up to date. To switch now, set `delivery` to `skills` and run `openspec update`: it removes the command files OpenSpec generated and lists any you edited instead of deleting them. To keep commands through the next major, set `delivery` to `both` explicitly. The plan is tracked in [#2022](https://github.com/Fission-AI/OpenSpec/issues/2022).
Two ways to change it:
**Interactively**: run `openspec config profile` and choose "Delivery only". Here's switching to skills only:
@@ -24,7 +24,7 @@ Which workflow set `openspec init` installs. Defaults to `core`: propose, explor
### delivery
Whether init installs workflows as skills, as slash commands, or both. Defaults to `both`.
Whether init installs workflows as skills, as slash commands, or both. Defaults to `both`. Commands are deprecated: the next major version changes the default to `skills`, and an explicit `both` or `commands` keeps its value ([#2022](https://github.com/Fission-AI/OpenSpec/issues/2022)).
### workflows
+12
View File
@@ -26,3 +26,15 @@ overrides:
# Dev-only (transitive via @vitest/ui); never in the published CLI. Remove once
# transitive fflate is >=0.8.3 (check: pnpm why fflate).
fflate@<0.8.3: '>=0.8.3 <0.9'
auditConfig:
ignoreGhsas:
# GHSA-vfj7-8cjw-p6xm — braces <=3.0.3 stack exhaustion on deeply nested
# patterns. Runtime (fast-glob > micromatch > braces), and no patched braces
# exists, so no override can fix it. Accepted residual risk: schema artifact
# `generates` and `apply.tracks` patterns reach fast-glob through
# resolveArtifactOutputs without a brace-depth guard, and the 10,000-character
# input limit does not prevent stack exhaustion. A crafted project-local or
# user schema can still terminate the openspec process. Remove this exception
# once braces ships a patched release (check: pnpm why braces; pnpm audit --prod).
- GHSA-vfj7-8cjw-p6xm
+35
View File
@@ -41,3 +41,38 @@ export function shouldGenerateCommandsForTool(toolId: string, delivery: Delivery
export function shouldReconcileCommandFilesForTool(toolId: string, delivery: Delivery): boolean {
return delivery === 'skills' && resolveCommandSurfaceCapability(toolId) === 'adapter-backed';
}
export const COMMAND_DEPRECATION_URL = 'https://github.com/Fission-AI/OpenSpec/issues/2022';
/**
* The notice `openspec update` prints while delivery gives a tool command files,
* which are deprecated in favor of skills. It is shown only for a tool that
* switching to skills-only delivery would change: one that gets command files
* now and would not under `skills`. A skills-only project is never told about a
* change that does not affect it, and a tool that keeps its commands under
* every delivery is never advised to switch away from its only working surface.
*
* Worded "by default" so it holds both for users on the implicit default, whom
* the next major moves to skills, and for users who set `delivery` explicitly,
* whose setting the next major keeps.
*/
export function formatCommandDeprecationNotice(
toolIds: readonly string[],
delivery: Delivery
): string[] {
const switchingChangesSomething = toolIds.some(
(toolId) =>
shouldGenerateCommandsForTool(toolId, delivery) &&
!shouldGenerateCommandsForTool(toolId, 'skills')
);
if (!switchingChangesSomething) {
return [];
}
return [
'Command files are deprecated: the next major version of OpenSpec installs skills only by default.',
' Switch to skills now: openspec config set delivery skills && openspec update',
' Keep commands for now: openspec config set delivery both',
` Details: ${COMMAND_DEPRECATION_URL}`,
];
}
+20
View File
@@ -69,6 +69,7 @@ import {
resolveCommandSurfaceCapability,
resolveCommandInvocation,
shouldGenerateCommandsForTool,
formatCommandDeprecationNotice,
shouldGenerateSkillsForTool,
shouldReconcileCommandFilesForTool,
shouldRemoveSkillsForTool,
@@ -280,6 +281,7 @@ export class UpdateCommand {
this.detectNewTools(resolvedProjectPath, configuredTools);
this.displayProfileNotes(resolvedProjectPath, configuredTools, desiredWorkflows, profile, delivery);
this.displaySetupNotes(configuredTools);
this.displayCommandDeprecation(configuredTools, delivery);
throwIfUpdateFailed(legacyUpgradeFailures);
return;
}
@@ -525,6 +527,7 @@ export class UpdateCommand {
// 14. Display the profile notes
this.displayProfileNotes(resolvedProjectPath, configuredAndNewTools, desiredWorkflows, profile, delivery);
this.displaySetupNotes(configuredAndNewTools);
this.displayCommandDeprecation(configuredAndNewTools, delivery);
// 15. List affected tools
if (updatedTools.length > 0) {
@@ -696,6 +699,23 @@ export class UpdateCommand {
}
}
/**
* Announces that generated command files are deprecated, on every run while
* delivery gives a configured tool command files, whether or not this run
* rewrote them. `update` is run on upgrade, so this reaches users
* before the next major changes the default, without nagging day to day.
*/
private displayCommandDeprecation(toolIds: string[], delivery: Delivery): void {
const [headline, ...detail] = formatCommandDeprecationNotice(toolIds, delivery);
if (!headline) return;
console.log();
console.log(chalk.yellow(headline));
for (const line of detail) {
console.log(chalk.dim(line));
}
}
/**
* Detects new tool directories that aren't currently configured and displays a hint.
*/
@@ -0,0 +1,34 @@
import { describe, it, expect } from 'vitest';
import {
COMMAND_DEPRECATION_URL,
formatCommandDeprecationNotice,
} from '../../src/core/command-surface.js';
describe('formatCommandDeprecationNotice', () => {
it.each(['both', 'commands'] as const)(
'announces the change when delivery %s still generates command files',
(delivery) => {
expect(formatCommandDeprecationNotice(['claude'], delivery)).toEqual([
'Command files are deprecated: the next major version of OpenSpec installs skills only by default.',
' Switch to skills now: openspec config set delivery skills && openspec update',
' Keep commands for now: openspec config set delivery both',
` Details: ${COMMAND_DEPRECATION_URL}`,
]);
}
);
it('says nothing to a project already on skills-only delivery', () => {
expect(formatCommandDeprecationNotice(['claude', 'cursor'], 'skills')).toEqual([]);
});
it('says nothing when no configured tool gets command files', () => {
// Codex is skills-invocable and has no command adapter, so a Codex-only
// project is unaffected by the deprecation even on the default delivery.
expect(formatCommandDeprecationNotice(['codex'], 'both')).toEqual([]);
expect(formatCommandDeprecationNotice([], 'both')).toEqual([]);
});
it('announces it once for a mixed project where one tool gets commands', () => {
expect(formatCommandDeprecationNotice(['codex', 'claude'], 'both')).toHaveLength(4);
});
});
+46
View File
@@ -1905,6 +1905,52 @@ metadata:
});
});
describe('command deprecation notice', () => {
const headline = 'Command files are deprecated';
async function runUpdate(command: UpdateCommand = new UpdateCommand()): Promise<string[]> {
const consoleSpy = vi.spyOn(console, 'log');
await command.execute(testDir);
const logs = consoleSpy.mock.calls.flat().map(String);
consoleSpy.mockRestore();
return logs;
}
it('announces the deprecation when update regenerates command files', async () => {
await new InitCommand({ tools: 'claude', force: true }).execute(testDir);
const logs = await runUpdate(new UpdateCommand({ force: true }));
expect(logs.some((entry) => entry.includes(headline))).toBe(true);
expect(
logs.some((entry) => entry.includes('openspec config set delivery skills && openspec update'))
).toBe(true);
expect(logs.some((entry) => entry.includes('issues/2022'))).toBe(true);
});
it('still announces it when every tool is already up to date', async () => {
// The up-to-date path returns early, and is what most users hit on a
// routine run. Missing it there would hide the notice from them.
await new InitCommand({ tools: 'claude', force: true }).execute(testDir);
const logs = await runUpdate();
expect(logs.some((entry) => entry.includes('up to date'))).toBe(true);
expect(logs.some((entry) => entry.includes(headline))).toBe(true);
});
it('stays quiet once delivery is skills-only', async () => {
setMockConfig({ featureFlags: {}, profile: 'core', delivery: 'skills' });
await new InitCommand({ tools: 'claude', force: true }).execute(testDir);
const forced = await runUpdate(new UpdateCommand({ force: true }));
const routine = await runUpdate();
expect(forced.some((entry) => entry.includes(headline))).toBe(false);
expect(routine.some((entry) => entry.includes(headline))).toBe(false);
});
});
describe('error handling', () => {
it('should report a failed legacy-only Codex bootstrap to automation', async () => {
setMockConfig({ featureFlags: {}, profile: 'core', delivery: 'commands' });
@@ -0,0 +1,46 @@
import { describe, expect, it } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
COMMAND_DEPRECATION_URL,
formatCommandDeprecationNotice,
} from '../src/core/command-surface.js';
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const read = (...segments: string[]) =>
fs.readFileSync(path.join(REPO_ROOT, 'docs-lab', ...segments), 'utf-8');
const PROFILES = read('customize', 'profiles.md');
const DELIVERY_SECTION = PROFILES.split('## Delivery: skills, commands, or both')[1].split(
'## Switching profiles'
)[0];
const CONFIG_JSON = read('reference', 'configuration', 'config-json.md');
const DELIVERY_FIELD = CONFIG_JSON.split('### delivery')[1].split('### workflows')[0];
// The CLI notice and the docs announce the same deprecation. Pin them to each
// other so neither can change its advice without the other (#2022).
describe('command deprecation documentation', () => {
const notice = formatCommandDeprecationNotice(['claude'], 'both').join('\n');
it('says what the CLI notice says in the delivery guide', () => {
expect(notice).toContain('the next major version of OpenSpec installs skills only by default');
expect(DELIVERY_SECTION).toContain(
'The next major version of OpenSpec installs skills only by default'
);
});
it('gives the same two choices as the notice', () => {
expect(notice).toContain('openspec config set delivery skills && openspec update');
expect(DELIVERY_SECTION).toContain('set `delivery` to `skills` and run `openspec update`');
expect(notice).toContain('openspec config set delivery both');
expect(DELIVERY_SECTION).toContain('set `delivery` to `both` explicitly');
});
it('links the same tracking issue from the notice and both pages', () => {
const issue = COMMAND_DEPRECATION_URL;
expect(notice).toContain(issue);
expect(DELIVERY_SECTION).toContain(issue);
expect(DELIVERY_FIELD).toContain(issue);
});
});