@codewalla_india/openspec 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -23,7 +23,7 @@ import { registerStoreCommand } from '../commands/store.js';
23
23
  import { registerDoctorCommand } from '../commands/doctor.js';
24
24
  import { registerContextCommand } from '../commands/context.js';
25
25
  import { registerWorksetCommand } from '../commands/workset.js';
26
- import { statusCommand, instructionsCommand, applyInstructionsCommand, templatesCommand, schemasCommand, newChangeCommand, DEFAULT_SCHEMA, } from '../commands/workflow/index.js';
26
+ import { statusCommand, instructionsCommand, applyInstructionsCommand, modifyInstructionsCommand, templatesCommand, schemasCommand, newChangeCommand, DEFAULT_SCHEMA, } from '../commands/workflow/index.js';
27
27
  import { requireTelemetryIdentity, TelemetryIdentityRequiredError, trackCommand, shutdown } from '../telemetry/index.js';
28
28
  import { buildCommandTelemetryContext, resolveTelemetryCommandPath, } from '../telemetry/command-context.js';
29
29
  import { COMMON_FLAGS } from '../core/completions/shared-flags.js';
@@ -505,14 +505,20 @@ program
505
505
  .option('--score <percent>', 'Quiz score 0-100 (required with --record-comprehension-pass)', parseInt)
506
506
  .option('--attempt <n>', 'Quiz attempt number', parseInt)
507
507
  .option('--question-count <n>', 'Number of quiz questions taken', parseInt)
508
+ .option('--artifact <id>', 'Source artifact to modify (use with: instructions modify)')
509
+ .option('--workflow-input <text>', 'User modify request for telemetry (use with: instructions modify)')
510
+ .option('--workflow-input-file <path>', 'Read modify request from a file (use with: instructions modify)')
511
+ .option('--editor <tool>', 'AI editor used (cursor, windsurf, claude)')
508
512
  .option('--store <id>', STORE_OPTION_DESCRIPTION)
509
513
  .addOption(hiddenStorePathOption())
