kantban-cli 0.1.14 → 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, execSync } 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 {
@@ -3246,153 +3494,6 @@ function cleanupOrphanedProcesses(boardId) {
3246
3494
  }
3247
3495
  } catch {
3248
3496
  }
3249
- try {
3250
- const boardDir = pidDir(boardId).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3251
- const out = execSync(
3252
- `ps aux | grep 'claude.*-p' | grep '${boardDir}' | grep -v grep | awk '{print $2}'`,
3253
- { encoding: "utf-8", timeout: 5e3 }
3254
- ).trim();
3255
- if (out) {
3256
- const pids = out.split("\n").filter(Boolean);
3257
- for (const pid of pids) {
3258
- try {
3259
- process.kill(parseInt(pid, 10), "SIGTERM");
3260
- } catch {
3261
- }
3262
- }
3263
- if (pids.length > 0) {
3264
- console.log(`Killed ${String(pids.length)} orphaned claude agent(s)`);
3265
- }
3266
- }
3267
- } catch {
3268
- }
3269
- }
3270
- var activeChildProcesses = /* @__PURE__ */ new Set();
3271
- var currentBoardId = "";
3272
- var CLAUDE_TIMEOUT_MS = 60 * 60 * 1e3;
3273
- async function invokeClaudeP(prompt, options) {
3274
- const args = ["-p", prompt, "--dangerously-skip-permissions", "--output-format", "stream-json", "--verbose"];
3275
- if (options.includeMcpConfig !== false && options.mcpConfigPath) {
3276
- args.push("--mcp-config", options.mcpConfigPath);
3277
- }
3278
- if (options.model) args.push("--model", options.model);
3279
- if (options.maxTurns) {
3280
- args.push("--max-turns", String(options.maxTurns));
3281
- } else if (options.maxBudgetUsd !== void 0 && options.maxBudgetUsd !== null) {
3282
- args.push("--max-turns", String(Math.max(1, Math.ceil(options.maxBudgetUsd * 10))));
3283
- }
3284
- if (options.worktree) args.push("--worktree", options.worktree);
3285
- if (options.tools !== void 0) {
3286
- args.push("--tools", options.tools);
3287
- }
3288
- if (options.allowedTools?.length) {
3289
- args.push("--allowedTools", ...options.allowedTools);
3290
- }
3291
- if (options.disallowedTools?.length) {
3292
- args.push("--disallowedTools", ...options.disallowedTools);
3293
- }
3294
- return new Promise((resolve) => {
3295
- const child = spawn2("claude", args, {
3296
- stdio: ["pipe", "pipe", "pipe"]
3297
- });
3298
- activeChildProcesses.add(child);
3299
- if (child.pid && currentBoardId) appendChildPid(currentBoardId, child.pid);
3300
- const parser = new StreamJsonParser();
3301
- parser.on("error", (err) => {
3302
- process.stderr.write(`[stream-parser] ${err.message}
3303
- `);
3304
- });
3305
- let lastOutput = "";
3306
- let tokensIn = 0;
3307
- let tokensOut = 0;
3308
- parser.on("event", (event) => {
3309
- if (event.type === "result") {
3310
- const usage = event.usage;
3311
- tokensIn += usage?.input_tokens ?? 0;
3312
- tokensOut += usage?.output_tokens ?? 0;
3313
- if (typeof event.result === "string") lastOutput = event.result;
3314
- }
3315
- options.onStreamEvent?.(event);
3316
- });
3317
- child.stdout?.on("data", (chunk) => {
3318
- parser.feed(chunk.toString());
3319
- });
3320
- let stderr = "";
3321
- child.stderr?.on("data", (chunk) => {
3322
- stderr += chunk.toString();
3323
- });
3324
- child.stdin?.end();
3325
- let killTimer;
3326
- const timeoutHandle = setTimeout(() => {
3327
- try {
3328
- child.kill("SIGTERM");
3329
- } catch {
3330
- }
3331
- killTimer = setTimeout(() => {
3332
- if (!resolved) {
3333
- try {
3334
- child.kill("SIGKILL");
3335
- } catch {
3336
- }
3337
- }
3338
- }, 5e3);
3339
- }, CLAUDE_TIMEOUT_MS);
3340
- let resolved = false;
3341
- child.on("close", (code) => {
3342
- if (resolved) return;
3343
- resolved = true;
3344
- clearTimeout(timeoutHandle);
3345
- if (killTimer) clearTimeout(killTimer);
3346
- activeChildProcesses.delete(child);
3347
- if (child.pid && currentBoardId) removeChildPid(currentBoardId, child.pid);
3348
- parser.flush();
3349
- resolve({
3350
- exitCode: code ?? 1,
3351
- output: lastOutput || stderr,
3352
- toolCallCount: parser.getToolCallCount(),
3353
- tokensIn,
3354
- tokensOut
3355
- });
3356
- });
3357
- child.on("error", (err) => {
3358
- if (resolved) return;
3359
- resolved = true;
3360
- clearTimeout(timeoutHandle);
3361
- if (killTimer) clearTimeout(killTimer);
3362
- activeChildProcesses.delete(child);
3363
- if (child.pid && currentBoardId) removeChildPid(currentBoardId, child.pid);
3364
- parser.flush();
3365
- resolve({
3366
- exitCode: 1,
3367
- output: err.message,
3368
- toolCallCount: parser.getToolCallCount(),
3369
- tokensIn,
3370
- tokensOut
3371
- });
3372
- });
3373
- });
3374
- }
3375
- function killAllChildProcesses() {
3376
- for (const child of activeChildProcesses) {
3377
- try {
3378
- if (child.pid) {
3379
- try {
3380
- process.kill(-child.pid, "SIGTERM");
3381
- } catch {
3382
- try {
3383
- child.kill("SIGTERM");
3384
- } catch {
3385
- }
3386
- }
3387
- } else {
3388
- try {
3389
- child.kill("SIGTERM");
3390
- } catch {
3391
- }
3392
- }
3393
- } catch {
3394
- }
3395
- }
3396
3497
  }
