kantban-cli 0.1.46 → 0.1.49

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-SDMNXFPV.js";
3
+ } from "./chunk-DENXSVKE.js";
4
4
  import {
5
5
  ClaudeProvider,
6
6
  RalphLoop,
@@ -11,15 +11,17 @@ import {
11
11
  generateGateProxyMcpConfig,
12
12
  generateMcpConfig,
13
13
  parseJsonFromLlmOutput,
14
- parseStuckDetectionResponse
15
- } from "./chunk-N4ZHMJD7.js";
14
+ parseStuckDetectionResponse,
15
+ reapOrphanedMcpConfigDirs
16
+ } from "./chunk-QHJZIGEE.js";
16
17
  import {
17
18
  LoopCheckpointSchema,
18
19
  VerdictSchema,
20
+ findUnmatchedColumnKeys,
19
21
  parseGateConfig,
20
22
  parseTimeout,
21
23
  resolveGatesForColumn
22
- } from "./chunk-DAFLEMLK.js";
24
+ } from "./chunk-4R27WTCJ.js";
23
25
  import {
24
26
  IS_WINDOWS,
25
27
  crossSpawnOptions,
@@ -31,7 +33,13 @@ import {
31
33
  // src/commands/pipeline.ts
32
34
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync3, unlinkSync as unlinkSync3, existsSync as existsSync3, appendFileSync as appendFileSync2 } from "fs";
33
35
  import { homedir as homedir2 } from "os";
34
- import { join as join3 } from "path";
36
+ import { join as join3, dirname as dirname2 } from "path";
37
+
38
+ // src/lib/harness-signal.ts
39
+ var HARNESS_SIGNAL_PREFIX = "@harness:";
40
+ function createHarnessSignalContent(kind, payload) {
41
+ return HARNESS_SIGNAL_PREFIX + JSON.stringify({ ...payload, kind });
42
+ }
35
43
 
36
44
  // src/lib/tool-profiles.ts
37
45
  function resolveToolRestrictions(builtinTools, allowedTools, disallowedTools) {
@@ -469,7 +477,9 @@ function parseReplannerResponse(raw) {
469
477
  }
470
478
 
471
479
  // src/lib/orchestrator.ts
472
- var ACTIVE_LOOP_ZOMBIE_TTL_MS = 90 * 60 * 1e3;
480
+ var ACTIVE_LOOP_ZOMBIE_TTL_MS = Number(
481
+ process.env.ACTIVE_LOOP_ZOMBIE_TTL_MS
482
+ ) || 3 * 60 * 60 * 1e3;
473
483
  function classifyTier(input) {
474
484
  if (input.invocationTier === "light") return "light";
475
485
  if (input.invocationTier === "heavy") return "heavy";
@@ -509,6 +519,12 @@ var PipelineOrchestrator = class {
509
519
  blockedColumns = /* @__PURE__ */ new Set();
510
520
  /** Per-ticket advisor invocation count for the current column transit */
511
521
  advisorBudget = /* @__PURE__ */ new Map();
522
+ /** Count of advisor-driven retries per ticket in the current column transit.
523
+ * Cleared on ticket:moved/archived/deleted and in terminalCleanup. */
524
+ gutterResetCount = /* @__PURE__ */ new Map();
525
+ gutterResetExhausted = /* @__PURE__ */ new Set();
526
+ /** Per-ticket deferred-dispatch signal state — used for dedup and cleanup */
527
+ deferredState = /* @__PURE__ */ new Map();
512
528
  /** Per-ticket model override (set by RETRY_DIFFERENT_MODEL, consumed by startTrackedLoop) */
513
529
  ticketModelOverrides = /* @__PURE__ */ new Map();
514
530
  /** Stable session ID for this orchestrator instance (pipeline run) */
@@ -550,6 +566,14 @@ var PipelineOrchestrator = class {
550
566
  get pipelineColumnIds() {
551
567
  return Array.from(this.pipelineColumns.keys());
552
568
  }
569
+ /** Returns the display names of every column on the board (not just pipeline columns). */
570
+ get allBoardColumnNames() {
571
+ return this.cachedBoardScope?.columns.map((c) => c.name) ?? [];
572
+ }
573
+ /** Returns {name, type} for every column on the board. */
574
+ get allBoardColumns() {
575
+ return this.cachedBoardScope?.columns.map((c) => ({ name: c.name, type: c.type })) ?? [];
576
+ }
553
577
  /** Returns the total number of active loops. */
554
578
  get activeLoopCount() {
555
579
  return this.activeLoops.size;
@@ -628,12 +652,16 @@ var PipelineOrchestrator = class {
628
652
  } : void 0,
629
653
  builtinTools: cfg?.builtin_tools,
630
654
  allowedTools: cfg?.allowed_tools,
631
- disallowedTools: cfg?.disallowed_tools
655
+ disallowedTools: cfg?.disallowed_tools,
656
+ maxGutterResetsPerTransit: cfg?.max_gutter_resets_per_transit,
657
+ repromptOnBranchMerged: cfg?.reprompt_on_branch_merged,
658
+ maxRepromptAttempts: cfg?.max_reprompt_attempts
632
659
  });
633
660
  this.columnScopes.set(col.id, colScope);
634
661
  this.loopQueues.set(col.id, []);
635
662
  })
636
663
  );
664
+ console.error(`[orchestrator] zombie TTL: ${ACTIVE_LOOP_ZOMBIE_TTL_MS}ms`);
637
665
  }
