thoth-agents 0.3.9 → 0.3.11

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.
Files changed (40) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/agents/prompt-dialects.d.ts +3 -0
  3. package/dist/{chunk-MY2GIOOH.js → chunk-CB3EBA7H.js} +4 -1
  4. package/dist/{chunk-2TLT6WWY.js → chunk-GIUU6FNE.js} +340 -296
  5. package/dist/{chunk-XEEYNMPC.js → chunk-ZEAAWBF2.js} +28 -83
  6. package/dist/cli/index.js +3 -3
  7. package/dist/cli/tui/index.js +2 -2
  8. package/dist/config/constants.d.ts +3 -3
  9. package/dist/harness/adapters/claude-code.d.ts +8 -0
  10. package/dist/harness/adapters/codex-surfaces.d.ts +2 -2
  11. package/dist/harness/core/agent-pack.d.ts +38 -3
  12. package/dist/harness/core/sdd.d.ts +9 -8
  13. package/dist/index.js +2 -2
  14. package/package.json +1 -1
  15. package/plugin/.claude-plugin/.thoth-agents-plugin-assets.json +7 -7
  16. package/plugin/.claude-plugin/plugin.json +1 -1
  17. package/plugin/.codex-plugin/plugin.json +1 -1
  18. package/plugin/agents/deep.md +10 -31
  19. package/plugin/agents/designer.md +10 -31
  20. package/plugin/agents/explorer.md +10 -30
  21. package/plugin/agents/librarian.md +10 -30
  22. package/plugin/agents/oracle.md +10 -30
  23. package/plugin/agents/orchestrator.md +39 -31
  24. package/plugin/agents/quick.md +10 -31
  25. package/plugin/skills/thoth-archive/SKILL.md +7 -0
  26. package/plugin/skills/thoth-archive/scripts/archive.mjs +58 -84
  27. package/plugin/skills/thoth-sdd/SKILL.md +13 -1
  28. package/plugin/skills/thoth-sdd/references/phases/implement.md +33 -6
  29. package/plugin/skills/thoth-sdd/references/phases/specify.md +12 -1
  30. package/plugin/skills/thoth-sdd/scripts/durable-deltas.mjs +158 -0
  31. package/plugin/skills/thoth-sdd/scripts/validate.mjs +67 -28
  32. package/plugin/skills/thoth-sdd/templates/spec.md +9 -1
  33. package/skills/thoth-archive/SKILL.md +7 -0
  34. package/skills/thoth-archive/scripts/archive.mjs +58 -84
  35. package/skills/thoth-sdd/SKILL.md +13 -1
  36. package/skills/thoth-sdd/references/phases/implement.md +33 -6
  37. package/skills/thoth-sdd/references/phases/specify.md +12 -1
  38. package/skills/thoth-sdd/scripts/durable-deltas.mjs +158 -0
  39. package/skills/thoth-sdd/scripts/validate.mjs +67 -28
  40. package/skills/thoth-sdd/templates/spec.md +9 -1
@@ -10,8 +10,13 @@ import {
10
10
  writeFileSync,
11
11
  } from 'node:fs';
12
12
  import { basename, dirname, join, resolve } from 'node:path';
13
+ import {
14
+ CAPABILITY_PATTERN,
15
+ parseCanonicalSpec,
16
+ parseRequirementDelta as parseSharedRequirementDelta,
17
+ preflightRequirementDeltas,
18
+ } from '../../thoth-sdd/scripts/durable-deltas.mjs';
13
19
 
