@fission-ai/openspec 1.11.0 → 1.12.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 +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/commands/change.js +6 -5
- package/dist/commands/validate.d.ts +57 -0
- package/dist/commands/validate.js +99 -13
- package/dist/core/command-generation/adapters/codeassistant.d.ts +15 -0
- package/dist/core/command-generation/adapters/codeassistant.js +29 -0
- package/dist/core/command-generation/adapters/index.d.ts +1 -0
- package/dist/core/command-generation/adapters/index.js +1 -0
- package/dist/core/command-generation/registry.js +2 -0
- package/dist/core/completions/command-registry.js +6 -0
- package/dist/core/config.js +1 -0
- package/dist/core/init.d.ts +1 -0
- package/dist/core/init.js +21 -34
- package/dist/core/openspec-root.d.ts +1 -0
- package/dist/core/openspec-root.js +10 -2
- package/dist/core/shared/ide-restart.d.ts +26 -0
- package/dist/core/shared/ide-restart.js +42 -0
- package/dist/core/shared/index.d.ts +1 -0
- package/dist/core/shared/index.js +1 -0
- package/dist/core/specs-apply.js +6 -1
- package/dist/core/templates/workflows/explore.js +29 -0
- package/dist/core/templates/workflows/ff-change.js +8 -0
- package/dist/core/templates/workflows/propose.js +8 -0
- package/dist/core/update.js +4 -9
- package/dist/core/validation/validator.d.ts +12 -3
- package/dist/core/validation/validator.js +74 -3
- package/dist/utils/command-references.js +7 -8
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -172,6 +172,7 @@ Both are in the default profile. If you want the expanded workflow (`/opsx:new`,
|
|
|
172
172
|
→ **[Concepts](docs/concepts.md)**: how it all fits<br>
|
|
173
173
|
→ **[Multi-Language](docs/multi-language.md)**: multi-language support<br>
|
|
174
174
|
→ **[Customization](docs/customization.md)**: make it yours<br>
|
|
175
|
+
→ **[Community Showcase](docs/community.md)**: projects and resources built with and for OpenSpec<br>
|
|
175
176
|
→ **[FAQ](docs/faq.md)** · **[Troubleshooting](docs/troubleshooting.md)** · **[Glossary](docs/glossary.md)**: quick help
|
|
176
177
|
|
|
177
178
|
|
package/dist/cli/index.js
CHANGED
|
@@ -449,6 +449,7 @@ program
|
|
|
449
449
|
.option('--changes', 'Validate all changes')
|
|
450
450
|
.option('--specs', 'Validate all specs')
|
|
451
451
|
.option('--archived', 'Validate that archived changes have all tasks completed (for pre-commit linting)')
|
|
452
|
+
.option('--report <full|findings>', 'Select bulk report content: full|findings; combine with --json for JSON')
|
|
452
453
|
.option('--type <type>', 'Specify item type when ambiguous: change|spec')
|
|
453
454
|
.option('--strict', 'Enable strict validation mode')
|
|
454
455
|
.option('--json', 'Output validation results as JSON')
|
package/dist/commands/change.js
CHANGED
|
@@ -480,11 +480,12 @@ export class ChangeCommand {
|
|
|
480
480
|
}
|
|
481
481
|
else {
|
|
482
482
|
console.error(`Change "${changeName}" has issues`);
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
483
|
+
}
|
|
484
|
+
report.issues.forEach(issue => {
|
|
485
|
+
const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
|
|
486
|
+
console.error(`${prefix} [${issue.level}] ${issue.path}: ${issue.message}`);
|
|
487
|
+
});
|
|
488
|
+
if (!report.valid) {
|
|
488
489
|
// Next steps footer to guide fixing issues
|
|
489
490
|
this.printNextSteps(report.issues);
|
|
490
491
|
if (!options?.json) {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { toRootOutput } from '../core/root-selection.js';
|
|
2
|
+
type ItemType = 'change' | 'spec';
|
|
1
3
|
interface ExecuteOptions {
|
|
2
4
|
all?: boolean;
|
|
3
5
|
changes?: boolean;
|
|
4
6
|
specs?: boolean;
|
|
5
7
|
archived?: boolean;
|
|
8
|
+
report?: string;
|
|
6
9
|
type?: string;
|
|
7
10
|
strict?: boolean;
|
|
8
11
|
json?: boolean;
|
|
@@ -12,6 +15,58 @@ interface ExecuteOptions {
|
|
|
12
15
|
store?: string;
|
|
13
16
|
storePath?: string;
|
|
14
17
|
}
|
|
18
|
+
interface BulkItemResult {
|
|
19
|
+
id: string;
|
|
20
|
+
type: ItemType;
|
|
21
|
+
valid: boolean;
|
|
22
|
+
issues: {
|
|
23
|
+
level: 'ERROR' | 'WARNING' | 'INFO';
|
|
24
|
+
path: string;
|
|
25
|
+
message: string;
|
|
26
|
+
}[];
|
|
27
|
+
durationMs: number;
|
|
28
|
+
}
|
|
29
|
+
type BulkScope = 'all' | 'changes' | 'specs' | 'archived';
|
|
30
|
+
interface BulkValidationResult<T extends BulkItemResult = BulkItemResult> {
|
|
31
|
+
items: T[];
|
|
32
|
+
summary: {
|
|
33
|
+
totals: {
|
|
34
|
+
items: number;
|
|
35
|
+
passed: number;
|
|
36
|
+
failed: number;
|
|
37
|
+
};
|
|
38
|
+
byType: Partial<Record<ItemType, {
|
|
39
|
+
items: number;
|
|
40
|
+
passed: number;
|
|
41
|
+
failed: number;
|
|
42
|
+
}>>;
|
|
43
|
+
};
|
|
44
|
+
root: ReturnType<typeof toRootOutput>;
|
|
45
|
+
}
|
|
46
|
+
/** Findings are a distinct report, not a partial full-v1 items collection. */
|
|
47
|
+
export declare function projectValidationFindings<T extends BulkItemResult>(full: BulkValidationResult<T>, scope: BulkScope): {
|
|
48
|
+
report: {
|
|
49
|
+
kind: "validation-findings";
|
|
50
|
+
version: "1.0";
|
|
51
|
+
scope: BulkScope;
|
|
52
|
+
returnedItems: number;
|
|
53
|
+
totalItems: number;
|
|
54
|
+
};
|
|
55
|
+
itemFindings: T[];
|
|
56
|
+
summary: {
|
|
57
|
+
totals: {
|
|
58
|
+
items: number;
|
|
59
|
+
passed: number;
|
|
60
|
+
failed: number;
|
|
61
|
+
};
|
|
62
|
+
byType: Partial<Record<ItemType, {
|
|
63
|
+
items: number;
|
|
64
|
+
passed: number;
|
|
65
|
+
failed: number;
|
|
66
|
+
}>>;
|
|
67
|
+
};
|
|
68
|
+
root: import("../core/root-selection.js").RootOutput;
|
|
69
|
+
};
|
|
15
70
|
export declare class ValidateCommand {
|
|
16
71
|
execute(itemName: string | undefined, options?: ExecuteOptions): Promise<void>;
|
|
17
72
|
private normalizeType;
|
|
@@ -29,6 +84,8 @@ export declare class ValidateCommand {
|
|
|
29
84
|
private validateByType;
|
|
30
85
|
private printReport;
|
|
31
86
|
private printNextSteps;
|
|
87
|
+
private printFindingsReport;
|
|
88
|
+
private printBulkDetails;
|
|
32
89
|
private runBulkValidation;
|
|
33
90
|
/**
|
|
34
91
|
* Lists archived change ids from the resolved root's archive directory,
|
|
@@ -10,9 +10,54 @@ import { nearestMatches } from '../utils/match.js';
|
|
|
10
10
|
import { promises as fs } from 'fs';
|
|
11
11
|
import { getTaskProgressDetailForChange } from '../utils/task-progress.js';
|
|
12
12
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
13
|
+
/** Findings are a distinct report, not a partial full-v1 items collection. */
|
|
14
|
+
export function projectValidationFindings(full, scope) {
|
|
15
|
+
const itemFindings = full.items.filter(item => item.issues.length > 0);
|
|
16
|
+
return {
|
|
17
|
+
report: {
|
|
18
|
+
kind: 'validation-findings',
|
|
19
|
+
version: '1.0',
|
|
20
|
+
scope,
|
|
21
|
+
returnedItems: itemFindings.length,
|
|
22
|
+
totalItems: full.summary.totals.items,
|
|
23
|
+
},
|
|
24
|
+
itemFindings,
|
|
25
|
+
summary: full.summary,
|
|
26
|
+
root: full.root,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
13
29
|
export class ValidateCommand {
|
|
14
30
|
async execute(itemName, options = {}) {
|
|
15
31
|
const bulk = options.all || options.changes || options.specs;
|
|
32
|
+
let findingsScope;
|
|
33
|
+
if (options.report !== undefined) {
|
|
34
|
+
const message = options.report !== 'full' && options.report !== 'findings'
|
|
35
|
+
? `Unknown validation report '${options.report}'.`
|
|
36
|
+
: itemName !== undefined
|
|
37
|
+
? 'A validation report cannot be combined with an item name.'
|
|
38
|
+
: options.archived && bulk
|
|
39
|
+
? 'A validation report cannot combine archived and active scopes.'
|
|
40
|
+
: !options.archived && !bulk
|
|
41
|
+
? 'A validation report requires an explicit bulk scope.'
|
|
42
|
+
: undefined;
|
|
43
|
+
if (message) {
|
|
44
|
+
const fix = 'Use --report full|findings with --all, --changes, --specs, or --archived, without an item name. Do not combine archived and active scopes.';
|
|
45
|
+
if (options.json) {
|
|
46
|
+
console.log(JSON.stringify({ status: [{ severity: 'error', code: 'invalid_validation_report_request', message, fix }] }, null, 2));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.error(`Error: ${message}`);
|
|
50
|
+
console.error(`Fix: ${fix}`);
|
|
51
|
+
}
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (options.report === 'findings') {
|
|
56
|
+
findingsScope = options.archived ? 'archived'
|
|
57
|
+
: options.all || (options.changes && options.specs) ? 'all'
|
|
58
|
+
: options.changes ? 'changes' : 'specs';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
16
61
|
const root = await resolveRootForCommand(options, {
|
|
17
62
|
json: options.json,
|
|
18
63
|
...(bulk ? { allowImplicitRoot: false } : {}),
|
|
@@ -29,6 +74,7 @@ export class ValidateCommand {
|
|
|
29
74
|
await this.runArchivedTaskValidation(root, {
|
|
30
75
|
json: !!options.json,
|
|
31
76
|
noInteractive: resolveNoInteractive(options),
|
|
77
|
+
findingsScope,
|
|
32
78
|
});
|
|
33
79
|
return;
|
|
34
80
|
}
|
|
@@ -37,7 +83,7 @@ export class ValidateCommand {
|
|
|
37
83
|
await this.runBulkValidation(root, {
|
|
38
84
|
changes: !!options.all || !!options.changes,
|
|
39
85
|
specs: !!options.all || !!options.specs,
|
|
40
|
-
}, { strict: !!options.strict, json: !!options.json, concurrency: options.concurrency, noInteractive: resolveNoInteractive(options) });
|
|
86
|
+
}, { strict: !!options.strict, json: !!options.json, concurrency: options.concurrency, noInteractive: resolveNoInteractive(options), findingsScope });
|
|
41
87
|
return;
|
|
42
88
|
}
|
|
43
89
|
// No item and no flags
|
|
@@ -191,11 +237,12 @@ export class ValidateCommand {
|
|
|
191
237
|
}
|
|
192
238
|
else {
|
|
193
239
|
console.error(`${type === 'change' ? 'Change' : 'Specification'} '${id}' has issues`);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
240
|
+
}
|
|
241
|
+
for (const issue of report.issues) {
|
|
242
|
+
const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
|
|
243
|
+
console.error(`${prefix} [${issue.level}] ${issue.path}: ${issue.message}`);
|
|
244
|
+
}
|
|
245
|
+
if (!report.valid) {
|
|
199
246
|
this.printNextSteps(type, id, root, report.issues);
|
|
200
247
|
}
|
|
201
248
|
}
|
|
@@ -228,6 +275,34 @@ export class ValidateCommand {
|
|
|
228
275
|
console.error('Next steps:');
|
|
229
276
|
bullets.forEach(b => console.error(` ${b}`));
|
|
230
277
|
}
|
|
278
|
+
printFindingsReport(full, scope, json, root) {
|
|
279
|
+
const findings = projectValidationFindings(full, scope);
|
|
280
|
+
if (json) {
|
|
281
|
+
console.log(JSON.stringify(findings, null, 2));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
console.log(`Scope: ${scope} (${findings.report.totalItems} items)`);
|
|
285
|
+
if (findings.itemFindings.length === 0) {
|
|
286
|
+
console.log('No item findings.');
|
|
287
|
+
}
|
|
288
|
+
for (const item of findings.itemFindings) {
|
|
289
|
+
console.error(`${item.type}/${item.id}`);
|
|
290
|
+
for (const issue of item.issues) {
|
|
291
|
+
console.error(` [${issue.level}] ${issue.path}: ${issue.message}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const totals = findings.summary.totals;
|
|
295
|
+
console.log(`Totals: ${totals.passed} passed, ${totals.failed} failed (${totals.items} items)`);
|
|
296
|
+
if (scope !== 'archived')
|
|
297
|
+
this.printBulkDetails(full.items, root);
|
|
298
|
+
}
|
|
299
|
+
printBulkDetails(results, root) {
|
|
300
|
+
const firstFailure = results.find((res) => !res.valid);
|
|
301
|
+
if (firstFailure) {
|
|
302
|
+
const storeFlag = isStoreSelectedRoot(root) ? ` --store ${root.storeId}` : '';
|
|
303
|
+
console.log(`Details: openspec validate ${firstFailure.id} --type ${firstFailure.type}${storeFlag}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
231
306
|
async runBulkValidation(root, scope, opts) {
|
|
232
307
|
const spinner = !opts.json && !opts.noInteractive ? ora('Validating...').start() : undefined;
|
|
233
308
|
const [changeIds, specIds] = await Promise.all([
|
|
@@ -269,7 +344,10 @@ export class ValidateCommand {
|
|
|
269
344
|
...(scope.specs ? { spec: { items: 0, passed: 0, failed: 0 } } : {}),
|
|
270
345
|
},
|
|
271
346
|
};
|
|
272
|
-
if (opts.
|
|
347
|
+
if (opts.findingsScope) {
|
|
348
|
+
this.printFindingsReport({ items: [], summary, root: toRootOutput(root) }, opts.findingsScope, opts.json, root);
|
|
349
|
+
}
|
|
350
|
+
else if (opts.json) {
|
|
273
351
|
const out = { items: [], summary, version: '1.0', root: toRootOutput(root) };
|
|
274
352
|
console.log(JSON.stringify(out, null, 2));
|
|
275
353
|
}
|
|
@@ -326,7 +404,10 @@ export class ValidateCommand {
|
|
|
326
404
|
...(scope.specs ? { spec: summarizeType(results, 'spec') } : {}),
|
|
327
405
|
},
|
|
328
406
|
};
|
|
329
|
-
if (opts.
|
|
407
|
+
if (opts.findingsScope) {
|
|
408
|
+
this.printFindingsReport({ items: results, summary, root: toRootOutput(root) }, opts.findingsScope, opts.json, root);
|
|
409
|
+
}
|
|
410
|
+
else if (opts.json) {
|
|
330
411
|
const out = { items: results, summary, version: '1.0', root: toRootOutput(root) };
|
|
331
412
|
console.log(JSON.stringify(out, null, 2));
|
|
332
413
|
}
|
|
@@ -336,13 +417,13 @@ export class ValidateCommand {
|
|
|
336
417
|
console.log(`✓ ${res.type}/${res.id}`);
|
|
337
418
|
else
|
|
338
419
|
console.error(`✗ ${res.type}/${res.id}`);
|
|
420
|
+
for (const issue of res.issues) {
|
|
421
|
+
const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
|
|
422
|
+
console.error(` ${prefix} [${issue.level}] ${issue.path}: ${issue.message}`);
|
|
423
|
+
}
|
|
339
424
|
}
|
|
340
425
|
console.log(`Totals: ${summary.totals.passed} passed, ${summary.totals.failed} failed (${summary.totals.items} items)`);
|
|
341
|
-
|
|
342
|
-
if (firstFailure) {
|
|
343
|
-
const storeFlag = isStoreSelectedRoot(root) ? ` --store ${root.storeId}` : '';
|
|
344
|
-
console.log(`Details: openspec validate ${firstFailure.id} --type ${firstFailure.type}${storeFlag}`);
|
|
345
|
-
}
|
|
426
|
+
this.printBulkDetails(results, root);
|
|
346
427
|
}
|
|
347
428
|
process.exitCode = failed > 0 ? 1 : 0;
|
|
348
429
|
}
|
|
@@ -438,6 +519,11 @@ export class ValidateCommand {
|
|
|
438
519
|
totals: { items: results.length, passed, failed },
|
|
439
520
|
byType: { change: summarizeType(results, 'change') },
|
|
440
521
|
};
|
|
522
|
+
if (opts.findingsScope) {
|
|
523
|
+
this.printFindingsReport({ items: results, summary, root: toRootOutput(root) }, opts.findingsScope, opts.json, root);
|
|
524
|
+
process.exitCode = failed > 0 ? 1 : 0;
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
441
527
|
if (opts.json) {
|
|
442
528
|
const out = { items: results, summary, version: '1.0', root: toRootOutput(root) };
|
|
443
529
|
console.log(JSON.stringify(out, null, 2));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SourceCraft Code Assistant Command Adapter
|
|
3
|
+
*
|
|
4
|
+
* Formats commands for the SourceCraft Code Assistant VS Code extension.
|
|
5
|
+
*
|
|
6
|
+
* @see https://sourcecraft.dev/portal/docs/en/code-assistant/operations/agent/slash-commands
|
|
7
|
+
*/
|
|
8
|
+
import type { ToolCommandAdapter } from '../types.js';
|
|
9
|
+
/**
|
|
10
|
+
* SourceCraft Code Assistant adapter for command generation.
|
|
11
|
+
* File path: .codeassistant/commands/opsx-<id>.md
|
|
12
|
+
* Format: YAML frontmatter with description
|
|
13
|
+
*/
|
|
14
|
+
export declare const codeassistantAdapter: ToolCommandAdapter;
|
|
15
|
+
//# sourceMappingURL=codeassistant.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SourceCraft Code Assistant Command Adapter
|
|
3
|
+
*
|
|
4
|
+
* Formats commands for the SourceCraft Code Assistant VS Code extension.
|
|
5
|
+
*
|
|
6
|
+
* @see https://sourcecraft.dev/portal/docs/en/code-assistant/operations/agent/slash-commands
|
|
7
|
+
*/
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { escapeYamlValue } from '../yaml.js';
|
|
10
|
+
/**
|
|
11
|
+
* SourceCraft Code Assistant adapter for command generation.
|
|
12
|
+
* File path: .codeassistant/commands/opsx-<id>.md
|
|
13
|
+
* Format: YAML frontmatter with description
|
|
14
|
+
*/
|
|
15
|
+
export const codeassistantAdapter = {
|
|
16
|
+
toolId: 'codeassistant',
|
|
17
|
+
getFilePath(commandId) {
|
|
18
|
+
return path.join('.codeassistant', 'commands', `opsx-${commandId}.md`);
|
|
19
|
+
},
|
|
20
|
+
formatFile(content) {
|
|
21
|
+
return `---
|
|
22
|
+
description: ${escapeYamlValue(content.description)}
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
${content.body}
|
|
26
|
+
`;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=codeassistant.js.map
|
|
@@ -26,6 +26,7 @@ export { kiroAdapter } from './kiro.js';
|
|
|
26
26
|
export { ohMyPiAdapter } from './oh-my-pi.js';
|
|
27
27
|
export { opencodeAdapter } from './opencode.js';
|
|
28
28
|
export { piAdapter } from './pi.js';
|
|
29
|
+
export { codeassistantAdapter } from './codeassistant.js';
|
|
29
30
|
export { qoderAdapter } from './qoder.js';
|
|
30
31
|
export { lingmaAdapter } from './lingma.js';
|
|
31
32
|
export { qwenAdapter } from './qwen.js';
|
|
@@ -26,6 +26,7 @@ export { kiroAdapter } from './kiro.js';
|
|
|
26
26
|
export { ohMyPiAdapter } from './oh-my-pi.js';
|
|
27
27
|
export { opencodeAdapter } from './opencode.js';
|
|
28
28
|
export { piAdapter } from './pi.js';
|
|
29
|
+
export { codeassistantAdapter } from './codeassistant.js';
|
|
29
30
|
export { qoderAdapter } from './qoder.js';
|
|
30
31
|
export { lingmaAdapter } from './lingma.js';
|
|
31
32
|
export { qwenAdapter } from './qwen.js';
|
|
@@ -27,6 +27,7 @@ import { kiroAdapter } from './adapters/kiro.js';
|
|
|
27
27
|
import { ohMyPiAdapter } from './adapters/oh-my-pi.js';
|
|
28
28
|
import { opencodeAdapter } from './adapters/opencode.js';
|
|
29
29
|
import { piAdapter } from './adapters/pi.js';
|
|
30
|
+
import { codeassistantAdapter } from './adapters/codeassistant.js';
|
|
30
31
|
import { qoderAdapter } from './adapters/qoder.js';
|
|
31
32
|
import { lingmaAdapter } from './adapters/lingma.js';
|
|
32
33
|
import { qwenAdapter } from './adapters/qwen.js';
|
|
@@ -63,6 +64,7 @@ export class CommandAdapterRegistry {
|
|
|
63
64
|
CommandAdapterRegistry.register(ohMyPiAdapter);
|
|
64
65
|
CommandAdapterRegistry.register(opencodeAdapter);
|
|
65
66
|
CommandAdapterRegistry.register(piAdapter);
|
|
67
|
+
CommandAdapterRegistry.register(codeassistantAdapter);
|
|
66
68
|
CommandAdapterRegistry.register(qoderAdapter);
|
|
67
69
|
CommandAdapterRegistry.register(lingmaAdapter);
|
|
68
70
|
CommandAdapterRegistry.register(qwenAdapter);
|
|
@@ -106,6 +106,12 @@ export const COMMAND_REGISTRY = [
|
|
|
106
106
|
name: 'archived',
|
|
107
107
|
description: 'Validate that archived changes have all tasks completed (for pre-commit linting)',
|
|
108
108
|
},
|
|
109
|
+
{
|
|
110
|
+
name: 'report',
|
|
111
|
+
description: 'Select bulk report content',
|
|
112
|
+
takesValue: true,
|
|
113
|
+
values: ['full', 'findings'],
|
|
114
|
+
},
|
|
109
115
|
COMMON_FLAGS.type,
|
|
110
116
|
COMMON_FLAGS.strict,
|
|
111
117
|
COMMON_FLAGS.jsonValidation,
|
package/dist/core/config.js
CHANGED
|
@@ -54,6 +54,7 @@ export const AI_TOOLS = [
|
|
|
54
54
|
{ name: 'Oh My Pi', value: 'oh-my-pi', available: true, successLabel: 'Oh My Pi', skillsDir: '.omp' },
|
|
55
55
|
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' },
|
|
56
56
|
{ name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' },
|
|
57
|
+
{ name: 'SourceCraft Code Assistant', value: 'codeassistant', available: true, successLabel: 'SourceCraft Code Assistant', skillsDir: '.codeassistant' },
|
|
57
58
|
{ name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder', requiresIdeRestart: true },
|
|
58
59
|
{ name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' },
|
|
59
60
|
{ name: 'Rovo Dev CLI', value: 'rovodev', available: true, successLabel: 'Rovo Dev CLI', skillsDir: '.rovodev', detectionPaths: ['.rovodev/skills', '.rovodev'] },
|
package/dist/core/init.d.ts
CHANGED
|
@@ -78,6 +78,7 @@ export declare class InitCommand {
|
|
|
78
78
|
private resolveToolsArg;
|
|
79
79
|
private validateTools;
|
|
80
80
|
private createDirectoryStructure;
|
|
81
|
+
private writeGitkeepFiles;
|
|
81
82
|
/**
|
|
82
83
|
* Generates skill files and slash commands for each selected tool,
|
|
83
84
|
* honoring the configured delivery mode (skills, commands, or both).
|
package/dist/core/init.js
CHANGED
|
@@ -12,6 +12,7 @@ import { createRequire } from 'module';
|
|
|
12
12
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
13
13
|
import { classifyOpenSpecDir, MAX_CONTEXT_SIZE, readProjectConfig, storePointerProblem, } from './project-config.js';
|
|
14
14
|
import { findRepoPlanningRootSync } from './planning-home.js';
|
|
15
|
+
import { ANCHORED_OPENSPEC_DIRS, ensureDirectoryAnchor } from './openspec-root.js';
|
|
15
16
|
import { getSkillReferenceTransformer, getTransformerForTool, usesNaturalLanguageSkillReferences } from '../utils/command-references.js';
|
|
16
17
|
import { AI_TOOLS, OPENSPEC_DIR_NAME, resolveToolIdAlias, } from './config.js';
|
|
17
18
|
import { PALETTE } from './styles/palette.js';
|
|
@@ -19,7 +20,7 @@ import { isInteractive } from '../utils/interactive.js';
|
|
|
19
20
|
import { serializeConfig } from './config-prompts.js';
|
|
20
21
|
import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
|
|
21
22
|
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDeferredGlobalPromptSummary, formatDetectionSummary, getLegacyGlobalPromptMatches, omitGlobalLegacyPromptFiles, pickGlobalLegacyPromptFiles, } from './legacy-cleanup.js';
|
|
22
|
-
import { getToolsWithSkillsDir, getToolStates, getSkillTemplates, getCommandContents, generateSkillContent, hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, } from './shared/index.js';
|
|
23
|
+
import { getToolsWithSkillsDir, getToolStates, getSkillTemplates, getCommandContents, generateSkillContent, hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, formatIdeRestart, } from './shared/index.js';
|
|
23
24
|
import { getGlobalConfig } from './global-config.js';
|
|
24
25
|
import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js';
|
|
25
26
|
import { getAvailableTools } from './available-tools.js';
|
|
@@ -590,7 +591,6 @@ export class InitCommand {
|
|
|
590
591
|
skillsRoot: isGlobalSkillTarget ? skillsPath : projectPath,
|
|
591
592
|
isGlobalSkillTarget,
|
|
592
593
|
wasConfigured: preState?.configured ?? false,
|
|
593
|
-
requiresIdeRestart: tool.requiresIdeRestart,
|
|
594
594
|
writesSkills: !tool.skillsDir || skillWriters.has(tool.value),
|
|
595
595
|
});
|
|
596
596
|
}
|
|
@@ -600,36 +600,37 @@ export class InitCommand {
|
|
|
600
600
|
// DIRECTORY STRUCTURE
|
|
601
601
|
// ═══════════════════════════════════════════════════════════
|
|
602
602
|
async createDirectoryStructure(openspecPath, extendMode) {
|
|
603
|
+
const directories = [
|
|
604
|
+
openspecPath,
|
|
605
|
+
path.join(openspecPath, 'specs'),
|
|
606
|
+
path.join(openspecPath, 'changes'),
|
|
607
|
+
path.join(openspecPath, 'changes', 'archive'),
|
|
608
|
+
];
|
|
603
609
|
if (extendMode) {
|
|
604
610
|
// In extend mode, just ensure directories exist without spinner
|
|
605
|
-
const directories = [
|
|
606
|
-
openspecPath,
|
|
607
|
-
path.join(openspecPath, 'specs'),
|
|
608
|
-
path.join(openspecPath, 'changes'),
|
|
609
|
-
path.join(openspecPath, 'changes', 'archive'),
|
|
610
|
-
];
|
|
611
611
|
for (const dir of directories) {
|
|
612
612
|
FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), dir);
|
|
613
613
|
await FileSystemUtils.createDirectory(dir);
|
|
614
614
|
}
|
|
615
|
+
await this.writeGitkeepFiles(openspecPath);
|
|
615
616
|
return;
|
|
616
617
|
}
|
|
617
618
|
const spinner = this.startSpinner('Creating OpenSpec structure...');
|
|
618
|
-
const directories = [
|
|
619
|
-
openspecPath,
|
|
620
|
-
path.join(openspecPath, 'specs'),
|
|
621
|
-
path.join(openspecPath, 'changes'),
|
|
622
|
-
path.join(openspecPath, 'changes', 'archive'),
|
|
623
|
-
];
|
|
624
619
|
for (const dir of directories) {
|
|
625
620
|
FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), dir);
|
|
626
621
|
await FileSystemUtils.createDirectory(dir);
|
|
627
622
|
}
|
|
623
|
+
await this.writeGitkeepFiles(openspecPath);
|
|
628
624
|
spinner.stopAndPersist({
|
|
629
625
|
symbol: PALETTE.white('▌'),
|
|
630
626
|
text: PALETTE.white('OpenSpec structure created'),
|
|
631
627
|
});
|
|
632
628
|
}
|
|
629
|
+
async writeGitkeepFiles(openspecPath) {
|
|
630
|
+
for (const relativeDir of ANCHORED_OPENSPEC_DIRS) {
|
|
631
|
+
await ensureDirectoryAnchor(path.dirname(openspecPath), relativeDir);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
633
634
|
// ═══════════════════════════════════════════════════════════
|
|
634
635
|
// SKILL & COMMAND GENERATION
|
|
635
636
|
// ═══════════════════════════════════════════════════════════
|
|
@@ -1034,27 +1035,13 @@ export class InitCommand {
|
|
|
1034
1035
|
console.log();
|
|
1035
1036
|
console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
|
|
1036
1037
|
console.log(`Feedback: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec/issues')}`);
|
|
1037
|
-
// Restart instruction
|
|
1038
|
-
//
|
|
1039
|
-
//
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
// under the active delivery (an IDE tool that generated nothing has nothing a
|
|
1043
|
-
// restart would pick up, even if a co-configured CLI tool did generate).
|
|
1044
|
-
// Wording follows what the IDE tool itself generated, not the global
|
|
1045
|
-
// aggregate: it must not say "commands" when the IDE tool only got skills
|
|
1046
|
-
// while a co-configured CLI tool got commands. Not "slash commands" either:
|
|
1047
|
-
// Amazon Q's generated files are prompt-library entries invoked with @, so a
|
|
1048
|
-
// restart line promising slash commands would be wrong for it.
|
|
1049
|
-
const restartCommandsGenerated = successfulTools.some((tool) => tool.requiresIdeRestart &&
|
|
1050
|
-
shouldGenerateCommandsForTool(tool.value, activeDelivery));
|
|
1051
|
-
const restartSkillsGenerated = successfulTools.some((tool) => tool.requiresIdeRestart &&
|
|
1052
|
-
shouldGenerateSkillsForTool(tool.value, activeDelivery));
|
|
1053
|
-
if (restartCommandsGenerated || restartSkillsGenerated) {
|
|
1038
|
+
// Restart instruction for successfully configured IDE/editor-resident tools
|
|
1039
|
+
// with a supported surface under the active delivery. The rule and wording live in
|
|
1040
|
+
// formatIdeRestart so `update` says the same thing for the same event.
|
|
1041
|
+
const restartHint = formatIdeRestart(successfulTools.map((tool) => tool.value), activeDelivery);
|
|
1042
|
+
if (restartHint) {
|
|
1054
1043
|
console.log();
|
|
1055
|
-
console.log(chalk.white(
|
|
1056
|
-
? 'Restart your IDE for the new commands to take effect.'
|
|
1057
|
-
: 'Restart your IDE for the new skills to take effect.'));
|
|
1044
|
+
console.log(chalk.white(restartHint));
|
|
1058
1045
|
}
|
|
1059
1046
|
console.log();
|
|
1060
1047
|
}
|
|
@@ -37,6 +37,7 @@ export interface EnsureOpenSpecRootResult {
|
|
|
37
37
|
createdPaths: CreatedPathLedgerEntry[];
|
|
38
38
|
}
|
|
39
39
|
export declare function inspectOpenSpecRoot(storeRoot: string): Promise<OpenSpecRootInspection>;
|
|
40
|
+
export declare function ensureDirectoryAnchor(storeRoot: string, relativeDir: string, ledger?: CreatedPathLedgerEntry[]): Promise<void>;
|
|
40
41
|
export interface EnsureOpenSpecRootOptions {
|
|
41
42
|
anchorEmptyDirectories?: boolean;
|
|
42
43
|
}
|
|
@@ -144,13 +144,21 @@ async function ensureDefaultConfig(storeRoot, ledger) {
|
|
|
144
144
|
kind: 'file',
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
|
-
async function ensureDirectoryAnchor(storeRoot, relativeDir, ledger) {
|
|
147
|
+
export async function ensureDirectoryAnchor(storeRoot, relativeDir, ledger = []) {
|
|
148
148
|
const directory = path.join(storeRoot, relativeDir);
|
|
149
149
|
if ((await fs.readdir(directory)).length > 0)
|
|
150
150
|
return;
|
|
151
151
|
const relativePath = `${relativeDir}/${DIRECTORY_ANCHOR_FILE_NAME}`;
|
|
152
152
|
const absolutePath = path.join(directory, DIRECTORY_ANCHOR_FILE_NAME);
|
|
153
|
-
|
|
153
|
+
try {
|
|
154
|
+
// A file or symlink may appear after readdir. Never replace or follow it.
|
|
155
|
+
await fs.writeFile(absolutePath, '', { encoding: 'utf-8', flag: 'wx' });
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (error.code === 'EEXIST')
|
|
159
|
+
return;
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
154
162
|
ledger.push({
|
|
155
163
|
relativePath: relativeArtifact(relativePath, 'file'),
|
|
156
164
|
absolutePath,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IDE restart hint
|
|
3
|
+
*
|
|
4
|
+
* Shared restart guidance for tools successfully configured by init or update.
|
|
5
|
+
* The wording covers additions, updates, and removals, including an empty
|
|
6
|
+
* workflow selection that removes every generated file.
|
|
7
|
+
*/
|
|
8
|
+
import type { Delivery } from '../global-config.js';
|
|
9
|
+
/** The surface a restart hint names. Absent when no hint is due. */
|
|
10
|
+
export type IdeRestartSurface = 'commands' | 'skills';
|
|
11
|
+
/**
|
|
12
|
+
* Both conditions stay coupled to the SAME tool: its surfaces are loaded by a
|
|
13
|
+
* long-running editor process (a CLI picks them up immediately, so a restart
|
|
14
|
+
* line would be wrong for it — see #1067), and it supports a generated surface
|
|
15
|
+
* under the active delivery. A CLI tool's commands must not determine the hint
|
|
16
|
+
* for an IDE tool that only supports skills. Commands take precedence when
|
|
17
|
+
* both surfaces are supported under the active delivery.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveIdeRestartSurface(toolIds: readonly string[], delivery: Delivery): IdeRestartSurface | null;
|
|
20
|
+
/**
|
|
21
|
+
* The restart line to print, or null when no restart is needed. Deliberately
|
|
22
|
+
* not "slash commands": Amazon Q's generated files are prompt-library entries
|
|
23
|
+
* invoked with `@`, so promising slash commands would be wrong for it.
|
|
24
|
+
*/
|
|
25
|
+
export declare function formatIdeRestart(toolIds: readonly string[], delivery: Delivery): string | null;
|
|
26
|
+
//# sourceMappingURL=ide-restart.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IDE restart hint
|
|
3
|
+
*
|
|
4
|
+
* Shared restart guidance for tools successfully configured by init or update.
|
|
5
|
+
* The wording covers additions, updates, and removals, including an empty
|
|
6
|
+
* workflow selection that removes every generated file.
|
|
7
|
+
*/
|
|
8
|
+
import { AI_TOOLS } from '../config.js';
|
|
9
|
+
import { shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, } from '../command-surface.js';
|
|
10
|
+
function isIdeResident(toolId) {
|
|
11
|
+
return Boolean(AI_TOOLS.find((tool) => tool.value === toolId)?.requiresIdeRestart);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Both conditions stay coupled to the SAME tool: its surfaces are loaded by a
|
|
15
|
+
* long-running editor process (a CLI picks them up immediately, so a restart
|
|
16
|
+
* line would be wrong for it — see #1067), and it supports a generated surface
|
|
17
|
+
* under the active delivery. A CLI tool's commands must not determine the hint
|
|
18
|
+
* for an IDE tool that only supports skills. Commands take precedence when
|
|
19
|
+
* both surfaces are supported under the active delivery.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveIdeRestartSurface(toolIds, delivery) {
|
|
22
|
+
const ideTools = [...new Set(toolIds)].filter(isIdeResident);
|
|
23
|
+
if (ideTools.some((toolId) => shouldGenerateCommandsForTool(toolId, delivery))) {
|
|
24
|
+
return 'commands';
|
|
25
|
+
}
|
|
26
|
+
if (ideTools.some((toolId) => shouldGenerateSkillsForTool(toolId, delivery))) {
|
|
27
|
+
return 'skills';
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The restart line to print, or null when no restart is needed. Deliberately
|
|
33
|
+
* not "slash commands": Amazon Q's generated files are prompt-library entries
|
|
34
|
+
* invoked with `@`, so promising slash commands would be wrong for it.
|
|
35
|
+
*/
|
|
36
|
+
export function formatIdeRestart(toolIds, delivery) {
|
|
37
|
+
const surface = resolveIdeRestartSurface(toolIds, delivery);
|
|
38
|
+
return surface
|
|
39
|
+
? `Restart your IDE to refresh ${surface}.`
|
|
40
|
+
: null;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=ide-restart.js.map
|
|
@@ -6,4 +6,5 @@
|
|
|
6
6
|
export { SKILL_NAMES, type SkillName, COMMAND_IDS, type CommandId, type ToolSkillStatus, type ToolVersionStatus, getToolsWithSkillsDir, getToolSkillStatus, getToolStates, extractGeneratedByVersion, getToolVersionStatus, getConfiguredTools, getAllToolVersionStatus, } from './tool-detection.js';
|
|
7
7
|
export { type SkillTemplateEntry, type CommandTemplateEntry, getSkillTemplates, getCommandTemplates, getCommandContents, generateSkillContent, } from './skill-generation.js';
|
|
8
8
|
export { type SkillCapableTool, toolSupportsSkills, getSkillCapableTools, hasGlobalSkillTarget, resolveToolSkillsDir, } from './skill-paths.js';
|
|
9
|
+
export { type IdeRestartSurface, resolveIdeRestartSurface, formatIdeRestart, } from './ide-restart.js';
|
|
9
10
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -6,4 +6,5 @@
|
|
|
6
6
|
export { SKILL_NAMES, COMMAND_IDS, getToolsWithSkillsDir, getToolSkillStatus, getToolStates, extractGeneratedByVersion, getToolVersionStatus, getConfiguredTools, getAllToolVersionStatus, } from './tool-detection.js';
|
|
7
7
|
export { getSkillTemplates, getCommandTemplates, getCommandContents, generateSkillContent, } from './skill-generation.js';
|
|
8
8
|
export { toolSupportsSkills, getSkillCapableTools, hasGlobalSkillTarget, resolveToolSkillsDir, } from './skill-paths.js';
|
|
9
|
+
export { resolveIdeRestartSurface, formatIdeRestart, } from './ide-restart.js';
|
|
9
10
|
//# sourceMappingURL=index.js.map
|
package/dist/core/specs-apply.js
CHANGED
|
@@ -214,7 +214,12 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
|
-
catch {
|
|
217
|
+
catch (error) {
|
|
218
|
+
// An unreadable target is not a new spec. Preserve the filesystem error
|
|
219
|
+
// for callers, rather than synthesizing a baseline or a missing-target finding.
|
|
220
|
+
const code = error?.code;
|
|
221
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR')
|
|
222
|
+
throw error;
|
|
218
223
|
// Target spec does not exist; MODIFIED and RENAMED are not allowed for new specs
|
|
219
224
|
// REMOVED will be ignored with a warning since there's nothing to remove
|
|
220
225
|
if (plan.modified.length > 0 || plan.renamed.length > 0) {
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
import { STORE_SELECTION_GUIDANCE } from './store-selection.js';
|
|
2
|
+
const PLANNING_GUIDANCE = `## Planning a Change
|
|
3
|
+
|
|
4
|
+
When the user is planning a change, guide them toward shared understanding with focused discovery questions. For open-ended discussion, follow the conversation without imposing an interview or a required output.
|
|
5
|
+
|
|
6
|
+
Before asking a factual question, follow the context discovery below and inspect relevant OpenSpec artifacts, source, tests, docs, and configuration. Do not ask the user to repeat facts you can verify. Summarize relevant findings without reproducing private context or rules. If evidence is missing, conflicting, or inaccessible, state that limitation and ask only for the clarification needed to proceed.
|
|
7
|
+
|
|
8
|
+
- **Follow dependencies** - Resolve the next blocking decision before its dependent details. For example, clarify the user's outcome and scope before choosing an API or data model. Revisit downstream assumptions when an earlier answer changes. Skip branches that do not matter to this goal.
|
|
9
|
+
- **Keep questions focused** - Ask one focused question at a time, and briefly explain why it matters and which decision it unlocks. Batch questions only if the user asks for a batch; keep them small and group related decisions.
|
|
10
|
+
- **Offer grounded recommendations** - When evidence supports a recommendation, state your preferred option and why it fits the user's goals, with alternatives and their tradeoffs when useful. Do not invent intent, priorities, or external constraints: ask the user when only they can answer. Avoid a fixed question format.
|
|
11
|
+
- **Keep a conversational record** - Track decisions in the conversation, not in files. Separate confirmed decisions from proposed defaults and unresolved questions. Silence is not acceptance. Accepting an answer or a batch of recommendations is not permission to write. Keep file-write confirmation separate from discovery questions and follow the guardrails below.
|
|
12
|
+
|
|
13
|
+
Stop asking when the user has enough clarity. Let them pause, pivot, or defer a decision; do not exhaust every branch or force a proposal.
|
|
14
|
+
|
|
15
|
+
For example, after inspecting the relevant code:
|
|
16
|
+
|
|
17
|
+
\`\`\`text
|
|
18
|
+
The CLI already uses SQLite and has no remote service. Is sharing state
|
|
19
|
+
across devices in scope? That determines whether local storage is enough.
|
|
20
|
+
If this stays a single-device tool, I recommend keeping SQLite to avoid
|
|
21
|
+
adding a service to operate; shared state would need a separate sync design.
|
|
22
|
+
\`\`\``;
|
|
2
23
|
export function getExploreSkillTemplate() {
|
|
3
24
|
return {
|
|
4
25
|
name: 'openspec-explore',
|
|
@@ -24,6 +45,10 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
24
45
|
|
|
25
46
|
---
|
|
26
47
|
|
|
48
|
+
${PLANNING_GUIDANCE}
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
27
52
|
## What You Might Do
|
|
28
53
|
|
|
29
54
|
Depending on what the user brings, you might:
|
|
@@ -341,6 +366,10 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
341
366
|
|
|
342
367
|
---
|
|
343
368
|
|
|
369
|
+
${PLANNING_GUIDANCE}
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
344
373
|
## What You Might Do
|
|
345
374
|
|
|
346
375
|
Depending on what the user brings, you might:
|
|
@@ -55,6 +55,10 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
55
55
|
- \`resolvedOutputPath\`: Resolved path or pattern to write the artifact
|
|
56
56
|
- \`dependencies\`: Completed artifacts to read for context
|
|
57
57
|
- Read any completed dependency files for context - always re-read them from disk, even if you saw them earlier in the conversation (the user may have edited them)
|
|
58
|
+
- **Inspect the relevant project before drafting**: Read \`context\` and \`rules\` first, then inspect relevant implementation, nearby tests, configuration, and documentation outside \`openspec/\`. Keep inspection read-only and proportional to the change; reuse findings for later artifacts and inspect more only as needed.
|
|
59
|
+
- Identify the target project from the request and project context; the planning home may be separate from the code. If the target is unclear, ask. For greenfield or non-code changes, inspect the available structure and relevant documents. If source is unavailable, state the limitation and ask when it materially affects the plan.
|
|
60
|
+
- Ground scope, approach, and tasks in what you find. Distinguish observed behavior from assumptions and proposed additions; surface conflicts with existing specs instead of silently deciding which is correct.
|
|
61
|
+
- Do this discovery now, rather than leaving generic "explore the codebase" or "make a plan" tasks for implementation. Keep any necessary follow-up investigation specific to an unresolved question.
|
|
58
62
|
- If the \`instruction\` field delegates creation to a specific skill or command, invoke it to produce the artifact instead of writing the file yourself, then verify the artifact file exists at \`resolvedOutputPath\`
|
|
59
63
|
- Otherwise create the artifact file using \`template\` as the structure and write it to \`resolvedOutputPath\`. If \`resolvedOutputPath\` is a glob, follow \`instruction\` to choose the concrete file path
|
|
60
64
|
- Apply \`context\` and \`rules\` as constraints - but do NOT copy them into the file
|
|
@@ -167,6 +171,10 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
167
171
|
- \`resolvedOutputPath\`: Resolved path or pattern to write the artifact
|
|
168
172
|
- \`dependencies\`: Completed artifacts to read for context
|
|
169
173
|
- Read any completed dependency files for context - always re-read them from disk, even if you saw them earlier in the conversation (the user may have edited them)
|
|
174
|
+
- **Inspect the relevant project before drafting**: Read \`context\` and \`rules\` first, then inspect relevant implementation, nearby tests, configuration, and documentation outside \`openspec/\`. Keep inspection read-only and proportional to the change; reuse findings for later artifacts and inspect more only as needed.
|
|
175
|
+
- Identify the target project from the request and project context; the planning home may be separate from the code. If the target is unclear, ask. For greenfield or non-code changes, inspect the available structure and relevant documents. If source is unavailable, state the limitation and ask when it materially affects the plan.
|
|
176
|
+
- Ground scope, approach, and tasks in what you find. Distinguish observed behavior from assumptions and proposed additions; surface conflicts with existing specs instead of silently deciding which is correct.
|
|
177
|
+
- Do this discovery now, rather than leaving generic "explore the codebase" or "make a plan" tasks for implementation. Keep any necessary follow-up investigation specific to an unresolved question.
|
|
170
178
|
- If the \`instruction\` field delegates creation to a specific skill or command, invoke it to produce the artifact instead of writing the file yourself, then verify the artifact file exists at \`resolvedOutputPath\`
|
|
171
179
|
- Otherwise create the artifact file using \`template\` as the structure and write it to \`resolvedOutputPath\`. If \`resolvedOutputPath\` is a glob, follow \`instruction\` to choose the concrete file path
|
|
172
180
|
- Apply \`context\` and \`rules\` as constraints - but do NOT copy them into the file
|
|
@@ -90,6 +90,10 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
90
90
|
- \`resolvedOutputPath\`: Resolved path or pattern to write the artifact
|
|
91
91
|
- \`dependencies\`: Completed artifacts to read for context
|
|
92
92
|
- Read any completed dependency files for context - always re-read them from disk, even if you saw them earlier in the conversation (the user may have edited them)
|
|
93
|
+
- **Inspect the relevant project before drafting**: Read \`context\` and \`rules\` first, then inspect relevant implementation, nearby tests, configuration, and documentation outside \`openspec/\`. Keep inspection read-only and proportional to the change; reuse findings for later artifacts and inspect more only as needed.
|
|
94
|
+
- Identify the target project from the request and project context; the planning home may be separate from the code. If the target is unclear, ask. For greenfield or non-code changes, inspect the available structure and relevant documents. If source is unavailable, state the limitation and ask when it materially affects the plan.
|
|
95
|
+
- Ground scope, approach, and tasks in what you find. Distinguish observed behavior from assumptions and proposed additions; surface conflicts with existing specs instead of silently deciding which is correct.
|
|
96
|
+
- Do this discovery now, rather than leaving generic "explore the codebase" or "make a plan" tasks for implementation. Keep any necessary follow-up investigation specific to an unresolved question.
|
|
93
97
|
- If the \`instruction\` field delegates creation to a specific skill or command, invoke it to produce the artifact instead of writing the file yourself, then verify the artifact file exists at \`resolvedOutputPath\`
|
|
94
98
|
- Otherwise create the artifact file using \`template\` as the structure and write it to \`resolvedOutputPath\`. If \`resolvedOutputPath\` is a glob, follow \`instruction\` to choose the concrete file path
|
|
95
99
|
- Apply \`context\` and \`rules\` as constraints - but do NOT copy them into the file
|
|
@@ -238,6 +242,10 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
238
242
|
- \`resolvedOutputPath\`: Resolved path or pattern to write the artifact
|
|
239
243
|
- \`dependencies\`: Completed artifacts to read for context
|
|
240
244
|
- Read any completed dependency files for context - always re-read them from disk, even if you saw them earlier in the conversation (the user may have edited them)
|
|
245
|
+
- **Inspect the relevant project before drafting**: Read \`context\` and \`rules\` first, then inspect relevant implementation, nearby tests, configuration, and documentation outside \`openspec/\`. Keep inspection read-only and proportional to the change; reuse findings for later artifacts and inspect more only as needed.
|
|
246
|
+
- Identify the target project from the request and project context; the planning home may be separate from the code. If the target is unclear, ask. For greenfield or non-code changes, inspect the available structure and relevant documents. If source is unavailable, state the limitation and ask when it materially affects the plan.
|
|
247
|
+
- Ground scope, approach, and tasks in what you find. Distinguish observed behavior from assumptions and proposed additions; surface conflicts with existing specs instead of silently deciding which is correct.
|
|
248
|
+
- Do this discovery now, rather than leaving generic "explore the codebase" or "make a plan" tasks for implementation. Keep any necessary follow-up investigation specific to an unresolved question.
|
|
241
249
|
- If the \`instruction\` field delegates creation to a specific skill or command, invoke it to produce the artifact instead of writing the file yourself, then verify the artifact file exists at \`resolvedOutputPath\`
|
|
242
250
|
- Otherwise create the artifact file using \`template\` as the structure and write it to \`resolvedOutputPath\`. If \`resolvedOutputPath\` is a glob, follow \`instruction\` to choose the concrete file path
|
|
243
251
|
- Apply \`context\` and \`rules\` as constraints - but do NOT copy them into the file
|
package/dist/core/update.js
CHANGED
|
@@ -13,7 +13,7 @@ import { FileSystemUtils } from '../utils/file-system.js';
|
|
|
13
13
|
import { getSkillReferenceTransformer, getTransformerForTool, transformToSkillReferences } from '../utils/command-references.js';
|
|
14
14
|
import { AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
|
|
15
15
|
import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
|
|
16
|
-
import { getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, } from './shared/index.js';
|
|
16
|
+
import { getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, formatIdeRestart, } from './shared/index.js';
|
|
17
17
|
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatDeferredGlobalPromptSummary, formatCleanupSummary, formatDetectionSummary, getLegacyGlobalPromptMatches, getLegacyWorkflowIdsForTool, getToolsFromLegacyArtifacts, omitGlobalLegacyPromptFiles, omitToolLegacyArtifacts, pickGlobalLegacyPromptFiles, } from './legacy-cleanup.js';
|
|
18
18
|
import { isInteractive } from '../utils/interactive.js';
|
|
19
19
|
import { getGlobalConfig } from './global-config.js';
|
|
@@ -327,14 +327,9 @@ export class UpdateCommand {
|
|
|
327
327
|
}
|
|
328
328
|
console.log();
|
|
329
329
|
const affectedToolIds = [...new Set([...newlyConfiguredTools, ...updatedToolIds])];
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
(shouldGenerateCommandsForTool(toolId, delivery) ||
|
|
334
|
-
shouldGenerateSkillsForTool(toolId, delivery)));
|
|
335
|
-
});
|
|
336
|
-
if (shouldRestartIde) {
|
|
337
|
-
console.log(chalk.dim('Restart your IDE for changes to take effect.'));
|
|
330
|
+
const restartHint = formatIdeRestart(affectedToolIds, delivery);
|
|
331
|
+
if (restartHint) {
|
|
332
|
+
console.log(chalk.dim(restartHint));
|
|
338
333
|
}
|
|
339
334
|
if (failedTools.length > 0) {
|
|
340
335
|
throw new Error(`OpenSpec update failed for: ${failedTools.map((tool) => tool.name).join(', ')}`);
|
|
@@ -19,9 +19,10 @@ export declare class Validator {
|
|
|
19
19
|
* - No duplicates within sections; no cross-section conflicts per spec
|
|
20
20
|
*
|
|
21
21
|
* When `options.mainSpecsDir` is given, MODIFIED blocks are also checked
|
|
22
|
-
* against the current main specs for
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* against the current main specs for scenario loss (#1477), and merge
|
|
23
|
+
* conflicts are reported as INFO without changing the verdict (#1112).
|
|
24
|
+
* When `options.projectRoot` is given, the schema's tracked task files are
|
|
25
|
+
* checked for ambiguous numbering (#1520). Omitting either
|
|
25
26
|
* option keeps existing library and archive callers behaving as before.
|
|
26
27
|
*/
|
|
27
28
|
validateChangeDeltaSpecs(changeDir: string, options?: {
|
|
@@ -47,6 +48,14 @@ export declare class Validator {
|
|
|
47
48
|
private applyChangeRules;
|
|
48
49
|
private enrichTopLevelError;
|
|
49
50
|
private extractNameFromPath;
|
|
51
|
+
/**
|
|
52
|
+
* Dry-run archive's merge builder without writing its result. Reusing the
|
|
53
|
+
* builder preserves its already-synced delta rules instead of duplicating them.
|
|
54
|
+
* INFO leaves the verdict unchanged: a missing target can be a typo or a
|
|
55
|
+
* requirement introduced by a sibling change that has not archived yet.
|
|
56
|
+
* This does not run archive's later merged-spec validation or retirement checks.
|
|
57
|
+
*/
|
|
58
|
+
private findArchiveBlockers;
|
|
50
59
|
private createReport;
|
|
51
60
|
isValid(report: ValidationReport): boolean;
|
|
52
61
|
private extractRequirementText;
|
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { SpecSchema, ChangeSchema } from '../schemas/index.js';
|
|
4
4
|
import { MarkdownParser } from '../parsers/markdown-parser.js';
|
|
5
5
|
import { ChangeParser } from '../parsers/change-parser.js';
|
|
6
|
+
import { findSpecUpdates, buildUpdatedSpec } from '../specs-apply.js';
|
|
6
7
|
import { MIN_PURPOSE_LENGTH, MAX_REQUIREMENT_TEXT_LENGTH, VALIDATION_MESSAGES } from './constants.js';
|
|
7
8
|
import { parseDeltaSpec, foldRequirementName, normalizeRequirementName, extractRequirementsSection, findMissingCurrentScenarios, } from '../parsers/requirement-blocks.js';
|
|
8
9
|
import { extractRequirementBody as extractRequirementBodyShared, containsShallOrMust as containsShallOrMustShared, countScenarios as countScenariosShared, } from '../parsers/requirement-text.js';
|
|
@@ -112,9 +113,10 @@ export class Validator {
|
|
|
112
113
|
* - No duplicates within sections; no cross-section conflicts per spec
|
|
113
114
|
*
|
|
114
115
|
* When `options.mainSpecsDir` is given, MODIFIED blocks are also checked
|
|
115
|
-
* against the current main specs for
|
|
116
|
-
*
|
|
117
|
-
*
|
|
116
|
+
* against the current main specs for scenario loss (#1477), and merge
|
|
117
|
+
* conflicts are reported as INFO without changing the verdict (#1112).
|
|
118
|
+
* When `options.projectRoot` is given, the schema's tracked task files are
|
|
119
|
+
* checked for ambiguous numbering (#1520). Omitting either
|
|
118
120
|
* option keeps existing library and archive callers behaving as before.
|
|
119
121
|
*/
|
|
120
122
|
async validateChangeDeltaSpecs(changeDir, options = {}) {
|
|
@@ -330,6 +332,20 @@ export class Validator {
|
|
|
330
332
|
}
|
|
331
333
|
}
|
|
332
334
|
}
|
|
335
|
+
// Reuse archive's merge builder to report conflicts with the main specs.
|
|
336
|
+
// Keep structural errors and scenario loss in their existing diagnostics.
|
|
337
|
+
if (options.mainSpecsDir) {
|
|
338
|
+
issues.push(...(await this.findArchiveBlockers(changeDir, options.mainSpecsDir, [
|
|
339
|
+
...issues.filter((issue) => issue.level === 'ERROR').map((issue) => issue.path),
|
|
340
|
+
// Collected in the loop above but not turned into issues until
|
|
341
|
+
// after this try block, so they are invisible to the filter. A
|
|
342
|
+
// delta with no parsed sections has nothing for the merge to
|
|
343
|
+
// apply, which it reports as a failure of its own - on top of the
|
|
344
|
+
// error that actually names the mistake.
|
|
345
|
+
...missingHeaderSpecs,
|
|
346
|
+
...emptySectionSpecs.map((spec) => spec.path),
|
|
347
|
+
])));
|
|
348
|
+
}
|
|
333
349
|
}
|
|
334
350
|
catch (error) {
|
|
335
351
|
// A missing specs dir (or a stray `specs` file) means no deltas;
|
|
@@ -671,6 +687,61 @@ export class Validator {
|
|
|
671
687
|
const dotIndex = fileName.lastIndexOf('.');
|
|
672
688
|
return dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName;
|
|
673
689
|
}
|
|
690
|
+
/**
|
|
691
|
+
* Dry-run archive's merge builder without writing its result. Reusing the
|
|
692
|
+
* builder preserves its already-synced delta rules instead of duplicating them.
|
|
693
|
+
* INFO leaves the verdict unchanged: a missing target can be a typo or a
|
|
694
|
+
* requirement introduced by a sibling change that has not archived yet.
|
|
695
|
+
* This does not run archive's later merged-spec validation or retirement checks.
|
|
696
|
+
*/
|
|
697
|
+
async findArchiveBlockers(changeDir, mainSpecsDir, alreadyReportedPaths) {
|
|
698
|
+
const alreadyReported = new Set(alreadyReportedPaths);
|
|
699
|
+
// Only ever reaches a generated skeleton's placeholder Purpose, which this
|
|
700
|
+
// dry run discards.
|
|
701
|
+
const changeName = path.basename(changeDir);
|
|
702
|
+
const issues = [];
|
|
703
|
+
let updates;
|
|
704
|
+
try {
|
|
705
|
+
updates = await findSpecUpdates(changeDir, mainSpecsDir);
|
|
706
|
+
}
|
|
707
|
+
catch (error) {
|
|
708
|
+
// An incomplete advisory check must not discard the validation report.
|
|
709
|
+
// Source discovery already ran above; archive retains its own path guards.
|
|
710
|
+
return [{
|
|
711
|
+
level: 'INFO',
|
|
712
|
+
path: 'specs',
|
|
713
|
+
message: `Could not check archive merge conflicts: ${error instanceof Error ? error.message : String(error)}`,
|
|
714
|
+
}];
|
|
715
|
+
}
|
|
716
|
+
for (const update of updates) {
|
|
717
|
+
// discoverSpecFiles builds both this id and the entryPath the checks
|
|
718
|
+
// above report under, from the same walk.
|
|
719
|
+
const entryPath = FileSystemUtils.toPosixPath(`${update.id}/spec.md`);
|
|
720
|
+
// A delta those checks already rejected would be reported twice, the
|
|
721
|
+
// second time in archive's wording rather than the wording that names
|
|
722
|
+
// the actual mistake.
|
|
723
|
+
if (alreadyReported.has(entryPath))
|
|
724
|
+
continue;
|
|
725
|
+
try {
|
|
726
|
+
await buildUpdatedSpec(update, changeName, { silent: true });
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
// Only the thrown preconditions, which carry no errno. A filesystem
|
|
730
|
+
// error says nothing about whether the delta applies, and `validate
|
|
731
|
+
// --all` reads six changes at once, so a transient EMFILE would report
|
|
732
|
+
// a collision that is not there - the same reason the scenario-loss
|
|
733
|
+
// check above reads only the codes that mean the file is unusable.
|
|
734
|
+
if (error?.code !== undefined)
|
|
735
|
+
continue;
|
|
736
|
+
issues.push({
|
|
737
|
+
level: 'INFO',
|
|
738
|
+
path: entryPath,
|
|
739
|
+
message: `Archive would refuse this delta: ${error instanceof Error ? error.message : String(error)}`,
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return issues;
|
|
744
|
+
}
|
|
674
745
|
createReport(issues) {
|
|
675
746
|
const errors = issues.filter(i => i.level === 'ERROR').length;
|
|
676
747
|
const warnings = issues.filter(i => i.level === 'WARNING').length;
|
|
@@ -60,15 +60,14 @@ const SKILL_INVOCATION_PREFIX = {
|
|
|
60
60
|
codex: '$',
|
|
61
61
|
};
|
|
62
62
|
/**
|
|
63
|
-
* Tools
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* type a command their CLI does not register.
|
|
63
|
+
* Tools with no documented slash invocation for skills: use automatic
|
|
64
|
+
* matching or natural-language prompts instead. SourceCraft Code Assistant
|
|
65
|
+
* supports separate command files, but its skills use description matching.
|
|
66
|
+
* Rovo Dev's `/skills` only manages skills (see docs/supported-tools.md).
|
|
67
|
+
* Skill references for these tools are spelled as prose ("the openspec-propose
|
|
68
|
+
* skill") so skills-only delivery does not advertise unregistered commands.
|
|
70
69
|
*/
|
|
71
|
-
const NATURAL_LANGUAGE_SKILL_TOOLS = new Set(['rovodev']);
|
|
70
|
+
const NATURAL_LANGUAGE_SKILL_TOOLS = new Set(['rovodev', 'codeassistant']);
|
|
72
71
|
/**
|
|
73
72
|
* Whether a tool references skills by natural language rather than a slash
|
|
74
73
|
* command (see NATURAL_LANGUAGE_SKILL_TOOLS).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fission-ai/openspec",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "AI-native system for spec-driven development",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openspec",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"node": ">=20.19.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@changesets/changelog-github": "^0.
|
|
45
|
-
"@changesets/cli": "^
|
|
44
|
+
"@changesets/changelog-github": "^1.0.0",
|
|
45
|
+
"@changesets/cli": "^3.0.1",
|
|
46
46
|
"@types/node": "^20.19.43",
|
|
47
47
|
"@vitest/ui": "^3.2.6",
|
|
48
48
|
"eslint": "^10.5.0",
|