kantban-cli 0.1.49 → 0.1.51

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runGates
3
- } from "./chunk-DENXSVKE.js";
3
+ } from "./chunk-GQQUH3TA.js";
4
4
  import {
5
5
  ClaudeProvider,
6
6
  RalphLoop,
@@ -8,12 +8,13 @@ import {
8
8
  cleanupGateProxyConfigs,
9
9
  cleanupMcpConfig,
10
10
  composeStuckDetectionPrompt,
11
+ detectBranchMerged,
11
12
  generateGateProxyMcpConfig,
12
13
  generateMcpConfig,
13
14
  parseJsonFromLlmOutput,
14
15
  parseStuckDetectionResponse,
15
16
  reapOrphanedMcpConfigDirs
16
- } from "./chunk-QHJZIGEE.js";
17
+ } from "./chunk-X2CJ3ZAI.js";
17
18
  import {
18
19
  LoopCheckpointSchema,
19
20
  VerdictSchema,
@@ -31,9 +32,11 @@ import {
31
32
  } from "./chunk-5ZU2OOES.js";
32
33
 
33
34
  // src/commands/pipeline.ts
34
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync3, unlinkSync as unlinkSync3, existsSync as existsSync3, appendFileSync as appendFileSync2 } from "fs";
35
- import { homedir as homedir2 } from "os";
36
- import { join as join3, dirname as dirname2 } from "path";
35
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync3, unlinkSync as unlinkSync3, existsSync as existsSync3, appendFileSync as appendFileSync2, promises as fsPromises } from "fs";
36
+ import { execFile } from "child_process";
37
+ import { homedir as homedir3 } from "os";
38
+ import { join as join4, dirname as dirname2, resolve, sep } from "path";
39
+ import { promisify } from "util";
37
40
 
38
41
  // src/lib/harness-signal.ts
39
42
  var HARNESS_SIGNAL_PREFIX = "@harness:";
