@markmdev/pebble 0.1.16 → 0.1.18

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/README.md CHANGED
@@ -84,7 +84,7 @@ pb ui
84
84
  - `-t, --type <type>` — Issue type: task, bug, epic (default: task)
85
85
  - `-p, --priority <n>` — Priority: 0=critical, 4=backlog (default: 2)
86
86
  - `-d, --description <text>` — Description
87
- - `--parent <id>` — Parent epic ID
87
+ - `--parent <id>` — Parent issue ID
88
88
 
89
89
  ### List
90
90
 
@@ -112,7 +112,7 @@ pb ui
112
112
  priority: 0-4; // 0=critical, 4=backlog
113
113
  status: 'open' | 'in_progress' | 'blocked' | 'closed';
114
114
  description?: string;
115
- parent?: string; // Parent epic ID
115
+ parent?: string; // Parent issue ID
116
116
  blockedBy: string[]; // IDs of blocking issues
117
117
  comments: Comment[];
118
118
  createdAt: string;
package/dist/cli/index.js CHANGED
@@ -16,7 +16,7 @@ import { dirname as dirname2, join as join3 } from "path";
16
16
  var ISSUE_TYPES = ["task", "bug", "epic", "verification"];
17
17
  var PRIORITIES = [0, 1, 2, 3, 4];
18
18
  var STATUSES = ["open", "in_progress", "blocked", "pending_verification", "closed"];
19
- var EVENT_TYPES = ["create", "update", "close", "reopen", "comment"];
19
+ var EVENT_TYPES = ["create", "update", "close", "reopen", "comment", "delete", "restore"];
20
20
  var PRIORITY_LABELS = {
21
21
  0: "critical",
22
22
  1: "high",
@@ -360,14 +360,37 @@ function computeState(events) {
360
360
  }
361
361
  break;
362
362
  }
363
+ case "delete": {
364
+ const issue = issues.get(event.issueId);
365
+ if (issue) {
366
+ issue.deleted = true;
367
+ issue.deletedAt = event.timestamp;
368
+ issue.updatedAt = event.timestamp;
369
+ if (event.source) issue.lastSource = event.source;
370
+ }
371
+ break;
372
+ }
373
+ case "restore": {
374
+ const issue = issues.get(event.issueId);
375
+ if (issue) {
376
+ issue.deleted = false;
377
+ issue.deletedAt = void 0;
378
+ issue.updatedAt = event.timestamp;
379
+ if (event.source) issue.lastSource = event.source;
380
+ }
381
+ break;
382
+ }
363
383
  }
364
384
  }
365
385
  return issues;
366
386
  }
