@sideboard-ai/core 0.1.242 → 0.1.244
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-PBRIHUYM.js → agents-BQZ2CHXK.js} +9 -5
- package/dist/{agents-CDQCQVWU.js → agents-Q23R4WOR.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-3Q4PVZ3U.js → chunk-2QRU4IFI.js} +32 -5
- package/dist/{chunk-3PGFAVFL.js → chunk-43F44EKX.js} +2 -2
- package/dist/{chunk-LRMJ6V6U.js → chunk-4HMV53AB.js} +1506 -1465
- package/dist/{chunk-OKHOYI53.js → chunk-576S3HJ7.js} +5 -5
- package/dist/{chunk-TB6UMHIF.js → chunk-5ZFULOTC.js} +10 -2
- package/dist/{chunk-3AJUH66R.js → chunk-6I5YQTNK.js} +2 -1
- package/dist/{chunk-NJ5OXOTN.js → chunk-7HOKP4GR.js} +2 -1
- package/dist/{chunk-DXY4K5MP.js → chunk-BL2UGKWU.js} +3 -3
- package/dist/{chunk-CWHMLCLN.js → chunk-C64YQAA6.js} +3 -3
- package/dist/{chunk-QXYWLO6W.js → chunk-CYYQ6577.js} +28 -5
- package/dist/{chunk-O3UAQAYA.js → chunk-E6C5NR46.js} +10 -2
- package/dist/{chunk-CRAXJAYJ.js → chunk-G45QP2ZD.js} +3 -3
- package/dist/{chunk-BILEJG3V.js → chunk-GFRC6JQN.js} +5 -5
- package/dist/{chunk-YXOOSFAF.js → chunk-IMO7AZI4.js} +2 -2
- 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-R3YKDTVE.js → chunk-YJJ2A5CI.js} +1572 -1529
- package/dist/{chunk-QLWMHQPH.js → chunk-YLGBQVRK.js} +1 -0
- package/dist/{coordinator-prompt-5UINTEA6.js → coordinator-prompt-WQIICPT6.js} +4 -4
- package/dist/{coordinator-prompt-ATK73HYU.js → coordinator-prompt-ZBYRREHD.js} +4 -4
- package/dist/{global-workspace-ZJBBICFJ.js → global-workspace-2N6KISDO.js} +4 -4
- package/dist/{global-workspace-TD5SARNW.js → global-workspace-3SQU4GL4.js} +4 -4
- package/dist/index.cjs +590 -470
- package/dist/index.d.cts +50 -3
- package/dist/index.d.ts +50 -3
- package/dist/index.js +73 -39
- package/dist/mcp/run-stdio.cjs +1649 -1552
- package/dist/mcp/run-stdio.js +51 -37
- package/dist/{orchestrator-VYNPNHY4.js → orchestrator-ABF6QFE6.js} +7 -7
- package/dist/{orchestrator-66ZFQHW5.js → orchestrator-ZJ6SYGW7.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-JX5R4V24.js → workspaces-CFI7XVAE.js} +4 -4
- package/dist/{workspaces-4SFSCYKT.js → workspaces-QLKWW3O2.js} +4 -4
- package/dist/{worktree-AGVPKDBH.js → worktree-4UAOZUJ4.js} +5 -3
- package/dist/{worktree-Y5FES5D6.js → worktree-PK3QETXO.js} +5 -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-2QRU4IFI.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-576S3HJ7.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-5ZFULOTC.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-3SQU4GL4.js");
|
|
492
494
|
if (isGlobalThread2(thread)) {
|
|
493
|
-
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-
|
|
495
|
+
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-ZBYRREHD.js");
|
|
494
496
|
ensureGlobalCoordinatorCwd2(
|
|
495
497
|
isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
|
|
496
498
|
);
|
|
@@ -681,9 +683,9 @@ function gitActionExpansion(action, base) {
|
|
|
681
683
|
case "commit-push":
|
|
682
684
|
return "Commit any uncommitted work with a purpose-stating message, then push to origin (`git push -u origin HEAD`; this updates an existing PR when one is linked). Do not start a checks loop unless a goal was given.";
|
|
683
685
|
case "create-draft":
|
|
684
|
-
return "Commit, push, then create a draft PR with `gh pr create --draft -R <origin-owner/name>` (title/body from the change purpose). Update the existing PR instead if one is linked.";
|
|
686
|
+
return "Commit, push, then create a draft PR with `gh pr create --draft --assignee @me -R <origin-owner/name>` (title/body from the change purpose). Update the existing PR instead if one is linked. Use a different `--assignee` only if the user named someone else.";
|
|
685
687
|
case "create-web":
|
|
686
|
-
return "Commit, push, then `gh pr create --web -R <origin-owner/name>`.";
|
|
688
|
+
return "Commit, push, then `gh pr create --web --assignee @me -R <origin-owner/name>`. Use a different `--assignee` only if the user named someone else.";
|
|
687
689
|
case "resolve-conflicts":
|
|
688
690
|
return `Fetch the PR base${base ? ` (\`${base}\`)` : ""}, merge it into this branch, resolve conflicts carefully, then commit and push until the PR is mergeable.`;
|
|
689
691
|
case "ready-for-review":
|
|
@@ -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,1610 +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
|
-
|
|
1734
|
-
|
|
1735
|
-
function
|
|
1736
|
-
const
|
|
1737
|
-
if (
|
|
1738
|
-
|
|
1739
|
-
if (settings?.filesToCopy?.length) return settings.filesToCopy;
|
|
1740
|
-
if (settings?.fileIncludeGlobs?.length) {
|
|
1741
|
-
const matched = [];
|
|
1742
|
-
try {
|
|
1743
|
-
for (const entry of readdirSync(repoPath, { withFileTypes: true })) {
|
|
1744
|
-
if (!entry.isFile()) continue;
|
|
1745
|
-
for (const glob of settings.fileIncludeGlobs) {
|
|
1746
|
-
if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob(basename(glob), entry.name)) {
|
|
1747
|
-
matched.push(entry.name);
|
|
1748
|
-
break;
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
} catch {
|
|
1753
|
-
}
|
|
1754
|
-
if (matched.length) return [...new Set(matched)];
|
|
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 {
|
|
1764
|
-
}
|
|
1765
|
-
if (defaults.length) return defaults;
|
|
1766
|
-
return [".env.local", ".env"];
|
|
1767
|
-
}
|
|
1768
|
-
function copyConfiguredFiles(repoPath, worktreePath) {
|
|
1769
|
-
const patterns = resolveFilesToCopy(repoPath);
|
|
1770
|
-
const copied = [];
|
|
1771
|
-
for (const rel of patterns) {
|
|
1772
|
-
const src = join5(repoPath, rel);
|
|
1773
|
-
if (!existsSync3(src)) continue;
|
|
1774
|
-
const dest = join5(worktreePath, rel);
|
|
1775
|
-
mkdirSync4(dirname3(dest), { recursive: true });
|
|
1776
|
-
copyFileSync(src, dest);
|
|
1777
|
-
copied.push(rel);
|
|
1778
|
-
}
|
|
1779
|
-
return copied;
|
|
1780
|
-
}
|
|
1781
|
-
var cachedLoginEnv = null;
|
|
1782
|
-
async function captureLoginEnv() {
|
|
1783
|
-
if (cachedLoginEnv) return { ...cachedLoginEnv };
|
|
1784
|
-
const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
|
|
1785
|
-
try {
|
|
1786
|
-
const result = await execa2(shell, ["-l", "-c", "env -0"], {
|
|
1787
|
-
reject: false,
|
|
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
|
-
}
|
|
1803
|
-
} catch {
|
|
1804
|
-
}
|
|
1805
|
-
cachedLoginEnv = { ...process.env };
|
|
1806
|
-
return { ...cachedLoginEnv };
|
|
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);
|
|
1807
1769
|
}
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
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
|
-
}
|
|
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;
|
|
1834
1790
|
}
|
|
1835
|
-
applyWorktreePkgCacheEnv(env, opts.worktreePath);
|
|
1836
|
-
return env;
|
|
1837
1791
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
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);
|
|
1842
1804
|
}
|
|
1843
|
-
function
|
|
1844
|
-
const
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
if (!Number.isFinite(pid) || pid <= 0 || pid === selfPid) continue;
|
|
1848
|
-
pids.push(pid);
|
|
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)}`);
|
|
1849
1809
|
}
|
|
1850
|
-
return
|
|
1810
|
+
return s;
|
|
1851
1811
|
}
|
|
1852
|
-
function
|
|
1812
|
+
function jobAlive(pid) {
|
|
1813
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1853
1814
|
try {
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1857
|
-
timeout: 2e3
|
|
1858
|
-
});
|
|
1859
|
-
return killableListenerPids(raw, selfPid);
|
|
1815
|
+
process.kill(pid, 0);
|
|
1816
|
+
return true;
|
|
1860
1817
|
} catch {
|
|
1861
|
-
return
|
|
1818
|
+
return false;
|
|
1862
1819
|
}
|
|
1863
1820
|
}
|
|
1864
|
-
function
|
|
1865
|
-
if (
|
|
1821
|
+
function processGroupAlive(pgid) {
|
|
1822
|
+
if (process.platform === "win32") return false;
|
|
1823
|
+
if (!Number.isInteger(pgid) || pgid <= 1) return false;
|
|
1866
1824
|
try {
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
});
|
|
1872
|
-
for (const line of raw.split("\n")) {
|
|
1873
|
-
if (line.startsWith("n") && line.length > 1) return line.slice(1);
|
|
1874
|
-
}
|
|
1875
|
-
} catch {
|
|
1825
|
+
process.kill(-pgid, 0);
|
|
1826
|
+
return true;
|
|
1827
|
+
} catch (err) {
|
|
1828
|
+
return err && typeof err === "object" && "code" in err && err.code === "EPERM";
|
|
1876
1829
|
}
|
|
1877
|
-
return null;
|
|
1878
1830
|
}
|
|
1879
|
-
function
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
} catch {
|
|
1883
|
-
return resolve(path);
|
|
1884
|
-
}
|
|
1831
|
+
function jobTreeAlive(pid) {
|
|
1832
|
+
if (pid == null) return false;
|
|
1833
|
+
return jobAlive(pid) || processGroupAlive(pid);
|
|
1885
1834
|
}
|
|
1886
|
-
function
|
|
1887
|
-
|
|
1888
|
-
const
|
|
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;
|
|
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;
|
|
1896
1839
|
}
|
|
1897
|
-
function
|
|
1898
|
-
|
|
1899
|
-
process.kill(pid, signal);
|
|
1900
|
-
} catch {
|
|
1901
|
-
}
|
|
1840
|
+
function jobDir(root, id, legacy = false) {
|
|
1841
|
+
return join5(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
|
|
1902
1842
|
}
|
|
1903
|
-
function
|
|
1904
|
-
const
|
|
1905
|
-
|
|
1906
|
-
|
|
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;
|
|
1907
1849
|
}
|
|
1908
|
-
function
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
try {
|
|
1913
|
-
process.kill(-pid, "SIGTERM");
|
|
1914
|
-
} catch {
|
|
1915
|
-
signalPid(pid, "SIGTERM");
|
|
1916
|
-
}
|
|
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);
|
|
1917
1854
|
}
|
|
1918
|
-
function
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
],
|
|
1931
|
-
{ stdio: "ignore", timeout: 3e3 }
|
|
1932
|
-
);
|
|
1933
|
-
continue;
|
|
1934
|
-
}
|
|
1935
|
-
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
1936
|
-
signalPid(pid, "SIGTERM");
|
|
1937
|
-
}
|
|
1938
|
-
} catch {
|
|
1939
|
-
}
|
|
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);
|
|
1940
1867
|
}
|
|
1868
|
+
return running.sort();
|
|
1941
1869
|
}
|
|
1942
|
-
function
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
killListenersOnPorts(ports);
|
|
1949
|
-
return;
|
|
1950
|
-
}
|
|
1951
|
-
const selfPid = process.pid;
|
|
1952
|
-
for (const port of ports) {
|
|
1953
|
-
if (!Number.isFinite(port) || port <= 0) continue;
|
|
1954
|
-
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
1955
|
-
const cwd = listenerCwd(pid);
|
|
1956
|
-
if (cwd && isOtherWorktreeListenerCwd(cwd, worktreePath)) continue;
|
|
1957
|
-
signalPid(pid, signal);
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
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);
|
|
1960
1876
|
}
|
|
1961
|
-
function
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
if (process.platform === "win32") {
|
|
1966
|
-
void execa2("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
|
|
1967
|
-
} else {
|
|
1968
|
-
process.kill(-pid, "SIGTERM");
|
|
1969
|
-
setTimeout(() => {
|
|
1970
|
-
try {
|
|
1971
|
-
process.kill(-pid, "SIGKILL");
|
|
1972
|
-
} catch {
|
|
1973
|
-
}
|
|
1974
|
-
}, 2500).unref?.();
|
|
1975
|
-
}
|
|
1976
|
-
} catch {
|
|
1977
|
-
try {
|
|
1978
|
-
child.kill("SIGTERM");
|
|
1979
|
-
} catch {
|
|
1980
|
-
}
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
killListenersOnPorts(ports);
|
|
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");
|
|
1984
1881
|
}
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
const
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
);
|
|
1997
|
-
try {
|
|
1998
|
-
mergeAgentGitAuthEnv(
|
|
1999
|
-
env,
|
|
2000
|
-
await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
|
|
2001
|
-
);
|
|
2002
|
-
} catch {
|
|
2003
|
-
}
|
|
2004
|
-
if (opts.heldPorts?.length) {
|
|
2005
|
-
await Promise.all(opts.heldPorts.map((h) => h.release()));
|
|
2006
|
-
}
|
|
2007
|
-
const shell = process.platform === "darwin" ? "zsh" : "bash";
|
|
2008
|
-
const child = execa2(shell, ["-lc", command], {
|
|
2009
|
-
cwd: opts.worktreePath,
|
|
2010
|
-
reject: false,
|
|
2011
|
-
env,
|
|
2012
|
-
// Own process group so Stop can tear down the whole tree (not just the shell).
|
|
2013
|
-
// There is no settings.toml `stop=` / teardown hook for run scripts.
|
|
2014
|
-
...process.platform === "win32" ? {} : { detached: true }
|
|
2015
|
-
});
|
|
2016
|
-
pipeLines(child.stdout, opts.onLine);
|
|
2017
|
-
pipeLines(child.stderr, opts.onLine);
|
|
2018
|
-
const ports = opts.ports ?? [];
|
|
2019
|
-
return {
|
|
2020
|
-
pid: child.pid,
|
|
2021
|
-
kill: () => killScriptTree(child, ports),
|
|
2022
|
-
done: child.then((r) => r.exitCode ?? null),
|
|
2023
|
-
child
|
|
2024
|
-
};
|
|
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");
|
|
2025
1893
|
}
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
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)}`;
|
|
2031
1902
|
}
|
|
2032
|
-
return
|
|
1903
|
+
return out;
|
|
2033
1904
|
}
|
|
2034
|
-
|
|
2035
|
-
const
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
defaultBranch: opts?.defaultBranch,
|
|
2043
|
-
onLine
|
|
2044
|
-
});
|
|
2045
|
-
const exitCode = await attachAbort(handle, opts?.signal);
|
|
2046
|
-
return {
|
|
2047
|
-
ran: true,
|
|
2048
|
-
exitCode,
|
|
2049
|
-
source,
|
|
2050
|
-
kill: handle.kill
|
|
2051
|
-
};
|
|
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(" ");
|
|
2052
1913
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
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(" ");
|
|
1920
|
+
}
|
|
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);
|
|
2062
1927
|
});
|
|
2063
|
-
const exitCode = await attachAbort(handle, opts?.signal);
|
|
2064
|
-
return {
|
|
2065
|
-
ran: true,
|
|
2066
|
-
exitCode,
|
|
2067
|
-
source: found.source,
|
|
2068
|
-
kill: handle.kill
|
|
2069
|
-
};
|
|
2070
1928
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
if (
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
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
|
+
};
|
|
2080
1941
|
}
|
|
2081
|
-
if (!
|
|
2082
|
-
|
|
1942
|
+
if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
|
|
1943
|
+
return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
|
|
2083
1944
|
}
|
|
2084
|
-
return
|
|
1945
|
+
return { action: "none" };
|
|
2085
1946
|
}
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
worktreePath,
|
|
2091
|
-
repoPath,
|
|
2092
|
-
onLine
|
|
2093
|
-
});
|
|
2094
|
-
const exitCode = await handle.done;
|
|
2095
|
-
return { ran: true, exitCode };
|
|
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");
|
|
2096
1951
|
}
|
|
2097
|
-
function
|
|
2098
|
-
const
|
|
2099
|
-
|
|
2100
|
-
return settings.runScripts.filter((s) => {
|
|
2101
|
-
if (!s.availableIn?.length) return true;
|
|
2102
|
-
return s.availableIn.includes("local");
|
|
2103
|
-
});
|
|
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;
|
|
2104
1955
|
}
|
|
2105
|
-
function
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
function getRunScript(worktreePath, repoPath, name) {
|
|
2111
|
-
const scripts = listRunScripts(worktreePath, repoPath);
|
|
2112
|
-
if (!scripts.length) return null;
|
|
2113
|
-
if (name) {
|
|
2114
|
-
return scripts.find((s) => s.name === name) ?? null;
|
|
2115
|
-
}
|
|
2116
|
-
return getDefaultRunScript(worktreePath, repoPath);
|
|
2117
|
-
}
|
|
2118
|
-
function getRunMode(worktreePath, repoPath) {
|
|
2119
|
-
return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
|
|
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;
|
|
2120
1961
|
}
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
if (released) {
|
|
2127
|
-
res();
|
|
2128
|
-
return;
|
|
2129
|
-
}
|
|
2130
|
-
released = true;
|
|
2131
|
-
server.close(() => res());
|
|
2132
|
-
});
|
|
2133
|
-
server.listen(0, "127.0.0.1", () => {
|
|
2134
|
-
const addr = server.address();
|
|
2135
|
-
if (!addr || typeof addr === "string") {
|
|
2136
|
-
void release().then(() => reject(new Error("Failed to allocate port")));
|
|
2137
|
-
return;
|
|
2138
|
-
}
|
|
2139
|
-
resolve2({ port: addr.port, release });
|
|
2140
|
-
});
|
|
2141
|
-
server.on("error", reject);
|
|
2142
|
-
});
|
|
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 };
|
|
2143
1967
|
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
server.close(() => res());
|
|
2156
|
-
});
|
|
2157
|
-
server.once("error", () => {
|
|
2158
|
-
try {
|
|
2159
|
-
server.close();
|
|
2160
|
-
} catch {
|
|
2161
|
-
}
|
|
2162
|
-
resolve2(null);
|
|
2163
|
-
});
|
|
2164
|
-
server.listen(port, "127.0.0.1", () => {
|
|
2165
|
-
resolve2({ port, release });
|
|
2166
|
-
});
|
|
2167
|
-
});
|
|
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
|
+
};
|
|
2168
1979
|
}
|
|
2169
|
-
function
|
|
2170
|
-
|
|
2171
|
-
const
|
|
2172
|
-
const
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
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 {
|
|
2178
1990
|
}
|
|
2179
|
-
|
|
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
|
+
};
|
|
2180
2006
|
}
|
|
2181
|
-
function
|
|
2182
|
-
|
|
2007
|
+
async function sleepMs(ms) {
|
|
2008
|
+
await new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2183
2009
|
}
|
|
2184
|
-
async function
|
|
2185
|
-
|
|
2186
|
-
const
|
|
2187
|
-
const
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
taken.add(ephemeral.port);
|
|
2202
|
-
}
|
|
2203
|
-
}
|
|
2204
|
-
return held;
|
|
2205
|
-
} catch (err) {
|
|
2206
|
-
await Promise.all(held.map((h) => h.release()));
|
|
2207
|
-
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
|
+
};
|
|
2208
2027
|
}
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
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
|
+
});
|
|
2215
2036
|
}
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
const takePrimary = async () => {
|
|
2222
|
-
const held2 = await tryReserveAvailablePorts(ports);
|
|
2223
|
-
if (held2.some((h) => h.port === primary)) return held2;
|
|
2224
|
-
await Promise.all(held2.map((h) => h.release()));
|
|
2225
|
-
return null;
|
|
2226
|
-
};
|
|
2227
|
-
let held = await takePrimary();
|
|
2228
|
-
if (held) return held;
|
|
2229
|
-
if (worktreePath) {
|
|
2230
|
-
killStaleWorktreeListeners(ports, worktreePath, "SIGTERM");
|
|
2231
|
-
} else {
|
|
2232
|
-
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())));
|
|
2233
2042
|
}
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
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 {
|
|
2240
2053
|
}
|
|
2241
2054
|
}
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
const preferred = normalizePreferredPorts(opts?.preferred, size);
|
|
2246
|
-
if (preferred.length) {
|
|
2247
|
-
const reused = await reclaimPreferredPorts(preferred, opts?.worktreePath);
|
|
2248
|
-
if (reused) return fillPortRange(reused, size);
|
|
2055
|
+
try {
|
|
2056
|
+
process.kill(pid, signal);
|
|
2057
|
+
} catch {
|
|
2249
2058
|
}
|
|
2250
|
-
const held = [await reservePort()];
|
|
2251
|
-
return fillPortRange(held, size);
|
|
2252
2059
|
}
|
|
2253
|
-
|
|
2254
|
-
const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
|
|
2255
|
-
if (!script) return null;
|
|
2256
|
-
const preferred = loadWorktreeRunPorts(worktreePath, script.name) ?? opts?.preferredPorts;
|
|
2257
|
-
const held = await reservePortRange(PORT_RANGE_SIZE, {
|
|
2258
|
-
preferred: preferred ?? void 0,
|
|
2259
|
-
worktreePath
|
|
2260
|
-
});
|
|
2261
|
-
const ports = held.map((h) => h.port);
|
|
2262
|
-
saveWorktreeRunPorts(worktreePath, script.name, ports);
|
|
2060
|
+
function appendJobLog(logFile, line) {
|
|
2263
2061
|
try {
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
defaultBranch: opts?.defaultBranch,
|
|
2270
|
-
onLine
|
|
2271
|
-
});
|
|
2272
|
-
return {
|
|
2273
|
-
pid: handle.pid,
|
|
2274
|
-
port: ports[0],
|
|
2275
|
-
ports,
|
|
2276
|
-
scriptName: script.name,
|
|
2277
|
-
kill: handle.kill,
|
|
2278
|
-
done: handle.done
|
|
2279
|
-
};
|
|
2280
|
-
} catch (err) {
|
|
2281
|
-
await Promise.all(held.map((h) => h.release()));
|
|
2282
|
-
throw err;
|
|
2062
|
+
mkdirSync4(dirname3(logFile), { recursive: true });
|
|
2063
|
+
const body = line.endsWith("\n") ? line : `${line}
|
|
2064
|
+
`;
|
|
2065
|
+
appendFileSync2(logFile, body);
|
|
2066
|
+
} catch {
|
|
2283
2067
|
}
|
|
2284
2068
|
}
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
}
|
|
2315
|
-
}
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
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)) {
|
|
2076
|
+
return {
|
|
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"
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
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())));
|
|
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
|
+
}
|
|
2322
2117
|
}
|
|
2118
|
+
snap = snapshotJob(dir);
|
|
2119
|
+
const stillRunning = jobTreeAlive(targetPid) || snap.running;
|
|
2120
|
+
const result = toResult(jobId, snap, { failed: true });
|
|
2121
|
+
const stopped = !stillRunning;
|
|
2323
2122
|
return {
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
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
|
|
2328
2132
|
};
|
|
2329
2133
|
}
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
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);
|
|
2336
2142
|
}
|
|
2337
|
-
function
|
|
2338
|
-
|
|
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("#"));
|
|
2339
2147
|
}
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
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)];
|
|
2346
2168
|
}
|
|
2347
|
-
const
|
|
2348
|
-
if (!mem) return;
|
|
2169
|
+
const defaults = [];
|
|
2349
2170
|
try {
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
);
|
|
2171
|
+
for (const entry of readdirSync2(repoPath, { withFileTypes: true })) {
|
|
2172
|
+
if (entry.isFile() && entry.name.startsWith(".env")) {
|
|
2173
|
+
defaults.push(entry.name);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2356
2176
|
} catch {
|
|
2357
2177
|
}
|
|
2178
|
+
if (defaults.length) return defaults;
|
|
2179
|
+
return [".env.local", ".env"];
|
|
2358
2180
|
}
|
|
2359
|
-
function
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
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;
|
|
2368
2193
|
}
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
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");
|
|
2372
2198
|
try {
|
|
2373
|
-
const
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
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
|
+
}
|
|
2380
2216
|
} catch {
|
|
2381
|
-
return null;
|
|
2382
2217
|
}
|
|
2218
|
+
cachedLoginEnv = { ...process.env };
|
|
2219
|
+
return { ...cachedLoginEnv };
|
|
2383
2220
|
}
|
|
2384
|
-
function
|
|
2385
|
-
const
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
if (
|
|
2396
|
-
|
|
2397
|
-
|
|
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
|
+
}
|
|
2398
2247
|
}
|
|
2399
|
-
|
|
2248
|
+
applyWorktreePkgCacheEnv(env, opts.worktreePath);
|
|
2249
|
+
return env;
|
|
2400
2250
|
}
|
|
2401
|
-
function
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
running: true,
|
|
2406
|
-
exitCode: null,
|
|
2407
|
-
source: null
|
|
2408
|
-
};
|
|
2409
|
-
memory.set(threadId, mem);
|
|
2410
|
-
persistNow(threadId);
|
|
2411
|
-
return materialize(mem);
|
|
2251
|
+
function pipeLines(stream, onLine) {
|
|
2252
|
+
if (!stream || !onLine) return;
|
|
2253
|
+
const rl = createInterface2({ input: stream });
|
|
2254
|
+
rl.on("line", onLine);
|
|
2412
2255
|
}
|
|
2413
|
-
function
|
|
2414
|
-
const
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
schedulePersist(threadId);
|
|
2420
|
-
}
|
|
2421
|
-
function finishSetupLog(threadId, exitCode, source) {
|
|
2422
|
-
const mem = memFor(threadId);
|
|
2423
|
-
mem.running = false;
|
|
2424
|
-
mem.exitCode = exitCode;
|
|
2425
|
-
mem.source = source ?? mem.source;
|
|
2426
|
-
persistNow(threadId);
|
|
2427
|
-
return materialize(mem);
|
|
2428
|
-
}
|
|
2429
|
-
|
|
2430
|
-
// src/store/run-log.ts
|
|
2431
|
-
import { createHash as createHash2 } from "crypto";
|
|
2432
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
2433
|
-
import { join as join7 } from "path";
|
|
2434
|
-
var EMPTY2 = {
|
|
2435
|
-
output: "",
|
|
2436
|
-
running: false,
|
|
2437
|
-
exitCode: null,
|
|
2438
|
-
scriptName: null
|
|
2439
|
-
};
|
|
2440
|
-
var MAX_RUN_LOG_CHARS = 256e3;
|
|
2441
|
-
var DEFAULT_RUN_LOG_TAIL_CHARS = 16e3;
|
|
2442
|
-
var memory2 = /* @__PURE__ */ new Map();
|
|
2443
|
-
var persistTimers2 = /* @__PURE__ */ new Map();
|
|
2444
|
-
function fromSnapshot2(snap) {
|
|
2445
|
-
return {
|
|
2446
|
-
chunks: snap.output ? [snap.output] : [],
|
|
2447
|
-
length: snap.output.length,
|
|
2448
|
-
running: snap.running,
|
|
2449
|
-
exitCode: snap.exitCode,
|
|
2450
|
-
scriptName: snap.scriptName
|
|
2451
|
-
};
|
|
2452
|
-
}
|
|
2453
|
-
function materialize2(mem) {
|
|
2454
|
-
let output = mem.chunks.join("\n");
|
|
2455
|
-
if (output.length > MAX_RUN_LOG_CHARS) {
|
|
2456
|
-
output = appendSetupOutput("", output, MAX_RUN_LOG_CHARS);
|
|
2457
|
-
mem.chunks = [output];
|
|
2458
|
-
mem.length = output.length;
|
|
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);
|
|
2459
2262
|
}
|
|
2460
|
-
return
|
|
2461
|
-
output,
|
|
2462
|
-
running: mem.running,
|
|
2463
|
-
exitCode: mem.exitCode,
|
|
2464
|
-
scriptName: mem.scriptName
|
|
2465
|
-
};
|
|
2466
|
-
}
|
|
2467
|
-
function safeScriptToken(scriptName) {
|
|
2468
|
-
const token = scriptName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
|
|
2469
|
-
return token || "dev";
|
|
2470
|
-
}
|
|
2471
|
-
function runLogPath(key) {
|
|
2472
|
-
return join7(threadsDir(), `${key}.run.log.json`);
|
|
2473
|
-
}
|
|
2474
|
-
function runLogKeyForWorktree(worktreePath, scriptName) {
|
|
2475
|
-
const norm = normalizeWorktreePath(worktreePath);
|
|
2476
|
-
const hash = createHash2("sha1").update(norm).digest("hex").slice(0, 16);
|
|
2477
|
-
return `wt-${hash}-run-${safeScriptToken(scriptName)}`;
|
|
2478
|
-
}
|
|
2479
|
-
function emptyRunLog() {
|
|
2480
|
-
return { ...EMPTY2 };
|
|
2481
|
-
}
|
|
2482
|
-
function sliceRunLogOutput(output, tailChars = DEFAULT_RUN_LOG_TAIL_CHARS) {
|
|
2483
|
-
const max = Math.max(1, tailChars);
|
|
2484
|
-
const outputChars = output.length;
|
|
2485
|
-
if (outputChars <= max) return { output, truncated: false, outputChars };
|
|
2486
|
-
const cut = output.slice(outputChars - max);
|
|
2487
|
-
const nl = cut.indexOf("\n");
|
|
2488
|
-
const text = nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
|
|
2489
|
-
return { output: text, truncated: true, outputChars };
|
|
2263
|
+
return pids;
|
|
2490
2264
|
}
|
|
2491
|
-
|
|
2492
|
-
function persistNow2(key) {
|
|
2493
|
-
const timer = persistTimers2.get(key);
|
|
2494
|
-
if (timer) {
|
|
2495
|
-
clearTimeout(timer);
|
|
2496
|
-
persistTimers2.delete(key);
|
|
2497
|
-
}
|
|
2498
|
-
const mem = memory2.get(key);
|
|
2499
|
-
if (!mem) return;
|
|
2265
|
+
function listUnixListenerPids(port, selfPid) {
|
|
2500
2266
|
try {
|
|
2501
|
-
|
|
2502
|
-
|
|
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);
|
|
2503
2273
|
} catch {
|
|
2274
|
+
return [];
|
|
2504
2275
|
}
|
|
2505
2276
|
}
|
|
2506
|
-
function
|
|
2507
|
-
if (
|
|
2508
|
-
persistTimers2.set(
|
|
2509
|
-
key,
|
|
2510
|
-
setTimeout(() => {
|
|
2511
|
-
persistTimers2.delete(key);
|
|
2512
|
-
persistNow2(key);
|
|
2513
|
-
}, PERSIST_DEBOUNCE_MS2)
|
|
2514
|
-
);
|
|
2515
|
-
}
|
|
2516
|
-
function loadFromDisk2(key) {
|
|
2517
|
-
const path = runLogPath(key);
|
|
2518
|
-
if (!existsSync5(path)) return null;
|
|
2277
|
+
function listenerCwd(pid) {
|
|
2278
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
2519
2279
|
try {
|
|
2520
|
-
const raw =
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
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
|
+
}
|
|
2527
2288
|
} catch {
|
|
2528
|
-
return null;
|
|
2529
2289
|
}
|
|
2290
|
+
return null;
|
|
2530
2291
|
}
|
|
2531
|
-
function
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
return mem;
|
|
2537
|
-
}
|
|
2538
|
-
function readRunLog(key) {
|
|
2539
|
-
const cached = memory2.get(key);
|
|
2540
|
-
if (cached) return materialize2(cached);
|
|
2541
|
-
const disk = loadFromDisk2(key);
|
|
2542
|
-
if (disk) {
|
|
2543
|
-
memory2.set(key, fromSnapshot2(disk));
|
|
2544
|
-
return disk;
|
|
2292
|
+
function realpathOrResolve(path) {
|
|
2293
|
+
try {
|
|
2294
|
+
return realpathSync(path);
|
|
2295
|
+
} catch {
|
|
2296
|
+
return resolve(path);
|
|
2545
2297
|
}
|
|
2546
|
-
return emptyRunLog();
|
|
2547
|
-
}
|
|
2548
|
-
function beginRunLog(key, scriptName) {
|
|
2549
|
-
const mem = {
|
|
2550
|
-
chunks: [],
|
|
2551
|
-
length: 0,
|
|
2552
|
-
running: true,
|
|
2553
|
-
exitCode: null,
|
|
2554
|
-
scriptName
|
|
2555
|
-
};
|
|
2556
|
-
memory2.set(key, mem);
|
|
2557
|
-
persistNow2(key);
|
|
2558
|
-
return materialize2(mem);
|
|
2559
2298
|
}
|
|
2560
|
-
function
|
|
2561
|
-
const
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
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;
|
|
2567
2309
|
}
|
|
2568
|
-
function
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
return materialize2(mem);
|
|
2310
|
+
function signalPid(pid, signal) {
|
|
2311
|
+
try {
|
|
2312
|
+
process.kill(pid, signal);
|
|
2313
|
+
} catch {
|
|
2314
|
+
}
|
|
2574
2315
|
}
|
|
2575
|
-
function
|
|
2576
|
-
|
|
2316
|
+
function isThisWorktreeListenerCwd(cwd, worktreePath) {
|
|
2317
|
+
const wt = realpathOrResolve(worktreePath);
|
|
2318
|
+
const c = realpathOrResolve(cwd);
|
|
2319
|
+
return c === wt || c.startsWith(wt + sep);
|
|
2577
2320
|
}
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
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;
|
|
2328
|
+
}
|
|
2329
|
+
try {
|
|
2330
|
+
process.kill(-pid, "SIGTERM");
|
|
2331
|
+
} catch {
|
|
2332
|
+
signalPid(pid, "SIGTERM");
|
|
2333
|
+
}
|
|
2584
2334
|
}
|
|
2585
|
-
|
|
2586
|
-
const
|
|
2587
|
-
const
|
|
2588
|
-
|
|
2589
|
-
repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
|
|
2590
|
-
);
|
|
2591
|
-
const homeRoot = sideboardWorkspacesDir();
|
|
2592
|
-
if (existsSync6(homeRoot)) {
|
|
2335
|
+
function killListenersOnPorts(ports) {
|
|
2336
|
+
const selfPid = process.pid;
|
|
2337
|
+
for (const port of ports) {
|
|
2338
|
+
if (!Number.isFinite(port) || port <= 0) continue;
|
|
2593
2339
|
try {
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
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;
|
|
2597
2351
|
}
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
}
|
|
2601
|
-
const orphans = [];
|
|
2602
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2603
|
-
for (const repoPath of repos) {
|
|
2604
|
-
if (!repoPath || !existsSync6(repoPath)) continue;
|
|
2605
|
-
try {
|
|
2606
|
-
const wts = await listWorktrees(repoPath);
|
|
2607
|
-
for (const wt of wts) {
|
|
2608
|
-
const path = wt.path.replace(/\/$/, "");
|
|
2609
|
-
if (!isSideboardWorktreePath(path)) continue;
|
|
2610
|
-
if (known.has(path)) continue;
|
|
2611
|
-
if (seen.has(path)) continue;
|
|
2612
|
-
seen.add(path);
|
|
2613
|
-
let mtimeMs = 0;
|
|
2614
|
-
try {
|
|
2615
|
-
mtimeMs = statSync(path).mtimeMs;
|
|
2616
|
-
} catch {
|
|
2617
|
-
mtimeMs = 0;
|
|
2618
|
-
}
|
|
2619
|
-
orphans.push({ path, repoPath, mtimeMs });
|
|
2620
|
-
}
|
|
2621
|
-
} catch {
|
|
2622
|
-
}
|
|
2623
|
-
try {
|
|
2624
|
-
const root = worktreesRoot(repoPath);
|
|
2625
|
-
if (existsSync6(root)) {
|
|
2626
|
-
for (const entry of readdirSync2(root, { withFileTypes: true })) {
|
|
2627
|
-
if (!entry.isDirectory()) continue;
|
|
2628
|
-
const path = join8(root, entry.name).replace(/\/$/, "");
|
|
2629
|
-
if (known.has(path) || seen.has(path)) continue;
|
|
2630
|
-
if (!existsSync6(join8(path, ".git"))) continue;
|
|
2631
|
-
seen.add(path);
|
|
2632
|
-
let mtimeMs = 0;
|
|
2633
|
-
try {
|
|
2634
|
-
mtimeMs = statSync(path).mtimeMs;
|
|
2635
|
-
} catch {
|
|
2636
|
-
mtimeMs = Date.now();
|
|
2637
|
-
}
|
|
2638
|
-
orphans.push({ path, repoPath, mtimeMs });
|
|
2639
|
-
}
|
|
2352
|
+
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
2353
|
+
signalPid(pid, "SIGTERM");
|
|
2640
2354
|
}
|
|
2641
2355
|
} catch {
|
|
2642
2356
|
}
|
|
2643
2357
|
}
|
|
2644
|
-
return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
2645
2358
|
}
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
const
|
|
2656
|
-
for (const
|
|
2657
|
-
if (
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
removed.push(orphan.path);
|
|
2663
|
-
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);
|
|
2664
2375
|
}
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
function killScriptTree(child, ports = []) {
|
|
2379
|
+
const pid = child.pid;
|
|
2380
|
+
if (pid) {
|
|
2665
2381
|
try {
|
|
2666
|
-
|
|
2667
|
-
|
|
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
|
+
}
|
|
2668
2393
|
} catch {
|
|
2669
|
-
|
|
2394
|
+
try {
|
|
2395
|
+
child.kill("SIGTERM");
|
|
2396
|
+
} catch {
|
|
2397
|
+
}
|
|
2670
2398
|
}
|
|
2671
2399
|
}
|
|
2672
|
-
|
|
2673
|
-
updateAdvancedSettings({
|
|
2674
|
-
worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2675
|
-
});
|
|
2676
|
-
}
|
|
2677
|
-
return { removed, kept, orphans };
|
|
2678
|
-
}
|
|
2679
|
-
function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
|
|
2680
|
-
const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
|
|
2681
|
-
const last = settings.advanced.worktreeLastCleanupAt;
|
|
2682
|
-
if (!last) return true;
|
|
2683
|
-
const elapsed = Date.now() - Date.parse(last);
|
|
2684
|
-
return elapsed >= intervalHours * 36e5;
|
|
2400
|
+
killListenersOnPorts(ports);
|
|
2685
2401
|
}
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
writeThread(
|
|
2402
|
+
async function spawnWorkspaceScript(command, opts) {
|
|
2403
|
+
const loginEnv = await captureLoginEnv();
|
|
2404
|
+
const env = buildWorkspaceScriptEnv(
|
|
2690
2405
|
{
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2406
|
+
worktreePath: opts.worktreePath,
|
|
2407
|
+
repoPath: opts.repoPath,
|
|
2408
|
+
workspaceName: opts.workspaceName,
|
|
2409
|
+
defaultBranch: opts.defaultBranch,
|
|
2410
|
+
ports: opts.ports
|
|
2696
2411
|
},
|
|
2697
|
-
|
|
2412
|
+
loginEnv
|
|
2698
2413
|
);
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2414
|
+
try {
|
|
2415
|
+
mergeAgentGitAuthEnv(
|
|
2416
|
+
env,
|
|
2417
|
+
await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
|
|
2418
|
+
);
|
|
2419
|
+
} catch {
|
|
2704
2420
|
}
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
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 }
|
|
2711
2432
|
});
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
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 });
|
|
2716
2448
|
}
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
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`);
|
|
2725
2493
|
}
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
try {
|
|
2730
|
-
deleteThreadRecord(id);
|
|
2731
|
-
purged.push(id);
|
|
2732
|
-
} catch {
|
|
2733
|
-
}
|
|
2494
|
+
let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
|
|
2495
|
+
if (!setup.ran) {
|
|
2496
|
+
setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
|
|
2734
2497
|
}
|
|
2735
|
-
if (
|
|
2736
|
-
|
|
2737
|
-
historyLastCleanupAt: new Date(nowMs).toISOString()
|
|
2738
|
-
});
|
|
2498
|
+
if (!setup.ran) {
|
|
2499
|
+
setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
|
|
2739
2500
|
}
|
|
2740
|
-
return
|
|
2501
|
+
return setup;
|
|
2741
2502
|
}
|
|
2742
|
-
function
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
const
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
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 };
|
|
2749
2513
|
}
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
const status = await execa3("git", ["status", "--porcelain"], {
|
|
2757
|
-
cwd: thread.repoPath,
|
|
2758
|
-
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");
|
|
2759
2520
|
});
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
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;
|
|
2764
2532
|
}
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
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);
|
|
2768
2559
|
});
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
const
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
);
|
|
2780
|
-
const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
2781
|
-
if (!shas.length) {
|
|
2782
|
-
return {
|
|
2783
|
-
applied: false,
|
|
2784
|
-
method,
|
|
2785
|
-
targetBranch: target,
|
|
2786
|
-
message: `No commits on ${thread.branchName} not already in ${target}`
|
|
2787
|
-
};
|
|
2788
|
-
}
|
|
2789
|
-
for (const sha of shas) {
|
|
2790
|
-
const cp = await execa3("git", ["cherry-pick", sha], {
|
|
2791
|
-
cwd: thread.repoPath,
|
|
2792
|
-
reject: false
|
|
2793
|
-
});
|
|
2794
|
-
if (cp.exitCode !== 0) {
|
|
2795
|
-
await execa3("git", ["cherry-pick", "--abort"], {
|
|
2796
|
-
cwd: thread.repoPath,
|
|
2797
|
-
reject: false
|
|
2798
|
-
});
|
|
2799
|
-
throw new Error(
|
|
2800
|
-
`Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
|
|
2801
|
-
);
|
|
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;
|
|
2802
2570
|
}
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
applied: true,
|
|
2806
|
-
method,
|
|
2807
|
-
targetBranch: target,
|
|
2808
|
-
message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
|
|
2809
|
-
};
|
|
2810
|
-
}
|
|
2811
|
-
const merge = await execa3(
|
|
2812
|
-
"git",
|
|
2813
|
-
["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
|
|
2814
|
-
{ cwd: thread.repoPath, reject: false }
|
|
2815
|
-
);
|
|
2816
|
-
if (merge.exitCode !== 0) {
|
|
2817
|
-
await execa3("git", ["merge", "--abort"], {
|
|
2818
|
-
cwd: thread.repoPath,
|
|
2819
|
-
reject: false
|
|
2571
|
+
released = true;
|
|
2572
|
+
server.close(() => res());
|
|
2820
2573
|
});
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
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
|
+
});
|
|
2831
2585
|
}
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
if (!name) {
|
|
2842
|
-
const leaf = basename2(url.replace(/\/$/, "").replace(/\.git$/, ""));
|
|
2843
|
-
name = leaf || "repo";
|
|
2844
|
-
}
|
|
2845
|
-
name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
2846
|
-
const dest = join9(sideboardReposDir(), name);
|
|
2847
|
-
if (existsSync7(dest)) {
|
|
2848
|
-
const repoPath2 = await resolveRepoRoot(dest);
|
|
2849
|
-
const workspace2 = await ensureWorkspace(repoPath2);
|
|
2850
|
-
return { repoPath: repoPath2, workspace: workspace2 };
|
|
2851
|
-
}
|
|
2852
|
-
const result = await execa4("git", ["clone", url, dest], { reject: false });
|
|
2853
|
-
if (result.exitCode !== 0) {
|
|
2854
|
-
throw new Error(
|
|
2855
|
-
`git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
|
|
2856
|
-
);
|
|
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;
|
|
2857
2595
|
}
|
|
2858
|
-
|
|
2859
|
-
const workspace = await ensureWorkspace(repoPath);
|
|
2860
|
-
return { repoPath, workspace };
|
|
2861
|
-
}
|
|
2862
|
-
|
|
2863
|
-
// src/types/thread.ts
|
|
2864
|
-
function isAdoptableRunScriptRequest(req) {
|
|
2865
|
-
return Boolean(req && !req.fulfilledAt && !req.error);
|
|
2596
|
+
return out;
|
|
2866
2597
|
}
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
import { readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
|
|
2870
|
-
import { join as join10 } from "path";
|
|
2871
|
-
function desktopHostPidPath() {
|
|
2872
|
-
return join10(appDataDir(), "desktop-host.pid");
|
|
2598
|
+
function delay(ms) {
|
|
2599
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2873
2600
|
}
|
|
2874
|
-
function
|
|
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));
|
|
2875
2605
|
try {
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
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);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
return held;
|
|
2622
|
+
} catch (err) {
|
|
2623
|
+
await Promise.all(held.map((h) => h.release()));
|
|
2624
|
+
throw err;
|
|
2881
2625
|
}
|
|
2882
2626
|
}
|
|
2883
|
-
function
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
return false;
|
|
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);
|
|
2889
2632
|
}
|
|
2633
|
+
return held;
|
|
2890
2634
|
}
|
|
2891
|
-
function
|
|
2892
|
-
const
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
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);
|
|
2650
|
+
}
|
|
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
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
return null;
|
|
2901
2660
|
}
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
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);
|
|
2666
|
+
}
|
|
2667
|
+
const held = [await reservePort()];
|
|
2668
|
+
return fillPortRange(held, size);
|
|
2907
2669
|
}
|
|
2908
|
-
function
|
|
2909
|
-
const
|
|
2910
|
-
|
|
2911
|
-
const
|
|
2912
|
-
const
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
"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."
|
|
2916
|
-
].join("\n");
|
|
2917
|
-
}
|
|
2918
|
-
function shouldNotifyParentAfterTurnError(opts) {
|
|
2919
|
-
return !opts.quotaFailoverHandled && !opts.crashContinued;
|
|
2920
|
-
}
|
|
2921
|
-
function shouldNotifyParentOfChildHalt(opts) {
|
|
2922
|
-
if (!isIncompleteChildStatus(opts.status)) return false;
|
|
2923
|
-
if (!opts.child.parentThreadId) return false;
|
|
2924
|
-
if (!opts.parent || opts.parent.status === "archived") return false;
|
|
2925
|
-
if (opts.parent.id === opts.child.id) return false;
|
|
2926
|
-
return isOrchestratorThread(opts.parent);
|
|
2927
|
-
}
|
|
2928
|
-
var notified = /* @__PURE__ */ new Set();
|
|
2929
|
-
function noticeKey(childId, status) {
|
|
2930
|
-
return `${childId}:${status}`;
|
|
2931
|
-
}
|
|
2932
|
-
function notifyParentOfChildHalt(child, status, send) {
|
|
2933
|
-
const parent = child.parentThreadId ? readThread(child.parentThreadId) : null;
|
|
2934
|
-
if (!shouldNotifyParentOfChildHalt({ child, parent, status })) return false;
|
|
2935
|
-
const key = noticeKey(child.id, status);
|
|
2936
|
-
if (notified.has(key)) return false;
|
|
2937
|
-
notified.add(key);
|
|
2938
|
-
const parentId = parent.id;
|
|
2939
|
-
void send(parentId, childHaltNotice(child, status)).catch(() => {
|
|
2940
|
-
notified.delete(key);
|
|
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
|
|
2941
2677
|
});
|
|
2942
|
-
|
|
2678
|
+
const ports = held.map((h) => h.port);
|
|
2679
|
+
saveWorktreeRunPorts(worktreePath, script.name, ports);
|
|
2680
|
+
try {
|
|
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;
|
|
2700
|
+
}
|
|
2943
2701
|
}
|
|
2944
2702
|
|
|
2945
|
-
// src/
|
|
2946
|
-
import {
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
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;
|
|
2962
2721
|
}
|
|
2963
|
-
var
|
|
2964
|
-
var
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
|
|
2981
|
-
default:
|
|
2982
|
-
return void 0;
|
|
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
|
+
};
|
|
2732
|
+
}
|
|
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;
|
|
2983
2739
|
}
|
|
2740
|
+
return {
|
|
2741
|
+
output,
|
|
2742
|
+
running: mem.running,
|
|
2743
|
+
exitCode: mem.exitCode,
|
|
2744
|
+
source: mem.source
|
|
2745
|
+
};
|
|
2984
2746
|
}
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
var MAX_JOB_CONTINUES = 8;
|
|
2988
|
-
var MAX_JOB_DELTA_LINES = 80;
|
|
2989
|
-
var MAX_JOB_DELTA_CHARS = 4e3;
|
|
2990
|
-
var DEFERRED_DONE_MAX_CHARS = 480;
|
|
2991
|
-
var GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
|
|
2992
|
-
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.";
|
|
2993
|
-
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.";
|
|
2994
|
-
var JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
|
|
2995
|
-
function mcpWaitForJobTimeoutMs(requested) {
|
|
2996
|
-
return mcpWaitForTurnTimeoutMs(requested);
|
|
2747
|
+
function setupLogPath(threadId) {
|
|
2748
|
+
return join7(threadsDir(), `${threadId}.setup.log.json`);
|
|
2997
2749
|
}
|
|
2998
|
-
function
|
|
2999
|
-
const
|
|
3000
|
-
|
|
3001
|
-
throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
|
|
3002
|
-
}
|
|
3003
|
-
return s;
|
|
2750
|
+
function setupLogKeyForWorktree(worktreePath) {
|
|
2751
|
+
const norm = normalizeWorktreePath(worktreePath);
|
|
2752
|
+
return `wt-${createHash("sha1").update(norm).digest("hex").slice(0, 16)}`;
|
|
3004
2753
|
}
|
|
3005
|
-
function
|
|
3006
|
-
|
|
2754
|
+
function emptySetupLog() {
|
|
2755
|
+
return { ...EMPTY };
|
|
2756
|
+
}
|
|
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);
|
|
2763
|
+
}
|
|
2764
|
+
const mem = memory.get(threadId);
|
|
2765
|
+
if (!mem) return;
|
|
3007
2766
|
try {
|
|
3008
|
-
|
|
3009
|
-
|
|
2767
|
+
writeFileSync4(
|
|
2768
|
+
setupLogPath(threadId),
|
|
2769
|
+
`${JSON.stringify(materialize(mem))}
|
|
2770
|
+
`,
|
|
2771
|
+
"utf8"
|
|
2772
|
+
);
|
|
3010
2773
|
} catch {
|
|
3011
|
-
return false;
|
|
3012
2774
|
}
|
|
3013
2775
|
}
|
|
3014
|
-
function
|
|
3015
|
-
if (
|
|
3016
|
-
|
|
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;
|
|
3017
2789
|
try {
|
|
3018
|
-
|
|
3019
|
-
return
|
|
3020
|
-
|
|
3021
|
-
|
|
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;
|
|
3022
2799
|
}
|
|
3023
2800
|
}
|
|
3024
|
-
function
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
const n = Number.parseInt(readFileSync7(file, "utf8").trim(), 10);
|
|
3031
|
-
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;
|
|
3032
2807
|
}
|
|
3033
|
-
function
|
|
3034
|
-
|
|
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();
|
|
3035
2817
|
}
|
|
3036
|
-
function
|
|
3037
|
-
const
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
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);
|
|
3042
2829
|
}
|
|
3043
|
-
function
|
|
3044
|
-
const
|
|
3045
|
-
|
|
3046
|
-
|
|
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);
|
|
3047
2837
|
}
|
|
3048
|
-
function
|
|
3049
|
-
const
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
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);
|
|
2845
|
+
}
|
|
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
|
+
};
|
|
2869
|
+
}
|
|
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;
|
|
3060
2876
|
}
|
|
3061
|
-
return
|
|
2877
|
+
return {
|
|
2878
|
+
output,
|
|
2879
|
+
running: mem.running,
|
|
2880
|
+
exitCode: mem.exitCode,
|
|
2881
|
+
scriptName: mem.scriptName
|
|
2882
|
+
};
|
|
3062
2883
|
}
|
|
3063
|
-
function
|
|
3064
|
-
const
|
|
3065
|
-
|
|
3066
|
-
const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
|
|
3067
|
-
if (!unquoted) return false;
|
|
3068
|
-
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);
|
|
2884
|
+
function safeScriptToken(scriptName) {
|
|
2885
|
+
const token = scriptName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
|
|
2886
|
+
return token || "dev";
|
|
3069
2887
|
}
|
|
3070
|
-
function
|
|
3071
|
-
|
|
3072
|
-
const t = text.trim();
|
|
3073
|
-
return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
|
|
2888
|
+
function runLogPath(key) {
|
|
2889
|
+
return join8(threadsDir(), `${key}.run.log.json`);
|
|
3074
2890
|
}
|
|
3075
|
-
function
|
|
3076
|
-
|
|
3077
|
-
const
|
|
3078
|
-
|
|
3079
|
-
if (watchFrames.length <= 1) return text;
|
|
3080
|
-
const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
|
|
3081
|
-
const last = watchFrames[watchFrames.length - 1] ?? "";
|
|
3082
|
-
const omitted = watchFrames.length - 1;
|
|
3083
|
-
const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
|
|
3084
|
-
`;
|
|
3085
|
-
return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
|
|
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)}`;
|
|
3086
2895
|
}
|
|
3087
|
-
function
|
|
3088
|
-
|
|
3089
|
-
const lines = collapsed.split("\n");
|
|
3090
|
-
let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
|
|
3091
|
-
${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
|
|
3092
|
-
if (out.length > MAX_JOB_DELTA_CHARS) {
|
|
3093
|
-
out = `\u2026(truncated ${out.length} chars)
|
|
3094
|
-
${out.slice(-MAX_JOB_DELTA_CHARS)}`;
|
|
3095
|
-
}
|
|
3096
|
-
return out;
|
|
2896
|
+
function emptyRunLog() {
|
|
2897
|
+
return { ...EMPTY2 };
|
|
3097
2898
|
}
|
|
3098
|
-
function
|
|
3099
|
-
const
|
|
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
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
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
|
+
}
|
|
3113
2922
|
}
|
|
3114
|
-
function
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
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
|
+
);
|
|
3121
2932
|
}
|
|
3122
|
-
function
|
|
3123
|
-
|
|
3124
|
-
if (
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
const farewell = looksLikeDeferredDonePromise(opts.chatText);
|
|
3128
|
-
if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
|
|
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"));
|
|
3129
2938
|
return {
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
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
|
|
3133
2943
|
};
|
|
2944
|
+
} catch {
|
|
2945
|
+
return null;
|
|
3134
2946
|
}
|
|
3135
|
-
|
|
3136
|
-
|
|
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;
|
|
3137
2962
|
}
|
|
3138
|
-
return
|
|
2963
|
+
return emptyRunLog();
|
|
3139
2964
|
}
|
|
3140
|
-
function
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
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);
|
|
3144
2976
|
}
|
|
3145
|
-
function
|
|
3146
|
-
const
|
|
3147
|
-
|
|
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);
|
|
3148
2984
|
}
|
|
3149
|
-
function
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
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);
|
|
3154
2991
|
}
|
|
3155
|
-
function
|
|
3156
|
-
|
|
3157
|
-
const cursor = readIntFile(cursorFile) ?? 0;
|
|
3158
|
-
const start = Math.min(Math.max(0, cursor), lines.length);
|
|
3159
|
-
return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
|
|
2992
|
+
function flushRunLog(key) {
|
|
2993
|
+
persistNow2(key);
|
|
3160
2994
|
}
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
exitCode: readIntFile(join11(dir, "exit")),
|
|
3168
|
-
log: join11(dir, "log"),
|
|
3169
|
-
cursor: join11(dir, "present.cursor"),
|
|
3170
|
-
progress: tailProgress(join11(dir, "log"))
|
|
3171
|
-
};
|
|
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/");
|
|
3172
3001
|
}
|
|
3173
|
-
function
|
|
3174
|
-
const
|
|
3175
|
-
const
|
|
3176
|
-
const
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
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
|
+
);
|
|
3183
3242
|
}
|
|
3184
|
-
const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
|
|
3185
|
-
const progress = extra?.progress ?? snap.progress;
|
|
3186
3243
|
return {
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
id,
|
|
3192
|
-
pid: snap.pid,
|
|
3193
|
-
exitCode: snap.exitCode ?? void 0,
|
|
3194
|
-
phase: lastProgressLine(progress),
|
|
3195
|
-
delta,
|
|
3196
|
-
progress,
|
|
3197
|
-
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}`
|
|
3198
3248
|
};
|
|
3199
3249
|
}
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
}
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
const
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
if (!
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
ok: false,
|
|
3213
|
-
failed: true,
|
|
3214
|
-
status: "failed",
|
|
3215
|
-
id: jobId,
|
|
3216
|
-
delta: "",
|
|
3217
|
-
progress: "No detached job. Start one first.",
|
|
3218
|
-
hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
|
|
3219
|
-
};
|
|
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";
|
|
3220
3262
|
}
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
if (
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
progress: "No detached job. Start one first."
|
|
3228
|
-
});
|
|
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 };
|
|
3229
3269
|
}
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
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
|
+
);
|
|
3235
3275
|
}
|
|
3236
|
-
|
|
3237
|
-
|
|
3276
|
+
const repoPath = await resolveRepoRoot(dest);
|
|
3277
|
+
const workspace = await ensureWorkspace(repoPath);
|
|
3278
|
+
return { repoPath, workspace };
|
|
3238
3279
|
}
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
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() {
|
|
3248
3293
|
try {
|
|
3249
|
-
|
|
3294
|
+
const pid = Number.parseInt(readFileSync7(desktopHostPidPath(), "utf8").trim(), 10);
|
|
3295
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
3296
|
+
return pid;
|
|
3250
3297
|
} catch {
|
|
3298
|
+
return null;
|
|
3251
3299
|
}
|
|
3252
3300
|
}
|
|
3253
|
-
function
|
|
3301
|
+
function pidAlive(pid) {
|
|
3254
3302
|
try {
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
`;
|
|
3258
|
-
appendFileSync2(logFile, body);
|
|
3303
|
+
process.kill(pid, 0);
|
|
3304
|
+
return true;
|
|
3259
3305
|
} catch {
|
|
3306
|
+
return false;
|
|
3260
3307
|
}
|
|
3261
3308
|
}
|
|
3262
|
-
|
|
3263
|
-
const
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
}
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
if (
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
const
|
|
3305
|
-
if (!
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
const stopped = !stillRunning;
|
|
3315
|
-
return {
|
|
3316
|
-
...result,
|
|
3317
|
-
stillRunning,
|
|
3318
|
-
ok: false,
|
|
3319
|
-
failed: true,
|
|
3320
|
-
status: stillRunning ? "running" : "failed",
|
|
3321
|
-
stopped,
|
|
3322
|
-
reason: stopped ? "stopped" : "still-running",
|
|
3323
|
-
stopReason: why || void 0,
|
|
3324
|
-
hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
|
|
3325
|
-
};
|
|
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;
|
|
3326
3361
|
}
|
|
3327
3362
|
|
|
3328
3363
|
// src/threads/create.ts
|
|
@@ -3896,7 +3931,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
3896
3931
|
});
|
|
3897
3932
|
await requireAgent(resolved.agent);
|
|
3898
3933
|
if (input.cowboy) {
|
|
3899
|
-
const { cowboyModeEnabled } = await import("./app-settings-
|
|
3934
|
+
const { cowboyModeEnabled } = await import("./app-settings-652J57CX.js");
|
|
3900
3935
|
if (!cowboyModeEnabled()) {
|
|
3901
3936
|
throw new Error(
|
|
3902
3937
|
"Cowboy mode is off. Enable it in Settings \u2192 Advanced."
|
|
@@ -3954,7 +3989,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
3954
3989
|
let sourceIsFork = false;
|
|
3955
3990
|
if (!input.cowboy && sourceType === "branch" && (!sourceRef || sourceRef === "HEAD" || sourceRef === "default")) {
|
|
3956
3991
|
if (isAbleTimeConnected() && getIssueSource() === "abletime") {
|
|
3957
|
-
const { ensureAbleTimeTask, issueAttachmentForAbleTimeTask } = await import("./abletime-
|
|
3992
|
+
const { ensureAbleTimeTask, issueAttachmentForAbleTimeTask } = await import("./abletime-OI6FNSXX.js");
|
|
3958
3993
|
const task = await ensureAbleTimeTask({
|
|
3959
3994
|
title: title?.trim() || "Untitled work",
|
|
3960
3995
|
description: input.prompt?.trim() || void 0
|
|
@@ -4517,7 +4552,7 @@ async function adoptThread(input) {
|
|
|
4517
4552
|
messages: input.messages ?? []
|
|
4518
4553
|
});
|
|
4519
4554
|
writeThread(thread);
|
|
4520
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
4555
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-QLKWW3O2.js");
|
|
4521
4556
|
await ensureWorkspace2(repoPath);
|
|
4522
4557
|
const { ensureWorktreeSideboardIgnored: ensureWorktreeSideboardIgnored2 } = await import("./worktree-exclude-5N2KTYIT.js");
|
|
4523
4558
|
await ensureWorktreeSideboardIgnored2(input.worktreePath);
|
|
@@ -6494,7 +6529,7 @@ function formatWorktreeDirective(thread, opts) {
|
|
|
6494
6529
|
const slug = opts?.githubSlug ?? "<origin-owner/name>";
|
|
6495
6530
|
const resolve2 = opts?.githubSlug ? "" : " Resolve `<origin-owner/name>` with `git remote get-url origin` in this worktree.";
|
|
6496
6531
|
lines.push(
|
|
6497
|
-
`- Prefer a draft PR first: \`gh pr create --draft -R ${slug}\` (update via \`gh pr edit -R ${slug}\`). Always pass \`-R\` \u2014 bare \`gh pr create\` may target upstream on dual-remote checkouts.${resolve2} Mark ready for review only when asked. If GitHub rejects the body as too long, retry with a short \`--body-file\` \u2014 do not paste a changelog or diff into \`--body\`.`
|
|
6532
|
+
`- Prefer a draft PR first: \`gh pr create --draft --assignee @me -R ${slug}\` (update via \`gh pr edit -R ${slug}\`). Always pass \`-R\` \u2014 bare \`gh pr create\` may target upstream on dual-remote checkouts.${resolve2} Assign to the GitHub-connected user (\`--assignee @me\`) unless the user named someone else. Mark ready for review only when asked. If GitHub rejects the body as too long, retry with a short \`--body-file\` \u2014 do not paste a changelog or diff into \`--body\`.`
|
|
6498
6533
|
);
|
|
6499
6534
|
}
|
|
6500
6535
|
lines.push(
|
|
@@ -7023,7 +7058,7 @@ function formatScheduledPrompt(name, prompt) {
|
|
|
7023
7058
|
${prompt}`;
|
|
7024
7059
|
}
|
|
7025
7060
|
async function defaultDeps() {
|
|
7026
|
-
const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-
|
|
7061
|
+
const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-ZJ6SYGW7.js");
|
|
7027
7062
|
const orch = getOrchestrator2();
|
|
7028
7063
|
return {
|
|
7029
7064
|
findThread: (id) => findThreadByRef(id),
|
|
@@ -7348,7 +7383,7 @@ var Orchestrator = class {
|
|
|
7348
7383
|
orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
|
|
7349
7384
|
});
|
|
7350
7385
|
}
|
|
7351
|
-
const { autoCleanupOrphansEnabled } = await import("./app-settings-
|
|
7386
|
+
const { autoCleanupOrphansEnabled } = await import("./app-settings-652J57CX.js");
|
|
7352
7387
|
if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
|
|
7353
7388
|
await cleanupOrphanWorktrees({ repoPaths });
|
|
7354
7389
|
}
|
|
@@ -7695,7 +7730,7 @@ var Orchestrator = class {
|
|
|
7695
7730
|
}
|
|
7696
7731
|
await setup;
|
|
7697
7732
|
if (skipSetup) return;
|
|
7698
|
-
const { autoRunAfterSetupEnabled } = await import("./app-settings-
|
|
7733
|
+
const { autoRunAfterSetupEnabled } = await import("./app-settings-652J57CX.js");
|
|
7699
7734
|
if (autoRunAfterSetupEnabled()) {
|
|
7700
7735
|
try {
|
|
7701
7736
|
await this.startDev(threadId);
|
|
@@ -7946,7 +7981,8 @@ var Orchestrator = class {
|
|
|
7946
7981
|
if (compact.didCompact) {
|
|
7947
7982
|
thread = updateThread(threadId, {
|
|
7948
7983
|
messages: compact.thread.messages,
|
|
7949
|
-
sessionId: compact.thread.sessionId
|
|
7984
|
+
sessionId: compact.thread.sessionId,
|
|
7985
|
+
compactedMessages: compact.thread.compactedMessages
|
|
7950
7986
|
});
|
|
7951
7987
|
this.emit({
|
|
7952
7988
|
type: "context_compacted",
|
|
@@ -7960,9 +7996,6 @@ var Orchestrator = class {
|
|
|
7960
7996
|
this.emit({ type: "error", threadId, message: `context compact failed: ${message}` });
|
|
7961
7997
|
thread = this.requireThread(threadId);
|
|
7962
7998
|
}
|
|
7963
|
-
if (this.stoppedTurns.has(threadId)) {
|
|
7964
|
-
return;
|
|
7965
|
-
}
|
|
7966
7999
|
const promptText = typeof prompt === "string" ? prompt : "";
|
|
7967
8000
|
const autoContinue = isJobContinuePrompt(promptText) || promptText.startsWith("The previous agent process ended before it finished.");
|
|
7968
8001
|
const parked = takePendingTurnAttachments(thread);
|
|
@@ -7979,6 +8012,14 @@ var Orchestrator = class {
|
|
|
7979
8012
|
agent: thread.agent,
|
|
7980
8013
|
orch: isOrchestratorThread(thread)
|
|
7981
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
|
+
}
|
|
7982
8023
|
thread = this.requireThread(threadId);
|
|
7983
8024
|
const gitPrompt = expandCanonicalGitRequest(promptText);
|
|
7984
8025
|
const { agentPrompt: expandedPrompt } = expandComposerPrompt(
|
|
@@ -8037,7 +8078,7 @@ var Orchestrator = class {
|
|
|
8037
8078
|
].filter(Boolean).join("\n\n") || void 0;
|
|
8038
8079
|
const prGateDirective = thread.agent !== "brightsy" && !isOrchestratorThread(thread) && mentionsPrGoal(promptText) ? formatPrGateDirective() : null;
|
|
8039
8080
|
const agentPrompt = [
|
|
8040
|
-
thread.planMode
|
|
8081
|
+
planModeTurnInstruction(thread.planMode),
|
|
8041
8082
|
orchestrationReminder,
|
|
8042
8083
|
prGateDirective,
|
|
8043
8084
|
injectedNoticeContext,
|
|
@@ -8059,7 +8100,7 @@ var Orchestrator = class {
|
|
|
8059
8100
|
}
|
|
8060
8101
|
const isBrightsy = fresh.agent === "brightsy";
|
|
8061
8102
|
const isOrchestration = isOrchestratorThread(fresh);
|
|
8062
|
-
const { autoRenameBranchEnabled, getGithubGitAuthMode, gitBranchPrefixSetting } = await import("./app-settings-
|
|
8103
|
+
const { autoRenameBranchEnabled, getGithubGitAuthMode, gitBranchPrefixSetting } = await import("./app-settings-652J57CX.js");
|
|
8063
8104
|
const gitAuthMode = getGithubGitAuthMode();
|
|
8064
8105
|
const worktreeDirective = isBrightsy || isOrchestration ? null : formatWorktreeDirective(fresh, {
|
|
8065
8106
|
githubSlug: await withTimeout(
|
|
@@ -9350,7 +9391,7 @@ var Orchestrator = class {
|
|
|
9350
9391
|
if (!titled.userSetTitle && meta.title && titled.title !== meta.title) {
|
|
9351
9392
|
updateThread(thread.id, { title: meta.title });
|
|
9352
9393
|
}
|
|
9353
|
-
const { autoArchiveOnMergeEnabled } = await import("./app-settings-
|
|
9394
|
+
const { autoArchiveOnMergeEnabled } = await import("./app-settings-652J57CX.js");
|
|
9354
9395
|
const latest = this.requireThread(thread.id);
|
|
9355
9396
|
if (!shouldAutoArchiveOnPrMerge({
|
|
9356
9397
|
previousPrState: prevState || null,
|
|
@@ -9632,7 +9673,7 @@ var Orchestrator = class {
|
|
|
9632
9673
|
this.enforceHistoryRetention();
|
|
9633
9674
|
if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
|
|
9634
9675
|
try {
|
|
9635
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
9676
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-QLKWW3O2.js");
|
|
9636
9677
|
await ensureWorkspace2(thread.repoPath);
|
|
9637
9678
|
} catch {
|
|
9638
9679
|
}
|
|
@@ -9659,7 +9700,7 @@ var Orchestrator = class {
|
|
|
9659
9700
|
await runArchiveScript(thread.repoPath, thread.worktreePath);
|
|
9660
9701
|
} catch {
|
|
9661
9702
|
}
|
|
9662
|
-
const { deleteBranchOnPurgeEnabled } = await import("./app-settings-
|
|
9703
|
+
const { deleteBranchOnPurgeEnabled } = await import("./app-settings-652J57CX.js");
|
|
9663
9704
|
const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
|
|
9664
9705
|
await removeWorktree(thread.repoPath, thread.worktreePath, {
|
|
9665
9706
|
deleteBranch: deleteBranch ? thread.branchName : void 0
|
|
@@ -9686,7 +9727,7 @@ var Orchestrator = class {
|
|
|
9686
9727
|
`Cowboy checkout missing: ${thread.worktreePath}. Re-add the project folder, then restore.`
|
|
9687
9728
|
);
|
|
9688
9729
|
}
|
|
9689
|
-
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-
|
|
9730
|
+
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-PK3QETXO.js");
|
|
9690
9731
|
const slug = thread.worktreePath.split("/").pop();
|
|
9691
9732
|
const dest = thread.worktreePath;
|
|
9692
9733
|
await withRepoGitLock(thread.repoPath, async () => {
|
|
@@ -9796,7 +9837,7 @@ async function startOrchestration(opts) {
|
|
|
9796
9837
|
sourceRef: "default",
|
|
9797
9838
|
...createOpts
|
|
9798
9839
|
}).catch(async () => {
|
|
9799
|
-
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-
|
|
9840
|
+
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-PK3QETXO.js");
|
|
9800
9841
|
const repo = await resolveRepoRoot2(repoPath);
|
|
9801
9842
|
const def = await resolveDefaultBranch2(repo);
|
|
9802
9843
|
return orch.createThread({
|
|
@@ -9806,7 +9847,7 @@ async function startOrchestration(opts) {
|
|
|
9806
9847
|
repoPath: repo
|
|
9807
9848
|
});
|
|
9808
9849
|
});
|
|
9809
|
-
const { updateThread: upd } = await import("./thread-store-
|
|
9850
|
+
const { updateThread: upd } = await import("./thread-store-JQB7KZJR.js");
|
|
9810
9851
|
const updated = upd(thread.id, {
|
|
9811
9852
|
sourceType: "orchestration",
|
|
9812
9853
|
sourceRef: goal
|
|
@@ -9822,14 +9863,14 @@ export {
|
|
|
9822
9863
|
needsCoordinatorAction,
|
|
9823
9864
|
AGENT_GIT_ACTIONS,
|
|
9824
9865
|
threadsSharingWorktree,
|
|
9825
|
-
MAX_RUN_LOG_CHARS,
|
|
9826
|
-
DEFAULT_RUN_LOG_TAIL_CHARS,
|
|
9827
|
-
sliceRunLogOutput,
|
|
9828
9866
|
mcpWaitForTurnTimeoutMs,
|
|
9829
9867
|
mcpWaitTaskHint,
|
|
9830
9868
|
mcpWaitForJobTimeoutMs,
|
|
9831
9869
|
waitForDetachedJob,
|
|
9832
9870
|
stopDetachedJob,
|
|
9871
|
+
MAX_RUN_LOG_CHARS,
|
|
9872
|
+
DEFAULT_RUN_LOG_TAIL_CHARS,
|
|
9873
|
+
sliceRunLogOutput,
|
|
9833
9874
|
BOARD_COLUMN_DEFS,
|
|
9834
9875
|
HOME_BOARD_CACHE_TTL_MS,
|
|
9835
9876
|
dedupeBoardIssues,
|