@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
@@ -1,4 +1,10 @@
1
1
  import { z } from 'zod';
2
+ export declare const OPERATION_IDS: readonly ["apply", "archive"];
3
+ export type OperationId = (typeof OPERATION_IDS)[number];
4
+ export interface OperationConfig {
5
+ guidance?: string[];
6
+ }
7
+ export type OperationsConfig = Partial<Record<OperationId, OperationConfig>>;
2
8
  /**
3
9
  * Zod schema for project configuration.
4
10
  *
@@ -16,7 +22,18 @@ export declare const ProjectConfigSchema: z.ZodObject<{
16
22
  schema: z.ZodString;
17
23
  context: z.ZodOptional<z.ZodString>;
18
24
  rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
25
+ operations: z.ZodOptional<z.ZodObject<{
26
+ apply: z.ZodOptional<z.ZodObject<{
27
+ guidance: z.ZodOptional<z.ZodArray<z.ZodString>>;
28
+ }, z.core.$strip>>;
29
+ archive: z.ZodOptional<z.ZodObject<{
30
+ guidance: z.ZodOptional<z.ZodArray<z.ZodString>>;
31
+ }, z.core.$strip>>;
32
+ }, z.core.$strip>>;
19
33
  store: z.ZodOptional<z.ZodString>;
34
+ githubCopilot: z.ZodOptional<z.ZodObject<{
35
+ cloudAgent: z.ZodOptional<z.ZodBoolean>;
36
+ }, z.core.$strip>>;
20
37
  }, z.core.$strip>;
21
38
  /** Normalized in-memory shape of a referenced store declaration. */
