@fission-ai/openspec 1.13.2 → 1.14.1

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 (99) hide show
  1. package/dist/cli/commands/config.d.ts +8 -0
  2. package/dist/cli/commands/config.js +88 -0
  3. package/dist/cli/commands/context.d.ts +3 -0
  4. package/dist/cli/commands/context.js +20 -0
  5. package/dist/cli/commands/doctor.d.ts +3 -0
  6. package/dist/cli/commands/doctor.js +18 -0
  7. package/dist/cli/commands/schema.d.ts +6 -0
  8. package/dist/cli/commands/schema.js +57 -0
  9. package/dist/cli/commands/spec.d.ts +3 -0
  10. package/dist/cli/commands/spec.js +42 -0
  11. package/dist/cli/commands/store.d.ts +3 -0
  12. package/dist/cli/commands/store.js +130 -0
  13. package/dist/cli/commands/workset.d.ts +3 -0
  14. package/dist/cli/commands/workset.js +96 -0
  15. package/dist/cli/index.js +106 -43
  16. package/dist/commands/config.d.ts +16 -7
  17. package/dist/commands/config.js +342 -395
  18. package/dist/commands/context.d.ts +8 -2
  19. package/dist/commands/context.js +22 -37
  20. package/dist/commands/doctor.d.ts +6 -7
  21. package/dist/commands/doctor.js +14 -27
  22. package/dist/commands/schema.d.ts +19 -5
  23. package/dist/commands/schema.js +662 -702
  24. package/dist/commands/spec.d.ts +13 -4
  25. package/dist/commands/spec.js +92 -120
  26. package/dist/commands/store.d.ts +27 -2
  27. package/dist/commands/store.js +1 -120
  28. package/dist/commands/workflow/default-schema.d.ts +2 -0
  29. package/dist/commands/workflow/default-schema.js +4 -0
  30. package/dist/commands/workflow/instructions.js +32 -12
  31. package/dist/commands/workflow/shared.d.ts +3 -1
  32. package/dist/commands/workflow/shared.js +1 -1
  33. package/dist/commands/workflow/status.js +15 -2
  34. package/dist/commands/workset.d.ts +22 -2
  35. package/dist/commands/workset.js +1 -88
  36. package/dist/core/archive.js +28 -7
  37. package/dist/core/artifact-graph/instruction-loader.d.ts +7 -0
  38. package/dist/core/artifact-graph/instruction-loader.js +18 -1
  39. package/dist/core/change-metadata/schema.d.ts +3 -0
  40. package/dist/core/change-metadata/schema.js +10 -0
  41. package/dist/core/change-status-policy.d.ts +8 -0
  42. package/dist/core/change-status-policy.js +33 -2
  43. package/dist/core/command-generation/adapters/atomcode.d.ts +24 -0
  44. package/dist/core/command-generation/adapters/atomcode.js +46 -0
  45. package/dist/core/command-generation/adapters/codestudio.d.ts +13 -0
  46. package/dist/core/command-generation/adapters/codestudio.js +27 -0
  47. package/dist/core/command-generation/adapters/easycode.d.ts +17 -0
  48. package/dist/core/command-generation/adapters/easycode.js +28 -0
  49. package/dist/core/command-generation/adapters/gemini.js +1 -35
  50. package/dist/core/command-generation/adapters/gigacode.d.ts +17 -0
  51. package/dist/core/command-generation/adapters/gigacode.js +31 -0
  52. package/dist/core/command-generation/adapters/index.d.ts +4 -0
  53. package/dist/core/command-generation/adapters/index.js +4 -0
  54. package/dist/core/command-generation/registry.js +8 -0
  55. package/dist/core/command-generation/toml.d.ts +19 -0
  56. package/dist/core/command-generation/toml.js +39 -0
  57. package/dist/core/command-surface.js +1 -1
  58. package/dist/core/completion-tip.js +3 -1
  59. package/dist/core/completions/command-registry.js +19 -0
  60. package/dist/core/completions/installers/zsh-installer.js +4 -2
  61. package/dist/core/config-prompts.js +6 -6
  62. package/dist/core/config-schema.d.ts +1 -1
  63. package/dist/core/config.js +11 -1
  64. package/dist/core/init.js +35 -11
  65. package/dist/core/legacy-cleanup.js +11 -7
  66. package/dist/core/list.d.ts +2 -0
  67. package/dist/core/list.js +49 -23
  68. package/dist/core/parsers/markdown-parser.js +5 -0
  69. package/dist/core/parsers/requirement-blocks.d.ts +6 -0
  70. package/dist/core/parsers/requirement-blocks.js +9 -2
  71. package/dist/core/project-config.js +45 -1
  72. package/dist/core/references.js +3 -1
  73. package/dist/core/root-selection.d.ts +6 -0
  74. package/dist/core/root-selection.js +16 -0
  75. package/dist/core/schemas/base.schema.d.ts +3 -0
  76. package/dist/core/schemas/base.schema.js +6 -0
  77. package/dist/core/schemas/change.schema.d.ts +8 -0
  78. package/dist/core/schemas/spec.schema.d.ts +2 -0
  79. package/dist/core/templates/workflows/apply-change.js +9 -5
  80. package/dist/core/templates/workflows/archive-change.js +26 -4
  81. package/dist/core/templates/workflows/bulk-archive-change.js +6 -2
  82. package/dist/core/templates/workflows/continue-change.js +17 -0
  83. package/dist/core/templates/workflows/verify-change.js +12 -12
  84. package/dist/core/validation/constants.d.ts +1 -1
  85. package/dist/core/validation/constants.js +1 -1
  86. package/dist/core/validation/validator.js +21 -2
  87. package/dist/core/version-check.d.ts +23 -1
  88. package/dist/core/version-check.js +112 -26
  89. package/dist/core/view.d.ts +2 -0
  90. package/dist/core/view.js +36 -2
  91. package/dist/utils/change-metadata.d.ts +20 -1
  92. package/dist/utils/change-metadata.js +57 -1
  93. package/dist/utils/command-references.d.ts +1 -1
  94. package/dist/utils/command-references.js +2 -2
  95. package/dist/utils/index.d.ts +1 -1
  96. package/dist/utils/index.js +1 -1
  97. package/package.json +1 -1
  98. package/schemas/spec-driven/schema.yaml +29 -4
  99. package/schemas/spec-driven/templates/proposal.md +6 -3
