@fission-ai/openspec 0.6.0 → 0.7.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 +5 -2
- package/dist/core/config.js +5 -4
- package/dist/core/configurators/slash/kilocode.d.ts +9 -0
- package/dist/core/configurators/slash/kilocode.js +17 -0
- package/dist/core/configurators/slash/registry.js +3 -0
- package/dist/core/init.d.ts +8 -0
- package/dist/core/init.js +151 -50
- package/dist/core/templates/slash-command-templates.js +2 -2
- package/dist/core/validation/constants.d.ts +1 -1
- package/dist/core/validation/constants.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,6 +84,9 @@ 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
|
+
|
|
89
|
+
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
90
|
|
|
88
91
|
#### AGENTS.md Compatible
|
|
89
92
|
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 +124,8 @@ openspec init
|
|
|
121
124
|
```
|
|
122
125
|
|
|
123
126
|
**What happens during initialization:**
|
|
124
|
-
- You'll be prompted to
|
|
125
|
-
- OpenSpec automatically configures slash commands
|
|
127
|
+
- 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
|
|
128
|
+
- OpenSpec automatically configures slash commands for the tools you choose and always writes a managed `AGENTS.md` hand-off at the project root
|
|
126
129
|
- A new `openspec/` directory structure is created in your project
|
|
127
130
|
|
|
128
131
|
**After setup:**
|
package/dist/core/config.js
CHANGED
|
@@ -4,9 +4,10 @@ export const OPENSPEC_MARKERS = {
|
|
|
4
4
|
end: '<!-- OPENSPEC:END -->'
|
|
5
5
|
};
|
|
6
6
|
export const AI_TOOLS = [
|
|
7
|
-
{ name: 'Claude Code
|
|
8
|
-
{ name: 'Cursor
|
|
9
|
-
{ name: 'OpenCode
|
|
10
|
-
{ name: '
|
|
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: 'AGENTS.md (works with Codex, Amp, VS Code, GitHub Copilot, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
|
|
11
12
|
];
|
|
12
13
|
//# 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,17 @@
|
|
|
1
1
|
import { ClaudeSlashCommandConfigurator } from './claude.js';
|
|
2
2
|
import { CursorSlashCommandConfigurator } from './cursor.js';
|
|
3
|
+
import { KiloCodeSlashCommandConfigurator } from './kilocode.js';
|
|
3
4
|
import { OpenCodeSlashCommandConfigurator } from './opencode.js';
|
|
4
5
|
export class SlashCommandRegistry {
|
|
5
6
|
static configurators = new Map();
|
|
6
7
|
static {
|
|
7
8
|
const claude = new ClaudeSlashCommandConfigurator();
|
|
8
9
|
const cursor = new CursorSlashCommandConfigurator();
|
|
10
|
+
const kilocode = new KiloCodeSlashCommandConfigurator();
|
|
9
11
|
const opencode = new OpenCodeSlashCommandConfigurator();
|
|
10
12
|
this.configurators.set(claude.toolId, claude);
|
|
11
13
|
this.configurators.set(cursor.toolId, cursor);
|
|
14
|
+
this.configurators.set(kilocode.toolId, kilocode);
|
|
12
15
|
this.configurators.set(opencode.toolId, opencode);
|
|
13
16
|
}
|
|
14
17
|
static register(configurator) {
|
package/dist/core/init.d.ts
CHANGED
|
@@ -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
|
|
40
|
-
const
|
|
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(
|
|
54
|
+
const pageSize = Math.max(config.choices.length, 1);
|
|
44
55
|
const updateSelected = (next) => {
|
|
45
|
-
const ordered =
|
|
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:
|
|
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
|
|
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
|
-
|
|
78
|
-
setCursor(previousIndex);
|
|
115
|
+
moveCursor(-1);
|
|
79
116
|
setError(null);
|
|
80
117
|
return;
|
|
81
118
|
}
|
|
82
119
|
if (isDownKey(key)) {
|
|
83
|
-
|
|
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
|
-
|
|
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
|
|
133
|
-
|
|
134
|
-
.
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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 (
|
|
172
|
-
lines.push(PALETTE.
|
|
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
|
-
|
|
176
|
-
lines.push(`${PALETTE.white('▌')} ${
|
|
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
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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** ...',
|