@fission-ai/openspec 1.14.1 → 1.14.3

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.
@@ -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
  *
@@ -8,13 +8,14 @@ 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';
15
15
  import { MIN_PURPOSE_LENGTH, PURPOSE_PLACEHOLDER_PREFIX, PURPOSE_PLACEHOLDER_SUFFIX, } from './validation/constants.js';
16
16
  import { discoverSpecFiles } from '../utils/spec-discovery.js';
17
17
  import { FileSystemUtils } from '../utils/file-system.js';
18
+ import { assertSpecFileWithin } from '../utils/spec-path-containment.js';
18
19
  import { matchLineEnding } from '../utils/line-endings.js';
19
20
  function isLexicallyWithin(allowedDirectory, targetPath) {
20
21
  const relative = path.relative(path.resolve(allowedDirectory), path.resolve(targetPath));
@@ -29,7 +30,7 @@ function resolveTrustedSpecPath(specsRoot, specPath, projectRoot) {
29
30
  }
30
31
  try {
31
32
  // Preserve spec.md links that remain inside the overall specs tree.
32
- FileSystemUtils.assertPathWithin(specsRoot, specPath);
33
+ assertSpecFileWithin(specsRoot, specPath);
33
34
  const root = FileSystemUtils.canonicalizeExistingPath(specsRoot);
34
35
  return {
35
36
  root,
@@ -52,7 +53,7 @@ function resolveTrustedSpecPath(specsRoot, specPath, projectRoot) {
52
53
  process.emitWarning(`Capability '${path.basename(path.dirname(specPath))}' links outside the project; writing to ${root}`, 'OpenSpecExternalSpecWrite');
53
54
  }
54
55
  const file = path.join(root, path.basename(specPath));
55
- FileSystemUtils.assertPathWithin(root, file);
56
+ assertSpecFileWithin(root, file);
56
57
  return { root, file };
57
58
  }
58
59
  }
@@ -60,7 +61,7 @@ function assertTrustedSpecPath(root, specPath) {
60
61
  if (FileSystemUtils.canonicalizeExistingPath(root) !== path.resolve(root)) {
61
62
  throw new Error(`Path is outside the allowed directory: ${specPath}`);
62
63
  }
63
- FileSystemUtils.assertPathWithin(root, specPath);
64
+ assertSpecFileWithin(root, specPath);
64
65
  }
65
66
  // -----------------------------------------------------------------------------
66
67
  // Public API
@@ -145,6 +146,10 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
145
146
  `which is not a delta section, so it was not applied. ` +
146
147
  `Move it under ADDED/MODIFIED/REMOVED/RENAMED Requirements.`);
147
148
  }
149
+ // Archive carries requirement blocks only; name any other text it leaves behind.
150
+ for (const lost of plan.unappliedContent) {
151
+ warn(`${specName} - ${describeUnappliedContent(lost)} (line ${lost.line}) is not carried into the main spec.`);
152
+ }
148
153
  // Pre-validate duplicates within sections
149
154
  const addedNames = new Set();