@@ -5,20 +5,24 @@
5
5
  */
6
6
  export { amazonQAdapter } from './amazon-q.js';
7
7
  export { antigravityAdapter } from './antigravity.js';
8
+ export { atomcodeAdapter } from './atomcode.js';
8
9
  export { auggieAdapter } from './auggie.js';
9
10
  export { bobAdapter } from './bob.js';
10
11
  export { claudeAdapter } from './claude.js';
11
12
  export { clineAdapter } from './cline.js';
12
13
  export { commandCodeAdapter } from './command-code.js';
13
14
  export { codebuddyAdapter } from './codebuddy.js';
15
+ export { codeStudioAdapter } from './codestudio.js';
14
16
  export { continueAdapter } from './continue.js';
15
17
  export { costrictAdapter } from './costrict.js';
16
18
  export { crushAdapter } from './crush.js';
17
19
  export { cursorAdapter } from './cursor.js';
18
20
  export { devinAdapter } from './devin.js';
21
+ export { easycodeAdapter } from './easycode.js';
19
22
  export { factoryAdapter } from './factory.js';
20
23
  export { geminiAdapter } from './gemini.js';
21
24
  export { githubCopilotAdapter } from './github-copilot.js';
25
+ export { gigacodeAdapter } from './gigacode.js';
22
26
  export { iflowAdapter } from './iflow.js';
23
27
  export { junieAdapter } from './junie.js';
24
28
  export { kilocodeAdapter } from './kilocode.js';
@@ -5,20 +5,24 @@
5
5
  */
6
6
  export { amazonQAdapter } from './amazon-q.js';
7
7
  export { antigravityAdapter } from './antigravity.js';
8
+ export { atomcodeAdapter } from './atomcode.js';
8
9
  export { auggieAdapter } from './auggie.js';
9
10
  export { bobAdapter } from './bob.js';
10
11
  export { claudeAdapter } from './claude.js';
11
12
  export { clineAdapter } from './cline.js';
12
13
  export { commandCodeAdapter } from './command-code.js';
13
14
  export { codebuddyAdapter } from './codebuddy.js';
15
+ export { codeStudioAdapter } from './codestudio.js';
14
16
  export { continueAdapter } from './continue.js';
15
17
  export { costrictAdapter } from './costrict.js';
16
18
  export { crushAdapter } from './crush.js';
17
19
  export { cursorAdapter } from './cursor.js';
18
20
  export { devinAdapter } from './devin.js';
21
+ export { easycodeAdapter } from './easycode.js';
19
22
  export { factoryAdapter } from './factory.js';
20
23
  export { geminiAdapter } from './gemini.js';
21
24
  export { githubCopilotAdapter } from './github-copilot.js';
25
+ export { gigacodeAdapter } from './gigacode.js';
22
26
  export { iflowAdapter } from './iflow.js';
23
27
  export { junieAdapter } from './junie.js';
24
28
  export { kilocodeAdapter } from './kilocode.js';
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { amazonQAdapter } from './adapters/amazon-q.js';
8
8
  import { antigravityAdapter } from './adapters/antigravity.js';
9
+ import { atomcodeAdapter } from './adapters/atomcode.js';
9
10
  import { auggieAdapter } from './adapters/auggie.js';
10
11
  import { bobAdapter } from './adapters/bob.js';
11
12
  import { claudeAdapter } from './adapters/claude.js';
@@ -13,13 +14,16 @@ import { clineAdapter } from './adapters/cline.js';
13
14
  import { commandCodeAdapter } from './adapters/command-code.js';
14
15
  import { devinAdapter } from './adapters/devin.js';
15
16
  import { codebuddyAdapter } from './adapters/codebuddy.js';
17
+ import { codeStudioAdapter } from './adapters/codestudio.js';
16
18
  import { continueAdapter } from './adapters/continue.js';
17
19
  import { costrictAdapter } from './adapters/costrict.js';
18
20
  import { crushAdapter } from './adapters/crush.js';
19
21
  import { cursorAdapter } from './adapters/cursor.js';
22
+ import { easycodeAdapter } from './adapters/easycode.js';
20
23
  import { factoryAdapter } from './adapters/factory.js';
21
24
  import { geminiAdapter } from './adapters/gemini.js';
22
25
  import { githubCopilotAdapter } from './adapters/github-copilot.js';
26
+ import { gigacodeAdapter } from './adapters/gigacode.js';
23
27
  import { iflowAdapter } from './adapters/iflow.js';
24
28
  import { junieAdapter } from './adapters/junie.js';
25
29
  import { kilocodeAdapter } from './adapters/kilocode.js';
