kantban-cli 0.1.49 → 0.1.52

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,19 +1,26 @@
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,
7
7
  classifyTrajectory,
8
8
  cleanupGateProxyConfigs,
9
9
  cleanupMcpConfig,
10
+ cleanupWorktree,
10
11
  composeStuckDetectionPrompt,
12
+ defaultWorktreeRoot,
13
+ detectBranchMerged,
14
+ ensureWorktreeRemote,
11
15
  generateGateProxyMcpConfig,
12
16
  generateMcpConfig,
17
+ generateWorktreeName,
18
+ mergeWorktreeBranch,
13
19
  parseJsonFromLlmOutput,
14
20
  parseStuckDetectionResponse,
15
- reapOrphanedMcpConfigDirs
16
- } from "./chunk-QHJZIGEE.js";
21
+ reapOrphanedMcpConfigDirs,
22
+ renderWorktreePath
23
+ } from "./chunk-3A4B7CUH.js";
17
24
  import {
18
25
  LoopCheckpointSchema,
19
26
  VerdictSchema,
@@ -31,9 +38,11 @@ import {
31
38
  } from "./chunk-5ZU2OOES.js";
32
39
 
33
40
  // 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";
41
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync3, unlinkSync as unlinkSync3, existsSync as existsSync3, appendFileSync as appendFileSync2, promises as fsPromises } from "fs";
42
+ import { execFile } from "child_process";
35
43
  import { homedir as homedir2 } from "os";
36
- import { join as join3, dirname as dirname2 } from "path";
44
+ import { join as join3, dirname as dirname2, resolve, sep } from "path";
45
+ import { promisify } from "util";
37
46
 
38
47
  // src/lib/harness-signal.ts
39
48
  var HARNESS_SIGNAL_PREFIX = "@harness:";
@@ -55,142 +64,6 @@ function resolveToolRestrictions(builtinTools, allowedTools, disallowedTools) {
55
64
  };
56
65
  }
57
66
 
