@markmdev/pebble 0.1.11 → 0.1.13

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
@@ -37,7 +37,7 @@ var TYPE_LABELS = {
37
37
 
38
38
  // src/cli/lib/storage.ts
39
39
  import * as fs from "fs";
40
- import * as path from "path";
40
+ import * as path2 from "path";
41
41
 
42
42
  // src/cli/lib/id.ts
43
43
  import * as crypto from "crypto";
@@ -59,23 +59,78 @@ function derivePrefix(folderName) {
59
59
  return clean.slice(0, 4).toUpperCase().padEnd(4, "X");
60
60
  }
61
61
 
62
+ // src/cli/lib/git.ts
63
+ import { execSync } from "child_process";
64
+ import path from "path";
65
+ function getMainWorktreeRoot() {
66
+ try {
67
+ const gitCommonDir = execSync("git rev-parse --git-common-dir", {
68
+ encoding: "utf-8",
69
+ stdio: ["pipe", "pipe", "pipe"]
70
+ }).trim();
71
+ const gitDir = execSync("git rev-parse --git-dir", {
72
+ encoding: "utf-8",
73
+ stdio: ["pipe", "pipe", "pipe"]
74
+ }).trim();
75
+ const normalizedCommon = path.resolve(gitCommonDir);
76
+ const normalizedGit = path.resolve(gitDir);
77
+ if (normalizedCommon === normalizedGit) {
78
+ return null;
79
+ }
80
+ const mainRoot = path.dirname(normalizedCommon);
81
+ return mainRoot;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
62
87
  // src/cli/lib/storage.ts
63
88
  var PEBBLE_DIR = ".pebble";
64
89
  var ISSUES_FILE = "issues.jsonl";
65
90
  var CONFIG_FILE = "config.json";
91
+ function getConfigSafe(pebbleDir) {
92
+ try {
93
+ const configPath = path2.join(pebbleDir, CONFIG_FILE);
94
+ if (!fs.existsSync(configPath)) {
95
+ return null;
96
+ }
97
+ const content = fs.readFileSync(configPath, "utf-8");
98
+ return JSON.parse(content);
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+ function resolveWorktreePebbleDir(localPebbleDir) {
104
+ if (process.env.PEBBLE_LOCAL === "1") {
105
+ return localPebbleDir;
106
+ }
107
+ const config = getConfigSafe(localPebbleDir);
108
+ if (config?.useMainTreePebble === false) {
109
+ return localPebbleDir;
110
+ }
111
+ const mainRoot = getMainWorktreeRoot();
112
+ if (!mainRoot) {
113
+ return localPebbleDir;
114
+ }
115
+ const mainPebble = path2.join(mainRoot, PEBBLE_DIR);
116
+ if (fs.existsSync(mainPebble) && fs.statSync(mainPebble).isDirectory()) {
117
+ return mainPebble;
118
+ }
119
+ return localPebbleDir;
120
+ }
66
121
  function discoverPebbleDir(startDir = process.cwd()) {
67
- let currentDir = path.resolve(startDir);
68
- const root = path.parse(currentDir).root;
122
+ let currentDir = path2.resolve(startDir);
123
+ const root = path2.parse(currentDir).root;
69
124
  while (currentDir !== root) {
70
- const pebbleDir = path.join(currentDir, PEBBLE_DIR);
125
+ const pebbleDir = path2.join(currentDir, PEBBLE_DIR);
71
126
  if (fs.existsSync(pebbleDir) && fs.statSync(pebbleDir).isDirectory()) {
72
- return pebbleDir;
127
+ return resolveWorktreePebbleDir(pebbleDir);
73
128
  }
74
- currentDir = path.dirname(currentDir);
129
+ currentDir = path2.dirname(currentDir);
75
130
  }
76
- const rootPebble = path.join(root, PEBBLE_DIR);
131
+ const rootPebble = path2.join(root, PEBBLE_DIR);
77
132
  if (fs.existsSync(rootPebble) && fs.statSync(rootPebble).isDirectory()) {
78
- return rootPebble;
133
+ return resolveWorktreePebbleDir(rootPebble);
79
134
  }
80
135
  return null;
81
136
  }
@@ -87,23 +142,35 @@ function getPebbleDir() {
87
142
  return dir;
88
143
  }
89
144
  function ensurePebbleDir(baseDir = process.cwd()) {
90
- const pebbleDir = path.join(baseDir, PEBBLE_DIR);
145
+ let targetDir = baseDir;
146
+ if (process.env.PEBBLE_LOCAL !== "1") {
147
+ const mainRoot = getMainWorktreeRoot();
148
+ if (mainRoot) {
149
+ const mainPebble = path2.join(mainRoot, PEBBLE_DIR);
150
+ if (fs.existsSync(mainPebble) && fs.statSync(mainPebble).isDirectory()) {
151
+ return mainPebble;
152
+ }
153
+ targetDir = mainRoot;
154
+ }
155
+ }
156
+ const pebbleDir = path2.join(targetDir, PEBBLE_DIR);
91
157
  if (!fs.existsSync(pebbleDir)) {
92
158
  fs.mkdirSync(pebbleDir, { recursive: true });
93
- const folderName = path.basename(baseDir);
159
+ const folderName = path2.basename(targetDir);
94
160
  const config = {
95
161
  prefix: derivePrefix(folderName),
96
- version: "0.1.0"
162
+ version: "0.1.0",
163
+ useMainTreePebble: true
97
164
  };
98
165
  setConfig(config, pebbleDir);
99
- const issuesPath = path.join(pebbleDir, ISSUES_FILE);
166
+ const issuesPath = path2.join(pebbleDir, ISSUES_FILE);
100
167
  fs.writeFileSync(issuesPath, "", "utf-8");
101
168
  }
102
169
  return pebbleDir;
103
170
  }
104
171
  function getIssuesPath(pebbleDir) {
105
172
  const dir = pebbleDir ?? getPebbleDir();
106
- return path.join(dir, ISSUES_FILE);
173
+ return path2.join(dir, ISSUES_FILE);
107
174
  }
108
175
  function appendEvent(event, pebbleDir) {
109
176
  const issuesPath = getIssuesPath(pebbleDir);
@@ -129,7 +196,7 @@ function readEvents(pebbleDir) {
129
196
  if (!dir) {
130
197
  return [];
131
198
  }
132
- const issuesPath = path.join(dir, ISSUES_FILE);
199
+ const issuesPath = path2.join(dir, ISSUES_FILE);
133
200
  if (!fs.existsSync(issuesPath)) {
134
201
  return [];
135
202
  }
@@ -145,7 +212,7 @@ function readEvents(pebbleDir) {
145
212
  }
146
213
  function getConfigPath(pebbleDir) {
147
214
  const dir = pebbleDir ?? getPebbleDir();
148
- return path.join(dir, CONFIG_FILE);
215
+ return path2.join(dir, CONFIG_FILE);
149
216
  }
150
217
  function getConfig(pebbleDir) {
151
218
  const configPath = getConfigPath(pebbleDir);
@@ -157,7 +224,7 @@ function getConfig(pebbleDir) {
157
224
  }
158
225
  function setConfig(config, pebbleDir) {
159
226
  const dir = pebbleDir ?? getPebbleDir();
160
- const configPath = path.join(dir, CONFIG_FILE);
227
+ const configPath = path2.join(dir, CONFIG_FILE);
161
228
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
162
229
  }
163
230
  function getOrCreatePebbleDir() {
@@ -491,6 +558,21 @@ function getOpenBlockers(issueId) {
491
558
  const state = computeState(events);
492
559
  return issue.blockedBy.map((id) => state.get(id)).filter((i) => i !== void 0 && i.status !== "closed");
493
560
  }
561
+ function getComputedState() {
562
+ const events = readEvents();
563
+ return computeState(events);
564
+ }
565
+ function getAncestryChain(issueId, state) {
566
+ const chain = [];
567
+ let current = state.get(issueId);
568
+ while (current?.parent) {
569
+ const parent = state.get(current.parent);
570
+ if (!parent) break;
571
+ chain.push({ id: parent.id, title: parent.title });
572
+ current = parent;
573
+ }
574
+ return chain;
575
+ }
494
576
 
495
577
  // src/shared/time.ts
496
578
  import { formatDistanceToNow, parseISO } from "date-fns";
@@ -536,7 +618,10 @@ function formatIssueDetailPretty(issue, ctx) {
536
618
  lines.push(`Type: ${formatType(issue.type)}`);
537
619
  lines.push(`Priority: ${formatPriority(issue.priority)}`);
538
620
  lines.push(`Status: ${formatStatus(issue.status)}`);
539
- if (issue.parent) {
621
+ if (ctx.ancestry && ctx.ancestry.length > 0) {
622
+ const chain = [...ctx.ancestry].reverse().map((a) => a.id).join(" > ");
623
+ lines.push(`Ancestry: ${chain}`);
624
+ } else if (issue.parent) {
540
625
  lines.push(`Parent: ${issue.parent}`);
541
626
  }
542
627
  if (issue.description) {
@@ -602,7 +687,8 @@ function outputIssueDetail(issue, ctx, pretty) {
602
687
  blocking: ctx.blocking.map((i) => i.id),
603
688
  children: ctx.children.map((i) => ({ id: i.id, title: i.title, status: i.status })),
604
689
  verifications: ctx.verifications.map((i) => ({ id: i.id, title: i.title, status: i.status })),
605
- related: ctx.related.map((i) => i.id)
690
+ related: ctx.related.map((i) => i.id),
691
+ ...ctx.ancestry && ctx.ancestry.length > 0 && { ancestry: ctx.ancestry }
606
692
  };
607
693
  console.log(formatJson(output));
608
694
  }
@@ -798,11 +884,12 @@ function formatIssueListVerbose(issues, sectionHeader) {
798
884
  lines.push("");
799
885
  }
800
886
  for (const info of issues) {
801
- const { issue, blocking, children, verifications, blockers, parent } = info;
887
+ const { issue, blocking, children, verifications, blockers, ancestry } = info;
802
888
  lines.push(`${issue.id}: ${issue.title}`);
803
889
  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})`);
890
+ if (ancestry.length > 0) {
891
+ const chain = [...ancestry].reverse().map((a) => a.id).join(" > ");
892
+ lines.push(` Ancestry: ${chain}`);
806
893
  }
807
894
  if (blocking.length > 0) {
808
895
  lines.push(` Blocking: ${blocking.join(", ")}`);
@@ -824,13 +911,13 @@ function outputIssueListVerbose(issues, pretty, sectionHeader, limitInfo) {
824
911
  console.log(formatLimitMessage(limitInfo));
825
912
  }
826
913
  } else {
827
- const output = issues.map(({ issue, blocking, children, verifications, blockers, parent }) => ({
914
+ const output = issues.map(({ issue, blocking, children, verifications, blockers, ancestry }) => ({
828
915
  ...issue,
829
916
  blocking,
830
917
  childrenCount: issue.type === "epic" ? children : void 0,
831
918
  verificationsCount: verifications,
832
919
  ...blockers && { openBlockers: blockers },
833
- ...parent && { parentInfo: parent }
920
+ ...ancestry.length > 0 && { ancestry }
834
921
  }));
835
922
  if (limitInfo?.limited) {
836
923
  console.log(formatJson({ issues: output, _meta: limitInfo }));
@@ -1137,11 +1224,31 @@ function closeCommand(program2) {
1137
1224
  };
1138
1225
  appendEvent(closeEvent, pebbleDir);
1139
1226
  const unblocked = getNewlyUnblocked(resolvedId);
1227
+ let autoClosed;
1228
+ if (issue.verifies) {
1229
+ const targetIssue = getIssue(issue.verifies);
1230
+ if (targetIssue && targetIssue.status === "pending_verification") {
1231
+ const remainingVerifications = getVerifications(issue.verifies).filter((v) => v.status !== "closed");
1232
+ if (remainingVerifications.length === 0) {
1233
+ const autoCloseEvent = {
1234
+ type: "close",
1235
+ issueId: issue.verifies,
1236
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1237
+ data: {
1238
+ reason: "All verifications completed"
1239
+ }
1240
+ };
1241
+ appendEvent(autoCloseEvent, pebbleDir);
1242
+ autoClosed = { id: targetIssue.id, title: targetIssue.title };
1243
+ }
1244
+ }
1245
+ }
1140
1246
  results.push({
1141
1247
  id: resolvedId,
1142
1248
  success: true,
1143
1249
  status: "closed",
1144
- unblocked: unblocked.length > 0 ? unblocked.map((i) => ({ id: i.id, title: i.title })) : void 0
1250
+ unblocked: unblocked.length > 0 ? unblocked.map((i) => ({ id: i.id, title: i.title })) : void 0,
1251
+ autoClosed
1145
1252
  });
1146
1253
  } catch (error) {
1147
1254
  results.push({ id, success: false, error: error.message });
@@ -1169,6 +1276,10 @@ Unblocked:`);
1169
1276
  console.log(` \u2192 ${u.id} - ${u.title}`);
1170
1277
  }
1171
1278
  }
1279
+ if (result.autoClosed) {
1280
+ console.log(`
1281
+ \u2713 ${result.autoClosed.id} auto-closed (all verifications complete)`);
1282
+ }
1172
1283
  }
1173
1284
  } else {
1174
1285
  console.log(formatJson({
@@ -1177,7 +1288,8 @@ Unblocked:`);
1177
1288
  status: result.status,
1178
1289
  ...result.pendingVerifications && { pendingVerifications: result.pendingVerifications },
1179
1290
  ...result.pendingVerifications && { hint: `pb verifications ${result.id}` },
1180
- ...result.unblocked && { unblocked: result.unblocked }
1291
+ ...result.unblocked && { unblocked: result.unblocked },
1292
+ ...result.autoClosed && { autoClosed: result.autoClosed }
1181
1293
  }));
1182
1294
  }
1183
1295
  } else {
@@ -1200,6 +1312,9 @@ Unblocked:`);
1200
1312
  console.log(` \u2192 ${u.id} - ${u.title}`);
1201
1313
  }
1202
1314
  }
1315
+ if (result.autoClosed) {
1316
+ console.log(` \u2713 ${result.autoClosed.id} auto-closed (all verifications complete)`);
1317
+ }
1203
1318
  }
1204
1319
  } else {
1205
1320
  console.log(`\u2717 ${result.id}: ${result.error}`);
@@ -1213,7 +1328,8 @@ Unblocked:`);
1213
1328
  ...r.error && { error: r.error },
1214
1329
  ...r.pendingVerifications && { pendingVerifications: r.pendingVerifications },
1215
1330
  ...r.pendingVerifications && { hint: `pb verifications ${r.id}` },
1216
- ...r.unblocked && { unblocked: r.unblocked }
1331
+ ...r.unblocked && { unblocked: r.unblocked },
1332
+ ...r.autoClosed && { autoClosed: r.autoClosed }
1217
1333
  }))));
1218
1334
  }
1219
1335
  }
@@ -1374,20 +1490,15 @@ function listCommand(program2) {
1374
1490
  limited: limit > 0 && total > limit
1375
1491
  };
1376
1492
  if (options.verbose) {
1493
+ const state = getComputedState();
1377
1494
  const verboseIssues = issues.map((issue) => {
1378
- const info = {
1495
+ return {
1379
1496
  issue,
1380
1497
  blocking: getBlocking(issue.id).map((i) => i.id),
1381
1498
  children: getChildren(issue.id).length,
1382
- verifications: getVerifications(issue.id).length
1499
+ verifications: getVerifications(issue.id).length,
1500
+ ancestry: getAncestryChain(issue.id, state)
1383
1501
  };
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
1502
  });
1392
1503
  let sectionHeader = "Issues";
1393
1504
  if (filters.status) {
@@ -1424,7 +1535,9 @@ function showCommand(program2) {
1424
1535
  const children = issue.type === "epic" ? getChildren(resolvedId) : [];
1425
1536
  const verifications = getVerifications(resolvedId);
1426
1537
  const related = getRelated(resolvedId);
1427
- outputIssueDetail(issue, { blocking, children, verifications, related }, pretty);
1538
+ const state = getComputedState();
1539
+ const ancestry = getAncestryChain(resolvedId, state);
1540
+ outputIssueDetail(issue, { blocking, children, verifications, related, ancestry }, pretty);
1428
1541
  } catch (error) {
1429
1542
  outputError(error, pretty);
1430
1543
  }
@@ -1457,20 +1570,15 @@ function readyCommand(program2) {
1457
1570
  limited: limit > 0 && total > limit
1458
1571
  };
1459
1572
  if (options.verbose) {
1573
+ const state = getComputedState();
1460
1574
  const verboseIssues = issues.map((issue) => {
1461
- const info = {
1575
+ return {
1462
1576
  issue,
1463
1577
  blocking: getBlocking(issue.id).map((i) => i.id),
1464
1578
  children: getChildren(issue.id).length,
1465
- verifications: getVerifications(issue.id).length
1579
+ verifications: getVerifications(issue.id).length,
1580
+ ancestry: getAncestryChain(issue.id, state)
1466
1581
  };
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
1582
  });
1475
1583
  outputIssueListVerbose(verboseIssues, pretty, "Ready Issues", limitInfo);
1476
1584
  } else {
@@ -1501,23 +1609,18 @@ function blockedCommand(program2) {
1501
1609
  limited: limit > 0 && total > limit
1502
1610
  };
1503
1611
  if (options.verbose) {
1612
+ const state = getComputedState();
1504
1613
  const verboseIssues = issues.map((issue) => {
1505
1614
  const allBlockers = getBlockers(issue.id);
1506
1615
  const openBlockers = allBlockers.filter((b) => b.status !== "closed").map((b) => b.id);
1507
- const info = {
1616
+ return {
1508
1617
  issue,
1509
1618
  blocking: getBlocking(issue.id).map((i) => i.id),
1510
1619
  children: getChildren(issue.id).length,
1511
1620
  verifications: getVerifications(issue.id).length,
1512
- blockers: openBlockers
1621
+ blockers: openBlockers,
1622
+ ancestry: getAncestryChain(issue.id, state)
1513
1623
  };
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;
1521
1624
  });
1522
1625
  outputIssueListVerbose(verboseIssues, pretty, "Blocked Issues", limitInfo);
1523
1626
  } else {
@@ -2007,7 +2110,7 @@ function formatGraphPretty(issues) {
2007
2110
  import express from "express";
2008
2111
  import cors from "cors";
2009
2112
  import { fileURLToPath } from "url";
2010
- import path2 from "path";
2113
+ import path3 from "path";
2011
2114
  import fs2 from "fs";
2012
2115
  import net from "net";
2013
2116
  import open from "open";
@@ -2102,14 +2205,14 @@ function uiCommand(program2) {
2102
2205
  console.error("Error: --files option requires at least one path");
2103
2206
  process.exit(1);
2104
2207
  }
2105
- issueFiles = issueFiles.map((p) => path2.resolve(process.cwd(), p));
2208
+ issueFiles = issueFiles.map((p) => path3.resolve(process.cwd(), p));
2106
2209
  console.log(`Multi-worktree mode: watching ${issueFiles.length} file(s)`);
2107
2210
  for (const f of issueFiles) {
2108
2211
  console.log(` - ${f}`);
2109
2212
  }
2110
2213
  } else {
2111
2214
  const pebbleDir = getOrCreatePebbleDir();
2112
- issueFiles = [path2.join(pebbleDir, "issues.jsonl")];
2215
+ issueFiles = [path3.join(pebbleDir, "issues.jsonl")];
2113
2216
  }
2114
2217
  if (!options.files) {
2115
2218
  getOrCreatePebbleDir();
@@ -2132,7 +2235,7 @@ function uiCommand(program2) {
2132
2235
  res.status(400).json({ error: "path is required" });
2133
2236
  return;
2134
2237
  }
2135
- const resolved = path2.resolve(process.cwd(), filePath);
2238
+ const resolved = path3.resolve(process.cwd(), filePath);
2136
2239
  if (!fs2.existsSync(resolved)) {
2137
2240
  res.status(400).json({ error: `File not found: ${filePath}` });
2138
2241
  return;
@@ -2170,10 +2273,10 @@ function uiCommand(program2) {
2170
2273
  });
2171
2274
  app.get("/api/worktrees", (_req, res) => {
2172
2275
  try {
2173
- const { execSync } = __require("child_process");
2276
+ const { execSync: execSync2 } = __require("child_process");
2174
2277
  let worktreeOutput;
2175
2278
  try {
2176
- worktreeOutput = execSync("git worktree list --porcelain", {
2279
+ worktreeOutput = execSync2("git worktree list --porcelain", {
2177
2280
  encoding: "utf-8",
2178
2281
  cwd: process.cwd()
2179
2282
  });
@@ -2195,7 +2298,7 @@ function uiCommand(program2) {
2195
2298
  }
2196
2299
  }
2197
2300
  if (worktreePath) {
2198
- const issuesFile = path2.join(worktreePath, ".pebble", "issues.jsonl");
2301
+ const issuesFile = path3.join(worktreePath, ".pebble", "issues.jsonl");
2199
2302
  const hasIssues = fs2.existsSync(issuesFile);
2200
2303
  const isActive = issueFiles.includes(issuesFile);
2201
2304
  let issueCount = 0;
@@ -2289,7 +2392,7 @@ data: ${message}
2289
2392
  }
2290
2393
  targetFile = issueFiles[targetIndex];
2291
2394
  }
2292
- const pebbleDir = targetFile ? path2.dirname(targetFile) : getOrCreatePebbleDir();
2395
+ const pebbleDir = targetFile ? path3.dirname(targetFile) : getOrCreatePebbleDir();
2293
2396
  const config = getConfig(pebbleDir);
2294
2397
  const { title, type, priority, description, parent } = req.body;
2295
2398
  if (!title || typeof title !== "string") {
@@ -2480,7 +2583,7 @@ data: ${message}
2480
2583
  return;
2481
2584
  }
2482
2585
  issue = localIssue;
2483
- targetFile = path2.join(pebbleDir, "issues.jsonl");
2586
+ targetFile = path3.join(pebbleDir, "issues.jsonl");
2484
2587
  }
2485
2588
  const { title, type, priority, status, description, parent, relatedTo } = req.body;
2486
2589
  const updates = {};
@@ -2608,7 +2711,7 @@ data: ${message}
2608
2711
  return;
2609
2712
  }
2610
2713
  issue = localIssue;
2611
- targetFile = path2.join(pebbleDir, "issues.jsonl");
2714
+ targetFile = path3.join(pebbleDir, "issues.jsonl");
2612
2715
  }
2613
2716
  if (issue.status === "closed") {
2614
2717
  res.status(400).json({ error: "Issue is already closed" });
@@ -2651,11 +2754,49 @@ data: ${message}
2651
2754
  data: { reason }
2652
2755
  };
2653
2756
  appendEventToFile(event, targetFile);
2757
+ let autoClosed;
2758
+ if (issue.verifies) {
2759
+ let targetIssue;
2760
+ let targetVerifications = [];
2761
+ if (isMultiWorktree()) {
2762
+ const found = findIssueInSources(issue.verifies, issueFiles);
2763
+ if (found) {
2764
+ targetIssue = found.issue;
2765
+ const allIssues = mergeIssuesFromFiles(issueFiles);
2766
+ targetVerifications = allIssues.filter(
2767
+ (i) => i.verifies === issue.verifies && i.status !== "closed"
2768
+ );
2769
+ }
2770
+ } else {
2771
+ targetIssue = getIssue(issue.verifies);
2772
+ targetVerifications = getVerifications(issue.verifies).filter((v) => v.status !== "closed");
2773
+ }
2774
+ if (targetIssue && targetIssue.status === "pending_verification" && targetVerifications.length === 0) {
2775
+ const autoCloseEvent = {
2776
+ type: "close",
2777
+ issueId: issue.verifies,
2778
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2779
+ data: { reason: "All verifications completed" }
2780
+ };
2781
+ if (isMultiWorktree()) {
2782
+ const targetFound = findIssueInSources(issue.verifies, issueFiles);
2783
+ if (targetFound) {
2784
+ appendEventToFile(autoCloseEvent, targetFound.targetFile);
2785
+ }
2786
+ } else {
2787
+ const pebbleDir = getOrCreatePebbleDir();
2788
+ appendEventToFile(autoCloseEvent, path3.join(pebbleDir, "issues.jsonl"));
2789
+ }
2790
+ autoClosed = { id: targetIssue.id, title: targetIssue.title };
2791
+ }
2792
+ }
2654
2793
  if (isMultiWorktree()) {
2655
2794
  const updated = findIssueInSources(issueId, issueFiles);
2656
- res.json(updated?.issue || { ...issue, status: "closed", updatedAt: timestamp });
2795
+ const result = updated?.issue || { ...issue, status: "closed", updatedAt: timestamp };
2796
+ res.json(autoClosed ? { ...result, _autoClosed: autoClosed } : result);
2657
2797
  } else {
2658
- res.json(getIssue(issueId));
2798
+ const result = getIssue(issueId);
2799
+ res.json(autoClosed ? { ...result, _autoClosed: autoClosed } : result);
2659
2800
  }
2660
2801
  } catch (error) {
2661
2802
  res.status(500).json({ error: error.message });
@@ -2684,7 +2825,7 @@ data: ${message}
2684
2825
  return;
2685
2826
  }
2686
2827
  issue = localIssue;
2687
- targetFile = path2.join(pebbleDir, "issues.jsonl");
2828
+ targetFile = path3.join(pebbleDir, "issues.jsonl");
2688
2829
  }
2689
2830
  if (issue.status !== "closed") {
2690
2831
  res.status(400).json({ error: "Issue is not closed" });
@@ -2732,7 +2873,7 @@ data: ${message}
2732
2873
  return;
2733
2874
  }
2734
2875
  issue = localIssue;
2735
- targetFile = path2.join(pebbleDir, "issues.jsonl");
2876
+ targetFile = path3.join(pebbleDir, "issues.jsonl");
2736
2877
  }
2737
2878
  const { text, author } = req.body;
2738
2879
  if (!text || typeof text !== "string" || text.trim() === "") {
@@ -2784,7 +2925,7 @@ data: ${message}
2784
2925
  return;
2785
2926
  }
2786
2927
  issue = localIssue;
2787
- targetFile = path2.join(pebbleDir, "issues.jsonl");
2928
+ targetFile = path3.join(pebbleDir, "issues.jsonl");
2788
2929
  }
2789
2930
  const { blockerId } = req.body;
2790
2931
  if (!blockerId) {
@@ -2858,7 +2999,7 @@ data: ${message}
2858
2999
  return;
2859
3000
  }
2860
3001
  issue = localIssue;
2861
- targetFile = path2.join(pebbleDir, "issues.jsonl");
3002
+ targetFile = path3.join(pebbleDir, "issues.jsonl");
2862
3003
  }
2863
3004
  let resolvedBlockerId;
2864
3005
  if (isMultiWorktree()) {
@@ -2897,11 +3038,11 @@ data: ${message}
2897
3038
  }
2898
3039
  });
2899
3040
  const __filename2 = fileURLToPath(import.meta.url);
2900
- const __dirname2 = path2.dirname(__filename2);
2901
- const uiPath = path2.resolve(__dirname2, "../ui");
3041
+ const __dirname2 = path3.dirname(__filename2);
3042
+ const uiPath = path3.resolve(__dirname2, "../ui");
2902
3043
  app.use(express.static(uiPath));
2903
3044
  app.get("*", (_req, res) => {
2904
- res.sendFile(path2.join(uiPath, "index.html"));
3045
+ res.sendFile(path3.join(uiPath, "index.html"));
2905
3046
  });
2906
3047
  const requestedPort = parseInt(options.port, 10);
2907
3048
  const actualPort = await findAvailablePort(requestedPort);
@@ -2925,7 +3066,7 @@ data: ${message}
2925
3066
  // src/cli/commands/import.ts
2926
3067
  import * as fs3 from "fs";
2927
3068
  import * as readline from "readline";
2928
- import * as path3 from "path";
3069
+ import * as path4 from "path";
2929
3070
  function importCommand(program2) {
2930
3071
  program2.command("import <file>").description("Import issues from a Beads issues.jsonl file").option("--dry-run", "Show what would be imported without writing").option("--prefix <prefix>", "Override the ID prefix (default: derive from folder name)").action(async (file, options) => {
2931
3072
  const pretty = program2.opts().pretty ?? false;
@@ -2938,7 +3079,7 @@ function importCommand(program2) {
2938
3079
  console.log(pretty ? "No issues found in file." : formatJson({ imported: 0 }));
2939
3080
  return;
2940
3081
  }
2941
- const prefix = options.prefix ?? derivePrefix(path3.basename(process.cwd()));
3082
+ const prefix = options.prefix ?? derivePrefix(path4.basename(process.cwd()));
2942
3083
  const { events, idMap, stats } = convertToPebbleEvents(beadsIssues, prefix);
2943
3084
  if (options.dryRun) {
2944
3085
  if (pretty) {
@@ -3116,7 +3257,7 @@ function generateSuffix() {
3116
3257
 
3117
3258
  // src/cli/commands/merge.ts
3118
3259
  import * as fs4 from "fs";
3119
- import * as path4 from "path";
3260
+ import * as path5 from "path";
3120
3261
  function mergeEvents(filePaths) {
3121
3262
  const seen = /* @__PURE__ */ new Map();
3122
3263
  for (const filePath of filePaths) {
@@ -3159,7 +3300,7 @@ function mergeCommand(program2) {
3159
3300
  const pretty = program2.opts().pretty ?? false;
3160
3301
  const filePaths = [];
3161
3302
  for (const file of files) {
3162
- const resolved = path4.resolve(process.cwd(), file);
3303
+ const resolved = path5.resolve(process.cwd(), file);
3163
3304
  if (!fs4.existsSync(resolved)) {
3164
3305
  console.error(`Error: File not found: ${file}`);
3165
3306
  process.exit(1);
@@ -3561,7 +3702,7 @@ function verificationsCommand(program2) {
3561
3702
  }
3562
3703
 
3563
3704
  // src/cli/commands/init.ts
3564
- import * as path5 from "path";
3705
+ import * as path6 from "path";
3565
3706
  function initCommand(program2) {
3566
3707
  program2.command("init").description("Initialize a new .pebble directory in the current directory").option("--force", "Re-initialize even if .pebble already exists").action((options) => {
3567
3708
  const existing = discoverPebbleDir();
@@ -3577,8 +3718,8 @@ function initCommand(program2) {
3577
3718
  console.log(JSON.stringify({
3578
3719
  initialized: true,
3579
3720
  path: pebbleDir,
3580
- configPath: path5.join(pebbleDir, "config.json"),
3581
- issuesPath: path5.join(pebbleDir, "issues.jsonl")
3721
+ configPath: path6.join(pebbleDir, "config.json"),
3722
+ issuesPath: path6.join(pebbleDir, "issues.jsonl")
3582
3723
  }));
3583
3724
  });
3584
3725
  }
@@ -3588,6 +3729,12 @@ var program = new Command();
3588
3729
  program.name("pebble").description("A lightweight JSONL-based issue tracker").version("0.1.0");
3589
3730
  program.option("-P, --pretty", "Human-readable output (default: JSON)");
3590
3731
  program.option("--json", "JSON output (this is the default, flag not needed)");
3732
+ program.option("--local", "Use local .pebble directory even in a git worktree");
3733
+ program.hook("preAction", () => {
3734
+ if (program.opts().local) {
3735
+ process.env.PEBBLE_LOCAL = "1";
3736
+ }
3737
+ });
3591
3738
  createCommand(program);
3592
3739
  updateCommand(program);
3593
3740
  closeCommand(program);