Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@studyzy/openspec-cn

Package Overview
Dependencies
Maintainers
1
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@studyzy/openspec-cn - npm Package Compare versions

Comparing version
1.1.1
to
1.2.0-1
+16
dist/core/available-tools.d.ts
/**
* Available Tools Detection
*
* Detects which AI tools are available in a project by scanning
* for their configuration directories.
*/
import { type AIToolOption } from './config.js';
/**
* Scans the project path for AI tool configuration directories and returns
* the tools that are present.
*
* Checks for each tool's `skillsDir` (e.g., `.claude/`, `.cursor/`) at the
* project root. Only tools with a `skillsDir` property are considered.
*/
export declare function getAvailableTools(projectPath: string): AIToolOption[];
//# sourceMappingURL=available-tools.d.ts.map
/**
* Available Tools Detection
*
* Detects which AI tools are available in a project by scanning
* for their configuration directories.
*/
import path from 'path';
import * as fs from 'fs';
import { AI_TOOLS } from './config.js';
/**
* Scans the project path for AI tool configuration directories and returns
* the tools that are present.
*
* Checks for each tool's `skillsDir` (e.g., `.claude/`, `.cursor/`) at the
* project root. Only tools with a `skillsDir` property are considered.
*/
export function getAvailableTools(projectPath) {
return AI_TOOLS.filter((tool) => {
if (!tool.skillsDir)
return false;
const dirPath = path.join(projectPath, tool.skillsDir);
try {
return fs.statSync(dirPath).isDirectory();
}
catch {
return false;
}
});
}
//# sourceMappingURL=available-tools.js.map
/**
* Kiro Command Adapter
*
* Formats commands for Kiro following its .prompt.md specification.
*/
import type { ToolCommandAdapter } from '../types.js';
/**
* Kiro adapter for command generation.
* File path: .kiro/prompts/opsx-<id>.prompt.md
* Frontmatter: description
*/
export declare const kiroAdapter: ToolCommandAdapter;
//# sourceMappingURL=kiro.d.ts.map
/**
* Kiro Command Adapter
*
* Formats commands for Kiro following its .prompt.md specification.
*/
import path from 'path';
/**
* Kiro adapter for command generation.
* File path: .kiro/prompts/opsx-<id>.prompt.md
* Frontmatter: description
*/
export const kiroAdapter = {
toolId: 'kiro',
getFilePath(commandId) {
return path.join('.kiro', 'prompts', `opsx-${commandId}.prompt.md`);
},
formatFile(content) {
return `---
description: ${content.description}
---
${content.body}
`;
},
};
//# sourceMappingURL=kiro.js.map
/**
* Pi Command Adapter
*
* Formats commands for Pi (pi.dev) following its prompt template specification.
* Pi prompt templates live in .pi/prompts/*.md with description frontmatter.
*/
import type { ToolCommandAdapter } from '../types.js';
/**
* Pi adapter for prompt template generation.
* File path: .pi/prompts/opsx-<id>.md
* Frontmatter: description
*/
export declare const piAdapter: ToolCommandAdapter;
//# sourceMappingURL=pi.d.ts.map
/**
* Pi Command Adapter
*
* Formats commands for Pi (pi.dev) following its prompt template specification.
* Pi prompt templates live in .pi/prompts/*.md with description frontmatter.
*/
import path from 'path';
/**
* Escapes a string value for safe YAML output.
* Quotes the string if it contains special YAML characters.
*/
function escapeYamlValue(value) {
// Check if value needs quoting (contains special YAML characters or starts/ends with whitespace)
const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
if (needsQuoting) {
// Use double quotes and escape internal double quotes and backslashes
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
return `"${escaped}"`;
}
return value;
}
/**
* Pi adapter for prompt template generation.
* File path: .pi/prompts/opsx-<id>.md
* Frontmatter: description
*/
export const piAdapter = {
toolId: 'pi',
getFilePath(commandId) {
return path.join('.pi', 'prompts', `opsx-${commandId}.md`);
},
formatFile(content) {
return `---
description: ${escapeYamlValue(content.description)}
---
${content.body}
`;
},
};
//# sourceMappingURL=pi.js.map
/**
* Migration Utilities
*
* One-time migration logic for existing projects when profile system is introduced.
* Called by both init and update commands before profile resolution.
*/
import type { AIToolOption } from './config.js';
/**
* Scans installed workflow files across all detected tools and returns
* the union of installed workflow IDs.
*/
export declare function scanInstalledWorkflows(projectPath: string, tools: AIToolOption[]): string[];
/**
* Performs one-time migration if the global config does not yet have a profile field.
* Called by both init and update before profile resolution.
*
* - If no profile field exists and workflows are installed: sets profile to 'custom'
* with the detected workflows, preserving the user's existing setup.
* - If no profile field exists and no workflows are installed: no-op (defaults apply).
* - If profile field already exists: no-op.
*/
export declare function migrateIfNeeded(projectPath: string, tools: AIToolOption[]): void;
//# sourceMappingURL=migration.d.ts.map
/**
* Migration Utilities
*
* One-time migration logic for existing projects when profile system is introduced.
* Called by both init and update commands before profile resolution.
*/
import { getGlobalConfig, getGlobalConfigPath, saveGlobalConfig } from './global-config.js';
import { CommandAdapterRegistry } from './command-generation/index.js';
import { WORKFLOW_TO_SKILL_DIR } from './profile-sync-drift.js';
import { ALL_WORKFLOWS } from './profiles.js';
import path from 'path';
import * as fs from 'fs';
function scanInstalledWorkflowArtifacts(projectPath, tools) {
const installed = new Set();
let hasSkills = false;
let hasCommands = false;
for (const tool of tools) {
if (!tool.skillsDir)
continue;
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
for (const workflowId of ALL_WORKFLOWS) {
const skillDirName = WORKFLOW_TO_SKILL_DIR[workflowId];
const skillFile = path.join(skillsDir, skillDirName, 'SKILL.md');
if (fs.existsSync(skillFile)) {
installed.add(workflowId);
hasSkills = true;
}
}
const adapter = CommandAdapterRegistry.get(tool.value);
if (!adapter)
continue;
for (const workflowId of ALL_WORKFLOWS) {
const commandPath = adapter.getFilePath(workflowId);
const fullPath = path.isAbsolute(commandPath)
? commandPath
: path.join(projectPath, commandPath);
if (fs.existsSync(fullPath)) {
installed.add(workflowId);
hasCommands = true;
}
}
}
return {
workflows: ALL_WORKFLOWS.filter((workflowId) => installed.has(workflowId)),
hasSkills,
hasCommands,
};
}
/**
* Scans installed workflow files across all detected tools and returns
* the union of installed workflow IDs.
*/
export function scanInstalledWorkflows(projectPath, tools) {
return scanInstalledWorkflowArtifacts(projectPath, tools).workflows;
}
function inferDelivery(artifacts) {
if (artifacts.hasSkills && artifacts.hasCommands) {
return 'both';
}
if (artifacts.hasCommands) {
return 'commands';
}
return 'skills';
}
/**
* Performs one-time migration if the global config does not yet have a profile field.
* Called by both init and update before profile resolution.
*
* - If no profile field exists and workflows are installed: sets profile to 'custom'
* with the detected workflows, preserving the user's existing setup.
* - If no profile field exists and no workflows are installed: no-op (defaults apply).
* - If profile field already exists: no-op.
*/
export function migrateIfNeeded(projectPath, tools) {
const config = getGlobalConfig();
// Check raw config file for profile field presence
const configPath = getGlobalConfigPath();
let rawConfig = {};
try {
if (fs.existsSync(configPath)) {
rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
}
}
catch {
return; // Can't read config, skip migration
}
// If profile is already explicitly set, no migration needed
if (rawConfig.profile !== undefined) {
return;
}
// Scan for installed workflows
const artifacts = scanInstalledWorkflowArtifacts(projectPath, tools);
const installedWorkflows = artifacts.workflows;
if (installedWorkflows.length === 0) {
// No workflows installed, new user — defaults will apply
return;
}
// Migrate: set profile to custom with detected workflows
config.profile = 'custom';
config.workflows = installedWorkflows;
if (rawConfig.delivery === undefined) {
config.delivery = inferDelivery(artifacts);
}
saveGlobalConfig(config);
console.log(`已迁移:自定义配置,共 ${installedWorkflows.length} 个工作流程`);
console.log("本版本新增:/opsx:propose。尝试 'openspec-cn config profile core' 获得精简体验。");
}
//# sourceMappingURL=migration.js.map
import type { Delivery } from './global-config.js';
import { ALL_WORKFLOWS } from './profiles.js';
type WorkflowId = (typeof ALL_WORKFLOWS)[number];
/**
* Maps workflow IDs to their skill directory names.
*/
export declare const WORKFLOW_TO_SKILL_DIR: Record<WorkflowId, string>;
/**
* Checks whether a tool has at least one generated OpenSpec command file.
*/
export declare function toolHasAnyConfiguredCommand(projectPath: string, toolId: string): boolean;
/**
* Returns tools with at least one generated command file on disk.
*/
export declare function getCommandConfiguredTools(projectPath: string): string[];
/**
* Returns tools that are configured via either skills or commands.
*/
export declare function getConfiguredToolsForProfileSync(projectPath: string): string[];
/**
* Detects if a single tool has profile/delivery drift against the desired state.
*
* This function covers:
* - required artifacts missing for selected workflows
* - artifacts that should not exist for the selected delivery mode
* - artifacts for workflows that were deselected from the current profile
*/
export declare function hasToolProfileOrDeliveryDrift(projectPath: string, toolId: string, desiredWorkflows: readonly string[], delivery: Delivery): boolean;
/**
* Returns configured tools that currently need a profile/delivery sync.
*/
export declare function getToolsNeedingProfileSync(projectPath: string, desiredWorkflows: readonly string[], delivery: Delivery, configuredTools?: readonly string[]): string[];
/**
* Detects whether the current project has any profile/delivery drift.
*/
export declare function hasProjectConfigDrift(projectPath: string, desiredWorkflows: readonly string[], delivery: Delivery): boolean;
export {};
//# sourceMappingURL=profile-sync-drift.d.ts.map
import path from 'path';
import * as fs from 'fs';
import { AI_TOOLS } from './config.js';
import { ALL_WORKFLOWS } from './profiles.js';
import { CommandAdapterRegistry } from './command-generation/index.js';
import { COMMAND_IDS, getConfiguredTools } from './shared/index.js';
/**
* Maps workflow IDs to their skill directory names.
*/
export const WORKFLOW_TO_SKILL_DIR = {
'explore': 'openspec-explore',
'new': 'openspec-new-change',
'continue': 'openspec-continue-change',
'apply': 'openspec-apply-change',
'ff': 'openspec-ff-change',
'sync': 'openspec-sync-specs',
'archive': 'openspec-archive-change',
'bulk-archive': 'openspec-bulk-archive-change',
'verify': 'openspec-verify-change',
'onboard': 'openspec-onboard',
'propose': 'openspec-propose',
};
function toKnownWorkflows(workflows) {
return workflows.filter((workflow) => ALL_WORKFLOWS.includes(workflow));
}
/**
* Checks whether a tool has at least one generated OpenSpec command file.
*/
export function toolHasAnyConfiguredCommand(projectPath, toolId) {
const adapter = CommandAdapterRegistry.get(toolId);
if (!adapter)
return false;
for (const commandId of COMMAND_IDS) {
const cmdPath = adapter.getFilePath(commandId);
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
if (fs.existsSync(fullPath)) {
return true;
}
}
return false;
}
/**
* Returns tools with at least one generated command file on disk.
*/
export function getCommandConfiguredTools(projectPath) {
return AI_TOOLS
.filter((tool) => {
if (!tool.skillsDir)
return false;
const toolDir = path.join(projectPath, tool.skillsDir);
try {
return fs.statSync(toolDir).isDirectory();
}
catch {
return false;
}
})
.map((tool) => tool.value)
.filter((toolId) => toolHasAnyConfiguredCommand(projectPath, toolId));
}
/**
* Returns tools that are configured via either skills or commands.
*/
export function getConfiguredToolsForProfileSync(projectPath) {
const skillConfigured = getConfiguredTools(projectPath);
const commandConfigured = getCommandConfiguredTools(projectPath);
return [...new Set([...skillConfigured, ...commandConfigured])];
}
/**
* Detects if a single tool has profile/delivery drift against the desired state.
*
* This function covers:
* - required artifacts missing for selected workflows
* - artifacts that should not exist for the selected delivery mode
* - artifacts for workflows that were deselected from the current profile
*/
export function hasToolProfileOrDeliveryDrift(projectPath, toolId, desiredWorkflows, delivery) {
const tool = AI_TOOLS.find((t) => t.value === toolId);
if (!tool?.skillsDir)
return false;
const knownDesiredWorkflows = toKnownWorkflows(desiredWorkflows);
const desiredWorkflowSet = new Set(knownDesiredWorkflows);
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
const adapter = CommandAdapterRegistry.get(toolId);
const shouldGenerateSkills = delivery !== 'commands';
const shouldGenerateCommands = delivery !== 'skills';
if (shouldGenerateSkills) {
for (const workflow of knownDesiredWorkflows) {
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
const skillFile = path.join(skillsDir, dirName, 'SKILL.md');
if (!fs.existsSync(skillFile)) {
return true;
}
}
// Deselecting workflows in a profile should trigger sync.
for (const workflow of ALL_WORKFLOWS) {
if (desiredWorkflowSet.has(workflow))
continue;
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
const skillDir = path.join(skillsDir, dirName);
if (fs.existsSync(skillDir)) {
return true;
}
}
}
else {
for (const workflow of ALL_WORKFLOWS) {
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
const skillDir = path.join(skillsDir, dirName);
if (fs.existsSync(skillDir)) {
return true;
}
}
}
if (shouldGenerateCommands && adapter) {
for (const workflow of knownDesiredWorkflows) {
const cmdPath = adapter.getFilePath(workflow);
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
if (!fs.existsSync(fullPath)) {
return true;
}
}
// Deselecting workflows in a profile should trigger sync.
for (const workflow of ALL_WORKFLOWS) {
if (desiredWorkflowSet.has(workflow))
continue;
const cmdPath = adapter.getFilePath(workflow);
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
if (fs.existsSync(fullPath)) {
return true;
}
}
}
else if (!shouldGenerateCommands && adapter) {
for (const workflow of ALL_WORKFLOWS) {
const cmdPath = adapter.getFilePath(workflow);
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
if (fs.existsSync(fullPath)) {
return true;
}
}
}
return false;
}
/**
* Returns configured tools that currently need a profile/delivery sync.
*/
export function getToolsNeedingProfileSync(projectPath, desiredWorkflows, delivery, configuredTools) {
const tools = configuredTools ? [...new Set(configuredTools)] : getConfiguredToolsForProfileSync(projectPath);
return tools.filter((toolId) => hasToolProfileOrDeliveryDrift(projectPath, toolId, desiredWorkflows, delivery));
}
function getInstalledWorkflowsForTool(projectPath, toolId, options) {
const tool = AI_TOOLS.find((t) => t.value === toolId);
if (!tool?.skillsDir)
return [];
const installed = new Set();
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
if (options.includeSkills) {
for (const workflow of ALL_WORKFLOWS) {
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
const skillFile = path.join(skillsDir, dirName, 'SKILL.md');
if (fs.existsSync(skillFile)) {
installed.add(workflow);
}
}
}
if (options.includeCommands) {
const adapter = CommandAdapterRegistry.get(toolId);
if (adapter) {
for (const workflow of ALL_WORKFLOWS) {
const cmdPath = adapter.getFilePath(workflow);
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
if (fs.existsSync(fullPath)) {
installed.add(workflow);
}
}
}
}
return [...installed];
}
/**
* Detects whether the current project has any profile/delivery drift.
*/
export function hasProjectConfigDrift(projectPath, desiredWorkflows, delivery) {
const configuredTools = getConfiguredToolsForProfileSync(projectPath);
if (getToolsNeedingProfileSync(projectPath, desiredWorkflows, delivery, configuredTools).length > 0) {
return true;
}
const desiredSet = new Set(toKnownWorkflows(desiredWorkflows));
const includeSkills = delivery !== 'commands';
const includeCommands = delivery !== 'skills';
for (const toolId of configuredTools) {
const installed = getInstalledWorkflowsForTool(projectPath, toolId, { includeSkills, includeCommands });
if (installed.some((workflow) => !desiredSet.has(workflow))) {
return true;
}
}
return false;
}
//# sourceMappingURL=profile-sync-drift.js.map
/**
* Profile System
*
* Defines workflow profiles that control which workflows are installed.
* Profiles determine WHICH workflows; delivery (in global config) determines HOW.
*/
import type { Profile } from './global-config.js';
/**
* Core workflows included in the 'core' profile.
* These provide the streamlined experience for new users.
*/
export declare const CORE_WORKFLOWS: readonly ["propose", "explore", "apply", "archive"];
/**
* All available workflows in the system.
*/
export declare const ALL_WORKFLOWS: readonly ["propose", "explore", "new", "continue", "apply", "ff", "sync", "archive", "bulk-archive", "verify", "onboard"];
export type WorkflowId = (typeof ALL_WORKFLOWS)[number];
export type CoreWorkflowId = (typeof CORE_WORKFLOWS)[number];
/**
* Resolves which workflows should be active for a given profile configuration.
*
* - 'core' profile always returns CORE_WORKFLOWS
* - 'custom' profile returns the provided customWorkflows, or empty array if not provided
*/
export declare function getProfileWorkflows(profile: Profile, customWorkflows?: string[]): readonly string[];
//# sourceMappingURL=profiles.d.ts.map
/**
* Profile System
*
* Defines workflow profiles that control which workflows are installed.
* Profiles determine WHICH workflows; delivery (in global config) determines HOW.
*/
/**
* Core workflows included in the 'core' profile.
* These provide the streamlined experience for new users.
*/
export const CORE_WORKFLOWS = ['propose', 'explore', 'apply', 'archive'];
/**
* All available workflows in the system.
*/
export const ALL_WORKFLOWS = [
'propose',
'explore',
'new',
'continue',
'apply',
'ff',
'sync',
'archive',
'bulk-archive',
'verify',
'onboard',
];
/**
* Resolves which workflows should be active for a given profile configuration.
*
* - 'core' profile always returns CORE_WORKFLOWS
* - 'custom' profile returns the provided customWorkflows, or empty array if not provided
*/
export function getProfileWorkflows(profile, customWorkflows) {
if (profile === 'custom') {
return customWorkflows ?? [];
}
return CORE_WORKFLOWS;
}
//# sourceMappingURL=profiles.js.map
/**
* Core template types for skills and slash commands.
*/
export interface SkillTemplate {
name: string;
description: string;
instructions: string;
license?: string;
compatibility?: string;
metadata?: Record<string, string>;
}
export interface CommandTemplate {
name: string;
description: string;
category: string;
tags: string[];
content: string;
}
//# sourceMappingURL=types.d.ts.map
/**
* Core template types for skills and slash commands.
*/
export {};
//# sourceMappingURL=types.js.map
/**
* Skill Template Workflow Modules
*
* This file is generated by splitting the legacy monolithic
* templates file into workflow-focused modules.
*/
import type { SkillTemplate, CommandTemplate } from '../types.js';
export declare function getApplyChangeSkillTemplate(): SkillTemplate;
export declare function getOpsxApplyCommandTemplate(): CommandTemplate;
//# sourceMappingURL=apply-change.d.ts.map
export function getApplyChangeSkillTemplate() {
return {
name: 'openspec-apply-change',
description: '实现 OpenSpec 变更中的任务。当用户想要开始实现、继续实现或处理任务时使用。',
instructions: `实现 OpenSpec 变更中的任务。
**输入**:可选指定变更名称。如果省略,检查是否可以从对话上下文中推断。如果模糊或不明确,你**必须**提示获取可用变更。
**步骤**
1. **选择变更**
如果提供了名称,使用它。否则:
- 如果用户提到了某个变更,从对话上下文中推断
- 如果只存在一个活动变更,自动选择
- 如果不明确,运行 \`openspec-cn list --json\` 获取可用变更,并使用 **AskUserQuestion tool** 让用户选择
始终宣布:"正在使用变更:<name>"以及如何覆盖(例如,\`/opsx:apply <other>\`)。
2. **检查状态以了解 Schema**
\`\`\`bash
openspec-cn status --change "<name>" --json
\`\`\`
解析 JSON 以了解:
- \`schemaName\`:正在使用的工作流(例如:"spec-driven")
- 哪个产出物包含任务(对于 spec-driven 通常是 "tasks",检查其他产出物的状态)
3. **获取应用指令**
\`\`\`bash
openspec-cn instructions apply --change "<name>" --json
\`\`\`
这返回:
- 上下文文件路径(因 Schema 而异 - 可能是 proposal/specs/design/tasks 或 spec/tests/implementation/docs)
- 进度(总计,完成,剩余)
- 带有状态的任务列表
- 基于当前状态的动态指令
**处理状态:**
- 如果 \`state: "blocked"\`(缺少产出物):显示消息,建议使用 openspec-continue-change
- 如果 \`state: "all_done"\`:祝贺,建议归档
- 否则:继续实现
4. **阅读上下文文件**
阅读 apply instructions 输出中 \`contextFiles\` 列出的文件。
文件取决于正在使用的 Schema:
- **spec-driven**: proposal, specs, design, tasks
- 其他模式:遵循 CLI 输出中的 contextFiles
5. **显示当前进度**
显示:
- 正在使用的 Schema
- 进度:"N/M 任务已完成"
- 剩余任务概览
- 来自 CLI 的动态指令
6. **实现任务(循环直到完成或受阻)**
对于每个待处理任务:
- 显示正在处理哪个任务
- 进行所需的代码更改
- 保持更改最小化且专注
- 在任务文件中标记任务完成:\`- [ ]\` → \`- [x]\`
- 继续下一个任务
**暂停如果:**
- 任务不清楚 → 询问澄清
- 实现揭示了设计问题 → 建议更新产出物
- 遇到错误或阻碍 → 报告并等待指导
- 用户中断
7. **完成或暂停时,显示状态**
显示:
- 本次会话完成的任务
- 总体进度:"N/M 任务已完成"
- 如果全部完成:建议归档
- 如果暂停:解释原因并等待指导
**实现期间的输出**
\`\`\`
## 正在实现:<change-name> (schema: <schema-name>)
正在处理任务 3/7:<task description>
[...正在进行实现...]
✓ 任务完成
正在处理任务 4/7:<task description>
[...正在进行实现...]
✓ 任务完成
\`\`\`
**完成时的输出**
\`\`\`
## 实现完成
**变更:** <change-name>
**Schema:** <schema-name>
**进度:** 7/7 任务已完成 ✓
### 本次会话已完成
- [x] 任务 1
- [x] 任务 2
...
所有任务已完成!准备归档此变更。
\`\`\`
**暂停时的输出(遇到问题)**
\`\`\`
## 实现暂停
**变更:** <change-name>
**Schema:** <schema-name>
**进度:** 4/7 任务已完成
### 遇到的问题
<问题描述>
**选项:**
1. <选项 1>
2. <选项 2>
3. 其他方法
您想怎么做?
\`\`\`
**护栏**
- 继续执行任务直到完成或受阻
- 开始前始终阅读上下文文件(来自 apply instructions 输出)
- 如果任务模棱两可,暂停并在实现前询问
- 如果实现揭示了问题,暂停并建议更新产出物
- 保持代码更改最小化并限定在每个任务范围内
- 完成每个任务后立即更新任务复选框
- 遇到错误、阻碍或不清楚的需求时暂停 - 不要猜测
- 使用 CLI 输出中的 contextFiles,不要假设特定的文件名
**流畅的工作流集成**
此技能支持"变更上的操作"模型:
- **可以随时调用**:在所有产出物完成之前(如果存在任务),部分实现之后,与其他操作交错
- **允许产出物更新**:如果实现揭示了设计问题,建议更新产出物 - 不是阶段锁定的,流畅地工作`,
license: 'MIT',
compatibility: '需要 openspec CLI。',
metadata: { author: 'openspec', version: '1.0' },
};
}
export function getOpsxApplyCommandTemplate() {
return {
name: 'OPSX: 应用',
description: '实现 OpenSpec 变更中的任务(实验性)',
category: '工作流',
tags: ['workflow', 'artifacts', 'experimental'],
content: `实现 OpenSpec 变更中的任务。
**输入**:可选择指定变更名称(例如,\`/opsx:apply add-auth\`)。如果省略,检查是否可以从对话上下文中推断出来。如果模糊或不明确,你必须提示可用的变更。
**步骤**
1. **选择变更**
如果提供了名称,使用它。否则:
- 如果用户提到了某个变更,从对话上下文中推断
- 如果只存在一个活动变更,自动选择
- 如果不明确,运行 \`openspec-cn list --json\` 获取可用变更,并使用 **AskUserQuestion tool** 让用户选择
始终宣布:"正在使用变更:<name>"以及如何覆盖(例如,\`/opsx:apply <other>\`)。
2. **检查状态以了解 Schema**
\`\`\`bash
openspec-cn status --change "<name>" --json
\`\`\`
解析 JSON 以了解:
- \`schemaName\`:正在使用的工作流(例如:"spec-driven")
- 哪个产出物包含任务(对于 spec-driven 通常是 "tasks",检查其他产出物的状态)
3. **获取应用指令**
\`\`\`bash
openspec-cn instructions apply --change "<name>" --json
\`\`\`
这返回:
- 上下文文件路径(因 Schema 而异)
- 进度(总计,完成,剩余)
- 带有状态的任务列表
- 基于当前状态的动态指令
**处理状态:**
- 如果 \`state: "blocked"\`(缺少产出物):显示消息,建议使用 \`/opsx:continue\`
- 如果 \`state: "all_done"\`:祝贺,建议归档
- 否则:继续实现
4. **阅读上下文文件**
阅读 apply instructions 输出中 \`contextFiles\` 列出的文件。
文件取决于正在使用的 Schema:
- **spec-driven**: proposal, specs, design, tasks
- 其他模式:遵循 CLI 输出中的 contextFiles
5. **显示当前进度**
显示:
- 正在使用的 Schema
- 进度:"N/M 任务已完成"
- 剩余任务概览
- 来自 CLI 的动态指令
6. **实现任务(循环直到完成或受阻)**
对于每个待处理任务:
- 显示正在处理哪个任务
- 进行所需的代码更改
- 保持更改最小化且专注
- 在任务文件中标记任务完成:\`- [ ]\` → \`- [x]\`
- 继续下一个任务
**暂停如果:**
- 任务不清楚 → 询问澄清
- 实现揭示了设计问题 → 建议更新产出物
- 遇到错误或阻碍 → 报告并等待指导
- 用户中断
7. **完成或暂停时,显示状态**
显示:
- 本次会话完成的任务
- 总体进度:"N/M 任务已完成"
- 如果全部完成:建议归档
- 如果暂停:解释原因并等待指导
**实现期间的输出**
\`\`\`
## 正在实现:<change-name> (schema: <schema-name>)
正在处理任务 3/7:<task description>
[...正在进行实现...]
✓ 任务完成
正在处理任务 4/7:<task description>
[...正在进行实现...]
✓ 任务完成
\`\`\`
**完成时的输出**
\`\`\`
## 实现完成
**变更:** <change-name>
**Schema:** <schema-name>
**进度:** 7/7 任务已完成 ✓
### 本次会话已完成
- [x] 任务 1
- [x] 任务 2
...
所有任务已完成!您可以使用 \`/opsx:archive\` 归档此变更。
\`\`\`
**暂停时的输出(遇到问题)**
\`\`\`
## 实现暂停
**变更:** <change-name>
**Schema:** <schema-name>
**进度:** 4/7 任务已完成
### 遇到的问题
<问题描述>
**选项:**
1. <选项 1>
2. <选项 2>
3. 其他方法
您想怎么做?
\`\`\`
**护栏**
- 继续执行任务直到完成或受阻
- 开始前始终阅读上下文文件(来自 apply instructions 输出)
- 如果任务模棱两可,暂停并在实现前询问
- 如果实现揭示了问题,暂停并建议更新产出物
- 保持代码更改最小化并限定在每个任务范围内
- 完成每个任务后立即更新任务复选框
- 遇到错误、阻碍或不清楚的需求时暂停 - 不要猜测
- 使用 CLI 输出中的 contextFiles,不要假设特定的文件名
**流畅的工作流集成**
此技能支持"变更上的操作"模型:
- **可以随时调用**:在所有产出物完成之前(如果存在任务),部分实现之后,与其他操作交错
- **允许产出物更新**:如果实现揭示了设计问题,建议更新产出物 - 不是阶段锁定的,流畅地工作`
};
}
//# sourceMappingURL=apply-change.js.map
/**
* Skill Template Workflow Modules
*
* This file is generated by splitting the legacy monolithic
* templates file into workflow-focused modules.
*/
import type { SkillTemplate, CommandTemplate } from '../types.js';
export declare function getArchiveChangeSkillTemplate(): SkillTemplate;
export declare function getOpsxArchiveCommandTemplate(): CommandTemplate;
//# sourceMappingURL=archive-change.d.ts.map
export function getArchiveChangeSkillTemplate() {
return {
name: 'openspec-archive-change',
description: '归档实验性工作流中已完成的变更。当用户想要在实现完成后最终确定并归档变更时使用。',
instructions: `归档实验性工作流中已完成的变更。
**输入**:可选指定变更名称。如果省略,检查是否可以从对话上下文中推断。如果模糊或不明确,你**必须**提示获取可用变更。
**步骤**
1. **如果没有提供变更名称,提示选择**
运行 \`openspec-cn list --json\` 获取可用变更。使用 **AskUserQuestion tool** 让用户选择。
仅显示活动变更(未归档的)。
如果可用,包括每个变更使用的 Schema。
**重要提示**:不要猜测或自动选择变更。始终让用户选择。
2. **检查产出物完成状态**
运行 \`openspec-cn status --change "<name>" --json\` 检查产出物完成情况。
解析 JSON 以了解:
- \`schemaName\`:正在使用的工作流
- \`artifacts\`:产出物列表及其状态(\`done\` 或其他)
**如果有任何产出物未 \`done\`:**
- 显示列出未完成产出物的警告
- 使用 **AskUserQuestion tool** 确认用户是否要继续
- 如果用户确认,则继续
3. **检查任务完成状态**
阅读任务文件(通常是 \`tasks.md\`)以检查未完成的任务。
统计标记为 \`- [ ]\`(未完成)与 \`- [x]\`(已完成)的任务。
**如果发现未完成的任务:**
- 显示警告,显示未完成任务的数量
- 使用 **AskUserQuestion tool** 确认用户是否要继续
- 如果用户确认,则继续
**如果没有任务文件存在:** 继续,无需任务相关警告。
4. **评估增量规范同步状态**
检查 \`openspec/changes/<name>/specs/\` 中的增量规范。如果不存在,则在没有同步提示的情况下继续。
**如果存在增量规范:**
- 将每个增量规范与其在 \`openspec/specs/<capability>/spec.md\` 对应的各主规范进行比较
- 确定将应用哪些更改(添加、修改、移除、重命名)
- 在提示前显示综合摘要
**提示选项:**
- 如果需要更改:"立即同步(推荐)"、"归档而不同步"
- 如果已同步:"立即归档"、"仍然同步"、"取消"
如果用户选择同步,使用 Task tool(subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>")。无论选择如何,都继续归档。
5. **执行归档**
如果归档目录不存在,则创建它:
\`\`\`bash
mkdir -p openspec/changes/archive
\`\`\`
使用当前日期生成目标名称:\`YYYY-MM-DD-<change-name>\`
**检查目标是否已存在:**
- 如果是:失败并报错,建议重命名现有归档或使用不同日期
- 如果否:将变更目录移动到归档
\`\`\`bash
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
\`\`\`
6. **显示摘要**
显示归档完成摘要,包括:
- 变更名称
- 使用的 Schema
- 归档位置
- 规范是否已同步(如果适用)
- 关于任何警告的说明(未完成的产出物/任务)
**成功时的输出**
\`\`\`
## 归档完成
**变更:** <change-name>
**模式:** <schema-name>
**归档至:** openspec/changes/archive/YYYY-MM-DD-<name>/
**规范:** ✓ 已同步到主规范(或 "无增量规范" 或 "同步已跳过")
所有产出物已完成。所有任务已完成。
\`\`\`
**防护措施**
- 如果未提供变更,始终提示选择
- 使用产出物图(openspec-cn status --json)进行完成度检查
- 不要在警告时阻止归档 - 只需告知并确认
- 移动到归档时保留 .openspec.yaml(它与目录一起移动)
- 显示清晰的操作摘要
- 如果请求同步,使用 openspec-sync-specs 方法(代理驱动)
- 如果存在增量规格说明,始终运行同步评估并在提示前显示综合摘要`,
license: 'MIT',
compatibility: '需要 openspec CLI。',
metadata: { author: 'openspec', version: '1.0' },
};
}
export function getOpsxArchiveCommandTemplate() {
return {
name: 'OPSX: 归档',
description: '归档实验性工作流中已完成的变更',
category: '工作流',
tags: ['workflow', 'archive', 'experimental'],
content: `归档实验性工作流中已完成的变更。
**输入**:可选择在 \`/opsx:archive\` 后指定变更名称(例如,\`/opsx:archive add-auth\`)。如果省略,检查是否可以从对话上下文中推断出来。如果模糊或不明确,你必须提示可用的变更。
**步骤**
1. **如果没有提供变更名称,提示选择**
运行 \`openspec-cn list --json\` 获取可用变更。使用 **AskUserQuestion tool** 让用户选择。
仅显示活动变更(未归档的)。
如果可用,包括每个变更使用的 Schema。
**重要提示**:不要猜测或自动选择变更。始终让用户选择。
2. **检查产出物完成状态**
运行 \`openspec-cn status --change "<name>" --json\` 检查产出物完成情况。
解析 JSON 以了解:
- \`schemaName\`:正在使用的工作流
- \`artifacts\`:产出物列表及其状态(\`done\` 或其他)
**如果有任何产出物未 \`done\`:**
- 显示列出未完成产出物的警告
- 提示用户确认是否继续
- 如果用户确认,则继续
3. **检查任务完成状态**
阅读任务文件(通常是 \`tasks.md\`)以检查未完成的任务。
统计标记为 \`- [ ]\`(未完成)与 \`- [x]\`(已完成)的任务。
**如果发现未完成的任务:**
- 显示警告,显示未完成任务的数量
- 提示用户确认是否继续
- 如果用户确认,则继续
**如果没有任务文件存在:** 继续,无需任务相关警告。
4. **评估增量规格说明同步状态**
在 \`openspec/changes/<name>/specs/\` 检查增量规格说明。如果不存在,不提示同步直接继续。
**如果存在增量规格说明:**
- 将每个增量规格说明与其在 \`openspec/specs/<capability>/spec.md\` 的相应主规格说明进行比较
- 确定将应用哪些更改(添加、修改、删除、重命名)
- 在提示前显示合并摘要
**提示选项:**
- 如果需要更改:"立即同步(推荐)","不同步直接归档"
- 如果已同步:"立即归档","仍要同步","取消"
如果用户选择同步,执行 \`/opsx:sync\` 逻辑。无论选择如何都继续归档。
5. **执行归档**
如果归档目录不存在,则创建它:
\`\`\`bash
mkdir -p openspec/changes/archive
\`\`\`
使用当前日期生成目标名称:\`YYYY-MM-DD-<change-name>\`
**检查目标是否已存在:**
- 如果是:失败并报错,建议重命名现有归档或使用不同日期
- 如果否:将变更目录移动到归档
\`\`\`bash
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
\`\`\`
6. **显示摘要**
显示归档完成摘要,包括:
- 变更名称
- 使用的 Schema
- 归档位置
- 规格说明同步状态(已同步 / 跳过同步 / 无增量规格说明)
- 任何警告的注释(未完成的产出物/任务)
**成功时的输出**
\`\`\`
## 归档完成
**变更:** <change-name>
**Schema:** <schema-name>
**归档至:** openspec/changes/archive/YYYY-MM-DD-<name>/
**规范:** ✓ 已同步到主规范
所有产出物已完成。所有任务已完成。
\`\`\`
**成功时的输出(无增量规范)**
\`\`\`
## 归档完成
**变更:** <change-name>
**Schema:** <schema-name>
**归档至:** openspec/changes/archive/YYYY-MM-DD-<name>/
**规范:** 无增量规范
所有产出物已完成。所有任务已完成。
\`\`\`
**成功时的输出(带警告)**
\`\`\`
## 归档完成(带警告)
**变更:** <change-name>
**Schema:** <schema-name>
**归档至:** openspec/changes/archive/YYYY-MM-DD-<name>/
**规格说明:** 跳过同步(用户选择跳过)
**警告:**
- 带有 2 个未完成产出物的归档
- 带有 3 个未完成任务的归档
- 增量规格说明同步已跳过(用户选择跳过)
如果这不是故意的,请检查归档。
\`\`\`
**错误时的输出(归档已存在)**
\`\`\`
## 归档失败
**变更:** <change-name>
**目标:** openspec/changes/archive/YYYY-MM-DD-<name>/
目标归档目录已存在。
**选项:**
1. 重命名现有归档
2. 如果是重复的,删除现有归档
3. 等待不同的日期再归档
\`\`\`
**防护措施**
- 如果未提供变更,始终提示选择
- 使用产出物图(openspec-cn status --json)进行完成度检查
- 不要在警告时阻止归档 - 只需告知并确认
- 移动到归档时保留 .openspec.yaml(它与目录一起移动)
- 显示清晰的操作摘要
- 如果请求同步,使用 Skill tool 调用 \`openspec-sync-specs\`(代理驱动)
- 如果存在增量规格说明,请始终运行同步评估,并在提示前显示综合摘要`
};
}
//# sourceMappingURL=archive-change.js.map
/**
* Skill Template Workflow Modules
*
* This file is generated by splitting the legacy monolithic
* templates file into workflow-focused modules.
*/
import type { SkillTemplate, CommandTemplate } from '../types.js';
export declare function getBulkArchiveChangeSkillTemplate(): SkillTemplate;
export declare function getOpsxBulkArchiveCommandTemplate(): CommandTemplate;
//# sourceMappingURL=bulk-archive-change.d.ts.map
export function getBulkArchiveChangeSkillTemplate() {
return {
name: 'openspec-bulk-archive-change',
description: '一次归档多个已完成的变更。用于归档多个并行变更。',
instructions: `在单个操作中归档多个已完成的变更。
此技能允许您批量归档变更,通过检查代码库以确定实际实现了什么来智能处理规格说明冲突。
**输入**:无需要求(会提示选择)
**步骤**
1. **获取活动变更**
运行 \`openspec-cn list --json\` 获取所有活动变更。
如果不存在活动变更,通知用户并停止。
2. **提示变更选择**
使用 **AskUserQuestion 工具**进行多选,让用户选择变更:
- 显示每个变更及其 Schema
- 包含"所有变更"选项
- 允许任意数量的选择(1+ 可用,2+ 是典型用例)
**重要提示**:不要自动选择。始终让用户选择。
3. **批量验证 - 收集所有选定变更的状态**
对于每个选定的变更,收集:
a. **产出物状态** - 运行 \`openspec-cn status --change "<name>" --json\`
- 解析 \`schemaName\` 和 \`artifacts\` 列表
- 注意哪些产出物是 \`done\` 状态而非其他状态
b. **任务完成度** - 读取 \`openspec/changes/<name>/tasks.md\`
- 统计 \`- [ ]\`(未完成)与 \`- [x]\`(已完成)
- 如果不存在任务文件,标注为"无任务"
c. **增量规格说明** - 检查 \`openspec/changes/<name>/specs/\` 目录
- 列出存在哪些能力规格说明
- 对于每个,提取需求名称(匹配 \`### 需求: <name>\` 的行)
4. **检测规格说明冲突**
构建 \`capability -> [涉及它的变更]\` 映射:
\`\`\`
auth -> [change-a, change-b] <- 冲突(2+ 个变更)
api -> [change-c] <- 正常(仅 1 个变更)
\`\`\`
当 2+ 个选定的变更具有相同能力的增量规格说明时,存在冲突。
5. **代理式解决冲突**
**对于每个冲突**,调查代码库:
a. **读取增量规格说明** 从每个冲突的变更中了解每个声称添加/修改的内容
b. **搜索代码库** 寻找实现证据:
- 查找实现每个增量规格说明中需求的代码
- 检查相关文件、函数或测试
c. **确定解决方案**:
- 如果只有一个变更实际实现 -> 同步该变更的规格说明
- 如果两者都实现 -> 按时间顺序应用(旧的先,新的覆盖)
- 如果两者都未实现 -> 跳过规格说明同步,警告用户
d. **记录解决方案** 对于每个冲突:
- 应用哪个变更的规格说明
- 按什么顺序(如果两者都有)
- 原理(在代码库中找到了什么)
6. **显示合并状态表**
显示汇总所有变更的表:
\`\`\`
| 变更 | 产出物 | 任务 | 规格说明 | 冲突 | 状态 |
|---------------------|-----------|-------|---------|-----------|--------|
| schema-management | 完成 | 5/5 | 2 增量 | 无 | 就绪 |
| project-config | 完成 | 3/3 | 1 增量 | 无 | 就绪 |
| add-oauth | 完成 | 4/4 | 1 增量 | auth (!) | 就绪* |
| add-verify-skill | 剩余 1 | 2/5 | 无 | 无 | 警告 |
\`\`\`
对于冲突,显示解决方案:
\`\`\`
* 冲突解决方案:
- auth 规格说明:将先应用 add-oauth 然后 add-jwt(两者都已实现,按时间顺序)
\`\`\`
对于未完成的变更,显示警告:
\`\`\`
警告:
- add-verify-skill:1 个未完成产出物,3 个未完成任务
\`\`\`
7. **确认批量操作**
使用 **AskUserQuestion 工具**进行单次确认:
- "归档 N 个变更?"根据状态提供选项
- 选项可能包括:
- "归档所有 N 个变更"
- "仅归档 N 个就绪变更(跳过未完成的)"
- "取消"
如果存在未完成的变更,请明确说明它们将带着警告被归档。
8. **对每个确认的变更执行归档**
按确定的顺序处理变更(遵循冲突解决方案):
a. **如果存在增量规格说明则同步规格说明**:
- 使用 openspec-sync-specs 方法(代理驱动的智能合并)
- 对于冲突,按已解决的顺序应用
- 跟踪是否已完成同步
b. **执行归档**:
\`\`\`bash
mkdir -p openspec/changes/archive
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
\`\`\`
c. **跟踪每个变更的结果**:
- 成功:成功归档
- 失败:归档期间出错(记录错误)
- 跳过:用户选择不归档(如适用)
9. **显示摘要**
显示最终结果:
\`\`\`
## 批量归档完成
已归档 3 个变更:
- schema-management-cli -> archive/2026-01-19-schema-management-cli/
- project-config -> archive/2026-01-19-project-config/
- add-oauth -> archive/2026-01-19-add-oauth/
跳过 1 个变更:
- add-verify-skill(用户选择不归档未完成的)
规格说明同步摘要:
- 4 个增量规格说明已同步到主规格说明
- 1 个冲突已解决(auth:按时间顺序应用两者)
\`\`\`
如果有任何失败:
\`\`\`
失败 1 个变更:
- some-change:归档目录已存在
\`\`\`
**冲突解决示例**
示例 1:仅一个已实现
\`\`\`
冲突:specs/auth/spec.md 被 [add-oauth, add-jwt] 涉及
检查 add-oauth:
- 增量添加"OAuth 提供商集成"需求
- 搜索代码库... 找到 src/auth/oauth.ts 实现 OAuth 流程
检查 add-jwt:
- 增量添加"JWT 令牌处理"需求
- 搜索代码库... 未找到 JWT 实现
解决方案:仅 add-oauth 已实现。将仅同步 add-oauth 规格说明。
\`\`\`
示例 2:两者都已实现
\`\`\`
冲突:specs/api/spec.md 被 [add-rest-api, add-graphql] 涉及
检查 add-rest-api(创建于 2026-01-10):
- 增量添加"REST 端点"需求
- 搜索代码库... 找到 src/api/rest.ts
检查 add-graphql(创建于 2026-01-15):
- 增量添加"GraphQL 架构"需求
- 搜索代码库... 找到 src/api/graphql.ts
解决方案:两者都已实现。将先应用 add-rest-api 规格说明,
然后应用 add-graphql 规格说明(按时间顺序,较新的优先)。
\`\`\`
**成功时的输出**
\`\`\`
## 批量归档完成
已归档 N 个变更:
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
- <change-2> -> archive/YYYY-MM-DD-<change-2>/
规格说明同步摘要:
- N 个增量规格说明已同步到主规格说明
- 无冲突(或:M 个冲突已解决)
\`\`\`
**部分成功时的输出**
\`\`\`
## 批量归档完成(部分)
已归档 N 个变更:
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
跳过 M 个变更:
- <change-2>(用户选择不归档未完成的)
失败 K 个变更:
- <change-3>:归档目录已存在
\`\`\`
**没有变更时的输出**
\`\`\`
## 无需归档的变更
未找到活动变更。使用 \`/opsx:new\` 创建新变更。
\`\`\`
**防护措施**
- 允许任意数量的变更(1+ 可以,2+ 是典型用例)
- 始终提示选择,永不自动选择
- 及早检测规格说明冲突并通过检查代码库解决
- 当两个变更都已实现时,按时间顺序应用规格说明
- 仅当实现缺失时跳过规格说明同步(警告用户)
- 在确认前显示清晰的每个变更状态
- 对整个批次使用单次确认
- 跟踪并报告所有结果(成功/跳过/失败)
- 移动到归档时保留 .openspec.yaml
- 归档目录目标使用当前日期:YYYY-MM-DD-<name>
- 如果归档目标已存在,该变更失败但继续处理其他变更`,
license: 'MIT',
compatibility: '需要 openspec CLI。',
metadata: { author: 'openspec', version: '1.0' },
};
}
export function getOpsxBulkArchiveCommandTemplate() {
return {
name: 'OPSX: 批量归档',
description: '一次归档多个已完成的变更',
category: '工作流',
tags: ['workflow', 'archive', 'experimental', 'bulk'],
content: `在单个操作中归档多个已完成的变更。
此技能允许您批量归档变更,通过检查代码库以确定实际实现了什么来智能处理规格说明冲突。
**输入**:无需要求(会提示选择)
**步骤**
1. **获取活动变更**
运行 \`openspec-cn list --json\` 获取所有活动变更。
如果不存在活动变更,通知用户并停止。
2. **提示变更选择**
使用 **AskUserQuestion 工具**进行多选,让用户选择变更:
- 显示每个变更及其 Schema
- 包含"所有变更"选项
- 允许任意数量的选择(1+ 可用,2+ 是典型用例)
**重要提示**:不要自动选择。始终让用户选择。
3. **批量验证 - 收集所有选定变更的状态**
对于每个选定的变更,收集:
a. **产出物状态** - 运行 \`openspec-cn status --change "<name>" --json\`
- 解析 \`schemaName\` 和 \`artifacts\` 列表
- 注意哪些产出物是 \`done\` 状态而非其他状态
b. **任务完成度** - 读取 \`openspec/changes/<name>/tasks.md\`
- 统计 \`- [ ]\`(未完成)与 \`- [x]\`(已完成)
- 如果不存在任务文件,标注为"无任务"
c. **增量规格说明** - 检查 \`openspec/changes/<name>/specs/\` 目录
- 列出存在哪些能力规格说明
- 对于每个,提取需求名称(匹配 \`### 需求: <name>\` 的行)
4. **检测规格说明冲突**
构建 \`capability -> [涉及它的变更]\` 映射:
\`\`\`
auth -> [change-a, change-b] <- 冲突(2+ 个变更)
api -> [change-c] <- 正常(仅 1 个变更)
\`\`\`
当 2+ 个选定的变更具有相同能力的增量规格说明时,存在冲突。
5. **代理式解决冲突**
**对于每个冲突**,调查代码库:
a. **读取增量规格说明** 从每个冲突的变更中了解每个声称添加/修改的内容
b. **搜索代码库** 寻找实现证据:
- 查找实现每个增量规格说明中需求的代码
- 检查相关文件、函数或测试
c. **确定解决方案**:
- 如果只有一个变更实际实现 -> 同步该变更的规格说明
- 如果两者都实现 -> 按时间顺序应用(旧的先,新的覆盖)
- 如果两者都未实现 -> 跳过规格说明同步,警告用户
d. **记录解决方案** 对于每个冲突:
- 应用哪个变更的规格说明
- 按什么顺序(如果两者都有)
- 原理(在代码库中找到了什么)
6. **显示合并状态表**
显示汇总所有变更的表:
\`\`\`
| 变更 | 产出物 | 任务 | 规格说明 | 冲突 | 状态 |
|---------------------|-----------|-------|---------|-----------|--------|
| schema-management | 完成 | 5/5 | 2 增量 | 无 | 就绪 |
| project-config | 完成 | 3/3 | 1 增量 | 无 | 就绪 |
| add-oauth | 完成 | 4/4 | 1 增量 | auth (!) | 就绪* |
| add-verify-skill | 剩余 1 | 2/5 | 无 | 无 | 警告 |
\`\`\`
对于冲突,显示解决方案:
\`\`\`
* 冲突解决方案:
- auth 规格说明:将先应用 add-oauth 然后 add-jwt(两者都已实现,按时间顺序)
\`\`\`
对于未完成的变更,显示警告:
\`\`\`
警告:
- add-verify-skill:1 个未完成产出物,3 个未完成任务
\`\`\`
7. **确认批量操作**
使用 **AskUserQuestion 工具**进行单次确认:
- "归档 N 个变更?"根据状态提供选项
- 选项可能包括:
- "归档所有 N 个变更"
- "仅归档 N 个就绪变更(跳过未完成的)"
- "取消"
如果存在未完成的变更,请明确说明它们将带着警告被归档。
8. **对每个确认的变更执行归档**
按确定的顺序处理变更(遵循冲突解决方案):
a. **如果存在增量规格说明则同步规格说明**:
- 使用 openspec-sync-specs 方法(代理驱动的智能合并)
- 对于冲突,按已解决的顺序应用
- 跟踪是否已完成同步
b. **执行归档**:
\`\`\`bash
mkdir -p openspec/changes/archive
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
\`\`\`
c. **跟踪每个变更的结果**:
- 成功:成功归档
- 失败:归档期间出错(记录错误)
- 跳过:用户选择不归档(如适用)
9. **显示摘要**
显示最终结果:
\`\`\`
## 批量归档完成
已归档 3 个变更:
- schema-management-cli -> archive/2026-01-19-schema-management-cli/
- project-config -> archive/2026-01-19-project-config/
- add-oauth -> archive/2026-01-19-add-oauth/
跳过 1 个变更:
- add-verify-skill(用户选择不归档未完成的)
规格说明同步摘要:
- 4 个增量规格说明已同步到主规格说明
- 1 个冲突已解决(auth:按时间顺序应用两者)
\`\`\`
如果有任何失败:
\`\`\`
失败 1 个变更:
- some-change:归档目录已存在
\`\`\`
**冲突解决示例**
示例 1:仅一个已实现
\`\`\`
冲突:specs/auth/spec.md 被 [add-oauth, add-jwt] 涉及
检查 add-oauth:
- 增量添加"OAuth 提供商集成"需求
- 搜索代码库... 找到 src/auth/oauth.ts 实现 OAuth 流程
检查 add-jwt:
- 增量添加"JWT 令牌处理"需求
- 搜索代码库... 未找到 JWT 实现
解决方案:仅 add-oauth 已实现。将仅同步 add-oauth 规格说明。
\`\`\`
示例 2:两者都已实现
\`\`\`
冲突:specs/api/spec.md 被 [add-rest-api, add-graphql] 涉及
检查 add-rest-api(创建于 2026-01-10):
- 增量添加"REST 端点"需求
- 搜索代码库... 找到 src/api/rest.ts
检查 add-graphql(创建于 2026-01-15):
- 增量添加"GraphQL 架构"需求
- 搜索代码库... 找到 src/api/graphql.ts
解决方案:两者都已实现。将先应用 add-rest-api 规格说明,
然后应用 add-graphql 规格说明(按时间顺序,较新的优先)。
\`\`\`
**成功时的输出**
\`\`\`
## 批量归档完成
已归档 N 个变更:
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
- <change-2> -> archive/YYYY-MM-DD-<change-2>/
规格说明同步摘要:
- N 个增量规格说明已同步到主规格说明
- 无冲突(或:M 个冲突已解决)
\`\`\`
**部分成功时的输出**
\`\`\`
## 批量归档完成(部分)
已归档 N 个变更:
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
跳过 M 个变更:
- <change-2>(用户选择不归档未完成的)
失败 K 个变更:
- <change-3>:归档目录已存在
\`\`\`
**没有变更时的输出**
\`\`\`
## 无需归档的变更
未找到活动变更。使用 \`/opsx:new\` 创建新变更。
\`\`\`
**防护措施**
- 允许任意数量的变更(1+ 可以,2+ 是典型用例)
- 始终提示选择,永不自动选择
- 及早检测规格说明冲突并通过检查代码库解决
- 当两个变更都已实现时,按时间顺序应用规格说明
- 仅当实现缺失时跳过规格说明同步(警告用户)
- 在确认前显示清晰的每个变更状态
- 对整个批次使用单次确认
- 跟踪并报告所有结果(成功/跳过/失败)
- 移动到归档时保留 .openspec.yaml
- 归档目录目标使用当前日期:YYYY-MM-DD-<name>
- 如果归档目标已存在,该变更失败但继续处理其他变更`
};
}
//# sourceMappingURL=bulk-archive-change.js.map
/**
* Skill Template Workflow Modules
*
* This file is generated by splitting the legacy monolithic
* templates file into workflow-focused modules.
*/
import type { SkillTemplate, CommandTemplate } from '../types.js';
export declare function getContinueChangeSkillTemplate(): SkillTemplate;
export declare function getOpsxContinueCommandTemplate(): CommandTemplate;
//# sourceMappingURL=continue-change.d.ts.map
export function getContinueChangeSkillTemplate() {
return {
name: 'openspec-continue-change',
description: '通过创建下一个产出物继续处理 OpenSpec 变更。当用户想要推进其变更、创建下一个产出物或继续其工作流程时使用。',
instructions: `通过创建下一个产出物继续处理变更。
**输入**:可选指定变更名称。如果省略,检查是否可以从对话上下文中推断。如果模糊或不明确,你**必须**提示获取可用变更。
**步骤**
1. **如果没有提供变更名称,提示选择**
运行 \`openspec-cn list --json\` 获取按最近修改排序的可用变更。然后使用 **AskUserQuestion tool** 让用户选择要处理哪个变更。
展示前 3-4 个最近修改的变更作为选项,显示:
- 变更名称
- Schema(如果存在 \`schema\` 字段,否则为 "spec-driven")
- 状态(例如:"0/5 tasks", "complete", "no tasks")
- 最近修改时间(来自 \`lastModified\` 字段)
将最近修改的变更标记为 "(推荐)",因为它很可能是用户想要继续的。
**重要提示**:不要猜测或自动选择变更。始终让用户选择。
2. **检查当前状态**
\`\`\`bash
openspec-cn status --change "<name>" --json
\`\`\`
解析 JSON 以了解当前状态。响应包括:
- \`schemaName\`:正在使用的工作流 schema(例如:"spec-driven")
- \`artifacts\`:产出物数组及其状态("done"、"ready"、"blocked")
- \`isComplete\`:布尔值,表示是否所有产出物都已完成
3. **根据状态行动**:
---
**如果所有产出物已完成 (\`isComplete: true\`)**:
- 祝贺用户
- 显示最终状态,包括使用的 Schema
- 建议:"所有产出物已创建!您现在可以实现此变更或将其归档。"
- 停止
---
**如果产出物准备好创建**(状态显示有 \`status: "ready"\` 的产出物):
- 从状态输出中选择第一个 \`status: "ready"\` 的产出物
- 获取其指令:
\`\`\`bash
openspec-cn instructions <artifact-id> --change "<name>" --json
\`\`\`
- 解析 JSON。关键字段包括:
- \`context\`:项目背景(对你的约束 - 不要包含在输出中)
- \`rules\`:产出物特定规则(对你的约束 - 不要包含在输出中)
- \`template\`:输出文件使用的结构
- \`instruction\`:schema 特定指导
- \`outputPath\`:产出物写入路径
- \`dependencies\`:已完成的依赖产出物(用于读取上下文)
- **创建产出物文件**:
- 阅读任何已完成的依赖文件以获取上下文
- 使用 \`template\` 作为结构 - 填充各个章节
- 在编写时将 \`context\` 和 \`rules\` 作为约束 - 但不要把它们复制进文件
- 写入指令中指定的 outputPath
- 展示创建了什么,以及现在解锁了什么
- 创建一个产出物后停止
---
**如果没有产出物准备好(全部受阻)**:
- 在有效的 Schema 下不应发生这种情况
- 显示状态并建议检查问题
4. **创建产出物后,显示进度**
\`\`\`bash
openspec-cn status --change "<name>"
\`\`\`
**输出**
每次调用后,显示:
- 创建了哪个产出物
- 正在使用的 Schema 工作流
- 当前进度(N/M 完成)
- 现在解锁了哪些产出物
- 提示:"想要继续吗?只需让我继续或告诉我下一步做什么。"
**产出物创建指南**
产出物类型及其用途取决于 Schema。使用指令输出中的 \`instruction\` 字段来了解要创建什么。
常见的产出物模式:
**spec-driven schema**(proposal → specs → design → tasks):
- **proposal.md**:如果变更不清楚,先向用户确认。填写"为什么""什么变化""能力""影响"。
- "能力"部分很关键——列出的每个能力都需要一个 spec 文件。
- **specs/<capability>/spec.md**:为提案"能力"部分列出的每个能力创建一个 spec(使用 capability 名称,而不是 change 名称)。
- **design.md**:记录技术决策、架构和实现方法。
- **tasks.md**:把实现拆分为带复选框的任务。
对于其他 schema,遵循 CLI 输出中的 \`instruction\` 字段。
**护栏**
- 每次调用只创建一个产出物
- 创建新产出物前,总是先阅读依赖产出物
- 不要跳过产出物,也不要乱序创建
- 如果上下文不清楚,创建前先询问用户
- 写入后先确认产出物文件存在,再标记进度
- 使用 schema 的产出物顺序,不要假设固定的产出物名称
- **重要**:\`context\` 和 \`rules\` 是对你的约束,不是文件内容
- 不要把 \`<context>\`、\`<rules>\`、\`<project_context>\` 块复制进产出物
- 它们用于指导你写作,但绝不能出现在输出中`,
license: 'MIT',
compatibility: '需要 openspec CLI。',
metadata: { author: 'openspec', version: '1.0' },
};
}
export function getOpsxContinueCommandTemplate() {
return {
name: 'OPSX: 继续',
description: '继续处理变更 - 创建下一个产出物(实验性)',
category: '工作流',
tags: ['workflow', 'artifacts', 'experimental'],
content: `通过创建下一个产出物继续处理变更。
**输入**:可选择在 \`/opsx:continue\` 后指定变更名称(例如,\`/opsx:continue add-auth\`)。如果省略,检查是否可以从对话上下文中推断出来。如果模糊或不明确,你必须提示可用的变更。
**步骤**
1. **如果没有提供变更名称,提示选择**
运行 \`openspec-cn list --json\` 获取按最近修改排序的可用变更。然后使用 **AskUserQuestion tool** 让用户选择要处理哪个变更。
展示前 3-4 个最近修改的变更作为选项,显示:
- 变更名称
- Schema(如果存在 \`schema\` 字段,否则为 "spec-driven")
- 状态(例如:"0/5 tasks", "complete", "no tasks")
- 最近修改时间(来自 \`lastModified\` 字段)
将最近修改的变更标记为 "(推荐)",因为它很可能是用户想要继续的。
**重要提示**:不要猜测或自动选择变更。始终让用户选择。
2. **检查当前状态**
\`\`\`bash
openspec-cn status --change "<name>" --json
\`\`\`
解析 JSON 以了解当前状态。响应包括:
- \`schemaName\`:正在使用的工作流 schema(例如:"spec-driven")
- \`artifacts\`:产出物数组及其状态("done"、"ready"、"blocked")
- \`isComplete\`:布尔值,表示是否所有产出物都已完成
3. **根据状态行动**:
---
**如果所有产出物已完成 (\`isComplete: true\`)**:
- 祝贺用户
- 显示最终状态,包括使用的 Schema
- 建议:"所有产出物已创建!您现在可以使用 \`/opsx:apply\` 实施此变更或使用 \`/opsx:archive\` 归档它。"
- 停止
---
**如果产出物准备好创建**(状态显示有 \`status: "ready"\` 的产出物):
- 从状态输出中选择第一个 \`status: "ready"\` 的产出物
- 获取其指令:
\`\`\`bash
openspec-cn instructions <artifact-id> --change "<name>" --json
\`\`\`
- 解析 JSON。关键字段包括:
- \`context\`:项目背景(对你的约束 - 不要包含在输出中)
- \`rules\`:产出物特定规则(对你的约束 - 不要包含在输出中)
- \`template\`:用于输出文件的结构
- \`instruction\`:Schema 特定指导
- \`outputPath\`:写入产出物的位置
- \`dependencies\`:已完成的产出物,用于读取上下文
- **创建产出物文件**:
- 读取任何已完成的依赖文件以获取上下文
- 使用 \`template\` 作为结构 - 填充其各个部分
- 在编写时应用 \`context\` 和 \`rules\` 作为约束 - 但不要将它们复制到文件中
- 写入指令中指定的输出路径
- 显示创建的内容以及现在解锁的内容
- 创建一个产出物后停止
---
**如果没有产出物准备好(全部受阻)**:
- 在有效的 Schema 下不应发生这种情况
- 显示状态并建议检查问题
4. **创建产出物后,显示进度**
\`\`\`bash
openspec-cn status --change "<name>"
\`\`\`
**输出**
每次调用后,显示:
- 创建了哪个产出物
- 正在使用的 Schema 工作流
- 当前进度(N/M 完成)
- 现在解锁了哪些产出物
- 提示:"运行 \`/opsx:continue\` 以创建下一个产出物"
**产出物创建指南**
产出物类型及其用途取决于 Schema。使用指令输出中的 \`instruction\` 字段来了解要创建什么。
常见的产出物模式:
**spec-driven schema**(proposal → specs → design → tasks):
- **proposal.md**:如果变更不清楚,先向用户确认。填写"为什么""什么变化""能力""影响"。
- "能力"部分很关键——列出的每个能力都需要一个 spec 文件。
- **specs/<capability>/spec.md**:为提案"能力"部分列出的每个能力创建一个 spec(使用 capability 名称,而不是 change 名称)。
- **design.md**:记录技术决策、架构和实现方法。
- **tasks.md**:把实现拆分为带复选框的任务。
对于其他 schema,遵循 CLI 输出中的 \`instruction\` 字段。
**护栏**
- 每次调用只创建一个产出物
- 创建新产出物前,总是先阅读依赖产出物
- 不要跳过产出物,也不要乱序创建
- 如果上下文不清楚,创建前先询问用户
- 写入后先确认产出物文件存在,再标记进度
- 使用 schema 的产出物顺序,不要假设固定的产出物名称
- **重要**:\`context\` 和 \`rules\` 是对你的约束,不是文件内容
- 不要把 \`<context>\`、\`<rules>\`、\`<project_context>\` 块复制进产出物
- 它们用于指导你写作,但绝不能出现在输出中`
};
}
//# sourceMappingURL=continue-change.js.map
/**
* Skill Template Workflow Modules
*
* This file is generated by splitting the legacy monolithic
* templates file into workflow-focused modules.
*/
import type { SkillTemplate, CommandTemplate } from '../types.js';
export declare function getExploreSkillTemplate(): SkillTemplate;
export declare function getOpsxExploreCommandTemplate(): CommandTemplate;
//# sourceMappingURL=explore.d.ts.map
export function getExploreSkillTemplate() {
return {
name: 'openspec-explore',
description: '进入探索模式 - 一个用于探索想法、调查问题和澄清需求的思考伙伴。当用户想要在进行更改之前或期间深入思考某事时使用。',
instructions: `进入探索模式。深入思考。自由想象。跟随对话的任何方向。
**重要提示:探索模式是为了思考,而不是为了实施。** 你可以阅读文件、搜索代码和调查代码库,但你绝不能编写代码或实现功能。如果用户要求你实现某些内容,请提醒他们先退出探索模式(例如,使用 \`/opsx:new\` 或 \`/opsx:ff\` 开始变更)。如果用户要求,你可以创建 OpenSpec 产出物(提案、设计、规格说明)——这是捕捉思考,而不是实施。
**这是一种姿态,而不是一种工作流。** 没有固定的步骤,没有要求的顺序,没有强制性的输出。你是一个思考伙伴,帮助用户进行探索。
---
## 姿态
- **好奇而非说教** - 提出自然产生的问题,不要照本宣科
- **开放话题而非审问** - 浮现多个有趣的方向,让用户选择产生共鸣的部分。不要把他们限制在单一的提问路径中。
- **可视化** - 在有助于澄清思路时大方使用 ASCII 图表
- **自适应** - 跟随有趣的话题,当新信息出现时及时转换
- **耐心** - 不要急于下结论,让问题的轮廓自然显现
- **务实** - 在相关时探索实际的代码库,不要仅仅停留在理论上
---
## 你可能做的事情
根据用户提出的内容,你可能会:
**探索问题空间**
- 针对他们所说的内容提出澄清性问题
- 挑战假设
- 重新构建问题
- 寻找类比
**调查代码库**
- 绘制与讨论相关的现有架构图
- 寻找集成点
- 识别已在使用的模式
- 揭示隐藏的复杂性
**比较选项**
- 头脑风暴多种方法
- 构建比较表
- 勾勒权衡
- 推荐路径(如果被询问)
**可视化**
\`\`\`
┌─────────────────────────────────────────┐
│ 大量使用 ASCII 图表 │
├─────────────────────────────────────────┤
│ │
│ ┌────────┐ ┌────────┐ │
│ │ 状态 │────────▶│ 状态 │ │
│ │ A │ │ B │ │
│ └────────┘ └────────┘ │
│ │
│ 系统图、状态机、数据流、 │
│ 架构草图、依赖图、比较表 │
│ │
└─────────────────────────────────────────┘
\`\`\`
**揭示风险和未知数**
- 识别可能出错的地方
- 发现理解上的差距
- 建议进行探针(Spike)或调查
---
## OpenSpec 意识
你拥有 OpenSpec 系统的完整上下文。自然地使用它,不要强行使用。
### 检查上下文
开始时,快速检查存在什么:
\`\`\`bash
openspec-cn list --json
\`\`\`
这会告诉你:
- 是否有活跃的变更
- 它们的名称、模式和状态
- 用户可能正在处理的内容
### 当不存在变更时
自由思考。当见解清晰时,你可以提议:
- "这感觉足够扎实,可以开始一个变更了。要我创建一个吗?"
→ 可以过渡到 \`/opsx:new\` 或 \`/opsx:ff\`
- 或者继续探索 - 没有形式化的压力
### 当存在变更时
如果用户提到变更或你检测到相关变更:
1. **阅读现有产出物以获取上下文**
- \`openspec/changes/<name>/proposal.md\`
- \`openspec/changes/<name>/design.md\`
- \`openspec/changes/<name>/tasks.md\`
- 等等
2. **在对话中自然地引用它们**
- "你的设计提到使用 Redis,但我们刚刚意识到 SQLite 更合适……"
- "提案将此范围限定为高级用户,但我们现在考虑面向所有人……"
3. **当做出决定时提议捕获**
| 见解类型 | 捕获位置 |
|--------------|------------------|
| 发现新需求 | \`specs/<capability>/spec.md\` |
| 需求变更 | \`specs/<capability>/spec.md\` |
| 做出设计决策 | \`design.md\` |
| 范围变更 | \`proposal.md\` |
| 识别出新工作 | \`tasks.md\` |
| 假设失效 | 相关产出物 |
提议示例:
- "这是一个设计决策。要把它记录在 design.md 中吗?"
- "这是一个新需求。要把它添加到 specs 中吗?"
- "这改变了范围。要更新 proposal 吗?"
4. **由用户决定** - 提议并继续。不要施压。不要自动捕获。
---
## 你不必做的事情
- 遵循脚本
- 每次都问相同的问题
- 生成特定的产出物
- 得出结论
- 如果离题有价值,不必保持在主题上
- 简短(这是思考时间)
---
## 处理不同的切入点
**用户提出一个模糊的想法:**
\`\`\`
用户: 我正在考虑添加实时协作
你: 实时协作是一个很大的领域。让我想想……
协作谱系
════════════════════════════════════════════
感知 协调 同步
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ 存在 │ │ 光标 │ │ CRDT │
│ "3人 │ │ 多选 │ │ 无冲突 │
│ 在线" │ │ │ │ │
└────────┘ └────────┘ └────────┘
│ │ │
简单 中等 复杂
你的想法倾向于哪里?
\`\`\`
**用户提出一个具体问题:**
\`\`\`
用户: 认证系统一团糟
你: [阅读代码库]
┌─────────────────────────────────────────────┐
│ 当前认证流程 │
└─────────────────────────────────────────────┘
┌─────────────┼─────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Google │ │ GitHub │ │ Email │
│ OAuth │ │ OAuth │ │ Magic │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└─────────────┼─────────────┘
┌───────────┐
│ 会话 │
└─────┬─────┘
┌───────────┐
│ 权限 │
└───────────┘
我看到三个纠结的地方。哪个最紧急?
\`\`\`
**用户在实施中途卡住:**
\`\`\`
用户: /opsx:explore add-auth-system
OAuth 集成比预期的要复杂
你: [阅读变更产出物]
你在任务 4:"实现 OAuth 流程"
让我追踪一下涉及的内容……
[绘制图表,探索选项,建议路径]
想要更新设计以反映这一点吗?
或者添加一个探针任务来调查?
\`\`\`
**用户想要比较选项:**
\`\`\`
用户: 我们应该用 Postgres 还是 SQLite?
你: 通用的回答很无聊。上下文是什么?
用户: 一个跟踪本地开发环境的 CLI 工具
你: 那改变了一切。
┌─────────────────────────────────────────────────┐
│ CLI 工具数据存储 │
└─────────────────────────────────────────────────┘
关键约束:
• 无后台进程运行
• 必须离线工作
• 单用户
SQLite Postgres
部署 嵌入式 ✓ 需要服务器 ✗
离线 是 ✓ 否 ✗
单文件 是 ✓ 否 ✗
SQLite。毫无疑问。
除非……有同步组件吗?
\`\`\`
---
## 结束探索
没有要求的结束方式。探索可能会:
- **流入行动**:"准备好开始了吗? /opsx:new 或 /opsx:ff"
- **导致产出物更新**:"已用这些决定更新 design.md"
- **仅提供清晰度**:用户得到了他们需要的,继续前进
- **稍后继续**:"我们可以随时继续这个话题"
当感觉事情变得清晰时,你可以总结:
\`\`\`
## 我们弄清楚了什么
**问题**:[清晰的理解]
**方法**:[如果出现了一个]
**未决问题**:[如果还有]
**下一步**(如果准备好了):
- 创建变更:/opsx:new <name>
- 快进到任务:/opsx:ff <name>
- 继续探索:继续交谈
\`\`\`
但这个总结是可选的。有时思考本身就是价值。
---
## 护栏
- **不要实施** - 绝不编写代码或实现功能。创建 OpenSpec 产出物是可以的,编写应用程序代码是不行的。
- **不要假装理解** - 如果某些事情不清楚,请深入挖掘
- **不要匆忙** - 发现是思考时间,而不是任务时间
- **不要强加结构** - 让模式自然浮现
- **不要自动捕捉** - 提议保存见解,不要直接做
- **要可视化** - 一个好的图表胜过千言万语
- **要探索代码库** - 将讨论建立在现实基础上
- **要质疑假设** - 包括用户的和你自己的`,
license: 'MIT',
compatibility: '需要 openspec CLI。',
metadata: { author: 'openspec', version: '1.0' },
};
}
export function getOpsxExploreCommandTemplate() {
return {
name: 'OPSX: 探索',
description: '进入探索模式 - 构思想法、调查问题、澄清需求',
category: '工作流',
tags: ['workflow', 'explore', 'experimental', 'thinking'],
content: `进入探索模式。深入思考。自由可视化。跟随对话的发展。
**重要提示:探索模式是为了思考,而不是为了实施。** 你可以阅读文件、搜索代码和调查代码库,但你绝不能编写代码或实现功能。如果用户要求你实现某些内容,请提醒他们先退出探索模式(例如,使用 \`/opsx:new\` 或 \`/opsx:ff\` 开始变更)。如果用户要求,你可以创建 OpenSpec 产出物(提案、设计、规格说明)——这是捕捉思考,而不是实施。
**这是一种姿态,而不是一种工作流。** 没有固定的步骤,没有要求的顺序,没有强制性的输出。你是一个思考伙伴,帮助用户进行探索。
**输入**:\`/opsx:explore\` 之后的参数是用户想要思考的任何内容。可能是:
- 一个模糊的想法:"实时协作"
- 一个具体的问题:"认证系统越来越难维护"
- 一个变更名称:"add-dark-mode"(在该变更的上下文中探索)
- 一个比较:"这个场景下该用 Postgres 还是 SQLite"
- 无(仅进入探索模式)
---
## 姿态
- **好奇而非说教** - 提出自然产生的问题,不要照本宣科
- **开放话题而非审问** - 浮现多个有趣的方向,让用户选择产生共鸣的部分。不要把他们限制在单一的提问路径中。
- **可视化** - 在有助于澄清思路时大方使用 ASCII 图表
- **自适应** - 跟随有趣的话题,当新信息出现时及时转换
- **耐心** - 不要急于下结论,让问题的轮廓自然显现
- **务实** - 在相关时探索实际的代码库,不要仅仅停留在理论上
---
## 你可能做的事情
根据用户提出的内容,你可能会:
**探索问题空间**
- 针对他们所说的内容提出澄清性问题
- 挑战假设
- 重新构建问题
- 寻找类比
**调查代码库**
- 绘制与讨论相关的现有架构图
- 寻找集成点
- 识别已在使用的模式
- 揭示隐藏的复杂性
**比较选项**
- 头脑风暴多种方法
- 构建比较表
- 勾勒权衡
- 推荐路径(如果被询问)
**可视化**
\`\`\`
┌─────────────────────────────────────────┐
│ 大量使用 ASCII 图表 │
├─────────────────────────────────────────┤
│ │
│ ┌────────┐ ┌────────┐ │
│ │ 状态 │────────▶│ 状态 │ │
│ │ A │ │ B │ │
│ └────────┘ └────────┘ │
│ │
│ 系统图、状态机、数据流、 │
│ 架构草图、依赖图、比较表 │
│ │
└─────────────────────────────────────────┘
\`\`\`
**揭示风险和未知数**
- 识别可能出错的地方
- 发现理解上的差距
- 建议进行探针(Spike)或调查
---
## OpenSpec 意识
你拥有 OpenSpec 系统的完整上下文。自然地使用它,不要强行使用。
### 检查上下文
开始时,快速检查存在什么:
\`\`\`bash
openspec-cn list --json
\`\`\`
这会告诉你:
- 是否有活跃的变更
- 它们的名称、模式和状态
- 用户可能正在处理的内容
如果用户提到了特定的变更名称,请阅读其产出物以获取上下文。
### 当不存在变更时
自由思考。当见解清晰时,你可以提议:
- "这感觉足够扎实,可以开始一个变更了。要我创建一个吗?"
→ 可以过渡到 \`/opsx:new\` 或 \`/opsx:ff\`
- 或者继续探索 - 没有形式化的压力
### 当存在变更时
如果用户提到变更或你检测到相关变更:
1. **阅读现有产出物以获取上下文**
- \`openspec/changes/<name>/proposal.md\`
- \`openspec/changes/<name>/design.md\`
- \`openspec/changes/<name>/tasks.md\`
- 等等
2. **在对话中自然地引用它们**
- "你的设计提到使用 Redis,但我们刚刚意识到 SQLite 更合适……"
- "提案将此范围限定为高级用户,但我们现在考虑面向所有人……"
3. **当做出决定时提议捕获**
| 见解类型 | 捕获位置 |
|--------------|------------------|
| 发现新需求 | \`specs/<capability>/spec.md\` |
| 需求变更 | \`specs/<capability>/spec.md\` |
| 做出设计决策 | \`design.md\` |
| 范围变更 | \`proposal.md\` |
| 识别出新工作 | \`tasks.md\` |
| 假设失效 | 相关产出物 |
提议示例:
- "这是一个设计决策。要把它记录在 design.md 中吗?"
- "这是一个新需求。要把它添加到 specs 中吗?"
- "这改变了范围。要更新 proposal 吗?"
4. **由用户决定** - 提议并继续。不要施压。不要自动捕获。
---
## 你不必做的事情
- 遵循脚本
- 每次都问相同的问题
- 生成特定的产出物
- 得出结论
- 如果离题有价值,不必保持在主题上
- 简短(这是思考时间)
---
## 结束探索
没有要求的结束方式。探索可能会:
- **流入行动**:"准备好开始了吗? \`/opsx:new\` 或 \`/opsx:ff\`"
- **导致产出物更新**:"已用这些决定更新 design.md"
- **仅提供清晰度**:用户得到了他们需要的,继续前进
- **稍后继续**:"我们可以随时继续这个话题"
当感觉事情变得清晰时,你可以总结 - 但这是可选的。有时思考本身就是价值。
---
## 护栏
- **不要实施** - 绝不编写代码或实现功能。创建 OpenSpec 产出物是可以的,编写应用程序代码是不行的。
- **不要假装理解** - 如果某些事情不清楚,请深入挖掘
- **不要匆忙** - 发现是思考时间,而不是任务时间
- **不要强加结构** - 让模式自然浮现
- **不要自动捕捉** - 提议保存见解,不要直接做
- **要可视化** - 一个好的图表胜过千言万语
- **要探索代码库** - 将讨论建立在现实基础上
- **要质疑假设** - 包括用户的和你自己的`
};
}
//# sourceMappingURL=explore.js.map
/**
* Skill Template Workflow Modules
*
* This file is generated by splitting the legacy monolithic
* templates file into workflow-focused modules.
*/
import type { SkillTemplate } from '../types.js';
export declare function getFeedbackSkillTemplate(): SkillTemplate;
//# sourceMappingURL=feedback.d.ts.map
export function getFeedbackSkillTemplate() {
return {
name: 'feedback',
description: '收集并提交有关 OpenSpec 的用户反馈,包含上下文增强和匿名化。',
instructions: `帮助用户提交有关 OpenSpec 的反馈。
**目标**:引导用户完成收集、增强和提交反馈的过程,同时通过匿名化确保隐私。
**过程**
1. **从对话中收集上下文**
- 审查最近的对话历史以获取上下文
- 识别正在执行的任务
- 记录哪些工作顺利或不顺利
- 捕捉具体的摩擦点或赞美
2. **起草增强的反馈**
- 创建一个清晰、具描述性的标题(单句,无需"反馈:"前缀)
- 编写包含以下内容的正文:
- 用户试图做什么
- 发生了什么(好或坏)
- 来自对话的相关上下文
- 任何具体的建议或要求
3. **对敏感信息进行匿名化**
- 将文件路径替换为 \`<path>\` 或通用描述
- 将 API 密钥、令牌、机密替换为 \`<redacted>\`
- 将公司/组织名称替换为 \`<company>\`
- 将个人姓名替换为 \`<user>\`
- 将特定的 URL 替换为 \`<url>\`(除非是公开的/相关的)
- 保留有助于理解问题的技术细节
4. **展示草案以供批准**
- 向用户展示完整的草案
- 清晰地展示标题和正文
- 在提交前征求明确的批准
- 允许用户要求修改
5. **确认后提交**
- 使用 \`openspec-cn feedback\` 命令进行提交
- 格式:\`openspec-cn feedback "标题" --body "正文内容"\`
- 该命令将自动添加元数据(版本、平台、时间戳)
**草案示例**
\`\`\`
标题:产出物工作流中的错误处理需要改进
正文:
我正在尝试创建一个新变更,并在产出物工作流中遇到了一个问题。
当我尝试在创建提案后继续时,系统没有清晰地表明我需要先完成
规范(specs)。
建议:在产出物工作流中添加更清晰的错误消息,解释依赖链。
例如:"无法创建 design.md,因为规范尚未完成(0/2 已完成)。"
上下文:使用 spec-driven 模式与 <path>/my-project
\`\`\`
**匿名化示例**
之前:
\`\`\`
正在处理 /Users/john/mycompany/auth-service/src/oauth.ts
失败,API 密钥为:sk_live_abc123xyz
在 Acme Corp 工作
\`\`\`
之后:
\`\`\`
正在处理 <path>/oauth.ts
失败,API 密钥为:<redacted>
在 <company> 工作
\`\`\`
**护栏**
- 必须在提交前展示完整草案
- 必须征求明确的批准
- 必须对敏感信息进行匿名化
- 允许用户在提交前修改草案
- 严禁在未经用户确认的情况下提交
- 务必包含相关的技术上下文
- 务必保留对话特定的见解
**需要用户确认**
始终询问:
\`\`\`
这是我起草的反馈:
标题:[标题]
正文:
[正文]
看起来可以吗?如果您愿意,我可以修改它,或者按原样提交。
\`\`\`
只有在用户确认后才继续提交。`,
license: 'MIT',
compatibility: '需要 openspec CLI。',
metadata: { author: 'openspec', version: '1.0' },
};
}
//# sourceMappingURL=feedback.js.map
/**
* Skill Template Workflow Modules
*
* This file is generated by splitting the legacy monolithic
* templates file into workflow-focused modules.
*/
import type { SkillTemplate, CommandTemplate } from '../types.js';
export declare function getFfChangeSkillTemplate(): SkillTemplate;
export declare function getOpsxFfCommandTemplate(): CommandTemplate;
//# sourceMappingURL=ff-change.d.ts.map
export function getFfChangeSkillTemplate() {
return {
name: 'openspec-ff-change',
description: '快速创建实现所需的所有产出物。当用户想要快速创建实现所需的所有产出物,而不是逐个创建时使用。',
instructions: `快速完成产出物创建 - 一次性生成开始实现所需的一切。
**输入**:用户的请求应包含变更名称(kebab-case)或对他们想要构建内容的描述。
**步骤**
1. **如果没有提供明确的输入,询问他们想要构建什么**
使用 **AskUserQuestion tool**(开放式,无预设选项)询问:
> "您想要处理什么变更?请描述您想要构建或修复的内容。"
根据他们的描述,推导出一个 kebab-case 名称(例如:"add user authentication" → \`add-user-auth\`)。
**重要提示**:在不了解用户想要构建什么的情况下,请勿继续。
2. **创建变更目录**
\`\`\`bash
openspec-cn new change "<name>"
\`\`\`
这将在 \`openspec/changes/<name>/\` 创建一个脚手架变更。
3. **获取产出物构建顺序**
\`\`\`bash
openspec-cn status --change "<name>" --json
\`\`\`
解析 JSON 以获取:
- \`applyRequires\`: 实现前所需的产出物 ID 数组(例如:\`["tasks"]\`)
- \`artifacts\`: 所有产出物及其状态和依赖项的列表
4. **按顺序创建产出物直到准备好应用**
使用 **TodoWrite tool** 跟踪产出物的进度。
按依赖顺序循环遍历产出物(没有待处理依赖项的产出物优先):
a. **对于每个 \`ready\`(依赖项已满足)的产出物**:
- 获取指令:
\`\`\`bash
openspec-cn instructions <artifact-id> --change "<name>" --json
\`\`\`
- 指令 JSON 包括:
- \`context\`:项目背景(对你的约束 - 不要包含在输出中)
- \`rules\`:产出物特定规则(对你的约束 - 不要包含在输出中)
- \`template\`:用于输出文件的结构
- \`instruction\`:此产出物类型的模式特定指导
- \`outputPath\`:写入产出物的位置
- \`dependencies\`:已完成的产出物,用于读取上下文
- 读取任何已完成的依赖文件以获取上下文
- 使用 \`template\` 作为结构创建产出物文件
- 应用 \`context\` 和 \`rules\` 作为约束 - 但不要将它们复制到文件中
- 显示简短进度:"✓ 已创建 <artifact-id>"
b. **继续直到所有 \`applyRequires\` 产出物完成**
- 创建每个产出物后,重新运行 \`openspec-cn status --change "<name>" --json\`
- 检查 \`applyRequires\` 中的每个产出物 ID 在 artifacts 数组中是否具有 \`status: "done"\`
- 当所有 \`applyRequires\` 产出物完成时停止
c. **如果产出物需要用户输入**(上下文不清楚):
- 使用 **AskUserQuestion tool** 进行澄清
- 然后继续创建
5. **显示最终状态**
\`\`\`bash
openspec-cn status --change "<name>"
\`\`\`
**输出**
完成所有产出物后,总结:
- 变更名称和位置
- 已创建产出物的列表及简要描述
- 准备就绪:"所有产出物已创建!准备好实现。"
- 提示:"运行 \`/opsx:apply\` 或要求我实现以开始处理任务。"
**产出物创建指南**
- 遵循每个产出物类型的 \`openspec-cn instructions\` 中的 \`instruction\` 字段
- 模式定义了每个产出物应包含的内容 - 遵循它
- 在创建新产出物之前阅读依赖产出物以获取上下文
- 使用 \`template\` 作为输出文件的结构 - 填充其各个部分
- **重要提示**:\`context\` 和 \`rules\` 是对你的约束,而不是文件内容
- 不要将 \`<context>\`、\`<rules>\`、\`<project_context>\` 块复制到产出物中
- 这些引导你编写内容,但不应出现在输出中
**护栏**
- 创建实现所需的所有产出物(由 Schema 的 \`apply.requires\` 定义)
- 在创建新产出物之前始终阅读依赖产出物
- 如果上下文极其不清楚,询问用户 - 但倾向于做出合理的决定以保持势头
- 如果同名变更已存在,建议继续处理该变更
- 在继续下一个之前,验证写入后每个产出物文件是否存在`,
license: 'MIT',
compatibility: '需要 openspec CLI。',
metadata: { author: 'openspec', version: '1.0' },
};
}
export function getOpsxFfCommandTemplate() {
return {
name: 'OPSX: 快进',
description: '一键创建变更并生成实现所需的所有产出物',
category: '工作流',
tags: ['workflow', 'artifacts', 'experimental'],
content: `快速完成产出物创建 - 生成开始实现所需的一切。
**输入**:\`/opsx:ff\` 之后的参数是变更名称(kebab-case),或用户想要构建内容的描述。
**步骤**
1. **如果没有提供输入,询问他们想要构建什么**
使用 **AskUserQuestion tool**(开放式,无预设选项)询问:
> "您想要处理什么变更?请描述您想要构建或修复的内容。"
根据他们的描述,推导出一个 kebab-case 名称(例如:"add user authentication" → \`add-user-auth\`)。
**重要提示**:在不了解用户想要构建什么的情况下,请勿继续。
2. **创建变更目录**
\`\`\`bash
openspec-cn new change "<name>"
\`\`\`
这将在 \`openspec/changes/<name>/\` 创建一个脚手架变更。
3. **获取产出物构建顺序**
\`\`\`bash
openspec-cn status --change "<name>" --json
\`\`\`
解析 JSON 以获取:
- \`applyRequires\`: 实现前所需的产出物 ID 数组(例如:\`["tasks"]\`)
- \`artifacts\`: 所有产出物及其状态和依赖项的列表
4. **按顺序创建产出物直到准备好应用**
使用 **TodoWrite tool** 跟踪产出物的进度。
按依赖顺序循环遍历产出物(没有待处理依赖项的产出物优先):
a. **对于每个 \`ready\`(依赖项已满足)的产出物**:
- 获取指令:
\`\`\`bash
openspec-cn instructions <artifact-id> --change "<name>" --json
\`\`\`
- 指令 JSON 包括:
- \`context\`:项目背景(对你的约束 - 不要包含在输出中)
- \`rules\`:产出物特定规则(对你的约束 - 不要包含在输出中)
- \`template\`:用于输出文件的结构
- \`instruction\`:此产出物类型的模式特定指导
- \`outputPath\`:写入产出物的位置
- \`dependencies\`:已完成的产出物,用于读取上下文
- 读取任何已完成的依赖文件以获取上下文
- 使用 \`template\` 作为结构创建产出物文件
- 应用 \`context\` 和 \`rules\` 作为约束 - 但不要将它们复制到文件中
- 显示简短进度:"✓ 已创建 <artifact-id>"
b. **继续直到所有 \`applyRequires\` 产出物完成**
- 创建每个产出物后,重新运行 \`openspec-cn status --change "<name>" --json\`
- 检查 \`applyRequires\` 中的每个产出物 ID 在 artifacts 数组中是否具有 \`status: "done"\`
- 当所有 \`applyRequires\` 产出物完成时停止
c. **如果产出物需要用户输入**(上下文不清楚):
- 使用 **AskUserQuestion tool** 进行澄清
- 然后继续创建
5. **显示最终状态**
\`\`\`bash
openspec-cn status --change "<name>"
\`\`\`
**输出**
完成所有产出物后,总结:
- 变更名称和位置
- 已创建产出物的列表及简要描述
- 准备就绪:"所有产出物已创建!准备好实现。"
- 提示:"运行 \`/opsx:apply\` 以开始实现。"
**产出物创建指南**
- 遵循每个产出物类型的 \`openspec-cn instructions\` 中的 \`instruction\` 字段
- Schema 定义了每个产出物应包含的内容 - 遵循它
- 在创建新产出物之前阅读依赖产出物以获取上下文
- 使用 \`template\` 作为起点,根据上下文填写
**护栏**
- 创建实现所需的所有产出物(由 Schema 的 \`apply.requires\` 定义)
- 在创建新产出物之前始终阅读依赖产出物
- 如果上下文极其不清楚,询问用户 - 但倾向于做出合理的决定以保持势头
- 如果同名变更已存在,询问用户是否要继续它或创建一个新的
- 在继续下一个之前,验证写入后每个产出物文件是否存在`
};
}
//# sourceMappingURL=ff-change.js.map
/**
* Skill Template Workflow Modules
*
* This file is generated by splitting the legacy monolithic
* templates file into workflow-focused modules.
*/
import type { SkillTemplate, CommandTemplate } from '../types.js';
export declare function getNewChangeSkillTemplate(): SkillTemplate;
export declare function getOpsxNewCommandTemplate(): CommandTemplate;
//# sourceMappingURL=new-change.d.ts.map
export function getNewChangeSkillTemplate() {
return {
name: 'openspec-new-change',
description: '使用实验性的产出物工作流启动一个新的 OpenSpec 变更。当用户想要通过结构化的分步方法创建新功能、修复或修改时使用。',
instructions: `使用实验性的产出物驱动方法启动新变更。
**输入**:用户的请求应当包含变更名称(kebab-case)或对想要构建内容的描述。
**步骤**
1. **如果没有提供明确的输入,询问用户想要构建什么**
使用 **AskUserQuestion Tool**(开放式,无预设选项)询问:
> "您想要处理什么变更?请描述您想要构建或修复的内容。"
根据他们的描述,推导出一个 kebab-case 名称(例如:"add user authentication" → \`add-user-auth\`)。
**重要提示**:在不了解用户想要构建什么的情况下,请勿继续。
2. **确定工作流 Schema**
除非用户明确要求不同的工作流,否则使用默认 Schema(省略 \`--schema\`)。
**仅当用户提到以下内容时才使用不同的模式:**
- 特定模式名称 → 使用 \`--schema <name>\`
- "显示工作流" 或 "有哪些工作流" → 运行 \`openspec-cn schemas --json\` 并让他们选择
**否则**:省略 \`--schema\` 以使用默认值。
3. **创建变更目录**
\`\`\`bash
openspec-cn new change "<name>"
\`\`\`
仅当用户请求特定工作流时才添加 \`--schema <name>\`。
这将在 \`openspec/changes/<name>/\` 下使用所选 Schema 创建一个脚手架变更。
4. **显示产出物状态**
\`\`\`bash
openspec-cn status --change "<name>"
\`\`\`
这会显示哪些产出物需要创建,以及哪些已就绪(依赖项已满足)。
5. **获取第一个产出物的指令**
第一个产出物取决于所使用的 schema(例如:spec-driven 通常先生成 \`proposal\`)。
检查 status 输出,找到第一个状态为 "ready" 的产出物。
\`\`\`bash
openspec-cn instructions <first-artifact-id> --change "<name>"
\`\`\`
这会输出创建第一个产出物所需的模板和上下文。
6. **停止并等待用户指示**
**输出**
完成上述步骤后,进行总结:
- 变更名称和位置
- 正在使用的 Schema/工作流及其产出物顺序
- 当前状态(0/N 个产出物已完成)
- 第一个产出物的模板
- 提示:"准备好创建第一个产出物了吗?请描述此变更的内容,我将为您起草,或者要求我继续。"
**护栏**
- 不要立即创建任何产出物 —— 仅显示指令
- 不要跳过显示第一个产出物模板的步骤
- 如果名称无效(非 kebab-case),请求有效的名称
- 如果同名变更已存在,建议继续处理该变更
- 如果使用非默认工作流,请传递 --schema`,
license: 'MIT',
compatibility: '需要 openspec CLI。',
metadata: { author: 'openspec', version: '1.0' },
};
}
export function getOpsxNewCommandTemplate() {
return {
name: 'OPSX: 新建',
description: '使用实验性的产出物工作流 (OPSX) 启动新变更',
category: '工作流',
tags: ['workflow', 'artifacts', 'experimental'],
content: `使用实验性的产出物驱动方法启动新变更。
**输入**:\`/opsx:new\` 之后的参数是变更名称(kebab-case),或用户想要构建内容的描述。
**步骤**
1. **如果没有提供输入,询问他们想要构建什么**
使用 **AskUserQuestion tool**(开放式,无预设选项)询问:
> "您想要处理什么变更?请描述您想要构建或修复的内容。"
根据他们的描述,推导出一个 kebab-case 名称(例如:"add user authentication" → \`add-user-auth\`)。
**重要提示**:在不了解用户想要构建什么的情况下,请勿继续。
2. **确定工作流 Schema**
除非用户明确要求不同的工作流,否则使用默认 Schema(省略 \`--schema\`)。
**仅当用户提到以下内容时才使用不同的模式:**
- 特定模式名称 → 使用 \`--schema <name>\`
- "显示工作流" 或 "有哪些工作流" → 运行 \`openspec-cn schemas --json\` 并让他们选择
**否则**:省略 \`--schema\` 以使用默认值。
3. **创建变更目录**
\`\`\`bash
openspec-cn new change "<name>"
\`\`\`
仅当用户请求特定工作流时才添加 \`--schema <name>\`。
这将在 \`openspec/changes/<name>/\` 下使用所选 Schema 创建一个脚手架变更。
4. **显示产出物状态**
\`\`\`bash
openspec-cn status --change "<name>"
\`\`\`
这会显示哪些产出物需要创建,以及哪些已就绪(依赖项已满足)。
5. **获取第一个产出物的指令**
第一个产出物取决于 Schema。检查状态输出,找到第一个状态为 "ready" 的产出物。
\`\`\`bash
openspec-cn instructions <first-artifact-id> --change "<name>"
\`\`\`
这会输出创建第一个产出物所需的模板和上下文。
6. **停止并等待用户指示**
**输出**
完成上述步骤后,进行总结:
- 变更名称和位置
- 正在使用的 Schema/工作流及其产出物顺序
- 当前状态(0/N 个产出物已完成)
- 第一个产出物的模板
- 提示:"准备好创建第一个产出物了吗?运行 \`/opsx:continue\` 或描述此变更的内容,我将为您起草。"
**护栏**
- 不要立即创建任何产出物 —— 仅显示指令
- 不要跳过显示第一个产出物模板的步骤
- 如果名称无效(非 kebab-case),请求有效的名称
- 如果同名变更已存在,建议使用 \`/opsx:continue\` 代替
- 如果使用非默认工作流,请传递 --schema`
};
}
//# sourceMappingURL=new-change.js.map
/**
* Skill Template Workflow Modules
*
* This file is generated by splitting the legacy monolithic
* templates file into workflow-focused modules.
*/
import type { SkillTemplate, CommandTemplate } from '../types.js';
export declare function getOnboardSkillTemplate(): SkillTemplate;
export declare function getOpsxOnboardCommandTemplate(): CommandTemplate;
//# sourceMappingURL=onboard.d.ts.map
export function getOnboardSkillTemplate() {
return {
name: 'openspec-onboard',
description: 'OpenSpec 引导式入门 - 通过讲解与在真实代码库中动手,走完一个完整的工作流周期。',
instructions: getOnboardInstructions(),
license: 'MIT',
compatibility: '需要 openspec CLI。',
metadata: { author: 'openspec', version: '1.0' },
};
}
function getOnboardInstructions() {
return `引导用户完成他们的第一个完整OpenSpec工作流周期。这是一个教学体验——你将在他们的代码库中完成实际工作,同时解释每个步骤。
---
## 准备阶段
开始前,检查OpenSpec CLI是否已安装:
\`\`\`bash
# Unix/macOS
openspec-cn --version 2>&1 || echo "CLI_NOT_INSTALLED"
# Windows (PowerShell)
# if (Get-Command openspec-cn -ErrorAction SilentlyContinue) { openspec-cn --version } else { echo "CLI_NOT_INSTALLED" }
\`\`\`
**如果CLI未安装:**
> OpenSpec CLI 未安装。请先安装它,然后返回 \`/opsx:onboard\`。
如果未安装,请在此停止。
---
## 阶段1:欢迎
显示:
\`\`\`
## 欢迎使用OpenSpec!
我将引导您完成一个完整的变更周期——从想法到实现——使用您代码库中的真实任务。在此过程中,您将通过实践学习工作流程。
**我们将要做的事情:**
1. 在您的代码库中选择一个小的真实任务
2. 简要探索问题
3. 创建一个变更(我们工作的容器)
4. 构建产出物:提案 → 规格说明 → 设计 → 任务
5. 实现任务
6. 归档完成的变更
**时间:** ~15-20分钟
让我们开始寻找要处理的内容。
\`\`\`
---
## 阶段2:任务选择
### 代码库分析
扫描代码库寻找小的改进机会。寻找:
1. **TODO/FIXME注释** - 在代码文件中搜索 \`TODO\`、\`FIXME\`、\`HACK\`、\`XXX\`
2. **缺少错误处理** - 吞没错误的 \`catch\` 块,没有try-catch的风险操作
3. **没有测试的函数** - 交叉引用 \`src/\` 和测试目录
4. **类型问题** - TypeScript文件中的 \`any\` 类型(\`: any\`、\`as any\`)
5. **调试产出物** - 非调试代码中的 \`console.log\`、\`console.debug\`、\`debugger\` 语句
6. **缺少验证** - 没有验证的用户输入处理程序
同时检查最近的git活动:
\`\`\`bash
# Unix/macOS
git log --oneline -10 2>/dev/null || echo "No git history"
# Windows (PowerShell)
# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" }
\`\`\`
### 提出建议
根据您的分析,提出3-4个具体建议:
\`\`\`
## 任务建议
基于扫描您的代码库,以下是一些好的入门任务:
**1. [最有希望的任务]**
位置:\`src/path/to/file.ts:42\`
范围:~1-2个文件,~20-30行
为什么好:[简要原因]
**2. [第二个任务]**
位置:\`src/another/file.ts\`
范围:~1个文件,~15行
为什么好:[简要原因]
**3. [第三个任务]**
位置:[位置]
范围:[估计]
为什么好:[简要原因]
**4. 其他内容?**
告诉我您想要处理什么。
哪个任务让您感兴趣?(选择一个数字或描述您自己的)
\`\`\`
**如果未找到任何内容:** 回退到询问用户想要构建什么:
> 我在您的代码库中没有找到明显的快速改进机会。您一直想要添加或修复什么小东西?
### 范围护栏
如果用户选择或描述的内容太大(主要功能,多天工作):
\`\`\`
这是一个有价值的任务,但对于您的第一次OpenSpec体验来说可能太大了。
对于学习工作流程,越小越好——它让您能够看到完整周期而不会陷入实现细节。
**选项:**
1. **切分成更小的部分** - [他们的任务]中最小的有用部分是什么?也许只是[具体切片]?
2. **选择其他内容** - 其他建议之一,或不同的任务?
3. **无论如何都做** - 如果您真的想处理这个,我们可以做。只是要知道需要更长时间。
您更喜欢哪种?
\`\`\`
如果用户坚持,让他们覆盖——这是一个软护栏。
---
## 阶段3:探索演示
一旦选择了任务,简要演示探索模式:
\`\`\`
在我们创建变更之前,让我快速向您展示**探索模式**——这是在承诺方向之前思考问题的方式。
\`\`\`
花1-2分钟调查相关代码:
- 阅读涉及的文件
- 如果需要,绘制快速ASCII图表
- 注意任何考虑事项
\`\`\`
## 快速探索
[您的简要分析——您发现了什么,任何考虑事项]
┌─────────────────────────────────────────┐
│ [可选:如果有帮助的ASCII图表] │
└─────────────────────────────────────────┘
探索模式(\`/opsx:explore\`)用于这种思考——在实现之前进行调查。您可以在需要思考问题时随时使用它。
现在让我们创建一个变更来保存我们的工作。
\`\`\`
**暂停** - 等待用户确认后再继续。
---
## 阶段4:创建变更
**解释:**
\`\`\`
## 创建变更
OpenSpec中的"变更"是围绕一项工作的所有思考和规划的容器。它位于 \`openspec/changes/<name>/\` 中,保存您的产出物——提案、规格说明、设计、任务。
让我为我们的任务创建一个。
\`\`\`
**执行:** 使用派生的kebab-case名称创建变更:
\`\`\`bash
openspec-cn new change "<derived-name>"
\`\`\`
**显示:**
\`\`\`
已创建:\`openspec/changes/<name>/\`
文件夹结构:
\`\`\`
openspec/changes/<name>/
├── proposal.md ← 为什么我们要做这个(空,我们将填充它)
├── design.md ← 我们将如何构建它(空)
├── specs/ ← 详细需求(空)
└── tasks.md ← 实现检查清单(空)
\`\`\`
现在让我们填充第一个产出物——提案。
\`\`\`
---
## 阶段5:提案
**解释:**
\`\`\`
## 提案
提案捕获**为什么**我们要进行此变更以及**什么**在高层级上涉及。这是工作的"电梯演讲"。
我将根据我们的任务起草一个。
\`\`\`
**执行:** 起草提案内容(暂时不保存):
\`\`\`
这是一个草案提案:
---
## 为什么
[1-2句话解释问题/机会]
## 什么变化
[将要不同的要点]
## 能力
### 新能力
- \`<能力名称>\`: [简要描述]
### 修改的能力
<!-- 如果修改现有行为 -->
## 影响
- \`src/path/to/file.ts\`: [什么变化]
- [其他文件如果适用]
---
这能捕捉意图吗?我可以在保存前调整。
\`\`\`
**暂停** - 等待用户批准/反馈。
批准后,保存提案:
\`\`\`bash
openspec-cn instructions proposal --change "<name>" --json
\`\`\`
然后将内容写入 \`openspec/changes/<name>/proposal.md\`。
\`\`\`
提案已保存。这是您的"为什么"文档——您随时可以回来在理解发展时完善它。
接下来:规格说明。
\`\`\`
---
## 阶段6:规格说明
**解释:**
\`\`\`
## 规格说明
规格说明以精确、可测试的术语定义**什么**我们正在构建。它们使用需求/场景格式,使预期行为清晰明了。
对于像这样的小任务,我们可能只需要一个规格说明文件。
\`\`\`
**执行:** 创建规格说明文件:
\`\`\`bash
# Unix/macOS
mkdir -p openspec/changes/<name>/specs/<capability-name>
# Windows (PowerShell)
# New-Item -ItemType Directory -Force -Path "openspec/changes/<name>/specs/<capability-name>"
\`\`\`
起草规格说明内容:
\`\`\`
这是规格说明:
---
## 新增需求
### 需求: <名称>
<系统应该做什么的描述>
#### 场景: <场景名称>
- **当** <触发条件>
- **那么** <预期结果>
- **并且** <如果需要额外结果>
---
这种格式——当/那么/并且——使需求可测试。您可以将它们字面地读作测试用例。
\`\`\`
保存到 \`openspec/changes/<name>/specs/<capability>/spec.md\`。
---
## 阶段7:设计
**解释:**
\`\`\`
## 设计
设计捕获**如何**我们将构建它——技术决策、权衡、方法。
对于小变更,这可能很简短。没关系——不是每个变更都需要深入的设计讨论。
\`\`\`
**执行:** 起草design.md:
\`\`\`
这是设计:
---
## 上下文
[关于当前状态的简要上下文]
## 目标/非目标
**目标:**
- [我们试图实现什么]
**非目标:**
- [明确超出范围的内容]
## 决策
### 决策1:[关键决策]
[方法解释和理由]
---
对于小任务,这捕获了关键决策而不过度工程化。
\`\`\`
保存到 \`openspec/changes/<name>/design.md\`。
---
## 阶段8:任务
**解释:**
\`\`\`
## 任务
最后,我们将工作分解为实现任务——驱动应用阶段的复选框。
这些应该小、清晰且逻辑顺序。
\`\`\`
**执行:** 基于规格说明和设计生成任务:
\`\`\`
这是实现任务:
---
## 1. [类别或文件]
- [ ] 1.1 [具体任务]
- [ ] 1.2 [具体任务]
## 2. 验证
- [ ] 2.1 [验证步骤]
---
每个复选框成为应用阶段的工作单元。准备好实现了吗?
\`\`\`
**暂停** - 等待用户确认他们准备好实现。
保存到 \`openspec/changes/<name>/tasks.md\`。
---
## 阶段9:应用(实现)
**解释:**
\`\`\`
## 实现
现在我们实现每个任务,在过程中勾选它们。我将宣布每个任务,并偶尔注意规格说明/设计如何影响方法。
\`\`\`
**执行:** 对于每个任务:
1. 宣布:"正在处理任务N:[描述]"
2. 在代码库中实现变更
3. 自然地引用规格说明/设计:"规格说明说X,所以我做Y"
4. 在tasks.md中标记完成:\`- [ ]\` → \`- [x]\`
5. 简要状态:"✓ 任务N完成"
保持叙述轻量——不要过度解释每一行代码。
所有任务后:
\`\`\`
## 实现完成
所有任务完成:
- [x] 任务1
- [x] 任务2
- [x] ...
变更已实现!还有一个步骤——让我们归档它。
\`\`\`
---
## 阶段10:归档
**解释:**
\`\`\`
## 归档
当变更完成时,我们归档它。这将把它从 \`openspec/changes/\` 移动到 \`openspec/changes/archive/YYYY-MM-DD-<name>/\`。
归档的变更成为您项目的决策历史——您随时可以找到它们来理解为什么某物以某种方式构建。
\`\`\`
**执行:**
\`\`\`bash
openspec-cn archive "<name>"
\`\`\`
**显示:**
\`\`\`
已归档到:\`openspec/changes/archive/YYYY-MM-DD-<name>/\`
变更现在是您项目历史的一部分。代码在您的代码库中,决策记录被保留。
\`\`\`
---
## 阶段11:回顾与下一步
\`\`\`
## 恭喜!
您刚刚完成了一个完整的OpenSpec周期:
1. **探索** - 思考问题
2. **新建** - 创建变更容器
3. **提案** - 捕获为什么
4. **规格说明** - 详细定义什么
5. **设计** - 决定如何
6. **任务** - 分解为步骤
7. **应用** - 实现工作
8. **归档** - 保留记录
同样的节奏适用于任何大小的变更——小修复或主要功能。
---
## 命令参考
**核心工作流:**
| 命令 | 做什么 |
|---------|--------------|
| \`/opsx:propose\` | 创建变更并生成所有产出物 |
| \`/opsx:explore\` | 在工作之前/期间思考问题 |
| \`/opsx:apply\` | 实现变更中的任务 |
| \`/opsx:archive\` | 归档完成的变更 |
**其他命令:**
| 命令 | 做什么 |
|---------|--------------|
| \`/opsx:new\` | 开始新变更,逐步通过产出物 |
| \`/opsx:continue\` | 继续处理现有变更 |
| \`/opsx:ff\` | 快进:一次创建所有产出物 |
| \`/opsx:verify\` | 验证实现是否匹配产出物 |
---
## 下一步是什么?
尝试 \`/opsx:propose\` 在您实际想要构建的内容上。您现在掌握了节奏!
\`\`\`
---
## 优雅退出处理
### 用户想要中途停止
如果用户说他们需要停止、想要暂停或似乎不投入:
\`\`\`
没问题!您的变更保存在 \`openspec/changes/<name>/\`。
要在以后继续:
- \`/opsx:continue <name>\` - 恢复产出物创建
- \`/opsx:apply <name>\` - 跳转到实现(如果任务存在)
工作不会丢失。随时回来。
\`\`\`
优雅退出,不施加压力。
### 用户只想要命令参考
如果用户说他们只想看命令或跳过教程:
\`\`\`
## OpenSpec快速参考
**核心工作流:**
| 命令 | 做什么 |
|---------|--------------|
| \`/opsx:propose <name>\` | 创建变更并生成所有产出物 |
| \`/opsx:explore\` | 思考问题(无代码更改) |
| \`/opsx:apply <name>\` | 实现任务 |
| \`/opsx:archive <name>\` | 完成后归档 |
**其他命令:**
| 命令 | 做什么 |
|---------|--------------|
| \`/opsx:new <name>\` | 开始新变更,逐步进行 |
| \`/opsx:continue <name>\` | 继续现有变更 |
| \`/opsx:ff <name>\` | 快进:一次创建所有产出物 |
| \`/opsx:verify <name>\` | 验证实现 |
尝试 \`/opsx:propose\` 开始您的第一个变更。
\`\`\`
优雅退出。
---
## 护栏
- **遵循解释→执行→显示→暂停模式**在关键转换点(探索后、提案草案后、任务后、归档后)
- **在实现期间保持叙述轻量**——教学而不说教
- **不要跳过阶段**即使变更很小——目标是教学工作流程
- **在标记点暂停等待确认**,但不要过度暂停
- **优雅处理退出**——从不施压用户继续
- **使用真实代码库任务**——不模拟或使用虚假示例
- **温和调整范围**——引导向更小任务但尊重用户选择`;
}
export function getOpsxOnboardCommandTemplate() {
return {
name: 'OPSX: 入门',
description: '引导式入门 - 通过完整的OpenSpec工作流周期进行讲解',
category: '工作流',
tags: ['workflow', 'onboarding', 'tutorial', 'learning'],
content: getOnboardInstructions(),
};
}
//# sourceMappingURL=onboard.js.map
/**
* Skill Template Workflow Modules
*
* This file is generated by splitting the legacy monolithic
* templates file into workflow-focused modules.
*/
import type { SkillTemplate, CommandTemplate } from '../types.js';
export declare function getOpsxProposeSkillTemplate(): SkillTemplate;
export declare function getOpsxProposeCommandTemplate(): CommandTemplate;
//# sourceMappingURL=propose.d.ts.map
export function getOpsxProposeSkillTemplate() {
return {
name: 'openspec-propose',
description: '一步提案新变更并生成所有产出物。当用户想要快速描述他们想要构建的内容,并获得包含设计、规格说明和任务的完整提案以准备实现时使用。',
instructions: `提案新变更 - 一步创建变更并生成所有产出物。
我将创建一个包含以下产出物的变更:
- proposal.md(什么和为什么)
- design.md(如何)
- tasks.md(实现步骤)
准备好实现后,运行 /opsx:apply
---
**输入**:用户的请求应包含变更名称(kebab-case)或对他们想要构建内容的描述。
**步骤**
1. **如果没有提供明确的输入,询问他们想要构建什么**
使用 **AskUserQuestion tool**(开放式,无预设选项)询问:
> "您想要处理什么变更?请描述您想要构建或修复的内容。"
根据他们的描述,推导出一个 kebab-case 名称(例如:"add user authentication" → \`add-user-auth\`)。
**重要提示**:在不了解用户想要构建什么的情况下,请勿继续。
2. **创建变更目录**
\`\`\`bash
openspec-cn new change "<name>"
\`\`\`
这将在 \`openspec/changes/<name>/\` 创建一个带有 \`.openspec.yaml\` 的脚手架变更。
3. **获取产出物构建顺序**
\`\`\`bash
openspec-cn status --change "<name>" --json
\`\`\`
解析 JSON 以获取:
- \`applyRequires\`: 实现前所需的产出物 ID 数组(例如:\`["tasks"]\`)
- \`artifacts\`: 所有产出物及其状态和依赖项的列表
4. **按顺序创建产出物直到准备好应用**
使用 **TodoWrite tool** 跟踪产出物的进度。
按依赖顺序循环遍历产出物(没有待处理依赖项的产出物优先):
a. **对于每个 \`ready\`(依赖项已满足)的产出物**:
- 获取指令:
\`\`\`bash
openspec-cn instructions <artifact-id> --change "<name>" --json
\`\`\`
- 指令 JSON 包括:
- \`context\`:项目背景(对你的约束 - 不要包含在输出中)
- \`rules\`:产出物特定规则(对你的约束 - 不要包含在输出中)
- \`template\`:用于输出文件的结构
- \`instruction\`:此产出物类型的 Schema 特定指导
- \`outputPath\`:写入产出物的位置
- \`dependencies\`:已完成的产出物,用于读取上下文
- 读取任何已完成的依赖文件以获取上下文
- 使用 \`template\` 作为结构创建产出物文件
- 应用 \`context\` 和 \`rules\` 作为约束 - 但不要将它们复制到文件中
- 显示简短进度:"✓ 已创建 <artifact-id>"
b. **继续直到所有 \`applyRequires\` 产出物完成**
- 创建每个产出物后,重新运行 \`openspec-cn status --change "<name>" --json\`
- 检查 \`applyRequires\` 中的每个产出物 ID 在 artifacts 数组中是否具有 \`status: "done"\`
- 当所有 \`applyRequires\` 产出物完成时停止
c. **如果产出物需要用户输入**(上下文不清楚):
- 使用 **AskUserQuestion tool** 进行澄清
- 然后继续创建
5. **显示最终状态**
\`\`\`bash
openspec-cn status --change "<name>"
\`\`\`
**输出**
完成所有产出物后,总结:
- 变更名称和位置
- 已创建产出物的列表及简要描述
- 准备就绪:"所有产出物已创建!准备好实现。"
- 提示:"运行 \`/opsx:apply\` 或要求我实现以开始处理任务。"
**产出物创建指南**
- 遵循每个产出物类型的 \`openspec-cn instructions\` 中的 \`instruction\` 字段
- Schema 定义了每个产出物应包含的内容 - 遵循它
- 在创建新产出物之前阅读依赖产出物以获取上下文
- 使用 \`template\` 作为输出文件的结构 - 填充其各个部分
- **重要提示**:\`context\` 和 \`rules\` 是对你的约束,而不是文件内容
- 不要将 \`<context>\`、\`<rules>\`、\`<project_context>\` 块复制到产出物中
- 这些引导你编写内容,但不应出现在输出中
**护栏**
- 创建实现所需的所有产出物(由 Schema 的 \`apply.requires\` 定义)
- 在创建新产出物之前始终阅读依赖产出物
- 如果上下文极其不清楚,询问用户 - 但倾向于做出合理的决定以保持势头
- 如果同名变更已存在,询问用户是否要继续它或创建一个新的
- 在继续下一个之前,验证写入后每个产出物文件是否存在`,
license: 'MIT',
compatibility: '需要 openspec CLI。',
metadata: { author: 'openspec', version: '1.0' },
};
}
export function getOpsxProposeCommandTemplate() {
return {
name: 'OPSX: 提案',
description: '提案新变更 - 一步创建并生成所有产出物',
category: '工作流',
tags: ['workflow', 'artifacts', 'experimental'],
content: `提案新变更 - 一步创建变更并生成所有产出物。
我将创建一个包含以下产出物的变更:
- proposal.md(什么和为什么)
- design.md(如何)
- tasks.md(实现步骤)
准备好实现后,运行 /opsx:apply
---
**输入**:\`/opsx:propose\` 之后的参数是变更名称(kebab-case),或用户想要构建内容的描述。
**步骤**
1. **如果没有提供输入,询问他们想要构建什么**
使用 **AskUserQuestion tool**(开放式,无预设选项)询问:
> "您想要处理什么变更?请描述您想要构建或修复的内容。"
根据他们的描述,推导出一个 kebab-case 名称(例如:"add user authentication" → \`add-user-auth\`)。
**重要提示**:在不了解用户想要构建什么的情况下,请勿继续。
2. **创建变更目录**
\`\`\`bash
openspec-cn new change "<name>"
\`\`\`
这将在 \`openspec/changes/<name>/\` 创建一个带有 \`.openspec.yaml\` 的脚手架变更。
3. **获取产出物构建顺序**
\`\`\`bash
openspec-cn status --change "<name>" --json
\`\`\`
解析 JSON 以获取:
- \`applyRequires\`: 实现前所需的产出物 ID 数组(例如:\`["tasks"]\`)
- \`artifacts\`: 所有产出物及其状态和依赖项的列表
4. **按顺序创建产出物直到准备好应用**
使用 **TodoWrite tool** 跟踪产出物的进度。
按依赖顺序循环遍历产出物(没有待处理依赖项的产出物优先):
a. **对于每个 \`ready\`(依赖项已满足)的产出物**:
- 获取指令:
\`\`\`bash
openspec-cn instructions <artifact-id> --change "<name>" --json
\`\`\`
- 指令 JSON 包括:
- \`context\`:项目背景(对你的约束 - 不要包含在输出中)
- \`rules\`:产出物特定规则(对你的约束 - 不要包含在输出中)
- \`template\`:用于输出文件的结构
- \`instruction\`:此产出物类型的 Schema 特定指导
- \`outputPath\`:写入产出物的位置
- \`dependencies\`:已完成的产出物,用于读取上下文
- 读取任何已完成的依赖文件以获取上下文
- 使用 \`template\` 作为结构创建产出物文件
- 应用 \`context\` 和 \`rules\` 作为约束 - 但不要将它们复制到文件中
- 显示简短进度:"✓ 已创建 <artifact-id>"
b. **继续直到所有 \`applyRequires\` 产出物完成**
- 创建每个产出物后,重新运行 \`openspec-cn status --change "<name>" --json\`
- 检查 \`applyRequires\` 中的每个产出物 ID 在 artifacts 数组中是否具有 \`status: "done"\`
- 当所有 \`applyRequires\` 产出物完成时停止
c. **如果产出物需要用户输入**(上下文不清楚):
- 使用 **AskUserQuestion tool** 进行澄清
- 然后继续创建
5. **显示最终状态**
\`\`\`bash
openspec-cn status --change "<name>"
\`\`\`
**输出**
完成所有产出物后,总结:
- 变更名称和位置
- 已创建产出物的列表及简要描述
- 准备就绪:"所有产出物已创建!准备好实现。"
- 提示:"运行 \`/opsx:apply\` 开始实现。"
**产出物创建指南**
- 遵循每个产出物类型的 \`openspec-cn instructions\` 中的 \`instruction\` 字段
- Schema 定义了每个产出物应包含的内容 - 遵循它
- 在创建新产出物之前阅读依赖产出物以获取上下文
- 使用 \`template\` 作为输出文件的结构 - 填充其各个部分
- **重要提示**:\`context\` 和 \`rules\` 是对你的约束,而不是文件内容
- 不要将 \`<context>\`、\`<rules>\`、\`<project_context>\` 块复制到产出物中
- 这些引导你编写内容,但不应出现在输出中
**护栏**
- 创建实现所需的所有产出物(由 Schema 的 \`apply.requires\` 定义)
- 在创建新产出物之前始终阅读依赖产出物
- 如果上下文极其不清楚,询问用户 - 但倾向于做出合理的决定以保持势头
- 如果同名变更已存在,询问用户是否要继续它或创建一个新的
- 在继续下一个之前,验证写入后每个产出物文件是否存在`
};
}
//# sourceMappingURL=propose.js.map
/**
* Skill Template Workflow Modules
*
* This file is generated by splitting the legacy monolithic
* templates file into workflow-focused modules.
*/
import type { SkillTemplate, CommandTemplate } from '../types.js';
export declare function getSyncSpecsSkillTemplate(): SkillTemplate;
export declare function getOpsxSyncCommandTemplate(): CommandTemplate;
//# sourceMappingURL=sync-specs.d.ts.map
export function getSyncSpecsSkillTemplate() {
return {
name: 'openspec-sync-specs',
description: '将变更中的增量规范同步到主规范。当用户想要使用增量规范中的更改更新主规范,而不归档该变更时使用。',
instructions: `将变更中的增量规范同步到主规范。
这是一个 **Agent 驱动** 的操作 - 你将读取增量规范并直接编辑主规范以应用更改。这允许智能合并(例如,添加场景而不复制整个需求)。
**输入**:可选指定变更名称。如果省略,检查是否可以从对话上下文中推断。如果模糊或不明确,你**必须**提示获取可用变更。
**步骤**
1. **如果没有提供变更名称,提示选择**
运行 \`openspec-cn list --json\` 获取可用变更。使用 **AskUserQuestion tool** 让用户选择。
显示具有增量规范(在 \`specs/\` 目录下)的变更。
**重要提示**:不要猜测或自动选择变更。始终让用户选择。
2. **查找增量规范**
在 \`openspec/changes/<name>/specs/*/spec.md\` 中查找增量规范文件。
每个增量规范文件包含如下部分:
- \`## 新增需求\` - 要添加的新需求
- \`## 修改需求\` - 对现有需求的更改
- \`## 移除需求\` - 要移除的需求
- \`## 重命名需求\` - 要重命名的需求(从/到 格式)
如果没有找到增量规范,通知用户并停止。
3. **对于每个增量规范,将更改应用到主规范**
对于在 \`openspec/changes/<name>/specs/<capability>/spec.md\` 处具有增量规范的每个 capability:
a. **阅读增量规范** 以了解预期的更改
b. **阅读主规范** 于 \`openspec/specs/<capability>/spec.md\`(可能尚不存在)
c. **智能地应用更改**:
**新增需求:**
- 如果需求在主规范中不存在 → 添加它
- 如果需求已存在 → 更新它以匹配(视为隐式 MODIFIED)
**修改需求:**
- 在主规范中找到该需求
- 应用更改 - 这可能是:
- 添加新场景(不需要复制现有场景)
- 修改现有场景
- 更改需求描述
- 保留增量中未提及的场景/内容
**移除需求:**
- 从主规范中移除整个需求块
**重命名需求:**
- 找到 FROM 需求,重命名为 TO
d. **创建新主规范** 如果 capability 尚不存在:
- 创建 \`openspec/specs/<capability>/spec.md\`
- 添加 目的 部分(可以简短,标记为 待定)
- 添加 需求 部分以及 新增需求
4. **显示摘要**
应用所有更改后,总结:
- 哪些 capability 已更新
- 做了什么更改(需求添加/修改/移除/重命名)
**增量规范格式参考**
\`\`\`markdown
## 新增需求
### 需求: 新功能
系统 应当 实现新的能力。
#### 场景: 基本场景
- **当** 用户执行 X
- **那么** 系统执行 Y
## 修改需求
### 需求: 现有功能
#### 场景: 需要新增的场景
- **当** 用户执行 A
- **那么** 系统执行 B
## 移除需求
### 需求: 已废弃功能
## 重命名需求
- 从: \`### 需求: Old Name\`
- 到: \`### 需求: New Name\`
\`\`\`
**关键原则:智能合并**
与程序化合并不同,你可以应用 **部分更新**:
- 要添加场景,只需将该场景包含在 MODIFIED 下 - 不要复制现有场景
- 增量代表 *意图*,而不是整体替换
- 使用你的判断力合理地合并更改
**成功时的输出**
\`\`\`
## 规范已同步:<change-name>
已更新主规范:
**<capability-1>**:
- 添加需求:"新功能"
- 修改需求:"现有功能"(添加了 1 个场景)
**<capability-2>**:
- 创建了新规范文件
- 添加需求:"另一个功能"
主规范现已更新。变更保持活动状态 - 在实现完成后归档。
\`\`\`
**护栏**
- 在进行更改之前阅读增量规范和主规范
- 保留增量中未提及的现有内容
- 如果不清楚,询问澄清
- 在进行时显示你正在更改的内容
- 操作应该是幂等的 - 运行两次应给出相同的结果`,
license: 'MIT',
compatibility: '需要 openspec CLI。',
metadata: { author: 'openspec', version: '1.0' },
};
}
export function getOpsxSyncCommandTemplate() {
return {
name: 'OPSX: 同步',
description: '将变更中的增量规范同步到主规范',
category: '工作流',
tags: ['workflow', 'specs', 'experimental'],
content: `将变更中的增量规范同步到主规范。
这是一个 **Agent 驱动** 的操作 - 你将读取增量规范并直接编辑主规范以应用更改。这允许智能合并(例如,添加场景而不复制整个需求)。
**输入**:可选择在 \`/opsx:sync\` 后指定变更名称(例如,\`/opsx:sync add-auth\`)。如果省略,检查是否可以从对话上下文中推断出来。如果模糊或不明确,你必须提示可用的变更。
**步骤**
1. **如果没有提供变更名称,提示选择**
运行 \`openspec-cn list --json\` 获取可用变更。使用 **AskUserQuestion tool** 让用户选择。
显示具有增量规范(在 \`specs/\` 目录下)的变更。
**重要提示**:不要猜测或自动选择变更。始终让用户选择。
2. **查找增量规范**
在 \`openspec/changes/<name>/specs/*/spec.md\` 中查找增量规范文件。
每个增量规范文件包含如下部分:
- \`## 新增需求\` - 要添加的新需求
- \`## 修改需求\` - 对现有需求的更改
- \`## 移除需求\` - 要移除的需求
- \`## 重命名需求\` - 要重命名的需求(从/到 格式)
如果没有找到增量规范,通知用户并停止。
3. **对于每个增量规范,将更改应用到主规范**
对于在 \`openspec/changes/<name>/specs/<capability>/spec.md\` 处具有增量规范的每个 capability:
a. **阅读增量规范** 以了解预期的更改
b. **阅读主规范** 于 \`openspec/specs/<capability>/spec.md\`(可能尚不存在)
c. **智能地应用更改**:
**新增需求:**
- 如果需求在主规范中不存在 → 添加它
- 如果需求已存在 → 更新它以匹配(视为隐式 MODIFIED)
**修改需求:**
- 在主规范中找到该需求
- 应用更改 - 这可能是:
- 添加新场景(不需要复制现有场景)
- 修改现有场景
- 更改需求描述
- 保留增量中未提及的场景/内容
**移除需求:**
- 从主规范中移除整个需求块
**重命名需求:**
- 找到 FROM 需求,重命名为 TO
d. **创建新主规范** 如果 capability 尚不存在:
- 创建 \`openspec/specs/<capability>/spec.md\`
- 添加 目的 部分(可以简短,标记为 待定)
- 添加 需求 部分以及 新增需求
4. **显示摘要**
应用所有更改后,总结:
- 哪些 capability 已更新
- 做了什么更改(需求添加/修改/移除/重命名)
**增量规范格式参考**
\`\`\`markdown
## 新增需求
### 需求: 新功能
系统 应当 实现新的能力。
#### 场景: 基本场景
- **当** 用户执行 X
- **那么** 系统执行 Y
## 修改需求
### 需求: 现有功能
#### 场景: 需要新增的场景
- **当** 用户执行 A
- **那么** 系统执行 B
## 移除需求
### 需求: 已废弃功能
## 重命名需求
- 从: \`### 需求: Old Name\`
- 到: \`### 需求: New Name\`
\`\`\`
**关键原则:智能合并**
与程序化合并不同,你可以应用 **部分更新**:
- 要添加场景,只需将该场景包含在 MODIFIED 下 - 不要复制现有场景
- 增量代表 *意图*,而不是整体替换
- 使用你的判断力合理地合并更改
**成功时的输出**
\`\`\`
## 规范已同步:<change-name>
已更新主规范:
**<capability-1>**:
- 添加需求:"新功能"
- 修改需求:"现有功能"(添加了 1 个场景)
**<capability-2>**:
- 创建了新规范文件
- 添加需求:"另一个功能"
主规范现已更新。变更保持活动状态 - 在实现完成后归档。
\`\`\`
**护栏**
- 在进行更改之前阅读增量规范和主规范
- 保留增量中未提及的现有内容
- 如果不清楚,询问澄清
- 在进行时显示你正在更改的内容
- 操作应该是幂等的 - 运行两次应给出相同的结果`
};
}
//# sourceMappingURL=sync-specs.js.map
/**
* Skill Template Workflow Modules
*
* This file is generated by splitting the legacy monolithic
* templates file into workflow-focused modules.
*/
import type { SkillTemplate, CommandTemplate } from '../types.js';
export declare function getVerifyChangeSkillTemplate(): SkillTemplate;
export declare function getOpsxVerifyCommandTemplate(): CommandTemplate;
//# sourceMappingURL=verify-change.d.ts.map
export function getVerifyChangeSkillTemplate() {
return {
name: 'openspec-verify-change',
description: '验证实现是否与变更产出物匹配。当用户想要在归档前验证实现是否完整、正确且一致时使用。',
instructions: `验证实现是否与变更产出物(规范、任务、设计)匹配。
**输入**:可选指定变更名称。如果省略,检查是否可以从对话上下文中推断。如果模糊或不明确,你**必须**提示获取可用变更。
**步骤**
1. **如果没有提供变更名称,提示选择**
运行 \`openspec-cn list --json\` 获取可用变更。使用 **AskUserQuestion tool** 让用户选择。
显示具有实现任务的变更(存在任务产出物)。
如果可用,包括每个变更使用的 Schema。
将任务未完成的变更标记为 "(进行中)"。
**重要提示**:不要猜测或自动选择变更。始终让用户选择。
2. **检查状态以了解 Schema**
\`\`\`bash
openspec-cn status --change "<name>" --json
\`\`\`
解析 JSON 以了解:
- \`schemaName\`:正在使用的工作流模式(例如:"spec-driven")
- 此变更存在哪些产出物
3. **获取变更目录并加载产出物**
\`\`\`bash
openspec-cn instructions apply --change "<name>" --json
\`\`\`
这会返回变更目录和上下文文件。从 \`contextFiles\` 读取所有可用产出物。
4. **初始化验证报告结构**
创建具有三个维度的报告结构:
- **完整性**:跟踪任务和规范覆盖率
- **正确性**:跟踪需求实现和场景覆盖率
- **一致性**:跟踪设计遵循情况和模式一致性
每个维度可以有 CRITICAL、WARNING 或 SUGGESTION 问题。
5. **验证完整性**
**任务完成情况**:
- 如果 contextFiles 中存在 tasks.md,读取它
- 解析复选框:\`- [ ]\`(未完成)vs \`- [x]\`(已完成)
- 统计已完成 vs 总任务数
- 如果存在未完成的任务:
- 为每个未完成任务添加 CRITICAL 问题
- 建议:"完成任务:<描述>" 或 "如果已实现则标记为完成"
**规范覆盖率**:
- 如果 \`openspec/changes/<name>/specs/\` 中存在增量规范:
- 提取所有需求(标记为 "### 需求:")
- 对于每个需求:
- 在代码库中搜索与需求相关的关键词
- 评估实现是否可能存在
- 如果需求看起来未实现:
- 添加 CRITICAL 问题:"未找到需求:<需求名称>"
- 建议:"实现需求 X:<描述>"
6. **验证正确性**
**需求实现映射**:
- 对于增量规范中的每个需求:
- 在代码库中搜索实现证据
- 如果找到,记录文件路径和行范围
- 评估实现是否符合需求意图
- 如果检测到偏差:
- 添加 WARNING:"实现可能偏离规范:<详情>"
- 建议:"根据需求 X 审查 <文件>:<行>"
**场景覆盖率**:
- 对于增量规范中的每个场景(标记为 "#### 场景:"):
- 检查代码中是否处理了条件
- 检查是否存在覆盖该场景的测试
- 如果场景看起来未覆盖:
- 添加 WARNING:"场景未覆盖:<场景名称>"
- 建议:"为场景添加测试或实现:<描述>"
7. **验证一致性**
**设计遵循情况**:
- 如果 contextFiles 中存在 design.md:
- 提取关键决策(查找 "Decision:"、"Approach:"、"Architecture:" 等部分)
- 验证实现是否遵循这些决策
- 如果检测到矛盾:
- 添加 WARNING:"未遵循设计决策:<决策>"
- 建议:"更新实现或修订 design.md 以匹配实际情况"
- 如果没有 design.md:跳过设计遵循检查,注明 "没有 design.md 可供验证"
**代码模式一致性**:
- 审查新代码与项目模式的一致性
- 检查文件命名、目录结构、编码风格
- 如果发现重大偏差:
- 添加 SUGGESTION:"代码模式偏差:<详情>"
- 建议:"考虑遵循项目模式:<示例>"
8. **生成验证报告**
**摘要记分卡**:
\`\`\`
## 验证报告:<change-name>
### 摘要
| 维度 | 状态 |
|----------|------------------|
| 完整性 | X/Y 任务,N 需求 |
| 正确性 | M/N 需求已覆盖 |
| 一致性 | 已遵循/存在问题 |
\`\`\`
**按优先级分类的问题**:
1. **CRITICAL**(归档前必须修复):
- 未完成的任务
- 缺失的需求实现
- 每个都有具体的、可操作的建议
2. **WARNING**(应该修复):
- 规范/设计偏差
- 缺失的场景覆盖
- 每个都有具体的建议
3. **SUGGESTION**(最好修复):
- 模式不一致
- 小改进
- 每个都有具体的建议
**最终评估**:
- 如果有 CRITICAL 问题:"发现 X 个关键问题。归档前请修复。"
- 如果只有警告:"没有关键问题。有 Y 个警告需要考虑。可以归档(但建议改进)。"
- 如果全部通过:"所有检查通过。可以归档。"
**验证启发式方法**
- **完整性**:关注客观的检查清单项(复选框、需求列表)
- **正确性**:使用关键词搜索、文件路径分析、合理推断 - 不要求完全确定
- **一致性**:寻找明显的不一致,不要挑剔风格
- **误报**:不确定时,优先使用 SUGGESTION 而非 WARNING,WARNING 而非 CRITICAL
- **可操作性**:每个问题都必须有具体的建议,并在适用时提供文件/行引用
**优雅降级**
- 如果只存在 tasks.md:仅验证任务完成情况,跳过规范/设计检查
- 如果存在任务 + 规范:验证完整性和正确性,跳过设计
- 如果存在完整产出物:验证所有三个维度
- 始终注明跳过了哪些检查以及原因
**输出格式**
使用清晰的 Markdown:
- 摘要记分卡使用表格
- 问题按组列出(CRITICAL/WARNING/SUGGESTION)
- 代码引用格式:\`file.ts:123\`
- 具体的、可操作的建议
- 不要使用模糊的建议,如 "考虑审查"`,
license: 'MIT',
compatibility: '需要 openspec CLI。',
metadata: { author: 'openspec', version: '1.0' },
};
}
export function getOpsxVerifyCommandTemplate() {
return {
name: 'OPSX: 验证',
description: '在归档前验证实现是否与变更产出物匹配',
category: '工作流',
tags: ['workflow', 'verify', 'experimental'],
content: `验证实现是否与变更产出物(规范、任务、设计)匹配。
**输入**:可选择在 \`/opsx:verify\` 后指定变更名称(例如,\`/opsx:verify add-auth\`)。如果省略,检查是否可以从对话上下文中推断出来。如果模糊或不明确,你必须提示可用的变更。
**步骤**
1. **如果没有提供变更名称,提示选择**
运行 \`openspec-cn list --json\` 获取可用变更。使用 **AskUserQuestion tool** 让用户选择。
显示具有实现任务的变更(存在任务产出物)。
如果可用,包括每个变更使用的 Schema。
将任务未完成的变更标记为 "(进行中)"。
**重要提示**:不要猜测或自动选择变更。始终让用户选择。
2. **检查状态以了解 Schema**
\`\`\`bash
openspec-cn status --change "<name>" --json
\`\`\`
解析 JSON 以了解:
- \`schemaName\`:正在使用的工作流模式(例如:"spec-driven")
- 此变更存在哪些产出物
3. **获取变更目录并加载产出物**
\`\`\`bash
openspec-cn instructions apply --change "<name>" --json
\`\`\`
这会返回变更目录和上下文文件。从 \`contextFiles\` 读取所有可用产出物。
4. **初始化验证报告结构**
创建具有三个维度的报告结构:
- **完整性**:跟踪任务和规范覆盖率
- **正确性**:跟踪需求实现和场景覆盖率
- **一致性**:跟踪设计遵循情况和模式一致性
每个维度可以有 CRITICAL、WARNING 或 SUGGESTION 问题。
5. **验证完整性**
**任务完成情况**:
- 如果 contextFiles 中存在 tasks.md,读取它
- 解析复选框:\`- [ ]\`(未完成)vs \`- [x]\`(已完成)
- 统计已完成 vs 总任务数
- 如果存在未完成的任务:
- 为每个未完成任务添加 CRITICAL 问题
- 建议:"完成任务:<描述>" 或 "如果已实现则标记为完成"
**规范覆盖率**:
- 如果 \`openspec/changes/<name>/specs/\` 中存在增量规范:
- 提取所有需求(标记为 "### 需求:")
- 对于每个需求:
- 在代码库中搜索与需求相关的关键词
- 评估实现是否可能存在
- 如果需求看起来未实现:
- 添加 CRITICAL 问题:"未找到需求:<需求名称>"
- 建议:"实现需求 X:<描述>"
6. **验证正确性**
**需求实现映射**:
- 对于增量规范中的每个需求:
- 在代码库中搜索实现证据
- 如果找到,记录文件路径和行范围
- 评估实现是否符合需求意图
- 如果检测到偏差:
- 添加 WARNING:"实现可能偏离规范:<详情>"
- 建议:"根据需求 X 审查 <文件>:<行>"
**场景覆盖率**:
- 对于增量规范中的每个场景(标记为 "#### 场景:"):
- 检查代码中是否处理了条件
- 检查是否存在覆盖该场景的测试
- 如果场景看起来未覆盖:
- 添加 WARNING:"场景未覆盖:<场景名称>"
- 建议:"为场景添加测试或实现:<描述>"
7. **验证一致性**
**设计遵循情况**:
- 如果 contextFiles 中存在 design.md:
- 提取关键决策(查找 "Decision:"、"Approach:"、"Architecture:" 等部分)
- 验证实现是否遵循这些决策
- 如果检测到矛盾:
- 添加 WARNING:"未遵循设计决策:<决策>"
- 建议:"更新实现或修订 design.md 以匹配实际情况"
- 如果没有 design.md:跳过设计遵循检查,注明 "没有 design.md 可供验证"
**代码模式一致性**:
- 审查新代码与项目模式的一致性
- 检查文件命名、目录结构、编码风格
- 如果发现重大偏差:
- 添加 SUGGESTION:"代码模式偏差:<详情>"
- 建议:"考虑遵循项目模式:<示例>"
8. **生成验证报告**
**摘要记分卡**:
\`\`\`
## 验证报告:<change-name>
### 摘要
| 维度 | 状态 |
|----------|------------------|
| 完整性 | X/Y 任务,N 需求 |
| 正确性 | M/N 需求已覆盖 |
| 一致性 | 已遵循/存在问题 |
\`\`\`
**按优先级分类的问题**:
1. **CRITICAL**(归档前必须修复):
- 未完成的任务
- 缺失的需求实现
- 每个都有具体的、可操作的建议
2. **WARNING**(应该修复):
- 规范/设计偏差
- 缺失的场景覆盖
- 每个都有具体的建议
3. **SUGGESTION**(最好修复):
- 模式不一致
- 小改进
- 每个都有具体的建议
**最终评估**:
- 如果有 CRITICAL 问题:"发现 X 个关键问题。归档前请修复。"
- 如果只有警告:"没有关键问题。有 Y 个警告需要考虑。可以归档(但建议改进)。"
- 如果全部通过:"所有检查通过。可以归档。"
**验证启发式方法**
- **完整性**:关注客观的检查清单项(复选框、需求列表)
- **正确性**:使用关键词搜索、文件路径分析、合理推断 - 不要求完全确定
- **一致性**:寻找明显的不一致,不要挑剔风格
- **误报**:不确定时,优先使用 SUGGESTION 而非 WARNING,WARNING 而非 CRITICAL
- **可操作性**:每个问题都必须有具体的建议,并在适用时提供文件/行引用
**优雅降级**
- 如果只存在 tasks.md:仅验证任务完成情况,跳过规范/设计检查
- 如果存在任务 + 规范:验证完整性和正确性,跳过设计
- 如果存在完整产出物:验证所有三个维度
- 始终注明跳过了哪些检查以及原因
**输出格式**
使用清晰的 Markdown:
- 摘要记分卡使用表格
- 问题按组列出(CRITICAL/WARNING/SUGGESTION)
- 代码引用格式:\`file.ts:123\`
- 具体的、可操作的建议
- 不要使用模糊的建议,如 "考虑审查"`
};
}
//# sourceMappingURL=verify-change.js.map
+3
-1