22
39
  export interface DeclarationEntry {
@@ -27,6 +44,11 @@ export interface DeclarationEntry {
27
44
  export type ProjectConfig = z.infer<typeof ProjectConfigSchema> & {
28
45
  references?: DeclarationEntry[];
29
46
  };
47
+ export interface OperationInputs {
48
+ context?: string;
49
+ operationGuidance?: string[];
50
+ }
51
+ export declare function loadOperationInputs(projectConfig: ProjectConfig | null, operationId: OperationId): OperationInputs;
30
52
  export declare const MAX_CONTEXT_SIZE: number;
31
53
  /**
32
54
  * Read and parse openspec/config.yaml from project root.
@@ -49,16 +71,16 @@ export declare const MAX_CONTEXT_SIZE: number;
49
71
  */
50
72
  export declare function readProjectConfig(projectRoot: string): ProjectConfig | null;
51
73
  /**
52
- * Validate artifact IDs in rules against a schema's artifacts.
53
- * Called during instruction loading (when schema is known).
54
- * Returns warnings for unknown artifact IDs.
74
+ * Validate artifact IDs in rules against the artifacts of every available
75
+ * schema. The `rules:` map is global, but each change can use a different
76
+ * schema, so a key is only unknown when it matches no artifact in ANY schema.
77
+ * Returns warnings for keys that are unknown everywhere.
55
78
  *
56
79
  * @param rules - The rules object from config
57
- * @param validArtifactIds - Set of valid artifact IDs from the schema
58
- * @param schemaName - Name of the schema for error messages
80
+ * @param validArtifactIds - Set of valid artifact IDs across all schemas
59
81
  * @returns Array of warning messages for unknown artifact IDs
60
82
  */
61
- export declare function validateConfigRules(rules: Record<string, string[]>, validArtifactIds: Set<string>, schemaName: string): string[];
83
+ export declare function validateConfigRules(rules: Record<string, string[]>, validArtifactIds: Set<string>): string[];
62
84
  /**
63
85
  * Suggest valid schema names when user provides invalid schema.
64
86
  * Uses fuzzy matching to find similar names.
@@ -2,6 +2,10 @@ import { existsSync, readFileSync, statSync } from 'fs';
2
2
  import path from 'path';
3
3
  import { parse as parseYaml } from 'yaml';
4
4
  import { z } from 'zod';
5
+ export const OPERATION_IDS = ['apply', 'archive'];
6
+ const OperationConfigSchema = z.object({
7
+ guidance: z.array(z.string()).optional(),
8
+ });
5
9
  /**
6
10
  * Zod schema for project configuration.
7
11
  *
@@ -34,6 +38,14 @@ export const ProjectConfigSchema = z.object({
34
38
  )
35
39
  .optional()
36
40
  .describe('Per-artifact rules, keyed by artifact ID'),
41
+ // Optional: per-operation advisory guidance, kept separate from artifact rules.
42
+ operations: z
43
+ .object({
44
+ apply: OperationConfigSchema.optional(),
45
+ archive: OperationConfigSchema.optional(),
46
+ })
47
+ .optional()
48
+ .describe('Per-operation advisory guidance'),
37
49
  // Note: the `references` field (id strings or {id, remote} maps) is
38
50
  // deliberately absent here — readProjectConfig parses and normalizes
39
51
  // it by hand (see DeclarationEntry below); a schema entry nothing
@@ -45,7 +57,70 @@ export const ProjectConfigSchema = z.object({
45
57
  .string()
46
58
  .optional()
47
59
  .describe('Store id used as the OpenSpec root when no local planning shape exists'),
60
+ // Optional: GitHub Copilot integration preferences. `cloudAgent` is the
61
+ // opt-in for generating the Copilot cloud coding-agent files (a GitHub
62
+ // Actions workflow + agent file); absent means "not yet decided".
63
+ githubCopilot: z
64
+ .object({
65
+ cloudAgent: z.boolean().optional(),
66
+ })
67
+ .optional()
68
+ .describe('GitHub Copilot integration preferences'),
48
69
  });
70
+ export function loadOperationInputs(projectConfig, operationId) {
71
+ const context = projectConfig?.context !== undefined && projectConfig.context.trim().length > 0
72
+ ? projectConfig.context
73
+ : undefined;
74
+ const guidance = projectConfig?.operations?.[operationId]?.guidance;
75
+ const operationGuidance = guidance && guidance.length > 0 ? guidance : undefined;
76
+ return {
77
+ ...(context !== undefined ? { context } : {}),
78
+ ...(operationGuidance !== undefined ? { operationGuidance } : {}),
79
+ };
80
+ }
81
+ function parseOperations(raw) {
82
+ if (raw === undefined) {
83
+ return undefined;
84
+ }
85
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
86
+ console.warn(`Invalid 'operations' field in config (must be object)`);
87
+ return undefined;
88
+ }
89
+ const supported = new Set(OPERATION_IDS);
90
+ const operations = {};
91
+ for (const [operationId, value] of Object.entries(raw)) {
92
+ if (!supported.has(operationId)) {
93
+ console.warn(`Unknown operation ID '${operationId}' in config. Supported operation IDs: ${OPERATION_IDS.join(', ')}`);
94
+ continue;
95
+ }
96
+ const typedOperationId = operationId;
97
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
98
+ console.warn(`Invalid 'operations.${operationId}' field in config (must be object), ignoring this operation`);
99
+ continue;
100
+ }
101
+ const operation = value;
102
+ const unknownFields = Object.keys(operation).filter((field) => field !== 'guidance');
103
+ if (unknownFields.length > 0) {
104
+ console.warn(`Unknown field(s) in 'operations.${operationId}': ${unknownFields.join(', ')}. Supported fields: guidance`);
105
+ }
106
+ if (operation.guidance === undefined) {
107
+ continue;
108
+ }
109
+ const guidanceResult = z.array(z.string()).safeParse(operation.guidance);
110
+ if (!guidanceResult.success) {
111
+ console.warn(`Guidance for operation '${operationId}' must be an array of strings, ignoring this operation's guidance`);
112
+ continue;
113
+ }
114
+ const guidance = guidanceResult.data.filter((entry) => entry.length > 0);
115
+ if (guidance.length < guidanceResult.data.length) {
116
+ console.warn(`Some guidance for operation '${operationId}' are empty strings, ignoring them`);
117
+ }
118
+ if (guidance.length > 0) {
119
+ operations[typedOperationId] = { guidance };
120
+ }
121
+ }
122
+ return Object.keys(operations).length > 0 ? operations : undefined;
123
+ }
49
124
  /**
50
125
  * Parser for `references:` declarations: string entries or
51
126
  * {id, remote} maps, normalized to DeclarationEntry[]. Dedup keys on
@@ -169,7 +244,11 @@ export function readProjectConfig(projectRoot) {
169
244
  const rulesField = z.record(z.string(), z.array(z.string()));
170
245
  // First check if it's an object structure (guard against null since typeof null === 'object')
171
246
  if (typeof raw.rules === 'object' && raw.rules !== null && !Array.isArray(raw.rules)) {
172
- const parsedRules = {};
247
+ // Artifact ids are intentionally not restricted to the built-in naming
248
+ // convention, so keys such as "constructor" remain valid for custom
249
+ // schemas. A null-prototype map preserves those keys as data without
250
+ // letting "__proto__" mutate the lookup object's prototype.
251
+ const parsedRules = Object.create(null);
173
252
  let hasValidRules = false;
174
253
  for (const [artifactId, rules] of Object.entries(raw.rules)) {
175
254
  const rulesArrayResult = z.array(z.string()).safeParse(rules);
@@ -196,6 +275,10 @@ export function readProjectConfig(projectRoot) {
196
275
  console.warn(`Invalid 'rules' field in config (must be object)`);
197
276
  }
198
277
  }
278
+ const operations = parseOperations(raw.operations);
279
+ if (operations) {
280
+ config.operations = operations;
281
+ }
199
282
  const references = parseDeclarationList(raw.references);
200
283
  if (references) {
201
284
  config.references = references;
@@ -211,6 +294,23 @@ export function readProjectConfig(projectRoot) {
211
294
  console.warn(`Warning: ignoring invalid store: field in ${configPathForWarnings(projectRoot)} (must be a single store id string).`);
212
295
  }
213
296
  }
297
+ // Parse githubCopilot preferences (only cloudAgent is recognized today).
298
+ if (raw.githubCopilot !== undefined) {
299
+ if (typeof raw.githubCopilot === 'object' &&
300
+ raw.githubCopilot !== null &&
301
+ !Array.isArray(raw.githubCopilot)) {
302
+ const cloudAgent = raw.githubCopilot.cloudAgent;
303
+ if (typeof cloudAgent === 'boolean') {
304
+ config.githubCopilot = { cloudAgent };
305
+ }
306
+ else if (cloudAgent !== undefined) {
307
+ console.warn(`Invalid 'githubCopilot.cloudAgent' field in config (must be a boolean)`);
308
+ }
309
+ }
310
+ else {
311
+ console.warn(`Invalid 'githubCopilot' field in config (must be an object)`);
312
+ }
313
+ }
214
314
  // Return partial config even if some fields failed
215
315
  return Object.keys(config).length > 0 ? config : null;
216
316
  }
@@ -223,22 +323,22 @@ function configPathForWarnings(projectRoot) {
223
323
  return resolveConfigFilePath(projectRoot) ?? path.join(projectRoot, 'openspec', 'config.yaml');
224
324
  }
225
325
  /**
226
- * Validate artifact IDs in rules against a schema's artifacts.
227
- * Called during instruction loading (when schema is known).
228
- * Returns warnings for unknown artifact IDs.
326
+ * Validate artifact IDs in rules against the artifacts of every available
327
+ * schema. The `rules:` map is global, but each change can use a different
328
+ * schema, so a key is only unknown when it matches no artifact in ANY schema.
329
+ * Returns warnings for keys that are unknown everywhere.
229
330
  *
230
331
  * @param rules - The rules object from config
231
- * @param validArtifactIds - Set of valid artifact IDs from the schema
232
- * @param schemaName - Name of the schema for error messages
332
+ * @param validArtifactIds - Set of valid artifact IDs across all schemas
233
333
  * @returns Array of warning messages for unknown artifact IDs
234
334
  */
235
- export function validateConfigRules(rules, validArtifactIds, schemaName) {
335
+ export function validateConfigRules(rules, validArtifactIds) {
236
336
  const warnings = [];
237
337
  for (const artifactId of Object.keys(rules)) {
238
338
  if (!validArtifactIds.has(artifactId)) {
239
339
  const validIds = Array.from(validArtifactIds).sort().join(', ');
240
340
  warnings.push(`Unknown artifact ID in rules: "${artifactId}". ` +
241
- `Valid IDs for schema "${schemaName}": ${validIds}`);
341
+ `It matches no artifact in any available schema. Known artifact IDs: ${validIds}`);
242
342
  }
243
343
  }
244
344
  return warnings;
@@ -53,6 +53,46 @@ function registerFix(id, remote) {
53
53
  }
54
54
  return `Get a checkout from a teammate and run: openspec store register <path> --id ${id}`;
55
55
  }
56
+ const WHITESPACE = /\s/;
57
+ /**
58
+ * Drop a CommonMark closing sequence (`## Purpose ##`). The closing run only
59
+ * counts when whitespace precedes it, so `Purpose###` keeps its hashes.
60
+ * Scans from the end so the cost stays linear in the title length.
61
+ */
62
+ function stripClosingSequence(title) {
63
+ let end = title.length;
64
+ while (end > 0 && WHITESPACE.test(title[end - 1])) {
65
+ end--;
66
+ }
67
+ const hashEnd = end;
68
+ while (end > 0 && title[end - 1] === '#') {
69
+ end--;
70
+ }
71
+ const noClosingRun = end === hashEnd;
72
+ const missingLeadingSpace = end === 0 || !WHITESPACE.test(title[end - 1]);
73
+ if (noClosingRun || missingLeadingSpace) {
74
+ return title.trim();
75
+ }
76
+ return title.slice(0, end).trim();
77
+ }
78
+ /**
79
+ * Heading title, or null when the line is not an ATX heading. Hand-rolled
80
+ * rather than a regex so a title padded with whitespace cannot backtrack.
81
+ */
82
+ function parseHeadingTitle(line) {
83
+ let level = 0;
84
+ while (level < 6 && level < line.length && line[level] === '#') {
85
+ level++;
86
+ }
87
+ if (level === 0 || level >= line.length || !WHITESPACE.test(line[level])) {
88
+ return null;
89
+ }
90
+ let start = level;
91
+ while (start < line.length && WHITESPACE.test(line[start])) {
92
+ start++;
93
+ }
94
+ return stripClosingSequence(line.slice(start));
95
+ }
56
96
  /**
57
97
  * Tolerant first-Purpose-line extraction. parseSpec() throws on specs
58
98
  * without Purpose/Requirements sections; the index must never fail on an
@@ -79,12 +119,11 @@ export function extractFirstPurposeLine(markdown) {
79
119
  if (fenceMarker !== null) {
80
120
  continue;
81
121
  }
82
- const heading = line.match(/^(#{1,6})\s+(.*)$/);
83
- if (heading) {
122
+ const title = parseHeadingTitle(line);
123
+ if (title !== null) {
84
124
  if (inPurpose) {
85
125
  return '';
86
126
  }
87
- const title = heading[2].replace(/\s+#+\s*$/, '').trim();
88
127
  inPurpose = title.toLowerCase() === 'purpose';
89
128
  continue;
90
129
  }
@@ -152,7 +191,6 @@ export function renderReferencedStoresSection(entries) {
152
191
  * let hostile content forge instruction lines (slice 6.1 hardening).
153
192
  */
154
193
  export function sanitizeInline(value, maxLength = 300) {
155
- // eslint-disable-next-line no-control-regex
156
194
  const flattened = value.replace(/[\u0000-\u001f\u007f]+/g, ' ').trim();
157
195
  return flattened.length > maxLength ? `${flattened.slice(0, maxLength)}…` : flattened;
158
196
  }
@@ -26,6 +26,10 @@ export interface RelationshipHealth {
26
26
  remote?: string;
27
27
  };
28
28
  origin_url?: string;
29
+ drift?: {
30
+ ahead: number;
31
+ behind: number;
32
+ };
29
33
  status: StoreDiagnostic[];
30
34
  } | null;
31
35
  references: ReferenceIndexEntry[];
@@ -42,6 +46,10 @@ export interface InspectRelationshipsInput {
42
46
  metadataValid: boolean;
43
47
  canonicalRemote?: string;
44
48
  originUrl?: string;
49
+ drift?: {
50
+ ahead: number;
51
+ behind: number;
52
+ };
45
53
  };
46
54
  referenceEntries: ReferenceIndexEntry[];
47
55
  registryUnreadable: boolean;
@@ -37,6 +37,18 @@ export function inspectRelationships(input) {
37
37
  input.storeFacts.canonicalRemote !== input.storeFacts.originUrl) {
38
38
  storeStatus.push(makeStoreDiagnostic('info', 'store_remote_divergence', `The store.yaml remote (${sanitizeInline(input.storeFacts.canonicalRemote, 200)}) differs from the checkout's origin (${sanitizeInline(input.storeFacts.originUrl, 200)}).`, { target: 'store.metadata' }));
39
39
  }
40
+ // Checkout behind its upstream tracking ref: a read-only staleness
41
+ // signal, not a version pin — OpenSpec never syncs stores, so this
42
+ // compares against the local upstream ref, not the live remote.
43
+ // Behind means teammates on newer commits may resolve different specs.
44
+ // Ahead-only is normal (OpenSpec never pushes stores), so it stays quiet.
45
+ const drift = input.storeFacts.drift;
46
+ if (drift && drift.behind > 0) {
47
+ const behindCommits = `${drift.behind} commit${drift.behind === 1 ? '' : 's'}`;
48
+ storeStatus.push(makeStoreDiagnostic('info', 'store_checkout_drift', drift.ahead > 0
49
+ ? `This store checkout has diverged from its upstream tracking branch (${drift.behind} behind, ${drift.ahead} ahead); teammates on newer commits may resolve different specs.`
50
+ : `This store checkout is ${behindCommits} behind its upstream tracking branch; teammates on newer commits may resolve different specs.`, { target: 'store.git' }));
51
+ }
40
52
  store = {
41
53
  id: input.storeFacts.id,
42
54
  metadata: {
@@ -47,6 +59,7 @@ export function inspectRelationships(input) {
47
59
  : {}),
48
60
  },
49
61
  ...(input.storeFacts.originUrl ? { origin_url: input.storeFacts.originUrl } : {}),
62
+ ...(drift ? { drift } : {}),
50
63
  status: storeStatus,
51
64
  };
52
65
  }
