kantban-cli 0.1.15 → 0.1.16

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-FF77FM7X.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,6 +3601,14 @@ 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);
3479
3613
  const effectiveMcpConfigPath = columnGates.length > 0 ? generateGateProxyMcpConfig(
3480
3614
  client.baseUrl,
@@ -3485,12 +3619,13 @@ async function runPipeline(client, args) {
3485
3619
  resolvedColumnName,
3486
3620
  projectId
3487
3621
  ) : mcpConfigPath;
3622
+ const columnMcpConfig = readMcpConfigAsProviderConfig(effectiveMcpConfigPath);
3488
3623
  const loopDeps = {
3489
3624
  fetchTicketContext: (tid) => client.get(`/projects/${projectId}/pipeline-context`, { ticketId: tid }),
3490
3625
  fetchColumnContext: (cid) => client.get(`/projects/${projectId}/pipeline-context`, { columnId: cid }),
3491
3626
  fetchFingerprint: (tid) => client.getFingerprint(projectId, tid),
3492
- invokeClaudeP,
3493
- mcpConfigPath: effectiveMcpConfigPath,
3627
+ provider: columnProvider,
3628
+ mcpConfig: columnMcpConfig,
3494
3629
  projectId,
3495
3630
  log: (msg) => {
3496
3631
  logger.orchestrator(`[${ticketId}] ${msg}`);
@@ -3570,25 +3705,25 @@ async function runPipeline(client, args) {
3570
3705
  if (deps.setFieldValue) {
3571
3706
  effectiveConfig.onCheckpoint = async (tid, checkpoint) => {
3572
3707
  try {
3573
- await deps.setFieldValue(tid, "loop_checkpoint", checkpoint);
3574
- } catch {
3708
+ const serialized = JSON.stringify(checkpoint);
3709
+ await deps.setFieldValue(tid, "loop_checkpoint", serialized);
3710
+ } catch (err) {
3711
+ console.error(` [checkpoint] Failed to write for ${tid}: ${err instanceof Error ? err.message : String(err)}`);
3575
3712
  }
3576
3713
  };
3577
3714
  }
3578
3715
  if (effectiveConfig.stuckDetection) {
3579
3716
  effectiveConfig.invokeStuckDetection = async (input) => {
3580
3717
  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
3718
+ const result = await intelligenceProvider.invoke({
3719
+ prompt,
3720
+ model: registry.resolveModel(intelligenceProvider, "fast"),
3721
+ maxTurns: 1
3587
3722
  });
3588
- if (exitCode !== 0) {
3589
- throw new Error(`Stuck detection call failed (exit ${String(exitCode)})`);
3723
+ if (result.exitCode !== 0) {
3724
+ throw new Error(`Stuck detection call failed (exit ${String(result.exitCode)})`);
3590
3725
  }
3591
- return parseStuckDetectionResponse(output);
3726
+ return parseStuckDetectionResponse(result.output);
3592
3727
  };
3593
3728
  }
3594
3729
  effectiveConfig.onPostIterationGates = async (tid, iteration) => {
@@ -3663,21 +3798,24 @@ async function runPipeline(client, args) {
3663
3798
  };
3664
3799
  const prompt = composeLightPrompt(lightCtx);
3665
3800
  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
3801
+ const lightModel = registry.resolveModel(intelligenceProvider, "fast");
3802
+ const lightResult = await intelligenceProvider.invoke({
3803
+ prompt,
3804
+ model: lightModel,
3805
+ maxTurns: 3
3675
3806
  });
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)}`);
3807
+ costTracker?.record({
3808
+ ticketId,
3809
+ columnId,
3810
+ model: lightModel,
3811
+ tokensIn: lightResult.usage.inputTokens,
3812
+ tokensOut: lightResult.usage.outputTokens,
3813
+ type: "light"
3814
+ });
3815
+ if (lightResult.exitCode !== 0) {
3816
+ throw new Error(`Light call exited with code ${lightResult.exitCode}: ${lightResult.output.slice(0, 200)}`);
3679
3817
  }
3680
- const response = parseLightResponse(output);
3818
+ const response = parseLightResponse(lightResult.output);
3681
3819
  logger.orchestrator(`[${ticketId}] Light call result: ${response.action} \u2014 ${response.reason}`);
3682
3820
  return response;
3683
3821
  },
@@ -3685,18 +3823,24 @@ async function runPipeline(client, args) {
3685
3823
  invokeAdvisor: async (input) => {
3686
3824
  const prompt = composeAdvisorPrompt(input);
3687
3825
  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
3826
+ const advisorModel = registry.resolveModel(intelligenceProvider, "fast");
3827
+ const advisorResult = await intelligenceProvider.invoke({
3828
+ prompt,
3829
+ model: advisorModel,
3830
+ maxTurns: 1
3831
+ });
3832
+ costTracker?.record({
3833
+ ticketId: input.ticketId,
3834
+ columnId: "",
3835
+ model: advisorModel,
3836
+ tokensIn: advisorResult.usage.inputTokens,
3837
+ tokensOut: advisorResult.usage.outputTokens,
3838
+ type: "advisor"
3694
3839
  });
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)}`);
3840
+ if (advisorResult.exitCode !== 0) {
3841
+ throw new Error(`Advisor call exited with code ${advisorResult.exitCode}: ${advisorResult.output.slice(0, 200)}`);
3698
3842
  }
3699
- const response = parseAdvisorResponse(output);
3843
+ const response = parseAdvisorResponse(advisorResult.output);
3700
3844
  logger.orchestrator(`[${input.ticketId}] Advisor: ${response.action} \u2014 ${response.reason}`);
3701
3845
  return response;
3702
3846
  },
@@ -3707,11 +3851,13 @@ async function runPipeline(client, args) {
3707
3851
  });
3708
3852
  },