367
- function getIssues(filters) {
387
+ function getIssues(filters, includeDeleted = false) {
368
388
  const events = readEvents();
369
389
  const state = computeState(events);
370
390
  let issues = Array.from(state.values());
391
+ if (!includeDeleted) {
392
+ issues = issues.filter((i) => !i.deleted);
393
+ }
371
394
  if (filters) {
372
395
  if (filters.status !== void 0) {
373
396
  issues = issues.filter((i) => i.status === filters.status);
@@ -384,10 +407,14 @@ function getIssues(filters) {
384
407
  }
385
408
  return issues;
386
409
  }
387
- function getIssue(id) {
410
+ function getIssue(id, includeDeleted = false) {
388
411
  const events = readEvents();
389
412
  const state = computeState(events);
390
- return state.get(id);
413
+ const issue = state.get(id);
414
+ if (issue && issue.deleted && !includeDeleted) {
415
+ return void 0;
416
+ }
417
+ return issue;
391
418
  }
392
419
  function resolveId(partial) {
393
420
  const events = readEvents();
@@ -430,6 +457,9 @@ function getReady() {
430
457
  const state = computeState(events);
431
458
  const issues = Array.from(state.values());
432
459
  return issues.filter((issue) => {
460
+ if (issue.deleted) {
461
+ return false;
462
+ }
433
463
  if (issue.status === "closed" || issue.status === "pending_verification") {
434
464
  return false;
435
465
  }
@@ -453,6 +483,9 @@ function getBlocked() {
453
483
  const state = computeState(events);
454
484
  const issues = Array.from(state.values());
455
485
  return issues.filter((issue) => {
486
+ if (issue.deleted) {
487
+ return false;
488
+ }
456
489
  if (issue.status === "closed") {
457
490
  return false;
458
491
  }
@@ -616,6 +649,20 @@ function getAncestryChain(issueId, state) {
616
649
  }
617
650
  return chain;
618
651
  }
652
+ function getDescendants(issueId, state) {
653
+ const issueState = state ?? getComputedState();
654
+ const descendants = [];
655
+ function collectDescendants(parentId) {
656
+ for (const issue of issueState.values()) {
657
+ if (issue.parent === parentId && !issue.deleted) {
658
+ descendants.push(issue);
659
+ collectDescendants(issue.id);
660
+ }
661
+ }
662
+ }
663
+ collectDescendants(issueId);
664
+ return descendants;
665
+ }
619
666
 
620
667
  // src/shared/time.ts
621
668
  import { formatDistanceToNow, parseISO } from "date-fns";
@@ -931,7 +978,7 @@ function formatIssueListVerbose(issues, sectionHeader) {
931
978
  lines.push(`${issue.id}: ${issue.title}`);
932
979
  lines.push(` Type: ${formatType(issue.type)} | Priority: P${issue.priority} | Created: ${formatRelativeTime(issue.createdAt)}`);
933
980
  if (ancestry.length > 0) {
934
- const chain = [...ancestry].reverse().map((a) => a.id).join(" > ");
981
+ const chain = [...ancestry].reverse().map((a) => a.title).join(" \u2192 ");
935
982
  lines.push(` Ancestry: ${chain}`);
936
983
  }
937
984
  if (blocking.length > 0) {
@@ -972,7 +1019,7 @@ function outputIssueListVerbose(issues, pretty, sectionHeader, limitInfo) {
972
1019
 
973
1020
  // src/cli/commands/create.ts
974
1021
  function createCommand(program2) {
975
- program2.command("create <title>").description("Create a new issue").option("-t, --type <type>", "Issue type (task, bug, epic, verification)", "task").option("-p, --priority <priority>", "Priority (0-4)", "2").option("-d, --description <desc>", "Description").option("--parent <id>", "Parent epic ID").option("--verifies <id>", "ID of issue this verifies (sets type to verification)").option("--blocked-by <ids>", "Comma-separated IDs of issues that block this one").option("--blocks <ids>", "Comma-separated IDs of issues this one will block").action(async (title, options) => {
1022
+ program2.command("create <title>").description("Create a new issue").option("-t, --type <type>", "Issue type (task, bug, epic, verification)", "task").option("-p, --priority <priority>", "Priority (0-4)", "2").option("-d, --description <desc>", "Description").option("--parent <id>", "Parent issue ID").option("--verifies <id>", "ID of issue this verifies (sets type to verification)").option("--blocked-by <ids>", "Comma-separated IDs of issues that block this one").option("--blocks <ids>", "Comma-separated IDs of issues this one will block").action(async (title, options) => {
976
1023
  const pretty = program2.opts().pretty ?? false;
977
1024
  try {
978
1025
  let type = options.type;
@@ -998,11 +1045,11 @@ function createCommand(program2) {
998
1045
  if (!parent) {
999
1046
  throw new Error(`Parent issue not found: ${options.parent}`);
1000
1047
  }
1001
- if (parent.type !== "epic") {
1002
- throw new Error(`Parent must be an epic, got: ${parent.type}`);
1048
+ if (parent.type === "verification") {
1049
+ throw new Error(`Verification issues cannot be parents`);
1003
1050
  }
1004
1051
  if (parent.status === "closed") {
1005
- throw new Error(`Cannot add children to closed epic: ${parentId}`);
1052
+ throw new Error(`Cannot add children to closed issue: ${parentId}`);
1006
1053
  }
1007
1054
  }
1008
1055
  let verifiesId;
@@ -1085,7 +1132,7 @@ function createCommand(program2) {
1085
1132
 
1086
1133
  // src/cli/commands/update.ts
1087
1134
  function updateCommand(program2) {
1088
- program2.command("update <ids...>").description("Update issues. Supports multiple IDs.").option("--status <status>", "Status (open, in_progress, blocked, closed)").option("--priority <priority>", "Priority (0-4)").option("--title <title>", "Title").option("--description <desc>", "Description").option("--parent <id>", 'Parent epic ID (use "null" to remove parent)').action(async (ids, options) => {
1135
+ program2.command("update <ids...>").description("Update issues. Supports multiple IDs.").option("--status <status>", "Status (open, in_progress, blocked, closed)").option("--priority <priority>", "Priority (0-4)").option("--title <title>", "Title").option("--description <desc>", "Description").option("--parent <id>", 'Parent issue ID (use "null" to remove parent)').action(async (ids, options) => {
1089
1136
  const pretty = program2.opts().pretty ?? false;
1090
1137
  try {
1091
1138
  const pebbleDir = getOrCreatePebbleDir();
@@ -1128,11 +1175,11 @@ function updateCommand(program2) {
1128
1175
  if (!parentIssue) {
1129
1176
  throw new Error(`Parent issue not found: ${options.parent}`);
1130
1177
  }
1131
- if (parentIssue.type !== "epic") {
1132
- throw new Error(`Parent must be an epic. ${parentId} is a ${parentIssue.type}`);
1178
+ if (parentIssue.type === "verification") {
1179
+ throw new Error(`Verification issues cannot be parents`);
1133
1180
  }
1134
1181
  if (parentIssue.status === "closed") {
1135
- throw new Error(`Cannot set parent to closed epic: ${parentId}`);
1182
+ throw new Error(`Cannot set parent to closed issue: ${parentId}`);
1136
1183
  }
1137
1184
  data.parent = parentId;
1138
1185
  }
@@ -1412,6 +1459,220 @@ function reopenCommand(program2) {
1412
1459
  });
1413
1460
  }
1414
1461
 
1462
+ // src/cli/commands/delete.ts
1463
+ function collectReferenceCleanup(deletedId, deletedIds, state, updates) {
1464
+ for (const [id, issue] of state) {
1465
+ if (deletedIds.has(id)) continue;
1466
+ if (issue.deleted) continue;
1467
+ let entry = updates.get(id);
1468
+ const currentBlockedBy = entry?.blockedBy ?? issue.blockedBy;
1469
+ if (currentBlockedBy.includes(deletedId)) {
1470
+ if (!entry) {
1471
+ entry = {};
1472
+ updates.set(id, entry);
1473
+ }
1474
+ entry.blockedBy = currentBlockedBy.filter((bid) => bid !== deletedId);
1475
+ }
1476
+ const currentRelatedTo = entry?.relatedTo ?? issue.relatedTo;
1477
+ if (currentRelatedTo.includes(deletedId)) {
1478
+ if (!entry) {
1479
+ entry = {};
1480
+ updates.set(id, entry);
1481
+ }
1482
+ entry.relatedTo = currentRelatedTo.filter((rid) => rid !== deletedId);
1483
+ }
1484
+ if (issue.parent === deletedId) {
1485
+ if (!entry) {
1486
+ entry = {};
1487
+ updates.set(id, entry);
1488
+ }
1489
+ entry.parent = "";
1490
+ }
1491
+ }
1492
+ }
1493
+ function emitReferenceCleanup(updates, pebbleDir, timestamp) {
1494
+ for (const [id, data] of updates) {
1495
+ if (Object.keys(data).length > 0) {
1496
+ const updateEvent = {
1497
+ type: "update",
1498
+ issueId: id,
1499
+ timestamp,
1500
+ data
1501
+ };
1502
+ appendEvent(updateEvent, pebbleDir);
1503
+ }
1504
+ }
1505
+ }
1506
+ function deleteCommand(program2) {
1507
+ program2.command("delete <ids...>").description("Delete issues (soft delete). Epics cascade-delete their children.").option("-r, --reason <reason>", "Reason for deleting").action(async (ids, options) => {
1508
+ const pretty = program2.opts().pretty ?? false;
1509
+ try {
1510
+ const pebbleDir = getOrCreatePebbleDir();
1511
+ const state = getComputedState();
1512
+ const allIds = ids.flatMap((id) => id.split(",").map((s) => s.trim()).filter(Boolean));
1513
+ if (allIds.length === 0) {
1514
+ throw new Error("No issue IDs provided");
1515
+ }
1516
+ const results = [];
1517
+ const toDelete = [];
1518
+ const alreadyQueued = /* @__PURE__ */ new Set();
1519
+ for (const id of allIds) {
1520
+ try {
1521
+ const resolvedId = resolveId(id);
1522
+ const issue = state.get(resolvedId);
1523
+ if (!issue) {
1524
+ results.push({ id, success: false, error: `Issue not found: ${id}` });
1525
+ continue;
1526
+ }
1527
+ if (issue.deleted) {
1528
+ results.push({ id: resolvedId, success: false, error: `Issue is already deleted: ${resolvedId}` });
1529
+ continue;
1530
+ }
1531
+ if (!alreadyQueued.has(resolvedId)) {
1532
+ toDelete.push({ id: resolvedId, cascade: false });
1533
+ alreadyQueued.add(resolvedId);
1534
+ }
1535
+ const descendants = getDescendants(resolvedId, state);
1536
+ for (const desc of descendants) {
1537
+ if (!alreadyQueued.has(desc.id) && !desc.deleted) {
1538
+ toDelete.push({ id: desc.id, cascade: true });
1539
+ alreadyQueued.add(desc.id);
1540
+ }
1541
+ }
1542
+ const verifications = getVerifications(resolvedId);
1543
+ for (const v of verifications) {
1544
+ if (!alreadyQueued.has(v.id) && !v.deleted) {
1545
+ toDelete.push({ id: v.id, cascade: true });
1546
+ alreadyQueued.add(v.id);
1547
+ }
1548
+ }
1549
+ } catch (error) {
1550
+ results.push({ id, success: false, error: error.message });
1551
+ }
1552
+ }
1553
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1554
+ const deletedIds = new Set(toDelete.map((d) => d.id));
1555
+ const referenceUpdates = /* @__PURE__ */ new Map();
1556
+ for (const { id } of toDelete) {
1557
+ collectReferenceCleanup(id, deletedIds, state, referenceUpdates);
1558
+ }
1559
+ emitReferenceCleanup(referenceUpdates, pebbleDir, timestamp);
1560
+ for (const { id, cascade } of toDelete) {
1561
+ const issue = state.get(id);
1562
+ if (!issue) continue;
1563
+ const deleteEvent = {
1564
+ type: "delete",
1565
+ issueId: id,
1566
+ timestamp,
1567
+ data: {
1568
+ reason: options.reason,
1569
+ cascade: cascade || void 0,
1570
+ previousStatus: issue.status
1571
+ }
1572
+ };
1573
+ appendEvent(deleteEvent, pebbleDir);
1574
+ results.push({ id, success: true, cascade: cascade || void 0 });
1575
+ }
1576
+ if (pretty) {
1577
+ const primary = results.filter((r) => r.success && !r.cascade);
1578
+ const cascaded = results.filter((r) => r.success && r.cascade);
1579
+ const failed = results.filter((r) => !r.success);
1580
+ for (const result of primary) {
1581
+ console.log(`\u{1F5D1}\uFE0F ${result.id} deleted`);
1582
+ }
1583
+ for (const result of cascaded) {
1584
+ console.log(` \u2514\u2500 ${result.id} deleted (cascade)`);
1585
+ }
1586
+ for (const result of failed) {
1587
+ console.log(`\u2717 ${result.id}: ${result.error}`);
1588
+ }
1589
+ } else {
1590
+ console.log(formatJson({
1591
+ deleted: results.filter((r) => r.success).map((r) => ({ id: r.id, cascade: r.cascade ?? false })),
1592
+ ...results.some((r) => !r.success) && {
1593
+ errors: results.filter((r) => !r.success).map((r) => ({ id: r.id, error: r.error }))
1594
+ }
1595
+ }));
1596
+ }
1597
+ } catch (error) {
1598
+ outputError(error, pretty);
1599
+ }
1600
+ });
1601
+ }
1602
+
1603
+ // src/cli/commands/restore.ts
1604
+ function restoreCommand(program2) {
1605
+ program2.command("restore <ids...>").description("Restore deleted issues").option("-r, --reason <reason>", "Reason for restoring").action(async (ids, options) => {
1606
+ const pretty = program2.opts().pretty ?? false;
1607
+ try {
1608
+ const pebbleDir = getOrCreatePebbleDir();
1609
+ const allIds = ids.flatMap((id) => id.split(",").map((s) => s.trim()).filter(Boolean));
1610
+ if (allIds.length === 0) {
1611
+ throw new Error("No issue IDs provided");
1612
+ }
1613
+ const results = [];
1614
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1615
+ for (const id of allIds) {
1616
+ try {
1617
+ const resolvedId = resolveId(id);
1618
+ const issue = getIssue(resolvedId, true);
1619
+ if (!issue) {
1620
+ results.push({ id, success: false, error: `Issue not found: ${id}` });
1621
+ continue;
1622
+ }
1623
+ if (!issue.deleted) {
1624
+ results.push({ id: resolvedId, success: false, error: `Issue is not deleted: ${resolvedId}` });
1625
+ continue;
1626
+ }
1627
+ const restoreEvent = {
1628
+ type: "restore",
1629
+ issueId: resolvedId,
1630
+ timestamp,
1631
+ data: {
1632
+ reason: options.reason
1633
+ }
1634
+ };
1635
+ appendEvent(restoreEvent, pebbleDir);
1636
+ results.push({ id: resolvedId, success: true });
1637
+ } catch (error) {
1638
+ results.push({ id, success: false, error: error.message });
1639
+ }
1640
+ }
1641
+ if (allIds.length === 1) {
1642
+ const result = results[0];
1643
+ if (result.success) {
1644
+ if (pretty) {
1645
+ console.log(`\u21A9\uFE0F ${result.id} restored`);
1646
+ } else {
1647
+ console.log(formatJson({ id: result.id, success: true }));
1648
+ }
1649
+ } else {
1650
+ throw new Error(result.error || "Unknown error");
1651
+ }
1652
+ } else {
1653
+ if (pretty) {
1654
+ for (const result of results) {
1655
+ if (result.success) {
1656
+ console.log(`\u21A9\uFE0F ${result.id} restored`);
1657
+ } else {
1658
+ console.log(`\u2717 ${result.id}: ${result.error}`);
1659
+ }
1660
+ }
1661
+ } else {
1662
+ console.log(formatJson({
1663
+ restored: results.filter((r) => r.success).map((r) => r.id),
1664
+ ...results.some((r) => !r.success) && {
1665
+ errors: results.filter((r) => !r.success).map((r) => ({ id: r.id, error: r.error }))
1666
+ }
1667
+ }));
1668
+ }
1669
+ }
1670
+ } catch (error) {
1671
+ outputError(error, pretty);
1672
+ }
1673
+ });
1674
+ }
1675
+
1415
1676
  // src/cli/commands/claim.ts
1416
1677
  function claimCommand(program2) {
1417
1678
  program2.command("claim <ids...>").description("Claim issues (set status to in_progress). Supports multiple IDs.").action(async (ids) => {
@@ -2459,12 +2720,12 @@ data: ${message}
2459
2720
  res.status(400).json({ error: `Parent issue not found: ${parent}` });
2460
2721
  return;
2461
2722
  }
2462
- if (parentIssue.type !== "epic") {
2463
- res.status(400).json({ error: "Parent must be an epic" });
2723
+ if (parentIssue.type === "verification") {
2724
+ res.status(400).json({ error: "Verification issues cannot be parents" });
2464
2725
  return;
2465
2726
  }
2466
2727
  if (parentIssue.status === "closed") {
2467
- res.status(400).json({ error: "Cannot add children to a closed epic" });
2728
+ res.status(400).json({ error: "Cannot add children to a closed issue" });
2468
2729
  return;
2469
2730
  }
2470
2731
  }
@@ -2670,8 +2931,8 @@ data: ${message}
2670
2931
  res.status(400).json({ error: `Parent issue not found: ${parent}` });
2671
2932
  return;
2672
2933
  }
2673
- if (parentFound.issue.type !== "epic") {
2674
- res.status(400).json({ error: "Parent must be an epic" });
2934
+ if (parentFound.issue.type === "verification") {
2935
+ res.status(400).json({ error: "Verification issues cannot be parents" });
2675
2936
  return;
2676
2937
  }
2677
2938
  } else {
@@ -2680,8 +2941,8 @@ data: ${message}
2680
2941
  res.status(400).json({ error: `Parent issue not found: ${parent}` });
2681
2942
  return;
2682
2943
  }
2683
- if (parentIssue.type !== "epic") {
2684
- res.status(400).json({ error: "Parent must be an epic" });
2944
+ if (parentIssue.type === "verification") {
2945
+ res.status(400).json({ error: "Verification issues cannot be parents" });
2685
2946
  return;
2686
2947
  }
2687
2948
  }
@@ -2894,6 +3155,151 @@ data: ${message}
2894
3155
  res.status(500).json({ error: error.message });
2895
3156
  }
2896
3157
  });
3158
+ app.post("/api/issues/:id/delete", (req, res) => {
3159
+ try {
3160
+ let issue;
3161
+ let issueId;
3162
+ let targetFile;
3163
+ if (isMultiWorktree()) {
3164
+ const found = findIssueInSources(req.params.id, issueFiles);
3165
+ if (!found) {
3166
+ res.status(404).json({ error: `Issue not found: ${req.params.id}` });
3167
+ return;
3168
+ }
3169
+ issue = found.issue;
3170
+ issueId = issue.id;
3171
+ targetFile = found.targetFile;
3172
+ } else {
3173
+ const pebbleDir = getOrCreatePebbleDir();
3174
+ issueId = resolveId(req.params.id);
3175
+ const localIssue = getIssue(issueId);
3176
+ if (!localIssue) {
3177
+ res.status(404).json({ error: `Issue not found: ${req.params.id}` });
3178
+ return;
3179
+ }
3180
+ issue = localIssue;
3181
+ targetFile = path3.join(pebbleDir, "issues.jsonl");
3182
+ }
3183
+ if (issue.deleted) {
3184
+ res.status(400).json({ error: "Issue is already deleted" });
3185
+ return;
3186
+ }
3187
+ const { reason } = req.body;
3188
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3189
+ const state = getComputedState();
3190
+ const toDelete = [];
3191
+ const alreadyQueued = /* @__PURE__ */ new Set();
3192
+ toDelete.push({ id: issueId, cascade: false });
3193
+ alreadyQueued.add(issueId);
3194
+ const descendants = getDescendants(issueId, state);
3195
+ for (const desc of descendants) {
3196
+ if (!alreadyQueued.has(desc.id) && !desc.deleted) {
3197
+ toDelete.push({ id: desc.id, cascade: true });
3198
+ alreadyQueued.add(desc.id);
3199
+ }
3200
+ }
3201
+ const verifications = getVerifications(issueId);
3202
+ for (const v of verifications) {
3203
+ if (!alreadyQueued.has(v.id) && !v.deleted) {
3204
+ toDelete.push({ id: v.id, cascade: true });
3205
+ alreadyQueued.add(v.id);
3206
+ }
3207
+ }
3208
+ const cleanupReferences = (deletedId) => {
3209
+ for (const [id, iss] of state) {
3210
+ if (id === deletedId || iss.deleted) continue;
3211
+ const updates = {};
3212
+ if (iss.blockedBy.includes(deletedId)) {
3213
+ updates.blockedBy = iss.blockedBy.filter((bid) => bid !== deletedId);
3214
+ }
3215
+ if (iss.relatedTo.includes(deletedId)) {
3216
+ updates.relatedTo = iss.relatedTo.filter((rid) => rid !== deletedId);
3217
+ }
3218
+ if (iss.parent === deletedId) {
3219
+ updates.parent = "";
3220
+ }
3221
+ if (Object.keys(updates).length > 0) {
3222
+ const updateEvent = {
3223
+ type: "update",
3224
+ issueId: id,
3225
+ timestamp,
3226
+ data: updates
3227
+ };
3228
+ appendEventToFile(updateEvent, targetFile);
3229
+ }
3230
+ }
3231
+ };
3232
+ for (const { id, cascade } of toDelete) {
3233
+ const iss = state.get(id);
3234
+ if (!iss) continue;
3235
+ cleanupReferences(id);
3236
+ const deleteEvent = {
3237
+ type: "delete",
3238
+ issueId: id,
3239
+ timestamp,
3240
+ data: {
3241
+ reason,
3242
+ cascade: cascade || void 0,
3243
+ previousStatus: iss.status
3244
+ }
3245
+ };
3246
+ appendEventToFile(deleteEvent, targetFile);
3247
+ }
3248
+ res.json({
3249
+ deleted: toDelete
3250
+ });
3251
+ } catch (error) {
3252
+ res.status(500).json({ error: error.message });
3253
+ }
3254
+ });
3255
+ app.post("/api/issues/:id/restore", (req, res) => {
3256
+ try {
3257
+ let issue;
3258
+ let issueId;
3259
+ let targetFile;
3260
+ if (isMultiWorktree()) {
3261
+ const found = findIssueInSources(req.params.id, issueFiles);
3262
+ if (!found) {
3263
+ res.status(404).json({ error: `Issue not found: ${req.params.id}` });
3264
+ return;
3265
+ }
3266
+ issue = found.issue;
3267
+ issueId = issue.id;
3268
+ targetFile = found.targetFile;
3269
+ } else {
3270
+ const pebbleDir = getOrCreatePebbleDir();
3271
+ issueId = resolveId(req.params.id);
3272
+ const localIssue = getIssue(issueId);
3273
+ if (!localIssue) {
3274
+ res.status(404).json({ error: `Issue not found: ${req.params.id}` });
3275
+ return;
3276
+ }
3277
+ issue = localIssue;
3278
+ targetFile = path3.join(pebbleDir, "issues.jsonl");
3279
+ }
3280
+ if (!issue.deleted) {
3281
+ res.status(400).json({ error: "Issue is not deleted" });
3282
+ return;
3283
+ }
3284
+ const { reason } = req.body;
3285
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3286
+ const event = {
3287
+ type: "restore",
3288
+ issueId,
3289
+ timestamp,
3290
+ data: { reason }
3291
+ };
3292
+ appendEventToFile(event, targetFile);
3293
+ if (isMultiWorktree()) {
3294
+ const updated = findIssueInSources(issueId, issueFiles);
3295
+ res.json(updated?.issue || { ...issue, deleted: false, deletedAt: void 0, updatedAt: timestamp });
3296
+ } else {
3297
+ res.json(getIssue(issueId));
3298
+ }
3299
+ } catch (error) {
3300
+ res.status(500).json({ error: error.message });
3301
+ }
3302
+ });
2897
3303
  app.post("/api/issues/:id/comments", (req, res) => {
2898
3304
  try {
2899
3305
  let issue;
@@ -3401,6 +3807,35 @@ function countVerifications(epicId) {
3401
3807
  }
3402
3808
  return { total, done };
3403
3809
  }
3810
+ function getIssueComments(issueId) {
3811
+ const events = readEvents();
3812
+ return events.filter((e) => e.type === "comment" && e.issueId === issueId).sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()).map((e) => ({ text: e.data.text, timestamp: e.timestamp, source: e.source }));
3813
+ }
3814
+ function formatInProgressPretty(issues) {
3815
+ if (issues.length === 0) return "";
3816
+ const lines = [];
3817
+ lines.push(`## In Progress (${issues.length})`);
3818
+ lines.push("");
3819
+ for (const issue of issues) {
3820
+ lines.push(`\u25B6 ${issue.id}: ${issue.title} [${issue.type}]`);
3821
+ lines.push(` Updated: ${formatRelativeTime(issue.updatedAt)}${issue.lastSource ? ` | Source: ${issue.lastSource}` : ""}`);
3822
+ if (issue.parent) {
3823
+ lines.push(` Parent: ${issue.parent.title}`);
3824
+ }
3825
+ if (issue.comments.length > 0) {
3826
+ const recentComments = issue.comments.slice(0, 3);
3827
+ for (const comment of recentComments) {
3828
+ const truncated = comment.text.length > 100 ? comment.text.substring(0, 100) + "..." : comment.text;
3829
+ lines.push(` \u2022 ${formatRelativeTime(comment.timestamp)}: ${truncated.replace(/\n/g, " ")}`);
3830
+ }
3831
+ if (issue.comments.length > 3) {
3832
+ lines.push(` (${issue.comments.length - 3} more comment${issue.comments.length - 3 === 1 ? "" : "s"})`);
3833
+ }
3834
+ }
3835
+ lines.push("");
3836
+ }
3837
+ return lines.join("\n");
3838
+ }
3404
3839
  function formatSummaryPretty(summaries, sectionHeader) {
3405
3840
  if (summaries.length === 0) {
3406
3841
  return "No epics found.";
@@ -3478,6 +3913,28 @@ function summaryCommand(program2) {
3478
3913
  }
3479
3914
  return;
3480
3915
  }
3916
+ const inProgressIssues = getIssues({ status: "in_progress" });
3917
+ inProgressIssues.sort(
3918
+ (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
3919
+ );
3920
+ const inProgressSummaries = inProgressIssues.map((issue) => {
3921
+ const summary = {
3922
+ id: issue.id,
3923
+ title: issue.title,
3924
+ type: issue.type,
3925
+ createdAt: issue.createdAt,
3926
+ updatedAt: issue.updatedAt,
3927
+ lastSource: issue.lastSource,
3928
+ comments: getIssueComments(issue.id)
3929
+ };
3930
+ if (issue.parent) {
3931
+ const parentIssue = getIssue(issue.parent, true);
3932
+ if (parentIssue) {
3933
+ summary.parent = { id: parentIssue.id, title: parentIssue.title };
3934
+ }
3935
+ }
3936
+ return summary;
3937
+ });
3481
3938
  const openEpics = allEpics.filter((e) => e.status !== "closed");
3482
3939
  const seventyTwoHoursAgo = Date.now() - 72 * 60 * 60 * 1e3;
3483
3940
  const closedEpics = allEpics.filter(
@@ -3495,7 +3952,12 @@ function summaryCommand(program2) {
3495
3952
  const closedSummaries = limitedClosed.map(buildSummary);
3496
3953
  if (pretty) {
3497
3954
  const output = [];
3955
+ const inProgressOutput = formatInProgressPretty(inProgressSummaries);
3956
+ if (inProgressOutput) {
3957
+ output.push(inProgressOutput);
3958
+ }
3498
3959
  if (openSummaries.length > 0) {
3960
+ if (output.length > 0) output.push("");
3499
3961
  output.push(formatSummaryPretty(openSummaries, "Open Epics"));
3500
3962
  }
3501
3963
  if (closedSummaries.length > 0) {
@@ -3503,11 +3965,11 @@ function summaryCommand(program2) {
3503
3965
  output.push(formatSummaryPretty(closedSummaries, "Recently Closed Epics (last 72h)"));
3504
3966
  }
3505
3967
  if (output.length === 0) {
3506
- output.push("No epics found.");
3968
+ output.push("No issues in progress and no epics found.");
3507
3969
  }
3508
3970
  console.log(output.join("\n"));
3509
3971
  } else {
3510
- console.log(formatJson({ open: openSummaries, closed: closedSummaries }));
3972
+ console.log(formatJson({ inProgress: inProgressSummaries, open: openSummaries, closed: closedSummaries }));
3511
3973
  }
3512
3974
  } catch (error) {
3513
3975
  outputError(error, pretty);
@@ -3786,6 +4248,8 @@ createCommand(program);
3786
4248
  updateCommand(program);
3787
4249
  closeCommand(program);
3788
4250
  reopenCommand(program);
4251
+ deleteCommand(program);
4252
+ restoreCommand(program);
3789
4253
  claimCommand(program);
3790
4254
  listCommand(program);
3791
4255
  showCommand(program);