@codewalla_india/openspec 1.1.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.
Files changed (42) hide show
  1. package/dist/cli/index.js +11 -4
  2. package/dist/commands/workflow/index.d.ts +2 -2
  3. package/dist/commands/workflow/index.js +1 -1
  4. package/dist/commands/workflow/instructions.d.ts +21 -1
  5. package/dist/commands/workflow/instructions.js +197 -32
  6. package/dist/commands/workflow/shared.d.ts +2 -0
  7. package/dist/core/artifact-graph/graph.d.ts +9 -0
  8. package/dist/core/artifact-graph/graph.js +37 -7
  9. package/dist/core/change-status-policy.js +1 -1
  10. package/dist/core/completions/command-registry.js +20 -0
  11. package/dist/core/profile-sync-drift.js +1 -0
  12. package/dist/core/profiles.d.ts +2 -2
  13. package/dist/core/profiles.js +2 -1
  14. package/dist/core/shared/skill-generation.js +3 -1
  15. package/dist/core/shared/tool-detection.d.ts +2 -2
  16. package/dist/core/shared/tool-detection.js +2 -0
  17. package/dist/core/templates/skill-templates.d.ts +1 -0
  18. package/dist/core/templates/skill-templates.js +1 -0
  19. package/dist/core/templates/workflows/modify-change.d.ts +7 -0
  20. package/dist/core/templates/workflows/modify-change.js +132 -0
  21. package/dist/core/templates/workflows/user-prompt-guidance.d.ts +1 -0
  22. package/dist/core/templates/workflows/user-prompt-guidance.js +5 -0
  23. package/dist/telemetry/caller.d.ts +5 -0
  24. package/dist/telemetry/caller.js +29 -0
  25. package/dist/telemetry/client.d.ts +5 -1
  26. package/dist/telemetry/client.js +11 -2
  27. package/dist/telemetry/command-context.d.ts +13 -0
  28. package/dist/telemetry/command-context.js +59 -0
  29. package/dist/telemetry/comprehension.d.ts +44 -0
  30. package/dist/telemetry/comprehension.js +105 -0
  31. package/dist/telemetry/content.d.ts +10 -0
  32. package/dist/telemetry/content.js +56 -0
  33. package/dist/telemetry/identify-cache.d.ts +7 -0
  34. package/dist/telemetry/identify-cache.js +47 -0
  35. package/dist/telemetry/index.d.ts +7 -3
  36. package/dist/telemetry/index.js +12 -3
  37. package/dist/telemetry/input.d.ts +3 -0
  38. package/dist/telemetry/input.js +15 -3
  39. package/dist/telemetry/marker.d.ts +13 -0
  40. package/dist/telemetry/workflow.d.ts +14 -2
  41. package/dist/telemetry/workflow.js +87 -12
  42. package/package.json +20 -18
package/dist/cli/index.js CHANGED
@@ -23,8 +23,9 @@ 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
+ import { buildCommandTelemetryContext, resolveTelemetryCommandPath, } from '../telemetry/command-context.js';
28
29
  import { COMMON_FLAGS } from '../core/completions/shared-flags.js';
29
30
  const STORE_OPTION_DESCRIPTION = COMMON_FLAGS.store.description;
30
31
  // Deliberate rejection path: --store-path stays registered (hidden) so the
@@ -112,7 +113,7 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
112
113
  if (opts.color === false) {
113
114
  process.env.NO_COLOR = '1';
114
115
  }
115
- const commandPath = getCommandPath(actionCommand);
116
+ const commandPath = resolveTelemetryCommandPath(getCommandPath(actionCommand), actionCommand);
116
117
  const isBootstrap = commandPath === 'init' || commandPath === 'update';
117
118
  if (!isBootstrap) {
118
119
  try {
@@ -125,7 +126,7 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
125
126
  throw error;
126
127
  }
127
128
  }
128
- await trackCommand(commandPath, version);
129
+ await trackCommand(commandPath, version, buildCommandTelemetryContext(actionCommand));
129
130
  });
130
131
  // Shutdown telemetry after command completes
