@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
|
@@ -4,17 +4,17 @@ import {
|
|
|
4
4
|
extractPresentedPlan,
|
|
5
5
|
readPlanFile,
|
|
6
6
|
writePlanFile
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-WDDUIGFK.js";
|
|
8
8
|
import {
|
|
9
9
|
releaseCaffeinateHoldForThread
|
|
10
10
|
} from "./chunk-DQN5U2VH.js";
|
|
11
11
|
import {
|
|
12
|
-
PLAN_MODE_INSTRUCTION,
|
|
13
12
|
adjustBrightsyTurnExit,
|
|
14
13
|
applyAgentEvent,
|
|
15
14
|
applyAgentRunnerHeapEnv,
|
|
16
15
|
clipAgentEventForPaint,
|
|
17
16
|
clipToolResultForStore,
|
|
17
|
+
compactSummaryPart,
|
|
18
18
|
fallbackTurnFailDetail,
|
|
19
19
|
finalizeParts,
|
|
20
20
|
formatAgentErrorContinuePrompt,
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
isInternalAgentStatusText,
|
|
25
25
|
isPollWrapperToolName,
|
|
26
26
|
isSessionQuotaLimit,
|
|
27
|
+
lastAssistantMessageText,
|
|
27
28
|
liveActivitySummary,
|
|
28
29
|
looksLikeAgentFailureMessage,
|
|
29
30
|
looksLikeInvalidAgentSession,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
parseBrightsyCliLine,
|
|
34
35
|
parseSessionQuotaResetAt,
|
|
35
36
|
partsToAssistantText,
|
|
37
|
+
planModeTurnInstruction,
|
|
36
38
|
pushTurnStderr,
|
|
37
39
|
resolveQuotaFallbackAgent,
|
|
38
40
|
shouldFeedErrorBackToAgent,
|
|
@@ -42,7 +44,7 @@ import {
|
|
|
42
44
|
summarizeTurnStderr,
|
|
43
45
|
toolDescription,
|
|
44
46
|
turnFailChatText
|
|
45
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-HR4RNC73.js";
|
|
46
48
|
import {
|
|
47
49
|
SLACK_REPLY_FORMATTING,
|
|
48
50
|
addWorkspace,
|
|
@@ -65,7 +67,7 @@ import {
|
|
|
65
67
|
stageAbsolutePathsAsAttachments,
|
|
66
68
|
stageBuffersAsAttachments,
|
|
67
69
|
syncWorkspacesFromThreads
|
|
68
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-ECZTXNYR.js";
|
|
69
71
|
import {
|
|
70
72
|
addPrStackLayer,
|
|
71
73
|
allocateTeamName,
|
|
@@ -114,7 +116,7 @@ import {
|
|
|
114
116
|
worktreeDisplayLabelForGroup,
|
|
115
117
|
worktreeNameFromPath,
|
|
116
118
|
worktreeSlugForTicket
|
|
117
|
-
} from "./chunk-
|
|
119
|
+
} from "./chunk-7UCZ4DRF.js";
|
|
118
120
|
import {
|
|
119
121
|
ATTACHMENTS_DIR,
|
|
120
122
|
DETACHED_JOBS_DIR,
|
|
@@ -132,7 +134,7 @@ import {
|
|
|
132
134
|
updateThread,
|
|
133
135
|
withThreadLock,
|
|
134
136
|
writeThread
|
|
135
|
-
} from "./chunk-
|
|
137
|
+
} from "./chunk-7HOKP4GR.js";
|
|
136
138
|
import {
|
|
137
139
|
ensureWorktreeSideboardIgnored
|
|
138
140
|
} from "./chunk-DD67JWTN.js";
|
|
@@ -173,7 +175,7 @@ import {
|
|
|
173
175
|
sumUsageList,
|
|
174
176
|
updateAdvancedSettings,
|
|
175
177
|
usageOnLimit
|
|
176
|
-
} from "./chunk-
|
|
178
|
+
} from "./chunk-YLGBQVRK.js";
|
|
177
179
|
import {
|
|
178
180
|
writePrivateFile
|
|
179
181
|
} from "./chunk-NSTQ6QKD.js";
|
|
@@ -488,9 +490,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
488
490
|
`Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
|
|
489
491
|
);
|
|
490
492
|
}
|
|
491
|
-
const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-
|
|
493
|
+
const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-GIQ6F6IJ.js");
|
|
492
494
|
if (isGlobalThread2(thread)) {
|
|
493
|
-
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-
|
|
495
|
+
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-PYIRQGU7.js");
|
|
494
496
|
ensureGlobalCoordinatorCwd2(
|
|
495
497
|
isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
|
|
496
498
|
);
|
|
@@ -791,15 +793,15 @@ function shouldAutoArchiveOnPrMerge(opts) {
|
|
|
791
793
|
// src/hook/conductor.ts
|
|
792
794
|
import {
|
|
793
795
|
copyFileSync,
|
|
794
|
-
existsSync as
|
|
795
|
-
mkdirSync as
|
|
796
|
-
readdirSync,
|
|
797
|
-
readFileSync as
|
|
796
|
+
existsSync as existsSync4,
|
|
797
|
+
mkdirSync as mkdirSync5,
|
|
798
|
+
readdirSync as readdirSync2,
|
|
799
|
+
readFileSync as readFileSync4,
|
|
798
800
|
realpathSync
|
|
799
801
|
} from "fs";
|
|
800
802
|
import { execFileSync } from "child_process";
|
|
801
803
|
import { createServer } from "net";
|
|
802
|
-
import { basename, dirname as
|
|
804
|
+
import { basename, dirname as dirname4, join as join6, resolve, sep } from "path";
|
|
803
805
|
import { execa as execa2 } from "execa";
|
|
804
806
|
import { createInterface as createInterface2 } from "readline";
|
|
805
807
|
|
|
@@ -1172,12 +1174,38 @@ ${match.text}`, "");
|
|
|
1172
1174
|
blocks.push("Continue from this context. Do not repeat the summary unless asked.");
|
|
1173
1175
|
return blocks.join("\n");
|
|
1174
1176
|
}
|
|
1177
|
+
function compactArchiveProse(message) {
|
|
1178
|
+
if (message.origin === "continue") return null;
|
|
1179
|
+
if (message.role === "summary") return null;
|
|
1180
|
+
let text = (message.text ?? "").trim();
|
|
1181
|
+
if (!text && message.role === "agent") {
|
|
1182
|
+
text = lastAssistantMessageText(message.parts).trim();
|
|
1183
|
+
}
|
|
1184
|
+
if (!text || isInternalAgentStatusText(text)) return null;
|
|
1185
|
+
return { role: message.role, text, ts: message.ts };
|
|
1186
|
+
}
|
|
1187
|
+
function messagesForCompactArchive(older) {
|
|
1188
|
+
const out = [];
|
|
1189
|
+
for (const message of older) {
|
|
1190
|
+
const prose = compactArchiveProse(message);
|
|
1191
|
+
if (prose) out.push(prose);
|
|
1192
|
+
}
|
|
1193
|
+
return out;
|
|
1194
|
+
}
|
|
1195
|
+
function appendCompactedMessages(existing, older) {
|
|
1196
|
+
const next = messagesForCompactArchive(older);
|
|
1197
|
+
if (!existing?.length) return next;
|
|
1198
|
+
if (!next.length) return existing;
|
|
1199
|
+
return [...existing, ...next];
|
|
1200
|
+
}
|
|
1175
1201
|
function applyCompaction(messages, summaryText, thresholds = {}) {
|
|
1176
1202
|
const { older, recent } = splitForCompaction(messages, thresholds);
|
|
1177
1203
|
if (older.length === 0) return messages;
|
|
1204
|
+
const text = summaryText.trim();
|
|
1178
1205
|
const summary = {
|
|
1179
1206
|
role: "summary",
|
|
1180
|
-
text
|
|
1207
|
+
text,
|
|
1208
|
+
parts: [compactSummaryPart(text)],
|
|
1181
1209
|
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
1182
1210
|
};
|
|
1183
1211
|
return [summary, ...recent];
|
|
@@ -1205,6 +1233,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
|
|
|
1205
1233
|
cwd: thread.worktreePath
|
|
1206
1234
|
});
|
|
1207
1235
|
let messages = applyCompaction(thread.messages, summary, thresholds);
|
|
1236
|
+
const compactedMessages = appendCompactedMessages(thread.compactedMessages, older);
|
|
1208
1237
|
const resetSession = shouldResetSessionForOccupancy({ messages: thread.messages });
|
|
1209
1238
|
if (resetSession) {
|
|
1210
1239
|
messages = applyForwardOccupancy(messages);
|
|
@@ -1212,6 +1241,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
|
|
|
1212
1241
|
const next = {
|
|
1213
1242
|
...thread,
|
|
1214
1243
|
messages,
|
|
1244
|
+
...compactedMessages.length ? { compactedMessages } : { compactedMessages: void 0 },
|
|
1215
1245
|
sessionId: resetSession ? null : thread.sessionId,
|
|
1216
1246
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1217
1247
|
};
|
|
@@ -1719,1595 +1749,1615 @@ function saveWorktreeRunPorts(worktreePath, scriptName, ports) {
|
|
|
1719
1749
|
`, "utf8");
|
|
1720
1750
|
}
|
|
1721
1751
|
|
|
1722
|
-
// src/
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1752
|
+
// src/mcp/wait-for-job.ts
|
|
1753
|
+
import {
|
|
1754
|
+
appendFileSync as appendFileSync2,
|
|
1755
|
+
existsSync as existsSync3,
|
|
1756
|
+
mkdirSync as mkdirSync4,
|
|
1757
|
+
readdirSync,
|
|
1758
|
+
readFileSync as readFileSync3,
|
|
1759
|
+
writeFileSync as writeFileSync3
|
|
1760
|
+
} from "fs";
|
|
1761
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
1762
|
+
|
|
1763
|
+
// src/mcp/wait-for-turn.ts
|
|
1764
|
+
var MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
|
|
1765
|
+
function mcpWaitForTurnTimeoutMs(requested) {
|
|
1766
|
+
const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
|
|
1767
|
+
if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
|
|
1768
|
+
return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
|
|
1734
1769
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
}
|
|
1756
|
-
const defaults = [];
|
|
1757
|
-
try {
|
|
1758
|
-
for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
|
|
1759
|
-
if (entry.isFile() && entry.name.startsWith(".env")) {
|
|
1760
|
-
defaults.push(entry.name);
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
} catch {
|
|
1770
|
+
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.";
|
|
1771
|
+
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.";
|
|
1772
|
+
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.";
|
|
1773
|
+
var MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
|
|
1774
|
+
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.";
|
|
1775
|
+
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.";
|
|
1776
|
+
function mcpWaitTaskHint(taskState, status) {
|
|
1777
|
+
switch (taskState) {
|
|
1778
|
+
case "submitted":
|
|
1779
|
+
return MCP_WAIT_QUEUED_HINT;
|
|
1780
|
+
case "working":
|
|
1781
|
+
return MCP_WAIT_STILL_RUNNING_HINT;
|
|
1782
|
+
case "input-required":
|
|
1783
|
+
return MCP_WAIT_INPUT_REQUIRED_HINT;
|
|
1784
|
+
case "canceled":
|
|
1785
|
+
return MCP_WAIT_STOPPED_HINT;
|
|
1786
|
+
case "failed":
|
|
1787
|
+
return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
|
|
1788
|
+
default:
|
|
1789
|
+
return void 0;
|
|
1764
1790
|
}
|
|
1765
|
-
if (defaults.length) return defaults;
|
|
1766
|
-
return [".env.local", ".env"];
|
|
1767
1791
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1792
|
+
|
|
1793
|
+
// src/mcp/wait-for-job.ts
|
|
1794
|
+
var MAX_JOB_CONTINUES = 8;
|
|
1795
|
+
var MAX_JOB_DELTA_LINES = 80;
|
|
1796
|
+
var MAX_JOB_DELTA_CHARS = 4e3;
|
|
1797
|
+
var DEFERRED_DONE_MAX_CHARS = 480;
|
|
1798
|
+
var GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
|
|
1799
|
+
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.";
|
|
1800
|
+
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.";
|
|
1801
|
+
var JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
|
|
1802
|
+
function mcpWaitForJobTimeoutMs(requested) {
|
|
1803
|
+
return mcpWaitForTurnTimeoutMs(requested);
|
|
1804
|
+
}
|
|
1805
|
+
function sanitizeDetachedJobId(id) {
|
|
1806
|
+
const s = id.trim();
|
|
1807
|
+
if (!JOB_ID_RE.test(s)) {
|
|
1808
|
+
throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
|
|
1778
1809
|
}
|
|
1779
|
-
return
|
|
1810
|
+
return s;
|
|
1780
1811
|
}
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
if (cachedLoginEnv) return { ...cachedLoginEnv };
|
|
1784
|
-
const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
|
|
1812
|
+
function jobAlive(pid) {
|
|
1813
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1785
1814
|
try {
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
timeout: 5e3
|
|
1789
|
-
});
|
|
1790
|
-
if (result.exitCode === 0 && result.stdout) {
|
|
1791
|
-
const env = { ...process.env };
|
|
1792
|
-
for (const entry of result.stdout.split("\0")) {
|
|
1793
|
-
if (!entry) continue;
|
|
1794
|
-
const eq = entry.indexOf("=");
|
|
1795
|
-
if (eq <= 0) continue;
|
|
1796
|
-
const key = entry.slice(0, eq);
|
|
1797
|
-
const value = entry.slice(eq + 1);
|
|
1798
|
-
if (key) env[key] = value;
|
|
1799
|
-
}
|
|
1800
|
-
cachedLoginEnv = env;
|
|
1801
|
-
return { ...env };
|
|
1802
|
-
}
|
|
1815
|
+
process.kill(pid, 0);
|
|
1816
|
+
return true;
|
|
1803
1817
|
} catch {
|
|
1818
|
+
return false;
|
|
1804
1819
|
}
|
|
1805
|
-
cachedLoginEnv = { ...process.env };
|
|
1806
|
-
return { ...cachedLoginEnv };
|
|
1807
1820
|
}
|
|
1808
|
-
function
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
|
|
1817
|
-
env.SIDEBOARD_ROOT_PATH = opts.repoPath;
|
|
1818
|
-
env.SIDEBOARD_IS_LOCAL = "1";
|
|
1819
|
-
if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
|
|
1820
|
-
env.CONDUCTOR_WORKSPACE_NAME = name;
|
|
1821
|
-
env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
|
|
1822
|
-
env.CONDUCTOR_ROOT_PATH = opts.repoPath;
|
|
1823
|
-
env.CONDUCTOR_IS_LOCAL = "1";
|
|
1824
|
-
if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
|
|
1825
|
-
if (primary != null) {
|
|
1826
|
-
env.SIDEBOARD_PORT = String(primary);
|
|
1827
|
-
env.CONDUCTOR_PORT = String(primary);
|
|
1828
|
-
env.PORT = String(primary);
|
|
1829
|
-
for (let i = 1; i < ports.length; i++) {
|
|
1830
|
-
const p = ports[i];
|
|
1831
|
-
env[`SIDEBOARD_PORT_${i}`] = String(p);
|
|
1832
|
-
env[`CONDUCTOR_PORT_${i}`] = String(p);
|
|
1833
|
-
}
|
|
1821
|
+
function processGroupAlive(pgid) {
|
|
1822
|
+
if (process.platform === "win32") return false;
|
|
1823
|
+
if (!Number.isInteger(pgid) || pgid <= 1) return false;
|
|
1824
|
+
try {
|
|
1825
|
+
process.kill(-pgid, 0);
|
|
1826
|
+
return true;
|
|
1827
|
+
} catch (err) {
|
|
1828
|
+
return err && typeof err === "object" && "code" in err && err.code === "EPERM";
|
|
1834
1829
|
}
|
|
1835
|
-
applyWorktreePkgCacheEnv(env, opts.worktreePath);
|
|
1836
|
-
return env;
|
|
1837
|
-
}
|
|
1838
|
-
function pipeLines(stream, onLine) {
|
|
1839
|
-
if (!stream || !onLine) return;
|
|
1840
|
-
const rl = createInterface2({ input: stream });
|
|
1841
|
-
rl.on("line", onLine);
|
|
1842
1830
|
}
|
|
1843
|
-
function
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
const pid = Number(token);
|
|
1847
|
-
if (!Number.isFinite(pid) || pid <= 0 || pid === selfPid) continue;
|
|
1848
|
-
pids.push(pid);
|
|
1849
|
-
}
|
|
1850
|
-
return pids;
|
|
1831
|
+
function jobTreeAlive(pid) {
|
|
1832
|
+
if (pid == null) return false;
|
|
1833
|
+
return jobAlive(pid) || processGroupAlive(pid);
|
|
1851
1834
|
}
|
|
1852
|
-
function
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1857
|
-
timeout: 2e3
|
|
1858
|
-
});
|
|
1859
|
-
return killableListenerPids(raw, selfPid);
|
|
1860
|
-
} catch {
|
|
1861
|
-
return [];
|
|
1862
|
-
}
|
|
1835
|
+
function readIntFile(file) {
|
|
1836
|
+
if (!existsSync3(file)) return null;
|
|
1837
|
+
const n = Number.parseInt(readFileSync3(file, "utf8").trim(), 10);
|
|
1838
|
+
return Number.isInteger(n) ? n : null;
|
|
1863
1839
|
}
|
|
1864
|
-
function
|
|
1865
|
-
|
|
1866
|
-
try {
|
|
1867
|
-
const raw = execFileSync("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
|
|
1868
|
-
encoding: "utf8",
|
|
1869
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1870
|
-
timeout: 2e3
|
|
1871
|
-
});
|
|
1872
|
-
for (const line of raw.split("\n")) {
|
|
1873
|
-
if (line.startsWith("n") && line.length > 1) return line.slice(1);
|
|
1874
|
-
}
|
|
1875
|
-
} catch {
|
|
1876
|
-
}
|
|
1877
|
-
return null;
|
|
1840
|
+
function jobDir(root, id, legacy = false) {
|
|
1841
|
+
return join5(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
|
|
1878
1842
|
}
|
|
1879
|
-
function
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1843
|
+
function resolveJobDir(root, id) {
|
|
1844
|
+
const modern = jobDir(root, id, false);
|
|
1845
|
+
if (existsSync3(modern)) return modern;
|
|
1846
|
+
const legacy = jobDir(root, id, true);
|
|
1847
|
+
if (existsSync3(legacy)) return legacy;
|
|
1848
|
+
return modern;
|
|
1885
1849
|
}
|
|
1886
|
-
function
|
|
1887
|
-
const
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
const parent = dirname3(wt);
|
|
1891
|
-
const thisName = basename(wt);
|
|
1892
|
-
if (!thisName || c === parent) return false;
|
|
1893
|
-
if (!c.startsWith(parent + sep)) return false;
|
|
1894
|
-
const first = c.slice(parent.length + 1).split(sep)[0];
|
|
1895
|
-
return Boolean(first) && first !== thisName;
|
|
1850
|
+
function listJobIdsIn(root, rel) {
|
|
1851
|
+
const dir = join5(root, rel);
|
|
1852
|
+
if (!existsSync3(dir)) return [];
|
|
1853
|
+
return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && JOB_ID_RE.test(e.name)).map((e) => e.name);
|
|
1896
1854
|
}
|
|
1897
|
-
function
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1855
|
+
function listRunningDetachedJobs(worktreePath) {
|
|
1856
|
+
const root = worktreePath.trim();
|
|
1857
|
+
if (!root) return [];
|
|
1858
|
+
const ids = /* @__PURE__ */ new Set([
|
|
1859
|
+
...listJobIdsIn(root, DETACHED_JOBS_DIR),
|
|
1860
|
+
...listJobIdsIn(root, LEGACY_DETACHED_JOBS_DIR)
|
|
1861
|
+
]);
|
|
1862
|
+
const running = [];
|
|
1863
|
+
for (const id of ids) {
|
|
1864
|
+
const dir = resolveJobDir(root, id);
|
|
1865
|
+
const pid = readIntFile(join5(dir, "pid"));
|
|
1866
|
+
if (jobTreeAlive(pid)) running.push(id);
|
|
1901
1867
|
}
|
|
1868
|
+
return running.sort();
|
|
1902
1869
|
}
|
|
1903
|
-
function
|
|
1904
|
-
const
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
execFileSync(
|
|
1910
|
-
"powershell",
|
|
1911
|
-
[
|
|
1912
|
-
"-NoProfile",
|
|
1913
|
-
"-Command",
|
|
1914
|
-
`Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -ne ${selfPid} } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
|
|
1915
|
-
],
|
|
1916
|
-
{ stdio: "ignore", timeout: 3e3 }
|
|
1917
|
-
);
|
|
1918
|
-
continue;
|
|
1919
|
-
}
|
|
1920
|
-
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
1921
|
-
signalPid(pid, "SIGTERM");
|
|
1922
|
-
}
|
|
1923
|
-
} catch {
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1870
|
+
function looksLikeDeferredDonePromise(text) {
|
|
1871
|
+
const t = (text ?? "").trim();
|
|
1872
|
+
if (!t || t.length > DEFERRED_DONE_MAX_CHARS) return false;
|
|
1873
|
+
const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
|
|
1874
|
+
if (!unquoted) return false;
|
|
1875
|
+
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);
|
|
1926
1876
|
}
|
|
1927
|
-
function
|
|
1928
|
-
if (
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
}
|
|
1932
|
-
if (process.platform === "win32") {
|
|
1933
|
-
killListenersOnPorts(ports);
|
|
1934
|
-
return;
|
|
1935
|
-
}
|
|
1936
|
-
const selfPid = process.pid;
|
|
1937
|
-
for (const port of ports) {
|
|
1938
|
-
if (!Number.isFinite(port) || port <= 0) continue;
|
|
1939
|
-
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
1940
|
-
const cwd = listenerCwd(pid);
|
|
1941
|
-
if (cwd && isOtherWorktreeListenerCwd(cwd, worktreePath)) continue;
|
|
1942
|
-
signalPid(pid, signal);
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1877
|
+
function isJobContinuePrompt(text) {
|
|
1878
|
+
if (typeof text !== "string") return false;
|
|
1879
|
+
const t = text.trim();
|
|
1880
|
+
return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
|
|
1945
1881
|
}
|
|
1946
|
-
function
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
} catch {
|
|
1958
|
-
}
|
|
1959
|
-
}, 2500).unref?.();
|
|
1960
|
-
}
|
|
1961
|
-
} catch {
|
|
1962
|
-
try {
|
|
1963
|
-
child.kill("SIGTERM");
|
|
1964
|
-
} catch {
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
killListenersOnPorts(ports);
|
|
1882
|
+
function collapseGhRunWatchLog(text) {
|
|
1883
|
+
if (!text.includes(GH_WATCH_REFRESH)) return text;
|
|
1884
|
+
const frames = text.split(/(?=Refreshing run status every 3 seconds\.)/);
|
|
1885
|
+
const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
|
|
1886
|
+
if (watchFrames.length <= 1) return text;
|
|
1887
|
+
const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
|
|
1888
|
+
const last = watchFrames[watchFrames.length - 1] ?? "";
|
|
1889
|
+
const omitted = watchFrames.length - 1;
|
|
1890
|
+
const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
|
|
1891
|
+
`;
|
|
1892
|
+
return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
|
|
1969
1893
|
}
|
|
1970
|
-
|
|
1971
|
-
const
|
|
1972
|
-
const
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
ports: opts.ports
|
|
1979
|
-
},
|
|
1980
|
-
loginEnv
|
|
1981
|
-
);
|
|
1982
|
-
try {
|
|
1983
|
-
mergeAgentGitAuthEnv(
|
|
1984
|
-
env,
|
|
1985
|
-
await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
|
|
1986
|
-
);
|
|
1987
|
-
} catch {
|
|
1988
|
-
}
|
|
1989
|
-
if (opts.heldPorts?.length) {
|
|
1990
|
-
await Promise.all(opts.heldPorts.map((h) => h.release()));
|
|
1894
|
+
function capJobLogDelta(text) {
|
|
1895
|
+
const collapsed = collapseGhRunWatchLog(text);
|
|
1896
|
+
const lines = collapsed.split("\n");
|
|
1897
|
+
let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
|
|
1898
|
+
${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
|
|
1899
|
+
if (out.length > MAX_JOB_DELTA_CHARS) {
|
|
1900
|
+
out = `\u2026(truncated ${out.length} chars)
|
|
1901
|
+
${out.slice(-MAX_JOB_DELTA_CHARS)}`;
|
|
1991
1902
|
}
|
|
1992
|
-
|
|
1993
|
-
const child = execa2(shell, ["-lc", command], {
|
|
1994
|
-
cwd: opts.worktreePath,
|
|
1995
|
-
reject: false,
|
|
1996
|
-
env,
|
|
1997
|
-
// Own process group so Stop can tear down the whole tree (not just the shell).
|
|
1998
|
-
// There is no settings.toml `stop=` / teardown hook for run scripts.
|
|
1999
|
-
...process.platform === "win32" ? {} : { detached: true }
|
|
2000
|
-
});
|
|
2001
|
-
pipeLines(child.stdout, opts.onLine);
|
|
2002
|
-
pipeLines(child.stderr, opts.onLine);
|
|
2003
|
-
const ports = opts.ports ?? [];
|
|
2004
|
-
return {
|
|
2005
|
-
pid: child.pid,
|
|
2006
|
-
kill: () => killScriptTree(child, ports),
|
|
2007
|
-
done: child.then((r) => r.exitCode ?? null),
|
|
2008
|
-
child
|
|
2009
|
-
};
|
|
1903
|
+
return out;
|
|
2010
1904
|
}
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
1905
|
+
function formatJobStillRunningContinuePrompt(jobIds) {
|
|
1906
|
+
const ids = jobIds.join(", ");
|
|
1907
|
+
return [
|
|
1908
|
+
`Detached job still running: ${ids}.`,
|
|
1909
|
+
"Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
|
|
1910
|
+
"If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
|
|
1911
|
+
"Then report the result. Do not tell the user you will let them know later."
|
|
1912
|
+
].join(" ");
|
|
2018
1913
|
}
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
worktreePath,
|
|
2026
|
-
repoPath,
|
|
2027
|
-
defaultBranch: opts?.defaultBranch,
|
|
2028
|
-
onLine
|
|
2029
|
-
});
|
|
2030
|
-
const exitCode = await attachAbort(handle, opts?.signal);
|
|
2031
|
-
return {
|
|
2032
|
-
ran: true,
|
|
2033
|
-
exitCode,
|
|
2034
|
-
source,
|
|
2035
|
-
kill: handle.kill
|
|
2036
|
-
};
|
|
1914
|
+
function formatDeferredDoneContinuePrompt() {
|
|
1915
|
+
return [
|
|
1916
|
+
"You ended the turn after promising to report later, but no detached job is running.",
|
|
1917
|
+
"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).",
|
|
1918
|
+
"Do not say you will let the user know later."
|
|
1919
|
+
].join(" ");
|
|
2037
1920
|
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
repoPath,
|
|
2045
|
-
defaultBranch: opts?.defaultBranch,
|
|
2046
|
-
onLine
|
|
1921
|
+
function turnWatchedDetachedJob(parts) {
|
|
1922
|
+
return parts.some((p) => {
|
|
1923
|
+
if (p.type !== "tool") return false;
|
|
1924
|
+
if (/wait_for_job$/i.test(p.name ?? "") || /stop_job$/i.test(p.name ?? "")) return true;
|
|
1925
|
+
const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
|
|
1926
|
+
return /detached-job\.(?:js|cjs)\b/i.test(blob);
|
|
2047
1927
|
});
|
|
2048
|
-
const exitCode = await attachAbort(handle, opts?.signal);
|
|
2049
|
-
return {
|
|
2050
|
-
ran: true,
|
|
2051
|
-
exitCode,
|
|
2052
|
-
source: found.source,
|
|
2053
|
-
kill: handle.kill
|
|
2054
|
-
};
|
|
2055
1928
|
}
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
if (
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
1929
|
+
function planJobContinue(opts) {
|
|
1930
|
+
if (opts.isOrchestrator) return { action: "none" };
|
|
1931
|
+
if (opts.agent === "brightsy") return { action: "none" };
|
|
1932
|
+
if (opts.queueLength > 0) return { action: "none" };
|
|
1933
|
+
if (opts.continueCount >= MAX_JOB_CONTINUES) return { action: "none" };
|
|
1934
|
+
const farewell = looksLikeDeferredDonePromise(opts.chatText);
|
|
1935
|
+
if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
|
|
1936
|
+
return {
|
|
1937
|
+
action: "wait",
|
|
1938
|
+
jobIds: opts.runningJobIds,
|
|
1939
|
+
prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
|
|
1940
|
+
};
|
|
2065
1941
|
}
|
|
2066
|
-
if (!
|
|
2067
|
-
|
|
1942
|
+
if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
|
|
1943
|
+
return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
|
|
2068
1944
|
}
|
|
2069
|
-
return
|
|
2070
|
-
}
|
|
2071
|
-
async function runArchiveScript(repoPath, worktreePath, onLine) {
|
|
2072
|
-
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
2073
|
-
if (!settings?.archive) return { ran: false, exitCode: null };
|
|
2074
|
-
const handle = await spawnWorkspaceScript(settings.archive, {
|
|
2075
|
-
worktreePath,
|
|
2076
|
-
repoPath,
|
|
2077
|
-
onLine
|
|
2078
|
-
});
|
|
2079
|
-
const exitCode = await handle.done;
|
|
2080
|
-
return { ran: true, exitCode };
|
|
1945
|
+
return { action: "none" };
|
|
2081
1946
|
}
|
|
2082
|
-
function
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
return
|
|
2086
|
-
if (!s.availableIn?.length) return true;
|
|
2087
|
-
return s.availableIn.includes("local");
|
|
2088
|
-
});
|
|
1947
|
+
function tailProgress(logFile, maxLines = 12) {
|
|
1948
|
+
if (!existsSync3(logFile)) return "(no log yet)";
|
|
1949
|
+
const lines = readFileSync3(logFile, "utf8").split("\n");
|
|
1950
|
+
return lines.slice(-maxLines).join("\n");
|
|
2089
1951
|
}
|
|
2090
|
-
function
|
|
2091
|
-
const
|
|
2092
|
-
|
|
2093
|
-
return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
|
|
1952
|
+
function lastProgressLine(progress) {
|
|
1953
|
+
const last = progress.split("\n").map((l) => l.trim()).filter((l) => l && l !== "(no log yet)").at(-1);
|
|
1954
|
+
return last ? last.slice(0, 72) : void 0;
|
|
2094
1955
|
}
|
|
2095
|
-
function
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
if (
|
|
2099
|
-
|
|
2100
|
-
}
|
|
2101
|
-
return getDefaultRunScript(worktreePath, repoPath);
|
|
1956
|
+
function readLogLines(file) {
|
|
1957
|
+
if (!existsSync3(file)) return [];
|
|
1958
|
+
const lines = readFileSync3(file, "utf8").split("\n");
|
|
1959
|
+
if (lines.length && lines[lines.length - 1] === "") lines.pop();
|
|
1960
|
+
return lines;
|
|
2102
1961
|
}
|
|
2103
|
-
function
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
return
|
|
2108
|
-
const server = createServer();
|
|
2109
|
-
let released = false;
|
|
2110
|
-
const release = () => new Promise((res) => {
|
|
2111
|
-
if (released) {
|
|
2112
|
-
res();
|
|
2113
|
-
return;
|
|
2114
|
-
}
|
|
2115
|
-
released = true;
|
|
2116
|
-
server.close(() => res());
|
|
2117
|
-
});
|
|
2118
|
-
server.listen(0, "127.0.0.1", () => {
|
|
2119
|
-
const addr = server.address();
|
|
2120
|
-
if (!addr || typeof addr === "string") {
|
|
2121
|
-
void release().then(() => reject(new Error("Failed to allocate port")));
|
|
2122
|
-
return;
|
|
2123
|
-
}
|
|
2124
|
-
resolve2({ port: addr.port, release });
|
|
2125
|
-
});
|
|
2126
|
-
server.on("error", reject);
|
|
2127
|
-
});
|
|
1962
|
+
function takeDelta(logFile, cursorFile) {
|
|
1963
|
+
const lines = readLogLines(logFile);
|
|
1964
|
+
const cursor = readIntFile(cursorFile) ?? 0;
|
|
1965
|
+
const start = Math.min(Math.max(0, cursor), lines.length);
|
|
1966
|
+
return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
|
|
2128
1967
|
}
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
server.close(() => res());
|
|
2141
|
-
});
|
|
2142
|
-
server.once("error", () => {
|
|
2143
|
-
try {
|
|
2144
|
-
server.close();
|
|
2145
|
-
} catch {
|
|
2146
|
-
}
|
|
2147
|
-
resolve2(null);
|
|
2148
|
-
});
|
|
2149
|
-
server.listen(port, "127.0.0.1", () => {
|
|
2150
|
-
resolve2({ port, release });
|
|
2151
|
-
});
|
|
2152
|
-
});
|
|
1968
|
+
function snapshotJob(dir) {
|
|
1969
|
+
const pid = readIntFile(join5(dir, "pid"));
|
|
1970
|
+
const running = jobTreeAlive(pid);
|
|
1971
|
+
return {
|
|
1972
|
+
pid,
|
|
1973
|
+
running,
|
|
1974
|
+
exitCode: readIntFile(join5(dir, "exit")),
|
|
1975
|
+
log: join5(dir, "log"),
|
|
1976
|
+
cursor: join5(dir, "present.cursor"),
|
|
1977
|
+
progress: tailProgress(join5(dir, "log"))
|
|
1978
|
+
};
|
|
2153
1979
|
}
|
|
2154
|
-
function
|
|
2155
|
-
|
|
2156
|
-
const
|
|
2157
|
-
const
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
1980
|
+
function toResult(id, snap, extra) {
|
|
1981
|
+
const failed = extra?.failed === true || !snap.running && snap.exitCode != null && snap.exitCode !== 0;
|
|
1982
|
+
const ok = !snap.running && snap.exitCode === 0;
|
|
1983
|
+
const stillRunning = snap.running && !ok;
|
|
1984
|
+
const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
|
|
1985
|
+
try {
|
|
1986
|
+
mkdirSync4(dirname3(snap.cursor), { recursive: true });
|
|
1987
|
+
writeFileSync3(snap.cursor, `${nextCursor}
|
|
1988
|
+
`);
|
|
1989
|
+
} catch {
|
|
2163
1990
|
}
|
|
2164
|
-
|
|
1991
|
+
const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
|
|
1992
|
+
const progress = extra?.progress ?? snap.progress;
|
|
1993
|
+
return {
|
|
1994
|
+
stillRunning,
|
|
1995
|
+
ok,
|
|
1996
|
+
failed: Boolean(failed && !stillRunning && !ok),
|
|
1997
|
+
status,
|
|
1998
|
+
id,
|
|
1999
|
+
pid: snap.pid,
|
|
2000
|
+
exitCode: snap.exitCode ?? void 0,
|
|
2001
|
+
phase: lastProgressLine(progress),
|
|
2002
|
+
delta,
|
|
2003
|
+
progress,
|
|
2004
|
+
hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
|
|
2005
|
+
};
|
|
2165
2006
|
}
|
|
2166
|
-
function
|
|
2167
|
-
|
|
2007
|
+
async function sleepMs(ms) {
|
|
2008
|
+
await new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2168
2009
|
}
|
|
2169
|
-
async function
|
|
2170
|
-
|
|
2171
|
-
const
|
|
2172
|
-
const
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
taken.add(ephemeral.port);
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
return held;
|
|
2190
|
-
} catch (err) {
|
|
2191
|
-
await Promise.all(held.map((h) => h.release()));
|
|
2192
|
-
throw err;
|
|
2010
|
+
async function waitForDetachedJob(cwd, id, opts) {
|
|
2011
|
+
const jobId = sanitizeDetachedJobId(id);
|
|
2012
|
+
const root = cwd.trim() || process.cwd();
|
|
2013
|
+
const dir = resolveJobDir(root, jobId);
|
|
2014
|
+
const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
|
|
2015
|
+
const sleep = opts?.sleep ?? sleepMs;
|
|
2016
|
+
if (!existsSync3(dir)) {
|
|
2017
|
+
return {
|
|
2018
|
+
stillRunning: false,
|
|
2019
|
+
ok: false,
|
|
2020
|
+
failed: true,
|
|
2021
|
+
status: "failed",
|
|
2022
|
+
id: jobId,
|
|
2023
|
+
delta: "",
|
|
2024
|
+
progress: "No detached job. Start one first.",
|
|
2025
|
+
hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
|
|
2026
|
+
};
|
|
2193
2027
|
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2028
|
+
const deadline = Date.now() + timeoutMs;
|
|
2029
|
+
let snap = snapshotJob(dir);
|
|
2030
|
+
if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
|
|
2031
|
+
if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
|
|
2032
|
+
return toResult(jobId, snap, {
|
|
2033
|
+
failed: true,
|
|
2034
|
+
progress: "No detached job. Start one first."
|
|
2035
|
+
});
|
|
2200
2036
|
}
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
const takePrimary = async () => {
|
|
2207
|
-
const held2 = await tryReserveAvailablePorts(ports);
|
|
2208
|
-
if (held2.some((h) => h.port === primary)) return held2;
|
|
2209
|
-
await Promise.all(held2.map((h) => h.release()));
|
|
2210
|
-
return null;
|
|
2211
|
-
};
|
|
2212
|
-
let held = await takePrimary();
|
|
2213
|
-
if (held) return held;
|
|
2214
|
-
if (worktreePath) {
|
|
2215
|
-
killStaleWorktreeListeners(ports, worktreePath, "SIGTERM");
|
|
2216
|
-
} else {
|
|
2217
|
-
killListenersOnPorts(ports);
|
|
2037
|
+
while (Date.now() < deadline) {
|
|
2038
|
+
snap = snapshotJob(dir);
|
|
2039
|
+
if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
|
|
2040
|
+
if (!snap.running) return toResult(jobId, snap, { failed: true });
|
|
2041
|
+
await sleep(Math.min(2e3, Math.max(50, deadline - Date.now())));
|
|
2218
2042
|
}
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2043
|
+
snap = snapshotJob(dir);
|
|
2044
|
+
return toResult(jobId, snap);
|
|
2045
|
+
}
|
|
2046
|
+
function killProcessTree(pid, signal) {
|
|
2047
|
+
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
2048
|
+
if (process.platform !== "win32") {
|
|
2049
|
+
try {
|
|
2050
|
+
process.kill(-pid, signal);
|
|
2051
|
+
return;
|
|
2052
|
+
} catch {
|
|
2225
2053
|
}
|
|
2226
2054
|
}
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
const preferred = normalizePreferredPorts(opts?.preferred, size);
|
|
2231
|
-
if (preferred.length) {
|
|
2232
|
-
const reused = await reclaimPreferredPorts(preferred, opts?.worktreePath);
|
|
2233
|
-
if (reused) return fillPortRange(reused, size);
|
|
2055
|
+
try {
|
|
2056
|
+
process.kill(pid, signal);
|
|
2057
|
+
} catch {
|
|
2234
2058
|
}
|
|
2235
|
-
const held = [await reservePort()];
|
|
2236
|
-
return fillPortRange(held, size);
|
|
2237
2059
|
}
|
|
2238
|
-
|
|
2239
|
-
const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
|
|
2240
|
-
if (!script) return null;
|
|
2241
|
-
const preferred = loadWorktreeRunPorts(worktreePath, script.name) ?? opts?.preferredPorts;
|
|
2242
|
-
const held = await reservePortRange(PORT_RANGE_SIZE, {
|
|
2243
|
-
preferred: preferred ?? void 0,
|
|
2244
|
-
worktreePath
|
|
2245
|
-
});
|
|
2246
|
-
const ports = held.map((h) => h.port);
|
|
2247
|
-
saveWorktreeRunPorts(worktreePath, script.name, ports);
|
|
2060
|
+
function appendJobLog(logFile, line) {
|
|
2248
2061
|
try {
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2062
|
+
mkdirSync4(dirname3(logFile), { recursive: true });
|
|
2063
|
+
const body = line.endsWith("\n") ? line : `${line}
|
|
2064
|
+
`;
|
|
2065
|
+
appendFileSync2(logFile, body);
|
|
2066
|
+
} catch {
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
async function stopDetachedJob(cwd, id, opts) {
|
|
2070
|
+
const jobId = sanitizeDetachedJobId(id);
|
|
2071
|
+
const root = cwd.trim() || process.cwd();
|
|
2072
|
+
const dir = resolveJobDir(root, jobId);
|
|
2073
|
+
const sleep = opts?.sleep ?? sleepMs;
|
|
2074
|
+
const why = opts?.reason?.trim() ?? "";
|
|
2075
|
+
if (!existsSync3(dir)) {
|
|
2257
2076
|
return {
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2077
|
+
stillRunning: false,
|
|
2078
|
+
ok: false,
|
|
2079
|
+
failed: true,
|
|
2080
|
+
status: "failed",
|
|
2081
|
+
id: jobId,
|
|
2082
|
+
delta: "",
|
|
2083
|
+
progress: "No detached job. Start one first.",
|
|
2084
|
+
hint: "Start with detached-job.cjs start <id> -- <command>, then wait_for_job or stop_job.",
|
|
2085
|
+
stopped: false,
|
|
2086
|
+
reason: "not-found"
|
|
2264
2087
|
};
|
|
2265
|
-
} catch (err) {
|
|
2266
|
-
await Promise.all(held.map((h) => h.release()));
|
|
2267
|
-
throw err;
|
|
2268
2088
|
}
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
const
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
const cut = next.slice(next.length - max);
|
|
2287
|
-
const nl = cut.indexOf("\n");
|
|
2288
|
-
return nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
|
|
2289
|
-
}
|
|
2290
|
-
var memory = /* @__PURE__ */ new Map();
|
|
2291
|
-
var persistTimers = /* @__PURE__ */ new Map();
|
|
2292
|
-
function fromSnapshot(snap) {
|
|
2293
|
-
return {
|
|
2294
|
-
chunks: snap.output ? [snap.output] : [],
|
|
2295
|
-
length: snap.output.length,
|
|
2296
|
-
running: snap.running,
|
|
2297
|
-
exitCode: snap.exitCode,
|
|
2298
|
-
source: snap.source
|
|
2299
|
-
};
|
|
2300
|
-
}
|
|
2301
|
-
function materialize(mem) {
|
|
2302
|
-
let output = mem.chunks.join("\n");
|
|
2303
|
-
if (output.length > MAX_SETUP_LOG_CHARS) {
|
|
2304
|
-
output = appendSetupOutput("", output);
|
|
2305
|
-
mem.chunks = [output];
|
|
2306
|
-
mem.length = output.length;
|
|
2089
|
+
let snap = snapshotJob(dir);
|
|
2090
|
+
if (!snap.running) {
|
|
2091
|
+
const result2 = toResult(jobId, snap);
|
|
2092
|
+
return {
|
|
2093
|
+
...result2,
|
|
2094
|
+
stopped: false,
|
|
2095
|
+
reason: "not-running",
|
|
2096
|
+
hint: "That job is already finished. Read the log; do not wait again unless you start a new command."
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
appendJobLog(snap.log, `$ stop${why ? `: ${why}` : ""}`);
|
|
2100
|
+
const targetPid = snap.pid;
|
|
2101
|
+
if (targetPid != null) killProcessTree(targetPid, "SIGTERM");
|
|
2102
|
+
const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
|
|
2103
|
+
const deadline = Date.now() + graceMs;
|
|
2104
|
+
while (Date.now() < deadline && jobTreeAlive(targetPid)) {
|
|
2105
|
+
await sleep(Math.min(100, Math.max(20, deadline - Date.now())));
|
|
2307
2106
|
}
|
|
2107
|
+
if (targetPid != null) {
|
|
2108
|
+
killProcessTree(targetPid, "SIGKILL");
|
|
2109
|
+
await sleep(100);
|
|
2110
|
+
}
|
|
2111
|
+
const exitFile = join5(dir, "exit");
|
|
2112
|
+
if (!existsSync3(exitFile)) {
|
|
2113
|
+
try {
|
|
2114
|
+
writeFileSync3(exitFile, "1\n");
|
|
2115
|
+
} catch {
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
snap = snapshotJob(dir);
|
|
2119
|
+
const stillRunning = jobTreeAlive(targetPid) || snap.running;
|
|
2120
|
+
const result = toResult(jobId, snap, { failed: true });
|
|
2121
|
+
const stopped = !stillRunning;
|
|
2308
2122
|
return {
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2123
|
+
...result,
|
|
2124
|
+
stillRunning,
|
|
2125
|
+
ok: false,
|
|
2126
|
+
failed: true,
|
|
2127
|
+
status: stillRunning ? "running" : "failed",
|
|
2128
|
+
stopped,
|
|
2129
|
+
reason: stopped ? "stopped" : "still-running",
|
|
2130
|
+
stopReason: why || void 0,
|
|
2131
|
+
hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
|
|
2313
2132
|
};
|
|
2314
2133
|
}
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2134
|
+
|
|
2135
|
+
// src/hook/conductor.ts
|
|
2136
|
+
var PORT_RANGE_SIZE = 10;
|
|
2137
|
+
var RECLAIM_RETRY_DELAY_MS = 40;
|
|
2138
|
+
var RECLAIM_TRIES = 8;
|
|
2139
|
+
function matchSimpleGlob(pattern, name) {
|
|
2140
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
2141
|
+
return new RegExp(`^${escaped}$`).test(name);
|
|
2321
2142
|
}
|
|
2322
|
-
function
|
|
2323
|
-
|
|
2143
|
+
function readWorktreeInclude(repoPath) {
|
|
2144
|
+
const path = join6(repoPath, ".worktreeinclude");
|
|
2145
|
+
if (!existsSync4(path)) return [];
|
|
2146
|
+
return readFileSync4(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
2324
2147
|
}
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2148
|
+
function resolveFilesToCopy(repoPath) {
|
|
2149
|
+
const fromInclude = readWorktreeInclude(repoPath);
|
|
2150
|
+
if (fromInclude.length) return fromInclude;
|
|
2151
|
+
const settings = loadRepoSettings(repoPath);
|
|
2152
|
+
if (settings?.filesToCopy?.length) return settings.filesToCopy;
|
|
2153
|
+
if (settings?.fileIncludeGlobs?.length) {
|
|
2154
|
+
const matched = [];
|
|
2155
|
+
try {
|
|
2156
|
+
for (const entry of readdirSync2(repoPath, { withFileTypes: true })) {
|
|
2157
|
+
if (!entry.isFile()) continue;
|
|
2158
|
+
for (const glob of settings.fileIncludeGlobs) {
|
|
2159
|
+
if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob(basename(glob), entry.name)) {
|
|
2160
|
+
matched.push(entry.name);
|
|
2161
|
+
break;
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
} catch {
|
|
2166
|
+
}
|
|
2167
|
+
if (matched.length) return [...new Set(matched)];
|
|
2331
2168
|
}
|
|
2332
|
-
const
|
|
2333
|
-
if (!mem) return;
|
|
2169
|
+
const defaults = [];
|
|
2334
2170
|
try {
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
);
|
|
2171
|
+
for (const entry of readdirSync2(repoPath, { withFileTypes: true })) {
|
|
2172
|
+
if (entry.isFile() && entry.name.startsWith(".env")) {
|
|
2173
|
+
defaults.push(entry.name);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2341
2176
|
} catch {
|
|
2342
2177
|
}
|
|
2178
|
+
if (defaults.length) return defaults;
|
|
2179
|
+
return [".env.local", ".env"];
|
|
2343
2180
|
}
|
|
2344
|
-
function
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2181
|
+
function copyConfiguredFiles(repoPath, worktreePath) {
|
|
2182
|
+
const patterns = resolveFilesToCopy(repoPath);
|
|
2183
|
+
const copied = [];
|
|
2184
|
+
for (const rel of patterns) {
|
|
2185
|
+
const src = join6(repoPath, rel);
|
|
2186
|
+
if (!existsSync4(src)) continue;
|
|
2187
|
+
const dest = join6(worktreePath, rel);
|
|
2188
|
+
mkdirSync5(dirname4(dest), { recursive: true });
|
|
2189
|
+
copyFileSync(src, dest);
|
|
2190
|
+
copied.push(rel);
|
|
2191
|
+
}
|
|
2192
|
+
return copied;
|
|
2353
2193
|
}
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
if (
|
|
2194
|
+
var cachedLoginEnv = null;
|
|
2195
|
+
async function captureLoginEnv() {
|
|
2196
|
+
if (cachedLoginEnv) return { ...cachedLoginEnv };
|
|
2197
|
+
const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
|
|
2357
2198
|
try {
|
|
2358
|
-
const
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2199
|
+
const result = await execa2(shell, ["-l", "-c", "env -0"], {
|
|
2200
|
+
reject: false,
|
|
2201
|
+
timeout: 5e3
|
|
2202
|
+
});
|
|
2203
|
+
if (result.exitCode === 0 && result.stdout) {
|
|
2204
|
+
const env = { ...process.env };
|
|
2205
|
+
for (const entry of result.stdout.split("\0")) {
|
|
2206
|
+
if (!entry) continue;
|
|
2207
|
+
const eq = entry.indexOf("=");
|
|
2208
|
+
if (eq <= 0) continue;
|
|
2209
|
+
const key = entry.slice(0, eq);
|
|
2210
|
+
const value = entry.slice(eq + 1);
|
|
2211
|
+
if (key) env[key] = value;
|
|
2212
|
+
}
|
|
2213
|
+
cachedLoginEnv = env;
|
|
2214
|
+
return { ...env };
|
|
2215
|
+
}
|
|
2365
2216
|
} catch {
|
|
2366
|
-
return null;
|
|
2367
2217
|
}
|
|
2218
|
+
cachedLoginEnv = { ...process.env };
|
|
2219
|
+
return { ...cachedLoginEnv };
|
|
2368
2220
|
}
|
|
2369
|
-
function
|
|
2370
|
-
const
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
if (
|
|
2381
|
-
|
|
2382
|
-
|
|
2221
|
+
function buildWorkspaceScriptEnv(opts, baseEnv) {
|
|
2222
|
+
const env = stripNestedElectronEnv({
|
|
2223
|
+
...baseEnv ?? process.env
|
|
2224
|
+
});
|
|
2225
|
+
const name = opts.workspaceName ?? basename(opts.worktreePath);
|
|
2226
|
+
const ports = opts.ports ?? [];
|
|
2227
|
+
const primary = ports[0];
|
|
2228
|
+
env.SIDEBOARD_WORKSPACE_NAME = name;
|
|
2229
|
+
env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
|
|
2230
|
+
env.SIDEBOARD_ROOT_PATH = opts.repoPath;
|
|
2231
|
+
env.SIDEBOARD_IS_LOCAL = "1";
|
|
2232
|
+
if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
|
|
2233
|
+
env.CONDUCTOR_WORKSPACE_NAME = name;
|
|
2234
|
+
env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
|
|
2235
|
+
env.CONDUCTOR_ROOT_PATH = opts.repoPath;
|
|
2236
|
+
env.CONDUCTOR_IS_LOCAL = "1";
|
|
2237
|
+
if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
|
|
2238
|
+
if (primary != null) {
|
|
2239
|
+
env.SIDEBOARD_PORT = String(primary);
|
|
2240
|
+
env.CONDUCTOR_PORT = String(primary);
|
|
2241
|
+
env.PORT = String(primary);
|
|
2242
|
+
for (let i = 1; i < ports.length; i++) {
|
|
2243
|
+
const p = ports[i];
|
|
2244
|
+
env[`SIDEBOARD_PORT_${i}`] = String(p);
|
|
2245
|
+
env[`CONDUCTOR_PORT_${i}`] = String(p);
|
|
2246
|
+
}
|
|
2383
2247
|
}
|
|
2384
|
-
|
|
2248
|
+
applyWorktreePkgCacheEnv(env, opts.worktreePath);
|
|
2249
|
+
return env;
|
|
2385
2250
|
}
|
|
2386
|
-
function
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
running: true,
|
|
2391
|
-
exitCode: null,
|
|
2392
|
-
source: null
|
|
2393
|
-
};
|
|
2394
|
-
memory.set(threadId, mem);
|
|
2395
|
-
persistNow(threadId);
|
|
2396
|
-
return materialize(mem);
|
|
2251
|
+
function pipeLines(stream, onLine) {
|
|
2252
|
+
if (!stream || !onLine) return;
|
|
2253
|
+
const rl = createInterface2({ input: stream });
|
|
2254
|
+
rl.on("line", onLine);
|
|
2397
2255
|
}
|
|
2398
|
-
function
|
|
2399
|
-
const
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2256
|
+
function killableListenerPids(raw, selfPid = process.pid) {
|
|
2257
|
+
const pids = [];
|
|
2258
|
+
for (const token of raw.trim().split(/\s+/)) {
|
|
2259
|
+
const pid = Number(token);
|
|
2260
|
+
if (!Number.isFinite(pid) || pid <= 0 || pid === selfPid) continue;
|
|
2261
|
+
pids.push(pid);
|
|
2262
|
+
}
|
|
2263
|
+
return pids;
|
|
2405
2264
|
}
|
|
2406
|
-
function
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
import { createHash as createHash2 } from "crypto";
|
|
2417
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
2418
|
-
import { join as join7 } from "path";
|
|
2419
|
-
var EMPTY2 = {
|
|
2420
|
-
output: "",
|
|
2421
|
-
running: false,
|
|
2422
|
-
exitCode: null,
|
|
2423
|
-
scriptName: null
|
|
2424
|
-
};
|
|
2425
|
-
var MAX_RUN_LOG_CHARS = 256e3;
|
|
2426
|
-
var DEFAULT_RUN_LOG_TAIL_CHARS = 16e3;
|
|
2427
|
-
var memory2 = /* @__PURE__ */ new Map();
|
|
2428
|
-
var persistTimers2 = /* @__PURE__ */ new Map();
|
|
2429
|
-
function fromSnapshot2(snap) {
|
|
2430
|
-
return {
|
|
2431
|
-
chunks: snap.output ? [snap.output] : [],
|
|
2432
|
-
length: snap.output.length,
|
|
2433
|
-
running: snap.running,
|
|
2434
|
-
exitCode: snap.exitCode,
|
|
2435
|
-
scriptName: snap.scriptName
|
|
2436
|
-
};
|
|
2437
|
-
}
|
|
2438
|
-
function materialize2(mem) {
|
|
2439
|
-
let output = mem.chunks.join("\n");
|
|
2440
|
-
if (output.length > MAX_RUN_LOG_CHARS) {
|
|
2441
|
-
output = appendSetupOutput("", output, MAX_RUN_LOG_CHARS);
|
|
2442
|
-
mem.chunks = [output];
|
|
2443
|
-
mem.length = output.length;
|
|
2265
|
+
function listUnixListenerPids(port, selfPid) {
|
|
2266
|
+
try {
|
|
2267
|
+
const raw = execFileSync("lsof", [`-tiTCP:${port}`, "-sTCP:LISTEN"], {
|
|
2268
|
+
encoding: "utf8",
|
|
2269
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2270
|
+
timeout: 2e3
|
|
2271
|
+
});
|
|
2272
|
+
return killableListenerPids(raw, selfPid);
|
|
2273
|
+
} catch {
|
|
2274
|
+
return [];
|
|
2444
2275
|
}
|
|
2445
|
-
return {
|
|
2446
|
-
output,
|
|
2447
|
-
running: mem.running,
|
|
2448
|
-
exitCode: mem.exitCode,
|
|
2449
|
-
scriptName: mem.scriptName
|
|
2450
|
-
};
|
|
2451
|
-
}
|
|
2452
|
-
function safeScriptToken(scriptName) {
|
|
2453
|
-
const token = scriptName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
|
|
2454
|
-
return token || "dev";
|
|
2455
|
-
}
|
|
2456
|
-
function runLogPath(key) {
|
|
2457
|
-
return join7(threadsDir(), `${key}.run.log.json`);
|
|
2458
|
-
}
|
|
2459
|
-
function runLogKeyForWorktree(worktreePath, scriptName) {
|
|
2460
|
-
const norm = normalizeWorktreePath(worktreePath);
|
|
2461
|
-
const hash = createHash2("sha1").update(norm).digest("hex").slice(0, 16);
|
|
2462
|
-
return `wt-${hash}-run-${safeScriptToken(scriptName)}`;
|
|
2463
|
-
}
|
|
2464
|
-
function emptyRunLog() {
|
|
2465
|
-
return { ...EMPTY2 };
|
|
2466
|
-
}
|
|
2467
|
-
function sliceRunLogOutput(output, tailChars = DEFAULT_RUN_LOG_TAIL_CHARS) {
|
|
2468
|
-
const max = Math.max(1, tailChars);
|
|
2469
|
-
const outputChars = output.length;
|
|
2470
|
-
if (outputChars <= max) return { output, truncated: false, outputChars };
|
|
2471
|
-
const cut = output.slice(outputChars - max);
|
|
2472
|
-
const nl = cut.indexOf("\n");
|
|
2473
|
-
const text = nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
|
|
2474
|
-
return { output: text, truncated: true, outputChars };
|
|
2475
2276
|
}
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
const timer = persistTimers2.get(key);
|
|
2479
|
-
if (timer) {
|
|
2480
|
-
clearTimeout(timer);
|
|
2481
|
-
persistTimers2.delete(key);
|
|
2482
|
-
}
|
|
2483
|
-
const mem = memory2.get(key);
|
|
2484
|
-
if (!mem) return;
|
|
2277
|
+
function listenerCwd(pid) {
|
|
2278
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
2485
2279
|
try {
|
|
2486
|
-
|
|
2487
|
-
|
|
2280
|
+
const raw = execFileSync("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
|
|
2281
|
+
encoding: "utf8",
|
|
2282
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2283
|
+
timeout: 2e3
|
|
2284
|
+
});
|
|
2285
|
+
for (const line of raw.split("\n")) {
|
|
2286
|
+
if (line.startsWith("n") && line.length > 1) return line.slice(1);
|
|
2287
|
+
}
|
|
2488
2288
|
} catch {
|
|
2489
2289
|
}
|
|
2290
|
+
return null;
|
|
2490
2291
|
}
|
|
2491
|
-
function
|
|
2492
|
-
if (persistTimers2.has(key)) return;
|
|
2493
|
-
persistTimers2.set(
|
|
2494
|
-
key,
|
|
2495
|
-
setTimeout(() => {
|
|
2496
|
-
persistTimers2.delete(key);
|
|
2497
|
-
persistNow2(key);
|
|
2498
|
-
}, PERSIST_DEBOUNCE_MS2)
|
|
2499
|
-
);
|
|
2500
|
-
}
|
|
2501
|
-
function loadFromDisk2(key) {
|
|
2502
|
-
const path = runLogPath(key);
|
|
2503
|
-
if (!existsSync5(path)) return null;
|
|
2292
|
+
function realpathOrResolve(path) {
|
|
2504
2293
|
try {
|
|
2505
|
-
|
|
2506
|
-
return {
|
|
2507
|
-
output: typeof raw.output === "string" ? raw.output : "",
|
|
2508
|
-
running: Boolean(raw.running),
|
|
2509
|
-
exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
|
|
2510
|
-
scriptName: typeof raw.scriptName === "string" ? raw.scriptName : null
|
|
2511
|
-
};
|
|
2294
|
+
return realpathSync(path);
|
|
2512
2295
|
} catch {
|
|
2513
|
-
return
|
|
2296
|
+
return resolve(path);
|
|
2514
2297
|
}
|
|
2515
2298
|
}
|
|
2516
|
-
function
|
|
2517
|
-
const
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2299
|
+
function isOtherWorktreeListenerCwd(cwd, worktreePath) {
|
|
2300
|
+
const wt = realpathOrResolve(worktreePath);
|
|
2301
|
+
const c = realpathOrResolve(cwd);
|
|
2302
|
+
if (c === wt || c.startsWith(wt + sep)) return false;
|
|
2303
|
+
const parent = dirname4(wt);
|
|
2304
|
+
const thisName = basename(wt);
|
|
2305
|
+
if (!thisName || c === parent) return false;
|
|
2306
|
+
if (!c.startsWith(parent + sep)) return false;
|
|
2307
|
+
const first = c.slice(parent.length + 1).split(sep)[0];
|
|
2308
|
+
return Boolean(first) && first !== thisName;
|
|
2522
2309
|
}
|
|
2523
|
-
function
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
if (disk) {
|
|
2528
|
-
memory2.set(key, fromSnapshot2(disk));
|
|
2529
|
-
return disk;
|
|
2310
|
+
function signalPid(pid, signal) {
|
|
2311
|
+
try {
|
|
2312
|
+
process.kill(pid, signal);
|
|
2313
|
+
} catch {
|
|
2530
2314
|
}
|
|
2531
|
-
return emptyRunLog();
|
|
2532
|
-
}
|
|
2533
|
-
function beginRunLog(key, scriptName) {
|
|
2534
|
-
const mem = {
|
|
2535
|
-
chunks: [],
|
|
2536
|
-
length: 0,
|
|
2537
|
-
running: true,
|
|
2538
|
-
exitCode: null,
|
|
2539
|
-
scriptName
|
|
2540
|
-
};
|
|
2541
|
-
memory2.set(key, mem);
|
|
2542
|
-
persistNow2(key);
|
|
2543
|
-
return materialize2(mem);
|
|
2544
|
-
}
|
|
2545
|
-
function appendRunLog(key, line) {
|
|
2546
|
-
const mem = memFor2(key);
|
|
2547
|
-
mem.chunks.push(line);
|
|
2548
|
-
mem.length += line.length + 1;
|
|
2549
|
-
mem.running = true;
|
|
2550
|
-
if (mem.length > MAX_RUN_LOG_CHARS * 2) materialize2(mem);
|
|
2551
|
-
schedulePersist2(key);
|
|
2552
|
-
}
|
|
2553
|
-
function finishRunLog(key, exitCode) {
|
|
2554
|
-
const mem = memFor2(key);
|
|
2555
|
-
mem.running = false;
|
|
2556
|
-
mem.exitCode = exitCode;
|
|
2557
|
-
persistNow2(key);
|
|
2558
|
-
return materialize2(mem);
|
|
2559
|
-
}
|
|
2560
|
-
function flushRunLog(key) {
|
|
2561
|
-
persistNow2(key);
|
|
2562
|
-
}
|
|
2563
|
-
|
|
2564
|
-
// src/git/orphan-cleanup.ts
|
|
2565
|
-
import { existsSync as existsSync6, readdirSync as readdirSync2, statSync } from "fs";
|
|
2566
|
-
import { join as join8 } from "path";
|
|
2567
|
-
function isSideboardWorktreePath(path) {
|
|
2568
|
-
return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
|
|
2569
2315
|
}
|
|
2570
|
-
|
|
2571
|
-
const
|
|
2572
|
-
const
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
}
|
|
2583
|
-
} catch {
|
|
2584
|
-
}
|
|
2316
|
+
function isThisWorktreeListenerCwd(cwd, worktreePath) {
|
|
2317
|
+
const wt = realpathOrResolve(worktreePath);
|
|
2318
|
+
const c = realpathOrResolve(cwd);
|
|
2319
|
+
return c === wt || c.startsWith(wt + sep);
|
|
2320
|
+
}
|
|
2321
|
+
function killDetachedRunTree(pid, worktreePath) {
|
|
2322
|
+
if (!Number.isFinite(pid) || pid <= 1 || pid === process.pid) return;
|
|
2323
|
+
const cwd = listenerCwd(pid);
|
|
2324
|
+
if (cwd) {
|
|
2325
|
+
if (!isThisWorktreeListenerCwd(cwd, worktreePath)) return;
|
|
2326
|
+
} else if (!processGroupAlive(pid)) {
|
|
2327
|
+
return;
|
|
2585
2328
|
}
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2329
|
+
try {
|
|
2330
|
+
process.kill(-pid, "SIGTERM");
|
|
2331
|
+
} catch {
|
|
2332
|
+
signalPid(pid, "SIGTERM");
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
function killListenersOnPorts(ports) {
|
|
2336
|
+
const selfPid = process.pid;
|
|
2337
|
+
for (const port of ports) {
|
|
2338
|
+
if (!Number.isFinite(port) || port <= 0) continue;
|
|
2590
2339
|
try {
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
mtimeMs = 0;
|
|
2603
|
-
}
|
|
2604
|
-
orphans.push({ path, repoPath, mtimeMs });
|
|
2340
|
+
if (process.platform === "win32") {
|
|
2341
|
+
execFileSync(
|
|
2342
|
+
"powershell",
|
|
2343
|
+
[
|
|
2344
|
+
"-NoProfile",
|
|
2345
|
+
"-Command",
|
|
2346
|
+
`Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -ne ${selfPid} } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
|
|
2347
|
+
],
|
|
2348
|
+
{ stdio: "ignore", timeout: 3e3 }
|
|
2349
|
+
);
|
|
2350
|
+
continue;
|
|
2605
2351
|
}
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
try {
|
|
2609
|
-
const root = worktreesRoot(repoPath);
|
|
2610
|
-
if (existsSync6(root)) {
|
|
2611
|
-
for (const entry of readdirSync2(root, { withFileTypes: true })) {
|
|
2612
|
-
if (!entry.isDirectory()) continue;
|
|
2613
|
-
const path = join8(root, entry.name).replace(/\/$/, "");
|
|
2614
|
-
if (known.has(path) || seen.has(path)) continue;
|
|
2615
|
-
if (!existsSync6(join8(path, ".git"))) continue;
|
|
2616
|
-
seen.add(path);
|
|
2617
|
-
let mtimeMs = 0;
|
|
2618
|
-
try {
|
|
2619
|
-
mtimeMs = statSync(path).mtimeMs;
|
|
2620
|
-
} catch {
|
|
2621
|
-
mtimeMs = Date.now();
|
|
2622
|
-
}
|
|
2623
|
-
orphans.push({ path, repoPath, mtimeMs });
|
|
2624
|
-
}
|
|
2352
|
+
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
2353
|
+
signalPid(pid, "SIGTERM");
|
|
2625
2354
|
}
|
|
2626
2355
|
} catch {
|
|
2627
2356
|
}
|
|
2628
2357
|
}
|
|
2629
|
-
return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
2630
2358
|
}
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
const
|
|
2641
|
-
for (const
|
|
2642
|
-
if (
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
removed.push(orphan.path);
|
|
2648
|
-
continue;
|
|
2359
|
+
function killStaleWorktreeListeners(ports, worktreePath, signal = "SIGTERM") {
|
|
2360
|
+
if (!worktreePath.trim()) {
|
|
2361
|
+
killListenersOnPorts(ports);
|
|
2362
|
+
return;
|
|
2363
|
+
}
|
|
2364
|
+
if (process.platform === "win32") {
|
|
2365
|
+
killListenersOnPorts(ports);
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
const selfPid = process.pid;
|
|
2369
|
+
for (const port of ports) {
|
|
2370
|
+
if (!Number.isFinite(port) || port <= 0) continue;
|
|
2371
|
+
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
2372
|
+
const cwd = listenerCwd(pid);
|
|
2373
|
+
if (cwd && isOtherWorktreeListenerCwd(cwd, worktreePath)) continue;
|
|
2374
|
+
signalPid(pid, signal);
|
|
2649
2375
|
}
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
function killScriptTree(child, ports = []) {
|
|
2379
|
+
const pid = child.pid;
|
|
2380
|
+
if (pid) {
|
|
2650
2381
|
try {
|
|
2651
|
-
|
|
2652
|
-
|
|
2382
|
+
if (process.platform === "win32") {
|
|
2383
|
+
void execa2("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
|
|
2384
|
+
} else {
|
|
2385
|
+
process.kill(-pid, "SIGTERM");
|
|
2386
|
+
setTimeout(() => {
|
|
2387
|
+
try {
|
|
2388
|
+
process.kill(-pid, "SIGKILL");
|
|
2389
|
+
} catch {
|
|
2390
|
+
}
|
|
2391
|
+
}, 2500).unref?.();
|
|
2392
|
+
}
|
|
2653
2393
|
} catch {
|
|
2654
|
-
|
|
2394
|
+
try {
|
|
2395
|
+
child.kill("SIGTERM");
|
|
2396
|
+
} catch {
|
|
2397
|
+
}
|
|
2655
2398
|
}
|
|
2656
2399
|
}
|
|
2657
|
-
|
|
2658
|
-
updateAdvancedSettings({
|
|
2659
|
-
worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2660
|
-
});
|
|
2661
|
-
}
|
|
2662
|
-
return { removed, kept, orphans };
|
|
2663
|
-
}
|
|
2664
|
-
function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
|
|
2665
|
-
const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
|
|
2666
|
-
const last = settings.advanced.worktreeLastCleanupAt;
|
|
2667
|
-
if (!last) return true;
|
|
2668
|
-
const elapsed = Date.now() - Date.parse(last);
|
|
2669
|
-
return elapsed >= intervalHours * 36e5;
|
|
2400
|
+
killListenersOnPorts(ports);
|
|
2670
2401
|
}
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
writeThread(
|
|
2402
|
+
async function spawnWorkspaceScript(command, opts) {
|
|
2403
|
+
const loginEnv = await captureLoginEnv();
|
|
2404
|
+
const env = buildWorkspaceScriptEnv(
|
|
2675
2405
|
{
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2406
|
+
worktreePath: opts.worktreePath,
|
|
2407
|
+
repoPath: opts.repoPath,
|
|
2408
|
+
workspaceName: opts.workspaceName,
|
|
2409
|
+
defaultBranch: opts.defaultBranch,
|
|
2410
|
+
ports: opts.ports
|
|
2681
2411
|
},
|
|
2682
|
-
|
|
2412
|
+
loginEnv
|
|
2683
2413
|
);
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2414
|
+
try {
|
|
2415
|
+
mergeAgentGitAuthEnv(
|
|
2416
|
+
env,
|
|
2417
|
+
await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
|
|
2418
|
+
);
|
|
2419
|
+
} catch {
|
|
2689
2420
|
}
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2421
|
+
if (opts.heldPorts?.length) {
|
|
2422
|
+
await Promise.all(opts.heldPorts.map((h) => h.release()));
|
|
2423
|
+
}
|
|
2424
|
+
const shell = process.platform === "darwin" ? "zsh" : "bash";
|
|
2425
|
+
const child = execa2(shell, ["-lc", command], {
|
|
2426
|
+
cwd: opts.worktreePath,
|
|
2427
|
+
reject: false,
|
|
2428
|
+
env,
|
|
2429
|
+
// Own process group so Stop can tear down the whole tree (not just the shell).
|
|
2430
|
+
// There is no settings.toml `stop=` / teardown hook for run scripts.
|
|
2431
|
+
...process.platform === "win32" ? {} : { detached: true }
|
|
2696
2432
|
});
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2433
|
+
pipeLines(child.stdout, opts.onLine);
|
|
2434
|
+
pipeLines(child.stderr, opts.onLine);
|
|
2435
|
+
const ports = opts.ports ?? [];
|
|
2436
|
+
return {
|
|
2437
|
+
pid: child.pid,
|
|
2438
|
+
kill: () => killScriptTree(child, ports),
|
|
2439
|
+
done: child.then((r) => r.exitCode ?? null),
|
|
2440
|
+
child
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
async function attachAbort(handle, signal) {
|
|
2444
|
+
if (signal) {
|
|
2445
|
+
const onAbort = () => handle.kill();
|
|
2446
|
+
if (signal.aborted) onAbort();
|
|
2447
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
2701
2448
|
}
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2449
|
+
return handle.done;
|
|
2450
|
+
}
|
|
2451
|
+
async function runSetupScript(repoPath, worktreePath, onLine, opts) {
|
|
2452
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
2453
|
+
if (!settings?.setup) return { ran: false, exitCode: null, source: null };
|
|
2454
|
+
const source = workspaceSettingsSourceLabel(worktreePath, repoPath);
|
|
2455
|
+
onLine?.(`[setup] ${source ?? "settings.toml"}`);
|
|
2456
|
+
const handle = await spawnWorkspaceScript(settings.setup, {
|
|
2457
|
+
worktreePath,
|
|
2458
|
+
repoPath,
|
|
2459
|
+
defaultBranch: opts?.defaultBranch,
|
|
2460
|
+
onLine
|
|
2461
|
+
});
|
|
2462
|
+
const exitCode = await attachAbort(handle, opts?.signal);
|
|
2463
|
+
return {
|
|
2464
|
+
ran: true,
|
|
2465
|
+
exitCode,
|
|
2466
|
+
source,
|
|
2467
|
+
kill: handle.kill
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
|
|
2471
|
+
const found = findConventionSetup(worktreePath, repoPath);
|
|
2472
|
+
if (!found) return { ran: false, exitCode: null, source: null };
|
|
2473
|
+
onLine?.(`[setup] ${found.source}`);
|
|
2474
|
+
const handle = await spawnWorkspaceScript(found.command, {
|
|
2475
|
+
worktreePath,
|
|
2476
|
+
repoPath,
|
|
2477
|
+
defaultBranch: opts?.defaultBranch,
|
|
2478
|
+
onLine
|
|
2479
|
+
});
|
|
2480
|
+
const exitCode = await attachAbort(handle, opts?.signal);
|
|
2481
|
+
return {
|
|
2482
|
+
ran: true,
|
|
2483
|
+
exitCode,
|
|
2484
|
+
source: found.source,
|
|
2485
|
+
kill: handle.kill
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
|
|
2489
|
+
await ensureWorktreeSideboardIgnored(worktreePath);
|
|
2490
|
+
const review = ensureReviewGuidelinesFile(worktreePath, repoPath);
|
|
2491
|
+
if (review.wrote) {
|
|
2492
|
+
onLine?.(`[setup] wrote ${review.path} \u2014 local Review guidelines`);
|
|
2710
2493
|
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
try {
|
|
2715
|
-
deleteThreadRecord(id);
|
|
2716
|
-
purged.push(id);
|
|
2717
|
-
} catch {
|
|
2718
|
-
}
|
|
2494
|
+
let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
|
|
2495
|
+
if (!setup.ran) {
|
|
2496
|
+
setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
|
|
2719
2497
|
}
|
|
2720
|
-
if (
|
|
2721
|
-
|
|
2722
|
-
historyLastCleanupAt: new Date(nowMs).toISOString()
|
|
2723
|
-
});
|
|
2498
|
+
if (!setup.ran) {
|
|
2499
|
+
setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
|
|
2724
2500
|
}
|
|
2725
|
-
return
|
|
2501
|
+
return setup;
|
|
2726
2502
|
}
|
|
2727
|
-
function
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
const
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2503
|
+
async function runArchiveScript(repoPath, worktreePath, onLine) {
|
|
2504
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
2505
|
+
if (!settings?.archive) return { ran: false, exitCode: null };
|
|
2506
|
+
const handle = await spawnWorkspaceScript(settings.archive, {
|
|
2507
|
+
worktreePath,
|
|
2508
|
+
repoPath,
|
|
2509
|
+
onLine
|
|
2510
|
+
});
|
|
2511
|
+
const exitCode = await handle.done;
|
|
2512
|
+
return { ran: true, exitCode };
|
|
2734
2513
|
}
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
const status = await execa3("git", ["status", "--porcelain"], {
|
|
2742
|
-
cwd: thread.repoPath,
|
|
2743
|
-
reject: false
|
|
2514
|
+
function listRunScripts(worktreePath, repoPath) {
|
|
2515
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
2516
|
+
if (!settings?.runScripts.length) return [];
|
|
2517
|
+
return settings.runScripts.filter((s) => {
|
|
2518
|
+
if (!s.availableIn?.length) return true;
|
|
2519
|
+
return s.availableIn.includes("local");
|
|
2744
2520
|
});
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2521
|
+
}
|
|
2522
|
+
function getDefaultRunScript(worktreePath, repoPath) {
|
|
2523
|
+
const scripts = listRunScripts(worktreePath, repoPath);
|
|
2524
|
+
if (!scripts.length) return null;
|
|
2525
|
+
return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
|
|
2526
|
+
}
|
|
2527
|
+
function getRunScript(worktreePath, repoPath, name) {
|
|
2528
|
+
const scripts = listRunScripts(worktreePath, repoPath);
|
|
2529
|
+
if (!scripts.length) return null;
|
|
2530
|
+
if (name) {
|
|
2531
|
+
return scripts.find((s) => s.name === name) ?? null;
|
|
2749
2532
|
}
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
);
|
|
2533
|
+
return getDefaultRunScript(worktreePath, repoPath);
|
|
2534
|
+
}
|
|
2535
|
+
function getRunMode(worktreePath, repoPath) {
|
|
2536
|
+
return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
|
|
2537
|
+
}
|
|
2538
|
+
async function reservePort() {
|
|
2539
|
+
return new Promise((resolve2, reject) => {
|
|
2540
|
+
const server = createServer();
|
|
2541
|
+
let released = false;
|
|
2542
|
+
const release = () => new Promise((res) => {
|
|
2543
|
+
if (released) {
|
|
2544
|
+
res();
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
released = true;
|
|
2548
|
+
server.close(() => res());
|
|
2549
|
+
});
|
|
2550
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2551
|
+
const addr = server.address();
|
|
2552
|
+
if (!addr || typeof addr === "string") {
|
|
2553
|
+
void release().then(() => reject(new Error("Failed to allocate port")));
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
resolve2({ port: addr.port, release });
|
|
2557
|
+
});
|
|
2558
|
+
server.on("error", reject);
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
async function tryReservePort(port) {
|
|
2562
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
|
|
2563
|
+
return new Promise((resolve2) => {
|
|
2564
|
+
const server = createServer();
|
|
2565
|
+
let released = false;
|
|
2566
|
+
const release = () => new Promise((res) => {
|
|
2567
|
+
if (released) {
|
|
2568
|
+
res();
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
released = true;
|
|
2572
|
+
server.close(() => res());
|
|
2573
|
+
});
|
|
2574
|
+
server.once("error", () => {
|
|
2575
|
+
try {
|
|
2576
|
+
server.close();
|
|
2577
|
+
} catch {
|
|
2578
|
+
}
|
|
2579
|
+
resolve2(null);
|
|
2580
|
+
});
|
|
2581
|
+
server.listen(port, "127.0.0.1", () => {
|
|
2582
|
+
resolve2({ port, release });
|
|
2583
|
+
});
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
function normalizePreferredPorts(ports, size) {
|
|
2587
|
+
if (!ports?.length) return [];
|
|
2588
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2589
|
+
const out = [];
|
|
2590
|
+
for (const p of ports) {
|
|
2591
|
+
if (!Number.isInteger(p) || p <= 0 || p > 65535 || seen.has(p)) continue;
|
|
2592
|
+
seen.add(p);
|
|
2593
|
+
out.push(p);
|
|
2594
|
+
if (out.length >= size) break;
|
|
2758
2595
|
}
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
}
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
reject: false
|
|
2783
|
-
});
|
|
2784
|
-
throw new Error(
|
|
2785
|
-
`Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
|
|
2786
|
-
);
|
|
2596
|
+
return out;
|
|
2597
|
+
}
|
|
2598
|
+
function delay(ms) {
|
|
2599
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2600
|
+
}
|
|
2601
|
+
async function fillPortRange(held, size) {
|
|
2602
|
+
if (held.length >= size) return held;
|
|
2603
|
+
const base = held[0]?.port;
|
|
2604
|
+
const taken = new Set(held.map((h) => h.port));
|
|
2605
|
+
try {
|
|
2606
|
+
let offset = 0;
|
|
2607
|
+
while (held.length < size) {
|
|
2608
|
+
const candidate = base != null ? base + offset : null;
|
|
2609
|
+
offset += 1;
|
|
2610
|
+
if (candidate != null && taken.has(candidate)) continue;
|
|
2611
|
+
const next = candidate != null ? await tryReservePort(candidate) : null;
|
|
2612
|
+
if (next) {
|
|
2613
|
+
held.push(next);
|
|
2614
|
+
taken.add(next.port);
|
|
2615
|
+
} else {
|
|
2616
|
+
const ephemeral = await reservePort();
|
|
2617
|
+
held.push(ephemeral);
|
|
2618
|
+
taken.add(ephemeral.port);
|
|
2787
2619
|
}
|
|
2788
2620
|
}
|
|
2789
|
-
return
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
|
|
2794
|
-
};
|
|
2795
|
-
}
|
|
2796
|
-
const merge = await execa3(
|
|
2797
|
-
"git",
|
|
2798
|
-
["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
|
|
2799
|
-
{ cwd: thread.repoPath, reject: false }
|
|
2800
|
-
);
|
|
2801
|
-
if (merge.exitCode !== 0) {
|
|
2802
|
-
await execa3("git", ["merge", "--abort"], {
|
|
2803
|
-
cwd: thread.repoPath,
|
|
2804
|
-
reject: false
|
|
2805
|
-
});
|
|
2806
|
-
throw new Error(
|
|
2807
|
-
`Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
|
|
2808
|
-
);
|
|
2621
|
+
return held;
|
|
2622
|
+
} catch (err) {
|
|
2623
|
+
await Promise.all(held.map((h) => h.release()));
|
|
2624
|
+
throw err;
|
|
2809
2625
|
}
|
|
2810
|
-
return {
|
|
2811
|
-
applied: true,
|
|
2812
|
-
method: "merge",
|
|
2813
|
-
targetBranch: target,
|
|
2814
|
-
message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
|
|
2815
|
-
};
|
|
2816
2626
|
}
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
async function cloneRepoIntoSideboard(opts) {
|
|
2823
|
-
const url = opts.url.trim();
|
|
2824
|
-
if (!url) throw new Error("Clone URL is required");
|
|
2825
|
-
let name = opts.name?.trim();
|
|
2826
|
-
if (!name) {
|
|
2827
|
-
const leaf = basename2(url.replace(/\/$/, "").replace(/\.git$/, ""));
|
|
2828
|
-
name = leaf || "repo";
|
|
2627
|
+
async function tryReserveAvailablePorts(ports) {
|
|
2628
|
+
const held = [];
|
|
2629
|
+
for (const port of ports) {
|
|
2630
|
+
const next = await tryReservePort(port);
|
|
2631
|
+
if (next) held.push(next);
|
|
2829
2632
|
}
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2633
|
+
return held;
|
|
2634
|
+
}
|
|
2635
|
+
async function reclaimPreferredPorts(ports, worktreePath) {
|
|
2636
|
+
const primary = ports[0];
|
|
2637
|
+
if (primary == null) return null;
|
|
2638
|
+
const takePrimary = async () => {
|
|
2639
|
+
const held2 = await tryReserveAvailablePorts(ports);
|
|
2640
|
+
if (held2.some((h) => h.port === primary)) return held2;
|
|
2641
|
+
await Promise.all(held2.map((h) => h.release()));
|
|
2642
|
+
return null;
|
|
2643
|
+
};
|
|
2644
|
+
let held = await takePrimary();
|
|
2645
|
+
if (held) return held;
|
|
2646
|
+
if (worktreePath) {
|
|
2647
|
+
killStaleWorktreeListeners(ports, worktreePath, "SIGTERM");
|
|
2648
|
+
} else {
|
|
2649
|
+
killListenersOnPorts(ports);
|
|
2836
2650
|
}
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
)
|
|
2651
|
+
for (let i = 0; i < RECLAIM_TRIES; i++) {
|
|
2652
|
+
await delay(RECLAIM_RETRY_DELAY_MS);
|
|
2653
|
+
held = await takePrimary();
|
|
2654
|
+
if (held) return held;
|
|
2655
|
+
if (i === 2 && worktreePath) {
|
|
2656
|
+
killStaleWorktreeListeners(ports, worktreePath, "SIGKILL");
|
|
2657
|
+
}
|
|
2842
2658
|
}
|
|
2843
|
-
|
|
2844
|
-
const workspace = await ensureWorkspace(repoPath);
|
|
2845
|
-
return { repoPath, workspace };
|
|
2846
|
-
}
|
|
2847
|
-
|
|
2848
|
-
// src/types/thread.ts
|
|
2849
|
-
function isAdoptableRunScriptRequest(req) {
|
|
2850
|
-
return Boolean(req && !req.fulfilledAt && !req.error);
|
|
2851
|
-
}
|
|
2852
|
-
|
|
2853
|
-
// src/store/desktop-host.ts
|
|
2854
|
-
import { readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
|
|
2855
|
-
import { join as join10 } from "path";
|
|
2856
|
-
function desktopHostPidPath() {
|
|
2857
|
-
return join10(appDataDir(), "desktop-host.pid");
|
|
2659
|
+
return null;
|
|
2858
2660
|
}
|
|
2859
|
-
function
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
return
|
|
2864
|
-
} catch {
|
|
2865
|
-
return null;
|
|
2661
|
+
async function reservePortRange(size = PORT_RANGE_SIZE, opts) {
|
|
2662
|
+
const preferred = normalizePreferredPorts(opts?.preferred, size);
|
|
2663
|
+
if (preferred.length) {
|
|
2664
|
+
const reused = await reclaimPreferredPorts(preferred, opts?.worktreePath);
|
|
2665
|
+
if (reused) return fillPortRange(reused, size);
|
|
2866
2666
|
}
|
|
2667
|
+
const held = [await reservePort()];
|
|
2668
|
+
return fillPortRange(held, size);
|
|
2867
2669
|
}
|
|
2868
|
-
function
|
|
2670
|
+
async function startDevServer(repoPath, worktreePath, onLine, opts) {
|
|
2671
|
+
const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
|
|
2672
|
+
if (!script) return null;
|
|
2673
|
+
const preferred = loadWorktreeRunPorts(worktreePath, script.name) ?? opts?.preferredPorts;
|
|
2674
|
+
const held = await reservePortRange(PORT_RANGE_SIZE, {
|
|
2675
|
+
preferred: preferred ?? void 0,
|
|
2676
|
+
worktreePath
|
|
2677
|
+
});
|
|
2678
|
+
const ports = held.map((h) => h.port);
|
|
2679
|
+
saveWorktreeRunPorts(worktreePath, script.name, ports);
|
|
2869
2680
|
try {
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2681
|
+
const handle = await spawnWorkspaceScript(script.command, {
|
|
2682
|
+
worktreePath,
|
|
2683
|
+
repoPath,
|
|
2684
|
+
ports,
|
|
2685
|
+
heldPorts: held,
|
|
2686
|
+
defaultBranch: opts?.defaultBranch,
|
|
2687
|
+
onLine
|
|
2688
|
+
});
|
|
2689
|
+
return {
|
|
2690
|
+
pid: handle.pid,
|
|
2691
|
+
port: ports[0],
|
|
2692
|
+
ports,
|
|
2693
|
+
scriptName: script.name,
|
|
2694
|
+
kill: handle.kill,
|
|
2695
|
+
done: handle.done
|
|
2696
|
+
};
|
|
2697
|
+
} catch (err) {
|
|
2698
|
+
await Promise.all(held.map((h) => h.release()));
|
|
2699
|
+
throw err;
|
|
2874
2700
|
}
|
|
2875
2701
|
}
|
|
2876
|
-
function isDesktopHostAlive() {
|
|
2877
|
-
const pid = readDesktopHostPid();
|
|
2878
|
-
return pid != null && pidAlive(pid);
|
|
2879
|
-
}
|
|
2880
|
-
function isThisProcessDesktopHost() {
|
|
2881
|
-
return readDesktopHostPid() === process.pid && pidAlive(process.pid);
|
|
2882
|
-
}
|
|
2883
|
-
function thisProcessShouldDrainAgentQueues() {
|
|
2884
|
-
if (isThisProcessDesktopHost()) return true;
|
|
2885
|
-
return !isDesktopHostAlive();
|
|
2886
|
-
}
|
|
2887
2702
|
|
|
2888
|
-
// src/
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
function shouldNotifyParentOfChildHalt(opts) {
|
|
2907
|
-
if (!isIncompleteChildStatus(opts.status)) return false;
|
|
2908
|
-
if (!opts.child.parentThreadId) return false;
|
|
2909
|
-
if (!opts.parent || opts.parent.status === "archived") return false;
|
|
2910
|
-
if (opts.parent.id === opts.child.id) return false;
|
|
2911
|
-
return isOrchestratorThread(opts.parent);
|
|
2703
|
+
// src/store/setup-log.ts
|
|
2704
|
+
import { createHash } from "crypto";
|
|
2705
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
2706
|
+
import { join as join7 } from "path";
|
|
2707
|
+
var EMPTY = {
|
|
2708
|
+
output: "",
|
|
2709
|
+
running: false,
|
|
2710
|
+
exitCode: null,
|
|
2711
|
+
source: null
|
|
2712
|
+
};
|
|
2713
|
+
var MAX_SETUP_LOG_CHARS = 256e3;
|
|
2714
|
+
function appendSetupOutput(prev, line, max = MAX_SETUP_LOG_CHARS) {
|
|
2715
|
+
const next = prev ? `${prev}
|
|
2716
|
+
${line}` : line;
|
|
2717
|
+
if (next.length <= max) return next;
|
|
2718
|
+
const cut = next.slice(next.length - max);
|
|
2719
|
+
const nl = cut.indexOf("\n");
|
|
2720
|
+
return nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
|
|
2912
2721
|
}
|
|
2913
|
-
var
|
|
2914
|
-
|
|
2915
|
-
|
|
2722
|
+
var memory = /* @__PURE__ */ new Map();
|
|
2723
|
+
var persistTimers = /* @__PURE__ */ new Map();
|
|
2724
|
+
function fromSnapshot(snap) {
|
|
2725
|
+
return {
|
|
2726
|
+
chunks: snap.output ? [snap.output] : [],
|
|
2727
|
+
length: snap.output.length,
|
|
2728
|
+
running: snap.running,
|
|
2729
|
+
exitCode: snap.exitCode,
|
|
2730
|
+
source: snap.source
|
|
2731
|
+
};
|
|
2916
2732
|
}
|
|
2917
|
-
function
|
|
2918
|
-
|
|
2919
|
-
if (
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2733
|
+
function materialize(mem) {
|
|
2734
|
+
let output = mem.chunks.join("\n");
|
|
2735
|
+
if (output.length > MAX_SETUP_LOG_CHARS) {
|
|
2736
|
+
output = appendSetupOutput("", output);
|
|
2737
|
+
mem.chunks = [output];
|
|
2738
|
+
mem.length = output.length;
|
|
2739
|
+
}
|
|
2740
|
+
return {
|
|
2741
|
+
output,
|
|
2742
|
+
running: mem.running,
|
|
2743
|
+
exitCode: mem.exitCode,
|
|
2744
|
+
source: mem.source
|
|
2745
|
+
};
|
|
2928
2746
|
}
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
import {
|
|
2932
|
-
appendFileSync as appendFileSync2,
|
|
2933
|
-
existsSync as existsSync8,
|
|
2934
|
-
mkdirSync as mkdirSync5,
|
|
2935
|
-
readdirSync as readdirSync3,
|
|
2936
|
-
readFileSync as readFileSync7,
|
|
2937
|
-
writeFileSync as writeFileSync6
|
|
2938
|
-
} from "fs";
|
|
2939
|
-
import { dirname as dirname4, join as join11 } from "path";
|
|
2940
|
-
|
|
2941
|
-
// src/mcp/wait-for-turn.ts
|
|
2942
|
-
var MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
|
|
2943
|
-
function mcpWaitForTurnTimeoutMs(requested) {
|
|
2944
|
-
const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
|
|
2945
|
-
if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
|
|
2946
|
-
return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
|
|
2747
|
+
function setupLogPath(threadId) {
|
|
2748
|
+
return join7(threadsDir(), `${threadId}.setup.log.json`);
|
|
2947
2749
|
}
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
var MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
|
|
2952
|
-
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.";
|
|
2953
|
-
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.";
|
|
2954
|
-
function mcpWaitTaskHint(taskState, status) {
|
|
2955
|
-
switch (taskState) {
|
|
2956
|
-
case "submitted":
|
|
2957
|
-
return MCP_WAIT_QUEUED_HINT;
|
|
2958
|
-
case "working":
|
|
2959
|
-
return MCP_WAIT_STILL_RUNNING_HINT;
|
|
2960
|
-
case "input-required":
|
|
2961
|
-
return MCP_WAIT_INPUT_REQUIRED_HINT;
|
|
2962
|
-
case "canceled":
|
|
2963
|
-
return MCP_WAIT_STOPPED_HINT;
|
|
2964
|
-
case "failed":
|
|
2965
|
-
return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
|
|
2966
|
-
default:
|
|
2967
|
-
return void 0;
|
|
2968
|
-
}
|
|
2750
|
+
function setupLogKeyForWorktree(worktreePath) {
|
|
2751
|
+
const norm = normalizeWorktreePath(worktreePath);
|
|
2752
|
+
return `wt-${createHash("sha1").update(norm).digest("hex").slice(0, 16)}`;
|
|
2969
2753
|
}
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
var MAX_JOB_CONTINUES = 8;
|
|
2973
|
-
var MAX_JOB_DELTA_LINES = 80;
|
|
2974
|
-
var MAX_JOB_DELTA_CHARS = 4e3;
|
|
2975
|
-
var DEFERRED_DONE_MAX_CHARS = 480;
|
|
2976
|
-
var GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
|
|
2977
|
-
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.";
|
|
2978
|
-
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.";
|
|
2979
|
-
var JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
|
|
2980
|
-
function mcpWaitForJobTimeoutMs(requested) {
|
|
2981
|
-
return mcpWaitForTurnTimeoutMs(requested);
|
|
2754
|
+
function emptySetupLog() {
|
|
2755
|
+
return { ...EMPTY };
|
|
2982
2756
|
}
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2757
|
+
var PERSIST_DEBOUNCE_MS = 500;
|
|
2758
|
+
function persistNow(threadId) {
|
|
2759
|
+
const timer = persistTimers.get(threadId);
|
|
2760
|
+
if (timer) {
|
|
2761
|
+
clearTimeout(timer);
|
|
2762
|
+
persistTimers.delete(threadId);
|
|
2987
2763
|
}
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
function jobAlive(pid) {
|
|
2991
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
2764
|
+
const mem = memory.get(threadId);
|
|
2765
|
+
if (!mem) return;
|
|
2992
2766
|
try {
|
|
2993
|
-
|
|
2994
|
-
|
|
2767
|
+
writeFileSync4(
|
|
2768
|
+
setupLogPath(threadId),
|
|
2769
|
+
`${JSON.stringify(materialize(mem))}
|
|
2770
|
+
`,
|
|
2771
|
+
"utf8"
|
|
2772
|
+
);
|
|
2995
2773
|
} catch {
|
|
2996
|
-
return false;
|
|
2997
2774
|
}
|
|
2998
2775
|
}
|
|
2999
|
-
function
|
|
3000
|
-
if (
|
|
3001
|
-
|
|
2776
|
+
function schedulePersist(threadId) {
|
|
2777
|
+
if (persistTimers.has(threadId)) return;
|
|
2778
|
+
persistTimers.set(
|
|
2779
|
+
threadId,
|
|
2780
|
+
setTimeout(() => {
|
|
2781
|
+
persistTimers.delete(threadId);
|
|
2782
|
+
persistNow(threadId);
|
|
2783
|
+
}, PERSIST_DEBOUNCE_MS)
|
|
2784
|
+
);
|
|
2785
|
+
}
|
|
2786
|
+
function loadFromDisk(threadId) {
|
|
2787
|
+
const path = setupLogPath(threadId);
|
|
2788
|
+
if (!existsSync5(path)) return null;
|
|
3002
2789
|
try {
|
|
3003
|
-
|
|
3004
|
-
return
|
|
3005
|
-
|
|
3006
|
-
|
|
2790
|
+
const raw = JSON.parse(readFileSync5(path, "utf8"));
|
|
2791
|
+
return {
|
|
2792
|
+
output: typeof raw.output === "string" ? raw.output : "",
|
|
2793
|
+
running: Boolean(raw.running),
|
|
2794
|
+
exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
|
|
2795
|
+
source: typeof raw.source === "string" ? raw.source : null
|
|
2796
|
+
};
|
|
2797
|
+
} catch {
|
|
2798
|
+
return null;
|
|
3007
2799
|
}
|
|
3008
2800
|
}
|
|
3009
|
-
function
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
const n = Number.parseInt(readFileSync7(file, "utf8").trim(), 10);
|
|
3016
|
-
return Number.isInteger(n) ? n : null;
|
|
2801
|
+
function memFor(threadId) {
|
|
2802
|
+
const cached = memory.get(threadId);
|
|
2803
|
+
if (cached) return cached;
|
|
2804
|
+
const mem = fromSnapshot(loadFromDisk(threadId) ?? emptySetupLog());
|
|
2805
|
+
memory.set(threadId, mem);
|
|
2806
|
+
return mem;
|
|
3017
2807
|
}
|
|
3018
|
-
function
|
|
3019
|
-
|
|
2808
|
+
function readSetupLog(threadId) {
|
|
2809
|
+
const cached = memory.get(threadId);
|
|
2810
|
+
if (cached) return materialize(cached);
|
|
2811
|
+
const disk = loadFromDisk(threadId);
|
|
2812
|
+
if (disk) {
|
|
2813
|
+
memory.set(threadId, fromSnapshot(disk));
|
|
2814
|
+
return disk;
|
|
2815
|
+
}
|
|
2816
|
+
return emptySetupLog();
|
|
3020
2817
|
}
|
|
3021
|
-
function
|
|
3022
|
-
const
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
2818
|
+
function beginSetupLog(threadId) {
|
|
2819
|
+
const mem = {
|
|
2820
|
+
chunks: [],
|
|
2821
|
+
length: 0,
|
|
2822
|
+
running: true,
|
|
2823
|
+
exitCode: null,
|
|
2824
|
+
source: null
|
|
2825
|
+
};
|
|
2826
|
+
memory.set(threadId, mem);
|
|
2827
|
+
persistNow(threadId);
|
|
2828
|
+
return materialize(mem);
|
|
3027
2829
|
}
|
|
3028
|
-
function
|
|
3029
|
-
const
|
|
3030
|
-
|
|
3031
|
-
|
|
2830
|
+
function appendSetupLog(threadId, line) {
|
|
2831
|
+
const mem = memFor(threadId);
|
|
2832
|
+
mem.chunks.push(line);
|
|
2833
|
+
mem.length += line.length + 1;
|
|
2834
|
+
mem.running = true;
|
|
2835
|
+
if (mem.length > MAX_SETUP_LOG_CHARS * 2) materialize(mem);
|
|
2836
|
+
schedulePersist(threadId);
|
|
3032
2837
|
}
|
|
3033
|
-
function
|
|
3034
|
-
const
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
const running = [];
|
|
3041
|
-
for (const id of ids) {
|
|
3042
|
-
const dir = resolveJobDir(root, id);
|
|
3043
|
-
const pid = readIntFile(join11(dir, "pid"));
|
|
3044
|
-
if (jobTreeAlive(pid)) running.push(id);
|
|
3045
|
-
}
|
|
3046
|
-
return running.sort();
|
|
2838
|
+
function finishSetupLog(threadId, exitCode, source) {
|
|
2839
|
+
const mem = memFor(threadId);
|
|
2840
|
+
mem.running = false;
|
|
2841
|
+
mem.exitCode = exitCode;
|
|
2842
|
+
mem.source = source ?? mem.source;
|
|
2843
|
+
persistNow(threadId);
|
|
2844
|
+
return materialize(mem);
|
|
3047
2845
|
}
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
2846
|
+
|
|
2847
|
+
// src/store/run-log.ts
|
|
2848
|
+
import { createHash as createHash2 } from "crypto";
|
|
2849
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
2850
|
+
import { join as join8 } from "path";
|
|
2851
|
+
var EMPTY2 = {
|
|
2852
|
+
output: "",
|
|
2853
|
+
running: false,
|
|
2854
|
+
exitCode: null,
|
|
2855
|
+
scriptName: null
|
|
2856
|
+
};
|
|
2857
|
+
var MAX_RUN_LOG_CHARS = 256e3;
|
|
2858
|
+
var DEFAULT_RUN_LOG_TAIL_CHARS = 16e3;
|
|
2859
|
+
var memory2 = /* @__PURE__ */ new Map();
|
|
2860
|
+
var persistTimers2 = /* @__PURE__ */ new Map();
|
|
2861
|
+
function fromSnapshot2(snap) {
|
|
2862
|
+
return {
|
|
2863
|
+
chunks: snap.output ? [snap.output] : [],
|
|
2864
|
+
length: snap.output.length,
|
|
2865
|
+
running: snap.running,
|
|
2866
|
+
exitCode: snap.exitCode,
|
|
2867
|
+
scriptName: snap.scriptName
|
|
2868
|
+
};
|
|
3054
2869
|
}
|
|
3055
|
-
function
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
2870
|
+
function materialize2(mem) {
|
|
2871
|
+
let output = mem.chunks.join("\n");
|
|
2872
|
+
if (output.length > MAX_RUN_LOG_CHARS) {
|
|
2873
|
+
output = appendSetupOutput("", output, MAX_RUN_LOG_CHARS);
|
|
2874
|
+
mem.chunks = [output];
|
|
2875
|
+
mem.length = output.length;
|
|
2876
|
+
}
|
|
2877
|
+
return {
|
|
2878
|
+
output,
|
|
2879
|
+
running: mem.running,
|
|
2880
|
+
exitCode: mem.exitCode,
|
|
2881
|
+
scriptName: mem.scriptName
|
|
2882
|
+
};
|
|
3059
2883
|
}
|
|
3060
|
-
function
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
|
|
3064
|
-
if (watchFrames.length <= 1) return text;
|
|
3065
|
-
const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
|
|
3066
|
-
const last = watchFrames[watchFrames.length - 1] ?? "";
|
|
3067
|
-
const omitted = watchFrames.length - 1;
|
|
3068
|
-
const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
|
|
3069
|
-
`;
|
|
3070
|
-
return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
|
|
2884
|
+
function safeScriptToken(scriptName) {
|
|
2885
|
+
const token = scriptName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
|
|
2886
|
+
return token || "dev";
|
|
3071
2887
|
}
|
|
3072
|
-
function
|
|
3073
|
-
|
|
3074
|
-
const lines = collapsed.split("\n");
|
|
3075
|
-
let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
|
|
3076
|
-
${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
|
|
3077
|
-
if (out.length > MAX_JOB_DELTA_CHARS) {
|
|
3078
|
-
out = `\u2026(truncated ${out.length} chars)
|
|
3079
|
-
${out.slice(-MAX_JOB_DELTA_CHARS)}`;
|
|
3080
|
-
}
|
|
3081
|
-
return out;
|
|
2888
|
+
function runLogPath(key) {
|
|
2889
|
+
return join8(threadsDir(), `${key}.run.log.json`);
|
|
3082
2890
|
}
|
|
3083
|
-
function
|
|
3084
|
-
const
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
"Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
|
|
3088
|
-
"If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
|
|
3089
|
-
"Then report the result. Do not tell the user you will let them know later."
|
|
3090
|
-
].join(" ");
|
|
2891
|
+
function runLogKeyForWorktree(worktreePath, scriptName) {
|
|
2892
|
+
const norm = normalizeWorktreePath(worktreePath);
|
|
2893
|
+
const hash = createHash2("sha1").update(norm).digest("hex").slice(0, 16);
|
|
2894
|
+
return `wt-${hash}-run-${safeScriptToken(scriptName)}`;
|
|
3091
2895
|
}
|
|
3092
|
-
function
|
|
3093
|
-
return
|
|
3094
|
-
"You ended the turn after promising to report later, but no detached job is running.",
|
|
3095
|
-
"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).",
|
|
3096
|
-
"Do not say you will let the user know later."
|
|
3097
|
-
].join(" ");
|
|
2896
|
+
function emptyRunLog() {
|
|
2897
|
+
return { ...EMPTY2 };
|
|
3098
2898
|
}
|
|
3099
|
-
function
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
2899
|
+
function sliceRunLogOutput(output, tailChars = DEFAULT_RUN_LOG_TAIL_CHARS) {
|
|
2900
|
+
const max = Math.max(1, tailChars);
|
|
2901
|
+
const outputChars = output.length;
|
|
2902
|
+
if (outputChars <= max) return { output, truncated: false, outputChars };
|
|
2903
|
+
const cut = output.slice(outputChars - max);
|
|
2904
|
+
const nl = cut.indexOf("\n");
|
|
2905
|
+
const text = nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
|
|
2906
|
+
return { output: text, truncated: true, outputChars };
|
|
3106
2907
|
}
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
if (
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
2908
|
+
var PERSIST_DEBOUNCE_MS2 = 500;
|
|
2909
|
+
function persistNow2(key) {
|
|
2910
|
+
const timer = persistTimers2.get(key);
|
|
2911
|
+
if (timer) {
|
|
2912
|
+
clearTimeout(timer);
|
|
2913
|
+
persistTimers2.delete(key);
|
|
2914
|
+
}
|
|
2915
|
+
const mem = memory2.get(key);
|
|
2916
|
+
if (!mem) return;
|
|
2917
|
+
try {
|
|
2918
|
+
writeFileSync5(runLogPath(key), `${JSON.stringify(materialize2(mem))}
|
|
2919
|
+
`, "utf8");
|
|
2920
|
+
} catch {
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
function schedulePersist2(key) {
|
|
2924
|
+
if (persistTimers2.has(key)) return;
|
|
2925
|
+
persistTimers2.set(
|
|
2926
|
+
key,
|
|
2927
|
+
setTimeout(() => {
|
|
2928
|
+
persistTimers2.delete(key);
|
|
2929
|
+
persistNow2(key);
|
|
2930
|
+
}, PERSIST_DEBOUNCE_MS2)
|
|
2931
|
+
);
|
|
2932
|
+
}
|
|
2933
|
+
function loadFromDisk2(key) {
|
|
2934
|
+
const path = runLogPath(key);
|
|
2935
|
+
if (!existsSync6(path)) return null;
|
|
2936
|
+
try {
|
|
2937
|
+
const raw = JSON.parse(readFileSync6(path, "utf8"));
|
|
3114
2938
|
return {
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
2939
|
+
output: typeof raw.output === "string" ? raw.output : "",
|
|
2940
|
+
running: Boolean(raw.running),
|
|
2941
|
+
exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
|
|
2942
|
+
scriptName: typeof raw.scriptName === "string" ? raw.scriptName : null
|
|
3118
2943
|
};
|
|
2944
|
+
} catch {
|
|
2945
|
+
return null;
|
|
3119
2946
|
}
|
|
3120
|
-
|
|
3121
|
-
|
|
2947
|
+
}
|
|
2948
|
+
function memFor2(key) {
|
|
2949
|
+
const cached = memory2.get(key);
|
|
2950
|
+
if (cached) return cached;
|
|
2951
|
+
const mem = fromSnapshot2(loadFromDisk2(key) ?? emptyRunLog());
|
|
2952
|
+
memory2.set(key, mem);
|
|
2953
|
+
return mem;
|
|
2954
|
+
}
|
|
2955
|
+
function readRunLog(key) {
|
|
2956
|
+
const cached = memory2.get(key);
|
|
2957
|
+
if (cached) return materialize2(cached);
|
|
2958
|
+
const disk = loadFromDisk2(key);
|
|
2959
|
+
if (disk) {
|
|
2960
|
+
memory2.set(key, fromSnapshot2(disk));
|
|
2961
|
+
return disk;
|
|
3122
2962
|
}
|
|
3123
|
-
return
|
|
2963
|
+
return emptyRunLog();
|
|
3124
2964
|
}
|
|
3125
|
-
function
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
2965
|
+
function beginRunLog(key, scriptName) {
|
|
2966
|
+
const mem = {
|
|
2967
|
+
chunks: [],
|
|
2968
|
+
length: 0,
|
|
2969
|
+
running: true,
|
|
2970
|
+
exitCode: null,
|
|
2971
|
+
scriptName
|
|
2972
|
+
};
|
|
2973
|
+
memory2.set(key, mem);
|
|
2974
|
+
persistNow2(key);
|
|
2975
|
+
return materialize2(mem);
|
|
3129
2976
|
}
|
|
3130
|
-
function
|
|
3131
|
-
const
|
|
3132
|
-
|
|
2977
|
+
function appendRunLog(key, line) {
|
|
2978
|
+
const mem = memFor2(key);
|
|
2979
|
+
mem.chunks.push(line);
|
|
2980
|
+
mem.length += line.length + 1;
|
|
2981
|
+
mem.running = true;
|
|
2982
|
+
if (mem.length > MAX_RUN_LOG_CHARS * 2) materialize2(mem);
|
|
2983
|
+
schedulePersist2(key);
|
|
3133
2984
|
}
|
|
3134
|
-
function
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
2985
|
+
function finishRunLog(key, exitCode) {
|
|
2986
|
+
const mem = memFor2(key);
|
|
2987
|
+
mem.running = false;
|
|
2988
|
+
mem.exitCode = exitCode;
|
|
2989
|
+
persistNow2(key);
|
|
2990
|
+
return materialize2(mem);
|
|
3139
2991
|
}
|
|
3140
|
-
function
|
|
3141
|
-
|
|
3142
|
-
const cursor = readIntFile(cursorFile) ?? 0;
|
|
3143
|
-
const start = Math.min(Math.max(0, cursor), lines.length);
|
|
3144
|
-
return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
|
|
2992
|
+
function flushRunLog(key) {
|
|
2993
|
+
persistNow2(key);
|
|
3145
2994
|
}
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
exitCode: readIntFile(join11(dir, "exit")),
|
|
3153
|
-
log: join11(dir, "log"),
|
|
3154
|
-
cursor: join11(dir, "present.cursor"),
|
|
3155
|
-
progress: tailProgress(join11(dir, "log"))
|
|
3156
|
-
};
|
|
2995
|
+
|
|
2996
|
+
// src/git/orphan-cleanup.ts
|
|
2997
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, statSync } from "fs";
|
|
2998
|
+
import { join as join9 } from "path";
|
|
2999
|
+
function isSideboardWorktreePath(path) {
|
|
3000
|
+
return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
|
|
3157
3001
|
}
|
|
3158
|
-
function
|
|
3159
|
-
const
|
|
3160
|
-
const
|
|
3161
|
-
const
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3002
|
+
async function findOrphanWorktrees(repoPaths) {
|
|
3003
|
+
const threads = listThreads({ includeArchived: true });
|
|
3004
|
+
const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
|
|
3005
|
+
const repos = new Set(
|
|
3006
|
+
repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
|
|
3007
|
+
);
|
|
3008
|
+
const homeRoot = sideboardWorkspacesDir();
|
|
3009
|
+
if (existsSync7(homeRoot)) {
|
|
3010
|
+
try {
|
|
3011
|
+
for (const entry of readdirSync3(homeRoot, { withFileTypes: true })) {
|
|
3012
|
+
if (!entry.isDirectory()) continue;
|
|
3013
|
+
void entry;
|
|
3014
|
+
}
|
|
3015
|
+
} catch {
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
const orphans = [];
|
|
3019
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3020
|
+
for (const repoPath of repos) {
|
|
3021
|
+
if (!repoPath || !existsSync7(repoPath)) continue;
|
|
3022
|
+
try {
|
|
3023
|
+
const wts = await listWorktrees(repoPath);
|
|
3024
|
+
for (const wt of wts) {
|
|
3025
|
+
const path = wt.path.replace(/\/$/, "");
|
|
3026
|
+
if (!isSideboardWorktreePath(path)) continue;
|
|
3027
|
+
if (known.has(path)) continue;
|
|
3028
|
+
if (seen.has(path)) continue;
|
|
3029
|
+
seen.add(path);
|
|
3030
|
+
let mtimeMs = 0;
|
|
3031
|
+
try {
|
|
3032
|
+
mtimeMs = statSync(path).mtimeMs;
|
|
3033
|
+
} catch {
|
|
3034
|
+
mtimeMs = 0;
|
|
3035
|
+
}
|
|
3036
|
+
orphans.push({ path, repoPath, mtimeMs });
|
|
3037
|
+
}
|
|
3038
|
+
} catch {
|
|
3039
|
+
}
|
|
3040
|
+
try {
|
|
3041
|
+
const root = worktreesRoot(repoPath);
|
|
3042
|
+
if (existsSync7(root)) {
|
|
3043
|
+
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
3044
|
+
if (!entry.isDirectory()) continue;
|
|
3045
|
+
const path = join9(root, entry.name).replace(/\/$/, "");
|
|
3046
|
+
if (known.has(path) || seen.has(path)) continue;
|
|
3047
|
+
if (!existsSync7(join9(path, ".git"))) continue;
|
|
3048
|
+
seen.add(path);
|
|
3049
|
+
let mtimeMs = 0;
|
|
3050
|
+
try {
|
|
3051
|
+
mtimeMs = statSync(path).mtimeMs;
|
|
3052
|
+
} catch {
|
|
3053
|
+
mtimeMs = Date.now();
|
|
3054
|
+
}
|
|
3055
|
+
orphans.push({ path, repoPath, mtimeMs });
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
} catch {
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
3062
|
+
}
|
|
3063
|
+
async function cleanupOrphanWorktrees(opts) {
|
|
3064
|
+
const settings = loadAppSettings();
|
|
3065
|
+
const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
|
|
3066
|
+
const orphans = await findOrphanWorktrees(opts?.repoPaths);
|
|
3067
|
+
const activeCount = listThreads().filter((t) => t.status !== "archived").length;
|
|
3068
|
+
const budget = Math.max(0, maxCount - activeCount);
|
|
3069
|
+
const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
3070
|
+
const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
|
|
3071
|
+
const removed = [];
|
|
3072
|
+
const kept = [];
|
|
3073
|
+
for (const orphan of orphans) {
|
|
3074
|
+
if (keep.has(orphan.path)) {
|
|
3075
|
+
kept.push(orphan.path);
|
|
3076
|
+
continue;
|
|
3077
|
+
}
|
|
3078
|
+
if (opts?.dryRun) {
|
|
3079
|
+
removed.push(orphan.path);
|
|
3080
|
+
continue;
|
|
3081
|
+
}
|
|
3082
|
+
try {
|
|
3083
|
+
await removeWorktree(orphan.repoPath, orphan.path);
|
|
3084
|
+
removed.push(orphan.path);
|
|
3085
|
+
} catch {
|
|
3086
|
+
kept.push(orphan.path);
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
if (!opts?.dryRun) {
|
|
3090
|
+
updateAdvancedSettings({
|
|
3091
|
+
worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
return { removed, kept, orphans };
|
|
3095
|
+
}
|
|
3096
|
+
function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
|
|
3097
|
+
const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
|
|
3098
|
+
const last = settings.advanced.worktreeLastCleanupAt;
|
|
3099
|
+
if (!last) return true;
|
|
3100
|
+
const elapsed = Date.now() - Date.parse(last);
|
|
3101
|
+
return elapsed >= intervalHours * 36e5;
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
// src/store/history-cleanup.ts
|
|
3105
|
+
function stripArchivedTranscript(thread) {
|
|
3106
|
+
writeThread(
|
|
3107
|
+
{
|
|
3108
|
+
...thread,
|
|
3109
|
+
attachments: [],
|
|
3110
|
+
pendingTurnAttachments: [],
|
|
3111
|
+
queueAttachments: [],
|
|
3112
|
+
compactedMessages: void 0,
|
|
3113
|
+
messages: [historyRetentionStubMessage(thread)]
|
|
3114
|
+
},
|
|
3115
|
+
{ touch: false }
|
|
3116
|
+
);
|
|
3117
|
+
}
|
|
3118
|
+
function cleanupArchivedHistory(opts = {}) {
|
|
3119
|
+
const settings = loadAppSettings();
|
|
3120
|
+
if (!opts.force && opts.purgeOlderThanDays == null && !autoCleanupHistoryEnabled(settings)) {
|
|
3121
|
+
return { stripped: [], purged: [] };
|
|
3122
|
+
}
|
|
3123
|
+
const archived = listThreads({ includeArchived: true }).filter((t) => t.status === "archived");
|
|
3124
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
3125
|
+
const plan = opts.purgeOlderThanDays != null ? { stripIds: [], purgeIds: planHistoryAgePurge(archived, opts.purgeOlderThanDays, nowMs) } : planHistoryRetention(archived, {
|
|
3126
|
+
maxCount: opts.maxCount ?? historyMaxCount(settings),
|
|
3127
|
+
maxDays: opts.maxDays ?? historyMaxDays(settings),
|
|
3128
|
+
nowMs
|
|
3129
|
+
});
|
|
3130
|
+
const stripped = [];
|
|
3131
|
+
const purged = [];
|
|
3132
|
+
if (opts.dryRun) {
|
|
3133
|
+
return { stripped: plan.stripIds, purged: plan.purgeIds };
|
|
3134
|
+
}
|
|
3135
|
+
for (const id of plan.stripIds) {
|
|
3136
|
+
const thread = readThread(id);
|
|
3137
|
+
if (!thread || thread.status !== "archived") continue;
|
|
3138
|
+
try {
|
|
3139
|
+
stripArchivedTranscript(thread);
|
|
3140
|
+
stripped.push(id);
|
|
3141
|
+
} catch {
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
for (const id of plan.purgeIds) {
|
|
3145
|
+
const thread = readThread(id);
|
|
3146
|
+
if (!thread || thread.status !== "archived") continue;
|
|
3147
|
+
try {
|
|
3148
|
+
deleteThreadRecord(id);
|
|
3149
|
+
purged.push(id);
|
|
3150
|
+
} catch {
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
if (stripped.length > 0 || purged.length > 0) {
|
|
3154
|
+
updateAdvancedSettings({
|
|
3155
|
+
historyLastCleanupAt: new Date(nowMs).toISOString()
|
|
3156
|
+
});
|
|
3157
|
+
}
|
|
3158
|
+
return { stripped, purged };
|
|
3159
|
+
}
|
|
3160
|
+
function shouldRunHistoryCleanup(settings = loadAppSettings()) {
|
|
3161
|
+
if (!autoCleanupHistoryEnabled(settings)) return false;
|
|
3162
|
+
const intervalHours = settings.advanced.historyCleanupIntervalHours ?? HISTORY_CLEANUP_INTERVAL_HOURS_DEFAULT;
|
|
3163
|
+
const last = settings.advanced.historyLastCleanupAt;
|
|
3164
|
+
if (!last) return true;
|
|
3165
|
+
const elapsed = Date.now() - Date.parse(last);
|
|
3166
|
+
return Number.isFinite(elapsed) && elapsed >= intervalHours * 36e5;
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
// src/git/apply-into-main.ts
|
|
3170
|
+
import { execa as execa3 } from "execa";
|
|
3171
|
+
async function applyThreadIntoMain(thread, opts) {
|
|
3172
|
+
const method = opts?.method ?? "merge";
|
|
3173
|
+
const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
|
|
3174
|
+
const status = await execa3("git", ["status", "--porcelain"], {
|
|
3175
|
+
cwd: thread.repoPath,
|
|
3176
|
+
reject: false
|
|
3177
|
+
});
|
|
3178
|
+
if (status.stdout.trim()) {
|
|
3179
|
+
throw new Error(
|
|
3180
|
+
`Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
|
|
3181
|
+
);
|
|
3182
|
+
}
|
|
3183
|
+
const checkout = await execa3("git", ["checkout", target], {
|
|
3184
|
+
cwd: thread.repoPath,
|
|
3185
|
+
reject: false
|
|
3186
|
+
});
|
|
3187
|
+
if (checkout.exitCode !== 0) {
|
|
3188
|
+
throw new Error(
|
|
3189
|
+
`Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
|
|
3190
|
+
);
|
|
3191
|
+
}
|
|
3192
|
+
if (method === "cherry-pick") {
|
|
3193
|
+
const log = await execa3(
|
|
3194
|
+
"git",
|
|
3195
|
+
["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
|
|
3196
|
+
{ cwd: thread.repoPath, reject: false }
|
|
3197
|
+
);
|
|
3198
|
+
const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
3199
|
+
if (!shas.length) {
|
|
3200
|
+
return {
|
|
3201
|
+
applied: false,
|
|
3202
|
+
method,
|
|
3203
|
+
targetBranch: target,
|
|
3204
|
+
message: `No commits on ${thread.branchName} not already in ${target}`
|
|
3205
|
+
};
|
|
3206
|
+
}
|
|
3207
|
+
for (const sha of shas) {
|
|
3208
|
+
const cp = await execa3("git", ["cherry-pick", sha], {
|
|
3209
|
+
cwd: thread.repoPath,
|
|
3210
|
+
reject: false
|
|
3211
|
+
});
|
|
3212
|
+
if (cp.exitCode !== 0) {
|
|
3213
|
+
await execa3("git", ["cherry-pick", "--abort"], {
|
|
3214
|
+
cwd: thread.repoPath,
|
|
3215
|
+
reject: false
|
|
3216
|
+
});
|
|
3217
|
+
throw new Error(
|
|
3218
|
+
`Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
|
|
3219
|
+
);
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
return {
|
|
3223
|
+
applied: true,
|
|
3224
|
+
method,
|
|
3225
|
+
targetBranch: target,
|
|
3226
|
+
message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
|
|
3227
|
+
};
|
|
3228
|
+
}
|
|
3229
|
+
const merge = await execa3(
|
|
3230
|
+
"git",
|
|
3231
|
+
["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
|
|
3232
|
+
{ cwd: thread.repoPath, reject: false }
|
|
3233
|
+
);
|
|
3234
|
+
if (merge.exitCode !== 0) {
|
|
3235
|
+
await execa3("git", ["merge", "--abort"], {
|
|
3236
|
+
cwd: thread.repoPath,
|
|
3237
|
+
reject: false
|
|
3238
|
+
});
|
|
3239
|
+
throw new Error(
|
|
3240
|
+
`Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
|
|
3241
|
+
);
|
|
3168
3242
|
}
|
|
3169
|
-
const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
|
|
3170
|
-
const progress = extra?.progress ?? snap.progress;
|
|
3171
3243
|
return {
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
id,
|
|
3177
|
-
pid: snap.pid,
|
|
3178
|
-
exitCode: snap.exitCode ?? void 0,
|
|
3179
|
-
phase: lastProgressLine(progress),
|
|
3180
|
-
delta,
|
|
3181
|
-
progress,
|
|
3182
|
-
hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
|
|
3244
|
+
applied: true,
|
|
3245
|
+
method: "merge",
|
|
3246
|
+
targetBranch: target,
|
|
3247
|
+
message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
|
|
3183
3248
|
};
|
|
3184
3249
|
}
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
}
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
const
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
if (!
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
ok: false,
|
|
3198
|
-
failed: true,
|
|
3199
|
-
status: "failed",
|
|
3200
|
-
id: jobId,
|
|
3201
|
-
delta: "",
|
|
3202
|
-
progress: "No detached job. Start one first.",
|
|
3203
|
-
hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
|
|
3204
|
-
};
|
|
3250
|
+
|
|
3251
|
+
// src/git/clone-repo.ts
|
|
3252
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3253
|
+
import { basename as basename2, join as join10 } from "path";
|
|
3254
|
+
import { execa as execa4 } from "execa";
|
|
3255
|
+
async function cloneRepoIntoSideboard(opts) {
|
|
3256
|
+
const url = opts.url.trim();
|
|
3257
|
+
if (!url) throw new Error("Clone URL is required");
|
|
3258
|
+
let name = opts.name?.trim();
|
|
3259
|
+
if (!name) {
|
|
3260
|
+
const leaf = basename2(url.replace(/\/$/, "").replace(/\.git$/, ""));
|
|
3261
|
+
name = leaf || "repo";
|
|
3205
3262
|
}
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
if (
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
progress: "No detached job. Start one first."
|
|
3213
|
-
});
|
|
3263
|
+
name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
3264
|
+
const dest = join10(sideboardReposDir(), name);
|
|
3265
|
+
if (existsSync8(dest)) {
|
|
3266
|
+
const repoPath2 = await resolveRepoRoot(dest);
|
|
3267
|
+
const workspace2 = await ensureWorkspace(repoPath2);
|
|
3268
|
+
return { repoPath: repoPath2, workspace: workspace2 };
|
|
3214
3269
|
}
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3270
|
+
const result = await execa4("git", ["clone", url, dest], { reject: false });
|
|
3271
|
+
if (result.exitCode !== 0) {
|
|
3272
|
+
throw new Error(
|
|
3273
|
+
`git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
|
|
3274
|
+
);
|
|
3220
3275
|
}
|
|
3221
|
-
|
|
3222
|
-
|
|
3276
|
+
const repoPath = await resolveRepoRoot(dest);
|
|
3277
|
+
const workspace = await ensureWorkspace(repoPath);
|
|
3278
|
+
return { repoPath, workspace };
|
|
3223
3279
|
}
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3280
|
+
|
|
3281
|
+
// src/types/thread.ts
|
|
3282
|
+
function isAdoptableRunScriptRequest(req) {
|
|
3283
|
+
return Boolean(req && !req.fulfilledAt && !req.error);
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
// src/store/desktop-host.ts
|
|
3287
|
+
import { readFileSync as readFileSync7, unlinkSync, writeFileSync as writeFileSync6 } from "fs";
|
|
3288
|
+
import { join as join11 } from "path";
|
|
3289
|
+
function desktopHostPidPath() {
|
|
3290
|
+
return join11(appDataDir(), "desktop-host.pid");
|
|
3291
|
+
}
|
|
3292
|
+
function readDesktopHostPid() {
|
|
3233
3293
|
try {
|
|
3234
|
-
|
|
3294
|
+
const pid = Number.parseInt(readFileSync7(desktopHostPidPath(), "utf8").trim(), 10);
|
|
3295
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
3296
|
+
return pid;
|
|
3235
3297
|
} catch {
|
|
3298
|
+
return null;
|
|
3236
3299
|
}
|
|
3237
3300
|
}
|
|
3238
|
-
function
|
|
3301
|
+
function pidAlive(pid) {
|
|
3239
3302
|
try {
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
`;
|
|
3243
|
-
appendFileSync2(logFile, body);
|
|
3303
|
+
process.kill(pid, 0);
|
|
3304
|
+
return true;
|
|
3244
3305
|
} catch {
|
|
3306
|
+
return false;
|
|
3245
3307
|
}
|
|
3246
3308
|
}
|
|
3247
|
-
|
|
3248
|
-
const
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
}
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
if (
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
const
|
|
3290
|
-
if (!
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
const stopped = !stillRunning;
|
|
3300
|
-
return {
|
|
3301
|
-
...result,
|
|
3302
|
-
stillRunning,
|
|
3303
|
-
ok: false,
|
|
3304
|
-
failed: true,
|
|
3305
|
-
status: stillRunning ? "running" : "failed",
|
|
3306
|
-
stopped,
|
|
3307
|
-
reason: stopped ? "stopped" : "still-running",
|
|
3308
|
-
stopReason: why || void 0,
|
|
3309
|
-
hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
|
|
3310
|
-
};
|
|
3309
|
+
function isDesktopHostAlive() {
|
|
3310
|
+
const pid = readDesktopHostPid();
|
|
3311
|
+
return pid != null && pidAlive(pid);
|
|
3312
|
+
}
|
|
3313
|
+
function isThisProcessDesktopHost() {
|
|
3314
|
+
return readDesktopHostPid() === process.pid && pidAlive(process.pid);
|
|
3315
|
+
}
|
|
3316
|
+
function thisProcessShouldDrainAgentQueues() {
|
|
3317
|
+
if (isThisProcessDesktopHost()) return true;
|
|
3318
|
+
return !isDesktopHostAlive();
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
// src/orchestrator/child-halt.ts
|
|
3322
|
+
var HALT_STATUSES = /* @__PURE__ */ new Set(["stopped", "error", "broken"]);
|
|
3323
|
+
function isIncompleteChildStatus(status) {
|
|
3324
|
+
return HALT_STATUSES.has(status);
|
|
3325
|
+
}
|
|
3326
|
+
function childHaltNotice(child, status) {
|
|
3327
|
+
const title = child.title?.trim() || "Untitled";
|
|
3328
|
+
const link = `[${title}](sideboard://thread/${child.id})`;
|
|
3329
|
+
const why = child.lastError?.trim();
|
|
3330
|
+
const extra = why ? ` lastError: ${why}` : "";
|
|
3331
|
+
return [
|
|
3332
|
+
`Sideboard: child worktree ${link} ${status} before finishing (status=${status}).${extra}`,
|
|
3333
|
+
"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."
|
|
3334
|
+
].join("\n");
|
|
3335
|
+
}
|
|
3336
|
+
function shouldNotifyParentAfterTurnError(opts) {
|
|
3337
|
+
return !opts.quotaFailoverHandled && !opts.crashContinued;
|
|
3338
|
+
}
|
|
3339
|
+
function shouldNotifyParentOfChildHalt(opts) {
|
|
3340
|
+
if (!isIncompleteChildStatus(opts.status)) return false;
|
|
3341
|
+
if (!opts.child.parentThreadId) return false;
|
|
3342
|
+
if (!opts.parent || opts.parent.status === "archived") return false;
|
|
3343
|
+
if (opts.parent.id === opts.child.id) return false;
|
|
3344
|
+
return isOrchestratorThread(opts.parent);
|
|
3345
|
+
}
|
|
3346
|
+
var notified = /* @__PURE__ */ new Set();
|
|
3347
|
+
function noticeKey(childId, status) {
|
|
3348
|
+
return `${childId}:${status}`;
|
|
3349
|
+
}
|
|
3350
|
+
function notifyParentOfChildHalt(child, status, send) {
|
|
3351
|
+
const parent = child.parentThreadId ? readThread(child.parentThreadId) : null;
|
|
3352
|
+
if (!shouldNotifyParentOfChildHalt({ child, parent, status })) return false;
|
|
3353
|
+
const key = noticeKey(child.id, status);
|
|
3354
|
+
if (notified.has(key)) return false;
|
|
3355
|
+
notified.add(key);
|
|
3356
|
+
const parentId = parent.id;
|
|
3357
|
+
void send(parentId, childHaltNotice(child, status)).catch(() => {
|
|
3358
|
+
notified.delete(key);
|
|
3359
|
+
});
|
|
3360
|
+
return true;
|
|
3311
3361
|
}
|
|
3312
3362
|
|
|
3313
3363
|
// src/threads/create.ts
|
|
@@ -3881,7 +3931,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
3881
3931
|
});
|
|
3882
3932
|
await requireAgent(resolved.agent);
|
|
3883
3933
|
if (input.cowboy) {
|
|
3884
|
-
const { cowboyModeEnabled } = await import("./app-settings-
|
|
3934
|
+
const { cowboyModeEnabled } = await import("./app-settings-652J57CX.js");
|
|
3885
3935
|
if (!cowboyModeEnabled()) {
|
|
3886
3936
|
throw new Error(
|
|
3887
3937
|
"Cowboy mode is off. Enable it in Settings \u2192 Advanced."
|
|
@@ -3939,7 +3989,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
3939
3989
|
let sourceIsFork = false;
|
|
3940
3990
|
if (!input.cowboy && sourceType === "branch" && (!sourceRef || sourceRef === "HEAD" || sourceRef === "default")) {
|
|
3941
3991
|
if (isAbleTimeConnected() && getIssueSource() === "abletime") {
|
|
3942
|
-
const { ensureAbleTimeTask, issueAttachmentForAbleTimeTask } = await import("./abletime-
|
|
3992
|
+
const { ensureAbleTimeTask, issueAttachmentForAbleTimeTask } = await import("./abletime-OI6FNSXX.js");
|
|
3943
3993
|
const task = await ensureAbleTimeTask({
|
|
3944
3994
|
title: title?.trim() || "Untitled work",
|
|
3945
3995
|
description: input.prompt?.trim() || void 0
|
|
@@ -4502,7 +4552,7 @@ async function adoptThread(input) {
|
|
|
4502
4552
|
messages: input.messages ?? []
|
|
4503
4553
|
});
|
|
4504
4554
|
writeThread(thread);
|
|
4505
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
4555
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-OIBSD5B6.js");
|
|
4506
4556
|
await ensureWorkspace2(repoPath);
|
|
4507
4557
|
const { ensureWorktreeSideboardIgnored: ensureWorktreeSideboardIgnored2 } = await import("./worktree-exclude-5N2KTYIT.js");
|
|
4508
4558
|
await ensureWorktreeSideboardIgnored2(input.worktreePath);
|
|
@@ -7008,7 +7058,7 @@ function formatScheduledPrompt(name, prompt) {
|
|
|
7008
7058
|
${prompt}`;
|
|
7009
7059
|
}
|
|
7010
7060
|
async function defaultDeps() {
|
|
7011
|
-
const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-
|
|
7061
|
+
const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-3IOISDFS.js");
|
|
7012
7062
|
const orch = getOrchestrator2();
|
|
7013
7063
|
return {
|
|
7014
7064
|
findThread: (id) => findThreadByRef(id),
|
|
@@ -7333,7 +7383,7 @@ var Orchestrator = class {
|
|
|
7333
7383
|
orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
|
|
7334
7384
|
});
|
|
7335
7385
|
}
|
|
7336
|
-
const { autoCleanupOrphansEnabled } = await import("./app-settings-
|
|
7386
|
+
const { autoCleanupOrphansEnabled } = await import("./app-settings-652J57CX.js");
|
|
7337
7387
|
if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
|
|
7338
7388
|
await cleanupOrphanWorktrees({ repoPaths });
|
|
7339
7389
|
}
|
|
@@ -7680,7 +7730,7 @@ var Orchestrator = class {
|
|
|
7680
7730
|
}
|
|
7681
7731
|
await setup;
|
|
7682
7732
|
if (skipSetup) return;
|
|
7683
|
-
const { autoRunAfterSetupEnabled } = await import("./app-settings-
|
|
7733
|
+
const { autoRunAfterSetupEnabled } = await import("./app-settings-652J57CX.js");
|
|
7684
7734
|
if (autoRunAfterSetupEnabled()) {
|
|
7685
7735
|
try {
|
|
7686
7736
|
await this.startDev(threadId);
|
|
@@ -7931,7 +7981,8 @@ var Orchestrator = class {
|
|
|
7931
7981
|
if (compact.didCompact) {
|
|
7932
7982
|
thread = updateThread(threadId, {
|
|
7933
7983
|
messages: compact.thread.messages,
|
|
7934
|
-
sessionId: compact.thread.sessionId
|
|
7984
|
+
sessionId: compact.thread.sessionId,
|
|
7985
|
+
compactedMessages: compact.thread.compactedMessages
|
|
7935
7986
|
});
|
|
7936
7987
|
this.emit({
|
|
7937
7988
|
type: "context_compacted",
|
|
@@ -7945,9 +7996,6 @@ var Orchestrator = class {
|
|
|
7945
7996
|
this.emit({ type: "error", threadId, message: `context compact failed: ${message}` });
|
|
7946
7997
|
thread = this.requireThread(threadId);
|
|
7947
7998
|
}
|
|
7948
|
-
if (this.stoppedTurns.has(threadId)) {
|
|
7949
|
-
return;
|
|
7950
|
-
}
|
|
7951
7999
|
const promptText = typeof prompt === "string" ? prompt : "";
|
|
7952
8000
|
const autoContinue = isJobContinuePrompt(promptText) || promptText.startsWith("The previous agent process ended before it finished.");
|
|
7953
8001
|
const parked = takePendingTurnAttachments(thread);
|
|
@@ -7964,6 +8012,14 @@ var Orchestrator = class {
|
|
|
7964
8012
|
agent: thread.agent,
|
|
7965
8013
|
orch: isOrchestratorThread(thread)
|
|
7966
8014
|
});
|
|
8015
|
+
if (this.stoppedTurns.has(threadId)) {
|
|
8016
|
+
const stopped = writeLiveStatus(threadId, "stopped");
|
|
8017
|
+
if (stopped?.status === "stopped") {
|
|
8018
|
+
this.emit({ type: "status_changed", threadId, status: "stopped" });
|
|
8019
|
+
}
|
|
8020
|
+
this.emit({ type: "turn_finished", threadId, exitCode: 1 });
|
|
8021
|
+
return;
|
|
8022
|
+
}
|
|
7967
8023
|
thread = this.requireThread(threadId);
|
|
7968
8024
|
const gitPrompt = expandCanonicalGitRequest(promptText);
|
|
7969
8025
|
const { agentPrompt: expandedPrompt } = expandComposerPrompt(
|
|
@@ -8022,7 +8078,7 @@ var Orchestrator = class {
|
|
|
8022
8078
|
].filter(Boolean).join("\n\n") || void 0;
|
|
8023
8079
|
const prGateDirective = thread.agent !== "brightsy" && !isOrchestratorThread(thread) && mentionsPrGoal(promptText) ? formatPrGateDirective() : null;
|
|
8024
8080
|
const agentPrompt = [
|
|
8025
|
-
thread.planMode
|
|
8081
|
+
planModeTurnInstruction(thread.planMode),
|
|
8026
8082
|
orchestrationReminder,
|
|
8027
8083
|
prGateDirective,
|
|
8028
8084
|
injectedNoticeContext,
|
|
@@ -8044,7 +8100,7 @@ var Orchestrator = class {
|
|
|
8044
8100
|
}
|
|
8045
8101
|
const isBrightsy = fresh.agent === "brightsy";
|
|
8046
8102
|
const isOrchestration = isOrchestratorThread(fresh);
|
|
8047
|
-
const { autoRenameBranchEnabled, getGithubGitAuthMode, gitBranchPrefixSetting } = await import("./app-settings-
|
|
8103
|
+
const { autoRenameBranchEnabled, getGithubGitAuthMode, gitBranchPrefixSetting } = await import("./app-settings-652J57CX.js");
|
|
8048
8104
|
const gitAuthMode = getGithubGitAuthMode();
|
|
8049
8105
|
const worktreeDirective = isBrightsy || isOrchestration ? null : formatWorktreeDirective(fresh, {
|
|
8050
8106
|
githubSlug: await withTimeout(
|
|
@@ -8525,6 +8581,9 @@ var Orchestrator = class {
|
|
|
8525
8581
|
}
|
|
8526
8582
|
const preferredPorts = active?.ports?.length ? active.ports : active?.port != null ? [active.port] : void 0;
|
|
8527
8583
|
if (active && !existing) {
|
|
8584
|
+
if (typeof active.pid === "number") {
|
|
8585
|
+
killDetachedRunTree(active.pid, thread.worktreePath);
|
|
8586
|
+
}
|
|
8528
8587
|
killListenersOnPorts(collectActiveRunPorts([active]));
|
|
8529
8588
|
this.syncWorktreeRuns(
|
|
8530
8589
|
thread.worktreePath,
|
|
@@ -8597,7 +8656,8 @@ var Orchestrator = class {
|
|
|
8597
8656
|
scriptName: resolvedName,
|
|
8598
8657
|
port: handle.port,
|
|
8599
8658
|
ports: handle.ports,
|
|
8600
|
-
startedAt
|
|
8659
|
+
startedAt,
|
|
8660
|
+
...typeof handle.pid === "number" && handle.pid > 1 ? { pid: handle.pid } : {}
|
|
8601
8661
|
};
|
|
8602
8662
|
const nextRuns = [
|
|
8603
8663
|
...latestShared.activeRuns.filter((r) => r.scriptName !== resolvedName),
|
|
@@ -8743,6 +8803,9 @@ var Orchestrator = class {
|
|
|
8743
8803
|
}
|
|
8744
8804
|
const run2 = shared.activeRuns.find((r) => r.scriptName === scriptName);
|
|
8745
8805
|
if (!hadHandle2 && run2) {
|
|
8806
|
+
if (typeof run2.pid === "number") {
|
|
8807
|
+
killDetachedRunTree(run2.pid, thread.worktreePath);
|
|
8808
|
+
}
|
|
8746
8809
|
killListenersOnPorts(collectActiveRunPorts([run2]));
|
|
8747
8810
|
}
|
|
8748
8811
|
const remaining = shared.activeRuns.filter((r) => r.scriptName !== scriptName);
|
|
@@ -8764,6 +8827,11 @@ var Orchestrator = class {
|
|
|
8764
8827
|
}
|
|
8765
8828
|
}
|
|
8766
8829
|
if (!hadHandle && shared.activeRuns.length > 0) {
|
|
8830
|
+
for (const run2 of shared.activeRuns) {
|
|
8831
|
+
if (typeof run2.pid === "number") {
|
|
8832
|
+
killDetachedRunTree(run2.pid, thread.worktreePath);
|
|
8833
|
+
}
|
|
8834
|
+
}
|
|
8767
8835
|
killListenersOnPorts(collectActiveRunPorts(shared.activeRuns));
|
|
8768
8836
|
}
|
|
8769
8837
|
this.syncWorktreeRuns(thread.worktreePath, [], null);
|
|
@@ -8805,6 +8873,11 @@ var Orchestrator = class {
|
|
|
8805
8873
|
(k) => isWorktreeRunProcessKey(k, thread.worktreePath) || siblings.some((t) => k.startsWith(`${t.id}:run:`) || k === `${t.id}:dev`)
|
|
8806
8874
|
);
|
|
8807
8875
|
if (!force && hasLiveHandle) continue;
|
|
8876
|
+
for (const run2 of shared.activeRuns) {
|
|
8877
|
+
if (typeof run2.pid === "number") {
|
|
8878
|
+
killDetachedRunTree(run2.pid, thread.worktreePath);
|
|
8879
|
+
}
|
|
8880
|
+
}
|
|
8808
8881
|
killListenersOnPorts(collectActiveRunPorts(shared.activeRuns));
|
|
8809
8882
|
if (shared.devPort != null) {
|
|
8810
8883
|
killListenersOnPorts([shared.devPort]);
|
|
@@ -9318,7 +9391,7 @@ var Orchestrator = class {
|
|
|
9318
9391
|
if (!titled.userSetTitle && meta.title && titled.title !== meta.title) {
|
|
9319
9392
|
updateThread(thread.id, { title: meta.title });
|
|
9320
9393
|
}
|
|
9321
|
-
const { autoArchiveOnMergeEnabled } = await import("./app-settings-
|
|
9394
|
+
const { autoArchiveOnMergeEnabled } = await import("./app-settings-652J57CX.js");
|
|
9322
9395
|
const latest = this.requireThread(thread.id);
|
|
9323
9396
|
if (!shouldAutoArchiveOnPrMerge({
|
|
9324
9397
|
previousPrState: prevState || null,
|
|
@@ -9600,7 +9673,7 @@ var Orchestrator = class {
|
|
|
9600
9673
|
this.enforceHistoryRetention();
|
|
9601
9674
|
if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
|
|
9602
9675
|
try {
|
|
9603
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
9676
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-OIBSD5B6.js");
|
|
9604
9677
|
await ensureWorkspace2(thread.repoPath);
|
|
9605
9678
|
} catch {
|
|
9606
9679
|
}
|
|
@@ -9627,7 +9700,7 @@ var Orchestrator = class {
|
|
|
9627
9700
|
await runArchiveScript(thread.repoPath, thread.worktreePath);
|
|
9628
9701
|
} catch {
|
|
9629
9702
|
}
|
|
9630
|
-
const { deleteBranchOnPurgeEnabled } = await import("./app-settings-
|
|
9703
|
+
const { deleteBranchOnPurgeEnabled } = await import("./app-settings-652J57CX.js");
|
|
9631
9704
|
const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
|
|
9632
9705
|
await removeWorktree(thread.repoPath, thread.worktreePath, {
|
|
9633
9706
|
deleteBranch: deleteBranch ? thread.branchName : void 0
|
|
@@ -9654,7 +9727,7 @@ var Orchestrator = class {
|
|
|
9654
9727
|
`Cowboy checkout missing: ${thread.worktreePath}. Re-add the project folder, then restore.`
|
|
9655
9728
|
);
|
|
9656
9729
|
}
|
|
9657
|
-
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-
|
|
9730
|
+
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-5VRZVGP3.js");
|
|
9658
9731
|
const slug = thread.worktreePath.split("/").pop();
|
|
9659
9732
|
const dest = thread.worktreePath;
|
|
9660
9733
|
await withRepoGitLock(thread.repoPath, async () => {
|
|
@@ -9764,7 +9837,7 @@ async function startOrchestration(opts) {
|
|
|
9764
9837
|
sourceRef: "default",
|
|
9765
9838
|
...createOpts
|
|
9766
9839
|
}).catch(async () => {
|
|
9767
|
-
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-
|
|
9840
|
+
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-5VRZVGP3.js");
|
|
9768
9841
|
const repo = await resolveRepoRoot2(repoPath);
|
|
9769
9842
|
const def = await resolveDefaultBranch2(repo);
|
|
9770
9843
|
return orch.createThread({
|
|
@@ -9774,7 +9847,7 @@ async function startOrchestration(opts) {
|
|
|
9774
9847
|
repoPath: repo
|
|
9775
9848
|
});
|
|
9776
9849
|
});
|
|
9777
|
-
const { updateThread: upd } = await import("./thread-store-
|
|
9850
|
+
const { updateThread: upd } = await import("./thread-store-JQB7KZJR.js");
|
|
9778
9851
|
const updated = upd(thread.id, {
|
|
9779
9852
|
sourceType: "orchestration",
|
|
9780
9853
|
sourceRef: goal
|
|
@@ -9790,14 +9863,14 @@ export {
|
|
|
9790
9863
|
needsCoordinatorAction,
|
|
9791
9864
|
AGENT_GIT_ACTIONS,
|
|
9792
9865
|
threadsSharingWorktree,
|
|
9793
|
-
MAX_RUN_LOG_CHARS,
|
|
9794
|
-
DEFAULT_RUN_LOG_TAIL_CHARS,
|
|
9795
|
-
sliceRunLogOutput,
|
|
9796
9866
|
mcpWaitForTurnTimeoutMs,
|
|
9797
9867
|
mcpWaitTaskHint,
|
|
9798
9868
|
mcpWaitForJobTimeoutMs,
|
|
9799
9869
|
waitForDetachedJob,
|
|
9800
9870
|
stopDetachedJob,
|
|
9871
|
+
MAX_RUN_LOG_CHARS,
|
|
9872
|
+
DEFAULT_RUN_LOG_TAIL_CHARS,
|
|
9873
|
+
sliceRunLogOutput,
|
|
9801
9874
|
BOARD_COLUMN_DEFS,
|
|
9802
9875
|
HOME_BOARD_CACHE_TTL_MS,
|
|
9803
9876
|
dedupeBoardIssues,
|