@fission-ai/openspec 1.13.2 → 1.14.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/dist/cli/index.js +39 -1
- package/dist/commands/spec.js +1 -0
- package/dist/commands/workflow/instructions.js +31 -11
- package/dist/commands/workflow/shared.d.ts +2 -0
- package/dist/commands/workflow/status.js +15 -2
- package/dist/core/archive.js +15 -2
- package/dist/core/artifact-graph/instruction-loader.d.ts +7 -0
- package/dist/core/artifact-graph/instruction-loader.js +18 -1
- package/dist/core/change-metadata/schema.d.ts +3 -0
- package/dist/core/change-metadata/schema.js +10 -0
- package/dist/core/change-status-policy.d.ts +8 -0
- package/dist/core/change-status-policy.js +33 -2
- package/dist/core/command-generation/adapters/atomcode.d.ts +24 -0
- package/dist/core/command-generation/adapters/atomcode.js +46 -0
- package/dist/core/command-generation/adapters/codestudio.d.ts +13 -0
- package/dist/core/command-generation/adapters/codestudio.js +27 -0
- package/dist/core/command-generation/adapters/easycode.d.ts +17 -0
- package/dist/core/command-generation/adapters/easycode.js +28 -0
- package/dist/core/command-generation/adapters/gemini.js +1 -35
- package/dist/core/command-generation/adapters/gigacode.d.ts +17 -0
- package/dist/core/command-generation/adapters/gigacode.js +31 -0
- package/dist/core/command-generation/adapters/index.d.ts +4 -0
- package/dist/core/command-generation/adapters/index.js +4 -0
- package/dist/core/command-generation/registry.js +8 -0
- package/dist/core/command-generation/toml.d.ts +19 -0
- package/dist/core/command-generation/toml.js +39 -0
- package/dist/core/command-surface.js +1 -1
- package/dist/core/completions/command-registry.js +19 -0
- package/dist/core/completions/installers/zsh-installer.js +4 -2
- package/dist/core/config-prompts.js +6 -6
- package/dist/core/config.js +11 -1
- package/dist/core/init.js +35 -11
- package/dist/core/legacy-cleanup.js +11 -7
- package/dist/core/list.d.ts +2 -0
- package/dist/core/list.js +49 -23
- package/dist/core/parsers/markdown-parser.js +5 -0
- package/dist/core/parsers/requirement-blocks.d.ts +6 -0
- package/dist/core/parsers/requirement-blocks.js +9 -2
- package/dist/core/project-config.js +45 -1
- package/dist/core/references.js +3 -1
- package/dist/core/root-selection.d.ts +6 -0
- package/dist/core/root-selection.js +16 -0
- package/dist/core/schemas/base.schema.d.ts +3 -0
- package/dist/core/schemas/base.schema.js +6 -0
- package/dist/core/schemas/change.schema.d.ts +8 -0
- package/dist/core/schemas/spec.schema.d.ts +2 -0
- package/dist/core/templates/workflows/apply-change.js +5 -2
- package/dist/core/templates/workflows/archive-change.js +26 -4
- package/dist/core/templates/workflows/bulk-archive-change.js +6 -2
- package/dist/core/validation/constants.d.ts +1 -1
- package/dist/core/validation/constants.js +1 -1
- package/dist/core/validation/validator.js +9 -1
- package/dist/core/version-check.d.ts +23 -1
- package/dist/core/version-check.js +112 -26
- package/dist/core/view.d.ts +2 -0
- package/dist/core/view.js +62 -4
- package/dist/utils/change-metadata.d.ts +20 -1
- package/dist/utils/change-metadata.js +57 -1
- package/dist/utils/command-references.d.ts +1 -1
- package/dist/utils/command-references.js +2 -2
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/package.json +1 -1
- package/schemas/spec-driven/schema.yaml +2 -1
- package/schemas/spec-driven/templates/proposal.md +6 -3
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GigaCode Command Adapter
|
|
3
|
+
*
|
|
4
|
+
* Formats commands for GigaCode using its Markdown custom command format.
|
|
5
|
+
* Project commands live in `.gigacode/commands/` and accept an optional
|
|
6
|
+
* `description` field in YAML frontmatter.
|
|
7
|
+
*
|
|
8
|
+
* @see https://gitverse.ru/docs/ai/ai-assistant-gigacode/gigacode-cli/commands
|
|
9
|
+
*/
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { escapeYamlValue } from '../yaml.js';
|
|
12
|
+
/**
|
|
13
|
+
* GigaCode adapter for command generation.
|
|
14
|
+
* File path: .gigacode/commands/opsx-<id>.md
|
|
15
|
+
* Format: Markdown with description frontmatter
|
|
16
|
+
*/
|
|
17
|
+
export const gigacodeAdapter = {
|
|
18
|
+
toolId: 'gigacode',
|
|
19
|
+
getFilePath(commandId) {
|
|
20
|
+
return path.join('.gigacode', 'commands', `opsx-${commandId}.md`);
|
|
21
|
+
},
|
|
22
|
+
formatFile(content) {
|
|
23
|
+
return `---
|
|
24
|
+
description: ${escapeYamlValue(content.description)}
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
${content.body}
|
|
28
|
+
`;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=gigacode.js.map
|
|
@@ -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';
|
|
@@ -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
|
-
//
|
|
198
|
-
|
|
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('#
|
|
23
|
-
lines.push('#
|
|
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('#
|
|
27
|
-
lines.push('#
|
|
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
|
|
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('');
|
package/dist/core/config.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
107
|
-
`
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
//
|
|
158
|
-
|
|
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
|
-
|
|
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
|
|
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('
|
|
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('
|
|
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
|
|
961
|
-
lines.push(chalk.dim('
|
|
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
|
package/dist/core/list.d.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
|
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,
|
|
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(
|
|
125
|
-
const changePath = path.join(
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
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
|
}
|