@fission-ai/openspec 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/cli/index.d.ts +15 -0
- package/dist/cli/index.js +39 -5
- package/dist/commands/change.js +16 -1
- package/dist/commands/config.d.ts +5 -0
- package/dist/commands/config.js +5 -1
- package/dist/commands/schema.js +281 -47
- package/dist/commands/spec.js +30 -6
- package/dist/commands/validate.d.ts +23 -0
- package/dist/commands/validate.js +148 -4
- package/dist/commands/workflow/instructions.js +32 -27
- package/dist/commands/workflow/schemas.d.ts +2 -0
- package/dist/commands/workflow/schemas.js +9 -2
- package/dist/commands/workflow/status.js +2 -2
- package/dist/commands/workflow/templates.js +15 -5
- package/dist/core/archive.d.ts +14 -0
- package/dist/core/archive.js +1272 -151
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/index.js +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +3 -1
- package/dist/core/artifact-graph/instruction-loader.js +15 -5
- package/dist/core/artifact-graph/outputs.d.ts +1 -0
- package/dist/core/artifact-graph/outputs.js +67 -5
- package/dist/core/artifact-graph/resolver.d.ts +0 -12
- package/dist/core/artifact-graph/resolver.js +49 -9
- package/dist/core/artifact-graph/types.js +21 -3
- package/dist/core/available-tools.d.ts +2 -2
- package/dist/core/available-tools.js +14 -3
- package/dist/core/change-metadata/schema.d.ts +1 -0
- package/dist/core/change-metadata/schema.js +7 -0
- package/dist/core/change-status-policy.js +2 -2
- package/dist/core/command-generation/adapters/command-code.d.ts +19 -0
- package/dist/core/command-generation/adapters/command-code.js +34 -0
- package/dist/core/command-generation/adapters/index.d.ts +1 -0
- package/dist/core/command-generation/adapters/index.js +1 -0
- package/dist/core/command-generation/registry.js +2 -0
- package/dist/core/completions/command-registry.js +13 -0
- package/dist/core/config-schema.d.ts +3 -0
- package/dist/core/config-schema.js +28 -1
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.js +40 -17
- package/dist/core/file-state.js +56 -24
- package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
- package/dist/core/github-copilot/cloud-agent.js +497 -0
- package/dist/core/global-config.d.ts +11 -0
- package/dist/core/init.d.ts +23 -0
- package/dist/core/init.js +253 -46
- package/dist/core/legacy-cleanup.d.ts +10 -0
- package/dist/core/legacy-cleanup.js +52 -28
- package/dist/core/migration.d.ts +6 -4
- package/dist/core/migration.js +78 -22
- package/dist/core/parsers/requirement-blocks.d.ts +10 -0
- package/dist/core/parsers/requirement-blocks.js +93 -1
- package/dist/core/parsers/requirement-text.d.ts +8 -0
- package/dist/core/parsers/requirement-text.js +4 -3
- package/dist/core/parsers/spec-structure.d.ts +1 -1
- package/dist/core/parsers/spec-structure.js +17 -1
- package/dist/core/profile-sync-drift.js +40 -5
- package/dist/core/project-config.d.ts +3 -0
- package/dist/core/project-config.js +31 -1
- package/dist/core/root-selection.d.ts +1 -1
- package/dist/core/shared/index.d.ts +1 -0
- package/dist/core/shared/index.js +1 -0
- package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
- package/dist/core/shared/skill-content-equivalence.js +41 -0
- package/dist/core/shared/skill-paths.d.ts +13 -0
- package/dist/core/shared/skill-paths.js +23 -0
- package/dist/core/shared/tool-detection.js +66 -35
- package/dist/core/shared-skill-target.d.ts +31 -0
- package/dist/core/shared-skill-target.js +186 -0
- package/dist/core/specs-apply.d.ts +72 -0
- package/dist/core/specs-apply.js +459 -59
- package/dist/core/templates/skill-templates.d.ts +1 -1
- package/dist/core/templates/skill-templates.js +1 -1
- package/dist/core/templates/workflows/apply-change.d.ts +10 -0
- package/dist/core/templates/workflows/apply-change.js +25 -182
- package/dist/core/templates/workflows/archive-change.js +8 -4
- package/dist/core/templates/workflows/bulk-archive-change.js +30 -26
- package/dist/core/templates/workflows/continue-change.js +6 -6
- package/dist/core/templates/workflows/explore.js +42 -18
- package/dist/core/templates/workflows/onboard.js +9 -3
- package/dist/core/templates/workflows/propose.js +72 -20
- package/dist/core/templates/workflows/store-selection.d.ts +1 -1
- package/dist/core/templates/workflows/store-selection.js +1 -1
- package/dist/core/templates/workflows/sync-specs.js +96 -18
- package/dist/core/templates/workflows/update-change.js +10 -8
- package/dist/core/update.d.ts +1 -0
- package/dist/core/update.js +136 -30
- package/dist/core/validation/task-numbering.d.ts +16 -0
- package/dist/core/validation/task-numbering.js +57 -0
- package/dist/core/validation/validator.d.ts +26 -3
- package/dist/core/validation/validator.js +189 -19
- package/dist/core/version-check.js +6 -11
- package/dist/telemetry/config.d.ts +3 -4
- package/dist/telemetry/index.d.ts +12 -5
- package/dist/telemetry/index.js +27 -9
- package/dist/utils/change-metadata.d.ts +17 -4
- package/dist/utils/change-metadata.js +26 -4
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +14 -2
- package/dist/utils/command-references.js +45 -3
- package/dist/utils/file-system.d.ts +10 -0
- package/dist/utils/file-system.js +70 -0
- package/dist/utils/interactive.d.ts +52 -0
- package/dist/utils/interactive.js +135 -0
- package/dist/utils/spec-discovery.d.ts +4 -4
- package/dist/utils/spec-discovery.js +19 -6
- package/dist/utils/task-progress.d.ts +51 -1
- package/dist/utils/task-progress.js +93 -41
- package/package.json +1 -1
- package/schemas/spec-driven/schema.yaml +8 -6
- package/schemas/spec-driven/templates/proposal.md +6 -4
package/dist/core/update.js
CHANGED
|
@@ -13,8 +13,8 @@ import { FileSystemUtils } from '../utils/file-system.js';
|
|
|
13
13
|
import { getSkillReferenceTransformer, getTransformerForTool, transformToSkillReferences } from '../utils/command-references.js';
|
|
14
14
|
import { AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
|
|
15
15
|
import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
|
|
16
|
-
import { getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, } from './shared/index.js';
|
|
17
|
-
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatDeferredGlobalPromptSummary, formatCleanupSummary, formatDetectionSummary, getLegacyGlobalPromptMatches, getLegacyWorkflowIdsForTool, getToolsFromLegacyArtifacts, omitGlobalLegacyPromptFiles, pickGlobalLegacyPromptFiles, } from './legacy-cleanup.js';
|
|
16
|
+
import { getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, } from './shared/index.js';
|
|
17
|
+
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatDeferredGlobalPromptSummary, formatCleanupSummary, formatDetectionSummary, getLegacyGlobalPromptMatches, getLegacyWorkflowIdsForTool, getToolsFromLegacyArtifacts, omitGlobalLegacyPromptFiles, omitToolLegacyArtifacts, pickGlobalLegacyPromptFiles, } from './legacy-cleanup.js';
|
|
18
18
|
import { isInteractive } from '../utils/interactive.js';
|
|
19
19
|
import { getGlobalConfig } from './global-config.js';
|
|
20
20
|
import { getProfileWorkflows, ALL_WORKFLOWS, CORE_WORKFLOWS } from './profiles.js';
|
|
@@ -23,6 +23,8 @@ import { getAvailableTools } from './available-tools.js';
|
|
|
23
23
|
import { WORKFLOW_TO_SKILL_DIR, getConfiguredToolsForProfileSync, getToolsNeedingProfileSync, } from './profile-sync-drift.js';
|
|
24
24
|
import { scanInstalledWorkflows as scanInstalledWorkflowsShared, migrateIfNeeded as migrateIfNeededShared, findLegacyToolMigrations, migrateLegacyToolDirs, describeLegacyMigration, legacyMigrationNotice, keptInPlaceNotice, hasMovableContent, } from './migration.js';
|
|
25
25
|
import { resolveCommandSurfaceCapability, resolveCommandInvocation, shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
|
|
26
|
+
import { writeSharedSkillTarget, sharedSkillRootOwner } from './shared-skill-target.js';
|
|
27
|
+
import { includesGitHubCopilot, writeCopilotCloudFiles, removeCopilotCloudFiles, isCopilotCloudEnabled, readCopilotCloudOptIn, findUnmanagedCloudFiles } from './github-copilot/cloud-agent.js';
|
|
26
28
|
const require = createRequire(import.meta.url);
|
|
27
29
|
const { version: OPENSPEC_VERSION } = require('../../package.json');
|
|
28
30
|
/**
|
|
@@ -80,6 +82,7 @@ export class UpdateCommand {
|
|
|
80
82
|
const { newlyConfiguredTools, workflowOverrides: legacyWorkflowOverrides, deferredGlobalCleanup, } = legacyUpgrade;
|
|
81
83
|
// 5. Find configured tools
|
|
82
84
|
const configuredTools = getConfiguredToolsForProfileSync(resolvedProjectPath);
|
|
85
|
+
const configuredAndNewTools = [...new Set([...configuredTools, ...newlyConfiguredTools])];
|
|
83
86
|
if (configuredTools.length === 0 && newlyConfiguredTools.length === 0) {
|
|
84
87
|
if (deferredGlobalCleanup) {
|
|
85
88
|
await this.performDeferredGlobalPromptCleanup(resolvedProjectPath, deferredGlobalCleanup);
|
|
@@ -94,6 +97,7 @@ export class UpdateCommand {
|
|
|
94
97
|
}
|
|
95
98
|
return;
|
|
96
99
|
}
|
|
100
|
+
await this.syncCopilotCloudFiles(resolvedProjectPath, configuredAndNewTools);
|
|
97
101
|
console.log(chalk.yellow('No configured tools found.'));
|
|
98
102
|
console.log(chalk.dim('Run "openspec init" to set up tools.'));
|
|
99
103
|
return;
|
|
@@ -107,7 +111,13 @@ export class UpdateCommand {
|
|
|
107
111
|
const statusByTool = new Map(toolStatuses.map((status) => [status.toolId, status]));
|
|
108
112
|
// 7. Smart update detection
|
|
109
113
|
const toolsNeedingVersionUpdate = toolStatuses
|
|
110
|
-
.filter((s) =>
|
|
114
|
+
.filter((s) => {
|
|
115
|
+
if (!s.needsUpdate || delivery !== 'commands') {
|
|
116
|
+
return s.needsUpdate;
|
|
117
|
+
}
|
|
118
|
+
const tool = AI_TOOLS.find((candidate) => candidate.value === s.toolId);
|
|
119
|
+
return !tool || !hasGlobalSkillTarget(tool);
|
|
120
|
+
})
|
|
111
121
|
.map((s) => s.toolId);
|
|
112
122
|
const toolsNeedingConfigSync = getToolsNeedingProfileSync(resolvedProjectPath, desiredWorkflows, delivery, configuredTools);
|
|
113
123
|
const toolsToUpdateSet = new Set([
|
|
@@ -121,6 +131,7 @@ export class UpdateCommand {
|
|
|
121
131
|
}
|
|
122
132
|
// All tools are up to date
|
|
123
133
|
this.displayUpToDateMessage(toolStatuses);
|
|
134
|
+
await this.syncCopilotCloudFiles(resolvedProjectPath, configuredAndNewTools);
|
|
124
135
|
// Still check for new tool directories and extra workflows
|
|
125
136
|
this.detectNewTools(resolvedProjectPath, configuredTools);
|
|
126
137
|
this.displayExtraWorkflowsNote(resolvedProjectPath, configuredTools, desiredWorkflows);
|
|
@@ -153,11 +164,12 @@ export class UpdateCommand {
|
|
|
153
164
|
let removedDeselectedSkillCount = 0;
|
|
154
165
|
for (const toolId of toolsToUpdate) {
|
|
155
166
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
156
|
-
if (!tool
|
|
167
|
+
if (!tool || !toolSupportsSkills(tool))
|
|
157
168
|
continue;
|
|
158
169
|
const spinner = ora(`Updating ${tool.name}...`).start();
|
|
159
170
|
try {
|
|
160
|
-
const skillsDir =
|
|
171
|
+
const skillsDir = resolveToolSkillsDir(resolvedProjectPath, tool);
|
|
172
|
+
const skillsRoot = hasGlobalSkillTarget(tool) ? skillsDir : resolvedProjectPath;
|
|
161
173
|
const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
|
|
162
174
|
const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
|
|
163
175
|
const toolWorkflows = legacyWorkflowOverrides[tool.value] ?? desiredWorkflows;
|
|
@@ -170,13 +182,18 @@ export class UpdateCommand {
|
|
|
170
182
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
171
183
|
const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
|
|
172
184
|
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
185
|
+
FileSystemUtils.assertPathWithin(skillsRoot, skillFile);
|
|
173
186
|
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
174
187
|
}
|
|
175
|
-
|
|
188
|
+
writeSharedSkillTarget(resolvedProjectPath, tool.value);
|
|
189
|
+
removedDeselectedSkillCount += await this.removeUnselectedSkillDirs(skillsRoot, skillsDir, toolWorkflows);
|
|
176
190
|
}
|
|
177
191
|
// Delete skill directories if delivery is commands-only
|
|
178
|
-
if (shouldRemoveSkillsForTool(tool.value, delivery)) {
|
|
179
|
-
removedSkillCount += await this.removeSkillDirs(skillsDir);
|
|
192
|
+
if (shouldRemoveSkillsForTool(tool.value, delivery) && !hasGlobalSkillTarget(tool)) {
|
|
193
|
+
removedSkillCount += await this.removeSkillDirs(skillsRoot, skillsDir);
|
|
194
|
+
// Persist the selected owner even when commands-only delivery leaves
|
|
195
|
+
// this target with no generated skills.
|
|
196
|
+
writeSharedSkillTarget(resolvedProjectPath, tool.value);
|
|
180
197
|
// A tool with no command adapter now has zero OpenSpec artifacts;
|
|
181
198
|
// say so like init does, rather than deleting its skills silently
|
|
182
199
|
// and letting tool detection re-suggest an init that would also
|
|
@@ -191,7 +208,7 @@ export class UpdateCommand {
|
|
|
191
208
|
if (adapter) {
|
|
192
209
|
const generatedCommands = generateCommands(commandContents, adapter);
|
|
193
210
|
for (const cmd of generatedCommands) {
|
|
194
|
-
const commandFile =
|
|
211
|
+
const commandFile = FileSystemUtils.resolveProjectArtifactPath(resolvedProjectPath, cmd.path);
|
|
195
212
|
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
|
|
196
213
|
}
|
|
197
214
|
removedDeselectedCommandCount += await this.removeUnselectedCommandFiles(resolvedProjectPath, toolId, toolWorkflows);
|
|
@@ -206,6 +223,12 @@ export class UpdateCommand {
|
|
|
206
223
|
}
|
|
207
224
|
spinner.succeed(`Updated ${tool.name}`);
|
|
208
225
|
updatedTools.push(tool.name);
|
|
226
|
+
for (const migration of migrateLegacyToolDirs(resolvedProjectPath, [tool.value], 'after-generation')) {
|
|
227
|
+
if (hasMovableContent(migration)) {
|
|
228
|
+
console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
|
|
229
|
+
}
|
|
230
|
+
this.reportKeptInPlace(migration);
|
|
231
|
+
}
|
|
209
232
|
}
|
|
210
233
|
catch (error) {
|
|
211
234
|
spinner.fail(`Failed to update ${tool.name}`);
|
|
@@ -282,7 +305,7 @@ export class UpdateCommand {
|
|
|
282
305
|
}
|
|
283
306
|
console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
|
|
284
307
|
}
|
|
285
|
-
|
|
308
|
+
await this.syncCopilotCloudFiles(resolvedProjectPath, configuredAndNewTools);
|
|
286
309
|
// 13. Detect new tool directories not currently configured
|
|
287
310
|
this.detectNewTools(resolvedProjectPath, configuredAndNewTools);
|
|
288
311
|
// 14. Display note about extra workflows not in profile
|
|
@@ -296,6 +319,51 @@ export class UpdateCommand {
|
|
|
296
319
|
}
|
|
297
320
|
console.log();
|
|
298
321
|
console.log(chalk.dim('Restart your IDE for changes to take effect.'));
|
|
322
|
+
if (failedTools.length > 0) {
|
|
323
|
+
throw new Error(`OpenSpec update failed for: ${failedTools.map((tool) => tool.name).join(', ')}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async syncCopilotCloudFiles(projectPath, configuredTools) {
|
|
327
|
+
try {
|
|
328
|
+
if (includesGitHubCopilot(configuredTools)) {
|
|
329
|
+
// Cloud files are opt-in (see cloud-agent.ts). `update` never prompts,
|
|
330
|
+
// so it only refreshes files the user has already opted into (via
|
|
331
|
+
// `openspec init` or a `githubCopilot.cloudAgent: true` config), or that
|
|
332
|
+
// a pre-opt-in project already has. Opting in is a deliberate init/config
|
|
333
|
+
// step, never a silent side effect of running update.
|
|
334
|
+
if (await isCopilotCloudEnabled(projectPath)) {
|
|
335
|
+
await writeCopilotCloudFiles(projectPath);
|
|
336
|
+
const collisions = await findUnmanagedCloudFiles(projectPath);
|
|
337
|
+
if (collisions.length > 0) {
|
|
338
|
+
console.log(chalk.dim(`Left your existing ${collisions.join(' and ')} untouched — add the OpenSpec ` +
|
|
339
|
+
`install step by hand so the Copilot cloud agent can run openspec.`));
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
// Explicit opt-out (githubCopilot.cloudAgent: false) means "not here":
|
|
344
|
+
// remove any managed files a prior opt-in left behind (customized files
|
|
345
|
+
// are preserved). If the user simply never decided, stay quiet unless
|
|
346
|
+
// we're at an interactive terminal, where a one-line hint aids discovery.
|
|
347
|
+
if (readCopilotCloudOptIn(projectPath) === false) {
|
|
348
|
+
const removed = await removeCopilotCloudFiles(projectPath);
|
|
349
|
+
if (removed > 0) {
|
|
350
|
+
console.log(chalk.dim(`Removed: ${removed} Copilot cloud agent file(s) (opted out of cloud files)`));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else if (isInteractive()) {
|
|
354
|
+
console.log(chalk.dim("GitHub Copilot cloud coding-agent files are available (opt-in). Enable with 'openspec init --copilot-cloud'."));
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const removed = await removeCopilotCloudFiles(projectPath);
|
|
359
|
+
if (removed > 0) {
|
|
360
|
+
console.log(chalk.dim(`Removed: ${removed} Copilot cloud agent file(s) (github-copilot not configured)`));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
365
|
+
console.warn(`Warning: failed to sync Copilot cloud agent files: ${message}`);
|
|
366
|
+
}
|
|
299
367
|
}
|
|
300
368
|
/**
|
|
301
369
|
* Display message when all tools are up to date.
|
|
@@ -387,18 +455,19 @@ export class UpdateCommand {
|
|
|
387
455
|
* Removes skill directories for workflows when delivery changed to commands-only.
|
|
388
456
|
* Returns the number of directories removed.
|
|
389
457
|
*/
|
|
390
|
-
async removeSkillDirs(skillsDir) {
|
|
458
|
+
async removeSkillDirs(skillsRoot, skillsDir) {
|
|
391
459
|
let removed = 0;
|
|
392
460
|
for (const workflow of ALL_WORKFLOWS) {
|
|
393
461
|
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
394
462
|
if (!dirName)
|
|
395
463
|
continue;
|
|
396
464
|
const skillDir = path.join(skillsDir, dirName);
|
|
465
|
+
if (!fs.existsSync(skillDir))
|
|
466
|
+
continue;
|
|
467
|
+
FileSystemUtils.assertPathWithin(skillsRoot, skillDir);
|
|
397
468
|
try {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
removed++;
|
|
401
|
-
}
|
|
469
|
+
await fs.promises.rm(skillDir, { recursive: true, force: true });
|
|
470
|
+
removed++;
|
|
402
471
|
}
|
|
403
472
|
catch {
|
|
404
473
|
// Ignore errors
|
|
@@ -410,7 +479,7 @@ export class UpdateCommand {
|
|
|
410
479
|
* Removes skill directories for workflows that are no longer selected in the active profile.
|
|
411
480
|
* Returns the number of directories removed.
|
|
412
481
|
*/
|
|
413
|
-
async removeUnselectedSkillDirs(skillsDir, desiredWorkflows) {
|
|
482
|
+
async removeUnselectedSkillDirs(skillsRoot, skillsDir, desiredWorkflows) {
|
|
414
483
|
const desiredSet = new Set(desiredWorkflows);
|
|
415
484
|
let removed = 0;
|
|
416
485
|
for (const workflow of ALL_WORKFLOWS) {
|
|
@@ -420,11 +489,12 @@ export class UpdateCommand {
|
|
|
420
489
|
if (!dirName)
|
|
421
490
|
continue;
|
|
422
491
|
const skillDir = path.join(skillsDir, dirName);
|
|
492
|
+
if (!fs.existsSync(skillDir))
|
|
493
|
+
continue;
|
|
494
|
+
FileSystemUtils.assertPathWithin(skillsRoot, skillDir);
|
|
423
495
|
try {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
removed++;
|
|
427
|
-
}
|
|
496
|
+
await fs.promises.rm(skillDir, { recursive: true, force: true });
|
|
497
|
+
removed++;
|
|
428
498
|
}
|
|
429
499
|
catch {
|
|
430
500
|
// Ignore errors
|
|
@@ -443,7 +513,7 @@ export class UpdateCommand {
|
|
|
443
513
|
return 0;
|
|
444
514
|
for (const workflow of ALL_WORKFLOWS) {
|
|
445
515
|
const cmdPath = adapter.getFilePath(workflow);
|
|
446
|
-
const fullPath =
|
|
516
|
+
const fullPath = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmdPath);
|
|
447
517
|
try {
|
|
448
518
|
if (fs.existsSync(fullPath)) {
|
|
449
519
|
await fs.promises.unlink(fullPath);
|
|
@@ -470,7 +540,7 @@ export class UpdateCommand {
|
|
|
470
540
|
if (desiredSet.has(workflow))
|
|
471
541
|
continue;
|
|
472
542
|
const cmdPath = adapter.getFilePath(workflow);
|
|
473
|
-
const fullPath =
|
|
543
|
+
const fullPath = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmdPath);
|
|
474
544
|
try {
|
|
475
545
|
if (fs.existsSync(fullPath)) {
|
|
476
546
|
await fs.promises.unlink(fullPath);
|
|
@@ -577,7 +647,7 @@ export class UpdateCommand {
|
|
|
577
647
|
const canPrompt = isInteractive();
|
|
578
648
|
if (this.force) {
|
|
579
649
|
const legacyUpgrade = await this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
|
|
580
|
-
await this.performImmediateLegacyCleanup(projectPath, detection);
|
|
650
|
+
await this.performImmediateLegacyCleanup(projectPath, detection, legacyUpgrade.skippedSharedSkillTools);
|
|
581
651
|
return {
|
|
582
652
|
...legacyUpgrade,
|
|
583
653
|
deferredGlobalCleanup: pickGlobalLegacyPromptFiles(detection, detection.globalSlashCommandFiles),
|
|
@@ -598,7 +668,7 @@ export class UpdateCommand {
|
|
|
598
668
|
});
|
|
599
669
|
if (shouldCleanup) {
|
|
600
670
|
const legacyUpgrade = await this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
|
|
601
|
-
await this.performImmediateLegacyCleanup(projectPath, detection);
|
|
671
|
+
await this.performImmediateLegacyCleanup(projectPath, detection, legacyUpgrade.skippedSharedSkillTools);
|
|
602
672
|
return {
|
|
603
673
|
...legacyUpgrade,
|
|
604
674
|
deferredGlobalCleanup: pickGlobalLegacyPromptFiles(detection, detection.globalSlashCommandFiles),
|
|
@@ -613,8 +683,10 @@ export class UpdateCommand {
|
|
|
613
683
|
/**
|
|
614
684
|
* Cleans approved repo-local legacy artifacts before configured tools refresh.
|
|
615
685
|
*/
|
|
616
|
-
async performImmediateLegacyCleanup(projectPath, detection) {
|
|
617
|
-
|
|
686
|
+
async performImmediateLegacyCleanup(projectPath, detection, skippedSharedSkillTools = []) {
|
|
687
|
+
// Tools whose upgrade was skipped (shared root owned by another) had no
|
|
688
|
+
// replacement written, so their repo-local legacy files must be preserved.
|
|
689
|
+
const immediateDetection = omitToolLegacyArtifacts(omitGlobalLegacyPromptFiles(detection), skippedSharedSkillTools);
|
|
618
690
|
if (immediateDetection.hasLegacyArtifacts) {
|
|
619
691
|
await this.performLegacyCleanup(projectPath, immediateDetection);
|
|
620
692
|
}
|
|
@@ -718,14 +790,16 @@ export class UpdateCommand {
|
|
|
718
790
|
const inferredCodexWorkflows = getLegacyWorkflowIdsForTool(detection, 'codex');
|
|
719
791
|
// Create skills/commands for selected tools using effective profile+delivery.
|
|
720
792
|
const newlyConfigured = [];
|
|
793
|
+
const skippedSharedSkillTools = [];
|
|
721
794
|
const workflowOverrides = {};
|
|
722
795
|
for (const toolId of selectedTools) {
|
|
723
796
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
724
|
-
if (!tool
|
|
797
|
+
if (!tool || !toolSupportsSkills(tool))
|
|
725
798
|
continue;
|
|
726
799
|
const spinner = ora(`Setting up ${tool.name}...`).start();
|
|
727
800
|
try {
|
|
728
|
-
const skillsDir =
|
|
801
|
+
const skillsDir = resolveToolSkillsDir(projectPath, tool);
|
|
802
|
+
const skillsRoot = hasGlobalSkillTarget(tool) ? skillsDir : projectPath;
|
|
729
803
|
const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
|
|
730
804
|
const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
|
|
731
805
|
const toolWorkflows = (tool.value === 'codex' && inferredCodexWorkflows.length > 0
|
|
@@ -736,6 +810,30 @@ export class UpdateCommand {
|
|
|
736
810
|
}
|
|
737
811
|
const skillTemplates = getSkillTemplates(toolWorkflows);
|
|
738
812
|
const commandContents = getCommandContents(toolWorkflows);
|
|
813
|
+
// A shared skills root (e.g. `.agents`) already owned by another tool
|
|
814
|
+
// must not be overwritten by a tool inferred from legacy artifacts: a
|
|
815
|
+
// Codex install detected only from global `~/.codex/prompts` would
|
|
816
|
+
// otherwise rewrite an existing vendor-neutral `agents` tree with
|
|
817
|
+
// Codex-specific syntax and flip its ownership marker `agents → codex`.
|
|
818
|
+
// Leave the established owner in place. (init applies the same
|
|
819
|
+
// one-writer rule up front when both targets are selected.)
|
|
820
|
+
//
|
|
821
|
+
// Skipping here means the tool is never recorded as configured, so a
|
|
822
|
+
// persistent legacy signal re-offers it on later runs. Because no
|
|
823
|
+
// replacement is written, this tool is also exempted from immediate
|
|
824
|
+
// legacy cleanup (see skippedSharedSkillTools) — otherwise a repo-local
|
|
825
|
+
// `.codex/prompts` would be deleted with nothing put in its place. That
|
|
826
|
+
// repeat is idempotent and harmless — the alternative is the silent
|
|
827
|
+
// hijack this prevents.
|
|
828
|
+
const sharedOwner = shouldGenerateSkills
|
|
829
|
+
? sharedSkillRootOwner(projectPath, tool.value)
|
|
830
|
+
: undefined;
|
|
831
|
+
if (sharedOwner) {
|
|
832
|
+
const ownerName = AI_TOOLS.find((candidate) => candidate.value === sharedOwner)?.name ?? sharedOwner;
|
|
833
|
+
spinner.info(`Skipped ${tool.name}: ${tool.skillsDir}/skills is already managed by another tool (${ownerName}).`);
|
|
834
|
+
skippedSharedSkillTools.push(tool.value);
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
739
837
|
// Create skill files when delivery includes skills
|
|
740
838
|
if (shouldGenerateSkills) {
|
|
741
839
|
for (const { template, dirName } of skillTemplates) {
|
|
@@ -743,8 +841,10 @@ export class UpdateCommand {
|
|
|
743
841
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
744
842
|
const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
|
|
745
843
|
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
844
|
+
FileSystemUtils.assertPathWithin(skillsRoot, skillFile);
|
|
746
845
|
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
747
846
|
}
|
|
847
|
+
writeSharedSkillTarget(projectPath, tool.value);
|
|
748
848
|
}
|
|
749
849
|
// Create commands when delivery includes commands
|
|
750
850
|
if (shouldGenerateCommands) {
|
|
@@ -752,13 +852,19 @@ export class UpdateCommand {
|
|
|
752
852
|
if (adapter) {
|
|
753
853
|
const generatedCommands = generateCommands(commandContents, adapter);
|
|
754
854
|
for (const cmd of generatedCommands) {
|
|
755
|
-
const commandFile =
|
|
855
|
+
const commandFile = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmd.path);
|
|
756
856
|
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
|
|
757
857
|
}
|
|
758
858
|
}
|
|
759
859
|
}
|
|
760
860
|
spinner.succeed(`Setup complete for ${tool.name}`);
|
|
761
861
|
newlyConfigured.push(toolId);
|
|
862
|
+
for (const migration of migrateLegacyToolDirs(projectPath, [tool.value], 'after-generation')) {
|
|
863
|
+
if (hasMovableContent(migration)) {
|
|
864
|
+
console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
|
|
865
|
+
}
|
|
866
|
+
this.reportKeptInPlace(migration);
|
|
867
|
+
}
|
|
762
868
|
}
|
|
763
869
|
catch (error) {
|
|
764
870
|
spinner.fail(`Failed to set up ${tool.name}`);
|
|
@@ -768,7 +874,7 @@ export class UpdateCommand {
|
|
|
768
874
|
if (newlyConfigured.length > 0) {
|
|
769
875
|
console.log();
|
|
770
876
|
}
|
|
771
|
-
return { newlyConfiguredTools: newlyConfigured, workflowOverrides };
|
|
877
|
+
return { newlyConfiguredTools: newlyConfigured, workflowOverrides, skippedSharedSkillTools };
|
|
772
878
|
}
|
|
773
879
|
}
|
|
774
880
|
//# sourceMappingURL=update.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface TaskNumberingDocument {
|
|
2
|
+
path: string;
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
export interface TaskNumberingIssue {
|
|
6
|
+
path: string;
|
|
7
|
+
line: number;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Finds ambiguous task references across the task files tracked by a change.
|
|
12
|
+
* Numbering is interpreted only inside `## N.` groups. Unnumbered sections,
|
|
13
|
+
* unnumbered tasks, and files without numbered groups are intentionally ignored.
|
|
14
|
+
*/
|
|
15
|
+
export declare function findTaskNumberingIssues(documents: readonly TaskNumberingDocument[]): TaskNumberingIssue[];
|
|
16
|
+
//# sourceMappingURL=task-numbering.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { parseTaskLines } from '../../utils/task-progress.js';
|
|
2
|
+
const LEVEL_TWO_HEADING = /^ {0,3}##(?!#)(?:[ \t]+|[ \t]*\r?$)/;
|
|
3
|
+
const NUMBERED_GROUP_HEADING = /^ {0,3}##[ \t]+(\d+)\.(?:[ \t]|\r?$)/;
|
|
4
|
+
const TASK_ID = /^(\d+(?:\.\d+)+(?:[A-Za-z]+)?)(?=\s|$)/;
|
|
5
|
+
/**
|
|
6
|
+
* Finds ambiguous task references across the task files tracked by a change.
|
|
7
|
+
* Numbering is interpreted only inside `## N.` groups. Unnumbered sections,
|
|
8
|
+
* unnumbered tasks, and files without numbered groups are intentionally ignored.
|
|
9
|
+
*/
|
|
10
|
+
export function findTaskNumberingIssues(documents) {
|
|
11
|
+
const issues = [];
|
|
12
|
+
const firstLocationById = new Map();
|
|
13
|
+
for (const document of documents) {
|
|
14
|
+
const lines = document.content.split('\n');
|
|
15
|
+
if (!lines.some((line) => NUMBERED_GROUP_HEADING.test(line)))
|
|
16
|
+
continue;
|
|
17
|
+
let currentGroup;
|
|
18
|
+
lines.forEach((line, index) => {
|
|
19
|
+
if (LEVEL_TWO_HEADING.test(line)) {
|
|
20
|
+
currentGroup = line.match(NUMBERED_GROUP_HEADING)?.[1];
|
|
21
|
+
}
|
|
22
|
+
if (currentGroup === undefined)
|
|
23
|
+
return;
|
|
24
|
+
const task = parseTaskLines(line)[0];
|
|
25
|
+
const id = task?.description.match(TASK_ID)?.[1];
|
|
26
|
+
if (!id)
|
|
27
|
+
return;
|
|
28
|
+
const lineNumber = index + 1;
|
|
29
|
+
const taskGroup = id.split('.')[0];
|
|
30
|
+
const normalizedTaskGroup = taskGroup.replace(/^0+(?=\d)/, '');
|
|
31
|
+
const normalizedCurrentGroup = currentGroup.replace(/^0+(?=\d)/, '');
|
|
32
|
+
if (normalizedTaskGroup !== normalizedCurrentGroup) {
|
|
33
|
+
issues.push({
|
|
34
|
+
path: document.path,
|
|
35
|
+
line: lineNumber,
|
|
36
|
+
message: `Task "${id}" is under group ${currentGroup}, but its leading number points to group ${taskGroup}. Move it to group ${taskGroup} or renumber it.`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const firstLocation = firstLocationById.get(id);
|
|
40
|
+
if (firstLocation !== undefined) {
|
|
41
|
+
const firstDeclaration = firstLocation.path === document.path
|
|
42
|
+
? `on line ${firstLocation.line}`
|
|
43
|
+
: `in ${firstLocation.path} on line ${firstLocation.line}`;
|
|
44
|
+
issues.push({
|
|
45
|
+
path: document.path,
|
|
46
|
+
line: lineNumber,
|
|
47
|
+
message: `Task ID "${id}" is duplicated; it was first declared ${firstDeclaration}.`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
firstLocationById.set(id, { path: document.path, line: lineNumber });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return issues;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=task-numbering.js.map
|
|
@@ -12,12 +12,35 @@ export declare class Validator {
|
|
|
12
12
|
* Validate delta-formatted spec files under a change directory.
|
|
13
13
|
* Enforces:
|
|
14
14
|
* - At least one delta across all files
|
|
15
|
-
* - ADDED/MODIFIED: each requirement has
|
|
15
|
+
* - ADDED/MODIFIED: each requirement has at least one scenario; missing
|
|
16
|
+
* English SHALL/MUST keywords are guidance unless strict mode is enabled
|
|
16
17
|
* - REMOVED: names only; no scenario/description required
|
|
17
18
|
* - RENAMED: pairs well-formed
|
|
18
19
|
* - No duplicates within sections; no cross-section conflicts per spec
|
|
20
|
+
*
|
|
21
|
+
* When `options.mainSpecsDir` is given, MODIFIED blocks are also checked
|
|
22
|
+
* against the current main specs for the scenario loss archive refuses to
|
|
23
|
+
* apply (#1477). When `options.projectRoot` is given, the schema's tracked
|
|
24
|
+
* task files are checked for ambiguous numbering (#1520). Omitting either
|
|
25
|
+
* option keeps existing library and archive callers behaving as before.
|
|
26
|
+
*/
|
|
27
|
+
validateChangeDeltaSpecs(changeDir: string, options?: {
|
|
28
|
+
mainSpecsDir?: string;
|
|
29
|
+
projectRoot?: string;
|
|
30
|
+
}): Promise<ValidationReport>;
|
|
31
|
+
private collectTaskNumberingIssues;
|
|
32
|
+
/**
|
|
33
|
+
* Report MODIFIED requirements whose block omits a scenario the main spec
|
|
34
|
+
* still carries. Uses the same comparison archive applies, so validate can
|
|
35
|
+
* only report what archive would refuse.
|
|
36
|
+
*
|
|
37
|
+
* Silent when the main spec or the requirement header is absent: applying a
|
|
38
|
+
* MODIFIED against a base that is not there yet is a different failure (a
|
|
39
|
+
* sister change still in flight is the legitimate case), and archive is the
|
|
40
|
+
* gate for it. A spec that exists but cannot be read is not absent, though —
|
|
41
|
+
* archive aborts on it, so reporting it beats calling the change valid.
|
|
19
42
|
*/
|
|
20
|
-
|
|
43
|
+
private findScenarioLossIssues;
|
|
21
44
|
private formatInvalidMarkerMessage;
|
|
22
45
|
private convertZodErrors;
|
|
23
46
|
private applySpecRules;
|
|
@@ -29,7 +52,7 @@ export declare class Validator {
|
|
|
29
52
|
private extractRequirementText;
|
|
30
53
|
private containsShallOrMust;
|
|
31
54
|
/**
|
|
32
|
-
* Build
|
|
55
|
+
* Build a message for a requirement block whose body lacks SHALL/MUST.
|
|
33
56
|
*
|
|
34
57
|
* When the SHALL/MUST keyword already appears in the requirement header (e.g.
|
|
35
58
|
* `### Requirement: The system SHALL ...`) the original generic error
|