kantban-cli 0.1.49 → 0.1.51
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-DENXSVKE.js → chunk-GQQUH3TA.js} +7 -2
- package/dist/chunk-GQQUH3TA.js.map +1 -0
- package/dist/{chunk-QHJZIGEE.js → chunk-X2CJ3ZAI.js} +51 -48
- package/dist/chunk-X2CJ3ZAI.js.map +1 -0
- package/dist/{cron-F6D6475M.js → cron-OCKARAAM.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/lib/gate-proxy-server.js +1 -1
- package/dist/{pipeline-GZOSDNPF.js → pipeline-6J64Z7VH.js} +254 -96
- package/dist/pipeline-6J64Z7VH.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-DENXSVKE.js.map +0 -1
- package/dist/chunk-QHJZIGEE.js.map +0 -1
- package/dist/pipeline-GZOSDNPF.js.map +0 -1
- /package/dist/{cron-F6D6475M.js.map → cron-OCKARAAM.js.map} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runGates
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-GQQUH3TA.js";
|
|
4
4
|
import {
|
|
5
5
|
ClaudeProvider,
|
|
6
6
|
RalphLoop,
|
|
@@ -8,12 +8,13 @@ import {
|
|
|
8
8
|
cleanupGateProxyConfigs,
|
|
9
9
|
cleanupMcpConfig,
|
|
10
10
|
composeStuckDetectionPrompt,
|
|
11
|
+
detectBranchMerged,
|
|
11
12
|
generateGateProxyMcpConfig,
|
|
12
13
|
generateMcpConfig,
|
|
13
14
|
parseJsonFromLlmOutput,
|
|
14
15
|
parseStuckDetectionResponse,
|
|
15
16
|
reapOrphanedMcpConfigDirs
|
|
16
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-X2CJ3ZAI.js";
|
|
17
18
|
import {
|
|
18
19
|
LoopCheckpointSchema,
|
|
19
20
|
VerdictSchema,
|
|
@@ -31,9 +32,11 @@ import {
|
|
|
31
32
|
} from "./chunk-5ZU2OOES.js";
|
|
32
33
|
|
|
33
34
|
// src/commands/pipeline.ts
|
|
34
|
-
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync3, unlinkSync as unlinkSync3, existsSync as existsSync3, appendFileSync as appendFileSync2 } from "fs";
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
35
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync3, unlinkSync as unlinkSync3, existsSync as existsSync3, appendFileSync as appendFileSync2, promises as fsPromises } from "fs";
|
|
36
|
+
import { execFile } from "child_process";
|
|
37
|
+
import { homedir as homedir3 } from "os";
|
|
38
|
+
import { join as join4, dirname as dirname2, resolve, sep } from "path";
|
|
39
|
+
import { promisify } from "util";
|
|
37
40
|
|
|
38
41
|
// src/lib/harness-signal.ts
|
|
39
42
|
var HARNESS_SIGNAL_PREFIX = "@harness:";
|
|
@@ -57,6 +60,8 @@ function resolveToolRestrictions(builtinTools, allowedTools, disallowedTools) {
|
|
|
57
60
|
|
|
58
61
|
// src/lib/worktree.ts
|
|
59
62
|
import { execFile as defaultExecFile, execFileSync } from "child_process";
|
|
63
|
+
import { homedir } from "os";
|
|
64
|
+
import { join } from "path";
|
|
60
65
|
function generateWorktreeName(ticketNumber, columnName) {
|
|
61
66
|
const slug = columnSlug(columnName);
|
|
62
67
|
return `kantban-${ticketNumber}-${slug}`;
|
|
@@ -64,10 +69,16 @@ function generateWorktreeName(ticketNumber, columnName) {
|
|
|
64
69
|
function columnSlug(columnName) {
|
|
65
70
|
return columnName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
66
71
|
}
|
|
67
|
-
function renderWorktreePath(ticketNumber, columnName, pathPattern) {
|
|
72
|
+
function renderWorktreePath(ticketNumber, columnName, pathPattern, options) {
|
|
73
|
+
const slug = columnSlug(columnName);
|
|
68
74
|
const worktreeName = generateWorktreeName(ticketNumber, columnName);
|
|
69
|
-
if (
|
|
70
|
-
|
|
75
|
+
if (pathPattern) {
|
|
76
|
+
return pathPattern.replace(/\{ticket_number\}/g, String(ticketNumber)).replace(/\{column_slug\}/g, slug).replace(/\{worktree_name\}/g, worktreeName);
|
|
77
|
+
}
|
|
78
|
+
return join(options.defaultRoot, options.boardId, slug, String(ticketNumber));
|
|
79
|
+
}
|
|
80
|
+
function defaultWorktreeRoot() {
|
|
81
|
+
return join(homedir(), ".kantban", "worktrees");
|
|
71
82
|
}
|
|
72
83
|
function isPlausibleRemoteUrl(url) {
|
|
73
84
|
return url.includes("/") || url.includes("@") || url.includes("://");
|
|
@@ -115,22 +126,22 @@ async function cleanupWorktree(worktreeName, exec = defaultExecFile) {
|
|
|
115
126
|
} catch {
|
|
116
127
|
return true;
|
|
117
128
|
}
|
|
118
|
-
return new Promise((
|
|
129
|
+
return new Promise((resolve2) => {
|
|
119
130
|
exec("git", ["worktree", "remove", "--force", target], (err) => {
|
|
120
131
|
if (err) {
|
|
121
132
|
console.error(`[worktree] cleanup failed for ${worktreeName} (${target}): ${err.message}`);
|
|
122
|
-
|
|
133
|
+
resolve2(false);
|
|
123
134
|
} else {
|
|
124
|
-
|
|
135
|
+
resolve2(true);
|
|
125
136
|
}
|
|
126
137
|
});
|
|
127
138
|
});
|
|
128
139
|
}
|
|
129
140
|
function execPromise(exec, cmd, args) {
|
|
130
|
-
return new Promise((
|
|
141
|
+
return new Promise((resolve2, reject) => {
|
|
131
142
|
exec(cmd, args, (err, stdout, stderr) => {
|
|
132
143
|
if (err) reject(Object.assign(err, { stdout, stderr }));
|
|
133
|
-
else
|
|
144
|
+
else resolve2({ stdout, stderr });
|
|
134
145
|
});
|
|
135
146
|
});
|
|
136
147
|
}
|
|
@@ -942,8 +953,8 @@ var PipelineOrchestrator = class {
|
|
|
942
953
|
}
|
|
943
954
|
for (const [ticketId, columnId] of Array.from(this.deferredTickets)) {
|
|
944
955
|
try {
|
|
945
|
-
const
|
|
946
|
-
if (!
|
|
956
|
+
const blocker = await this.deps.hasUnresolvedBlockers(ticketId);
|
|
957
|
+
if (!blocker.blocked) {
|
|
947
958
|
this.deferredTickets.delete(ticketId);
|
|
948
959
|
await this.spawnOrQueue(ticketId, columnId, true);
|
|
949
960
|
} else {
|
|
@@ -957,26 +968,56 @@ var PipelineOrchestrator = class {
|
|
|
957
968
|
}
|
|
958
969
|
await this.refreshBoardScope();
|
|
959
970
|
this.blockedColumns.clear();
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
this.
|
|
969
|
-
|
|
971
|
+
await Promise.all(
|
|
972
|
+
Array.from(this.pipelineColumns.keys()).map(async (columnId) => {
|
|
973
|
+
await this.refreshColumnScope(columnId);
|
|
974
|
+
const colScope = this.columnScopes.get(columnId);
|
|
975
|
+
if (!colScope) {
|
|
976
|
+
console.error(` [scan] column=${columnId} reason="no cached scope"`);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (this.isColumnBlocked(columnId)) {
|
|
980
|
+
this.blockedColumns.add(columnId);
|
|
981
|
+
console.error(
|
|
982
|
+
` [scan] column=${columnId} name="${colScope.column.name}" tickets=${colScope.tickets.length} dispatched=0 queued=0 skipped=${colScope.tickets.length} reason="firing_constraint"`
|
|
983
|
+
);
|
|
984
|
+
for (const ticket of colScope.tickets) {
|
|
985
|
+
await this.emitDispatchDeferred(ticket.id, "firing_constraint", { columnId });
|
|
986
|
+
}
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
let dispatched = 0;
|
|
990
|
+
let queued = 0;
|
|
991
|
+
let skipped = 0;
|
|
992
|
+
const reasons = /* @__PURE__ */ new Set();
|
|
970
993
|
for (const ticket of colScope.tickets) {
|
|
971
|
-
|
|
994
|
+
const wasActive = this.activeLoops.has(ticket.id) || this.spawning.has(ticket.id);
|
|
995
|
+
const wasQueued = (this.loopQueues.get(columnId) ?? []).includes(ticket.id);
|
|
996
|
+
const wasDeferred = this.deferredTickets.has(ticket.id);
|
|
997
|
+
await this.spawnOrQueue(ticket.id, columnId);
|
|
998
|
+
const nowActive = this.activeLoops.has(ticket.id) || this.spawning.has(ticket.id);
|
|
999
|
+
const nowQueued = (this.loopQueues.get(columnId) ?? []).includes(ticket.id);
|
|
1000
|
+
const nowDeferred = this.deferredTickets.has(ticket.id);
|
|
1001
|
+
if (!wasActive && nowActive) {
|
|
1002
|
+
dispatched++;
|
|
1003
|
+
reasons.add("dispatched");
|
|
1004
|
+
} else if (!wasQueued && nowQueued) {
|
|
1005
|
+
queued++;
|
|
1006
|
+
reasons.add("capped");
|
|
1007
|
+
} else if (!wasDeferred && nowDeferred) {
|
|
1008
|
+
skipped++;
|
|
1009
|
+
reasons.add("blocked");
|
|
1010
|
+
} else {
|
|
1011
|
+
skipped++;
|
|
1012
|
+
reasons.add("known");
|
|
1013
|
+
}
|
|
972
1014
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
}
|
|
1015
|
+
const reason = reasons.size === 0 ? "none" : reasons.size === 1 ? [...reasons][0] : "mixed";
|
|
1016
|
+
console.error(
|
|
1017
|
+
` [scan] column=${columnId} name="${colScope.column.name}" tickets=${colScope.tickets.length} dispatched=${dispatched} queued=${queued} skipped=${skipped} reason="${reason}"`
|
|
1018
|
+
);
|
|
1019
|
+
})
|
|
1020
|
+
);
|
|
980
1021
|
}
|
|
981
1022
|
/**
|
|
982
1023
|
* Handle a pipeline event (typically from WebSocket via EventQueue).
|
|
@@ -1098,15 +1139,58 @@ var PipelineOrchestrator = class {
|
|
|
1098
1139
|
}
|
|
1099
1140
|
return;
|
|
1100
1141
|
}
|
|
1142
|
+
const isMergeColumn = colConfig.name.toLowerCase().includes("merge");
|
|
1143
|
+
if (isMergeColumn && colConfig.worktreeEnabled && this.deps.detectBranchMerged && this.deps.moveTicketToColumn) {
|
|
1144
|
+
const ticket = this.columnScopes.get(columnId)?.tickets.find((t) => t.id === ticketId);
|
|
1145
|
+
if (ticket) {
|
|
1146
|
+
const wPath = renderWorktreePath(
|
|
1147
|
+
ticket.ticket_number,
|
|
1148
|
+
colConfig.name,
|
|
1149
|
+
colConfig.worktreePathPattern,
|
|
1150
|
+
{ boardId: this.boardId, defaultRoot: defaultWorktreeRoot() }
|
|
1151
|
+
);
|
|
1152
|
+
try {
|
|
1153
|
+
const mergeResult = await this.deps.detectBranchMerged({
|
|
1154
|
+
worktreePath: wPath,
|
|
1155
|
+
iterationStartedAt: /* @__PURE__ */ new Date(0),
|
|
1156
|
+
targetBranch: colConfig.worktreeIntegrationBranch ?? "main"
|
|
1157
|
+
});
|
|
1158
|
+
if (mergeResult.merged) {
|
|
1159
|
+
const nextColumnId = this.findNextColumnId(columnId);
|
|
1160
|
+
if (nextColumnId) {
|
|
1161
|
+
await this.deps.createComment(
|
|
1162
|
+
ticketId,
|
|
1163
|
+
`## Merge Complete (auto-finalized)
|
|
1164
|
+
|
|
1165
|
+
Merge commit \`${mergeResult.mergeCommitSha}\` detected on \`origin/${colConfig.worktreeIntegrationBranch ?? "main"}\` at ${mergeResult.mergeCommitTime.toISOString()}. The previous agent did not record the column move before exiting; the orchestrator is finalizing the move now.`
|
|
1166
|
+
);
|
|
1167
|
+
await this.deps.moveTicketToColumn(ticketId, nextColumnId, {
|
|
1168
|
+
reason: "merge_finalize_recovery",
|
|
1169
|
+
merge_commit_sha: mergeResult.mergeCommitSha
|
|
1170
|
+
});
|
|
1171
|
+
console.error(
|
|
1172
|
+
` [merge-recovery] ticket=${ticketId} column=${columnId} target=${colConfig.worktreeIntegrationBranch ?? "main"} sha=${mergeResult.mergeCommitSha}`
|
|
1173
|
+
);
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
} catch {
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1101
1181
|
this.spawning.add(ticketId);
|
|
1102
1182
|
this.reserveSlot(columnId);
|
|
1103
1183
|
try {
|
|
1104
|
-
const
|
|
1105
|
-
if (blocked) {
|
|
1184
|
+
const result = await this.deps.hasUnresolvedBlockers(ticketId);
|
|
1185
|
+
if (result.blocked) {
|
|
1106
1186
|
this.spawning.delete(ticketId);
|
|
1107
1187
|
this.releaseSlot(columnId);
|
|
1108
1188
|
this.deferredTickets.set(ticketId, columnId);
|
|
1109
|
-
|
|
1189
|
+
const blockerSummary = result.blockers.map((b) => `${b.ticket_number}:${b.column}`).join(",");
|
|
1190
|
+
const linkIdSummary = result.linkIds.join(",");
|
|
1191
|
+
console.error(
|
|
1192
|
+
` [scan] skip ticket=${ticketId} reason=blocks count=${result.blockers.length} link_ids=[${linkIdSummary}] blockers=[${blockerSummary}]`
|
|
1193
|
+
);
|
|
1110
1194
|
void this.emitDispatchDeferred(ticketId, "unresolved_blockers", { columnId });
|
|
1111
1195
|
void this.drainQueue(columnId).catch((err) => {
|
|
1112
1196
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1121,9 +1205,9 @@ var PipelineOrchestrator = class {
|
|
|
1121
1205
|
this.releaseSlot(columnId);
|
|
1122
1206
|
this.deferredTickets.set(ticketId, columnId);
|
|
1123
1207
|
void this.emitDispatchDeferred(ticketId, "unresolved_blockers", { columnId });
|
|
1124
|
-
void this.drainQueue(columnId).catch((
|
|
1125
|
-
const
|
|
1126
|
-
console.error(` [error] drainQueue failed for column ${columnId}: ${
|
|
1208
|
+
void this.drainQueue(columnId).catch((dErr) => {
|
|
1209
|
+
const dMsg = dErr instanceof Error ? dErr.message : String(dErr);
|
|
1210
|
+
console.error(` [error] drainQueue failed for column ${columnId}: ${dMsg}`);
|
|
1127
1211
|
});
|
|
1128
1212
|
return;
|
|
1129
1213
|
}
|
|
@@ -1271,16 +1355,16 @@ var PipelineOrchestrator = class {
|
|
|
1271
1355
|
gutterThreshold: config.gutterThreshold,
|
|
1272
1356
|
...effectiveModel !== void 0 && { model: effectiveModel },
|
|
1273
1357
|
...config.maxBudgetUsd !== void 0 && { maxBudgetUsd: config.maxBudgetUsd },
|
|
1274
|
-
// Resolve worktree name (branch) and path from ticket context. When the
|
|
1275
|
-
// column's agent_config specifies a path_pattern, the filesystem path is
|
|
1276
|
-
// rendered from it — otherwise the name doubles as a relative path.
|
|
1277
1358
|
...(() => {
|
|
1278
1359
|
const colScope = this.columnScopes.get(columnId);
|
|
1279
1360
|
const ticket = colScope?.tickets.find((t) => t.id === ticketId);
|
|
1280
1361
|
if (!config.worktreeEnabled || !ticket) return {};
|
|
1281
1362
|
const wName = generateWorktreeName(ticket.ticket_number, config.name);
|
|
1282
|
-
const wPath = renderWorktreePath(ticket.ticket_number, config.name, config.worktreePathPattern
|
|
1283
|
-
|
|
1363
|
+
const wPath = renderWorktreePath(ticket.ticket_number, config.name, config.worktreePathPattern, {
|
|
1364
|
+
boardId: this.boardId,
|
|
1365
|
+
defaultRoot: defaultWorktreeRoot()
|
|
1366
|
+
});
|
|
1367
|
+
return { worktreeName: wName, worktreePath: wPath };
|
|
1284
1368
|
})(),
|
|
1285
1369
|
...config.lookaheadColumnId !== void 0 && { lookaheadColumnId: config.lookaheadColumnId },
|
|
1286
1370
|
// Resume from checkpoint iteration/gutter if provided
|
|
@@ -1799,6 +1883,19 @@ ${findingsText}`)
|
|
|
1799
1883
|
const nextBoardCol = bs.columns.filter((c) => c.position > currentBoardCol.position && this.pipelineColumns.has(c.id)).sort((a, b) => a.position - b.position)[0];
|
|
1800
1884
|
return nextBoardCol ? this.pipelineColumns.get(nextBoardCol.id) ?? null : null;
|
|
1801
1885
|
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Find the ID of the next board column by position, regardless of pipeline status.
|
|
1888
|
+
* Used by merge-recovery to target the downstream column (e.g. Done) which may not
|
|
1889
|
+
* be a pipeline column.
|
|
1890
|
+
*/
|
|
1891
|
+
findNextColumnId(currentColumnId) {
|
|
1892
|
+
const bs = this.cachedBoardScope;
|
|
1893
|
+
if (!bs) return null;
|
|
1894
|
+
const currentBoardCol = bs.columns.find((c) => c.id === currentColumnId);
|
|
1895
|
+
if (!currentBoardCol) return null;
|
|
1896
|
+
const nextBoardCol = bs.columns.filter((c) => c.position > currentBoardCol.position).sort((a, b) => a.position - b.position)[0];
|
|
1897
|
+
return nextBoardCol?.id ?? null;
|
|
1898
|
+
}
|
|
1802
1899
|
/**
|
|
1803
1900
|
* Called when a loop finishes. Cleans up tracking and drains the queue.
|
|
1804
1901
|
*/
|
|
@@ -2186,8 +2283,8 @@ ${findingsText}`)
|
|
|
2186
2283
|
this.spawning.add(nextTicketId);
|
|
2187
2284
|
this.reserveSlot(columnId);
|
|
2188
2285
|
try {
|
|
2189
|
-
const
|
|
2190
|
-
if (blocked) {
|
|
2286
|
+
const blockerResult = await this.deps.hasUnresolvedBlockers(nextTicketId);
|
|
2287
|
+
if (blockerResult.blocked) {
|
|
2191
2288
|
this.deferredTickets.set(nextTicketId, columnId);
|
|
2192
2289
|
this.spawning.delete(nextTicketId);
|
|
2193
2290
|
this.releaseSlot(columnId);
|
|
@@ -2542,7 +2639,7 @@ var PipelineWsClient = class _PipelineWsClient {
|
|
|
2542
2639
|
this.cleanupTimers();
|
|
2543
2640
|
const { ticket } = await this.client.post("/ws-ticket");
|
|
2544
2641
|
const wsUrl = this.client.baseUrl.replace(/^http/, "ws") + `/ws?ticket=${ticket}&clientType=cli`;
|
|
2545
|
-
return new Promise((
|
|
2642
|
+
return new Promise((resolve2, reject) => {
|
|
2546
2643
|
let resolved = false;
|
|
2547
2644
|
this.ws = new WebSocket(wsUrl);
|
|
2548
2645
|
let subscribed = false;
|
|
@@ -2571,7 +2668,7 @@ var PipelineWsClient = class _PipelineWsClient {
|
|
|
2571
2668
|
this.options.onConnect();
|
|
2572
2669
|
if (!resolved) {
|
|
2573
2670
|
resolved = true;
|
|
2574
|
-
|
|
2671
|
+
resolve2();
|
|
2575
2672
|
}
|
|
2576
2673
|
}
|
|
2577
2674
|
this.options.onEvent(event);
|
|
@@ -2683,7 +2780,7 @@ var PipelineWsClient = class _PipelineWsClient {
|
|
|
2683
2780
|
|
|
2684
2781
|
// src/lib/logger.ts
|
|
2685
2782
|
import { mkdirSync, writeFileSync, appendFileSync, readdirSync, rmSync, statSync, renameSync } from "fs";
|
|
2686
|
-
import { join } from "path";
|
|
2783
|
+
import { join as join2 } from "path";
|
|
2687
2784
|
var MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
2688
2785
|
var MAX_ROTATED_FILES = 3;
|
|
2689
2786
|
var ROTATION_CHECK_INTERVAL = 100;
|
|
@@ -2691,11 +2788,11 @@ var PipelineLogger = class {
|
|
|
2691
2788
|
boardDir;
|
|
2692
2789
|
writeCount = 0;
|
|
2693
2790
|
constructor(baseDir, boardId) {
|
|
2694
|
-
this.boardDir =
|
|
2791
|
+
this.boardDir = join2(baseDir, boardId);
|
|
2695
2792
|
mkdirSync(this.boardDir, { recursive: true });
|
|
2696
2793
|
}
|
|
2697
2794
|
orchestrator(message) {
|
|
2698
|
-
const logPath =
|
|
2795
|
+
const logPath = join2(this.boardDir, "orchestrator.log");
|
|
2699
2796
|
const entry = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
|
|
2700
2797
|
`;
|
|
2701
2798
|
appendFileSync(logPath, entry);
|
|
@@ -2727,10 +2824,10 @@ var PipelineLogger = class {
|
|
|
2727
2824
|
}
|
|
2728
2825
|
}
|
|
2729
2826
|
iteration(ticketNumber, iterationNum, data) {
|
|
2730
|
-
const ticketDir =
|
|
2827
|
+
const ticketDir = join2(this.boardDir, ticketNumber);
|
|
2731
2828
|
mkdirSync(ticketDir, { recursive: true });
|
|
2732
2829
|
const padded = String(iterationNum).padStart(3, "0");
|
|
2733
|
-
const logPath =
|
|
2830
|
+
const logPath = join2(ticketDir, `iteration-${padded}.log`);
|
|
2734
2831
|
const entry = {
|
|
2735
2832
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2736
2833
|
iteration: iterationNum,
|
|
@@ -2749,7 +2846,7 @@ var PipelineLogger = class {
|
|
|
2749
2846
|
try {
|
|
2750
2847
|
const entries = readdirSync(this.boardDir, { withFileTypes: true });
|
|
2751
2848
|
for (const entry of entries) {
|
|
2752
|
-
const fullPath =
|
|
2849
|
+
const fullPath = join2(this.boardDir, entry.name);
|
|
2753
2850
|
if (entry.isDirectory() && entry.name !== ".") {
|
|
2754
2851
|
try {
|
|
2755
2852
|
const stat = statSync(fullPath);
|
|
@@ -3535,8 +3632,9 @@ var CodexProvider = class {
|
|
|
3535
3632
|
execFileSync2("git", ["worktree", "add", request.workingDirectory, branch], {
|
|
3536
3633
|
stdio: "pipe"
|
|
3537
3634
|
});
|
|
3538
|
-
} catch {
|
|
3539
|
-
|
|
3635
|
+
} catch (err) {
|
|
3636
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3637
|
+
throw new Error(`worktree_creation_failed: ${msg}`);
|
|
3540
3638
|
}
|
|
3541
3639
|
}
|
|
3542
3640
|
} else {
|
|
@@ -3555,17 +3653,18 @@ var CodexProvider = class {
|
|
|
3555
3653
|
execFileSync2("git", ["worktree", "add", request.workingDirectory, branch], {
|
|
3556
3654
|
stdio: "pipe"
|
|
3557
3655
|
});
|
|
3558
|
-
} catch {
|
|
3559
|
-
|
|
3656
|
+
} catch (err) {
|
|
3657
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3658
|
+
throw new Error(`worktree_creation_failed: ${msg}`);
|
|
3560
3659
|
}
|
|
3561
3660
|
}
|
|
3562
3661
|
}
|
|
3563
3662
|
}
|
|
3564
|
-
if (
|
|
3663
|
+
if (existsSync(request.workingDirectory)) {
|
|
3565
3664
|
ensureWorktreeRemote(request.workingDirectory);
|
|
3566
3665
|
}
|
|
3567
3666
|
}
|
|
3568
|
-
return new Promise((
|
|
3667
|
+
return new Promise((resolve2) => {
|
|
3569
3668
|
const [cmd, prefixArgs] = resolveCommand("codex");
|
|
3570
3669
|
const resolvedArgs = [...prefixArgs, ...args];
|
|
3571
3670
|
const child = spawn2(cmd, resolvedArgs, {
|
|
@@ -3616,7 +3715,7 @@ var CodexProvider = class {
|
|
|
3616
3715
|
durationMs: Date.now() - startTime
|
|
3617
3716
|
};
|
|
3618
3717
|
if (degraded.length > 0) result.degradedCapabilities = degraded;
|
|
3619
|
-
|
|
3718
|
+
resolve2(result);
|
|
3620
3719
|
};
|
|
3621
3720
|
child.on("close", (code) => finish(code));
|
|
3622
3721
|
child.on("error", (err) => finish(1, err.message));
|
|
@@ -3689,8 +3788,8 @@ var CodexProvider = class {
|
|
|
3689
3788
|
// src/providers/gemini-provider.ts
|
|
3690
3789
|
import { spawn as spawn3, execFileSync as execFileSync3 } from "child_process";
|
|
3691
3790
|
import { existsSync as existsSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, rmSync as rmSync3 } from "fs";
|
|
3692
|
-
import { join as
|
|
3693
|
-
import { homedir } from "os";
|
|
3791
|
+
import { join as join3, dirname } from "path";
|
|
3792
|
+
import { homedir as homedir2 } from "os";
|
|
3694
3793
|
import { fileURLToPath } from "url";
|
|
3695
3794
|
import { readFileSync as readFileSync2 } from "fs";
|
|
3696
3795
|
|
|
@@ -3853,13 +3952,13 @@ function resolveHookScriptPath() {
|
|
|
3853
3952
|
try {
|
|
3854
3953
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
3855
3954
|
const candidates = [
|
|
3856
|
-
|
|
3955
|
+
join3(thisDir, "lib", "gemini-hooks.mjs"),
|
|
3857
3956
|
// dist/lib/ (flat bundle)
|
|
3858
|
-
|
|
3957
|
+
join3(thisDir, "..", "lib", "gemini-hooks.mjs"),
|
|
3859
3958
|
// dist/../lib/ (nested)
|
|
3860
|
-
|
|
3959
|
+
join3(thisDir, "..", "src", "lib", "gemini-hooks.mjs"),
|
|
3861
3960
|
// source from dist/
|
|
3862
|
-
|
|
3961
|
+
join3(thisDir, "..", "..", "src", "lib", "gemini-hooks.mjs")
|
|
3863
3962
|
// source from dist/providers/
|
|
3864
3963
|
];
|
|
3865
3964
|
for (const p of candidates) {
|
|
@@ -3942,7 +4041,7 @@ var GeminiProvider = class _GeminiProvider {
|
|
|
3942
4041
|
if (request.workingDirectory && settingsDir) {
|
|
3943
4042
|
this.copySettingsToDir(settingsDir, request.workingDirectory);
|
|
3944
4043
|
}
|
|
3945
|
-
return new Promise((
|
|
4044
|
+
return new Promise((resolve2) => {
|
|
3946
4045
|
const [cmd, prefixArgs] = resolveCommand("gemini");
|
|
3947
4046
|
const resolvedArgs = [...prefixArgs, ...args];
|
|
3948
4047
|
const child = spawn3(cmd, resolvedArgs, {
|
|
@@ -4015,7 +4114,7 @@ var GeminiProvider = class _GeminiProvider {
|
|
|
4015
4114
|
durationMs: Date.now() - startTime
|
|
4016
4115
|
};
|
|
4017
4116
|
if (degraded.length > 0) result.degradedCapabilities = degraded;
|
|
4018
|
-
|
|
4117
|
+
resolve2(result);
|
|
4019
4118
|
};
|
|
4020
4119
|
child.on("close", (code) => finish(code));
|
|
4021
4120
|
child.on("error", (err) => finish(1, err.message));
|
|
@@ -4053,9 +4152,9 @@ var GeminiProvider = class _GeminiProvider {
|
|
|
4053
4152
|
}
|
|
4054
4153
|
writeGeminiSettings(request, degraded) {
|
|
4055
4154
|
try {
|
|
4056
|
-
const baseDir =
|
|
4057
|
-
const dir =
|
|
4058
|
-
const geminiDir =
|
|
4155
|
+
const baseDir = join3(homedir2(), ".kantban", "pipelines", "gemini-tmp");
|
|
4156
|
+
const dir = join3(baseDir, `session-${Date.now()}`);
|
|
4157
|
+
const geminiDir = join3(dir, ".gemini");
|
|
4059
4158
|
mkdirSync2(geminiDir, { recursive: true, mode: 448 });
|
|
4060
4159
|
const settings = {};
|
|
4061
4160
|
if (request.mcpConfig && Object.keys(request.mcpConfig.servers).length > 0) {
|
|
@@ -4079,20 +4178,20 @@ var GeminiProvider = class _GeminiProvider {
|
|
|
4079
4178
|
hookConfig.disallowedTools = tr.disallowedTools ? translateToolNames(tr.disallowedTools) : null;
|
|
4080
4179
|
if (tr.tools !== void 0) hookConfig.builtinToolsMode = tr.tools;
|
|
4081
4180
|
const ext = IS_WINDOWS ? ".cmd" : ".sh";
|
|
4082
|
-
const wrapperPath =
|
|
4083
|
-
writeFileSync3(wrapperPath, this.generateHookWrapper(hookPath, "BeforeToolSelection",
|
|
4181
|
+
const wrapperPath = join3(dir, `.kantban-hook-before-tool${ext}`);
|
|
4182
|
+
writeFileSync3(wrapperPath, this.generateHookWrapper(hookPath, "BeforeToolSelection", join3(dir, ".kantban-hook-config.json")), { mode: 493 });
|
|
4084
4183
|
hooks.BeforeToolSelection = [{ matcher: "*", hooks: [{ type: "command", command: wrapperPath, timeout: 3e4 }] }];
|
|
4085
4184
|
}
|
|
4086
4185
|
if (request.maxTurns && request.mcpConfig) {
|
|
4087
4186
|
hookConfig.maxTurns = request.maxTurns;
|
|
4088
|
-
hookConfig.turnFile =
|
|
4187
|
+
hookConfig.turnFile = join3(dir, ".kantban-turn-counter");
|
|
4089
4188
|
const ext = IS_WINDOWS ? ".cmd" : ".sh";
|
|
4090
|
-
const wrapperPath =
|
|
4091
|
-
writeFileSync3(wrapperPath, this.generateHookWrapper(hookPath, "AfterAgent",
|
|
4189
|
+
const wrapperPath = join3(dir, `.kantban-hook-after-agent${ext}`);
|
|
4190
|
+
writeFileSync3(wrapperPath, this.generateHookWrapper(hookPath, "AfterAgent", join3(dir, ".kantban-hook-config.json")), { mode: 493 });
|
|
4092
4191
|
hooks.AfterAgent = [{ matcher: "*", hooks: [{ type: "command", command: wrapperPath, timeout: 3e4 }] }];
|
|
4093
4192
|
}
|
|
4094
4193
|
if (Object.keys(hookConfig).length > 0) {
|
|
4095
|
-
writeFileSync3(
|
|
4194
|
+
writeFileSync3(join3(dir, ".kantban-hook-config.json"), JSON.stringify(hookConfig), { mode: 384 });
|
|
4096
4195
|
}
|
|
4097
4196
|
if (Object.keys(hooks).length > 0) {
|
|
4098
4197
|
settings.hooks = hooks;
|
|
@@ -4105,7 +4204,7 @@ var GeminiProvider = class _GeminiProvider {
|
|
|
4105
4204
|
process.stderr.write(`[gemini] Hook script not found \u2014 tool scoping and turn limits unavailable
|
|
4106
4205
|
`);
|
|
4107
4206
|
}
|
|
4108
|
-
writeFileSync3(
|
|
4207
|
+
writeFileSync3(join3(geminiDir, "settings.json"), JSON.stringify(settings, null, 2), { mode: 384 });
|
|
4109
4208
|
return dir;
|
|
4110
4209
|
} catch (err) {
|
|
4111
4210
|
process.stderr.write(`[gemini] Failed to write settings.json: ${err}
|
|
@@ -4171,17 +4270,29 @@ node "${hookScript}" "${event}" "${configFile}" < "$STDIN_FILE"
|
|
|
4171
4270
|
}
|
|
4172
4271
|
copySettingsToDir(settingsDir, targetDir) {
|
|
4173
4272
|
try {
|
|
4174
|
-
const sourceFile =
|
|
4175
|
-
const targetGeminiDir =
|
|
4273
|
+
const sourceFile = join3(settingsDir, ".gemini", "settings.json");
|
|
4274
|
+
const targetGeminiDir = join3(targetDir, ".gemini");
|
|
4176
4275
|
mkdirSync2(targetGeminiDir, { recursive: true, mode: 448 });
|
|
4177
4276
|
const content = readFileSync2(sourceFile, "utf-8");
|
|
4178
|
-
writeFileSync3(
|
|
4277
|
+
writeFileSync3(join3(targetGeminiDir, "settings.json"), content, { mode: 384 });
|
|
4179
4278
|
} catch {
|
|
4180
4279
|
}
|
|
4181
4280
|
}
|
|
4182
4281
|
};
|
|
4183
4282
|
|
|
4184
4283
|
// src/commands/pipeline.ts
|
|
4284
|
+
var execFileAsync = promisify(execFile);
|
|
4285
|
+
var _cachedPrimaryRepoRoot = null;
|
|
4286
|
+
async function getPrimaryRepoRoot() {
|
|
4287
|
+
if (_cachedPrimaryRepoRoot) return _cachedPrimaryRepoRoot;
|
|
4288
|
+
try {
|
|
4289
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"]);
|
|
4290
|
+
_cachedPrimaryRepoRoot = resolve(stdout.trim());
|
|
4291
|
+
} catch {
|
|
4292
|
+
_cachedPrimaryRepoRoot = resolve(process.cwd());
|
|
4293
|
+
}
|
|
4294
|
+
return _cachedPrimaryRepoRoot;
|
|
4295
|
+
}
|
|
4185
4296
|
function createProviderRegistry() {
|
|
4186
4297
|
const registry = new ProviderRegistry();
|
|
4187
4298
|
registry.register(new ClaudeProvider());
|
|
@@ -4303,10 +4414,10 @@ Flags:
|
|
|
4303
4414
|
return { boardId, once, dryRun, columnFilter, maxIterations, maxBudget, model, provider, concurrency, logRetention, yes };
|
|
4304
4415
|
}
|
|
4305
4416
|
function pidDir(boardId) {
|
|
4306
|
-
return
|
|
4417
|
+
return join4(homedir3(), ".kantban", "pipelines", boardId);
|
|
4307
4418
|
}
|
|
4308
4419
|
function pidFilePath(boardId) {
|
|
4309
|
-
return
|
|
4420
|
+
return join4(pidDir(boardId), "orchestrator.pid");
|
|
4310
4421
|
}
|
|
4311
4422
|
function writePidFile(boardId) {
|
|
4312
4423
|
const dir = pidDir(boardId);
|
|
@@ -4320,7 +4431,7 @@ function removePidFile(boardId) {
|
|
|
4320
4431
|
}
|
|
4321
4432
|
}
|
|
4322
4433
|
function childManifestPath(boardId) {
|
|
4323
|
-
return
|
|
4434
|
+
return join4(pidDir(boardId), "children.pid");
|
|
4324
4435
|
}
|
|
4325
4436
|
function readChildManifest(boardId) {
|
|
4326
4437
|
try {
|
|
@@ -4365,7 +4476,7 @@ function cleanupOrphanedProcesses(boardId) {
|
|
|
4365
4476
|
console.log(`Killed ${String(manifestPids.length)} orphaned child process(es) from manifest`);
|
|
4366
4477
|
removeChildManifest(boardId);
|
|
4367
4478
|
}
|
|
4368
|
-
const staleReaperPath =
|
|
4479
|
+
const staleReaperPath = join4(pidDir(boardId), "reaper.pid");
|
|
4369
4480
|
try {
|
|
4370
4481
|
if (existsSync3(staleReaperPath)) {
|
|
4371
4482
|
const reaperPid = parseInt(readFileSync3(staleReaperPath, "utf-8").trim(), 10);
|
|
@@ -4398,14 +4509,14 @@ Press Ctrl+C to cancel, or re-run with --yes to skip this warning.
|
|
|
4398
4509
|
`);
|
|
4399
4510
|
}
|
|
4400
4511
|
function waitForConfirmation() {
|
|
4401
|
-
return new Promise((
|
|
4512
|
+
return new Promise((resolve2) => {
|
|
4402
4513
|
process.stdout.write("Continue? [y/N] ");
|
|
4403
4514
|
process.stdin.setEncoding("utf8");
|
|
4404
4515
|
process.stdin.once("data", (data) => {
|
|
4405
4516
|
const answer = data.trim().toLowerCase();
|
|
4406
|
-
|
|
4517
|
+
resolve2(answer === "y" || answer === "yes");
|
|
4407
4518
|
});
|
|
4408
|
-
process.stdin.once("end", () =>
|
|
4519
|
+
process.stdin.once("end", () => resolve2(false));
|
|
4409
4520
|
});
|
|
4410
4521
|
}
|
|
4411
4522
|
async function runPipeline(client, args) {
|
|
@@ -4424,7 +4535,7 @@ async function runPipeline(client, args) {
|
|
|
4424
4535
|
return;
|
|
4425
4536
|
}
|
|
4426
4537
|
}
|
|
4427
|
-
const gateFilePath =
|
|
4538
|
+
const gateFilePath = join4(process.cwd(), "pipeline.gates.yaml");
|
|
4428
4539
|
let gateConfig;
|
|
4429
4540
|
if (!existsSync3(gateFilePath)) {
|
|
4430
4541
|
console.error(`Error: pipeline.gates.yaml not found in ${process.cwd()}`);
|
|
@@ -4461,7 +4572,7 @@ async function runPipeline(client, args) {
|
|
|
4461
4572
|
...opts.provider ? { intelligence_provider: opts.provider } : {}
|
|
4462
4573
|
};
|
|
4463
4574
|
const intelligenceProvider = registry.resolveForIntelligence(boardProviderConfig);
|
|
4464
|
-
const logBaseDir =
|
|
4575
|
+
const logBaseDir = join4(homedir3(), ".kantban", "pipelines");
|
|
4465
4576
|
const logger = new PipelineLogger(logBaseDir, opts.boardId);
|
|
4466
4577
|
logger.pruneOldLogs(opts.logRetention);
|
|
4467
4578
|
let runMemory = null;
|
|
@@ -4483,6 +4594,32 @@ async function runPipeline(client, args) {
|
|
|
4483
4594
|
...opts.maxBudget !== null && { maxBudgetUsd: opts.maxBudget },
|
|
4484
4595
|
...opts.model !== null && { model: opts.model }
|
|
4485
4596
|
};
|
|
4597
|
+
if (effectiveConfig.worktreePath) {
|
|
4598
|
+
const primaryRepoRoot = await getPrimaryRepoRoot();
|
|
4599
|
+
const resolvedCwd = resolve(effectiveConfig.worktreePath);
|
|
4600
|
+
const inside = resolvedCwd === primaryRepoRoot || resolvedCwd.startsWith(primaryRepoRoot + sep) || primaryRepoRoot.startsWith(resolvedCwd + sep);
|
|
4601
|
+
if (inside) {
|
|
4602
|
+
const errMsg = `[fatal] dispatch refused: resolved cwd "${resolvedCwd}" is inside, equal to, or a parent of the primary repo "${primaryRepoRoot}". Configure agent_config.worktree.path_pattern to a path outside the primary repo.`;
|
|
4603
|
+
logger.orchestrator(errMsg);
|
|
4604
|
+
await deps.createComment(ticketId, `[dispatch] failed: cwd would be inside the primary repo. Update agent_config.worktree.path_pattern to a path outside this repo.`);
|
|
4605
|
+
return { reason: "error", iterations: 0, gutterCount: 0, lastError: errMsg };
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
if (effectiveConfig.worktreePath) {
|
|
4609
|
+
const parent = dirname2(effectiveConfig.worktreePath);
|
|
4610
|
+
try {
|
|
4611
|
+
await fsPromises.mkdir(parent, { recursive: true });
|
|
4612
|
+
} catch (err) {
|
|
4613
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4614
|
+
const errMsg = `[dispatch] failed ticket=${ticketId} reason=worktree_unrenderable path="${effectiveConfig.worktreePath}" error="${msg}"`;
|
|
4615
|
+
logger.orchestrator(errMsg);
|
|
4616
|
+
await deps.createComment(
|
|
4617
|
+
ticketId,
|
|
4618
|
+
`[dispatch] failed: cannot create worktree parent directory at "${parent}": ${msg}. Fix the path_pattern or its parent directory; the orchestrator will retry on the next scan.`
|
|
4619
|
+
);
|
|
4620
|
+
return { reason: "error", iterations: 0, gutterCount: 0, lastError: errMsg };
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4486
4623
|
const mem = runMemory;
|
|
4487
4624
|
const colScopeForName = await client.get(
|
|
4488
4625
|
`/projects/${projectId}/pipeline-context`,
|
|
@@ -4499,7 +4636,7 @@ async function runPipeline(client, args) {
|
|
|
4499
4636
|
effectiveConfig.model = registry.resolveModel(columnProvider, effectiveConfig.model);
|
|
4500
4637
|
}
|
|
4501
4638
|
const { gates: columnGates } = resolveGatesForColumn(gateConfig, resolvedColumnName);
|
|
4502
|
-
const gateCwd = effectiveConfig.worktreePath ?? (effectiveConfig.worktreeName ?
|
|
4639
|
+
const gateCwd = effectiveConfig.worktreePath ?? (effectiveConfig.worktreeName ? join4(process.cwd(), effectiveConfig.worktreeName) : process.cwd());
|
|
4503
4640
|
const effectiveMcpConfigPath = columnGates.length > 0 ? generateGateProxyMcpConfig(
|
|
4504
4641
|
client.baseUrl,
|
|
4505
4642
|
client.token,
|
|
@@ -4682,7 +4819,27 @@ async function runPipeline(client, args) {
|
|
|
4682
4819
|
const blockers = await client.get(
|
|
4683
4820
|
`/projects/${projectId}/tickets/${ticketId}/unresolved-blockers`
|
|
4684
4821
|
);
|
|
4685
|
-
|
|
4822
|
+
if (!blockers || blockers.length === 0) {
|
|
4823
|
+
return { blocked: false };
|
|
4824
|
+
}
|
|
4825
|
+
let linkIds = [];
|
|
4826
|
+
try {
|
|
4827
|
+
const links = await client.get(
|
|
4828
|
+
`/projects/${projectId}/ticket-links?ticketId=${ticketId}&linkType=blocks`
|
|
4829
|
+
);
|
|
4830
|
+
linkIds = (links ?? []).filter((l) => l.target_id === ticketId && l.link_type === "blocks").map((l) => l.id);
|
|
4831
|
+
} catch {
|
|
4832
|
+
linkIds = [];
|
|
4833
|
+
}
|
|
4834
|
+
return {
|
|
4835
|
+
blocked: true,
|
|
4836
|
+
blockers: blockers.map((b) => ({
|
|
4837
|
+
id: b.id,
|
|
4838
|
+
ticket_number: b.ticket_number,
|
|
4839
|
+
column: b.column
|
|
4840
|
+
})),
|
|
4841
|
+
linkIds
|
|
4842
|
+
};
|
|
4686
4843
|
},
|
|
4687
4844
|
dispatchLightCall: async (ticketId, columnId) => {
|
|
4688
4845
|
const [ticketCtx, colScope, boardScope] = await Promise.all([
|
|
@@ -4796,6 +4953,7 @@ async function runPipeline(client, args) {
|
|
|
4796
4953
|
appendRunMemory: (section, content) => runMemory ? runMemory.append(section, content) : Promise.resolve(),
|
|
4797
4954
|
cleanupWorktree: (name) => cleanupWorktree(name),
|
|
4798
4955
|
mergeWorktree: (name, integrationBranch) => mergeWorktreeBranch(name, integrationBranch),
|
|
4956
|
+
detectBranchMerged: async (opts2) => detectBranchMerged(opts2),
|
|
4799
4957
|
// Pipeline event emission — wsClient captured by reference (set later before any loops run)
|
|
4800
4958
|
emitPipelineEvent: (event) => {
|
|
4801
4959
|
wsClient?.send(event);
|
|
@@ -4987,7 +5145,7 @@ Received ${signal}. Shutting down gracefully...`);
|
|
|
4987
5145
|
cleanupOrphanedProcesses(opts.boardId);
|
|
4988
5146
|
writePidFile(opts.boardId);
|
|
4989
5147
|
logger.orchestrator(`PID file written: ${String(process.pid)}`);
|
|
4990
|
-
const reaperPidPath =
|
|
5148
|
+
const reaperPidPath = join4(pidDir(opts.boardId), "reaper.pid");
|
|
4991
5149
|
const reaperProcess = spawnReaper({
|
|
4992
5150
|
orchestratorPid: process.pid,
|
|
4993
5151
|
manifestPath: childManifestPath(opts.boardId),
|
|
@@ -5127,7 +5285,7 @@ async function waitForAllLoops(orchestrator, timeoutMs = 4 * 60 * 60 * 1e3) {
|
|
|
5127
5285
|
);
|
|
5128
5286
|
return;
|
|
5129
5287
|
}
|
|
5130
|
-
await new Promise((
|
|
5288
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3));
|
|
5131
5289
|
if (activeRalphLoops.size === 0 && orchestrator.activeLoopCount === 0 && !orchestrator.hasActiveQueuedWork && !orchestrator.hasCompletingWork) {
|
|
5132
5290
|
consecutiveIdle++;
|
|
5133
5291
|
} else {
|
|
@@ -5177,4 +5335,4 @@ export {
|
|
|
5177
5335
|
runPipeline,
|
|
5178
5336
|
stopPipeline
|
|
5179
5337
|
};
|
|
5180
|
-
//# sourceMappingURL=pipeline-
|
|
5338
|
+
//# sourceMappingURL=pipeline-6J64Z7VH.js.map
|