@markmdev/pebble 0.1.6 → 0.1.8

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
@@ -12,7 +12,7 @@ import { Command } from "commander";
12
12
  // src/shared/types.ts
13
13
  var ISSUE_TYPES = ["task", "bug", "epic", "verification"];
14
14
  var PRIORITIES = [0, 1, 2, 3, 4];
15
- var STATUSES = ["open", "in_progress", "blocked", "closed"];
15
+ var STATUSES = ["open", "in_progress", "blocked", "pending_verification", "closed"];
16
16
  var EVENT_TYPES = ["create", "update", "close", "reopen", "comment"];
17
17
  var PRIORITY_LABELS = {
18
18
  0: "critical",
@@ -25,6 +25,7 @@ var STATUS_LABELS = {
25
25
  open: "Open",
26
26
  in_progress: "In Progress",
27
27
  blocked: "Blocked",
28
+ pending_verification: "Pending Verification",
28
29
  closed: "Closed"
29
30
  };
30
31
  var TYPE_LABELS = {
@@ -183,6 +184,7 @@ function computeState(events) {
183
184
  description: createEvent.data.description,
184
185
  parent: createEvent.data.parent,
185
186
  blockedBy: [],
187
+ relatedTo: [],
186
188
  verifies: createEvent.data.verifies,
187
189
  comments: [],
188
190
  createdAt: event.timestamp,
@@ -216,6 +218,9 @@ function computeState(events) {
216
218
  if (updateEvent.data.blockedBy !== void 0) {
217
219
  issue.blockedBy = updateEvent.data.blockedBy;
218
220
  }
221
+ if (updateEvent.data.relatedTo !== void 0) {
222
+ issue.relatedTo = updateEvent.data.relatedTo;
223
+ }
219
224
  issue.updatedAt = event.timestamp;
220
225
  }
221
226
  break;
@@ -315,7 +320,7 @@ function getReady() {
315
320
  const state = computeState(events);
316
321
  const issues = Array.from(state.values());
317
322
  return issues.filter((issue) => {
318
- if (issue.status === "closed") {
323
+ if (issue.status === "closed" || issue.status === "pending_verification") {
319
324
  return false;
320
325
  }
321
326
  for (const blockerId of issue.blockedBy) {
@@ -456,6 +461,36 @@ function getNewlyUnblocked(closedIssueId) {
456
461
  }
457
462
  return result;
458
463
  }
464
+ function getRelated(issueId) {
465
+ const issue = getIssue(issueId);
466
+ if (!issue) {
467
+ return [];
468
+ }
469
+ const events = readEvents();
470
+ const state = computeState(events);
471
+ return issue.relatedTo.map((id) => state.get(id)).filter((i) => i !== void 0);
472
+ }
473
+ function hasOpenBlockersById(issueId) {
474
+ const issue = getIssue(issueId);
475
+ if (!issue) {
476
+ return false;
477
+ }
478
+ const events = readEvents();
479
+ const state = computeState(events);
480
+ return issue.blockedBy.some((blockerId) => {
481
+ const blocker = state.get(blockerId);
482
+ return blocker && blocker.status !== "closed";
483
+ });
484
+ }
485
+ function getOpenBlockers(issueId) {
486
+ const issue = getIssue(issueId);
487
+ if (!issue) {
488
+ return [];
489
+ }
490
+ const events = readEvents();
491
+ const state = computeState(events);
492
+ return issue.blockedBy.map((id) => state.get(id)).filter((i) => i !== void 0 && i.status !== "closed");
493
+ }
459
494
 
460
495
  // src/cli/lib/output.ts
461
496
  function formatJson(data) {
@@ -547,7 +582,7 @@ function formatIssueListPretty(issues) {
547
582
  lines.push(`Total: ${issues.length} issue(s)`);
548
583
  return lines.join("\n");
549
584
  }
550
- function formatDepsPretty(issueId, blockedBy, blocking) {
585
+ function formatDepsPretty(issueId, blockedBy, blocking, related = []) {
551
586
  const lines = [];
552
587
  lines.push(`Dependencies for ${issueId}`);
553
588
  lines.push("\u2500".repeat(40));
@@ -570,6 +605,16 @@ function formatDepsPretty(issueId, blockedBy, blocking) {
570
605
  lines.push(` \u25CB ${issue.id} - ${truncate(issue.title, 30)}`);
571
606
  }
572
607
  }
608
+ lines.push("");
609
+ lines.push("Related:");
610
+ if (related.length === 0) {
611
+ lines.push(" (none)");
612
+ } else {
613
+ for (const issue of related) {
614
+ const status = issue.status === "closed" ? "\u2713" : "\u25CB";
615
+ lines.push(` ${status} ${issue.id} - ${truncate(issue.title, 30)}`);
616
+ }
617
+ }
573
618
  return lines.join("\n");
574
619
  }
575
620
  function formatError(error) {
@@ -832,6 +877,12 @@ function updateCommand(program2) {
832
877
  results.push({ id, success: false, error: `Issue not found: ${id}` });
833
878
  continue;
834
879
  }
880
+ if (data.status === "in_progress" && hasOpenBlockersById(resolvedId)) {
881
+ const blockers = getOpenBlockers(resolvedId);
882
+ const blockerIds = blockers.map((b) => b.id).join(", ");
883
+ results.push({ id: resolvedId, success: false, error: `Cannot set to in_progress - blocked by: ${blockerIds}` });
884
+ continue;
885
+ }
835
886
  const event = {
836
887
  type: "update",
837
888
  issueId: resolvedId,
@@ -914,6 +965,25 @@ function closeCommand(program2) {
914
965
  };
915
966
  appendEvent(commentEvent, pebbleDir);
916
967
  }
968
+ const pendingVerifications = getVerifications(resolvedId).filter((v) => v.status !== "closed");
969
+ if (pendingVerifications.length > 0) {
970
+ const updateEvent = {
971
+ type: "update",
972
+ issueId: resolvedId,
973
+ timestamp,
974
+ data: {
975
+ status: "pending_verification"
976
+ }
977
+ };
978
+ appendEvent(updateEvent, pebbleDir);
979
+ results.push({
980
+ id: resolvedId,
981
+ success: true,
982
+ status: "pending_verification",
983
+ pendingVerifications: pendingVerifications.map((v) => ({ id: v.id, title: v.title }))
984
+ });
985
+ continue;
986
+ }
917
987
  const closeEvent = {
918
988
  type: "close",
919
989
  issueId: resolvedId,
@@ -927,6 +997,7 @@ function closeCommand(program2) {
927
997
  results.push({
928
998
  id: resolvedId,
929
999
  success: true,
1000
+ status: "closed",
930
1001
  unblocked: unblocked.length > 0 ? unblocked.map((i) => ({ id: i.id, title: i.title })) : void 0
931
1002
  });
932
1003
  } catch (error) {
@@ -937,18 +1008,29 @@ function closeCommand(program2) {
937
1008
  const result = results[0];
938
1009
  if (result.success) {
939
1010
  if (pretty) {
940
- console.log(`\u2713 ${result.id}`);
941
- if (result.unblocked && result.unblocked.length > 0) {
1011
+ if (result.status === "pending_verification") {
1012
+ console.log(`\u23F3 ${result.id} \u2192 pending_verification`);
942
1013
  console.log(`
1014
+ Pending verifications:`);
1015
+ for (const v of result.pendingVerifications || []) {
1016
+ console.log(` \u2022 ${v.id} - ${v.title}`);
1017
+ }
1018
+ } else {
1019
+ console.log(`\u2713 ${result.id}`);
1020
+ if (result.unblocked && result.unblocked.length > 0) {
1021
+ console.log(`
943
1022
  Unblocked:`);
944
- for (const u of result.unblocked) {
945
- console.log(` \u2192 ${u.id} - ${u.title}`);
1023
+ for (const u of result.unblocked) {
1024
+ console.log(` \u2192 ${u.id} - ${u.title}`);
1025
+ }
946
1026
  }
947
1027
  }
948
1028
  } else {
949
1029
  console.log(formatJson({
950
1030
  id: result.id,
951
1031
  success: true,
1032
+ status: result.status,
1033
+ ...result.pendingVerifications && { pendingVerifications: result.pendingVerifications },
952
1034
  ...result.unblocked && { unblocked: result.unblocked }
953
1035
  }));
954
1036
  }
@@ -959,10 +1041,17 @@ Unblocked:`);
959
1041
  if (pretty) {
960
1042
  for (const result of results) {
961
1043
  if (result.success) {
962
- console.log(`\u2713 ${result.id}`);
963
- if (result.unblocked && result.unblocked.length > 0) {
964
- for (const u of result.unblocked) {
965
- console.log(` \u2192 ${u.id} - ${u.title}`);
1044
+ if (result.status === "pending_verification") {
1045
+ console.log(`\u23F3 ${result.id} \u2192 pending_verification`);
1046
+ for (const v of result.pendingVerifications || []) {
1047
+ console.log(` \u2022 ${v.id} - ${v.title}`);
1048
+ }
1049
+ } else {
1050
+ console.log(`\u2713 ${result.id}`);
1051
+ if (result.unblocked && result.unblocked.length > 0) {
1052
+ for (const u of result.unblocked) {
1053
+ console.log(` \u2192 ${u.id} - ${u.title}`);
1054
+ }
966
1055
  }
967
1056
  }
968
1057
  } else {
@@ -973,7 +1062,9 @@ Unblocked:`);
973
1062
  console.log(formatJson(results.map((r) => ({
974
1063
  id: r.id,
975
1064
  success: r.success,
1065
+ status: r.status,
976
1066
  ...r.error && { error: r.error },
1067
+ ...r.pendingVerifications && { pendingVerifications: r.pendingVerifications },
977
1068
  ...r.unblocked && { unblocked: r.unblocked }
978
1069
  }))));
979
1070
  }
@@ -1041,6 +1132,12 @@ function claimCommand(program2) {
1041
1132
  results.push({ id: resolvedId, success: false, error: `Cannot claim closed issue: ${resolvedId}` });
1042
1133
  continue;
1043
1134
  }
1135
+ if (hasOpenBlockersById(resolvedId)) {
1136
+ const blockers = getOpenBlockers(resolvedId);
1137
+ const blockerIds = blockers.map((b) => b.id).join(", ");
1138
+ results.push({ id: resolvedId, success: false, error: `Cannot claim blocked issue. Blocked by: ${blockerIds}` });
1139
+ continue;
1140
+ }
1044
1141
  const event = {
1045
1142
  type: "update",
1046
1143
  issueId: resolvedId,
@@ -1264,6 +1361,99 @@ function depCommand(program2) {
1264
1361
  outputError(error, pretty);
1265
1362
  }
1266
1363
  });
1364
+ dep.command("relate <id1> <id2>").description("Add a bidirectional related link between two issues").action(async (id1, id2) => {
1365
+ const pretty = program2.opts().pretty ?? false;
1366
+ try {
1367
+ const pebbleDir = getOrCreatePebbleDir();
1368
+ const resolvedId1 = resolveId(id1);
1369
+ const resolvedId2 = resolveId(id2);
1370
+ const issue1 = getIssue(resolvedId1);
1371
+ if (!issue1) {
1372
+ throw new Error(`Issue not found: ${id1}`);
1373
+ }
1374
+ const issue2 = getIssue(resolvedId2);
1375
+ if (!issue2) {
1376
+ throw new Error(`Issue not found: ${id2}`);
1377
+ }
1378
+ if (resolvedId1 === resolvedId2) {
1379
+ throw new Error("Cannot relate issue to itself");
1380
+ }
1381
+ if (issue1.relatedTo.includes(resolvedId2)) {
1382
+ throw new Error(`Issues are already related: ${resolvedId1} \u2194 ${resolvedId2}`);
1383
+ }
1384
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1385
+ const event1 = {
1386
+ type: "update",
1387
+ issueId: resolvedId1,
1388
+ timestamp,
1389
+ data: {
1390
+ relatedTo: [...issue1.relatedTo, resolvedId2]
1391
+ }
1392
+ };
1393
+ const event2 = {
1394
+ type: "update",
1395
+ issueId: resolvedId2,
1396
+ timestamp,
1397
+ data: {
1398
+ relatedTo: [...issue2.relatedTo, resolvedId1]
1399
+ }
1400
+ };
1401
+ appendEvent(event1, pebbleDir);
1402
+ appendEvent(event2, pebbleDir);
1403
+ if (pretty) {
1404
+ console.log(`\u2713 ${resolvedId1} \u2194 ${resolvedId2}`);
1405
+ } else {
1406
+ console.log(formatJson({ id1: resolvedId1, id2: resolvedId2, related: true }));
1407
+ }
1408
+ } catch (error) {
1409
+ outputError(error, pretty);
1410
+ }
1411
+ });
1412
+ dep.command("unrelate <id1> <id2>").description("Remove a bidirectional related link between two issues").action(async (id1, id2) => {
1413
+ const pretty = program2.opts().pretty ?? false;
1414
+ try {
1415
+ const pebbleDir = getOrCreatePebbleDir();
1416
+ const resolvedId1 = resolveId(id1);
1417
+ const resolvedId2 = resolveId(id2);
1418
+ const issue1 = getIssue(resolvedId1);
1419
+ if (!issue1) {
1420
+ throw new Error(`Issue not found: ${id1}`);
1421
+ }
1422
+ const issue2 = getIssue(resolvedId2);
1423
+ if (!issue2) {
1424
+ throw new Error(`Issue not found: ${id2}`);
1425
+ }
1426
+ if (!issue1.relatedTo.includes(resolvedId2)) {
1427
+ throw new Error(`Issues are not related: ${resolvedId1} \u2194 ${resolvedId2}`);
1428
+ }
1429
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1430
+ const event1 = {
1431
+ type: "update",
1432
+ issueId: resolvedId1,
1433
+ timestamp,
1434
+ data: {
1435
+ relatedTo: issue1.relatedTo.filter((id) => id !== resolvedId2)
1436
+ }
1437
+ };
1438
+ const event2 = {
1439
+ type: "update",
1440
+ issueId: resolvedId2,
1441
+ timestamp,
1442
+ data: {
1443
+ relatedTo: issue2.relatedTo.filter((id) => id !== resolvedId1)
1444
+ }
1445
+ };
1446
+ appendEvent(event1, pebbleDir);
1447
+ appendEvent(event2, pebbleDir);
1448
+ if (pretty) {
1449
+ console.log(`\u2713 ${resolvedId1} \u21AE ${resolvedId2}`);
1450
+ } else {
1451
+ console.log(formatJson({ id1: resolvedId1, id2: resolvedId2, related: false }));
1452
+ }
1453
+ } catch (error) {
1454
+ outputError(error, pretty);
1455
+ }
1456
+ });
1267
1457
  dep.command("list <id>").description("List dependencies for an issue").action(async (id) => {
1268
1458
  const pretty = program2.opts().pretty ?? false;
1269
1459
  try {
@@ -1274,13 +1464,15 @@ function depCommand(program2) {
1274
1464
  }
1275
1465
  const blockedBy = getBlockers(resolvedId);
1276
1466
  const blocking = getBlocking(resolvedId);
1467
+ const related = getRelated(resolvedId);
1277
1468
  if (pretty) {
1278
- console.log(formatDepsPretty(resolvedId, blockedBy, blocking));
1469
+ console.log(formatDepsPretty(resolvedId, blockedBy, blocking, related));
1279
1470
  } else {
1280
1471
  console.log(formatJson({
1281
1472
  issueId: resolvedId,
1282
1473
  blockedBy: blockedBy.map((i) => ({ id: i.id, title: i.title, status: i.status })),
1283
- blocking: blocking.map((i) => ({ id: i.id, title: i.title, status: i.status }))
1474
+ blocking: blocking.map((i) => ({ id: i.id, title: i.title, status: i.status })),
1475
+ related: related.map((i) => ({ id: i.id, title: i.title, status: i.status }))
1284
1476
  }));
1285
1477
  }
1286
1478
  } catch (error) {
@@ -1880,9 +2072,26 @@ data: ${message}
1880
2072
  results.push({ id: issueId, success: false, error: "Cannot close epic with open children" });
1881
2073
  continue;
1882
2074
  }
2075
+ const pendingVerifications = getVerifications(issueId).filter((v) => v.status !== "closed");
2076
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2077
+ if (pendingVerifications.length > 0) {
2078
+ const updateEvent = {
2079
+ type: "update",
2080
+ issueId,
2081
+ timestamp,
2082
+ data: { status: "pending_verification" }
2083
+ };
2084
+ appendEvent(updateEvent, pebbleDir);
2085
+ results.push({
2086
+ id: issueId,
2087
+ success: true,
2088
+ error: `Moved to pending_verification (${pendingVerifications.length} verification(s) pending)`
2089
+ });
2090
+ continue;
2091
+ }
1883
2092
  const event = {
1884
2093
  issueId,
1885
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2094
+ timestamp,
1886
2095
  type: "close",
1887
2096
  data: { reason: "Bulk close" }
1888
2097
  };
@@ -1910,7 +2119,7 @@ data: ${message}
1910
2119
  return;
1911
2120
  }
1912
2121
  if (updates.status) {
1913
- const validStatuses = ["open", "in_progress", "blocked"];
2122
+ const validStatuses = ["open", "in_progress", "blocked", "pending_verification"];
1914
2123
  if (!validStatuses.includes(updates.status)) {
1915
2124
  res.status(400).json({
1916
2125
  error: `Invalid status: ${updates.status}. Use close endpoint to close issues.`
@@ -1978,7 +2187,7 @@ data: ${message}
1978
2187
  issue = localIssue;
1979
2188
  targetFile = path2.join(pebbleDir, "issues.jsonl");
1980
2189
  }
1981
- const { title, type, priority, status, description, parent } = req.body;
2190
+ const { title, type, priority, status, description, parent, relatedTo } = req.body;
1982
2191
  const updates = {};
1983
2192
  if (title !== void 0) {
1984
2193
  if (typeof title !== "string" || title.trim() === "") {
@@ -2037,6 +2246,28 @@ data: ${message}
2037
2246
  }
2038
2247
  updates.parent = parent;
2039
2248
  }
2249
+ if (relatedTo !== void 0) {
2250
+ if (!Array.isArray(relatedTo)) {
2251
+ res.status(400).json({ error: "relatedTo must be an array" });
2252
+ return;
2253
+ }
2254
+ for (const relatedId of relatedTo) {
2255
+ if (isMultiWorktree()) {
2256
+ const found = findIssueInSources(relatedId, issueFiles);
2257
+ if (!found) {
2258
+ res.status(400).json({ error: `Related issue not found: ${relatedId}` });
2259
+ return;
2260
+ }
2261
+ } else {
2262
+ const relatedIssue = getIssue(relatedId);
2263
+ if (!relatedIssue) {
2264
+ res.status(400).json({ error: `Related issue not found: ${relatedId}` });
2265
+ return;
2266
+ }
2267
+ }
2268
+ }
2269
+ updates.relatedTo = relatedTo;
2270
+ }
2040
2271
  if (Object.keys(updates).length === 0) {
2041
2272
  res.status(400).json({ error: "No valid updates provided" });
2042
2273
  return;
@@ -2094,6 +2325,30 @@ data: ${message}
2094
2325
  }
2095
2326
  const { reason } = req.body;
2096
2327
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2328
+ let pendingVerifications = [];
2329
+ if (isMultiWorktree()) {
2330
+ const allIssues = mergeIssuesFromFiles(issueFiles);
2331
+ pendingVerifications = allIssues.filter(
2332
+ (i) => i.verifies === issueId && i.status !== "closed"
2333
+ );
2334
+ } else {
2335
+ pendingVerifications = getVerifications(issueId).filter((v) => v.status !== "closed");
2336
+ }
2337
+ if (pendingVerifications.length > 0) {
2338
+ const updateEvent = {
2339
+ type: "update",
2340
+ issueId,
2341
+ timestamp,
2342
+ data: { status: "pending_verification" }
2343
+ };
2344
+ appendEventToFile(updateEvent, targetFile);
2345
+ const updatedIssue = { ...issue, status: "pending_verification", updatedAt: timestamp };
2346
+ res.json({
2347
+ ...updatedIssue,
2348
+ _pendingVerifications: pendingVerifications.map((v) => ({ id: v.id, title: v.title }))
2349
+ });
2350
+ return;
2351
+ }
2097
2352
  const event = {
2098
2353
  type: "close",
2099
2354
  issueId,