gmc-openspec 1.1.0 → 1.4.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 (110) 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/planning-home.js +5 -21
  90. package/dist/core/validation/validator.d.ts +11 -0
  91. package/dist/core/validation/validator.js +19 -2
  92. package/dist/core/workspace/foundation.d.ts +28 -48
  93. package/dist/core/workspace/foundation.js +130 -214
  94. package/dist/core/workspace/index.d.ts +2 -0
  95. package/dist/core/workspace/index.js +2 -0
  96. package/dist/core/workspace/legacy-state.d.ts +28 -0
  97. package/dist/core/workspace/legacy-state.js +200 -0
  98. package/dist/core/workspace/open-surface.d.ts +29 -8
  99. package/dist/core/workspace/open-surface.js +122 -44
  100. package/dist/core/workspace/openers.js +11 -6
  101. package/dist/core/workspace/registry.d.ts +24 -0
  102. package/dist/core/workspace/registry.js +146 -0
  103. package/dist/core/workspace/skills.d.ts +4 -2
  104. package/dist/core/workspace/skills.js +2 -2
  105. package/dist/core/workspace/state-io.d.ts +10 -0
  106. package/dist/core/workspace/state-io.js +119 -0
  107. package/dist/utils/change-metadata.d.ts +5 -2
  108. package/dist/utils/change-metadata.js +6 -12
  109. package/dist/utils/change-utils.d.ts +2 -2
  110. package/package.json +1 -1
@@ -6,6 +6,7 @@ import { detectCompleted } from './state.js';
6
6
  import { resolveArtifactOutputs } from './outputs.js';
7
7
  import { readChangeMetadata, resolveSchemaForChange } from '../../utils/change-metadata.js';
8
8
  import { FileSystemUtils } from '../../utils/file-system.js';
9
+ import { buildActionContext, buildNextSteps, summarizeAffectedAreas, summarizePlanningHome, } from '../change-status-policy.js';
9
10
  import { readProjectConfig, validateConfigRules } from '../project-config.js';
10
11
  // Session-level cache for validation warnings (avoid repeating same warnings)
11
12
  const shownWarnings = new Set();