131
132
  program.hook('postAction', async () => {
@@ -504,14 +505,20 @@ program
504
505
  .option('--score <percent>', 'Quiz score 0-100 (required with --record-comprehension-pass)', parseInt)
505
506
  .option('--attempt <n>', 'Quiz attempt number', parseInt)
506
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)')
507
512
  .option('--store <id>', STORE_OPTION_DESCRIPTION)
508
513
  .addOption(hiddenStorePathOption())
509
514
  .action(async (artifactId, options) => {
510
515
  try {
511
- // Special case: "apply" is not an artifact, but a command to get apply instructions
512
516
  if (artifactId === 'apply') {
513
517
  await applyInstructionsCommand(options);
514
518
  }
519
+ else if (artifactId === 'modify') {
520
+ await modifyInstructionsCommand(options);
521
+ }
515
522
  else {
516
523
  await instructionsCommand(artifactId, options);
517
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
- import { checkComprehensionGate, ComprehensionPassError, recordComprehensionPass, } from '../../core/comprehension/index.js';
17
- import { trackEvent, maybeEmitProposalReady, maybeEmitApplyReady, trackArtifactInstructions, trackArtifactContentChanges, trackComprehensionRetakeRequired, } from '../../telemetry/index.js';
16
+ import { checkComprehensionGate, ComprehensionPassError, computeSpecStats, recordComprehensionPass, resolveComprehensionConfig, } from '../../core/comprehension/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 {
@@ -101,6 +102,7 @@ export async function instructionsCommand(artifactId, options) {
101
102
  changeName,
102
103
  artifactId,
103
104
  artifactWasDone: artifactOutputs.length > 0,
105
+ artifactPaths: artifactOutputs,
104
106
  });
105
107
  const contextFiles = {};
106
108
  for (const a of context.graph.getAllArtifacts()) {
@@ -244,6 +246,27 @@ function parseTasksFile(content) {
244
246
  }
245
247
  return tasks;
246
248
  }
249
+ function resolveComprehensionQuestionCount(optionCount, specPaths, projectConfig, pendingTaskCount, artifactPresence) {
250
+ if (optionCount !== undefined && optionCount > 0) {
251
+ return optionCount;
252
+ }
253
+ const config = resolveComprehensionConfig(projectConfig);
254
+ return computeSpecStats(specPaths, config, pendingTaskCount, artifactPresence).questionCount;
255
+ }
256
+ async function emitComprehensionAttemptAfterPass(params) {
257
+ await trackComprehensionAttempt({
258
+ changeDir: params.changeDir,
259
+ changeName: params.changeName,
260
+ attempt: params.attempt,
261
+ scorePercent: params.scorePercent,
262
+ thresholdPercent: params.thresholdPercent,
263
+ questionCount: params.questionCount,
264
+ passed: true,
265
+ failureCountBefore: params.failureCountBefore,
266
+ nextMilestone: params.applyReadyEmitted ? 'apply_ready' : undefined,
267
+ contextFiles: params.contextFiles,
268
+ });
269
+ }
247
270
  /**
248
271
  * Generates apply instructions for implementing tasks from a change.
249
272
  * Schema-aware: reads apply phase configuration from schema to determine
@@ -331,6 +354,7 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
331
354
  }
332
355
  let missingComprehension;
333
356
  let comprehension;
357
+ let applyReadyEmitted = false;
334
358
  await trackArtifactContentChanges({ changeDir, changeName, contextFiles });
335
359
  await maybeEmitProposalReady({
336
360
  changeDir,
@@ -338,6 +362,7 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
338
362
  schema: context.schemaName,
339
363
  missingArtifacts,
340
364
  artifactCount: schema.artifacts.length,
365
+ contextFiles,
341
366
  });
342
367
  if (state === 'ready') {
343
368
  const specPaths = contextFiles.specs ?? [];
@@ -355,17 +380,27 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
355
380
  else if (gate.active && gate.info) {
356
381
  comprehension = gate.info;
357
382
  }
358
- await maybeEmitApplyReady({ changeDir, changeName, state });
383
+ applyReadyEmitted = await maybeEmitApplyReady({
384
+ changeDir,
385
+ changeName,
386
+ state,
387
+ contextFiles,
388
+ });
359
389
  if (gate.active && gate.info) {
360
- await trackEvent('comprehension_gate_checked', {
361
- change_name: changeName,
362
- required: true,
390
+ await trackComprehensionGateChecked({
391
+ changeDir,
392
+ changeName,
363
393
  passed: gate.passed,
364
- threshold_percent: gate.info.thresholdPercent,
365
- question_count: gate.info.questionCount,
394
+ gateInfo: gate.info,
395
+ state: gate.passed ? 'ready' : 'blocked',
396
+ contextFiles,
366
397
  });
367
398
  if (!gate.passed && gate.info.bestScorePercent !== undefined) {
368
- await trackComprehensionRetakeRequired(changeName);
399
+ await trackComprehensionRetakeRequired({
400
+ changeDir,
401
+ changeName,
402
+ gateInfo: gate.info,
403
+ });
369
404
  }
370
405
  }
371
406
  }
@@ -381,6 +416,7 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
381
416
  missingComprehension,
382
417
  comprehension,
383
418
  instruction,
419
+ applyReadyEmitted,
384
420
  ...(references !== undefined ? { references } : {}),
385
421
  };
386
422
  }
@@ -432,6 +468,8 @@ export async function applyInstructionsCommand(options) {
432
468
  }
433
469
  }
434
470
  const artifactPresence = buildArtifactPresence(contextFilesForPresence, pendingTaskCount);
471
+ const questionCount = resolveComprehensionQuestionCount(options.questionCount, specPaths, projectConfig, pendingTaskCount, artifactPresence);
472
+ const { attempt, failureCountBefore } = await incrementComprehensionAttempt(changeDir);
435
473
  try {
436
474
  const record = recordComprehensionPass({
437
475
  changeDir,
@@ -440,24 +478,29 @@ export async function applyInstructionsCommand(options) {
440
478
  planPath,
441
479
  projectConfig,
442
480
  scorePercent: options.score,
443
- attempt: options.attempt ?? 1,
444
- questionCount: options.questionCount ?? 0,
481
+ attempt,
482
+ questionCount,
445
483
  pendingTaskCount,
446
484
  artifactPresence,
447
485
  });
448
- await trackEvent('comprehension_pass_recorded', {
449
- change_name: changeName,
450
- score_percent: record.score_percent,
486
+ spinner?.stop();
487
+ const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema, {
488
+ planningHome,
489
+ references,
490
+ projectConfig,
491
+ });
492
+ await emitComprehensionAttemptAfterPass({
493
+ changeDir,
494
+ changeName,
451
495
  attempt: record.attempt,
452
- question_count: options.questionCount ?? 0,
496
+ failureCountBefore,
497
+ scorePercent: record.score_percent,
498
+ thresholdPercent: record.threshold_percent,
499
+ questionCount: record.question_count,
500
+ contextFiles: instructions.contextFiles,
501
+ applyReadyEmitted: instructions.applyReadyEmitted ?? false,
453
502
  });
454
- spinner?.stop();
455
503
  if (options.json) {
456
- const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema, {
457
- planningHome,
458
- references,
459
- projectConfig,
460
- });
461
504
  console.log(JSON.stringify({
462
505
  recorded: true,
463
506
  comprehensionPass: record,
@@ -467,21 +510,23 @@ export async function applyInstructionsCommand(options) {
467
510
  return;
468
511
  }
469
512
  console.log(`Comprehension pass recorded (${record.score_percent}%).`);
470
- const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema, {
471
- planningHome,
472
- references,
473
- projectConfig,
474
- });
475
513
  printApplyInstructionsText(instructions);
476
514
  return;
477
515
  }
478
516
  catch (error) {
479
517
  spinner?.stop();
480
518
  if (error instanceof ComprehensionPassError) {
481
- await trackEvent('comprehension_pass_failed', {
482
- change_name: changeName,
483
- score_percent: options.score,
484
- attempt: options.attempt ?? 1,
519
+ await incrementComprehensionFailureCount(changeDir);
520
+ await trackComprehensionAttempt({
521
+ changeDir,
522
+ changeName,
523
+ attempt,
524
+ scorePercent: options.score,
525
+ thresholdPercent: error.threshold,
526
+ questionCount,
527
+ passed: false,
528
+ failureCountBefore,
529
+ contextFiles: contextFilesForPresence,
485
530
  });
486
531
  if (options.json) {
487
532
  console.log(JSON.stringify({
@@ -590,4 +635,124 @@ export function printApplyInstructionsText(instructions) {
590
635
  console.log('### Instruction');
591
636
  console.log(instruction);
592
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
+ }
593
758
  //# sourceMappingURL=instructions.js.map
@@ -48,6 +48,8 @@ export interface ApplyInstructions {
48
48
  instruction: string;
49
49
  /** Referenced-store index (read-only upstream context; omitted when none declared) */
50
50
  references?: ReferenceIndexEntry[];
51
+ /** True when apply_ready telemetry was emitted during this generation */
52
+ applyReadyEmitted?: boolean;
51
53
  }
52
54
  export declare const DEFAULT_SCHEMA = "spec-driven";
53
55
  export declare function printJson(payload: unknown): void;
@@ -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.