gmc-openspec 1.1.0 → 1.4.2

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 (111) hide show
  1. package/README.md +2 -2
  2. package/bin/openspec.js +3 -1
  3. package/dist/cli/index.d.ts +4 -1
  4. package/dist/cli/index.js +36 -2
  5. package/dist/commands/config.js +4 -4
  6. package/dist/commands/context-store.d.ts +3 -0
  7. package/dist/commands/context-store.js +475 -0
  8. package/dist/commands/initiative.d.ts +13 -0
  9. package/dist/commands/initiative.js +318 -0
  10. package/dist/commands/workflow/index.d.ts +2 -0
  11. package/dist/commands/workflow/index.js +1 -0
  12. package/dist/commands/workflow/initiative-link.d.ts +24 -0
  13. package/dist/commands/workflow/initiative-link.js +47 -0
  14. package/dist/commands/workflow/instructions.js +10 -2
  15. package/dist/commands/workflow/new-change.d.ts +4 -0
  16. package/dist/commands/workflow/new-change.js +72 -23
  17. package/dist/commands/workflow/set-change.d.ts +13 -0
  18. package/dist/commands/workflow/set-change.js +87 -0
  19. package/dist/commands/workflow/shared.d.ts +2 -0
  20. package/dist/commands/workflow/status.js +3 -0
  21. package/dist/commands/workspace/context-status.d.ts +4 -0
  22. package/dist/commands/workspace/context-status.js +59 -0
  23. package/dist/commands/workspace/open-target-selection.d.ts +13 -0
  24. package/dist/commands/workspace/open-target-selection.js +146 -0
  25. package/dist/commands/workspace/open-view.d.ts +62 -0
  26. package/dist/commands/workspace/open-view.js +249 -0
  27. package/dist/commands/workspace/open.d.ts +16 -8
  28. package/dist/commands/workspace/open.js +40 -14
  29. package/dist/commands/workspace/opener-selection.d.ts +11 -0
  30. package/dist/commands/workspace/opener-selection.js +98 -0
  31. package/dist/commands/workspace/operations.d.ts +14 -8
  32. package/dist/commands/workspace/operations.js +228 -160
  33. package/dist/commands/workspace/prompt-theme.d.ts +29 -0
  34. package/dist/commands/workspace/prompt-theme.js +24 -0
  35. package/dist/commands/workspace/registration.d.ts +13 -0
  36. package/dist/commands/workspace/registration.js +84 -0
  37. package/dist/commands/workspace/selection.d.ts +3 -0
  38. package/dist/commands/workspace/selection.js +42 -40
  39. package/dist/commands/workspace/setup-prompts.d.ts +13 -0
  40. package/dist/commands/workspace/setup-prompts.js +121 -0
  41. package/dist/commands/workspace/types.d.ts +15 -0
  42. package/dist/commands/workspace.js +59 -340
  43. package/dist/core/artifact-graph/index.d.ts +2 -1
  44. package/dist/core/artifact-graph/instruction-loader.d.ts +10 -23
  45. package/dist/core/artifact-graph/instruction-loader.js +28 -89
  46. package/dist/core/artifact-graph/types.d.ts +0 -7
  47. package/dist/core/artifact-graph/types.js +0 -19
  48. package/dist/core/change-metadata/index.d.ts +2 -0
  49. package/dist/core/change-metadata/index.js +2 -0
  50. package/dist/core/change-metadata/schema.d.ts +18 -0
  51. package/dist/core/change-metadata/schema.js +28 -0
  52. package/dist/core/change-status-policy.d.ts +50 -0
  53. package/dist/core/change-status-policy.js +70 -0
  54. package/dist/core/collections/index.d.ts +3 -0
  55. package/dist/core/collections/index.js +3 -0
  56. package/dist/core/collections/initiatives/collection.d.ts +4 -0
  57. package/dist/core/collections/initiatives/collection.js +17 -0
  58. package/dist/core/collections/initiatives/index.d.ts +6 -0
  59. package/dist/core/collections/initiatives/index.js +6 -0
  60. package/dist/core/collections/initiatives/operations.d.ts +49 -0
  61. package/dist/core/collections/initiatives/operations.js +175 -0
  62. package/dist/core/collections/initiatives/resolution.d.ts +87 -0
  63. package/dist/core/collections/initiatives/resolution.js +374 -0
  64. package/dist/core/collections/initiatives/schema.d.ts +41 -0
  65. package/dist/core/collections/initiatives/schema.js +134 -0
  66. package/dist/core/collections/initiatives/templates.d.ts +12 -0
  67. package/dist/core/collections/initiatives/templates.js +90 -0
  68. package/dist/core/collections/runtime.d.ts +46 -0
  69. package/dist/core/collections/runtime.js +194 -0
  70. package/dist/core/completions/command-registry.d.ts +1 -5
  71. package/dist/core/completions/command-registry.js +475 -70
  72. package/dist/core/completions/shared-flags.d.ts +12 -0
  73. package/dist/core/completions/shared-flags.js +28 -0
  74. package/dist/core/config.js +2 -1
  75. package/dist/core/context-store/binding.d.ts +53 -0
  76. package/dist/core/context-store/binding.js +197 -0
  77. package/dist/core/context-store/errors.d.ts +20 -0
  78. package/dist/core/context-store/errors.js +22 -0
  79. package/dist/core/context-store/foundation.d.ts +55 -0
  80. package/dist/core/context-store/foundation.js +321 -0
  81. package/dist/core/context-store/index.d.ts +6 -0
  82. package/dist/core/context-store/index.js +6 -0
  83. package/dist/core/context-store/operations.d.ts +85 -0
  84. package/dist/core/context-store/operations.js +528 -0
  85. package/dist/core/context-store/registry.d.ts +45 -0
  86. package/dist/core/context-store/registry.js +229 -0
  87. package/dist/core/index.d.ts +2 -0
  88. package/dist/core/index.js +2 -0
  89. package/dist/core/jira/templates.js +14 -5
  90. package/dist/core/planning-home.js +5 -21
  91. package/dist/core/validation/validator.d.ts +11 -0
  92. package/dist/core/validation/validator.js +19 -2
  93. package/dist/core/workspace/foundation.d.ts +28 -48
  94. package/dist/core/workspace/foundation.js +130 -214
  95. package/dist/core/workspace/index.d.ts +2 -0
  96. package/dist/core/workspace/index.js +2 -0
  97. package/dist/core/workspace/legacy-state.d.ts +28 -0
  98. package/dist/core/workspace/legacy-state.js +200 -0
  99. package/dist/core/workspace/open-surface.d.ts +29 -8
  100. package/dist/core/workspace/open-surface.js +122 -44
  101. package/dist/core/workspace/openers.js +11 -6
  102. package/dist/core/workspace/registry.d.ts +24 -0
  103. package/dist/core/workspace/registry.js +146 -0
  104. package/dist/core/workspace/skills.d.ts +4 -2
  105. package/dist/core/workspace/skills.js +2 -2
  106. package/dist/core/workspace/state-io.d.ts +10 -0
  107. package/dist/core/workspace/state-io.js +119 -0
  108. package/dist/utils/change-metadata.d.ts +5 -2
  109. package/dist/utils/change-metadata.js +6 -12
  110. package/dist/utils/change-utils.d.ts +2 -2
  111. package/package.json +1 -1
