kantban-cli 0.1.38 → 0.1.40

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.
@@ -12,7 +12,7 @@ import {
12
12
  generateMcpConfig,
13
13
  parseJsonFromLlmOutput,
14
14
  parseStuckDetectionResponse
15
- } from "./chunk-2CY5OPZN.js";
15
+ } from "./chunk-FOVFBT7C.js";
16
16
  import {
17
17
  LoopCheckpointSchema,
18
18
  VerdictSchema,
@@ -50,9 +50,17 @@ function resolveToolRestrictions(builtinTools, allowedTools, disallowedTools) {
50
50
  // src/lib/worktree.ts
51
51
  import { execFile as defaultExecFile, execFileSync } from "child_process";
52
52
  function generateWorktreeName(ticketNumber, columnName) {
53
- const slug = columnName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
53
+ const slug = columnSlug(columnName);
54
54
  return `kantban-${ticketNumber}-${slug}`;
55
55
  }
56
+ function columnSlug(columnName) {
57
+ return columnName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
58
+ }
59
+ function renderWorktreePath(ticketNumber, columnName, pathPattern) {
60
+ const worktreeName = generateWorktreeName(ticketNumber, columnName);
61
+ if (!pathPattern) return worktreeName;
62
+ return pathPattern.replace(/\{ticket_number\}/g, String(ticketNumber)).replace(/\{column_slug\}/g, columnSlug(columnName)).replace(/\{worktree_name\}/g, worktreeName);
63
+ }
56
64
  function isPlausibleRemoteUrl(url) {
57
65
  return url.includes("/") || url.includes("@") || url.includes("://");
58
66
  }
@@ -91,16 +99,18 @@ function ensureWorktreeRemote(worktreePath) {
91
99
  }
92
100
  }
93
101
  async function cleanupWorktree(worktreeName, exec = defaultExecFile) {
102
+ let target = worktreeName;
94
103
  try {
95
104
  const path = await findWorktreeForBranch(exec, worktreeName);
96
105
  if (!path) return true;
106
+ target = path;
97
107
  } catch {
98
108
  return true;
99
109
  }
100
110
  return new Promise((resolve) => {
101
- exec("git", ["worktree", "remove", "--force", worktreeName], (err) => {
111
+ exec("git", ["worktree", "remove", "--force", target], (err) => {
102
112
  if (err) {
103
- console.error(`[worktree] cleanup failed for ${worktreeName}: ${err.message}`);
113
+ console.error(`[worktree] cleanup failed for ${worktreeName} (${target}): ${err.message}`);
104
114
  resolve(false);
105
115
  } else {
106
116
  resolve(true);
@@ -486,6 +496,8 @@ var PipelineOrchestrator = class {
486
496
  spawning = /* @__PURE__ */ new Set();
487
497
  /** Ticket IDs currently in onLoopComplete (prevents double-spawn during async advisor recovery) */
488
498
  completing = /* @__PURE__ */ new Set();
499
+ /** Ticket IDs aborted due to ticket:moved — suppresses onLoopComplete comment/advisor */
500
+ abortedForMove = /* @__PURE__ */ new Set();
489
501
  /** Per-column reservation count for in-flight spawns (prevents concurrency overshoot) */
490
502
  columnReservations = /* @__PURE__ */ new Map();
491
503
  /** Cached board scope for constraint evaluation — refreshed each scan cycle */
@@ -592,6 +604,7 @@ var PipelineOrchestrator = class {
592
604
  modelPreference: cfg?.model_preference,
593
605
  maxBudgetUsd: cfg?.max_budget_usd,
594
606
  worktreeEnabled: cfg?.worktree?.enabled,
607
+ worktreePathPattern: cfg?.worktree?.path_pattern,
595
608
  worktreeOnMove: cfg?.worktree?.on_move,
596
609
  worktreeOnDone: cfg?.worktree?.on_done,
597
610
  worktreeIntegrationBranch: cfg?.worktree?.integration_branch,
@@ -924,6 +937,26 @@ var PipelineOrchestrator = class {
924
937
  switch (event.type) {
925
938
  case "ticket:moved":
926
939
  case "ticket:created": {
940
+ if (event.type === "ticket:moved" && event.fromColumnId) {
941
+ const oldLoop = this.activeLoops.get(event.ticketId);
942
+ if (oldLoop) {
943
+ oldLoop.abort.abort();
944
+ this.activeLoops.delete(event.ticketId);
945
+ this.abortedForMove.add(event.ticketId);
946
+ }
947
+ this.knownTickets.delete(event.ticketId);
948
+ this.spawning.delete(event.ticketId);
949
+ this.deferredTickets.delete(event.ticketId);
950
+ const oldQueue = this.loopQueues.get(event.fromColumnId);
951
+ if (oldQueue) {
952
+ const idx = oldQueue.indexOf(event.ticketId);
953
+ if (idx !== -1) oldQueue.splice(idx, 1);
954
+ }
955
+ void this.drainQueue(event.fromColumnId).catch((err) => {
956
+ const msg = err instanceof Error ? err.message : String(err);
957
+ console.error(` [error] drainQueue failed for old column ${event.fromColumnId}: ${msg}`);
958
+ });
959
+ }
927
960
  if (event.columnId && this.pipelineColumns.has(event.columnId)) {
928
961
  if (this.isColumnBlocked(event.columnId)) {
929
962
  console.error(` [event] ${event.type} ${event.ticketId} \u2192 column ${event.columnId}: BLOCKED by firing constraints \u2014 deferred`);
@@ -1113,7 +1146,8 @@ var PipelineOrchestrator = class {
1113
1146
  lastError: err instanceof Error ? err.message : String(err)
1114
1147
  })
1115
1148
  );
1116
- this.activeLoops.set(ticketId, { columnId, promise });
1149
+ const abort = new AbortController();
1150
+ this.activeLoops.set(ticketId, { columnId, promise, abort });
1117
1151
  this.lastFiredAt.set(columnId, /* @__PURE__ */ new Date());
1118
1152
  void promise.then(
1119
1153
  (result) => this.onLoopComplete(ticketId, columnId, result).catch((err) => {
@@ -1138,7 +1172,8 @@ var PipelineOrchestrator = class {
1138
1172
  });
1139
1173
  placeholder.catch(() => {
1140
1174
  });
1141
- this.activeLoops.set(ticketId, { columnId, promise: placeholder });
1175
+ const abort = new AbortController();
1176
+ this.activeLoops.set(ticketId, { columnId, promise: placeholder, abort });
1142
1177
  this.lastFiredAt.set(columnId, /* @__PURE__ */ new Date());
1143
1178
  void readCheckpoint(
1144
1179
  {
@@ -1150,6 +1185,7 @@ var PipelineOrchestrator = class {
1150
1185
  columnId
1151
1186
  ).then((checkpoint) => {
1152
1187
  clearTimeout(timeoutId);
1188
+ if (!this.activeLoops.has(ticketId)) return;
1153
1189
  if (checkpoint) {
1154
1190
  console.error(` [checkpoint] Resuming ${ticketId} at iteration ${String(checkpoint.iteration + 1)} (model: ${checkpoint.model_tier})`);
1155
1191
  this.startLoopWithConfig(ticketId, columnId, config, checkpoint.iteration + 1, checkpoint.gutter_count, modelOverride ?? checkpoint.model_tier, checkpoint.last_fingerprint);
@@ -1158,6 +1194,7 @@ var PipelineOrchestrator = class {
1158
1194
  }
1159
1195
  }).catch((err) => {
1160
1196
  clearTimeout(timeoutId);
1197
+ if (!this.activeLoops.has(ticketId)) return;
1161
1198
  const msg = err instanceof Error ? err.message : String(err);
1162
1199
  console.error(` [warn] Checkpoint read failed for ${ticketId} (starting fresh): ${msg}`);
1163
1200
  this.startLoopWithConfig(ticketId, columnId, config, void 0, void 0, modelOverride);
@@ -1177,12 +1214,16 @@ var PipelineOrchestrator = class {
1177
1214
  gutterThreshold: config.gutterThreshold,
1178
1215
  ...effectiveModel !== void 0 && { model: effectiveModel },
1179
1216
  ...config.maxBudgetUsd !== void 0 && { maxBudgetUsd: config.maxBudgetUsd },
1180
- // Resolve worktree name from ticket context
1217
+ // Resolve worktree name (branch) and path from ticket context. When the
1218
+ // column's agent_config specifies a path_pattern, the filesystem path is
1219
+ // rendered from it — otherwise the name doubles as a relative path.
1181
1220
  ...(() => {
1182
1221
  const colScope = this.columnScopes.get(columnId);
1183
1222
  const ticket = colScope?.tickets.find((t) => t.id === ticketId);
1184
- const wName = config.worktreeEnabled && ticket ? generateWorktreeName(ticket.ticket_number, config.name) : void 0;
1185
- return wName !== void 0 ? { worktreeName: wName } : {};
1223
+ if (!config.worktreeEnabled || !ticket) return {};
1224
+ const wName = generateWorktreeName(ticket.ticket_number, config.name);
1225
+ const wPath = renderWorktreePath(ticket.ticket_number, config.name, config.worktreePathPattern);
1226
+ return wPath !== wName ? { worktreeName: wName, worktreePath: wPath } : { worktreeName: wName };
1186
1227
  })(),
1187
1228
  ...config.lookaheadColumnId !== void 0 && { lookaheadColumnId: config.lookaheadColumnId },
1188
1229
  // Resume from checkpoint iteration/gutter if provided
@@ -1200,8 +1241,10 @@ var PipelineOrchestrator = class {
1200
1241
  if (toolRestrictions.tools !== void 0 || toolRestrictions.allowedTools || toolRestrictions.disallowedTools) {
1201
1242
  loopConfig.toolRestrictions = toolRestrictions;
1202
1243
  }
1244
+ const abort = new AbortController();
1245
+ loopConfig.abortSignal = abort.signal;
1203
1246
  const promise = this.deps.startLoop(ticketId, columnId, loopConfig);
1204
- this.activeLoops.set(ticketId, { columnId, promise });
1247
+ this.activeLoops.set(ticketId, { columnId, promise, abort });
1205
1248
  this.lastFiredAt.set(columnId, /* @__PURE__ */ new Date());
1206
1249
  void promise.then(
1207
1250
  (result) => this.onLoopComplete(ticketId, columnId, result).catch((err) => {
@@ -1661,6 +1704,11 @@ ${findingsText}`)
1661
1704
  async onLoopComplete(ticketId, columnId, result) {
1662
1705
  this.completing.add(ticketId);
1663
1706
  this.activeLoops.delete(ticketId);
1707
+ if (this.abortedForMove.has(ticketId)) {
1708
+ this.abortedForMove.delete(ticketId);
1709
+ this.completing.delete(ticketId);
1710
+ return;
1711
+ }
1664
1712
  try {
1665
1713
  const colScope = this.columnScopes.get(columnId);
1666
1714
  const ticket = colScope?.tickets.find((t) => t.id === ticketId);
@@ -3301,14 +3349,15 @@ var CodexProvider = class {
3301
3349
  const env = this.buildEnv();
3302
3350
  const startTime = Date.now();
3303
3351
  if (request.workingDirectory) {
3352
+ const branch = request.branch ?? request.workingDirectory;
3304
3353
  if (!existsSync(request.workingDirectory)) {
3305
3354
  try {
3306
- execFileSync2("git", ["worktree", "add", "-b", request.workingDirectory, request.workingDirectory, "HEAD"], {
3355
+ execFileSync2("git", ["worktree", "add", "-b", branch, request.workingDirectory, "HEAD"], {
3307
3356
  stdio: "pipe"
3308
3357
  });
3309
3358
  } catch {
3310
3359
  try {
3311
- execFileSync2("git", ["worktree", "add", request.workingDirectory, request.workingDirectory], {
3360
+ execFileSync2("git", ["worktree", "add", request.workingDirectory, branch], {
3312
3361
  stdio: "pipe"
3313
3362
  });
3314
3363
  } catch {
@@ -3323,12 +3372,12 @@ var CodexProvider = class {
3323
3372
  } catch {
3324
3373
  try {
3325
3374
  rmSync2(request.workingDirectory, { recursive: true, force: true });
3326
- execFileSync2("git", ["worktree", "add", "-b", request.workingDirectory, request.workingDirectory, "HEAD"], {
3375
+ execFileSync2("git", ["worktree", "add", "-b", branch, request.workingDirectory, "HEAD"], {
3327
3376
  stdio: "pipe"
3328
3377
  });
3329
3378
  } catch {
3330
3379
  try {
3331
- execFileSync2("git", ["worktree", "add", request.workingDirectory, request.workingDirectory], {
3380
+ execFileSync2("git", ["worktree", "add", request.workingDirectory, branch], {
3332
3381
  stdio: "pipe"
3333
3382
  });
3334
3383
  } catch {
@@ -3423,6 +3472,8 @@ var CodexProvider = class {
3423
3472
  for (const [name, server] of Object.entries(request.mcpConfig.servers)) {
3424
3473
  args.push("-c", `mcp_servers.${name}.command=${JSON.stringify(server.command)}`);
3425
3474
  args.push("-c", `mcp_servers.${name}.args=${JSON.stringify(server.args)}`);
3475
+ args.push("-c", `mcp_servers.${name}.tool_timeout_sec=1800`);
3476
+ args.push("-c", `mcp_servers.${name}.startup_timeout_sec=120`);
3426
3477
  if (Object.keys(server.env).length > 0) {
3427
3478
  for (const [key, value] of Object.entries(server.env)) {
3428
3479
  args.push("-c", `mcp_servers.${name}.env.${key}=${JSON.stringify(value)}`);
@@ -4740,6 +4791,7 @@ Received ${signal}. Shutting down gracefully...`);
4740
4791
  const ticket = payload["ticket"];
4741
4792
  const ticketId = (typeof payload["ticketId"] === "string" ? payload["ticketId"] : null) ?? (ticket && typeof ticket["id"] === "string" ? ticket["id"] : null);
4742
4793
  const columnId = (typeof payload["columnId"] === "string" ? payload["columnId"] : null) ?? (ticket && typeof ticket["column_id"] === "string" ? ticket["column_id"] : null);
4794
+ const fromColumnId = typeof payload["fromColumnId"] === "string" ? payload["fromColumnId"] : null;
4743
4795
  if (wsEvent.type === "firing_constraint:created" || wsEvent.type === "firing_constraint:updated" || wsEvent.type === "firing_constraint:deleted") {
4744
4796
  logger.orchestrator(`WS event: ${wsEvent.type} \u2014 refreshing constraint caches`);
4745
4797
  void orchestrator.refreshConstraints().catch((err) => {
@@ -4751,9 +4803,9 @@ Received ${signal}. Shutting down gracefully...`);
4751
4803
  if (!ticketId) return;
4752
4804
  const eventType = wsEvent.type;
4753
4805
  if (eventType === "ticket:created" || eventType === "ticket:moved" || eventType === "ticket:updated" || eventType === "ticket:archived" || eventType === "ticket:deleted") {
4754
- const pipelineEvent = { type: eventType, ticketId, columnId };
4806
+ const pipelineEvent = { type: eventType, ticketId, columnId, fromColumnId };
4755
4807
  eventQueue.push(pipelineEvent);
4756
- logger.orchestrator(`WS event: ${eventType} ticket=${ticketId} column=${columnId ?? "null"}`);
4808
+ logger.orchestrator(`WS event: ${eventType} ticket=${ticketId} column=${columnId ?? "null"} from=${fromColumnId ?? "null"}`);
4757
4809
  }
4758
4810
  },
4759
4811
  onConnect: () => {
@@ -4896,4 +4948,4 @@ export {
4896
4948
  runPipeline,
4897
4949
  stopPipeline
4898
4950
  };
4899
- //# sourceMappingURL=pipeline-IAKINX5A.js.map
4951
+ //# sourceMappingURL=pipeline-ILE7LMGI.js.map