@fission-ai/openspec 1.7.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 +3 -1
- package/dist/cli/index.js +6 -1
- package/dist/commands/change.js +15 -1
- package/dist/commands/schema.js +89 -20
- package/dist/commands/spec.js +30 -6
- package/dist/commands/validate.js +2 -2
- package/dist/commands/workflow/instructions.js +32 -27
- package/dist/commands/workflow/status.js +2 -2
- package/dist/commands/workflow/templates.js +15 -5
- package/dist/core/archive.d.ts +14 -0
- package/dist/core/archive.js +1265 -151
- 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 +3 -1
- package/dist/core/artifact-graph/instruction-loader.js +15 -5
- 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.js +35 -9
- 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 +1 -0
- package/dist/core/change-metadata/schema.js +7 -0
- package/dist/core/change-status-policy.js +2 -2
- package/dist/core/completions/command-registry.js +8 -0
- package/dist/core/config-schema.d.ts +3 -0
- package/dist/core/config-schema.js +28 -1
- package/dist/core/config.d.ts +3 -0
- package/dist/core/config.js +24 -2
- 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 +11 -0
- package/dist/core/init.d.ts +23 -0
- package/dist/core/init.js +234 -39
- package/dist/core/legacy-cleanup.js +4 -2
- package/dist/core/migration.d.ts +6 -4
- package/dist/core/migration.js +78 -22
- package/dist/core/parsers/requirement-blocks.d.ts +10 -0
- package/dist/core/parsers/requirement-blocks.js +60 -0
- package/dist/core/parsers/spec-structure.d.ts +1 -1
- package/dist/core/parsers/spec-structure.js +17 -1
- package/dist/core/profile-sync-drift.js +40 -5
- package/dist/core/project-config.d.ts +3 -0
- package/dist/core/project-config.js +31 -1
- 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.js +66 -35
- 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 +72 -0
- package/dist/core/specs-apply.js +448 -55
- 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 +22 -182
- package/dist/core/templates/workflows/archive-change.js +8 -4
- package/dist/core/templates/workflows/bulk-archive-change.js +30 -26
- package/dist/core/templates/workflows/continue-change.js +6 -6
- package/dist/core/templates/workflows/explore.js +42 -18
- package/dist/core/templates/workflows/onboard.js +9 -3
- package/dist/core/templates/workflows/propose.js +72 -20
- 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 +96 -18
- package/dist/core/templates/workflows/update-change.js +10 -8
- package/dist/core/update.d.ts +1 -0
- package/dist/core/update.js +103 -24
- package/dist/core/validation/validator.d.ts +23 -3
- package/dist/core/validation/validator.js +134 -18
- package/dist/core/version-check.js +6 -11
- package/dist/telemetry/config.d.ts +3 -4
- package/dist/telemetry/index.d.ts +9 -4
- package/dist/telemetry/index.js +20 -8
- package/dist/utils/change-metadata.d.ts +17 -4
- package/dist/utils/change-metadata.js +26 -4
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +14 -2
- package/dist/utils/command-references.js +45 -3
- package/dist/utils/file-system.d.ts +10 -0
- package/dist/utils/file-system.js +70 -0
- package/dist/utils/interactive.d.ts +23 -0
- package/dist/utils/interactive.js +32 -0
- package/dist/utils/spec-discovery.d.ts +4 -4
- package/dist/utils/spec-discovery.js +19 -6
- package/dist/utils/task-progress.d.ts +17 -0
- package/dist/utils/task-progress.js +41 -13
- package/package.json +1 -1
- package/schemas/spec-driven/schema.yaml +8 -6
- package/schemas/spec-driven/templates/proposal.md +6 -4
|
@@ -9,6 +9,8 @@ This is an **agent-driven** operation - you will read delta specs and directly e
|
|
|
9
9
|
|
|
10
10
|
${STORE_SELECTION_GUIDANCE}
|
|
11
11
|
|
|
12
|
+
\`<capability-path>\` is the spec directory relative to \`specs/\` (for example, \`user-auth\` or \`identity/user-auth\`). Preserve the full path from each delta spec when resolving its main spec.
|
|
13
|
+
|
|
12
14
|
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
|
13
15
|
|
|
14
16
|
**Steps**
|
|
@@ -42,8 +44,10 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
42
44
|
instructions or writing a main spec.
|
|
43
45
|
|
|
44
46
|
Sync every path in \`existingOutputPaths\` unless the caller narrowed the set.
|
|
45
|
-
A caller narrows it by naming an explicit list of
|
|
46
|
-
|
|
47
|
+
A caller narrows it by naming an explicit list of complete entries from
|
|
48
|
+
\`existingOutputPaths\` — copy those absolute values verbatim. Archive does
|
|
49
|
+
this inline, and a user can too (for example, by selecting the entry ending
|
|
50
|
+
in \`/specs/billing/invoices/spec.md\`).
|
|
47
51
|
Then sync only the named paths and leave the remaining delta specs untouched:
|
|
48
52
|
bulk archive excludes a delta whose implementation it could not find, and
|
|
49
53
|
syncing it anyway would write a main spec the caller deliberately withheld.
|
|
@@ -83,7 +87,7 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
83
87
|
|
|
84
88
|
a. **Read the delta spec** to understand the intended changes
|
|
85
89
|
|
|
86
|
-
b. **Read the main spec** at \`<planningHome.root>/openspec/specs/<capability>/spec.md\` (may not exist yet)
|
|
90
|
+
b. **Read the main spec** at \`<planningHome.root>/openspec/specs/<capability-path>/spec.md\` (may not exist yet)
|
|
87
91
|
|
|
88
92
|
c. **Apply changes intelligently**:
|
|
89
93
|
|
|
@@ -94,13 +98,35 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
94
98
|
**MODIFIED Requirements:**
|
|
95
99
|
- Find the requirement in main spec
|
|
96
100
|
- Apply the changes - this can be:
|
|
97
|
-
- Adding new scenarios
|
|
101
|
+
- Adding new scenarios the main spec does not have yet
|
|
98
102
|
- Modifying existing scenarios
|
|
99
103
|
- Changing the requirement description
|
|
100
104
|
- Preserve scenarios/content not mentioned in the delta
|
|
101
105
|
|
|
102
106
|
**REMOVED Requirements:**
|
|
103
107
|
- Remove the entire requirement block from main spec
|
|
108
|
+
- Retiring the capability. Delete the whole \`spec.md\` - and the directory once
|
|
109
|
+
nothing else is left in it - only when ALL of these hold:
|
|
110
|
+
1. removing the requirements *this run* left no requirement blocks;
|
|
111
|
+
2. the rest of the spec is well-formed (it still has a \`## Purpose\`);
|
|
112
|
+
3. the main spec was not already empty before this sync - if you removed
|
|
113
|
+
nothing, change nothing;
|
|
114
|
+
4. every other nonblank line in the whole file is accounted for as the
|
|
115
|
+
title, Purpose, Requirements header, or a canonical requirement's
|
|
116
|
+
statement, scenarios, or fenced examples;
|
|
117
|
+
5. the change's \`.openspec.yaml\` declares \`retire_capabilities: true\`;
|
|
118
|
+
6. the \`spec.md\` resolves inside the real specs root (do not follow a
|
|
119
|
+
capability-directory symlink to delete an external file).
|
|
120
|
+
If removing the selected requirements would leave no requirement blocks and
|
|
121
|
+
any retirement condition is not satisfied, do not modify the main spec. Stop
|
|
122
|
+
the sync for that capability, report the blocking condition, and tell the user
|
|
123
|
+
how to resolve it. Never write or leave an empty \`## Requirements\` section.
|
|
124
|
+
When only the marker is missing, say that too - it is the one thing the user
|
|
125
|
+
can add to make the retirement go through.
|
|
126
|
+
- Deleting the file also deletes its \`## Purpose\`; any other section blocks
|
|
127
|
+
retirement. Name Purpose when you report the retirement. Include a pasteable
|
|
128
|
+
\`git checkout\` only when the spec lived in the caller's checkout;
|
|
129
|
+
otherwise give checkout-scoped recovery guidance.
|
|
104
130
|
|
|
105
131
|
**RENAMED Requirements:**
|
|
106
132
|
- Find the FROM requirement, rename to TO
|
|
@@ -110,19 +136,26 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
110
136
|
(this is what \`openspec archive\` does; it warns and moves on)
|
|
111
137
|
|
|
112
138
|
d. **Create new main spec** if capability doesn't exist yet:
|
|
113
|
-
- Create \`<planningHome.root>/openspec/specs/<capability>/spec.md\`
|
|
139
|
+
- Create \`<planningHome.root>/openspec/specs/<capability-path>/spec.md\`
|
|
114
140
|
- Add Purpose section: copy the delta's \`## Purpose\` body verbatim when it has one
|
|
115
141
|
(this is what \`openspec archive\` does); only write a brief TBD placeholder when it does not
|
|
116
142
|
- Add Requirements section with the ADDED requirements
|
|
117
143
|
- Follow the **Main Spec Format Reference** below
|
|
118
144
|
|
|
119
|
-
5. **
|
|
145
|
+
5. **Validate updated main specs**
|
|
146
|
+
|
|
147
|
+
Run \`openspec validate --specs\` with the same selected-root flags used earlier.
|
|
148
|
+
If validation fails, report the problems and do not claim the sync succeeded.
|
|
149
|
+
|
|
150
|
+
6. **Show summary**
|
|
120
151
|
|
|
121
152
|
After applying all changes, summarize:
|
|
122
153
|
- Which capabilities were updated
|
|
123
154
|
- What changes were made (requirements added/modified/removed/renamed)
|
|
124
155
|
- Any new main spec left with a TBD Purpose placeholder, so it gets written
|
|
125
156
|
now rather than lingering
|
|
157
|
+
- Any capability retired, naming the deleted \`spec.md\`, its Purpose, and
|
|
158
|
+
either a pasteable \`git checkout\` or checkout-scoped recovery guidance
|
|
126
159
|
|
|
127
160
|
**Delta Spec Format Reference**
|
|
128
161
|
|
|
@@ -143,6 +176,12 @@ The system SHALL do something new.
|
|
|
143
176
|
## MODIFIED Requirements
|
|
144
177
|
|
|
145
178
|
### Requirement: Existing Feature
|
|
179
|
+
The system SHALL keep doing the existing thing, now also handling A.
|
|
180
|
+
|
|
181
|
+
#### Scenario: Scenario the main spec already has
|
|
182
|
+
- **WHEN** user does X
|
|
183
|
+
- **THEN** system does Y
|
|
184
|
+
|
|
146
185
|
#### Scenario: New scenario to add
|
|
147
186
|
- **WHEN** user does A
|
|
148
187
|
- **THEN** system does B
|
|
@@ -179,9 +218,9 @@ The system SHALL do something new.
|
|
|
179
218
|
|
|
180
219
|
**Key Principle: Intelligent Merging**
|
|
181
220
|
|
|
182
|
-
Unlike programmatic merging, you
|
|
183
|
-
-
|
|
184
|
-
-
|
|
221
|
+
Unlike programmatic merging, you merge rather than overwrite:
|
|
222
|
+
- A MODIFIED block carries the whole requirement - body plus every scenario that survives the change. \`openspec validate\` and \`openspec archive\` both reject one that drops a scenario the main spec still has.
|
|
223
|
+
- Keep anything the delta does not mention, in the main spec's existing order
|
|
185
224
|
- Use your judgment to merge changes sensibly
|
|
186
225
|
|
|
187
226
|
**Output On Success**
|
|
@@ -231,6 +270,8 @@ This is an **agent-driven** operation - you will read delta specs and directly e
|
|
|
231
270
|
|
|
232
271
|
${STORE_SELECTION_GUIDANCE}
|
|
233
272
|
|
|
273
|
+
\`<capability-path>\` is the spec directory relative to \`specs/\` (for example, \`user-auth\` or \`identity/user-auth\`). Preserve the full path from each delta spec when resolving its main spec.
|
|
274
|
+
|
|
234
275
|
**Input**: Optionally specify a change name after \`/opsx:sync\` (e.g., \`/opsx:sync add-auth\`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
|
235
276
|
|
|
236
277
|
**Steps**
|
|
@@ -264,8 +305,10 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
264
305
|
instructions or writing a main spec.
|
|
265
306
|
|
|
266
307
|
Sync every path in \`existingOutputPaths\` unless the caller narrowed the set.
|
|
267
|
-
A caller narrows it by naming an explicit list of
|
|
268
|
-
|
|
308
|
+
A caller narrows it by naming an explicit list of complete entries from
|
|
309
|
+
\`existingOutputPaths\` — copy those absolute values verbatim. Archive does
|
|
310
|
+
this inline, and a user can too (for example, by selecting the entry ending
|
|
311
|
+
in \`/specs/billing/invoices/spec.md\`).
|
|
269
312
|
Then sync only the named paths and leave the remaining delta specs untouched:
|
|
270
313
|
bulk archive excludes a delta whose implementation it could not find, and
|
|
271
314
|
syncing it anyway would write a main spec the caller deliberately withheld.
|
|
@@ -305,7 +348,7 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
305
348
|
|
|
306
349
|
a. **Read the delta spec** to understand the intended changes
|
|
307
350
|
|
|
308
|
-
b. **Read the main spec** at \`<planningHome.root>/openspec/specs/<capability>/spec.md\` (may not exist yet)
|
|
351
|
+
b. **Read the main spec** at \`<planningHome.root>/openspec/specs/<capability-path>/spec.md\` (may not exist yet)
|
|
309
352
|
|
|
310
353
|
c. **Apply changes intelligently**:
|
|
311
354
|
|
|
@@ -316,13 +359,35 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
316
359
|
**MODIFIED Requirements:**
|
|
317
360
|
- Find the requirement in main spec
|
|
318
361
|
- Apply the changes - this can be:
|
|
319
|
-
- Adding new scenarios
|
|
362
|
+
- Adding new scenarios the main spec does not have yet
|
|
320
363
|
- Modifying existing scenarios
|
|
321
364
|
- Changing the requirement description
|
|
322
365
|
- Preserve scenarios/content not mentioned in the delta
|
|
323
366
|
|
|
324
367
|
**REMOVED Requirements:**
|
|
325
368
|
- Remove the entire requirement block from main spec
|
|
369
|
+
- Retiring the capability. Delete the whole \`spec.md\` - and the directory once
|
|
370
|
+
nothing else is left in it - only when ALL of these hold:
|
|
371
|
+
1. removing the requirements *this run* left no requirement blocks;
|
|
372
|
+
2. the rest of the spec is well-formed (it still has a \`## Purpose\`);
|
|
373
|
+
3. the main spec was not already empty before this sync - if you removed
|
|
374
|
+
nothing, change nothing;
|
|
375
|
+
4. every other nonblank line in the whole file is accounted for as the
|
|
376
|
+
title, Purpose, Requirements header, or a canonical requirement's
|
|
377
|
+
statement, scenarios, or fenced examples;
|
|
378
|
+
5. the change's \`.openspec.yaml\` declares \`retire_capabilities: true\`;
|
|
379
|
+
6. the \`spec.md\` resolves inside the real specs root (do not follow a
|
|
380
|
+
capability-directory symlink to delete an external file).
|
|
381
|
+
If removing the selected requirements would leave no requirement blocks and
|
|
382
|
+
any retirement condition is not satisfied, do not modify the main spec. Stop
|
|
383
|
+
the sync for that capability, report the blocking condition, and tell the user
|
|
384
|
+
how to resolve it. Never write or leave an empty \`## Requirements\` section.
|
|
385
|
+
When only the marker is missing, say that too - it is the one thing the user
|
|
386
|
+
can add to make the retirement go through.
|
|
387
|
+
- Deleting the file also deletes its \`## Purpose\`; any other section blocks
|
|
388
|
+
retirement. Name Purpose when you report the retirement. Include a pasteable
|
|
389
|
+
\`git checkout\` only when the spec lived in the caller's checkout;
|
|
390
|
+
otherwise give checkout-scoped recovery guidance.
|
|
326
391
|
|
|
327
392
|
**RENAMED Requirements:**
|
|
328
393
|
- Find the FROM requirement, rename to TO
|
|
@@ -332,19 +397,26 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
332
397
|
(this is what \`openspec archive\` does; it warns and moves on)
|
|
333
398
|
|
|
334
399
|
d. **Create new main spec** if capability doesn't exist yet:
|
|
335
|
-
- Create \`<planningHome.root>/openspec/specs/<capability>/spec.md\`
|
|
400
|
+
- Create \`<planningHome.root>/openspec/specs/<capability-path>/spec.md\`
|
|
336
401
|
- Add Purpose section: copy the delta's \`## Purpose\` body verbatim when it has one
|
|
337
402
|
(this is what \`openspec archive\` does); only write a brief TBD placeholder when it does not
|
|
338
403
|
- Add Requirements section with the ADDED requirements
|
|
339
404
|
- Follow the **Main Spec Format Reference** below
|
|
340
405
|
|
|
341
|
-
5. **
|
|
406
|
+
5. **Validate updated main specs**
|
|
407
|
+
|
|
408
|
+
Run \`openspec validate --specs\` with the same selected-root flags used earlier.
|
|
409
|
+
If validation fails, report the problems and do not claim the sync succeeded.
|
|
410
|
+
|
|
411
|
+
6. **Show summary**
|
|
342
412
|
|
|
343
413
|
After applying all changes, summarize:
|
|
344
414
|
- Which capabilities were updated
|
|
345
415
|
- What changes were made (requirements added/modified/removed/renamed)
|
|
346
416
|
- Any new main spec left with a TBD Purpose placeholder, so it gets written
|
|
347
417
|
now rather than lingering
|
|
418
|
+
- Any capability retired, naming the deleted \`spec.md\`, its Purpose, and
|
|
419
|
+
either a pasteable \`git checkout\` or checkout-scoped recovery guidance
|
|
348
420
|
|
|
349
421
|
**Delta Spec Format Reference**
|
|
350
422
|
|
|
@@ -365,6 +437,12 @@ The system SHALL do something new.
|
|
|
365
437
|
## MODIFIED Requirements
|
|
366
438
|
|
|
367
439
|
### Requirement: Existing Feature
|
|
440
|
+
The system SHALL keep doing the existing thing, now also handling A.
|
|
441
|
+
|
|
442
|
+
#### Scenario: Scenario the main spec already has
|
|
443
|
+
- **WHEN** user does X
|
|
444
|
+
- **THEN** system does Y
|
|
445
|
+
|
|
368
446
|
#### Scenario: New scenario to add
|
|
369
447
|
- **WHEN** user does A
|
|
370
448
|
- **THEN** system does B
|
|
@@ -401,9 +479,9 @@ The system SHALL do something new.
|
|
|
401
479
|
|
|
402
480
|
**Key Principle: Intelligent Merging**
|
|
403
481
|
|
|
404
|
-
Unlike programmatic merging, you
|
|
405
|
-
-
|
|
406
|
-
-
|
|
482
|
+
Unlike programmatic merging, you merge rather than overwrite:
|
|
483
|
+
- A MODIFIED block carries the whole requirement - body plus every scenario that survives the change. \`openspec validate\` and \`openspec archive\` both reject one that drops a scenario the main spec still has.
|
|
484
|
+
- Keep anything the delta does not mention, in the main spec's existing order
|
|
407
485
|
- Use your judgment to merge changes sensibly
|
|
408
486
|
|
|
409
487
|
**Output On Success**
|
|
@@ -9,6 +9,8 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
9
9
|
|
|
10
10
|
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
|
11
11
|
|
|
12
|
+
\`/opsx:continue\` is an expanded-profile workflow and may not be installed. Before suggesting it anywhere below, verify that it is available. If it is unavailable, \`openspec status --change "<name>" --json\` shows the next artifact and \`openspec instructions "<artifact-id>" --change "<name>" --json\` explains how to create it.
|
|
13
|
+
|
|
12
14
|
**Steps**
|
|
13
15
|
|
|
14
16
|
1. **Select the change**
|
|
@@ -35,7 +37,7 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
35
37
|
Parse the JSON to understand current state. The response includes:
|
|
36
38
|
- \`schemaName\`: The workflow schema being used (e.g., "spec-driven")
|
|
37
39
|
- \`artifacts\`: Array of artifacts with their status ("done", "skipped", "ready", "blocked")
|
|
38
|
-
- \`
|
|
40
|
+
- \`isPlanningComplete\`: Boolean indicating if all planning artifacts are complete. Older CLI versions expose the same value as \`isComplete\`.
|
|
39
41
|
- \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\`: path and scope context. Use these instead of assuming repo-local paths.
|
|
40
42
|
|
|
41
43
|
The artifact ids and paths come from the active schema - do NOT assume them, and do NOT branch on hardcoded artifact names. Custom schemas must work unchanged.
|
|
@@ -58,7 +60,7 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
58
60
|
- If the user rejects a revision, do not write it - leave that artifact unchanged.
|
|
59
61
|
- When a substantial rewrite is needed, get that artifact's rules and template first:
|
|
60
62
|
\`\`\`bash
|
|
61
|
-
openspec instructions <artifact-id> --change "<name>" --json
|
|
63
|
+
openspec instructions "<artifact-id>" --change "<name>" --json
|
|
62
64
|
\`\`\`
|
|
63
65
|
|
|
64
66
|
6. **Point to the next step (guidance only - NEVER act on it)**
|
|
@@ -79,8 +81,7 @@ After each invocation, show:
|
|
|
79
81
|
- Edit only the concrete files in \`existingOutputPaths\`; never write to a glob \`resolvedOutputPath\`.
|
|
80
82
|
- Do not advance the build frontier: no new artifacts, no new files under glob artifacts - that is \`/opsx:continue\`'s job.
|
|
81
83
|
- Confirm every edit with the user before writing.
|
|
82
|
-
- If the request changes the change's *intent* rather than refining it, recommend starting fresh with \`/opsx:new\` (the "Update vs. Start Fresh" heuristic).
|
|
83
|
-
- \`/opsx:continue\` and \`/opsx:new\` may not be installed (core profile). When suggesting one that is unavailable, point to the CLI instead: \`openspec status --change "<name>" --json\` shows the next artifact and \`openspec instructions <artifact-id> --change "<name>" --json\` explains how to create it.`,
|
|
84
|
+
- If the request changes the change's *intent* rather than refining it, first verify whether the expanded-profile \`/opsx:new\` workflow is available. If it is, recommend starting fresh with \`/opsx:new\` (the "Update vs. Start Fresh" heuristic). If it is unavailable, ask for a distinct unused change name and recommend \`openspec new change "<new-change-name>"\` instead.`,
|
|
84
85
|
license: 'MIT',
|
|
85
86
|
compatibility: 'Requires openspec CLI.',
|
|
86
87
|
metadata: { author: 'openspec', version: '1.0' },
|
|
@@ -98,6 +99,8 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
98
99
|
|
|
99
100
|
**Input**: Optionally specify a change name after \`/opsx:update\` (e.g., \`/opsx:update add-auth\`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
|
100
101
|
|
|
102
|
+
\`/opsx:continue\` is an expanded-profile workflow and may not be installed. Before suggesting it anywhere below, verify that it is available. If it is unavailable, \`openspec status --change "<name>" --json\` shows the next artifact and \`openspec instructions "<artifact-id>" --change "<name>" --json\` explains how to create it.
|
|
103
|
+
|
|
101
104
|
**Steps**
|
|
102
105
|
|
|
103
106
|
1. **Select the change**
|
|
@@ -124,7 +127,7 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
124
127
|
Parse the JSON to understand current state. The response includes:
|
|
125
128
|
- \`schemaName\`: The workflow schema being used (e.g., "spec-driven")
|
|
126
129
|
- \`artifacts\`: Array of artifacts with their status ("done", "skipped", "ready", "blocked")
|
|
127
|
-
- \`
|
|
130
|
+
- \`isPlanningComplete\`: Boolean indicating if all planning artifacts are complete. Older CLI versions expose the same value as \`isComplete\`.
|
|
128
131
|
- \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\`: path and scope context. Use these instead of assuming repo-local paths.
|
|
129
132
|
|
|
130
133
|
The artifact ids and paths come from the active schema - do NOT assume them, and do NOT branch on hardcoded artifact names. Custom schemas must work unchanged.
|
|
@@ -147,7 +150,7 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
147
150
|
- If the user rejects a revision, do not write it - leave that artifact unchanged.
|
|
148
151
|
- When a substantial rewrite is needed, get that artifact's rules and template first:
|
|
149
152
|
\`\`\`bash
|
|
150
|
-
openspec instructions <artifact-id> --change "<name>" --json
|
|
153
|
+
openspec instructions "<artifact-id>" --change "<name>" --json
|
|
151
154
|
\`\`\`
|
|
152
155
|
|
|
153
156
|
6. **Point to the next step (guidance only - NEVER act on it)**
|
|
@@ -168,8 +171,7 @@ After each invocation, show:
|
|
|
168
171
|
- Edit only the concrete files in \`existingOutputPaths\`; never write to a glob \`resolvedOutputPath\`.
|
|
169
172
|
- Do not advance the build frontier: no new artifacts, no new files under glob artifacts - that is \`/opsx:continue\`'s job.
|
|
170
173
|
- Confirm every edit with the user before writing.
|
|
171
|
-
- If the request changes the change's *intent* rather than refining it, recommend starting fresh with \`/opsx:new\` (the "Update vs. Start Fresh" heuristic).
|
|
172
|
-
- \`/opsx:continue\` and \`/opsx:new\` may not be installed (core profile). When suggesting one that is unavailable, point to the CLI instead: \`openspec status --change "<name>" --json\` shows the next artifact and \`openspec instructions <artifact-id> --change "<name>" --json\` explains how to create it.`
|
|
174
|
+
- If the request changes the change's *intent* rather than refining it, first verify whether the expanded-profile \`/opsx:new\` workflow is available. If it is, recommend starting fresh with \`/opsx:new\` (the "Update vs. Start Fresh" heuristic). If it is unavailable, ask for a distinct unused change name and recommend \`openspec new change "<new-change-name>"\` instead.`
|
|
173
175
|
};
|
|
174
176
|
}
|
|
175
177
|
//# sourceMappingURL=update-change.js.map
|
package/dist/core/update.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export declare class UpdateCommand {
|
|
|
28
28
|
* @param projectPath - Path to the project root containing the openspec directory
|
|
29
29
|
*/
|
|
30
30
|
execute(projectPath: string): Promise<void>;
|
|
31
|
+
private syncCopilotCloudFiles;
|
|
31
32
|
/**
|
|
32
33
|
* Display message when all tools are up to date.
|
|
33
34
|
*/
|
package/dist/core/update.js
CHANGED
|
@@ -13,7 +13,7 @@ import { FileSystemUtils } from '../utils/file-system.js';
|
|
|
13
13
|
import { getSkillReferenceTransformer, getTransformerForTool, transformToSkillReferences } from '../utils/command-references.js';
|
|
14
14
|
import { AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
|
|
15
15
|
import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
|
|
16
|
-
import { getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, } from './shared/index.js';
|
|
16
|
+
import { getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, } from './shared/index.js';
|
|
17
17
|
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatDeferredGlobalPromptSummary, formatCleanupSummary, formatDetectionSummary, getLegacyGlobalPromptMatches, getLegacyWorkflowIdsForTool, getToolsFromLegacyArtifacts, omitGlobalLegacyPromptFiles, pickGlobalLegacyPromptFiles, } from './legacy-cleanup.js';
|
|
18
18
|
import { isInteractive } from '../utils/interactive.js';
|
|
19
19
|
import { getGlobalConfig } from './global-config.js';
|
|
@@ -23,6 +23,8 @@ import { getAvailableTools } from './available-tools.js';
|
|
|
23
23
|
import { WORKFLOW_TO_SKILL_DIR, getConfiguredToolsForProfileSync, getToolsNeedingProfileSync, } from './profile-sync-drift.js';
|
|
24
24
|
import { scanInstalledWorkflows as scanInstalledWorkflowsShared, migrateIfNeeded as migrateIfNeededShared, findLegacyToolMigrations, migrateLegacyToolDirs, describeLegacyMigration, legacyMigrationNotice, keptInPlaceNotice, hasMovableContent, } from './migration.js';
|
|
25
25
|
import { resolveCommandSurfaceCapability, resolveCommandInvocation, shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
|
|
26
|
+
import { writeSharedSkillTarget } from './shared-skill-target.js';
|
|
27
|
+
import { includesGitHubCopilot, writeCopilotCloudFiles, removeCopilotCloudFiles, isCopilotCloudEnabled, readCopilotCloudOptIn, findUnmanagedCloudFiles } from './github-copilot/cloud-agent.js';
|
|
26
28
|
const require = createRequire(import.meta.url);
|
|
27
29
|
const { version: OPENSPEC_VERSION } = require('../../package.json');
|
|
28
30
|
/**
|
|
@@ -80,6 +82,7 @@ export class UpdateCommand {
|
|
|
80
82
|
const { newlyConfiguredTools, workflowOverrides: legacyWorkflowOverrides, deferredGlobalCleanup, } = legacyUpgrade;
|
|
81
83
|
// 5. Find configured tools
|
|
82
84
|
const configuredTools = getConfiguredToolsForProfileSync(resolvedProjectPath);
|
|
85
|
+
const configuredAndNewTools = [...new Set([...configuredTools, ...newlyConfiguredTools])];
|
|
83
86
|
if (configuredTools.length === 0 && newlyConfiguredTools.length === 0) {
|
|
84
87
|
if (deferredGlobalCleanup) {
|
|
85
88
|
await this.performDeferredGlobalPromptCleanup(resolvedProjectPath, deferredGlobalCleanup);
|
|
@@ -94,6 +97,7 @@ export class UpdateCommand {
|
|
|
94
97
|
}
|
|
95
98
|
return;
|
|
96
99
|
}
|
|
100
|
+
await this.syncCopilotCloudFiles(resolvedProjectPath, configuredAndNewTools);
|
|
97
101
|
console.log(chalk.yellow('No configured tools found.'));
|
|
98
102
|
console.log(chalk.dim('Run "openspec init" to set up tools.'));
|
|
99
103
|
return;
|
|
@@ -107,7 +111,13 @@ export class UpdateCommand {
|
|
|
107
111
|
const statusByTool = new Map(toolStatuses.map((status) => [status.toolId, status]));
|
|
108
112
|
// 7. Smart update detection
|
|
109
113
|
const toolsNeedingVersionUpdate = toolStatuses
|
|
110
|
-
.filter((s) =>
|
|
114
|
+
.filter((s) => {
|
|
115
|
+
if (!s.needsUpdate || delivery !== 'commands') {
|
|
116
|
+
return s.needsUpdate;
|
|
117
|
+
}
|
|
118
|
+
const tool = AI_TOOLS.find((candidate) => candidate.value === s.toolId);
|
|
119
|
+
return !tool || !hasGlobalSkillTarget(tool);
|
|
120
|
+
})
|
|
111
121
|
.map((s) => s.toolId);
|
|
112
122
|
const toolsNeedingConfigSync = getToolsNeedingProfileSync(resolvedProjectPath, desiredWorkflows, delivery, configuredTools);
|
|
113
123
|
const toolsToUpdateSet = new Set([
|
|
@@ -121,6 +131,7 @@ export class UpdateCommand {
|
|
|
121
131
|
}
|
|
122
132
|
// All tools are up to date
|
|
123
133
|
this.displayUpToDateMessage(toolStatuses);
|
|
134
|
+
await this.syncCopilotCloudFiles(resolvedProjectPath, configuredAndNewTools);
|
|
124
135
|
// Still check for new tool directories and extra workflows
|
|
125
136
|
this.detectNewTools(resolvedProjectPath, configuredTools);
|
|
126
137
|
this.displayExtraWorkflowsNote(resolvedProjectPath, configuredTools, desiredWorkflows);
|
|
@@ -153,11 +164,12 @@ export class UpdateCommand {
|
|
|
153
164
|
let removedDeselectedSkillCount = 0;
|
|
154
165
|
for (const toolId of toolsToUpdate) {
|
|
155
166
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
156
|
-
if (!tool
|
|
167
|
+
if (!tool || !toolSupportsSkills(tool))
|
|
157
168
|
continue;
|
|
158
169
|
const spinner = ora(`Updating ${tool.name}...`).start();
|
|
159
170
|
try {
|
|
160
|
-
const skillsDir =
|
|
171
|
+
const skillsDir = resolveToolSkillsDir(resolvedProjectPath, tool);
|
|
172
|
+
const skillsRoot = hasGlobalSkillTarget(tool) ? skillsDir : resolvedProjectPath;
|
|
161
173
|
const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
|
|
162
174
|
const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
|
|
163
175
|
const toolWorkflows = legacyWorkflowOverrides[tool.value] ?? desiredWorkflows;
|
|
@@ -170,13 +182,18 @@ export class UpdateCommand {
|
|
|
170
182
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
171
183
|
const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
|
|
172
184
|
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
185
|
+
FileSystemUtils.assertPathWithin(skillsRoot, skillFile);
|
|
173
186
|
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
174
187
|
}
|
|
175
|
-
|
|
188
|
+
writeSharedSkillTarget(resolvedProjectPath, tool.value);
|
|
189
|
+
removedDeselectedSkillCount += await this.removeUnselectedSkillDirs(skillsRoot, skillsDir, toolWorkflows);
|
|
176
190
|
}
|
|
177
191
|
// Delete skill directories if delivery is commands-only
|
|
178
|
-
if (shouldRemoveSkillsForTool(tool.value, delivery)) {
|
|
179
|
-
removedSkillCount += await this.removeSkillDirs(skillsDir);
|
|
192
|
+
if (shouldRemoveSkillsForTool(tool.value, delivery) && !hasGlobalSkillTarget(tool)) {
|
|
193
|
+
removedSkillCount += await this.removeSkillDirs(skillsRoot, skillsDir);
|
|
194
|
+
// Persist the selected owner even when commands-only delivery leaves
|
|
195
|
+
// this target with no generated skills.
|
|
196
|
+
writeSharedSkillTarget(resolvedProjectPath, tool.value);
|
|
180
197
|
// A tool with no command adapter now has zero OpenSpec artifacts;
|
|
181
198
|
// say so like init does, rather than deleting its skills silently
|
|
182
199
|
// and letting tool detection re-suggest an init that would also
|
|
@@ -191,7 +208,7 @@ export class UpdateCommand {
|
|
|
191
208
|
if (adapter) {
|
|
192
209
|
const generatedCommands = generateCommands(commandContents, adapter);
|
|
193
210
|
for (const cmd of generatedCommands) {
|
|
194
|
-
const commandFile =
|
|
211
|
+
const commandFile = FileSystemUtils.resolveProjectArtifactPath(resolvedProjectPath, cmd.path);
|
|
195
212
|
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
|
|
196
213
|
}
|
|
197
214
|
removedDeselectedCommandCount += await this.removeUnselectedCommandFiles(resolvedProjectPath, toolId, toolWorkflows);
|
|
@@ -206,6 +223,12 @@ export class UpdateCommand {
|
|
|
206
223
|
}
|
|
207
224
|
spinner.succeed(`Updated ${tool.name}`);
|
|
208
225
|
updatedTools.push(tool.name);
|
|
226
|
+
for (const migration of migrateLegacyToolDirs(resolvedProjectPath, [tool.value], 'after-generation')) {
|
|
227
|
+
if (hasMovableContent(migration)) {
|
|
228
|
+
console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
|
|
229
|
+
}
|
|
230
|
+
this.reportKeptInPlace(migration);
|
|
231
|
+
}
|
|
209
232
|
}
|
|
210
233
|
catch (error) {
|
|
211
234
|
spinner.fail(`Failed to update ${tool.name}`);
|
|
@@ -282,7 +305,7 @@ export class UpdateCommand {
|
|
|
282
305
|
}
|
|
283
306
|
console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
|
|
284
307
|
}
|
|
285
|
-
|
|
308
|
+
await this.syncCopilotCloudFiles(resolvedProjectPath, configuredAndNewTools);
|
|
286
309
|
// 13. Detect new tool directories not currently configured
|
|
287
310
|
this.detectNewTools(resolvedProjectPath, configuredAndNewTools);
|
|
288
311
|
// 14. Display note about extra workflows not in profile
|
|
@@ -296,6 +319,51 @@ export class UpdateCommand {
|
|
|
296
319
|
}
|
|
297
320
|
console.log();
|
|
298
321
|
console.log(chalk.dim('Restart your IDE for changes to take effect.'));
|
|
322
|
+
if (failedTools.length > 0) {
|
|
323
|
+
throw new Error(`OpenSpec update failed for: ${failedTools.map((tool) => tool.name).join(', ')}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async syncCopilotCloudFiles(projectPath, configuredTools) {
|
|
327
|
+
try {
|
|
328
|
+
if (includesGitHubCopilot(configuredTools)) {
|
|
329
|
+
// Cloud files are opt-in (see cloud-agent.ts). `update` never prompts,
|
|
330
|
+
// so it only refreshes files the user has already opted into (via
|
|
331
|
+
// `openspec init` or a `githubCopilot.cloudAgent: true` config), or that
|
|
332
|
+
// a pre-opt-in project already has. Opting in is a deliberate init/config
|
|
333
|
+
// step, never a silent side effect of running update.
|
|
334
|
+
if (await isCopilotCloudEnabled(projectPath)) {
|
|
335
|
+
await writeCopilotCloudFiles(projectPath);
|
|
336
|
+
const collisions = await findUnmanagedCloudFiles(projectPath);
|
|
337
|
+
if (collisions.length > 0) {
|
|
338
|
+
console.log(chalk.dim(`Left your existing ${collisions.join(' and ')} untouched — add the OpenSpec ` +
|
|
339
|
+
`install step by hand so the Copilot cloud agent can run openspec.`));
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
// Explicit opt-out (githubCopilot.cloudAgent: false) means "not here":
|
|
344
|
+
// remove any managed files a prior opt-in left behind (customized files
|
|
345
|
+
// are preserved). If the user simply never decided, stay quiet unless
|
|
346
|
+
// we're at an interactive terminal, where a one-line hint aids discovery.
|
|
347
|
+
if (readCopilotCloudOptIn(projectPath) === false) {
|
|
348
|
+
const removed = await removeCopilotCloudFiles(projectPath);
|
|
349
|
+
if (removed > 0) {
|
|
350
|
+
console.log(chalk.dim(`Removed: ${removed} Copilot cloud agent file(s) (opted out of cloud files)`));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else if (isInteractive()) {
|
|
354
|
+
console.log(chalk.dim("GitHub Copilot cloud coding-agent files are available (opt-in). Enable with 'openspec init --copilot-cloud'."));
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const removed = await removeCopilotCloudFiles(projectPath);
|
|
359
|
+
if (removed > 0) {
|
|
360
|
+
console.log(chalk.dim(`Removed: ${removed} Copilot cloud agent file(s) (github-copilot not configured)`));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
365
|
+
console.warn(`Warning: failed to sync Copilot cloud agent files: ${message}`);
|
|
366
|
+
}
|
|
299
367
|
}
|
|
300
368
|
/**
|
|
301
369
|
* Display message when all tools are up to date.
|
|
@@ -387,18 +455,19 @@ export class UpdateCommand {
|
|
|
387
455
|
* Removes skill directories for workflows when delivery changed to commands-only.
|
|
388
456
|
* Returns the number of directories removed.
|
|
389
457
|
*/
|
|
390
|
-
async removeSkillDirs(skillsDir) {
|
|
458
|
+
async removeSkillDirs(skillsRoot, skillsDir) {
|
|
391
459
|
let removed = 0;
|
|
392
460
|
for (const workflow of ALL_WORKFLOWS) {
|
|
393
461
|
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
394
462
|
if (!dirName)
|
|
395
463
|
continue;
|
|
396
464
|
const skillDir = path.join(skillsDir, dirName);
|
|
465
|
+
if (!fs.existsSync(skillDir))
|
|
466
|
+
continue;
|
|
467
|
+
FileSystemUtils.assertPathWithin(skillsRoot, skillDir);
|
|
397
468
|
try {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
removed++;
|
|
401
|
-
}
|
|
469
|
+
await fs.promises.rm(skillDir, { recursive: true, force: true });
|
|
470
|
+
removed++;
|
|
402
471
|
}
|
|
403
472
|
catch {
|
|
404
473
|
// Ignore errors
|
|
@@ -410,7 +479,7 @@ export class UpdateCommand {
|
|
|
410
479
|
* Removes skill directories for workflows that are no longer selected in the active profile.
|
|
411
480
|
* Returns the number of directories removed.
|
|
412
481
|
*/
|
|
413
|
-
async removeUnselectedSkillDirs(skillsDir, desiredWorkflows) {
|
|
482
|
+
async removeUnselectedSkillDirs(skillsRoot, skillsDir, desiredWorkflows) {
|
|
414
483
|
const desiredSet = new Set(desiredWorkflows);
|
|
415
484
|
let removed = 0;
|
|
416
485
|
for (const workflow of ALL_WORKFLOWS) {
|
|
@@ -420,11 +489,12 @@ export class UpdateCommand {
|
|
|
420
489
|
if (!dirName)
|
|
421
490
|
continue;
|
|
422
491
|
const skillDir = path.join(skillsDir, dirName);
|
|
492
|
+
if (!fs.existsSync(skillDir))
|
|
493
|
+
continue;
|
|
494
|
+
FileSystemUtils.assertPathWithin(skillsRoot, skillDir);
|
|
423
495
|
try {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
removed++;
|
|
427
|
-
}
|
|
496
|
+
await fs.promises.rm(skillDir, { recursive: true, force: true });
|
|
497
|
+
removed++;
|
|
428
498
|
}
|
|
429
499
|
catch {
|
|
430
500
|
// Ignore errors
|
|
@@ -443,7 +513,7 @@ export class UpdateCommand {
|
|
|
443
513
|
return 0;
|
|
444
514
|
for (const workflow of ALL_WORKFLOWS) {
|
|
445
515
|
const cmdPath = adapter.getFilePath(workflow);
|
|
446
|
-
const fullPath =
|
|
516
|
+
const fullPath = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmdPath);
|
|
447
517
|
try {
|
|
448
518
|
if (fs.existsSync(fullPath)) {
|
|
449
519
|
await fs.promises.unlink(fullPath);
|
|
@@ -470,7 +540,7 @@ export class UpdateCommand {
|
|
|
470
540
|
if (desiredSet.has(workflow))
|
|
471
541
|
continue;
|
|
472
542
|
const cmdPath = adapter.getFilePath(workflow);
|
|
473
|
-
const fullPath =
|
|
543
|
+
const fullPath = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmdPath);
|
|
474
544
|
try {
|
|
475
545
|
if (fs.existsSync(fullPath)) {
|
|
476
546
|
await fs.promises.unlink(fullPath);
|
|
@@ -721,11 +791,12 @@ export class UpdateCommand {
|
|
|
721
791
|
const workflowOverrides = {};
|
|
722
792
|
for (const toolId of selectedTools) {
|
|
723
793
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
724
|
-
if (!tool
|
|
794
|
+
if (!tool || !toolSupportsSkills(tool))
|
|
725
795
|
continue;
|
|
726
796
|
const spinner = ora(`Setting up ${tool.name}...`).start();
|
|
727
797
|
try {
|
|
728
|
-
const skillsDir =
|
|
798
|
+
const skillsDir = resolveToolSkillsDir(projectPath, tool);
|
|
799
|
+
const skillsRoot = hasGlobalSkillTarget(tool) ? skillsDir : projectPath;
|
|
729
800
|
const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
|
|
730
801
|
const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
|
|
731
802
|
const toolWorkflows = (tool.value === 'codex' && inferredCodexWorkflows.length > 0
|
|
@@ -743,8 +814,10 @@ export class UpdateCommand {
|
|
|
743
814
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
744
815
|
const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
|
|
745
816
|
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
817
|
+
FileSystemUtils.assertPathWithin(skillsRoot, skillFile);
|
|
746
818
|
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
747
819
|
}
|
|
820
|
+
writeSharedSkillTarget(projectPath, tool.value);
|
|
748
821
|
}
|
|
749
822
|
// Create commands when delivery includes commands
|
|
750
823
|
if (shouldGenerateCommands) {
|
|
@@ -752,13 +825,19 @@ export class UpdateCommand {
|
|
|
752
825
|
if (adapter) {
|
|
753
826
|
const generatedCommands = generateCommands(commandContents, adapter);
|
|
754
827
|
for (const cmd of generatedCommands) {
|
|
755
|
-
const commandFile =
|
|
828
|
+
const commandFile = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmd.path);
|
|
756
829
|
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
|
|
757
830
|
}
|
|
758
831
|
}
|
|
759
832
|
}
|
|
760
833
|
spinner.succeed(`Setup complete for ${tool.name}`);
|
|
761
834
|
newlyConfigured.push(toolId);
|
|
835
|
+
for (const migration of migrateLegacyToolDirs(projectPath, [tool.value], 'after-generation')) {
|
|
836
|
+
if (hasMovableContent(migration)) {
|
|
837
|
+
console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
|
|
838
|
+
}
|
|
839
|
+
this.reportKeptInPlace(migration);
|
|
840
|
+
}
|
|
762
841
|
}
|
|
763
842
|
catch (error) {
|
|
764
843
|
spinner.fail(`Failed to set up ${tool.name}`);
|