@markmdev/pebble 0.1.9 → 0.1.11

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
  }
@@ -529,8 +546,10 @@ function formatIssueDetailPretty(issue, ctx) {
529
546
  }
530
547
  if (ctx.children.length > 0) {
531
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)` : "";
532
551
  lines.push("");
533
- lines.push(`Children (${closedChildren.length}/${ctx.children.length} done):`);
552
+ lines.push(`Children (${closedChildren.length}/${ctx.children.length} done${pendingStr}):`);
534
553
  for (const child of ctx.children) {
535
554
  const statusIcon = child.status === "closed" ? "\u2713" : child.status === "in_progress" ? "\u25B6" : "\u25CB";
536
555
  lines.push(` ${statusIcon} ${child.id} - ${child.title} [${child.status}]`);
@@ -671,11 +690,94 @@ function outputMutationSuccess(id, pretty) {
671
690
  console.log(JSON.stringify({ id, success: true }));
672
691
  }
673
692
  }
674
- function outputIssueList(issues, pretty) {
693
+ function outputIssueList(issues, pretty, limitInfo) {
675
694
  if (pretty) {
676
695
  console.log(formatIssueListPretty(issues));
696
+ if (limitInfo?.limited) {
697
+ console.log(formatLimitMessage(limitInfo));
698
+ }
699
+ } else {
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
+ }
677
775
  } else {
678
- console.log(formatJson(issues));
776
+ if (limitInfo?.limited) {
777
+ console.log(formatJson({ issues: tree, _meta: limitInfo }));
778
+ } else {
779
+ console.log(formatJson(tree));
780
+ }
679
781
  }
680
782
  }
681
783
  function outputError(error, pretty) {
@@ -686,42 +788,55 @@ function outputError(error, pretty) {
686
788
  }
687
789
  process.exit(1);
688
790
  }
689
- function formatIssueListVerbose(issues) {
791
+ function formatIssueListVerbose(issues, sectionHeader) {
690
792
  if (issues.length === 0) {
691
793
  return "No issues found.";
692
794
  }
693
795
  const lines = [];
796
+ if (sectionHeader) {
797
+ lines.push(`## ${sectionHeader} (${issues.length})`);
798
+ lines.push("");
799
+ }
694
800
  for (const info of issues) {
695
- const { issue, blocking, children, verifications, blockers } = info;
696
- lines.push(`${issue.id} - ${issue.title}`);
697
- lines.push("\u2500".repeat(60));
698
- lines.push(` Type: ${formatType(issue.type)}`);
699
- lines.push(` Priority: P${issue.priority}`);
700
- lines.push(` Status: ${issue.status}`);
701
- lines.push(` Parent: ${issue.parent || "-"}`);
702
- lines.push(` Children: ${issue.type === "epic" ? children : "-"}`);
703
- lines.push(` Blocking: ${blocking.length > 0 ? blocking.join(", ") : "[]"}`);
704
- 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
+ }
705
810
  if (blockers && blockers.length > 0) {
706
- 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}`);
707
815
  }
708
816
  lines.push("");
709
817
  }
710
- lines.push(`Total: ${issues.length} issue(s)`);
711
818
  return lines.join("\n");
712
819
  }
713
- function outputIssueListVerbose(issues, pretty) {
820
+ function outputIssueListVerbose(issues, pretty, sectionHeader, limitInfo) {
714
821
  if (pretty) {
715
- console.log(formatIssueListVerbose(issues));
822
+ console.log(formatIssueListVerbose(issues, sectionHeader));
823
+ if (limitInfo?.limited) {
824
+ console.log(formatLimitMessage(limitInfo));
825
+ }
716
826
  } else {
717
- const output = issues.map(({ issue, blocking, children, verifications, blockers }) => ({
827
+ const output = issues.map(({ issue, blocking, children, verifications, blockers, parent }) => ({
718
828
  ...issue,
719
829
  blocking,
720
830
  childrenCount: issue.type === "epic" ? children : void 0,
721
831
  verificationsCount: verifications,
722
- ...blockers && { openBlockers: blockers }
832
+ ...blockers && { openBlockers: blockers },
833
+ ...parent && { parentInfo: parent }
723
834
  }));
724
- console.log(formatJson(output));
835
+ if (limitInfo?.limited) {
836
+ console.log(formatJson({ issues: output, _meta: limitInfo }));
837
+ } else {
838
+ console.log(formatJson(output));
839
+ }
725
840
  }
726
841
  }
727
842
 
@@ -1043,6 +1158,8 @@ Pending verifications:`);
1043
1158
  for (const v of result.pendingVerifications || []) {
1044
1159
  console.log(` \u2022 ${v.id} - ${v.title}`);
1045
1160
  }