@@ -0,0 +1,229 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import { getContextStoreMetadataPath, getContextStoreMetadataDir, listContextStoreRegistryEntries, readContextStoreRegistryState, readOptionalContextStoreMetadataState, resolveGitContextStoreBackendConfig, updateContextStoreRegistryState, validateContextStoreId, writeContextStoreMetadataState, } from './foundation.js';
3
+ import { ContextStoreError } from './errors.js';
4
+ import { FileSystemUtils } from '../../utils/file-system.js';
5
+ export function getStoreRootForBackend(backend) {
6
+ switch (backend.type) {
7
+ case 'git':
8
+ return backend.local_path;
9
+ }
10
+ }
11
+ function normalizePathForComparison(targetPath) {
12
+ try {
13
+ return FileSystemUtils.canonicalizeExistingPath(targetPath);
14
+ }
15
+ catch {
16
+ return targetPath;
17
+ }
18
+ }
19
+ export function assertNoRegisteredStoreConflict(registry, id, backend) {
20
+ const nextPath = normalizePathForComparison(getStoreRootForBackend(backend));
21
+ for (const entry of listContextStoreRegistryEntries(registry ?? { version: 1, stores: {} })) {
22
+ const entryPath = normalizePathForComparison(getStoreRootForBackend(entry.backend));
23
+ if (entry.id === id && entryPath === nextPath) {
24
+ continue;
25
+ }
26
+ if (entry.id === id) {
27
+ throw new ContextStoreError(`Context store '${id}' is already registered at ${getStoreRootForBackend(entry.backend)}.`, 'context_store_id_conflict', {
28
+ target: 'context_store.id',
29
+ fix: 'Use the existing registration or choose a different context store id.',
30
+ });
31
+ }
32
+ if (entryPath === nextPath) {
33
+ throw new ContextStoreError(`Context store path is already registered as '${entry.id}'.`, 'context_store_path_conflict', {
34
+ target: 'context_store.root',
35
+ fix: `Use the existing '${entry.id}' registration or choose a different path.`,
36
+ });
37
+ }
38
+ }
39
+ }
40
+ function withRegisteredStore(registry, id, backend) {
41
+ assertNoRegisteredStoreConflict(registry, id, backend);
42
+ const stores = {
43
+ ...(registry?.stores ?? {}),
44
+ [id]: {
45
+ backend,
46
+ },
47
+ };
48
+ return {
49
+ version: 1,
50
+ stores: Object.fromEntries(Object.entries(stores).sort(([leftId], [rightId]) => leftId.localeCompare(rightId))),
51
+ };
52
+ }
53
+ function getRegisteredStoreOrThrow(registry, id) {
54
+ const entry = registry?.stores[id];
55
+ if (!entry) {
56
+ throw new ContextStoreError(`Unknown context store '${id}'`, 'context_store_not_found', {
57
+ target: 'context_store.id',
58
+ fix: 'Run openspec context-store list to see registered stores.',
59
+ });
60
+ }
61
+ return {
62
+ id,
63
+ backend: entry.backend,
64
+ };
65
+ }
66
+ function contextStoreBackendsMatch(actual, expected) {
67
+ return (actual.type === expected.type &&
68
+ normalizePathForComparison(actual.local_path) ===
69
+ normalizePathForComparison(expected.local_path) &&
70
+ actual.remote === expected.remote &&
71
+ actual.branch === expected.branch);
72
+ }
73
+ function assertExpectedRegisteredBackend(id, actual, expected) {
74
+ if (!expected || contextStoreBackendsMatch(actual, expected))
75
+ return;
76
+ throw new ContextStoreError(`Context store '${id}' changed before cleanup completed.`, 'context_store_registry_changed', {
77
+ target: 'context_store.registry',
78
+ fix: 'Retry the cleanup command after reviewing the current context-store registration.',
79
+ });
80
+ }
81
+ function withoutRegisteredStore(registry, id, expectedBackend) {
82
+ const removed = getRegisteredStoreOrThrow(registry, id);
83
+ assertExpectedRegisteredBackend(id, removed.backend, expectedBackend);
84
+ const stores = { ...(registry?.stores ?? {}) };
85
+ delete stores[id];
86
+ return {
87
+ removed,
88
+ next: {
89
+ version: 1,
90
+ stores: Object.fromEntries(Object.entries(stores).sort(([leftId], [rightId]) => leftId.localeCompare(rightId))),
91
+ },
92
+ };
93
+ }
94
+ async function ensureStoreMetadata(storeRoot, id, options) {
95
+ const metadata = await readOptionalContextStoreMetadataState(storeRoot);
96
+ if (!metadata) {
97
+ if (!options.writeIfMissing) {
98
+ throw new ContextStoreError(`Registered context store '${id}' is missing metadata at ${getContextStoreMetadataPath(storeRoot)}`, 'context_store_metadata_missing', {
99
+ target: 'context_store.metadata',
100
+ fix: `Create ${getContextStoreMetadataPath(storeRoot)} or rerun context-store register.`,
101
+ });
102
+ }
103
+ await writeContextStoreMetadataState(storeRoot, {
104
+ version: 1,
105
+ id,
106
+ });
107
+ return true;
108
+ }
109
+ if (metadata.id !== id) {
110
+ throw new ContextStoreError(`Context store metadata id '${metadata.id}' does not match registered id '${id}'`, 'context_store_metadata_id_mismatch', {
111
+ target: 'context_store.metadata',
112
+ fix: 'Repair the local registry or store metadata so the ids match.',
113
+ });
114
+ }
115
+ return false;
116
+ }
117
+ export async function commitContextStoreRegistration(input) {
118
+ const id = validateContextStoreId(input.id);
119
+ const backend = input.backend;
120
+ const storeRoot = getStoreRootForBackend(backend);
121
+ let metadataCreated = false;
122
+ try {
123
+ metadataCreated = await ensureStoreMetadata(storeRoot, id, {
124
+ writeIfMissing: input.writeMetadataIfMissing,
125
+ });
126
+ await updateContextStoreRegistryState((registry) => withRegisteredStore(registry, id, backend), { globalDataDir: input.globalDataDir });
127
+ }
128
+ catch (error) {
129
+ if (metadataCreated) {
130
+ await fs.rm(getContextStoreMetadataPath(storeRoot), { force: true });
131
+ await fs.rmdir(getContextStoreMetadataDir(storeRoot)).catch(() => undefined);
132
+ }
133
+ throw error;
134
+ }
135
+ return {
136
+ id,
137
+ storeRoot,
138
+ backend,
139
+ metadataCreated,
140
+ };
141
+ }
142
+ export async function registerContextStore(input) {
143
+ const id = validateContextStoreId(input.id);
144
+ const backend = await resolveGitContextStoreBackendConfig({
145
+ localPath: input.localPath,
146
+ ...(input.remote !== undefined ? { remote: input.remote } : {}),
147
+ ...(input.branch !== undefined ? { branch: input.branch } : {}),
148
+ }, input.cwd);
149
+ const storeRoot = getStoreRootForBackend(backend);
150
+ const committed = await commitContextStoreRegistration({
151
+ id,
152
+ backend,
153
+ writeMetadataIfMissing: true,
154
+ ...(input.globalDataDir ? { globalDataDir: input.globalDataDir } : {}),
155
+ });
156
+ return {
157
+ id: committed.id,
158
+ storeRoot: committed.storeRoot,
159
+ backend: committed.backend,
160
+ };
161
+ }
162
+ export async function listRegisteredContextStores(options = {}) {
163
+ const registry = await readContextStoreRegistryState(options);
164
+ if (!registry) {
165
+ return [];
166
+ }
167
+ return listContextStoreRegistryEntries(registry).map((entry) => ({
168
+ ...entry,
169
+ storeRoot: getStoreRootForBackend(entry.backend),
170
+ }));
171
+ }
172
+ export async function getRegisteredContextStore(input) {
173
+ const id = validateContextStoreId(input.id);
174
+ const registry = await readContextStoreRegistryState({
175
+ globalDataDir: input.globalDataDir,
176
+ });
177
+ const entry = getRegisteredStoreOrThrow(registry, id);
178
+ assertExpectedRegisteredBackend(id, entry.backend, input.expectedBackend);
179
+ return {
180
+ ...entry,
181
+ storeRoot: getStoreRootForBackend(entry.backend),
182
+ };
183
+ }
184
+ export async function unregisterContextStoreRegistration(input) {
185
+ const id = validateContextStoreId(input.id);
186
+ let removed;
187
+ await updateContextStoreRegistryState(async (registry) => {
188
+ const result = withoutRegisteredStore(registry, id, input.expectedBackend);
189
+ const removedEntry = {
190
+ ...result.removed,
191
+ storeRoot: getStoreRootForBackend(result.removed.backend),
192
+ };
193
+ await input.beforeCommit?.(removedEntry);
194
+ removed = result.removed;
195
+ return result.next;
196
+ }, { globalDataDir: input.globalDataDir });
197
+ if (!removed) {
198
+ throw new ContextStoreError(`Unknown context store '${id}'`, 'context_store_not_found', {
199
+ target: 'context_store.id',
200
+ fix: 'Run openspec context-store list to see registered stores.',
201
+ });
202
+ }
203
+ return {
204
+ ...removed,
205
+ storeRoot: getStoreRootForBackend(removed.backend),
206
+ };
207
+ }
208
+ export async function resolveRegisteredContextStore(input) {
209
+ const id = validateContextStoreId(input.id);
210
+ const registry = await readContextStoreRegistryState({
211
+ globalDataDir: input.globalDataDir,
212
+ });
213
+ if (!registry) {
214
+ throw new ContextStoreError('No context store registry found', 'no_context_store_registry', {
215
+ target: 'context_store.id',
216
+ fix: 'Register a context store before using --store, or pass --store-path <path>.',
217
+ });
218
+ }
219
+ const entry = getRegisteredStoreOrThrow(registry, id);
220
+ const backend = entry.backend;
221
+ const storeRoot = getStoreRootForBackend(backend);
222
+ await ensureStoreMetadata(storeRoot, id, { writeIfMissing: false });
223
+ return {
224
+ id,
225
+ storeRoot,
226
+ backend,
227
+ };
228
+ }
229
+ //# sourceMappingURL=registry.js.map
@@ -1,5 +1,7 @@
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
2
  export * from './workspace/index.js';