638
666
  /**
639
667
  * Refresh the cached column scope for a single column.
@@ -939,6 +967,9 @@ var PipelineOrchestrator = class {
939
967
  if (this.isColumnBlocked(columnId)) {
940
968
  this.blockedColumns.add(columnId);
941
969
  console.error(` [scan] Column ${columnId} (${colScope.column.name}): BLOCKED by firing constraints \u2014 skipping ${String(colScope.tickets.length)} ticket(s)`);
970
+ for (const ticket of colScope.tickets) {
971
+ await this.emitDispatchDeferred(ticket.id, "firing_constraint", { columnId });
972
+ }
942
973
  continue;
943
974
  }
944
975
  console.error(` [scan] Column ${columnId} (${colScope.column.name}): ${String(colScope.tickets.length)} ticket(s)`);
@@ -965,6 +996,7 @@ var PipelineOrchestrator = class {
965
996
  this.knownTickets.delete(event.ticketId);
966
997
  this.spawning.delete(event.ticketId);
967
998
  this.deferredTickets.delete(event.ticketId);
999
+ void this.clearDispatchDeferred(event.ticketId);
968
1000
  const oldQueue = this.loopQueues.get(event.fromColumnId);
969
1001
  if (oldQueue) {
970
1002
  const idx = oldQueue.indexOf(event.ticketId);
@@ -979,6 +1011,7 @@ var PipelineOrchestrator = class {
979
1011
  if (this.isColumnBlocked(event.columnId)) {
980
1012
  console.error(` [event] ${event.type} ${event.ticketId} \u2192 column ${event.columnId}: BLOCKED by firing constraints \u2014 deferred`);
981
1013
  this.deferredTickets.set(event.ticketId, event.columnId);
1014
+ await this.emitDispatchDeferred(event.ticketId, "firing_constraint", { columnId: event.columnId });
982
1015
  } else {
983
1016
  await this.spawnOrQueue(event.ticketId, event.columnId, true);
984
1017
  }
@@ -1028,6 +1061,9 @@ var PipelineOrchestrator = class {
1028
1061
  this.deferredTickets.delete(event.ticketId);
1029
1062
  this.spawning.delete(event.ticketId);
1030
1063
  this.advisorBudget.delete(event.ticketId);
1064
+ this.gutterResetCount.delete(event.ticketId);
1065
+ this.gutterResetExhausted.delete(event.ticketId);
1066
+ void this.clearDispatchDeferred(event.ticketId);
1031
1067
  for (const [, queue] of this.loopQueues) {
1032
1068
  const idx = queue.indexOf(event.ticketId);
1033
1069
  if (idx !== -1) {
@@ -1071,6 +1107,7 @@ var PipelineOrchestrator = class {
1071
1107
  this.releaseSlot(columnId);
1072
1108
  this.deferredTickets.set(ticketId, columnId);
1073
1109
  console.error(` [skip] ${ticketId} has unresolved blockers \u2014 deferred`);
1110
+ void this.emitDispatchDeferred(ticketId, "unresolved_blockers", { columnId });
1074
1111
  void this.drainQueue(columnId).catch((err) => {
1075
1112
  const msg = err instanceof Error ? err.message : String(err);
1076
1113
  console.error(` [error] drainQueue failed for column ${columnId}: ${msg}`);
@@ -1083,6 +1120,7 @@ var PipelineOrchestrator = class {
1083
1120
  this.spawning.delete(ticketId);
1084
1121
  this.releaseSlot(columnId);
1085
1122
  this.deferredTickets.set(ticketId, columnId);
1123
+ void this.emitDispatchDeferred(ticketId, "unresolved_blockers", { columnId });
1086
1124
  void this.drainQueue(columnId).catch((err2) => {
1087
1125
  const msg2 = err2 instanceof Error ? err2.message : String(err2);
1088
1126
  console.error(` [error] drainQueue failed for column ${columnId}: ${msg2}`);
@@ -1091,6 +1129,7 @@ var PipelineOrchestrator = class {
1091
1129
  }
1092
1130
  try {
1093
1131
  await this.deps.claimTicket(ticketId);
1132
+ await this.clearDispatchDeferred(ticketId);
1094
1133
  this.startTrackedLoop(ticketId, columnId, colConfig);
1095
1134
  } catch (err) {
1096
1135
  const msg = err instanceof Error ? err.message : String(err);
@@ -1249,6 +1288,8 @@ var PipelineOrchestrator = class {
1249
1288
  ...startGutterCount !== void 0 && { startGutterCount },
1250
1289
  ...startFingerprint !== void 0 && { startFingerprint },
1251
1290
  ...config.stuckDetection && { stuckDetection: config.stuckDetection },
1291
+ ...config.repromptOnBranchMerged !== void 0 && { repromptOnBranchMerged: config.repromptOnBranchMerged },
1292
+ ...config.maxRepromptAttempts !== void 0 && { maxRepromptAttempts: config.maxRepromptAttempts },
1252
1293
  ...this.deps.costTracker && { isBudgetExhausted: () => this.deps.costTracker.isExhausted() }
1253
1294
  };
1254
1295
  const toolRestrictions = resolveToolRestrictions(
@@ -1278,6 +1319,33 @@ var PipelineOrchestrator = class {
1278
1319
  })
1279
1320
  );
1280
1321
  }
1322
+ /**
1323
+ * Increment the gutter-reset counter and check the per-column cap.
1324
+ * Returns false if the cap has been exceeded (and emits signal + comment).
1325
+ * Callers should abort the retry on false.
1326
+ */
1327
+ async checkGutterResetCap(ticketId, colConfig) {
1328
+ const cap = colConfig.maxGutterResetsPerTransit ?? 2;
1329
+ const count = (this.gutterResetCount.get(ticketId) ?? 0) + 1;
1330
+ this.gutterResetCount.set(ticketId, count);
1331
+ if (count > cap) {
1332
+ this.gutterResetExhausted.add(ticketId);
1333
+ await this.emitHarnessSignal(ticketId, "agent_stalled_exhausted", {
1334
+ resetCount: count,
1335
+ cap
1336
+ });
1337
+ await this.safeAction(
1338
+ ticketId,
1339
+ "stalled:exhaustedComment",
1340
+ () => this.deps.createComment(
1341
+ ticketId,
1342
+ `Stalled after ${String(count)} gutter reset attempt(s) (cap: ${String(cap)}) \u2014 human review needed. See 'agent_stalled_exhausted' signal for details.`
1343
+ )
1344
+ );
1345
+ return false;
1346
+ }
1347
+ return true;
1348
+ }
1281
1349
  /**
1282
1350
  * Phase 2: Invoke the advisor for failure recovery.
1283
1351
  * Returns true if the ticket should be retried, false if handled.
@@ -1381,6 +1449,10 @@ var PipelineOrchestrator = class {
1381
1449
  }
1382
1450
  switch (response.action) {
1383
1451
  case "RETRY_WITH_FEEDBACK": {
1452
+ if (!await this.checkGutterResetCap(ticketId, colConfig)) {
1453
+ this.completing.delete(ticketId);
1454
+ return false;
1455
+ }
1384
1456
  if (response.feedback) {
1385
1457
  await this.safeAction(
1386
1458
  ticketId,
@@ -1395,6 +1467,10 @@ ${response.feedback}`)
1395
1467
  return true;
1396
1468
  }
1397
1469
  case "RETRY_DIFFERENT_MODEL": {
1470
+ if (!await this.checkGutterResetCap(ticketId, colConfig)) {
1471
+ this.completing.delete(ticketId);
1472
+ return false;
1473
+ }
1398
1474
  const currentModel = result.model ?? colConfig.modelPreference ?? "default";
1399
1475
  const escalation = colConfig.modelRouting?.escalation ?? [];
1400
1476
  let nextModel;
@@ -1553,10 +1629,17 @@ ${response.feedback}`)
1553
1629
  );
1554
1630
  }
1555
1631
  }
1632
+ await this.emitHarnessSignal(ticketId, "advisor_action", {
1633
+ action: "ESCALATE",
1634
+ reason: response.reason
1635
+ });
1556
1636
  await this.safeAction(
1557
1637
  ticketId,
1558
- "advisor:createComment",
1559
- () => this.deps.createComment(ticketId, `ADVISOR: Escalated for human review \u2014 ${response.reason}`)
1638
+ "advisor:escalateComment",
1639
+ () => this.deps.createComment(
1640
+ ticketId,
1641
+ `Needs human review \u2014 see 'advisor_action' signal for details.`
1642
+ )
1560
1643
  );
1561
1644
  return false;
1562
1645
  }
@@ -1812,6 +1895,8 @@ ${findingsText}`)
1812
1895
  const colConfig = this.pipelineColumns.get(columnId);
1813
1896
  const terminalCleanup = async () => {
1814
1897
  this.advisorBudget.delete(ticketId);
1898
+ this.gutterResetCount.delete(ticketId);
1899
+ this.gutterResetExhausted.delete(ticketId);
1815
1900
  if (colConfig?.checkpointEnabled && this.deps.setFieldValue && result.reason !== "stopped") {
1816
1901
  await this.safeAction(
1817
1902
  ticketId,
@@ -1945,47 +2030,41 @@ ${findingsText}`)
1945
2030
  }
1946
2031
  break;
1947
2032
  case "max_iterations":
1948
- comment = `Pipeline agent reached iteration limit (${result.iterations}) without advancing. Manual review needed.`;
2033
+ await this.emitHarnessSignal(ticketId, "agent_max_iterations", {
2034
+ iterations: result.iterations
2035
+ });
2036
+ comment = `Needs human review \u2014 see 'agent_max_iterations' signal for details.`;
1949
2037
  break;
1950
2038
  case "stalled":
1951
- comment = `Pipeline agent stalled \u2014 no progress for ${result.gutterCount} consecutive iterations (of ${result.iterations} total). Manual review needed.`;
2039
+ if (!this.gutterResetExhausted.has(ticketId)) {
2040
+ await this.emitHarnessSignal(ticketId, "agent_stalled", {
2041
+ iterations: result.iterations,
2042
+ gutterCount: result.gutterCount
2043
+ });
2044
+ }
1952
2045
  break;
1953
2046
  case "error":
1954
- comment = `Pipeline agent encountered an error after ${result.iterations} iteration(s): ${result.lastError ?? "unknown error"}`;
2047
+ await this.emitHarnessSignal(ticketId, "agent_error", {
2048
+ iterations: result.iterations,
2049
+ lastError: result.lastError ?? "unknown error"
2050
+ });
2051
+ comment = `Needs human review \u2014 see 'agent_error' signal for details.`;
1955
2052
  break;
1956
2053
  case "stopped":
1957
2054
  comment = `Pipeline agent was stopped externally after ${result.iterations} iteration(s).`;
1958
2055
  break;
1959
2056
  case "deleted":
1960
- comment = `Pipeline agent stopped \u2014 ticket was deleted or archived during iteration ${result.iterations}.`;
1961
2057
  break;
1962
2058
  case "budget":
1963
2059
  comment = `Pipeline budget exhausted after ${result.iterations} iteration(s). Increase token budget in pipeline.gates.yaml settings.budget to continue.`;
1964
2060
  break;
1965
2061
  }
1966
- if (result.reason !== "deleted") {
2062
+ if (comment !== void 0) {
1967
2063
  await this.deps.createComment(ticketId, comment).catch((err) => {
1968
2064
  const msg = err instanceof Error ? err.message : String(err);
1969
2065
  console.error(` [warn] Failed to write completion comment for ${ticketId}: ${msg}`);
1970
2066
  });
1971
2067
  }
1972
- if (result.reason === "stalled") {
1973
- await this.deps.createSignal(
1974
- ticketId,
1975
- `Previous pipeline run stalled after ${result.iterations} iterations with no progress. Review comments for details before retrying.`
1976
- ).catch((err) => {
1977
- const msg = err instanceof Error ? err.message : String(err);
1978
- console.error(` [warn] Failed to write signal for ${ticketId}: ${msg}`);
1979
- });
1980
- } else if (result.reason === "error") {
1981
- await this.deps.createSignal(
1982
- ticketId,
1983
- `Previous pipeline run failed after ${result.iterations} iteration(s): ${result.lastError ?? "unknown error"}. Review comments for details before retrying.`
1984
- ).catch((err) => {
1985
- const msg = err instanceof Error ? err.message : String(err);
1986
- console.error(` [warn] Failed to write signal for ${ticketId}: ${msg}`);
1987
- });
1988
- }
1989
2068
  await terminalCleanup();
1990
2069
  } finally {
1991
2070
  this.completing.delete(ticketId);
@@ -2112,6 +2191,7 @@ ${findingsText}`)
2112
2191
  this.deferredTickets.set(nextTicketId, columnId);
2113
2192
  this.spawning.delete(nextTicketId);
2114
2193
  this.releaseSlot(columnId);
2194
+ void this.emitDispatchDeferred(nextTicketId, "unresolved_blockers", { columnId });
2115
2195
  continue;
2116
2196
  }
2117
2197
  } catch (err) {
@@ -2120,10 +2200,12 @@ ${findingsText}`)
2120
2200
  this.deferredTickets.set(nextTicketId, columnId);
2121
2201
  this.spawning.delete(nextTicketId);
2122
2202
  this.releaseSlot(columnId);
2203
+ void this.emitDispatchDeferred(nextTicketId, "unresolved_blockers", { columnId });
2123
2204
  continue;
2124
2205
  }
2125
2206
  try {
2126
2207
  await this.deps.claimTicket(nextTicketId);
2208
+ await this.clearDispatchDeferred(nextTicketId);
2127
2209
  this.startTrackedLoop(nextTicketId, columnId, colConfig);
2128
2210
  } catch (err) {
2129
2211
  const msg = err instanceof Error ? err.message : String(err);
@@ -2152,6 +2234,63 @@ ${findingsText}`)
2152
2234
  const current = this.columnReservations.get(columnId) ?? 0;
2153
2235
  if (current > 0) this.columnReservations.set(columnId, current - 1);
2154
2236
  }
2237
+ /**
2238
+ * Emit a dispatch_deferred harness signal for a ticket.
2239
+ * State-change dedup: same reason → no-op; different reason → delete old + create new.
2240
+ * Best-effort: signal creation failures are logged but never thrown.
2241
+ */
2242
+ /**
2243
+ * Emit a structured harness signal on a ticket. Uses createSignal (plain, non-tracked).
2244
+ * Best-effort: failures are logged but never thrown.
2245
+ */
2246
+ async emitHarnessSignal(ticketId, kind, payload) {
2247
+ const content = createHarnessSignalContent(kind, payload);
2248
+ try {
2249
+ await this.deps.createSignal(ticketId, content);
2250
+ } catch (err) {
2251
+ const msg = err instanceof Error ? err.message : String(err);
2252
+ console.error(`[harness-signal] failed to create ${kind} signal for ${ticketId}: ${msg}`);
2253
+ }
2254
+ }
2255
+ async emitDispatchDeferred(ticketId, reason, extras) {
2256
+ if (!this.deps.createHarnessSignal) return;
2257
+ const existing = this.deferredState.get(ticketId);
2258
+ if (existing?.reason === reason) return;
2259
+ if (existing && this.deps.deleteSignal) {
2260
+ try {
2261
+ await this.deps.deleteSignal(existing.signalId);
2262
+ } catch {
2263
+ }
2264
+ }
2265
+ const firstDeferredAt = existing?.firstDeferredAt ?? (/* @__PURE__ */ new Date()).toISOString();
2266
+ const content = createHarnessSignalContent("dispatch_deferred", {
2267
+ reason,
2268
+ firstDeferredAt,
2269
+ ...extras
2270
+ });
2271
+ try {
2272
+ const signal = await this.deps.createHarnessSignal(ticketId, content);
2273
+ this.deferredState.set(ticketId, { reason, signalId: signal.id, firstDeferredAt });
2274
+ } catch (err) {
2275
+ const msg = err instanceof Error ? err.message : String(err);
2276
+ console.error(` [dispatch-deferred] failed to create signal for ticket ${ticketId}: ${msg}`);
2277
+ }
2278
+ }
2279
+ /**
2280
+ * Clear the dispatch_deferred harness signal for a ticket (called on dispatch success or ticket:moved).
2281
+ * Best-effort: deletion failures are swallowed.
2282
+ */
2283
+ async clearDispatchDeferred(ticketId) {
2284
+ const existing = this.deferredState.get(ticketId);
2285
+ if (!existing) return;
2286
+ this.deferredState.delete(ticketId);
2287
+ if (this.deps.deleteSignal) {
2288
+ try {
2289
+ await this.deps.deleteSignal(existing.signalId);
2290
+ } catch {
2291
+ }
2292
+ }
2293
+ }
2155
2294
  };
