@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.
Files changed (95) hide show
  1. package/README.md +3 -1
  2. package/dist/cli/index.js +6 -1
  3. package/dist/commands/change.js +15 -1
  4. package/dist/commands/schema.js +89 -20
  5. package/dist/commands/spec.js +30 -6
  6. package/dist/commands/validate.js +2 -2
  7. package/dist/commands/workflow/instructions.js +32 -27
  8. package/dist/commands/workflow/status.js +2 -2
  9. package/dist/commands/workflow/templates.js +15 -5
  10. package/dist/core/archive.d.ts +14 -0
  11. package/dist/core/archive.js +1265 -151
  12. package/dist/core/artifact-graph/index.d.ts +1 -1
  13. package/dist/core/artifact-graph/index.js +1 -1
  14. package/dist/core/artifact-graph/instruction-loader.d.ts +3 -1
  15. package/dist/core/artifact-graph/instruction-loader.js +15 -5
  16. package/dist/core/artifact-graph/outputs.d.ts +1 -0
  17. package/dist/core/artifact-graph/outputs.js +67 -5
  18. package/dist/core/artifact-graph/resolver.js +35 -9
  19. package/dist/core/artifact-graph/types.js +21 -3
  20. package/dist/core/available-tools.d.ts +2 -2
  21. package/dist/core/available-tools.js +14 -3
  22. package/dist/core/change-metadata/schema.d.ts +1 -0
  23. package/dist/core/change-metadata/schema.js +7 -0
  24. package/dist/core/change-status-policy.js +2 -2
  25. package/dist/core/completions/command-registry.js +8 -0
  26. package/dist/core/config-schema.d.ts +3 -0
  27. package/dist/core/config-schema.js +28 -1
  28. package/dist/core/config.d.ts +3 -0
  29. package/dist/core/config.js +24 -2
  30. package/dist/core/file-state.js +56 -24
  31. package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
  32. package/dist/core/github-copilot/cloud-agent.js +497 -0
  33. package/dist/core/global-config.d.ts +11 -0
  34. package/dist/core/init.d.ts +23 -0
  35. package/dist/core/init.js +234 -39
  36. package/dist/core/legacy-cleanup.js +4 -2
  37. package/dist/core/migration.d.ts +6 -4
  38. package/dist/core/migration.js +78 -22
  39. package/dist/core/parsers/requirement-blocks.d.ts +10 -0
  40. package/dist/core/parsers/requirement-blocks.js +60 -0
  41. package/dist/core/parsers/spec-structure.d.ts +1 -1
  42. package/dist/core/parsers/spec-structure.js +17 -1
  43. package/dist/core/profile-sync-drift.js +40 -5
  44. package/dist/core/project-config.d.ts +3 -0
  45. package/dist/core/project-config.js +31 -1
  46. package/dist/core/shared/index.d.ts +1 -0
  47. package/dist/core/shared/index.js +1 -0
  48. package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
  49. package/dist/core/shared/skill-content-equivalence.js +41 -0
  50. package/dist/core/shared/skill-paths.d.ts +13 -0
  51. package/dist/core/shared/skill-paths.js +23 -0
  52. package/dist/core/shared/tool-detection.js +66 -35
  53. package/dist/core/shared-skill-target.d.ts +16 -0
  54. package/dist/core/shared-skill-target.js +156 -0
  55. package/dist/core/specs-apply.d.ts +72 -0
  56. package/dist/core/specs-apply.js +448 -55
  57. package/dist/core/templates/skill-templates.d.ts +1 -1
  58. package/dist/core/templates/skill-templates.js +1 -1
  59. package/dist/core/templates/workflows/apply-change.d.ts +10 -0
  60. package/dist/core/templates/workflows/apply-change.js +22 -182
  61. package/dist/core/templates/workflows/archive-change.js +8 -4
  62. package/dist/core/templates/workflows/bulk-archive-change.js +30 -26
  63. package/dist/core/templates/workflows/continue-change.js +6 -6
  64. package/dist/core/templates/workflows/explore.js +42 -18
  65. package/dist/core/templates/workflows/onboard.js +9 -3
  66. package/dist/core/templates/workflows/propose.js +72 -20
  67. package/dist/core/templates/workflows/store-selection.d.ts +1 -1
  68. package/dist/core/templates/workflows/store-selection.js +1 -1
  69. package/dist/core/templates/workflows/sync-specs.js +96 -18
  70. package/dist/core/templates/workflows/update-change.js +10 -8
  71. package/dist/core/update.d.ts +1 -0
  72. package/dist/core/update.js +103 -24
  73. package/dist/core/validation/validator.d.ts +23 -3
  74. package/dist/core/validation/validator.js +134 -18
  75. package/dist/core/version-check.js +6 -11
  76. package/dist/telemetry/config.d.ts +3 -4
  77. package/dist/telemetry/index.d.ts +9 -4
  78. package/dist/telemetry/index.js +20 -8
  79. package/dist/utils/change-metadata.d.ts +17 -4
  80. package/dist/utils/change-metadata.js +26 -4
  81. package/dist/utils/ci.d.ts +12 -0
  82. package/dist/utils/ci.js +16 -0
  83. package/dist/utils/command-references.d.ts +14 -2
  84. package/dist/utils/command-references.js +45 -3
  85. package/dist/utils/file-system.d.ts +10 -0
  86. package/dist/utils/file-system.js +70 -0
  87. package/dist/utils/interactive.d.ts +23 -0
  88. package/dist/utils/interactive.js +32 -0
  89. package/dist/utils/spec-discovery.d.ts +4 -4
  90. package/dist/utils/spec-discovery.js +19 -6
  91. package/dist/utils/task-progress.d.ts +17 -0
  92. package/dist/utils/task-progress.js +41 -13
  93. package/package.json +1 -1
  94. package/schemas/spec-driven/schema.yaml +8 -6
  95. 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 delta spec paths to sync —
