@industry-theme/backlogmd-kanban-panel 1.2.9 → 1.2.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.
@@ -1,11 +1,9 @@
1
- var __defProp = Object.defineProperty;
2
- var __defNormalProp = (obj, key2, value) => key2 in obj ? __defProp(obj, key2, { enumerable: true, configurable: true, writable: true, value }) : obj[key2] = value;
3
- var __publicField = (obj, key2, value) => __defNormalProp(obj, typeof key2 !== "symbol" ? key2 + "" : key2, value);
4
1
  import * as React2 from "react";
5
2
  import React2__default, { useLayoutEffect, useEffect, useRef, useMemo, useCallback, useState, createContext, memo, useReducer, useContext, forwardRef, cloneElement, createElement, useImperativeHandle } from "react";
6
3
  import { unstable_batchedUpdates, createPortal } from "react-dom";
7
4
  import { jsxs, Fragment, jsx } from "react/jsx-runtime";
8
- import { trace, SpanStatusCode, context } from "@opentelemetry/api";
5
+ import { DEFAULT_TASK_STATUSES, Core } from "@backlog-md/core";
6
+ import { trace, context, SpanStatusCode } from "@opentelemetry/api";
9
7
  const canUseDOM = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined";
10
8
  function isWindow(element2) {
11
9
  const elementString = Object.prototype.toString.call(element2);
@@ -6338,2152 +6336,7 @@ function gt(o, e, t, r2) {
6338
6336
  };
6339
6337
  }, [o, e, t, r2]);
6340
6338
  }