2156
2295
 
2157
2296
  // src/lib/run-memory.ts
@@ -3517,22 +3656,22 @@ var CodexProvider = class {
3517
3656
  }
3518
3657
  }
3519
3658
  }
3659
+ const WRITING_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "Bash", "NotebookEdit"]);
3520
3660
  if (request.toolRestrictions) {
3521
3661
  const tr = request.toolRestrictions;
3522
- const writingTools = ["Write", "Edit", "Bash", "NotebookEdit", "shell", "file_write", "file_edit"];
3523
3662
  if (tr.tools === "") {
3524
3663
  args.push("--sandbox", "read-only");
3525
3664
  degraded.push("builtinToolStripping");
3526
- } else if (tr.disallowedTools?.some((t) => writingTools.includes(t))) {
3527
- args.push("--sandbox", "read-only");
3528
- degraded.push("toolDenylist");
3665
+ } else {
3666
+ const hasWritingDeny = (tr.disallowedTools ?? []).some((t) => WRITING_TOOLS.has(t));
3667
+ if (hasWritingDeny) {
3668
+ args.push("--sandbox", "read-only");
3669
+ degraded.push("toolDenylistAdvisory");
3670
+ }
3529
3671
  }
3530
3672
  if (tr.allowedTools?.length) {
3531
3673
  degraded.push("toolAllowlist");
3532
3674
  }
3533
- if (tr.disallowedTools?.length && !degraded.includes("toolDenylist")) {
3534
- degraded.push("toolDenylist");
3535
- }
3536
3675
  }