58
- // src/lib/worktree.ts
59
- import { execFile as defaultExecFile, execFileSync } from "child_process";
60
- function generateWorktreeName(ticketNumber, columnName) {
61
- const slug = columnSlug(columnName);
62
- return `kantban-${ticketNumber}-${slug}`;
63
- }
64
- function columnSlug(columnName) {
65
- return columnName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
66
- }
67
- function renderWorktreePath(ticketNumber, columnName, pathPattern) {
68
- 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);
71
- }
72
- function isPlausibleRemoteUrl(url) {
73
- return url.includes("/") || url.includes("@") || url.includes("://");
74
- }
75
- function ensureWorktreeRemote(worktreePath) {
76
- try {
77
- const remotes = normalizeEol(execFileSync("git", ["-C", worktreePath, "remote"], {
78
- stdio: "pipe",
79
- encoding: "utf-8"
80
- })).trim();
81
- if (remotes.split("\n").includes("origin")) {
82
- const currentUrl = normalizeEol(execFileSync("git", ["-C", worktreePath, "remote", "get-url", "origin"], {
83
- stdio: "pipe",
84
- encoding: "utf-8"
85
- })).trim();
86
- if (isPlausibleRemoteUrl(currentUrl)) return;
87
- console.error(`[worktree] Invalid origin URL in ${worktreePath}: "${currentUrl}" \u2014 fixing`);
88
- execFileSync("git", ["-C", worktreePath, "remote", "remove", "origin"], {
89
- stdio: "pipe"
90
- });
91
- }
92
- const originUrl = normalizeEol(execFileSync("git", ["remote", "get-url", "origin"], {
93
- stdio: "pipe",
94
- encoding: "utf-8"
95
- })).trim();
96
- if (originUrl && isPlausibleRemoteUrl(originUrl)) {
97
- execFileSync("git", ["-C", worktreePath, "remote", "add", "origin", originUrl], {
98
- stdio: "pipe"
99
- });
100
- console.error(`[worktree] Added missing origin remote to ${worktreePath}: ${originUrl}`);
101
- } else {
102
- console.error(`[worktree] WARNING: main repo origin URL is also invalid: "${originUrl}"`);
103
- }
104
- } catch (err) {
105
- const msg = err instanceof Error ? err.message : String(err);
106
- console.error(`[worktree] Failed to ensure remote for ${worktreePath}: ${msg}`);
107
- }
108
- }
109
- async function cleanupWorktree(worktreeName, exec = defaultExecFile) {
110
- let target = worktreeName;
111
- try {
112
- const path = await findWorktreeForBranch(exec, worktreeName);
113
- if (!path) return true;
114
- target = path;
115
- } catch {
116
- return true;
117
- }
118
- return new Promise((resolve) => {
119
- exec("git", ["worktree", "remove", "--force", target], (err) => {
120
- if (err) {
121
- console.error(`[worktree] cleanup failed for ${worktreeName} (${target}): ${err.message}`);
122
- resolve(false);
123
- } else {
124
- resolve(true);
125
- }
126
- });
127
- });
128
- }
129
- function execPromise(exec, cmd, args) {
130
- return new Promise((resolve, reject) => {
131
- exec(cmd, args, (err, stdout, stderr) => {
132
- if (err) reject(Object.assign(err, { stdout, stderr }));
133
- else resolve({ stdout, stderr });
134
- });
135
- });
136
- }
137
- async function findWorktreeForBranch(exec, branch) {
138
- try {
139
- const { stdout } = await execPromise(exec, "git", ["worktree", "list", "--porcelain"]);
140
- const targetRef = `refs/heads/${branch}`;
141
- let currentPath = null;
142
- for (const line of normalizeEol(stdout).split("\n")) {
143
- if (line.startsWith("worktree ")) currentPath = line.slice("worktree ".length);
144
- if (line.startsWith("branch ") && line.slice("branch ".length) === targetRef && currentPath) {
145
- return currentPath;
146
- }
147
- }
148
- return null;
149
- } catch {
150
- return null;
151
- }
152
- }
153
- async function mergeWorktreeBranch(worktreeName, integrationBranch, exec = defaultExecFile) {
154
- try {
155
- try {
156
- await execPromise(exec, "git", ["rev-parse", "--verify", worktreeName]);
157
- } catch {
158
- return true;
159
- }
160
- await execPromise(exec, "git", ["branch", integrationBranch, "HEAD"]).catch(() => {
161
- });
162
- const checkedOutPath = await findWorktreeForBranch(exec, integrationBranch);
163
- if (checkedOutPath) {
164
- await execPromise(exec, "git", ["-C", checkedOutPath, "merge", "--no-edit", worktreeName]);
165
- console.error(`[worktree] merged ${worktreeName} \u2192 ${integrationBranch}`);
166
- return true;
167
- }
168
- const { stdout: baseOut } = await execPromise(exec, "git", ["merge-base", integrationBranch, worktreeName]);
169
- const mergeBase = baseOut.trim();
170
- const { stdout: integrationSha } = await execPromise(exec, "git", ["rev-parse", integrationBranch]);
171
- if (integrationSha.trim() === mergeBase) {
172
- const { stdout: worktreeSha } = await execPromise(exec, "git", ["rev-parse", worktreeName]);
173
- await execPromise(exec, "git", ["update-ref", `refs/heads/${integrationBranch}`, worktreeSha.trim()]);
174
- console.error(`[worktree] fast-forward merged ${worktreeName} \u2192 ${integrationBranch}`);
175
- return true;
176
- }
177
- const tmpWorktree = `merge-tmp-${Date.now()}`;
178
- try {
179
- await execPromise(exec, "git", ["worktree", "add", tmpWorktree, integrationBranch]);
180
- await execPromise(exec, "git", ["-C", tmpWorktree, "merge", "--no-edit", worktreeName]);
181
- console.error(`[worktree] merged ${worktreeName} \u2192 ${integrationBranch}`);
182
- } finally {
183
- await execPromise(exec, "git", ["worktree", "remove", "--force", tmpWorktree]).catch(() => {
184
- });
185
- }
186
- return true;
187
- } catch (err) {
188
- const msg = err instanceof Error ? err.message : String(err);
189
- console.error(`[worktree] merge failed for ${worktreeName} \u2192 ${integrationBranch}: ${msg}`);
190
- return false;
191
- }
192
- }
193
-
194
67
  // src/lib/constraint-evaluator.ts
