@fission-ai/openspec 1.11.0 → 1.13.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 (37) hide show
  1. package/README.md +3 -14
  2. package/dist/cli/index.js +1 -0
  3. package/dist/commands/change.js +6 -5
  4. package/dist/commands/validate.d.ts +57 -0
  5. package/dist/commands/validate.js +99 -13
  6. package/dist/commands/workflow/instructions.js +140 -5
  7. package/dist/commands/workflow/shared.d.ts +8 -0
  8. package/dist/core/command-generation/adapters/codeassistant.d.ts +15 -0
  9. package/dist/core/command-generation/adapters/codeassistant.js +29 -0
  10. package/dist/core/command-generation/adapters/index.d.ts +1 -0
  11. package/dist/core/command-generation/adapters/index.js +1 -0
  12. package/dist/core/command-generation/registry.js +2 -0
  13. package/dist/core/completions/command-registry.js +6 -0
  14. package/dist/core/config.js +1 -0
  15. package/dist/core/init.d.ts +1 -0
  16. package/dist/core/init.js +41 -34
  17. package/dist/core/onboarding-commands.d.ts +11 -1
  18. package/dist/core/onboarding-commands.js +26 -0
  19. package/dist/core/openspec-root.d.ts +1 -0
  20. package/dist/core/openspec-root.js +10 -2
  21. package/dist/core/parsers/requirement-blocks.js +77 -31
  22. package/dist/core/shared/ide-restart.d.ts +26 -0
  23. package/dist/core/shared/ide-restart.js +42 -0
  24. package/dist/core/shared/index.d.ts +1 -0
  25. package/dist/core/shared/index.js +1 -0
  26. package/dist/core/shared/tool-detection.js +28 -1
  27. package/dist/core/specs-apply.js +184 -8
  28. package/dist/core/templates/workflows/explore.js +45 -0
  29. package/dist/core/templates/workflows/ff-change.js +8 -0
  30. package/dist/core/templates/workflows/propose.js +40 -12
  31. package/dist/core/update.d.ts +22 -0
  32. package/dist/core/update.js +55 -17
  33. package/dist/core/validation/validator.d.ts +12 -3
  34. package/dist/core/validation/validator.js +74 -3
  35. package/dist/utils/command-references.js +7 -8
  36. package/package.json +3 -3
  37. package/schemas/spec-driven/schema.yaml +14 -2
