thoth-agents 0.3.8 → 0.3.10
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/.claude-plugin/marketplace.json +1 -1
- package/README.md +27 -0
- package/dist/agents/prompt-dialects.d.ts +3 -0
- package/dist/{chunk-3WNTCPVK.js → chunk-JBG7XMIG.js} +1291 -290
- package/dist/{chunk-F2K4CS5J.js → chunk-M2WF46BP.js} +186 -128
- package/dist/{chunk-6GQYMWWT.js → chunk-TKIDBRM3.js} +1 -1
- package/dist/cli/commands.d.ts +1 -0
- package/dist/cli/config-io.d.ts +2 -1
- package/dist/cli/index.js +126 -428
- package/dist/cli/install-completion.d.ts +34 -0
- package/dist/cli/install-ledger.d.ts +44 -0
- package/dist/cli/install.d.ts +9 -1
- package/dist/cli/operations/claude-code.d.ts +12 -1
- package/dist/cli/operations/codex.d.ts +16 -0
- package/dist/cli/operations/opencode.d.ts +20 -3
- package/dist/cli/operations/types.d.ts +13 -0
- package/dist/cli/package-version.d.ts +19 -0
- package/dist/cli/tui/index.js +63 -24
- package/dist/harness/core/sdd.d.ts +1 -1
- package/dist/hooks/auto-update-checker/checker.d.ts +0 -5
- package/dist/hooks/auto-update-checker/index.d.ts +1 -2
- package/dist/hooks/auto-update-checker/types.d.ts +0 -1
- package/dist/index.js +98 -251
- package/package.json +1 -1
- package/plugin/.claude-plugin/.thoth-agents-plugin-assets.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/agents/orchestrator.md +17 -17
- package/plugin/skills/thoth-archive/SKILL.md +7 -0
- package/plugin/skills/thoth-archive/scripts/archive.mjs +28 -74
- package/plugin/skills/thoth-sdd/references/phases/specify.md +12 -1
- package/plugin/skills/thoth-sdd/scripts/durable-deltas.mjs +158 -0
- package/plugin/skills/thoth-sdd/scripts/validate.mjs +67 -28
- package/plugin/skills/thoth-sdd/templates/spec.md +9 -1
- package/skills/thoth-archive/SKILL.md +7 -0
- package/skills/thoth-archive/scripts/archive.mjs +28 -74
- package/skills/thoth-sdd/references/phases/specify.md +12 -1
- package/skills/thoth-sdd/scripts/durable-deltas.mjs +158 -0
- package/skills/thoth-sdd/scripts/validate.mjs +67 -28
- package/skills/thoth-sdd/templates/spec.md +9 -1
- package/dist/hooks/auto-update-checker/cache.d.ts +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thoth-agents",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"description": "Adaptive multi-harness orchestration with seven roles, runtime-autonomous Spec Kit-compatible SDD routes, and external provider boundaries.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
{
|
|
35
35
|
"path": "agents/orchestrator.md",
|
|
36
36
|
"kind": "agent-config",
|
|
37
|
-
"sha256": "sha256:
|
|
37
|
+
"sha256": "sha256:9a1a9de425e7499110e457b58481d953391d07841dc319f0f8c7b09d64fa8998"
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"path": "agents/quick.md",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thoth-agents",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"description": "Bundled Spec Kit-compatible SDD skills and MCP configuration; the seven-role global Codex layer requires thoth-agents CLI setup.",
|
|
5
5
|
"skills": "./skills/",
|
|
6
6
|
"mcpServers": "./codex.mcp.json"
|
|
@@ -13,10 +13,19 @@ You are the adaptive root for thoth-agents. Keep requirements, decisions, execut
|
|
|
13
13
|
- Delegate only for net gain from specialization, context isolation, review, or safe parallelism. The maximum delegation depth is 1; children never delegate.
|
|
14
14
|
- Maintain one writer for each mutable surface. Parallelize only independent work with no overlapping writes.
|
|
15
15
|
- Keep prompts bounded; request distilled evidence, not raw logs or full files.
|
|
16
|
+
- Preserve unrelated changes; report changed files, evidence, risks, and capability gaps.
|
|
16
17
|
- Use `AskUserQuestion` only when a material unresolved choice changes the result. Continue all safe non-blocked work first.
|
|
17
18
|
- Use `TodoWrite` only when the work genuinely has multiple dependent steps.
|
|
18
19
|
</operating-model>
|
|
19
20
|
|
|
21
|
+
<delegation-lifecycle>
|
|
22
|
+
- A new objective, SDD phase, mutable surface, or independent judgment is a work boundary: start a fresh specialist using a normal `Agent` invocation. Never treat completed agents as a reusable role pool.
|
|
23
|
+
- Independent context: do not use `fork` for independent work.
|
|
24
|
+
- Continue with `SendMessage` to the prior agent ID only to steer, complete, or clarify the same bounded assignment; never to cross a work boundary.
|
|
25
|
+
- TaskOutput on the same task session only collects the active nonterminal assignment and does not authorize later reuse.
|
|
26
|
+
- Every Oracle plan review, verification round, and approval or PASS judgment uses a fresh Oracle instance. An existing Oracle session may only clarify its current findings.
|
|
27
|
+
</delegation-lifecycle>
|
|
28
|
+
|
|
20
29
|
<routing>
|
|
21
30
|
- thoth-agents:explorer: Resolve broad or uncertain repository questions and return distilled evidence.
|
|
22
31
|
- thoth-agents:librarian: Gather current authoritative evidence and separate documented facts from inference.
|
|
@@ -55,29 +64,20 @@ You are the adaptive root for thoth-agents. Keep requirements, decisions, execut
|
|
|
55
64
|
</external-skills>
|
|
56
65
|
|
|
57
66
|
<memory>
|
|
58
|
-
- Load the installed `thoth-mem` skill for resume or prior work
|
|
59
|
-
- Preserve a durable decision, root cause, convention, or discovery only when reusable. Root owns the stable root session ID, project, lifecycle, real-user intent, and authorization
|
|
60
|
-
- Follow the skill at verified compaction
|
|
61
|
-
- `openspec/` remains canonical; do not mirror SDD phase artifacts
|
|
67
|
+
- Load the installed `thoth-mem` skill for resume or prior work; never invent its protocol.
|
|
68
|
+
- Preserve a durable decision, root cause, convention, or discovery only when reusable. Root owns the stable root session ID, project, lifecycle, real-user intent, and authorization.
|
|
69
|
+
- Follow the skill at verified compaction or a meaningful semantic boundary; children receive only bounded MEMORY and never own root lifecycle.
|
|
70
|
+
- `openspec/` remains canonical; do not mirror SDD phase artifacts. A memory failure does not block unrelated work.
|
|
62
71
|
</memory>
|
|
63
72
|
|
|
64
73
|
<artifacts>
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
- thoth-agents:oracle returns read-only findings. Root persists verification; after PASS, archive syncs declared durable deltas and moves the change to openspec/changes/archive/YYYY-MM-DD-<feature>/.
|
|
74
|
+
- In openspec/changes/<feature>/, Accelerated and Full require spec.md, plan.md, tasks.md, verify-report.md, and archive-report.md.
|
|
75
|
+
- Root owns openspec/ gates and task state, moves [~] -> [x] after evidence, and keeps one product writer.
|
|
76
|
+
- thoth-agents:oracle returns read-only findings; root persists verification and archives declared durable deltas after PASS.
|
|
69
77
|
</artifacts>
|
|
70
78
|
|
|
71
|
-
<execution>
|
|
72
|
-
- Validate contracts and tests first; use test-first work for behavior. Root or one writer implements; do not delegate merely because an agent exists.
|
|
73
|
-
- thoth-agents:oracle owns selected plan review and every verification; root and writers never self-approve.
|
|
74
|
-
- Preserve unrelated working-tree changes. Never instruct an agent to discard them.
|
|
75
|
-
- Report changed files, evidence, risks, and capability gaps truthfully.
|
|
76
|
-
</execution>
|
|
77
|
-
|
|
78
79
|
<delegation>
|
|
79
|
-
-
|
|
80
|
-
- Parallelize only independent work and await requested results before synthesis.
|
|
80
|
+
- Use this envelope for all `Agent` delegation; parallelize only independent work and await results.
|
|
81
81
|
- Child return fields: conclusion, evidence, verification, risks, openQuestions, nextAction.
|
|
82
82
|
|
|
83
83
|
## PHASE
|
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -418,27 +405,6 @@ function parseChangeSpec(spec) {
|
|
|
418
405
|
};
|
|
419
406
|
}
|
|
420
407
|
|
|
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
408
|
function newCanonicalSpec(capability) {
|
|
443
409
|
const title = capability
|
|
444
410
|
.split('-')
|
|
@@ -480,41 +446,34 @@ function planCanonicalUpdates(specRoot, deltas) {
|
|
|
480
446
|
for (const [capability, capabilityDeltas] of byCapability) {
|
|
481
447
|
const path = join(specRoot, capability, 'spec.md');
|
|
482
448
|
const present = existsSync(path);
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
449
|
+
let canonical;
|
|
450
|
+
try {
|
|
451
|
+
canonical = present
|
|
452
|
+
? parseCanonicalSpec(readFileSync(path, 'utf8'))
|
|
453
|
+
: newCanonicalSpec(capability);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
`SDD-SPEC-DELTA-BASELINE: ${capability} canonical baseline is invalid: ${errorMessage(error)}`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
const preflight = preflightRequirementDeltas({
|
|
460
|
+
capability,
|
|
461
|
+
present,
|
|
462
|
+
requirements: canonical.requirements,
|
|
463
|
+
deltas: capabilityDeltas,
|
|
464
|
+
});
|
|
465
|
+
if (preflight.errors.length > 0) {
|
|
466
|
+
const [first] = preflight.errors;
|
|
467
|
+
throw new Error(`${first.code}: ${first.message}`);
|
|
468
|
+
}
|
|
486
469
|
|
|
487
470
|
for (const delta of capabilityDeltas) {
|
|
488
471
|
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
472
|
canonical.requirements.set(delta.title, renderRequirement(delta));
|
|
495
473
|
continue;
|
|
496
474
|
}
|
|
497
475
|
|
|
498
|
-
if (!present) {
|
|
499
|
-
throw new Error(
|
|
500
|
-
`${capability} has no canonical specification for ${delta.operation}`,
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
476
|
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
477
|
const entries = [...canonical.requirements.entries()];
|
|
519
478
|
canonical.requirements = new Map(
|
|
520
479
|
entries.map(([title, block]) =>
|
|
@@ -526,11 +485,6 @@ function planCanonicalUpdates(specRoot, deltas) {
|
|
|
526
485
|
continue;
|
|
527
486
|
}
|
|
528
487
|
|
|
529
|
-
if (!canonical.requirements.has(delta.title)) {
|
|
530
|
-
throw new Error(
|
|
531
|
-
`${capability} does not contain requirement: ${delta.title}`,
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
488
|
if (delta.operation === 'MODIFIED') {
|
|
535
489
|
canonical.requirements.set(delta.title, renderRequirement(delta));
|
|
536
490
|
} else if (delta.operation === 'REMOVED') {
|
|
@@ -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]**: `[
|
|
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
|