@backlog-md/core 0.3.7 → 0.3.9

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
@@ -4,7 +4,9 @@
4
4
  * Provides a runtime-agnostic API for managing Backlog.md projects
5
5
  * by accepting adapter implementations for I/O operations.
6
6
  */
7
+ import { context, SpanStatusCode, trace } from "@opentelemetry/api";
7
8
  import { extractMilestoneIdFromFilename, extractTaskIndexFromPath, getMilestoneFilename, parseMilestoneMarkdown, parseTaskMarkdown, serializeMilestoneMarkdown, serializeTaskMarkdown, } from "../markdown";
9
+ import { getTracer } from "../telemetry";
8
10
  import { DEFAULT_TASK_STATUSES } from "../types";
9
11
  import { groupTasksByMilestone, groupTasksByStatus, milestoneKey, sortTasks, sortTasksBy, } from "../utils";
10
12
  import { parseBacklogConfig, serializeBacklogConfig } from "./config-parser";
@@ -26,6 +28,7 @@ import { parseBacklogConfig, serializeBacklogConfig } from "./config-parser";
26
28
  export class Core {
27
29
  projectRoot;
28
30
  fs;
31
+ tracer;
29
32
  config = null;
30
33
  tasks = new Map();
31
34
  initialized = false;
@@ -35,6 +38,9 @@ export class Core {
35
38
  constructor(options) {
36
39
  this.projectRoot = options.projectRoot;
37
40
  this.fs = options.adapters.fs;
41
+ // Get tracer from global provider (set up by the application)
42
+ // Returns no-op tracer if no provider is registered
43
+ this.tracer = getTracer();
38
44
  }
39
45
  /**
40
46
  * Check if projectRoot contains a valid Backlog.md project
@@ -106,25 +112,80 @@ export class Core {
106
112
  async initialize() {
107
113
  if (this.initialized)
108
114
  return;
109
- // Load config
110
- const configPath = this.fs.join(this.projectRoot, "backlog", "config.yml");
111
- const configExists = await this.fs.exists(configPath);
112
- if (!configExists) {
113
- throw new Error(`Not a Backlog.md project: config.yml not found at ${configPath}`);
114
- }
115
- const configContent = await this.fs.readFile(configPath);
116
- this.config = parseBacklogConfig(configContent);
117
- // Load tasks from tasks/ directory
118
- const tasksDir = this.fs.join(this.projectRoot, "backlog", "tasks");
119
- if (await this.fs.exists(tasksDir)) {
120
- await this.loadTasksFromDirectory(tasksDir, "local");
121
- }
122
- // Load tasks from completed/ directory
123
- const completedDir = this.fs.join(this.projectRoot, "backlog", "completed");
124
- if (await this.fs.exists(completedDir)) {
125
- await this.loadTasksFromDirectory(completedDir, "completed");
126
- }
127
- this.initialized = true;
115
+ const span = this.tracer.startSpan("core.init", {
116
+ attributes: {
117
+ projectRoot: this.projectRoot,
118
+ mode: "full",
119
+ },
120
+ });
121
+ return await context.with(trace.setSpan(context.active(), span), async () => {
122
+ const startTime = Date.now();
123
+ try {
124
+ span.addEvent("core.init.started", {
125
+ projectRoot: this.projectRoot,
126
+ mode: "full",
127
+ });
128
+ // Load config
129
+ const configPath = this.fs.join(this.projectRoot, "backlog", "config.yml");
130
+ const configExists = await this.fs.exists(configPath);
131
+ if (!configExists) {
132
+ const error = new Error(`Not a Backlog.md project: config.yml not found at ${configPath}`);
133
+ span.addEvent("core.init.error", {
134
+ "error.type": "ConfigNotFoundError",
135
+ "error.message": error.message,
136
+ stage: "config",
137
+ });
138
+ span.recordException(error);
139
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
140
+ throw error;
141
+ }
142
+ const configContent = await this.fs.readFile(configPath);
143
+ this.config = parseBacklogConfig(configContent);
144
+ span.addEvent("core.init.config.loaded", {
145
+ projectName: this.config.projectName,
146
+ statusCount: this.config.statuses.length,
147
+ labelCount: this.config.labels?.length ?? 0,
148
+ });
149
+ // Load tasks from tasks/ directory
150
+ let localTaskCount = 0;
151
+ const tasksDir = this.fs.join(this.projectRoot, "backlog", "tasks");
152
+ if (await this.fs.exists(tasksDir)) {
153
+ await this.loadTasksFromDirectory(tasksDir, "local");
154
+ localTaskCount = Array.from(this.tasks.values()).filter((t) => t.source === "local").length;
155
+ }
156
+ // Load tasks from completed/ directory
157
+ let completedTaskCount = 0;
158
+ const completedDir = this.fs.join(this.projectRoot, "backlog", "completed");
159
+ if (await this.fs.exists(completedDir)) {
160
+ await this.loadTasksFromDirectory(completedDir, "completed");
161
+ completedTaskCount = Array.from(this.tasks.values()).filter((t) => t.source === "completed").length;
162
+ }
163
+ const totalTaskCount = this.tasks.size;
164
+ span.addEvent("core.init.tasks.loaded", {
165
+ localTaskCount,
166
+ completedTaskCount,
167
+ totalTaskCount,
168
+ });
169
+ this.initialized = true;
170
+ const duration = Date.now() - startTime;
171
+ span.addEvent("core.init.complete", {
172
+ success: true,
173
+ "duration.ms": duration,
174
+ taskCount: totalTaskCount,
175
+ });
176
+ span.setStatus({ code: SpanStatusCode.OK });
177
+ }
178
+ catch (error) {
179
+ if (error instanceof Error) {
180
+ span.recordException(error);
181
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
182
+ }
183
+ throw error;
184
+ }
185
+ finally {
186
+ span.end();
187
+ }
188
+ });
128
189
  }
129
190
  /**
130
191
  * Initialize with lazy loading (no file content reads)
@@ -474,43 +535,76 @@ export class Core {
474
535
  * @returns Created milestone
475
536
  */
476
537
  async createMilestone(input) {
477
- const milestonesDir = this.getMilestonesDir();
478
- // Ensure milestones directory exists
479
- await this.fs.createDir(milestonesDir, { recursive: true });
480
- // Find next available milestone ID
481
- const entries = await this.fs.readDir(milestonesDir).catch(() => []);
482
- const existingIds = entries
483
- .map((f) => {
484
- const match = f.match(/^m-(\d+)/);
485
- return match?.[1] ? parseInt(match[1], 10) : -1;
486
- })
487
- .filter((id) => id >= 0);
488
- const nextId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 0;
489
- const id = `m-${nextId}`;
490
- const description = input.description || `Milestone: ${input.title}`;
491
- // Create a temporary milestone to generate content
492
- const tempMilestone = {
493
- id,
494
- title: input.title,
495
- description,
496
- rawContent: "",
497
- tasks: [],
498
- };
499
- // Generate content
500
- const content = serializeMilestoneMarkdown(tempMilestone);
501
- // Create the final milestone with correct rawContent
502
- const milestone = {
503
- id,
504
- title: input.title,
505
- description,
506
- rawContent: content,
507
- tasks: [],
508
- };
509
- // Write file
510
- const filename = getMilestoneFilename(id, input.title);
511
- const filepath = this.fs.join(milestonesDir, filename);
512
- await this.fs.writeFile(filepath, content);
513
- return milestone;
538
+ const startTime = Date.now();
539
+ const span = this.tracer.startSpan("milestone.create", {
540
+ attributes: {
541
+ "input.title": input.title,
542
+ "input.hasDescription": input.description !== undefined,
543
+ },
544
+ });
545
+ return await context.with(trace.setSpan(context.active(), span), async () => {
546
+ try {
547
+ span.addEvent("milestone.create.started", {
548
+ "input.title": input.title,
549
+ "input.hasDescription": input.description !== undefined,
550
+ });
551
+ const milestonesDir = this.getMilestonesDir();
552
+ // Ensure milestones directory exists
553
+ await this.fs.createDir(milestonesDir, { recursive: true });
554
+ // Find next available milestone ID
555
+ const entries = await this.fs.readDir(milestonesDir).catch(() => []);
556
+ const existingIds = entries
557
+ .map((f) => {
558
+ const match = f.match(/^m-(\d+)/);
559
+ return match?.[1] ? parseInt(match[1], 10) : -1;
560
+ })
561
+ .filter((id) => id >= 0);
562
+ const nextId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 0;
563
+ const id = `m-${nextId}`;
564
+ const description = input.description || `Milestone: ${input.title}`;
565
+ // Create a temporary milestone to generate content
566
+ const tempMilestone = {
567
+ id,
568
+ title: input.title,
569
+ description,
570
+ rawContent: "",
571
+ tasks: [],
572
+ };
573
+ // Generate content
574
+ const content = serializeMilestoneMarkdown(tempMilestone);
575
+ // Create the final milestone with correct rawContent
576
+ const milestone = {
577
+ id,
578
+ title: input.title,
579
+ description,
580
+ rawContent: content,
581
+ tasks: [],
582
+ };
583
+ // Write file
584
+ const filename = getMilestoneFilename(id, input.title);
585
+ const filepath = this.fs.join(milestonesDir, filename);
586
+ await this.fs.writeFile(filepath, content);
587
+ span.addEvent("milestone.create.complete", {
588
+ "output.milestoneId": id,
589
+ "duration.ms": Date.now() - startTime,
590
+ });
591
+ span.setStatus({ code: SpanStatusCode.OK });
592
+ span.end();
593
+ return milestone;
594
+ }
595
+ catch (error) {
596
+ span.addEvent("milestone.create.error", {
597
+ "error.type": error instanceof Error ? error.name : "UnknownError",
598
+ "error.message": error instanceof Error ? error.message : String(error),
599
+ });
600
+ span.setStatus({
601
+ code: SpanStatusCode.ERROR,
602
+ message: error instanceof Error ? error.message : String(error),
603
+ });
604
+ span.end();
605
+ throw error;
606
+ }
607
+ });
514
608
  }
515
609
  /**
516
610
  * Update an existing milestone
@@ -520,49 +614,101 @@ export class Core {
520
614
  * @returns Updated milestone or null if not found
521
615
  */
522
616
  async updateMilestone(id, input) {
523
- const existing = await this.loadMilestone(id);
524
- if (!existing) {
525
- return null;
526
- }
527
- const milestonesDir = this.getMilestonesDir();
528
- const entries = await this.fs.readDir(milestonesDir);
529
- // Find the current file
530
- const currentFile = entries.find((entry) => {
531
- const fileId = extractMilestoneIdFromFilename(entry);
532
- return fileId === id;
617
+ const startTime = Date.now();
618
+ const span = this.tracer.startSpan("milestone.update", {
619
+ attributes: {
620
+ "input.milestoneId": id,
621
+ "input.hasTitle": input.title !== undefined,
622
+ "input.hasDescription": input.description !== undefined,
623
+ },
624
+ });
625
+ return await context.with(trace.setSpan(context.active(), span), async () => {
626
+ try {
627
+ span.addEvent("milestone.update.started", {
628
+ "input.milestoneId": id,
629
+ "input.hasTitle": input.title !== undefined,
630
+ "input.hasDescription": input.description !== undefined,
631
+ });
632
+ const existing = await this.loadMilestone(id);
633
+ if (!existing) {
634
+ span.addEvent("milestone.update.error", {
635
+ "error.type": "NotFoundError",
636
+ "error.message": "Milestone not found",
637
+ "input.milestoneId": id,
638
+ });
639
+ span.setStatus({ code: SpanStatusCode.OK });
640
+ span.end();
641
+ return null;
642
+ }
643
+ const milestonesDir = this.getMilestonesDir();
644
+ const entries = await this.fs.readDir(milestonesDir);
645
+ // Find the current file
646
+ const currentFile = entries.find((entry) => {
647
+ const fileId = extractMilestoneIdFromFilename(entry);
648
+ return fileId === id;
649
+ });
650
+ if (!currentFile) {
651
+ span.addEvent("milestone.update.error", {
652
+ "error.type": "NotFoundError",
653
+ "error.message": "Milestone file not found",
654
+ "input.milestoneId": id,
655
+ });
656
+ span.setStatus({ code: SpanStatusCode.OK });
657
+ span.end();
658
+ return null;
659
+ }
660
+ // Build updated values
661
+ const newTitle = input.title ?? existing.title;
662
+ const newDescription = input.description ?? existing.description;
663
+ const titleChanged = input.title !== undefined && input.title !== existing.title;
664
+ // Create a temporary milestone to generate content
665
+ const tempMilestone = {
666
+ id: existing.id,
667
+ title: newTitle,
668
+ description: newDescription,
669
+ rawContent: "",
670
+ tasks: existing.tasks,
671
+ };
672
+ // Generate new content
673
+ const content = serializeMilestoneMarkdown(tempMilestone);
674
+ // Create the final updated milestone
675
+ const updated = {
676
+ id: existing.id,
677
+ title: newTitle,
678
+ description: newDescription,
679
+ rawContent: content,
680
+ tasks: existing.tasks,
681
+ };
682
+ // Delete old file
683
+ const oldPath = this.fs.join(milestonesDir, currentFile);
684
+ await this.fs.deleteFile(oldPath);
685
+ // Write new file (with potentially new filename if title changed)
686
+ const newFilename = getMilestoneFilename(id, updated.title);
687
+ const newPath = this.fs.join(milestonesDir, newFilename);
688
+ await this.fs.writeFile(newPath, content);
689
+ span.addEvent("milestone.update.complete", {
690
+ "output.milestoneId": id,
691
+ "output.titleChanged": titleChanged,
692
+ "duration.ms": Date.now() - startTime,
693
+ });
694
+ span.setStatus({ code: SpanStatusCode.OK });
695
+ span.end();
696
+ return updated;
697
+ }
698
+ catch (error) {
699
+ span.addEvent("milestone.update.error", {
700
+ "error.type": error instanceof Error ? error.name : "UnknownError",
701
+ "error.message": error instanceof Error ? error.message : String(error),
702
+ "input.milestoneId": id,
703
+ });
704
+ span.setStatus({
705
+ code: SpanStatusCode.ERROR,
706
+ message: error instanceof Error ? error.message : String(error),
707
+ });
708
+ span.end();
709
+ throw error;
710
+ }
533
711
  });
534
- if (!currentFile) {
535
- return null;
536
- }
537
- // Build updated values
538
- const newTitle = input.title ?? existing.title;
539
- const newDescription = input.description ?? existing.description;
540
- // Create a temporary milestone to generate content
541
- const tempMilestone = {
542
- id: existing.id,
543
- title: newTitle,
544
- description: newDescription,
545
- rawContent: "",
546
- tasks: existing.tasks,
547
- };
548
- // Generate new content
549
- const content = serializeMilestoneMarkdown(tempMilestone);
550
- // Create the final updated milestone
551
- const updated = {
552
- id: existing.id,
553
- title: newTitle,
554
- description: newDescription,
555
- rawContent: content,
556
- tasks: existing.tasks,
557
- };
558
- // Delete old file
559
- const oldPath = this.fs.join(milestonesDir, currentFile);
560
- await this.fs.deleteFile(oldPath);
561
- // Write new file (with potentially new filename if title changed)
562
- const newFilename = getMilestoneFilename(id, updated.title);
563
- const newPath = this.fs.join(milestonesDir, newFilename);
564
- await this.fs.writeFile(newPath, content);
565
- return updated;
566
712
  }
567
713
  /**
568
714
  * Delete a milestone
@@ -571,27 +717,69 @@ export class Core {
571
717
  * @returns true if deleted, false if not found
572
718
  */
573
719
  async deleteMilestone(id) {
574
- const milestonesDir = this.getMilestonesDir();
575
- if (!(await this.fs.exists(milestonesDir))) {
576
- return false;
577
- }
578
- const entries = await this.fs.readDir(milestonesDir);
579
- // Find file matching the ID
580
- const milestoneFile = entries.find((entry) => {
581
- const fileId = extractMilestoneIdFromFilename(entry);
582
- return fileId === id;
720
+ const startTime = Date.now();
721
+ const span = this.tracer.startSpan("milestone.delete", {
722
+ attributes: {
723
+ "input.milestoneId": id,
724
+ },
725
+ });
726
+ return await context.with(trace.setSpan(context.active(), span), async () => {
727
+ try {
728
+ span.addEvent("milestone.delete.started", {
729
+ "input.milestoneId": id,
730
+ });
731
+ const milestonesDir = this.getMilestonesDir();
732
+ if (!(await this.fs.exists(milestonesDir))) {
733
+ span.addEvent("milestone.delete.error", {
734
+ "error.type": "NotFoundError",
735
+ "error.message": "Milestones directory not found",
736
+ "input.milestoneId": id,
737
+ });
738
+ span.setStatus({ code: SpanStatusCode.OK });
739
+ span.end();
740
+ return false;
741
+ }
742
+ const entries = await this.fs.readDir(milestonesDir);
743
+ // Find file matching the ID
744
+ const milestoneFile = entries.find((entry) => {
745
+ const fileId = extractMilestoneIdFromFilename(entry);
746
+ return fileId === id;
747
+ });
748
+ if (!milestoneFile) {
749
+ span.addEvent("milestone.delete.error", {
750
+ "error.type": "NotFoundError",
751
+ "error.message": "Milestone not found",
752
+ "input.milestoneId": id,
753
+ });
754
+ span.setStatus({ code: SpanStatusCode.OK });
755
+ span.end();
756
+ return false;
757
+ }
758
+ const filepath = this.fs.join(milestonesDir, milestoneFile);
759
+ await this.fs.deleteFile(filepath);
760
+ span.addEvent("milestone.delete.complete", {
761
+ "output.milestoneId": id,
762
+ "output.deleted": true,
763
+ "duration.ms": Date.now() - startTime,
764
+ });
765
+ span.setStatus({ code: SpanStatusCode.OK });
766
+ span.end();
767
+ return true;
768
+ }
769
+ catch (error) {
770
+ span.addEvent("milestone.delete.error", {
771
+ "error.type": error instanceof Error ? error.name : "UnknownError",
772
+ "error.message": error instanceof Error ? error.message : String(error),
773
+ "input.milestoneId": id,
774
+ });
775
+ span.setStatus({
776
+ code: SpanStatusCode.ERROR,
777
+ message: error instanceof Error ? error.message : String(error),
778
+ });
779
+ span.end();
780
+ throw error;
781
+ }
583
782
  });
584
- if (!milestoneFile) {
585
- return false;
586
- }
587
- const filepath = this.fs.join(milestonesDir, milestoneFile);
588
- try {
589
- await this.fs.deleteFile(filepath);
590
- return true;
591
- }
592
- catch {
593
- return false;
594
- }
595
783
  }
596
784
  /**
597
785
  * Get a single task by ID
@@ -867,81 +1055,117 @@ export class Core {
867
1055
  */
868
1056
  async createTask(input) {
869
1057
  this.ensureInitialized();
870
- const tasksDir = this.getTasksDir();
871
- // Ensure tasks directory exists
872
- await this.fs.createDir(tasksDir, { recursive: true });
873
- // Generate next task ID
874
- // Use taskIndex as source of truth (works for both lazy and full initialization)
875
- const existingIds = Array.from(this.lazyInitialized ? this.taskIndex.keys() : this.tasks.keys())
876
- .map((id) => parseInt(id.replace(/\D/g, ""), 10))
877
- .filter((n) => !Number.isNaN(n));
878
- const nextId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 1;
879
- const taskId = String(nextId);
880
- // Validate and normalize status
881
- const configStatuses = this.config?.statuses || [
882
- DEFAULT_TASK_STATUSES.TODO,
883
- DEFAULT_TASK_STATUSES.IN_PROGRESS,
884
- DEFAULT_TASK_STATUSES.DONE,
885
- ];
886
- let status = input.status || this.config?.defaultStatus || DEFAULT_TASK_STATUSES.TODO;
887
- // Validate status against configured statuses
888
- if (!configStatuses.includes(status)) {
889
- console.warn(`Warning: Status "${status}" is not in configured statuses [${configStatuses.join(", ")}]. ` +
890
- `Using default status "${this.config?.defaultStatus || DEFAULT_TASK_STATUSES.TODO}" instead.`);
891
- status = this.config?.defaultStatus || DEFAULT_TASK_STATUSES.TODO;
892
- }
893
- // Build task object
894
- const now = new Date().toISOString().split("T")[0];
895
- const task = {
896
- id: taskId,
897
- title: input.title,
898
- status,
899
- priority: input.priority,
900
- assignee: input.assignee || [],
901
- createdDate: now,
902
- labels: input.labels || [],
903
- milestone: input.milestone,
904
- dependencies: input.dependencies || [],
905
- references: input.references || [],
906
- parentTaskId: input.parentTaskId,
907
- description: input.description,
908
- implementationPlan: input.implementationPlan,
909
- implementationNotes: input.implementationNotes,
910
- acceptanceCriteriaItems: input.acceptanceCriteria?.map((ac, i) => ({
911
- index: i + 1,
912
- text: ac.text,
913
- checked: ac.checked || false,
914
- })),
915
- rawContent: input.rawContent,
916
- source: "local",
917
- };
918
- // Serialize and write file
919
- const content = serializeTaskMarkdown(task);
920
- const safeTitle = input.title
921
- .replace(/[<>:"/\\|?*]/g, "")
922
- .replace(/\s+/g, " ")
923
- .slice(0, 50);
924
- const filename = `${taskId} - ${safeTitle}.md`;
925
- const filepath = this.fs.join(tasksDir, filename);
926
- await this.fs.writeFile(filepath, content);
927
- // Update in-memory cache
928
- task.filePath = filepath;
929
- this.tasks.set(taskId, task);
930
- // Also update taskIndex if in lazy mode
931
- if (this.lazyInitialized) {
932
- const relativePath = filepath.replace(this.projectRoot + "/", "");
933
- this.taskIndex.set(taskId, {
934
- id: taskId,
935
- filePath: relativePath,
936
- title: task.title,
937
- source: "tasks",
938
- });
939
- }
940
- // Sync milestone if specified
941
- if (input.milestone) {
942
- await this.addTaskToMilestone(taskId, input.milestone);
943
- }
944
- return task;
1058
+ const startTime = Date.now();
1059
+ const span = this.tracer.startSpan("task.create", {
1060
+ attributes: {
1061
+ "input.title": input.title,
1062
+ "input.status": input.status,
1063
+ "input.milestoneId": input.milestone,
1064
+ },
1065
+ });
1066
+ return await context.with(trace.setSpan(context.active(), span), async () => {
1067
+ try {
1068
+ span.addEvent("task.create.started", {
1069
+ "input.title": input.title,
1070
+ "input.status": input.status,
1071
+ "input.milestoneId": input.milestone,
1072
+ });
1073
+ const tasksDir = this.getTasksDir();
1074
+ // Ensure tasks directory exists
1075
+ await this.fs.createDir(tasksDir, { recursive: true });
1076
+ // Generate next task ID
1077
+ // Use taskIndex as source of truth (works for both lazy and full initialization)
1078
+ const existingIds = Array.from(this.lazyInitialized ? this.taskIndex.keys() : this.tasks.keys())
1079
+ .map((id) => parseInt(id.replace(/\D/g, ""), 10))
1080
+ .filter((n) => !Number.isNaN(n));
1081
+ const nextId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 1;
1082
+ const taskId = String(nextId);
1083
+ // Validate and normalize status
1084
+ const configStatuses = this.config?.statuses || [
1085
+ DEFAULT_TASK_STATUSES.TODO,
1086
+ DEFAULT_TASK_STATUSES.IN_PROGRESS,
1087
+ DEFAULT_TASK_STATUSES.DONE,
1088
+ ];
1089
+ let status = input.status || this.config?.defaultStatus || DEFAULT_TASK_STATUSES.TODO;
1090
+ // Validate status against configured statuses
1091
+ if (!configStatuses.includes(status)) {
1092
+ console.warn(`Warning: Status "${status}" is not in configured statuses [${configStatuses.join(", ")}]. ` +
1093
+ `Using default status "${this.config?.defaultStatus || DEFAULT_TASK_STATUSES.TODO}" instead.`);
1094
+ status = this.config?.defaultStatus || DEFAULT_TASK_STATUSES.TODO;
1095
+ }
1096
+ // Build task object
1097
+ const now = new Date().toISOString().split("T")[0];
1098
+ const task = {
1099
+ id: taskId,
1100
+ title: input.title,
1101
+ status,
1102
+ priority: input.priority,
1103
+ assignee: input.assignee || [],
1104
+ createdDate: now,
1105
+ labels: input.labels || [],
1106
+ milestone: input.milestone,
1107
+ dependencies: input.dependencies || [],
1108
+ references: input.references || [],
1109
+ parentTaskId: input.parentTaskId,
1110
+ description: input.description,
1111
+ implementationPlan: input.implementationPlan,
1112
+ implementationNotes: input.implementationNotes,
1113
+ acceptanceCriteriaItems: input.acceptanceCriteria?.map((ac, i) => ({
1114
+ index: i + 1,
1115
+ text: ac.text,
1116
+ checked: ac.checked || false,
1117
+ })),
1118
+ rawContent: input.rawContent,
1119
+ source: "local",
1120
+ };
1121
+ // Serialize and write file
1122
+ const content = serializeTaskMarkdown(task);
1123
+ const safeTitle = input.title
1124
+ .replace(/[<>:"/\\|?*]/g, "")
1125
+ .replace(/\s+/g, " ")
1126
+ .slice(0, 50);
1127
+ const filename = `${taskId} - ${safeTitle}.md`;
1128
+ const filepath = this.fs.join(tasksDir, filename);
1129
+ await this.fs.writeFile(filepath, content);
1130
+ // Update in-memory cache
1131
+ task.filePath = filepath;
1132
+ this.tasks.set(taskId, task);
1133
+ // Also update taskIndex if in lazy mode
1134
+ if (this.lazyInitialized) {
1135
+ const relativePath = filepath.replace(`${this.projectRoot}/`, "");
1136
+ this.taskIndex.set(taskId, {
1137
+ id: taskId,
1138
+ filePath: relativePath,
1139
+ title: task.title,
1140
+ source: "tasks",
1141
+ });
1142
+ }
1143
+ // Sync milestone if specified
1144
+ if (input.milestone) {
1145
+ await this.addTaskToMilestone(taskId, input.milestone);
1146
+ }
1147
+ span.addEvent("task.create.complete", {
1148
+ "output.taskId": taskId,
1149
+ "output.taskIndex": nextId,
1150
+ "duration.ms": Date.now() - startTime,
1151
+ });
1152
+ span.setStatus({ code: SpanStatusCode.OK });
1153
+ span.end();
1154
+ return task;
1155
+ }
1156
+ catch (error) {
1157
+ span.addEvent("task.create.error", {
1158
+ "error.type": error instanceof Error ? error.name : "UnknownError",
1159
+ "error.message": error instanceof Error ? error.message : String(error),
1160
+ });
1161
+ span.setStatus({
1162
+ code: SpanStatusCode.ERROR,
1163
+ message: error instanceof Error ? error.message : String(error),
1164
+ });
1165
+ span.end();
1166
+ throw error;
1167
+ }
1168
+ });
945
1169
  }
946
1170
  /**
947
1171
  * Update an existing task
@@ -952,105 +1176,153 @@ export class Core {
952
1176
  */
953
1177
  async updateTask(id, input) {
954
1178
  this.ensureInitialized();
955
- const existing = this.tasks.get(id);
956
- if (!existing) {
957
- return null;
958
- }
959
- const oldMilestone = existing.milestone;
960
- const newMilestone = input.milestone === null
961
- ? undefined
962
- : input.milestone !== undefined
963
- ? input.milestone
964
- : oldMilestone;
965
- // Build updated task
966
- const now = new Date().toISOString().split("T")[0];
967
- const updated = {
968
- ...existing,
969
- title: input.title ?? existing.title,
970
- status: input.status ?? existing.status,
971
- priority: input.priority ?? existing.priority,
972
- milestone: newMilestone,
973
- updatedDate: now,
974
- description: input.description ?? existing.description,
975
- implementationPlan: input.clearImplementationPlan
976
- ? undefined
977
- : (input.implementationPlan ?? existing.implementationPlan),
978
- implementationNotes: input.clearImplementationNotes
979
- ? undefined
980
- : (input.implementationNotes ?? existing.implementationNotes),
981
- ordinal: input.ordinal ?? existing.ordinal,
982
- dependencies: input.dependencies ?? existing.dependencies,
983
- references: input.references ?? existing.references ?? [],
984
- };
985
- // Handle label operations
986
- if (input.labels) {
987
- updated.labels = input.labels;
988
- }
989
- else {
990
- if (input.addLabels) {
991
- updated.labels = [...new Set([...updated.labels, ...input.addLabels])];
992
- }
993
- if (input.removeLabels) {
994
- updated.labels = updated.labels.filter((l) => !input.removeLabels?.includes(l));
995
- }
996
- }
997
- // Handle assignee
998
- if (input.assignee) {
999
- updated.assignee = input.assignee;
1000
- }
1001
- // Handle dependency operations
1002
- if (input.addDependencies) {
1003
- updated.dependencies = [...new Set([...updated.dependencies, ...input.addDependencies])];
1004
- }
1005
- if (input.removeDependencies) {
1006
- updated.dependencies = updated.dependencies.filter((d) => !input.removeDependencies?.includes(d));
1007
- }
1008
- // Handle references operations
1009
- if (input.addReferences) {
1010
- updated.references = [...new Set([...(updated.references || []), ...input.addReferences])];
1011
- }
1012
- if (input.removeReferences) {
1013
- updated.references = (updated.references || []).filter((r) => !input.removeReferences?.includes(r));
1014
- }
1015
- // Handle acceptance criteria
1016
- if (input.acceptanceCriteria) {
1017
- updated.acceptanceCriteriaItems = input.acceptanceCriteria.map((ac, i) => ({
1018
- index: i + 1,
1019
- text: ac.text,
1020
- checked: ac.checked || false,
1021
- }));
1022
- }
1023
- // Serialize and write file
1024
- const content = serializeTaskMarkdown(updated);
1025
- // Delete old file if exists
1026
- if (existing.filePath) {
1027
- await this.fs.deleteFile(existing.filePath).catch(() => { });
1028
- }
1029
- // Write new file
1030
- const tasksDir = this.getTasksDir();
1031
- const safeTitle = updated.title
1032
- .replace(/[<>:"/\\|?*]/g, "")
1033
- .replace(/\s+/g, " ")
1034
- .slice(0, 50);
1035
- const filename = `${id} - ${safeTitle}.md`;
1036
- const filepath = this.fs.join(tasksDir, filename);
1037
- await this.fs.writeFile(filepath, content);
1038
- // Update in-memory cache
1039
- updated.filePath = filepath;
1040
- this.tasks.set(id, updated);
1041
- // Handle milestone sync
1042
- const milestoneChanged = milestoneKey(oldMilestone) !== milestoneKey(newMilestone);
1043
- if (milestoneChanged) {
1044
- // Remove from old milestone
1045
- if (oldMilestone) {
1046
- await this.removeTaskFromMilestone(id, oldMilestone);
1179
+ const startTime = Date.now();
1180
+ const span = this.tracer.startSpan("task.update", {
1181
+ attributes: {
1182
+ "input.taskId": id,
1183
+ "input.hasTitle": input.title !== undefined,
1184
+ "input.hasStatus": input.status !== undefined,
1185
+ "input.hasMilestone": input.milestone !== undefined,
1186
+ },
1187
+ });
1188
+ return await context.with(trace.setSpan(context.active(), span), async () => {
1189
+ try {
1190
+ span.addEvent("task.update.started", {
1191
+ "input.taskId": id,
1192
+ "input.hasTitle": input.title !== undefined,
1193
+ "input.hasStatus": input.status !== undefined,
1194
+ "input.hasMilestone": input.milestone !== undefined,
1195
+ });
1196
+ const existing = this.tasks.get(id);
1197
+ if (!existing) {
1198
+ span.addEvent("task.update.error", {
1199
+ "error.type": "NotFoundError",
1200
+ "error.message": "Task not found",
1201
+ "input.taskId": id,
1202
+ });
1203
+ span.setStatus({ code: SpanStatusCode.OK }); // Not found is not an error
1204
+ span.end();
1205
+ return null;
1206
+ }
1207
+ const oldMilestone = existing.milestone;
1208
+ const newMilestone = input.milestone === null
1209
+ ? undefined
1210
+ : input.milestone !== undefined
1211
+ ? input.milestone
1212
+ : oldMilestone;
1213
+ // Build updated task
1214
+ const now = new Date().toISOString().split("T")[0];
1215
+ const updated = {
1216
+ ...existing,
1217
+ title: input.title ?? existing.title,
1218
+ status: input.status ?? existing.status,
1219
+ priority: input.priority ?? existing.priority,
1220
+ milestone: newMilestone,
1221
+ updatedDate: now,
1222
+ description: input.description ?? existing.description,
1223
+ implementationPlan: input.clearImplementationPlan
1224
+ ? undefined
1225
+ : (input.implementationPlan ?? existing.implementationPlan),
1226
+ implementationNotes: input.clearImplementationNotes
1227
+ ? undefined
1228
+ : (input.implementationNotes ?? existing.implementationNotes),
1229
+ ordinal: input.ordinal ?? existing.ordinal,
1230
+ dependencies: input.dependencies ?? existing.dependencies,
1231
+ references: input.references ?? existing.references ?? [],
1232
+ };
1233
+ // Handle label operations
1234
+ if (input.labels) {
1235
+ updated.labels = input.labels;
1236
+ }
1237
+ else {
1238
+ if (input.addLabels) {
1239
+ updated.labels = [...new Set([...updated.labels, ...input.addLabels])];
1240
+ }
1241
+ if (input.removeLabels) {
1242
+ updated.labels = updated.labels.filter((l) => !input.removeLabels?.includes(l));
1243
+ }
1244
+ }
1245
+ // Handle assignee
1246
+ if (input.assignee) {
1247
+ updated.assignee = input.assignee;
1248
+ }
1249
+ // Handle dependency operations
1250
+ if (input.addDependencies) {
1251
+ updated.dependencies = [...new Set([...updated.dependencies, ...input.addDependencies])];
1252
+ }
1253
+ if (input.removeDependencies) {
1254
+ updated.dependencies = updated.dependencies.filter((d) => !input.removeDependencies?.includes(d));
1255
+ }
1256
+ // Handle references operations
1257
+ if (input.addReferences) {
1258
+ updated.references = [
1259
+ ...new Set([...(updated.references || []), ...input.addReferences]),
1260
+ ];
1261
+ }
1262
+ if (input.removeReferences) {
1263
+ updated.references = (updated.references || []).filter((r) => !input.removeReferences?.includes(r));
1264
+ }
1265
+ // Handle acceptance criteria
1266
+ if (input.acceptanceCriteria) {
1267
+ updated.acceptanceCriteriaItems = input.acceptanceCriteria.map((ac, i) => ({
1268
+ index: i + 1,
1269
+ text: ac.text,
1270
+ checked: ac.checked || false,
1271
+ }));
1272
+ }
1273
+ // Serialize and write file
1274
+ const content = serializeTaskMarkdown(updated);
1275
+ // Delete old file if exists
1276
+ if (existing.filePath) {
1277
+ await this.fs.deleteFile(existing.filePath).catch(() => { });
1278
+ }
1279
+ // Write new file
1280
+ const tasksDir = this.getTasksDir();
1281
+ const safeTitle = updated.title
1282
+ .replace(/[<>:"/\\|?*]/g, "")
1283
+ .replace(/\s+/g, " ")
1284
+ .slice(0, 50);
1285
+ const filename = `${id} - ${safeTitle}.md`;
1286
+ const filepath = this.fs.join(tasksDir, filename);
1287
+ await this.fs.writeFile(filepath, content);
1288
+ // Update in-memory cache
1289
+ updated.filePath = filepath;
1290
+ this.tasks.set(id, updated);
1291
+ // Handle milestone sync
1292
+ const milestoneChanged = milestoneKey(oldMilestone) !== milestoneKey(newMilestone);
1293
+ if (milestoneChanged) {
1294
+ // Remove from old milestone
1295
+ if (oldMilestone) {
1296
+ await this.removeTaskFromMilestone(id, oldMilestone);
1297
+ }
1298
+ // Add to new milestone
1299
+ if (newMilestone) {
1300
+ await this.addTaskToMilestone(id, newMilestone);
1301
+ }
1302
+ }
1303
+ span.addEvent("task.update.complete", {
1304
+ "output.taskId": id,
1305
+ "output.statusChanged": input.status !== undefined,
1306
+ "duration.ms": Date.now() - startTime,
1307
+ });
1308
+ span.setStatus({ code: SpanStatusCode.OK });
1309
+ span.end();
1310
+ return updated;
1047
1311
  }
1048
- // Add to new milestone
1049
- if (newMilestone) {
1050
- await this.addTaskToMilestone(id, newMilestone);
1312
+ catch (error) {
1313
+ span.addEvent("task.update.error", {
1314
+ "error.type": error instanceof Error ? error.name : "UnknownError",
1315
+ "error.message": error instanceof Error ? error.message : String(error),
1316
+ "input.taskId": id,
1317
+ });
1318
+ span.setStatus({
1319
+ code: SpanStatusCode.ERROR,
1320
+ message: error instanceof Error ? error.message : String(error),
1321
+ });
1322
+ span.end();
1323
+ throw error;
1051
1324
  }
1052
- }
1053
- return updated;
1325
+ });
1054
1326
  }
1055
1327
  /**
1056
1328
  * Delete a task
@@ -1060,26 +1332,62 @@ export class Core {
1060
1332
  */
1061
1333
  async deleteTask(id) {
1062
1334
  this.ensureInitialized();
1063
- const task = this.tasks.get(id);
1064
- if (!task) {
1065
- return false;
1066
- }
1067
- // Remove from milestone if assigned
1068
- if (task.milestone) {
1069
- await this.removeTaskFromMilestone(id, task.milestone);
1070
- }
1071
- // Delete file
1072
- if (task.filePath) {
1335
+ const startTime = Date.now();
1336
+ const span = this.tracer.startSpan("task.delete", {
1337
+ attributes: { "input.taskId": id },
1338
+ });
1339
+ return await context.with(trace.setSpan(context.active(), span), async () => {
1073
1340
  try {
1074
- await this.fs.deleteFile(task.filePath);
1341
+ span.addEvent("task.delete.started", { "input.taskId": id });
1342
+ const task = this.tasks.get(id);
1343
+ if (!task) {
1344
+ span.addEvent("task.delete.error", {
1345
+ "error.type": "NotFoundError",
1346
+ "error.message": "Task not found",
1347
+ "input.taskId": id,
1348
+ });
1349
+ span.setStatus({ code: SpanStatusCode.OK });
1350
+ span.end();
1351
+ return false;
1352
+ }
1353
+ // Remove from milestone if assigned
1354
+ if (task.milestone) {
1355
+ await this.removeTaskFromMilestone(id, task.milestone);
1356
+ }
1357
+ // Delete file
1358
+ if (task.filePath) {
1359
+ try {
1360
+ await this.fs.deleteFile(task.filePath);
1361
+ }
1362
+ catch {
1363
+ // File may already be deleted
1364
+ }
1365
+ }
1366
+ // Remove from in-memory cache
1367
+ this.tasks.delete(id);
1368
+ span.addEvent("task.delete.complete", {
1369
+ "output.taskId": id,
1370
+ "output.deleted": true,
1371
+ "duration.ms": Date.now() - startTime,
1372
+ });
1373
+ span.setStatus({ code: SpanStatusCode.OK });
1374
+ span.end();
1375
+ return true;
1075
1376
  }
1076
- catch {
1077
- // File may already be deleted
1377
+ catch (error) {
1378
+ span.addEvent("task.delete.error", {
1379
+ "error.type": error instanceof Error ? error.name : "UnknownError",
1380
+ "error.message": error instanceof Error ? error.message : String(error),
1381
+ "input.taskId": id,
1382
+ });
1383
+ span.setStatus({
1384
+ code: SpanStatusCode.ERROR,
1385
+ message: error instanceof Error ? error.message : String(error),
1386
+ });
1387
+ span.end();
1388
+ throw error;
1078
1389
  }
1079
- }
1080
- // Remove from in-memory cache
1081
- this.tasks.delete(id);
1082
- return true;
1390
+ });
1083
1391
  }
1084
1392
  /**
1085
1393
  * Archive a task (move from tasks/ to completed/)
@@ -1089,53 +1397,96 @@ export class Core {
1089
1397
  */
1090
1398
  async archiveTask(id) {
1091
1399
  this.ensureInitialized();
1092
- const task = this.tasks.get(id);
1093
- if (!task) {
1094
- return null;
1095
- }
1096
- // Check if already in completed
1097
- if (task.source === "completed") {
1098
- return null;
1099
- }
1100
- const completedDir = this.getCompletedDir();
1101
- // Ensure completed directory exists
1102
- await this.fs.createDir(completedDir, { recursive: true });
1103
- // Build new filepath in completed/
1104
- const safeTitle = task.title
1105
- .replace(/[<>:"/\\|?*]/g, "")
1106
- .replace(/\s+/g, " ")
1107
- .slice(0, 50);
1108
- const filename = `${id} - ${safeTitle}.md`;
1109
- const newFilepath = this.fs.join(completedDir, filename);
1110
- // Delete old file
1111
- if (task.filePath) {
1400
+ const startTime = Date.now();
1401
+ const span = this.tracer.startSpan("task.archive", {
1402
+ attributes: { "input.taskId": id },
1403
+ });
1404
+ return await context.with(trace.setSpan(context.active(), span), async () => {
1112
1405
  try {
1113
- await this.fs.deleteFile(task.filePath);
1114
- }
1115
- catch {
1116
- // File may not exist
1406
+ span.addEvent("task.archive.started", { "input.taskId": id });
1407
+ const task = this.tasks.get(id);
1408
+ if (!task) {
1409
+ span.addEvent("task.archive.error", {
1410
+ "error.type": "NotFoundError",
1411
+ "error.message": "Task not found",
1412
+ "input.taskId": id,
1413
+ });
1414
+ span.setStatus({ code: SpanStatusCode.OK }); // Not found is not an error
1415
+ span.end();
1416
+ return null;
1417
+ }
1418
+ // Check if already in completed
1419
+ if (task.source === "completed") {
1420
+ span.addEvent("task.archive.error", {
1421
+ "error.type": "InvalidStateError",
1422
+ "error.message": "Task already archived",
1423
+ "input.taskId": id,
1424
+ });
1425
+ span.setStatus({ code: SpanStatusCode.OK });
1426
+ span.end();
1427
+ return null;
1428
+ }
1429
+ const completedDir = this.getCompletedDir();
1430
+ // Ensure completed directory exists
1431
+ await this.fs.createDir(completedDir, { recursive: true });
1432
+ // Build new filepath in completed/
1433
+ const safeTitle = task.title
1434
+ .replace(/[<>:"/\\|?*]/g, "")
1435
+ .replace(/\s+/g, " ")
1436
+ .slice(0, 50);
1437
+ const filename = `${id} - ${safeTitle}.md`;
1438
+ const newFilepath = this.fs.join(completedDir, filename);
1439
+ // Delete old file
1440
+ if (task.filePath) {
1441
+ try {
1442
+ await this.fs.deleteFile(task.filePath);
1443
+ }
1444
+ catch {
1445
+ // File may not exist
1446
+ }
1447
+ }
1448
+ // Update task
1449
+ const archived = {
1450
+ ...task,
1451
+ source: "completed",
1452
+ filePath: newFilepath,
1453
+ };
1454
+ // Write to new location
1455
+ const content = serializeTaskMarkdown(archived);
1456
+ await this.fs.writeFile(newFilepath, content);
1457
+ // Update in-memory cache
1458
+ this.tasks.set(id, archived);
1459
+ // Update task index if lazy initialized
1460
+ if (this.lazyInitialized) {
1461
+ const entry = this.taskIndex.get(id);
1462
+ if (entry) {
1463
+ entry.source = "completed";
1464
+ entry.filePath = newFilepath;
1465
+ }
1466
+ }
1467
+ span.addEvent("task.archive.complete", {
1468
+ "output.taskId": id,
1469
+ "output.newPath": newFilepath,
1470
+ "duration.ms": Date.now() - startTime,
1471
+ });
1472
+ span.setStatus({ code: SpanStatusCode.OK });
1473
+ span.end();
1474
+ return archived;
1117
1475
  }
1118
- }
1119
- // Update task
1120
- const archived = {
1121
- ...task,
1122
- source: "completed",
1123
- filePath: newFilepath,
1124
- };
1125
- // Write to new location
1126
- const content = serializeTaskMarkdown(archived);
1127
- await this.fs.writeFile(newFilepath, content);
1128
- // Update in-memory cache
1129
- this.tasks.set(id, archived);
1130
- // Update task index if lazy initialized
1131
- if (this.lazyInitialized) {
1132
- const entry = this.taskIndex.get(id);
1133
- if (entry) {
1134
- entry.source = "completed";
1135
- entry.filePath = newFilepath;
1476
+ catch (error) {
1477
+ span.addEvent("task.archive.error", {
1478
+ "error.type": error instanceof Error ? error.name : "UnknownError",
1479
+ "error.message": error instanceof Error ? error.message : String(error),
1480
+ "input.taskId": id,
1481
+ });
1482
+ span.setStatus({
1483
+ code: SpanStatusCode.ERROR,
1484
+ message: error instanceof Error ? error.message : String(error),
1485
+ });
1486
+ span.end();
1487
+ throw error;
1136
1488
  }
1137
- }
1138
- return archived;
1489
+ });
1139
1490
  }
1140
1491
  /**
1141
1492
  * Restore a task (move from completed/ to tasks/)
@@ -1145,53 +1496,96 @@ export class Core {
1145
1496
  */
1146
1497
  async restoreTask(id) {
1147
1498
  this.ensureInitialized();
1148
- const task = this.tasks.get(id);
1149
- if (!task) {
1150
- return null;
1151
- }
1152
- // Check if in completed
1153
- if (task.source !== "completed") {
1154
- return null;
1155
- }
1156
- const tasksDir = this.getTasksDir();
1157
- // Ensure tasks directory exists
1158
- await this.fs.createDir(tasksDir, { recursive: true });
1159
- // Build new filepath in tasks/
1160
- const safeTitle = task.title
1161
- .replace(/[<>:"/\\|?*]/g, "")
1162
- .replace(/\s+/g, " ")
1163
- .slice(0, 50);
1164
- const filename = `${id} - ${safeTitle}.md`;
1165
- const newFilepath = this.fs.join(tasksDir, filename);
1166
- // Delete old file
1167
- if (task.filePath) {
1499
+ const startTime = Date.now();
1500
+ const span = this.tracer.startSpan("task.restore", {
1501
+ attributes: { "input.taskId": id },
1502
+ });
1503
+ return await context.with(trace.setSpan(context.active(), span), async () => {
1168
1504
  try {
1169
- await this.fs.deleteFile(task.filePath);
1170
- }
1171
- catch {
1172
- // File may not exist
1505
+ span.addEvent("task.restore.started", { "input.taskId": id });
1506
+ const task = this.tasks.get(id);
1507
+ if (!task) {
1508
+ span.addEvent("task.restore.error", {
1509
+ "error.type": "NotFoundError",
1510
+ "error.message": "Task not found",
1511
+ "input.taskId": id,
1512
+ });
1513
+ span.setStatus({ code: SpanStatusCode.OK }); // Not found is not an error
1514
+ span.end();
1515
+ return null;
1516
+ }
1517
+ // Check if in completed
1518
+ if (task.source !== "completed") {
1519
+ span.addEvent("task.restore.error", {
1520
+ "error.type": "InvalidStateError",
1521
+ "error.message": "Task not in completed",
1522
+ "input.taskId": id,
1523
+ });
1524
+ span.setStatus({ code: SpanStatusCode.OK });
1525
+ span.end();
1526
+ return null;
1527
+ }
1528
+ const tasksDir = this.getTasksDir();
1529
+ // Ensure tasks directory exists
1530
+ await this.fs.createDir(tasksDir, { recursive: true });
1531
+ // Build new filepath in tasks/
1532
+ const safeTitle = task.title
1533
+ .replace(/[<>:"/\\|?*]/g, "")
1534
+ .replace(/\s+/g, " ")
1535
+ .slice(0, 50);
1536
+ const filename = `${id} - ${safeTitle}.md`;
1537
+ const newFilepath = this.fs.join(tasksDir, filename);
1538
+ // Delete old file
1539
+ if (task.filePath) {
1540
+ try {
1541
+ await this.fs.deleteFile(task.filePath);
1542
+ }
1543
+ catch {
1544
+ // File may not exist
1545
+ }
1546
+ }
1547
+ // Update task
1548
+ const restored = {
1549
+ ...task,
1550
+ source: "local",
1551
+ filePath: newFilepath,
1552
+ };
1553
+ // Write to new location
1554
+ const content = serializeTaskMarkdown(restored);
1555
+ await this.fs.writeFile(newFilepath, content);
1556
+ // Update in-memory cache
1557
+ this.tasks.set(id, restored);
1558
+ // Update task index if lazy initialized
1559
+ if (this.lazyInitialized) {
1560
+ const entry = this.taskIndex.get(id);
1561
+ if (entry) {
1562
+ entry.source = "tasks";
1563
+ entry.filePath = newFilepath;
1564
+ }
1565
+ }
1566
+ span.addEvent("task.restore.complete", {
1567
+ "output.taskId": id,
1568
+ "output.newPath": newFilepath,
1569
+ "duration.ms": Date.now() - startTime,
1570
+ });
1571
+ span.setStatus({ code: SpanStatusCode.OK });
1572
+ span.end();
1573
+ return restored;
1173
1574
  }
1174
- }
1175
- // Update task
1176
- const restored = {
1177
- ...task,
1178
- source: "local",
1179
- filePath: newFilepath,
1180
- };
1181
- // Write to new location
1182
- const content = serializeTaskMarkdown(restored);
1183
- await this.fs.writeFile(newFilepath, content);
1184
- // Update in-memory cache
1185
- this.tasks.set(id, restored);
1186
- // Update task index if lazy initialized
1187
- if (this.lazyInitialized) {
1188
- const entry = this.taskIndex.get(id);
1189
- if (entry) {
1190
- entry.source = "tasks";
1191
- entry.filePath = newFilepath;
1575
+ catch (error) {
1576
+ span.addEvent("task.restore.error", {
1577
+ "error.type": error instanceof Error ? error.name : "UnknownError",
1578
+ "error.message": error instanceof Error ? error.message : String(error),
1579
+ "input.taskId": id,
1580
+ });
1581
+ span.setStatus({
1582
+ code: SpanStatusCode.ERROR,
1583
+ message: error instanceof Error ? error.message : String(error),
1584
+ });
1585
+ span.end();
1586
+ throw error;
1192
1587
  }
1193
- }
1194
- return restored;
1588
+ });
1195
1589
  }
1196
1590
  /**
1197
1591
  * Load specific tasks by their IDs (for lazy loading milestone tasks)