@fission-ai/openspec 1.6.0-beta.1 → 1.7.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 +28 -1
- package/dist/cli/index.js +73 -7
- package/dist/commands/change.d.ts +0 -1
- package/dist/commands/change.js +90 -50
- package/dist/commands/config.js +14 -8
- package/dist/commands/doctor.js +9 -4
- package/dist/commands/feedback.js +74 -23
- package/dist/commands/schema.js +21 -11
- package/dist/commands/show.js +5 -2
- package/dist/commands/spec.js +20 -24
- package/dist/commands/validate.js +18 -3
- package/dist/commands/workflow/index.d.ts +1 -1
- package/dist/commands/workflow/index.js +1 -1
- package/dist/commands/workflow/instructions.d.ts +7 -1
- package/dist/commands/workflow/instructions.js +89 -6
- package/dist/commands/workflow/shared.d.ts +13 -2
- package/dist/commands/workflow/shared.js +34 -4
- package/dist/commands/workflow/status.js +7 -2
- package/dist/core/archive.d.ts +0 -1
- package/dist/core/archive.js +96 -33
- package/dist/core/artifact-graph/graph.d.ts +16 -0
- package/dist/core/artifact-graph/graph.js +30 -6
- package/dist/core/artifact-graph/instruction-loader.d.ts +27 -2
- package/dist/core/artifact-graph/instruction-loader.js +55 -7
- package/dist/core/artifact-graph/resolver.d.ts +14 -0
- package/dist/core/artifact-graph/resolver.js +34 -6
- package/dist/core/change-metadata/schema.d.ts +1 -0
- package/dist/core/change-metadata/schema.js +6 -0
- package/dist/core/change-status-policy.d.ts +1 -1
- package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
- package/dist/core/command-generation/adapters/amazon-q.js +7 -1
- package/dist/core/command-generation/adapters/antigravity.js +2 -1
- package/dist/core/command-generation/adapters/auggie.js +2 -1
- package/dist/core/command-generation/adapters/bob.d.ts +5 -1
- package/dist/core/command-generation/adapters/bob.js +6 -5
- package/dist/core/command-generation/adapters/claude.js +1 -8
- package/dist/core/command-generation/adapters/codebuddy.js +3 -2
- package/dist/core/command-generation/adapters/continue.js +3 -2
- package/dist/core/command-generation/adapters/costrict.js +2 -1
- package/dist/core/command-generation/adapters/crush.js +5 -5
- package/dist/core/command-generation/adapters/cursor.js +2 -2
- package/dist/core/command-generation/adapters/devin.d.ts +19 -0
- package/dist/core/command-generation/adapters/devin.js +36 -0
- package/dist/core/command-generation/adapters/factory.js +2 -1
- package/dist/core/command-generation/adapters/gemini.js +37 -2
- package/dist/core/command-generation/adapters/github-copilot.js +2 -1
- package/dist/core/command-generation/adapters/iflow.js +5 -4
- package/dist/core/command-generation/adapters/index.d.ts +2 -2
- package/dist/core/command-generation/adapters/index.js +2 -2
- package/dist/core/command-generation/adapters/junie.js +2 -1
- package/dist/core/command-generation/adapters/kiro.js +2 -1
- package/dist/core/command-generation/adapters/lingma.js +5 -5
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/oh-my-pi.js +3 -5
- package/dist/core/command-generation/adapters/opencode.js +3 -5
- package/dist/core/command-generation/adapters/pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/pi.js +3 -6
- package/dist/core/command-generation/adapters/qoder.js +5 -5
- package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
- package/dist/core/command-generation/adapters/qwen.js +12 -7
- package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
- package/dist/core/command-generation/adapters/roocode.js +4 -4
- package/dist/core/command-generation/adapters/trae.js +1 -21
- package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
- package/dist/core/command-generation/adapters/zcode.js +33 -0
- package/dist/core/command-generation/generator.d.ts +8 -0
- package/dist/core/command-generation/generator.js +15 -1
- package/dist/core/command-generation/index.d.ts +1 -1
- package/dist/core/command-generation/index.js +1 -1
- package/dist/core/command-generation/invocation.d.ts +73 -0
- package/dist/core/command-generation/invocation.js +77 -0
- package/dist/core/command-generation/registry.js +4 -4
- package/dist/core/command-generation/types.d.ts +9 -2
- package/dist/core/command-generation/yaml.d.ts +20 -6
- package/dist/core/command-generation/yaml.js +31 -21
- package/dist/core/command-surface.d.ts +15 -0
- package/dist/core/command-surface.js +33 -0
- package/dist/core/completions/command-registry.js +8 -2
- package/dist/core/completions/generators/powershell-generator.js +15 -8
- package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
- package/dist/core/completions/installers/zsh-installer.js +20 -4
- package/dist/core/config-prompts.d.ts +1 -1
- package/dist/core/config-prompts.js +14 -1
- package/dist/core/config-schema.d.ts +7 -1
- package/dist/core/config-schema.js +40 -1
- package/dist/core/config.d.ts +12 -0
- package/dist/core/config.js +21 -3
- package/dist/core/global-config.d.ts +5 -0
- package/dist/core/init.d.ts +35 -0
- package/dist/core/init.js +225 -39
- package/dist/core/legacy-cleanup.d.ts +59 -0
- package/dist/core/legacy-cleanup.js +249 -22
- package/dist/core/list.js +5 -7
- package/dist/core/migration.d.ts +71 -1
- package/dist/core/migration.js +294 -1
- package/dist/core/onboarding-commands.d.ts +30 -0
- package/dist/core/onboarding-commands.js +38 -0
- package/dist/core/parsers/change-parser.d.ts +17 -2
- package/dist/core/parsers/change-parser.js +33 -20
- package/dist/core/parsers/code-fence.d.ts +15 -0
- package/dist/core/parsers/code-fence.js +49 -0
- package/dist/core/parsers/markdown-parser.js +2 -1
- package/dist/core/parsers/requirement-blocks.d.ts +8 -0
- package/dist/core/parsers/requirement-blocks.js +60 -35
- package/dist/core/parsers/requirement-text.d.ts +1 -8
- package/dist/core/parsers/requirement-text.js +4 -42
- package/dist/core/parsers/spec-structure.js +3 -29
- package/dist/core/profile-sync-drift.d.ts +0 -8
- package/dist/core/profile-sync-drift.js +9 -45
- package/dist/core/project-config.d.ts +25 -6
- package/dist/core/project-config.js +77 -7
- package/dist/core/references.js +42 -4
- package/dist/core/relationship-health.d.ts +8 -0
- package/dist/core/relationship-health.js +13 -0
- package/dist/core/root-selection.d.ts +9 -6
- package/dist/core/root-selection.js +42 -5
- package/dist/core/shared/tool-detection.d.ts +25 -5
- package/dist/core/shared/tool-detection.js +105 -6
- package/dist/core/specs-apply.d.ts +8 -33
- package/dist/core/specs-apply.js +252 -157
- package/dist/core/store/git.d.ts +13 -0
- package/dist/core/store/git.js +24 -0
- package/dist/core/templates/workflows/apply-change.js +54 -4
- package/dist/core/templates/workflows/archive-change.js +166 -42
- package/dist/core/templates/workflows/bulk-archive-change.js +240 -63
- package/dist/core/templates/workflows/continue-change.js +32 -40
- package/dist/core/templates/workflows/explore.js +12 -0
- package/dist/core/templates/workflows/ff-change.js +46 -30
- package/dist/core/templates/workflows/new-change.js +2 -2
- package/dist/core/templates/workflows/onboard.js +4 -4
- package/dist/core/templates/workflows/propose.js +50 -32
- package/dist/core/templates/workflows/store-selection.d.ts +1 -1
- package/dist/core/templates/workflows/store-selection.js +1 -1
- package/dist/core/templates/workflows/sync-specs.js +172 -22
- package/dist/core/templates/workflows/update-change.js +20 -12
- package/dist/core/templates/workflows/verify-change.js +16 -10
- package/dist/core/update.d.ts +39 -5
- package/dist/core/update.js +288 -71
- package/dist/core/validation/constants.d.ts +4 -1
- package/dist/core/validation/constants.js +4 -1
- package/dist/core/validation/validator.d.ts +1 -7
- package/dist/core/validation/validator.js +92 -36
- package/dist/core/version-check.d.ts +152 -0
- package/dist/core/version-check.js +736 -0
- package/dist/core/view.js +12 -17
- package/dist/prompts/searchable-multi-select.js +1 -1
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.js +55 -33
- package/dist/ui/welcome-screen.d.ts +9 -1
- package/dist/ui/welcome-screen.js +91 -38
- package/dist/utils/change-metadata.d.ts +30 -0
- package/dist/utils/change-metadata.js +101 -7
- package/dist/utils/change-utils.d.ts +10 -4
- package/dist/utils/change-utils.js +20 -13
- package/dist/utils/command-references.d.ts +80 -6
- package/dist/utils/command-references.js +138 -7
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/item-discovery.d.ts +19 -0
- package/dist/utils/item-discovery.js +30 -48
- package/dist/utils/shell-detection.d.ts +1 -1
- package/dist/utils/shell-detection.js +64 -11
- package/dist/utils/spec-discovery.d.ts +40 -0
- package/dist/utils/spec-discovery.js +102 -0
- package/package.json +29 -30
- package/schemas/spec-driven/schema.yaml +60 -5
- package/schemas/spec-driven/templates/design.md +2 -2
- package/schemas/spec-driven/templates/proposal.md +5 -1
- package/schemas/spec-driven/templates/spec.md +3 -0
- package/dist/core/command-generation/adapters/codex.d.ts +0 -16
- package/dist/core/command-generation/adapters/codex.js +0 -39
- package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
- package/dist/core/command-generation/adapters/windsurf.js +0 -38
package/dist/commands/schema.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import { stringify as stringifyYaml } from 'yaml';
|
|
5
|
-
import { getSchemaDir, getProjectSchemasDir, getUserSchemasDir, getPackageSchemasDir, listSchemas, } from '../core/artifact-graph/resolver.js';
|
|
5
|
+
import { getSchemaDir, getProjectSchemasDir, getUserSchemasDir, getPackageSchemasDir, isSchemaDir, listSchemas, } from '../core/artifact-graph/resolver.js';
|
|
6
6
|
import { parseSchema, SchemaValidationError } from '../core/artifact-graph/schema.js';
|
|
7
7
|
/**
|
|
8
8
|
* Check all three locations for a schema and return which ones exist.
|
|
@@ -339,7 +339,7 @@ export function registerSchemaCommand(program) {
|
|
|
339
339
|
const schemaResults = [];
|
|
340
340
|
let anyInvalid = false;
|
|
341
341
|
for (const entry of entries) {
|
|
342
|
-
if (!entry
|
|
342
|
+
if (!isSchemaDir(projectSchemasDir, entry))
|
|
343
343
|
continue;
|
|
344
344
|
const schemaDir = path.join(projectSchemasDir, entry.name);
|
|
345
345
|
const schemaPath = path.join(schemaDir, 'schema.yaml');
|
|
@@ -584,8 +584,9 @@ export function registerSchemaCommand(program) {
|
|
|
584
584
|
return;
|
|
585
585
|
}
|
|
586
586
|
const schemaDir = path.join(getProjectSchemasDir(projectRoot), name);
|
|
587
|
-
// Check
|
|
588
|
-
|
|
587
|
+
// Check overwrite permission without mutating the destination
|
|
588
|
+
const schemaExists = fs.existsSync(schemaDir);
|
|
589
|
+
if (schemaExists) {
|
|
589
590
|
if (!options?.force) {
|
|
590
591
|
if (options?.json) {
|
|
591
592
|
console.log(JSON.stringify({
|
|
@@ -601,9 +602,6 @@ export function registerSchemaCommand(program) {
|
|
|
601
602
|
process.exitCode = 1;
|
|
602
603
|
return;
|
|
603
604
|
}
|
|
604
|
-
if (spinner)
|
|
605
|
-
spinner.start(`Removing existing schema '${name}'...`);
|
|
606
|
-
fs.rmSync(schemaDir, { recursive: true });
|
|
607
605
|
}
|
|
608
606
|
// Determine artifacts and description
|
|
609
607
|
let description;
|
|
@@ -625,6 +623,12 @@ export function registerSchemaCommand(program) {
|
|
|
625
623
|
}));
|
|
626
624
|
selectedArtifactIds = await checkbox({
|
|
627
625
|
message: 'Select artifacts to include:',
|
|
626
|
+
theme: {
|
|
627
|
+
icon: {
|
|
628
|
+
checked: '[x]',
|
|
629
|
+
unchecked: '[ ]',
|
|
630
|
+
},
|
|
631
|
+
},
|
|
628
632
|
choices: artifactChoices,
|
|
629
633
|
});
|
|
630
634
|
if (selectedArtifactIds.length === 0) {
|
|
@@ -673,10 +677,6 @@ export function registerSchemaCommand(program) {
|
|
|
673
677
|
selectedArtifactIds = DEFAULT_ARTIFACTS.map((a) => a.id);
|
|
674
678
|
}
|
|
675
679
|
}
|
|
676
|
-
// Create schema directory
|
|
677
|
-
if (spinner)
|
|
678
|
-
spinner.start(`Creating schema '${name}'...`);
|
|
679
|
-
fs.mkdirSync(schemaDir, { recursive: true });
|
|
680
680
|
// Build artifacts array with proper dependencies
|
|
681
681
|
const selectedArtifacts = selectedArtifactIds.map((id) => {
|
|
682
682
|
const template = DEFAULT_ARTIFACTS.find((a) => a.id === id);
|
|
@@ -718,6 +718,16 @@ export function registerSchemaCommand(program) {
|
|
|
718
718
|
tracks: 'tasks.md',
|
|
719
719
|
};
|
|
720
720
|
}
|
|
721
|
+
// Replace only after all inputs have been collected and validated
|
|
722
|
+
if (schemaExists) {
|
|
723
|
+
if (spinner)
|
|
724
|
+
spinner.start(`Removing existing schema '${name}'...`);
|
|
725
|
+
fs.rmSync(schemaDir, { recursive: true });
|
|
726
|
+
}
|
|
727
|
+
// Create schema directory
|
|
728
|
+
if (spinner)
|
|
729
|
+
spinner.start(`Creating schema '${name}'...`);
|
|
730
|
+
fs.mkdirSync(schemaDir, { recursive: true });
|
|
721
731
|
fs.writeFileSync(path.join(schemaDir, 'schema.yaml'), stringifyYaml(schema));
|
|
722
732
|
// Create template files in templates/ subdirectory (standard location)
|
|
723
733
|
const templatesDir = path.join(schemaDir, 'templates');
|
package/dist/commands/show.js
CHANGED
|
@@ -157,14 +157,17 @@ export class ShowCommand {
|
|
|
157
157
|
}
|
|
158
158
|
warnIrrelevantFlags(type, options) {
|
|
159
159
|
const irrelevant = [];
|
|
160
|
+
// --no-scenarios makes commander default `scenarios` to true, so its
|
|
161
|
+
// presence alone does not mean the user passed it — only false does.
|
|
162
|
+
const isUserProvided = (k) => k === 'scenarios' ? options[k] === false : k in options;
|
|
160
163
|
if (type === 'change') {
|
|
161
164
|
for (const k of SPEC_FLAG_KEYS)
|
|
162
|
-
if (k
|
|
165
|
+
if (isUserProvided(k))
|
|
163
166
|
irrelevant.push(k);
|
|
164
167
|
}
|
|
165
168
|
else {
|
|
166
169
|
for (const k of CHANGE_FLAG_KEYS)
|
|
167
|
-
if (k
|
|
170
|
+
if (isUserProvided(k))
|
|
168
171
|
irrelevant.push(k);
|
|
169
172
|
}
|
|
170
173
|
if (irrelevant.length > 0) {
|
package/dist/commands/spec.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { MarkdownParser } from '../core/parsers/markdown-parser.js';
|
|
4
4
|
import { Validator } from '../core/validation/validator.js';
|
|
5
5
|
import { isInteractive } from '../utils/interactive.js';
|
|
6
6
|
import { getSpecIds } from '../utils/item-discovery.js';
|
|
7
|
+
import { discoverSpecFiles } from '../utils/spec-discovery.js';
|
|
7
8
|
const SPECS_DIR = 'openspec/specs';
|
|
8
9
|
function parseSpecFromFile(specPath, specId) {
|
|
9
10
|
const content = readFileSync(specPath, 'utf-8');
|
|
@@ -127,36 +128,31 @@ export function registerSpecCommand(rootProgram) {
|
|
|
127
128
|
.description('List all available specifications')
|
|
128
129
|
.option('--json', 'Output as JSON')
|
|
129
130
|
.option('--long', 'Show id and title with counts')
|
|
130
|
-
.action((options) => {
|
|
131
|
+
.action(async (options) => {
|
|
131
132
|
try {
|
|
132
133
|
if (!existsSync(SPECS_DIR)) {
|
|
133
134
|
console.log('No items found');
|
|
134
135
|
return;
|
|
135
136
|
}
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
.map(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
requirementCount: 0
|
|
154
|
-
};
|
|
155
|
-
}
|
|
137
|
+
const discovered = await discoverSpecFiles(SPECS_DIR);
|
|
138
|
+
const specs = discovered
|
|
139
|
+
.map(({ id, specFile }) => {
|
|
140
|
+
try {
|
|
141
|
+
const spec = parseSpecFromFile(specFile, id);
|
|
142
|
+
return {
|
|
143
|
+
id,
|
|
144
|
+
title: spec.name,
|
|
145
|
+
requirementCount: spec.requirements.length
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return {
|
|
150
|
+
id,
|
|
151
|
+
title: id,
|
|
152
|
+
requirementCount: 0
|
|
153
|
+
};
|
|
156
154
|
}
|
|
157
|
-
return null;
|
|
158
155
|
})
|
|
159
|
-
.filter((spec) => spec !== null)
|
|
160
156
|
.sort((a, b) => a.id.localeCompare(b.id));
|
|
161
157
|
if (options.json) {
|
|
162
158
|
console.log(JSON.stringify(specs, null, 2));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ora from 'ora';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { Validator } from '../core/validation/validator.js';
|
|
4
|
+
import { VALIDATION_MESSAGES } from '../core/validation/constants.js';
|
|
4
5
|
import { resolveRootForCommand, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from '../core/root-selection.js';
|
|
5
6
|
import { isInteractive, resolveNoInteractive } from '../utils/interactive.js';
|
|
6
7
|
import { getSpecIds } from '../utils/item-discovery.js';
|
|
@@ -174,12 +175,26 @@ export class ValidateCommand {
|
|
|
174
175
|
const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
|
|
175
176
|
console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
|
|
176
177
|
}
|
|
177
|
-
this.printNextSteps(type, id, root);
|
|
178
|
+
this.printNextSteps(type, id, root, report.issues);
|
|
178
179
|
}
|
|
179
180
|
}
|
|
180
|
-
printNextSteps(type, id, root) {
|
|
181
|
+
printNextSteps(type, id, root, issues = []) {
|
|
181
182
|
const bullets = [];
|
|
182
|
-
|
|
183
|
+
// The delta-authoring bullets contradict a marker-related error ("add
|
|
184
|
+
// deltas" vs "remove skip_specs or the files"), so branch on the exact
|
|
185
|
+
// marker messages - the generic no-deltas guidance also mentions
|
|
186
|
+
// skip_specs, which must not trigger this.
|
|
187
|
+
const conflictIssue = issues.some(i => i.message.includes(VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_CONFLICT));
|
|
188
|
+
const invalidMarkerIssue = issues.some(i => i.message.includes(VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_INVALID_METADATA));
|
|
189
|
+
if (type === 'change' && conflictIssue) {
|
|
190
|
+
bullets.push('- This change declares skip_specs (no spec deltas): delete the files under specs/, or remove skip_specs from .openspec.yaml if requirements do change');
|
|
191
|
+
bullets.push('- skip_specs is only honored when .openspec.yaml is valid change metadata (schema: <name> naming a known schema is required)');
|
|
192
|
+
}
|
|
193
|
+
else if (type === 'change' && invalidMarkerIssue) {
|
|
194
|
+
bullets.push('- Fix .openspec.yaml so the skip_specs marker can be honored (schema: <name> naming a known schema is required)');
|
|
195
|
+
bullets.push('- Or remove skip_specs from .openspec.yaml and add delta specs instead');
|
|
196
|
+
}
|
|
197
|
+
else if (type === 'change') {
|
|
183
198
|
bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
|
|
184
199
|
bullets.push('- Each requirement MUST include at least one #### Scenario: block');
|
|
185
200
|
bullets.push(`- Debug parsed deltas: ${withStoreFlag(root, `openspec show ${id} --json --deltas-only`)}`);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { statusCommand } from './status.js';
|
|
7
7
|
export type { StatusOptions } from './status.js';
|
|
8
|
-
export { instructionsCommand, applyInstructionsCommand } from './instructions.js';
|
|
8
|
+
export { instructionsCommand, applyInstructionsCommand, archiveInstructionsCommand, } from './instructions.js';
|
|
9
9
|
export type { InstructionsOptions } from './instructions.js';
|
|
10
10
|
export { templatesCommand } from './templates.js';
|
|
11
11
|
export type { TemplatesOptions } from './templates.js';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Commands for the artifact-driven workflow: status, instructions, templates, schemas, new change.
|
|
5
5
|
*/
|
|
6
6
|
export { statusCommand } from './status.js';
|
|
7
|
-
export { instructionsCommand, applyInstructionsCommand } from './instructions.js';
|
|
7
|
+
export { instructionsCommand, applyInstructionsCommand, archiveInstructionsCommand, } 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';
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
import { type ArtifactInstructions } from '../../core/artifact-graph/index.js';
|
|
8
8
|
import { type PlanningHome } from '../../core/planning-home.js';
|
|
9
9
|
import { type ReferenceIndexEntry } from '../../core/references.js';
|
|
10
|
-
import { type
|
|
10
|
+
import { type ProjectConfig } from '../../core/project-config.js';
|
|
11
|
+
import { type ApplyInstructions, type ArchiveInstructions } from './shared.js';
|
|
11
12
|
export interface InstructionsOptions {
|
|
12
13
|
change?: string;
|
|
13
14
|
schema?: string;
|
|
@@ -22,11 +23,13 @@ export interface ApplyInstructionsOptions {
|
|
|
22
23
|
storePath?: string;
|
|
23
24
|
json?: boolean;
|
|
24
25
|
}
|
|
26
|
+
export type ArchiveInstructionsOptions = ApplyInstructionsOptions;
|
|
25
27
|
export declare function instructionsCommand(artifactId: string | undefined, options: InstructionsOptions): Promise<void>;
|
|
26
28
|
export declare function printInstructionsText(instructions: ArtifactInstructions, isBlocked: boolean): void;
|
|
27
29
|
export interface GenerateApplyInstructionsOptions {
|
|
28
30
|
planningHome?: PlanningHome;
|
|
29
31
|
references?: ReferenceIndexEntry[];
|
|
32
|
+
projectConfig?: ProjectConfig | null;
|
|
30
33
|
}
|
|
31
34
|
/**
|
|
32
35
|
* Generates apply instructions for implementing tasks from a change.
|
|
@@ -36,4 +39,7 @@ export interface GenerateApplyInstructionsOptions {
|
|
|
36
39
|
export declare function generateApplyInstructions(projectRoot: string, changeName: string, schemaName?: string, options?: GenerateApplyInstructionsOptions): Promise<ApplyInstructions>;
|
|
37
40
|
export declare function applyInstructionsCommand(options: ApplyInstructionsOptions): Promise<void>;
|
|
38
41
|
export declare function printApplyInstructionsText(instructions: ApplyInstructions): void;
|
|
42
|
+
export declare function generateArchiveInstructions(changeName: string, projectConfig: ProjectConfig | null): ArchiveInstructions;
|
|
43
|
+
export declare function archiveInstructionsCommand(options: ArchiveInstructionsOptions): Promise<void>;
|
|
44
|
+
export declare function printArchiveInstructionsText(instructions: ArchiveInstructions): void;
|
|
39
45
|
//# sourceMappingURL=instructions.d.ts.map
|
|
@@ -12,7 +12,7 @@ import { getChangeDir, resolveCurrentPlanningHomeSync, } from '../../core/planni
|
|
|
12
12
|
import { resolveRootForCommand, withStoreFlag, toPlanningHome, toRootOutput, } from '../../core/root-selection.js';
|
|
13
13
|
import { assembleReferenceIndex, renderReferencedStoresBlock, renderReferencedStoresSection, } from '../../core/references.js';
|
|
14
14
|
import { readRegistrySnapshot } from '../../core/store/registry.js';
|
|
15
|
-
import { readProjectConfig } from '../../core/project-config.js';
|
|
15
|
+
import { loadOperationInputs, readProjectConfig, } from '../../core/project-config.js';
|
|
16
16
|
import { validateChangeExists, validateSchemaExists, } from './shared.js';
|
|
17
17
|
// -----------------------------------------------------------------------------
|
|
18
18
|
// Artifact Instructions Command
|
|
@@ -55,10 +55,12 @@ export async function instructionsCommand(artifactId, options) {
|
|
|
55
55
|
if (options.schema) {
|
|
56
56
|
validateSchemaExists(options.schema, projectRoot);
|
|
57
57
|
}
|
|
58
|
+
const { projectConfig, references } = await loadRootConfigContext(root);
|
|
58
59
|
// loadChangeContext will auto-detect schema from metadata if not provided
|
|
59
60
|
const context = loadChangeContext(projectRoot, changeName, options.schema, {
|
|
60
61
|
changeDir: getChangeDir(planningHome, changeName),
|
|
61
62
|
planningHome,
|
|
63
|
+
projectConfig,
|
|
62
64
|
});
|
|
63
65
|
if (!artifactId) {
|
|
64
66
|
spinner?.stop();
|
|
@@ -71,7 +73,6 @@ export async function instructionsCommand(artifactId, options) {
|
|
|
71
73
|
const validIds = context.graph.getAllArtifacts().map((a) => a.id);
|
|
72
74
|
throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'. Valid artifacts:\n ${validIds.join('\n ')}`);
|
|
73
75
|
}
|
|
74
|
-
const { projectConfig, references } = await loadRootConfigContext(root);
|
|
75
76
|
const instructions = generateInstructions(context, artifactId, projectRoot, {
|
|
76
77
|
projectConfig,
|
|
77
78
|
references,
|
|
@@ -94,6 +95,17 @@ export function printInstructionsText(instructions, isBlocked) {
|
|
|
94
95
|
// Opening tag
|
|
95
96
|
console.log(`<artifact id="${artifactId}" change="${changeName}" schema="${schemaName}">`);
|
|
96
97
|
console.log();
|
|
98
|
+
// Artifacts skipped via skip_specs get no creation directive: emitting the
|
|
99
|
+
// task/template anyway would prompt an agent to write spec files that
|
|
100
|
+
// validate then rejects as conflicting with the marker.
|
|
101
|
+
if (instructions.skipped) {
|
|
102
|
+
console.log('<warning>');
|
|
103
|
+
console.log(instructions.warning ?? 'This artifact is skipped (skip_specs is set in .openspec.yaml).');
|
|
104
|
+
console.log('</warning>');
|
|
105
|
+
console.log();
|
|
106
|
+
console.log('</artifact>');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
97
109
|
// Warning for blocked artifacts
|
|
98
110
|
if (isBlocked) {
|
|
99
111
|
const missing = dependencies.filter((d) => !d.done).map((d) => d.id);
|
|
@@ -135,9 +147,18 @@ export function printInstructionsText(instructions, isBlocked) {
|
|
|
135
147
|
// Dependencies (files to read for context)
|
|
136
148
|
if (dependencies.length > 0) {
|
|
137
149
|
console.log('<dependencies>');
|
|
138
|
-
console.log('Read these files
|
|
150
|
+
console.log('Read the current contents of these files before creating this artifact (re-read them from disk even if you saw them earlier - they may have been edited):');
|
|
139
151
|
console.log();
|
|
140
152
|
for (const dep of dependencies) {
|
|
153
|
+
// A dependency satisfied via skip_specs has no files by design: telling
|
|
154
|
+
// the agent to read them (or calling them "done") would send it hunting
|
|
155
|
+
// for spec files that must not exist.
|
|
156
|
+
if (dep.skipped) {
|
|
157
|
+
console.log(`<dependency id="${dep.id}" status="skipped">`);
|
|
158
|
+
console.log(` <description>Skipped: the change declares skip_specs, so this artifact has no files to read.</description>`);
|
|
159
|
+
console.log('</dependency>');
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
141
162
|
const status = dep.done ? 'done' : 'missing';
|
|
142
163
|
const fullPath = path.join(changeDir, dep.path);
|
|
143
164
|
console.log(`<dependency id="${dep.id}" status="${status}">`);
|
|
@@ -219,6 +240,7 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
219
240
|
const context = loadChangeContext(projectRoot, changeName, schemaName, {
|
|
220
241
|
changeDir: getChangeDir(planningHome, changeName),
|
|
221
242
|
planningHome,
|
|
243
|
+
projectConfig: options.projectConfig,
|
|
222
244
|
});
|
|
223
245
|
const changeDir = context.changeDir;
|
|
224
246
|
// Get the full schema to access the apply phase configuration
|
|
@@ -229,9 +251,15 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
229
251
|
const requiredArtifactIds = applyConfig?.requires ?? schema.artifacts.map((a) => a.id);
|
|
230
252
|
const tracksFile = applyConfig?.tracks ?? null;
|
|
231
253
|
const schemaInstruction = applyConfig?.instruction ?? null;
|
|
232
|
-
|
|
254
|
+
const operationInputs = loadOperationInputs(options.projectConfig ?? null, 'apply');
|
|
255
|
+
// Check which required artifacts are missing. Artifacts the change skips
|
|
256
|
+
// via skip_specs count as present - their files must not exist, and
|
|
257
|
+
// status already reports them complete, so apply cannot block on them.
|
|
233
258
|
const missingArtifacts = [];
|
|
234
259
|
for (const artifactId of requiredArtifactIds) {
|
|
260
|
+
if (context.skippedArtifacts?.has(artifactId)) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
235
263
|
const artifact = schema.artifacts.find((a) => a.id === artifactId);
|
|
236
264
|
if (artifact && resolveArtifactOutputs(changeDir, artifact.generates).length === 0) {
|
|
237
265
|
missingArtifacts.push(artifactId);
|
|
@@ -303,6 +331,7 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
303
331
|
missingArtifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
|
|
304
332
|
instruction,
|
|
305
333
|
...(references !== undefined ? { references } : {}),
|
|
334
|
+
...operationInputs,
|
|
306
335
|
};
|
|
307
336
|
}
|
|
308
337
|
export async function applyInstructionsCommand(options) {
|
|
@@ -320,11 +349,13 @@ export async function applyInstructionsCommand(options) {
|
|
|
320
349
|
if (options.schema) {
|
|
321
350
|
validateSchemaExists(options.schema, projectRoot);
|
|
322
351
|
}
|
|
323
|
-
//
|
|
324
|
-
|
|
352
|
+
// One parsed config snapshot supplies schema fallback, references, context,
|
|
353
|
+
// and operation guidance for this command.
|
|
354
|
+
const { projectConfig, references } = await loadRootConfigContext(root);
|
|
325
355
|
const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema, {
|
|
326
356
|
planningHome,
|
|
327
357
|
references,
|
|
358
|
+
projectConfig,
|
|
328
359
|
});
|
|
329
360
|
spinner?.stop();
|
|
330
361
|
if (options.json) {
|
|
@@ -389,5 +420,57 @@ export function printApplyInstructionsText(instructions) {
|
|
|
389
420
|
// Instruction
|
|
390
421
|
console.log('### Instruction');
|
|
391
422
|
console.log(instruction);
|
|
423
|
+
console.log();
|
|
424
|
+
printOperationInputsText(instructions);
|
|
425
|
+
}
|
|
426
|
+
export function generateArchiveInstructions(changeName, projectConfig) {
|
|
427
|
+
return {
|
|
428
|
+
changeName,
|
|
429
|
+
...loadOperationInputs(projectConfig, 'archive'),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
export async function archiveInstructionsCommand(options) {
|
|
433
|
+
const root = await resolveRootForCommand(options, { json: options.json });
|
|
434
|
+
if (!root) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const spinner = options.json ? undefined : ora('Loading archive inputs...').start();
|
|
438
|
+
try {
|
|
439
|
+
const changeName = await validateChangeExists(options.change, root.path, root.changesDir, { newChangeHint: withStoreFlag(root, 'openspec new change <name>') });
|
|
440
|
+
const projectConfig = readProjectConfig(root.path);
|
|
441
|
+
const instructions = generateArchiveInstructions(changeName, projectConfig);
|
|
442
|
+
spinner?.stop();
|
|
443
|
+
if (options.json) {
|
|
444
|
+
console.log(JSON.stringify({ ...instructions, root: toRootOutput(root) }, null, 2));
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
printArchiveInstructionsText(instructions);
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
spinner?.stop();
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
export function printArchiveInstructionsText(instructions) {
|
|
455
|
+
console.log(`## Archive Inputs: ${instructions.changeName}`);
|
|
456
|
+
console.log();
|
|
457
|
+
printOperationInputsText(instructions);
|
|
458
|
+
}
|
|
459
|
+
function printOperationInputsText(inputs) {
|
|
460
|
+
if (inputs.context) {
|
|
461
|
+
console.log('### Project Context (required instruction input)');
|
|
462
|
+
console.log(inputs.context);
|
|
463
|
+
console.log();
|
|
464
|
+
}
|
|
465
|
+
if (inputs.operationGuidance && inputs.operationGuidance.length > 0) {
|
|
466
|
+
console.log('### Operation Guidance (advisory)');
|
|
467
|
+
for (const guidance of inputs.operationGuidance) {
|
|
468
|
+
console.log(`- ${guidance}`);
|
|
469
|
+
}
|
|
470
|
+
console.log();
|
|
471
|
+
}
|
|
472
|
+
if (!inputs.context && !inputs.operationGuidance) {
|
|
473
|
+
console.log('No project context or operation guidance configured.');
|
|
474
|
+
}
|
|
392
475
|
}
|
|
393
476
|
//# sourceMappingURL=instructions.js.map
|
|
@@ -33,6 +33,17 @@ export interface ApplyInstructions {
|
|
|
33
33
|
instruction: string;
|
|
34
34
|
/** Referenced-store index (read-only upstream context; omitted when none declared) */
|
|
35
35
|
references?: ReferenceIndexEntry[];
|
|
36
|
+
/** Current project background from the selected root. */
|
|
37
|
+
context?: string;
|
|
38
|
+
/** Current advisory guidance for apply. */
|
|
39
|
+
operationGuidance?: string[];
|
|
40
|
+
}
|
|
41
|
+
export interface ArchiveInstructions {
|
|
42
|
+
changeName: string;
|
|
43
|
+
/** Current project background from the selected root. */
|
|
44
|
+
context?: string;
|
|
45
|
+
/** Current advisory guidance for archive. */
|
|
46
|
+
operationGuidance?: string[];
|
|
36
47
|
}
|
|
37
48
|
export declare const DEFAULT_SCHEMA = "spec-driven";
|
|
38
49
|
export declare function printJson(payload: unknown): void;
|
|
@@ -44,11 +55,11 @@ export declare function isColorDisabled(): boolean;
|
|
|
44
55
|
/**
|
|
45
56
|
* Gets the color function based on status.
|
|
46
57
|
*/
|
|
47
|
-
export declare function getStatusColor(status: 'done' | 'ready' | 'blocked'): (text: string) => string;
|
|
58
|
+
export declare function getStatusColor(status: 'done' | 'skipped' | 'ready' | 'blocked'): (text: string) => string;
|
|
48
59
|
/**
|
|
49
60
|
* Gets the status indicator for an artifact.
|
|
50
61
|
*/
|
|
51
|
-
export declare function getStatusIndicator(status: 'done' | 'ready' | 'blocked'): string;
|
|
62
|
+
export declare function getStatusIndicator(status: 'done' | 'skipped' | 'ready' | 'blocked'): string;
|
|
52
63
|
/**
|
|
53
64
|
* Returns the list of available change directory names under openspec/changes/.
|
|
54
65
|
* Excludes the archive directory and hidden directories.
|
|
@@ -9,7 +9,6 @@ import path from 'path';
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import { getSchemaDir, listSchemas } from '../../core/artifact-graph/index.js';
|
|
11
11
|
import { isRootSelectionError } from '../../core/root-selection.js';
|
|
12
|
-
import { validateChangeName } from '../../utils/change-utils.js';
|
|
13
12
|
// -----------------------------------------------------------------------------
|
|
14
13
|
// Constants
|
|
15
14
|
// -----------------------------------------------------------------------------
|
|
@@ -46,6 +45,8 @@ export function getStatusColor(status) {
|
|
|
46
45
|
switch (status) {
|
|
47
46
|
case 'done':
|
|
48
47
|
return chalk.green;
|
|
48
|
+
case 'skipped':
|
|
49
|
+
return chalk.gray;
|
|
49
50
|
case 'ready':
|
|
50
51
|
return chalk.yellow;
|
|
51
52
|
case 'blocked':
|
|
@@ -60,6 +61,8 @@ export function getStatusIndicator(status) {
|
|
|
60
61
|
switch (status) {
|
|
61
62
|
case 'done':
|
|
62
63
|
return color('[x]');
|
|
64
|
+
case 'skipped':
|
|
65
|
+
return color('[~]');
|
|
63
66
|
case 'ready':
|
|
64
67
|
return color('[ ]');
|
|
65
68
|
case 'blocked':
|
|
@@ -84,6 +87,33 @@ export async function getAvailableChanges(projectRoot, changesDir = path.join(pr
|
|
|
84
87
|
throw error;
|
|
85
88
|
}
|
|
86
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Validates a change name used to look up an existing change directory.
|
|
92
|
+
* Lookup accepts any directory name that `getAvailableChanges` could return
|
|
93
|
+
* (the kebab-case convention in `validateChangeName` applies at creation
|
|
94
|
+
* time only); it only rejects names that would escape the changes directory
|
|
95
|
+
* or address entries `getAvailableChanges` excludes (hidden dirs, archive).
|
|
96
|
+
*
|
|
97
|
+
* @returns An error message, or undefined if the name is safe to look up
|
|
98
|
+
*/
|
|
99
|
+
function validateChangeLookupName(changeName) {
|
|
100
|
+
if (changeName === '.' || changeName === '..') {
|
|
101
|
+
return 'Change name cannot be a relative path segment';
|
|
102
|
+
}
|
|
103
|
+
if (changeName.includes('/') || changeName.includes('\\')) {
|
|
104
|
+
return 'Change name cannot contain path separators';
|
|
105
|
+
}
|
|
106
|
+
if (changeName.includes('\0')) {
|
|
107
|
+
return 'Change name cannot contain null characters';
|
|
108
|
+
}
|
|
109
|
+
if (changeName.startsWith('.')) {
|
|
110
|
+
return 'Change name cannot start with a dot';
|
|
111
|
+
}
|
|
112
|
+
if (changeName === 'archive') {
|
|
113
|
+
return "'archive' is reserved for archived changes";
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
87
117
|
/**
|
|
88
118
|
* Validates that a change exists and returns available changes if not.
|
|
89
119
|
* Checks directory existence directly to support scaffolded changes (without proposal.md).
|
|
@@ -100,9 +130,9 @@ export async function validateChangeExists(changeName, projectRoot, changesDir =
|
|
|
100
130
|
throw new Error(`Missing required option --change. Available changes:\n ${available.join('\n ')}`);
|
|
101
131
|
}
|
|
102
132
|
// Validate change name format to prevent path traversal
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
105
|
-
throw new Error(`Invalid change name '${changeName}': ${
|
|
133
|
+
const lookupError = validateChangeLookupName(changeName);
|
|
134
|
+
if (lookupError) {
|
|
135
|
+
throw new Error(`Invalid change name '${changeName}': ${lookupError}`);
|
|
106
136
|
}
|
|
107
137
|
// Check directory existence directly
|
|
108
138
|
const changePath = path.join(changesDir, changeName);
|
|
@@ -67,18 +67,23 @@ export async function statusCommand(options) {
|
|
|
67
67
|
}
|
|
68
68
|
export function printStatusText(status) {
|
|
69
69
|
const doneCount = status.artifacts.filter((a) => a.status === 'done').length;
|
|
70
|
-
const
|
|
70
|
+
const skippedCount = status.artifacts.filter((a) => a.status === 'skipped').length;
|
|
71
|
+
const total = status.artifacts.length - skippedCount;
|
|
71
72
|
console.log(`Change: ${status.changeName}`);
|
|
72
73
|
console.log(`Schema: ${status.schemaName}`);
|
|
73
74
|
if (status.changeRoot) {
|
|
74
75
|
console.log(`Change root: ${status.changeRoot}`);
|
|
75
76
|
}
|
|
76
|
-
|
|
77
|
+
const skippedSuffix = skippedCount > 0 ? ` (${skippedCount} skipped)` : '';
|
|
78
|
+
console.log(`Progress: ${doneCount}/${total} artifacts complete${skippedSuffix}`);
|
|
77
79
|
console.log();
|
|
78
80
|
for (const artifact of status.artifacts) {
|
|
79
81
|
const indicator = getStatusIndicator(artifact.status);
|
|
80
82
|
const color = getStatusColor(artifact.status);
|
|
81
83
|
let line = `${indicator} ${artifact.id}`;
|
|
84
|
+
if (artifact.status === 'skipped') {
|
|
85
|
+
line += color(' (skipped: change declares skip_specs)');
|
|
86
|
+
}
|
|
82
87
|
if (artifact.status === 'blocked' && artifact.missingDeps && artifact.missingDeps.length > 0) {
|
|
83
88
|
line += color(` (blocked by: ${artifact.missingDeps.join(', ')})`);
|
|
84
89
|
}
|