3709
3853
  getFieldValues: async (ticketId) => {
3710
- const ctx = await client.get(
3711
- `/projects/${projectId}/pipeline-context`,
3712
- { ticketId }
3854
+ const data = await client.get(
3855
+ `/projects/${projectId}/tickets/${ticketId}/field-values`
3713
3856
  );
3714
- return ctx.field_values;
3857
+ return data.map((fv) => ({
3858
+ field_name: fv.field_name,
3859
+ value: fv.text_value ?? fv.json_value ?? fv.number_value
3860
+ }));
3715
3861
  },
3716
3862
  // Ticket management for advisor actions (ESCALATE, SPLIT_TICKET)
3717
3863
  moveTicketToColumn: async (ticketId, columnId, handoff) => {
@@ -3749,23 +3895,29 @@ async function runPipeline(client, args) {
3749
3895
  gateSnapshotStore,
3750
3896
  invokeReplanner: async (state) => {
3751
3897
  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
3898
+ const replannerModel = registry.resolveModel(intelligenceProvider, "fast");
3899
+ const replannerResult = await intelligenceProvider.invoke({
3900
+ prompt,
3901
+ model: replannerModel,
3902
+ maxTurns: 1
3903
+ });
3904
+ costTracker?.record({
3905
+ ticketId: "replanner",
3906
+ columnId: "pipeline",
3907
+ model: replannerModel,
3908
+ tokensIn: replannerResult.usage.inputTokens,
3909
+ tokensOut: replannerResult.usage.outputTokens,
3910
+ type: "replanner"
3758
3911
  });
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);
3912
+ if (replannerResult.exitCode !== 0) throw new Error(`Replanner failed`);
3913
+ return parseReplannerResponse(replannerResult.output);
3762
3914
  }
3763
3915
  };
3764
3916
  const orchestrator = new PipelineOrchestrator(opts.boardId, projectId, deps);
3765
3917
  console.log(`Initializing pipeline for board ${opts.boardId}...`);
3766
3918
  logger.orchestrator("Initializing pipeline");
3767
3919
  try {
3768
- await orchestrator.initialize();
3920
+ await orchestrator.initialize(opts.columnFilter);
3769
3921
  } catch (err) {
3770
3922
  const message = err instanceof Error ? err.message : String(err);
3771
3923
  console.error(`Error: Failed to initialize pipeline: ${message}`);
@@ -3866,34 +4018,10 @@ Received ${signal}. Shutting down gracefully...`);
3866
4018
  for (const loop of activeRalphLoops) {
3867
4019
  loop.stop();
3868
4020
  }
3869
- killAllChildProcesses();
3870
4021
  const deadline = Date.now() + 5e3;
3871
- while (activeChildProcesses.size > 0 && Date.now() < deadline) {
4022
+ while (activeRalphLoops.size > 0 && Date.now() < deadline) {
3872
4023
  await new Promise((r) => setTimeout(r, 200));
3873
4024
  }
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
4025
  if (costTracker) {
3898
4026
  console.log("\n--- Pipeline Cost Report ---");
3899
4027
  console.log(costTracker.generateReport(gateConfig.settings?.pricing));
@@ -4068,7 +4196,7 @@ async function waitForAllLoops(orchestrator, timeoutMs = 4 * 60 * 60 * 1e3) {
4068
4196
  return;
4069
4197
  }
4070
4198
  await new Promise((resolve) => setTimeout(resolve, 1e3));
4071
- if (activeRalphLoops.size === 0 && orchestrator.activeLoopCount === 0 && !orchestrator.hasActiveQueuedWork) {
4199
+ if (activeRalphLoops.size === 0 && orchestrator.activeLoopCount === 0 && !orchestrator.hasActiveQueuedWork && !orchestrator.hasCompletingWork) {
4072
4200
  consecutiveIdle++;
4073
4201
  } else {
4074
4202
  consecutiveIdle = 0;
@@ -4122,4 +4250,4 @@ export {
4122
4250
  runPipeline,
4123
4251
  stopPipeline
4124
4252
  };
4125
- //# sourceMappingURL=pipeline-UNO4PIW4.js.map
4253
+ //# sourceMappingURL=pipeline-6SDPVNFK.js.map