@fission-ai/openspec 1.4.0 → 1.5.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 (206) hide show
  1. package/README.md +24 -3
  2. package/dist/cli/index.d.ts +5 -0
  3. package/dist/cli/index.js +94 -76
  4. package/dist/commands/change.d.ts +5 -1
  5. package/dist/commands/change.js +17 -15
  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 +2 -0
  22. package/dist/commands/validate.js +75 -39
  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 +209 -56
  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.js +1 -14
  50. package/dist/core/command-generation/adapters/cursor.js +1 -14
  51. package/dist/core/command-generation/adapters/pi.js +1 -14
  52. package/dist/core/command-generation/adapters/windsurf.js +1 -14
  53. package/dist/core/command-generation/yaml.d.ts +22 -0
  54. package/dist/core/command-generation/yaml.js +38 -0
  55. package/dist/core/completions/command-registry.js +87 -289
  56. package/dist/core/completions/generators/zsh-generator.js +3 -1
  57. package/dist/core/completions/installers/bash-installer.js +3 -0
  58. package/dist/core/completions/installers/fish-installer.js +8 -0
  59. package/dist/core/completions/installers/powershell-installer.js +30 -2
  60. package/dist/core/completions/installers/zsh-installer.js +3 -0
  61. package/dist/core/completions/shared-flags.d.ts +1 -0
  62. package/dist/core/completions/shared-flags.js +5 -0
  63. package/dist/core/config-schema.d.ts +2 -1
  64. package/dist/core/config-schema.js +26 -0
  65. package/dist/core/file-state.d.ts +36 -0
  66. package/dist/core/file-state.js +112 -0
  67. package/dist/core/global-config.d.ts +2 -0
  68. package/dist/core/id.d.ts +17 -0
  69. package/dist/core/id.js +30 -0
  70. package/dist/core/index.d.ts +3 -3
  71. package/dist/core/index.js +3 -3
  72. package/dist/core/init.js +24 -4
  73. package/dist/core/list.d.ts +2 -0
  74. package/dist/core/list.js +19 -5
  75. package/dist/core/openers.d.ts +77 -0
  76. package/dist/core/openers.js +251 -0
  77. package/dist/core/openspec-root.d.ts +45 -0
  78. package/dist/core/openspec-root.js +192 -0
  79. package/dist/core/planning-home.d.ts +1 -6
  80. package/dist/core/planning-home.js +2 -43
  81. package/dist/core/project-config.d.ts +44 -1
  82. package/dist/core/project-config.js +160 -10
  83. package/dist/core/references.d.ts +63 -0
  84. package/dist/core/references.js +310 -0
  85. package/dist/core/relationship-health.d.ts +65 -0
  86. package/dist/core/relationship-health.js +64 -0
  87. package/dist/core/root-selection.d.ts +122 -0
  88. package/dist/core/root-selection.js +337 -0
  89. package/dist/core/specs-apply.d.ts +6 -1
  90. package/dist/core/specs-apply.js +6 -4
  91. package/dist/core/store/errors.d.ts +20 -0
  92. package/dist/core/{context-store → store}/errors.js +3 -3
  93. package/dist/core/store/foundation.d.ts +56 -0
  94. package/dist/core/store/foundation.js +251 -0
  95. package/dist/core/store/git.d.ts +23 -0
  96. package/dist/core/store/git.js +137 -0
  97. package/dist/core/{context-store → store}/index.d.ts +0 -1
  98. package/dist/core/{context-store → store}/index.js +0 -1
  99. package/dist/core/store/operations.d.ts +114 -0
  100. package/dist/core/store/operations.js +783 -0
  101. package/dist/core/store/registry.d.ts +58 -0
  102. package/dist/core/store/registry.js +275 -0
  103. package/dist/core/templates/workflows/apply-change.js +5 -4
  104. package/dist/core/templates/workflows/archive-change.js +5 -4
  105. package/dist/core/templates/workflows/bulk-archive-change.js +5 -4
  106. package/dist/core/templates/workflows/continue-change.js +7 -2
  107. package/dist/core/templates/workflows/explore.js +5 -0
  108. package/dist/core/templates/workflows/ff-change.js +5 -0
  109. package/dist/core/templates/workflows/new-change.js +5 -0
  110. package/dist/core/templates/workflows/onboard.js +4 -1
  111. package/dist/core/templates/workflows/propose.js +5 -0
  112. package/dist/core/templates/workflows/store-selection.d.ts +8 -0
  113. package/dist/core/templates/workflows/store-selection.js +8 -0
  114. package/dist/core/templates/workflows/sync-specs.js +5 -4
  115. package/dist/core/templates/workflows/verify-change.js +5 -4
  116. package/dist/core/working-set.d.ts +47 -0
  117. package/dist/core/working-set.js +43 -0
  118. package/dist/core/worksets.d.ts +75 -0
  119. package/dist/core/worksets.js +245 -0
  120. package/dist/core/zod-issues.d.ts +4 -0
  121. package/dist/core/zod-issues.js +10 -0
  122. package/dist/utils/change-metadata.d.ts +1 -0
  123. package/dist/utils/change-metadata.js +1 -1
  124. package/dist/utils/change-utils.js +15 -0
  125. package/dist/utils/file-system.js +36 -17
  126. package/package.json +6 -6
  127. package/dist/commands/context-store.d.ts +0 -3
  128. package/dist/commands/context-store.js +0 -475
  129. package/dist/commands/initiative.d.ts +0 -13
  130. package/dist/commands/initiative.js +0 -318
  131. package/dist/commands/workflow/initiative-link.d.ts +0 -24
  132. package/dist/commands/workflow/initiative-link.js +0 -47
  133. package/dist/commands/workflow/set-change.d.ts +0 -13
  134. package/dist/commands/workflow/set-change.js +0 -87
  135. package/dist/commands/workspace/context-status.d.ts +0 -4
  136. package/dist/commands/workspace/context-status.js +0 -59
  137. package/dist/commands/workspace/open-target-selection.d.ts +0 -13
  138. package/dist/commands/workspace/open-target-selection.js +0 -146
  139. package/dist/commands/workspace/open-view.d.ts +0 -62
  140. package/dist/commands/workspace/open-view.js +0 -249
  141. package/dist/commands/workspace/open.d.ts +0 -37
  142. package/dist/commands/workspace/open.js +0 -110
  143. package/dist/commands/workspace/opener-selection.d.ts +0 -11
  144. package/dist/commands/workspace/opener-selection.js +0 -98
  145. package/dist/commands/workspace/operations.d.ts +0 -29
  146. package/dist/commands/workspace/operations.js +0 -543
  147. package/dist/commands/workspace/prompt-theme.d.ts +0 -29
  148. package/dist/commands/workspace/prompt-theme.js +0 -24
  149. package/dist/commands/workspace/registration.d.ts +0 -13
  150. package/dist/commands/workspace/registration.js +0 -84
  151. package/dist/commands/workspace/selection.d.ts +0 -9
  152. package/dist/commands/workspace/selection.js +0 -115
  153. package/dist/commands/workspace/setup-prompts.d.ts +0 -13
  154. package/dist/commands/workspace/setup-prompts.js +0 -121
  155. package/dist/commands/workspace/types.d.ts +0 -103
  156. package/dist/commands/workspace/types.js +0 -36
  157. package/dist/commands/workspace.d.ts +0 -6
  158. package/dist/commands/workspace.js +0 -587
  159. package/dist/core/collections/index.d.ts +0 -3
  160. package/dist/core/collections/index.js +0 -3
  161. package/dist/core/collections/initiatives/collection.d.ts +0 -4
  162. package/dist/core/collections/initiatives/collection.js +0 -17
  163. package/dist/core/collections/initiatives/index.d.ts +0 -6
  164. package/dist/core/collections/initiatives/index.js +0 -6
  165. package/dist/core/collections/initiatives/operations.d.ts +0 -49
  166. package/dist/core/collections/initiatives/operations.js +0 -175
  167. package/dist/core/collections/initiatives/resolution.d.ts +0 -87
  168. package/dist/core/collections/initiatives/resolution.js +0 -374
  169. package/dist/core/collections/initiatives/schema.d.ts +0 -41
  170. package/dist/core/collections/initiatives/schema.js +0 -134
  171. package/dist/core/collections/initiatives/templates.d.ts +0 -12
  172. package/dist/core/collections/initiatives/templates.js +0 -90
  173. package/dist/core/collections/runtime.d.ts +0 -46
  174. package/dist/core/collections/runtime.js +0 -194
  175. package/dist/core/context-store/binding.d.ts +0 -53
  176. package/dist/core/context-store/binding.js +0 -197
  177. package/dist/core/context-store/errors.d.ts +0 -20
  178. package/dist/core/context-store/foundation.d.ts +0 -55
  179. package/dist/core/context-store/foundation.js +0 -321
  180. package/dist/core/context-store/operations.d.ts +0 -85
  181. package/dist/core/context-store/operations.js +0 -528
  182. package/dist/core/context-store/registry.d.ts +0 -45
  183. package/dist/core/context-store/registry.js +0 -229
  184. package/dist/core/workspace/foundation.d.ts +0 -67
  185. package/dist/core/workspace/foundation.js +0 -295
  186. package/dist/core/workspace/index.d.ts +0 -8
  187. package/dist/core/workspace/index.js +0 -8
  188. package/dist/core/workspace/legacy-state.d.ts +0 -28
  189. package/dist/core/workspace/legacy-state.js +0 -200
  190. package/dist/core/workspace/link-input.d.ts +0 -9
  191. package/dist/core/workspace/link-input.js +0 -32
  192. package/dist/core/workspace/open-surface.d.ts +0 -45
  193. package/dist/core/workspace/open-surface.js +0 -215
  194. package/dist/core/workspace/openers.d.ts +0 -21
  195. package/dist/core/workspace/openers.js +0 -124
  196. package/dist/core/workspace/registry.d.ts +0 -24
  197. package/dist/core/workspace/registry.js +0 -146
  198. package/dist/core/workspace/skills.d.ts +0 -57
  199. package/dist/core/workspace/skills.js +0 -334
  200. package/dist/core/workspace/state-io.d.ts +0 -10
  201. package/dist/core/workspace/state-io.js +0 -119
  202. package/schemas/workspace-planning/schema.yaml +0 -72
  203. package/schemas/workspace-planning/templates/design.md +0 -33
  204. package/schemas/workspace-planning/templates/proposal.md +0 -28
  205. package/schemas/workspace-planning/templates/spec.md +0 -9
  206. 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
  *
