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
26 changes: 13 additions & 13 deletions src/core/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
} from './legacy-cleanup.js';
import { isInteractive } from '../utils/interactive.js';
import { getGlobalConfig, type Delivery, type Profile } from './global-config.js';
import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js';
import { getProfileWorkflows, ALL_WORKFLOWS, CORE_WORKFLOWS } from './profiles.js';
import { getAvailableTools } from './available-tools.js';
import {
WORKFLOW_TO_SKILL_DIR,
Expand All @@ -50,7 +50,6 @@ import {

const require = createRequire(import.meta.url);
const { version: OPENSPEC_VERSION } = require('../../package.json');
const OLD_CORE_WORKFLOWS = ['propose', 'explore', 'apply', 'archive'] as const;

/**
* Options for the update command.
Expand Down Expand Up @@ -156,7 +155,7 @@ export class UpdateCommand {
// Still check for new tool directories and extra workflows
this.detectNewTools(resolvedProjectPath, configuredTools);
this.displayExtraWorkflowsNote(resolvedProjectPath, configuredTools, desiredWorkflows);
this.displayOldCoreCustomProfileNote(profile, globalConfig.workflows);
this.displayMissingCoreWorkflowsNote(profile, globalConfig.workflows);
return;
}

Expand Down Expand Up @@ -284,7 +283,7 @@ export class UpdateCommand {

// 14. Display note about extra workflows not in profile
this.displayExtraWorkflowsNote(resolvedProjectPath, configuredAndNewTools, desiredWorkflows);
this.displayOldCoreCustomProfileNote(profile, globalConfig.workflows);
this.displayMissingCoreWorkflowsNote(profile, globalConfig.workflows);

// 15. List affected tools
if (updatedTools.length > 0) {
Expand Down Expand Up @@ -373,25 +372,26 @@ export class UpdateCommand {
}

/**
* Suggest opting back into core when a custom profile still matches the old
* pre-sync core set. Keep custom profiles user-owned; do not mutate them.
* Point out core workflows a custom profile is missing, so releases that
* grow CORE_WORKFLOWS stay discoverable. Keep custom profiles user-owned;
* do not mutate them.
*/
private displayOldCoreCustomProfileNote(profile: Profile, workflows?: readonly string[]): void {
private displayMissingCoreWorkflowsNote(profile: Profile, workflows?: readonly string[]): void {
if (profile !== 'custom' || !workflows) {
return;
}

const workflowSet = new Set(workflows);
const matchesOldCore =
workflowSet.size === OLD_CORE_WORKFLOWS.length &&
OLD_CORE_WORKFLOWS.every((workflow) => workflowSet.has(workflow));
const missing = CORE_WORKFLOWS.filter((workflow) => !workflowSet.has(workflow));

if (!matchesOldCore) {
if (missing.length === 0) {
return;
}

console.log(chalk.dim('Note: The core profile now includes sync. Your custom profile is preserving the old core workflow set.'));
console.log(chalk.dim('Run `openspec config profile core` and then `openspec update` to add sync.'));
const label = missing.length === 1 ? 'workflow' : 'workflows';
const pronoun = missing.length === 1 ? 'it' : 'them';
console.log(chalk.dim(`Note: Your custom profile is missing ${missing.length} core ${label}: ${missing.join(', ')}`));
console.log(chalk.dim(`Run \`openspec config profile\` to add ${pronoun}, or \`openspec config profile core\` to use the core set.`));
}

/**
Expand Down
59 changes: 56 additions & 3 deletions test/core/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1428,7 +1428,7 @@ More user content after markers.
)).toBe(false);
});

it('should suggest core preset when custom profile preserves the old core workflow set', async () => {
it('should list missing core workflows when custom profile preserves the old core workflow set', async () => {
setMockConfig({
featureFlags: {},
profile: 'custom',
Expand All @@ -1447,10 +1447,10 @@ More user content after markers.
call.map(arg => String(arg)).join(' ')
);
expect(calls.some(call =>
call.includes('The core profile now includes sync')
call.includes('Your custom profile is missing 2 core workflows: update, sync')
)).toBe(true);
expect(calls.some(call =>
call.includes('openspec config profile core') && call.includes('openspec update')
call.includes('openspec config profile core')
)).toBe(true);

expect(await FileSystemUtils.fileExists(
Expand All @@ -1463,6 +1463,59 @@ More user content after markers.
consoleSpy.mockRestore();
});

it('should list a single missing core workflow when custom profile lacks only update', async () => {
setMockConfig({
featureFlags: {},
profile: 'custom',
delivery: 'both',
workflows: ['propose', 'explore', 'apply', 'sync', 'archive'],
});

const initCommand = new InitCommand({ tools: 'claude', force: true });
await initCommand.execute(testDir);

const consoleSpy = vi.spyOn(console, 'log');

await updateCommand.execute(testDir);

const calls = consoleSpy.mock.calls.map(call =>
call.map(arg => String(arg)).join(' ')
);
expect(calls.some(call =>
call.includes('Your custom profile is missing 1 core workflow: update')
)).toBe(true);
expect(calls.some(call =>
call.includes('to add it, or')
)).toBe(true);

consoleSpy.mockRestore();
});

it('should not display a missing-core note when custom profile covers core workflows', async () => {
setMockConfig({
featureFlags: {},
profile: 'custom',
delivery: 'both',
workflows: ['propose', 'explore', 'apply', 'update', 'sync', 'archive', 'verify'],
});

const initCommand = new InitCommand({ tools: 'claude', force: true });
await initCommand.execute(testDir);

const consoleSpy = vi.spyOn(console, 'log');

await updateCommand.execute(testDir);

const calls = consoleSpy.mock.calls.map(call =>
call.map(arg => String(arg)).join(' ')
);
expect(calls.some(call =>
call.includes('Your custom profile is missing')
)).toBe(false);

consoleSpy.mockRestore();
});

it('should respect skills-only delivery setting', async () => {
setMockConfig({
featureFlags: {},
Expand Down
Loading