@fission-ai/openspec 1.13.2 → 1.14.1

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 (99) hide show
  1. package/dist/cli/commands/config.d.ts +8 -0
  2. package/dist/cli/commands/config.js +88 -0
  3. package/dist/cli/commands/context.d.ts +3 -0
  4. package/dist/cli/commands/context.js +20 -0
  5. package/dist/cli/commands/doctor.d.ts +3 -0
  6. package/dist/cli/commands/doctor.js +18 -0
  7. package/dist/cli/commands/schema.d.ts +6 -0
  8. package/dist/cli/commands/schema.js +57 -0
  9. package/dist/cli/commands/spec.d.ts +3 -0
  10. package/dist/cli/commands/spec.js +42 -0
  11. package/dist/cli/commands/store.d.ts +3 -0
  12. package/dist/cli/commands/store.js +130 -0
  13. package/dist/cli/commands/workset.d.ts +3 -0
  14. package/dist/cli/commands/workset.js +96 -0
  15. package/dist/cli/index.js +106 -43
  16. package/dist/commands/config.d.ts +16 -7
  17. package/dist/commands/config.js +342 -395
  18. package/dist/commands/context.d.ts +8 -2
  19. package/dist/commands/context.js +22 -37
  20. package/dist/commands/doctor.d.ts +6 -7
  21. package/dist/commands/doctor.js +14 -27
  22. package/dist/commands/schema.d.ts +19 -5
  23. package/dist/commands/schema.js +662 -702
  24. package/dist/commands/spec.d.ts +13 -4
  25. package/dist/commands/spec.js +92 -120
  26. package/dist/commands/store.d.ts +27 -2
  27. package/dist/commands/store.js +1 -120
  28. package/dist/commands/workflow/default-schema.d.ts +2 -0
  29. package/dist/commands/workflow/default-schema.js +4 -0
  30. package/dist/commands/workflow/instructions.js +32 -12
  31. package/dist/commands/workflow/shared.d.ts +3 -1
  32. package/dist/commands/workflow/shared.js +1 -1
  33. package/dist/commands/workflow/status.js +15 -2
  34. package/dist/commands/workset.d.ts +22 -2
  35. package/dist/commands/workset.js +1 -88
  36. package/dist/core/archive.js +28 -7
  37. package/dist/core/artifact-graph/instruction-loader.d.ts +7 -0
  38. package/dist/core/artifact-graph/instruction-loader.js +18 -1
  39. package/dist/core/change-metadata/schema.d.ts +3 -0
  40. package/dist/core/change-metadata/schema.js +10 -0
  41. package/dist/core/change-status-policy.d.ts +8 -0
  42. package/dist/core/change-status-policy.js +33 -2
  43. package/dist/core/command-generation/adapters/atomcode.d.ts +24 -0
  44. package/dist/core/command-generation/adapters/atomcode.js +46 -0
  45. package/dist/core/command-generation/adapters/codestudio.d.ts +13 -0
  46. package/dist/core/command-generation/adapters/codestudio.js +27 -0
  47. package/dist/core/command-generation/adapters/easycode.d.ts +17 -0
  48. package/dist/core/command-generation/adapters/easycode.js +28 -0
  49. package/dist/core/command-generation/adapters/gemini.js +1 -35
  50. package/dist/core/command-generation/adapters/gigacode.d.ts +17 -0
  51. package/dist/core/command-generation/adapters/gigacode.js +31 -0
  52. package/dist/core/command-generation/adapters/index.d.ts +4 -0
  53. package/dist/core/command-generation/adapters/index.js +4 -0
  54. package/dist/core/command-generation/registry.js +8 -0
  55. package/dist/core/command-generation/toml.d.ts +19 -0
  56. package/dist/core/command-generation/toml.js +39 -0
  57. package/dist/core/command-surface.js +1 -1
  58. package/dist/core/completion-tip.js +3 -1
  59. package/dist/core/completions/command-registry.js +19 -0
  60. package/dist/core/completions/installers/zsh-installer.js +4 -2
  61. package/dist/core/config-prompts.js +6 -6
  62. package/dist/core/config-schema.d.ts +1 -1
  63. package/dist/core/config.js +11 -1
  64. package/dist/core/init.js +35 -11
  65. package/dist/core/legacy-cleanup.js +11 -7
  66. package/dist/core/list.d.ts +2 -0
  67. package/dist/core/list.js +49 -23
  68. package/dist/core/parsers/markdown-parser.js +5 -0
  69. package/dist/core/parsers/requirement-blocks.d.ts +6 -0
  70. package/dist/core/parsers/requirement-blocks.js +9 -2
  71. package/dist/core/project-config.js +45 -1
  72. package/dist/core/references.js +3 -1
  73. package/dist/core/root-selection.d.ts +6 -0
  74. package/dist/core/root-selection.js +16 -0
  75. package/dist/core/schemas/base.schema.d.ts +3 -0
  76. package/dist/core/schemas/base.schema.js +6 -0
  77. package/dist/core/schemas/change.schema.d.ts +8 -0
  78. package/dist/core/schemas/spec.schema.d.ts +2 -0
  79. package/dist/core/templates/workflows/apply-change.js +9 -5
  80. package/dist/core/templates/workflows/archive-change.js +26 -4
  81. package/dist/core/templates/workflows/bulk-archive-change.js +6 -2
  82. package/dist/core/templates/workflows/continue-change.js +17 -0
  83. package/dist/core/templates/workflows/verify-change.js +12 -12
  84. package/dist/core/validation/constants.d.ts +1 -1
  85. package/dist/core/validation/constants.js +1 -1
  86. package/dist/core/validation/validator.js +21 -2
  87. package/dist/core/version-check.d.ts +23 -1
  88. package/dist/core/version-check.js +112 -26
  89. package/dist/core/view.d.ts +2 -0
  90. package/dist/core/view.js +36 -2
  91. package/dist/utils/change-metadata.d.ts +20 -1
  92. package/dist/utils/change-metadata.js +57 -1
  93. package/dist/utils/command-references.d.ts +1 -1
  94. package/dist/utils/command-references.js +2 -2
  95. package/dist/utils/index.d.ts +1 -1
  96. package/dist/utils/index.js +1 -1
  97. package/package.json +1 -1
  98. package/schemas/spec-driven/schema.yaml +29 -4
  99. package/schemas/spec-driven/templates/proposal.md +6 -3