3
3
  export * from './jira/index.js';
4
+ export * from './context-store/index.js';
5
+ export * from './collections/index.js';
4
6
  export * from './planning-home.js';
5
7
  //# sourceMappingURL=index.d.ts.map
@@ -2,5 +2,7 @@
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
3
  export * from './workspace/index.js';
4
4
  export * from './jira/index.js';
5
+ export * from './context-store/index.js';
6
+ export * from './collections/index.js';
5
7
  export * from './planning-home.js';
6
8
  //# sourceMappingURL=index.js.map
@@ -155,7 +155,11 @@ ${SHARED_GUARDRAILS}
155
155
  ## Writeback Rules
156
156
 
157
157
  - This is the only Jira intake workflow allowed to write to Jira.
158
- - Before any Jira comment, child issue creation, link update, or status change, show a concrete diff of the write.
158
+ - Child issue writeback is the default strategy for reviewed stories.
159
+ - For every non-deferred story missing \`stories.<storyId>.jiraIssueKey\` in \`mapping.yaml\`, the writeback plan MUST include a \`createJiraIssue\` operation.
160
+ - Do not create duplicate Jira child issues for stories that already have \`jiraIssueKey\`; plan only needed link, update, or status changes for those stories.
161
+ - Summary comment sync is optional. Do not include a Jira comment by default unless the user explicitly requests one, or there are no child issues to create.
162
+ - Before any child issue creation, link update, status change, field edit, or optional comment, show a concrete diff of the write.
159
163
  - Ask for explicit confirmation before each batch of writes.
160
164
  - If \`openspec/jira.yaml\` has \`writeback.enabled: false\`, ask the user to confirm this one-time sync before writing.
161
165
  - Use only the official Atlassian MCP Jira write tools needed for the confirmed diff: \`addCommentToJiraIssue\`, \`editJiraIssue\`, \`getTransitionsForJiraIssue\`, \`transitionJiraIssue\`, and \`createJiraIssue\`.
@@ -164,10 +168,15 @@ ${SHARED_GUARDRAILS}
164
168
 
165
169
  1. Run \`openspec jira validate <JIRA-ID> --stage propose --json\` when syncing proposed OpenSpec changes.
166
170
  2. Read \`mapping.yaml\`, \`stories.md\`, and current OpenSpec change status.
167
- 3. Prepare a writeback plan: comments, links, child issues, or status moves.
168
- 4. Show the diff and wait for explicit user approval.
169
- 5. Use the named Atlassian MCP write tools only after approval.
170
- 6. Update \`intake.yaml\` status to \`synced\` only after successful writeback.
171
+ 3. Identify all non-deferred reviewed stories and split them into:
172
+ - stories missing \`jiraIssueKey\`, which must be planned as Jira child issues;
173
+ - stories with \`jiraIssueKey\`, which must not be recreated.
174
+ 4. Prepare a child-issue-first writeback plan. For each \`createJiraIssue\`, show the Jira summary, issue type, parent/source issue, description or acceptance criteria, trace links, and corresponding OpenSpec change.
175
+ 5. Include an optional summary comment only when the user requested comment sync, or when there are no child issues to create.
176
+ 6. Show the full diff and wait for explicit user approval.
177
+ 7. Use the named Atlassian MCP write tools only after approval.
178
+ 8. After each successful \`createJiraIssue\`, update the matching \`stories.<storyId>.jiraIssueKey\` in \`mapping.yaml\`.
179
+ 9. Update \`intake.yaml\` status to \`synced\` only after successful writeback and local mapping updates.
171
180
 
172
181
  ## Output
173
182
 
@@ -1,6 +1,6 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
- import { getWorkspaceChangesDir, getWorkspaceSharedStatePath, parseWorkspaceSharedState, } from './workspace/index.js';
3
+ import { getWorkspaceChangesDir, readWorkspaceViewStateSync, workspaceStateFileExistsSync, } from './workspace/index.js';
4
4
  import { FileSystemUtils } from '../utils/file-system.js';
5
5
  const REPO_DEFAULT_SCHEMA = 'spec-driven';
6
6
  const WORKSPACE_DEFAULT_SCHEMA = 'workspace-planning';
@@ -12,14 +12,6 @@ function pathExistsAsDirectory(candidatePath) {
12
12
  return false;
13
13
  }
14
14
  }
15
- function pathExistsAsFile(candidatePath) {
16
- try {
17
- return fs.statSync(candidatePath).isFile();
18
- }
19
- catch {
20
- return false;
21
- }
22
- }
23
15
  function getSearchStartDirectory(startPath) {
24
16
  const resolved = path.resolve(startPath);
25
17
  try {
@@ -45,7 +37,7 @@ function findNearestAncestor(startPath, predicate) {
45
37
  }
46
38
  }
47
39
  export function findWorkspacePlanningRootSync(startPath = process.cwd()) {
48
- return findNearestAncestor(startPath, (dirPath) => pathExistsAsFile(getWorkspaceSharedStatePath(dirPath)));
40
+ return findNearestAncestor(startPath, workspaceStateFileExistsSync);
49
41
  }
50
42
  export function findRepoPlanningRootSync(startPath = process.cwd()) {
51
43
  return findNearestAncestor(startPath, (dirPath) => pathExistsAsDirectory(path.join(dirPath, 'openspec')));
@@ -66,24 +58,16 @@ function relativePlanningPath(fromPath, toPath) {
66
58
  }
67
59
  return path.posix.relative(fromPath.replace(/\\/g, '/'), toPath.replace(/\\/g, '/'));
68
60
  }
69
- function readWorkspaceSharedStateSync(workspaceRoot) {
70
- try {
71
- return parseWorkspaceSharedState(fs.readFileSync(getWorkspaceSharedStatePath(workspaceRoot), 'utf-8'));
72
- }
73
- catch {
74
- return null;
75
- }
76
- }
77
61
  function workspacePlanningHome(workspaceRoot) {
78
- const sharedState = readWorkspaceSharedStateSync(workspaceRoot);
62
+ const viewState = readWorkspaceViewStateSync(workspaceRoot);
79
63
  return {
80
64
  kind: 'workspace',
81
65
  root: workspaceRoot,
82
66
  changesDir: getWorkspaceChangesDir(workspaceRoot),
83
67
  defaultSchema: WORKSPACE_DEFAULT_SCHEMA,
84
68
  workspace: {
85
- name: sharedState?.name ?? path.basename(workspaceRoot),
86
- links: Object.keys(sharedState?.links ?? {}).sort((a, b) => a.localeCompare(b)),
69
+ name: viewState?.name ?? path.basename(workspaceRoot),
70
+ links: Object.keys(viewState?.links ?? {}).sort((a, b) => a.localeCompare(b)),
87
71
  },
88
72
  };
89
73
  }
@@ -27,6 +27,17 @@ export declare class Validator {
27
27
  isValid(report: ValidationReport): boolean;
28
28
  private extractRequirementText;
29
29
  private containsShallOrMust;
30
+ /**
31
+ * Build an error message for a requirement block whose body lacks SHALL/MUST.
32
+ *
33
+ * When the SHALL/MUST keyword already appears in the requirement header (e.g.
34
+ * `### Requirement: The system SHALL ...`) the original generic error
35
+ * ("must contain SHALL or MUST") is confusing because the keyword is visibly
36
+ * present in the spec. Per the OpenSpec conventions the keyword has to live
37
+ * on the requirement body line (the line right after the header), so we point
38
+ * the author at that exact fix when the keyword is found in the header only.
39
+ */
40
+ private buildMissingShallOrMustMessage;
30
41
  private countScenarios;
