@fission-ai/openspec 1.6.0 → 1.7.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 (175) hide show
  1. package/README.md +28 -1
  2. package/dist/cli/index.js +73 -7
  3. package/dist/commands/change.d.ts +0 -1
  4. package/dist/commands/change.js +90 -50
  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 +21 -11
  9. package/dist/commands/show.js +5 -2
  10. package/dist/commands/spec.js +20 -24
  11. package/dist/commands/validate.js +18 -3
  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 +89 -6
  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 +7 -2
  19. package/dist/core/archive.d.ts +0 -1
  20. package/dist/core/archive.js +96 -33
  21. package/dist/core/artifact-graph/graph.d.ts +16 -0
  22. package/dist/core/artifact-graph/graph.js +30 -6
  23. package/dist/core/artifact-graph/instruction-loader.d.ts +27 -2
  24. package/dist/core/artifact-graph/instruction-loader.js +55 -7
  25. package/dist/core/artifact-graph/resolver.d.ts +14 -0
  26. package/dist/core/artifact-graph/resolver.js +34 -6
  27. package/dist/core/change-metadata/schema.d.ts +1 -0
  28. package/dist/core/change-metadata/schema.js +6 -0
  29. package/dist/core/change-status-policy.d.ts +1 -1
  30. package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
  31. package/dist/core/command-generation/adapters/amazon-q.js +7 -1
  32. package/dist/core/command-generation/adapters/antigravity.js +2 -1
  33. package/dist/core/command-generation/adapters/auggie.js +2 -1
  34. package/dist/core/command-generation/adapters/bob.d.ts +5 -1
  35. package/dist/core/command-generation/adapters/bob.js +6 -5
  36. package/dist/core/command-generation/adapters/claude.js +1 -8
  37. package/dist/core/command-generation/adapters/codebuddy.js +3 -2
  38. package/dist/core/command-generation/adapters/continue.js +3 -2
  39. package/dist/core/command-generation/adapters/costrict.js +2 -1
  40. package/dist/core/command-generation/adapters/crush.js +5 -5
  41. package/dist/core/command-generation/adapters/cursor.js +2 -2
  42. package/dist/core/command-generation/adapters/devin.d.ts +19 -0
  43. package/dist/core/command-generation/adapters/devin.js +36 -0
  44. package/dist/core/command-generation/adapters/factory.js +2 -1
  45. package/dist/core/command-generation/adapters/gemini.js +37 -2
  46. package/dist/core/command-generation/adapters/github-copilot.js +2 -1
  47. package/dist/core/command-generation/adapters/iflow.js +5 -4
  48. package/dist/core/command-generation/adapters/index.d.ts +2 -2
  49. package/dist/core/command-generation/adapters/index.js +2 -2
  50. package/dist/core/command-generation/adapters/junie.js +2 -1
  51. package/dist/core/command-generation/adapters/kiro.js +2 -1
  52. package/dist/core/command-generation/adapters/lingma.js +5 -5
  53. package/dist/core/command-generation/adapters/oh-my-pi.d.ts +2 -2
  54. package/dist/core/command-generation/adapters/oh-my-pi.js +3 -5
  55. package/dist/core/command-generation/adapters/opencode.js +3 -5
  56. package/dist/core/command-generation/adapters/pi.d.ts +2 -2
  57. package/dist/core/command-generation/adapters/pi.js +3 -6
  58. package/dist/core/command-generation/adapters/qoder.js +5 -5
  59. package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
  60. package/dist/core/command-generation/adapters/qwen.js +12 -7
  61. package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
  62. package/dist/core/command-generation/adapters/roocode.js +4 -4
  63. package/dist/core/command-generation/adapters/trae.js +1 -21
  64. package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
  65. package/dist/core/command-generation/adapters/zcode.js +33 -0
  66. package/dist/core/command-generation/generator.d.ts +8 -0
  67. package/dist/core/command-generation/generator.js +15 -1
  68. package/dist/core/command-generation/index.d.ts +1 -1
  69. package/dist/core/command-generation/index.js +1 -1
  70. package/dist/core/command-generation/invocation.d.ts +73 -0
  71. package/dist/core/command-generation/invocation.js +77 -0
  72. package/dist/core/command-generation/registry.js +4 -4
  73. package/dist/core/command-generation/types.d.ts +9 -2
  74. package/dist/core/command-generation/yaml.d.ts +20 -6
  75. package/dist/core/command-generation/yaml.js +31 -21
  76. package/dist/core/command-surface.d.ts +15 -0
  77. package/dist/core/command-surface.js +33 -0
  78. package/dist/core/completions/command-registry.js +8 -2
  79. package/dist/core/completions/generators/powershell-generator.js +15 -8
  80. package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
  81. package/dist/core/completions/installers/zsh-installer.js +20 -4
  82. package/dist/core/config-prompts.d.ts +1 -1
  83. package/dist/core/config-prompts.js +14 -1
  84. package/dist/core/config-schema.d.ts +7 -1
  85. package/dist/core/config-schema.js +40 -1
  86. package/dist/core/config.d.ts +12 -0
  87. package/dist/core/config.js +21 -3
  88. package/dist/core/global-config.d.ts +5 -0
  89. package/dist/core/init.d.ts +35 -0
  90. package/dist/core/init.js +225 -39
  91. package/dist/core/legacy-cleanup.d.ts +59 -0
  92. package/dist/core/legacy-cleanup.js +249 -22
  93. package/dist/core/list.js +5 -7
  94. package/dist/core/migration.d.ts +71 -1
  95. package/dist/core/migration.js +294 -1
  96. package/dist/core/onboarding-commands.d.ts +30 -0
  97. package/dist/core/onboarding-commands.js +38 -0
  98. package/dist/core/parsers/change-parser.d.ts +17 -2
  99. package/dist/core/parsers/change-parser.js +33 -20
  100. package/dist/core/parsers/code-fence.d.ts +15 -0
  101. package/dist/core/parsers/code-fence.js +49 -0
  102. package/dist/core/parsers/markdown-parser.js +2 -1
  103. package/dist/core/parsers/requirement-blocks.d.ts +8 -0
  104. package/dist/core/parsers/requirement-blocks.js +60 -35
  105. package/dist/core/parsers/requirement-text.d.ts +1 -8
  106. package/dist/core/parsers/requirement-text.js +4 -42
  107. package/dist/core/parsers/spec-structure.js +3 -29
  108. package/dist/core/profile-sync-drift.d.ts +0 -8
  109. package/dist/core/profile-sync-drift.js +9 -45
  110. package/dist/core/project-config.d.ts +25 -6
  111. package/dist/core/project-config.js +77 -7
  112. package/dist/core/references.js +42 -4
  113. package/dist/core/relationship-health.d.ts +8 -0
  114. package/dist/core/relationship-health.js +13 -0
  115. package/dist/core/root-selection.d.ts +9 -6
  116. package/dist/core/root-selection.js +42 -5
  117. package/dist/core/shared/tool-detection.d.ts +25 -5
  118. package/dist/core/shared/tool-detection.js +105 -6
  119. package/dist/core/specs-apply.d.ts +8 -33
  120. package/dist/core/specs-apply.js +252 -157
  121. package/dist/core/store/git.d.ts +13 -0
  122. package/dist/core/store/git.js +24 -0
  123. package/dist/core/templates/workflows/apply-change.js +54 -4
  124. package/dist/core/templates/workflows/archive-change.js +166 -42
  125. package/dist/core/templates/workflows/bulk-archive-change.js +240 -63
  126. package/dist/core/templates/workflows/continue-change.js +32 -40
  127. package/dist/core/templates/workflows/explore.js +12 -0
  128. package/dist/core/templates/workflows/ff-change.js +46 -30
  129. package/dist/core/templates/workflows/new-change.js +2 -2
  130. package/dist/core/templates/workflows/onboard.js +4 -4
  131. package/dist/core/templates/workflows/propose.js +50 -32
  132. package/dist/core/templates/workflows/store-selection.d.ts +1 -1
  133. package/dist/core/templates/workflows/store-selection.js +1 -1
  134. package/dist/core/templates/workflows/sync-specs.js +172 -22
  135. package/dist/core/templates/workflows/update-change.js +20 -12
  136. package/dist/core/templates/workflows/verify-change.js +16 -10
  137. package/dist/core/update.d.ts +39 -5
  138. package/dist/core/update.js +288 -71
  139. package/dist/core/validation/constants.d.ts +4 -1
  140. package/dist/core/validation/constants.js +4 -1
  141. package/dist/core/validation/validator.d.ts +1 -7
  142. package/dist/core/validation/validator.js +92 -36
  143. package/dist/core/version-check.d.ts +152 -0
  144. package/dist/core/version-check.js +736 -0
  145. package/dist/core/view.js +12 -17
  146. package/dist/prompts/searchable-multi-select.js +1 -1
  147. package/dist/telemetry/index.d.ts +1 -1
  148. package/dist/telemetry/index.js +55 -33
  149. package/dist/ui/welcome-screen.d.ts +9 -1
  150. package/dist/ui/welcome-screen.js +91 -38
  151. package/dist/utils/change-metadata.d.ts +30 -0
  152. package/dist/utils/change-metadata.js +101 -7
  153. package/dist/utils/change-utils.d.ts +10 -4
  154. package/dist/utils/change-utils.js +20 -13
  155. package/dist/utils/command-references.d.ts +80 -6
  156. package/dist/utils/command-references.js +138 -7
  157. package/dist/utils/date.d.ts +8 -0
  158. package/dist/utils/date.js +13 -0
  159. package/dist/utils/index.d.ts +1 -1
  160. package/dist/utils/index.js +1 -1
  161. package/dist/utils/item-discovery.d.ts +19 -0
  162. package/dist/utils/item-discovery.js +30 -48
  163. package/dist/utils/shell-detection.d.ts +1 -1
  164. package/dist/utils/shell-detection.js +64 -11
  165. package/dist/utils/spec-discovery.d.ts +40 -0
  166. package/dist/utils/spec-discovery.js +102 -0
  167. package/package.json +14 -12
  168. package/schemas/spec-driven/schema.yaml +60 -5
  169. package/schemas/spec-driven/templates/design.md +2 -2
  170. package/schemas/spec-driven/templates/proposal.md +5 -1
  171. package/schemas/spec-driven/templates/spec.md +3 -0
  172. package/dist/core/command-generation/adapters/codex.d.ts +0 -16
  173. package/dist/core/command-generation/adapters/codex.js +0 -39
  174. package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
  175. package/dist/core/command-generation/adapters/windsurf.js +0 -38
