kantban-cli 0.1.47 → 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-VDCHOIQI.js → chunk-4R27WTCJ.js} +22 -15
- package/dist/chunk-4R27WTCJ.js.map +1 -0
- package/dist/{chunk-O47Q6CNM.js → chunk-GQQUH3TA.js} +15 -4
- package/dist/chunk-GQQUH3TA.js.map +1 -0
- package/dist/{chunk-UKWQ6VUQ.js → chunk-X2CJ3ZAI.js} +150 -8
- package/dist/chunk-X2CJ3ZAI.js.map +1 -0
- package/dist/{cron-TLNKEKOW.js → cron-OCKARAAM.js} +3 -3
- package/dist/index.js +3 -3
- package/dist/lib/gate-proxy-server.js +38 -18
- package/dist/lib/gate-proxy-server.js.map +1 -1
- package/dist/{pipeline-EUOOLKHN.js → pipeline-6J64Z7VH.js} +479 -142
- package/dist/pipeline-6J64Z7VH.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-O47Q6CNM.js.map +0 -1
- package/dist/chunk-UKWQ6VUQ.js.map +0 -1
- package/dist/chunk-VDCHOIQI.js.map +0 -1
- package/dist/pipeline-EUOOLKHN.js.map +0 -1
- /package/dist/{cron-TLNKEKOW.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,11 +8,13 @@ import {
|
|
|
8
8
|
cleanupGateProxyConfigs,
|
|
9
9
|
cleanupMcpConfig,
|
|
10
10
|
composeStuckDetectionPrompt,
|
|
11
|
+
detectBranchMerged,
|
|
11
12
|
generateGateProxyMcpConfig,
|
|
12
13
|
generateMcpConfig,
|
|
13
14
|
parseJsonFromLlmOutput,
|
|
14
|
-
parseStuckDetectionResponse
|
|
15
|
-
|
|
15
|
+
parseStuckDetectionResponse,
|
|
16
|
+
reapOrphanedMcpConfigDirs
|
|
17
|
+
} from "./chunk-X2CJ3ZAI.js";
|
|
16
18
|
import {
|
|
17
19
|
LoopCheckpointSchema,
|
|
18
20
|
VerdictSchema,
|
|
@@ -20,7 +22,7 @@ import {
|
|
|
20
22
|
parseGateConfig,
|
|
21
23
|
parseTimeout,
|
|
22
24
|
resolveGatesForColumn
|
|
23
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-4R27WTCJ.js";
|
|
24
26
|
import {
|
|
25
27
|
IS_WINDOWS,
|
|
26
28
|
crossSpawnOptions,
|
|
@@ -30,9 +32,17 @@ import {
|
|
|
30
32
|
} from "./chunk-5ZU2OOES.js";
|
|
31
33
|
|
|
32
34
|
// src/commands/pipeline.ts
|
|
33
|
-
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync3, unlinkSync as unlinkSync3, existsSync as existsSync3, appendFileSync as appendFileSync2 } from "fs";
|
|
34
|
-
import {
|
|
35
|
-
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";
|
|
40
|
+
|
|
41
|
+
// src/lib/harness-signal.ts
|
|
42
|
+
var HARNESS_SIGNAL_PREFIX = "@harness:";
|
|
43
|
+
function createHarnessSignalContent(kind, payload) {
|
|
44
|
+
return HARNESS_SIGNAL_PREFIX + JSON.stringify({ ...payload, kind });
|
|
45
|
+
}
|
|
36
46
|
|
|
37
47
|
// src/lib/tool-profiles.ts
|
|
38
48
|
function resolveToolRestrictions(builtinTools, allowedTools, disallowedTools) {
|
|
@@ -50,6 +60,8 @@ function resolveToolRestrictions(builtinTools, allowedTools, disallowedTools) {
|
|
|
50
60
|
|
|
51
61
|
// src/lib/worktree.ts
|
|
52
62
|
import { execFile as defaultExecFile, execFileSync } from "child_process";
|
|
63
|
+
import { homedir } from "os";
|
|
64
|
+
import { join } from "path";
|
|
53
65
|
function generateWorktreeName(ticketNumber, columnName) {
|
|
54
66
|
const slug = columnSlug(columnName);
|
|
55
67
|
return `kantban-${ticketNumber}-${slug}`;
|
|
@@ -57,10 +69,16 @@ function generateWorktreeName(ticketNumber, columnName) {
|
|
|
57
69
|
function columnSlug(columnName) {
|
|
58
70
|
return columnName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
59
71
|
}
|
|
60
|
-
function renderWorktreePath(ticketNumber, columnName, pathPattern) {
|
|
72
|
+
function renderWorktreePath(ticketNumber, columnName, pathPattern, options) {
|
|
73
|
+
const slug = columnSlug(columnName);
|
|
61
74
|
const worktreeName = generateWorktreeName(ticketNumber, columnName);
|
|
62
|
-
if (
|
|
63
|
-
|
|
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");
|
|
64
82
|
}
|
|
65
83
|
function isPlausibleRemoteUrl(url) {
|
|
66
84
|
return url.includes("/") || url.includes("@") || url.includes("://");
|
|
@@ -108,22 +126,22 @@ async function cleanupWorktree(worktreeName, exec = defaultExecFile) {
|
|
|
108
126
|
} catch {
|
|
109
127
|
return true;
|
|
110
128
|
}
|
|
111
|
-
return new Promise((
|
|
129
|
+
return new Promise((resolve2) => {
|
|
112
130
|
exec("git", ["worktree", "remove", "--force", target], (err) => {
|
|
113
131
|
if (err) {
|
|
114
132
|
console.error(`[worktree] cleanup failed for ${worktreeName} (${target}): ${err.message}`);
|
|
115
|
-
|
|
133
|
+
resolve2(false);
|
|
116
134
|
} else {
|
|
117
|
-
|
|
135
|
+
resolve2(true);
|
|
118
136
|
}
|
|
119
137
|
});
|
|
120
138
|
});
|
|
121
139
|
}
|
|
122
140
|
function execPromise(exec, cmd, args) {
|
|
123
|
-
return new Promise((
|
|
141
|
+
return new Promise((resolve2, reject) => {
|
|
124
142
|
exec(cmd, args, (err, stdout, stderr) => {
|
|
125
143
|
if (err) reject(Object.assign(err, { stdout, stderr }));
|
|
126
|
-
else
|
|
144
|
+
else resolve2({ stdout, stderr });
|
|
127
145
|
});
|
|
128
146
|
});
|
|
129
147
|
}
|
|
@@ -470,7 +488,9 @@ function parseReplannerResponse(raw) {
|
|
|
470
488
|
}
|
|
471
489
|
|
|
472
490
|
// src/lib/orchestrator.ts
|
|
473
|
-
var ACTIVE_LOOP_ZOMBIE_TTL_MS =
|
|
491
|
+
var ACTIVE_LOOP_ZOMBIE_TTL_MS = Number(
|
|
492
|
+
process.env.ACTIVE_LOOP_ZOMBIE_TTL_MS
|
|
493
|
+
) || 3 * 60 * 60 * 1e3;
|
|
474
494
|
function classifyTier(input) {
|
|
475
495
|
if (input.invocationTier === "light") return "light";
|
|
476
496
|
if (input.invocationTier === "heavy") return "heavy";
|
|
@@ -510,6 +530,12 @@ var PipelineOrchestrator = class {
|
|
|
510
530
|
blockedColumns = /* @__PURE__ */ new Set();
|
|
511
531
|
/** Per-ticket advisor invocation count for the current column transit */
|
|
512
532
|
advisorBudget = /* @__PURE__ */ new Map();
|
|
533
|
+
/** Count of advisor-driven retries per ticket in the current column transit.
|
|
534
|
+
* Cleared on ticket:moved/archived/deleted and in terminalCleanup. */
|
|
535
|
+
gutterResetCount = /* @__PURE__ */ new Map();
|
|
536
|
+
gutterResetExhausted = /* @__PURE__ */ new Set();
|
|
537
|
+
/** Per-ticket deferred-dispatch signal state — used for dedup and cleanup */
|
|
538
|
+
deferredState = /* @__PURE__ */ new Map();
|
|
513
539
|
/** Per-ticket model override (set by RETRY_DIFFERENT_MODEL, consumed by startTrackedLoop) */
|
|
514
540
|
ticketModelOverrides = /* @__PURE__ */ new Map();
|
|
515
541
|
/** Stable session ID for this orchestrator instance (pipeline run) */
|
|
@@ -555,6 +581,10 @@ var PipelineOrchestrator = class {
|
|
|
555
581
|
get allBoardColumnNames() {
|
|
556
582
|
return this.cachedBoardScope?.columns.map((c) => c.name) ?? [];
|
|
557
583
|
}
|
|
584
|
+
/** Returns {name, type} for every column on the board. */
|
|
585
|
+
get allBoardColumns() {
|
|
586
|
+
return this.cachedBoardScope?.columns.map((c) => ({ name: c.name, type: c.type })) ?? [];
|
|
587
|
+
}
|
|
558
588
|
/** Returns the total number of active loops. */
|
|
559
589
|
get activeLoopCount() {
|
|
560
590
|
return this.activeLoops.size;
|
|
@@ -633,12 +663,16 @@ var PipelineOrchestrator = class {
|
|
|
633
663
|
} : void 0,
|
|
634
664
|
builtinTools: cfg?.builtin_tools,
|
|
635
665
|
allowedTools: cfg?.allowed_tools,
|
|
636
|
-
disallowedTools: cfg?.disallowed_tools
|
|
666
|
+
disallowedTools: cfg?.disallowed_tools,
|
|
667
|
+
maxGutterResetsPerTransit: cfg?.max_gutter_resets_per_transit,
|
|
668
|
+
repromptOnBranchMerged: cfg?.reprompt_on_branch_merged,
|
|
669
|
+
maxRepromptAttempts: cfg?.max_reprompt_attempts
|
|
637
670
|
});
|
|
638
671
|
this.columnScopes.set(col.id, colScope);
|
|
639
672
|
this.loopQueues.set(col.id, []);
|
|
640
673
|
})
|
|
641
674
|
);
|
|
675
|
+
console.error(`[orchestrator] zombie TTL: ${ACTIVE_LOOP_ZOMBIE_TTL_MS}ms`);
|
|
642
676
|
}
|
|
643
677
|
/**
|
|
644
678
|
* Refresh the cached column scope for a single column.
|
|
@@ -919,8 +953,8 @@ var PipelineOrchestrator = class {
|
|
|
919
953
|
}
|
|
920
954
|
for (const [ticketId, columnId] of Array.from(this.deferredTickets)) {
|
|
921
955
|
try {
|
|
922
|
-
const
|
|
923
|
-
if (!
|
|
956
|
+
const blocker = await this.deps.hasUnresolvedBlockers(ticketId);
|
|
957
|
+
if (!blocker.blocked) {
|
|
924
958
|
this.deferredTickets.delete(ticketId);
|
|
925
959
|
await this.spawnOrQueue(ticketId, columnId, true);
|
|
926
960
|
} else {
|
|
@@ -934,23 +968,56 @@ var PipelineOrchestrator = class {
|
|
|
934
968
|
}
|
|
935
969
|
await this.refreshBoardScope();
|
|
936
970
|
this.blockedColumns.clear();
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
this.
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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();
|
|
993
|
+
for (const ticket of colScope.tickets) {
|
|
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
|
+
}
|
|
1014
|
+
}
|
|
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
|
+
);
|
|
954
1021
|
}
|
|
955
1022
|
/**
|
|
956
1023
|
* Handle a pipeline event (typically from WebSocket via EventQueue).
|
|
@@ -970,6 +1037,7 @@ var PipelineOrchestrator = class {
|
|
|
970
1037
|
this.knownTickets.delete(event.ticketId);
|
|
971
1038
|
this.spawning.delete(event.ticketId);
|
|
972
1039
|
this.deferredTickets.delete(event.ticketId);
|
|
1040
|
+
void this.clearDispatchDeferred(event.ticketId);
|
|
973
1041
|
const oldQueue = this.loopQueues.get(event.fromColumnId);
|
|
974
1042
|
if (oldQueue) {
|
|
975
1043
|
const idx = oldQueue.indexOf(event.ticketId);
|
|
@@ -984,6 +1052,7 @@ var PipelineOrchestrator = class {
|
|
|
984
1052
|
if (this.isColumnBlocked(event.columnId)) {
|
|
985
1053
|
console.error(` [event] ${event.type} ${event.ticketId} \u2192 column ${event.columnId}: BLOCKED by firing constraints \u2014 deferred`);
|
|
986
1054
|
this.deferredTickets.set(event.ticketId, event.columnId);
|
|
1055
|
+
await this.emitDispatchDeferred(event.ticketId, "firing_constraint", { columnId: event.columnId });
|
|
987
1056
|
} else {
|
|
988
1057
|
await this.spawnOrQueue(event.ticketId, event.columnId, true);
|
|
989
1058
|
}
|
|
@@ -1033,6 +1102,9 @@ var PipelineOrchestrator = class {
|
|
|
1033
1102
|
this.deferredTickets.delete(event.ticketId);
|
|
1034
1103
|
this.spawning.delete(event.ticketId);
|
|
1035
1104
|
this.advisorBudget.delete(event.ticketId);
|
|
1105
|
+
this.gutterResetCount.delete(event.ticketId);
|
|
1106
|
+
this.gutterResetExhausted.delete(event.ticketId);
|
|
1107
|
+
void this.clearDispatchDeferred(event.ticketId);
|
|
1036
1108
|
for (const [, queue] of this.loopQueues) {
|
|
1037
1109
|
const idx = queue.indexOf(event.ticketId);
|
|
1038
1110
|
if (idx !== -1) {
|
|
@@ -1067,15 +1139,59 @@ var PipelineOrchestrator = class {
|
|
|
1067
1139
|
}
|
|
1068
1140
|
return;
|
|
1069
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
|
+
}
|
|
1070
1181
|
this.spawning.add(ticketId);
|
|
1071
1182
|
this.reserveSlot(columnId);
|
|
1072
1183
|
try {
|
|
1073
|
-
const
|
|
1074
|
-
if (blocked) {
|
|
1184
|
+
const result = await this.deps.hasUnresolvedBlockers(ticketId);
|
|
1185
|
+
if (result.blocked) {
|
|
1075
1186
|
this.spawning.delete(ticketId);
|
|
1076
1187
|
this.releaseSlot(columnId);
|
|
1077
1188
|
this.deferredTickets.set(ticketId, columnId);
|
|
1078
|
-
|
|
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
|
+
);
|
|
1194
|
+
void this.emitDispatchDeferred(ticketId, "unresolved_blockers", { columnId });
|
|
1079
1195
|
void this.drainQueue(columnId).catch((err) => {
|
|
1080
1196
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1081
1197
|
console.error(` [error] drainQueue failed for column ${columnId}: ${msg}`);
|
|
@@ -1088,14 +1204,16 @@ var PipelineOrchestrator = class {
|
|
|
1088
1204
|
this.spawning.delete(ticketId);
|
|
1089
1205
|
this.releaseSlot(columnId);
|
|
1090
1206
|
this.deferredTickets.set(ticketId, columnId);
|
|
1091
|
-
void this.
|
|
1092
|
-
|
|
1093
|
-
|
|
1207
|
+
void this.emitDispatchDeferred(ticketId, "unresolved_blockers", { 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}`);
|
|
1094
1211
|
});
|
|
1095
1212
|
return;
|
|
1096
1213
|
}
|
|
1097
1214
|
try {
|
|
1098
1215
|
await this.deps.claimTicket(ticketId);
|
|
1216
|
+
await this.clearDispatchDeferred(ticketId);
|
|
1099
1217
|
this.startTrackedLoop(ticketId, columnId, colConfig);
|
|
1100
1218
|
} catch (err) {
|
|
1101
1219
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1237,16 +1355,16 @@ var PipelineOrchestrator = class {
|
|
|
1237
1355
|
gutterThreshold: config.gutterThreshold,
|
|
1238
1356
|
...effectiveModel !== void 0 && { model: effectiveModel },
|
|
1239
1357
|
...config.maxBudgetUsd !== void 0 && { maxBudgetUsd: config.maxBudgetUsd },
|
|
1240
|
-
// Resolve worktree name (branch) and path from ticket context. When the
|
|
1241
|
-
// column's agent_config specifies a path_pattern, the filesystem path is
|
|
1242
|
-
// rendered from it — otherwise the name doubles as a relative path.
|
|
1243
1358
|
...(() => {
|
|
1244
1359
|
const colScope = this.columnScopes.get(columnId);
|
|
1245
1360
|
const ticket = colScope?.tickets.find((t) => t.id === ticketId);
|
|
1246
1361
|
if (!config.worktreeEnabled || !ticket) return {};
|
|
1247
1362
|
const wName = generateWorktreeName(ticket.ticket_number, config.name);
|
|
1248
|
-
const wPath = renderWorktreePath(ticket.ticket_number, config.name, config.worktreePathPattern
|
|
1249
|
-
|
|
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 };
|
|
1250
1368
|
})(),
|
|
1251
1369
|
...config.lookaheadColumnId !== void 0 && { lookaheadColumnId: config.lookaheadColumnId },
|
|
1252
1370
|
// Resume from checkpoint iteration/gutter if provided
|
|
@@ -1254,6 +1372,8 @@ var PipelineOrchestrator = class {
|
|
|
1254
1372
|
...startGutterCount !== void 0 && { startGutterCount },
|
|
1255
1373
|
...startFingerprint !== void 0 && { startFingerprint },
|
|
1256
1374
|
...config.stuckDetection && { stuckDetection: config.stuckDetection },
|
|
1375
|
+
...config.repromptOnBranchMerged !== void 0 && { repromptOnBranchMerged: config.repromptOnBranchMerged },
|
|
1376
|
+
...config.maxRepromptAttempts !== void 0 && { maxRepromptAttempts: config.maxRepromptAttempts },
|
|
1257
1377
|
...this.deps.costTracker && { isBudgetExhausted: () => this.deps.costTracker.isExhausted() }
|
|
1258
1378
|
};
|
|
1259
1379
|
const toolRestrictions = resolveToolRestrictions(
|
|
@@ -1283,6 +1403,33 @@ var PipelineOrchestrator = class {
|
|
|
1283
1403
|
})
|
|
1284
1404
|
);
|
|
1285
1405
|
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Increment the gutter-reset counter and check the per-column cap.
|
|
1408
|
+
* Returns false if the cap has been exceeded (and emits signal + comment).
|
|
1409
|
+
* Callers should abort the retry on false.
|
|
1410
|
+
*/
|
|
1411
|
+
async checkGutterResetCap(ticketId, colConfig) {
|
|
1412
|
+
const cap = colConfig.maxGutterResetsPerTransit ?? 2;
|
|
1413
|
+
const count = (this.gutterResetCount.get(ticketId) ?? 0) + 1;
|
|
1414
|
+
this.gutterResetCount.set(ticketId, count);
|
|
1415
|
+
if (count > cap) {
|
|
1416
|
+
this.gutterResetExhausted.add(ticketId);
|
|
1417
|
+
await this.emitHarnessSignal(ticketId, "agent_stalled_exhausted", {
|
|
1418
|
+
resetCount: count,
|
|
1419
|
+
cap
|
|
1420
|
+
});
|
|
1421
|
+
await this.safeAction(
|
|
1422
|
+
ticketId,
|
|
1423
|
+
"stalled:exhaustedComment",
|
|
1424
|
+
() => this.deps.createComment(
|
|
1425
|
+
ticketId,
|
|
1426
|
+
`Stalled after ${String(count)} gutter reset attempt(s) (cap: ${String(cap)}) \u2014 human review needed. See 'agent_stalled_exhausted' signal for details.`
|
|
1427
|
+
)
|
|
1428
|
+
);
|
|
1429
|
+
return false;
|
|
1430
|
+
}
|
|
1431
|
+
return true;
|
|
1432
|
+
}
|
|
1286
1433
|
/**
|
|
1287
1434
|
* Phase 2: Invoke the advisor for failure recovery.
|
|
1288
1435
|
* Returns true if the ticket should be retried, false if handled.
|
|
@@ -1386,6 +1533,10 @@ var PipelineOrchestrator = class {
|
|
|
1386
1533
|
}
|
|
1387
1534
|
switch (response.action) {
|
|
1388
1535
|
case "RETRY_WITH_FEEDBACK": {
|
|
1536
|
+
if (!await this.checkGutterResetCap(ticketId, colConfig)) {
|
|
1537
|
+
this.completing.delete(ticketId);
|
|
1538
|
+
return false;
|
|
1539
|
+
}
|
|
1389
1540
|
if (response.feedback) {
|
|
1390
1541
|
await this.safeAction(
|
|
1391
1542
|
ticketId,
|
|
@@ -1400,6 +1551,10 @@ ${response.feedback}`)
|
|
|
1400
1551
|
return true;
|
|
1401
1552
|
}
|
|
1402
1553
|
case "RETRY_DIFFERENT_MODEL": {
|
|
1554
|
+
if (!await this.checkGutterResetCap(ticketId, colConfig)) {
|
|
1555
|
+
this.completing.delete(ticketId);
|
|
1556
|
+
return false;
|
|
1557
|
+
}
|
|
1403
1558
|
const currentModel = result.model ?? colConfig.modelPreference ?? "default";
|
|
1404
1559
|
const escalation = colConfig.modelRouting?.escalation ?? [];
|
|
1405
1560
|
let nextModel;
|
|
@@ -1558,10 +1713,17 @@ ${response.feedback}`)
|
|
|
1558
1713
|
);
|
|
1559
1714
|
}
|
|
1560
1715
|
}
|
|
1716
|
+
await this.emitHarnessSignal(ticketId, "advisor_action", {
|
|
1717
|
+
action: "ESCALATE",
|
|
1718
|
+
reason: response.reason
|
|
1719
|
+
});
|
|
1561
1720
|
await this.safeAction(
|
|
1562
1721
|
ticketId,
|
|
1563
|
-
"advisor:
|
|
1564
|
-
() => this.deps.createComment(
|
|
1722
|
+
"advisor:escalateComment",
|
|
1723
|
+
() => this.deps.createComment(
|
|
1724
|
+
ticketId,
|
|
1725
|
+
`Needs human review \u2014 see 'advisor_action' signal for details.`
|
|
1726
|
+
)
|
|
1565
1727
|
);
|
|
1566
1728
|
return false;
|
|
1567
1729
|
}
|
|
@@ -1721,6 +1883,19 @@ ${findingsText}`)
|
|
|
1721
1883
|
const nextBoardCol = bs.columns.filter((c) => c.position > currentBoardCol.position && this.pipelineColumns.has(c.id)).sort((a, b) => a.position - b.position)[0];
|
|
1722
1884
|
return nextBoardCol ? this.pipelineColumns.get(nextBoardCol.id) ?? null : null;
|
|
1723
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
|
+
}
|
|
1724
1899
|
/**
|
|
1725
1900
|
* Called when a loop finishes. Cleans up tracking and drains the queue.
|
|
1726
1901
|
*/
|
|
@@ -1817,6 +1992,8 @@ ${findingsText}`)
|
|
|
1817
1992
|
const colConfig = this.pipelineColumns.get(columnId);
|
|
1818
1993
|
const terminalCleanup = async () => {
|
|
1819
1994
|
this.advisorBudget.delete(ticketId);
|
|
1995
|
+
this.gutterResetCount.delete(ticketId);
|
|
1996
|
+
this.gutterResetExhausted.delete(ticketId);
|
|
1820
1997
|
if (colConfig?.checkpointEnabled && this.deps.setFieldValue && result.reason !== "stopped") {
|
|
1821
1998
|
await this.safeAction(
|
|
1822
1999
|
ticketId,
|
|
@@ -1950,47 +2127,41 @@ ${findingsText}`)
|
|
|
1950
2127
|
}
|
|
1951
2128
|
break;
|
|
1952
2129
|
case "max_iterations":
|
|
1953
|
-
|
|
2130
|
+
await this.emitHarnessSignal(ticketId, "agent_max_iterations", {
|
|
2131
|
+
iterations: result.iterations
|
|
2132
|
+
});
|
|
2133
|
+
comment = `Needs human review \u2014 see 'agent_max_iterations' signal for details.`;
|
|
1954
2134
|
break;
|
|
1955
2135
|
case "stalled":
|
|
1956
|
-
|
|
2136
|
+
if (!this.gutterResetExhausted.has(ticketId)) {
|
|
2137
|
+
await this.emitHarnessSignal(ticketId, "agent_stalled", {
|
|
2138
|
+
iterations: result.iterations,
|
|
2139
|
+
gutterCount: result.gutterCount
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
1957
2142
|
break;
|
|
1958
2143
|
case "error":
|
|
1959
|
-
|
|
2144
|
+
await this.emitHarnessSignal(ticketId, "agent_error", {
|
|
2145
|
+
iterations: result.iterations,
|
|
2146
|
+
lastError: result.lastError ?? "unknown error"
|
|
2147
|
+
});
|
|
2148
|
+
comment = `Needs human review \u2014 see 'agent_error' signal for details.`;
|
|
1960
2149
|
break;
|
|
1961
2150
|
case "stopped":
|
|
1962
2151
|
comment = `Pipeline agent was stopped externally after ${result.iterations} iteration(s).`;
|
|
1963
2152
|
break;
|
|
1964
2153
|
case "deleted":
|
|
1965
|
-
comment = `Pipeline agent stopped \u2014 ticket was deleted or archived during iteration ${result.iterations}.`;
|
|
1966
2154
|
break;
|
|
1967
2155
|
case "budget":
|
|
1968
2156
|
comment = `Pipeline budget exhausted after ${result.iterations} iteration(s). Increase token budget in pipeline.gates.yaml settings.budget to continue.`;
|
|
1969
2157
|
break;
|
|
1970
2158
|
}
|
|
1971
|
-
if (
|
|
2159
|
+
if (comment !== void 0) {
|
|
1972
2160
|
await this.deps.createComment(ticketId, comment).catch((err) => {
|
|
1973
2161
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1974
2162
|
console.error(` [warn] Failed to write completion comment for ${ticketId}: ${msg}`);
|
|
1975
2163
|
});
|
|
1976
2164
|
}
|
|
1977
|
-
if (result.reason === "stalled") {
|
|
1978
|
-
await this.deps.createSignal(
|
|
1979
|
-
ticketId,
|
|
1980
|
-
`Previous pipeline run stalled after ${result.iterations} iterations with no progress. Review comments for details before retrying.`
|
|
1981
|
-
).catch((err) => {
|
|
1982
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1983
|
-
console.error(` [warn] Failed to write signal for ${ticketId}: ${msg}`);
|
|
1984
|
-
});
|
|
1985
|
-
} else if (result.reason === "error") {
|
|
1986
|
-
await this.deps.createSignal(
|
|
1987
|
-
ticketId,
|
|
1988
|
-
`Previous pipeline run failed after ${result.iterations} iteration(s): ${result.lastError ?? "unknown error"}. Review comments for details before retrying.`
|
|
1989
|
-
).catch((err) => {
|
|
1990
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1991
|
-
console.error(` [warn] Failed to write signal for ${ticketId}: ${msg}`);
|
|
1992
|
-
});
|
|
1993
|
-
}
|
|
1994
2165
|
await terminalCleanup();
|
|
1995
2166
|
} finally {
|
|
1996
2167
|
this.completing.delete(ticketId);
|
|
@@ -2112,11 +2283,12 @@ ${findingsText}`)
|
|
|
2112
2283
|
this.spawning.add(nextTicketId);
|
|
2113
2284
|
this.reserveSlot(columnId);
|
|
2114
2285
|
try {
|
|
2115
|
-
const
|
|
2116
|
-
if (blocked) {
|
|
2286
|
+
const blockerResult = await this.deps.hasUnresolvedBlockers(nextTicketId);
|
|
2287
|
+
if (blockerResult.blocked) {
|
|
2117
2288
|
this.deferredTickets.set(nextTicketId, columnId);
|
|
2118
2289
|
this.spawning.delete(nextTicketId);
|
|
2119
2290
|
this.releaseSlot(columnId);
|
|
2291
|
+
void this.emitDispatchDeferred(nextTicketId, "unresolved_blockers", { columnId });
|
|
2120
2292
|
continue;
|
|
2121
2293
|
}
|
|
2122
2294
|
} catch (err) {
|
|
@@ -2125,10 +2297,12 @@ ${findingsText}`)
|
|
|
2125
2297
|
this.deferredTickets.set(nextTicketId, columnId);
|
|
2126
2298
|
this.spawning.delete(nextTicketId);
|
|
2127
2299
|
this.releaseSlot(columnId);
|
|
2300
|
+
void this.emitDispatchDeferred(nextTicketId, "unresolved_blockers", { columnId });
|
|
2128
2301
|
continue;
|
|
2129
2302
|
}
|
|
2130
2303
|
try {
|
|
2131
2304
|
await this.deps.claimTicket(nextTicketId);
|
|
2305
|
+
await this.clearDispatchDeferred(nextTicketId);
|
|
2132
2306
|
this.startTrackedLoop(nextTicketId, columnId, colConfig);
|
|
2133
2307
|
} catch (err) {
|
|
2134
2308
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -2157,6 +2331,63 @@ ${findingsText}`)
|
|
|
2157
2331
|
const current = this.columnReservations.get(columnId) ?? 0;
|
|
2158
2332
|
if (current > 0) this.columnReservations.set(columnId, current - 1);
|
|
2159
2333
|
}
|
|
2334
|
+
/**
|
|
2335
|
+
* Emit a dispatch_deferred harness signal for a ticket.
|
|
2336
|
+
* State-change dedup: same reason → no-op; different reason → delete old + create new.
|
|
2337
|
+
* Best-effort: signal creation failures are logged but never thrown.
|
|
2338
|
+
*/
|
|
2339
|
+
/**
|
|
2340
|
+
* Emit a structured harness signal on a ticket. Uses createSignal (plain, non-tracked).
|
|
2341
|
+
* Best-effort: failures are logged but never thrown.
|
|
2342
|
+
*/
|
|
2343
|
+
async emitHarnessSignal(ticketId, kind, payload) {
|
|
2344
|
+
const content = createHarnessSignalContent(kind, payload);
|
|
2345
|
+
try {
|
|
2346
|
+
await this.deps.createSignal(ticketId, content);
|
|
2347
|
+
} catch (err) {
|
|
2348
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2349
|
+
console.error(`[harness-signal] failed to create ${kind} signal for ${ticketId}: ${msg}`);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
async emitDispatchDeferred(ticketId, reason, extras) {
|
|
2353
|
+
if (!this.deps.createHarnessSignal) return;
|
|
2354
|
+
const existing = this.deferredState.get(ticketId);
|
|
2355
|
+
if (existing?.reason === reason) return;
|
|
2356
|
+
if (existing && this.deps.deleteSignal) {
|
|
2357
|
+
try {
|
|
2358
|
+
await this.deps.deleteSignal(existing.signalId);
|
|
2359
|
+
} catch {
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
const firstDeferredAt = existing?.firstDeferredAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2363
|
+
const content = createHarnessSignalContent("dispatch_deferred", {
|
|
2364
|
+
reason,
|
|
2365
|
+
firstDeferredAt,
|
|
2366
|
+
...extras
|
|
2367
|
+
});
|
|
2368
|
+
try {
|
|
2369
|
+
const signal = await this.deps.createHarnessSignal(ticketId, content);
|
|
2370
|
+
this.deferredState.set(ticketId, { reason, signalId: signal.id, firstDeferredAt });
|
|
2371
|
+
} catch (err) {
|
|
2372
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2373
|
+
console.error(` [dispatch-deferred] failed to create signal for ticket ${ticketId}: ${msg}`);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
/**
|
|
2377
|
+
* Clear the dispatch_deferred harness signal for a ticket (called on dispatch success or ticket:moved).
|
|
2378
|
+
* Best-effort: deletion failures are swallowed.
|
|
2379
|
+
*/
|
|
2380
|
+
async clearDispatchDeferred(ticketId) {
|
|
2381
|
+
const existing = this.deferredState.get(ticketId);
|
|
2382
|
+
if (!existing) return;
|
|
2383
|
+
this.deferredState.delete(ticketId);
|
|
2384
|
+
if (this.deps.deleteSignal) {
|
|
2385
|
+
try {
|
|
2386
|
+
await this.deps.deleteSignal(existing.signalId);
|
|
2387
|
+
} catch {
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2160
2391
|
};
|
|
2161
2392
|
|
|
2162
2393
|
// src/lib/run-memory.ts
|
|
@@ -2408,7 +2639,7 @@ var PipelineWsClient = class _PipelineWsClient {
|
|
|
2408
2639
|
this.cleanupTimers();
|
|
2409
2640
|
const { ticket } = await this.client.post("/ws-ticket");
|
|
2410
2641
|
const wsUrl = this.client.baseUrl.replace(/^http/, "ws") + `/ws?ticket=${ticket}&clientType=cli`;
|
|
2411
|
-
return new Promise((
|
|
2642
|
+
return new Promise((resolve2, reject) => {
|
|
2412
2643
|
let resolved = false;
|
|
2413
2644
|
this.ws = new WebSocket(wsUrl);
|
|
2414
2645
|
let subscribed = false;
|
|
@@ -2437,7 +2668,7 @@ var PipelineWsClient = class _PipelineWsClient {
|
|
|
2437
2668
|
this.options.onConnect();
|
|
2438
2669
|
if (!resolved) {
|
|
2439
2670
|
resolved = true;
|
|
2440
|
-
|
|
2671
|
+
resolve2();
|
|
2441
2672
|
}
|
|
2442
2673
|
}
|
|
2443
2674
|
this.options.onEvent(event);
|
|
@@ -2549,7 +2780,7 @@ var PipelineWsClient = class _PipelineWsClient {
|
|
|
2549
2780
|
|
|
2550
2781
|
// src/lib/logger.ts
|
|
2551
2782
|
import { mkdirSync, writeFileSync, appendFileSync, readdirSync, rmSync, statSync, renameSync } from "fs";
|
|
2552
|
-
import { join } from "path";
|
|
2783
|
+
import { join as join2 } from "path";
|
|
2553
2784
|
var MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
2554
2785
|
var MAX_ROTATED_FILES = 3;
|
|
2555
2786
|
var ROTATION_CHECK_INTERVAL = 100;
|
|
@@ -2557,11 +2788,11 @@ var PipelineLogger = class {
|
|
|
2557
2788
|
boardDir;
|
|
2558
2789
|
writeCount = 0;
|
|
2559
2790
|
constructor(baseDir, boardId) {
|
|
2560
|
-
this.boardDir =
|
|
2791
|
+
this.boardDir = join2(baseDir, boardId);
|
|
2561
2792
|
mkdirSync(this.boardDir, { recursive: true });
|
|
2562
2793
|
}
|
|
2563
2794
|
orchestrator(message) {
|
|
2564
|
-
const logPath =
|
|
2795
|
+
const logPath = join2(this.boardDir, "orchestrator.log");
|
|
2565
2796
|
const entry = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
|
|
2566
2797
|
`;
|
|
2567
2798
|
appendFileSync(logPath, entry);
|
|
@@ -2593,10 +2824,10 @@ var PipelineLogger = class {
|
|
|
2593
2824
|
}
|
|
2594
2825
|
}
|
|
2595
2826
|
iteration(ticketNumber, iterationNum, data) {
|
|
2596
|
-
const ticketDir =
|
|
2827
|
+
const ticketDir = join2(this.boardDir, ticketNumber);
|
|
2597
2828
|
mkdirSync(ticketDir, { recursive: true });
|
|
2598
2829
|
const padded = String(iterationNum).padStart(3, "0");
|
|
2599
|
-
const logPath =
|
|
2830
|
+
const logPath = join2(ticketDir, `iteration-${padded}.log`);
|
|
2600
2831
|
const entry = {
|
|
2601
2832
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2602
2833
|
iteration: iterationNum,
|
|
@@ -2615,7 +2846,7 @@ var PipelineLogger = class {
|
|
|
2615
2846
|
try {
|
|
2616
2847
|
const entries = readdirSync(this.boardDir, { withFileTypes: true });
|
|
2617
2848
|
for (const entry of entries) {
|
|
2618
|
-
const fullPath =
|
|
2849
|
+
const fullPath = join2(this.boardDir, entry.name);
|
|
2619
2850
|
if (entry.isDirectory() && entry.name !== ".") {
|
|
2620
2851
|
try {
|
|
2621
2852
|
const stat = statSync(fullPath);
|
|
@@ -3401,8 +3632,9 @@ var CodexProvider = class {
|
|
|
3401
3632
|
execFileSync2("git", ["worktree", "add", request.workingDirectory, branch], {
|
|
3402
3633
|
stdio: "pipe"
|
|
3403
3634
|
});
|
|
3404
|
-
} catch {
|
|
3405
|
-
|
|
3635
|
+
} catch (err) {
|
|
3636
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3637
|
+
throw new Error(`worktree_creation_failed: ${msg}`);
|
|
3406
3638
|
}
|
|
3407
3639
|
}
|
|
3408
3640
|
} else {
|
|
@@ -3421,17 +3653,18 @@ var CodexProvider = class {
|
|
|
3421
3653
|
execFileSync2("git", ["worktree", "add", request.workingDirectory, branch], {
|
|
3422
3654
|
stdio: "pipe"
|
|
3423
3655
|
});
|
|
3424
|
-
} catch {
|
|
3425
|
-
|
|
3656
|
+
} catch (err) {
|
|
3657
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3658
|
+
throw new Error(`worktree_creation_failed: ${msg}`);
|
|
3426
3659
|
}
|
|
3427
3660
|
}
|
|
3428
3661
|
}
|
|
3429
3662
|
}
|
|
3430
|
-
if (
|
|
3663
|
+
if (existsSync(request.workingDirectory)) {
|
|
3431
3664
|
ensureWorktreeRemote(request.workingDirectory);
|
|
3432
3665
|
}
|
|
3433
3666
|
}
|
|
3434
|
-
return new Promise((
|
|
3667
|
+
return new Promise((resolve2) => {
|
|
3435
3668
|
const [cmd, prefixArgs] = resolveCommand("codex");
|
|
3436
3669
|
const resolvedArgs = [...prefixArgs, ...args];
|
|
3437
3670
|
const child = spawn2(cmd, resolvedArgs, {
|
|
@@ -3482,7 +3715,7 @@ var CodexProvider = class {
|
|
|
3482
3715
|
durationMs: Date.now() - startTime
|
|
3483
3716
|
};
|
|
3484
3717
|
if (degraded.length > 0) result.degradedCapabilities = degraded;
|
|
3485
|
-
|
|
3718
|
+
resolve2(result);
|
|
3486
3719
|
};
|
|
3487
3720
|
child.on("close", (code) => finish(code));
|
|
3488
3721
|
child.on("error", (err) => finish(1, err.message));
|
|
@@ -3522,22 +3755,22 @@ var CodexProvider = class {
|
|
|
3522
3755
|
}
|
|
3523
3756
|
}
|
|
3524
3757
|
}
|
|
3758
|
+
const WRITING_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "Bash", "NotebookEdit"]);
|
|
3525
3759
|
if (request.toolRestrictions) {
|
|
3526
3760
|
const tr = request.toolRestrictions;
|
|
3527
|
-
const writingTools = ["Write", "Edit", "Bash", "NotebookEdit", "shell", "file_write", "file_edit"];
|
|
3528
3761
|
if (tr.tools === "") {
|
|
3529
3762
|
args.push("--sandbox", "read-only");
|
|
3530
3763
|
degraded.push("builtinToolStripping");
|
|
3531
|
-
} else
|
|
3532
|
-
|
|
3533
|
-
|
|
3764
|
+
} else {
|
|
3765
|
+
const hasWritingDeny = (tr.disallowedTools ?? []).some((t) => WRITING_TOOLS.has(t));
|
|
3766
|
+
if (hasWritingDeny) {
|
|
3767
|
+
args.push("--sandbox", "read-only");
|
|
3768
|
+
degraded.push("toolDenylistAdvisory");
|
|
3769
|
+
}
|
|
3534
3770
|
}
|
|
3535
3771
|
if (tr.allowedTools?.length) {
|
|
3536
3772
|
degraded.push("toolAllowlist");
|
|
3537
3773
|
}
|
|
3538
|
-
if (tr.disallowedTools?.length && !degraded.includes("toolDenylist")) {
|
|
3539
|
-
degraded.push("toolDenylist");
|
|
3540
|
-
}
|
|
3541
3774
|
}
|
|
3542
3775
|
if (request.maxTurns) {
|
|
3543
3776
|
degraded.push("maxTurns");
|
|
@@ -3555,8 +3788,8 @@ var CodexProvider = class {
|
|
|
3555
3788
|
// src/providers/gemini-provider.ts
|
|
3556
3789
|
import { spawn as spawn3, execFileSync as execFileSync3 } from "child_process";
|
|
3557
3790
|
import { existsSync as existsSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, rmSync as rmSync3 } from "fs";
|
|
3558
|
-
import { join as
|
|
3559
|
-
import { homedir } from "os";
|
|
3791
|
+
import { join as join3, dirname } from "path";
|
|
3792
|
+
import { homedir as homedir2 } from "os";
|
|
3560
3793
|
import { fileURLToPath } from "url";
|
|
3561
3794
|
import { readFileSync as readFileSync2 } from "fs";
|
|
3562
3795
|
|
|
@@ -3719,13 +3952,13 @@ function resolveHookScriptPath() {
|
|
|
3719
3952
|
try {
|
|
3720
3953
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
3721
3954
|
const candidates = [
|
|
3722
|
-
|
|
3955
|
+
join3(thisDir, "lib", "gemini-hooks.mjs"),
|
|
3723
3956
|
// dist/lib/ (flat bundle)
|
|
3724
|
-
|
|
3957
|
+
join3(thisDir, "..", "lib", "gemini-hooks.mjs"),
|
|
3725
3958
|
// dist/../lib/ (nested)
|
|
3726
|
-
|
|
3959
|
+
join3(thisDir, "..", "src", "lib", "gemini-hooks.mjs"),
|
|
3727
3960
|
// source from dist/
|
|
3728
|
-
|
|
3961
|
+
join3(thisDir, "..", "..", "src", "lib", "gemini-hooks.mjs")
|
|
3729
3962
|
// source from dist/providers/
|
|
3730
3963
|
];
|
|
3731
3964
|
for (const p of candidates) {
|
|
@@ -3808,7 +4041,7 @@ var GeminiProvider = class _GeminiProvider {
|
|
|
3808
4041
|
if (request.workingDirectory && settingsDir) {
|
|
3809
4042
|
this.copySettingsToDir(settingsDir, request.workingDirectory);
|
|
3810
4043
|
}
|
|
3811
|
-
return new Promise((
|
|
4044
|
+
return new Promise((resolve2) => {
|
|
3812
4045
|
const [cmd, prefixArgs] = resolveCommand("gemini");
|
|
3813
4046
|
const resolvedArgs = [...prefixArgs, ...args];
|
|
3814
4047
|
const child = spawn3(cmd, resolvedArgs, {
|
|
@@ -3881,7 +4114,7 @@ var GeminiProvider = class _GeminiProvider {
|
|
|
3881
4114
|
durationMs: Date.now() - startTime
|
|
3882
4115
|
};
|
|
3883
4116
|
if (degraded.length > 0) result.degradedCapabilities = degraded;
|
|
3884
|
-
|
|
4117
|
+
resolve2(result);
|
|
3885
4118
|
};
|
|
3886
4119
|
child.on("close", (code) => finish(code));
|
|
3887
4120
|
child.on("error", (err) => finish(1, err.message));
|
|
@@ -3919,9 +4152,9 @@ var GeminiProvider = class _GeminiProvider {
|
|
|
3919
4152
|
}
|
|
3920
4153
|
writeGeminiSettings(request, degraded) {
|
|
3921
4154
|
try {
|
|
3922
|
-
const baseDir =
|
|
3923
|
-
const dir =
|
|
3924
|
-
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");
|
|
3925
4158
|
mkdirSync2(geminiDir, { recursive: true, mode: 448 });
|
|
3926
4159
|
const settings = {};
|
|
3927
4160
|
if (request.mcpConfig && Object.keys(request.mcpConfig.servers).length > 0) {
|
|
@@ -3945,20 +4178,20 @@ var GeminiProvider = class _GeminiProvider {
|
|
|
3945
4178
|
hookConfig.disallowedTools = tr.disallowedTools ? translateToolNames(tr.disallowedTools) : null;
|
|
3946
4179
|
if (tr.tools !== void 0) hookConfig.builtinToolsMode = tr.tools;
|
|
3947
4180
|
const ext = IS_WINDOWS ? ".cmd" : ".sh";
|
|
3948
|
-
const wrapperPath =
|
|
3949
|
-
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 });
|
|
3950
4183
|
hooks.BeforeToolSelection = [{ matcher: "*", hooks: [{ type: "command", command: wrapperPath, timeout: 3e4 }] }];
|
|
3951
4184
|
}
|
|
3952
4185
|
if (request.maxTurns && request.mcpConfig) {
|
|
3953
4186
|
hookConfig.maxTurns = request.maxTurns;
|
|
3954
|
-
hookConfig.turnFile =
|
|
4187
|
+
hookConfig.turnFile = join3(dir, ".kantban-turn-counter");
|
|
3955
4188
|
const ext = IS_WINDOWS ? ".cmd" : ".sh";
|
|
3956
|
-
const wrapperPath =
|
|
3957
|
-
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 });
|
|
3958
4191
|
hooks.AfterAgent = [{ matcher: "*", hooks: [{ type: "command", command: wrapperPath, timeout: 3e4 }] }];
|
|
3959
4192
|
}
|
|
3960
4193
|
if (Object.keys(hookConfig).length > 0) {
|
|
3961
|
-
writeFileSync3(
|
|
4194
|
+
writeFileSync3(join3(dir, ".kantban-hook-config.json"), JSON.stringify(hookConfig), { mode: 384 });
|
|
3962
4195
|
}
|
|
3963
4196
|
if (Object.keys(hooks).length > 0) {
|
|
3964
4197
|
settings.hooks = hooks;
|
|
@@ -3971,7 +4204,7 @@ var GeminiProvider = class _GeminiProvider {
|
|
|
3971
4204
|
process.stderr.write(`[gemini] Hook script not found \u2014 tool scoping and turn limits unavailable
|
|
3972
4205
|
`);
|
|
3973
4206
|
}
|
|
3974
|
-
writeFileSync3(
|
|
4207
|
+
writeFileSync3(join3(geminiDir, "settings.json"), JSON.stringify(settings, null, 2), { mode: 384 });
|
|
3975
4208
|
return dir;
|
|
3976
4209
|
} catch (err) {
|
|
3977
4210
|
process.stderr.write(`[gemini] Failed to write settings.json: ${err}
|
|
@@ -4037,17 +4270,29 @@ node "${hookScript}" "${event}" "${configFile}" < "$STDIN_FILE"
|
|
|
4037
4270
|
}
|
|
4038
4271
|
copySettingsToDir(settingsDir, targetDir) {
|
|
4039
4272
|
try {
|
|
4040
|
-
const sourceFile =
|
|
4041
|
-
const targetGeminiDir =
|
|
4273
|
+
const sourceFile = join3(settingsDir, ".gemini", "settings.json");
|
|
4274
|
+
const targetGeminiDir = join3(targetDir, ".gemini");
|
|
4042
4275
|
mkdirSync2(targetGeminiDir, { recursive: true, mode: 448 });
|
|
4043
4276
|
const content = readFileSync2(sourceFile, "utf-8");
|
|
4044
|
-
writeFileSync3(
|
|
4277
|
+
writeFileSync3(join3(targetGeminiDir, "settings.json"), content, { mode: 384 });
|
|
4045
4278
|
} catch {
|
|
4046
4279
|
}
|
|
4047
4280
|
}
|
|
4048
4281
|
};
|
|
4049
4282
|
|
|
4050
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
|
+
}
|
|
4051
4296
|
function createProviderRegistry() {
|
|
4052
4297
|
const registry = new ProviderRegistry();
|
|
4053
4298
|
registry.register(new ClaudeProvider());
|
|
@@ -4055,9 +4300,25 @@ function createProviderRegistry() {
|
|
|
4055
4300
|
registry.register(new GeminiProvider());
|
|
4056
4301
|
return registry;
|
|
4057
4302
|
}
|
|
4058
|
-
function readMcpConfigAsProviderConfig(filePath) {
|
|
4059
|
-
|
|
4060
|
-
|
|
4303
|
+
function readMcpConfigAsProviderConfig(filePath, regenerate) {
|
|
4304
|
+
function parse(path) {
|
|
4305
|
+
const raw = JSON.parse(readFileSync3(path, "utf-8"));
|
|
4306
|
+
return { servers: raw.mcpServers };
|
|
4307
|
+
}
|
|
4308
|
+
try {
|
|
4309
|
+
return parse(filePath);
|
|
4310
|
+
} catch (err) {
|
|
4311
|
+
const code = err?.code;
|
|
4312
|
+
if (code === "ENOENT") {
|
|
4313
|
+
process.stderr.write(
|
|
4314
|
+
`[mcp-config] regenerating missing file at ${filePath} (pid=${process.pid})
|
|
4315
|
+
`
|
|
4316
|
+
);
|
|
4317
|
+
const newPath = regenerate();
|
|
4318
|
+
return parse(newPath);
|
|
4319
|
+
}
|
|
4320
|
+
throw err;
|
|
4321
|
+
}
|
|
4061
4322
|
}
|
|
4062
4323
|
function parseArgs(args) {
|
|
4063
4324
|
const positional = [];
|
|
@@ -4153,10 +4414,10 @@ Flags:
|
|
|
4153
4414
|
return { boardId, once, dryRun, columnFilter, maxIterations, maxBudget, model, provider, concurrency, logRetention, yes };
|
|
4154
4415
|
}
|
|
4155
4416
|
function pidDir(boardId) {
|
|
4156
|
-
return
|
|
4417
|
+
return join4(homedir3(), ".kantban", "pipelines", boardId);
|
|
4157
4418
|
}
|
|
4158
4419
|
function pidFilePath(boardId) {
|
|
4159
|
-
return
|
|
4420
|
+
return join4(pidDir(boardId), "orchestrator.pid");
|
|
4160
4421
|
}
|
|
4161
4422
|
function writePidFile(boardId) {
|
|
4162
4423
|
const dir = pidDir(boardId);
|
|
@@ -4170,7 +4431,7 @@ function removePidFile(boardId) {
|
|
|
4170
4431
|
}
|
|
4171
4432
|
}
|
|
4172
4433
|
function childManifestPath(boardId) {
|
|
4173
|
-
return
|
|
4434
|
+
return join4(pidDir(boardId), "children.pid");
|
|
4174
4435
|
}
|
|
4175
4436
|
function readChildManifest(boardId) {
|
|
4176
4437
|
try {
|
|
@@ -4215,7 +4476,7 @@ function cleanupOrphanedProcesses(boardId) {
|
|
|
4215
4476
|
console.log(`Killed ${String(manifestPids.length)} orphaned child process(es) from manifest`);
|
|
4216
4477
|
removeChildManifest(boardId);
|
|
4217
4478
|
}
|
|
4218
|
-
const staleReaperPath =
|
|
4479
|
+
const staleReaperPath = join4(pidDir(boardId), "reaper.pid");
|
|
4219
4480
|
try {
|
|
4220
4481
|
if (existsSync3(staleReaperPath)) {
|
|
4221
4482
|
const reaperPid = parseInt(readFileSync3(staleReaperPath, "utf-8").trim(), 10);
|
|
@@ -4248,14 +4509,14 @@ Press Ctrl+C to cancel, or re-run with --yes to skip this warning.
|
|
|
4248
4509
|
`);
|
|
4249
4510
|
}
|
|
4250
4511
|
function waitForConfirmation() {
|
|
4251
|
-
return new Promise((
|
|
4512
|
+
return new Promise((resolve2) => {
|
|
4252
4513
|
process.stdout.write("Continue? [y/N] ");
|
|
4253
4514
|
process.stdin.setEncoding("utf8");
|
|
4254
4515
|
process.stdin.once("data", (data) => {
|
|
4255
4516
|
const answer = data.trim().toLowerCase();
|
|
4256
|
-
|
|
4517
|
+
resolve2(answer === "y" || answer === "yes");
|
|
4257
4518
|
});
|
|
4258
|
-
process.stdin.once("end", () =>
|
|
4519
|
+
process.stdin.once("end", () => resolve2(false));
|
|
4259
4520
|
});
|
|
4260
4521
|
}
|
|
4261
4522
|
async function runPipeline(client, args) {
|
|
@@ -4274,7 +4535,7 @@ async function runPipeline(client, args) {
|
|
|
4274
4535
|
return;
|
|
4275
4536
|
}
|
|
4276
4537
|
}
|
|
4277
|
-
const gateFilePath =
|
|
4538
|
+
const gateFilePath = join4(process.cwd(), "pipeline.gates.yaml");
|
|
4278
4539
|
let gateConfig;
|
|
4279
4540
|
if (!existsSync3(gateFilePath)) {
|
|
4280
4541
|
console.error(`Error: pipeline.gates.yaml not found in ${process.cwd()}`);
|
|
@@ -4304,13 +4565,14 @@ async function runPipeline(client, args) {
|
|
|
4304
4565
|
process.exit(1);
|
|
4305
4566
|
}
|
|
4306
4567
|
}
|
|
4568
|
+
reapOrphanedMcpConfigDirs(pidDir(opts.boardId));
|
|
4307
4569
|
const mcpConfigPath = generateMcpConfig(client.baseUrl, client.token, opts.boardId);
|
|
4308
4570
|
const boardProviderConfig = {
|
|
4309
4571
|
...opts.provider ? { default_provider: opts.provider } : {},
|
|
4310
4572
|
...opts.provider ? { intelligence_provider: opts.provider } : {}
|
|
4311
4573
|
};
|
|
4312
4574
|
const intelligenceProvider = registry.resolveForIntelligence(boardProviderConfig);
|
|
4313
|
-
const logBaseDir =
|
|
4575
|
+
const logBaseDir = join4(homedir3(), ".kantban", "pipelines");
|
|
4314
4576
|
const logger = new PipelineLogger(logBaseDir, opts.boardId);
|
|
4315
4577
|
logger.pruneOldLogs(opts.logRetention);
|
|
4316
4578
|
let runMemory = null;
|
|
@@ -4332,6 +4594,32 @@ async function runPipeline(client, args) {
|
|
|
4332
4594
|
...opts.maxBudget !== null && { maxBudgetUsd: opts.maxBudget },
|
|
4333
4595
|
...opts.model !== null && { model: opts.model }
|
|
4334
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
|
+
}
|
|
4335
4623
|
const mem = runMemory;
|
|
4336
4624
|
const colScopeForName = await client.get(
|
|
4337
4625
|
`/projects/${projectId}/pipeline-context`,
|
|
@@ -4347,8 +4635,8 @@ async function runPipeline(client, args) {
|
|
|
4347
4635
|
if (effectiveConfig.model) {
|
|
4348
4636
|
effectiveConfig.model = registry.resolveModel(columnProvider, effectiveConfig.model);
|
|
4349
4637
|
}
|
|
4350
|
-
const columnGates = resolveGatesForColumn(gateConfig, resolvedColumnName);
|
|
4351
|
-
const gateCwd = effectiveConfig.worktreePath ?? (effectiveConfig.worktreeName ?
|
|
4638
|
+
const { gates: columnGates } = resolveGatesForColumn(gateConfig, resolvedColumnName);
|
|
4639
|
+
const gateCwd = effectiveConfig.worktreePath ?? (effectiveConfig.worktreeName ? join4(process.cwd(), effectiveConfig.worktreeName) : process.cwd());
|
|
4352
4640
|
const effectiveMcpConfigPath = columnGates.length > 0 ? generateGateProxyMcpConfig(
|
|
4353
4641
|
client.baseUrl,
|
|
4354
4642
|
client.token,
|
|
@@ -4360,7 +4648,20 @@ async function runPipeline(client, args) {
|
|
|
4360
4648
|
gateCwd,
|
|
4361
4649
|
ticketId
|
|
4362
4650
|
) : mcpConfigPath;
|
|
4363
|
-
const columnMcpConfig = readMcpConfigAsProviderConfig(
|
|
4651
|
+
const columnMcpConfig = readMcpConfigAsProviderConfig(
|
|
4652
|
+
effectiveMcpConfigPath,
|
|
4653
|
+
() => columnGates.length > 0 ? generateGateProxyMcpConfig(
|
|
4654
|
+
client.baseUrl,
|
|
4655
|
+
client.token,
|
|
4656
|
+
opts.boardId,
|
|
4657
|
+
gateFilePath,
|
|
4658
|
+
columnId,
|
|
4659
|
+
resolvedColumnName,
|
|
4660
|
+
projectId,
|
|
4661
|
+
gateCwd,
|
|
4662
|
+
ticketId
|
|
4663
|
+
) : generateMcpConfig(client.baseUrl, client.token, opts.boardId)
|
|
4664
|
+
);
|
|
4364
4665
|
const loopDeps = {
|
|
4365
4666
|
fetchTicketContext: (tid) => client.get(`/projects/${projectId}/pipeline-context`, { ticketId: tid }),
|
|
4366
4667
|
fetchColumnContext: (cid) => client.get(`/projects/${projectId}/pipeline-context`, { columnId: cid }),
|
|
@@ -4468,7 +4769,7 @@ async function runPipeline(client, args) {
|
|
|
4468
4769
|
};
|
|
4469
4770
|
}
|
|
4470
4771
|
effectiveConfig.onPostIterationGates = async (tid, iteration) => {
|
|
4471
|
-
const gates = resolveGatesForColumn(gateConfig, resolvedColumnName);
|
|
4772
|
+
const { gates } = resolveGatesForColumn(gateConfig, resolvedColumnName);
|
|
4472
4773
|
if (gates.length === 0) {
|
|
4473
4774
|
return gateSnapshotStore.record(tid, iteration, []);
|
|
4474
4775
|
}
|
|
@@ -4504,6 +4805,12 @@ async function runPipeline(client, args) {
|
|
|
4504
4805
|
contentPrefix,
|
|
4505
4806
|
content: body
|
|
4506
4807
|
}),
|
|
4808
|
+
createHarnessSignal: (ticketId, content) => client.post(`/projects/${projectId}/signals`, {
|
|
4809
|
+
scopeType: "ticket",
|
|
4810
|
+
scopeId: ticketId,
|
|
4811
|
+
content
|
|
4812
|
+
}),
|
|
4813
|
+
deleteSignal: (signalId) => client.delete(`/projects/${projectId}/signals/${signalId}`),
|
|
4507
4814
|
claimTicket: (ticketId) => client.claimTicket(projectId, ticketId),
|
|
4508
4815
|
fetchBlockedTickets: (ticketId) => client.get(
|
|
4509
4816
|
`/projects/${projectId}/tickets/${ticketId}/blocked-tickets`
|
|
@@ -4512,7 +4819,27 @@ async function runPipeline(client, args) {
|
|
|
4512
4819
|
const blockers = await client.get(
|
|
4513
4820
|
`/projects/${projectId}/tickets/${ticketId}/unresolved-blockers`
|
|
4514
4821
|
);
|
|
4515
|
-
|
|
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
|
+
};
|
|
4516
4843
|
},
|
|
4517
4844
|
dispatchLightCall: async (ticketId, columnId) => {
|
|
4518
4845
|
const [ticketCtx, colScope, boardScope] = await Promise.all([
|
|
@@ -4626,6 +4953,7 @@ async function runPipeline(client, args) {
|
|
|
4626
4953
|
appendRunMemory: (section, content) => runMemory ? runMemory.append(section, content) : Promise.resolve(),
|
|
4627
4954
|
cleanupWorktree: (name) => cleanupWorktree(name),
|
|
4628
4955
|
mergeWorktree: (name, integrationBranch) => mergeWorktreeBranch(name, integrationBranch),
|
|
4956
|
+
detectBranchMerged: async (opts2) => detectBranchMerged(opts2),
|
|
4629
4957
|
// Pipeline event emission — wsClient captured by reference (set later before any loops run)
|
|
4630
4958
|
emitPipelineEvent: (event) => {
|
|
4631
4959
|
wsClient?.send(event);
|
|
@@ -4664,7 +4992,7 @@ async function runPipeline(client, args) {
|
|
|
4664
4992
|
console.error(`Error: Failed to initialize pipeline: ${message}`);
|
|
4665
4993
|
console.error("Check that the board exists, has pipeline columns configured, and the API is reachable.");
|
|
4666
4994
|
cleanupMcpConfig(mcpConfigPath);
|
|
4667
|
-
cleanupGateProxyConfigs(
|
|
4995
|
+
cleanupGateProxyConfigs(dirname2(mcpConfigPath));
|
|
4668
4996
|
process.exit(1);
|
|
4669
4997
|
}
|
|
4670
4998
|
const columnIds = orchestrator.pipelineColumnIds;
|
|
@@ -4720,7 +5048,7 @@ async function runPipeline(client, args) {
|
|
|
4720
5048
|
if (columnIds.length === 0) {
|
|
4721
5049
|
console.log('No pipeline columns found (columns need has_prompt=true and type !== "done").');
|
|
4722
5050
|
cleanupMcpConfig(mcpConfigPath);
|
|
4723
|
-
cleanupGateProxyConfigs(
|
|
5051
|
+
cleanupGateProxyConfigs(dirname2(mcpConfigPath));
|
|
4724
5052
|
return;
|
|
4725
5053
|
}
|
|
4726
5054
|
console.log(`Discovered ${String(columnIds.length)} pipeline column(s).`);
|
|
@@ -4734,6 +5062,15 @@ async function runPipeline(client, args) {
|
|
|
4734
5062
|
` No board column name matched (after canonicalization). These overrides will never fire.`
|
|
4735
5063
|
);
|
|
4736
5064
|
}
|
|
5065
|
+
for (const col of orchestrator.allBoardColumns) {
|
|
5066
|
+
if (col.type !== "done") continue;
|
|
5067
|
+
const { source } = resolveGatesForColumn(gateConfig, col.name);
|
|
5068
|
+
if (source === "override") {
|
|
5069
|
+
console.warn(
|
|
5070
|
+
`[gate-config] column "${col.name}" is done-type AND has an explicit override \u2014 gates will run on moves INTO this column (bypass disabled by your pipeline.gates.yaml).`
|
|
5071
|
+
);
|
|
5072
|
+
}
|
|
5073
|
+
}
|
|
4737
5074
|
if (opts.dryRun) {
|
|
4738
5075
|
console.log("\n--- Dry Run Configuration ---");
|
|
4739
5076
|
console.log(`Board ID: ${opts.boardId}`);
|
|
@@ -4749,7 +5086,7 @@ async function runPipeline(client, args) {
|
|
|
4749
5086
|
console.log(`MCP config: ${mcpConfigPath}`);
|
|
4750
5087
|
console.log("\n[Dry run -- no agents started]");
|
|
4751
5088
|
cleanupMcpConfig(mcpConfigPath);
|
|
4752
|
-
cleanupGateProxyConfigs(
|
|
5089
|
+
cleanupGateProxyConfigs(dirname2(mcpConfigPath));
|
|
4753
5090
|
return;
|
|
4754
5091
|
}
|
|
4755
5092
|
let shutdownInProgress = false;
|
|
@@ -4777,7 +5114,7 @@ Received ${signal}. Shutting down gracefully...`);
|
|
|
4777
5114
|
console.log(costTracker.generateReport(gateConfig.settings?.pricing));
|
|
4778
5115
|
}
|
|
4779
5116
|
cleanupMcpConfig(mcpConfigPath);
|
|
4780
|
-
cleanupGateProxyConfigs(
|
|
5117
|
+
cleanupGateProxyConfigs(dirname2(mcpConfigPath));
|
|
4781
5118
|
killReaper(reaperPidPath);
|
|
4782
5119
|
removePidFile(opts.boardId);
|
|
4783
5120
|
removeChildManifest(opts.boardId);
|
|
@@ -4808,14 +5145,14 @@ Received ${signal}. Shutting down gracefully...`);
|
|
|
4808
5145
|
cleanupOrphanedProcesses(opts.boardId);
|
|
4809
5146
|
writePidFile(opts.boardId);
|
|
4810
5147
|
logger.orchestrator(`PID file written: ${String(process.pid)}`);
|
|
4811
|
-
const reaperPidPath =
|
|
5148
|
+
const reaperPidPath = join4(pidDir(opts.boardId), "reaper.pid");
|
|
4812
5149
|
const reaperProcess = spawnReaper({
|
|
4813
5150
|
orchestratorPid: process.pid,
|
|
4814
5151
|
manifestPath: childManifestPath(opts.boardId),
|
|
4815
5152
|
pidFilePath: pidFilePath(opts.boardId),
|
|
4816
5153
|
reaperPidPath,
|
|
4817
5154
|
mcpConfigPath,
|
|
4818
|
-
pipelineDir:
|
|
5155
|
+
pipelineDir: dirname2(mcpConfigPath)
|
|
4819
5156
|
});
|
|
4820
5157
|
logger.orchestrator(`Watchdog reaper spawned (PID ${String(reaperProcess.pid ?? "unknown")})`);
|
|
4821
5158
|
let eventQueue = null;
|
|
@@ -4900,7 +5237,7 @@ Received ${signal}. Shutting down gracefully...`);
|
|
|
4900
5237
|
wsClient.stop();
|
|
4901
5238
|
eventQueue.stop();
|
|
4902
5239
|
cleanupMcpConfig(mcpConfigPath);
|
|
4903
|
-
cleanupGateProxyConfigs(
|
|
5240
|
+
cleanupGateProxyConfigs(dirname2(mcpConfigPath));
|
|
4904
5241
|
killReaper(reaperPidPath);
|
|
4905
5242
|
removePidFile(opts.boardId);
|
|
4906
5243
|
removeChildManifest(opts.boardId);
|
|
@@ -4948,7 +5285,7 @@ async function waitForAllLoops(orchestrator, timeoutMs = 4 * 60 * 60 * 1e3) {
|
|
|
4948
5285
|
);
|
|
4949
5286
|
return;
|
|
4950
5287
|
}
|
|
4951
|
-
await new Promise((
|
|
5288
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3));
|
|
4952
5289
|
if (activeRalphLoops.size === 0 && orchestrator.activeLoopCount === 0 && !orchestrator.hasActiveQueuedWork && !orchestrator.hasCompletingWork) {
|
|
4953
5290
|
consecutiveIdle++;
|
|
4954
5291
|
} else {
|
|
@@ -4998,4 +5335,4 @@ export {
|
|
|
4998
5335
|
runPipeline,
|
|
4999
5336
|
stopPipeline
|
|
5000
5337
|
};
|
|
5001
|
-
//# sourceMappingURL=pipeline-
|
|
5338
|
+
//# sourceMappingURL=pipeline-6J64Z7VH.js.map
|