@sideboard-ai/core 0.1.241 → 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-63UHRQ64.js → chunk-DARSRQUE.js} +1523 -1450
- 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-EE3REM4B.js → chunk-NEOAHPMK.js} +1582 -1505
- 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 +609 -463
- package/dist/index.d.cts +56 -2
- package/dist/index.d.ts +56 -2
- package/dist/index.js +74 -38
- package/dist/mcp/run-stdio.cjs +1029 -908
- package/dist/mcp/run-stdio.js +50 -36
- package/dist/{orchestrator-ZUOEBJFN.js → orchestrator-3IOISDFS.js} +7 -7
- package/dist/{orchestrator-7AJEFWGQ.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,1649 +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
|
-
|
|
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);
|
|
1740
1774
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
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 {
|
|
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;
|
|
1770
1795
|
}
|
|
1771
|
-
if (defaults.length) return defaults;
|
|
1772
|
-
return [".env.local", ".env"];
|
|
1773
1796
|
}
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
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);
|
|
1809
|
+
}
|
|
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)}`);
|
|
1784
1814
|
}
|
|
1785
|
-
return
|
|
1815
|
+
return s;
|
|
1786
1816
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
if (cachedLoginEnv) return { ...cachedLoginEnv };
|
|
1790
|
-
const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
|
|
1817
|
+
function jobAlive(pid) {
|
|
1818
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1791
1819
|
try {
|
|
1792
|
-
|
|
1793
|
-
|
|
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
|
-
}
|
|
1820
|
+
process.kill(pid, 0);
|
|
1821
|
+
return true;
|
|
1809
1822
|
} catch {
|
|
1823
|
+
return false;
|
|
1810
1824
|
}
|
|
1811
|
-
cachedLoginEnv = { ...process.env };
|
|
1812
|
-
return { ...cachedLoginEnv };
|
|
1813
1825
|
}
|
|
1814
|
-
function
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
|
|
1823
|
-
env.SIDEBOARD_ROOT_PATH = opts.repoPath;
|
|
1824
|
-
env.SIDEBOARD_IS_LOCAL = "1";
|
|
1825
|
-
if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
|
|
1826
|
-
env.CONDUCTOR_WORKSPACE_NAME = name;
|
|
1827
|
-
env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
|
|
1828
|
-
env.CONDUCTOR_ROOT_PATH = opts.repoPath;
|
|
1829
|
-
env.CONDUCTOR_IS_LOCAL = "1";
|
|
1830
|
-
if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
|
|
1831
|
-
if (primary != null) {
|
|
1832
|
-
env.SIDEBOARD_PORT = String(primary);
|
|
1833
|
-
env.CONDUCTOR_PORT = String(primary);
|
|
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
|
-
}
|
|
1826
|
+
function processGroupAlive(pgid) {
|
|
1827
|
+
if (process.platform === "win32") return false;
|
|
1828
|
+
if (!Number.isInteger(pgid) || pgid <= 1) return false;
|
|
1829
|
+
try {
|
|
1830
|
+
process.kill(-pgid, 0);
|
|
1831
|
+
return true;
|
|
1832
|
+
} catch (err) {
|
|
1833
|
+
return err && typeof err === "object" && "code" in err && err.code === "EPERM";
|
|
1840
1834
|
}
|
|
1841
|
-
applyWorktreePkgCacheEnv(env, opts.worktreePath);
|
|
1842
|
-
return env;
|
|
1843
|
-
}
|
|
1844
|
-
function pipeLines(stream, onLine) {
|
|
1845
|
-
if (!stream || !onLine) return;
|
|
1846
|
-
const rl = createInterface2({ input: stream });
|
|
1847
|
-
rl.on("line", onLine);
|
|
1848
1835
|
}
|
|
1849
|
-
function
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
const pid = Number(token);
|
|
1853
|
-
if (!Number.isFinite(pid) || pid <= 0 || pid === selfPid) continue;
|
|
1854
|
-
pids.push(pid);
|
|
1855
|
-
}
|
|
1856
|
-
return pids;
|
|
1836
|
+
function jobTreeAlive(pid) {
|
|
1837
|
+
if (pid == null) return false;
|
|
1838
|
+
return jobAlive(pid) || processGroupAlive(pid);
|
|
1857
1839
|
}
|
|
1858
|
-
function
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1863
|
-
timeout: 2e3
|
|
1864
|
-
});
|
|
1865
|
-
return killableListenerPids(raw, selfPid);
|
|
1866
|
-
} catch {
|
|
1867
|
-
return [];
|
|
1868
|
-
}
|
|
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;
|
|
1869
1844
|
}
|
|
1870
|
-
function
|
|
1871
|
-
|
|
1872
|
-
try {
|
|
1873
|
-
const raw = execFileSync("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
|
|
1874
|
-
encoding: "utf8",
|
|
1875
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1876
|
-
timeout: 2e3
|
|
1877
|
-
});
|
|
1878
|
-
for (const line of raw.split("\n")) {
|
|
1879
|
-
if (line.startsWith("n") && line.length > 1) return line.slice(1);
|
|
1880
|
-
}
|
|
1881
|
-
} catch {
|
|
1882
|
-
}
|
|
1883
|
-
return null;
|
|
1845
|
+
function jobDir(root, id, legacy = false) {
|
|
1846
|
+
return join5(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
|
|
1884
1847
|
}
|
|
1885
|
-
function
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
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;
|
|
1891
1854
|
}
|
|
1892
|
-
function
|
|
1893
|
-
const
|
|
1894
|
-
|
|
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;
|
|
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);
|
|
1902
1859
|
}
|
|
1903
|
-
function
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
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);
|
|
1907
1872
|
}
|
|
1873
|
+
return running.sort();
|
|
1908
1874
|
}
|
|
1909
|
-
function
|
|
1910
|
-
const
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
execFileSync(
|
|
1916
|
-
"powershell",
|
|
1917
|
-
[
|
|
1918
|
-
"-NoProfile",
|
|
1919
|
-
"-Command",
|
|
1920
|
-
`Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -ne ${selfPid} } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
|
|
1921
|
-
],
|
|
1922
|
-
{ stdio: "ignore", timeout: 3e3 }
|
|
1923
|
-
);
|
|
1924
|
-
continue;
|
|
1925
|
-
}
|
|
1926
|
-
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
1927
|
-
signalPid(pid, "SIGTERM");
|
|
1928
|
-
}
|
|
1929
|
-
} catch {
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
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);
|
|
1932
1881
|
}
|
|
1933
|
-
function
|
|
1934
|
-
if (
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
}
|
|
1938
|
-
if (process.platform === "win32") {
|
|
1939
|
-
killListenersOnPorts(ports);
|
|
1940
|
-
return;
|
|
1941
|
-
}
|
|
1942
|
-
const selfPid = process.pid;
|
|
1943
|
-
for (const port of ports) {
|
|
1944
|
-
if (!Number.isFinite(port) || port <= 0) continue;
|
|
1945
|
-
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
1946
|
-
const cwd = listenerCwd(pid);
|
|
1947
|
-
if (cwd && isOtherWorktreeListenerCwd(cwd, worktreePath)) continue;
|
|
1948
|
-
signalPid(pid, signal);
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
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");
|
|
1951
1886
|
}
|
|
1952
|
-
function
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
} catch {
|
|
1964
|
-
}
|
|
1965
|
-
}, 2500).unref?.();
|
|
1966
|
-
}
|
|
1967
|
-
} catch {
|
|
1968
|
-
try {
|
|
1969
|
-
child.kill("SIGTERM");
|
|
1970
|
-
} catch {
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
killListenersOnPorts(ports);
|
|
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");
|
|
1975
1898
|
}
|
|
1976
|
-
|
|
1977
|
-
const
|
|
1978
|
-
const
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
ports: opts.ports
|
|
1985
|
-
},
|
|
1986
|
-
loginEnv
|
|
1987
|
-
);
|
|
1988
|
-
try {
|
|
1989
|
-
mergeAgentGitAuthEnv(
|
|
1990
|
-
env,
|
|
1991
|
-
await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
|
|
1992
|
-
);
|
|
1993
|
-
} catch {
|
|
1994
|
-
}
|
|
1995
|
-
if (opts.heldPorts?.length) {
|
|
1996
|
-
await Promise.all(opts.heldPorts.map((h) => h.release()));
|
|
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)}`;
|
|
1997
1907
|
}
|
|
1998
|
-
|
|
1999
|
-
const child = execa2(shell, ["-lc", command], {
|
|
2000
|
-
cwd: opts.worktreePath,
|
|
2001
|
-
reject: false,
|
|
2002
|
-
env,
|
|
2003
|
-
// Own process group so Stop can tear down the whole tree (not just the shell).
|
|
2004
|
-
// There is no settings.toml `stop=` / teardown hook for run scripts.
|
|
2005
|
-
...process.platform === "win32" ? {} : { detached: true }
|
|
2006
|
-
});
|
|
2007
|
-
pipeLines(child.stdout, opts.onLine);
|
|
2008
|
-
pipeLines(child.stderr, opts.onLine);
|
|
2009
|
-
const ports = opts.ports ?? [];
|
|
2010
|
-
return {
|
|
2011
|
-
pid: child.pid,
|
|
2012
|
-
kill: () => killScriptTree(child, ports),
|
|
2013
|
-
done: child.then((r) => r.exitCode ?? null),
|
|
2014
|
-
child
|
|
2015
|
-
};
|
|
1908
|
+
return out;
|
|
2016
1909
|
}
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
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(" ");
|
|
2024
1918
|
}
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
worktreePath,
|
|
2032
|
-
repoPath,
|
|
2033
|
-
defaultBranch: opts?.defaultBranch,
|
|
2034
|
-
onLine
|
|
2035
|
-
});
|
|
2036
|
-
const exitCode = await attachAbort(handle, opts?.signal);
|
|
2037
|
-
return {
|
|
2038
|
-
ran: true,
|
|
2039
|
-
exitCode,
|
|
2040
|
-
source,
|
|
2041
|
-
kill: handle.kill
|
|
2042
|
-
};
|
|
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(" ");
|
|
2043
1925
|
}
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
repoPath,
|
|
2051
|
-
defaultBranch: opts?.defaultBranch,
|
|
2052
|
-
onLine
|
|
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);
|
|
2053
1932
|
});
|
|
2054
|
-
const exitCode = await attachAbort(handle, opts?.signal);
|
|
2055
|
-
return {
|
|
2056
|
-
ran: true,
|
|
2057
|
-
exitCode,
|
|
2058
|
-
source: found.source,
|
|
2059
|
-
kill: handle.kill
|
|
2060
|
-
};
|
|
2061
1933
|
}
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
if (
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
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
|
+
};
|
|
2071
1946
|
}
|
|
2072
|
-
if (!
|
|
2073
|
-
|
|
1947
|
+
if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
|
|
1948
|
+
return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
|
|
2074
1949
|
}
|
|
2075
|
-
return
|
|
2076
|
-
}
|
|
2077
|
-
async function runArchiveScript(repoPath, worktreePath, onLine) {
|
|
2078
|
-
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
2079
|
-
if (!settings?.archive) return { ran: false, exitCode: null };
|
|
2080
|
-
const handle = await spawnWorkspaceScript(settings.archive, {
|
|
2081
|
-
worktreePath,
|
|
2082
|
-
repoPath,
|
|
2083
|
-
onLine
|
|
2084
|
-
});
|
|
2085
|
-
const exitCode = await handle.done;
|
|
2086
|
-
return { ran: true, exitCode };
|
|
1950
|
+
return { action: "none" };
|
|
2087
1951
|
}
|
|
2088
|
-
function
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
return
|
|
2092
|
-
if (!s.availableIn?.length) return true;
|
|
2093
|
-
return s.availableIn.includes("local");
|
|
2094
|
-
});
|
|
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");
|
|
2095
1956
|
}
|
|
2096
|
-
function
|
|
2097
|
-
const
|
|
2098
|
-
|
|
2099
|
-
return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
|
|
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;
|
|
2100
1960
|
}
|
|
2101
|
-
function
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
if (
|
|
2105
|
-
|
|
2106
|
-
}
|
|
2107
|
-
return getDefaultRunScript(worktreePath, repoPath);
|
|
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;
|
|
2108
1966
|
}
|
|
2109
|
-
function
|
|
2110
|
-
|
|
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 };
|
|
2111
1972
|
}
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
});
|
|
2124
|
-
server.listen(0, "127.0.0.1", () => {
|
|
2125
|
-
const addr = server.address();
|
|
2126
|
-
if (!addr || typeof addr === "string") {
|
|
2127
|
-
void release().then(() => reject(new Error("Failed to allocate port")));
|
|
2128
|
-
return;
|
|
2129
|
-
}
|
|
2130
|
-
resolve2({ port: addr.port, release });
|
|
2131
|
-
});
|
|
2132
|
-
server.on("error", reject);
|
|
2133
|
-
});
|
|
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
|
+
};
|
|
2134
1984
|
}
|
|
2135
|
-
|
|
2136
|
-
const
|
|
2137
|
-
|
|
2138
|
-
|
|
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
|
+
};
|
|
2139
2011
|
}
|
|
2140
|
-
async function
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2161
|
-
|
|
2012
|
+
async function sleepMs(ms) {
|
|
2013
|
+
await new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2014
|
+
}
|
|
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."
|
|
2162
2040
|
});
|
|
2163
|
-
}
|
|
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);
|
|
2164
2050
|
}
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
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 {
|
|
2172
2058
|
}
|
|
2173
|
-
held.push(next);
|
|
2174
2059
|
}
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
if (!ports?.length) return [];
|
|
2179
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2180
|
-
const out = [];
|
|
2181
|
-
for (const p of ports) {
|
|
2182
|
-
if (!Number.isInteger(p) || p <= 0 || p > 65535 || seen.has(p)) continue;
|
|
2183
|
-
seen.add(p);
|
|
2184
|
-
out.push(p);
|
|
2185
|
-
if (out.length >= size) break;
|
|
2060
|
+
try {
|
|
2061
|
+
process.kill(pid, signal);
|
|
2062
|
+
} catch {
|
|
2186
2063
|
}
|
|
2187
|
-
return out;
|
|
2188
2064
|
}
|
|
2189
|
-
function
|
|
2190
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2191
|
-
}
|
|
2192
|
-
async function fillPortRange(held, size) {
|
|
2193
|
-
if (held.length >= size) return held;
|
|
2194
|
-
const base = held[0]?.port;
|
|
2195
|
-
const taken = new Set(held.map((h) => h.port));
|
|
2065
|
+
function appendJobLog(logFile, line) {
|
|
2196
2066
|
try {
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
const next = candidate != null ? await tryReservePort(candidate) : null;
|
|
2203
|
-
if (next) {
|
|
2204
|
-
held.push(next);
|
|
2205
|
-
taken.add(next.port);
|
|
2206
|
-
} else {
|
|
2207
|
-
const ephemeral = await reservePort();
|
|
2208
|
-
held.push(ephemeral);
|
|
2209
|
-
taken.add(ephemeral.port);
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
return held;
|
|
2213
|
-
} catch (err) {
|
|
2214
|
-
await Promise.all(held.map((h) => h.release()));
|
|
2215
|
-
throw err;
|
|
2067
|
+
mkdirSync4(dirname3(logFile), { recursive: true });
|
|
2068
|
+
const body = line.endsWith("\n") ? line : `${line}
|
|
2069
|
+
`;
|
|
2070
|
+
appendFileSync2(logFile, body);
|
|
2071
|
+
} catch {
|
|
2216
2072
|
}
|
|
2217
2073
|
}
|
|
2218
|
-
async function
|
|
2219
|
-
const
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
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
|
+
};
|
|
2223
2093
|
}
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
return null;
|
|
2234
|
-
};
|
|
2235
|
-
let held = await takePrimary();
|
|
2236
|
-
if (held) return held;
|
|
2237
|
-
if (worktreePath) {
|
|
2238
|
-
killStaleWorktreeListeners(ports, worktreePath, "SIGTERM");
|
|
2239
|
-
} else {
|
|
2240
|
-
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
|
+
};
|
|
2241
2103
|
}
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
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())));
|
|
2249
2111
|
}
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
const preferred = normalizePreferredPorts(opts?.preferred, size);
|
|
2254
|
-
if (preferred.length) {
|
|
2255
|
-
const reused = await reclaimPreferredPorts(preferred, opts?.worktreePath);
|
|
2256
|
-
if (reused) return fillPortRange(reused, size);
|
|
2112
|
+
if (targetPid != null) {
|
|
2113
|
+
killProcessTree(targetPid, "SIGKILL");
|
|
2114
|
+
await sleep(100);
|
|
2257
2115
|
}
|
|
2258
|
-
const
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
await Promise.all(held.map((h) => h.release()));
|
|
2265
|
-
return ports;
|
|
2266
|
-
}
|
|
2267
|
-
async function startDevServer(repoPath, worktreePath, onLine, opts) {
|
|
2268
|
-
const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
|
|
2269
|
-
if (!script) return null;
|
|
2270
|
-
const preferred = loadWorktreeRunPorts(worktreePath, script.name) ?? opts?.preferredPorts;
|
|
2271
|
-
const held = await reservePortRange(PORT_RANGE_SIZE, {
|
|
2272
|
-
preferred: preferred ?? void 0,
|
|
2273
|
-
worktreePath
|
|
2274
|
-
});
|
|
2275
|
-
const ports = held.map((h) => h.port);
|
|
2276
|
-
saveWorktreeRunPorts(worktreePath, script.name, ports);
|
|
2277
|
-
try {
|
|
2278
|
-
const handle = await spawnWorkspaceScript(script.command, {
|
|
2279
|
-
worktreePath,
|
|
2280
|
-
repoPath,
|
|
2281
|
-
ports,
|
|
2282
|
-
heldPorts: held,
|
|
2283
|
-
defaultBranch: opts?.defaultBranch,
|
|
2284
|
-
onLine
|
|
2285
|
-
});
|
|
2286
|
-
return {
|
|
2287
|
-
pid: handle.pid,
|
|
2288
|
-
port: ports[0],
|
|
2289
|
-
ports,
|
|
2290
|
-
scriptName: script.name,
|
|
2291
|
-
kill: handle.kill,
|
|
2292
|
-
done: handle.done
|
|
2293
|
-
};
|
|
2294
|
-
} catch (err) {
|
|
2295
|
-
await Promise.all(held.map((h) => h.release()));
|
|
2296
|
-
throw err;
|
|
2297
|
-
}
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
// src/store/setup-log.ts
|
|
2301
|
-
import { createHash } from "crypto";
|
|
2302
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
2303
|
-
import { join as join6 } from "path";
|
|
2304
|
-
var EMPTY = {
|
|
2305
|
-
output: "",
|
|
2306
|
-
running: false,
|
|
2307
|
-
exitCode: null,
|
|
2308
|
-
source: null
|
|
2309
|
-
};
|
|
2310
|
-
var MAX_SETUP_LOG_CHARS = 256e3;
|
|
2311
|
-
function appendSetupOutput(prev, line, max = MAX_SETUP_LOG_CHARS) {
|
|
2312
|
-
const next = prev ? `${prev}
|
|
2313
|
-
${line}` : line;
|
|
2314
|
-
if (next.length <= max) return next;
|
|
2315
|
-
const cut = next.slice(next.length - max);
|
|
2316
|
-
const nl = cut.indexOf("\n");
|
|
2317
|
-
return nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
|
|
2318
|
-
}
|
|
2319
|
-
var memory = /* @__PURE__ */ new Map();
|
|
2320
|
-
var persistTimers = /* @__PURE__ */ new Map();
|
|
2321
|
-
function fromSnapshot(snap) {
|
|
2322
|
-
return {
|
|
2323
|
-
chunks: snap.output ? [snap.output] : [],
|
|
2324
|
-
length: snap.output.length,
|
|
2325
|
-
running: snap.running,
|
|
2326
|
-
exitCode: snap.exitCode,
|
|
2327
|
-
source: snap.source
|
|
2328
|
-
};
|
|
2329
|
-
}
|
|
2330
|
-
function materialize(mem) {
|
|
2331
|
-
let output = mem.chunks.join("\n");
|
|
2332
|
-
if (output.length > MAX_SETUP_LOG_CHARS) {
|
|
2333
|
-
output = appendSetupOutput("", output);
|
|
2334
|
-
mem.chunks = [output];
|
|
2335
|
-
mem.length = output.length;
|
|
2116
|
+
const exitFile = join5(dir, "exit");
|
|
2117
|
+
if (!existsSync3(exitFile)) {
|
|
2118
|
+
try {
|
|
2119
|
+
writeFileSync3(exitFile, "1\n");
|
|
2120
|
+
} catch {
|
|
2121
|
+
}
|
|
2336
2122
|
}
|
|
2123
|
+
snap = snapshotJob(dir);
|
|
2124
|
+
const stillRunning = jobTreeAlive(targetPid) || snap.running;
|
|
2125
|
+
const result = toResult(jobId, snap, { failed: true });
|
|
2126
|
+
const stopped = !stillRunning;
|
|
2337
2127
|
return {
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
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
|
|
2342
2137
|
};
|
|
2343
2138
|
}
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
}
|
|
2351
|
-
|
|
2352
|
-
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);
|
|
2353
2147
|
}
|
|
2354
|
-
function
|
|
2355
|
-
|
|
2356
|
-
if (!
|
|
2357
|
-
|
|
2358
|
-
if (prev.startsWith(incoming) || prev.endsWith(incoming)) return prev;
|
|
2359
|
-
if (incoming.startsWith(prev) || incoming.endsWith(prev)) return incoming;
|
|
2360
|
-
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("#"));
|
|
2361
2152
|
}
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
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)];
|
|
2368
2173
|
}
|
|
2369
|
-
const
|
|
2370
|
-
if (!mem) return;
|
|
2174
|
+
const defaults = [];
|
|
2371
2175
|
try {
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
);
|
|
2176
|
+
for (const entry of readdirSync2(repoPath, { withFileTypes: true })) {
|
|
2177
|
+
if (entry.isFile() && entry.name.startsWith(".env")) {
|
|
2178
|
+
defaults.push(entry.name);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2378
2181
|
} catch {
|
|
2379
2182
|
}
|
|
2183
|
+
if (defaults.length) return defaults;
|
|
2184
|
+
return [".env.local", ".env"];
|
|
2380
2185
|
}
|
|
2381
|
-
function
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
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;
|
|
2390
2198
|
}
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
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");
|
|
2394
2203
|
try {
|
|
2395
|
-
const
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
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
|
+
}
|
|
2402
2221
|
} catch {
|
|
2403
|
-
return null;
|
|
2404
2222
|
}
|
|
2223
|
+
cachedLoginEnv = { ...process.env };
|
|
2224
|
+
return { ...cachedLoginEnv };
|
|
2405
2225
|
}
|
|
2406
|
-
function
|
|
2407
|
-
const
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
if (
|
|
2418
|
-
|
|
2419
|
-
|
|
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
|
+
}
|
|
2420
2252
|
}
|
|
2421
|
-
|
|
2253
|
+
applyWorktreePkgCacheEnv(env, opts.worktreePath);
|
|
2254
|
+
return env;
|
|
2422
2255
|
}
|
|
2423
|
-
function
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
running: true,
|
|
2428
|
-
exitCode: null,
|
|
2429
|
-
source: null
|
|
2430
|
-
};
|
|
2431
|
-
memory.set(threadId, mem);
|
|
2432
|
-
persistNow(threadId);
|
|
2433
|
-
return materialize(mem);
|
|
2434
|
-
}
|
|
2435
|
-
function appendSetupLog(threadId, line) {
|
|
2436
|
-
const mem = memFor(threadId);
|
|
2437
|
-
mem.chunks.push(line);
|
|
2438
|
-
mem.length += line.length + 1;
|
|
2439
|
-
mem.running = true;
|
|
2440
|
-
if (mem.length > MAX_SETUP_LOG_CHARS * 2) materialize(mem);
|
|
2441
|
-
schedulePersist(threadId);
|
|
2442
|
-
}
|
|
2443
|
-
function finishSetupLog(threadId, exitCode, source) {
|
|
2444
|
-
const mem = memFor(threadId);
|
|
2445
|
-
mem.running = false;
|
|
2446
|
-
mem.exitCode = exitCode;
|
|
2447
|
-
mem.source = source ?? mem.source;
|
|
2448
|
-
persistNow(threadId);
|
|
2449
|
-
return materialize(mem);
|
|
2450
|
-
}
|
|
2451
|
-
|
|
2452
|
-
// src/store/run-log.ts
|
|
2453
|
-
import { createHash as createHash2 } from "crypto";
|
|
2454
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
2455
|
-
import { join as join7 } from "path";
|
|
2456
|
-
var EMPTY2 = {
|
|
2457
|
-
output: "",
|
|
2458
|
-
running: false,
|
|
2459
|
-
exitCode: null,
|
|
2460
|
-
scriptName: null
|
|
2461
|
-
};
|
|
2462
|
-
var MAX_RUN_LOG_CHARS = 256e3;
|
|
2463
|
-
var DEFAULT_RUN_LOG_TAIL_CHARS = 16e3;
|
|
2464
|
-
var memory2 = /* @__PURE__ */ new Map();
|
|
2465
|
-
var persistTimers2 = /* @__PURE__ */ new Map();
|
|
2466
|
-
function fromSnapshot2(snap) {
|
|
2467
|
-
return {
|
|
2468
|
-
chunks: snap.output ? [snap.output] : [],
|
|
2469
|
-
length: snap.output.length,
|
|
2470
|
-
running: snap.running,
|
|
2471
|
-
exitCode: snap.exitCode,
|
|
2472
|
-
scriptName: snap.scriptName
|
|
2473
|
-
};
|
|
2256
|
+
function pipeLines(stream, onLine) {
|
|
2257
|
+
if (!stream || !onLine) return;
|
|
2258
|
+
const rl = createInterface2({ input: stream });
|
|
2259
|
+
rl.on("line", onLine);
|
|
2474
2260
|
}
|
|
2475
|
-
function
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
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);
|
|
2481
2267
|
}
|
|
2482
|
-
return
|
|
2483
|
-
output,
|
|
2484
|
-
running: mem.running,
|
|
2485
|
-
exitCode: mem.exitCode,
|
|
2486
|
-
scriptName: mem.scriptName
|
|
2487
|
-
};
|
|
2488
|
-
}
|
|
2489
|
-
function safeScriptToken(scriptName) {
|
|
2490
|
-
const token = scriptName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
|
|
2491
|
-
return token || "dev";
|
|
2492
|
-
}
|
|
2493
|
-
function runLogPath(key) {
|
|
2494
|
-
return join7(threadsDir(), `${key}.run.log.json`);
|
|
2495
|
-
}
|
|
2496
|
-
function runLogKeyForWorktree(worktreePath, scriptName) {
|
|
2497
|
-
const norm = normalizeWorktreePath(worktreePath);
|
|
2498
|
-
const hash = createHash2("sha1").update(norm).digest("hex").slice(0, 16);
|
|
2499
|
-
return `wt-${hash}-run-${safeScriptToken(scriptName)}`;
|
|
2500
|
-
}
|
|
2501
|
-
function emptyRunLog() {
|
|
2502
|
-
return { ...EMPTY2 };
|
|
2503
|
-
}
|
|
2504
|
-
function sliceRunLogOutput(output, tailChars = DEFAULT_RUN_LOG_TAIL_CHARS) {
|
|
2505
|
-
const max = Math.max(1, tailChars);
|
|
2506
|
-
const outputChars = output.length;
|
|
2507
|
-
if (outputChars <= max) return { output, truncated: false, outputChars };
|
|
2508
|
-
const cut = output.slice(outputChars - max);
|
|
2509
|
-
const nl = cut.indexOf("\n");
|
|
2510
|
-
const text = nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
|
|
2511
|
-
return { output: text, truncated: true, outputChars };
|
|
2268
|
+
return pids;
|
|
2512
2269
|
}
|
|
2513
|
-
|
|
2514
|
-
function persistNow2(key) {
|
|
2515
|
-
const timer = persistTimers2.get(key);
|
|
2516
|
-
if (timer) {
|
|
2517
|
-
clearTimeout(timer);
|
|
2518
|
-
persistTimers2.delete(key);
|
|
2519
|
-
}
|
|
2520
|
-
const mem = memory2.get(key);
|
|
2521
|
-
if (!mem) return;
|
|
2270
|
+
function listUnixListenerPids(port, selfPid) {
|
|
2522
2271
|
try {
|
|
2523
|
-
|
|
2524
|
-
|
|
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);
|
|
2525
2278
|
} catch {
|
|
2279
|
+
return [];
|
|
2526
2280
|
}
|
|
2527
2281
|
}
|
|
2528
|
-
function
|
|
2529
|
-
if (
|
|
2530
|
-
persistTimers2.set(
|
|
2531
|
-
key,
|
|
2532
|
-
setTimeout(() => {
|
|
2533
|
-
persistTimers2.delete(key);
|
|
2534
|
-
persistNow2(key);
|
|
2535
|
-
}, PERSIST_DEBOUNCE_MS2)
|
|
2536
|
-
);
|
|
2537
|
-
}
|
|
2538
|
-
function loadFromDisk2(key) {
|
|
2539
|
-
const path = runLogPath(key);
|
|
2540
|
-
if (!existsSync5(path)) return null;
|
|
2282
|
+
function listenerCwd(pid) {
|
|
2283
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
2541
2284
|
try {
|
|
2542
|
-
const raw =
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
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
|
+
}
|
|
2549
2293
|
} catch {
|
|
2550
|
-
return null;
|
|
2551
2294
|
}
|
|
2295
|
+
return null;
|
|
2552
2296
|
}
|
|
2553
|
-
function
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
return mem;
|
|
2559
|
-
}
|
|
2560
|
-
function readRunLog(key) {
|
|
2561
|
-
const cached = memory2.get(key);
|
|
2562
|
-
if (cached) return materialize2(cached);
|
|
2563
|
-
const disk = loadFromDisk2(key);
|
|
2564
|
-
if (disk) {
|
|
2565
|
-
memory2.set(key, fromSnapshot2(disk));
|
|
2566
|
-
return disk;
|
|
2297
|
+
function realpathOrResolve(path) {
|
|
2298
|
+
try {
|
|
2299
|
+
return realpathSync(path);
|
|
2300
|
+
} catch {
|
|
2301
|
+
return resolve(path);
|
|
2567
2302
|
}
|
|
2568
|
-
return emptyRunLog();
|
|
2569
|
-
}
|
|
2570
|
-
function beginRunLog(key, scriptName) {
|
|
2571
|
-
const mem = {
|
|
2572
|
-
chunks: [],
|
|
2573
|
-
length: 0,
|
|
2574
|
-
running: true,
|
|
2575
|
-
exitCode: null,
|
|
2576
|
-
scriptName
|
|
2577
|
-
};
|
|
2578
|
-
memory2.set(key, mem);
|
|
2579
|
-
persistNow2(key);
|
|
2580
|
-
return materialize2(mem);
|
|
2581
|
-
}
|
|
2582
|
-
function appendRunLog(key, line) {
|
|
2583
|
-
const mem = memFor2(key);
|
|
2584
|
-
mem.chunks.push(line);
|
|
2585
|
-
mem.length += line.length + 1;
|
|
2586
|
-
mem.running = true;
|
|
2587
|
-
if (mem.length > MAX_RUN_LOG_CHARS * 2) materialize2(mem);
|
|
2588
|
-
schedulePersist2(key);
|
|
2589
2303
|
}
|
|
2590
|
-
function
|
|
2591
|
-
const
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
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;
|
|
2596
2314
|
}
|
|
2597
|
-
function
|
|
2598
|
-
|
|
2315
|
+
function signalPid(pid, signal) {
|
|
2316
|
+
try {
|
|
2317
|
+
process.kill(pid, signal);
|
|
2318
|
+
} catch {
|
|
2319
|
+
}
|
|
2599
2320
|
}
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2321
|
+
function isThisWorktreeListenerCwd(cwd, worktreePath) {
|
|
2322
|
+
const wt = realpathOrResolve(worktreePath);
|
|
2323
|
+
const c = realpathOrResolve(cwd);
|
|
2324
|
+
return c === wt || c.startsWith(wt + sep);
|
|
2325
|
+
}
|
|
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
|
+
}
|
|
2334
|
+
try {
|
|
2335
|
+
process.kill(-pid, "SIGTERM");
|
|
2336
|
+
} catch {
|
|
2337
|
+
signalPid(pid, "SIGTERM");
|
|
2338
|
+
}
|
|
2606
2339
|
}
|
|
2607
|
-
|
|
2608
|
-
const
|
|
2609
|
-
const
|
|
2610
|
-
|
|
2611
|
-
repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
|
|
2612
|
-
);
|
|
2613
|
-
const homeRoot = sideboardWorkspacesDir();
|
|
2614
|
-
if (existsSync6(homeRoot)) {
|
|
2340
|
+
function killListenersOnPorts(ports) {
|
|
2341
|
+
const selfPid = process.pid;
|
|
2342
|
+
for (const port of ports) {
|
|
2343
|
+
if (!Number.isFinite(port) || port <= 0) continue;
|
|
2615
2344
|
try {
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
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");
|
|
2619
2359
|
}
|
|
2620
2360
|
} catch {
|
|
2621
2361
|
}
|
|
2622
2362
|
}
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
}
|
|
2641
|
-
orphans.push({ path, repoPath, mtimeMs });
|
|
2642
|
-
}
|
|
2643
|
-
} catch {
|
|
2363
|
+
}
|
|
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);
|
|
2644
2380
|
}
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
function killScriptTree(child, ports = []) {
|
|
2384
|
+
const pid = child.pid;
|
|
2385
|
+
if (pid) {
|
|
2645
2386
|
try {
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
if (known.has(path) || seen.has(path)) continue;
|
|
2652
|
-
if (!existsSync6(join8(path, ".git"))) continue;
|
|
2653
|
-
seen.add(path);
|
|
2654
|
-
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(() => {
|
|
2655
2392
|
try {
|
|
2656
|
-
|
|
2393
|
+
process.kill(-pid, "SIGKILL");
|
|
2657
2394
|
} catch {
|
|
2658
|
-
mtimeMs = Date.now();
|
|
2659
2395
|
}
|
|
2660
|
-
|
|
2661
|
-
}
|
|
2396
|
+
}, 2500).unref?.();
|
|
2662
2397
|
}
|
|
2663
2398
|
} catch {
|
|
2399
|
+
try {
|
|
2400
|
+
child.kill("SIGTERM");
|
|
2401
|
+
} catch {
|
|
2402
|
+
}
|
|
2664
2403
|
}
|
|
2665
2404
|
}
|
|
2666
|
-
|
|
2405
|
+
killListenersOnPorts(ports);
|
|
2667
2406
|
}
|
|
2668
|
-
async function
|
|
2669
|
-
const
|
|
2670
|
-
const
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
}
|
|
2687
|
-
try {
|
|
2688
|
-
await removeWorktree(orphan.repoPath, orphan.path);
|
|
2689
|
-
removed.push(orphan.path);
|
|
2690
|
-
} catch {
|
|
2691
|
-
kept.push(orphan.path);
|
|
2692
|
-
}
|
|
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 {
|
|
2693
2425
|
}
|
|
2694
|
-
if (
|
|
2695
|
-
|
|
2696
|
-
worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2697
|
-
});
|
|
2426
|
+
if (opts.heldPorts?.length) {
|
|
2427
|
+
await Promise.all(opts.heldPorts.map((h) => h.release()));
|
|
2698
2428
|
}
|
|
2699
|
-
|
|
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
|
+
};
|
|
2700
2447
|
}
|
|
2701
|
-
function
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
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;
|
|
2707
2455
|
}
|
|
2708
|
-
function
|
|
2709
|
-
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);
|
|
2710
2468
|
return {
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2469
|
+
ran: true,
|
|
2470
|
+
exitCode,
|
|
2471
|
+
source,
|
|
2472
|
+
kill: handle.kill
|
|
2715
2473
|
};
|
|
2716
2474
|
}
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
messages: [historyRetentionStubMessage(thread)]
|
|
2727
|
-
},
|
|
2728
|
-
{ touch: false }
|
|
2729
|
-
);
|
|
2730
|
-
}
|
|
2731
|
-
function cleanupArchivedHistory(opts = {}) {
|
|
2732
|
-
const settings = loadAppSettings();
|
|
2733
|
-
if (!opts.force && opts.purgeOlderThanDays == null && !autoCleanupHistoryEnabled(settings)) {
|
|
2734
|
-
return { stripped: [], purged: [] };
|
|
2735
|
-
}
|
|
2736
|
-
const archived = listThreads({ includeArchived: true }).filter((t) => t.status === "archived");
|
|
2737
|
-
const nowMs = opts.nowMs ?? Date.now();
|
|
2738
|
-
const plan = opts.purgeOlderThanDays != null ? { stripIds: [], purgeIds: planHistoryAgePurge(archived, opts.purgeOlderThanDays, nowMs) } : planHistoryRetention(archived, {
|
|
2739
|
-
maxCount: opts.maxCount ?? historyMaxCount(settings),
|
|
2740
|
-
maxDays: opts.maxDays ?? historyMaxDays(settings),
|
|
2741
|
-
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
|
|
2742
2484
|
});
|
|
2743
|
-
const
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
}
|
|
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`);
|
|
2756
2498
|
}
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
try {
|
|
2761
|
-
deleteThreadRecord(id);
|
|
2762
|
-
purged.push(id);
|
|
2763
|
-
} catch {
|
|
2764
|
-
}
|
|
2499
|
+
let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
|
|
2500
|
+
if (!setup.ran) {
|
|
2501
|
+
setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
|
|
2765
2502
|
}
|
|
2766
|
-
if (
|
|
2767
|
-
|
|
2768
|
-
historyLastCleanupAt: new Date(nowMs).toISOString()
|
|
2769
|
-
});
|
|
2503
|
+
if (!setup.ran) {
|
|
2504
|
+
setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
|
|
2770
2505
|
}
|
|
2771
|
-
return
|
|
2506
|
+
return setup;
|
|
2772
2507
|
}
|
|
2773
|
-
function
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
const
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
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
|
|
2515
|
+
});
|
|
2516
|
+
const exitCode = await handle.done;
|
|
2517
|
+
return { ran: true, exitCode };
|
|
2780
2518
|
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
const status = await execa3("git", ["status", "--porcelain"], {
|
|
2788
|
-
cwd: thread.repoPath,
|
|
2789
|
-
reject: false
|
|
2790
|
-
});
|
|
2791
|
-
if (status.stdout.trim()) {
|
|
2792
|
-
throw new Error(
|
|
2793
|
-
`Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
|
|
2794
|
-
);
|
|
2795
|
-
}
|
|
2796
|
-
const checkout = await execa3("git", ["checkout", target], {
|
|
2797
|
-
cwd: thread.repoPath,
|
|
2798
|
-
reject: false
|
|
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");
|
|
2799
2525
|
});
|
|
2800
|
-
if (checkout.exitCode !== 0) {
|
|
2801
|
-
throw new Error(
|
|
2802
|
-
`Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
|
|
2803
|
-
);
|
|
2804
|
-
}
|
|
2805
|
-
if (method === "cherry-pick") {
|
|
2806
|
-
const log = await execa3(
|
|
2807
|
-
"git",
|
|
2808
|
-
["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
|
|
2809
|
-
{ cwd: thread.repoPath, reject: false }
|
|
2810
|
-
);
|
|
2811
|
-
const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
2812
|
-
if (!shas.length) {
|
|
2813
|
-
return {
|
|
2814
|
-
applied: false,
|
|
2815
|
-
method,
|
|
2816
|
-
targetBranch: target,
|
|
2817
|
-
message: `No commits on ${thread.branchName} not already in ${target}`
|
|
2818
|
-
};
|
|
2819
|
-
}
|
|
2820
|
-
for (const sha of shas) {
|
|
2821
|
-
const cp = await execa3("git", ["cherry-pick", sha], {
|
|
2822
|
-
cwd: thread.repoPath,
|
|
2823
|
-
reject: false
|
|
2824
|
-
});
|
|
2825
|
-
if (cp.exitCode !== 0) {
|
|
2826
|
-
await execa3("git", ["cherry-pick", "--abort"], {
|
|
2827
|
-
cwd: thread.repoPath,
|
|
2828
|
-
reject: false
|
|
2829
|
-
});
|
|
2830
|
-
throw new Error(
|
|
2831
|
-
`Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
|
|
2832
|
-
);
|
|
2833
|
-
}
|
|
2834
|
-
}
|
|
2835
|
-
return {
|
|
2836
|
-
applied: true,
|
|
2837
|
-
method,
|
|
2838
|
-
targetBranch: target,
|
|
2839
|
-
message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
|
|
2840
|
-
};
|
|
2841
|
-
}
|
|
2842
|
-
const merge = await execa3(
|
|
2843
|
-
"git",
|
|
2844
|
-
["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
|
|
2845
|
-
{ cwd: thread.repoPath, reject: false }
|
|
2846
|
-
);
|
|
2847
|
-
if (merge.exitCode !== 0) {
|
|
2848
|
-
await execa3("git", ["merge", "--abort"], {
|
|
2849
|
-
cwd: thread.repoPath,
|
|
2850
|
-
reject: false
|
|
2851
|
-
});
|
|
2852
|
-
throw new Error(
|
|
2853
|
-
`Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
|
|
2854
|
-
);
|
|
2855
|
-
}
|
|
2856
|
-
return {
|
|
2857
|
-
applied: true,
|
|
2858
|
-
method: "merge",
|
|
2859
|
-
targetBranch: target,
|
|
2860
|
-
message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
|
|
2861
|
-
};
|
|
2862
2526
|
}
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
const
|
|
2870
|
-
if (!
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
const leaf = basename2(url.replace(/\/$/, "").replace(/\.git$/, ""));
|
|
2874
|
-
name = leaf || "repo";
|
|
2875
|
-
}
|
|
2876
|
-
name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
2877
|
-
const dest = join9(sideboardReposDir(), name);
|
|
2878
|
-
if (existsSync7(dest)) {
|
|
2879
|
-
const repoPath2 = await resolveRepoRoot(dest);
|
|
2880
|
-
const workspace2 = await ensureWorkspace(repoPath2);
|
|
2881
|
-
return { repoPath: repoPath2, workspace: workspace2 };
|
|
2882
|
-
}
|
|
2883
|
-
const result = await execa4("git", ["clone", url, dest], { reject: false });
|
|
2884
|
-
if (result.exitCode !== 0) {
|
|
2885
|
-
throw new Error(
|
|
2886
|
-
`git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
|
|
2887
|
-
);
|
|
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;
|
|
2531
|
+
}
|
|
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;
|
|
2888
2537
|
}
|
|
2889
|
-
|
|
2890
|
-
const workspace = await ensureWorkspace(repoPath);
|
|
2891
|
-
return { repoPath, workspace };
|
|
2538
|
+
return getDefaultRunScript(worktreePath, repoPath);
|
|
2892
2539
|
}
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
function isUnclaimedRunScriptRequest(req) {
|
|
2896
|
-
return Boolean(req && !req.claimedAt && !req.fulfilledAt && !req.error);
|
|
2540
|
+
function getRunMode(worktreePath, repoPath) {
|
|
2541
|
+
return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
|
|
2897
2542
|
}
|
|
2898
|
-
function
|
|
2899
|
-
return
|
|
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
|
+
});
|
|
2900
2565
|
}
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
function desktopHostPidPath() {
|
|
2906
|
-
return join10(appDataDir(), "desktop-host.pid");
|
|
2566
|
+
async function allocatePort() {
|
|
2567
|
+
const held = await reservePort();
|
|
2568
|
+
await held.release();
|
|
2569
|
+
return held.port;
|
|
2907
2570
|
}
|
|
2908
|
-
function
|
|
2909
|
-
|
|
2910
|
-
|
|
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
|
+
});
|
|
2911
2595
|
}
|
|
2912
|
-
function
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
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);
|
|
2917
2605
|
}
|
|
2606
|
+
return held;
|
|
2918
2607
|
}
|
|
2919
|
-
function
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
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;
|
|
2926
2617
|
}
|
|
2618
|
+
return out;
|
|
2927
2619
|
}
|
|
2928
|
-
function
|
|
2620
|
+
function delay(ms) {
|
|
2621
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2622
|
+
}
|
|
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));
|
|
2929
2627
|
try {
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
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;
|
|
2934
2647
|
}
|
|
2935
2648
|
}
|
|
2936
|
-
function
|
|
2937
|
-
const
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
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;
|
|
2942
2656
|
}
|
|
2943
|
-
function
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
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;
|
|
2965
2682
|
}
|
|
2966
|
-
function
|
|
2967
|
-
|
|
2968
|
-
if (
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
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);
|
|
2972
2691
|
}
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
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;
|
|
2976
2697
|
}
|
|
2977
|
-
function
|
|
2978
|
-
const
|
|
2979
|
-
if (!
|
|
2980
|
-
const
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
void send(parentId, childHaltNotice(child, status)).catch(() => {
|
|
2985
|
-
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
|
|
2986
2705
|
});
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
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.";
|
|
3010
|
-
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.";
|
|
3011
|
-
var MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
|
|
3012
|
-
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.";
|
|
3013
|
-
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.";
|
|
3014
|
-
function mcpWaitTaskHint(taskState, status) {
|
|
3015
|
-
switch (taskState) {
|
|
3016
|
-
case "submitted":
|
|
3017
|
-
return MCP_WAIT_QUEUED_HINT;
|
|
3018
|
-
case "working":
|
|
3019
|
-
return MCP_WAIT_STILL_RUNNING_HINT;
|
|
3020
|
-
case "input-required":
|
|
3021
|
-
return MCP_WAIT_INPUT_REQUIRED_HINT;
|
|
3022
|
-
case "canceled":
|
|
3023
|
-
return MCP_WAIT_STOPPED_HINT;
|
|
3024
|
-
case "failed":
|
|
3025
|
-
return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
|
|
3026
|
-
default:
|
|
3027
|
-
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;
|
|
3028
2728
|
}
|
|
3029
2729
|
}
|
|
3030
2730
|
|
|
3031
|
-
// src/
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
var
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
if (
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
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;
|
|
3049
2749
|
}
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
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
|
+
};
|
|
3058
2760
|
}
|
|
3059
|
-
function
|
|
3060
|
-
|
|
3061
|
-
if (
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
} catch (err) {
|
|
3066
|
-
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;
|
|
3067
2767
|
}
|
|
2768
|
+
return {
|
|
2769
|
+
output,
|
|
2770
|
+
running: mem.running,
|
|
2771
|
+
exitCode: mem.exitCode,
|
|
2772
|
+
source: mem.source
|
|
2773
|
+
};
|
|
3068
2774
|
}
|
|
3069
|
-
function
|
|
3070
|
-
|
|
3071
|
-
return jobAlive(pid) || processGroupAlive(pid);
|
|
3072
|
-
}
|
|
3073
|
-
function readIntFile(file) {
|
|
3074
|
-
if (!existsSync8(file)) return null;
|
|
3075
|
-
const n = Number.parseInt(readFileSync7(file, "utf8").trim(), 10);
|
|
3076
|
-
return Number.isInteger(n) ? n : null;
|
|
2775
|
+
function setupLogPath(threadId) {
|
|
2776
|
+
return join7(threadsDir(), `${threadId}.setup.log.json`);
|
|
3077
2777
|
}
|
|
3078
|
-
function
|
|
3079
|
-
|
|
2778
|
+
function setupLogKeyForWorktree(worktreePath) {
|
|
2779
|
+
const norm = normalizeWorktreePath(worktreePath);
|
|
2780
|
+
return `wt-${createHash("sha1").update(norm).digest("hex").slice(0, 16)}`;
|
|
3080
2781
|
}
|
|
3081
|
-
function
|
|
3082
|
-
|
|
3083
|
-
if (existsSync8(modern)) return modern;
|
|
3084
|
-
const legacy = jobDir(root, id, true);
|
|
3085
|
-
if (existsSync8(legacy)) return legacy;
|
|
3086
|
-
return modern;
|
|
2782
|
+
function emptySetupLog() {
|
|
2783
|
+
return { ...EMPTY };
|
|
3087
2784
|
}
|
|
3088
|
-
function
|
|
3089
|
-
|
|
3090
|
-
if (!
|
|
3091
|
-
|
|
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;
|
|
3092
2792
|
}
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
const
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
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 {
|
|
3105
2810
|
}
|
|
3106
|
-
return running.sort();
|
|
3107
2811
|
}
|
|
3108
|
-
function
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
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
|
+
);
|
|
3114
2821
|
}
|
|
3115
|
-
function
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
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;
|
|
2835
|
+
}
|
|
3119
2836
|
}
|
|
3120
|
-
function
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
const
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
const last = watchFrames[watchFrames.length - 1] ?? "";
|
|
3127
|
-
const omitted = watchFrames.length - 1;
|
|
3128
|
-
const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
|
|
3129
|
-
`;
|
|
3130
|
-
return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
|
|
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;
|
|
3131
2843
|
}
|
|
3132
|
-
function
|
|
3133
|
-
const
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
${out.slice(-MAX_JOB_DELTA_CHARS)}`;
|
|
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;
|
|
3140
2851
|
}
|
|
3141
|
-
return
|
|
2852
|
+
return emptySetupLog();
|
|
3142
2853
|
}
|
|
3143
|
-
function
|
|
3144
|
-
const
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
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);
|
|
3151
2865
|
}
|
|
3152
|
-
function
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
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);
|
|
3158
2873
|
}
|
|
3159
|
-
function
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
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);
|
|
3166
2881
|
}
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
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;
|
|
3182
2912
|
}
|
|
3183
|
-
return {
|
|
2913
|
+
return {
|
|
2914
|
+
output,
|
|
2915
|
+
running: mem.running,
|
|
2916
|
+
exitCode: mem.exitCode,
|
|
2917
|
+
scriptName: mem.scriptName
|
|
2918
|
+
};
|
|
3184
2919
|
}
|
|
3185
|
-
function
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
return lines.slice(-maxLines).join("\n");
|
|
2920
|
+
function safeScriptToken(scriptName) {
|
|
2921
|
+
const token = scriptName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
|
|
2922
|
+
return token || "dev";
|
|
3189
2923
|
}
|
|
3190
|
-
function
|
|
3191
|
-
|
|
3192
|
-
return last ? last.slice(0, 72) : void 0;
|
|
2924
|
+
function runLogPath(key) {
|
|
2925
|
+
return join8(threadsDir(), `${key}.run.log.json`);
|
|
3193
2926
|
}
|
|
3194
|
-
function
|
|
3195
|
-
|
|
3196
|
-
const
|
|
3197
|
-
|
|
3198
|
-
return lines;
|
|
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)}`;
|
|
3199
2931
|
}
|
|
3200
|
-
function
|
|
3201
|
-
|
|
3202
|
-
const cursor = readIntFile(cursorFile) ?? 0;
|
|
3203
|
-
const start = Math.min(Math.max(0, cursor), lines.length);
|
|
3204
|
-
return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
|
|
2932
|
+
function emptyRunLog() {
|
|
2933
|
+
return { ...EMPTY2 };
|
|
3205
2934
|
}
|
|
3206
|
-
function
|
|
3207
|
-
const
|
|
3208
|
-
const
|
|
3209
|
-
return {
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
cursor: join11(dir, "present.cursor"),
|
|
3215
|
-
progress: tailProgress(join11(dir, "log"))
|
|
3216
|
-
};
|
|
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 };
|
|
3217
2943
|
}
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
const
|
|
3221
|
-
|
|
3222
|
-
|
|
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;
|
|
3223
2953
|
try {
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
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"));
|
|
2974
|
+
return {
|
|
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
|
|
2979
|
+
};
|
|
3227
2980
|
} catch {
|
|
2981
|
+
return null;
|
|
2982
|
+
}
|
|
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
|
+
);
|
|
3228
3287
|
}
|
|
3229
|
-
const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
|
|
3230
|
-
const progress = extra?.progress ?? snap.progress;
|
|
3231
3288
|
return {
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
id,
|
|
3237
|
-
pid: snap.pid,
|
|
3238
|
-
exitCode: snap.exitCode ?? void 0,
|
|
3239
|
-
phase: lastProgressLine(progress),
|
|
3240
|
-
delta,
|
|
3241
|
-
progress,
|
|
3242
|
-
hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
|
|
3289
|
+
applied: true,
|
|
3290
|
+
method: "merge",
|
|
3291
|
+
targetBranch: target,
|
|
3292
|
+
message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
|
|
3243
3293
|
};
|
|
3244
3294
|
}
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
}
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
const
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
if (!
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
ok: false,
|
|
3258
|
-
failed: true,
|
|
3259
|
-
status: "failed",
|
|
3260
|
-
id: jobId,
|
|
3261
|
-
delta: "",
|
|
3262
|
-
progress: "No detached job. Start one first.",
|
|
3263
|
-
hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
|
|
3264
|
-
};
|
|
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";
|
|
3265
3307
|
}
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
if (
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
progress: "No detached job. Start one first."
|
|
3273
|
-
});
|
|
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 };
|
|
3274
3314
|
}
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
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
|
+
);
|
|
3280
3320
|
}
|
|
3281
|
-
|
|
3282
|
-
|
|
3321
|
+
const repoPath = await resolveRepoRoot(dest);
|
|
3322
|
+
const workspace = await ensureWorkspace(repoPath);
|
|
3323
|
+
return { repoPath, workspace };
|
|
3283
3324
|
}
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3325
|
+
|
|
3326
|
+
// src/types/thread.ts
|
|
3327
|
+
function isUnclaimedRunScriptRequest(req) {
|
|
3328
|
+
return Boolean(req && !req.claimedAt && !req.fulfilledAt && !req.error);
|
|
3329
|
+
}
|
|
3330
|
+
function isAdoptableRunScriptRequest(req) {
|
|
3331
|
+
return Boolean(req && !req.fulfilledAt && !req.error);
|
|
3332
|
+
}
|
|
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");
|
|
3339
|
+
}
|
|
3340
|
+
function claimDesktopHost(pid = process.pid) {
|
|
3341
|
+
writeFileSync6(desktopHostPidPath(), `${pid}
|
|
3342
|
+
`, "utf8");
|
|
3343
|
+
}
|
|
3344
|
+
function releaseDesktopHost(pid = process.pid) {
|
|
3345
|
+
if (readDesktopHostPid() !== pid) return;
|
|
3293
3346
|
try {
|
|
3294
|
-
|
|
3347
|
+
unlinkSync(desktopHostPidPath());
|
|
3295
3348
|
} catch {
|
|
3296
3349
|
}
|
|
3297
3350
|
}
|
|
3298
|
-
function
|
|
3351
|
+
function readDesktopHostPid() {
|
|
3299
3352
|
try {
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
appendFileSync2(logFile, body);
|
|
3353
|
+
const pid = Number.parseInt(readFileSync7(desktopHostPidPath(), "utf8").trim(), 10);
|
|
3354
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
3355
|
+
return pid;
|
|
3304
3356
|
} catch {
|
|
3357
|
+
return null;
|
|
3305
3358
|
}
|
|
3306
3359
|
}
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
if (!existsSync8(dir)) {
|
|
3314
|
-
return {
|
|
3315
|
-
stillRunning: false,
|
|
3316
|
-
ok: false,
|
|
3317
|
-
failed: true,
|
|
3318
|
-
status: "failed",
|
|
3319
|
-
id: jobId,
|
|
3320
|
-
delta: "",
|
|
3321
|
-
progress: "No detached job. Start one first.",
|
|
3322
|
-
hint: "Start with detached-job.cjs start <id> -- <command>, then wait_for_job or stop_job.",
|
|
3323
|
-
stopped: false,
|
|
3324
|
-
reason: "not-found"
|
|
3325
|
-
};
|
|
3326
|
-
}
|
|
3327
|
-
let snap = snapshotJob(dir);
|
|
3328
|
-
if (!snap.running) {
|
|
3329
|
-
const result2 = toResult(jobId, snap);
|
|
3330
|
-
return {
|
|
3331
|
-
...result2,
|
|
3332
|
-
stopped: false,
|
|
3333
|
-
reason: "not-running",
|
|
3334
|
-
hint: "That job is already finished. Read the log; do not wait again unless you start a new command."
|
|
3335
|
-
};
|
|
3336
|
-
}
|
|
3337
|
-
appendJobLog(snap.log, `$ stop${why ? `: ${why}` : ""}`);
|
|
3338
|
-
const targetPid = snap.pid;
|
|
3339
|
-
if (targetPid != null) killProcessTree(targetPid, "SIGTERM");
|
|
3340
|
-
const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
|
|
3341
|
-
const deadline = Date.now() + graceMs;
|
|
3342
|
-
while (Date.now() < deadline && jobTreeAlive(targetPid)) {
|
|
3343
|
-
await sleep(Math.min(100, Math.max(20, deadline - Date.now())));
|
|
3344
|
-
}
|
|
3345
|
-
if (targetPid != null) {
|
|
3346
|
-
killProcessTree(targetPid, "SIGKILL");
|
|
3347
|
-
await sleep(100);
|
|
3348
|
-
}
|
|
3349
|
-
const exitFile = join11(dir, "exit");
|
|
3350
|
-
if (!existsSync8(exitFile)) {
|
|
3351
|
-
try {
|
|
3352
|
-
writeFileSync6(exitFile, "1\n");
|
|
3353
|
-
} catch {
|
|
3354
|
-
}
|
|
3360
|
+
function pidAlive(pid) {
|
|
3361
|
+
try {
|
|
3362
|
+
process.kill(pid, 0);
|
|
3363
|
+
return true;
|
|
3364
|
+
} catch {
|
|
3365
|
+
return false;
|
|
3355
3366
|
}
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
const
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3367
|
+
}
|
|
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;
|
|
3371
3420
|
}
|
|
3372
3421
|
|
|
3373
3422
|
// src/threads/create.ts
|
|
@@ -4012,7 +4061,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
4012
4061
|
});
|
|
4013
4062
|
await requireAgent(resolved.agent);
|
|
4014
4063
|
if (input.cowboy) {
|
|
4015
|
-
const { cowboyModeEnabled } = await import("./app-settings-
|
|
4064
|
+
const { cowboyModeEnabled } = await import("./app-settings-VOPXJMGV.js");
|
|
4016
4065
|
if (!cowboyModeEnabled()) {
|
|
4017
4066
|
throw new Error(
|
|
4018
4067
|
"Cowboy mode is off. Enable it in Settings \u2192 Advanced."
|
|
@@ -4070,7 +4119,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
4070
4119
|
let sourceIsFork = false;
|
|
4071
4120
|
if (!input.cowboy && sourceType === "branch" && (!sourceRef || sourceRef === "HEAD" || sourceRef === "default")) {
|
|
4072
4121
|
if (isAbleTimeConnected() && getIssueSource() === "abletime") {
|
|
4073
|
-
const { ensureAbleTimeTask, issueAttachmentForAbleTimeTask } = await import("./abletime-
|
|
4122
|
+
const { ensureAbleTimeTask, issueAttachmentForAbleTimeTask } = await import("./abletime-CWIJ7INC.js");
|
|
4074
4123
|
const task = await ensureAbleTimeTask({
|
|
4075
4124
|
title: title?.trim() || "Untitled work",
|
|
4076
4125
|
description: input.prompt?.trim() || void 0
|
|
@@ -4172,7 +4221,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
4172
4221
|
return readThread(thread.id) ?? thread;
|
|
4173
4222
|
}
|
|
4174
4223
|
async function listLinearIssues(agent, repoPath) {
|
|
4175
|
-
const { getAdapter: getAdapter2 } = await import("./agents-
|
|
4224
|
+
const { getAdapter: getAdapter2 } = await import("./agents-CPME6O2H.js");
|
|
4176
4225
|
await requireAgent(agent, { requireLinear: true });
|
|
4177
4226
|
const adapter = getAdapter2(agent);
|
|
4178
4227
|
if (!adapter.listLinearIssues) {
|
|
@@ -4645,7 +4694,7 @@ async function adoptThread(input) {
|
|
|
4645
4694
|
messages: input.messages ?? []
|
|
4646
4695
|
});
|
|
4647
4696
|
writeThread(thread);
|
|
4648
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
4697
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-WYC5TLC3.js");
|
|
4649
4698
|
await ensureWorkspace2(repoPath);
|
|
4650
4699
|
const { ensureWorktreeSideboardIgnored: ensureWorktreeSideboardIgnored2 } = await import("./worktree-exclude-RXZPTCNH.js");
|
|
4651
4700
|
await ensureWorktreeSideboardIgnored2(input.worktreePath);
|
|
@@ -7357,7 +7406,7 @@ function formatScheduledPrompt(name, prompt) {
|
|
|
7357
7406
|
${prompt}`;
|
|
7358
7407
|
}
|
|
7359
7408
|
async function defaultDeps() {
|
|
7360
|
-
const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-
|
|
7409
|
+
const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-YDPDMXUY.js");
|
|
7361
7410
|
const orch = getOrchestrator2();
|
|
7362
7411
|
return {
|
|
7363
7412
|
findThread: (id) => findThreadByRef(id),
|
|
@@ -7682,7 +7731,7 @@ var Orchestrator = class {
|
|
|
7682
7731
|
orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
|
|
7683
7732
|
});
|
|
7684
7733
|
}
|
|
7685
|
-
const { autoCleanupOrphansEnabled } = await import("./app-settings-
|
|
7734
|
+
const { autoCleanupOrphansEnabled } = await import("./app-settings-VOPXJMGV.js");
|
|
7686
7735
|
if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
|
|
7687
7736
|
await cleanupOrphanWorktrees({ repoPaths });
|
|
7688
7737
|
}
|
|
@@ -8029,7 +8078,7 @@ var Orchestrator = class {
|
|
|
8029
8078
|
}
|
|
8030
8079
|
await setup;
|
|
8031
8080
|
if (skipSetup) return;
|
|
8032
|
-
const { autoRunAfterSetupEnabled } = await import("./app-settings-
|
|
8081
|
+
const { autoRunAfterSetupEnabled } = await import("./app-settings-VOPXJMGV.js");
|
|
8033
8082
|
if (autoRunAfterSetupEnabled()) {
|
|
8034
8083
|
try {
|
|
8035
8084
|
await this.startDev(threadId);
|
|
@@ -8280,7 +8329,8 @@ var Orchestrator = class {
|
|
|
8280
8329
|
if (compact.didCompact) {
|
|
8281
8330
|
thread = updateThread(threadId, {
|
|
8282
8331
|
messages: compact.thread.messages,
|
|
8283
|
-
sessionId: compact.thread.sessionId
|
|
8332
|
+
sessionId: compact.thread.sessionId,
|
|
8333
|
+
compactedMessages: compact.thread.compactedMessages
|
|
8284
8334
|
});
|
|
8285
8335
|
this.emit({
|
|
8286
8336
|
type: "context_compacted",
|
|
@@ -8294,9 +8344,6 @@ var Orchestrator = class {
|
|
|
8294
8344
|
this.emit({ type: "error", threadId, message: `context compact failed: ${message}` });
|
|
8295
8345
|
thread = this.requireThread(threadId);
|
|
8296
8346
|
}
|
|
8297
|
-
if (this.stoppedTurns.has(threadId)) {
|
|
8298
|
-
return;
|
|
8299
|
-
}
|
|
8300
8347
|
const promptText = typeof prompt === "string" ? prompt : "";
|
|
8301
8348
|
const autoContinue = isJobContinuePrompt(promptText) || promptText.startsWith("The previous agent process ended before it finished.");
|
|
8302
8349
|
const parked = takePendingTurnAttachments(thread);
|
|
@@ -8313,6 +8360,14 @@ var Orchestrator = class {
|
|
|
8313
8360
|
agent: thread.agent,
|
|
8314
8361
|
orch: isOrchestratorThread(thread)
|
|
8315
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
|
+
}
|
|
8316
8371
|
thread = this.requireThread(threadId);
|
|
8317
8372
|
const gitPrompt = expandCanonicalGitRequest(promptText);
|
|
8318
8373
|
const { agentPrompt: expandedPrompt } = expandComposerPrompt(
|
|
@@ -8371,7 +8426,7 @@ var Orchestrator = class {
|
|
|
8371
8426
|
].filter(Boolean).join("\n\n") || void 0;
|
|
8372
8427
|
const prGateDirective = thread.agent !== "brightsy" && !isOrchestratorThread(thread) && mentionsPrGoal(promptText) ? formatPrGateDirective() : null;
|
|
8373
8428
|
const agentPrompt = [
|
|
8374
|
-
thread.planMode
|
|
8429
|
+
planModeTurnInstruction(thread.planMode),
|
|
8375
8430
|
orchestrationReminder,
|
|
8376
8431
|
prGateDirective,
|
|
8377
8432
|
injectedNoticeContext,
|
|
@@ -8393,7 +8448,7 @@ var Orchestrator = class {
|
|
|
8393
8448
|
}
|
|
8394
8449
|
const isBrightsy = fresh.agent === "brightsy";
|
|
8395
8450
|
const isOrchestration = isOrchestratorThread(fresh);
|
|
8396
|
-
const { autoRenameBranchEnabled, getGithubGitAuthMode, gitBranchPrefixSetting } = await import("./app-settings-
|
|
8451
|
+
const { autoRenameBranchEnabled, getGithubGitAuthMode, gitBranchPrefixSetting } = await import("./app-settings-VOPXJMGV.js");
|
|
8397
8452
|
const gitAuthMode = getGithubGitAuthMode();
|
|
8398
8453
|
const worktreeDirective = isBrightsy || isOrchestration ? null : formatWorktreeDirective(fresh, {
|
|
8399
8454
|
githubSlug: await withTimeout(
|
|
@@ -8874,6 +8929,9 @@ var Orchestrator = class {
|
|
|
8874
8929
|
}
|
|
8875
8930
|
const preferredPorts = active?.ports?.length ? active.ports : active?.port != null ? [active.port] : void 0;
|
|
8876
8931
|
if (active && !existing) {
|
|
8932
|
+
if (typeof active.pid === "number") {
|
|
8933
|
+
killDetachedRunTree(active.pid, thread.worktreePath);
|
|
8934
|
+
}
|
|
8877
8935
|
killListenersOnPorts(collectActiveRunPorts([active]));
|
|
8878
8936
|
this.syncWorktreeRuns(
|
|
8879
8937
|
thread.worktreePath,
|
|
@@ -8946,7 +9004,8 @@ var Orchestrator = class {
|
|
|
8946
9004
|
scriptName: resolvedName,
|
|
8947
9005
|
port: handle.port,
|
|
8948
9006
|
ports: handle.ports,
|
|
8949
|
-
startedAt
|
|
9007
|
+
startedAt,
|
|
9008
|
+
...typeof handle.pid === "number" && handle.pid > 1 ? { pid: handle.pid } : {}
|
|
8950
9009
|
};
|
|
8951
9010
|
const nextRuns = [
|
|
8952
9011
|
...latestShared.activeRuns.filter((r) => r.scriptName !== resolvedName),
|
|
@@ -9092,6 +9151,9 @@ var Orchestrator = class {
|
|
|
9092
9151
|
}
|
|
9093
9152
|
const run2 = shared.activeRuns.find((r) => r.scriptName === scriptName);
|
|
9094
9153
|
if (!hadHandle2 && run2) {
|
|
9154
|
+
if (typeof run2.pid === "number") {
|
|
9155
|
+
killDetachedRunTree(run2.pid, thread.worktreePath);
|
|
9156
|
+
}
|
|
9095
9157
|
killListenersOnPorts(collectActiveRunPorts([run2]));
|
|
9096
9158
|
}
|
|
9097
9159
|
const remaining = shared.activeRuns.filter((r) => r.scriptName !== scriptName);
|
|
@@ -9113,6 +9175,11 @@ var Orchestrator = class {
|
|
|
9113
9175
|
}
|
|
9114
9176
|
}
|
|
9115
9177
|
if (!hadHandle && shared.activeRuns.length > 0) {
|
|
9178
|
+
for (const run2 of shared.activeRuns) {
|
|
9179
|
+
if (typeof run2.pid === "number") {
|
|
9180
|
+
killDetachedRunTree(run2.pid, thread.worktreePath);
|
|
9181
|
+
}
|
|
9182
|
+
}
|
|
9116
9183
|
killListenersOnPorts(collectActiveRunPorts(shared.activeRuns));
|
|
9117
9184
|
}
|
|
9118
9185
|
this.syncWorktreeRuns(thread.worktreePath, [], null);
|
|
@@ -9154,6 +9221,11 @@ var Orchestrator = class {
|
|
|
9154
9221
|
(k) => isWorktreeRunProcessKey(k, thread.worktreePath) || siblings.some((t) => k.startsWith(`${t.id}:run:`) || k === `${t.id}:dev`)
|
|
9155
9222
|
);
|
|
9156
9223
|
if (!force && hasLiveHandle) continue;
|
|
9224
|
+
for (const run2 of shared.activeRuns) {
|
|
9225
|
+
if (typeof run2.pid === "number") {
|
|
9226
|
+
killDetachedRunTree(run2.pid, thread.worktreePath);
|
|
9227
|
+
}
|
|
9228
|
+
}
|
|
9157
9229
|
killListenersOnPorts(collectActiveRunPorts(shared.activeRuns));
|
|
9158
9230
|
if (shared.devPort != null) {
|
|
9159
9231
|
killListenersOnPorts([shared.devPort]);
|
|
@@ -9667,7 +9739,7 @@ var Orchestrator = class {
|
|
|
9667
9739
|
if (!titled.userSetTitle && meta.title && titled.title !== meta.title) {
|
|
9668
9740
|
updateThread(thread.id, { title: meta.title });
|
|
9669
9741
|
}
|
|
9670
|
-
const { autoArchiveOnMergeEnabled } = await import("./app-settings-
|
|
9742
|
+
const { autoArchiveOnMergeEnabled } = await import("./app-settings-VOPXJMGV.js");
|
|
9671
9743
|
const latest = this.requireThread(thread.id);
|
|
9672
9744
|
if (!shouldAutoArchiveOnPrMerge({
|
|
9673
9745
|
previousPrState: prevState || null,
|
|
@@ -9949,7 +10021,7 @@ var Orchestrator = class {
|
|
|
9949
10021
|
this.enforceHistoryRetention();
|
|
9950
10022
|
if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
|
|
9951
10023
|
try {
|
|
9952
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
10024
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-WYC5TLC3.js");
|
|
9953
10025
|
await ensureWorkspace2(thread.repoPath);
|
|
9954
10026
|
} catch {
|
|
9955
10027
|
}
|
|
@@ -9976,7 +10048,7 @@ var Orchestrator = class {
|
|
|
9976
10048
|
await runArchiveScript(thread.repoPath, thread.worktreePath);
|
|
9977
10049
|
} catch {
|
|
9978
10050
|
}
|
|
9979
|
-
const { deleteBranchOnPurgeEnabled } = await import("./app-settings-
|
|
10051
|
+
const { deleteBranchOnPurgeEnabled } = await import("./app-settings-VOPXJMGV.js");
|
|
9980
10052
|
const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
|
|
9981
10053
|
await removeWorktree(thread.repoPath, thread.worktreePath, {
|
|
9982
10054
|
deleteBranch: deleteBranch ? thread.branchName : void 0
|
|
@@ -10003,7 +10075,7 @@ var Orchestrator = class {
|
|
|
10003
10075
|
`Cowboy checkout missing: ${thread.worktreePath}. Re-add the project folder, then restore.`
|
|
10004
10076
|
);
|
|
10005
10077
|
}
|
|
10006
|
-
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-
|
|
10078
|
+
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-DGDM7LGV.js");
|
|
10007
10079
|
const slug = thread.worktreePath.split("/").pop();
|
|
10008
10080
|
const dest = thread.worktreePath;
|
|
10009
10081
|
await withRepoGitLock(thread.repoPath, async () => {
|
|
@@ -10113,7 +10185,7 @@ async function startOrchestration(opts) {
|
|
|
10113
10185
|
sourceRef: "default",
|
|
10114
10186
|
...createOpts
|
|
10115
10187
|
}).catch(async () => {
|
|
10116
|
-
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-
|
|
10188
|
+
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-DGDM7LGV.js");
|
|
10117
10189
|
const repo = await resolveRepoRoot2(repoPath);
|
|
10118
10190
|
const def = await resolveDefaultBranch2(repo);
|
|
10119
10191
|
return orch.createThread({
|
|
@@ -10123,7 +10195,7 @@ async function startOrchestration(opts) {
|
|
|
10123
10195
|
repoPath: repo
|
|
10124
10196
|
});
|
|
10125
10197
|
});
|
|
10126
|
-
const { updateThread: upd } = await import("./thread-store-
|
|
10198
|
+
const { updateThread: upd } = await import("./thread-store-SCV2IZYS.js");
|
|
10127
10199
|
const updated = upd(thread.id, {
|
|
10128
10200
|
sourceType: "orchestration",
|
|
10129
10201
|
sourceRef: goal
|
|
@@ -10228,6 +10300,9 @@ export {
|
|
|
10228
10300
|
findLastBrightsyContextSummary,
|
|
10229
10301
|
messagesSinceLastBrightsyContextSummary,
|
|
10230
10302
|
buildBrightsySessionSeed,
|
|
10303
|
+
compactArchiveProse,
|
|
10304
|
+
messagesForCompactArchive,
|
|
10305
|
+
appendCompactedMessages,
|
|
10231
10306
|
applyCompaction,
|
|
10232
10307
|
lastRequestOccupancy,
|
|
10233
10308
|
shouldResetSessionForOccupancy,
|
|
@@ -10263,6 +10338,11 @@ export {
|
|
|
10263
10338
|
worktreeRunPortsPath,
|
|
10264
10339
|
loadWorktreeRunPorts,
|
|
10265
10340
|
saveWorktreeRunPorts,
|
|
10341
|
+
mcpWaitForTurnTimeoutMs,
|
|
10342
|
+
mcpWaitTaskHint,
|
|
10343
|
+
mcpWaitForJobTimeoutMs,
|
|
10344
|
+
waitForDetachedJob,
|
|
10345
|
+
stopDetachedJob,
|
|
10266
10346
|
PORT_RANGE_SIZE,
|
|
10267
10347
|
readWorktreeInclude,
|
|
10268
10348
|
resolveFilesToCopy,
|
|
@@ -10272,6 +10352,8 @@ export {
|
|
|
10272
10352
|
killableListenerPids,
|
|
10273
10353
|
listenerCwd,
|
|
10274
10354
|
isOtherWorktreeListenerCwd,
|
|
10355
|
+
isThisWorktreeListenerCwd,
|
|
10356
|
+
killDetachedRunTree,
|
|
10275
10357
|
killListenersOnPorts,
|
|
10276
10358
|
killStaleWorktreeListeners,
|
|
10277
10359
|
runSetupScript,
|
|
@@ -10297,11 +10379,6 @@ export {
|
|
|
10297
10379
|
shouldRunHistoryCleanup,
|
|
10298
10380
|
applyThreadIntoMain,
|
|
10299
10381
|
cloneRepoIntoSideboard,
|
|
10300
|
-
mcpWaitForTurnTimeoutMs,
|
|
10301
|
-
mcpWaitTaskHint,
|
|
10302
|
-
mcpWaitForJobTimeoutMs,
|
|
10303
|
-
waitForDetachedJob,
|
|
10304
|
-
stopDetachedJob,
|
|
10305
10382
|
detectAgents,
|
|
10306
10383
|
requireAgent,
|
|
10307
10384
|
createThread,
|