@fission-ai/openspec 1.8.0 → 1.10.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/dist/cli/index.d.ts +32 -0
- package/dist/cli/index.js +80 -9
- package/dist/commands/change.js +1 -0
- package/dist/commands/config.d.ts +5 -0
- package/dist/commands/config.js +19 -4
- package/dist/commands/feedback.js +28 -7
- package/dist/commands/schema.js +193 -28
- package/dist/commands/validate.d.ts +23 -0
- package/dist/commands/validate.js +148 -4
- package/dist/commands/workflow/schemas.d.ts +2 -0
- package/dist/commands/workflow/schemas.js +9 -2
- package/dist/core/archive.js +74 -10
- package/dist/core/artifact-graph/instruction-loader.js +2 -7
- package/dist/core/artifact-graph/outputs.d.ts +4 -0
- package/dist/core/artifact-graph/outputs.js +7 -0
- package/dist/core/artifact-graph/resolver.d.ts +0 -12
- package/dist/core/artifact-graph/resolver.js +14 -0
- package/dist/core/command-generation/adapters/command-code.d.ts +19 -0
- package/dist/core/command-generation/adapters/command-code.js +34 -0
- package/dist/core/command-generation/adapters/index.d.ts +1 -0
- package/dist/core/command-generation/adapters/index.js +1 -0
- package/dist/core/command-generation/adapters/opencode.d.ts +2 -1
- package/dist/core/command-generation/adapters/opencode.js +13 -2
- package/dist/core/command-generation/registry.js +2 -0
- package/dist/core/completion-tip.d.ts +15 -0
- package/dist/core/completion-tip.js +133 -0
- package/dist/core/completions/command-registry.js +10 -0
- package/dist/core/completions/factory.d.ts +11 -0
- package/dist/core/completions/installers/bash-installer.d.ts +7 -0
- package/dist/core/completions/installers/bash-installer.js +15 -0
- package/dist/core/completions/installers/fish-installer.d.ts +7 -0
- package/dist/core/completions/installers/fish-installer.js +15 -0
- package/dist/core/completions/installers/powershell-installer.d.ts +7 -0
- package/dist/core/completions/installers/powershell-installer.js +15 -0
- package/dist/core/config-prompts.js +19 -10
- package/dist/core/config-schema.d.ts +1 -0
- package/dist/core/config-schema.js +2 -0
- package/dist/core/config.d.ts +1 -0
- package/dist/core/config.js +17 -15
- package/dist/core/global-config.d.ts +2 -0
- package/dist/core/init.d.ts +5 -0
- package/dist/core/init.js +110 -14
- package/dist/core/legacy-cleanup.d.ts +10 -0
- package/dist/core/legacy-cleanup.js +48 -26
- package/dist/core/parsers/requirement-blocks.js +41 -9
- package/dist/core/parsers/requirement-text.d.ts +8 -0
- package/dist/core/parsers/requirement-text.js +4 -3
- package/dist/core/profiles.d.ts +1 -1
- package/dist/core/profiles.js +11 -2
- package/dist/core/root-selection.d.ts +1 -1
- package/dist/core/shared-skill-target.d.ts +15 -0
- package/dist/core/shared-skill-target.js +30 -0
- package/dist/core/specs-apply.js +11 -4
- package/dist/core/templates/workflows/apply-change.js +3 -0
- package/dist/core/templates/workflows/feedback.js +1 -0
- package/dist/core/templates/workflows/onboard.js +4 -4
- package/dist/core/templates/workflows/propose.js +2 -2
- 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/update-change.js +4 -4
- package/dist/core/update.js +49 -11
- package/dist/core/validation/task-numbering.d.ts +16 -0
- package/dist/core/validation/task-numbering.js +57 -0
- package/dist/core/validation/validator.d.ts +5 -2
- package/dist/core/validation/validator.js +57 -3
- package/dist/telemetry/index.d.ts +3 -1
- package/dist/telemetry/index.js +10 -3
- package/dist/utils/change-metadata.js +17 -13
- package/dist/utils/change-utils.js +5 -0
- package/dist/utils/interactive.d.ts +30 -1
- package/dist/utils/interactive.js +105 -2
- package/dist/utils/task-progress.d.ts +34 -1
- package/dist/utils/task-progress.js +52 -28
- package/package.json +3 -6
- package/schemas/spec-driven/schema.yaml +17 -7
- package/scripts/postinstall.js +0 -83
package/dist/core/init.js
CHANGED
|
@@ -10,7 +10,7 @@ import ora from 'ora';
|
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import { createRequire } from 'module';
|
|
12
12
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
13
|
-
import { classifyOpenSpecDir, storePointerProblem } from './project-config.js';
|
|
13
|
+
import { classifyOpenSpecDir, MAX_CONTEXT_SIZE, readProjectConfig, storePointerProblem, } from './project-config.js';
|
|
14
14
|
import { findRepoPlanningRootSync } from './planning-home.js';
|
|
15
15
|
import { getSkillReferenceTransformer, getTransformerForTool, usesNaturalLanguageSkillReferences } from '../utils/command-references.js';
|
|
16
16
|
import { AI_TOOLS, OPENSPEC_DIR_NAME, resolveToolIdAlias, } from './config.js';
|
|
@@ -33,6 +33,13 @@ const { version: OPENSPEC_VERSION } = require('../../package.json');
|
|
|
33
33
|
// Constants
|
|
34
34
|
// -----------------------------------------------------------------------------
|
|
35
35
|
const DEFAULT_SCHEMA = 'spec-driven';
|
|
36
|
+
function formatLanguageContext(language) {
|
|
37
|
+
return [
|
|
38
|
+
`Language: ${language}`,
|
|
39
|
+
`All artifacts must be written in ${language}.`,
|
|
40
|
+
'Keep OpenSpec structural headings and SHALL/MUST keywords in English.',
|
|
41
|
+
].join('\n');
|
|
42
|
+
}
|
|
36
43
|
const PROGRESS_SPINNER = {
|
|
37
44
|
interval: 80,
|
|
38
45
|
frames: ['░░░', '▒░░', '▒▒░', '▒▒▒', '▓▒▒', '▓▓▒', '▓▓▓', '▒▓▓', '░▒▓'],
|
|
@@ -56,6 +63,7 @@ const WORKFLOW_TO_SKILL_DIR = {
|
|
|
56
63
|
// -----------------------------------------------------------------------------
|
|
57
64
|
export class InitCommand {
|
|
58
65
|
toolsArg;
|
|
66
|
+
language;
|
|
59
67
|
force;
|
|
60
68
|
interactiveOption;
|
|
61
69
|
profileOverride;
|
|
@@ -63,6 +71,7 @@ export class InitCommand {
|
|
|
63
71
|
copilotCloudOption;
|
|
64
72
|
constructor(options = {}) {
|
|
65
73
|
this.toolsArg = options.tools;
|
|
74
|
+
this.language = this.normalizeLanguage(options.language);
|
|
66
75
|
this.force = options.force ?? false;
|
|
67
76
|
this.interactiveOption = options.interactive;
|
|
68
77
|
this.profileOverride = options.profile;
|
|
@@ -97,6 +106,7 @@ export class InitCommand {
|
|
|
97
106
|
}
|
|
98
107
|
}
|
|
99
108
|
}
|
|
109
|
+
await this.assertLanguageCanBeApplied(projectPath, openspecPath);
|
|
100
110
|
// Check for legacy artifacts and handle cleanup
|
|
101
111
|
const deferredLegacyCleanup = await this.handleLegacyCleanup(projectPath, extendMode);
|
|
102
112
|
// Migrate OpenSpec-managed skills left in renamed tool directories
|
|
@@ -517,11 +527,31 @@ export class InitCommand {
|
|
|
517
527
|
}
|
|
518
528
|
validateTools(toolIds, toolStates, projectPath) {
|
|
519
529
|
const validatedTools = [];
|
|
520
|
-
const
|
|
521
|
-
|
|
530
|
+
const sharedAgentsTargets = ['codex', 'zed', 'agents'];
|
|
531
|
+
const selectedSharedTargets = sharedAgentsTargets.filter((toolId) => toolIds.includes(toolId));
|
|
532
|
+
// A Codex-rendered tree already serves Zed. Keep it when Zed is added later
|
|
533
|
+
// so Codex users do not lose the `$openspec-*` references they require.
|
|
534
|
+
const preserveConfiguredCodex = selectedSharedTargets.includes('zed') &&
|
|
535
|
+
toolStates.get('codex')?.configured;
|
|
536
|
+
const sharedTargetCandidates = preserveConfiguredCodex
|
|
537
|
+
? [...new Set([...selectedSharedTargets, 'codex'])]
|
|
538
|
+
: selectedSharedTargets;
|
|
539
|
+
const sharedTargetOwner = sharedTargetCandidates.includes('codex')
|
|
540
|
+
? 'codex'
|
|
541
|
+
: selectedSharedTargets.includes('zed')
|
|
542
|
+
? 'zed'
|
|
543
|
+
: selectedSharedTargets[0];
|
|
544
|
+
const firstSharedIndex = toolIds.findIndex((id) => sharedAgentsTargets.includes(id));
|
|
545
|
+
const reconciledToolIds = sharedTargetCandidates.length > 1
|
|
546
|
+
? toolIds.flatMap((toolId, index) => {
|
|
547
|
+
if (!sharedAgentsTargets.includes(toolId))
|
|
548
|
+
return [toolId];
|
|
549
|
+
return index === firstSharedIndex && sharedTargetOwner ? [sharedTargetOwner] : [];
|
|
550
|
+
})
|
|
522
551
|
: toolIds;
|
|
523
|
-
if (reconciledToolIds.length !== toolIds.length
|
|
524
|
-
|
|
552
|
+
if (reconciledToolIds.length !== toolIds.length ||
|
|
553
|
+
reconciledToolIds.some((toolId, index) => toolId !== toolIds[index])) {
|
|
554
|
+
console.log(chalk.dim(`Codex, Zed, and agents share .agents/skills; writing one tree for ${sharedTargetOwner}.`));
|
|
525
555
|
}
|
|
526
556
|
for (const toolId of reconciledToolIds) {
|
|
527
557
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
@@ -544,6 +574,7 @@ export class InitCommand {
|
|
|
544
574
|
skillsRoot: isGlobalSkillTarget ? skillsPath : projectPath,
|
|
545
575
|
isGlobalSkillTarget,
|
|
546
576
|
wasConfigured: preState?.configured ?? false,
|
|
577
|
+
requiresIdeRestart: tool.requiresIdeRestart,
|
|
547
578
|
});
|
|
548
579
|
}
|
|
549
580
|
return validatedTools;
|
|
@@ -698,6 +729,53 @@ export class InitCommand {
|
|
|
698
729
|
// ═══════════════════════════════════════════════════════════
|
|
699
730
|
// CONFIG FILE
|
|
700
731
|
// ═══════════════════════════════════════════════════════════
|
|
732
|
+
normalizeLanguage(language) {
|
|
733
|
+
if (language === undefined)
|
|
734
|
+
return undefined;
|
|
735
|
+
const normalized = language.trim();
|
|
736
|
+
if (!normalized) {
|
|
737
|
+
throw new Error('The --language option requires a non-empty value.');
|
|
738
|
+
}
|
|
739
|
+
if (/\p{Cc}|\p{Bidi_Control}|[\u200B\u2028\u2029\uFEFF]/u.test(normalized)) {
|
|
740
|
+
throw new Error('The --language option must be a single line without control or invisible formatting characters.');
|
|
741
|
+
}
|
|
742
|
+
const serializedContext = `${formatLanguageContext(normalized)}\n`;
|
|
743
|
+
if (Buffer.byteLength(serializedContext, 'utf8') > MAX_CONTEXT_SIZE) {
|
|
744
|
+
throw new Error(`The --language option is too long for OpenSpec's ${MAX_CONTEXT_SIZE / 1024}KB project context limit.`);
|
|
745
|
+
}
|
|
746
|
+
return normalized;
|
|
747
|
+
}
|
|
748
|
+
languageContext() {
|
|
749
|
+
if (!this.language)
|
|
750
|
+
return undefined;
|
|
751
|
+
return formatLanguageContext(this.language);
|
|
752
|
+
}
|
|
753
|
+
async assertLanguageCanBeApplied(projectPath, openspecPath) {
|
|
754
|
+
const languageContext = this.languageContext();
|
|
755
|
+
if (!languageContext)
|
|
756
|
+
return;
|
|
757
|
+
const configPath = path.join(openspecPath, 'config.yaml');
|
|
758
|
+
const hasConfig = fs.existsSync(configPath) ||
|
|
759
|
+
fs.existsSync(path.join(openspecPath, 'config.yml'));
|
|
760
|
+
if (!hasConfig) {
|
|
761
|
+
try {
|
|
762
|
+
FileSystemUtils.assertProjectArtifactPath(projectPath, configPath);
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
const reason = error instanceof Error ? `: ${error.message}` : '';
|
|
766
|
+
throw new Error(`Cannot create openspec/config.yaml for --language${reason}`);
|
|
767
|
+
}
|
|
768
|
+
if (!(await FileSystemUtils.canWriteFile(configPath))) {
|
|
769
|
+
throw new Error('Cannot create openspec/config.yaml for --language: the destination is not writable.');
|
|
770
|
+
}
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const existingContext = readProjectConfig(projectPath)?.context;
|
|
774
|
+
if (existingContext?.includes(languageContext))
|
|
775
|
+
return;
|
|
776
|
+
throw new Error('--language does not overwrite an existing OpenSpec config. ' +
|
|
777
|
+
'Add the language instruction to its context field instead.');
|
|
778
|
+
}
|
|
701
779
|
async createConfig(openspecPath, extendMode) {
|
|
702
780
|
const configPath = path.join(openspecPath, 'config.yaml');
|
|
703
781
|
const configYmlPath = path.join(openspecPath, 'config.yml');
|
|
@@ -707,12 +785,19 @@ export class InitCommand {
|
|
|
707
785
|
return 'exists';
|
|
708
786
|
}
|
|
709
787
|
try {
|
|
710
|
-
const yamlContent = serializeConfig({
|
|
788
|
+
const yamlContent = serializeConfig({
|
|
789
|
+
schema: DEFAULT_SCHEMA,
|
|
790
|
+
context: this.languageContext(),
|
|
791
|
+
});
|
|
711
792
|
FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), configPath);
|
|
712
793
|
await FileSystemUtils.writeFile(configPath, yamlContent);
|
|
713
794
|
return 'created';
|
|
714
795
|
}
|
|
715
|
-
catch {
|
|
796
|
+
catch (error) {
|
|
797
|
+
if (this.language) {
|
|
798
|
+
const reason = error instanceof Error ? `: ${error.message}` : '';
|
|
799
|
+
throw new Error(`Failed to create openspec/config.yaml for --language${reason}`);
|
|
800
|
+
}
|
|
716
801
|
return 'skipped';
|
|
717
802
|
}
|
|
718
803
|
}
|
|
@@ -930,14 +1015,25 @@ export class InitCommand {
|
|
|
930
1015
|
console.log();
|
|
931
1016
|
console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
|
|
932
1017
|
console.log(`Feedback: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec/issues')}`);
|
|
933
|
-
// Restart instruction
|
|
934
|
-
//
|
|
935
|
-
//
|
|
936
|
-
//
|
|
937
|
-
//
|
|
938
|
-
|
|
1018
|
+
// Restart instruction only when at least one IDE/editor-resident tool
|
|
1019
|
+
// actually received a generated surface. Two conditions, coupled to the SAME
|
|
1020
|
+
// tool: (1) its commands/skills are loaded by a long-running editor process
|
|
1021
|
+
// (CLI tools pick the files up immediately, so a restart line would be wrong
|
|
1022
|
+
// for them — see #1067), and (2) a surface was actually generated for it
|
|
1023
|
+
// under the active delivery (an IDE tool that generated nothing has nothing a
|
|
1024
|
+
// restart would pick up, even if a co-configured CLI tool did generate).
|
|
1025
|
+
// Wording follows what the IDE tool itself generated, not the global
|
|
1026
|
+
// aggregate: it must not say "commands" when the IDE tool only got skills
|
|
1027
|
+
// while a co-configured CLI tool got commands. Not "slash commands" either:
|
|
1028
|
+
// Amazon Q's generated files are prompt-library entries invoked with @, so a
|
|
1029
|
+
// restart line promising slash commands would be wrong for it.
|
|
1030
|
+
const restartCommandsGenerated = successfulTools.some((tool) => tool.requiresIdeRestart &&
|
|
1031
|
+
shouldGenerateCommandsForTool(tool.value, activeDelivery));
|
|
1032
|
+
const restartSkillsGenerated = successfulTools.some((tool) => tool.requiresIdeRestart &&
|
|
1033
|
+
shouldGenerateSkillsForTool(tool.value, activeDelivery));
|
|
1034
|
+
if (restartCommandsGenerated || restartSkillsGenerated) {
|
|
939
1035
|
console.log();
|
|
940
|
-
console.log(chalk.white(
|
|
1036
|
+
console.log(chalk.white(restartCommandsGenerated
|
|
941
1037
|
? 'Restart your IDE for the new commands to take effect.'
|
|
942
1038
|
: 'Restart your IDE for the new skills to take effect.'));
|
|
943
1039
|
}
|
|
@@ -206,6 +206,16 @@ export declare function getLegacyWorkflowIdsForTool(detection: LegacyDetectionRe
|
|
|
206
206
|
* callers can safely perform the immediate, non-deferred cleanup pass.
|
|
207
207
|
*/
|
|
208
208
|
export declare function omitGlobalLegacyPromptFiles(detection: LegacyDetectionResult): LegacyDetectionResult;
|
|
209
|
+
/**
|
|
210
|
+
* Returns a detection snapshot with the repo-local slash-command artifacts of
|
|
211
|
+
* the given tools removed. The legacy-upgrade path uses this to skip cleaning a
|
|
212
|
+
* tool's legacy files when its replacement was deliberately NOT written — e.g. a
|
|
213
|
+
* Codex upgrade suppressed because the shared `.agents` root is already owned by
|
|
214
|
+
* another tool. Deleting the legacy prompt without writing its replacement would
|
|
215
|
+
* violate the cleanup contract ("remove X because replacement Y now exists") and
|
|
216
|
+
* strip the tool's only OpenSpec integration.
|
|
217
|
+
*/
|
|
218
|
+
export declare function omitToolLegacyArtifacts(detection: LegacyDetectionResult, toolIds: readonly string[]): LegacyDetectionResult;
|
|
209
219
|
/**
|
|
210
220
|
* Builds a detection snapshot containing only the selected global Codex prompt
|
|
211
221
|
* matches for replacement-gated cleanup.
|
|
@@ -614,41 +614,42 @@ export function formatDeferredGlobalPromptSummary(detection) {
|
|
|
614
614
|
*/
|
|
615
615
|
export function getToolsFromLegacyArtifacts(detection) {
|
|
616
616
|
const tools = new Set();
|
|
617
|
-
// Match directories to tool IDs
|
|
618
617
|
for (const dir of detection.slashCommandDirs) {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
break;
|
|
623
|
-
}
|
|
624
|
-
}
|
|
618
|
+
const toolId = legacyToolIdForDir(dir);
|
|
619
|
+
if (toolId)
|
|
620
|
+
tools.add(toolId);
|
|
625
621
|
}
|
|
626
|
-
// Match files to tool IDs using glob patterns
|
|
627
622
|
for (const file of detection.slashCommandFiles) {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
if (pattern.type === 'files' && pattern.pattern) {
|
|
632
|
-
const patterns = Array.isArray(pattern.pattern) ? pattern.pattern : [pattern.pattern];
|
|
633
|
-
let matched = false;
|
|
634
|
-
for (const p of patterns) {
|
|
635
|
-
const regex = globToRegex(p);
|
|
636
|
-
if (regex.test(normalizedFile)) {
|
|
637
|
-
tools.add(toolId);
|
|
638
|
-
matched = true;
|
|
639
|
-
break;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
if (matched)
|
|
643
|
-
break;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
623
|
+
const toolId = legacyToolIdForFile(file);
|
|
624
|
+
if (toolId)
|
|
625
|
+
tools.add(toolId);
|
|
646
626
|
}
|
|
647
627
|
for (const prompt of getLegacyGlobalPromptMatches(detection)) {
|
|
648
628
|
tools.add(prompt.toolId);
|
|
649
629
|
}
|
|
650
630
|
return Array.from(tools);
|
|
651
631
|
}
|
|
632
|
+
/** The tool that owns a repo-local legacy slash-command directory, if any. */
|
|
633
|
+
function legacyToolIdForDir(dir) {
|
|
634
|
+
for (const [toolId, pattern] of Object.entries(LEGACY_SLASH_COMMAND_PATHS)) {
|
|
635
|
+
if (pattern.type === 'directory' && pattern.path === dir)
|
|
636
|
+
return toolId;
|
|
637
|
+
}
|
|
638
|
+
return undefined;
|
|
639
|
+
}
|
|
640
|
+
/** The tool that owns a repo-local legacy slash-command file, if any. */
|
|
641
|
+
function legacyToolIdForFile(file) {
|
|
642
|
+
// Normalize to forward slashes so the glob patterns match on Windows too.
|
|
643
|
+
const normalizedFile = normalizePathForMatch(file);
|
|
644
|
+
for (const [toolId, pattern] of Object.entries(LEGACY_SLASH_COMMAND_PATHS)) {
|
|
645
|
+
if (pattern.type !== 'files' || !pattern.pattern)
|
|
646
|
+
continue;
|
|
647
|
+
const patterns = Array.isArray(pattern.pattern) ? pattern.pattern : [pattern.pattern];
|
|
648
|
+
if (patterns.some((p) => globToRegex(p).test(normalizedFile)))
|
|
649
|
+
return toolId;
|
|
650
|
+
}
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
652
653
|
/**
|
|
653
654
|
* Normalizes global Codex prompt matches so callers can rely on workflow-aware
|
|
654
655
|
* metadata even when older detection results only carry file paths.
|
|
@@ -699,6 +700,27 @@ export function omitGlobalLegacyPromptFiles(detection) {
|
|
|
699
700
|
nextDetection.hasLegacyArtifacts = hasLegacyArtifacts(nextDetection);
|
|
700
701
|
return nextDetection;
|
|
701
702
|
}
|
|
703
|
+
/**
|
|
704
|
+
* Returns a detection snapshot with the repo-local slash-command artifacts of
|
|
705
|
+
* the given tools removed. The legacy-upgrade path uses this to skip cleaning a
|
|
706
|
+
* tool's legacy files when its replacement was deliberately NOT written — e.g. a
|
|
707
|
+
* Codex upgrade suppressed because the shared `.agents` root is already owned by
|
|
708
|
+
* another tool. Deleting the legacy prompt without writing its replacement would
|
|
709
|
+
* violate the cleanup contract ("remove X because replacement Y now exists") and
|
|
710
|
+
* strip the tool's only OpenSpec integration.
|
|
711
|
+
*/
|
|
712
|
+
export function omitToolLegacyArtifacts(detection, toolIds) {
|
|
713
|
+
if (toolIds.length === 0)
|
|
714
|
+
return detection;
|
|
715
|
+
const skip = new Set(toolIds);
|
|
716
|
+
const nextDetection = {
|
|
717
|
+
...detection,
|
|
718
|
+
slashCommandDirs: detection.slashCommandDirs.filter((dir) => !skip.has(legacyToolIdForDir(dir) ?? '')),
|
|
719
|
+
slashCommandFiles: detection.slashCommandFiles.filter((file) => !skip.has(legacyToolIdForFile(file) ?? '')),
|
|
720
|
+
};
|
|
721
|
+
nextDetection.hasLegacyArtifacts = hasLegacyArtifacts(nextDetection);
|
|
722
|
+
return nextDetection;
|
|
723
|
+
}
|
|
702
724
|
/**
|
|
703
725
|
* Builds a detection snapshot containing only the selected global Codex prompt
|
|
704
726
|
* matches for replacement-gated cleanup.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildCodeFenceMask } from './requirement-text.js';
|
|
1
|
+
import { buildCodeFenceMask, SCENARIO_HEADER } from './requirement-text.js';
|
|
2
2
|
export function normalizeRequirementName(name) {
|
|
3
3
|
return name.trim();
|
|
4
4
|
}
|
|
@@ -289,25 +289,57 @@ export function findMissingCurrentScenarios(current, incoming) {
|
|
|
289
289
|
}
|
|
290
290
|
return missing;
|
|
291
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Any non-fenced level-4 header on the given (masked) line. Reuses the spec
|
|
294
|
+
* path's SCENARIO_HEADER so the two counters cannot drift apart.
|
|
295
|
+
*/
|
|
296
|
+
function scenarioHeaderAt(lines, mask, index) {
|
|
297
|
+
return !mask[index] && SCENARIO_HEADER.test(lines[index]);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* The scenario name for a `#### ` header, matching the label the author reads:
|
|
301
|
+
* the header text with the leading `####`, an optional CommonMark closing `#`
|
|
302
|
+
* run (`#### Foo ####` renders as `Foo`), and an optional `Scenario:` prefix
|
|
303
|
+
* stripped. Both the current and incoming blocks run through here, so the
|
|
304
|
+
* comparison in findMissingCurrentScenarios stays internally consistent
|
|
305
|
+
* regardless of label — and two headers that render to the same title (one
|
|
306
|
+
* ATX-closed, one not) are not mistaken for a dropped scenario.
|
|
307
|
+
*/
|
|
308
|
+
function scenarioNameAt(line) {
|
|
309
|
+
return line
|
|
310
|
+
.replace(SCENARIO_HEADER, '')
|
|
311
|
+
// Optional ATX closing sequence. CommonMark only treats a trailing `#` run
|
|
312
|
+
// as a close when it is preceded by a space or tab — not any Unicode space —
|
|
313
|
+
// so this uses `[ \t]`, not `\s`. A looser `\s` could strip a `#` run after
|
|
314
|
+
// an exotic space (e.g. NBSP) that CommonMark keeps, folding two distinct
|
|
315
|
+
// scenario names into one and masking a real loss. `[ \t]` keeps the fold
|
|
316
|
+
// faithful to how the header actually renders.
|
|
317
|
+
.replace(/[ \t]+#+[ \t]*$/, '')
|
|
318
|
+
.replace(/^Scenario:\s*/i, '')
|
|
319
|
+
.trim();
|
|
320
|
+
}
|
|
292
321
|
function parseScenarioBlocks(requirementRaw) {
|
|
293
322
|
const lines = requirementRaw.replace(/\r\n?/g, '\n').split('\n');
|
|
294
|
-
// A
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
323
|
+
// A scenario is ANY non-fenced `#### ` header, matching the spec path's
|
|
324
|
+
// SCENARIO_HEADER / countScenarios (requirement-text.ts) exactly — not only
|
|
325
|
+
// `#### Scenario:`. The two MUST agree: a level-4 child whose header is not
|
|
326
|
+
// literally `Scenario:` (e.g. `#### Edge case`) is still a scenario the spec
|
|
327
|
+
// path counts, so a MODIFIED block that drops it would otherwise slip past
|
|
328
|
+
// this loss check and be deleted by archive with no error (the parity the
|
|
329
|
+
// SCENARIO_HEADER comment warns not to break). A `####` inside a fenced
|
|
330
|
+
// example is masked out, matching countScenarios.
|
|
298
331
|
const mask = buildCodeFenceMask(lines);
|
|
299
332
|
const scenarios = [];
|
|
300
333
|
let index = 0;
|
|
301
334
|
while (index < lines.length) {
|
|
302
|
-
|
|
303
|
-
if (!headerMatch) {
|
|
335
|
+
if (!scenarioHeaderAt(lines, mask, index)) {
|
|
304
336
|
index++;
|
|
305
337
|
continue;
|
|
306
338
|
}
|
|
307
339
|
const start = index;
|
|
308
|
-
const name =
|
|
340
|
+
const name = scenarioNameAt(lines[index]);
|
|
309
341
|
index++;
|
|
310
|
-
while (index < lines.length && (mask
|
|
342
|
+
while (index < lines.length && !scenarioHeaderAt(lines, mask, index)) {
|
|
311
343
|
index++;
|
|
312
344
|
}
|
|
313
345
|
scenarios.push({
|
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
* `validate <change>`, `validate <spec>`, and `archive`.
|
|
10
10
|
*/
|
|
11
11
|
export { buildCodeFenceMask } from './code-fence.js';
|
|
12
|
+
/**
|
|
13
|
+
* A level-4 header. Deliberately matches ANY `####` header, not only
|
|
14
|
+
* `#### Scenario:` — the spec path treats every level-4 child of a requirement
|
|
15
|
+
* as a scenario, so the delta counter must too (parity). The delta/loss path
|
|
16
|
+
* reuses this exact constant via `scenarioHeaderAt` in requirement-blocks.ts;
|
|
17
|
+
* keep both paths on it rather than reintroducing a separate `Scenario:` regex.
|
|
18
|
+
*/
|
|
19
|
+
export declare const SCENARIO_HEADER: RegExp;
|
|
12
20
|
/**
|
|
13
21
|
* The one predicate for normative-keyword detection. Matches `SHALL` or `MUST`
|
|
14
22
|
* as whole words so the change-delta reader and the schema-based reader accept
|
|
@@ -19,10 +19,11 @@ const HEADER_LINE = /^#{1,6}\s/;
|
|
|
19
19
|
/**
|
|
20
20
|
* A level-4 header. Deliberately matches ANY `####` header, not only
|
|
21
21
|
* `#### Scenario:` — the spec path treats every level-4 child of a requirement
|
|
22
|
-
* as a scenario, so the delta counter must too (parity).
|
|
23
|
-
*
|
|
22
|
+
* as a scenario, so the delta counter must too (parity). The delta/loss path
|
|
23
|
+
* reuses this exact constant via `scenarioHeaderAt` in requirement-blocks.ts;
|
|
24
|
+
* keep both paths on it rather than reintroducing a separate `Scenario:` regex.
|
|
24
25
|
*/
|
|
25
|
-
const SCENARIO_HEADER = /^####\s+/;
|
|
26
|
+
export const SCENARIO_HEADER = /^####\s+/;
|
|
26
27
|
/**
|
|
27
28
|
* The one predicate for normative-keyword detection. Matches `SHALL` or `MUST`
|
|
28
29
|
* as whole words so the change-delta reader and the schema-based reader accept
|
package/dist/core/profiles.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type CoreWorkflowId = (typeof CORE_WORKFLOWS)[number];
|
|
|
20
20
|
* Resolves which workflows should be active for a given profile configuration.
|
|
21
21
|
*
|
|
22
22
|
* - 'core' profile always returns CORE_WORKFLOWS
|
|
23
|
-
* - 'custom' profile returns the provided customWorkflows
|
|
23
|
+
* - 'custom' profile returns the provided customWorkflows and required dependencies
|
|
24
24
|
*/
|
|
25
25
|
export declare function getProfileWorkflows(profile: Profile, customWorkflows?: string[]): readonly string[];
|
|
26
26
|
//# sourceMappingURL=profiles.d.ts.map
|
package/dist/core/profiles.js
CHANGED
|
@@ -30,11 +30,20 @@ export const ALL_WORKFLOWS = [
|
|
|
30
30
|
* Resolves which workflows should be active for a given profile configuration.
|
|
31
31
|
*
|
|
32
32
|
* - 'core' profile always returns CORE_WORKFLOWS
|
|
33
|
-
* - 'custom' profile returns the provided customWorkflows
|
|
33
|
+
* - 'custom' profile returns the provided customWorkflows and required dependencies
|
|
34
34
|
*/
|
|
35
35
|
export function getProfileWorkflows(profile, customWorkflows) {
|
|
36
36
|
if (profile === 'custom') {
|
|
37
|
-
|
|
37
|
+
const workflows = customWorkflows ?? [];
|
|
38
|
+
const syncDependentIndex = workflows.findIndex((workflow) => workflow === 'archive' || workflow === 'bulk-archive');
|
|
39
|
+
if (syncDependentIndex !== -1 && !workflows.includes('sync')) {
|
|
40
|
+
return [
|
|
41
|
+
...workflows.slice(0, syncDependentIndex),
|
|
42
|
+
'sync',
|
|
43
|
+
...workflows.slice(syncDependentIndex),
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
return workflows;
|
|
38
47
|
}
|
|
39
48
|
return CORE_WORKFLOWS;
|
|
40
49
|
}
|
|
@@ -119,7 +119,7 @@ export declare function toPlanningHome(root: ResolvedOpenSpecRoot): PlanningHome
|
|
|
119
119
|
export declare function resolveRootForCommand(selector: StoreSelectorOptions, output?: {
|
|
120
120
|
json?: boolean;
|
|
121
121
|
failurePayload?: Record<string, unknown>;
|
|
122
|
-
/**
|
|
122
|
+
/** Commands that require an existing root set this to false. */
|
|
123
123
|
allowImplicitRoot?: boolean;
|
|
124
124
|
}): Promise<ResolvedOpenSpecRoot | null>;
|
|
125
125
|
//# sourceMappingURL=root-selection.d.ts.map
|
|
@@ -12,5 +12,20 @@ export declare function reconcileSharedSkillTargets(projectPath: string, tools:
|
|
|
12
12
|
* Non-shared roots are always active.
|
|
13
13
|
*/
|
|
14
14
|
export declare function isSharedSkillTargetActive(projectPath: string, toolId: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* The tool that already owns `toolId`'s shared skills root, when a DIFFERENT
|
|
17
|
+
* one does. Returns the owner's tool id only when the root already carries an
|
|
18
|
+
* ownership signal (a marker or generated skills) AND reconciliation resolves
|
|
19
|
+
* it to another tool. An empty or unclaimed root returns undefined, so a
|
|
20
|
+
* genuine first-time legacy upgrade — e.g. a Codex-only user with no `.agents`
|
|
21
|
+
* yet — is never reported as owned.
|
|
22
|
+
*/
|
|
23
|
+
export declare function sharedSkillRootOwner(projectPath: string, toolId: string): string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Whether generating `toolId` into its shared skills root would clobber a tree
|
|
26
|
+
* a DIFFERENT tool already owns — the guard the legacy-upgrade path uses before
|
|
27
|
+
* writing skills. See {@link sharedSkillRootOwner} for the ownership rules.
|
|
28
|
+
*/
|
|
29
|
+
export declare function sharedSkillRootOwnedByOther(projectPath: string, toolId: string): boolean;
|
|
15
30
|
export declare function writeSharedSkillTarget(projectPath: string, toolId: string): void;
|
|
16
31
|
//# sourceMappingURL=shared-skill-target.d.ts.map
|
|
@@ -141,6 +141,36 @@ export function isSharedSkillTargetActive(projectPath, toolId) {
|
|
|
141
141
|
return reconcileSharedSkillTargets(projectPath, sharingRoot)
|
|
142
142
|
.some((candidate) => candidate.value === toolId);
|
|
143
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* The tool that already owns `toolId`'s shared skills root, when a DIFFERENT
|
|
146
|
+
* one does. Returns the owner's tool id only when the root already carries an
|
|
147
|
+
* ownership signal (a marker or generated skills) AND reconciliation resolves
|
|
148
|
+
* it to another tool. An empty or unclaimed root returns undefined, so a
|
|
149
|
+
* genuine first-time legacy upgrade — e.g. a Codex-only user with no `.agents`
|
|
150
|
+
* yet — is never reported as owned.
|
|
151
|
+
*/
|
|
152
|
+
export function sharedSkillRootOwner(projectPath, toolId) {
|
|
153
|
+
const tool = AI_TOOLS.find((candidate) => candidate.value === toolId);
|
|
154
|
+
if (!tool?.skillsDir)
|
|
155
|
+
return undefined;
|
|
156
|
+
const sharingRoot = AI_TOOLS.filter((candidate) => candidate.skillsDir === tool.skillsDir);
|
|
157
|
+
if (sharingRoot.length < 2)
|
|
158
|
+
return undefined;
|
|
159
|
+
const hasOwnerSignal = readSharedSkillTarget(projectPath, tool.skillsDir) !== undefined ||
|
|
160
|
+
hasCurrentSkills(projectPath, tool.skillsDir);
|
|
161
|
+
if (!hasOwnerSignal)
|
|
162
|
+
return undefined;
|
|
163
|
+
const owner = reconcileSharedSkillTargets(projectPath, sharingRoot)[0]?.value;
|
|
164
|
+
return owner && owner !== toolId ? owner : undefined;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Whether generating `toolId` into its shared skills root would clobber a tree
|
|
168
|
+
* a DIFFERENT tool already owns — the guard the legacy-upgrade path uses before
|
|
169
|
+
* writing skills. See {@link sharedSkillRootOwner} for the ownership rules.
|
|
170
|
+
*/
|
|
171
|
+
export function sharedSkillRootOwnedByOther(projectPath, toolId) {
|
|
172
|
+
return sharedSkillRootOwner(projectPath, toolId) !== undefined;
|
|
173
|
+
}
|
|
144
174
|
export function writeSharedSkillTarget(projectPath, toolId) {
|
|
145
175
|
const tool = AI_TOOLS.find((candidate) => candidate.value === toolId);
|
|
146
176
|
if (!tool?.skillsDir)
|
package/dist/core/specs-apply.js
CHANGED
|
@@ -404,10 +404,17 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
404
404
|
.concat(keptOrder.map((b) => b.raw))
|
|
405
405
|
.join('\n\n')
|
|
406
406
|
.trimEnd();
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
407
|
+
// The blank lines surrounding `## Requirements` belong to no slice: `before`
|
|
408
|
+
// and `after` carry at most one trailing/leading '\n' by construction, and
|
|
409
|
+
// the body is trimEnd()ed. Joining the slices with a bare '\n' therefore
|
|
410
|
+
// glued the heading to the Purpose paragraph and the first requirement, so
|
|
411
|
+
// every archive rewrote a well-formatted spec into that shape. Separate
|
|
412
|
+
// non-empty slices with one blank line instead.
|
|
413
|
+
const rebuilt = [parts.before.trimEnd(), parts.headerLine, reqBody, parts.after.trim()]
|
|
414
|
+
.filter((s) => s !== '')
|
|
415
|
+
.join('\n\n')
|
|
416
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
417
|
+
.trimEnd() + '\n';
|
|
411
418
|
return {
|
|
412
419
|
rebuilt,
|
|
413
420
|
counts: {
|
|
@@ -99,6 +99,7 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
99
99
|
**Pause if:**
|
|
100
100
|
- Task is unclear → ask for clarification
|
|
101
101
|
- Implementation reveals a design issue → suggest updating artifacts
|
|
102
|
+
- A task needs work beyond what the spec and tasks describe, or you are tempted to drop, narrow, defer, or accept exceptions to specified behavior to make it fit → surface the added scope and ask; do not absorb it silently
|
|
102
103
|
- Error or blocker encountered → report and wait for guidance
|
|
103
104
|
- User interrupts
|
|
104
105
|
|
|
@@ -169,6 +170,8 @@ What would you like to do?
|
|
|
169
170
|
- Keep code changes minimal and scoped to each task
|
|
170
171
|
- Update task checkbox immediately after completing each task
|
|
171
172
|
- Pause on errors, blockers, or unclear requirements - don't guess
|
|
173
|
+
- When a task needs work beyond what the spec describes, surface the added scope and pause - never silently narrow, defer, or simplify away specified behavior
|
|
174
|
+
- Only mark a task \`- [x]\` when its specified behavior is fully implemented, not when it is partially done or deferred
|
|
172
175
|
- Use contextFiles from CLI output, don't assume specific file names
|
|
173
176
|
- Do not use context or operation guidance as proof that a task is complete
|
|
174
177
|
- Apply relevant project context; report conflicts with controlling workflow inputs
|
|
@@ -39,6 +39,7 @@ export function getFeedbackSkillTemplate() {
|
|
|
39
39
|
5. **Submit on confirmation**
|
|
40
40
|
- Use the \`openspec feedback\` command to submit
|
|
41
41
|
- Format: \`openspec feedback "title" --body "body content"\`
|
|
42
|
+
- The command preserves the title text in the issue body and shortens long GitHub issue titles
|
|
42
43
|
- The command will automatically add metadata (version, platform, timestamp)
|
|
43
44
|
|
|
44
45
|
**Example Draft**
|
|
@@ -374,12 +374,12 @@ Here are the implementation tasks:
|
|
|
374
374
|
|
|
375
375
|
## 1. [Category or file]
|
|
376
376
|
|
|
377
|
-
- [ ] 1.1 [Specific task]
|
|
378
|
-
- [ ] 1.2 [Specific task]
|
|
377
|
+
- [ ] 1.1 [Specific task] — verify: [test, command, observable behavior, or delivered artifact]
|
|
378
|
+
- [ ] 1.2 [Specific task] — verify: [test, command, observable behavior, or delivered artifact]
|
|
379
379
|
|
|
380
|
-
## 2.
|
|
380
|
+
## 2. Integration Verification
|
|
381
381
|
|
|
382
|
-
- [ ] 2.1 [
|
|
382
|
+
- [ ] 2.1 Verify [broader integration or system behavior] with [end-to-end test or observable result]
|
|
383
383
|
|
|
384
384
|
---
|
|
385
385
|
|
|
@@ -42,7 +42,7 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
42
42
|
|
|
43
43
|
**Use a different schema only if the user:**
|
|
44
44
|
- Explicitly requests a specific schema by name → use \`--schema <schema-name>\`
|
|
45
|
-
- Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running \`openspec context --json\` from the current working directory. If the user explicitly selected a registered store, use \`openspec context --json --store "<store-id>"\`. Then run \`openspec schemas --json\` with its working directory set to the returned \`root.path\` and let them choose. This preserves roots selected by a local \`store:\` pointer or the global \`defaultStore\`;
|
|
45
|
+
- Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running \`openspec context --json\` from the current working directory. If the user explicitly selected a registered store, use \`openspec context --json --store "<store-id>"\`. Then run \`openspec schemas --json\` with its working directory set to the returned \`root.path\` and let them choose. This preserves roots selected by a local \`store:\` pointer or the global \`defaultStore\`; when a registered store was explicitly selected, append \`--store "<store-id>"\` to \`openspec schemas --json\` as well. If context reports only \`no_openspec_root\`, run \`openspec schemas --json\` from the current working directory instead. Do not use this fallback for invalid or unavailable stores.
|
|
46
46
|
|
|
47
47
|
Otherwise, omit \`--schema\` to preserve the configured default.
|
|
48
48
|
|
|
@@ -190,7 +190,7 @@ ${STORE_SELECTION_GUIDANCE}
|
|
|
190
190
|
|
|
191
191
|
**Use a different schema only if the user:**
|
|
192
192
|
- Explicitly requests a specific schema by name → use \`--schema <schema-name>\`
|
|
193
|
-
- Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running \`openspec context --json\` from the current working directory. If the user explicitly selected a registered store, use \`openspec context --json --store "<store-id>"\`. Then run \`openspec schemas --json\` with its working directory set to the returned \`root.path\` and let them choose. This preserves roots selected by a local \`store:\` pointer or the global \`defaultStore\`;
|
|
193
|
+
- Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running \`openspec context --json\` from the current working directory. If the user explicitly selected a registered store, use \`openspec context --json --store "<store-id>"\`. Then run \`openspec schemas --json\` with its working directory set to the returned \`root.path\` and let them choose. This preserves roots selected by a local \`store:\` pointer or the global \`defaultStore\`; when a registered store was explicitly selected, append \`--store "<store-id>"\` to \`openspec schemas --json\` as well. If context reports only \`no_openspec_root\`, run \`openspec schemas --json\` from the current working directory instead. Do not use this fallback for invalid or unavailable stores.
|
|
194
194
|
|
|
195
195
|
Otherwise, omit \`--schema\` to preserve the configured default.
|
|
196
196
|
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* Interpolated into every workflow's instructions so generated skills
|
|
5
5
|
* consistently teach how to target a registered store with `--store <id>`.
|
|
6
6
|
*/
|
|
7
|
-
export declare const STORE_SELECTION_GUIDANCE = "**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change \"<name>\" --json --store \"<id>\"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.";
|
|
7
|
+
export declare const STORE_SELECTION_GUIDANCE = "**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `schemas`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change \"<name>\" --json --store \"<id>\"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.";
|
|
8
8
|
//# sourceMappingURL=store-selection.d.ts.map
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* Interpolated into every workflow's instructions so generated skills
|
|
5
5
|
* consistently teach how to target a registered store with `--store <id>`.
|
|
6
6
|
*/
|
|
7
|
-
export const STORE_SELECTION_GUIDANCE = `**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run \`openspec store list --json\` to discover registered store ids, then pass \`--store <id>\` on the commands that read or write specs and changes (\`new change\`, \`status\`, \`instructions\`, \`list\`, \`show\`, \`validate\`, \`archive\`, \`doctor\`, \`context\`, \`view\`). Once selected, treat \`--store <id>\` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run \`openspec status --change "<name>" --json --store "<id>"\`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local \`openspec/\` root.`;
|
|
7
|
+
export const STORE_SELECTION_GUIDANCE = `**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run \`openspec store list --json\` to discover registered store ids, then pass \`--store <id>\` on the commands that read or write specs and changes (\`new change\`, \`status\`, \`instructions\`, \`list\`, \`show\`, \`validate\`, \`archive\`, \`doctor\`, \`context\`, \`schemas\`, \`view\`). Once selected, treat \`--store <id>\` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run \`openspec status --change "<name>" --json --store "<id>"\`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local \`openspec/\` root.`;
|
|
8
8
|
//# sourceMappingURL=store-selection.js.map
|