@fission-ai/openspec 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -2
- package/dist/cli/index.js +79 -8
- package/dist/commands/change.d.ts +0 -1
- package/dist/commands/change.js +105 -51
- package/dist/commands/config.js +14 -8
- package/dist/commands/doctor.js +9 -4
- package/dist/commands/feedback.js +74 -23
- package/dist/commands/schema.js +110 -31
- package/dist/commands/show.js +5 -2
- package/dist/commands/spec.js +49 -29
- package/dist/commands/validate.js +20 -5
- package/dist/commands/workflow/index.d.ts +1 -1
- package/dist/commands/workflow/index.js +1 -1
- package/dist/commands/workflow/instructions.d.ts +7 -1
- package/dist/commands/workflow/instructions.js +121 -33
- package/dist/commands/workflow/shared.d.ts +13 -2
- package/dist/commands/workflow/shared.js +34 -4
- package/dist/commands/workflow/status.js +9 -4
- package/dist/commands/workflow/templates.js +15 -5
- package/dist/core/archive.d.ts +14 -1
- package/dist/core/archive.js +1332 -155
- package/dist/core/artifact-graph/graph.d.ts +16 -0
- package/dist/core/artifact-graph/graph.js +30 -6
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/index.js +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +30 -3
- package/dist/core/artifact-graph/instruction-loader.js +70 -12
- package/dist/core/artifact-graph/outputs.d.ts +1 -0
- package/dist/core/artifact-graph/outputs.js +67 -5
- package/dist/core/artifact-graph/resolver.d.ts +14 -0
- package/dist/core/artifact-graph/resolver.js +69 -15
- package/dist/core/artifact-graph/types.js +21 -3
- package/dist/core/available-tools.d.ts +2 -2
- package/dist/core/available-tools.js +14 -3
- package/dist/core/change-metadata/schema.d.ts +2 -0
- package/dist/core/change-metadata/schema.js +13 -0
- package/dist/core/change-status-policy.d.ts +1 -1
- package/dist/core/change-status-policy.js +2 -2
- package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
- package/dist/core/command-generation/adapters/amazon-q.js +7 -1
- package/dist/core/command-generation/adapters/antigravity.js +2 -1
- package/dist/core/command-generation/adapters/auggie.js +2 -1
- package/dist/core/command-generation/adapters/bob.d.ts +5 -1
- package/dist/core/command-generation/adapters/bob.js +6 -5
- package/dist/core/command-generation/adapters/claude.js +1 -8
- package/dist/core/command-generation/adapters/codebuddy.js +3 -2
- package/dist/core/command-generation/adapters/continue.js +3 -2
- package/dist/core/command-generation/adapters/costrict.js +2 -1
- package/dist/core/command-generation/adapters/crush.js +5 -5
- package/dist/core/command-generation/adapters/cursor.js +2 -2
- package/dist/core/command-generation/adapters/devin.d.ts +19 -0
- package/dist/core/command-generation/adapters/devin.js +36 -0
- package/dist/core/command-generation/adapters/factory.js +2 -1
- package/dist/core/command-generation/adapters/gemini.js +37 -2
- package/dist/core/command-generation/adapters/github-copilot.js +2 -1
- package/dist/core/command-generation/adapters/iflow.js +5 -4
- package/dist/core/command-generation/adapters/index.d.ts +2 -2
- package/dist/core/command-generation/adapters/index.js +2 -2
- package/dist/core/command-generation/adapters/junie.js +2 -1
- package/dist/core/command-generation/adapters/kiro.js +2 -1
- package/dist/core/command-generation/adapters/lingma.js +5 -5
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/oh-my-pi.js +3 -5
- package/dist/core/command-generation/adapters/opencode.js +3 -5
- package/dist/core/command-generation/adapters/pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/pi.js +3 -6
- package/dist/core/command-generation/adapters/qoder.js +5 -5
- package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
- package/dist/core/command-generation/adapters/qwen.js +12 -7
- package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
- package/dist/core/command-generation/adapters/roocode.js +4 -4
- package/dist/core/command-generation/adapters/trae.js +1 -21
- package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
- package/dist/core/command-generation/adapters/zcode.js +33 -0
- package/dist/core/command-generation/generator.d.ts +8 -0
- package/dist/core/command-generation/generator.js +15 -1
- package/dist/core/command-generation/index.d.ts +1 -1
- package/dist/core/command-generation/index.js +1 -1
- package/dist/core/command-generation/invocation.d.ts +73 -0
- package/dist/core/command-generation/invocation.js +77 -0
- package/dist/core/command-generation/registry.js +4 -4
- package/dist/core/command-generation/types.d.ts +9 -2
- package/dist/core/command-generation/yaml.d.ts +20 -6
- package/dist/core/command-generation/yaml.js +31 -21
- package/dist/core/command-surface.d.ts +15 -0
- package/dist/core/command-surface.js +33 -0
- package/dist/core/completions/command-registry.js +16 -2
- package/dist/core/completions/generators/powershell-generator.js +15 -8
- package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
- package/dist/core/completions/installers/zsh-installer.js +20 -4
- package/dist/core/config-prompts.d.ts +1 -1
- package/dist/core/config-prompts.js +14 -1
- package/dist/core/config-schema.d.ts +10 -1
- package/dist/core/config-schema.js +67 -1
- package/dist/core/config.d.ts +15 -0
- package/dist/core/config.js +45 -5
- package/dist/core/file-state.js +56 -24
- package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
- package/dist/core/github-copilot/cloud-agent.js +497 -0
- package/dist/core/global-config.d.ts +16 -0
- package/dist/core/init.d.ts +58 -0
- package/dist/core/init.js +450 -69
- package/dist/core/legacy-cleanup.d.ts +59 -0
- package/dist/core/legacy-cleanup.js +253 -24
- package/dist/core/list.js +5 -7
- package/dist/core/migration.d.ts +73 -1
- package/dist/core/migration.js +360 -11
- package/dist/core/onboarding-commands.d.ts +30 -0
- package/dist/core/onboarding-commands.js +38 -0
- package/dist/core/parsers/change-parser.d.ts +17 -2
- package/dist/core/parsers/change-parser.js +33 -20
- package/dist/core/parsers/code-fence.d.ts +15 -0
- package/dist/core/parsers/code-fence.js +49 -0
- package/dist/core/parsers/markdown-parser.js +2 -1
- package/dist/core/parsers/requirement-blocks.d.ts +18 -0
- package/dist/core/parsers/requirement-blocks.js +120 -35
- package/dist/core/parsers/requirement-text.d.ts +1 -8
- package/dist/core/parsers/requirement-text.js +4 -42
- package/dist/core/parsers/spec-structure.d.ts +1 -1
- package/dist/core/parsers/spec-structure.js +20 -30
- package/dist/core/profile-sync-drift.d.ts +0 -8
- package/dist/core/profile-sync-drift.js +48 -49
- package/dist/core/project-config.d.ts +28 -6
- package/dist/core/project-config.js +108 -8
- package/dist/core/references.js +42 -4
- package/dist/core/relationship-health.d.ts +8 -0
- package/dist/core/relationship-health.js +13 -0
- package/dist/core/root-selection.d.ts +9 -6
- package/dist/core/root-selection.js +42 -5
- package/dist/core/shared/index.d.ts +1 -0
- package/dist/core/shared/index.js +1 -0
- package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
- package/dist/core/shared/skill-content-equivalence.js +41 -0
- package/dist/core/shared/skill-paths.d.ts +13 -0
- package/dist/core/shared/skill-paths.js +23 -0
- package/dist/core/shared/tool-detection.d.ts +25 -5
- package/dist/core/shared/tool-detection.js +166 -36
- package/dist/core/shared-skill-target.d.ts +16 -0
- package/dist/core/shared-skill-target.js +156 -0
- package/dist/core/specs-apply.d.ts +80 -33
- package/dist/core/specs-apply.js +659 -171
- package/dist/core/store/git.d.ts +13 -0
- package/dist/core/store/git.js +24 -0
- package/dist/core/templates/skill-templates.d.ts +1 -1
- package/dist/core/templates/skill-templates.js +1 -1
- package/dist/core/templates/workflows/apply-change.d.ts +10 -0
- package/dist/core/templates/workflows/apply-change.js +48 -158
- package/dist/core/templates/workflows/archive-change.js +170 -42
- package/dist/core/templates/workflows/bulk-archive-change.js +256 -75
- package/dist/core/templates/workflows/continue-change.js +38 -46
- package/dist/core/templates/workflows/explore.js +54 -18
- package/dist/core/templates/workflows/ff-change.js +46 -30
- package/dist/core/templates/workflows/new-change.js +2 -2
- package/dist/core/templates/workflows/onboard.js +13 -7
- package/dist/core/templates/workflows/propose.js +116 -46
- package/dist/core/templates/workflows/store-selection.d.ts +1 -1
- package/dist/core/templates/workflows/store-selection.js +1 -1
- package/dist/core/templates/workflows/sync-specs.js +260 -32
- package/dist/core/templates/workflows/update-change.js +26 -16
- package/dist/core/templates/workflows/verify-change.js +16 -10
- package/dist/core/update.d.ts +40 -5
- package/dist/core/update.js +389 -93
- package/dist/core/validation/constants.d.ts +4 -1
- package/dist/core/validation/constants.js +4 -1
- package/dist/core/validation/validator.d.ts +22 -8
- package/dist/core/validation/validator.js +222 -50
- package/dist/core/version-check.d.ts +152 -0
- package/dist/core/version-check.js +731 -0
- package/dist/core/view.js +12 -17
- package/dist/prompts/searchable-multi-select.js +1 -1
- package/dist/telemetry/config.d.ts +3 -4
- package/dist/telemetry/index.d.ts +10 -5
- package/dist/telemetry/index.js +75 -41
- package/dist/ui/welcome-screen.d.ts +9 -1
- package/dist/ui/welcome-screen.js +91 -38
- package/dist/utils/change-metadata.d.ts +43 -0
- package/dist/utils/change-metadata.js +123 -7
- package/dist/utils/change-utils.d.ts +10 -4
- package/dist/utils/change-utils.js +20 -13
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +92 -6
- package/dist/utils/command-references.js +180 -7
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/file-system.d.ts +10 -0
- package/dist/utils/file-system.js +70 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/interactive.d.ts +23 -0
- package/dist/utils/interactive.js +32 -0
- package/dist/utils/item-discovery.d.ts +19 -0
- package/dist/utils/item-discovery.js +30 -48
- package/dist/utils/shell-detection.d.ts +1 -1
- package/dist/utils/shell-detection.js +64 -11
- package/dist/utils/spec-discovery.d.ts +40 -0
- package/dist/utils/spec-discovery.js +115 -0
- package/dist/utils/task-progress.d.ts +17 -0
- package/dist/utils/task-progress.js +41 -13
- package/package.json +14 -12
- package/schemas/spec-driven/schema.yaml +67 -10
- package/schemas/spec-driven/templates/design.md +2 -2
- package/schemas/spec-driven/templates/proposal.md +10 -4
- package/schemas/spec-driven/templates/spec.md +3 -0
- package/dist/core/command-generation/adapters/codex.d.ts +0 -16
- package/dist/core/command-generation/adapters/codex.js +0 -39
- package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
- package/dist/core/command-generation/adapters/windsurf.js +0 -38
|
@@ -5,24 +5,18 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import * as fs from 'fs';
|
|
8
|
-
import { AI_TOOLS } from '../config.js';
|
|
8
|
+
import { AI_TOOLS, OPENSPEC_SKILL_NAMES } from '../config.js';
|
|
9
|
+
import { CommandAdapterRegistry, generateCommands } from '../command-generation/index.js';
|
|
10
|
+
import { getCommandContents } from './skill-generation.js';
|
|
11
|
+
import { getGlobalConfig } from '../global-config.js';
|
|
12
|
+
import { getProfileWorkflows, ALL_WORKFLOWS } from '../profiles.js';
|
|
13
|
+
import { isSharedSkillTargetActive, readSharedSkillTarget, reconcileSharedSkillTargets, } from '../shared-skill-target.js';
|
|
14
|
+
import { shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, } from '../command-surface.js';
|
|
15
|
+
import { getSkillCapableTools, resolveToolSkillsDir, toolSupportsSkills, } from './skill-paths.js';
|
|
9
16
|
/**
|
|
10
17
|
* Names of skill directories created by openspec init.
|
|
11
18
|
*/
|
|
12
|
-
export const SKILL_NAMES =
|
|
13
|
-
'openspec-explore',
|
|
14
|
-
'openspec-new-change',
|
|
15
|
-
'openspec-continue-change',
|
|
16
|
-
'openspec-apply-change',
|
|
17
|
-
'openspec-update-change',
|
|
18
|
-
'openspec-ff-change',
|
|
19
|
-
'openspec-sync-specs',
|
|
20
|
-
'openspec-archive-change',
|
|
21
|
-
'openspec-bulk-archive-change',
|
|
22
|
-
'openspec-verify-change',
|
|
23
|
-
'openspec-onboard',
|
|
24
|
-
'openspec-propose',
|
|
25
|
-
];
|
|
19
|
+
export const SKILL_NAMES = OPENSPEC_SKILL_NAMES;
|
|
26
20
|
/**
|
|
27
21
|
* IDs of command templates created by openspec init.
|
|
28
22
|
*/
|
|
@@ -44,21 +38,26 @@ export const COMMAND_IDS = [
|
|
|
44
38
|
* Gets the list of tools with skillsDir configured.
|
|
45
39
|
*/
|
|
46
40
|
export function getToolsWithSkillsDir() {
|
|
47
|
-
return
|
|
41
|
+
return getSkillCapableTools().map((tool) => tool.value);
|
|
48
42
|
}
|
|
49
43
|
/**
|
|
50
44
|
* Checks which skill files exist for a tool.
|
|
51
45
|
*/
|
|
52
46
|
export function getToolSkillStatus(projectRoot, toolId) {
|
|
53
47
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
54
|
-
if (!tool
|
|
48
|
+
if (!tool || !toolSupportsSkills(tool)) {
|
|
49
|
+
return { configured: false, fullyConfigured: false, skillCount: 0 };
|
|
50
|
+
}
|
|
51
|
+
if (tool.skillsDir && !isSharedSkillTargetActive(projectRoot, toolId)) {
|
|
55
52
|
return { configured: false, fullyConfigured: false, skillCount: 0 };
|
|
56
53
|
}
|
|
57
|
-
const
|
|
54
|
+
const skillsDirs = [
|
|
55
|
+
resolveToolSkillsDir(projectRoot, tool),
|
|
56
|
+
...(tool.legacySkillsDirs ?? []).map((root) => path.join(projectRoot, root, 'skills')),
|
|
57
|
+
];
|
|
58
58
|
let skillCount = 0;
|
|
59
59
|
for (const skillName of SKILL_NAMES) {
|
|
60
|
-
|
|
61
|
-
if (fs.existsSync(skillFile)) {
|
|
60
|
+
if (skillsDirs.some((skillsDir) => fs.existsSync(path.join(skillsDir, skillName, 'SKILL.md')))) {
|
|
62
61
|
skillCount++;
|
|
63
62
|
}
|
|
64
63
|
}
|
|
@@ -68,14 +67,108 @@ export function getToolSkillStatus(projectRoot, toolId) {
|
|
|
68
67
|
skillCount,
|
|
69
68
|
};
|
|
70
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Checks whether a tool has at least one generated OpenSpec command file.
|
|
72
|
+
*/
|
|
73
|
+
export function toolHasAnyConfiguredCommand(projectPath, toolId) {
|
|
74
|
+
const adapter = CommandAdapterRegistry.get(toolId);
|
|
75
|
+
if (!adapter)
|
|
76
|
+
return false;
|
|
77
|
+
for (const commandId of COMMAND_IDS) {
|
|
78
|
+
const cmdPath = adapter.getFilePath(commandId);
|
|
79
|
+
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
|
|
80
|
+
if (fs.existsSync(fullPath)) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Normalizes checkout artifacts that are not real content drift: a UTF-8 BOM and
|
|
88
|
+
* CRLF line endings, which a Windows clone with `core.autocrlf` reintroduces on
|
|
89
|
+
* every checkout of committed command files.
|
|
90
|
+
*/
|
|
91
|
+
function normalizeCommandContent(content) {
|
|
92
|
+
return content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Checks whether command files for a tool on disk match current generated command contents.
|
|
96
|
+
*
|
|
97
|
+
* Command files carry no version stamp, so content equality is the only available
|
|
98
|
+
* "is this current?" signal for a commands-only install.
|
|
99
|
+
*/
|
|
100
|
+
export function areCommandFilesUpToDate(projectRoot, toolId, options) {
|
|
101
|
+
const adapter = CommandAdapterRegistry.get(toolId);
|
|
102
|
+
if (!adapter)
|
|
103
|
+
return false;
|
|
104
|
+
let workflows;
|
|
105
|
+
if (options?.workflows) {
|
|
106
|
+
workflows = options.workflows;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
try {
|
|
110
|
+
const globalCfg = getGlobalConfig();
|
|
111
|
+
const profile = globalCfg.profile ?? 'core';
|
|
112
|
+
workflows = getProfileWorkflows(profile, globalCfg.workflows);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
workflows = ALL_WORKFLOWS;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const knownWorkflows = workflows.filter((w) => ALL_WORKFLOWS.includes(w));
|
|
119
|
+
const commandContents = getCommandContents(knownWorkflows);
|
|
120
|
+
const generatedCommands = generateCommands(commandContents, adapter);
|
|
121
|
+
if (generatedCommands.length === 0) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
for (const cmd of generatedCommands) {
|
|
125
|
+
const cmdPath = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectRoot, cmd.path);
|
|
126
|
+
if (!fs.existsSync(cmdPath)) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const existingContent = fs.readFileSync(cmdPath, 'utf-8');
|
|
131
|
+
if (normalizeCommandContent(existingContent) !== normalizeCommandContent(cmd.fileContent)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Also check no extra command files exist for deselected workflows
|
|
140
|
+
const desiredWorkflowSet = new Set(knownWorkflows);
|
|
141
|
+
for (const workflow of ALL_WORKFLOWS) {
|
|
142
|
+
if (desiredWorkflowSet.has(workflow))
|
|
143
|
+
continue;
|
|
144
|
+
const cmdPath = adapter.getFilePath(workflow);
|
|
145
|
+
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectRoot, cmdPath);
|
|
146
|
+
if (fs.existsSync(fullPath)) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
71
152
|
/**
|
|
72
153
|
* Gets the skill status for all tools with skillsDir configured.
|
|
73
154
|
*/
|
|
74
155
|
export function getToolStates(projectRoot) {
|
|
75
156
|
const states = new Map();
|
|
76
|
-
const
|
|
77
|
-
for (const
|
|
78
|
-
|
|
157
|
+
const tools = getSkillCapableTools();
|
|
158
|
+
for (const tool of tools) {
|
|
159
|
+
const skillStatus = getToolSkillStatus(projectRoot, tool.value);
|
|
160
|
+
const markerConfigured = Boolean(tool.skillsDir) &&
|
|
161
|
+
readSharedSkillTarget(projectRoot, tool.skillsDir) === tool.value;
|
|
162
|
+
states.set(tool.value, markerConfigured
|
|
163
|
+
? { ...skillStatus, configured: true }
|
|
164
|
+
: skillStatus);
|
|
165
|
+
}
|
|
166
|
+
const configuredTools = tools.filter((tool) => tool.skillsDir && states.get(tool.value)?.configured);
|
|
167
|
+
const activeSharedTargets = new Set(reconcileSharedSkillTargets(projectRoot, configuredTools).map((tool) => tool.value));
|
|
168
|
+
for (const tool of configuredTools) {
|
|
169
|
+
if (!activeSharedTargets.has(tool.value)) {
|
|
170
|
+
states.set(tool.value, { configured: false, fullyConfigured: false, skillCount: 0 });
|
|
171
|
+
}
|
|
79
172
|
}
|
|
80
173
|
return states;
|
|
81
174
|
}
|
|
@@ -109,11 +202,12 @@ export function extractGeneratedByVersion(skillFilePath) {
|
|
|
109
202
|
}
|
|
110
203
|
}
|
|
111
204
|
/**
|
|
112
|
-
* Gets version status for a tool by reading
|
|
205
|
+
* Gets version status for a tool by reading its skill files, falling back to a
|
|
206
|
+
* command-content fingerprint for installs that have commands but no skills.
|
|
113
207
|
*/
|
|
114
|
-
export function getToolVersionStatus(projectRoot, toolId, currentVersion) {
|
|
208
|
+
export function getToolVersionStatus(projectRoot, toolId, currentVersion, options) {
|
|
115
209
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
116
|
-
if (!tool
|
|
210
|
+
if (!tool || !toolSupportsSkills(tool)) {
|
|
117
211
|
return {
|
|
118
212
|
toolId,
|
|
119
213
|
toolName: toolId,
|
|
@@ -122,17 +216,43 @@ export function getToolVersionStatus(projectRoot, toolId, currentVersion) {
|
|
|
122
216
|
needsUpdate: false,
|
|
123
217
|
};
|
|
124
218
|
}
|
|
125
|
-
const
|
|
219
|
+
const skillsDirs = [
|
|
220
|
+
resolveToolSkillsDir(projectRoot, tool),
|
|
221
|
+
...(tool.legacySkillsDirs ?? []).map((root) => path.join(projectRoot, root, 'skills')),
|
|
222
|
+
];
|
|
126
223
|
let generatedByVersion = null;
|
|
127
|
-
|
|
224
|
+
let foundSkill = false;
|
|
225
|
+
// 1. Find the first skill file that exists and read its version
|
|
128
226
|
for (const skillName of SKILL_NAMES) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
227
|
+
for (const skillsDir of skillsDirs) {
|
|
228
|
+
const skillFile = path.join(skillsDir, skillName, 'SKILL.md');
|
|
229
|
+
if (fs.existsSync(skillFile)) {
|
|
230
|
+
generatedByVersion = extractGeneratedByVersion(skillFile);
|
|
231
|
+
foundSkill = true;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (foundSkill)
|
|
132
236
|
break;
|
|
237
|
+
}
|
|
238
|
+
const skillConfigured = getToolSkillStatus(projectRoot, toolId).configured;
|
|
239
|
+
const commandConfigured = toolHasAnyConfiguredCommand(projectRoot, toolId);
|
|
240
|
+
const markerConfigured = Boolean(tool.skillsDir) &&
|
|
241
|
+
readSharedSkillTarget(projectRoot, tool.skillsDir) === toolId;
|
|
242
|
+
const configured = skillConfigured || commandConfigured || markerConfigured;
|
|
243
|
+
// 2. Commands-only installs have no skill file to read a version from, so fall
|
|
244
|
+
// back to comparing the generated command content. Deliberately skipped when
|
|
245
|
+
// skill files exist: an unreadable version there must still force a rewrite.
|
|
246
|
+
if (!skillConfigured && commandConfigured && areCommandFilesUpToDate(projectRoot, toolId, options)) {
|
|
247
|
+
generatedByVersion = currentVersion;
|
|
248
|
+
}
|
|
249
|
+
if (!skillConfigured && !commandConfigured && markerConfigured) {
|
|
250
|
+
const delivery = getGlobalConfig().delivery ?? 'both';
|
|
251
|
+
if (!shouldGenerateSkillsForTool(toolId, delivery) &&
|
|
252
|
+
!shouldGenerateCommandsForTool(toolId, delivery)) {
|
|
253
|
+
generatedByVersion = currentVersion;
|
|
133
254
|
}
|
|
134
255
|
}
|
|
135
|
-
const configured = getToolSkillStatus(projectRoot, toolId).configured;
|
|
136
256
|
const needsUpdate = configured && (generatedByVersion === null || generatedByVersion !== currentVersion);
|
|
137
257
|
return {
|
|
138
258
|
toolId,
|
|
@@ -143,12 +263,22 @@ export function getToolVersionStatus(projectRoot, toolId, currentVersion) {
|
|
|
143
263
|
};
|
|
144
264
|
}
|
|
145
265
|
/**
|
|
146
|
-
* Gets all configured tools in the project.
|
|
266
|
+
* Gets all configured tools in the project (configured via skills or commands).
|
|
147
267
|
*/
|
|
148
268
|
export function getConfiguredTools(projectRoot) {
|
|
149
|
-
|
|
150
|
-
.filter((t) =>
|
|
151
|
-
|
|
269
|
+
const configured = AI_TOOLS
|
|
270
|
+
.filter((t) => {
|
|
271
|
+
if (!toolSupportsSkills(t))
|
|
272
|
+
return false;
|
|
273
|
+
return (getToolSkillStatus(projectRoot, t.value).configured ||
|
|
274
|
+
toolHasAnyConfiguredCommand(projectRoot, t.value) ||
|
|
275
|
+
(Boolean(t.skillsDir) &&
|
|
276
|
+
readSharedSkillTarget(projectRoot, t.skillsDir) === t.value));
|
|
277
|
+
});
|
|
278
|
+
const activeProjectTools = new Set(reconcileSharedSkillTargets(projectRoot, configured.filter((tool) => tool.skillsDir)).map((tool) => tool.value));
|
|
279
|
+
return configured
|
|
280
|
+
.filter((tool) => tool.globalSkillsDir || activeProjectTools.has(tool.value))
|
|
281
|
+
.map((tool) => tool.value);
|
|
152
282
|
}
|
|
153
283
|
/**
|
|
154
284
|
* Gets version status for all configured tools.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type AIToolOption } from './config.js';
|
|
2
|
+
/** Reads a valid-looking marker value without letting linked roots escape. */
|
|
3
|
+
export declare function readSharedSkillTarget(projectPath: string, skillsDir: string): string | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* A shared skill root can only hold one rendered variant of each skill.
|
|
6
|
+
* Keep the writer recorded so later updates do not infer every tool that
|
|
7
|
+
* happens to use the same directory.
|
|
8
|
+
*/
|
|
9
|
+
export declare function reconcileSharedSkillTargets(projectPath: string, tools: AIToolOption[]): AIToolOption[];
|
|
10
|
+
/**
|
|
11
|
+
* Returns whether a tool is the active writer for its physical skills root.
|
|
12
|
+
* Non-shared roots are always active.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isSharedSkillTargetActive(projectPath: string, toolId: string): boolean;
|
|
15
|
+
export declare function writeSharedSkillTarget(projectPath: string, toolId: string): void;
|
|
16
|
+
//# sourceMappingURL=shared-skill-target.d.ts.map
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import { AI_TOOLS, OPENSPEC_SKILL_NAMES } from './config.js';
|
|
4
|
+
import { FileSystemUtils } from '../utils/file-system.js';
|
|
5
|
+
const TARGET_MARKER = '.openspec-target';
|
|
6
|
+
/** Returns the ownership-marker path for one shared skills root. */
|
|
7
|
+
function markerPath(projectPath, skillsDir) {
|
|
8
|
+
return path.join(projectPath, skillsDir, 'skills', TARGET_MARKER);
|
|
9
|
+
}
|
|
10
|
+
/** Reads a valid-looking marker value without letting linked roots escape. */
|
|
11
|
+
export function readSharedSkillTarget(projectPath, skillsDir) {
|
|
12
|
+
try {
|
|
13
|
+
const target = markerPath(projectPath, skillsDir);
|
|
14
|
+
FileSystemUtils.assertProjectArtifactPath(projectPath, target);
|
|
15
|
+
return fs.readFileSync(target, 'utf-8').trim() || undefined;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Whether a tool still has an allowlisted managed skill under an old root. */
|
|
22
|
+
function hasLegacySkills(projectPath, tool) {
|
|
23
|
+
return (tool.legacySkillsDirs ?? []).some((root) => {
|
|
24
|
+
const skillsDir = path.join(projectPath, root, 'skills');
|
|
25
|
+
return OPENSPEC_SKILL_NAMES.some((skillName) => {
|
|
26
|
+
try {
|
|
27
|
+
const skillFile = path.join(skillsDir, skillName, 'SKILL.md');
|
|
28
|
+
FileSystemUtils.assertProjectArtifactPath(projectPath, skillFile);
|
|
29
|
+
return fs.existsSync(skillFile);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Infers pre-marker ownership from generated invocation syntax. This preserves
|
|
39
|
+
* both existing generic `.agents` trees and Codex trees users moved manually.
|
|
40
|
+
*/
|
|
41
|
+
function inferSharedSkillTarget(projectPath, skillsDir) {
|
|
42
|
+
let foundGenericReference = false;
|
|
43
|
+
for (const skillName of OPENSPEC_SKILL_NAMES) {
|
|
44
|
+
const skillFile = path.join(projectPath, skillsDir, 'skills', skillName, 'SKILL.md');
|
|
45
|
+
try {
|
|
46
|
+
FileSystemUtils.assertProjectArtifactPath(projectPath, skillFile);
|
|
47
|
+
const content = fs.readFileSync(skillFile, 'utf-8');
|
|
48
|
+
if (content.includes('$openspec-'))
|
|
49
|
+
return 'codex';
|
|
50
|
+
if (content.includes('/openspec-'))
|
|
51
|
+
foundGenericReference = true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Missing, unreadable, or out-of-project files provide no ownership signal.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return foundGenericReference ? 'agents' : undefined;
|
|
58
|
+
}
|
|
59
|
+
/** Whether the canonical shared root already contains an OpenSpec skill. */
|
|
60
|
+
function hasCurrentSkills(projectPath, skillsDir) {
|
|
61
|
+
return OPENSPEC_SKILL_NAMES.some((skillName) => {
|
|
62
|
+
const skillFile = path.join(projectPath, skillsDir, 'skills', skillName, 'SKILL.md');
|
|
63
|
+
try {
|
|
64
|
+
FileSystemUtils.assertProjectArtifactPath(projectPath, skillFile);
|
|
65
|
+
return fs.existsSync(skillFile);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A shared skill root can only hold one rendered variant of each skill.
|
|
74
|
+
* Keep the writer recorded so later updates do not infer every tool that
|
|
75
|
+
* happens to use the same directory.
|
|
76
|
+
*/
|
|
77
|
+
export function reconcileSharedSkillTargets(projectPath, tools) {
|
|
78
|
+
const byRoot = new Map();
|
|
79
|
+
for (const tool of tools) {
|
|
80
|
+
if (!tool.skillsDir)
|
|
81
|
+
continue;
|
|
82
|
+
const group = byRoot.get(tool.skillsDir) ?? [];
|
|
83
|
+
group.push(tool);
|
|
84
|
+
byRoot.set(tool.skillsDir, group);
|
|
85
|
+
}
|
|
86
|
+
const reconciled = [];
|
|
87
|
+
for (const group of byRoot.values()) {
|
|
88
|
+
if (group.length === 1) {
|
|
89
|
+
reconciled.push(group[0]);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const root = group[0].skillsDir;
|
|
93
|
+
const marked = readSharedSkillTarget(projectPath, root);
|
|
94
|
+
const markedTool = group.find((tool) => tool.value === marked);
|
|
95
|
+
if (markedTool) {
|
|
96
|
+
reconciled.push(markedTool);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const inferred = inferSharedSkillTarget(projectPath, root);
|
|
100
|
+
const legacyCodex = group.find((tool) => tool.value === 'codex' && hasLegacySkills(projectPath, tool));
|
|
101
|
+
if (inferred === 'agents' && legacyCodex) {
|
|
102
|
+
// Before ownership markers existed, selecting both targets produced a
|
|
103
|
+
// generic canonical tree plus a Codex-only legacy tree. Codex now emits
|
|
104
|
+
// a dual-syntax canonical tree, so it can safely consolidate that state.
|
|
105
|
+
reconciled.push(legacyCodex);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const inferredTool = group.find((tool) => tool.value === inferred);
|
|
109
|
+
if (inferredTool) {
|
|
110
|
+
reconciled.push(inferredTool);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
// An unmarked canonical tree predates Codex's move into `.agents`; keep
|
|
114
|
+
// that established agents target instead of overwriting it from `.codex`.
|
|
115
|
+
if (hasCurrentSkills(projectPath, root)) {
|
|
116
|
+
reconciled.push(group.find((tool) => tool.value === 'agents') ?? group[0]);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const legacyTool = group.find((tool) => hasLegacySkills(projectPath, tool));
|
|
120
|
+
if (legacyTool) {
|
|
121
|
+
reconciled.push(legacyTool);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
// `.agents` existed as the vendor-neutral target before Codex adopted it.
|
|
125
|
+
// Unmarked trees therefore retain that established meaning.
|
|
126
|
+
reconciled.push(group.find((tool) => tool.value === 'agents') ?? group[0]);
|
|
127
|
+
}
|
|
128
|
+
return reconciled;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Returns whether a tool is the active writer for its physical skills root.
|
|
132
|
+
* Non-shared roots are always active.
|
|
133
|
+
*/
|
|
134
|
+
export function isSharedSkillTargetActive(projectPath, toolId) {
|
|
135
|
+
const tool = AI_TOOLS.find((candidate) => candidate.value === toolId);
|
|
136
|
+
if (!tool?.skillsDir)
|
|
137
|
+
return false;
|
|
138
|
+
const sharingRoot = AI_TOOLS.filter((candidate) => candidate.skillsDir === tool.skillsDir);
|
|
139
|
+
if (sharingRoot.length < 2)
|
|
140
|
+
return true;
|
|
141
|
+
return reconcileSharedSkillTargets(projectPath, sharingRoot)
|
|
142
|
+
.some((candidate) => candidate.value === toolId);
|
|
143
|
+
}
|
|
144
|
+
export function writeSharedSkillTarget(projectPath, toolId) {
|
|
145
|
+
const tool = AI_TOOLS.find((candidate) => candidate.value === toolId);
|
|
146
|
+
if (!tool?.skillsDir)
|
|
147
|
+
return;
|
|
148
|
+
const sharingRoot = AI_TOOLS.filter((candidate) => candidate.skillsDir === tool.skillsDir);
|
|
149
|
+
if (sharingRoot.length < 2)
|
|
150
|
+
return;
|
|
151
|
+
const target = markerPath(projectPath, tool.skillsDir);
|
|
152
|
+
FileSystemUtils.assertProjectArtifactPath(projectPath, target);
|
|
153
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
154
|
+
fs.writeFileSync(target, `${toolId}\n`, 'utf-8');
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=shared-skill-target.js.map
|
|
@@ -5,28 +5,16 @@
|
|
|
5
5
|
* Applies delta specs from a change to main specs without archiving.
|
|
6
6
|
*/
|
|
7
7
|
export interface SpecUpdate {
|
|
8
|
+
/** Capability id relative to the specs root, forward-slash separated (e.g. "web" or "platform/session-layout"). */
|
|
9
|
+
id: string;
|
|
10
|
+
/** Allowed root for the delta source. */
|
|
11
|
+
sourceRoot: string;
|
|
8
12
|
source: string;
|
|
13
|
+
/** Allowed root for the main-spec target. */
|
|
14
|
+
targetRoot: string;
|
|
9
15
|
target: string;
|
|
10
16
|
exists: boolean;
|
|
11
17
|
}
|
|
12
|
-
export interface ApplyResult {
|
|
13
|
-
capability: string;
|
|
14
|
-
added: number;
|
|
15
|
-
modified: number;
|
|
16
|
-
removed: number;
|
|
17
|
-
renamed: number;
|
|
18
|
-
}
|
|
19
|
-
export interface SpecsApplyOutput {
|
|
20
|
-
changeName: string;
|
|
21
|
-
capabilities: ApplyResult[];
|
|
22
|
-
totals: {
|
|
23
|
-
added: number;
|
|
24
|
-
modified: number;
|
|
25
|
-
removed: number;
|
|
26
|
-
renamed: number;
|
|
27
|
-
};
|
|
28
|
-
noChanges: boolean;
|
|
29
|
-
}
|
|
30
18
|
/**
|
|
31
19
|
* Find all delta spec files that need to be applied from a change.
|
|
32
20
|
*/
|
|
@@ -45,7 +33,75 @@ export declare function buildUpdatedSpec(update: SpecUpdate, changeName: string,
|
|
|
45
33
|
removed: number;
|
|
46
34
|
renamed: number;
|
|
47
35
|
};
|
|
36
|
+
warnings: string[];
|
|
37
|
+
/**
|
|
38
|
+
* Every canonical `### Requirement:` block the delta could act on is gone.
|
|
39
|
+
* This is only a *candidate* signal for retirement (#1302): the validator, not
|
|
40
|
+
* this count, decides whether `rebuilt` is actually unwritable - it recognises
|
|
41
|
+
* requirement shapes this parser sweeps into the preamble, so a spec can be
|
|
42
|
+
* blockless here and still validate. See `isRetirableSpec` in archive.ts.
|
|
43
|
+
*/
|
|
44
|
+
noRequirementBlocks: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Every non-blank line of the spec this merge cannot name.
|
|
47
|
+
*
|
|
48
|
+
* Retirement deletes the whole file, so the only safe question is whether the
|
|
49
|
+
* merge can account for all of it. `extractRequirementsSection` splits a spec
|
|
50
|
+
* into five slices, and auditing a subset is how this guard kept failing: for
|
|
51
|
+
* seven rounds it looked for requirement-SHAPED text and was beaten by a new
|
|
52
|
+
* disguise each time, and when it started asking where content landed it
|
|
53
|
+
* still read only the preamble and the tail - so content simply moved into a
|
|
54
|
+
* slice nobody checked, and authored prose sitting inside a removed block's
|
|
55
|
+
* raw was deleted while the report said only "Purpose" was lost.
|
|
56
|
+
*
|
|
57
|
+
* So this accounts for the whole file: the title, the `## Purpose` section,
|
|
58
|
+
* the `## Requirements` header, and, inside each requirement block, the parts
|
|
59
|
+
* that make up a requirement - its header, its statement, and its scenarios'
|
|
60
|
+
* bullets. Every other non-blank line is reported and refuses the retirement.
|
|
61
|
+
*
|
|
62
|
+
* Fails safe in every direction: a line this cannot classify counts as
|
|
63
|
+
* unaccounted, which refuses rather than deletes.
|
|
64
|
+
*/
|
|
65
|
+
unaccountedContent: string[];
|
|
48
66
|
}>;
|
|
67
|
+
/**
|
|
68
|
+
* Retire a capability whose last requirement a delta removed: delete its main
|
|
69
|
+
* spec and prune any directories the deletion leaves empty. Returns false when
|
|
70
|
+
* there was nothing to delete.
|
|
71
|
+
*
|
|
72
|
+
* Gated by the caller on the change's `retire_capabilities` marker, so the one
|
|
73
|
+
* archive action that removes a file from `openspec/specs/` is always something
|
|
74
|
+
* the author asked for rather than something inferred from a delta's shape. The
|
|
75
|
+
* file is recoverable from git, which the report names; applying REMOVED already
|
|
76
|
+
* deletes requirement content from a main spec, so deleting the spec once
|
|
77
|
+
* nothing is left is the same operation carried to its end rather than a new
|
|
78
|
+
* kind of act.
|
|
79
|
+
*
|
|
80
|
+
* Only the generated `spec.md` is removed - a directory holding anything else (a
|
|
81
|
+
* nested capability, a hand-kept note) is left in place.
|
|
82
|
+
*
|
|
83
|
+
* The target must resolve inside the selected specs root. A capability-directory
|
|
84
|
+
* symlink must not turn a retirement marker into authorization to delete an
|
|
85
|
+
* unrelated external file. A symlinked `spec.md` itself is safe: unlink removes
|
|
86
|
+
* the link and leaves its target alone.
|
|
87
|
+
*
|
|
88
|
+
* Directory pruning IS bounded, by REAL paths rather than string prefixes:
|
|
89
|
+
* `path.resolve` collapses `..` but does not resolve symlinks, and `readdir` and
|
|
90
|
+
* `rmdir` both follow them, so a symlinked capability directory would otherwise
|
|
91
|
+
* let the walk delete directories outside the specs root entirely.
|
|
92
|
+
*/
|
|
93
|
+
export declare function retireSpec(update: SpecUpdate, mainSpecsDir: string, options?: {
|
|
94
|
+
silent?: boolean;
|
|
95
|
+
displayPath?: string;
|
|
96
|
+
beforeMutate?: () => Promise<void>;
|
|
97
|
+
verifyDisplaced?: (displacedPath: string) => Promise<void>;
|
|
98
|
+
deferDelete?: boolean;
|
|
99
|
+
}): Promise<{
|
|
100
|
+
retired: boolean;
|
|
101
|
+
resolvedPath?: string;
|
|
102
|
+
displacedPath?: string;
|
|
103
|
+
}>;
|
|
104
|
+
export declare function finalizeRetiredSpec(target: string, displacedPath: string, mainSpecsDir: string): Promise<void>;
|
|
49
105
|
/**
|
|
50
106
|
* Write an updated spec to disk.
|
|
51
107
|
*/
|
|
@@ -57,22 +113,13 @@ export declare function writeUpdatedSpec(update: SpecUpdate, rebuilt: string, co
|
|
|
57
113
|
}, options?: {
|
|
58
114
|
silent?: boolean;
|
|
59
115
|
displayPath?: string;
|
|
116
|
+
beforeMutate?: () => Promise<void>;
|
|
60
117
|
}): Promise<void>;
|
|
61
118
|
/**
|
|
62
|
-
* Build a skeleton spec for new capabilities.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
* Apply all delta specs from a change to main specs.
|
|
67
|
-
*
|
|
68
|
-
* @param projectRoot - The project root directory
|
|
69
|
-
* @param changeName - The name of the change to apply
|
|
70
|
-
* @param options - Options for the operation
|
|
71
|
-
* @returns Result of the operation with counts
|
|
119
|
+
* Build a skeleton spec for new capabilities. When the delta spec authored a
|
|
120
|
+
* `## Purpose`, carry it over instead of the TBD placeholder (#1413) - archive
|
|
121
|
+
* invents the Purpose for a brand-new main spec either way, and the author's
|
|
122
|
+
* own wording beats a placeholder they then have to hand-edit.
|
|
72
123
|
*/
|
|
73
|
-
export declare function
|
|
74
|
-
dryRun?: boolean;
|
|
75
|
-
skipValidation?: boolean;
|
|
76
|
-
silent?: boolean;
|
|
77
|
-
}): Promise<SpecsApplyOutput>;
|
|
124
|
+
export declare function buildSpecSkeleton(specFolderName: string, changeName: string, purpose?: string): string;
|
|
78
125
|
//# sourceMappingURL=specs-apply.d.ts.map
|