@markmdev/pebble 0.1.20 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -566,10 +566,12 @@ function getBlocking(issueId) {
566
566
  (issue) => issue.blockedBy.includes(issueId)
567
567
  );
568
568
  }
569
- function getChildren(epicId) {
569
+ function getChildren(epicId, includeDeleted = false) {
570
570
  const events = readEvents();
571
571
  const state = computeState(events);
572
- return Array.from(state.values()).filter((issue) => issue.parent === epicId);
572
+ return Array.from(state.values()).filter(
573
+ (issue) => issue.parent === epicId && (includeDeleted || !issue.deleted)
574
+ );
573
575
  }
574
576
  function getVerifications(issueId) {
575
577
  const events = readEvents();
@@ -656,6 +658,73 @@ function getAncestryChain(issueId, state) {
656
658
  }
657
659
  return chain;
658
660
  }
661
+ function getAncestryBlocker(issueId, state) {
662
+ let current = state.get(issueId);
663
+ while (current?.parent) {
664
+ const parent = state.get(current.parent);
665
+ if (!parent) break;
666
+ const openBlockers = parent.blockedBy.map((id) => state.get(id)).filter((i) => i !== void 0 && i.status !== "closed" && !i.deleted);
667
+ if (openBlockers.length > 0) {
668
+ return { blockedAncestor: parent, blockers: openBlockers };
669
+ }
670
+ current = parent;
671
+ }
672
+ return null;
673
+ }
674
+ function claimWithCascade(issueId, pebbleDir) {
675
+ const events = readEvents();
676
+ const state = computeState(events);
677
+ const issue = state.get(issueId);
678
+ if (!issue) {
679
+ return { success: false, error: `Issue not found: ${issueId}` };
680
+ }
681
+ if (issue.deleted) {
682
+ return { success: false, error: `Cannot claim deleted issue: ${issueId}` };
683
+ }
684
+ if (issue.status === "closed") {
685
+ return { success: false, error: `Cannot claim closed issue: ${issueId}` };
686
+ }
687
+ const openBlockers = issue.blockedBy.map((id) => state.get(id)).filter((i) => i !== void 0 && i.status !== "closed" && !i.deleted);
688
+ if (openBlockers.length > 0) {
689
+ const blockerIds = openBlockers.map((b) => b.id).join(", ");
690
+ return {
691
+ success: false,
692
+ error: `Cannot claim blocked issue. Blocked by: ${blockerIds}`
693
+ };
694
+ }
695
+ const ancestryBlocker = getAncestryBlocker(issueId, state);
696
+ if (ancestryBlocker) {
697
+ const blockerIds = ancestryBlocker.blockers.map((b) => b.id).join(", ");
698
+ return {
699
+ success: false,
700
+ error: `Parent ${ancestryBlocker.blockedAncestor.id} is blocked by: ${blockerIds}`
701
+ };
702
+ }
703
+ const toClaim = [];
704
+ if (issue.status !== "in_progress") {
705
+ toClaim.push(issueId);
706
+ }
707
+ let current = issue;
708
+ while (current.parent) {
709
+ const parent = state.get(current.parent);
710
+ if (!parent) break;
711
+ if (parent.status === "open") {
712
+ toClaim.push(parent.id);
713
+ }
714
+ current = parent;
715
+ }
716
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
717
+ for (const id of toClaim) {
718
+ const event = {
719
+ type: "update",
720
+ issueId: id,
721
+ timestamp,
722
+ data: { status: "in_progress" }
723
+ };
724
+ appendEvent(event, pebbleDir);
725
+ }
726
+ return { success: true, claimedIds: toClaim };
727
+ }
659
728
  function getDescendants(issueId, state) {
660
729
  const issueState = state ?? getComputedState();
661
730
  const descendants = [];
@@ -1148,6 +1217,15 @@ function createCommand(program2) {
1148
1217
  }
1149
1218
  };
1150
1219
  appendEvent(event, pebbleDir);
1220
+ if (parentId) {
1221
+ const parentUpdateEvent = {
1222
+ type: "update",
1223
+ issueId: parentId,
1224
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1225
+ data: {}
1226
+ };
1227
+ appendEvent(parentUpdateEvent, pebbleDir);
1228
+ }
1151
1229
  if (blockedByIds.length > 0) {
1152
1230
  const depEvent = {
1153
1231
  type: "update",
@@ -1322,8 +1400,8 @@ function closeCommand(program2) {
1322
1400
  results.push({ id: resolvedId, success: false, error: `Issue is already closed: ${resolvedId}` });
1323
1401
  continue;
1324
1402
  }
1325
- if (issue.type === "epic" && hasOpenChildren(resolvedId)) {
1326
- results.push({ id: resolvedId, success: false, error: `Cannot close epic with open children: ${resolvedId}` });
1403
+ if (hasOpenChildren(resolvedId)) {
1404
+ results.push({ id: resolvedId, success: false, error: `Cannot close issue with open children: ${resolvedId}` });
1327
1405
  continue;
1328
1406
  }
1329
1407
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
@@ -1729,7 +1807,7 @@ function restoreCommand(program2) {
1729
1807
 
1730
1808
  // src/cli/commands/claim.ts
1731
1809
  function claimCommand(program2) {
1732
- program2.command("claim <ids...>").description("Claim issues (set status to in_progress). Supports multiple IDs.").action(async (ids) => {
1810
+ program2.command("claim <ids...>").description("Claim issues (set status to in_progress). Cascades to open parent issues.").action(async (ids) => {
1733
1811
  const pretty = program2.opts().pretty ?? false;
1734
1812
  try {
1735
1813
  const pebbleDir = getOrCreatePebbleDir();
@@ -1741,35 +1819,20 @@ function claimCommand(program2) {
1741
1819
  for (const id of allIds) {
1742
1820
  try {
1743
1821
  const resolvedId = resolveId(id);
1744
- const issue = getIssue(resolvedId);
1745
- if (!issue) {
1746
- results.push({ id, success: false, error: `Issue not found: ${id}` });
1747
- continue;
1748
- }
1749
- if (issue.status === "in_progress") {
1750
- results.push({ id: resolvedId, success: true });
1751
- continue;
1752
- }
1753
- if (issue.status === "closed") {
1754
- results.push({ id: resolvedId, success: false, error: `Cannot claim closed issue: ${resolvedId}` });
1755
- continue;
1756
- }
1757
- if (hasOpenBlockersById(resolvedId)) {
1758
- const blockers = getOpenBlockers(resolvedId);
1759
- const blockerIds = blockers.map((b) => b.id).join(", ");
1760
- results.push({ id: resolvedId, success: false, error: `Cannot claim blocked issue. Blocked by: ${blockerIds}` });
1761
- continue;
1822
+ const result = claimWithCascade(resolvedId, pebbleDir);
1823
+ if (result.success) {
1824
+ results.push({
1825
+ id: resolvedId,
1826
+ success: true,
1827
+ claimedIds: result.claimedIds
1828
+ });
1829
+ } else {
1830
+ results.push({
1831
+ id: resolvedId,
1832
+ success: false,
1833
+ error: result.error
1834
+ });
1762
1835
  }
1763
- const event = {
1764
- type: "update",
1765
- issueId: resolvedId,
1766
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1767
- data: {
1768
- status: "in_progress"
1769
- }
1770
- };
1771
- appendEvent(event, pebbleDir);
1772
- results.push({ id: resolvedId, success: true });
1773
1836
  } catch (error) {
1774
1837
  results.push({ id, success: false, error: error.message });
1775
1838
  }
@@ -1777,7 +1840,20 @@ function claimCommand(program2) {
1777
1840
  if (allIds.length === 1) {
1778
1841
  const result = results[0];
1779
1842
  if (result.success) {
1780
- outputMutationSuccess(result.id, pretty);
1843
+ if (pretty) {
1844
+ const cascaded = result.claimedIds?.filter((cid) => cid !== result.id) ?? [];
1845
+ if (cascaded.length > 0) {
1846
+ console.log(`\u2713 ${result.id} (also claimed: ${cascaded.join(", ")})`);
1847
+ } else {
1848
+ console.log(`\u2713 ${result.id}`);
1849
+ }
1850
+ } else {
1851
+ console.log(formatJson({
1852
+ id: result.id,
1853
+ success: true,
1854
+ claimedIds: result.claimedIds
1855
+ }));
1856
+ }
1781
1857
  } else {
1782
1858
  throw new Error(result.error || "Unknown error");
1783
1859
  }
@@ -1785,7 +1861,12 @@ function claimCommand(program2) {
1785
1861
  if (pretty) {
1786
1862
  for (const result of results) {
1787
1863
  if (result.success) {
1788
- console.log(`\u2713 ${result.id}`);
1864
+ const cascaded = result.claimedIds?.filter((cid) => cid !== result.id) ?? [];
1865
+ if (cascaded.length > 0) {
1866
+ console.log(`\u2713 ${result.id} (also claimed: ${cascaded.join(", ")})`);
1867
+ } else {
1868
+ console.log(`\u2713 ${result.id}`);
1869
+ }
1789
1870
  } else {
1790
1871
  console.log(`\u2717 ${result.id}: ${result.error}`);
1791
1872
  }
@@ -1794,6 +1875,7 @@ function claimCommand(program2) {
1794
1875
  console.log(formatJson(results.map((r) => ({
1795
1876
  id: r.id,
1796
1877
  success: r.success,
1878
+ ...r.claimedIds && { claimedIds: r.claimedIds },
1797
1879
  ...r.error && { error: r.error }
1798
1880
  }))));
1799
1881
  }
@@ -2805,6 +2887,15 @@ data: ${message}
2805
2887
  }
2806
2888
  };
2807
2889
  appendEvent(event, pebbleDir);
2890
+ if (parent) {
2891
+ const parentUpdateEvent = {
2892
+ type: "update",
2893
+ issueId: parent,
2894
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2895
+ data: {}
2896
+ };
2897
+ appendEvent(parentUpdateEvent, pebbleDir);
2898
+ }
2808
2899
  const issue = getIssue(issueId);
2809
2900
  const result = parentReopened ? { ...issue, _parentReopened: parentReopened } : issue;
2810
2901
  res.status(201).json(result);
@@ -2833,8 +2924,8 @@ data: ${message}
2833
2924
  results.push({ id: issueId, success: true });
2834
2925
  continue;
2835
2926
  }
2836
- if (issue.type === "epic" && hasOpenChildren(issueId)) {
2837
- results.push({ id: issueId, success: false, error: "Cannot close epic with open children" });
2927
+ if (hasOpenChildren(issueId)) {
2928
+ results.push({ id: issueId, success: false, error: "Cannot close issue with open children" });
2838
2929
  continue;
2839
2930
  }
2840
2931
  const pendingVerifications = getVerifications(issueId).filter((v) => v.status !== "closed");
@@ -2975,11 +3066,47 @@ data: ${message}
2975
3066
  }
2976
3067
  updates.priority = priority;
2977
3068
  }
3069
+ const cascadeClaim = [];
2978
3070
  if (status !== void 0) {
2979
3071
  if (!STATUSES.includes(status)) {
2980
3072
  res.status(400).json({ error: `Invalid status. Must be one of: ${STATUSES.join(", ")}` });
2981
3073
  return;
2982
3074
  }
3075
+ if (status === "in_progress" && issue.status !== "in_progress") {
3076
+ const state = isMultiWorktree() ? computeState(issueFiles.flatMap((f) => readEventsFromFile(f))) : getComputedState();
3077
+ const openBlockers = issue.blockedBy.map((id) => state.get(id)).filter((i) => i !== void 0 && i.status !== "closed" && !i.deleted);
3078
+ if (openBlockers.length > 0) {
3079
+ const blockerIds = openBlockers.map((b) => b.id).join(", ");
3080
+ res.status(400).json({
3081
+ error: `Cannot claim blocked issue. Blocked by: ${blockerIds}`
3082
+ });
3083
+ return;
3084
+ }
3085
+ const ancestryBlocker = getAncestryBlocker(issueId, state);
3086
+ if (ancestryBlocker) {
3087
+ const blockerIds = ancestryBlocker.blockers.map((b) => b.id).join(", ");
3088
+ res.status(400).json({
3089
+ error: `Parent ${ancestryBlocker.blockedAncestor.id} is blocked by: ${blockerIds}`
3090
+ });
3091
+ return;
3092
+ }
3093
+ let current = issue;
3094
+ while (current.parent) {
3095
+ const parent2 = state.get(current.parent);
3096
+ if (!parent2) break;
3097
+ if (parent2.status === "open") {
3098
+ let parentTargetFile = targetFile;
3099
+ if (isMultiWorktree()) {
3100
+ const parentFound = findIssueInSources(parent2.id, issueFiles);
3101
+ if (parentFound) {
3102
+ parentTargetFile = parentFound.targetFile;
3103
+ }
3104
+ }
3105
+ cascadeClaim.push({ id: parent2.id, targetFile: parentTargetFile });
3106
+ }
3107
+ current = parent2;
3108
+ }
3109
+ }
2983
3110
  updates.status = status;
2984
3111
  }
2985
3112
  if (description !== void 0) {
@@ -3045,11 +3172,32 @@ data: ${message}
3045
3172
  data: updates
3046
3173
  };
3047
3174
  appendEventToFile(event, targetFile);
3175
+ const cascadeClaimedIds = [];
3176
+ for (const { id, targetFile: parentTargetFile } of cascadeClaim) {
3177
+ const cascadeEvent = {
3178
+ type: "update",
3179
+ issueId: id,
3180
+ timestamp,
3181
+ data: { status: "in_progress" }
3182
+ };
3183
+ appendEventToFile(cascadeEvent, parentTargetFile);
3184
+ cascadeClaimedIds.push(id);
3185
+ }
3048
3186
  if (isMultiWorktree()) {
3049
3187
  const updated = findIssueInSources(issueId, issueFiles);
3050
- res.json(updated?.issue || { ...issue, ...updates, updatedAt: timestamp });
3188
+ const baseIssue = updated?.issue || { ...issue, ...updates, updatedAt: timestamp };
3189
+ if (cascadeClaimedIds.length > 0) {
3190
+ res.json({ ...baseIssue, _cascadeClaimed: cascadeClaimedIds });
3191
+ } else {
3192
+ res.json(baseIssue);
3193
+ }
3051
3194
  } else {
3052
- res.json(getIssue(issueId));
3195
+ const updatedIssue = getIssue(issueId);
3196
+ if (cascadeClaimedIds.length > 0) {
3197
+ res.json({ ...updatedIssue, _cascadeClaimed: cascadeClaimedIds });
3198
+ } else {
3199
+ res.json(updatedIssue);
3200
+ }
3053
3201
  }
3054
3202
  } catch (error) {
3055
3203
  res.status(500).json({ error: error.message });
@@ -3084,8 +3232,8 @@ data: ${message}
3084
3232
  res.status(400).json({ error: "Issue is already closed" });
3085
3233
  return;
3086
3234
  }
3087
- if (!isMultiWorktree() && issue.type === "epic" && hasOpenChildren(issueId)) {
3088
- res.status(400).json({ error: "Cannot close epic with open children" });
3235
+ if (!isMultiWorktree() && hasOpenChildren(issueId)) {
3236
+ res.status(400).json({ error: "Cannot close issue with open children" });
3089
3237
  return;
3090
3238
  }
3091
3239
  const { reason } = req.body;
@@ -3898,6 +4046,21 @@ function formatInProgressPretty(issues) {
3898
4046
  }
3899
4047
  return lines.join("\n");
3900
4048
  }
4049
+ function formatClosedIssuesPretty(issues) {
4050
+ if (issues.length === 0) return "";
4051
+ const lines = [];
4052
+ lines.push(`## Recently Closed (${issues.length})`);
4053
+ lines.push("");
4054
+ for (const issue of issues) {
4055
+ lines.push(`\u2713 ${issue.id}: ${issue.title} [${issue.type}]`);
4056
+ lines.push(` Closed: ${formatRelativeTime(issue.closedAt)}${issue.lastSource ? ` | Source: ${issue.lastSource}` : ""}`);
4057
+ if (issue.parent) {
4058
+ lines.push(` Parent: ${issue.parent.title}`);
4059
+ }
4060
+ lines.push("");
4061
+ }
4062
+ return lines.join("\n");
4063
+ }
3901
4064
  function formatSummaryPretty(summaries, sectionHeader) {
3902
4065
  if (summaries.length === 0) {
3903
4066
  return "No epics found.";
@@ -4012,6 +4175,27 @@ function summaryCommand(program2) {
4012
4175
  const limitedClosed = limit > 0 ? closedEpics.slice(0, limit) : closedEpics;
4013
4176
  const openSummaries = limitedOpen.map(buildSummary);
4014
4177
  const closedSummaries = limitedClosed.map(buildSummary);
4178
+ let closedIssues = getIssues({ status: "closed" });
4179
+ closedIssues.sort(
4180
+ (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
4181
+ );
4182
+ closedIssues = closedIssues.slice(0, 20);
4183
+ const recentlyClosedSummaries = closedIssues.map((issue) => {
4184
+ const summary = {
4185
+ id: issue.id,
4186
+ title: issue.title,
4187
+ type: issue.type,
4188
+ closedAt: issue.updatedAt,
4189
+ lastSource: issue.lastSource
4190
+ };
4191
+ if (issue.parent) {
4192
+ const parentIssue = getIssue(issue.parent, true);
4193
+ if (parentIssue) {
4194
+ summary.parent = { id: parentIssue.id, title: parentIssue.title };
4195
+ }
4196
+ }
4197
+ return summary;
4198
+ });
4015
4199
  if (pretty) {
4016
4200
  const output = [];
4017
4201
  const inProgressOutput = formatInProgressPretty(inProgressSummaries);
@@ -4026,12 +4210,16 @@ function summaryCommand(program2) {
4026
4210
  if (output.length > 0) output.push("");
4027
4211
  output.push(formatSummaryPretty(closedSummaries, "Recently Closed Epics (last 72h)"));
4028
4212
  }
4213
+ if (recentlyClosedSummaries.length > 0) {
4214
+ if (output.length > 0) output.push("");
4215
+ output.push(formatClosedIssuesPretty(recentlyClosedSummaries));
4216
+ }
4029
4217
  if (output.length === 0) {
4030
4218
  output.push("No issues in progress and no epics found.");
4031
4219
  }
4032
4220
  console.log(output.join("\n"));
4033
4221
  } else {
4034
- console.log(formatJson({ inProgress: inProgressSummaries, open: openSummaries, closed: closedSummaries }));
4222
+ console.log(formatJson({ inProgress: inProgressSummaries, open: openSummaries, closed: closedSummaries, recentlyClosed: recentlyClosedSummaries }));
4035
4223
  }
4036
4224
  } catch (error) {
4037
4225
  outputError(error, pretty);