kantban-cli 0.1.49 → 0.1.52
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-QHJZIGEE.js → chunk-3A4B7CUH.js} +265 -72
- package/dist/chunk-3A4B7CUH.js.map +1 -0
- package/dist/{chunk-DENXSVKE.js → chunk-GQQUH3TA.js} +7 -2
- package/dist/chunk-GQQUH3TA.js.map +1 -0
- package/dist/{cron-F6D6475M.js → cron-VZMNM2XT.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/lib/gate-proxy-server.js +1 -1
- package/dist/{pipeline-GZOSDNPF.js → pipeline-NRG2Q2TE.js} +223 -203
- package/dist/pipeline-NRG2Q2TE.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-VZMNM2XT.js.map} +0 -0
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runGates
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-GQQUH3TA.js";
|
|
4
4
|
import {
|
|
5
5
|
ClaudeProvider,
|
|
6
6
|
RalphLoop,
|
|
7
7
|
classifyTrajectory,
|
|
8
8
|
cleanupGateProxyConfigs,
|
|
9
9
|
cleanupMcpConfig,
|
|
10
|
+
cleanupWorktree,
|
|
10
11
|
composeStuckDetectionPrompt,
|
|
12
|
+
defaultWorktreeRoot,
|
|
13
|
+
detectBranchMerged,
|
|
14
|
+
ensureWorktreeRemote,
|
|
11
15
|
generateGateProxyMcpConfig,
|
|
12
16
|
generateMcpConfig,
|
|
17
|
+
generateWorktreeName,
|
|
18
|
+
mergeWorktreeBranch,
|
|
13
19
|
parseJsonFromLlmOutput,
|
|
14
20
|
parseStuckDetectionResponse,
|
|
15
|
-
reapOrphanedMcpConfigDirs
|
|
16
|
-
|
|
21
|
+
reapOrphanedMcpConfigDirs,
|
|
22
|
+
renderWorktreePath
|
|
23
|
+
} from "./chunk-3A4B7CUH.js";
|
|
17
24
|
import {
|
|
18
25
|
LoopCheckpointSchema,
|
|
19
26
|
VerdictSchema,
|
|
@@ -31,9 +38,11 @@ import {
|
|
|
31
38
|
} from "./chunk-5ZU2OOES.js";
|
|
32
39
|
|
|
33
40
|
// 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";
|
|
41
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync3, unlinkSync as unlinkSync3, existsSync as existsSync3, appendFileSync as appendFileSync2, promises as fsPromises } from "fs";
|
|
42
|
+
import { execFile } from "child_process";
|
|
35
43
|
import { homedir as homedir2 } from "os";
|
|
36
|
-
import { join as join3, dirname as dirname2 } from "path";
|
|
44
|
+
import { join as join3, dirname as dirname2, resolve, sep } from "path";
|
|
45
|
+
import { promisify } from "util";
|
|
37
46
|
|
|
38
47
|
// src/lib/harness-signal.ts
|
|
39
48
|
var HARNESS_SIGNAL_PREFIX = "@harness:";
|
|
@@ -55,142 +64,6 @@ function resolveToolRestrictions(builtinTools, allowedTools, disallowedTools) {
|
|
|
55
64
|
};
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
// src/lib/worktree.ts
|
|
59
|
-
import { execFile as defaultExecFile, execFileSync } from "child_process";
|
|
60
|
-
function generateWorktreeName(ticketNumber, columnName) {
|
|
61
|
-
const slug = columnSlug(columnName);
|
|
62
|
-
return `kantban-${ticketNumber}-${slug}`;
|
|
63
|
-
}
|
|
64
|
-
function columnSlug(columnName) {
|
|
65
|
-
return columnName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
66
|
-
}
|
|
67
|
-
function renderWorktreePath(ticketNumber, columnName, pathPattern) {
|
|
68
|
-
const worktreeName = generateWorktreeName(ticketNumber, columnName);
|
|
69
|
-
if (!pathPattern) return worktreeName;
|
|
70
|
-
return pathPattern.replace(/\{ticket_number\}/g, String(ticketNumber)).replace(/\{column_slug\}/g, columnSlug(columnName)).replace(/\{worktree_name\}/g, worktreeName);
|
|
71
|
-
}
|
|
72
|
-
function isPlausibleRemoteUrl(url) {
|
|
73
|
-
return url.includes("/") || url.includes("@") || url.includes("://");
|
|
74
|
-
}
|
|
75
|
-
function ensureWorktreeRemote(worktreePath) {
|
|
76
|
-
try {
|
|
77
|
-
const remotes = normalizeEol(execFileSync("git", ["-C", worktreePath, "remote"], {
|
|
78
|
-
stdio: "pipe",
|
|
79
|
-
encoding: "utf-8"
|
|
80
|
-
})).trim();
|
|
81
|
-
if (remotes.split("\n").includes("origin")) {
|
|
82
|
-
const currentUrl = normalizeEol(execFileSync("git", ["-C", worktreePath, "remote", "get-url", "origin"], {
|
|
83
|
-
stdio: "pipe",
|
|
84
|
-
encoding: "utf-8"
|
|
85
|
-
})).trim();
|
|
86
|
-
if (isPlausibleRemoteUrl(currentUrl)) return;
|
|
87
|
-
console.error(`[worktree] Invalid origin URL in ${worktreePath}: "${currentUrl}" \u2014 fixing`);
|
|
88
|
-
execFileSync("git", ["-C", worktreePath, "remote", "remove", "origin"], {
|
|
89
|
-
stdio: "pipe"
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
const originUrl = normalizeEol(execFileSync("git", ["remote", "get-url", "origin"], {
|
|
93
|
-
stdio: "pipe",
|
|
94
|
-
encoding: "utf-8"
|
|
95
|
-
})).trim();
|
|
96
|
-
if (originUrl && isPlausibleRemoteUrl(originUrl)) {
|
|
97
|
-
execFileSync("git", ["-C", worktreePath, "remote", "add", "origin", originUrl], {
|
|
98
|
-
stdio: "pipe"
|
|
99
|
-
});
|
|
100
|
-
console.error(`[worktree] Added missing origin remote to ${worktreePath}: ${originUrl}`);
|
|
101
|
-
} else {
|
|
102
|
-
console.error(`[worktree] WARNING: main repo origin URL is also invalid: "${originUrl}"`);
|
|
103
|
-
}
|
|
104
|
-
} catch (err) {
|
|
105
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
106
|
-
console.error(`[worktree] Failed to ensure remote for ${worktreePath}: ${msg}`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
async function cleanupWorktree(worktreeName, exec = defaultExecFile) {
|
|
110
|
-
let target = worktreeName;
|
|
111
|
-
try {
|
|
112
|
-
const path = await findWorktreeForBranch(exec, worktreeName);
|
|
113
|
-
if (!path) return true;
|
|
114
|
-
target = path;
|
|
115
|
-
} catch {
|
|
116
|
-
return true;
|
|
117
|
-
}
|
|
118
|
-
return new Promise((resolve) => {
|
|
119
|
-
exec("git", ["worktree", "remove", "--force", target], (err) => {
|
|
120
|
-
if (err) {
|
|
121
|
-
console.error(`[worktree] cleanup failed for ${worktreeName} (${target}): ${err.message}`);
|
|
122
|
-
resolve(false);
|
|
123
|
-
} else {
|
|
124
|
-
resolve(true);
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
function execPromise(exec, cmd, args) {
|
|
130
|
-
return new Promise((resolve, reject) => {
|
|
131
|
-
exec(cmd, args, (err, stdout, stderr) => {
|
|
132
|
-
if (err) reject(Object.assign(err, { stdout, stderr }));
|
|
133
|
-
else resolve({ stdout, stderr });
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
async function findWorktreeForBranch(exec, branch) {
|
|
138
|
-
try {
|
|
139
|
-
const { stdout } = await execPromise(exec, "git", ["worktree", "list", "--porcelain"]);
|
|
140
|
-
const targetRef = `refs/heads/${branch}`;
|
|
141
|
-
let currentPath = null;
|
|
142
|
-
for (const line of normalizeEol(stdout).split("\n")) {
|
|
143
|
-
if (line.startsWith("worktree ")) currentPath = line.slice("worktree ".length);
|
|
144
|
-
if (line.startsWith("branch ") && line.slice("branch ".length) === targetRef && currentPath) {
|
|
145
|
-
return currentPath;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return null;
|
|
149
|
-
} catch {
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
async function mergeWorktreeBranch(worktreeName, integrationBranch, exec = defaultExecFile) {
|
|
154
|
-
try {
|
|
155
|
-
try {
|
|
156
|
-
await execPromise(exec, "git", ["rev-parse", "--verify", worktreeName]);
|
|
157
|
-
} catch {
|
|
158
|
-
return true;
|
|
159
|
-
}
|
|
160
|
-
await execPromise(exec, "git", ["branch", integrationBranch, "HEAD"]).catch(() => {
|
|
161
|
-
});
|
|
162
|
-
const checkedOutPath = await findWorktreeForBranch(exec, integrationBranch);
|
|
163
|
-
if (checkedOutPath) {
|
|
164
|
-
await execPromise(exec, "git", ["-C", checkedOutPath, "merge", "--no-edit", worktreeName]);
|
|
165
|
-
console.error(`[worktree] merged ${worktreeName} \u2192 ${integrationBranch}`);
|
|
166
|
-
return true;
|
|
167
|
-
}
|
|
168
|
-
const { stdout: baseOut } = await execPromise(exec, "git", ["merge-base", integrationBranch, worktreeName]);
|
|
169
|
-
const mergeBase = baseOut.trim();
|
|
170
|
-
const { stdout: integrationSha } = await execPromise(exec, "git", ["rev-parse", integrationBranch]);
|
|
171
|
-
if (integrationSha.trim() === mergeBase) {
|
|
172
|
-
const { stdout: worktreeSha } = await execPromise(exec, "git", ["rev-parse", worktreeName]);
|
|
173
|
-
await execPromise(exec, "git", ["update-ref", `refs/heads/${integrationBranch}`, worktreeSha.trim()]);
|
|
174
|
-
console.error(`[worktree] fast-forward merged ${worktreeName} \u2192 ${integrationBranch}`);
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
const tmpWorktree = `merge-tmp-${Date.now()}`;
|
|
178
|
-
try {
|
|
179
|
-
await execPromise(exec, "git", ["worktree", "add", tmpWorktree, integrationBranch]);
|
|
180
|
-
await execPromise(exec, "git", ["-C", tmpWorktree, "merge", "--no-edit", worktreeName]);
|
|
181
|
-
console.error(`[worktree] merged ${worktreeName} \u2192 ${integrationBranch}`);
|
|
182
|
-
} finally {
|
|
183
|
-
await execPromise(exec, "git", ["worktree", "remove", "--force", tmpWorktree]).catch(() => {
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
return true;
|
|
187
|
-
} catch (err) {
|
|
188
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
189
|
-
console.error(`[worktree] merge failed for ${worktreeName} \u2192 ${integrationBranch}: ${msg}`);
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
67
|
// src/lib/constraint-evaluator.ts
|
|
195
68
|
function resolveColumn(board, columnId, subjectRef) {
|
|
196
69
|
const ref = subjectRef ?? "self";
|
|
@@ -942,8 +815,8 @@ var PipelineOrchestrator = class {
|
|
|
942
815
|
}
|
|
943
816
|
for (const [ticketId, columnId] of Array.from(this.deferredTickets)) {
|
|
944
817
|
try {
|
|
945
|
-
const
|
|
946
|
-
if (!
|
|
818
|
+
const blocker = await this.deps.hasUnresolvedBlockers(ticketId);
|
|
819
|
+
if (!blocker.blocked) {
|
|
947
820
|
this.deferredTickets.delete(ticketId);
|
|
948
821
|
await this.spawnOrQueue(ticketId, columnId, true);
|
|
949
822
|
} else {
|
|
@@ -957,26 +830,56 @@ var PipelineOrchestrator = class {
|
|
|
957
830
|
}
|
|
958
831
|
await this.refreshBoardScope();
|
|
959
832
|
this.blockedColumns.clear();
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
this.
|
|
969
|
-
|
|
833
|
+
await Promise.all(
|
|
834
|
+
Array.from(this.pipelineColumns.keys()).map(async (columnId) => {
|
|
835
|
+
await this.refreshColumnScope(columnId);
|
|
836
|
+
const colScope = this.columnScopes.get(columnId);
|
|
837
|
+
if (!colScope) {
|
|
838
|
+
console.error(` [scan] column=${columnId} reason="no cached scope"`);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (this.isColumnBlocked(columnId)) {
|
|
842
|
+
this.blockedColumns.add(columnId);
|
|
843
|
+
console.error(
|
|
844
|
+
` [scan] column=${columnId} name="${colScope.column.name}" tickets=${colScope.tickets.length} dispatched=0 queued=0 skipped=${colScope.tickets.length} reason="firing_constraint"`
|
|
845
|
+
);
|
|
846
|
+
for (const ticket of colScope.tickets) {
|
|
847
|
+
await this.emitDispatchDeferred(ticket.id, "firing_constraint", { columnId });
|
|
848
|
+
}
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
let dispatched = 0;
|
|
852
|
+
let queued = 0;
|
|
853
|
+
let skipped = 0;
|
|
854
|
+
const reasons = /* @__PURE__ */ new Set();
|
|
970
855
|
for (const ticket of colScope.tickets) {
|
|
971
|
-
|
|
856
|
+
const wasActive = this.activeLoops.has(ticket.id) || this.spawning.has(ticket.id);
|
|
857
|
+
const wasQueued = (this.loopQueues.get(columnId) ?? []).includes(ticket.id);
|
|
858
|
+
const wasDeferred = this.deferredTickets.has(ticket.id);
|
|
859
|
+
await this.spawnOrQueue(ticket.id, columnId);
|
|
860
|
+
const nowActive = this.activeLoops.has(ticket.id) || this.spawning.has(ticket.id);
|
|
861
|
+
const nowQueued = (this.loopQueues.get(columnId) ?? []).includes(ticket.id);
|
|
862
|
+
const nowDeferred = this.deferredTickets.has(ticket.id);
|
|
863
|
+
if (!wasActive && nowActive) {
|
|
864
|
+
dispatched++;
|
|
865
|
+
reasons.add("dispatched");
|
|
866
|
+
} else if (!wasQueued && nowQueued) {
|
|
867
|
+
queued++;
|
|
868
|
+
reasons.add("capped");
|
|
869
|
+
} else if (!wasDeferred && nowDeferred) {
|
|
870
|
+
skipped++;
|
|
871
|
+
reasons.add("blocked");
|
|
872
|
+
} else {
|
|
873
|
+
skipped++;
|
|
874
|
+
reasons.add("known");
|
|
875
|
+
}
|
|
972
876
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
}
|
|
877
|
+
const reason = reasons.size === 0 ? "none" : reasons.size === 1 ? [...reasons][0] : "mixed";
|
|
878
|
+
console.error(
|
|
879
|
+
` [scan] column=${columnId} name="${colScope.column.name}" tickets=${colScope.tickets.length} dispatched=${dispatched} queued=${queued} skipped=${skipped} reason="${reason}"`
|
|
880
|
+
);
|
|
881
|
+
})
|
|
882
|
+
);
|
|
980
883
|
}
|
|
981
884
|
/**
|
|
982
885
|
* Handle a pipeline event (typically from WebSocket via EventQueue).
|
|
@@ -1098,15 +1001,58 @@ var PipelineOrchestrator = class {
|
|
|
1098
1001
|
}
|
|
1099
1002
|
return;
|
|
1100
1003
|
}
|
|
1004
|
+
const isMergeColumn = colConfig.name.toLowerCase().includes("merge");
|
|
1005
|
+
if (isMergeColumn && colConfig.worktreeEnabled && this.deps.detectBranchMerged && this.deps.moveTicketToColumn) {
|
|
1006
|
+
const ticket = this.columnScopes.get(columnId)?.tickets.find((t) => t.id === ticketId);
|
|
1007
|
+
if (ticket) {
|
|
1008
|
+
const wPath = renderWorktreePath(
|
|
1009
|
+
ticket.ticket_number,
|
|
1010
|
+
colConfig.name,
|
|
1011
|
+
colConfig.worktreePathPattern,
|
|
1012
|
+
{ boardId: this.boardId, defaultRoot: defaultWorktreeRoot() }
|
|
1013
|
+
);
|
|
1014
|
+
try {
|
|
1015
|
+
const mergeResult = await this.deps.detectBranchMerged({
|
|
1016
|
+
worktreePath: wPath,
|
|
1017
|
+
iterationStartedAt: /* @__PURE__ */ new Date(0),
|
|
1018
|
+
targetBranch: colConfig.worktreeIntegrationBranch ?? "main"
|
|
1019
|
+
});
|
|
1020
|
+
if (mergeResult.merged) {
|
|
1021
|
+
const nextColumnId = this.findNextColumnId(columnId);
|
|
1022
|
+
if (nextColumnId) {
|
|
1023
|
+
await this.deps.createComment(
|
|
1024
|
+
ticketId,
|
|
1025
|
+
`## Merge Complete (auto-finalized)
|
|
1026
|
+
|
|
1027
|
+
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.`
|
|
1028
|
+
);
|
|
1029
|
+
await this.deps.moveTicketToColumn(ticketId, nextColumnId, {
|
|
1030
|
+
reason: "merge_finalize_recovery",
|
|
1031
|
+
merge_commit_sha: mergeResult.mergeCommitSha
|
|
1032
|
+
});
|
|
1033
|
+
console.error(
|
|
1034
|
+
` [merge-recovery] ticket=${ticketId} column=${columnId} target=${colConfig.worktreeIntegrationBranch ?? "main"} sha=${mergeResult.mergeCommitSha}`
|
|
1035
|
+
);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
} catch {
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1101
1043
|
this.spawning.add(ticketId);
|
|
1102
1044
|
this.reserveSlot(columnId);
|
|
1103
1045
|
try {
|
|
1104
|
-
const
|
|
1105
|
-
if (blocked) {
|
|
1046
|
+
const result = await this.deps.hasUnresolvedBlockers(ticketId);
|
|
1047
|
+
if (result.blocked) {
|
|
1106
1048
|
this.spawning.delete(ticketId);
|
|
1107
1049
|
this.releaseSlot(columnId);
|
|
1108
1050
|
this.deferredTickets.set(ticketId, columnId);
|
|
1109
|
-
|
|
1051
|
+
const blockerSummary = result.blockers.map((b) => `${b.ticket_number}:${b.column}`).join(",");
|
|
1052
|
+
const linkIdSummary = result.linkIds.join(",");
|
|
1053
|
+
console.error(
|
|
1054
|
+
` [scan] skip ticket=${ticketId} reason=blocks count=${result.blockers.length} link_ids=[${linkIdSummary}] blockers=[${blockerSummary}]`
|
|
1055
|
+
);
|
|
1110
1056
|
void this.emitDispatchDeferred(ticketId, "unresolved_blockers", { columnId });
|
|
1111
1057
|
void this.drainQueue(columnId).catch((err) => {
|
|
1112
1058
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1121,9 +1067,9 @@ var PipelineOrchestrator = class {
|
|
|
1121
1067
|
this.releaseSlot(columnId);
|
|
1122
1068
|
this.deferredTickets.set(ticketId, columnId);
|
|
1123
1069
|
void this.emitDispatchDeferred(ticketId, "unresolved_blockers", { columnId });
|
|
1124
|
-
void this.drainQueue(columnId).catch((
|
|
1125
|
-
const
|
|
1126
|
-
console.error(` [error] drainQueue failed for column ${columnId}: ${
|
|
1070
|
+
void this.drainQueue(columnId).catch((dErr) => {
|
|
1071
|
+
const dMsg = dErr instanceof Error ? dErr.message : String(dErr);
|
|
1072
|
+
console.error(` [error] drainQueue failed for column ${columnId}: ${dMsg}`);
|
|
1127
1073
|
});
|
|
1128
1074
|
return;
|
|
1129
1075
|
}
|
|
@@ -1271,16 +1217,16 @@ var PipelineOrchestrator = class {
|
|
|
1271
1217
|
gutterThreshold: config.gutterThreshold,
|
|
1272
1218
|
...effectiveModel !== void 0 && { model: effectiveModel },
|
|
1273
1219
|
...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
1220
|
...(() => {
|
|
1278
1221
|
const colScope = this.columnScopes.get(columnId);
|
|
1279
1222
|
const ticket = colScope?.tickets.find((t) => t.id === ticketId);
|
|
1280
1223
|
if (!config.worktreeEnabled || !ticket) return {};
|
|
1281
1224
|
const wName = generateWorktreeName(ticket.ticket_number, config.name);
|
|
1282
|
-
const wPath = renderWorktreePath(ticket.ticket_number, config.name, config.worktreePathPattern
|
|
1283
|
-
|
|
1225
|
+
const wPath = renderWorktreePath(ticket.ticket_number, config.name, config.worktreePathPattern, {
|
|
1226
|
+
boardId: this.boardId,
|
|
1227
|
+
defaultRoot: defaultWorktreeRoot()
|
|
1228
|
+
});
|
|
1229
|
+
return { worktreeName: wName, worktreePath: wPath };
|
|
1284
1230
|
})(),
|
|
1285
1231
|
...config.lookaheadColumnId !== void 0 && { lookaheadColumnId: config.lookaheadColumnId },
|
|
1286
1232
|
// Resume from checkpoint iteration/gutter if provided
|
|
@@ -1799,6 +1745,19 @@ ${findingsText}`)
|
|
|
1799
1745
|
const nextBoardCol = bs.columns.filter((c) => c.position > currentBoardCol.position && this.pipelineColumns.has(c.id)).sort((a, b) => a.position - b.position)[0];
|
|
1800
1746
|
return nextBoardCol ? this.pipelineColumns.get(nextBoardCol.id) ?? null : null;
|
|
1801
1747
|
}
|
|
1748
|
+
/**
|
|
1749
|
+
* Find the ID of the next board column by position, regardless of pipeline status.
|
|
1750
|
+
* Used by merge-recovery to target the downstream column (e.g. Done) which may not
|
|
1751
|
+
* be a pipeline column.
|
|
1752
|
+
*/
|
|
1753
|
+
findNextColumnId(currentColumnId) {
|
|
1754
|
+
const bs = this.cachedBoardScope;
|
|
1755
|
+
if (!bs) return null;
|
|
1756
|
+
const currentBoardCol = bs.columns.find((c) => c.id === currentColumnId);
|
|
1757
|
+
if (!currentBoardCol) return null;
|
|
1758
|
+
const nextBoardCol = bs.columns.filter((c) => c.position > currentBoardCol.position).sort((a, b) => a.position - b.position)[0];
|
|
1759
|
+
return nextBoardCol?.id ?? null;
|
|
1760
|
+
}
|
|
1802
1761
|
/**
|
|
1803
1762
|
* Called when a loop finishes. Cleans up tracking and drains the queue.
|
|
1804
1763
|
*/
|
|
@@ -2186,8 +2145,8 @@ ${findingsText}`)
|
|
|
2186
2145
|
this.spawning.add(nextTicketId);
|
|
2187
2146
|
this.reserveSlot(columnId);
|
|
2188
2147
|
try {
|
|
2189
|
-
const
|
|
2190
|
-
if (blocked) {
|
|
2148
|
+
const blockerResult = await this.deps.hasUnresolvedBlockers(nextTicketId);
|
|
2149
|
+
if (blockerResult.blocked) {
|
|
2191
2150
|
this.deferredTickets.set(nextTicketId, columnId);
|
|
2192
2151
|
this.spawning.delete(nextTicketId);
|
|
2193
2152
|
this.releaseSlot(columnId);
|
|
@@ -2542,7 +2501,7 @@ var PipelineWsClient = class _PipelineWsClient {
|
|
|
2542
2501
|
this.cleanupTimers();
|
|
2543
2502
|
const { ticket } = await this.client.post("/ws-ticket");
|
|
2544
2503
|
const wsUrl = this.client.baseUrl.replace(/^http/, "ws") + `/ws?ticket=${ticket}&clientType=cli`;
|
|
2545
|
-
return new Promise((
|
|
2504
|
+
return new Promise((resolve2, reject) => {
|
|
2546
2505
|
let resolved = false;
|
|
2547
2506
|
this.ws = new WebSocket(wsUrl);
|
|
2548
2507
|
let subscribed = false;
|
|
@@ -2571,7 +2530,7 @@ var PipelineWsClient = class _PipelineWsClient {
|
|
|
2571
2530
|
this.options.onConnect();
|
|
2572
2531
|
if (!resolved) {
|
|
2573
2532
|
resolved = true;
|
|
2574
|
-
|
|
2533
|
+
resolve2();
|
|
2575
2534
|
}
|
|
2576
2535
|
}
|
|
2577
2536
|
this.options.onEvent(event);
|
|
@@ -3386,7 +3345,7 @@ var ProviderRegistry = class {
|
|
|
3386
3345
|
};
|
|
3387
3346
|
|
|
3388
3347
|
// src/providers/codex-provider.ts
|
|
3389
|
-
import { spawn as spawn2, execFileSync
|
|
3348
|
+
import { spawn as spawn2, execFileSync } from "child_process";
|
|
3390
3349
|
import { existsSync, rmSync as rmSync2 } from "fs";
|
|
3391
3350
|
|
|
3392
3351
|
// src/providers/codex-jsonl-parser.ts
|
|
@@ -3527,45 +3486,47 @@ var CodexProvider = class {
|
|
|
3527
3486
|
const branch = request.branch ?? request.workingDirectory;
|
|
3528
3487
|
if (!existsSync(request.workingDirectory)) {
|
|
3529
3488
|
try {
|
|
3530
|
-
|
|
3489
|
+
execFileSync("git", ["worktree", "add", "-b", branch, request.workingDirectory, "HEAD"], {
|
|
3531
3490
|
stdio: "pipe"
|
|
3532
3491
|
});
|
|
3533
3492
|
} catch {
|
|
3534
3493
|
try {
|
|
3535
|
-
|
|
3494
|
+
execFileSync("git", ["worktree", "add", request.workingDirectory, branch], {
|
|
3536
3495
|
stdio: "pipe"
|
|
3537
3496
|
});
|
|
3538
|
-
} catch {
|
|
3539
|
-
|
|
3497
|
+
} catch (err) {
|
|
3498
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3499
|
+
throw new Error(`worktree_creation_failed: ${msg}`);
|
|
3540
3500
|
}
|
|
3541
3501
|
}
|
|
3542
3502
|
} else {
|
|
3543
3503
|
try {
|
|
3544
|
-
|
|
3504
|
+
execFileSync("git", ["-C", request.workingDirectory, "rev-parse", "--git-dir"], {
|
|
3545
3505
|
stdio: "pipe"
|
|
3546
3506
|
});
|
|
3547
3507
|
} catch {
|
|
3548
3508
|
try {
|
|
3549
3509
|
rmSync2(request.workingDirectory, { recursive: true, force: true });
|
|
3550
|
-
|
|
3510
|
+
execFileSync("git", ["worktree", "add", "-b", branch, request.workingDirectory, "HEAD"], {
|
|
3551
3511
|
stdio: "pipe"
|
|
3552
3512
|
});
|
|
3553
3513
|
} catch {
|
|
3554
3514
|
try {
|
|
3555
|
-
|
|
3515
|
+
execFileSync("git", ["worktree", "add", request.workingDirectory, branch], {
|
|
3556
3516
|
stdio: "pipe"
|
|
3557
3517
|
});
|
|
3558
|
-
} catch {
|
|
3559
|
-
|
|
3518
|
+
} catch (err) {
|
|
3519
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3520
|
+
throw new Error(`worktree_creation_failed: ${msg}`);
|
|
3560
3521
|
}
|
|
3561
3522
|
}
|
|
3562
3523
|
}
|
|
3563
3524
|
}
|
|
3564
|
-
if (
|
|
3525
|
+
if (existsSync(request.workingDirectory)) {
|
|
3565
3526
|
ensureWorktreeRemote(request.workingDirectory);
|
|
3566
3527
|
}
|
|
3567
3528
|
}
|
|
3568
|
-
return new Promise((
|
|
3529
|
+
return new Promise((resolve2) => {
|
|
3569
3530
|
const [cmd, prefixArgs] = resolveCommand("codex");
|
|
3570
3531
|
const resolvedArgs = [...prefixArgs, ...args];
|
|
3571
3532
|
const child = spawn2(cmd, resolvedArgs, {
|
|
@@ -3616,7 +3577,7 @@ var CodexProvider = class {
|
|
|
3616
3577
|
durationMs: Date.now() - startTime
|
|
3617
3578
|
};
|
|
3618
3579
|
if (degraded.length > 0) result.degradedCapabilities = degraded;
|
|
3619
|
-
|
|
3580
|
+
resolve2(result);
|
|
3620
3581
|
};
|
|
3621
3582
|
child.on("close", (code) => finish(code));
|
|
3622
3583
|
child.on("error", (err) => finish(1, err.message));
|
|
@@ -3687,7 +3648,7 @@ var CodexProvider = class {
|
|
|
3687
3648
|
};
|
|
3688
3649
|
|
|
3689
3650
|
// src/providers/gemini-provider.ts
|
|
3690
|
-
import { spawn as spawn3, execFileSync as
|
|
3651
|
+
import { spawn as spawn3, execFileSync as execFileSync2 } from "child_process";
|
|
3691
3652
|
import { existsSync as existsSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, rmSync as rmSync3 } from "fs";
|
|
3692
3653
|
import { join as join2, dirname } from "path";
|
|
3693
3654
|
import { homedir } from "os";
|
|
@@ -3899,12 +3860,12 @@ var GeminiProvider = class _GeminiProvider {
|
|
|
3899
3860
|
if (request.workingDirectory) {
|
|
3900
3861
|
if (!existsSync2(request.workingDirectory)) {
|
|
3901
3862
|
try {
|
|
3902
|
-
|
|
3863
|
+
execFileSync2("git", ["worktree", "add", "-b", request.workingDirectory, request.workingDirectory, "HEAD"], {
|
|
3903
3864
|
stdio: "pipe"
|
|
3904
3865
|
});
|
|
3905
3866
|
} catch {
|
|
3906
3867
|
try {
|
|
3907
|
-
|
|
3868
|
+
execFileSync2("git", ["worktree", "add", request.workingDirectory, request.workingDirectory], {
|
|
3908
3869
|
stdio: "pipe"
|
|
3909
3870
|
});
|
|
3910
3871
|
} catch {
|
|
@@ -3913,18 +3874,18 @@ var GeminiProvider = class _GeminiProvider {
|
|
|
3913
3874
|
}
|
|
3914
3875
|
} else {
|
|
3915
3876
|
try {
|
|
3916
|
-
|
|
3877
|
+
execFileSync2("git", ["-C", request.workingDirectory, "rev-parse", "--git-dir"], {
|
|
3917
3878
|
stdio: "pipe"
|
|
3918
3879
|
});
|
|
3919
3880
|
} catch {
|
|
3920
3881
|
try {
|
|
3921
3882
|
rmSync3(request.workingDirectory, { recursive: true, force: true });
|
|
3922
|
-
|
|
3883
|
+
execFileSync2("git", ["worktree", "add", "-b", request.workingDirectory, request.workingDirectory, "HEAD"], {
|
|
3923
3884
|
stdio: "pipe"
|
|
3924
3885
|
});
|
|
3925
3886
|
} catch {
|
|
3926
3887
|
try {
|
|
3927
|
-
|
|
3888
|
+
execFileSync2("git", ["worktree", "add", request.workingDirectory, request.workingDirectory], {
|
|
3928
3889
|
stdio: "pipe"
|
|
3929
3890
|
});
|
|
3930
3891
|
} catch {
|
|
@@ -3942,7 +3903,7 @@ var GeminiProvider = class _GeminiProvider {
|
|
|
3942
3903
|
if (request.workingDirectory && settingsDir) {
|
|
3943
3904
|
this.copySettingsToDir(settingsDir, request.workingDirectory);
|
|
3944
3905
|
}
|
|
3945
|
-
return new Promise((
|
|
3906
|
+
return new Promise((resolve2) => {
|
|
3946
3907
|
const [cmd, prefixArgs] = resolveCommand("gemini");
|
|
3947
3908
|
const resolvedArgs = [...prefixArgs, ...args];
|
|
3948
3909
|
const child = spawn3(cmd, resolvedArgs, {
|
|
@@ -4015,7 +3976,7 @@ var GeminiProvider = class _GeminiProvider {
|
|
|
4015
3976
|
durationMs: Date.now() - startTime
|
|
4016
3977
|
};
|
|
4017
3978
|
if (degraded.length > 0) result.degradedCapabilities = degraded;
|
|
4018
|
-
|
|
3979
|
+
resolve2(result);
|
|
4019
3980
|
};
|
|
4020
3981
|
child.on("close", (code) => finish(code));
|
|
4021
3982
|
child.on("error", (err) => finish(1, err.message));
|
|
@@ -4182,6 +4143,18 @@ node "${hookScript}" "${event}" "${configFile}" < "$STDIN_FILE"
|
|
|
4182
4143
|
};
|
|
4183
4144
|
|
|
4184
4145
|
// src/commands/pipeline.ts
|
|
4146
|
+
var execFileAsync = promisify(execFile);
|
|
4147
|
+
var _cachedPrimaryRepoRoot = null;
|
|
4148
|
+
async function getPrimaryRepoRoot() {
|
|
4149
|
+
if (_cachedPrimaryRepoRoot) return _cachedPrimaryRepoRoot;
|
|
4150
|
+
try {
|
|
4151
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"]);
|
|
4152
|
+
_cachedPrimaryRepoRoot = resolve(stdout.trim());
|
|
4153
|
+
} catch {
|
|
4154
|
+
_cachedPrimaryRepoRoot = resolve(process.cwd());
|
|
4155
|
+
}
|
|
4156
|
+
return _cachedPrimaryRepoRoot;
|
|
4157
|
+
}
|
|
4185
4158
|
function createProviderRegistry() {
|
|
4186
4159
|
const registry = new ProviderRegistry();
|
|
4187
4160
|
registry.register(new ClaudeProvider());
|
|
@@ -4398,14 +4371,14 @@ Press Ctrl+C to cancel, or re-run with --yes to skip this warning.
|
|
|
4398
4371
|
`);
|
|
4399
4372
|
}
|
|
4400
4373
|
function waitForConfirmation() {
|
|
4401
|
-
return new Promise((
|
|
4374
|
+
return new Promise((resolve2) => {
|
|
4402
4375
|
process.stdout.write("Continue? [y/N] ");
|
|
4403
4376
|
process.stdin.setEncoding("utf8");
|
|
4404
4377
|
process.stdin.once("data", (data) => {
|
|
4405
4378
|
const answer = data.trim().toLowerCase();
|
|
4406
|
-
|
|
4379
|
+
resolve2(answer === "y" || answer === "yes");
|
|
4407
4380
|
});
|
|
4408
|
-
process.stdin.once("end", () =>
|
|
4381
|
+
process.stdin.once("end", () => resolve2(false));
|
|
4409
4382
|
});
|
|
4410
4383
|
}
|
|
4411
4384
|
async function runPipeline(client, args) {
|
|
@@ -4483,6 +4456,32 @@ async function runPipeline(client, args) {
|
|
|
4483
4456
|
...opts.maxBudget !== null && { maxBudgetUsd: opts.maxBudget },
|
|
4484
4457
|
...opts.model !== null && { model: opts.model }
|
|
4485
4458
|
};
|
|
4459
|
+
if (effectiveConfig.worktreePath) {
|
|
4460
|
+
const primaryRepoRoot = await getPrimaryRepoRoot();
|
|
4461
|
+
const resolvedCwd = resolve(effectiveConfig.worktreePath);
|
|
4462
|
+
const inside = resolvedCwd === primaryRepoRoot || resolvedCwd.startsWith(primaryRepoRoot + sep) || primaryRepoRoot.startsWith(resolvedCwd + sep);
|
|
4463
|
+
if (inside) {
|
|
4464
|
+
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.`;
|
|
4465
|
+
logger.orchestrator(errMsg);
|
|
4466
|
+
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.`);
|
|
4467
|
+
return { reason: "error", iterations: 0, gutterCount: 0, lastError: errMsg };
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
4470
|
+
if (effectiveConfig.worktreePath) {
|
|
4471
|
+
const parent = dirname2(effectiveConfig.worktreePath);
|
|
4472
|
+
try {
|
|
4473
|
+
await fsPromises.mkdir(parent, { recursive: true });
|
|
4474
|
+
} catch (err) {
|
|
4475
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4476
|
+
const errMsg = `[dispatch] failed ticket=${ticketId} reason=worktree_unrenderable path="${effectiveConfig.worktreePath}" error="${msg}"`;
|
|
4477
|
+
logger.orchestrator(errMsg);
|
|
4478
|
+
await deps.createComment(
|
|
4479
|
+
ticketId,
|
|
4480
|
+
`[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.`
|
|
4481
|
+
);
|
|
4482
|
+
return { reason: "error", iterations: 0, gutterCount: 0, lastError: errMsg };
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4486
4485
|
const mem = runMemory;
|
|
4487
4486
|
const colScopeForName = await client.get(
|
|
4488
4487
|
`/projects/${projectId}/pipeline-context`,
|
|
@@ -4682,7 +4681,27 @@ async function runPipeline(client, args) {
|
|
|
4682
4681
|
const blockers = await client.get(
|
|
4683
4682
|
`/projects/${projectId}/tickets/${ticketId}/unresolved-blockers`
|
|
4684
4683
|
);
|
|
4685
|
-
|
|
4684
|
+
if (!blockers || blockers.length === 0) {
|
|
4685
|
+
return { blocked: false };
|
|
4686
|
+
}
|
|
4687
|
+
let linkIds = [];
|
|
4688
|
+
try {
|
|
4689
|
+
const links = await client.get(
|
|
4690
|
+
`/projects/${projectId}/ticket-links?ticketId=${ticketId}&linkType=blocks`
|
|
4691
|
+
);
|
|
4692
|
+
linkIds = (links ?? []).filter((l) => l.target_id === ticketId && l.link_type === "blocks").map((l) => l.id);
|
|
4693
|
+
} catch {
|
|
4694
|
+
linkIds = [];
|
|
4695
|
+
}
|
|
4696
|
+
return {
|
|
4697
|
+
blocked: true,
|
|
4698
|
+
blockers: blockers.map((b) => ({
|
|
4699
|
+
id: b.id,
|
|
4700
|
+
ticket_number: b.ticket_number,
|
|
4701
|
+
column: b.column
|
|
4702
|
+
})),
|
|
4703
|
+
linkIds
|
|
4704
|
+
};
|
|
4686
4705
|
},
|
|
4687
4706
|
dispatchLightCall: async (ticketId, columnId) => {
|
|
4688
4707
|
const [ticketCtx, colScope, boardScope] = await Promise.all([
|
|
@@ -4796,6 +4815,7 @@ async function runPipeline(client, args) {
|
|
|
4796
4815
|
appendRunMemory: (section, content) => runMemory ? runMemory.append(section, content) : Promise.resolve(),
|
|
4797
4816
|
cleanupWorktree: (name) => cleanupWorktree(name),
|
|
4798
4817
|
mergeWorktree: (name, integrationBranch) => mergeWorktreeBranch(name, integrationBranch),
|
|
4818
|
+
detectBranchMerged: async (opts2) => detectBranchMerged(opts2),
|
|
4799
4819
|
// Pipeline event emission — wsClient captured by reference (set later before any loops run)
|
|
4800
4820
|
emitPipelineEvent: (event) => {
|
|
4801
4821
|
wsClient?.send(event);
|
|
@@ -5127,7 +5147,7 @@ async function waitForAllLoops(orchestrator, timeoutMs = 4 * 60 * 60 * 1e3) {
|
|
|
5127
5147
|
);
|
|
5128
5148
|
return;
|
|
5129
5149
|
}
|
|
5130
|
-
await new Promise((
|
|
5150
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3));
|
|
5131
5151
|
if (activeRalphLoops.size === 0 && orchestrator.activeLoopCount === 0 && !orchestrator.hasActiveQueuedWork && !orchestrator.hasCompletingWork) {
|
|
5132
5152
|
consecutiveIdle++;
|
|
5133
5153
|
} else {
|
|
@@ -5177,4 +5197,4 @@ export {
|
|
|
5177
5197
|
runPipeline,
|
|
5178
5198
|
stopPipeline
|
|
5179
5199
|
};
|
|
5180
|
-
//# sourceMappingURL=pipeline-
|
|
5200
|
+
//# sourceMappingURL=pipeline-NRG2Q2TE.js.map
|