package/README.md CHANGED
@@ -172,6 +172,7 @@ Both are in the default profile. If you want the expanded workflow (`/opsx:new`,
172
172
  → **[Concepts](docs/concepts.md)**: how it all fits<br>
173
173
  → **[Multi-Language](docs/multi-language.md)**: multi-language support<br>
174
174
  → **[Customization](docs/customization.md)**: make it yours<br>
175
+ → **[Community Showcase](docs/community.md)**: projects and resources built with and for OpenSpec<br>
175
176
  → **[FAQ](docs/faq.md)** · **[Troubleshooting](docs/troubleshooting.md)** · **[Glossary](docs/glossary.md)**: quick help
176
177
 
177
178
 
@@ -223,21 +224,9 @@ openspec update
223
224
 
224
225
  ## Contributing
225
226
 
226
- **Small fixes** — Bug fixes, typo corrections, and minor improvements can be submitted directly as PRs.
227
+ Open a discussion (for core design changes) or an issue before you open a PR, and link the issue or discussion from the PR. New features, significant refactors, and architectural changes need an OpenSpec change proposal first.
227
228
 
228
- **Larger changes** — For new features, significant refactors, or architectural changes, please submit an OpenSpec change proposal first so we can align on intent and goals before implementation begins.
229
-
230
- When writing proposals, keep the OpenSpec philosophy in mind: we serve a wide variety of users across different coding agents, models, and use cases. Changes should work well for everyone.
231
-
232
- **AI-generated code is welcome** — as long as it's been tested and verified. PRs containing AI-generated code should mention the coding agent and model used (e.g., "Generated with Claude Code using claude-opus-4-5-20251101").
233
-
234
- ### Development
235
-
236
- - Install dependencies: `pnpm install`
237
- - Build: `pnpm run build`
238
- - Test: `pnpm test`
239
- - Develop CLI locally: `pnpm run dev` or `pnpm run dev:cli`
240
- - Conventional commits (one-line): `type(scope): subject`
229
+ → **[CONTRIBUTING.md](CONTRIBUTING.md)**: the full process, from first issue to merged PR
241
230
 
242
231
  ## Other
243
232
 
package/dist/cli/index.js CHANGED
@@ -449,6 +449,7 @@ program
449
449
  .option('--changes', 'Validate all changes')
450
450
  .option('--specs', 'Validate all specs')
451
451
  .option('--archived', 'Validate that archived changes have all tasks completed (for pre-commit linting)')
452
+ .option('--report <full|findings>', 'Select bulk report content: full|findings; combine with --json for JSON')
452
453
  .option('--type <type>', 'Specify item type when ambiguous: change|spec')
453
454
  .option('--strict', 'Enable strict validation mode')
454
455
  .option('--json', 'Output validation results as JSON')
@@ -480,11 +480,12 @@ export class ChangeCommand {
480
480
  }
481
481
  else {
482
482
  console.error(`Change "${changeName}" has issues`);
483
- report.issues.forEach(issue => {
484
- const label = issue.level === 'ERROR' ? 'ERROR' : 'WARNING';
485
- const prefix = issue.level === 'ERROR' ? '✗' : '⚠';
486
- console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
487
- });
483
+ }
484
+ report.issues.forEach(issue => {
485
+ const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
486
+ console.error(`${prefix} [${issue.level}] ${issue.path}: ${issue.message}`);
487
+ });
488
+ if (!report.valid) {
488
489
  // Next steps footer to guide fixing issues
489
490
  this.printNextSteps(report.issues);
490
491
  if (!options?.json) {
@@ -1,8 +1,11 @@
1
+ import { toRootOutput } from '../core/root-selection.js';
2
+ type ItemType = 'change' | 'spec';
1
3
  interface ExecuteOptions {
2
4
  all?: boolean;
3
5
  changes?: boolean;
4
6
  specs?: boolean;
5
7
  archived?: boolean;
8
+ report?: string;
6
9
  type?: string;
7
10
  strict?: boolean;
8
11
  json?: boolean;
@@ -12,6 +15,58 @@ interface ExecuteOptions {
12
15
  store?: string;
13
16
  storePath?: string;
14
17
  }
18
+ interface BulkItemResult {
19
+ id: string;
20
+ type: ItemType;
21
+ valid: boolean;
22
+ issues: {
23
+ level: 'ERROR' | 'WARNING' | 'INFO';
24
+ path: string;
25
+ message: string;
26
+ }[];
27
+ durationMs: number;
28
+ }
29
+ type BulkScope = 'all' | 'changes' | 'specs' | 'archived';
30
+ interface BulkValidationResult<T extends BulkItemResult = BulkItemResult> {
31
+ items: T[];
32
+ summary: {
33
+ totals: {
34
+ items: number;
35
+ passed: number;
36
+ failed: number;
37
+ };
38
+ byType: Partial<Record<ItemType, {
39
+ items: number;
40
+ passed: number;
41
+ failed: number;
42
+ }>>;
43
+ };
44
+ root: ReturnType<typeof toRootOutput>;
45
+ }
46
+ /** Findings are a distinct report, not a partial full-v1 items collection. */
47
+ export declare function projectValidationFindings<T extends BulkItemResult>(full: BulkValidationResult<T>, scope: BulkScope): {
48
+ report: {
49
+ kind: "validation-findings";
50
+ version: "1.0";
51
+ scope: BulkScope;
52
+ returnedItems: number;
53
+ totalItems: number;
54
+ };
55
+ itemFindings: T[];
56
+ summary: {
57
+ totals: {
58
+ items: number;
59
+ passed: number;
60
+ failed: number;
61
+ };
62
+ byType: Partial<Record<ItemType, {
63
+ items: number;
64
+ passed: number;
65
+ failed: number;
66
+ }>>;
67
+ };
68
+ root: import("../core/root-selection.js").RootOutput;
69
+ };
15
70
  export declare class ValidateCommand {
16
71
  execute(itemName: string | undefined, options?: ExecuteOptions): Promise<void>;
17
72
  private normalizeType;
@@ -29,6 +84,8 @@ export declare class ValidateCommand {
29
84
  private validateByType;
30
85
  private printReport;
31
86
  private printNextSteps;
87
+ private printFindingsReport;
88
+ private printBulkDetails;
32
89
  private runBulkValidation;
33
90
  /**
34
91
  * Lists archived change ids from the resolved root's archive directory,
@@ -10,9 +10,54 @@ import { nearestMatches } from '../utils/match.js';
10
10
  import { promises as fs } from 'fs';
11
11
  import { getTaskProgressDetailForChange } from '../utils/task-progress.js';
12
12
  import { FileSystemUtils } from '../utils/file-system.js';
13
+ /** Findings are a distinct report, not a partial full-v1 items collection. */
14
+ export function projectValidationFindings(full, scope) {
15
+ const itemFindings = full.items.filter(item => item.issues.length > 0);
16
+ return {
17
+ report: {
18
+ kind: 'validation-findings',
19
+ version: '1.0',
20
+ scope,
21
+ returnedItems: itemFindings.length,
22
+ totalItems: full.summary.totals.items,
23
+ },
24
+ itemFindings,
25
+ summary: full.summary,
26
+ root: full.root,
27
+ };
28
+ }
13
29
  export class ValidateCommand {
14
30
  async execute(itemName, options = {}) {
15
31
  const bulk = options.all || options.changes || options.specs;
32
+ let findingsScope;
33
+ if (options.report !== undefined) {
34
+ const message = options.report !== 'full' && options.report !== 'findings'
35
+ ? `Unknown validation report '${options.report}'.`
36
+ : itemName !== undefined
37
+ ? 'A validation report cannot be combined with an item name.'
38
+ : options.archived && bulk
39
+ ? 'A validation report cannot combine archived and active scopes.'
40
+ : !options.archived && !bulk
41
+ ? 'A validation report requires an explicit bulk scope.'
42
+ : undefined;
43
+ if (message) {
44
+ const fix = 'Use --report full|findings with --all, --changes, --specs, or --archived, without an item name. Do not combine archived and active scopes.';
45
+ if (options.json) {
46
+ console.log(JSON.stringify({ status: [{ severity: 'error', code: 'invalid_validation_report_request', message, fix }] }, null, 2));
47
+ }
48
+ else {
49
+ console.error(`Error: ${message}`);
50
+ console.error(`Fix: ${fix}`);
51
+ }
52
+ process.exitCode = 1;
53
+ return;
54
+ }
55
+ if (options.report === 'findings') {
56
+ findingsScope = options.archived ? 'archived'
57
+ : options.all || (options.changes && options.specs) ? 'all'
58
+ : options.changes ? 'changes' : 'specs';
59
+ }
60
+ }
16
61
  const root = await resolveRootForCommand(options, {
17
62
  json: options.json,
18
63
  ...(bulk ? { allowImplicitRoot: false } : {}),
@@ -29,6 +74,7 @@ export class ValidateCommand {
29
74
  await this.runArchivedTaskValidation(root, {
30
75
  json: !!options.json,
31
76
  noInteractive: resolveNoInteractive(options),
77
+ findingsScope,
32
78
  });
33
79
  return;
34
80
  }
@@ -37,7 +83,7 @@ export class ValidateCommand {
37
83
  await this.runBulkValidation(root, {
38
84
  changes: !!options.all || !!options.changes,
39
85
  specs: !!options.all || !!options.specs,
40
- }, { strict: !!options.strict, json: !!options.json, concurrency: options.concurrency, noInteractive: resolveNoInteractive(options) });
86
+ }, { strict: !!options.strict, json: !!options.json, concurrency: options.concurrency, noInteractive: resolveNoInteractive(options), findingsScope });
41
87
  return;
42
88
  }
43
89
  // No item and no flags
@@ -191,11 +237,12 @@ export class ValidateCommand {
191
237
  }
192
238
  else {
193
239
  console.error(`${type === 'change' ? 'Change' : 'Specification'} '${id}' has issues`);
194
- for (const issue of report.issues) {
195
- const label = issue.level === 'ERROR' ? 'ERROR' : issue.level;
196
- const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
197
- console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
198
- }
240
+ }
241
+ for (const issue of report.issues) {
242
+ const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
243
+ console.error(`${prefix} [${issue.level}] ${issue.path}: ${issue.message}`);
244
+ }
245
+ if (!report.valid) {
199
246
  this.printNextSteps(type, id, root, report.issues);
200
247
  }
201
248
  }
@@ -228,6 +275,34 @@ export class ValidateCommand {
228
275
  console.error('Next steps:');
229
276
  bullets.forEach(b => console.error(` ${b}`));
230
277
  }
278
+ printFindingsReport(full, scope, json, root) {
279
+ const findings = projectValidationFindings(full, scope);
280
+ if (json) {
281
+ console.log(JSON.stringify(findings, null, 2));
282
+ return;
283
+ }
284
+ console.log(`Scope: ${scope} (${findings.report.totalItems} items)`);
285
+ if (findings.itemFindings.length === 0) {
286
+ console.log('No item findings.');
287
+ }
288
+ for (const item of findings.itemFindings) {
289
+ console.error(`${item.type}/${item.id}`);
290
+ for (const issue of item.issues) {
291
+ console.error(` [${issue.level}] ${issue.path}: ${issue.message}`);
292
+ }
293
+ }
294
+ const totals = findings.summary.totals;
295
+ console.log(`Totals: ${totals.passed} passed, ${totals.failed} failed (${totals.items} items)`);
296
+ if (scope !== 'archived')
297
+ this.printBulkDetails(full.items, root);
298
+ }
299
+ printBulkDetails(results, root) {
300
+ const firstFailure = results.find((res) => !res.valid);
301
+ if (firstFailure) {
302
+ const storeFlag = isStoreSelectedRoot(root) ? ` --store ${root.storeId}` : '';
303
+ console.log(`Details: openspec validate ${firstFailure.id} --type ${firstFailure.type}${storeFlag}`);
304
+ }
305
+ }
231
306
  async runBulkValidation(root, scope, opts) {
232
307
  const spinner = !opts.json && !opts.noInteractive ? ora('Validating...').start() : undefined;
233
308
  const [changeIds, specIds] = await Promise.all([
@@ -269,7 +344,10 @@ export class ValidateCommand {
269
344
  ...(scope.specs ? { spec: { items: 0, passed: 0, failed: 0 } } : {}),
270
345
  },
271
346
  };
272
- if (opts.json) {
347
+ if (opts.findingsScope) {
348
+ this.printFindingsReport({ items: [], summary, root: toRootOutput(root) }, opts.findingsScope, opts.json, root);
349
+ }
350
+ else if (opts.json) {
273
351
  const out = { items: [], summary, version: '1.0', root: toRootOutput(root) };
274
352
  console.log(JSON.stringify(out, null, 2));
275
353
  }
@@ -326,7 +404,10 @@ export class ValidateCommand {
326
404
  ...(scope.specs ? { spec: summarizeType(results, 'spec') } : {}),
327
405
  },
328
406
  };
329
- if (opts.json) {
407
+ if (opts.findingsScope) {
408
+ this.printFindingsReport({ items: results, summary, root: toRootOutput(root) }, opts.findingsScope, opts.json, root);
409
+ }
410
+ else if (opts.json) {
330
411
  const out = { items: results, summary, version: '1.0', root: toRootOutput(root) };
331
412
  console.log(JSON.stringify(out, null, 2));
332
413
  }
@@ -336,13 +417,13 @@ export class ValidateCommand {
336
417
  console.log(`✓ ${res.type}/${res.id}`);
337
418
  else
338
419
  console.error(`✗ ${res.type}/${res.id}`);
420
+ for (const issue of res.issues) {
421
+ const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
422
+ console.error(` ${prefix} [${issue.level}] ${issue.path}: ${issue.message}`);
423
+ }
339
424
  }
340
425
  console.log(`Totals: ${summary.totals.passed} passed, ${summary.totals.failed} failed (${summary.totals.items} items)`);
341
- const firstFailure = results.find((res) => !res.valid);
342
- if (firstFailure) {
343
- const storeFlag = isStoreSelectedRoot(root) ? ` --store ${root.storeId}` : '';
344
- console.log(`Details: openspec validate ${firstFailure.id} --type ${firstFailure.type}${storeFlag}`);
345
- }
426
+ this.printBulkDetails(results, root);
346
427
  }
347
428
  process.exitCode = failed > 0 ? 1 : 0;
348
429
  }
@@ -438,6 +519,11 @@ export class ValidateCommand {
438
519
  totals: { items: results.length, passed, failed },
439
520
  byType: { change: summarizeType(results, 'change') },
440
521
  };
522
+ if (opts.findingsScope) {
523
+ this.printFindingsReport({ items: results, summary, root: toRootOutput(root) }, opts.findingsScope, opts.json, root);
524
+ process.exitCode = failed > 0 ? 1 : 0;
525
+ return;
526
+ }
441
527
  if (opts.json) {
442
528
  const out = { items: results, summary, version: '1.0', root: toRootOutput(root) };
443
529
  console.log(JSON.stringify(out, null, 2));
@@ -8,6 +8,7 @@ import ora from 'ora';
8
8
  import path from 'path';
9
9
  import * as fs from 'fs';
10
10
  import { loadChangeContext, generateInstructions, resolveSchema, resolveArtifactOutputPath, resolveArtifactOutputs, } from '../../core/artifact-graph/index.js';
11
+ import { isSpecsArtifactPath } from '../../core/artifact-graph/outputs.js';
11
12
  import { getChangeDir, resolveCurrentPlanningHomeSync, } from '../../core/planning-home.js';
12
13
  import { resolveRootForCommand, withStoreFlag, toPlanningHome, toRootOutput, } from '../../core/root-selection.js';
13
14
  import { assembleReferenceIndex, renderReferencedStoresBlock, renderReferencedStoresSection, } from '../../core/references.js';
@@ -15,6 +16,7 @@ import { readRegistrySnapshot } from '../../core/store/registry.js';
15
16
  import { loadOperationInputs, readProjectConfig, } from '../../core/project-config.js';
16
17
  import { validateChangeExists, validateSchemaExists, } from './shared.js';
17
18
  import { parseTaskLines } from '../../utils/task-progress.js';
19
+ import { METADATA_FILENAME } from '../../utils/change-metadata.js';
18
20
  // -----------------------------------------------------------------------------
19
21
  // Artifact Instructions Command
20
22
  // -----------------------------------------------------------------------------
@@ -230,6 +232,99 @@ function toTaskItems(parsed) {
230
232
  }
231
233
  return tasks;
232
234
  }
235
+ /**
236
+ * The command that builds one artifact.
237
+ *
238
+ * Every earlier remedy here named the `openspec-continue-change` skill, which
239
+ * the `core` profile never installs - the advice was a dead end for the default
240
+ * install. The CLI verb exists on every profile and is what the skill runs.
241
+ */
242
+ function describeArtifactRemedy(changeName, artifactId, options = {}) {
243
+ const target = artifactId ?? '<artifact>';
244
+ const verb = options.many ? 'Create each with' : 'Create it with';
245
+ return (`${verb} \`openspec instructions ${target} --change ${changeName}\`` +
246
+ ` (\`openspec status --change ${changeName}\` shows what is left).`);
247
+ }
248
+ /**
249
+ * Finds the artifact a schema path is generated by, so a remedy can name it.
250
+ */
251
+ function findArtifactIdFor(schema, generates) {
252
+ return schema.artifacts.find((artifact) => artifact.generates === generates)?.id;
253
+ }
254
+ /**
255
+ * Everything still to build before apply can run, in build order.
256
+ *
257
+ * Apply blocks on the schema's `apply.requires` alone, so its own list stops at
258
+ * the first hop: a change with only a proposal is told "Missing artifacts:
259
+ * tasks" while the specs `tasks` depends on are missing too. An agent that
260
+ * takes that literally writes the tracking file straight from the proposal and
261
+ * skips the artifacts in between - the failure reported in #834 and #869.
262
+ * Walking `requires` names the whole chain, the same set and order
263
+ * `openspec status` already prints, without changing what apply blocks on.
264
+ */
265
+ function collectMissingPrerequisites(input) {
266
+ const { requiredArtifactIds, schema, buildOrder, completed } = input;
267
+ const byId = new Map(schema.artifacts.map((artifact) => [artifact.id, artifact]));
268
+ const missing = new Set();
269
+ const queue = [...requiredArtifactIds];
270
+ const seen = new Set(queue);
271
+ while (queue.length > 0) {
272
+ const id = queue.shift();
273
+ const artifact = byId.get(id);
274
+ if (!artifact)
275
+ continue;
276
+ if (!completed.has(id))
277
+ missing.add(id);
278
+ for (const dependency of artifact.requires) {
279
+ if (seen.has(dependency))
280
+ continue;
281
+ seen.add(dependency);
282
+ queue.push(dependency);
283
+ }
284
+ }
285
+ const order = new Map(buildOrder.map((id, index) => [id, index]));
286
+ return [...missing].sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));
287
+ }
288
+ /**
289
+ * Warnings apply reports alongside its instruction.
290
+ *
291
+ * Apply gates on the schema's `apply.requires` only, so a change whose tasks
292
+ * file was written ahead of its specs reads as ready even though no delta spec
293
+ * exists - the state `openspec validate` rejects. Blocking here would be a
294
+ * policy change; naming the gap is not, and it is what keeps apply from being
295
+ * the one surface that green-lights a change every other surface flags.
296
+ *
297
+ * Only reported once apply is past its own gate: for a change that has not
298
+ * reached tasks yet, the missing specs are the next step rather than a warning.
299
+ * Schemas that declare no spec-producing artifact carry `skip_specs` from
300
+ * creation, so this never fires on them.
301
+ */
302
+ function collectApplyWarnings(input) {
303
+ const { state, schema, changeDir, changeName, skippedArtifacts } = input;
304
+ if (state === 'blocked')
305
+ return [];
306
+ const specArtifacts = schema.artifacts.filter((artifact) => isSpecsArtifactPath(artifact.generates));
307
+ if (specArtifacts.length === 0)
308
+ return [];
309
+ if (specArtifacts.some((artifact) => skippedArtifacts?.has(artifact.id)))
310
+ return [];
311
+ const hasDeltas = specArtifacts.some((artifact) => resolveArtifactOutputs(changeDir, artifact.generates).length > 0);
312
+ if (hasDeltas)
313
+ return [];
314
+ const metadataPath = path.join(changeDir, METADATA_FILENAME);
315
+ // The command names the artifact this schema actually declares, never the
316
+ // literal `specs`. A schema whose spec-producing artifact is `contracts` was
317
+ // told to run `openspec instructions specs`, an artifact it does not have,
318
+ // so the warning dead-ended at the exact step meant to resolve it. With more
319
+ // than one such artifact there is no single right answer, so the id becomes
320
+ // a placeholder rather than a guess.
321
+ const specTarget = specArtifacts.length === 1 ? specArtifacts[0].id : '<artifact-id>';
322
+ return [
323
+ `This change has no delta specs and does not declare \`skip_specs: true\`, so \`openspec validate ${changeName}\` fails on it. ` +
324
+ `Write the delta specs before implementing (\`openspec instructions ${specTarget} --change ${changeName}\`), ` +
325
+ `or add \`skip_specs: true\` to ${metadataPath} if this change really changes no specified behavior.`,
326
+ ];
327
+ }
233
328
  /**
234
329
  * Generates apply instructions for implementing tasks from a change.
235
330
  * Schema-aware: reads apply phase configuration from schema to determine
@@ -267,6 +362,13 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
267
362
  missingArtifacts.push(artifactId);
268
363
  }
269
364
  }
365
+ // Everything still to build, not just the first hop apply blocks on.
366
+ const missingPrerequisites = collectMissingPrerequisites({
367
+ requiredArtifactIds: [...requiredArtifactIds],
368
+ schema,
369
+ buildOrder: context.graph.getBuildOrder(),
370
+ completed: context.completed,
371
+ });
270
372
  // Build context files from all existing artifacts in schema
271
373
  const contextFiles = {};
272
374
  for (const artifact of schema.artifacts) {
@@ -297,20 +399,33 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
297
399
  let instruction;
298
400
  if (missingArtifacts.length > 0) {
299
401
  state = 'blocked';
300
- instruction = `Cannot apply this change yet. Missing artifacts: ${missingArtifacts.join(', ')}.\nUse the openspec-continue-change skill to create the missing artifacts first.`;
402
+ const chain = missingPrerequisites.length > missingArtifacts.length
403
+ ? `\nNot created yet, in build order: ${missingPrerequisites.join(', ')}.` +
404
+ ` Build the ones this change needs before applying - the schema says which are conditional.`
405
+ : '';
406
+ instruction =
407
+ `Cannot apply this change yet. Missing artifacts: ${missingArtifacts.join(', ')}.${chain}` +
408
+ `\n${describeArtifactRemedy(changeName,
409
+ // Only name one when one is left: the first of several would be the
410
+ // schema's conditional artifact as often as not.
411
+ missingPrerequisites.length === 1 ? missingPrerequisites[0] : undefined, { many: missingPrerequisites.length > 1 })}`;
301
412
  }
302
413
  else if (tracksFile && !tracksFileExists) {
303
414
  // Tracking file configured but doesn't exist yet
304
415
  const tracksFilename = path.basename(tracksFile);
305
416
  state = 'blocked';
306
- instruction = `The ${tracksFilename} file is missing and must be created.\nUse openspec-continue-change to generate the tracking file.`;
417
+ instruction =
418
+ `The ${tracksFilename} file is missing and must be created.` +
419
+ `\n${describeArtifactRemedy(changeName, findArtifactIdFor(schema, tracksFile))}`;
307
420
  }
308
421
  else if (tracksFile && tracksFileExists && tasks.length === 0) {
309
422
  // Tracking file exists but lists nothing an agent can work on: either no
310
423
  // checkboxes at all, or only checkboxes with no text after them.
311
424
  const tracksFilename = path.basename(tracksFile);
312
425
  state = 'blocked';
313
- instruction = `The ${tracksFilename} file exists but contains no tasks to work on.\nAdd tasks to ${tracksFilename} or regenerate it with openspec-continue-change.`;
426
+ instruction =
427
+ `The ${tracksFilename} file exists but contains no tasks to work on.` +
428
+ `\nAdd tasks to ${tracksFilename}, or rebuild it: ${describeArtifactRemedy(changeName, findArtifactIdFor(schema, tracksFile))}`;
314
429
  }
315
430
  else if (tracksFile && remaining === 0 && total > 0) {
316
431
  state = 'all_done';
@@ -325,6 +440,13 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
325
440
  state = 'ready';
326
441
  instruction = schemaInstruction?.trim() ?? 'Read context files, work through pending tasks, mark complete as you go.\nPause if you hit blockers or need clarification.';
327
442
  }
443
+ const warnings = collectApplyWarnings({
444
+ state,
445
+ schema,
446
+ changeDir,
447
+ changeName,
448
+ skippedArtifacts: context.skippedArtifacts,
449
+ });
328
450
  return {
329
451
  changeName,
330
452
  changeDir,
@@ -334,6 +456,8 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
334
456
  tasks,
335
457
  state,
336
458
  missingArtifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
459
+ ...(missingPrerequisites.length > 0 ? { missingPrerequisites } : {}),
460
+ ...(warnings.length > 0 ? { warnings } : {}),
337
461
  instruction,
338
462
  ...(references !== undefined ? { references } : {}),
339
463
  ...operationInputs,
@@ -375,7 +499,7 @@ export async function applyInstructionsCommand(options) {
375
499
  }
376
500
  }
377
501
  export function printApplyInstructionsText(instructions) {
378
- const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction } = instructions;
502
+ const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, warnings, instruction } = instructions;
379
503
  console.log(`## Apply: ${changeName}`);
380
504
  console.log(`Schema: ${schemaName}`);
381
505
  console.log();
@@ -388,7 +512,18 @@ export function printApplyInstructionsText(instructions) {
388
512
  console.log('### ⚠️ Blocked');
389
513
  console.log();
390
514
  console.log(`Missing artifacts: ${missingArtifacts.join(', ')}`);
391
- console.log('Use the openspec-continue-change skill to create these first.');
515
+ if (instructions.missingPrerequisites &&
516
+ instructions.missingPrerequisites.length > missingArtifacts.length) {
517
+ console.log(`Not created yet, in build order: ${instructions.missingPrerequisites.join(', ')}`);
518
+ }
519
+ console.log();
520
+ }
521
+ if (warnings && warnings.length > 0) {
522
+ console.log('### ⚠️ Warnings');
523
+ console.log();
524
+ for (const warning of warnings) {
525
+ console.log(`- ${warning}`);
526
+ }
392
527
  console.log();
393
528
  }
394
529
  // Context files (dynamically from schema)
@@ -30,6 +30,14 @@ export interface ApplyInstructions {
30
30
  tasks: TaskItem[];
31
31
  state: 'blocked' | 'all_done' | 'ready';
32
32
  missingArtifacts?: string[];
33
+ /**
34
+ * Everything still to build before apply can run, in build order - the
35
+ * transitive closure of the schema's `apply.requires`, so it can be longer
36
+ * than `missingArtifacts`, which stops at the first hop apply blocks on.
37
+ */
38
+ missingPrerequisites?: string[];
39
+ /** Non-blocking problems with the change, reported alongside the instruction. */
40
+ warnings?: string[];
33
41
  instruction: string;
34
42
  /** Referenced-store index (read-only upstream context; omitted when none declared) */
35
43
  references?: ReferenceIndexEntry[];
@@ -0,0 +1,15 @@
1
+ /**
2
+ * SourceCraft Code Assistant Command Adapter
3
+ *
4
+ * Formats commands for the SourceCraft Code Assistant VS Code extension.
5
+ *
6
+ * @see https://sourcecraft.dev/portal/docs/en/code-assistant/operations/agent/slash-commands
7
+ */
8
+ import type { ToolCommandAdapter } from '../types.js';
9
+ /**
10
+ * SourceCraft Code Assistant adapter for command generation.
11
+ * File path: .codeassistant/commands/opsx-<id>.md
12
+ * Format: YAML frontmatter with description
13
+ */
14
+ export declare const codeassistantAdapter: ToolCommandAdapter;
15
+ //# sourceMappingURL=codeassistant.d.ts.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * SourceCraft Code Assistant Command Adapter
3
+ *
4
+ * Formats commands for the SourceCraft Code Assistant VS Code extension.
5
+ *
6
+ * @see https://sourcecraft.dev/portal/docs/en/code-assistant/operations/agent/slash-commands
7
+ */
8
+ import path from 'path';
9
+ import { escapeYamlValue } from '../yaml.js';
10
+ /**
11
+ * SourceCraft Code Assistant adapter for command generation.
12
+ * File path: .codeassistant/commands/opsx-<id>.md
13
+ * Format: YAML frontmatter with description
14
+ */
15
+ export const codeassistantAdapter = {
16
+ toolId: 'codeassistant',
17
+ getFilePath(commandId) {
18
+ return path.join('.codeassistant', 'commands', `opsx-${commandId}.md`);
19
+ },
20
+ formatFile(content) {
21
+ return `---
22
+ description: ${escapeYamlValue(content.description)}
23
+ ---
24
+
25
+ ${content.body}
26
+ `;
27
+ },
28
+ };
29
+ //# sourceMappingURL=codeassistant.js.map
@@ -26,6 +26,7 @@ export { kiroAdapter } from './kiro.js';
26
26
  export { ohMyPiAdapter } from './oh-my-pi.js';
27
27
  export { opencodeAdapter } from './opencode.js';
28
28
  export { piAdapter } from './pi.js';
29
+ export { codeassistantAdapter } from './codeassistant.js';
29
30
  export { qoderAdapter } from './qoder.js';
30
31
  export { lingmaAdapter } from './lingma.js';
31
32
  export { qwenAdapter } from './qwen.js';
@@ -26,6 +26,7 @@ export { kiroAdapter } from './kiro.js';
26
26
  export { ohMyPiAdapter } from './oh-my-pi.js';
27
27
  export { opencodeAdapter } from './opencode.js';
28
28
  export { piAdapter } from './pi.js';
29
+ export { codeassistantAdapter } from './codeassistant.js';
29
30
  export { qoderAdapter } from './qoder.js';
30
31
  export { lingmaAdapter } from './lingma.js';
31
32
  export { qwenAdapter } from './qwen.js';
@@ -27,6 +27,7 @@ import { kiroAdapter } from './adapters/kiro.js';
27
27
  import { ohMyPiAdapter } from './adapters/oh-my-pi.js';
28
28
  import { opencodeAdapter } from './adapters/opencode.js';
29
29
  import { piAdapter } from './adapters/pi.js';
30
+ import { codeassistantAdapter } from './adapters/codeassistant.js';
30
31
  import { qoderAdapter } from './adapters/qoder.js';
31
32
  import { lingmaAdapter } from './adapters/lingma.js';
32
33
  import { qwenAdapter } from './adapters/qwen.js';
@@ -63,6 +64,7 @@ export class CommandAdapterRegistry {
63
64
  CommandAdapterRegistry.register(ohMyPiAdapter);
64
65
  CommandAdapterRegistry.register(opencodeAdapter);
65
66
  CommandAdapterRegistry.register(piAdapter);
67
+ CommandAdapterRegistry.register(codeassistantAdapter);
66
68
  CommandAdapterRegistry.register(qoderAdapter);
67
69
  CommandAdapterRegistry.register(lingmaAdapter);
68
70
  CommandAdapterRegistry.register(qwenAdapter);
@@ -106,6 +106,12 @@ export const COMMAND_REGISTRY = [
106
106
  name: 'archived',
107
107
  description: 'Validate that archived changes have all tasks completed (for pre-commit linting)',
108
108
  },
109
+ {
110
+ name: 'report',
111
+ description: 'Select bulk report content',
112
+ takesValue: true,
113
+ values: ['full', 'findings'],
114
+ },
109
115
  COMMON_FLAGS.type,
110
116
  COMMON_FLAGS.strict,
111
117
  COMMON_FLAGS.jsonValidation,
@@ -54,6 +54,7 @@ export const AI_TOOLS = [
54
54
  { name: 'Oh My Pi', value: 'oh-my-pi', available: true, successLabel: 'Oh My Pi', skillsDir: '.omp' },
55
55
  { name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' },
56
56
  { name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' },
57
+ { name: 'SourceCraft Code Assistant', value: 'codeassistant', available: true, successLabel: 'SourceCraft Code Assistant', skillsDir: '.codeassistant' },
57
58
  { name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder', requiresIdeRestart: true },
58
59
  { name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' },
59
60
  { name: 'Rovo Dev CLI', value: 'rovodev', available: true, successLabel: 'Rovo Dev CLI', skillsDir: '.rovodev', detectionPaths: ['.rovodev/skills', '.rovodev'] },