@@ -7,8 +7,11 @@
7
7
  * - `--store <id>` selects a registered store's root.
8
8
  * - Without `--store`, the nearest ancestor containing `openspec/` wins.
9
9
  * Leftover workspace view state is never considered a root here.
10
- * - With no nearest root, registered stores produce a selection hint error;
11
- * otherwise commands may treat the current directory as an implicit root.
10
+ * - With no nearest root, a global `defaultStore` (if set) is the last
11
+ * machine-level fallback before the selection hint error.
12
+ * - With no nearest root and no default, registered stores produce a
13
+ * selection hint error; otherwise commands may treat the current
14
+ * directory as an implicit root.
12
15
  *
13
16
  * Diagnostic codes reuse the store taxonomy where an error passes
14
17
  * through unchanged (`invalid_store_id`, metadata parse failures);
@@ -19,7 +22,7 @@
19
22
  * `no_openspec_root`).
20
23
  */
21
24
  import { type PlanningHome } from './planning-home.js';
22
- export type OpenSpecRootSource = 'store' | 'declared' | 'nearest' | 'implicit';
25
+ export type OpenSpecRootSource = 'store' | 'declared' | 'global_default' | 'nearest' | 'implicit';
23
26
  export interface StoreSelectorOptions {
24
27
  store?: string;
25
28
  storePath?: string;
@@ -84,9 +87,9 @@ export interface RootOutput {
84
87
  }
85
88
  export declare function toRootOutput(root: ResolvedOpenSpecRoot): RootOutput;
86
89
  /**
87
- * A store-selected root — explicit `--store` or the declared fallback.
88
- * Cross-root behavior (absolute paths, --store hints, suppressed
89
- * noun-form suggestions) keys on this, never on `source` directly.
90
+ * A store-selected root — explicit `--store`, a declared pointer, or the
91
+ * global default. Cross-root behavior (absolute paths, --store hints,
92
+ * suppressed noun-form suggestions) keys on this, never on `source` directly.
90
93
  */
91
94
  export declare function isStoreSelectedRoot(root: ResolvedOpenSpecRoot): root is ResolvedOpenSpecRoot & {
92
95
  storeId: string;
@@ -7,8 +7,11 @@
7
7
  * - `--store <id>` selects a registered store's root.
8
8
  * - Without `--store`, the nearest ancestor containing `openspec/` wins.
9
9
  * Leftover workspace view state is never considered a root here.
10
- * - With no nearest root, registered stores produce a selection hint error;
11
- * otherwise commands may treat the current directory as an implicit root.
10
+ * - With no nearest root, a global `defaultStore` (if set) is the last
11
+ * machine-level fallback before the selection hint error.
12
+ * - With no nearest root and no default, registered stores produce a
13
+ * selection hint error; otherwise commands may treat the current
14
+ * directory as an implicit root.
12
15
  *
13
16
  * Diagnostic codes reuse the store taxonomy where an error passes
14
17
  * through unchanged (`invalid_store_id`, metadata parse failures);
@@ -26,6 +29,7 @@ import { getStoreRootForBackend } from './store/registry.js';
26
29
  import { inspectOpenSpecRoot } from './openspec-root.js';
27
30
  import { findRepoPlanningRootSync } from './planning-home.js';
28
31
  import { classifyOpenSpecDir, storePointerProblem } from './project-config.js';
32
+ import { getGlobalConfig } from './global-config.js';
29
33
  import { FileSystemUtils } from '../utils/file-system.js';
30
34
  export class RootSelectionError extends Error {
31
35
  diagnostic;
@@ -221,6 +225,32 @@ async function resolveNearestOrDeclaredRoot(nearestRoot, globalDataDir) {
221
225
  throw error;
222
226
  }
223
227
  }
228
+ /**
229
+ * The machine-level fallback: the global `defaultStore` resolved as a root,
230
+ * with its own provenance (`global_default`) so JSON surfaces can tell a
231
+ * machine-wide default from a repo's `store:` pointer. Mirrors the
232
+ * declared-pointer catch — a stale or unregistered id degrades to the
233
+ * underlying error, reshaped to point at clearing the global default
234
+ * rather than passing --store.
235
+ */
236
+ async function resolveDefaultStoreRoot(id, globalDataDir) {
237
+ try {
238
+ return await resolveStoreRoot(id, globalDataDir, 'global_default');
239
+ }
240
+ catch (error) {
241
+ if (error instanceof RootSelectionError) {
242
+ const staleFix = error.diagnostic.code === 'unknown_store' ||
243
+ error.diagnostic.code === 'no_registered_stores'
244
+ ? `Register the store (openspec store register <path> --id ${id}) or clear the stale global default (openspec config unset defaultStore).`
245
+ : error.diagnostic.fix;
246
+ throw new RootSelectionError(`Global defaultStore '${id}': ${error.message}`, error.diagnostic.code, {
247
+ ...(error.diagnostic.target ? { target: error.diagnostic.target } : {}),
248
+ ...(staleFix ? { fix: staleFix } : {}),
249
+ });
250
+ }
251
+ throw error;
252
+ }
253
+ }
224
254
  export async function resolveOpenSpecRoot(options = {}) {
225
255
  if (options.storePath !== undefined) {
226
256
  throw new RootSelectionError('--store-path is not supported. Register the path with openspec store register <path>, then select it with --store <id>.', 'store_path_not_supported', {
@@ -236,6 +266,13 @@ export async function resolveOpenSpecRoot(options = {}) {
236
266
  if (nearestRoot) {
237
267
  return resolveNearestOrDeclaredRoot(nearestRoot, options.globalDataDir);
238
268
  }
269
+ // Machine-level fallback: a global defaultStore is consulted only after
270
+ // --store, the nearest local root, and project-level pointers have all
271
+ // failed to resolve — it changes the failure path, never the precedence.
272
+ const defaultStore = getGlobalConfig().defaultStore;
273
+ if (defaultStore) {
274
+ return resolveDefaultStoreRoot(defaultStore, options.globalDataDir);
275
+ }
239
276
  let registry;
240
277
  try {
241
278
  registry = await readStoreRegistryState(options.globalDataDir ? { globalDataDir: options.globalDataDir } : {});
@@ -265,9 +302,9 @@ export function toRootOutput(root) {
265
302
  };
266
303
  }
267
304
  /**
268
- * A store-selected root — explicit `--store` or the declared fallback.
269
- * Cross-root behavior (absolute paths, --store hints, suppressed
270
- * noun-form suggestions) keys on this, never on `source` directly.
305
+ * A store-selected root — explicit `--store`, a declared pointer, or the
306
+ * global default. Cross-root behavior (absolute paths, --store hints,
307
+ * suppressed noun-form suggestions) keys on this, never on `source` directly.
271
308
  */
272
309
  export function isStoreSelectedRoot(root) {
273
310
  return root.storeId !== undefined;
@@ -5,4 +5,5 @@
5
5
  */
6
6
  export { SKILL_NAMES, type SkillName, COMMAND_IDS, type CommandId, type ToolSkillStatus, type ToolVersionStatus, getToolsWithSkillsDir, getToolSkillStatus, getToolStates, extractGeneratedByVersion, getToolVersionStatus, getConfiguredTools, getAllToolVersionStatus, } from './tool-detection.js';
7
7
  export { type SkillTemplateEntry, type CommandTemplateEntry, getSkillTemplates, getCommandTemplates, getCommandContents, generateSkillContent, } from './skill-generation.js';
8
+ export { type SkillCapableTool, toolSupportsSkills, getSkillCapableTools, hasGlobalSkillTarget, resolveToolSkillsDir, } from './skill-paths.js';
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -5,4 +5,5 @@
5
5
  */
6
6
  export { SKILL_NAMES, COMMAND_IDS, getToolsWithSkillsDir, getToolSkillStatus, getToolStates, extractGeneratedByVersion, getToolVersionStatus, getConfiguredTools, getAllToolVersionStatus, } from './tool-detection.js';
7
7
  export { getSkillTemplates, getCommandTemplates, getCommandContents, generateSkillContent, } from './skill-generation.js';
8
+ export { toolSupportsSkills, getSkillCapableTools, hasGlobalSkillTarget, resolveToolSkillsDir, } from './skill-paths.js';
8
9
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Returns whether a legacy Codex skill differs from the current canonical
3
+ * replacement only by generated version, checkout line endings/BOM, or the
4
+ * known Codex/generic dual-reference expansion.
5
+ */
6
+ export declare function isLegacyCodexSkillEquivalentToCurrent(legacyContent: string, currentContent: string): boolean;
7
+ //# sourceMappingURL=skill-content-equivalence.d.ts.map
@@ -0,0 +1,41 @@
1
+ import { OPENSPEC_SKILL_NAMES } from '../config.js';
2
+ const GENERATED_VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
3
+ const OPENSPEC_SKILL_NAME_SET = new Set(OPENSPEC_SKILL_NAMES);
4
+ /**
5
+ * Normalizes checkout line endings and a valid generated version inside the
6
+ * YAML frontmatter. Free-form `generatedBy` text in the instructions remains
7
+ * material.
8
+ */
9
+ function normalizeGeneratedSkill(content) {
10
+ const normalized = content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
11
+ const frontmatter = normalized.match(/^---\n[\s\S]*?\n---(?:\n|$)/)?.[0];
12
+ if (!frontmatter)
13
+ return normalized;
14
+ const versionLine = /^(\s*generatedBy:\s*)(?:"([^"\n]+)"|'([^'\n]+)'|([^\s"'#]+))\s*$/m;
15
+ const normalizedFrontmatter = frontmatter.replace(versionLine, (line, prefix, doubleQuoted, singleQuoted, bare) => {
16
+ const version = doubleQuoted ?? singleQuoted ?? bare;
17
+ return version && GENERATED_VERSION.test(version)
18
+ ? `${prefix}"<generated-version>"`
19
+ : line;
20
+ });
21
+ return normalizedFrontmatter + normalized.slice(frontmatter.length);
22
+ }
23
+ /**
24
+ * Converts only known generated dual references in current Codex content back
25
+ * to the direct syntax used by legacy `.codex` output.
26
+ */
27
+ function toLegacyCodexReferences(content) {
28
+ return content.replace(/\$(openspec-[a-z0-9-]+) \(Codex\) or \/\1 \(other agents\)/g, (match, skillName) => OPENSPEC_SKILL_NAME_SET.has(skillName) ? `$${skillName}` : match);
29
+ }
30
+ /**
31
+ * Returns whether a legacy Codex skill differs from the current canonical
32
+ * replacement only by generated version, checkout line endings/BOM, or the
33
+ * known Codex/generic dual-reference expansion.
34
+ */
35
+ export function isLegacyCodexSkillEquivalentToCurrent(legacyContent, currentContent) {
36
+ const normalizedLegacy = normalizeGeneratedSkill(legacyContent);
37
+ const normalizedCurrent = normalizeGeneratedSkill(currentContent);
38
+ return (normalizedLegacy === normalizedCurrent ||
39
+ normalizedLegacy === toLegacyCodexReferences(normalizedCurrent));
40
+ }
41
+ //# sourceMappingURL=skill-content-equivalence.js.map
@@ -0,0 +1,13 @@
1
+ import { type AIToolOption } from '../config.js';
2
+ export type SkillCapableTool = AIToolOption & ({
3
+ skillsDir: string;
4
+ } | {
5
+ globalSkillsDir: string;
6
+ });
7
+ export declare function toolSupportsSkills(tool: AIToolOption): tool is SkillCapableTool;
8
+ export declare function getSkillCapableTools(): SkillCapableTool[];
9
+ export declare function hasGlobalSkillTarget(tool: AIToolOption): boolean;
10
+ export declare function resolveToolSkillsDir(projectRoot: string, tool: SkillCapableTool, options?: {
11
+ homeDir?: string;
12
+ }): string;
13
+ //# sourceMappingURL=skill-paths.d.ts.map
@@ -0,0 +1,23 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { AI_TOOLS } from '../config.js';
4
+ export function toolSupportsSkills(tool) {
5
+ return Boolean(tool.skillsDir || tool.globalSkillsDir);
6
+ }
7
+ export function getSkillCapableTools() {
8
+ return AI_TOOLS.filter(toolSupportsSkills);
9
+ }
10
+ export function hasGlobalSkillTarget(tool) {
11
+ return Boolean(tool.globalSkillsDir);
12
+ }
13
+ export function resolveToolSkillsDir(projectRoot, tool, options = {}) {
14
+ if (tool.globalSkillsDir) {
15
+ const homeDir = options.homeDir ?? process.env.USERPROFILE ?? process.env.HOME ?? os.homedir();
16
+ return path.join(homeDir, tool.globalSkillsDir, 'skills');
17
+ }
18
+ if (tool.skillsDir) {
19
+ return path.join(projectRoot, tool.skillsDir, 'skills');
20
+ }
21
+ throw new Error(`Tool '${tool.value}' does not support skill generation.`);
22
+ }
23
+ //# sourceMappingURL=skill-paths.js.map
@@ -32,9 +32,13 @@ export interface ToolVersionStatus {
32
32
  toolId: string;
33
33
  /** The tool's display name */
34
34
  toolName: string;
35
- /** Whether the tool has any skills configured */
35
+ /** Whether the tool has any skills or commands configured */
36
36
  configured: boolean;
37
- /** The generatedBy version found in the skill files, or null if not found */
37
+ /**
38
+ * The generatedBy version recorded in the tool's skill files. For a tool that
39
+ * has commands but no skills, the current version when the command files match
40
+ * what would be generated now. Null when neither says the files are current.
41
+ */
38
42
  generatedByVersion: string | null;
39
43
  /** Whether the tool needs updating (version mismatch or missing) */
40
44
  needsUpdate: boolean;
@@ -47,6 +51,19 @@ export declare function getToolsWithSkillsDir(): string[];
47
51
  * Checks which skill files exist for a tool.
48
52
  */
49
53
  export declare function getToolSkillStatus(projectRoot: string, toolId: string): ToolSkillStatus;
54
+ /**
55
+ * Checks whether a tool has at least one generated OpenSpec command file.
56
+ */
57
+ export declare function toolHasAnyConfiguredCommand(projectPath: string, toolId: string): boolean;
58
+ /**
59
+ * Checks whether command files for a tool on disk match current generated command contents.
60
+ *
61
+ * Command files carry no version stamp, so content equality is the only available
62
+ * "is this current?" signal for a commands-only install.
63
+ */
64
+ export declare function areCommandFilesUpToDate(projectRoot: string, toolId: string, options?: {
65
+ workflows?: readonly string[];
66
+ }): boolean;
50
67
  /**
51
68
  * Gets the skill status for all tools with skillsDir configured.
52
69
  */
@@ -57,11 +74,14 @@ export declare function getToolStates(projectRoot: string): Map<string, ToolSkil
57
74
  */
58
75
  export declare function extractGeneratedByVersion(skillFilePath: string): string | null;
59
76
  /**
60
- * Gets version status for a tool by reading the first available skill file.
77
+ * Gets version status for a tool by reading its skill files, falling back to a
78
+ * command-content fingerprint for installs that have commands but no skills.
61
79
  */
62
- export declare function getToolVersionStatus(projectRoot: string, toolId: string, currentVersion: string): ToolVersionStatus;
80
+ export declare function getToolVersionStatus(projectRoot: string, toolId: string, currentVersion: string, options?: {
81
+ workflows?: readonly string[];
82
+ }): ToolVersionStatus;
63
83
  /**
64
- * Gets all configured tools in the project.
84
+ * Gets all configured tools in the project (configured via skills or commands).
65
85
  */
66
86
  export declare function getConfiguredTools(projectRoot: string): string[];
67
87
  /**