@fission-ai/openspec 1.4.1 → 1.6.0-beta.1

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 (241) hide show
  1. package/README.md +37 -4
  2. package/dist/cli/index.d.ts +5 -0
  3. package/dist/cli/index.js +95 -95
  4. package/dist/commands/change.d.ts +5 -2
  5. package/dist/commands/change.js +23 -55
  6. package/dist/commands/config.js +6 -65
  7. package/dist/commands/context.d.ts +3 -0
  8. package/dist/commands/context.js +155 -0
  9. package/dist/commands/doctor.d.ts +8 -0
  10. package/dist/commands/doctor.js +163 -0
  11. package/dist/commands/shared-gather.d.ts +14 -0
  12. package/dist/commands/shared-gather.js +31 -0
  13. package/dist/commands/shared-output.d.ts +18 -0
  14. package/dist/commands/shared-output.js +61 -0
  15. package/dist/commands/show.d.ts +11 -6
  16. package/dist/commands/show.js +70 -25
  17. package/dist/commands/spec.d.ts +5 -1
  18. package/dist/commands/spec.js +15 -4
  19. package/dist/commands/store.d.ts +3 -0
  20. package/dist/commands/store.js +547 -0
  21. package/dist/commands/validate.d.ts +10 -0
  22. package/dist/commands/validate.js +88 -40
  23. package/dist/commands/workflow/index.d.ts +0 -2
  24. package/dist/commands/workflow/index.js +0 -1
  25. package/dist/commands/workflow/instructions.d.ts +11 -1
  26. package/dist/commands/workflow/instructions.js +71 -22
  27. package/dist/commands/workflow/new-change.d.ts +6 -3
  28. package/dist/commands/workflow/new-change.js +42 -77
  29. package/dist/commands/workflow/shared.d.ts +15 -3
  30. package/dist/commands/workflow/shared.js +20 -3
  31. package/dist/commands/workflow/status.d.ts +2 -0
  32. package/dist/commands/workflow/status.js +19 -17
  33. package/dist/commands/workset-input.d.ts +19 -0
  34. package/dist/commands/workset-input.js +112 -0
  35. package/dist/commands/workset-prompts.d.ts +12 -0
  36. package/dist/commands/workset-prompts.js +143 -0
  37. package/dist/commands/workset.d.ts +25 -0
  38. package/dist/commands/workset.js +446 -0
  39. package/dist/core/archive.d.ts +17 -6
  40. package/dist/core/archive.js +220 -68
  41. package/dist/core/artifact-graph/index.d.ts +1 -1
  42. package/dist/core/artifact-graph/instruction-loader.d.ts +18 -13
  43. package/dist/core/artifact-graph/instruction-loader.js +8 -31
  44. package/dist/core/change-metadata/schema.d.ts +1 -0
  45. package/dist/core/change-metadata/schema.js +4 -2
  46. package/dist/core/change-status-policy.d.ts +5 -18
  47. package/dist/core/change-status-policy.js +2 -37
  48. package/dist/core/command-generation/adapters/bob.js +1 -14
  49. package/dist/core/command-generation/adapters/claude.d.ts +1 -1
  50. package/dist/core/command-generation/adapters/claude.js +4 -15
  51. package/dist/core/command-generation/adapters/cursor.js +1 -14
  52. package/dist/core/command-generation/adapters/index.d.ts +2 -0
  53. package/dist/core/command-generation/adapters/index.js +2 -0
  54. package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
  55. package/dist/core/command-generation/adapters/oh-my-pi.js +44 -0
  56. package/dist/core/command-generation/adapters/pi.js +1 -14
  57. package/dist/core/command-generation/adapters/trae.d.ts +13 -0
  58. package/dist/core/command-generation/adapters/trae.js +48 -0
  59. package/dist/core/command-generation/adapters/windsurf.js +1 -14
  60. package/dist/core/command-generation/registry.js +4 -0
  61. package/dist/core/command-generation/yaml.d.ts +22 -0
  62. package/dist/core/command-generation/yaml.js +38 -0
  63. package/dist/core/completions/command-registry.js +87 -289
  64. package/dist/core/completions/generators/zsh-generator.js +3 -1
  65. package/dist/core/completions/installers/bash-installer.js +3 -0
  66. package/dist/core/completions/installers/fish-installer.js +8 -0
  67. package/dist/core/completions/installers/powershell-installer.js +30 -2
  68. package/dist/core/completions/installers/zsh-installer.js +3 -0
  69. package/dist/core/completions/shared-flags.d.ts +1 -0
  70. package/dist/core/completions/shared-flags.js +5 -0
  71. package/dist/core/config-schema.d.ts +2 -1
  72. package/dist/core/config-schema.js +26 -0
  73. package/dist/core/config.js +1 -0
  74. package/dist/core/file-state.d.ts +36 -0
  75. package/dist/core/file-state.js +112 -0
  76. package/dist/core/global-config.d.ts +2 -0
  77. package/dist/core/id.d.ts +17 -0
  78. package/dist/core/id.js +30 -0
  79. package/dist/core/index.d.ts +3 -3
  80. package/dist/core/index.js +3 -3
  81. package/dist/core/init.js +27 -6
  82. package/dist/core/list.d.ts +2 -0
  83. package/dist/core/list.js +37 -14
  84. package/dist/core/openers.d.ts +77 -0
  85. package/dist/core/openers.js +251 -0
  86. package/dist/core/openspec-root.d.ts +45 -0
  87. package/dist/core/openspec-root.js +195 -0
  88. package/dist/core/parsers/change-parser.js +2 -1
  89. package/dist/core/parsers/markdown-parser.d.ts +0 -3
  90. package/dist/core/parsers/markdown-parser.js +5 -59
  91. package/dist/core/parsers/requirement-blocks.d.ts +11 -0
  92. package/dist/core/parsers/requirement-blocks.js +42 -8
  93. package/dist/core/parsers/requirement-text.d.ts +55 -0
  94. package/dist/core/parsers/requirement-text.js +136 -0
  95. package/dist/core/planning-home.d.ts +1 -6
  96. package/dist/core/planning-home.js +2 -43
  97. package/dist/core/profile-sync-drift.js +1 -0
  98. package/dist/core/profiles.d.ts +2 -2
  99. package/dist/core/profiles.js +2 -1
  100. package/dist/core/project-config.d.ts +44 -1
  101. package/dist/core/project-config.js +160 -10
  102. package/dist/core/references.d.ts +63 -0
  103. package/dist/core/references.js +310 -0
  104. package/dist/core/relationship-health.d.ts +65 -0
  105. package/dist/core/relationship-health.js +64 -0
  106. package/dist/core/root-selection.d.ts +122 -0
  107. package/dist/core/root-selection.js +337 -0
  108. package/dist/core/schemas/base.schema.js +7 -2
  109. package/dist/core/shared/allowed-tools.d.ts +12 -0
  110. package/dist/core/shared/allowed-tools.js +12 -0
  111. package/dist/core/shared/skill-generation.js +5 -1
  112. package/dist/core/shared/tool-detection.d.ts +2 -2
  113. package/dist/core/shared/tool-detection.js +2 -0
  114. package/dist/core/specs-apply.d.ts +6 -1
  115. package/dist/core/specs-apply.js +41 -5
  116. package/dist/core/store/errors.d.ts +20 -0
  117. package/dist/core/{context-store → store}/errors.js +3 -3
  118. package/dist/core/store/foundation.d.ts +56 -0
  119. package/dist/core/store/foundation.js +251 -0
  120. package/dist/core/store/git.d.ts +23 -0
  121. package/dist/core/store/git.js +137 -0
  122. package/dist/core/{context-store → store}/index.d.ts +0 -1
  123. package/dist/core/{context-store → store}/index.js +0 -1
  124. package/dist/core/store/operations.d.ts +114 -0
  125. package/dist/core/store/operations.js +803 -0
  126. package/dist/core/store/registry.d.ts +58 -0
  127. package/dist/core/store/registry.js +275 -0
  128. package/dist/core/templates/skill-templates.d.ts +1 -0
  129. package/dist/core/templates/skill-templates.js +1 -0
  130. package/dist/core/templates/workflows/apply-change.js +5 -4
  131. package/dist/core/templates/workflows/archive-change.js +5 -4
  132. package/dist/core/templates/workflows/bulk-archive-change.js +5 -4
  133. package/dist/core/templates/workflows/continue-change.js +7 -2
  134. package/dist/core/templates/workflows/explore.js +5 -0
  135. package/dist/core/templates/workflows/ff-change.js +5 -0
  136. package/dist/core/templates/workflows/new-change.js +5 -0
  137. package/dist/core/templates/workflows/onboard.js +4 -1
  138. package/dist/core/templates/workflows/propose.js +5 -0
  139. package/dist/core/templates/workflows/store-selection.d.ts +8 -0
  140. package/dist/core/templates/workflows/store-selection.js +8 -0
  141. package/dist/core/templates/workflows/sync-specs.js +5 -4
  142. package/dist/core/templates/workflows/update-change.d.ts +10 -0
  143. package/dist/core/templates/workflows/update-change.js +167 -0
  144. package/dist/core/templates/workflows/verify-change.js +5 -4
  145. package/dist/core/update.js +4 -4
  146. package/dist/core/validation/validator.d.ts +7 -0
  147. package/dist/core/validation/validator.js +101 -38
  148. package/dist/core/view.js +1 -1
  149. package/dist/core/working-set.d.ts +47 -0
  150. package/dist/core/working-set.js +43 -0
  151. package/dist/core/worksets.d.ts +75 -0
  152. package/dist/core/worksets.js +245 -0
  153. package/dist/core/zod-issues.d.ts +4 -0
  154. package/dist/core/zod-issues.js +10 -0
  155. package/dist/utils/change-metadata.d.ts +1 -0
  156. package/dist/utils/change-metadata.js +1 -1
  157. package/dist/utils/change-utils.js +15 -0
  158. package/dist/utils/file-system.js +36 -17
  159. package/dist/utils/task-progress.d.ts +10 -1
  160. package/dist/utils/task-progress.js +66 -2
  161. package/package.json +26 -23
  162. package/dist/commands/context-store.d.ts +0 -3
  163. package/dist/commands/context-store.js +0 -475
  164. package/dist/commands/initiative.d.ts +0 -13
  165. package/dist/commands/initiative.js +0 -318
  166. package/dist/commands/workflow/initiative-link.d.ts +0 -24
  167. package/dist/commands/workflow/initiative-link.js +0 -47
  168. package/dist/commands/workflow/set-change.d.ts +0 -13
  169. package/dist/commands/workflow/set-change.js +0 -87
  170. package/dist/commands/workspace/context-status.d.ts +0 -4
  171. package/dist/commands/workspace/context-status.js +0 -59
  172. package/dist/commands/workspace/open-target-selection.d.ts +0 -13
  173. package/dist/commands/workspace/open-target-selection.js +0 -146
  174. package/dist/commands/workspace/open-view.d.ts +0 -62
  175. package/dist/commands/workspace/open-view.js +0 -249
  176. package/dist/commands/workspace/open.d.ts +0 -37
  177. package/dist/commands/workspace/open.js +0 -110
  178. package/dist/commands/workspace/opener-selection.d.ts +0 -11
  179. package/dist/commands/workspace/opener-selection.js +0 -98
  180. package/dist/commands/workspace/operations.d.ts +0 -29
  181. package/dist/commands/workspace/operations.js +0 -543
  182. package/dist/commands/workspace/prompt-theme.d.ts +0 -29
  183. package/dist/commands/workspace/prompt-theme.js +0 -24
  184. package/dist/commands/workspace/registration.d.ts +0 -13
  185. package/dist/commands/workspace/registration.js +0 -84
  186. package/dist/commands/workspace/selection.d.ts +0 -8
  187. package/dist/commands/workspace/selection.js +0 -104
  188. package/dist/commands/workspace/setup-prompts.d.ts +0 -13
  189. package/dist/commands/workspace/setup-prompts.js +0 -121
  190. package/dist/commands/workspace/types.d.ts +0 -103
  191. package/dist/commands/workspace/types.js +0 -36
  192. package/dist/commands/workspace.d.ts +0 -5
  193. package/dist/commands/workspace.js +0 -574
  194. package/dist/core/collections/index.d.ts +0 -3
  195. package/dist/core/collections/index.js +0 -3
  196. package/dist/core/collections/initiatives/collection.d.ts +0 -4
  197. package/dist/core/collections/initiatives/collection.js +0 -17
  198. package/dist/core/collections/initiatives/index.d.ts +0 -6
  199. package/dist/core/collections/initiatives/index.js +0 -6
  200. package/dist/core/collections/initiatives/operations.d.ts +0 -49
  201. package/dist/core/collections/initiatives/operations.js +0 -175
  202. package/dist/core/collections/initiatives/resolution.d.ts +0 -87
  203. package/dist/core/collections/initiatives/resolution.js +0 -374
  204. package/dist/core/collections/initiatives/schema.d.ts +0 -41
  205. package/dist/core/collections/initiatives/schema.js +0 -134
  206. package/dist/core/collections/initiatives/templates.d.ts +0 -12
  207. package/dist/core/collections/initiatives/templates.js +0 -90
  208. package/dist/core/collections/runtime.d.ts +0 -46
  209. package/dist/core/collections/runtime.js +0 -194
  210. package/dist/core/context-store/binding.d.ts +0 -53
  211. package/dist/core/context-store/binding.js +0 -197
  212. package/dist/core/context-store/errors.d.ts +0 -20
  213. package/dist/core/context-store/foundation.d.ts +0 -55
  214. package/dist/core/context-store/foundation.js +0 -321
  215. package/dist/core/context-store/operations.d.ts +0 -85
  216. package/dist/core/context-store/operations.js +0 -528
  217. package/dist/core/context-store/registry.d.ts +0 -45
  218. package/dist/core/context-store/registry.js +0 -229
  219. package/dist/core/workspace/foundation.d.ts +0 -67
  220. package/dist/core/workspace/foundation.js +0 -295
  221. package/dist/core/workspace/index.d.ts +0 -8
  222. package/dist/core/workspace/index.js +0 -8
  223. package/dist/core/workspace/legacy-state.d.ts +0 -28
  224. package/dist/core/workspace/legacy-state.js +0 -200
  225. package/dist/core/workspace/link-input.d.ts +0 -9
  226. package/dist/core/workspace/link-input.js +0 -32
  227. package/dist/core/workspace/open-surface.d.ts +0 -45
  228. package/dist/core/workspace/open-surface.js +0 -215
  229. package/dist/core/workspace/openers.d.ts +0 -21
  230. package/dist/core/workspace/openers.js +0 -124
  231. package/dist/core/workspace/registry.d.ts +0 -24
  232. package/dist/core/workspace/registry.js +0 -146
  233. package/dist/core/workspace/skills.d.ts +0 -57
  234. package/dist/core/workspace/skills.js +0 -334
  235. package/dist/core/workspace/state-io.d.ts +0 -10
  236. package/dist/core/workspace/state-io.js +0 -121
  237. package/schemas/workspace-planning/schema.yaml +0 -72
  238. package/schemas/workspace-planning/templates/design.md +0 -33
  239. package/schemas/workspace-planning/templates/proposal.md +0 -28
  240. package/schemas/workspace-planning/templates/spec.md +0 -9
  241. package/schemas/workspace-planning/templates/tasks.md +0 -15