3397
3498
  function printSafetyWarning() {
3398
3499
  console.log(`
@@ -3427,7 +3528,7 @@ async function runPipeline(client, args) {
3427
3528
  return;
3428
3529
  }
3429
3530
  const opts = parseArgs(args);
3430
- currentBoardId = opts.boardId;
3531
+ const registry = createProviderRegistry();
3431
3532
  if (!opts.yes && !opts.dryRun) {
3432
3533
  printSafetyWarning();
3433
3534
  const confirmed = await waitForConfirmation();
@@ -3438,7 +3539,7 @@ async function runPipeline(client, args) {
3438
3539
  }
3439
3540
  const gateFilePath = join2(process.cwd(), "pipeline.gates.yaml");
3440
3541
  let gateConfig;
3441
- if (!existsSync(gateFilePath)) {
3542
+ if (!existsSync2(gateFilePath)) {
3442
3543
  console.error(`Error: pipeline.gates.yaml not found in ${process.cwd()}`);
3443
3544
  console.error('Run "kantban pipeline init" to generate a starter gate file.');
3444
3545
  process.exit(1);
@@ -3467,6 +3568,11 @@ async function runPipeline(client, args) {
3467
3568
  }
3468
3569
  }
3469
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);
3470
3576
  const logBaseDir = join2(homedir(), ".kantban", "pipelines");
3471
3577
  const logger = new PipelineLogger(logBaseDir, opts.boardId);
3472
3578
  logger.pruneOldLogs(opts.logRetention);
@@ -3495,6 +3601,14 @@ async function runPipeline(client, args) {
3495
3601
  { columnId }
3496
3602
  );
3497
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
+ }
3498
3612
  const columnGates = resolveGatesForColumn(gateConfig, resolvedColumnName);
3499
3613
  const effectiveMcpConfigPath = columnGates.length > 0 ? generateGateProxyMcpConfig(
3500
3614
  client.baseUrl,
@@ -3505,12 +3619,13 @@ async function runPipeline(client, args) {
3505
3619
  resolvedColumnName,
3506
3620
  projectId
3507
3621
  ) : mcpConfigPath;
3622
+ const columnMcpConfig = readMcpConfigAsProviderConfig(effectiveMcpConfigPath);
3508
3623
  const loopDeps = {
3509
3624
  fetchTicketContext: (tid) => client.get(`/projects/${projectId}/pipeline-context`, { ticketId: tid }),
3510
3625
  fetchColumnContext: (cid) => client.get(`/projects/${projectId}/pipeline-context`, { columnId: cid }),
3511
3626
  fetchFingerprint: (tid) => client.getFingerprint(projectId, tid),
3512
- invokeClaudeP,
3513
- mcpConfigPath: effectiveMcpConfigPath,
3627
+ provider: columnProvider,
3628
+ mcpConfig: columnMcpConfig,
3514
3629
  projectId,
3515
3630
  log: (msg) => {
3516
3631
  logger.orchestrator(`[${ticketId}] ${msg}`);
@@ -3590,25 +3705,25 @@ async function runPipeline(client, args) {
3590
3705
  if (deps.setFieldValue) {
3591
3706
  effectiveConfig.onCheckpoint = async (tid, checkpoint) => {
3592
3707
  try {
3593
- await deps.setFieldValue(tid, "loop_checkpoint", checkpoint);
3594
- } 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)}`);
3595
3712
  }