@@ -8,7 +8,6 @@
8
8
  */
9
9
  import * as os from 'node:os';
10
10
  import { createRequire } from 'node:module';
11
- import { Option } from 'commander';
12
11
  import { buildWorksetCodeWorkspaceJson, getWorkset, getWorksetCodeWorkspacePath, listWorksets, readWorksetsState, removeWorkset, updateWorksetsState, validateWorksetName, withWorkset, withWorksetsLock, worksetNotFoundError, } from '../core/worksets.js';
13
12
  import { buildLaunchCommand, findOpener, isOpenerCommandAvailable, isOpenerEnabled, listOpenerChoices, mergeOpenerTable, } from '../core/openers.js';
14
13
  import { pathIsDirectory, writeFileAtomically } from '../core/file-state.js';
@@ -18,7 +17,6 @@ import { isInteractive } from '../utils/interactive.js';
18
17
  import { asErrorMessage, emitFailure, isPromptCancellationError, printJson, } from './shared-output.js';
19
18
  import { finalizeWorkset, firstInstalledAlternative, formatMemberRows, noToolInstalledError, resolveMemberFlags, toolUnavailableError, toolUnknownError, } from './workset-input.js';
20
19
  import { composeInteractively, confirmRemoveInteractively, promptOpenNow, promptToolFromChoices, } from './workset-prompts.js';
21
- import { COMMAND_REGISTRY } from '../core/completions/command-registry.js';
22
20
  // cross-spawn is CJS with no types and only `workset open` needs it -
23
21
  // loaded lazily so every other CLI invocation skips its module graph.
24
22
  let cachedSpawn;
@@ -93,7 +91,7 @@ export function exitCodeForLaunch(result) {
93
91
  }
94
92
  return result.code ?? 0;
95
93
  }
