@fission-ai/openspec 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,6 +84,10 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
84
84
  | **Claude Code** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` |
85
85
  | **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
86
86
  | **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
87
+ | **Kilo Code** | `/openspec-proposal.md`, `/openspec-apply.md`, `/openspec-archive.md` (`.kilocode/workflows/`) |
88
+ | **Windsurf** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.windsurf/workflows/`) |
89
+
90
+ Kilo Code discovers team workflows automatically. Save the generated files under `.kilocode/workflows/` and trigger them from the command palette with `/openspec-proposal.md`, `/openspec-apply.md`, or `/openspec-archive.md`.
87
91
 
88
92
  #### AGENTS.md Compatible
89
93
  These tools automatically read workflow instructions from `openspec/AGENTS.md`. Ask them to follow the OpenSpec workflow if they need a reminder. Learn more about the [AGENTS.md convention](https://agents.md/).
@@ -121,8 +125,8 @@ openspec init
121
125
  ```
122
126
 
123
127
  **What happens during initialization:**
124
- - You'll be prompted to select your AI tool (Claude Code, Cursor, etc.)
125
- - OpenSpec automatically configures slash commands or `AGENTS.md` based on your selection
128
+ - You'll be prompted to pick any natively supported AI tools (Claude Code, Cursor, OpenCode, etc.); other assistants always rely on the shared `AGENTS.md` stub
129
+ - OpenSpec automatically configures slash commands for the tools you choose and always writes a managed `AGENTS.md` hand-off at the project root
126
130
  - A new `openspec/` directory structure is created in your project
127
131
 
128
132
  **After setup:**
@@ -4,9 +4,11 @@ export const OPENSPEC_MARKERS = {
4
4
  end: '<!-- OPENSPEC:END -->'
5
5
  };
6
6
  export const AI_TOOLS = [
7
- { name: 'Claude Code (✅ OpenSpec custom slash commands available)', value: 'claude', available: true, successLabel: 'Claude Code' },
8
- { name: 'Cursor (✅ OpenSpec custom slash commands available)', value: 'cursor', available: true, successLabel: 'Cursor' },
9
- { name: 'OpenCode (✅ OpenSpec custom slash commands available)', value: 'opencode', available: true, successLabel: 'OpenCode' },
10
- { name: 'AGENTS.md (works with Codex, Amp, Copilot, …)', value: 'agents', available: true, successLabel: 'your AGENTS.md-compatible assistant' }
7
+ { name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code' },
8
+ { name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
9
+ { name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' },
10
+ { name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
11
+ { name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf' },
12
+ { name: 'AGENTS.md (works with Codex, Amp, VS Code, GitHub Copilot, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
11
13
  ];
12
14
  //# sourceMappingURL=config.js.map
@@ -0,0 +1,9 @@
1
+ import { SlashCommandConfigurator } from "./base.js";
2
+ import { SlashCommandId } from "../../templates/index.js";
3
+ export declare class KiloCodeSlashCommandConfigurator extends SlashCommandConfigurator {
4
+ readonly toolId = "kilocode";
5
+ readonly isAvailable = true;
6
+ protected getRelativePath(id: SlashCommandId): string;
7
+ protected getFrontmatter(_id: SlashCommandId): string | undefined;
8
+ }
9
+ //# sourceMappingURL=kilocode.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { SlashCommandConfigurator } from "./base.js";
2
+ const FILE_PATHS = {
3
+ proposal: ".kilocode/workflows/openspec-proposal.md",
4
+ apply: ".kilocode/workflows/openspec-apply.md",
5
+ archive: ".kilocode/workflows/openspec-archive.md"
6
+ };
7
+ export class KiloCodeSlashCommandConfigurator extends SlashCommandConfigurator {
8
+ toolId = "kilocode";
9
+ isAvailable = true;
10
+ getRelativePath(id) {
11
+ return FILE_PATHS[id];
12
+ }
13
+ getFrontmatter(_id) {
14
+ return undefined;
15
+ }
16
+ }
17
+ //# sourceMappingURL=kilocode.js.map
@@ -1,14 +1,20 @@
1
1
  import { ClaudeSlashCommandConfigurator } from './claude.js';
2
2
  import { CursorSlashCommandConfigurator } from './cursor.js';
3
+ import { WindsurfSlashCommandConfigurator } from './windsurf.js';
4
+ import { KiloCodeSlashCommandConfigurator } from './kilocode.js';
3
5
  import { OpenCodeSlashCommandConfigurator } from './opencode.js';
4
6
  export class SlashCommandRegistry {
5
7
  static configurators = new Map();
6
8
  static {
7
9
  const claude = new ClaudeSlashCommandConfigurator();
8
10
  const cursor = new CursorSlashCommandConfigurator();
11
+ const windsurf = new WindsurfSlashCommandConfigurator();
12
+ const kilocode = new KiloCodeSlashCommandConfigurator();
9
13
  const opencode = new OpenCodeSlashCommandConfigurator();
10
14
  this.configurators.set(claude.toolId, claude);
11
15
  this.configurators.set(cursor.toolId, cursor);
16
+ this.configurators.set(windsurf.toolId, windsurf);
17
+ this.configurators.set(kilocode.toolId, kilocode);
12
18
  this.configurators.set(opencode.toolId, opencode);
13
19
  }
14
20
  static register(configurator) {
@@ -0,0 +1,9 @@
1
+ import { SlashCommandConfigurator } from './base.js';
2
+ import { SlashCommandId } from '../../templates/index.js';
3
+ export declare class WindsurfSlashCommandConfigurator extends SlashCommandConfigurator {
4
+ readonly toolId = "windsurf";
5
+ readonly isAvailable = true;
6
+ protected getRelativePath(id: SlashCommandId): string;
7
+ protected getFrontmatter(id: SlashCommandId): string | undefined;
8
+ }
9
+ //# sourceMappingURL=windsurf.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { SlashCommandConfigurator } from './base.js';
2
+ const FILE_PATHS = {
3
+ proposal: '.windsurf/workflows/openspec-proposal.md',
4
+ apply: '.windsurf/workflows/openspec-apply.md',
5
+ archive: '.windsurf/workflows/openspec-archive.md'
6
+ };
7
+ export class WindsurfSlashCommandConfigurator extends SlashCommandConfigurator {
8
+ toolId = 'windsurf';
9
+ isAvailable = true;
10
+ getRelativePath(id) {
11
+ return FILE_PATHS[id];
12
+ }
13
+ getFrontmatter(id) {
14
+ const descriptions = {
15
+ proposal: 'Scaffold a new OpenSpec change and validate strictly.',
16
+ apply: 'Implement an approved OpenSpec change and keep tasks in sync.',
17
+ archive: 'Archive a deployed OpenSpec change and update specs.'
18
+ };
19
+ const description = descriptions[id];
20
+ return `---\ndescription: ${description}\nauto_execution_mode: 3\n---`;
21
+ }
22
+ }
23
+ //# sourceMappingURL=windsurf.js.map
@@ -3,9 +3,16 @@ type ToolLabel = {
3
3
  annotation?: string;
4
4
  };
5
5
  type ToolWizardChoice = {
6
+ kind: 'heading' | 'info';
7
+ value: string;
8
+ label: ToolLabel;
9
+ selectable: false;
10
+ } | {
11
+ kind: 'option';
6
12
  value: string;
7
13
  label: ToolLabel;
8
14
  configured: boolean;
15
+ selectable: true;
9
16
  };
10
17
  type ToolWizardConfig = {
11
18
  extendMode: boolean;
@@ -29,6 +36,7 @@ export declare class InitCommand {
29
36
  private createDirectoryStructure;
30
37
  private generateFiles;
31
38
  private configureAITools;
39
+ private configureRootAgentsStub;
32
40
  private displaySuccessMessage;
33
41
  private formatToolNames;
34
42
  private renderBanner;
package/dist/core/init.js CHANGED
@@ -33,16 +33,27 @@ const parseToolLabel = (raw) => {
33
33
  annotation: match[2].trim(),
34
34
  };
35
35
  };
36
+ const isSelectableChoice = (choice) => choice.selectable;
37
+ const ROOT_STUB_CHOICE_VALUE = '__root_stub__';
38
+ const OTHER_TOOLS_HEADING_VALUE = '__heading-other__';
39
+ const LIST_SPACER_VALUE = '__list-spacer__';
36
40
  const toolSelectionWizard = createPrompt((config, done) => {
37
41
  const totalSteps = 3;
38
42
  const [step, setStep] = useState('intro');
39
- const [cursor, setCursor] = useState(0);
40
- const [selected, setSelected] = useState(() => config.initialSelected ?? []);
43
+ const selectableChoices = config.choices.filter(isSelectableChoice);
44
+ const initialCursorIndex = config.choices.findIndex((choice) => choice.selectable);
45
+ const [cursor, setCursor] = useState(initialCursorIndex === -1 ? 0 : initialCursorIndex);
46
+ const [selected, setSelected] = useState(() => {
47
+ const initial = new Set((config.initialSelected ?? []).filter((value) => selectableChoices.some((choice) => choice.value === value)));
48
+ return selectableChoices
49
+ .map((choice) => choice.value)
50
+ .filter((value) => initial.has(value));
51
+ });
41
52
  const [error, setError] = useState(null);
42
53
  const selectedSet = new Set(selected);
43
- const pageSize = Math.max(Math.min(config.choices.length, 7), 1);
54
+ const pageSize = Math.max(config.choices.length, 1);
44
55
  const updateSelected = (next) => {
45
- const ordered = config.choices
56
+ const ordered = selectableChoices
46
57
  .map((choice) => choice.value)
47
58
  .filter((value) => next.has(value));
48
59
  setSelected(ordered);
@@ -51,8 +62,13 @@ const toolSelectionWizard = createPrompt((config, done) => {
51
62
  items: config.choices,
52
63
  active: cursor,
53
64
  pageSize,
54
- loop: config.choices.length > 1,
65
+ loop: false,
55
66
  renderItem: ({ item, isActive }) => {
67
+ if (!item.selectable) {
68
+ const prefix = item.kind === 'info' ? ' ' : '';
69
+ const textColor = item.kind === 'heading' ? PALETTE.lightGray : PALETTE.midGray;
70
+ return `${PALETTE.midGray(' ')} ${PALETTE.midGray(' ')} ${textColor(`${prefix}${item.label.primary}`)}`;
71
+ }
56
72
  const isSelected = selectedSet.has(item.value);
57
73
  const cursorSymbol = isActive
58
74
  ? PALETTE.white('›')
@@ -61,10 +77,32 @@ const toolSelectionWizard = createPrompt((config, done) => {
61
77
  ? PALETTE.white('◉')
62
78
  : PALETTE.midGray('○');
63
79
  const nameColor = isActive ? PALETTE.white : PALETTE.midGray;
64
- const label = `${nameColor(item.label.primary)}${item.configured ? PALETTE.midGray(' (already configured)') : ''}`;
80
+ const annotation = item.label.annotation
81
+ ? PALETTE.midGray(` (${item.label.annotation})`)
82
+ : '';
83
+ const configuredNote = item.configured
84
+ ? PALETTE.midGray(' (already configured)')
85
+ : '';
86
+ const label = `${nameColor(item.label.primary)}${annotation}${configuredNote}`;
65
87
  return `${cursorSymbol} ${indicator} ${label}`;
66
88
  },
67
89
  });
90
+ const moveCursor = (direction) => {
91
+ if (selectableChoices.length === 0) {
92
+ return;
93
+ }
94
+ let nextIndex = cursor;
95
+ while (true) {
96
+ nextIndex = nextIndex + direction;
97
+ if (nextIndex < 0 || nextIndex >= config.choices.length) {
98
+ return;
99
+ }
100
+ if (config.choices[nextIndex]?.selectable) {
101
+ setCursor(nextIndex);
102
+ return;
103
+ }
104
+ }
105
+ };
68
106
  useKeypress((key) => {
69
107
  if (step === 'intro') {
70
108
  if (isEnterKey(key)) {
@@ -74,20 +112,18 @@ const toolSelectionWizard = createPrompt((config, done) => {
74
112
  }
75
113
  if (step === 'select') {
76
114
  if (isUpKey(key)) {
77
- const previousIndex = cursor <= 0 ? config.choices.length - 1 : cursor - 1;
78
- setCursor(previousIndex);
115
+ moveCursor(-1);
79
116
  setError(null);
80
117
  return;
81
118
  }
82
119
  if (isDownKey(key)) {
83
- const nextIndex = cursor >= config.choices.length - 1 ? 0 : cursor + 1;
84
- setCursor(nextIndex);
120
+ moveCursor(1);
85
121
  setError(null);
86
122
  return;
87
123
  }
88
124
  if (isSpaceKey(key)) {
89
125
  const current = config.choices[cursor];
90
- if (!current)
126
+ if (!current || !current.selectable)
91
127
  return;
92
128
  const next = new Set(selected);
93
129
  if (next.has(current.value)) {
@@ -101,16 +137,13 @@ const toolSelectionWizard = createPrompt((config, done) => {
101
137
  return;
102
138
  }
103
139
  if (isEnterKey(key)) {
104
- if (selected.length === 0) {
105
- setError('Select at least one AI tool to continue.');
106
- return;
107
- }
108
140
  setStep('review');
109
141
  setError(null);
110
142
  return;
111
143
  }
112
144
  if (key.name === 'escape') {
113
- setSelected([]);
145
+ const next = new Set();
146
+ updateSelected(next);
114
147
  setError(null);
115
148
  }
116
149
  return;
@@ -119,7 +152,7 @@ const toolSelectionWizard = createPrompt((config, done) => {
119
152
  if (isEnterKey(key)) {
120
153
  const finalSelection = config.choices
121
154
  .map((choice) => choice.value)
122
- .filter((value) => selectedSet.has(value));
155
+ .filter((value) => selectedSet.has(value) && value !== ROOT_STUB_CHOICE_VALUE);
123
156
  done(finalSelection);
124
157
  return;
125
158
  }
@@ -129,9 +162,21 @@ const toolSelectionWizard = createPrompt((config, done) => {
129
162
  }
130
163
  }
131
164
  });
132
- const selectedNames = config.choices
133
- .filter((choice) => selectedSet.has(choice.value))
134
- .map((choice) => choice.label.primary);
165
+ const rootStubChoice = selectableChoices.find((choice) => choice.value === ROOT_STUB_CHOICE_VALUE);
166
+ const rootStubSelected = rootStubChoice
167
+ ? selectedSet.has(ROOT_STUB_CHOICE_VALUE)
168
+ : false;
169
+ const nativeChoices = selectableChoices.filter((choice) => choice.value !== ROOT_STUB_CHOICE_VALUE);
170
+ const selectedNativeChoices = nativeChoices.filter((choice) => selectedSet.has(choice.value));
171
+ const formatSummaryLabel = (choice) => {
172
+ const annotation = choice.label.annotation
173
+ ? PALETTE.midGray(` (${choice.label.annotation})`)
174
+ : '';
175
+ const configuredNote = choice.configured
176
+ ? PALETTE.midGray(' (already configured)')
177
+ : '';
178
+ return `${PALETTE.white(choice.label.primary)}${annotation}${configuredNote}`;
179
+ };
135
180
  const stepIndex = step === 'intro' ? 1 : step === 'select' ? 2 : 3;
136
181
  const lines = [];
137
182
  lines.push(PALETTE.midGray(`Step ${stepIndex}/${totalSteps}`));
@@ -154,13 +199,16 @@ const toolSelectionWizard = createPrompt((config, done) => {
154
199
  lines.push('');
155
200
  lines.push(page);
156
201
  lines.push('');
157
- if (selectedNames.length === 0) {
158
- lines.push(`${PALETTE.midGray('Selected')}: ${PALETTE.midGray('None selected yet')}`);
202
+ lines.push(PALETTE.midGray('Selected configuration:'));
203
+ if (rootStubSelected && rootStubChoice) {
204
+ lines.push(` ${PALETTE.white('-')} ${formatSummaryLabel(rootStubChoice)}`);
205
+ }
206
+ if (selectedNativeChoices.length === 0) {
207
+ lines.push(` ${PALETTE.midGray('- No natively supported providers selected')}`);
159
208
  }
160
209
  else {
161
- lines.push(PALETTE.midGray('Selected:'));
162
- selectedNames.forEach((name) => {
163
- lines.push(` ${PALETTE.white('-')} ${PALETTE.white(name)}`);
210
+ selectedNativeChoices.forEach((choice) => {
211
+ lines.push(` ${PALETTE.white('-')} ${formatSummaryLabel(choice)}`);
164
212
  });
165
213
  }
166
214
  }
@@ -168,12 +216,15 @@ const toolSelectionWizard = createPrompt((config, done) => {
168
216
  lines.push(PALETTE.white('Review selections'));
169
217
  lines.push(PALETTE.midGray('Press Enter to confirm or Backspace to adjust.'));
170
218
  lines.push('');
171
- if (selectedNames.length === 0) {
172
- lines.push(PALETTE.midGray('No tools selected. Press Backspace to return.'));
219
+ if (rootStubSelected && rootStubChoice) {
220
+ lines.push(`${PALETTE.white('')} ${formatSummaryLabel(rootStubChoice)}`);
221
+ }
222
+ if (selectedNativeChoices.length === 0) {
223
+ lines.push(PALETTE.midGray('No natively supported providers selected. Universal instructions will still be applied.'));
173
224
  }
174
225
  else {
175
- selectedNames.forEach((name) => {
176
- lines.push(`${PALETTE.white('▌')} ${PALETTE.white(name)}`);
226
+ selectedNativeChoices.forEach((choice) => {
227
+ lines.push(`${PALETTE.white('▌')} ${formatSummaryLabel(choice)}`);
177
228
  });
178
229
  }
179
230
  }
@@ -197,13 +248,6 @@ export class InitCommand {
197
248
  this.renderBanner(extendMode);
198
249
  // Get configuration (after validation to avoid prompts if validation fails)
199
250
  const config = await this.getConfiguration(existingToolStates, extendMode);
200
- if (config.aiTools.length === 0) {
201
- if (extendMode) {
202
- throw new Error(`OpenSpec seems to already be initialized at ${openspecPath}.\n` +
203
- `Use 'openspec update' to update the structure.`);
204
- }
205
- throw new Error('You must select at least one AI tool to configure.');
206
- }
207
251
  const availableTools = AI_TOOLS.filter((tool) => tool.available);
208
252
  const selectedIds = new Set(config.aiTools);
209
253
  const selectedTools = availableTools.filter((tool) => selectedIds.has(tool.value));
@@ -226,13 +270,13 @@ export class InitCommand {
226
270
  }
227
271
  // Step 2: Configure AI tools
228
272
  const toolSpinner = this.startSpinner('Configuring AI tools...');
229
- await this.configureAITools(projectPath, openspecDir, config.aiTools);
273
+ const rootStubStatus = await this.configureAITools(projectPath, openspecDir, config.aiTools);
230
274
  toolSpinner.stopAndPersist({
231
275
  symbol: PALETTE.white('▌'),
232
276
  text: PALETTE.white('AI tools configured'),
233
277
  });
234
278
  // Success message
235
- this.displaySuccessMessage(selectedTools, created, refreshed, skippedExisting, skipped, extendMode);
279
+ this.displaySuccessMessage(selectedTools, created, refreshed, skippedExisting, skipped, extendMode, rootStubStatus);
236
280
  }
237
281
  async validate(projectPath, _openspecPath) {
238
282
  const extendMode = await FileSystemUtils.directoryExists(_openspecPath);
@@ -248,25 +292,64 @@ export class InitCommand {
248
292
  }
249
293
  async promptForAITools(existingTools, extendMode) {
250
294
  const availableTools = AI_TOOLS.filter((tool) => tool.available);
251
- if (availableTools.length === 0) {
252
- return [];
253
- }
254
295
  const baseMessage = extendMode
255
- ? 'Which AI tools would you like to add or refresh?'
256
- : 'Which AI tools do you use?';
257
- const initialSelected = extendMode
296
+ ? 'Which natively supported AI tools would you like to add or refresh?'
297
+ : 'Which natively supported AI tools do you use?';
298
+ const initialNativeSelection = extendMode
258
299
  ? availableTools
259
300
  .filter((tool) => existingTools[tool.value])
260
301
  .map((tool) => tool.value)
261
302
  : [];
262
- return this.prompt({
263
- extendMode,
264
- baseMessage,
265
- choices: availableTools.map((tool) => ({
303
+ const initialSelected = Array.from(new Set(initialNativeSelection));
304
+ const choices = [
305
+ {
306
+ kind: 'heading',
307
+ value: '__heading-native__',
308
+ label: {
309
+ primary: 'Natively supported providers (✔ OpenSpec custom slash commands available)',
310
+ },
311
+ selectable: false,
312
+ },
313
+ ...availableTools.map((tool) => ({
314
+ kind: 'option',
266
315
  value: tool.value,
267
316
  label: parseToolLabel(tool.name),
268
317
  configured: Boolean(existingTools[tool.value]),
318
+ selectable: true,
269
319
  })),
320
+ ...(availableTools.length
321
+ ? [
322
+ {
323
+ kind: 'info',
324
+ value: LIST_SPACER_VALUE,
325
+ label: { primary: '' },
326
+ selectable: false,
327
+ },
328
+ ]
329
+ : []),
330
+ {
331
+ kind: 'heading',
332
+ value: OTHER_TOOLS_HEADING_VALUE,
333
+ label: {
334
+ primary: 'Other tools (use Universal AGENTS.md for Codex, Amp, VS Code, GitHub Copilot, …)',
335
+ },
336
+ selectable: false,
337
+ },
338
+ {
339
+ kind: 'option',
340
+ value: ROOT_STUB_CHOICE_VALUE,
341
+ label: {
342
+ primary: 'Universal AGENTS.md',
343
+ annotation: 'always available',
344
+ },
345
+ configured: extendMode,
346
+ selectable: true,
347
+ },
348
+ ];
349
+ return this.prompt({
350
+ extendMode,
351
+ baseMessage,
352
+ choices,
270
353
  initialSelected,
271
354
  });
272
355
  }
@@ -316,6 +399,7 @@ export class InitCommand {
316
399
  }
317
400
  }
318
401
  async configureAITools(projectPath, openspecDir, toolIds) {
402
+ const rootStubStatus = await this.configureRootAgentsStub(projectPath, openspecDir);
319
403
  for (const toolId of toolIds) {
320
404
  const configurator = ToolRegistry.get(toolId);
321
405
  if (configurator && configurator.isAvailable) {
@@ -326,8 +410,19 @@ export class InitCommand {
326
410
  await slashConfigurator.generateAll(projectPath, openspecDir);
327
411
  }
328
412
  }
413
+ return rootStubStatus;
329
414
  }
330
- displaySuccessMessage(selectedTools, created, refreshed, skippedExisting, skipped, extendMode) {
415
+ async configureRootAgentsStub(projectPath, openspecDir) {
416
+ const configurator = ToolRegistry.get('agents');
417
+ if (!configurator || !configurator.isAvailable) {
418
+ return 'skipped';
419
+ }
420
+ const stubPath = path.join(projectPath, configurator.configFileName);
421
+ const existed = await FileSystemUtils.fileExists(stubPath);
422
+ await configurator.configure(projectPath, openspecDir);
423
+ return existed ? 'updated' : 'created';
424
+ }
425
+ displaySuccessMessage(selectedTools, created, refreshed, skippedExisting, skipped, extendMode, rootStubStatus) {
331
426
  console.log(); // Empty line for spacing
332
427
  const successHeadline = extendMode
333
428
  ? 'OpenSpec tool configuration updated!'
@@ -336,6 +431,12 @@ export class InitCommand {
336
431
  console.log();
337
432
  console.log(PALETTE.lightGray('Tool summary:'));
338
433
  const summaryLines = [
434
+ rootStubStatus === 'created'
435
+ ? `${PALETTE.white('▌')} ${PALETTE.white('Root AGENTS.md stub created for other assistants')}`
436
+ : null,
437
+ rootStubStatus === 'updated'
438
+ ? `${PALETTE.lightGray('▌')} ${PALETTE.lightGray('Root AGENTS.md stub refreshed for other assistants')}`
439
+ : null,
339
440
  created.length
340
441
  ? `${PALETTE.white('▌')} ${PALETTE.white('Created:')} ${this.formatToolNames(created)}`
341
442
  : null,
@@ -375,7 +476,7 @@ export class InitCommand {
375
476
  .map((tool) => tool.successLabel ?? tool.name)
376
477
  .filter((name) => Boolean(name));
377
478
  if (names.length === 0)
378
- return PALETTE.lightGray('your AI assistant');
479
+ return PALETTE.lightGray('your AGENTS.md-compatible assistant');
379
480
  if (names.length === 1)
380
481
  return PALETTE.white(names[0]);
381
482
  const base = names.slice(0, -1).map((name) => PALETTE.white(name));
@@ -1,14 +1,14 @@
1
1
  const baseGuardrails = `**Guardrails**
2
2
  - Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
3
3
  - Keep changes tightly scoped to the requested outcome.
4
- - Refer to \`openspec/AGENTS.md\` if you need additional OpenSpec conventions or clarifications.`;
4
+ - Refer to \`openspec/AGENTS.md\` (located inside the \`openspec/\` directory—run \`ls openspec\` or \`openspec update\` if you don't see it) if you need additional OpenSpec conventions or clarifications.`;
5
5
  const proposalGuardrails = `${baseGuardrails}\n- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.`;
6
6
  const proposalSteps = `**Steps**
7
7
  1. Review \`openspec/project.md\`, run \`openspec list\` and \`openspec list --specs\`, and inspect related code or docs (e.g., via \`rg\`/\`ls\`) to ground the proposal in current behaviour; note any gaps that require clarification.
8
8
  2. Choose a unique verb-led \`change-id\` and scaffold \`proposal.md\`, \`tasks.md\`, and \`design.md\` (when needed) under \`openspec/changes/<id>/\`.
9
9
  3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing.
10
10
  4. Capture architectural reasoning in \`design.md\` when the solution spans multiple systems, introduces new patterns, or demands trade-off discussion before committing to specs.
11
- 5. Draft spec deltas in \`changes/<id>/specs/\` using \`## ADDED|MODIFIED|REMOVED Requirements\` with at least one \`#### Scenario:\` per requirement and cross-reference related capabilities when relevant.
11
+ 5. Draft spec deltas in \`changes/<id>/specs/<capability>/spec.md\` (one folder per capability) using \`## ADDED|MODIFIED|REMOVED Requirements\` with at least one \`#### Scenario:\` per requirement and cross-reference related capabilities when relevant.
12
12
  6. Draft \`tasks.md\` as an ordered list of small, verifiable work items that deliver user-visible progress, include validation (tests, tooling), and highlight dependencies or parallelizable work.
13
13
  7. Validate with \`openspec validate <id> --strict\` and resolve every issue before sharing the proposal.`;
14
14
  const proposalReferences = `**Reference**
@@ -26,7 +26,7 @@ export declare const VALIDATION_MESSAGES: {
26
26
  readonly REQUIREMENT_TOO_LONG: "Requirement text is very long (>500 characters). Consider breaking it down.";
27
27
  readonly DELTA_DESCRIPTION_TOO_BRIEF: "Delta description is too brief";
28
28
  readonly DELTA_MISSING_REQUIREMENTS: "Delta should include requirements";
29
- readonly GUIDE_NO_DELTAS: "No deltas found. Ensure your change has a specs/ directory with .md files using delta headers (## ADDED/MODIFIED/REMOVED/RENAMED Requirements) and that each requirement includes at least one \"#### Scenario:\" block. Tip: run \"openspec change show <change-id> --json --deltas-only\" to inspect parsed deltas.";
29
+ readonly GUIDE_NO_DELTAS: "No deltas found. Ensure your change has a specs/ directory with capability folders (e.g. specs/http-server/spec.md) containing .md files that use delta headers (## ADDED/MODIFIED/REMOVED/RENAMED Requirements) and that each requirement includes at least one \"#### Scenario:\" block. Tip: run \"openspec change show <change-id> --json --deltas-only\" to inspect parsed deltas.";
30
30
  readonly GUIDE_MISSING_SPEC_SECTIONS: "Missing required sections. Expected headers: \"## Purpose\" and \"## Requirements\". Example:\n## Purpose\n[brief purpose]\n\n## Requirements\n### Requirement: Clear requirement statement\nUsers SHALL ...\n\n#### Scenario: Descriptive name\n- **WHEN** ...\n- **THEN** ...";
31
31
  readonly GUIDE_MISSING_CHANGE_SECTIONS: "Missing required sections. Expected headers: \"## Why\" and \"## What Changes\". Ensure deltas are documented in specs/ using delta headers.";
32
32
  readonly GUIDE_SCENARIO_FORMAT: "Scenarios must use level-4 headers. Convert bullet lists into:\n#### Scenario: Short name\n- **WHEN** ...\n- **THEN** ...\n- **AND** ...";
@@ -32,7 +32,7 @@ export const VALIDATION_MESSAGES = {
32
32
  DELTA_DESCRIPTION_TOO_BRIEF: 'Delta description is too brief',
33
33
  DELTA_MISSING_REQUIREMENTS: 'Delta should include requirements',
34
34
  // Guidance snippets (appended to primary messages for remediation)
35
- GUIDE_NO_DELTAS: 'No deltas found. Ensure your change has a specs/ directory with .md files using delta headers (## ADDED/MODIFIED/REMOVED/RENAMED Requirements) and that each requirement includes at least one "#### Scenario:" block. Tip: run "openspec change show <change-id> --json --deltas-only" to inspect parsed deltas.',
35
+ GUIDE_NO_DELTAS: 'No deltas found. Ensure your change has a specs/ directory with capability folders (e.g. specs/http-server/spec.md) containing .md files that use delta headers (## ADDED/MODIFIED/REMOVED/RENAMED Requirements) and that each requirement includes at least one "#### Scenario:" block. Tip: run "openspec change show <change-id> --json --deltas-only" to inspect parsed deltas.',
36
36
  GUIDE_MISSING_SPEC_SECTIONS: 'Missing required sections. Expected headers: "## Purpose" and "## Requirements". Example:\n## Purpose\n[brief purpose]\n\n## Requirements\n### Requirement: Clear requirement statement\nUsers SHALL ...\n\n#### Scenario: Descriptive name\n- **WHEN** ...\n- **THEN** ...',
37
37
  GUIDE_MISSING_CHANGE_SECTIONS: 'Missing required sections. Expected headers: "## Why" and "## What Changes". Ensure deltas are documented in specs/ using delta headers.',
38
38
  GUIDE_SCENARIO_FORMAT: 'Scenarios must use level-4 headers. Convert bullet lists into:\n#### Scenario: Short name\n- **WHEN** ...\n- **THEN** ...\n- **AND** ...',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fission-ai/openspec",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "openspec",
@@ -62,6 +62,7 @@
62
62
  "test:watch": "vitest",
63
63
  "test:ui": "vitest --ui",
64
64
  "test:coverage": "vitest --coverage",
65
+ "release": "pnpm run build && pnpm exec changeset publish",
65
66
  "changeset": "changeset"
66
67
  }
67
68
  }