@fission-ai/openspec 1.12.0 → 1.13.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.
- package/README.md +3 -15
- package/dist/commands/change.js +12 -1
- package/dist/commands/config.d.ts +9 -0
- package/dist/commands/config.js +161 -22
- package/dist/commands/feedback.js +6 -4
- package/dist/commands/schema.js +30 -9
- package/dist/commands/store.js +5 -2
- package/dist/commands/validate.d.ts +7 -0
- package/dist/commands/validate.js +63 -1
- package/dist/commands/workflow/instructions.js +170 -16
- package/dist/commands/workflow/new-change.js +20 -0
- package/dist/commands/workflow/shared.d.ts +8 -0
- package/dist/commands/workflow/shared.js +8 -0
- package/dist/commands/workflow/status.d.ts +5 -1
- package/dist/commands/workflow/status.js +42 -5
- package/dist/core/archive.js +17 -1
- package/dist/core/artifact-graph/schema.d.ts +14 -0
- package/dist/core/artifact-graph/schema.js +52 -0
- package/dist/core/artifact-graph/types.js +12 -1
- package/dist/core/change-status-policy.d.ts +14 -0
- package/dist/core/change-status-policy.js +14 -6
- package/dist/core/command-generation/generator.js +2 -0
- package/dist/core/completion-tip.js +7 -7
- package/dist/core/completions/installers/bash-installer.js +15 -7
- package/dist/core/completions/installers/shell-quote.d.ts +11 -0
- package/dist/core/completions/installers/shell-quote.js +13 -0
- package/dist/core/completions/installers/zsh-installer.js +3 -2
- package/dist/core/config-schema.d.ts +1 -1
- package/dist/core/config.d.ts +16 -0
- package/dist/core/config.js +27 -1
- package/dist/core/global-config.d.ts +30 -2
- package/dist/core/global-config.js +57 -3
- package/dist/core/init.js +33 -3
- package/dist/core/legacy-cleanup.d.ts +4 -0
- package/dist/core/legacy-cleanup.js +180 -12
- package/dist/core/list.js +55 -11
- package/dist/core/migration.js +6 -1
- package/dist/core/onboarding-commands.d.ts +11 -1
- package/dist/core/onboarding-commands.js +26 -0
- package/dist/core/parsers/change-parser.d.ts +12 -1
- package/dist/core/parsers/change-parser.js +85 -87
- package/dist/core/parsers/markdown-parser.js +4 -3
- package/dist/core/parsers/requirement-blocks.d.ts +44 -3
- package/dist/core/parsers/requirement-blocks.js +164 -43
- package/dist/core/parsers/requirement-text.d.ts +13 -2
- package/dist/core/parsers/requirement-text.js +34 -7
- package/dist/core/parsers/spec-structure.js +4 -1
- package/dist/core/project-config.d.ts +9 -1
- package/dist/core/project-config.js +11 -2
- package/dist/core/references.d.ts +20 -0
- package/dist/core/references.js +56 -0
- package/dist/core/shared/skill-content-equivalence.js +5 -1
- package/dist/core/shared/skill-generation.js +36 -8
- package/dist/core/shared/tool-detection.js +37 -4
- package/dist/core/specs-apply.js +260 -18
- package/dist/core/store/git.d.ts +33 -0
- package/dist/core/store/git.js +60 -6
- package/dist/core/store/operations.d.ts +1 -1
- package/dist/core/store/operations.js +32 -10
- package/dist/core/store/registry.d.ts +2 -1
- package/dist/core/store/registry.js +5 -1
- package/dist/core/templates/optional-workflow.d.ts +69 -0
- package/dist/core/templates/optional-workflow.js +140 -0
- package/dist/core/templates/workflows/apply-change.js +18 -3
- package/dist/core/templates/workflows/archive-change.js +50 -15
- package/dist/core/templates/workflows/bulk-archive-change.js +63 -13
- package/dist/core/templates/workflows/continue-change.js +14 -2
- package/dist/core/templates/workflows/explore.js +48 -14
- package/dist/core/templates/workflows/ff-change.js +19 -3
- package/dist/core/templates/workflows/new-change.js +15 -3
- package/dist/core/templates/workflows/onboard.js +67 -37
- package/dist/core/templates/workflows/project-root.d.ts +29 -0
- package/dist/core/templates/workflows/project-root.js +38 -0
- package/dist/core/templates/workflows/propose.js +52 -15
- package/dist/core/templates/workflows/sync-specs.js +40 -1
- package/dist/core/templates/workflows/update-change.js +52 -25
- package/dist/core/templates/workflows/verify-change.js +12 -3
- package/dist/core/update.d.ts +34 -0
- package/dist/core/update.js +109 -11
- package/dist/core/validation/task-checkboxes.d.ts +34 -0
- package/dist/core/validation/task-checkboxes.js +181 -0
- package/dist/core/validation/validator.d.ts +53 -1
- package/dist/core/validation/validator.js +154 -40
- package/dist/core/version-check.d.ts +14 -7
- package/dist/core/version-check.js +63 -17
- package/dist/core/worksets.js +6 -4
- package/dist/prompts/searchable-multi-select.d.ts +7 -0
- package/dist/prompts/searchable-multi-select.js +35 -6
- package/dist/telemetry/config.js +14 -2
- package/dist/telemetry/index.d.ts +4 -3
- package/dist/telemetry/index.js +22 -10
- package/dist/telemetry/opt-out.d.ts +17 -0
- package/dist/telemetry/opt-out.js +27 -0
- package/dist/utils/nested-change.d.ts +35 -0
- package/dist/utils/nested-change.js +186 -0
- package/dist/utils/spec-discovery.d.ts +20 -0
- package/dist/utils/spec-discovery.js +59 -0
- package/dist/utils/task-progress.d.ts +1 -1
- package/dist/utils/task-progress.js +38 -6
- package/package.json +4 -4
- package/schemas/spec-driven/schema.yaml +24 -5
- package/schemas/spec-driven/templates/design.md +2 -0
- package/schemas/spec-driven/templates/proposal.md +2 -0
- package/schemas/spec-driven/templates/spec.md +2 -0
- package/schemas/spec-driven/templates/tasks.md +2 -0
|
@@ -8,13 +8,16 @@ import ora from 'ora';
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import { loadChangeContext, generateInstructions, resolveSchema, resolveArtifactOutputPath, resolveArtifactOutputs, } from '../../core/artifact-graph/index.js';
|
|
11
|
+
import { isSpecsArtifactPath } from '../../core/artifact-graph/outputs.js';
|
|
12
|
+
import { findUnreadDeltaFiles } from '../../utils/spec-discovery.js';
|
|
11
13
|
import { getChangeDir, resolveCurrentPlanningHomeSync, } from '../../core/planning-home.js';
|
|
12
14
|
import { resolveRootForCommand, withStoreFlag, toPlanningHome, toRootOutput, } from '../../core/root-selection.js';
|
|
13
|
-
import { assembleReferenceIndex, renderReferencedStoresBlock, renderReferencedStoresSection, } from '../../core/references.js';
|
|
15
|
+
import { assembleReferenceIndex, escapeEnvelopeAttribute, escapeEnvelopeTags, renderReferencedStoresBlock, renderReferencedStoresSection, sanitizeInline, } from '../../core/references.js';
|
|
14
16
|
import { readRegistrySnapshot } from '../../core/store/registry.js';
|
|
15
17
|
import { loadOperationInputs, readProjectConfig, } from '../../core/project-config.js';
|
|
16
18
|
import { validateChangeExists, validateSchemaExists, } from './shared.js';
|
|
17
19
|
import { parseTaskLines } from '../../utils/task-progress.js';
|
|
20
|
+
import { METADATA_FILENAME } from '../../utils/change-metadata.js';
|
|
18
21
|
// -----------------------------------------------------------------------------
|
|
19
22
|
// Artifact Instructions Command
|
|
20
23
|
// -----------------------------------------------------------------------------
|
|
@@ -93,8 +96,12 @@ export async function instructionsCommand(artifactId, options) {
|
|
|
93
96
|
}
|
|
94
97
|
export function printInstructionsText(instructions, isBlocked) {
|
|
95
98
|
const { artifactId, changeName, schemaName, changeDir, resolvedOutputPath, description, instruction, context, rules, template, dependencies, unlocks, } = instructions;
|
|
96
|
-
// Opening tag
|
|
97
|
-
|
|
99
|
+
// Opening tag. The change name is a directory name read from disk, and the
|
|
100
|
+
// read path rejects only separators and NUL - a quote in it would otherwise
|
|
101
|
+
// close the attribute and forge siblings on this tag.
|
|
102
|
+
console.log(`<artifact id="${escapeEnvelopeAttribute(artifactId)}"` +
|
|
103
|
+
` change="${escapeEnvelopeAttribute(changeName)}"` +
|
|
104
|
+
` schema="${escapeEnvelopeAttribute(schemaName)}">`);
|
|
98
105
|
console.log();
|
|
99
106
|
// Artifacts skipped via skip_specs get no creation directive: emitting the
|
|
100
107
|
// task/template anyway would prompt an agent to write spec files that
|
|
@@ -118,15 +125,15 @@ export function printInstructionsText(instructions, isBlocked) {
|
|
|
118
125
|
}
|
|
119
126
|
// Task directive
|
|
120
127
|
console.log('<task>');
|
|
121
|
-
console.log(`Create the ${artifactId} artifact for change "${changeName}".`);
|
|
122
|
-
console.log(description);
|
|
128
|
+
console.log(`Create the ${escapeEnvelopeTags(artifactId)} artifact for change "${escapeEnvelopeTags(changeName)}".`);
|
|
129
|
+
console.log(escapeEnvelopeTags(description));
|
|
123
130
|
console.log('</task>');
|
|
124
131
|
console.log();
|
|
125
132
|
// Project context (AI constraint - do not include in output)
|
|
126
133
|
if (context) {
|
|
127
134
|
console.log('<project_context>');
|
|
128
135
|
console.log('<!-- This is background information for you. Do NOT include this in your output. -->');
|
|
129
|
-
console.log(context);
|
|
136
|
+
console.log(escapeEnvelopeTags(context));
|
|
130
137
|
console.log('</project_context>');
|
|
131
138
|
console.log();
|
|
132
139
|
}
|
|
@@ -140,7 +147,9 @@ export function printInstructionsText(instructions, isBlocked) {
|
|
|
140
147
|
console.log('<rules>');
|
|
141
148
|
console.log('<!-- These are constraints for you to follow. Do NOT include this in your output. -->');
|
|
142
149
|
for (const rule of rules) {
|
|
143
|
-
|
|
150
|
+
// Flattened so a newline cannot forge a sibling bullet, but never
|
|
151
|
+
// truncated: these are instructions an agent has to follow in full.
|
|
152
|
+
console.log(`- ${escapeEnvelopeTags(sanitizeInline(rule, Infinity))}`);
|
|
144
153
|
}
|
|
145
154
|
console.log('</rules>');
|
|
146
155
|
console.log();
|
|
@@ -164,7 +173,7 @@ export function printInstructionsText(instructions, isBlocked) {
|
|
|
164
173
|
const fullPath = path.join(changeDir, dep.path);
|
|
165
174
|
console.log(`<dependency id="${dep.id}" status="${status}">`);
|
|
166
175
|
console.log(` <path>${fullPath}</path>`);
|
|
167
|
-
console.log(` <description>${dep.description}</description>`);
|
|
176
|
+
console.log(` <description>${escapeEnvelopeTags(dep.description)}</description>`);
|
|
168
177
|
console.log('</dependency>');
|
|
169
178
|
}
|
|
170
179
|
console.log('</dependencies>');
|
|
@@ -178,14 +187,17 @@ export function printInstructionsText(instructions, isBlocked) {
|
|
|
178
187
|
// Instruction (guidance)
|
|
179
188
|
if (instruction) {
|
|
180
189
|
console.log('<instruction>');
|
|
181
|
-
console.log(instruction.trim());
|
|
190
|
+
console.log(escapeEnvelopeTags(instruction.trim()));
|
|
182
191
|
console.log('</instruction>');
|
|
183
192
|
console.log();
|
|
184
193
|
}
|
|
185
194
|
// Template
|
|
186
195
|
console.log('<template>');
|
|
187
196
|
console.log('<!-- Use this as the structure for your output file. Fill in the sections. -->');
|
|
188
|
-
|
|
197
|
+
// Copied verbatim into the artifact file, so its `<!-- ... -->` comments and
|
|
198
|
+
// `<placeholder>` markers must survive - only the envelope's own closing
|
|
199
|
+
// tags are neutralized.
|
|
200
|
+
console.log(escapeEnvelopeTags(template.trim()));
|
|
189
201
|
console.log('</template>');
|
|
190
202
|
console.log();
|
|
191
203
|
// Success criteria placeholder
|
|
@@ -230,6 +242,106 @@ function toTaskItems(parsed) {
|
|
|
230
242
|
}
|
|
231
243
|
return tasks;
|
|
232
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* The command that builds one artifact.
|
|
247
|
+
*
|
|
248
|
+
* Every earlier remedy here named the `openspec-continue-change` skill, which
|
|
249
|
+
* the `core` profile never installs - the advice was a dead end for the default
|
|
250
|
+
* install. The CLI verb exists on every profile and is what the skill runs.
|
|
251
|
+
*/
|
|
252
|
+
function describeArtifactRemedy(changeName, artifactId, options = {}) {
|
|
253
|
+
const target = artifactId ?? '<artifact>';
|
|
254
|
+
const verb = options.many ? 'Create each with' : 'Create it with';
|
|
255
|
+
return (`${verb} \`openspec instructions ${target} --change ${changeName}\`` +
|
|
256
|
+
` (\`openspec status --change ${changeName}\` shows what is left).`);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Finds the artifact a schema path is generated by, so a remedy can name it.
|
|
260
|
+
*/
|
|
261
|
+
function findArtifactIdFor(schema, generates) {
|
|
262
|
+
return schema.artifacts.find((artifact) => artifact.generates === generates)?.id;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Everything still to build before apply can run, in build order.
|
|
266
|
+
*
|
|
267
|
+
* Apply blocks on the schema's `apply.requires` alone, so its own list stops at
|
|
268
|
+
* the first hop: a change with only a proposal is told "Missing artifacts:
|
|
269
|
+
* tasks" while the specs `tasks` depends on are missing too. An agent that
|
|
270
|
+
* takes that literally writes the tracking file straight from the proposal and
|
|
271
|
+
* skips the artifacts in between - the failure reported in #834 and #869.
|
|
272
|
+
* Walking `requires` names the whole chain, the same set and order
|
|
273
|
+
* `openspec status` already prints, without changing what apply blocks on.
|
|
274
|
+
*/
|
|
275
|
+
function collectMissingPrerequisites(input) {
|
|
276
|
+
const { requiredArtifactIds, schema, buildOrder, completed } = input;
|
|
277
|
+
const byId = new Map(schema.artifacts.map((artifact) => [artifact.id, artifact]));
|
|
278
|
+
const missing = new Set();
|
|
279
|
+
const queue = [...requiredArtifactIds];
|
|
280
|
+
const seen = new Set(queue);
|
|
281
|
+
while (queue.length > 0) {
|
|
282
|
+
const id = queue.shift();
|
|
283
|
+
const artifact = byId.get(id);
|
|
284
|
+
if (!artifact)
|
|
285
|
+
continue;
|
|
286
|
+
if (!completed.has(id))
|
|
287
|
+
missing.add(id);
|
|
288
|
+
for (const dependency of artifact.requires) {
|
|
289
|
+
if (seen.has(dependency))
|
|
290
|
+
continue;
|
|
291
|
+
seen.add(dependency);
|
|
292
|
+
queue.push(dependency);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const order = new Map(buildOrder.map((id, index) => [id, index]));
|
|
296
|
+
return [...missing].sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Warnings apply reports alongside its instruction.
|
|
300
|
+
*
|
|
301
|
+
* Apply gates on the schema's `apply.requires` only, so a change whose tasks
|
|
302
|
+
* file was written ahead of its specs reads as ready even though no delta spec
|
|
303
|
+
* exists - the state `openspec validate` rejects. Blocking here would be a
|
|
304
|
+
* policy change; naming the gap is not, and it is what keeps apply from being
|
|
305
|
+
* the one surface that green-lights a change every other surface flags.
|
|
306
|
+
*
|
|
307
|
+
* Only reported once apply is past its own gate: for a change that has not
|
|
308
|
+
* reached tasks yet, the missing specs are the next step rather than a warning.
|
|
309
|
+
* Schemas that declare no spec-producing artifact carry `skip_specs` from
|
|
310
|
+
* creation, so this never fires on them.
|
|
311
|
+
*
|
|
312
|
+
* A delta file the merge path never reads (specs/<capability>.md, a note
|
|
313
|
+
* beside spec.md) still satisfies the specs glob, so it reads as written here
|
|
314
|
+
* while validate rejects it and archive would drop it. Each one is named.
|
|
315
|
+
*/
|
|
316
|
+
async function collectApplyWarnings(input) {
|
|
317
|
+
const { state, schema, changeDir, changeName, skippedArtifacts } = input;
|
|
318
|
+
if (state === 'blocked')
|
|
319
|
+
return [];
|
|
320
|
+
const specArtifacts = schema.artifacts.filter((artifact) => isSpecsArtifactPath(artifact.generates));
|
|
321
|
+
if (specArtifacts.length === 0)
|
|
322
|
+
return [];
|
|
323
|
+
if (specArtifacts.some((artifact) => skippedArtifacts?.has(artifact.id)))
|
|
324
|
+
return [];
|
|
325
|
+
const warnings = (await findUnreadDeltaFiles(path.join(changeDir, 'specs'))).map((file) => `specs/${file.path} is not a capability's spec.md, so \`openspec validate ${changeName}\` rejects it and archive never merges it. ` +
|
|
326
|
+
`Move its requirements into specs/${file.expected}.`);
|
|
327
|
+
const hasDeltas = specArtifacts.some((artifact) => resolveArtifactOutputs(changeDir, artifact.generates).length > 0);
|
|
328
|
+
if (hasDeltas)
|
|
329
|
+
return warnings;
|
|
330
|
+
const metadataPath = path.join(changeDir, METADATA_FILENAME);
|
|
331
|
+
// The command names the artifact this schema actually declares, never the
|
|
332
|
+
// literal `specs`. A schema whose spec-producing artifact is `contracts` was
|
|
333
|
+
// told to run `openspec instructions specs`, an artifact it does not have,
|
|
334
|
+
// so the warning dead-ended at the exact step meant to resolve it. With more
|
|
335
|
+
// than one such artifact there is no single right answer, so the id becomes
|
|
336
|
+
// a placeholder rather than a guess.
|
|
337
|
+
const specTarget = specArtifacts.length === 1 ? specArtifacts[0].id : '<artifact-id>';
|
|
338
|
+
return [
|
|
339
|
+
...warnings,
|
|
340
|
+
`This change has no delta specs and does not declare \`skip_specs: true\`, so \`openspec validate ${changeName}\` fails on it. ` +
|
|
341
|
+
`Write the delta specs before implementing (\`openspec instructions ${specTarget} --change ${changeName}\`), ` +
|
|
342
|
+
`or add \`skip_specs: true\` to ${metadataPath} if this change really changes no specified behavior.`,
|
|
343
|
+
];
|
|
344
|
+
}
|
|
233
345
|
/**
|
|
234
346
|
* Generates apply instructions for implementing tasks from a change.
|
|
235
347
|
* Schema-aware: reads apply phase configuration from schema to determine
|
|
@@ -267,6 +379,13 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
267
379
|
missingArtifacts.push(artifactId);
|
|
268
380
|
}
|
|
269
381
|
}
|
|
382
|
+
// Everything still to build, not just the first hop apply blocks on.
|
|
383
|
+
const missingPrerequisites = collectMissingPrerequisites({
|
|
384
|
+
requiredArtifactIds: [...requiredArtifactIds],
|
|
385
|
+
schema,
|
|
386
|
+
buildOrder: context.graph.getBuildOrder(),
|
|
387
|
+
completed: context.completed,
|
|
388
|
+
});
|
|
270
389
|
// Build context files from all existing artifacts in schema
|
|
271
390
|
const contextFiles = {};
|
|
272
391
|
for (const artifact of schema.artifacts) {
|
|
@@ -297,20 +416,33 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
297
416
|
let instruction;
|
|
298
417
|
if (missingArtifacts.length > 0) {
|
|
299
418
|
state = 'blocked';
|
|
300
|
-
|
|
419
|
+
const chain = missingPrerequisites.length > missingArtifacts.length
|
|
420
|
+
? `\nNot created yet, in build order: ${missingPrerequisites.join(', ')}.` +
|
|
421
|
+
` Build the ones this change needs before applying - the schema says which are conditional.`
|
|
422
|
+
: '';
|
|
423
|
+
instruction =
|
|
424
|
+
`Cannot apply this change yet. Missing artifacts: ${missingArtifacts.join(', ')}.${chain}` +
|
|
425
|
+
`\n${describeArtifactRemedy(changeName,
|
|
426
|
+
// Only name one when one is left: the first of several would be the
|
|
427
|
+
// schema's conditional artifact as often as not.
|
|
428
|
+
missingPrerequisites.length === 1 ? missingPrerequisites[0] : undefined, { many: missingPrerequisites.length > 1 })}`;
|
|
301
429
|
}
|
|
302
430
|
else if (tracksFile && !tracksFileExists) {
|
|
303
431
|
// Tracking file configured but doesn't exist yet
|
|
304
432
|
const tracksFilename = path.basename(tracksFile);
|
|
305
433
|
state = 'blocked';
|
|
306
|
-
instruction =
|
|
434
|
+
instruction =
|
|
435
|
+
`The ${tracksFilename} file is missing and must be created.` +
|
|
436
|
+
`\n${describeArtifactRemedy(changeName, findArtifactIdFor(schema, tracksFile))}`;
|
|
307
437
|
}
|
|
308
438
|
else if (tracksFile && tracksFileExists && tasks.length === 0) {
|
|
309
439
|
// Tracking file exists but lists nothing an agent can work on: either no
|
|
310
440
|
// checkboxes at all, or only checkboxes with no text after them.
|
|
311
441
|
const tracksFilename = path.basename(tracksFile);
|
|
312
442
|
state = 'blocked';
|
|
313
|
-
instruction =
|
|
443
|
+
instruction =
|
|
444
|
+
`The ${tracksFilename} file exists but contains no tasks to work on.` +
|
|
445
|
+
`\nAdd tasks to ${tracksFilename}, or rebuild it: ${describeArtifactRemedy(changeName, findArtifactIdFor(schema, tracksFile))}`;
|
|
314
446
|
}
|
|
315
447
|
else if (tracksFile && remaining === 0 && total > 0) {
|
|
316
448
|
state = 'all_done';
|
|
@@ -325,6 +457,13 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
325
457
|
state = 'ready';
|
|
326
458
|
instruction = schemaInstruction?.trim() ?? 'Read context files, work through pending tasks, mark complete as you go.\nPause if you hit blockers or need clarification.';
|
|
327
459
|
}
|
|
460
|
+
const warnings = await collectApplyWarnings({
|
|
461
|
+
state,
|
|
462
|
+
schema,
|
|
463
|
+
changeDir,
|
|
464
|
+
changeName,
|
|
465
|
+
skippedArtifacts: context.skippedArtifacts,
|
|
466
|
+
});
|
|
328
467
|
return {
|
|
329
468
|
changeName,
|
|
330
469
|
changeDir,
|
|
@@ -334,6 +473,8 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
334
473
|
tasks,
|
|
335
474
|
state,
|
|
336
475
|
missingArtifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
|
|
476
|
+
...(missingPrerequisites.length > 0 ? { missingPrerequisites } : {}),
|
|
477
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
337
478
|
instruction,
|
|
338
479
|
...(references !== undefined ? { references } : {}),
|
|
339
480
|
...operationInputs,
|
|
@@ -375,7 +516,7 @@ export async function applyInstructionsCommand(options) {
|
|
|
375
516
|
}
|
|
376
517
|
}
|
|
377
518
|
export function printApplyInstructionsText(instructions) {
|
|
378
|
-
const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction } = instructions;
|
|
519
|
+
const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, warnings, instruction } = instructions;
|
|
379
520
|
console.log(`## Apply: ${changeName}`);
|
|
380
521
|
console.log(`Schema: ${schemaName}`);
|
|
381
522
|
console.log();
|
|
@@ -388,7 +529,18 @@ export function printApplyInstructionsText(instructions) {
|
|
|
388
529
|
console.log('### ⚠️ Blocked');
|
|
389
530
|
console.log();
|
|
390
531
|
console.log(`Missing artifacts: ${missingArtifacts.join(', ')}`);
|
|
391
|
-
|
|
532
|
+
if (instructions.missingPrerequisites &&
|
|
533
|
+
instructions.missingPrerequisites.length > missingArtifacts.length) {
|
|
534
|
+
console.log(`Not created yet, in build order: ${instructions.missingPrerequisites.join(', ')}`);
|
|
535
|
+
}
|
|
536
|
+
console.log();
|
|
537
|
+
}
|
|
538
|
+
if (warnings && warnings.length > 0) {
|
|
539
|
+
console.log('### ⚠️ Warnings');
|
|
540
|
+
console.log();
|
|
541
|
+
for (const warning of warnings) {
|
|
542
|
+
console.log(`- ${warning}`);
|
|
543
|
+
}
|
|
392
544
|
console.log();
|
|
393
545
|
}
|
|
394
546
|
// Context files (dynamically from schema)
|
|
@@ -464,13 +616,15 @@ export function printArchiveInstructionsText(instructions) {
|
|
|
464
616
|
function printOperationInputsText(inputs) {
|
|
465
617
|
if (inputs.context) {
|
|
466
618
|
console.log('### Project Context (required instruction input)');
|
|
619
|
+
// Printed verbatim on purpose. Escaping a leading `#` would also fire inside
|
|
620
|
+
// fenced code (`# install deps`), so heading forgery is not guarded here.
|
|
467
621
|
console.log(inputs.context);
|
|
468
622
|
console.log();
|
|
469
623
|
}
|
|
470
624
|
if (inputs.operationGuidance && inputs.operationGuidance.length > 0) {
|
|
471
625
|
console.log('### Operation Guidance (advisory)');
|
|
472
626
|
for (const guidance of inputs.operationGuidance) {
|
|
473
|
-
console.log(`- ${guidance}`);
|
|
627
|
+
console.log(`- ${sanitizeInline(guidance, Infinity)}`);
|
|
474
628
|
}
|
|
475
629
|
console.log();
|
|
476
630
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* root; initiative linking and workspace affected areas are no longer part of
|
|
7
7
|
* this command.
|
|
8
8
|
*/
|
|
9
|
+
import chalk from 'chalk';
|
|
9
10
|
import ora from 'ora';
|
|
10
11
|
import path from 'path';
|
|
11
12
|
import { createChange, validateChangeName } from '../../utils/change-utils.js';
|
|
@@ -33,6 +34,24 @@ function printCreatedChangeHuman(payload, root) {
|
|
|
33
34
|
console.log(`Schema: ${payload.change.schema}`);
|
|
34
35
|
console.log(`Next: ${withStoreFlag(root, `openspec status --change ${payload.change.id}`)}`);
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* An implicit root is the fallback taken when no `openspec/` directory was
|
|
39
|
+
* found: creating a change there materializes OpenSpec in whatever directory
|
|
40
|
+
* the caller happened to be in, which is how an agent ends up adopting a
|
|
41
|
+
* project that never ran `openspec init` (#1645). The creation itself stays
|
|
42
|
+
* zero-config; this only makes it visible.
|
|
43
|
+
*/
|
|
44
|
+
function printImplicitRootNotice(root) {
|
|
45
|
+
if (root.source !== 'implicit') {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const openspecDir = path.dirname(root.changesDir);
|
|
49
|
+
const relative = path.relative(process.cwd(), openspecDir);
|
|
50
|
+
const location = relative && !relative.startsWith('..') ? relative : openspecDir;
|
|
51
|
+
console.log();
|
|
52
|
+
console.log(chalk.dim(`Note: no OpenSpec root was found here, so one was created at ${location}/.`));
|
|
53
|
+
console.log(chalk.dim('Run `openspec init` to finish setting this project up, or delete that directory if you meant a different project.'));
|
|
54
|
+
}
|
|
36
55
|
export async function newChangeCommand(name, options) {
|
|
37
56
|
const spinner = options.json ? undefined : ora();
|
|
38
57
|
try {
|
|
@@ -89,6 +108,7 @@ export async function newChangeCommand(name, options) {
|
|
|
89
108
|
}
|
|
90
109
|
spinner?.stop();
|
|
91
110
|
printCreatedChangeHuman(payload, root);
|
|
111
|
+
printImplicitRootNotice(root);
|
|
92
112
|
}
|
|
93
113
|
catch (error) {
|
|
94
114
|
spinner?.stop();
|
|
@@ -30,6 +30,14 @@ export interface ApplyInstructions {
|
|
|
30
30
|
tasks: TaskItem[];
|
|
31
31
|
state: 'blocked' | 'all_done' | 'ready';
|
|
32
32
|
missingArtifacts?: string[];
|
|
33
|
+
/**
|
|
34
|
+
* Everything still to build before apply can run, in build order - the
|
|
35
|
+
* transitive closure of the schema's `apply.requires`, so it can be longer
|
|
36
|
+
* than `missingArtifacts`, which stops at the first hop apply blocks on.
|
|
37
|
+
*/
|
|
38
|
+
missingPrerequisites?: string[];
|
|
39
|
+
/** Non-blocking problems with the change, reported alongside the instruction. */
|
|
40
|
+
warnings?: string[];
|
|
33
41
|
instruction: string;
|
|
34
42
|
/** Referenced-store index (read-only upstream context; omitted when none declared) */
|
|
35
43
|
references?: ReferenceIndexEntry[];
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import path from 'path';
|
|
9
|
+
import { describeNestedChange, findNestedChangesIn, } from '../../utils/nested-change.js';
|
|
9
10
|
import * as fs from 'fs';
|
|
10
11
|
import { getSchemaDir, listSchemas } from '../../core/artifact-graph/index.js';
|
|
11
12
|
import { isRootSelectionError } from '../../core/root-selection.js';
|
|
@@ -144,6 +145,13 @@ export async function validateChangeExists(changeName, projectRoot, changesDir =
|
|
|
144
145
|
}
|
|
145
146
|
throw new Error(`Change '${changeName}' not found. Available changes:\n ${available.join('\n ')}`);
|
|
146
147
|
}
|
|
148
|
+
// The directory exists but is a namespace folder wrapping nested change
|
|
149
|
+
// directories. Every artifact lookup below it would report "not started" for
|
|
150
|
+
// work that is in fact there, so say what is actually wrong instead (#1846).
|
|
151
|
+
const nested = await findNestedChangesIn(changesDir, changeName);
|
|
152
|
+
if (nested) {
|
|
153
|
+
throw new Error(describeNestedChange(nested));
|
|
154
|
+
}
|
|
147
155
|
return changeName;
|
|
148
156
|
}
|
|
149
157
|
/**
|
|
@@ -14,5 +14,9 @@ export interface StatusOptions {
|
|
|
14
14
|
}
|
|
15
15
|
export declare const BATCH_STATUS_FAILURE_PAYLOAD: Record<string, unknown>;
|
|
16
16
|
export declare function statusCommand(options: StatusOptions): Promise<void>;
|
|
17
|
-
export
|
|
17
|
+
export interface PrintStatusTextOptions {
|
|
18
|
+
/** Selected store id, so the printed command carries `--store`. */
|
|
19
|
+
storeId?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function printStatusText(status: ChangeStatus, options?: PrintStatusTextOptions): void;
|
|
18
22
|
//# sourceMappingURL=status.d.ts.map
|
|
@@ -8,7 +8,9 @@ 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 { resolveNextStep } from '../../core/change-status-policy.js';
|
|
11
12
|
import { asStatus } from '../shared-output.js';
|
|
13
|
+
import { describeNestedChange, findNestedChanges, } from '../../utils/nested-change.js';
|
|
12
14
|
import { validateChangeExists, validateSchemaExists, getAvailableChanges, getStatusIndicator, getStatusColor, } from './shared.js';
|
|
13
15
|
// The --all --json failure null-shape. Root-selection failures (handled in
|
|
14
16
|
// resolveRootForCommand) and thrown errors (caught by the CLI wrapper) must
|
|
@@ -37,12 +39,16 @@ export async function statusCommand(options) {
|
|
|
37
39
|
const projectRoot = root.path;
|
|
38
40
|
const rootOutput = toRootOutput(root);
|
|
39
41
|
const newChangeHint = withStoreFlag(root, 'openspec new change <name>');
|
|
42
|
+
// One store-flag decision serves the JSON `nextSteps` sentence and the text
|
|
43
|
+
// `Next:` line, so a store-selected root can never carry `--store` in one
|
|
44
|
+
// and drop it from the other.
|
|
45
|
+
const storeOptions = isStoreSelectedRoot(root) ? { storeId: root.storeId } : {};
|
|
40
46
|
// Single definition of "load one change's status" so the batch and
|
|
41
47
|
// single-change payloads can never drift apart.
|
|
42
48
|
const loadStatus = (changeName) => formatChangeStatus(loadChangeContext(projectRoot, changeName, options.schema, {
|
|
43
49
|
changeDir: getChangeDir(planningHome, changeName),
|
|
44
50
|
planningHome,
|
|
45
|
-
}),
|
|
51
|
+
}), storeOptions);
|
|
46
52
|
// Handle no-changes case gracefully — status is informational,
|
|
47
53
|
// so "no changes" is a valid state, not an error.
|
|
48
54
|
if (!options.change) {
|
|
@@ -66,7 +72,23 @@ export async function statusCommand(options) {
|
|
|
66
72
|
// with the same comparator validate --all uses so the two batch
|
|
67
73
|
// commands order a given change set identically.
|
|
68
74
|
const entries = [];
|
|
75
|
+
// The sweep reads each directory straight through `loadStatus`, so a
|
|
76
|
+
// namespace folder wrapping nested changes would report a whole
|
|
77
|
+
// artifact plan for work that is not there (#1846). It carries the same
|
|
78
|
+
// per-change diagnostic a malformed change does.
|
|
79
|
+
const nestedByName = new Map((await findNestedChanges(root.changesDir, available)).map((finding) => [
|
|
80
|
+
finding.name,
|
|
81
|
+
finding,
|
|
82
|
+
]));
|
|
69
83
|
for (const changeName of available.sort((a, b) => a.localeCompare(b))) {
|
|
84
|
+
const nested = nestedByName.get(changeName);
|
|
85
|
+
if (nested) {
|
|
86
|
+
entries.push({
|
|
87
|
+
changeName,
|
|
88
|
+
status: [asStatus(new Error(describeNestedChange(nested)), 'change_error')],
|
|
89
|
+
});
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
70
92
|
try {
|
|
71
93
|
entries.push(loadStatus(changeName));
|
|
72
94
|
}
|
|
@@ -90,7 +112,7 @@ export async function statusCommand(options) {
|
|
|
90
112
|
console.log();
|
|
91
113
|
}
|
|
92
114
|
if ('artifacts' in entry) {
|
|
93
|
-
printStatusText(entry);
|
|
115
|
+
printStatusText(entry, storeOptions);
|
|
94
116
|
}
|
|
95
117
|
else {
|
|
96
118
|
console.log(chalk.red(`✗ ${entry.changeName}: ${entry.status[0]?.message}`));
|
|
@@ -122,14 +144,14 @@ export async function statusCommand(options) {
|
|
|
122
144
|
console.log(JSON.stringify({ ...status, root: rootOutput }, null, 2));
|
|
123
145
|
return;
|
|
124
146
|
}
|
|
125
|
-
printStatusText(status);
|
|
147
|
+
printStatusText(status, storeOptions);
|
|
126
148
|
}
|
|
127
149
|
catch (error) {
|
|
128
150
|
spinner?.stop();
|
|
129
151
|
throw error;
|
|
130
152
|
}
|
|
131
153
|
}
|
|
132
|
-
export function printStatusText(status) {
|
|
154
|
+
export function printStatusText(status, options = {}) {
|
|
133
155
|
const doneCount = status.artifacts.filter((a) => a.status === 'done').length;
|
|
134
156
|
const skippedCount = status.artifacts.filter((a) => a.status === 'skipped').length;
|
|
135
157
|
const total = status.artifacts.length - skippedCount;
|
|
@@ -153,9 +175,24 @@ export function printStatusText(status) {
|
|
|
153
175
|
}
|
|
154
176
|
console.log(line);
|
|
155
177
|
}
|
|
156
|
-
|
|
178
|
+
// Derived from the same inputs as the JSON `nextSteps` sentence, so the two
|
|
179
|
+
// surfaces always name the same command. Without this line the text surface
|
|
180
|
+
// reports state and no verb, which leaves someone resuming a change - after a
|
|
181
|
+
// lost session, or on a change they did not start - with nowhere to go.
|
|
182
|
+
const nextStep = resolveNextStep({
|
|
183
|
+
changeName: status.changeName,
|
|
184
|
+
artifactStatuses: status.artifacts,
|
|
185
|
+
allArtifactsComplete: status.isPlanningComplete,
|
|
186
|
+
...(options.storeId ? { storeId: options.storeId } : {}),
|
|
187
|
+
});
|
|
188
|
+
if (status.isPlanningComplete || nextStep) {
|
|
157
189
|
console.log();
|
|
190
|
+
}
|
|
191
|
+
if (status.isPlanningComplete) {
|
|
158
192
|
console.log(chalk.green('All planning artifacts complete!'));
|
|
159
193
|
}
|
|
194
|
+
if (nextStep) {
|
|
195
|
+
console.log(`Next: ${nextStep.command}`);
|
|
196
|
+
}
|
|
160
197
|
}
|
|
161
198
|
//# sourceMappingURL=status.js.map
|
package/dist/core/archive.js
CHANGED
|
@@ -8,11 +8,12 @@ import { VALIDATION_MESSAGES } from './validation/constants.js';
|
|
|
8
8
|
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, } from './specs-apply.js';
|
|
11
|
-
import { discoverSpecFiles, hasAnyFileUnder } from '../utils/spec-discovery.js';
|
|
11
|
+
import { discoverSpecFiles, findUnreadDeltaFiles, hasAnyFileUnder } from '../utils/spec-discovery.js';
|
|
12
12
|
import { METADATA_FILENAME, readRetireCapabilitiesMarker, readSkipSpecsMarker } 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';
|
|
16
|
+
import { describeNestedChange, findNestedChangesIn, } from '../utils/nested-change.js';
|
|
16
17
|
function isMissingPathError(error) {
|
|
17
18
|
return (typeof error === 'object' &&
|
|
18
19
|
error !== null &&
|
|
@@ -949,6 +950,14 @@ export class ArchiveCommand {
|
|
|
949
950
|
? `Change '${changeName}' not found. Available changes: ${available.join(', ')}`
|
|
950
951
|
: `Change '${changeName}' not found. No active changes exist in this root.`);
|
|
951
952
|
}
|
|
953
|
+
// Archiving a namespace folder moves an active, unfinished change into the
|
|
954
|
+
// archive under a name nobody will look for, and never applies its deltas.
|
|
955
|
+
// That is silent data loss, so it is refused outright rather than warned
|
|
956
|
+
// about (#1846).
|
|
957
|
+
const nested = await findNestedChangesIn(changesDir, changeName);
|
|
958
|
+
if (nested) {
|
|
959
|
+
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.`);
|
|
960
|
+
}
|
|
952
961
|
const skipValidation = options.validate === false || options.noValidate === true;
|
|
953
962
|
// Validate specs and change before archiving
|
|
954
963
|
if (!skipValidation) {
|
|
@@ -993,6 +1002,13 @@ export class ArchiveCommand {
|
|
|
993
1002
|
// folder, so only a regular file counts.
|
|
994
1003
|
const rootSpecStat = await fs.stat(path.join(changeSpecsDir, 'spec.md')).catch(() => null);
|
|
995
1004
|
let hasDeltaSpecs = rootSpecStat?.isFile() === true;
|
|
1005
|
+
// Likewise for delta sections in any other file the merge path does not
|
|
1006
|
+
// read (specs/<capability>.md, a note beside spec.md): without this the
|
|
1007
|
+
// zero-delta leniency below archives the change as done with nothing
|
|
1008
|
+
// merged, although validate rejects it.
|
|
1009
|
+
if (!hasDeltaSpecs) {
|
|
1010
|
+
hasDeltaSpecs = (await findUnreadDeltaFiles(changeSpecsDir)).length > 0;
|
|
1011
|
+
}
|
|
996
1012
|
// A change that declares skip_specs must not carry any file under
|
|
997
1013
|
// specs/ — validate reports that as a conflict, so archive has to run
|
|
998
1014
|
// the same check instead of skipping validation because the files
|
|
@@ -10,4 +10,18 @@ export declare function loadSchema(filePath: string): SchemaYaml;
|
|
|
10
10
|
* Parses and validates an artifact schema from YAML content.
|
|
11
11
|
*/
|
|
12
12
|
export declare function parseSchema(yamlContent: string): SchemaYaml;
|
|
13
|
+
/**
|
|
14
|
+
* Describes an `apply.tracks` value that is not exactly equal to any artifact's
|
|
15
|
+
* `generates` value, or returns undefined when there is nothing to report.
|
|
16
|
+
*
|
|
17
|
+
* The tracked-tasks lookups select the artifact whose `generates` string equals
|
|
18
|
+
* `tracks`, so this is a progress-discovery problem, not a claim that nothing
|
|
19
|
+
* produces the file: a glob `generates: tasks/*.md` really does generate
|
|
20
|
+
* `tracks: tasks/main.md`, yet the strings differ, so the lookup still misses.
|
|
21
|
+
* Either way apply keeps working (it reads the path directly), but `openspec
|
|
22
|
+
* list` and `openspec status` fall back to counting the top-level `tasks.md`,
|
|
23
|
+
* and apply's remedy cannot name an artifact to build. A typo such as
|
|
24
|
+
* `task.md` is the other usual cause.
|
|
25
|
+
*/
|
|
26
|
+
export declare function findApplyTracksWarning(schema: SchemaYaml): string | undefined;
|
|
13
27
|
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -30,6 +30,8 @@ export function parseSchema(yamlContent) {
|
|
|
30
30
|
validateNoDuplicateIds(schema.artifacts);
|
|
31
31
|
// Check that all requires references are valid
|
|
32
32
|
validateRequiresReferences(schema.artifacts);
|
|
33
|
+
// Check that the apply phase names artifacts this schema declares
|
|
34
|
+
validateApplyReferences(schema);
|
|
33
35
|
// Check for cycles
|
|
34
36
|
validateNoCycles(schema.artifacts);
|
|
35
37
|
return schema;
|
|
@@ -59,6 +61,56 @@ function validateRequiresReferences(artifacts) {
|
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Validates that every `apply.requires` id is an artifact the schema declares.
|
|
66
|
+
*
|
|
67
|
+
* Apply skips an id that no artifact declares, so a typo silently dropped that
|
|
68
|
+
* artifact from the apply gate. An unknown artifact `requires` is already a
|
|
69
|
+
* load error, and this is the same kind of reference.
|
|
70
|
+
*
|
|
71
|
+
* `apply.tracks` is deliberately not checked here. It is a path, not an id:
|
|
72
|
+
* apply reads it as written, so a schema whose `tracks` value does not exactly
|
|
73
|
+
* match any `generates` value (a hand-written `TODO.md`, or `tasks/main.md`
|
|
74
|
+
* under a glob `generates: tasks/*.md` that really does produce it) works
|
|
75
|
+
* today, and failing the load would break every command on it.
|
|
76
|
+
* `openspec schema validate` reports that case as a warning instead
|
|
77
|
+
* (see `findApplyTracksWarning`).
|
|
78
|
+
*/
|
|
79
|
+
function validateApplyReferences(schema) {
|
|
80
|
+
const apply = schema.apply;
|
|
81
|
+
if (!apply)
|
|
82
|
+
return;
|
|
83
|
+
const validIds = schema.artifacts.map(a => a.id);
|
|
84
|
+
for (const req of apply.requires) {
|
|
85
|
+
if (!validIds.includes(req)) {
|
|
86
|
+
throw new SchemaValidationError(`Invalid apply.requires reference: '${req}' does not exist (artifacts: ${validIds.join(', ')})`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Describes an `apply.tracks` value that is not exactly equal to any artifact's
|
|
92
|
+
* `generates` value, or returns undefined when there is nothing to report.
|
|
93
|
+
*
|
|
94
|
+
* The tracked-tasks lookups select the artifact whose `generates` string equals
|
|
95
|
+
* `tracks`, so this is a progress-discovery problem, not a claim that nothing
|
|
96
|
+
* produces the file: a glob `generates: tasks/*.md` really does generate
|
|
97
|
+
* `tracks: tasks/main.md`, yet the strings differ, so the lookup still misses.
|
|
98
|
+
* Either way apply keeps working (it reads the path directly), but `openspec
|
|
99
|
+
* list` and `openspec status` fall back to counting the top-level `tasks.md`,
|
|
100
|
+
* and apply's remedy cannot name an artifact to build. A typo such as
|
|
101
|
+
* `task.md` is the other usual cause.
|
|
102
|
+
*/
|
|
103
|
+
export function findApplyTracksWarning(schema) {
|
|
104
|
+
const tracks = schema.apply?.tracks;
|
|
105
|
+
if (tracks == null || schema.artifacts.some(a => a.generates === tracks))
|
|
106
|
+
return undefined;
|
|
107
|
+
return (`apply.tracks '${tracks}' does not exactly match any artifact's generates value ` +
|
|
108
|
+
`(generates: ${schema.artifacts.map(a => a.generates).join(', ')}), ` +
|
|
109
|
+
`so OpenSpec cannot tell which artifact's progress it tracks. ` +
|
|
110
|
+
`Apply still reads that file as written, but list and status count tasks.md instead. ` +
|
|
111
|
+
`Make apply.tracks exactly equal one of those generates values, ` +
|
|
112
|
+
`or confirm that file is maintained outside the artifact graph.`);
|
|
113
|
+
}
|
|
62
114
|
/**
|
|
63
115
|
* Validates that there are no cyclic dependencies.
|
|
64
116
|
* Uses DFS to detect cycles and reports the full cycle path.
|