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.
- package/dist/chunk-4IUZAIFL.js +102 -0
- package/dist/chunk-4IUZAIFL.js.map +1 -0
- package/dist/chunk-CQP4B53A.js +140 -0
- package/dist/chunk-CQP4B53A.js.map +1 -0
- package/dist/chunk-FF77FM7X.js +1159 -0
- package/dist/chunk-FF77FM7X.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-FJVZR2JW.js} +10 -18
- package/dist/cron-FJVZR2JW.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 +462 -0
- package/dist/lib/gate-proxy-server.js.map +1 -0
- package/dist/{pipeline-7OFX75AU.js → pipeline-6SDPVNFK.js} +494 -386
- package/dist/pipeline-6SDPVNFK.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-7OFX75AU.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-FF77FM7X.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 {
|
|
@@ -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
|
-
|
|
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 (!
|
|
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
|
-
|
|
3513
|
-
|
|
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
|
-
|
|
3594
|
-
|
|
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
|
|
3602
|
-
|
|
3603
|
-
model: "
|
|
3604
|
-
|
|
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
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
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({
|
|
3697
|
-
|
|
3698
|
-
|
|
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
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
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
|
-
|
|
3716
|
-
|
|
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
|
|
3731
|
-
`/projects/${projectId}/
|
|
3732
|
-
{ ticketId }
|
|
3854
|
+
const data = await client.get(
|
|
3855
|
+
`/projects/${projectId}/tickets/${ticketId}/field-values`
|
|
3733
3856
|
);
|
|
3734
|
-
return
|
|
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
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
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
|
-
|
|
3780
|
-
|
|
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 (
|
|
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-
|
|
4253
|
+
//# sourceMappingURL=pipeline-6SDPVNFK.js.map
|