195
68
  function resolveColumn(board, columnId, subjectRef) {
196
69
  const ref = subjectRef ?? "self";
@@ -942,8 +815,8 @@ var PipelineOrchestrator = class {
942
815
  }
943
816
  for (const [ticketId, columnId] of Array.from(this.deferredTickets)) {
944
817
  try {
945
- const stillBlocked = await this.deps.hasUnresolvedBlockers(ticketId);
946
- if (!stillBlocked) {
818
+ const blocker = await this.deps.hasUnresolvedBlockers(ticketId);
819
+ if (!blocker.blocked) {
947
820
  this.deferredTickets.delete(ticketId);
948
821
  await this.spawnOrQueue(ticketId, columnId, true);
949
822
  } else {
@@ -957,26 +830,56 @@ var PipelineOrchestrator = class {
957
830
  }
958
831
  await this.refreshBoardScope();
959
832
  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)`);
833
+ await Promise.all(
834
+ Array.from(this.pipelineColumns.keys()).map(async (columnId) => {
835
+ await this.refreshColumnScope(columnId);
836
+ const colScope = this.columnScopes.get(columnId);
837
+ if (!colScope) {
838
+ console.error(` [scan] column=${columnId} reason="no cached scope"`);
839
+ return;
840
+ }
841
+ if (this.isColumnBlocked(columnId)) {
842
+ this.blockedColumns.add(columnId);
843
+ console.error(
844
+ ` [scan] column=${columnId} name="${colScope.column.name}" tickets=${colScope.tickets.length} dispatched=0 queued=0 skipped=${colScope.tickets.length} reason="firing_constraint"`
845
+ );
846
+ for (const ticket of colScope.tickets) {
847
+ await this.emitDispatchDeferred(ticket.id, "firing_constraint", { columnId });
848
+ }
849
+ return;
850
+ }
851
+ let dispatched = 0;
852
+ let queued = 0;
853
+ let skipped = 0;
854
+ const reasons = /* @__PURE__ */ new Set();
970
855
  for (const ticket of colScope.tickets) {
971
- await this.emitDispatchDeferred(ticket.id, "firing_constraint", { columnId });
856
+ const wasActive = this.activeLoops.has(ticket.id) || this.spawning.has(ticket.id);
857
+ const wasQueued = (this.loopQueues.get(columnId) ?? []).includes(ticket.id);
858
+ const wasDeferred = this.deferredTickets.has(ticket.id);
859
+ await this.spawnOrQueue(ticket.id, columnId);
860
+ const nowActive = this.activeLoops.has(ticket.id) || this.spawning.has(ticket.id);
861
+ const nowQueued = (this.loopQueues.get(columnId) ?? []).includes(ticket.id);
862
+ const nowDeferred = this.deferredTickets.has(ticket.id);
863
+ if (!wasActive && nowActive) {
864
+ dispatched++;
865
+ reasons.add("dispatched");
866
+ } else if (!wasQueued && nowQueued) {
867
+ queued++;
868
+ reasons.add("capped");
869
+ } else if (!wasDeferred && nowDeferred) {
870
+ skipped++;
871
+ reasons.add("blocked");
872
+ } else {
873
+ skipped++;
874
+ reasons.add("known");
875
+ }
972
876
  }
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
- }
877
+ const reason = reasons.size === 0 ? "none" : reasons.size === 1 ? [...reasons][0] : "mixed";
878
+ console.error(
879
+ ` [scan] column=${columnId} name="${colScope.column.name}" tickets=${colScope.tickets.length} dispatched=${dispatched} queued=${queued} skipped=${skipped} reason="${reason}"`
880
+ );
881
+ })
882
+ );
980
883
  }
981
884
  /**
982
885
  * Handle a pipeline event (typically from WebSocket via EventQueue).
@@ -1098,15 +1001,58 @@ var PipelineOrchestrator = class {
1098
1001
  }
1099
1002
  return;
1100
1003
  }
1004
+ const isMergeColumn = colConfig.name.toLowerCase().includes("merge");
1005
+ if (isMergeColumn && colConfig.worktreeEnabled && this.deps.detectBranchMerged && this.deps.moveTicketToColumn) {
1006
+ const ticket = this.columnScopes.get(columnId)?.tickets.find((t) => t.id === ticketId);
1007
+ if (ticket) {
1008
+ const wPath = renderWorktreePath(
1009
+ ticket.ticket_number,
1010
+ colConfig.name,
1011
+ colConfig.worktreePathPattern,
1012
+ { boardId: this.boardId, defaultRoot: defaultWorktreeRoot() }
1013
+ );
1014
+ try {
1015
+ const mergeResult = await this.deps.detectBranchMerged({
1016
+ worktreePath: wPath,
1017
+ iterationStartedAt: /* @__PURE__ */ new Date(0),
1018
+ targetBranch: colConfig.worktreeIntegrationBranch ?? "main"
1019
+ });
1020
+ if (mergeResult.merged) {
1021
+ const nextColumnId = this.findNextColumnId(columnId);
1022
+ if (nextColumnId) {
1023
+ await this.deps.createComment(
1024
+ ticketId,
1025
+ `## Merge Complete (auto-finalized)
1026
+
1027
+ 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.`
1028
+ );
1029
+ await this.deps.moveTicketToColumn(ticketId, nextColumnId, {
1030
+ reason: "merge_finalize_recovery",
1031
+ merge_commit_sha: mergeResult.mergeCommitSha
1032
+ });
1033
+ console.error(
1034
+ ` [merge-recovery] ticket=${ticketId} column=${columnId} target=${colConfig.worktreeIntegrationBranch ?? "main"} sha=${mergeResult.mergeCommitSha}`
1035
+ );
1036
+ return;
1037
+ }
1038
+ }
1039
+ } catch {
1040
+ }
1041
+ }
1042
+ }
1101
1043
  this.spawning.add(ticketId);
1102
1044
  this.reserveSlot(columnId);
1103
1045
  try {
1104
- const blocked = await this.deps.hasUnresolvedBlockers(ticketId);
1105
- if (blocked) {
1046
+ const result = await this.deps.hasUnresolvedBlockers(ticketId);
1047
+ if (result.blocked) {
1106
1048
  this.spawning.delete(ticketId);
1107
1049
  this.releaseSlot(columnId);
1108
1050
  this.deferredTickets.set(ticketId, columnId);
1109
- console.error(` [skip] ${ticketId} has unresolved blockers \u2014 deferred`);
1051
+ const blockerSummary = result.blockers.map((b) => `${b.ticket_number}:${b.column}`).join(",");
1052
+ const linkIdSummary = result.linkIds.join(",");
1053
+ console.error(
1054
+ ` [scan] skip ticket=${ticketId} reason=blocks count=${result.blockers.length} link_ids=[${linkIdSummary}] blockers=[${blockerSummary}]`
1055
+ );
1110
1056
  void this.emitDispatchDeferred(ticketId, "unresolved_blockers", { columnId });
1111
1057
  void this.drainQueue(columnId).catch((err) => {
1112
1058
  const msg = err instanceof Error ? err.message : String(err);
@@ -1121,9 +1067,9 @@ var PipelineOrchestrator = class {
1121
1067
  this.releaseSlot(columnId);
1122
1068
  this.deferredTickets.set(ticketId, columnId);
1123
1069
  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}`);
1070
+ void this.drainQueue(columnId).catch((dErr) => {
1071
+ const dMsg = dErr instanceof Error ? dErr.message : String(dErr);
1072
+ console.error(` [error] drainQueue failed for column ${columnId}: ${dMsg}`);
1127
1073
  });
1128
1074
  return;
1129
1075
  }
@@ -1271,16 +1217,16 @@ var PipelineOrchestrator = class {
1271
1217
  gutterThreshold: config.gutterThreshold,
1272
1218
  ...effectiveModel !== void 0 && { model: effectiveModel },
1273
1219
  ...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
1220
  ...(() => {
1278
1221
  const colScope = this.columnScopes.get(columnId);
1279
1222
  const ticket = colScope?.tickets.find((t) => t.id === ticketId);
1280
1223
  if (!config.worktreeEnabled || !ticket) return {};
1281
1224
  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 };
1225
+ const wPath = renderWorktreePath(ticket.ticket_number, config.name, config.worktreePathPattern, {
1226
+ boardId: this.boardId,
1227
+ defaultRoot: defaultWorktreeRoot()
1228
+ });
1229
+ return { worktreeName: wName, worktreePath: wPath };
1284
1230
  })(),
1285
1231
  ...config.lookaheadColumnId !== void 0 && { lookaheadColumnId: config.lookaheadColumnId },
1286
1232
  // Resume from checkpoint iteration/gutter if provided
@@ -1799,6 +1745,19 @@ ${findingsText}`)
1799
1745
  const nextBoardCol = bs.columns.filter((c) => c.position > currentBoardCol.position && this.pipelineColumns.has(c.id)).sort((a, b) => a.position - b.position)[0];
1800
1746
  return nextBoardCol ? this.pipelineColumns.get(nextBoardCol.id) ?? null : null;
1801
1747
  }
1748
+ /**
1749
+ * Find the ID of the next board column by position, regardless of pipeline status.
1750
+ * Used by merge-recovery to target the downstream column (e.g. Done) which may not
1751
+ * be a pipeline column.
1752
+ */
1753
+ findNextColumnId(currentColumnId) {
1754
+ const bs = this.cachedBoardScope;
1755
+ if (!bs) return null;
1756
+ const currentBoardCol = bs.columns.find((c) => c.id === currentColumnId);
1757
+ if (!currentBoardCol) return null;
1758
+ const nextBoardCol = bs.columns.filter((c) => c.position > currentBoardCol.position).sort((a, b) => a.position - b.position)[0];
1759
+ return nextBoardCol?.id ?? null;
1760
+ }
1802
1761
  /**
1803
1762
  * Called when a loop finishes. Cleans up tracking and drains the queue.
1804
1763
  */
@@ -2186,8 +2145,8 @@ ${findingsText}`)
2186
2145
  this.spawning.add(nextTicketId);
2187
2146
  this.reserveSlot(columnId);
2188
2147
  try {
2189
- const blocked = await this.deps.hasUnresolvedBlockers(nextTicketId);
2190
- if (blocked) {
2148
+ const blockerResult = await this.deps.hasUnresolvedBlockers(nextTicketId);
2149
+ if (blockerResult.blocked) {
2191
2150
  this.deferredTickets.set(nextTicketId, columnId);
2192
2151
  this.spawning.delete(nextTicketId);
2193
2152
  this.releaseSlot(columnId);
@@ -2542,7 +2501,7 @@ var PipelineWsClient = class _PipelineWsClient {
2542
2501
  this.cleanupTimers();
2543
2502
  const { ticket } = await this.client.post("/ws-ticket");
2544
2503
  const wsUrl = this.client.baseUrl.replace(/^http/, "ws") + `/ws?ticket=${ticket}&clientType=cli`;
2545
- return new Promise((resolve, reject) => {
2504
+ return new Promise((resolve2, reject) => {
2546
2505
  let resolved = false;
2547
2506
  this.ws = new WebSocket(wsUrl);
2548
2507
  let subscribed = false;
@@ -2571,7 +2530,7 @@ var PipelineWsClient = class _PipelineWsClient {
2571
2530
  this.options.onConnect();
2572
2531
  if (!resolved) {
2573
2532
  resolved = true;
2574
- resolve();
2533
+ resolve2();
2575
2534
  }
2576
2535
  }
2577
2536
  this.options.onEvent(event);
@@ -3386,7 +3345,7 @@ var ProviderRegistry = class {
3386
3345
  };
3387
3346
 
3388
3347
  // src/providers/codex-provider.ts
3389
- import { spawn as spawn2, execFileSync as execFileSync2 } from "child_process";
3348
+ import { spawn as spawn2, execFileSync } from "child_process";
3390
3349
  import { existsSync, rmSync as rmSync2 } from "fs";
3391
3350
 
3392
3351
  // src/providers/codex-jsonl-parser.ts
@@ -3527,45 +3486,47 @@ var CodexProvider = class {
3527
3486
  const branch = request.branch ?? request.workingDirectory;
3528
3487
  if (!existsSync(request.workingDirectory)) {
3529
3488
  try {
3530
- execFileSync2("git", ["worktree", "add", "-b", branch, request.workingDirectory, "HEAD"], {
3489
+ execFileSync("git", ["worktree", "add", "-b", branch, request.workingDirectory, "HEAD"], {
3531
3490
  stdio: "pipe"
3532
3491
  });
3533
3492
  } catch {
3534
3493
  try {
3535
- execFileSync2("git", ["worktree", "add", request.workingDirectory, branch], {
3494
+ execFileSync("git", ["worktree", "add", request.workingDirectory, branch], {
3536
3495
  stdio: "pipe"
3537
3496
  });
3538
- } catch {
3539
- degraded.push("worktreeCreation");
3497
+ } catch (err) {
3498
+ const msg = err instanceof Error ? err.message : String(err);
3499
+ throw new Error(`worktree_creation_failed: ${msg}`);
3540
3500
  }
3541
3501
  }
3542
3502
  } else {
3543
3503
  try {
3544
- execFileSync2("git", ["-C", request.workingDirectory, "rev-parse", "--git-dir"], {
3504
+ execFileSync("git", ["-C", request.workingDirectory, "rev-parse", "--git-dir"], {
3545
3505
  stdio: "pipe"
3546
3506
  });
3547
3507
  } catch {
3548
3508
  try {
3549
3509
  rmSync2(request.workingDirectory, { recursive: true, force: true });
3550
- execFileSync2("git", ["worktree", "add", "-b", branch, request.workingDirectory, "HEAD"], {
3510
+ execFileSync("git", ["worktree", "add", "-b", branch, request.workingDirectory, "HEAD"], {
3551
3511
  stdio: "pipe"
3552
3512
  });
3553
3513
  } catch {
3554
3514
  try {
3555
- execFileSync2("git", ["worktree", "add", request.workingDirectory, branch], {
3515
+ execFileSync("git", ["worktree", "add", request.workingDirectory, branch], {
3556
3516
  stdio: "pipe"
3557
3517
  });
3558
- } catch {
3559
- degraded.push("worktreeCreation");
3518
+ } catch (err) {
3519
+ const msg = err instanceof Error ? err.message : String(err);
3520
+ throw new Error(`worktree_creation_failed: ${msg}`);
3560
3521
  }
3561
3522
  }
3562
3523
  }
3563
3524
  }
3564
- if (!degraded.includes("worktreeCreation") && existsSync(request.workingDirectory)) {
3525
+ if (existsSync(request.workingDirectory)) {
3565
3526
  ensureWorktreeRemote(request.workingDirectory);
3566
3527
  }
3567
3528
  }
3568
- return new Promise((resolve) => {
3529
+ return new Promise((resolve2) => {
3569
3530
  const [cmd, prefixArgs] = resolveCommand("codex");
3570
3531
  const resolvedArgs = [...prefixArgs, ...args];
3571
3532
  const child = spawn2(cmd, resolvedArgs, {
@@ -3616,7 +3577,7 @@ var CodexProvider = class {
3616
3577
  durationMs: Date.now() - startTime
3617
3578
  };
3618
3579
  if (degraded.length > 0) result.degradedCapabilities = degraded;
3619
- resolve(result);
3580
+ resolve2(result);
3620
3581
  };
3621
3582
  child.on("close", (code) => finish(code));
3622
3583
  child.on("error", (err) => finish(1, err.message));
@@ -3687,7 +3648,7 @@ var CodexProvider = class {
3687
3648
  };
3688
3649
 
3689
3650
  // src/providers/gemini-provider.ts
3690
- import { spawn as spawn3, execFileSync as execFileSync3 } from "child_process";
3651
+ import { spawn as spawn3, execFileSync as execFileSync2 } from "child_process";
3691
3652
  import { existsSync as existsSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, rmSync as rmSync3 } from "fs";
3692
3653
  import { join as join2, dirname } from "path";
3693
3654
  import { homedir } from "os";
@@ -3899,12 +3860,12 @@ var GeminiProvider = class _GeminiProvider {
3899
3860
  if (request.workingDirectory) {
3900
3861
  if (!existsSync2(request.workingDirectory)) {
3901
3862
  try {
3902
- execFileSync3("git", ["worktree", "add", "-b", request.workingDirectory, request.workingDirectory, "HEAD"], {
3863
+ execFileSync2("git", ["worktree", "add", "-b", request.workingDirectory, request.workingDirectory, "HEAD"], {
3903
3864
  stdio: "pipe"
3904
3865
  });
3905
3866
  } catch {
3906
3867
  try {
3907
- execFileSync3("git", ["worktree", "add", request.workingDirectory, request.workingDirectory], {
3868
+ execFileSync2("git", ["worktree", "add", request.workingDirectory, request.workingDirectory], {
3908
3869
  stdio: "pipe"
3909
3870
  });
3910
3871
  } catch {
@@ -3913,18 +3874,18 @@ var GeminiProvider = class _GeminiProvider {
3913
3874
  }
3914
3875
  } else {
3915
3876
  try {
3916
- execFileSync3("git", ["-C", request.workingDirectory, "rev-parse", "--git-dir"], {
3877
+ execFileSync2("git", ["-C", request.workingDirectory, "rev-parse", "--git-dir"], {
3917
3878
  stdio: "pipe"
3918
3879
  });
3919
3880
  } catch {
3920
3881
  try {
3921
3882
  rmSync3(request.workingDirectory, { recursive: true, force: true });
3922
- execFileSync3("git", ["worktree", "add", "-b", request.workingDirectory, request.workingDirectory, "HEAD"], {
3883
+ execFileSync2("git", ["worktree", "add", "-b", request.workingDirectory, request.workingDirectory, "HEAD"], {
3923
3884
  stdio: "pipe"
3924
3885
  });
3925
3886
  } catch {
3926
3887
  try {
3927
- execFileSync3("git", ["worktree", "add", request.workingDirectory, request.workingDirectory], {
3888
+ execFileSync2("git", ["worktree", "add", request.workingDirectory, request.workingDirectory], {
3928
3889
  stdio: "pipe"
3929
3890
  });
3930
3891
  } catch {
@@ -3942,7 +3903,7 @@ var GeminiProvider = class _GeminiProvider {
3942
3903
  if (request.workingDirectory && settingsDir) {
3943
3904
  this.copySettingsToDir(settingsDir, request.workingDirectory);
3944
3905
  }
3945
- return new Promise((resolve) => {
3906
+ return new Promise((resolve2) => {
3946
3907
  const [cmd, prefixArgs] = resolveCommand("gemini");
3947
3908
  const resolvedArgs = [...prefixArgs, ...args];
3948
3909
  const child = spawn3(cmd, resolvedArgs, {
@@ -4015,7 +3976,7 @@ var GeminiProvider = class _GeminiProvider {
4015
3976
  durationMs: Date.now() - startTime
4016
3977
  };
4017
3978
  if (degraded.length > 0) result.degradedCapabilities = degraded;
4018
- resolve(result);
3979
+ resolve2(result);
4019
3980
  };
4020
3981
  child.on("close", (code) => finish(code));
4021
3982
  child.on("error", (err) => finish(1, err.message));
@@ -4182,6 +4143,18 @@ node "${hookScript}" "${event}" "${configFile}" < "$STDIN_FILE"
4182
4143
  };
4183
4144
 
4184
4145
  // src/commands/pipeline.ts
4146
+ var execFileAsync = promisify(execFile);
4147
+ var _cachedPrimaryRepoRoot = null;
4148
+ async function getPrimaryRepoRoot() {
4149
+ if (_cachedPrimaryRepoRoot) return _cachedPrimaryRepoRoot;
4150
+ try {
4151
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"]);
4152
+ _cachedPrimaryRepoRoot = resolve(stdout.trim());
4153
+ } catch {
4154
+ _cachedPrimaryRepoRoot = resolve(process.cwd());
4155
+ }
4156
+ return _cachedPrimaryRepoRoot;
4157
+ }
4185
4158
  function createProviderRegistry() {
4186
4159
  const registry = new ProviderRegistry();
4187
4160
  registry.register(new ClaudeProvider());
@@ -4398,14 +4371,14 @@ Press Ctrl+C to cancel, or re-run with --yes to skip this warning.
4398
4371
  `);
4399
4372
  }
4400
4373
  function waitForConfirmation() {
4401
- return new Promise((resolve) => {
4374
+ return new Promise((resolve2) => {
4402
4375
  process.stdout.write("Continue? [y/N] ");
4403
4376
  process.stdin.setEncoding("utf8");
4404
4377
  process.stdin.once("data", (data) => {
4405
4378
  const answer = data.trim().toLowerCase();
4406
- resolve(answer === "y" || answer === "yes");
4379
+ resolve2(answer === "y" || answer === "yes");
4407
4380
  });
4408
- process.stdin.once("end", () => resolve(false));
4381
+ process.stdin.once("end", () => resolve2(false));
4409
4382
  });
4410
4383
  }
4411
4384
  async function runPipeline(client, args) {
@@ -4483,6 +4456,32 @@ async function runPipeline(client, args) {
4483
4456
  ...opts.maxBudget !== null && { maxBudgetUsd: opts.maxBudget },
4484
4457
  ...opts.model !== null && { model: opts.model }
4485
4458
  };
4459
+ if (effectiveConfig.worktreePath) {
4460
+ const primaryRepoRoot = await getPrimaryRepoRoot();
4461
+ const resolvedCwd = resolve(effectiveConfig.worktreePath);
4462
+ const inside = resolvedCwd === primaryRepoRoot || resolvedCwd.startsWith(primaryRepoRoot + sep) || primaryRepoRoot.startsWith(resolvedCwd + sep);
4463
+ if (inside) {
4464
+ 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.`;
4465
+ logger.orchestrator(errMsg);
4466
+ 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.`);
4467
+ return { reason: "error", iterations: 0, gutterCount: 0, lastError: errMsg };
4468
+ }
4469
+ }
4470
+ if (effectiveConfig.worktreePath) {
4471
+ const parent = dirname2(effectiveConfig.worktreePath);
4472
+ try {
4473
+ await fsPromises.mkdir(parent, { recursive: true });
4474
+ } catch (err) {
4475
+ const msg = err instanceof Error ? err.message : String(err);
4476
+ const errMsg = `[dispatch] failed ticket=${ticketId} reason=worktree_unrenderable path="${effectiveConfig.worktreePath}" error="${msg}"`;
4477
+ logger.orchestrator(errMsg);
4478
+ await deps.createComment(
4479
+ ticketId,
4480
+ `[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.`
4481
+ );
4482
+ return { reason: "error", iterations: 0, gutterCount: 0, lastError: errMsg };
4483
+ }
4484
+ }
4486
4485
  const mem = runMemory;
4487
4486
  const colScopeForName = await client.get(
4488
4487
  `/projects/${projectId}/pipeline-context`,
@@ -4682,7 +4681,27 @@ async function runPipeline(client, args) {
4682
4681
  const blockers = await client.get(
4683
4682
  `/projects/${projectId}/tickets/${ticketId}/unresolved-blockers`
4684
4683
  );
4685
- return blockers.length > 0;
4684
+ if (!blockers || blockers.length === 0) {
4685
+ return { blocked: false };
4686
+ }
4687
+ let linkIds = [];
4688
+ try {
4689
+ const links = await client.get(
4690
+ `/projects/${projectId}/ticket-links?ticketId=${ticketId}&linkType=blocks`
4691
+ );
4692
+ linkIds = (links ?? []).filter((l) => l.target_id === ticketId && l.link_type === "blocks").map((l) => l.id);
4693
+ } catch {
4694
+ linkIds = [];
4695
+ }
4696
+ return {
4697
+ blocked: true,
4698
+ blockers: blockers.map((b) => ({
4699
+ id: b.id,
4700
+ ticket_number: b.ticket_number,
4701
+ column: b.column
4702
+ })),
4703
+ linkIds
4704
+ };
4686
4705
  },
4687
4706
  dispatchLightCall: async (ticketId, columnId) => {
4688
4707
  const [ticketCtx, colScope, boardScope] = await Promise.all([
@@ -4796,6 +4815,7 @@ async function runPipeline(client, args) {
4796
4815
  appendRunMemory: (section, content) => runMemory ? runMemory.append(section, content) : Promise.resolve(),
4797
4816
  cleanupWorktree: (name) => cleanupWorktree(name),
4798
4817
  mergeWorktree: (name, integrationBranch) => mergeWorktreeBranch(name, integrationBranch),
4818
+ detectBranchMerged: async (opts2) => detectBranchMerged(opts2),
4799
4819
  // Pipeline event emission — wsClient captured by reference (set later before any loops run)
4800
4820
  emitPipelineEvent: (event) => {
4801
4821
  wsClient?.send(event);
@@ -5127,7 +5147,7 @@ async function waitForAllLoops(orchestrator, timeoutMs = 4 * 60 * 60 * 1e3) {
5127
5147
  );
5128
5148
  return;
5129
5149
  }
5130
- await new Promise((resolve) => setTimeout(resolve, 1e3));
5150
+ await new Promise((resolve2) => setTimeout(resolve2, 1e3));
5131
5151
  if (activeRalphLoops.size === 0 && orchestrator.activeLoopCount === 0 && !orchestrator.hasActiveQueuedWork && !orchestrator.hasCompletingWork) {
5132
5152
  consecutiveIdle++;
5133
5153
  } else {
@@ -5177,4 +5197,4 @@ export {
5177
5197
  runPipeline,
5178
5198
  stopPipeline
5179
5199
  };
5180
- //# sourceMappingURL=pipeline-GZOSDNPF.js.map
5200
+ //# sourceMappingURL=pipeline-NRG2Q2TE.js.map