kantban-cli 0.1.15 → 0.1.17

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,23 +1,28 @@
1
1
  import {
2
- LoopCheckpointSchema,
2
+ runGates
3
+ } from "./chunk-4IUZAIFL.js";
4
+ import {
5
+ ClaudeProvider,
3
6
  RalphLoop,
4
- VerdictSchema,
5
7
  classifyTrajectory,
6
8
  cleanupGateProxyConfigs,
7
9
  cleanupMcpConfig,
8
10
  composeStuckDetectionPrompt,
9
11
  generateGateProxyMcpConfig,
10
12
  generateMcpConfig,
11
- parseGateConfig,
12
13
  parseJsonFromLlmOutput,
13
- parseStuckDetectionResponse,
14
+ parseStuckDetectionResponse
15
+ } from "./chunk-4RQDDZLM.js";
16
+ import {
17
+ LoopCheckpointSchema,
18
+ VerdictSchema,
19
+ parseGateConfig,
14
20
  parseTimeout,
15
21
  resolveGatesForColumn
16
- } from "./chunk-KGS3M2MY.js";
22
+ } from "./chunk-MN4H5NSU.js";
17
23
 
18
24
  // src/commands/pipeline.ts
19
- import { spawn as spawn2 } from "child_process";
20
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, readFileSync as readFileSync2, unlinkSync as unlinkSync2, existsSync, appendFileSync as appendFileSync2 } from "fs";
25
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, readFileSync as readFileSync2, unlinkSync as unlinkSync2, existsSync as existsSync2, appendFileSync as appendFileSync2 } from "fs";
21
26
  import { homedir } from "os";
22
27
  import { join as join2 } from "path";
23
28
 
@@ -42,6 +47,12 @@ function generateWorktreeName(ticketNumber, columnName) {
42
47
  return `kantban-${ticketNumber}-${slug}`;
43
48
  }
