opencode-swarm 7.107.0 → 7.107.1

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 (28) hide show
  1. package/dist/agents/explorer.d.ts +2 -2
  2. package/dist/cli/curator-drift-6ad8mxxb.js +211 -0
  3. package/dist/cli/curator-llm-factory-gdhtm1hh.js +31 -0
  4. package/dist/cli/curator-np2ky29t.js +51 -0
  5. package/dist/cli/{evidence-summary-service-zhw4rkjb.js → evidence-summary-service-dh44gevz.js} +4 -3
  6. package/dist/cli/{explorer-rwfvbbx5.js → explorer-ksr3xq7n.js} +1 -1
  7. package/dist/cli/{guardrail-explain-6xt0pb56.js → guardrail-explain-414smfg4.js} +13 -11
  8. package/dist/cli/hive-promoter-y9r8d315.js +39 -0
  9. package/dist/cli/{index-ydgy7vg5.js → index-09xpycan.js} +31 -5
  10. package/dist/cli/index-357p0baj.js +719 -0
  11. package/dist/cli/{index-wa6at603.js → index-7v734syt.js} +1 -1
  12. package/dist/cli/{index-v90bnn0f.js → index-87d4wsv9.js} +14 -12
  13. package/dist/cli/{index-vwyjkzgs.js → index-9wstcavp.js} +1 -1
  14. package/dist/cli/index-c8s9a3zh.js +73 -0
  15. package/dist/cli/{index-fhw0jm5c.js → index-gt514nt1.js} +23018 -21209
  16. package/dist/cli/{index-25pev4e7.js → index-jjp5qrjv.js} +122 -834
  17. package/dist/cli/{index-e9n3xsk0.js → index-t4hbye3v.js} +1 -1
  18. package/dist/cli/index.js +12 -10
  19. package/dist/cli/{pending-delegations-dvprsdfy.js → pending-delegations-46xf2n2e.js} +2 -2
  20. package/dist/cli/{pr-subscriptions-zfxjtyzw.js → pr-subscriptions-3c34rnm1.js} +3 -3
  21. package/dist/cli/{skill-generator-99rrfwae.js → skill-generator-y6mmq8n5.js} +1 -1
  22. package/dist/commands/curate.d.ts +38 -1
  23. package/dist/commands/registry.d.ts +2 -2
  24. package/dist/hooks/curator-postmortem.d.ts +52 -3
  25. package/dist/hooks/curator.d.ts +2 -1
  26. package/dist/index.js +349 -313
  27. package/package.json +1 -1
  28. package/dist/cli/{index-7ztmmqpt.js → index-mpe0yk9s.js} +3 -3