1161
+ console.log(`
1162
+ Run: pb verifications ${result.id}`);
1046
1163
  } else {
1047
1164
  console.log(`\u2713 ${result.id}`);
1048
1165
  if (result.unblocked && result.unblocked.length > 0) {
@@ -1059,6 +1176,7 @@ Unblocked:`);
1059
1176
  success: true,
1060
1177
  status: result.status,
1061
1178
  ...result.pendingVerifications && { pendingVerifications: result.pendingVerifications },
1179
+ ...result.pendingVerifications && { hint: `pb verifications ${result.id}` },
1062
1180
  ...result.unblocked && { unblocked: result.unblocked }
1063
1181
  }));
1064
1182
  }
@@ -1074,6 +1192,7 @@ Unblocked:`);
1074
1192
  for (const v of result.pendingVerifications || []) {
1075
1193
  console.log(` \u2022 ${v.id} - ${v.title}`);
1076
1194
  }
1195
+ console.log(` Run: pb verifications ${result.id}`);
1077
1196
  } else {
1078
1197
  console.log(`\u2713 ${result.id}`);
1079
1198
  if (result.unblocked && result.unblocked.length > 0) {
@@ -1093,6 +1212,7 @@ Unblocked:`);
1093
1212
  status: r.status,
1094
1213
  ...r.error && { error: r.error },
1095
1214
  ...r.pendingVerifications && { pendingVerifications: r.pendingVerifications },
1215
+ ...r.pendingVerifications && { hint: `pb verifications ${r.id}` },
1096
1216
  ...r.unblocked && { unblocked: r.unblocked }
1097
1217
  }))));
1098
1218
  }
@@ -1212,7 +1332,7 @@ function claimCommand(program2) {
1212
1332
 
1213
1333
  // src/cli/commands/list.ts
1214
1334
  function listCommand(program2) {
1215
- 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) => {
1216
1336
  const pretty = program2.opts().pretty ?? false;
1217
1337
  try {
1218
1338
  getOrCreatePebbleDir();
@@ -1241,8 +1361,48 @@ function listCommand(program2) {
1241
1361
  if (options.parent !== void 0) {
1242
1362
  filters.parent = resolveId(options.parent);
1243
1363
  }
1244
- const issues = getIssues(filters);
1245
- 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
+ }
1246
1406
  } catch (error) {
1247
1407
  outputError(error, pretty);
1248
1408
  }
@@ -1273,21 +1433,48 @@ function showCommand(program2) {
1273
1433
 
1274
1434
  // src/cli/commands/ready.ts
1275
1435
  function readyCommand(program2) {
1276
- 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) => {
1277
1437
  const pretty = program2.opts().pretty ?? false;
1278
1438
  try {
1279
1439
  getOrCreatePebbleDir();
1280
- 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
+ };
1281
1459
  if (options.verbose) {
1282
- const verboseIssues = issues.map((issue) => ({
1283
- issue,
1284
- blocking: getBlocking(issue.id).map((i) => i.id),
1285
- children: getChildren(issue.id).length,
1286
- verifications: getVerifications(issue.id).length
1287
- }));
1288
- 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);
1289
1476
  } else {
1290
- outputIssueList(issues, pretty);
1477
+ outputIssueList(issues, pretty, limitInfo);
1291
1478
  }
1292
1479
  } catch (error) {
1293
1480
  outputError(error, pretty);
@@ -1297,26 +1484,44 @@ function readyCommand(program2) {
1297
1484
 
1298
1485
  // src/cli/commands/blocked.ts
1299
1486
  function blockedCommand(program2) {
1300
- 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) => {
1301
1488
  const pretty = program2.opts().pretty ?? false;
1302
1489
  try {
1303
1490
  getOrCreatePebbleDir();
1304
- 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
+ };
1305
1503
  if (options.verbose) {
1306
1504
  const verboseIssues = issues.map((issue) => {
1307
1505
  const allBlockers = getBlockers(issue.id);
1308
1506
  const openBlockers = allBlockers.filter((b) => b.status !== "closed").map((b) => b.id);
1309
- return {
1507
+ const info = {
1310
1508
  issue,
1311
1509
  blocking: getBlocking(issue.id).map((i) => i.id),
1312
1510
  children: getChildren(issue.id).length,
1313
1511
  verifications: getVerifications(issue.id).length,
1314
1512
  blockers: openBlockers
1315
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;
1316
1521
  });
1317
- outputIssueListVerbose(verboseIssues, pretty);
1522
+ outputIssueListVerbose(verboseIssues, pretty, "Blocked Issues", limitInfo);
1318
1523
  } else {
1319
- outputIssueList(issues, pretty);
1524
+ outputIssueList(issues, pretty, limitInfo);
1320
1525
  }
1321
1526
  } catch (error) {
1322
1527
  outputError(error, pretty);
@@ -1327,39 +1532,55 @@ function blockedCommand(program2) {
1327
1532
  // src/cli/commands/dep.ts
1328
1533
  function depCommand(program2) {
1329
1534
  const dep = program2.command("dep").description("Manage dependencies");
1330
- 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) => {
1331
1536
  const pretty = program2.opts().pretty ?? false;
1332
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
+ }
1333
1547
  const pebbleDir = getOrCreatePebbleDir();
1334
- const resolvedId = resolveId(id);
1335
- const resolvedBlockerId = resolveId(blockerId);
1336
- 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);
1337
1558
  if (!issue) {
1338
- throw new Error(`Issue not found: ${id}`);
1559
+ throw new Error(`Issue not found: ${blockedId}`);
1339
1560
  }
1340
- const blocker = getIssue(resolvedBlockerId);
1561
+ const blocker = getIssue(blockerIdResolved);
1341
1562
  if (!blocker) {
1342
- throw new Error(`Blocker issue not found: ${blockerId}`);
1563
+ throw new Error(`Blocker issue not found: ${blockerIdResolved}`);
1343
1564
  }
1344
- if (resolvedId === resolvedBlockerId) {
1565
+ if (blockedId === blockerIdResolved) {
1345
1566
  throw new Error("Cannot add self as blocker");
1346
1567
  }
1347
- if (issue.blockedBy.includes(resolvedBlockerId)) {
1348
- 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}`);
1349
1570
  }
