@fission-ai/openspec 1.4.1 → 1.6.0-beta.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 +37 -4
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +95 -95
- package/dist/commands/change.d.ts +5 -2
- package/dist/commands/change.js +23 -55
- 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 +10 -0
- package/dist/commands/validate.js +88 -40
- 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 +220 -68
- 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.d.ts +1 -1
- package/dist/core/command-generation/adapters/claude.js +4 -15
- package/dist/core/command-generation/adapters/cursor.js +1 -14
- package/dist/core/command-generation/adapters/index.d.ts +2 -0
- package/dist/core/command-generation/adapters/index.js +2 -0
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
- package/dist/core/command-generation/adapters/oh-my-pi.js +44 -0
- package/dist/core/command-generation/adapters/pi.js +1 -14
- package/dist/core/command-generation/adapters/trae.d.ts +13 -0
- package/dist/core/command-generation/adapters/trae.js +48 -0
- package/dist/core/command-generation/adapters/windsurf.js +1 -14
- package/dist/core/command-generation/registry.js +4 -0
- 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/config.js +1 -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 +27 -6
- package/dist/core/list.d.ts +2 -0
- package/dist/core/list.js +37 -14
- 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 +195 -0
- package/dist/core/parsers/change-parser.js +2 -1
- package/dist/core/parsers/markdown-parser.d.ts +0 -3
- package/dist/core/parsers/markdown-parser.js +5 -59
- package/dist/core/parsers/requirement-blocks.d.ts +11 -0
- package/dist/core/parsers/requirement-blocks.js +42 -8
- package/dist/core/parsers/requirement-text.d.ts +55 -0
- package/dist/core/parsers/requirement-text.js +136 -0
- package/dist/core/planning-home.d.ts +1 -6
- package/dist/core/planning-home.js +2 -43
- package/dist/core/profile-sync-drift.js +1 -0
- package/dist/core/profiles.d.ts +2 -2
- package/dist/core/profiles.js +2 -1
- 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/schemas/base.schema.js +7 -2
- package/dist/core/shared/allowed-tools.d.ts +12 -0
- package/dist/core/shared/allowed-tools.js +12 -0
- package/dist/core/shared/skill-generation.js +5 -1
- package/dist/core/shared/tool-detection.d.ts +2 -2
- package/dist/core/shared/tool-detection.js +2 -0
- package/dist/core/specs-apply.d.ts +6 -1
- package/dist/core/specs-apply.js +41 -5
- 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 +803 -0
- package/dist/core/store/registry.d.ts +58 -0
- package/dist/core/store/registry.js +275 -0
- package/dist/core/templates/skill-templates.d.ts +1 -0
- package/dist/core/templates/skill-templates.js +1 -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/update-change.d.ts +10 -0
- package/dist/core/templates/workflows/update-change.js +167 -0
- package/dist/core/templates/workflows/verify-change.js +5 -4
- package/dist/core/update.js +4 -4
- package/dist/core/validation/validator.d.ts +7 -0
- package/dist/core/validation/validator.js +101 -38
- package/dist/core/view.js +1 -1
- 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/dist/utils/task-progress.d.ts +10 -1
- package/dist/utils/task-progress.js +66 -2
- package/package.json +26 -23
- 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 -8
- package/dist/commands/workspace/selection.js +0 -104
- 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 -5
- package/dist/commands/workspace.js +0 -574
- 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 -121
- 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
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import ora from 'ora';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { Validator } from '../core/validation/validator.js';
|
|
4
|
+
import { resolveRootForCommand, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from '../core/root-selection.js';
|
|
4
5
|
import { isInteractive, resolveNoInteractive } from '../utils/interactive.js';
|
|
5
|
-
import {
|
|
6
|
+
import { getSpecIds } from '../utils/item-discovery.js';
|
|
7
|
+
import { getAvailableChanges } from './workflow/shared.js';
|
|
6
8
|
import { nearestMatches } from '../utils/match.js';
|
|
7
9
|
export class ValidateCommand {
|
|
8
10
|
async execute(itemName, options = {}) {
|
|
11
|
+
const root = await resolveRootForCommand(options, { json: options.json });
|
|
12
|
+
if (!root) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
9
15
|
const interactive = isInteractive(options);
|
|
10
16
|
// Handle bulk flags first
|
|
11
17
|
if (options.all || options.changes || options.specs) {
|
|
12
|
-
await this.runBulkValidation({
|
|
18
|
+
await this.runBulkValidation(root, {
|
|
13
19
|
changes: !!options.all || !!options.changes,
|
|
14
20
|
specs: !!options.all || !!options.specs,
|
|
15
21
|
}, { strict: !!options.strict, json: !!options.json, concurrency: options.concurrency, noInteractive: resolveNoInteractive(options) });
|
|
@@ -18,16 +24,16 @@ export class ValidateCommand {
|
|
|
18
24
|
// No item and no flags
|
|
19
25
|
if (!itemName) {
|
|
20
26
|
if (interactive) {
|
|
21
|
-
await this.runInteractiveSelector({ strict: !!options.strict, json: !!options.json, concurrency: options.concurrency });
|
|
27
|
+
await this.runInteractiveSelector(root, { strict: !!options.strict, json: !!options.json, concurrency: options.concurrency });
|
|
22
28
|
return;
|
|
23
29
|
}
|
|
24
|
-
this.printNonInteractiveHint();
|
|
30
|
+
this.printNonInteractiveHint(root);
|
|
25
31
|
process.exitCode = 1;
|
|
26
32
|
return;
|
|
27
33
|
}
|
|
28
34
|
// Direct item validation with type detection or override
|
|
29
35
|
const typeOverride = this.normalizeType(options.type);
|
|
30
|
-
await this.validateDirectItem(itemName, { typeOverride, strict: !!options.strict, json: !!options.json });
|
|
36
|
+
await this.validateDirectItem(root, itemName, { typeOverride, strict: !!options.strict, json: !!options.json });
|
|
31
37
|
}
|
|
32
38
|
normalizeType(value) {
|
|
33
39
|
if (!value)
|
|
@@ -37,7 +43,18 @@ export class ValidateCommand {
|
|
|
37
43
|
return v;
|
|
38
44
|
return undefined;
|
|
39
45
|
}
|
|
40
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Resolve change IDs by directory existence within the resolved root — the
|
|
48
|
+
* same rule `openspec status`/`instructions` use (`getAvailableChanges`) —
|
|
49
|
+
* rather than requiring `proposal.md`. This lets `validate` resolve a
|
|
50
|
+
* scaffolded or still-authoring change that the sibling commands already
|
|
51
|
+
* resolve (#1182). Sorted to preserve the prior `getActiveChangeIds` ordering.
|
|
52
|
+
*/
|
|
53
|
+
async listChangeIds(root) {
|
|
54
|
+
const ids = await getAvailableChanges(root.path, root.changesDir);
|
|
55
|
+
return ids.sort();
|
|
56
|
+
}
|
|
57
|
+
async runInteractiveSelector(root, opts) {
|
|
41
58
|
const { select } = await import('@inquirer/prompts');
|
|
42
59
|
const choice = await select({
|
|
43
60
|
message: 'What would you like to validate?',
|
|
@@ -49,13 +66,13 @@ export class ValidateCommand {
|
|
|
49
66
|
],
|
|
50
67
|
});
|
|
51
68
|
if (choice === 'all')
|
|
52
|
-
return this.runBulkValidation({ changes: true, specs: true }, opts);
|
|
69
|
+
return this.runBulkValidation(root, { changes: true, specs: true }, opts);
|
|
53
70
|
if (choice === 'changes')
|
|
54
|
-
return this.runBulkValidation({ changes: true, specs: false }, opts);
|
|
71
|
+
return this.runBulkValidation(root, { changes: true, specs: false }, opts);
|
|
55
72
|
if (choice === 'specs')
|
|
56
|
-
return this.runBulkValidation({ changes: false, specs: true }, opts);
|
|
73
|
+
return this.runBulkValidation(root, { changes: false, specs: true }, opts);
|
|
57
74
|
// one
|
|
58
|
-
const [changes, specs] = await Promise.all([
|
|
75
|
+
const [changes, specs] = await Promise.all([this.listChangeIds(root), getSpecIds(root.path)]);
|
|
59
76
|
const items = [];
|
|
60
77
|
items.push(...changes.map(id => ({ name: `change/${id}`, value: { type: 'change', id } })));
|
|
61
78
|
items.push(...specs.map(id => ({ name: `spec/${id}`, value: { type: 'spec', id } })));
|
|
@@ -65,59 +82,85 @@ export class ValidateCommand {
|
|
|
65
82
|
return;
|
|
66
83
|
}
|
|
67
84
|
const picked = await select({ message: 'Pick an item', choices: items });
|
|
68
|
-
await this.validateByType(picked.type, picked.id, opts);
|
|
85
|
+
await this.validateByType(root, picked.type, picked.id, opts);
|
|
69
86
|
}
|
|
70
|
-
printNonInteractiveHint() {
|
|
87
|
+
printNonInteractiveHint(root) {
|
|
71
88
|
console.error('Nothing to validate. Try one of:');
|
|
72
|
-
console.error('
|
|
73
|
-
console.error('
|
|
74
|
-
console.error('
|
|
75
|
-
console.error('
|
|
89
|
+
console.error(` ${withStoreFlag(root, 'openspec validate --all')}`);
|
|
90
|
+
console.error(` ${withStoreFlag(root, 'openspec validate --changes')}`);
|
|
91
|
+
console.error(` ${withStoreFlag(root, 'openspec validate --specs')}`);
|
|
92
|
+
console.error(` ${withStoreFlag(root, 'openspec validate <item-name>')}`);
|
|
76
93
|
console.error('Or run in an interactive terminal.');
|
|
77
94
|
}
|
|
78
|
-
async validateDirectItem(itemName, opts) {
|
|
79
|
-
const [changes, specs] = await Promise.all([
|
|
95
|
+
async validateDirectItem(root, itemName, opts) {
|
|
96
|
+
const [changes, specs] = await Promise.all([this.listChangeIds(root), getSpecIds(root.path)]);
|
|
80
97
|
const isChange = changes.includes(itemName);
|
|
81
98
|
const isSpec = specs.includes(itemName);
|
|
82
99
|
const type = opts.typeOverride ?? (isChange ? 'change' : isSpec ? 'spec' : undefined);
|
|
83
100
|
if (!type) {
|
|
84
|
-
console.error(`Unknown item '${itemName}'`);
|
|
85
101
|
const suggestions = nearestMatches(itemName, [...changes, ...specs]);
|
|
86
|
-
|
|
87
|
-
|
|
102
|
+
const message = suggestions.length
|
|
103
|
+
? `Unknown item '${itemName}'. Did you mean: ${suggestions.join(', ')}?`
|
|
104
|
+
: `Unknown item '${itemName}'.`;
|
|
105
|
+
if (opts.json) {
|
|
106
|
+
console.log(JSON.stringify({ status: [{ severity: 'error', code: 'unknown_item', message }] }, null, 2));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
console.error(message);
|
|
110
|
+
}
|
|
88
111
|
process.exitCode = 1;
|
|
89
112
|
return;
|
|
90
113
|
}
|
|
91
114
|
if (!opts.typeOverride && isChange && isSpec) {
|
|
115
|
+
if (opts.json) {
|
|
116
|
+
console.log(JSON.stringify({
|
|
117
|
+
status: [
|
|
118
|
+
{
|
|
119
|
+
severity: 'error',
|
|
120
|
+
code: 'ambiguous_item',
|
|
121
|
+
message: `Ambiguous item '${itemName}' matches both a change and a spec.`,
|
|
122
|
+
fix: 'Pass --type change|spec.',
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
}, null, 2));
|
|
126
|
+
process.exitCode = 1;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
92
129
|
console.error(`Ambiguous item '${itemName}' matches both a change and a spec.`);
|
|
93
|
-
|
|
130
|
+
// The noun-form commands are cwd-based and cannot reach a selected store.
|
|
131
|
+
if (isStoreSelectedRoot(root)) {
|
|
132
|
+
console.error('Pass --type change|spec.');
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
console.error('Pass --type change|spec, or use: openspec change validate / openspec spec validate');
|
|
136
|
+
}
|
|
94
137
|
process.exitCode = 1;
|
|
95
138
|
return;
|
|
96
139
|
}
|
|
97
|
-
await this.validateByType(type, itemName, opts);
|
|
140
|
+
await this.validateByType(root, type, itemName, opts);
|
|
98
141
|
}
|
|
99
|
-
async validateByType(type, id, opts) {
|
|
142
|
+
async validateByType(root, type, id, opts) {
|
|
100
143
|
const validator = new Validator(opts.strict);
|
|
101
144
|
if (type === 'change') {
|
|
102
|
-
const changeDir = path.join(
|
|
145
|
+
const changeDir = path.join(root.changesDir, id);
|
|
103
146
|
const start = Date.now();
|
|
104
147
|
const report = await validator.validateChangeDeltaSpecs(changeDir);
|
|
105
148
|
const durationMs = Date.now() - start;
|
|
106
|
-
this.printReport('change', id, report, durationMs, opts.json);
|
|
149
|
+
this.printReport('change', id, report, durationMs, opts.json, root);
|
|
107
150
|
// Non-zero exit if invalid (keeps enriched output test semantics)
|
|
108
151
|
process.exitCode = report.valid ? 0 : 1;
|
|
109
152
|
return;
|
|
110
153
|
}
|
|
111
|
-
const file = path.join(
|
|
154
|
+
const file = path.join(root.specsDir, id, 'spec.md');
|
|
112
155
|
const start = Date.now();
|
|
113
156
|
const report = await validator.validateSpec(file);
|
|
114
157
|
const durationMs = Date.now() - start;
|
|
115
|
-
this.printReport('spec', id, report, durationMs, opts.json);
|
|
158
|
+
this.printReport('spec', id, report, durationMs, opts.json, root);
|
|
116
159
|
process.exitCode = report.valid ? 0 : 1;
|
|
117
160
|
}
|
|
118
|
-
printReport(type, id, report, durationMs, json) {
|
|
161
|
+
printReport(type, id, report, durationMs, json, root) {
|
|
119
162
|
if (json) {
|
|
120
|
-
const out = { items: [{ id, type, valid: report.valid, issues: report.issues, durationMs }], summary: { totals: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 }, byType: { [type]: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 } } }, version: '1.0' };
|
|
163
|
+
const out = { items: [{ id, type, valid: report.valid, issues: report.issues, durationMs }], summary: { totals: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 }, byType: { [type]: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 } } }, version: '1.0', root: toRootOutput(root) };
|
|
121
164
|
console.log(JSON.stringify(out, null, 2));
|
|
122
165
|
return;
|
|
123
166
|
}
|
|
@@ -131,15 +174,15 @@ export class ValidateCommand {
|
|
|
131
174
|
const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
|
|
132
175
|
console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
|
|
133
176
|
}
|
|
134
|
-
this.printNextSteps(type);
|
|
177
|
+
this.printNextSteps(type, id, root);
|
|
135
178
|
}
|
|
136
179
|
}
|
|
137
|
-
printNextSteps(type) {
|
|
180
|
+
printNextSteps(type, id, root) {
|
|
138
181
|
const bullets = [];
|
|
139
182
|
if (type === 'change') {
|
|
140
183
|
bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
|
|
141
184
|
bullets.push('- Each requirement MUST include at least one #### Scenario: block');
|
|
142
|
-
bullets.push(
|
|
185
|
+
bullets.push(`- Debug parsed deltas: ${withStoreFlag(root, `openspec show ${id} --json --deltas-only`)}`);
|
|
143
186
|
}
|
|
144
187
|
else {
|
|
145
188
|
bullets.push('- Ensure spec includes ## Purpose and ## Requirements sections');
|
|
@@ -149,11 +192,11 @@ export class ValidateCommand {
|
|
|
149
192
|
console.error('Next steps:');
|
|
150
193
|
bullets.forEach(b => console.error(` ${b}`));
|
|
151
194
|
}
|
|
152
|
-
async runBulkValidation(scope, opts) {
|
|
195
|
+
async runBulkValidation(root, scope, opts) {
|
|
153
196
|
const spinner = !opts.json && !opts.noInteractive ? ora('Validating...').start() : undefined;
|
|
154
197
|
const [changeIds, specIds] = await Promise.all([
|
|
155
|
-
scope.changes ?
|
|
156
|
-
scope.specs ? getSpecIds() : Promise.resolve([]),
|
|
198
|
+
scope.changes ? this.listChangeIds(root) : Promise.resolve([]),
|
|
199
|
+
scope.specs ? getSpecIds(root.path) : Promise.resolve([]),
|
|
157
200
|
]);
|
|
158
201
|
const DEFAULT_CONCURRENCY = 6;
|
|
159
202
|
const maxSuggestions = 5; // used by nearestMatches
|
|
@@ -163,7 +206,7 @@ export class ValidateCommand {
|
|
|
163
206
|
for (const id of changeIds) {
|
|
164
207
|
queue.push(async () => {
|
|
165
208
|
const start = Date.now();
|
|
166
|
-
const changeDir = path.join(
|
|
209
|
+
const changeDir = path.join(root.changesDir, id);
|
|
167
210
|
const report = await validator.validateChangeDeltaSpecs(changeDir);
|
|
168
211
|
const durationMs = Date.now() - start;
|
|
169
212
|
return { id, type: 'change', valid: report.valid, issues: report.issues, durationMs };
|
|
@@ -172,7 +215,7 @@ export class ValidateCommand {
|
|
|
172
215
|
for (const id of specIds) {
|
|
173
216
|
queue.push(async () => {
|
|
174
217
|
const start = Date.now();
|
|
175
|
-
const file = path.join(
|
|
218
|
+
const file = path.join(root.specsDir, id, 'spec.md');
|
|
176
219
|
const report = await validator.validateSpec(file);
|
|
177
220
|
const durationMs = Date.now() - start;
|
|
178
221
|
return { id, type: 'spec', valid: report.valid, issues: report.issues, durationMs };
|
|
@@ -188,7 +231,7 @@ export class ValidateCommand {
|
|
|
188
231
|
},
|
|
189
232
|
};
|
|
190
233
|
if (opts.json) {
|
|
191
|
-
const out = { items: [], summary, version: '1.0' };
|
|
234
|
+
const out = { items: [], summary, version: '1.0', root: toRootOutput(root) };
|
|
192
235
|
console.log(JSON.stringify(out, null, 2));
|
|
193
236
|
}
|
|
194
237
|
else {
|
|
@@ -245,7 +288,7 @@ export class ValidateCommand {
|
|
|
245
288
|
},
|
|
246
289
|
};
|
|
247
290
|
if (opts.json) {
|
|
248
|
-
const out = { items: results, summary, version: '1.0' };
|
|
291
|
+
const out = { items: results, summary, version: '1.0', root: toRootOutput(root) };
|
|
249
292
|
console.log(JSON.stringify(out, null, 2));
|
|
250
293
|
}
|
|
251
294
|
else {
|
|
@@ -256,6 +299,11 @@ export class ValidateCommand {
|
|
|
256
299
|
console.error(`✗ ${res.type}/${res.id}`);
|
|
257
300
|
}
|
|
258
301
|
console.log(`Totals: ${summary.totals.passed} passed, ${summary.totals.failed} failed (${summary.totals.items} items)`);
|
|
302
|
+
const firstFailure = results.find((res) => !res.valid);
|
|
303
|
+
if (firstFailure) {
|
|
304
|
+
const storeFlag = isStoreSelectedRoot(root) ? ` --store ${root.storeId}` : '';
|
|
305
|
+
console.log(`Details: openspec validate ${firstFailure.id} --type ${firstFailure.type}${storeFlag}`);
|
|
306
|
+
}
|
|
259
307
|
}
|
|
260
308
|
process.exitCode = failed > 0 ? 1 : 0;
|
|
261
309
|
}
|
|
@@ -13,7 +13,5 @@ export { schemasCommand } from './schemas.js';
|
|
|
13
13
|
export type { SchemasOptions } from './schemas.js';
|
|
14
14
|
export { newChangeCommand } from './new-change.js';
|
|
15
15
|
export type { NewChangeOptions } from './new-change.js';
|
|
16
|
-
export { setChangeCommand } from './set-change.js';
|
|
17
|
-
export type { SetChangeOptions } from './set-change.js';
|
|
18
16
|
export { DEFAULT_SCHEMA } from './shared.js';
|
|
19
17
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -8,6 +8,5 @@ export { instructionsCommand, applyInstructionsCommand } from './instructions.js
|
|
|
8
8
|
export { templatesCommand } from './templates.js';
|
|
9
9
|
export { schemasCommand } from './schemas.js';
|
|
10
10
|
export { newChangeCommand } from './new-change.js';
|
|
11
|
-
export { setChangeCommand } from './set-change.js';
|
|
12
11
|
export { DEFAULT_SCHEMA } from './shared.js';
|
|
13
12
|
//# sourceMappingURL=index.js.map
|
|
@@ -5,25 +5,35 @@
|
|
|
5
5
|
* Includes both artifact instructions and apply instructions.
|
|
6
6
|
*/
|
|
7
7
|
import { type ArtifactInstructions } from '../../core/artifact-graph/index.js';
|
|
8
|
+
import { type PlanningHome } from '../../core/planning-home.js';
|
|
9
|
+
import { type ReferenceIndexEntry } from '../../core/references.js';
|
|
8
10
|
import { type ApplyInstructions } from './shared.js';
|
|
9
11
|
export interface InstructionsOptions {
|
|
10
12
|
change?: string;
|
|
11
13
|
schema?: string;
|
|
14
|
+
store?: string;
|
|
15
|
+
storePath?: string;
|
|
12
16
|
json?: boolean;
|
|
13
17
|
}
|
|
14
18
|
export interface ApplyInstructionsOptions {
|
|
15
19
|
change?: string;
|
|
16
20
|
schema?: string;
|
|
21
|
+
store?: string;
|
|
22
|
+
storePath?: string;
|
|
17
23
|
json?: boolean;
|
|
18
24
|
}
|
|
19
25
|
export declare function instructionsCommand(artifactId: string | undefined, options: InstructionsOptions): Promise<void>;
|
|
20
26
|
export declare function printInstructionsText(instructions: ArtifactInstructions, isBlocked: boolean): void;
|
|
27
|
+
export interface GenerateApplyInstructionsOptions {
|
|
28
|
+
planningHome?: PlanningHome;
|
|
29
|
+
references?: ReferenceIndexEntry[];
|
|
30
|
+
}
|
|
21
31
|
/**
|
|
22
32
|
* Generates apply instructions for implementing tasks from a change.
|
|
23
33
|
* Schema-aware: reads apply phase configuration from schema to determine
|
|
24
34
|
* required artifacts, tracking file, and instruction.
|
|
25
35
|
*/
|
|
26
|
-
export declare function generateApplyInstructions(projectRoot: string, changeName: string, schemaName?: string,
|
|
36
|
+
export declare function generateApplyInstructions(projectRoot: string, changeName: string, schemaName?: string, options?: GenerateApplyInstructionsOptions): Promise<ApplyInstructions>;
|
|
27
37
|
export declare function applyInstructionsCommand(options: ApplyInstructionsOptions): Promise<void>;
|
|
28
38
|
export declare function printApplyInstructionsText(instructions: ApplyInstructions): void;
|
|
29
39
|
//# sourceMappingURL=instructions.d.ts.map
|
|
@@ -8,17 +8,49 @@ import ora from 'ora';
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import { loadChangeContext, generateInstructions, resolveSchema, resolveArtifactOutputs, } from '../../core/artifact-graph/index.js';
|
|
11
|
-
import { getChangeDir, resolveCurrentPlanningHomeSync } from '../../core/planning-home.js';
|
|
11
|
+
import { getChangeDir, resolveCurrentPlanningHomeSync, } from '../../core/planning-home.js';
|
|
12
|
+
import { resolveRootForCommand, withStoreFlag, toPlanningHome, toRootOutput, } from '../../core/root-selection.js';
|
|
13
|
+
import { assembleReferenceIndex, renderReferencedStoresBlock, renderReferencedStoresSection, } from '../../core/references.js';
|
|
14
|
+
import { readRegistrySnapshot } from '../../core/store/registry.js';
|
|
15
|
+
import { readProjectConfig } from '../../core/project-config.js';
|
|
12
16
|
import { validateChangeExists, validateSchemaExists, } from './shared.js';
|
|
13
17
|
// -----------------------------------------------------------------------------
|
|
14
18
|
// Artifact Instructions Command
|
|
15
19
|
// -----------------------------------------------------------------------------
|
|
20
|
+
/**
|
|
21
|
+
* Reads the resolved root's config once, assembles the referenced-store
|
|
22
|
+
* index when references are declared, and resolves the config path for
|
|
23
|
+
* fix text. Shared by both instruction surfaces.
|
|
24
|
+
*/
|
|
25
|
+
async function loadRootConfigContext(root) {
|
|
26
|
+
// readProjectConfig never throws: missing/unparseable configs are null.
|
|
27
|
+
const projectConfig = readProjectConfig(root.path);
|
|
28
|
+
// One registry read serves every relationship consumer in this
|
|
29
|
+
// output so it never carries a torn snapshot.
|
|
30
|
+
const snapshot = await readRegistrySnapshot();
|
|
31
|
+
const registryEntries = snapshot.entries;
|
|
32
|
+
const declared = projectConfig?.references ?? [];
|
|
33
|
+
const index = declared.length > 0
|
|
34
|
+
? await assembleReferenceIndex({ references: declared, resolvedRoot: root, registryEntries })
|
|
35
|
+
: [];
|
|
36
|
+
// Omitted, not empty: an index emptied by self-reference omission must
|
|
37
|
+
// look identical to an undeclared one in JSON.
|
|
38
|
+
return {
|
|
39
|
+
projectConfig,
|
|
40
|
+
references: index.length > 0 ? index : undefined,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
16
43
|
export async function instructionsCommand(artifactId, options) {
|
|
44
|
+
// Resolve (and banner) before the spinner starts so stderr stays readable.
|
|
45
|
+
const root = await resolveRootForCommand(options, { json: options.json });
|
|
46
|
+
if (!root) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
17
49
|
const spinner = options.json ? undefined : ora('Generating instructions...').start();
|
|
18
50
|
try {
|
|
19
|
-
const planningHome =
|
|
20
|
-
const projectRoot =
|
|
21
|
-
const changeName = await validateChangeExists(options.change, projectRoot,
|
|
51
|
+
const planningHome = toPlanningHome(root);
|
|
52
|
+
const projectRoot = root.path;
|
|
53
|
+
const changeName = await validateChangeExists(options.change, projectRoot, root.changesDir, { newChangeHint: withStoreFlag(root, 'openspec new change <name>') });
|
|
22
54
|
// Validate schema if explicitly provided
|
|
23
55
|
if (options.schema) {
|
|
24
56
|
validateSchemaExists(options.schema, projectRoot);
|
|
@@ -39,11 +71,15 @@ export async function instructionsCommand(artifactId, options) {
|
|
|
39
71
|
const validIds = context.graph.getAllArtifacts().map((a) => a.id);
|
|
40
72
|
throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'. Valid artifacts:\n ${validIds.join('\n ')}`);
|
|
41
73
|
}
|
|
42
|
-
const
|
|
74
|
+
const { projectConfig, references } = await loadRootConfigContext(root);
|
|
75
|
+
const instructions = generateInstructions(context, artifactId, projectRoot, {
|
|
76
|
+
projectConfig,
|
|
77
|
+
references,
|
|
78
|
+
});
|
|
43
79
|
const isBlocked = instructions.dependencies.some((d) => !d.done);
|
|
44
80
|
spinner?.stop();
|
|
45
81
|
if (options.json) {
|
|
46
|
-
console.log(JSON.stringify(instructions, null, 2));
|
|
82
|
+
console.log(JSON.stringify({ ...instructions, root: toRootOutput(root) }, null, 2));
|
|
47
83
|
return;
|
|
48
84
|
}
|
|
49
85
|
printInstructionsText(instructions, isBlocked);
|
|
@@ -54,14 +90,10 @@ export async function instructionsCommand(artifactId, options) {
|
|
|
54
90
|
}
|
|
55
91
|
}
|
|
56
92
|
export function printInstructionsText(instructions, isBlocked) {
|
|
57
|
-
const { artifactId, changeName, schemaName, changeDir,
|
|
93
|
+
const { artifactId, changeName, schemaName, changeDir, resolvedOutputPath, description, instruction, context, rules, template, dependencies, unlocks, } = instructions;
|
|
58
94
|
// Opening tag
|
|
59
95
|
console.log(`<artifact id="${artifactId}" change="${changeName}" schema="${schemaName}">`);
|
|
60
96
|
console.log();
|
|
61
|
-
if (initiative) {
|
|
62
|
-
console.log(`<initiative store="${initiative.store}" id="${initiative.id}" />`);
|
|
63
|
-
console.log();
|
|
64
|
-
}
|
|
65
97
|
// Warning for blocked artifacts
|
|
66
98
|
if (isBlocked) {
|
|
67
99
|
const missing = dependencies.filter((d) => !d.done).map((d) => d.id);
|
|
@@ -85,6 +117,11 @@ export function printInstructionsText(instructions, isBlocked) {
|
|
|
85
117
|
console.log('</project_context>');
|
|
86
118
|
console.log();
|
|
87
119
|
}
|
|
120
|
+
// Referenced-store index (read-only upstream context)
|
|
121
|
+
if (instructions.references && instructions.references.length > 0) {
|
|
122
|
+
console.log(renderReferencedStoresBlock(instructions.references));
|
|
123
|
+
console.log();
|
|
124
|
+
}
|
|
88
125
|
// Rules (AI constraint - do not include in output)
|
|
89
126
|
if (rules && rules.length > 0) {
|
|
90
127
|
console.log('<rules>');
|
|
@@ -175,7 +212,9 @@ function parseTasksFile(content) {
|
|
|
175
212
|
* Schema-aware: reads apply phase configuration from schema to determine
|
|
176
213
|
* required artifacts, tracking file, and instruction.
|
|
177
214
|
*/
|
|
178
|
-
export async function generateApplyInstructions(projectRoot, changeName, schemaName,
|
|
215
|
+
export async function generateApplyInstructions(projectRoot, changeName, schemaName, options = {}) {
|
|
216
|
+
const planningHome = options.planningHome ?? resolveCurrentPlanningHomeSync({ startPath: projectRoot });
|
|
217
|
+
const references = options.references;
|
|
179
218
|
// loadChangeContext will auto-detect schema from metadata if not provided
|
|
180
219
|
const context = loadChangeContext(projectRoot, changeName, schemaName, {
|
|
181
220
|
changeDir: getChangeDir(planningHome, changeName),
|
|
@@ -257,30 +296,39 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
257
296
|
changeName,
|
|
258
297
|
changeDir,
|
|
259
298
|
schemaName: context.schemaName,
|
|
260
|
-
...(context.initiative ? { initiative: context.initiative } : {}),
|
|
261
299
|
contextFiles,
|
|
262
300
|
progress: { total, complete, remaining },
|
|
263
301
|
tasks,
|
|
264
302
|
state,
|
|
265
303
|
missingArtifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
|
|
266
304
|
instruction,
|
|
305
|
+
...(references !== undefined ? { references } : {}),
|
|
267
306
|
};
|
|
268
307
|
}
|
|
269
308
|
export async function applyInstructionsCommand(options) {
|
|
309
|
+
// Resolve (and banner) before the spinner starts so stderr stays readable.
|
|
310
|
+
const root = await resolveRootForCommand(options, { json: options.json });
|
|
311
|
+
if (!root) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
270
314
|
const spinner = options.json ? undefined : ora('Generating apply instructions...').start();
|
|
271
315
|
try {
|
|
272
|
-
const planningHome =
|
|
273
|
-
const projectRoot =
|
|
274
|
-
const changeName = await validateChangeExists(options.change, projectRoot,
|
|
316
|
+
const planningHome = toPlanningHome(root);
|
|
317
|
+
const projectRoot = root.path;
|
|
318
|
+
const changeName = await validateChangeExists(options.change, projectRoot, root.changesDir, { newChangeHint: withStoreFlag(root, 'openspec new change <name>') });
|
|
275
319
|
// Validate schema if explicitly provided
|
|
276
320
|
if (options.schema) {
|
|
277
321
|
validateSchemaExists(options.schema, projectRoot);
|
|
278
322
|
}
|
|
279
323
|
// generateApplyInstructions uses loadChangeContext which auto-detects schema
|
|
280
|
-
const
|
|
324
|
+
const { references } = await loadRootConfigContext(root);
|
|
325
|
+
const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema, {
|
|
326
|
+
planningHome,
|
|
327
|
+
references,
|
|
328
|
+
});
|
|
281
329
|
spinner?.stop();
|
|
282
330
|
if (options.json) {
|
|
283
|
-
console.log(JSON.stringify(instructions, null, 2));
|
|
331
|
+
console.log(JSON.stringify({ ...instructions, root: toRootOutput(root) }, null, 2));
|
|
284
332
|
return;
|
|
285
333
|
}
|
|
286
334
|
printApplyInstructionsText(instructions);
|
|
@@ -291,13 +339,14 @@ export async function applyInstructionsCommand(options) {
|
|
|
291
339
|
}
|
|
292
340
|
}
|
|
293
341
|
export function printApplyInstructionsText(instructions) {
|
|
294
|
-
const { changeName, schemaName,
|
|
342
|
+
const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction } = instructions;
|
|
295
343
|
console.log(`## Apply: ${changeName}`);
|
|
296
344
|
console.log(`Schema: ${schemaName}`);
|
|
297
|
-
if (initiative) {
|
|
298
|
-
console.log(`Initiative: ${initiative.store}/${initiative.id}`);
|
|
299
|
-
}
|
|
300
345
|
console.log();
|
|
346
|
+
if (instructions.references && instructions.references.length > 0) {
|
|
347
|
+
console.log(renderReferencedStoresSection(instructions.references));
|
|
348
|
+
console.log();
|
|
349
|
+
}
|
|
301
350
|
// Warning for blocked state
|
|
302
351
|
if (state === 'blocked' && missingArtifacts) {
|
|
303
352
|
console.log('### ⚠️ Blocked');
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* New Change Command
|
|
3
3
|
*
|
|
4
|
-
* Creates a new change directory with optional description and schema
|
|
4
|
+
* Creates a new change directory with optional description and schema in the
|
|
5
|
+
* resolved OpenSpec root. `--store <id>` selects a registered store's
|
|
6
|
+
* root; initiative linking and workspace affected areas are no longer part of
|
|
7
|
+
* this command.
|
|
5
8
|
*/
|
|
6
9
|
export interface NewChangeOptions {
|
|
7
10
|
description?: string;
|
|
8
11
|
goal?: string;
|
|
9
|
-
areas?: string;
|
|
10
12
|
schema?: string;
|
|
11
|
-
initiative?: string;
|
|
12
13
|
store?: string;
|
|
13
14
|
storePath?: string;
|
|
15
|
+
initiative?: string;
|
|
16
|
+
areas?: string;
|
|
14
17
|
json?: boolean;
|
|
15
18
|
}
|
|
16
19
|
export declare function newChangeCommand(name: string | undefined, options: NewChangeOptions): Promise<void>;
|