@codewalla_india/openspec 1.3.1 → 1.3.2
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/LICENSE +22 -22
- package/README.md +272 -191
- package/bin/openspec.js +5 -5
- package/dist/cli/index.js +99 -62
- package/dist/commands/change.d.ts +0 -2
- package/dist/commands/change.js +107 -86
- package/dist/commands/config.js +14 -16
- package/dist/commands/doctor.js +9 -4
- package/dist/commands/feedback.js +79 -30
- package/dist/commands/modify.d.ts +26 -0
- package/dist/commands/modify.js +147 -0
- package/dist/commands/schema.js +170 -91
- package/dist/commands/show.js +5 -2
- package/dist/commands/spec.js +49 -29
- package/dist/commands/store.js +15 -21
- package/dist/commands/validate.d.ts +8 -0
- package/dist/commands/validate.js +43 -20
- package/dist/commands/workflow/index.d.ts +2 -2
- package/dist/commands/workflow/index.js +1 -1
- package/dist/commands/workflow/instructions.d.ts +6 -26
- package/dist/commands/workflow/instructions.js +183 -376
- package/dist/commands/workflow/new-change.d.ts +0 -4
- package/dist/commands/workflow/new-change.js +10 -28
- package/dist/commands/workflow/shared.d.ts +34 -19
- package/dist/commands/workflow/shared.js +34 -4
- package/dist/commands/workflow/status.js +10 -32
- package/dist/commands/workflow/templates.js +15 -5
- package/dist/commands/workset.d.ts +2 -2
- package/dist/commands/workset.js +19 -22
- package/dist/comprehension-quiz/fingerprint.d.ts +39 -0
- package/dist/comprehension-quiz/fingerprint.js +84 -0
- package/dist/comprehension-quiz/index.d.ts +42 -0
- package/dist/comprehension-quiz/index.js +47 -0
- package/dist/comprehension-quiz/pass-record.d.ts +44 -0
- package/dist/comprehension-quiz/pass-record.js +98 -0
- package/dist/comprehension-quiz/providers/anthropic.d.ts +34 -0
- package/dist/comprehension-quiz/providers/anthropic.js +53 -0
- package/dist/comprehension-quiz/providers/factory.d.ts +44 -0
- package/dist/comprehension-quiz/providers/factory.js +174 -0
- package/dist/comprehension-quiz/providers/interface.d.ts +59 -0
- package/dist/comprehension-quiz/providers/interface.js +63 -0
- package/dist/comprehension-quiz/providers/local.d.ts +34 -0
- package/dist/comprehension-quiz/providers/local.js +53 -0
- package/dist/comprehension-quiz/providers/ollama.d.ts +34 -0
- package/dist/comprehension-quiz/providers/ollama.js +53 -0
- package/dist/comprehension-quiz/providers/openai.d.ts +34 -0
- package/dist/comprehension-quiz/providers/openai.js +53 -0
- package/dist/comprehension-quiz/question-generator.d.ts +69 -0
- package/dist/comprehension-quiz/question-generator.js +141 -0
- package/dist/comprehension-quiz/quiz-executor.d.ts +70 -0
- package/dist/comprehension-quiz/quiz-executor.js +165 -0
- package/dist/comprehension-quiz/types.d.ts +115 -0
- package/dist/comprehension-quiz/types.js +11 -0
- package/dist/core/archive.d.ts +14 -1
- package/dist/core/archive.js +1350 -190
- package/dist/core/artifact-graph/graph.d.ts +28 -9
- package/dist/core/artifact-graph/graph.js +94 -43
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/index.js +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +30 -3
- package/dist/core/artifact-graph/instruction-loader.js +70 -12
- package/dist/core/artifact-graph/outputs.d.ts +1 -0
- package/dist/core/artifact-graph/outputs.js +67 -5
- package/dist/core/artifact-graph/resolver.d.ts +14 -0
- package/dist/core/artifact-graph/resolver.js +69 -15
- package/dist/core/artifact-graph/types.js +21 -3
- package/dist/core/available-tools.d.ts +2 -2
- package/dist/core/available-tools.js +14 -3
- package/dist/core/change-metadata/schema.d.ts +2 -0
- package/dist/core/change-metadata/schema.js +13 -0
- package/dist/core/change-status-policy.d.ts +10 -1
- package/dist/core/change-status-policy.js +30 -2
- package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
- package/dist/core/command-generation/adapters/amazon-q.js +11 -5
- package/dist/core/command-generation/adapters/antigravity.js +6 -5
- package/dist/core/command-generation/adapters/auggie.js +7 -6
- package/dist/core/command-generation/adapters/bob.d.ts +5 -1
- package/dist/core/command-generation/adapters/bob.js +11 -10
- package/dist/core/command-generation/adapters/claude.d.ts +1 -1
- package/dist/core/command-generation/adapters/claude.js +12 -17
- package/dist/core/command-generation/adapters/cline.js +5 -5
- package/dist/core/command-generation/adapters/codebuddy.js +8 -7
- package/dist/core/command-generation/adapters/continue.js +8 -7
- package/dist/core/command-generation/adapters/costrict.js +7 -6
- package/dist/core/command-generation/adapters/crush.js +9 -9
- package/dist/core/command-generation/adapters/cursor.js +8 -8
- 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 +7 -6
- package/dist/core/command-generation/adapters/gemini.js +40 -5
- package/dist/core/command-generation/adapters/github-copilot.js +6 -5
- package/dist/core/command-generation/adapters/iflow.js +9 -8
- package/dist/core/command-generation/adapters/index.d.ts +4 -2
- package/dist/core/command-generation/adapters/index.js +4 -2
- package/dist/core/command-generation/adapters/junie.js +6 -5
- package/dist/core/command-generation/adapters/kilocode.js +1 -1
- package/dist/core/command-generation/adapters/kiro.js +6 -5
- package/dist/core/command-generation/adapters/lingma.js +9 -9
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
- package/dist/core/command-generation/adapters/oh-my-pi.js +42 -0
- package/dist/core/command-generation/adapters/opencode.js +6 -8
- package/dist/core/command-generation/adapters/pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/pi.js +7 -10
- package/dist/core/command-generation/adapters/qoder.js +9 -9
- package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
- package/dist/core/command-generation/adapters/qwen.js +14 -9
- package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
- package/dist/core/command-generation/adapters/roocode.js +9 -9
- package/dist/core/command-generation/adapters/trae.d.ts +13 -0
- package/dist/core/command-generation/adapters/trae.js +28 -0
- 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 +8 -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 +16 -61
- package/dist/core/completions/generators/bash-generator.js +41 -41
- package/dist/core/completions/generators/fish-generator.js +7 -7
- package/dist/core/completions/generators/powershell-generator.js +44 -37
- package/dist/core/completions/generators/zsh-generator.js +33 -33
- package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
- package/dist/core/completions/installers/zsh-installer.js +20 -4
- package/dist/core/completions/templates/bash-templates.js +24 -24
- package/dist/core/completions/templates/fish-templates.js +38 -38
- package/dist/core/completions/templates/powershell-templates.js +28 -28
- package/dist/core/completions/templates/zsh-templates.js +39 -39
- package/dist/core/config-prompts.d.ts +1 -1
- package/dist/core/config-prompts.js +14 -1
- package/dist/core/config-schema.d.ts +10 -1
- package/dist/core/config-schema.js +67 -1
- package/dist/core/config.d.ts +15 -0
- package/dist/core/config.js +46 -5
- package/dist/core/file-state.js +56 -24
- package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
- package/dist/core/github-copilot/cloud-agent.js +497 -0
- package/dist/core/global-config.d.ts +16 -0
- package/dist/core/init.d.ts +58 -0
- package/dist/core/init.js +474 -73
- package/dist/core/legacy-cleanup.d.ts +59 -0
- package/dist/core/legacy-cleanup.js +253 -24
- package/dist/core/list.js +23 -16
- package/dist/core/migration.d.ts +73 -1
- package/dist/core/migration.js +360 -11
- package/dist/core/modify/conflict-detector.d.ts +26 -0
- package/dist/core/modify/conflict-detector.js +62 -0
- package/dist/core/modify/dependency-propagator.d.ts +38 -0
- package/dist/core/modify/dependency-propagator.js +103 -0
- package/dist/core/modify/history-tracker.d.ts +34 -0
- package/dist/core/modify/history-tracker.js +114 -0
- package/dist/core/modify/index.d.ts +7 -0
- package/dist/core/modify/index.js +7 -0
- package/dist/core/onboarding-commands.d.ts +30 -0
- package/dist/core/onboarding-commands.js +38 -0
- package/dist/core/openspec-root.js +18 -15
- package/dist/core/parsers/change-parser.d.ts +17 -2
- package/dist/core/parsers/change-parser.js +35 -21
- 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.d.ts +0 -3
- package/dist/core/parsers/markdown-parser.js +7 -60
- package/dist/core/parsers/requirement-blocks.d.ts +29 -0
- package/dist/core/parsers/requirement-blocks.js +150 -31
- package/dist/core/parsers/requirement-text.d.ts +48 -0
- package/dist/core/parsers/requirement-text.js +98 -0
- package/dist/core/parsers/spec-structure.d.ts +1 -1
- package/dist/core/parsers/spec-structure.js +20 -30
- package/dist/core/profile-sync-drift.d.ts +0 -8
- package/dist/core/profile-sync-drift.js +49 -50
- package/dist/core/profiles.d.ts +2 -2
- package/dist/core/profiles.js +2 -2
- package/dist/core/project-config.d.ts +27 -18
- package/dist/core/project-config.js +101 -34
- 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/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/index.d.ts +1 -0
- package/dist/core/shared/index.js +1 -0
- package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
- package/dist/core/shared/skill-content-equivalence.js +41 -0
- package/dist/core/shared/skill-generation.js +17 -13
- package/dist/core/shared/skill-paths.d.ts +13 -0
- package/dist/core/shared/skill-paths.js +23 -0
- package/dist/core/shared/tool-detection.d.ts +27 -7
- package/dist/core/shared/tool-detection.js +167 -37
- package/dist/core/shared-skill-target.d.ts +16 -0
- package/dist/core/shared-skill-target.js +156 -0
- package/dist/core/specs-apply.d.ts +80 -33
- package/dist/core/specs-apply.js +667 -145
- package/dist/core/store/git.d.ts +13 -0
- package/dist/core/store/git.js +24 -0
- package/dist/core/store/operations.js +20 -0
- package/dist/core/templates/skill-templates.d.ts +3 -2
- package/dist/core/templates/skill-templates.js +3 -2
- package/dist/core/templates/workflows/apply-change.d.ts +10 -0
- package/dist/core/templates/workflows/apply-change.js +186 -224
- package/dist/core/templates/workflows/archive-change.js +387 -260
- package/dist/core/templates/workflows/bulk-archive-change.js +655 -475
- package/dist/core/templates/workflows/continue-change.js +210 -219
- package/dist/core/templates/workflows/explore.js +479 -443
- package/dist/core/templates/workflows/feedback.js +97 -97
- package/dist/core/templates/workflows/ff-change.js +200 -191
- package/dist/core/templates/workflows/modify-change.d.ts +4 -1
- package/dist/core/templates/workflows/modify-change.js +237 -115
- package/dist/core/templates/workflows/new-change.js +127 -134
- package/dist/core/templates/workflows/onboard.js +548 -542
- package/dist/core/templates/workflows/propose.js +273 -217
- 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 +500 -273
- package/dist/core/templates/workflows/update-change.d.ts +10 -0
- package/dist/core/templates/workflows/update-change.js +177 -0
- package/dist/core/templates/workflows/verify-change.js +324 -326
- package/dist/core/update.d.ts +40 -5
- package/dist/core/update.js +411 -96
- package/dist/core/validation/constants.d.ts +4 -1
- package/dist/core/validation/constants.js +4 -1
- package/dist/core/validation/plan-validator.d.ts +8 -0
- package/dist/core/validation/plan-validator.js +45 -0
- 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 +27 -3
- package/dist/core/validation/validator.js +337 -48
- package/dist/core/version-check.d.ts +152 -0
- package/dist/core/version-check.js +731 -0
- package/dist/core/view.js +13 -18
- package/dist/prompts/searchable-multi-select.js +1 -1
- package/dist/telemetry/caller-detection.d.ts +27 -0
- package/dist/telemetry/caller-detection.js +85 -0
- package/dist/telemetry/caller-detection.test.d.ts +5 -0
- package/dist/telemetry/config.d.ts +48 -2
- package/dist/telemetry/config.js +113 -11
- package/dist/telemetry/config.test.d.ts +5 -0
- package/dist/telemetry/deduplication.d.ts +35 -0
- package/dist/telemetry/deduplication.js +82 -0
- package/dist/telemetry/deduplication.test.d.ts +5 -0
- package/dist/telemetry/identity.d.ts +62 -22
- package/dist/telemetry/identity.js +147 -102
- package/dist/telemetry/index.d.ts +126 -15
- package/dist/telemetry/index.js +569 -37
- package/dist/telemetry/sanitization.d.ts +40 -0
- package/dist/telemetry/sanitization.js +114 -0
- package/dist/telemetry/sanitization.test.d.ts +5 -0
- package/dist/telemetry/throttling.d.ts +20 -0
- package/dist/telemetry/throttling.js +70 -0
- package/dist/ui/welcome-screen.d.ts +9 -1
- package/dist/ui/welcome-screen.js +91 -38
- package/dist/utils/change-metadata.d.ts +43 -0
- package/dist/utils/change-metadata.js +123 -7
- package/dist/utils/change-utils.d.ts +10 -4
- package/dist/utils/change-utils.js +20 -13
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +92 -6
- package/dist/utils/command-references.js +180 -7
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/file-system.d.ts +10 -0
- package/dist/utils/file-system.js +70 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/interactive.d.ts +23 -0
- package/dist/utils/interactive.js +32 -0
- 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 +115 -0
- package/dist/utils/task-progress.d.ts +29 -1
- package/dist/utils/task-progress.js +109 -15
- package/package.json +96 -84
- package/schemas/spec-driven/schema.yaml +252 -184
- package/schemas/spec-driven/templates/design.md +19 -19
- package/schemas/spec-driven/templates/plan-template.md +91 -0
- package/schemas/spec-driven/templates/proposal.md +29 -24
- package/schemas/spec-driven/templates/spec.md +11 -8
- package/schemas/spec-driven/templates/tasks.md +9 -9
- package/scripts/postinstall.js +83 -83
- 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/core/comprehension/config.d.ts +0 -20
- package/dist/core/comprehension/config.js +0 -23
- package/dist/core/comprehension/fingerprint.d.ts +0 -14
- package/dist/core/comprehension/fingerprint.js +0 -33
- package/dist/core/comprehension/index.d.ts +0 -57
- package/dist/core/comprehension/index.js +0 -87
- package/dist/core/comprehension/pass-record.d.ts +0 -29
- package/dist/core/comprehension/pass-record.js +0 -64
- package/dist/core/comprehension/stats.d.ts +0 -35
- package/dist/core/comprehension/stats.js +0 -133
- package/dist/core/templates/workflows/comprehension-guidance.d.ts +0 -9
- package/dist/core/templates/workflows/comprehension-guidance.js +0 -73
- package/dist/core/templates/workflows/mcp-guidance.d.ts +0 -13
- package/dist/core/templates/workflows/mcp-guidance.js +0 -131
- package/dist/core/templates/workflows/user-prompt-guidance.d.ts +0 -16
- package/dist/core/templates/workflows/user-prompt-guidance.js +0 -48
- package/dist/telemetry/caller.d.ts +0 -5
- package/dist/telemetry/caller.js +0 -29
- package/dist/telemetry/client.d.ts +0 -27
- package/dist/telemetry/client.js +0 -127
- package/dist/telemetry/command-context.d.ts +0 -13
- package/dist/telemetry/command-context.js +0 -59
- package/dist/telemetry/comprehension.d.ts +0 -44
- package/dist/telemetry/comprehension.js +0 -105
- package/dist/telemetry/content.d.ts +0 -10
- package/dist/telemetry/content.js +0 -56
- package/dist/telemetry/git-stats.d.ts +0 -12
- package/dist/telemetry/git-stats.js +0 -69
- package/dist/telemetry/identify-cache.d.ts +0 -7
- package/dist/telemetry/identify-cache.js +0 -47
- package/dist/telemetry/input.d.ts +0 -17
- package/dist/telemetry/input.js +0 -68
- package/dist/telemetry/marker.d.ts +0 -37
- package/dist/telemetry/marker.js +0 -67
- package/dist/telemetry/workflow.d.ts +0 -85
- package/dist/telemetry/workflow.js +0 -318
- package/schemas/spec-driven/templates/plan.md +0 -19
|
@@ -3,7 +3,12 @@ import * as fs from 'fs';
|
|
|
3
3
|
import { AI_TOOLS } from './config.js';
|
|
4
4
|
import { ALL_WORKFLOWS } from './profiles.js';
|
|
5
5
|
import { CommandAdapterRegistry } from './command-generation/index.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getConfiguredTools } from './shared/index.js';
|
|
7
|
+
import { shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
|
|
8
|
+
import { readSharedSkillTarget } from './shared-skill-target.js';
|
|
9
|
+
import { FileSystemUtils } from '../utils/file-system.js';
|
|
10
|
+
import { isLegacyCodexSkillEquivalentToCurrent } from './shared/skill-content-equivalence.js';
|
|
11
|
+
import { hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, } from './shared/skill-paths.js';
|
|
7
12
|
/**
|
|
8
13
|
* Maps workflow IDs to their skill directory names.
|
|
9
14
|
*/
|
|
@@ -11,8 +16,8 @@ export const WORKFLOW_TO_SKILL_DIR = {
|
|
|
11
16
|
'explore': 'openspec-explore',
|
|
12
17
|
'new': 'openspec-new-change',
|
|
13
18
|
'continue': 'openspec-continue-change',
|
|
14
|
-
'modify': 'openspec-modify-change',
|
|
15
19
|
'apply': 'openspec-apply-change',
|
|
20
|
+
'update': 'openspec-update-change',
|
|
16
21
|
'ff': 'openspec-ff-change',
|
|
17
22
|
'sync': 'openspec-sync-specs',
|
|
18
23
|
'archive': 'openspec-archive-change',
|
|
@@ -24,48 +29,11 @@ export const WORKFLOW_TO_SKILL_DIR = {
|
|
|
24
29
|
function toKnownWorkflows(workflows) {
|
|
25
30
|
return workflows.filter((workflow) => ALL_WORKFLOWS.includes(workflow));
|
|
26
31
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Checks whether a tool has at least one generated OpenSpec command file.
|
|
29
|
-
*/
|
|
30
|
-
export function toolHasAnyConfiguredCommand(projectPath, toolId) {
|
|
31
|
-
const adapter = CommandAdapterRegistry.get(toolId);
|
|
32
|
-
if (!adapter)
|
|
33
|
-
return false;
|
|
34
|
-
for (const commandId of COMMAND_IDS) {
|
|
35
|
-
const cmdPath = adapter.getFilePath(commandId);
|
|
36
|
-
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
|
|
37
|
-
if (fs.existsSync(fullPath)) {
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Returns tools with at least one generated command file on disk.
|
|
45
|
-
*/
|
|
46
|
-
export function getCommandConfiguredTools(projectPath) {
|
|
47
|
-
return AI_TOOLS
|
|
48
|
-
.filter((tool) => {
|
|
49
|
-
if (!tool.skillsDir)
|
|
50
|
-
return false;
|
|
51
|
-
const toolDir = path.join(projectPath, tool.skillsDir);
|
|
52
|
-
try {
|
|
53
|
-
return fs.statSync(toolDir).isDirectory();
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
.map((tool) => tool.value)
|
|
60
|
-
.filter((toolId) => toolHasAnyConfiguredCommand(projectPath, toolId));
|
|
61
|
-
}
|
|
62
32
|
/**
|
|
63
33
|
* Returns tools that are configured via either skills or commands.
|
|
64
34
|
*/
|
|
65
35
|
export function getConfiguredToolsForProfileSync(projectPath) {
|
|
66
|
-
|
|
67
|
-
const commandConfigured = getCommandConfiguredTools(projectPath);
|
|
68
|
-
return [...new Set([...skillConfigured, ...commandConfigured])];
|
|
36
|
+
return getConfiguredTools(projectPath);
|
|
69
37
|
}
|
|
70
38
|
/**
|
|
71
39
|
* Detects if a single tool has profile/delivery drift against the desired state.
|
|
@@ -77,14 +45,45 @@ export function getConfiguredToolsForProfileSync(projectPath) {
|
|
|
77
45
|
*/
|
|
78
46
|
export function hasToolProfileOrDeliveryDrift(projectPath, toolId, desiredWorkflows, delivery) {
|
|
79
47
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
80
|
-
if (!tool
|
|
48
|
+
if (!tool || !toolSupportsSkills(tool))
|
|
81
49
|
return false;
|
|
82
50
|
const knownDesiredWorkflows = toKnownWorkflows(desiredWorkflows);
|
|
83
51
|
const desiredWorkflowSet = new Set(knownDesiredWorkflows);
|
|
84
|
-
const skillsDir =
|
|
52
|
+
const skillsDir = resolveToolSkillsDir(projectPath, tool);
|
|
85
53
|
const adapter = CommandAdapterRegistry.get(toolId);
|
|
86
|
-
const shouldGenerateSkills = delivery
|
|
87
|
-
const shouldGenerateCommands = delivery
|
|
54
|
+
const shouldGenerateSkills = shouldGenerateSkillsForTool(toolId, delivery);
|
|
55
|
+
const shouldGenerateCommands = shouldGenerateCommandsForTool(toolId, delivery);
|
|
56
|
+
const sharedTarget = tool.skillsDir
|
|
57
|
+
? readSharedSkillTarget(projectPath, tool.skillsDir)
|
|
58
|
+
: undefined;
|
|
59
|
+
for (const root of tool.legacySkillsDirs ?? []) {
|
|
60
|
+
for (const workflow of knownDesiredWorkflows) {
|
|
61
|
+
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
62
|
+
const legacySkill = path.join(projectPath, root, 'skills', dirName, 'SKILL.md');
|
|
63
|
+
if (!fs.existsSync(legacySkill))
|
|
64
|
+
continue;
|
|
65
|
+
const currentSkill = path.join(skillsDir, dirName, 'SKILL.md');
|
|
66
|
+
if (!fs.existsSync(currentSkill) || sharedTarget !== toolId) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
if (FileSystemUtils.canonicalizeExistingPath(legacySkill) ===
|
|
71
|
+
FileSystemUtils.canonicalizeExistingPath(currentSkill)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// Equivalent generated copies are actionable: migration can safely
|
|
75
|
+
// remove the redundant legacy file even when version, line endings,
|
|
76
|
+
// or supported invocation syntax changed. Materially divergent copies
|
|
77
|
+
// stay in place without forcing an update on every run.
|
|
78
|
+
if (isLegacyCodexSkillEquivalentToCurrent(fs.readFileSync(legacySkill, 'utf-8'), fs.readFileSync(currentSkill, 'utf-8'))) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
88
87
|
if (shouldGenerateSkills) {
|
|
89
88
|
for (const workflow of knownDesiredWorkflows) {
|
|
90
89
|
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
@@ -104,7 +103,7 @@ export function hasToolProfileOrDeliveryDrift(projectPath, toolId, desiredWorkfl
|
|
|
104
103
|
}
|
|
105
104
|
}
|
|
106
105
|
}
|
|
107
|
-
else {
|
|
106
|
+
else if (shouldRemoveSkillsForTool(toolId, delivery) && !hasGlobalSkillTarget(tool)) {
|
|
108
107
|
for (const workflow of ALL_WORKFLOWS) {
|
|
109
108
|
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
110
109
|
const skillDir = path.join(skillsDir, dirName);
|
|
@@ -132,7 +131,7 @@ export function hasToolProfileOrDeliveryDrift(projectPath, toolId, desiredWorkfl
|
|
|
132
131
|
}
|
|
133
132
|
}
|
|
134
133
|
}
|
|
135
|
-
else if (
|
|
134
|
+
else if (shouldReconcileCommandFilesForTool(toolId, delivery) && adapter) {
|
|
136
135
|
for (const workflow of ALL_WORKFLOWS) {
|
|
137
136
|
const cmdPath = adapter.getFilePath(workflow);
|
|
138
137
|
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
|
|
@@ -152,10 +151,10 @@ export function getToolsNeedingProfileSync(projectPath, desiredWorkflows, delive
|
|
|
152
151
|
}
|
|
153
152
|
function getInstalledWorkflowsForTool(projectPath, toolId, options) {
|
|
154
153
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
155
|
-
if (!tool
|
|
154
|
+
if (!tool || !toolSupportsSkills(tool))
|
|
156
155
|
return [];
|
|
157
156
|
const installed = new Set();
|
|
158
|
-
const skillsDir =
|
|
157
|
+
const skillsDir = resolveToolSkillsDir(projectPath, tool);
|
|
159
158
|
if (options.includeSkills) {
|
|
160
159
|
for (const workflow of ALL_WORKFLOWS) {
|
|
161
160
|
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
@@ -188,9 +187,9 @@ export function hasProjectConfigDrift(projectPath, desiredWorkflows, delivery) {
|
|
|
188
187
|
return true;
|
|
189
188
|
}
|
|
190
189
|
const desiredSet = new Set(toKnownWorkflows(desiredWorkflows));
|
|
191
|
-
const includeSkills = delivery !== 'commands';
|
|
192
|
-
const includeCommands = delivery !== 'skills';
|
|
193
190
|
for (const toolId of configuredTools) {
|
|
191
|
+
const includeSkills = shouldGenerateSkillsForTool(toolId, delivery);
|
|
192
|
+
const includeCommands = shouldGenerateCommandsForTool(toolId, delivery);
|
|
194
193
|
const installed = getInstalledWorkflowsForTool(projectPath, toolId, { includeSkills, includeCommands });
|
|
195
194
|
if (installed.some((workflow) => !desiredSet.has(workflow))) {
|
|
196
195
|
return true;
|
package/dist/core/profiles.d.ts
CHANGED
|
@@ -9,11 +9,11 @@ import type { Profile } from './global-config.js';
|
|
|
9
9
|
* Core workflows included in the 'core' profile.
|
|
10
10
|
* These provide the streamlined experience for new users.
|
|
11
11
|
*/
|
|
12
|
-
export declare const CORE_WORKFLOWS: readonly ["propose", "explore", "
|
|
12
|
+
export declare const CORE_WORKFLOWS: readonly ["propose", "explore", "apply", "update", "sync", "archive"];
|
|
13
13
|
/**
|
|
14
14
|
* All available workflows in the system.
|
|
15
15
|
*/
|
|
16
|
-
export declare const ALL_WORKFLOWS: readonly ["propose", "explore", "
|
|
16
|
+
export declare const ALL_WORKFLOWS: readonly ["propose", "explore", "new", "continue", "apply", "update", "ff", "sync", "archive", "bulk-archive", "verify", "onboard"];
|
|
17
17
|
export type WorkflowId = (typeof ALL_WORKFLOWS)[number];
|
|
18
18
|
export type CoreWorkflowId = (typeof CORE_WORKFLOWS)[number];
|
|
19
19
|
/**
|
package/dist/core/profiles.js
CHANGED
|
@@ -8,17 +8,17 @@
|
|
|
8
8
|
* Core workflows included in the 'core' profile.
|
|
9
9
|
* These provide the streamlined experience for new users.
|
|
10
10
|
*/
|
|
11
|
-
export const CORE_WORKFLOWS = ['propose', 'explore', '
|
|
11
|
+
export const CORE_WORKFLOWS = ['propose', 'explore', 'apply', 'update', 'sync', 'archive'];
|
|
12
12
|
/**
|
|
13
13
|
* All available workflows in the system.
|
|
14
14
|
*/
|
|
15
15
|
export const ALL_WORKFLOWS = [
|
|
16
16
|
'propose',
|
|
17
17
|
'explore',
|
|
18
|
-
'modify',
|
|
19
18
|
'new',
|
|
20
19
|
'continue',
|
|
21
20
|
'apply',
|
|
21
|
+
'update',
|
|
22
22
|
'ff',
|
|
23
23
|
'sync',
|
|
24
24
|
'archive',
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
export declare const OPERATION_IDS: readonly ["apply", "archive"];
|
|
3
|
+
export type OperationId = (typeof OPERATION_IDS)[number];
|
|
4
|
+
export interface OperationConfig {
|
|
5
|
+
guidance?: string[];
|
|
6
|
+
}
|
|
7
|
+
export type OperationsConfig = Partial<Record<OperationId, OperationConfig>>;
|
|
2
8
|
/**
|
|
3
9
|
* Zod schema for project configuration.
|
|
4
10
|
*
|
|
@@ -16,12 +22,17 @@ export declare const ProjectConfigSchema: z.ZodObject<{
|
|
|
16
22
|
schema: z.ZodString;
|
|
17
23
|
context: z.ZodOptional<z.ZodString>;
|
|
18
24
|
rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
25
|
+
operations: z.ZodOptional<z.ZodObject<{
|
|
26
|
+
apply: z.ZodOptional<z.ZodObject<{
|
|
27
|
+
guidance: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
28
|
+
}, z.core.$strip>>;
|
|
29
|
+
archive: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
guidance: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
31
|
+
}, z.core.$strip>>;
|
|
32
|
+
}, z.core.$strip>>;
|
|
19
33
|
store: z.ZodOptional<z.ZodString>;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
threshold_percent: z.ZodOptional<z.ZodNumber>;
|
|
23
|
-
min_questions: z.ZodOptional<z.ZodNumber>;
|
|
24
|
-
max_questions: z.ZodOptional<z.ZodNumber>;
|
|
34
|
+
githubCopilot: z.ZodOptional<z.ZodObject<{
|
|
35
|
+
cloudAgent: z.ZodOptional<z.ZodBoolean>;
|
|
25
36
|
}, z.core.$strip>>;
|
|
26
37
|
}, z.core.$strip>;
|
|
27
38
|
/** Normalized in-memory shape of a referenced store declaration. */
|
|
@@ -32,14 +43,12 @@ export interface DeclarationEntry {
|
|
|
32
43
|
}
|
|
33
44
|
export type ProjectConfig = z.infer<typeof ProjectConfigSchema> & {
|
|
34
45
|
references?: DeclarationEntry[];
|
|
35
|
-
/** Normalized comprehension settings (camelCase in memory). */
|
|
36
|
-
comprehension?: {
|
|
37
|
-
enabled?: boolean;
|
|
38
|
-
thresholdPercent?: number;
|
|
39
|
-
minQuestions?: number;
|
|
40
|
-
maxQuestions?: number;
|
|
41
|
-
};
|
|
42
46
|
};
|
|
47
|
+
export interface OperationInputs {
|
|
48
|
+
context?: string;
|
|
49
|
+
operationGuidance?: string[];
|
|
50
|
+
}
|
|
51
|
+
export declare function loadOperationInputs(projectConfig: ProjectConfig | null, operationId: OperationId): OperationInputs;
|
|
43
52
|
export declare const MAX_CONTEXT_SIZE: number;
|
|
44
53
|
/**
|
|
45
54
|
* Read and parse openspec/config.yaml from project root.
|
|
@@ -62,16 +71,16 @@ export declare const MAX_CONTEXT_SIZE: number;
|
|
|
62
71
|
*/
|
|
63
72
|
export declare function readProjectConfig(projectRoot: string): ProjectConfig | null;
|
|
64
73
|
/**
|
|
65
|
-
* Validate artifact IDs in rules against
|
|
66
|
-
*
|
|
67
|
-
*
|
|
74
|
+
* Validate artifact IDs in rules against the artifacts of every available
|
|
75
|
+
* schema. The `rules:` map is global, but each change can use a different
|
|
76
|
+
* schema, so a key is only unknown when it matches no artifact in ANY schema.
|
|
77
|
+
* Returns warnings for keys that are unknown everywhere.
|
|
68
78
|
*
|
|
69
79
|
* @param rules - The rules object from config
|
|
70
|
-
* @param validArtifactIds - Set of valid artifact IDs
|
|
71
|
-
* @param schemaName - Name of the schema for error messages
|
|
80
|
+
* @param validArtifactIds - Set of valid artifact IDs across all schemas
|
|
72
81
|
* @returns Array of warning messages for unknown artifact IDs
|
|
73
82
|
*/
|
|
74
|
-
export declare function validateConfigRules(rules: Record<string, string[]>, validArtifactIds: Set<string
|
|
83
|
+
export declare function validateConfigRules(rules: Record<string, string[]>, validArtifactIds: Set<string>): string[];
|
|
75
84
|
/**
|
|
76
85
|
* Suggest valid schema names when user provides invalid schema.
|
|
77
86
|
* Uses fuzzy matching to find similar names.
|
|
@@ -2,6 +2,10 @@ import { existsSync, readFileSync, statSync } from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { parse as parseYaml } from 'yaml';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
export const OPERATION_IDS = ['apply', 'archive'];
|
|
6
|
+
const OperationConfigSchema = z.object({
|
|
7
|
+
guidance: z.array(z.string()).optional(),
|
|
8
|
+
});
|
|
5
9
|
/**
|
|
6
10
|
* Zod schema for project configuration.
|
|
7
11
|
*
|
|
@@ -34,6 +38,14 @@ export const ProjectConfigSchema = z.object({
|
|
|
34
38
|
)
|
|
35
39
|
.optional()
|
|
36
40
|
.describe('Per-artifact rules, keyed by artifact ID'),
|
|
41
|
+
// Optional: per-operation advisory guidance, kept separate from artifact rules.
|
|
42
|
+
operations: z
|
|
43
|
+
.object({
|
|
44
|
+
apply: OperationConfigSchema.optional(),
|
|
45
|
+
archive: OperationConfigSchema.optional(),
|
|
46
|
+
})
|
|
47
|
+
.optional()
|
|
48
|
+
.describe('Per-operation advisory guidance'),
|
|
37
49
|
// Note: the `references` field (id strings or {id, remote} maps) is
|
|
38
50
|
// deliberately absent here — readProjectConfig parses and normalizes
|
|
39
51
|
// it by hand (see DeclarationEntry below); a schema entry nothing
|
|
@@ -45,17 +57,70 @@ export const ProjectConfigSchema = z.object({
|
|
|
45
57
|
.string()
|
|
46
58
|
.optional()
|
|
47
59
|
.describe('Store id used as the OpenSpec root when no local planning shape exists'),
|
|
48
|
-
// Optional:
|
|
49
|
-
|
|
60
|
+
// Optional: GitHub Copilot integration preferences. `cloudAgent` is the
|
|
61
|
+
// opt-in for generating the Copilot cloud coding-agent files (a GitHub
|
|
62
|
+
// Actions workflow + agent file); absent means "not yet decided".
|
|
63
|
+
githubCopilot: z
|
|
50
64
|
.object({
|
|
51
|
-
|
|
52
|
-
threshold_percent: z.number().int().min(0).max(100).optional(),
|
|
53
|
-
min_questions: z.number().int().positive().optional(),
|
|
54
|
-
max_questions: z.number().int().positive().optional(),
|
|
65
|
+
cloudAgent: z.boolean().optional(),
|
|
55
66
|
})
|
|
56
67
|
.optional()
|
|
57
|
-
.describe('
|
|
68
|
+
.describe('GitHub Copilot integration preferences'),
|
|
58
69
|
});
|
|
70
|
+
export function loadOperationInputs(projectConfig, operationId) {
|
|
71
|
+
const context = projectConfig?.context !== undefined && projectConfig.context.trim().length > 0
|
|
72
|
+
? projectConfig.context
|
|
73
|
+
: undefined;
|
|
74
|
+
const guidance = projectConfig?.operations?.[operationId]?.guidance;
|
|
75
|
+
const operationGuidance = guidance && guidance.length > 0 ? guidance : undefined;
|
|
76
|
+
return {
|
|
77
|
+
...(context !== undefined ? { context } : {}),
|
|
78
|
+
...(operationGuidance !== undefined ? { operationGuidance } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function parseOperations(raw) {
|
|
82
|
+
if (raw === undefined) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
86
|
+
console.warn(`Invalid 'operations' field in config (must be object)`);
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const supported = new Set(OPERATION_IDS);
|
|
90
|
+
const operations = {};
|
|
91
|
+
for (const [operationId, value] of Object.entries(raw)) {
|
|
92
|
+
if (!supported.has(operationId)) {
|
|
93
|
+
console.warn(`Unknown operation ID '${operationId}' in config. Supported operation IDs: ${OPERATION_IDS.join(', ')}`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const typedOperationId = operationId;
|
|
97
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
98
|
+
console.warn(`Invalid 'operations.${operationId}' field in config (must be object), ignoring this operation`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const operation = value;
|
|
102
|
+
const unknownFields = Object.keys(operation).filter((field) => field !== 'guidance');
|
|
103
|
+
if (unknownFields.length > 0) {
|
|
104
|
+
console.warn(`Unknown field(s) in 'operations.${operationId}': ${unknownFields.join(', ')}. Supported fields: guidance`);
|
|
105
|
+
}
|
|
106
|
+
if (operation.guidance === undefined) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const guidanceResult = z.array(z.string()).safeParse(operation.guidance);
|
|
110
|
+
if (!guidanceResult.success) {
|
|
111
|
+
console.warn(`Guidance for operation '${operationId}' must be an array of strings, ignoring this operation's guidance`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const guidance = guidanceResult.data.filter((entry) => entry.length > 0);
|
|
115
|
+
if (guidance.length < guidanceResult.data.length) {
|
|
116
|
+
console.warn(`Some guidance for operation '${operationId}' are empty strings, ignoring them`);
|
|
117
|
+
}
|
|
118
|
+
if (guidance.length > 0) {
|
|
119
|
+
operations[typedOperationId] = { guidance };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return Object.keys(operations).length > 0 ? operations : undefined;
|
|
123
|
+
}
|
|
59
124
|
/**
|
|
60
125
|
* Parser for `references:` declarations: string entries or
|
|
61
126
|
* {id, remote} maps, normalized to DeclarationEntry[]. Dedup keys on
|
|
@@ -179,7 +244,11 @@ export function readProjectConfig(projectRoot) {
|
|
|
179
244
|
const rulesField = z.record(z.string(), z.array(z.string()));
|
|
180
245
|
// First check if it's an object structure (guard against null since typeof null === 'object')
|
|
181
246
|
if (typeof raw.rules === 'object' && raw.rules !== null && !Array.isArray(raw.rules)) {
|
|
182
|
-
|
|
247
|
+
// Artifact ids are intentionally not restricted to the built-in naming
|
|
248
|
+
// convention, so keys such as "constructor" remain valid for custom
|
|
249
|
+
// schemas. A null-prototype map preserves those keys as data without
|
|
250
|
+
// letting "__proto__" mutate the lookup object's prototype.
|
|
251
|
+
const parsedRules = Object.create(null);
|
|
183
252
|
let hasValidRules = false;
|
|
184
253
|
for (const [artifactId, rules] of Object.entries(raw.rules)) {
|
|
185
254
|
const rulesArrayResult = z.array(z.string()).safeParse(rules);
|
|
@@ -206,6 +275,10 @@ export function readProjectConfig(projectRoot) {
|
|
|
206
275
|
console.warn(`Invalid 'rules' field in config (must be object)`);
|
|
207
276
|
}
|
|
208
277
|
}
|
|
278
|
+
const operations = parseOperations(raw.operations);
|
|
279
|
+
if (operations) {
|
|
280
|
+
config.operations = operations;
|
|
281
|
+
}
|
|
209
282
|
const references = parseDeclarationList(raw.references);
|
|
210
283
|
if (references) {
|
|
211
284
|
config.references = references;
|
|
@@ -221,27 +294,21 @@ export function readProjectConfig(projectRoot) {
|
|
|
221
294
|
console.warn(`Warning: ignoring invalid store: field in ${configPathForWarnings(projectRoot)} (must be a single store id string).`);
|
|
222
295
|
}
|
|
223
296
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
...(c.threshold_percent !== undefined
|
|
237
|
-
? { thresholdPercent: c.threshold_percent }
|
|
238
|
-
: {}),
|
|
239
|
-
...(c.min_questions !== undefined ? { minQuestions: c.min_questions } : {}),
|
|
240
|
-
...(c.max_questions !== undefined ? { maxQuestions: c.max_questions } : {}),
|
|
241
|
-
};
|
|
297
|
+
// Parse githubCopilot preferences (only cloudAgent is recognized today).
|
|
298
|
+
if (raw.githubCopilot !== undefined) {
|
|
299
|
+
if (typeof raw.githubCopilot === 'object' &&
|
|
300
|
+
raw.githubCopilot !== null &&
|
|
301
|
+
!Array.isArray(raw.githubCopilot)) {
|
|
302
|
+
const cloudAgent = raw.githubCopilot.cloudAgent;
|
|
303
|
+
if (typeof cloudAgent === 'boolean') {
|
|
304
|
+
config.githubCopilot = { cloudAgent };
|
|
305
|
+
}
|
|
306
|
+
else if (cloudAgent !== undefined) {
|
|
307
|
+
console.warn(`Invalid 'githubCopilot.cloudAgent' field in config (must be a boolean)`);
|
|
308
|
+
}
|
|
242
309
|
}
|
|
243
310
|
else {
|
|
244
|
-
console.warn(`Invalid '
|
|
311
|
+
console.warn(`Invalid 'githubCopilot' field in config (must be an object)`);
|
|
245
312
|
}
|
|
246
313
|
}
|
|
247
314
|
// Return partial config even if some fields failed
|
|
@@ -256,22 +323,22 @@ function configPathForWarnings(projectRoot) {
|
|
|
256
323
|
return resolveConfigFilePath(projectRoot) ?? path.join(projectRoot, 'openspec', 'config.yaml');
|
|
257
324
|
}
|
|
258
325
|
/**
|
|
259
|
-
* Validate artifact IDs in rules against
|
|
260
|
-
*
|
|
261
|
-
*
|
|
326
|
+
* Validate artifact IDs in rules against the artifacts of every available
|
|
327
|
+
* schema. The `rules:` map is global, but each change can use a different
|
|
328
|
+
* schema, so a key is only unknown when it matches no artifact in ANY schema.
|
|
329
|
+
* Returns warnings for keys that are unknown everywhere.
|
|
262
330
|
*
|
|
263
331
|
* @param rules - The rules object from config
|
|
264
|
-
* @param validArtifactIds - Set of valid artifact IDs
|
|
265
|
-
* @param schemaName - Name of the schema for error messages
|
|
332
|
+
* @param validArtifactIds - Set of valid artifact IDs across all schemas
|
|
266
333
|
* @returns Array of warning messages for unknown artifact IDs
|
|
267
334
|
*/
|
|
268
|
-
export function validateConfigRules(rules, validArtifactIds
|
|
335
|
+
export function validateConfigRules(rules, validArtifactIds) {
|
|
269
336
|
const warnings = [];
|
|
270
337
|
for (const artifactId of Object.keys(rules)) {
|
|
271
338
|
if (!validArtifactIds.has(artifactId)) {
|
|
272
339
|
const validIds = Array.from(validArtifactIds).sort().join(', ');
|
|
273
340
|
warnings.push(`Unknown artifact ID in rules: "${artifactId}". ` +
|
|
274
|
-
`
|
|
341
|
+
`It matches no artifact in any available schema. Known artifact IDs: ${validIds}`);
|
|
275
342
|
}
|
|
276
343
|
}
|
|
277
344
|
return warnings;
|
package/dist/core/references.js
CHANGED
|
@@ -53,6 +53,46 @@ function registerFix(id, remote) {
|
|
|
53
53
|
}
|
|
54
54
|
return `Get a checkout from a teammate and run: openspec store register <path> --id ${id}`;
|
|
55
55
|
}
|
|
56
|
+
const WHITESPACE = /\s/;
|
|
57
|
+
/**
|
|
58
|
+
* Drop a CommonMark closing sequence (`## Purpose ##`). The closing run only
|
|
59
|
+
* counts when whitespace precedes it, so `Purpose###` keeps its hashes.
|
|
60
|
+
* Scans from the end so the cost stays linear in the title length.
|
|
61
|
+
*/
|
|
62
|
+
function stripClosingSequence(title) {
|
|
63
|
+
let end = title.length;
|
|
64
|
+
while (end > 0 && WHITESPACE.test(title[end - 1])) {
|
|
65
|
+
end--;
|
|
66
|
+
}
|
|
67
|
+
const hashEnd = end;
|
|
68
|
+
while (end > 0 && title[end - 1] === '#') {
|
|
69
|
+
end--;
|
|
70
|
+
}
|
|
71
|
+
const noClosingRun = end === hashEnd;
|
|
72
|
+
const missingLeadingSpace = end === 0 || !WHITESPACE.test(title[end - 1]);
|
|
73
|
+
if (noClosingRun || missingLeadingSpace) {
|
|
74
|
+
return title.trim();
|
|
75
|
+
}
|
|
76
|
+
return title.slice(0, end).trim();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Heading title, or null when the line is not an ATX heading. Hand-rolled
|
|
80
|
+
* rather than a regex so a title padded with whitespace cannot backtrack.
|
|
81
|
+
*/
|
|
82
|
+
function parseHeadingTitle(line) {
|
|
83
|
+
let level = 0;
|
|
84
|
+
while (level < 6 && level < line.length && line[level] === '#') {
|
|
85
|
+
level++;
|
|
86
|
+
}
|
|
87
|
+
if (level === 0 || level >= line.length || !WHITESPACE.test(line[level])) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
let start = level;
|
|
91
|
+
while (start < line.length && WHITESPACE.test(line[start])) {
|
|
92
|
+
start++;
|
|
93
|
+
}
|
|
94
|
+
return stripClosingSequence(line.slice(start));
|
|
95
|
+
}
|
|
56
96
|
/**
|
|
57
97
|
* Tolerant first-Purpose-line extraction. parseSpec() throws on specs
|
|
58
98
|
* without Purpose/Requirements sections; the index must never fail on an
|
|
@@ -79,12 +119,11 @@ export function extractFirstPurposeLine(markdown) {
|
|
|
79
119
|
if (fenceMarker !== null) {
|
|
80
120
|
continue;
|
|
81
121
|
}
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
122
|
+
const title = parseHeadingTitle(line);
|
|
123
|
+
if (title !== null) {
|
|
84
124
|
if (inPurpose) {
|
|
85
125
|
return '';
|
|
86
126
|
}
|
|
87
|
-
const title = heading[2].replace(/\s+#+\s*$/, '').trim();
|
|
88
127
|
inPurpose = title.toLowerCase() === 'purpose';
|
|
89
128
|
continue;
|
|
90
129
|
}
|
|
@@ -152,7 +191,6 @@ export function renderReferencedStoresSection(entries) {
|
|
|
152
191
|
* let hostile content forge instruction lines (slice 6.1 hardening).
|
|
153
192
|
*/
|
|
154
193
|
export function sanitizeInline(value, maxLength = 300) {
|
|
155
|
-
// eslint-disable-next-line no-control-regex
|
|
156
194
|
const flattened = value.replace(/[\u0000-\u001f\u007f]+/g, ' ').trim();
|
|
157
195
|
return flattened.length > maxLength ? `${flattened.slice(0, maxLength)}…` : flattened;
|
|
158
196
|
}
|
|
@@ -26,6 +26,10 @@ export interface RelationshipHealth {
|
|
|
26
26
|
remote?: string;
|
|
27
27
|
};
|
|
28
28
|
origin_url?: string;
|
|
29
|
+
drift?: {
|
|
30
|
+
ahead: number;
|
|
31
|
+
behind: number;
|
|
32
|
+
};
|
|
29
33
|
status: StoreDiagnostic[];
|
|
30
34
|
} | null;
|
|
31
35
|
references: ReferenceIndexEntry[];
|
|
@@ -42,6 +46,10 @@ export interface InspectRelationshipsInput {
|
|
|
42
46
|
metadataValid: boolean;
|
|
43
47
|
canonicalRemote?: string;
|
|
44
48
|
originUrl?: string;
|
|
49
|
+
drift?: {
|
|
50
|
+
ahead: number;
|
|
51
|
+
behind: number;
|
|
52
|
+
};
|
|
45
53
|
};
|
|
46
54
|
referenceEntries: ReferenceIndexEntry[];
|
|
47
55
|
registryUnreadable: boolean;
|
|
@@ -37,6 +37,18 @@ export function inspectRelationships(input) {
|
|
|
37
37
|
input.storeFacts.canonicalRemote !== input.storeFacts.originUrl) {
|
|
38
38
|
storeStatus.push(makeStoreDiagnostic('info', 'store_remote_divergence', `The store.yaml remote (${sanitizeInline(input.storeFacts.canonicalRemote, 200)}) differs from the checkout's origin (${sanitizeInline(input.storeFacts.originUrl, 200)}).`, { target: 'store.metadata' }));
|
|
39
39
|
}
|
|
40
|
+
// Checkout behind its upstream tracking ref: a read-only staleness
|
|
41
|
+
// signal, not a version pin — OpenSpec never syncs stores, so this
|
|
42
|
+
// compares against the local upstream ref, not the live remote.
|
|
43
|
+
// Behind means teammates on newer commits may resolve different specs.
|
|
44
|
+
// Ahead-only is normal (OpenSpec never pushes stores), so it stays quiet.
|
|
45
|
+
const drift = input.storeFacts.drift;
|
|
46
|
+
if (drift && drift.behind > 0) {
|
|
47
|
+
const behindCommits = `${drift.behind} commit${drift.behind === 1 ? '' : 's'}`;
|
|
48
|
+
storeStatus.push(makeStoreDiagnostic('info', 'store_checkout_drift', drift.ahead > 0
|
|
49
|
+
? `This store checkout has diverged from its upstream tracking branch (${drift.behind} behind, ${drift.ahead} ahead); teammates on newer commits may resolve different specs.`
|
|
50
|
+
: `This store checkout is ${behindCommits} behind its upstream tracking branch; teammates on newer commits may resolve different specs.`, { target: 'store.git' }));
|
|
51
|
+
}
|
|
40
52
|
store = {
|
|
41
53
|
id: input.storeFacts.id,
|
|
42
54
|
metadata: {
|
|
@@ -47,6 +59,7 @@ export function inspectRelationships(input) {
|
|
|
47
59
|
: {}),
|
|
48
60
|
},
|
|
49
61
|
...(input.storeFacts.originUrl ? { origin_url: input.storeFacts.originUrl } : {}),
|
|
62
|
+
...(drift ? { drift } : {}),
|
|
50
63
|
status: storeStatus,
|
|
51
64
|
};
|
|
52
65
|
}
|
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* - `--store <id>` selects a registered store's root.
|
|
8
8
|
* - Without `--store`, the nearest ancestor containing `openspec/` wins.
|
|
9
9
|
* Leftover workspace view state is never considered a root here.
|
|
10
|
-
* - With no nearest root,
|
|
11
|
-
*
|
|
10
|
+
* - With no nearest root, a global `defaultStore` (if set) is the last
|
|
11
|
+
* machine-level fallback before the selection hint error.
|
|
12
|
+
* - With no nearest root and no default, registered stores produce a
|
|
13
|
+
* selection hint error; otherwise commands may treat the current
|
|
14
|
+
* directory as an implicit root.
|
|
12
15
|
*
|
|
13
16
|
* Diagnostic codes reuse the store taxonomy where an error passes
|
|
14
17
|
* through unchanged (`invalid_store_id`, metadata parse failures);
|
|
@@ -19,7 +22,7 @@
|
|
|
19
22
|
* `no_openspec_root`).
|
|
20
23
|
*/
|
|
21
24
|
import { type PlanningHome } from './planning-home.js';
|
|
22
|
-
export type OpenSpecRootSource = 'store' | 'declared' | 'nearest' | 'implicit';
|
|
25
|
+
export type OpenSpecRootSource = 'store' | 'declared' | 'global_default' | 'nearest' | 'implicit';
|
|
23
26
|
export interface StoreSelectorOptions {
|
|
24
27
|
store?: string;
|
|
25
28
|
storePath?: string;
|
|
@@ -84,9 +87,9 @@ export interface RootOutput {
|
|
|
84
87
|
}
|
|
85
88
|
export declare function toRootOutput(root: ResolvedOpenSpecRoot): RootOutput;
|
|
86
89
|
/**
|
|
87
|
-
* A store-selected root — explicit `--store
|
|
88
|
-
* Cross-root behavior (absolute paths, --store hints,
|
|
89
|
-
* noun-form suggestions) keys on this, never on `source` directly.
|
|
90
|
+
* A store-selected root — explicit `--store`, a declared pointer, or the
|
|
91
|
+
* global default. Cross-root behavior (absolute paths, --store hints,
|
|
92
|
+
* suppressed noun-form suggestions) keys on this, never on `source` directly.
|
|
90
93
|
*/
|
|
91
94
|
export declare function isStoreSelectedRoot(root: ResolvedOpenSpecRoot): root is ResolvedOpenSpecRoot & {
|
|
92
95
|
storeId: string;
|