@@ -62,8 +63,10 @@ export function loadTemplate(schemaName, templatePath, projectRoot) {
62
63
  */
63
64
  export function loadChangeContext(projectRoot, changeName, schemaName, options = {}) {
64
65
  const changeDir = FileSystemUtils.canonicalizeExistingPath(options.changeDir ?? path.join(projectRoot, 'openspec', 'changes', changeName));
65
- // Resolve schema: explicit > metadata > default
66
- const resolvedSchemaName = resolveSchemaForChange(changeDir, schemaName, projectRoot);
66
+ const metadata = readChangeMetadata(changeDir, projectRoot) ?? undefined;
67
+ const resolvedSchemaName = resolveSchemaForChange(changeDir, schemaName, projectRoot, {
68
+ metadata: metadata ?? null,
69
+ });
67
70
  const schema = resolveSchema(resolvedSchemaName, projectRoot);
68
71
  const graph = ArtifactGraph.fromSchema(schema);
69
72
  const completed = detectCompleted(graph, changeDir);
@@ -75,6 +78,8 @@ export function loadChangeContext(projectRoot, changeName, schemaName, options =
75
78
  changeDir,
76
79
  projectRoot,
77
80
  ...(options.planningHome ? { planningHome: options.planningHome } : {}),
81
+ ...(metadata ? { metadata } : {}),
82
+ ...(metadata?.initiative ? { initiative: metadata.initiative } : {}),
78
83
  };
79
84
  }
80
85
  /**
@@ -133,6 +138,7 @@ export function generateInstructions(context, artifactId, projectRoot) {
133
138
  schemaName: context.schemaName,
134
139
  changeDir: context.changeDir,
135
140
  planningHome: summarizePlanningHome(context.planningHome),
141
+ ...(context.initiative ? { initiative: context.initiative } : {}),
136
142
  outputPath: artifact.generates,
137
143
  resolvedOutputPath: path.join(context.changeDir, artifact.generates),
138
144
  existingOutputPaths: resolveArtifactOutputs(context.changeDir, artifact.generates),
@@ -171,89 +177,6 @@ function getUnlockedArtifacts(graph, artifactId) {
171
177
  }
172
178
  return unlocks.sort();
173
179
  }
174
- function summarizePlanningHome(planningHome) {
175
- if (!planningHome) {
176
- return undefined;
177
- }
178
- return {
179
- kind: planningHome.kind,
180
- root: planningHome.root,
181
- changesDir: planningHome.changesDir,
182
- defaultSchema: planningHome.defaultSchema,
183
- ...(planningHome.workspace ? { workspaceName: planningHome.workspace.name } : {}),
184
- };
185
- }
186
- function getWorkspaceSpecAreaSegments(context) {
187
- if (context.planningHome?.kind !== 'workspace') {
188
- return [];
189
- }
190
- const specArtifact = context.graph.getArtifact('specs');
191
- if (!specArtifact) {
192
- return [];
193
- }
194
- return resolveArtifactOutputs(context.changeDir, specArtifact.generates)
195
- .map((outputPath) => path.relative(path.join(context.changeDir, 'specs'), outputPath))
196
- .filter((relativePath) => relativePath.length > 0 && !relativePath.startsWith('..'))
197
- .map((relativePath) => relativePath.split(path.sep)[0])
198
- .filter((areaName) => areaName.length > 0);
199
- }
200
- function getAffectedAreasSummary(context) {
201
- if (context.planningHome?.kind !== 'workspace') {
202
- return undefined;
203
- }
204
- const metadata = readChangeMetadata(context.changeDir, context.projectRoot);
205
- const known = Array.from(new Set([...(metadata?.affected_areas ?? []), ...getWorkspaceSpecAreaSegments(context)])).sort((a, b) => a.localeCompare(b));
206
- const validAreas = new Set(context.planningHome.workspace?.links ?? []);
207
- const invalid = known.filter((areaName) => validAreas.size > 0 && !validAreas.has(areaName));
208
- return {
209
- known,
210
- unresolved: known.length === 0,
211
- invalid,
212
- };
213
- }
214
- function buildActionContext(context, artifactIds) {
215
- if (context.planningHome?.kind === 'workspace') {
216
- return {
217
- mode: 'workspace-planning',
218
- sourceOfTruth: 'workspace',
219
- planningArtifacts: artifactIds,
220
- linkedContext: (context.planningHome.workspace?.links ?? []).map((name) => ({ name })),
221
- allowedEditRoots: [],
222
- requiresAffectedAreaSelection: true,
223
- constraints: [
224
- 'Use workspace-level planning artifacts as the source of truth.',
225
- 'Treat linked repos and folders as exploration context until an affected area is selected.',
226
- 'Do not make implementation edits without an explicit allowed edit root.',
227
- ],
228
- };
229
- }
230
- return {
231
- mode: 'repo-local',
232
- sourceOfTruth: 'repo',
233
- planningArtifacts: artifactIds,
234
- linkedContext: [],
235
- allowedEditRoots: [context.projectRoot],
236
- requiresAffectedAreaSelection: false,
237
- constraints: ['Repo-local change artifacts and implementation edits are scoped to this project.'],
238
- };
239
- }
240
- function buildNextSteps(context, artifactStatuses, affectedAreas) {
241
- const readyArtifact = artifactStatuses.find((artifact) => artifact.status === 'ready');
242
- const steps = [];
243
- if (readyArtifact) {
244
- steps.push(`Run openspec instructions ${readyArtifact.id} --change "${context.changeName}" --json before writing that artifact.`);
245
- }
246
- else if (context.graph.isComplete(context.completed)) {
247
- steps.push('All planning artifacts are complete; review tasks before implementation.');
248
- }
249
- if (context.planningHome?.kind === 'workspace') {
250
- if (affectedAreas?.unresolved) {
251
- steps.push('Identify affected areas in workspace specs or coordination tasks as planning continues.');
252
- }
253
- steps.push('Select an affected area and allowed edit root before implementation edits.');
254
- }
255
- return steps;
256
- }
257
180
  /**
258
181
  * Formats the status of all artifacts in a change.
259
182
  *
@@ -299,18 +222,34 @@ export function formatChangeStatus(context) {
299
222
  const buildOrder = context.graph.getBuildOrder();
300
223
  const orderMap = new Map(buildOrder.map((id, idx) => [id, idx]));
301
224
  artifactStatuses.sort((a, b) => (orderMap.get(a.id) ?? 0) - (orderMap.get(b.id) ?? 0));
302
- const affectedAreas = getAffectedAreasSummary(context);
225
+ const affectedAreas = summarizeAffectedAreas({
226
+ planningHome: context.planningHome,
227
+ metadata: context.metadata,
228
+ });
229
+ const isComplete = context.graph.isComplete(context.completed);
230
+ const artifactIds = artifactStatuses.map((artifact) => artifact.id);
303
231
  return {
304
232
  changeName: context.changeName,
305
233
  schemaName: context.schemaName,
306
234
  planningHome: summarizePlanningHome(context.planningHome),
235
+ ...(context.initiative ? { initiative: context.initiative } : {}),
307
236
  changeRoot: context.changeDir,
308
237
  artifactPaths,
309
238
  affectedAreas,
310
- isComplete: context.graph.isComplete(context.completed),
239
+ isComplete,
311
240
  applyRequires,
312
- nextSteps: buildNextSteps(context, artifactStatuses, affectedAreas),
313
- actionContext: buildActionContext(context, artifactStatuses.map((artifact) => artifact.id)),
241
+ nextSteps: buildNextSteps({
242
+ changeName: context.changeName,
243
+ planningHome: context.planningHome,
244
+ artifactStatuses,
245
+ affectedAreas,
246
+ allArtifactsComplete: isComplete,
247
+ }),
248
+ actionContext: buildActionContext({
249
+ planningHome: context.planningHome,
250
+ projectRoot: context.projectRoot,
251
+ artifactIds,
252
+ }),
314
253
  artifacts: artifactStatuses,
315
254
  };
316
255
  }
@@ -33,13 +33,6 @@ export declare const SchemaYamlSchema: z.ZodObject<{
33
33
  export type Artifact = z.infer<typeof ArtifactSchema>;
34
34
  export type ApplyPhase = z.infer<typeof ApplyPhaseSchema>;
35
35
  export type SchemaYaml = z.infer<typeof SchemaYamlSchema>;
36
- export declare const ChangeMetadataSchema: z.ZodObject<{
37
- schema: z.ZodString;
38
- created: z.ZodOptional<z.ZodString>;
39
- goal: z.ZodOptional<z.ZodString>;
40
- affected_areas: z.ZodOptional<z.ZodArray<z.ZodString>>;
41
- }, z.core.$strip>;
42
- export type ChangeMetadata = z.infer<typeof ChangeMetadataSchema>;
43
36
  export type CompletedSet = Set<string>;
44
37
  export interface BlockedArtifacts {
45
38
  [artifactId: string]: string[];
@@ -26,23 +26,4 @@ export const SchemaYamlSchema = z.object({
26
26
  // Optional apply phase configuration (for schema-aware apply instructions)
27
27
  apply: ApplyPhaseSchema.optional(),
28
28
  });
29
- // Per-change metadata schema
30
- // Note: schema field is validated at parse time against available schemas
31
- // using a lazy import to avoid circular dependencies
32
- export const ChangeMetadataSchema = z.object({
33
- // Required: which workflow schema this change uses
34
- schema: z.string().min(1, { message: 'schema is required' }),
35
- // Optional: creation timestamp (ISO date string)
36
- created: z
37
- .string()
38
- .regex(/^\d{4}-\d{2}-\d{2}$/, {
39
- message: 'created must be YYYY-MM-DD format',
40
- })
41
- .optional(),
42
- // Optional workspace planning metadata. These fields are intentionally
43
- // lightweight and do not replace the normal proposal/specs/design/tasks
44
- // artifacts as the source of planning detail.
45
- goal: z.string().min(1).optional(),
46
- affected_areas: z.array(z.string().min(1)).optional(),
47
- });
48
29
  //# sourceMappingURL=types.js.map
@@ -0,0 +1,2 @@
1
+ export * from './schema.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ export * from './schema.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+ export declare const InitiativeLinkSchema: z.ZodObject<{
3
+ store: z.ZodString;
4
+ id: z.ZodString;
5
+ }, z.core.$strict>;
6
+ export type InitiativeLink = z.infer<typeof InitiativeLinkSchema>;
7
+ export declare const ChangeMetadataSchema: z.ZodObject<{
8
+ schema: z.ZodString;
9
+ created: z.ZodOptional<z.ZodString>;
10
+ goal: z.ZodOptional<z.ZodString>;
11
+ affected_areas: z.ZodOptional<z.ZodArray<z.ZodString>>;
12
+ initiative: z.ZodOptional<z.ZodObject<{
13
+ store: z.ZodString;
14
+ id: z.ZodString;
15
+ }, z.core.$strict>>;
16
+ }, z.core.$strip>;
17
+ export type ChangeMetadata = z.infer<typeof ChangeMetadataSchema>;
18
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ const KebabIdentifierSchema = (label) => z.string().superRefine((value, ctx) => {
3
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(value)) {
4
+ ctx.addIssue({
5
+ code: 'custom',
6
+ message: `${label} must be kebab-case with lowercase letters, numbers, and single hyphen separators`,
7
+ });
8
+ }
9
+ });
10
+ export const InitiativeLinkSchema = z.object({
11
+ store: KebabIdentifierSchema('Context store id'),
12
+ id: KebabIdentifierSchema('Initiative id'),
13
+ }).strict();
14
+ // Per-change metadata schema. The schema field is validated against available
15
+ // workflow schemas when metadata is read or written.
16
+ export const ChangeMetadataSchema = z.object({
17
+ schema: z.string().min(1, { message: 'schema is required' }),
18
+ created: z
19
+ .string()
20
+ .regex(/^\d{4}-\d{2}-\d{2}$/, {
21
+ message: 'created must be YYYY-MM-DD format',
22
+ })
23
+ .optional(),
24
+ goal: z.string().min(1).optional(),
25
+ affected_areas: z.array(z.string().min(1)).optional(),
26
+ initiative: InitiativeLinkSchema.optional(),
27
+ });
28
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1,50 @@
1
+ import type { ChangeMetadata } from './change-metadata/index.js';
2
+ import type { PlanningHome } from './planning-home.js';
3
+ export interface PlanningHomeSummary {
4
+ kind: 'repo' | 'workspace';
5
+ root: string;
6
+ changesDir: string;
7
+ defaultSchema: string;
8
+ workspaceName?: string;
9
+ }
10
+ export interface AffectedAreasSummary {
11
+ known: string[];
12
+ unresolved: boolean;
13
+ invalid: string[];
14
+ }
15
+ export interface ActionContext {
16
+ mode: 'repo-local' | 'workspace-planning';
17
+ sourceOfTruth: 'repo' | 'workspace-local';
18
+ planningArtifacts: string[];
19
+ linkedContext: Array<{
20
+ name: string;
21
+ }>;
22
+ allowedEditRoots: string[];
23
+ requiresAffectedAreaSelection: boolean;
24
+ constraints: string[];
25
+ }
26
+ export interface ChangeStatusPolicyArtifact {
27
+ id: string;
28
+ status: 'done' | 'ready' | 'blocked';
29
+ }
30
+ export interface AffectedAreasInput {
31
+ planningHome?: PlanningHome;
32
+ metadata?: ChangeMetadata;
33
+ }
34
+ export interface ChangeNextStepsInput {
35
+ changeName: string;
36
+ planningHome?: PlanningHome;
37
+ artifactStatuses: ChangeStatusPolicyArtifact[];
38
+ affectedAreas?: AffectedAreasSummary;
39
+ allArtifactsComplete: boolean;
40
+ }
41
+ export interface ActionContextInput {
42
+ planningHome?: PlanningHome;
43
+ projectRoot: string;
44
+ artifactIds: string[];
45
+ }
46
+ export declare function summarizePlanningHome(planningHome: PlanningHome | undefined): PlanningHomeSummary | undefined;
47
+ export declare function summarizeAffectedAreas(input: AffectedAreasInput): AffectedAreasSummary | undefined;
48
+ export declare function buildActionContext(input: ActionContextInput): ActionContext;
49
+ export declare function buildNextSteps(input: ChangeNextStepsInput): string[];
50
+ //# sourceMappingURL=change-status-policy.d.ts.map
@@ -0,0 +1,70 @@
1
+ export function summarizePlanningHome(planningHome) {
2
+ if (!planningHome) {
3
+ return undefined;
4
+ }
5
+ return {
6
+ kind: planningHome.kind,
7
+ root: planningHome.root,
8
+ changesDir: planningHome.changesDir,
9
+ defaultSchema: planningHome.defaultSchema,
10
+ ...(planningHome.workspace ? { workspaceName: planningHome.workspace.name } : {}),
11
+ };
12
+ }
13
+ export function summarizeAffectedAreas(input) {
14
+ if (input.planningHome?.kind !== 'workspace') {
15
+ return undefined;
16
+ }
17
+ const known = Array.from(new Set(input.metadata?.affected_areas ?? [])).sort((a, b) => a.localeCompare(b));
18
+ const validAreas = new Set(input.planningHome.workspace?.links ?? []);
19
+ const invalid = known.filter((areaName) => validAreas.size > 0 && !validAreas.has(areaName));
20
+ return {
21
+ known,
22
+ unresolved: known.length === 0,
23
+ invalid,
24
+ };
25
+ }
26
+ export function buildActionContext(input) {
27
+ if (input.planningHome?.kind === 'workspace') {
28
+ return {
29
+ mode: 'workspace-planning',
30
+ sourceOfTruth: 'workspace-local',
31
+ planningArtifacts: input.artifactIds,
32
+ linkedContext: (input.planningHome.workspace?.links ?? []).map((name) => ({ name })),
33
+ allowedEditRoots: [],
34
+ requiresAffectedAreaSelection: true,
35
+ constraints: [
36
+ 'Treat workspace-local planning artifacts as compatibility context for this local view.',
37
+ 'Use initiatives for durable coordination when initiative context exists.',
38
+ 'Treat linked repos and folders as context until an explicit edit root is selected.',
39
+ 'Do not make implementation edits without an explicit allowed edit root.',
40
+ ],
41
+ };
42
+ }
43
+ return {
44
+ mode: 'repo-local',
45
+ sourceOfTruth: 'repo',
46
+ planningArtifacts: input.artifactIds,
47
+ linkedContext: [],
48
+ allowedEditRoots: [input.projectRoot],
49
+ requiresAffectedAreaSelection: false,
50
+ constraints: ['Repo-local change artifacts and implementation edits are scoped to this project.'],
51
+ };
52
+ }
53
+ export function buildNextSteps(input) {
54
+ const readyArtifact = input.artifactStatuses.find((artifact) => artifact.status === 'ready');
55
+ const steps = [];
56
+ if (readyArtifact) {
57
+ steps.push(`Run openspec instructions ${readyArtifact.id} --change "${input.changeName}" --json before writing that artifact.`);
58
+ }
59
+ else if (input.allArtifactsComplete) {
60
+ steps.push('All planning artifacts are complete; review tasks before implementation.');
61
+ }
62
+ if (input.planningHome?.kind === 'workspace') {
63
+ if (input.affectedAreas?.unresolved) {
64
+ steps.push('Identify affected areas in change metadata or coordination tasks as planning continues.');
65
+ }
66
+ steps.push('Select an affected area and allowed edit root before implementation edits.');
67
+ }
68
+ return steps;
69
+ }
70
+ //# sourceMappingURL=change-status-policy.js.map
@@ -0,0 +1,3 @@
1
+ export * from './runtime.js';
2
+ export * from './initiatives/index.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export * from './runtime.js';
2
+ export * from './initiatives/index.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,4 @@
1
+ import { type CollectionRegistry, type MountedCollection } from '../runtime.js';
2
+ export declare function createInitiativesCollectionRegistry(): CollectionRegistry;
3
+ export declare function mountInitiativesCollection(storeRoot: string): MountedCollection;
4
+ //# sourceMappingURL=collection.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { createCollectionRegistry, mountCollections, } from '../runtime.js';
2
+ import { INITIATIVE_COLLECTION_ID } from './schema.js';
3
+ export function createInitiativesCollectionRegistry() {
4
+ return createCollectionRegistry([
5
+ {
6
+ id: INITIATIVE_COLLECTION_ID,
7
+ mount: INITIATIVE_COLLECTION_ID,
8
+ },
9
+ ]);
10
+ }
11
+ export function mountInitiativesCollection(storeRoot) {
12
+ return mountCollections({
13
+ storeRoot,
14
+ collections: createInitiativesCollectionRegistry(),
15
+ }).require(INITIATIVE_COLLECTION_ID);
16
+ }
17
+ //# sourceMappingURL=collection.js.map
@@ -0,0 +1,6 @@
1
+ export * from './collection.js';
2
+ export * from './schema.js';
3
+ export * from './templates.js';
4
+ export * from './operations.js';
5
+ export * from './resolution.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,6 @@
1
+ export * from './collection.js';
2
+ export * from './schema.js';
3
+ export * from './templates.js';
4
+ export * from './operations.js';
5
+ export * from './resolution.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,49 @@
1
+ import * as nodeFs from 'node:fs';
2
+ import type { MountedCollection } from '../runtime.js';
3
+ import { type InitiativeMetadata, type InitiativeState, type InitiativeStatus } from './schema.js';
4
+ import { type InitiativeTemplateFile } from './templates.js';
5
+ export interface InitiativeDirectoryEntry {
6
+ name: string;
7
+ isDirectory(): boolean;
8
+ }
9
+ export interface InitiativeOperationsFileSystem {
10
+ mkdir(dirPath: string, options: {
11
+ recursive?: boolean;
12
+ }): Promise<void>;
13
+ writeFile(filePath: string, content: string, options: {
14
+ flag?: nodeFs.OpenMode;
15
+ }): Promise<void>;
16
+ readFile(filePath: string): Promise<string>;
17
+ readdir(dirPath: string, options: {
18
+ withFileTypes: true;
19
+ }): Promise<readonly InitiativeDirectoryEntry[]>;
20
+ rm(dirPath: string, options: {
21
+ recursive?: boolean;
22
+ force?: boolean;
23
+ }): Promise<void>;
24
+ }
25
+ export interface InitiativeOperationDependencies {
26
+ fileSystem?: InitiativeOperationsFileSystem;
27
+ }
28
+ export interface CreateInitiativeInput extends InitiativeOperationDependencies {
29
+ collection: MountedCollection;
30
+ id: string;
31
+ title: string;
32
+ summary: string;
33
+ status?: InitiativeStatus;
34
+ owners?: string[];
35
+ metadata?: InitiativeMetadata;
36
+ getCurrentDate?: () => string;
37
+ buildTemplateFiles?: (state: InitiativeState) => readonly InitiativeTemplateFile[];
38
+ }
39
+ export interface ListInitiativesInput extends InitiativeOperationDependencies {
40
+ collection: MountedCollection;
41
+ }
42
+ export interface ReadInitiativeInput extends InitiativeOperationDependencies {
43
+ collection: MountedCollection;
44
+ id: string;
45
+ }
46
+ export declare function createInitiative(input: CreateInitiativeInput): Promise<InitiativeState>;
47
+ export declare function readInitiative(input: ReadInitiativeInput): Promise<InitiativeState | null>;
48
+ export declare function listInitiatives(input: ListInitiativesInput): Promise<InitiativeState[]>;
49
+ //# sourceMappingURL=operations.d.ts.map
@@ -0,0 +1,175 @@
1
+ import * as nodeFs from 'node:fs';
2
+ import { INITIATIVE_COLLECTION_ID, INITIATIVE_FILE_NAME, parseInitiativeState, serializeInitiativeState, validateInitiativeId, } from './schema.js';
3
+ import { buildDefaultInitiativeFiles, } from './templates.js';
4
+ const fs = nodeFs.promises;
5
+ const nodeFileSystem = {
6
+ async mkdir(dirPath, options) {
7
+ await fs.mkdir(dirPath, options);
8
+ },
9
+ async writeFile(filePath, content, options) {
10
+ await fs.writeFile(filePath, content, {
11
+ encoding: 'utf-8',
12
+ flag: options.flag ?? 'w',
13
+ });
14
+ },
15
+ async readFile(filePath) {
16
+ return fs.readFile(filePath, 'utf-8');
17
+ },
18
+ async readdir(dirPath, options) {
19
+ return fs.readdir(dirPath, options);
20
+ },
21
+ async rm(dirPath, options) {
22
+ await fs.rm(dirPath, options);
23
+ },
24
+ };
25
+ function getCurrentDate() {
26
+ return new Date().toISOString().split('T')[0];
27
+ }
28
+ function getFileSystem(fileSystem) {
29
+ return fileSystem ?? nodeFileSystem;
30
+ }
31
+ function isFileNotFoundError(error) {
32
+ return (typeof error === 'object' &&
33
+ error !== null &&
34
+ 'code' in error &&
35
+ error.code === 'ENOENT');
36
+ }
37
+ function isPathExistsError(error) {
38
+ return (typeof error === 'object' &&
39
+ error !== null &&
40
+ 'code' in error &&
41
+ error.code === 'EEXIST');
42
+ }
43
+ function errorMessage(error) {
44
+ return error instanceof Error ? error.message : String(error);
45
+ }
46
+ function assertInitiativesCollection(collection) {
47
+ if (collection.collectionId !== INITIATIVE_COLLECTION_ID) {
48
+ throw new Error(`Expected mounted '${INITIATIVE_COLLECTION_ID}' collection, got '${collection.collectionId}'`);
49
+ }
50
+ }
51
+ function resolveInitiativeFilePath(collection, initiativeId, fileName) {
52
+ return collection.resolvePath(`${initiativeId}/${fileName}`);
53
+ }
54
+ function normalizeCreateState(input) {
55
+ return parseInitiativeState(serializeInitiativeState({
56
+ version: 1,
57
+ id: validateInitiativeId(input.id),
58
+ title: input.title,
59
+ summary: input.summary,
60
+ status: input.status ?? 'exploring',
61
+ created: (input.getCurrentDate ?? getCurrentDate)(),
62
+ owners: input.owners ?? [],
63
+ metadata: input.metadata ?? {},
64
+ }));
65
+ }
66
+ async function writeExclusiveFile(fileSystem, filePath, content) {
67
+ await fileSystem.writeFile(filePath, content, { flag: 'wx' });
68
+ }
69
+ async function cleanupCreatedInitiative(fileSystem, initiativeRoot, originalError, initiativeId) {
70
+ try {
71
+ await fileSystem.rm(initiativeRoot, { recursive: true, force: true });
72
+ }
73
+ catch (cleanupError) {
74
+ throw new Error(`Failed to create initiative '${initiativeId}' and cleanup failed: ${errorMessage(originalError)}; cleanup: ${errorMessage(cleanupError)}`);
75
+ }
76
+ throw new Error(`Failed to create initiative '${initiativeId}': ${errorMessage(originalError)}`);
77
+ }
78
+ export async function createInitiative(input) {
79
+ assertInitiativesCollection(input.collection);
80
+ const state = normalizeCreateState(input);
81
+ const fileSystem = getFileSystem(input.fileSystem);
82
+ const initiativeRoot = input.collection.resolvePath(state.id);
83
+ const buildTemplateFiles = input.buildTemplateFiles ?? buildDefaultInitiativeFiles;
84
+ try {
85
+ await fileSystem.mkdir(input.collection.resolvePath(), { recursive: true });
86
+ await fileSystem.mkdir(initiativeRoot, { recursive: false });
87
+ }
88
+ catch (error) {
89
+ if (isPathExistsError(error)) {
90
+ throw new Error(`Initiative '${state.id}' already exists at ${initiativeRoot}`);
91
+ }
92
+ throw new Error(`Failed to create initiative '${state.id}': ${errorMessage(error)}`);
93
+ }
94
+ try {
95
+ await writeExclusiveFile(fileSystem, resolveInitiativeFilePath(input.collection, state.id, INITIATIVE_FILE_NAME), serializeInitiativeState(state));
96
+ for (const templateFile of buildTemplateFiles(state)) {
97
+ await writeExclusiveFile(fileSystem, resolveInitiativeFilePath(input.collection, state.id, templateFile.fileName), templateFile.content);
98
+ }
99
+ }
100
+ catch (error) {
101
+ await cleanupCreatedInitiative(fileSystem, initiativeRoot, error, state.id);
102
+ }
103
+ return state;
104
+ }
105
+ export async function readInitiative(input) {
106
+ assertInitiativesCollection(input.collection);
107
+ const initiativeId = validateInitiativeId(input.id);
108
+ const fileSystem = getFileSystem(input.fileSystem);
109
+ const initiativeFilePath = resolveInitiativeFilePath(input.collection, initiativeId, INITIATIVE_FILE_NAME);
110
+ let content;
111
+ try {
112
+ content = await fileSystem.readFile(initiativeFilePath);
113
+ }
114
+ catch (error) {
115
+ if (isFileNotFoundError(error)) {
116
+ return null;
117
+ }
118
+ throw new Error(`Invalid initiative '${initiativeId}': failed to read ${INITIATIVE_FILE_NAME}: ${errorMessage(error)}`);
119
+ }
120
+ let state;
121
+ try {
122
+ state = parseInitiativeState(content);
123
+ }
124
+ catch (error) {
125
+ throw new Error(`Invalid initiative '${initiativeId}': ${errorMessage(error)}`);
126
+ }
127
+ if (state.id !== initiativeId) {
128
+ throw new Error(`Invalid initiative '${initiativeId}': ${INITIATIVE_FILE_NAME} id '${state.id}' must match folder name`);
129
+ }
130
+ return state;
131
+ }
132
+ export async function listInitiatives(input) {
133
+ assertInitiativesCollection(input.collection);
134
+ const fileSystem = getFileSystem(input.fileSystem);
135
+ let entries;
136
+ try {
137
+ entries = await fileSystem.readdir(input.collection.resolvePath(), { withFileTypes: true });
138
+ }
139
+ catch (error) {
140
+ if (isFileNotFoundError(error)) {
141
+ return [];
142
+ }
143
+ throw new Error(`Failed to list initiatives: ${errorMessage(error)}`);
144
+ }
145
+ const initiatives = [];
146
+ for (const entry of entries) {
147
+ if (!entry.isDirectory()) {
148
+ continue;
149
+ }
150
+ const initiativeFilePath = resolveInitiativeFilePath(input.collection, entry.name, INITIATIVE_FILE_NAME);
151
+ let content;
152
+ try {
153
+ content = await fileSystem.readFile(initiativeFilePath);
154
+ }
155
+ catch (error) {
156
+ if (isFileNotFoundError(error)) {
157
+ continue;
158
+ }
159
+ throw new Error(`Invalid initiative '${entry.name}': failed to read ${INITIATIVE_FILE_NAME}: ${errorMessage(error)}`);
160
+ }
161
+ let state;
162
+ try {
163
+ state = parseInitiativeState(content);
164
+ }
165
+ catch (error) {
166
+ throw new Error(`Invalid initiative '${entry.name}': ${errorMessage(error)}`);
167
+ }
168
+ if (state.id !== entry.name) {
169
+ throw new Error(`Invalid initiative '${entry.name}': ${INITIATIVE_FILE_NAME} id '${state.id}' must match folder name`);
170
+ }
171
+ initiatives.push(state);
172
+ }
173
+ return initiatives.sort((a, b) => a.id.localeCompare(b.id));
174
+ }
175
+ //# sourceMappingURL=operations.js.map