@backlog-md/core 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/dist/core/Core.js CHANGED
@@ -199,31 +199,85 @@ export class Core {
199
199
  async initializeLazy(filePaths) {
200
200
  if (this.lazyInitialized)
201
201
  return;
202
- // Load config
203
- const configPath = this.fs.join(this.projectRoot, "backlog", "config.yml");
204
- const configExists = await this.fs.exists(configPath);
205
- if (!configExists) {
206
- throw new Error(`Not a Backlog.md project: config.yml not found at ${configPath}`);
207
- }
208
- const configContent = await this.fs.readFile(configPath);
209
- this.config = parseBacklogConfig(configContent);
210
- // Build task index from file paths only (no file reads)
211
- this.taskIndex.clear();
212
- for (const filePath of filePaths) {
213
- if (!filePath.endsWith(".md"))
214
- continue;
215
- // Check for backlog/tasks or backlog/completed (with or without leading slash)
216
- const isTaskFile = filePath.includes("backlog/tasks/") || filePath.includes("backlog\\tasks\\");
217
- const isCompletedFile = filePath.includes("backlog/completed/") || filePath.includes("backlog\\completed\\");
218
- if (!isTaskFile && !isCompletedFile)
219
- continue;
220
- // Skip config.yml
221
- if (filePath.endsWith("config.yml"))
222
- continue;
223
- const indexEntry = extractTaskIndexFromPath(filePath);
224
- this.taskIndex.set(indexEntry.id, indexEntry);
225
- }
226
- this.lazyInitialized = true;
202
+ const span = this.tracer.startSpan("core.init", {
203
+ attributes: {
204
+ projectRoot: this.projectRoot,
205
+ mode: "lazy",
206
+ "input.filePathCount": filePaths.length,
207
+ },
208
+ });
209
+ return await context.with(trace.setSpan(context.active(), span), async () => {
210
+ const startTime = Date.now();
211
+ try {
212
+ span.addEvent("core.init.started", {
213
+ projectRoot: this.projectRoot,
214
+ mode: "lazy",
215
+ });
216
+ // Load config
217
+ const configPath = this.fs.join(this.projectRoot, "backlog", "config.yml");
218
+ const configExists = await this.fs.exists(configPath);
219
+ if (!configExists) {
220
+ const error = new Error(`Not a Backlog.md project: config.yml not found at ${configPath}`);
221
+ span.addEvent("core.init.error", {
222
+ "error.type": "ConfigNotFoundError",
223
+ "error.message": error.message,
224
+ stage: "config",
225
+ });
226
+ span.recordException(error);
227
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
228
+ throw error;
229
+ }
230
+ const configContent = await this.fs.readFile(configPath);
231
+ this.config = parseBacklogConfig(configContent);
232
+ span.addEvent("core.init.config.loaded", {
233
+ projectName: this.config.projectName,
234
+ statusCount: this.config.statuses.length,
235
+ labelCount: this.config.labels?.length ?? 0,
236
+ });
237
+ // Build task index from file paths only (no file reads)
238
+ this.taskIndex.clear();
239
+ let tasksIndexed = 0;
240
+ let completedIndexed = 0;
241
+ for (const filePath of filePaths) {
242
+ if (!filePath.endsWith(".md"))
243
+ continue;
244
+ // Check for backlog/tasks or backlog/completed (with or without leading slash)
245
+ const isTaskFile = filePath.includes("backlog/tasks/") || filePath.includes("backlog\\tasks\\");
246
+ const isCompletedFile = filePath.includes("backlog/completed/") || filePath.includes("backlog\\completed\\");
247
+ if (!isTaskFile && !isCompletedFile)
248
+ continue;
249
+ // Skip config.yml
250
+ if (filePath.endsWith("config.yml"))
251
+ continue;
252
+ const indexEntry = extractTaskIndexFromPath(filePath);
253
+ this.taskIndex.set(indexEntry.id, indexEntry);
254
+ if (isTaskFile)
255
+ tasksIndexed++;
256
+ if (isCompletedFile)
257
+ completedIndexed++;
258
+ }
259
+ this.lazyInitialized = true;
260
+ const duration = Date.now() - startTime;
261
+ span.addEvent("core.init.complete", {
262
+ success: true,
263
+ "duration.ms": duration,
264
+ tasksIndexed,
265
+ completedIndexed,
266
+ totalIndexed: this.taskIndex.size,
267
+ });
268
+ span.setStatus({ code: SpanStatusCode.OK });
269
+ }
270
+ catch (error) {
271
+ if (error instanceof Error) {
272
+ span.recordException(error);
273
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
274
+ }
275
+ throw error;
276
+ }
277
+ finally {
278
+ span.end();
279
+ }
280
+ });
227
281
  }
228
282
  /**
229
283
  * Check if lazy initialization is complete
@@ -535,43 +589,76 @@ export class Core {
535
589
  * @returns Created milestone
536
590
  */
537
591
  async createMilestone(input) {
538
- const milestonesDir = this.getMilestonesDir();
539
- // Ensure milestones directory exists
540
- await this.fs.createDir(milestonesDir, { recursive: true });
541
- // Find next available milestone ID
542
- const entries = await this.fs.readDir(milestonesDir).catch(() => []);
543
- const existingIds = entries
544
- .map((f) => {
545
- const match = f.match(/^m-(\d+)/);
546
- return match?.[1] ? parseInt(match[1], 10) : -1;
547
- })
548
- .filter((id) => id >= 0);
549
- const nextId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 0;
550
- const id = `m-${nextId}`;
551
- const description = input.description || `Milestone: ${input.title}`;
552
- // Create a temporary milestone to generate content
553
- const tempMilestone = {
554
- id,
555
- title: input.title,
556
- description,
557
- rawContent: "",
558
- tasks: [],
559
- };
560
- // Generate content
561
- const content = serializeMilestoneMarkdown(tempMilestone);
562
- // Create the final milestone with correct rawContent
563
- const milestone = {
564
- id,
565
- title: input.title,
566
- description,
567
- rawContent: content,
568
- tasks: [],
569
- };
570
- // Write file
571
- const filename = getMilestoneFilename(id, input.title);
572
- const filepath = this.fs.join(milestonesDir, filename);
573
- await this.fs.writeFile(filepath, content);
574
- return milestone;
592
+ const startTime = Date.now();
593
+ const span = this.tracer.startSpan("milestone.create", {
594
+ attributes: {
595
+ "input.title": input.title,
596
+ "input.hasDescription": input.description !== undefined,
597
+ },
598
+ });
599
+ return await context.with(trace.setSpan(context.active(), span), async () => {
600
+ try {
601
+ span.addEvent("milestone.create.started", {
602
+ "input.title": input.title,
603
+ "input.hasDescription": input.description !== undefined,
604
+ });
605
+ const milestonesDir = this.getMilestonesDir();
606
+ // Ensure milestones directory exists
607
+ await this.fs.createDir(milestonesDir, { recursive: true });
608
+ // Find next available milestone ID
609
+ const entries = await this.fs.readDir(milestonesDir).catch(() => []);
610
+ const existingIds = entries
611
+ .map((f) => {
612
+ const match = f.match(/^m-(\d+)/);
613
+ return match?.[1] ? parseInt(match[1], 10) : -1;
614
+ })
615
+ .filter((id) => id >= 0);
616
+ const nextId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 0;
617
+ const id = `m-${nextId}`;
618
+ const description = input.description || `Milestone: ${input.title}`;
619
+ // Create a temporary milestone to generate content
620
+ const tempMilestone = {
621
+ id,
622
+ title: input.title,
623
+ description,
624
+ rawContent: "",
625
+ tasks: [],
626
+ };
627
+ // Generate content
628
+ const content = serializeMilestoneMarkdown(tempMilestone);
629
+ // Create the final milestone with correct rawContent
630
+ const milestone = {
631
+ id,
632
+ title: input.title,
633
+ description,
634
+ rawContent: content,
635
+ tasks: [],
636
+ };
637
+ // Write file
638
+ const filename = getMilestoneFilename(id, input.title);
639
+ const filepath = this.fs.join(milestonesDir, filename);
640
+ await this.fs.writeFile(filepath, content);
641
+ span.addEvent("milestone.create.complete", {
642
+ "output.milestoneId": id,
643
+ "duration.ms": Date.now() - startTime,
644
+ });
645
+ span.setStatus({ code: SpanStatusCode.OK });
646
+ span.end();
647
+ return milestone;
648
+ }
649
+ catch (error) {
650
+ span.addEvent("milestone.create.error", {
651
+ "error.type": error instanceof Error ? error.name : "UnknownError",
652
+ "error.message": error instanceof Error ? error.message : String(error),
653
+ });
654
+ span.setStatus({
655
+ code: SpanStatusCode.ERROR,
656
+ message: error instanceof Error ? error.message : String(error),
657
+ });
658
+ span.end();
659
+ throw error;
660
+ }
661
+ });
575
662
  }
576
663
  /**
577
664
  * Update an existing milestone
@@ -581,49 +668,101 @@ export class Core {
581
668
  * @returns Updated milestone or null if not found
582
669
  */
583
670
  async updateMilestone(id, input) {
584
- const existing = await this.loadMilestone(id);
585
- if (!existing) {
586
- return null;
587
- }
588
- const milestonesDir = this.getMilestonesDir();
589
- const entries = await this.fs.readDir(milestonesDir);
590
- // Find the current file
591
- const currentFile = entries.find((entry) => {
592
- const fileId = extractMilestoneIdFromFilename(entry);
593
- return fileId === id;
671
+ const startTime = Date.now();
672
+ const span = this.tracer.startSpan("milestone.update", {
673
+ attributes: {
674
+ "input.milestoneId": id,
675
+ "input.hasTitle": input.title !== undefined,
676
+ "input.hasDescription": input.description !== undefined,
677
+ },
678
+ });
679
+ return await context.with(trace.setSpan(context.active(), span), async () => {
680
+ try {
681
+ span.addEvent("milestone.update.started", {
682
+ "input.milestoneId": id,
683
+ "input.hasTitle": input.title !== undefined,
684
+ "input.hasDescription": input.description !== undefined,
685
+ });
686
+ const existing = await this.loadMilestone(id);
687
+ if (!existing) {
688
+ span.addEvent("milestone.update.error", {
689
+ "error.type": "NotFoundError",
690
+ "error.message": "Milestone not found",
691
+ "input.milestoneId": id,
692
+ });
693
+ span.setStatus({ code: SpanStatusCode.OK });
694
+ span.end();
695
+ return null;
696
+ }
697
+ const milestonesDir = this.getMilestonesDir();
698
+ const entries = await this.fs.readDir(milestonesDir);
699
+ // Find the current file
700
+ const currentFile = entries.find((entry) => {
701
+ const fileId = extractMilestoneIdFromFilename(entry);
702
+ return fileId === id;
703
+ });
704
+ if (!currentFile) {
705
+ span.addEvent("milestone.update.error", {
706
+ "error.type": "NotFoundError",
707
+ "error.message": "Milestone file not found",
708
+ "input.milestoneId": id,
709
+ });
710
+ span.setStatus({ code: SpanStatusCode.OK });
711
+ span.end();
712
+ return null;
713
+ }
714
+ // Build updated values
715
+ const newTitle = input.title ?? existing.title;
716
+ const newDescription = input.description ?? existing.description;
717
+ const titleChanged = input.title !== undefined && input.title !== existing.title;
718
+ // Create a temporary milestone to generate content
719
+ const tempMilestone = {
720
+ id: existing.id,
721
+ title: newTitle,
722
+ description: newDescription,
723
+ rawContent: "",
724
+ tasks: existing.tasks,
725
+ };
726
+ // Generate new content
727
+ const content = serializeMilestoneMarkdown(tempMilestone);
728
+ // Create the final updated milestone
729
+ const updated = {
730
+ id: existing.id,
731
+ title: newTitle,
732
+ description: newDescription,
733
+ rawContent: content,
734
+ tasks: existing.tasks,
735
+ };
736
+ // Delete old file
737
+ const oldPath = this.fs.join(milestonesDir, currentFile);
738
+ await this.fs.deleteFile(oldPath);
739
+ // Write new file (with potentially new filename if title changed)
740
+ const newFilename = getMilestoneFilename(id, updated.title);
741
+ const newPath = this.fs.join(milestonesDir, newFilename);
742
+ await this.fs.writeFile(newPath, content);
743
+ span.addEvent("milestone.update.complete", {
744
+ "output.milestoneId": id,
745
+ "output.titleChanged": titleChanged,
746
+ "duration.ms": Date.now() - startTime,
747
+ });
748
+ span.setStatus({ code: SpanStatusCode.OK });
749
+ span.end();
750
+ return updated;
751
+ }
752
+ catch (error) {
753
+ span.addEvent("milestone.update.error", {
754
+ "error.type": error instanceof Error ? error.name : "UnknownError",
755
+ "error.message": error instanceof Error ? error.message : String(error),
756
+ "input.milestoneId": id,
757
+ });
758
+ span.setStatus({
759
+ code: SpanStatusCode.ERROR,
760
+ message: error instanceof Error ? error.message : String(error),
761
+ });
762
+ span.end();
763
+ throw error;
764
+ }
594
765
  });
595
- if (!currentFile) {
596
- return null;
597
- }
598
- // Build updated values
599
- const newTitle = input.title ?? existing.title;
600
- const newDescription = input.description ?? existing.description;
601
- // Create a temporary milestone to generate content
602
- const tempMilestone = {
603
- id: existing.id,
604
- title: newTitle,
605
- description: newDescription,
606
- rawContent: "",
607
- tasks: existing.tasks,
608
- };
609
- // Generate new content
610
- const content = serializeMilestoneMarkdown(tempMilestone);
611
- // Create the final updated milestone
612
- const updated = {
613
- id: existing.id,
614
- title: newTitle,
615
- description: newDescription,
616
- rawContent: content,
617
- tasks: existing.tasks,
618
- };
619
- // Delete old file
620
- const oldPath = this.fs.join(milestonesDir, currentFile);
621
- await this.fs.deleteFile(oldPath);
622
- // Write new file (with potentially new filename if title changed)
623
- const newFilename = getMilestoneFilename(id, updated.title);
624
- const newPath = this.fs.join(milestonesDir, newFilename);
625
- await this.fs.writeFile(newPath, content);
626
- return updated;
627
766
  }
628
767
  /**
629
768
  * Delete a milestone
@@ -632,27 +771,69 @@ export class Core {
632
771
  * @returns true if deleted, false if not found
633
772
  */
634
773
  async deleteMilestone(id) {
635
- const milestonesDir = this.getMilestonesDir();
636
- if (!(await this.fs.exists(milestonesDir))) {
637
- return false;
638
- }
639
- const entries = await this.fs.readDir(milestonesDir);
640
- // Find file matching the ID
641
- const milestoneFile = entries.find((entry) => {
642
- const fileId = extractMilestoneIdFromFilename(entry);
643
- return fileId === id;
774
+ const startTime = Date.now();
775
+ const span = this.tracer.startSpan("milestone.delete", {
776
+ attributes: {
777
+ "input.milestoneId": id,
778
+ },
779
+ });
780
+ return await context.with(trace.setSpan(context.active(), span), async () => {
781
+ try {
782
+ span.addEvent("milestone.delete.started", {
783
+ "input.milestoneId": id,
784
+ });
785
+ const milestonesDir = this.getMilestonesDir();
786
+ if (!(await this.fs.exists(milestonesDir))) {
787
+ span.addEvent("milestone.delete.error", {
788
+ "error.type": "NotFoundError",
789
+ "error.message": "Milestones directory not found",
790
+ "input.milestoneId": id,
791
+ });
792
+ span.setStatus({ code: SpanStatusCode.OK });
793
+ span.end();
794
+ return false;
795
+ }
796
+ const entries = await this.fs.readDir(milestonesDir);
797
+ // Find file matching the ID
798
+ const milestoneFile = entries.find((entry) => {
799
+ const fileId = extractMilestoneIdFromFilename(entry);
800
+ return fileId === id;
801
+ });
802
+ if (!milestoneFile) {
803
+ span.addEvent("milestone.delete.error", {
804
+ "error.type": "NotFoundError",
805
+ "error.message": "Milestone not found",
806
+ "input.milestoneId": id,
807
+ });
808
+ span.setStatus({ code: SpanStatusCode.OK });
809
+ span.end();
810
+ return false;
811
+ }
812
+ const filepath = this.fs.join(milestonesDir, milestoneFile);
813
+ await this.fs.deleteFile(filepath);
814
+ span.addEvent("milestone.delete.complete", {
815
+ "output.milestoneId": id,
816
+ "output.deleted": true,
817
+ "duration.ms": Date.now() - startTime,
818
+ });
819
+ span.setStatus({ code: SpanStatusCode.OK });
820
+ span.end();
821
+ return true;
822
+ }
823
+ catch (error) {
824
+ span.addEvent("milestone.delete.error", {
825
+ "error.type": error instanceof Error ? error.name : "UnknownError",
826
+ "error.message": error instanceof Error ? error.message : String(error),
827
+ "input.milestoneId": id,
828
+ });
829
+ span.setStatus({
830
+ code: SpanStatusCode.ERROR,
831
+ message: error instanceof Error ? error.message : String(error),
832
+ });
833
+ span.end();
834
+ throw error;
835
+ }
644
836
  });
645
- if (!milestoneFile) {
646
- return false;
647
- }
648
- const filepath = this.fs.join(milestonesDir, milestoneFile);
649
- try {
650
- await this.fs.deleteFile(filepath);
651
- return true;
652
- }
653
- catch {
654
- return false;
655
- }
656
837
  }
657
838
  /**
658
839
  * Get a single task by ID
@@ -928,81 +1109,117 @@ export class Core {
928
1109
  */
929
1110
  async createTask(input) {
930
1111
  this.ensureInitialized();
931
- const tasksDir = this.getTasksDir();
932
- // Ensure tasks directory exists
933
- await this.fs.createDir(tasksDir, { recursive: true });
934
- // Generate next task ID
935
- // Use taskIndex as source of truth (works for both lazy and full initialization)
936
- const existingIds = Array.from(this.lazyInitialized ? this.taskIndex.keys() : this.tasks.keys())
937
- .map((id) => parseInt(id.replace(/\D/g, ""), 10))
938
- .filter((n) => !Number.isNaN(n));
939
- const nextId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 1;
940
- const taskId = String(nextId);
941
- // Validate and normalize status
942
- const configStatuses = this.config?.statuses || [
943
- DEFAULT_TASK_STATUSES.TODO,
944
- DEFAULT_TASK_STATUSES.IN_PROGRESS,
945
- DEFAULT_TASK_STATUSES.DONE,
946
- ];
947
- let status = input.status || this.config?.defaultStatus || DEFAULT_TASK_STATUSES.TODO;
948
- // Validate status against configured statuses
949
- if (!configStatuses.includes(status)) {
950
- console.warn(`Warning: Status "${status}" is not in configured statuses [${configStatuses.join(", ")}]. ` +
951
- `Using default status "${this.config?.defaultStatus || DEFAULT_TASK_STATUSES.TODO}" instead.`);
952
- status = this.config?.defaultStatus || DEFAULT_TASK_STATUSES.TODO;
953
- }
954
- // Build task object
955
- const now = new Date().toISOString().split("T")[0];
956
- const task = {
957
- id: taskId,
958
- title: input.title,
959
- status,
960
- priority: input.priority,
961
- assignee: input.assignee || [],
962
- createdDate: now,
963
- labels: input.labels || [],
964
- milestone: input.milestone,
965
- dependencies: input.dependencies || [],
966
- references: input.references || [],
967
- parentTaskId: input.parentTaskId,
968
- description: input.description,
969
- implementationPlan: input.implementationPlan,
970
- implementationNotes: input.implementationNotes,
971
- acceptanceCriteriaItems: input.acceptanceCriteria?.map((ac, i) => ({
972
- index: i + 1,
973
- text: ac.text,
974
- checked: ac.checked || false,
975
- })),
976
- rawContent: input.rawContent,
977
- source: "local",
978
- };
979
- // Serialize and write file
980
- const content = serializeTaskMarkdown(task);
981
- const safeTitle = input.title
982
- .replace(/[<>:"/\\|?*]/g, "")
983
- .replace(/\s+/g, " ")
984
- .slice(0, 50);
985
- const filename = `${taskId} - ${safeTitle}.md`;
986
- const filepath = this.fs.join(tasksDir, filename);
987
- await this.fs.writeFile(filepath, content);
988
- // Update in-memory cache
989
- task.filePath = filepath;
990
- this.tasks.set(taskId, task);
991
- // Also update taskIndex if in lazy mode
992
- if (this.lazyInitialized) {
993
- const relativePath = filepath.replace(`${this.projectRoot}/`, "");
994
- this.taskIndex.set(taskId, {
995
- id: taskId,
996
- filePath: relativePath,
997
- title: task.title,
998
- source: "tasks",
999
- });
1000
- }
1001
- // Sync milestone if specified
1002
- if (input.milestone) {
1003
- await this.addTaskToMilestone(taskId, input.milestone);
1004
- }
1005
- return task;
1112
+ const startTime = Date.now();
1113
+ const span = this.tracer.startSpan("task.create", {
1114
+ attributes: {
1115
+ "input.title": input.title,
1116
+ "input.status": input.status,
1117
+ "input.milestoneId": input.milestone,
1118
+ },
1119
+ });
1120
+ return await context.with(trace.setSpan(context.active(), span), async () => {
1121
+ try {
1122
+ span.addEvent("task.create.started", {
1123
+ "input.title": input.title,
1124
+ "input.status": input.status,
1125
+ "input.milestoneId": input.milestone,
1126
+ });
1127
+ const tasksDir = this.getTasksDir();
1128
+ // Ensure tasks directory exists
1129
+ await this.fs.createDir(tasksDir, { recursive: true });
1130
+ // Generate next task ID
1131
+ // Use taskIndex as source of truth (works for both lazy and full initialization)
1132
+ const existingIds = Array.from(this.lazyInitialized ? this.taskIndex.keys() : this.tasks.keys())
1133
+ .map((id) => parseInt(id.replace(/\D/g, ""), 10))
1134
+ .filter((n) => !Number.isNaN(n));
1135
+ const nextId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 1;
1136
+ const taskId = String(nextId);
1137
+ // Validate and normalize status
1138
+ const configStatuses = this.config?.statuses || [
1139
+ DEFAULT_TASK_STATUSES.TODO,
1140
+ DEFAULT_TASK_STATUSES.IN_PROGRESS,
1141
+ DEFAULT_TASK_STATUSES.DONE,
1142
+ ];
1143
+ let status = input.status || this.config?.defaultStatus || DEFAULT_TASK_STATUSES.TODO;
1144
+ // Validate status against configured statuses
1145
+ if (!configStatuses.includes(status)) {
1146
+ console.warn(`Warning: Status "${status}" is not in configured statuses [${configStatuses.join(", ")}]. ` +
1147
+ `Using default status "${this.config?.defaultStatus || DEFAULT_TASK_STATUSES.TODO}" instead.`);
1148
+ status = this.config?.defaultStatus || DEFAULT_TASK_STATUSES.TODO;
1149
+ }
1150
+ // Build task object
1151
+ const now = new Date().toISOString().split("T")[0];
1152
+ const task = {
1153
+ id: taskId,
1154
+ title: input.title,
1155
+ status,
1156
+ priority: input.priority,
1157
+ assignee: input.assignee || [],
1158
+ createdDate: now,
1159
+ labels: input.labels || [],
1160
+ milestone: input.milestone,
1161
+ dependencies: input.dependencies || [],
1162
+ references: input.references || [],
1163
+ parentTaskId: input.parentTaskId,
1164
+ description: input.description,
1165
+ implementationPlan: input.implementationPlan,
1166
+ implementationNotes: input.implementationNotes,
1167
+ acceptanceCriteriaItems: input.acceptanceCriteria?.map((ac, i) => ({
1168
+ index: i + 1,
1169
+ text: ac.text,
1170
+ checked: ac.checked || false,
1171
+ })),
1172
+ rawContent: input.rawContent,
1173
+ source: "local",
1174
+ };
1175
+ // Serialize and write file
1176
+ const content = serializeTaskMarkdown(task);
1177
+ const safeTitle = input.title
1178
+ .replace(/[<>:"/\\|?*]/g, "")
1179
+ .replace(/\s+/g, " ")
1180
+ .slice(0, 50);
1181
+ const filename = `${taskId} - ${safeTitle}.md`;
1182
+ const filepath = this.fs.join(tasksDir, filename);
1183
+ await this.fs.writeFile(filepath, content);
1184
+ // Update in-memory cache
1185
+ task.filePath = filepath;
1186
+ this.tasks.set(taskId, task);
1187
+ // Also update taskIndex if in lazy mode
1188
+ if (this.lazyInitialized) {
1189
+ const relativePath = filepath.replace(`${this.projectRoot}/`, "");
1190
+ this.taskIndex.set(taskId, {
1191
+ id: taskId,
1192
+ filePath: relativePath,
1193
+ title: task.title,
1194
+ source: "tasks",
1195
+ });
1196
+ }
1197
+ // Sync milestone if specified
1198
+ if (input.milestone) {
1199
+ await this.addTaskToMilestone(taskId, input.milestone);
1200
+ }
1201
+ span.addEvent("task.create.complete", {
1202
+ "output.taskId": taskId,
1203
+ "output.taskIndex": nextId,
1204
+ "duration.ms": Date.now() - startTime,
1205
+ });
1206
+ span.setStatus({ code: SpanStatusCode.OK });
1207
+ span.end();
1208
+ return task;
1209
+ }
1210
+ catch (error) {
1211
+ span.addEvent("task.create.error", {
1212
+ "error.type": error instanceof Error ? error.name : "UnknownError",
1213
+ "error.message": error instanceof Error ? error.message : String(error),
1214
+ });
1215
+ span.setStatus({
1216
+ code: SpanStatusCode.ERROR,
1217
+ message: error instanceof Error ? error.message : String(error),
1218
+ });
1219
+ span.end();
1220
+ throw error;
1221
+ }
1222
+ });
1006
1223
  }
1007
1224
  /**
1008
1225
  * Update an existing task
@@ -1013,105 +1230,153 @@ export class Core {
1013
1230
  */
1014
1231
  async updateTask(id, input) {
1015
1232
  this.ensureInitialized();
1016
- const existing = this.tasks.get(id);
1017
- if (!existing) {
1018
- return null;
1019
- }
1020
- const oldMilestone = existing.milestone;
1021
- const newMilestone = input.milestone === null
1022
- ? undefined
1023
- : input.milestone !== undefined
1024
- ? input.milestone
1025
- : oldMilestone;
1026
- // Build updated task
1027
- const now = new Date().toISOString().split("T")[0];
1028
- const updated = {
1029
- ...existing,
1030
- title: input.title ?? existing.title,
1031
- status: input.status ?? existing.status,
1032
- priority: input.priority ?? existing.priority,
1033
- milestone: newMilestone,
1034
- updatedDate: now,
1035
- description: input.description ?? existing.description,
1036
- implementationPlan: input.clearImplementationPlan
1037
- ? undefined
1038
- : (input.implementationPlan ?? existing.implementationPlan),
1039
- implementationNotes: input.clearImplementationNotes
1040
- ? undefined
1041
- : (input.implementationNotes ?? existing.implementationNotes),
1042
- ordinal: input.ordinal ?? existing.ordinal,
1043
- dependencies: input.dependencies ?? existing.dependencies,
1044
- references: input.references ?? existing.references ?? [],
1045
- };
1046
- // Handle label operations
1047
- if (input.labels) {
1048
- updated.labels = input.labels;
1049
- }
1050
- else {
1051
- if (input.addLabels) {
1052
- updated.labels = [...new Set([...updated.labels, ...input.addLabels])];
1053
- }
1054
- if (input.removeLabels) {
1055
- updated.labels = updated.labels.filter((l) => !input.removeLabels?.includes(l));
1056
- }
1057
- }
1058
- // Handle assignee
1059
- if (input.assignee) {
1060
- updated.assignee = input.assignee;
1061
- }
1062
- // Handle dependency operations
1063
- if (input.addDependencies) {
1064
- updated.dependencies = [...new Set([...updated.dependencies, ...input.addDependencies])];
1065
- }
1066
- if (input.removeDependencies) {
1067
- updated.dependencies = updated.dependencies.filter((d) => !input.removeDependencies?.includes(d));
1068
- }
1069
- // Handle references operations
1070
- if (input.addReferences) {
1071
- updated.references = [...new Set([...(updated.references || []), ...input.addReferences])];
1072
- }
1073
- if (input.removeReferences) {
1074
- updated.references = (updated.references || []).filter((r) => !input.removeReferences?.includes(r));
1075
- }
1076
- // Handle acceptance criteria
1077
- if (input.acceptanceCriteria) {
1078
- updated.acceptanceCriteriaItems = input.acceptanceCriteria.map((ac, i) => ({
1079
- index: i + 1,
1080
- text: ac.text,
1081
- checked: ac.checked || false,
1082
- }));
1083
- }
1084
- // Serialize and write file
1085
- const content = serializeTaskMarkdown(updated);
1086
- // Delete old file if exists
1087
- if (existing.filePath) {
1088
- await this.fs.deleteFile(existing.filePath).catch(() => { });
1089
- }
1090
- // Write new file
1091
- const tasksDir = this.getTasksDir();
1092
- const safeTitle = updated.title
1093
- .replace(/[<>:"/\\|?*]/g, "")
1094
- .replace(/\s+/g, " ")
1095
- .slice(0, 50);
1096
- const filename = `${id} - ${safeTitle}.md`;
1097
- const filepath = this.fs.join(tasksDir, filename);
1098
- await this.fs.writeFile(filepath, content);
1099
- // Update in-memory cache
1100
- updated.filePath = filepath;
1101
- this.tasks.set(id, updated);
1102
- // Handle milestone sync
1103
- const milestoneChanged = milestoneKey(oldMilestone) !== milestoneKey(newMilestone);
1104
- if (milestoneChanged) {
1105
- // Remove from old milestone
1106
- if (oldMilestone) {
1107
- await this.removeTaskFromMilestone(id, oldMilestone);
1233
+ const startTime = Date.now();
1234
+ const span = this.tracer.startSpan("task.update", {
1235
+ attributes: {
1236
+ "input.taskId": id,
1237
+ "input.hasTitle": input.title !== undefined,
1238
+ "input.hasStatus": input.status !== undefined,
1239
+ "input.hasMilestone": input.milestone !== undefined,
1240
+ },
1241
+ });
1242
+ return await context.with(trace.setSpan(context.active(), span), async () => {
1243
+ try {
1244
+ span.addEvent("task.update.started", {
1245
+ "input.taskId": id,
1246
+ "input.hasTitle": input.title !== undefined,
1247
+ "input.hasStatus": input.status !== undefined,
1248
+ "input.hasMilestone": input.milestone !== undefined,
1249
+ });
1250
+ const existing = this.tasks.get(id);
1251
+ if (!existing) {
1252
+ span.addEvent("task.update.error", {
1253
+ "error.type": "NotFoundError",
1254
+ "error.message": "Task not found",
1255
+ "input.taskId": id,
1256
+ });
1257
+ span.setStatus({ code: SpanStatusCode.OK }); // Not found is not an error
1258
+ span.end();
1259
+ return null;
1260
+ }
1261
+ const oldMilestone = existing.milestone;
1262
+ const newMilestone = input.milestone === null
1263
+ ? undefined
1264
+ : input.milestone !== undefined
1265
+ ? input.milestone
1266
+ : oldMilestone;
1267
+ // Build updated task
1268
+ const now = new Date().toISOString().split("T")[0];
1269
+ const updated = {
1270
+ ...existing,
1271
+ title: input.title ?? existing.title,
1272
+ status: input.status ?? existing.status,
1273
+ priority: input.priority ?? existing.priority,
1274
+ milestone: newMilestone,
1275
+ updatedDate: now,
1276
+ description: input.description ?? existing.description,
1277
+ implementationPlan: input.clearImplementationPlan
1278
+ ? undefined
1279
+ : (input.implementationPlan ?? existing.implementationPlan),
1280
+ implementationNotes: input.clearImplementationNotes
1281
+ ? undefined
1282
+ : (input.implementationNotes ?? existing.implementationNotes),
1283
+ ordinal: input.ordinal ?? existing.ordinal,
1284
+ dependencies: input.dependencies ?? existing.dependencies,
1285
+ references: input.references ?? existing.references ?? [],
1286
+ };
1287
+ // Handle label operations
1288
+ if (input.labels) {
1289
+ updated.labels = input.labels;
1290
+ }
1291
+ else {
1292
+ if (input.addLabels) {
1293
+ updated.labels = [...new Set([...updated.labels, ...input.addLabels])];
1294
+ }
1295
+ if (input.removeLabels) {
1296
+ updated.labels = updated.labels.filter((l) => !input.removeLabels?.includes(l));
1297
+ }
1298
+ }
1299
+ // Handle assignee
1300
+ if (input.assignee) {
1301
+ updated.assignee = input.assignee;
1302
+ }
1303
+ // Handle dependency operations
1304
+ if (input.addDependencies) {
1305
+ updated.dependencies = [...new Set([...updated.dependencies, ...input.addDependencies])];
1306
+ }
1307
+ if (input.removeDependencies) {
1308
+ updated.dependencies = updated.dependencies.filter((d) => !input.removeDependencies?.includes(d));
1309
+ }
1310
+ // Handle references operations
1311
+ if (input.addReferences) {
1312
+ updated.references = [
1313
+ ...new Set([...(updated.references || []), ...input.addReferences]),
1314
+ ];
1315
+ }
1316
+ if (input.removeReferences) {
1317
+ updated.references = (updated.references || []).filter((r) => !input.removeReferences?.includes(r));
1318
+ }
1319
+ // Handle acceptance criteria
1320
+ if (input.acceptanceCriteria) {
1321
+ updated.acceptanceCriteriaItems = input.acceptanceCriteria.map((ac, i) => ({
1322
+ index: i + 1,
1323
+ text: ac.text,
1324
+ checked: ac.checked || false,
1325
+ }));
1326
+ }
1327
+ // Serialize and write file
1328
+ const content = serializeTaskMarkdown(updated);
1329
+ // Delete old file if exists
1330
+ if (existing.filePath) {
1331
+ await this.fs.deleteFile(existing.filePath).catch(() => { });
1332
+ }
1333
+ // Write new file
1334
+ const tasksDir = this.getTasksDir();
1335
+ const safeTitle = updated.title
1336
+ .replace(/[<>:"/\\|?*]/g, "")
1337
+ .replace(/\s+/g, " ")
1338
+ .slice(0, 50);
1339
+ const filename = `${id} - ${safeTitle}.md`;
1340
+ const filepath = this.fs.join(tasksDir, filename);
1341
+ await this.fs.writeFile(filepath, content);
1342
+ // Update in-memory cache
1343
+ updated.filePath = filepath;
1344
+ this.tasks.set(id, updated);
1345
+ // Handle milestone sync
1346
+ const milestoneChanged = milestoneKey(oldMilestone) !== milestoneKey(newMilestone);
1347
+ if (milestoneChanged) {
1348
+ // Remove from old milestone
1349
+ if (oldMilestone) {
1350
+ await this.removeTaskFromMilestone(id, oldMilestone);
1351
+ }
1352
+ // Add to new milestone
1353
+ if (newMilestone) {
1354
+ await this.addTaskToMilestone(id, newMilestone);
1355
+ }
1356
+ }
1357
+ span.addEvent("task.update.complete", {
1358
+ "output.taskId": id,
1359
+ "output.statusChanged": input.status !== undefined,
1360
+ "duration.ms": Date.now() - startTime,
1361
+ });
1362
+ span.setStatus({ code: SpanStatusCode.OK });
1363
+ span.end();
1364
+ return updated;
1108
1365
  }
1109
- // Add to new milestone
1110
- if (newMilestone) {
1111
- await this.addTaskToMilestone(id, newMilestone);
1366
+ catch (error) {
1367
+ span.addEvent("task.update.error", {
1368
+ "error.type": error instanceof Error ? error.name : "UnknownError",
1369
+ "error.message": error instanceof Error ? error.message : String(error),
1370
+ "input.taskId": id,
1371
+ });
1372
+ span.setStatus({
1373
+ code: SpanStatusCode.ERROR,
1374
+ message: error instanceof Error ? error.message : String(error),
1375
+ });
1376
+ span.end();
1377
+ throw error;
1112
1378
  }
1113
- }
1114
- return updated;
1379
+ });
1115
1380
  }
1116
1381
  /**
1117
1382
  * Delete a task
@@ -1121,26 +1386,62 @@ export class Core {
1121
1386
  */
1122
1387
  async deleteTask(id) {
1123
1388
  this.ensureInitialized();
1124
- const task = this.tasks.get(id);
1125
- if (!task) {
1126
- return false;
1127
- }
1128
- // Remove from milestone if assigned
1129
- if (task.milestone) {
1130
- await this.removeTaskFromMilestone(id, task.milestone);
1131
- }
1132
- // Delete file
1133
- if (task.filePath) {
1389
+ const startTime = Date.now();
1390
+ const span = this.tracer.startSpan("task.delete", {
1391
+ attributes: { "input.taskId": id },
1392
+ });
1393
+ return await context.with(trace.setSpan(context.active(), span), async () => {
1134
1394
  try {
1135
- await this.fs.deleteFile(task.filePath);
1395
+ span.addEvent("task.delete.started", { "input.taskId": id });
1396
+ const task = this.tasks.get(id);
1397
+ if (!task) {
1398
+ span.addEvent("task.delete.error", {
1399
+ "error.type": "NotFoundError",
1400
+ "error.message": "Task not found",
1401
+ "input.taskId": id,
1402
+ });
1403
+ span.setStatus({ code: SpanStatusCode.OK });
1404
+ span.end();
1405
+ return false;
1406
+ }
1407
+ // Remove from milestone if assigned
1408
+ if (task.milestone) {
1409
+ await this.removeTaskFromMilestone(id, task.milestone);
1410
+ }
1411
+ // Delete file
1412
+ if (task.filePath) {
1413
+ try {
1414
+ await this.fs.deleteFile(task.filePath);
1415
+ }
1416
+ catch {
1417
+ // File may already be deleted
1418
+ }
1419
+ }
1420
+ // Remove from in-memory cache
1421
+ this.tasks.delete(id);
1422
+ span.addEvent("task.delete.complete", {
1423
+ "output.taskId": id,
1424
+ "output.deleted": true,
1425
+ "duration.ms": Date.now() - startTime,
1426
+ });
1427
+ span.setStatus({ code: SpanStatusCode.OK });
1428
+ span.end();
1429
+ return true;
1136
1430
  }
1137
- catch {
1138
- // File may already be deleted
1431
+ catch (error) {
1432
+ span.addEvent("task.delete.error", {
1433
+ "error.type": error instanceof Error ? error.name : "UnknownError",
1434
+ "error.message": error instanceof Error ? error.message : String(error),
1435
+ "input.taskId": id,
1436
+ });
1437
+ span.setStatus({
1438
+ code: SpanStatusCode.ERROR,
1439
+ message: error instanceof Error ? error.message : String(error),
1440
+ });
1441
+ span.end();
1442
+ throw error;
1139
1443
  }
1140
- }
1141
- // Remove from in-memory cache
1142
- this.tasks.delete(id);
1143
- return true;
1444
+ });
1144
1445
  }
1145
1446
  /**
1146
1447
  * Archive a task (move from tasks/ to completed/)
@@ -1150,53 +1451,96 @@ export class Core {
1150
1451
  */
1151
1452
  async archiveTask(id) {
1152
1453
  this.ensureInitialized();
1153
- const task = this.tasks.get(id);
1154
- if (!task) {
1155
- return null;
1156
- }
1157
- // Check if already in completed
1158
- if (task.source === "completed") {
1159
- return null;
1160
- }
1161
- const completedDir = this.getCompletedDir();
1162
- // Ensure completed directory exists
1163
- await this.fs.createDir(completedDir, { recursive: true });
1164
- // Build new filepath in completed/
1165
- const safeTitle = task.title
1166
- .replace(/[<>:"/\\|?*]/g, "")
1167
- .replace(/\s+/g, " ")
1168
- .slice(0, 50);
1169
- const filename = `${id} - ${safeTitle}.md`;
1170
- const newFilepath = this.fs.join(completedDir, filename);
1171
- // Delete old file
1172
- if (task.filePath) {
1454
+ const startTime = Date.now();
1455
+ const span = this.tracer.startSpan("task.archive", {
1456
+ attributes: { "input.taskId": id },
1457
+ });
1458
+ return await context.with(trace.setSpan(context.active(), span), async () => {
1173
1459
  try {
1174
- await this.fs.deleteFile(task.filePath);
1175
- }
1176
- catch {
1177
- // File may not exist
1460
+ span.addEvent("task.archive.started", { "input.taskId": id });
1461
+ const task = this.tasks.get(id);
1462
+ if (!task) {
1463
+ span.addEvent("task.archive.error", {
1464
+ "error.type": "NotFoundError",
1465
+ "error.message": "Task not found",
1466
+ "input.taskId": id,
1467
+ });
1468
+ span.setStatus({ code: SpanStatusCode.OK }); // Not found is not an error
1469
+ span.end();
1470
+ return null;
1471
+ }
1472
+ // Check if already in completed
1473
+ if (task.source === "completed") {
1474
+ span.addEvent("task.archive.error", {
1475
+ "error.type": "InvalidStateError",
1476
+ "error.message": "Task already archived",
1477
+ "input.taskId": id,
1478
+ });
1479
+ span.setStatus({ code: SpanStatusCode.OK });
1480
+ span.end();
1481
+ return null;
1482
+ }
1483
+ const completedDir = this.getCompletedDir();
1484
+ // Ensure completed directory exists
1485
+ await this.fs.createDir(completedDir, { recursive: true });
1486
+ // Build new filepath in completed/
1487
+ const safeTitle = task.title
1488
+ .replace(/[<>:"/\\|?*]/g, "")
1489
+ .replace(/\s+/g, " ")
1490
+ .slice(0, 50);
1491
+ const filename = `${id} - ${safeTitle}.md`;
1492
+ const newFilepath = this.fs.join(completedDir, filename);
1493
+ // Delete old file
1494
+ if (task.filePath) {
1495
+ try {
1496
+ await this.fs.deleteFile(task.filePath);
1497
+ }
1498
+ catch {
1499
+ // File may not exist
1500
+ }
1501
+ }
1502
+ // Update task
1503
+ const archived = {
1504
+ ...task,
1505
+ source: "completed",
1506
+ filePath: newFilepath,
1507
+ };
1508
+ // Write to new location
1509
+ const content = serializeTaskMarkdown(archived);
1510
+ await this.fs.writeFile(newFilepath, content);
1511
+ // Update in-memory cache
1512
+ this.tasks.set(id, archived);
1513
+ // Update task index if lazy initialized
1514
+ if (this.lazyInitialized) {
1515
+ const entry = this.taskIndex.get(id);
1516
+ if (entry) {
1517
+ entry.source = "completed";
1518
+ entry.filePath = newFilepath;
1519
+ }
1520
+ }
1521
+ span.addEvent("task.archive.complete", {
1522
+ "output.taskId": id,
1523
+ "output.newPath": newFilepath,
1524
+ "duration.ms": Date.now() - startTime,
1525
+ });
1526
+ span.setStatus({ code: SpanStatusCode.OK });
1527
+ span.end();
1528
+ return archived;
1178
1529
  }
1179
- }
1180
- // Update task
1181
- const archived = {
1182
- ...task,
1183
- source: "completed",
1184
- filePath: newFilepath,
1185
- };
1186
- // Write to new location
1187
- const content = serializeTaskMarkdown(archived);
1188
- await this.fs.writeFile(newFilepath, content);
1189
- // Update in-memory cache
1190
- this.tasks.set(id, archived);
1191
- // Update task index if lazy initialized
1192
- if (this.lazyInitialized) {
1193
- const entry = this.taskIndex.get(id);
1194
- if (entry) {
1195
- entry.source = "completed";
1196
- entry.filePath = newFilepath;
1530
+ catch (error) {
1531
+ span.addEvent("task.archive.error", {
1532
+ "error.type": error instanceof Error ? error.name : "UnknownError",
1533
+ "error.message": error instanceof Error ? error.message : String(error),
1534
+ "input.taskId": id,
1535
+ });
1536
+ span.setStatus({
1537
+ code: SpanStatusCode.ERROR,
1538
+ message: error instanceof Error ? error.message : String(error),
1539
+ });
1540
+ span.end();
1541
+ throw error;
1197
1542
  }
1198
- }
1199
- return archived;
1543
+ });
1200
1544
  }
1201
1545
  /**
1202
1546
  * Restore a task (move from completed/ to tasks/)
@@ -1206,53 +1550,96 @@ export class Core {
1206
1550
  */
1207
1551
  async restoreTask(id) {
1208
1552
  this.ensureInitialized();
1209
- const task = this.tasks.get(id);
1210
- if (!task) {
1211
- return null;
1212
- }
1213
- // Check if in completed
1214
- if (task.source !== "completed") {
1215
- return null;
1216
- }
1217
- const tasksDir = this.getTasksDir();
1218
- // Ensure tasks directory exists
1219
- await this.fs.createDir(tasksDir, { recursive: true });
1220
- // Build new filepath in tasks/
1221
- const safeTitle = task.title
1222
- .replace(/[<>:"/\\|?*]/g, "")
1223
- .replace(/\s+/g, " ")
1224
- .slice(0, 50);
1225
- const filename = `${id} - ${safeTitle}.md`;
1226
- const newFilepath = this.fs.join(tasksDir, filename);
1227
- // Delete old file
1228
- if (task.filePath) {
1553
+ const startTime = Date.now();
1554
+ const span = this.tracer.startSpan("task.restore", {
1555
+ attributes: { "input.taskId": id },
1556
+ });
1557
+ return await context.with(trace.setSpan(context.active(), span), async () => {
1229
1558
  try {
1230
- await this.fs.deleteFile(task.filePath);
1231
- }
1232
- catch {
1233
- // File may not exist
1559
+ span.addEvent("task.restore.started", { "input.taskId": id });
1560
+ const task = this.tasks.get(id);
1561
+ if (!task) {
1562
+ span.addEvent("task.restore.error", {
1563
+ "error.type": "NotFoundError",
1564
+ "error.message": "Task not found",
1565
+ "input.taskId": id,
1566
+ });
1567
+ span.setStatus({ code: SpanStatusCode.OK }); // Not found is not an error
1568
+ span.end();
1569
+ return null;
1570
+ }
1571
+ // Check if in completed
1572
+ if (task.source !== "completed") {
1573
+ span.addEvent("task.restore.error", {
1574
+ "error.type": "InvalidStateError",
1575
+ "error.message": "Task not in completed",
1576
+ "input.taskId": id,
1577
+ });
1578
+ span.setStatus({ code: SpanStatusCode.OK });
1579
+ span.end();
1580
+ return null;
1581
+ }
1582
+ const tasksDir = this.getTasksDir();
1583
+ // Ensure tasks directory exists
1584
+ await this.fs.createDir(tasksDir, { recursive: true });
1585
+ // Build new filepath in tasks/
1586
+ const safeTitle = task.title
1587
+ .replace(/[<>:"/\\|?*]/g, "")
1588
+ .replace(/\s+/g, " ")
1589
+ .slice(0, 50);
1590
+ const filename = `${id} - ${safeTitle}.md`;
1591
+ const newFilepath = this.fs.join(tasksDir, filename);
1592
+ // Delete old file
1593
+ if (task.filePath) {
1594
+ try {
1595
+ await this.fs.deleteFile(task.filePath);
1596
+ }
1597
+ catch {
1598
+ // File may not exist
1599
+ }
1600
+ }
1601
+ // Update task
1602
+ const restored = {
1603
+ ...task,
1604
+ source: "local",
1605
+ filePath: newFilepath,
1606
+ };
1607
+ // Write to new location
1608
+ const content = serializeTaskMarkdown(restored);
1609
+ await this.fs.writeFile(newFilepath, content);
1610
+ // Update in-memory cache
1611
+ this.tasks.set(id, restored);
1612
+ // Update task index if lazy initialized
1613
+ if (this.lazyInitialized) {
1614
+ const entry = this.taskIndex.get(id);
1615
+ if (entry) {
1616
+ entry.source = "tasks";
1617
+ entry.filePath = newFilepath;
1618
+ }
1619
+ }
1620
+ span.addEvent("task.restore.complete", {
1621
+ "output.taskId": id,
1622
+ "output.newPath": newFilepath,
1623
+ "duration.ms": Date.now() - startTime,
1624
+ });
1625
+ span.setStatus({ code: SpanStatusCode.OK });
1626
+ span.end();
1627
+ return restored;
1234
1628
  }
1235
- }
1236
- // Update task
1237
- const restored = {
1238
- ...task,
1239
- source: "local",
1240
- filePath: newFilepath,
1241
- };
1242
- // Write to new location
1243
- const content = serializeTaskMarkdown(restored);
1244
- await this.fs.writeFile(newFilepath, content);
1245
- // Update in-memory cache
1246
- this.tasks.set(id, restored);
1247
- // Update task index if lazy initialized
1248
- if (this.lazyInitialized) {
1249
- const entry = this.taskIndex.get(id);
1250
- if (entry) {
1251
- entry.source = "tasks";
1252
- entry.filePath = newFilepath;
1629
+ catch (error) {
1630
+ span.addEvent("task.restore.error", {
1631
+ "error.type": error instanceof Error ? error.name : "UnknownError",
1632
+ "error.message": error instanceof Error ? error.message : String(error),
1633
+ "input.taskId": id,
1634
+ });
1635
+ span.setStatus({
1636
+ code: SpanStatusCode.ERROR,
1637
+ message: error instanceof Error ? error.message : String(error),
1638
+ });
1639
+ span.end();
1640
+ throw error;
1253
1641
  }
1254
- }
1255
- return restored;
1642
+ });
1256
1643
  }
1257
1644
  /**
1258
1645
  * Load specific tasks by their IDs (for lazy loading milestone tasks)