@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.
Files changed (208) hide show
  1. package/README.md +31 -2
  2. package/dist/cli/index.js +79 -8
  3. package/dist/commands/change.d.ts +0 -1
  4. package/dist/commands/change.js +105 -51
  5. package/dist/commands/config.js +14 -8
  6. package/dist/commands/doctor.js +9 -4
  7. package/dist/commands/feedback.js +74 -23
  8. package/dist/commands/schema.js +110 -31
  9. package/dist/commands/show.js +5 -2
  10. package/dist/commands/spec.js +49 -29
  11. package/dist/commands/validate.js +20 -5
  12. package/dist/commands/workflow/index.d.ts +1 -1
  13. package/dist/commands/workflow/index.js +1 -1
  14. package/dist/commands/workflow/instructions.d.ts +7 -1
  15. package/dist/commands/workflow/instructions.js +121 -33
  16. package/dist/commands/workflow/shared.d.ts +13 -2
  17. package/dist/commands/workflow/shared.js +34 -4
  18. package/dist/commands/workflow/status.js +9 -4
  19. package/dist/commands/workflow/templates.js +15 -5
  20. package/dist/core/archive.d.ts +14 -1
  21. package/dist/core/archive.js +1332 -155
  22. package/dist/core/artifact-graph/graph.d.ts +16 -0
  23. package/dist/core/artifact-graph/graph.js +30 -6
  24. package/dist/core/artifact-graph/index.d.ts +1 -1
  25. package/dist/core/artifact-graph/index.js +1 -1
  26. package/dist/core/artifact-graph/instruction-loader.d.ts +30 -3
  27. package/dist/core/artifact-graph/instruction-loader.js +70 -12
  28. package/dist/core/artifact-graph/outputs.d.ts +1 -0
  29. package/dist/core/artifact-graph/outputs.js +67 -5
  30. package/dist/core/artifact-graph/resolver.d.ts +14 -0
  31. package/dist/core/artifact-graph/resolver.js +69 -15
  32. package/dist/core/artifact-graph/types.js +21 -3
  33. package/dist/core/available-tools.d.ts +2 -2
  34. package/dist/core/available-tools.js +14 -3
  35. package/dist/core/change-metadata/schema.d.ts +2 -0
  36. package/dist/core/change-metadata/schema.js +13 -0
  37. package/dist/core/change-status-policy.d.ts +1 -1
  38. package/dist/core/change-status-policy.js +2 -2
  39. package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
  40. package/dist/core/command-generation/adapters/amazon-q.js +7 -1
  41. package/dist/core/command-generation/adapters/antigravity.js +2 -1
  42. package/dist/core/command-generation/adapters/auggie.js +2 -1
  43. package/dist/core/command-generation/adapters/bob.d.ts +5 -1
  44. package/dist/core/command-generation/adapters/bob.js +6 -5
  45. package/dist/core/command-generation/adapters/claude.js +1 -8
  46. package/dist/core/command-generation/adapters/codebuddy.js +3 -2
  47. package/dist/core/command-generation/adapters/continue.js +3 -2
  48. package/dist/core/command-generation/adapters/costrict.js +2 -1
  49. package/dist/core/command-generation/adapters/crush.js +5 -5
  50. package/dist/core/command-generation/adapters/cursor.js +2 -2
  51. package/dist/core/command-generation/adapters/devin.d.ts +19 -0
  52. package/dist/core/command-generation/adapters/devin.js +36 -0
  53. package/dist/core/command-generation/adapters/factory.js +2 -1
  54. package/dist/core/command-generation/adapters/gemini.js +37 -2
  55. package/dist/core/command-generation/adapters/github-copilot.js +2 -1
  56. package/dist/core/command-generation/adapters/iflow.js +5 -4
  57. package/dist/core/command-generation/adapters/index.d.ts +2 -2
  58. package/dist/core/command-generation/adapters/index.js +2 -2
  59. package/dist/core/command-generation/adapters/junie.js +2 -1
  60. package/dist/core/command-generation/adapters/kiro.js +2 -1
  61. package/dist/core/command-generation/adapters/lingma.js +5 -5
  62. package/dist/core/command-generation/adapters/oh-my-pi.d.ts +2 -2
  63. package/dist/core/command-generation/adapters/oh-my-pi.js +3 -5
  64. package/dist/core/command-generation/adapters/opencode.js +3 -5
  65. package/dist/core/command-generation/adapters/pi.d.ts +2 -2
  66. package/dist/core/command-generation/adapters/pi.js +3 -6
  67. package/dist/core/command-generation/adapters/qoder.js +5 -5
  68. package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
  69. package/dist/core/command-generation/adapters/qwen.js +12 -7
  70. package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
  71. package/dist/core/command-generation/adapters/roocode.js +4 -4
  72. package/dist/core/command-generation/adapters/trae.js +1 -21
  73. package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
  74. package/dist/core/command-generation/adapters/zcode.js +33 -0
  75. package/dist/core/command-generation/generator.d.ts +8 -0
  76. package/dist/core/command-generation/generator.js +15 -1
  77. package/dist/core/command-generation/index.d.ts +1 -1
  78. package/dist/core/command-generation/index.js +1 -1
  79. package/dist/core/command-generation/invocation.d.ts +73 -0
  80. package/dist/core/command-generation/invocation.js +77 -0
  81. package/dist/core/command-generation/registry.js +4 -4
  82. package/dist/core/command-generation/types.d.ts +9 -2
  83. package/dist/core/command-generation/yaml.d.ts +20 -6
  84. package/dist/core/command-generation/yaml.js +31 -21
  85. package/dist/core/command-surface.d.ts +15 -0
  86. package/dist/core/command-surface.js +33 -0
  87. package/dist/core/completions/command-registry.js +16 -2
  88. package/dist/core/completions/generators/powershell-generator.js +15 -8
  89. package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
  90. package/dist/core/completions/installers/zsh-installer.js +20 -4
  91. package/dist/core/config-prompts.d.ts +1 -1
  92. package/dist/core/config-prompts.js +14 -1
  93. package/dist/core/config-schema.d.ts +10 -1
  94. package/dist/core/config-schema.js +67 -1
  95. package/dist/core/config.d.ts +15 -0
  96. package/dist/core/config.js +45 -5
  97. package/dist/core/file-state.js +56 -24
  98. package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
  99. package/dist/core/github-copilot/cloud-agent.js +497 -0
  100. package/dist/core/global-config.d.ts +16 -0
  101. package/dist/core/init.d.ts +58 -0
  102. package/dist/core/init.js +450 -69
  103. package/dist/core/legacy-cleanup.d.ts +59 -0
  104. package/dist/core/legacy-cleanup.js +253 -24
  105. package/dist/core/list.js +5 -7
  106. package/dist/core/migration.d.ts +73 -1
  107. package/dist/core/migration.js +360 -11
  108. package/dist/core/onboarding-commands.d.ts +30 -0
  109. package/dist/core/onboarding-commands.js +38 -0
  110. package/dist/core/parsers/change-parser.d.ts +17 -2
  111. package/dist/core/parsers/change-parser.js +33 -20
  112. package/dist/core/parsers/code-fence.d.ts +15 -0
  113. package/dist/core/parsers/code-fence.js +49 -0
  114. package/dist/core/parsers/markdown-parser.js +2 -1
  115. package/dist/core/parsers/requirement-blocks.d.ts +18 -0
  116. package/dist/core/parsers/requirement-blocks.js +120 -35
  117. package/dist/core/parsers/requirement-text.d.ts +1 -8
  118. package/dist/core/parsers/requirement-text.js +4 -42
  119. package/dist/core/parsers/spec-structure.d.ts +1 -1
  120. package/dist/core/parsers/spec-structure.js +20 -30
  121. package/dist/core/profile-sync-drift.d.ts +0 -8
  122. package/dist/core/profile-sync-drift.js +48 -49
  123. package/dist/core/project-config.d.ts +28 -6
  124. package/dist/core/project-config.js +108 -8
  125. package/dist/core/references.js +42 -4
  126. package/dist/core/relationship-health.d.ts +8 -0
  127. package/dist/core/relationship-health.js +13 -0
  128. package/dist/core/root-selection.d.ts +9 -6
  129. package/dist/core/root-selection.js +42 -5
  130. package/dist/core/shared/index.d.ts +1 -0
  131. package/dist/core/shared/index.js +1 -0
  132. package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
  133. package/dist/core/shared/skill-content-equivalence.js +41 -0
  134. package/dist/core/shared/skill-paths.d.ts +13 -0
  135. package/dist/core/shared/skill-paths.js +23 -0
  136. package/dist/core/shared/tool-detection.d.ts +25 -5
  137. package/dist/core/shared/tool-detection.js +166 -36
  138. package/dist/core/shared-skill-target.d.ts +16 -0
  139. package/dist/core/shared-skill-target.js +156 -0
  140. package/dist/core/specs-apply.d.ts +80 -33
  141. package/dist/core/specs-apply.js +659 -171
  142. package/dist/core/store/git.d.ts +13 -0
  143. package/dist/core/store/git.js +24 -0
  144. package/dist/core/templates/skill-templates.d.ts +1 -1
  145. package/dist/core/templates/skill-templates.js +1 -1
  146. package/dist/core/templates/workflows/apply-change.d.ts +10 -0
  147. package/dist/core/templates/workflows/apply-change.js +48 -158
  148. package/dist/core/templates/workflows/archive-change.js +170 -42
  149. package/dist/core/templates/workflows/bulk-archive-change.js +256 -75
  150. package/dist/core/templates/workflows/continue-change.js +38 -46
  151. package/dist/core/templates/workflows/explore.js +54 -18
  152. package/dist/core/templates/workflows/ff-change.js +46 -30
  153. package/dist/core/templates/workflows/new-change.js +2 -2
  154. package/dist/core/templates/workflows/onboard.js +13 -7
  155. package/dist/core/templates/workflows/propose.js +116 -46
  156. package/dist/core/templates/workflows/store-selection.d.ts +1 -1
  157. package/dist/core/templates/workflows/store-selection.js +1 -1
  158. package/dist/core/templates/workflows/sync-specs.js +260 -32
  159. package/dist/core/templates/workflows/update-change.js +26 -16
  160. package/dist/core/templates/workflows/verify-change.js +16 -10
  161. package/dist/core/update.d.ts +40 -5
  162. package/dist/core/update.js +389 -93
  163. package/dist/core/validation/constants.d.ts +4 -1
  164. package/dist/core/validation/constants.js +4 -1
  165. package/dist/core/validation/validator.d.ts +22 -8
  166. package/dist/core/validation/validator.js +222 -50
  167. package/dist/core/version-check.d.ts +152 -0
  168. package/dist/core/version-check.js +731 -0
  169. package/dist/core/view.js +12 -17
  170. package/dist/prompts/searchable-multi-select.js +1 -1
  171. package/dist/telemetry/config.d.ts +3 -4
  172. package/dist/telemetry/index.d.ts +10 -5
  173. package/dist/telemetry/index.js +75 -41
  174. package/dist/ui/welcome-screen.d.ts +9 -1
  175. package/dist/ui/welcome-screen.js +91 -38
  176. package/dist/utils/change-metadata.d.ts +43 -0
  177. package/dist/utils/change-metadata.js +123 -7
  178. package/dist/utils/change-utils.d.ts +10 -4
  179. package/dist/utils/change-utils.js +20 -13
  180. package/dist/utils/ci.d.ts +12 -0
  181. package/dist/utils/ci.js +16 -0
  182. package/dist/utils/command-references.d.ts +92 -6
  183. package/dist/utils/command-references.js +180 -7
  184. package/dist/utils/date.d.ts +8 -0
  185. package/dist/utils/date.js +13 -0
  186. package/dist/utils/file-system.d.ts +10 -0
  187. package/dist/utils/file-system.js +70 -0
  188. package/dist/utils/index.d.ts +1 -1
  189. package/dist/utils/index.js +1 -1
  190. package/dist/utils/interactive.d.ts +23 -0
  191. package/dist/utils/interactive.js +32 -0
  192. package/dist/utils/item-discovery.d.ts +19 -0
  193. package/dist/utils/item-discovery.js +30 -48
  194. package/dist/utils/shell-detection.d.ts +1 -1
  195. package/dist/utils/shell-detection.js +64 -11
  196. package/dist/utils/spec-discovery.d.ts +40 -0
  197. package/dist/utils/spec-discovery.js +115 -0
  198. package/dist/utils/task-progress.d.ts +17 -0
  199. package/dist/utils/task-progress.js +41 -13
  200. package/package.json +14 -12
  201. package/schemas/spec-driven/schema.yaml +67 -10
  202. package/schemas/spec-driven/templates/design.md +2 -2
  203. package/schemas/spec-driven/templates/proposal.md +10 -4
  204. package/schemas/spec-driven/templates/spec.md +3 -0
  205. package/dist/core/command-generation/adapters/codex.d.ts +0 -16
  206. package/dist/core/command-generation/adapters/codex.js +0 -39
  207. package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
  208. package/dist/core/command-generation/adapters/windsurf.js +0 -38
