@fission-ai/openspec 1.13.0 → 1.13.2
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 +16 -4
- 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 +69 -22
- package/dist/commands/workflow/new-change.js +20 -0
- package/dist/commands/workflow/shared.d.ts +5 -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 +260 -77
- package/dist/core/artifact-graph/outputs.d.ts +1 -1
- package/dist/core/artifact-graph/outputs.js +38 -8
- 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/adapters/continue.js +4 -0
- package/dist/core/command-generation/adapters/kilocode.d.ts +1 -1
- package/dist/core/command-generation/adapters/kilocode.js +2 -2
- 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 +22 -6
- package/dist/core/legacy-cleanup.d.ts +5 -1
- package/dist/core/legacy-cleanup.js +207 -14
- package/dist/core/list.js +55 -11
- package/dist/core/migration.js +6 -1
- 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 +78 -6
- package/dist/core/parsers/requirement-blocks.js +155 -38
- 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 +9 -3
- package/dist/core/specs-apply.d.ts +20 -0
- package/dist/core/specs-apply.js +101 -17
- 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 +70 -21
- package/dist/core/templates/workflows/bulk-archive-change.js +85 -21
- package/dist/core/templates/workflows/continue-change.js +14 -4
- package/dist/core/templates/workflows/explore.js +34 -16
- package/dist/core/templates/workflows/ff-change.js +21 -5
- package/dist/core/templates/workflows/new-change.js +15 -3
- package/dist/core/templates/workflows/onboard.js +76 -41
- 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 +22 -5
- package/dist/core/templates/workflows/sync-specs.js +40 -1
- package/dist/core/templates/workflows/update-change.js +66 -27
- package/dist/core/templates/workflows/verify-change.js +121 -49
- package/dist/core/update.d.ts +12 -0
- package/dist/core/update.js +78 -10
- package/dist/core/validation/purpose-placeholder.js +24 -9
- 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 +160 -45
- 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/file-system.js +20 -3
- package/dist/utils/line-endings.d.ts +34 -0
- package/dist/utils/line-endings.js +46 -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 +5 -5
- package/schemas/spec-driven/schema.yaml +18 -3
- 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
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
import ora from 'ora';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import * as fs from 'fs';
|
|
10
|
-
import { loadChangeContext, generateInstructions, resolveSchema,
|
|
10
|
+
import { loadChangeContext, generateInstructions, resolveSchema, resolveArtifactOutputs, } from '../../core/artifact-graph/index.js';
|
|
11
11
|
import { isSpecsArtifactPath } from '../../core/artifact-graph/outputs.js';
|
|
12
|
+
import { findUnreadDeltaFiles } from '../../utils/spec-discovery.js';
|
|
12
13
|
import { getChangeDir, resolveCurrentPlanningHomeSync, } from '../../core/planning-home.js';
|
|
13
14
|
import { resolveRootForCommand, withStoreFlag, toPlanningHome, toRootOutput, } from '../../core/root-selection.js';
|
|
14
|
-
import { assembleReferenceIndex, renderReferencedStoresBlock, renderReferencedStoresSection, } from '../../core/references.js';
|
|
15
|
+
import { assembleReferenceIndex, escapeEnvelopeAttribute, escapeEnvelopeTags, renderReferencedStoresBlock, renderReferencedStoresSection, sanitizeInline, } from '../../core/references.js';
|
|
15
16
|
import { readRegistrySnapshot } from '../../core/store/registry.js';
|
|
16
17
|
import { loadOperationInputs, readProjectConfig, } from '../../core/project-config.js';
|
|
17
18
|
import { validateChangeExists, validateSchemaExists, } from './shared.js';
|
|
@@ -95,8 +96,12 @@ export async function instructionsCommand(artifactId, options) {
|
|
|
95
96
|
}
|
|
96
97
|
export function printInstructionsText(instructions, isBlocked) {
|
|
97
98
|
const { artifactId, changeName, schemaName, changeDir, resolvedOutputPath, description, instruction, context, rules, template, dependencies, unlocks, } = instructions;
|
|
98
|
-
// Opening tag
|
|
99
|
-
|
|
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)}">`);
|
|
100
105
|
console.log();
|
|
101
106
|
// Artifacts skipped via skip_specs get no creation directive: emitting the
|
|
102
107
|
// task/template anyway would prompt an agent to write spec files that
|
|
@@ -120,15 +125,15 @@ export function printInstructionsText(instructions, isBlocked) {
|
|
|
120
125
|
}
|
|
121
126
|
// Task directive
|
|
122
127
|
console.log('<task>');
|
|
123
|
-
console.log(`Create the ${artifactId} artifact for change "${changeName}".`);
|
|
124
|
-
console.log(description);
|
|
128
|
+
console.log(`Create the ${escapeEnvelopeTags(artifactId)} artifact for change "${escapeEnvelopeTags(changeName)}".`);
|
|
129
|
+
console.log(escapeEnvelopeTags(description));
|
|
125
130
|
console.log('</task>');
|
|
126
131
|
console.log();
|
|
127
132
|
// Project context (AI constraint - do not include in output)
|
|
128
133
|
if (context) {
|
|
129
134
|
console.log('<project_context>');
|
|
130
135
|
console.log('<!-- This is background information for you. Do NOT include this in your output. -->');
|
|
131
|
-
console.log(context);
|
|
136
|
+
console.log(escapeEnvelopeTags(context));
|
|
132
137
|
console.log('</project_context>');
|
|
133
138
|
console.log();
|
|
134
139
|
}
|
|
@@ -142,7 +147,9 @@ export function printInstructionsText(instructions, isBlocked) {
|
|
|
142
147
|
console.log('<rules>');
|
|
143
148
|
console.log('<!-- These are constraints for you to follow. Do NOT include this in your output. -->');
|
|
144
149
|
for (const rule of rules) {
|
|
145
|
-
|
|
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))}`);
|
|
146
153
|
}
|
|
147
154
|
console.log('</rules>');
|
|
148
155
|
console.log();
|
|
@@ -166,7 +173,7 @@ export function printInstructionsText(instructions, isBlocked) {
|
|
|
166
173
|
const fullPath = path.join(changeDir, dep.path);
|
|
167
174
|
console.log(`<dependency id="${dep.id}" status="${status}">`);
|
|
168
175
|
console.log(` <path>${fullPath}</path>`);
|
|
169
|
-
console.log(` <description>${dep.description}</description>`);
|
|
176
|
+
console.log(` <description>${escapeEnvelopeTags(dep.description)}</description>`);
|
|
170
177
|
console.log('</dependency>');
|
|
171
178
|
}
|
|
172
179
|
console.log('</dependencies>');
|
|
@@ -180,14 +187,17 @@ export function printInstructionsText(instructions, isBlocked) {
|
|
|
180
187
|
// Instruction (guidance)
|
|
181
188
|
if (instruction) {
|
|
182
189
|
console.log('<instruction>');
|
|
183
|
-
console.log(instruction.trim());
|
|
190
|
+
console.log(escapeEnvelopeTags(instruction.trim()));
|
|
184
191
|
console.log('</instruction>');
|
|
185
192
|
console.log();
|
|
186
193
|
}
|
|
187
194
|
// Template
|
|
188
195
|
console.log('<template>');
|
|
189
196
|
console.log('<!-- Use this as the structure for your output file. Fill in the sections. -->');
|
|
190
|
-
|
|
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()));
|
|
191
201
|
console.log('</template>');
|
|
192
202
|
console.log();
|
|
193
203
|
// Success criteria placeholder
|
|
@@ -298,8 +308,12 @@ function collectMissingPrerequisites(input) {
|
|
|
298
308
|
* reached tasks yet, the missing specs are the next step rather than a warning.
|
|
299
309
|
* Schemas that declare no spec-producing artifact carry `skip_specs` from
|
|
300
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.
|
|
301
315
|
*/
|
|
302
|
-
function collectApplyWarnings(input) {
|
|
316
|
+
async function collectApplyWarnings(input) {
|
|
303
317
|
const { state, schema, changeDir, changeName, skippedArtifacts } = input;
|
|
304
318
|
if (state === 'blocked')
|
|
305
319
|
return [];
|
|
@@ -308,9 +322,11 @@ function collectApplyWarnings(input) {
|
|
|
308
322
|
return [];
|
|
309
323
|
if (specArtifacts.some((artifact) => skippedArtifacts?.has(artifact.id)))
|
|
310
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}.`);
|
|
311
327
|
const hasDeltas = specArtifacts.some((artifact) => resolveArtifactOutputs(changeDir, artifact.generates).length > 0);
|
|
312
328
|
if (hasDeltas)
|
|
313
|
-
return
|
|
329
|
+
return warnings;
|
|
314
330
|
const metadataPath = path.join(changeDir, METADATA_FILENAME);
|
|
315
331
|
// The command names the artifact this schema actually declares, never the
|
|
316
332
|
// literal `specs`. A schema whose spec-producing artifact is `contracts` was
|
|
@@ -320,6 +336,7 @@ function collectApplyWarnings(input) {
|
|
|
320
336
|
// a placeholder rather than a guess.
|
|
321
337
|
const specTarget = specArtifacts.length === 1 ? specArtifacts[0].id : '<artifact-id>';
|
|
322
338
|
return [
|
|
339
|
+
...warnings,
|
|
323
340
|
`This change has no delta specs and does not declare \`skip_specs: true\`, so \`openspec validate ${changeName}\` fails on it. ` +
|
|
324
341
|
`Write the delta specs before implementing (\`openspec instructions ${specTarget} --change ${changeName}\`), ` +
|
|
325
342
|
`or add \`skip_specs: true\` to ${metadataPath} if this change really changes no specified behavior.`,
|
|
@@ -377,15 +394,28 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
377
394
|
contextFiles[artifact.id] = outputs;
|
|
378
395
|
}
|
|
379
396
|
}
|
|
380
|
-
// Parse
|
|
397
|
+
// Parse every concrete file matched by apply.tracks. A tracking path may be
|
|
398
|
+
// a glob owned by an artifact with any ID, so treating it as one literal
|
|
399
|
+
// path loses task evidence for valid custom schemas.
|
|
381
400
|
let parsedTasks = [];
|
|
401
|
+
const unavailableTrackingFiles = [];
|
|
382
402
|
let tracksFileExists = false;
|
|
383
403
|
if (tracksFile) {
|
|
384
|
-
const
|
|
385
|
-
tracksFileExists =
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
404
|
+
const tracksPaths = resolveArtifactOutputs(changeDir, tracksFile);
|
|
405
|
+
tracksFileExists = tracksPaths.length > 0;
|
|
406
|
+
for (const tracksPath of tracksPaths) {
|
|
407
|
+
try {
|
|
408
|
+
const tasksContent = await fs.promises.readFile(tracksPath, 'utf-8');
|
|
409
|
+
parsedTasks.push(...parseTaskLines(tasksContent));
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
const code = error?.code;
|
|
413
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
414
|
+
unavailableTrackingFiles.push({
|
|
415
|
+
path: tracksPath,
|
|
416
|
+
reason: code && !message.includes(code) ? `${code}: ${message}` : message,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
389
419
|
}
|
|
390
420
|
}
|
|
391
421
|
const tasks = toTaskItems(parsedTasks);
|
|
@@ -418,6 +448,10 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
418
448
|
`The ${tracksFilename} file is missing and must be created.` +
|
|
419
449
|
`\n${describeArtifactRemedy(changeName, findArtifactIdFor(schema, tracksFile))}`;
|
|
420
450
|
}
|
|
451
|
+
else if (tracksFile && unavailableTrackingFiles.length > 0 && tasks.length === 0) {
|
|
452
|
+
state = 'blocked';
|
|
453
|
+
instruction = 'No readable task descriptions are available.';
|
|
454
|
+
}
|
|
421
455
|
else if (tracksFile && tracksFileExists && tasks.length === 0) {
|
|
422
456
|
// Tracking file exists but lists nothing an agent can work on: either no
|
|
423
457
|
// checkboxes at all, or only checkboxes with no text after them.
|
|
@@ -427,7 +461,10 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
427
461
|
`The ${tracksFilename} file exists but contains no tasks to work on.` +
|
|
428
462
|
`\nAdd tasks to ${tracksFilename}, or rebuild it: ${describeArtifactRemedy(changeName, findArtifactIdFor(schema, tracksFile))}`;
|
|
429
463
|
}
|
|
430
|
-
else if (tracksFile &&
|
|
464
|
+
else if (tracksFile &&
|
|
465
|
+
unavailableTrackingFiles.length === 0 &&
|
|
466
|
+
remaining === 0 &&
|
|
467
|
+
total > 0) {
|
|
431
468
|
state = 'all_done';
|
|
432
469
|
instruction = 'All tasks are complete! This change is ready to be archived.\nConsider running tests and reviewing the changes before archiving.';
|
|
433
470
|
}
|
|
@@ -440,7 +477,13 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
440
477
|
state = 'ready';
|
|
441
478
|
instruction = schemaInstruction?.trim() ?? 'Read context files, work through pending tasks, mark complete as you go.\nPause if you hit blockers or need clarification.';
|
|
442
479
|
}
|
|
443
|
-
|
|
480
|
+
if (unavailableTrackingFiles.length > 0) {
|
|
481
|
+
const unavailableDetails = unavailableTrackingFiles
|
|
482
|
+
.map((file) => `- ${file.path}: ${file.reason}`)
|
|
483
|
+
.join('\n');
|
|
484
|
+
instruction += `\nTask completion is not verified because tracking evidence was unavailable:\n${unavailableDetails}`;
|
|
485
|
+
}
|
|
486
|
+
const warnings = await collectApplyWarnings({
|
|
444
487
|
state,
|
|
445
488
|
schema,
|
|
446
489
|
changeDir,
|
|
@@ -454,6 +497,8 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
454
497
|
contextFiles,
|
|
455
498
|
progress: { total, complete, remaining },
|
|
456
499
|
tasks,
|
|
500
|
+
taskTrackingConfigured: tracksFile !== null,
|
|
501
|
+
...(unavailableTrackingFiles.length > 0 ? { unavailableTrackingFiles } : {}),
|
|
457
502
|
state,
|
|
458
503
|
missingArtifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
|
|
459
504
|
...(missingPrerequisites.length > 0 ? { missingPrerequisites } : {}),
|
|
@@ -599,13 +644,15 @@ export function printArchiveInstructionsText(instructions) {
|
|
|
599
644
|
function printOperationInputsText(inputs) {
|
|
600
645
|
if (inputs.context) {
|
|
601
646
|
console.log('### Project Context (required instruction input)');
|
|
647
|
+
// Printed verbatim on purpose. Escaping a leading `#` would also fire inside
|
|
648
|
+
// fenced code (`# install deps`), so heading forgery is not guarded here.
|
|
602
649
|
console.log(inputs.context);
|
|
603
650
|
console.log();
|
|
604
651
|
}
|
|
605
652
|
if (inputs.operationGuidance && inputs.operationGuidance.length > 0) {
|
|
606
653
|
console.log('### Operation Guidance (advisory)');
|
|
607
654
|
for (const guidance of inputs.operationGuidance) {
|
|
608
|
-
console.log(`- ${guidance}`);
|
|
655
|
+
console.log(`- ${sanitizeInline(guidance, Infinity)}`);
|
|
609
656
|
}
|
|
610
657
|
console.log();
|
|
611
658
|
}
|
|
@@ -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();
|
|
@@ -28,6 +28,11 @@ export interface ApplyInstructions {
|
|
|
28
28
|
remaining: number;
|
|
29
29
|
};
|
|
30
30
|
tasks: TaskItem[];
|
|
31
|
+
taskTrackingConfigured: boolean;
|
|
32
|
+
unavailableTrackingFiles?: Array<{
|
|
33
|
+
path: string;
|
|
34
|
+
reason: string;
|
|
35
|
+
}>;
|
|
31
36
|
state: 'blocked' | 'all_done' | 'ready';
|
|
32
37
|
missingArtifacts?: string[];
|
|
33
38
|
/**
|
|
@@ -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
|