@@ -24,5 +24,10 @@ export const COMMON_FLAGS = {
24
24
  takesValue: true,
25
25
  values: ['change', 'spec'],
26
26
  },
27
+ store: {
28
+ name: 'store',
29
+ description: "Store id to use as the OpenSpec root (a store is a standalone OpenSpec repo you've registered)",
30
+ takesValue: true,
31
+ },
27
32
  };
28
33
  //# sourceMappingURL=shared-flags.js.map
@@ -58,13 +58,14 @@ export declare function deleteNestedValue(obj: Record<string, unknown>, path: st
58
58
  * Coerce a string value to its appropriate type.
59
59
  * - "true" / "false" -> boolean
60
60
  * - Numeric strings -> number
61
+ * - JSON arrays/objects -> parsed containers
61
62
  * - Everything else -> string
62
63
  *
63
64
  * @param value - The string value to coerce
64
65
  * @param forceString - If true, always return the value as a string
65
66
  * @returns The coerced value
66
67
  */
67
- export declare function coerceValue(value: string, forceString?: boolean): string | number | boolean;
68
+ export declare function coerceValue(value: string, forceString?: boolean): string | number | boolean | unknown[] | Record<string, unknown>;
68
69
  /**
69
70
  * Format a value for YAML-like display.
70
71
  *
@@ -125,6 +125,7 @@ export function deleteNestedValue(obj, path) {
125
125
  * Coerce a string value to its appropriate type.
126
126
  * - "true" / "false" -> boolean
127
127
  * - Numeric strings -> number
128
+ * - JSON arrays/objects -> parsed containers
128
129
  * - Everything else -> string
129
130
  *
130
131
  * @param value - The string value to coerce
@@ -147,8 +148,33 @@ export function coerceValue(value, forceString = false) {
147
148
  if (!isNaN(num) && isFinite(num) && value.trim() !== '') {
148
149
  return num;
149
150
  }
151
+ const jsonContainer = parseJsonContainer(value);
152
+ if (jsonContainer !== undefined) {
153
+ return jsonContainer;
154
+ }
150
155
  return value;
151
156
  }
157
+ function parseJsonContainer(value) {
158
+ const trimmed = value.trim();
159
+ const looksLikeContainer = (trimmed.startsWith('[') && trimmed.endsWith(']')) ||
160
+ (trimmed.startsWith('{') && trimmed.endsWith('}'));
161
+ if (!looksLikeContainer) {
162
+ return undefined;
163
+ }
164
+ try {
165
+ const parsed = JSON.parse(trimmed);
166
+ if (Array.isArray(parsed)) {
167
+ return parsed;
168
+ }
169
+ if (parsed !== null && typeof parsed === 'object') {
170
+ return parsed;
171
+ }
172
+ }
173
+ catch {
174
+ return undefined;
175
+ }
176
+ return undefined;
177
+ }
152
178
  /**
153
179
  * Format a value for YAML-like display.
154
180
  *
@@ -27,6 +27,7 @@ export const AI_TOOLS = [
27
27
  { name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' },
28
28
  { name: 'Lingma', value: 'lingma', available: true, successLabel: 'Lingma', skillsDir: '.lingma' },
29
29
  { name: 'Mistral Vibe', value: 'vibe', available: true, successLabel: 'Mistral Vibe', skillsDir: '.vibe' },
30
+ { name: 'Oh My Pi', value: 'oh-my-pi', available: true, successLabel: 'Oh My Pi', skillsDir: '.omp' },
30
31
  { name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' },
31
32
  { name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' },
32
33
  { name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' },
@@ -0,0 +1,36 @@
1
+ import * as nodeFs from 'node:fs';
2
+ import { StoreError } from './store/errors.js';
3
+ /**
4
+ * Shared machine-local state-file mechanics (extracted from the store
5
+ * registry in slice 7.1, its second consumer). Callers own the
6
+ * diagnostic data (code, target, wording); the factory owns the
7
+ * shared mechanics - the fix strings describe the lock's own
8
+ * behavior (stale-steal, creation), so their templates live here.
9
+ */
10
+ export type FileLockErrorKind = 'create-failed' | 'timeout';
11
+ export interface FileLockErrorInfo {
12
+ lockPath: string;
13
+ /** The original errno error for 'create-failed'. */
14
+ cause?: unknown;
15
+ }
16
+ export interface FileLockOptions {
17
+ lockPath: string;
18
+ errorFor: (kind: FileLockErrorKind, info: FileLockErrorInfo) => Error;
19
+ }
20
+ export interface LockErrorData {
21
+ /** Noun phrase for the create-failed message, e.g. "the registry lock file". */
22
+ createSubject: string;
23
+ /** The full timeout message, e.g. "Store registry is busy." */
24
+ busyMessage: string;
25
+ code: string;
26
+ target: string;
27
+ }
28
+ /** One template for lock diagnostics; callers supply the data. */
29
+ export declare function makeLockErrorFactory(data: LockErrorData): (kind: FileLockErrorKind, info: FileLockErrorInfo) => StoreError;
30
+ export declare function isNodeErrorCode(error: unknown, code: string): boolean;
31
+ export declare function pathIsFile(filePath: string): Promise<boolean>;
32
+ export declare function pathIsDirectory(dirPath: string): Promise<boolean>;
33
+ export declare function writeFileAtomically(filePath: string, content: string): Promise<void>;
34
+ export declare function acquireFileLock(options: FileLockOptions): Promise<nodeFs.promises.FileHandle>;
35
+ export declare function releaseFileLock(lock: nodeFs.promises.FileHandle, lockPath: string): Promise<void>;
36
+ //# sourceMappingURL=file-state.d.ts.map
@@ -0,0 +1,112 @@
1
+ import * as nodeFs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { FileSystemUtils } from '../utils/file-system.js';
4
+ import { StoreError } from './store/errors.js';
5
+ const fs = nodeFs.promises;
6
+ /** One template for lock diagnostics; callers supply the data. */
7
+ export function makeLockErrorFactory(data) {
8
+ return (kind, info) => {
9
+ if (kind === 'create-failed') {
10
+ // A permission or filesystem problem, not contention - say so.
11
+ return new StoreError(`Cannot create ${data.createSubject} ${info.lockPath} (${info.cause?.code ?? info.cause}).`, data.code, {
12
+ target: data.target,
13
+ fix: `Check permissions on ${path.dirname(info.lockPath)}.`,
14
+ });
15
+ }
16
+ return new StoreError(data.busyMessage, data.code, {
17
+ target: data.target,
18
+ fix: `Retry shortly; if this persists, delete the stale lock file ${info.lockPath}.`,
19
+ });
20
+ };
21
+ }
22
+ const STALE_LOCK_THRESHOLD_MS = 30_000;
23
+ const LOCK_DEADLINE_MS = 5000;
24
+ const LOCK_POLL_MS = 25;
25
+ export function isNodeErrorCode(error, code) {
26
+ return (typeof error === 'object' &&
27
+ error !== null &&
28
+ 'code' in error &&
29
+ error.code === code);
30
+ }
31
+ export async function pathIsFile(filePath) {
32
+ try {
33
+ return (await fs.stat(filePath)).isFile();
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ // Deliberately not FileSystemUtils.directoryExists: that variant
40
+ // debug-logs non-ENOENT failures, which is noise inside prompt
41
+ // validators, and pathIsFile has no FileSystemUtils equivalent - the
42
+ // silent symmetric pair lives here.
43
+ export async function pathIsDirectory(dirPath) {
44
+ try {
45
+ return (await fs.stat(dirPath)).isDirectory();
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ async function sleep(milliseconds) {
52
+ await new Promise((resolve) => setTimeout(resolve, milliseconds));
53
+ }
54
+ export async function writeFileAtomically(filePath, content) {
55
+ const dirPath = path.dirname(filePath);
56
+ await FileSystemUtils.createDirectory(dirPath);
57
+ const tempPath = path.join(dirPath, `.${path.basename(filePath)}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`);
58
+ try {
59
+ await fs.writeFile(tempPath, content, 'utf-8');
60
+ await fs.rename(tempPath, filePath);
61
+ }
62
+ catch (error) {
63
+ await fs.rm(tempPath, { force: true }).catch(() => undefined);
64
+ throw error;
65
+ }
66
+ }
67
+ export async function acquireFileLock(options) {
68
+ const { lockPath, errorFor } = options;
69
+ const lockDir = path.dirname(lockPath);
70
+ await FileSystemUtils.createDirectory(lockDir);
71
+ if (!(await FileSystemUtils.canWriteFile(lockDir))) {
72
+ throw errorFor('create-failed', { lockPath, cause: 'EACCES' });
73
+ }
74
+ const deadline = Date.now() + LOCK_DEADLINE_MS;
75
+ while (true) {
76
+ try {
77
+ return await fs.open(lockPath, 'wx');
78
+ }
79
+ catch (error) {
80
+ if (!isNodeErrorCode(error, 'EEXIST')) {
81
+ // A permission or filesystem problem, not contention - say so.
82
+ throw errorFor('create-failed', { lockPath, cause: error });
83
+ }
84
+ // A crashed process leaves the lock behind forever; state-file
85
+ // writes are sub-second, so an old lock is an orphan - steal it.
86
+ let staleStolen = false;
87
+ try {
88
+ const lockStat = await fs.stat(lockPath);
89
+ if (Date.now() - lockStat.mtimeMs > STALE_LOCK_THRESHOLD_MS) {
90
+ await fs.rm(lockPath, { force: true });
91
+ staleStolen = true;
92
+ }
93
+ }
94
+ catch {
95
+ // The holder released between open and stat - retry, but stay
96
+ // bounded: a persistently failing stat (EPERM, delete-pending)
97
+ // must hit the deadline instead of spinning forever.
98
+ }
99
+ if (!staleStolen) {
100
+ if (Date.now() >= deadline) {
101
+ throw errorFor('timeout', { lockPath });
102
+ }
103
+ await sleep(LOCK_POLL_MS);
104
+ }
105
+ }
106
+ }
107
+ }
108
+ export async function releaseFileLock(lock, lockPath) {
109
+ await lock.close().catch(() => undefined);
110
+ await fs.rm(lockPath, { force: true }).catch(() => undefined);
111
+ }
112
+ //# sourceMappingURL=file-state.js.map
@@ -8,6 +8,8 @@ export interface GlobalConfig {
8
8
  profile?: Profile;
9
9
  delivery?: Delivery;
10
10
  workflows?: string[];
11
+ /** Workset opener rows (slice 7.1); hand-edited, validated on use. */
12
+ openers?: unknown;
11
13
  }
12
14
  /**
13
15
  * Gets the global configuration directory path following XDG Base Directory Specification.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The one kebab id grammar. Store ids, change ids, and legacy initiative ids
3
+ * all share it.
4
+ */
5
+ export declare const KEBAB_ID_REGEX: RegExp;
6
+ export declare function isKebabId(value: string): boolean;
7
+ /** Human rendering of the grammar, shared so the wording never forks. */
8
+ export declare const KEBAB_ID_DESCRIPTION = "must be kebab-case with lowercase letters, numbers, and single hyphen separators";
9
+ /** The fix-line twin of KEBAB_ID_DESCRIPTION, shared for the same reason. */
10
+ export declare const KEBAB_ID_FIX = "Use kebab-case with lowercase letters, numbers, and single hyphen separators.";
11
+ /**
12
+ * The folder-safe-name grammar (store ids layer the kebab grammar on
13
+ * top of it; workset member labels use it alone). Returns a problem
14
+ * description, or null when valid.
15
+ */
16
+ export declare function folderStyleNameProblem(value: string, label: string): string | null;
17
+ //# sourceMappingURL=id.d.ts.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The one kebab id grammar. Store ids, change ids, and legacy initiative ids
3
+ * all share it.
4
+ */
5
+ export const KEBAB_ID_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
6
+ export function isKebabId(value) {
7
+ return KEBAB_ID_REGEX.test(value);
8
+ }
9
+ /** Human rendering of the grammar, shared so the wording never forks. */
10
+ export const KEBAB_ID_DESCRIPTION = 'must be kebab-case with lowercase letters, numbers, and single hyphen separators';
11
+ /** The fix-line twin of KEBAB_ID_DESCRIPTION, shared for the same reason. */
12
+ export const KEBAB_ID_FIX = 'Use kebab-case with lowercase letters, numbers, and single hyphen separators.';
13
+ /**
14
+ * The folder-safe-name grammar (store ids layer the kebab grammar on
15
+ * top of it; workset member labels use it alone). Returns a problem
16
+ * description, or null when valid.
17
+ */
18
+ export function folderStyleNameProblem(value, label) {
19
+ if (value.length === 0) {
20
+ return `${label} must not be empty`;
21
+ }
22
+ if (value === '.' || value === '..') {
23
+ return `${label} must not be '${value}'`;
24
+ }
25
+ if (/[\\/]/u.test(value)) {
26
+ return `${label} must not contain path separators`;
27
+ }
28
+ return null;
29
+ }
30
+ //# sourceMappingURL=id.js.map
@@ -1,6 +1,6 @@
1
1
  export { GLOBAL_CONFIG_DIR_NAME, GLOBAL_CONFIG_FILE_NAME, GLOBAL_DATA_DIR_NAME, type GlobalDataDirOptions, type GlobalConfig, getGlobalConfigDir, getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, getGlobalDataDir } from './global-config.js';
2
- export * from './workspace/index.js';
3
- export * from './context-store/index.js';
4
- export * from './collections/index.js';
2
+ export * from './references.js';
3
+ export * from './store/index.js';
5
4
  export * from './planning-home.js';
5
+ export * from './openspec-root.js';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1,7 +1,7 @@
1
1
  // Core OpenSpec logic will be implemented here
2
2
  export { GLOBAL_CONFIG_DIR_NAME, GLOBAL_CONFIG_FILE_NAME, GLOBAL_DATA_DIR_NAME, getGlobalConfigDir, getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, getGlobalDataDir } from './global-config.js';
3
- export * from './workspace/index.js';
4
- export * from './context-store/index.js';
5
- export * from './collections/index.js';
3
+ export * from './references.js';
4
+ export * from './store/index.js';
6
5
  export * from './planning-home.js';
6
+ export * from './openspec-root.js';
7
7
  //# sourceMappingURL=index.js.map
package/dist/core/init.js CHANGED
@@ -10,6 +10,8 @@ import ora from 'ora';
10
10
  import * as fs from 'fs';
11
11
  import { createRequire } from 'module';
12
12
  import { FileSystemUtils } from '../utils/file-system.js';
13
+ import { classifyOpenSpecDir, storePointerProblem } from './project-config.js';
14
+ import { findRepoPlanningRootSync } from './planning-home.js';
13
15
  import { transformToHyphenCommands } from '../utils/command-references.js';
14
16
  import { AI_TOOLS, OPENSPEC_DIR_NAME, } from './config.js';
15
17
  import { PALETTE } from './styles/palette.js';
@@ -37,6 +39,7 @@ const WORKFLOW_TO_SKILL_DIR = {
37
39
  'new': 'openspec-new-change',
38
40
  'continue': 'openspec-continue-change',
39
41
  'apply': 'openspec-apply-change',
42
+ 'update': 'openspec-update-change',
40
43
  'ff': 'openspec-ff-change',
41
44
  'sync': 'openspec-sync-specs',
42
45
  'archive': 'openspec-archive-change',
@@ -65,6 +68,28 @@ export class InitCommand {
65
68
  const openspecPath = path.join(projectPath, openspecDir);
66
69
  // Validation happens silently in the background
67
70
  const extendMode = await this.validate(projectPath, openspecPath);
71
+ // Pointer guard (slice 3.2): a config-only openspec/ with a store:
72
+ // declaration is externalized planning, not a root to extend — and a
73
+ // subdirectory of such a repo must not silently grow a nested root.
74
+ // Refuse before legacy cleanup, migration, or prompts touch anything.
75
+ // In extend mode the walk finds projectPath itself; otherwise it
76
+ // finds the nearest ancestor root (so pointer-repo subdirectories
77
+ // refuse exactly where a normal command would resolve the pointer).
78
+ const guardRoot = findRepoPlanningRootSync(projectPath);
79
+ if (guardRoot) {
80
+ const { hasPlanningShape, pointer } = classifyOpenSpecDir(guardRoot);
81
+ if (!hasPlanningShape) {
82
+ if (pointer.malformed) {
83
+ throw new Error(`The store declaration in ${pointer.filePath} is invalid (` +
84
+ storePointerProblem(pointer.malformed) +
85
+ `). Fix or remove the store: line before running openspec init.`);
86
+ }
87
+ if (pointer.value !== undefined) {
88
+ throw new Error(`This repo's planning is externalized to store '${pointer.value}' (${pointer.filePath}). ` +
89
+ `Remove the store: line first to convert this repo to a local OpenSpec root.`);
90
+ }
91
+ }
92
+ }
68
93
  // Check for legacy artifacts and handle cleanup
69
94
  await this.handleLegacyCleanup(projectPath, extendMode);
70
95
  // Detect available tools in the project (task 7.1)
@@ -379,8 +404,8 @@ export class InitCommand {
379
404
  const skillDir = path.join(skillsDir, dirName);
380
405
  const skillFile = path.join(skillDir, 'SKILL.md');
381
406
  // Generate SKILL.md content with YAML frontmatter including generatedBy
382
- // Use hyphen-based command references for tools where filename = command name
383
- const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined;
407
+ // Use hyphen-based command references for tools where filename === command name (oh-my-pi, opencode, pi)
408
+ const transformer = (tool.value === 'opencode' || tool.value === 'pi' || tool.value === 'oh-my-pi') ? transformToHyphenCommands : undefined;
384
409
  const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
385
410
  // Write the skill file
386
411
  await FileSystemUtils.writeFile(skillFile, skillContent);
@@ -440,10 +465,6 @@ export class InitCommand {
440
465
  if (configYamlExists || configYmlExists) {
441
466
  return 'exists';
442
467
  }
443
- // In non-interactive mode without --force, skip config creation
444
- if (!this.canPromptInteractively() && !this.force) {
445
- return 'skipped';
446
- }
447
468
  try {
448
469
  const yamlContent = serializeConfig({ schema: DEFAULT_SCHEMA });
449
470
  await FileSystemUtils.writeFile(configPath, yamlContent);
@@ -1,6 +1,8 @@
1
+ import type { RootOutput } from './root-selection.js';
1
2
  interface ListOptions {
2
3
  sort?: 'recent' | 'name';
3
4
  json?: boolean;
5
+ root?: RootOutput;
4
6
  }
5
7
  export declare class ListCommand {
6
8
  execute(targetPath?: string, mode?: 'changes' | 'specs', options?: ListOptions): Promise<void>;
package/dist/core/list.js CHANGED
@@ -4,6 +4,22 @@ import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progre
4
4
  import { readFileSync } from 'fs';
5
5
  import { join } from 'path';
6
6
  import { MarkdownParser } from './parsers/markdown-parser.js';
7
+ function isMissingPathError(error) {
8
+ return (typeof error === 'object' &&
9
+ error !== null &&
10
+ 'code' in error &&
11
+ error.code === 'ENOENT');
12
+ }
13
+ async function readChangeDirectoryEntries(changesDir) {
14
+ try {
15
+ return await fs.readdir(changesDir, { withFileTypes: true });
16
+ }
17
+ catch (error) {
18
+ if (isMissingPathError(error))
19
+ return [];
20
+ throw error;
21
+ }
22
+ }
7
23
  /**
8
24
  * Get the most recent modification time of any file in a directory (recursive).
9
25
  * Falls back to the directory's own mtime if no files are found.
@@ -61,24 +77,17 @@ function formatRelativeTime(date) {
61
77
  }
62
78
  export class ListCommand {
63
79
  async execute(targetPath = '.', mode = 'changes', options = {}) {
64
- const { sort = 'recent', json = false } = options;
80
+ const { sort = 'recent', json = false, root } = options;
65
81
  if (mode === 'changes') {
66
82
  const changesDir = path.join(targetPath, 'openspec', 'changes');
67
- // Check if changes directory exists
68
- try {
69
- await fs.access(changesDir);
70
- }
71
- catch {
72
- throw new Error("No OpenSpec changes directory found. Run 'openspec init' first.");
73
- }
74
83
  // Get all directories in changes (excluding archive)
75
- const entries = await fs.readdir(changesDir, { withFileTypes: true });
84
+ const entries = await readChangeDirectoryEntries(changesDir);
76
85
  const changeDirs = entries
77
86
  .filter(entry => entry.isDirectory() && entry.name !== 'archive')
78
87
  .map(entry => entry.name);
79
88
  if (changeDirs.length === 0) {
80
89
  if (json) {
81
- console.log(JSON.stringify({ changes: [] }));
90
+ console.log(JSON.stringify({ changes: [], ...(root ? { root } : {}) }, null, 2));
82
91
  }
83
92
  else {
84
93
  console.log('No active changes found.');
@@ -88,7 +97,7 @@ export class ListCommand {
88
97
  // Collect information about each change
89
98
  const changes = [];
90
99
  for (const changeDir of changeDirs) {
91
- const progress = await getTaskProgressForChange(changesDir, changeDir);
100
+ const progress = await getTaskProgressForChange(changesDir, changeDir, targetPath);
92
101
  const changePath = path.join(changesDir, changeDir);
93
102
  const lastModified = await getLastModified(changePath);
94
103
  changes.push({
@@ -114,7 +123,7 @@ export class ListCommand {
114
123
  lastModified: c.lastModified.toISOString(),
115
124
  status: c.totalTasks === 0 ? 'no-tasks' : c.completedTasks === c.totalTasks ? 'complete' : 'in-progress'
116
125
  }));
117
- console.log(JSON.stringify({ changes: jsonOutput }, null, 2));
126
+ console.log(JSON.stringify({ changes: jsonOutput, ...(root ? { root } : {}) }, null, 2));
118
127
  return;
119
128
  }
120
129
  // Display results
@@ -135,13 +144,23 @@ export class ListCommand {
135
144
  await fs.access(specsDir);
136
145
  }
137
146
  catch {
138
- console.log('No specs found.');
147
+ if (json) {
148
+ console.log(JSON.stringify({ specs: [], ...(root ? { root } : {}) }, null, 2));
149
+ }
150
+ else {
151
+ console.log('No specs found.');
152
+ }
139
153
  return;
140
154
  }
141
155
  const entries = await fs.readdir(specsDir, { withFileTypes: true });
142
156
  const specDirs = entries.filter(e => e.isDirectory()).map(e => e.name);
143
157
  if (specDirs.length === 0) {
144
- console.log('No specs found.');
158
+ if (json) {
159
+ console.log(JSON.stringify({ specs: [], ...(root ? { root } : {}) }, null, 2));
160
+ }
161
+ else {
162
+ console.log('No specs found.');
163
+ }
145
164
  return;
146
165
  }
147
166
  const specs = [];
@@ -159,6 +178,10 @@ export class ListCommand {
159
178
  }
160
179
  }
161
180
  specs.sort((a, b) => a.id.localeCompare(b.id));
181
+ if (json) {
182
+ console.log(JSON.stringify({ specs, ...(root ? { root } : {}) }, null, 2));
183
+ return;
184
+ }
162
185
  console.log('Specs:');
163
186
  const padding = ' ';
164
187
  const nameWidth = Math.max(...specs.map(s => s.id.length));
@@ -0,0 +1,77 @@
1
+ import type { WorksetMember } from './worksets.js';
2
+ /**
3
+ * The workset opener table (slice 7.1). Supporting a new tool is
4
+ * configuration, not code: every tool is an instance of one of exactly
5
+ * two launch styles - 'workspace-file' (invoke with the generated
6
+ * .code-workspace) or 'attach-dirs' (pre-args plus one attach flag per
7
+ * member; no positional, ever - agent sessions open clean). Users add
8
+ * tools or adjust parameters under the global config file's `openers`
9
+ * key (the git difftool/mergetool pattern).
10
+ */
11
+ export type OpenerStyle = 'workspace-file' | 'attach-dirs';
12
+ export interface OpenerDefinition {
13
+ id: string;
14
+ label: string;
15
+ style: OpenerStyle;
16
+ command: string;
17
+ /** Pre-args before any attach flags or the workspace-file path. */
18
+ args: string[];
19
+ /** attach-dirs only; one flag + path pair per member. */
20
+ attachFlag: string;
21
+ }
22
+ /**
23
+ * Temporary kill-switch (2026-06): worksets open only in IDE-style
24
+ * ('workspace-file') tools while the CLI-agent ('attach-dirs') open flow
25
+ * is reworked. The agents (Claude Code, codex) launch in a single primary
26
+ * cwd rather than a true combined multi-root view, which makes "where does
27
+ * my change land?" ambiguous. Default off; set
28
+ * OPENSPEC_ENABLE_CLI_AGENT_OPENERS=1 to restore them (internal rollback seam).
29
+ */
30
+ export declare function isCliAgentOpenersEnabled(): boolean;
31
+ /** Whether a tool can be opened right now (CLI-agent styles are gated). */
32
+ export declare function isOpenerEnabled(opener: OpenerDefinition): boolean;
33
+ export declare const BUILTIN_OPENERS: readonly OpenerDefinition[];
34
+ export declare function mergeOpenerTable(rawOpeners: unknown, configPath: string): OpenerDefinition[];
35
+ export interface OpenerScanOptions {
36
+ env?: NodeJS.ProcessEnv;
37
+ platform?: NodeJS.Platform;
38
+ /** Stat seam for tests (win32 candidate paths on posix hosts). */
39
+ isExecutableFile?: (candidatePath: string) => boolean;
40
+ }
41
+ /**
42
+ * PATH availability scan (ported from the deleted workspace openers
43
+ * at f858c19^, sharpened for injectability: the path module is keyed
44
+ * by the injected platform, and a command already carrying a known
45
+ * executable extension matches as-is).
46
+ */
47
+ export declare function isOpenerCommandAvailable(command: string, options?: OpenerScanOptions): boolean;
48
+ export interface OpenerChoice {
49
+ opener: OpenerDefinition;
50
+ available: boolean;
51
+ /** `(<command> not found on PATH)` when unavailable. */
52
+ note: string | null;
53
+ }
54
+ /** Table order preserved, available tools first (stable sort). */
55
+ export declare function listOpenerChoices(table: OpenerDefinition[], options?: OpenerScanOptions): OpenerChoice[];
56
+ export declare function findOpener(table: OpenerDefinition[], id: string): OpenerDefinition | null;
57
+ export interface LaunchCommand {
58
+ executable: string;
59
+ args: string[];
60
+ /** The surviving primary member's path. */
61
+ cwd: string;
62
+ label: string;
63
+ style: OpenerStyle;
64
+ }
65
+ /**
66
+ * Pure argv builder. workspace-file: pre-args + the generated file's
67
+ * absolute path (which also defuses the cursor shim's `agent`
68
+ * first-arg hijack). attach-dirs: pre-args + one attach flag + path
69
+ * pair per surviving member, the primary included (the locked "one
70
+ * attach flag per member"); never a trailing positional - both agent
71
+ * CLIs would read one as a starter prompt, which 7.1 locks out.
72
+ */
73
+ export declare function buildLaunchCommand(opener: OpenerDefinition, input: {
74
+ members: WorksetMember[];
75
+ codeWorkspacePath: string;
76
+ }): LaunchCommand;
77
+ //# sourceMappingURL=openers.d.ts.map