150
155
  for (const add of plan.added) {
@@ -1038,6 +1043,14 @@ export async function pruneEmptyDirs(startDir, boundaryDir) {
1038
1043
  */
1039
1044
  export async function writeUpdatedSpec(update, rebuilt, counts, options = {}) {
1040
1045
  assertTrustedSpecPath(update.targetRoot, update.target);
1046
+ // Pin what the assertion just checked. A capability's spec.md may be a link
1047
+ // to the file the project keeps elsewhere, so the write follows a link by
1048
+ // design - and resolving it once, here, is what keeps that from becoming a
1049
+ // redirect: a link repointed after this line, including from inside
1050
+ // `beforeMutate`, cannot send the write to a file nothing verified. The
1051
+ // bytes still land in the same place a path-based write would have put them,
1052
+ // which keeps the in-place semantics below intact.
1053
+ const verifiedTarget = FileSystemUtils.canonicalizeExistingPath(update.target);
1041
1054
  // Create target directory if needed
1042
1055
  const targetDir = path.dirname(update.target);
1043
1056
  await fs.mkdir(targetDir, { recursive: true });
@@ -1047,17 +1060,26 @@ export async function writeUpdatedSpec(update, rebuilt, counts, options = {}) {
1047
1060
  // requirement moved. A spec that does not exist yet stays LF.
1048
1061
  // Only a missing file means "no convention to match". Swallowing every error
1049
1062
  // would read an existing but unreadable spec as absent and rewrite it as LF.
1050
- const previous = await fs.readFile(update.target, 'utf-8').catch((error) => {
1063
+ const previous = await fs.readFile(verifiedTarget, 'utf-8').catch((error) => {
1051
1064
  if (error.code === 'ENOENT')
1052
1065
  return undefined;
1053
1066
  throw error;
1054
1067
  });
1055
1068
  const toWrite = previous === undefined ? rebuilt : matchLineEnding(rebuilt, previous);
1056
1069
  await options.beforeMutate?.();
1070
+ // `beforeMutate` awaits arbitrary work, so it is the widest gap between the
1071
+ // check above and the write below. Re-check there, and refuse unless the
1072
+ // target still resolves to the very file that was verified: pinning a
1073
+ // pathname is not enough on its own, because the pinned path can itself be
1074
+ // replaced by a link to somewhere else.
1075
+ assertTrustedSpecPath(update.targetRoot, update.target);
1076
+ if (FileSystemUtils.canonicalizeExistingPath(update.target) !== verifiedTarget) {
1077
+ throw new Error(`Spec target for '${update.id}' changed before it could be written: ${update.target}`);
1078
+ }
1057
1079
  // Preserve the established in-place write semantics: symlink referents,
1058
1080
  // hard-linked specs, ACLs, extended attributes, and filesystems without hard
1059
1081
  // links must continue to behave as they did before capability retirement.
1060
- await fs.writeFile(update.target, toWrite);
1082
+ await fs.writeFile(verifiedTarget, toWrite);
1061
1083
  if (options.silent)
1062
1084
  return;
1063
1085
  const specName = update.id;
@@ -14,8 +14,8 @@ const PLANNING_COMPLETE_HANDOFF = optionalWorkflow('apply', 'You can now impleme
14
14
  */
15
15
  const PARTIAL_ARTIFACT_CHECK = `**Before anything else, finish a partly written artifact**:
16
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`;
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, and verify each one remains inside \`changeRoot\` after resolving symlinks, both in its parent directories and in the file itself if it already exists
18
+ - Never write an expected path that fails that check; stop and tell the user. If any are missing, write only the missing files, each at its checked path - when the \`instruction\` field delegates creation to a skill or command, invoke it only if it can write each file to its checked path; otherwise stop and tell the user - 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`;
19
19
  export function getContinueChangeSkillTemplate() {
20
20
  return {
21
21
  name: 'openspec-continue-change',
@@ -88,10 +88,13 @@ ${PROJECT_ROOT_GUARD}
88
88
  - \`skipped\`/\`warning\`: present when the change declares skip_specs and this artifact must NOT be created - pick another artifact
89
89
  - **Create the artifact file**:
90
90
  - Read any completed dependency files for context - always re-read them from disk, even if you saw them earlier in the conversation (the user may have edited them)
91
- - If the \`instruction\` field delegates creation to a specific skill or command, invoke it to produce the artifact instead of writing the file yourself, then verify the artifact file exists at \`resolvedOutputPath\`
92
- - Otherwise use \`template\` as the structure - fill in its sections
93
- - Apply \`context\` and \`rules\` as constraints when writing - but do NOT copy them into the file
94
- - Write to the \`resolvedOutputPath\` specified in instructions. If it is a glob pattern, choose the concrete file path using the schema instruction and the change's context
91
+ - Choose the output path before creating anything. If the \`resolvedOutputPath\` specified in instructions is a glob pattern, choose a concrete path inside \`changeRoot\` using the schema instruction and the change's context, and verify it remains inside \`changeRoot\` after resolving symlinks, both in its parent directories and in the file itself if it already exists; if it does not, stop and tell the user. Otherwise the output path is \`resolvedOutputPath\`
92
+ - If the \`instruction\` field delegates creation to a specific skill or command, invoke it only if it can write the artifact to that output path; otherwise stop and tell the user
93
+ - Otherwise, write it yourself:
94
+ - Use \`template\` as the structure - fill in its sections
95
+ - Apply \`context\` and \`rules\` as constraints when writing - but do NOT copy them into the file
96
+ - Write the file to that output path
97
+ - Either way, verify the artifact file exists at that output path
95
98
  - Show what was created and what's now unlocked
96
99
  - STOP after creating ONE artifact
97
100
 
@@ -119,7 +122,7 @@ After each invocation, show:
119
122
 
120
123
  The artifact types and their purpose depend on the schema. The \`instruction\` field from the instructions output is the authoritative guidance for each artifact - follow it even when the artifact has a familiar name (proposal.md, tasks.md, etc.), since custom schemas may define different content or a different process for the same file names.
121
124
 
122
- If the \`instruction\` field directs you to use a specific skill or command to create the artifact, invoke it instead of writing the artifact directly.
125
+ If the \`instruction\` field directs you to use a specific skill or command to create the artifact, invoke it instead of writing the artifact directly - but only once you have chosen and checked the output path, and only if it can write there.
123
126
 
124
127
  **Guardrails**
125
128
  - Create ONE artifact per invocation
@@ -210,10 +213,13 @@ ${PROJECT_ROOT_GUARD}
210
213
  - \`skipped\`/\`warning\`: present when the change declares skip_specs and this artifact must NOT be created - pick another artifact
211
214
  - **Create the artifact file**:
212
215
  - Read any completed dependency files for context - always re-read them from disk, even if you saw them earlier in the conversation (the user may have edited them)
213
- - If the \`instruction\` field delegates creation to a specific skill or command, invoke it to produce the artifact instead of writing the file yourself, then verify the artifact file exists at \`resolvedOutputPath\`
214
- - Otherwise use \`template\` as the structure - fill in its sections
215
- - Apply \`context\` and \`rules\` as constraints when writing - but do NOT copy them into the file
216
- - Write to the \`resolvedOutputPath\` specified in instructions. If it is a glob pattern, choose the concrete file path using the schema instruction and the change's context
216
+ - Choose the output path before creating anything. If the \`resolvedOutputPath\` specified in instructions is a glob pattern, choose a concrete path inside \`changeRoot\` using the schema instruction and the change's context, and verify it remains inside \`changeRoot\` after resolving symlinks, both in its parent directories and in the file itself if it already exists; if it does not, stop and tell the user. Otherwise the output path is \`resolvedOutputPath\`
217
+ - If the \`instruction\` field delegates creation to a specific skill or command, invoke it only if it can write the artifact to that output path; otherwise stop and tell the user
218
+ - Otherwise, write it yourself:
219
+ - Use \`template\` as the structure - fill in its sections
220
+ - Apply \`context\` and \`rules\` as constraints when writing - but do NOT copy them into the file
221
+ - Write the file to that output path
222
+ - Either way, verify the artifact file exists at that output path
217
223
  - Show what was created and what's now unlocked
218
224
  - STOP after creating ONE artifact
219
225
 
@@ -241,7 +247,7 @@ After each invocation, show:
241
247
 
242
248
  The artifact types and their purpose depend on the schema. The \`instruction\` field from the instructions output is the authoritative guidance for each artifact - follow it even when the artifact has a familiar name (proposal.md, tasks.md, etc.), since custom schemas may define different content or a different process for the same file names.
243
249
 
244
- If the \`instruction\` field directs you to use a specific skill or command to create the artifact, invoke it instead of writing the artifact directly.
250
+ If the \`instruction\` field directs you to use a specific skill or command to create the artifact, invoke it instead of writing the artifact directly - but only once you have chosen and checked the output path, and only if it can write there.
245
251
 
246
252
  **Guardrails**
247
253
  - Create ONE artifact per invocation
@@ -156,7 +156,7 @@ If the user asks you to capture the exploration as a new change, that request is
156
156
 
157
157
  1. Run \`openspec new change "<name>"\` (with \`--store <id>\` when applicable) before creating any artifacts. Never create a new change directory under \`openspec/changes/\` by hand; the CLI scaffold creates required metadata such as \`.openspec.yaml\`. Keep the selected \`--store <id>\` on every applicable follow-up \`status\` and \`instructions\` command.
158
158
  2. Run \`openspec status --change "<name>" --json\` (append the confirmed \`--store "<id>"\` only for a registered standalone store), then process the requested artifacts in dependency order. For each requested artifact that is \`ready\`, run \`openspec instructions "<artifact-id>" --change "<name>" --json\` (append the confirmed \`--store "<id>"\` only for a registered standalone store). Before creating a requested artifact, evaluate any condition in its own \`instruction\` against the explored change; record a deliberate skip instead when the condition does not apply. If a requested artifact is blocked by a direct prerequisite the user did not request, run \`openspec instructions "<prerequisite-id>" --change "<name>" --json\` (append the confirmed \`--store "<id>"\` only for a registered standalone store) for that prerequisite whether it is \`ready\` or \`blocked\`. If its own \`instruction\` states a condition, evaluate that condition against the explored change and record a deliberate skip only when the condition does not apply. If the condition applies, or the prerequisite is not conditional, treat it as a normal prerequisite and ask before expanding the capture. Do not create an unrequested prerequisite unless the user approves.
159
- 3. Follow the returned \`template\` and \`instruction\` fields. Read completed dependency files listed in \`dependencies\`, and apply \`context\` and \`rules\` as constraints without copying them into the artifact. If the instruction delegates creation to a specific skill or command, invoke it; otherwise write the artifact to \`resolvedOutputPath\`, using the instruction to choose a concrete path when it is a glob. Verify that the selected concrete output exists.
159
+ 3. Follow the returned \`template\` and \`instruction\` fields. Read completed dependency files listed in \`dependencies\`, and apply \`context\` and \`rules\` as constraints without copying them into the artifact. Choose the output path before creating anything: when \`resolvedOutputPath\` is a glob, use the instruction to choose a concrete path inside \`changeRoot\` and verify it remains inside \`changeRoot\` after resolving symlinks, both in its parent directories and in the file itself if it already exists; if it does not, stop and tell the user. Otherwise the output path is \`resolvedOutputPath\`. If the instruction delegates creation to a specific skill or command, invoke it only if it can write the artifact to that output path; otherwise stop and tell the user. Otherwise write the artifact to that output path. Verify that the selected concrete output exists at that output path.
160
160
  4. After creating each artifact, re-run \`openspec status --change "<name>" --json\` (append the confirmed \`--store "<id>"\` only for a registered standalone store) and continue until every requested artifact is \`done\`, \`skipped\`, or was deliberately skipped because its own \`instruction\` stated a condition that did not apply. Tell the user about a deliberate conditional skip, remember it, and do not reconsider it. Dependencies are enablers, not gates: if a requested artifact is still \`blocked\` only because you deliberately skipped a conditional prerequisite, run \`openspec instructions "<artifact-id>" --change "<name>" --json\` (append the confirmed \`--store "<id>"\` only for a registered standalone store) despite the blocked status, then create it using step 3 only when those recorded conditional skips are its sole missing dependencies. If a requested artifact is blocked by a prerequisite the user did not ask to capture and cannot be conditionally skipped, explain that dependency and ask before expanding the capture.
161
161
 
162
162
  Capture the artifact(s) the user requested without asking them to invoke another workflow command. If they asked only to start a change, stop after scaffolding and show its status. When the requested capture is done, stop there and name where the work continues: ${CAPTURE_PLANNING_HANDOFF}, and ${CAPTURE_APPLY_HANDOFF}. Capturing artifacts never starts implementing them.
@@ -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.md with these decisions"
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
 
@@ -489,7 +491,7 @@ If the user asks you to capture the exploration as a new change, that request is
489
491
 
490
492
  1. Run \`openspec new change "<name>"\` (with \`--store <id>\` when applicable) before creating any artifacts. Never create a new change directory under \`openspec/changes/\` by hand; the CLI scaffold creates required metadata such as \`.openspec.yaml\`. Keep the selected \`--store <id>\` on every applicable follow-up \`status\` and \`instructions\` command.
491
493
  2. Run \`openspec status --change "<name>" --json\` (append the confirmed \`--store "<id>"\` only for a registered standalone store), then process the requested artifacts in dependency order. For each requested artifact that is \`ready\`, run \`openspec instructions "<artifact-id>" --change "<name>" --json\` (append the confirmed \`--store "<id>"\` only for a registered standalone store). Before creating a requested artifact, evaluate any condition in its own \`instruction\` against the explored change; record a deliberate skip instead when the condition does not apply. If a requested artifact is blocked by a direct prerequisite the user did not request, run \`openspec instructions "<prerequisite-id>" --change "<name>" --json\` (append the confirmed \`--store "<id>"\` only for a registered standalone store) for that prerequisite whether it is \`ready\` or \`blocked\`. If its own \`instruction\` states a condition, evaluate that condition against the explored change and record a deliberate skip only when the condition does not apply. If the condition applies, or the prerequisite is not conditional, treat it as a normal prerequisite and ask before expanding the capture. Do not create an unrequested prerequisite unless the user approves.
492
- 3. Follow the returned \`template\` and \`instruction\` fields. Read completed dependency files listed in \`dependencies\`, and apply \`context\` and \`rules\` as constraints without copying them into the artifact. If the instruction delegates creation to a specific skill or command, invoke it; otherwise write the artifact to \`resolvedOutputPath\`, using the instruction to choose a concrete path when it is a glob. Verify that the selected concrete output exists.
494
+ 3. Follow the returned \`template\` and \`instruction\` fields. Read completed dependency files listed in \`dependencies\`, and apply \`context\` and \`rules\` as constraints without copying them into the artifact. Choose the output path before creating anything: when \`resolvedOutputPath\` is a glob, use the instruction to choose a concrete path inside \`changeRoot\` and verify it remains inside \`changeRoot\` after resolving symlinks, both in its parent directories and in the file itself if it already exists; if it does not, stop and tell the user. Otherwise the output path is \`resolvedOutputPath\`. If the instruction delegates creation to a specific skill or command, invoke it only if it can write the artifact to that output path; otherwise stop and tell the user. Otherwise write the artifact to that output path. Verify that the selected concrete output exists at that output path.
493
495
  4. After creating each artifact, re-run \`openspec status --change "<name>" --json\` (append the confirmed \`--store "<id>"\` only for a registered standalone store) and continue until every requested artifact is \`done\`, \`skipped\`, or was deliberately skipped because its own \`instruction\` stated a condition that did not apply. Tell the user about a deliberate conditional skip, remember it, and do not reconsider it. Dependencies are enablers, not gates: if a requested artifact is still \`blocked\` only because you deliberately skipped a conditional prerequisite, run \`openspec instructions "<artifact-id>" --change "<name>" --json\` (append the confirmed \`--store "<id>"\` only for a registered standalone store) despite the blocked status, then create it using step 3 only when those recorded conditional skips are its sole missing dependencies. If a requested artifact is blocked by a prerequisite the user did not ask to capture and cannot be conditionally skipped, explain that dependency and ask before expanding the capture.
494
496
 
495
497
  Capture the artifact(s) the user requested without asking them to invoke another workflow command. If they asked only to start a change, stop after scaffolding and show its status. When the requested capture is done, stop there and name where the work continues: ${CAPTURE_PLANNING_HANDOFF}, and ${CAPTURE_APPLY_HANDOFF}. Capturing artifacts never starts implementing them.
@@ -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.md with these decisions"
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
 
@@ -73,8 +73,9 @@ ${PROJECT_ROOT_GUARD}
73
73
  - Identify the target project from the request and project context; the planning home may be separate from the code. If the target is unclear, ask. For greenfield or non-code changes, inspect the available structure and relevant documents. If source is unavailable, state the limitation and ask when it materially affects the plan.
74
74
  - Ground scope, approach, and tasks in what you find. Distinguish observed behavior from assumptions and proposed additions; surface conflicts with existing specs instead of silently deciding which is correct.
75
75
  - Do this discovery now, rather than leaving generic "explore the codebase" or "make a plan" tasks for implementation. Keep any necessary follow-up investigation specific to an unresolved question.
76
- - If the \`instruction\` field delegates creation to a specific skill or command, invoke it to produce the artifact instead of writing the file yourself, then verify the artifact file exists at \`resolvedOutputPath\`
77
- - Otherwise create the artifact file using \`template\` as the structure and write it to \`resolvedOutputPath\`. If \`resolvedOutputPath\` is a glob, follow \`instruction\` to choose the concrete file path
76
+ - Choose the output path before creating anything. If \`resolvedOutputPath\` is a glob, follow \`instruction\` to choose a concrete path inside \`changeRoot\` that matches the glob, and verify it remains inside \`changeRoot\` after resolving symlinks, both in its parent directories and in the file itself if it already exists; if it does not, stop and tell the user. Otherwise the output path is \`resolvedOutputPath\`
77
+ - If the \`instruction\` field delegates creation to a specific skill or command, invoke it only if it can write the artifact to that output path; otherwise stop and tell the user. Then verify the artifact file exists at that output path
78
+ - Otherwise create the artifact file using \`template\` as the structure and write it to that output path
78
79
  - Apply \`context\` and \`rules\` as constraints - but do NOT copy them into the file
79
80
  - Show brief progress: "✓ Created <artifact-id>"
80
81
 
@@ -108,7 +109,7 @@ After completing all artifacts, summarize:
108
109
  **Artifact Creation Guidelines**
109
110
 
110
111
  - Follow the \`instruction\` field from \`openspec instructions\` for each artifact type - it is the authoritative guidance, even for familiar artifact names
111
- - If the \`instruction\` field directs you to use a specific skill or command to create the artifact, invoke it instead of writing the artifact directly
112
+ - If the \`instruction\` field directs you to use a specific skill or command to create the artifact, invoke it instead of writing the artifact directly - but only once you have chosen and checked the output path, and only if it can write there
112
113
  - The schema defines what each artifact should contain - follow it
113
114
  - Read dependency artifacts for context before creating new ones
114
115
  - Use \`template\` as the structure for your output file - fill in its sections
@@ -191,8 +192,9 @@ ${PROJECT_ROOT_GUARD}
191
192
  - Identify the target project from the request and project context; the planning home may be separate from the code. If the target is unclear, ask. For greenfield or non-code changes, inspect the available structure and relevant documents. If source is unavailable, state the limitation and ask when it materially affects the plan.
192
193
  - Ground scope, approach, and tasks in what you find. Distinguish observed behavior from assumptions and proposed additions; surface conflicts with existing specs instead of silently deciding which is correct.
193
194
  - Do this discovery now, rather than leaving generic "explore the codebase" or "make a plan" tasks for implementation. Keep any necessary follow-up investigation specific to an unresolved question.
194
- - If the \`instruction\` field delegates creation to a specific skill or command, invoke it to produce the artifact instead of writing the file yourself, then verify the artifact file exists at \`resolvedOutputPath\`
195
- - Otherwise create the artifact file using \`template\` as the structure and write it to \`resolvedOutputPath\`. If \`resolvedOutputPath\` is a glob, follow \`instruction\` to choose the concrete file path
195
+ - Choose the output path before creating anything. If \`resolvedOutputPath\` is a glob, follow \`instruction\` to choose a concrete path inside \`changeRoot\` that matches the glob, and verify it remains inside \`changeRoot\` after resolving symlinks, both in its parent directories and in the file itself if it already exists; if it does not, stop and tell the user. Otherwise the output path is \`resolvedOutputPath\`
196
+ - If the \`instruction\` field delegates creation to a specific skill or command, invoke it only if it can write the artifact to that output path; otherwise stop and tell the user. Then verify the artifact file exists at that output path
197
+ - Otherwise create the artifact file using \`template\` as the structure and write it to that output path
196
198
  - Apply \`context\` and \`rules\` as constraints - but do NOT copy them into the file
197
199
  - Show brief progress: "✓ Created <artifact-id>"
198
200
 
@@ -226,7 +228,7 @@ After completing all artifacts, summarize:
226
228
  **Artifact Creation Guidelines**
227
229
 
228
230
  - Follow the \`instruction\` field from \`openspec instructions\` for each artifact type - it is the authoritative guidance, even for familiar artifact names
229
- - If the \`instruction\` field directs you to use a specific skill or command to create the artifact, invoke it instead of writing the artifact directly
231
+ - If the \`instruction\` field directs you to use a specific skill or command to create the artifact, invoke it instead of writing the artifact directly - but only once you have chosen and checked the output path, and only if it can write there
230
232
  - The schema defines what each artifact should contain - follow it
231
233
  - Read dependency artifacts for context before creating new ones
232
234
  - Use \`template\` as the structure for your output file - fill in its sections
@@ -119,8 +119,9 @@ ${PROJECT_ROOT_GUARD}
119
119
  - Identify the target project from the request and project context; the planning home may be separate from the code. If the target is unclear, ask. For greenfield or non-code changes, inspect the available structure and relevant documents. If source is unavailable, state the limitation and ask when it materially affects the plan.
120
120
  - Ground scope, approach, and tasks in what you find. Distinguish observed behavior from assumptions and proposed additions; surface conflicts with existing specs instead of silently deciding which is correct.
121
121
  - Do this discovery now, rather than leaving generic "explore the codebase" or "make a plan" tasks for implementation. Keep any necessary follow-up investigation specific to an unresolved question.
122
- - If the \`instruction\` field delegates creation to a specific skill or command, invoke it to produce the artifact instead of writing the file yourself, then verify the artifact file exists at \`resolvedOutputPath\`
123
- - Otherwise create the artifact file using \`template\` as the structure and write it to \`resolvedOutputPath\`. If \`resolvedOutputPath\` is a glob, follow \`instruction\` to choose the concrete file path
122
+ - Choose the output path before creating anything. If \`resolvedOutputPath\` is a glob, follow \`instruction\` to choose a concrete path inside \`changeRoot\` that matches the glob, and verify it remains inside \`changeRoot\` after resolving symlinks, both in its parent directories and in the file itself if it already exists; if it does not, stop and tell the user. Otherwise the output path is \`resolvedOutputPath\`
123
+ - If the \`instruction\` field delegates creation to a specific skill or command, invoke it only if it can write the artifact to that output path; otherwise stop and tell the user. Then verify the artifact file exists at that output path
124
+ - Otherwise create the artifact file using \`template\` as the structure and write it to that output path
124
125
  - Apply \`context\` and \`rules\` as constraints - but do NOT copy them into the file
125
126
  - Show brief progress: "Created <artifact-id>"
126
127
 
@@ -154,7 +155,7 @@ After completing all artifacts, summarize:
154
155
  **Artifact Creation Guidelines**
155
156
 
156
157
  - Follow the \`instruction\` field from \`openspec instructions\` for each artifact type - it is the authoritative guidance, even for familiar artifact names
157
- - If the \`instruction\` field directs you to use a specific skill or command to create the artifact, invoke it instead of writing the artifact directly
158
+ - If the \`instruction\` field directs you to use a specific skill or command to create the artifact, invoke it instead of writing the artifact directly - but only once you have chosen and checked the output path, and only if it can write there
158
159
  - The schema defines what each artifact should contain - follow it
159
160
  - Read dependency artifacts for context before creating new ones
160
161
  - Use \`template\` as the structure for your output file - fill in its sections
@@ -283,8 +284,9 @@ ${PROJECT_ROOT_GUARD}
283
284
  - Identify the target project from the request and project context; the planning home may be separate from the code. If the target is unclear, ask. For greenfield or non-code changes, inspect the available structure and relevant documents. If source is unavailable, state the limitation and ask when it materially affects the plan.
284
285
  - Ground scope, approach, and tasks in what you find. Distinguish observed behavior from assumptions and proposed additions; surface conflicts with existing specs instead of silently deciding which is correct.
285
286
  - Do this discovery now, rather than leaving generic "explore the codebase" or "make a plan" tasks for implementation. Keep any necessary follow-up investigation specific to an unresolved question.
286
- - If the \`instruction\` field delegates creation to a specific skill or command, invoke it to produce the artifact instead of writing the file yourself, then verify the artifact file exists at \`resolvedOutputPath\`
287
- - Otherwise create the artifact file using \`template\` as the structure and write it to \`resolvedOutputPath\`. If \`resolvedOutputPath\` is a glob, follow \`instruction\` to choose the concrete file path
287
+ - Choose the output path before creating anything. If \`resolvedOutputPath\` is a glob, follow \`instruction\` to choose a concrete path inside \`changeRoot\` that matches the glob, and verify it remains inside \`changeRoot\` after resolving symlinks, both in its parent directories and in the file itself if it already exists; if it does not, stop and tell the user. Otherwise the output path is \`resolvedOutputPath\`
288
+ - If the \`instruction\` field delegates creation to a specific skill or command, invoke it only if it can write the artifact to that output path; otherwise stop and tell the user. Then verify the artifact file exists at that output path
289
+ - Otherwise create the artifact file using \`template\` as the structure and write it to that output path
288
290
  - Apply \`context\` and \`rules\` as constraints - but do NOT copy them into the file
289
291
  - Show brief progress: "Created <artifact-id>"
290
292
 
@@ -318,7 +320,7 @@ After completing all artifacts, summarize:
318
320
  **Artifact Creation Guidelines**
319
321
 
320
322
  - Follow the \`instruction\` field from \`openspec instructions\` for each artifact type - it is the authoritative guidance, even for familiar artifact names
321
- - If the \`instruction\` field directs you to use a specific skill or command to create the artifact, invoke it instead of writing the artifact directly
323
+ - If the \`instruction\` field directs you to use a specific skill or command to create the artifact, invoke it instead of writing the artifact directly - but only once you have chosen and checked the output path, and only if it can write there
322
324
  - The schema defines what each artifact should contain - follow it
323
325
  - Read dependency artifacts for context before creating new ones
324
326
  - Use \`template\` as the structure for your output file - fill in its sections
@@ -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\`; never write to a glob \`resolvedOutputPath\`.
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\`; never write to a glob \`resolvedOutputPath\`.
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}.`
@@ -87,8 +87,11 @@ ${PROJECT_ROOT_GUARD}
87
87
  - Add CRITICAL issue: "Requirement not found: <requirement name>"
88
88
  - Recommendation: "Implement requirement X: <description>"
89
89
  - For each REMOVED requirement, the change asks for the behavior to be gone, so invert the check:
90
- - Search codebase for the removed behavior. Matches in \`openspec/\` artifacts or docs, or in code that serves only the Migration note or an ADDED requirement, are not evidence by themselves. Report any code path that still delivers the removed behavior, including one shared with an ADDED requirement.
91
- - Finding no implementation is the expected result. Never report a REMOVED requirement as "Requirement not found" or recommend implementing it.
90
+ - A REMOVED block carries only the requirement name and any \`**Reason**\`/\`**Migration**\` notes, so the delta does not describe the behavior itself. Before searching, read the baseline requirement in the main spec at \`<planningHome.root>/openspec/specs/<capability-path>/spec.md\`, using the same capability path as the delta spec. Match it by normalized name, scanning that file's requirement headings rather than string-matching the delta's exact title. Its body and scenarios are the evidence for the behavior that must be gone; never search from the requirement name alone.
91
+ - If that file holds no such requirement under this or a near-equivalent name - for example because an earlier sync already applied the removal - recover the baseline from the same file's history, in the planning repository rather than the current directory: \`git -C "<planningHome.root>" log -p -- openspec/specs/<capability-path>/spec.md\`, or \`git -C "<planningHome.root>" show <commit>^:./openspec/specs/<capability-path>/spec.md\` once you have identified the commit that applied the removal. Empty output means the pathspec matched nothing, not that there is no baseline: \`git log\` reports an unmatched path as success, so re-check the path before drawing any conclusion from silence.
92
+ - A baseline that adds nothing to its own title - no body beyond a restatement of the name, no scenarios - is unusable evidence, the same as a missing one. If no usable baseline is available in either place, mark **Spec Coverage** as not verified for that entry with the reason and stop there: do not search, and never count an unsearched removal as passing.
93
+ - Search codebase for that behavior. Matches in \`openspec/\` artifacts or docs, or in code that serves only the Migration note or an ADDED requirement, are not evidence by themselves. Report any code path that still delivers the removed behavior, including one shared with an ADDED requirement.
94
+ - Finding no implementation is the expected result, but only once the baseline behavior was the thing you searched for. Never report a REMOVED requirement as "Requirement not found" or recommend implementing it.
92
95
  - If the behavior is still present:
93
96
  - Add CRITICAL issue: "Removed requirement still implemented: <requirement name>"
94
97
  - Recommendation: "Remove the remaining implementation at <file>:<lines>, following the requirement's Migration note if it has one"
@@ -104,7 +107,7 @@ ${PROJECT_ROOT_GUARD}
104
107
 
105
108
  6. **Verify Correctness**
106
109
 
107
- If the delta specs are readable and contain at least one REMOVED or RENAMED requirement but no ADDED or MODIFIED requirements (the change only removes or renames requirements), report **Requirement Implementation Mapping** and **Scenario Coverage** as **Not applicable**. The REMOVED and RENAMED checks under Spec Coverage are the evidence for such a change (each RENAMED entry is checked there against its baseline behavior), so do not mark these two checks as not verified. A delta spec with no parseable requirements at all is unusable evidence, not a removal-only change: mark these checks as not verified.
110
+ If the delta specs are readable and contain at least one REMOVED or RENAMED requirement but no ADDED or MODIFIED requirements (the change only removes or renames requirements), report **Requirement Implementation Mapping** and **Scenario Coverage** as **Not applicable**. The REMOVED and RENAMED checks under Spec Coverage are the evidence for such a change (each REMOVED and RENAMED entry with a usable baseline is checked there against its baseline behavior, and an entry already marked not verified there stays not verified), so do not mark these two checks as not verified. A delta spec with no parseable requirements at all is unusable evidence, not a removal-only change: mark these checks as not verified.
108
111
 
109
112
  **Requirement Implementation Mapping**:
110
113
  - For each ADDED or MODIFIED requirement from delta specs (REMOVED entries, and RENAMED entries without a MODIFIED block, were settled under Spec Coverage):
@@ -158,7 +161,7 @@ ${PROJECT_ROOT_GUARD}
158
161
  | Coherence | Followed/Issues |
159
162
  \`\`\`
160
163
 
161
- In each Status cell, report the results of checks that ran and \`Not verified (<reason>)\` for every skipped check. If all checks in a dimension were skipped, start the cell with \`Not verified\`. Never score a skipped check as passing. Treat every not verified or partially verified check as skipped in the final assessment. Count only ADDED and MODIFIED requirements in N, and report REMOVED and RENAMED requirements separately (for example, "1 removal confirmed, 1 rename verified"). For a change that only removes or renames requirements, the Correctness cell reads \`Not applicable (no ADDED or MODIFIED requirements)\`.
164
+ In each Status cell, report the results of checks that ran and \`Not verified (<reason>)\` for every skipped check. If all checks in a dimension were skipped, start the cell with \`Not verified\`. Never score a skipped check as passing. Treat every not verified or partially verified check as skipped in the final assessment. Count only ADDED and MODIFIED requirements in N, and report REMOVED and RENAMED requirements separately (for example, "1 removal confirmed, 1 rename verified", or "1 removal not verified (baseline unavailable)"). For a change that only removes or renames requirements, the Correctness cell reads \`Not applicable (no ADDED or MODIFIED requirements)\`.
162
165
 
163
166
  **Issues by Priority**:
164
167
 
@@ -296,8 +299,11 @@ ${PROJECT_ROOT_GUARD}
296
299
  - Add CRITICAL issue: "Requirement not found: <requirement name>"
297
300
  - Recommendation: "Implement requirement X: <description>"
298
301
  - For each REMOVED requirement, the change asks for the behavior to be gone, so invert the check:
299
- - Search codebase for the removed behavior. Matches in \`openspec/\` artifacts or docs, or in code that serves only the Migration note or an ADDED requirement, are not evidence by themselves. Report any code path that still delivers the removed behavior, including one shared with an ADDED requirement.
300
- - Finding no implementation is the expected result. Never report a REMOVED requirement as "Requirement not found" or recommend implementing it.
302
+ - A REMOVED block carries only the requirement name and any \`**Reason**\`/\`**Migration**\` notes, so the delta does not describe the behavior itself. Before searching, read the baseline requirement in the main spec at \`<planningHome.root>/openspec/specs/<capability-path>/spec.md\`, using the same capability path as the delta spec. Match it by normalized name, scanning that file's requirement headings rather than string-matching the delta's exact title. Its body and scenarios are the evidence for the behavior that must be gone; never search from the requirement name alone.
303
+ - If that file holds no such requirement under this or a near-equivalent name - for example because an earlier sync already applied the removal - recover the baseline from the same file's history, in the planning repository rather than the current directory: \`git -C "<planningHome.root>" log -p -- openspec/specs/<capability-path>/spec.md\`, or \`git -C "<planningHome.root>" show <commit>^:./openspec/specs/<capability-path>/spec.md\` once you have identified the commit that applied the removal. Empty output means the pathspec matched nothing, not that there is no baseline: \`git log\` reports an unmatched path as success, so re-check the path before drawing any conclusion from silence.
304
+ - A baseline that adds nothing to its own title - no body beyond a restatement of the name, no scenarios - is unusable evidence, the same as a missing one. If no usable baseline is available in either place, mark **Spec Coverage** as not verified for that entry with the reason and stop there: do not search, and never count an unsearched removal as passing.
305
+ - Search codebase for that behavior. Matches in \`openspec/\` artifacts or docs, or in code that serves only the Migration note or an ADDED requirement, are not evidence by themselves. Report any code path that still delivers the removed behavior, including one shared with an ADDED requirement.
306
+ - Finding no implementation is the expected result, but only once the baseline behavior was the thing you searched for. Never report a REMOVED requirement as "Requirement not found" or recommend implementing it.
301
307
  - If the behavior is still present:
302
308
  - Add CRITICAL issue: "Removed requirement still implemented: <requirement name>"
303
309
  - Recommendation: "Remove the remaining implementation at <file>:<lines>, following the requirement's Migration note if it has one"
@@ -313,7 +319,7 @@ ${PROJECT_ROOT_GUARD}
313
319
 
314
320
  6. **Verify Correctness**
315
321
 
316
- If the delta specs are readable and contain at least one REMOVED or RENAMED requirement but no ADDED or MODIFIED requirements (the change only removes or renames requirements), report **Requirement Implementation Mapping** and **Scenario Coverage** as **Not applicable**. The REMOVED and RENAMED checks under Spec Coverage are the evidence for such a change (each RENAMED entry is checked there against its baseline behavior), so do not mark these two checks as not verified. A delta spec with no parseable requirements at all is unusable evidence, not a removal-only change: mark these checks as not verified.
322
+ If the delta specs are readable and contain at least one REMOVED or RENAMED requirement but no ADDED or MODIFIED requirements (the change only removes or renames requirements), report **Requirement Implementation Mapping** and **Scenario Coverage** as **Not applicable**. The REMOVED and RENAMED checks under Spec Coverage are the evidence for such a change (each REMOVED and RENAMED entry with a usable baseline is checked there against its baseline behavior, and an entry already marked not verified there stays not verified), so do not mark these two checks as not verified. A delta spec with no parseable requirements at all is unusable evidence, not a removal-only change: mark these checks as not verified.
317
323
 
318
324
  **Requirement Implementation Mapping**:
319
325
  - For each ADDED or MODIFIED requirement from delta specs (REMOVED entries, and RENAMED entries without a MODIFIED block, were settled under Spec Coverage):
@@ -367,7 +373,7 @@ ${PROJECT_ROOT_GUARD}
367
373
  | Coherence | Followed/Issues |
368
374
  \`\`\`
369
375
 
370
- In each Status cell, report the results of checks that ran and \`Not verified (<reason>)\` for every skipped check. If all checks in a dimension were skipped, start the cell with \`Not verified\`. Never score a skipped check as passing. Treat every not verified or partially verified check as skipped in the final assessment. Count only ADDED and MODIFIED requirements in N, and report REMOVED and RENAMED requirements separately (for example, "1 removal confirmed, 1 rename verified"). For a change that only removes or renames requirements, the Correctness cell reads \`Not applicable (no ADDED or MODIFIED requirements)\`.
376
+ In each Status cell, report the results of checks that ran and \`Not verified (<reason>)\` for every skipped check. If all checks in a dimension were skipped, start the cell with \`Not verified\`. Never score a skipped check as passing. Treat every not verified or partially verified check as skipped in the final assessment. Count only ADDED and MODIFIED requirements in N, and report REMOVED and RENAMED requirements separately (for example, "1 removal confirmed, 1 rename verified", or "1 removal not verified (baseline unavailable)"). For a change that only removes or renames requirements, the Correctness cell reads \`Not applicable (no ADDED or MODIFIED requirements)\`.
371
377
 
372
378
  **Issues by Priority**:
373
379
 
@@ -5,10 +5,11 @@ import { MarkdownParser } from '../parsers/markdown-parser.js';
5
5
  import { ChangeParser } from '../parsers/change-parser.js';
6
6
  import { findSpecUpdates, buildUpdatedSpec } from '../specs-apply.js';
7
7
  import { MIN_PURPOSE_LENGTH, MAX_REQUIREMENT_TEXT_LENGTH, VALIDATION_MESSAGES } from './constants.js';
8
- import { parseDeltaSpec, foldRequirementName, normalizeRequirementName, extractRequirementsSection, diffScenarioNames, describeScenarioBalance, } from '../parsers/requirement-blocks.js';
8
+ import { parseDeltaSpec, foldRequirementName, normalizeRequirementName, extractRequirementsSection, diffScenarioNames, describeScenarioBalance, describeUnappliedContent, } from '../parsers/requirement-blocks.js';
9
9
  import { extractRequirementBody as extractRequirementBodyShared, containsShallOrMust as containsShallOrMustShared, countScenarios as countScenariosShared, countEmptyScenarios, } from '../parsers/requirement-text.js';
10
10
  import { findMainSpecStructureIssues } from '../parsers/spec-structure.js';
11
11
  import { FileSystemUtils } from '../../utils/file-system.js';
12
+ import { assertDiscoveredSpecFile } from '../../utils/spec-path-containment.js';
12
13
  import { discoverSpecFiles, findUnreadDeltaFiles, hasAnyFileUnder } from '../../utils/spec-discovery.js';
13
14
  import { METADATA_FILENAME, formatUnknownChangeMetadataKeysMessage, readSkipSpecsMarker, readUnknownChangeMetadataKeys, resolveSchemaForChange, } from '../../utils/change-metadata.js';
14
15
  import { resolveTaskFilesForChange } from '../../utils/task-progress.js';
@@ -203,6 +204,16 @@ export class Validator {
203
204
  message: `Requirement "${orphan.name}" is ${where}, which is not a delta section, so it is ignored. Move it under "## ADDED Requirements", "## MODIFIED Requirements", "## REMOVED Requirements", or "## RENAMED Requirements".`,
204
205
  });
205
206
  }
207
+ // Archive carries requirement blocks only, by design. Warn so text
208
+ // written elsewhere is moved rather than lost without notice.
209
+ for (const lost of plan.unappliedContent) {
210
+ issues.push({
211
+ level: 'WARNING',
212
+ path: entryPath,
213
+ line: lost.line,
214
+ message: `${describeUnappliedContent(lost, true)} is not carried into the main spec by archive. Move it into a requirement, or into proposal.md or design.md.`,
215
+ });
216
+ }
206
217
  const sectionNames = [];
207
218
  if (plan.sectionPresence.added)
208
219
  sectionNames.push('## ADDED Requirements');
@@ -304,8 +315,7 @@ export class Validator {
304
315
  // authoring time instead of days later at archive time (#1477).
305
316
  if (options.mainSpecsDir && plan.modified.length > 0) {
306
317
  const mainSpecFile = path.join(options.mainSpecsDir, ...specId.split('/'), 'spec.md');
307
- FileSystemUtils.assertPathWithin(path.dirname(mainSpecFile), mainSpecFile);
308
- issues.push(...(await this.findScenarioLossIssues(plan.modified, plan.renamed, mainSpecFile, entryPath, path.dirname(mainSpecFile))));
318
+ issues.push(...(await this.findScenarioLossIssues(plan.modified, plan.renamed, mainSpecFile, entryPath, options.mainSpecsDir)));
309
319
  }
310
320
  // Validate REMOVED (names only)
311
321
  for (const name of plan.removed) {
@@ -596,7 +606,7 @@ export class Validator {
596
606
  */
597
607
  async findScenarioLossIssues(modified, renamed, mainSpecFile, entryPath, mainSpecRoot) {
598
608
  let mainContent;
599
- FileSystemUtils.assertPathWithin(mainSpecRoot, mainSpecFile);
609
+ assertDiscoveredSpecFile(mainSpecRoot, mainSpecFile);
600
610
  try {
601
611
  mainContent = await fs.readFile(mainSpecFile, 'utf-8');
602
612
  }