46
- archive does this inline, and a user can too ("only sync the billing delta").
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 (don't need to copy existing ones)
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. **Show summary**
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 can apply **partial updates**:
183
- - To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
184
- - The delta represents *intent*, not a wholesale replacement
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 delta spec paths to sync —
268
- archive does this inline, and a user can too ("only sync the billing delta").
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 (don't need to copy existing ones)
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. **Show summary**
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 can apply **partial updates**:
405
- - To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
406
- - The delta represents *intent*, not a wholesale replacement
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
- - \`isComplete\`: Boolean indicating if all artifacts are complete
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
- - \`isComplete\`: Boolean indicating if all artifacts are complete
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
@@ -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
  */
@@ -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) => s.needsUpdate)
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?.skillsDir)
167
+ if (!tool || !toolSupportsSkills(tool))
157
168
  continue;
158
169
  const spinner = ora(`Updating ${tool.name}...`).start();
159
170
  try {
160
- const skillsDir = path.join(resolvedProjectPath, tool.skillsDir, 'skills');
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
- removedDeselectedSkillCount += await this.removeUnselectedSkillDirs(skillsDir, toolWorkflows);
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 = path.isAbsolute(cmd.path) ? cmd.path : path.join(resolvedProjectPath, cmd.path);
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
- const configuredAndNewTools = [...new Set([...configuredTools, ...newlyConfiguredTools])];
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
- if (fs.existsSync(skillDir)) {
399
- await fs.promises.rm(skillDir, { recursive: true, force: true });
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
- if (fs.existsSync(skillDir)) {
425
- await fs.promises.rm(skillDir, { recursive: true, force: true });
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 = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
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 = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
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?.skillsDir)
794
+ if (!tool || !toolSupportsSkills(tool))
725
795
  continue;
726
796
  const spinner = ora(`Setting up ${tool.name}...`).start();
727
797
  try {
728
- const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
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 = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path);
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}`);