@@ -75,2 +75,3 @@ import { Command } from 'commander';

.option('--force', '自动清理旧文件而不提示')
.option('--profile <profile>', 'Override global config profile (core or custom)')
.action(async (targetPath = '.', options) => {

@@ -102,2 +103,3 @@ try {

force: options?.force,
profile: options?.profile,
});

@@ -216,3 +218,3 @@ await initCommand.execute(targetPath);

try {
console.error('警告:"openspec change list" 已弃用。请使用 "openspec-cn list"。');
console.error('警告:"openspec-cn change list" 已弃用。请使用 "openspec-cn list"。');
const changeCommand = new ChangeCommand();

@@ -219,0 +221,0 @@ await changeCommand.list(options);

@@ -271,3 +271,3 @@ import { promises as fs } from 'fs';

bullets.push('- 确保变更在specs/中有增量:使用标题## 新增|修改|移除|重命名需求');
bullets.push('- 每个需求必须至少包含一个#### 场景:块');
bullets.push('- 每个需求必须至少包含一个#### 场景:块');
bullets.push('- 调试解析的增量:openspec-cn change show <id> --json --deltas-only');

@@ -274,0 +274,0 @@ console.error('后续步骤:');

@@ -37,3 +37,3 @@ import ora from 'ora';

console.error('错误:无法自动检测 Shell。请明确指定 Shell。');
console.error(`用法:openspec completion ${operationName} [shell]`);
console.error(`用法:openspec-cn completion ${operationName} [shell]`);
console.error(`当前支持的 Shell 有:${CompletionFactory.getSupportedShells().join(', ')}`);

@@ -40,0 +40,0 @@ process.exitCode = 1;

import { Command } from 'commander';
import { GlobalConfig } from '../core/global-config.js';
import type { Profile, Delivery } from '../core/global-config.js';
interface ProfileState {
profile: Profile;
delivery: Delivery;
workflows: string[];
}
interface ProfileStateDiff {
hasChanges: boolean;
lines: string[];
}
/**
* Resolve the effective current profile state from global config defaults.
*/
export declare function resolveCurrentProfileState(config: GlobalConfig): ProfileState;
/**
* Derive profile type from selected workflows.
*/
export declare function deriveProfileFromWorkflowSelection(selectedWorkflows: string[]): Profile;
/**
* Format a compact workflow summary for the profile header.
*/
export declare function formatWorkflowSummary(workflows: readonly string[], profile: Profile): string;
/**
* Build a user-facing diff summary between two profile states.
*/
export declare function diffProfileState(before: ProfileState, after: ProfileState): ProfileStateDiff;
/**
* Register the config command and all its subcommands.

@@ -8,2 +35,3 @@ *

export declare function registerConfigCommand(program: Command): void;
export {};
//# sourceMappingURL=config.d.ts.map

@@ -1,6 +0,146 @@

import { spawn } from 'node:child_process';
import { spawn, execSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, } from '../core/global-config.js';
import { getNestedValue, setNestedValue, deleteNestedValue, coerceValue, formatValueYaml, validateConfigKeyPath, validateConfig, DEFAULT_CONFIG, } from '../core/config-schema.js';
import { CORE_WORKFLOWS, ALL_WORKFLOWS, getProfileWorkflows } from '../core/profiles.js';
import { OPENSPEC_DIR_NAME } from '../core/config.js';
import { hasProjectConfigDrift } from '../core/profile-sync-drift.js';
const WORKFLOW_PROMPT_META = {
propose: {
name: 'Propose change',
description: 'Create proposal, design, and tasks from a request',
},
explore: {
name: 'Explore ideas',
description: 'Investigate a problem before implementation',
},
new: {
name: 'New change',
description: 'Create a new change scaffold quickly',
},
continue: {
name: 'Continue change',
description: 'Resume work on an existing change',
},
apply: {
name: 'Apply tasks',
description: 'Implement tasks from the current change',
},
ff: {
name: 'Fast-forward',
description: 'Run a faster implementation workflow',
},
sync: {
name: 'Sync specs',
description: 'Sync change artifacts with specs',
},
archive: {
name: 'Archive change',
description: 'Finalize and archive a completed change',
},
'bulk-archive': {
name: 'Bulk archive',
description: 'Archive multiple completed changes together',
},
verify: {
name: 'Verify change',
description: 'Run verification checks against a change',
},
onboard: {
name: 'Onboard',
description: 'Guided onboarding flow for OpenSpec',
},
};
function isPromptCancellationError(error) {
return (error instanceof Error &&
(error.name === 'ExitPromptError' || error.message.includes('force closed the prompt with SIGINT')));
}
/**
* Resolve the effective current profile state from global config defaults.
*/
export function resolveCurrentProfileState(config) {
const profile = config.profile || 'core';
const delivery = config.delivery || 'both';
const workflows = [
...getProfileWorkflows(profile, config.workflows ? [...config.workflows] : undefined),
];
return { profile, delivery, workflows };
}
/**
* Derive profile type from selected workflows.
*/
export function deriveProfileFromWorkflowSelection(selectedWorkflows) {
const isCoreMatch = selectedWorkflows.length === CORE_WORKFLOWS.length &&
CORE_WORKFLOWS.every((w) => selectedWorkflows.includes(w));
return isCoreMatch ? 'core' : 'custom';
}
/**
* Format a compact workflow summary for the profile header.
*/
export function formatWorkflowSummary(workflows, profile) {
return `${workflows.length} selected (${profile})`;
}
function stableWorkflowOrder(workflows) {
const seen = new Set();
const ordered = [];
for (const workflow of ALL_WORKFLOWS) {
if (workflows.includes(workflow) && !seen.has(workflow)) {
ordered.push(workflow);
seen.add(workflow);
}
}
const extras = workflows.filter((w) => !ALL_WORKFLOWS.includes(w));
extras.sort();
for (const extra of extras) {
if (!seen.has(extra)) {
ordered.push(extra);
seen.add(extra);
}
}
return ordered;
}
/**
* Build a user-facing diff summary between two profile states.
*/
export function diffProfileState(before, after) {
const lines = [];
if (before.delivery !== after.delivery) {
lines.push(`delivery: ${before.delivery} -> ${after.delivery}`);
}
if (before.profile !== after.profile) {
lines.push(`profile: ${before.profile} -> ${after.profile}`);
}
const beforeOrdered = stableWorkflowOrder(before.workflows);
const afterOrdered = stableWorkflowOrder(after.workflows);
const beforeSet = new Set(beforeOrdered);
const afterSet = new Set(afterOrdered);
const added = afterOrdered.filter((w) => !beforeSet.has(w));
const removed = beforeOrdered.filter((w) => !afterSet.has(w));
if (added.length > 0 || removed.length > 0) {
const tokens = [];
if (added.length > 0) {
tokens.push(`added ${added.join(', ')}`);
}
if (removed.length > 0) {
tokens.push(`removed ${removed.join(', ')}`);
}
lines.push(`workflows: ${tokens.join('; ')}`);
}
return {
hasChanges: lines.length > 0,
lines,
};
}
function maybeWarnConfigDrift(projectDir, state, colorize) {
const openspecDir = path.join(projectDir, OPENSPEC_DIR_NAME);
if (!fs.existsSync(openspecDir)) {
return;
}
if (!hasProjectConfigDrift(projectDir, state.workflows, state.delivery)) {
return;
}
console.log(colorize('Warning: Global config is not applied to this project. Run `openspec update` to sync.'));
}
/**
* Register the config command and all its subcommands.

@@ -40,3 +180,29 @@ *

else {
// Read raw config to determine which values are explicit vs defaults
const configPath = getGlobalConfigPath();
let rawConfig = {};
try {
if (fs.existsSync(configPath)) {
rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
}
}
catch {
// If reading fails, treat all as defaults
}
console.log(formatValueYaml(config));
// Annotate profile settings
const profileSource = rawConfig.profile !== undefined ? '(explicit)' : '(default)';
const deliverySource = rawConfig.delivery !== undefined ? '(explicit)' : '(default)';
console.log(`\nProfile settings:`);
console.log(` profile: ${config.profile} ${profileSource}`);
console.log(` delivery: ${config.delivery} ${deliverySource}`);
if (config.profile === 'core') {
console.log(` workflows: ${CORE_WORKFLOWS.join(', ')} (from core profile)`);
}
else if (config.workflows && config.workflows.length > 0) {
console.log(` workflows: ${config.workflows.join(', ')} (explicit)`);
}
else {
console.log(` workflows: (none)`);
}
}

@@ -74,3 +240,3 @@ });

console.error(`错误:无效的配置键 "${key}"。${reason}`);
console.error('使用 "openspec config list" 查看可用键。');
console.error('使用 "openspec-cn config list" 查看可用键。');
console.error('传递 --allow-unknown 以跳过此检查。');

@@ -122,3 +288,3 @@ process.exitCode = 1;

console.error('错误:重置时必须指定 --all 参数');
console.error('用法:openspec config reset --all [-y]');
console.error('用法:openspec-cn config reset --all [-y]');
process.exitCode = 1;

@@ -129,6 +295,17 @@ return;

const { confirm } = await import('@inquirer/prompts');
const confirmed = await confirm({
message: '是否将所有配置重置为默认值?',
default: false,
});
let confirmed;
try {
confirmed = await confirm({
message: '是否将所有配置重置为默认值?',
default: false,
});
}
catch (error) {
if (isPromptCancellationError(error)) {
console.log('Reset cancelled.');
process.exitCode = 130;
return;
}
throw error;
}
if (!confirmed) {

@@ -201,3 +378,180 @@ console.log('已取消重置。');

});
// config profile [preset]
configCmd
.command('profile [preset]')
.description('Configure workflow profile (interactive picker or preset shortcut)')
.action(async (preset) => {
// Preset shortcut: `openspec config profile core`
if (preset === 'core') {
const config = getGlobalConfig();
config.profile = 'core';
config.workflows = [...CORE_WORKFLOWS];
// Preserve delivery setting
saveGlobalConfig(config);
console.log('Config updated. Run `openspec update` in your projects to apply.');
return;
}
if (preset) {
console.error(`Error: Unknown profile preset "${preset}". Available presets: core`);
process.exitCode = 1;
return;
}
// Non-interactive check
if (!process.stdout.isTTY) {
console.error('Interactive mode required. Use `openspec config profile core` or set config via environment/flags.');
process.exitCode = 1;
return;
}
// Interactive picker
const { select, checkbox, confirm } = await import('@inquirer/prompts');
const chalk = (await import('chalk')).default;
try {
const config = getGlobalConfig();
const currentState = resolveCurrentProfileState(config);
console.log(chalk.bold('\nCurrent profile settings'));
console.log(` Delivery: ${currentState.delivery}`);
console.log(` Workflows: ${formatWorkflowSummary(currentState.workflows, currentState.profile)}`);
console.log(chalk.dim(' Delivery = where workflows are installed (skills, commands, or both)'));
console.log(chalk.dim(' Workflows = which actions are available (propose, explore, apply, etc.)'));
console.log();
const action = await select({
message: 'What do you want to configure?',
choices: [
{
value: 'both',
name: 'Delivery and workflows',
description: 'Update install mode and available actions together',
},
{
value: 'delivery',
name: 'Delivery only',
description: 'Change where workflows are installed',
},
{
value: 'workflows',
name: 'Workflows only',
description: 'Change which workflow actions are available',
},
{
value: 'keep',
name: 'Keep current settings (exit)',
description: 'Leave configuration unchanged and exit',
},
],
});
if (action === 'keep') {
console.log('No config changes.');
maybeWarnConfigDrift(process.cwd(), currentState, chalk.yellow);
return;
}
const nextState = {
profile: currentState.profile,
delivery: currentState.delivery,
workflows: [...currentState.workflows],
};
if (action === 'both' || action === 'delivery') {
const deliveryChoices = [
{
value: 'both',
name: 'Both (skills + commands)',
description: 'Install workflows as both skills and slash commands',
},
{
value: 'skills',
name: 'Skills only',
description: 'Install workflows only as skills',
},
{
value: 'commands',
name: 'Commands only',
description: 'Install workflows only as slash commands',
},
];
for (const choice of deliveryChoices) {
if (choice.value === currentState.delivery) {
choice.name += ' [current]';
}
}
nextState.delivery = await select({
message: 'Delivery mode (how workflows are installed):',
choices: deliveryChoices,
default: currentState.delivery,
});
}
if (action === 'both' || action === 'workflows') {
const formatWorkflowChoice = (workflow) => {
const metadata = WORKFLOW_PROMPT_META[workflow] ?? {
name: workflow,
description: `Workflow: ${workflow}`,
};
return {
value: workflow,
name: metadata.name,
description: metadata.description,
short: metadata.name,
checked: currentState.workflows.includes(workflow),
};
};
const selectedWorkflows = await checkbox({
message: 'Select workflows to make available:',
instructions: 'Space to toggle, Enter to confirm',
pageSize: ALL_WORKFLOWS.length,
theme: {
icon: {
checked: '[x]',
unchecked: '[ ]',
},
},
choices: ALL_WORKFLOWS.map(formatWorkflowChoice),
});
nextState.workflows = selectedWorkflows;
nextState.profile = deriveProfileFromWorkflowSelection(selectedWorkflows);
}
const diff = diffProfileState(currentState, nextState);
if (!diff.hasChanges) {
console.log('No config changes.');
maybeWarnConfigDrift(process.cwd(), nextState, chalk.yellow);
return;
}
console.log(chalk.bold('\nConfig changes:'));
for (const line of diff.lines) {
console.log(` ${line}`);
}
console.log();
config.profile = nextState.profile;
config.delivery = nextState.delivery;
config.workflows = nextState.workflows;
saveGlobalConfig(config);
// Check if inside an OpenSpec project
const projectDir = process.cwd();
const openspecDir = path.join(projectDir, OPENSPEC_DIR_NAME);
if (fs.existsSync(openspecDir)) {
const applyNow = await confirm({
message: 'Apply changes to this project now?',
default: true,
});
if (applyNow) {
try {
execSync('npx openspec update', { stdio: 'inherit', cwd: projectDir });
console.log('Run `openspec update` in your other projects to apply.');
}
catch {
console.error('`openspec update` failed. Please run it manually to apply the profile changes.');
process.exitCode = 1;
}
return;
}
}
console.log('Config updated. Run `openspec update` in your projects to apply.');
}
catch (error) {
if (isPromptCancellationError(error)) {
console.log('Config profile cancelled.');
process.exitCode = 130;
return;
}
throw error;
}
});
}
//# sourceMappingURL=config.js.map

@@ -63,6 +63,6 @@ import { execSync, execFileSync } from 'child_process';

return `---
Submitted via OpenSpec CLI
- Version: ${version}
- Platform: ${platform}
- Timestamp: ${timestamp}`;
通过 OpenSpec CLI 提交
- 版本: ${version}
- 平台: ${platform}
- 时间戳: ${timestamp}`;
}

@@ -73,3 +73,3 @@ /**

function formatTitle(message) {
return `Feedback: ${message}`;
return `反馈: ${message}`;
}

@@ -92,3 +92,3 @@ /**

function generateManualSubmissionUrl(title, body) {
const repo = 'Fission-AI/OpenSpec';
const repo = 'studyzy/OpenSpec-cn';
const encodedTitle = encodeURIComponent(title);

@@ -103,8 +103,8 @@ const encodedBody = encodeURIComponent(body);

function displayFormattedFeedback(title, body) {
console.log('\n--- FORMATTED FEEDBACK ---');
console.log(`Title: ${title}`);
console.log(`Labels: feedback`);
console.log('\nBody:');
console.log('\n--- 格式化后的反馈内容 ---');
console.log(`标题: ${title}`);
console.log(`标签: feedback`);
console.log('\n正文:');
console.log(body);
console.log('--- END FEEDBACK ---\n');
console.log('--- 反馈结束 ---\n');
}

@@ -121,3 +121,3 @@ /**

'--repo',
'Fission-AI/OpenSpec',
'studyzy/OpenSpec-cn',
'--title',

@@ -131,4 +131,4 @@ title,

const issueUrl = result.trim();
console.log(`\n✓ Feedback submitted successfully!`);
console.log(`Issue URL: ${issueUrl}\n`);
console.log(`\n✓ 反馈提交成功!`);
console.log(`Issue 链接: ${issueUrl}\n`);
}

@@ -152,13 +152,13 @@ catch (error) {

if (reason === 'missing') {
console.log('⚠️ GitHub CLI not found. Manual submission required.');
console.log('⚠️ 未找到 GitHub CLI。需要手动提交。');
}
else {
console.log('⚠️ GitHub authentication required. Manual submission required.');
console.log('⚠️ GitHub 未认证。需要手动提交。');
}
displayFormattedFeedback(title, body);
const manualUrl = generateManualSubmissionUrl(title, body);
console.log('Please submit your feedback manually:');
console.log('请手动提交您的反馈:');
console.log(manualUrl);
if (reason === 'unauthenticated') {
console.log('\nTo auto-submit in the future: gh auth login');
console.log('\n若要将来自动提交,请运行: gh auth login');
}

@@ -165,0 +165,0 @@ // Exit with success code (fallback is successful)

@@ -81,3 +81,3 @@ import * as fs from 'node:fs';

if (verbose) {
console.log(' Checking schema.yaml exists...');
console.log(' 正在检查 schema.yaml 是否存在...');
}

@@ -88,3 +88,3 @@ if (!fs.existsSync(schemaPath)) {

path: 'schema.yaml',
message: 'schema.yaml not found',
message: '未找到 schema.yaml',
});

@@ -95,3 +95,3 @@ return { valid: false, issues };

if (verbose) {
console.log(' Parsing YAML...');
console.log(' 正在解析 YAML...');
}

@@ -106,3 +106,3 @@ let content;

path: 'schema.yaml',
message: `Failed to read file: ${err.message}`,
message: `读取文件失败: ${err.message}`,
});

@@ -113,3 +113,3 @@ return { valid: false, issues };

if (verbose) {
console.log(' Validating schema structure...');
console.log(' 正在验证 Schema 结构...');
}

@@ -132,3 +132,3 @@ let schema;

path: 'schema.yaml',
message: `Parse error: ${err.message}`,
message: `解析错误: ${err.message}`,
});

@@ -141,3 +141,3 @@ }

if (verbose) {
console.log(' Checking template files...');
console.log(' 正在检查模板文件...');
}

@@ -152,3 +152,3 @@ for (const artifact of schema.artifacts) {

path: `artifacts.${artifact.id}.template`,
message: `Template file '${artifact.template}' not found for artifact '${artifact.id}'`,
message: `未找到 Artifact '${artifact.id}' 的模板文件 '${artifact.template}'`,
});

@@ -160,3 +160,3 @@ }

if (verbose) {
console.log(' Dependency graph validation passed (via parseSchema)');
console.log(' 依赖图验证通过 (经由 parseSchema)');
}

@@ -194,3 +194,3 @@ return { valid: issues.length === 0, issues };

id: 'proposal',
description: 'High-level description of the change, its motivation, and scope',
description: '变更的高层描述、动机和范围',
generates: 'proposal.md',

@@ -201,3 +201,3 @@ template: 'proposal.md',

id: 'specs',
description: 'Detailed specifications with requirements and scenarios',
description: '包含需求和场景的详细规格说明',
generates: 'specs/**/*.md',

@@ -208,3 +208,3 @@ template: 'specs/spec.md',

id: 'design',
description: 'Technical design decisions and implementation approach',
description: '技术设计决策和实施方法',
generates: 'design.md',

@@ -215,3 +215,3 @@ template: 'design.md',

id: 'tasks',
description: 'Implementation checklist with trackable tasks',
description: '包含可追踪任务的实施清单',
generates: 'tasks.md',

@@ -227,6 +227,6 @@ template: 'tasks.md',

.command('schema')
.description('Manage workflow schemas [experimental]');
.description('管理工作流 Schema [实验性]');
// Experimental warning
schemaCmd.hook('preAction', () => {
console.error('Note: Schema commands are experimental and may change.');
console.error('注意:Schema 命令处于实验阶段,可能会发生变化。');
});

@@ -236,5 +236,5 @@ // schema which

.command('which [name]')
.description('Show where a schema resolves from')
.option('--json', 'Output as JSON')
.option('--all', 'List all schemas with their resolution sources')
.description('显示 Schema 的解析来源')
.option('--json', '以 JSON 格式输出')
.option('--all', '列出所有 Schema 及其解析来源')
.action(async (name, options) => {

@@ -251,3 +251,3 @@ try {

if (schemas.length === 0) {
console.log('No schemas found.');
console.log('未找到 Schema。');
return;

@@ -262,6 +262,6 @@ }

if (bySource.project.length > 0) {
console.log('\nProject schemas:');
console.log('\n项目 Schema:');
for (const schema of bySource.project) {
const shadowInfo = schema.shadows.length > 0
? ` (shadows: ${schema.shadows.map((s) => s.source).join(', ')})`
? ` (遮蔽: ${schema.shadows.map((s) => s.source).join(', ')})`
: '';

@@ -272,6 +272,6 @@ console.log(` ${schema.name}${shadowInfo}`);

if (bySource.user.length > 0) {
console.log('\nUser schemas:');
console.log('\n用户 Schema:');
for (const schema of bySource.user) {
const shadowInfo = schema.shadows.length > 0
? ` (shadows: ${schema.shadows.map((s) => s.source).join(', ')})`
? ` (遮蔽: ${schema.shadows.map((s) => s.source).join(', ')})`
: '';

@@ -282,3 +282,3 @@ console.log(` ${schema.name}${shadowInfo}`);

if (bySource.package.length > 0) {
console.log('\nPackage schemas:');
console.log('\n包 Schema:');
for (const schema of bySource.package) {

@@ -292,3 +292,3 @@ console.log(` ${schema.name}`);

if (!name) {
console.error('Error: Schema name is required (or use --all to list all schemas)');
console.error('错误:必须指定 Schema 名称(或使用 --all 列出所有 Schema)');
process.exitCode = 1;

@@ -302,3 +302,3 @@ return;

console.log(JSON.stringify({
error: `Schema '${name}' not found`,
error: `未找到 Schema '${name}'`,
available,

@@ -308,4 +308,4 @@ }, null, 2));

else {
console.error(`Error: Schema '${name}' not found`);
console.error(`Available schemas: ${available.join(', ')}`);
console.error(`错误:未找到 Schema '${name}'`);
console.error(`可用 Schema: ${available.join(', ')}`);
}

@@ -319,7 +319,7 @@ process.exitCode = 1;

else {
console.log(`Schema: ${resolution.name}`);
console.log(`Source: ${resolution.source}`);
console.log(`Path: ${resolution.path}`);
console.log(`Schema:${resolution.name}`);
console.log(`来源: ${resolution.source}`);
console.log(`路径: ${resolution.path}`);
if (resolution.shadows.length > 0) {
console.log('\nShadows:');
console.log('\n遮蔽 (Shadows):');
for (const shadow of resolution.shadows) {

@@ -332,3 +332,3 @@ console.log(` ${shadow.source}: ${shadow.path}`);

catch (error) {
console.error(`Error: ${error.message}`);
console.error(`错误:${error.message}`);
process.exitCode = 1;

@@ -340,5 +340,5 @@ }

.command('validate [name]')
.description('Validate a schema structure and templates')
.option('--json', 'Output as JSON')
.option('--verbose', 'Show detailed validation steps')
.description('验证 Schema 结构和模板')
.option('--json', '以 JSON 格式输出')
.option('--verbose', '显示详细验证步骤')
.action(async (name, options) => {

@@ -354,3 +354,3 @@ try {

valid: true,
message: 'No project schemas directory found',
message: '未找到项目 Schema 目录',
schemas: [],

@@ -360,3 +360,3 @@ }, null, 2));

else {
console.log('No project schemas directory found.');
console.log('未找到项目 Schema 目录。');
}

@@ -376,3 +376,3 @@ return;

if (options?.verbose && !options?.json) {
console.log(`\nValidating ${entry.name}...`);
console.log(`\n正在验证 ${entry.name}...`);
}

@@ -398,6 +398,6 @@ const result = validateSchema(schemaDir, options?.verbose && !options?.json);

if (schemaResults.length === 0) {
console.log('No schemas found in project.');
console.log('项目中未找到 Schema。');
return;
}
console.log('\nValidation Results:');
console.log('\n验证结果:');
for (const result of schemaResults) {

@@ -423,3 +423,3 @@ const status = result.valid ? '✓' : '✗';

valid: false,
error: `Schema '${name}' not found`,
error: `未找到 Schema '${name}'`,
available,

@@ -429,4 +429,4 @@ }, null, 2));

else {
console.error(`Error: Schema '${name}' not found`);
console.error(`Available schemas: ${available.join(', ')}`);
console.error(`错误:未找到 Schema '${name}'`);
console.error(`可用 Schema: ${available.join(', ')}`);
}

@@ -437,3 +437,3 @@ process.exitCode = 1;

if (options?.verbose && !options?.json) {
console.log(`Validating ${name}...`);
console.log(`正在验证 ${name}...`);
}

@@ -451,6 +451,6 @@ const result = validateSchema(schemaDir, options?.verbose && !options?.json);

if (result.valid) {
console.log(`✓ Schema '${name}' is valid`);
console.log(`✓ Schema '${name}' 有效`);
}
else {
console.log(`✗ Schema '${name}' has errors:`);
console.log(`✗ Schema '${name}' 存在错误:`);
for (const issue of result.issues) {

@@ -471,3 +471,3 @@ console.log(` ${issue.level}: ${issue.message}`);

else {
console.error(`Error: ${error.message}`);
console.error(`错误:${error.message}`);
}

@@ -480,5 +480,5 @@ process.exitCode = 1;

.command('fork <source> [name]')
.description('Copy an existing schema to project for customization')
.option('--json', 'Output as JSON')
.option('--force', 'Overwrite existing destination')
.description('复制现有 Schema 到项目中以进行自定义')
.option('--json', '以 JSON 格式输出')
.option('--force', '覆盖现有目标')
.action(async (source, name, options) => {

@@ -494,8 +494,8 @@ const spinner = options?.json ? null : ora();

forked: false,
error: `Invalid schema name '${destinationName}'. Use kebab-case (e.g., my-workflow)`,
error: `Schema 名称 '${destinationName}' 无效。请使用短横线连接的小写字母 (例如: my-workflow)`,
}, null, 2));
}
else {
console.error(`Error: Invalid schema name '${destinationName}'`);
console.error('Schema names must be kebab-case (e.g., my-workflow)');
console.error(`错误:Schema 名称 '${destinationName}' 无效`);
console.error('Schema 名称必须使用短横线连接的小写字母 (例如: my-workflow)');
}

@@ -512,3 +512,3 @@ process.exitCode = 1;

forked: false,
error: `Schema '${source}' not found`,
error: `未找到 Schema '${source}'`,
available,

@@ -518,4 +518,4 @@ }, null, 2));

else {
console.error(`Error: Schema '${source}' not found`);
console.error(`Available schemas: ${available.join(', ')}`);
console.error(`错误:未找到 Schema '${source}'`);
console.error(`可用 Schema: ${available.join(', ')}`);
}

@@ -535,9 +535,9 @@ process.exitCode = 1;

forked: false,
error: `Schema '${destinationName}' already exists`,
suggestion: 'Use --force to overwrite',
error: `Schema '${destinationName}' 已存在`,
suggestion: '使用 --force 覆盖',
}, null, 2));
}
else {
console.error(`Error: Schema '${destinationName}' already exists at ${destinationDir}`);
console.error('Use --force to overwrite');
console.error(`错误:Schema '${destinationName}' 已存在于 ${destinationDir}`);
console.error('使用 --force 覆盖');
}

@@ -549,3 +549,3 @@ process.exitCode = 1;

if (spinner)
spinner.start(`Removing existing schema '${destinationName}'...`);
spinner.start(`正在删除现有 Schema '${destinationName}'...`);
fs.rmSync(destinationDir, { recursive: true });

@@ -555,3 +555,3 @@ }

if (spinner)
spinner.start(`Forking '${source}' to '${destinationName}'...`);
spinner.start(`正在将 '${source}' Fork 到 '${destinationName}'...`);
copyDirRecursive(sourceDir, destinationDir);

@@ -565,3 +565,3 @@ // Update name in schema.yaml

if (spinner)
spinner.succeed(`Forked '${source}' to '${destinationName}'`);
spinner.succeed(`已将 '${source}' Fork 到 '${destinationName}'`);
if (options?.json) {

@@ -578,5 +578,5 @@ console.log(JSON.stringify({

else {
console.log(`\nSource: ${sourceDir} (${sourceLocation})`);
console.log(`Destination: ${destinationDir}`);
console.log(`\nYou can now customize the schema at:`);
console.log(`\n来源: ${sourceDir} (${sourceLocation})`);
console.log(`目标: ${destinationDir}`);
console.log(`\n现在您可以在以下位置自定义 Schema:`);
console.log(` ${destinationDir}/schema.yaml`);

@@ -587,3 +587,3 @@ }

if (spinner)
spinner.fail(`Fork failed`);
spinner.fail(`Fork 失败`);
if (options?.json) {

@@ -596,3 +596,3 @@ console.log(JSON.stringify({

else {
console.error(`Error: ${error.message}`);
console.error(`错误:${error.message}`);
}

@@ -605,9 +605,9 @@ process.exitCode = 1;

.command('init <name>')
.description('Create a new project-local schema')
.option('--json', 'Output as JSON')
.option('--description <text>', 'Schema description')
.option('--artifacts <list>', 'Comma-separated artifact IDs (proposal,specs,design,tasks)')
.option('--default', 'Set as project default schema')
.option('--no-default', 'Do not prompt to set as default')
.option('--force', 'Overwrite existing schema')
.description('创建一个新的项目本地 Schema')
.option('--json', '以 JSON 格式输出')
.option('--description <text>', 'Schema 描述')
.option('--artifacts <list>', '逗号分隔的 Artifact ID (proposal,specs,design,tasks)')
.option('--default', '设为项目默认 Schema')
.option('--no-default', '不提示设为默认')
.option('--force', '覆盖现有 Schema')
.action(async (name, options) => {

@@ -622,8 +622,8 @@ const spinner = options?.json ? null : ora();

created: false,
error: `Invalid schema name '${name}'. Use kebab-case (e.g., my-workflow)`,
error: `Schema 名称 '${name}' 无效。请使用短横线连接的小写字母 (例如: my-workflow)`,
}, null, 2));
}
else {
console.error(`Error: Invalid schema name '${name}'`);
console.error('Schema names must be kebab-case (e.g., my-workflow)');
console.error(`错误:Schema 名称 '${name}' 无效`);
console.error('Schema 名称必须使用短横线连接的小写字母 (例如: my-workflow)');
}

@@ -640,9 +640,9 @@ process.exitCode = 1;

created: false,
error: `Schema '${name}' already exists`,
suggestion: 'Use --force to overwrite or "openspec schema fork" to copy',
error: `Schema '${name}' 已存在`,
suggestion: '使用 --force 覆盖或使用 "openspec-cn schema fork" 进行复制',
}, null, 2));
}
else {
console.error(`Error: Schema '${name}' already exists at ${schemaDir}`);
console.error('Use --force to overwrite or "openspec schema fork" to copy');
console.error(`错误:Schema '${name}' 已存在于 ${schemaDir}`);
console.error('使用 --force 覆盖或使用 "openspec-cn schema fork" 进行复制');
}

@@ -653,3 +653,3 @@ process.exitCode = 1;

if (spinner)
spinner.start(`Removing existing schema '${name}'...`);
spinner.start(`正在删除现有 Schema '${name}'...`);
fs.rmSync(schemaDir, { recursive: true });

@@ -667,4 +667,4 @@ }

description = await input({
message: 'Schema description:',
default: `Custom workflow schema for ${name}`,
message: 'Schema 描述:',
default: `${name} 的自定义工作流 Schema`,
});

@@ -677,7 +677,7 @@ const artifactChoices = DEFAULT_ARTIFACTS.map((a) => ({

selectedArtifactIds = await checkbox({
message: 'Select artifacts to include:',
message: '选择要包含的 Artifact:',
choices: artifactChoices,
});
if (selectedArtifactIds.length === 0) {
console.error('Error: At least one artifact must be selected');
console.error('错误:必须至少选择一个 Artifact');
process.exitCode = 1;

@@ -689,3 +689,3 @@ return;

const setAsDefault = await confirm({
message: 'Set as project default schema?',
message: '设为项目默认 Schema?',
default: false,

@@ -700,3 +700,3 @@ });

// Non-interactive mode
description = options?.description || `Custom workflow schema for ${name}`;
description = options?.description || `${name} 的自定义工作流 Schema`;
if (options?.artifacts) {

@@ -711,3 +711,3 @@ selectedArtifactIds = options.artifacts.split(',').map((a) => a.trim());

created: false,
error: `Unknown artifact '${id}'`,
error: `未知 Artifact '${id}'`,
valid: validIds,

@@ -717,4 +717,4 @@ }, null, 2));

else {
console.error(`Error: Unknown artifact '${id}'`);
console.error(`Valid artifacts: ${validIds.join(', ')}`);
console.error(`错误:未知 Artifact '${id}'`);
console.error(`有效 Artifact: ${validIds.join(', ')}`);
}

@@ -733,3 +733,3 @@ process.exitCode = 1;

if (spinner)
spinner.start(`Creating schema '${name}'...`);
spinner.start(`正在创建 Schema '${name}'...`);
fs.mkdirSync(schemaDir, { recursive: true });

@@ -810,3 +810,3 @@ // Build artifacts array with proper dependencies

if (spinner)
spinner.succeed(`Created schema '${name}'`);
spinner.succeed(`已创建 Schema '${name}'`);
if (options?.json) {

@@ -822,11 +822,11 @@ console.log(JSON.stringify({

else {
console.log(`\nSchema created at: ${schemaDir}`);
console.log(`\nSchema 创建于: ${schemaDir}`);
console.log(`\nArtifacts: ${selectedArtifactIds.join(', ')}`);
if (options?.default) {
console.log(`\nSet as project default schema.`);
console.log(`\n已设为项目默认 Schema。`);
}
console.log(`\nNext steps:`);
console.log(` 1. Edit ${schemaDir}/schema.yaml to customize artifacts`);
console.log(` 2. Modify templates in the schema directory`);
console.log(` 3. Use with: openspec new --schema ${name}`);
console.log(`\n后续步骤:`);
console.log(` 1. 编辑 ${schemaDir}/schema.yaml 以自定义 Artifact`);
console.log(` 2. 修改 Schema 目录中的模板`);
console.log(` 3. 使用命令: openspec-cn new --schema ${name}`);
}

@@ -836,3 +836,3 @@ }

if (spinner)
spinner.fail(`Creation failed`);
spinner.fail(`创建失败`);
if (options?.json) {

@@ -857,65 +857,65 @@ console.log(JSON.stringify({

case 'proposal':
return `## Why
return `## 为什么
<!-- Describe the motivation for this change -->
<!-- 描述此变更的动机 -->
## What Changes
## 变更内容
<!-- Describe what will change -->
<!-- 描述将要变更的内容 -->
## Capabilities
## 能力
### New Capabilities
<!-- List new capabilities -->
### 新增能力
<!-- 列出新增能力 -->
### Modified Capabilities
<!-- List modified capabilities -->
### 修改的能力
<!-- 列出修改的能力 -->
## Impact
## 影响
<!-- Describe the impact on existing functionality -->
<!-- 描述对现有功能的影响 -->
`;
case 'specs':
return `## ADDED Requirements
return `## 新增需求
### Requirement: Example requirement
### 需求: 示例需求
Description of the requirement.
需求描述。
#### Scenario: Example scenario
- **WHEN** some condition
- **THEN** some outcome
#### 场景: 示例场景
- **当** 满足某些条件
- **则** 产生某些结果
`;
case 'design':
return `## Context
return `## 背景
<!-- Background and context -->
<!-- 背景和上下文 -->
## Goals / Non-Goals
## 目标 / 非目标
**Goals:**
<!-- List goals -->
**目标:**
<!-- 列出目标 -->
**Non-Goals:**
<!-- List non-goals -->
**非目标:**
<!-- 列出非目标 -->
## Decisions
## 决策
### 1. Decision Name
### 1. 决策名称
Description and rationale.
描述和理由。
**Alternatives considered:**
- Alternative 1: Rejected because...
**考虑过的替代方案:**
- 替代方案 1: 被拒绝,因为...
## Risks / Trade-offs
## 风险 / 权衡
<!-- List risks and trade-offs -->
<!-- 列出风险和权衡 -->
`;
case 'tasks':
return `## Implementation Tasks
return `## 实施任务
- [ ] Task 1
- [ ] Task 2
- [ ] Task 3
- [ ] 任务 1
- [ ] 任务 2
- [ ] 任务 3
`;

@@ -925,3 +925,3 @@ default:

<!-- Add content here -->
<!-- 在此添加内容 -->
`;

@@ -928,0 +928,0 @@ }

@@ -141,3 +141,3 @@ import ora from 'ora';

bullets.push('- 确保变更在specs/中有增量:使用标题## 新增|修改|移除|重命名需求');
bullets.push('- 每个需求必须至少包含一个#### 场景:块');
bullets.push('- 每个需求必须至少包含一个#### 场景:块');
bullets.push('- 调试解析的增量:openspec-cn change show <id> --json --deltas-only');

@@ -147,3 +147,3 @@ }

bullets.push('- 确保规范包含## 目的和## 需求部分');
bullets.push('- 每个需求必须至少包含一个#### 场景:块');
bullets.push('- 每个需求必须至少包含一个#### 场景:块');
bullets.push('- 使用--json重新运行以查看结构化报告');

@@ -150,0 +150,0 @@ }

@@ -56,3 +56,3 @@ /**

console.log(`Schema: ${schemaName}`);
console.log(`Source: ${source}`);
console.log(`来源:${source}`);
console.log();

@@ -59,0 +59,0 @@ for (const t of templates) {

@@ -22,3 +22,5 @@ /**

export { kilocodeAdapter } from './kilocode.js';
export { kiroAdapter } from './kiro.js';
export { opencodeAdapter } from './opencode.js';
export { piAdapter } from './pi.js';
export { qoderAdapter } from './qoder.js';

@@ -25,0 +27,0 @@ export { qwenAdapter } from './qwen.js';

@@ -22,3 +22,5 @@ /**

export { kilocodeAdapter } from './kilocode.js';
export { kiroAdapter } from './kiro.js';
export { opencodeAdapter } from './opencode.js';
export { piAdapter } from './pi.js';
export { qoderAdapter } from './qoder.js';

@@ -25,0 +27,0 @@ export { qwenAdapter } from './qwen.js';

@@ -23,3 +23,5 @@ /**

import { kilocodeAdapter } from './adapters/kilocode.js';
import { kiroAdapter } from './adapters/kiro.js';
import { opencodeAdapter } from './adapters/opencode.js';
import { piAdapter } from './adapters/pi.js';
import { qoderAdapter } from './adapters/qoder.js';

@@ -52,3 +54,5 @@ import { qwenAdapter } from './adapters/qwen.js';

CommandAdapterRegistry.register(kilocodeAdapter);
CommandAdapterRegistry.register(kiroAdapter);
CommandAdapterRegistry.register(opencodeAdapter);
CommandAdapterRegistry.register(piAdapter);
CommandAdapterRegistry.register(qoderAdapter);

@@ -55,0 +59,0 @@ CommandAdapterRegistry.register(qwenAdapter);

@@ -377,2 +377,7 @@ /**

},
{
name: 'profile',
description: 'Configure workflow profile (interactive picker or preset shortcut)',
flags: [],
},
],

@@ -379,0 +384,0 @@ },

@@ -143,3 +143,5 @@ import { promises as fs } from 'fs';

const newContent = profileContent + openspecBlock;
await fs.writeFile(profilePath, newContent, 'utf-8');
// Use UTF-8 with BOM for Windows PowerShell 5.1 compatibility
const bomContent = '\uFEFF' + newContent;
await fs.writeFile(profilePath, bomContent, 'utf-8');
anyConfigured = true;

@@ -190,3 +192,5 @@ }

const newContent = (beforeBlock.trimEnd() + '\n' + afterBlock.trimStart()).trim() + '\n';
await fs.writeFile(profilePath, newContent, 'utf-8');
// Use UTF-8 with BOM for Windows PowerShell 5.1 compatibility
const bomContent = '\uFEFF' + newContent;
await fs.writeFile(profilePath, bomContent, 'utf-8');
anyRemoved = true;

@@ -212,3 +216,7 @@ }

try {
const existingContent = await fs.readFile(targetPath, 'utf-8');
let existingContent = await fs.readFile(targetPath, 'utf-8');
// Remove BOM if present for comparison (BOM is added on write for Windows compatibility)
if (existingContent.charCodeAt(0) === 0xfeff) {
existingContent = existingContent.slice(1);
}
if (existingContent === completionScript) {

@@ -238,4 +246,5 @@ // Already installed and up to date

const backupPath = isUpdate ? await this.backupExistingFile(targetPath) : undefined;
// Write the completion script
await fs.writeFile(targetPath, completionScript, 'utf-8');
// Write the completion script with UTF-8 BOM for Windows PowerShell 5.1 compatibility
const bomCompletionScript = '\uFEFF' + completionScript;
await fs.writeFile(targetPath, bomCompletionScript, 'utf-8');
// Auto-configure PowerShell profile

@@ -242,0 +251,0 @@ const profileConfigured = await this.configureProfile(targetPath);

@@ -5,3 +5,3 @@ /**

*/
export declare const ZSH_DYNAMIC_HELPERS = "# Dynamic completion helpers\n\n# Use openspec __complete to get available changes\n_openspec_complete_changes() {\n local -a changes\n while IFS=$'\\t' read -r id desc; do\n changes+=(\"$id:$desc\")\n done < <(openspec __complete changes 2>/dev/null)\n _describe \"change\" changes\n}\n\n# Use openspec __complete to get available specs\n_openspec_complete_specs() {\n local -a specs\n while IFS=$'\\t' read -r id desc; do\n specs+=(\"$id:$desc\")\n done < <(openspec __complete specs 2>/dev/null)\n _describe \"spec\" specs\n}\n\n# Get both changes and specs\n_openspec_complete_items() {\n local -a items\n while IFS=$'\\t' read -r id desc; do\n items+=(\"$id:$desc\")\n done < <(openspec __complete changes 2>/dev/null)\n while IFS=$'\\t' read -r id desc; do\n items+=(\"$id:$desc\")\n done < <(openspec __complete specs 2>/dev/null)\n _describe \"item\" items\n}";
export declare const ZSH_DYNAMIC_HELPERS = "# Dynamic completion helpers\n\n# Use openspec-cn __complete to get available changes\n_openspec_complete_changes() {\n local -a changes\n while IFS=$'\\t' read -r id desc; do\n changes+=(\"$id:$desc\")\n done < <(openspec-cn __complete changes 2>/dev/null)\n _describe \"change\" changes\n}\n\n# Use openspec-cn __complete to get available specs\n_openspec_complete_specs() {\n local -a specs\n while IFS=$'\\t' read -r id desc; do\n specs+=(\"$id:$desc\")\n done < <(openspec-cn __complete specs 2>/dev/null)\n _describe \"spec\" specs\n}\n\n# Get both changes and specs\n_openspec_complete_items() {\n local -a items\n while IFS=$'\\t' read -r id desc; do\n items+=(\"$id:$desc\")\n done < <(openspec-cn __complete changes 2>/dev/null)\n while IFS=$'\\t' read -r id desc; do\n items+=(\"$id:$desc\")\n done < <(openspec-cn __complete specs 2>/dev/null)\n _describe \"item\" items\n}";
//# sourceMappingURL=zsh-templates.d.ts.map

@@ -7,3 +7,3 @@ /**

# Use openspec __complete to get available changes
# Use openspec-cn __complete to get available changes
_openspec_complete_changes() {

@@ -13,7 +13,7 @@ local -a changes

changes+=("$id:$desc")
done < <(openspec __complete changes 2>/dev/null)
done < <(openspec-cn __complete changes 2>/dev/null)
_describe "change" changes
}
# Use openspec __complete to get available specs
# Use openspec-cn __complete to get available specs
_openspec_complete_specs() {

@@ -23,3 +23,3 @@ local -a specs

specs+=("$id:$desc")
done < <(openspec __complete specs 2>/dev/null)
done < <(openspec-cn __complete specs 2>/dev/null)
_describe "spec" specs

@@ -33,8 +33,8 @@ }

items+=("$id:$desc")
done < <(openspec __complete changes 2>/dev/null)
done < <(openspec-cn __complete changes 2>/dev/null)
while IFS=$'\\t' read -r id desc; do
items+=("$id:$desc")
done < <(openspec __complete specs 2>/dev/null)
done < <(openspec-cn __complete specs 2>/dev/null)
_describe "item" items
}`;
//# sourceMappingURL=zsh-templates.js.map

@@ -13,23 +13,23 @@ /**

// Context section with comments
lines.push('# Project context (optional)');
lines.push('# This is shown to AI when creating artifacts.');
lines.push('# Add your tech stack, conventions, style guides, domain knowledge, etc.');
lines.push('# Example:');
lines.push('# 项目上下文(可选)');
lines.push('# 在创建工件时向 AI 显示此信息。');
lines.push('# 添加您的技术栈、约定、风格指南、领域知识等。');
lines.push('# 示例:');
lines.push('# context: |');
lines.push('# Tech stack: TypeScript, React, Node.js');
lines.push('# We use conventional commits');
lines.push('# Domain: e-commerce platform');
lines.push('# 技术栈:TypeScript, React, Node.js');
lines.push('# 我们使用约定式提交');
lines.push('# 领域:电商平台');
lines.push('');
// Rules section with comments
lines.push('# Per-artifact rules (optional)');
lines.push('# Add custom rules for specific artifacts.');
lines.push('# Example:');
lines.push('# 每个工件的规则(可选)');
lines.push('# 为特定工件添加自定义规则。');
lines.push('# 示例:');
lines.push('# rules:');
lines.push('# proposal:');
lines.push('# - Keep proposals under 500 words');
lines.push('# - Always include a "Non-goals" section');
lines.push('# - 保持提案在500字以内');
lines.push('# - 始终包含"非目标"部分');
lines.push('# tasks:');
lines.push('# - Break tasks into chunks of max 2 hours');
lines.push('# - 将任务分解为最多2小时的块');
return lines.join('\n') + '\n';
}
//# sourceMappingURL=config-prompts.js.map

@@ -8,2 +8,12 @@ import { z } from 'zod';

featureFlags: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodBoolean>>>;
profile: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
core: "core";
custom: "custom";
}>>>;
delivery: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
commands: "commands";
skills: "skills";
both: "both";
}>>>;
workflows: z.ZodOptional<z.ZodArray<z.ZodString>>;
}, z.core.$loose>;

@@ -10,0 +20,0 @@ export type GlobalConfigType = z.infer<typeof GlobalConfigSchema>;

@@ -12,2 +12,13 @@ import { z } from 'zod';

.default({}),
profile: z
.enum(['core', 'custom'])
.optional()
.default('core'),
delivery: z
.enum(['both', 'skills', 'commands'])
.optional()
.default('both'),
workflows: z
.array(z.string())
.optional(),
})

@@ -20,4 +31,6 @@ .passthrough();

featureFlags: {},
profile: 'core',
delivery: 'both',
};
const KNOWN_TOP_LEVEL_KEYS = new Set(Object.keys(DEFAULT_CONFIG));
const KNOWN_TOP_LEVEL_KEYS = new Set([...Object.keys(DEFAULT_CONFIG), 'workflows']);
/**

@@ -24,0 +37,0 @@ * Validate a config key path for CLI set operations.

@@ -23,3 +23,5 @@ export const OPENSPEC_DIR_NAME = 'openspec';

{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode' },
{ name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' },
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' },
{ name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' },
{ name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' },

@@ -26,0 +28,0 @@ { name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' },

export declare const GLOBAL_CONFIG_DIR_NAME = "openspec";
export declare const GLOBAL_CONFIG_FILE_NAME = "config.json";
export declare const GLOBAL_DATA_DIR_NAME = "openspec";
export type Profile = 'core' | 'custom';
export type Delivery = 'both' | 'skills' | 'commands';
export interface GlobalConfig {
featureFlags?: Record<string, boolean>;
profile?: Profile;
delivery?: Delivery;
workflows?: string[];
}

@@ -7,0 +12,0 @@ /**

@@ -9,3 +9,5 @@ import * as fs from 'node:fs';

const DEFAULT_CONFIG = {
featureFlags: {}
featureFlags: {},
profile: 'core',
delivery: 'both',
};

@@ -85,3 +87,3 @@ /**

// Merge with defaults (loaded values take precedence)
return {
const merged = {
...DEFAULT_CONFIG,

@@ -95,2 +97,10 @@ ...parsed,

};
// Schema evolution: apply defaults for new fields if not present in loaded config
if (parsed.profile === undefined) {
merged.profile = DEFAULT_CONFIG.profile;
}
if (parsed.delivery === undefined) {
merged.delivery = DEFAULT_CONFIG.delivery;
}
return merged;
}

@@ -97,0 +107,0 @@ catch (error) {

@@ -11,2 +11,3 @@ /**

interactive?: boolean;
profile?: string;
};

@@ -17,2 +18,3 @@ export declare class InitCommand {

private readonly interactiveOption?;
private readonly profileOverride?;
constructor(options?: InitCommandOptions);

@@ -22,2 +24,3 @@ execute(targetPath: string): Promise<void>;

private canPromptInteractively;
private resolveProfileOverride;
private handleLegacyCleanup;

@@ -33,4 +36,6 @@ private performLegacyCleanup;

private startSpinner;
private removeSkillDirs;
private removeCommandFiles;
}
export {};
//# sourceMappingURL=init.d.ts.map

@@ -21,2 +21,6 @@ /**

import { getToolsWithSkillsDir, getToolStates, getSkillTemplates, getCommandContents, generateSkillContent, } from './shared/index.js';
import { getGlobalConfig } from './global-config.js';
import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js';
import { getAvailableTools } from './available-tools.js';
import { migrateIfNeeded } from './migration.js';
const require = createRequire(import.meta.url);

@@ -32,2 +36,15 @@ const { version: OPENSPEC_VERSION } = require('../../package.json');

};
const WORKFLOW_TO_SKILL_DIR = {
'explore': 'openspec-explore',
'new': 'openspec-new-change',
'continue': 'openspec-continue-change',
'apply': 'openspec-apply-change',
'ff': 'openspec-ff-change',
'sync': 'openspec-sync-specs',
'archive': 'openspec-archive-change',
'bulk-archive': 'openspec-bulk-archive-change',
'verify': 'openspec-verify-change',
'onboard': 'openspec-onboard',
'propose': 'openspec-propose',
};
// -----------------------------------------------------------------------------

@@ -40,2 +57,3 @@ // Init Command Class

interactiveOption;
profileOverride;
constructor(options = {}) {

@@ -45,2 +63,3 @@ this.toolsArg = options.tools;

this.interactiveOption = options.interactive;
this.profileOverride = options.profile;
}

@@ -55,2 +74,8 @@ async execute(targetPath) {

await this.handleLegacyCleanup(projectPath, extendMode);
// Detect available tools in the project (task 7.1)
const detectedTools = getAvailableTools(projectPath);
// Migration check: migrate existing projects to profile system (task 7.3)
if (extendMode) {
migrateIfNeeded(projectPath, detectedTools);
}
// Show animated welcome screen (interactive mode only)

@@ -62,6 +87,9 @@ const canPrompt = this.canPromptInteractively();

}
// Validate profile override early so invalid values fail before tool setup.
// The resolved value is consumed later when generation reads effective config.
this.resolveProfileOverride();
// Get tool states before processing
const toolStates = getToolStates(projectPath);
// Get tool selection
const selectedToolIds = await this.getSelectedTools(toolStates, extendMode);
// Get tool selection (pass detected tools for pre-selection)
const selectedToolIds = await this.getSelectedTools(toolStates, extendMode, detectedTools, projectPath);
// Validate selected tools

@@ -96,2 +124,11 @@ const validatedTools = this.validateTools(selectedToolIds, toolStates);

}
resolveProfileOverride() {
if (this.profileOverride === undefined) {
return undefined;
}
if (this.profileOverride === 'core' || this.profileOverride === 'custom') {
return this.profileOverride;
}
throw new Error(`Invalid profile "${this.profileOverride}". Available profiles: core, custom`);
}
// ═══════════════════════════════════════════════════════════

@@ -149,3 +186,3 @@ // LEGACY CLEANUP

// ═══════════════════════════════════════════════════════════
async getSelectedTools(toolStates, extendMode) {
async getSelectedTools(toolStates, extendMode, detectedTools, projectPath) {
// Check for --tools flag first

@@ -157,9 +194,21 @@ const nonInteractiveSelection = this.resolveToolsArg();

const validTools = getToolsWithSkillsDir();
const detectedToolIds = new Set(detectedTools.map((t) => t.value));
const configuredToolIds = new Set([...toolStates.entries()]
.filter(([, status]) => status.configured)
.map(([toolId]) => toolId));
const shouldPreselectDetected = !extendMode && configuredToolIds.size === 0;
const canPrompt = this.canPromptInteractively();
if (!canPrompt || validTools.length === 0) {
throw new Error(`Missing required option --tools. Valid tools:\n ${validTools.join('\n ')}\n\nUse --tools all, --tools none, or --tools claude,cursor,...`);
// Non-interactive mode: use detected tools as fallback (task 7.8)
if (!canPrompt) {
if (detectedToolIds.size > 0) {
return [...detectedToolIds];
}
throw new Error(`No tools detected and no --tools flag provided. Valid tools:\n ${validTools.join('\n ')}\n\nUse --tools all, --tools none, or --tools claude,cursor,...`);
}
if (validTools.length === 0) {
throw new Error(`No tools available for skill generation.`);
}
// Interactive mode: show searchable multi-select
const { searchableMultiSelect } = await import('../prompts/searchable-multi-select.js');
// Build choices with configured status and sort configured tools first
// Build choices: pre-select configured tools; keep detected tools visible but unselected.
const sortedChoices = validTools

@@ -170,2 +219,3 @@ .map((toolId) => {

const configured = status?.configured ?? false;
const detected = detectedToolIds.has(toolId);
return {

@@ -175,7 +225,8 @@ name: tool?.name || toolId,

configured,
preSelected: configured, // Pre-select configured tools for easy refresh
detected: detected && !configured,
preSelected: configured || (shouldPreselectDetected && detected && !configured),
};
})
.sort((a, b) => {
// Configured tools first
// Configured tools first, then detected (not configured), then everything else.
if (a.configured && !b.configured)

@@ -185,4 +236,23 @@ return -1;

return 1;
if (a.detected && !b.detected)
return -1;
if (!a.detected && b.detected)
return 1;
return 0;
});
const configuredNames = validTools
.filter((toolId) => configuredToolIds.has(toolId))
.map((toolId) => AI_TOOLS.find((t) => t.value === toolId)?.name || toolId);
if (configuredNames.length > 0) {
console.log(`OpenSpec 已配置:${configuredNames.join(', ')}(已预选)`);
}
const detectedOnlyNames = detectedTools
.filter((tool) => !configuredToolIds.has(tool.value))
.map((tool) => tool.name);
if (detectedOnlyNames.length > 0) {
const detectionLabel = shouldPreselectDetected
? '首次设置已预选'
: '未预选';
console.log(`检测到工具目录:${detectedOnlyNames.join(', ')}(${detectionLabel})`);
}
const selectedTools = await searchableMultiSelect({

@@ -303,5 +373,14 @@ message: `选择要设置的工具(共 ${validTools.length} 个可用)`,

const commandsSkipped = [];
// Get skill and command templates once (shared across all tools)
const skillTemplates = getSkillTemplates();
const commandContents = getCommandContents();
let removedCommandCount = 0;
let removedSkillCount = 0;
// Read global config for profile and delivery settings (use --profile override if set)
const globalConfig = getGlobalConfig();
const profile = this.resolveProfileOverride() ?? globalConfig.profile ?? 'core';
const delivery = globalConfig.delivery ?? 'both';
const workflows = getProfileWorkflows(profile, globalConfig.workflows);
// Get skill and command templates filtered by profile workflows
const shouldGenerateSkills = delivery !== 'commands';
const shouldGenerateCommands = delivery !== 'skills';
const skillTemplates = shouldGenerateSkills ? getSkillTemplates(workflows) : [];
const commandContents = shouldGenerateCommands ? getCommandContents(workflows) : [];
// Process each tool

@@ -311,26 +390,38 @@ for (const tool of tools) {

try {
// Use tool-specific skillsDir
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
// Create skill directories and SKILL.md files
for (const { template, dirName } of skillTemplates) {
const skillDir = path.join(skillsDir, dirName);
const skillFile = path.join(skillDir, 'SKILL.md');
// Generate SKILL.md content with YAML frontmatter including generatedBy
// Use hyphen-based command references for OpenCode
const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
// Write the skill file
await FileSystemUtils.writeFile(skillFile, skillContent);
// Generate skill files if delivery includes skills
if (shouldGenerateSkills) {
// Use tool-specific skillsDir
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
// Create skill directories and SKILL.md files
for (const { template, dirName } of skillTemplates) {
const skillDir = path.join(skillsDir, dirName);
const skillFile = path.join(skillDir, 'SKILL.md');
// Generate SKILL.md content with YAML frontmatter including generatedBy
// Use hyphen-based command references for OpenCode
const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
// Write the skill file
await FileSystemUtils.writeFile(skillFile, skillContent);
}
}
// Generate commands using the adapter system
const adapter = CommandAdapterRegistry.get(tool.value);
if (adapter) {
const generatedCommands = generateCommands(commandContents, adapter);
for (const cmd of generatedCommands) {
const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path);
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
if (!shouldGenerateSkills) {
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
removedSkillCount += await this.removeSkillDirs(skillsDir);
}
// Generate commands if delivery includes commands
if (shouldGenerateCommands) {
const adapter = CommandAdapterRegistry.get(tool.value);
if (adapter) {
const generatedCommands = generateCommands(commandContents, adapter);
for (const cmd of generatedCommands) {
const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path);
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
}
}
else {
commandsSkipped.push(tool.value);
}
}
else {
commandsSkipped.push(tool.value);
if (!shouldGenerateCommands) {
removedCommandCount += await this.removeCommandFiles(projectPath, tool.value);
}

@@ -350,3 +441,10 @@ spinner.succeed(`Setup complete for ${tool.name}`);

}
return { createdTools, refreshedTools, failedTools, commandsSkipped };
return {
createdTools,
refreshedTools,
failedTools,
commandsSkipped,
removedCommandCount,
removedSkillCount,
};
}

@@ -391,13 +489,21 @@ // ═══════════════════════════════════════════════════════════

}
// Show counts
// Show counts (respecting profile filter)
const successfulTools = [...results.createdTools, ...results.refreshedTools];
if (successfulTools.length > 0) {
const globalConfig = getGlobalConfig();
const profile = this.profileOverride ?? globalConfig.profile ?? 'core';
const delivery = globalConfig.delivery ?? 'both';
const workflows = getProfileWorkflows(profile, globalConfig.workflows);
const toolDirs = [...new Set(successfulTools.map((t) => t.skillsDir))].join(', ');
const hasCommands = results.commandsSkipped.length < successfulTools.length;
if (hasCommands) {
console.log(`${getSkillTemplates().length} 个技能和 ${getCommandContents().length} 个命令在 ${toolDirs}/ 中`);
const skillCount = delivery !== 'commands' ? getSkillTemplates(workflows).length : 0;
const commandCount = delivery !== 'skills' ? getCommandContents(workflows).length : 0;
if (skillCount > 0 && commandCount > 0) {
console.log(`${skillCount} 个技能和 ${commandCount} 个命令在 ${toolDirs}/ 中`);
}
else {
console.log(`${getSkillTemplates().length} 个技能在 ${toolDirs}/ 中`);
else if (skillCount > 0) {
console.log(`${skillCount} 个技能在 ${toolDirs}/ 中`);
}
else if (commandCount > 0) {
console.log(`${commandCount} 个命令在 ${toolDirs}/ 中`);
}
}

@@ -412,2 +518,8 @@ // Show failures

}
if (results.removedCommandCount > 0) {
console.log(chalk.dim(`Removed: ${results.removedCommandCount} command files (delivery: skills)`));
}
if (results.removedSkillCount > 0) {
console.log(chalk.dim(`Removed: ${results.removedSkillCount} skill directories (delivery: commands)`));
}
// Config status

@@ -427,8 +539,18 @@ if (configStatus === 'created') {

}
// Getting started
// Getting started (task 7.6: show propose if in profile)
const globalCfg = getGlobalConfig();
const activeProfile = this.profileOverride ?? globalCfg.profile ?? 'core';
const activeWorkflows = [...getProfileWorkflows(activeProfile, globalCfg.workflows)];
console.log();
console.log(chalk.bold('开始使用:'));
console.log(' /opsx:new 开始一个新变更');
console.log(' /opsx:continue 创建下一个产出物');
console.log(' /opsx:apply 实现任务');
if (activeWorkflows.includes('propose')) {
console.log(chalk.bold('开始使用:'));
console.log(' 开始您的第一个变更:/opsx:propose "您的想法"');
}
else if (activeWorkflows.includes('new')) {
console.log(chalk.bold('开始使用:'));
console.log(' 开始您的第一个变更:/opsx:new "您的想法"');
}
else {
console.log("完成。运行 'openspec-cn config profile' 配置您的工作流程。");
}
// Links

@@ -453,3 +575,42 @@ console.log();

}
async removeSkillDirs(skillsDir) {
let removed = 0;
for (const workflow of ALL_WORKFLOWS) {
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
if (!dirName)
continue;
const skillDir = path.join(skillsDir, dirName);
try {
if (fs.existsSync(skillDir)) {
await fs.promises.rm(skillDir, { recursive: true, force: true });
removed++;
}
}
catch {
// Ignore errors
}
}
return removed;
}
async removeCommandFiles(projectPath, toolId) {
let removed = 0;
const adapter = CommandAdapterRegistry.get(toolId);
if (!adapter)
return 0;
for (const workflow of ALL_WORKFLOWS) {
const cmdPath = adapter.getFilePath(workflow);
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
try {
if (fs.existsSync(fullPath)) {
await fs.promises.unlink(fullPath);
removed++;
}
}
catch {
// Ignore errors
}
}
return removed;
}
}
//# sourceMappingURL=init.js.map

@@ -40,2 +40,3 @@ /**

'kilocode': { type: 'files', pattern: '.kilocode/workflows/openspec-*.md' },
'kiro': { type: 'files', pattern: '.kiro/prompts/openspec-*.prompt.md' },
'github-copilot': { type: 'files', pattern: '.github/prompts/openspec-*.prompt.md' },

@@ -42,0 +43,0 @@ 'amazon-q': { type: 'files', pattern: '.amazonq/prompts/openspec-*.md' },

@@ -6,4 +6,4 @@ /**

*/
export { SKILL_NAMES, type SkillName, type ToolSkillStatus, type ToolVersionStatus, getToolsWithSkillsDir, getToolSkillStatus, getToolStates, extractGeneratedByVersion, getToolVersionStatus, getConfiguredTools, getAllToolVersionStatus, } from './tool-detection.js';
export { SKILL_NAMES, type SkillName, COMMAND_IDS, type CommandId, type ToolSkillStatus, type ToolVersionStatus, getToolsWithSkillsDir, getToolSkillStatus, getToolStates, extractGeneratedByVersion, getToolVersionStatus, getConfiguredTools, getAllToolVersionStatus, } from './tool-detection.js';
export { type SkillTemplateEntry, type CommandTemplateEntry, getSkillTemplates, getCommandTemplates, getCommandContents, generateSkillContent, } from './skill-generation.js';
//# sourceMappingURL=index.d.ts.map

@@ -6,4 +6,4 @@ /**

*/
export { SKILL_NAMES, getToolsWithSkillsDir, getToolSkillStatus, getToolStates, extractGeneratedByVersion, getToolVersionStatus, getConfiguredTools, getAllToolVersionStatus, } from './tool-detection.js';
export { SKILL_NAMES, COMMAND_IDS, getToolsWithSkillsDir, getToolSkillStatus, getToolStates, extractGeneratedByVersion, getToolVersionStatus, getConfiguredTools, getAllToolVersionStatus, } from './tool-detection.js';
export { getSkillTemplates, getCommandTemplates, getCommandContents, generateSkillContent, } from './skill-generation.js';
//# sourceMappingURL=index.js.map

@@ -9,3 +9,3 @@ /**

/**
* Skill template with directory name mapping.
* Skill template with directory name and workflow ID mapping.
*/

@@ -15,2 +15,3 @@ export interface SkillTemplateEntry {

dirName: string;
workflowId: string;
}

@@ -25,13 +26,19 @@ /**

/**
* Gets all skill templates with their directory names.
* Gets skill templates with their directory names, optionally filtered by workflow IDs.
*
* @param workflowFilter - If provided, only return templates whose workflowId is in this array
*/
export declare function getSkillTemplates(): SkillTemplateEntry[];
export declare function getSkillTemplates(workflowFilter?: readonly string[]): SkillTemplateEntry[];
/**
* Gets all command templates with their IDs.
* Gets command templates with their IDs, optionally filtered by workflow IDs.
*
* @param workflowFilter - If provided, only return templates whose id is in this array
*/
export declare function getCommandTemplates(): CommandTemplateEntry[];
export declare function getCommandTemplates(workflowFilter?: readonly string[]): CommandTemplateEntry[];
/**
* Converts command templates to CommandContent array.
* Converts command templates to CommandContent array, optionally filtered by workflow IDs.
*
* @param workflowFilter - If provided, only return contents whose id is in this array
*/
export declare function getCommandContents(): CommandContent[];
export declare function getCommandContents(workflowFilter?: readonly string[]): CommandContent[];
/**

@@ -38,0 +45,0 @@ * Generates skill file content with YAML frontmatter.

@@ -6,25 +6,34 @@ /**

*/
import { getExploreSkillTemplate, getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getBulkArchiveChangeSkillTemplate, getVerifyChangeSkillTemplate, getOnboardSkillTemplate, getOpsxExploreCommandTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate, getOpsxBulkArchiveCommandTemplate, getOpsxVerifyCommandTemplate, getOpsxOnboardCommandTemplate, } from '../templates/skill-templates.js';
import { getExploreSkillTemplate, getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getBulkArchiveChangeSkillTemplate, getVerifyChangeSkillTemplate, getOnboardSkillTemplate, getOpsxProposeSkillTemplate, getOpsxExploreCommandTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate, getOpsxBulkArchiveCommandTemplate, getOpsxVerifyCommandTemplate, getOpsxOnboardCommandTemplate, getOpsxProposeCommandTemplate, } from '../templates/skill-templates.js';
/**
* Gets all skill templates with their directory names.
* Gets skill templates with their directory names, optionally filtered by workflow IDs.
*
* @param workflowFilter - If provided, only return templates whose workflowId is in this array
*/
export function getSkillTemplates() {
return [
{ template: getExploreSkillTemplate(), dirName: 'openspec-explore' },
{ template: getNewChangeSkillTemplate(), dirName: 'openspec-new-change' },
{ template: getContinueChangeSkillTemplate(), dirName: 'openspec-continue-change' },
{ template: getApplyChangeSkillTemplate(), dirName: 'openspec-apply-change' },
{ template: getFfChangeSkillTemplate(), dirName: 'openspec-ff-change' },
{ template: getSyncSpecsSkillTemplate(), dirName: 'openspec-sync-specs' },
{ template: getArchiveChangeSkillTemplate(), dirName: 'openspec-archive-change' },
{ template: getBulkArchiveChangeSkillTemplate(), dirName: 'openspec-bulk-archive-change' },
{ template: getVerifyChangeSkillTemplate(), dirName: 'openspec-verify-change' },
{ template: getOnboardSkillTemplate(), dirName: 'openspec-onboard' },
export function getSkillTemplates(workflowFilter) {
const all = [
{ template: getExploreSkillTemplate(), dirName: 'openspec-explore', workflowId: 'explore' },
{ template: getNewChangeSkillTemplate(), dirName: 'openspec-new-change', workflowId: 'new' },
{ template: getContinueChangeSkillTemplate(), dirName: 'openspec-continue-change', workflowId: 'continue' },
{ template: getApplyChangeSkillTemplate(), dirName: 'openspec-apply-change', workflowId: 'apply' },
{ template: getFfChangeSkillTemplate(), dirName: 'openspec-ff-change', workflowId: 'ff' },
{ template: getSyncSpecsSkillTemplate(), dirName: 'openspec-sync-specs', workflowId: 'sync' },
{ template: getArchiveChangeSkillTemplate(), dirName: 'openspec-archive-change', workflowId: 'archive' },
{ template: getBulkArchiveChangeSkillTemplate(), dirName: 'openspec-bulk-archive-change', workflowId: 'bulk-archive' },
{ template: getVerifyChangeSkillTemplate(), dirName: 'openspec-verify-change', workflowId: 'verify' },
{ template: getOnboardSkillTemplate(), dirName: 'openspec-onboard', workflowId: 'onboard' },
{ template: getOpsxProposeSkillTemplate(), dirName: 'openspec-propose', workflowId: 'propose' },
];
if (!workflowFilter)
return all;
const filterSet = new Set(workflowFilter);
return all.filter(entry => filterSet.has(entry.workflowId));
}
/**
* Gets all command templates with their IDs.
* Gets command templates with their IDs, optionally filtered by workflow IDs.
*
* @param workflowFilter - If provided, only return templates whose id is in this array
*/
export function getCommandTemplates() {
return [
export function getCommandTemplates(workflowFilter) {
const all = [
{ template: getOpsxExploreCommandTemplate(), id: 'explore' },

@@ -40,9 +49,16 @@ { template: getOpsxNewCommandTemplate(), id: 'new' },

{ template: getOpsxOnboardCommandTemplate(), id: 'onboard' },
{ template: getOpsxProposeCommandTemplate(), id: 'propose' },
];
if (!workflowFilter)
return all;
const filterSet = new Set(workflowFilter);
return all.filter(entry => filterSet.has(entry.id));
}
/**
* Converts command templates to CommandContent array.
* Converts command templates to CommandContent array, optionally filtered by workflow IDs.
*
* @param workflowFilter - If provided, only return contents whose id is in this array
*/
export function getCommandContents() {
const commandTemplates = getCommandTemplates();
export function getCommandContents(workflowFilter) {
const commandTemplates = getCommandTemplates(workflowFilter);
return commandTemplates.map(({ template, id }) => ({

@@ -49,0 +65,0 @@ id,

@@ -9,5 +9,10 @@ /**

*/
export declare const SKILL_NAMES: readonly ["openspec-explore", "openspec-new-change", "openspec-continue-change", "openspec-apply-change", "openspec-ff-change", "openspec-sync-specs", "openspec-archive-change", "openspec-bulk-archive-change", "openspec-verify-change"];
export declare const SKILL_NAMES: readonly ["openspec-explore", "openspec-new-change", "openspec-continue-change", "openspec-apply-change", "openspec-ff-change", "openspec-sync-specs", "openspec-archive-change", "openspec-bulk-archive-change", "openspec-verify-change", "openspec-onboard", "openspec-propose"];
export type SkillName = (typeof SKILL_NAMES)[number];
/**
* IDs of command templates created by openspec init.
*/
export declare const COMMAND_IDS: readonly ["explore", "new", "continue", "apply", "ff", "sync", "archive", "bulk-archive", "verify", "onboard", "propose"];
export type CommandId = (typeof COMMAND_IDS)[number];
/**
* Status of skill configuration for a tool.

@@ -18,5 +23,5 @@ */

configured: boolean;
/** Whether all 9 skills are configured */
/** Whether all skills are configured */
fullyConfigured: boolean;
/** Number of skills currently configured (0-9) */
/** Number of skills currently configured */
skillCount: number;

@@ -23,0 +28,0 @@ }

@@ -22,4 +22,22 @@ /**

'openspec-verify-change',
'openspec-onboard',
'openspec-propose',
];
/**
* IDs of command templates created by openspec init.
*/
export const COMMAND_IDS = [
'explore',
'new',
'continue',
'apply',
'ff',
'sync',
'archive',
'bulk-archive',
'verify',
'onboard',
'propose',
];
/**
* Gets the list of tools with skillsDir configured.

@@ -26,0 +44,0 @@ */

@@ -70,3 +70,3 @@ /**

if (addedNames.has(name)) {
throw new Error(`${specName} 验证失败 - "新增需求"部分存在重复需求: "### 需求: ${add.name}"`);
throw new Error(`${specName} 验证失败 - "新增需求"部分存在重复需求: "### 需求: ${add.name}"`);
}

@@ -79,3 +79,3 @@ addedNames.add(name);

if (modifiedNames.has(name)) {
throw new Error(`${specName} 验证失败 - "修改需求"部分存在重复需求: "### 需求: ${mod.name}"`);
throw new Error(`${specName} 验证失败 - "修改需求"部分存在重复需求: "### 需求: ${mod.name}"`);
}

@@ -88,3 +88,3 @@ modifiedNames.add(name);

if (removedNamesSet.has(name)) {
throw new Error(`${specName} 验证失败 - "移除需求"部分存在重复需求: "### 需求: ${rem}"`);
throw new Error(`${specName} 验证失败 - "移除需求"部分存在重复需求: "### 需求: ${rem}"`);
}

@@ -99,6 +99,6 @@ removedNamesSet.add(name);

if (renamedFromSet.has(fromNorm)) {
throw new Error(`${specName} 验证失败 - "重命名需求"部分的 FROM 存在重复: "### 需求: ${from}"`);
throw new Error(`${specName} 验证失败 - "重命名需求"部分的 FROM 存在重复: "### 需求: ${from}"`);
}
if (renamedToSet.has(toNorm)) {
throw new Error(`${specName} 验证失败 - "重命名需求"部分的 TO 存在重复: "### 需求: ${to}"`);
throw new Error(`${specName} 验证失败 - "重命名需求"部分的 TO 存在重复: "### 需求: ${to}"`);
}

@@ -125,7 +125,7 @@ renamedFromSet.add(fromNorm);

if (modifiedNames.has(fromNorm)) {
throw new Error(`${specName} 验证失败 - 当存在重命名时,"修改需求"必须引用新标题 "### 需求: ${to}"`);
throw new Error(`${specName} 验证失败 - 当存在重命名时,"修改需求"必须引用新标题 "### 需求: ${to}"`);
}
// Detect ADDED colliding with a RENAMED TO
if (addedNames.has(toNorm)) {
throw new Error(`${specName} 验证失败 - "重命名需求"中的 TO 标题与"新增需求"冲突: "### 需求: ${to}"`);
throw new Error(`${specName} 验证失败 - "重命名需求"中的 TO 标题与"新增需求"冲突: "### 需求: ${to}"`);
}

@@ -135,3 +135,3 @@ }

const c = conflicts[0];
throw new Error(`${specName} 验证失败 - 需求在多个部分(${c.a} 和 ${c.b})同时出现: "### 需求: ${c.name}"`);
throw new Error(`${specName} 验证失败 - 需求在多个部分(${c.a} 和 ${c.b})同时出现: "### 需求: ${c.name}"`);
}

@@ -174,6 +174,6 @@ const hasAnyDelta = plan.added.length + plan.modified.length + plan.removed.length + plan.renamed.length > 0;

if (!nameToBlock.has(from)) {
throw new Error(`${specName} 重命名失败:未找到源需求 "### 需求: ${r.from}"`);
throw new Error(`${specName} 重命名失败:未找到源需求 "### 需求: ${r.from}"`);
}
if (nameToBlock.has(to)) {
throw new Error(`${specName} 重命名失败:目标需求已存在 "### 需求: ${r.to}"`);
throw new Error(`${specName} 重命名失败:目标需求已存在 "### 需求: ${r.to}"`);
}

@@ -199,3 +199,3 @@ const block = nameToBlock.get(from);

if (!isNewSpec) {
throw new Error(`${specName} 移除失败:未找到需求 "### 需求: ${name}"`);
throw new Error(`${specName} 移除失败:未找到需求 "### 需求: ${name}"`);
}

@@ -211,3 +211,3 @@ // Skip removal for new specs (already warned above)

if (!nameToBlock.has(key)) {
throw new Error(`${specName} 修改失败:未找到需求 "### 需求: ${mod.name}"`);
throw new Error(`${specName} 修改失败:未找到需求 "### 需求: ${mod.name}"`);
}

@@ -217,3 +217,3 @@ // Replace block with provided raw (ensure header line matches key)

if (!modHeaderMatch || normalizeRequirementName(modHeaderMatch[1]) !== key) {
throw new Error(`${specName} 修改失败:内容中的标题不匹配 "### 需求: ${mod.name}"`);
throw new Error(`${specName} 修改失败:内容中的标题不匹配 "### 需求: ${mod.name}"`);
}

@@ -226,3 +226,3 @@ nameToBlock.set(key, mod);

if (nameToBlock.has(key)) {
throw new Error(`${specName} 新增失败:需求已存在 "### 需求: ${add.name}"`);
throw new Error(`${specName} 新增失败:需求已存在 "### 需求: ${add.name}"`);
}

@@ -229,0 +229,0 @@ nameToBlock.set(key, add);

@@ -7,3 +7,3 @@ /**

*/
export { getExploreSkillTemplate, getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getBulkArchiveChangeSkillTemplate, getVerifyChangeSkillTemplate, getOpsxExploreCommandTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate, getOpsxBulkArchiveCommandTemplate, getOpsxVerifyCommandTemplate, } from './skill-templates.js';
export * from './skill-templates.js';
//# sourceMappingURL=index.d.ts.map

@@ -7,4 +7,4 @@ /**

*/
// Re-export skill templates for convenience
export { getExploreSkillTemplate, getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getBulkArchiveChangeSkillTemplate, getVerifyChangeSkillTemplate, getOpsxExploreCommandTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate, getOpsxBulkArchiveCommandTemplate, getOpsxVerifyCommandTemplate, } from './skill-templates.js';
// Re-export all skill templates and related types through the compatibility facade.
export * from './skill-templates.js';
//# sourceMappingURL=index.js.map
/**
* Agent Skill Templates
*
* Templates for generating Agent Skills compatible with:
* - Claude Code
* - Cursor (Settings → Rules → Import Settings)
* - Windsurf
* - Other Agent Skills-compatible editors
* Compatibility facade that re-exports split workflow template modules.
*/
export interface SkillTemplate {
name: string;
description: string;
instructions: string;
license?: string;
compatibility?: string;
metadata?: Record<string, string>;
}
/**
* Template for openspec-explore skill
* Explore mode - adaptive thinking partner for exploring ideas and problems
*/
export declare function getExploreSkillTemplate(): SkillTemplate;
/**
* Template for openspec-new-change skill
* Based on /opsx:new command
*/
export declare function getNewChangeSkillTemplate(): SkillTemplate;
/**
* Template for openspec-continue-change skill
* Based on /opsx:continue command
*/
export declare function getContinueChangeSkillTemplate(): SkillTemplate;
/**
* Template for openspec-apply-change skill
* For implementing tasks from a completed (or in-progress) change
*/
export declare function getApplyChangeSkillTemplate(): SkillTemplate;
/**
* Template for openspec-ff-change skill
* Fast-forward through artifact creation
*/
export declare function getFfChangeSkillTemplate(): SkillTemplate;
/**
* Template for openspec-sync-specs skill
* For syncing delta specs from a change to main specs (agent-driven)
*/
export declare function getSyncSpecsSkillTemplate(): SkillTemplate;
/**
* Template for openspec-onboard skill
* Guided onboarding through the complete OpenSpec workflow
*/
export declare function getOnboardSkillTemplate(): SkillTemplate;
export interface CommandTemplate {
name: string;
description: string;
category: string;
tags: string[];
content: string;
}
/**
* Template for /opsx:explore slash command
* Explore mode - adaptive thinking partner
*/
export declare function getOpsxExploreCommandTemplate(): CommandTemplate;
/**
* Template for /opsx:new slash command
*/
export declare function getOpsxNewCommandTemplate(): CommandTemplate;
/**
* Template for /opsx:continue slash command
*/
export declare function getOpsxContinueCommandTemplate(): CommandTemplate;
/**
* Template for /opsx:apply slash command
*/
export declare function getOpsxApplyCommandTemplate(): CommandTemplate;
/**
* Template for /opsx:ff slash command
*/
export declare function getOpsxFfCommandTemplate(): CommandTemplate;
/**
* Template for openspec-archive-change skill
* For archiving completed changes in the experimental workflow
*/
export declare function getArchiveChangeSkillTemplate(): SkillTemplate;
/**
* Template for openspec-bulk-archive-change skill
* For archiving multiple completed changes at once
*/
export declare function getBulkArchiveChangeSkillTemplate(): SkillTemplate;
/**
* Template for /opsx:sync slash command
*/
export declare function getOpsxSyncCommandTemplate(): CommandTemplate;
/**
* Template for openspec-verify-change skill
* For verifying implementation matches change artifacts before archiving
*/
export declare function getVerifyChangeSkillTemplate(): SkillTemplate;
/**
* Template for /opsx:archive slash command
*/
export declare function getOpsxArchiveCommandTemplate(): CommandTemplate;
/**
* Template for /opsx:onboard slash command
* Guided onboarding through the complete OpenSpec workflow
*/
export declare function getOpsxOnboardCommandTemplate(): CommandTemplate;
/**
* Template for /opsx:bulk-archive slash command
*/
export declare function getOpsxBulkArchiveCommandTemplate(): CommandTemplate;
/**
* Template for /opsx:verify slash command
*/
export declare function getOpsxVerifyCommandTemplate(): CommandTemplate;
/**
* Template for feedback skill
* For collecting and submitting user feedback with context enrichment
*/
export declare function getFeedbackSkillTemplate(): SkillTemplate;
export type { SkillTemplate, CommandTemplate } from './types.js';
export { getExploreSkillTemplate, getOpsxExploreCommandTemplate } from './workflows/explore.js';
export { getNewChangeSkillTemplate, getOpsxNewCommandTemplate } from './workflows/new-change.js';
export { getContinueChangeSkillTemplate, getOpsxContinueCommandTemplate } from './workflows/continue-change.js';
export { getApplyChangeSkillTemplate, getOpsxApplyCommandTemplate } from './workflows/apply-change.js';
export { getFfChangeSkillTemplate, getOpsxFfCommandTemplate } from './workflows/ff-change.js';
export { getSyncSpecsSkillTemplate, getOpsxSyncCommandTemplate } from './workflows/sync-specs.js';
export { getArchiveChangeSkillTemplate, getOpsxArchiveCommandTemplate } from './workflows/archive-change.js';
export { getBulkArchiveChangeSkillTemplate, getOpsxBulkArchiveCommandTemplate } from './workflows/bulk-archive-change.js';
export { getVerifyChangeSkillTemplate, getOpsxVerifyCommandTemplate } from './workflows/verify-change.js';
export { getOnboardSkillTemplate, getOpsxOnboardCommandTemplate } from './workflows/onboard.js';
export { getOpsxProposeSkillTemplate, getOpsxProposeCommandTemplate } from './workflows/propose.js';
export { getFeedbackSkillTemplate } from './workflows/feedback.js';
//# sourceMappingURL=skill-templates.d.ts.map

@@ -5,3 +5,3 @@ /**

* Refreshes OpenSpec skills and commands for configured tools.
* Supports smart update detection to skip updates when already current.
* Supports profile-aware updates, delivery changes, migration, and smart update detection.
*/

@@ -15,2 +15,9 @@ /**

}
/**
* Scans installed workflow artifacts (skills and managed commands) across all configured tools.
* Returns the union of detected workflow IDs that match ALL_WORKFLOWS.
*
* Wrapper around the shared migration module's scanInstalledWorkflows that accepts tool IDs.
*/
export declare function scanInstalledWorkflows(projectPath: string, toolIds: string[]): string[];
export declare class UpdateCommand {

@@ -29,2 +36,30 @@ private readonly force;

/**
* Detects new tool directories that aren't currently configured and displays a hint.
*/
private detectNewTools;
/**
* Displays a note about extra workflows installed that aren't in the current profile.
*/
private displayExtraWorkflowsNote;
/**
* Removes skill directories for workflows when delivery changed to commands-only.
* Returns the number of directories removed.
*/
private removeSkillDirs;
/**
* Removes skill directories for workflows that are no longer selected in the active profile.
* Returns the number of directories removed.
*/
private removeUnselectedSkillDirs;
/**
* Removes command files for workflows when delivery changed to skills-only.
* Returns the number of files removed.
*/
private removeCommandFiles;
/**
* Removes command files for workflows that are no longer selected in the active profile.
* Returns the number of files removed.
*/
private removeUnselectedCommandFiles;
/**
* Detect and handle legacy OpenSpec artifacts.

@@ -31,0 +66,0 @@ * Unlike init, update warns but continues if legacy files found in non-interactive mode.

@@ -5,3 +5,3 @@ /**

* Refreshes OpenSpec skills and commands for configured tools.
* Supports smart update detection to skip updates when already current.
* Supports profile-aware updates, delivery changes, migration, and smart update detection.
*/

@@ -11,2 +11,3 @@ import path from 'path';

import ora from 'ora';
import * as fs from 'fs';
import { createRequire } from 'module';

@@ -17,7 +18,24 @@ import { FileSystemUtils } from '../utils/file-system.js';

import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
import { getConfiguredTools, getAllToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, } from './shared/index.js';
import { getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, } from './shared/index.js';
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDetectionSummary, getToolsFromLegacyArtifacts, } from './legacy-cleanup.js';
import { isInteractive } from '../utils/interactive.js';
import { getGlobalConfig } from './global-config.js';
import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js';
import { getAvailableTools } from './available-tools.js';
import { WORKFLOW_TO_SKILL_DIR, getCommandConfiguredTools, getConfiguredToolsForProfileSync, getToolsNeedingProfileSync, } from './profile-sync-drift.js';
import { scanInstalledWorkflows as scanInstalledWorkflowsShared, migrateIfNeeded as migrateIfNeededShared, } from './migration.js';
const require = createRequire(import.meta.url);
const { version: OPENSPEC_VERSION } = require('../../package.json');
/**
* Scans installed workflow artifacts (skills and managed commands) across all configured tools.
* Returns the union of detected workflow IDs that match ALL_WORKFLOWS.
*
* Wrapper around the shared migration module's scanInstalledWorkflows that accepts tool IDs.
*/
export function scanInstalledWorkflows(projectPath, toolIds) {
const tools = toolIds
.map((id) => AI_TOOLS.find((t) => t.value === id))
.filter((t) => t != null);
return scanInstalledWorkflowsShared(projectPath, tools);
}
export class UpdateCommand {

@@ -35,6 +53,18 @@ force;

}
// 2. Detect and handle legacy artifacts + upgrade legacy tools to new skills
const newlyConfiguredTools = await this.handleLegacyCleanup(resolvedProjectPath);
// 3. Find configured tools
const configuredTools = getConfiguredTools(resolvedProjectPath);
// 2. Perform one-time migration if needed before any legacy upgrade generation.
// Use detected tool directories to preserve existing opsx skills/commands.
const detectedTools = getAvailableTools(resolvedProjectPath);
migrateIfNeededShared(resolvedProjectPath, detectedTools);
// 3. Read global config for profile/delivery
const globalConfig = getGlobalConfig();
const profile = globalConfig.profile ?? 'core';
const delivery = globalConfig.delivery ?? 'both';
const profileWorkflows = getProfileWorkflows(profile, globalConfig.workflows);
const desiredWorkflows = profileWorkflows.filter((workflow) => ALL_WORKFLOWS.includes(workflow));
const shouldGenerateSkills = delivery !== 'commands';
const shouldGenerateCommands = delivery !== 'skills';
// 4. Detect and handle legacy artifacts + upgrade legacy tools using effective config
const newlyConfiguredTools = await this.handleLegacyCleanup(resolvedProjectPath, desiredWorkflows, delivery);
// 5. Find configured tools
const configuredTools = getConfiguredToolsForProfileSync(resolvedProjectPath);
if (configuredTools.length === 0 && newlyConfiguredTools.length === 0) {

@@ -45,13 +75,32 @@ console.log(chalk.yellow('未找到配置的工具。'));

}
// 4. Check version status for all configured tools
const toolStatuses = getAllToolVersionStatus(resolvedProjectPath, OPENSPEC_VERSION);
// 5. Smart update detection
const toolsNeedingUpdate = toolStatuses.filter((s) => s.needsUpdate);
const toolsUpToDate = toolStatuses.filter((s) => !s.needsUpdate);
if (!this.force && toolsNeedingUpdate.length === 0) {
// 6. Check version status for all configured tools
const commandConfiguredTools = getCommandConfiguredTools(resolvedProjectPath);
const commandConfiguredSet = new Set(commandConfiguredTools);
const toolStatuses = configuredTools.map((toolId) => {
const status = getToolVersionStatus(resolvedProjectPath, toolId, OPENSPEC_VERSION);
if (!status.configured && commandConfiguredSet.has(toolId)) {
return { ...status, configured: true };
}
return status;
});
const statusByTool = new Map(toolStatuses.map((status) => [status.toolId, status]));
// 7. Smart update detection
const toolsNeedingVersionUpdate = toolStatuses
.filter((s) => s.needsUpdate)
.map((s) => s.toolId);
const toolsNeedingConfigSync = getToolsNeedingProfileSync(resolvedProjectPath, desiredWorkflows, delivery, configuredTools);
const toolsToUpdateSet = new Set([
...toolsNeedingVersionUpdate,
...toolsNeedingConfigSync,
]);
const toolsUpToDate = toolStatuses.filter((s) => !toolsToUpdateSet.has(s.toolId));
if (!this.force && toolsToUpdateSet.size === 0) {
// All tools are up to date
this.displayUpToDateMessage(toolStatuses);
// Still check for new tool directories and extra workflows
this.detectNewTools(resolvedProjectPath, configuredTools);
this.displayExtraWorkflowsNote(resolvedProjectPath, configuredTools, desiredWorkflows);
return;
}
// 6. Display update plan
// 8. Display update plan
if (this.force) {

@@ -61,12 +110,16 @@ console.log(`强制更新 ${configuredTools.length} 个工具: ${configuredTools.join(', ')}`);

else {
this.displayUpdatePlan(toolsNeedingUpdate, toolsUpToDate);
this.displayUpdatePlan([...toolsToUpdateSet], statusByTool, toolsUpToDate);
}
console.log();
// 7. Prepare templates
const skillTemplates = getSkillTemplates();
const commandContents = getCommandContents();
// 8. Update tools (all if force, otherwise only those needing update)
const toolsToUpdate = this.force ? configuredTools : toolsNeedingUpdate.map((s) => s.toolId);
// 9. Determine what to generate based on delivery
const skillTemplates = shouldGenerateSkills ? getSkillTemplates(desiredWorkflows) : [];
const commandContents = shouldGenerateCommands ? getCommandContents(desiredWorkflows) : [];
// 10. Update tools (all if force, otherwise only those needing update)
const toolsToUpdate = this.force ? configuredTools : [...toolsToUpdateSet];
const updatedTools = [];
const failedTools = [];
let removedCommandCount = 0;
let removedSkillCount = 0;
let removedDeselectedCommandCount = 0;
let removedDeselectedSkillCount = 0;
for (const toolId of toolsToUpdate) {

@@ -79,20 +132,34 @@ const tool = AI_TOOLS.find((t) => t.value === toolId);

const skillsDir = path.join(resolvedProjectPath, tool.skillsDir, 'skills');
// Update skill files
for (const { template, dirName } of skillTemplates) {
const skillDir = path.join(skillsDir, dirName);
const skillFile = path.join(skillDir, 'SKILL.md');
// Use hyphen-based command references for OpenCode
const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
await FileSystemUtils.writeFile(skillFile, skillContent);
// Generate skill files if delivery includes skills
if (shouldGenerateSkills) {
for (const { template, dirName } of skillTemplates) {
const skillDir = path.join(skillsDir, dirName);
const skillFile = path.join(skillDir, 'SKILL.md');
// Use hyphen-based command references for OpenCode
const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
await FileSystemUtils.writeFile(skillFile, skillContent);
}
removedDeselectedSkillCount += await this.removeUnselectedSkillDirs(skillsDir, desiredWorkflows);
}
// Update commands
const adapter = CommandAdapterRegistry.get(tool.value);
if (adapter) {
const generatedCommands = generateCommands(commandContents, adapter);
for (const cmd of generatedCommands) {
const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(resolvedProjectPath, cmd.path);
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
// Delete skill directories if delivery is commands-only
if (!shouldGenerateSkills) {
removedSkillCount += await this.removeSkillDirs(skillsDir);
}
// Generate commands if delivery includes commands
if (shouldGenerateCommands) {
const adapter = CommandAdapterRegistry.get(tool.value);
if (adapter) {
const generatedCommands = generateCommands(commandContents, adapter);
for (const cmd of generatedCommands) {
const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(resolvedProjectPath, cmd.path);
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
}
removedDeselectedCommandCount += await this.removeUnselectedCommandFiles(resolvedProjectPath, toolId, desiredWorkflows);
}
}
// Delete command files if delivery is skills-only
if (!shouldGenerateCommands) {
removedCommandCount += await this.removeCommandFiles(resolvedProjectPath, toolId);
}
spinner.succeed(`已更新 ${tool.name}`);

@@ -109,3 +176,3 @@ updatedTools.push(tool.name);

}
// 9. Summary
// 11. Summary
console.log();

@@ -118,3 +185,15 @@ if (updatedTools.length > 0) {

}
// 10. Show onboarding message for newly configured tools from legacy upgrade
if (removedCommandCount > 0) {
console.log(chalk.dim(`已删除: ${removedCommandCount} 个命令文件(交付模式: skills)`));
}
if (removedSkillCount > 0) {
console.log(chalk.dim(`已删除: ${removedSkillCount} 个技能目录(交付模式: commands)`));
}
if (removedDeselectedCommandCount > 0) {
console.log(chalk.dim(`已删除: ${removedDeselectedCommandCount} 个命令文件(已取消选择的工作流)`));
}
if (removedDeselectedSkillCount > 0) {
console.log(chalk.dim(`已删除: ${removedDeselectedSkillCount} 个技能目录(已取消选择的工作流)`));
}
// 12. Show onboarding message for newly configured tools from legacy upgrade
if (newlyConfiguredTools.length > 0) {

@@ -129,2 +208,12 @@ console.log();

}
const configuredAndNewTools = [...new Set([...configuredTools, ...newlyConfiguredTools])];
// 13. Detect new tool directories not currently configured
this.detectNewTools(resolvedProjectPath, configuredAndNewTools);
// 14. Display note about extra workflows not in profile
this.displayExtraWorkflowsNote(resolvedProjectPath, configuredAndNewTools, desiredWorkflows);
// 15. List affected tools
if (updatedTools.length > 0) {
const toolDisplayNames = updatedTools;
console.log(chalk.dim(`工具: ${toolDisplayNames.join(', ')}`));
}
console.log();

@@ -141,3 +230,3 @@ console.log(chalk.dim('重启 IDE 以使更改生效。'));

console.log();
console.log(chalk.dim('使用 --force 强制刷新 skills。'));
console.log(chalk.dim('使用 --force 强制刷新文件.'));
}

@@ -147,8 +236,12 @@ /**

*/
displayUpdatePlan(needingUpdate, upToDate) {
const updates = needingUpdate.map((s) => {
const fromVersion = s.generatedByVersion ?? 'unknown';
return `${s.toolId} (${fromVersion} → ${OPENSPEC_VERSION})`;
displayUpdatePlan(toolsToUpdate, statusByTool, upToDate) {
const updates = toolsToUpdate.map((toolId) => {
const status = statusByTool.get(toolId);
if (status?.needsUpdate) {
const fromVersion = status.generatedByVersion ?? 'unknown';
return `${status.toolId} (${fromVersion} → ${OPENSPEC_VERSION})`;
}
return `${toolId} (配置同步)`;
});
console.log(`正在更新 ${needingUpdate.length} 个工具: ${updates.join(', ')}`);
console.log(`正在更新 ${toolsToUpdate.length} 个工具: ${updates.join(', ')}`);
if (upToDate.length > 0) {

@@ -160,2 +253,128 @@ const upToDateNames = upToDate.map((s) => s.toolId);

/**
* Detects new tool directories that aren't currently configured and displays a hint.
*/
detectNewTools(projectPath, configuredTools) {
const availableTools = getAvailableTools(projectPath);
const configuredSet = new Set(configuredTools);
const newTools = availableTools.filter((t) => !configuredSet.has(t.value));
if (newTools.length > 0) {
const newToolNames = newTools.map((tool) => tool.name);
const isSingleTool = newToolNames.length === 1;
const toolNoun = isSingleTool ? '工具' : '工具';
console.log();
console.log(chalk.yellow(`检测到新的${toolNoun}:${newToolNames.join(', ')}。运行 'openspec-cn init' 以添加${isSingleTool ? '它' : '它们'}。`));
}
}
/**
* Displays a note about extra workflows installed that aren't in the current profile.
*/
displayExtraWorkflowsNote(projectPath, configuredTools, profileWorkflows) {
const installedWorkflows = scanInstalledWorkflows(projectPath, configuredTools);
const profileSet = new Set(profileWorkflows);
const extraWorkflows = installedWorkflows.filter((w) => !profileSet.has(w));
if (extraWorkflows.length > 0) {
console.log(chalk.dim(`注意:有 ${extraWorkflows.length} 个额外工作流不在当前配置文件中(使用 \`openspec-cn config profile\` 管理)`));
}
}
/**
* Removes skill directories for workflows when delivery changed to commands-only.
* Returns the number of directories removed.
*/
async removeSkillDirs(skillsDir) {
let removed = 0;
for (const workflow of ALL_WORKFLOWS) {
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
if (!dirName)
continue;
const skillDir = path.join(skillsDir, dirName);
try {
if (fs.existsSync(skillDir)) {
await fs.promises.rm(skillDir, { recursive: true, force: true });
removed++;
}
}
catch {
// Ignore errors
}
}
return removed;
}
/**
* Removes skill directories for workflows that are no longer selected in the active profile.
* Returns the number of directories removed.
*/
async removeUnselectedSkillDirs(skillsDir, desiredWorkflows) {
const desiredSet = new Set(desiredWorkflows);
let removed = 0;
for (const workflow of ALL_WORKFLOWS) {
if (desiredSet.has(workflow))
continue;
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
if (!dirName)
continue;
const skillDir = path.join(skillsDir, dirName);
try {
if (fs.existsSync(skillDir)) {
await fs.promises.rm(skillDir, { recursive: true, force: true });
removed++;
}
}
catch {
// Ignore errors
}
}
return removed;
}
/**
* Removes command files for workflows when delivery changed to skills-only.
* Returns the number of files removed.
*/
async removeCommandFiles(projectPath, toolId) {
let removed = 0;
const adapter = CommandAdapterRegistry.get(toolId);
if (!adapter)
return 0;
for (const workflow of ALL_WORKFLOWS) {
const cmdPath = adapter.getFilePath(workflow);
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
try {
if (fs.existsSync(fullPath)) {
await fs.promises.unlink(fullPath);
removed++;
}
}
catch {
// Ignore errors
}
}
return removed;
}
/**
* Removes command files for workflows that are no longer selected in the active profile.
* Returns the number of files removed.
*/
async removeUnselectedCommandFiles(projectPath, toolId, desiredWorkflows) {
let removed = 0;
const adapter = CommandAdapterRegistry.get(toolId);
if (!adapter)
return 0;
const desiredSet = new Set(desiredWorkflows);
for (const workflow of ALL_WORKFLOWS) {
if (desiredSet.has(workflow))
continue;
const cmdPath = adapter.getFilePath(workflow);
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
try {
if (fs.existsSync(fullPath)) {
await fs.promises.unlink(fullPath);
removed++;
}
}
catch {
// Ignore errors
}
}
return removed;
}
/**
* Detect and handle legacy OpenSpec artifacts.

@@ -165,3 +384,3 @@ * Unlike init, update warns but continues if legacy files found in non-interactive mode.

*/
async handleLegacyCleanup(projectPath) {
async handleLegacyCleanup(projectPath, desiredWorkflows, delivery) {
// Detect legacy artifacts

@@ -181,3 +400,3 @@ const detection = await detectLegacyArtifacts(projectPath);

// Then upgrade legacy tools to new skills
return this.upgradeLegacyTools(projectPath, detection, canPrompt);
return this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
}

@@ -187,3 +406,3 @@ if (!canPrompt) {

// (Unlike init, update doesn't abort - user may just want to update skills)
console.log(chalk.yellow('⚠ Run with --force to auto-cleanup legacy files, or run interactively.'));
console.log(chalk.yellow('⚠ 使用 --force 自动清理旧文件,或在交互模式下运行。'));
console.log();

@@ -195,3 +414,3 @@ return [];

const shouldCleanup = await confirm({
message: 'Upgrade and clean up legacy files?',
message: '升级并清理旧版文件?',
default: true,

@@ -202,6 +421,6 @@ });

// Then upgrade legacy tools to new skills
return this.upgradeLegacyTools(projectPath, detection, canPrompt);
return this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
}
else {
console.log(chalk.dim('Skipping legacy cleanup. Continuing with skill update...'));
console.log(chalk.dim('跳过旧版清理。继续更新技能...'));
console.log();

@@ -215,5 +434,5 @@ return [];

async performLegacyCleanup(projectPath, detection) {
const spinner = ora('Cleaning up legacy files...').start();
const spinner = ora('正在清理旧版文件...').start();
const result = await cleanupLegacyArtifacts(projectPath, detection);
spinner.succeed('Legacy files cleaned up');
spinner.succeed('旧版文件已清理完成');
const summary = formatCleanupSummary(result);

@@ -230,3 +449,3 @@ if (summary) {

*/
async upgradeLegacyTools(projectPath, detection, canPrompt) {
async upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery) {
// Get tools that had legacy artifacts

@@ -238,3 +457,3 @@ const legacyTools = getToolsFromLegacyArtifacts(detection);

// Get currently configured tools
const configuredTools = getConfiguredTools(projectPath);
const configuredTools = getConfiguredToolsForProfileSync(projectPath);
const configuredSet = new Set(configuredTools);

@@ -263,3 +482,3 @@ // Filter to tools that aren't already configured

selectedTools = validUnconfiguredTools;
console.log(`Setting up skills for: ${selectedTools.join(', ')}`);
console.log(`正在为以下工具设置技能:${selectedTools.join(', ')}`);
}

@@ -279,3 +498,3 @@ else {

selectedTools = await searchableMultiSelect({
message: 'Select tools to set up with the new skill system:',
message: '选择要使用新技能系统设置的工具:',
pageSize: 15,

@@ -286,3 +505,3 @@ choices: sortedChoices,

if (selectedTools.length === 0) {
console.log(chalk.dim('Skipping tool setup.'));
console.log(chalk.dim('跳过工具设置。'));
console.log();

@@ -292,6 +511,8 @@ return [];

}
// Create skills for selected tools
// Create skills/commands for selected tools using effective profile+delivery.
const newlyConfigured = [];
const skillTemplates = getSkillTemplates();
const commandContents = getCommandContents();
const shouldGenerateSkills = delivery !== 'commands';
const shouldGenerateCommands = delivery !== 'skills';
const skillTemplates = shouldGenerateSkills ? getSkillTemplates(desiredWorkflows) : [];
const commandContents = shouldGenerateCommands ? getCommandContents(desiredWorkflows) : [];
for (const toolId of selectedTools) {

@@ -301,28 +522,32 @@ const tool = AI_TOOLS.find((t) => t.value === toolId);

continue;
const spinner = ora(`Setting up ${tool.name}...`).start();
const spinner = ora(`正在设置 ${tool.name}...`).start();
try {
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
// Create skill files
for (const { template, dirName } of skillTemplates) {
const skillDir = path.join(skillsDir, dirName);
const skillFile = path.join(skillDir, 'SKILL.md');
// Use hyphen-based command references for OpenCode
const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
await FileSystemUtils.writeFile(skillFile, skillContent);
// Create skill files when delivery includes skills
if (shouldGenerateSkills) {
for (const { template, dirName } of skillTemplates) {
const skillDir = path.join(skillsDir, dirName);
const skillFile = path.join(skillDir, 'SKILL.md');
// Use hyphen-based command references for OpenCode
const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
await FileSystemUtils.writeFile(skillFile, skillContent);
}
}
// Create commands
const adapter = CommandAdapterRegistry.get(tool.value);
if (adapter) {
const generatedCommands = generateCommands(commandContents, adapter);
for (const cmd of generatedCommands) {
const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path);
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
// Create commands when delivery includes commands
if (shouldGenerateCommands) {
const adapter = CommandAdapterRegistry.get(tool.value);
if (adapter) {
const generatedCommands = generateCommands(commandContents, adapter);
for (const cmd of generatedCommands) {
const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path);
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
}
}
}
spinner.succeed(`Setup complete for ${tool.name}`);
spinner.succeed(`${tool.name} 设置完成`);
newlyConfigured.push(toolId);
}
catch (error) {
spinner.fail(`Failed to set up ${tool.name}`);
spinner.fail(`${tool.name} 设置失败`);
console.log(chalk.red(` ${error instanceof Error ? error.message : String(error)}`));

@@ -329,0 +554,0 @@ }

@@ -29,7 +29,7 @@ /**

readonly DELTA_MISSING_REQUIREMENTS: "增量应包含需求";
readonly GUIDE_NO_DELTAS: "未找到增量。确保您的变更在specs/目录下有功能文件夹(例如specs/http-server/spec.md),其中包含使用增量标题(## 新增需求/修改需求/移除需求/重命名需求)的.md文件,并且每个需求至少包含一个\"#### 场景:\"块。提示:运行\"openspec-cn change show <change-id> --json --deltas-only\"来检查解析的增量。";
readonly GUIDE_MISSING_SPEC_SECTIONS: "缺少必需部分。预期标题:\"## 目的\"和\"## 需求\"。示例:\n## 目的\n[简要目的]\n\n## 需求\n### 需求:清晰的需求陈述\n用户应当...\n\n#### 场景:描述性名称\n- **当** ...\n- **那么** ...";
readonly GUIDE_NO_DELTAS: "未找到增量。确保您的变更在specs/目录下有功能文件夹(例如specs/http-server/spec.md),其中包含使用增量标题(## 新增需求/修改需求/移除需求/重命名需求)的.md文件,并且每个需求至少包含一个\"#### 场景:\"块。提示:运行\"openspec-cn change show <change-id> --json --deltas-only\"来检查解析的增量。";
readonly GUIDE_MISSING_SPEC_SECTIONS: "缺少必需部分。预期标题:\"## 目的\"和\"## 需求\"。示例:\n## 目的\n[简要目的]\n\n## 需求\n### 需求:清晰的需求陈述\n用户应当...\n\n#### 场景:描述性名称\n- **当** ...\n- **那么** ...";
readonly GUIDE_MISSING_CHANGE_SECTIONS: "缺少必需部分。预期标题:\"## 为什么\"和\"## 变更内容\"。确保在specs/中使用增量标题记录增量。";
readonly GUIDE_SCENARIO_FORMAT: "场景必须使用四级标题。将项目符号列表转换为:\n#### 场景:简短名称\n- **当** ...\n- **那么** ...\n- **并且** ...";
readonly GUIDE_SCENARIO_FORMAT: "场景必须使用四级标题。将项目符号列表转换为:\n#### 场景:简短名称\n- **当** ...\n- **那么** ...\n- **并且** ...";
};
//# sourceMappingURL=constants.d.ts.map

@@ -35,7 +35,7 @@ /**

// Guidance snippets (appended to primary messages for remediation)
GUIDE_NO_DELTAS: '未找到增量。确保您的变更在specs/目录下有功能文件夹(例如specs/http-server/spec.md),其中包含使用增量标题(## 新增需求/修改需求/移除需求/重命名需求)的.md文件,并且每个需求至少包含一个"#### 场景:"块。提示:运行"openspec-cn change show <change-id> --json --deltas-only"来检查解析的增量。',
GUIDE_MISSING_SPEC_SECTIONS: '缺少必需部分。预期标题:"## 目的"和"## 需求"。示例:\n## 目的\n[简要目的]\n\n## 需求\n### 需求:清晰的需求陈述\n用户应当...\n\n#### 场景:描述性名称\n- **当** ...\n- **那么** ...',
GUIDE_NO_DELTAS: '未找到增量。确保您的变更在specs/目录下有功能文件夹(例如specs/http-server/spec.md),其中包含使用增量标题(## 新增需求/修改需求/移除需求/重命名需求)的.md文件,并且每个需求至少包含一个"#### 场景:"块。提示:运行"openspec-cn change show <change-id> --json --deltas-only"来检查解析的增量。',
GUIDE_MISSING_SPEC_SECTIONS: '缺少必需部分。预期标题:"## 目的"和"## 需求"。示例:\n## 目的\n[简要目的]\n\n## 需求\n### 需求:清晰的需求陈述\n用户应当...\n\n#### 场景:描述性名称\n- **当** ...\n- **那么** ...',
GUIDE_MISSING_CHANGE_SECTIONS: '缺少必需部分。预期标题:"## 为什么"和"## 变更内容"。确保在specs/中使用增量标题记录增量。',
GUIDE_SCENARIO_FORMAT: '场景必须使用四级标题。将项目符号列表转换为:\n#### 场景:简短名称\n- **当** ...\n- **那么** ...\n- **并且** ...',
GUIDE_SCENARIO_FORMAT: '场景必须使用四级标题。将项目符号列表转换为:\n#### 场景:简短名称\n- **当** ...\n- **那么** ...\n- **并且** ...',
};
//# sourceMappingURL=constants.js.map

@@ -243,3 +243,3 @@ import { readFileSync, promises as fs } from 'fs';

path: specPath,
message: `找到了增量部分 ${this.formatSectionList(sections)},但未解析到任何需求条目。请确保每个部分至少包含一个 "### 需求:" 块(移除需求部分可以使用项目符号列表语法)。`,
message: `找到了增量部分 ${this.formatSectionList(sections)},但未解析到任何需求条目。请确保每个部分至少包含一个 "### 需求:" 块(移除需求部分可以使用项目符号列表语法)。`,
});

@@ -246,0 +246,0 @@ }

@@ -6,2 +6,3 @@ interface Choice {

configured?: boolean;
detected?: boolean;
configuredLabel?: string;

@@ -22,5 +23,5 @@ preSelected?: boolean;

* - ↑↓ to navigate
* - Enter to add highlighted item
* - Space to toggle highlighted item selection
* - Backspace to remove last selected item (or delete search char)
* - Tab to confirm selections
* - Enter to confirm selections
*/

@@ -27,0 +28,0 @@ export declare function searchableMultiSelect(config: Config): Promise<string[]>;

@@ -29,4 +29,4 @@ import chalk from 'chalk';

return;
// Tab to confirm
if (key.name === 'tab') {
// Enter to confirm/submit
if (isEnterKey(key)) {
if (validate) {

@@ -43,9 +43,12 @@ const result = validate(selectedValues);

}
// Enter to add item
if (isEnterKey(key)) {
// Space to toggle selection
if (key.name === 'space') {
const choice = filteredChoices[cursor];
if (choice && !selectedSet.has(choice.value)) {
setSelectedValues([...selectedValues, choice.value]);
setSearchText('');
setCursor(0);
if (choice) {
if (selectedSet.has(choice.value)) {
setSelectedValues(selectedValues.filter(v => v !== choice.value));
}
else {
setSelectedValues([...selectedValues, choice.value]);
}
}

@@ -100,3 +103,3 @@ return;

// Instructions
lines.push(` ${chalk.cyan('↑↓')} 导航 • ${chalk.cyan('Enter')} 添加 • ${chalk.cyan('Backspace')} 删除 • ${chalk.cyan('Tab')} 确认`);
lines.push(` ${chalk.cyan('↑↓')} 导航 • ${chalk.cyan('Space')} 添加 • ${chalk.cyan('Backspace')} 删除 • ${chalk.cyan('Enter')} 确认`);
// List

@@ -120,5 +123,12 @@ if (filteredChoices.length === 0) {

const isRefresh = selected && item.configured;
const statusLabel = !selected
? item.configured
? ' (configured)'
: item.detected
? ' (detected)'
: ''
: '';
const suffix = selected
? chalk.dim(isRefresh ? ' (refresh)' : ' (selected)')
: '';
: chalk.dim(statusLabel);
lines.push(` ${arrow} ${icon} ${name}${suffix}`);

@@ -144,5 +154,5 @@ }

* - ↑↓ to navigate
* - Enter to add highlighted item
* - Space to toggle highlighted item selection
* - Backspace to remove last selected item (or delete search char)
* - Tab to confirm selections
* - Enter to confirm selections
*/

@@ -149,0 +159,0 @@ export async function searchableMultiSelect(config) {

{
"name": "@studyzy/openspec-cn",
"version": "1.1.1",
"version": "1.2.0-1",
"description": "面向AI编程助手的规范驱动开发框架OpenSpec的简体中文汉化版本",

@@ -43,2 +43,20 @@ "keywords": [

],
"scripts": {
"lint": "eslint src/",
"build": "node build.js",
"dev": "tsc --watch",
"dev:cli": "pnpm build && node bin/openspec.js",
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage",
"test:postinstall": "node scripts/postinstall.js",
"prepare": "pnpm run build",
"prepublishOnly": "pnpm run build",
"postinstall": "node scripts/postinstall.js",
"check:pack-version": "node scripts/pack-version-check.mjs",
"release": "pnpm run release:ci",
"release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
"changeset": "changeset"
},
"engines": {

@@ -67,19 +85,3 @@ "node": ">=20.19.0"

"zod": "^4.0.17"
},
"scripts": {
"lint": "eslint src/",
"build": "node build.js",
"dev": "tsc --watch",
"dev:cli": "pnpm build && node bin/openspec.js",
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage",
"test:postinstall": "node scripts/postinstall.js",
"postinstall": "node scripts/postinstall.js",
"check:pack-version": "node scripts/pack-version-check.mjs",
"release": "pnpm run release:ci",
"release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
"changeset": "changeset"
}
}
}

@@ -45,5 +45,5 @@ name: spec-driven

格式要求:
- 每个需求:`### 需求:<名称>` 后面跟描述
- 每个需求:`### 需求:<名称>` 后面跟描述
- 使用 SHALL/MUST/必须/禁止等规范性词汇(避免使用 should/may)
- 每个场景:`#### 场景:<名称>` 使用 当/那么(WHEN/THEN)格式
- 每个场景:`#### 场景:<名称>` 使用 当/那么(WHEN/THEN)格式
- **关键提示**:场景标题必须恰好使用 4 个井号 (`####`)。使用 3 个井号或列表将导致处理失败。

@@ -54,3 +54,3 @@ - 每个需求必须至少有一个场景。

1. 在项目的 specs/<capability>/spec.md 中找到现有的需求
2. 复制整个需求块(从 `### 需求:` 到所有场景)
2. 复制整个需求块(从 `### 需求:` 到所有场景)
3. 粘贴到 `## 修改需求` 下并编辑以反映新行为

@@ -65,6 +65,6 @@ 4. 确保标题文本完全匹配(空格不敏感)

### 需求:用户可以导出数据
### 需求:用户可以导出数据
系统应当允许用户以 CSV 格式导出其数据。
#### 场景:成功导出
#### 场景:成功导出
- **当** 用户点击“导出”按钮

@@ -75,3 +75,3 @@ - **那么** 系统下载包含所有用户数据的 CSV 文件

### 需求:旧版导出
### 需求:旧版导出
**Reason**: 被新导出系统取代

@@ -78,0 +78,0 @@ **Migration**: 使用 /api/v2/export 处的新导出端点

## 新增需求
### 需求:<!-- 需求名称 -->
### 需求:<!-- 需求名称 -->
<!-- 需求描述文本(必须包含"必须"、"禁止"等关键词) -->
#### 场景:<!-- 场景名称 -->
#### 场景:<!-- 场景名称 -->
- **当** <!-- 条件 -->

@@ -8,0 +8,0 @@ - **那么** <!-- 预期结果 -->

Sorry, the diff of this file is too big to display