3537
3676
  if (request.maxTurns) {
3538
3677
  degraded.push("maxTurns");
@@ -4050,9 +4189,25 @@ function createProviderRegistry() {
4050
4189
  registry.register(new GeminiProvider());
4051
4190
  return registry;
4052
4191
  }
4053
- function readMcpConfigAsProviderConfig(filePath) {
4054
- const raw = JSON.parse(readFileSync3(filePath, "utf-8"));
4055
- return { servers: raw.mcpServers };
4192
+ function readMcpConfigAsProviderConfig(filePath, regenerate) {
4193
+ function parse(path) {
4194
+ const raw = JSON.parse(readFileSync3(path, "utf-8"));
4195
+ return { servers: raw.mcpServers };
4196
+ }
4197
+ try {
4198
+ return parse(filePath);
4199
+ } catch (err) {
4200
+ const code = err?.code;
4201
+ if (code === "ENOENT") {
4202
+ process.stderr.write(
4203
+ `[mcp-config] regenerating missing file at ${filePath} (pid=${process.pid})
4204
+ `
4205
+ );
4206
+ const newPath = regenerate();
4207
+ return parse(newPath);
4208
+ }
4209
+ throw err;
4210
+ }
4056
4211
  }
4057
4212
  function parseArgs(args) {
4058
4213
  const positional = [];
@@ -4255,7 +4410,7 @@ function waitForConfirmation() {
4255
4410
  }
4256
4411
  async function runPipeline(client, args) {
4257
4412
  if (args[0] === "init") {
4258
- const { runPipelineInit } = await import("./pipeline-init-IGZZOOLK.js");
4413
+ const { runPipelineInit } = await import("./pipeline-init-AUKPFJYE.js");
4259
4414
  await runPipelineInit();
4260
4415
  return;
4261
4416
  }
@@ -4299,6 +4454,7 @@ async function runPipeline(client, args) {
4299
4454
  process.exit(1);
4300
4455
  }
4301
4456
  }
4457
+ reapOrphanedMcpConfigDirs(pidDir(opts.boardId));
4302
4458
  const mcpConfigPath = generateMcpConfig(client.baseUrl, client.token, opts.boardId);
4303
4459
  const boardProviderConfig = {
4304
4460
  ...opts.provider ? { default_provider: opts.provider } : {},
@@ -4342,7 +4498,7 @@ async function runPipeline(client, args) {
4342
4498
  if (effectiveConfig.model) {
4343
4499
  effectiveConfig.model = registry.resolveModel(columnProvider, effectiveConfig.model);
4344
4500
  }
4345
- const columnGates = resolveGatesForColumn(gateConfig, resolvedColumnName);
4501
+ const { gates: columnGates } = resolveGatesForColumn(gateConfig, resolvedColumnName);
4346
4502
  const gateCwd = effectiveConfig.worktreePath ?? (effectiveConfig.worktreeName ? join3(process.cwd(), effectiveConfig.worktreeName) : process.cwd());
4347
4503
  const effectiveMcpConfigPath = columnGates.length > 0 ? generateGateProxyMcpConfig(
4348
4504
  client.baseUrl,
@@ -4355,7 +4511,20 @@ async function runPipeline(client, args) {
4355
4511
  gateCwd,
4356
4512
  ticketId
4357
4513
  ) : mcpConfigPath;
4358
- const columnMcpConfig = readMcpConfigAsProviderConfig(effectiveMcpConfigPath);
4514
+ const columnMcpConfig = readMcpConfigAsProviderConfig(
4515
+ effectiveMcpConfigPath,
4516
+ () => columnGates.length > 0 ? generateGateProxyMcpConfig(
4517
+ client.baseUrl,
4518
+ client.token,
4519
+ opts.boardId,
4520
+ gateFilePath,
4521
+ columnId,
4522
+ resolvedColumnName,
4523
+ projectId,
4524
+ gateCwd,
4525
+ ticketId
4526
+ ) : generateMcpConfig(client.baseUrl, client.token, opts.boardId)
4527
+ );
4359
4528
  const loopDeps = {
4360
4529
  fetchTicketContext: (tid) => client.get(`/projects/${projectId}/pipeline-context`, { ticketId: tid }),
4361
4530
  fetchColumnContext: (cid) => client.get(`/projects/${projectId}/pipeline-context`, { columnId: cid }),
@@ -4463,7 +4632,7 @@ async function runPipeline(client, args) {
4463
4632
  };
4464
4633
  }
4465
4634
  effectiveConfig.onPostIterationGates = async (tid, iteration) => {
4466
- const gates = resolveGatesForColumn(gateConfig, resolvedColumnName);
4635
+ const { gates } = resolveGatesForColumn(gateConfig, resolvedColumnName);
4467
4636
  if (gates.length === 0) {
4468
4637
  return gateSnapshotStore.record(tid, iteration, []);
4469
4638
  }
@@ -4499,6 +4668,12 @@ async function runPipeline(client, args) {
4499
4668
  contentPrefix,
4500
4669
  content: body
4501
4670
  }),
4671
+ createHarnessSignal: (ticketId, content) => client.post(`/projects/${projectId}/signals`, {
4672
+ scopeType: "ticket",
4673
+ scopeId: ticketId,
4674
+ content
4675
+ }),
4676
+ deleteSignal: (signalId) => client.delete(`/projects/${projectId}/signals/${signalId}`),
4502
4677
  claimTicket: (ticketId) => client.claimTicket(projectId, ticketId),
4503
4678
  fetchBlockedTickets: (ticketId) => client.get(
4504
4679
  `/projects/${projectId}/tickets/${ticketId}/blocked-tickets`
@@ -4659,7 +4834,7 @@ async function runPipeline(client, args) {
4659
4834
  console.error(`Error: Failed to initialize pipeline: ${message}`);
4660
4835
  console.error("Check that the board exists, has pipeline columns configured, and the API is reachable.");
4661
4836
  cleanupMcpConfig(mcpConfigPath);
4662
- cleanupGateProxyConfigs(pidDir(opts.boardId));
4837
+ cleanupGateProxyConfigs(dirname2(mcpConfigPath));
4663
4838
  process.exit(1);
4664
4839
  }
4665
4840
  const columnIds = orchestrator.pipelineColumnIds;
@@ -4715,11 +4890,29 @@ async function runPipeline(client, args) {
4715
4890
  if (columnIds.length === 0) {
4716
4891
  console.log('No pipeline columns found (columns need has_prompt=true and type !== "done").');
4717
4892
  cleanupMcpConfig(mcpConfigPath);
4718
- cleanupGateProxyConfigs(pidDir(opts.boardId));
4893
+ cleanupGateProxyConfigs(dirname2(mcpConfigPath));
4719
4894
  return;
4720
4895
  }
4721
4896
  console.log(`Discovered ${String(columnIds.length)} pipeline column(s).`);
4722
4897
  logger.orchestrator(`Discovered ${String(columnIds.length)} pipeline columns: ${columnIds.join(", ")}`);
4898
+ const unmatchedKeys = findUnmatchedColumnKeys(gateConfig, orchestrator.allBoardColumnNames);
4899
+ if (unmatchedKeys.length > 0) {
4900
+ console.warn(
4901
+ `[gate-config] Unused column keys in pipeline.gates.yaml: ${unmatchedKeys.join(", ")}.`
4902
+ );
4903
+ console.warn(
4904
+ ` No board column name matched (after canonicalization). These overrides will never fire.`
4905
+ );
4906
+ }
4907
+ for (const col of orchestrator.allBoardColumns) {
4908
+ if (col.type !== "done") continue;
4909
+ const { source } = resolveGatesForColumn(gateConfig, col.name);
4910
+ if (source === "override") {
4911
+ console.warn(
4912
+ `[gate-config] column "${col.name}" is done-type AND has an explicit override \u2014 gates will run on moves INTO this column (bypass disabled by your pipeline.gates.yaml).`
4913
+ );
4914
+ }
4915
+ }
4723
4916
  if (opts.dryRun) {
4724
4917
  console.log("\n--- Dry Run Configuration ---");
4725
4918
  console.log(`Board ID: ${opts.boardId}`);
@@ -4735,7 +4928,7 @@ async function runPipeline(client, args) {
4735
4928
  console.log(`MCP config: ${mcpConfigPath}`);
4736
4929
  console.log("\n[Dry run -- no agents started]");
4737
4930
  cleanupMcpConfig(mcpConfigPath);
4738
- cleanupGateProxyConfigs(pidDir(opts.boardId));
4931
+ cleanupGateProxyConfigs(dirname2(mcpConfigPath));
4739
4932
  return;
4740
4933
  }
4741
4934
  let shutdownInProgress = false;
@@ -4763,7 +4956,7 @@ Received ${signal}. Shutting down gracefully...`);
4763
4956
  console.log(costTracker.generateReport(gateConfig.settings?.pricing));
4764
4957
  }
4765
4958
  cleanupMcpConfig(mcpConfigPath);
4766
- cleanupGateProxyConfigs(pidDir(opts.boardId));
4959
+ cleanupGateProxyConfigs(dirname2(mcpConfigPath));
4767
4960
  killReaper(reaperPidPath);
4768
4961
  removePidFile(opts.boardId);
4769
4962
  removeChildManifest(opts.boardId);
@@ -4801,7 +4994,7 @@ Received ${signal}. Shutting down gracefully...`);
4801
4994
  pidFilePath: pidFilePath(opts.boardId),
4802
4995
  reaperPidPath,
4803
4996
  mcpConfigPath,
4804
- pipelineDir: pidDir(opts.boardId)
4997
+ pipelineDir: dirname2(mcpConfigPath)
4805
4998
  });
4806
4999
  logger.orchestrator(`Watchdog reaper spawned (PID ${String(reaperProcess.pid ?? "unknown")})`);
4807
5000
  let eventQueue = null;
@@ -4886,7 +5079,7 @@ Received ${signal}. Shutting down gracefully...`);
4886
5079
  wsClient.stop();
4887
5080
  eventQueue.stop();
4888
5081
  cleanupMcpConfig(mcpConfigPath);
4889
- cleanupGateProxyConfigs(pidDir(opts.boardId));
5082
+ cleanupGateProxyConfigs(dirname2(mcpConfigPath));
4890
5083
  killReaper(reaperPidPath);
4891
5084
  removePidFile(opts.boardId);
4892
5085
  removeChildManifest(opts.boardId);
@@ -4984,4 +5177,4 @@ export {
4984
5177
  runPipeline,
4985
5178
  stopPipeline
4986
5179
  };
4987
- //# sourceMappingURL=pipeline-IT7LJXKA.js.map
5180
+ //# sourceMappingURL=pipeline-GZOSDNPF.js.map