@@ -57,6 +60,8 @@ function resolveToolRestrictions(builtinTools, allowedTools, disallowedTools) {
57
60
 
58
61
  // src/lib/worktree.ts
59
62
  import { execFile as defaultExecFile, execFileSync } from "child_process";
63
+ import { homedir } from "os";
64
+ import { join } from "path";
60
65
  function generateWorktreeName(ticketNumber, columnName) {
61
66
  const slug = columnSlug(columnName);
62
67
  return `kantban-${ticketNumber}-${slug}`;
@@ -64,10 +69,16 @@ function generateWorktreeName(ticketNumber, columnName) {
64
69
  function columnSlug(columnName) {
65
70
  return columnName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
66
71
  }
67
- function renderWorktreePath(ticketNumber, columnName, pathPattern) {
72
+ function renderWorktreePath(ticketNumber, columnName, pathPattern, options) {
73
+ const slug = columnSlug(columnName);
68
74
  const worktreeName = generateWorktreeName(ticketNumber, columnName);
69
- if (!pathPattern) return worktreeName;
70
- return pathPattern.replace(/\{ticket_number\}/g, String(ticketNumber)).replace(/\{column_slug\}/g, columnSlug(columnName)).replace(/\{worktree_name\}/g, worktreeName);
75
+ if (pathPattern) {
76
+ return pathPattern.replace(/\{ticket_number\}/g, String(ticketNumber)).replace(/\{column_slug\}/g, slug).replace(/\{worktree_name\}/g, worktreeName);
77
+ }
78
+ return join(options.defaultRoot, options.boardId, slug, String(ticketNumber));
79
+ }
80
+ function defaultWorktreeRoot() {
81
+ return join(homedir(), ".kantban", "worktrees");
71
82
  }
72
83
  function isPlausibleRemoteUrl(url) {
73
84
  return url.includes("/") || url.includes("@") || url.includes("://");
@@ -115,22 +126,22 @@ async function cleanupWorktree(worktreeName, exec = defaultExecFile) {
115
126
  } catch {
116
127
  return true;
117
128
  }
118
- return new Promise((resolve) => {
129
+ return new Promise((resolve2) => {
119
130
  exec("git", ["worktree", "remove", "--force", target], (err) => {
120
131
  if (err) {
121
132
  console.error(`[worktree] cleanup failed for ${worktreeName} (${target}): ${err.message}`);
122
- resolve(false);
133
+ resolve2(false);
123
134
  } else {
124
- resolve(true);
135
+ resolve2(true);
125
136
  }
126
137
  });
127
138
  });
128
139
  }
129
140
  function execPromise(exec, cmd, args) {
130
- return new Promise((resolve, reject) => {
141
+ return new Promise((resolve2, reject) => {
131
142
  exec(cmd, args, (err, stdout, stderr) => {
132
143
  if (err) reject(Object.assign(err, { stdout, stderr }));
133
- else resolve({ stdout, stderr });
144
+ else resolve2({ stdout, stderr });
134
145
  });
135
146
  });
136
147
  }
@@ -942,8 +953,8 @@ var PipelineOrchestrator = class {
942
953
  }
943
954
  for (const [ticketId, columnId] of Array.from(this.deferredTickets)) {
944
955
  try {
945
- const stillBlocked = await this.deps.hasUnresolvedBlockers(ticketId);
946
- if (!stillBlocked) {
956
+ const blocker = await this.deps.hasUnresolvedBlockers(ticketId);
957
+ if (!blocker.blocked) {
947
958
  this.deferredTickets.delete(ticketId);
948
959
  await this.spawnOrQueue(ticketId, columnId, true);
949
960
  } else {
@@ -957,26 +968,56 @@ var PipelineOrchestrator = class {
957
968
  }
958
969
  await this.refreshBoardScope();
959
970
  this.blockedColumns.clear();
960
- for (const [columnId] of this.pipelineColumns) {
961
- await this.refreshColumnScope(columnId);
962
- const colScope = this.columnScopes.get(columnId);
963
- if (!colScope) {
964
- console.error(` [scan] Column ${columnId}: no cached scope`);
965
- continue;
966
- }
967
- if (this.isColumnBlocked(columnId)) {
968
- this.blockedColumns.add(columnId);
969
- console.error(` [scan] Column ${columnId} (${colScope.column.name}): BLOCKED by firing constraints \u2014 skipping ${String(colScope.tickets.length)} ticket(s)`);
971
+ await Promise.all(
972
+ Array.from(this.pipelineColumns.keys()).map(async (columnId) => {
973
+ await this.refreshColumnScope(columnId);
974
+ const colScope = this.columnScopes.get(columnId);
975
+ if (!colScope) {
976
+ console.error(` [scan] column=${columnId} reason="no cached scope"`);
977
+ return;
978
+ }
979
+ if (this.isColumnBlocked(columnId)) {
980
+ this.blockedColumns.add(columnId);
981
+ console.error(
982
+ ` [scan] column=${columnId} name="${colScope.column.name}" tickets=${colScope.tickets.length} dispatched=0 queued=0 skipped=${colScope.tickets.length} reason="firing_constraint"`
983
+ );
984
+ for (const ticket of colScope.tickets) {
985
+ await this.emitDispatchDeferred(ticket.id, "firing_constraint", { columnId });
986
+ }
987
+ return;
988
+ }
989
+ let dispatched = 0;
990
+ let queued = 0;
991
+ let skipped = 0;
992
+ const reasons = /* @__PURE__ */ new Set();
970
993
  for (const ticket of colScope.tickets) {
971
- await this.emitDispatchDeferred(ticket.id, "firing_constraint", { columnId });
994
+ const wasActive = this.activeLoops.has(ticket.id) || this.spawning.has(ticket.id);
995
+ const wasQueued = (this.loopQueues.get(columnId) ?? []).includes(ticket.id);
996
+ const wasDeferred = this.deferredTickets.has(ticket.id);
997
+ await this.spawnOrQueue(ticket.id, columnId);
998
+ const nowActive = this.activeLoops.has(ticket.id) || this.spawning.has(ticket.id);
999
+ const nowQueued = (this.loopQueues.get(columnId) ?? []).includes(ticket.id);
1000
+ const nowDeferred = this.deferredTickets.has(ticket.id);
1001
+ if (!wasActive && nowActive) {
1002
+ dispatched++;
1003
+ reasons.add("dispatched");
1004
+ } else if (!wasQueued && nowQueued) {
1005
+ queued++;
1006
+ reasons.add("capped");
1007
+ } else if (!wasDeferred && nowDeferred) {
1008
+ skipped++;
1009
+ reasons.add("blocked");
1010
+ } else {
1011
+ skipped++;
1012
+ reasons.add("known");
1013
+ }
972
1014
  }
973
- continue;
974
- }
975
- console.error(` [scan] Column ${columnId} (${colScope.column.name}): ${String(colScope.tickets.length)} ticket(s)`);
976
- for (const ticket of colScope.tickets) {
977
- await this.spawnOrQueue(ticket.id, columnId);
978
- }
979
- }
1015
+ const reason = reasons.size === 0 ? "none" : reasons.size === 1 ? [...reasons][0] : "mixed";
1016
+ console.error(
1017
+ ` [scan] column=${columnId} name="${colScope.column.name}" tickets=${colScope.tickets.length} dispatched=${dispatched} queued=${queued} skipped=${skipped} reason="${reason}"`
1018
+ );
1019
+ })
1020
+ );
980
1021
  }
981
1022
  /**
982
1023
  * Handle a pipeline event (typically from WebSocket via EventQueue).
@@ -1098,15 +1139,58 @@ var PipelineOrchestrator = class {
1098
1139
  }
1099
1140
  return;
1100
1141
  }
1142
+ const isMergeColumn = colConfig.name.toLowerCase().includes("merge");
1143
+ if (isMergeColumn && colConfig.worktreeEnabled && this.deps.detectBranchMerged && this.deps.moveTicketToColumn) {
1144
+ const ticket = this.columnScopes.get(columnId)?.tickets.find((t) => t.id === ticketId);
1145
+ if (ticket) {
1146
+ const wPath = renderWorktreePath(
1147
+ ticket.ticket_number,
1148
+ colConfig.name,
1149
+ colConfig.worktreePathPattern,
1150
+ { boardId: this.boardId, defaultRoot: defaultWorktreeRoot() }
1151
+ );
1152
+ try {
1153
+ const mergeResult = await this.deps.detectBranchMerged({
1154
+ worktreePath: wPath,
1155
+ iterationStartedAt: /* @__PURE__ */ new Date(0),
1156
+ targetBranch: colConfig.worktreeIntegrationBranch ?? "main"
1157
+ });
1158
+ if (mergeResult.merged) {
1159
+ const nextColumnId = this.findNextColumnId(columnId);
1160
+ if (nextColumnId) {
1161
+ await this.deps.createComment(
1162
+ ticketId,
1163
+ `## Merge Complete (auto-finalized)
1164
+
1165
+ Merge commit \`${mergeResult.mergeCommitSha}\` detected on \`origin/${colConfig.worktreeIntegrationBranch ?? "main"}\` at ${mergeResult.mergeCommitTime.toISOString()}. The previous agent did not record the column move before exiting; the orchestrator is finalizing the move now.`
1166
+ );
1167
+ await this.deps.moveTicketToColumn(ticketId, nextColumnId, {
1168
+ reason: "merge_finalize_recovery",
1169
+ merge_commit_sha: mergeResult.mergeCommitSha
1170
+ });
1171
+ console.error(
1172
+ ` [merge-recovery] ticket=${ticketId} column=${columnId} target=${colConfig.worktreeIntegrationBranch ?? "main"} sha=${mergeResult.mergeCommitSha}`
1173
+ );
1174
+ return;
1175
+ }
1176
+ }
1177
+ } catch {
1178
+ }
1179
+ }
1180
+ }
1101
1181
  this.spawning.add(ticketId);
1102
1182
  this.reserveSlot(columnId);
1103
1183
  try {
1104
- const blocked = await this.deps.hasUnresolvedBlockers(ticketId);
1105
- if (blocked) {
1184
+ const result = await this.deps.hasUnresolvedBlockers(ticketId);
1185
+ if (result.blocked) {
1106
1186
  this.spawning.delete(ticketId);
1107
1187
  this.releaseSlot(columnId);
1108
1188
  this.deferredTickets.set(ticketId, columnId);
1109
- console.error(` [skip] ${ticketId} has unresolved blockers \u2014 deferred`);
1189
+ const blockerSummary = result.blockers.map((b) => `${b.ticket_number}:${b.column}`).join(",");
1190
+ const linkIdSummary = result.linkIds.join(",");
1191
+ console.error(
1192
+ ` [scan] skip ticket=${ticketId} reason=blocks count=${result.blockers.length} link_ids=[${linkIdSummary}] blockers=[${blockerSummary}]`
1193
+ );
1110
1194
  void this.emitDispatchDeferred(ticketId, "unresolved_blockers", { columnId });
1111
1195
  void this.drainQueue(columnId).catch((err) => {
1112
1196
  const msg = err instanceof Error ? err.message : String(err);
@@ -1121,9 +1205,9 @@ var PipelineOrchestrator = class {
1121
1205
  this.releaseSlot(columnId);
1122
1206
  this.deferredTickets.set(ticketId, columnId);
1123
1207
  void this.emitDispatchDeferred(ticketId, "unresolved_blockers", { columnId });
1124
- void this.drainQueue(columnId).catch((err2) => {
1125
- const msg2 = err2 instanceof Error ? err2.message : String(err2);
1126
- console.error(` [error] drainQueue failed for column ${columnId}: ${msg2}`);
1208
+ void this.drainQueue(columnId).catch((dErr) => {
1209
+ const dMsg = dErr instanceof Error ? dErr.message : String(dErr);
1210
+ console.error(` [error] drainQueue failed for column ${columnId}: ${dMsg}`);
1127
1211
  });
1128
1212
  return;
1129
1213
  }
@@ -1271,16 +1355,16 @@ var PipelineOrchestrator = class {
1271
1355
  gutterThreshold: config.gutterThreshold,
1272
1356
  ...effectiveModel !== void 0 && { model: effectiveModel },
1273
1357
  ...config.maxBudgetUsd !== void 0 && { maxBudgetUsd: config.maxBudgetUsd },
1274
- // Resolve worktree name (branch) and path from ticket context. When the
1275
- // column's agent_config specifies a path_pattern, the filesystem path is
1276
- // rendered from it — otherwise the name doubles as a relative path.
1277
1358
  ...(() => {
1278
1359
  const colScope = this.columnScopes.get(columnId);
1279
1360
  const ticket = colScope?.tickets.find((t) => t.id === ticketId);
1280
1361
  if (!config.worktreeEnabled || !ticket) return {};
1281
1362
  const wName = generateWorktreeName(ticket.ticket_number, config.name);
1282
- const wPath = renderWorktreePath(ticket.ticket_number, config.name, config.worktreePathPattern);
1283
- return wPath !== wName ? { worktreeName: wName, worktreePath: wPath } : { worktreeName: wName };
1363
+ const wPath = renderWorktreePath(ticket.ticket_number, config.name, config.worktreePathPattern, {
1364
+ boardId: this.boardId,
1365
+ defaultRoot: defaultWorktreeRoot()
1366
+ });
1367
+ return { worktreeName: wName, worktreePath: wPath };
1284
1368
  })(),
1285
1369
  ...config.lookaheadColumnId !== void 0 && { lookaheadColumnId: config.lookaheadColumnId },
1286
1370
  // Resume from checkpoint iteration/gutter if provided
@@ -1799,6 +1883,19 @@ ${findingsText}`)
1799
1883
  const nextBoardCol = bs.columns.filter((c) => c.position > currentBoardCol.position && this.pipelineColumns.has(c.id)).sort((a, b) => a.position - b.position)[0];
1800
1884
  return nextBoardCol ? this.pipelineColumns.get(nextBoardCol.id) ?? null : null;
1801
1885
  }
1886
+ /**
1887
+ * Find the ID of the next board column by position, regardless of pipeline status.
1888
+ * Used by merge-recovery to target the downstream column (e.g. Done) which may not
1889
+ * be a pipeline column.
1890
+ */
1891
+ findNextColumnId(currentColumnId) {
1892
+ const bs = this.cachedBoardScope;
1893
+ if (!bs) return null;
1894
+ const currentBoardCol = bs.columns.find((c) => c.id === currentColumnId);
1895
+ if (!currentBoardCol) return null;
1896
+ const nextBoardCol = bs.columns.filter((c) => c.position > currentBoardCol.position).sort((a, b) => a.position - b.position)[0];
1897
+ return nextBoardCol?.id ?? null;
1898
+ }
1802
1899
  /**
1803
1900
  * Called when a loop finishes. Cleans up tracking and drains the queue.
1804
1901
  */
@@ -2186,8 +2283,8 @@ ${findingsText}`)
2186
2283
  this.spawning.add(nextTicketId);
2187
2284
  this.reserveSlot(columnId);
2188
2285
  try {
2189
- const blocked = await this.deps.hasUnresolvedBlockers(nextTicketId);
2190
- if (blocked) {
2286
+ const blockerResult = await this.deps.hasUnresolvedBlockers(nextTicketId);
2287
+ if (blockerResult.blocked) {
2191
2288
  this.deferredTickets.set(nextTicketId, columnId);
2192
2289
  this.spawning.delete(nextTicketId);
2193
2290
  this.releaseSlot(columnId);
@@ -2542,7 +2639,7 @@ var PipelineWsClient = class _PipelineWsClient {
2542
2639
  this.cleanupTimers();
2543
2640
  const { ticket } = await this.client.post("/ws-ticket");
2544
2641
  const wsUrl = this.client.baseUrl.replace(/^http/, "ws") + `/ws?ticket=${ticket}&clientType=cli`;
2545
- return new Promise((resolve, reject) => {
2642
+ return new Promise((resolve2, reject) => {
2546
2643
  let resolved = false;
2547
2644
  this.ws = new WebSocket(wsUrl);
2548
2645
  let subscribed = false;
@@ -2571,7 +2668,7 @@ var PipelineWsClient = class _PipelineWsClient {
2571
2668
  this.options.onConnect();
2572
2669
  if (!resolved) {
2573
2670
  resolved = true;
2574
- resolve();
2671
+ resolve2();
2575
2672
  }
2576
2673
  }
2577
2674
  this.options.onEvent(event);
@@ -2683,7 +2780,7 @@ var PipelineWsClient = class _PipelineWsClient {
2683
2780
 
2684
2781
  // src/lib/logger.ts
2685
2782
  import { mkdirSync, writeFileSync, appendFileSync, readdirSync, rmSync, statSync, renameSync } from "fs";
2686
- import { join } from "path";
2783
+ import { join as join2 } from "path";
2687
2784
  var MAX_LOG_BYTES = 5 * 1024 * 1024;
2688
2785
  var MAX_ROTATED_FILES = 3;
2689
2786
  var ROTATION_CHECK_INTERVAL = 100;
@@ -2691,11 +2788,11 @@ var PipelineLogger = class {
2691
2788
  boardDir;
2692
2789
  writeCount = 0;
2693
2790
  constructor(baseDir, boardId) {
2694
- this.boardDir = join(baseDir, boardId);
2791
+ this.boardDir = join2(baseDir, boardId);
2695
2792
  mkdirSync(this.boardDir, { recursive: true });
2696
2793
  }
2697
2794
  orchestrator(message) {
2698
- const logPath = join(this.boardDir, "orchestrator.log");
2795
+ const logPath = join2(this.boardDir, "orchestrator.log");
2699
2796
  const entry = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
2700
2797
  `;
2701
2798
  appendFileSync(logPath, entry);
@@ -2727,10 +2824,10 @@ var PipelineLogger = class {
2727
2824
  }
2728
2825
  }
2729
2826
  iteration(ticketNumber, iterationNum, data) {
2730
- const ticketDir = join(this.boardDir, ticketNumber);
2827
+ const ticketDir = join2(this.boardDir, ticketNumber);
2731
2828
  mkdirSync(ticketDir, { recursive: true });
2732
2829
  const padded = String(iterationNum).padStart(3, "0");
2733
- const logPath = join(ticketDir, `iteration-${padded}.log`);
2830
+ const logPath = join2(ticketDir, `iteration-${padded}.log`);
2734
2831
  const entry = {
2735
2832
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2736
2833
  iteration: iterationNum,
@@ -2749,7 +2846,7 @@ var PipelineLogger = class {
2749
2846
  try {
2750
2847
  const entries = readdirSync(this.boardDir, { withFileTypes: true });
2751
2848
  for (const entry of entries) {
2752
- const fullPath = join(this.boardDir, entry.name);
2849
+ const fullPath = join2(this.boardDir, entry.name);
2753
2850
  if (entry.isDirectory() && entry.name !== ".") {
2754
2851
  try {
2755
2852
  const stat = statSync(fullPath);
@@ -3535,8 +3632,9 @@ var CodexProvider = class {
3535
3632
  execFileSync2("git", ["worktree", "add", request.workingDirectory, branch], {
3536
3633
  stdio: "pipe"
3537
3634
  });
3538
- } catch {
3539
- degraded.push("worktreeCreation");
3635
+ } catch (err) {
3636
+ const msg = err instanceof Error ? err.message : String(err);
3637
+ throw new Error(`worktree_creation_failed: ${msg}`);
3540
3638
  }
3541
3639
  }
3542
3640
  } else {
@@ -3555,17 +3653,18 @@ var CodexProvider = class {
3555
3653
  execFileSync2("git", ["worktree", "add", request.workingDirectory, branch], {
3556
3654
  stdio: "pipe"
3557
3655
  });
3558
- } catch {
3559
- degraded.push("worktreeCreation");
3656
+ } catch (err) {
3657
+ const msg = err instanceof Error ? err.message : String(err);
3658
+ throw new Error(`worktree_creation_failed: ${msg}`);
3560
3659
  }
3561
3660
  }
3562
3661
  }
3563
3662
  }
3564
- if (!degraded.includes("worktreeCreation") && existsSync(request.workingDirectory)) {
3663
+ if (existsSync(request.workingDirectory)) {
3565
3664
  ensureWorktreeRemote(request.workingDirectory);
3566
3665
  }
3567
3666
  }
3568
- return new Promise((resolve) => {
3667
+ return new Promise((resolve2) => {
3569
3668
  const [cmd, prefixArgs] = resolveCommand("codex");
3570
3669
  const resolvedArgs = [...prefixArgs, ...args];
3571
3670
  const child = spawn2(cmd, resolvedArgs, {
@@ -3616,7 +3715,7 @@ var CodexProvider = class {
3616
3715
  durationMs: Date.now() - startTime
3617
3716
  };
3618
3717
  if (degraded.length > 0) result.degradedCapabilities = degraded;
3619
- resolve(result);
3718
+ resolve2(result);
3620
3719
  };
3621
3720
  child.on("close", (code) => finish(code));
3622
3721
  child.on("error", (err) => finish(1, err.message));
@@ -3689,8 +3788,8 @@ var CodexProvider = class {
3689
3788
  // src/providers/gemini-provider.ts
3690
3789
  import { spawn as spawn3, execFileSync as execFileSync3 } from "child_process";
3691
3790
  import { existsSync as existsSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, rmSync as rmSync3 } from "fs";
3692
- import { join as join2, dirname } from "path";
3693
- import { homedir } from "os";
3791
+ import { join as join3, dirname } from "path";
3792
+ import { homedir as homedir2 } from "os";
3694
3793
  import { fileURLToPath } from "url";
3695
3794
  import { readFileSync as readFileSync2 } from "fs";
3696
3795
 
@@ -3853,13 +3952,13 @@ function resolveHookScriptPath() {
3853
3952
  try {
3854
3953
  const thisDir = dirname(fileURLToPath(import.meta.url));
3855
3954
  const candidates = [
3856
- join2(thisDir, "lib", "gemini-hooks.mjs"),
3955
+ join3(thisDir, "lib", "gemini-hooks.mjs"),
3857
3956
  // dist/lib/ (flat bundle)
3858
- join2(thisDir, "..", "lib", "gemini-hooks.mjs"),
3957
+ join3(thisDir, "..", "lib", "gemini-hooks.mjs"),
3859
3958
  // dist/../lib/ (nested)
3860
- join2(thisDir, "..", "src", "lib", "gemini-hooks.mjs"),
3959
+ join3(thisDir, "..", "src", "lib", "gemini-hooks.mjs"),
3861
3960
  // source from dist/
3862
- join2(thisDir, "..", "..", "src", "lib", "gemini-hooks.mjs")
3961
+ join3(thisDir, "..", "..", "src", "lib", "gemini-hooks.mjs")
3863
3962
  // source from dist/providers/
3864
3963
  ];
3865
3964
  for (const p of candidates) {
@@ -3942,7 +4041,7 @@ var GeminiProvider = class _GeminiProvider {
3942
4041
  if (request.workingDirectory && settingsDir) {
3943
4042
  this.copySettingsToDir(settingsDir, request.workingDirectory);
3944
4043
  }
3945
- return new Promise((resolve) => {
4044
+ return new Promise((resolve2) => {
3946
4045
  const [cmd, prefixArgs] = resolveCommand("gemini");
3947
4046
  const resolvedArgs = [...prefixArgs, ...args];
3948
4047
  const child = spawn3(cmd, resolvedArgs, {
@@ -4015,7 +4114,7 @@ var GeminiProvider = class _GeminiProvider {
4015
4114
  durationMs: Date.now() - startTime
4016
4115
  };
4017
4116
  if (degraded.length > 0) result.degradedCapabilities = degraded;
4018
- resolve(result);
4117
+ resolve2(result);
4019
4118
  };
4020
4119
  child.on("close", (code) => finish(code));
4021
4120
  child.on("error", (err) => finish(1, err.message));
@@ -4053,9 +4152,9 @@ var GeminiProvider = class _GeminiProvider {
4053
4152
  }
4054
4153
  writeGeminiSettings(request, degraded) {
4055
4154
  try {
4056
- const baseDir = join2(homedir(), ".kantban", "pipelines", "gemini-tmp");
4057
- const dir = join2(baseDir, `session-${Date.now()}`);
4058
- const geminiDir = join2(dir, ".gemini");
4155
+ const baseDir = join3(homedir2(), ".kantban", "pipelines", "gemini-tmp");
4156
+ const dir = join3(baseDir, `session-${Date.now()}`);
4157
+ const geminiDir = join3(dir, ".gemini");
4059
4158
  mkdirSync2(geminiDir, { recursive: true, mode: 448 });
4060
4159
  const settings = {};
4061
4160
  if (request.mcpConfig && Object.keys(request.mcpConfig.servers).length > 0) {
@@ -4079,20 +4178,20 @@ var GeminiProvider = class _GeminiProvider {
4079
4178
  hookConfig.disallowedTools = tr.disallowedTools ? translateToolNames(tr.disallowedTools) : null;
4080
4179
  if (tr.tools !== void 0) hookConfig.builtinToolsMode = tr.tools;
4081
4180
  const ext = IS_WINDOWS ? ".cmd" : ".sh";
4082
- const wrapperPath = join2(dir, `.kantban-hook-before-tool${ext}`);
4083
- writeFileSync3(wrapperPath, this.generateHookWrapper(hookPath, "BeforeToolSelection", join2(dir, ".kantban-hook-config.json")), { mode: 493 });
4181
+ const wrapperPath = join3(dir, `.kantban-hook-before-tool${ext}`);
4182
+ writeFileSync3(wrapperPath, this.generateHookWrapper(hookPath, "BeforeToolSelection", join3(dir, ".kantban-hook-config.json")), { mode: 493 });
4084
4183
  hooks.BeforeToolSelection = [{ matcher: "*", hooks: [{ type: "command", command: wrapperPath, timeout: 3e4 }] }];
4085
4184
  }
4086
4185
  if (request.maxTurns && request.mcpConfig) {
4087
4186
  hookConfig.maxTurns = request.maxTurns;
4088
- hookConfig.turnFile = join2(dir, ".kantban-turn-counter");
4187
+ hookConfig.turnFile = join3(dir, ".kantban-turn-counter");
4089
4188
  const ext = IS_WINDOWS ? ".cmd" : ".sh";
4090
- const wrapperPath = join2(dir, `.kantban-hook-after-agent${ext}`);
4091
- writeFileSync3(wrapperPath, this.generateHookWrapper(hookPath, "AfterAgent", join2(dir, ".kantban-hook-config.json")), { mode: 493 });
4189
+ const wrapperPath = join3(dir, `.kantban-hook-after-agent${ext}`);
4190
+ writeFileSync3(wrapperPath, this.generateHookWrapper(hookPath, "AfterAgent", join3(dir, ".kantban-hook-config.json")), { mode: 493 });
4092
4191
  hooks.AfterAgent = [{ matcher: "*", hooks: [{ type: "command", command: wrapperPath, timeout: 3e4 }] }];
4093
4192
  }
4094
4193
  if (Object.keys(hookConfig).length > 0) {
4095
- writeFileSync3(join2(dir, ".kantban-hook-config.json"), JSON.stringify(hookConfig), { mode: 384 });
4194
+ writeFileSync3(join3(dir, ".kantban-hook-config.json"), JSON.stringify(hookConfig), { mode: 384 });
4096
4195
  }
4097
4196
  if (Object.keys(hooks).length > 0) {
4098
4197
  settings.hooks = hooks;
@@ -4105,7 +4204,7 @@ var GeminiProvider = class _GeminiProvider {
4105
4204
  process.stderr.write(`[gemini] Hook script not found \u2014 tool scoping and turn limits unavailable
4106
4205
  `);
4107
4206
  }
4108
- writeFileSync3(join2(geminiDir, "settings.json"), JSON.stringify(settings, null, 2), { mode: 384 });
4207
+ writeFileSync3(join3(geminiDir, "settings.json"), JSON.stringify(settings, null, 2), { mode: 384 });
4109
4208
  return dir;
4110
4209
  } catch (err) {
4111
4210
  process.stderr.write(`[gemini] Failed to write settings.json: ${err}
@@ -4171,17 +4270,29 @@ node "${hookScript}" "${event}" "${configFile}" < "$STDIN_FILE"
4171
4270
  }
4172
4271
  copySettingsToDir(settingsDir, targetDir) {
4173
4272
  try {
4174
- const sourceFile = join2(settingsDir, ".gemini", "settings.json");
4175
- const targetGeminiDir = join2(targetDir, ".gemini");
4273
+ const sourceFile = join3(settingsDir, ".gemini", "settings.json");
4274
+ const targetGeminiDir = join3(targetDir, ".gemini");
4176
4275
  mkdirSync2(targetGeminiDir, { recursive: true, mode: 448 });
4177
4276
  const content = readFileSync2(sourceFile, "utf-8");
4178
- writeFileSync3(join2(targetGeminiDir, "settings.json"), content, { mode: 384 });
4277
+ writeFileSync3(join3(targetGeminiDir, "settings.json"), content, { mode: 384 });
4179
4278
  } catch {
4180
4279
  }
4181
4280
  }
4182
4281
  };
4183
4282
 
4184
4283
  // src/commands/pipeline.ts
4284
+ var execFileAsync = promisify(execFile);
4285
+ var _cachedPrimaryRepoRoot = null;
4286
+ async function getPrimaryRepoRoot() {
4287
+ if (_cachedPrimaryRepoRoot) return _cachedPrimaryRepoRoot;
4288
+ try {
4289
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"]);
4290
+ _cachedPrimaryRepoRoot = resolve(stdout.trim());
4291
+ } catch {
4292
+ _cachedPrimaryRepoRoot = resolve(process.cwd());
4293
+ }
4294
+ return _cachedPrimaryRepoRoot;
4295
+ }
4185
4296
  function createProviderRegistry() {
4186
4297
  const registry = new ProviderRegistry();
4187
4298
  registry.register(new ClaudeProvider());
@@ -4303,10 +4414,10 @@ Flags:
4303
4414
  return { boardId, once, dryRun, columnFilter, maxIterations, maxBudget, model, provider, concurrency, logRetention, yes };
4304
4415
  }
4305
4416
  function pidDir(boardId) {
4306
- return join3(homedir2(), ".kantban", "pipelines", boardId);
4417
+ return join4(homedir3(), ".kantban", "pipelines", boardId);
4307
4418
  }
4308
4419
  function pidFilePath(boardId) {
4309
- return join3(pidDir(boardId), "orchestrator.pid");
4420
+ return join4(pidDir(boardId), "orchestrator.pid");
4310
4421
  }
4311
4422
  function writePidFile(boardId) {
4312
4423
  const dir = pidDir(boardId);
@@ -4320,7 +4431,7 @@ function removePidFile(boardId) {
4320
4431
  }
4321
4432
  }
4322
4433
  function childManifestPath(boardId) {
4323
- return join3(pidDir(boardId), "children.pid");
4434
+ return join4(pidDir(boardId), "children.pid");
4324
4435
  }
4325
4436
  function readChildManifest(boardId) {
4326
4437
  try {
@@ -4365,7 +4476,7 @@ function cleanupOrphanedProcesses(boardId) {
4365
4476
  console.log(`Killed ${String(manifestPids.length)} orphaned child process(es) from manifest`);
4366
4477
  removeChildManifest(boardId);
4367
4478
  }
4368
- const staleReaperPath = join3(pidDir(boardId), "reaper.pid");
4479
+ const staleReaperPath = join4(pidDir(boardId), "reaper.pid");
4369
4480
  try {
4370
4481
  if (existsSync3(staleReaperPath)) {
4371
4482
  const reaperPid = parseInt(readFileSync3(staleReaperPath, "utf-8").trim(), 10);
@@ -4398,14 +4509,14 @@ Press Ctrl+C to cancel, or re-run with --yes to skip this warning.
4398
4509
  `);
4399
4510
  }
4400
4511
  function waitForConfirmation() {
4401
- return new Promise((resolve) => {
4512
+ return new Promise((resolve2) => {
4402
4513
  process.stdout.write("Continue? [y/N] ");
4403
4514
  process.stdin.setEncoding("utf8");
4404
4515
  process.stdin.once("data", (data) => {
4405
4516
  const answer = data.trim().toLowerCase();
4406
- resolve(answer === "y" || answer === "yes");
4517
+ resolve2(answer === "y" || answer === "yes");
4407
4518
  });
4408
- process.stdin.once("end", () => resolve(false));
4519
+ process.stdin.once("end", () => resolve2(false));
4409
4520
  });
4410
4521
  }
4411
4522
  async function runPipeline(client, args) {
@@ -4424,7 +4535,7 @@ async function runPipeline(client, args) {
4424
4535
  return;
4425
4536
  }
4426
4537
  }
4427
- const gateFilePath = join3(process.cwd(), "pipeline.gates.yaml");
4538
+ const gateFilePath = join4(process.cwd(), "pipeline.gates.yaml");
4428
4539
  let gateConfig;
4429
4540
  if (!existsSync3(gateFilePath)) {
4430
4541
  console.error(`Error: pipeline.gates.yaml not found in ${process.cwd()}`);
@@ -4461,7 +4572,7 @@ async function runPipeline(client, args) {
4461
4572
  ...opts.provider ? { intelligence_provider: opts.provider } : {}
4462
4573
  };
4463
4574
  const intelligenceProvider = registry.resolveForIntelligence(boardProviderConfig);
4464
- const logBaseDir = join3(homedir2(), ".kantban", "pipelines");
4575
+ const logBaseDir = join4(homedir3(), ".kantban", "pipelines");
4465
4576
  const logger = new PipelineLogger(logBaseDir, opts.boardId);
4466
4577
  logger.pruneOldLogs(opts.logRetention);
4467
4578
  let runMemory = null;
@@ -4483,6 +4594,32 @@ async function runPipeline(client, args) {
4483
4594
  ...opts.maxBudget !== null && { maxBudgetUsd: opts.maxBudget },
4484
4595
  ...opts.model !== null && { model: opts.model }
4485
4596
  };
4597
+ if (effectiveConfig.worktreePath) {
4598
+ const primaryRepoRoot = await getPrimaryRepoRoot();
4599
+ const resolvedCwd = resolve(effectiveConfig.worktreePath);
4600
+ const inside = resolvedCwd === primaryRepoRoot || resolvedCwd.startsWith(primaryRepoRoot + sep) || primaryRepoRoot.startsWith(resolvedCwd + sep);
4601
+ if (inside) {
4602
+ const errMsg = `[fatal] dispatch refused: resolved cwd "${resolvedCwd}" is inside, equal to, or a parent of the primary repo "${primaryRepoRoot}". Configure agent_config.worktree.path_pattern to a path outside the primary repo.`;
4603
+ logger.orchestrator(errMsg);
4604
+ await deps.createComment(ticketId, `[dispatch] failed: cwd would be inside the primary repo. Update agent_config.worktree.path_pattern to a path outside this repo.`);
4605
+ return { reason: "error", iterations: 0, gutterCount: 0, lastError: errMsg };
4606
+ }
4607
+ }
4608
+ if (effectiveConfig.worktreePath) {
4609
+ const parent = dirname2(effectiveConfig.worktreePath);
4610
+ try {
4611
+ await fsPromises.mkdir(parent, { recursive: true });
4612
+ } catch (err) {
4613
+ const msg = err instanceof Error ? err.message : String(err);
4614
+ const errMsg = `[dispatch] failed ticket=${ticketId} reason=worktree_unrenderable path="${effectiveConfig.worktreePath}" error="${msg}"`;
4615
+ logger.orchestrator(errMsg);
4616
+ await deps.createComment(
4617
+ ticketId,
4618
+ `[dispatch] failed: cannot create worktree parent directory at "${parent}": ${msg}. Fix the path_pattern or its parent directory; the orchestrator will retry on the next scan.`
4619
+ );
4620
+ return { reason: "error", iterations: 0, gutterCount: 0, lastError: errMsg };
4621
+ }
4622
+ }
4486
4623
  const mem = runMemory;
4487
4624
  const colScopeForName = await client.get(
4488
4625
  `/projects/${projectId}/pipeline-context`,
@@ -4499,7 +4636,7 @@ async function runPipeline(client, args) {
4499
4636
  effectiveConfig.model = registry.resolveModel(columnProvider, effectiveConfig.model);
4500
4637
  }
4501
4638
  const { gates: columnGates } = resolveGatesForColumn(gateConfig, resolvedColumnName);
4502
- const gateCwd = effectiveConfig.worktreePath ?? (effectiveConfig.worktreeName ? join3(process.cwd(), effectiveConfig.worktreeName) : process.cwd());
4639
+ const gateCwd = effectiveConfig.worktreePath ?? (effectiveConfig.worktreeName ? join4(process.cwd(), effectiveConfig.worktreeName) : process.cwd());
4503
4640
  const effectiveMcpConfigPath = columnGates.length > 0 ? generateGateProxyMcpConfig(
4504
4641
  client.baseUrl,
4505
4642
  client.token,
@@ -4682,7 +4819,27 @@ async function runPipeline(client, args) {
4682
4819
  const blockers = await client.get(
4683
4820
  `/projects/${projectId}/tickets/${ticketId}/unresolved-blockers`
4684
4821
  );
4685
- return blockers.length > 0;
4822
+ if (!blockers || blockers.length === 0) {
4823
+ return { blocked: false };
4824
+ }
4825
+ let linkIds = [];
4826
+ try {
4827
+ const links = await client.get(
4828
+ `/projects/${projectId}/ticket-links?ticketId=${ticketId}&linkType=blocks`
4829
+ );
4830
+ linkIds = (links ?? []).filter((l) => l.target_id === ticketId && l.link_type === "blocks").map((l) => l.id);
4831
+ } catch {
4832
+ linkIds = [];
4833
+ }
4834
+ return {
4835
+ blocked: true,
4836
+ blockers: blockers.map((b) => ({
4837
+ id: b.id,
4838
+ ticket_number: b.ticket_number,
4839
+ column: b.column
4840
+ })),
4841
+ linkIds
4842
+ };
4686
4843
  },
4687
4844
  dispatchLightCall: async (ticketId, columnId) => {
4688
4845
  const [ticketCtx, colScope, boardScope] = await Promise.all([
@@ -4796,6 +4953,7 @@ async function runPipeline(client, args) {
4796
4953
  appendRunMemory: (section, content) => runMemory ? runMemory.append(section, content) : Promise.resolve(),
4797
4954
  cleanupWorktree: (name) => cleanupWorktree(name),
4798
4955
  mergeWorktree: (name, integrationBranch) => mergeWorktreeBranch(name, integrationBranch),
4956
+ detectBranchMerged: async (opts2) => detectBranchMerged(opts2),
4799
4957
  // Pipeline event emission — wsClient captured by reference (set later before any loops run)
4800
4958
  emitPipelineEvent: (event) => {
4801
4959
  wsClient?.send(event);
@@ -4987,7 +5145,7 @@ Received ${signal}. Shutting down gracefully...`);
4987
5145
  cleanupOrphanedProcesses(opts.boardId);
4988
5146
  writePidFile(opts.boardId);
4989
5147
  logger.orchestrator(`PID file written: ${String(process.pid)}`);
4990
- const reaperPidPath = join3(pidDir(opts.boardId), "reaper.pid");
5148
+ const reaperPidPath = join4(pidDir(opts.boardId), "reaper.pid");
4991
5149
  const reaperProcess = spawnReaper({
4992
5150
  orchestratorPid: process.pid,
4993
5151
  manifestPath: childManifestPath(opts.boardId),
@@ -5127,7 +5285,7 @@ async function waitForAllLoops(orchestrator, timeoutMs = 4 * 60 * 60 * 1e3) {
5127
5285
  );
5128
5286
  return;
5129
5287
  }
5130
- await new Promise((resolve) => setTimeout(resolve, 1e3));
5288
+ await new Promise((resolve2) => setTimeout(resolve2, 1e3));
5131
5289
  if (activeRalphLoops.size === 0 && orchestrator.activeLoopCount === 0 && !orchestrator.hasActiveQueuedWork && !orchestrator.hasCompletingWork) {
5132
5290
  consecutiveIdle++;
5133
5291
  } else {
@@ -5177,4 +5335,4 @@ export {
5177
5335
  runPipeline,
5178
5336
  stopPipeline
5179
5337
  };
5180
- //# sourceMappingURL=pipeline-GZOSDNPF.js.map
5338
+ //# sourceMappingURL=pipeline-6J64Z7VH.js.map