96
- class WorksetCommand {
94
+ export class WorksetCommand {
97
95
  async create(name, options = {}) {
98
96
  try {
99
97
  const interactive = !options.json && isInteractive();
@@ -358,89 +356,4 @@ class WorksetCommand {
358
356
  }
359
357
  }
360
358
  }
361
- function collectMember(value, previous) {
362
- return [...previous, value];
363
- }
364
- export function registerWorksetCommand(program) {
365
- const worksetCommand = new WorksetCommand();
366
- const groupDescription = COMMAND_REGISTRY.find((entry) => entry.name === 'workset')?.description ??
367
- 'Compose, keep, and open personal working views (purely local)';
368
- const workset = program.command('workset').description(groupDescription);
369
- // Parsed at the group level so `openspec workset --json` keeps the
370
- // one-JSON-document contract instead of a raw Commander error. The
371
- // parent option matches anywhere; actions read optsWithGlobals().
372
- workset.addOption(new Option('--json', 'Output as JSON').hideHelp());
373
- workset
374
- .command('create [name]')
375
- .description('Compose and save a named working view of folders you choose')
376
- .option('--member <member>', 'Member folder as <path> or <name>=<path>; repeatable, first is the primary', collectMember, [])
377
- .option('--tool <id>', 'Preferred tool to open this workset with')
378
- .option('--json', 'Output as JSON')
379
- .action(async (name, _options, command) => {
380
- await worksetCommand.create(name, command.optsWithGlobals());
381
- });
382
- workset
383
- .command('list')
384
- .alias('ls')
385
- .description('Show saved worksets with their members')
386
- .option('--json', 'Output as JSON')
387
- .action(async (_options, command) => {
388
- await worksetCommand.list(command.optsWithGlobals());
389
- });
390
- workset
391
- .command('open <name>')
392
- .description('Open a saved workset in your tool (editor window or agent session)')
393
- .option('--tool <id>', 'Open with this tool just this once')
394
- .addOption(
395
- // Parsed so Commander never owns the error; rejected in the
396
- // action with one JSON document. Hidden because help should not
397
- // advertise a mode that only rejects.
398
- new Option('--json', 'Not supported for open').hideHelp())
399
- .action(async (name, _options, command) => {
400
- await worksetCommand.open(name, command.optsWithGlobals());
401
- });
402
- workset
403
- .command('remove <name>')
404
- .description('Delete a saved workset (member folders are never touched)')
405
- .option('--yes', 'Confirm removal non-interactively')
406
- .option('--json', 'Output as JSON')
407
- .action(async (name, _options, command) => {
408
- await worksetCommand.remove(name, command.optsWithGlobals());
409
- });
410
- const subcommandsLine = workset.commands
411
- .map((subcommand) => {
412
- const aliases = subcommand.aliases();
413
- return aliases.length > 0
414
- ? `${subcommand.name()} (${aliases.join(', ')})`
415
- : subcommand.name();
416
- })
417
- .join(', ');
418
- // One handler owns missing AND unknown subcommands: known
419
- // subcommands dispatch above; everything else lands in this action
420
- // (allowExcessArguments routes the unknown operand here), keeping
421
- // the one-JSON-document contract for `--json` probes.
422
- workset.allowExcessArguments(true);
423
- workset.action(() => {
424
- const attempted = workset.args.filter((operand) => !operand.startsWith('-'));
425
- const message = attempted.length > 0
426
- ? `Unknown command '${attempted[0]}' for 'openspec workset'. Workset subcommands: ${subcommandsLine}.`
427
- : `Missing subcommand for 'openspec workset'. Workset subcommands: ${subcommandsLine}.`;
428
- if (workset.opts().json) {
429
- printJson({
430
- status: [
431
- {
432
- severity: 'error',
433
- code: 'unknown_workset_subcommand',
434
- message,
435
- fix: 'Run one of the workset subcommands.',
436
- },
437
- ],
438
- });
439
- }
440
- else {
441
- console.error(`Error: ${message}`);
442
- }
443
- process.exitCode = 1;
444
- });
445
- }
446
359
  //# sourceMappingURL=workset.js.map
@@ -9,7 +9,7 @@ import chalk from 'chalk';
9
9
  import { emitStoreRootBanner, isRootSelectionError, resolveOpenSpecRoot, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from './root-selection.js';
10
10
  import { findSpecUpdates, buildUpdatedSpec, writeUpdatedSpec, retireSpec, finalizeRetiredSpec, pruneEmptyDirs, } from './specs-apply.js';
11
11
  import { discoverSpecFiles, findUnreadDeltaFiles, hasAnyFileUnder } from '../utils/spec-discovery.js';
12
- import { METADATA_FILENAME, readRetireCapabilitiesMarker, readSkipSpecsMarker } from '../utils/change-metadata.js';
12
+ import { METADATA_FILENAME, formatUnknownChangeMetadataKeysMessage, readRetireCapabilitiesMarker, readSkipSpecsMarker, readUnknownChangeMetadataKeys, } from '../utils/change-metadata.js';
13
13
  import { confirmPrompt, isNonInteractivePromptError } from '../utils/interactive.js';
14
14
  import { FileSystemUtils } from '../utils/file-system.js';
15
15
  import { folderStyleNameProblem } from './id.js';
@@ -247,6 +247,14 @@ function toArchiveDiagnostic(error) {
247
247
  if (isRootSelectionError(error)) {
248
248
  return error.diagnostic;
249
249
  }
250
+ if (error instanceof RetirementCleanupError) {
251
+ return {
252
+ severity: 'error',
253
+ code: 'archive_retirement_cleanup_failed',
254
+ message: error.message,
255
+ fix: 'Inspect the archived change and all recovery paths in this diagnostic; preserve any needed content before cleanup.',
256
+ };
257
+ }
250
258
  return {
251
259
  severity: 'error',
252
260
  code: 'archive_error',
@@ -379,7 +387,7 @@ async function assertCopiedDirectoryUnchanged(stagedSource, destination, expecte
379
387
  */
380
388
  class MoveDestinationRetainedError extends Error {
381
389
  }
382
- class RetirementBackupsRetainedError extends Error {
390
+ class RetirementCleanupError extends Error {
383
391
  }
384
392
  function isFallbackRenameCode(code) {
385
393
  return code === 'EPERM' || code === 'EXDEV';
@@ -1012,12 +1020,12 @@ async function finalizeRetirementBackups(snapshots, mainSpecsDir) {
1012
1020
  snapshot.displacedPath = undefined;
1013
1021
  }
1014
1022
  catch (error) {
1015
- errors.push(`Could not remove the committed retirement backup at ${displacedPath} ` +
1023
+ errors.push(`Could not finalize the retirement backup at ${displacedPath} ` +
1016
1024
  `(${error instanceof Error ? error.message : String(error)}).`);
1017
1025
  }
1018
1026
  }
1019
1027
  if (errors.length > 0) {
1020
- throw new RetirementBackupsRetainedError(`${errors.join(' ')} The change remains archived and each listed backup was retained for recovery.`);
1028
+ throw new RetirementCleanupError(`${errors.join(' ')} The change was archived, but retirement cleanup did not complete. Inspect all reported recovery paths before recovery or cleanup.`);
1021
1029
  }
1022
1030
  }
1023
1031
  export class ArchiveCommand {
@@ -1125,6 +1133,13 @@ export class ArchiveCommand {
1125
1133
  if (nested) {
1126
1134
  throw new ArchiveBlockedError('archive_change_is_namespace_folder', `Cannot archive '${changeName}': ${describeNestedChange(nested)}`, `Rename openspec/changes/${nested.nested[0]}/ to a flat change directory, then archive it.`);
1127
1135
  }
1136
+ const unknownMetadataKeys = readUnknownChangeMetadataKeys(changeDir);
1137
+ const unknownMetadataWarning = unknownMetadataKeys.length > 0
1138
+ ? formatUnknownChangeMetadataKeysMessage(unknownMetadataKeys)
1139
+ : undefined;
1140
+ if (unknownMetadataWarning && !json) {
1141
+ console.warn(chalk.yellow(unknownMetadataWarning));
1142
+ }
1128
1143
  const skipValidation = options.validate === false || options.noValidate === true;
1129
1144
  // Validate specs and change before archiving
1130
1145
  if (!skipValidation) {
@@ -1770,11 +1785,11 @@ export class ArchiveCommand {
1770
1785
  await finalizeRetirementBackups(specSnapshots, mainSpecsDir);
1771
1786
  }
1772
1787
  catch (cleanupError) {
1773
- throw new RetirementBackupsRetainedError(`${error.message} ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
1788
+ throw new RetirementCleanupError(`${error.message} ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
1774
1789
  }
1775
1790
  throw error;
1776
1791
  }
1777
- if (error instanceof RetirementBackupsRetainedError)
1792
+ if (error instanceof RetirementCleanupError)
1778
1793
  throw error;
1779
1794
  const rollbackErrors = [];
1780
1795
  try {
@@ -1827,7 +1842,13 @@ export class ArchiveCommand {
1827
1842
  path: archivePath,
1828
1843
  specsUpdated,
1829
1844
  ...(totals ? { totals } : {}),
1830
- ...(specWarnings.length > 0 ? { warnings: specWarnings } : {}),
1845
+ ...(specWarnings.length > 0 || unknownMetadataWarning
1846
+ ? {
1847
+ warnings: unknownMetadataWarning
1848
+ ? [...specWarnings, unknownMetadataWarning]
1849
+ : specWarnings,
1850
+ }
1851
+ : {}),
1831
1852
  };
1832
1853
  }
1833
1854
  finally {
@@ -32,6 +32,8 @@ export interface ChangeContext {
32
32
  planningHome?: PlanningHome;
33
33
  /** Parsed change metadata, when present */
34
34
  metadata?: ChangeMetadata;
35
+ /** Non-fatal metadata diagnostics for text and JSON command surfaces */
36
+ warnings?: string[];
35
37
  /**
36
38
  * Artifact IDs counted as complete only because the change declares
37
39
  * skip_specs, not because their files exist. Kept separate so status can
@@ -85,6 +87,8 @@ export interface ArtifactInstructions {
85
87
  skipped?: boolean;
86
88
  /** Present only when skipped: tells the consumer not to create the artifact */
87
89
  warning?: string;
90
+ /** Non-fatal metadata diagnostics */
91
+ warnings?: string[];
88
92
  }
89
93
  /**
90
94
  * Warning attached to instructions for an artifact skipped via skip_specs.
@@ -152,6 +156,8 @@ export interface ChangeStatus {
152
156
  applyRequires: string[];
153
157
  /** Status of each artifact */
154
158
  artifacts: ArtifactStatus[];
159
+ /** Non-fatal metadata diagnostics */
160
+ warnings?: string[];
155
161
  }
156
162
  export interface ArtifactPathSummary {
157
163
  outputPath: string;
@@ -211,5 +217,6 @@ export declare function generateInstructions(context: ChangeContext, artifactId:
211
217
  */
212
218
  export declare function formatChangeStatus(context: ChangeContext, options?: {
213
219
  storeId?: string;
220
+ implementationRoot?: string;
214
221
  }): ChangeStatus;
215
222
  //# sourceMappingURL=instruction-loader.d.ts.map
@@ -4,7 +4,7 @@ import { getSchemaDir, resolveSchema, listSchemasWithInfo } from './resolver.js'
4
4
  import { ArtifactGraph } from './graph.js';
5
5
  import { detectCompleted } from './state.js';
6
6
  import { isSpecsArtifactPath, resolveArtifactOutputPath, resolveArtifactOutputs, } from './outputs.js';
7
- import { readChangeMetadata, resolveSchemaForChange } from '../../utils/change-metadata.js';
7
+ import { formatUnknownChangeMetadataKeysMessage, readChangeMetadata, readUnknownChangeMetadataKeys, resolveSchemaForChange, } from '../../utils/change-metadata.js';
8
8
  import { FileSystemUtils } from '../../utils/file-system.js';
9
9
  import { buildActionContext, buildNextSteps, summarizePlanningHome, } from '../change-status-policy.js';
10
10
  import { readProjectConfig, validateConfigRules } from '../project-config.js';
@@ -78,6 +78,10 @@ export function loadTemplate(schemaName, templatePath, projectRoot) {
78
78
  export function loadChangeContext(projectRoot, changeName, schemaName, options = {}) {
79
79
  const changeDir = FileSystemUtils.canonicalizeExistingPath(options.changeDir ?? path.join(projectRoot, 'openspec', 'changes', changeName));
80
80
  const metadata = readChangeMetadata(changeDir, projectRoot) ?? undefined;
81
+ const unknownMetadataKeys = readUnknownChangeMetadataKeys(changeDir);
82
+ const warnings = unknownMetadataKeys.length > 0
83
+ ? [formatUnknownChangeMetadataKeysMessage(unknownMetadataKeys)]
84
+ : [];
81
85
  const resolvedSchemaName = resolveSchemaForChange(changeDir, schemaName, projectRoot, {
82
86
  metadata: metadata ?? null,
83
87
  projectConfig: options.projectConfig,
@@ -107,6 +111,7 @@ export function loadChangeContext(projectRoot, changeName, schemaName, options =
107
111
  projectRoot,
108
112
  ...(options.planningHome ? { planningHome: options.planningHome } : {}),
109
113
  ...(metadata ? { metadata } : {}),
114
+ ...(warnings.length > 0 ? { warnings } : {}),
110
115
  ...(skippedArtifacts.size > 0 ? { skippedArtifacts } : {}),
111
116
  };
112
117
  }
@@ -164,6 +169,7 @@ export function generateInstructions(context, artifactId, projectRoot, options =
164
169
  context: configContext,
165
170
  rules: configRules,
166
171
  ...(options.references !== undefined ? { references: options.references } : {}),
172
+ ...(context.warnings ? { warnings: context.warnings } : {}),
167
173
  ...(context.skippedArtifacts?.has(artifact.id)
168
174
  ? { skipped: true, warning: SKIP_SPECS_INSTRUCTIONS_WARNING }
169
175
  : {}),
@@ -279,8 +285,19 @@ export function formatChangeStatus(context, options = {}) {
279
285
  actionContext: buildActionContext({
280
286
  projectRoot: context.projectRoot,
281
287
  artifactIds,
288
+ ...(options.storeId
289
+ ? {
290
+ store: {
291
+ id: options.storeId,
292
+ ...(options.implementationRoot
293
+ ? { implementationRoot: options.implementationRoot }
294
+ : {}),
295
+ },
296
+ }
297
+ : {}),
282
298
  }),
283
299
  artifacts: artifactStatuses,
300
+ ...(context.warnings ? { warnings: context.warnings } : {}),
284
301
  };
285
302
  }
286
303
  //# sourceMappingURL=instruction-loader.js.map
@@ -5,6 +5,9 @@ export declare const InitiativeLinkSchema: z.ZodObject<{
5
5
  id: z.ZodString;
6
6
  }, z.core.$strict>;
7
7
  export type InitiativeLink = z.infer<typeof InitiativeLinkSchema>;
8
+ /** Top-level keys ChangeMetadataSchema recognizes. Anything else is ignored. */
9
+ export declare const CHANGE_METADATA_KNOWN_KEYS: readonly ["schema", "created", "goal", "affected_areas", "initiative", "skip_specs", "retire_capabilities"];
10
+ export type ChangeMetadataKnownKey = (typeof CHANGE_METADATA_KNOWN_KEYS)[number];
8
11
  export declare const ChangeMetadataSchema: z.ZodObject<{
9
12
  schema: z.ZodString;
10
13
  created: z.ZodOptional<z.ZodString>;
@@ -13,6 +13,16 @@ export const InitiativeLinkSchema = z.object({
13
13
  store: KebabIdentifierSchema('Store id'),
14
14
  id: KebabIdentifierSchema('Initiative id'),
15
15
  }).strict();
16
+ /** Top-level keys ChangeMetadataSchema recognizes. Anything else is ignored. */
17
+ export const CHANGE_METADATA_KNOWN_KEYS = [
18
+ 'schema',
19
+ 'created',
20
+ 'goal',
21
+ 'affected_areas',
22
+ 'initiative',
23
+ 'skip_specs',
24
+ 'retire_capabilities',
25
+ ];
16
26
  // Per-change metadata schema. The schema field is validated against available
17
27
  // workflow schemas when metadata is read or written.
18
28
  export const ChangeMetadataSchema = z.object({
@@ -30,6 +30,14 @@ export interface ChangeNextStepsInput {
30
30
  export interface ActionContextInput {
31
31
  projectRoot: string;
32
32
  artifactIds: string[];
33
+ /**
34
+ * Set when the root is a store: the store holds the planning artifacts,
35
+ * and `implementationRoot` is the project that declares it, if any.
36
+ */
37
+ store?: {
38
+ id: string;
39
+ implementationRoot?: string;
40
+ };
33
41
  }
34
42
  export declare function summarizePlanningHome(planningHome: PlanningHome | undefined): PlanningHomeSummary | undefined;
35
43
  export declare function buildActionContext(input: ActionContextInput): ActionContext;
@@ -10,14 +10,45 @@ export function summarizePlanningHome(planningHome) {
10
10
  };
11
11
  }
12
12
  export function buildActionContext(input) {
13
+ const scope = editScope(input);
14
+ // Keys stay in the published contract order.
13
15
  return {
14
16
  mode: 'repo-local',
15
17
  sourceOfTruth: 'repo',
16
18
  planningArtifacts: input.artifactIds,
17
19
  linkedContext: [],
18
- allowedEditRoots: [input.projectRoot],
20
+ allowedEditRoots: scope.allowedEditRoots,
19
21
  requiresAffectedAreaSelection: false,
20
- constraints: ['Repo-local change artifacts and implementation edits are scoped to this project.'],
22
+ constraints: scope.constraints,
23
+ };
24
+ }
25
+ /**
26
+ * A store holds planning artifacts only. The CLI does not route tasks to
27
+ * repos, so it names the declaring project on the current path as the edit
28
+ * root and has the agent ask before going anywhere else (#2013).
29
+ */
30
+ function editScope(input) {
31
+ if (!input.store) {
32
+ return {
33
+ allowedEditRoots: [input.projectRoot],
34
+ constraints: ['Repo-local change artifacts and implementation edits are scoped to this project.'],
35
+ };
36
+ }
37
+ const planning = `Change artifacts live in store '${input.store.id}' (${input.projectRoot}).`;
38
+ const { implementationRoot } = input.store;
39
+ if (implementationRoot) {
40
+ return {
41
+ allowedEditRoots: [implementationRoot, input.projectRoot],
42
+ constraints: [
43
+ `${planning} Implementation edits go in ${implementationRoot}, the project on the current path that declares this store; ask the user before editing any other repository.`,
44
+ ],
45
+ };
46
+ }
47
+ return {
48
+ allowedEditRoots: [input.projectRoot],
49
+ constraints: [
50
+ `${planning} OpenSpec could not determine which repository implements this change; ask the user which repository to edit, and make implementation edits there.`,
51
+ ],
21
52
  };
22
53
  }
23
54
  export function resolveNextStep(input) {
@@ -0,0 +1,24 @@
1
+ /**
2
+ * AtomCode Command Adapter
3
+ *
4
+ * Formats project commands for AtomCode.
5
+ * https://github.com/atomgit-atomcode/atomcode#custom-commands
6
+ */
7
+ import type { ToolCommandAdapter } from '../types.js';
8
+ /**
9
+ * AtomCode adapter for command generation.
10
+ * File path: .atomcode/commands/opsx-<id>.md
11
+ * Frontmatter: name, description, args
12
+ *
13
+ * AtomCode's custom-command parser reads name and args literally without YAML
14
+ * unquoting, so these controlled identifiers must stay unquoted.
15
+ * The command name matches the filename.
16
+ *
17
+ * `args` mirrors what the workflow actually accepts. AtomCode executes an
18
+ * `args: none` command straight from the slash menu, while `optional` completes
19
+ * to `/name ` and waits for a second Enter. Advertising arguments a workflow
20
+ * never reads would cost every user that extra keystroke, so only workflows
21
+ * carrying an `**Input**:` contract declare `optional` and receive $ARGUMENTS.
22
+ */
23
+ export declare const atomcodeAdapter: ToolCommandAdapter;
24
+ //# sourceMappingURL=atomcode.d.ts.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * AtomCode Command Adapter
3
+ *
4
+ * Formats project commands for AtomCode.
5
+ * https://github.com/atomgit-atomcode/atomcode#custom-commands
6
+ */
7
+ import path from 'path';
8
+ import { stringify } from 'yaml';
9
+ /** A workflow declares its invocation input with an `**Input**:` heading. */
10
+ const INPUT_HEADING = /^\*\*Input\*\*:/m;
11
+ /**
12
+ * AtomCode adapter for command generation.
13
+ * File path: .atomcode/commands/opsx-<id>.md
14
+ * Frontmatter: name, description, args
15
+ *
16
+ * AtomCode's custom-command parser reads name and args literally without YAML
17
+ * unquoting, so these controlled identifiers must stay unquoted.
18
+ * The command name matches the filename.
19
+ *
20
+ * `args` mirrors what the workflow actually accepts. AtomCode executes an
21
+ * `args: none` command straight from the slash menu, while `optional` completes
22
+ * to `/name ` and waits for a second Enter. Advertising arguments a workflow
23
+ * never reads would cost every user that extra keystroke, so only workflows
24
+ * carrying an `**Input**:` contract declare `optional` and receive $ARGUMENTS.
25
+ */
26
+ export const atomcodeAdapter = {
27
+ toolId: 'atomcode',
28
+ getFilePath(commandId) {
29
+ return path.join('.atomcode', 'commands', `opsx-${commandId}.md`);
30
+ },
31
+ formatFile(content) {
32
+ // Keep ordinary descriptions plain for the literal custom-command parser,
33
+ // while keeping special values valid YAML for frontmatter consumers.
34
+ const description = stringify({ description: content.description }, { lineWidth: 0, blockQuote: false });
35
+ const acceptsInput = INPUT_HEADING.test(content.body);
36
+ const argumentsBlock = acceptsInput ? '\n**Provided arguments**: $ARGUMENTS\n' : '';
37
+ return `---
38
+ name: opsx-${content.id}
39
+ ${description}args: ${acceptsInput ? 'optional' : 'none'}
40
+ ---
41
+ ${argumentsBlock}
42
+ ${content.body}
43
+ `;
44
+ },
45
+ };
46
+ //# sourceMappingURL=atomcode.js.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Code Studio Command Adapter
3
+ *
4
+ * Formats commands for Syncfusion Code Studio following its .prompt.md specification.
5
+ */
6
+ import type { ToolCommandAdapter } from '../types.js';
7
+ /**
8
+ * Code Studio adapter for command generation.
9
+ * File path: .codestudio/prompts/opsx-<id>.prompt.md
10
+ * Frontmatter: description
11
+ */
12
+ export declare const codeStudioAdapter: ToolCommandAdapter;
13
+ //# sourceMappingURL=codestudio.d.ts.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Code Studio Command Adapter
3
+ *
4
+ * Formats commands for Syncfusion Code Studio following its .prompt.md specification.
5
+ */
6
+ import path from 'path';
7
+ import { escapeYamlValue } from '../yaml.js';
8
+ /**
9
+ * Code Studio adapter for command generation.
10
+ * File path: .codestudio/prompts/opsx-<id>.prompt.md
11
+ * Frontmatter: description
12
+ */
13
+ export const codeStudioAdapter = {
14
+ toolId: 'codestudio',
15
+ getFilePath(commandId) {
16
+ return path.join('.codestudio', 'prompts', `opsx-${commandId}.prompt.md`);
17
+ },
18
+ formatFile(content) {
19
+ return `---
20
+ description: ${escapeYamlValue(content.description)}
21
+ ---
22
+
23
+ ${content.body}
24
+ `;
25
+ },
26
+ };
27
+ //# sourceMappingURL=codestudio.js.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * EasyCode Command Adapter
3
+ *
4
+ * Formats commands for OrionStarAI/EasyCode using its TOML command format.
5
+ * https://github.com/OrionStarAI/EasyCode
6
+ */
7
+ import type { ToolCommandAdapter } from '../types.js';
8
+ /**
9
+ * EasyCode adapter for command generation.
10
+ * File path: .easycode/commands/opsx/<id>.toml
11
+ *
12
+ * Format:
13
+ * description = "<basic-string>" single-line, backslash/quote-safe
14
+ * prompt = """<multiline-string>""" multiline, backslash/triple-quote-safe
15
+ */
16
+ export declare const easycodeAdapter: ToolCommandAdapter;
17
+ //# sourceMappingURL=easycode.d.ts.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * EasyCode Command Adapter
3
+ *
4
+ * Formats commands for OrionStarAI/EasyCode using its TOML command format.
5
+ * https://github.com/OrionStarAI/EasyCode
6
+ */
7
+ import path from 'path';
8
+ import { escapeTomlBasicString, escapeTomlMultilineBasicString } from '../toml.js';
9
+ /**
10
+ * EasyCode adapter for command generation.
11
+ * File path: .easycode/commands/opsx/<id>.toml
12
+ *
13
+ * Format:
14
+ * description = "<basic-string>" single-line, backslash/quote-safe
15
+ * prompt = """<multiline-string>""" multiline, backslash/triple-quote-safe
16
+ */
17
+ export const easycodeAdapter = {
18
+ toolId: 'easycode',
19
+ getFilePath(commandId) {
20
+ return path.join('.easycode', 'commands', 'opsx', `${commandId}.toml`);
21
+ },
22
+ formatFile(content) {
23
+ const safeDesc = escapeTomlBasicString(content.description);
24
+ const safeBody = escapeTomlMultilineBasicString(content.body);
25
+ return `description = "${safeDesc}"\n\nprompt = """\n${safeBody}\n"""\n`;
26
+ },
27
+ };
28
+ //# sourceMappingURL=easycode.js.map
@@ -4,41 +4,7 @@
4
4
  * Formats commands for Gemini CLI following its TOML specification.
5
5
  */
6
6
  import path from 'path';
7
- /**
8
- * Control characters (C0 except tab/newline/carriage return, plus DEL) are
9
- * invalid inside TOML strings and must be written as escapes.
10
- */
11
- const TOML_CONTROL_CHARS = new RegExp('[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]', 'g');
12
- /**
13
- * TOML basic strings are escape-active: a backslash or double quote in the
14
- * value breaks the file if written raw. Newlines cannot appear in a
15
- * single-line basic string at all, so they are escaped too.
16
- */
17
- function escapeTomlBasicString(value) {
18
- return value
19
- .replace(/\\/g, '\\\\')
20
- .replace(/"/g, '\\"')
21
- .replace(/\n/g, '\\n')
22
- .replace(/\r/g, '\\r')
23
- .replace(/\t/g, '\\t')
24
- .replace(TOML_CONTROL_CHARS, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
25
- }
26
- /**
27
- * Multiline basic strings keep raw newlines and tabs, but backslashes are
28
- * still escape-active, any run of three quotes would end the string, and the
29
- * same control characters are invalid as in single-line basic strings — a
30
- * lone carriage return included (only LF and CRLF may appear raw; CRLF is
31
- * normalized away so the emitted file is single-convention). Escapes are
32
- * introduced after backslash-doubling so they are not re-doubled.
33
- */
34
- function escapeTomlMultilineBasicString(value) {
35
- return value
36
- .replace(/\r\n/g, '\n')
37
- .replace(/\\/g, '\\\\')
38
- .replace(/"""/g, '""\\"')
39
- .replace(/\r/g, '\\r')
40
- .replace(TOML_CONTROL_CHARS, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
41
- }
7
+ import { escapeTomlBasicString, escapeTomlMultilineBasicString } from '../toml.js';
42
8
  /**
43
9
  * Gemini adapter for command generation.
44
10
  * File path: .gemini/commands/opsx/<id>.toml
@@ -0,0 +1,17 @@
1
+ /**
2
+ * GigaCode Command Adapter
3
+ *
4
+ * Formats commands for GigaCode using its Markdown custom command format.
5
+ * Project commands live in `.gigacode/commands/` and accept an optional
6
+ * `description` field in YAML frontmatter.
7
+ *
8
+ * @see https://gitverse.ru/docs/ai/ai-assistant-gigacode/gigacode-cli/commands
9
+ */
10
+ import type { ToolCommandAdapter } from '../types.js';
11
+ /**
12
+ * GigaCode adapter for command generation.
13
+ * File path: .gigacode/commands/opsx-<id>.md
14
+ * Format: Markdown with description frontmatter
15
+ */
16
+ export declare const gigacodeAdapter: ToolCommandAdapter;
17
+ //# sourceMappingURL=gigacode.d.ts.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * GigaCode Command Adapter
3
+ *
4
+ * Formats commands for GigaCode using its Markdown custom command format.
5
+ * Project commands live in `.gigacode/commands/` and accept an optional
6
+ * `description` field in YAML frontmatter.
7
+ *
8
+ * @see https://gitverse.ru/docs/ai/ai-assistant-gigacode/gigacode-cli/commands
9
+ */
10
+ import path from 'path';
11
+ import { escapeYamlValue } from '../yaml.js';
12
+ /**
13
+ * GigaCode adapter for command generation.
14
+ * File path: .gigacode/commands/opsx-<id>.md
15
+ * Format: Markdown with description frontmatter
16
+ */
17
+ export const gigacodeAdapter = {
18
+ toolId: 'gigacode',
19
+ getFilePath(commandId) {
20
+ return path.join('.gigacode', 'commands', `opsx-${commandId}.md`);
21
+ },
22
+ formatFile(content) {
23
+ return `---
24
+ description: ${escapeYamlValue(content.description)}
25
+ ---
26
+
27
+ ${content.body}
28
+ `;
29
+ },
30
+ };
31
+ //# sourceMappingURL=gigacode.js.map