gmc-openspec 1.0.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.
- package/README.md +2 -2
- package/bin/openspec.js +3 -1
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.js +36 -2
- package/dist/commands/config.js +4 -4
- package/dist/commands/context-store.d.ts +3 -0
- package/dist/commands/context-store.js +475 -0
- package/dist/commands/initiative.d.ts +13 -0
- package/dist/commands/initiative.js +318 -0
- package/dist/commands/workflow/index.d.ts +2 -0
- package/dist/commands/workflow/index.js +1 -0
- package/dist/commands/workflow/initiative-link.d.ts +24 -0
- package/dist/commands/workflow/initiative-link.js +47 -0
- package/dist/commands/workflow/instructions.js +10 -2
- package/dist/commands/workflow/new-change.d.ts +4 -0
- package/dist/commands/workflow/new-change.js +72 -23
- package/dist/commands/workflow/set-change.d.ts +13 -0
- package/dist/commands/workflow/set-change.js +87 -0
- package/dist/commands/workflow/shared.d.ts +2 -0
- package/dist/commands/workflow/status.js +3 -0
- package/dist/commands/workspace/context-status.d.ts +4 -0
- package/dist/commands/workspace/context-status.js +59 -0
- package/dist/commands/workspace/open-target-selection.d.ts +13 -0
- package/dist/commands/workspace/open-target-selection.js +146 -0
- package/dist/commands/workspace/open-view.d.ts +62 -0
- package/dist/commands/workspace/open-view.js +249 -0
- package/dist/commands/workspace/open.d.ts +16 -8
- package/dist/commands/workspace/open.js +40 -14
- package/dist/commands/workspace/opener-selection.d.ts +11 -0
- package/dist/commands/workspace/opener-selection.js +98 -0
- package/dist/commands/workspace/operations.d.ts +14 -8
- package/dist/commands/workspace/operations.js +228 -160
- package/dist/commands/workspace/prompt-theme.d.ts +29 -0
- package/dist/commands/workspace/prompt-theme.js +24 -0
- package/dist/commands/workspace/registration.d.ts +13 -0
- package/dist/commands/workspace/registration.js +84 -0
- package/dist/commands/workspace/selection.d.ts +3 -0
- package/dist/commands/workspace/selection.js +42 -40
- package/dist/commands/workspace/setup-prompts.d.ts +13 -0
- package/dist/commands/workspace/setup-prompts.js +121 -0
- package/dist/commands/workspace/types.d.ts +15 -0
- package/dist/commands/workspace.js +59 -340
- package/dist/core/artifact-graph/index.d.ts +2 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +10 -23
- package/dist/core/artifact-graph/instruction-loader.js +28 -89
- package/dist/core/artifact-graph/types.d.ts +0 -7
- package/dist/core/artifact-graph/types.js +0 -19
- package/dist/core/change-metadata/index.d.ts +2 -0
- package/dist/core/change-metadata/index.js +2 -0
- package/dist/core/change-metadata/schema.d.ts +18 -0
- package/dist/core/change-metadata/schema.js +28 -0
- package/dist/core/change-status-policy.d.ts +50 -0
- package/dist/core/change-status-policy.js +70 -0
- package/dist/core/collections/index.d.ts +3 -0
- package/dist/core/collections/index.js +3 -0
- package/dist/core/collections/initiatives/collection.d.ts +4 -0
- package/dist/core/collections/initiatives/collection.js +17 -0
- package/dist/core/collections/initiatives/index.d.ts +6 -0
- package/dist/core/collections/initiatives/index.js +6 -0
- package/dist/core/collections/initiatives/operations.d.ts +49 -0
- package/dist/core/collections/initiatives/operations.js +175 -0
- package/dist/core/collections/initiatives/resolution.d.ts +87 -0
- package/dist/core/collections/initiatives/resolution.js +374 -0
- package/dist/core/collections/initiatives/schema.d.ts +41 -0
- package/dist/core/collections/initiatives/schema.js +134 -0
- package/dist/core/collections/initiatives/templates.d.ts +12 -0
- package/dist/core/collections/initiatives/templates.js +90 -0
- package/dist/core/collections/runtime.d.ts +46 -0
- package/dist/core/collections/runtime.js +194 -0
- package/dist/core/completions/command-registry.d.ts +1 -5
- package/dist/core/completions/command-registry.js +475 -70
- package/dist/core/completions/shared-flags.d.ts +12 -0
- package/dist/core/completions/shared-flags.js +28 -0
- package/dist/core/config.js +2 -1
- package/dist/core/context-store/binding.d.ts +53 -0
- package/dist/core/context-store/binding.js +197 -0
- package/dist/core/context-store/errors.d.ts +20 -0
- package/dist/core/context-store/errors.js +22 -0
- package/dist/core/context-store/foundation.d.ts +55 -0
- package/dist/core/context-store/foundation.js +321 -0
- package/dist/core/context-store/index.d.ts +6 -0
- package/dist/core/context-store/index.js +6 -0
- package/dist/core/context-store/operations.d.ts +85 -0
- package/dist/core/context-store/operations.js +528 -0
- package/dist/core/context-store/registry.d.ts +45 -0
- package/dist/core/context-store/registry.js +229 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/planning-home.js +5 -21
- package/dist/core/validation/validator.d.ts +11 -0
- package/dist/core/validation/validator.js +19 -2
- package/dist/core/workspace/foundation.d.ts +28 -48
- package/dist/core/workspace/foundation.js +130 -214
- package/dist/core/workspace/index.d.ts +2 -0
- package/dist/core/workspace/index.js +2 -0
- package/dist/core/workspace/legacy-state.d.ts +28 -0
- package/dist/core/workspace/legacy-state.js +200 -0
- package/dist/core/workspace/open-surface.d.ts +29 -8
- package/dist/core/workspace/open-surface.js +122 -44
- package/dist/core/workspace/openers.js +11 -6
- package/dist/core/workspace/registry.d.ts +24 -0
- package/dist/core/workspace/registry.js +146 -0
- package/dist/core/workspace/skills.d.ts +4 -2
- package/dist/core/workspace/skills.js +2 -2
- package/dist/core/workspace/state-io.d.ts +10 -0
- package/dist/core/workspace/state-io.js +119 -0
- package/dist/utils/change-metadata.d.ts +5 -2
- package/dist/utils/change-metadata.js +6 -12
- package/dist/utils/change-utils.d.ts +2 -2
- package/package.json +17 -19
|
@@ -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
|
-
|
|
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 =
|
|
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
|
|
239
|
+
isComplete,
|
|
311
240
|
applyRequires,
|
|
312
|
-
nextSteps: buildNextSteps(
|
|
313
|
-
|
|
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,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,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,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
|