@fission-ai/openspec 1.10.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.
Files changed (59) hide show
  1. package/README.md +1 -0
  2. package/dist/cli/index.js +12 -2
  3. package/dist/commands/change.d.ts +24 -0
  4. package/dist/commands/change.js +229 -5
  5. package/dist/commands/schema.d.ts +5 -0
  6. package/dist/commands/schema.js +201 -35
  7. package/dist/commands/show.js +1 -1
  8. package/dist/commands/validate.d.ts +57 -0
  9. package/dist/commands/validate.js +99 -13
  10. package/dist/commands/workflow/index.d.ts +1 -1
  11. package/dist/commands/workflow/index.js +1 -1
  12. package/dist/commands/workflow/status.d.ts +2 -0
  13. package/dist/commands/workflow/status.js +76 -12
  14. package/dist/core/artifact-graph/resolver.js +15 -9
  15. package/dist/core/available-tools.js +16 -1
  16. package/dist/core/command-generation/adapters/antigravity.d.ts +1 -1
  17. package/dist/core/command-generation/adapters/antigravity.js +2 -2
  18. package/dist/core/command-generation/adapters/codeassistant.d.ts +15 -0
  19. package/dist/core/command-generation/adapters/codeassistant.js +29 -0
  20. package/dist/core/command-generation/adapters/index.d.ts +1 -0
  21. package/dist/core/command-generation/adapters/index.js +1 -0
  22. package/dist/core/command-generation/registry.js +2 -0
  23. package/dist/core/completions/command-registry.js +21 -0
  24. package/dist/core/completions/generators/fish-generator.d.ts +12 -0
  25. package/dist/core/completions/generators/fish-generator.js +79 -47
  26. package/dist/core/completions/templates/fish-templates.d.ts +1 -1
  27. package/dist/core/completions/templates/fish-templates.js +107 -10
  28. package/dist/core/completions/types.d.ts +4 -0
  29. package/dist/core/config.js +7 -1
  30. package/dist/core/init.d.ts +1 -0
  31. package/dist/core/init.js +71 -65
  32. package/dist/core/legacy-cleanup.js +6 -0
  33. package/dist/core/migration.js +42 -12
  34. package/dist/core/openspec-root.d.ts +1 -0
  35. package/dist/core/openspec-root.js +10 -2
  36. package/dist/core/parsers/requirement-blocks.d.ts +1 -0
  37. package/dist/core/parsers/requirement-blocks.js +2 -0
  38. package/dist/core/shared/ide-restart.d.ts +26 -0
  39. package/dist/core/shared/ide-restart.js +42 -0
  40. package/dist/core/shared/index.d.ts +1 -0
  41. package/dist/core/shared/index.js +1 -0
  42. package/dist/core/shared/tool-detection.js +9 -3
  43. package/dist/core/shared-skill-target.d.ts +13 -0
  44. package/dist/core/shared-skill-target.js +43 -1
  45. package/dist/core/specs-apply.js +22 -9
  46. package/dist/core/templates/workflows/explore.js +108 -73
  47. package/dist/core/templates/workflows/ff-change.js +8 -0
  48. package/dist/core/templates/workflows/propose.js +8 -0
  49. package/dist/core/update.js +28 -31
  50. package/dist/core/validation/constants.d.ts +3 -0
  51. package/dist/core/validation/constants.js +12 -0
  52. package/dist/core/validation/purpose-placeholder.d.ts +45 -0
  53. package/dist/core/validation/purpose-placeholder.js +117 -0
  54. package/dist/core/validation/validator.d.ts +12 -3
  55. package/dist/core/validation/validator.js +90 -4
  56. package/dist/utils/command-references.js +7 -8
  57. package/dist/utils/requirement-diff.d.ts +42 -0
  58. package/dist/utils/requirement-diff.js +61 -0
  59. package/package.json +4 -3
