kantban-cli 0.1.47 → 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-O47Q6CNM.js";
3
+ } from "./chunk-DENXSVKE.js";
4
4
  import {
5
5
  ClaudeProvider,
6
6
  RalphLoop,
@@ -11,8 +11,9 @@ import {
11
11
  generateGateProxyMcpConfig,
12
12
  generateMcpConfig,
13
13
  parseJsonFromLlmOutput,
14
- parseStuckDetectionResponse
15
- } from "./chunk-UKWQ6VUQ.js";
14
+ parseStuckDetectionResponse,
15
+ reapOrphanedMcpConfigDirs
16
+ } from "./chunk-QHJZIGEE.js";
16
17
  import {
17
18
  LoopCheckpointSchema,
18
19
  VerdictSchema,
@@ -20,7 +21,7 @@ import {
20
21
  parseGateConfig,
21
22
  parseTimeout,
22
23
  resolveGatesForColumn
23
- } from "./chunk-VDCHOIQI.js";
24
+ } from "./chunk-4R27WTCJ.js";
24
25
  import {
25
26
  IS_WINDOWS,
26
27
  crossSpawnOptions,
@@ -32,7 +33,13 @@ import {
32
33
  // src/commands/pipeline.ts
33
34
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync3, unlinkSync as unlinkSync3, existsSync as existsSync3, appendFileSync as appendFileSync2 } from "fs";
34
35
  import { homedir as homedir2 } from "os";
35
- 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
+ }
36
43
 
37
44
  // src/lib/tool-profiles.ts
38
45
  function resolveToolRestrictions(builtinTools, allowedTools, disallowedTools) {
@@ -470,7 +477,9 @@ function parseReplannerResponse(raw) {
470
477
  }
471
478
 
472
479
  // src/lib/orchestrator.ts
473
- 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;
474
483
  function classifyTier(input) {
475
484
  if (input.invocationTier === "light") return "light";
476
485
  if (input.invocationTier === "heavy") return "heavy";
@@ -510,6 +519,12 @@ var PipelineOrchestrator = class {
510
519
  blockedColumns = /* @__PURE__ */ new Set();
511
520
  /** Per-ticket advisor invocation count for the current column transit */
512
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();
513
528
  /** Per-ticket model override (set by RETRY_DIFFERENT_MODEL, consumed by startTrackedLoop) */
514
529
  ticketModelOverrides = /* @__PURE__ */ new Map();
515
530
  /** Stable session ID for this orchestrator instance (pipeline run) */
@@ -555,6 +570,10 @@ var PipelineOrchestrator = class {
555
570
  get allBoardColumnNames() {
556
571
  return this.cachedBoardScope?.columns.map((c) => c.name) ?? [];
557
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
+ }
558
577
  /** Returns the total number of active loops. */
559
578
  get activeLoopCount() {
560
579
  return this.activeLoops.size;
@@ -633,12 +652,16 @@ var PipelineOrchestrator = class {
633
652
  } : void 0,
634
653
  builtinTools: cfg?.builtin_tools,
635
654
  allowedTools: cfg?.allowed_tools,
636
- 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
637
659
  });
638
660
  this.columnScopes.set(col.id, colScope);
639
661
  this.loopQueues.set(col.id, []);
640
662
  })
641
663
  );
664
+ console.error(`[orchestrator] zombie TTL: ${ACTIVE_LOOP_ZOMBIE_TTL_MS}ms`);
642
665
  }
