@markmdev/pebble 0.1.8 → 0.1.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.
package/dist/cli/index.js CHANGED
@@ -492,7 +492,24 @@ function getOpenBlockers(issueId) {
492
492
  return issue.blockedBy.map((id) => state.get(id)).filter((i) => i !== void 0 && i.status !== "closed");
493
493
  }
494
494
 
495
+ // src/shared/time.ts
496
+ import { formatDistanceToNow, parseISO } from "date-fns";
497
+ function formatRelativeTime(isoString) {
498
+ try {
499
+ const date = parseISO(isoString);
500
+ return formatDistanceToNow(date, { addSuffix: true });
501
+ } catch {
502
+ return isoString;
503
+ }
504
+ }
505
+
495
506
  // src/cli/lib/output.ts
507
+ function formatLimitMessage(info) {
508
+ if (!info.limited) return "";
509
+ return `
510
+ ---
511
+ Showing ${info.shown} of ${info.total} issues. Use --all or --limit <n> to see more.`;
512
+ }
496
513
  function formatJson(data) {
497
514
  return JSON.stringify(data, null, 2);
498
515
  }
@@ -512,7 +529,7 @@ function truncate(str, maxLength) {
512
529
  function pad(str, width) {
513
530
  return str.padEnd(width);
514
531
  }
515
- function formatIssuePrettyWithBlocking(issue, blocking) {
532
+ function formatIssueDetailPretty(issue, ctx) {
516
533
  const lines = [];
517
534
  lines.push(`${issue.id} - ${issue.title}`);
518
535
  lines.push("\u2500".repeat(60));
@@ -527,13 +544,40 @@ function formatIssuePrettyWithBlocking(issue, blocking) {
527
544
  lines.push("Description:");
528
545
  lines.push(issue.description);
529
546
  }
547
+ if (ctx.children.length > 0) {
548
+ const closedChildren = ctx.children.filter((c) => c.status === "closed");
549
+ const pendingChildren = ctx.children.filter((c) => c.status === "pending_verification");
550
+ const pendingStr = pendingChildren.length > 0 ? ` (${pendingChildren.length} pending verification)` : "";
551
+ lines.push("");
552
+ lines.push(`Children (${closedChildren.length}/${ctx.children.length} done${pendingStr}):`);
553
+ for (const child of ctx.children) {
554
+ const statusIcon = child.status === "closed" ? "\u2713" : child.status === "in_progress" ? "\u25B6" : "\u25CB";
555
+ lines.push(` ${statusIcon} ${child.id} - ${child.title} [${child.status}]`);
556
+ }
557
+ }
530
558
  if (issue.blockedBy.length > 0) {
531
559
  lines.push("");
532
560
  lines.push(`Blocked by: ${issue.blockedBy.join(", ")}`);
533
561
  }
534
- if (blocking.length > 0) {
562
+ if (ctx.blocking.length > 0) {
563
+ lines.push("");
564
+ lines.push(`Blocking: ${ctx.blocking.map((i) => i.id).join(", ")}`);
565
+ }
566
+ if (ctx.verifications.length > 0) {
567
+ const closedVerifications = ctx.verifications.filter((v) => v.status === "closed");
535
568
  lines.push("");
536
- lines.push(`Blocking: ${blocking.map((i) => i.id).join(", ")}`);
569
+ lines.push(`Verifications (${closedVerifications.length}/${ctx.verifications.length} done):`);
570
+ for (const v of ctx.verifications) {
571
+ const statusIcon = v.status === "closed" ? "\u2713" : "\u25CB";
572
+ lines.push(` ${statusIcon} ${v.id} - ${v.title} [${v.status}]`);
573
+ }
574
+ } else if (issue.status === "pending_verification") {
575
+ lines.push("");
576
+ lines.push("Verifications: None found (status may be stale)");
577
+ }
578
+ if (ctx.related.length > 0) {
579
+ lines.push("");
580
+ lines.push(`Related: ${ctx.related.map((r) => r.id).join(", ")}`);
537
581
  }
538
582
  if (issue.comments.length > 0) {
539
583
  lines.push("");
@@ -549,6 +593,20 @@ function formatIssuePrettyWithBlocking(issue, blocking) {
549
593
  lines.push(`Updated: ${new Date(issue.updatedAt).toLocaleString()}`);
550
594
  return lines.join("\n");
551
595
  }
596
+ function outputIssueDetail(issue, ctx, pretty) {
597
+ if (pretty) {
598
+ console.log(formatIssueDetailPretty(issue, ctx));
599
+ } else {
600
+ const output = {
601
+ ...issue,
602
+ blocking: ctx.blocking.map((i) => i.id),
603
+ children: ctx.children.map((i) => ({ id: i.id, title: i.title, status: i.status })),
604
+ verifications: ctx.verifications.map((i) => ({ id: i.id, title: i.title, status: i.status })),
605
+ related: ctx.related.map((i) => i.id)
606
+ };
607
+ console.log(formatJson(output));
608
+ }
609
+ }
552
610
  function formatIssueListPretty(issues) {
553
611
  if (issues.length === 0) {
554
612
  return "No issues found.";
@@ -625,17 +683,6 @@ function formatErrorPretty(error) {
625
683
  const message = error instanceof Error ? error.message : error;
626
684
  return `Error: ${message}`;
627
685
  }
628
- function outputIssueWithBlocking(issue, blocking, pretty) {
629
- if (pretty) {
630
- console.log(formatIssuePrettyWithBlocking(issue, blocking));
631
- } else {
632
- const output = {
633
- ...issue,
634
- blocking: blocking.map((i) => i.id)
635
- };
636
- console.log(formatJson(output));
637
- }
638
- }
639
686
  function outputMutationSuccess(id, pretty) {
640
687
  if (pretty) {
641
688
  console.log(`\u2713 ${id}`);
@@ -643,11 +690,94 @@ function outputMutationSuccess(id, pretty) {
643
690
  console.log(JSON.stringify({ id, success: true }));
644
691
  }
645
692
  }
646
- function outputIssueList(issues, pretty) {
693
+ function outputIssueList(issues, pretty, limitInfo) {
647
694
  if (pretty) {
648
695
  console.log(formatIssueListPretty(issues));
696
+ if (limitInfo?.limited) {
697
+ console.log(formatLimitMessage(limitInfo));
698
+ }
649
699
  } else {
650
- console.log(formatJson(issues));
700
+ if (limitInfo?.limited) {
701
+ console.log(formatJson({ issues, _meta: limitInfo }));
702
+ } else {
703
+ console.log(formatJson(issues));
704
+ }
705
+ }
706
+ }
707
+ function buildIssueTree(issues) {
708
+ const issueMap = /* @__PURE__ */ new Map();
709
+ for (const issue of issues) {
710
+ issueMap.set(issue.id, issue);
711
+ }
712
+ const childIds = /* @__PURE__ */ new Set();
713
+ for (const issue of issues) {
714
+ if (issue.parent && issueMap.has(issue.parent)) {
715
+ childIds.add(issue.id);
716
+ }
717
+ }
718
+ const buildNode = (issue) => {
719
+ const children = issues.filter((i) => i.parent === issue.id).map(buildNode);
720
+ return {
721
+ id: issue.id,
722
+ title: issue.title,
723
+ type: issue.type,
724
+ priority: issue.priority,
725
+ status: issue.status,
726
+ createdAt: issue.createdAt,
727
+ childrenCount: children.length,
728
+ ...children.length > 0 && { children }
729
+ };
730
+ };
731
+ const roots = issues.filter((i) => !childIds.has(i.id));
732
+ return roots.map(buildNode);
733
+ }
734
+ function formatIssueTreePretty(nodes, sectionHeader) {
735
+ if (nodes.length === 0) {
736
+ return "No issues found.";
737
+ }
738
+ const lines = [];
739
+ if (sectionHeader) {
740
+ lines.push(`## ${sectionHeader}`);
741
+ lines.push("");
742
+ }
743
+ const countAll = (node) => {
744
+ const children = node.children ?? [];
745
+ return 1 + children.reduce((sum, child) => sum + countAll(child), 0);
746
+ };
747
+ const totalCount = nodes.reduce((sum, node) => sum + countAll(node), 0);
748
+ const formatNode = (node, prefix, isLast, isRoot) => {
749
+ const connector = isRoot ? "" : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
750
+ const statusIcon = node.status === "closed" ? "\u2713" : node.status === "in_progress" ? "\u25B6" : node.status === "pending_verification" ? "\u23F3" : "\u25CB";
751
+ const statusText = STATUS_LABELS[node.status].toLowerCase();
752
+ const relativeTime = formatRelativeTime(node.createdAt);
753
+ lines.push(`${prefix}${connector}${statusIcon} ${node.id}: ${node.title} [${node.type}] P${node.priority} ${statusText} ${relativeTime}`);
754
+ const children = node.children ?? [];
755
+ const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "\u2502 ");
756
+ children.forEach((child, index) => {
757
+ const childIsLast = index === children.length - 1;
758
+ formatNode(child, childPrefix, childIsLast, false);
759
+ });
760
+ };
761
+ nodes.forEach((node, index) => {
762
+ formatNode(node, "", index === nodes.length - 1, true);
763
+ });
764
+ lines.push("");
765
+ lines.push(`Total: ${totalCount} issue(s)`);
766
+ return lines.join("\n");
767
+ }
768
+ function outputIssueTree(issues, pretty, sectionHeader, limitInfo) {
769
+ const tree = buildIssueTree(issues);
770
+ if (pretty) {
771
+ console.log(formatIssueTreePretty(tree, sectionHeader));
772
+ if (limitInfo?.limited) {
773
+ console.log(formatLimitMessage(limitInfo));
774
+ }
775
+ } else {
776
+ if (limitInfo?.limited) {
777
+ console.log(formatJson({ issues: tree, _meta: limitInfo }));
778
+ } else {
779
+ console.log(formatJson(tree));
780
+ }
651
781
  }
652
782
  }
653
783
  function outputError(error, pretty) {
@@ -658,42 +788,55 @@ function outputError(error, pretty) {
658
788
  }
659
789
  process.exit(1);
660
790
  }
661
- function formatIssueListVerbose(issues) {
791
+ function formatIssueListVerbose(issues, sectionHeader) {
662
792
  if (issues.length === 0) {
663
793
  return "No issues found.";
664
794
  }
665
795
  const lines = [];
796
+ if (sectionHeader) {
797
+ lines.push(`## ${sectionHeader} (${issues.length})`);
798
+ lines.push("");
799
+ }
666
800
  for (const info of issues) {
667
- const { issue, blocking, children, verifications, blockers } = info;
668
- lines.push(`${issue.id} - ${issue.title}`);
669
- lines.push("\u2500".repeat(60));
670
- lines.push(` Type: ${formatType(issue.type)}`);
671
- lines.push(` Priority: P${issue.priority}`);
672
- lines.push(` Status: ${issue.status}`);
673
- lines.push(` Parent: ${issue.parent || "-"}`);
674
- lines.push(` Children: ${issue.type === "epic" ? children : "-"}`);
675
- lines.push(` Blocking: ${blocking.length > 0 ? blocking.join(", ") : "[]"}`);
676
- lines.push(` Verifications: ${verifications}`);
801
+ const { issue, blocking, children, verifications, blockers, parent } = info;
802
+ lines.push(`${issue.id}: ${issue.title}`);
803
+ lines.push(` Type: ${formatType(issue.type)} | Priority: P${issue.priority} | Created: ${formatRelativeTime(issue.createdAt)}`);
804
+ if (parent) {
805
+ lines.push(` Epic: ${parent.id} (${parent.title})`);
806
+ }
807
+ if (blocking.length > 0) {
808
+ lines.push(` Blocking: ${blocking.join(", ")}`);
809
+ }
677
810
  if (blockers && blockers.length > 0) {
678
- lines.push(` Blocked by: ${blockers.join(", ")}`);
811
+ lines.push(` Blocked by: ${blockers.join(", ")}`);
812
+ }
813
+ if (issue.type === "epic" && children > 0) {
814
+ lines.push(` Children: ${children} | Verifications: ${verifications}`);
679
815
  }
680
816
  lines.push("");
681
817
  }
682
- lines.push(`Total: ${issues.length} issue(s)`);
683
818
  return lines.join("\n");
684
819
  }
685
- function outputIssueListVerbose(issues, pretty) {
820
+ function outputIssueListVerbose(issues, pretty, sectionHeader, limitInfo) {
686
821
  if (pretty) {
687
- console.log(formatIssueListVerbose(issues));
822
+ console.log(formatIssueListVerbose(issues, sectionHeader));
823
+ if (limitInfo?.limited) {
824
+ console.log(formatLimitMessage(limitInfo));
825
+ }
688
826
  } else {
689
- const output = issues.map(({ issue, blocking, children, verifications, blockers }) => ({
827
+ const output = issues.map(({ issue, blocking, children, verifications, blockers, parent }) => ({
690
828
  ...issue,
691
829
  blocking,
692
830
  childrenCount: issue.type === "epic" ? children : void 0,
693
831
  verificationsCount: verifications,
694
- ...blockers && { openBlockers: blockers }
832
+ ...blockers && { openBlockers: blockers },
833
+ ...parent && { parentInfo: parent }
695
834
  }));
696
- console.log(formatJson(output));
835
+ if (limitInfo?.limited) {
836
+ console.log(formatJson({ issues: output, _meta: limitInfo }));
837
+ } else {
838
+ console.log(formatJson(output));
839
+ }
697
840
  }
698
841
  }
699
842
 
@@ -1015,6 +1158,8 @@ Pending verifications:`);
1015
1158
  for (const v of result.pendingVerifications || []) {
1016
1159
  console.log(` \u2022 ${v.id} - ${v.title}`);
1017
1160
  }
1161
+ console.log(`
1162
+ Run: pb verifications ${result.id}`);
1018
1163
  } else {
1019
1164
  console.log(`\u2713 ${result.id}`);
1020
1165
  if (result.unblocked && result.unblocked.length > 0) {
@@ -1031,6 +1176,7 @@ Unblocked:`);
1031
1176
  success: true,
1032
1177
  status: result.status,
1033
1178
  ...result.pendingVerifications && { pendingVerifications: result.pendingVerifications },
1179
+ ...result.pendingVerifications && { hint: `pb verifications ${result.id}` },
1034
1180
  ...result.unblocked && { unblocked: result.unblocked }
1035
1181
  }));
1036
1182
  }
@@ -1046,6 +1192,7 @@ Unblocked:`);
1046
1192
  for (const v of result.pendingVerifications || []) {
1047
1193
  console.log(` \u2022 ${v.id} - ${v.title}`);
1048
1194
  }
1195
+ console.log(` Run: pb verifications ${result.id}`);
1049
1196
  } else {
1050
1197
  console.log(`\u2713 ${result.id}`);
1051
1198
  if (result.unblocked && result.unblocked.length > 0) {
@@ -1065,6 +1212,7 @@ Unblocked:`);
1065
1212
  status: r.status,
1066
1213
  ...r.error && { error: r.error },
1067
1214
  ...r.pendingVerifications && { pendingVerifications: r.pendingVerifications },
1215
+ ...r.pendingVerifications && { hint: `pb verifications ${r.id}` },
1068
1216
  ...r.unblocked && { unblocked: r.unblocked }
1069
1217
  }))));
1070
1218
  }
@@ -1184,7 +1332,7 @@ function claimCommand(program2) {
1184
1332
 
1185
1333
  // src/cli/commands/list.ts
1186
1334
  function listCommand(program2) {
1187
- program2.command("list").description("List issues").option("--status <status>", "Filter by status").option("-t, --type <type>", "Filter by type").option("--priority <priority>", "Filter by priority").option("--parent <id>", "Filter by parent epic").action(async (options) => {
1335
+ program2.command("list").description("List issues").option("--status <status>", "Filter by status").option("-t, --type <type>", "Filter by type").option("--priority <priority>", "Filter by priority").option("--parent <id>", "Filter by parent epic").option("-v, --verbose", "Show expanded details (parent, children, blocking, verifications)").option("--flat", "Show flat list instead of hierarchical tree").option("--limit <n>", "Max issues to return (default: 30)").option("--all", "Show all issues (no limit)").action(async (options) => {
1188
1336
  const pretty = program2.opts().pretty ?? false;
1189
1337
  try {
1190
1338
  getOrCreatePebbleDir();
@@ -1213,8 +1361,48 @@ function listCommand(program2) {
1213
1361
  if (options.parent !== void 0) {
1214
1362
  filters.parent = resolveId(options.parent);
1215
1363
  }
1216
- const issues = getIssues(filters);
1217
- outputIssueList(issues, pretty);
1364
+ let issues = getIssues(filters);
1365
+ issues.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
1366
+ const total = issues.length;
1367
+ const limit = options.all ? 0 : options.limit ? parseInt(options.limit, 10) : 30;
1368
+ if (limit > 0 && issues.length > limit) {
1369
+ issues = issues.slice(0, limit);
1370
+ }
1371
+ const limitInfo = {
1372
+ total,
1373
+ shown: issues.length,
1374
+ limited: limit > 0 && total > limit
1375
+ };
1376
+ if (options.verbose) {
1377
+ const verboseIssues = issues.map((issue) => {
1378
+ const info = {
1379
+ issue,
1380
+ blocking: getBlocking(issue.id).map((i) => i.id),
1381
+ children: getChildren(issue.id).length,
1382
+ verifications: getVerifications(issue.id).length
1383
+ };
1384
+ if (issue.parent) {
1385
+ const parentIssue = getIssue(issue.parent);
1386
+ if (parentIssue) {
1387
+ info.parent = { id: parentIssue.id, title: parentIssue.title };
1388
+ }
1389
+ }
1390
+ return info;
1391
+ });
1392
+ let sectionHeader = "Issues";
1393
+ if (filters.status) {
1394
+ sectionHeader = `${STATUS_LABELS[filters.status]} Issues`;
1395
+ }
1396
+ outputIssueListVerbose(verboseIssues, pretty, sectionHeader, limitInfo);
1397
+ } else if (options.flat) {
1398
+ outputIssueList(issues, pretty, limitInfo);
1399
+ } else {
1400
+ let sectionHeader = "Issues";
1401
+ if (filters.status) {
1402
+ sectionHeader = `${STATUS_LABELS[filters.status]} Issues`;
1403
+ }
1404
+ outputIssueTree(issues, pretty, sectionHeader, limitInfo);
1405
+ }
1218
1406
  } catch (error) {
1219
1407
  outputError(error, pretty);
1220
1408
  }
@@ -1233,7 +1421,10 @@ function showCommand(program2) {
1233
1421
  throw new Error(`Issue not found: ${id}`);
1234
1422
  }
1235
1423
  const blocking = getBlocking(resolvedId);
1236
- outputIssueWithBlocking(issue, blocking, pretty);
1424
+ const children = issue.type === "epic" ? getChildren(resolvedId) : [];
1425
+ const verifications = getVerifications(resolvedId);
1426
+ const related = getRelated(resolvedId);
1427
+ outputIssueDetail(issue, { blocking, children, verifications, related }, pretty);
1237
1428
  } catch (error) {
1238
1429
  outputError(error, pretty);
1239
1430
  }
@@ -1242,21 +1433,48 @@ function showCommand(program2) {
1242
1433
 
1243
1434
  // src/cli/commands/ready.ts
1244
1435
  function readyCommand(program2) {
1245
- program2.command("ready").description("Show issues ready for work (no open blockers)").option("-v, --verbose", "Show expanded details (parent, children, blocking, verifications)").action(async (options) => {
1436
+ program2.command("ready").description("Show issues ready for work (no open blockers)").option("-v, --verbose", "Show expanded details (parent, children, blocking, verifications)").option("-t, --type <type>", "Filter by type: task, bug, epic, verification").option("--limit <n>", "Max issues to return (default: 30)").option("--all", "Show all issues (no limit)").action(async (options) => {
1246
1437
  const pretty = program2.opts().pretty ?? false;
1247
1438
  try {
1248
1439
  getOrCreatePebbleDir();
1249
- const issues = getReady();
1440
+ let issues = getReady();
1441
+ if (options.type) {
1442
+ const typeFilter = options.type.toLowerCase();
1443
+ if (!ISSUE_TYPES.includes(typeFilter)) {
1444
+ throw new Error(`Invalid type: ${options.type}. Must be one of: ${ISSUE_TYPES.join(", ")}`);
1445
+ }
1446
+ issues = issues.filter((i) => i.type === typeFilter);
1447
+ }
1448
+ issues.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
1449
+ const total = issues.length;
1450
+ const limit = options.all ? 0 : options.limit ? parseInt(options.limit, 10) : 30;
1451
+ if (limit > 0 && issues.length > limit) {
1452
+ issues = issues.slice(0, limit);
1453
+ }
1454
+ const limitInfo = {
1455
+ total,
1456
+ shown: issues.length,
1457
+ limited: limit > 0 && total > limit
1458
+ };
1250
1459
  if (options.verbose) {
1251
- const verboseIssues = issues.map((issue) => ({
1252
- issue,
1253
- blocking: getBlocking(issue.id).map((i) => i.id),
1254
- children: getChildren(issue.id).length,
1255
- verifications: getVerifications(issue.id).length
1256
- }));
1257
- outputIssueListVerbose(verboseIssues, pretty);
1460
+ const verboseIssues = issues.map((issue) => {
1461
+ const info = {
1462
+ issue,
1463
+ blocking: getBlocking(issue.id).map((i) => i.id),
1464
+ children: getChildren(issue.id).length,
1465
+ verifications: getVerifications(issue.id).length
1466
+ };
1467
+ if (issue.parent) {
1468
+ const parentIssue = getIssue(issue.parent);
1469
+ if (parentIssue) {
1470
+ info.parent = { id: parentIssue.id, title: parentIssue.title };
1471
+ }
1472
+ }
1473
+ return info;
1474
+ });
1475
+ outputIssueListVerbose(verboseIssues, pretty, "Ready Issues", limitInfo);
1258
1476
  } else {
1259
- outputIssueList(issues, pretty);
1477
+ outputIssueList(issues, pretty, limitInfo);
1260
1478
  }
1261
1479
  } catch (error) {
1262
1480
  outputError(error, pretty);
@@ -1266,26 +1484,44 @@ function readyCommand(program2) {
1266
1484
 
1267
1485
  // src/cli/commands/blocked.ts
1268
1486
  function blockedCommand(program2) {
1269
- program2.command("blocked").description("Show blocked issues (have open blockers)").option("-v, --verbose", "Show expanded details including WHY each issue is blocked").action(async (options) => {
1487
+ program2.command("blocked").description("Show blocked issues (have open blockers)").option("-v, --verbose", "Show expanded details including WHY each issue is blocked").option("--limit <n>", "Max issues to return (default: 30)").option("--all", "Show all issues (no limit)").action(async (options) => {
1270
1488
  const pretty = program2.opts().pretty ?? false;
1271
1489
  try {
1272
1490
  getOrCreatePebbleDir();
1273
- const issues = getBlocked();
1491
+ let issues = getBlocked();
1492
+ issues.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
1493
+ const total = issues.length;
1494
+ const limit = options.all ? 0 : options.limit ? parseInt(options.limit, 10) : 30;
1495
+ if (limit > 0 && issues.length > limit) {
1496
+ issues = issues.slice(0, limit);
1497
+ }
1498
+ const limitInfo = {
1499
+ total,
1500
+ shown: issues.length,
1501
+ limited: limit > 0 && total > limit
1502
+ };
1274
1503
  if (options.verbose) {
1275
1504
  const verboseIssues = issues.map((issue) => {
1276
1505
  const allBlockers = getBlockers(issue.id);
1277
1506
  const openBlockers = allBlockers.filter((b) => b.status !== "closed").map((b) => b.id);
1278
- return {
1507
+ const info = {
1279
1508
  issue,
1280
1509
  blocking: getBlocking(issue.id).map((i) => i.id),
1281
1510
  children: getChildren(issue.id).length,
1282
1511
  verifications: getVerifications(issue.id).length,
1283
1512
  blockers: openBlockers
1284
1513
  };
1514
+ if (issue.parent) {
1515
+ const parentIssue = getIssue(issue.parent);
1516
+ if (parentIssue) {
1517
+ info.parent = { id: parentIssue.id, title: parentIssue.title };
1518
+ }
1519
+ }
1520
+ return info;
1285
1521
  });
1286
- outputIssueListVerbose(verboseIssues, pretty);
1522
+ outputIssueListVerbose(verboseIssues, pretty, "Blocked Issues", limitInfo);
1287
1523
  } else {
1288
- outputIssueList(issues, pretty);
1524
+ outputIssueList(issues, pretty, limitInfo);
1289
1525
  }
1290
1526
  } catch (error) {
1291
1527
  outputError(error, pretty);
@@ -1296,39 +1532,55 @@ function blockedCommand(program2) {
1296
1532
  // src/cli/commands/dep.ts
1297
1533
  function depCommand(program2) {
1298
1534
  const dep = program2.command("dep").description("Manage dependencies");
1299
- dep.command("add <id> <blockerId>").description("Add a blocking dependency").action(async (id, blockerId) => {
1535
+ dep.command("add <id> [blockerId]").description("Add a blocking dependency. Use --needs or --blocks for self-documenting syntax.").option("--needs <id>", "Issue that must be completed first (first arg needs this)").option("--blocks <id>", "Issue that this blocks (first arg blocks this)").action(async (id, blockerId, options) => {
1300
1536
  const pretty = program2.opts().pretty ?? false;
1301
1537
  try {
1538
+ if (options.needs && options.blocks) {
1539
+ throw new Error("Cannot use both --needs and --blocks");
1540
+ }
1541
+ if (blockerId && (options.needs || options.blocks)) {
1542
+ throw new Error("Cannot combine positional blockerId with --needs or --blocks");
1543
+ }
1544
+ if (!blockerId && !options.needs && !options.blocks) {
1545
+ throw new Error("Must provide blockerId, --needs <id>, or --blocks <id>");
1546
+ }
1302
1547
  const pebbleDir = getOrCreatePebbleDir();
1303
- const resolvedId = resolveId(id);
1304
- const resolvedBlockerId = resolveId(blockerId);
1305
- const issue = getIssue(resolvedId);
1548
+ let blockedId;
1549
+ let blockerIdResolved;
1550
+ if (options.blocks) {
1551
+ blockedId = resolveId(options.blocks);
1552
+ blockerIdResolved = resolveId(id);
1553
+ } else {
1554
+ blockedId = resolveId(id);
1555
+ blockerIdResolved = resolveId(options.needs || blockerId);
1556
+ }
1557
+ const issue = getIssue(blockedId);
1306
1558
  if (!issue) {
1307
- throw new Error(`Issue not found: ${id}`);
1559
+ throw new Error(`Issue not found: ${blockedId}`);
1308
1560
  }
1309
- const blocker = getIssue(resolvedBlockerId);
1561
+ const blocker = getIssue(blockerIdResolved);
1310
1562
  if (!blocker) {
1311
- throw new Error(`Blocker issue not found: ${blockerId}`);
1563
+ throw new Error(`Blocker issue not found: ${blockerIdResolved}`);
1312
1564
  }
1313
- if (resolvedId === resolvedBlockerId) {
1565
+ if (blockedId === blockerIdResolved) {
1314
1566
  throw new Error("Cannot add self as blocker");
1315
1567
  }
1316
- if (issue.blockedBy.includes(resolvedBlockerId)) {
1317
- throw new Error(`Dependency already exists: ${resolvedId} is blocked by ${resolvedBlockerId}`);
1568
+ if (issue.blockedBy.includes(blockerIdResolved)) {
1569
+ throw new Error(`Dependency already exists: ${blockedId} is blocked by ${blockerIdResolved}`);
1318
1570
  }
1319
- if (detectCycle(resolvedId, resolvedBlockerId)) {
1571
+ if (detectCycle(blockedId, blockerIdResolved)) {
1320
1572
  throw new Error(`Adding this dependency would create a cycle`);
1321
1573
  }
1322
1574
  const event = {
1323
1575
  type: "update",
1324
- issueId: resolvedId,
1576
+ issueId: blockedId,
1325
1577
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1326
1578
  data: {
1327
- blockedBy: [...issue.blockedBy, resolvedBlockerId]
1579
+ blockedBy: [...issue.blockedBy, blockerIdResolved]
1328
1580
  }
1329
1581
  };
1330
1582
  appendEvent(event, pebbleDir);
1331
- outputMutationSuccess(resolvedId, pretty);
1583
+ outputMutationSuccess(blockedId, pretty);
1332
1584
  } catch (error) {
1333
1585
  outputError(error, pretty);
1334
1586
  }
@@ -1479,7 +1731,7 @@ function depCommand(program2) {
1479
1731
  outputError(error, pretty);
1480
1732
  }
1481
1733
  });
1482
- dep.command("tree <id>").description("Show dependency tree").action(async (id) => {
1734
+ dep.command("tree <id>").description("Show issue tree (children, verifications, and full hierarchy)").action(async (id) => {
1483
1735
  const pretty = program2.opts().pretty ?? false;
1484
1736
  try {
1485
1737
  const resolvedId = resolveId(id);
@@ -1489,10 +1741,9 @@ function depCommand(program2) {
1489
1741
  }
1490
1742
  const events = readEvents();
1491
1743
  const state = computeState(events);
1492
- const visited = /* @__PURE__ */ new Set();
1493
- const tree = buildDepTree(resolvedId, visited, 0, state);
1744
+ const tree = buildIssueTree2(resolvedId, state);
1494
1745
  if (pretty) {
1495
- console.log(formatDepTree(tree));
1746
+ console.log(formatIssueTreePretty2(tree));
1496
1747
  } else {
1497
1748
  console.log(formatJson(tree));
1498
1749
  }
@@ -1501,50 +1752,94 @@ function depCommand(program2) {
1501
1752
  }
1502
1753
  });
1503
1754
  }
1504
- function buildDepTree(issueId, visited, depth, state) {
1505
- if (visited.has(issueId)) {
1506
- return null;
1507
- }
1508
- visited.add(issueId);
1755
+ function buildIssueTree2(issueId, state) {
1509
1756
  const issue = state.get(issueId);
1510
1757
  if (!issue) {
1511
1758
  return null;
1512
1759
  }
1513
- const blockedBy = [];
1514
- for (const blockerId of issue.blockedBy) {
1515
- const child = buildDepTree(blockerId, visited, depth + 1, state);
1516
- if (child) {
1517
- blockedBy.push(child);
1760
+ const buildChildren = (id, visited2) => {
1761
+ const children = [];
1762
+ for (const [, i] of state) {
1763
+ if ((i.parent === id || i.verifies === id) && !visited2.has(i.id)) {
1764
+ visited2.add(i.id);
1765
+ const nodeChildren = buildChildren(i.id, visited2);
1766
+ children.push({
1767
+ id: i.id,
1768
+ title: i.title,
1769
+ type: i.type,
1770
+ priority: i.priority,
1771
+ status: i.status,
1772
+ isTarget: i.id === issueId,
1773
+ childrenCount: nodeChildren.length,
1774
+ ...nodeChildren.length > 0 && { children: nodeChildren }
1775
+ });
1776
+ }
1518
1777
  }
1519
- }
1520
- return {
1778
+ return children;
1779
+ };
1780
+ const visited = /* @__PURE__ */ new Set([issueId]);
1781
+ const targetChildren = buildChildren(issueId, visited);
1782
+ const targetNode = {
1521
1783
  id: issue.id,
1522
1784
  title: issue.title,
1785
+ type: issue.type,
1786
+ priority: issue.priority,
1523
1787
  status: issue.status,
1524
- depth,
1525
- blockedBy
1788
+ isTarget: true,
1789
+ childrenCount: targetChildren.length,
1790
+ ...targetChildren.length > 0 && { children: targetChildren }
1526
1791
  };
1792
+ let currentNode = targetNode;
1793
+ let currentIssue = issue;
1794
+ while (currentIssue.parent) {
1795
+ const parentIssue = state.get(currentIssue.parent);
1796
+ if (!parentIssue) break;
1797
+ const siblings = [];
1798
+ for (const [, i] of state) {
1799
+ if ((i.parent === parentIssue.id || i.verifies === parentIssue.id) && i.id !== currentIssue.id) {
1800
+ siblings.push({
1801
+ id: i.id,
1802
+ title: i.title,
1803
+ type: i.type,
1804
+ priority: i.priority,
1805
+ status: i.status,
1806
+ childrenCount: 0
1807
+ });
1808
+ }
1809
+ }
1810
+ const parentNodeChildren = [currentNode, ...siblings];
1811
+ const parentNode = {
1812
+ id: parentIssue.id,
1813
+ title: parentIssue.title,
1814
+ type: parentIssue.type,
1815
+ priority: parentIssue.priority,
1816
+ status: parentIssue.status,
1817
+ childrenCount: parentNodeChildren.length,
1818
+ children: parentNodeChildren
1819
+ };
1820
+ currentNode = parentNode;
1821
+ currentIssue = parentIssue;
1822
+ }
1823
+ return currentNode;
1527
1824
  }
1528
- function formatDepTree(node, prefix = "", isRoot = true) {
1825
+ function formatIssueTreePretty2(node) {
1529
1826
  if (!node) {
1530
- return "";
1827
+ return "Issue not found.";
1531
1828
  }
1532
1829
  const lines = [];
1533
- const statusIcon = node.status === "closed" ? "\u2713" : "\u25CB";
1534
- if (isRoot) {
1535
- lines.push(`${statusIcon} ${node.id} - ${node.title}`);
1536
- }
1537
- for (let i = 0; i < node.blockedBy.length; i++) {
1538
- const child = node.blockedBy[i];
1539
- const isLast = i === node.blockedBy.length - 1;
1540
- const connector = isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
1541
- const childPrefix = prefix + (isLast ? " " : "\u2502 ");
1542
- const childStatusIcon = child.status === "closed" ? "\u2713" : "\u25CB";
1543
- lines.push(`${prefix}${connector}${childStatusIcon} ${child.id} - ${child.title}`);
1544
- if (child.blockedBy.length > 0) {
1545
- lines.push(formatDepTree(child, childPrefix, false));
1546
- }
1547
- }
1830
+ const formatNode = (n, prefix, isLast, isRoot) => {
1831
+ const connector = isRoot ? "" : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
1832
+ const statusIcon = n.status === "closed" ? "\u2713" : n.status === "in_progress" ? "\u25B6" : n.status === "pending_verification" ? "\u23F3" : "\u25CB";
1833
+ const marker = n.isTarget ? " \u25C0" : "";
1834
+ lines.push(`${prefix}${connector}${statusIcon} ${n.id}: ${n.title} [${n.type}] P${n.priority}${marker}`);
1835
+ const children = n.children ?? [];
1836
+ const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "\u2502 ");
1837
+ children.forEach((child, index) => {
1838
+ const childIsLast = index === children.length - 1;
1839
+ formatNode(child, childPrefix, childIsLast, false);
1840
+ });
1841
+ };
1842
+ formatNode(node, "", true, true);
1548
1843
  return lines.join("\n");
1549
1844
  }
1550
1845
 
@@ -2904,28 +3199,48 @@ function countChildren(epicId) {
2904
3199
  return {
2905
3200
  total: children.length,
2906
3201
  done: children.filter((c) => c.status === "closed").length,
3202
+ pending_verification: children.filter((c) => c.status === "pending_verification").length,
2907
3203
  in_progress: children.filter((c) => c.status === "in_progress").length,
2908
3204
  open: children.filter((c) => c.status === "open").length,
2909
3205
  blocked: children.filter((c) => c.status === "blocked").length
2910
3206
  };
2911
3207
  }
2912
- function formatSummaryPretty(summaries) {
3208
+ function countVerifications(epicId) {
3209
+ const children = getChildren(epicId);
3210
+ let total = 0;
3211
+ let done = 0;
3212
+ for (const child of children) {
3213
+ const verifications = getVerifications(child.id);
3214
+ total += verifications.length;
3215
+ done += verifications.filter((v) => v.status === "closed").length;
3216
+ }
3217
+ return { total, done };
3218
+ }
3219
+ function formatSummaryPretty(summaries, sectionHeader) {
2913
3220
  if (summaries.length === 0) {
2914
3221
  return "No epics found.";
2915
3222
  }
2916
3223
  const lines = [];
3224
+ lines.push(`## ${sectionHeader} (${summaries.length})`);
3225
+ lines.push("");
2917
3226
  for (const summary of summaries) {
2918
- const { children } = summary;
2919
- const progress = children.total > 0 ? `(${children.done}/${children.total} done)` : "(no children)";
2920
- lines.push(`${summary.id} ${summary.title} ${progress}`);
3227
+ const { children, verifications } = summary;
3228
+ lines.push(`${summary.id}: ${summary.title}`);
3229
+ lines.push(` Created: ${formatRelativeTime(summary.createdAt)} | Updated: ${formatRelativeTime(summary.updatedAt)}`);
3230
+ const pendingStr = children.pending_verification > 0 ? ` (${children.pending_verification} pending verification)` : "";
3231
+ const issueCount = `Issues: ${children.done}/${children.total} done${pendingStr}`;
3232
+ const verifCount = `Verifications: ${verifications.done}/${verifications.total} done`;
3233
+ lines.push(` ${issueCount} | ${verifCount}`);
2921
3234
  if (summary.parent) {
2922
- lines.push(` Parent: ${summary.parent.id} "${summary.parent.title}"`);
3235
+ lines.push(` Parent: ${summary.parent.id} (${summary.parent.title})`);
2923
3236
  }
2924
3237
  if (summary.description) {
2925
- const desc = summary.description.split("\n")[0];
2926
- const truncated = desc.length > 60 ? desc.slice(0, 57) + "..." : desc;
2927
- lines.push(` ${truncated}`);
3238
+ lines.push("");
3239
+ lines.push(` ${summary.description}`);
2928
3240
  }
3241
+ lines.push("");
3242
+ lines.push(` Run \`pb list --parent ${summary.id}\` to see all issues.`);
3243
+ lines.push("");
2929
3244
  }
2930
3245
  return lines.join("\n");
2931
3246
  }
@@ -2934,31 +3249,17 @@ function summaryCommand(program2) {
2934
3249
  const pretty = program2.opts().pretty ?? false;
2935
3250
  try {
2936
3251
  getOrCreatePebbleDir();
2937
- let epics = getIssues({ type: "epic" });
2938
- if (options.includeClosed) {
2939
- } else if (options.status !== void 0) {
2940
- const status = options.status;
2941
- if (!STATUSES.includes(status)) {
2942
- throw new Error(`Invalid status: ${status}. Must be one of: ${STATUSES.join(", ")}`);
2943
- }
2944
- epics = epics.filter((e) => e.status === status);
2945
- } else {
2946
- epics = epics.filter((e) => e.status !== "closed");
2947
- }
2948
- epics.sort(
2949
- (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
2950
- );
2951
- const limit = parseInt(options.limit, 10);
2952
- if (limit > 0) {
2953
- epics = epics.slice(0, limit);
2954
- }
2955
- const summaries = epics.map((epic) => {
3252
+ const allEpics = getIssues({ type: "epic" });
3253
+ const buildSummary = (epic) => {
2956
3254
  const summary = {
2957
3255
  id: epic.id,
2958
3256
  title: epic.title,
2959
3257
  description: epic.description,
2960
3258
  status: epic.status,
2961
- children: countChildren(epic.id)
3259
+ createdAt: epic.createdAt,
3260
+ updatedAt: epic.updatedAt,
3261
+ children: countChildren(epic.id),
3262
+ verifications: countVerifications(epic.id)
2962
3263
  };
2963
3264
  if (epic.parent) {
2964
3265
  const parentIssue = getIssue(epic.parent);
@@ -2970,9 +3271,60 @@ function summaryCommand(program2) {
2970
3271
  }
2971
3272
  }
2972
3273
  return summary;
2973
- });
3274
+ };
3275
+ const limit = parseInt(options.limit, 10);
3276
+ if (options.includeClosed) {
3277
+ const openEpics = allEpics.filter((e) => e.status !== "closed");
3278
+ const seventyTwoHoursAgo = Date.now() - 72 * 60 * 60 * 1e3;
3279
+ const closedEpics = allEpics.filter(
3280
+ (e) => e.status === "closed" && new Date(e.updatedAt).getTime() > seventyTwoHoursAgo
3281
+ );
3282
+ openEpics.sort(
3283
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
3284
+ );
3285
+ closedEpics.sort(
3286
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
3287
+ );
3288
+ const limitedOpen = limit > 0 ? openEpics.slice(0, limit) : openEpics;
3289
+ const limitedClosed = limit > 0 ? closedEpics.slice(0, limit) : closedEpics;
3290
+ const openSummaries = limitedOpen.map(buildSummary);
3291
+ const closedSummaries = limitedClosed.map(buildSummary);
3292
+ if (pretty) {
3293
+ const output = [];
3294
+ if (openSummaries.length > 0) {
3295
+ output.push(formatSummaryPretty(openSummaries, "Open Epics"));
3296
+ }
3297
+ if (closedSummaries.length > 0) {
3298
+ if (output.length > 0) output.push("");
3299
+ output.push(formatSummaryPretty(closedSummaries, "Recently Closed Epics (last 72h)"));
3300
+ }
3301
+ console.log(output.join("\n"));
3302
+ } else {
3303
+ console.log(formatJson({ open: openSummaries, closed: closedSummaries }));
3304
+ }
3305
+ return;
3306
+ }
3307
+ let epics = allEpics;
3308
+ let sectionHeader = "Open Epics";
3309
+ if (options.status !== void 0) {
3310
+ const status = options.status;
3311
+ if (!STATUSES.includes(status)) {
3312
+ throw new Error(`Invalid status: ${status}. Must be one of: ${STATUSES.join(", ")}`);
3313
+ }
3314
+ epics = epics.filter((e) => e.status === status);
3315
+ sectionHeader = `${STATUS_LABELS[status]} Epics`;
3316
+ } else {
3317
+ epics = epics.filter((e) => e.status !== "closed");
3318
+ }
3319
+ epics.sort(
3320
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
3321
+ );
3322
+ if (limit > 0) {
3323
+ epics = epics.slice(0, limit);
3324
+ }
3325
+ const summaries = epics.map(buildSummary);
2974
3326
  if (pretty) {
2975
- console.log(formatSummaryPretty(summaries));
3327
+ console.log(formatSummaryPretty(summaries, sectionHeader));
2976
3328
  } else {
2977
3329
  console.log(formatJson(summaries));
2978
3330
  }
@@ -2998,7 +3350,7 @@ function parseDuration(duration) {
2998
3350
  };
2999
3351
  return value * multipliers[unit];
3000
3352
  }
3001
- function formatRelativeTime(timestamp) {
3353
+ function formatRelativeTime2(timestamp) {
3002
3354
  const now = Date.now();
3003
3355
  const then = new Date(timestamp).getTime();
3004
3356
  const diff = now - then;
@@ -3017,7 +3369,7 @@ function formatHistoryPretty(entries) {
3017
3369
  }
3018
3370
  const lines = [];
3019
3371
  for (const entry of entries) {
3020
- const time = formatRelativeTime(entry.timestamp);
3372
+ const time = formatRelativeTime2(entry.timestamp);
3021
3373
  const eventLabel = entry.event.charAt(0).toUpperCase() + entry.event.slice(1);
3022
3374
  let line = `[${time}] ${eventLabel} ${entry.issue.id} "${entry.issue.title}" (${entry.issue.type})`;
3023
3375
  if (entry.parent) {
@@ -3142,7 +3494,7 @@ function searchCommand(program2) {
3142
3494
  const bInTitle = b.title.toLowerCase().includes(lowerQuery);
3143
3495
  if (aInTitle && !bInTitle) return -1;
3144
3496
  if (!aInTitle && bInTitle) return 1;
3145
- return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
3497
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
3146
3498
  });
3147
3499
  const limit = parseInt(options.limit, 10);
3148
3500
  if (limit > 0) {
@@ -3157,7 +3509,7 @@ function searchCommand(program2) {
3157
3509
 
3158
3510
  // src/cli/commands/verifications.ts
3159
3511
  function verificationsCommand(program2) {
3160
- program2.command("verifications <id>").description("List verification issues for a given issue").action(async (id) => {
3512
+ program2.command("verifications <id>").description("List verification issues for a given issue").option("--limit <n>", "Max verifications to return (default: 30)").option("--all", "Show all verifications (no limit)").action(async (id, options) => {
3161
3513
  const pretty = program2.opts().pretty ?? false;
3162
3514
  try {
3163
3515
  getOrCreatePebbleDir();
@@ -3166,7 +3518,18 @@ function verificationsCommand(program2) {
3166
3518
  if (!issue) {
3167
3519
  throw new Error(`Issue not found: ${id}`);
3168
3520
  }
3169
- const verifications = getVerifications(resolvedId);
3521
+ let verifications = getVerifications(resolvedId);
3522
+ verifications.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
3523
+ const total = verifications.length;
3524
+ const limit = options.all ? 0 : options.limit ? parseInt(options.limit, 10) : 30;
3525
+ if (limit > 0 && verifications.length > limit) {
3526
+ verifications = verifications.slice(0, limit);
3527
+ }
3528
+ const limitInfo = {
3529
+ total,
3530
+ shown: verifications.length,
3531
+ limited: limit > 0 && total > limit
3532
+ };
3170
3533
  if (pretty) {
3171
3534
  if (verifications.length === 0) {
3172
3535
  console.log(`No verifications for ${resolvedId}`);
@@ -3179,16 +3542,21 @@ function verificationsCommand(program2) {
3179
3542
  }
3180
3543
  console.log("");
3181
3544
  console.log(`Total: ${verifications.length} verification(s)`);
3545
+ if (limitInfo.limited) {
3546
+ console.log(formatLimitMessage(limitInfo));
3547
+ }
3182
3548
  }
3183
3549
  } else {
3184
- console.log(formatJson({
3550
+ const output = {
3185
3551
  issueId: resolvedId,
3186
3552
  verifications: verifications.map((v) => ({
3187
3553
  id: v.id,
3188
3554
  title: v.title,
3189
3555
  status: v.status
3190
- }))
3191
- }));
3556
+ })),
3557
+ ...limitInfo.limited && { _meta: limitInfo }
3558
+ };
3559
+ console.log(formatJson(output));
3192
3560
  }
3193
3561
  } catch (error) {
3194
3562
  outputError(error, pretty);
@@ -3223,6 +3591,7 @@ function initCommand(program2) {
3223
3591
  var program = new Command();
3224
3592
  program.name("pebble").description("A lightweight JSONL-based issue tracker").version("0.1.0");
3225
3593
  program.option("-P, --pretty", "Human-readable output (default: JSON)");
3594
+ program.option("--json", "JSON output (this is the default, flag not needed)");
3226
3595
  createCommand(program);
3227
3596
  updateCommand(program);
3228
3597
  closeCommand(program);