31
42
  private formatSectionList;
32
43
  }
@@ -150,7 +150,7 @@ export class Validator {
150
150
  issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" is missing requirement text` });
151
151
  }
152
152
  else if (!this.containsShallOrMust(requirementText)) {
153
- issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" must contain SHALL or MUST` });
153
+ issues.push({ level: 'ERROR', path: entryPath, message: this.buildMissingShallOrMustMessage('ADDED', block.name) });
154
154
  }
155
155
  const scenarioCount = this.countScenarios(block.raw);
156
156
  if (scenarioCount < 1) {
@@ -172,7 +172,7 @@ export class Validator {
172
172
  issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" is missing requirement text` });
173
173
  }
174
174
  else if (!this.containsShallOrMust(requirementText)) {
175
- issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" must contain SHALL or MUST` });
175
+ issues.push({ level: 'ERROR', path: entryPath, message: this.buildMissingShallOrMustMessage('MODIFIED', block.name) });
176
176
  }
177
177
  const scenarioCount = this.countScenarios(block.raw);
178
178
  if (scenarioCount < 1) {
@@ -401,6 +401,23 @@ export class Validator {
401
401
  containsShallOrMust(text) {
402
402
  return /\b(SHALL|MUST)\b/.test(text);
403
403
  }
404
+ /**
405
+ * Build an error message for a requirement block whose body lacks SHALL/MUST.
406
+ *
407
+ * When the SHALL/MUST keyword already appears in the requirement header (e.g.
408
+ * `### Requirement: The system SHALL ...`) the original generic error
409
+ * ("must contain SHALL or MUST") is confusing because the keyword is visibly
410
+ * present in the spec. Per the OpenSpec conventions the keyword has to live
411
+ * on the requirement body line (the line right after the header), so we point
412
+ * the author at that exact fix when the keyword is found in the header only.
413
+ */
414
+ buildMissingShallOrMustMessage(action, blockName) {
415
+ const base = `${action} "${blockName}" must contain SHALL or MUST`;
416
+ if (this.containsShallOrMust(blockName)) {
417
+ return `${base} in the requirement body, not only in the header. Move the SHALL/MUST statement to the line immediately after the "### Requirement: ..." header.`;
418
+ }
419
+ return base;
420
+ }
404
421
  countScenarios(blockRaw) {
405
422
  const matches = blockRaw.match(/^####\s+/gm);
406
423
  return matches ? matches.length : 0;
@@ -1,13 +1,10 @@
1
+ import { type ContextStoreBinding, type ContextStoreSelector } from '../context-store/index.js';
1
2
  export declare const WORKSPACE_METADATA_DIR_NAME = ".openspec-workspace";
2
- export declare const WORKSPACE_SHARED_STATE_FILE_NAME = "workspace.yaml";
3
- export declare const WORKSPACE_LOCAL_STATE_FILE_NAME = "local.yaml";
3
+ export declare const WORKSPACE_VIEW_STATE_FILE_NAME = "workspace.yaml";
4
4
  export declare const WORKSPACE_CHANGES_DIR_NAME = "changes";
5
- export declare const MANAGED_WORKSPACES_DIR_NAME = "workspaces";
6
- export declare const WORKSPACE_REGISTRY_FILE_NAME = "registry.yaml";
7
- export declare const WORKSPACE_LOCAL_STATE_IGNORE_PATTERN = ".openspec-workspace/local.yaml";
8
5
  export declare const WORKSPACE_CODE_WORKSPACE_EXTENSION = ".code-workspace";
9
- export declare const WORKSPACE_SUPPORTED_OPENER_VALUES: readonly ["codex", "claude", "github-copilot", "editor"];
10
- export declare const WORKSPACE_AGENT_OPENER_IDS: readonly ["codex", "claude", "github-copilot"];
6
+ export declare const WORKSPACE_SUPPORTED_OPENER_VALUES: readonly ["codex-cli", "claude", "github-copilot", "editor"];
7
+ export declare const WORKSPACE_AGENT_OPENER_IDS: readonly ["codex-cli", "claude", "github-copilot"];
11
8
  export declare const WORKSPACE_EDITOR_OPENER_IDS: readonly ["vscode"];
12
9
  export type WorkspaceSupportedOpenerValue = typeof WORKSPACE_SUPPORTED_OPENER_VALUES[number];
13
10
  export type WorkspaceAgentOpenerId = typeof WORKSPACE_AGENT_OPENER_IDS[number];
@@ -19,16 +16,20 @@ export type WorkspacePreferredOpener = {
19
16
  kind: 'editor';
20
17
  id: WorkspaceEditorOpenerId;
21
18
  };
22
- export interface WorkspaceSharedState {
23
- version: 1;
24
- name: string;
25
- links: Record<string, WorkspaceLinkState>;
19
+ export interface WorkspaceContextState {
20
+ kind: 'initiative';
21
+ store: ContextStoreBinding;
22
+ initiative: {
23
+ id: string;
24
+ };
26
25
  }
27
- export type WorkspaceLinkState = Record<string, unknown>;
28
- export interface WorkspaceLocalState {
26
+ export interface WorkspaceViewState {
29
27
  version: 1;
30
- paths: Record<string, string>;
28
+ name: string;
29
+ context: WorkspaceContextState | null;
30
+ links: Record<string, string | null>;
31
31
  preferred_opener?: WorkspacePreferredOpener;
32
+ tools?: string[];
32
33
  workspace_skills?: WorkspaceSkillState;
33
34
  }
34
35
  export interface WorkspaceSkillState {
@@ -38,50 +39,29 @@ export interface WorkspaceSkillState {
38
39
  last_applied_workflow_ids?: string[];
39
40
  last_applied_at?: string;
40
41
  }
41
- export interface WorkspaceRegistryState {
42
- version: 1;
43
- workspaces: Record<string, string>;
44
- }
45
- export interface WorkspaceRegistryEntry {
46
- name: string;
47
- workspaceRoot: string;
48
- }
49
- export interface WorkspacePathOptions {
50
- globalDataDir?: string;
51
- }
52
42
  export declare function getWorkspaceMetadataDir(workspaceRoot: string): string;
53
- export declare function getWorkspaceSharedStatePath(workspaceRoot: string): string;
54
- export declare function getWorkspaceLocalStatePath(workspaceRoot: string): string;
43
+ export declare function getWorkspaceViewStatePath(workspaceRoot: string): string;
55
44
  export declare function getWorkspaceChangesDir(workspaceRoot: string): string;
56
- export declare function getManagedWorkspacesDir(options?: WorkspacePathOptions): string;
57
- export declare function getManagedWorkspaceRoot(workspaceName: string, options?: WorkspacePathOptions): string;
58
- export declare function getWorkspaceRegistryPath(options?: WorkspacePathOptions): string;
59
45
  export declare function getWorkspaceCodeWorkspaceFileName(workspaceName: string): string;
60
46
  export declare function getWorkspaceCodeWorkspacePath(workspaceRoot: string, workspaceName: string): string;
61
- export declare function getWorkspacePortableIgnorePatterns(workspaceName?: string): string[];
47
+ /**
48
+ * @deprecated Managed workspaces no longer create portable ignore rules.
49
+ * This compatibility shim remains for callers that still ask which ignore
50
+ * patterns OpenSpec owns for workspace-local generated files.
51
+ */
52
+ export declare function getWorkspacePortableIgnorePatterns(_workspaceName?: string): string[];
62
53
  export declare function validateWorkspaceName(name: string): string;
63
54
  export declare function validateWorkspaceLinkName(name: string): string;
64
55
  export declare function isValidWorkspaceName(name: string): boolean;
65
56
  export declare function isValidWorkspaceLinkName(name: string): boolean;
66
- export declare function isWorkspaceRoot(candidateRoot: string): Promise<boolean>;
67
- export declare function findWorkspaceRoot(startPath?: string): Promise<string | null>;
68
57
  export declare function isWorkspaceAgentOpenerId(value: string): value is WorkspaceAgentOpenerId;
69
58
  export declare function isWorkspaceSupportedOpenerValue(value: string): value is WorkspaceSupportedOpenerValue;
70
59
  export declare function parseWorkspacePreferredOpenerValue(value: string): WorkspacePreferredOpener;
71
60
  export declare function validateWorkspacePreferredOpener(opener: WorkspacePreferredOpener): WorkspacePreferredOpener;
72
- export declare function parseWorkspaceSharedState(content: string): WorkspaceSharedState;
73
- export declare function parseWorkspaceLocalState(content: string): WorkspaceLocalState;
74
- export declare function parseWorkspaceRegistryState(content: string): WorkspaceRegistryState;
75
- export declare function serializeWorkspaceSharedState(state: WorkspaceSharedState): string;
76
- export declare function serializeWorkspaceLocalState(state: WorkspaceLocalState): string;
77
- export declare function serializeWorkspaceRegistryState(state: WorkspaceRegistryState): string;
78
- export declare function listWorkspaceRegistryEntries(registry: WorkspaceRegistryState): WorkspaceRegistryEntry[];
79
- export declare function readWorkspaceSharedState(workspaceRoot: string): Promise<WorkspaceSharedState>;
80
- export declare function readWorkspaceLocalState(workspaceRoot: string): Promise<WorkspaceLocalState>;
81
- export declare function readOptionalWorkspaceLocalState(workspaceRoot: string): Promise<WorkspaceLocalState | null>;
82
- export declare function writeWorkspaceSharedState(workspaceRoot: string, state: WorkspaceSharedState): Promise<void>;
83
- export declare function writeWorkspaceLocalState(workspaceRoot: string, state: WorkspaceLocalState): Promise<void>;
84
- export declare function readWorkspaceRegistryState(options?: WorkspacePathOptions): Promise<WorkspaceRegistryState | null>;
85
- export declare function writeWorkspaceRegistryState(state: WorkspaceRegistryState, options?: WorkspacePathOptions): Promise<void>;
86
- export declare function workspaceChangesDirExists(workspaceRoot: string): Promise<boolean>;
61
+ export declare function createWorkspaceInitiativeContext(store: ContextStoreBinding, initiativeId: string): WorkspaceContextState;
62
+ export declare function getWorkspaceContextStoreId(context: WorkspaceContextState): string;
63
+ export declare function getWorkspaceContextStoreSelector(context: WorkspaceContextState): ContextStoreSelector;
64
+ export declare function getWorkspaceContextInitiativeId(context: WorkspaceContextState): string;
65
+ export declare function parseWorkspaceViewState(content: string): WorkspaceViewState;
66
+ export declare function serializeWorkspaceViewState(state: WorkspaceViewState): string;
87
67
  //# sourceMappingURL=foundation.d.ts.map