@sudocode-ai/integration-speckit 0.1.14

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 (53) hide show
  1. package/dist/id-generator.d.ts +149 -0
  2. package/dist/id-generator.d.ts.map +1 -0
  3. package/dist/id-generator.js +197 -0
  4. package/dist/id-generator.js.map +1 -0
  5. package/dist/index.d.ts +33 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +1017 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/parser/index.d.ts +11 -0
  10. package/dist/parser/index.d.ts.map +1 -0
  11. package/dist/parser/index.js +16 -0
  12. package/dist/parser/index.js.map +1 -0
  13. package/dist/parser/markdown-utils.d.ts +138 -0
  14. package/dist/parser/markdown-utils.d.ts.map +1 -0
  15. package/dist/parser/markdown-utils.js +283 -0
  16. package/dist/parser/markdown-utils.js.map +1 -0
  17. package/dist/parser/plan-parser.d.ts +97 -0
  18. package/dist/parser/plan-parser.d.ts.map +1 -0
  19. package/dist/parser/plan-parser.js +286 -0
  20. package/dist/parser/plan-parser.js.map +1 -0
  21. package/dist/parser/spec-parser.d.ts +95 -0
  22. package/dist/parser/spec-parser.d.ts.map +1 -0
  23. package/dist/parser/spec-parser.js +250 -0
  24. package/dist/parser/spec-parser.js.map +1 -0
  25. package/dist/parser/supporting-docs.d.ts +119 -0
  26. package/dist/parser/supporting-docs.d.ts.map +1 -0
  27. package/dist/parser/supporting-docs.js +324 -0
  28. package/dist/parser/supporting-docs.js.map +1 -0
  29. package/dist/parser/tasks-parser.d.ts +171 -0
  30. package/dist/parser/tasks-parser.d.ts.map +1 -0
  31. package/dist/parser/tasks-parser.js +281 -0
  32. package/dist/parser/tasks-parser.js.map +1 -0
  33. package/dist/relationship-mapper.d.ts +165 -0
  34. package/dist/relationship-mapper.d.ts.map +1 -0
  35. package/dist/relationship-mapper.js +238 -0
  36. package/dist/relationship-mapper.js.map +1 -0
  37. package/dist/watcher.d.ts +137 -0
  38. package/dist/watcher.d.ts.map +1 -0
  39. package/dist/watcher.js +599 -0
  40. package/dist/watcher.js.map +1 -0
  41. package/dist/writer/index.d.ts +8 -0
  42. package/dist/writer/index.d.ts.map +1 -0
  43. package/dist/writer/index.js +10 -0
  44. package/dist/writer/index.js.map +1 -0
  45. package/dist/writer/spec-writer.d.ts +70 -0
  46. package/dist/writer/spec-writer.d.ts.map +1 -0
  47. package/dist/writer/spec-writer.js +261 -0
  48. package/dist/writer/spec-writer.js.map +1 -0
  49. package/dist/writer/tasks-writer.d.ts +47 -0
  50. package/dist/writer/tasks-writer.d.ts.map +1 -0
  51. package/dist/writer/tasks-writer.js +161 -0
  52. package/dist/writer/tasks-writer.js.map +1 -0
  53. package/package.json +42 -0
