Compare commits

...
Author SHA1 Message Date
openspec-release-bot[bot]andgithub-actions[bot] 3b16b13613 Version Packages (#541)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-20 17:21:29 -08:00
Tabish Bidiwale c4cfdc7c49 Add changeset for bulk-archive skill and setup simplification (#540) 2026-01-20 17:17:31 -08:00
Tabish Bidiwale e0736807b4 refactor(setup): simplify config creation and fix test hanging (#537)
* refactor(setup): simplify config creation and fix test hanging

- Replace interactive config prompts with automatic config creation using
  default schema. The generated config includes helpful comments explaining
  context and rules options.
- Remove unused promptForConfig, promptForArtifactRules, and isExitPromptError
  functions from config-prompts.ts
- Add forceExit: true to vitest config to prevent worker processes from hanging
  after tests complete

* docs: add schema-alias-support change proposal

Proposal to add schema alias support so `openspec-default` and `spec-driven`
can be used interchangeably, enabling a rename without breaking existing configs.

* fix(test): remove invalid forceExit config and add proper teardown

- Remove `forceExit: true` from vitest.config.ts (Jest option, not Vitest)
- Add actual teardown logic in vitest.setup.ts that forces exit after 1s
  grace period if processes are still hanging
2026-01-20 14:32:56 -08:00
Tabish Bidiwale fdb05a723e feat(skills): add bulk-archive skill for archiving multiple changes (#527)
Add `/opsx:bulk-archive` skill that allows archiving multiple completed
changes in a single operation. Features include:

- Multi-select change selection via AskUserQuestion
- Batch validation of artifacts, tasks, and delta specs
- Spec conflict detection when multiple changes touch same capability
- Agentic conflict resolution by checking codebase for implementation
- Consolidated status table before confirmation
- Single confirmation for entire batch operation
- Comprehensive summary showing archived/skipped/failed changes

This is useful when working on multiple changes in parallel and wanting
to archive them together after implementation is complete.
2026-01-20 11:37:33 -08:00
8 changed files with 602 additions and 272 deletions
+12
View File
@@ -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
View File
@@ -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",
+34 -82
View File
@@ -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
View File
@@ -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';
}
+493
View File
@@ -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
View File
@@ -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();
}