@@ -2,7 +2,7 @@ import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import ora from 'ora';
4
4
  import { stringify as stringifyYaml } from 'yaml';
5
- import { getSchemaDir, getProjectSchemasDir, getUserSchemasDir, getPackageSchemasDir, listSchemas, } from '../core/artifact-graph/resolver.js';
5
+ import { getSchemaDir, getProjectSchemasDir, getUserSchemasDir, getPackageSchemasDir, isSchemaDir, listSchemas, } from '../core/artifact-graph/resolver.js';
6
6
  import { parseSchema, SchemaValidationError } from '../core/artifact-graph/schema.js';
7
7
  /**
8
8
  * Check all three locations for a schema and return which ones exist.
@@ -339,7 +339,7 @@ export function registerSchemaCommand(program) {
339
339
  const schemaResults = [];
340
340
  let anyInvalid = false;
341
341
  for (const entry of entries) {
342
- if (!entry.isDirectory())
342
+ if (!isSchemaDir(projectSchemasDir, entry))
343
343
  continue;
344
344
  const schemaDir = path.join(projectSchemasDir, entry.name);
345
345
  const schemaPath = path.join(schemaDir, 'schema.yaml');
@@ -584,8 +584,9 @@ export function registerSchemaCommand(program) {
584
584
  return;
585
585
  }
586
586
  const schemaDir = path.join(getProjectSchemasDir(projectRoot), name);
587
- // Check if exists
588
- if (fs.existsSync(schemaDir)) {
587
+ // Check overwrite permission without mutating the destination
588
+ const schemaExists = fs.existsSync(schemaDir);
589
+ if (schemaExists) {
589
590
  if (!options?.force) {
590
591
  if (options?.json) {
591
592
  console.log(JSON.stringify({
@@ -601,9 +602,6 @@ export function registerSchemaCommand(program) {
601
602
  process.exitCode = 1;
602
603
  return;
603
604
  }
604
- if (spinner)
605
- spinner.start(`Removing existing schema '${name}'...`);
606
- fs.rmSync(schemaDir, { recursive: true });
607
605
  }
608
606
  // Determine artifacts and description
609
607
  let description;
@@ -625,6 +623,12 @@ export function registerSchemaCommand(program) {
625
623
  }));
626
624
  selectedArtifactIds = await checkbox({
627
625
  message: 'Select artifacts to include:',
626
+ theme: {
627
+ icon: {
628
+ checked: '[x]',
629
+ unchecked: '[ ]',
630
+ },
631
+ },
628
632
  choices: artifactChoices,
629
633
  });
630
634
  if (selectedArtifactIds.length === 0) {
@@ -673,10 +677,6 @@ export function registerSchemaCommand(program) {
673
677
  selectedArtifactIds = DEFAULT_ARTIFACTS.map((a) => a.id);
674
678
  }
675
679
  }
676
- // Create schema directory
677
- if (spinner)
678
- spinner.start(`Creating schema '${name}'...`);
679
- fs.mkdirSync(schemaDir, { recursive: true });
680
680
  // Build artifacts array with proper dependencies
681
681
  const selectedArtifacts = selectedArtifactIds.map((id) => {
682
682
  const template = DEFAULT_ARTIFACTS.find((a) => a.id === id);
@@ -718,6 +718,16 @@ export function registerSchemaCommand(program) {
718
718
  tracks: 'tasks.md',
719
719
  };
720
720
  }
721
+ // Replace only after all inputs have been collected and validated
722
+ if (schemaExists) {
723
+ if (spinner)
724
+ spinner.start(`Removing existing schema '${name}'...`);
725
+ fs.rmSync(schemaDir, { recursive: true });
726
+ }
727
+ // Create schema directory
728
+ if (spinner)
729
+ spinner.start(`Creating schema '${name}'...`);
730
+ fs.mkdirSync(schemaDir, { recursive: true });
721
731
  fs.writeFileSync(path.join(schemaDir, 'schema.yaml'), stringifyYaml(schema));
722
732
  // Create template files in templates/ subdirectory (standard location)
723
733
  const templatesDir = path.join(schemaDir, 'templates');
@@ -157,14 +157,17 @@ export class ShowCommand {
157
157
  }
158
158
  warnIrrelevantFlags(type, options) {
159
159
  const irrelevant = [];
160
+ // --no-scenarios makes commander default `scenarios` to true, so its
161
+ // presence alone does not mean the user passed it — only false does.
162
+ const isUserProvided = (k) => k === 'scenarios' ? options[k] === false : k in options;
160
163
  if (type === 'change') {
161
164
  for (const k of SPEC_FLAG_KEYS)
162
- if (k in options)
165
+ if (isUserProvided(k))
163
166
  irrelevant.push(k);
164
167
  }
165
168
  else {
166
169
  for (const k of CHANGE_FLAG_KEYS)
167
- if (k in options)
170
+ if (isUserProvided(k))
168
171
  irrelevant.push(k);
169
172
  }
170
173
  if (irrelevant.length > 0) {
@@ -1,9 +1,10 @@
1
- import { existsSync, readdirSync, readFileSync } from 'fs';
1
+ import { existsSync, readFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { MarkdownParser } from '../core/parsers/markdown-parser.js';
4
4
  import { Validator } from '../core/validation/validator.js';
5
5
  import { isInteractive } from '../utils/interactive.js';
6
6
  import { getSpecIds } from '../utils/item-discovery.js';
7
+ import { discoverSpecFiles } from '../utils/spec-discovery.js';
7
8
  const SPECS_DIR = 'openspec/specs';
8
9
  function parseSpecFromFile(specPath, specId) {
9
10
  const content = readFileSync(specPath, 'utf-8');
@@ -127,36 +128,31 @@ export function registerSpecCommand(rootProgram) {
127
128
  .description('List all available specifications')
128
129
  .option('--json', 'Output as JSON')
129
130
  .option('--long', 'Show id and title with counts')
130
- .action((options) => {
131
+ .action(async (options) => {
131
132
  try {
132
133
  if (!existsSync(SPECS_DIR)) {
133
134
  console.log('No items found');
134
135
  return;
135
136
  }
136
- const specs = readdirSync(SPECS_DIR, { withFileTypes: true })
137
- .filter(dirent => dirent.isDirectory())
138
- .map(dirent => {
139
- const specPath = join(SPECS_DIR, dirent.name, 'spec.md');
140
- if (existsSync(specPath)) {
141
- try {
142
- const spec = parseSpecFromFile(specPath, dirent.name);
143
- return {
144
- id: dirent.name,
145
- title: spec.name,
146
- requirementCount: spec.requirements.length
147
- };
148
- }
149
- catch {
150
- return {
151
- id: dirent.name,
152
- title: dirent.name,
153
- requirementCount: 0
154
- };
155
- }
137
+ const discovered = await discoverSpecFiles(SPECS_DIR);
138
+ const specs = discovered
139
+ .map(({ id, specFile }) => {
140
+ try {
141
+ const spec = parseSpecFromFile(specFile, id);
142
+ return {
143
+ id,
144
+ title: spec.name,
145
+ requirementCount: spec.requirements.length
146
+ };
147
+ }
148
+ catch {
149
+ return {
150
+ id,
151
+ title: id,
152
+ requirementCount: 0
153
+ };
156
154
  }
157
- return null;
158
155
  })
159
- .filter((spec) => spec !== null)
160
156
  .sort((a, b) => a.id.localeCompare(b.id));
161
157
  if (options.json) {
162
158
  console.log(JSON.stringify(specs, null, 2));
@@ -1,6 +1,7 @@
1
1
  import ora from 'ora';
2
2
  import path from 'path';
3
3
  import { Validator } from '../core/validation/validator.js';
4
+ import { VALIDATION_MESSAGES } from '../core/validation/constants.js';
4
5
  import { resolveRootForCommand, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from '../core/root-selection.js';
5
6
  import { isInteractive, resolveNoInteractive } from '../utils/interactive.js';
6
7
  import { getSpecIds } from '../utils/item-discovery.js';
@@ -174,12 +175,26 @@ export class ValidateCommand {
174
175
  const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
175
176
  console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
176
177
  }
177
- this.printNextSteps(type, id, root);
178
+ this.printNextSteps(type, id, root, report.issues);
178
179
  }
179
180
  }
180
- printNextSteps(type, id, root) {
181
+ printNextSteps(type, id, root, issues = []) {
181
182
  const bullets = [];
182
- if (type === 'change') {
183
+ // The delta-authoring bullets contradict a marker-related error ("add
184
+ // deltas" vs "remove skip_specs or the files"), so branch on the exact
185
+ // marker messages - the generic no-deltas guidance also mentions
186
+ // skip_specs, which must not trigger this.
187
+ const conflictIssue = issues.some(i => i.message.includes(VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_CONFLICT));
188
+ const invalidMarkerIssue = issues.some(i => i.message.includes(VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_INVALID_METADATA));
189
+ if (type === 'change' && conflictIssue) {
190
+ bullets.push('- This change declares skip_specs (no spec deltas): delete the files under specs/, or remove skip_specs from .openspec.yaml if requirements do change');
191
+ bullets.push('- skip_specs is only honored when .openspec.yaml is valid change metadata (schema: <name> naming a known schema is required)');
192
+ }
193
+ else if (type === 'change' && invalidMarkerIssue) {
194
+ bullets.push('- Fix .openspec.yaml so the skip_specs marker can be honored (schema: <name> naming a known schema is required)');
195
+ bullets.push('- Or remove skip_specs from .openspec.yaml and add delta specs instead');
196
+ }
197
+ else if (type === 'change') {
183
198
  bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
184
199
  bullets.push('- Each requirement MUST include at least one #### Scenario: block');
185
200
  bullets.push(`- Debug parsed deltas: ${withStoreFlag(root, `openspec show ${id} --json --deltas-only`)}`);
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export { statusCommand } from './status.js';
7
7
  export type { StatusOptions } from './status.js';
8
- export { instructionsCommand, applyInstructionsCommand } from './instructions.js';
8
+ export { instructionsCommand, applyInstructionsCommand, archiveInstructionsCommand, } from './instructions.js';
9
9
  export type { InstructionsOptions } from './instructions.js';
10
10
  export { templatesCommand } from './templates.js';
11
11
  export type { TemplatesOptions } from './templates.js';
@@ -4,7 +4,7 @@
4
4
  * Commands for the artifact-driven workflow: status, instructions, templates, schemas, new change.
5
5
  */
