@fission-ai/openspec 1.14.0 → 1.14.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 +15 -31
- package/dist/cli/commands/config.d.ts +8 -0
- package/dist/cli/commands/config.js +88 -0
- package/dist/cli/commands/context.d.ts +3 -0
- package/dist/cli/commands/context.js +20 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +18 -0
- package/dist/cli/commands/schema.d.ts +6 -0
- package/dist/cli/commands/schema.js +57 -0
- package/dist/cli/commands/spec.d.ts +3 -0
- package/dist/cli/commands/spec.js +42 -0
- package/dist/cli/commands/store.d.ts +3 -0
- package/dist/cli/commands/store.js +130 -0
- package/dist/cli/commands/workset.d.ts +3 -0
- package/dist/cli/commands/workset.js +96 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +86 -43
- package/dist/commands/completion.js +4 -1
- package/dist/commands/config.d.ts +16 -7
- package/dist/commands/config.js +342 -395
- package/dist/commands/context.d.ts +8 -2
- package/dist/commands/context.js +22 -37
- package/dist/commands/doctor.d.ts +6 -7
- package/dist/commands/doctor.js +14 -27
- package/dist/commands/schema.d.ts +19 -5
- package/dist/commands/schema.js +662 -702
- package/dist/commands/spec.d.ts +13 -4
- package/dist/commands/spec.js +91 -120
- package/dist/commands/store.d.ts +27 -2
- package/dist/commands/store.js +1 -120
- package/dist/commands/workflow/default-schema.d.ts +2 -0
- package/dist/commands/workflow/default-schema.js +4 -0
- package/dist/commands/workflow/instructions.js +1 -1
- package/dist/commands/workflow/shared.d.ts +1 -1
- package/dist/commands/workflow/shared.js +1 -1
- package/dist/commands/workset.d.ts +22 -2
- package/dist/commands/workset.js +1 -88
- package/dist/core/archive.js +32 -12
- package/dist/core/artifact-graph/outputs.js +39 -1
- package/dist/core/completion-tip.js +3 -1
- package/dist/core/completions/completion-provider.js +4 -2
- package/dist/core/config-schema.d.ts +1 -1
- package/dist/core/parsers/requirement-blocks.d.ts +11 -0
- package/dist/core/parsers/requirement-blocks.js +57 -0
- package/dist/core/specs-apply.js +5 -1
- package/dist/core/templates/workflows/apply-change.js +4 -3
- package/dist/core/templates/workflows/continue-change.js +17 -0
- package/dist/core/templates/workflows/explore.js +8 -4
- package/dist/core/templates/workflows/update-change.js +4 -4
- package/dist/core/templates/workflows/verify-change.js +26 -20
- package/dist/core/validation/validator.js +23 -2
- package/dist/core/view.js +2 -26
- package/dist/utils/change-metadata.d.ts +17 -0
- package/dist/utils/change-metadata.js +27 -2
- package/dist/utils/file-system.d.ts +7 -0
- package/dist/utils/file-system.js +25 -0
- package/package.json +1 -1
- package/schemas/spec-driven/schema.yaml +28 -4
package/dist/core/archive.js
CHANGED
|
@@ -247,6 +247,14 @@ function toArchiveDiagnostic(error) {
|
|
|
247
247
|
if (isRootSelectionError(error)) {
|
|
248
248
|
return error.diagnostic;
|
|
249
249
|
}
|
|
250
|
+
if (error instanceof RetirementCleanupError) {
|
|
251
|
+
return {
|
|
252
|
+
severity: 'error',
|
|
253
|
+
code: 'archive_retirement_cleanup_failed',
|
|
254
|
+
message: error.message,
|
|
255
|
+
fix: 'Inspect the archived change and all recovery paths in this diagnostic; preserve any needed content before cleanup.',
|
|
256
|
+
};
|
|
257
|
+
}
|
|
250
258
|
return {
|
|
251
259
|
severity: 'error',
|
|
252
260
|
code: 'archive_error',
|
|
@@ -379,7 +387,7 @@ async function assertCopiedDirectoryUnchanged(stagedSource, destination, expecte
|
|
|
379
387
|
*/
|
|
380
388
|
class MoveDestinationRetainedError extends Error {
|
|
381
389
|
}
|
|
382
|
-
class
|
|
390
|
+
class RetirementCleanupError extends Error {
|
|
383
391
|
}
|
|
384
392
|
function isFallbackRenameCode(code) {
|
|
385
393
|
return code === 'EPERM' || code === 'EXDEV';
|
|
@@ -1012,12 +1020,12 @@ async function finalizeRetirementBackups(snapshots, mainSpecsDir) {
|
|
|
1012
1020
|
snapshot.displacedPath = undefined;
|
|
1013
1021
|
}
|
|
1014
1022
|
catch (error) {
|
|
1015
|
-
errors.push(`Could not
|
|
1023
|
+
errors.push(`Could not finalize the retirement backup at ${displacedPath} ` +
|
|
1016
1024
|
`(${error instanceof Error ? error.message : String(error)}).`);
|
|
1017
1025
|
}
|
|
1018
1026
|
}
|
|
1019
1027
|
if (errors.length > 0) {
|
|
1020
|
-
throw new
|
|
1028
|
+
throw new RetirementCleanupError(`${errors.join(' ')} The change was archived, but retirement cleanup did not complete. Inspect all reported recovery paths before recovery or cleanup.`);
|
|
1021
1029
|
}
|
|
1022
1030
|
}
|
|
1023
1031
|
export class ArchiveCommand {
|
|
@@ -1070,13 +1078,22 @@ export class ArchiveCommand {
|
|
|
1070
1078
|
const changesDir = root.changesDir;
|
|
1071
1079
|
const archiveDir = root.archiveDir;
|
|
1072
1080
|
const mainSpecsDir = root.specsDir;
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1081
|
+
const openspecDir = path.dirname(changesDir);
|
|
1082
|
+
// openspec/, changes/ and specs/ may be project-owned symlinks to another
|
|
1083
|
+
// directory; archive/ may not leave the changes directory it lives in.
|
|
1084
|
+
for (const [allowedDirectory, managedDir, allowLinkedLeaf] of [
|
|
1085
|
+
[root.path, openspecDir, true],
|
|
1086
|
+
[openspecDir, changesDir, true],
|
|
1087
|
+
[changesDir, archiveDir, false],
|
|
1088
|
+
[openspecDir, mainSpecsDir, true],
|
|
1077
1089
|
]) {
|
|
1078
1090
|
try {
|
|
1079
|
-
|
|
1091
|
+
if (allowLinkedLeaf) {
|
|
1092
|
+
FileSystemUtils.assertPathWithinAllowingLinkedLeaf(allowedDirectory, managedDir);
|
|
1093
|
+
}
|
|
1094
|
+
else {
|
|
1095
|
+
FileSystemUtils.assertPathWithin(allowedDirectory, managedDir);
|
|
1096
|
+
}
|
|
1080
1097
|
}
|
|
1081
1098
|
catch {
|
|
1082
1099
|
throw new ArchiveBlockedError('archive_path_outside_root', `Refusing to archive through a path outside the OpenSpec root: ${managedDir}`);
|
|
@@ -1736,6 +1753,9 @@ export class ArchiveCommand {
|
|
|
1736
1753
|
if (hasRetirements) {
|
|
1737
1754
|
await assertRetirementAuthorization(changeDir, retirementAuthorizationFingerprint);
|
|
1738
1755
|
}
|
|
1756
|
+
// update.source is canonical (symlinks resolved), so measure it from
|
|
1757
|
+
// the canonical change directory; changes/ may itself be a symlink.
|
|
1758
|
+
const canonicalChangeDir = FileSystemUtils.canonicalizeExistingPath(changeDir);
|
|
1739
1759
|
const verifyArchivedDeltas = async (stagedSource) => {
|
|
1740
1760
|
if (hasRetirements) {
|
|
1741
1761
|
await assertRetirementAuthorization(archivePath, retirementAuthorizationFingerprint,
|
|
@@ -1749,13 +1769,13 @@ export class ArchiveCommand {
|
|
|
1749
1769
|
}
|
|
1750
1770
|
}
|
|
1751
1771
|
for (const proposed of prepared) {
|
|
1752
|
-
const archivedSource = path.join(archivePath, path.relative(
|
|
1772
|
+
const archivedSource = path.join(archivePath, path.relative(canonicalChangeDir, proposed.update.source));
|
|
1753
1773
|
if ((await fingerprintPortableContent(archivedSource)) !==
|
|
1754
1774
|
proposed.sourceContentFingerprint) {
|
|
1755
1775
|
throw new Error(`The archived delta for '${proposed.update.id}' changed during the final move.`);
|
|
1756
1776
|
}
|
|
1757
1777
|
if (stagedSource) {
|
|
1758
|
-
const stagedDelta = path.join(stagedSource, path.relative(
|
|
1778
|
+
const stagedDelta = path.join(stagedSource, path.relative(canonicalChangeDir, proposed.update.source));
|
|
1759
1779
|
if ((await fingerprintPortableContent(stagedDelta)) !==
|
|
1760
1780
|
proposed.sourceContentFingerprint) {
|
|
1761
1781
|
throw new Error(`The active delta for '${proposed.update.id}' changed during the fallback copy.`);
|
|
@@ -1777,11 +1797,11 @@ export class ArchiveCommand {
|
|
|
1777
1797
|
await finalizeRetirementBackups(specSnapshots, mainSpecsDir);
|
|
1778
1798
|
}
|
|
1779
1799
|
catch (cleanupError) {
|
|
1780
|
-
throw new
|
|
1800
|
+
throw new RetirementCleanupError(`${error.message} ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
1781
1801
|
}
|
|
1782
1802
|
throw error;
|
|
1783
1803
|
}
|
|
1784
|
-
if (error instanceof
|
|
1804
|
+
if (error instanceof RetirementCleanupError)
|
|
1785
1805
|
throw error;
|
|
1786
1806
|
const rollbackErrors = [];
|
|
1787
1807
|
try {
|
|
@@ -4,6 +4,41 @@ import fg from 'fast-glob';
|
|
|
4
4
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
5
5
|
const EXTGLOB_RE = /[!*+?@]\([^(]*\)/u;
|
|
6
6
|
const BRACE_EXPANSION_SEPARATORS_RE = /,|\.\./u;
|
|
7
|
+
// braces <=3.0.3 (fast-glob > micromatch) has no depth guard (GHSA-vfj7-8cjw-p6xm),
|
|
8
|
+
// so reject nesting no real artifact pattern needs before it reaches the parser.
|
|
9
|
+
const MAX_BRACE_NESTING = 16;
|
|
10
|
+
function assertBraceNesting(pattern) {
|
|
11
|
+
// Mirror the braces parser: `[...]` and quoted runs are literal, `(` nests like
|
|
12
|
+
// `{`, and a closer only closes the innermost group of its own kind.
|
|
13
|
+
const open = [];
|
|
14
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
15
|
+
const char = pattern[index];
|
|
16
|
+
if (char === '[') {
|
|
17
|
+
for (let brackets = 1; brackets > 0 && index + 1 < pattern.length;) {
|
|
18
|
+
index += 1;
|
|
19
|
+
if (pattern[index] === '[')
|
|
20
|
+
brackets += 1;
|
|
21
|
+
else if (pattern[index] === ']')
|
|
22
|
+
brackets -= 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else if (char === '"' || char === "'" || char === '`') {
|
|
26
|
+
const end = pattern.indexOf(char, index + 1);
|
|
27
|
+
index = end === -1 ? pattern.length : end;
|
|
28
|
+
}
|
|
29
|
+
else if (char === '{' || char === '(') {
|
|
30
|
+
open.push(char);
|
|
31
|
+
if (open.length > MAX_BRACE_NESTING) {
|
|
32
|
+
// A pattern this deep can run to thousands of characters; quote only its start.
|
|
33
|
+
const shown = pattern.length > 80 ? `${pattern.slice(0, 80)}...` : pattern;
|
|
34
|
+
throw new Error(`Artifact output pattern nests braces more than ${MAX_BRACE_NESTING} levels deep: ${shown}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if ((char === '}' && open.at(-1) === '{') || (char === ')' && open.at(-1) === '(')) {
|
|
38
|
+
open.pop();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
7
42
|
function hasBraceExpansion(pattern) {
|
|
8
43
|
const openings = [];
|
|
9
44
|
for (let index = 0; index < pattern.length; index += 1) {
|
|
@@ -90,8 +125,11 @@ function assertGlobDirectoryTraversal(changeDir, currentDir, directorySegments,
|
|
|
90
125
|
* Returns absolute file paths. Glob matches are sorted for deterministic output.
|
|
91
126
|
*/
|
|
92
127
|
export function resolveArtifactOutputs(changeDir, generates) {
|
|
128
|
+
const isGlob = isGlobPattern(generates);
|
|
129
|
+
if (isGlob)
|
|
130
|
+
assertBraceNesting(FileSystemUtils.toPosixPath(generates));
|
|
93
131
|
const outputPath = resolveArtifactOutputPath(changeDir, generates);
|
|
94
|
-
if (!
|
|
132
|
+
if (!isGlob) {
|
|
95
133
|
try {
|
|
96
134
|
return fs.statSync(outputPath).isFile()
|
|
97
135
|
? [FileSystemUtils.canonicalizeExistingPath(outputPath)]
|
|
@@ -22,7 +22,6 @@ import { getGlobalConfigPath } from './global-config.js';
|
|
|
22
22
|
import { writeFileAtomically } from './file-state.js';
|
|
23
23
|
import { isCiEnvironment } from '../utils/ci.js';
|
|
24
24
|
import { detectShell } from '../utils/shell-detection.js';
|
|
25
|
-
import { CompletionFactory } from './completions/factory.js';
|
|
26
25
|
export const COMPLETION_TIP_MESSAGE = "Tip: Run 'openspec completion install' for shell completions";
|
|
27
26
|
function isSuppressedByEnv() {
|
|
28
27
|
// isCiEnvironment, not a CI==='true' string check: providers set CI to "True",
|
|
@@ -54,6 +53,9 @@ async function decideTip() {
|
|
|
54
53
|
if (!shell) {
|
|
55
54
|
return 'retire';
|
|
56
55
|
}
|
|
56
|
+
// Loaded only here: every shell's generator and installer, needed only on
|
|
57
|
+
// the rare run that still owes the tip.
|
|
58
|
+
const { CompletionFactory } = await import('./completions/factory.js');
|
|
57
59
|
return (await CompletionFactory.createInstaller(shell).isInstalled())
|
|
58
60
|
? 'retire'
|
|
59
61
|
: 'show';
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { getActiveChangeIds, getSpecIds } from '../../utils/item-discovery.js';
|
|
2
|
-
import { listSchemas } from '../artifact-graph/index.js';
|
|
3
2
|
/**
|
|
4
3
|
* Provides dynamic completion suggestions for OpenSpec items (changes and specs).
|
|
5
4
|
* Implements a 2-second cache to avoid excessive file system operations during
|
|
@@ -74,7 +73,10 @@ export class CompletionProvider {
|
|
|
74
73
|
if (this.schemaCache && now - this.schemaCache.timestamp < this.cacheTTL) {
|
|
75
74
|
return this.schemaCache.data;
|
|
76
75
|
}
|
|
77
|
-
// Fetch fresh data
|
|
76
|
+
// Fetch fresh data. The resolver (and the yaml and zod it parses schemas
|
|
77
|
+
// with) loads only here: completing changes or specs never needs it, and
|
|
78
|
+
// the artifact-graph barrel would also pull in fast-glob.
|
|
79
|
+
const { listSchemas } = await import('../artifact-graph/resolver.js');
|
|
78
80
|
const schemaNames = listSchemas(this.projectRoot);
|
|
79
81
|
// Update cache
|
|
80
82
|
this.schemaCache = {
|
|
@@ -10,9 +10,9 @@ export declare const GlobalConfigSchema: z.ZodObject<{
|
|
|
10
10
|
core: "core";
|
|
11
11
|
}>>>;
|
|
12
12
|
delivery: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
13
|
-
commands: "commands";
|
|
14
13
|
both: "both";
|
|
15
14
|
skills: "skills";
|
|
15
|
+
commands: "commands";
|
|
16
16
|
}>>>;
|
|
17
17
|
workflows: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
18
18
|
defaultStore: z.ZodOptional<z.ZodString>;
|
|
@@ -70,6 +70,15 @@ export interface OrphanedRequirement {
|
|
|
70
70
|
section: string | null;
|
|
71
71
|
line: number;
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Authored text archive does not carry into the main spec: a non-delta `## `
|
|
75
|
+
* section, text above the first `## `, or prose before the first requirement
|
|
76
|
+
* in ADDED/MODIFIED. `section` is null for text above the first `## `.
|
|
77
|
+
*/
|
|
78
|
+
export interface UnappliedContent {
|
|
79
|
+
section: string | null;
|
|
80
|
+
line: number;
|
|
81
|
+
}
|
|
73
82
|
export interface DeltaPlan {
|
|
74
83
|
added: RequirementBlock[];
|
|
75
84
|
modified: RequirementBlock[];
|
|
@@ -83,6 +92,7 @@ export interface DeltaPlan {
|
|
|
83
92
|
unpairedRenames: UnpairedRename[];
|
|
84
93
|
/** Canonical requirement blocks written outside every delta section. */
|
|
85
94
|
orphanedRequirements: OrphanedRequirement[];
|
|
95
|
+
unappliedContent: UnappliedContent[];
|
|
86
96
|
skippedHeaders: SkippedHeader[];
|
|
87
97
|
sectionPresence: {
|
|
88
98
|
added: boolean;
|
|
@@ -95,6 +105,7 @@ export interface DeltaPlan {
|
|
|
95
105
|
* Parse a delta-formatted spec change file content into a DeltaPlan with raw blocks.
|
|
96
106
|
*/
|
|
97
107
|
export declare function parseDeltaSpec(content: string): DeltaPlan;
|
|
108
|
+
export declare function describeUnappliedContent(content: UnappliedContent, sentenceStart?: boolean): string;
|
|
98
109
|
/** Both directions of the scenario-name comparison, plus the two totals. */
|
|
99
110
|
export interface ScenarioNameDiff {
|
|
100
111
|
/** Names the current block has that the incoming block does not cover. */
|
|
@@ -136,6 +136,7 @@ export function parseDeltaSpec(content) {
|
|
|
136
136
|
renamed: renamedPairs,
|
|
137
137
|
unpairedRenames,
|
|
138
138
|
orphanedRequirements: findOrphanedRequirements(lines, fenceMask),
|
|
139
|
+
unappliedContent: findUnappliedContent(lines, fenceMask),
|
|
139
140
|
skippedHeaders,
|
|
140
141
|
sectionPresence: {
|
|
141
142
|
added: addedLookup.found,
|
|
@@ -187,6 +188,62 @@ function findOrphanedRequirements(lines, fenceMask) {
|
|
|
187
188
|
}
|
|
188
189
|
return orphans;
|
|
189
190
|
}
|
|
191
|
+
export function describeUnappliedContent(content, sentenceStart = false) {
|
|
192
|
+
const text = content.section === null
|
|
193
|
+
? 'text above the first "## " section'
|
|
194
|
+
: DELTA_SECTION_TITLES.has(content.section.toLowerCase())
|
|
195
|
+
? `text before the first requirement in "## ${content.section}"`
|
|
196
|
+
: `section "## ${content.section}"`;
|
|
197
|
+
return sentenceStart ? text.charAt(0).toUpperCase() + text.slice(1) : text;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Where text the merge never reads begins. HTML comments, `# ` titles, Purpose
|
|
201
|
+
* (warned about separately), REMOVED/RENAMED bodies, and stray `###` headers
|
|
202
|
+
* before the first requirement (INFO since #498) are not reported.
|
|
203
|
+
*/
|
|
204
|
+
function findUnappliedContent(lines, fenceMask) {
|
|
205
|
+
// Blank out comments outside fences, keeping line count; an unterminated one
|
|
206
|
+
// runs to EOF. Comments only decide whether a line holds text: sections come
|
|
207
|
+
// from the same split the merge reads, where a `## ` line inside a comment is
|
|
208
|
+
// still a boundary. Fenced lines keep their text, so a `<!--` in a code sample
|
|
209
|
+
// never hides what follows the fence.
|
|
210
|
+
const comments = lines
|
|
211
|
+
.map((line, i) => (fenceMask[i] ? '' : line))
|
|
212
|
+
.join('\n')
|
|
213
|
+
.replace(/<!--[\s\S]*?(?:--!?>|$)/g, (m) => m.replace(/[^\n]/g, ' '))
|
|
214
|
+
.split('\n');
|
|
215
|
+
const masked = lines.map((line, i) => (fenceMask[i] ? line : comments[i]));
|
|
216
|
+
const sections = splitTopLevelSections(lines, fenceMask);
|
|
217
|
+
const firstText = (from, to, skip) => {
|
|
218
|
+
for (let i = from; i < to; i++)
|
|
219
|
+
if (masked[i].trim() && !skip?.test(masked[i]))
|
|
220
|
+
return i;
|
|
221
|
+
return -1;
|
|
222
|
+
};
|
|
223
|
+
const found = [];
|
|
224
|
+
const preamble = firstText(0, sections.length ? sections[0].body.bodyStartLine - 2 : lines.length, /^#\s/);
|
|
225
|
+
if (preamble !== -1)
|
|
226
|
+
found.push({ section: null, line: preamble + 1 });
|
|
227
|
+
for (const { title, body } of sections) {
|
|
228
|
+
const folded = title.toLowerCase();
|
|
229
|
+
const start = body.bodyStartLine - 1;
|
|
230
|
+
if (folded === 'added requirements' || folded === 'modified requirements') {
|
|
231
|
+
const block = body.lines.findIndex((l, i) => !body.fenceMask[i] && REQUIREMENT_HEADER_REGEX.test(l));
|
|
232
|
+
const first = firstText(start, start + (block === -1 ? body.lines.length : block), /^###\s/);
|
|
233
|
+
if (first !== -1)
|
|
234
|
+
found.push({ section: title, line: first + 1 });
|
|
235
|
+
}
|
|
236
|
+
else if (!DELTA_SECTION_TITLES.has(folded)) {
|
|
237
|
+
// The Purpose carry-over reads past commented-out headers, so only a
|
|
238
|
+
// `## Purpose` outside a comment is the one archive handles.
|
|
239
|
+
if (folded === 'purpose' && masked[start - 1].trim())
|
|
240
|
+
continue;
|
|
241
|
+
if (firstText(start, start + body.lines.length) !== -1)
|
|
242
|
+
found.push({ section: title, line: start });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return found;
|
|
246
|
+
}
|
|
190
247
|
/**
|
|
191
248
|
* Every `## ` section, as a LIST rather than a title-keyed record.
|
|
192
249
|
*
|
package/dist/core/specs-apply.js
CHANGED
|
@@ -8,7 +8,7 @@ import { promises as fs } from 'fs';
|
|
|
8
8
|
import { randomUUID } from 'crypto';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import chalk from 'chalk';
|
|
11
|
-
import { extractRequirementsSection, diffScenarioNames, describeScenarioBalance, foldRequirementName, parseDeltaSpec, normalizeRequirementName, } from './parsers/requirement-blocks.js';
|
|
11
|
+
import { extractRequirementsSection, diffScenarioNames, describeScenarioBalance, describeUnappliedContent, foldRequirementName, parseDeltaSpec, normalizeRequirementName, } from './parsers/requirement-blocks.js';
|
|
12
12
|
import { findMainSpecStructureIssues } from './parsers/spec-structure.js';
|
|
13
13
|
import { buildCodeFenceMask } from './parsers/code-fence.js';
|
|
14
14
|
import { MarkdownParser } from './parsers/markdown-parser.js';
|
|
@@ -145,6 +145,10 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
|
145
145
|
`which is not a delta section, so it was not applied. ` +
|
|
146
146
|
`Move it under ADDED/MODIFIED/REMOVED/RENAMED Requirements.`);
|
|
147
147
|
}
|
|
148
|
+
// Archive carries requirement blocks only; name any other text it leaves behind.
|
|
149
|
+
for (const lost of plan.unappliedContent) {
|
|
150
|
+
warn(`${specName} - ${describeUnappliedContent(lost)} (line ${lost.line}) is not carried into the main spec.`);
|
|
151
|
+
}
|
|
148
152
|
// Pre-validate duplicates within sections
|
|
149
153
|
const addedNames = new Set();
|
|
150
154
|
for (const add of plan.added) {
|
|
@@ -66,7 +66,7 @@ ${PROJECT_ROOT_GUARD}
|
|
|
66
66
|
- If \`state: "blocked"\`: show the message and pause implementation.
|
|
67
67
|
- If \`missingArtifacts\` is non-empty: ${BLOCKED_STATE_HANDOFF}
|
|
68
68
|
- Otherwise, follow the CLI instruction to create or repair the schema-configured tracking file from existing planning artifacts. Do not assume another artifact is ready or start implementation while blocked.
|
|
69
|
-
- If \`state: "all_done"\`:
|
|
69
|
+
- If \`state: "all_done"\`: report that all tracked tasks are complete and suggest review or verification as appropriate before archiving
|
|
70
70
|
- Otherwise: proceed to implementation
|
|
71
71
|
|
|
72
72
|
Treat \`context\` as a required prompt-level input. Read and consider it, and
|
|
@@ -125,7 +125,7 @@ ${PROJECT_ROOT_GUARD}
|
|
|
125
125
|
Display:
|
|
126
126
|
- Tasks completed this session
|
|
127
127
|
- Overall progress: "N/M tasks complete"
|
|
128
|
-
- If all done: suggest
|
|
128
|
+
- If all done: report that tracked tasks are complete and suggest review or verification as appropriate before archiving
|
|
129
129
|
- If paused: explain why and wait for guidance
|
|
130
130
|
|
|
131
131
|
**Output During Implementation**
|
|
@@ -156,7 +156,8 @@ Working on task 4/7: <task description>
|
|
|
156
156
|
- [x] Task 2
|
|
157
157
|
...
|
|
158
158
|
|
|
159
|
-
All tasks complete
|
|
159
|
+
All tracked tasks are complete. Review or verify the change as appropriate
|
|
160
|
+
before archiving. ${ARCHIVE_HANDOFF}
|
|
160
161
|
\`\`\`
|
|
161
162
|
|
|
162
163
|
**Output On Pause (Issue Encountered)**
|
|
@@ -7,6 +7,15 @@ import { PROJECT_ROOT_GUARD } from './project-root.js';
|
|
|
7
7
|
* optional-workflow.ts).
|
|
8
8
|
*/
|
|
9
9
|
const PLANNING_COMPLETE_HANDOFF = optionalWorkflow('apply', 'You can now implement this change with `/opsx:apply`.', 'You can now implement this change - `openspec instructions apply --change "<name>" --json` returns the tasks and how to work them.') + ' ' + optionalWorkflow('archive', 'Once implementation and any tracked work are complete, archive it with `/opsx:archive`.', 'Once implementation and any tracked work are complete, archive it with `openspec archive "<name>"`.');
|
|
10
|
+
/**
|
|
11
|
+
* A glob artifact reads `done` once one matching file exists, so a run
|
|
12
|
+
* interrupted while writing several files leaves it `done` with files missing,
|
|
13
|
+
* and the next run would move on without reading its instruction again (#2034).
|
|
14
|
+
*/
|
|
15
|
+
const PARTIAL_ARTIFACT_CHECK = `**Before anything else, finish a partly written artifact**:
|
|
16
|
+
- An artifact whose output is a glob pattern (e.g. \`specs/**/*.md\`) reads \`done\` as soon as one matching file exists, so a run interrupted while writing its files leaves it \`done\` with some still missing
|
|
17
|
+
- For each such \`done\` artifact, first get its instructions and read any completed dependencies needed to determine the expected files. Then compare those files with \`artifactPaths.<id>.existingOutputPaths\`. For spec-driven's \`specs\`, read the proposal and check for one \`specs/<capability-path>/spec.md\` per capability it lists; resolve each expected path against \`changeRoot\` before comparing path identity
|
|
18
|
+
- If any are missing, write only the missing files, and STOP - that counts as this invocation's ONE artifact. If you cannot tell whether a file was left out on purpose, ask the user`;
|
|
10
19
|
export function getContinueChangeSkillTemplate() {
|
|
11
20
|
return {
|
|
12
21
|
name: 'openspec-continue-change',
|
|
@@ -51,6 +60,10 @@ ${PROJECT_ROOT_GUARD}
|
|
|
51
60
|
|
|
52
61
|
---
|
|
53
62
|
|
|
63
|
+
${PARTIAL_ARTIFACT_CHECK}
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
54
67
|
**If all planning artifacts are complete (\`isPlanningComplete: true\`, or legacy \`isComplete: true\`)**:
|
|
55
68
|
- Congratulate the user
|
|
56
69
|
- Show final status including the schema used
|
|
@@ -169,6 +182,10 @@ ${PROJECT_ROOT_GUARD}
|
|
|
169
182
|
|
|
170
183
|
---
|
|
171
184
|
|
|
185
|
+
${PARTIAL_ARTIFACT_CHECK}
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
172
189
|
**If all planning artifacts are complete (\`isPlanningComplete: true\`, or legacy \`isComplete: true\`)**:
|
|
173
190
|
- Congratulate the user
|
|
174
191
|
- Show final status including the schema used
|
|
@@ -178,6 +178,8 @@ If the user mentions a change or you detect one is relevant:
|
|
|
178
178
|
|
|
179
179
|
\`<capability-path>\` is the spec directory relative to \`specs/\` (for example, \`user-auth\` or \`identity/user-auth\`). Preserve an existing capability's full path and follow the project's established organization for new capabilities.
|
|
180
180
|
|
|
181
|
+
The files below are the default spec-driven schema's. With another schema, capture each insight in whichever artifact from \`artifactPaths\` fits it, and name that artifact in your offer; never create an artifact the schema does not define.
|
|
182
|
+
|
|
181
183
|
| Insight Type | Where to Capture |
|
|
182
184
|
|----------------------------|-------------------------------------|
|
|
183
185
|
| New requirement discovered | \`specs/<capability-path>/spec.md\` |
|
|
@@ -187,7 +189,7 @@ If the user mentions a change or you detect one is relevant:
|
|
|
187
189
|
| New work identified | \`tasks.md\` |
|
|
188
190
|
| Assumption invalidated | Relevant artifact |
|
|
189
191
|
|
|
190
|
-
Example offers:
|
|
192
|
+
Example offers (default schema):
|
|
191
193
|
- "That's a design decision. Capture it in design.md?"
|
|
192
194
|
- "This is a new requirement. Add it to specs?"
|
|
193
195
|
- "This changes scope. Update the proposal?"
|
|
@@ -316,7 +318,7 @@ You: That changes everything.
|
|
|
316
318
|
There's no required ending. Discovery might:
|
|
317
319
|
|
|
318
320
|
- **Flow into a proposal**: "${DISCOVERY_END_HANDOFF}"
|
|
319
|
-
- **Result in artifact updates**: "Updated design
|
|
321
|
+
- **Result in artifact updates**: "Updated the design with these decisions"
|
|
320
322
|
- **Just provide clarity**: User has what they need, moves on
|
|
321
323
|
- **Continue later**: "We can pick this up anytime"
|
|
322
324
|
|
|
@@ -511,6 +513,8 @@ If the user mentions a change or you detect one is relevant:
|
|
|
511
513
|
|
|
512
514
|
\`<capability-path>\` is the spec directory relative to \`specs/\` (for example, \`user-auth\` or \`identity/user-auth\`). Preserve an existing capability's full path and follow the project's established organization for new capabilities.
|
|
513
515
|
|
|
516
|
+
The files below are the default spec-driven schema's. With another schema, capture each insight in whichever artifact from \`artifactPaths\` fits it, and name that artifact in your offer; never create an artifact the schema does not define.
|
|
517
|
+
|
|
514
518
|
| Insight Type | Where to Capture |
|
|
515
519
|
|----------------------------|-------------------------------------|
|
|
516
520
|
| New requirement discovered | \`specs/<capability-path>/spec.md\` |
|
|
@@ -520,7 +524,7 @@ If the user mentions a change or you detect one is relevant:
|
|
|
520
524
|
| New work identified | \`tasks.md\` |
|
|
521
525
|
| Assumption invalidated | Relevant artifact |
|
|
522
526
|
|
|
523
|
-
Example offers:
|
|
527
|
+
Example offers (default schema):
|
|
524
528
|
- "That's a design decision. Capture it in design.md?"
|
|
525
529
|
- "This is a new requirement. Add it to specs?"
|
|
526
530
|
- "This changes scope. Update the proposal?"
|
|
@@ -545,7 +549,7 @@ If the user mentions a change or you detect one is relevant:
|
|
|
545
549
|
There's no required ending. Discovery might:
|
|
546
550
|
|
|
547
551
|
- **Flow into a proposal**: "${DISCOVERY_END_HANDOFF}"
|
|
548
|
-
- **Result in artifact updates**: "Updated design
|
|
552
|
+
- **Result in artifact updates**: "Updated the design with these decisions"
|
|
549
553
|
- **Just provide clarity**: User has what they need, moves on
|
|
550
554
|
- **Continue later**: "We can pick this up anytime"
|
|
551
555
|
|
|
@@ -64,7 +64,7 @@ ${CONTINUE_SCOPE_NOTE}
|
|
|
64
64
|
|
|
65
65
|
The artifact ids and paths come from the active schema - do NOT assume them, and do NOT branch on hardcoded artifact names. Custom schemas must work unchanged.
|
|
66
66
|
|
|
67
|
-
The files to edit are \`artifactPaths.<id>.existingOutputPaths\` - the concrete files that exist on disk, already glob-expanded for glob artifacts (e.g. \`specs/**/*.md\`). Do NOT write to \`resolvedOutputPath\`: for a glob artifact it is still the glob pattern, not a real file.
|
|
67
|
+
The files to edit are \`artifactPaths.<id>.existingOutputPaths\` - the concrete files that exist on disk, already glob-expanded for glob artifacts (e.g. \`specs/**/*.md\`), plus any confirmed concrete path allowed under a partly populated glob artifact (step 4). Do NOT write to \`resolvedOutputPath\`: for a glob artifact it is still the glob pattern, not a real file.
|
|
68
68
|
|
|
69
69
|
3. **Understand the request**
|
|
70
70
|
- If the user asked for a specific revision ("the design now uses X"), that is the starting edit.
|
|
@@ -108,7 +108,7 @@ After each invocation, show:
|
|
|
108
108
|
**Guardrails**
|
|
109
109
|
- Planning artifacts only - NEVER edit implementation code. If the revised plan implies code changes, ${APPLY_GUARDRAIL}.
|
|
110
110
|
- Use the artifact ids and paths reported by \`openspec status\`; never branch on hardcoded artifact names.
|
|
111
|
-
- Edit only the concrete files in \`existingOutputPaths
|
|
111
|
+
- Edit only the concrete files in \`existingOutputPaths\`, plus any confirmed concrete path allowed under a partly populated glob artifact (step 4); never write to a glob \`resolvedOutputPath\`.
|
|
112
112
|
- Do not advance the build frontier: if an artifact has empty \`existingOutputPaths\` and status \`ready\` or \`blocked\`, ${CONTINUE_FRONTIER}. Leave \`skipped\` artifacts untouched. The only new-file scope is a confirmed concrete path under a glob artifact whose \`existingOutputPaths\` is non-empty.
|
|
113
113
|
- Confirm every edit with the user before writing.
|
|
114
114
|
- If the request changes the change's *intent* rather than refining it, ${INTENT_CHANGE_GUARDRAIL}.`,
|
|
@@ -163,7 +163,7 @@ ${CONTINUE_SCOPE_NOTE}
|
|
|
163
163
|
|
|
164
164
|
The artifact ids and paths come from the active schema - do NOT assume them, and do NOT branch on hardcoded artifact names. Custom schemas must work unchanged.
|
|
165
165
|
|
|
166
|
-
The files to edit are \`artifactPaths.<id>.existingOutputPaths\` - the concrete files that exist on disk, already glob-expanded for glob artifacts (e.g. \`specs/**/*.md\`). Do NOT write to \`resolvedOutputPath\`: for a glob artifact it is still the glob pattern, not a real file.
|
|
166
|
+
The files to edit are \`artifactPaths.<id>.existingOutputPaths\` - the concrete files that exist on disk, already glob-expanded for glob artifacts (e.g. \`specs/**/*.md\`), plus any confirmed concrete path allowed under a partly populated glob artifact (step 4). Do NOT write to \`resolvedOutputPath\`: for a glob artifact it is still the glob pattern, not a real file.
|
|
167
167
|
|
|
168
168
|
3. **Understand the request**
|
|
169
169
|
- If the user asked for a specific revision ("the design now uses X"), that is the starting edit.
|
|
@@ -207,7 +207,7 @@ After each invocation, show:
|
|
|
207
207
|
**Guardrails**
|
|
208
208
|
- Planning artifacts only - NEVER edit implementation code. If the revised plan implies code changes, ${APPLY_GUARDRAIL}.
|
|
209
209
|
- Use the artifact ids and paths reported by \`openspec status\`; never branch on hardcoded artifact names.
|
|
210
|
-
- Edit only the concrete files in \`existingOutputPaths
|
|
210
|
+
- Edit only the concrete files in \`existingOutputPaths\`, plus any confirmed concrete path allowed under a partly populated glob artifact (step 4); never write to a glob \`resolvedOutputPath\`.
|
|
211
211
|
- Do not advance the build frontier: if an artifact has empty \`existingOutputPaths\` and status \`ready\` or \`blocked\`, ${CONTINUE_FRONTIER}. Leave \`skipped\` artifacts untouched. The only new-file scope is a confirmed concrete path under a glob artifact whose \`existingOutputPaths\` is non-empty.
|
|
212
212
|
- Confirm every edit with the user before writing.
|
|
213
213
|
- If the request changes the change's *intent* rather than refining it, ${INTENT_CHANGE_GUARDRAIL}.`
|