1350
- if (detectCycle(resolvedId, resolvedBlockerId)) {
1571
+ if (detectCycle(blockedId, blockerIdResolved)) {
1351
1572
  throw new Error(`Adding this dependency would create a cycle`);
1352
1573
  }
1353
1574
  const event = {
1354
1575
  type: "update",
1355
- issueId: resolvedId,
1576
+ issueId: blockedId,
1356
1577
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1357
1578
  data: {
1358
- blockedBy: [...issue.blockedBy, resolvedBlockerId]
1579
+ blockedBy: [...issue.blockedBy, blockerIdResolved]
1359
1580
  }
1360
1581
  };
1361
1582
  appendEvent(event, pebbleDir);
1362
- outputMutationSuccess(resolvedId, pretty);
1583
+ outputMutationSuccess(blockedId, pretty);
1363
1584
  } catch (error) {
1364
1585
  outputError(error, pretty);
1365
1586
  }
@@ -1510,7 +1731,7 @@ function depCommand(program2) {
1510
1731
  outputError(error, pretty);
1511
1732
  }
1512
1733
  });
1513
- 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) => {
1514
1735
  const pretty = program2.opts().pretty ?? false;
1515
1736
  try {
1516
1737
  const resolvedId = resolveId(id);
@@ -1520,10 +1741,9 @@ function depCommand(program2) {
1520
1741
  }
1521
1742
  const events = readEvents();
1522
1743
  const state = computeState(events);
1523
- const visited = /* @__PURE__ */ new Set();
1524
- const tree = buildDepTree(resolvedId, visited, 0, state);
1744
+ const tree = buildIssueTree2(resolvedId, state);
1525
1745
  if (pretty) {
1526
- console.log(formatDepTree(tree));
1746
+ console.log(formatIssueTreePretty2(tree));
1527
1747
  } else {
1528
1748
  console.log(formatJson(tree));
1529
1749
  }
@@ -1532,50 +1752,94 @@ function depCommand(program2) {
1532
1752
  }
1533
1753
  });
1534
1754
  }