3596
3713
  };
3597
3714
  }
3598
3715
  if (effectiveConfig.stuckDetection) {
3599
3716
  effectiveConfig.invokeStuckDetection = async (input) => {
3600
3717
  const prompt = composeStuckDetectionPrompt(input);
3601
- const { exitCode, output } = await invokeClaudeP(prompt, {
3602
- mcpConfigPath,
3603
- model: "haiku",
3604
- maxBudgetUsd: 0.01,
3605
- tools: "",
3606
- includeMcpConfig: false
3718
+ const result = await intelligenceProvider.invoke({
3719
+ prompt,
3720
+ model: registry.resolveModel(intelligenceProvider, "fast"),
3721
+ maxTurns: 1
3607
3722
  });
3608
- if (exitCode !== 0) {
3609
- 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)})`);
3610
3725
  }
3611
- return parseStuckDetectionResponse(output);
3726
+ return parseStuckDetectionResponse(result.output);
3612
3727
  };
3613
3728
  }
3614
3729
  effectiveConfig.onPostIterationGates = async (tid, iteration) => {
@@ -3683,21 +3798,24 @@ async function runPipeline(client, args) {
3683
3798
  };
3684
3799
  const prompt = composeLightPrompt(lightCtx);
3685
3800
  logger.orchestrator(`[${ticketId}] Light call: composing prompt for column "${colScope.column.name}"`);
3686
- const { exitCode, output, tokensIn, tokensOut } = await invokeClaudeP(prompt, {
3687
- mcpConfigPath,
3688
- model: "haiku",
3689
- maxBudgetUsd: 0.01,
3690
- maxTurns: 3,
3691
- tools: "",
3692
- // Strip all built-in tools
3693
- includeMcpConfig: false
3694
- // 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
3695
3806
  });
3696
- costTracker?.record({ ticketId, columnId, model: "haiku", tokensIn, tokensOut, type: "light" });
3697
- if (exitCode !== 0) {
3698
- 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)}`);
3699
3817
  }
3700
- const response = parseLightResponse(output);
3818
+ const response = parseLightResponse(lightResult.output);
3701
3819
  logger.orchestrator(`[${ticketId}] Light call result: ${response.action} \u2014 ${response.reason}`);
3702
3820
  return response;
3703
3821
  },