@@ -7,13 +7,14 @@
7
7
  import ora from 'ora';
8
8
  import path from 'path';
9
9
  import * as fs from 'fs';
10
- import { loadChangeContext, generateInstructions, resolveSchema, resolveArtifactOutputs, } from '../../core/artifact-graph/index.js';
10
+ import { loadChangeContext, generateInstructions, resolveSchema, resolveArtifactOutputPath, resolveArtifactOutputs, } from '../../core/artifact-graph/index.js';
11
11
  import { getChangeDir, resolveCurrentPlanningHomeSync, } from '../../core/planning-home.js';
12
12
  import { resolveRootForCommand, withStoreFlag, toPlanningHome, toRootOutput, } from '../../core/root-selection.js';
13
13
  import { assembleReferenceIndex, renderReferencedStoresBlock, renderReferencedStoresSection, } from '../../core/references.js';
14
14
  import { readRegistrySnapshot } from '../../core/store/registry.js';
15
- import { readProjectConfig } from '../../core/project-config.js';
15
+ import { loadOperationInputs, readProjectConfig, } from '../../core/project-config.js';
16
16
  import { validateChangeExists, validateSchemaExists, } from './shared.js';
17
+ import { parseTaskLines } from '../../utils/task-progress.js';
17
18
  // -----------------------------------------------------------------------------