510
514
  .action(async (artifactId, options) => {
511
515
  try {
512
- // Special case: "apply" is not an artifact, but a command to get apply instructions
513
516
  if (artifactId === 'apply') {
514
517
  await applyInstructionsCommand(options);
515
518
  }
519
+ else if (artifactId === 'modify') {
520
+ await modifyInstructionsCommand(options);
521
+ }
516
522
  else {
517
523
  await instructionsCommand(artifactId, options);
518
524
  }
@@ -5,8 +5,8 @@
5
5
  */
6
6
  export { statusCommand } from './status.js';
7
7
  export type { StatusOptions } from './status.js';
8
- export { instructionsCommand, applyInstructionsCommand } from './instructions.js';
9
- export type { InstructionsOptions } from './instructions.js';
8
+ export { instructionsCommand, applyInstructionsCommand, modifyInstructionsCommand } from './instructions.js';
9
+ export type { InstructionsOptions, ModifyInstructionsOptions } from './instructions.js';
10
10
  export { templatesCommand } from './templates.js';
11
11
  export type { TemplatesOptions } from './templates.js';
12
12
  export { schemasCommand } from './schemas.js';
@@ -4,7 +4,7 @@
4
4
  * Commands for the artifact-driven workflow: status, instructions, templates, schemas, new change.
5
5
  */
6
6
  export { statusCommand } from './status.js';
7
- export { instructionsCommand, applyInstructionsCommand } from './instructions.js';
7
+ export { instructionsCommand, applyInstructionsCommand, modifyInstructionsCommand } from './instructions.js';
8
8
  export { templatesCommand } from './templates.js';
9
9
  export { schemasCommand } from './schemas.js';
10
10
  export { newChangeCommand } from './new-change.js';
@@ -4,7 +4,7 @@
4
4
  * Generates enriched instructions for creating artifacts or applying tasks.
5
5
  * Includes both artifact instructions and apply instructions.
6
6
  */
7
- import { type ArtifactInstructions } from '../../core/artifact-graph/index.js';
7
+ import { type ArtifactInstructions, type ArtifactPathSummary } from '../../core/artifact-graph/index.js';
8
8
  import { type PlanningHome } from '../../core/planning-home.js';
9
9
  import { type ReferenceIndexEntry } from '../../core/references.js';
10
10
  import { type ProjectConfig } from '../../core/project-config.js';
@@ -27,6 +27,24 @@ export interface ApplyInstructionsOptions {
27
27
  attempt?: number;
28
28
  questionCount?: number;
29
29
  }
30
+ export interface ModifyInstructionsOptions extends InstructionsOptions {
31
+ artifact?: string;
32
+ workflowInput?: string;
33
+ workflowInputFile?: string;
34
+ editor?: string;
35
+ }
36
+ export interface ModifyInstructions {
37
+ changeName: string;
38
+ schemaName: string;
39
+ sourceArtifact: string;
40
+ modifyInput?: string;
41
+ downstreamArtifacts: string[];
42
+ artifactsToUpdate: string[];
43
+ changeRoot: string;
44
+ artifactPaths: Record<string, ArtifactPathSummary>;
45
+ phase: 'pre_apply';
46
+ instruction: string;
47
+ }
30
48
  export declare function instructionsCommand(artifactId: string | undefined, options: InstructionsOptions): Promise<void>;
31
49
  export declare function printInstructionsText(instructions: ArtifactInstructions, isBlocked: boolean): void;
32
50
  export interface GenerateApplyInstructionsOptions {
@@ -42,4 +60,6 @@ export interface GenerateApplyInstructionsOptions {
42
60
  export declare function generateApplyInstructions(projectRoot: string, changeName: string, schemaName?: string, options?: GenerateApplyInstructionsOptions): Promise<ApplyInstructions>;
43
61
  export declare function applyInstructionsCommand(options: ApplyInstructionsOptions): Promise<void>;
44
62
  export declare function printApplyInstructionsText(instructions: ApplyInstructions): void;
63
+ export declare function modifyInstructionsCommand(options: ModifyInstructionsOptions): Promise<void>;
64
+ export declare function printModifyInstructionsText(instructions: ModifyInstructions): void;
45
65
  //# sourceMappingURL=instructions.d.ts.map
@@ -7,14 +7,15 @@
7
7
  import ora from 'ora';
8
8
  import path from 'path';
9
9
  import * as fs from 'fs';
10
- import { loadChangeContext, generateInstructions, resolveSchema, resolveArtifactOutputs, } from '../../core/artifact-graph/index.js';
10
+ import { loadChangeContext, generateInstructions, formatChangeStatus, resolveSchema, resolveArtifactOutputs, } from '../../core/artifact-graph/index.js';
11
11
  import { getChangeDir, resolveCurrentPlanningHomeSync, } from '../../core/planning-home.js';
12
- import { resolveRootForCommand, withStoreFlag, toPlanningHome, toRootOutput, } from '../../core/root-selection.js';
12
+ import { resolveRootForCommand, withStoreFlag, toPlanningHome, toRootOutput, isStoreSelectedRoot, } from '../../core/root-selection.js';
13
13
  import { assembleReferenceIndex, renderReferencedStoresBlock, renderReferencedStoresSection, } from '../../core/references.js';
14
14
  import { readRegistrySnapshot } from '../../core/store/registry.js';
15
15
  import { readProjectConfig } from '../../core/project-config.js';
16
16
  import { checkComprehensionGate, ComprehensionPassError, computeSpecStats, recordComprehensionPass, resolveComprehensionConfig, } from '../../core/comprehension/index.js';
17
- import { maybeEmitProposalReady, maybeEmitApplyReady, trackArtifactInstructions, trackArtifactContentChanges, incrementComprehensionAttempt, incrementComprehensionFailureCount, trackComprehensionAttempt, trackComprehensionGateChecked, trackComprehensionRetakeRequired, } from '../../telemetry/index.js';
17
+ import { maybeEmitProposalReady, maybeEmitApplyReady, trackArtifactInstructions, trackArtifactContentChanges, trackArtifactModifyRequested, incrementComprehensionAttempt, incrementComprehensionFailureCount, trackComprehensionAttempt, trackComprehensionGateChecked, trackComprehensionRetakeRequired, } from '../../telemetry/index.js';
18
+ import { normalizeEditor, resolveWorkflowInputAsync, } from '../../telemetry/input.js';
18
19
  import { validateChangeExists, validateSchemaExists, } from './shared.js';
19
20
  function buildArtifactPresence(contextFiles, pendingTaskCount) {
20
21
  return {
@@ -634,4 +635,124 @@ export function printApplyInstructionsText(instructions) {
634
635
  console.log('### Instruction');
635
636
  console.log(instruction);
636
637
  }
638
+ // -----------------------------------------------------------------------------
639
+ // Modify Instructions Command (pre-apply artifact revision)
640
+ // -----------------------------------------------------------------------------
641
+ function buildModifyInstruction(sourceArtifact, downstreamArtifacts, modifyInput) {
642
+ const downstreamLine = downstreamArtifacts.length > 0
643
+ ? `Downstream artifacts to refresh: ${downstreamArtifacts.join(', ')}.`
644
+ : 'No downstream artifacts depend on this source.';
645
+ const requestLine = modifyInput
646
+ ? `User modify request: ${modifyInput}`
647
+ : 'Apply the user modify request from the conversation.';
648
+ return [
649
+ `Revise the "${sourceArtifact}" artifact for this change (pre-apply only).`,
650
+ requestLine,
651
+ downstreamLine,
652
+ 'For each artifact in artifactsToUpdate (in order): run openspec instructions <id> --change <name> --json, read upstream dependencies, apply a surgical edit aligned with the modify request, and write to resolvedOutputPath.',
653
+ 'When proposal capabilities change, add/remove/rename specs under specs/ accordingly.',
654
+ 'After all updates, run openspec status --change <name> --json, then hand off to /opsx:apply.',
655
+ ].join('\n');
656
+ }
657
+ export async function modifyInstructionsCommand(options) {
658
+ const root = await resolveRootForCommand(options, { json: options.json });
659
+ if (!root) {
660
+ return;
661
+ }
662
+ const spinner = options.json ? undefined : ora('Generating modify instructions...').start();
663
+ try {
664
+ const planningHome = toPlanningHome(root);
665
+ const projectRoot = root.path;
666
+ const changeName = await validateChangeExists(options.change, projectRoot, root.changesDir, { newChangeHint: withStoreFlag(root, 'openspec new change <name>') });
667
+ if (options.schema) {
668
+ validateSchemaExists(options.schema, projectRoot);
669
+ }
670
+ const sourceArtifact = options.artifact?.trim();
671
+ if (!sourceArtifact) {
672
+ spinner?.stop();
673
+ throw new Error('Missing required option --artifact. Specify the artifact to modify (e.g., design, proposal, tasks).');
674
+ }
675
+ const modifyInput = await resolveWorkflowInputAsync({
676
+ workflowInput: options.workflowInput,
677
+ workflowInputFile: options.workflowInputFile,
678
+ });
679
+ const editor = normalizeEditor(options.editor);
680
+ const context = loadChangeContext(projectRoot, changeName, options.schema, {
681
+ changeDir: getChangeDir(planningHome, changeName),
682
+ planningHome,
683
+ });
684
+ const artifact = context.graph.getArtifact(sourceArtifact);
685
+ if (!artifact) {
686
+ spinner?.stop();
687
+ const validIds = context.graph.getAllArtifacts().map((a) => a.id);
688
+ throw new Error(`Artifact '${sourceArtifact}' not found in schema '${context.schemaName}'. Valid artifacts:\n ${validIds.join('\n ')}`);
689
+ }
690
+ const applyInstructions = await generateApplyInstructions(projectRoot, changeName, options.schema, {
691
+ planningHome,
692
+ });
693
+ if (applyInstructions.progress.complete > 0) {
694
+ spinner?.stop();
695
+ throw new Error(`Apply has already started (${applyInstructions.progress.complete}/${applyInstructions.progress.total} tasks complete). /opsx:modify is pre-apply only. Edit artifacts manually or start a new change.`);
696
+ }
697
+ if (applyInstructions.missingArtifacts && applyInstructions.missingArtifacts.length > 0) {
698
+ spinner?.stop();
699
+ throw new Error(`Change is not ready to modify. Missing artifacts: ${applyInstructions.missingArtifacts.join(', ')}. Use /opsx:continue or /opsx:propose to create them first.`);
700
+ }
701
+ const sourceOutputs = resolveArtifactOutputs(context.changeDir, artifact.generates);
702
+ if (sourceOutputs.length === 0) {
703
+ spinner?.stop();
704
+ throw new Error(`Artifact '${sourceArtifact}' does not exist yet. Use /opsx:continue to create it first.`);
705
+ }
706
+ const downstreamArtifacts = context.graph.getTransitiveDependents(sourceArtifact);
707
+ const artifactsToUpdate = [sourceArtifact, ...downstreamArtifacts];
708
+ const status = formatChangeStatus(context, isStoreSelectedRoot(root) ? { storeId: root.storeId } : {});
709
+ await trackArtifactModifyRequested({
710
+ changeDir: context.changeDir,
711
+ changeName,
712
+ schema: context.schemaName,
713
+ sourceArtifactId: sourceArtifact,
714
+ downstreamArtifactIds: downstreamArtifacts,
715
+ artifactsToUpdate,
716
+ modifyInput,
717
+ editor,
718
+ });
719
+ const payload = {
720
+ changeName,
721
+ schemaName: context.schemaName,
722
+ sourceArtifact,
723
+ ...(modifyInput ? { modifyInput } : {}),
724
+ downstreamArtifacts,
725
+ artifactsToUpdate,
726
+ changeRoot: context.changeDir,
727
+ artifactPaths: status.artifactPaths,
728
+ phase: 'pre_apply',
729
+ instruction: buildModifyInstruction(sourceArtifact, downstreamArtifacts, modifyInput),
730
+ };
731
+ spinner?.stop();
732
+ if (options.json) {
733
+ console.log(JSON.stringify({ ...payload, root: toRootOutput(root) }, null, 2));
734
+ return;
735
+ }
736
+ printModifyInstructionsText(payload);
737
+ }
738
+ catch (error) {
739
+ spinner?.stop();
740
+ throw error;
741
+ }
742
+ }
743
+ export function printModifyInstructionsText(instructions) {
744
+ console.log(`## Modify: ${instructions.changeName}`);
745
+ console.log(`Schema: ${instructions.schemaName}`);
746
+ console.log(`Phase: ${instructions.phase}`);
747
+ console.log();
748
+ console.log(`Source artifact: ${instructions.sourceArtifact}`);
749
+ if (instructions.modifyInput) {
750
+ console.log(`Modify request: ${instructions.modifyInput}`);
751
+ }
752
+ console.log(`Downstream: ${instructions.downstreamArtifacts.length > 0 ? instructions.downstreamArtifacts.join(', ') : '(none)'}`);
753
+ console.log(`Artifacts to update: ${instructions.artifactsToUpdate.join(', ')}`);
754
+ console.log();
755
+ console.log('### Instruction');
756
+ console.log(instructions.instruction);
757
+ }
637
758
  //# sourceMappingURL=instructions.js.map
@@ -35,6 +35,15 @@ export declare class ArtifactGraph {
35
35
  * Gets the schema version.
36
36
  */
37
37
  getVersion(): number;
38
+ /**
39
+ * Builds reverse adjacency: artifact ID -> IDs of artifacts that depend on it.
40
+ */
41
+ private buildDependentsMap;
42
+ /**
43
+ * Returns all transitive downstream artifact IDs that depend on the given artifact,
44
+ * sorted by build order (excludes the source artifact itself).
45
+ */
46
+ getTransitiveDependents(artifactId: string): string[];
38
47
  /**
39
48
  * Computes the topological build order using Kahn's algorithm.
40
49
  * Returns artifact IDs in the order they should be built.
@@ -55,23 +55,53 @@ export class ArtifactGraph {
55
55
  return this.schema.version;
56
56
  }
57
57
  /**
58
- * Computes the topological build order using Kahn's algorithm.
59
- * Returns artifact IDs in the order they should be built.
58
+ * Builds reverse adjacency: artifact ID -> IDs of artifacts that depend on it.
60
59
  */
61
- getBuildOrder() {
62
- const inDegree = new Map();
60
+ buildDependentsMap() {
63
61
  const dependents = new Map();
64
- // Initialize all artifacts
65
62
  for (const artifact of this.artifacts.values()) {
66
- inDegree.set(artifact.id, artifact.requires.length);
67
63
  dependents.set(artifact.id, []);
68
64
  }
69
- // Build reverse adjacency (who depends on whom)
70
65
  for (const artifact of this.artifacts.values()) {
71
66
  for (const req of artifact.requires) {
72
67
  dependents.get(req).push(artifact.id);
73
68
  }
74
69
  }
70
+ return dependents;
71
+ }
72
+ /**
73
+ * Returns all transitive downstream artifact IDs that depend on the given artifact,
74
+ * sorted by build order (excludes the source artifact itself).
75
+ */
76
+ getTransitiveDependents(artifactId) {
77
+ if (!this.artifacts.has(artifactId)) {
78
+ return [];
79
+ }
80
+ const dependents = this.buildDependentsMap();
81
+ const collected = new Set();
82
+ const queue = [artifactId];
83
+ while (queue.length > 0) {
84
+ const current = queue.shift();
85
+ for (const dependentId of dependents.get(current) ?? []) {
86
+ if (!collected.has(dependentId)) {
87
+ collected.add(dependentId);
88
+ queue.push(dependentId);
89
+ }
90
+ }
91
+ }
92
+ return this.getBuildOrder().filter((id) => collected.has(id));
93
+ }
94
+ /**
95
+ * Computes the topological build order using Kahn's algorithm.
96
+ * Returns artifact IDs in the order they should be built.
97
+ */
98
+ getBuildOrder() {
99
+ const inDegree = new Map();
100
+ const dependents = this.buildDependentsMap();
101
+ // Initialize in-degrees
102
+ for (const artifact of this.artifacts.values()) {
103
+ inDegree.set(artifact.id, artifact.requires.length);
104
+ }
75
105
  // Start with roots (in-degree 0), sorted for determinism
76
106
  const queue = [...this.artifacts.keys()]
77
107
  .filter(id => inDegree.get(id) === 0)
@@ -28,7 +28,7 @@ export function buildNextSteps(input) {
28
28
  steps.push(`Run openspec instructions ${readyArtifact.id} --change "${input.changeName}"${storeFlag} --json before writing that artifact.`);
29
29
  }
30
30
  else if (input.allArtifactsComplete) {
31
- steps.push('All planning artifacts are complete; review tasks before implementation.');
31
+ steps.push('Review planning artifacts; use /opsx:modify to revise or /opsx:apply to implement.');
32
32
  }
33
33
  return steps;
34
34
  }
@@ -210,6 +210,26 @@ export const COMMAND_REGISTRY = [
210
210
  description: 'Number of quiz questions taken',
211
211
  takesValue: true,
212
212
  },
213
+ {
214
+ name: 'artifact',
215
+ description: 'Source artifact to modify (use with: instructions modify)',
216
+ takesValue: true,
217
+ },
218
+ {
219
+ name: 'workflow-input',
220
+ description: 'User modify request for telemetry (use with: instructions modify)',
221
+ takesValue: true,
222
+ },
223
+ {
224
+ name: 'workflow-input-file',
225
+ description: 'Read modify request from a file (use with: instructions modify)',
226
+ takesValue: true,
227
+ },
228
+ {
229
+ name: 'editor',
230
+ description: 'AI editor used (cursor, windsurf, claude)',
231
+ takesValue: true,
232
+ },
213
233
  COMMON_FLAGS.json,
214
234
  COMMON_FLAGS.store,
215
235
  ],
@@ -11,6 +11,7 @@ export const WORKFLOW_TO_SKILL_DIR = {
11
11
  'explore': 'openspec-explore',
12
12
  'new': 'openspec-new-change',
13
13
  'continue': 'openspec-continue-change',
14
+ 'modify': 'openspec-modify-change',
14
15
  'apply': 'openspec-apply-change',
15
16
  'ff': 'openspec-ff-change',
16
17
  'sync': 'openspec-sync-specs',
@@ -9,11 +9,11 @@ import type { Profile } from './global-config.js';
9
9
  * Core workflows included in the 'core' profile.
10
10
  * These provide the streamlined experience for new users.
11
11
  */
12
- export declare const CORE_WORKFLOWS: readonly ["propose", "explore", "apply", "sync", "archive"];
12
+ export declare const CORE_WORKFLOWS: readonly ["propose", "explore", "modify", "apply", "sync", "archive"];
13
13
  /**
14
14
  * All available workflows in the system.
15
15
  */
16
- export declare const ALL_WORKFLOWS: readonly ["propose", "explore", "new", "continue", "apply", "ff", "sync", "archive", "bulk-archive", "verify", "onboard"];
16
+ export declare const ALL_WORKFLOWS: readonly ["propose", "explore", "modify", "new", "continue", "apply", "ff", "sync", "archive", "bulk-archive", "verify", "onboard"];
17
17
  export type WorkflowId = (typeof ALL_WORKFLOWS)[number];
18
18
  export type CoreWorkflowId = (typeof CORE_WORKFLOWS)[number];
19
19
  /**
@@ -8,13 +8,14 @@
8
8
  * Core workflows included in the 'core' profile.
9
9
  * These provide the streamlined experience for new users.
10
10
  */
11
- export const CORE_WORKFLOWS = ['propose', 'explore', 'apply', 'sync', 'archive'];
11
+ export const CORE_WORKFLOWS = ['propose', 'explore', 'modify', 'apply', 'sync', 'archive'];
12
12
  /**
13
13
  * All available workflows in the system.
14
14
  */
15
15
  export const ALL_WORKFLOWS = [
16
16
  'propose',
17
17
  'explore',
18
+ 'modify',
18
19
  'new',
19
20
  'continue',
20
21
  'apply',
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Shared utilities for generating skill and command files.
5
5
  */
6
- 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';
6
+ import { getExploreSkillTemplate, getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getBulkArchiveChangeSkillTemplate, getVerifyChangeSkillTemplate, getOnboardSkillTemplate, getOpsxProposeSkillTemplate, getModifyChangeSkillTemplate, getOpsxExploreCommandTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate, getOpsxBulkArchiveCommandTemplate, getOpsxVerifyCommandTemplate, getOpsxOnboardCommandTemplate, getOpsxProposeCommandTemplate, getOpsxModifyCommandTemplate, } from '../templates/skill-templates.js';
7
7
  /**
8
8
  * Gets skill templates with their directory names, optionally filtered by workflow IDs.
9
9
  *
@@ -15,6 +15,7 @@ export function getSkillTemplates(workflowFilter) {
15
15
  { template: getNewChangeSkillTemplate(), dirName: 'openspec-new-change', workflowId: 'new' },
16
16
  { template: getContinueChangeSkillTemplate(), dirName: 'openspec-continue-change', workflowId: 'continue' },
17
17
  { template: getApplyChangeSkillTemplate(), dirName: 'openspec-apply-change', workflowId: 'apply' },
18
+ { template: getModifyChangeSkillTemplate(), dirName: 'openspec-modify-change', workflowId: 'modify' },
18
19
  { template: getFfChangeSkillTemplate(), dirName: 'openspec-ff-change', workflowId: 'ff' },
19
20
  { template: getSyncSpecsSkillTemplate(), dirName: 'openspec-sync-specs', workflowId: 'sync' },
20
21
  { template: getArchiveChangeSkillTemplate(), dirName: 'openspec-archive-change', workflowId: 'archive' },
@@ -39,6 +40,7 @@ export function getCommandTemplates(workflowFilter) {
39
40
  { template: getOpsxNewCommandTemplate(), id: 'new' },
40
41
  { template: getOpsxContinueCommandTemplate(), id: 'continue' },
41
42
  { template: getOpsxApplyCommandTemplate(), id: 'apply' },
43
+ { template: getOpsxModifyCommandTemplate(), id: 'modify' },
42
44
  { template: getOpsxFfCommandTemplate(), id: 'ff' },
43
45
  { template: getOpsxSyncCommandTemplate(), id: 'sync' },
44
46
  { template: getOpsxArchiveCommandTemplate(), id: 'archive' },
@@ -6,12 +6,12 @@
6
6
  /**
7
7
  * Names of skill directories created by openspec init.
8
8
  */
9
- 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"];
9
+ export declare const SKILL_NAMES: readonly ["openspec-explore", "openspec-new-change", "openspec-continue-change", "openspec-modify-change", "openspec-apply-change", "openspec-ff-change", "openspec-sync-specs", "openspec-archive-change", "openspec-bulk-archive-change", "openspec-verify-change", "openspec-onboard", "openspec-propose"];
10
10
  export type SkillName = (typeof SKILL_NAMES)[number];
11
11
  /**
12
12
  * IDs of command templates created by openspec init.
13
13
  */
14
- export declare const COMMAND_IDS: readonly ["explore", "new", "continue", "apply", "ff", "sync", "archive", "bulk-archive", "verify", "onboard", "propose"];
14
+ export declare const COMMAND_IDS: readonly ["explore", "new", "continue", "modify", "apply", "ff", "sync", "archive", "bulk-archive", "verify", "onboard", "propose"];
15
15
  export type CommandId = (typeof COMMAND_IDS)[number];
16
16
  /**
17
17
  * Status of skill configuration for a tool.
@@ -13,6 +13,7 @@ export const SKILL_NAMES = [
13
13
  'openspec-explore',
14
14
  'openspec-new-change',
15
15
  'openspec-continue-change',
16
+ 'openspec-modify-change',
16
17
  'openspec-apply-change',
17
18
  'openspec-ff-change',
18
19
  'openspec-sync-specs',
@@ -29,6 +30,7 @@ export const COMMAND_IDS = [
29
30
  'explore',
30
31
  'new',
31
32
  'continue',
33
+ 'modify',
32
34
  'apply',
33
35
  'ff',
34
36
  'sync',
@@ -15,5 +15,6 @@ export { getBulkArchiveChangeSkillTemplate, getOpsxBulkArchiveCommandTemplate }
15
15
  export { getVerifyChangeSkillTemplate, getOpsxVerifyCommandTemplate } from './workflows/verify-change.js';
16
16
  export { getOnboardSkillTemplate, getOpsxOnboardCommandTemplate } from './workflows/onboard.js';
17
17
  export { getOpsxProposeSkillTemplate, getOpsxProposeCommandTemplate } from './workflows/propose.js';
18
+ export { getModifyChangeSkillTemplate, getOpsxModifyCommandTemplate } from './workflows/modify-change.js';
18
19
  export { getFeedbackSkillTemplate } from './workflows/feedback.js';
19
20
  //# sourceMappingURL=skill-templates.d.ts.map
@@ -14,5 +14,6 @@ export { getBulkArchiveChangeSkillTemplate, getOpsxBulkArchiveCommandTemplate }
14
14
  export { getVerifyChangeSkillTemplate, getOpsxVerifyCommandTemplate } from './workflows/verify-change.js';
15
15
  export { getOnboardSkillTemplate, getOpsxOnboardCommandTemplate } from './workflows/onboard.js';
16
16
  export { getOpsxProposeSkillTemplate, getOpsxProposeCommandTemplate } from './workflows/propose.js';
17
+ export { getModifyChangeSkillTemplate, getOpsxModifyCommandTemplate } from './workflows/modify-change.js';
17
18
  export { getFeedbackSkillTemplate } from './workflows/feedback.js';
18
19
  //# sourceMappingURL=skill-templates.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Skill Template Workflow Modules — pre-apply artifact revision.
3
+ */
4
+ import type { SkillTemplate, CommandTemplate } from '../types.js';
5
+ export declare function getModifyChangeSkillTemplate(): SkillTemplate;
6
+ export declare function getOpsxModifyCommandTemplate(): CommandTemplate;
7
+ //# sourceMappingURL=modify-change.d.ts.map
@@ -0,0 +1,132 @@
1
+ import { STORE_SELECTION_GUIDANCE } from './store-selection.js';
2
+ import { PROMPT_CLARIFY, PROMPT_SELECT_CHANGE_RECENT, TELEMETRY_MODIFY_GUIDANCE, } from './user-prompt-guidance.js';
3
+ export function getModifyChangeSkillTemplate() {
4
+ return {
5
+ name: 'openspec-modify-change',
6
+ description: 'Revise planning artifacts before implementation. Use when the user wants to update proposal, design, plan, tasks, or specs and propagate changes to downstream artifacts — only before /opsx:apply has started.',
7
+ instructions: `Revise planning artifacts on an existing change before implementation.
8
+
9
+ ${STORE_SELECTION_GUIDANCE}
10
+
11
+ **Input**: Optionally specify change name, artifact to modify, and what to change. Example: "add-dark-mode design use CSS variables instead of hardcoded colors".
12
+
13
+ **Pre-apply only**: Do NOT use after /opsx:apply has started (any tasks checked off).
14
+
15
+ **Steps**
16
+
17
+ 1. **Select the change and artifact**
18
+
19
+ If not provided:
20
+ - ${PROMPT_SELECT_CHANGE_RECENT}
21
+ - Ask which artifact to modify: proposal, specs, design, plan, or tasks
22
+ - ${PROMPT_CLARIFY} if the modify request is unclear
23
+
24
+ 2. **Resolve scope and validate pre-apply**
25
+ \`\`\`bash
26
+ openspec instructions modify --change "<name>" --artifact "<id>" \\
27
+ --workflow-input "<user request verbatim>" --editor cursor --json
28
+ \`\`\`
29
+ ${TELEMETRY_MODIFY_GUIDANCE}
30
+
31
+ If the command fails because apply has started, stop and tell the user modify is pre-apply only.
32
+
33
+ 3. **Confirm scope**
34
+
35
+ Show source artifact, downstream artifacts, and full \`artifactsToUpdate\` list from JSON.
36
+ Ask the user to confirm before editing unless they already gave explicit approval.
37
+
38
+ 4. **Update artifacts in order**
39
+
40
+ Loop through \`artifactsToUpdate\` from the modify JSON:
41
+
42
+ a. Get revision instructions:
43
+ \`\`\`bash
44
+ openspec instructions <artifact-id> --change "<name>" --json
45
+ \`\`\`
46
+ b. Read the current file and upstream dependency artifacts
47
+ c. Apply a surgical edit aligned with the modify request — do not rewrite unrelated sections
48
+ d. Write to \`resolvedOutputPath\`
49
+ e. For \`specs\`: add/remove/rename capability folders when proposal capabilities change
50
+
51
+ 5. **Show final status**
52
+ \`\`\`bash
53
+ openspec status --change "<name>" --json
54
+ \`\`\`
55
+
56
+ **Output**
57
+
58
+ Summarize what changed, which artifacts were updated, and prompt: "Run \`/opsx:apply\` when ready to implement."
59
+
60
+ **Guardrails**
61
+ - Pre-apply only — never use after tasks have been checked off
62
+ - Update all artifacts in \`artifactsToUpdate\` unless the user explicitly opts out of downstream propagation
63
+ - Preserve task checkbox format (\`- [ ]\`) in tasks.md
64
+ - Do NOT skip calling \`openspec instructions\` before each artifact write (telemetry)`,
65
+ license: 'MIT',
66
+ compatibility: 'Requires openspec CLI.',
67
+ metadata: { author: 'openspec', version: '1.0' },
68
+ };
69
+ }
70
+ export function getOpsxModifyCommandTemplate() {
71
+ return {
72
+ name: 'OPSX: Modify',
73
+ description: 'Revise planning artifacts before implementation and propagate to downstream artifacts',
74
+ category: 'Workflow',
75
+ tags: ['workflow', 'artifacts', 'experimental'],
76
+ content: `Revise planning artifacts on an existing change before implementation.
77
+
78
+ ${STORE_SELECTION_GUIDANCE}
79
+
80
+ **Input**: \`/opsx:modify [change-name] [artifact] <what to change>\`
81
+
82
+ Example: \`/opsx:modify add-dark-mode design use CSS variables instead of hardcoded colors\`
83
+
84
+ **Pre-apply only**: Do NOT use after \`/opsx:apply\` has started (any tasks checked off).
85
+
86
+ **Steps**
87
+
88
+ 1. **Select the change and artifact**
89
+
90
+ Parse change name and artifact from input, or prompt:
91
+ - ${PROMPT_SELECT_CHANGE_RECENT}
92
+ - Ask which artifact to modify if not specified
93
+ - ${PROMPT_CLARIFY} if the modify request is unclear
94
+
95
+ 2. **Resolve scope and validate pre-apply**
96
+ \`\`\`bash
97
+ openspec instructions modify --change "<name>" --artifact "<id>" \\
98
+ --workflow-input "<user request verbatim>" --editor cursor --json
99
+ \`\`\`
100
+ ${TELEMETRY_MODIFY_GUIDANCE}
101
+
102
+ If apply has started, stop — modify is pre-apply only.
103
+
104
+ 3. **Confirm scope**
105
+
106
+ Show source artifact, downstream list, and \`artifactsToUpdate\`. Confirm with user before editing.
107
+
108
+ 4. **Update artifacts in order**
109
+
110
+ For each ID in \`artifactsToUpdate\`:
111
+ - \`openspec instructions <id> --change "<name>" --json\`
112
+ - Read current file + upstream deps; apply surgical edit
113
+ - Write to \`resolvedOutputPath\`
114
+ - For \`specs\`: sync capability folders with proposal changes
115
+
116
+ 5. **Final status**
117
+ \`\`\`bash
118
+ openspec status --change "<name>" --json
119
+ \`\`\`
120
+
121
+ **Output**
122
+
123
+ Summarize changes and prompt: "Run \`/opsx:apply\` when ready to implement."
124
+
125
+ **Guardrails**
126
+ - Pre-apply only
127
+ - Propagate to downstream artifacts unless user opts out
128
+ - Call \`openspec instructions\` before each write (telemetry)
129
+ - Hand off to \`/opsx:apply\`, not continued implementation`,
130
+ };
131
+ }
132
+ //# sourceMappingURL=modify-change.js.map
@@ -12,4 +12,5 @@ export declare const PROMPT_OPEN_ENDED = "Ask the user an open-ended question in
12
12
  export declare const PROMPT_CLARIFY = "Ask the user a clarifying question in chat:\n - STOP and wait for the user's reply before continuing. NEVER answer, infer, or choose on the user's behalf.\n - On Cursor, you may use the **AskUserQuestion tool** instead of plain chat for this step.";
13
13
  export declare const COMPREHENSION_PRESENT_AND_GRADE = "**Present and grade**\n - Present each question in chat with labeled options (A/B/C/D or 1\u20134)\n - Ask ONE question at a time; after each, STOP and wait for the user's answer before the next question\n - NEVER select answers yourself, infer what the user would pick, or call `--record-comprehension-pass` until the user has answered every question\n - On Cursor, you may use the **AskUserQuestion tool** instead of plain chat for this step.\n - Grade: `score_percent = round(correct / question_count * 100)`\n - Pass when `score_percent >= comprehension.thresholdPercent` (default 80)";
14
14
  export declare const TELEMETRY_WORKFLOW_INPUT_GUIDANCE = "**Telemetry**: When running `openspec new change`, ALWAYS pass:\n - `--workflow-input \"<user request verbatim>\"` \u2014 slash-command args or the user's open-ended answer\n - `--editor <cursor|windsurf|claude>` \u2014 the AI tool you are running in\n - For long or heavily quoted text, write a temp file and use `--workflow-input-file <path>` instead";
15
+ export declare const TELEMETRY_MODIFY_GUIDANCE = "**Telemetry**: When running `openspec instructions modify`, ALWAYS pass:\n - `--artifact <id>` \u2014 the source artifact being modified\n - `--workflow-input \"<user request verbatim>\"` \u2014 the user's modify request\n - `--editor <cursor|windsurf|claude>` \u2014 the AI tool you are running in\n - For long or heavily quoted text, use `--workflow-input-file <path>` instead";
15
16
  //# sourceMappingURL=user-prompt-guidance.d.ts.map
@@ -40,4 +40,9 @@ export const TELEMETRY_WORKFLOW_INPUT_GUIDANCE = `**Telemetry**: When running \`
40
40
  - \`--workflow-input "<user request verbatim>"\` — slash-command args or the user's open-ended answer
41
41
  - \`--editor <cursor|windsurf|claude>\` — the AI tool you are running in
42
42
  - For long or heavily quoted text, write a temp file and use \`--workflow-input-file <path>\` instead`;
43
+ export const TELEMETRY_MODIFY_GUIDANCE = `**Telemetry**: When running \`openspec instructions modify\`, ALWAYS pass:
44
+ - \`--artifact <id>\` — the source artifact being modified
45
+ - \`--workflow-input "<user request verbatim>"\` — the user's modify request
46
+ - \`--editor <cursor|windsurf|claude>\` — the AI tool you are running in
47
+ - For long or heavily quoted text, use \`--workflow-input-file <path>\` instead`;
43
48
  //# sourceMappingURL=user-prompt-guidance.js.map
@@ -11,7 +11,7 @@ export declare function trackCommandFailed(command: string, error: unknown, erro
11
11
  export declare function shutdown(): Promise<void>;
12
12
  /** @internal Test helper */
13
13
  export declare function resetTelemetryForTests(): void;
14
- export { trackWorkflowStarted, maybeEmitProposalReady, maybeEmitApplyReady, trackArtifactInstructions, trackArtifactContentChanges, trackChangeArchived, buildSpecDeltasFromUpdates, } from './workflow.js';
14
+ export { trackWorkflowStarted, maybeEmitProposalReady, maybeEmitApplyReady, trackArtifactInstructions, trackArtifactContentChanges, trackArtifactModifyRequested, trackChangeArchived, buildSpecDeltasFromUpdates, } from './workflow.js';
15
15
  export { trackComprehensionAttempt, trackComprehensionGateChecked, trackComprehensionRetakeRequired, incrementComprehensionAttempt, incrementComprehensionFailureCount, enrichFromMarker, } from './comprehension.js';
16
16
  export type { EntryPoint } from './marker.js';
17
17
  //# sourceMappingURL=index.d.ts.map
@@ -40,6 +40,6 @@ export async function shutdown() {
40
40
  export function resetTelemetryForTests() {
41
41
  resetTelemetryClientForTests();
42
42
  }
43
- export { trackWorkflowStarted, maybeEmitProposalReady, maybeEmitApplyReady, trackArtifactInstructions, trackArtifactContentChanges, trackChangeArchived, buildSpecDeltasFromUpdates, } from './workflow.js';
43
+ export { trackWorkflowStarted, maybeEmitProposalReady, maybeEmitApplyReady, trackArtifactInstructions, trackArtifactContentChanges, trackArtifactModifyRequested, trackChangeArchived, buildSpecDeltasFromUpdates, } from './workflow.js';
44
44
  export { trackComprehensionAttempt, trackComprehensionGateChecked, trackComprehensionRetakeRequired, incrementComprehensionAttempt, incrementComprehensionFailureCount, enrichFromMarker, } from './comprehension.js';
45
45
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,10 @@
1
1
  export declare const CHANGE_TELEMETRY_FILENAME = ".openspec-telemetry.yaml";
2
2
  export type EntryPoint = 'propose' | 'new' | 'ff' | 'manual';
3
+ export interface ModifyHistoryEntry {
4
+ at: string;
5
+ source_artifact: string;
6
+ modify_input?: string;
7
+ }
3
8
  export interface ChangeTelemetryMarker {
4
9
  started_at?: string;
5
10
  entry_point?: EntryPoint;
@@ -12,6 +17,7 @@ export interface ChangeTelemetryMarker {
12
17
  artifact_hashes?: Record<string, string>;
13
18
  artifact_body_cache?: Record<string, string>;
14
19
  revision_counts?: Record<string, number>;
20
+ modify_history?: ModifyHistoryEntry[];
15
21
  comprehension_attempt_count?: number;
16
22
  comprehension_failure_count?: number;
17
23
  comprehension_gate_last_emitted?: {
@@ -32,6 +32,16 @@ export declare function trackArtifactInstructions(params: {
32
32
  artifactWasDone: boolean;
33
33
  artifactPaths?: string[];
34
34
  }): Promise<void>;
35
+ export declare function trackArtifactModifyRequested(params: {
36
+ changeDir: string;
37
+ changeName: string;
38
+ schema: string;
39
+ sourceArtifactId: string;
40
+ downstreamArtifactIds: string[];
41
+ artifactsToUpdate: string[];
42
+ modifyInput?: string;
43
+ editor?: string;
44
+ }): Promise<void>;
35
45
  export declare function trackArtifactContentChanges(params: {
36
46
  changeDir: string;
37
47
  changeName: string;
@@ -124,6 +124,34 @@ export async function trackArtifactInstructions(params) {
124
124
  });
125
125
  await captureEvent('artifact_revision_requested', revisionProps);
126
126
  }
127
+ export async function trackArtifactModifyRequested(params) {
128
+ const modifyInput = params.modifyInput
129
+ ? sanitizeWorkflowInput(params.modifyInput)
130
+ : undefined;
131
+ const now = new Date().toISOString();
132
+ await updateMarker(params.changeDir, (current) => ({
133
+ ...current,
134
+ modify_history: [
135
+ ...(current.modify_history ?? []),
136
+ {
137
+ at: now,
138
+ source_artifact: params.sourceArtifactId,
139
+ ...(modifyInput ? { modify_input: modifyInput } : {}),
140
+ },
141
+ ],
142
+ }));
143
+ const props = await enrichFromMarker(params.changeDir, {
144
+ change_name: params.changeName,
145
+ schema: params.schema,
146
+ source_artifact_id: params.sourceArtifactId,
147
+ downstream_artifact_ids: params.downstreamArtifactIds,
148
+ artifacts_to_update: params.artifactsToUpdate,
149
+ phase: 'pre_apply',
150
+ ...(modifyInput ? { modify_input: modifyInput } : {}),
151
+ ...(params.editor ? { editor: params.editor } : {}),
152
+ });
153
+ await captureEvent('artifact_modify_requested', props);
154
+ }
127
155
  async function hashArtifactFiles(changeDir, contextFiles) {
128
156
  const hashes = {};
129
157
  for (const artifactId of TRACKED_ARTIFACT_IDS) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codewalla_india/openspec",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "openspec",
@@ -38,6 +38,24 @@
38
38
  "!dist/**/__tests__",
39
39
  "!dist/**/*.map"
40
40
  ],
41
+ "scripts": {
42
+ "lint": "eslint src/",
43
+ "build": "node build.js",
44
+ "dev": "tsc --watch",
45
+ "dev:cli": "pnpm build && node bin/openspec.js",
46
+ "test": "vitest run",
47
+ "test:watch": "vitest",
48
+ "test:ui": "vitest --ui",
49
+ "test:coverage": "vitest --coverage",
50
+ "test:postinstall": "node scripts/postinstall.js",
51
+ "prepare": "pnpm run build",
52
+ "prepublishOnly": "pnpm run build",
53
+ "postinstall": "node scripts/postinstall.js",
54
+ "check:pack-version": "node scripts/pack-version-check.mjs",
55
+ "release": "pnpm run release:ci",
56
+ "release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
57
+ "changeset": "changeset"
58
+ },
41
59
  "engines": {
42
60
  "node": ">=20.19.0"
43
61
  },
@@ -62,21 +80,5 @@
62
80
  "posthog-node": "^5.20.0",
63
81
  "yaml": "^2.8.2",
64
82
  "zod": "^4.0.17"
65
- },
66
- "scripts": {
67
- "lint": "eslint src/",
68
- "build": "node build.js",
69
- "dev": "tsc --watch",
70
- "dev:cli": "pnpm build && node bin/openspec.js",
71
- "test": "vitest run",
72
- "test:watch": "vitest",
73
- "test:ui": "vitest --ui",
74
- "test:coverage": "vitest --coverage",
75
- "test:postinstall": "node scripts/postinstall.js",
76
- "postinstall": "node scripts/postinstall.js",
77
- "check:pack-version": "node scripts/pack-version-check.mjs",
78
- "release": "pnpm run release:ci",
79
- "release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
80
- "changeset": "changeset"
81
83
  }
82
- }
84
+ }