@@ -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';
@@ -65,6 +67,28 @@ export class InitCommand {
65
67
  const openspecPath = path.join(projectPath, openspecDir);
66
68
  // Validation happens silently in the background
67
69
  const extendMode = await this.validate(projectPath, openspecPath);
70
+ // Pointer guard (slice 3.2): a config-only openspec/ with a store:
71
+ // declaration is externalized planning, not a root to extend — and a
72
+ // subdirectory of such a repo must not silently grow a nested root.
73
+ // Refuse before legacy cleanup, migration, or prompts touch anything.
74
+ // In extend mode the walk finds projectPath itself; otherwise it
75
+ // finds the nearest ancestor root (so pointer-repo subdirectories
76
+ // refuse exactly where a normal command would resolve the pointer).
77
+ const guardRoot = findRepoPlanningRootSync(projectPath);
78
+ if (guardRoot) {
79
+ const { hasPlanningShape, pointer } = classifyOpenSpecDir(guardRoot);
80
+ if (!hasPlanningShape) {
81
+ if (pointer.malformed) {
82
+ throw new Error(`The store declaration in ${pointer.filePath} is invalid (` +
83
+ storePointerProblem(pointer.malformed) +
84
+ `). Fix or remove the store: line before running openspec init.`);
85
+ }
86
+ if (pointer.value !== undefined) {
87
+ throw new Error(`This repo's planning is externalized to store '${pointer.value}' (${pointer.filePath}). ` +
88
+ `Remove the store: line first to convert this repo to a local OpenSpec root.`);
89
+ }
90
+ }
91
+ }
68
92
  // Check for legacy artifacts and handle cleanup
69
93
  await this.handleLegacyCleanup(projectPath, extendMode);
70
94
  // Detect available tools in the project (task 7.1)
@@ -440,10 +464,6 @@ export class InitCommand {
440
464
  if (configYamlExists || configYmlExists) {
441
465
  return 'exists';
442
466
  }
443
- // In non-interactive mode without --force, skip config creation
444
- if (!this.canPromptInteractively() && !this.force) {
445
- return 'skipped';
446
- }
447
467
  try {
448
468
  const yamlContent = serializeConfig({ schema: DEFAULT_SCHEMA });
449
469
  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
@@ -61,7 +61,7 @@ function formatRelativeTime(date) {
61
61
  }
62
62
  export class ListCommand {
63
63
  async execute(targetPath = '.', mode = 'changes', options = {}) {
64
- const { sort = 'recent', json = false } = options;
64
+ const { sort = 'recent', json = false, root } = options;
65
65
  if (mode === 'changes') {
66
66
  const changesDir = path.join(targetPath, 'openspec', 'changes');
67
67
  // Check if changes directory exists
@@ -78,7 +78,7 @@ export class ListCommand {
78
78
  .map(entry => entry.name);
79
79
  if (changeDirs.length === 0) {
80
80
  if (json) {
81
- console.log(JSON.stringify({ changes: [] }));
81
+ console.log(JSON.stringify({ changes: [], ...(root ? { root } : {}) }, null, 2));
82
82
  }
83
83
  else {
84
84
  console.log('No active changes found.');
@@ -114,7 +114,7 @@ export class ListCommand {
114
114
  lastModified: c.lastModified.toISOString(),
115
115
  status: c.totalTasks === 0 ? 'no-tasks' : c.completedTasks === c.totalTasks ? 'complete' : 'in-progress'
116
116
  }));
117
- console.log(JSON.stringify({ changes: jsonOutput }, null, 2));
117
+ console.log(JSON.stringify({ changes: jsonOutput, ...(root ? { root } : {}) }, null, 2));
118
118
  return;
119
119
  }
120
120
  // Display results
@@ -135,13 +135,23 @@ export class ListCommand {
135
135
  await fs.access(specsDir);
136
136
  }
137
137
  catch {
138
- console.log('No specs found.');
138
+ if (json) {
139
+ console.log(JSON.stringify({ specs: [], ...(root ? { root } : {}) }, null, 2));
140
+ }
141
+ else {
142
+ console.log('No specs found.');
143
+ }
139
144
  return;
140
145
  }
141
146
  const entries = await fs.readdir(specsDir, { withFileTypes: true });
142
147
  const specDirs = entries.filter(e => e.isDirectory()).map(e => e.name);
143
148
  if (specDirs.length === 0) {
144
- console.log('No specs found.');
149
+ if (json) {
150
+ console.log(JSON.stringify({ specs: [], ...(root ? { root } : {}) }, null, 2));
151
+ }
152
+ else {
153
+ console.log('No specs found.');
154
+ }
145
155
  return;
146
156
  }
147
157
  const specs = [];
@@ -159,6 +169,10 @@ export class ListCommand {
159
169
  }
160
170
  }
161
171
  specs.sort((a, b) => a.id.localeCompare(b.id));
172
+ if (json) {
173
+ console.log(JSON.stringify({ specs, ...(root ? { root } : {}) }, null, 2));
174
+ return;
175
+ }
162
176
  console.log('Specs:');
163
177
  const padding = ' ';
164
178
  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