1535
- function buildDepTree(issueId, visited, depth, state) {
1536
- if (visited.has(issueId)) {
1537
- return null;
1538
- }
1539
- visited.add(issueId);
1755
+ function buildIssueTree2(issueId, state) {
1540
1756
  const issue = state.get(issueId);
1541
1757
  if (!issue) {
1542
1758
  return null;
1543
1759
  }
1544
- const blockedBy = [];
1545
- for (const blockerId of issue.blockedBy) {
1546
- const child = buildDepTree(blockerId, visited, depth + 1, state);
1547
- if (child) {
1548
- 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
+ }
1549
1777
  }
1550
- }
1551
- return {
1778
+ return children;
1779
+ };
1780
+ const visited = /* @__PURE__ */ new Set([issueId]);
1781
+ const targetChildren = buildChildren(issueId, visited);
1782
+ const targetNode = {
1552
1783
  id: issue.id,
1553
1784
  title: issue.title,
1785
+ type: issue.type,
1786
+ priority: issue.priority,
1554
1787
  status: issue.status,
1555
- depth,
1556
- blockedBy
1788
+ isTarget: true,
1789
+ childrenCount: targetChildren.length,
1790
+ ...targetChildren.length > 0 && { children: targetChildren }
1557
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;
1558
1824
  }
1559
- function formatDepTree(node, prefix = "", isRoot = true) {
1825
+ function formatIssueTreePretty2(node) {
1560
1826
  if (!node) {
1561
- return "";
1827
+ return "Issue not found.";
1562
1828
  }
1563
1829
  const lines = [];
1564
- const statusIcon = node.status === "closed" ? "\u2713" : "\u25CB";
1565
- if (isRoot) {
1566
- lines.push(`${statusIcon} ${node.id} - ${node.title}`);
1567
- }
1568
- for (let i = 0; i < node.blockedBy.length; i++) {
1569
- const child = node.blockedBy[i];
1570
- const isLast = i === node.blockedBy.length - 1;
1571
- const connector = isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
1572
- const childPrefix = prefix + (isLast ? " " : "\u2502 ");
1573
- const childStatusIcon = child.status === "closed" ? "\u2713" : "\u25CB";
1574
- lines.push(`${prefix}${connector}${childStatusIcon} ${child.id} - ${child.title}`);
1575
- if (child.blockedBy.length > 0) {
1576
- lines.push(formatDepTree(child, childPrefix, false));
1577
- }
1578
- }
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);
1579
1843
  return lines.join("\n");
1580
1844
  }
1581
1845
 
@@ -2935,61 +3199,67 @@ function countChildren(epicId) {
2935
3199
  return {
2936
3200
  total: children.length,
2937
3201
  done: children.filter((c) => c.status === "closed").length,
3202
+ pending_verification: children.filter((c) => c.status === "pending_verification").length,
2938
3203
  in_progress: children.filter((c) => c.status === "in_progress").length,
2939
3204
  open: children.filter((c) => c.status === "open").length,
2940
3205
  blocked: children.filter((c) => c.status === "blocked").length
2941
3206
  };
2942
3207
  }
2943
- 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) {
2944
3220
  if (summaries.length === 0) {
2945
3221
  return "No epics found.";
2946
3222
  }
2947
3223
  const lines = [];
3224
+ lines.push(`## ${sectionHeader} (${summaries.length})`);
3225
+ lines.push("");
2948
3226
  for (const summary of summaries) {
2949
- const { children } = summary;
2950
- const progress = children.total > 0 ? `(${children.done}/${children.total} done)` : "(no children)";
2951
- 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}`);
2952
3234
  if (summary.parent) {
2953
- lines.push(` Parent: ${summary.parent.id} "${summary.parent.title}"`);
3235
+ lines.push(` Parent: ${summary.parent.id} (${summary.parent.title})`);
2954
3236
  }
2955
3237
  if (summary.description) {
2956
- const desc = summary.description.split("\n")[0];
2957
- const truncated = desc.length > 60 ? desc.slice(0, 57) + "..." : desc;
2958
- lines.push(` ${truncated}`);
3238
+ lines.push("");
3239
+ lines.push(` ${summary.description}`);
2959
3240
  }
3241
+ lines.push("");
3242
+ lines.push(` Run \`pb list --parent ${summary.id}\` to see all issues.`);
3243
+ lines.push("");
2960
3244
  }
2961
3245
  return lines.join("\n");
2962
3246
  }
