@markmdev/pebble 0.1.21 → 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
@@ -658,6 +658,73 @@ function getAncestryChain(issueId, state) {
658
658
  }
659
659
  return chain;
660
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
+ }
661
728
  function getDescendants(issueId, state) {
662
729
  const issueState = state ?? getComputedState();
663
730
  const descendants = [];
@@ -1740,7 +1807,7 @@ function restoreCommand(program2) {
1740
1807
 
1741
1808
  // src/cli/commands/claim.ts
1742
1809
  function claimCommand(program2) {
1743
- 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) => {
1744
1811
  const pretty = program2.opts().pretty ?? false;
1745
1812
  try {
1746
1813
  const pebbleDir = getOrCreatePebbleDir();
@@ -1752,35 +1819,20 @@ function claimCommand(program2) {
1752
1819
  for (const id of allIds) {
1753
1820
  try {
1754
1821
  const resolvedId = resolveId(id);
1755
- const issue = getIssue(resolvedId);
1756
- if (!issue) {
1757
- results.push({ id, success: false, error: `Issue not found: ${id}` });
1758
- continue;
1759
- }
1760
- if (issue.status === "in_progress") {
1761
- results.push({ id: resolvedId, success: true });
1762
- continue;
1763
- }
1764
- if (issue.status === "closed") {
1765
- results.push({ id: resolvedId, success: false, error: `Cannot claim closed issue: ${resolvedId}` });
1766
- continue;
1767
- }
1768
- if (hasOpenBlockersById(resolvedId)) {
1769
- const blockers = getOpenBlockers(resolvedId);
1770
- const blockerIds = blockers.map((b) => b.id).join(", ");
1771
- results.push({ id: resolvedId, success: false, error: `Cannot claim blocked issue. Blocked by: ${blockerIds}` });
1772
- 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
+ });
1773
1835
  }
1774
- const event = {
1775
- type: "update",
1776
- issueId: resolvedId,
1777
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1778
- data: {
1779
- status: "in_progress"
1780
- }
1781
- };
1782
- appendEvent(event, pebbleDir);
1783
- results.push({ id: resolvedId, success: true });
1784
1836
  } catch (error) {
1785
1837
  results.push({ id, success: false, error: error.message });
1786
1838
  }
@@ -1788,7 +1840,20 @@ function claimCommand(program2) {
1788
1840
  if (allIds.length === 1) {
1789
1841
  const result = results[0];
1790
1842
  if (result.success) {
1791
- 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
+ }
1792
1857
  } else {
1793
1858
  throw new Error(result.error || "Unknown error");
1794
1859
  }
@@ -1796,7 +1861,12 @@ function claimCommand(program2) {
1796
1861
  if (pretty) {
1797
1862
  for (const result of results) {
1798
1863
  if (result.success) {
1799
- 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
+ }
1800
1870
  } else {
1801
1871
  console.log(`\u2717 ${result.id}: ${result.error}`);
1802
1872
  }
@@ -1805,6 +1875,7 @@ function claimCommand(program2) {
1805
1875
  console.log(formatJson(results.map((r) => ({
1806
1876
  id: r.id,
1807
1877
  success: r.success,
1878
+ ...r.claimedIds && { claimedIds: r.claimedIds },
1808
1879
  ...r.error && { error: r.error }
1809
1880
  }))));
1810
1881
  }
@@ -2995,11 +3066,47 @@ data: ${message}
2995
3066
  }
2996
3067
  updates.priority = priority;
2997
3068
  }
3069
+ const cascadeClaim = [];
2998
3070
  if (status !== void 0) {
2999
3071
  if (!STATUSES.includes(status)) {
3000
3072
  res.status(400).json({ error: `Invalid status. Must be one of: ${STATUSES.join(", ")}` });
3001
3073
  return;
3002
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
+ }
3003
3110
  updates.status = status;
3004
3111
  }
3005
3112
  if (description !== void 0) {
@@ -3065,11 +3172,32 @@ data: ${message}
3065
3172
  data: updates
3066
3173
  };
3067
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
+ }
3068
3186
  if (isMultiWorktree()) {
3069
3187
  const updated = findIssueInSources(issueId, issueFiles);
3070
- 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
+ }
3071
3194
  } else {
3072
- 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
+ }
3073
3201
  }
3074
3202
  } catch (error) {
3075
3203
  res.status(500).json({ error: error.message });