@fission-ai/openspec 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -3
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +94 -76
- package/dist/commands/change.d.ts +5 -1
- package/dist/commands/change.js +17 -15
- package/dist/commands/config.js +6 -65
- package/dist/commands/context.d.ts +3 -0
- package/dist/commands/context.js +155 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.js +163 -0
- package/dist/commands/shared-gather.d.ts +14 -0
- package/dist/commands/shared-gather.js +31 -0
- package/dist/commands/shared-output.d.ts +18 -0
- package/dist/commands/shared-output.js +61 -0
- package/dist/commands/show.d.ts +11 -6
- package/dist/commands/show.js +70 -25
- package/dist/commands/spec.d.ts +5 -1
- package/dist/commands/spec.js +15 -4
- package/dist/commands/store.d.ts +3 -0
- package/dist/commands/store.js +547 -0
- package/dist/commands/validate.d.ts +2 -0
- package/dist/commands/validate.js +75 -39
- package/dist/commands/workflow/index.d.ts +0 -2
- package/dist/commands/workflow/index.js +0 -1
- package/dist/commands/workflow/instructions.d.ts +11 -1
- package/dist/commands/workflow/instructions.js +71 -22
- package/dist/commands/workflow/new-change.d.ts +6 -3
- package/dist/commands/workflow/new-change.js +42 -77
- package/dist/commands/workflow/shared.d.ts +15 -3
- package/dist/commands/workflow/shared.js +20 -3
- package/dist/commands/workflow/status.d.ts +2 -0
- package/dist/commands/workflow/status.js +19 -17
- package/dist/commands/workset-input.d.ts +19 -0
- package/dist/commands/workset-input.js +112 -0
- package/dist/commands/workset-prompts.d.ts +12 -0
- package/dist/commands/workset-prompts.js +143 -0
- package/dist/commands/workset.d.ts +25 -0
- package/dist/commands/workset.js +446 -0
- package/dist/core/archive.d.ts +17 -6
- package/dist/core/archive.js +209 -56
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +18 -13
- package/dist/core/artifact-graph/instruction-loader.js +8 -31
- package/dist/core/change-metadata/schema.d.ts +1 -0
- package/dist/core/change-metadata/schema.js +4 -2
- package/dist/core/change-status-policy.d.ts +5 -18
- package/dist/core/change-status-policy.js +2 -37
- package/dist/core/command-generation/adapters/bob.js +1 -14
- package/dist/core/command-generation/adapters/claude.js +1 -14
- package/dist/core/command-generation/adapters/cursor.js +1 -14
- package/dist/core/command-generation/adapters/pi.js +1 -14
- package/dist/core/command-generation/adapters/windsurf.js +1 -14
- package/dist/core/command-generation/yaml.d.ts +22 -0
- package/dist/core/command-generation/yaml.js +38 -0
- package/dist/core/completions/command-registry.js +87 -289
- package/dist/core/completions/generators/zsh-generator.js +3 -1
- package/dist/core/completions/installers/bash-installer.js +3 -0
- package/dist/core/completions/installers/fish-installer.js +8 -0
- package/dist/core/completions/installers/powershell-installer.js +30 -2
- package/dist/core/completions/installers/zsh-installer.js +3 -0
- package/dist/core/completions/shared-flags.d.ts +1 -0
- package/dist/core/completions/shared-flags.js +5 -0
- package/dist/core/config-schema.d.ts +2 -1
- package/dist/core/config-schema.js +26 -0
- package/dist/core/file-state.d.ts +36 -0
- package/dist/core/file-state.js +112 -0
- package/dist/core/global-config.d.ts +2 -0
- package/dist/core/id.d.ts +17 -0
- package/dist/core/id.js +30 -0
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +3 -3
- package/dist/core/init.js +24 -4
- package/dist/core/list.d.ts +2 -0
- package/dist/core/list.js +19 -5
- package/dist/core/openers.d.ts +77 -0
- package/dist/core/openers.js +251 -0
- package/dist/core/openspec-root.d.ts +45 -0
- package/dist/core/openspec-root.js +192 -0
- package/dist/core/planning-home.d.ts +1 -6
- package/dist/core/planning-home.js +2 -43
- package/dist/core/project-config.d.ts +44 -1
- package/dist/core/project-config.js +160 -10
- package/dist/core/references.d.ts +63 -0
- package/dist/core/references.js +310 -0
- package/dist/core/relationship-health.d.ts +65 -0
- package/dist/core/relationship-health.js +64 -0
- package/dist/core/root-selection.d.ts +122 -0
- package/dist/core/root-selection.js +337 -0
- package/dist/core/specs-apply.d.ts +6 -1
- package/dist/core/specs-apply.js +6 -4
- package/dist/core/store/errors.d.ts +20 -0
- package/dist/core/{context-store → store}/errors.js +3 -3
- package/dist/core/store/foundation.d.ts +56 -0
- package/dist/core/store/foundation.js +251 -0
- package/dist/core/store/git.d.ts +23 -0
- package/dist/core/store/git.js +137 -0
- package/dist/core/{context-store → store}/index.d.ts +0 -1
- package/dist/core/{context-store → store}/index.js +0 -1
- package/dist/core/store/operations.d.ts +114 -0
- package/dist/core/store/operations.js +783 -0
- package/dist/core/store/registry.d.ts +58 -0
- package/dist/core/store/registry.js +275 -0
- package/dist/core/templates/workflows/apply-change.js +5 -4
- package/dist/core/templates/workflows/archive-change.js +5 -4
- package/dist/core/templates/workflows/bulk-archive-change.js +5 -4
- package/dist/core/templates/workflows/continue-change.js +7 -2
- package/dist/core/templates/workflows/explore.js +5 -0
- package/dist/core/templates/workflows/ff-change.js +5 -0
- package/dist/core/templates/workflows/new-change.js +5 -0
- package/dist/core/templates/workflows/onboard.js +4 -1
- package/dist/core/templates/workflows/propose.js +5 -0
- package/dist/core/templates/workflows/store-selection.d.ts +8 -0
- package/dist/core/templates/workflows/store-selection.js +8 -0
- package/dist/core/templates/workflows/sync-specs.js +5 -4
- package/dist/core/templates/workflows/verify-change.js +5 -4
- package/dist/core/working-set.d.ts +47 -0
- package/dist/core/working-set.js +43 -0
- package/dist/core/worksets.d.ts +75 -0
- package/dist/core/worksets.js +245 -0
- package/dist/core/zod-issues.d.ts +4 -0
- package/dist/core/zod-issues.js +10 -0
- package/dist/utils/change-metadata.d.ts +1 -0
- package/dist/utils/change-metadata.js +1 -1
- package/dist/utils/change-utils.js +15 -0
- package/dist/utils/file-system.js +36 -17
- package/package.json +6 -6
- package/dist/commands/context-store.d.ts +0 -3
- package/dist/commands/context-store.js +0 -475
- package/dist/commands/initiative.d.ts +0 -13
- package/dist/commands/initiative.js +0 -318
- package/dist/commands/workflow/initiative-link.d.ts +0 -24
- package/dist/commands/workflow/initiative-link.js +0 -47
- package/dist/commands/workflow/set-change.d.ts +0 -13
- package/dist/commands/workflow/set-change.js +0 -87
- package/dist/commands/workspace/context-status.d.ts +0 -4
- package/dist/commands/workspace/context-status.js +0 -59
- package/dist/commands/workspace/open-target-selection.d.ts +0 -13
- package/dist/commands/workspace/open-target-selection.js +0 -146
- package/dist/commands/workspace/open-view.d.ts +0 -62
- package/dist/commands/workspace/open-view.js +0 -249
- package/dist/commands/workspace/open.d.ts +0 -37
- package/dist/commands/workspace/open.js +0 -110
- package/dist/commands/workspace/opener-selection.d.ts +0 -11
- package/dist/commands/workspace/opener-selection.js +0 -98
- package/dist/commands/workspace/operations.d.ts +0 -29
- package/dist/commands/workspace/operations.js +0 -543
- package/dist/commands/workspace/prompt-theme.d.ts +0 -29
- package/dist/commands/workspace/prompt-theme.js +0 -24
- package/dist/commands/workspace/registration.d.ts +0 -13
- package/dist/commands/workspace/registration.js +0 -84
- package/dist/commands/workspace/selection.d.ts +0 -9
- package/dist/commands/workspace/selection.js +0 -115
- package/dist/commands/workspace/setup-prompts.d.ts +0 -13
- package/dist/commands/workspace/setup-prompts.js +0 -121
- package/dist/commands/workspace/types.d.ts +0 -103
- package/dist/commands/workspace/types.js +0 -36
- package/dist/commands/workspace.d.ts +0 -6
- package/dist/commands/workspace.js +0 -587
- package/dist/core/collections/index.d.ts +0 -3
- package/dist/core/collections/index.js +0 -3
- package/dist/core/collections/initiatives/collection.d.ts +0 -4
- package/dist/core/collections/initiatives/collection.js +0 -17
- package/dist/core/collections/initiatives/index.d.ts +0 -6
- package/dist/core/collections/initiatives/index.js +0 -6
- package/dist/core/collections/initiatives/operations.d.ts +0 -49
- package/dist/core/collections/initiatives/operations.js +0 -175
- package/dist/core/collections/initiatives/resolution.d.ts +0 -87
- package/dist/core/collections/initiatives/resolution.js +0 -374
- package/dist/core/collections/initiatives/schema.d.ts +0 -41
- package/dist/core/collections/initiatives/schema.js +0 -134
- package/dist/core/collections/initiatives/templates.d.ts +0 -12
- package/dist/core/collections/initiatives/templates.js +0 -90
- package/dist/core/collections/runtime.d.ts +0 -46
- package/dist/core/collections/runtime.js +0 -194
- package/dist/core/context-store/binding.d.ts +0 -53
- package/dist/core/context-store/binding.js +0 -197
- package/dist/core/context-store/errors.d.ts +0 -20
- package/dist/core/context-store/foundation.d.ts +0 -55
- package/dist/core/context-store/foundation.js +0 -321
- package/dist/core/context-store/operations.d.ts +0 -85
- package/dist/core/context-store/operations.js +0 -528
- package/dist/core/context-store/registry.d.ts +0 -45
- package/dist/core/context-store/registry.js +0 -229
- package/dist/core/workspace/foundation.d.ts +0 -67
- package/dist/core/workspace/foundation.js +0 -295
- package/dist/core/workspace/index.d.ts +0 -8
- package/dist/core/workspace/index.js +0 -8
- package/dist/core/workspace/legacy-state.d.ts +0 -28
- package/dist/core/workspace/legacy-state.js +0 -200
- package/dist/core/workspace/link-input.d.ts +0 -9
- package/dist/core/workspace/link-input.js +0 -32
- package/dist/core/workspace/open-surface.d.ts +0 -45
- package/dist/core/workspace/open-surface.js +0 -215
- package/dist/core/workspace/openers.d.ts +0 -21
- package/dist/core/workspace/openers.js +0 -124
- package/dist/core/workspace/registry.d.ts +0 -24
- package/dist/core/workspace/registry.js +0 -146
- package/dist/core/workspace/skills.d.ts +0 -57
- package/dist/core/workspace/skills.js +0 -334
- package/dist/core/workspace/state-io.d.ts +0 -10
- package/dist/core/workspace/state-io.js +0 -119
- package/schemas/workspace-planning/schema.yaml +0 -72
- package/schemas/workspace-planning/templates/design.md +0 -33
- package/schemas/workspace-planning/templates/proposal.md +0 -28
- package/schemas/workspace-planning/templates/spec.md +0 -9
- package/schemas/workspace-planning/templates/tasks.md +0 -15
package/dist/core/archive.js
CHANGED
|
@@ -3,7 +3,51 @@ import path from 'path';
|
|
|
3
3
|
import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
|
|
4
4
|
import { Validator } from './validation/validator.js';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
+
import { emitStoreRootBanner, isRootSelectionError, resolveOpenSpecRoot, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from './root-selection.js';
|
|
6
7
|
import { findSpecUpdates, buildUpdatedSpec, writeUpdatedSpec, } from './specs-apply.js';
|
|
8
|
+
async function listActiveChangeNames(changesDir) {
|
|
9
|
+
try {
|
|
10
|
+
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
|
11
|
+
return entries
|
|
12
|
+
.filter((entry) => entry.isDirectory() && entry.name !== 'archive')
|
|
13
|
+
.map((entry) => entry.name)
|
|
14
|
+
.sort();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* JSON mode is non-interactive: any point where the human flow would prompt or
|
|
22
|
+
* print prose instead throws this error, which becomes a machine-readable
|
|
23
|
+
* status entry with a non-zero exit code.
|
|
24
|
+
*/
|
|
25
|
+
class ArchiveBlockedError extends Error {
|
|
26
|
+
diagnostic;
|
|
27
|
+
constructor(code, message, fix) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'ArchiveBlockedError';
|
|
30
|
+
this.diagnostic = {
|
|
31
|
+
severity: 'error',
|
|
32
|
+
code,
|
|
33
|
+
message,
|
|
34
|
+
...(fix ? { fix } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function toArchiveDiagnostic(error) {
|
|
39
|
+
if (error instanceof ArchiveBlockedError) {
|
|
40
|
+
return error.diagnostic;
|
|
41
|
+
}
|
|
42
|
+
if (isRootSelectionError(error)) {
|
|
43
|
+
return error.diagnostic;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
severity: 'error',
|
|
47
|
+
code: 'archive_error',
|
|
48
|
+
message: error instanceof Error ? error.message : String(error),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
7
51
|
/**
|
|
8
52
|
* Recursively copy a directory. Used when fs.rename fails (e.g. EPERM on Windows).
|
|
9
53
|
*/
|
|
@@ -44,10 +88,54 @@ async function moveDirectory(src, dest) {
|
|
|
44
88
|
}
|
|
45
89
|
export class ArchiveCommand {
|
|
46
90
|
async execute(changeName, options = {}) {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
91
|
+
const json = !!options.json;
|
|
92
|
+
let root;
|
|
93
|
+
try {
|
|
94
|
+
root = await resolveOpenSpecRoot({
|
|
95
|
+
...(options.store !== undefined ? { store: options.store } : {}),
|
|
96
|
+
...(options.storePath !== undefined ? { storePath: options.storePath } : {}),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (json && isRootSelectionError(error)) {
|
|
101
|
+
this.printJsonFailure(undefined, toArchiveDiagnostic(error));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
if (json) {
|
|
107
|
+
try {
|
|
108
|
+
const result = await this.run(changeName, options, root, true);
|
|
109
|
+
if (!result) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
console.log(JSON.stringify({ archive: result, root: toRootOutput(root) }, null, 2));
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
this.printJsonFailure(root, toArchiveDiagnostic(error));
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
emitStoreRootBanner(root);
|
|
120
|
+
await this.run(changeName, options, root, false);
|
|
121
|
+
}
|
|
122
|
+
printJsonFailure(root, diagnostic) {
|
|
123
|
+
console.log(JSON.stringify({
|
|
124
|
+
archive: null,
|
|
125
|
+
...(root ? { root: toRootOutput(root) } : {}),
|
|
126
|
+
status: [diagnostic],
|
|
127
|
+
}, null, 2));
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Shared archive flow. In human mode (json=false) prompts and prose match
|
|
132
|
+
* the historical behavior and cancellations return null. In JSON mode no
|
|
133
|
+
* prose reaches stdout and every blocked path throws.
|
|
134
|
+
*/
|
|
135
|
+
async run(changeName, options, root, json) {
|
|
136
|
+
const changesDir = root.changesDir;
|
|
137
|
+
const archiveDir = root.archiveDir;
|
|
138
|
+
const mainSpecsDir = root.specsDir;
|
|
51
139
|
// Check if changes directory exists
|
|
52
140
|
try {
|
|
53
141
|
await fs.access(changesDir);
|
|
@@ -57,10 +145,13 @@ export class ArchiveCommand {
|
|
|
57
145
|
}
|
|
58
146
|
// Get change name interactively if not provided
|
|
59
147
|
if (!changeName) {
|
|
148
|
+
if (json) {
|
|
149
|
+
throw new ArchiveBlockedError('archive_change_name_required', 'A change name is required: archive --json is non-interactive.', withStoreFlag(root, 'openspec archive <change-name> --json'));
|
|
150
|
+
}
|
|
60
151
|
const selectedChange = await this.selectChange(changesDir);
|
|
61
152
|
if (!selectedChange) {
|
|
62
153
|
console.log('No change selected. Aborting.');
|
|
63
|
-
return;
|
|
154
|
+
return null;
|
|
64
155
|
}
|
|
65
156
|
changeName = selectedChange;
|
|
66
157
|
}
|
|
@@ -73,29 +164,34 @@ export class ArchiveCommand {
|
|
|
73
164
|
}
|
|
74
165
|
}
|
|
75
166
|
catch {
|
|
76
|
-
|
|
167
|
+
const available = await listActiveChangeNames(changesDir);
|
|
168
|
+
throw new ArchiveBlockedError('archive_change_not_found', available.length > 0
|
|
169
|
+
? `Change '${changeName}' not found. Available changes: ${available.join(', ')}`
|
|
170
|
+
: `Change '${changeName}' not found. No active changes exist in this root.`);
|
|
77
171
|
}
|
|
78
172
|
const skipValidation = options.validate === false || options.noValidate === true;
|
|
79
173
|
// Validate specs and change before archiving
|
|
80
174
|
if (!skipValidation) {
|
|
81
175
|
const validator = new Validator();
|
|
82
176
|
let hasValidationErrors = false;
|
|
83
|
-
// Validate proposal.md (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
177
|
+
// Validate proposal.md (informative only; human mode prints warnings)
|
|
178
|
+
if (!json) {
|
|
179
|
+
const changeFile = path.join(changeDir, 'proposal.md');
|
|
180
|
+
try {
|
|
181
|
+
await fs.access(changeFile);
|
|
182
|
+
const changeReport = await validator.validateChange(changeFile);
|
|
183
|
+
// Proposal validation is informative only (do not block archive)
|
|
184
|
+
if (!changeReport.valid) {
|
|
185
|
+
console.log(chalk.yellow(`\nProposal warnings in proposal.md (non-blocking):`));
|
|
186
|
+
for (const issue of changeReport.issues) {
|
|
187
|
+
const symbol = issue.level === 'ERROR' ? '⚠' : (issue.level === 'WARNING' ? '⚠' : 'ℹ');
|
|
188
|
+
console.log(chalk.yellow(` ${symbol} ${issue.message}`));
|
|
189
|
+
}
|
|
94
190
|
}
|
|
95
191
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
192
|
+
catch {
|
|
193
|
+
// Change file doesn't exist, skip validation
|
|
194
|
+
}
|
|
99
195
|
}
|
|
100
196
|
// Validate delta-formatted spec files under the change directory if present
|
|
101
197
|
const changeSpecsDir = path.join(changeDir, 'specs');
|
|
@@ -122,21 +218,31 @@ export class ArchiveCommand {
|
|
|
122
218
|
const deltaReport = await validator.validateChangeDeltaSpecs(changeDir);
|
|
123
219
|
if (!deltaReport.valid) {
|
|
124
220
|
hasValidationErrors = true;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
221
|
+
if (!json) {
|
|
222
|
+
console.log(chalk.red(`\nValidation errors in change delta specs:`));
|
|
223
|
+
for (const issue of deltaReport.issues) {
|
|
224
|
+
if (issue.level === 'ERROR') {
|
|
225
|
+
console.log(chalk.red(` ✗ ${issue.message}`));
|
|
226
|
+
}
|
|
227
|
+
else if (issue.level === 'WARNING') {
|
|
228
|
+
console.log(chalk.yellow(` ⚠ ${issue.message}`));
|
|
229
|
+
}
|
|
132
230
|
}
|
|
133
231
|
}
|
|
134
232
|
}
|
|
135
233
|
}
|
|
136
234
|
if (hasValidationErrors) {
|
|
235
|
+
if (json) {
|
|
236
|
+
throw new ArchiveBlockedError('archive_validation_failed', `Validation failed for change '${changeName}'.`, `Run ${withStoreFlag(root, `openspec validate ${changeName}`)} for details, fix the errors, or rerun with --no-validate.`);
|
|
237
|
+
}
|
|
137
238
|
console.log(chalk.red('\nValidation failed. Please fix the errors before archiving.'));
|
|
138
239
|
console.log(chalk.yellow('To skip validation (not recommended), use --no-validate flag.'));
|
|
139
|
-
return;
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else if (json) {
|
|
244
|
+
if (!options.yes) {
|
|
245
|
+
throw new ArchiveBlockedError('archive_confirmation_required', 'Skipping validation requires confirmation: rerun with --yes.', withStoreFlag(root, 'openspec archive <change-name> --json --no-validate --yes'));
|
|
140
246
|
}
|
|
141
247
|
}
|
|
142
248
|
else {
|
|
@@ -150,7 +256,7 @@ export class ArchiveCommand {
|
|
|
150
256
|
});
|
|
151
257
|
if (!proceed) {
|
|
152
258
|
console.log('Archive cancelled.');
|
|
153
|
-
return;
|
|
259
|
+
return null;
|
|
154
260
|
}
|
|
155
261
|
}
|
|
156
262
|
else {
|
|
@@ -161,11 +267,18 @@ export class ArchiveCommand {
|
|
|
161
267
|
}
|
|
162
268
|
// Show progress and check for incomplete tasks
|
|
163
269
|
const progress = await getTaskProgressForChange(changesDir, changeName);
|
|
164
|
-
|
|
165
|
-
|
|
270
|
+
if (!json) {
|
|
271
|
+
const status = formatTaskStatus(progress);
|
|
272
|
+
console.log(`Task status: ${status}`);
|
|
273
|
+
}
|
|
166
274
|
const incompleteTasks = Math.max(progress.total - progress.completed, 0);
|
|
167
275
|
if (incompleteTasks > 0) {
|
|
168
|
-
if (
|
|
276
|
+
if (json) {
|
|
277
|
+
if (!options.yes) {
|
|
278
|
+
throw new ArchiveBlockedError('archive_tasks_incomplete', `${incompleteTasks} incomplete task(s) found for change '${changeName}'.`, 'Complete the tasks or rerun with --yes.');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else if (!options.yes) {
|
|
169
282
|
const { confirm } = await import('@inquirer/prompts');
|
|
170
283
|
const proceed = await confirm({
|
|
171
284
|
message: `Warning: ${incompleteTasks} incomplete task(s) found. Continue?`,
|
|
@@ -173,7 +286,7 @@ export class ArchiveCommand {
|
|
|
173
286
|
});
|
|
174
287
|
if (!proceed) {
|
|
175
288
|
console.log('Archive cancelled.');
|
|
176
|
-
return;
|
|
289
|
+
return null;
|
|
177
290
|
}
|
|
178
291
|
}
|
|
179
292
|
else {
|
|
@@ -181,21 +294,30 @@ export class ArchiveCommand {
|
|
|
181
294
|
}
|
|
182
295
|
}
|
|
183
296
|
// Handle spec updates unless skipSpecs flag is set
|
|
297
|
+
let specsUpdated = false;
|
|
298
|
+
let totals;
|
|
184
299
|
if (options.skipSpecs) {
|
|
185
|
-
|
|
300
|
+
if (!json) {
|
|
301
|
+
console.log('Skipping spec updates (--skip-specs flag provided).');
|
|
302
|
+
}
|
|
186
303
|
}
|
|
187
304
|
else {
|
|
188
305
|
// Find specs to update
|
|
189
306
|
const specUpdates = await findSpecUpdates(changeDir, mainSpecsDir);
|
|
190
307
|
if (specUpdates.length > 0) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
308
|
+
if (!json) {
|
|
309
|
+
console.log('\nSpecs to update:');
|
|
310
|
+
for (const update of specUpdates) {
|
|
311
|
+
const status = update.exists ? 'update' : 'create';
|
|
312
|
+
const capability = path.basename(path.dirname(update.target));
|
|
313
|
+
console.log(` ${capability}: ${status}`);
|
|
314
|
+
}
|
|
196
315
|
}
|
|
197
316
|
let shouldUpdateSpecs = true;
|
|
198
317
|
if (!options.yes) {
|
|
318
|
+
if (json) {
|
|
319
|
+
throw new ArchiveBlockedError('archive_confirmation_required', `Updating ${specUpdates.length} spec(s) requires confirmation: rerun with --yes.`, withStoreFlag(root, 'openspec archive <change-name> --json --yes'));
|
|
320
|
+
}
|
|
199
321
|
const { confirm } = await import('@inquirer/prompts');
|
|
200
322
|
shouldUpdateSpecs = await confirm({
|
|
201
323
|
message: 'Proceed with spec updates?',
|
|
@@ -210,22 +332,28 @@ export class ArchiveCommand {
|
|
|
210
332
|
const prepared = [];
|
|
211
333
|
try {
|
|
212
334
|
for (const update of specUpdates) {
|
|
213
|
-
const built = await buildUpdatedSpec(update, changeName);
|
|
335
|
+
const built = await buildUpdatedSpec(update, changeName, { silent: json });
|
|
214
336
|
prepared.push({ update, rebuilt: built.rebuilt, counts: built.counts });
|
|
215
337
|
}
|
|
216
338
|
}
|
|
217
339
|
catch (err) {
|
|
340
|
+
if (json) {
|
|
341
|
+
throw new ArchiveBlockedError('archive_spec_update_failed', String(err.message || err), 'Fix the change delta specs and rerun. No files were changed.');
|
|
342
|
+
}
|
|
218
343
|
console.log(String(err.message || err));
|
|
219
344
|
console.log('Aborted. No files were changed.');
|
|
220
|
-
return;
|
|
345
|
+
return null;
|
|
221
346
|
}
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
347
|
+
// Validate every rebuilt spec before writing any of them, so a
|
|
348
|
+
// late validation failure really does leave all targets unchanged.
|
|
349
|
+
if (!skipValidation) {
|
|
350
|
+
for (const p of prepared) {
|
|
351
|
+
const specName = path.basename(path.dirname(p.update.target));
|
|
227
352
|
const report = await new Validator().validateSpecContent(specName, p.rebuilt);
|
|
228
353
|
if (!report.valid) {
|
|
354
|
+
if (json) {
|
|
355
|
+
throw new ArchiveBlockedError('archive_spec_validation_failed', `Rebuilt spec for '${specName}' failed validation. No files were changed.`, `Run ${withStoreFlag(root, `openspec validate ${specName}`)} after fixing the change deltas.`);
|
|
356
|
+
}
|
|
229
357
|
console.log(chalk.red(`\nValidation errors in rebuilt spec for ${specName} (will not write changes):`));
|
|
230
358
|
for (const issue of report.issues) {
|
|
231
359
|
if (issue.level === 'ERROR')
|
|
@@ -234,17 +362,29 @@ export class ArchiveCommand {
|
|
|
234
362
|
console.log(chalk.yellow(` ⚠ ${issue.message}`));
|
|
235
363
|
}
|
|
236
364
|
console.log('Aborted. No files were changed.');
|
|
237
|
-
return;
|
|
365
|
+
return null;
|
|
238
366
|
}
|
|
239
367
|
}
|
|
240
|
-
await writeUpdatedSpec(p.update, p.rebuilt, p.counts);
|
|
241
|
-
totals.added += p.counts.added;
|
|
242
|
-
totals.modified += p.counts.modified;
|
|
243
|
-
totals.removed += p.counts.removed;
|
|
244
|
-
totals.renamed += p.counts.renamed;
|
|
245
368
|
}
|
|
246
|
-
|
|
247
|
-
|
|
369
|
+
// All validations passed; write files and display counts
|
|
370
|
+
const writeTotals = { added: 0, modified: 0, removed: 0, renamed: 0 };
|
|
371
|
+
for (const p of prepared) {
|
|
372
|
+
await writeUpdatedSpec(p.update, p.rebuilt, p.counts, {
|
|
373
|
+
silent: json,
|
|
374
|
+
// Cross-root paths must be absolute when a store is selected.
|
|
375
|
+
...(isStoreSelectedRoot(root) ? { displayPath: p.update.target } : {}),
|
|
376
|
+
});
|
|
377
|
+
writeTotals.added += p.counts.added;
|
|
378
|
+
writeTotals.modified += p.counts.modified;
|
|
379
|
+
writeTotals.removed += p.counts.removed;
|
|
380
|
+
writeTotals.renamed += p.counts.renamed;
|
|
381
|
+
}
|
|
382
|
+
specsUpdated = true;
|
|
383
|
+
totals = writeTotals;
|
|
384
|
+
if (!json) {
|
|
385
|
+
console.log(`Totals: + ${writeTotals.added}, ~ ${writeTotals.modified}, - ${writeTotals.removed}, → ${writeTotals.renamed}`);
|
|
386
|
+
console.log('Specs updated successfully.');
|
|
387
|
+
}
|
|
248
388
|
}
|
|
249
389
|
}
|
|
250
390
|
}
|
|
@@ -252,20 +392,33 @@ export class ArchiveCommand {
|
|
|
252
392
|
const archiveName = `${this.getArchiveDate()}-${changeName}`;
|
|
253
393
|
const archivePath = path.join(archiveDir, archiveName);
|
|
254
394
|
// Check if archive already exists
|
|
395
|
+
let archiveExists = false;
|
|
255
396
|
try {
|
|
256
397
|
await fs.access(archivePath);
|
|
257
|
-
|
|
398
|
+
archiveExists = true;
|
|
258
399
|
}
|
|
259
400
|
catch (error) {
|
|
260
401
|
if (error.code !== 'ENOENT') {
|
|
261
402
|
throw error;
|
|
262
403
|
}
|
|
263
404
|
}
|
|
405
|
+
if (archiveExists) {
|
|
406
|
+
throw new ArchiveBlockedError('archive_target_exists', `Archive '${archiveName}' already exists.`);
|
|
407
|
+
}
|
|
264
408
|
// Create archive directory if needed
|
|
265
409
|
await fs.mkdir(archiveDir, { recursive: true });
|
|
266
410
|
// Move change to archive (uses copy+remove on EPERM/EXDEV, e.g. Windows)
|
|
267
411
|
await moveDirectory(changeDir, archivePath);
|
|
268
|
-
|
|
412
|
+
if (!json) {
|
|
413
|
+
console.log(`Change '${changeName}' archived as '${archiveName}'.`);
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
change: changeName,
|
|
417
|
+
archivedAs: archiveName,
|
|
418
|
+
path: archivePath,
|
|
419
|
+
specsUpdated,
|
|
420
|
+
...(totals ? { totals } : {}),
|
|
421
|
+
};
|
|
269
422
|
}
|
|
270
423
|
async selectChange(changesDir) {
|
|
271
424
|
const { select } = await import('@inquirer/prompts');
|
|
@@ -5,5 +5,5 @@ export { detectCompleted } from './state.js';
|
|
|
5
5
|
export { artifactOutputExists, isGlobPattern, resolveArtifactOutputs } from './outputs.js';
|
|
6
6
|
export { resolveSchema, listSchemas, listSchemasWithInfo, getSchemaDir, getPackageSchemasDir, getUserSchemasDir, SchemaLoadError, type SchemaInfo, } from './resolver.js';
|
|
7
7
|
export { loadTemplate, loadChangeContext, generateInstructions, formatChangeStatus, TemplateLoadError, type ChangeContext, type LoadChangeContextOptions, type ArtifactInstructions, type DependencyInfo, type ArtifactStatus, type ChangeStatus, type ArtifactPathSummary, } from './instruction-loader.js';
|
|
8
|
-
export type { PlanningHomeSummary,
|
|
8
|
+
export type { PlanningHomeSummary, ActionContext, } from '../change-status-policy.js';
|
|
9
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { ArtifactGraph } from './graph.js';
|
|
2
|
-
import { type ActionContext, type
|
|
2
|
+
import { type ActionContext, type PlanningHomeSummary } from '../change-status-policy.js';
|
|
3
|
+
import { type ProjectConfig } from '../project-config.js';
|
|
4
|
+
import type { ReferenceIndexEntry } from '../references.js';
|
|
3
5
|
import type { PlanningHome } from '../planning-home.js';
|
|
4
|
-
import type { ChangeMetadata
|
|
6
|
+
import type { ChangeMetadata } from '../change-metadata/index.js';
|
|
5
7
|
import type { CompletedSet } from './types.js';
|
|
6
8
|
/**
|
|
7
9
|
* Error thrown when loading a template fails.
|
|
@@ -30,8 +32,6 @@ export interface ChangeContext {
|
|
|
30
32
|
planningHome?: PlanningHome;
|
|
31
33
|
/** Parsed change metadata, when present */
|
|
32
34
|
metadata?: ChangeMetadata;
|
|
33
|
-
/** Stored initiative link, when this change is linked to shared context */
|
|
34
|
-
initiative?: InitiativeLink;
|
|
35
35
|
}
|
|
36
36
|
export interface LoadChangeContextOptions {
|
|
37
37
|
changeDir?: string;
|
|
@@ -51,8 +51,6 @@ export interface ArtifactInstructions {
|
|
|
51
51
|
changeDir: string;
|
|
52
52
|
/** Resolved planning home for this change */
|
|
53
53
|
planningHome?: PlanningHomeSummary;
|
|
54
|
-
/** Stored initiative link, when this change is linked to shared context */
|
|
55
|
-
initiative?: InitiativeLink;
|
|
56
54
|
/** Output path pattern (e.g., "proposal.md") */
|
|
57
55
|
outputPath: string;
|
|
58
56
|
/** Absolute output path or glob pattern resolved under the change directory */
|
|
@@ -67,6 +65,8 @@ export interface ArtifactInstructions {
|
|
|
67
65
|
context: string | undefined;
|
|
68
66
|
/** Artifact-specific rules from config (constraints for AI, not to be included in output) */
|
|
69
67
|
rules: string[] | undefined;
|
|
68
|
+
/** Referenced-store index (read-only upstream context; omitted when no references are declared) */
|
|
69
|
+
references?: ReferenceIndexEntry[];
|
|
70
70
|
/** Template content (structure to follow - this IS the output format) */
|
|
71
71
|
template: string;
|
|
72
72
|
/** Dependencies with completion status and paths */
|
|
@@ -108,16 +108,13 @@ export interface ChangeStatus {
|
|
|
108
108
|
changeName: string;
|
|
109
109
|
/** Schema name */
|
|
110
110
|
schemaName: string;
|
|
111
|
-
/**
|
|
111
|
+
/** Planning home facts (generated skills derive the archive dir
|
|
112
|
+
* from planningHome.changesDir - a published agent contract). */
|
|
112
113
|
planningHome?: PlanningHomeSummary;
|
|
113
|
-
/** Stored initiative link, when this change is linked to shared context */
|
|
114
|
-
initiative?: InitiativeLink;
|
|
115
114
|
/** Full path to the change root */
|
|
116
115
|
changeRoot: string;
|
|
117
116
|
/** Absolute artifact path details keyed by artifact ID */
|
|
118
117
|
artifactPaths: Record<string, ArtifactPathSummary>;
|
|
119
|
-
/** Workspace affected-area summary, when available */
|
|
120
|
-
affectedAreas?: AffectedAreasSummary;
|
|
121
118
|
/** Plain-language next steps for users and agents */
|
|
122
119
|
nextSteps: string[];
|
|
123
120
|
/** Machine-readable action constraints for agents */
|
|
@@ -172,12 +169,20 @@ export declare function loadChangeContext(projectRoot: string, changeName: strin
|
|
|
172
169
|
* @returns Enriched artifact instructions
|
|
173
170
|
* @throws Error if artifact not found
|
|
174
171
|
*/
|
|
175
|
-
export
|
|
172
|
+
export interface GenerateInstructionsOptions {
|
|
173
|
+
/** Pre-read project config; suppresses the internal read (no double read). */
|
|
174
|
+
projectConfig?: ProjectConfig | null;
|
|
175
|
+
/** Referenced-store index assembled at the command boundary. */
|
|
176
|
+
references?: ReferenceIndexEntry[];
|
|
177
|
+
}
|
|
178
|
+
export declare function generateInstructions(context: ChangeContext, artifactId: string, projectRoot?: string, options?: GenerateInstructionsOptions): ArtifactInstructions;
|
|
176
179
|
/**
|
|
177
180
|
* Formats the status of all artifacts in a change.
|
|
178
181
|
*
|
|
179
182
|
* @param context - Change context
|
|
180
183
|
* @returns Formatted change status
|
|
181
184
|
*/
|
|
182
|
-
export declare function formatChangeStatus(context: ChangeContext
|
|
185
|
+
export declare function formatChangeStatus(context: ChangeContext, options?: {
|
|
186
|
+
storeId?: string;
|
|
187
|
+
}): ChangeStatus;
|
|
183
188
|
//# sourceMappingURL=instruction-loader.d.ts.map
|
|
@@ -6,7 +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,
|
|
9
|
+
import { buildActionContext, buildNextSteps, summarizePlanningHome, } from '../change-status-policy.js';
|
|
10
10
|
import { readProjectConfig, validateConfigRules } from '../project-config.js';
|
|
11
11
|
// Session-level cache for validation warnings (avoid repeating same warnings)
|
|
12
12
|
const shownWarnings = new Set();
|
|
@@ -79,24 +79,9 @@ export function loadChangeContext(projectRoot, changeName, schemaName, options =
|
|
|
79
79
|
projectRoot,
|
|
80
80
|
...(options.planningHome ? { planningHome: options.planningHome } : {}),
|
|
81
81
|
...(metadata ? { metadata } : {}),
|
|
82
|
-
...(metadata?.initiative ? { initiative: metadata.initiative } : {}),
|
|
83
82
|
};
|
|
84
83
|
}
|
|
85
|
-
|
|
86
|
-
* Generates enriched instructions for creating an artifact.
|
|
87
|
-
*
|
|
88
|
-
* Instruction injection order:
|
|
89
|
-
* 1. <context> - Project context from config (if present)
|
|
90
|
-
* 2. <rules> - Artifact-specific rules from config (if present)
|
|
91
|
-
* 3. <template> - Schema's template content
|
|
92
|
-
*
|
|
93
|
-
* @param context - Change context
|
|
94
|
-
* @param artifactId - Artifact ID to generate instructions for
|
|
95
|
-
* @param projectRoot - Project root directory (for reading config)
|
|
96
|
-
* @returns Enriched artifact instructions
|
|
97
|
-
* @throws Error if artifact not found
|
|
98
|
-
*/
|
|
99
|
-
export function generateInstructions(context, artifactId, projectRoot) {
|
|
84
|
+
export function generateInstructions(context, artifactId, projectRoot, options = {}) {
|
|
100
85
|
const artifact = context.graph.getArtifact(artifactId);
|
|
101
86
|
if (!artifact) {
|
|
102
87
|
throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'`);
|
|
@@ -106,9 +91,9 @@ export function generateInstructions(context, artifactId, projectRoot) {
|
|
|
106
91
|
const unlocks = getUnlockedArtifacts(context.graph, artifactId);
|
|
107
92
|
// Use projectRoot from context if not explicitly provided
|
|
108
93
|
const effectiveProjectRoot = projectRoot ?? context.projectRoot;
|
|
109
|
-
//
|
|
110
|
-
let projectConfig = null;
|
|
111
|
-
if (effectiveProjectRoot) {
|
|
94
|
+
// Use the pre-read config when provided; otherwise read it here.
|
|
95
|
+
let projectConfig = options.projectConfig ?? null;
|
|
96
|
+
if (options.projectConfig === undefined && effectiveProjectRoot) {
|
|
112
97
|
try {
|
|
113
98
|
projectConfig = readProjectConfig(effectiveProjectRoot);
|
|
114
99
|
}
|
|
@@ -138,7 +123,6 @@ export function generateInstructions(context, artifactId, projectRoot) {
|
|
|
138
123
|
schemaName: context.schemaName,
|
|
139
124
|
changeDir: context.changeDir,
|
|
140
125
|
planningHome: summarizePlanningHome(context.planningHome),
|
|
141
|
-
...(context.initiative ? { initiative: context.initiative } : {}),
|
|
142
126
|
outputPath: artifact.generates,
|
|
143
127
|
resolvedOutputPath: path.join(context.changeDir, artifact.generates),
|
|
144
128
|
existingOutputPaths: resolveArtifactOutputs(context.changeDir, artifact.generates),
|
|
@@ -146,6 +130,7 @@ export function generateInstructions(context, artifactId, projectRoot) {
|
|
|
146
130
|
instruction: artifact.instruction,
|
|
147
131
|
context: configContext,
|
|
148
132
|
rules: configRules,
|
|
133
|
+
...(options.references !== undefined ? { references: options.references } : {}),
|
|
149
134
|
template: templateContent,
|
|
150
135
|
dependencies,
|
|
151
136
|
unlocks,
|
|
@@ -183,7 +168,7 @@ function getUnlockedArtifacts(graph, artifactId) {
|
|
|
183
168
|
* @param context - Change context
|
|
184
169
|
* @returns Formatted change status
|
|
185
170
|
*/
|
|
186
|
-
export function formatChangeStatus(context) {
|
|
171
|
+
export function formatChangeStatus(context, options = {}) {
|
|
187
172
|
// Load schema to get apply phase configuration
|
|
188
173
|
const schema = resolveSchema(context.schemaName, context.projectRoot);
|
|
189
174
|
const applyRequires = schema.apply?.requires ?? schema.artifacts.map(a => a.id);
|
|
@@ -222,31 +207,23 @@ export function formatChangeStatus(context) {
|
|
|
222
207
|
const buildOrder = context.graph.getBuildOrder();
|
|
223
208
|
const orderMap = new Map(buildOrder.map((id, idx) => [id, idx]));
|
|
224
209
|
artifactStatuses.sort((a, b) => (orderMap.get(a.id) ?? 0) - (orderMap.get(b.id) ?? 0));
|
|
225
|
-
const affectedAreas = summarizeAffectedAreas({
|
|
226
|
-
planningHome: context.planningHome,
|
|
227
|
-
metadata: context.metadata,
|
|
228
|
-
});
|
|
229
210
|
const isComplete = context.graph.isComplete(context.completed);
|
|
230
211
|
const artifactIds = artifactStatuses.map((artifact) => artifact.id);
|
|
231
212
|
return {
|
|
232
213
|
changeName: context.changeName,
|
|
233
214
|
schemaName: context.schemaName,
|
|
234
215
|
planningHome: summarizePlanningHome(context.planningHome),
|
|
235
|
-
...(context.initiative ? { initiative: context.initiative } : {}),
|
|
236
216
|
changeRoot: context.changeDir,
|
|
237
217
|
artifactPaths,
|
|
238
|
-
affectedAreas,
|
|
239
218
|
isComplete,
|
|
240
219
|
applyRequires,
|
|
241
220
|
nextSteps: buildNextSteps({
|
|
242
221
|
changeName: context.changeName,
|
|
243
|
-
planningHome: context.planningHome,
|
|
244
222
|
artifactStatuses,
|
|
245
|
-
affectedAreas,
|
|
246
223
|
allArtifactsComplete: isComplete,
|
|
224
|
+
...(options.storeId ? { storeId: options.storeId } : {}),
|
|
247
225
|
}),
|
|
248
226
|
actionContext: buildActionContext({
|
|
249
|
-
planningHome: context.planningHome,
|
|
250
227
|
projectRoot: context.projectRoot,
|
|
251
228
|
artifactIds,
|
|
252
229
|
}),
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { isKebabId } from '../id.js';
|
|
3
|
+
export { isKebabId } from '../id.js';
|
|
2
4
|
const KebabIdentifierSchema = (label) => z.string().superRefine((value, ctx) => {
|
|
3
|
-
if (
|
|
5
|
+
if (!isKebabId(value)) {
|
|
4
6
|
ctx.addIssue({
|
|
5
7
|
code: 'custom',
|
|
6
8
|
message: `${label} must be kebab-case with lowercase letters, numbers, and single hyphen separators`,
|
|
@@ -8,7 +10,7 @@ const KebabIdentifierSchema = (label) => z.string().superRefine((value, ctx) =>
|
|
|
8
10
|
}
|
|
9
11
|
});
|
|
10
12
|
export const InitiativeLinkSchema = z.object({
|
|
11
|
-
store: KebabIdentifierSchema('
|
|
13
|
+
store: KebabIdentifierSchema('Store id'),
|
|
12
14
|
id: KebabIdentifierSchema('Initiative id'),
|
|
13
15
|
}).strict();
|
|
14
16
|
// Per-change metadata schema. The schema field is validated against available
|