2963
3247
  function summaryCommand(program2) {
2964
- program2.command("summary").description("Show epic summary with child completion status").option("--status <status>", "Filter epics by status (default: open)").option("--limit <n>", "Max epics to return", "10").option("--include-closed", "Include closed epics").action(async (options) => {
3248
+ program2.command("summary").description("Show epic summary with child completion status").option("--status <status>", "Filter epics by specific status").option("--limit <n>", "Max epics to return per section", "10").action(async (options) => {
2965
3249
  const pretty = program2.opts().pretty ?? false;
2966
3250
  try {
2967
3251
  getOrCreatePebbleDir();
2968
- let epics = getIssues({ type: "epic" });
2969
- if (options.includeClosed) {
2970
- } else if (options.status !== void 0) {
2971
- const status = options.status;
2972
- if (!STATUSES.includes(status)) {
2973
- throw new Error(`Invalid status: ${status}. Must be one of: ${STATUSES.join(", ")}`);
2974
- }
2975
- epics = epics.filter((e) => e.status === status);
2976
- } else {
2977
- epics = epics.filter((e) => e.status !== "closed");
2978
- }
2979
- epics.sort(
2980
- (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
2981
- );
2982
- const limit = parseInt(options.limit, 10);
2983
- if (limit > 0) {
2984
- epics = epics.slice(0, limit);
2985
- }
2986
- const summaries = epics.map((epic) => {
3252
+ const allEpics = getIssues({ type: "epic" });
3253
+ const buildSummary = (epic) => {
2987
3254
  const summary = {
2988
3255
  id: epic.id,
2989
3256
  title: epic.title,
2990
3257
  description: epic.description,
2991
3258
  status: epic.status,
2992
- children: countChildren(epic.id)
3259
+ createdAt: epic.createdAt,
3260
+ updatedAt: epic.updatedAt,
3261
+ children: countChildren(epic.id),
3262
+ verifications: countVerifications(epic.id)
2993
3263
  };
2994
3264
  if (epic.parent) {
2995
3265
  const parentIssue = getIssue(epic.parent);
@@ -3001,11 +3271,58 @@ function summaryCommand(program2) {
3001
3271
  }
3002
3272
  }
3003
3273
  return summary;
3004
- });
3274
+ };
3275
+ const limit = parseInt(options.limit, 10);
3276
+ if (options.status !== void 0) {
3277
+ const status = options.status;
3278
+ if (!STATUSES.includes(status)) {
3279
+ throw new Error(`Invalid status: ${status}. Must be one of: ${STATUSES.join(", ")}`);
3280
+ }
3281
+ let epics = allEpics.filter((e) => e.status === status);
3282
+ epics.sort(
3283
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
3284
+ );
3285
+ if (limit > 0) {
3286
+ epics = epics.slice(0, limit);
3287
+ }
3288
+ const summaries = epics.map(buildSummary);
3289
+ if (pretty) {
3290
+ console.log(formatSummaryPretty(summaries, `${STATUS_LABELS[status]} Epics`));
3291
+ } else {
3292
+ console.log(formatJson(summaries));
3293
+ }
3294
+ return;
3295
+ }
3296
+ const openEpics = allEpics.filter((e) => e.status !== "closed");
3297
+ const seventyTwoHoursAgo = Date.now() - 72 * 60 * 60 * 1e3;
3298
+ const closedEpics = allEpics.filter(
3299
+ (e) => e.status === "closed" && new Date(e.updatedAt).getTime() > seventyTwoHoursAgo
3300
+ );
3301
+ openEpics.sort(
3302
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
3303
+ );
3304
+ closedEpics.sort(
3305
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
3306
+ );
3307
+ const limitedOpen = limit > 0 ? openEpics.slice(0, limit) : openEpics;
3308
+ const limitedClosed = limit > 0 ? closedEpics.slice(0, limit) : closedEpics;
3309
+ const openSummaries = limitedOpen.map(buildSummary);
3310
+ const closedSummaries = limitedClosed.map(buildSummary);
3005
3311
  if (pretty) {
3006
- console.log(formatSummaryPretty(summaries));
3312
+ const output = [];
3313
+ if (openSummaries.length > 0) {
3314
+ output.push(formatSummaryPretty(openSummaries, "Open Epics"));
3315
+ }
3316
+ if (closedSummaries.length > 0) {
3317
+ if (output.length > 0) output.push("");
3318
+ output.push(formatSummaryPretty(closedSummaries, "Recently Closed Epics (last 72h)"));
3319
+ }
3320
+ if (output.length === 0) {
3321
+ output.push("No epics found.");
3322
+ }
3323
+ console.log(output.join("\n"));
3007
3324
  } else {
3008
- console.log(formatJson(summaries));
3325
+ console.log(formatJson({ open: openSummaries, closed: closedSummaries }));
3009
3326
  }
3010
3327
  } catch (error) {
3011
3328
  outputError(error, pretty);
@@ -3029,7 +3346,7 @@ function parseDuration(duration) {
3029
3346
  };
3030
3347
  return value * multipliers[unit];
3031
3348
  }
3032
- function formatRelativeTime(timestamp) {
3349
+ function formatRelativeTime2(timestamp) {
3033
3350
  const now = Date.now();
3034
3351
  const then = new Date(timestamp).getTime();
3035
3352
  const diff = now - then;
@@ -3048,7 +3365,7 @@ function formatHistoryPretty(entries) {
3048
3365
  }
3049
3366
  const lines = [];
3050
3367
  for (const entry of entries) {
3051
- const time = formatRelativeTime(entry.timestamp);
3368
+ const time = formatRelativeTime2(entry.timestamp);
3052
3369
  const eventLabel = entry.event.charAt(0).toUpperCase() + entry.event.slice(1);
3053
3370
  let line = `[${time}] ${eventLabel} ${entry.issue.id} "${entry.issue.title}" (${entry.issue.type})`;
3054
3371
  if (entry.parent) {
@@ -3173,7 +3490,7 @@ function searchCommand(program2) {
3173
3490
  const bInTitle = b.title.toLowerCase().includes(lowerQuery);
3174
3491
  if (aInTitle && !bInTitle) return -1;
3175
3492
  if (!aInTitle && bInTitle) return 1;
3176
- return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
3493
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
3177
3494
  });
3178
3495
  const limit = parseInt(options.limit, 10);
3179
3496
  if (limit > 0) {
@@ -3188,7 +3505,7 @@ function searchCommand(program2) {
3188
3505
 
3189
3506
  // src/cli/commands/verifications.ts
3190
3507
  function verificationsCommand(program2) {
3191
- program2.command("verifications <id>").description("List verification issues for a given issue").action(async (id) => {
3508
+ 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) => {
3192
3509
  const pretty = program2.opts().pretty ?? false;
3193
3510
  try {
3194
3511
  getOrCreatePebbleDir();
@@ -3197,7 +3514,18 @@ function verificationsCommand(program2) {
3197
3514
  if (!issue) {
3198
3515
  throw new Error(`Issue not found: ${id}`);
3199
3516
  }
3200
- const verifications = getVerifications(resolvedId);
3517
+ let verifications = getVerifications(resolvedId);
3518
+ verifications.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
3519
+ const total = verifications.length;
3520
+ const limit = options.all ? 0 : options.limit ? parseInt(options.limit, 10) : 30;
3521
+ if (limit > 0 && verifications.length > limit) {
3522
+ verifications = verifications.slice(0, limit);
3523
+ }
3524
+ const limitInfo = {
3525
+ total,
3526
+ shown: verifications.length,
3527
+ limited: limit > 0 && total > limit
3528
+ };
3201
3529
  if (pretty) {
3202
3530
  if (verifications.length === 0) {
3203
3531
  console.log(`No verifications for ${resolvedId}`);
@@ -3210,16 +3538,21 @@ function verificationsCommand(program2) {
3210
3538
  }
3211
3539
  console.log("");
3212
3540
  console.log(`Total: ${verifications.length} verification(s)`);
3541
+ if (limitInfo.limited) {
3542
+ console.log(formatLimitMessage(limitInfo));
3543
+ }
3213
3544
  }
3214
3545
  } else {
3215
- console.log(formatJson({
3546
+ const output = {
3216
3547
  issueId: resolvedId,
3217
3548
  verifications: verifications.map((v) => ({
3218
3549
  id: v.id,
3219
3550
  title: v.title,
3220
3551
  status: v.status
3221
- }))
3222
- }));
3552
+ })),
3553
+ ...limitInfo.limited && { _meta: limitInfo }
3554
+ };
3555
+ console.log(formatJson(output));
3223
3556
  }
3224
3557
  } catch (error) {
3225
3558
  outputError(error, pretty);
@@ -3255,13 +3588,6 @@ var program = new Command();
3255
3588
  program.name("pebble").description("A lightweight JSONL-based issue tracker").version("0.1.0");
3256
3589
  program.option("-P, --pretty", "Human-readable output (default: JSON)");
3257
3590
  program.option("--json", "JSON output (this is the default, flag not needed)");
3258
- program.hook("preAction", () => {
3259
- const opts = program.opts();
3260
- if (opts.json) {
3261
- console.error("Note: --json flag is unnecessary. JSON is the default output format.");
3262
- console.error("Use --pretty (-P) for human-readable output.");
3263
- }
3264
- });
3265
3591
  createCommand(program);
3266
3592
  updateCommand(program);
3267
3593
  closeCommand(program);