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.
- package/dist/chunk-4IUZAIFL.js +102 -0
- package/dist/chunk-4IUZAIFL.js.map +1 -0
- package/dist/chunk-4RQDDZLM.js +1162 -0
- package/dist/chunk-4RQDDZLM.js.map +1 -0
- package/dist/chunk-CQP4B53A.js +140 -0
- package/dist/chunk-CQP4B53A.js.map +1 -0
- package/dist/chunk-MN4H5NSU.js +3149 -0
- package/dist/chunk-MN4H5NSU.js.map +1 -0
- package/dist/{cron-AZPDPON3.js → cron-CO7W4PHL.js} +10 -18
- package/dist/cron-CO7W4PHL.js.map +1 -0
- package/dist/index.js +6 -138
- package/dist/index.js.map +1 -1
- package/dist/lib/gate-proxy-server.d.ts +1 -0
- package/dist/lib/gate-proxy-server.js +467 -0
- package/dist/lib/gate-proxy-server.js.map +1 -0
- package/dist/{pipeline-UNO4PIW4.js → pipeline-O2JPCI2C.js} +498 -367
- package/dist/pipeline-O2JPCI2C.js.map +1 -0
- package/package.json +3 -1
- package/dist/chunk-KGS3M2MY.js +0 -4067
- package/dist/chunk-KGS3M2MY.js.map +0 -1
- package/dist/cron-AZPDPON3.js.map +0 -1
- package/dist/pipeline-UNO4PIW4.js.map +0 -1
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
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-
|
|
22
|
+
} from "./chunk-MN4H5NSU.js";
|
|
17
23
|
|
|
18
24
|
// src/commands/pipeline.ts
|
|
19
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
3493
|
-
|
|
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
|
-
|
|
3574
|
-
|
|
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
|
|
3582
|
-
|
|
3583
|
-
model: "
|
|
3584
|
-
|
|
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
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
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({
|
|
3677
|
-
|
|
3678
|
-
|
|
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
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
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
|
-
|
|
3696
|
-
|
|
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
|
|
3711
|
-
`/projects/${projectId}/
|
|
3712
|
-
{ ticketId }
|
|
3857
|
+
const data = await client.get(
|
|
3858
|
+
`/projects/${projectId}/tickets/${ticketId}/field-values`
|
|
3713
3859
|
);
|
|
3714
|
-
return
|
|
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
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
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
|
-
|
|
3760
|
-
|
|
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 (
|
|
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-
|
|
4256
|
+
//# sourceMappingURL=pipeline-O2JPCI2C.js.map
|