mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 14:38:54 +08:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b16b13613 | ||
|
|
c4cfdc7c49 | ||
|
|
e0736807b4 | ||
|
|
fdb05a723e |
@@ -1,5 +1,17 @@
|
||||
# @fission-ai/openspec
|
||||
|
||||
## 0.23.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#540](https://github.com/Fission-AI/OpenSpec/pull/540) [`c4cfdc7`](https://github.com/Fission-AI/OpenSpec/commit/c4cfdc7c499daef30d8a218f5f59b8d9e5adb754) Thanks [@TabishB](https://github.com/TabishB)! - ### New Features
|
||||
|
||||
- **Bulk archive skill** — Archive multiple completed changes in a single operation with `/opsx:bulk-archive`. Includes batch validation, spec conflict detection, and consolidated confirmation
|
||||
|
||||
### Other
|
||||
|
||||
- **Simplified setup** — Config creation now uses sensible defaults with helpful comments instead of interactive prompts
|
||||
|
||||
## 0.22.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-20
|
||||
@@ -0,0 +1,28 @@
|
||||
## Why
|
||||
|
||||
We want to rename `spec-driven` to `openspec-default` to better reflect that it's the standard/default workflow. However, renaming directly would break existing projects that have `schema: spec-driven` in their `openspec/config.yaml`. Adding alias support allows both names to work interchangeably, enabling a smooth transition with no breaking changes.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add schema alias resolution in the schema resolver
|
||||
- `openspec-default` and `spec-driven` will both resolve to the same schema
|
||||
- The physical directory remains `schemas/spec-driven/` (or could be renamed to `schemas/openspec-default/` with `spec-driven` as the alias)
|
||||
- All CLI commands and config files accept either name
|
||||
- No changes required to existing user configs
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `schema-aliases`: Support for schema name aliases so multiple names can resolve to the same schema directory
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
<!-- No existing spec-level behavior is changing - this is purely additive -->
|
||||
|
||||
## Impact
|
||||
|
||||
- `src/core/artifact-graph/resolver.ts` - Add alias resolution logic
|
||||
- `schemas/` directory - Potentially rename `spec-driven` to `openspec-default`
|
||||
- Documentation - Update to prefer `openspec-default` while noting `spec-driven` still works
|
||||
- Default schema constants - Update `DEFAULT_SCHEMA` to `openspec-default`
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "0.22.0",
|
||||
"version": "0.23.0",
|
||||
"description": "AI-native system for spec-driven development",
|
||||
"keywords": [
|
||||
"openspec",
|
||||
|
||||
@@ -28,9 +28,9 @@ import {
|
||||
type SchemaInfo,
|
||||
} from '../core/artifact-graph/index.js';
|
||||
import { createChange, validateChangeName } from '../utils/change-utils.js';
|
||||
import { getExploreSkillTemplate, getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getVerifyChangeSkillTemplate, getOpsxExploreCommandTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate, getOpsxVerifyCommandTemplate } from '../core/templates/skill-templates.js';
|
||||
import { getExploreSkillTemplate, getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getBulkArchiveChangeSkillTemplate, getVerifyChangeSkillTemplate, getOpsxExploreCommandTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate, getOpsxBulkArchiveCommandTemplate, getOpsxVerifyCommandTemplate } from '../core/templates/skill-templates.js';
|
||||
import { FileSystemUtils } from '../utils/file-system.js';
|
||||
import { promptForConfig, serializeConfig, isExitPromptError } from '../core/config-prompts.js';
|
||||
import { serializeConfig } from '../core/config-prompts.js';
|
||||
import { readProjectConfig } from '../core/project-config.js';
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -818,6 +818,7 @@ async function artifactExperimentalSetupCommand(): Promise<void> {
|
||||
const ffChangeSkill = getFfChangeSkillTemplate();
|
||||
const syncSpecsSkill = getSyncSpecsSkillTemplate();
|
||||
const archiveChangeSkill = getArchiveChangeSkillTemplate();
|
||||
const bulkArchiveChangeSkill = getBulkArchiveChangeSkillTemplate();
|
||||
const verifyChangeSkill = getVerifyChangeSkillTemplate();
|
||||
|
||||
// Get command templates
|
||||
@@ -828,6 +829,7 @@ async function artifactExperimentalSetupCommand(): Promise<void> {
|
||||
const ffCommand = getOpsxFfCommandTemplate();
|
||||
const syncCommand = getOpsxSyncCommandTemplate();
|
||||
const archiveCommand = getOpsxArchiveCommandTemplate();
|
||||
const bulkArchiveCommand = getOpsxBulkArchiveCommandTemplate();
|
||||
const verifyCommand = getOpsxVerifyCommandTemplate();
|
||||
|
||||
// Create skill directories and SKILL.md files
|
||||
@@ -839,6 +841,7 @@ async function artifactExperimentalSetupCommand(): Promise<void> {
|
||||
{ template: ffChangeSkill, dirName: 'openspec-ff-change' },
|
||||
{ template: syncSpecsSkill, dirName: 'openspec-sync-specs' },
|
||||
{ template: archiveChangeSkill, dirName: 'openspec-archive-change' },
|
||||
{ template: bulkArchiveChangeSkill, dirName: 'openspec-bulk-archive-change' },
|
||||
{ template: verifyChangeSkill, dirName: 'openspec-verify-change' },
|
||||
];
|
||||
|
||||
@@ -871,6 +874,7 @@ ${template.instructions}
|
||||
{ template: ffCommand, fileName: 'ff.md' },
|
||||
{ template: syncCommand, fileName: 'sync.md' },
|
||||
{ template: archiveCommand, fileName: 'archive.md' },
|
||||
{ template: bulkArchiveCommand, fileName: 'bulk-archive.md' },
|
||||
{ template: verifyCommand, fileName: 'verify.md' },
|
||||
];
|
||||
|
||||
@@ -941,89 +945,37 @@ ${template.content}
|
||||
console.log(chalk.dim(' schema: spec-driven'));
|
||||
console.log();
|
||||
} else {
|
||||
// Prompt for config creation
|
||||
// Create config with default schema
|
||||
const yamlContent = serializeConfig({ schema: DEFAULT_SCHEMA });
|
||||
|
||||
try {
|
||||
const configResult = await promptForConfig(projectRoot);
|
||||
await FileSystemUtils.writeFile(configPath, yamlContent);
|
||||
|
||||
if (configResult.createConfig && configResult.schema) {
|
||||
// Build config object
|
||||
const config = {
|
||||
schema: configResult.schema,
|
||||
context: configResult.context,
|
||||
rules: configResult.rules,
|
||||
};
|
||||
console.log();
|
||||
console.log(chalk.green('✓ Created openspec/config.yaml'));
|
||||
console.log();
|
||||
console.log(` Default schema: ${chalk.cyan(DEFAULT_SCHEMA)}`);
|
||||
console.log();
|
||||
console.log(chalk.dim(' Edit the file to add project context and per-artifact rules.'));
|
||||
console.log();
|
||||
|
||||
// Serialize to YAML
|
||||
const yamlContent = serializeConfig(config);
|
||||
|
||||
// Write config file
|
||||
try {
|
||||
await FileSystemUtils.writeFile(configPath, yamlContent);
|
||||
|
||||
console.log();
|
||||
console.log(chalk.green('✓ Created openspec/config.yaml'));
|
||||
console.log();
|
||||
console.log('━'.repeat(70));
|
||||
console.log();
|
||||
console.log(chalk.bold('📖 Config created at: openspec/config.yaml'));
|
||||
|
||||
// Display summary
|
||||
const contextLines = config.context ? config.context.split('\n').length : 0;
|
||||
const rulesCount = config.rules ? Object.keys(config.rules).length : 0;
|
||||
|
||||
console.log(` • Default schema: ${chalk.cyan(config.schema)}`);
|
||||
if (contextLines > 0) {
|
||||
console.log(` • Project context: ${chalk.cyan(`Added (${contextLines} lines)`)}`);
|
||||
}
|
||||
if (rulesCount > 0) {
|
||||
console.log(` • Rules: ${chalk.cyan(`${rulesCount} artifact${rulesCount > 1 ? 's' : ''} configured`)}`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
// Usage examples
|
||||
console.log(chalk.bold('Usage:'));
|
||||
console.log(' • New changes automatically use this schema');
|
||||
console.log(' • Context injected into all artifact instructions');
|
||||
console.log(' • Rules applied to matching artifacts');
|
||||
console.log();
|
||||
|
||||
// Git commit suggestion
|
||||
console.log(chalk.bold('To share with team:'));
|
||||
console.log(chalk.dim(' git add openspec/config.yaml .claude/'));
|
||||
console.log(chalk.dim(' git commit -m "Setup OpenSpec experimental workflow with project config"'));
|
||||
console.log();
|
||||
} catch (writeError) {
|
||||
// Handle file write errors
|
||||
console.error();
|
||||
console.error(chalk.red('✗ Failed to write openspec/config.yaml'));
|
||||
console.error(chalk.dim(` ${(writeError as Error).message}`));
|
||||
console.error();
|
||||
console.error('Fallback: Create config manually:');
|
||||
console.error(chalk.dim(' 1. Create openspec/config.yaml'));
|
||||
console.error(chalk.dim(' 2. Copy the following content:'));
|
||||
console.error();
|
||||
console.error(chalk.dim(yamlContent));
|
||||
console.error();
|
||||
}
|
||||
} else {
|
||||
// User chose not to create config
|
||||
console.log();
|
||||
console.log(chalk.blue('ℹ️ Skipped config creation.'));
|
||||
console.log(' You can create openspec/config.yaml manually later.');
|
||||
console.log();
|
||||
}
|
||||
} catch (promptError) {
|
||||
if (isExitPromptError(promptError)) {
|
||||
// User cancelled (Ctrl+C)
|
||||
console.log();
|
||||
console.log(chalk.blue('ℹ️ Config creation cancelled'));
|
||||
console.log(' Skills and commands already created');
|
||||
console.log(' Run setup again to create config later');
|
||||
console.log();
|
||||
} else {
|
||||
// Unexpected error
|
||||
throw promptError;
|
||||
}
|
||||
// Git commit suggestion
|
||||
console.log(chalk.bold('To share with team:'));
|
||||
console.log(chalk.dim(' git add openspec/config.yaml .claude/'));
|
||||
console.log(chalk.dim(' git commit -m "Setup OpenSpec experimental workflow"'));
|
||||
console.log();
|
||||
} catch (writeError) {
|
||||
// Handle file write errors
|
||||
console.error();
|
||||
console.error(chalk.red('✗ Failed to write openspec/config.yaml'));
|
||||
console.error(chalk.dim(` ${(writeError as Error).message}`));
|
||||
console.error();
|
||||
console.error('Fallback: Create config manually:');
|
||||
console.error(chalk.dim(' 1. Create openspec/config.yaml'));
|
||||
console.error(chalk.dim(' 2. Copy the following content:'));
|
||||
console.error();
|
||||
console.error(chalk.dim(yamlContent));
|
||||
console.error();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+27
-187
@@ -1,199 +1,39 @@
|
||||
import { stringify as stringifyYaml } from 'yaml';
|
||||
import { listSchemasWithInfo, resolveSchema } from './artifact-graph/resolver.js';
|
||||
import type { ProjectConfig } from './project-config.js';
|
||||
|
||||
/**
|
||||
* Check if an error is an ExitPromptError (user cancelled with Ctrl+C).
|
||||
* Used instead of instanceof check since @inquirer modules use dynamic imports.
|
||||
*/
|
||||
export function isExitPromptError(error: unknown): boolean {
|
||||
return (
|
||||
error !== null &&
|
||||
typeof error === 'object' &&
|
||||
'name' in error &&
|
||||
(error as { name: string }).name === 'ExitPromptError'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of interactive config creation prompts.
|
||||
*/
|
||||
export interface ConfigPromptResult {
|
||||
/** Whether to create config file */
|
||||
createConfig: boolean;
|
||||
/** Selected schema name */
|
||||
schema?: string;
|
||||
/** Project context (optional) */
|
||||
context?: string;
|
||||
/** Per-artifact rules (optional) */
|
||||
rules?: Record<string, string[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt user to create project config interactively.
|
||||
* Used by experimental setup command.
|
||||
*
|
||||
* @param projectRoot - Optional project root for project-local schema resolution
|
||||
* @returns Config prompt result
|
||||
* @throws ExitPromptError if user cancels (Ctrl+C)
|
||||
*/
|
||||
export async function promptForConfig(
|
||||
projectRoot?: string
|
||||
): Promise<ConfigPromptResult> {
|
||||
// Dynamic imports to prevent pre-commit hook hangs (see #367)
|
||||
const { confirm, select, editor, checkbox } = await import('@inquirer/prompts');
|
||||
|
||||
// Ask if user wants to create config
|
||||
const shouldCreate = await confirm({
|
||||
message: 'Create openspec/config.yaml?',
|
||||
default: true,
|
||||
});
|
||||
|
||||
if (!shouldCreate) {
|
||||
return { createConfig: false };
|
||||
}
|
||||
|
||||
// Get available schemas
|
||||
const schemas = listSchemasWithInfo(projectRoot);
|
||||
|
||||
if (schemas.length === 0) {
|
||||
throw new Error('No schemas found. Cannot create config.');
|
||||
}
|
||||
|
||||
// Prompt for schema selection
|
||||
const selectedSchema = await select({
|
||||
message: 'Default schema for new changes?',
|
||||
choices: schemas.map((s) => ({
|
||||
name: `${s.name} (${s.artifacts.join(' → ')})`,
|
||||
value: s.name,
|
||||
description: s.description || undefined,
|
||||
})),
|
||||
});
|
||||
|
||||
// Prompt for project context
|
||||
console.log('\nAdd project context? (optional)');
|
||||
console.log('Context is shown to AI when creating artifacts.');
|
||||
console.log('Examples: tech stack, conventions, style guides, domain knowledge\n');
|
||||
|
||||
const contextInput = await editor({
|
||||
message: 'Press Enter to skip, or edit context:',
|
||||
default: '',
|
||||
waitForUseInput: false,
|
||||
});
|
||||
|
||||
const context = contextInput.trim() || undefined;
|
||||
|
||||
// Prompt for per-artifact rules
|
||||
const addRules = await confirm({
|
||||
message: 'Add per-artifact rules? (optional)',
|
||||
default: false,
|
||||
});
|
||||
|
||||
let rules: Record<string, string[]> | undefined;
|
||||
|
||||
if (addRules) {
|
||||
// Load the selected schema to get artifact list
|
||||
const schema = resolveSchema(selectedSchema, projectRoot);
|
||||
const artifactIds = schema.artifacts.map((a) => a.id);
|
||||
|
||||
// Let user select which artifacts to add rules for
|
||||
const selectedArtifacts = await checkbox({
|
||||
message: 'Which artifacts should have custom rules?',
|
||||
choices: artifactIds.map((id) => ({
|
||||
name: id,
|
||||
value: id,
|
||||
})),
|
||||
});
|
||||
|
||||
if (selectedArtifacts.length > 0) {
|
||||
rules = {};
|
||||
|
||||
// For each selected artifact, collect rules line by line
|
||||
for (const artifactId of selectedArtifacts) {
|
||||
const artifactRules = await promptForArtifactRules(artifactId);
|
||||
if (artifactRules.length > 0) {
|
||||
rules[artifactId] = artifactRules;
|
||||
}
|
||||
}
|
||||
|
||||
// If no rules were actually added, set to undefined
|
||||
if (Object.keys(rules).length === 0) {
|
||||
rules = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
createConfig: true,
|
||||
schema: selectedSchema,
|
||||
context,
|
||||
rules,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for rules for a specific artifact.
|
||||
* Collects rules one per line until user enters empty line.
|
||||
*
|
||||
* @param artifactId - The artifact ID to collect rules for
|
||||
* @returns Array of rules
|
||||
*/
|
||||
async function promptForArtifactRules(artifactId: string): Promise<string[]> {
|
||||
// Dynamic import to prevent pre-commit hook hangs (see #367)
|
||||
const { input } = await import('@inquirer/prompts');
|
||||
|
||||
const rules: string[] = [];
|
||||
|
||||
console.log(`\nRules for ${artifactId} artifact:`);
|
||||
console.log('Enter rules one per line, press Enter on empty line to finish:\n');
|
||||
|
||||
while (true) {
|
||||
const rule = await input({
|
||||
message: '│',
|
||||
validate: () => {
|
||||
// Empty string is valid (signals end of input)
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
const trimmed = rule.trim();
|
||||
|
||||
// Empty line signals end of input
|
||||
if (!trimmed) {
|
||||
break;
|
||||
}
|
||||
|
||||
rules.push(trimmed);
|
||||
}
|
||||
|
||||
return rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize config to YAML string with proper multi-line formatting.
|
||||
* Serialize config to YAML string with helpful comments.
|
||||
*
|
||||
* @param config - Partial config object (schema required, context/rules optional)
|
||||
* @returns YAML string ready to write to file
|
||||
*/
|
||||
export function serializeConfig(config: Partial<ProjectConfig>): string {
|
||||
// Build clean config object (only include defined fields)
|
||||
const cleanConfig: Record<string, unknown> = {
|
||||
schema: config.schema,
|
||||
};
|
||||
const lines: string[] = [];
|
||||
|
||||
if (config.context) {
|
||||
cleanConfig.context = config.context;
|
||||
}
|
||||
// Schema (required)
|
||||
lines.push(`schema: ${config.schema}`);
|
||||
lines.push('');
|
||||
|
||||
if (config.rules && Object.keys(config.rules).length > 0) {
|
||||
cleanConfig.rules = config.rules;
|
||||
}
|
||||
// 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('# context: |');
|
||||
lines.push('# Tech stack: TypeScript, React, Node.js');
|
||||
lines.push('# We use conventional commits');
|
||||
lines.push('# Domain: e-commerce platform');
|
||||
lines.push('');
|
||||
|
||||
// Serialize to YAML with proper formatting
|
||||
return stringifyYaml(cleanConfig, {
|
||||
indent: 2,
|
||||
lineWidth: 0, // Don't wrap long lines
|
||||
defaultStringType: 'PLAIN',
|
||||
defaultKeyType: 'PLAIN',
|
||||
});
|
||||
// Rules section with comments
|
||||
lines.push('# Per-artifact rules (optional)');
|
||||
lines.push('# Add custom rules for specific artifacts.');
|
||||
lines.push('# Example:');
|
||||
lines.push('# rules:');
|
||||
lines.push('# proposal:');
|
||||
lines.push('# - Keep proposals under 500 words');
|
||||
lines.push('# - Always include a "Non-goals" section');
|
||||
lines.push('# tasks:');
|
||||
lines.push('# - Break tasks into chunks of max 2 hours');
|
||||
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
@@ -1634,6 +1634,252 @@ All artifacts complete. All tasks complete.
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Template for openspec-bulk-archive-change skill
|
||||
* For archiving multiple completed changes at once
|
||||
*/
|
||||
export function getBulkArchiveChangeSkillTemplate(): SkillTemplate {
|
||||
return {
|
||||
name: 'openspec-bulk-archive-change',
|
||||
description: 'Archive multiple completed changes at once. Use when archiving several parallel changes.',
|
||||
instructions: `Archive multiple completed changes in a single operation.
|
||||
|
||||
This skill allows you to batch-archive changes, handling spec conflicts intelligently by checking the codebase to determine what's actually implemented.
|
||||
|
||||
**Input**: None required (prompts for selection)
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Get active changes**
|
||||
|
||||
Run \`openspec list --json\` to get all active changes.
|
||||
|
||||
If no active changes exist, inform user and stop.
|
||||
|
||||
2. **Prompt for change selection**
|
||||
|
||||
Use **AskUserQuestion tool** with multi-select to let user choose changes:
|
||||
- Show each change with its schema
|
||||
- Include an option for "All changes"
|
||||
- Allow any number of selections (1+ works, 2+ is the typical use case)
|
||||
|
||||
**IMPORTANT**: Do NOT auto-select. Always let the user choose.
|
||||
|
||||
3. **Batch validation - gather status for all selected changes**
|
||||
|
||||
For each selected change, collect:
|
||||
|
||||
a. **Artifact status** - Run \`openspec status --change "<name>" --json\`
|
||||
- Parse \`schemaName\` and \`artifacts\` list
|
||||
- Note which artifacts are \`done\` vs other states
|
||||
|
||||
b. **Task completion** - Read \`openspec/changes/<name>/tasks.md\`
|
||||
- Count \`- [ ]\` (incomplete) vs \`- [x]\` (complete)
|
||||
- If no tasks file exists, note as "No tasks"
|
||||
|
||||
c. **Delta specs** - Check \`openspec/changes/<name>/specs/\` directory
|
||||
- List which capability specs exist
|
||||
- For each, extract requirement names (lines matching \`### Requirement: <name>\`)
|
||||
|
||||
4. **Detect spec conflicts**
|
||||
|
||||
Build a map of \`capability -> [changes that touch it]\`:
|
||||
|
||||
\`\`\`
|
||||
auth -> [change-a, change-b] <- CONFLICT (2+ changes)
|
||||
api -> [change-c] <- OK (only 1 change)
|
||||
\`\`\`
|
||||
|
||||
A conflict exists when 2+ selected changes have delta specs for the same capability.
|
||||
|
||||
5. **Resolve conflicts agentically**
|
||||
|
||||
**For each conflict**, investigate the codebase:
|
||||
|
||||
a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify
|
||||
|
||||
b. **Search the codebase** for implementation evidence:
|
||||
- Look for code implementing requirements from each delta spec
|
||||
- Check for related files, functions, or tests
|
||||
|
||||
c. **Determine resolution**:
|
||||
- If only one change is actually implemented -> sync that one's specs
|
||||
- If both implemented -> apply in chronological order (older first, newer overwrites)
|
||||
- If neither implemented -> skip spec sync, warn user
|
||||
|
||||
d. **Record resolution** for each conflict:
|
||||
- Which change's specs to apply
|
||||
- In what order (if both)
|
||||
- Rationale (what was found in codebase)
|
||||
|
||||
6. **Show consolidated status table**
|
||||
|
||||
Display a table summarizing all changes:
|
||||
|
||||
\`\`\`
|
||||
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
|
||||
|---------------------|-----------|-------|---------|-----------|--------|
|
||||
| schema-management | Done | 5/5 | 2 delta | None | Ready |
|
||||
| project-config | Done | 3/3 | 1 delta | None | Ready |
|
||||
| add-oauth | Done | 4/4 | 1 delta | auth (!) | Ready* |
|
||||
| add-verify-skill | 1 left | 2/5 | None | None | Warn |
|
||||
\`\`\`
|
||||
|
||||
For conflicts, show the resolution:
|
||||
\`\`\`
|
||||
* Conflict resolution:
|
||||
- auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order)
|
||||
\`\`\`
|
||||
|
||||
For incomplete changes, show warnings:
|
||||
\`\`\`
|
||||
Warnings:
|
||||
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
|
||||
\`\`\`
|
||||
|
||||
7. **Confirm batch operation**
|
||||
|
||||
Use **AskUserQuestion tool** with a single confirmation:
|
||||
|
||||
- "Archive N changes?" with options based on status
|
||||
- Options might include:
|
||||
- "Archive all N changes"
|
||||
- "Archive only N ready changes (skip incomplete)"
|
||||
- "Cancel"
|
||||
|
||||
If there are incomplete changes, make clear they'll be archived with warnings.
|
||||
|
||||
8. **Execute archive for each confirmed change**
|
||||
|
||||
Process changes in the determined order (respecting conflict resolution):
|
||||
|
||||
a. **Sync specs** if delta specs exist:
|
||||
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
|
||||
- For conflicts, apply in resolved order
|
||||
- Track if sync was done
|
||||
|
||||
b. **Perform the archive**:
|
||||
\`\`\`bash
|
||||
mkdir -p openspec/changes/archive
|
||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||
\`\`\`
|
||||
|
||||
c. **Track outcome** for each change:
|
||||
- Success: archived successfully
|
||||
- Failed: error during archive (record error)
|
||||
- Skipped: user chose not to archive (if applicable)
|
||||
|
||||
9. **Display summary**
|
||||
|
||||
Show final results:
|
||||
|
||||
\`\`\`
|
||||
## Bulk Archive Complete
|
||||
|
||||
Archived 3 changes:
|
||||
- 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/
|
||||
|
||||
Skipped 1 change:
|
||||
- add-verify-skill (user chose not to archive incomplete)
|
||||
|
||||
Spec sync summary:
|
||||
- 4 delta specs synced to main specs
|
||||
- 1 conflict resolved (auth: applied both in chronological order)
|
||||
\`\`\`
|
||||
|
||||
If any failures:
|
||||
\`\`\`
|
||||
Failed 1 change:
|
||||
- some-change: Archive directory already exists
|
||||
\`\`\`
|
||||
|
||||
**Conflict Resolution Examples**
|
||||
|
||||
Example 1: Only one implemented
|
||||
\`\`\`
|
||||
Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt]
|
||||
|
||||
Checking add-oauth:
|
||||
- Delta adds "OAuth Provider Integration" requirement
|
||||
- Searching codebase... found src/auth/oauth.ts implementing OAuth flow
|
||||
|
||||
Checking add-jwt:
|
||||
- Delta adds "JWT Token Handling" requirement
|
||||
- Searching codebase... no JWT implementation found
|
||||
|
||||
Resolution: Only add-oauth is implemented. Will sync add-oauth specs only.
|
||||
\`\`\`
|
||||
|
||||
Example 2: Both implemented
|
||||
\`\`\`
|
||||
Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql]
|
||||
|
||||
Checking add-rest-api (created 2026-01-10):
|
||||
- Delta adds "REST Endpoints" requirement
|
||||
- Searching codebase... found src/api/rest.ts
|
||||
|
||||
Checking add-graphql (created 2026-01-15):
|
||||
- Delta adds "GraphQL Schema" requirement
|
||||
- Searching codebase... found src/api/graphql.ts
|
||||
|
||||
Resolution: Both implemented. Will apply add-rest-api specs first,
|
||||
then add-graphql specs (chronological order, newer takes precedence).
|
||||
\`\`\`
|
||||
|
||||
**Output On Success**
|
||||
|
||||
\`\`\`
|
||||
## Bulk Archive Complete
|
||||
|
||||
Archived N changes:
|
||||
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
|
||||
- <change-2> -> archive/YYYY-MM-DD-<change-2>/
|
||||
|
||||
Spec sync summary:
|
||||
- N delta specs synced to main specs
|
||||
- No conflicts (or: M conflicts resolved)
|
||||
\`\`\`
|
||||
|
||||
**Output On Partial Success**
|
||||
|
||||
\`\`\`
|
||||
## Bulk Archive Complete (partial)
|
||||
|
||||
Archived N changes:
|
||||
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
|
||||
|
||||
Skipped M changes:
|
||||
- <change-2> (user chose not to archive incomplete)
|
||||
|
||||
Failed K changes:
|
||||
- <change-3>: Archive directory already exists
|
||||
\`\`\`
|
||||
|
||||
**Output When No Changes**
|
||||
|
||||
\`\`\`
|
||||
## No Changes to Archive
|
||||
|
||||
No active changes found. Use \`/opsx:new\` to create a new change.
|
||||
\`\`\`
|
||||
|
||||
**Guardrails**
|
||||
- Allow any number of changes (1+ is fine, 2+ is the typical use case)
|
||||
- Always prompt for selection, never auto-select
|
||||
- Detect spec conflicts early and resolve by checking codebase
|
||||
- When both changes are implemented, apply specs in chronological order
|
||||
- Skip spec sync only when implementation is missing (warn user)
|
||||
- Show clear per-change status before confirming
|
||||
- Use single confirmation for entire batch
|
||||
- Track and report all outcomes (success/skip/fail)
|
||||
- Preserve .openspec.yaml when moving to archive
|
||||
- Archive directory target uses current date: YYYY-MM-DD-<name>
|
||||
- If archive target exists, fail that change but continue with others`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Template for /opsx:sync slash command
|
||||
*/
|
||||
@@ -2103,6 +2349,253 @@ Target archive directory already exists.
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Template for /opsx:bulk-archive slash command
|
||||
*/
|
||||
export function getOpsxBulkArchiveCommandTemplate(): CommandTemplate {
|
||||
return {
|
||||
name: 'OPSX: Bulk Archive',
|
||||
description: 'Archive multiple completed changes at once',
|
||||
category: 'Workflow',
|
||||
tags: ['workflow', 'archive', 'experimental', 'bulk'],
|
||||
content: `Archive multiple completed changes in a single operation.
|
||||
|
||||
This skill allows you to batch-archive changes, handling spec conflicts intelligently by checking the codebase to determine what's actually implemented.
|
||||
|
||||
**Input**: None required (prompts for selection)
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Get active changes**
|
||||
|
||||
Run \`openspec list --json\` to get all active changes.
|
||||
|
||||
If no active changes exist, inform user and stop.
|
||||
|
||||
2. **Prompt for change selection**
|
||||
|
||||
Use **AskUserQuestion tool** with multi-select to let user choose changes:
|
||||
- Show each change with its schema
|
||||
- Include an option for "All changes"
|
||||
- Allow any number of selections (1+ works, 2+ is the typical use case)
|
||||
|
||||
**IMPORTANT**: Do NOT auto-select. Always let the user choose.
|
||||
|
||||
3. **Batch validation - gather status for all selected changes**
|
||||
|
||||
For each selected change, collect:
|
||||
|
||||
a. **Artifact status** - Run \`openspec status --change "<name>" --json\`
|
||||
- Parse \`schemaName\` and \`artifacts\` list
|
||||
- Note which artifacts are \`done\` vs other states
|
||||
|
||||
b. **Task completion** - Read \`openspec/changes/<name>/tasks.md\`
|
||||
- Count \`- [ ]\` (incomplete) vs \`- [x]\` (complete)
|
||||
- If no tasks file exists, note as "No tasks"
|
||||
|
||||
c. **Delta specs** - Check \`openspec/changes/<name>/specs/\` directory
|
||||
- List which capability specs exist
|
||||
- For each, extract requirement names (lines matching \`### Requirement: <name>\`)
|
||||
|
||||
4. **Detect spec conflicts**
|
||||
|
||||
Build a map of \`capability -> [changes that touch it]\`:
|
||||
|
||||
\`\`\`
|
||||
auth -> [change-a, change-b] <- CONFLICT (2+ changes)
|
||||
api -> [change-c] <- OK (only 1 change)
|
||||
\`\`\`
|
||||
|
||||
A conflict exists when 2+ selected changes have delta specs for the same capability.
|
||||
|
||||
5. **Resolve conflicts agentically**
|
||||
|
||||
**For each conflict**, investigate the codebase:
|
||||
|
||||
a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify
|
||||
|
||||
b. **Search the codebase** for implementation evidence:
|
||||
- Look for code implementing requirements from each delta spec
|
||||
- Check for related files, functions, or tests
|
||||
|
||||
c. **Determine resolution**:
|
||||
- If only one change is actually implemented -> sync that one's specs
|
||||
- If both implemented -> apply in chronological order (older first, newer overwrites)
|
||||
- If neither implemented -> skip spec sync, warn user
|
||||
|
||||
d. **Record resolution** for each conflict:
|
||||
- Which change's specs to apply
|
||||
- In what order (if both)
|
||||
- Rationale (what was found in codebase)
|
||||
|
||||
6. **Show consolidated status table**
|
||||
|
||||
Display a table summarizing all changes:
|
||||
|
||||
\`\`\`
|
||||
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
|
||||
|---------------------|-----------|-------|---------|-----------|--------|
|
||||
| schema-management | Done | 5/5 | 2 delta | None | Ready |
|
||||
| project-config | Done | 3/3 | 1 delta | None | Ready |
|
||||
| add-oauth | Done | 4/4 | 1 delta | auth (!) | Ready* |
|
||||
| add-verify-skill | 1 left | 2/5 | None | None | Warn |
|
||||
\`\`\`
|
||||
|
||||
For conflicts, show the resolution:
|
||||
\`\`\`
|
||||
* Conflict resolution:
|
||||
- auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order)
|
||||
\`\`\`
|
||||
|
||||
For incomplete changes, show warnings:
|
||||
\`\`\`
|
||||
Warnings:
|
||||
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
|
||||
\`\`\`
|
||||
|
||||
7. **Confirm batch operation**
|
||||
|
||||
Use **AskUserQuestion tool** with a single confirmation:
|
||||
|
||||
- "Archive N changes?" with options based on status
|
||||
- Options might include:
|
||||
- "Archive all N changes"
|
||||
- "Archive only N ready changes (skip incomplete)"
|
||||
- "Cancel"
|
||||
|
||||
If there are incomplete changes, make clear they'll be archived with warnings.
|
||||
|
||||
8. **Execute archive for each confirmed change**
|
||||
|
||||
Process changes in the determined order (respecting conflict resolution):
|
||||
|
||||
a. **Sync specs** if delta specs exist:
|
||||
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
|
||||
- For conflicts, apply in resolved order
|
||||
- Track if sync was done
|
||||
|
||||
b. **Perform the archive**:
|
||||
\`\`\`bash
|
||||
mkdir -p openspec/changes/archive
|
||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||
\`\`\`
|
||||
|
||||
c. **Track outcome** for each change:
|
||||
- Success: archived successfully
|
||||
- Failed: error during archive (record error)
|
||||
- Skipped: user chose not to archive (if applicable)
|
||||
|
||||
9. **Display summary**
|
||||
|
||||
Show final results:
|
||||
|
||||
\`\`\`
|
||||
## Bulk Archive Complete
|
||||
|
||||
Archived 3 changes:
|
||||
- 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/
|
||||
|
||||
Skipped 1 change:
|
||||
- add-verify-skill (user chose not to archive incomplete)
|
||||
|
||||
Spec sync summary:
|
||||
- 4 delta specs synced to main specs
|
||||
- 1 conflict resolved (auth: applied both in chronological order)
|
||||
\`\`\`
|
||||
|
||||
If any failures:
|
||||
\`\`\`
|
||||
Failed 1 change:
|
||||
- some-change: Archive directory already exists
|
||||
\`\`\`
|
||||
|
||||
**Conflict Resolution Examples**
|
||||
|
||||
Example 1: Only one implemented
|
||||
\`\`\`
|
||||
Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt]
|
||||
|
||||
Checking add-oauth:
|
||||
- Delta adds "OAuth Provider Integration" requirement
|
||||
- Searching codebase... found src/auth/oauth.ts implementing OAuth flow
|
||||
|
||||
Checking add-jwt:
|
||||
- Delta adds "JWT Token Handling" requirement
|
||||
- Searching codebase... no JWT implementation found
|
||||
|
||||
Resolution: Only add-oauth is implemented. Will sync add-oauth specs only.
|
||||
\`\`\`
|
||||
|
||||
Example 2: Both implemented
|
||||
\`\`\`
|
||||
Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql]
|
||||
|
||||
Checking add-rest-api (created 2026-01-10):
|
||||
- Delta adds "REST Endpoints" requirement
|
||||
- Searching codebase... found src/api/rest.ts
|
||||
|
||||
Checking add-graphql (created 2026-01-15):
|
||||
- Delta adds "GraphQL Schema" requirement
|
||||
- Searching codebase... found src/api/graphql.ts
|
||||
|
||||
Resolution: Both implemented. Will apply add-rest-api specs first,
|
||||
then add-graphql specs (chronological order, newer takes precedence).
|
||||
\`\`\`
|
||||
|
||||
**Output On Success**
|
||||
|
||||
\`\`\`
|
||||
## Bulk Archive Complete
|
||||
|
||||
Archived N changes:
|
||||
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
|
||||
- <change-2> -> archive/YYYY-MM-DD-<change-2>/
|
||||
|
||||
Spec sync summary:
|
||||
- N delta specs synced to main specs
|
||||
- No conflicts (or: M conflicts resolved)
|
||||
\`\`\`
|
||||
|
||||
**Output On Partial Success**
|
||||
|
||||
\`\`\`
|
||||
## Bulk Archive Complete (partial)
|
||||
|
||||
Archived N changes:
|
||||
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
|
||||
|
||||
Skipped M changes:
|
||||
- <change-2> (user chose not to archive incomplete)
|
||||
|
||||
Failed K changes:
|
||||
- <change-3>: Archive directory already exists
|
||||
\`\`\`
|
||||
|
||||
**Output When No Changes**
|
||||
|
||||
\`\`\`
|
||||
## No Changes to Archive
|
||||
|
||||
No active changes found. Use \`/opsx:new\` to create a new change.
|
||||
\`\`\`
|
||||
|
||||
**Guardrails**
|
||||
- Allow any number of changes (1+ is fine, 2+ is the typical use case)
|
||||
- Always prompt for selection, never auto-select
|
||||
- Detect spec conflicts early and resolve by checking codebase
|
||||
- When both changes are implemented, apply specs in chronological order
|
||||
- Skip spec sync only when implementation is missing (warn user)
|
||||
- Show clear per-change status before confirming
|
||||
- Use single confirmation for entire batch
|
||||
- Track and report all outcomes (success/skip/fail)
|
||||
- Preserve .openspec.yaml when moving to archive
|
||||
- Archive directory target uses current date: YYYY-MM-DD-<name>
|
||||
- If archive target exists, fail that change but continue with others`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Template for /opsx:verify slash command
|
||||
*/
|
||||
|
||||
+5
-2
@@ -7,6 +7,9 @@ export async function setup() {
|
||||
|
||||
// Global teardown to ensure clean exit
|
||||
export async function teardown() {
|
||||
// Clear any remaining timers
|
||||
// This helps prevent hanging handles from keeping the process alive
|
||||
// Force exit after a short grace period if the process hasn't exited cleanly.
|
||||
// This handles cases where child processes or open handles keep the worker alive.
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 1000).unref();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user