18
19
  // Artifact Instructions Command
19
20
  // -----------------------------------------------------------------------------
@@ -55,10 +56,12 @@ export async function instructionsCommand(artifactId, options) {
55
56
  if (options.schema) {
56
57
  validateSchemaExists(options.schema, projectRoot);
57
58
  }
59
+ const { projectConfig, references } = await loadRootConfigContext(root);
58
60
  // loadChangeContext will auto-detect schema from metadata if not provided
59
61
  const context = loadChangeContext(projectRoot, changeName, options.schema, {
60
62
  changeDir: getChangeDir(planningHome, changeName),
61
63
  planningHome,
64
+ projectConfig,
62
65
  });
63
66
  if (!artifactId) {
64
67
  spinner?.stop();
@@ -71,7 +74,6 @@ export async function instructionsCommand(artifactId, options) {
71
74
  const validIds = context.graph.getAllArtifacts().map((a) => a.id);
72
75
  throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'. Valid artifacts:\n ${validIds.join('\n ')}`);
73
76
  }
74
- const { projectConfig, references } = await loadRootConfigContext(root);
75
77
  const instructions = generateInstructions(context, artifactId, projectRoot, {
76
78
  projectConfig,
77
79
  references,
@@ -94,6 +96,17 @@ export function printInstructionsText(instructions, isBlocked) {
94
96
  // Opening tag
95
97
  console.log(`<artifact id="${artifactId}" change="${changeName}" schema="${schemaName}">`);
96
98
  console.log();
99
+ // Artifacts skipped via skip_specs get no creation directive: emitting the
100
+ // task/template anyway would prompt an agent to write spec files that
101
+ // validate then rejects as conflicting with the marker.
102
+ if (instructions.skipped) {
103
+ console.log('<warning>');
104
+ console.log(instructions.warning ?? 'This artifact is skipped (skip_specs is set in .openspec.yaml).');
105
+ console.log('</warning>');
106
+ console.log();
107
+ console.log('</artifact>');
108
+ return;
109
+ }
97
110
  // Warning for blocked artifacts
98
111
  if (isBlocked) {
99
112
  const missing = dependencies.filter((d) => !d.done).map((d) => d.id);
@@ -135,9 +148,18 @@ export function printInstructionsText(instructions, isBlocked) {
135
148
  // Dependencies (files to read for context)
136
149
  if (dependencies.length > 0) {
137
150
  console.log('<dependencies>');
138
- console.log('Read these files for context before creating this artifact:');
151
+ console.log('Read the current contents of these files before creating this artifact (re-read them from disk even if you saw them earlier - they may have been edited):');
139
152
  console.log();
140
153
  for (const dep of dependencies) {
154
+ // A dependency satisfied via skip_specs has no files by design: telling
155
+ // the agent to read them (or calling them "done") would send it hunting
156
+ // for spec files that must not exist.
157
+ if (dep.skipped) {
158
+ console.log(`<dependency id="${dep.id}" status="skipped">`);
159
+ console.log(` <description>Skipped: the change declares skip_specs, so this artifact has no files to read.</description>`);
160
+ console.log('</dependency>');
161
+ continue;
162
+ }
141
163
  const status = dep.done ? 'done' : 'missing';
142
164
  const fullPath = path.join(changeDir, dep.path);
143
165
  console.log(`<dependency id="${dep.id}" status="${status}">`);
@@ -185,25 +207,26 @@ export function printInstructionsText(instructions, isBlocked) {
185
207
  // Apply Instructions Command
186
208
  // -----------------------------------------------------------------------------
187
209
  /**
188
- * Parses tasks.md content and extracts task items with their completion status.
210
+ * Turns parsed task lines into the listed task items.
211
+ *
212
+ * A checkbox with no text after it is left out of the list: this is work for an
213
+ * agent to act on and tick off, and a bare `- [ ]` gives it nothing to match.
214
+ * It still counts toward progress, which is taken from every parsed line, so
215
+ * this list can be shorter than the totals beside it but never disagrees with
216
+ * `openspec list` or archive about how much work is left. An empty list is also
217
+ * what puts apply in its "nothing to work on" state, so a file of nothing but
218
+ * text-less checkboxes asks to be rewritten instead of being called done.
189
219
  */
190
- function parseTasksFile(content) {
220
+ function toTaskItems(parsed) {
191
221
  const tasks = [];
192
- const lines = content.split('\n');
193
- let taskIndex = 0;
194
- for (const line of lines) {
195
- // Match checkbox patterns: - [ ] or - [x] or - [X]
196
- const checkboxMatch = line.match(/^[-*]\s*\[([ xX])\]\s*(.+)\s*$/);
197
- if (checkboxMatch) {
198
- taskIndex++;
199
- const done = checkboxMatch[1].toLowerCase() === 'x';
200
- const description = checkboxMatch[2].trim();
201
- tasks.push({
202
- id: `${taskIndex}`,
203
- description,
204
- done,
205
- });
206
- }
222
+ for (const task of parsed) {
223
+ if (task.description.length === 0)
224
+ continue;
225
+ tasks.push({
226
+ id: `${tasks.length + 1}`,
227
+ description: task.description,
228
+ done: task.done,
229
+ });
207
230
  }
208
231
  return tasks;
209
232
  }
@@ -219,6 +242,7 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
219
242
  const context = loadChangeContext(projectRoot, changeName, schemaName, {
220
243
  changeDir: getChangeDir(planningHome, changeName),
221
244
  planningHome,
245
+ projectConfig: options.projectConfig,
222
246
  });
223
247
  const changeDir = context.changeDir;
224
248
  // Get the full schema to access the apply phase configuration
@@ -229,9 +253,15 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
229
253
  const requiredArtifactIds = applyConfig?.requires ?? schema.artifacts.map((a) => a.id);
230
254
  const tracksFile = applyConfig?.tracks ?? null;
231
255
  const schemaInstruction = applyConfig?.instruction ?? null;
232
- // Check which required artifacts are missing
256
+ const operationInputs = loadOperationInputs(options.projectConfig ?? null, 'apply');
257
+ // Check which required artifacts are missing. Artifacts the change skips
258
+ // via skip_specs count as present - their files must not exist, and
259
+ // status already reports them complete, so apply cannot block on them.
233
260
  const missingArtifacts = [];
234
261
  for (const artifactId of requiredArtifactIds) {
262
+ if (context.skippedArtifacts?.has(artifactId)) {
263
+ continue;
264
+ }
235
265
  const artifact = schema.artifacts.find((a) => a.id === artifactId);
236
266
  if (artifact && resolveArtifactOutputs(changeDir, artifact.generates).length === 0) {
237
267
  missingArtifacts.push(artifactId);
@@ -246,19 +276,21 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
246
276
  }
247
277
  }
248
278
  // Parse tasks if tracking file exists
249
- let tasks = [];
279
+ let parsedTasks = [];
250
280
  let tracksFileExists = false;
251
281
  if (tracksFile) {
252
- const tracksPath = path.join(changeDir, tracksFile);
282
+ const tracksPath = resolveArtifactOutputPath(changeDir, tracksFile);
253
283
  tracksFileExists = fs.existsSync(tracksPath);
254
284
  if (tracksFileExists) {
255
285
  const tasksContent = await fs.promises.readFile(tracksPath, 'utf-8');
256
- tasks = parseTasksFile(tasksContent);
286
+ parsedTasks = parseTaskLines(tasksContent);
257
287
  }
258
288
  }
259
- // Calculate progress
260
- const total = tasks.length;
261
- const complete = tasks.filter((t) => t.done).length;
289
+ const tasks = toTaskItems(parsedTasks);
290
+ // Calculate progress over every checkbox in the file, listed or not, so these
291
+ // numbers match `openspec list` and archive's incomplete-task check.
292
+ const total = parsedTasks.length;
293
+ const complete = parsedTasks.filter((task) => task.done).length;
262
294
  const remaining = total - complete;
263
295
  // Determine state and instruction
264
296
  let state;
@@ -273,11 +305,12 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
273
305
  state = 'blocked';
274
306
  instruction = `The ${tracksFilename} file is missing and must be created.\nUse openspec-continue-change to generate the tracking file.`;
275
307
  }
276
- else if (tracksFile && tracksFileExists && total === 0) {
277
- // Tracking file exists but contains no tasks
308
+ else if (tracksFile && tracksFileExists && tasks.length === 0) {
309
+ // Tracking file exists but lists nothing an agent can work on: either no
310
+ // checkboxes at all, or only checkboxes with no text after them.
278
311
  const tracksFilename = path.basename(tracksFile);
279
312
  state = 'blocked';
280
- instruction = `The ${tracksFilename} file exists but contains no tasks.\nAdd tasks to ${tracksFilename} or regenerate it with openspec-continue-change.`;
313
+ instruction = `The ${tracksFilename} file exists but contains no tasks to work on.\nAdd tasks to ${tracksFilename} or regenerate it with openspec-continue-change.`;
281
314
  }
282
315
  else if (tracksFile && remaining === 0 && total > 0) {
283
316
  state = 'all_done';
@@ -303,6 +336,7 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
303
336
  missingArtifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
304
337
  instruction,
305
338
  ...(references !== undefined ? { references } : {}),
339
+ ...operationInputs,
306
340
  };
307
341
  }
308
342
  export async function applyInstructionsCommand(options) {
@@ -320,11 +354,13 @@ export async function applyInstructionsCommand(options) {
320
354
  if (options.schema) {
321
355
  validateSchemaExists(options.schema, projectRoot);
322
356
  }
323
- // generateApplyInstructions uses loadChangeContext which auto-detects schema
324
- const { references } = await loadRootConfigContext(root);
357
+ // One parsed config snapshot supplies schema fallback, references, context,
358
+ // and operation guidance for this command.
359
+ const { projectConfig, references } = await loadRootConfigContext(root);
325
360
  const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema, {
326
361
  planningHome,
327
362
  references,
363
+ projectConfig,
328
364
  });
329
365
  spinner?.stop();
330
366
  if (options.json) {
@@ -389,5 +425,57 @@ export function printApplyInstructionsText(instructions) {
389
425
  // Instruction
390
426
  console.log('### Instruction');
391
427
  console.log(instruction);
428
+ console.log();
429
+ printOperationInputsText(instructions);
430
+ }
431
+ export function generateArchiveInstructions(changeName, projectConfig) {
432
+ return {
433
+ changeName,
434
+ ...loadOperationInputs(projectConfig, 'archive'),
435
+ };
436
+ }
437
+ export async function archiveInstructionsCommand(options) {
438
+ const root = await resolveRootForCommand(options, { json: options.json });
439
+ if (!root) {
440
+ return;
441
+ }
442
+ const spinner = options.json ? undefined : ora('Loading archive inputs...').start();
443
+ try {
444
+ const changeName = await validateChangeExists(options.change, root.path, root.changesDir, { newChangeHint: withStoreFlag(root, 'openspec new change <name>') });
445
+ const projectConfig = readProjectConfig(root.path);
446
+ const instructions = generateArchiveInstructions(changeName, projectConfig);
447
+ spinner?.stop();
448
+ if (options.json) {
449
+ console.log(JSON.stringify({ ...instructions, root: toRootOutput(root) }, null, 2));
450
+ return;
451
+ }
452
+ printArchiveInstructionsText(instructions);
453
+ }
454
+ catch (error) {
455
+ spinner?.stop();
456
+ throw error;
457
+ }
458
+ }
459
+ export function printArchiveInstructionsText(instructions) {
460
+ console.log(`## Archive Inputs: ${instructions.changeName}`);
461
+ console.log();
462
+ printOperationInputsText(instructions);
463
+ }
464
+ function printOperationInputsText(inputs) {
465
+ if (inputs.context) {
466
+ console.log('### Project Context (required instruction input)');
467
+ console.log(inputs.context);
468
+ console.log();
469
+ }
470
+ if (inputs.operationGuidance && inputs.operationGuidance.length > 0) {
471
+ console.log('### Operation Guidance (advisory)');
472
+ for (const guidance of inputs.operationGuidance) {
473
+ console.log(`- ${guidance}`);
474
+ }
475
+ console.log();
476
+ }
477
+ if (!inputs.context && !inputs.operationGuidance) {
478
+ console.log('No project context or operation guidance configured.');
479
+ }
392
480
  }
393
481
  //# sourceMappingURL=instructions.js.map
@@ -33,6 +33,17 @@ export interface ApplyInstructions {
33
33
  instruction: string;
34
34
  /** Referenced-store index (read-only upstream context; omitted when none declared) */
35
35
  references?: ReferenceIndexEntry[];
36
+ /** Current project background from the selected root. */
37
+ context?: string;
38
+ /** Current advisory guidance for apply. */
39
+ operationGuidance?: string[];
40
+ }
41
+ export interface ArchiveInstructions {
42
+ changeName: string;
43
+ /** Current project background from the selected root. */
44
+ context?: string;
45
+ /** Current advisory guidance for archive. */
46
+ operationGuidance?: string[];
36
47
  }
37
48
  export declare const DEFAULT_SCHEMA = "spec-driven";
38
49
  export declare function printJson(payload: unknown): void;
@@ -44,11 +55,11 @@ export declare function isColorDisabled(): boolean;
44
55
  /**
45
56
  * Gets the color function based on status.
46
57
  */
47
- export declare function getStatusColor(status: 'done' | 'ready' | 'blocked'): (text: string) => string;
58
+ export declare function getStatusColor(status: 'done' | 'skipped' | 'ready' | 'blocked'): (text: string) => string;
48
59
  /**
49
60
  * Gets the status indicator for an artifact.
50
61
  */
51
- export declare function getStatusIndicator(status: 'done' | 'ready' | 'blocked'): string;
62
+ export declare function getStatusIndicator(status: 'done' | 'skipped' | 'ready' | 'blocked'): string;
52
63
  /**
53
64
  * Returns the list of available change directory names under openspec/changes/.
54
65
  * Excludes the archive directory and hidden directories.
@@ -9,7 +9,6 @@ import path from 'path';
9
9
  import * as fs from 'fs';
10
10
  import { getSchemaDir, listSchemas } from '../../core/artifact-graph/index.js';
11
11
  import { isRootSelectionError } from '../../core/root-selection.js';
12
- import { validateChangeName } from '../../utils/change-utils.js';
13
12
  // -----------------------------------------------------------------------------
14
13
  // Constants
15
14
  // -----------------------------------------------------------------------------
@@ -46,6 +45,8 @@ export function getStatusColor(status) {
46
45
  switch (status) {
47
46
  case 'done':
48
47
  return chalk.green;
48
+ case 'skipped':
49
+ return chalk.gray;
49
50
  case 'ready':
50
51
  return chalk.yellow;
51
52
  case 'blocked':
@@ -60,6 +61,8 @@ export function getStatusIndicator(status) {
60
61
  switch (status) {
61
62
  case 'done':
62
63
  return color('[x]');
64
+ case 'skipped':
65
+ return color('[~]');
63
66
  case 'ready':
64
67
  return color('[ ]');
65
68
  case 'blocked':
@@ -84,6 +87,33 @@ export async function getAvailableChanges(projectRoot, changesDir = path.join(pr
84
87
  throw error;
85
88
  }
86
89
  }
90
+ /**
91
+ * Validates a change name used to look up an existing change directory.
92
+ * Lookup accepts any directory name that `getAvailableChanges` could return
93
+ * (the kebab-case convention in `validateChangeName` applies at creation
94
+ * time only); it only rejects names that would escape the changes directory
95
+ * or address entries `getAvailableChanges` excludes (hidden dirs, archive).
96
+ *
97
+ * @returns An error message, or undefined if the name is safe to look up
98
+ */
99
+ function validateChangeLookupName(changeName) {
100
+ if (changeName === '.' || changeName === '..') {
101
+ return 'Change name cannot be a relative path segment';
102
+ }
103
+ if (changeName.includes('/') || changeName.includes('\\')) {
104
+ return 'Change name cannot contain path separators';
105
+ }
106
+ if (changeName.includes('\0')) {
107
+ return 'Change name cannot contain null characters';
108
+ }
109
+ if (changeName.startsWith('.')) {
110
+ return 'Change name cannot start with a dot';
111
+ }
112
+ if (changeName === 'archive') {
113
+ return "'archive' is reserved for archived changes";
114
+ }
115
+ return undefined;
116
+ }
87
117
  /**
88
118
  * Validates that a change exists and returns available changes if not.
89
119
  * Checks directory existence directly to support scaffolded changes (without proposal.md).
@@ -100,9 +130,9 @@ export async function validateChangeExists(changeName, projectRoot, changesDir =
100
130
  throw new Error(`Missing required option --change. Available changes:\n ${available.join('\n ')}`);
101
131
  }
102
132
  // Validate change name format to prevent path traversal
103
- const nameValidation = validateChangeName(changeName);
104
- if (!nameValidation.valid) {
105
- throw new Error(`Invalid change name '${changeName}': ${nameValidation.error}`);
133
+ const lookupError = validateChangeLookupName(changeName);
134
+ if (lookupError) {
135
+ throw new Error(`Invalid change name '${changeName}': ${lookupError}`);
106
136
  }
107
137
  // Check directory existence directly
108
138
  const changePath = path.join(changesDir, changeName);
@@ -67,26 +67,31 @@ export async function statusCommand(options) {
67
67
  }
68
68
  export function printStatusText(status) {
69
69
  const doneCount = status.artifacts.filter((a) => a.status === 'done').length;
70
- const total = status.artifacts.length;
70
+ const skippedCount = status.artifacts.filter((a) => a.status === 'skipped').length;
71
+ const total = status.artifacts.length - skippedCount;
71
72
  console.log(`Change: ${status.changeName}`);
72
73
  console.log(`Schema: ${status.schemaName}`);
73
74
  if (status.changeRoot) {
74
75
  console.log(`Change root: ${status.changeRoot}`);
75
76
  }
76
- console.log(`Progress: ${doneCount}/${total} artifacts complete`);
77
+ const skippedSuffix = skippedCount > 0 ? ` (${skippedCount} skipped)` : '';
78
+ console.log(`Progress: ${doneCount}/${total} artifacts complete${skippedSuffix}`);
77
79
  console.log();
78
80
  for (const artifact of status.artifacts) {
79
81
  const indicator = getStatusIndicator(artifact.status);
80
82
  const color = getStatusColor(artifact.status);
81
83
  let line = `${indicator} ${artifact.id}`;
84
+ if (artifact.status === 'skipped') {
85
+ line += color(' (skipped: change declares skip_specs)');
86
+ }
82
87
  if (artifact.status === 'blocked' && artifact.missingDeps && artifact.missingDeps.length > 0) {
83
88
  line += color(` (blocked by: ${artifact.missingDeps.join(', ')})`);
84
89
  }
85
90
  console.log(line);
86
91
  }
87
- if (status.isComplete) {
92
+ if (status.isPlanningComplete) {
88
93
  console.log();
89
- console.log(chalk.green('All artifacts complete!'));
94
+ console.log(chalk.green('All planning artifacts complete!'));
90
95
  }
91
96
  }
92
97
  //# sourceMappingURL=status.js.map
@@ -39,11 +39,21 @@ export async function templatesCommand(options) {
39
39
  else {
40
40
  source = 'package';
41
41
  }
42
- const templates = graph.getAllArtifacts().map((artifact) => ({
43
- artifactId: artifact.id,
44
- templatePath: FileSystemUtils.canonicalizeExistingPath(path.join(schemaDir, 'templates', artifact.template)),
45
- source,
46
- }));
42
+ const templatesDir = path.join(schemaDir, 'templates');
43
+ const templates = graph.getAllArtifacts().map((artifact) => {
44
+ const templatePath = path.join(templatesDir, artifact.template);
45
+ try {
46
+ FileSystemUtils.assertPathWithin(templatesDir, templatePath);
47
+ return {
48
+ artifactId: artifact.id,
49
+ templatePath: FileSystemUtils.canonicalizeExistingPath(templatePath),
50
+ source,
51
+ };
52
+ }
53
+ catch {
54
+ throw new Error(`Template '${artifact.template}' for artifact '${artifact.id}' points outside the schema templates directory`);
55
+ }
56
+ });
47
57
  spinner?.stop();
48
58
  if (options.json) {
49
59
  const output = {};
@@ -1,3 +1,17 @@
1
+ /**
2
+ * True when the ONLY thing wrong with a rebuilt spec is that it has no
3
+ * requirements. That is the exact failure retiring a capability replaces
4
+ * (#1302); anything else means the spec is broken in a way the author still has
5
+ * to fix, so archive must abort exactly as it always did instead of retiring.
6
+ *
7
+ * Asking the validator - rather than counting requirement blocks a second time -
8
+ * is what makes "this spec could not have been written anyway" true by
9
+ * construction. The two counts genuinely disagree: `MarkdownParser` accepts any
10
+ * `###` heading under `## Requirements` as a requirement, while the delta block
11
+ * parser only indexes canonical `### Requirement:` headers and sweeps the rest
12
+ * into the preamble, which survives into the rebuilt spec.
13
+ */
14
+ export declare function isRetirableSpec(specName: string, rebuilt: string): Promise<boolean>;
1
15
  export interface ArchiveOptions {
2
16
  yes?: boolean;
3
17
  skipSpecs?: boolean;
@@ -17,6 +31,5 @@ export declare class ArchiveCommand {
17
31
  */
18
32
  private run;
19
33
  private selectChange;
20
- private getArchiveDate;
21
34
  }
22
35
  //# sourceMappingURL=archive.d.ts.map