14
- const CAPABILITY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
15
20
  const TEST_FAULTS = new Set(
16
21
  (process.env.THOTH_ARCHIVE_TEST_FAULT ?? '')
17
22
  .split(',')
@@ -270,26 +275,8 @@ function assertArchivable(change, contract) {
270
275
  }
271
276
 
272
277
  function parseDeltaMetadata(metadata) {
273
- if (metadata === 'INTERNAL') return { operation: 'INTERNAL' };
274
-
275
- const change = /^(ADDED|MODIFIED|REMOVED)\s+(\S+)$/.exec(metadata);
276
- if (change && CAPABILITY_PATTERN.test(change[2])) {
277
- return { operation: change[1], capability: change[2] };
278
- }
279
-
280
- const rename = /^RENAMED\s+(\S+)\s+FROM\s+(.+)$/.exec(metadata);
281
- if (
282
- rename &&
283
- CAPABILITY_PATTERN.test(rename[1]) &&
284
- rename[2].trim().length > 0
285
- ) {
286
- return {
287
- operation: 'RENAMED',
288
- capability: rename[1],
289
- previousTitle: rename[2].trim(),
290
- };
291
- }
292
-
278
+ const delta = parseSharedRequirementDelta(metadata);
279
+ if (delta) return delta;
293
280
  throw new Error(`Invalid requirement delta metadata: [${metadata}]`);
294
281
  }
295
282
 
@@ -313,16 +300,36 @@ function parseStoryScenarios(spec) {
313
300
  const covers =
314
301
  /^\*\*Covers\*\*:\s*(.+)$/im.exec(story)?.[1].match(/\bFR-\d{3}\b/g) ??
315
302
  [];
316
- const scenarios = [
317
- ...story.matchAll(
318
- /^\d+\.\s+\*\*Given\*\*\s+(.+?),\s+\*\*When\*\*\s+(.+?),\s+\*\*Then\*\*\s+(.+?)\.?\s*$/gim,
319
- ),
320
- ].map((scenario, scenarioIndex) => ({
321
- title: `US${storyMatch[1]} - ${storyMatch[2].trim()} ${scenarioIndex + 1}`,
322
- given: scenario[1].trim(),
323
- when: scenario[2].trim(),
324
- result: scenario[3].trim(),
325
- }));
303
+ const acceptanceMarker = /^\*\*Acceptance scenarios\*\*:\s*$/im.exec(story);
304
+ const acceptance = acceptanceMarker
305
+ ? story.slice((acceptanceMarker.index ?? 0) + acceptanceMarker[0].length)
306
+ : '';
307
+ const scenarioStarts = [
308
+ ...acceptance.matchAll(/^\d+\.\s+\*\*Given\*\*/gim),
309
+ ];
310
+ const scenarios = scenarioStarts.map((scenarioStart, scenarioIndex) => {
311
+ const start = scenarioStart.index ?? 0;
312
+ const end = scenarioStarts[scenarioIndex + 1]?.index ?? acceptance.length;
313
+ const normalized = acceptance
314
+ .slice(start, end)
315
+ .replace(/\s+/g, ' ')
316
+ .trim();
317
+ const scenario =
318
+ /^\d+\.\s+\*\*Given\*\*\s+(.+?),\s+\*\*When\*\*\s+(.+?),\s+\*\*Then\*\*\s+(.+?)\.?\s*$/i.exec(
319
+ normalized,
320
+ );
321
+ if (!scenario) {
322
+ throw new Error(
323
+ `US${storyMatch[1]} acceptance scenario ${scenarioIndex + 1} must use Given, When, and Then`,
324
+ );
325
+ }
326
+ return {
327
+ title: `US${storyMatch[1]} - ${storyMatch[2].trim()} ${scenarioIndex + 1}`,
328
+ given: scenario[1].trim(),
329
+ when: scenario[2].trim(),
330
+ result: scenario[3].trim(),
331
+ };
332
+ });
326
333
 
327
334
  for (const requirementId of covers) {
328
335
  const current = scenariosByRequirement.get(requirementId) ?? [];
@@ -418,27 +425,6 @@ function parseChangeSpec(spec) {
418
425
  };
419
426
  }
420
427
 
421
- function parseCanonicalSpec(content) {
422
- const headings = [...content.matchAll(/^### Requirement:\s+(.+?)\s*$/gm)];
423
- const requirements = new Map();
424
- const prefix =
425
- headings.length === 0
426
- ? content.trimEnd()
427
- : content.slice(0, headings[0].index).trimEnd();
428
-
429
- for (const [index, heading] of headings.entries()) {
430
- const start = heading.index ?? 0;
431
- const end = headings[index + 1]?.index ?? content.length;
432
- const title = heading[1].trim();
433
- if (requirements.has(title)) {
434
- throw new Error(`Canonical specification repeats requirement: ${title}`);
435
- }
436
- requirements.set(title, content.slice(start, end).trim());
437
- }
438
-
439
- return { prefix, requirements };
440
- }
441
-
442
428
  function newCanonicalSpec(capability) {
443
429
  const title = capability
444
430
  .split('-')
@@ -480,41 +466,34 @@ function planCanonicalUpdates(specRoot, deltas) {
480
466
  for (const [capability, capabilityDeltas] of byCapability) {
481
467
  const path = join(specRoot, capability, 'spec.md');
482
468
  const present = existsSync(path);
483
- const canonical = present
484
- ? parseCanonicalSpec(readFileSync(path, 'utf8'))
485
- : newCanonicalSpec(capability);
469
+ let canonical;
470
+ try {
471
+ canonical = present
472
+ ? parseCanonicalSpec(readFileSync(path, 'utf8'))
473
+ : newCanonicalSpec(capability);
474
+ } catch (error) {
475
+ throw new Error(
476
+ `SDD-SPEC-DELTA-BASELINE: ${capability} canonical baseline is invalid: ${errorMessage(error)}`,
477
+ );
478
+ }
479
+ const preflight = preflightRequirementDeltas({
480
+ capability,
481
+ present,
482
+ requirements: canonical.requirements,
483
+ deltas: capabilityDeltas,
484
+ });
485
+ if (preflight.errors.length > 0) {
486
+ const [first] = preflight.errors;
487
+ throw new Error(`${first.code}: ${first.message}`);
488
+ }
486
489
 
487
490
  for (const delta of capabilityDeltas) {
488
491
  if (delta.operation === 'ADDED') {
489
- if (canonical.requirements.has(delta.title)) {
490
- throw new Error(
491
- `${capability} already contains requirement: ${delta.title}`,
492
- );
493
- }
494
492
  canonical.requirements.set(delta.title, renderRequirement(delta));
495
493
  continue;
496
494
  }
497
495
 
498
- if (!present) {
499
- throw new Error(
500
- `${capability} has no canonical specification for ${delta.operation}`,
501
- );
502
- }
503
-
504
496
  if (delta.operation === 'RENAMED') {
505
- if (!canonical.requirements.has(delta.previousTitle)) {
506
- throw new Error(
507
- `${capability} does not contain requirement: ${delta.previousTitle}`,
508
- );
509
- }
510
- if (
511
- delta.previousTitle !== delta.title &&
512
- canonical.requirements.has(delta.title)
513
- ) {
514
- throw new Error(
515
- `${capability} already contains requirement: ${delta.title}`,
516
- );
517
- }
518
497
  const entries = [...canonical.requirements.entries()];
519
498
  canonical.requirements = new Map(
520
499
  entries.map(([title, block]) =>
@@ -526,11 +505,6 @@ function planCanonicalUpdates(specRoot, deltas) {
526
505
  continue;
527
506
  }
528
507
 
529
- if (!canonical.requirements.has(delta.title)) {
530
- throw new Error(
531
- `${capability} does not contain requirement: ${delta.title}`,
532
- );
533
- }
534
508
  if (delta.operation === 'MODIFIED') {
535
509
  canonical.requirements.set(delta.title, renderRequirement(delta));
536
510
  } else if (delta.operation === 'REMOVED') {
@@ -31,7 +31,19 @@ Accelerated the minimum recommendation but does not force it.
31
31
 
32
32
  - Root owns sequential coordination artifacts, gate execution, and archive.
33
33
  - Explorer owns Full-route repository discovery.
34
- - Designer, quick, deep, or root may implement according to scope.
34
+ - SDD routes govern artifacts and gates, not implementation ownership. Root,
35
+ `designer`, `quick`, or `deep` may implement in Direct, Accelerated, or Full.
36
+ - Root decides ownership from explicit safe user direction and demonstrated net
37
+ gain. Delegation benefits include specialization, context isolation,
38
+ independent bounded work, quality, latency, or total cost; root continuity
39
+ benefits include short or sequential work, shared mutable state, accumulated
40
+ context, rediscovery, and coordination overhead. Route, file count, or cheaper
41
+ model price alone never selects an owner.
42
+ - Only after deciding to delegate implementation, select `designer` for UI/UX,
43
+ `quick` for known narrow low-risk work, or `deep` for coupled, edge-case-heavy,
44
+ migration, concurrency, shared-contract, or high-risk work.
45
+ - Each mutable surface has one writer; independent non-overlapping surfaces may
46
+ split, while coupled surfaces use one `deep` writer and ordered handoffs.
35
47
  - Oracle owns `plan-review` when the user selects it and **every** `verify`. The
36
48
  implementation writer never approves its own work.
37
49
 
@@ -1,12 +1,39 @@
1
1
  # Implement contract
2
2
 
3
- **Owner**: root, designer, quick, or deep; exactly one writer per mutable surface
3
+ **Owner**: adaptive root, `designer`, `quick`, or `deep` in every route; exactly
4
+ one writer per mutable surface
4
5
 
5
- Root marks selected artifact-backed tasks `[~]`, then assigns exact paths,
6
- accepted FR/buildable-SC scope, non-goals, and verification commands. Use the
7
- mandatory `tdd` skill for behavior changes and work in vertical red/green
8
- slices. Child writers never edit task state; root marks `[x]` only after checking
9
- task-specific evidence.
6
+ SDD routes govern artifacts and gates, not implementation ownership. Before
7
+ implementation or dispatch, root records the decided owner, task-shape/net-gain
8
+ rationale, exact paths, accepted FR/buildable-SC scope, non-goals, and
9
+ verification commands. Route, file count, or cheaper model price alone is not an
10
+ ownership rationale.
11
+
12
+ Root may retain implementation when short or sequential work, shared mutable
13
+ state, accumulated context, rediscovery, or coordination overhead outweighs
14
+ delegation benefit. Delegate when specialization, context isolation, independent
15
+ bounded work, quality, latency, or total cost creates demonstrated net gain. If
16
+ root owns the surface, do not dispatch a writer. If a specialist owns it, send a
17
+ bounded handoff with the exact surface, requirements, and checks.
18
+
19
+ Root marks selected artifact-backed tasks `[~]`; Direct or no-artifact work has
20
+ no task state. Use the mandatory `tdd` skill for behavior changes and work in
21
+ vertical red/green slices. Child writers never edit task state; root marks `[x]`
22
+ only after checking task-specific evidence.
23
+
24
+ After deciding delegation creates net gain, select deterministically:
25
+
26
+ - `designer` for user-facing UI/UX, interaction, accessibility, responsive, or
27
+ visual-quality work;
28
+ - `quick` for a known narrow mechanical low-risk surface with exact targets;
29
+ - `deep` for coupled multi-file, shared-contract, migration, concurrency,
30
+ edge-case-heavy, or high-risk work.
31
+
32
+ Proven independent surfaces may use different writers only with non-overlapping
33
+ ownership. Overlapping or compatibility-coupled work uses one `deep` writer and
34
+ ordered handoffs. A writer escalates before expanding beyond its routing
35
+ boundary. The implementation writer never performs final verification; root
36
+ hands the unchanged candidate to a fresh Oracle.
10
37
 
11
38
  Implementation evidence may refine an artifact without restarting the workflow:
12
39
 
@@ -11,6 +11,16 @@ implementation-neutral.
11
11
  `None`).
12
12
  - Define prioritized, independently testable `US#` stories. Every story needs
13
13
  `Covers: FR-###, SC-###` and Given/When/Then acceptance scenarios.
14
+ - Before choosing durable delta metadata, read
15
+ `openspec/specs/<capability>/spec.md` when it exists and compare the exact
16
+ `### Requirement:` titles and normative behavior:
17
+ - use `ADDED` only when no canonical requirement already expresses the
18
+ behavior;
19
+ - use `MODIFIED` or `REMOVED` only with the exact existing title;
20
+ - use `RENAMED <capability> FROM <exact previous title>` when the title changes;
21
+ - only `ADDED` is valid when the canonical capability does not exist.
22
+ A validator warning on `ADDED` into an existing nonempty capability requires
23
+ semantic-overlap review; exact-title validity alone does not prove new intent.
14
24
  - Give every sequential FR a descriptive title, a normative `MUST`/`SHALL`
15
25
  statement, and exactly one delta marker: `INTERNAL`, `ADDED`, `MODIFIED`,
16
26
  `REMOVED`, or `RENAMED ... FROM ...`.
@@ -20,4 +30,5 @@ implementation-neutral.
20
30
  - State assumptions, dependencies, edge cases, and explicit non-goals.
21
31
 
22
32
  Do not advance while a material ambiguity would change scope, behavior,
23
- architecture, or durable delta intent.
33
+ architecture, or durable delta intent. Run the `specify` gate before planning;
34
+ do not defer canonical delta reconciliation until archive.
@@ -0,0 +1,158 @@
1
+ export const CAPABILITY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2
+
3
+ export function parseRequirementDelta(metadata) {
4
+ if (metadata === 'INTERNAL') return { operation: 'INTERNAL' };
5
+
6
+ const change = /^(ADDED|MODIFIED|REMOVED)\s+(\S+)$/.exec(metadata);
7
+ if (change && CAPABILITY_PATTERN.test(change[2])) {
8
+ return { operation: change[1], capability: change[2] };
9
+ }
10
+
11
+ const rename = /^RENAMED\s+(\S+)\s+FROM\s+(.+)$/.exec(metadata);
12
+ if (
13
+ rename &&
14
+ CAPABILITY_PATTERN.test(rename[1]) &&
15
+ rename[2].trim().length > 0
16
+ ) {
17
+ return {
18
+ operation: 'RENAMED',
19
+ capability: rename[1],
20
+ previousTitle: rename[2].trim(),
21
+ };
22
+ }
23
+
24
+ return undefined;
25
+ }
26
+
27
+ export function parseCanonicalSpec(content) {
28
+ const candidates = [...content.matchAll(/^### Requirement:[^\r\n]*$/gm)];
29
+ const headings = [
30
+ ...content.matchAll(/^### Requirement:[ \t]+(\S(?:[^\r\n]*?\S)?)[ \t]*$/gm),
31
+ ];
32
+ if (candidates.length !== headings.length) {
33
+ throw new Error(
34
+ 'Canonical specification contains a malformed Requirement heading',
35
+ );
36
+ }
37
+ const requirements = new Map();
38
+ const prefix =
39
+ headings.length === 0
40
+ ? content.trimEnd()
41
+ : content.slice(0, headings[0].index).trimEnd();
42
+
43
+ for (const [index, heading] of headings.entries()) {
44
+ const start = heading.index ?? 0;
45
+ const end = headings[index + 1]?.index ?? content.length;
46
+ const title = heading[1].trim();
47
+ if (requirements.has(title)) {
48
+ throw new Error(`Canonical specification repeats requirement: ${title}`);
49
+ }
50
+ requirements.set(title, content.slice(start, end).trim());
51
+ }
52
+
53
+ return { prefix, requirements };
54
+ }
55
+
56
+ function issue(code, capability, title, message) {
57
+ return { code, capability, title, message };
58
+ }
59
+
60
+ export function preflightRequirementDeltas({
61
+ capability,
62
+ present,
63
+ requirements,
64
+ deltas,
65
+ }) {
66
+ const initialTitles = [...requirements.keys()];
67
+ const titles = new Set(initialTitles);
68
+ const errors = [];
69
+ const warnings = [];
70
+
71
+ for (const delta of deltas) {
72
+ if (delta.operation === 'ADDED') {
73
+ if (titles.has(delta.title)) {
74
+ errors.push(
75
+ issue(
76
+ 'SDD-SPEC-DELTA-ADDED-EXISTS',
77
+ capability,
78
+ delta.title,
79
+ `${capability} already contains requirement: ${delta.title}`,
80
+ ),
81
+ );
82
+ continue;
83
+ }
84
+ if (present && initialTitles.length > 0) {
85
+ warnings.push(
86
+ issue(
87
+ 'SDD-SPEC-DELTA-ADDED-REVIEW',
88
+ capability,
89
+ delta.title,
90
+ `${capability} already contains canonical requirements; confirm ${delta.title} does not overlap: ${initialTitles.join(', ')}`,
91
+ ),
92
+ );
93
+ }
94
+ titles.add(delta.title);
95
+ continue;
96
+ }
97
+
98
+ if (!present) {
99
+ const code =
100
+ delta.operation === 'RENAMED'
101
+ ? 'SDD-SPEC-DELTA-RENAMED-SOURCE-MISSING'
102
+ : `SDD-SPEC-DELTA-${delta.operation}-MISSING`;
103
+ errors.push(
104
+ issue(
105
+ code,
106
+ capability,
107
+ delta.title,
108
+ `${capability} has no canonical specification for ${delta.operation}: ${delta.title}`,
109
+ ),
110
+ );
111
+ continue;
112
+ }
113
+
114
+ if (delta.operation === 'RENAMED') {
115
+ if (!titles.has(delta.previousTitle)) {
116
+ errors.push(
117
+ issue(
118
+ 'SDD-SPEC-DELTA-RENAMED-SOURCE-MISSING',
119
+ capability,
120
+ delta.title,
121
+ `${capability} does not contain requirement: ${delta.previousTitle}`,
122
+ ),
123
+ );
124
+ continue;
125
+ }
126
+ if (delta.previousTitle !== delta.title && titles.has(delta.title)) {
127
+ errors.push(
128
+ issue(
129
+ 'SDD-SPEC-DELTA-RENAMED-TARGET-EXISTS',
130
+ capability,
131
+ delta.title,
132
+ `${capability} already contains requirement: ${delta.title}`,
133
+ ),
134
+ );
135
+ continue;
136
+ }
137
+ titles.delete(delta.previousTitle);
138
+ titles.add(delta.title);
139
+ continue;
140
+ }
141
+
142
+ if (!titles.has(delta.title)) {
143
+ errors.push(
144
+ issue(
145
+ `SDD-SPEC-DELTA-${delta.operation}-MISSING`,
146
+ capability,
147
+ delta.title,
148
+ `${capability} does not contain requirement: ${delta.title}`,
149
+ ),
150
+ );
151
+ continue;
152
+ }
153
+
154
+ if (delta.operation === 'REMOVED') titles.delete(delta.title);
155
+ }
156
+
157
+ return { errors, warnings, titles };
158
+ }
@@ -2,6 +2,12 @@
2
2
 
3
3
  import { existsSync, readFileSync } from 'node:fs';
4
4
  import { join, posix, resolve } from 'node:path';
5
+ import {
6
+ CAPABILITY_PATTERN,
7
+ parseCanonicalSpec,
8
+ parseRequirementDelta,
9
+ preflightRequirementDeltas,
10
+ } from './durable-deltas.mjs';
5
11
 
6
12
  function parseArgs(argv) {
7
13
  const values = { route: 'full', through: 'ready', json: false };
@@ -86,34 +92,6 @@ function checkUniqueSequential(ids, prefix, artifact, errors) {
86
92
  }
87
93
  }
88
94
 
89
- const CAPABILITY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
90
-
91
- function parseRequirementDelta(metadata) {
92
- if (metadata === 'INTERNAL') {
93
- return { operation: 'INTERNAL' };
94
- }
95
-
96
- const change = /^(ADDED|MODIFIED|REMOVED)\s+(\S+)$/.exec(metadata);
97
- if (change && CAPABILITY_PATTERN.test(change[2])) {
98
- return { operation: change[1], capability: change[2] };
99
- }
100
-
101
- const rename = /^RENAMED\s+(\S+)\s+FROM\s+(.+)$/.exec(metadata);
102
- if (
103
- rename &&
104
- CAPABILITY_PATTERN.test(rename[1]) &&
105
- rename[2].trim().length > 0
106
- ) {
107
- return {
108
- operation: 'RENAMED',
109
- capability: rename[1],
110
- previousTitle: rename[2].trim(),
111
- };
112
- }
113
-
114
- return undefined;
115
- }
116
-
117
95
  function parseAffectedCapabilities(intentSection) {
118
96
  const match = /^\*\*Affected capabilities\*\*:\s*(.+)$/im.exec(
119
97
  intentSection ?? '',
@@ -264,6 +242,7 @@ function validateSpec(content, errors) {
264
242
  ),
265
243
  );
266
244
  }
245
+ const durableDeltas = [];
267
246
  if (frIds.length === 0) {
268
247
  errors.push(issue('SDD-SPEC-FR', artifact, 'Add FR-### requirements.'));
269
248
  } else {
@@ -298,6 +277,11 @@ function validateSpec(content, errors) {
298
277
  `FR-${match[1]} targets undeclared capability ${delta.capability}.`,
299
278
  ),
300
279
  );
280
+ } else if (delta.operation !== 'INTERNAL') {
281
+ durableDeltas.push({
282
+ title: match[2].trim(),
283
+ ...delta,
284
+ });
301
285
  }
302
286
  }
303
287
  }
@@ -369,9 +353,56 @@ function validateSpec(content, errors) {
369
353
  outcomeScIds: successCriteria
370
354
  .filter((match) => match[2].toLowerCase() === 'outcome')
371
355
  .map((match) => match[1]),
356
+ durableDeltas,
372
357
  };
373
358
  }
374
359
 
360
+ function validateDurableDeltaIntent(specRoot, deltas, errors, warnings) {
361
+ const byCapability = new Map();
362
+ for (const delta of deltas) {
363
+ const current = byCapability.get(delta.capability) ?? [];
364
+ current.push(delta);
365
+ byCapability.set(delta.capability, current);
366
+ }
367
+
368
+ for (const [capability, capabilityDeltas] of byCapability) {
369
+ const path = join(specRoot, capability, 'spec.md');
370
+ const present = existsSync(path);
371
+ let canonical;
372
+ try {
373
+ canonical = present
374
+ ? parseCanonicalSpec(readFileSync(path, 'utf8'))
375
+ : { requirements: new Map() };
376
+ } catch (error) {
377
+ errors.push(
378
+ issue(
379
+ 'SDD-SPEC-DELTA-BASELINE',
380
+ 'spec.md',
381
+ `${capability} canonical baseline is invalid: ${error instanceof Error ? error.message : String(error)}`,
382
+ ),
383
+ );
384
+ continue;
385
+ }
386
+
387
+ const result = preflightRequirementDeltas({
388
+ capability,
389
+ present,
390
+ requirements: canonical.requirements,
391
+ deltas: capabilityDeltas,
392
+ });
393
+ errors.push(
394
+ ...result.errors.map((entry) =>
395
+ issue(entry.code, 'spec.md', entry.message),
396
+ ),
397
+ );
398
+ warnings.push(
399
+ ...result.warnings.map((entry) =>
400
+ issue(entry.code, 'spec.md', entry.message),
401
+ ),
402
+ );
403
+ }
404
+ }
405
+
375
406
  function constitutionEntries(content) {
376
407
  if (content === undefined) return [];
377
408
  return content
@@ -1047,6 +1078,14 @@ function validate({ change, route, through }) {
1047
1078
 
1048
1079
  const specContract =
1049
1080
  spec === undefined ? undefined : validateSpec(spec, errors);
1081
+ if (specContract !== undefined) {
1082
+ validateDurableDeltaIntent(
1083
+ join(root, '..', '..', 'specs'),
1084
+ specContract.durableDeltas,
1085
+ errors,
1086
+ warnings,
1087
+ );
1088
+ }
1050
1089
  const constitution = readArtifact(
1051
1090
  join(root, '..', '..'),
1052
1091
  'memory/constitution.md',
@@ -30,7 +30,15 @@ As a [actor], I can [capability] so that [value].
30
30
 
31
31
  ## Functional requirements
32
32
 
33
- - **FR-001 — [Requirement name]**: `[ADDED capability-slug]` The system MUST [observable behavior].
33
+ - **FR-001 — [Requirement name]**: `[DELTA capability-slug]` The system MUST [observable behavior].
34
+
35
+ Replace `DELTA` after inspecting `openspec/specs/<capability>/spec.md`. Use
36
+ `ADDED` only for behavior not expressed by a canonical requirement, `MODIFIED`
37
+ or `REMOVED` only with the exact existing requirement title, and
38
+ `RENAMED capability-slug FROM Exact previous title` when the title changes. Only
39
+ `ADDED` is valid when the canonical capability is absent. An addition to an
40
+ existing nonempty capability requires semantic-overlap review even when its
41
+ exact title is new.
34
42
 
35
43
  Use exactly one delta marker per FR: `[INTERNAL]`, `[ADDED capability-slug]`,
36
44
  `[MODIFIED capability-slug]`, `[REMOVED capability-slug]`, or
@@ -31,6 +31,13 @@ The script reads delta metadata directly from `spec.md`:
31
31
  - `[RENAMED capability FROM Previous title]` renames and replaces it.
32
32
  - `[INTERNAL]` never changes `openspec/specs/`.
33
33
 
34
+ The SDD validator preflights these operations against the canonical requirement
35
+ titles from `specify` onward. Archive reuses the same ordered parser and
36
+ preflight as a final defense before staging writes, preserving stable
37
+ `SDD-SPEC-DELTA-*` incompatibility codes. An `ADDED` warning for an existing
38
+ nonempty capability requires semantic-overlap review because differently named
39
+ requirements cannot be proven distinct by exact-title tooling alone.
40
+
34
41
  All delta targets are validated before any permanent specification changes. The
35
42
  operation stages and rolls back the full canonical update if a delta, report
36
43
  update, or final move raises a handled error in the active process. Report and