@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
|
@@ -11,6 +11,14 @@ export interface RequirementsSectionParts {
|
|
|
11
11
|
after: string;
|
|
12
12
|
}
|
|
13
13
|
export declare function normalizeRequirementName(name: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Case- and whitespace-insensitive fold of a requirement name. Requirement
|
|
16
|
+
* matching itself is case-sensitive (normalizeRequirementName); this fold
|
|
17
|
+
* exists only for typo detection - near-miss REMOVED headers and the
|
|
18
|
+
* RENAMED+REMOVED cross-section conflict - where two spellings that differ
|
|
19
|
+
* only in case or interior whitespace mean a mistake, never two requirements.
|
|
20
|
+
*/
|
|
21
|
+
export declare function foldRequirementName(name: string): string;
|
|
14
22
|
/**
|
|
15
23
|
* Extracts the Requirements section from a spec file and parses requirement blocks.
|
|
16
24
|
*/
|
|
@@ -45,4 +53,14 @@ export interface DeltaPlan {
|
|
|
45
53
|
* Parse a delta-formatted spec change file content into a DeltaPlan with raw blocks.
|
|
46
54
|
*/
|
|
47
55
|
export declare function parseDeltaSpec(content: string): DeltaPlan;
|
|
56
|
+
/**
|
|
57
|
+
* Scenario names the current requirement block has and the incoming
|
|
58
|
+
* (MODIFIED) block does not. A MODIFIED requirement replaces the whole block,
|
|
59
|
+
* so every name reported here would be dropped from the main spec.
|
|
60
|
+
*
|
|
61
|
+
* Shared by archive (which refuses to apply the block) and validate (which
|
|
62
|
+
* reports the same loss at authoring time, #1477), so the two cannot disagree
|
|
63
|
+
* about what counts as a dropped scenario.
|
|
64
|
+
*/
|
|
65
|
+
export declare function findMissingCurrentScenarios(current: RequirementBlock, incoming: RequirementBlock): string[];
|
|
48
66
|
//# sourceMappingURL=requirement-blocks.d.ts.map
|
|
@@ -2,6 +2,16 @@ import { buildCodeFenceMask } from './requirement-text.js';
|
|
|
2
2
|
export function normalizeRequirementName(name) {
|
|
3
3
|
return name.trim();
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Case- and whitespace-insensitive fold of a requirement name. Requirement
|
|
7
|
+
* matching itself is case-sensitive (normalizeRequirementName); this fold
|
|
8
|
+
* exists only for typo detection - near-miss REMOVED headers and the
|
|
9
|
+
* RENAMED+REMOVED cross-section conflict - where two spellings that differ
|
|
10
|
+
* only in case or interior whitespace mean a mistake, never two requirements.
|
|
11
|
+
*/
|
|
12
|
+
export function foldRequirementName(name) {
|
|
13
|
+
return normalizeRequirementName(name).toLowerCase().replace(/\s+/g, ' ');
|
|
14
|
+
}
|
|
5
15
|
/** The canonical requirement header the delta reader recognizes. */
|
|
6
16
|
const REQUIREMENT_HEADER_REGEX = /^###\s*Requirement:\s*(.+)\s*$/i;
|
|
7
17
|
/**
|
|
@@ -10,7 +20,8 @@ const REQUIREMENT_HEADER_REGEX = /^###\s*Requirement:\s*(.+)\s*$/i;
|
|
|
10
20
|
export function extractRequirementsSection(content) {
|
|
11
21
|
const normalized = normalizeLineEndings(content);
|
|
12
22
|
const lines = normalized.split('\n');
|
|
13
|
-
const
|
|
23
|
+
const fenceMask = buildCodeFenceMask(lines);
|
|
24
|
+
const reqHeaderIndex = lines.findIndex((l, i) => !fenceMask[i] && /^##\s+Requirements\s*$/i.test(l));
|
|
14
25
|
if (reqHeaderIndex === -1) {
|
|
15
26
|
// No requirements section; create an empty one at the end
|
|
16
27
|
const before = content.trimEnd();
|
|
@@ -26,7 +37,7 @@ export function extractRequirementsSection(content) {
|
|
|
26
37
|
// Find end of this section: next line that starts with '## ' at same or higher level
|
|
27
38
|
let endIndex = lines.length;
|
|
28
39
|
for (let i = reqHeaderIndex + 1; i < lines.length; i++) {
|
|
29
|
-
if (/^##\s+/.test(lines[i])) {
|
|
40
|
+
if (!fenceMask[i] && /^##\s+/.test(lines[i])) {
|
|
30
41
|
endIndex = i;
|
|
31
42
|
break;
|
|
32
43
|
}
|
|
@@ -34,29 +45,31 @@ export function extractRequirementsSection(content) {
|
|
|
34
45
|
const before = lines.slice(0, reqHeaderIndex).join('\n');
|
|
35
46
|
const headerLine = lines[reqHeaderIndex];
|
|
36
47
|
const sectionBodyLines = lines.slice(reqHeaderIndex + 1, endIndex);
|
|
48
|
+
const sectionBodyMask = fenceMask.slice(reqHeaderIndex + 1, endIndex);
|
|
49
|
+
const isRequirementHeader = (cursor) => !sectionBodyMask[cursor] && REQUIREMENT_HEADER_REGEX.test(sectionBodyLines[cursor]);
|
|
50
|
+
const isTopLevelHeader = (cursor) => !sectionBodyMask[cursor] && /^##\s+/.test(sectionBodyLines[cursor]);
|
|
37
51
|
// Parse requirement blocks within section body
|
|
38
52
|
const blocks = [];
|
|
39
53
|
let cursor = 0;
|
|
40
54
|
let preambleLines = [];
|
|
41
55
|
// Collect preamble lines until first requirement header
|
|
42
|
-
while (cursor < sectionBodyLines.length && !
|
|
56
|
+
while (cursor < sectionBodyLines.length && !isRequirementHeader(cursor)) {
|
|
43
57
|
preambleLines.push(sectionBodyLines[cursor]);
|
|
44
58
|
cursor++;
|
|
45
59
|
}
|
|
46
60
|
while (cursor < sectionBodyLines.length) {
|
|
47
|
-
const headerStart = cursor;
|
|
48
61
|
const headerLineCandidate = sectionBodyLines[cursor];
|
|
49
|
-
|
|
50
|
-
if (!headerMatch) {
|
|
62
|
+
if (!isRequirementHeader(cursor)) {
|
|
51
63
|
// Not a requirement header; skip line defensively
|
|
52
64
|
cursor++;
|
|
53
65
|
continue;
|
|
54
66
|
}
|
|
67
|
+
const headerMatch = headerLineCandidate.match(REQUIREMENT_HEADER_REGEX);
|
|
55
68
|
const name = normalizeRequirementName(headerMatch[1]);
|
|
56
69
|
cursor++;
|
|
57
70
|
// Gather lines until next requirement header or end of section
|
|
58
71
|
const bodyLines = [headerLineCandidate];
|
|
59
|
-
while (cursor < sectionBodyLines.length && !
|
|
72
|
+
while (cursor < sectionBodyLines.length && !isRequirementHeader(cursor) && !isTopLevelHeader(cursor)) {
|
|
60
73
|
bodyLines.push(sectionBodyLines[cursor]);
|
|
61
74
|
cursor++;
|
|
62
75
|
}
|
|
@@ -74,14 +87,18 @@ export function extractRequirementsSection(content) {
|
|
|
74
87
|
};
|
|
75
88
|
}
|
|
76
89
|
function normalizeLineEndings(content) {
|
|
77
|
-
|
|
90
|
+
// Strip a UTF-8 BOM: Windows editors and PowerShell redirects prepend one,
|
|
91
|
+
// and it would keep the first line's `## ADDED Requirements` from matching.
|
|
92
|
+
return content.replace(/^/, '').replace(/\r\n?/g, '\n');
|
|
78
93
|
}
|
|
79
94
|
/**
|
|
80
95
|
* Parse a delta-formatted spec change file content into a DeltaPlan with raw blocks.
|
|
81
96
|
*/
|
|
82
97
|
export function parseDeltaSpec(content) {
|
|
83
98
|
const normalized = normalizeLineEndings(content);
|
|
84
|
-
const
|
|
99
|
+
const lines = normalized.split('\n');
|
|
100
|
+
const fenceMask = buildCodeFenceMask(lines);
|
|
101
|
+
const sections = splitTopLevelSections(lines, fenceMask);
|
|
85
102
|
const addedLookup = getSectionCaseInsensitive(sections, 'ADDED Requirements');
|
|
86
103
|
const modifiedLookup = getSectionCaseInsensitive(sections, 'MODIFIED Requirements');
|
|
87
104
|
const removedLookup = getSectionCaseInsensitive(sections, 'REMOVED Requirements');
|
|
@@ -114,43 +131,45 @@ export function parseDeltaSpec(content) {
|
|
|
114
131
|
},
|
|
115
132
|
};
|
|
116
133
|
}
|
|
117
|
-
function splitTopLevelSections(
|
|
118
|
-
const lines = content.split('\n');
|
|
134
|
+
function splitTopLevelSections(lines, fenceMask) {
|
|
119
135
|
const result = {};
|
|
120
136
|
const indices = [];
|
|
121
137
|
for (let i = 0; i < lines.length; i++) {
|
|
138
|
+
if (fenceMask[i])
|
|
139
|
+
continue;
|
|
122
140
|
const m = lines[i].match(/^(##)\s+(.+)$/);
|
|
123
141
|
if (m) {
|
|
124
|
-
|
|
125
|
-
indices.push({ title: m[2].trim(), index: i, level });
|
|
142
|
+
indices.push({ title: m[2].trim(), index: i });
|
|
126
143
|
}
|
|
127
144
|
}
|
|
128
145
|
for (let i = 0; i < indices.length; i++) {
|
|
129
146
|
const current = indices[i];
|
|
130
147
|
const next = indices[i + 1];
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
148
|
+
const end = next ? next.index : lines.length;
|
|
149
|
+
result[current.title] = {
|
|
150
|
+
lines: lines.slice(current.index + 1, end),
|
|
151
|
+
fenceMask: fenceMask.slice(current.index + 1, end),
|
|
152
|
+
bodyStartLine: current.index + 2,
|
|
153
|
+
};
|
|
134
154
|
}
|
|
135
155
|
return result;
|
|
136
156
|
}
|
|
157
|
+
const EMPTY_SECTION_BODY = { lines: [], fenceMask: [], bodyStartLine: 0 };
|
|
137
158
|
function getSectionCaseInsensitive(sections, desired) {
|
|
138
159
|
const target = desired.toLowerCase();
|
|
139
|
-
for (const [title,
|
|
140
|
-
if (title.toLowerCase() === target)
|
|
141
|
-
return { title, body, bodyStartLine, found: true };
|
|
160
|
+
for (const [title, body] of Object.entries(sections)) {
|
|
161
|
+
if (title.toLowerCase() === target) {
|
|
162
|
+
return { title, body, bodyStartLine: body.bodyStartLine, found: true };
|
|
163
|
+
}
|
|
142
164
|
}
|
|
143
|
-
return { title: desired, body:
|
|
165
|
+
return { title: desired, body: EMPTY_SECTION_BODY, bodyStartLine: 0, found: false };
|
|
144
166
|
}
|
|
145
167
|
function parseRequirementBlocksFromSection(sectionBody, skipped) {
|
|
146
|
-
|
|
168
|
+
const { lines, fenceMask } = sectionBody;
|
|
169
|
+
if (lines.length === 0)
|
|
147
170
|
return [];
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
// it skips them, so the INFO note describes the reader's real boundaries.
|
|
151
|
-
// Fence-masked lines are excluded: the body reader treats them as fenced
|
|
152
|
-
// content, not as headers.
|
|
153
|
-
const fenceMask = skipped ? buildCodeFenceMask(lines) : undefined;
|
|
171
|
+
const isRequirementHeader = (i) => !fenceMask[i] && REQUIREMENT_HEADER_REGEX.test(lines[i]);
|
|
172
|
+
const isTopLevelHeader = (i) => !fenceMask[i] && /^##\s+/.test(lines[i]);
|
|
154
173
|
const recordIfSkippedHeader = (index) => {
|
|
155
174
|
if (!skipped || fenceMask[index])
|
|
156
175
|
return;
|
|
@@ -167,7 +186,7 @@ function parseRequirementBlocksFromSection(sectionBody, skipped) {
|
|
|
167
186
|
let i = 0;
|
|
168
187
|
while (i < lines.length) {
|
|
169
188
|
// Seek next requirement header
|
|
170
|
-
while (i < lines.length && !
|
|
189
|
+
while (i < lines.length && !isRequirementHeader(i)) {
|
|
171
190
|
recordIfSkippedHeader(i);
|
|
172
191
|
i++;
|
|
173
192
|
}
|
|
@@ -182,7 +201,7 @@ function parseRequirementBlocksFromSection(sectionBody, skipped) {
|
|
|
182
201
|
const name = normalizeRequirementName(m[1]);
|
|
183
202
|
const buf = [headerLine];
|
|
184
203
|
i++;
|
|
185
|
-
while (i < lines.length && !
|
|
204
|
+
while (i < lines.length && !isRequirementHeader(i) && !isTopLevelHeader(i)) {
|
|
186
205
|
recordIfSkippedHeader(i);
|
|
187
206
|
buf.push(lines[i]);
|
|
188
207
|
i++;
|
|
@@ -192,11 +211,14 @@ function parseRequirementBlocksFromSection(sectionBody, skipped) {
|
|
|
192
211
|
return blocks;
|
|
193
212
|
}
|
|
194
213
|
function parseRemovedNames(sectionBody) {
|
|
195
|
-
|
|
214
|
+
const { lines, fenceMask } = sectionBody;
|
|
215
|
+
if (lines.length === 0)
|
|
196
216
|
return [];
|
|
197
217
|
const names = [];
|
|
198
|
-
|
|
199
|
-
|
|
218
|
+
for (let i = 0; i < lines.length; i++) {
|
|
219
|
+
if (fenceMask[i])
|
|
220
|
+
continue;
|
|
221
|
+
const line = lines[i];
|
|
200
222
|
const m = line.match(REQUIREMENT_HEADER_REGEX);
|
|
201
223
|
if (m) {
|
|
202
224
|
names.push(normalizeRequirementName(m[1]));
|
|
@@ -211,12 +233,15 @@ function parseRemovedNames(sectionBody) {
|
|
|
211
233
|
return names;
|
|
212
234
|
}
|
|
213
235
|
function parseRenamedPairs(sectionBody) {
|
|
214
|
-
|
|
236
|
+
const { lines, fenceMask } = sectionBody;
|
|
237
|
+
if (lines.length === 0)
|
|
215
238
|
return [];
|
|
216
239
|
const pairs = [];
|
|
217
|
-
const lines = normalizeLineEndings(sectionBody).split('\n');
|
|
218
240
|
let current = {};
|
|
219
|
-
for (
|
|
241
|
+
for (let i = 0; i < lines.length; i++) {
|
|
242
|
+
if (fenceMask[i])
|
|
243
|
+
continue;
|
|
244
|
+
const line = lines[i];
|
|
220
245
|
const fromMatch = line.match(/^\s*-?\s*FROM:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
|
|
221
246
|
const toMatch = line.match(/^\s*-?\s*TO:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
|
|
222
247
|
if (fromMatch) {
|
|
@@ -232,4 +257,64 @@ function parseRenamedPairs(sectionBody) {
|
|
|
232
257
|
}
|
|
233
258
|
return pairs;
|
|
234
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Scenario names the current requirement block has and the incoming
|
|
262
|
+
* (MODIFIED) block does not. A MODIFIED requirement replaces the whole block,
|
|
263
|
+
* so every name reported here would be dropped from the main spec.
|
|
264
|
+
*
|
|
265
|
+
* Shared by archive (which refuses to apply the block) and validate (which
|
|
266
|
+
* reports the same loss at authoring time, #1477), so the two cannot disagree
|
|
267
|
+
* about what counts as a dropped scenario.
|
|
268
|
+
*/
|
|
269
|
+
export function findMissingCurrentScenarios(current, incoming) {
|
|
270
|
+
// Multiplicity-aware: a name present N times in current and M times in
|
|
271
|
+
// incoming means max(0, N - M) instances are missing. Set membership would
|
|
272
|
+
// treat N>M as fully covered and let archive silently drop duplicates
|
|
273
|
+
// (residual #1246 / duplicate-scenario-name blind spot).
|
|
274
|
+
const remainingIncoming = new Map();
|
|
275
|
+
for (const scenario of parseScenarioBlocks(incoming.raw)) {
|
|
276
|
+
const name = scenario.name;
|
|
277
|
+
remainingIncoming.set(name, (remainingIncoming.get(name) ?? 0) + 1);
|
|
278
|
+
}
|
|
279
|
+
const missing = [];
|
|
280
|
+
for (const scenario of parseScenarioBlocks(current.raw)) {
|
|
281
|
+
const name = scenario.name;
|
|
282
|
+
const remaining = remainingIncoming.get(name) ?? 0;
|
|
283
|
+
if (remaining > 0) {
|
|
284
|
+
remainingIncoming.set(name, remaining - 1);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
missing.push(name);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return missing;
|
|
291
|
+
}
|
|
292
|
+
function parseScenarioBlocks(requirementRaw) {
|
|
293
|
+
const lines = requirementRaw.replace(/\r\n?/g, '\n').split('\n');
|
|
294
|
+
// A `#### Scenario:` inside a fenced example is not a real scenario. The
|
|
295
|
+
// validator's countScenarios already ignores fenced lines; the drift check
|
|
296
|
+
// must agree with it, or a fenced sample can false-abort an archive (or
|
|
297
|
+
// mask a genuinely dropped scenario).
|
|
298
|
+
const mask = buildCodeFenceMask(lines);
|
|
299
|
+
const scenarios = [];
|
|
300
|
+
let index = 0;
|
|
301
|
+
while (index < lines.length) {
|
|
302
|
+
const headerMatch = mask[index] ? null : lines[index].match(/^####\s*Scenario:\s*(.+)\s*$/);
|
|
303
|
+
if (!headerMatch) {
|
|
304
|
+
index++;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const start = index;
|
|
308
|
+
const name = headerMatch[1].trim();
|
|
309
|
+
index++;
|
|
310
|
+
while (index < lines.length && (mask[index] || !/^####\s*Scenario:\s*(.+)\s*$/.test(lines[index]))) {
|
|
311
|
+
index++;
|
|
312
|
+
}
|
|
313
|
+
scenarios.push({
|
|
314
|
+
name,
|
|
315
|
+
raw: lines.slice(start, index).join('\n').trimEnd(),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return scenarios;
|
|
319
|
+
}
|
|
235
320
|
//# sourceMappingURL=requirement-blocks.js.map
|
|
@@ -8,14 +8,7 @@
|
|
|
8
8
|
* extraction, scenario counting, and `SHALL`/`MUST` detection in
|
|
9
9
|
* `validate <change>`, `validate <spec>`, and `archive`.
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
* Build a per-line mask marking lines that fall inside a fenced code block
|
|
13
|
-
* (``` ``` ``` or ``` ~~~ ```), including the fence lines themselves. Mirrors the
|
|
14
|
-
* fence rules markdown uses: a fence opens on the first ```` ```/~~~ ```` of
|
|
15
|
-
* length >= 3 and closes on a line of the same marker whose length is >= the
|
|
16
|
-
* opening length, with nothing but whitespace after it.
|
|
17
|
-
*/
|
|
18
|
-
export declare function buildCodeFenceMask(lines: string[]): boolean[];
|
|
11
|
+
export { buildCodeFenceMask } from './code-fence.js';
|
|
19
12
|
/**
|
|
20
13
|
* The one predicate for normative-keyword detection. Matches `SHALL` or `MUST`
|
|
21
14
|
* as whole words so the change-delta reader and the schema-based reader accept
|
|
@@ -8,48 +8,10 @@
|
|
|
8
8
|
* extraction, scenario counting, and `SHALL`/`MUST` detection in
|
|
9
9
|
* `validate <change>`, `validate <spec>`, and `archive`.
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* length >= 3 and closes on a line of the same marker whose length is >= the
|
|
16
|
-
* opening length, with nothing but whitespace after it.
|
|
17
|
-
*/
|
|
18
|
-
export function buildCodeFenceMask(lines) {
|
|
19
|
-
const mask = new Array(lines.length).fill(false);
|
|
20
|
-
let activeFence = null;
|
|
21
|
-
for (let i = 0; i < lines.length; i++) {
|
|
22
|
-
const fence = getFenceMarker(lines[i]);
|
|
23
|
-
if (!activeFence) {
|
|
24
|
-
if (fence) {
|
|
25
|
-
activeFence = fence;
|
|
26
|
-
mask[i] = true;
|
|
27
|
-
}
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
mask[i] = true;
|
|
31
|
-
if (isClosingFence(lines[i], activeFence)) {
|
|
32
|
-
activeFence = null;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return mask;
|
|
36
|
-
}
|
|
37
|
-
function getFenceMarker(line) {
|
|
38
|
-
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})/);
|
|
39
|
-
if (!fenceMatch) {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
marker: fenceMatch[1][0],
|
|
44
|
-
length: fenceMatch[1].length,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
function isClosingFence(line, activeFence) {
|
|
48
|
-
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})\s*$/);
|
|
49
|
-
return Boolean(fenceMatch &&
|
|
50
|
-
fenceMatch[1][0] === activeFence.marker &&
|
|
51
|
-
fenceMatch[1].length >= activeFence.length);
|
|
52
|
-
}
|
|
11
|
+
// Re-exported so existing importers keep working; the single implementation
|
|
12
|
+
// lives in code-fence.ts.
|
|
13
|
+
export { buildCodeFenceMask } from './code-fence.js';
|
|
14
|
+
import { buildCodeFenceMask } from './code-fence.js';
|
|
53
15
|
/** Lines that look like `**ID**: ...` / `**Priority**: ...` metadata. */
|
|
54
16
|
const METADATA_LINE = /^\*\*[^*]+\*\*:/;
|
|
55
17
|
/** Any markdown header line — the boundary where a requirement body ends. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildCodeFenceMask } from './code-fence.js';
|
|
1
2
|
const REQUIREMENTS_SECTION_HEADER = /^##\s+Requirements\s*$/i;
|
|
2
3
|
const TOP_LEVEL_SECTION_HEADER = /^##\s+/;
|
|
3
4
|
const DELTA_HEADER = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\s*$/i;
|
|
@@ -7,6 +8,7 @@ export function findMainSpecStructureIssues(content) {
|
|
|
7
8
|
const stripped = stripFencedCodeBlocksPreservingLines(normalized);
|
|
8
9
|
const lines = stripped.split('\n');
|
|
9
10
|
const issues = [];
|
|
11
|
+
const requirementLines = new Map();
|
|
10
12
|
const requirementsHeaderIndex = lines.findIndex(line => REQUIREMENTS_SECTION_HEADER.test(line));
|
|
11
13
|
let requirementsEndIndex = lines.length;
|
|
12
14
|
if (requirementsHeaderIndex !== -1) {
|
|
@@ -29,7 +31,7 @@ export function findMainSpecStructureIssues(content) {
|
|
|
29
31
|
line: i + 1,
|
|
30
32
|
header: trimmed,
|
|
31
33
|
message: `Main spec contains delta header "${trimmed}". ` +
|
|
32
|
-
'Delta headers are only valid inside openspec/changes/<name>/specs/<capability>/spec.md ' +
|
|
34
|
+
'Delta headers are only valid inside openspec/changes/<name>/specs/<capability-path>/spec.md ' +
|
|
33
35
|
'and truncate the parsed ## Requirements section.',
|
|
34
36
|
});
|
|
35
37
|
continue;
|
|
@@ -49,40 +51,28 @@ export function findMainSpecStructureIssues(content) {
|
|
|
49
51
|
message: `Requirement header "${trimmed}" appears outside the main ## Requirements section. ` +
|
|
50
52
|
'Main specs only parse requirements inside that section, so this requirement is currently invisible to validate, list, and archive.',
|
|
51
53
|
});
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const requirementName = requirementMatch[1].trim();
|
|
57
|
+
const previousLine = requirementLines.get(requirementName);
|
|
58
|
+
if (previousLine !== undefined) {
|
|
59
|
+
issues.push({
|
|
60
|
+
kind: 'duplicate-requirement',
|
|
61
|
+
line: i + 1,
|
|
62
|
+
header: trimmed,
|
|
63
|
+
message: `Requirement header "${trimmed}" duplicates the requirement declared on line ${previousLine}. ` +
|
|
64
|
+
'Requirement names must be unique so spec updates cannot discard one block while updating another.',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
requirementLines.set(requirementName, i + 1);
|
|
52
69
|
}
|
|
53
70
|
}
|
|
54
71
|
return issues;
|
|
55
72
|
}
|
|
56
73
|
export function stripFencedCodeBlocksPreservingLines(content) {
|
|
57
74
|
const lines = content.split('\n');
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
for (const line of lines) {
|
|
61
|
-
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})(.*)$/);
|
|
62
|
-
if (!activeFence) {
|
|
63
|
-
if (fenceMatch) {
|
|
64
|
-
activeFence = {
|
|
65
|
-
marker: fenceMatch[1][0],
|
|
66
|
-
length: fenceMatch[1].length,
|
|
67
|
-
};
|
|
68
|
-
output.push('');
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
output.push(line);
|
|
72
|
-
}
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
output.push('');
|
|
76
|
-
if (isClosingFence(line, activeFence)) {
|
|
77
|
-
activeFence = null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return output.join('\n');
|
|
81
|
-
}
|
|
82
|
-
function isClosingFence(line, activeFence) {
|
|
83
|
-
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})\s*$/);
|
|
84
|
-
return Boolean(fenceMatch &&
|
|
85
|
-
fenceMatch[1][0] === activeFence.marker &&
|
|
86
|
-
fenceMatch[1].length >= activeFence.length);
|
|
75
|
+
const mask = buildCodeFenceMask(lines);
|
|
76
|
+
return lines.map((line, i) => (mask[i] ? '' : line)).join('\n');
|
|
87
77
|
}
|
|
88
78
|
//# sourceMappingURL=spec-structure.js.map
|
|
@@ -5,14 +5,6 @@ type WorkflowId = (typeof ALL_WORKFLOWS)[number];
|
|
|
5
5
|
* Maps workflow IDs to their skill directory names.
|
|
6
6
|
*/
|
|
7
7
|
export declare const WORKFLOW_TO_SKILL_DIR: Record<WorkflowId, string>;
|
|
8
|
-
/**
|
|
9
|
-
* Checks whether a tool has at least one generated OpenSpec command file.
|
|
10
|
-
*/
|
|
11
|
-
export declare function toolHasAnyConfiguredCommand(projectPath: string, toolId: string): boolean;
|
|
12
|
-
/**
|
|
13
|
-
* Returns tools with at least one generated command file on disk.
|
|
14
|
-
*/
|
|
15
|
-
export declare function getCommandConfiguredTools(projectPath: string): string[];
|
|
16
8
|
/**
|
|
17
9
|
* Returns tools that are configured via either skills or commands.
|
|
18
10
|
*/
|
|
@@ -3,7 +3,12 @@ import * as fs from 'fs';
|
|
|
3
3
|
import { AI_TOOLS } from './config.js';
|
|
4
4
|
import { ALL_WORKFLOWS } from './profiles.js';
|
|
5
5
|
import { CommandAdapterRegistry } from './command-generation/index.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getConfiguredTools } from './shared/index.js';
|
|
7
|
+
import { shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
|
|
8
|
+
import { readSharedSkillTarget } from './shared-skill-target.js';
|
|
9
|
+
import { FileSystemUtils } from '../utils/file-system.js';
|
|
10
|
+
import { isLegacyCodexSkillEquivalentToCurrent } from './shared/skill-content-equivalence.js';
|
|
11
|
+
import { hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, } from './shared/skill-paths.js';
|
|
7
12
|
/**
|
|
8
13
|
* Maps workflow IDs to their skill directory names.
|
|
9
14
|
*/
|
|
@@ -24,48 +29,11 @@ export const WORKFLOW_TO_SKILL_DIR = {
|
|
|
24
29
|
function toKnownWorkflows(workflows) {
|
|
25
30
|
return workflows.filter((workflow) => ALL_WORKFLOWS.includes(workflow));
|
|
26
31
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Checks whether a tool has at least one generated OpenSpec command file.
|
|
29
|
-
*/
|
|
30
|
-
export function toolHasAnyConfiguredCommand(projectPath, toolId) {
|
|
31
|
-
const adapter = CommandAdapterRegistry.get(toolId);
|
|
32
|
-
if (!adapter)
|
|
33
|
-
return false;
|
|
34
|
-
for (const commandId of COMMAND_IDS) {
|
|
35
|
-
const cmdPath = adapter.getFilePath(commandId);
|
|
36
|
-
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
|
|
37
|
-
if (fs.existsSync(fullPath)) {
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Returns tools with at least one generated command file on disk.
|
|
45
|
-
*/
|
|
46
|
-
export function getCommandConfiguredTools(projectPath) {
|
|
47
|
-
return AI_TOOLS
|
|
48
|
-
.filter((tool) => {
|
|
49
|
-
if (!tool.skillsDir)
|
|
50
|
-
return false;
|
|
51
|
-
const toolDir = path.join(projectPath, tool.skillsDir);
|
|
52
|
-
try {
|
|
53
|
-
return fs.statSync(toolDir).isDirectory();
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
.map((tool) => tool.value)
|
|
60
|
-
.filter((toolId) => toolHasAnyConfiguredCommand(projectPath, toolId));
|
|
61
|
-
}
|
|
62
32
|
/**
|
|
63
33
|
* Returns tools that are configured via either skills or commands.
|
|
64
34
|
*/
|
|
65
35
|
export function getConfiguredToolsForProfileSync(projectPath) {
|
|
66
|
-
|
|
67
|
-
const commandConfigured = getCommandConfiguredTools(projectPath);
|
|
68
|
-
return [...new Set([...skillConfigured, ...commandConfigured])];
|
|
36
|
+
return getConfiguredTools(projectPath);
|
|
69
37
|
}
|
|
70
38
|
/**
|
|
71
39
|
* Detects if a single tool has profile/delivery drift against the desired state.
|
|
@@ -77,14 +45,45 @@ export function getConfiguredToolsForProfileSync(projectPath) {
|
|
|
77
45
|
*/
|
|
78
46
|
export function hasToolProfileOrDeliveryDrift(projectPath, toolId, desiredWorkflows, delivery) {
|
|
79
47
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
80
|
-
if (!tool
|
|
48
|
+
if (!tool || !toolSupportsSkills(tool))
|
|
81
49
|
return false;
|
|
82
50
|
const knownDesiredWorkflows = toKnownWorkflows(desiredWorkflows);
|
|
83
51
|
const desiredWorkflowSet = new Set(knownDesiredWorkflows);
|
|
84
|
-
const skillsDir =
|
|
52
|
+
const skillsDir = resolveToolSkillsDir(projectPath, tool);
|
|
85
53
|
const adapter = CommandAdapterRegistry.get(toolId);
|
|
86
|
-
const shouldGenerateSkills = delivery
|
|
87
|
-
const shouldGenerateCommands = delivery
|
|
54
|
+
const shouldGenerateSkills = shouldGenerateSkillsForTool(toolId, delivery);
|
|
55
|
+
const shouldGenerateCommands = shouldGenerateCommandsForTool(toolId, delivery);
|
|
56
|
+
const sharedTarget = tool.skillsDir
|
|
57
|
+
? readSharedSkillTarget(projectPath, tool.skillsDir)
|
|
58
|
+
: undefined;
|
|
59
|
+
for (const root of tool.legacySkillsDirs ?? []) {
|
|
60
|
+
for (const workflow of knownDesiredWorkflows) {
|
|
61
|
+
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
62
|
+
const legacySkill = path.join(projectPath, root, 'skills', dirName, 'SKILL.md');
|
|
63
|
+
if (!fs.existsSync(legacySkill))
|
|
64
|
+
continue;
|
|
65
|
+
const currentSkill = path.join(skillsDir, dirName, 'SKILL.md');
|
|
66
|
+
if (!fs.existsSync(currentSkill) || sharedTarget !== toolId) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
if (FileSystemUtils.canonicalizeExistingPath(legacySkill) ===
|
|
71
|
+
FileSystemUtils.canonicalizeExistingPath(currentSkill)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// Equivalent generated copies are actionable: migration can safely
|
|
75
|
+
// remove the redundant legacy file even when version, line endings,
|
|
76
|
+
// or supported invocation syntax changed. Materially divergent copies
|
|
77
|
+
// stay in place without forcing an update on every run.
|
|
78
|
+
if (isLegacyCodexSkillEquivalentToCurrent(fs.readFileSync(legacySkill, 'utf-8'), fs.readFileSync(currentSkill, 'utf-8'))) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
88
87
|
if (shouldGenerateSkills) {
|
|
89
88
|
for (const workflow of knownDesiredWorkflows) {
|
|
90
89
|
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
@@ -104,7 +103,7 @@ export function hasToolProfileOrDeliveryDrift(projectPath, toolId, desiredWorkfl
|
|
|
104
103
|
}
|
|
105
104
|
}
|
|
106
105
|
}
|
|
107
|
-
else {
|
|
106
|
+
else if (shouldRemoveSkillsForTool(toolId, delivery) && !hasGlobalSkillTarget(tool)) {
|
|
108
107
|
for (const workflow of ALL_WORKFLOWS) {
|
|
109
108
|
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
110
109
|
const skillDir = path.join(skillsDir, dirName);
|
|
@@ -132,7 +131,7 @@ export function hasToolProfileOrDeliveryDrift(projectPath, toolId, desiredWorkfl
|
|
|
132
131
|
}
|
|
133
132
|
}
|
|
134
133
|
}
|
|
135
|
-
else if (
|
|
134
|
+
else if (shouldReconcileCommandFilesForTool(toolId, delivery) && adapter) {
|
|
136
135
|
for (const workflow of ALL_WORKFLOWS) {
|
|
137
136
|
const cmdPath = adapter.getFilePath(workflow);
|
|
138
137
|
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
|
|
@@ -152,10 +151,10 @@ export function getToolsNeedingProfileSync(projectPath, desiredWorkflows, delive
|
|
|
152
151
|
}
|
|
153
152
|
function getInstalledWorkflowsForTool(projectPath, toolId, options) {
|
|
154
153
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
155
|
-
if (!tool
|
|
154
|
+
if (!tool || !toolSupportsSkills(tool))
|
|
156
155
|
return [];
|
|
157
156
|
const installed = new Set();
|
|
158
|
-
const skillsDir =
|
|
157
|
+
const skillsDir = resolveToolSkillsDir(projectPath, tool);
|
|
159
158
|
if (options.includeSkills) {
|
|
160
159
|
for (const workflow of ALL_WORKFLOWS) {
|
|
161
160
|
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
@@ -188,9 +187,9 @@ export function hasProjectConfigDrift(projectPath, desiredWorkflows, delivery) {
|
|
|
188
187
|
return true;
|
|
189
188
|
}
|
|
190
189
|
const desiredSet = new Set(toKnownWorkflows(desiredWorkflows));
|
|
191
|
-
const includeSkills = delivery !== 'commands';
|
|
192
|
-
const includeCommands = delivery !== 'skills';
|
|
193
190
|
for (const toolId of configuredTools) {
|
|
191
|
+
const includeSkills = shouldGenerateSkillsForTool(toolId, delivery);
|
|
192
|
+
const includeCommands = shouldGenerateCommandsForTool(toolId, delivery);
|
|
194
193
|
const installed = getInstalledWorkflowsForTool(projectPath, toolId, { includeSkills, includeCommands });
|
|
195
194
|
if (installed.some((workflow) => !desiredSet.has(workflow))) {
|
|
196
195
|
return true;
|