@markmdev/pebble 0.1.5 → 0.1.7

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 = {
@@ -149,7 +150,7 @@ function getConfigPath(pebbleDir) {
149
150
  function getConfig(pebbleDir) {
150
151
  const configPath = getConfigPath(pebbleDir);
151
152
  if (!fs.existsSync(configPath)) {
152
- throw new Error("Config file not found. Initialize .pebble first.");
153
+ throw new Error("No .pebble directory found. Run 'pb init' to initialize.");
153
154
  }
154
155
  const content = fs.readFileSync(configPath, "utf-8");
155
156
  return JSON.parse(content);
@@ -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,
@@ -211,11 +213,14 @@ function computeState(events) {
211
213
  issue.description = updateEvent.data.description;
212
214
  }
213
215
  if (updateEvent.data.parent !== void 0) {
214
- issue.parent = updateEvent.data.parent;
216
+ issue.parent = updateEvent.data.parent || void 0;
215
217
  }
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) {
@@ -613,10 +658,48 @@ function outputError(error, pretty) {
613
658
  }
614
659
  process.exit(1);
615
660
  }
661
+ function formatIssueListVerbose(issues) {
662
+ if (issues.length === 0) {
663
+ return "No issues found.";
664
+ }
665
+ const lines = [];
666
+ 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}`);
677
+ if (blockers && blockers.length > 0) {
678
+ lines.push(` Blocked by: ${blockers.join(", ")}`);
679
+ }
680
+ lines.push("");
681
+ }
682
+ lines.push(`Total: ${issues.length} issue(s)`);
683
+ return lines.join("\n");
684
+ }
685
+ function outputIssueListVerbose(issues, pretty) {
686
+ if (pretty) {
687
+ console.log(formatIssueListVerbose(issues));
688
+ } else {
689
+ const output = issues.map(({ issue, blocking, children, verifications, blockers }) => ({
690
+ ...issue,
691
+ blocking,
692
+ childrenCount: issue.type === "epic" ? children : void 0,
693
+ verificationsCount: verifications,
694
+ ...blockers && { openBlockers: blockers }
695
+ }));
696
+ console.log(formatJson(output));
697
+ }
698
+ }
616
699
 
617
700
  // src/cli/commands/create.ts
618
701
  function createCommand(program2) {
619
- program2.command("create <title>").description("Create a new issue").option("-t, --type <type>", "Issue type (task, bug, epic, verification)", "task").option("-p, --priority <priority>", "Priority (0-4)", "2").option("-d, --description <desc>", "Description").option("--parent <id>", "Parent epic ID").option("--verifies <id>", "ID of issue this verifies (sets type to verification)").action(async (title, options) => {
702
+ program2.command("create <title>").description("Create a new issue").option("-t, --type <type>", "Issue type (task, bug, epic, verification)", "task").option("-p, --priority <priority>", "Priority (0-4)", "2").option("-d, --description <desc>", "Description").option("--parent <id>", "Parent epic ID").option("--verifies <id>", "ID of issue this verifies (sets type to verification)").option("--blocked-by <ids>", "Comma-separated IDs of issues that block this one").option("--blocks <ids>", "Comma-separated IDs of issues this one will block").action(async (title, options) => {
620
703
  const pretty = program2.opts().pretty ?? false;
621
704
  try {
622
705
  let type = options.type;
@@ -657,6 +740,33 @@ function createCommand(program2) {
657
740
  throw new Error(`Target issue not found: ${options.verifies}`);
658
741
  }
659
742
  }
743
+ const blockedByIds = [];
744
+ if (options.blockedBy) {
745
+ const ids = options.blockedBy.split(",").map((s) => s.trim()).filter(Boolean);
746
+ for (const rawId of ids) {
747
+ const resolvedId = resolveId(rawId);
748
+ const blocker = getIssue(resolvedId);
749
+ if (!blocker) {
750
+ throw new Error(`Blocker issue not found: ${rawId}`);
751
+ }
752
+ if (blocker.status === "closed") {
753
+ throw new Error(`Cannot be blocked by closed issue: ${resolvedId}`);
754
+ }
755
+ blockedByIds.push(resolvedId);
756
+ }
757
+ }
758
+ const blocksIds = [];
759
+ if (options.blocks) {
760
+ const ids = options.blocks.split(",").map((s) => s.trim()).filter(Boolean);
761
+ for (const rawId of ids) {
762
+ const resolvedId = resolveId(rawId);
763
+ const blocked = getIssue(resolvedId);
764
+ if (!blocked) {
765
+ throw new Error(`Issue to block not found: ${rawId}`);
766
+ }
767
+ blocksIds.push(resolvedId);
768
+ }
769
+ }
660
770
  const id = generateId(config.prefix);
661
771
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
662
772
  const event = {
@@ -673,6 +783,26 @@ function createCommand(program2) {
673
783
  }
674
784
  };
675
785
  appendEvent(event, pebbleDir);
786
+ if (blockedByIds.length > 0) {
787
+ const depEvent = {
788
+ type: "update",
789
+ issueId: id,
790
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
791
+ data: { blockedBy: blockedByIds }
792
+ };
793
+ appendEvent(depEvent, pebbleDir);
794
+ }
795
+ for (const targetId of blocksIds) {
796
+ const target = getIssue(targetId);
797
+ const existingBlockers = target?.blockedBy || [];
798
+ const depEvent = {
799
+ type: "update",
800
+ issueId: targetId,
801
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
802
+ data: { blockedBy: [...existingBlockers, id] }
803
+ };
804
+ appendEvent(depEvent, pebbleDir);
805
+ }
676
806
  outputMutationSuccess(id, pretty);
677
807
  } catch (error) {
678
808
  outputError(error, pretty);
@@ -682,7 +812,7 @@ function createCommand(program2) {
682
812
 
683
813
  // src/cli/commands/update.ts
684
814
  function updateCommand(program2) {
685
- program2.command("update <ids...>").description("Update issues. Supports multiple IDs.").option("--status <status>", "Status (open, in_progress, blocked, closed)").option("--priority <priority>", "Priority (0-4)").option("--title <title>", "Title").option("--description <desc>", "Description").action(async (ids, options) => {
815
+ program2.command("update <ids...>").description("Update issues. Supports multiple IDs.").option("--status <status>", "Status (open, in_progress, blocked, closed)").option("--priority <priority>", "Priority (0-4)").option("--title <title>", "Title").option("--description <desc>", "Description").option("--parent <id>", 'Parent epic ID (use "null" to remove parent)').action(async (ids, options) => {
686
816
  const pretty = program2.opts().pretty ?? false;
687
817
  try {
688
818
  const pebbleDir = getOrCreatePebbleDir();
@@ -716,8 +846,27 @@ function updateCommand(program2) {
716
846
  data.description = options.description;
717
847
  hasChanges = true;
718
848
  }
849
+ if (options.parent !== void 0) {
850
+ if (options.parent.toLowerCase() === "null") {
851
+ data.parent = "";
852
+ } else {
853
+ const parentId = resolveId(options.parent);
854
+ const parentIssue = getIssue(parentId);
855
+ if (!parentIssue) {
856
+ throw new Error(`Parent issue not found: ${options.parent}`);
857
+ }
858
+ if (parentIssue.type !== "epic") {
859
+ throw new Error(`Parent must be an epic. ${parentId} is a ${parentIssue.type}`);
860
+ }
861
+ if (parentIssue.status === "closed") {
862
+ throw new Error(`Cannot set parent to closed epic: ${parentId}`);
863
+ }
864
+ data.parent = parentId;
865
+ }
866
+ hasChanges = true;
867
+ }
719
868
  if (!hasChanges) {
720
- throw new Error("No changes specified. Use --status, --priority, --title, or --description");
869
+ throw new Error("No changes specified. Use --status, --priority, --title, --description, or --parent");
721
870
  }
722
871
  const results = [];
723
872
  for (const id of allIds) {
@@ -728,6 +877,12 @@ function updateCommand(program2) {
728
877
  results.push({ id, success: false, error: `Issue not found: ${id}` });
729
878
  continue;
730
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
+ }
731
886
  const event = {
732
887
  type: "update",
733
888
  issueId: resolvedId,
@@ -810,6 +965,25 @@ function closeCommand(program2) {
810
965
  };
811
966
  appendEvent(commentEvent, pebbleDir);
812
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
+ }
813
987
  const closeEvent = {
814
988
  type: "close",
815
989
  issueId: resolvedId,
@@ -823,6 +997,7 @@ function closeCommand(program2) {
823
997
  results.push({
824
998
  id: resolvedId,
825
999
  success: true,
1000
+ status: "closed",
826
1001
  unblocked: unblocked.length > 0 ? unblocked.map((i) => ({ id: i.id, title: i.title })) : void 0
827
1002
  });
828
1003
  } catch (error) {
@@ -833,18 +1008,29 @@ function closeCommand(program2) {
833
1008
  const result = results[0];
834
1009
  if (result.success) {
835
1010
  if (pretty) {
836
- console.log(`\u2713 ${result.id}`);
837
- if (result.unblocked && result.unblocked.length > 0) {
1011
+ if (result.status === "pending_verification") {
1012
+ console.log(`\u23F3 ${result.id} \u2192 pending_verification`);
838
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(`
839
1022
  Unblocked:`);
840
- for (const u of result.unblocked) {
841
- console.log(` \u2192 ${u.id} - ${u.title}`);
1023
+ for (const u of result.unblocked) {
1024
+ console.log(` \u2192 ${u.id} - ${u.title}`);
1025
+ }
842
1026
  }
843
1027
  }
844
1028
  } else {
845
1029
  console.log(formatJson({
846
1030
  id: result.id,
847
1031
  success: true,
1032
+ status: result.status,
1033
+ ...result.pendingVerifications && { pendingVerifications: result.pendingVerifications },
848
1034
  ...result.unblocked && { unblocked: result.unblocked }
849
1035
  }));
850
1036
  }
@@ -855,10 +1041,17 @@ Unblocked:`);
855
1041
  if (pretty) {
856
1042
  for (const result of results) {
857
1043
  if (result.success) {
858
- console.log(`\u2713 ${result.id}`);
859
- if (result.unblocked && result.unblocked.length > 0) {
860
- for (const u of result.unblocked) {
861
- 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
+ }
862
1055
  }
863
1056
  }
864
1057
  } else {
@@ -869,7 +1062,9 @@ Unblocked:`);
869
1062
  console.log(formatJson(results.map((r) => ({
870
1063
  id: r.id,
871
1064
  success: r.success,
1065
+ status: r.status,
872
1066
  ...r.error && { error: r.error },
1067
+ ...r.pendingVerifications && { pendingVerifications: r.pendingVerifications },
873
1068
  ...r.unblocked && { unblocked: r.unblocked }
874
1069
  }))));
875
1070
  }
@@ -937,6 +1132,12 @@ function claimCommand(program2) {
937
1132
  results.push({ id: resolvedId, success: false, error: `Cannot claim closed issue: ${resolvedId}` });
938
1133
  continue;
939
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
+ }
940
1141
  const event = {
941
1142
  type: "update",
942
1143
  issueId: resolvedId,
@@ -1041,12 +1242,22 @@ function showCommand(program2) {
1041
1242
 
1042
1243
  // src/cli/commands/ready.ts
1043
1244
  function readyCommand(program2) {
1044
- program2.command("ready").description("Show issues ready for work (no open blockers)").action(async () => {
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) => {
1045
1246
  const pretty = program2.opts().pretty ?? false;
1046
1247
  try {
1047
1248
  getOrCreatePebbleDir();
1048
1249
  const issues = getReady();
1049
- outputIssueList(issues, pretty);
1250
+ 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);
1258
+ } else {
1259
+ outputIssueList(issues, pretty);
1260
+ }
1050
1261
  } catch (error) {
1051
1262
  outputError(error, pretty);
1052
1263
  }
@@ -1055,12 +1266,27 @@ function readyCommand(program2) {
1055
1266
 
1056
1267
  // src/cli/commands/blocked.ts
1057
1268
  function blockedCommand(program2) {
1058
- program2.command("blocked").description("Show blocked issues (have open blockers)").action(async () => {
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) => {
1059
1270
  const pretty = program2.opts().pretty ?? false;
1060
1271
  try {
1061
1272
  getOrCreatePebbleDir();
1062
1273
  const issues = getBlocked();
1063
- outputIssueList(issues, pretty);
1274
+ if (options.verbose) {
1275
+ const verboseIssues = issues.map((issue) => {
1276
+ const allBlockers = getBlockers(issue.id);
1277
+ const openBlockers = allBlockers.filter((b) => b.status !== "closed").map((b) => b.id);
1278
+ return {
1279
+ issue,
1280
+ blocking: getBlocking(issue.id).map((i) => i.id),
1281
+ children: getChildren(issue.id).length,
1282
+ verifications: getVerifications(issue.id).length,
1283
+ blockers: openBlockers
1284
+ };
1285
+ });
1286
+ outputIssueListVerbose(verboseIssues, pretty);
1287
+ } else {
1288
+ outputIssueList(issues, pretty);
1289
+ }
1064
1290
  } catch (error) {
1065
1291
  outputError(error, pretty);
1066
1292
  }
@@ -1135,6 +1361,99 @@ function depCommand(program2) {
1135
1361
  outputError(error, pretty);
1136
1362
  }
1137
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
+ });
1138
1457
  dep.command("list <id>").description("List dependencies for an issue").action(async (id) => {
1139
1458
  const pretty = program2.opts().pretty ?? false;
1140
1459
  try {
@@ -1145,13 +1464,15 @@ function depCommand(program2) {
1145
1464
  }
1146
1465
  const blockedBy = getBlockers(resolvedId);
1147
1466
  const blocking = getBlocking(resolvedId);
1467
+ const related = getRelated(resolvedId);
1148
1468
  if (pretty) {
1149
- console.log(formatDepsPretty(resolvedId, blockedBy, blocking));
1469
+ console.log(formatDepsPretty(resolvedId, blockedBy, blocking, related));
1150
1470
  } else {
1151
1471
  console.log(formatJson({
1152
1472
  issueId: resolvedId,
1153
1473
  blockedBy: blockedBy.map((i) => ({ id: i.id, title: i.title, status: i.status })),
1154
- 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 }))
1155
1476
  }));
1156
1477
  }
1157
1478
  } catch (error) {
@@ -1781,7 +2102,7 @@ data: ${message}
1781
2102
  return;
1782
2103
  }
1783
2104
  if (updates.status) {
1784
- const validStatuses = ["open", "in_progress", "blocked"];
2105
+ const validStatuses = ["open", "in_progress", "blocked", "pending_verification"];
1785
2106
  if (!validStatuses.includes(updates.status)) {
1786
2107
  res.status(400).json({
1787
2108
  error: `Invalid status: ${updates.status}. Use close endpoint to close issues.`
@@ -2439,15 +2760,19 @@ function generateSuffix() {
2439
2760
  import * as fs4 from "fs";
2440
2761
  import * as path4 from "path";
2441
2762
  function mergeEvents(filePaths) {
2442
- const allEvents = [];
2763
+ const seen = /* @__PURE__ */ new Map();
2443
2764
  for (const filePath of filePaths) {
2444
2765
  const events = readEventsFromFile(filePath);
2445
2766
  for (const event of events) {
2446
- allEvents.push({ ...event, _source: filePath });
2767
+ const key = `${event.issueId}-${event.timestamp}-${event.type}`;
2768
+ if (!seen.has(key)) {
2769
+ seen.set(key, event);
2770
+ }
2447
2771
  }
2448
2772
  }
2773
+ const allEvents = Array.from(seen.values());
2449
2774
  allEvents.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
2450
- return allEvents.map(({ _source, ...event }) => event);
2775
+ return allEvents;
2451
2776
  }
2452
2777
  function mergeIssues(filePaths) {
2453
2778
  const merged = /* @__PURE__ */ new Map();
@@ -2808,6 +3133,29 @@ function verificationsCommand(program2) {
2808
3133
  });
2809
3134
  }
2810
3135
 
3136
+ // src/cli/commands/init.ts
3137
+ import * as path5 from "path";
3138
+ function initCommand(program2) {
3139
+ program2.command("init").description("Initialize a new .pebble directory in the current directory").option("--force", "Re-initialize even if .pebble already exists").action((options) => {
3140
+ const existing = discoverPebbleDir();
3141
+ if (existing && !options.force) {
3142
+ console.error(JSON.stringify({
3143
+ error: "Already initialized",
3144
+ path: existing,
3145
+ hint: "Use --force to re-initialize"
3146
+ }));
3147
+ process.exit(1);
3148
+ }
3149
+ const pebbleDir = ensurePebbleDir(process.cwd());
3150
+ console.log(JSON.stringify({
3151
+ initialized: true,
3152
+ path: pebbleDir,
3153
+ configPath: path5.join(pebbleDir, "config.json"),
3154
+ issuesPath: path5.join(pebbleDir, "issues.jsonl")
3155
+ }));
3156
+ });
3157
+ }
3158
+
2811
3159
  // src/cli/index.ts
2812
3160
  var program = new Command();
2813
3161
  program.name("pebble").description("A lightweight JSONL-based issue tracker").version("0.1.0");
@@ -2831,5 +3179,6 @@ summaryCommand(program);
2831
3179
  historyCommand(program);
2832
3180
  searchCommand(program);
2833
3181
  verificationsCommand(program);
3182
+ initCommand(program);
2834
3183
  program.parse();
2835
3184
  //# sourceMappingURL=index.js.map