@fission-ai/openspec 1.10.0 → 1.12.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 (59) hide show
  1. package/README.md +1 -0
  2. package/dist/cli/index.js +12 -2
  3. package/dist/commands/change.d.ts +24 -0
  4. package/dist/commands/change.js +229 -5
  5. package/dist/commands/schema.d.ts +5 -0
  6. package/dist/commands/schema.js +201 -35
  7. package/dist/commands/show.js +1 -1
  8. package/dist/commands/validate.d.ts +57 -0
  9. package/dist/commands/validate.js +99 -13
  10. package/dist/commands/workflow/index.d.ts +1 -1
  11. package/dist/commands/workflow/index.js +1 -1
  12. package/dist/commands/workflow/status.d.ts +2 -0
  13. package/dist/commands/workflow/status.js +76 -12
  14. package/dist/core/artifact-graph/resolver.js +15 -9
  15. package/dist/core/available-tools.js +16 -1
  16. package/dist/core/command-generation/adapters/antigravity.d.ts +1 -1
  17. package/dist/core/command-generation/adapters/antigravity.js +2 -2
  18. package/dist/core/command-generation/adapters/codeassistant.d.ts +15 -0
  19. package/dist/core/command-generation/adapters/codeassistant.js +29 -0
  20. package/dist/core/command-generation/adapters/index.d.ts +1 -0
  21. package/dist/core/command-generation/adapters/index.js +1 -0
  22. package/dist/core/command-generation/registry.js +2 -0
  23. package/dist/core/completions/command-registry.js +21 -0
  24. package/dist/core/completions/generators/fish-generator.d.ts +12 -0
  25. package/dist/core/completions/generators/fish-generator.js +79 -47
  26. package/dist/core/completions/templates/fish-templates.d.ts +1 -1
  27. package/dist/core/completions/templates/fish-templates.js +107 -10
  28. package/dist/core/completions/types.d.ts +4 -0
  29. package/dist/core/config.js +7 -1
  30. package/dist/core/init.d.ts +1 -0
  31. package/dist/core/init.js +71 -65
  32. package/dist/core/legacy-cleanup.js +6 -0
  33. package/dist/core/migration.js +42 -12
  34. package/dist/core/openspec-root.d.ts +1 -0
  35. package/dist/core/openspec-root.js +10 -2
  36. package/dist/core/parsers/requirement-blocks.d.ts +1 -0
  37. package/dist/core/parsers/requirement-blocks.js +2 -0
  38. package/dist/core/shared/ide-restart.d.ts +26 -0
  39. package/dist/core/shared/ide-restart.js +42 -0
  40. package/dist/core/shared/index.d.ts +1 -0
  41. package/dist/core/shared/index.js +1 -0
  42. package/dist/core/shared/tool-detection.js +9 -3
  43. package/dist/core/shared-skill-target.d.ts +13 -0
  44. package/dist/core/shared-skill-target.js +43 -1
  45. package/dist/core/specs-apply.js +22 -9
  46. package/dist/core/templates/workflows/explore.js +108 -73
  47. package/dist/core/templates/workflows/ff-change.js +8 -0
  48. package/dist/core/templates/workflows/propose.js +8 -0
  49. package/dist/core/update.js +28 -31
  50. package/dist/core/validation/constants.d.ts +3 -0
  51. package/dist/core/validation/constants.js +12 -0
  52. package/dist/core/validation/purpose-placeholder.d.ts +45 -0
  53. package/dist/core/validation/purpose-placeholder.js +117 -0
  54. package/dist/core/validation/validator.d.ts +12 -3
  55. package/dist/core/validation/validator.js +90 -4
  56. package/dist/utils/command-references.js +7 -8
  57. package/dist/utils/requirement-diff.d.ts +42 -0
  58. package/dist/utils/requirement-diff.js +61 -0
  59. package/package.json +4 -3
