@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.
- package/dist/cli/index.js +11 -4
- package/dist/commands/workflow/index.d.ts +2 -2
- package/dist/commands/workflow/index.js +1 -1
- package/dist/commands/workflow/instructions.d.ts +21 -1
- package/dist/commands/workflow/instructions.js +197 -32
- package/dist/commands/workflow/shared.d.ts +2 -0
- package/dist/core/artifact-graph/graph.d.ts +9 -0
- package/dist/core/artifact-graph/graph.js +37 -7
- package/dist/core/change-status-policy.js +1 -1
- package/dist/core/completions/command-registry.js +20 -0
- package/dist/core/profile-sync-drift.js +1 -0
- package/dist/core/profiles.d.ts +2 -2
- package/dist/core/profiles.js +2 -1
- package/dist/core/shared/skill-generation.js +3 -1
- package/dist/core/shared/tool-detection.d.ts +2 -2
- package/dist/core/shared/tool-detection.js +2 -0
- package/dist/core/templates/skill-templates.d.ts +1 -0
- package/dist/core/templates/skill-templates.js +1 -0
- package/dist/core/templates/workflows/modify-change.d.ts +7 -0
- package/dist/core/templates/workflows/modify-change.js +132 -0
- package/dist/core/templates/workflows/user-prompt-guidance.d.ts +1 -0
- package/dist/core/templates/workflows/user-prompt-guidance.js +5 -0
- package/dist/telemetry/caller.d.ts +5 -0
- package/dist/telemetry/caller.js +29 -0
- package/dist/telemetry/client.d.ts +5 -1
- package/dist/telemetry/client.js +11 -2
- package/dist/telemetry/command-context.d.ts +13 -0
- package/dist/telemetry/command-context.js +59 -0
- package/dist/telemetry/comprehension.d.ts +44 -0
- package/dist/telemetry/comprehension.js +105 -0
- package/dist/telemetry/content.d.ts +10 -0
- package/dist/telemetry/content.js +56 -0
- package/dist/telemetry/identify-cache.d.ts +7 -0
- package/dist/telemetry/identify-cache.js +47 -0
- package/dist/telemetry/index.d.ts +7 -3
- package/dist/telemetry/index.js +12 -3
- package/dist/telemetry/input.d.ts +3 -0
- package/dist/telemetry/input.js +15 -3
- package/dist/telemetry/marker.d.ts +13 -0
- package/dist/telemetry/workflow.d.ts +14 -2
- package/dist/telemetry/workflow.js +87 -12
- 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 {
|
|
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({
|
|
383
|
+
applyReadyEmitted = await maybeEmitApplyReady({
|
|
384
|
+
changeDir,
|
|
385
|
+
changeName,
|
|
386
|
+
state,
|
|
387
|
+
contextFiles,
|
|
388
|
+
});
|
|
359
389
|
if (gate.active && gate.info) {
|
|
360
|
-
await
|
|
361
|
-
|
|
362
|
-
|
|
390
|
+
await trackComprehensionGateChecked({
|
|
391
|
+
changeDir,
|
|
392
|
+
changeName,
|
|
363
393
|
passed: gate.passed,
|
|
364
|
-
|
|
365
|
-
|
|
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(
|
|
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
|
|
444
|
-
questionCount
|
|
481
|
+
attempt,
|
|
482
|
+
questionCount,
|
|
445
483
|
pendingTaskCount,
|
|
446
484
|
artifactPresence,
|
|
447
485
|
});
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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('
|
|
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',
|
package/dist/core/profiles.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/core/profiles.js
CHANGED
|
@@ -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.
|