643
666
  /**
644
667
  * Refresh the cached column scope for a single column.
@@ -944,6 +967,9 @@ var PipelineOrchestrator = class {
944
967
  if (this.isColumnBlocked(columnId)) {
945
968
  this.blockedColumns.add(columnId);
946
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
+ }
947
973
  continue;
948
974
  }
949
975
  console.error(` [scan] Column ${columnId} (${colScope.column.name}): ${String(colScope.tickets.length)} ticket(s)`);
@@ -970,6 +996,7 @@ var PipelineOrchestrator = class {
970
996
  this.knownTickets.delete(event.ticketId);
971
997
  this.spawning.delete(event.ticketId);
972
998
  this.deferredTickets.delete(event.ticketId);
999
+ void this.clearDispatchDeferred(event.ticketId);
973
1000
  const oldQueue = this.loopQueues.get(event.fromColumnId);
974
1001
  if (oldQueue) {
975
1002
  const idx = oldQueue.indexOf(event.ticketId);
@@ -984,6 +1011,7 @@ var PipelineOrchestrator = class {
984
1011
  if (this.isColumnBlocked(event.columnId)) {
985
1012
  console.error(` [event] ${event.type} ${event.ticketId} \u2192 column ${event.columnId}: BLOCKED by firing constraints \u2014 deferred`);
986
1013
  this.deferredTickets.set(event.ticketId, event.columnId);
1014
+ await this.emitDispatchDeferred(event.ticketId, "firing_constraint", { columnId: event.columnId });
987
1015
  } else {
988
1016
  await this.spawnOrQueue(event.ticketId, event.columnId, true);
989
1017
  }
@@ -1033,6 +1061,9 @@ var PipelineOrchestrator = class {
1033
1061
  this.deferredTickets.delete(event.ticketId);
1034
1062
  this.spawning.delete(event.ticketId);
1035
1063
  this.advisorBudget.delete(event.ticketId);
1064
+ this.gutterResetCount.delete(event.ticketId);
1065
+ this.gutterResetExhausted.delete(event.ticketId);
1066
+ void this.clearDispatchDeferred(event.ticketId);
1036
1067
  for (const [, queue] of this.loopQueues) {
1037
1068
  const idx = queue.indexOf(event.ticketId);
1038
1069
  if (idx !== -1) {
@@ -1076,6 +1107,7 @@ var PipelineOrchestrator = class {
1076
1107
  this.releaseSlot(columnId);
1077
1108
  this.deferredTickets.set(ticketId, columnId);
1078
1109
  console.error(` [skip] ${ticketId} has unresolved blockers \u2014 deferred`);
1110
+ void this.emitDispatchDeferred(ticketId, "unresolved_blockers", { columnId });
1079
1111
  void this.drainQueue(columnId).catch((err) => {
1080
1112
  const msg = err instanceof Error ? err.message : String(err);
1081
1113
  console.error(` [error] drainQueue failed for column ${columnId}: ${msg}`);
@@ -1088,6 +1120,7 @@ var PipelineOrchestrator = class {
1088
1120
  this.spawning.delete(ticketId);
1089
1121
  this.releaseSlot(columnId);
1090
1122
  this.deferredTickets.set(ticketId, columnId);
1123
+ void this.emitDispatchDeferred(ticketId, "unresolved_blockers", { columnId });
1091
1124
  void this.drainQueue(columnId).catch((err2) => {
1092
1125
  const msg2 = err2 instanceof Error ? err2.message : String(err2);
1093
1126
  console.error(` [error] drainQueue failed for column ${columnId}: ${msg2}`);
@@ -1096,6 +1129,7 @@ var PipelineOrchestrator = class {
1096
1129
  }
1097
1130
  try {
1098
1131
  await this.deps.claimTicket(ticketId);
1132
+ await this.clearDispatchDeferred(ticketId);
1099
1133
  this.startTrackedLoop(ticketId, columnId, colConfig);
1100
1134
  } catch (err) {
1101
1135
  const msg = err instanceof Error ? err.message : String(err);
@@ -1254,6 +1288,8 @@ var PipelineOrchestrator = class {
1254
1288
  ...startGutterCount !== void 0 && { startGutterCount },
1255
1289
  ...startFingerprint !== void 0 && { startFingerprint },
1256
1290
  ...config.stuckDetection && { stuckDetection: config.stuckDetection },
1291
+ ...config.repromptOnBranchMerged !== void 0 && { repromptOnBranchMerged: config.repromptOnBranchMerged },
1292
+ ...config.maxRepromptAttempts !== void 0 && { maxRepromptAttempts: config.maxRepromptAttempts },
1257
1293
  ...this.deps.costTracker && { isBudgetExhausted: () => this.deps.costTracker.isExhausted() }
1258
1294
  };
1259
1295
  const toolRestrictions = resolveToolRestrictions(
@@ -1283,6 +1319,33 @@ var PipelineOrchestrator = class {
1283
1319
  })
1284
1320
  );
1285
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
+ }
1286
1349
  /**
1287
1350
  * Phase 2: Invoke the advisor for failure recovery.
1288
1351
  * Returns true if the ticket should be retried, false if handled.
@@ -1386,6 +1449,10 @@ var PipelineOrchestrator = class {
1386
1449
  }
1387
1450
  switch (response.action) {
1388
1451
  case "RETRY_WITH_FEEDBACK": {
1452
+ if (!await this.checkGutterResetCap(ticketId, colConfig)) {
1453
+ this.completing.delete(ticketId);
1454
+ return false;
1455
+ }
1389
1456
  if (response.feedback) {
1390
1457
  await this.safeAction(
1391
1458
  ticketId,
@@ -1400,6 +1467,10 @@ ${response.feedback}`)
1400
1467
  return true;
1401
1468
  }
1402
1469
  case "RETRY_DIFFERENT_MODEL": {
1470
+ if (!await this.checkGutterResetCap(ticketId, colConfig)) {
1471
+ this.completing.delete(ticketId);
1472
+ return false;
1473
+ }
1403
1474
  const currentModel = result.model ?? colConfig.modelPreference ?? "default";
1404
1475
  const escalation = colConfig.modelRouting?.escalation ?? [];
1405
1476
  let nextModel;
@@ -1558,10 +1629,17 @@ ${response.feedback}`)
1558
1629
  );
1559
1630
  }
1560
1631
  }
1632
+ await this.emitHarnessSignal(ticketId, "advisor_action", {
1633
+ action: "ESCALATE",
1634
+ reason: response.reason
1635
+ });
1561
1636
  await this.safeAction(
1562
1637
  ticketId,
1563
- "advisor:createComment",
1564
- () => 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
+ )
1565
1643
  );
1566
1644
  return false;
1567
1645
  }
@@ -1817,6 +1895,8 @@ ${findingsText}`)
1817
1895
  const colConfig = this.pipelineColumns.get(columnId);
1818
1896
  const terminalCleanup = async () => {
1819
1897
  this.advisorBudget.delete(ticketId);
1898
+ this.gutterResetCount.delete(ticketId);
1899
+ this.gutterResetExhausted.delete(ticketId);
1820
1900
  if (colConfig?.checkpointEnabled && this.deps.setFieldValue && result.reason !== "stopped") {
1821
1901
  await this.safeAction(
1822
1902
  ticketId,
@@ -1950,47 +2030,41 @@ ${findingsText}`)
1950
2030
  }
1951
2031
  break;
1952
2032
  case "max_iterations":
1953
- 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.`;
1954
2037
  break;
1955
2038
  case "stalled":
1956
- 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
+ }
1957
2045
  break;
1958
2046
  case "error":
1959
- 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.`;
1960
2052
  break;
1961
2053
  case "stopped":
1962
2054
  comment = `Pipeline agent was stopped externally after ${result.iterations} iteration(s).`;
1963
2055
  break;
1964
2056
  case "deleted":
1965
- comment = `Pipeline agent stopped \u2014 ticket was deleted or archived during iteration ${result.iterations}.`;
1966
2057
  break;
1967
2058
  case "budget":
1968
2059
  comment = `Pipeline budget exhausted after ${result.iterations} iteration(s). Increase token budget in pipeline.gates.yaml settings.budget to continue.`;
1969
2060
  break;
1970
2061
  }
1971
- if (result.reason !== "deleted") {
2062
+ if (comment !== void 0) {
1972
2063
  await this.deps.createComment(ticketId, comment).catch((err) => {
1973
2064
  const msg = err instanceof Error ? err.message : String(err);
1974
2065
  console.error(` [warn] Failed to write completion comment for ${ticketId}: ${msg}`);
1975
2066
  });
1976
2067
  }
1977
- if (result.reason === "stalled") {
1978
- await this.deps.createSignal(
1979
- ticketId,
1980
- `Previous pipeline run stalled after ${result.iterations} iterations with no progress. Review comments for details before retrying.`
1981
- ).catch((err) => {
1982
- const msg = err instanceof Error ? err.message : String(err);
1983
- console.error(` [warn] Failed to write signal for ${ticketId}: ${msg}`);
1984
- });
1985
- } else if (result.reason === "error") {
1986
- await this.deps.createSignal(
1987
- ticketId,
1988
- `Previous pipeline run failed after ${result.iterations} iteration(s): ${result.lastError ?? "unknown error"}. Review comments for details before retrying.`
1989
- ).catch((err) => {
1990
- const msg = err instanceof Error ? err.message : String(err);
1991
- console.error(` [warn] Failed to write signal for ${ticketId}: ${msg}`);
1992
- });
1993
- }
1994
2068
  await terminalCleanup();
1995
2069
  } finally {
1996
2070
  this.completing.delete(ticketId);
@@ -2117,6 +2191,7 @@ ${findingsText}`)
2117
2191
  this.deferredTickets.set(nextTicketId, columnId);
2118
2192
  this.spawning.delete(nextTicketId);
2119
2193
  this.releaseSlot(columnId);
2194
+ void this.emitDispatchDeferred(nextTicketId, "unresolved_blockers", { columnId });
2120
2195
  continue;
2121
2196
  }
2122
2197
  } catch (err) {
@@ -2125,10 +2200,12 @@ ${findingsText}`)
2125
2200
  this.deferredTickets.set(nextTicketId, columnId);
2126
2201
  this.spawning.delete(nextTicketId);
2127
2202
  this.releaseSlot(columnId);
2203
+ void this.emitDispatchDeferred(nextTicketId, "unresolved_blockers", { columnId });
2128
2204
  continue;
2129
2205
  }
2130
2206
  try {
2131
2207
  await this.deps.claimTicket(nextTicketId);
2208
+ await this.clearDispatchDeferred(nextTicketId);
2132
2209
  this.startTrackedLoop(nextTicketId, columnId, colConfig);
2133
2210
  } catch (err) {
2134
2211
  const msg = err instanceof Error ? err.message : String(err);
@@ -2157,6 +2234,63 @@ ${findingsText}`)
2157
2234
  const current = this.columnReservations.get(columnId) ?? 0;
2158
2235
  if (current > 0) this.columnReservations.set(columnId, current - 1);
2159
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
+ }
2160
2294
  };
2161
2295
 
2162
2296
  // src/lib/run-memory.ts
@@ -3522,22 +3656,22 @@ var CodexProvider = class {
3522
3656
  }
3523
3657
  }
3524
3658
  }
3659
+ const WRITING_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "Bash", "NotebookEdit"]);
3525
3660
  if (request.toolRestrictions) {
3526
3661
  const tr = request.toolRestrictions;
3527
- const writingTools = ["Write", "Edit", "Bash", "NotebookEdit", "shell", "file_write", "file_edit"];
3528
3662
  if (tr.tools === "") {
3529
3663
  args.push("--sandbox", "read-only");
3530
3664
  degraded.push("builtinToolStripping");
3531
- } else if (tr.disallowedTools?.some((t) => writingTools.includes(t))) {
3532
- args.push("--sandbox", "read-only");
3533
- 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
+ }
3534
3671
  }
3535
3672
  if (tr.allowedTools?.length) {
3536
3673
  degraded.push("toolAllowlist");
3537
3674
  }
3538
- if (tr.disallowedTools?.length && !degraded.includes("toolDenylist")) {
3539
- degraded.push("toolDenylist");
3540
- }
3541
3675
  }
3542
3676
  if (request.maxTurns) {
3543
3677
  degraded.push("maxTurns");
@@ -4055,9 +4189,25 @@ function createProviderRegistry() {
4055
4189
  registry.register(new GeminiProvider());
4056
4190
  return registry;
4057
4191
  }
4058
- function readMcpConfigAsProviderConfig(filePath) {
4059
- const raw = JSON.parse(readFileSync3(filePath, "utf-8"));
4060
- 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
+ }
4061
4211
  }
4062
4212
  function parseArgs(args) {
4063
4213
  const positional = [];
@@ -4304,6 +4454,7 @@ async function runPipeline(client, args) {
4304
4454
  process.exit(1);
4305
4455
  }
4306
4456
  }
4457
+ reapOrphanedMcpConfigDirs(pidDir(opts.boardId));
4307
4458
  const mcpConfigPath = generateMcpConfig(client.baseUrl, client.token, opts.boardId);
4308
4459
  const boardProviderConfig = {
4309
4460
  ...opts.provider ? { default_provider: opts.provider } : {},
@@ -4347,7 +4498,7 @@ async function runPipeline(client, args) {
4347
4498
  if (effectiveConfig.model) {
4348
4499
  effectiveConfig.model = registry.resolveModel(columnProvider, effectiveConfig.model);
4349
4500
  }
4350
- const columnGates = resolveGatesForColumn(gateConfig, resolvedColumnName);
4501
+ const { gates: columnGates } = resolveGatesForColumn(gateConfig, resolvedColumnName);
4351
4502
  const gateCwd = effectiveConfig.worktreePath ?? (effectiveConfig.worktreeName ? join3(process.cwd(), effectiveConfig.worktreeName) : process.cwd());
4352
4503
  const effectiveMcpConfigPath = columnGates.length > 0 ? generateGateProxyMcpConfig(
4353
4504
  client.baseUrl,
@@ -4360,7 +4511,20 @@ async function runPipeline(client, args) {
4360
4511
  gateCwd,
4361
4512
  ticketId
4362
4513
  ) : mcpConfigPath;
4363
- 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
+ );
4364
4528
  const loopDeps = {
4365
4529
  fetchTicketContext: (tid) => client.get(`/projects/${projectId}/pipeline-context`, { ticketId: tid }),
4366
4530
  fetchColumnContext: (cid) => client.get(`/projects/${projectId}/pipeline-context`, { columnId: cid }),
@@ -4468,7 +4632,7 @@ async function runPipeline(client, args) {
4468
4632
  };
4469
4633
  }
4470
4634
  effectiveConfig.onPostIterationGates = async (tid, iteration) => {
4471
- const gates = resolveGatesForColumn(gateConfig, resolvedColumnName);
4635
+ const { gates } = resolveGatesForColumn(gateConfig, resolvedColumnName);
4472
4636
  if (gates.length === 0) {
4473
4637
  return gateSnapshotStore.record(tid, iteration, []);
4474
4638
  }
@@ -4504,6 +4668,12 @@ async function runPipeline(client, args) {
4504
4668
  contentPrefix,
4505
4669
  content: body
4506
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}`),
4507
4677
  claimTicket: (ticketId) => client.claimTicket(projectId, ticketId),
4508
4678
  fetchBlockedTickets: (ticketId) => client.get(
4509
4679
  `/projects/${projectId}/tickets/${ticketId}/blocked-tickets`
@@ -4664,7 +4834,7 @@ async function runPipeline(client, args) {
4664
4834
  console.error(`Error: Failed to initialize pipeline: ${message}`);
4665
4835
  console.error("Check that the board exists, has pipeline columns configured, and the API is reachable.");
4666
4836
  cleanupMcpConfig(mcpConfigPath);
4667
- cleanupGateProxyConfigs(pidDir(opts.boardId));
4837
+ cleanupGateProxyConfigs(dirname2(mcpConfigPath));
4668
4838
  process.exit(1);
4669
4839
  }
4670
4840
  const columnIds = orchestrator.pipelineColumnIds;
@@ -4720,7 +4890,7 @@ async function runPipeline(client, args) {
4720
4890
  if (columnIds.length === 0) {
4721
4891
  console.log('No pipeline columns found (columns need has_prompt=true and type !== "done").');
4722
4892
  cleanupMcpConfig(mcpConfigPath);
4723
- cleanupGateProxyConfigs(pidDir(opts.boardId));
4893
+ cleanupGateProxyConfigs(dirname2(mcpConfigPath));
4724
4894
  return;
4725
4895
  }
4726
4896
  console.log(`Discovered ${String(columnIds.length)} pipeline column(s).`);
@@ -4734,6 +4904,15 @@ async function runPipeline(client, args) {
4734
4904
  ` No board column name matched (after canonicalization). These overrides will never fire.`
4735
4905
  );
4736
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
+ }
4737
4916
  if (opts.dryRun) {
4738
4917
  console.log("\n--- Dry Run Configuration ---");
4739
4918
  console.log(`Board ID: ${opts.boardId}`);
@@ -4749,7 +4928,7 @@ async function runPipeline(client, args) {
4749
4928
  console.log(`MCP config: ${mcpConfigPath}`);
4750
4929
  console.log("\n[Dry run -- no agents started]");
4751
4930
  cleanupMcpConfig(mcpConfigPath);
4752
- cleanupGateProxyConfigs(pidDir(opts.boardId));
4931
+ cleanupGateProxyConfigs(dirname2(mcpConfigPath));
4753
4932
  return;
4754
4933
  }
4755
4934
  let shutdownInProgress = false;
@@ -4777,7 +4956,7 @@ Received ${signal}. Shutting down gracefully...`);
4777
4956
  console.log(costTracker.generateReport(gateConfig.settings?.pricing));
4778
4957
  }
4779
4958
  cleanupMcpConfig(mcpConfigPath);
4780
- cleanupGateProxyConfigs(pidDir(opts.boardId));
4959
+ cleanupGateProxyConfigs(dirname2(mcpConfigPath));
4781
4960
  killReaper(reaperPidPath);
4782
4961
  removePidFile(opts.boardId);
4783
4962
  removeChildManifest(opts.boardId);
@@ -4815,7 +4994,7 @@ Received ${signal}. Shutting down gracefully...`);
4815
4994
  pidFilePath: pidFilePath(opts.boardId),
4816
4995
  reaperPidPath,
4817
4996
  mcpConfigPath,
4818
- pipelineDir: pidDir(opts.boardId)
4997
+ pipelineDir: dirname2(mcpConfigPath)
4819
4998
  });
4820
4999
  logger.orchestrator(`Watchdog reaper spawned (PID ${String(reaperProcess.pid ?? "unknown")})`);
4821
5000
  let eventQueue = null;
@@ -4900,7 +5079,7 @@ Received ${signal}. Shutting down gracefully...`);
4900
5079
  wsClient.stop();
4901
5080
  eventQueue.stop();
4902
5081
  cleanupMcpConfig(mcpConfigPath);
4903
- cleanupGateProxyConfigs(pidDir(opts.boardId));
5082
+ cleanupGateProxyConfigs(dirname2(mcpConfigPath));
4904
5083
  killReaper(reaperPidPath);
4905
5084
  removePidFile(opts.boardId);
4906
5085
  removeChildManifest(opts.boardId);
@@ -4998,4 +5177,4 @@ export {
4998
5177
  runPipeline,
4999
5178
  stopPipeline
5000
5179
  };
5001
- //# sourceMappingURL=pipeline-EUOOLKHN.js.map
5180
+ //# sourceMappingURL=pipeline-GZOSDNPF.js.map