@@ -3705,18 +3823,24 @@ async function runPipeline(client, args) {
3705
3823
  invokeAdvisor: async (input) => {
3706
3824
  const prompt = composeAdvisorPrompt(input);
3707
3825
  logger.orchestrator(`[${input.ticketId}] Advisor: invoking for ${input.exitReason}`);
3708
- const { exitCode, output, tokensIn, tokensOut } = await invokeClaudeP(prompt, {
3709
- mcpConfigPath,
3710
- model: "haiku",
3711
- maxBudgetUsd: 0.01,
3712
- tools: "",
3713
- 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"
3714
3839
  });
3715
- costTracker?.record({ ticketId: input.ticketId, columnId: "", model: "haiku", tokensIn, tokensOut, type: "advisor" });
3716
- if (exitCode !== 0) {
3717
- 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)}`);
3718
3842
  }
3719
- const response = parseAdvisorResponse(output);
3843
+ const response = parseAdvisorResponse(advisorResult.output);
3720
3844
  logger.orchestrator(`[${input.ticketId}] Advisor: ${response.action} \u2014 ${response.reason}`);
3721
3845
  return response;
3722
3846
  },
@@ -3727,11 +3851,13 @@ async function runPipeline(client, args) {
3727
3851
  });
3728
3852
  },
3729
3853
  getFieldValues: async (ticketId) => {
3730
- const ctx = await client.get(
3731
- `/projects/${projectId}/pipeline-context`,
3732
- { ticketId }
3854
+ const data = await client.get(
3855
+ `/projects/${projectId}/tickets/${ticketId}/field-values`
3733
3856
  );
3734
- 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
+ }));
3735
3861
  },
3736
3862
  // Ticket management for advisor actions (ESCALATE, SPLIT_TICKET)
3737
3863
  moveTicketToColumn: async (ticketId, columnId, handoff) => {
@@ -3769,23 +3895,29 @@ async function runPipeline(client, args) {
3769
3895
  gateSnapshotStore,
3770
3896
  invokeReplanner: async (state) => {
3771
3897
  const prompt = composeReplannerPrompt(state);
3772
- const { exitCode, output, tokensIn, tokensOut } = await invokeClaudeP(prompt, {
3773
- mcpConfigPath,
3774
- model: "haiku",
3775
- maxBudgetUsd: 0.01,
3776
- tools: "",
3777
- 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"
3778
3911
  });
3779
- costTracker?.record({ ticketId: "replanner", columnId: "pipeline", model: "haiku", tokensIn, tokensOut, type: "replanner" });
3780
- if (exitCode !== 0) throw new Error(`Replanner failed`);
3781
- return parseReplannerResponse(output);
3912
+ if (replannerResult.exitCode !== 0) throw new Error(`Replanner failed`);
3913
+ return parseReplannerResponse(replannerResult.output);
3782
3914
  }
3783
3915
  };
3784
3916
  const orchestrator = new PipelineOrchestrator(opts.boardId, projectId, deps);
3785
3917
  console.log(`Initializing pipeline for board ${opts.boardId}...`);
3786
3918
  logger.orchestrator("Initializing pipeline");
3787
3919
  try {
3788
- await orchestrator.initialize();
3920
+ await orchestrator.initialize(opts.columnFilter);
3789
3921
  } catch (err) {
3790
3922
  const message = err instanceof Error ? err.message : String(err);
3791
3923
  console.error(`Error: Failed to initialize pipeline: ${message}`);
@@ -3886,34 +4018,10 @@ Received ${signal}. Shutting down gracefully...`);
3886
4018
  for (const loop of activeRalphLoops) {
3887
4019
  loop.stop();
3888
4020
  }
3889
- killAllChildProcesses();
3890
4021
  const deadline = Date.now() + 5e3;
3891
- while (activeChildProcesses.size > 0 && Date.now() < deadline) {
4022
+ while (activeRalphLoops.size > 0 && Date.now() < deadline) {
3892
4023
  await new Promise((r) => setTimeout(r, 200));
3893
4024
  }
3894
- if (activeChildProcesses.size > 0) {
3895
- console.error(`Warning: ${String(activeChildProcesses.size)} child process(es) did not exit. Sending SIGKILL...`);
3896
- for (const child of activeChildProcesses) {
3897
- try {
3898
- if (child.pid) {
3899
- try {
3900
- process.kill(-child.pid, "SIGKILL");
3901
- } catch {
3902
- try {
3903
- child.kill("SIGKILL");
3904
- } catch {
3905
- }
3906
- }
3907
- } else {
3908
- try {
3909
- child.kill("SIGKILL");
3910
- } catch {
3911
- }
3912
- }
3913
- } catch {
3914
- }
3915
- }
3916
- }
3917
4025
  if (costTracker) {
3918
4026
  console.log("\n--- Pipeline Cost Report ---");
3919
4027
  console.log(costTracker.generateReport(gateConfig.settings?.pricing));
@@ -4088,7 +4196,7 @@ async function waitForAllLoops(orchestrator, timeoutMs = 4 * 60 * 60 * 1e3) {
4088
4196
  return;
4089
4197
  }
4090
4198
  await new Promise((resolve) => setTimeout(resolve, 1e3));
4091
- if (activeRalphLoops.size === 0 && orchestrator.activeLoopCount === 0 && !orchestrator.hasActiveQueuedWork) {
4199
+ if (activeRalphLoops.size === 0 && orchestrator.activeLoopCount === 0 && !orchestrator.hasActiveQueuedWork && !orchestrator.hasCompletingWork) {
4092
4200
  consecutiveIdle++;
4093
4201
  } else {
4094
4202
  consecutiveIdle = 0;
@@ -4142,4 +4250,4 @@ export {
4142
4250
  runPipeline,
4143
4251
  stopPipeline
4144
4252
  };
4145
- //# sourceMappingURL=pipeline-7OFX75AU.js.map
4253
+ //# sourceMappingURL=pipeline-6SDPVNFK.js.map