@@ -43,6 +47,7 @@ export class CommandAdapterRegistry {
43
47
  static {
44
48
  CommandAdapterRegistry.register(amazonQAdapter);
45
49
  CommandAdapterRegistry.register(antigravityAdapter);
50
+ CommandAdapterRegistry.register(atomcodeAdapter);
46
51
  CommandAdapterRegistry.register(auggieAdapter);
47
52
  CommandAdapterRegistry.register(bobAdapter);
48
53
  CommandAdapterRegistry.register(claudeAdapter);
@@ -50,13 +55,16 @@ export class CommandAdapterRegistry {
50
55
  CommandAdapterRegistry.register(commandCodeAdapter);
51
56
  CommandAdapterRegistry.register(devinAdapter);
52
57
  CommandAdapterRegistry.register(codebuddyAdapter);
58
+ CommandAdapterRegistry.register(codeStudioAdapter);
53
59
  CommandAdapterRegistry.register(continueAdapter);
54
60
  CommandAdapterRegistry.register(costrictAdapter);
55
61
  CommandAdapterRegistry.register(crushAdapter);
56
62
  CommandAdapterRegistry.register(cursorAdapter);
63
+ CommandAdapterRegistry.register(easycodeAdapter);
57
64
  CommandAdapterRegistry.register(factoryAdapter);
58
65
  CommandAdapterRegistry.register(geminiAdapter);
59
66
  CommandAdapterRegistry.register(githubCopilotAdapter);
67
+ CommandAdapterRegistry.register(gigacodeAdapter);
60
68
  CommandAdapterRegistry.register(iflowAdapter);
61
69
  CommandAdapterRegistry.register(junieAdapter);
62
70
  CommandAdapterRegistry.register(kilocodeAdapter);
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared TOML string escaping for command adapters.
3
+ */
4
+ /**
5
+ * TOML basic strings are escape-active: a backslash or double quote in the
6
+ * value breaks the file if written raw. Newlines cannot appear in a
7
+ * single-line basic string at all, so they are escaped too.
8
+ */
9
+ export declare function escapeTomlBasicString(value: string): string;
10
+ /**
11
+ * Multiline basic strings keep raw newlines and tabs, but backslashes are
12
+ * still escape-active, any run of three quotes would end the string, and the
13
+ * same control characters are invalid as in single-line basic strings — a
14
+ * lone carriage return included (only LF and CRLF may appear raw; CRLF is
15
+ * normalized away so the emitted file is single-convention). Escapes are
16
+ * introduced after backslash-doubling so they are not re-doubled.
17
+ */
18
+ export declare function escapeTomlMultilineBasicString(value: string): string;
19
+ //# sourceMappingURL=toml.d.ts.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Shared TOML string escaping for command adapters.
3
+ */
4
+ /**
5
+ * Control characters (C0 except tab/newline/carriage return, plus DEL) are
6
+ * invalid inside TOML strings and must be written as escapes.
7
+ */
8
+ const TOML_CONTROL_CHARS = new RegExp('[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]', 'g');
9
+ /**
10
+ * TOML basic strings are escape-active: a backslash or double quote in the
11
+ * value breaks the file if written raw. Newlines cannot appear in a
12
+ * single-line basic string at all, so they are escaped too.
13
+ */
14
+ export function escapeTomlBasicString(value) {
15
+ return value
16
+ .replace(/\\/g, '\\\\')
17
+ .replace(/"/g, '\\"')
18
+ .replace(/\n/g, '\\n')
19
+ .replace(/\r/g, '\\r')
20
+ .replace(/\t/g, '\\t')
21
+ .replace(TOML_CONTROL_CHARS, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
22
+ }
23
+ /**
24
+ * Multiline basic strings keep raw newlines and tabs, but backslashes are
25
+ * still escape-active, any run of three quotes would end the string, and the
26
+ * same control characters are invalid as in single-line basic strings — a
27
+ * lone carriage return included (only LF and CRLF may appear raw; CRLF is
28
+ * normalized away so the emitted file is single-convention). Escapes are
29
+ * introduced after backslash-doubling so they are not re-doubled.
30
+ */
31
+ export function escapeTomlMultilineBasicString(value) {
32
+ return value
33
+ .replace(/\r\n/g, '\n')
34
+ .replace(/\\/g, '\\\\')
35
+ .replace(/"""/g, '""\\"')
36
+ .replace(/\r/g, '\\r')
37
+ .replace(TOML_CONTROL_CHARS, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
38
+ }
39
+ //# sourceMappingURL=toml.js.map
@@ -13,7 +13,7 @@ export function resolveCommandSurfaceCapability(toolId) {
13
13
  if (CommandAdapterRegistry.has(toolId)) {
14
14
  return 'adapter-backed';
15
15
  }
16
- if (toolId === 'codex') {
16
+ if (toolId === 'codex' || toolId === 'warp') {
17
17
  return 'skills-invocable';
18
18
  }
19
19
  return 'none';
@@ -22,7 +22,6 @@ import { getGlobalConfigPath } from './global-config.js';
22
22
  import { writeFileAtomically } from './file-state.js';
23
23
  import { isCiEnvironment } from '../utils/ci.js';
24
24
  import { detectShell } from '../utils/shell-detection.js';
25
- import { CompletionFactory } from './completions/factory.js';
26
25
  export const COMPLETION_TIP_MESSAGE = "Tip: Run 'openspec completion install' for shell completions";
27
26
  function isSuppressedByEnv() {
28
27
  // isCiEnvironment, not a CI==='true' string check: providers set CI to "True",
@@ -54,6 +53,9 @@ async function decideTip() {
54
53
  if (!shell) {
55
54
  return 'retire';
56
55
  }
56
+ // Loaded only here: every shell's generator and installer, needed only on
57
+ // the rare run that still owes the tip.
58
+ const { CompletionFactory } = await import('./completions/factory.js');
57
59
  return (await CompletionFactory.createInstaller(shell).isInstalled())
58
60
  ? 'retire'
59
61
  : 'show';
@@ -54,6 +54,17 @@ export const COMMAND_REGISTRY = [
54
54
  },
55
55
  ],
56
56
  },
57
+ {
58
+ name: 'version',
59
+ description: 'Report the installed OpenSpec version and update availability',
60
+ flags: [
61
+ COMMON_FLAGS.json,
62
+ {
63
+ name: 'check',
64
+ description: 'Check the registry for a newer version',
65
+ },
66
+ ],
67
+ },
57
68
  {
58
69
  name: 'list',
59
70
  description: 'List items (changes by default, or specs with --specs)',
@@ -66,6 +77,14 @@ export const COMMAND_REGISTRY = [
66
77
  name: 'changes',
67
78
  description: 'List changes explicitly (default)',
68
79
  },
80
+ {
81
+ name: 'archived',
82
+ description: 'Show only archived changes',
83
+ },
84
+ {
85
+ name: 'all',
86
+ description: 'Show both active and archived changes',
87
+ },
69
88
  {
70
89
  name: 'sort',
71
90
  description: 'Sort order: "recent" (default) or "name"',
@@ -194,8 +194,10 @@ export class ZshInstaller {
194
194
  }
195
195
  // Remove lines between markers (inclusive)
196
196
  lines.splice(startIndex, endIndex - startIndex + 1);
197
- // Remove trailing empty lines at the start if the markers were at the top
198
- while (lines.length > 0 && lines[0].trim() === '') {
197
+ // Install puts the block at the top of the file followed by one blank
198
+ // separator line; drop that line too so the file reads as it did before.
199
+ // Everything else, including blank lines the user had at the top, is left as is.
200
+ if (startIndex === 0 && lines.length > 0 && lines[0].trim() === '') {
199
201
  lines.shift();
200
202
  }
201
203
  // Write back
@@ -19,13 +19,13 @@ export function serializeConfig(config) {
19
19
  else {
20
20
  // Context section with comments
21
21
  lines.push('# Project context (optional)');
22
- lines.push('# This is shown to AI when creating artifacts.');
23
- lines.push('# Add your tech stack, conventions, style guides, domain knowledge, etc.');
22
+ lines.push('# Add only constraints that should guide OpenSpec artifacts and workflows.');
23
+ lines.push('# Include constraints an agent cannot infer by reading the code.');
24
+ lines.push('# Keep general project documentation and discoverable codebase facts out.');
24
25
  lines.push('# Example:');
25
26
  lines.push('# context: |');
26
- lines.push('# Tech stack: TypeScript, React, Node.js');
27
- lines.push('# We use conventional commits');
28
- lines.push('# Domain: e-commerce platform');
27
+ lines.push('# Designs and tasks must cover Windows, macOS, and Linux');
28
+ lines.push('# Write all artifacts in Spanish');
29
29
  lines.push('');
30
30
  }
31
31
  // Rules section with comments
@@ -35,7 +35,7 @@ export function serializeConfig(config) {
35
35
  lines.push('# rules:');
36
36
  lines.push('# proposal:');
37
37
  lines.push('# - Keep proposals under 500 words');
38
- lines.push('# - Always include a "Non-goals" section');
38
+ lines.push('# - Always state what is out of scope');
39
39
  lines.push('# tasks:');
40
40
  lines.push('# - Break tasks into chunks of max 2 hours');
41
41
  lines.push('');
@@ -10,9 +10,9 @@ export declare const GlobalConfigSchema: z.ZodObject<{
10
10
  core: "core";
11
11
  }>>>;
12
12
  delivery: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
13
- commands: "commands";
14
13
  both: "both";
15
14
  skills: "skills";
15
+ commands: "commands";
16
16
  }>>>;
17
17
  workflows: z.ZodOptional<z.ZodArray<z.ZodString>>;
18
18
  defaultStore: z.ZodOptional<z.ZodString>;
@@ -19,29 +19,37 @@ 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: 'Amp', value: 'amp', available: true, successLabel: 'Amp', skillsDir: '.agents', detectionPaths: ['.amp', '.agents/skills'] },
22
23
  // Antigravity moved workspace skills and workflows from `.agent` to the
23
24
  // shared `.agents` root in v1.20.5. Detection keys off `.agent` and
24
25
  // `.agents/workflows` rather than the bare `.agents` root: that root is
25
26
  // shared with Codex, Zed, and the vendor-neutral target, so its presence
26
27
  // alone says nothing about Antigravity.
27
28
  { name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity', skillsDir: '.agents', legacySkillsDirs: ['.agent'], detectionPaths: ['.agent', '.agents/workflows'], requiresIdeRestart: true },
29
+ { name: 'AtomCode', value: 'atomcode', available: true, successLabel: 'AtomCode', skillsDir: '.atomcode' },
28
30
  { name: 'Auggie (Augment CLI)', value: 'auggie', available: true, successLabel: 'Auggie', skillsDir: '.augment' },
29
- { name: 'Bob Shell', value: 'bob', available: true, successLabel: 'Bob Shell', skillsDir: '.bob' },
31
+ { name: 'IBM Bob', value: 'bob', available: true, successLabel: 'IBM Bob', skillsDir: '.bob' },
30
32
  { name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code', skillsDir: '.claude' },
31
33
  { name: 'Cline', value: 'cline', available: true, successLabel: 'Cline', skillsDir: '.cline', requiresIdeRestart: true },
32
34
  { name: 'Command Code', value: 'command-code', available: true, successLabel: 'Command Code', skillsDir: '.commandcode' },
33
35
  { name: 'CodeArts', value: 'codeartsagent', available: true, successLabel: 'CodeArts', skillsDir: '.codeartsdoer' },
34
36
  { name: 'Codex', value: 'codex', available: true, successLabel: 'Codex', skillsDir: '.agents', legacySkillsDirs: ['.codex'], detectionPaths: ['.agents/skills', '.codex/skills'] },
37
+ { name: 'DeepSeek Harness', value: 'dsh', available: true, successLabel: 'DeepSeek Harness', skillsDir: '.dsh' },
35
38
  { name: 'Devin Desktop (formerly Windsurf)', value: 'devin', available: true, successLabel: 'Devin Desktop', skillsDir: '.devin', detectionPaths: ['.devin', '.windsurf'], requiresIdeRestart: true },
36
39
  { name: 'ForgeCode', value: 'forgecode', available: true, successLabel: 'ForgeCode', skillsDir: '.forge' },
37
40
  { name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code', skillsDir: '.codebuddy' },
41
+ { name: 'Code Studio', value: 'codestudio', available: true, successLabel: 'Code Studio', skillsDir: '.codestudio', requiresIdeRestart: true },
38
42
  { name: 'Continue', value: 'continue', available: true, successLabel: 'Continue (VS Code / JetBrains / Cli)', skillsDir: '.continue', requiresIdeRestart: true },
39
43
  { name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict', skillsDir: '.cospec', requiresIdeRestart: true },
40
44
  { name: 'Crush', value: 'crush', available: true, successLabel: 'Crush', skillsDir: '.crush' },
41
45
  { name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor', skillsDir: '.cursor', requiresIdeRestart: true },
46
+ { name: 'EasyCode', value: 'easycode', available: true, successLabel: 'EasyCode', skillsDir: '.easycode' },
42
47
  { name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid', skillsDir: '.factory' },
43
48
  { name: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI', skillsDir: '.gemini' },
44
49
  { name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github', detectionPaths: ['.github/copilot-instructions.md', '.github/instructions', '.github/workflows/copilot-setup-steps.yml', '.github/prompts', '.github/agents', '.github/skills', '.github/.mcp.json'], requiresIdeRestart: true },
50
+ { name: 'GigaCode', value: 'gigacode', available: true, successLabel: 'GigaCode', skillsDir: '.gigacode' },
51
+ { name: 'Grok Build', value: 'grok', available: true, successLabel: 'Grok Build', skillsDir: '.grok' },
52
+ { name: 'GSD', value: 'gsd', available: true, successLabel: 'GSD', skillsDir: '.agents', detectionPaths: ['.gsd'] },
45
53
  { name: 'Hermes Agent', value: 'hermes', available: true, successLabel: 'Hermes Agent', skillsDir: '.hermes', detectionPaths: ['.hermes', 'HERMES.md', '.hermes.md'], setupNote: "Hermes only loads skills from ~/.hermes/skills by default. Add this project's .hermes/skills directory to skills.external_dirs in ~/.hermes/config.yaml so Hermes picks up the generated OpenSpec skills." },
46
54
  { name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow', skillsDir: '.iflow' },
47
55
  { name: 'Junie', value: 'junie', available: true, successLabel: 'Junie', skillsDir: '.junie', requiresIdeRestart: true },
@@ -60,6 +68,8 @@ export const AI_TOOLS = [
60
68
  { name: 'Rovo Dev CLI', value: 'rovodev', available: true, successLabel: 'Rovo Dev CLI', skillsDir: '.rovodev', detectionPaths: ['.rovodev/skills', '.rovodev'] },
61
69
  { name: 'Zoo Code', value: 'roocode', available: true, successLabel: 'Zoo Code', skillsDir: '.roo', requiresIdeRestart: true },
62
70
  { name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae', requiresIdeRestart: true },
71
+ { name: 'Veai', value: 'veai', available: true, successLabel: 'Veai', skillsDir: '.veai' },
72
+ { name: 'Warp', value: 'warp', available: true, successLabel: 'Warp', skillsDir: '.warp', detectionPaths: ['.warp', 'WARP.md'] },
63
73
  { name: 'Zed Agent', value: 'zed', available: true, successLabel: 'Zed Agent', skillsDir: '.agents', detectionPaths: ['.zed', '.agents/skills'] },
64
74
  { name: 'ZCode', value: 'zcode', available: true, successLabel: 'ZCode', skillsDir: '.zcode' },
65
75
  // Vendor-neutral target for assistants that read the shared `.agents` root.
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 { resolveOpenSpecRoot } from './root-selection.js';
15
16
  import { ANCHORED_OPENSPEC_DIRS, ensureDirectoryAnchor } from './openspec-root.js';
16
17
  import { getSkillReferenceTransformer, getTransformerForTool, usesNaturalLanguageSkillReferences } from '../utils/command-references.js';
17
18
  import { AI_TOOLS, getUniversalTool, universalToolFallbackHint, OPENSPEC_DIR_NAME, resolveToolIdAlias, } from './config.js';
@@ -81,7 +82,7 @@ export class InitCommand {
81
82
  this.copilotCloudOption = options.copilotCloud;
82
83
  }
83
84
  async execute(targetPath) {
84
- const projectPath = path.resolve(targetPath);
85
+ const projectPath = FileSystemUtils.canonicalizeExistingPath(targetPath);
85
86
  const openspecDir = OPENSPEC_DIR_NAME;
86
87
  const openspecPath = path.join(projectPath, openspecDir);
87
88
  // Validation happens silently in the background
@@ -94,6 +95,7 @@ export class InitCommand {
94
95
  // finds the nearest ancestor root (so pointer-repo subdirectories
95
96
  // refuse exactly where a normal command would resolve the pointer).
96
97
  const guardRoot = findRepoPlanningRootSync(projectPath);
98
+ let integrationsOnly = false;
97
99
  if (guardRoot) {
98
100
  const { hasPlanningShape, pointer } = classifyOpenSpecDir(guardRoot);
99
101
  if (!hasPlanningShape) {
@@ -103,14 +105,30 @@ export class InitCommand {
103
105
  `). Fix or remove the store: line before running openspec init.`);
104
106
  }
105
107
  if (pointer.value !== undefined) {
106
- throw new Error(`This repo's planning is externalized to store '${pointer.value}' (${pointer.filePath}). ` +
107
- `Remove the store: line first to convert this repo to a local OpenSpec root.`);
108
+ if (path.resolve(guardRoot) !== projectPath) {
109
+ throw new Error(`This repo's planning is externalized to store '${pointer.value}' (${pointer.filePath}). ` +
110
+ 'Run openspec init from the pointer repo root to install integrations.');
111
+ }
112
+ // A valid pointer repo already has its planning root in the declared
113
+ // store. Init should still be able to install agent integrations in
114
+ // the code repo, without creating a second local planning root.
115
+ await resolveOpenSpecRoot({ startPath: projectPath });
116
+ integrationsOnly = true;
108
117
  }
109
118
  }
110
119
  }
111
- await this.assertLanguageCanBeApplied(projectPath, openspecPath);
112
- // Check for legacy artifacts and handle cleanup
113
- const deferredLegacyCleanup = await this.handleLegacyCleanup(projectPath, extendMode);
120
+ if (!integrationsOnly) {
121
+ await this.assertLanguageCanBeApplied(projectPath, openspecPath);
122
+ }
123
+ else if (this.language) {
124
+ throw new Error('--language cannot update an external store through a pointer repo. ' +
125
+ 'Run init in the store root, or edit the store config directly.');
126
+ }
127
+ // Pointer repos keep their local planning files untouched. Normal init may
128
+ // still remove OpenSpec-managed artifacts from older layouts.
129
+ const deferredLegacyCleanup = integrationsOnly
130
+ ? null
131
+ : await this.handleLegacyCleanup(projectPath, extendMode);
114
132
  // Migrate OpenSpec-managed skills left in renamed tool directories
115
133
  // (e.g. .kimi -> .kimi-code) before detection so they stay recognized.
116
134
  migrateLegacyToolDirs(projectPath);
@@ -154,8 +172,11 @@ export class InitCommand {
154
172
  // is made before generation so the write can be gated, and persisted after
155
173
  // config.yaml exists so future non-interactive updates honor it.
156
174
  const copilotDecision = await this.resolveCopilotCloudDecision(projectPath, validatedTools);
157
- // Create directory structure and config
158
- await this.createDirectoryStructure(openspecPath, extendMode);
175
+ // Pointer repos only receive integrations. Their planning structure and
176
+ // config stay in the declared store.
177
+ if (!integrationsOnly) {
178
+ await this.createDirectoryStructure(openspecPath, extendMode);
179
+ }
159
180
  // Generate skills and commands for each tool
160
181
  const results = await this.generateSkillsAndCommands(projectPath, validatedTools, copilotDecision.write);
161
182
  // Legacy cleanup was deferred to avoid interfering with skill/command generation;
@@ -163,12 +184,15 @@ export class InitCommand {
163
184
  if (deferredLegacyCleanup) {
164
185
  await this.finalizeDeferredLegacyCleanup(projectPath, deferredLegacyCleanup);
165
186
  }
166
- // Create config.yaml if needed
167
- const configStatus = await this.createConfig(openspecPath, extendMode);
187
+ // Create config.yaml if needed. A pointer repo already has the config that
188
+ // declares its store, so preserve it byte-for-byte.
189
+ const configStatus = integrationsOnly
190
+ ? 'exists'
191
+ : await this.createConfig(openspecPath, extendMode);
168
192
  // Persist an explicit Copilot cloud decision so `openspec update` (which
169
193
  // never prompts) honors it. Best-effort: a config-write failure must not
170
194
  // fail an otherwise-successful init.
171
- if (copilotDecision.persist !== undefined) {
195
+ if (!integrationsOnly && copilotDecision.persist !== undefined) {
172
196
  try {
173
197
  await persistCopilotCloudOptIn(projectPath, copilotDecision.persist);
174
198
  }
@@ -745,10 +745,10 @@ export function formatDetectionSummary(detection) {
745
745
  lines.push('agents. This simplifies your setup while keeping everything working');
746
746
  lines.push('as before.');
747
747
  lines.push('');
748
- // Section 1: Files to remove (no user content to preserve)
748
+ // Section 1: Files to remove entirely
749
749
  if (removals.length > 0) {
750
750
  lines.push(chalk.bold('Files to remove'));
751
- lines.push(chalk.dim('No user content to preserve:'));
751
+ lines.push(chalk.dim('These files will be deleted entirely. Back up any custom content before proceeding:'));
752
752
  for (const { path } of removals) {
753
753
  lines.push(` • ${path}`);
754
754
  }
@@ -953,12 +953,16 @@ export function formatProjectMdMigrationHint() {
953
953
  lines.push(' • openspec/project.md');
954
954
  lines.push(chalk.dim(' We won\'t delete this file. It may contain useful project context.'));
955
955
  lines.push('');
956
- lines.push(chalk.dim(' The new openspec/config.yaml has a "context:" section for planning'));
957
- lines.push(chalk.dim(' context. This is included in every OpenSpec request and works more'));
958
- lines.push(chalk.dim(' reliably than the old project.md approach.'));
956
+ lines.push(chalk.dim(' Ask your AI assistant:'));
959
957
  lines.push('');
960
- lines.push(chalk.dim(' Review project.md, move any useful content to config.yaml\'s context'));
961
- lines.push(chalk.dim(' section, then delete the file when ready.'));
958
+ lines.push(chalk.dim(' Review openspec/project.md and migrate its useful content to'));
959
+ lines.push(chalk.dim(' openspec/config.yaml. Keep context concise: include only project-wide'));
960
+ lines.push(chalk.dim(' facts needed during artifact creation, apply, and archive. Move'));
961
+ lines.push(chalk.dim(' artifact-specific guidance into rules for the matching artifacts.'));
962
+ lines.push(chalk.dim(' Move guidance for apply or archive into the matching operations entry.'));
963
+ lines.push(chalk.dim(' Leave out generic, outdated, or verbose material. Do not delete project.md.'));
964
+ lines.push('');
965
+ lines.push(chalk.dim(' Review config.yaml, then delete project.md when ready.'));
962
966
  return lines.join('\n');
963
967
  }
964
968
  //# sourceMappingURL=legacy-cleanup.js.map
@@ -3,6 +3,8 @@ interface ListOptions {
3
3
  sort?: 'recent' | 'name';
4
4
  json?: boolean;
5
5
  root?: RootOutput;
6
+ archived?: boolean;
7
+ all?: boolean;
6
8
  }
7
9
  export declare class ListCommand {
8
10
  execute(targetPath?: string, mode?: 'changes' | 'specs', options?: ListOptions): Promise<void>;
package/dist/core/list.js CHANGED
@@ -35,8 +35,9 @@ async function readChangeDirectoryEntries(changesDir) {
35
35
  /**
36
36
  * Get the most recent modification time of any file in a directory (recursive).
37
37
  * Falls back to the directory's own mtime if no files are found.
38
+ * Archived links use their own mtime: moving a change can break relative targets.
38
39
  */
39
- async function getLastModified(dirPath) {
40
+ async function getLastModified(dirPath, archived = false) {
40
41
  let latest = null;
41
42
  async function walk(dir) {
42
43
  const entries = await fs.readdir(dir, { withFileTypes: true });
@@ -47,7 +48,9 @@ async function getLastModified(dirPath) {
47
48
  await walk(fullPath);
48
49
  }
49
50
  else {
50
- const stat = await fs.stat(fullPath);
51
+ const stat = archived && entry.isSymbolicLink()
52
+ ? await fs.lstat(fullPath)
53
+ : await fs.stat(fullPath);
51
54
  if (latest === null || stat.mtime > latest) {
52
55
  latest = stat.mtime;
53
56
  }
@@ -96,20 +99,31 @@ function formatRelativeTime(date) {
96
99
  }
97
100
  export class ListCommand {
98
101
  async execute(targetPath = '.', mode = 'changes', options = {}) {
99
- const { sort = 'recent', json = false, root } = options;
102
+ const { sort = 'recent', json = false, root, archived = false, all = false } = options;
103
+ if (mode === 'specs' && (archived || all)) {
104
+ throw new Error('--archived and --all can only be used when listing changes.');
105
+ }
100
106
  if (mode === 'changes') {
101
107
  const changesDir = path.join(targetPath, 'openspec', 'changes');
102
- // Get all directories in changes (excluding archive)
108
+ const archiveDir = path.join(changesDir, 'archive');
109
+ const includeArchived = archived || all;
110
+ // Read the parent even for --archived: Windows can report ENOENT for
111
+ // changes/archive when changes is a file, hiding a malformed root.
103
112
  const entries = await readChangeDirectoryEntries(changesDir);
104
- const changeDirs = entries
113
+ const activeDirs = !archived || all ? entries
105
114
  .filter(entry => entry.isDirectory() && entry.name !== 'archive')
106
- .map(entry => entry.name);
115
+ .map(entry => ({ name: entry.name, parent: changesDir, archived: false })) : [];
116
+ const archiveEntries = includeArchived ? await readChangeDirectoryEntries(archiveDir) : [];
117
+ const archivedDirs = archiveEntries
118
+ .filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
119
+ .map(entry => ({ name: entry.name, parent: archiveDir, archived: true }));
120
+ const changeDirs = [...activeDirs, ...archivedDirs];
107
121
  if (changeDirs.length === 0) {
108
122
  if (json) {
109
123
  console.log(JSON.stringify({ changes: [], ...(root ? { root } : {}) }, null, 2));
110
124
  }
111
125
  else {
112
- console.log('No active changes found.');
126
+ console.log(all ? 'No changes found.' : archived ? 'No archived changes found.' : 'No active changes found.');
113
127
  }
114
128
  return;
115
129
  }
@@ -118,18 +132,21 @@ export class ListCommand {
118
132
  // A directory that only wraps nested change directories is still listed -
119
133
  // hiding it would hide a real change whenever the probe is wrong - but it
120
134
  // is listed as what it is, so the nesting stops failing silently (#1846).
121
- const nestedFindings = await findNestedChanges(changesDir, changeDirs);
135
+ const nestedFindings = await findNestedChanges(changesDir, activeDirs.map((changeDir) => changeDir.name));
122
136
  const nestedByName = new Map(nestedFindings.map((finding) => [finding.name, finding]));
123
137
  for (const changeDir of changeDirs) {
124
- const progress = await getTaskProgressForChange(changesDir, changeDir, targetPath);
125
- const changePath = path.join(changesDir, changeDir);
126
- const lastModified = await getLastModified(changePath);
138
+ const progress = await getTaskProgressForChange(changeDir.parent, changeDir.name, targetPath);
139
+ const changePath = path.join(changeDir.parent, changeDir.name);
140
+ const lastModified = await getLastModified(changePath, changeDir.archived);
127
141
  changes.push({
128
- name: changeDir,
142
+ name: changeDir.name,
129
143
  completedTasks: progress.completed,
130
144
  totalTasks: progress.total,
131
145
  lastModified,
132
- ...(nestedByName.has(changeDir) ? { nested: nestedByName.get(changeDir).nested } : {})
146
+ archived: changeDir.archived,
147
+ ...(!changeDir.archived && nestedByName.has(changeDir.name)
148
+ ? { nested: nestedByName.get(changeDir.name).nested }
149
+ : {})
133
150
  });
134
151
  }
135
152
  // Sort by preference (default: recent first)
@@ -147,6 +164,7 @@ export class ListCommand {
147
164
  totalTasks: c.totalTasks,
148
165
  lastModified: c.lastModified.toISOString(),
149
166
  status: c.totalTasks === 0 ? 'no-tasks' : c.completedTasks === c.totalTasks ? 'complete' : 'in-progress',
167
+ ...(includeArchived ? { archived: c.archived } : {}),
150
168
  ...(c.nested ? { nested: c.nested } : {})
151
169
  }));
152
170
  // Additive: the entries keep their shape so existing consumers are
@@ -165,16 +183,24 @@ export class ListCommand {
165
183
  return;
166
184
  }
167
185
  // Display results
168
- console.log('Changes:');
169
- const padding = ' ';
170
- const nameWidth = Math.max(...changes.map(c => c.name.length));
171
- for (const change of changes) {
172
- const paddedName = change.name.padEnd(nameWidth);
173
- const status = change.nested
174
- ? 'not a change'
175
- : formatTaskStatus({ total: change.totalTasks, completed: change.completedTasks });
176
- const timeAgo = formatRelativeTime(change.lastModified);
177
- console.log(`${padding}${paddedName} ${status.padEnd(12)} ${timeAgo}`);
186
+ const groups = [
187
+ { heading: 'Changes:', changes: changes.filter(change => !change.archived) },
188
+ { heading: 'Archived Changes:', changes: changes.filter(change => change.archived) }
189
+ ].filter(group => group.changes.length > 0);
190
+ for (const [index, group] of groups.entries()) {
191
+ if (index > 0)
192
+ console.log('');
193
+ console.log(group.heading);
194
+ const padding = ' ';
195
+ const nameWidth = Math.max(...group.changes.map(c => c.name.length));
196
+ for (const change of group.changes) {
197
+ const paddedName = change.name.padEnd(nameWidth);
198
+ const status = change.nested
199
+ ? 'not a change'
200
+ : formatTaskStatus({ total: change.totalTasks, completed: change.completedTasks });
201
+ const timeAgo = formatRelativeTime(change.lastModified);
202
+ console.log(`${padding}${paddedName} ${status.padEnd(12)} ${timeAgo}`);
203
+ }
178
204
  }
179
205
  for (const finding of nestedFindings) {
180
206
  console.log('');
@@ -1,4 +1,5 @@
1
1
  import { buildCodeFenceMask, extractRequirementText, hasScenarioBody } from './requirement-text.js';
2
+ import { normalizeRequirementName, scenarioNameFromHeaderText } from './requirement-blocks.js';
2
3
  export class MarkdownParser {
3
4
  lines;
4
5
  codeFenceLineMask;
@@ -121,6 +122,9 @@ export class MarkdownParser {
121
122
  const text = extractRequirementText(child.title, child.content.split('\n'));
122
123
  const scenarios = this.parseScenarios(child);
123
124
  requirements.push({
125
+ // The name archive matches on, so a JSON reader can cite a requirement
126
+ // the way a MODIFIED or REMOVED header must.
127
+ name: normalizeRequirementName(child.title.replace(/^Requirement:\s*/i, '')),
124
128
  text,
125
129
  scenarios,
126
130
  });
@@ -134,6 +138,7 @@ export class MarkdownParser {
134
138
  // body is not a scenario; the delta counter applies the same rule.
135
139
  if (hasScenarioBody(scenarioSection.content)) {
136
140
  scenarios.push({
141
+ name: scenarioNameFromHeaderText(scenarioSection.title),
137
142
  rawText: scenarioSection.content
138
143
  });
139
144
  }