44
49
  async function cleanupWorktree(worktreeName, exec = defaultExecFile) {
50
+ try {
51
+ const path = await findWorktreeForBranch(exec, worktreeName);
52
+ if (!path) return true;
53
+ } catch {
54
+ return true;
55
+ }
45
56
  return new Promise((resolve) => {
46
57
  exec("git", ["worktree", "remove", "--force", worktreeName], (err) => {
47
58
  if (err) {
@@ -79,6 +90,11 @@ async function findWorktreeForBranch(exec, branch) {
79
90
  }
80
91
  async function mergeWorktreeBranch(worktreeName, integrationBranch, exec = defaultExecFile) {
81
92
  try {
93
+ try {
94
+ await execPromise(exec, "git", ["rev-parse", "--verify", worktreeName]);
95
+ } catch {
96
+ return true;
97
+ }
82
98
  await execPromise(exec, "git", ["branch", integrationBranch, "HEAD"]).catch(() => {
83
99
  });
84
100
  const checkedOutPath = await findWorktreeForBranch(exec, integrationBranch);
@@ -284,7 +300,16 @@ async function readCheckpoint(deps, ticketId, currentColumnId, staleMinutes) {
284
300
  const fields = await deps.getFieldValues(ticketId);
285
301
  const entry = fields.find((f) => f.field_name === CHECKPOINT_FIELD);
286
302
  if (!entry) return null;
287
- const parsed = LoopCheckpointSchema.safeParse(entry.value);
303
+ let raw = entry.value;
304
+ if (typeof raw === "string") {
305
+ if (!raw.trim()) return null;
306
+ try {
307
+ raw = JSON.parse(raw);
308
+ } catch {
309
+ return null;
310
+ }
311
+ }
312
+ const parsed = LoopCheckpointSchema.safeParse(raw);
288
313
  if (!parsed.success) return null;
289
314
  const checkpoint = parsed.data;
290
315
  if (checkpoint.column_id !== currentColumnId) return null;
@@ -486,6 +511,10 @@ var PipelineOrchestrator = class {
486
511
  }
487
512
  return this.spawning.size > 0;
488
513
  }
514
+ /** Returns true if any onLoopComplete callbacks are still running (for --once mode drain). */
515
+ get hasCompletingWork() {
516
+ return this.completing.size > 0;
517
+ }
489
518
  /** Returns the number of queued (waiting) tickets for a column. */
490
519
  queuedCount(columnId) {
491
520
  return this.loopQueues.get(columnId)?.length ?? 0;
@@ -494,12 +523,17 @@ var PipelineOrchestrator = class {
494
523
  * Initialize the orchestrator: fetch board scope, identify pipeline columns,
495
524
  * and fetch column-level agent configs.
496
525
  */
497
- async initialize() {
526
+ async initialize(columnFilter) {
498
527
  const boardScope = await this.deps.fetchBoardScope(this.boardId);
499
528
  this.cachedBoardScope = boardScope;
500
- const pipelineCols = boardScope.columns.filter(
529
+ let pipelineCols = boardScope.columns.filter(
501
530
  (col) => (col.has_prompt || col.type === "evaluator") && col.type !== "done"
502
531
  );
532
+ if (columnFilter) {
533
+ pipelineCols = pipelineCols.filter(
534
+ (col) => col.id === columnFilter || col.name.toLowerCase() === columnFilter.toLowerCase()
535
+ );
536
+ }
503
537
  await Promise.all(
504
538
  pipelineCols.map(async (col) => {
505
539
  const colScope = await this.deps.fetchColumnScope(col.id);
@@ -1110,7 +1144,8 @@ var PipelineOrchestrator = class {
1110
1144
  ...startIteration !== void 0 && { startIteration },
1111
1145
  ...startGutterCount !== void 0 && { startGutterCount },
1112
1146
  ...startFingerprint !== void 0 && { startFingerprint },
1113
- ...config.stuckDetection && { stuckDetection: config.stuckDetection }
1147
+ ...config.stuckDetection && { stuckDetection: config.stuckDetection },
1148
+ ...this.deps.costTracker && { isBudgetExhausted: () => this.deps.costTracker.isExhausted() }
1114
1149
  };
1115
1150
  const toolRestrictions = resolveToolRestrictions(
1116
1151
  config.builtinTools,
@@ -1648,14 +1683,14 @@ ${findingsText}`)
1648
1683
  const colConfig = this.pipelineColumns.get(columnId);
1649
1684
  const terminalCleanup = async () => {
1650
1685
  this.advisorBudget.delete(ticketId);
1651
- if (colConfig?.checkpointEnabled && this.deps.setFieldValue) {
1686
+ if (colConfig?.checkpointEnabled && this.deps.setFieldValue && result.reason !== "stopped") {
1652
1687
  await this.safeAction(
1653
1688
  ticketId,
1654
1689
  "clearCheckpoint",
1655
- () => this.deps.setFieldValue(ticketId, "loop_checkpoint", null)
1690
+ () => this.deps.setFieldValue(ticketId, "loop_checkpoint", "")
1656
1691
  );
1657
1692
  }
1658
- const isTerminal = result.reason === "moved" || result.reason === "max_iterations" || result.reason === "error" || result.reason === "stalled" || result.reason === "stopped" || result.reason === "deleted";
1693
+ const isTerminal = result.reason === "moved" || result.reason === "max_iterations" || result.reason === "error" || result.reason === "stalled" || result.reason === "stopped" || result.reason === "deleted" || result.reason === "budget";
1659
1694
  if (isTerminal && colConfig) {
1660
1695
  const colScope2 = this.columnScopes.get(columnId);
1661
1696
  const ticket2 = colScope2?.tickets.find((t) => t.id === ticketId);
@@ -1795,6 +1830,9 @@ ${findingsText}`)
1795
1830
  case "deleted":
1796
1831
  comment = `Pipeline agent stopped \u2014 ticket was deleted or archived during iteration ${result.iterations}.`;
1797
1832
  break;
1833
+ case "budget":
1834
+ comment = `Pipeline budget exhausted after ${result.iterations} iteration(s). Increase token budget in pipeline.gates.yaml settings.budget to continue.`;
1835
+ break;
1798
1836
  }
1799
1837
  if (result.reason !== "deleted") {
1800
1838
  await this.deps.createComment(ticketId, comment).catch((err) => {
@@ -2685,56 +2723,6 @@ function killReaper(reaperPidPath) {
2685
2723
  }
2686
2724
  }
2687
2725
 
2688
- // src/lib/stream-parser.ts
2689
- import { EventEmitter } from "events";
2690
- var StreamJsonParser = class extends EventEmitter {
2691
- buffer = "";
2692
- toolCallCount = 0;
2693
- feed(chunk) {
2694
- this.buffer += chunk;
2695
- const lines = this.buffer.split("\n");
2696
- this.buffer = lines.pop() ?? "";
2697
- for (const line of lines) {
2698
- const trimmed = line.trim();
2699
- if (!trimmed) continue;
2700
- try {
2701
- const event = JSON.parse(trimmed);
2702
- if (event.type === "assistant" && Array.isArray(event.content)) {
2703
- for (const block of event.content) {
2704
- if (block.type === "tool_use") this.toolCallCount++;
2705
- }
2706
- }
2707
- this.emit("event", event);
2708
- } catch {
2709
- this.emit("error", new Error(`Failed to parse stream-json line: ${trimmed.slice(0, 100)}`));
2710
- }
2711
- }
2712
- }
2713
- /** Flush any remaining buffer content (call after stream ends). */
2714
- flush() {
2715
- const trimmed = this.buffer.trim();
2716
- this.buffer = "";
2717
- if (!trimmed) return;
2718
- try {
2719
- const event = JSON.parse(trimmed);
2720
- if (event.type === "assistant" && Array.isArray(event.content)) {
2721
- for (const block of event.content) {
2722
- if (block.type === "tool_use") this.toolCallCount++;
2723
- }
2724
- }
2725
- this.emit("event", event);
2726
- } catch {
2727
- }
2728
- }
2729
- getToolCallCount() {
2730
- return this.toolCallCount;
2731
- }
2732
- reset() {
2733
- this.buffer = "";
2734
- this.toolCallCount = 0;
2735
- }
2736
- };
2737
-
2738
2726
  // src/lib/gate-snapshot.ts
2739
2727
  var MAX_SNAPSHOTS_PER_TICKET = 100;
2740
2728
  function computeDelta(previous, currentResults) {
@@ -2788,84 +2776,6 @@ var GateSnapshotStore = class {
2788
2776
  }
2789
2777
  };
2790
2778
 
2791
- // src/lib/gate-runner.ts
2792
- import { execFile } from "child_process";
2793
- async function runGate(gate, options = {}) {
2794
- const timeoutMs = gate.timeout ? parseTimeout(gate.timeout) : options.timeoutMs ?? 6e4;
2795
- const start = Date.now();
2796
- return new Promise((resolve) => {
2797
- const child = execFile("sh", ["-c", gate.run], {
2798
- timeout: timeoutMs,
2799
- cwd: options.cwd,
2800
- env: { ...process.env, ...options.env },
2801
- maxBuffer: options.maxBuffer ?? 1024 * 1024
2802
- // 1MB output cap
2803
- }, (error, stdout, stderr) => {
2804
- const duration_ms = Date.now() - start;
2805
- const output = (stdout + stderr).trim();
2806
- const timed_out = error?.killed === true;
2807
- const buffer_exceeded = error?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
2808
- if (error) {
2809
- let annotation = "";
2810
- if (timed_out) annotation = `
2811
- [TIMED OUT after ${timeoutMs}ms]`;
2812
- else if (buffer_exceeded) annotation = `
2813
- [OUTPUT TRUNCATED \u2014 buffer limit exceeded]`;
2814
- resolve({
2815
- name: gate.name,
2816
- passed: false,
2817
- required: gate.required ?? true,
2818
- duration_ms,
2819
- output: output + annotation,
2820
- stderr: stderr.trim(),
2821
- exit_code: child.exitCode ?? (typeof error.code === "number" ? error.code : 1),
2822
- timed_out: timed_out || buffer_exceeded
2823
- });
2824
- return;
2825
- }
2826
- resolve({
2827
- name: gate.name,
2828
- passed: true,
2829
- required: gate.required ?? true,
2830
- duration_ms,
2831
- output,
2832
- stderr: stderr.trim(),
2833
- exit_code: 0,
2834
- timed_out: false
2835
- });
2836
- });
2837
- });
2838
- }
2839
- async function runGates(gates, options = {}) {
2840
- const results = [];
2841
- const totalStart = Date.now();
2842
- for (const gate of gates) {
2843
- if (options.totalTimeoutMs) {
2844
- const elapsed = Date.now() - totalStart;
2845
- if (elapsed >= options.totalTimeoutMs) {
2846
- results.push({
2847
- name: gate.name,
2848
- passed: false,
2849
- required: gate.required ?? true,
2850
- duration_ms: 0,
2851
- output: "[SKIPPED \u2014 total timeout exceeded]",
2852
- stderr: "",
2853
- exit_code: -1,
2854
- timed_out: true
2855
- });
2856
- continue;
2857
- }
2858
- const remainingMs = options.totalTimeoutMs - elapsed;
2859
- const gateTimeout = gate.timeout ? parseTimeout(gate.timeout) : options.timeoutMs ?? 6e4;
2860
- const effectiveTimeout = Math.min(gateTimeout, remainingMs);
2861
- results.push(await runGate(gate, { ...options, timeoutMs: effectiveTimeout }));
2862
- } else {
2863
- results.push(await runGate(gate, options));
2864
- }
2865
- }
2866
- return results;
2867
- }
2868
-
2869
2779
  // src/lib/cost-tracker.ts
2870
2780
  var PipelineCostTracker = class {
2871
2781
  runId;
@@ -3063,7 +2973,355 @@ var PipelineEventEmitter = class _PipelineEventEmitter {
3063
2973
  }
3064
2974
  };
3065
2975
 
2976
+ // src/providers/registry.ts
2977
+ var TIER_VALUES = ["fast", "default", "thorough"];
2978
+ var ProviderRegistry = class {
2979
+ providers = /* @__PURE__ */ new Map();
2980
+ register(provider) {
2981
+ this.providers.set(provider.id, provider);
2982
+ }
2983
+ get(id) {
2984
+ const provider = this.providers.get(id);
2985
+ if (!provider) throw new Error(`Unknown provider: ${id}. Registered: ${[...this.providers.keys()].join(", ")}`);
2986
+ return provider;
2987
+ }
2988
+ list() {
2989
+ return [...this.providers.values()];
2990
+ }
2991
+ resolveForColumn(board, column) {
2992
+ const id = column.provider ?? board.default_provider ?? "claude";
2993
+ return this.get(id);
2994
+ }
2995
+ resolveForIntelligence(board) {
2996
+ const id = board.intelligence_provider ?? board.default_provider ?? "claude";
2997
+ return this.get(id);
2998
+ }
2999
+ async preflightAll(providerIds) {
3000
+ const unique = [...new Set(providerIds)];
3001
+ const results = await Promise.all(
3002
+ unique.map(async (id) => {
3003
+ const provider = this.get(id);
3004
+ const result = await provider.preflight();
3005
+ return { providerId: id, result };
3006
+ })
3007
+ );
3008
+ return results;
3009
+ }
3010
+ resolveModel(provider, preference) {
3011
+ if (TIER_VALUES.includes(preference)) {
3012
+ const model = provider.capabilities().supportedModels.find((m) => m.tier === preference);
3013
+ if (model) return model.id;
3014
+ }
3015
+ return preference;
3016
+ }
3017
+ };
3018
+
3019
+ // src/providers/codex-provider.ts
3020
+ import { spawn as spawn2, execFileSync } from "child_process";
3021
+ import { existsSync } from "fs";
3022
+
3023
+ // src/providers/codex-jsonl-parser.ts
3024
+ var CodexJsonlParser = class {
3025
+ buffer = "";
3026
+ toolCallCount = 0;
3027
+ inputTokens = 0;
3028
+ outputTokens = 0;
3029
+ lastOutput = "";
3030
+ onEvent = () => {
3031
+ };
3032
+ onError = () => {
3033
+ };
3034
+ feed(chunk) {
3035
+ this.buffer += chunk;
3036
+ const lines = this.buffer.split("\n");
3037
+ this.buffer = lines.pop() ?? "";
3038
+ for (const line of lines) {
3039
+ this.parseLine(line.trim());
3040
+ }
3041
+ }
3042
+ flush() {
3043
+ const trimmed = this.buffer.trim();
3044
+ this.buffer = "";
3045
+ if (trimmed) this.parseLine(trimmed);
3046
+ }
3047
+ getToolCallCount() {
3048
+ return this.toolCallCount;
3049
+ }
3050
+ getUsage() {
3051
+ return { inputTokens: this.inputTokens, outputTokens: this.outputTokens };
3052
+ }
3053
+ getLastOutput() {
3054
+ return this.lastOutput;
3055
+ }
3056
+ reset() {
3057
+ this.buffer = "";
3058
+ this.toolCallCount = 0;
3059
+ this.inputTokens = 0;
3060
+ this.outputTokens = 0;
3061
+ this.lastOutput = "";
3062
+ }
3063
+ parseLine(line) {
3064
+ if (!line) return;
3065
+ try {
3066
+ const raw = JSON.parse(line);
3067
+ this.translateEvent(raw);
3068
+ } catch {
3069
+ this.onError(new Error(`Failed to parse Codex JSONL: ${line.slice(0, 100)}`));
3070
+ }
3071
+ }
3072
+ translateEvent(raw) {
3073
+ const eventType = raw.type;
3074
+ const item = raw.item;
3075
+ if (eventType === "item.started" && item) {
3076
+ const itemType = item.type;
3077
+ if (itemType === "command_execution") {
3078
+ this.toolCallCount++;
3079
+ this.onEvent({ type: "tool_call", tool: "shell", input: { command: item.command } });
3080
+ } else if (itemType === "mcp_tool_call") {
3081
+ this.toolCallCount++;
3082
+ this.onEvent({
3083
+ type: "tool_call",
3084
+ tool: item.tool ?? "unknown",
3085
+ input: item.arguments
3086
+ });
3087
+ }
3088
+ } else if (eventType === "item.completed" && item) {
3089
+ const itemType = item.type;
3090
+ if (itemType === "agent_message") {
3091
+ const text = item.text ?? "";
3092
+ this.lastOutput = text;
3093
+ this.onEvent({ type: "text", text });
3094
+ } else if (itemType === "command_execution") {
3095
+ this.onEvent({
3096
+ type: "tool_result",
3097
+ tool: "shell",
3098
+ output: item.aggregated_output ?? ""
3099
+ });
3100
+ } else if (itemType === "mcp_tool_call") {
3101
+ this.onEvent({
3102
+ type: "tool_result",
3103
+ tool: item.tool ?? "unknown",
3104
+ output: item.result !== void 0 ? item.result : item.error
3105
+ });
3106
+ }
3107
+ } else if (eventType === "turn.completed") {
3108
+ const usage = raw.usage;
3109
+ const inTok = usage?.input_tokens ?? 0;
3110
+ const outTok = usage?.output_tokens ?? 0;
3111
+ this.inputTokens += inTok;
3112
+ this.outputTokens += outTok;
3113
+ if (inTok || outTok) {
3114
+ this.onEvent({ type: "usage", inputTokens: inTok, outputTokens: outTok });
3115
+ }
3116
+ } else if (eventType === "turn.failed") {
3117
+ const err = raw.error;
3118
+ const msg = err?.message ?? "turn failed";
3119
+ this.lastOutput = msg;
3120
+ this.onEvent({ type: "error", message: msg });
3121
+ } else if (eventType === "error") {
3122
+ const msg = raw.message ?? "unknown error";
3123
+ this.lastOutput = msg;
3124
+ this.onEvent({ type: "error", message: msg });
3125
+ }
3126
+ }
3127
+ };
3128
+
3129
+ // src/providers/codex-provider.ts
3130
+ var CODEX_TIMEOUT_MS = 60 * 60 * 1e3;
3131
+ var CodexProvider = class {
3132
+ id = "codex";
3133
+ displayName = "Codex CLI";
3134
+ capabilities() {
3135
+ return {
3136
+ supportsToolAllowlist: false,
3137
+ supportsToolDenylist: false,
3138
+ supportsBuiltinToolStripping: false,
3139
+ supportsMaxTurns: false,
3140
+ supportsMcpConfigInjection: false,
3141
+ supportsMcpConfigOverride: true,
3142
+ supportsWorktreeFlag: false,
3143
+ supportsSandboxModes: true,
3144
+ supportedModels: [
3145
+ { id: "gpt-5.1-codex-mini", displayName: "Codex Mini", tier: "fast" },
3146
+ { id: "gpt-5.3-codex", displayName: "GPT-5.3 Codex", tier: "default" },
3147
+ { id: "gpt-5.4", displayName: "GPT-5.4", tier: "thorough" }
3148
+ ],
3149
+ streamFormat: "jsonl"
3150
+ };
3151
+ }
3152
+ async invoke(request) {
3153
+ const degraded = [];
3154
+ const args = this.buildArgs(request, degraded);
3155
+ const env = this.buildEnv();
3156
+ const startTime = Date.now();
3157
+ if (request.workingDirectory) {
3158
+ if (!existsSync(request.workingDirectory)) {
3159
+ try {
3160
+ execFileSync("git", ["worktree", "add", "-b", request.workingDirectory, request.workingDirectory, "HEAD"], {
3161
+ stdio: "pipe"
3162
+ });
3163
+ } catch {
3164
+ try {
3165
+ execFileSync("git", ["worktree", "add", request.workingDirectory, request.workingDirectory], {
3166
+ stdio: "pipe"
3167
+ });
3168
+ } catch {
3169
+ degraded.push("worktreeCreation");
3170
+ }
3171
+ }
3172
+ } else {
3173
+ try {
3174
+ execFileSync("git", ["-C", request.workingDirectory, "rev-parse", "--git-dir"], {
3175
+ stdio: "pipe"
3176
+ });
3177
+ } catch {
3178
+ try {
3179
+ execFileSync("rm", ["-rf", request.workingDirectory], { stdio: "pipe" });
3180
+ execFileSync("git", ["worktree", "add", "-b", request.workingDirectory, request.workingDirectory, "HEAD"], {
3181
+ stdio: "pipe"
3182
+ });
3183
+ } catch {
3184
+ try {
3185
+ execFileSync("git", ["worktree", "add", request.workingDirectory, request.workingDirectory], {
3186
+ stdio: "pipe"
3187
+ });
3188
+ } catch {
3189
+ degraded.push("worktreeCreation");
3190
+ }
3191
+ }
3192
+ }
3193
+ }
3194
+ }
3195
+ return new Promise((resolve) => {
3196
+ const child = spawn2("codex", args, {
3197
+ stdio: ["ignore", "pipe", "pipe"],
3198
+ env: { ...process.env, ...env }
3199
+ });
3200
+ const parser = new CodexJsonlParser();
3201
+ parser.onEvent = (event) => request.onStreamEvent?.(event);
3202
+ parser.onError = (err) => process.stderr.write(`[codex-jsonl] ${err.message}
3203
+ `);
3204
+ let stderr = "";
3205
+ child.stdout?.on("data", (chunk) => parser.feed(chunk.toString()));
3206
+ child.stderr?.on("data", (chunk) => {
3207
+ stderr += chunk.toString();
3208
+ });
3209
+ if (request.abortSignal) {
3210
+ request.abortSignal.addEventListener("abort", () => {
3211
+ try {
3212
+ child.kill("SIGTERM");
3213
+ } catch {
3214
+ }
3215
+ }, { once: true });
3216
+ }
3217
+ let killTimer;
3218
+ const timeoutHandle = setTimeout(() => {
3219
+ try {
3220
+ child.kill("SIGTERM");
3221
+ } catch {
3222
+ }
3223
+ killTimer = setTimeout(() => {
3224
+ try {
3225
+ child.kill("SIGKILL");
3226
+ } catch {
3227
+ }
3228
+ }, 5e3);
3229
+ }, CODEX_TIMEOUT_MS);
3230
+ let resolved = false;
3231
+ const finish = (code, errorMsg) => {
3232
+ if (resolved) return;
3233
+ resolved = true;
3234
+ clearTimeout(timeoutHandle);
3235
+ if (killTimer) clearTimeout(killTimer);
3236
+ parser.flush();
3237
+ const usage = parser.getUsage();
3238
+ const result = {
3239
+ exitCode: code ?? 1,
3240
+ output: errorMsg ?? (parser.getLastOutput() || stderr),
3241
+ toolCallCount: parser.getToolCallCount(),
3242
+ usage,
3243
+ durationMs: Date.now() - startTime
3244
+ };
3245
+ if (degraded.length > 0) result.degradedCapabilities = degraded;
3246
+ resolve(result);
3247
+ };
3248
+ child.on("close", (code) => finish(code));
3249
+ child.on("error", (err) => finish(1, err.message));
3250
+ });
3251
+ }
3252
+ async preflight() {
3253
+ try {
3254
+ const which = await import("which");
3255
+ const w = which;
3256
+ const whichSync = w.sync ?? w.default?.sync ?? w.default;
3257
+ whichSync("codex");
3258
+ return { available: true, authenticated: true };
3259
+ } catch {
3260
+ return { available: false, authenticated: false, error: "codex binary not found on PATH" };
3261
+ }
3262
+ }
3263
+ buildArgs(request, degraded) {
3264
+ const args = [
3265
+ "exec",
3266
+ "--json",
3267
+ "--dangerously-bypass-approvals-and-sandbox"
3268
+ ];
3269
+ if (request.model) args.push("-m", request.model);
3270
+ if (request.workingDirectory) {
3271
+ args.push("-C", request.workingDirectory);
3272
+ }
3273
+ if (request.mcpConfig && Object.keys(request.mcpConfig.servers).length > 0) {
3274
+ for (const [name, server] of Object.entries(request.mcpConfig.servers)) {
3275
+ args.push("-c", `mcp_servers.${name}.command=${JSON.stringify(server.command)}`);
3276
+ args.push("-c", `mcp_servers.${name}.args=${JSON.stringify(server.args)}`);
3277
+ if (Object.keys(server.env).length > 0) {
3278
+ for (const [key, value] of Object.entries(server.env)) {
3279
+ args.push("-c", `mcp_servers.${name}.env.${key}=${JSON.stringify(value)}`);
3280
+ }
3281
+ }
3282
+ }
3283
+ }
3284
+ if (request.toolRestrictions) {
3285
+ const tr = request.toolRestrictions;
3286
+ const writingTools = ["Write", "Edit", "Bash", "NotebookEdit", "shell", "file_write", "file_edit"];
3287
+ if (tr.tools === "") {
3288
+ args.push("--sandbox", "read-only");
3289
+ degraded.push("builtinToolStripping");
3290
+ } else if (tr.disallowedTools?.some((t) => writingTools.includes(t))) {
3291
+ args.push("--sandbox", "read-only");
3292
+ degraded.push("toolDenylist");
3293
+ }
3294
+ if (tr.allowedTools?.length) {
3295
+ degraded.push("toolAllowlist");
3296
+ }
3297
+ if (tr.disallowedTools?.length && !degraded.includes("toolDenylist")) {
3298
+ degraded.push("toolDenylist");
3299
+ }
3300
+ }
3301
+ if (request.maxTurns) {
3302
+ degraded.push("maxTurns");
3303
+ }
3304
+ args.push(request.prompt);
3305
+ return args;
3306
+ }
3307
+ buildEnv() {
3308
+ const env = {};
3309
+ env.CODEX_FEATURE_TOOL_CALL_MCP_ELICITATION = "false";
3310
+ return env;
3311
+ }
3312
+ };
3313
+
3066
3314
  // src/commands/pipeline.ts
3315
+ function createProviderRegistry() {
3316
+ const registry = new ProviderRegistry();
3317
+ registry.register(new ClaudeProvider());
3318
+ registry.register(new CodexProvider());
3319
+ return registry;
3320
+ }
3321
+ function readMcpConfigAsProviderConfig(filePath) {
3322
+ const raw = JSON.parse(readFileSync2(filePath, "utf-8"));
3323
+ return { servers: raw.mcpServers };
3324
+ }
3067
3325
  function parseArgs(args) {
3068
3326
  const positional = [];
3069
3327
  let once = false;
@@ -3072,6 +3330,7 @@ function parseArgs(args) {
3072
3330
  let maxIterations = null;
3073
3331
  let maxBudget = null;
3074
3332
  let model = null;
3333
+ let provider = null;
3075
3334
  let concurrency = null;
3076
3335
  let logRetention = 7;
3077
3336
  let yes = false;
@@ -3112,6 +3371,9 @@ function parseArgs(args) {
3112
3371
  case "--model":
3113
3372
  model = args[++i] ?? null;
3114
3373
  break;
3374
+ case "--provider":
3375
+ provider = args[++i] ?? null;
3376
+ break;
3115
3377
  case "--concurrency": {
3116
3378
  const val = Number(args[++i]);
3117
3379
  if (isNaN(val) || val <= 0) {
@@ -3145,12 +3407,13 @@ Flags:
3145
3407
  --max-iterations <n> Override max iterations per ticket
3146
3408
  --max-budget <usd> Per-ticket budget cap (USD)
3147
3409
  --model <model> Override model preference
3410
+ --provider <id> Override default provider (claude, codex)
3148
3411
  --concurrency <n> Override concurrency per column
3149
3412
  --log-retention <d> Log retention in days (default: 7)
3150
3413
  --yes, -y Skip safety confirmation`);
3151
3414
  process.exit(1);
3152
3415
  }
3153
- return { boardId, once, dryRun, columnFilter, maxIterations, maxBudget, model, concurrency, logRetention, yes };
3416
+ return { boardId, once, dryRun, columnFilter, maxIterations, maxBudget, model, provider, concurrency, logRetention, yes };
3154
3417
  }
3155
3418
  function pidDir(boardId) {
3156
3419
  return join2(homedir(), ".kantban", "pipelines", boardId);
@@ -3172,21 +3435,6 @@ function removePidFile(boardId) {
3172
3435
  function childManifestPath(boardId) {
3173
3436
  return join2(pidDir(boardId), "children.pid");
3174
3437
  }
3175
- function appendChildPid(boardId, pid) {
3176
- const dir = pidDir(boardId);
3177
- mkdirSync2(dir, { recursive: true, mode: 448 });
3178
- appendFileSync2(childManifestPath(boardId), `${String(pid)}
3179
- `, { mode: 384 });
3180
- }
3181
- function removeChildPid(boardId, pid) {
3182
- const manifestPath = childManifestPath(boardId);
3183
- try {
3184
- const contents = readFileSync2(manifestPath, "utf-8");
3185
- const pids = contents.split("\n").filter((l) => l.trim() !== "" && l.trim() !== String(pid));
3186
- writeFileSync3(manifestPath, pids.length > 0 ? pids.join("\n") + "\n" : "", { mode: 384 });
3187
- } catch {
3188
- }
3189
- }
3190
3438
  function readChildManifest(boardId) {
3191
3439
  try {
3192
3440
  const contents = readFileSync2(childManifestPath(boardId), "utf-8");
@@ -3203,7 +3451,7 @@ function removeChildManifest(boardId) {
3203
3451
  }
3204
3452
  function cleanupOrphanedProcesses(boardId) {
3205
3453
  const pidPath = pidFilePath(boardId);
3206
- if (existsSync(pidPath)) {
3454
+ if (existsSync2(pidPath)) {
3207
3455
  try {
3208
3456
  const stalePid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
3209
3457
  if (stalePid && stalePid !== process.pid) {
@@ -3232,7 +3480,7 @@ function cleanupOrphanedProcesses(boardId) {
3232
3480
  }
3233
3481
  const staleReaperPath = join2(pidDir(boardId), "reaper.pid");
3234
3482
  try {
3235
- if (existsSync(staleReaperPath)) {
3483
+ if (existsSync2(staleReaperPath)) {
3236
3484
  const reaperPid = parseInt(readFileSync2(staleReaperPath, "utf-8").trim(), 10);
3237
3485
  if (reaperPid && !isNaN(reaperPid)) {
3238
3486
  try {
@@ -3247,133 +3495,6 @@ function cleanupOrphanedProcesses(boardId) {
3247
3495
  } catch {
3248
3496
  }
3249
3497
  }
3250
- var activeChildProcesses = /* @__PURE__ */ new Set();
3251
- var currentBoardId = "";
3252
- var CLAUDE_TIMEOUT_MS = 60 * 60 * 1e3;
3253
- async function invokeClaudeP(prompt, options) {
3254
- const args = ["-p", prompt, "--dangerously-skip-permissions", "--output-format", "stream-json", "--verbose"];
3255
- if (options.includeMcpConfig !== false && options.mcpConfigPath) {
3256
- args.push("--mcp-config", options.mcpConfigPath);
3257
- }
3258
- if (options.model) args.push("--model", options.model);
3259
- if (options.maxTurns) {
3260
- args.push("--max-turns", String(options.maxTurns));
3261
- } else if (options.maxBudgetUsd !== void 0 && options.maxBudgetUsd !== null) {
3262
- args.push("--max-turns", String(Math.max(1, Math.ceil(options.maxBudgetUsd * 10))));
3263
- }
3264
- if (options.worktree) args.push("--worktree", options.worktree);
3265
- if (options.tools !== void 0) {
3266
- args.push("--tools", options.tools);
3267
- }
3268
- if (options.allowedTools?.length) {
3269
- args.push("--allowedTools", ...options.allowedTools);
3270
- }
3271
- if (options.disallowedTools?.length) {
3272
- args.push("--disallowedTools", ...options.disallowedTools);
3273
- }
3274
- return new Promise((resolve) => {
3275
- const child = spawn2("claude", args, {
3276
- stdio: ["pipe", "pipe", "pipe"]
3277
- });
3278
- activeChildProcesses.add(child);
3279
- if (child.pid && currentBoardId) appendChildPid(currentBoardId, child.pid);
3280
- const parser = new StreamJsonParser();
3281
- parser.on("error", (err) => {
3282
- process.stderr.write(`[stream-parser] ${err.message}
3283
- `);
3284
- });
3285
- let lastOutput = "";
3286
- let tokensIn = 0;
3287
- let tokensOut = 0;
3288
- parser.on("event", (event) => {
3289
- if (event.type === "result") {
3290
- const usage = event.usage;
3291
- tokensIn += usage?.input_tokens ?? 0;
3292
- tokensOut += usage?.output_tokens ?? 0;
3293
- if (typeof event.result === "string") lastOutput = event.result;
3294
- }
3295
- options.onStreamEvent?.(event);
3296
- });
3297
- child.stdout?.on("data", (chunk) => {
3298
- parser.feed(chunk.toString());
3299
- });
3300
- let stderr = "";
3301
- child.stderr?.on("data", (chunk) => {
3302
- stderr += chunk.toString();
3303
- });
3304
- child.stdin?.end();
3305
- let killTimer;
3306
- const timeoutHandle = setTimeout(() => {
3307
- try {
3308
- child.kill("SIGTERM");
3309
- } catch {
3310
- }
3311
- killTimer = setTimeout(() => {
3312
- if (!resolved) {
3313
- try {
3314
- child.kill("SIGKILL");
3315
- } catch {
3316
- }
3317
- }
3318
- }, 5e3);
3319
- }, CLAUDE_TIMEOUT_MS);
3320
- let resolved = false;
3321
- child.on("close", (code) => {
3322
- if (resolved) return;
3323
- resolved = true;
3324
- clearTimeout(timeoutHandle);
3325
- if (killTimer) clearTimeout(killTimer);
3326
- activeChildProcesses.delete(child);
3327
- if (child.pid && currentBoardId) removeChildPid(currentBoardId, child.pid);
3328
- parser.flush();
3329
- resolve({
3330
- exitCode: code ?? 1,
3331
- output: lastOutput || stderr,
3332
- toolCallCount: parser.getToolCallCount(),
3333
- tokensIn,
3334
- tokensOut
3335
- });
3336
- });
3337
- child.on("error", (err) => {
3338
- if (resolved) return;
3339
- resolved = true;
3340
- clearTimeout(timeoutHandle);
3341
- if (killTimer) clearTimeout(killTimer);
3342
- activeChildProcesses.delete(child);
3343
- if (child.pid && currentBoardId) removeChildPid(currentBoardId, child.pid);
3344
- parser.flush();
3345
- resolve({
3346
- exitCode: 1,
3347
- output: err.message,
3348
- toolCallCount: parser.getToolCallCount(),
3349
- tokensIn,
3350
- tokensOut
3351
- });
3352
- });
3353
- });
3354
- }
3355
- function killAllChildProcesses() {
3356
- for (const child of activeChildProcesses) {
3357
- try {
3358
- if (child.pid) {
3359
- try {
3360
- process.kill(-child.pid, "SIGTERM");
3361
- } catch {
3362
- try {
3363
- child.kill("SIGTERM");
3364
- } catch {
3365
- }
3366
- }
3367
- } else {
3368
- try {
3369
- child.kill("SIGTERM");
3370
- } catch {
3371
- }
3372
- }
3373
- } catch {
3374
- }
3375
- }
3376
- }
3377
3498
  function printSafetyWarning() {
3378
3499
  console.log(`
3379
3500
  === SAFETY WARNING ===
@@ -3407,7 +3528,7 @@ async function runPipeline(client, args) {
3407
3528
  return;
3408
3529
  }
3409
3530
  const opts = parseArgs(args);
3410
- currentBoardId = opts.boardId;
3531
+ const registry = createProviderRegistry();
3411
3532
  if (!opts.yes && !opts.dryRun) {
3412
3533
  printSafetyWarning();
3413
3534
  const confirmed = await waitForConfirmation();
@@ -3418,7 +3539,7 @@ async function runPipeline(client, args) {
3418
3539
  }
3419
3540
  const gateFilePath = join2(process.cwd(), "pipeline.gates.yaml");
3420
3541
  let gateConfig;
3421
- if (!existsSync(gateFilePath)) {
3542
+ if (!existsSync2(gateFilePath)) {
3422
3543
  console.error(`Error: pipeline.gates.yaml not found in ${process.cwd()}`);
3423
3544
  console.error('Run "kantban pipeline init" to generate a starter gate file.');
3424
3545
  process.exit(1);
@@ -3447,6 +3568,11 @@ async function runPipeline(client, args) {
3447
3568
  }
3448
3569
  }
3449
3570
  const mcpConfigPath = generateMcpConfig(client.baseUrl, client.token, opts.boardId);
3571
+ const boardProviderConfig = {
3572
+ default_provider: opts.provider ?? void 0,
3573
+ intelligence_provider: opts.provider ?? void 0
3574
+ };
3575
+ const intelligenceProvider = registry.resolveForIntelligence(boardProviderConfig);
3450
3576
  const logBaseDir = join2(homedir(), ".kantban", "pipelines");
3451
3577
  const logger = new PipelineLogger(logBaseDir, opts.boardId);
3452
3578
  logger.pruneOldLogs(opts.logRetention);
@@ -3475,7 +3601,16 @@ async function runPipeline(client, args) {
3475
3601
  { columnId }
3476
3602
  );
3477
3603
  const resolvedColumnName = colScopeForName.column.name;
3604
+ const columnProviderOverride = colScopeForName.agent_config?.provider;
3605
+ const columnProvider = registry.resolveForColumn(
3606
+ boardProviderConfig,
3607
+ columnProviderOverride ? { provider: columnProviderOverride } : {}
3608
+ );
3609
+ if (effectiveConfig.model) {
3610
+ effectiveConfig.model = registry.resolveModel(columnProvider, effectiveConfig.model);
3611
+ }
3478
3612
  const columnGates = resolveGatesForColumn(gateConfig, resolvedColumnName);
3613
+ const gateCwd = effectiveConfig.worktreeName ? join2(process.cwd(), effectiveConfig.worktreeName) : void 0;
3479
3614
  const effectiveMcpConfigPath = columnGates.length > 0 ? generateGateProxyMcpConfig(
3480
3615
  client.baseUrl,
3481
3616
  client.token,
@@ -3483,14 +3618,17 @@ async function runPipeline(client, args) {
3483
3618
  gateFilePath,
3484
3619
  columnId,
3485
3620
  resolvedColumnName,
3486
- projectId
3621
+ projectId,
3622
+ gateCwd,
3623
+ ticketId
3487
3624
  ) : mcpConfigPath;
3625
+ const columnMcpConfig = readMcpConfigAsProviderConfig(effectiveMcpConfigPath);
3488
3626
  const loopDeps = {
3489
3627
  fetchTicketContext: (tid) => client.get(`/projects/${projectId}/pipeline-context`, { ticketId: tid }),
3490
3628
  fetchColumnContext: (cid) => client.get(`/projects/${projectId}/pipeline-context`, { columnId: cid }),
3491
3629
  fetchFingerprint: (tid) => client.getFingerprint(projectId, tid),
3492
- invokeClaudeP,
3493
- mcpConfigPath: effectiveMcpConfigPath,
3630
+ provider: columnProvider,
3631
+ mcpConfig: columnMcpConfig,
3494
3632
  projectId,
3495
3633
  log: (msg) => {
3496
3634
  logger.orchestrator(`[${ticketId}] ${msg}`);
@@ -3570,25 +3708,25 @@ async function runPipeline(client, args) {
3570
3708
  if (deps.setFieldValue) {
3571
3709
  effectiveConfig.onCheckpoint = async (tid, checkpoint) => {
3572
3710
  try {
3573
- await deps.setFieldValue(tid, "loop_checkpoint", checkpoint);
3574
- } catch {
3711
+ const serialized = JSON.stringify(checkpoint);
3712
+ await deps.setFieldValue(tid, "loop_checkpoint", serialized);
3713
+ } catch (err) {
3714
+ console.error(` [checkpoint] Failed to write for ${tid}: ${err instanceof Error ? err.message : String(err)}`);
3575
3715
  }
3576
3716
  };
3577
3717
  }
3578
3718
  if (effectiveConfig.stuckDetection) {
3579
3719
  effectiveConfig.invokeStuckDetection = async (input) => {
3580
3720
  const prompt = composeStuckDetectionPrompt(input);
3581
- const { exitCode, output } = await invokeClaudeP(prompt, {
3582
- mcpConfigPath,
3583
- model: "haiku",
3584
- maxBudgetUsd: 0.01,
3585
- tools: "",
3586
- includeMcpConfig: false
3721
+ const result = await intelligenceProvider.invoke({
3722
+ prompt,
3723
+ model: registry.resolveModel(intelligenceProvider, "fast"),
3724
+ maxTurns: 1
3587
3725
  });
3588
- if (exitCode !== 0) {
3589
- throw new Error(`Stuck detection call failed (exit ${String(exitCode)})`);
3726
+ if (result.exitCode !== 0) {
3727
+ throw new Error(`Stuck detection call failed (exit ${String(result.exitCode)})`);
3590
3728
  }
3591
- return parseStuckDetectionResponse(output);
3729
+ return parseStuckDetectionResponse(result.output);
3592
3730
  };
3593
3731
  }
3594
3732
  effectiveConfig.onPostIterationGates = async (tid, iteration) => {
@@ -3663,21 +3801,24 @@ async function runPipeline(client, args) {
3663
3801
  };
3664
3802
  const prompt = composeLightPrompt(lightCtx);
3665
3803
  logger.orchestrator(`[${ticketId}] Light call: composing prompt for column "${colScope.column.name}"`);
3666
- const { exitCode, output, tokensIn, tokensOut } = await invokeClaudeP(prompt, {
3667
- mcpConfigPath,
3668
- model: "haiku",
3669
- maxBudgetUsd: 0.01,
3670
- maxTurns: 3,
3671
- tools: "",
3672
- // Strip all built-in tools
3673
- includeMcpConfig: false
3674
- // No MCP tools either
3804
+ const lightModel = registry.resolveModel(intelligenceProvider, "fast");
3805
+ const lightResult = await intelligenceProvider.invoke({
3806
+ prompt,
3807
+ model: lightModel,
3808
+ maxTurns: 3
3675
3809
  });
3676
- costTracker?.record({ ticketId, columnId, model: "haiku", tokensIn, tokensOut, type: "light" });
3677
- if (exitCode !== 0) {
3678
- throw new Error(`Light call exited with code ${exitCode}: ${output.slice(0, 200)}`);
3810
+ costTracker?.record({
3811
+ ticketId,
3812
+ columnId,
3813
+ model: lightModel,
3814
+ tokensIn: lightResult.usage.inputTokens,
3815
+ tokensOut: lightResult.usage.outputTokens,
3816
+ type: "light"
3817
+ });
3818
+ if (lightResult.exitCode !== 0) {
3819
+ throw new Error(`Light call exited with code ${lightResult.exitCode}: ${lightResult.output.slice(0, 200)}`);
3679
3820
  }
3680
- const response = parseLightResponse(output);
3821
+ const response = parseLightResponse(lightResult.output);
3681
3822
  logger.orchestrator(`[${ticketId}] Light call result: ${response.action} \u2014 ${response.reason}`);
3682
3823
  return response;
3683
3824
  },
@@ -3685,18 +3826,24 @@ async function runPipeline(client, args) {
3685
3826
  invokeAdvisor: async (input) => {
3686
3827
  const prompt = composeAdvisorPrompt(input);
3687
3828
  logger.orchestrator(`[${input.ticketId}] Advisor: invoking for ${input.exitReason}`);
3688
- const { exitCode, output, tokensIn, tokensOut } = await invokeClaudeP(prompt, {
3689
- mcpConfigPath,
3690
- model: "haiku",
3691
- maxBudgetUsd: 0.01,
3692
- tools: "",
3693
- includeMcpConfig: false
3829
+ const advisorModel = registry.resolveModel(intelligenceProvider, "fast");
3830
+ const advisorResult = await intelligenceProvider.invoke({
3831
+ prompt,
3832
+ model: advisorModel,
3833
+ maxTurns: 1
3834
+ });
3835
+ costTracker?.record({
3836
+ ticketId: input.ticketId,
3837
+ columnId: "",
3838
+ model: advisorModel,
3839
+ tokensIn: advisorResult.usage.inputTokens,
3840
+ tokensOut: advisorResult.usage.outputTokens,
3841
+ type: "advisor"
3694
3842
  });
3695
- costTracker?.record({ ticketId: input.ticketId, columnId: "", model: "haiku", tokensIn, tokensOut, type: "advisor" });
3696
- if (exitCode !== 0) {
3697
- throw new Error(`Advisor call exited with code ${exitCode}: ${output.slice(0, 200)}`);
3843
+ if (advisorResult.exitCode !== 0) {
3844
+ throw new Error(`Advisor call exited with code ${advisorResult.exitCode}: ${advisorResult.output.slice(0, 200)}`);
3698
3845
  }
3699
- const response = parseAdvisorResponse(output);
3846
+ const response = parseAdvisorResponse(advisorResult.output);
3700
3847
  logger.orchestrator(`[${input.ticketId}] Advisor: ${response.action} \u2014 ${response.reason}`);
3701
3848
  return response;
3702
3849
  },
@@ -3707,11 +3854,13 @@ async function runPipeline(client, args) {
3707
3854
  });
3708
3855
  },
3709
3856
  getFieldValues: async (ticketId) => {
3710
- const ctx = await client.get(
3711
- `/projects/${projectId}/pipeline-context`,
3712
- { ticketId }
3857
+ const data = await client.get(
3858
+ `/projects/${projectId}/tickets/${ticketId}/field-values`
3713
3859
  );
3714
- return ctx.field_values;
3860
+ return data.map((fv) => ({
3861
+ field_name: fv.field_name,
3862
+ value: fv.text_value ?? fv.json_value ?? fv.number_value
3863
+ }));
3715
3864
  },
3716
3865
  // Ticket management for advisor actions (ESCALATE, SPLIT_TICKET)
3717
3866
  moveTicketToColumn: async (ticketId, columnId, handoff) => {
@@ -3749,23 +3898,29 @@ async function runPipeline(client, args) {
3749
3898
  gateSnapshotStore,
3750
3899
  invokeReplanner: async (state) => {
3751
3900
  const prompt = composeReplannerPrompt(state);
3752
- const { exitCode, output, tokensIn, tokensOut } = await invokeClaudeP(prompt, {
3753
- mcpConfigPath,
3754
- model: "haiku",
3755
- maxBudgetUsd: 0.01,
3756
- tools: "",
3757
- includeMcpConfig: false
3901
+ const replannerModel = registry.resolveModel(intelligenceProvider, "fast");
3902
+ const replannerResult = await intelligenceProvider.invoke({
3903
+ prompt,
3904
+ model: replannerModel,
3905
+ maxTurns: 1
3906
+ });
3907
+ costTracker?.record({
3908
+ ticketId: "replanner",
3909
+ columnId: "pipeline",
3910
+ model: replannerModel,
3911
+ tokensIn: replannerResult.usage.inputTokens,
3912
+ tokensOut: replannerResult.usage.outputTokens,
3913
+ type: "replanner"
3758
3914
  });
3759
- costTracker?.record({ ticketId: "replanner", columnId: "pipeline", model: "haiku", tokensIn, tokensOut, type: "replanner" });
3760
- if (exitCode !== 0) throw new Error(`Replanner failed`);
3761
- return parseReplannerResponse(output);
3915
+ if (replannerResult.exitCode !== 0) throw new Error(`Replanner failed`);
3916
+ return parseReplannerResponse(replannerResult.output);
3762
3917
  }
3763
3918
  };
3764
3919
  const orchestrator = new PipelineOrchestrator(opts.boardId, projectId, deps);
3765
3920
  console.log(`Initializing pipeline for board ${opts.boardId}...`);
3766
3921
  logger.orchestrator("Initializing pipeline");
3767
3922
  try {
3768
- await orchestrator.initialize();
3923
+ await orchestrator.initialize(opts.columnFilter);
3769
3924
  } catch (err) {
3770
3925
  const message = err instanceof Error ? err.message : String(err);
3771
3926
  console.error(`Error: Failed to initialize pipeline: ${message}`);
@@ -3866,34 +4021,10 @@ Received ${signal}. Shutting down gracefully...`);
3866
4021
  for (const loop of activeRalphLoops) {
3867
4022
  loop.stop();
3868
4023
  }
3869
- killAllChildProcesses();
3870
4024
  const deadline = Date.now() + 5e3;
3871
- while (activeChildProcesses.size > 0 && Date.now() < deadline) {
4025
+ while (activeRalphLoops.size > 0 && Date.now() < deadline) {
3872
4026
  await new Promise((r) => setTimeout(r, 200));
3873
4027
  }
3874
- if (activeChildProcesses.size > 0) {
3875
- console.error(`Warning: ${String(activeChildProcesses.size)} child process(es) did not exit. Sending SIGKILL...`);
3876
- for (const child of activeChildProcesses) {
3877
- try {
3878
- if (child.pid) {
3879
- try {
3880
- process.kill(-child.pid, "SIGKILL");
3881
- } catch {
3882
- try {
3883
- child.kill("SIGKILL");
3884
- } catch {
3885
- }
3886
- }
3887
- } else {
3888
- try {
3889
- child.kill("SIGKILL");
3890
- } catch {
3891
- }
3892
- }
3893
- } catch {
3894
- }
3895
- }
3896
- }
3897
4028
  if (costTracker) {
3898
4029
  console.log("\n--- Pipeline Cost Report ---");
3899
4030
  console.log(costTracker.generateReport(gateConfig.settings?.pricing));
@@ -4068,7 +4199,7 @@ async function waitForAllLoops(orchestrator, timeoutMs = 4 * 60 * 60 * 1e3) {
4068
4199
  return;
4069
4200
  }
4070
4201
  await new Promise((resolve) => setTimeout(resolve, 1e3));
4071
- if (activeRalphLoops.size === 0 && orchestrator.activeLoopCount === 0 && !orchestrator.hasActiveQueuedWork) {
4202
+ if (activeRalphLoops.size === 0 && orchestrator.activeLoopCount === 0 && !orchestrator.hasActiveQueuedWork && !orchestrator.hasCompletingWork) {
4072
4203
  consecutiveIdle++;
4073
4204
  } else {
4074
4205
  consecutiveIdle = 0;
@@ -4122,4 +4253,4 @@ export {
4122
4253
  runPipeline,
4123
4254
  stopPipeline
4124
4255
  };
4125
- //# sourceMappingURL=pipeline-UNO4PIW4.js.map
4256
+ //# sourceMappingURL=pipeline-O2JPCI2C.js.map