@@ -24,6 +24,18 @@ export declare class FishGenerator implements CompletionGenerator {
24
24
  * Generate positional argument completion
25
25
  */
26
26
  private generatePositionalCompletion;
27
+ /**
28
+ * Generate indexed positional completions.
29
+ */
30
+ private generateIndexedPositionalCompletions;
31
+ /**
32
+ * Collect the long and short names for flags that consume the next token.
33
+ */
34
+ private collectValueFlags;
35
+ /**
36
+ * Build a Fish condition that matches command words in their actual slots.
37
+ */
38
+ private commandPathCondition;
27
39
  /**
28
40
  * Escape description text for Fish
29
41
  */
@@ -12,30 +12,26 @@ export class FishGenerator {
12
12
  * @returns Fish completion script as a string
13
13
  */
14
14
  generate(commands) {
15
- // Build top-level commands using push() for loop clarity
16
15
  const topLevelLines = [];
17
16
  for (const cmd of commands) {
18
17
  topLevelLines.push(`# ${cmd.name} command`);
19
- topLevelLines.push(`complete -c openspec -n '__fish_openspec_no_subcommand' -a '${cmd.name}' -d '${this.escapeDescription(cmd.description)}'`);
18
+ topLevelLines.push(`complete -c openspec -n '__fish_openspec_no_subcommand' -f -a '${cmd.name}' -d '${this.escapeDescription(cmd.description)}'`);
20
19
  }
21
20
  const topLevelCommands = topLevelLines.join('\n');
22
- // Build command-specific completions using push() for loop clarity
23
21
  const commandCompletionLines = [];
24
22
  for (const cmd of commands) {
25
23
  commandCompletionLines.push(...this.generateCommandCompletions(cmd));
26
24
  commandCompletionLines.push('');
27
25
  }
28
26
  const commandCompletions = commandCompletionLines.join('\n');
29
- // Static helper functions from template
30
27
  const helperFunctions = FISH_STATIC_HELPERS;
31
- // Dynamic completion helpers from template
32
28
  const dynamicHelpers = FISH_DYNAMIC_HELPERS;
33
- // Assemble final script with template literal
34
29
  return `# Fish completion script for OpenSpec CLI
35
30
  # Auto-generated - do not edit manually
36
31
 
37
32
  ${helperFunctions}
38
33
  ${dynamicHelpers}
34
+ complete -c openspec -l no-color -f -d 'Disable color output'
39
35
  ${topLevelCommands}
40
36
 
41
37
  ${commandCompletions}`;
@@ -45,38 +41,45 @@ ${commandCompletions}`;
45
41
  */
46
42
  generateCommandCompletions(cmd) {
47
43
  const lines = [];
48
- // If command has subcommands
49
44
  if (cmd.subcommands && cmd.subcommands.length > 0) {
50
- // Add subcommand completions
45
+ const commandCondition = this.commandPathCondition([cmd.name]);
46
+ const parentValueFlags = this.collectValueFlags(cmd.flags);
47
+ const noSubcommandCondition = cmd.subcommands
48
+ .map((subcmd) => `not ${this.commandPathCondition([cmd.name, subcmd.name], parentValueFlags)}`)
49
+ .join('; and ');
51
50
  for (const subcmd of cmd.subcommands) {
52
- lines.push(`complete -c openspec -n '__fish_openspec_using_subcommand ${cmd.name}; and not __fish_openspec_using_subcommand ${subcmd.name}' -a '${subcmd.name}' -d '${this.escapeDescription(subcmd.description)}'`);
51
+ lines.push(`complete -c openspec -n '${commandCondition}; and ${noSubcommandCondition}' -f -a '${subcmd.name}' -d '${this.escapeDescription(subcmd.description)}'`);
53
52
  }
54
53
  lines.push('');
55
- // Add flags for parent command
56
54
  for (const flag of cmd.flags) {
57
- lines.push(...this.generateFlagCompletion(flag, `__fish_openspec_using_subcommand ${cmd.name}`));
55
+ lines.push(...this.generateFlagCompletion(flag, commandCondition));
58
56
  }
59
- // Add completions for each subcommand
60
57
  for (const subcmd of cmd.subcommands) {
58
+ const subcommandCondition = this.commandPathCondition([cmd.name, subcmd.name], parentValueFlags);
61
59
  lines.push(`# ${cmd.name} ${subcmd.name} flags`);
60
+ lines.push(`complete -c openspec -n '${subcommandCondition}' -f`);
62
61
  for (const flag of subcmd.flags) {
63
- lines.push(...this.generateFlagCompletion(flag, `__fish_openspec_using_subcommand ${cmd.name}; and __fish_openspec_using_subcommand ${subcmd.name}`));
62
+ lines.push(...this.generateFlagCompletion(flag, subcommandCondition));
64
63
  }
65
- // Add positional completions for subcommand
66
- if (subcmd.acceptsPositional) {
67
- lines.push(...this.generatePositionalCompletion(subcmd.positionalType, `__fish_openspec_using_subcommand ${cmd.name}; and __fish_openspec_using_subcommand ${subcmd.name}`));
64
+ if (subcmd.positionals?.length) {
65
+ lines.push(...this.generateIndexedPositionalCompletions(subcmd.positionals, subcommandCondition, this.collectValueFlags(cmd.flags, subcmd.flags), 2));
66
+ }
67
+ else if (subcmd.acceptsPositional) {
68
+ lines.push(...this.generatePositionalCompletion(subcmd.positionalType, subcommandCondition));
68
69
  }
69
70
  }
70
71
  }
71
72
  else {
72
- // Command without subcommands
73
73
  lines.push(`# ${cmd.name} flags`);
74
+ lines.push(`complete -c openspec -n '__fish_openspec_using_command_path ${cmd.name}' -f`);
74
75
  for (const flag of cmd.flags) {
75
- lines.push(...this.generateFlagCompletion(flag, `__fish_openspec_using_subcommand ${cmd.name}`));
76
+ lines.push(...this.generateFlagCompletion(flag, `__fish_openspec_using_command_path ${cmd.name}`));
77
+ }
78
+ if (cmd.positionals?.length) {
79
+ lines.push(...this.generateIndexedPositionalCompletions(cmd.positionals, `__fish_openspec_using_command_path ${cmd.name}`, this.collectValueFlags(cmd.flags), 1));
76
80
  }
77
- // Add positional completions
78
- if (cmd.acceptsPositional) {
79
- lines.push(...this.generatePositionalCompletion(cmd.positionalType, `__fish_openspec_using_subcommand ${cmd.name}`));
81
+ else if (cmd.acceptsPositional) {
82
+ lines.push(...this.generatePositionalCompletion(cmd.positionalType, `__fish_openspec_using_command_path ${cmd.name}`));
80
83
  }
81
84
  }
82
85
  return lines;
@@ -86,36 +89,26 @@ ${commandCompletions}`;
86
89
  */
87
90
  generateFlagCompletion(flag, condition) {
88
91
  const lines = [];
89
- const longFlag = `--${flag.name}`;
90
- const shortFlag = flag.short ? `-${flag.short}` : undefined;
92
+ const description = this.escapeDescription(flag.description);
93
+ const shortFlag = flag.short ? `-s ${flag.short} ` : '';
94
+ const flagOptions = `${shortFlag}-l ${flag.name}`;
91
95
  if (flag.takesValue && flag.values) {
92
- // Flag with enum values
93
96
  for (const value of flag.values) {
94
- if (shortFlag) {
95
- lines.push(`complete -c openspec -n '${condition}' -s ${flag.short} -l ${flag.name} -a '${value}' -d '${this.escapeDescription(flag.description)}'`);
96
- }
97
- else {
98
- lines.push(`complete -c openspec -n '${condition}' -l ${flag.name} -a '${value}' -d '${this.escapeDescription(flag.description)}'`);
99
- }
97
+ lines.push(`complete -c openspec -n '${condition}' ${flagOptions} -r -f -a '${value}' -d '${description}'`);
100
98
  }
101
99
  }
102
100
  else if (flag.takesValue) {
103
- // Flag that takes a value but no specific values defined
104
- if (shortFlag) {
105
- lines.push(`complete -c openspec -n '${condition}' -s ${flag.short} -l ${flag.name} -r -d '${this.escapeDescription(flag.description)}'`);
106
- }
107
- else {
108
- lines.push(`complete -c openspec -n '${condition}' -l ${flag.name} -r -d '${this.escapeDescription(flag.description)}'`);
101
+ lines.push(`complete -c openspec -n '${condition}' ${flagOptions} -r -f -d '${description}'`);
102
+ if (flag.completionType === 'path') {
103
+ const optionNames = [`--${flag.name}`, ...(flag.short ? [`-${flag.short}`] : [])];
104
+ lines.push(`complete -c openspec -n '${condition}; and __fish_openspec_completing_option_value ${optionNames.join(' ')}' ${flagOptions} -r -F -d '${description}'`);
105
+ if (flag.short) {
106
+ lines.push(`complete -c openspec -n '${condition}' ${flagOptions} -r -f -a '(__fish_openspec_complete_attached_short_path -${flag.short})' -d '${description}'`);
107
+ }
109
108
  }
110
109
  }
111
110
  else {
112
- // Boolean flag
113
- if (shortFlag) {
114
- lines.push(`complete -c openspec -n '${condition}' -s ${flag.short} -l ${flag.name} -d '${this.escapeDescription(flag.description)}'`);
115
- }
116
- else {
117
- lines.push(`complete -c openspec -n '${condition}' -l ${flag.name} -d '${this.escapeDescription(flag.description)}'`);
118
- }
111
+ lines.push(`complete -c openspec -n '${condition}' ${flagOptions} -f -d '${description}'`);
119
112
  }
120
113
  return lines;
121
114
  }
@@ -141,20 +134,59 @@ ${commandCompletions}`;
141
134
  lines.push(`complete -c openspec -n '${condition}' -a 'zsh bash fish powershell' -f`);
142
135
  break;
143
136
  case 'path':
144
- // Fish automatically completes files, no need to specify
137
+ // -F re-enables filesystem completion: sibling rules in the same
138
+ // context carry -f, and Fish never restores files without --force-files.
139
+ lines.push(`complete -c openspec -n '${condition}' -F`);
140
+ break;
141
+ default:
142
+ lines.push(`complete -c openspec -n '${condition}' -f`);
145
143
  break;
146
144
  }
147
145
  return lines;
148
146
  }
147
+ /**
148
+ * Generate indexed positional completions.
149
+ */
150
+ generateIndexedPositionalCompletions(positionals, condition, valueFlags, depth) {
151
+ const lines = [];
152
+ for (const [index, positional] of positionals.entries()) {
153
+ const indexCondition = `${condition}; and __fish_openspec_positional_index ${index} ${depth}${valueFlags.length ? ` ${valueFlags.join(' ')}` : ''}`;
154
+ lines.push(...this.generatePositionalCompletion(positional.type, indexCondition));
155
+ }
156
+ return lines;
157
+ }
158
+ /**
159
+ * Collect the long and short names for flags that consume the next token.
160
+ */
161
+ collectValueFlags(...flagGroups) {
162
+ const flags = new Set();
163
+ for (const group of flagGroups) {
164
+ for (const flag of group) {
165
+ if (!flag.takesValue) {
166
+ continue;
167
+ }
168
+ flags.add(`--${flag.name}`);
169
+ if (flag.short) {
170
+ flags.add(`-${flag.short}`);
171
+ }
172
+ }
173
+ }
174
+ return [...flags];
175
+ }
176
+ /**
177
+ * Build a Fish condition that matches command words in their actual slots.
178
+ */
179
+ commandPathCondition(path, valueFlags = []) {
180
+ const valueFlagArguments = valueFlags.length ? ` -- ${valueFlags.join(' ')}` : '';
181
+ return `__fish_openspec_using_command_path ${path.join(' ')}${valueFlagArguments}`;
182
+ }
149
183
  /**
150
184
  * Escape description text for Fish
151
185
  */
152
186
  escapeDescription(description) {
153
187
  return description
154
188
  .replace(/\\/g, '\\\\') // Backslashes first
155
- .replace(/'/g, "\\'") // Single quotes
156
- .replace(/\$/g, '\\$') // Dollar signs (prevents $())
157
- .replace(/`/g, '\\`'); // Backticks
189
+ .replace(/'/g, "\\'"); // Single quotes
158
190
  }
159
191
  }
160
192
  //# sourceMappingURL=fish-generator.js.map
@@ -2,6 +2,6 @@
2
2
  * Static template strings for Fish completion scripts.
3
3
  * These are Fish-specific helper functions that never change.
4
4
  */
5
- export declare const FISH_STATIC_HELPERS = "# Helper function to check if a subcommand is present\nfunction __fish_openspec_using_subcommand\n set -l cmd (commandline -opc)\n set -e cmd[1]\n for i in $argv\n if contains -- $i $cmd\n return 0\n end\n end\n return 1\nend\n\nfunction __fish_openspec_no_subcommand\n set -l cmd (commandline -opc)\n test (count $cmd) -eq 1\nend";
5
+ export declare const FISH_STATIC_HELPERS = "# Helper function to match the command path at the start of the invocation\nfunction __fish_openspec_using_command_path\n set -l expected\n set -l value_flags\n set -l reading_value_flags 0\n for argument in $argv\n if test \"$argument\" = --\n set reading_value_flags 1\n continue\n end\n if test $reading_value_flags -eq 1\n set -a value_flags $argument\n else\n set -a expected $argument\n end\n end\n\n set -l tokens (commandline -opc)\n set -e tokens[1]\n set -l path\n set -l skip 0\n for token in $tokens\n if test $skip -eq 1\n set skip 0\n continue\n end\n if test \"$token\" = --no-color\n continue\n end\n if contains -- $token $value_flags\n set skip 1\n continue\n end\n if string match -q -- '-*' $token\n continue\n end\n set -a path $token\n if test (count $path) -eq (count $expected)\n break\n end\n end\n test (count $path) -eq (count $expected); or return 1\n for index in (seq (count $expected))\n test \"$path[$index]\" = \"$expected[$index]\"; or return 1\n end\n return 0\nend\n\nfunction __fish_openspec_no_subcommand\n set -l tokens (commandline -opc)\n set -e tokens[1]\n for token in $tokens\n if test \"$token\" != --no-color\n return 1\n end\n end\n return 0\nend\n\nfunction __fish_openspec_completing_option_value\n set -l current (commandline -ct)\n for option in $argv\n if string match -q -- \"$option=*\" \"$current\"\n return 0\n end\n end\n\n set -l tokens (commandline -opc)\n test (count $tokens) -gt 0; or return 1\n contains -- $tokens[-1] $argv\nend\n\nfunction __fish_openspec_complete_attached_short_path\n set -l option $argv[1]\n set -l current (commandline -ct)\n test \"$current\" != \"$option\"; or return 1\n string match -q -- \"$option*\" \"$current\"; or return 1\n set -l value (string sub -s (math (string length -- \"$option\") + 1) -- \"$current\")\n __fish_complete_path \"$value\"\nend\n\nfunction __fish_openspec_positional_index\n set -l target $argv[1]\n set -l depth $argv[2]\n set -l value_flags $argv[3..]\n set -l tokens (commandline -opc)\n set -e tokens[1]\n set -l count 0\n set -l options 1\n set -l skip 0\n for token in $tokens\n if test $skip -eq 1\n set skip 0\n continue\n end\n if test $options -eq 1\n if test \"$token\" = --\n set options 0\n continue\n end\n if contains -- $token $value_flags\n set skip 1\n continue\n end\n if string match -q -- '-*' $token\n continue\n end\n end\n set count (math $count + 1)\n end\n test $skip -eq 0; or return 1\n test $count -eq (math $target + $depth)\nend";
6
6
  export declare const FISH_DYNAMIC_HELPERS = "# Dynamic completion helpers\n\nfunction __fish_openspec_changes\n openspec __complete changes 2>/dev/null | while read -l id desc\n printf '%s\\t%s\\n' \"$id\" \"$desc\"\n end\nend\n\nfunction __fish_openspec_specs\n openspec __complete specs 2>/dev/null | while read -l id desc\n printf '%s\\t%s\\n' \"$id\" \"$desc\"\n end\nend\n\nfunction __fish_openspec_items\n __fish_openspec_changes\n __fish_openspec_specs\nend\n\nfunction __fish_openspec_schemas\n openspec __complete schemas 2>/dev/null | while read -l id desc\n printf '%s\\t%s\\n' \"$id\" \"$desc\"\n end\nend";
7
7
  //# sourceMappingURL=fish-templates.d.ts.map
@@ -2,21 +2,118 @@
2
2
  * Static template strings for Fish completion scripts.
3
3
  * These are Fish-specific helper functions that never change.
4
4
  */
5
- export const FISH_STATIC_HELPERS = `# Helper function to check if a subcommand is present
6
- function __fish_openspec_using_subcommand
7
- set -l cmd (commandline -opc)
8
- set -e cmd[1]
9
- for i in $argv
10
- if contains -- $i $cmd
11
- return 0
5
+ export const FISH_STATIC_HELPERS = `# Helper function to match the command path at the start of the invocation
6
+ function __fish_openspec_using_command_path
7
+ set -l expected
8
+ set -l value_flags
9
+ set -l reading_value_flags 0
10
+ for argument in $argv
11
+ if test "$argument" = --
12
+ set reading_value_flags 1
13
+ continue
14
+ end
15
+ if test $reading_value_flags -eq 1
16
+ set -a value_flags $argument
17
+ else
18
+ set -a expected $argument
12
19
  end
13
20
  end
14
- return 1
21
+
22
+ set -l tokens (commandline -opc)
23
+ set -e tokens[1]
24
+ set -l path
25
+ set -l skip 0
26
+ for token in $tokens
27
+ if test $skip -eq 1
28
+ set skip 0
29
+ continue
30
+ end
31
+ if test "$token" = --no-color
32
+ continue
33
+ end
34
+ if contains -- $token $value_flags
35
+ set skip 1
36
+ continue
37
+ end
38
+ if string match -q -- '-*' $token
39
+ continue
40
+ end
41
+ set -a path $token
42
+ if test (count $path) -eq (count $expected)
43
+ break
44
+ end
45
+ end
46
+ test (count $path) -eq (count $expected); or return 1
47
+ for index in (seq (count $expected))
48
+ test "$path[$index]" = "$expected[$index]"; or return 1
49
+ end
50
+ return 0
15
51
  end
16
52
 
17
53
  function __fish_openspec_no_subcommand
18
- set -l cmd (commandline -opc)
19
- test (count $cmd) -eq 1
54
+ set -l tokens (commandline -opc)
55
+ set -e tokens[1]
56
+ for token in $tokens
57
+ if test "$token" != --no-color
58
+ return 1
59
+ end
60
+ end
61
+ return 0
62
+ end
63
+
64
+ function __fish_openspec_completing_option_value
65
+ set -l current (commandline -ct)
66
+ for option in $argv
67
+ if string match -q -- "$option=*" "$current"
68
+ return 0
69
+ end
70
+ end
71
+
72
+ set -l tokens (commandline -opc)
73
+ test (count $tokens) -gt 0; or return 1
74
+ contains -- $tokens[-1] $argv
75
+ end
76
+
77
+ function __fish_openspec_complete_attached_short_path
78
+ set -l option $argv[1]
79
+ set -l current (commandline -ct)
80
+ test "$current" != "$option"; or return 1
81
+ string match -q -- "$option*" "$current"; or return 1
82
+ set -l value (string sub -s (math (string length -- "$option") + 1) -- "$current")
83
+ __fish_complete_path "$value"
84
+ end
85
+
86
+ function __fish_openspec_positional_index
87
+ set -l target $argv[1]
88
+ set -l depth $argv[2]
89
+ set -l value_flags $argv[3..]
90
+ set -l tokens (commandline -opc)
91
+ set -e tokens[1]
92
+ set -l count 0
93
+ set -l options 1
94
+ set -l skip 0
95
+ for token in $tokens
96
+ if test $skip -eq 1
97
+ set skip 0
98
+ continue
99
+ end
100
+ if test $options -eq 1
101
+ if test "$token" = --
102
+ set options 0
103
+ continue
104
+ end
105
+ if contains -- $token $value_flags
106
+ set skip 1
107
+ continue
108
+ end
109
+ if string match -q -- '-*' $token
110
+ continue
111
+ end
112
+ end
113
+ set count (math $count + 1)
114
+ end
115
+ test $skip -eq 0; or return 1
116
+ test $count -eq (math $target + $depth)
20
117
  end`;
21
118
  export const FISH_DYNAMIC_HELPERS = `# Dynamic completion helpers
22
119
 
@@ -19,6 +19,10 @@ export interface FlagDefinition {
19
19
  * Whether the flag takes an argument value
20
20
  */
21
21
  takesValue?: boolean;
22
+ /**
23
+ * Completion type for the flag value.
24
+ */
25
+ completionType?: PositionalType;
22
26
  /**
23
27
  * Possible values for the flag (for completion suggestions)
24
28
  */
@@ -19,7 +19,12 @@ export const OPENSPEC_MARKERS = {
19
19
  };
20
20
  export const AI_TOOLS = [
21
21
  { name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer', skillsDir: '.amazonq', requiresIdeRestart: true },
22
- { name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity', skillsDir: '.agent', requiresIdeRestart: true },
22
+ // Antigravity moved workspace skills and workflows from `.agent` to the
23
+ // shared `.agents` root in v1.20.5. Detection keys off `.agent` and
24
+ // `.agents/workflows` rather than the bare `.agents` root: that root is
25
+ // shared with Codex, Zed, and the vendor-neutral target, so its presence
26
+ // alone says nothing about Antigravity.
27
+ { name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity', skillsDir: '.agents', legacySkillsDirs: ['.agent'], detectionPaths: ['.agent', '.agents/workflows'], requiresIdeRestart: true },
23
28
  { name: 'Auggie (Augment CLI)', value: 'auggie', available: true, successLabel: 'Auggie', skillsDir: '.augment' },
24
29
  { name: 'Bob Shell', value: 'bob', available: true, successLabel: 'Bob Shell', skillsDir: '.bob' },
25
30
  { name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code', skillsDir: '.claude' },
@@ -49,6 +54,7 @@ export const AI_TOOLS = [
49
54
  { name: 'Oh My Pi', value: 'oh-my-pi', available: true, successLabel: 'Oh My Pi', skillsDir: '.omp' },
50
55
  { name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' },
51
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' },
52
58
  { name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder', requiresIdeRestart: true },
53
59
  { name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' },
54
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,11 +20,11 @@ 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';
26
- import { writeSharedSkillTarget } from './shared-skill-target.js';
27
+ import { resolveSharedSkillWriters, sharedSkillRootOwner, writeSharedSkillTarget, } from './shared-skill-target.js';
27
28
  import { migrateIfNeeded, migrateLegacyToolDirs, describeLegacyMigration, keptInPlaceNotice, hasMovableContent, scanInstalledWorkflows as scanInstalledWorkflowsShared } from './migration.js';
28
29
  import { resolveCommandSurfaceCapability, resolveCommandInvocation, shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
29
30
  import { writeCopilotCloudFiles, readCopilotCloudOptIn, hasExistingManagedCloudFiles, persistCopilotCloudOptIn, removeCopilotCloudFiles, findUnmanagedCloudFiles, listManagedCloudFiles, } from './github-copilot/cloud-agent.js';
@@ -526,34 +527,8 @@ export class InitCommand {
526
527
  return deduped;
527
528
  }
528
529
  validateTools(toolIds, toolStates, projectPath) {
529
- const validatedTools = [];
530
- const sharedAgentsTargets = ['codex', 'zed', 'agents'];
531
- const selectedSharedTargets = sharedAgentsTargets.filter((toolId) => toolIds.includes(toolId));
532
- // A Codex-rendered tree already serves Zed. Keep it when Zed is added later
533
- // so Codex users do not lose the `$openspec-*` references they require.
534
- const preserveConfiguredCodex = selectedSharedTargets.includes('zed') &&
535
- toolStates.get('codex')?.configured;
536
- const sharedTargetCandidates = preserveConfiguredCodex
537
- ? [...new Set([...selectedSharedTargets, 'codex'])]
538
- : selectedSharedTargets;
539
- const sharedTargetOwner = sharedTargetCandidates.includes('codex')
540
- ? 'codex'
541
- : selectedSharedTargets.includes('zed')
542
- ? 'zed'
543
- : selectedSharedTargets[0];
544
- const firstSharedIndex = toolIds.findIndex((id) => sharedAgentsTargets.includes(id));
545
- const reconciledToolIds = sharedTargetCandidates.length > 1
546
- ? toolIds.flatMap((toolId, index) => {
547
- if (!sharedAgentsTargets.includes(toolId))
548
- return [toolId];
549
- return index === firstSharedIndex && sharedTargetOwner ? [sharedTargetOwner] : [];
550
- })
551
- : toolIds;
552
- if (reconciledToolIds.length !== toolIds.length ||
553
- reconciledToolIds.some((toolId, index) => toolId !== toolIds[index])) {
554
- console.log(chalk.dim(`Codex, Zed, and agents share .agents/skills; writing one tree for ${sharedTargetOwner}.`));
555
- }
556
- for (const toolId of reconciledToolIds) {
530
+ const selectedTools = [];
531
+ for (const toolId of toolIds) {
557
532
  const tool = AI_TOOLS.find((t) => t.value === toolId);
558
533
  if (!tool) {
559
534
  const validToolIds = getToolsWithSkillsDir();
@@ -563,6 +538,48 @@ export class InitCommand {
563
538
  const validToolsWithSkills = getToolsWithSkillsDir();
564
539
  throw new Error(`Tool '${toolId}' does not support skill generation.\nTools with skill generation support:\n ${validToolsWithSkills.join('\n ')}`);
565
540
  }
541
+ selectedTools.push(tool);
542
+ }
543
+ // A selected tool may share its physical skills root with an already
544
+ // configured owner. Include that owner in the refresh without dropping the
545
+ // selected tool: it may still have an independent command surface.
546
+ const generationTools = [...selectedTools];
547
+ const delivery = getGlobalConfig().delivery ?? 'both';
548
+ for (const selected of selectedTools) {
549
+ if (!selected.skillsDir)
550
+ continue;
551
+ const selectedOwner = selected.value === 'codex' ||
552
+ !shouldGenerateSkillsForTool(selected.value, delivery)
553
+ ? undefined
554
+ : sharedSkillRootOwner(projectPath, selected.value);
555
+ for (const candidate of AI_TOOLS) {
556
+ if (candidate.skillsDir === selected.skillsDir &&
557
+ toolStates.get(candidate.value)?.configured &&
558
+ candidate.value === selectedOwner &&
559
+ !generationTools.includes(candidate)) {
560
+ generationTools.push(candidate);
561
+ }
562
+ }
563
+ }
564
+ const skillWriters = resolveSharedSkillWriters(projectPath, generationTools);
565
+ const sharedRoots = new Map();
566
+ for (const tool of generationTools) {
567
+ if (!tool.skillsDir)
568
+ continue;
569
+ const group = sharedRoots.get(tool.skillsDir) ?? [];
570
+ group.push(tool);
571
+ sharedRoots.set(tool.skillsDir, group);
572
+ }
573
+ for (const [root, group] of sharedRoots) {
574
+ if (group.length < 2)
575
+ continue;
576
+ const owner = group.find((tool) => skillWriters.has(tool.value));
577
+ console.log(chalk.dim(`${group.map((tool) => tool.name).join(', ')} share ${root}/skills; writing one tree for ${owner?.value}.`));
578
+ }
579
+ const validatedTools = [];
580
+ for (const tool of generationTools) {
581
+ if (!toolSupportsSkills(tool))
582
+ continue;
566
583
  const preState = toolStates.get(tool.value);
567
584
  const skillsPath = resolveToolSkillsDir(projectPath, tool);
568
585
  const isGlobalSkillTarget = hasGlobalSkillTarget(tool);
@@ -574,7 +591,7 @@ export class InitCommand {
574
591
  skillsRoot: isGlobalSkillTarget ? skillsPath : projectPath,
575
592
  isGlobalSkillTarget,
576
593
  wasConfigured: preState?.configured ?? false,
577
- requiresIdeRestart: tool.requiresIdeRestart,
594
+ writesSkills: !tool.skillsDir || skillWriters.has(tool.value),
578
595
  });
579
596
  }
580
597
  return validatedTools;
@@ -583,36 +600,37 @@ export class InitCommand {
583
600
  // DIRECTORY STRUCTURE
584
601
  // ═══════════════════════════════════════════════════════════
585
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
+ ];
586
609
  if (extendMode) {
587
610
  // In extend mode, just ensure directories exist without spinner
588
- const directories = [
589
- openspecPath,
590
- path.join(openspecPath, 'specs'),
591
- path.join(openspecPath, 'changes'),
592
- path.join(openspecPath, 'changes', 'archive'),
593
- ];
594
611
  for (const dir of directories) {
595
612
  FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), dir);
596
613
  await FileSystemUtils.createDirectory(dir);
597
614
  }
615
+ await this.writeGitkeepFiles(openspecPath);
598
616
  return;
599
617
  }
600
618
  const spinner = this.startSpinner('Creating OpenSpec structure...');
601
- const directories = [
602
- openspecPath,
603
- path.join(openspecPath, 'specs'),
604
- path.join(openspecPath, 'changes'),
605
- path.join(openspecPath, 'changes', 'archive'),
606
- ];
607
619
  for (const dir of directories) {
608
620
  FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), dir);
609
621
  await FileSystemUtils.createDirectory(dir);
610
622
  }
623
+ await this.writeGitkeepFiles(openspecPath);
611
624
  spinner.stopAndPersist({
612
625
  symbol: PALETTE.white('▌'),
613
626
  text: PALETTE.white('OpenSpec structure created'),
614
627
  });
615
628
  }
629
+ async writeGitkeepFiles(openspecPath) {
630
+ for (const relativeDir of ANCHORED_OPENSPEC_DIRS) {
631
+ await ensureDirectoryAnchor(path.dirname(openspecPath), relativeDir);
632
+ }
633
+ }
616
634
  // ═══════════════════════════════════════════════════════════
617
635
  // SKILL & COMMAND GENERATION
618
636
  // ═══════════════════════════════════════════════════════════
@@ -648,7 +666,7 @@ export class InitCommand {
648
666
  const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
649
667
  const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
650
668
  // Generate skill files if the selected delivery and tool capability allow skills
651
- if (shouldGenerateSkills) {
669
+ if (shouldGenerateSkills && tool.writesSkills) {
652
670
  // Create skill directories and SKILL.md files
653
671
  for (const { template, dirName } of skillTemplates) {
654
672
  const skillDir = path.join(tool.skillsPath, dirName);
@@ -662,7 +680,9 @@ export class InitCommand {
662
680
  }
663
681
  writeSharedSkillTarget(projectPath, tool.value);
664
682
  }
665
- if (shouldRemoveSkillsForTool(tool.value, delivery) && !tool.isGlobalSkillTarget) {
683
+ if (shouldRemoveSkillsForTool(tool.value, delivery) &&
684
+ tool.writesSkills &&
685
+ !tool.isGlobalSkillTarget) {
666
686
  removedSkillCount += await this.removeSkillDirs(tool.skillsRoot, tool.skillsPath);
667
687
  // Retain an explicit selection even when this delivery mode produces
668
688
  // no skills, so a divergent legacy sibling cannot reclaim ownership.
@@ -1015,27 +1035,13 @@ export class InitCommand {
1015
1035
  console.log();
1016
1036
  console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
1017
1037
  console.log(`Feedback: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec/issues')}`);
1018
- // Restart instruction only when at least one IDE/editor-resident tool
1019
- // actually received a generated surface. Two conditions, coupled to the SAME
1020
- // tool: (1) its commands/skills are loaded by a long-running editor process
1021
- // (CLI tools pick the files up immediately, so a restart line would be wrong
1022
- // for them — see #1067), and (2) a surface was actually generated for it
1023
- // under the active delivery (an IDE tool that generated nothing has nothing a
1024
- // restart would pick up, even if a co-configured CLI tool did generate).
1025
- // Wording follows what the IDE tool itself generated, not the global
1026
- // aggregate: it must not say "commands" when the IDE tool only got skills
1027
- // while a co-configured CLI tool got commands. Not "slash commands" either:
1028
- // Amazon Q's generated files are prompt-library entries invoked with @, so a
1029
- // restart line promising slash commands would be wrong for it.
1030
- const restartCommandsGenerated = successfulTools.some((tool) => tool.requiresIdeRestart &&
1031
- shouldGenerateCommandsForTool(tool.value, activeDelivery));
1032
- const restartSkillsGenerated = successfulTools.some((tool) => tool.requiresIdeRestart &&
1033
- shouldGenerateSkillsForTool(tool.value, activeDelivery));
1034
- 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) {
1035
1043
  console.log();
1036
- console.log(chalk.white(restartCommandsGenerated
1037
- ? 'Restart your IDE for the new commands to take effect.'
1038
- : 'Restart your IDE for the new skills to take effect.'));
1044
+ console.log(chalk.white(restartHint));
1039
1045
  }
1040
1046
  console.log();
1041
1047
  }