6341
- const DEFAULT_TASK_STATUSES = {
6342
- TODO: "To Do",
6343
- IN_PROGRESS: "In Progress",
6344
- DONE: "Done"
6345
- };
6346
- function parseTaskMarkdown(content2, filePath) {
6347
- const { frontmatter, title, rawContent, acceptanceCriteria, description } = parseMarkdownContent(content2);
6348
- const id = frontmatter.id || extractIdFromPath(filePath);
6349
- const taskTitle = frontmatter.title || title || `Task ${id}`;
6350
- return {
6351
- id,
6352
- title: taskTitle,
6353
- status: frontmatter.status || "backlog",
6354
- priority: frontmatter.priority,
6355
- assignee: frontmatter.assignee || [],
6356
- reporter: frontmatter.reporter,
6357
- createdDate: frontmatter.createdDate || (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
6358
- updatedDate: frontmatter.updatedDate,
6359
- labels: frontmatter.labels || [],
6360
- milestone: frontmatter.milestone,
6361
- dependencies: frontmatter.dependencies || [],
6362
- references: frontmatter.references || [],
6363
- parentTaskId: frontmatter.parentTaskId,
6364
- subtasks: frontmatter.subtasks,
6365
- branch: frontmatter.branch,
6366
- ordinal: frontmatter.ordinal,
6367
- rawContent,
6368
- description,
6369
- acceptanceCriteriaItems: acceptanceCriteria,
6370
- filePath
6371
- };
6372
- }
6373
- function serializeTaskMarkdown(task) {
6374
- const lines = [];
6375
- lines.push("---");
6376
- lines.push(`status: ${task.status}`);
6377
- if (task.priority) {
6378
- lines.push(`priority: ${task.priority}`);
6379
- }
6380
- if (task.assignee && task.assignee.length > 0) {
6381
- lines.push(`assignee: [${task.assignee.join(", ")}]`);
6382
- }
6383
- if (task.reporter) {
6384
- lines.push(`reporter: ${task.reporter}`);
6385
- }
6386
- if (task.labels && task.labels.length > 0) {
6387
- lines.push(`labels: [${task.labels.join(", ")}]`);
6388
- }
6389
- if (task.milestone) {
6390
- lines.push(`milestone: ${task.milestone}`);
6391
- }
6392
- if (task.dependencies && task.dependencies.length > 0) {
6393
- lines.push(`dependencies: [${task.dependencies.join(", ")}]`);
6394
- }
6395
- if (task.references && task.references.length > 0) {
6396
- lines.push(`references: [${task.references.join(", ")}]`);
6397
- }
6398
- if (task.parentTaskId) {
6399
- lines.push(`parentTaskId: ${task.parentTaskId}`);
6400
- }
6401
- if (task.subtasks && task.subtasks.length > 0) {
6402
- lines.push(`subtasks: [${task.subtasks.join(", ")}]`);
6403
- }
6404
- if (task.branch) {
6405
- lines.push(`branch: ${task.branch}`);
6406
- }
6407
- if (task.ordinal !== void 0) {
6408
- lines.push(`ordinal: ${task.ordinal}`);
6409
- }
6410
- if (task.createdDate) {
6411
- lines.push(`createdDate: ${task.createdDate}`);
6412
- }
6413
- if (task.updatedDate) {
6414
- lines.push(`updatedDate: ${task.updatedDate}`);
6415
- }
6416
- lines.push("---");
6417
- lines.push("");
6418
- lines.push(`# ${task.title}`);
6419
- lines.push("");
6420
- if (task.description) {
6421
- lines.push(task.description);
6422
- lines.push("");
6423
- }
6424
- if (task.acceptanceCriteriaItems && task.acceptanceCriteriaItems.length > 0) {
6425
- lines.push("## Acceptance Criteria");
6426
- lines.push("");
6427
- for (const criterion of task.acceptanceCriteriaItems) {
6428
- const checkbox = criterion.checked ? "[x]" : "[ ]";
6429
- lines.push(`- ${checkbox} ${criterion.text}`);
6430
- }
6431
- lines.push("");
6432
- }
6433
- if (task.implementationPlan) {
6434
- lines.push("## Implementation Plan");
6435
- lines.push("");
6436
- lines.push(task.implementationPlan);
6437
- lines.push("");
6438
- }
6439
- if (task.implementationNotes) {
6440
- lines.push("## Implementation Notes");
6441
- lines.push("");
6442
- lines.push(task.implementationNotes);
6443
- lines.push("");
6444
- }
6445
- return lines.join("\n");
6446
- }
6447
- function parseMarkdownContent(content2) {
6448
- let frontmatter = {};
6449
- let remaining = content2;
6450
- const frontmatterMatch = content2.match(/^---\n([\s\S]*?)\n---\n/);
6451
- if (frontmatterMatch) {
6452
- frontmatter = parseFrontmatter(frontmatterMatch[1]);
6453
- remaining = content2.slice(frontmatterMatch[0].length);
6454
- }
6455
- let title = "";
6456
- const titleMatch = remaining.match(/^#\s+(.+)$/m);
6457
- if (titleMatch) {
6458
- title = titleMatch[1].trim();
6459
- }
6460
- const acceptanceCriteria = parseAcceptanceCriteria(remaining);
6461
- const rawContent = remaining.trim();
6462
- const description = extractDescription(remaining, title);
6463
- return { frontmatter, title, rawContent, description, acceptanceCriteria };
6464
- }
6465
- function parseFrontmatter(raw2) {
6466
- const result = {};
6467
- for (const line of raw2.split("\n")) {
6468
- const match = line.match(/^(\w+):\s*(.+)$/);
6469
- if (!match)
6470
- continue;
6471
- const [, key2, value] = match;
6472
- let cleanValue = value.trim();
6473
- if (cleanValue.startsWith('"') && cleanValue.endsWith('"') || cleanValue.startsWith("'") && cleanValue.endsWith("'")) {
6474
- cleanValue = cleanValue.slice(1, -1);
6475
- }
6476
- switch (key2) {
6477
- case "id":
6478
- case "title":
6479
- result[key2] = cleanValue;
6480
- break;
6481
- case "status":
6482
- result.status = cleanValue;
6483
- break;
6484
- case "priority":
6485
- result.priority = cleanValue;
6486
- break;
6487
- case "ordinal":
6488
- result.ordinal = parseInt(cleanValue, 10);
6489
- break;
6490
- case "reporter":
6491
- case "milestone":
6492
- case "parentTaskId":
6493
- case "branch":
6494
- case "createdDate":
6495
- case "updatedDate":
6496
- result[key2] = cleanValue;
6497
- break;
6498
- case "assignee":
6499
- case "labels":
6500
- case "dependencies":
6501
- case "references":
6502
- case "subtasks":
6503
- result[key2] = parseArrayValue(value);
6504
- break;
6505
- }
6506
- }
6507
- return result;
6508
- }
6509
- function parseArrayValue(value) {
6510
- const cleaned = value.replace(/^\[|\]$/g, "").trim();
6511
- if (!cleaned)
6512
- return [];
6513
- return cleaned.split(",").map((s2) => s2.trim()).filter(Boolean);
6514
- }
6515
- function parseAcceptanceCriteria(content2) {
6516
- const criteria = [];
6517
- const checkboxPattern = /^-\s*\[([ xX])\]\s*(.+)$/gm;
6518
- const matches = content2.matchAll(checkboxPattern);
6519
- let index2 = 1;
6520
- for (const match of matches) {
6521
- criteria.push({
6522
- index: index2++,
6523
- checked: match[1].toLowerCase() === "x",
6524
- text: match[2].trim()
6525
- });
6526
- }
6527
- return criteria;
6528
- }
6529
- function extractDescription(content2, title) {
6530
- const descriptionSection = extractSection(content2, "Description");
6531
- if (descriptionSection) {
6532
- return stripHtmlComments(descriptionSection).trim();
6533
- }
6534
- let body = content2;
6535
- if (title) {
6536
- body = body.replace(new RegExp(`^#\\s+${escapeRegex(title)}\\s*\\n?`, "m"), "");
6537
- }
6538
- const firstSectionMatch = body.match(/^##\s+/m);
6539
- if (firstSectionMatch && firstSectionMatch.index !== void 0) {
6540
- body = body.slice(0, firstSectionMatch.index);
6541
- }
6542
- body = stripHtmlComments(body);
6543
- return body.trim();
6544
- }
6545
- function stripHtmlComments(content2) {
6546
- return content2.replace(/<!--[\s\S]*?-->/g, "").trim();
6547
- }
6548
- function extractIdFromPath(filePath) {
6549
- const filename = filePath.split("/").pop() || "";
6550
- const match = filename.match(/^(?:task-)?(\d+(?:\.\d+)?)\s*-/);
6551
- if (match) {
6552
- return match[1];
6553
- }
6554
- return filename.replace(/\.md$/, "");
6555
- }
6556
- function extractTitleFromPath(filePath) {
6557
- const filename = filePath.split("/").pop() || "";
6558
- const match = filename.match(/^(?:task-)?\d+(?:\.\d+)?\s*-\s*(.+)\.md$/);
6559
- if (match) {
6560
- return match[1].trim();
6561
- }
6562
- return filename.replace(/\.md$/, "");
6563
- }
6564
- function extractSourceFromPath(filePath) {
6565
- if (filePath.includes("/completed/") || filePath.includes("\\completed\\")) {
6566
- return "completed";
6567
- }
6568
- return "tasks";
6569
- }
6570
- function extractTaskIndexFromPath(filePath) {
6571
- return {
6572
- id: extractIdFromPath(filePath),
6573
- filePath,
6574
- title: extractTitleFromPath(filePath),
6575
- source: extractSourceFromPath(filePath)
6576
- };
6577
- }
6578
- function escapeRegex(str) {
6579
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6580
- }
6581
- function extractSection(content2, sectionTitle) {
6582
- var _a;
6583
- const src = content2.replace(/\r\n/g, "\n");
6584
- const regex2 = new RegExp(`## ${sectionTitle}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`, "i");
6585
- const match = src.match(regex2);
6586
- return (_a = match == null ? void 0 : match[1]) == null ? void 0 : _a.trim();
6587
- }
6588
- function parseMilestoneFrontmatter(raw2) {
6589
- const result = {};
6590
- for (const line of raw2.split("\n")) {
6591
- const match = line.match(/^(\w+):\s*(.+)$/);
6592
- if (!match)
6593
- continue;
6594
- const [, key2, value] = match;
6595
- let cleanValue = value.trim();
6596
- if (cleanValue.startsWith('"') && cleanValue.endsWith('"') || cleanValue.startsWith("'") && cleanValue.endsWith("'")) {
6597
- cleanValue = cleanValue.slice(1, -1);
6598
- }
6599
- switch (key2) {
6600
- case "id":
6601
- case "title":
6602
- result[key2] = cleanValue;
6603
- break;
6604
- case "tasks":
6605
- if (cleanValue.startsWith("[") && cleanValue.endsWith("]")) {
6606
- result.tasks = cleanValue.slice(1, -1).split(",").map((s2) => s2.trim()).filter(Boolean);
6607
- }
6608
- break;
6609
- }
6610
- }
6611
- return result;
6612
- }
6613
- function parseMilestoneMarkdown(content2) {
6614
- let frontmatter = {};
6615
- let remaining = content2;
6616
- const frontmatterMatch = content2.match(/^---\n([\s\S]*?)\n---\n/);
6617
- if (frontmatterMatch) {
6618
- frontmatter = parseMilestoneFrontmatter(frontmatterMatch[1]);
6619
- remaining = content2.slice(frontmatterMatch[0].length);
6620
- }
6621
- const rawContent = remaining.trim();
6622
- const description = extractSection(rawContent, "Description") || "";
6623
- return {
6624
- id: frontmatter.id || "",
6625
- title: frontmatter.title || "",
6626
- description,
6627
- rawContent,
6628
- tasks: frontmatter.tasks || []
6629
- };
6630
- }
6631
- function serializeMilestoneMarkdown(milestone) {
6632
- const lines = [];
6633
- lines.push("---");
6634
- lines.push(`id: ${milestone.id}`);
6635
- const escapedTitle = milestone.title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
6636
- lines.push(`title: "${escapedTitle}"`);
6637
- lines.push(`tasks: [${milestone.tasks.join(", ")}]`);
6638
- lines.push("---");
6639
- lines.push("");
6640
- lines.push("## Description");
6641
- lines.push("");
6642
- lines.push(milestone.description || `Milestone: ${milestone.title}`);
6643
- lines.push("");
6644
- return lines.join("\n");
6645
- }
6646
- function getMilestoneFilename(id, title) {
6647
- const safeTitle = title.replace(/[<>:"/\\|?*]/g, "").replace(/\s+/g, "-").toLowerCase().slice(0, 50);
6648
- return `${id} - ${safeTitle}.md`;
6649
- }
6650
- function extractMilestoneIdFromFilename(filename) {
6651
- const match = filename.match(/^(m-\d+)/);
6652
- return match ? match[1] : null;
6653
- }
6654
- const version$1 = "0.3.13";
6655
- const packageJson$1 = {
6656
- version: version$1
6657
- };
6658
- const TRACER_NAME$1 = "@backlog-md/core";
6659
- const TRACER_VERSION$1 = packageJson$1.version;
6660
- function getTracer$1() {
6661
- return trace.getTracer(TRACER_NAME$1, TRACER_VERSION$1);
6662
- }
6663
- const NO_MILESTONE_KEY = "__none";
6664
- function normalizeMilestoneName(name2) {
6665
- return name2.trim();
6666
- }
6667
- function milestoneKey(name2) {
6668
- return normalizeMilestoneName(name2 ?? "").toLowerCase();
6669
- }
6670
- function isDoneStatus(status) {
6671
- const normalized = (status ?? "").toLowerCase();
6672
- return normalized.includes("done") || normalized.includes("complete");
6673
- }
6674
- function getMilestoneLabel(milestoneId, milestoneEntities) {
6675
- if (!milestoneId) {
6676
- return "Tasks without milestone";
6677
- }
6678
- const entity = milestoneEntities.find((m) => milestoneKey(m.id) === milestoneKey(milestoneId));
6679
- return (entity == null ? void 0 : entity.title) || milestoneId;
6680
- }
6681
- function collectMilestoneIds(tasks, milestoneEntities) {
6682
- const merged = [];
6683
- const seen = /* @__PURE__ */ new Set();
6684
- const addMilestone = (value) => {
6685
- const normalized = normalizeMilestoneName(value);
6686
- if (!normalized)
6687
- return;
6688
- const key2 = milestoneKey(normalized);
6689
- if (seen.has(key2))
6690
- return;
6691
- seen.add(key2);
6692
- merged.push(normalized);
6693
- };
6694
- for (const entity of milestoneEntities) {
6695
- addMilestone(entity.id);
6696
- }
6697
- for (const task of tasks) {
6698
- addMilestone(task.milestone ?? "");
6699
- }
6700
- return merged;
6701
- }
6702
- function collectMilestones(tasks, configMilestones) {
6703
- const merged = [];
6704
- const seen = /* @__PURE__ */ new Set();
6705
- const addMilestone = (value) => {
6706
- const normalized = normalizeMilestoneName(value);
6707
- if (!normalized)
6708
- return;
6709
- const key2 = milestoneKey(normalized);
6710
- if (seen.has(key2))
6711
- return;
6712
- seen.add(key2);
6713
- merged.push(normalized);
6714
- };
6715
- for (const m of configMilestones) {
6716
- addMilestone(m);
6717
- }
6718
- for (const task of tasks) {
6719
- addMilestone(task.milestone ?? "");
6720
- }
6721
- return merged;
6722
- }
6723
- function createBucket(milestoneId, tasks, statuses, milestoneEntities, isNoMilestone) {
6724
- const bucketMilestoneKey = milestoneKey(milestoneId);
6725
- const bucketTasks = tasks.filter((task) => {
6726
- const taskMilestoneKey = milestoneKey(task.milestone);
6727
- return bucketMilestoneKey ? taskMilestoneKey === bucketMilestoneKey : !taskMilestoneKey;
6728
- });
6729
- const counts = {};
6730
- for (const status of statuses) {
6731
- counts[status] = 0;
6732
- }
6733
- for (const task of bucketTasks) {
6734
- const status = task.status ?? "";
6735
- counts[status] = (counts[status] ?? 0) + 1;
6736
- }
6737
- const doneCount = bucketTasks.filter((t) => isDoneStatus(t.status)).length;
6738
- const progress = bucketTasks.length > 0 ? Math.round(doneCount / bucketTasks.length * 100) : 0;
6739
- const key2 = bucketMilestoneKey ? bucketMilestoneKey : NO_MILESTONE_KEY;
6740
- const label = getMilestoneLabel(milestoneId, milestoneEntities);
6741
- return {
6742
- key: key2,
6743
- label,
6744
- milestone: milestoneId,
6745
- isNoMilestone,
6746
- tasks: bucketTasks,
6747
- statusCounts: counts,
6748
- total: bucketTasks.length,
6749
- doneCount,
6750
- progress
6751
- };
6752
- }
6753
- function buildMilestoneBuckets(tasks, milestoneEntities, statuses) {
6754
- const allMilestoneIds = collectMilestoneIds(tasks, milestoneEntities);
6755
- const buckets = [
6756
- // "No milestone" bucket first
6757
- createBucket(void 0, tasks, statuses, milestoneEntities, true),
6758
- // Then each milestone bucket
6759
- ...allMilestoneIds.map((m) => createBucket(m, tasks, statuses, milestoneEntities, false))
6760
- ];
6761
- return buckets;
6762
- }
6763
- function buildMilestoneBucketsFromConfig(tasks, configMilestones, statuses) {
6764
- const milestoneEntities = configMilestones.map((id) => ({
6765
- id,
6766
- title: id,
6767
- description: "",
6768
- rawContent: "",
6769
- tasks: []
6770
- }));
6771
- return buildMilestoneBuckets(tasks, milestoneEntities, statuses);
6772
- }
6773
- function groupTasksByMilestone(tasks, configMilestones, statuses) {
6774
- const milestones = collectMilestones(tasks, configMilestones);
6775
- const buckets = buildMilestoneBucketsFromConfig(tasks, configMilestones, statuses);
6776
- return {
6777
- milestones,
6778
- buckets
6779
- };
6780
- }
6781
- const PRIORITY_ORDER = {
6782
- high: 0,
6783
- medium: 1,
6784
- low: 2
6785
- };
6786
- function sortTasksByTitle(tasks, direction = "asc") {
6787
- return [...tasks].sort((a, b) => {
6788
- const cmp = a.title.localeCompare(b.title);
6789
- return direction === "asc" ? cmp : -cmp;
6790
- });
6791
- }
6792
- function sortTasksBy(tasks, sortBy = "title", direction = "asc") {
6793
- switch (sortBy) {
6794
- case "title":
6795
- return sortTasksByTitle(tasks, direction);
6796
- case "createdDate":
6797
- return [...tasks].sort((a, b) => {
6798
- const cmp = a.createdDate.localeCompare(b.createdDate);
6799
- return direction === "asc" ? cmp : -cmp;
6800
- });
6801
- default: {
6802
- const sorted = sortTasks(tasks);
6803
- return direction === "desc" ? sorted.reverse() : sorted;
6804
- }
6805
- }
6806
- }
6807
- function sortTasks(tasks) {
6808
- return [...tasks].sort((a, b) => {
6809
- if (a.ordinal !== void 0 && b.ordinal !== void 0) {
6810
- return a.ordinal - b.ordinal;
6811
- }
6812
- if (a.ordinal !== void 0)
6813
- return -1;
6814
- if (b.ordinal !== void 0)
6815
- return 1;
6816
- const aPri = a.priority ? PRIORITY_ORDER[a.priority] : 3;
6817
- const bPri = b.priority ? PRIORITY_ORDER[b.priority] : 3;
6818
- if (aPri !== bPri)
6819
- return aPri - bPri;
6820
- return b.createdDate.localeCompare(a.createdDate);
6821
- });
6822
- }
6823
- function groupTasksByStatus(tasks, statuses) {
6824
- const grouped = /* @__PURE__ */ new Map();
6825
- for (const status of statuses) {
6826
- grouped.set(status, []);
6827
- }
6828
- for (const task of tasks) {
6829
- const list2 = grouped.get(task.status);
6830
- if (list2) {
6831
- list2.push(task);
6832
- } else {
6833
- grouped.set(task.status, [task]);
6834
- }
6835
- }
6836
- for (const [status, statusTasks] of grouped) {
6837
- grouped.set(status, sortTasks(statusTasks));
6838
- }
6839
- return grouped;
6840
- }
6841
- const DEFAULT_STATUSES = ["To Do", "In Progress", "Done"];
6842
- function parseBacklogConfig(content2) {
6843
- const config = {};
6844
- const lines = content2.split("\n");
6845
- for (const line of lines) {
6846
- const trimmed = line.trim();
6847
- if (!trimmed || trimmed.startsWith("#"))
6848
- continue;
6849
- const colonIndex = trimmed.indexOf(":");
6850
- if (colonIndex === -1)
6851
- continue;
6852
- const key2 = trimmed.substring(0, colonIndex).trim();
6853
- const value = trimmed.substring(colonIndex + 1).trim();
6854
- switch (key2) {
6855
- case "project_name":
6856
- config.projectName = value.replace(/['"]/g, "");
6857
- break;
6858
- case "default_assignee":
6859
- config.defaultAssignee = value.replace(/['"]/g, "");
6860
- break;
6861
- case "default_reporter":
6862
- config.defaultReporter = value.replace(/['"]/g, "");
6863
- break;
6864
- case "default_status":
6865
- config.defaultStatus = value.replace(/['"]/g, "");
6866
- break;
6867
- case "statuses":
6868
- case "labels":
6869
- case "milestones":
6870
- if (value.startsWith("[") && value.endsWith("]")) {
6871
- const arrayContent = value.slice(1, -1);
6872
- config[key2] = arrayContent.split(",").map((item) => item.trim().replace(/['"]/g, "")).filter(Boolean);
6873
- }
6874
- break;
6875
- case "date_format":
6876
- config.dateFormat = value.replace(/['"]/g, "");
6877
- break;
6878
- case "max_column_width":
6879
- config.maxColumnWidth = parseInt(value, 10);
6880
- break;
6881
- case "task_resolution_strategy":
6882
- config.taskResolutionStrategy = value.replace(/['"]/g, "");
6883
- break;
6884
- case "default_editor":
6885
- config.defaultEditor = value.replace(/["']/g, "");
6886
- break;
6887
- case "auto_open_browser":
6888
- config.autoOpenBrowser = value.toLowerCase() === "true";
6889
- break;
6890
- case "default_port":
6891
- config.defaultPort = parseInt(value, 10);
6892
- break;
6893
- case "remote_operations":
6894
- config.remoteOperations = value.toLowerCase() === "true";
6895
- break;
6896
- case "auto_commit":
6897
- config.autoCommit = value.toLowerCase() === "true";
6898
- break;
6899
- case "zero_padded_ids":
6900
- config.zeroPaddedIds = parseInt(value, 10);
6901
- break;
6902
- case "timezone_preference":
6903
- config.timezonePreference = value.replace(/['"]/g, "");
6904
- break;
6905
- case "include_date_time_in_dates":
6906
- config.includeDateTimeInDates = value.toLowerCase() === "true";
6907
- break;
6908
- case "bypass_git_hooks":
6909
- config.bypassGitHooks = value.toLowerCase() === "true";
6910
- break;
6911
- case "check_active_branches":
6912
- config.checkActiveBranches = value.toLowerCase() === "true";
6913
- break;
6914
- case "active_branch_days":
6915
- config.activeBranchDays = parseInt(value, 10);
6916
- break;
6917
- }
6918
- }
6919
- return {
6920
- projectName: config.projectName || "Backlog",
6921
- statuses: config.statuses || [...DEFAULT_STATUSES],
6922
- labels: config.labels || [],
6923
- milestones: config.milestones || [],
6924
- defaultStatus: config.defaultStatus || DEFAULT_STATUSES[0],
6925
- dateFormat: config.dateFormat || "YYYY-MM-DD",
6926
- defaultAssignee: config.defaultAssignee,
6927
- defaultReporter: config.defaultReporter,
6928
- maxColumnWidth: config.maxColumnWidth,
6929
- taskResolutionStrategy: config.taskResolutionStrategy,
6930
- defaultEditor: config.defaultEditor,
6931
- autoOpenBrowser: config.autoOpenBrowser,
6932
- defaultPort: config.defaultPort,
6933
- remoteOperations: config.remoteOperations,
6934
- autoCommit: config.autoCommit,
6935
- zeroPaddedIds: config.zeroPaddedIds,
6936
- timezonePreference: config.timezonePreference,
6937
- includeDateTimeInDates: config.includeDateTimeInDates,
6938
- bypassGitHooks: config.bypassGitHooks,
6939
- checkActiveBranches: config.checkActiveBranches,
6940
- activeBranchDays: config.activeBranchDays
6941
- };
6942
- }
6943
- function serializeBacklogConfig(config) {
6944
- const lines = [];
6945
- lines.push(`project_name: "${config.projectName}"`);
6946
- if (config.defaultStatus) {
6947
- lines.push(`default_status: "${config.defaultStatus}"`);
6948
- }
6949
- lines.push(`statuses: [${config.statuses.map((s2) => `"${s2}"`).join(", ")}]`);
6950
- lines.push(`labels: [${(config.labels || []).map((l) => `"${l}"`).join(", ")}]`);
6951
- lines.push(`milestones: [${(config.milestones || []).map((m) => `"${m}"`).join(", ")}]`);
6952
- if (config.dateFormat) {
6953
- lines.push(`date_format: "${config.dateFormat}"`);
6954
- }
6955
- if (config.defaultAssignee) {
6956
- lines.push(`default_assignee: "${config.defaultAssignee}"`);
6957
- }
6958
- if (config.defaultReporter) {
6959
- lines.push(`default_reporter: "${config.defaultReporter}"`);
6960
- }
6961
- if (config.defaultEditor) {
6962
- lines.push(`default_editor: "${config.defaultEditor}"`);
6963
- }
6964
- if (typeof config.autoCommit === "boolean") {
6965
- lines.push(`auto_commit: ${config.autoCommit}`);
6966
- }
6967
- if (typeof config.zeroPaddedIds === "number") {
6968
- lines.push(`zero_padded_ids: ${config.zeroPaddedIds}`);
6969
- }
6970
- if (typeof config.autoOpenBrowser === "boolean") {
6971
- lines.push(`auto_open_browser: ${config.autoOpenBrowser}`);
6972
- }
6973
- if (typeof config.defaultPort === "number") {
6974
- lines.push(`default_port: ${config.defaultPort}`);
6975
- }
6976
- if (typeof config.remoteOperations === "boolean") {
6977
- lines.push(`remote_operations: ${config.remoteOperations}`);
6978
- }
6979
- if (typeof config.bypassGitHooks === "boolean") {
6980
- lines.push(`bypass_git_hooks: ${config.bypassGitHooks}`);
6981
- }
6982
- if (typeof config.checkActiveBranches === "boolean") {
6983
- lines.push(`check_active_branches: ${config.checkActiveBranches}`);
6984
- }
6985
- if (typeof config.activeBranchDays === "number") {
6986
- lines.push(`active_branch_days: ${config.activeBranchDays}`);
6987
- }
6988
- return `${lines.join("\n")}
6989
- `;
6990
- }
6991
- class Core {
6992
- constructor(options) {
6993
- __publicField(this, "projectRoot");
6994
- __publicField(this, "fs");
6995
- __publicField(this, "tracer");
6996
- __publicField(this, "config", null);
6997
- __publicField(this, "tasks", /* @__PURE__ */ new Map());
6998
- __publicField(this, "initialized", false);
6999
- /** Lightweight task index for lazy loading (no file reads) */
7000
- __publicField(this, "taskIndex", /* @__PURE__ */ new Map());
7001
- __publicField(this, "lazyInitialized", false);
7002
- this.projectRoot = options.projectRoot;
7003
- this.fs = options.adapters.fs;
7004
- this.tracer = getTracer$1();
7005
- }
7006
- /**
7007
- * Check if projectRoot contains a valid Backlog.md project
7008
- */
7009
- async isBacklogProject() {
7010
- const configPath = this.fs.join(this.projectRoot, "backlog", "config.yml");
7011
- return this.fs.exists(configPath);
7012
- }
7013
- /**
7014
- * Initialize a new Backlog.md project in the projectRoot directory
7015
- *
7016
- * Creates the backlog/ directory and config.yml file.
7017
- * Task directories (tasks/, completed/) are created lazily when needed.
7018
- *
7019
- * @param options - Optional configuration for the new project
7020
- * @throws Error if project already exists
7021
- *
7022
- * @example
7023
- * ```typescript
7024
- * const core = new Core({ projectRoot: '/path/to/project', adapters: { fs } });
7025
- *
7026
- * // Initialize with defaults
7027
- * await core.initProject();
7028
- *
7029
- * // Or with custom options
7030
- * await core.initProject({
7031
- * projectName: 'My Project',
7032
- * statuses: ['Backlog', 'In Progress', 'Review', 'Done'],
7033
- * labels: ['bug', 'feature', 'docs']
7034
- * });
7035
- * ```
7036
- */
7037
- async initProject(options = {}) {
7038
- if (await this.isBacklogProject()) {
7039
- throw new Error(`Already a Backlog.md project: config.yml exists at ${this.fs.join(this.projectRoot, "backlog", "config.yml")}`);
7040
- }
7041
- const dirName = this.projectRoot.split("/").pop() || "Backlog";
7042
- const projectName = options.projectName || dirName;
7043
- const statuses = options.statuses || [
7044
- DEFAULT_TASK_STATUSES.TODO,
7045
- DEFAULT_TASK_STATUSES.IN_PROGRESS,
7046
- DEFAULT_TASK_STATUSES.DONE
7047
- ];
7048
- const config = {
7049
- projectName,
7050
- statuses,
7051
- labels: options.labels || [],
7052
- milestones: [],
7053
- defaultStatus: options.defaultStatus || statuses[0],
7054
- dateFormat: "YYYY-MM-DD"
7055
- };
7056
- const backlogDir = this.fs.join(this.projectRoot, "backlog");
7057
- await this.fs.createDir(backlogDir, { recursive: true });
7058
- const configPath = this.fs.join(backlogDir, "config.yml");
7059
- const configContent = serializeBacklogConfig(config);
7060
- await this.fs.writeFile(configPath, configContent);
7061
- }
7062
- /**
7063
- * Initialize the Core instance
7064
- *
7065
- * Loads configuration and discovers all tasks.
7066
- * Must be called before using other methods.
7067
- */
7068
- async initialize() {
7069
- if (this.initialized)
7070
- return;
7071
- return this.tracer.startActiveSpan("core.init", {
7072
- attributes: {
7073
- projectRoot: this.projectRoot,
7074
- mode: "full"
7075
- }
7076
- }, async (span) => {
7077
- var _a;
7078
- const startTime = Date.now();
7079
- try {
7080
- span.addEvent("core.init.started", {
7081
- projectRoot: this.projectRoot,
7082
- mode: "full"
7083
- });
7084
- const configPath = this.fs.join(this.projectRoot, "backlog", "config.yml");
7085
- const configExists = await this.fs.exists(configPath);
7086
- if (!configExists) {
7087
- const error = new Error(`Not a Backlog.md project: config.yml not found at ${configPath}`);
7088
- span.addEvent("core.init.error", {
7089
- "error.type": "ConfigNotFoundError",
7090
- "error.message": error.message,
7091
- stage: "config"
7092
- });
7093
- span.recordException(error);
7094
- span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
7095
- throw error;
7096
- }
7097
- const configContent = await this.fs.readFile(configPath);
7098
- this.config = parseBacklogConfig(configContent);
7099
- span.addEvent("core.init.config.loaded", {
7100
- projectName: this.config.projectName,
7101
- statusCount: this.config.statuses.length,
7102
- labelCount: ((_a = this.config.labels) == null ? void 0 : _a.length) ?? 0
7103
- });
7104
- let localTaskCount = 0;
7105
- const tasksDir = this.fs.join(this.projectRoot, "backlog", "tasks");
7106
- if (await this.fs.exists(tasksDir)) {
7107
- await this.loadTasksFromDirectory(tasksDir, "local");
7108
- localTaskCount = Array.from(this.tasks.values()).filter((t) => t.source === "local").length;
7109
- }
7110
- let completedTaskCount = 0;
7111
- const completedDir = this.fs.join(this.projectRoot, "backlog", "completed");
7112
- if (await this.fs.exists(completedDir)) {
7113
- await this.loadTasksFromDirectory(completedDir, "completed");
7114
- completedTaskCount = Array.from(this.tasks.values()).filter((t) => t.source === "completed").length;
7115
- }
7116
- const totalTaskCount = this.tasks.size;
7117
- span.addEvent("core.init.tasks.loaded", {
7118
- localTaskCount,
7119
- completedTaskCount,
7120
- totalTaskCount
7121
- });
7122
- this.initialized = true;
7123
- const duration = Date.now() - startTime;
7124
- span.addEvent("core.init.complete", {
7125
- success: true,
7126
- "duration.ms": duration,
7127
- taskCount: totalTaskCount
7128
- });
7129
- span.setStatus({ code: SpanStatusCode.OK });
7130
- } catch (error) {
7131
- if (error instanceof Error) {
7132
- span.recordException(error);
7133
- span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
7134
- }
7135
- throw error;
7136
- } finally {
7137
- span.end();
7138
- }
7139
- });
7140
- }
7141
- /**
7142
- * Initialize with lazy loading (no file content reads)
7143
- *
7144
- * Only loads config and builds task index from file paths.
7145
- * Task content is loaded on-demand via loadTask().
7146
- * Use this for web/panel contexts where file reads are expensive.
7147
- *
7148
- * @param filePaths - Array of all file paths in the project
7149
- */
7150
- async initializeLazy(filePaths) {
7151
- if (this.lazyInitialized)
7152
- return;
7153
- return this.tracer.startActiveSpan("core.init", {
7154
- attributes: {
7155
- projectRoot: this.projectRoot,
7156
- mode: "lazy",
7157
- "input.filePathCount": filePaths.length
7158
- }
7159
- }, async (span) => {
7160
- var _a;
7161
- const startTime = Date.now();
7162
- try {
7163
- span.addEvent("core.init.started", {
7164
- projectRoot: this.projectRoot,
7165
- mode: "lazy"
7166
- });
7167
- const configPath = this.fs.join(this.projectRoot, "backlog", "config.yml");
7168
- const configExists = await this.fs.exists(configPath);
7169
- if (!configExists) {
7170
- const error = new Error(`Not a Backlog.md project: config.yml not found at ${configPath}`);
7171
- span.addEvent("core.init.error", {
7172
- "error.type": "ConfigNotFoundError",
7173
- "error.message": error.message,
7174
- stage: "config"
7175
- });
7176
- span.recordException(error);
7177
- span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
7178
- throw error;
7179
- }
7180
- const configContent = await this.fs.readFile(configPath);
7181
- this.config = parseBacklogConfig(configContent);
7182
- span.addEvent("core.init.config.loaded", {
7183
- projectName: this.config.projectName,
7184
- statusCount: this.config.statuses.length,
7185
- labelCount: ((_a = this.config.labels) == null ? void 0 : _a.length) ?? 0
7186
- });
7187
- this.taskIndex.clear();
7188
- let tasksIndexed = 0;
7189
- let completedIndexed = 0;
7190
- for (const filePath of filePaths) {
7191
- if (!filePath.endsWith(".md"))
7192
- continue;
7193
- const isTaskFile = filePath.includes("backlog/tasks/") || filePath.includes("backlog\\tasks\\");
7194
- const isCompletedFile = filePath.includes("backlog/completed/") || filePath.includes("backlog\\completed\\");
7195
- if (!isTaskFile && !isCompletedFile)
7196
- continue;
7197
- if (filePath.endsWith("config.yml"))
7198
- continue;
7199
- const indexEntry = extractTaskIndexFromPath(filePath);
7200
- this.taskIndex.set(indexEntry.id, indexEntry);
7201
- if (isTaskFile)
7202
- tasksIndexed++;
7203
- if (isCompletedFile)
7204
- completedIndexed++;
7205
- }
7206
- this.lazyInitialized = true;
7207
- const duration = Date.now() - startTime;
7208
- span.addEvent("core.init.complete", {
7209
- success: true,
7210
- "duration.ms": duration,
7211
- tasksIndexed,
7212
- completedIndexed,
7213
- totalIndexed: this.taskIndex.size
7214
- });
7215
- span.setStatus({ code: SpanStatusCode.OK });
7216
- } catch (error) {
7217
- if (error instanceof Error) {
7218
- span.recordException(error);
7219
- span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
7220
- }
7221
- throw error;
7222
- } finally {
7223
- span.end();
7224
- }
7225
- });
7226
- }
7227
- /**
7228
- * Check if lazy initialization is complete
7229
- */
7230
- isLazyInitialized() {
7231
- return this.lazyInitialized;
7232
- }
7233
- /**
7234
- * Get the task index (lightweight entries)
7235
- */
7236
- getTaskIndex() {
7237
- if (!this.lazyInitialized) {
7238
- throw new Error("Core not lazy initialized. Call initializeLazy() first.");
7239
- }
7240
- return this.taskIndex;
7241
- }
7242
- /**
7243
- * Load a single task by ID (on-demand loading)
7244
- *
7245
- * @param id - Task ID
7246
- * @returns Task or undefined if not found
7247
- */
7248
- async loadTask(id) {
7249
- if (this.tasks.has(id)) {
7250
- return this.tasks.get(id);
7251
- }
7252
- const indexEntry = this.taskIndex.get(id);
7253
- if (!indexEntry) {
7254
- return void 0;
7255
- }
7256
- try {
7257
- const content2 = await this.fs.readFile(indexEntry.filePath);
7258
- const task = parseTaskMarkdown(content2, indexEntry.filePath);
7259
- task.source = indexEntry.source === "completed" ? "completed" : "local";
7260
- this.tasks.set(task.id, task);
7261
- return task;
7262
- } catch (error) {
7263
- console.warn(`Failed to load task ${id} from ${indexEntry.filePath}:`, error);
7264
- return void 0;
7265
- }
7266
- }
7267
- /**
7268
- * Load multiple tasks by ID (on-demand loading)
7269
- *
7270
- * @param ids - Array of task IDs to load
7271
- * @returns Array of loaded tasks (undefined entries filtered out)
7272
- */
7273
- async loadTasks(ids2) {
7274
- const tasks = await Promise.all(ids2.map((id) => this.loadTask(id)));
7275
- return tasks.filter((t) => t !== void 0);
7276
- }
7277
- /**
7278
- * Get tasks by source (tasks/completed) with pagination
7279
- *
7280
- * This is a lazy-loading alternative to getTasksByStatusPaginated().
7281
- * Groups by directory (tasks/ or completed/) instead of status.
7282
- * Only loads task content for items in the requested page.
7283
- *
7284
- * @param options - Per-source pagination options
7285
- * @returns Paginated tasks grouped by source
7286
- */
7287
- async getTasksBySourcePaginated(options) {
7288
- if (!this.lazyInitialized) {
7289
- throw new Error("Core not lazy initialized. Call initializeLazy() first.");
7290
- }
7291
- const tasksLimit = (options == null ? void 0 : options.tasksLimit) ?? 10;
7292
- const completedLimit = (options == null ? void 0 : options.completedLimit) ?? 10;
7293
- const offset = (options == null ? void 0 : options.offset) ?? 0;
7294
- const tasksSortDirection = (options == null ? void 0 : options.tasksSortDirection) ?? "asc";
7295
- const completedSortByIdDesc = (options == null ? void 0 : options.completedSortByIdDesc) ?? true;
7296
- const sources = ["tasks", "completed"];
7297
- const bySource = /* @__PURE__ */ new Map();
7298
- for (const source2 of sources) {
7299
- let entries = Array.from(this.taskIndex.values()).filter((e) => e.source === source2);
7300
- const limit = source2 === "tasks" ? tasksLimit : completedLimit;
7301
- entries = entries.sort((a, b) => {
7302
- const aNum = parseInt(a.id.replace(/\D/g, ""), 10) || 0;
7303
- const bNum = parseInt(b.id.replace(/\D/g, ""), 10) || 0;
7304
- if (source2 === "completed") {
7305
- return completedSortByIdDesc ? bNum - aNum : aNum - bNum;
7306
- } else {
7307
- return tasksSortDirection === "desc" ? bNum - aNum : aNum - bNum;
7308
- }
7309
- });
7310
- const total = entries.length;
7311
- const pageEntries = entries.slice(offset, offset + limit);
7312
- const items = await this.loadTasks(pageEntries.map((e) => e.id));
7313
- bySource.set(source2, {
7314
- items,
7315
- total,
7316
- hasMore: offset + limit < total,
7317
- offset,
7318
- limit
7319
- });
7320
- }
7321
- return {
7322
- bySource,
7323
- sources
7324
- };
7325
- }
7326
- /**
7327
- * Load more tasks for a specific source (lazy loading)
7328
- *
7329
- * @param source - Source to load more from ("tasks" or "completed")
7330
- * @param currentOffset - Current offset (items already loaded)
7331
- * @param options - Pagination options
7332
- * @returns Paginated result for the source
7333
- */
7334
- async loadMoreForSource(source2, currentOffset, options) {
7335
- if (!this.lazyInitialized) {
7336
- throw new Error("Core not lazy initialized. Call initializeLazy() first.");
7337
- }
7338
- const limit = (options == null ? void 0 : options.limit) ?? 10;
7339
- const sortDirection = (options == null ? void 0 : options.sortDirection) ?? "asc";
7340
- const completedSortByIdDesc = (options == null ? void 0 : options.completedSortByIdDesc) ?? true;
7341
- let entries = Array.from(this.taskIndex.values()).filter((e) => e.source === source2);
7342
- entries = entries.sort((a, b) => {
7343
- const aNum = parseInt(a.id.replace(/\D/g, ""), 10) || 0;
7344
- const bNum = parseInt(b.id.replace(/\D/g, ""), 10) || 0;
7345
- if (source2 === "completed") {
7346
- return completedSortByIdDesc ? bNum - aNum : aNum - bNum;
7347
- } else {
7348
- return sortDirection === "desc" ? bNum - aNum : aNum - bNum;
7349
- }
7350
- });
7351
- const total = entries.length;
7352
- const pageEntries = entries.slice(currentOffset, currentOffset + limit);
7353
- const items = await this.loadTasks(pageEntries.map((e) => e.id));
7354
- return {
7355
- items,
7356
- total,
7357
- hasMore: currentOffset + limit < total,
7358
- offset: currentOffset,
7359
- limit
7360
- };
7361
- }
7362
- /**
7363
- * Get the loaded configuration
7364
- *
7365
- * @throws Error if not initialized
7366
- */
7367
- getConfig() {
7368
- this.ensureInitialized();
7369
- return this.safeConfig;
7370
- }
7371
- /**
7372
- * List all tasks, optionally filtered
7373
- *
7374
- * @param filter - Optional filter criteria
7375
- * @returns Sorted array of tasks
7376
- */
7377
- listTasks(filter) {
7378
- this.ensureInitialized();
7379
- let tasks = Array.from(this.tasks.values());
7380
- if (filter == null ? void 0 : filter.status) {
7381
- tasks = tasks.filter((t) => t.status === filter.status);
7382
- }
7383
- if (filter == null ? void 0 : filter.assignee) {
7384
- const assignee = filter.assignee;
7385
- tasks = tasks.filter((t) => t.assignee.includes(assignee));
7386
- }
7387
- if (filter == null ? void 0 : filter.priority) {
7388
- tasks = tasks.filter((t) => t.priority === filter.priority);
7389
- }
7390
- if (filter == null ? void 0 : filter.milestone) {
7391
- const filterKey = milestoneKey(filter.milestone);
7392
- tasks = tasks.filter((t) => milestoneKey(t.milestone) === filterKey);
7393
- }
7394
- if ((filter == null ? void 0 : filter.labels) && filter.labels.length > 0) {
7395
- tasks = tasks.filter((t) => {
7396
- var _a;
7397
- return (_a = filter.labels) == null ? void 0 : _a.some((label) => t.labels.includes(label));
7398
- });
7399
- }
7400
- if (filter == null ? void 0 : filter.parentTaskId) {
7401
- tasks = tasks.filter((t) => t.parentTaskId === filter.parentTaskId);
7402
- }
7403
- return sortTasks(tasks);
7404
- }
7405
- /**
7406
- * Get tasks grouped by status
7407
- *
7408
- * This is the primary method for kanban-style displays.
7409
- * Returns a Map with status as key and sorted tasks as value.
7410
- * The Map preserves the order of statuses from config.
7411
- */
7412
- getTasksByStatus() {
7413
- this.ensureInitialized();
7414
- const tasks = Array.from(this.tasks.values());
7415
- return groupTasksByStatus(tasks, this.safeConfig.statuses);
7416
- }
7417
- /**
7418
- * Get tasks grouped by milestone
7419
- *
7420
- * Returns a MilestoneSummary with:
7421
- * - milestones: List of milestone IDs in display order
7422
- * - buckets: Array of MilestoneBucket with tasks, progress, status counts
7423
- *
7424
- * The first bucket is always "Tasks without milestone".
7425
- * Each bucket includes progress percentage based on done status.
7426
- *
7427
- * @example
7428
- * ```typescript
7429
- * const summary = core.getTasksByMilestone();
7430
- * for (const bucket of summary.buckets) {
7431
- * console.log(`${bucket.label}: ${bucket.progress}% complete`);
7432
- * console.log(` ${bucket.doneCount}/${bucket.total} tasks done`);
7433
- * }
7434
- * ```
7435
- */
7436
- getTasksByMilestone() {
7437
- this.ensureInitialized();
7438
- const tasks = Array.from(this.tasks.values());
7439
- return groupTasksByMilestone(tasks, this.safeConfig.milestones, this.safeConfig.statuses);
7440
- }
7441
- // =========================================================================
7442
- // Milestone CRUD Operations
7443
- // =========================================================================
7444
- /**
7445
- * Get the milestones directory path
7446
- */
7447
- getMilestonesDir() {
7448
- return this.fs.join(this.projectRoot, "backlog", "milestones");
7449
- }
7450
- /**
7451
- * List all milestones from the milestones directory
7452
- *
7453
- * @returns Array of Milestone objects sorted by ID
7454
- */
7455
- async listMilestones() {
7456
- const milestonesDir = this.getMilestonesDir();
7457
- if (!await this.fs.exists(milestonesDir)) {
7458
- return [];
7459
- }
7460
- const entries = await this.fs.readDir(milestonesDir);
7461
- const milestones = [];
7462
- for (const entry of entries) {
7463
- if (!entry.endsWith(".md"))
7464
- continue;
7465
- if (entry.toLowerCase() === "readme.md")
7466
- continue;
7467
- const milestoneId = extractMilestoneIdFromFilename(entry);
7468
- if (!milestoneId)
7469
- continue;
7470
- const filepath = this.fs.join(milestonesDir, entry);
7471
- if (await this.fs.isDirectory(filepath))
7472
- continue;
7473
- try {
7474
- const content2 = await this.fs.readFile(filepath);
7475
- milestones.push(parseMilestoneMarkdown(content2));
7476
- } catch (error) {
7477
- console.warn(`Failed to parse milestone file ${filepath}:`, error);
7478
- }
7479
- }
7480
- return milestones.sort((a, b) => a.id.localeCompare(b.id, void 0, { numeric: true }));
7481
- }
7482
- /**
7483
- * Load a single milestone by ID
7484
- *
7485
- * @param id - Milestone ID (e.g., "m-0")
7486
- * @returns Milestone or null if not found
7487
- */
7488
- async loadMilestone(id) {
7489
- const milestonesDir = this.getMilestonesDir();
7490
- if (!await this.fs.exists(milestonesDir)) {
7491
- return null;
7492
- }
7493
- const entries = await this.fs.readDir(milestonesDir);
7494
- const milestoneFile = entries.find((entry) => {
7495
- const fileId = extractMilestoneIdFromFilename(entry);
7496
- return fileId === id;
7497
- });
7498
- if (!milestoneFile) {
7499
- return null;
7500
- }
7501
- const filepath = this.fs.join(milestonesDir, milestoneFile);
7502
- try {
7503
- const content2 = await this.fs.readFile(filepath);
7504
- return parseMilestoneMarkdown(content2);
7505
- } catch {
7506
- return null;
7507
- }
7508
- }
7509
- /**
7510
- * Create a new milestone
7511
- *
7512
- * @param input - Milestone creation input
7513
- * @returns Created milestone
7514
- */
7515
- async createMilestone(input) {
7516
- const startTime = Date.now();
7517
- const span = this.tracer.startSpan("milestone.create", {
7518
- attributes: {
7519
- "input.title": input.title,
7520
- "input.hasDescription": input.description !== void 0
7521
- }
7522
- });
7523
- return await context.with(trace.setSpan(context.active(), span), async () => {
7524
- try {
7525
- span.addEvent("milestone.create.started", {
7526
- "input.title": input.title,
7527
- "input.hasDescription": input.description !== void 0
7528
- });
7529
- const milestonesDir = this.getMilestonesDir();
7530
- await this.fs.createDir(milestonesDir, { recursive: true });
7531
- const entries = await this.fs.readDir(milestonesDir).catch(() => []);
7532
- const existingIds = entries.map((f) => {
7533
- const match = f.match(/^m-(\d+)/);
7534
- return (match == null ? void 0 : match[1]) ? parseInt(match[1], 10) : -1;
7535
- }).filter((id2) => id2 >= 0);
7536
- const nextId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 0;
7537
- const id = `m-${nextId}`;
7538
- const description = input.description || `Milestone: ${input.title}`;
7539
- const tempMilestone = {
7540
- id,
7541
- title: input.title,
7542
- description,
7543
- rawContent: "",
7544
- tasks: []
7545
- };
7546
- const content2 = serializeMilestoneMarkdown(tempMilestone);
7547
- const milestone = {
7548
- id,
7549
- title: input.title,
7550
- description,
7551
- rawContent: content2,
7552
- tasks: []
7553
- };
7554
- const filename = getMilestoneFilename(id, input.title);
7555
- const filepath = this.fs.join(milestonesDir, filename);
7556
- await this.fs.writeFile(filepath, content2);
7557
- span.addEvent("milestone.create.complete", {
7558
- "output.milestoneId": id,
7559
- "duration.ms": Date.now() - startTime
7560
- });
7561
- span.setStatus({ code: SpanStatusCode.OK });
7562
- span.end();
7563
- return milestone;
7564
- } catch (error) {
7565
- span.addEvent("milestone.create.error", {
7566
- "error.type": error instanceof Error ? error.name : "UnknownError",
7567
- "error.message": error instanceof Error ? error.message : String(error)
7568
- });
7569
- span.setStatus({
7570
- code: SpanStatusCode.ERROR,
7571
- message: error instanceof Error ? error.message : String(error)
7572
- });
7573
- span.end();
7574
- throw error;
7575
- }
7576
- });
7577
- }
7578
- /**
7579
- * Update an existing milestone
7580
- *
7581
- * @param id - Milestone ID to update
7582
- * @param input - Fields to update
7583
- * @returns Updated milestone or null if not found
7584
- */
7585
- async updateMilestone(id, input) {
7586
- const startTime = Date.now();
7587
- const span = this.tracer.startSpan("milestone.update", {
7588
- attributes: {
7589
- "input.milestoneId": id,
7590
- "input.hasTitle": input.title !== void 0,
7591
- "input.hasDescription": input.description !== void 0
7592
- }
7593
- });
7594
- return await context.with(trace.setSpan(context.active(), span), async () => {
7595
- try {
7596
- span.addEvent("milestone.update.started", {
7597
- "input.milestoneId": id,
7598
- "input.hasTitle": input.title !== void 0,
7599
- "input.hasDescription": input.description !== void 0
7600
- });
7601
- const existing = await this.loadMilestone(id);
7602
- if (!existing) {
7603
- span.addEvent("milestone.update.error", {
7604
- "error.type": "NotFoundError",
7605
- "error.message": "Milestone not found",
7606
- "input.milestoneId": id
7607
- });
7608
- span.setStatus({ code: SpanStatusCode.OK });
7609
- span.end();
7610
- return null;
7611
- }
7612
- const milestonesDir = this.getMilestonesDir();
7613
- const entries = await this.fs.readDir(milestonesDir);
7614
- const currentFile = entries.find((entry) => {
7615
- const fileId = extractMilestoneIdFromFilename(entry);
7616
- return fileId === id;
7617
- });
7618
- if (!currentFile) {
7619
- span.addEvent("milestone.update.error", {
7620
- "error.type": "NotFoundError",
7621
- "error.message": "Milestone file not found",
7622
- "input.milestoneId": id
7623
- });
7624
- span.setStatus({ code: SpanStatusCode.OK });
7625
- span.end();
7626
- return null;
7627
- }
7628
- const newTitle = input.title ?? existing.title;
7629
- const newDescription = input.description ?? existing.description;
7630
- const titleChanged = input.title !== void 0 && input.title !== existing.title;
7631
- const tempMilestone = {
7632
- id: existing.id,
7633
- title: newTitle,
7634
- description: newDescription,
7635
- rawContent: "",
7636
- tasks: existing.tasks
7637
- };
7638
- const content2 = serializeMilestoneMarkdown(tempMilestone);
7639
- const updated = {
7640
- id: existing.id,
7641
- title: newTitle,
7642
- description: newDescription,
7643
- rawContent: content2,
7644
- tasks: existing.tasks
7645
- };
7646
- const oldPath = this.fs.join(milestonesDir, currentFile);
7647
- await this.fs.deleteFile(oldPath);
7648
- const newFilename = getMilestoneFilename(id, updated.title);
7649
- const newPath = this.fs.join(milestonesDir, newFilename);
7650
- await this.fs.writeFile(newPath, content2);
7651
- span.addEvent("milestone.update.complete", {
7652
- "output.milestoneId": id,
7653
- "output.titleChanged": titleChanged,
7654
- "duration.ms": Date.now() - startTime
7655
- });
7656
- span.setStatus({ code: SpanStatusCode.OK });
7657
- span.end();
7658
- return updated;
7659
- } catch (error) {
7660
- span.addEvent("milestone.update.error", {
7661
- "error.type": error instanceof Error ? error.name : "UnknownError",
7662
- "error.message": error instanceof Error ? error.message : String(error),
7663
- "input.milestoneId": id
7664
- });
7665
- span.setStatus({
7666
- code: SpanStatusCode.ERROR,
7667
- message: error instanceof Error ? error.message : String(error)
7668
- });
7669
- span.end();
7670
- throw error;
7671
- }
7672
- });
7673
- }
7674
- /**
7675
- * Delete a milestone
7676
- *
7677
- * @param id - Milestone ID to delete
7678
- * @returns true if deleted, false if not found
7679
- */
7680
- async deleteMilestone(id) {
7681
- const startTime = Date.now();
7682
- const span = this.tracer.startSpan("milestone.delete", {
7683
- attributes: {
7684
- "input.milestoneId": id
7685
- }
7686
- });
7687
- return await context.with(trace.setSpan(context.active(), span), async () => {
7688
- try {
7689
- span.addEvent("milestone.delete.started", {
7690
- "input.milestoneId": id
7691
- });
7692
- const milestonesDir = this.getMilestonesDir();
7693
- if (!await this.fs.exists(milestonesDir)) {
7694
- span.addEvent("milestone.delete.error", {
7695
- "error.type": "NotFoundError",
7696
- "error.message": "Milestones directory not found",
7697
- "input.milestoneId": id
7698
- });
7699
- span.setStatus({ code: SpanStatusCode.OK });
7700
- span.end();
7701
- return false;
7702
- }
7703
- const entries = await this.fs.readDir(milestonesDir);
7704
- const milestoneFile = entries.find((entry) => {
7705
- const fileId = extractMilestoneIdFromFilename(entry);
7706
- return fileId === id;
7707
- });
7708
- if (!milestoneFile) {
7709
- span.addEvent("milestone.delete.error", {
7710
- "error.type": "NotFoundError",
7711
- "error.message": "Milestone not found",
7712
- "input.milestoneId": id
7713
- });
7714
- span.setStatus({ code: SpanStatusCode.OK });
7715
- span.end();
7716
- return false;
7717
- }
7718
- const filepath = this.fs.join(milestonesDir, milestoneFile);
7719
- await this.fs.deleteFile(filepath);
7720
- span.addEvent("milestone.delete.complete", {
7721
- "output.milestoneId": id,
7722
- "output.deleted": true,
7723
- "duration.ms": Date.now() - startTime
7724
- });
7725
- span.setStatus({ code: SpanStatusCode.OK });
7726
- span.end();
7727
- return true;
7728
- } catch (error) {
7729
- span.addEvent("milestone.delete.error", {
7730
- "error.type": error instanceof Error ? error.name : "UnknownError",
7731
- "error.message": error instanceof Error ? error.message : String(error),
7732
- "input.milestoneId": id
7733
- });
7734
- span.setStatus({
7735
- code: SpanStatusCode.ERROR,
7736
- message: error instanceof Error ? error.message : String(error)
7737
- });
7738
- span.end();
7739
- throw error;
7740
- }
7741
- });
7742
- }
7743
- /**
7744
- * Get a single task by ID
7745
- *
7746
- * @param id - Task ID
7747
- * @returns Task or undefined if not found
7748
- */
7749
- getTask(id) {
7750
- this.ensureInitialized();
7751
- return this.tasks.get(id);
7752
- }
7753
- /**
7754
- * List tasks with pagination
7755
- *
7756
- * @param filter - Filter and pagination options
7757
- * @returns Paginated result with tasks
7758
- */
7759
- listTasksPaginated(filter) {
7760
- this.ensureInitialized();
7761
- let tasks = this.applyFilters(Array.from(this.tasks.values()), filter);
7762
- const pagination = (filter == null ? void 0 : filter.pagination) ?? {};
7763
- const sortBy = pagination.sortBy ?? "title";
7764
- const sortDirection = pagination.sortDirection ?? "asc";
7765
- tasks = sortTasksBy(tasks, sortBy, sortDirection);
7766
- const limit = pagination.limit ?? 10;
7767
- const offset = pagination.offset ?? 0;
7768
- const total = tasks.length;
7769
- const items = tasks.slice(offset, offset + limit);
7770
- return {
7771
- items,
7772
- total,
7773
- hasMore: offset + limit < total,
7774
- offset,
7775
- limit
7776
- };
7777
- }
7778
- /**
7779
- * Get tasks by status with pagination per column
7780
- *
7781
- * @param pagination - Pagination options (applied per status)
7782
- * @returns Paginated tasks grouped by status
7783
- */
7784
- getTasksByStatusPaginated(pagination) {
7785
- this.ensureInitialized();
7786
- const limit = (pagination == null ? void 0 : pagination.limit) ?? 10;
7787
- const offset = (pagination == null ? void 0 : pagination.offset) ?? 0;
7788
- const sortBy = (pagination == null ? void 0 : pagination.sortBy) ?? "title";
7789
- const sortDirection = (pagination == null ? void 0 : pagination.sortDirection) ?? "asc";
7790
- const byStatus = /* @__PURE__ */ new Map();
7791
- const allGrouped = /* @__PURE__ */ new Map();
7792
- for (const status of this.safeConfig.statuses) {
7793
- allGrouped.set(status, []);
7794
- }
7795
- for (const task of this.tasks.values()) {
7796
- const list2 = allGrouped.get(task.status);
7797
- if (list2) {
7798
- list2.push(task);
7799
- } else {
7800
- allGrouped.set(task.status, [task]);
7801
- }
7802
- }
7803
- for (const status of this.safeConfig.statuses) {
7804
- let tasks = allGrouped.get(status) ?? [];
7805
- tasks = sortTasksBy(tasks, sortBy, sortDirection);
7806
- const total = tasks.length;
7807
- const items = tasks.slice(offset, offset + limit);
7808
- byStatus.set(status, {
7809
- items,
7810
- total,
7811
- hasMore: offset + limit < total,
7812
- offset,
7813
- limit
7814
- });
7815
- }
7816
- return {
7817
- byStatus,
7818
- statuses: this.safeConfig.statuses
7819
- };
7820
- }
7821
- /**
7822
- * Load more tasks for a specific status
7823
- *
7824
- * @param status - Status column to load more from
7825
- * @param currentOffset - Current offset (items already loaded)
7826
- * @param pagination - Pagination options (limit, sortBy, sortDirection)
7827
- * @returns Paginated result for the status
7828
- */
7829
- loadMoreForStatus(status, currentOffset, pagination) {
7830
- this.ensureInitialized();
7831
- const limit = (pagination == null ? void 0 : pagination.limit) ?? 10;
7832
- const sortBy = (pagination == null ? void 0 : pagination.sortBy) ?? "title";
7833
- const sortDirection = (pagination == null ? void 0 : pagination.sortDirection) ?? "asc";
7834
- let tasks = Array.from(this.tasks.values()).filter((t) => t.status === status);
7835
- tasks = sortTasksBy(tasks, sortBy, sortDirection);
7836
- const total = tasks.length;
7837
- const items = tasks.slice(currentOffset, currentOffset + limit);
7838
- return {
7839
- items,
7840
- total,
7841
- hasMore: currentOffset + limit < total,
7842
- offset: currentOffset,
7843
- limit
7844
- };
7845
- }
7846
- /**
7847
- * Reload all tasks from disk
7848
- *
7849
- * Useful after external changes to task files.
7850
- */
7851
- async reload() {
7852
- this.tasks.clear();
7853
- this.initialized = false;
7854
- await this.initialize();
7855
- }
7856
- // --- Private methods ---
7857
- ensureInitialized() {
7858
- if (!this.initialized && !this.lazyInitialized || !this.config) {
7859
- throw new Error("Core not initialized. Call initialize() or initializeLazy() first.");
7860
- }
7861
- }
7862
- /**
7863
- * Get config with type safety (use after ensureInitialized)
7864
- */
7865
- get safeConfig() {
7866
- return this.config;
7867
- }
7868
- applyFilters(tasks, filter) {
7869
- if (!filter)
7870
- return tasks;
7871
- let result = tasks;
7872
- if (filter.status) {
7873
- result = result.filter((t) => t.status === filter.status);
7874
- }
7875
- if (filter.assignee) {
7876
- const assignee = filter.assignee;
7877
- result = result.filter((t) => t.assignee.includes(assignee));
7878
- }
7879
- if (filter.priority) {
7880
- result = result.filter((t) => t.priority === filter.priority);
7881
- }
7882
- if (filter.milestone) {
7883
- const filterKey = milestoneKey(filter.milestone);
7884
- result = result.filter((t) => milestoneKey(t.milestone) === filterKey);
7885
- }
7886
- if (filter.labels && filter.labels.length > 0) {
7887
- result = result.filter((t) => {
7888
- var _a;
7889
- return (_a = filter.labels) == null ? void 0 : _a.some((label) => t.labels.includes(label));
7890
- });
7891
- }
7892
- if (filter.parentTaskId) {
7893
- result = result.filter((t) => t.parentTaskId === filter.parentTaskId);
7894
- }
7895
- return result;
7896
- }
7897
- async loadTasksFromDirectory(dir, source2) {
7898
- const entries = await this.fs.readDir(dir);
7899
- for (const entry of entries) {
7900
- const fullPath = this.fs.join(dir, entry);
7901
- if (await this.fs.isDirectory(fullPath)) {
7902
- continue;
7903
- }
7904
- if (!entry.endsWith(".md")) {
7905
- continue;
7906
- }
7907
- try {
7908
- const content2 = await this.fs.readFile(fullPath);
7909
- const task = parseTaskMarkdown(content2, fullPath);
7910
- task.source = source2;
7911
- this.tasks.set(task.id, task);
7912
- } catch (error) {
7913
- console.warn(`Failed to parse task file ${fullPath}:`, error);
7914
- }
7915
- }
7916
- }
7917
- // =========================================================================
7918
- // Milestone-Task Sync Helpers
7919
- // =========================================================================
7920
- /**
7921
- * Add a task ID to a milestone's tasks array
7922
- *
7923
- * @param taskId - Task ID to add
7924
- * @param milestoneId - Milestone ID to update
7925
- */
7926
- async addTaskToMilestone(taskId, milestoneId) {
7927
- const milestone = await this.loadMilestone(milestoneId);
7928
- if (!milestone) {
7929
- console.warn(`Milestone ${milestoneId} not found when adding task ${taskId}`);
7930
- return;
7931
- }
7932
- if (milestone.tasks.includes(taskId)) {
7933
- return;
7934
- }
7935
- const updatedMilestone = {
7936
- ...milestone,
7937
- tasks: [...milestone.tasks, taskId]
7938
- };
7939
- await this.writeMilestoneFile(updatedMilestone);
7940
- }
7941
- /**
7942
- * Remove a task ID from a milestone's tasks array
7943
- *
7944
- * @param taskId - Task ID to remove
7945
- * @param milestoneId - Milestone ID to update
7946
- */
7947
- async removeTaskFromMilestone(taskId, milestoneId) {
7948
- const milestone = await this.loadMilestone(milestoneId);
7949
- if (!milestone) {
7950
- return;
7951
- }
7952
- if (!milestone.tasks.includes(taskId)) {
7953
- return;
7954
- }
7955
- const updatedMilestone = {
7956
- ...milestone,
7957
- tasks: milestone.tasks.filter((id) => id !== taskId)
7958
- };
7959
- await this.writeMilestoneFile(updatedMilestone);
7960
- }
7961
- /**
7962
- * Write a milestone to disk
7963
- */
7964
- async writeMilestoneFile(milestone) {
7965
- const milestonesDir = this.getMilestonesDir();
7966
- const entries = await this.fs.readDir(milestonesDir).catch(() => []);
7967
- const currentFile = entries.find((entry) => {
7968
- const fileId = extractMilestoneIdFromFilename(entry);
7969
- return fileId === milestone.id;
7970
- });
7971
- if (currentFile) {
7972
- const oldPath = this.fs.join(milestonesDir, currentFile);
7973
- await this.fs.deleteFile(oldPath).catch(() => {
7974
- });
7975
- }
7976
- const content2 = serializeMilestoneMarkdown(milestone);
7977
- const filename = getMilestoneFilename(milestone.id, milestone.title);
7978
- const filepath = this.fs.join(milestonesDir, filename);
7979
- await this.fs.writeFile(filepath, content2);
7980
- }
7981
- /**
7982
- * Get the tasks directory path
7983
- */
7984
- getTasksDir() {
7985
- return this.fs.join(this.projectRoot, "backlog", "tasks");
7986
- }
7987
- /**
7988
- * Get the completed directory path
7989
- */
7990
- getCompletedDir() {
7991
- return this.fs.join(this.projectRoot, "backlog", "completed");
7992
- }
7993
- // =========================================================================
7994
- // Task CRUD Operations
7995
- // =========================================================================
7996
- /**
7997
- * Create a new task
7998
- *
7999
- * @param input - Task creation input
8000
- * @returns Created task
8001
- */
8002
- async createTask(input) {
8003
- this.ensureInitialized();
8004
- const startTime = Date.now();
8005
- const span = this.tracer.startSpan("task.create", {
8006
- attributes: {
8007
- "input.title": input.title,
8008
- "input.status": input.status,
8009
- "input.milestoneId": input.milestone
8010
- }
8011
- });
8012
- return await context.with(trace.setSpan(context.active(), span), async () => {
8013
- var _a, _b, _c, _d, _e2;
8014
- try {
8015
- span.addEvent("task.create.started", {
8016
- "input.title": input.title,
8017
- "input.status": input.status,
8018
- "input.milestoneId": input.milestone
8019
- });
8020
- const tasksDir = this.getTasksDir();
8021
- await this.fs.createDir(tasksDir, { recursive: true });
8022
- const existingIds = Array.from(this.lazyInitialized ? this.taskIndex.keys() : this.tasks.keys()).map((id) => parseInt(id.replace(/\D/g, ""), 10)).filter((n) => !Number.isNaN(n));
8023
- const nextId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 1;
8024
- const taskId = String(nextId);
8025
- const configStatuses = ((_a = this.config) == null ? void 0 : _a.statuses) || [
8026
- DEFAULT_TASK_STATUSES.TODO,
8027
- DEFAULT_TASK_STATUSES.IN_PROGRESS,
8028
- DEFAULT_TASK_STATUSES.DONE
8029
- ];
8030
- let status = input.status || ((_b = this.config) == null ? void 0 : _b.defaultStatus) || DEFAULT_TASK_STATUSES.TODO;
8031
- if (!configStatuses.includes(status)) {
8032
- console.warn(`Warning: Status "${status}" is not in configured statuses [${configStatuses.join(", ")}]. Using default status "${((_c = this.config) == null ? void 0 : _c.defaultStatus) || DEFAULT_TASK_STATUSES.TODO}" instead.`);
8033
- status = ((_d = this.config) == null ? void 0 : _d.defaultStatus) || DEFAULT_TASK_STATUSES.TODO;
8034
- }
8035
- const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
8036
- const task = {
8037
- id: taskId,
8038
- title: input.title,
8039
- status,
8040
- priority: input.priority,
8041
- assignee: input.assignee || [],
8042
- createdDate: now,
8043
- labels: input.labels || [],
8044
- milestone: input.milestone,
8045
- dependencies: input.dependencies || [],
8046
- references: input.references || [],
8047
- parentTaskId: input.parentTaskId,
8048
- description: input.description,
8049
- implementationPlan: input.implementationPlan,
8050
- implementationNotes: input.implementationNotes,
8051
- acceptanceCriteriaItems: (_e2 = input.acceptanceCriteria) == null ? void 0 : _e2.map((ac, i) => ({
8052
- index: i + 1,
8053
- text: ac.text,
8054
- checked: ac.checked || false
8055
- })),
8056
- rawContent: input.rawContent,
8057
- source: "local"
8058
- };
8059
- const content2 = serializeTaskMarkdown(task);
8060
- const safeTitle = input.title.replace(/[<>:"/\\|?*]/g, "").replace(/\s+/g, " ").slice(0, 50);
8061
- const filename = `${taskId} - ${safeTitle}.md`;
8062
- const filepath = this.fs.join(tasksDir, filename);
8063
- await this.fs.writeFile(filepath, content2);
8064
- task.filePath = filepath;
8065
- this.tasks.set(taskId, task);
8066
- if (this.lazyInitialized) {
8067
- const relativePath = filepath.replace(`${this.projectRoot}/`, "");
8068
- this.taskIndex.set(taskId, {
8069
- id: taskId,
8070
- filePath: relativePath,
8071
- title: task.title,
8072
- source: "tasks"
8073
- });
8074
- }
8075
- if (input.milestone) {
8076
- await this.addTaskToMilestone(taskId, input.milestone);
8077
- }
8078
- span.addEvent("task.create.complete", {
8079
- "output.taskId": taskId,
8080
- "output.taskIndex": nextId,
8081
- "duration.ms": Date.now() - startTime
8082
- });
8083
- span.setStatus({ code: SpanStatusCode.OK });
8084
- span.end();
8085
- return task;
8086
- } catch (error) {
8087
- span.addEvent("task.create.error", {
8088
- "error.type": error instanceof Error ? error.name : "UnknownError",
8089
- "error.message": error instanceof Error ? error.message : String(error)
8090
- });
8091
- span.setStatus({
8092
- code: SpanStatusCode.ERROR,
8093
- message: error instanceof Error ? error.message : String(error)
8094
- });
8095
- span.end();
8096
- throw error;
8097
- }
8098
- });
8099
- }
8100
- /**
8101
- * Update an existing task
8102
- *
8103
- * @param id - Task ID to update
8104
- * @param input - Fields to update
8105
- * @returns Updated task or null if not found
8106
- */
8107
- async updateTask(id, input) {
8108
- this.ensureInitialized();
8109
- const startTime = Date.now();
8110
- const span = this.tracer.startSpan("task.update", {
8111
- attributes: {
8112
- "input.taskId": id,
8113
- "input.hasTitle": input.title !== void 0,
8114
- "input.hasStatus": input.status !== void 0,
8115
- "input.hasMilestone": input.milestone !== void 0
8116
- }
8117
- });
8118
- return await context.with(trace.setSpan(context.active(), span), async () => {
8119
- try {
8120
- span.addEvent("task.update.started", {
8121
- "input.taskId": id,
8122
- "input.hasTitle": input.title !== void 0,
8123
- "input.hasStatus": input.status !== void 0,
8124
- "input.hasMilestone": input.milestone !== void 0
8125
- });
8126
- const existing = this.tasks.get(id);
8127
- if (!existing) {
8128
- span.addEvent("task.update.error", {
8129
- "error.type": "NotFoundError",
8130
- "error.message": "Task not found",
8131
- "input.taskId": id
8132
- });
8133
- span.setStatus({ code: SpanStatusCode.OK });
8134
- span.end();
8135
- return null;
8136
- }
8137
- const oldMilestone = existing.milestone;
8138
- const newMilestone = input.milestone === null ? void 0 : input.milestone !== void 0 ? input.milestone : oldMilestone;
8139
- const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
8140
- const updated = {
8141
- ...existing,
8142
- title: input.title ?? existing.title,
8143
- status: input.status ?? existing.status,
8144
- priority: input.priority ?? existing.priority,
8145
- milestone: newMilestone,
8146
- updatedDate: now,
8147
- description: input.description ?? existing.description,
8148
- implementationPlan: input.clearImplementationPlan ? void 0 : input.implementationPlan ?? existing.implementationPlan,
8149
- implementationNotes: input.clearImplementationNotes ? void 0 : input.implementationNotes ?? existing.implementationNotes,
8150
- ordinal: input.ordinal ?? existing.ordinal,
8151
- dependencies: input.dependencies ?? existing.dependencies,
8152
- references: input.references ?? existing.references ?? []
8153
- };
8154
- if (input.labels) {
8155
- updated.labels = input.labels;
8156
- } else {
8157
- if (input.addLabels) {
8158
- updated.labels = [.../* @__PURE__ */ new Set([...updated.labels, ...input.addLabels])];
8159
- }
8160
- if (input.removeLabels) {
8161
- updated.labels = updated.labels.filter((l) => {
8162
- var _a;
8163
- return !((_a = input.removeLabels) == null ? void 0 : _a.includes(l));
8164
- });
8165
- }
8166
- }
8167
- if (input.assignee) {
8168
- updated.assignee = input.assignee;
8169
- }
8170
- if (input.addDependencies) {
8171
- updated.dependencies = [.../* @__PURE__ */ new Set([...updated.dependencies, ...input.addDependencies])];
8172
- }
8173
- if (input.removeDependencies) {
8174
- updated.dependencies = updated.dependencies.filter((d) => {
8175
- var _a;
8176
- return !((_a = input.removeDependencies) == null ? void 0 : _a.includes(d));
8177
- });
8178
- }
8179
- if (input.addReferences) {
8180
- updated.references = [
8181
- .../* @__PURE__ */ new Set([...updated.references || [], ...input.addReferences])
8182
- ];
8183
- }
8184
- if (input.removeReferences) {
8185
- updated.references = (updated.references || []).filter((r2) => {
8186
- var _a;
8187
- return !((_a = input.removeReferences) == null ? void 0 : _a.includes(r2));
8188
- });
8189
- }
8190
- if (input.acceptanceCriteria) {
8191
- updated.acceptanceCriteriaItems = input.acceptanceCriteria.map((ac, i) => ({
8192
- index: i + 1,
8193
- text: ac.text,
8194
- checked: ac.checked || false
8195
- }));
8196
- }
8197
- const content2 = serializeTaskMarkdown(updated);
8198
- if (existing.filePath) {
8199
- await this.fs.deleteFile(existing.filePath).catch(() => {
8200
- });
8201
- }
8202
- const tasksDir = this.getTasksDir();
8203
- const safeTitle = updated.title.replace(/[<>:"/\\|?*]/g, "").replace(/\s+/g, " ").slice(0, 50);
8204
- const filename = `${id} - ${safeTitle}.md`;
8205
- const filepath = this.fs.join(tasksDir, filename);
8206
- await this.fs.writeFile(filepath, content2);
8207
- updated.filePath = filepath;
8208
- this.tasks.set(id, updated);
8209
- const milestoneChanged = milestoneKey(oldMilestone) !== milestoneKey(newMilestone);
8210
- if (milestoneChanged) {
8211
- if (oldMilestone) {
8212
- await this.removeTaskFromMilestone(id, oldMilestone);
8213
- }
8214
- if (newMilestone) {
8215
- await this.addTaskToMilestone(id, newMilestone);
8216
- }
8217
- }
8218
- span.addEvent("task.update.complete", {
8219
- "output.taskId": id,
8220
- "output.statusChanged": input.status !== void 0,
8221
- "duration.ms": Date.now() - startTime
8222
- });
8223
- span.setStatus({ code: SpanStatusCode.OK });
8224
- span.end();
8225
- return updated;
8226
- } catch (error) {
8227
- span.addEvent("task.update.error", {
8228
- "error.type": error instanceof Error ? error.name : "UnknownError",
8229
- "error.message": error instanceof Error ? error.message : String(error),
8230
- "input.taskId": id
8231
- });
8232
- span.setStatus({
8233
- code: SpanStatusCode.ERROR,
8234
- message: error instanceof Error ? error.message : String(error)
8235
- });
8236
- span.end();
8237
- throw error;
8238
- }
8239
- });
8240
- }
8241
- /**
8242
- * Delete a task
8243
- *
8244
- * @param id - Task ID to delete
8245
- * @returns true if deleted, false if not found
8246
- */
8247
- async deleteTask(id) {
8248
- this.ensureInitialized();
8249
- const startTime = Date.now();
8250
- const span = this.tracer.startSpan("task.delete", {
8251
- attributes: { "input.taskId": id }
8252
- });
8253
- return await context.with(trace.setSpan(context.active(), span), async () => {
8254
- try {
8255
- span.addEvent("task.delete.started", { "input.taskId": id });
8256
- const task = this.tasks.get(id);
8257
- if (!task) {
8258
- span.addEvent("task.delete.error", {
8259
- "error.type": "NotFoundError",
8260
- "error.message": "Task not found",
8261
- "input.taskId": id
8262
- });
8263
- span.setStatus({ code: SpanStatusCode.OK });
8264
- span.end();
8265
- return false;
8266
- }
8267
- if (task.milestone) {
8268
- await this.removeTaskFromMilestone(id, task.milestone);
8269
- }
8270
- if (task.filePath) {
8271
- try {
8272
- await this.fs.deleteFile(task.filePath);
8273
- } catch {
8274
- }
8275
- }
8276
- this.tasks.delete(id);
8277
- span.addEvent("task.delete.complete", {
8278
- "output.taskId": id,
8279
- "output.deleted": true,
8280
- "duration.ms": Date.now() - startTime
8281
- });
8282
- span.setStatus({ code: SpanStatusCode.OK });
8283
- span.end();
8284
- return true;
8285
- } catch (error) {
8286
- span.addEvent("task.delete.error", {
8287
- "error.type": error instanceof Error ? error.name : "UnknownError",
8288
- "error.message": error instanceof Error ? error.message : String(error),
8289
- "input.taskId": id
8290
- });
8291
- span.setStatus({
8292
- code: SpanStatusCode.ERROR,
8293
- message: error instanceof Error ? error.message : String(error)
8294
- });
8295
- span.end();
8296
- throw error;
8297
- }
8298
- });
8299
- }
8300
- /**
8301
- * Archive a task (move from tasks/ to completed/)
8302
- *
8303
- * @param id - Task ID to archive
8304
- * @returns Archived task or null if not found or already archived
8305
- */
8306
- async archiveTask(id) {
8307
- this.ensureInitialized();
8308
- const startTime = Date.now();
8309
- const span = this.tracer.startSpan("task.archive", {
8310
- attributes: { "input.taskId": id }
8311
- });
8312
- return await context.with(trace.setSpan(context.active(), span), async () => {
8313
- try {
8314
- span.addEvent("task.archive.started", { "input.taskId": id });
8315
- const task = this.tasks.get(id);
8316
- if (!task) {
8317
- span.addEvent("task.archive.error", {
8318
- "error.type": "NotFoundError",
8319
- "error.message": "Task not found",
8320
- "input.taskId": id
8321
- });
8322
- span.setStatus({ code: SpanStatusCode.OK });
8323
- span.end();
8324
- return null;
8325
- }
8326
- if (task.source === "completed") {
8327
- span.addEvent("task.archive.error", {
8328
- "error.type": "InvalidStateError",
8329
- "error.message": "Task already archived",
8330
- "input.taskId": id
8331
- });
8332
- span.setStatus({ code: SpanStatusCode.OK });
8333
- span.end();
8334
- return null;
8335
- }
8336
- const completedDir = this.getCompletedDir();
8337
- await this.fs.createDir(completedDir, { recursive: true });
8338
- const safeTitle = task.title.replace(/[<>:"/\\|?*]/g, "").replace(/\s+/g, " ").slice(0, 50);
8339
- const filename = `${id} - ${safeTitle}.md`;
8340
- const newFilepath = this.fs.join(completedDir, filename);
8341
- if (task.filePath) {
8342
- try {
8343
- await this.fs.deleteFile(task.filePath);
8344
- } catch {
8345
- }
8346
- }
8347
- const archived = {
8348
- ...task,
8349
- source: "completed",
8350
- filePath: newFilepath
8351
- };
8352
- const content2 = serializeTaskMarkdown(archived);
8353
- await this.fs.writeFile(newFilepath, content2);
8354
- this.tasks.set(id, archived);
8355
- if (this.lazyInitialized) {
8356
- const entry = this.taskIndex.get(id);
8357
- if (entry) {
8358
- entry.source = "completed";
8359
- entry.filePath = newFilepath;
8360
- }
8361
- }
8362
- span.addEvent("task.archive.complete", {
8363
- "output.taskId": id,
8364
- "output.newPath": newFilepath,
8365
- "duration.ms": Date.now() - startTime
8366
- });
8367
- span.setStatus({ code: SpanStatusCode.OK });
8368
- span.end();
8369
- return archived;
8370
- } catch (error) {
8371
- span.addEvent("task.archive.error", {
8372
- "error.type": error instanceof Error ? error.name : "UnknownError",
8373
- "error.message": error instanceof Error ? error.message : String(error),
8374
- "input.taskId": id
8375
- });
8376
- span.setStatus({
8377
- code: SpanStatusCode.ERROR,
8378
- message: error instanceof Error ? error.message : String(error)
8379
- });
8380
- span.end();
8381
- throw error;
8382
- }
8383
- });
8384
- }
8385
- /**
8386
- * Restore a task (move from completed/ to tasks/)
8387
- *
8388
- * @param id - Task ID to restore
8389
- * @returns Restored task or null if not found or not archived
8390
- */
8391
- async restoreTask(id) {
8392
- this.ensureInitialized();
8393
- const startTime = Date.now();
8394
- const span = this.tracer.startSpan("task.restore", {
8395
- attributes: { "input.taskId": id }
8396
- });
8397
- return await context.with(trace.setSpan(context.active(), span), async () => {
8398
- try {
8399
- span.addEvent("task.restore.started", { "input.taskId": id });
8400
- const task = this.tasks.get(id);
8401
- if (!task) {
8402
- span.addEvent("task.restore.error", {
8403
- "error.type": "NotFoundError",
8404
- "error.message": "Task not found",
8405
- "input.taskId": id
8406
- });
8407
- span.setStatus({ code: SpanStatusCode.OK });
8408
- span.end();
8409
- return null;
8410
- }
8411
- if (task.source !== "completed") {
8412
- span.addEvent("task.restore.error", {
8413
- "error.type": "InvalidStateError",
8414
- "error.message": "Task not in completed",
8415
- "input.taskId": id
8416
- });
8417
- span.setStatus({ code: SpanStatusCode.OK });
8418
- span.end();
8419
- return null;
8420
- }
8421
- const tasksDir = this.getTasksDir();
8422
- await this.fs.createDir(tasksDir, { recursive: true });
8423
- const safeTitle = task.title.replace(/[<>:"/\\|?*]/g, "").replace(/\s+/g, " ").slice(0, 50);
8424
- const filename = `${id} - ${safeTitle}.md`;
8425
- const newFilepath = this.fs.join(tasksDir, filename);
8426
- if (task.filePath) {
8427
- try {
8428
- await this.fs.deleteFile(task.filePath);
8429
- } catch {
8430
- }
8431
- }
8432
- const restored = {
8433
- ...task,
8434
- source: "local",
8435
- filePath: newFilepath
8436
- };
8437
- const content2 = serializeTaskMarkdown(restored);
8438
- await this.fs.writeFile(newFilepath, content2);
8439
- this.tasks.set(id, restored);
8440
- if (this.lazyInitialized) {
8441
- const entry = this.taskIndex.get(id);
8442
- if (entry) {
8443
- entry.source = "tasks";
8444
- entry.filePath = newFilepath;
8445
- }
8446
- }
8447
- span.addEvent("task.restore.complete", {
8448
- "output.taskId": id,
8449
- "output.newPath": newFilepath,
8450
- "duration.ms": Date.now() - startTime
8451
- });
8452
- span.setStatus({ code: SpanStatusCode.OK });
8453
- span.end();
8454
- return restored;
8455
- } catch (error) {
8456
- span.addEvent("task.restore.error", {
8457
- "error.type": error instanceof Error ? error.name : "UnknownError",
8458
- "error.message": error instanceof Error ? error.message : String(error),
8459
- "input.taskId": id
8460
- });
8461
- span.setStatus({
8462
- code: SpanStatusCode.ERROR,
8463
- message: error instanceof Error ? error.message : String(error)
8464
- });
8465
- span.end();
8466
- throw error;
8467
- }
8468
- });
8469
- }
8470
- /**
8471
- * Load specific tasks by their IDs (for lazy loading milestone tasks)
8472
- *
8473
- * @param ids - Array of task IDs to load
8474
- * @returns Array of loaded tasks (missing tasks excluded)
8475
- */
8476
- async loadTasksByIds(ids2) {
8477
- if (this.initialized) {
8478
- return ids2.map((id) => this.tasks.get(id)).filter((t) => t !== void 0);
8479
- }
8480
- if (this.lazyInitialized) {
8481
- return this.loadTasks(ids2);
8482
- }
8483
- throw new Error("Core not initialized. Call initialize() or initializeLazy() first.");
8484
- }
8485
- }
8486
- const version = "1.2.9";
6339
+ const version = "1.2.10";
8487
6340
  const packageJson = {
8488
6341
  version
8489
6342
  };