@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 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')
@@ -480,11 +480,12 @@ export class ChangeCommand {
480
480
  }
481
481
  else {
482
482
  console.error(`Change "${changeName}" has issues`);
483
- report.issues.forEach(issue => {
484
- const label = issue.level === 'ERROR' ? 'ERROR' : 'WARNING';
485
- const prefix = issue.level === 'ERROR' ? '✗' : '⚠';
486
- console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
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
- for (const issue of report.issues) {
195
- const label = issue.level === 'ERROR' ? 'ERROR' : issue.level;
196
- const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
197
- console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
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.json) {
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.json) {
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
- const firstFailure = results.find((res) => !res.valid);
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,
@@ -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'] },
@@ -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 only when at least one IDE/editor-resident tool
1038
- // actually received a generated surface. Two conditions, coupled to the SAME
1039
- // tool: (1) its commands/skills are loaded by a long-running editor process
1040
- // (CLI tools pick the files up immediately, so a restart line would be wrong
1041
- // for them — see #1067), and (2) a surface was actually generated for it
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(restartCommandsGenerated
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
- await fs.writeFile(absolutePath, '', 'utf-8');
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
@@ -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
@@ -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 shouldRestartIde = affectedToolIds.some((toolId) => {
331
- const tool = AI_TOOLS.find((candidate) => candidate.value === toolId);
332
- return Boolean(tool?.requiresIdeRestart &&
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 the scenario loss archive refuses to
23
- * apply (#1477). When `options.projectRoot` is given, the schema's tracked
24
- * task files are checked for ambiguous numbering (#1520). Omitting either
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 the scenario loss archive refuses to
116
- * apply (#1477). When `options.projectRoot` is given, the schema's tracked
117
- * task files are checked for ambiguous numbering (#1520). Omitting either
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 that have no slash-command surface at all: skills are matched
64
- * automatically or invoked by natural-language prompts, never by typing a
65
- * `/<name>` command. Rovo Dev CLI is such a tool — `/skills` only manages
66
- * skills, and any `/openspec-*` form would be a dead command (see
67
- * docs/supported-tools.md). References for these tools are spelled as prose
68
- * ("the openspec-propose skill") so generated content never tells the user to
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.11.0",
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.7.0",
45
- "@changesets/cli": "^2.31.1",
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",