@sideboard-ai/core 0.1.242 → 0.1.243
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/{abletime-VSV6AUBV.js → abletime-CWIJ7INC.js} +4 -4
- package/dist/{abletime-UX4VUENQ.js → abletime-OI6FNSXX.js} +4 -4
- package/dist/{abletime-oauth-DKBUZV2C.js → abletime-oauth-CHVTUZBR.js} +3 -3
- package/dist/{abletime-oauth-QMDV45JC.js → abletime-oauth-PUIPZTA4.js} +3 -3
- package/dist/{abletime-rest-NL56UVK5.js → abletime-rest-32NIVXAY.js} +3 -3
- package/dist/{abletime-rest-VPBC2XS5.js → abletime-rest-APJUKFKL.js} +3 -3
- package/dist/{agents-CDQCQVWU.js → agents-CPME6O2H.js} +9 -5
- package/dist/{agents-PBRIHUYM.js → agents-VKO5UZO6.js} +9 -5
- package/dist/{app-settings-AGDLF75Z.js → app-settings-652J57CX.js} +1 -1
- package/dist/{app-settings-I27UBVI4.js → app-settings-VOPXJMGV.js} +1 -1
- package/dist/{chunk-BILEJG3V.js → chunk-3P5OCI2X.js} +3 -3
- package/dist/{chunk-3PGFAVFL.js → chunk-43F44EKX.js} +2 -2
- package/dist/{chunk-3AJUH66R.js → chunk-6I5YQTNK.js} +2 -1
- package/dist/{chunk-NJ5OXOTN.js → chunk-7HOKP4GR.js} +2 -1
- package/dist/{chunk-TB6UMHIF.js → chunk-7UCZ4DRF.js} +2 -2
- package/dist/{chunk-DXY4K5MP.js → chunk-BL2UGKWU.js} +3 -3
- package/dist/{chunk-CWHMLCLN.js → chunk-C64YQAA6.js} +3 -3
- package/dist/{chunk-O3UAQAYA.js → chunk-CCD2QIFK.js} +2 -2
- package/dist/{chunk-LRMJ6V6U.js → chunk-DARSRQUE.js} +1503 -1462
- package/dist/{chunk-OKHOYI53.js → chunk-ECZTXNYR.js} +3 -3
- package/dist/{chunk-CRAXJAYJ.js → chunk-G45QP2ZD.js} +3 -3
- package/dist/{chunk-3Q4PVZ3U.js → chunk-HR4RNC73.js} +32 -5
- package/dist/{chunk-YXOOSFAF.js → chunk-IMO7AZI4.js} +2 -2
- package/dist/{chunk-QXYWLO6W.js → chunk-ISZERVDT.js} +28 -5
- package/dist/{chunk-R3YKDTVE.js → chunk-NEOAHPMK.js} +1569 -1526
- package/dist/{chunk-DJU54RYW.js → chunk-PGPSIEMW.js} +13 -0
- package/dist/{chunk-P3DCZRER.js → chunk-PPJ5JTWA.js} +1 -0
- package/dist/{chunk-5ELC3KK3.js → chunk-QNJXJIWJ.js} +5 -5
- package/dist/{chunk-JFOPOMK7.js → chunk-WDDUIGFK.js} +13 -0
- package/dist/{chunk-IVXPXYH2.js → chunk-WURF67ID.js} +5 -5
- package/dist/{chunk-QLWMHQPH.js → chunk-YLGBQVRK.js} +1 -0
- package/dist/{coordinator-prompt-5UINTEA6.js → coordinator-prompt-AJ3E6LBX.js} +4 -4
- package/dist/{coordinator-prompt-ATK73HYU.js → coordinator-prompt-PYIRQGU7.js} +4 -4
- package/dist/{global-workspace-TD5SARNW.js → global-workspace-GIQ6F6IJ.js} +4 -4
- package/dist/{global-workspace-ZJBBICFJ.js → global-workspace-GQM653DP.js} +4 -4
- package/dist/index.cjs +574 -464
- package/dist/index.d.cts +43 -3
- package/dist/index.d.ts +43 -3
- package/dist/index.js +70 -38
- package/dist/mcp/run-stdio.cjs +1633 -1544
- package/dist/mcp/run-stdio.js +50 -36
- package/dist/{orchestrator-66ZFQHW5.js → orchestrator-3IOISDFS.js} +7 -7
- package/dist/{orchestrator-VYNPNHY4.js → orchestrator-YDPDMXUY.js} +7 -7
- package/dist/{plan-file-UUKCZEYP.js → plan-file-7MMIPV7Y.js} +9 -1
- package/dist/{plan-file-2EPHYRX7.js → plan-file-CKXUFTQ2.js} +9 -1
- package/dist/{thread-store-YOPKY74K.js → thread-store-JQB7KZJR.js} +1 -1
- package/dist/{thread-store-AYPBEZT5.js → thread-store-SCV2IZYS.js} +1 -1
- package/dist/{workspaces-4SFSCYKT.js → workspaces-OIBSD5B6.js} +4 -4
- package/dist/{workspaces-JX5R4V24.js → workspaces-WYC5TLC3.js} +4 -4
- package/dist/{worktree-Y5FES5D6.js → worktree-5VRZVGP3.js} +3 -3
- package/dist/{worktree-AGVPKDBH.js → worktree-DGDM7LGV.js} +3 -3
- package/package.json +1 -1
|
@@ -2,13 +2,13 @@ import {
|
|
|
2
2
|
extractPresentedPlan,
|
|
3
3
|
readPlanFile,
|
|
4
4
|
writePlanFile
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-PGPSIEMW.js";
|
|
6
6
|
import {
|
|
7
|
-
PLAN_MODE_INSTRUCTION,
|
|
8
7
|
adjustBrightsyTurnExit,
|
|
9
8
|
allAdapters,
|
|
10
9
|
applyAgentEvent,
|
|
11
10
|
clipAgentEventForPaint,
|
|
11
|
+
compactSummaryPart,
|
|
12
12
|
finalizeParts,
|
|
13
13
|
getAdapter,
|
|
14
14
|
isBrightsyNdjsonLine,
|
|
@@ -21,10 +21,11 @@ import {
|
|
|
21
21
|
parseBrightsyCliLine,
|
|
22
22
|
parseSessionQuotaResetAt,
|
|
23
23
|
partsToAssistantText,
|
|
24
|
+
planModeTurnInstruction,
|
|
24
25
|
resolveQuotaFallbackAgent,
|
|
25
26
|
stripBrightsyNdjsonNoise,
|
|
26
27
|
toolDescription
|
|
27
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-ISZERVDT.js";
|
|
28
29
|
import {
|
|
29
30
|
SLACK_REPLY_FORMATTING,
|
|
30
31
|
addWorkspace,
|
|
@@ -47,7 +48,7 @@ import {
|
|
|
47
48
|
stageAbsolutePathsAsAttachments,
|
|
48
49
|
stageBuffersAsAttachments,
|
|
49
50
|
syncWorkspacesFromThreads
|
|
50
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-3P5OCI2X.js";
|
|
51
52
|
import {
|
|
52
53
|
addPrStackLayer,
|
|
53
54
|
allocateTeamName,
|
|
@@ -96,7 +97,7 @@ import {
|
|
|
96
97
|
worktreeDisplayLabelForGroup,
|
|
97
98
|
worktreeNameFromPath,
|
|
98
99
|
worktreeSlugForTicket
|
|
99
|
-
} from "./chunk-
|
|
100
|
+
} from "./chunk-CCD2QIFK.js";
|
|
100
101
|
import {
|
|
101
102
|
ATTACHMENTS_DIR,
|
|
102
103
|
DETACHED_JOBS_DIR,
|
|
@@ -159,7 +160,7 @@ import {
|
|
|
159
160
|
sumUsageList,
|
|
160
161
|
updateAdvancedSettings,
|
|
161
162
|
usageOnLimit
|
|
162
|
-
} from "./chunk-
|
|
163
|
+
} from "./chunk-PPJ5JTWA.js";
|
|
163
164
|
import {
|
|
164
165
|
examplePrefixedBranch,
|
|
165
166
|
sanitizeGitBranchPrefix
|
|
@@ -181,7 +182,7 @@ import {
|
|
|
181
182
|
updateThread,
|
|
182
183
|
withThreadLock,
|
|
183
184
|
writeThread
|
|
184
|
-
} from "./chunk-
|
|
185
|
+
} from "./chunk-6I5YQTNK.js";
|
|
185
186
|
import {
|
|
186
187
|
annotateDevAccessRuns,
|
|
187
188
|
appDataDir,
|
|
@@ -441,9 +442,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
441
442
|
`Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
|
|
442
443
|
);
|
|
443
444
|
}
|
|
444
|
-
const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-
|
|
445
|
+
const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-GQM653DP.js");
|
|
445
446
|
if (isGlobalThread2(thread)) {
|
|
446
|
-
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-
|
|
447
|
+
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-AJ3E6LBX.js");
|
|
447
448
|
ensureGlobalCoordinatorCwd2(
|
|
448
449
|
isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
|
|
449
450
|
);
|
|
@@ -744,15 +745,15 @@ function shouldAutoArchiveOnPrMerge(opts) {
|
|
|
744
745
|
// src/hook/conductor.ts
|
|
745
746
|
import {
|
|
746
747
|
copyFileSync,
|
|
747
|
-
existsSync as
|
|
748
|
-
mkdirSync as
|
|
749
|
-
readdirSync,
|
|
750
|
-
readFileSync as
|
|
748
|
+
existsSync as existsSync4,
|
|
749
|
+
mkdirSync as mkdirSync5,
|
|
750
|
+
readdirSync as readdirSync2,
|
|
751
|
+
readFileSync as readFileSync4,
|
|
751
752
|
realpathSync
|
|
752
753
|
} from "fs";
|
|
753
754
|
import { execFileSync } from "child_process";
|
|
754
755
|
import { createServer } from "net";
|
|
755
|
-
import { basename, dirname as
|
|
756
|
+
import { basename, dirname as dirname4, join as join6, resolve, sep } from "path";
|
|
756
757
|
import { execa as execa2 } from "execa";
|
|
757
758
|
import { createInterface as createInterface2 } from "readline";
|
|
758
759
|
|
|
@@ -1150,12 +1151,38 @@ ${match.text}`, "");
|
|
|
1150
1151
|
blocks.push("Continue from this context. Do not repeat the summary unless asked.");
|
|
1151
1152
|
return blocks.join("\n");
|
|
1152
1153
|
}
|
|
1154
|
+
function compactArchiveProse(message) {
|
|
1155
|
+
if (message.origin === "continue") return null;
|
|
1156
|
+
if (message.role === "summary") return null;
|
|
1157
|
+
let text = (message.text ?? "").trim();
|
|
1158
|
+
if (!text && message.role === "agent") {
|
|
1159
|
+
text = lastAssistantMessageText(message.parts).trim();
|
|
1160
|
+
}
|
|
1161
|
+
if (!text || isInternalAgentStatusText(text)) return null;
|
|
1162
|
+
return { role: message.role, text, ts: message.ts };
|
|
1163
|
+
}
|
|
1164
|
+
function messagesForCompactArchive(older) {
|
|
1165
|
+
const out = [];
|
|
1166
|
+
for (const message of older) {
|
|
1167
|
+
const prose = compactArchiveProse(message);
|
|
1168
|
+
if (prose) out.push(prose);
|
|
1169
|
+
}
|
|
1170
|
+
return out;
|
|
1171
|
+
}
|
|
1172
|
+
function appendCompactedMessages(existing, older) {
|
|
1173
|
+
const next = messagesForCompactArchive(older);
|
|
1174
|
+
if (!existing?.length) return next;
|
|
1175
|
+
if (!next.length) return existing;
|
|
1176
|
+
return [...existing, ...next];
|
|
1177
|
+
}
|
|
1153
1178
|
function applyCompaction(messages, summaryText, thresholds = {}) {
|
|
1154
1179
|
const { older, recent } = splitForCompaction(messages, thresholds);
|
|
1155
1180
|
if (older.length === 0) return messages;
|
|
1181
|
+
const text = summaryText.trim();
|
|
1156
1182
|
const summary = {
|
|
1157
1183
|
role: "summary",
|
|
1158
|
-
text
|
|
1184
|
+
text,
|
|
1185
|
+
parts: [compactSummaryPart(text)],
|
|
1159
1186
|
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
1160
1187
|
};
|
|
1161
1188
|
return [summary, ...recent];
|
|
@@ -1183,6 +1210,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
|
|
|
1183
1210
|
cwd: thread.worktreePath
|
|
1184
1211
|
});
|
|
1185
1212
|
let messages = applyCompaction(thread.messages, summary, thresholds);
|
|
1213
|
+
const compactedMessages = appendCompactedMessages(thread.compactedMessages, older);
|
|
1186
1214
|
const resetSession = shouldResetSessionForOccupancy({ messages: thread.messages });
|
|
1187
1215
|
if (resetSession) {
|
|
1188
1216
|
messages = applyForwardOccupancy(messages);
|
|
@@ -1190,6 +1218,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
|
|
|
1190
1218
|
const next = {
|
|
1191
1219
|
...thread,
|
|
1192
1220
|
messages,
|
|
1221
|
+
...compactedMessages.length ? { compactedMessages } : { compactedMessages: void 0 },
|
|
1193
1222
|
sessionId: resetSession ? null : thread.sessionId,
|
|
1194
1223
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1195
1224
|
};
|
|
@@ -1725,1664 +1754,1669 @@ function saveWorktreeRunPorts(worktreePath, scriptName, ports) {
|
|
|
1725
1754
|
`, "utf8");
|
|
1726
1755
|
}
|
|
1727
1756
|
|
|
1728
|
-
// src/
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
function
|
|
1742
|
-
const
|
|
1743
|
-
if (
|
|
1744
|
-
|
|
1745
|
-
if (settings?.filesToCopy?.length) return settings.filesToCopy;
|
|
1746
|
-
if (settings?.fileIncludeGlobs?.length) {
|
|
1747
|
-
const matched = [];
|
|
1748
|
-
try {
|
|
1749
|
-
for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
|
|
1750
|
-
if (!entry.isFile()) continue;
|
|
1751
|
-
for (const glob of settings.fileIncludeGlobs) {
|
|
1752
|
-
if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob(basename(glob), entry.name)) {
|
|
1753
|
-
matched.push(entry.name);
|
|
1754
|
-
break;
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
} catch {
|
|
1759
|
-
}
|
|
1760
|
-
if (matched.length) return [...new Set(matched)];
|
|
1761
|
-
}
|
|
1762
|
-
const defaults = [];
|
|
1763
|
-
try {
|
|
1764
|
-
for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
|
|
1765
|
-
if (entry.isFile() && entry.name.startsWith(".env")) {
|
|
1766
|
-
defaults.push(entry.name);
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
} catch {
|
|
1770
|
-
}
|
|
1771
|
-
if (defaults.length) return defaults;
|
|
1772
|
-
return [".env.local", ".env"];
|
|
1773
|
-
}
|
|
1774
|
-
function copyConfiguredFiles(repoPath, worktreePath) {
|
|
1775
|
-
const patterns = resolveFilesToCopy(repoPath);
|
|
1776
|
-
const copied = [];
|
|
1777
|
-
for (const rel of patterns) {
|
|
1778
|
-
const src = join5(repoPath, rel);
|
|
1779
|
-
if (!existsSync3(src)) continue;
|
|
1780
|
-
const dest = join5(worktreePath, rel);
|
|
1781
|
-
mkdirSync4(dirname3(dest), { recursive: true });
|
|
1782
|
-
copyFileSync(src, dest);
|
|
1783
|
-
copied.push(rel);
|
|
1784
|
-
}
|
|
1785
|
-
return copied;
|
|
1786
|
-
}
|
|
1787
|
-
var cachedLoginEnv = null;
|
|
1788
|
-
async function captureLoginEnv() {
|
|
1789
|
-
if (cachedLoginEnv) return { ...cachedLoginEnv };
|
|
1790
|
-
const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
|
|
1791
|
-
try {
|
|
1792
|
-
const result = await execa2(shell, ["-l", "-c", "env -0"], {
|
|
1793
|
-
reject: false,
|
|
1794
|
-
timeout: 5e3
|
|
1795
|
-
});
|
|
1796
|
-
if (result.exitCode === 0 && result.stdout) {
|
|
1797
|
-
const env = { ...process.env };
|
|
1798
|
-
for (const entry of result.stdout.split("\0")) {
|
|
1799
|
-
if (!entry) continue;
|
|
1800
|
-
const eq = entry.indexOf("=");
|
|
1801
|
-
if (eq <= 0) continue;
|
|
1802
|
-
const key = entry.slice(0, eq);
|
|
1803
|
-
const value = entry.slice(eq + 1);
|
|
1804
|
-
if (key) env[key] = value;
|
|
1805
|
-
}
|
|
1806
|
-
cachedLoginEnv = env;
|
|
1807
|
-
return { ...env };
|
|
1808
|
-
}
|
|
1809
|
-
} catch {
|
|
1810
|
-
}
|
|
1811
|
-
cachedLoginEnv = { ...process.env };
|
|
1812
|
-
return { ...cachedLoginEnv };
|
|
1757
|
+
// src/mcp/wait-for-job.ts
|
|
1758
|
+
import {
|
|
1759
|
+
appendFileSync as appendFileSync2,
|
|
1760
|
+
existsSync as existsSync3,
|
|
1761
|
+
mkdirSync as mkdirSync4,
|
|
1762
|
+
readdirSync,
|
|
1763
|
+
readFileSync as readFileSync3,
|
|
1764
|
+
writeFileSync as writeFileSync3
|
|
1765
|
+
} from "fs";
|
|
1766
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
1767
|
+
|
|
1768
|
+
// src/mcp/wait-for-turn.ts
|
|
1769
|
+
var MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
|
|
1770
|
+
function mcpWaitForTurnTimeoutMs(requested) {
|
|
1771
|
+
const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
|
|
1772
|
+
if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
|
|
1773
|
+
return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
|
|
1813
1774
|
}
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
env.PORT = String(primary);
|
|
1835
|
-
for (let i = 1; i < ports.length; i++) {
|
|
1836
|
-
const p = ports[i];
|
|
1837
|
-
env[`SIDEBOARD_PORT_${i}`] = String(p);
|
|
1838
|
-
env[`CONDUCTOR_PORT_${i}`] = String(p);
|
|
1839
|
-
}
|
|
1775
|
+
var MCP_WAIT_STILL_RUNNING_HINT = "Child is still working. Call wait_for_turn again. Do not send_to_thread a check-in (that steers / interrupts) or assume a hang while progress is updating.";
|
|
1776
|
+
var MCP_WAIT_QUEUED_HINT = "Child is queued waiting for a concurrency slot \u2014 it has not started yet. Call wait_for_turn again. Do not send_to_thread a check-in (that steers / interrupts), force_stop, or assume it failed to start.";
|
|
1777
|
+
var MCP_WAIT_STOPPED_HINT = "Child was stopped before the turn finished. Do not treat this as success. send_to_thread to resume, or tell the user.";
|
|
1778
|
+
var MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
|
|
1779
|
+
var MCP_WAIT_ERROR_HINT = "Child turn failed. lastError/text is the failure \u2014 switch agent, tell the user, or retry. Do not treat empty text as success.";
|
|
1780
|
+
var MCP_WAIT_INPUT_REQUIRED_HINT = "Child asked the user a question (ask_user). taskState is input-required. Wait for the user to answer in that chat \u2014 do not send_to_thread a check-in.";
|
|
1781
|
+
function mcpWaitTaskHint(taskState, status) {
|
|
1782
|
+
switch (taskState) {
|
|
1783
|
+
case "submitted":
|
|
1784
|
+
return MCP_WAIT_QUEUED_HINT;
|
|
1785
|
+
case "working":
|
|
1786
|
+
return MCP_WAIT_STILL_RUNNING_HINT;
|
|
1787
|
+
case "input-required":
|
|
1788
|
+
return MCP_WAIT_INPUT_REQUIRED_HINT;
|
|
1789
|
+
case "canceled":
|
|
1790
|
+
return MCP_WAIT_STOPPED_HINT;
|
|
1791
|
+
case "failed":
|
|
1792
|
+
return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
|
|
1793
|
+
default:
|
|
1794
|
+
return void 0;
|
|
1840
1795
|
}
|
|
1841
|
-
applyWorktreePkgCacheEnv(env, opts.worktreePath);
|
|
1842
|
-
return env;
|
|
1843
1796
|
}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1797
|
+
|
|
1798
|
+
// src/mcp/wait-for-job.ts
|
|
1799
|
+
var MAX_JOB_CONTINUES = 8;
|
|
1800
|
+
var MAX_JOB_DELTA_LINES = 80;
|
|
1801
|
+
var MAX_JOB_DELTA_CHARS = 4e3;
|
|
1802
|
+
var DEFERRED_DONE_MAX_CHARS = 480;
|
|
1803
|
+
var GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
|
|
1804
|
+
var MCP_WAIT_JOB_STILL_RUNNING_HINT = "Job is still running. The type=log pane already appended this delta. Call wait_for_job again. If it is hanging, producing no useful output, or doing the wrong thing, call stop_job (same id) instead of looping forever. Do not end the turn or tell the user you will let them know later.";
|
|
1805
|
+
var MCP_STOP_JOB_HINT = "Stopped. The type=log pane is status=failed with the last delta. Do not wait again unless you start a new command.";
|
|
1806
|
+
var JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
|
|
1807
|
+
function mcpWaitForJobTimeoutMs(requested) {
|
|
1808
|
+
return mcpWaitForTurnTimeoutMs(requested);
|
|
1848
1809
|
}
|
|
1849
|
-
function
|
|
1850
|
-
const
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
if (!Number.isFinite(pid) || pid <= 0 || pid === selfPid) continue;
|
|
1854
|
-
pids.push(pid);
|
|
1810
|
+
function sanitizeDetachedJobId(id) {
|
|
1811
|
+
const s = id.trim();
|
|
1812
|
+
if (!JOB_ID_RE.test(s)) {
|
|
1813
|
+
throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
|
|
1855
1814
|
}
|
|
1856
|
-
return
|
|
1815
|
+
return s;
|
|
1857
1816
|
}
|
|
1858
|
-
function
|
|
1817
|
+
function jobAlive(pid) {
|
|
1818
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1859
1819
|
try {
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1863
|
-
timeout: 2e3
|
|
1864
|
-
});
|
|
1865
|
-
return killableListenerPids(raw, selfPid);
|
|
1820
|
+
process.kill(pid, 0);
|
|
1821
|
+
return true;
|
|
1866
1822
|
} catch {
|
|
1867
|
-
return
|
|
1823
|
+
return false;
|
|
1868
1824
|
}
|
|
1869
1825
|
}
|
|
1870
|
-
function
|
|
1871
|
-
if (
|
|
1826
|
+
function processGroupAlive(pgid) {
|
|
1827
|
+
if (process.platform === "win32") return false;
|
|
1828
|
+
if (!Number.isInteger(pgid) || pgid <= 1) return false;
|
|
1872
1829
|
try {
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
});
|
|
1878
|
-
for (const line of raw.split("\n")) {
|
|
1879
|
-
if (line.startsWith("n") && line.length > 1) return line.slice(1);
|
|
1880
|
-
}
|
|
1881
|
-
} catch {
|
|
1830
|
+
process.kill(-pgid, 0);
|
|
1831
|
+
return true;
|
|
1832
|
+
} catch (err) {
|
|
1833
|
+
return err && typeof err === "object" && "code" in err && err.code === "EPERM";
|
|
1882
1834
|
}
|
|
1883
|
-
return null;
|
|
1884
1835
|
}
|
|
1885
|
-
function
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
} catch {
|
|
1889
|
-
return resolve(path);
|
|
1890
|
-
}
|
|
1836
|
+
function jobTreeAlive(pid) {
|
|
1837
|
+
if (pid == null) return false;
|
|
1838
|
+
return jobAlive(pid) || processGroupAlive(pid);
|
|
1891
1839
|
}
|
|
1892
|
-
function
|
|
1893
|
-
|
|
1894
|
-
const
|
|
1895
|
-
|
|
1896
|
-
const parent = dirname3(wt);
|
|
1897
|
-
const thisName = basename(wt);
|
|
1898
|
-
if (!thisName || c === parent) return false;
|
|
1899
|
-
if (!c.startsWith(parent + sep)) return false;
|
|
1900
|
-
const first = c.slice(parent.length + 1).split(sep)[0];
|
|
1901
|
-
return Boolean(first) && first !== thisName;
|
|
1840
|
+
function readIntFile(file) {
|
|
1841
|
+
if (!existsSync3(file)) return null;
|
|
1842
|
+
const n = Number.parseInt(readFileSync3(file, "utf8").trim(), 10);
|
|
1843
|
+
return Number.isInteger(n) ? n : null;
|
|
1902
1844
|
}
|
|
1903
|
-
function
|
|
1904
|
-
|
|
1905
|
-
process.kill(pid, signal);
|
|
1906
|
-
} catch {
|
|
1907
|
-
}
|
|
1845
|
+
function jobDir(root, id, legacy = false) {
|
|
1846
|
+
return join5(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
|
|
1908
1847
|
}
|
|
1909
|
-
function
|
|
1910
|
-
const
|
|
1911
|
-
|
|
1912
|
-
|
|
1848
|
+
function resolveJobDir(root, id) {
|
|
1849
|
+
const modern = jobDir(root, id, false);
|
|
1850
|
+
if (existsSync3(modern)) return modern;
|
|
1851
|
+
const legacy = jobDir(root, id, true);
|
|
1852
|
+
if (existsSync3(legacy)) return legacy;
|
|
1853
|
+
return modern;
|
|
1913
1854
|
}
|
|
1914
|
-
function
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
try {
|
|
1919
|
-
process.kill(-pid, "SIGTERM");
|
|
1920
|
-
} catch {
|
|
1921
|
-
signalPid(pid, "SIGTERM");
|
|
1922
|
-
}
|
|
1855
|
+
function listJobIdsIn(root, rel) {
|
|
1856
|
+
const dir = join5(root, rel);
|
|
1857
|
+
if (!existsSync3(dir)) return [];
|
|
1858
|
+
return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && JOB_ID_RE.test(e.name)).map((e) => e.name);
|
|
1923
1859
|
}
|
|
1924
|
-
function
|
|
1925
|
-
const
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
],
|
|
1937
|
-
{ stdio: "ignore", timeout: 3e3 }
|
|
1938
|
-
);
|
|
1939
|
-
continue;
|
|
1940
|
-
}
|
|
1941
|
-
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
1942
|
-
signalPid(pid, "SIGTERM");
|
|
1943
|
-
}
|
|
1944
|
-
} catch {
|
|
1945
|
-
}
|
|
1860
|
+
function listRunningDetachedJobs(worktreePath) {
|
|
1861
|
+
const root = worktreePath.trim();
|
|
1862
|
+
if (!root) return [];
|
|
1863
|
+
const ids = /* @__PURE__ */ new Set([
|
|
1864
|
+
...listJobIdsIn(root, DETACHED_JOBS_DIR),
|
|
1865
|
+
...listJobIdsIn(root, LEGACY_DETACHED_JOBS_DIR)
|
|
1866
|
+
]);
|
|
1867
|
+
const running = [];
|
|
1868
|
+
for (const id of ids) {
|
|
1869
|
+
const dir = resolveJobDir(root, id);
|
|
1870
|
+
const pid = readIntFile(join5(dir, "pid"));
|
|
1871
|
+
if (jobTreeAlive(pid)) running.push(id);
|
|
1946
1872
|
}
|
|
1873
|
+
return running.sort();
|
|
1947
1874
|
}
|
|
1948
|
-
function
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
killListenersOnPorts(ports);
|
|
1955
|
-
return;
|
|
1956
|
-
}
|
|
1957
|
-
const selfPid = process.pid;
|
|
1958
|
-
for (const port of ports) {
|
|
1959
|
-
if (!Number.isFinite(port) || port <= 0) continue;
|
|
1960
|
-
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
1961
|
-
const cwd = listenerCwd(pid);
|
|
1962
|
-
if (cwd && isOtherWorktreeListenerCwd(cwd, worktreePath)) continue;
|
|
1963
|
-
signalPid(pid, signal);
|
|
1964
|
-
}
|
|
1965
|
-
}
|
|
1875
|
+
function looksLikeDeferredDonePromise(text) {
|
|
1876
|
+
const t = (text ?? "").trim();
|
|
1877
|
+
if (!t || t.length > DEFERRED_DONE_MAX_CHARS) return false;
|
|
1878
|
+
const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
|
|
1879
|
+
if (!unquoted) return false;
|
|
1880
|
+
return /\b(i['’]?ll|i will)\s+let you know\b/i.test(unquoted) || /\blet you know when\b/i.test(unquoted) || /\b(check back|ping me)\s+when\b/i.test(unquoted) || /\bi(?:['’]ll| will)\s+(report|update you)\s+when\b/i.test(unquoted);
|
|
1966
1881
|
}
|
|
1967
|
-
function
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
if (process.platform === "win32") {
|
|
1972
|
-
void execa2("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
|
|
1973
|
-
} else {
|
|
1974
|
-
process.kill(-pid, "SIGTERM");
|
|
1975
|
-
setTimeout(() => {
|
|
1976
|
-
try {
|
|
1977
|
-
process.kill(-pid, "SIGKILL");
|
|
1978
|
-
} catch {
|
|
1979
|
-
}
|
|
1980
|
-
}, 2500).unref?.();
|
|
1981
|
-
}
|
|
1982
|
-
} catch {
|
|
1983
|
-
try {
|
|
1984
|
-
child.kill("SIGTERM");
|
|
1985
|
-
} catch {
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
killListenersOnPorts(ports);
|
|
1882
|
+
function isJobContinuePrompt(text) {
|
|
1883
|
+
if (typeof text !== "string") return false;
|
|
1884
|
+
const t = text.trim();
|
|
1885
|
+
return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
|
|
1990
1886
|
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
const
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
);
|
|
2003
|
-
try {
|
|
2004
|
-
mergeAgentGitAuthEnv(
|
|
2005
|
-
env,
|
|
2006
|
-
await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
|
|
2007
|
-
);
|
|
2008
|
-
} catch {
|
|
2009
|
-
}
|
|
2010
|
-
if (opts.heldPorts?.length) {
|
|
2011
|
-
await Promise.all(opts.heldPorts.map((h) => h.release()));
|
|
2012
|
-
}
|
|
2013
|
-
const shell = process.platform === "darwin" ? "zsh" : "bash";
|
|
2014
|
-
const child = execa2(shell, ["-lc", command], {
|
|
2015
|
-
cwd: opts.worktreePath,
|
|
2016
|
-
reject: false,
|
|
2017
|
-
env,
|
|
2018
|
-
// Own process group so Stop can tear down the whole tree (not just the shell).
|
|
2019
|
-
// There is no settings.toml `stop=` / teardown hook for run scripts.
|
|
2020
|
-
...process.platform === "win32" ? {} : { detached: true }
|
|
2021
|
-
});
|
|
2022
|
-
pipeLines(child.stdout, opts.onLine);
|
|
2023
|
-
pipeLines(child.stderr, opts.onLine);
|
|
2024
|
-
const ports = opts.ports ?? [];
|
|
2025
|
-
return {
|
|
2026
|
-
pid: child.pid,
|
|
2027
|
-
kill: () => killScriptTree(child, ports),
|
|
2028
|
-
done: child.then((r) => r.exitCode ?? null),
|
|
2029
|
-
child
|
|
2030
|
-
};
|
|
1887
|
+
function collapseGhRunWatchLog(text) {
|
|
1888
|
+
if (!text.includes(GH_WATCH_REFRESH)) return text;
|
|
1889
|
+
const frames = text.split(/(?=Refreshing run status every 3 seconds\.)/);
|
|
1890
|
+
const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
|
|
1891
|
+
if (watchFrames.length <= 1) return text;
|
|
1892
|
+
const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
|
|
1893
|
+
const last = watchFrames[watchFrames.length - 1] ?? "";
|
|
1894
|
+
const omitted = watchFrames.length - 1;
|
|
1895
|
+
const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
|
|
1896
|
+
`;
|
|
1897
|
+
return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
|
|
2031
1898
|
}
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
1899
|
+
function capJobLogDelta(text) {
|
|
1900
|
+
const collapsed = collapseGhRunWatchLog(text);
|
|
1901
|
+
const lines = collapsed.split("\n");
|
|
1902
|
+
let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
|
|
1903
|
+
${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
|
|
1904
|
+
if (out.length > MAX_JOB_DELTA_CHARS) {
|
|
1905
|
+
out = `\u2026(truncated ${out.length} chars)
|
|
1906
|
+
${out.slice(-MAX_JOB_DELTA_CHARS)}`;
|
|
2037
1907
|
}
|
|
2038
|
-
return
|
|
1908
|
+
return out;
|
|
2039
1909
|
}
|
|
2040
|
-
|
|
2041
|
-
const
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
defaultBranch: opts?.defaultBranch,
|
|
2049
|
-
onLine
|
|
2050
|
-
});
|
|
2051
|
-
const exitCode = await attachAbort(handle, opts?.signal);
|
|
2052
|
-
return {
|
|
2053
|
-
ran: true,
|
|
2054
|
-
exitCode,
|
|
2055
|
-
source,
|
|
2056
|
-
kill: handle.kill
|
|
2057
|
-
};
|
|
1910
|
+
function formatJobStillRunningContinuePrompt(jobIds) {
|
|
1911
|
+
const ids = jobIds.join(", ");
|
|
1912
|
+
return [
|
|
1913
|
+
`Detached job still running: ${ids}.`,
|
|
1914
|
+
"Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
|
|
1915
|
+
"If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
|
|
1916
|
+
"Then report the result. Do not tell the user you will let them know later."
|
|
1917
|
+
].join(" ");
|
|
2058
1918
|
}
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
1919
|
+
function formatDeferredDoneContinuePrompt() {
|
|
1920
|
+
return [
|
|
1921
|
+
"You ended the turn after promising to report later, but no detached job is running.",
|
|
1922
|
+
"If tests/pack/deploy still need to run: start once with detached-job.cjs, then loop wait_for_job until stillRunning is false (the log pane updates from wait JSON).",
|
|
1923
|
+
"Do not say you will let the user know later."
|
|
1924
|
+
].join(" ");
|
|
1925
|
+
}
|
|
1926
|
+
function turnWatchedDetachedJob(parts) {
|
|
1927
|
+
return parts.some((p) => {
|
|
1928
|
+
if (p.type !== "tool") return false;
|
|
1929
|
+
if (/wait_for_job$/i.test(p.name ?? "") || /stop_job$/i.test(p.name ?? "")) return true;
|
|
1930
|
+
const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
|
|
1931
|
+
return /detached-job\.(?:js|cjs)\b/i.test(blob);
|
|
2068
1932
|
});
|
|
2069
|
-
const exitCode = await attachAbort(handle, opts?.signal);
|
|
2070
|
-
return {
|
|
2071
|
-
ran: true,
|
|
2072
|
-
exitCode,
|
|
2073
|
-
source: found.source,
|
|
2074
|
-
kill: handle.kill
|
|
2075
|
-
};
|
|
2076
1933
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
if (
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
1934
|
+
function planJobContinue(opts) {
|
|
1935
|
+
if (opts.isOrchestrator) return { action: "none" };
|
|
1936
|
+
if (opts.agent === "brightsy") return { action: "none" };
|
|
1937
|
+
if (opts.queueLength > 0) return { action: "none" };
|
|
1938
|
+
if (opts.continueCount >= MAX_JOB_CONTINUES) return { action: "none" };
|
|
1939
|
+
const farewell = looksLikeDeferredDonePromise(opts.chatText);
|
|
1940
|
+
if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
|
|
1941
|
+
return {
|
|
1942
|
+
action: "wait",
|
|
1943
|
+
jobIds: opts.runningJobIds,
|
|
1944
|
+
prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
|
|
1945
|
+
};
|
|
2086
1946
|
}
|
|
2087
|
-
if (!
|
|
2088
|
-
|
|
1947
|
+
if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
|
|
1948
|
+
return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
|
|
2089
1949
|
}
|
|
2090
|
-
return
|
|
1950
|
+
return { action: "none" };
|
|
2091
1951
|
}
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
worktreePath,
|
|
2097
|
-
repoPath,
|
|
2098
|
-
onLine
|
|
2099
|
-
});
|
|
2100
|
-
const exitCode = await handle.done;
|
|
2101
|
-
return { ran: true, exitCode };
|
|
1952
|
+
function tailProgress(logFile, maxLines = 12) {
|
|
1953
|
+
if (!existsSync3(logFile)) return "(no log yet)";
|
|
1954
|
+
const lines = readFileSync3(logFile, "utf8").split("\n");
|
|
1955
|
+
return lines.slice(-maxLines).join("\n");
|
|
2102
1956
|
}
|
|
2103
|
-
function
|
|
2104
|
-
const
|
|
2105
|
-
|
|
2106
|
-
return settings.runScripts.filter((s) => {
|
|
2107
|
-
if (!s.availableIn?.length) return true;
|
|
2108
|
-
return s.availableIn.includes("local");
|
|
2109
|
-
});
|
|
1957
|
+
function lastProgressLine(progress) {
|
|
1958
|
+
const last = progress.split("\n").map((l) => l.trim()).filter((l) => l && l !== "(no log yet)").at(-1);
|
|
1959
|
+
return last ? last.slice(0, 72) : void 0;
|
|
2110
1960
|
}
|
|
2111
|
-
function
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
1961
|
+
function readLogLines(file) {
|
|
1962
|
+
if (!existsSync3(file)) return [];
|
|
1963
|
+
const lines = readFileSync3(file, "utf8").split("\n");
|
|
1964
|
+
if (lines.length && lines[lines.length - 1] === "") lines.pop();
|
|
1965
|
+
return lines;
|
|
2115
1966
|
}
|
|
2116
|
-
function
|
|
2117
|
-
const
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
}
|
|
2122
|
-
return getDefaultRunScript(worktreePath, repoPath);
|
|
1967
|
+
function takeDelta(logFile, cursorFile) {
|
|
1968
|
+
const lines = readLogLines(logFile);
|
|
1969
|
+
const cursor = readIntFile(cursorFile) ?? 0;
|
|
1970
|
+
const start = Math.min(Math.max(0, cursor), lines.length);
|
|
1971
|
+
return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
|
|
2123
1972
|
}
|
|
2124
|
-
function
|
|
2125
|
-
|
|
1973
|
+
function snapshotJob(dir) {
|
|
1974
|
+
const pid = readIntFile(join5(dir, "pid"));
|
|
1975
|
+
const running = jobTreeAlive(pid);
|
|
1976
|
+
return {
|
|
1977
|
+
pid,
|
|
1978
|
+
running,
|
|
1979
|
+
exitCode: readIntFile(join5(dir, "exit")),
|
|
1980
|
+
log: join5(dir, "log"),
|
|
1981
|
+
cursor: join5(dir, "present.cursor"),
|
|
1982
|
+
progress: tailProgress(join5(dir, "log"))
|
|
1983
|
+
};
|
|
2126
1984
|
}
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
1985
|
+
function toResult(id, snap, extra) {
|
|
1986
|
+
const failed = extra?.failed === true || !snap.running && snap.exitCode != null && snap.exitCode !== 0;
|
|
1987
|
+
const ok = !snap.running && snap.exitCode === 0;
|
|
1988
|
+
const stillRunning = snap.running && !ok;
|
|
1989
|
+
const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
|
|
1990
|
+
try {
|
|
1991
|
+
mkdirSync4(dirname3(snap.cursor), { recursive: true });
|
|
1992
|
+
writeFileSync3(snap.cursor, `${nextCursor}
|
|
1993
|
+
`);
|
|
1994
|
+
} catch {
|
|
1995
|
+
}
|
|
1996
|
+
const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
|
|
1997
|
+
const progress = extra?.progress ?? snap.progress;
|
|
1998
|
+
return {
|
|
1999
|
+
stillRunning,
|
|
2000
|
+
ok,
|
|
2001
|
+
failed: Boolean(failed && !stillRunning && !ok),
|
|
2002
|
+
status,
|
|
2003
|
+
id,
|
|
2004
|
+
pid: snap.pid,
|
|
2005
|
+
exitCode: snap.exitCode ?? void 0,
|
|
2006
|
+
phase: lastProgressLine(progress),
|
|
2007
|
+
delta,
|
|
2008
|
+
progress,
|
|
2009
|
+
hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
|
|
2010
|
+
};
|
|
2149
2011
|
}
|
|
2150
|
-
async function
|
|
2151
|
-
|
|
2152
|
-
await held.release();
|
|
2153
|
-
return held.port;
|
|
2012
|
+
async function sleepMs(ms) {
|
|
2013
|
+
await new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2154
2014
|
}
|
|
2155
|
-
async function
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2015
|
+
async function waitForDetachedJob(cwd, id, opts) {
|
|
2016
|
+
const jobId = sanitizeDetachedJobId(id);
|
|
2017
|
+
const root = cwd.trim() || process.cwd();
|
|
2018
|
+
const dir = resolveJobDir(root, jobId);
|
|
2019
|
+
const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
|
|
2020
|
+
const sleep = opts?.sleep ?? sleepMs;
|
|
2021
|
+
if (!existsSync3(dir)) {
|
|
2022
|
+
return {
|
|
2023
|
+
stillRunning: false,
|
|
2024
|
+
ok: false,
|
|
2025
|
+
failed: true,
|
|
2026
|
+
status: "failed",
|
|
2027
|
+
id: jobId,
|
|
2028
|
+
delta: "",
|
|
2029
|
+
progress: "No detached job. Start one first.",
|
|
2030
|
+
hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
const deadline = Date.now() + timeoutMs;
|
|
2034
|
+
let snap = snapshotJob(dir);
|
|
2035
|
+
if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
|
|
2036
|
+
if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
|
|
2037
|
+
return toResult(jobId, snap, {
|
|
2038
|
+
failed: true,
|
|
2039
|
+
progress: "No detached job. Start one first."
|
|
2177
2040
|
});
|
|
2178
|
-
}
|
|
2041
|
+
}
|
|
2042
|
+
while (Date.now() < deadline) {
|
|
2043
|
+
snap = snapshotJob(dir);
|
|
2044
|
+
if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
|
|
2045
|
+
if (!snap.running) return toResult(jobId, snap, { failed: true });
|
|
2046
|
+
await sleep(Math.min(2e3, Math.max(50, deadline - Date.now())));
|
|
2047
|
+
}
|
|
2048
|
+
snap = snapshotJob(dir);
|
|
2049
|
+
return toResult(jobId, snap);
|
|
2179
2050
|
}
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2051
|
+
function killProcessTree(pid, signal) {
|
|
2052
|
+
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
2053
|
+
if (process.platform !== "win32") {
|
|
2054
|
+
try {
|
|
2055
|
+
process.kill(-pid, signal);
|
|
2056
|
+
return;
|
|
2057
|
+
} catch {
|
|
2187
2058
|
}
|
|
2188
|
-
held.push(next);
|
|
2189
2059
|
}
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
if (!ports?.length) return [];
|
|
2194
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2195
|
-
const out = [];
|
|
2196
|
-
for (const p of ports) {
|
|
2197
|
-
if (!Number.isInteger(p) || p <= 0 || p > 65535 || seen.has(p)) continue;
|
|
2198
|
-
seen.add(p);
|
|
2199
|
-
out.push(p);
|
|
2200
|
-
if (out.length >= size) break;
|
|
2060
|
+
try {
|
|
2061
|
+
process.kill(pid, signal);
|
|
2062
|
+
} catch {
|
|
2201
2063
|
}
|
|
2202
|
-
return out;
|
|
2203
|
-
}
|
|
2204
|
-
function delay(ms) {
|
|
2205
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2206
2064
|
}
|
|
2207
|
-
|
|
2208
|
-
if (held.length >= size) return held;
|
|
2209
|
-
const base = held[0]?.port;
|
|
2210
|
-
const taken = new Set(held.map((h) => h.port));
|
|
2065
|
+
function appendJobLog(logFile, line) {
|
|
2211
2066
|
try {
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
const next = candidate != null ? await tryReservePort(candidate) : null;
|
|
2218
|
-
if (next) {
|
|
2219
|
-
held.push(next);
|
|
2220
|
-
taken.add(next.port);
|
|
2221
|
-
} else {
|
|
2222
|
-
const ephemeral = await reservePort();
|
|
2223
|
-
held.push(ephemeral);
|
|
2224
|
-
taken.add(ephemeral.port);
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
return held;
|
|
2228
|
-
} catch (err) {
|
|
2229
|
-
await Promise.all(held.map((h) => h.release()));
|
|
2230
|
-
throw err;
|
|
2067
|
+
mkdirSync4(dirname3(logFile), { recursive: true });
|
|
2068
|
+
const body = line.endsWith("\n") ? line : `${line}
|
|
2069
|
+
`;
|
|
2070
|
+
appendFileSync2(logFile, body);
|
|
2071
|
+
} catch {
|
|
2231
2072
|
}
|
|
2232
2073
|
}
|
|
2233
|
-
async function
|
|
2234
|
-
const
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2074
|
+
async function stopDetachedJob(cwd, id, opts) {
|
|
2075
|
+
const jobId = sanitizeDetachedJobId(id);
|
|
2076
|
+
const root = cwd.trim() || process.cwd();
|
|
2077
|
+
const dir = resolveJobDir(root, jobId);
|
|
2078
|
+
const sleep = opts?.sleep ?? sleepMs;
|
|
2079
|
+
const why = opts?.reason?.trim() ?? "";
|
|
2080
|
+
if (!existsSync3(dir)) {
|
|
2081
|
+
return {
|
|
2082
|
+
stillRunning: false,
|
|
2083
|
+
ok: false,
|
|
2084
|
+
failed: true,
|
|
2085
|
+
status: "failed",
|
|
2086
|
+
id: jobId,
|
|
2087
|
+
delta: "",
|
|
2088
|
+
progress: "No detached job. Start one first.",
|
|
2089
|
+
hint: "Start with detached-job.cjs start <id> -- <command>, then wait_for_job or stop_job.",
|
|
2090
|
+
stopped: false,
|
|
2091
|
+
reason: "not-found"
|
|
2092
|
+
};
|
|
2238
2093
|
}
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
return null;
|
|
2249
|
-
};
|
|
2250
|
-
let held = await takePrimary();
|
|
2251
|
-
if (held) return held;
|
|
2252
|
-
if (worktreePath) {
|
|
2253
|
-
killStaleWorktreeListeners(ports, worktreePath, "SIGTERM");
|
|
2254
|
-
} else {
|
|
2255
|
-
killListenersOnPorts(ports);
|
|
2094
|
+
let snap = snapshotJob(dir);
|
|
2095
|
+
if (!snap.running) {
|
|
2096
|
+
const result2 = toResult(jobId, snap);
|
|
2097
|
+
return {
|
|
2098
|
+
...result2,
|
|
2099
|
+
stopped: false,
|
|
2100
|
+
reason: "not-running",
|
|
2101
|
+
hint: "That job is already finished. Read the log; do not wait again unless you start a new command."
|
|
2102
|
+
};
|
|
2256
2103
|
}
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2104
|
+
appendJobLog(snap.log, `$ stop${why ? `: ${why}` : ""}`);
|
|
2105
|
+
const targetPid = snap.pid;
|
|
2106
|
+
if (targetPid != null) killProcessTree(targetPid, "SIGTERM");
|
|
2107
|
+
const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
|
|
2108
|
+
const deadline = Date.now() + graceMs;
|
|
2109
|
+
while (Date.now() < deadline && jobTreeAlive(targetPid)) {
|
|
2110
|
+
await sleep(Math.min(100, Math.max(20, deadline - Date.now())));
|
|
2264
2111
|
}
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
const preferred = normalizePreferredPorts(opts?.preferred, size);
|
|
2269
|
-
if (preferred.length) {
|
|
2270
|
-
const reused = await reclaimPreferredPorts(preferred, opts?.worktreePath);
|
|
2271
|
-
if (reused) return fillPortRange(reused, size);
|
|
2272
|
-
}
|
|
2273
|
-
const held = [await reservePort()];
|
|
2274
|
-
return fillPortRange(held, size);
|
|
2275
|
-
}
|
|
2276
|
-
async function allocatePortRange(size = PORT_RANGE_SIZE) {
|
|
2277
|
-
const held = await reservePortRange(size);
|
|
2278
|
-
const ports = held.map((h) => h.port);
|
|
2279
|
-
await Promise.all(held.map((h) => h.release()));
|
|
2280
|
-
return ports;
|
|
2281
|
-
}
|
|
2282
|
-
async function startDevServer(repoPath, worktreePath, onLine, opts) {
|
|
2283
|
-
const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
|
|
2284
|
-
if (!script) return null;
|
|
2285
|
-
const preferred = loadWorktreeRunPorts(worktreePath, script.name) ?? opts?.preferredPorts;
|
|
2286
|
-
const held = await reservePortRange(PORT_RANGE_SIZE, {
|
|
2287
|
-
preferred: preferred ?? void 0,
|
|
2288
|
-
worktreePath
|
|
2289
|
-
});
|
|
2290
|
-
const ports = held.map((h) => h.port);
|
|
2291
|
-
saveWorktreeRunPorts(worktreePath, script.name, ports);
|
|
2292
|
-
try {
|
|
2293
|
-
const handle = await spawnWorkspaceScript(script.command, {
|
|
2294
|
-
worktreePath,
|
|
2295
|
-
repoPath,
|
|
2296
|
-
ports,
|
|
2297
|
-
heldPorts: held,
|
|
2298
|
-
defaultBranch: opts?.defaultBranch,
|
|
2299
|
-
onLine
|
|
2300
|
-
});
|
|
2301
|
-
return {
|
|
2302
|
-
pid: handle.pid,
|
|
2303
|
-
port: ports[0],
|
|
2304
|
-
ports,
|
|
2305
|
-
scriptName: script.name,
|
|
2306
|
-
kill: handle.kill,
|
|
2307
|
-
done: handle.done
|
|
2308
|
-
};
|
|
2309
|
-
} catch (err) {
|
|
2310
|
-
await Promise.all(held.map((h) => h.release()));
|
|
2311
|
-
throw err;
|
|
2112
|
+
if (targetPid != null) {
|
|
2113
|
+
killProcessTree(targetPid, "SIGKILL");
|
|
2114
|
+
await sleep(100);
|
|
2312
2115
|
}
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
var EMPTY = {
|
|
2320
|
-
output: "",
|
|
2321
|
-
running: false,
|
|
2322
|
-
exitCode: null,
|
|
2323
|
-
source: null
|
|
2324
|
-
};
|
|
2325
|
-
var MAX_SETUP_LOG_CHARS = 256e3;
|
|
2326
|
-
function appendSetupOutput(prev, line, max = MAX_SETUP_LOG_CHARS) {
|
|
2327
|
-
const next = prev ? `${prev}
|
|
2328
|
-
${line}` : line;
|
|
2329
|
-
if (next.length <= max) return next;
|
|
2330
|
-
const cut = next.slice(next.length - max);
|
|
2331
|
-
const nl = cut.indexOf("\n");
|
|
2332
|
-
return nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
|
|
2333
|
-
}
|
|
2334
|
-
var memory = /* @__PURE__ */ new Map();
|
|
2335
|
-
var persistTimers = /* @__PURE__ */ new Map();
|
|
2336
|
-
function fromSnapshot(snap) {
|
|
2337
|
-
return {
|
|
2338
|
-
chunks: snap.output ? [snap.output] : [],
|
|
2339
|
-
length: snap.output.length,
|
|
2340
|
-
running: snap.running,
|
|
2341
|
-
exitCode: snap.exitCode,
|
|
2342
|
-
source: snap.source
|
|
2343
|
-
};
|
|
2344
|
-
}
|
|
2345
|
-
function materialize(mem) {
|
|
2346
|
-
let output = mem.chunks.join("\n");
|
|
2347
|
-
if (output.length > MAX_SETUP_LOG_CHARS) {
|
|
2348
|
-
output = appendSetupOutput("", output);
|
|
2349
|
-
mem.chunks = [output];
|
|
2350
|
-
mem.length = output.length;
|
|
2116
|
+
const exitFile = join5(dir, "exit");
|
|
2117
|
+
if (!existsSync3(exitFile)) {
|
|
2118
|
+
try {
|
|
2119
|
+
writeFileSync3(exitFile, "1\n");
|
|
2120
|
+
} catch {
|
|
2121
|
+
}
|
|
2351
2122
|
}
|
|
2123
|
+
snap = snapshotJob(dir);
|
|
2124
|
+
const stillRunning = jobTreeAlive(targetPid) || snap.running;
|
|
2125
|
+
const result = toResult(jobId, snap, { failed: true });
|
|
2126
|
+
const stopped = !stillRunning;
|
|
2352
2127
|
return {
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2128
|
+
...result,
|
|
2129
|
+
stillRunning,
|
|
2130
|
+
ok: false,
|
|
2131
|
+
failed: true,
|
|
2132
|
+
status: stillRunning ? "running" : "failed",
|
|
2133
|
+
stopped,
|
|
2134
|
+
reason: stopped ? "stopped" : "still-running",
|
|
2135
|
+
stopReason: why || void 0,
|
|
2136
|
+
hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
|
|
2357
2137
|
};
|
|
2358
2138
|
}
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
}
|
|
2366
|
-
|
|
2367
|
-
return { ...EMPTY };
|
|
2139
|
+
|
|
2140
|
+
// src/hook/conductor.ts
|
|
2141
|
+
var PORT_RANGE_SIZE = 10;
|
|
2142
|
+
var RECLAIM_RETRY_DELAY_MS = 40;
|
|
2143
|
+
var RECLAIM_TRIES = 8;
|
|
2144
|
+
function matchSimpleGlob(pattern, name) {
|
|
2145
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
2146
|
+
return new RegExp(`^${escaped}$`).test(name);
|
|
2368
2147
|
}
|
|
2369
|
-
function
|
|
2370
|
-
|
|
2371
|
-
if (!
|
|
2372
|
-
|
|
2373
|
-
if (prev.startsWith(incoming) || prev.endsWith(incoming)) return prev;
|
|
2374
|
-
if (incoming.startsWith(prev) || incoming.endsWith(prev)) return incoming;
|
|
2375
|
-
return incoming.length >= prev.length ? incoming : prev;
|
|
2148
|
+
function readWorktreeInclude(repoPath) {
|
|
2149
|
+
const path = join6(repoPath, ".worktreeinclude");
|
|
2150
|
+
if (!existsSync4(path)) return [];
|
|
2151
|
+
return readFileSync4(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
2376
2152
|
}
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2153
|
+
function resolveFilesToCopy(repoPath) {
|
|
2154
|
+
const fromInclude = readWorktreeInclude(repoPath);
|
|
2155
|
+
if (fromInclude.length) return fromInclude;
|
|
2156
|
+
const settings = loadRepoSettings(repoPath);
|
|
2157
|
+
if (settings?.filesToCopy?.length) return settings.filesToCopy;
|
|
2158
|
+
if (settings?.fileIncludeGlobs?.length) {
|
|
2159
|
+
const matched = [];
|
|
2160
|
+
try {
|
|
2161
|
+
for (const entry of readdirSync2(repoPath, { withFileTypes: true })) {
|
|
2162
|
+
if (!entry.isFile()) continue;
|
|
2163
|
+
for (const glob of settings.fileIncludeGlobs) {
|
|
2164
|
+
if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob(basename(glob), entry.name)) {
|
|
2165
|
+
matched.push(entry.name);
|
|
2166
|
+
break;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
} catch {
|
|
2171
|
+
}
|
|
2172
|
+
if (matched.length) return [...new Set(matched)];
|
|
2383
2173
|
}
|
|
2384
|
-
const
|
|
2385
|
-
if (!mem) return;
|
|
2174
|
+
const defaults = [];
|
|
2386
2175
|
try {
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
);
|
|
2176
|
+
for (const entry of readdirSync2(repoPath, { withFileTypes: true })) {
|
|
2177
|
+
if (entry.isFile() && entry.name.startsWith(".env")) {
|
|
2178
|
+
defaults.push(entry.name);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2393
2181
|
} catch {
|
|
2394
2182
|
}
|
|
2183
|
+
if (defaults.length) return defaults;
|
|
2184
|
+
return [".env.local", ".env"];
|
|
2395
2185
|
}
|
|
2396
|
-
function
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2186
|
+
function copyConfiguredFiles(repoPath, worktreePath) {
|
|
2187
|
+
const patterns = resolveFilesToCopy(repoPath);
|
|
2188
|
+
const copied = [];
|
|
2189
|
+
for (const rel of patterns) {
|
|
2190
|
+
const src = join6(repoPath, rel);
|
|
2191
|
+
if (!existsSync4(src)) continue;
|
|
2192
|
+
const dest = join6(worktreePath, rel);
|
|
2193
|
+
mkdirSync5(dirname4(dest), { recursive: true });
|
|
2194
|
+
copyFileSync(src, dest);
|
|
2195
|
+
copied.push(rel);
|
|
2196
|
+
}
|
|
2197
|
+
return copied;
|
|
2405
2198
|
}
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
if (
|
|
2199
|
+
var cachedLoginEnv = null;
|
|
2200
|
+
async function captureLoginEnv() {
|
|
2201
|
+
if (cachedLoginEnv) return { ...cachedLoginEnv };
|
|
2202
|
+
const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
|
|
2409
2203
|
try {
|
|
2410
|
-
const
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2204
|
+
const result = await execa2(shell, ["-l", "-c", "env -0"], {
|
|
2205
|
+
reject: false,
|
|
2206
|
+
timeout: 5e3
|
|
2207
|
+
});
|
|
2208
|
+
if (result.exitCode === 0 && result.stdout) {
|
|
2209
|
+
const env = { ...process.env };
|
|
2210
|
+
for (const entry of result.stdout.split("\0")) {
|
|
2211
|
+
if (!entry) continue;
|
|
2212
|
+
const eq = entry.indexOf("=");
|
|
2213
|
+
if (eq <= 0) continue;
|
|
2214
|
+
const key = entry.slice(0, eq);
|
|
2215
|
+
const value = entry.slice(eq + 1);
|
|
2216
|
+
if (key) env[key] = value;
|
|
2217
|
+
}
|
|
2218
|
+
cachedLoginEnv = env;
|
|
2219
|
+
return { ...env };
|
|
2220
|
+
}
|
|
2417
2221
|
} catch {
|
|
2418
|
-
return null;
|
|
2419
2222
|
}
|
|
2223
|
+
cachedLoginEnv = { ...process.env };
|
|
2224
|
+
return { ...cachedLoginEnv };
|
|
2420
2225
|
}
|
|
2421
|
-
function
|
|
2422
|
-
const
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2226
|
+
function buildWorkspaceScriptEnv(opts, baseEnv) {
|
|
2227
|
+
const env = stripNestedElectronEnv({
|
|
2228
|
+
...baseEnv ?? process.env
|
|
2229
|
+
});
|
|
2230
|
+
const name = opts.workspaceName ?? basename(opts.worktreePath);
|
|
2231
|
+
const ports = opts.ports ?? [];
|
|
2232
|
+
const primary = ports[0];
|
|
2233
|
+
env.SIDEBOARD_WORKSPACE_NAME = name;
|
|
2234
|
+
env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
|
|
2235
|
+
env.SIDEBOARD_ROOT_PATH = opts.repoPath;
|
|
2236
|
+
env.SIDEBOARD_IS_LOCAL = "1";
|
|
2237
|
+
if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
|
|
2238
|
+
env.CONDUCTOR_WORKSPACE_NAME = name;
|
|
2239
|
+
env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
|
|
2240
|
+
env.CONDUCTOR_ROOT_PATH = opts.repoPath;
|
|
2241
|
+
env.CONDUCTOR_IS_LOCAL = "1";
|
|
2242
|
+
if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
|
|
2243
|
+
if (primary != null) {
|
|
2244
|
+
env.SIDEBOARD_PORT = String(primary);
|
|
2245
|
+
env.CONDUCTOR_PORT = String(primary);
|
|
2246
|
+
env.PORT = String(primary);
|
|
2247
|
+
for (let i = 1; i < ports.length; i++) {
|
|
2248
|
+
const p = ports[i];
|
|
2249
|
+
env[`SIDEBOARD_PORT_${i}`] = String(p);
|
|
2250
|
+
env[`CONDUCTOR_PORT_${i}`] = String(p);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
applyWorktreePkgCacheEnv(env, opts.worktreePath);
|
|
2254
|
+
return env;
|
|
2427
2255
|
}
|
|
2428
|
-
function
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
if (disk) {
|
|
2433
|
-
memory.set(threadId, fromSnapshot(disk));
|
|
2434
|
-
return disk;
|
|
2435
|
-
}
|
|
2436
|
-
return emptySetupLog();
|
|
2437
|
-
}
|
|
2438
|
-
function beginSetupLog(threadId) {
|
|
2439
|
-
const mem = {
|
|
2440
|
-
chunks: [],
|
|
2441
|
-
length: 0,
|
|
2442
|
-
running: true,
|
|
2443
|
-
exitCode: null,
|
|
2444
|
-
source: null
|
|
2445
|
-
};
|
|
2446
|
-
memory.set(threadId, mem);
|
|
2447
|
-
persistNow(threadId);
|
|
2448
|
-
return materialize(mem);
|
|
2449
|
-
}
|
|
2450
|
-
function appendSetupLog(threadId, line) {
|
|
2451
|
-
const mem = memFor(threadId);
|
|
2452
|
-
mem.chunks.push(line);
|
|
2453
|
-
mem.length += line.length + 1;
|
|
2454
|
-
mem.running = true;
|
|
2455
|
-
if (mem.length > MAX_SETUP_LOG_CHARS * 2) materialize(mem);
|
|
2456
|
-
schedulePersist(threadId);
|
|
2457
|
-
}
|
|
2458
|
-
function finishSetupLog(threadId, exitCode, source) {
|
|
2459
|
-
const mem = memFor(threadId);
|
|
2460
|
-
mem.running = false;
|
|
2461
|
-
mem.exitCode = exitCode;
|
|
2462
|
-
mem.source = source ?? mem.source;
|
|
2463
|
-
persistNow(threadId);
|
|
2464
|
-
return materialize(mem);
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
|
-
// src/store/run-log.ts
|
|
2468
|
-
import { createHash as createHash2 } from "crypto";
|
|
2469
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
2470
|
-
import { join as join7 } from "path";
|
|
2471
|
-
var EMPTY2 = {
|
|
2472
|
-
output: "",
|
|
2473
|
-
running: false,
|
|
2474
|
-
exitCode: null,
|
|
2475
|
-
scriptName: null
|
|
2476
|
-
};
|
|
2477
|
-
var MAX_RUN_LOG_CHARS = 256e3;
|
|
2478
|
-
var DEFAULT_RUN_LOG_TAIL_CHARS = 16e3;
|
|
2479
|
-
var memory2 = /* @__PURE__ */ new Map();
|
|
2480
|
-
var persistTimers2 = /* @__PURE__ */ new Map();
|
|
2481
|
-
function fromSnapshot2(snap) {
|
|
2482
|
-
return {
|
|
2483
|
-
chunks: snap.output ? [snap.output] : [],
|
|
2484
|
-
length: snap.output.length,
|
|
2485
|
-
running: snap.running,
|
|
2486
|
-
exitCode: snap.exitCode,
|
|
2487
|
-
scriptName: snap.scriptName
|
|
2488
|
-
};
|
|
2256
|
+
function pipeLines(stream, onLine) {
|
|
2257
|
+
if (!stream || !onLine) return;
|
|
2258
|
+
const rl = createInterface2({ input: stream });
|
|
2259
|
+
rl.on("line", onLine);
|
|
2489
2260
|
}
|
|
2490
|
-
function
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2261
|
+
function killableListenerPids(raw, selfPid = process.pid) {
|
|
2262
|
+
const pids = [];
|
|
2263
|
+
for (const token of raw.trim().split(/\s+/)) {
|
|
2264
|
+
const pid = Number(token);
|
|
2265
|
+
if (!Number.isFinite(pid) || pid <= 0 || pid === selfPid) continue;
|
|
2266
|
+
pids.push(pid);
|
|
2496
2267
|
}
|
|
2497
|
-
return
|
|
2498
|
-
output,
|
|
2499
|
-
running: mem.running,
|
|
2500
|
-
exitCode: mem.exitCode,
|
|
2501
|
-
scriptName: mem.scriptName
|
|
2502
|
-
};
|
|
2503
|
-
}
|
|
2504
|
-
function safeScriptToken(scriptName) {
|
|
2505
|
-
const token = scriptName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
|
|
2506
|
-
return token || "dev";
|
|
2268
|
+
return pids;
|
|
2507
2269
|
}
|
|
2508
|
-
function
|
|
2509
|
-
|
|
2270
|
+
function listUnixListenerPids(port, selfPid) {
|
|
2271
|
+
try {
|
|
2272
|
+
const raw = execFileSync("lsof", [`-tiTCP:${port}`, "-sTCP:LISTEN"], {
|
|
2273
|
+
encoding: "utf8",
|
|
2274
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2275
|
+
timeout: 2e3
|
|
2276
|
+
});
|
|
2277
|
+
return killableListenerPids(raw, selfPid);
|
|
2278
|
+
} catch {
|
|
2279
|
+
return [];
|
|
2280
|
+
}
|
|
2510
2281
|
}
|
|
2511
|
-
function
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2282
|
+
function listenerCwd(pid) {
|
|
2283
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
2284
|
+
try {
|
|
2285
|
+
const raw = execFileSync("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
|
|
2286
|
+
encoding: "utf8",
|
|
2287
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2288
|
+
timeout: 2e3
|
|
2289
|
+
});
|
|
2290
|
+
for (const line of raw.split("\n")) {
|
|
2291
|
+
if (line.startsWith("n") && line.length > 1) return line.slice(1);
|
|
2292
|
+
}
|
|
2293
|
+
} catch {
|
|
2294
|
+
}
|
|
2295
|
+
return null;
|
|
2515
2296
|
}
|
|
2516
|
-
function
|
|
2517
|
-
|
|
2297
|
+
function realpathOrResolve(path) {
|
|
2298
|
+
try {
|
|
2299
|
+
return realpathSync(path);
|
|
2300
|
+
} catch {
|
|
2301
|
+
return resolve(path);
|
|
2302
|
+
}
|
|
2518
2303
|
}
|
|
2519
|
-
function
|
|
2520
|
-
const
|
|
2521
|
-
const
|
|
2522
|
-
if (
|
|
2523
|
-
const
|
|
2524
|
-
const
|
|
2525
|
-
|
|
2526
|
-
|
|
2304
|
+
function isOtherWorktreeListenerCwd(cwd, worktreePath) {
|
|
2305
|
+
const wt = realpathOrResolve(worktreePath);
|
|
2306
|
+
const c = realpathOrResolve(cwd);
|
|
2307
|
+
if (c === wt || c.startsWith(wt + sep)) return false;
|
|
2308
|
+
const parent = dirname4(wt);
|
|
2309
|
+
const thisName = basename(wt);
|
|
2310
|
+
if (!thisName || c === parent) return false;
|
|
2311
|
+
if (!c.startsWith(parent + sep)) return false;
|
|
2312
|
+
const first = c.slice(parent.length + 1).split(sep)[0];
|
|
2313
|
+
return Boolean(first) && first !== thisName;
|
|
2527
2314
|
}
|
|
2528
|
-
|
|
2529
|
-
function persistNow2(key) {
|
|
2530
|
-
const timer = persistTimers2.get(key);
|
|
2531
|
-
if (timer) {
|
|
2532
|
-
clearTimeout(timer);
|
|
2533
|
-
persistTimers2.delete(key);
|
|
2534
|
-
}
|
|
2535
|
-
const mem = memory2.get(key);
|
|
2536
|
-
if (!mem) return;
|
|
2315
|
+
function signalPid(pid, signal) {
|
|
2537
2316
|
try {
|
|
2538
|
-
|
|
2539
|
-
`, "utf8");
|
|
2317
|
+
process.kill(pid, signal);
|
|
2540
2318
|
} catch {
|
|
2541
2319
|
}
|
|
2542
2320
|
}
|
|
2543
|
-
function
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
setTimeout(() => {
|
|
2548
|
-
persistTimers2.delete(key);
|
|
2549
|
-
persistNow2(key);
|
|
2550
|
-
}, PERSIST_DEBOUNCE_MS2)
|
|
2551
|
-
);
|
|
2321
|
+
function isThisWorktreeListenerCwd(cwd, worktreePath) {
|
|
2322
|
+
const wt = realpathOrResolve(worktreePath);
|
|
2323
|
+
const c = realpathOrResolve(cwd);
|
|
2324
|
+
return c === wt || c.startsWith(wt + sep);
|
|
2552
2325
|
}
|
|
2553
|
-
function
|
|
2554
|
-
|
|
2555
|
-
|
|
2326
|
+
function killDetachedRunTree(pid, worktreePath) {
|
|
2327
|
+
if (!Number.isFinite(pid) || pid <= 1 || pid === process.pid) return;
|
|
2328
|
+
const cwd = listenerCwd(pid);
|
|
2329
|
+
if (cwd) {
|
|
2330
|
+
if (!isThisWorktreeListenerCwd(cwd, worktreePath)) return;
|
|
2331
|
+
} else if (!processGroupAlive(pid)) {
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2556
2334
|
try {
|
|
2557
|
-
|
|
2558
|
-
return {
|
|
2559
|
-
output: typeof raw.output === "string" ? raw.output : "",
|
|
2560
|
-
running: Boolean(raw.running),
|
|
2561
|
-
exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
|
|
2562
|
-
scriptName: typeof raw.scriptName === "string" ? raw.scriptName : null
|
|
2563
|
-
};
|
|
2335
|
+
process.kill(-pid, "SIGTERM");
|
|
2564
2336
|
} catch {
|
|
2565
|
-
|
|
2337
|
+
signalPid(pid, "SIGTERM");
|
|
2566
2338
|
}
|
|
2567
2339
|
}
|
|
2568
|
-
function
|
|
2569
|
-
const
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2340
|
+
function killListenersOnPorts(ports) {
|
|
2341
|
+
const selfPid = process.pid;
|
|
2342
|
+
for (const port of ports) {
|
|
2343
|
+
if (!Number.isFinite(port) || port <= 0) continue;
|
|
2344
|
+
try {
|
|
2345
|
+
if (process.platform === "win32") {
|
|
2346
|
+
execFileSync(
|
|
2347
|
+
"powershell",
|
|
2348
|
+
[
|
|
2349
|
+
"-NoProfile",
|
|
2350
|
+
"-Command",
|
|
2351
|
+
`Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -ne ${selfPid} } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
|
|
2352
|
+
],
|
|
2353
|
+
{ stdio: "ignore", timeout: 3e3 }
|
|
2354
|
+
);
|
|
2355
|
+
continue;
|
|
2356
|
+
}
|
|
2357
|
+
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
2358
|
+
signalPid(pid, "SIGTERM");
|
|
2359
|
+
}
|
|
2360
|
+
} catch {
|
|
2361
|
+
}
|
|
2582
2362
|
}
|
|
2583
|
-
return emptyRunLog();
|
|
2584
2363
|
}
|
|
2585
|
-
function
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2364
|
+
function killStaleWorktreeListeners(ports, worktreePath, signal = "SIGTERM") {
|
|
2365
|
+
if (!worktreePath.trim()) {
|
|
2366
|
+
killListenersOnPorts(ports);
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
if (process.platform === "win32") {
|
|
2370
|
+
killListenersOnPorts(ports);
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
const selfPid = process.pid;
|
|
2374
|
+
for (const port of ports) {
|
|
2375
|
+
if (!Number.isFinite(port) || port <= 0) continue;
|
|
2376
|
+
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
2377
|
+
const cwd = listenerCwd(pid);
|
|
2378
|
+
if (cwd && isOtherWorktreeListenerCwd(cwd, worktreePath)) continue;
|
|
2379
|
+
signalPid(pid, signal);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2596
2382
|
}
|
|
2597
|
-
function
|
|
2598
|
-
const
|
|
2599
|
-
|
|
2600
|
-
mem.length += line.length + 1;
|
|
2601
|
-
mem.running = true;
|
|
2602
|
-
if (mem.length > MAX_RUN_LOG_CHARS * 2) materialize2(mem);
|
|
2603
|
-
schedulePersist2(key);
|
|
2604
|
-
}
|
|
2605
|
-
function finishRunLog(key, exitCode) {
|
|
2606
|
-
const mem = memFor2(key);
|
|
2607
|
-
mem.running = false;
|
|
2608
|
-
mem.exitCode = exitCode;
|
|
2609
|
-
persistNow2(key);
|
|
2610
|
-
return materialize2(mem);
|
|
2611
|
-
}
|
|
2612
|
-
function flushRunLog(key) {
|
|
2613
|
-
persistNow2(key);
|
|
2614
|
-
}
|
|
2615
|
-
|
|
2616
|
-
// src/git/orphan-cleanup.ts
|
|
2617
|
-
import { existsSync as existsSync6, readdirSync as readdirSync2, statSync } from "fs";
|
|
2618
|
-
import { join as join8 } from "path";
|
|
2619
|
-
function isSideboardWorktreePath(path) {
|
|
2620
|
-
return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
|
|
2621
|
-
}
|
|
2622
|
-
async function findOrphanWorktrees(repoPaths) {
|
|
2623
|
-
const threads = listThreads({ includeArchived: true });
|
|
2624
|
-
const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
|
|
2625
|
-
const repos = new Set(
|
|
2626
|
-
repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
|
|
2627
|
-
);
|
|
2628
|
-
const homeRoot = sideboardWorkspacesDir();
|
|
2629
|
-
if (existsSync6(homeRoot)) {
|
|
2630
|
-
try {
|
|
2631
|
-
for (const entry of readdirSync2(homeRoot, { withFileTypes: true })) {
|
|
2632
|
-
if (!entry.isDirectory()) continue;
|
|
2633
|
-
void entry;
|
|
2634
|
-
}
|
|
2635
|
-
} catch {
|
|
2636
|
-
}
|
|
2637
|
-
}
|
|
2638
|
-
const orphans = [];
|
|
2639
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2640
|
-
for (const repoPath of repos) {
|
|
2641
|
-
if (!repoPath || !existsSync6(repoPath)) continue;
|
|
2642
|
-
try {
|
|
2643
|
-
const wts = await listWorktrees(repoPath);
|
|
2644
|
-
for (const wt of wts) {
|
|
2645
|
-
const path = wt.path.replace(/\/$/, "");
|
|
2646
|
-
if (!isSideboardWorktreePath(path)) continue;
|
|
2647
|
-
if (known.has(path)) continue;
|
|
2648
|
-
if (seen.has(path)) continue;
|
|
2649
|
-
seen.add(path);
|
|
2650
|
-
let mtimeMs = 0;
|
|
2651
|
-
try {
|
|
2652
|
-
mtimeMs = statSync(path).mtimeMs;
|
|
2653
|
-
} catch {
|
|
2654
|
-
mtimeMs = 0;
|
|
2655
|
-
}
|
|
2656
|
-
orphans.push({ path, repoPath, mtimeMs });
|
|
2657
|
-
}
|
|
2658
|
-
} catch {
|
|
2659
|
-
}
|
|
2383
|
+
function killScriptTree(child, ports = []) {
|
|
2384
|
+
const pid = child.pid;
|
|
2385
|
+
if (pid) {
|
|
2660
2386
|
try {
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
if (known.has(path) || seen.has(path)) continue;
|
|
2667
|
-
if (!existsSync6(join8(path, ".git"))) continue;
|
|
2668
|
-
seen.add(path);
|
|
2669
|
-
let mtimeMs = 0;
|
|
2387
|
+
if (process.platform === "win32") {
|
|
2388
|
+
void execa2("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
|
|
2389
|
+
} else {
|
|
2390
|
+
process.kill(-pid, "SIGTERM");
|
|
2391
|
+
setTimeout(() => {
|
|
2670
2392
|
try {
|
|
2671
|
-
|
|
2393
|
+
process.kill(-pid, "SIGKILL");
|
|
2672
2394
|
} catch {
|
|
2673
|
-
mtimeMs = Date.now();
|
|
2674
2395
|
}
|
|
2675
|
-
|
|
2676
|
-
}
|
|
2396
|
+
}, 2500).unref?.();
|
|
2677
2397
|
}
|
|
2678
2398
|
} catch {
|
|
2399
|
+
try {
|
|
2400
|
+
child.kill("SIGTERM");
|
|
2401
|
+
} catch {
|
|
2402
|
+
}
|
|
2679
2403
|
}
|
|
2680
2404
|
}
|
|
2681
|
-
|
|
2405
|
+
killListenersOnPorts(ports);
|
|
2682
2406
|
}
|
|
2683
|
-
async function
|
|
2684
|
-
const
|
|
2685
|
-
const
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
}
|
|
2702
|
-
try {
|
|
2703
|
-
await removeWorktree(orphan.repoPath, orphan.path);
|
|
2704
|
-
removed.push(orphan.path);
|
|
2705
|
-
} catch {
|
|
2706
|
-
kept.push(orphan.path);
|
|
2707
|
-
}
|
|
2407
|
+
async function spawnWorkspaceScript(command, opts) {
|
|
2408
|
+
const loginEnv = await captureLoginEnv();
|
|
2409
|
+
const env = buildWorkspaceScriptEnv(
|
|
2410
|
+
{
|
|
2411
|
+
worktreePath: opts.worktreePath,
|
|
2412
|
+
repoPath: opts.repoPath,
|
|
2413
|
+
workspaceName: opts.workspaceName,
|
|
2414
|
+
defaultBranch: opts.defaultBranch,
|
|
2415
|
+
ports: opts.ports
|
|
2416
|
+
},
|
|
2417
|
+
loginEnv
|
|
2418
|
+
);
|
|
2419
|
+
try {
|
|
2420
|
+
mergeAgentGitAuthEnv(
|
|
2421
|
+
env,
|
|
2422
|
+
await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
|
|
2423
|
+
);
|
|
2424
|
+
} catch {
|
|
2708
2425
|
}
|
|
2709
|
-
if (
|
|
2710
|
-
|
|
2711
|
-
worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2712
|
-
});
|
|
2426
|
+
if (opts.heldPorts?.length) {
|
|
2427
|
+
await Promise.all(opts.heldPorts.map((h) => h.release()));
|
|
2713
2428
|
}
|
|
2714
|
-
|
|
2429
|
+
const shell = process.platform === "darwin" ? "zsh" : "bash";
|
|
2430
|
+
const child = execa2(shell, ["-lc", command], {
|
|
2431
|
+
cwd: opts.worktreePath,
|
|
2432
|
+
reject: false,
|
|
2433
|
+
env,
|
|
2434
|
+
// Own process group so Stop can tear down the whole tree (not just the shell).
|
|
2435
|
+
// There is no settings.toml `stop=` / teardown hook for run scripts.
|
|
2436
|
+
...process.platform === "win32" ? {} : { detached: true }
|
|
2437
|
+
});
|
|
2438
|
+
pipeLines(child.stdout, opts.onLine);
|
|
2439
|
+
pipeLines(child.stderr, opts.onLine);
|
|
2440
|
+
const ports = opts.ports ?? [];
|
|
2441
|
+
return {
|
|
2442
|
+
pid: child.pid,
|
|
2443
|
+
kill: () => killScriptTree(child, ports),
|
|
2444
|
+
done: child.then((r) => r.exitCode ?? null),
|
|
2445
|
+
child
|
|
2446
|
+
};
|
|
2715
2447
|
}
|
|
2716
|
-
function
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2448
|
+
async function attachAbort(handle, signal) {
|
|
2449
|
+
if (signal) {
|
|
2450
|
+
const onAbort = () => handle.kill();
|
|
2451
|
+
if (signal.aborted) onAbort();
|
|
2452
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
2453
|
+
}
|
|
2454
|
+
return handle.done;
|
|
2722
2455
|
}
|
|
2723
|
-
function
|
|
2724
|
-
const
|
|
2456
|
+
async function runSetupScript(repoPath, worktreePath, onLine, opts) {
|
|
2457
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
2458
|
+
if (!settings?.setup) return { ran: false, exitCode: null, source: null };
|
|
2459
|
+
const source = workspaceSettingsSourceLabel(worktreePath, repoPath);
|
|
2460
|
+
onLine?.(`[setup] ${source ?? "settings.toml"}`);
|
|
2461
|
+
const handle = await spawnWorkspaceScript(settings.setup, {
|
|
2462
|
+
worktreePath,
|
|
2463
|
+
repoPath,
|
|
2464
|
+
defaultBranch: opts?.defaultBranch,
|
|
2465
|
+
onLine
|
|
2466
|
+
});
|
|
2467
|
+
const exitCode = await attachAbort(handle, opts?.signal);
|
|
2725
2468
|
return {
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2469
|
+
ran: true,
|
|
2470
|
+
exitCode,
|
|
2471
|
+
source,
|
|
2472
|
+
kill: handle.kill
|
|
2730
2473
|
};
|
|
2731
2474
|
}
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
messages: [historyRetentionStubMessage(thread)]
|
|
2742
|
-
},
|
|
2743
|
-
{ touch: false }
|
|
2744
|
-
);
|
|
2745
|
-
}
|
|
2746
|
-
function cleanupArchivedHistory(opts = {}) {
|
|
2747
|
-
const settings = loadAppSettings();
|
|
2748
|
-
if (!opts.force && opts.purgeOlderThanDays == null && !autoCleanupHistoryEnabled(settings)) {
|
|
2749
|
-
return { stripped: [], purged: [] };
|
|
2750
|
-
}
|
|
2751
|
-
const archived = listThreads({ includeArchived: true }).filter((t) => t.status === "archived");
|
|
2752
|
-
const nowMs = opts.nowMs ?? Date.now();
|
|
2753
|
-
const plan = opts.purgeOlderThanDays != null ? { stripIds: [], purgeIds: planHistoryAgePurge(archived, opts.purgeOlderThanDays, nowMs) } : planHistoryRetention(archived, {
|
|
2754
|
-
maxCount: opts.maxCount ?? historyMaxCount(settings),
|
|
2755
|
-
maxDays: opts.maxDays ?? historyMaxDays(settings),
|
|
2756
|
-
nowMs
|
|
2475
|
+
async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
|
|
2476
|
+
const found = findConventionSetup(worktreePath, repoPath);
|
|
2477
|
+
if (!found) return { ran: false, exitCode: null, source: null };
|
|
2478
|
+
onLine?.(`[setup] ${found.source}`);
|
|
2479
|
+
const handle = await spawnWorkspaceScript(found.command, {
|
|
2480
|
+
worktreePath,
|
|
2481
|
+
repoPath,
|
|
2482
|
+
defaultBranch: opts?.defaultBranch,
|
|
2483
|
+
onLine
|
|
2757
2484
|
});
|
|
2758
|
-
const
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2485
|
+
const exitCode = await attachAbort(handle, opts?.signal);
|
|
2486
|
+
return {
|
|
2487
|
+
ran: true,
|
|
2488
|
+
exitCode,
|
|
2489
|
+
source: found.source,
|
|
2490
|
+
kill: handle.kill
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2493
|
+
async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
|
|
2494
|
+
await ensureWorktreeSideboardIgnored(worktreePath);
|
|
2495
|
+
const review = ensureReviewGuidelinesFile(worktreePath, repoPath);
|
|
2496
|
+
if (review.wrote) {
|
|
2497
|
+
onLine?.(`[setup] wrote ${review.path} \u2014 local Review guidelines`);
|
|
2762
2498
|
}
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
try {
|
|
2767
|
-
stripArchivedTranscript(thread);
|
|
2768
|
-
stripped.push(id);
|
|
2769
|
-
} catch {
|
|
2770
|
-
}
|
|
2499
|
+
let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
|
|
2500
|
+
if (!setup.ran) {
|
|
2501
|
+
setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
|
|
2771
2502
|
}
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
if (!thread || thread.status !== "archived") continue;
|
|
2775
|
-
try {
|
|
2776
|
-
deleteThreadRecord(id);
|
|
2777
|
-
purged.push(id);
|
|
2778
|
-
} catch {
|
|
2779
|
-
}
|
|
2503
|
+
if (!setup.ran) {
|
|
2504
|
+
setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
|
|
2780
2505
|
}
|
|
2781
|
-
|
|
2782
|
-
updateAdvancedSettings({
|
|
2783
|
-
historyLastCleanupAt: new Date(nowMs).toISOString()
|
|
2784
|
-
});
|
|
2785
|
-
}
|
|
2786
|
-
return { stripped, purged };
|
|
2787
|
-
}
|
|
2788
|
-
function shouldRunHistoryCleanup(settings = loadAppSettings()) {
|
|
2789
|
-
if (!autoCleanupHistoryEnabled(settings)) return false;
|
|
2790
|
-
const intervalHours = settings.advanced.historyCleanupIntervalHours ?? HISTORY_CLEANUP_INTERVAL_HOURS_DEFAULT;
|
|
2791
|
-
const last = settings.advanced.historyLastCleanupAt;
|
|
2792
|
-
if (!last) return true;
|
|
2793
|
-
const elapsed = Date.now() - Date.parse(last);
|
|
2794
|
-
return Number.isFinite(elapsed) && elapsed >= intervalHours * 36e5;
|
|
2506
|
+
return setup;
|
|
2795
2507
|
}
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
cwd: thread.repoPath,
|
|
2804
|
-
reject: false
|
|
2508
|
+
async function runArchiveScript(repoPath, worktreePath, onLine) {
|
|
2509
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
2510
|
+
if (!settings?.archive) return { ran: false, exitCode: null };
|
|
2511
|
+
const handle = await spawnWorkspaceScript(settings.archive, {
|
|
2512
|
+
worktreePath,
|
|
2513
|
+
repoPath,
|
|
2514
|
+
onLine
|
|
2805
2515
|
});
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2516
|
+
const exitCode = await handle.done;
|
|
2517
|
+
return { ran: true, exitCode };
|
|
2518
|
+
}
|
|
2519
|
+
function listRunScripts(worktreePath, repoPath) {
|
|
2520
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
2521
|
+
if (!settings?.runScripts.length) return [];
|
|
2522
|
+
return settings.runScripts.filter((s) => {
|
|
2523
|
+
if (!s.availableIn?.length) return true;
|
|
2524
|
+
return s.availableIn.includes("local");
|
|
2814
2525
|
});
|
|
2815
|
-
if (checkout.exitCode !== 0) {
|
|
2816
|
-
throw new Error(
|
|
2817
|
-
`Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
|
|
2818
|
-
);
|
|
2819
|
-
}
|
|
2820
|
-
if (method === "cherry-pick") {
|
|
2821
|
-
const log = await execa3(
|
|
2822
|
-
"git",
|
|
2823
|
-
["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
|
|
2824
|
-
{ cwd: thread.repoPath, reject: false }
|
|
2825
|
-
);
|
|
2826
|
-
const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
2827
|
-
if (!shas.length) {
|
|
2828
|
-
return {
|
|
2829
|
-
applied: false,
|
|
2830
|
-
method,
|
|
2831
|
-
targetBranch: target,
|
|
2832
|
-
message: `No commits on ${thread.branchName} not already in ${target}`
|
|
2833
|
-
};
|
|
2834
|
-
}
|
|
2835
|
-
for (const sha of shas) {
|
|
2836
|
-
const cp = await execa3("git", ["cherry-pick", sha], {
|
|
2837
|
-
cwd: thread.repoPath,
|
|
2838
|
-
reject: false
|
|
2839
|
-
});
|
|
2840
|
-
if (cp.exitCode !== 0) {
|
|
2841
|
-
await execa3("git", ["cherry-pick", "--abort"], {
|
|
2842
|
-
cwd: thread.repoPath,
|
|
2843
|
-
reject: false
|
|
2844
|
-
});
|
|
2845
|
-
throw new Error(
|
|
2846
|
-
`Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
|
|
2847
|
-
);
|
|
2848
|
-
}
|
|
2849
|
-
}
|
|
2850
|
-
return {
|
|
2851
|
-
applied: true,
|
|
2852
|
-
method,
|
|
2853
|
-
targetBranch: target,
|
|
2854
|
-
message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
|
|
2855
|
-
};
|
|
2856
|
-
}
|
|
2857
|
-
const merge = await execa3(
|
|
2858
|
-
"git",
|
|
2859
|
-
["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
|
|
2860
|
-
{ cwd: thread.repoPath, reject: false }
|
|
2861
|
-
);
|
|
2862
|
-
if (merge.exitCode !== 0) {
|
|
2863
|
-
await execa3("git", ["merge", "--abort"], {
|
|
2864
|
-
cwd: thread.repoPath,
|
|
2865
|
-
reject: false
|
|
2866
|
-
});
|
|
2867
|
-
throw new Error(
|
|
2868
|
-
`Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
|
|
2869
|
-
);
|
|
2870
|
-
}
|
|
2871
|
-
return {
|
|
2872
|
-
applied: true,
|
|
2873
|
-
method: "merge",
|
|
2874
|
-
targetBranch: target,
|
|
2875
|
-
message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
|
|
2876
|
-
};
|
|
2877
2526
|
}
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
import { execa as execa4 } from "execa";
|
|
2883
|
-
async function cloneRepoIntoSideboard(opts) {
|
|
2884
|
-
const url = opts.url.trim();
|
|
2885
|
-
if (!url) throw new Error("Clone URL is required");
|
|
2886
|
-
let name = opts.name?.trim();
|
|
2887
|
-
if (!name) {
|
|
2888
|
-
const leaf = basename2(url.replace(/\/$/, "").replace(/\.git$/, ""));
|
|
2889
|
-
name = leaf || "repo";
|
|
2890
|
-
}
|
|
2891
|
-
name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
2892
|
-
const dest = join9(sideboardReposDir(), name);
|
|
2893
|
-
if (existsSync7(dest)) {
|
|
2894
|
-
const repoPath2 = await resolveRepoRoot(dest);
|
|
2895
|
-
const workspace2 = await ensureWorkspace(repoPath2);
|
|
2896
|
-
return { repoPath: repoPath2, workspace: workspace2 };
|
|
2897
|
-
}
|
|
2898
|
-
const result = await execa4("git", ["clone", url, dest], { reject: false });
|
|
2899
|
-
if (result.exitCode !== 0) {
|
|
2900
|
-
throw new Error(
|
|
2901
|
-
`git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
|
|
2902
|
-
);
|
|
2903
|
-
}
|
|
2904
|
-
const repoPath = await resolveRepoRoot(dest);
|
|
2905
|
-
const workspace = await ensureWorkspace(repoPath);
|
|
2906
|
-
return { repoPath, workspace };
|
|
2527
|
+
function getDefaultRunScript(worktreePath, repoPath) {
|
|
2528
|
+
const scripts = listRunScripts(worktreePath, repoPath);
|
|
2529
|
+
if (!scripts.length) return null;
|
|
2530
|
+
return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
|
|
2907
2531
|
}
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2532
|
+
function getRunScript(worktreePath, repoPath, name) {
|
|
2533
|
+
const scripts = listRunScripts(worktreePath, repoPath);
|
|
2534
|
+
if (!scripts.length) return null;
|
|
2535
|
+
if (name) {
|
|
2536
|
+
return scripts.find((s) => s.name === name) ?? null;
|
|
2537
|
+
}
|
|
2538
|
+
return getDefaultRunScript(worktreePath, repoPath);
|
|
2912
2539
|
}
|
|
2913
|
-
function
|
|
2914
|
-
return
|
|
2540
|
+
function getRunMode(worktreePath, repoPath) {
|
|
2541
|
+
return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
|
|
2915
2542
|
}
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2543
|
+
async function reservePort() {
|
|
2544
|
+
return new Promise((resolve2, reject) => {
|
|
2545
|
+
const server = createServer();
|
|
2546
|
+
let released = false;
|
|
2547
|
+
const release = () => new Promise((res) => {
|
|
2548
|
+
if (released) {
|
|
2549
|
+
res();
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
released = true;
|
|
2553
|
+
server.close(() => res());
|
|
2554
|
+
});
|
|
2555
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2556
|
+
const addr = server.address();
|
|
2557
|
+
if (!addr || typeof addr === "string") {
|
|
2558
|
+
void release().then(() => reject(new Error("Failed to allocate port")));
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
2561
|
+
resolve2({ port: addr.port, release });
|
|
2562
|
+
});
|
|
2563
|
+
server.on("error", reject);
|
|
2564
|
+
});
|
|
2922
2565
|
}
|
|
2923
|
-
function
|
|
2924
|
-
|
|
2925
|
-
|
|
2566
|
+
async function allocatePort() {
|
|
2567
|
+
const held = await reservePort();
|
|
2568
|
+
await held.release();
|
|
2569
|
+
return held.port;
|
|
2926
2570
|
}
|
|
2927
|
-
function
|
|
2928
|
-
if (
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2571
|
+
async function tryReservePort(port) {
|
|
2572
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
|
|
2573
|
+
return new Promise((resolve2) => {
|
|
2574
|
+
const server = createServer();
|
|
2575
|
+
let released = false;
|
|
2576
|
+
const release = () => new Promise((res) => {
|
|
2577
|
+
if (released) {
|
|
2578
|
+
res();
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
released = true;
|
|
2582
|
+
server.close(() => res());
|
|
2583
|
+
});
|
|
2584
|
+
server.once("error", () => {
|
|
2585
|
+
try {
|
|
2586
|
+
server.close();
|
|
2587
|
+
} catch {
|
|
2588
|
+
}
|
|
2589
|
+
resolve2(null);
|
|
2590
|
+
});
|
|
2591
|
+
server.listen(port, "127.0.0.1", () => {
|
|
2592
|
+
resolve2({ port, release });
|
|
2593
|
+
});
|
|
2594
|
+
});
|
|
2933
2595
|
}
|
|
2934
|
-
function
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2596
|
+
async function tryReservePorts(ports) {
|
|
2597
|
+
const held = [];
|
|
2598
|
+
for (const port of ports) {
|
|
2599
|
+
const next = await tryReservePort(port);
|
|
2600
|
+
if (!next) {
|
|
2601
|
+
await Promise.all(held.map((h) => h.release()));
|
|
2602
|
+
return null;
|
|
2603
|
+
}
|
|
2604
|
+
held.push(next);
|
|
2941
2605
|
}
|
|
2606
|
+
return held;
|
|
2942
2607
|
}
|
|
2943
|
-
function
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2608
|
+
function normalizePreferredPorts(ports, size) {
|
|
2609
|
+
if (!ports?.length) return [];
|
|
2610
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2611
|
+
const out = [];
|
|
2612
|
+
for (const p of ports) {
|
|
2613
|
+
if (!Number.isInteger(p) || p <= 0 || p > 65535 || seen.has(p)) continue;
|
|
2614
|
+
seen.add(p);
|
|
2615
|
+
out.push(p);
|
|
2616
|
+
if (out.length >= size) break;
|
|
2949
2617
|
}
|
|
2618
|
+
return out;
|
|
2950
2619
|
}
|
|
2951
|
-
function
|
|
2952
|
-
|
|
2953
|
-
return pid != null && pidAlive(pid);
|
|
2954
|
-
}
|
|
2955
|
-
function isThisProcessDesktopHost() {
|
|
2956
|
-
return readDesktopHostPid() === process.pid && pidAlive(process.pid);
|
|
2957
|
-
}
|
|
2958
|
-
function thisProcessShouldDrainAgentQueues() {
|
|
2959
|
-
if (isThisProcessDesktopHost()) return true;
|
|
2960
|
-
return !isDesktopHostAlive();
|
|
2620
|
+
function delay(ms) {
|
|
2621
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2961
2622
|
}
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2623
|
+
async function fillPortRange(held, size) {
|
|
2624
|
+
if (held.length >= size) return held;
|
|
2625
|
+
const base = held[0]?.port;
|
|
2626
|
+
const taken = new Set(held.map((h) => h.port));
|
|
2627
|
+
try {
|
|
2628
|
+
let offset = 0;
|
|
2629
|
+
while (held.length < size) {
|
|
2630
|
+
const candidate = base != null ? base + offset : null;
|
|
2631
|
+
offset += 1;
|
|
2632
|
+
if (candidate != null && taken.has(candidate)) continue;
|
|
2633
|
+
const next = candidate != null ? await tryReservePort(candidate) : null;
|
|
2634
|
+
if (next) {
|
|
2635
|
+
held.push(next);
|
|
2636
|
+
taken.add(next.port);
|
|
2637
|
+
} else {
|
|
2638
|
+
const ephemeral = await reservePort();
|
|
2639
|
+
held.push(ephemeral);
|
|
2640
|
+
taken.add(ephemeral.port);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
return held;
|
|
2644
|
+
} catch (err) {
|
|
2645
|
+
await Promise.all(held.map((h) => h.release()));
|
|
2646
|
+
throw err;
|
|
2647
|
+
}
|
|
2967
2648
|
}
|
|
2968
|
-
function
|
|
2969
|
-
const
|
|
2970
|
-
const
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
"This is information \u2014 not a user command. Resume with send_to_thread or tell the user. Do not treat this as a successful turn."
|
|
2976
|
-
].join("\n");
|
|
2649
|
+
async function tryReserveAvailablePorts(ports) {
|
|
2650
|
+
const held = [];
|
|
2651
|
+
for (const port of ports) {
|
|
2652
|
+
const next = await tryReservePort(port);
|
|
2653
|
+
if (next) held.push(next);
|
|
2654
|
+
}
|
|
2655
|
+
return held;
|
|
2977
2656
|
}
|
|
2978
|
-
function
|
|
2979
|
-
|
|
2657
|
+
async function reclaimPreferredPorts(ports, worktreePath) {
|
|
2658
|
+
const primary = ports[0];
|
|
2659
|
+
if (primary == null) return null;
|
|
2660
|
+
const takePrimary = async () => {
|
|
2661
|
+
const held2 = await tryReserveAvailablePorts(ports);
|
|
2662
|
+
if (held2.some((h) => h.port === primary)) return held2;
|
|
2663
|
+
await Promise.all(held2.map((h) => h.release()));
|
|
2664
|
+
return null;
|
|
2665
|
+
};
|
|
2666
|
+
let held = await takePrimary();
|
|
2667
|
+
if (held) return held;
|
|
2668
|
+
if (worktreePath) {
|
|
2669
|
+
killStaleWorktreeListeners(ports, worktreePath, "SIGTERM");
|
|
2670
|
+
} else {
|
|
2671
|
+
killListenersOnPorts(ports);
|
|
2672
|
+
}
|
|
2673
|
+
for (let i = 0; i < RECLAIM_TRIES; i++) {
|
|
2674
|
+
await delay(RECLAIM_RETRY_DELAY_MS);
|
|
2675
|
+
held = await takePrimary();
|
|
2676
|
+
if (held) return held;
|
|
2677
|
+
if (i === 2 && worktreePath) {
|
|
2678
|
+
killStaleWorktreeListeners(ports, worktreePath, "SIGKILL");
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
return null;
|
|
2980
2682
|
}
|
|
2981
|
-
function
|
|
2982
|
-
|
|
2983
|
-
if (
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2683
|
+
async function reservePortRange(size = PORT_RANGE_SIZE, opts) {
|
|
2684
|
+
const preferred = normalizePreferredPorts(opts?.preferred, size);
|
|
2685
|
+
if (preferred.length) {
|
|
2686
|
+
const reused = await reclaimPreferredPorts(preferred, opts?.worktreePath);
|
|
2687
|
+
if (reused) return fillPortRange(reused, size);
|
|
2688
|
+
}
|
|
2689
|
+
const held = [await reservePort()];
|
|
2690
|
+
return fillPortRange(held, size);
|
|
2987
2691
|
}
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2692
|
+
async function allocatePortRange(size = PORT_RANGE_SIZE) {
|
|
2693
|
+
const held = await reservePortRange(size);
|
|
2694
|
+
const ports = held.map((h) => h.port);
|
|
2695
|
+
await Promise.all(held.map((h) => h.release()));
|
|
2696
|
+
return ports;
|
|
2991
2697
|
}
|
|
2992
|
-
function
|
|
2993
|
-
const
|
|
2994
|
-
if (!
|
|
2995
|
-
const
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
void send(parentId, childHaltNotice(child, status)).catch(() => {
|
|
3000
|
-
notified.delete(key);
|
|
2698
|
+
async function startDevServer(repoPath, worktreePath, onLine, opts) {
|
|
2699
|
+
const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
|
|
2700
|
+
if (!script) return null;
|
|
2701
|
+
const preferred = loadWorktreeRunPorts(worktreePath, script.name) ?? opts?.preferredPorts;
|
|
2702
|
+
const held = await reservePortRange(PORT_RANGE_SIZE, {
|
|
2703
|
+
preferred: preferred ?? void 0,
|
|
2704
|
+
worktreePath
|
|
3001
2705
|
});
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
var MCP_WAIT_QUEUED_HINT = "Child is queued waiting for a concurrency slot \u2014 it has not started yet. Call wait_for_turn again. Do not send_to_thread a check-in (that steers / interrupts), force_stop, or assume it failed to start.";
|
|
3025
|
-
var MCP_WAIT_STOPPED_HINT = "Child was stopped before the turn finished. Do not treat this as success. send_to_thread to resume, or tell the user.";
|
|
3026
|
-
var MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
|
|
3027
|
-
var MCP_WAIT_ERROR_HINT = "Child turn failed. lastError/text is the failure \u2014 switch agent, tell the user, or retry. Do not treat empty text as success.";
|
|
3028
|
-
var MCP_WAIT_INPUT_REQUIRED_HINT = "Child asked the user a question (ask_user). taskState is input-required. Wait for the user to answer in that chat \u2014 do not send_to_thread a check-in.";
|
|
3029
|
-
function mcpWaitTaskHint(taskState, status) {
|
|
3030
|
-
switch (taskState) {
|
|
3031
|
-
case "submitted":
|
|
3032
|
-
return MCP_WAIT_QUEUED_HINT;
|
|
3033
|
-
case "working":
|
|
3034
|
-
return MCP_WAIT_STILL_RUNNING_HINT;
|
|
3035
|
-
case "input-required":
|
|
3036
|
-
return MCP_WAIT_INPUT_REQUIRED_HINT;
|
|
3037
|
-
case "canceled":
|
|
3038
|
-
return MCP_WAIT_STOPPED_HINT;
|
|
3039
|
-
case "failed":
|
|
3040
|
-
return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
|
|
3041
|
-
default:
|
|
3042
|
-
return void 0;
|
|
2706
|
+
const ports = held.map((h) => h.port);
|
|
2707
|
+
saveWorktreeRunPorts(worktreePath, script.name, ports);
|
|
2708
|
+
try {
|
|
2709
|
+
const handle = await spawnWorkspaceScript(script.command, {
|
|
2710
|
+
worktreePath,
|
|
2711
|
+
repoPath,
|
|
2712
|
+
ports,
|
|
2713
|
+
heldPorts: held,
|
|
2714
|
+
defaultBranch: opts?.defaultBranch,
|
|
2715
|
+
onLine
|
|
2716
|
+
});
|
|
2717
|
+
return {
|
|
2718
|
+
pid: handle.pid,
|
|
2719
|
+
port: ports[0],
|
|
2720
|
+
ports,
|
|
2721
|
+
scriptName: script.name,
|
|
2722
|
+
kill: handle.kill,
|
|
2723
|
+
done: handle.done
|
|
2724
|
+
};
|
|
2725
|
+
} catch (err) {
|
|
2726
|
+
await Promise.all(held.map((h) => h.release()));
|
|
2727
|
+
throw err;
|
|
3043
2728
|
}
|
|
3044
2729
|
}
|
|
3045
2730
|
|
|
3046
|
-
// src/
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
var
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
if (
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
return
|
|
2731
|
+
// src/store/setup-log.ts
|
|
2732
|
+
import { createHash } from "crypto";
|
|
2733
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
2734
|
+
import { join as join7 } from "path";
|
|
2735
|
+
var EMPTY = {
|
|
2736
|
+
output: "",
|
|
2737
|
+
running: false,
|
|
2738
|
+
exitCode: null,
|
|
2739
|
+
source: null
|
|
2740
|
+
};
|
|
2741
|
+
var MAX_SETUP_LOG_CHARS = 256e3;
|
|
2742
|
+
function appendSetupOutput(prev, line, max = MAX_SETUP_LOG_CHARS) {
|
|
2743
|
+
const next = prev ? `${prev}
|
|
2744
|
+
${line}` : line;
|
|
2745
|
+
if (next.length <= max) return next;
|
|
2746
|
+
const cut = next.slice(next.length - max);
|
|
2747
|
+
const nl = cut.indexOf("\n");
|
|
2748
|
+
return nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
|
|
3064
2749
|
}
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
2750
|
+
var memory = /* @__PURE__ */ new Map();
|
|
2751
|
+
var persistTimers = /* @__PURE__ */ new Map();
|
|
2752
|
+
function fromSnapshot(snap) {
|
|
2753
|
+
return {
|
|
2754
|
+
chunks: snap.output ? [snap.output] : [],
|
|
2755
|
+
length: snap.output.length,
|
|
2756
|
+
running: snap.running,
|
|
2757
|
+
exitCode: snap.exitCode,
|
|
2758
|
+
source: snap.source
|
|
2759
|
+
};
|
|
3073
2760
|
}
|
|
3074
|
-
function
|
|
3075
|
-
|
|
3076
|
-
if (
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
} catch (err) {
|
|
3081
|
-
return err && typeof err === "object" && "code" in err && err.code === "EPERM";
|
|
2761
|
+
function materialize(mem) {
|
|
2762
|
+
let output = mem.chunks.join("\n");
|
|
2763
|
+
if (output.length > MAX_SETUP_LOG_CHARS) {
|
|
2764
|
+
output = appendSetupOutput("", output);
|
|
2765
|
+
mem.chunks = [output];
|
|
2766
|
+
mem.length = output.length;
|
|
3082
2767
|
}
|
|
2768
|
+
return {
|
|
2769
|
+
output,
|
|
2770
|
+
running: mem.running,
|
|
2771
|
+
exitCode: mem.exitCode,
|
|
2772
|
+
source: mem.source
|
|
2773
|
+
};
|
|
3083
2774
|
}
|
|
3084
|
-
function
|
|
3085
|
-
|
|
3086
|
-
return jobAlive(pid) || processGroupAlive(pid);
|
|
2775
|
+
function setupLogPath(threadId) {
|
|
2776
|
+
return join7(threadsDir(), `${threadId}.setup.log.json`);
|
|
3087
2777
|
}
|
|
3088
|
-
function
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
return Number.isInteger(n) ? n : null;
|
|
2778
|
+
function setupLogKeyForWorktree(worktreePath) {
|
|
2779
|
+
const norm = normalizeWorktreePath(worktreePath);
|
|
2780
|
+
return `wt-${createHash("sha1").update(norm).digest("hex").slice(0, 16)}`;
|
|
3092
2781
|
}
|
|
3093
|
-
function
|
|
3094
|
-
return
|
|
2782
|
+
function emptySetupLog() {
|
|
2783
|
+
return { ...EMPTY };
|
|
3095
2784
|
}
|
|
3096
|
-
function
|
|
3097
|
-
|
|
3098
|
-
if (
|
|
3099
|
-
|
|
3100
|
-
if (
|
|
3101
|
-
return
|
|
2785
|
+
function mergeSetupOutput(prev, incoming) {
|
|
2786
|
+
if (!prev) return incoming;
|
|
2787
|
+
if (!incoming) return prev;
|
|
2788
|
+
if (prev === incoming) return prev;
|
|
2789
|
+
if (prev.startsWith(incoming) || prev.endsWith(incoming)) return prev;
|
|
2790
|
+
if (incoming.startsWith(prev) || incoming.endsWith(prev)) return incoming;
|
|
2791
|
+
return incoming.length >= prev.length ? incoming : prev;
|
|
3102
2792
|
}
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
2793
|
+
var PERSIST_DEBOUNCE_MS = 500;
|
|
2794
|
+
function persistNow(threadId) {
|
|
2795
|
+
const timer = persistTimers.get(threadId);
|
|
2796
|
+
if (timer) {
|
|
2797
|
+
clearTimeout(timer);
|
|
2798
|
+
persistTimers.delete(threadId);
|
|
2799
|
+
}
|
|
2800
|
+
const mem = memory.get(threadId);
|
|
2801
|
+
if (!mem) return;
|
|
2802
|
+
try {
|
|
2803
|
+
writeFileSync4(
|
|
2804
|
+
setupLogPath(threadId),
|
|
2805
|
+
`${JSON.stringify(materialize(mem))}
|
|
2806
|
+
`,
|
|
2807
|
+
"utf8"
|
|
2808
|
+
);
|
|
2809
|
+
} catch {
|
|
2810
|
+
}
|
|
3107
2811
|
}
|
|
3108
|
-
function
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
2812
|
+
function schedulePersist(threadId) {
|
|
2813
|
+
if (persistTimers.has(threadId)) return;
|
|
2814
|
+
persistTimers.set(
|
|
2815
|
+
threadId,
|
|
2816
|
+
setTimeout(() => {
|
|
2817
|
+
persistTimers.delete(threadId);
|
|
2818
|
+
persistNow(threadId);
|
|
2819
|
+
}, PERSIST_DEBOUNCE_MS)
|
|
2820
|
+
);
|
|
2821
|
+
}
|
|
2822
|
+
function loadFromDisk(threadId) {
|
|
2823
|
+
const path = setupLogPath(threadId);
|
|
2824
|
+
if (!existsSync5(path)) return null;
|
|
2825
|
+
try {
|
|
2826
|
+
const raw = JSON.parse(readFileSync5(path, "utf8"));
|
|
2827
|
+
return {
|
|
2828
|
+
output: typeof raw.output === "string" ? raw.output : "",
|
|
2829
|
+
running: Boolean(raw.running),
|
|
2830
|
+
exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
|
|
2831
|
+
source: typeof raw.source === "string" ? raw.source : null
|
|
2832
|
+
};
|
|
2833
|
+
} catch {
|
|
2834
|
+
return null;
|
|
3120
2835
|
}
|
|
3121
|
-
return running.sort();
|
|
3122
2836
|
}
|
|
3123
|
-
function
|
|
3124
|
-
const
|
|
3125
|
-
if (
|
|
3126
|
-
const
|
|
3127
|
-
|
|
3128
|
-
return
|
|
2837
|
+
function memFor(threadId) {
|
|
2838
|
+
const cached = memory.get(threadId);
|
|
2839
|
+
if (cached) return cached;
|
|
2840
|
+
const mem = fromSnapshot(loadFromDisk(threadId) ?? emptySetupLog());
|
|
2841
|
+
memory.set(threadId, mem);
|
|
2842
|
+
return mem;
|
|
3129
2843
|
}
|
|
3130
|
-
function
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
2844
|
+
function readSetupLog(threadId) {
|
|
2845
|
+
const cached = memory.get(threadId);
|
|
2846
|
+
if (cached) return materialize(cached);
|
|
2847
|
+
const disk = loadFromDisk(threadId);
|
|
2848
|
+
if (disk) {
|
|
2849
|
+
memory.set(threadId, fromSnapshot(disk));
|
|
2850
|
+
return disk;
|
|
2851
|
+
}
|
|
2852
|
+
return emptySetupLog();
|
|
3134
2853
|
}
|
|
3135
|
-
function
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
return
|
|
2854
|
+
function beginSetupLog(threadId) {
|
|
2855
|
+
const mem = {
|
|
2856
|
+
chunks: [],
|
|
2857
|
+
length: 0,
|
|
2858
|
+
running: true,
|
|
2859
|
+
exitCode: null,
|
|
2860
|
+
source: null
|
|
2861
|
+
};
|
|
2862
|
+
memory.set(threadId, mem);
|
|
2863
|
+
persistNow(threadId);
|
|
2864
|
+
return materialize(mem);
|
|
3146
2865
|
}
|
|
3147
|
-
function
|
|
3148
|
-
const
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
if (
|
|
3153
|
-
|
|
3154
|
-
|
|
2866
|
+
function appendSetupLog(threadId, line) {
|
|
2867
|
+
const mem = memFor(threadId);
|
|
2868
|
+
mem.chunks.push(line);
|
|
2869
|
+
mem.length += line.length + 1;
|
|
2870
|
+
mem.running = true;
|
|
2871
|
+
if (mem.length > MAX_SETUP_LOG_CHARS * 2) materialize(mem);
|
|
2872
|
+
schedulePersist(threadId);
|
|
2873
|
+
}
|
|
2874
|
+
function finishSetupLog(threadId, exitCode, source) {
|
|
2875
|
+
const mem = memFor(threadId);
|
|
2876
|
+
mem.running = false;
|
|
2877
|
+
mem.exitCode = exitCode;
|
|
2878
|
+
mem.source = source ?? mem.source;
|
|
2879
|
+
persistNow(threadId);
|
|
2880
|
+
return materialize(mem);
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
// src/store/run-log.ts
|
|
2884
|
+
import { createHash as createHash2 } from "crypto";
|
|
2885
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
2886
|
+
import { join as join8 } from "path";
|
|
2887
|
+
var EMPTY2 = {
|
|
2888
|
+
output: "",
|
|
2889
|
+
running: false,
|
|
2890
|
+
exitCode: null,
|
|
2891
|
+
scriptName: null
|
|
2892
|
+
};
|
|
2893
|
+
var MAX_RUN_LOG_CHARS = 256e3;
|
|
2894
|
+
var DEFAULT_RUN_LOG_TAIL_CHARS = 16e3;
|
|
2895
|
+
var memory2 = /* @__PURE__ */ new Map();
|
|
2896
|
+
var persistTimers2 = /* @__PURE__ */ new Map();
|
|
2897
|
+
function fromSnapshot2(snap) {
|
|
2898
|
+
return {
|
|
2899
|
+
chunks: snap.output ? [snap.output] : [],
|
|
2900
|
+
length: snap.output.length,
|
|
2901
|
+
running: snap.running,
|
|
2902
|
+
exitCode: snap.exitCode,
|
|
2903
|
+
scriptName: snap.scriptName
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
function materialize2(mem) {
|
|
2907
|
+
let output = mem.chunks.join("\n");
|
|
2908
|
+
if (output.length > MAX_RUN_LOG_CHARS) {
|
|
2909
|
+
output = appendSetupOutput("", output, MAX_RUN_LOG_CHARS);
|
|
2910
|
+
mem.chunks = [output];
|
|
2911
|
+
mem.length = output.length;
|
|
3155
2912
|
}
|
|
3156
|
-
return
|
|
2913
|
+
return {
|
|
2914
|
+
output,
|
|
2915
|
+
running: mem.running,
|
|
2916
|
+
exitCode: mem.exitCode,
|
|
2917
|
+
scriptName: mem.scriptName
|
|
2918
|
+
};
|
|
3157
2919
|
}
|
|
3158
|
-
function
|
|
3159
|
-
const
|
|
3160
|
-
return
|
|
3161
|
-
`Detached job still running: ${ids}.`,
|
|
3162
|
-
"Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
|
|
3163
|
-
"If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
|
|
3164
|
-
"Then report the result. Do not tell the user you will let them know later."
|
|
3165
|
-
].join(" ");
|
|
2920
|
+
function safeScriptToken(scriptName) {
|
|
2921
|
+
const token = scriptName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
|
|
2922
|
+
return token || "dev";
|
|
3166
2923
|
}
|
|
3167
|
-
function
|
|
3168
|
-
return
|
|
3169
|
-
"You ended the turn after promising to report later, but no detached job is running.",
|
|
3170
|
-
"If tests/pack/deploy still need to run: start once with detached-job.cjs, then loop wait_for_job until stillRunning is false (the log pane updates from wait JSON).",
|
|
3171
|
-
"Do not say you will let the user know later."
|
|
3172
|
-
].join(" ");
|
|
2924
|
+
function runLogPath(key) {
|
|
2925
|
+
return join8(threadsDir(), `${key}.run.log.json`);
|
|
3173
2926
|
}
|
|
3174
|
-
function
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
|
|
3179
|
-
return /detached-job\.(?:js|cjs)\b/i.test(blob);
|
|
3180
|
-
});
|
|
2927
|
+
function runLogKeyForWorktree(worktreePath, scriptName) {
|
|
2928
|
+
const norm = normalizeWorktreePath(worktreePath);
|
|
2929
|
+
const hash = createHash2("sha1").update(norm).digest("hex").slice(0, 16);
|
|
2930
|
+
return `wt-${hash}-run-${safeScriptToken(scriptName)}`;
|
|
3181
2931
|
}
|
|
3182
|
-
function
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
const
|
|
3188
|
-
if (
|
|
2932
|
+
function emptyRunLog() {
|
|
2933
|
+
return { ...EMPTY2 };
|
|
2934
|
+
}
|
|
2935
|
+
function sliceRunLogOutput(output, tailChars = DEFAULT_RUN_LOG_TAIL_CHARS) {
|
|
2936
|
+
const max = Math.max(1, tailChars);
|
|
2937
|
+
const outputChars = output.length;
|
|
2938
|
+
if (outputChars <= max) return { output, truncated: false, outputChars };
|
|
2939
|
+
const cut = output.slice(outputChars - max);
|
|
2940
|
+
const nl = cut.indexOf("\n");
|
|
2941
|
+
const text = nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
|
|
2942
|
+
return { output: text, truncated: true, outputChars };
|
|
2943
|
+
}
|
|
2944
|
+
var PERSIST_DEBOUNCE_MS2 = 500;
|
|
2945
|
+
function persistNow2(key) {
|
|
2946
|
+
const timer = persistTimers2.get(key);
|
|
2947
|
+
if (timer) {
|
|
2948
|
+
clearTimeout(timer);
|
|
2949
|
+
persistTimers2.delete(key);
|
|
2950
|
+
}
|
|
2951
|
+
const mem = memory2.get(key);
|
|
2952
|
+
if (!mem) return;
|
|
2953
|
+
try {
|
|
2954
|
+
writeFileSync5(runLogPath(key), `${JSON.stringify(materialize2(mem))}
|
|
2955
|
+
`, "utf8");
|
|
2956
|
+
} catch {
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
function schedulePersist2(key) {
|
|
2960
|
+
if (persistTimers2.has(key)) return;
|
|
2961
|
+
persistTimers2.set(
|
|
2962
|
+
key,
|
|
2963
|
+
setTimeout(() => {
|
|
2964
|
+
persistTimers2.delete(key);
|
|
2965
|
+
persistNow2(key);
|
|
2966
|
+
}, PERSIST_DEBOUNCE_MS2)
|
|
2967
|
+
);
|
|
2968
|
+
}
|
|
2969
|
+
function loadFromDisk2(key) {
|
|
2970
|
+
const path = runLogPath(key);
|
|
2971
|
+
if (!existsSync6(path)) return null;
|
|
2972
|
+
try {
|
|
2973
|
+
const raw = JSON.parse(readFileSync6(path, "utf8"));
|
|
3189
2974
|
return {
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
2975
|
+
output: typeof raw.output === "string" ? raw.output : "",
|
|
2976
|
+
running: Boolean(raw.running),
|
|
2977
|
+
exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
|
|
2978
|
+
scriptName: typeof raw.scriptName === "string" ? raw.scriptName : null
|
|
3193
2979
|
};
|
|
2980
|
+
} catch {
|
|
2981
|
+
return null;
|
|
3194
2982
|
}
|
|
3195
|
-
|
|
3196
|
-
|
|
2983
|
+
}
|
|
2984
|
+
function memFor2(key) {
|
|
2985
|
+
const cached = memory2.get(key);
|
|
2986
|
+
if (cached) return cached;
|
|
2987
|
+
const mem = fromSnapshot2(loadFromDisk2(key) ?? emptyRunLog());
|
|
2988
|
+
memory2.set(key, mem);
|
|
2989
|
+
return mem;
|
|
2990
|
+
}
|
|
2991
|
+
function readRunLog(key) {
|
|
2992
|
+
const cached = memory2.get(key);
|
|
2993
|
+
if (cached) return materialize2(cached);
|
|
2994
|
+
const disk = loadFromDisk2(key);
|
|
2995
|
+
if (disk) {
|
|
2996
|
+
memory2.set(key, fromSnapshot2(disk));
|
|
2997
|
+
return disk;
|
|
2998
|
+
}
|
|
2999
|
+
return emptyRunLog();
|
|
3000
|
+
}
|
|
3001
|
+
function beginRunLog(key, scriptName) {
|
|
3002
|
+
const mem = {
|
|
3003
|
+
chunks: [],
|
|
3004
|
+
length: 0,
|
|
3005
|
+
running: true,
|
|
3006
|
+
exitCode: null,
|
|
3007
|
+
scriptName
|
|
3008
|
+
};
|
|
3009
|
+
memory2.set(key, mem);
|
|
3010
|
+
persistNow2(key);
|
|
3011
|
+
return materialize2(mem);
|
|
3012
|
+
}
|
|
3013
|
+
function appendRunLog(key, line) {
|
|
3014
|
+
const mem = memFor2(key);
|
|
3015
|
+
mem.chunks.push(line);
|
|
3016
|
+
mem.length += line.length + 1;
|
|
3017
|
+
mem.running = true;
|
|
3018
|
+
if (mem.length > MAX_RUN_LOG_CHARS * 2) materialize2(mem);
|
|
3019
|
+
schedulePersist2(key);
|
|
3020
|
+
}
|
|
3021
|
+
function finishRunLog(key, exitCode) {
|
|
3022
|
+
const mem = memFor2(key);
|
|
3023
|
+
mem.running = false;
|
|
3024
|
+
mem.exitCode = exitCode;
|
|
3025
|
+
persistNow2(key);
|
|
3026
|
+
return materialize2(mem);
|
|
3027
|
+
}
|
|
3028
|
+
function flushRunLog(key) {
|
|
3029
|
+
persistNow2(key);
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
// src/git/orphan-cleanup.ts
|
|
3033
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, statSync } from "fs";
|
|
3034
|
+
import { join as join9 } from "path";
|
|
3035
|
+
function isSideboardWorktreePath(path) {
|
|
3036
|
+
return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
|
|
3037
|
+
}
|
|
3038
|
+
async function findOrphanWorktrees(repoPaths) {
|
|
3039
|
+
const threads = listThreads({ includeArchived: true });
|
|
3040
|
+
const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
|
|
3041
|
+
const repos = new Set(
|
|
3042
|
+
repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
|
|
3043
|
+
);
|
|
3044
|
+
const homeRoot = sideboardWorkspacesDir();
|
|
3045
|
+
if (existsSync7(homeRoot)) {
|
|
3046
|
+
try {
|
|
3047
|
+
for (const entry of readdirSync3(homeRoot, { withFileTypes: true })) {
|
|
3048
|
+
if (!entry.isDirectory()) continue;
|
|
3049
|
+
void entry;
|
|
3050
|
+
}
|
|
3051
|
+
} catch {
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
const orphans = [];
|
|
3055
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3056
|
+
for (const repoPath of repos) {
|
|
3057
|
+
if (!repoPath || !existsSync7(repoPath)) continue;
|
|
3058
|
+
try {
|
|
3059
|
+
const wts = await listWorktrees(repoPath);
|
|
3060
|
+
for (const wt of wts) {
|
|
3061
|
+
const path = wt.path.replace(/\/$/, "");
|
|
3062
|
+
if (!isSideboardWorktreePath(path)) continue;
|
|
3063
|
+
if (known.has(path)) continue;
|
|
3064
|
+
if (seen.has(path)) continue;
|
|
3065
|
+
seen.add(path);
|
|
3066
|
+
let mtimeMs = 0;
|
|
3067
|
+
try {
|
|
3068
|
+
mtimeMs = statSync(path).mtimeMs;
|
|
3069
|
+
} catch {
|
|
3070
|
+
mtimeMs = 0;
|
|
3071
|
+
}
|
|
3072
|
+
orphans.push({ path, repoPath, mtimeMs });
|
|
3073
|
+
}
|
|
3074
|
+
} catch {
|
|
3075
|
+
}
|
|
3076
|
+
try {
|
|
3077
|
+
const root = worktreesRoot(repoPath);
|
|
3078
|
+
if (existsSync7(root)) {
|
|
3079
|
+
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
3080
|
+
if (!entry.isDirectory()) continue;
|
|
3081
|
+
const path = join9(root, entry.name).replace(/\/$/, "");
|
|
3082
|
+
if (known.has(path) || seen.has(path)) continue;
|
|
3083
|
+
if (!existsSync7(join9(path, ".git"))) continue;
|
|
3084
|
+
seen.add(path);
|
|
3085
|
+
let mtimeMs = 0;
|
|
3086
|
+
try {
|
|
3087
|
+
mtimeMs = statSync(path).mtimeMs;
|
|
3088
|
+
} catch {
|
|
3089
|
+
mtimeMs = Date.now();
|
|
3090
|
+
}
|
|
3091
|
+
orphans.push({ path, repoPath, mtimeMs });
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
} catch {
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
3098
|
+
}
|
|
3099
|
+
async function cleanupOrphanWorktrees(opts) {
|
|
3100
|
+
const settings = loadAppSettings();
|
|
3101
|
+
const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
|
|
3102
|
+
const orphans = await findOrphanWorktrees(opts?.repoPaths);
|
|
3103
|
+
const activeCount = listThreads().filter((t) => t.status !== "archived").length;
|
|
3104
|
+
const budget = Math.max(0, maxCount - activeCount);
|
|
3105
|
+
const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
3106
|
+
const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
|
|
3107
|
+
const removed = [];
|
|
3108
|
+
const kept = [];
|
|
3109
|
+
for (const orphan of orphans) {
|
|
3110
|
+
if (keep.has(orphan.path)) {
|
|
3111
|
+
kept.push(orphan.path);
|
|
3112
|
+
continue;
|
|
3113
|
+
}
|
|
3114
|
+
if (opts?.dryRun) {
|
|
3115
|
+
removed.push(orphan.path);
|
|
3116
|
+
continue;
|
|
3117
|
+
}
|
|
3118
|
+
try {
|
|
3119
|
+
await removeWorktree(orphan.repoPath, orphan.path);
|
|
3120
|
+
removed.push(orphan.path);
|
|
3121
|
+
} catch {
|
|
3122
|
+
kept.push(orphan.path);
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
if (!opts?.dryRun) {
|
|
3126
|
+
updateAdvancedSettings({
|
|
3127
|
+
worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3128
|
+
});
|
|
3129
|
+
}
|
|
3130
|
+
return { removed, kept, orphans };
|
|
3131
|
+
}
|
|
3132
|
+
function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
|
|
3133
|
+
const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
|
|
3134
|
+
const last = settings.advanced.worktreeLastCleanupAt;
|
|
3135
|
+
if (!last) return true;
|
|
3136
|
+
const elapsed = Date.now() - Date.parse(last);
|
|
3137
|
+
return elapsed >= intervalHours * 36e5;
|
|
3138
|
+
}
|
|
3139
|
+
function worktreeCleanupSettings() {
|
|
3140
|
+
const a = loadAppSettings().advanced;
|
|
3141
|
+
return {
|
|
3142
|
+
worktreeMaxCount: a.worktreeMaxCount,
|
|
3143
|
+
worktreeCleanupIntervalHours: a.worktreeCleanupIntervalHours,
|
|
3144
|
+
worktreeLastCleanupAt: a.worktreeLastCleanupAt,
|
|
3145
|
+
autoCleanupOrphans: a.autoCleanupOrphans
|
|
3146
|
+
};
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
// src/store/history-cleanup.ts
|
|
3150
|
+
function stripArchivedTranscript(thread) {
|
|
3151
|
+
writeThread(
|
|
3152
|
+
{
|
|
3153
|
+
...thread,
|
|
3154
|
+
attachments: [],
|
|
3155
|
+
pendingTurnAttachments: [],
|
|
3156
|
+
queueAttachments: [],
|
|
3157
|
+
compactedMessages: void 0,
|
|
3158
|
+
messages: [historyRetentionStubMessage(thread)]
|
|
3159
|
+
},
|
|
3160
|
+
{ touch: false }
|
|
3161
|
+
);
|
|
3162
|
+
}
|
|
3163
|
+
function cleanupArchivedHistory(opts = {}) {
|
|
3164
|
+
const settings = loadAppSettings();
|
|
3165
|
+
if (!opts.force && opts.purgeOlderThanDays == null && !autoCleanupHistoryEnabled(settings)) {
|
|
3166
|
+
return { stripped: [], purged: [] };
|
|
3167
|
+
}
|
|
3168
|
+
const archived = listThreads({ includeArchived: true }).filter((t) => t.status === "archived");
|
|
3169
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
3170
|
+
const plan = opts.purgeOlderThanDays != null ? { stripIds: [], purgeIds: planHistoryAgePurge(archived, opts.purgeOlderThanDays, nowMs) } : planHistoryRetention(archived, {
|
|
3171
|
+
maxCount: opts.maxCount ?? historyMaxCount(settings),
|
|
3172
|
+
maxDays: opts.maxDays ?? historyMaxDays(settings),
|
|
3173
|
+
nowMs
|
|
3174
|
+
});
|
|
3175
|
+
const stripped = [];
|
|
3176
|
+
const purged = [];
|
|
3177
|
+
if (opts.dryRun) {
|
|
3178
|
+
return { stripped: plan.stripIds, purged: plan.purgeIds };
|
|
3179
|
+
}
|
|
3180
|
+
for (const id of plan.stripIds) {
|
|
3181
|
+
const thread = readThread(id);
|
|
3182
|
+
if (!thread || thread.status !== "archived") continue;
|
|
3183
|
+
try {
|
|
3184
|
+
stripArchivedTranscript(thread);
|
|
3185
|
+
stripped.push(id);
|
|
3186
|
+
} catch {
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
for (const id of plan.purgeIds) {
|
|
3190
|
+
const thread = readThread(id);
|
|
3191
|
+
if (!thread || thread.status !== "archived") continue;
|
|
3192
|
+
try {
|
|
3193
|
+
deleteThreadRecord(id);
|
|
3194
|
+
purged.push(id);
|
|
3195
|
+
} catch {
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
if (stripped.length > 0 || purged.length > 0) {
|
|
3199
|
+
updateAdvancedSettings({
|
|
3200
|
+
historyLastCleanupAt: new Date(nowMs).toISOString()
|
|
3201
|
+
});
|
|
3202
|
+
}
|
|
3203
|
+
return { stripped, purged };
|
|
3204
|
+
}
|
|
3205
|
+
function shouldRunHistoryCleanup(settings = loadAppSettings()) {
|
|
3206
|
+
if (!autoCleanupHistoryEnabled(settings)) return false;
|
|
3207
|
+
const intervalHours = settings.advanced.historyCleanupIntervalHours ?? HISTORY_CLEANUP_INTERVAL_HOURS_DEFAULT;
|
|
3208
|
+
const last = settings.advanced.historyLastCleanupAt;
|
|
3209
|
+
if (!last) return true;
|
|
3210
|
+
const elapsed = Date.now() - Date.parse(last);
|
|
3211
|
+
return Number.isFinite(elapsed) && elapsed >= intervalHours * 36e5;
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
// src/git/apply-into-main.ts
|
|
3215
|
+
import { execa as execa3 } from "execa";
|
|
3216
|
+
async function applyThreadIntoMain(thread, opts) {
|
|
3217
|
+
const method = opts?.method ?? "merge";
|
|
3218
|
+
const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
|
|
3219
|
+
const status = await execa3("git", ["status", "--porcelain"], {
|
|
3220
|
+
cwd: thread.repoPath,
|
|
3221
|
+
reject: false
|
|
3222
|
+
});
|
|
3223
|
+
if (status.stdout.trim()) {
|
|
3224
|
+
throw new Error(
|
|
3225
|
+
`Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
|
|
3226
|
+
);
|
|
3227
|
+
}
|
|
3228
|
+
const checkout = await execa3("git", ["checkout", target], {
|
|
3229
|
+
cwd: thread.repoPath,
|
|
3230
|
+
reject: false
|
|
3231
|
+
});
|
|
3232
|
+
if (checkout.exitCode !== 0) {
|
|
3233
|
+
throw new Error(
|
|
3234
|
+
`Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3237
|
+
if (method === "cherry-pick") {
|
|
3238
|
+
const log = await execa3(
|
|
3239
|
+
"git",
|
|
3240
|
+
["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
|
|
3241
|
+
{ cwd: thread.repoPath, reject: false }
|
|
3242
|
+
);
|
|
3243
|
+
const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
3244
|
+
if (!shas.length) {
|
|
3245
|
+
return {
|
|
3246
|
+
applied: false,
|
|
3247
|
+
method,
|
|
3248
|
+
targetBranch: target,
|
|
3249
|
+
message: `No commits on ${thread.branchName} not already in ${target}`
|
|
3250
|
+
};
|
|
3251
|
+
}
|
|
3252
|
+
for (const sha of shas) {
|
|
3253
|
+
const cp = await execa3("git", ["cherry-pick", sha], {
|
|
3254
|
+
cwd: thread.repoPath,
|
|
3255
|
+
reject: false
|
|
3256
|
+
});
|
|
3257
|
+
if (cp.exitCode !== 0) {
|
|
3258
|
+
await execa3("git", ["cherry-pick", "--abort"], {
|
|
3259
|
+
cwd: thread.repoPath,
|
|
3260
|
+
reject: false
|
|
3261
|
+
});
|
|
3262
|
+
throw new Error(
|
|
3263
|
+
`Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
|
|
3264
|
+
);
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
return {
|
|
3268
|
+
applied: true,
|
|
3269
|
+
method,
|
|
3270
|
+
targetBranch: target,
|
|
3271
|
+
message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
3274
|
+
const merge = await execa3(
|
|
3275
|
+
"git",
|
|
3276
|
+
["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
|
|
3277
|
+
{ cwd: thread.repoPath, reject: false }
|
|
3278
|
+
);
|
|
3279
|
+
if (merge.exitCode !== 0) {
|
|
3280
|
+
await execa3("git", ["merge", "--abort"], {
|
|
3281
|
+
cwd: thread.repoPath,
|
|
3282
|
+
reject: false
|
|
3283
|
+
});
|
|
3284
|
+
throw new Error(
|
|
3285
|
+
`Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
|
|
3286
|
+
);
|
|
3287
|
+
}
|
|
3288
|
+
return {
|
|
3289
|
+
applied: true,
|
|
3290
|
+
method: "merge",
|
|
3291
|
+
targetBranch: target,
|
|
3292
|
+
message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
|
|
3293
|
+
};
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
// src/git/clone-repo.ts
|
|
3297
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3298
|
+
import { basename as basename2, join as join10 } from "path";
|
|
3299
|
+
import { execa as execa4 } from "execa";
|
|
3300
|
+
async function cloneRepoIntoSideboard(opts) {
|
|
3301
|
+
const url = opts.url.trim();
|
|
3302
|
+
if (!url) throw new Error("Clone URL is required");
|
|
3303
|
+
let name = opts.name?.trim();
|
|
3304
|
+
if (!name) {
|
|
3305
|
+
const leaf = basename2(url.replace(/\/$/, "").replace(/\.git$/, ""));
|
|
3306
|
+
name = leaf || "repo";
|
|
3307
|
+
}
|
|
3308
|
+
name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
3309
|
+
const dest = join10(sideboardReposDir(), name);
|
|
3310
|
+
if (existsSync8(dest)) {
|
|
3311
|
+
const repoPath2 = await resolveRepoRoot(dest);
|
|
3312
|
+
const workspace2 = await ensureWorkspace(repoPath2);
|
|
3313
|
+
return { repoPath: repoPath2, workspace: workspace2 };
|
|
3314
|
+
}
|
|
3315
|
+
const result = await execa4("git", ["clone", url, dest], { reject: false });
|
|
3316
|
+
if (result.exitCode !== 0) {
|
|
3317
|
+
throw new Error(
|
|
3318
|
+
`git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
|
|
3319
|
+
);
|
|
3197
3320
|
}
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
if (!existsSync8(logFile)) return "(no log yet)";
|
|
3202
|
-
const lines = readFileSync7(logFile, "utf8").split("\n");
|
|
3203
|
-
return lines.slice(-maxLines).join("\n");
|
|
3321
|
+
const repoPath = await resolveRepoRoot(dest);
|
|
3322
|
+
const workspace = await ensureWorkspace(repoPath);
|
|
3323
|
+
return { repoPath, workspace };
|
|
3204
3324
|
}
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3325
|
+
|
|
3326
|
+
// src/types/thread.ts
|
|
3327
|
+
function isUnclaimedRunScriptRequest(req) {
|
|
3328
|
+
return Boolean(req && !req.claimedAt && !req.fulfilledAt && !req.error);
|
|
3208
3329
|
}
|
|
3209
|
-
function
|
|
3210
|
-
|
|
3211
|
-
const lines = readFileSync7(file, "utf8").split("\n");
|
|
3212
|
-
if (lines.length && lines[lines.length - 1] === "") lines.pop();
|
|
3213
|
-
return lines;
|
|
3330
|
+
function isAdoptableRunScriptRequest(req) {
|
|
3331
|
+
return Boolean(req && !req.fulfilledAt && !req.error);
|
|
3214
3332
|
}
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3333
|
+
|
|
3334
|
+
// src/store/desktop-host.ts
|
|
3335
|
+
import { readFileSync as readFileSync7, unlinkSync, writeFileSync as writeFileSync6 } from "fs";
|
|
3336
|
+
import { join as join11 } from "path";
|
|
3337
|
+
function desktopHostPidPath() {
|
|
3338
|
+
return join11(appDataDir(), "desktop-host.pid");
|
|
3220
3339
|
}
|
|
3221
|
-
function
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
return {
|
|
3225
|
-
pid,
|
|
3226
|
-
running,
|
|
3227
|
-
exitCode: readIntFile(join11(dir, "exit")),
|
|
3228
|
-
log: join11(dir, "log"),
|
|
3229
|
-
cursor: join11(dir, "present.cursor"),
|
|
3230
|
-
progress: tailProgress(join11(dir, "log"))
|
|
3231
|
-
};
|
|
3340
|
+
function claimDesktopHost(pid = process.pid) {
|
|
3341
|
+
writeFileSync6(desktopHostPidPath(), `${pid}
|
|
3342
|
+
`, "utf8");
|
|
3232
3343
|
}
|
|
3233
|
-
function
|
|
3234
|
-
|
|
3235
|
-
const ok = !snap.running && snap.exitCode === 0;
|
|
3236
|
-
const stillRunning = snap.running && !ok;
|
|
3237
|
-
const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
|
|
3344
|
+
function releaseDesktopHost(pid = process.pid) {
|
|
3345
|
+
if (readDesktopHostPid() !== pid) return;
|
|
3238
3346
|
try {
|
|
3239
|
-
|
|
3240
|
-
writeFileSync6(snap.cursor, `${nextCursor}
|
|
3241
|
-
`);
|
|
3347
|
+
unlinkSync(desktopHostPidPath());
|
|
3242
3348
|
} catch {
|
|
3243
3349
|
}
|
|
3244
|
-
const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
|
|
3245
|
-
const progress = extra?.progress ?? snap.progress;
|
|
3246
|
-
return {
|
|
3247
|
-
stillRunning,
|
|
3248
|
-
ok,
|
|
3249
|
-
failed: Boolean(failed && !stillRunning && !ok),
|
|
3250
|
-
status,
|
|
3251
|
-
id,
|
|
3252
|
-
pid: snap.pid,
|
|
3253
|
-
exitCode: snap.exitCode ?? void 0,
|
|
3254
|
-
phase: lastProgressLine(progress),
|
|
3255
|
-
delta,
|
|
3256
|
-
progress,
|
|
3257
|
-
hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
|
|
3258
|
-
};
|
|
3259
|
-
}
|
|
3260
|
-
async function sleepMs(ms) {
|
|
3261
|
-
await new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3262
|
-
}
|
|
3263
|
-
async function waitForDetachedJob(cwd, id, opts) {
|
|
3264
|
-
const jobId = sanitizeDetachedJobId(id);
|
|
3265
|
-
const root = cwd.trim() || process.cwd();
|
|
3266
|
-
const dir = resolveJobDir(root, jobId);
|
|
3267
|
-
const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
|
|
3268
|
-
const sleep = opts?.sleep ?? sleepMs;
|
|
3269
|
-
if (!existsSync8(dir)) {
|
|
3270
|
-
return {
|
|
3271
|
-
stillRunning: false,
|
|
3272
|
-
ok: false,
|
|
3273
|
-
failed: true,
|
|
3274
|
-
status: "failed",
|
|
3275
|
-
id: jobId,
|
|
3276
|
-
delta: "",
|
|
3277
|
-
progress: "No detached job. Start one first.",
|
|
3278
|
-
hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
|
|
3279
|
-
};
|
|
3280
|
-
}
|
|
3281
|
-
const deadline = Date.now() + timeoutMs;
|
|
3282
|
-
let snap = snapshotJob(dir);
|
|
3283
|
-
if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
|
|
3284
|
-
if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
|
|
3285
|
-
return toResult(jobId, snap, {
|
|
3286
|
-
failed: true,
|
|
3287
|
-
progress: "No detached job. Start one first."
|
|
3288
|
-
});
|
|
3289
|
-
}
|
|
3290
|
-
while (Date.now() < deadline) {
|
|
3291
|
-
snap = snapshotJob(dir);
|
|
3292
|
-
if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
|
|
3293
|
-
if (!snap.running) return toResult(jobId, snap, { failed: true });
|
|
3294
|
-
await sleep(Math.min(2e3, Math.max(50, deadline - Date.now())));
|
|
3295
|
-
}
|
|
3296
|
-
snap = snapshotJob(dir);
|
|
3297
|
-
return toResult(jobId, snap);
|
|
3298
3350
|
}
|
|
3299
|
-
function
|
|
3300
|
-
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
3301
|
-
if (process.platform !== "win32") {
|
|
3302
|
-
try {
|
|
3303
|
-
process.kill(-pid, signal);
|
|
3304
|
-
return;
|
|
3305
|
-
} catch {
|
|
3306
|
-
}
|
|
3307
|
-
}
|
|
3351
|
+
function readDesktopHostPid() {
|
|
3308
3352
|
try {
|
|
3309
|
-
|
|
3353
|
+
const pid = Number.parseInt(readFileSync7(desktopHostPidPath(), "utf8").trim(), 10);
|
|
3354
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
3355
|
+
return pid;
|
|
3310
3356
|
} catch {
|
|
3357
|
+
return null;
|
|
3311
3358
|
}
|
|
3312
3359
|
}
|
|
3313
|
-
function
|
|
3360
|
+
function pidAlive(pid) {
|
|
3314
3361
|
try {
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
`;
|
|
3318
|
-
appendFileSync2(logFile, body);
|
|
3362
|
+
process.kill(pid, 0);
|
|
3363
|
+
return true;
|
|
3319
3364
|
} catch {
|
|
3365
|
+
return false;
|
|
3320
3366
|
}
|
|
3321
3367
|
}
|
|
3322
|
-
|
|
3323
|
-
const
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
}
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
if (
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
const
|
|
3365
|
-
if (!
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
const stopped = !stillRunning;
|
|
3375
|
-
return {
|
|
3376
|
-
...result,
|
|
3377
|
-
stillRunning,
|
|
3378
|
-
ok: false,
|
|
3379
|
-
failed: true,
|
|
3380
|
-
status: stillRunning ? "running" : "failed",
|
|
3381
|
-
stopped,
|
|
3382
|
-
reason: stopped ? "stopped" : "still-running",
|
|
3383
|
-
stopReason: why || void 0,
|
|
3384
|
-
hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
|
|
3385
|
-
};
|
|
3368
|
+
function isDesktopHostAlive() {
|
|
3369
|
+
const pid = readDesktopHostPid();
|
|
3370
|
+
return pid != null && pidAlive(pid);
|
|
3371
|
+
}
|
|
3372
|
+
function isThisProcessDesktopHost() {
|
|
3373
|
+
return readDesktopHostPid() === process.pid && pidAlive(process.pid);
|
|
3374
|
+
}
|
|
3375
|
+
function thisProcessShouldDrainAgentQueues() {
|
|
3376
|
+
if (isThisProcessDesktopHost()) return true;
|
|
3377
|
+
return !isDesktopHostAlive();
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
// src/orchestrator/child-halt.ts
|
|
3381
|
+
var HALT_STATUSES = /* @__PURE__ */ new Set(["stopped", "error", "broken"]);
|
|
3382
|
+
function isIncompleteChildStatus(status) {
|
|
3383
|
+
return HALT_STATUSES.has(status);
|
|
3384
|
+
}
|
|
3385
|
+
function childHaltNotice(child, status) {
|
|
3386
|
+
const title = child.title?.trim() || "Untitled";
|
|
3387
|
+
const link = `[${title}](sideboard://thread/${child.id})`;
|
|
3388
|
+
const why = child.lastError?.trim();
|
|
3389
|
+
const extra = why ? ` lastError: ${why}` : "";
|
|
3390
|
+
return [
|
|
3391
|
+
`Sideboard: child worktree ${link} ${status} before finishing (status=${status}).${extra}`,
|
|
3392
|
+
"This is information \u2014 not a user command. Resume with send_to_thread or tell the user. Do not treat this as a successful turn."
|
|
3393
|
+
].join("\n");
|
|
3394
|
+
}
|
|
3395
|
+
function shouldNotifyParentAfterTurnError(opts) {
|
|
3396
|
+
return !opts.quotaFailoverHandled && !opts.crashContinued;
|
|
3397
|
+
}
|
|
3398
|
+
function shouldNotifyParentOfChildHalt(opts) {
|
|
3399
|
+
if (!isIncompleteChildStatus(opts.status)) return false;
|
|
3400
|
+
if (!opts.child.parentThreadId) return false;
|
|
3401
|
+
if (!opts.parent || opts.parent.status === "archived") return false;
|
|
3402
|
+
if (opts.parent.id === opts.child.id) return false;
|
|
3403
|
+
return isOrchestratorThread(opts.parent);
|
|
3404
|
+
}
|
|
3405
|
+
var notified = /* @__PURE__ */ new Set();
|
|
3406
|
+
function noticeKey(childId, status) {
|
|
3407
|
+
return `${childId}:${status}`;
|
|
3408
|
+
}
|
|
3409
|
+
function notifyParentOfChildHalt(child, status, send) {
|
|
3410
|
+
const parent = child.parentThreadId ? readThread(child.parentThreadId) : null;
|
|
3411
|
+
if (!shouldNotifyParentOfChildHalt({ child, parent, status })) return false;
|
|
3412
|
+
const key = noticeKey(child.id, status);
|
|
3413
|
+
if (notified.has(key)) return false;
|
|
3414
|
+
notified.add(key);
|
|
3415
|
+
const parentId = parent.id;
|
|
3416
|
+
void send(parentId, childHaltNotice(child, status)).catch(() => {
|
|
3417
|
+
notified.delete(key);
|
|
3418
|
+
});
|
|
3419
|
+
return true;
|
|
3386
3420
|
}
|
|
3387
3421
|
|
|
3388
3422
|
// src/threads/create.ts
|
|
@@ -4027,7 +4061,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
4027
4061
|
});
|
|
4028
4062
|
await requireAgent(resolved.agent);
|
|
4029
4063
|
if (input.cowboy) {
|
|
4030
|
-
const { cowboyModeEnabled } = await import("./app-settings-
|
|
4064
|
+
const { cowboyModeEnabled } = await import("./app-settings-VOPXJMGV.js");
|
|
4031
4065
|
if (!cowboyModeEnabled()) {
|
|
4032
4066
|
throw new Error(
|
|
4033
4067
|
"Cowboy mode is off. Enable it in Settings \u2192 Advanced."
|
|
@@ -4085,7 +4119,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
4085
4119
|
let sourceIsFork = false;
|
|
4086
4120
|
if (!input.cowboy && sourceType === "branch" && (!sourceRef || sourceRef === "HEAD" || sourceRef === "default")) {
|
|
4087
4121
|
if (isAbleTimeConnected() && getIssueSource() === "abletime") {
|
|
4088
|
-
const { ensureAbleTimeTask, issueAttachmentForAbleTimeTask } = await import("./abletime-
|
|
4122
|
+
const { ensureAbleTimeTask, issueAttachmentForAbleTimeTask } = await import("./abletime-CWIJ7INC.js");
|
|
4089
4123
|
const task = await ensureAbleTimeTask({
|
|
4090
4124
|
title: title?.trim() || "Untitled work",
|
|
4091
4125
|
description: input.prompt?.trim() || void 0
|
|
@@ -4187,7 +4221,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
4187
4221
|
return readThread(thread.id) ?? thread;
|
|
4188
4222
|
}
|
|
4189
4223
|
async function listLinearIssues(agent, repoPath) {
|
|
4190
|
-
const { getAdapter: getAdapter2 } = await import("./agents-
|
|
4224
|
+
const { getAdapter: getAdapter2 } = await import("./agents-CPME6O2H.js");
|
|
4191
4225
|
await requireAgent(agent, { requireLinear: true });
|
|
4192
4226
|
const adapter = getAdapter2(agent);
|
|
4193
4227
|
if (!adapter.listLinearIssues) {
|
|
@@ -4660,7 +4694,7 @@ async function adoptThread(input) {
|
|
|
4660
4694
|
messages: input.messages ?? []
|
|
4661
4695
|
});
|
|
4662
4696
|
writeThread(thread);
|
|
4663
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
4697
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-WYC5TLC3.js");
|
|
4664
4698
|
await ensureWorkspace2(repoPath);
|
|
4665
4699
|
const { ensureWorktreeSideboardIgnored: ensureWorktreeSideboardIgnored2 } = await import("./worktree-exclude-RXZPTCNH.js");
|
|
4666
4700
|
await ensureWorktreeSideboardIgnored2(input.worktreePath);
|
|
@@ -7372,7 +7406,7 @@ function formatScheduledPrompt(name, prompt) {
|
|
|
7372
7406
|
${prompt}`;
|
|
7373
7407
|
}
|
|
7374
7408
|
async function defaultDeps() {
|
|
7375
|
-
const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-
|
|
7409
|
+
const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-YDPDMXUY.js");
|
|
7376
7410
|
const orch = getOrchestrator2();
|
|
7377
7411
|
return {
|
|
7378
7412
|
findThread: (id) => findThreadByRef(id),
|
|
@@ -7697,7 +7731,7 @@ var Orchestrator = class {
|
|
|
7697
7731
|
orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
|
|
7698
7732
|
});
|
|
7699
7733
|
}
|
|
7700
|
-
const { autoCleanupOrphansEnabled } = await import("./app-settings-
|
|
7734
|
+
const { autoCleanupOrphansEnabled } = await import("./app-settings-VOPXJMGV.js");
|
|
7701
7735
|
if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
|
|
7702
7736
|
await cleanupOrphanWorktrees({ repoPaths });
|
|
7703
7737
|
}
|
|
@@ -8044,7 +8078,7 @@ var Orchestrator = class {
|
|
|
8044
8078
|
}
|
|
8045
8079
|
await setup;
|
|
8046
8080
|
if (skipSetup) return;
|
|
8047
|
-
const { autoRunAfterSetupEnabled } = await import("./app-settings-
|
|
8081
|
+
const { autoRunAfterSetupEnabled } = await import("./app-settings-VOPXJMGV.js");
|
|
8048
8082
|
if (autoRunAfterSetupEnabled()) {
|
|
8049
8083
|
try {
|
|
8050
8084
|
await this.startDev(threadId);
|
|
@@ -8295,7 +8329,8 @@ var Orchestrator = class {
|
|
|
8295
8329
|
if (compact.didCompact) {
|
|
8296
8330
|
thread = updateThread(threadId, {
|
|
8297
8331
|
messages: compact.thread.messages,
|
|
8298
|
-
sessionId: compact.thread.sessionId
|
|
8332
|
+
sessionId: compact.thread.sessionId,
|
|
8333
|
+
compactedMessages: compact.thread.compactedMessages
|
|
8299
8334
|
});
|
|
8300
8335
|
this.emit({
|
|
8301
8336
|
type: "context_compacted",
|
|
@@ -8309,9 +8344,6 @@ var Orchestrator = class {
|
|
|
8309
8344
|
this.emit({ type: "error", threadId, message: `context compact failed: ${message}` });
|
|
8310
8345
|
thread = this.requireThread(threadId);
|
|
8311
8346
|
}
|
|
8312
|
-
if (this.stoppedTurns.has(threadId)) {
|
|
8313
|
-
return;
|
|
8314
|
-
}
|
|
8315
8347
|
const promptText = typeof prompt === "string" ? prompt : "";
|
|
8316
8348
|
const autoContinue = isJobContinuePrompt(promptText) || promptText.startsWith("The previous agent process ended before it finished.");
|
|
8317
8349
|
const parked = takePendingTurnAttachments(thread);
|
|
@@ -8328,6 +8360,14 @@ var Orchestrator = class {
|
|
|
8328
8360
|
agent: thread.agent,
|
|
8329
8361
|
orch: isOrchestratorThread(thread)
|
|
8330
8362
|
});
|
|
8363
|
+
if (this.stoppedTurns.has(threadId)) {
|
|
8364
|
+
const stopped = writeLiveStatus(threadId, "stopped");
|
|
8365
|
+
if (stopped?.status === "stopped") {
|
|
8366
|
+
this.emit({ type: "status_changed", threadId, status: "stopped" });
|
|
8367
|
+
}
|
|
8368
|
+
this.emit({ type: "turn_finished", threadId, exitCode: 1 });
|
|
8369
|
+
return;
|
|
8370
|
+
}
|
|
8331
8371
|
thread = this.requireThread(threadId);
|
|
8332
8372
|
const gitPrompt = expandCanonicalGitRequest(promptText);
|
|
8333
8373
|
const { agentPrompt: expandedPrompt } = expandComposerPrompt(
|
|
@@ -8386,7 +8426,7 @@ var Orchestrator = class {
|
|
|
8386
8426
|
].filter(Boolean).join("\n\n") || void 0;
|
|
8387
8427
|
const prGateDirective = thread.agent !== "brightsy" && !isOrchestratorThread(thread) && mentionsPrGoal(promptText) ? formatPrGateDirective() : null;
|
|
8388
8428
|
const agentPrompt = [
|
|
8389
|
-
thread.planMode
|
|
8429
|
+
planModeTurnInstruction(thread.planMode),
|
|
8390
8430
|
orchestrationReminder,
|
|
8391
8431
|
prGateDirective,
|
|
8392
8432
|
injectedNoticeContext,
|
|
@@ -8408,7 +8448,7 @@ var Orchestrator = class {
|
|
|
8408
8448
|
}
|
|
8409
8449
|
const isBrightsy = fresh.agent === "brightsy";
|
|
8410
8450
|
const isOrchestration = isOrchestratorThread(fresh);
|
|
8411
|
-
const { autoRenameBranchEnabled, getGithubGitAuthMode, gitBranchPrefixSetting } = await import("./app-settings-
|
|
8451
|
+
const { autoRenameBranchEnabled, getGithubGitAuthMode, gitBranchPrefixSetting } = await import("./app-settings-VOPXJMGV.js");
|
|
8412
8452
|
const gitAuthMode = getGithubGitAuthMode();
|
|
8413
8453
|
const worktreeDirective = isBrightsy || isOrchestration ? null : formatWorktreeDirective(fresh, {
|
|
8414
8454
|
githubSlug: await withTimeout(
|
|
@@ -9699,7 +9739,7 @@ var Orchestrator = class {
|
|
|
9699
9739
|
if (!titled.userSetTitle && meta.title && titled.title !== meta.title) {
|
|
9700
9740
|
updateThread(thread.id, { title: meta.title });
|
|
9701
9741
|
}
|
|
9702
|
-
const { autoArchiveOnMergeEnabled } = await import("./app-settings-
|
|
9742
|
+
const { autoArchiveOnMergeEnabled } = await import("./app-settings-VOPXJMGV.js");
|
|
9703
9743
|
const latest = this.requireThread(thread.id);
|
|
9704
9744
|
if (!shouldAutoArchiveOnPrMerge({
|
|
9705
9745
|
previousPrState: prevState || null,
|
|
@@ -9981,7 +10021,7 @@ var Orchestrator = class {
|
|
|
9981
10021
|
this.enforceHistoryRetention();
|
|
9982
10022
|
if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
|
|
9983
10023
|
try {
|
|
9984
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
10024
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-WYC5TLC3.js");
|
|
9985
10025
|
await ensureWorkspace2(thread.repoPath);
|
|
9986
10026
|
} catch {
|
|
9987
10027
|
}
|
|
@@ -10008,7 +10048,7 @@ var Orchestrator = class {
|
|
|
10008
10048
|
await runArchiveScript(thread.repoPath, thread.worktreePath);
|
|
10009
10049
|
} catch {
|
|
10010
10050
|
}
|
|
10011
|
-
const { deleteBranchOnPurgeEnabled } = await import("./app-settings-
|
|
10051
|
+
const { deleteBranchOnPurgeEnabled } = await import("./app-settings-VOPXJMGV.js");
|
|
10012
10052
|
const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
|
|
10013
10053
|
await removeWorktree(thread.repoPath, thread.worktreePath, {
|
|
10014
10054
|
deleteBranch: deleteBranch ? thread.branchName : void 0
|
|
@@ -10035,7 +10075,7 @@ var Orchestrator = class {
|
|
|
10035
10075
|
`Cowboy checkout missing: ${thread.worktreePath}. Re-add the project folder, then restore.`
|
|
10036
10076
|
);
|
|
10037
10077
|
}
|
|
10038
|
-
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-
|
|
10078
|
+
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-DGDM7LGV.js");
|
|
10039
10079
|
const slug = thread.worktreePath.split("/").pop();
|
|
10040
10080
|
const dest = thread.worktreePath;
|
|
10041
10081
|
await withRepoGitLock(thread.repoPath, async () => {
|
|
@@ -10145,7 +10185,7 @@ async function startOrchestration(opts) {
|
|
|
10145
10185
|
sourceRef: "default",
|
|
10146
10186
|
...createOpts
|
|
10147
10187
|
}).catch(async () => {
|
|
10148
|
-
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-
|
|
10188
|
+
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-DGDM7LGV.js");
|
|
10149
10189
|
const repo = await resolveRepoRoot2(repoPath);
|
|
10150
10190
|
const def = await resolveDefaultBranch2(repo);
|
|
10151
10191
|
return orch.createThread({
|
|
@@ -10155,7 +10195,7 @@ async function startOrchestration(opts) {
|
|
|
10155
10195
|
repoPath: repo
|
|
10156
10196
|
});
|
|
10157
10197
|
});
|
|
10158
|
-
const { updateThread: upd } = await import("./thread-store-
|
|
10198
|
+
const { updateThread: upd } = await import("./thread-store-SCV2IZYS.js");
|
|
10159
10199
|
const updated = upd(thread.id, {
|
|
10160
10200
|
sourceType: "orchestration",
|
|
10161
10201
|
sourceRef: goal
|
|
@@ -10260,6 +10300,9 @@ export {
|
|
|
10260
10300
|
findLastBrightsyContextSummary,
|
|
10261
10301
|
messagesSinceLastBrightsyContextSummary,
|
|
10262
10302
|
buildBrightsySessionSeed,
|
|
10303
|
+
compactArchiveProse,
|
|
10304
|
+
messagesForCompactArchive,
|
|
10305
|
+
appendCompactedMessages,
|
|
10263
10306
|
applyCompaction,
|
|
10264
10307
|
lastRequestOccupancy,
|
|
10265
10308
|
shouldResetSessionForOccupancy,
|
|
@@ -10295,6 +10338,11 @@ export {
|
|
|
10295
10338
|
worktreeRunPortsPath,
|
|
10296
10339
|
loadWorktreeRunPorts,
|
|
10297
10340
|
saveWorktreeRunPorts,
|
|
10341
|
+
mcpWaitForTurnTimeoutMs,
|
|
10342
|
+
mcpWaitTaskHint,
|
|
10343
|
+
mcpWaitForJobTimeoutMs,
|
|
10344
|
+
waitForDetachedJob,
|
|
10345
|
+
stopDetachedJob,
|
|
10298
10346
|
PORT_RANGE_SIZE,
|
|
10299
10347
|
readWorktreeInclude,
|
|
10300
10348
|
resolveFilesToCopy,
|
|
@@ -10331,11 +10379,6 @@ export {
|
|
|
10331
10379
|
shouldRunHistoryCleanup,
|
|
10332
10380
|
applyThreadIntoMain,
|
|
10333
10381
|
cloneRepoIntoSideboard,
|
|
10334
|
-
mcpWaitForTurnTimeoutMs,
|
|
10335
|
-
mcpWaitTaskHint,
|
|
10336
|
-
mcpWaitForJobTimeoutMs,
|
|
10337
|
-
waitForDetachedJob,
|
|
10338
|
-
stopDetachedJob,
|
|
10339
10382
|
detectAgents,
|
|
10340
10383
|
requireAgent,
|
|
10341
10384
|
createThread,
|