@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.d.ts.map +1 -1
- package/dist/core/Core.js +788 -401
- package/dist/core/Core.js.map +1 -1
- package/dist/test/milestone-crud.otel.test.d.ts +8 -0
- package/dist/test/milestone-crud.otel.test.d.ts.map +1 -0
- package/dist/test/milestone-crud.otel.test.js +143 -0
- package/dist/test/milestone-crud.otel.test.js.map +1 -0
- package/dist/test/task-crud.otel.test.d.ts +8 -0
- package/dist/test/task-crud.otel.test.d.ts.map +1 -0
- package/dist/test/task-crud.otel.test.js +181 -0
- package/dist/test/task-crud.otel.test.js.map +1 -0
- package/package.json +1 -1
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
updated
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
}
|
|
1128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
}
|
|
1157
|
-
|
|
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
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
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
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
}
|
|
1213
|
-
|
|
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
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
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
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
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)
|