@@ -4,7 +4,7 @@ import { resolveRootForCommand, toRootOutput, withStoreFlag, isStoreSelectedRoot
4
4
  import { ChangeCommand } from './change.js';
5
5
  import { SpecCommand } from './spec.js';
6
6
  import { nearestMatches } from '../utils/match.js';
7
- const CHANGE_FLAG_KEYS = new Set(['deltasOnly', 'requirementsOnly']);
7
+ const CHANGE_FLAG_KEYS = new Set(['deltasOnly', 'requirementsOnly', 'diff']);
8
8
  const SPEC_FLAG_KEYS = new Set(['requirements', 'scenarios', 'requirement']);
9
9
  export class ShowCommand {
10
10
  async execute(itemName, options = {}) {
@@ -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));
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Commands for the artifact-driven workflow: status, instructions, templates, schemas, new change.
5
5
  */
6
- export { statusCommand } from './status.js';
6
+ export { statusCommand, BATCH_STATUS_FAILURE_PAYLOAD } from './status.js';
7
7
  export type { StatusOptions } from './status.js';
8
8
  export { instructionsCommand, applyInstructionsCommand, archiveInstructionsCommand, } from './instructions.js';
9
9
  export type { InstructionsOptions } from './instructions.js';
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Commands for the artifact-driven workflow: status, instructions, templates, schemas, new change.
5
5
  */
6
- export { statusCommand } from './status.js';
6
+ export { statusCommand, BATCH_STATUS_FAILURE_PAYLOAD } from './status.js';
7
7
  export { instructionsCommand, applyInstructionsCommand, archiveInstructionsCommand, } from './instructions.js';
8
8
  export { templatesCommand } from './templates.js';
9
9
  export { schemasCommand } from './schemas.js';
@@ -6,11 +6,13 @@
6
6
  import { type ChangeStatus } from '../../core/artifact-graph/index.js';
7
7
  export interface StatusOptions {
8
8
  change?: string;
9
+ all?: boolean;
9
10
  schema?: string;
10
11
  store?: string;
11
12
  storePath?: string;
12
13
  json?: boolean;
13
14
  }
15
+ export declare const BATCH_STATUS_FAILURE_PAYLOAD: Record<string, unknown>;
14
16
  export declare function statusCommand(options: StatusOptions): Promise<void>;
15
17
  export declare function printStatusText(status: ChangeStatus): void;
16
18
  //# sourceMappingURL=status.d.ts.map
@@ -8,14 +8,26 @@ import chalk from 'chalk';
8
8
  import { getChangeDir } from '../../core/planning-home.js';
9
9
  import { resolveRootForCommand, toPlanningHome, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from '../../core/root-selection.js';
10
10
  import { loadChangeContext, formatChangeStatus, } from '../../core/artifact-graph/index.js';
11
+ import { asStatus } from '../shared-output.js';
11
12
  import { validateChangeExists, validateSchemaExists, getAvailableChanges, getStatusIndicator, getStatusColor, } from './shared.js';
12
- // -----------------------------------------------------------------------------
13
- // Command Implementation
14
- // -----------------------------------------------------------------------------
13
+ // The --all --json failure null-shape. Root-selection failures (handled in
14
+ // resolveRootForCommand) and thrown errors (caught by the CLI wrapper) must
15
+ // emit the same shape, so both call sites reference this one constant.
16
+ export const BATCH_STATUS_FAILURE_PAYLOAD = {
17
+ changes: [],
18
+ root: null,
19
+ };
15
20
  export async function statusCommand(options) {
21
+ if (options.all && options.change) {
22
+ throw new Error('The --all and --change options are mutually exclusive.');
23
+ }
16
24
  // The root resolves (and the store banner prints) before the spinner starts
17
- // so the two do not fight over stderr.
18
- const root = await resolveRootForCommand(options, { json: options.json });
25
+ // so the two do not fight over stderr. The batch null-shape rides along so
26
+ // a root-selection failure under --all --json still carries `changes: []`.
27
+ const root = await resolveRootForCommand(options, {
28
+ json: options.json,
29
+ failurePayload: options.all ? BATCH_STATUS_FAILURE_PAYLOAD : undefined,
30
+ });
19
31
  if (!root) {
20
32
  return;
21
33
  }
@@ -25,9 +37,20 @@ export async function statusCommand(options) {
25
37
  const projectRoot = root.path;
26
38
  const rootOutput = toRootOutput(root);
27
39
  const newChangeHint = withStoreFlag(root, 'openspec new change <name>');
40
+ // Single definition of "load one change's status" so the batch and
41
+ // single-change payloads can never drift apart.
42
+ const loadStatus = (changeName) => formatChangeStatus(loadChangeContext(projectRoot, changeName, options.schema, {
43
+ changeDir: getChangeDir(planningHome, changeName),
44
+ planningHome,
45
+ }), isStoreSelectedRoot(root) ? { storeId: root.storeId } : {});
28
46
  // Handle no-changes case gracefully — status is informational,
29
47
  // so "no changes" is a valid state, not an error.
30
48
  if (!options.change) {
49
+ // Validate before the no-changes early return so a bogus --schema
50
+ // fails the same way whether or not any change exists yet.
51
+ if (options.all && options.schema) {
52
+ validateSchemaExists(options.schema, projectRoot);
53
+ }
31
54
  const available = await getAvailableChanges(projectRoot, root.changesDir);
32
55
  if (available.length === 0) {
33
56
  spinner?.stop();
@@ -38,9 +61,54 @@ export async function statusCommand(options) {
38
61
  console.log(`No active changes. Create one with: ${newChangeHint}`);
39
62
  return;
40
63
  }
41
- // Changes exist but --change not provided
64
+ if (options.all) {
65
+ // readdir order is platform-dependent; sort for deterministic output,
66
+ // with the same comparator validate --all uses so the two batch
67
+ // commands order a given change set identically.
68
+ const entries = [];
69
+ for (const changeName of available.sort((a, b) => a.localeCompare(b))) {
70
+ try {
71
+ entries.push(loadStatus(changeName));
72
+ }
73
+ catch (error) {
74
+ // One malformed change must not blank the sweep; carry its
75
+ // diagnostic in place and keep going.
76
+ entries.push({ changeName, status: [asStatus(error, 'change_error')] });
77
+ }
78
+ }
79
+ spinner?.stop();
80
+ const failed = entries.some((entry) => !('artifacts' in entry));
81
+ if (options.json) {
82
+ console.log(JSON.stringify({ changes: entries, root: rootOutput }, null, 2));
83
+ if (failed) {
84
+ process.exitCode = 1;
85
+ }
86
+ return;
87
+ }
88
+ entries.forEach((entry, index) => {
89
+ if (index > 0) {
90
+ console.log();
91
+ }
92
+ if ('artifacts' in entry) {
93
+ printStatusText(entry);
94
+ }
95
+ else {
96
+ console.log(chalk.red(`✗ ${entry.changeName}: ${entry.status[0]?.message}`));
97
+ }
98
+ });
99
+ // A partial load is still a failed command in both output modes;
100
+ // JSON callers can parse the complete envelope independently of
101
+ // the process exit code.
102
+ if (failed) {
103
+ process.exitCode = 1;
104
+ }
105
+ return;
106
+ }
107
+ // Changes exist but neither --change nor --all provided. Name --all
108
+ // here too: it is the other way to answer this prompt, and a caller
109
+ // who wants every change should not have to find it in --help.
42
110
  spinner?.stop();
43
- throw new Error(`Missing required option --change. Available changes:\n ${available.join('\n ')}`);
111
+ throw new Error(`Missing required option --change (or --all for every active change). Available changes:\n ${available.join('\n ')}`);
44
112
  }
45
113
  const changeName = await validateChangeExists(options.change, projectRoot, root.changesDir, { newChangeHint });
46
114
  // Validate schema if explicitly provided
@@ -48,11 +116,7 @@ export async function statusCommand(options) {
48
116
  validateSchemaExists(options.schema, projectRoot);
49
117
  }
50
118
  // loadChangeContext will auto-detect schema from metadata if not provided
51
- const context = loadChangeContext(projectRoot, changeName, options.schema, {
52
- changeDir: getChangeDir(planningHome, changeName),
53
- planningHome,
54
- });
55
- const status = formatChangeStatus(context, isStoreSelectedRoot(root) ? { storeId: root.storeId } : {});
119
+ const status = loadStatus(changeName);
56
120
  spinner?.stop();
57
121
  if (options.json) {
58
122
  console.log(JSON.stringify({ ...status, root: rootOutput }, null, 2));
@@ -53,18 +53,24 @@ export function getProjectSchemasDir(projectRoot) {
53
53
  * @param entry - The directory entry from `fs.readdirSync(..., { withFileTypes: true })`
54
54
  */
55
55
  /**
56
- * Directories `schema fork` creates transiently while swapping a fork into
57
- * place: a staging copy (`.fork-staging-<rand>`, created via mkdtemp) and a
58
- * backup of the previous destination (`<name>.fork-backup-<pid>-<ts>`). Either
59
- * can briefly coexist with real schemas in the schemas dir, so discovery must
60
- * never surface them. Real schema names are kebab-case (no dots), so excluding
61
- * these dot-bearing temp names can never hide a legitimate schema.
56
+ * Directories `schema fork` and `schema init` create transiently while swapping
57
+ * a schema into place: a staging copy (`.fork-staging-<rand>` /
58
+ * `.init-staging-<rand>`, created via mkdtemp) and a backup of the previous
59
+ * destination (`<name>.fork-backup-<pid>-<ts>` /
60
+ * `<name>.init-backup-<pid>-<ts>`). Either can briefly coexist with real
61
+ * schemas in the schemas dir, and a backup outlives the run when its cleanup is
62
+ * blocked, so discovery must never surface them. Real schema names are
63
+ * kebab-case (no dots), so excluding these dot-bearing temp names can never
64
+ * hide a legitimate schema.
62
65
  */
63
- function isOwnedForkTempDir(name) {
64
- return name.startsWith('.fork-staging-') || name.includes('.fork-backup-');
66
+ function isOwnedTransientSchemaDir(name) {
67
+ return (name.startsWith('.fork-staging-') ||
68
+ name.includes('.fork-backup-') ||
69
+ name.startsWith('.init-staging-') ||
70
+ name.includes('.init-backup-'));
65
71
  }
66
72
  export function isSchemaDir(parentDir, entry) {
67
- if (isOwnedForkTempDir(entry.name)) {
73
+ if (isOwnedTransientSchemaDir(entry.name)) {
68
74
  return false;
69
75
  }
70
76
  if (entry.isDirectory()) {
@@ -49,6 +49,21 @@ export function getAvailableTools(projectPath) {
49
49
  }
50
50
  });
51
51
  const activeProjectTools = new Set(reconcileSharedSkillTargets(projectPath, available.filter((tool) => tool.skillsDir)).map((tool) => tool.value));
52
- return available.filter((tool) => tool.globalSkillsDir || activeProjectTools.has(tool.value));
52
+ const hasIndependentDetectionPath = (tool) => (tool.detectionPaths ?? []).some((detectionPath) => {
53
+ // Skill roots still go through managed-content reconciliation below;
54
+ // their mere existence is not an independent tool signal.
55
+ if (detectionPath.endsWith('/skills'))
56
+ return false;
57
+ try {
58
+ fs.statSync(path.join(projectPath, detectionPath));
59
+ return true;
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ });
65
+ return available.filter((tool) => tool.globalSkillsDir ||
66
+ hasIndependentDetectionPath(tool) ||
67
+ activeProjectTools.has(tool.value));
53
68
  }
54
69
  //# sourceMappingURL=available-tools.js.map
@@ -6,7 +6,7 @@
6
6
  import type { ToolCommandAdapter } from '../types.js';
7
7
  /**
8
8
  * Antigravity adapter for command generation.
9
- * File path: .agent/workflows/opsx-<id>.md
9
+ * File path: .agents/workflows/opsx-<id>.md
10
10
  * Frontmatter: description
11
11
  */
12
12
  export declare const antigravityAdapter: ToolCommandAdapter;
@@ -7,13 +7,13 @@ import path from 'path';
7
7
  import { escapeYamlValue } from '../yaml.js';
8
8
  /**
9
9
  * Antigravity adapter for command generation.
10
- * File path: .agent/workflows/opsx-<id>.md
10
+ * File path: .agents/workflows/opsx-<id>.md
11
11
  * Frontmatter: description
12
12
  */
13
13
  export const antigravityAdapter = {
14
14
  toolId: 'antigravity',
15
15
  getFilePath(commandId) {
16
- return path.join('.agent', 'workflows', `opsx-${commandId}.md`);
16
+ return path.join('.agents', 'workflows', `opsx-${commandId}.md`);
17
17
  },
18
18
  formatFile(content) {
19
19
  return `---
@@ -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,
@@ -136,6 +142,10 @@ export const COMMAND_REGISTRY = [
136
142
  name: 'requirements-only',
137
143
  description: 'Alias for --deltas-only (deprecated, change-specific)',
138
144
  },
145
+ {
146
+ name: 'diff',
147
+ description: 'Show per-requirement diffs for delta specs (change-specific)',
148
+ },
139
149
  {
140
150
  name: 'requirements',
141
151
  description: 'Show only requirements, exclude scenarios (JSON only, spec-specific)',
@@ -189,6 +199,10 @@ export const COMMAND_REGISTRY = [
189
199
  description: 'Change name to show status for',
190
200
  takesValue: true,
191
201
  },
202
+ {
203
+ name: 'all',
204
+ description: 'Show status for all active changes',
205
+ },
192
206
  {
193
207
  name: 'schema',
194
208
  description: 'Schema override',
@@ -285,6 +299,7 @@ export const COMMAND_REGISTRY = [
285
299
  name: 'path',
286
300
  description: 'Directory to use for the store',
287
301
  takesValue: true,
302
+ completionType: 'path',
288
303
  },
289
304
  {
290
305
  name: 'init-git',
@@ -377,6 +392,7 @@ export const COMMAND_REGISTRY = [
377
392
  name: 'code-workspace',
378
393
  description: 'Also write a VS Code workspace file for the set',
379
394
  takesValue: true,
395
+ completionType: 'path',
380
396
  },
381
397
  {
382
398
  name: 'force',
@@ -407,6 +423,7 @@ export const COMMAND_REGISTRY = [
407
423
  name: 'member',
408
424
  description: 'Member folder as <path> or <name>=<path>; repeatable, first is the primary',
409
425
  takesValue: true,
426
+ completionType: 'path',
410
427
  },
411
428
  {
412
429
  name: 'tool',
@@ -488,6 +505,10 @@ export const COMMAND_REGISTRY = [
488
505
  name: 'requirements-only',
489
506
  description: 'Alias for --deltas-only (deprecated)',
490
507
  },
508
+ {
509
+ name: 'diff',
510
+ description: 'Show per-requirement diffs for delta specs',
511
+ },
491
512
  COMMON_FLAGS.noInteractive,
492
513
  ],
493
514
  },