package/dist/index.js ADDED
@@ -0,0 +1,1017 @@
1
+ /**
2
+ * Spec-Kit Integration Plugin for sudocode
3
+ *
4
+ * Provides integration with spec-kit - a markdown-based specification system.
5
+ * Syncs specs, plans, and tasks to sudocode's specs and issues.
6
+ */
7
+ // Re-export ID generation utilities
8
+ export { generateSpecId, generateTaskIssueId, parseSpecId, isValidSpecKitId, extractFeatureNumber, extractFileType, getFeatureSpecId, getFeaturePlanId, getFeatureTasksId, } from "./id-generator.js";
9
+ // Re-export relationship mapping utilities
10
+ export { mapFeatureRelationships, mapTaskDependencies, mapSupportingDocRelationships, mapPlanToSpecRelationship, mapTaskToPlanRelationship, getStandardSupportingDocTypes, createContractDocInfo, } from "./relationship-mapper.js";
11
+ // Re-export writer utilities
12
+ export { updateTaskStatus, getTaskStatus, getAllTaskStatuses, updateSpecContent, getSpecTitle, getSpecStatus, } from "./writer/index.js";
13
+ // Re-export watcher
14
+ export { SpecKitWatcher, } from "./watcher.js";
15
+ // Re-export parser utilities
16
+ export {
17
+ // Markdown utilities
18
+ PATTERNS, extractMetadata, extractTitle, extractTitleWithPrefixRemoval, extractMetadataValue, extractCrossReferences, findContentStartIndex, extractSection, parseDate, escapeRegex, cleanTaskDescription, normalizeStatus,
19
+ // Spec parser
20
+ parseSpec, parseSpecContent, isSpecFile, getSpecFileTitle, getSpecFileStatus,
21
+ // Plan parser
22
+ parsePlan, parsePlanContent, isPlanFile, getPlanFileTitle, getPlanFileStatus,
23
+ // Tasks parser
24
+ parseTasks, parseTasksContent, getAllTasks, getTaskById, getIncompleteTasks, getParallelizableTasks, getTasksByPhase, getTasksByUserStory, isTasksFile, getTaskStats,
25
+ // Supporting documents parser
26
+ parseResearch, parseDataModel, parseSupportingDoc, parseContract, parseContractsDirectory, discoverSupportingDocs, detectDocType, } from "./parser/index.js";
27
+ import { existsSync, readFileSync, readdirSync } from "fs";
28
+ import * as path from "path";
29
+ import { createHash } from "crypto";
30
+ import { parseSpecId, generateSpecId, generateTaskIssueId, getFeatureSpecId, getFeaturePlanId, } from "./id-generator.js";
31
+ import { updateTaskStatus, updateSpecContent } from "./writer/index.js";
32
+ import { parseSpec } from "./parser/spec-parser.js";
33
+ import { parsePlan } from "./parser/plan-parser.js";
34
+ import { parseTasks } from "./parser/tasks-parser.js";
35
+ import { discoverSupportingDocs, } from "./parser/supporting-docs.js";
36
+ import { SpecKitWatcher } from "./watcher.js";
37
+ /**
38
+ * Configuration schema for UI form generation
39
+ */
40
+ const configSchema = {
41
+ type: "object",
42
+ properties: {
43
+ path: {
44
+ type: "string",
45
+ title: "Spec-Kit Path",
46
+ description: "Path to the .specify directory (relative to project root)",
47
+ default: ".specify",
48
+ required: true,
49
+ },
50
+ spec_prefix: {
51
+ type: "string",
52
+ title: "Spec Prefix",
53
+ description: "Prefix for spec IDs imported from spec-kit",
54
+ default: "sk",
55
+ },
56
+ task_prefix: {
57
+ type: "string",
58
+ title: "Task Prefix",
59
+ description: "Prefix for task IDs imported from spec-kit",
60
+ default: "skt",
61
+ },
62
+ include_supporting_docs: {
63
+ type: "boolean",
64
+ title: "Include Supporting Docs",
65
+ description: "Include research.md, data-model.md, and contracts/*.md",
66
+ default: true,
67
+ },
68
+ include_constitution: {
69
+ type: "boolean",
70
+ title: "Include Constitution",
71
+ description: "Include constitution.md as root project spec",
72
+ default: true,
73
+ },
74
+ },
75
+ required: ["path"],
76
+ };
77
+ /**
78
+ * Spec-kit integration plugin
79
+ */
80
+ const specKitPlugin = {
81
+ name: "spec-kit",
82
+ displayName: "Spec-Kit",
83
+ version: "0.1.0",
84
+ description: "Integration with spec-kit markdown-based specification system",
85
+ configSchema,
86
+ validateConfig(options) {
87
+ const errors = [];
88
+ const warnings = [];
89
+ // Check required path field
90
+ if (!options.path || typeof options.path !== "string") {
91
+ errors.push("spec-kit.options.path is required");
92
+ }
93
+ // Validate spec_prefix if provided
94
+ if (options.spec_prefix !== undefined) {
95
+ if (typeof options.spec_prefix !== "string") {
96
+ errors.push("spec-kit.options.spec_prefix must be a string");
97
+ }
98
+ else if (!/^[a-z]{1,4}$/i.test(options.spec_prefix)) {
99
+ warnings.push("spec-kit.options.spec_prefix should be 1-4 alphabetic characters");
100
+ }
101
+ }
102
+ // Validate task_prefix if provided
103
+ if (options.task_prefix !== undefined) {
104
+ if (typeof options.task_prefix !== "string") {
105
+ errors.push("spec-kit.options.task_prefix must be a string");
106
+ }
107
+ else if (!/^[a-z]{1,4}$/i.test(options.task_prefix)) {
108
+ warnings.push("spec-kit.options.task_prefix should be 1-4 alphabetic characters");
109
+ }
110
+ }
111
+ // Validate boolean options
112
+ if (options.include_supporting_docs !== undefined &&
113
+ typeof options.include_supporting_docs !== "boolean") {
114
+ errors.push("spec-kit.options.include_supporting_docs must be a boolean");
115
+ }
116
+ if (options.include_constitution !== undefined &&
117
+ typeof options.include_constitution !== "boolean") {
118
+ errors.push("spec-kit.options.include_constitution must be a boolean");
119
+ }
120
+ return {
121
+ valid: errors.length === 0,
122
+ errors,
123
+ warnings,
124
+ };
125
+ },
126
+ async testConnection(options, projectPath) {
127
+ const specKitPath = options.path;
128
+ if (!specKitPath) {
129
+ return {
130
+ success: false,
131
+ configured: true,
132
+ enabled: true,
133
+ error: "Spec-kit path is not configured",
134
+ };
135
+ }
136
+ const resolvedPath = path.resolve(projectPath, specKitPath);
137
+ if (!existsSync(resolvedPath)) {
138
+ return {
139
+ success: false,
140
+ configured: true,
141
+ enabled: true,
142
+ error: `Spec-kit directory not found: ${resolvedPath}`,
143
+ details: { path: specKitPath, resolvedPath },
144
+ };
145
+ }
146
+ // Check for specs subdirectory
147
+ const specsPath = path.join(resolvedPath, "specs");
148
+ const hasSpecs = existsSync(specsPath);
149
+ // Check for common spec-kit files
150
+ const hasConstitution = existsSync(path.join(resolvedPath, "constitution.md"));
151
+ return {
152
+ success: true,
153
+ configured: true,
154
+ enabled: true,
155
+ details: {
156
+ path: specKitPath,
157
+ resolvedPath,
158
+ hasSpecsDirectory: hasSpecs,
159
+ hasConstitution,
160
+ },
161
+ };
162
+ },
163
+ createProvider(options, projectPath) {
164
+ return new SpecKitProvider(options, projectPath);
165
+ },
166
+ };
167
+ /**
168
+ * Spec-kit provider implementation
169
+ */
170
+ class SpecKitProvider {
171
+ name = "spec-kit";
172
+ supportsWatch = true;
173
+ supportsPolling = true;
174
+ options;
175
+ projectPath;
176
+ resolvedPath;
177
+ // Change tracking for getChangesSince
178
+ entityHashes = new Map();
179
+ // File watcher instance
180
+ watcher = null;
181
+ constructor(options, projectPath) {
182
+ this.options = options;
183
+ this.projectPath = projectPath;
184
+ this.resolvedPath = path.resolve(projectPath, options.path);
185
+ }
186
+ async initialize() {
187
+ console.log(`[spec-kit] Initializing provider for path: ${this.resolvedPath}`);
188
+ if (!existsSync(this.resolvedPath)) {
189
+ throw new Error(`Spec-kit directory not found: ${this.resolvedPath}`);
190
+ }
191
+ // Check for specs subdirectory
192
+ const specsDir = path.join(this.resolvedPath, "specs");
193
+ if (!existsSync(specsDir)) {
194
+ console.warn(`[spec-kit] Note: specs directory does not exist yet at ${specsDir}`);
195
+ }
196
+ // Note: We intentionally do NOT pre-populate entityHashes here.
197
+ // This allows getChangesSince to detect all existing entities as "new" on first sync,
198
+ // enabling auto-import of existing spec-kit entities into sudocode.
199
+ // The hash cache gets populated as entities are synced/detected.
200
+ console.log(`[spec-kit] Provider initialized successfully (hash cache empty for fresh import)`);
201
+ }
202
+ async validate() {
203
+ const errors = [];
204
+ if (!existsSync(this.resolvedPath)) {
205
+ errors.push(`Spec-kit directory not found: ${this.resolvedPath}`);
206
+ return { valid: false, errors };
207
+ }
208
+ // Check for specs subdirectory
209
+ const specsDir = path.join(this.resolvedPath, "specs");
210
+ if (!existsSync(specsDir)) {
211
+ // Not an error - directory might not exist yet, but warn
212
+ console.log(`[spec-kit] Note: specs directory does not exist yet at ${specsDir}`);
213
+ }
214
+ const valid = errors.length === 0;
215
+ console.log(`[spec-kit] Validation result: valid=${valid}, errors=${errors.length}`);
216
+ return { valid, errors };
217
+ }
218
+ async dispose() {
219
+ console.log(`[spec-kit] Disposing provider`);
220
+ this.stopWatching();
221
+ // Clear entity state cache
222
+ this.entityHashes.clear();
223
+ console.log(`[spec-kit] Provider disposed successfully`);
224
+ }
225
+ async fetchEntity(externalId) {
226
+ console.log(`[spec-kit] fetchEntity called for: ${externalId}`);
227
+ // Parse the ID to determine file type and path
228
+ const parsed = parseSpecId(externalId);
229
+ if (!parsed) {
230
+ console.warn(`[spec-kit] Invalid ID format: ${externalId}`);
231
+ return null;
232
+ }
233
+ const prefix = this.options.spec_prefix || "sk";
234
+ const taskPrefix = this.options.task_prefix || "skt";
235
+ // Check if this is a task (issue) or spec
236
+ if (parsed.isTask && parsed.featureNumber) {
237
+ // Task entity - find it in the tasks.md file
238
+ const tasksFilePath = this.getTasksFilePath(parsed.featureNumber);
239
+ if (!existsSync(tasksFilePath)) {
240
+ return null;
241
+ }
242
+ const tasksFile = parseTasks(tasksFilePath);
243
+ if (!tasksFile) {
244
+ return null;
245
+ }
246
+ const task = tasksFile.tasks.find((t) => t.taskId === parsed.fileType);
247
+ if (!task) {
248
+ return null;
249
+ }
250
+ return this.taskToExternalEntity(task, parsed.featureNumber, externalId, tasksFilePath);
251
+ }
252
+ // Spec entity - determine file path based on feature number and file type
253
+ if (parsed.featureNumber) {
254
+ const filePath = this.getSpecFilePath(parsed.featureNumber, parsed.fileType);
255
+ if (!existsSync(filePath)) {
256
+ return null;
257
+ }
258
+ // Get the feature directory name for title generation
259
+ const featureDirName = this.getFeatureDirName(parsed.featureNumber);
260
+ // Parse based on file type
261
+ if (parsed.fileType === "spec") {
262
+ const spec = parseSpec(filePath);
263
+ if (!spec)
264
+ return null;
265
+ return this.specToExternalEntity(spec, externalId, featureDirName || undefined, "spec");
266
+ }
267
+ else if (parsed.fileType === "plan") {
268
+ const plan = parsePlan(filePath);
269
+ if (!plan)
270
+ return null;
271
+ return this.planToExternalEntity(plan, externalId, featureDirName || undefined, parsed.featureNumber || undefined);
272
+ }
273
+ else if (parsed.fileType === "research" ||
274
+ parsed.fileType === "data-model") {
275
+ const featureDir = path.dirname(filePath);
276
+ const docs = discoverSupportingDocs(featureDir);
277
+ const doc = parsed.fileType === "research" ? docs.research : docs.dataModel;
278
+ if (!doc)
279
+ return null;
280
+ return this.supportingDocToExternalEntity(doc, externalId, featureDirName || undefined, parsed.featureNumber || undefined);
281
+ }
282
+ else if (parsed.fileType.startsWith("contract-")) {
283
+ const featureDir = path.dirname(filePath);
284
+ const contractsDir = path.join(featureDir, "contracts");
285
+ const contractName = parsed.fileType.replace("contract-", "");
286
+ const docs = discoverSupportingDocs(featureDir);
287
+ const contract = docs.contracts.find((c) => c.name === contractName);
288
+ if (!contract)
289
+ return null;
290
+ return this.contractToExternalEntity(contract, externalId, featureDirName || undefined, parsed.featureNumber || undefined);
291
+ }
292
+ }
293
+ else {
294
+ // Non-feature file (e.g., constitution.md)
295
+ if (parsed.fileType === "constitution") {
296
+ const constitutionPath = path.join(this.resolvedPath, "memory", "constitution.md");
297
+ if (!existsSync(constitutionPath)) {
298
+ return null;
299
+ }
300
+ const spec = parseSpec(constitutionPath);
301
+ if (!spec)
302
+ return null;
303
+ return this.specToExternalEntity(spec, externalId, undefined, "constitution");
304
+ }
305
+ }
306
+ return null;
307
+ }
308
+ async searchEntities(query) {
309
+ console.log(`[spec-kit] searchEntities called with query: ${query}`);
310
+ const entities = [];
311
+ const prefix = this.options.spec_prefix || "sk";
312
+ const taskPrefix = this.options.task_prefix || "skt";
313
+ const includeSupportingDocs = this.options.include_supporting_docs !== false;
314
+ const includeConstitution = this.options.include_constitution !== false;
315
+ // Scan specs directory for feature directories
316
+ const specsDir = path.join(this.resolvedPath, "specs");
317
+ if (existsSync(specsDir)) {
318
+ try {
319
+ const entries = readdirSync(specsDir, { withFileTypes: true });
320
+ for (const entry of entries) {
321
+ if (!entry.isDirectory())
322
+ continue;
323
+ // Feature directories match pattern like "001-auth", "002-payments"
324
+ const featureMatch = entry.name.match(/^(\d+)-/);
325
+ if (!featureMatch)
326
+ continue;
327
+ const featureNumber = featureMatch[1];
328
+ const featureDir = path.join(specsDir, entry.name);
329
+ // Parse spec.md
330
+ const specPath = path.join(featureDir, "spec.md");
331
+ if (existsSync(specPath)) {
332
+ const spec = parseSpec(specPath);
333
+ if (spec) {
334
+ const specId = generateSpecId(`specs/${entry.name}/spec.md`, prefix);
335
+ const entity = this.specToExternalEntity(spec, specId, entry.name, "spec");
336
+ if (this.matchesQuery(entity, query)) {
337
+ entities.push(entity);
338
+ }
339
+ }
340
+ }
341
+ // Parse plan.md
342
+ const planPath = path.join(featureDir, "plan.md");
343
+ if (existsSync(planPath)) {
344
+ const plan = parsePlan(planPath);
345
+ if (plan) {
346
+ const planId = generateSpecId(`specs/${entry.name}/plan.md`, prefix);
347
+ const entity = this.planToExternalEntity(plan, planId, entry.name, featureNumber);
348
+ if (this.matchesQuery(entity, query)) {
349
+ entities.push(entity);
350
+ }
351
+ }
352
+ }
353
+ // Parse tasks.md - each task becomes an Issue
354
+ const tasksPath = path.join(featureDir, "tasks.md");
355
+ if (existsSync(tasksPath)) {
356
+ const tasksFile = parseTasks(tasksPath);
357
+ if (tasksFile) {
358
+ for (const task of tasksFile.tasks) {
359
+ const taskIssueId = generateTaskIssueId(featureNumber, task.taskId, taskPrefix);
360
+ const entity = this.taskToExternalEntity(task, featureNumber, taskIssueId, tasksPath);
361
+ if (this.matchesQuery(entity, query)) {
362
+ entities.push(entity);
363
+ }
364
+ }
365
+ }
366
+ }
367
+ // Optionally include supporting docs
368
+ if (includeSupportingDocs) {
369
+ const docs = discoverSupportingDocs(featureDir);
370
+ if (docs.research) {
371
+ const docId = generateSpecId(`specs/${entry.name}/research.md`, prefix);
372
+ const entity = this.supportingDocToExternalEntity(docs.research, docId, entry.name, featureNumber);
373
+ if (this.matchesQuery(entity, query)) {
374
+ entities.push(entity);
375
+ }
376
+ }
377
+ if (docs.dataModel) {
378
+ const docId = generateSpecId(`specs/${entry.name}/data-model.md`, prefix);
379
+ const entity = this.supportingDocToExternalEntity(docs.dataModel, docId, entry.name, featureNumber);
380
+ if (this.matchesQuery(entity, query)) {
381
+ entities.push(entity);
382
+ }
383
+ }
384
+ for (const contract of docs.contracts) {
385
+ const docId = `${prefix}-${featureNumber}-contract-${contract.name}`;
386
+ const entity = this.contractToExternalEntity(contract, docId, entry.name, featureNumber);
387
+ if (this.matchesQuery(entity, query)) {
388
+ entities.push(entity);
389
+ }
390
+ }
391
+ for (const other of docs.other) {
392
+ const docId = generateSpecId(`specs/${entry.name}/${other.fileName}.md`, prefix);
393
+ const entity = this.supportingDocToExternalEntity(other, docId, entry.name, featureNumber);
394
+ if (this.matchesQuery(entity, query)) {
395
+ entities.push(entity);
396
+ }
397
+ }
398
+ }
399
+ }
400
+ }
401
+ catch (error) {
402
+ console.error(`[spec-kit] Error scanning specs directory:`, error);
403
+ }
404
+ }
405
+ // Include constitution.md if configured
406
+ if (includeConstitution) {
407
+ const constitutionPath = path.join(this.resolvedPath, "memory", "constitution.md");
408
+ if (existsSync(constitutionPath)) {
409
+ const spec = parseSpec(constitutionPath);
410
+ if (spec) {
411
+ const constitutionId = `${prefix}-constitution`;
412
+ // Use "constitution" as title (no feature dir for global files)
413
+ const entity = this.specToExternalEntity(spec, constitutionId, undefined, "constitution");
414
+ if (this.matchesQuery(entity, query)) {
415
+ entities.push(entity);
416
+ }
417
+ }
418
+ }
419
+ }
420
+ console.log(`[spec-kit] searchEntities found ${entities.length} entities`);
421
+ return entities;
422
+ }
423
+ async createEntity(entity) {
424
+ // Spec-kit uses file-based storage, so creating entities is not directly supported
425
+ // Entities are created by adding files to the spec-kit directory structure
426
+ console.log(`[spec-kit] createEntity called:`, entity.title);
427
+ throw new Error("createEntity not supported: spec-kit entities are created by adding files to the .specify directory");
428
+ }
429
+ async updateEntity(externalId, entity) {
430
+ console.log(`[spec-kit] updateEntity called for ${externalId}:`, entity);
431
+ // Parse the external ID to determine what type of entity this is
432
+ const parsed = parseSpecId(externalId);
433
+ if (!parsed) {
434
+ throw new Error(`Invalid spec-kit ID format: ${externalId}`);
435
+ }
436
+ // Handle task updates (issues that map to tasks.md entries)
437
+ if (parsed.isTask && parsed.featureNumber) {
438
+ await this.updateTaskEntity(parsed.featureNumber, parsed.fileType, entity);
439
+ return;
440
+ }
441
+ // Handle spec/plan updates
442
+ if (parsed.featureNumber) {
443
+ await this.updateSpecEntity(parsed.featureNumber, parsed.fileType, entity);
444
+ return;
445
+ }
446
+ // Handle non-feature files (e.g., constitution.md)
447
+ await this.updateNonFeatureEntity(parsed.fileType, entity);
448
+ }
449
+ async deleteEntity(externalId) {
450
+ // Spec-kit uses file-based storage, so deleting entities is not directly supported
451
+ // Entities are deleted by removing files from the spec-kit directory structure
452
+ console.log(`[spec-kit] deleteEntity called for: ${externalId}`);
453
+ throw new Error("deleteEntity not supported: spec-kit entities are deleted by removing files from the .specify directory");
454
+ }
455
+ /**
456
+ * Update a task entity (corresponds to a line in tasks.md)
457
+ */
458
+ async updateTaskEntity(featureNumber, taskId, entity) {
459
+ const tasksFilePath = this.getTasksFilePath(featureNumber);
460
+ if (!existsSync(tasksFilePath)) {
461
+ throw new Error(`Tasks file not found: ${tasksFilePath}`);
462
+ }
463
+ // Check if this is an issue with status
464
+ const issue = entity;
465
+ if (issue.status !== undefined) {
466
+ const completed = issue.status === "closed";
467
+ const result = updateTaskStatus(tasksFilePath, taskId, completed);
468
+ if (!result.success) {
469
+ throw new Error(result.error || "Failed to update task status");
470
+ }
471
+ // Update hash cache to prevent false change detection
472
+ this.updateHashCache(tasksFilePath);
473
+ console.log(`[spec-kit] Updated task ${taskId} in feature ${featureNumber}: ${result.previousStatus} -> ${result.newStatus}`);
474
+ }
475
+ }
476
+ /**
477
+ * Update a spec/plan entity
478
+ */
479
+ async updateSpecEntity(featureNumber, fileType, entity) {
480
+ const filePath = this.getSpecFilePath(featureNumber, fileType);
481
+ if (!existsSync(filePath)) {
482
+ throw new Error(`Spec file not found: ${filePath}`);
483
+ }
484
+ const spec = entity;
485
+ const updates = {};
486
+ if (spec.title !== undefined) {
487
+ updates.title = spec.title;
488
+ }
489
+ // Map sudocode priority/status to spec-kit status if applicable
490
+ // Note: spec-kit doesn't have a standard status field, but we support it if present
491
+ const issue = entity;
492
+ if (issue.status !== undefined) {
493
+ updates.status = this.mapStatusToSpecKit(issue.status);
494
+ }
495
+ if (spec.content !== undefined) {
496
+ updates.content = spec.content;
497
+ }
498
+ const result = updateSpecContent(filePath, updates);
499
+ if (!result.success) {
500
+ throw new Error(result.error || "Failed to update spec content");
501
+ }
502
+ // Update hash cache to prevent false change detection
503
+ this.updateHashCache(filePath);
504
+ console.log(`[spec-kit] Updated ${fileType} for feature ${featureNumber}:`, result.changes);
505
+ }
506
+ /**
507
+ * Update a non-feature entity (e.g., constitution.md)
508
+ */
509
+ async updateNonFeatureEntity(fileType, entity) {
510
+ // Map file type to path
511
+ let filePath;
512
+ if (fileType === "constitution") {
513
+ filePath = path.join(this.resolvedPath, "memory", "constitution.md");
514
+ }
515
+ else {
516
+ filePath = path.join(this.resolvedPath, `${fileType}.md`);
517
+ }
518
+ if (!existsSync(filePath)) {
519
+ throw new Error(`Spec file not found: ${filePath}`);
520
+ }
521
+ const spec = entity;
522
+ const updates = {};
523
+ if (spec.title !== undefined) {
524
+ updates.title = spec.title;
525
+ }
526
+ if (spec.content !== undefined) {
527
+ updates.content = spec.content;
528
+ }
529
+ const result = updateSpecContent(filePath, updates);
530
+ if (!result.success) {
531
+ throw new Error(result.error || "Failed to update spec content");
532
+ }
533
+ // Update hash cache to prevent false change detection
534
+ this.updateHashCache(filePath);
535
+ console.log(`[spec-kit] Updated ${fileType}:`, result.changes);
536
+ }
537
+ /**
538
+ * Get the path to a feature's tasks.md file
539
+ */
540
+ getTasksFilePath(featureNumber) {
541
+ // Find the feature directory by looking for XXX-* pattern
542
+ const specsDir = path.join(this.resolvedPath, "specs");
543
+ if (!existsSync(specsDir)) {
544
+ return path.join(specsDir, `${featureNumber}-unknown`, "tasks.md");
545
+ }
546
+ const dirs = readdirSync(specsDir, { withFileTypes: true })
547
+ .filter((d) => d.isDirectory())
548
+ .map((d) => d.name);
549
+ const featureDir = dirs.find((dir) => dir.startsWith(`${featureNumber}-`));
550
+ if (featureDir) {
551
+ return path.join(specsDir, featureDir, "tasks.md");
552
+ }
553
+ // Fallback: return best guess path
554
+ return path.join(specsDir, `${featureNumber}-unknown`, "tasks.md");
555
+ }
556
+ /**
557
+ * Get the path to a feature's spec file
558
+ */
559
+ getSpecFilePath(featureNumber, fileType) {
560
+ const specsDir = path.join(this.resolvedPath, "specs");
561
+ if (!existsSync(specsDir)) {
562
+ return path.join(specsDir, `${featureNumber}-unknown`, `${fileType}.md`);
563
+ }
564
+ const dirs = readdirSync(specsDir, { withFileTypes: true })
565
+ .filter((d) => d.isDirectory())
566
+ .map((d) => d.name);
567
+ const featureDir = dirs.find((dir) => dir.startsWith(`${featureNumber}-`));
568
+ if (featureDir) {
569
+ // Handle contract files
570
+ if (fileType.startsWith("contract-")) {
571
+ const contractName = fileType.replace("contract-", "");
572
+ return path.join(specsDir, featureDir, "contracts", `${contractName}.json`);
573
+ }
574
+ return path.join(specsDir, featureDir, `${fileType}.md`);
575
+ }
576
+ // Fallback
577
+ return path.join(specsDir, `${featureNumber}-unknown`, `${fileType}.md`);
578
+ }
579
+ /**
580
+ * Get the feature directory name (e.g., "001-test-feature") from a feature number
581
+ */
582
+ getFeatureDirName(featureNumber) {
583
+ const specsDir = path.join(this.resolvedPath, "specs");
584
+ if (!existsSync(specsDir)) {
585
+ return null;
586
+ }
587
+ const dirs = readdirSync(specsDir, { withFileTypes: true })
588
+ .filter((d) => d.isDirectory())
589
+ .map((d) => d.name);
590
+ return dirs.find((dir) => dir.startsWith(`${featureNumber}-`)) || null;
591
+ }
592
+ /**
593
+ * Update the hash cache for a file after writing
594
+ */
595
+ updateHashCache(filePath) {
596
+ try {
597
+ const content = readFileSync(filePath, "utf-8");
598
+ const hash = createHash("sha256").update(content).digest("hex");
599
+ this.entityHashes.set(filePath, hash);
600
+ }
601
+ catch {
602
+ // Ignore errors updating cache
603
+ }
604
+ }
605
+ /**
606
+ * Map sudocode status to spec-kit status string
607
+ */
608
+ mapStatusToSpecKit(status) {
609
+ const statusMap = {
610
+ open: "Open",
611
+ in_progress: "In Progress",
612
+ blocked: "Blocked",
613
+ needs_review: "Needs Review",
614
+ closed: "Complete",
615
+ };
616
+ return statusMap[status] || "Open";
617
+ }
618
+ async getChangesSince(timestamp) {
619
+ console.log(`[spec-kit] getChangesSince called for: ${timestamp.toISOString()}`);
620
+ console.log(`[spec-kit] getChangesSince: current entityHashes count: ${this.entityHashes.size}`);
621
+ const changes = [];
622
+ const currentEntities = await this.searchEntities();
623
+ console.log(`[spec-kit] getChangesSince: searchEntities returned ${currentEntities.length} entities`);
624
+ const currentIds = new Set();
625
+ // Check for created and updated entities
626
+ for (const entity of currentEntities) {
627
+ currentIds.add(entity.id);
628
+ const newHash = this.computeEntityHash(entity);
629
+ const cachedHash = this.entityHashes.get(entity.id);
630
+ if (!cachedHash) {
631
+ // New entity
632
+ console.log(`[spec-kit] getChangesSince: NEW entity detected: ${entity.id} (type=${entity.type})`);
633
+ changes.push({
634
+ entity_id: entity.id,
635
+ entity_type: entity.type,
636
+ change_type: "created",
637
+ timestamp: entity.created_at || new Date().toISOString(),
638
+ data: entity,
639
+ });
640
+ this.entityHashes.set(entity.id, newHash);
641
+ }
642
+ else if (newHash !== cachedHash) {
643
+ // Updated entity
644
+ console.log(`[spec-kit] getChangesSince: UPDATED entity detected: ${entity.id}`);
645
+ changes.push({
646
+ entity_id: entity.id,
647
+ entity_type: entity.type,
648
+ change_type: "updated",
649
+ timestamp: entity.updated_at || new Date().toISOString(),
650
+ data: entity,
651
+ });
652
+ this.entityHashes.set(entity.id, newHash);
653
+ }
654
+ }
655
+ // Check for deleted entities
656
+ const now = new Date().toISOString();
657
+ for (const [id, _hash] of this.entityHashes) {
658
+ if (!currentIds.has(id)) {
659
+ // Determine entity type from ID
660
+ const parsed = parseSpecId(id);
661
+ const entityType = parsed?.isTask ? "issue" : "spec";
662
+ console.log(`[spec-kit] getChangesSince: DELETED entity detected: ${id}`);
663
+ changes.push({
664
+ entity_id: id,
665
+ entity_type: entityType,
666
+ change_type: "deleted",
667
+ timestamp: now,
668
+ });
669
+ this.entityHashes.delete(id);
670
+ }
671
+ }
672
+ console.log(`[spec-kit] getChangesSince found ${changes.length} changes:`, changes.map(c => `${c.entity_id}(${c.change_type})`).join(", "));
673
+ return changes;
674
+ }
675
+ startWatching(callback) {
676
+ console.log(`[spec-kit] startWatching called`);
677
+ if (this.watcher) {
678
+ console.warn(`[spec-kit] Watcher already running`);
679
+ return;
680
+ }
681
+ this.watcher = new SpecKitWatcher({
682
+ specifyPath: this.resolvedPath,
683
+ specPrefix: this.options.spec_prefix || "sk",
684
+ taskPrefix: this.options.task_prefix || "skt",
685
+ includeSupportingDocs: this.options.include_supporting_docs !== false,
686
+ includeConstitution: this.options.include_constitution !== false,
687
+ });
688
+ this.watcher.start(callback);
689
+ console.log(`[spec-kit] Watcher started for ${this.resolvedPath}`);
690
+ }
691
+ stopWatching() {
692
+ console.log(`[spec-kit] stopWatching called`);
693
+ if (this.watcher) {
694
+ this.watcher.stop();
695
+ this.watcher = null;
696
+ console.log(`[spec-kit] Watcher stopped`);
697
+ }
698
+ }
699
+ mapToSudocode(external) {
700
+ console.log(`[spec-kit] mapToSudocode: external.type=${external.type}, title=${external.title}`);
701
+ if (external.type === "issue") {
702
+ const result = {
703
+ issue: {
704
+ title: external.title,
705
+ content: external.description || "",
706
+ priority: external.priority ?? 2,
707
+ status: this.mapStatus(external.status),
708
+ },
709
+ };
710
+ console.log(`[spec-kit] mapToSudocode: returning issue with status=${result.issue.status}`);
711
+ return result;
712
+ }
713
+ const result = {
714
+ spec: {
715
+ title: external.title,
716
+ content: external.description || "",
717
+ priority: external.priority ?? 2,
718
+ },
719
+ };
720
+ console.log(`[spec-kit] mapToSudocode: returning spec`);
721
+ return result;
722
+ }
723
+ mapFromSudocode(entity) {
724
+ const isIssue = "status" in entity;
725
+ return {
726
+ type: isIssue ? "issue" : "spec",
727
+ title: entity.title,
728
+ description: entity.content,
729
+ priority: entity.priority,
730
+ status: isIssue ? entity.status : undefined,
731
+ };
732
+ }
733
+ mapStatus(externalStatus) {
734
+ if (!externalStatus)
735
+ return "open";
736
+ const statusMap = {
737
+ open: "open",
738
+ in_progress: "in_progress",
739
+ blocked: "blocked",
740
+ needs_review: "needs_review",
741
+ closed: "closed",
742
+ done: "closed",
743
+ completed: "closed",
744
+ };
745
+ return statusMap[externalStatus.toLowerCase()] || "open";
746
+ }
747
+ // ===========================================================================
748
+ // Entity Conversion Helpers
749
+ // ===========================================================================
750
+ /**
751
+ * Capture current entity state for change detection
752
+ */
753
+ async captureEntityState() {
754
+ console.log(`[spec-kit] captureEntityState: capturing initial entity state...`);
755
+ const entities = await this.searchEntities();
756
+ this.entityHashes.clear();
757
+ for (const entity of entities) {
758
+ const hash = this.computeEntityHash(entity);
759
+ this.entityHashes.set(entity.id, hash);
760
+ }
761
+ console.log(`[spec-kit] captureEntityState: captured ${this.entityHashes.size} entities`);
762
+ }
763
+ /**
764
+ * Compute a hash for an entity to detect changes
765
+ */
766
+ computeEntityHash(entity) {
767
+ // Create a canonical representation for hashing
768
+ const canonical = JSON.stringify({
769
+ id: entity.id,
770
+ type: entity.type,
771
+ title: entity.title,
772
+ description: entity.description,
773
+ status: entity.status,
774
+ priority: entity.priority,
775
+ });
776
+ return createHash("sha256").update(canonical).digest("hex");
777
+ }
778
+ /**
779
+ * Check if an entity matches a query string
780
+ */
781
+ matchesQuery(entity, query) {
782
+ if (!query)
783
+ return true;
784
+ const lowerQuery = query.toLowerCase();
785
+ return (entity.title.toLowerCase().includes(lowerQuery) ||
786
+ (entity.description?.toLowerCase().includes(lowerQuery) ?? false));
787
+ }
788
+ /**
789
+ * Convert a parsed spec to ExternalEntity
790
+ * @param spec - Parsed spec data
791
+ * @param id - External entity ID
792
+ * @param featureDirName - Feature directory name (e.g., "001-test-feature")
793
+ * @param fileType - File type for title suffix (e.g., "spec", "plan")
794
+ */
795
+ specToExternalEntity(spec, id, featureDirName, fileType = "spec") {
796
+ // Use directory-based title: "001-test-feature (spec)" instead of extracted title
797
+ const title = featureDirName
798
+ ? `${featureDirName} (${fileType})`
799
+ : spec.title;
800
+ // Read raw file content (including frontmatter) instead of processed content
801
+ let rawContent = spec.content;
802
+ try {
803
+ rawContent = readFileSync(spec.filePath, "utf-8");
804
+ }
805
+ catch {
806
+ // Fall back to parsed content if file read fails
807
+ }
808
+ return {
809
+ id,
810
+ type: "spec",
811
+ title,
812
+ description: rawContent,
813
+ status: spec.status || undefined,
814
+ priority: this.statusToPriority(spec.status),
815
+ created_at: spec.createdAt?.toISOString(),
816
+ raw: {
817
+ rawTitle: spec.rawTitle,
818
+ featureBranch: spec.featureBranch,
819
+ metadata: Object.fromEntries(spec.metadata),
820
+ crossReferences: spec.crossReferences,
821
+ filePath: spec.filePath,
822
+ },
823
+ };
824
+ }
825
+ /**
826
+ * Convert a parsed plan to ExternalEntity
827
+ * @param plan - Parsed plan data
828
+ * @param id - External entity ID
829
+ * @param featureDirName - Feature directory name (e.g., "001-test-feature")
830
+ */
831
+ planToExternalEntity(plan, id, featureDirName, featureNumber) {
832
+ // Use directory-based title: "001-test-feature (plan)" instead of extracted title
833
+ const title = featureDirName
834
+ ? `${featureDirName} (plan)`
835
+ : plan.title;
836
+ // Read raw file content (including frontmatter) instead of processed content
837
+ let rawContent = plan.content;
838
+ try {
839
+ rawContent = readFileSync(plan.filePath, "utf-8");
840
+ }
841
+ catch {
842
+ // Fall back to parsed content if file read fails
843
+ }
844
+ // Plan implements Spec relationship
845
+ const relationships = [];
846
+ if (featureNumber) {
847
+ const specId = getFeatureSpecId(featureNumber, this.options.spec_prefix || "sk");
848
+ relationships.push({
849
+ targetId: specId,
850
+ targetType: "spec",
851
+ relationshipType: "implements",
852
+ });
853
+ }
854
+ return {
855
+ id,
856
+ type: "spec",
857
+ title,
858
+ description: rawContent,
859
+ status: plan.status || undefined,
860
+ priority: this.statusToPriority(plan.status),
861
+ created_at: plan.createdAt?.toISOString(),
862
+ relationships: relationships.length > 0 ? relationships : undefined,
863
+ raw: {
864
+ rawTitle: plan.rawTitle,
865
+ branch: plan.branch,
866
+ specReference: plan.specReference,
867
+ metadata: Object.fromEntries(plan.metadata),
868
+ crossReferences: plan.crossReferences,
869
+ filePath: plan.filePath,
870
+ },
871
+ };
872
+ }
873
+ /**
874
+ * Convert a parsed task to ExternalEntity (as issue)
875
+ */
876
+ taskToExternalEntity(task, featureNumber, id, tasksFilePath) {
877
+ const status = task.completed ? "closed" : "open";
878
+ // Task implements Plan relationship
879
+ const planId = getFeaturePlanId(featureNumber, this.options.spec_prefix || "sk");
880
+ const relationships = [
881
+ {
882
+ targetId: planId,
883
+ targetType: "spec",
884
+ relationshipType: "implements",
885
+ },
886
+ ];
887
+ return {
888
+ id,
889
+ type: "issue",
890
+ title: `${task.taskId}: ${task.description}`,
891
+ description: task.description,
892
+ status,
893
+ priority: task.parallelizable ? 1 : 2, // Parallelizable tasks get higher priority
894
+ relationships,
895
+ raw: {
896
+ taskId: task.taskId,
897
+ completed: task.completed,
898
+ parallelizable: task.parallelizable,
899
+ userStory: task.userStory,
900
+ phase: task.phase,
901
+ phaseName: task.phaseName,
902
+ lineNumber: task.lineNumber,
903
+ indentLevel: task.indentLevel,
904
+ rawLine: task.rawLine,
905
+ featureNumber,
906
+ tasksFilePath,
907
+ },
908
+ };
909
+ }
910
+ /**
911
+ * Convert a parsed supporting document to ExternalEntity
912
+ * @param doc - Parsed supporting document data
913
+ * @param id - External entity ID
914
+ * @param featureDirName - Feature directory name (e.g., "001-test-feature")
915
+ * @param featureNumber - Feature number (e.g., "001") for relationship creation
916
+ */
917
+ supportingDocToExternalEntity(doc, id, featureDirName, featureNumber) {
918
+ // Use directory-based title: "001-test-feature (research)" instead of extracted title
919
+ const title = featureDirName
920
+ ? `${featureDirName} (${doc.fileName})`
921
+ : doc.title;
922
+ // Read raw file content (including frontmatter) instead of processed content
923
+ let rawContent = doc.content;
924
+ try {
925
+ rawContent = readFileSync(doc.filePath, "utf-8");
926
+ }
927
+ catch {
928
+ // Fall back to parsed content if file read fails
929
+ }
930
+ // Supporting doc references Plan relationship
931
+ const relationships = [];
932
+ if (featureNumber) {
933
+ const planId = getFeaturePlanId(featureNumber, this.options.spec_prefix || "sk");
934
+ relationships.push({
935
+ targetId: planId,
936
+ targetType: "spec",
937
+ relationshipType: "references",
938
+ });
939
+ }
940
+ return {
941
+ id,
942
+ type: "spec",
943
+ title,
944
+ description: rawContent,
945
+ relationships: relationships.length > 0 ? relationships : undefined,
946
+ raw: {
947
+ docType: doc.type,
948
+ metadata: Object.fromEntries(doc.metadata),
949
+ crossReferences: doc.crossReferences,
950
+ filePath: doc.filePath,
951
+ fileName: doc.fileName,
952
+ fileExtension: doc.fileExtension,
953
+ },
954
+ };
955
+ }
956
+ /**
957
+ * Convert a parsed contract to ExternalEntity
958
+ * @param contract - Parsed contract data
959
+ * @param id - External entity ID
960
+ * @param featureDirName - Feature directory name (e.g., "001-test-feature")
961
+ * @param featureNumber - Feature number (e.g., "001") for relationship creation
962
+ */
963
+ contractToExternalEntity(contract, id, featureDirName, featureNumber) {
964
+ // Use directory-based title: "001-test-feature (contract-api)" instead of contract name
965
+ const title = featureDirName
966
+ ? `${featureDirName} (contract-${contract.name})`
967
+ : contract.name;
968
+ // Contract references Plan relationship
969
+ const relationships = [];
970
+ if (featureNumber) {
971
+ const planId = getFeaturePlanId(featureNumber, this.options.spec_prefix || "sk");
972
+ relationships.push({
973
+ targetId: planId,
974
+ targetType: "spec",
975
+ relationshipType: "references",
976
+ });
977
+ }
978
+ return {
979
+ id,
980
+ type: "spec",
981
+ title,
982
+ description: JSON.stringify(contract.data, null, 2),
983
+ relationships: relationships.length > 0 ? relationships : undefined,
984
+ raw: {
985
+ contractName: contract.name,
986
+ format: contract.format,
987
+ data: contract.data,
988
+ filePath: contract.filePath,
989
+ },
990
+ };
991
+ }
992
+ /**
993
+ * Map spec-kit status to sudocode priority
994
+ * Draft/Open -> lower priority, Complete -> normal
995
+ */
996
+ statusToPriority(status) {
997
+ if (!status)
998
+ return 2;
999
+ const statusLower = status.toLowerCase();
1000
+ if (statusLower === "draft" || statusLower === "open") {
1001
+ return 3; // Lower priority for drafts
1002
+ }
1003
+ if (statusLower === "in progress" ||
1004
+ statusLower === "in_progress" ||
1005
+ statusLower === "active") {
1006
+ return 1; // Higher priority for in-progress
1007
+ }
1008
+ if (statusLower === "complete" ||
1009
+ statusLower === "completed" ||
1010
+ statusLower === "done") {
1011
+ return 2; // Normal priority for complete
1012
+ }
1013
+ return 2; // Default
1014
+ }
1015
+ }
1016
+ export default specKitPlugin;
1017
+ //# sourceMappingURL=index.js.map