6
6
  export { statusCommand } from './status.js';
7
- export { instructionsCommand, applyInstructionsCommand } from './instructions.js';
7
+ export { instructionsCommand, applyInstructionsCommand, archiveInstructionsCommand, } from './instructions.js';
8
8
  export { templatesCommand } from './templates.js';
9
9
  export { schemasCommand } from './schemas.js';
10
10
  export { newChangeCommand } from './new-change.js';
@@ -7,7 +7,8 @@
7
7
  import { type ArtifactInstructions } from '../../core/artifact-graph/index.js';
8
8
  import { type PlanningHome } from '../../core/planning-home.js';
9
9
  import { type ReferenceIndexEntry } from '../../core/references.js';
10
- import { type ApplyInstructions } from './shared.js';
10
+ import { type ProjectConfig } from '../../core/project-config.js';
11
+ import { type ApplyInstructions, type ArchiveInstructions } from './shared.js';
11
12
  export interface InstructionsOptions {
12
13
  change?: string;
13
14
  schema?: string;
@@ -22,11 +23,13 @@ export interface ApplyInstructionsOptions {
22
23
  storePath?: string;
23
24
  json?: boolean;
24
25
  }
26
+ export type ArchiveInstructionsOptions = ApplyInstructionsOptions;
25
27
  export declare function instructionsCommand(artifactId: string | undefined, options: InstructionsOptions): Promise<void>;
26
28
  export declare function printInstructionsText(instructions: ArtifactInstructions, isBlocked: boolean): void;
27
29
  export interface GenerateApplyInstructionsOptions {
28
30
  planningHome?: PlanningHome;
29
31
  references?: ReferenceIndexEntry[];
32
+ projectConfig?: ProjectConfig | null;
30
33
  }
31
34
  /**
32
35
  * Generates apply instructions for implementing tasks from a change.
@@ -36,4 +39,7 @@ export interface GenerateApplyInstructionsOptions {
36
39
  export declare function generateApplyInstructions(projectRoot: string, changeName: string, schemaName?: string, options?: GenerateApplyInstructionsOptions): Promise<ApplyInstructions>;
37
40
  export declare function applyInstructionsCommand(options: ApplyInstructionsOptions): Promise<void>;
38
41
  export declare function printApplyInstructionsText(instructions: ApplyInstructions): void;
42
+ export declare function generateArchiveInstructions(changeName: string, projectConfig: ProjectConfig | null): ArchiveInstructions;
43
+ export declare function archiveInstructionsCommand(options: ArchiveInstructionsOptions): Promise<void>;
44
+ export declare function printArchiveInstructionsText(instructions: ArchiveInstructions): void;
39
45
  //# sourceMappingURL=instructions.d.ts.map
@@ -12,7 +12,7 @@ import { getChangeDir, resolveCurrentPlanningHomeSync, } from '../../core/planni
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
17
  // -----------------------------------------------------------------------------
18
18
  // Artifact Instructions Command
@@ -55,10 +55,12 @@ export async function instructionsCommand(artifactId, options) {
55
55
  if (options.schema) {
56
56
  validateSchemaExists(options.schema, projectRoot);
57
57
  }
58
+ const { projectConfig, references } = await loadRootConfigContext(root);
58
59
  // loadChangeContext will auto-detect schema from metadata if not provided
59
60
  const context = loadChangeContext(projectRoot, changeName, options.schema, {
60
61
  changeDir: getChangeDir(planningHome, changeName),
61
62
  planningHome,
63
+ projectConfig,
62
64
  });
63
65
  if (!artifactId) {
64
66
  spinner?.stop();
@@ -71,7 +73,6 @@ export async function instructionsCommand(artifactId, options) {
71
73
  const validIds = context.graph.getAllArtifacts().map((a) => a.id);
72
74
  throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'. Valid artifacts:\n ${validIds.join('\n ')}`);
73
75
  }
74
- const { projectConfig, references } = await loadRootConfigContext(root);
75
76
  const instructions = generateInstructions(context, artifactId, projectRoot, {
76
77
  projectConfig,
77
78
  references,
@@ -94,6 +95,17 @@ export function printInstructionsText(instructions, isBlocked) {
94
95
  // Opening tag
95
96
  console.log(`<artifact id="${artifactId}" change="${changeName}" schema="${schemaName}">`);
96
97
  console.log();
98
+ // Artifacts skipped via skip_specs get no creation directive: emitting the
99
+ // task/template anyway would prompt an agent to write spec files that
100
+ // validate then rejects as conflicting with the marker.
101
+ if (instructions.skipped) {
102
+ console.log('<warning>');
103
+ console.log(instructions.warning ?? 'This artifact is skipped (skip_specs is set in .openspec.yaml).');
104
+ console.log('</warning>');
105
+ console.log();
106
+ console.log('</artifact>');
107
+ return;
108
+ }
97
109
  // Warning for blocked artifacts
98
110
  if (isBlocked) {
99
111
  const missing = dependencies.filter((d) => !d.done).map((d) => d.id);
@@ -135,9 +147,18 @@ export function printInstructionsText(instructions, isBlocked) {
135
147
  // Dependencies (files to read for context)
136
148
  if (dependencies.length > 0) {
137
149
  console.log('<dependencies>');
138
- console.log('Read these files for context before creating this artifact:');
150
+ 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
151
  console.log();
140
152
  for (const dep of dependencies) {
153
+ // A dependency satisfied via skip_specs has no files by design: telling
154
+ // the agent to read them (or calling them "done") would send it hunting
155
+ // for spec files that must not exist.
156
+ if (dep.skipped) {
157
+ console.log(`<dependency id="${dep.id}" status="skipped">`);
158
+ console.log(` <description>Skipped: the change declares skip_specs, so this artifact has no files to read.</description>`);
159
+ console.log('</dependency>');
160
+ continue;
161
+ }
141
162
  const status = dep.done ? 'done' : 'missing';
142
163
  const fullPath = path.join(changeDir, dep.path);
143
164
  console.log(`<dependency id="${dep.id}" status="${status}">`);
@@ -219,6 +240,7 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
219
240
  const context = loadChangeContext(projectRoot, changeName, schemaName, {
220
241
  changeDir: getChangeDir(planningHome, changeName),
221
242
  planningHome,
243
+ projectConfig: options.projectConfig,
222
244
  });
223
245
  const changeDir = context.changeDir;
224
246
  // Get the full schema to access the apply phase configuration
@@ -229,9 +251,15 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
229
251
  const requiredArtifactIds = applyConfig?.requires ?? schema.artifacts.map((a) => a.id);
230
252
  const tracksFile = applyConfig?.tracks ?? null;
231
253
  const schemaInstruction = applyConfig?.instruction ?? null;
232
- // Check which required artifacts are missing
254
+ const operationInputs = loadOperationInputs(options.projectConfig ?? null, 'apply');
255
+ // Check which required artifacts are missing. Artifacts the change skips
256
+ // via skip_specs count as present - their files must not exist, and
257
+ // status already reports them complete, so apply cannot block on them.
233
258
  const missingArtifacts = [];
234
259
  for (const artifactId of requiredArtifactIds) {
260
+ if (context.skippedArtifacts?.has(artifactId)) {
261
+ continue;
262
+ }
235
263
  const artifact = schema.artifacts.find((a) => a.id === artifactId);
236
264
  if (artifact && resolveArtifactOutputs(changeDir, artifact.generates).length === 0) {
237
265
  missingArtifacts.push(artifactId);
@@ -303,6 +331,7 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
303
331
  missingArtifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
304
332
  instruction,
305
333
  ...(references !== undefined ? { references } : {}),
334
+ ...operationInputs,
306
335
  };
307
336
  }
308
337
  export async function applyInstructionsCommand(options) {
@@ -320,11 +349,13 @@ export async function applyInstructionsCommand(options) {
320
349
  if (options.schema) {
321
350
  validateSchemaExists(options.schema, projectRoot);
322
351
  }
323
- // generateApplyInstructions uses loadChangeContext which auto-detects schema
324
- const { references } = await loadRootConfigContext(root);
352
+ // One parsed config snapshot supplies schema fallback, references, context,
353
+ // and operation guidance for this command.
354
+ const { projectConfig, references } = await loadRootConfigContext(root);
325
355
  const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema, {
326
356
  planningHome,
327
357
  references,
358
+ projectConfig,
328
359
  });
329
360
  spinner?.stop();
330
361
  if (options.json) {
@@ -389,5 +420,57 @@ export function printApplyInstructionsText(instructions) {
389
420
  // Instruction
390
421
  console.log('### Instruction');
391
422
  console.log(instruction);
423
+ console.log();
424
+ printOperationInputsText(instructions);
425
+ }
426
+ export function generateArchiveInstructions(changeName, projectConfig) {
427
+ return {
428
+ changeName,
429
+ ...loadOperationInputs(projectConfig, 'archive'),
430
+ };
431
+ }
432
+ export async function archiveInstructionsCommand(options) {
433
+ const root = await resolveRootForCommand(options, { json: options.json });
434
+ if (!root) {
435
+ return;
436
+ }
437
+ const spinner = options.json ? undefined : ora('Loading archive inputs...').start();
438
+ try {
439
+ const changeName = await validateChangeExists(options.change, root.path, root.changesDir, { newChangeHint: withStoreFlag(root, 'openspec new change <name>') });
440
+ const projectConfig = readProjectConfig(root.path);
441
+ const instructions = generateArchiveInstructions(changeName, projectConfig);
442
+ spinner?.stop();
443
+ if (options.json) {
444
+ console.log(JSON.stringify({ ...instructions, root: toRootOutput(root) }, null, 2));
445
+ return;
446
+ }
447
+ printArchiveInstructionsText(instructions);
448
+ }
449
+ catch (error) {
450
+ spinner?.stop();
451
+ throw error;
452
+ }
453
+ }
454
+ export function printArchiveInstructionsText(instructions) {
455
+ console.log(`## Archive Inputs: ${instructions.changeName}`);
456
+ console.log();
457
+ printOperationInputsText(instructions);
458
+ }
459
+ function printOperationInputsText(inputs) {
460
+ if (inputs.context) {
461
+ console.log('### Project Context (required instruction input)');
462
+ console.log(inputs.context);
463
+ console.log();
464
+ }
465
+ if (inputs.operationGuidance && inputs.operationGuidance.length > 0) {
466
+ console.log('### Operation Guidance (advisory)');
467
+ for (const guidance of inputs.operationGuidance) {
468
+ console.log(`- ${guidance}`);
469
+ }
470
+ console.log();
471
+ }
472
+ if (!inputs.context && !inputs.operationGuidance) {
473
+ console.log('No project context or operation guidance configured.');
474
+ }
392
475
  }
393
476
  //# 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,18 +67,23 @@ 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
  }
@@ -17,6 +17,5 @@ export declare class ArchiveCommand {
17
17
  */
18
18
  private run;
19
19
  private selectChange;
20
- private getArchiveDate;
21
20
  }
22
21
  //# sourceMappingURL=archive.d.ts.map