@@ -0,0 +1,719 @@
1
+ // @bun
2
+ import {
3
+ exports_external
4
+ } from "./index-293f68mj.js";
5
+
6
+ // src/sdd/effective-spec.ts
7
+ import { createHash } from "crypto";
8
+ import * as fs from "fs";
9
+ import * as path from "path";
10
+
11
+ // src/config/spec-schema.ts
12
+ var ObligationSchema = exports_external.enum(["MUST", "SHALL", "SHOULD", "MAY"]);
13
+ var SpecRequirementSchema = exports_external.object({
14
+ id: exports_external.string().regex(/^FR-(?!000)\d{3}$/, "Requirement ID must match FR-### pattern (e.g., FR-001)"),
15
+ obligation: ObligationSchema,
16
+ text: exports_external.string().min(1)
17
+ });
18
+ var SpecScenarioSchema = exports_external.object({
19
+ name: exports_external.string().min(1),
20
+ given: exports_external.array(exports_external.string()).optional().default([]),
21
+ when: exports_external.array(exports_external.string()).min(1, 'Scenario must have at least one "when" clause'),
22
+ thenClauses: exports_external.array(exports_external.string()).min(1, 'Scenario must have at least one "then" clause')
23
+ });
24
+ var SpecSectionSchema = exports_external.object({
25
+ name: exports_external.string().min(1),
26
+ requirements: exports_external.array(SpecRequirementSchema).default([])
27
+ });
28
+ var SwarmSpecSchema = exports_external.object({
29
+ title: exports_external.string().min(1),
30
+ purpose: exports_external.string().min(1),
31
+ sections: exports_external.array(SpecSectionSchema).min(1, "Spec must have at least one section")
32
+ });
33
+ var SpecDeltaSchema = exports_external.object({
34
+ added: exports_external.array(SpecRequirementSchema).default([]),
35
+ modified: exports_external.array(SpecRequirementSchema).default([]),
36
+ removed: exports_external.array(SpecRequirementSchema).default([])
37
+ });
38
+ var DeltaSpecSchema = exports_external.union([
39
+ SwarmSpecSchema,
40
+ SpecDeltaSchema
41
+ ]);
42
+ var FENCED_BLOCK_PATTERN = /```[\s\S]*?```/g;
43
+ var INLINE_CODE_PATTERN = /`[^`]*`/g;
44
+ var FR_ID_PATTERN = /\bFR-\d{3}\b/g;
45
+ var OBLIGATION_PATTERN = /\b(MUST|SHALL|SHOULD|MAY)\b/g;
46
+ var SECTION_HEADER_PATTERN = /^##\s+.+$/gm;
47
+ function stripCodeBlocks(content) {
48
+ return content.replace(FENCED_BLOCK_PATTERN, "").replace(INLINE_CODE_PATTERN, "");
49
+ }
50
+ function getLineNumber(content, position) {
51
+ const prefix = content.substring(0, position);
52
+ return (prefix.match(/\n/g) || []).length + 1;
53
+ }
54
+ function validateSpecContent(content) {
55
+ const issues = [];
56
+ if (!content || content.trim().length === 0) {
57
+ return { valid: false, issues: [{ line: 1, message: "Content is empty" }] };
58
+ }
59
+ const strippedContent = stripCodeBlocks(content);
60
+ const frMatches = strippedContent.match(FR_ID_PATTERN);
61
+ if (!frMatches || frMatches.length === 0) {
62
+ issues.push({
63
+ line: 1,
64
+ message: "No FR-### requirement IDs found in spec content"
65
+ });
66
+ }
67
+ const obligationMatches = strippedContent.match(OBLIGATION_PATTERN);
68
+ if (!obligationMatches || obligationMatches.length === 0) {
69
+ issues.push({
70
+ line: 1,
71
+ message: "No obligation keywords (MUST, SHALL, SHOULD, MAY) found in spec content"
72
+ });
73
+ }
74
+ const sectionMatches = strippedContent.match(SECTION_HEADER_PATTERN);
75
+ if (!sectionMatches || sectionMatches.length === 0) {
76
+ issues.push({
77
+ line: 1,
78
+ message: "No section headers (## Section Name) found in spec content"
79
+ });
80
+ }
81
+ const idMatches = strippedContent.matchAll(/\bFR-(\d{3})\b/g);
82
+ for (const idMatch of idMatches) {
83
+ const num = parseInt(idMatch[1], 10);
84
+ if (num === 0) {
85
+ const pos = idMatch.index;
86
+ issues.push({
87
+ line: getLineNumber(strippedContent, pos),
88
+ message: `Invalid FR-ID "${idMatch[0]}" \u2014 number must be 001-999`
89
+ });
90
+ }
91
+ }
92
+ return {
93
+ valid: issues.length === 0,
94
+ issues
95
+ };
96
+ }
97
+
98
+ // src/sdd/effective-spec.ts
99
+ var SWARM_SPEC_REL = path.join(".swarm", "spec.md");
100
+ var OPENSPEC_ROOT = "openspec";
101
+ var SPECKIT_MARKER = ".specify";
102
+ var SPECKIT_SPECS_DIR = "specs";
103
+ var MAX_SPEC_BYTES = 256 * 1024;
104
+ var MAX_SOURCE_BYTES = 512 * 1024;
105
+ var MAX_SPEC_FILES = 100;
106
+ var MAX_WALK_DEPTH = 10;
107
+ var SPECKIT_REQUIRED_SECTIONS = [
108
+ "## Functional Requirements",
109
+ "## Success Criteria"
110
+ ];
111
+ function toPosix(relPath) {
112
+ return relPath.split(path.sep).join("/");
113
+ }
114
+ function hash(content) {
115
+ return createHash("sha256").update(content, "utf-8").digest("hex");
116
+ }
117
+ function readTextBounded(absPath) {
118
+ let stat;
119
+ try {
120
+ stat = fs.lstatSync(absPath);
121
+ } catch {
122
+ return null;
123
+ }
124
+ if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) {
125
+ return null;
126
+ }
127
+ try {
128
+ return fs.readFileSync(absPath, "utf-8");
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+ function fileArtifact(root, absPath) {
134
+ try {
135
+ const stat = fs.lstatSync(absPath);
136
+ if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES)
137
+ return null;
138
+ return {
139
+ relPath: toPosix(path.relative(root, absPath)),
140
+ bytes: stat.size,
141
+ mtimeMs: stat.mtimeMs
142
+ };
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
147
+ function walkSpecFiles(root, startRel) {
148
+ const start = path.join(root, startRel);
149
+ if (!fs.existsSync(start))
150
+ return [];
151
+ const artifacts = [];
152
+ const stack = [
153
+ { abs: start, depth: 0 }
154
+ ];
155
+ while (stack.length > 0 && artifacts.length < MAX_SPEC_FILES) {
156
+ const item = stack.pop();
157
+ if (!item || item.depth > MAX_WALK_DEPTH)
158
+ continue;
159
+ let entries;
160
+ try {
161
+ entries = fs.readdirSync(item.abs, { withFileTypes: true });
162
+ } catch {
163
+ continue;
164
+ }
165
+ const dirents = entries.filter((entry) => typeof entry?.name === "string");
166
+ for (const entry of dirents.sort((a, b) => b.name.localeCompare(a.name))) {
167
+ const abs = path.join(item.abs, entry.name);
168
+ if (entry.isSymbolicLink())
169
+ continue;
170
+ if (entry.isDirectory()) {
171
+ stack.push({ abs, depth: item.depth + 1 });
172
+ } else if (entry.isFile() && entry.name === "spec.md") {
173
+ const artifact = fileArtifact(root, abs);
174
+ if (artifact)
175
+ artifacts.push(artifact);
176
+ if (artifacts.length >= MAX_SPEC_FILES)
177
+ break;
178
+ }
179
+ }
180
+ }
181
+ return artifacts.sort((a, b) => a.relPath.localeCompare(b.relPath));
182
+ }
183
+ function listOpenSpecChanges(root) {
184
+ const changesDir = path.join(root, OPENSPEC_ROOT, "changes");
185
+ if (!fs.existsSync(changesDir))
186
+ return [];
187
+ let entries;
188
+ try {
189
+ entries = fs.readdirSync(changesDir, { withFileTypes: true });
190
+ } catch {
191
+ return [];
192
+ }
193
+ return entries.filter((entry) => entry.isDirectory() && entry.name !== "archive").sort((a, b) => a.name.localeCompare(b.name)).map((entry) => {
194
+ const rel = path.join(OPENSPEC_ROOT, "changes", entry.name);
195
+ return {
196
+ id: entry.name,
197
+ proposal: fs.existsSync(path.join(root, rel, "proposal.md")),
198
+ design: fs.existsSync(path.join(root, rel, "design.md")),
199
+ tasks: fs.existsSync(path.join(root, rel, "tasks.md")),
200
+ specs: walkSpecFiles(root, path.join(rel, "specs"))
201
+ };
202
+ });
203
+ }
204
+ function detectKind(line, current) {
205
+ const upper = line.toUpperCase();
206
+ if (/^#{2,6}\s+ADDED\b/.test(upper))
207
+ return "ADDED";
208
+ if (/^#{2,6}\s+MODIFIED\b/.test(upper))
209
+ return "MODIFIED";
210
+ if (/^#{2,6}\s+REMOVED\b/.test(upper))
211
+ return "REMOVED";
212
+ return current;
213
+ }
214
+ function requirementTextFromBlock(title, block, kind) {
215
+ const joined = block.join(" ").replace(/\s+/g, " ").trim();
216
+ const obligation = joined.match(/\b(MUST|SHALL|SHOULD|MAY)\b/i)?.[1];
217
+ if (obligation)
218
+ return joined;
219
+ if (kind === "REMOVED") {
220
+ return `MAY remove or retire behavior for ${title}.`;
221
+ }
222
+ return `MUST satisfy OpenSpec requirement ${title}.`;
223
+ }
224
+ function parseRequirements(content, sourceRel, defaultKind) {
225
+ const requirements = [];
226
+ const lines = content.replace(/\r\n/g, `
227
+ `).split(`
228
+ `);
229
+ let kind = defaultKind;
230
+ for (let i = 0;i < lines.length; i++) {
231
+ const line = lines[i];
232
+ kind = detectKind(line, kind);
233
+ const explicit = line.match(/\b(FR-(?!000)\d{3})\b/);
234
+ if (explicit && /\b(MUST|SHALL|SHOULD|MAY)\b/i.test(line)) {
235
+ requirements.push({
236
+ id: explicit[1].toUpperCase(),
237
+ kind,
238
+ title: explicit[1].toUpperCase(),
239
+ text: line.trim().replace(/^\s*[-*]\s*/, ""),
240
+ sourceRel
241
+ });
242
+ continue;
243
+ }
244
+ const openSpecReq = line.match(/^#{3,4}\s+Requirement:\s*(.+)$/i);
245
+ if (!openSpecReq)
246
+ continue;
247
+ const title = openSpecReq[1].trim();
248
+ const block = [];
249
+ for (let j = i + 1;j < lines.length; j++) {
250
+ if (/^##\s+/.test(lines[j]))
251
+ break;
252
+ if (/^#{3,4}\s+Requirement:/i.test(lines[j]))
253
+ break;
254
+ const trimmed = lines[j].trim();
255
+ if (trimmed)
256
+ block.push(trimmed);
257
+ }
258
+ const id = block.join(`
259
+ `).match(/\b(FR-(?!000)\d{3})\b/)?.[1] ?? null;
260
+ requirements.push({
261
+ id: id?.toUpperCase() ?? null,
262
+ kind,
263
+ title,
264
+ text: requirementTextFromBlock(title, block, kind),
265
+ sourceRel
266
+ });
267
+ }
268
+ return requirements;
269
+ }
270
+ function nextFrId(used, warnings, reserved) {
271
+ for (let n = 1;n <= 999; n++) {
272
+ const id = `FR-${String(n).padStart(3, "0")}`;
273
+ if (!used.has(id) && !reserved?.has(id)) {
274
+ used.add(id);
275
+ return id;
276
+ }
277
+ }
278
+ warnings.push("More than 999 FR identifiers are required; reusing FR-999.");
279
+ return "FR-999";
280
+ }
281
+ function renderRequirement(req, used, warnings, reserved) {
282
+ const id = req.id && !used.has(req.id) ? req.id : nextFrId(used, warnings, reserved);
283
+ if (req.id && req.id !== id) {
284
+ warnings.push(`Duplicate requirement id ${req.id} in ${req.sourceRel}; generated ${id}.`);
285
+ }
286
+ if (req.id === id)
287
+ used.add(id);
288
+ let text = req.text;
289
+ if (!text.includes(id)) {
290
+ text = `${id}: ${text}`;
291
+ }
292
+ return `- ${text} _(source: ${req.sourceRel})_`;
293
+ }
294
+ function detectSpeckit(directory) {
295
+ const root = path.resolve(directory);
296
+ const markerPath = path.join(root, SPECKIT_MARKER);
297
+ const markerPresent = fs.statSync(markerPath, { throwIfNoEntry: false })?.isDirectory() ?? false;
298
+ if (!markerPresent) {
299
+ return { markerPresent: false, features: [] };
300
+ }
301
+ const specsRoot = path.join(root, SPECKIT_SPECS_DIR);
302
+ if (!fs.existsSync(specsRoot)) {
303
+ return { markerPresent: true, features: [] };
304
+ }
305
+ let entries;
306
+ try {
307
+ entries = fs.readdirSync(specsRoot, { withFileTypes: true });
308
+ } catch {
309
+ return { markerPresent: true, features: [] };
310
+ }
311
+ const featureDirs = entries.filter((entry) => typeof entry?.name === "string" && !entry.isSymbolicLink() && entry.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
312
+ const features = [];
313
+ for (const entry of featureDirs) {
314
+ if (features.length >= MAX_SPEC_FILES)
315
+ break;
316
+ const specAbs = path.join(specsRoot, entry.name, "spec.md");
317
+ let stat;
318
+ try {
319
+ stat = fs.lstatSync(specAbs);
320
+ } catch {
321
+ continue;
322
+ }
323
+ if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES)
324
+ continue;
325
+ features.push({
326
+ featureId: entry.name,
327
+ specRelPath: toPosix(path.relative(root, specAbs))
328
+ });
329
+ }
330
+ return { markerPresent: true, features };
331
+ }
332
+ function parseSpeckitRequirements(content, sourceRel) {
333
+ const requirements = [];
334
+ const lines = content.replace(/\r\n/g, `
335
+ `).split(`
336
+ `);
337
+ let inFrSection = false;
338
+ for (const line of lines) {
339
+ if (/^##\s+/.test(line)) {
340
+ inFrSection = /^##\s+Functional Requirements\s*$/i.test(line);
341
+ continue;
342
+ }
343
+ if (!inFrSection)
344
+ continue;
345
+ if (!/^\s*[-*]\s+/.test(line))
346
+ continue;
347
+ if (!/\b(MUST|SHALL|SHOULD|MAY)\b/i.test(line))
348
+ continue;
349
+ const text = line.trim().replace(/^\s*[-*]\s+/, "");
350
+ const explicit = line.match(/\b(FR-(?!000)\d{3})\b/);
351
+ if (explicit) {
352
+ requirements.push({
353
+ id: explicit[1].toUpperCase(),
354
+ kind: "CURRENT",
355
+ title: explicit[1].toUpperCase(),
356
+ text,
357
+ sourceRel
358
+ });
359
+ continue;
360
+ }
361
+ requirements.push({
362
+ id: null,
363
+ kind: "CURRENT",
364
+ title: text,
365
+ text,
366
+ sourceRel
367
+ });
368
+ }
369
+ return requirements;
370
+ }
371
+ function resolveSpeckitProjection(directory, options = {}) {
372
+ const root = path.resolve(directory);
373
+ const detection = detectSpeckit(root);
374
+ if (!detection.markerPresent) {
375
+ return { kind: "not_speckit" };
376
+ }
377
+ if (detection.features.length === 0) {
378
+ return { kind: "empty" };
379
+ }
380
+ let selectedFeature;
381
+ if (options.feature) {
382
+ const found = detection.features.find((f) => f.featureId === options.feature);
383
+ if (!found) {
384
+ return {
385
+ kind: "unknown_feature",
386
+ feature: options.feature,
387
+ available: detection.features.map((f) => f.featureId)
388
+ };
389
+ }
390
+ selectedFeature = found;
391
+ } else if (detection.features.length === 1) {
392
+ selectedFeature = detection.features[0];
393
+ } else {
394
+ return {
395
+ kind: "ambiguous",
396
+ features: detection.features.map((f) => f.featureId)
397
+ };
398
+ }
399
+ const specAbs = path.join(root, selectedFeature.specRelPath);
400
+ const content = readTextBounded(specAbs);
401
+ if (content === null) {
402
+ return { kind: "zero_requirements", feature: selectedFeature.featureId };
403
+ }
404
+ const warnings = [];
405
+ const usedIds = new Set;
406
+ const requirements = parseSpeckitRequirements(content, selectedFeature.specRelPath);
407
+ if (requirements.length === 0) {
408
+ return { kind: "zero_requirements", feature: selectedFeature.featureId };
409
+ }
410
+ const reservedIds = new Set;
411
+ for (const req of requirements) {
412
+ if (req.id)
413
+ reservedIds.add(req.id);
414
+ }
415
+ let mtimeMs = 0;
416
+ try {
417
+ mtimeMs = fs.lstatSync(specAbs).mtimeMs;
418
+ } catch {}
419
+ const lines = [
420
+ "# Specification: Effective SDD Projection",
421
+ "",
422
+ "Generated from Spec-Kit feature artifacts. Update the source artifacts, then run `/swarm sdd project` to refresh this projection.",
423
+ "",
424
+ "## Source Artifacts",
425
+ `- ${selectedFeature.specRelPath}`,
426
+ "",
427
+ "## Functional Requirements"
428
+ ];
429
+ for (const req of requirements) {
430
+ lines.push(renderRequirement(req, usedIds, warnings, reservedIds));
431
+ }
432
+ const projected = `${lines.join(`
433
+ `)}
434
+ `;
435
+ if (projected.length > MAX_SPEC_BYTES) {
436
+ return {
437
+ kind: "too_large",
438
+ feature: selectedFeature.featureId,
439
+ bytes: projected.length
440
+ };
441
+ }
442
+ const validation = validateSpecContent(projected);
443
+ if (!validation.valid) {
444
+ warnings.push(...validation.issues.map((issue) => `Projection line ${issue.line}: ${issue.message}`));
445
+ }
446
+ const spec = {
447
+ source: "speckit_projection",
448
+ content: projected,
449
+ hash: hash(projected),
450
+ mtime: mtimeMs > 0 ? new Date(mtimeMs).toISOString() : null,
451
+ sourcePaths: [selectedFeature.specRelPath],
452
+ warnings
453
+ };
454
+ return { kind: "ok", spec, feature: selectedFeature.featureId };
455
+ }
456
+ function buildSpeckitProjectionSync(directory, options = {}) {
457
+ const resolution = resolveSpeckitProjection(directory, options);
458
+ return resolution.kind === "ok" ? resolution.spec : null;
459
+ }
460
+ function loadSddStatusSync(directory, opts) {
461
+ const root = path.resolve(directory);
462
+ const swSpecPath = path.join(root, SWARM_SPEC_REL);
463
+ const openSpecPath = path.join(root, OPENSPEC_ROOT);
464
+ const errors = [];
465
+ const warnings = [];
466
+ const swSpecExists = fs.existsSync(swSpecPath);
467
+ const openSpecExists = fs.existsSync(openSpecPath);
468
+ const currentSpecs = walkSpecFiles(root, path.join(OPENSPEC_ROOT, "specs"));
469
+ const changes = listOpenSpecChanges(root);
470
+ const effectiveSpec = readEffectiveSpecSync(root, opts);
471
+ if (openSpecExists && currentSpecs.length === 0 && changes.length === 0) {
472
+ errors.push("openspec/ exists but contains no specs or active changes.");
473
+ }
474
+ for (const change of changes) {
475
+ if (!change.proposal) {
476
+ warnings.push(`Change ${change.id} is missing proposal.md.`);
477
+ }
478
+ if (!change.tasks) {
479
+ warnings.push(`Change ${change.id} is missing tasks.md; tasks remain proposal input, not plan state.`);
480
+ }
481
+ if (change.specs.length === 0) {
482
+ errors.push(`Change ${change.id} has no specs/**/spec.md delta files.`);
483
+ }
484
+ }
485
+ return {
486
+ provider: effectiveSpec?.source ?? "none",
487
+ swSpecExists,
488
+ openSpecExists,
489
+ currentSpecs,
490
+ changes,
491
+ effectiveSpec,
492
+ errors,
493
+ warnings
494
+ };
495
+ }
496
+ function buildOpenSpecProjectionSync(directory, options = {}) {
497
+ const root = path.resolve(directory);
498
+ const currentSpecs = walkSpecFiles(root, path.join(OPENSPEC_ROOT, "specs"));
499
+ const allChanges = listOpenSpecChanges(root);
500
+ const changes = options.changeId ? allChanges.filter((change) => change.id === options.changeId) : allChanges;
501
+ const sourcePaths = [];
502
+ const warnings = [];
503
+ const usedIds = new Set;
504
+ const currentRequirements = [];
505
+ const changeRequirements = new Map;
506
+ let parsedRequirementCount = 0;
507
+ let mtimeMs = 0;
508
+ if (options.changeId && changes.length === 0)
509
+ return null;
510
+ if (currentSpecs.length === 0 && changes.length === 0)
511
+ return null;
512
+ for (const artifact of currentSpecs) {
513
+ const abs = path.join(root, artifact.relPath);
514
+ const content2 = readTextBounded(abs);
515
+ if (content2 === null) {
516
+ warnings.push(`Skipped unreadable or oversized spec ${artifact.relPath}.`);
517
+ continue;
518
+ }
519
+ sourcePaths.push(artifact.relPath);
520
+ mtimeMs = Math.max(mtimeMs, artifact.mtimeMs);
521
+ const parsed = parseRequirements(content2, artifact.relPath, "CURRENT");
522
+ currentRequirements.push(...parsed);
523
+ parsedRequirementCount += parsed.length;
524
+ }
525
+ for (const change of changes) {
526
+ const reqs = [];
527
+ for (const artifact of change.specs) {
528
+ const abs = path.join(root, artifact.relPath);
529
+ const content2 = readTextBounded(abs);
530
+ if (content2 === null) {
531
+ warnings.push(`Skipped unreadable or oversized spec ${artifact.relPath}.`);
532
+ continue;
533
+ }
534
+ sourcePaths.push(artifact.relPath);
535
+ mtimeMs = Math.max(mtimeMs, artifact.mtimeMs);
536
+ const parsed = parseRequirements(content2, artifact.relPath, "ADDED");
537
+ reqs.push(...parsed);
538
+ parsedRequirementCount += parsed.length;
539
+ }
540
+ changeRequirements.set(change.id, reqs);
541
+ }
542
+ if (sourcePaths.length > 0 && parsedRequirementCount === 0) {
543
+ return null;
544
+ }
545
+ const lines = [
546
+ "# Specification: Effective SDD Projection",
547
+ "",
548
+ "Generated from OpenSpec-compatible artifacts. Update the source artifacts, then run `/swarm sdd project` to refresh this projection.",
549
+ "",
550
+ "## Source Artifacts",
551
+ ...sourcePaths.map((rel) => `- ${rel}`),
552
+ "",
553
+ "## Current Requirements"
554
+ ];
555
+ if (currentRequirements.length === 0) {
556
+ lines.push("- No current OpenSpec requirements found in source artifacts.");
557
+ warnings.push("No current requirements found; projection includes an advisory note only.");
558
+ } else {
559
+ for (const req of currentRequirements) {
560
+ lines.push(renderRequirement(req, usedIds, warnings));
561
+ }
562
+ }
563
+ for (const [changeId, reqs] of changeRequirements.entries()) {
564
+ lines.push("", `## Pending Change: ${changeId}`);
565
+ if (reqs.length === 0) {
566
+ lines.push(`- ${nextFrId(usedIds, warnings)} SHOULD add OpenSpec delta requirements for change ${changeId}.`);
567
+ warnings.push(`Change ${changeId} contains no parsable requirements.`);
568
+ continue;
569
+ }
570
+ for (const req of reqs) {
571
+ lines.push(renderRequirement(req, usedIds, warnings));
572
+ }
573
+ }
574
+ const content = `${lines.join(`
575
+ `)}
576
+ `;
577
+ if (content.length > MAX_SPEC_BYTES) {
578
+ warnings.push(`Projected spec exceeds ${MAX_SPEC_BYTES} bytes; refusing to use projection.`);
579
+ return null;
580
+ }
581
+ const validation = validateSpecContent(content);
582
+ if (!validation.valid) {
583
+ warnings.push(...validation.issues.map((issue) => `Projection line ${issue.line}: ${issue.message}`));
584
+ }
585
+ return {
586
+ source: "openspec_projection",
587
+ content,
588
+ hash: hash(content),
589
+ mtime: mtimeMs > 0 ? new Date(mtimeMs).toISOString() : null,
590
+ sourcePaths,
591
+ warnings
592
+ };
593
+ }
594
+ function readEffectiveSpecSync(directory, opts) {
595
+ const root = path.resolve(directory);
596
+ const swSpecPath = path.join(root, SWARM_SPEC_REL);
597
+ try {
598
+ const stat = fs.lstatSync(swSpecPath);
599
+ if (stat.isFile() && stat.size <= MAX_SPEC_BYTES) {
600
+ const content = fs.readFileSync(swSpecPath, "utf-8");
601
+ return {
602
+ source: "swarm",
603
+ content,
604
+ hash: hash(content),
605
+ mtime: stat.mtime.toISOString(),
606
+ sourcePaths: [toPosix(SWARM_SPEC_REL)],
607
+ warnings: []
608
+ };
609
+ }
610
+ } catch (error) {
611
+ if (error.code !== "ENOENT") {
612
+ throw error;
613
+ }
614
+ }
615
+ if (opts?.source) {
616
+ switch (opts.source) {
617
+ case "openspec":
618
+ return buildOpenSpecProjectionSync(root);
619
+ case "speckit":
620
+ return buildSpeckitProjectionSync(root, { feature: opts.feature });
621
+ case "swarm":
622
+ return null;
623
+ }
624
+ }
625
+ const speckitDetection = detectSpeckit(root);
626
+ if (!speckitDetection.markerPresent) {
627
+ return buildOpenSpecProjectionSync(root);
628
+ }
629
+ const speckitPresent = speckitDetection.features.length > 0;
630
+ if (!speckitPresent) {
631
+ return buildOpenSpecProjectionSync(root);
632
+ }
633
+ const openspecProjection = buildOpenSpecProjectionSync(root);
634
+ const openspecPresent = openspecProjection !== null;
635
+ if (openspecPresent) {
636
+ console.warn("[opencode-swarm] Multiple SDD sources detected (openspec and speckit). " + "Enforcement/projection is suppressed until you disambiguate. " + "Pass --source openspec or --source speckit to select a provider.");
637
+ return null;
638
+ }
639
+ return buildSpeckitProjectionSync(root, { feature: opts?.feature });
640
+ }
641
+ function writeProjectedSpecSync(directory, options = {}) {
642
+ const root = path.resolve(directory);
643
+ const projection = options.source === "speckit" ? buildSpeckitProjectionSync(root, { feature: options.feature }) : buildOpenSpecProjectionSync(root, { changeId: options.changeId });
644
+ const target = path.join(root, SWARM_SPEC_REL);
645
+ if (!projection || options.dryRun) {
646
+ return { written: false, projection, path: target };
647
+ }
648
+ fs.mkdirSync(path.dirname(target), { recursive: true });
649
+ let archivePath;
650
+ if (fs.existsSync(target)) {
651
+ const prior = fs.readFileSync(target, "utf-8");
652
+ if (prior !== projection.content) {
653
+ const archiveDir = path.join(root, ".swarm", "spec-archive");
654
+ fs.mkdirSync(archiveDir, { recursive: true });
655
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
656
+ archivePath = path.join(archiveDir, `sdd-projection-${stamp}.md`);
657
+ fs.writeFileSync(archivePath, prior, "utf-8");
658
+ }
659
+ }
660
+ const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
661
+ fs.writeFileSync(tmp, projection.content, "utf-8");
662
+ fs.renameSync(tmp, target);
663
+ return { written: true, projection, archivePath, path: target };
664
+ }
665
+ function validateSpeckit(directory, options = {}) {
666
+ const root = path.resolve(directory);
667
+ const resolution = resolveSpeckitProjection(root, options);
668
+ const problems = [];
669
+ if (resolution.kind !== "ok" && resolution.kind !== "zero_requirements") {
670
+ return { resolution, problems };
671
+ }
672
+ if (resolution.kind === "zero_requirements") {
673
+ problems.push(`Feature '${resolution.feature}' contains no parsable functional requirements.`);
674
+ }
675
+ const detection = detectSpeckit(root);
676
+ const featureEntry = detection.features.find((f) => f.featureId === resolution.feature);
677
+ if (!featureEntry) {
678
+ return { resolution, problems };
679
+ }
680
+ const specAbs = path.join(root, featureEntry.specRelPath);
681
+ let specContent = null;
682
+ try {
683
+ specContent = readTextBounded(specAbs);
684
+ } catch {}
685
+ if (specContent !== null) {
686
+ const specLines = specContent.replace(/\r\n/g, `
687
+ `).split(`
688
+ `);
689
+ for (const requiredHeader of SPECKIT_REQUIRED_SECTIONS) {
690
+ if (!specLines.some((line) => line.trimEnd() === requiredHeader)) {
691
+ problems.push(`Missing required spec.md section: ${requiredHeader}`);
692
+ }
693
+ }
694
+ }
695
+ const tasksAbs = path.join(path.dirname(specAbs), "tasks.md");
696
+ let tasksContent = null;
697
+ try {
698
+ tasksContent = readTextBounded(tasksAbs);
699
+ } catch {}
700
+ if (tasksContent !== null) {
701
+ const tasksLines = tasksContent.replace(/\r\n/g, `
702
+ `).split(`
703
+ `);
704
+ for (const line of tasksLines) {
705
+ const taskMatch = line.match(/^\s*-\s+\[[ xX]\]\s+(T\d+)/);
706
+ if (!taskMatch)
707
+ continue;
708
+ const taskId = taskMatch[1];
709
+ const hasStoryRef = /\[US\d+\]/i.test(line);
710
+ const hasReqRef = /\bFR-(?!000)\d{3}\b/i.test(line);
711
+ if (!hasStoryRef && !hasReqRef) {
712
+ problems.push(`Task ${taskId} has no spec/requirement reference.`);
713
+ }
714
+ }
715
+ }
716
+ return { resolution, problems };
717
+ }
718
+
719
+ export { detectSpeckit, resolveSpeckitProjection, loadSddStatusSync, buildOpenSpecProjectionSync, readEffectiveSpecSync, writeProjectedSpecSync, validateSpeckit };