@sideboard-ai/core 0.1.264 → 0.1.267
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-6AOJI2YZ.js → abletime-4ZSDI4YP.js} +9 -6
- package/dist/{abletime-U75ZGDIX.js → abletime-JNQAHLHH.js} +7 -4
- package/dist/{abletime-oauth-TA2YDCYX.js → abletime-oauth-4RXZH2KU.js} +3 -3
- package/dist/{abletime-oauth-LOAHGG4X.js → abletime-oauth-DCEIINYT.js} +5 -5
- package/dist/{abletime-rest-CTA5WV26.js → abletime-rest-KVBJFVFD.js} +3 -3
- package/dist/{abletime-rest-YZPFM6IY.js → abletime-rest-VN6PZ6P2.js} +5 -5
- package/dist/agents/cursor-runner.js +7 -5
- package/dist/{agents-3TUMFSMH.js → agents-G3JTULM2.js} +7 -5
- package/dist/{agents-6FWDRVA2.js → agents-XRXJCAWJ.js} +11 -10
- package/dist/{app-settings-RKFFUIFQ.js → app-settings-L5HGYXWJ.js} +1 -3
- package/dist/{app-settings-MVZPM6WW.js → app-settings-RAC4PSSP.js} +3 -5
- package/dist/{caffeinate-hold-UAGLSGMM.js → caffeinate-hold-7JM5H5P3.js} +2 -2
- package/dist/{chunk-F5MFNUAA.js → chunk-27Z4MEQ5.js} +1 -1
- package/dist/{chunk-EK3KVT7I.js → chunk-2XIRV7FS.js} +53 -1254
- package/dist/{chunk-3H7O27X2.js → chunk-3VBS6MES.js} +39 -1017
- package/dist/chunk-4ECDIKAS.js +2273 -0
- package/dist/{chunk-H547WSSZ.js → chunk-4RZRHTSZ.js} +1 -1
- package/dist/{chunk-QN5DL2QO.js → chunk-5DZUWGKV.js} +1 -1
- package/dist/chunk-5N4Q46SG.js +392 -0
- package/dist/{chunk-YKYEHDMX.js → chunk-AGXZAMOF.js} +5 -5
- package/dist/{chunk-RKPBIB2U.js → chunk-DB7MVDYS.js} +33 -7
- package/dist/{chunk-BOMSJUXW.js → chunk-DBFAFI5A.js} +5 -5
- package/dist/chunk-FAGK33E2.js +994 -0
- package/dist/{chunk-A5SAYFJ4.js → chunk-FIBZEFKA.js} +4 -390
- package/dist/{chunk-5PK53SAP.js → chunk-GONPMH36.js} +5 -5
- package/dist/{chunk-L5WGAIL2.js → chunk-GZXKUUJO.js} +35 -9
- package/dist/{chunk-Z2LGMV6C.js → chunk-H5XL4KUS.js} +21 -0
- package/dist/{chunk-T3AYRQJZ.js → chunk-HSRN2CSL.js} +473 -2487
- package/dist/{chunk-TJ673EZT.js → chunk-JANCPF7Z.js} +2 -2
- package/dist/chunk-KZ2CDZBZ.js +828 -0
- package/dist/{chunk-6YY7XPPL.js → chunk-L6CYOON4.js} +5 -5
- package/dist/{chunk-6YOXQVJ7.js → chunk-LAIBYKZS.js} +3 -3
- package/dist/{chunk-4ALDX5IO.js → chunk-NWE3NOWQ.js} +3 -3
- package/dist/{chunk-36FOBJBF.js → chunk-OIENQEGG.js} +63 -38
- package/dist/{chunk-2QUQH2SZ.js → chunk-PAH7KDL2.js} +68 -24
- package/dist/{chunk-XAISOBBG.js → chunk-TQRSVE7D.js} +1 -1
- package/dist/chunk-UKSJO36E.js +1224 -0
- package/dist/{chunk-CEMNIGQH.js → chunk-UN5Q7POQ.js} +1 -1
- package/dist/{chunk-5ZPT5YAV.js → chunk-USBZ7PH4.js} +1 -1
- package/dist/chunk-UXI52MLJ.js +826 -0
- package/dist/chunk-X7RP3TQB.js +2385 -0
- package/dist/{chunk-QX6QUZVD.js → chunk-ZSGSVFWD.js} +424 -2611
- package/dist/conductor-MLM5NGTP.js +104 -0
- package/dist/conductor-NRKTEN5X.js +105 -0
- package/dist/{connected-teams-7CENZDWU.js → connected-teams-JIUOSZ6C.js} +2 -2
- package/dist/{coordinator-prompt-J7DKHOCH.js → coordinator-prompt-4GHKCVRH.js} +7 -7
- package/dist/{coordinator-prompt-HZCQZ6C5.js → coordinator-prompt-BKRQDZFT.js} +3 -3
- package/dist/{cursor-recover-FALSNEZN.js → cursor-recover-MB3T2K5T.js} +2 -2
- package/dist/{global-workspace-TI26K25J.js → global-workspace-EEWJOMED.js} +9 -3
- package/dist/{global-workspace-6LDUZ2NF.js → global-workspace-EWFL4HPW.js} +13 -7
- package/dist/index.cjs +3918 -2592
- package/dist/index.d.cts +363 -68
- package/dist/index.d.ts +363 -68
- package/dist/index.js +4181 -3462
- package/dist/{managed-accounts-4BLTJKAN.js → managed-accounts-46A4KRUE.js} +2 -2
- package/dist/mcp/run-stdio.cjs +1894 -2012
- package/dist/mcp/run-stdio.js +719 -1445
- package/dist/{orchestrator-5YU46YNS.js → orchestrator-6ZQ2CYS4.js} +9 -6
- package/dist/{orchestrator-4JSG7BWC.js → orchestrator-MA3MHYIM.js} +15 -13
- package/dist/{paths-IUR3ABNG.js → paths-HYT5WOOX.js} +1 -1
- package/dist/{thread-store-FJ5CDX2M.js → thread-store-B3ZM4QEZ.js} +3 -3
- package/dist/{workspaces-QJ3XIPUJ.js → workspaces-F33Z4CEY.js} +3 -3
- package/dist/{workspaces-WOQHJFRQ.js → workspaces-SLP24Z3S.js} +7 -7
- package/dist/{worktree-6U2FA5LO.js → worktree-ACW23KJ5.js} +6 -6
- package/dist/{worktree-KQA4ON2G.js → worktree-C7RPVHZ7.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-CEOTK5GZ.js +0 -479
- package/dist/chunk-K7B25HO4.js +0 -477
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
titleFromPrompt
|
|
5
|
+
} from "./chunk-LX5R5JOR.js";
|
|
3
6
|
import {
|
|
4
7
|
extractPresentedPlan,
|
|
5
8
|
readPlanFile,
|
|
@@ -7,36 +10,73 @@ import {
|
|
|
7
10
|
} from "./chunk-4GK32AYX.js";
|
|
8
11
|
import {
|
|
9
12
|
releaseCaffeinateHoldForThread
|
|
10
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-5DZUWGKV.js";
|
|
11
14
|
import {
|
|
12
15
|
adjustBrightsyTurnExit,
|
|
13
|
-
applyAgentEvent,
|
|
14
16
|
applyAgentRunnerHeapEnv,
|
|
17
|
+
getAdapter,
|
|
18
|
+
isSessionQuotaLimit,
|
|
19
|
+
packagedMcpDir,
|
|
20
|
+
parseBrightsyCliLine,
|
|
21
|
+
parseSessionQuotaResetAt,
|
|
22
|
+
planModeTurnInstruction,
|
|
23
|
+
resolveQuotaFallbackAgent
|
|
24
|
+
} from "./chunk-2XIRV7FS.js";
|
|
25
|
+
import {
|
|
26
|
+
REVIEW_TAB_TITLE,
|
|
27
|
+
applyWorktreePkgCacheEnv,
|
|
28
|
+
buildBrightsySessionSeed,
|
|
29
|
+
buildForkTranscriptAttachment,
|
|
30
|
+
buildSessionSeed,
|
|
31
|
+
copyConfiguredFiles,
|
|
32
|
+
createChatTab,
|
|
33
|
+
deriveTaskState,
|
|
34
|
+
forkChatTab,
|
|
35
|
+
forkMessageSlice,
|
|
36
|
+
formatReviewWriteGateDirective,
|
|
37
|
+
formatReviewWriteGateReminder,
|
|
38
|
+
getDefaultRunScript,
|
|
39
|
+
getRunMode,
|
|
40
|
+
isJobContinuePrompt,
|
|
41
|
+
isReviewWriteGatedThread,
|
|
42
|
+
isWorktreeRunProcessKey,
|
|
43
|
+
killDetachedRunTree,
|
|
44
|
+
killListenersOnPorts,
|
|
45
|
+
lastActivityAtForWait,
|
|
46
|
+
listRunScripts,
|
|
47
|
+
listRunningDetachedJobs,
|
|
48
|
+
maybeCompactContext,
|
|
49
|
+
mergeWorktreeActiveRuns,
|
|
50
|
+
planJobContinue,
|
|
51
|
+
requestReview,
|
|
52
|
+
resolveWorktreeSetupLog,
|
|
53
|
+
runArchiveScript,
|
|
54
|
+
runWorkspaceSetup,
|
|
55
|
+
startDevServer,
|
|
56
|
+
threadsSharingWorktree,
|
|
57
|
+
turnWatchedDetachedJob,
|
|
58
|
+
worktreeDevProcessKey,
|
|
59
|
+
worktreeRunProcessKey,
|
|
60
|
+
worktreeSetupProcessKey
|
|
61
|
+
} from "./chunk-4ECDIKAS.js";
|
|
62
|
+
import {
|
|
63
|
+
applyAgentEvent,
|
|
15
64
|
clipAgentEventForPaint,
|
|
16
|
-
clipToolResultForStore,
|
|
17
|
-
compactSummaryPart,
|
|
18
65
|
fallbackTurnFailDetail,
|
|
19
66
|
finalizeParts,
|
|
20
67
|
formatAgentErrorContinuePrompt,
|
|
21
68
|
formatTurnExitError,
|
|
22
|
-
getAdapter,
|
|
23
69
|
isBrightsyNdjsonLine,
|
|
24
70
|
isInternalAgentStatusText,
|
|
25
71
|
isPollWrapperToolName,
|
|
26
|
-
isSessionQuotaLimit,
|
|
27
|
-
lastAssistantMessageText,
|
|
28
72
|
liveActivitySummary,
|
|
29
73
|
looksLikeAgentFailureMessage,
|
|
30
74
|
looksLikeInvalidAgentSession,
|
|
31
75
|
looksLikeV8Oom,
|
|
76
|
+
messagePartParentId,
|
|
32
77
|
normalizeParseResult,
|
|
33
|
-
packagedMcpDir,
|
|
34
|
-
parseBrightsyCliLine,
|
|
35
|
-
parseSessionQuotaResetAt,
|
|
36
78
|
partsToAssistantText,
|
|
37
|
-
planModeTurnInstruction,
|
|
38
79
|
pushTurnStderr,
|
|
39
|
-
resolveQuotaFallbackAgent,
|
|
40
80
|
shouldFeedErrorBackToAgent,
|
|
41
81
|
shouldRetryCodexPluginIsolate,
|
|
42
82
|
shouldRetryFailedAgentTurn,
|
|
@@ -44,7 +84,7 @@ import {
|
|
|
44
84
|
summarizeTurnStderr,
|
|
45
85
|
toolDescription,
|
|
46
86
|
turnFailChatText
|
|
47
|
-
} from "./chunk-
|
|
87
|
+
} from "./chunk-UKSJO36E.js";
|
|
48
88
|
import {
|
|
49
89
|
SLACK_REPLY_FORMATTING,
|
|
50
90
|
addWorkspace,
|
|
@@ -56,22 +96,19 @@ import {
|
|
|
56
96
|
ensureGlobalCoordinatorCwd,
|
|
57
97
|
ensureWorkspace,
|
|
58
98
|
healOrchestrationSoccerTitles,
|
|
59
|
-
isCloudCoordinatorThread,
|
|
60
99
|
isGlobalRepoPath,
|
|
61
100
|
isGlobalThread,
|
|
62
101
|
isOrchestratorThread,
|
|
63
|
-
isSlackCoordinatorThread,
|
|
64
102
|
orchestratorSessionPoisonedByBuiltins,
|
|
65
103
|
persistPendingFileAttachments,
|
|
66
104
|
removeWorkspace,
|
|
67
105
|
stageAbsolutePathsAsAttachments,
|
|
68
106
|
stageBuffersAsAttachments,
|
|
69
107
|
syncWorkspacesFromThreads
|
|
70
|
-
} from "./chunk-
|
|
108
|
+
} from "./chunk-GZXKUUJO.js";
|
|
71
109
|
import {
|
|
72
110
|
addPrStackLayer,
|
|
73
111
|
agentNicknameFromWorktreePath,
|
|
74
|
-
allocateTeamName,
|
|
75
112
|
allocateTeamSlug,
|
|
76
113
|
canonicalizeRepoPath,
|
|
77
114
|
ciChecksFailed,
|
|
@@ -113,7 +150,6 @@ import {
|
|
|
113
150
|
resolvePrSelector,
|
|
114
151
|
resolveRepoRoot,
|
|
115
152
|
switchWorktreeBranch,
|
|
116
|
-
takenSlugsFromThread,
|
|
117
153
|
teamSlugFromName,
|
|
118
154
|
ticketSlugForBranch,
|
|
119
155
|
withRepoGitLock,
|
|
@@ -121,26 +157,13 @@ import {
|
|
|
121
157
|
worktreeIdentityTitles,
|
|
122
158
|
worktreeNameFromPath,
|
|
123
159
|
worktreeSlugForTicket
|
|
124
|
-
} from "./chunk-
|
|
125
|
-
import {
|
|
126
|
-
ATTACHMENTS_DIR,
|
|
127
|
-
DETACHED_JOBS_DIR,
|
|
128
|
-
LEGACY_ATTACHMENTS_DIR,
|
|
129
|
-
LEGACY_DETACHED_JOBS_DIR
|
|
130
|
-
} from "./chunk-N2WW62LF.js";
|
|
131
|
-
import {
|
|
132
|
-
titleFromPrompt
|
|
133
|
-
} from "./chunk-LX5R5JOR.js";
|
|
134
|
-
import {
|
|
135
|
-
ensureWorktreeSideboardIgnored
|
|
136
|
-
} from "./chunk-DD67JWTN.js";
|
|
160
|
+
} from "./chunk-NWE3NOWQ.js";
|
|
137
161
|
import {
|
|
138
162
|
clearStaleIndexLocks,
|
|
139
163
|
enrichPathWithNpmGlobalBin,
|
|
140
164
|
ensureAgentPath,
|
|
141
165
|
git,
|
|
142
|
-
resolveGitDirsForLockRecovery
|
|
143
|
-
run
|
|
166
|
+
resolveGitDirsForLockRecovery
|
|
144
167
|
} from "./chunk-2UG25RMK.js";
|
|
145
168
|
import {
|
|
146
169
|
HISTORY_CLEANUP_INTERVAL_HOURS_DEFAULT,
|
|
@@ -150,7 +173,6 @@ import {
|
|
|
150
173
|
caffeinateWhileSchedulesEnabled,
|
|
151
174
|
childEnvWithAppSettings,
|
|
152
175
|
coerceOrchestratorAgent,
|
|
153
|
-
contextTokens,
|
|
154
176
|
followUpBehavior,
|
|
155
177
|
formatViewerContextDirective,
|
|
156
178
|
formatViewerContextReminder,
|
|
@@ -173,13 +195,10 @@ import {
|
|
|
173
195
|
updateAdvancedSettings,
|
|
174
196
|
usageOnLimit,
|
|
175
197
|
withTurnScopedCost
|
|
176
|
-
} from "./chunk-
|
|
198
|
+
} from "./chunk-OIENQEGG.js";
|
|
177
199
|
import {
|
|
178
200
|
writePrivateFile
|
|
179
201
|
} from "./chunk-NSTQ6QKD.js";
|
|
180
|
-
import {
|
|
181
|
-
stripNestedElectronEnv
|
|
182
|
-
} from "./chunk-WWUABK6B.js";
|
|
183
202
|
import {
|
|
184
203
|
examplePrefixedBranch,
|
|
185
204
|
sanitizeGitBranchPrefix
|
|
@@ -189,7 +208,7 @@ import {
|
|
|
189
208
|
applyManagedAccountEnv,
|
|
190
209
|
nextManagedAccount,
|
|
191
210
|
resolveAccountForThread
|
|
192
|
-
} from "./chunk-
|
|
211
|
+
} from "./chunk-UN5Q7POQ.js";
|
|
193
212
|
import {
|
|
194
213
|
appendMessage,
|
|
195
214
|
createEmptyThread,
|
|
@@ -201,21 +220,17 @@ import {
|
|
|
201
220
|
updateThread,
|
|
202
221
|
withThreadLock,
|
|
203
222
|
writeThread
|
|
204
|
-
} from "./chunk-
|
|
223
|
+
} from "./chunk-JANCPF7Z.js";
|
|
205
224
|
import {
|
|
206
225
|
annotateDevAccessRuns,
|
|
207
226
|
appDataDir,
|
|
208
|
-
findConventionSetup,
|
|
209
|
-
loadRepoSettings,
|
|
210
227
|
loadWorkspaceSettings,
|
|
211
|
-
runCursorWorktreeSetup,
|
|
212
228
|
sideboardReposDir,
|
|
213
229
|
sideboardWorkspacesDir,
|
|
214
230
|
threadLivePath,
|
|
215
231
|
threadsDir,
|
|
216
|
-
workspaceSettingsSourceLabel,
|
|
217
232
|
worktreesRoot
|
|
218
|
-
} from "./chunk-
|
|
233
|
+
} from "./chunk-H5XL4KUS.js";
|
|
219
234
|
|
|
220
235
|
// src/orchestrator/orchestrator.ts
|
|
221
236
|
import { EventEmitter } from "events";
|
|
@@ -258,92 +273,9 @@ function formatInjectedNoticesForTurn(notices) {
|
|
|
258
273
|
].join("\n\n");
|
|
259
274
|
}
|
|
260
275
|
|
|
261
|
-
// src/orchestrator/task-state.ts
|
|
262
|
-
function isIncompleteTaskState(state) {
|
|
263
|
-
return state === "failed" || state === "canceled";
|
|
264
|
-
}
|
|
265
|
-
function needsCoordinatorAction(state) {
|
|
266
|
-
return isIncompleteTaskState(state) || state === "input-required";
|
|
267
|
-
}
|
|
268
|
-
var ASK_USER_TOOL = /(^|[_.:/])ask_user$/i;
|
|
269
|
-
function endedOnAskUser(parts) {
|
|
270
|
-
if (!parts?.length) return false;
|
|
271
|
-
for (let i = parts.length - 1; i >= 0; i--) {
|
|
272
|
-
const p = parts[i];
|
|
273
|
-
if (p.type === "tool" && !p.parentId) {
|
|
274
|
-
return ASK_USER_TOOL.test(p.name);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
function deriveTaskState(input) {
|
|
280
|
-
if (input.stillRunning) {
|
|
281
|
-
return input.status === "queued" ? "submitted" : "working";
|
|
282
|
-
}
|
|
283
|
-
switch (input.status) {
|
|
284
|
-
case "error":
|
|
285
|
-
case "broken":
|
|
286
|
-
return "failed";
|
|
287
|
-
case "stopped":
|
|
288
|
-
return "canceled";
|
|
289
|
-
default:
|
|
290
|
-
return endedOnAskUser(input.lastAgentParts) ? "input-required" : "completed";
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// src/mcp/wait-for-turn.ts
|
|
295
|
-
var MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
|
|
296
|
-
function mcpWaitForTurnTimeoutMs(requested) {
|
|
297
|
-
const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
|
|
298
|
-
if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
|
|
299
|
-
return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
|
|
300
|
-
}
|
|
301
|
-
var MCP_WAIT_STILL_RUNNING_HINT = "Child is still working. Call wait_for_turn again. Do not send_to_chat a check-in (that steers / interrupts) or assume a hang while progress is updating.";
|
|
302
|
-
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_chat a check-in (that steers / interrupts), force_stop, or assume it failed to start.";
|
|
303
|
-
var MCP_WAIT_STOPPED_HINT = "Child was stopped before the turn finished. Do not treat this as success. send_to_chat to resume, or tell the user.";
|
|
304
|
-
var MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
|
|
305
|
-
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.";
|
|
306
|
-
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_chat a check-in.";
|
|
307
|
-
function mcpWaitTaskHint(taskState, status) {
|
|
308
|
-
switch (taskState) {
|
|
309
|
-
case "submitted":
|
|
310
|
-
return MCP_WAIT_QUEUED_HINT;
|
|
311
|
-
case "working":
|
|
312
|
-
return MCP_WAIT_STILL_RUNNING_HINT;
|
|
313
|
-
case "input-required":
|
|
314
|
-
return MCP_WAIT_INPUT_REQUIRED_HINT;
|
|
315
|
-
case "canceled":
|
|
316
|
-
return MCP_WAIT_STOPPED_HINT;
|
|
317
|
-
case "failed":
|
|
318
|
-
return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
|
|
319
|
-
default:
|
|
320
|
-
return void 0;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
function nonemptyIso(value) {
|
|
324
|
-
const trimmed = value?.trim();
|
|
325
|
-
return trimmed ? trimmed : null;
|
|
326
|
-
}
|
|
327
|
-
function lastActivityAtForWait(stillRunning, liveUpdatedAt, fallbacks) {
|
|
328
|
-
if (stillRunning) return nonemptyIso(liveUpdatedAt);
|
|
329
|
-
for (const value of fallbacks) {
|
|
330
|
-
const iso = nonemptyIso(value);
|
|
331
|
-
if (iso) return iso;
|
|
332
|
-
}
|
|
333
|
-
return null;
|
|
334
|
-
}
|
|
335
|
-
function waitForTurnToolResult(input) {
|
|
336
|
-
return {
|
|
337
|
-
...input,
|
|
338
|
-
usage: input.usage ?? null,
|
|
339
|
-
hint: mcpWaitTaskHint(input.taskState, input.status),
|
|
340
|
-
incomplete: needsCoordinatorAction(input.taskState)
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
|
|
344
276
|
// src/orchestrator/orchestrator.ts
|
|
345
|
-
import { randomUUID as
|
|
346
|
-
import { existsSync as
|
|
277
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
278
|
+
import { existsSync as existsSync14 } from "fs";
|
|
347
279
|
|
|
348
280
|
// src/agents/spawn.ts
|
|
349
281
|
import { createInterface } from "readline";
|
|
@@ -377,6 +309,49 @@ async function withTimeout(promise, ms, label) {
|
|
|
377
309
|
}
|
|
378
310
|
}
|
|
379
311
|
|
|
312
|
+
// src/agents/stdout-replay.ts
|
|
313
|
+
var REPLAY_MIN_CHARS = 80;
|
|
314
|
+
function textSoFar(parts, parentId) {
|
|
315
|
+
const parent = parentId?.trim() || "";
|
|
316
|
+
let out = "";
|
|
317
|
+
for (const part of parts) {
|
|
318
|
+
if (part.type !== "text") continue;
|
|
319
|
+
if ((messagePartParentId(part) ?? "") !== parent) continue;
|
|
320
|
+
out += part.text;
|
|
321
|
+
}
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
function replaySized(text) {
|
|
325
|
+
return text.trim().length >= REPLAY_MIN_CHARS;
|
|
326
|
+
}
|
|
327
|
+
function tailAfterAccumulated(accumulated, incoming) {
|
|
328
|
+
if (incoming.startsWith(accumulated)) return incoming.slice(accumulated.length);
|
|
329
|
+
const acc = accumulated.trim();
|
|
330
|
+
const inc = incoming.trim();
|
|
331
|
+
if (!acc || !inc.startsWith(acc)) return null;
|
|
332
|
+
const at = incoming.indexOf(acc);
|
|
333
|
+
if (at < 0) return null;
|
|
334
|
+
return incoming.slice(at + acc.length);
|
|
335
|
+
}
|
|
336
|
+
function foldDuplicateStdout(accumulated, incoming) {
|
|
337
|
+
if (!incoming) return null;
|
|
338
|
+
if (!accumulated) return incoming;
|
|
339
|
+
const accTrim = accumulated.trim();
|
|
340
|
+
const incTrim = incoming.trim();
|
|
341
|
+
if (!incTrim) return incoming;
|
|
342
|
+
if (incTrim === accTrim) return null;
|
|
343
|
+
if (!replaySized(incoming)) return incoming;
|
|
344
|
+
if (accTrim.includes(incTrim)) return null;
|
|
345
|
+
let tail = tailAfterAccumulated(accumulated, incoming);
|
|
346
|
+
if (tail == null) return incoming;
|
|
347
|
+
for (let i = 0; i < 3 && tail.trim(); i++) {
|
|
348
|
+
const next = tailAfterAccumulated(accumulated, tail);
|
|
349
|
+
if (next == null) break;
|
|
350
|
+
tail = next;
|
|
351
|
+
}
|
|
352
|
+
return tail.trim() ? tail : null;
|
|
353
|
+
}
|
|
354
|
+
|
|
380
355
|
// src/hook/agent-host-env.ts
|
|
381
356
|
import { delimiter } from "path";
|
|
382
357
|
var DROP_EXACT = /* @__PURE__ */ new Set([
|
|
@@ -433,56 +408,6 @@ function sanitizeAgentHostEnv(env, cwd) {
|
|
|
433
408
|
return env;
|
|
434
409
|
}
|
|
435
410
|
|
|
436
|
-
// src/hook/worktree-pkg-cache.ts
|
|
437
|
-
import { existsSync, mkdirSync } from "fs";
|
|
438
|
-
import { join as join2 } from "path";
|
|
439
|
-
var WORKTREE_PKG_CACHE_REL = ".context/.sideboard/pkg-cache";
|
|
440
|
-
function worktreePkgCacheRoot(worktreePath) {
|
|
441
|
-
return join2(worktreePath, WORKTREE_PKG_CACHE_REL);
|
|
442
|
-
}
|
|
443
|
-
function worktreePkgCacheEnv(worktreePath) {
|
|
444
|
-
const root = worktreePkgCacheRoot(worktreePath);
|
|
445
|
-
const store = join2(root, "pnpm-store");
|
|
446
|
-
const pnpmCache = join2(root, "pnpm");
|
|
447
|
-
const npmCache = join2(root, "npm");
|
|
448
|
-
const nodeGyp = join2(root, "node-gyp");
|
|
449
|
-
const electron = join2(root, "electron");
|
|
450
|
-
return {
|
|
451
|
-
npm_config_store_dir: store,
|
|
452
|
-
PNPM_STORE_DIR: store,
|
|
453
|
-
npm_config_cache_dir: pnpmCache,
|
|
454
|
-
npm_config_cache: npmCache,
|
|
455
|
-
npm_config_devdir: nodeGyp,
|
|
456
|
-
// @electron/get — do not set ELECTRON_CACHE (stripNestedElectronEnv).
|
|
457
|
-
electron_config_cache: electron
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
function isSet(value) {
|
|
461
|
-
return value != null && value.trim() !== "";
|
|
462
|
-
}
|
|
463
|
-
function fillGroup(env, keys, values) {
|
|
464
|
-
if (keys.some((key) => isSet(env[key]))) return;
|
|
465
|
-
for (const key of keys) {
|
|
466
|
-
const value = values[key];
|
|
467
|
-
if (value) env[key] = value;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
function ensureWorktreePkgCacheDirs(worktreePath) {
|
|
471
|
-
if (!existsSync(worktreePath)) return;
|
|
472
|
-
mkdirSync(worktreePkgCacheRoot(worktreePath), { recursive: true });
|
|
473
|
-
}
|
|
474
|
-
function applyWorktreePkgCacheEnv(env, worktreePath) {
|
|
475
|
-
const cwd = worktreePath.trim();
|
|
476
|
-
if (!cwd) return;
|
|
477
|
-
const next = worktreePkgCacheEnv(cwd);
|
|
478
|
-
fillGroup(env, ["npm_config_store_dir", "PNPM_STORE_DIR"], next);
|
|
479
|
-
fillGroup(env, ["npm_config_cache_dir"], next);
|
|
480
|
-
fillGroup(env, ["npm_config_cache"], next);
|
|
481
|
-
fillGroup(env, ["npm_config_devdir"], next);
|
|
482
|
-
fillGroup(env, ["electron_config_cache"], next);
|
|
483
|
-
ensureWorktreePkgCacheDirs(cwd);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
411
|
// src/agents/cursor-stream-coalesce.ts
|
|
487
412
|
function isTextEvent(event) {
|
|
488
413
|
return event.type === "stdout" || event.type === "thinking";
|
|
@@ -560,9 +485,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
560
485
|
`Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
|
|
561
486
|
);
|
|
562
487
|
}
|
|
563
|
-
const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-
|
|
488
|
+
const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-EWFL4HPW.js");
|
|
564
489
|
if (isGlobalThread2(thread)) {
|
|
565
|
-
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-
|
|
490
|
+
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-4GHKCVRH.js");
|
|
566
491
|
ensureGlobalCoordinatorCwd2(
|
|
567
492
|
isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
|
|
568
493
|
);
|
|
@@ -669,7 +594,8 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
669
594
|
events = normalizeParseResult(parseBrightsyCliLine(line));
|
|
670
595
|
}
|
|
671
596
|
}
|
|
672
|
-
for (const
|
|
597
|
+
for (const event of events) {
|
|
598
|
+
let parsed = event;
|
|
673
599
|
if (parsed.type === "session_id") {
|
|
674
600
|
sessionId = parsed.data;
|
|
675
601
|
outbound.push(parsed);
|
|
@@ -685,9 +611,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
685
611
|
if (thread.agent === "brightsy" && isBrightsyNdjsonLine(parsed.data)) {
|
|
686
612
|
continue;
|
|
687
613
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
}
|
|
614
|
+
const folded = foldDuplicateStdout(textSoFar(parts, parsed.parentId), parsed.data);
|
|
615
|
+
if (folded == null) continue;
|
|
616
|
+
if (folded !== parsed.data) parsed = { ...parsed, data: folded };
|
|
691
617
|
}
|
|
692
618
|
parts = applyAgentEvent(parts, parsed);
|
|
693
619
|
if (parsed.type === "stdout") {
|
|
@@ -729,2117 +655,185 @@ var AGENT_GIT_ACTIONS = [
|
|
|
729
655
|
"ready-for-review",
|
|
730
656
|
"merge"
|
|
731
657
|
];
|
|
732
|
-
function agentGitPrompt(action, opts) {
|
|
733
|
-
switch (action) {
|
|
734
|
-
case "commit-push":
|
|
735
|
-
return "Commit and push.";
|
|
736
|
-
case "create-draft":
|
|
737
|
-
return "Commit, push, and open a draft PR.";
|
|
738
|
-
case "create-web":
|
|
739
|
-
return "Commit, push, and open a PR in the browser.";
|
|
740
|
-
case "resolve-conflicts": {
|
|
741
|
-
const base = opts?.prBase?.trim().replace(/^refs\/heads\//, "");
|
|
742
|
-
const named = base ? ` (${base})` : "";
|
|
743
|
-
return `Merge the remote branch${named} into your branch and resolve conflicts. Then, commit and push your changes.`;
|
|
744
|
-
}
|
|
745
|
-
case "ready-for-review":
|
|
746
|
-
return "Ready for review.";
|
|
747
|
-
case "merge":
|
|
748
|
-
return "Merge PR.";
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
function resolveSidebarGitPrompt(action, opts) {
|
|
752
|
-
if (action === "create-pr" || action === "create-draft" || action === "create-web") {
|
|
753
|
-
const custom = opts?.createPr?.trim();
|
|
754
|
-
if (custom) return custom;
|
|
755
|
-
return agentGitPrompt(action === "create-pr" ? "create-draft" : action);
|
|
756
|
-
}
|
|
757
|
-
if (action === "resolve-conflicts") {
|
|
758
|
-
const custom = opts?.resolveMergeConflicts?.trim();
|
|
759
|
-
if (custom) return custom;
|
|
760
|
-
return agentGitPrompt("resolve-conflicts", opts);
|
|
761
|
-
}
|
|
762
|
-
return agentGitPrompt(action, opts);
|
|
763
|
-
}
|
|
764
|
-
var RESOLVE_CONFLICTS_RE = /^Merge the remote branch(?: \(([^)]+)\))? into your branch and resolve conflicts\. Then, commit and push your changes\.$/;
|
|
765
|
-
function gitActionExpansion(action, base) {
|
|
766
|
-
switch (action) {
|
|
767
|
-
case "commit-push":
|
|
768
|
-
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.";
|
|
769
|
-
case "create-draft":
|
|
770
|
-
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.";
|
|
771
|
-
case "create-web":
|
|
772
|
-
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.";
|
|
773
|
-
case "resolve-conflicts":
|
|
774
|
-
return `Fetch the PR base${base ? ` (\`${base}\`)` : ""}, merge it into this branch, resolve conflicts carefully, then commit and push until the PR is mergeable.`;
|
|
775
|
-
case "ready-for-review":
|
|
776
|
-
return "This phrase is the explicit ask to mark this thread's draft pull request ready for review on GitHub (`gh pr ready -R <origin-owner/name>` or `gh pr ready`). Update title/body if the purpose drifted. Do not merge.";
|
|
777
|
-
case "merge":
|
|
778
|
-
return "This phrase is the explicit ask to merge this thread's open pull request on GitHub. If `gh stack view` shows a stack, use `gh stack merge`; otherwise `gh pr merge` (respect repo defaults / squash vs merge). Do not force-push main/master or merge locally into the main checkout.";
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
function expandCanonicalGitRequest(prompt) {
|
|
782
|
-
const trimmed = prompt.trim();
|
|
783
|
-
let action = null;
|
|
784
|
-
let base = null;
|
|
785
|
-
for (const candidate of AGENT_GIT_ACTIONS) {
|
|
786
|
-
if (candidate === "resolve-conflicts") continue;
|
|
787
|
-
if (agentGitPrompt(candidate) === trimmed) {
|
|
788
|
-
action = candidate;
|
|
789
|
-
break;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
if (!action) {
|
|
793
|
-
const m = RESOLVE_CONFLICTS_RE.exec(trimmed);
|
|
794
|
-
if (m) {
|
|
795
|
-
action = "resolve-conflicts";
|
|
796
|
-
base = m[1] ?? null;
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
let expansion = action ? gitActionExpansion(action, base) : null;
|
|
800
|
-
if (!expansion && trimmed === "Address review comments.") {
|
|
801
|
-
expansion = "Read the PR review feedback (`gh pr view --comments`, `gh api` review comments), make the requested changes, commit, and push.";
|
|
802
|
-
} else if (!expansion && FIX_CI_RE.test(trimmed)) {
|
|
803
|
-
expansion = "Investigate that failing check (`gh pr checks`, `gh run view --log-failed`), fix it, commit, and push. Loop only if a goal to keep going until it passes was given.";
|
|
804
|
-
}
|
|
805
|
-
if (!expansion) return prompt;
|
|
806
|
-
return `${trimmed}
|
|
807
|
-
|
|
808
|
-
(Sideboard git request \u2014 do not ask for clarification: ${expansion})`;
|
|
809
|
-
}
|
|
810
|
-
var FIX_CI_RE = /^Fix CI: .+\.$/;
|
|
811
|
-
|
|
812
|
-
// src/git/pr-merge-archive.ts
|
|
813
|
-
function normalizePrState(state) {
|
|
814
|
-
return (state ?? "").trim().toUpperCase();
|
|
815
|
-
}
|
|
816
|
-
function samePrIdentity(a, b) {
|
|
817
|
-
const left = (a ?? "").trim().replace(/\/+$/, "");
|
|
818
|
-
const right = (b ?? "").trim().replace(/\/+$/, "");
|
|
819
|
-
if (!left || !right) return false;
|
|
820
|
-
if (left === right) return true;
|
|
821
|
-
const ln = left.match(/\/pull\/(\d+)/i)?.[1];
|
|
822
|
-
const rn = right.match(/\/pull\/(\d+)/i)?.[1];
|
|
823
|
-
return Boolean(ln && rn && ln === rn);
|
|
824
|
-
}
|
|
825
|
-
function shouldStopRunOnPrRetarget(previousPrUrl, nextPrUrl) {
|
|
826
|
-
const prev = (previousPrUrl ?? "").trim();
|
|
827
|
-
const next = (nextPrUrl ?? "").trim();
|
|
828
|
-
if (!prev || !next) return false;
|
|
829
|
-
return !samePrIdentity(prev, next);
|
|
830
|
-
}
|
|
831
|
-
function shouldPersistFetchedPrMeta(opts) {
|
|
832
|
-
const meta = opts.metaUrl?.trim() || "";
|
|
833
|
-
if (!meta) return false;
|
|
834
|
-
const head = opts.headPrUrl?.trim() || "";
|
|
835
|
-
if (head) return samePrIdentity(meta, head);
|
|
836
|
-
const live = opts.livePrUrl?.trim() || "";
|
|
837
|
-
if (live) return samePrIdentity(meta, live);
|
|
838
|
-
return true;
|
|
839
|
-
}
|
|
840
|
-
function sameLoginList(a, b) {
|
|
841
|
-
const left = [...a ?? []].map((s) => s.trim().toLowerCase()).filter(Boolean).sort();
|
|
842
|
-
const right = [...b ?? []].map((s) => s.trim().toLowerCase()).filter(Boolean).sort();
|
|
843
|
-
return left.length === right.length && left.every((login, i) => login === right[i]);
|
|
844
|
-
}
|
|
845
|
-
function threadPrMetaPatch(thread, meta) {
|
|
846
|
-
const prevState = normalizePrState(thread.prState);
|
|
847
|
-
const nextState = normalizePrState(meta.state);
|
|
848
|
-
const patch = {};
|
|
849
|
-
if (meta.url && meta.url !== thread.prUrl) patch.prUrl = meta.url;
|
|
850
|
-
if (meta.title && meta.title !== thread.prTitle) patch.prTitle = meta.title;
|
|
851
|
-
if (nextState && nextState !== prevState) patch.prState = nextState;
|
|
852
|
-
const nextDraft = Boolean(meta.isDraft) && nextState !== "MERGED" && nextState !== "CLOSED";
|
|
853
|
-
if (nextDraft !== Boolean(thread.prIsDraft)) patch.prIsDraft = nextDraft;
|
|
854
|
-
const nextAuthor = meta.authorLogin?.trim() || "";
|
|
855
|
-
if (nextAuthor && nextAuthor !== (thread.prAuthorLogin ?? "").trim()) {
|
|
856
|
-
patch.prAuthorLogin = nextAuthor;
|
|
857
|
-
}
|
|
858
|
-
const nextReviewers = meta.reviewerLogins ?? [];
|
|
859
|
-
if (!sameLoginList(nextReviewers, thread.prReviewerLogins)) {
|
|
860
|
-
patch.prReviewerLogins = nextReviewers;
|
|
861
|
-
}
|
|
862
|
-
const nextDecision = meta.reviewDecision ?? null;
|
|
863
|
-
if ((thread.prReviewDecision ?? null) !== nextDecision) {
|
|
864
|
-
patch.prReviewDecision = nextDecision;
|
|
865
|
-
}
|
|
866
|
-
const nextMergeable = meta.mergeable ?? null;
|
|
867
|
-
if ((thread.prMergeable ?? null) !== nextMergeable) {
|
|
868
|
-
patch.prMergeable = nextMergeable;
|
|
869
|
-
}
|
|
870
|
-
const nextMergeState = meta.mergeStateStatus ?? null;
|
|
871
|
-
if ((thread.prMergeStateStatus ?? null) !== nextMergeState) {
|
|
872
|
-
patch.prMergeStateStatus = nextMergeState;
|
|
873
|
-
}
|
|
874
|
-
const nextQueued = Boolean(meta.isInMergeQueue);
|
|
875
|
-
if (Boolean(thread.prIsInMergeQueue) !== nextQueued) {
|
|
876
|
-
patch.prIsInMergeQueue = nextQueued;
|
|
877
|
-
}
|
|
878
|
-
const nextLabels = meta.labels ?? [];
|
|
879
|
-
if (!sameLoginList(nextLabels, thread.prLabels)) {
|
|
880
|
-
patch.prLabels = nextLabels;
|
|
881
|
-
}
|
|
882
|
-
const mergeState = (nextMergeState ?? "").toUpperCase();
|
|
883
|
-
if (mergeState === "CLEAN" && thread.prChecksFailed) {
|
|
884
|
-
patch.prChecksFailed = false;
|
|
885
|
-
} else if ((mergeState === "UNSTABLE" || mergeState === "BLOCKED" && (nextDecision ?? "").toUpperCase() === "APPROVED") && !thread.prChecksFailed) {
|
|
886
|
-
patch.prChecksFailed = true;
|
|
887
|
-
}
|
|
888
|
-
if (thread.skipAutoArchiveOnMerge && nextState && nextState !== "MERGED" && nextState !== "CLOSED") {
|
|
889
|
-
patch.skipAutoArchiveOnMerge = false;
|
|
890
|
-
}
|
|
891
|
-
return patch;
|
|
892
|
-
}
|
|
893
|
-
function threadPrChecksFailedPatch(thread, failed) {
|
|
894
|
-
if (thread.prChecksFailed === failed) return {};
|
|
895
|
-
return { prChecksFailed: failed };
|
|
896
|
-
}
|
|
897
|
-
function shouldAutoArchiveOnPrMerge(opts) {
|
|
898
|
-
if (!opts.autoArchiveEnabled) return false;
|
|
899
|
-
if (opts.isGlobal) return false;
|
|
900
|
-
if (opts.skipAutoArchiveOnMerge) return false;
|
|
901
|
-
if (opts.threadStatus === "archived") return false;
|
|
902
|
-
const next = normalizePrState(opts.nextPrState);
|
|
903
|
-
if (next !== "MERGED") return false;
|
|
904
|
-
return normalizePrState(opts.previousPrState) !== "MERGED";
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
// src/hook/conductor.ts
|
|
908
|
-
import {
|
|
909
|
-
copyFileSync,
|
|
910
|
-
existsSync as existsSync4,
|
|
911
|
-
mkdirSync as mkdirSync5,
|
|
912
|
-
readdirSync as readdirSync2,
|
|
913
|
-
readFileSync as readFileSync4,
|
|
914
|
-
realpathSync
|
|
915
|
-
} from "fs";
|
|
916
|
-
import { execFileSync } from "child_process";
|
|
917
|
-
import { createServer } from "net";
|
|
918
|
-
import { basename, dirname as dirname4, join as join6, resolve, sep } from "path";
|
|
919
|
-
import { execa as execa2 } from "execa";
|
|
920
|
-
import { createInterface as createInterface2 } from "readline";
|
|
921
|
-
|
|
922
|
-
// src/review/request-review.ts
|
|
923
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
924
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
925
|
-
import { dirname, join as join3 } from "path";
|
|
926
|
-
|
|
927
|
-
// src/threads/chat-tabs.ts
|
|
928
|
-
import { randomUUID } from "crypto";
|
|
929
|
-
|
|
930
|
-
// src/composer/context-estimate.ts
|
|
931
|
-
var CHARS_PER_CONTEXT_TOKEN = 4;
|
|
932
|
-
function estimateMessageChars(message) {
|
|
933
|
-
let n = message.text.length + 16;
|
|
934
|
-
for (const part of message.parts ?? []) {
|
|
935
|
-
n += estimatePartChars(part);
|
|
936
|
-
}
|
|
937
|
-
return n;
|
|
938
|
-
}
|
|
939
|
-
function estimatePartChars(part) {
|
|
940
|
-
switch (part.type) {
|
|
941
|
-
case "text":
|
|
942
|
-
case "thinking":
|
|
943
|
-
return part.text.length;
|
|
944
|
-
case "tool": {
|
|
945
|
-
const input = part.input ? JSON.stringify(part.input) : "";
|
|
946
|
-
return part.name.length + (part.description?.length ?? 0) + (part.detail?.length ?? 0) + (part.result?.length ?? 0) + input.length + 32;
|
|
947
|
-
}
|
|
948
|
-
default:
|
|
949
|
-
return 0;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
function estimateThreadChars(messages) {
|
|
953
|
-
return messages.reduce((sum, m) => sum + estimateMessageChars(m), 0);
|
|
954
|
-
}
|
|
955
|
-
function estimateOccupancyTokens(messages) {
|
|
956
|
-
return Math.ceil(estimateThreadChars(messages) / CHARS_PER_CONTEXT_TOKEN);
|
|
957
|
-
}
|
|
958
|
-
function applyForwardOccupancy(messages) {
|
|
959
|
-
const tokens = estimateOccupancyTokens(messages);
|
|
960
|
-
if (tokens <= 0) return messages;
|
|
961
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
962
|
-
const m = messages[i];
|
|
963
|
-
if (m?.role !== "agent" || !m.usage) continue;
|
|
964
|
-
const current = contextTokens(m.usage);
|
|
965
|
-
if (tokens >= current) return messages;
|
|
966
|
-
const next = messages.slice();
|
|
967
|
-
next[i] = { ...m, usage: { ...m.usage, lastRequestTokens: tokens } };
|
|
968
|
-
return next;
|
|
969
|
-
}
|
|
970
|
-
return messages;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
// src/composer/summarize.ts
|
|
974
|
-
var MAX_TRANSCRIPT_CHARS = 12e4;
|
|
975
|
-
var MAX_SUMMARY_CHARS = 6e3;
|
|
976
|
-
var CLAUDE_SUMMARY_TIMEOUT_MS = 2e4;
|
|
977
|
-
async function summarizeConversation(transcript, opts) {
|
|
978
|
-
const clipped = transcript.length > MAX_TRANSCRIPT_CHARS ? `${transcript.slice(0, MAX_TRANSCRIPT_CHARS)}
|
|
979
|
-
|
|
980
|
-
[\u2026truncated\u2026]` : transcript;
|
|
981
|
-
const claudeSummary = await tryClaudeSummary(clipped, opts);
|
|
982
|
-
if (claudeSummary) {
|
|
983
|
-
return { summary: clipSummary(claudeSummary), method: "claude" };
|
|
984
|
-
}
|
|
985
|
-
return { summary: clipSummary(extractiveSummary(clipped)), method: "extractive" };
|
|
986
|
-
}
|
|
987
|
-
async function tryClaudeSummary(transcript, opts) {
|
|
988
|
-
ensureAgentPath();
|
|
989
|
-
const prompt = [
|
|
990
|
-
"Summarize this coding-agent conversation for continuity in a fresh session.",
|
|
991
|
-
"Use concise markdown bullets covering:",
|
|
992
|
-
"- Goal / request",
|
|
993
|
-
"- Key decisions and constraints",
|
|
994
|
-
"- Files / areas touched",
|
|
995
|
-
"- Current state and open tasks",
|
|
996
|
-
"- Anything the next turn must remember",
|
|
997
|
-
"Do not invent details. No preamble.",
|
|
998
|
-
"",
|
|
999
|
-
"Transcript:",
|
|
1000
|
-
transcript
|
|
1001
|
-
].join("\n");
|
|
1002
|
-
try {
|
|
1003
|
-
const { stdout, exitCode } = await run(
|
|
1004
|
-
"claude",
|
|
1005
|
-
[
|
|
1006
|
-
"-p",
|
|
1007
|
-
prompt,
|
|
1008
|
-
"--output-format",
|
|
1009
|
-
"text",
|
|
1010
|
-
"--permission-mode",
|
|
1011
|
-
"bypassPermissions",
|
|
1012
|
-
"--model",
|
|
1013
|
-
"haiku"
|
|
1014
|
-
],
|
|
1015
|
-
{
|
|
1016
|
-
cwd: opts?.cwd,
|
|
1017
|
-
reject: false,
|
|
1018
|
-
timeoutMs: opts?.timeoutMs ?? CLAUDE_SUMMARY_TIMEOUT_MS
|
|
1019
|
-
}
|
|
1020
|
-
);
|
|
1021
|
-
if (exitCode !== 0) return null;
|
|
1022
|
-
const text = stdout.trim();
|
|
1023
|
-
if (text.length < 40) return null;
|
|
1024
|
-
return text;
|
|
1025
|
-
} catch {
|
|
1026
|
-
return null;
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
function extractiveSummary(transcript) {
|
|
1030
|
-
const lines = transcript.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1031
|
-
const users = [];
|
|
1032
|
-
const tools = [];
|
|
1033
|
-
const agents = [];
|
|
1034
|
-
let mode = "none";
|
|
1035
|
-
for (const line of lines) {
|
|
1036
|
-
if (line.startsWith("### User")) {
|
|
1037
|
-
mode = "user";
|
|
1038
|
-
continue;
|
|
1039
|
-
}
|
|
1040
|
-
if (line.startsWith("### Agent") || line.startsWith("## Prior")) {
|
|
1041
|
-
mode = "agent";
|
|
1042
|
-
continue;
|
|
1043
|
-
}
|
|
1044
|
-
if (line.startsWith("- ") && line.includes(":")) {
|
|
1045
|
-
tools.push(line.replace(/^- /, ""));
|
|
1046
|
-
continue;
|
|
1047
|
-
}
|
|
1048
|
-
if (mode === "user" && users.length < 8) {
|
|
1049
|
-
users.push(line.slice(0, 240));
|
|
1050
|
-
} else if (mode === "agent" && agents.length < 6) {
|
|
1051
|
-
agents.push(line.slice(0, 240));
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
const uniqueTools = [...new Set(tools)].slice(0, 20);
|
|
1055
|
-
const parts = [
|
|
1056
|
-
"## Compacted context (extractive)",
|
|
1057
|
-
"",
|
|
1058
|
-
"### User goals",
|
|
1059
|
-
...users.length ? users.map((u) => `- ${u}`) : ["- (none captured)"],
|
|
1060
|
-
"",
|
|
1061
|
-
"### Recent agent notes",
|
|
1062
|
-
...agents.length ? agents.map((a) => `- ${a}`) : ["- (none captured)"]
|
|
1063
|
-
];
|
|
1064
|
-
if (uniqueTools.length) {
|
|
1065
|
-
parts.push("", "### Tools / files", ...uniqueTools.map((t) => `- ${t}`));
|
|
1066
|
-
}
|
|
1067
|
-
parts.push(
|
|
1068
|
-
"",
|
|
1069
|
-
"_Automatic extractive summary \u2014 Claude summarizer was unavailable._"
|
|
1070
|
-
);
|
|
1071
|
-
return parts.join("\n");
|
|
1072
|
-
}
|
|
1073
|
-
function clipSummary(text) {
|
|
1074
|
-
if (text.length <= MAX_SUMMARY_CHARS) return text;
|
|
1075
|
-
return `${text.slice(0, MAX_SUMMARY_CHARS)}
|
|
1076
|
-
|
|
1077
|
-
[\u2026summary truncated\u2026]`;
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
// src/composer/context-compact.ts
|
|
1081
|
-
var CONTEXT_COMPACT_CHARS = 4e5;
|
|
1082
|
-
var CONTEXT_KEEP_RECENT_CHARS = 24e3;
|
|
1083
|
-
var CONTEXT_KEEP_RECENT_MESSAGES = 12;
|
|
1084
|
-
var CONTEXT_MIN_MESSAGES = 10;
|
|
1085
|
-
var SESSION_RESET_OCCUPANCY_TOKENS = 75e4;
|
|
1086
|
-
function shouldCompactContext(messages, thresholds = {}) {
|
|
1087
|
-
const maxChars = thresholds.maxChars ?? CONTEXT_COMPACT_CHARS;
|
|
1088
|
-
const minMessages = thresholds.minMessages ?? CONTEXT_MIN_MESSAGES;
|
|
1089
|
-
if (messages.length < minMessages) return false;
|
|
1090
|
-
return estimateThreadChars(messages) >= maxChars;
|
|
1091
|
-
}
|
|
1092
|
-
function splitForCompaction(messages, thresholds = {}) {
|
|
1093
|
-
const keepChars = thresholds.keepRecentChars ?? CONTEXT_KEEP_RECENT_CHARS;
|
|
1094
|
-
const keepCount = thresholds.keepRecentMessages ?? CONTEXT_KEEP_RECENT_MESSAGES;
|
|
1095
|
-
if (messages.length === 0) return { older: [], recent: [] };
|
|
1096
|
-
let recentChars = 0;
|
|
1097
|
-
let cut = messages.length;
|
|
1098
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1099
|
-
const next = recentChars + estimateMessageChars(messages[i]);
|
|
1100
|
-
const kept = messages.length - i;
|
|
1101
|
-
if (kept >= keepCount && next > keepChars) break;
|
|
1102
|
-
recentChars = next;
|
|
1103
|
-
cut = i;
|
|
1104
|
-
}
|
|
1105
|
-
const minRecent = Math.min(keepCount, messages.length);
|
|
1106
|
-
cut = Math.min(cut, messages.length - minRecent);
|
|
1107
|
-
if (cut <= 0) return { older: [], recent: messages };
|
|
1108
|
-
return {
|
|
1109
|
-
older: messages.slice(0, cut),
|
|
1110
|
-
recent: messages.slice(cut)
|
|
1111
|
-
};
|
|
1112
|
-
}
|
|
1113
|
-
function formatToolPart(t, detail) {
|
|
1114
|
-
if (detail === "summary") {
|
|
1115
|
-
const label = t.description || t.detail || t.name;
|
|
1116
|
-
const path = t.filePath ? ` (${t.filePath})` : "";
|
|
1117
|
-
return `- ${t.name}: ${label}${path}`;
|
|
1118
|
-
}
|
|
1119
|
-
const lines = [`#### Tool: ${t.name}`, `Status: ${t.status}`];
|
|
1120
|
-
if (t.description) lines.push(`Description: ${t.description}`);
|
|
1121
|
-
if (t.filePath) lines.push(`Path: ${t.filePath}`);
|
|
1122
|
-
if (t.input && Object.keys(t.input).length > 0) {
|
|
1123
|
-
lines.push("Input:");
|
|
1124
|
-
lines.push("```json");
|
|
1125
|
-
lines.push(clipToolResultForStore(JSON.stringify(t.input, null, 2)) ?? "");
|
|
1126
|
-
lines.push("```");
|
|
1127
|
-
} else if (t.detail) {
|
|
1128
|
-
lines.push(`Detail: ${t.detail}`);
|
|
1129
|
-
}
|
|
1130
|
-
if (t.result != null && t.result !== "") {
|
|
1131
|
-
lines.push("Result:");
|
|
1132
|
-
lines.push("```");
|
|
1133
|
-
lines.push(t.result);
|
|
1134
|
-
lines.push("```");
|
|
1135
|
-
}
|
|
1136
|
-
return lines.join("\n");
|
|
1137
|
-
}
|
|
1138
|
-
function formatMessagesAsTranscript(messages, opts) {
|
|
1139
|
-
const tools = opts?.tools ?? "full";
|
|
1140
|
-
const thinking = opts?.thinking ?? true;
|
|
1141
|
-
return messages.map((m) => formatMessageBlock(m, { tools, thinking })).join("\n\n");
|
|
1142
|
-
}
|
|
1143
|
-
function formatMessageBlock(m, opts) {
|
|
1144
|
-
if (m.role === "summary") return `## Prior summary
|
|
1145
|
-
${m.text}`;
|
|
1146
|
-
if (m.role === "user") return `### User
|
|
1147
|
-
${m.text}`;
|
|
1148
|
-
const bits = [];
|
|
1149
|
-
if (m.text?.trim()) {
|
|
1150
|
-
bits.push(`### Agent
|
|
1151
|
-
${m.text}`);
|
|
1152
|
-
} else {
|
|
1153
|
-
bits.push("### Agent");
|
|
1154
|
-
}
|
|
1155
|
-
if (opts.thinking) {
|
|
1156
|
-
for (const th of m.parts ?? []) {
|
|
1157
|
-
if (th.type !== "thinking" || !th.text.trim()) continue;
|
|
1158
|
-
bits.push("Thinking:");
|
|
1159
|
-
bits.push(th.text);
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
const toolParts = (m.parts ?? []).filter(
|
|
1163
|
-
(p) => p.type === "tool"
|
|
1164
|
-
);
|
|
1165
|
-
if (opts.tools !== "none" && toolParts.length > 0) {
|
|
1166
|
-
if (opts.tools === "summary") bits.push("Tools:");
|
|
1167
|
-
for (const t of toolParts) {
|
|
1168
|
-
bits.push(formatToolPart(t, opts.tools));
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
return bits.join("\n");
|
|
1172
|
-
}
|
|
1173
|
-
var SEED_MAX_CHARS = 6e4;
|
|
1174
|
-
var SEED_FULL_TOOL_MESSAGES = 6;
|
|
1175
|
-
function budgetSeedBlocks(messages, blocks, maxChars) {
|
|
1176
|
-
const keep = new Array(blocks.length).fill(false);
|
|
1177
|
-
let used = 0;
|
|
1178
|
-
for (let i = 0; i < blocks.length; i++) {
|
|
1179
|
-
if (messages[i].role === "summary") {
|
|
1180
|
-
keep[i] = true;
|
|
1181
|
-
used += blocks[i].length + 2;
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
1185
|
-
if (keep[i]) continue;
|
|
1186
|
-
const next = used + blocks[i].length + 2;
|
|
1187
|
-
if (next > maxChars && used > 0) continue;
|
|
1188
|
-
keep[i] = true;
|
|
1189
|
-
used = next;
|
|
1190
|
-
}
|
|
1191
|
-
const dropped = keep.filter((k) => !k).length;
|
|
1192
|
-
const marker = `_(${dropped} older message${dropped === 1 ? "" : "s"} omitted for length)_`;
|
|
1193
|
-
const kept = [];
|
|
1194
|
-
let markerPlaced = false;
|
|
1195
|
-
for (let i = 0; i < blocks.length; i++) {
|
|
1196
|
-
if (!keep[i]) continue;
|
|
1197
|
-
if (dropped > 0 && !markerPlaced && messages[i].role !== "summary") {
|
|
1198
|
-
kept.push(marker);
|
|
1199
|
-
markerPlaced = true;
|
|
1200
|
-
}
|
|
1201
|
-
kept.push(blocks[i]);
|
|
1202
|
-
}
|
|
1203
|
-
if (dropped > 0 && !markerPlaced) kept.push(marker);
|
|
1204
|
-
return kept.join("\n\n");
|
|
1205
|
-
}
|
|
1206
|
-
function buildSessionSeed(messages, opts) {
|
|
1207
|
-
if (messages.length === 0) return null;
|
|
1208
|
-
const tools = opts?.tools ?? "full";
|
|
1209
|
-
const fullCount = opts?.fullToolMessages ?? SEED_FULL_TOOL_MESSAGES;
|
|
1210
|
-
const maxChars = opts?.maxChars ?? SEED_MAX_CHARS;
|
|
1211
|
-
const olderTools = tools === "none" ? "none" : "summary";
|
|
1212
|
-
const fullFrom = Math.max(0, messages.length - fullCount);
|
|
1213
|
-
const blocks = messages.map(
|
|
1214
|
-
(m, i) => formatMessageBlock(m, {
|
|
1215
|
-
tools: i >= fullFrom ? tools : olderTools,
|
|
1216
|
-
thinking: false
|
|
1217
|
-
})
|
|
1218
|
-
);
|
|
1219
|
-
const body = budgetSeedBlocks(messages, blocks, maxChars);
|
|
1220
|
-
if (!body.trim()) return null;
|
|
1221
|
-
return [
|
|
1222
|
-
"Sideboard conversation context (restored after compaction or a new session):",
|
|
1223
|
-
"",
|
|
1224
|
-
body,
|
|
1225
|
-
"",
|
|
1226
|
-
"Continue from this context. Do not repeat the summary unless asked."
|
|
1227
|
-
].join("\n");
|
|
1228
|
-
}
|
|
1229
|
-
var BRIGHTSY_SUMMARIZE_CONTEXT_TOOL = "summarize_context";
|
|
1230
|
-
function extractBrightsyContextSummary(result) {
|
|
1231
|
-
if (!result?.trim()) return null;
|
|
1232
|
-
const trimmed = result.trim();
|
|
1233
|
-
const lower = trimmed.toLowerCase();
|
|
1234
|
-
if (lower.startsWith("context summarization failed") || lower.startsWith("nothing to summarize") || lower.startsWith("no messages found") || lower.startsWith("messages are required") || lower.startsWith("agent id is required")) {
|
|
1235
|
-
return null;
|
|
1236
|
-
}
|
|
1237
|
-
try {
|
|
1238
|
-
const parsed = JSON.parse(trimmed);
|
|
1239
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1240
|
-
if (parsed.error != null && typeof parsed.context_summary !== "string") {
|
|
1241
|
-
return null;
|
|
1242
|
-
}
|
|
1243
|
-
if (typeof parsed.context_summary === "string" && parsed.context_summary.trim()) {
|
|
1244
|
-
return parsed.context_summary.trim();
|
|
1245
|
-
}
|
|
1246
|
-
return null;
|
|
1247
|
-
}
|
|
1248
|
-
} catch {
|
|
1249
|
-
}
|
|
1250
|
-
if (trimmed.includes('"error"') && !trimmed.includes("context_summary")) {
|
|
1251
|
-
return null;
|
|
1252
|
-
}
|
|
1253
|
-
return trimmed;
|
|
1254
|
-
}
|
|
1255
|
-
function findLastBrightsyContextSummary(messages) {
|
|
1256
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1257
|
-
const message = messages[i];
|
|
1258
|
-
if (message?.role !== "agent") continue;
|
|
1259
|
-
for (const part of message.parts ?? []) {
|
|
1260
|
-
if (part.type !== "tool" || part.name !== BRIGHTSY_SUMMARIZE_CONTEXT_TOOL) {
|
|
1261
|
-
continue;
|
|
1262
|
-
}
|
|
1263
|
-
if (part.status === "error") continue;
|
|
1264
|
-
const text = extractBrightsyContextSummary(part.result);
|
|
1265
|
-
if (text) return { index: i, text };
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
return null;
|
|
1269
|
-
}
|
|
1270
|
-
function buildBrightsySessionSeed(messages, opts) {
|
|
1271
|
-
const match = findLastBrightsyContextSummary(messages);
|
|
1272
|
-
const tail = match ? messages.slice(match.index + 1) : messages;
|
|
1273
|
-
const budget = Math.max(1e3, (opts?.maxChars ?? SEED_MAX_CHARS) - (match?.text.length ?? 0));
|
|
1274
|
-
const tailBlocks = tail.map((m) => formatMessageBlock(m, { tools: "none", thinking: false }));
|
|
1275
|
-
const body = budgetSeedBlocks(tail, tailBlocks, budget);
|
|
1276
|
-
if (!match && !body.trim()) return null;
|
|
1277
|
-
const blocks = [
|
|
1278
|
-
"Sideboard conversation context (restored after compaction or a new session):",
|
|
1279
|
-
""
|
|
1280
|
-
];
|
|
1281
|
-
if (match) {
|
|
1282
|
-
blocks.push(`## Prior summary
|
|
1283
|
-
${match.text}`, "");
|
|
1284
|
-
}
|
|
1285
|
-
if (body.trim()) {
|
|
1286
|
-
blocks.push(body, "");
|
|
1287
|
-
}
|
|
1288
|
-
blocks.push("Continue from this context. Do not repeat the summary unless asked.");
|
|
1289
|
-
return blocks.join("\n");
|
|
1290
|
-
}
|
|
1291
|
-
function compactArchiveProse(message) {
|
|
1292
|
-
if (message.origin === "continue") return null;
|
|
1293
|
-
if (message.role === "summary") return null;
|
|
1294
|
-
let text = (message.text ?? "").trim();
|
|
1295
|
-
if (!text && message.role === "agent") {
|
|
1296
|
-
text = lastAssistantMessageText(message.parts).trim();
|
|
1297
|
-
}
|
|
1298
|
-
if (!text || isInternalAgentStatusText(text)) return null;
|
|
1299
|
-
return { role: message.role, text, ts: message.ts };
|
|
1300
|
-
}
|
|
1301
|
-
function messagesForCompactArchive(older) {
|
|
1302
|
-
const out = [];
|
|
1303
|
-
for (const message of older) {
|
|
1304
|
-
const prose = compactArchiveProse(message);
|
|
1305
|
-
if (prose) out.push(prose);
|
|
1306
|
-
}
|
|
1307
|
-
return out;
|
|
1308
|
-
}
|
|
1309
|
-
function appendCompactedMessages(existing, older) {
|
|
1310
|
-
const next = messagesForCompactArchive(older);
|
|
1311
|
-
if (!existing?.length) return next;
|
|
1312
|
-
if (!next.length) return existing;
|
|
1313
|
-
return [...existing, ...next];
|
|
1314
|
-
}
|
|
1315
|
-
function applyCompaction(messages, summaryText, thresholds = {}) {
|
|
1316
|
-
const { older, recent } = splitForCompaction(messages, thresholds);
|
|
1317
|
-
if (older.length === 0) return messages;
|
|
1318
|
-
const text = summaryText.trim();
|
|
1319
|
-
const summary = {
|
|
1320
|
-
role: "summary",
|
|
1321
|
-
text,
|
|
1322
|
-
parts: [compactSummaryPart(text)],
|
|
1323
|
-
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
1324
|
-
};
|
|
1325
|
-
return [summary, ...recent];
|
|
1326
|
-
}
|
|
1327
|
-
function lastRequestOccupancy(thread) {
|
|
1328
|
-
for (let i = thread.messages.length - 1; i >= 0; i--) {
|
|
1329
|
-
const usage = thread.messages[i]?.usage;
|
|
1330
|
-
if (usage) return contextTokens(usage);
|
|
1331
|
-
}
|
|
1332
|
-
return 0;
|
|
1333
|
-
}
|
|
1334
|
-
function shouldResetSessionForOccupancy(thread, occupancyTokens = SESSION_RESET_OCCUPANCY_TOKENS) {
|
|
1335
|
-
return lastRequestOccupancy(thread) >= occupancyTokens;
|
|
1336
|
-
}
|
|
1337
|
-
async function maybeCompactContext(thread, thresholds = {}, summarize = summarizeConversation) {
|
|
1338
|
-
if (!shouldCompactContext(thread.messages, thresholds)) {
|
|
1339
|
-
return { didCompact: false, thread };
|
|
1340
|
-
}
|
|
1341
|
-
const { older } = splitForCompaction(thread.messages, thresholds);
|
|
1342
|
-
if (older.length === 0) {
|
|
1343
|
-
return { didCompact: false, thread };
|
|
1344
|
-
}
|
|
1345
|
-
const transcript = formatMessagesAsTranscript(older, { tools: "summary" });
|
|
1346
|
-
const { summary, method } = await summarize(transcript, {
|
|
1347
|
-
cwd: thread.worktreePath
|
|
1348
|
-
});
|
|
1349
|
-
let messages = applyCompaction(thread.messages, summary, thresholds);
|
|
1350
|
-
const compactedMessages = appendCompactedMessages(thread.compactedMessages, older);
|
|
1351
|
-
const resetSession = shouldResetSessionForOccupancy({ messages: thread.messages });
|
|
1352
|
-
if (resetSession) {
|
|
1353
|
-
messages = applyForwardOccupancy(messages);
|
|
1354
|
-
}
|
|
1355
|
-
const next = {
|
|
1356
|
-
...thread,
|
|
1357
|
-
messages,
|
|
1358
|
-
...compactedMessages.length ? { compactedMessages } : { compactedMessages: void 0 },
|
|
1359
|
-
sessionId: resetSession ? null : thread.sessionId,
|
|
1360
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1361
|
-
};
|
|
1362
|
-
return {
|
|
1363
|
-
didCompact: true,
|
|
1364
|
-
thread: next,
|
|
1365
|
-
summary,
|
|
1366
|
-
method,
|
|
1367
|
-
olderCount: older.length
|
|
1368
|
-
};
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
// src/threads/worktree-runtime.ts
|
|
1372
|
-
function worktreeProcessPrefix(worktreePath) {
|
|
1373
|
-
return `wt:${normalizeWorktreePath(worktreePath)}`;
|
|
1374
|
-
}
|
|
1375
|
-
function worktreeSetupProcessKey(worktreePath) {
|
|
1376
|
-
return `${worktreeProcessPrefix(worktreePath)}:setup`;
|
|
1377
|
-
}
|
|
1378
|
-
function worktreeRunProcessKey(worktreePath, scriptName) {
|
|
1379
|
-
return `${worktreeProcessPrefix(worktreePath)}:run:${scriptName}`;
|
|
1380
|
-
}
|
|
1381
|
-
function worktreeDevProcessKey(worktreePath) {
|
|
1382
|
-
return `${worktreeProcessPrefix(worktreePath)}:dev`;
|
|
1383
|
-
}
|
|
1384
|
-
function isWorktreeRunProcessKey(key, worktreePath) {
|
|
1385
|
-
const prefix = worktreeProcessPrefix(worktreePath);
|
|
1386
|
-
return key.startsWith(`${prefix}:run:`) || key === `${prefix}:dev`;
|
|
1387
|
-
}
|
|
1388
|
-
function pickRichestSetupLog(snaps) {
|
|
1389
|
-
let best;
|
|
1390
|
-
for (const snap of snaps) {
|
|
1391
|
-
if (!snap.output && !snap.running && snap.exitCode == null) continue;
|
|
1392
|
-
if (!best) {
|
|
1393
|
-
best = snap;
|
|
1394
|
-
continue;
|
|
1395
|
-
}
|
|
1396
|
-
if (snap.running && !best.running) {
|
|
1397
|
-
best = snap;
|
|
1398
|
-
continue;
|
|
1399
|
-
}
|
|
1400
|
-
if (snap.running === best.running && snap.output.length > best.output.length) {
|
|
1401
|
-
best = snap;
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
return best;
|
|
1405
|
-
}
|
|
1406
|
-
function resolveWorktreeSetupLog(shared, legacyChatLogs) {
|
|
1407
|
-
if (shared.output || shared.running || shared.exitCode != null) return shared;
|
|
1408
|
-
return pickRichestSetupLog(legacyChatLogs);
|
|
1409
|
-
}
|
|
1410
|
-
function mergeWorktreeActiveRuns(threads) {
|
|
1411
|
-
const byName = /* @__PURE__ */ new Map();
|
|
1412
|
-
let devPort = null;
|
|
1413
|
-
for (const thread of threads) {
|
|
1414
|
-
if (thread.devPort != null && devPort == null) devPort = thread.devPort;
|
|
1415
|
-
for (const run2 of thread.activeRuns ?? []) {
|
|
1416
|
-
if (!byName.has(run2.scriptName)) byName.set(run2.scriptName, run2);
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
return { activeRuns: [...byName.values()], devPort };
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
// src/threads/chat-tabs.ts
|
|
1423
|
-
function sameWorktreePath(a, b) {
|
|
1424
|
-
return normalizeWorktreePath(a) === normalizeWorktreePath(b);
|
|
1425
|
-
}
|
|
1426
|
-
function takenTeamSlugsForChatTab(worktreePath) {
|
|
1427
|
-
const key = normalizeWorktreePath(worktreePath);
|
|
1428
|
-
const taken = /* @__PURE__ */ new Set();
|
|
1429
|
-
const dir = worktreeNameFromPath(key);
|
|
1430
|
-
if (dir) taken.add(dir.toLowerCase());
|
|
1431
|
-
for (const sibling of threadsSharingWorktree(key)) {
|
|
1432
|
-
for (const slug of takenSlugsFromThread(sibling)) {
|
|
1433
|
-
taken.add(slug);
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
return [...taken];
|
|
1437
|
-
}
|
|
1438
|
-
function requireThread(idOrRef) {
|
|
1439
|
-
const thread = findThreadByRef(idOrRef) ?? null;
|
|
1440
|
-
if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
|
|
1441
|
-
return thread;
|
|
1442
|
-
}
|
|
1443
|
-
function worktreeBindingFrom(from) {
|
|
1444
|
-
return {
|
|
1445
|
-
worktreePath: normalizeWorktreePath(from.worktreePath),
|
|
1446
|
-
repoPath: from.repoPath,
|
|
1447
|
-
branchName: from.branchName,
|
|
1448
|
-
sourceRef: from.sourceRef,
|
|
1449
|
-
// Legacy pinned-repo orchestration: new tabs become normal worktree agents.
|
|
1450
|
-
// Global: keep orchestration so chat tabs stay fleet orchestrators (MCP + Bash).
|
|
1451
|
-
sourceType: isGlobalThread(from) ? "orchestration" : from.sourceType === "orchestration" ? "branch" : from.sourceType,
|
|
1452
|
-
sourceIsFork: from.sourceIsFork,
|
|
1453
|
-
parentThreadId: from.parentThreadId,
|
|
1454
|
-
prUrl: from.prUrl,
|
|
1455
|
-
prTitle: from.prTitle,
|
|
1456
|
-
prState: from.prState,
|
|
1457
|
-
prIsDraft: from.prIsDraft,
|
|
1458
|
-
prAuthorLogin: from.prAuthorLogin,
|
|
1459
|
-
prReviewerLogins: from.prReviewerLogins,
|
|
1460
|
-
prReviewDecision: from.prReviewDecision,
|
|
1461
|
-
prMergeable: from.prMergeable,
|
|
1462
|
-
prMergeStateStatus: from.prMergeStateStatus,
|
|
1463
|
-
prIsInMergeQueue: from.prIsInMergeQueue,
|
|
1464
|
-
prLabels: from.prLabels,
|
|
1465
|
-
prChecksFailed: from.prChecksFailed,
|
|
1466
|
-
skipAutoArchiveOnMerge: from.skipAutoArchiveOnMerge,
|
|
1467
|
-
stackId: from.stackId,
|
|
1468
|
-
stackLayer: from.stackLayer,
|
|
1469
|
-
cowboy: from.cowboy
|
|
1470
|
-
};
|
|
1471
|
-
}
|
|
1472
|
-
function threadsSharingWorktree(worktreePath) {
|
|
1473
|
-
if (!worktreePath) return [];
|
|
1474
|
-
const key = normalizeWorktreePath(worktreePath);
|
|
1475
|
-
return listThreads({ includeArchived: true }).filter(
|
|
1476
|
-
(t) => Boolean(t.worktreePath) && sameWorktreePath(t.worktreePath, key) && t.status !== "archived"
|
|
1477
|
-
).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
1478
|
-
}
|
|
1479
|
-
function formatTranscriptMarkdown(title, messages) {
|
|
1480
|
-
const body = formatMessagesAsTranscript(messages, { tools: "full" });
|
|
1481
|
-
return [`# Transcript: ${title}`, "", `Forked ${(/* @__PURE__ */ new Date()).toISOString()}`, "", body].join("\n");
|
|
1482
|
-
}
|
|
1483
|
-
function forkMessageSlice(from, throughIndex) {
|
|
1484
|
-
const through = throughIndex === void 0 ? from.messages.length - 1 : Math.min(throughIndex, from.messages.length - 1);
|
|
1485
|
-
return through >= 0 ? from.messages.slice(0, through + 1) : [];
|
|
1486
|
-
}
|
|
1487
|
-
function buildForkTranscriptAttachment(baseTitle, messages) {
|
|
1488
|
-
const title = baseTitle || "Chat";
|
|
1489
|
-
return {
|
|
1490
|
-
id: randomUUID(),
|
|
1491
|
-
name: `Transcript of ${title}.md`,
|
|
1492
|
-
kind: "transcript",
|
|
1493
|
-
content: formatTranscriptMarkdown(title, messages)
|
|
1494
|
-
};
|
|
1495
|
-
}
|
|
1496
|
-
function createChatTab(input) {
|
|
1497
|
-
const from = requireThread(input.fromThreadId);
|
|
1498
|
-
const binding = worktreeBindingFrom(from);
|
|
1499
|
-
const explicitTitle = input.title?.trim();
|
|
1500
|
-
const title = explicitTitle || allocateTeamName(takenTeamSlugsForChatTab(binding.worktreePath)).name;
|
|
1501
|
-
const nextAgent = input.agent ?? from.agent;
|
|
1502
|
-
if (isOrchestratorThread(from) || binding.sourceType === "orchestration") {
|
|
1503
|
-
assertOrchestratorCapableAgent(nextAgent);
|
|
1504
|
-
}
|
|
1505
|
-
const singletonInbox = isSlackCoordinatorThread(from) || isCloudCoordinatorThread(from);
|
|
1506
|
-
const thread = createEmptyThread({
|
|
1507
|
-
title,
|
|
1508
|
-
// Chat-tab nicknames (soccer team or explicit) must stick. Post-turn
|
|
1509
|
-
// syncThreadBranchFromGit would otherwise rewrite every sibling to the
|
|
1510
|
-
// shared worktree folder name (e.g. fork "Arsenal" → "Monaco").
|
|
1511
|
-
userSetTitle: true,
|
|
1512
|
-
...binding,
|
|
1513
|
-
// Slack / Brightsy cloud identity stays on the original chat. A + tab
|
|
1514
|
-
// that copies `slack:team:user` would steal inbound DMs after you close
|
|
1515
|
-
// the connected chat.
|
|
1516
|
-
sourceRef: singletonInbox ? title : binding.sourceRef,
|
|
1517
|
-
agent: nextAgent,
|
|
1518
|
-
model: input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model,
|
|
1519
|
-
effort: input.effort !== void 0 ? input.effort : from.effort,
|
|
1520
|
-
fast: input.fast !== void 0 ? Boolean(input.fast) : from.fast,
|
|
1521
|
-
planMode: from.planMode,
|
|
1522
|
-
autonomy: input.autonomy ?? from.autonomy,
|
|
1523
|
-
attachments: input.attachments ?? [],
|
|
1524
|
-
status: "idle",
|
|
1525
|
-
...input.accountId !== void 0 ? { accountId: input.accountId } : {},
|
|
1526
|
-
...mergeWorktreeActiveRuns(threadsSharingWorktree(binding.worktreePath))
|
|
1527
|
-
});
|
|
1528
|
-
writeThread(thread);
|
|
1529
|
-
return thread;
|
|
1530
|
-
}
|
|
1531
|
-
function forkChatTab(input) {
|
|
1532
|
-
const from = requireThread(input.threadId);
|
|
1533
|
-
const slice = forkMessageSlice(from, input.throughIndex);
|
|
1534
|
-
const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
|
|
1535
|
-
const tab = createChatTab({
|
|
1536
|
-
fromThreadId: input.threadId,
|
|
1537
|
-
agent: input.agent ?? from.agent,
|
|
1538
|
-
model: input.model,
|
|
1539
|
-
title: input.title?.trim() || void 0,
|
|
1540
|
-
attachments: [attachment]
|
|
1541
|
-
});
|
|
1542
|
-
if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
|
|
1543
|
-
const next = { ...tab, parentThreadId: from.id };
|
|
1544
|
-
writeThread(next);
|
|
1545
|
-
return next;
|
|
1546
|
-
}
|
|
1547
|
-
return tab;
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
// src/review/review-write-gate.ts
|
|
1551
|
-
var REVIEW_TAB_TITLE = "Review";
|
|
1552
|
-
function hasLiveReviewSibling(thread, siblings = []) {
|
|
1553
|
-
const selfId = thread.id;
|
|
1554
|
-
return siblings.some(
|
|
1555
|
-
(t) => t.id !== selfId && t.status !== "archived" && t.title.trim() === REVIEW_TAB_TITLE
|
|
1556
|
-
);
|
|
1557
|
-
}
|
|
1558
|
-
function isReviewWriteGatedThread(thread, siblings) {
|
|
1559
|
-
if (thread.sourceType === "pr" || thread.title.trim() === REVIEW_TAB_TITLE) {
|
|
1560
|
-
return true;
|
|
1561
|
-
}
|
|
1562
|
-
return hasLiveReviewSibling(thread, siblings);
|
|
1563
|
-
}
|
|
1564
|
-
function formatReviewWriteGateDirective() {
|
|
1565
|
-
return [
|
|
1566
|
-
"Review writes are public. The PR or ticket author sees GitHub/Linear/AbleTime comments, ticket updates, and submitted reviews immediately \u2014 a draft will confuse them.",
|
|
1567
|
-
"Write the recommendation in chat so the user can work through the feedback first. Do not call github_comment, linear_comment, abletime_comment, github_update_issue, linear_update_issue, abletime_update_task, or run `gh pr review` / `gh pr comment`, until they type a next step that asks you to post.",
|
|
1568
|
-
"After the review is in chat, stop. Do not call ask_user. The user needs time to read and will type the next steps. If they already asked you to post a specific comment or update in this turn, that is confirmation.",
|
|
1569
|
-
"Do not request reviewers, change labels, or assign yourself unless they asked \u2014 those also notify the author."
|
|
1570
|
-
].join("\n");
|
|
1571
|
-
}
|
|
1572
|
-
function formatReviewWriteGateReminder() {
|
|
1573
|
-
return "Review writes: keep the review in chat. Do not ask_user after it \u2014 they read and type next steps. Do not github_comment / linear_comment / *_update_* / `gh pr review` on the PR or ticket until they ask. Authors see those immediately.";
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
// src/review/review-request-template.ts
|
|
1577
|
-
var REVIEW_REQUEST_TEMPLATE = `# Review guidelines:
|
|
1578
|
-
|
|
1579
|
-
You are reviewing a proposed code change so a human can decide whether it is **ready to merge / land**. Findings matter, but the primary deliverable is a clear readiness recommendation \u2014 not a laundry list of style notes.
|
|
1580
|
-
|
|
1581
|
-
## Required outcome
|
|
1582
|
-
|
|
1583
|
-
Start your reply with a **Recommendation** section using exactly one of:
|
|
1584
|
-
|
|
1585
|
-
- **Approve** \u2014 ready to merge as-is (or with only trivial nits the author can ignore).
|
|
1586
|
-
- **Approve with nits** \u2014 ready to merge; list only optional polish that should not block.
|
|
1587
|
-
- **Request changes** \u2014 not ready; blocking issues must be fixed first.
|
|
1588
|
-
- **Needs more information** \u2014 cannot judge readiness yet (missing context, incomplete diff, unclear intent).
|
|
1589
|
-
|
|
1590
|
-
In 1\u20133 sentences, say **why** \u2014 grounded in correctness, risk, test coverage, and scope \u2014 not vibes. If you request changes, name the blockers explicitly.
|
|
1591
|
-
|
|
1592
|
-
People running this review are asking \u201Ccan we ship this?\u201D Treat that as the question you answer first.
|
|
1593
|
-
|
|
1594
|
-
## Confirm before posting
|
|
1595
|
-
|
|
1596
|
-
The report stays in this chat until the user posts it. Do not call \`github_comment\`, \`linear_comment\`, \`abletime_comment\`, \`github_update_issue\`, \`linear_update_issue\`, \`abletime_update_task\`, or run \`gh pr review\` / \`gh pr comment\`, until they type a next step that asks you to post.
|
|
1597
|
-
|
|
1598
|
-
After the Recommendation is in chat, stop. Do not call ask_user. The user needs time to read the review and will type the next steps. If they already asked you to post a specific comment or update in this turn, that is confirmation.
|
|
1599
|
-
|
|
1600
|
-
The PR or ticket author sees those writes immediately \u2014 a draft review will confuse them. Do not request reviewers, change labels, or assign yourself unless they asked.
|
|
1601
|
-
|
|
1602
|
-
## Findings
|
|
1603
|
-
|
|
1604
|
-
Below are guidelines for determining whether an issue is worth flagging to the original author.
|
|
1605
|
-
|
|
1606
|
-
These are not the final word. More specific guidelines elsewhere (developer message, user message, a file, etc.) override these.
|
|
1607
|
-
|
|
1608
|
-
Flag something as a bug / blocking finding only when:
|
|
1609
|
-
|
|
1610
|
-
1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
|
|
1611
|
-
2. The bug is discrete and actionable (not a vague codebase-wide complaint or a bundle of unrelated issues).
|
|
1612
|
-
3. Fixing it does not demand rigor absent from the rest of the codebase.
|
|
1613
|
-
4. The issue was introduced by this change (do not flag pre-existing bugs unless they are newly exposed by this PR).
|
|
1614
|
-
5. The author would likely fix it if made aware.
|
|
1615
|
-
6. It does not rely on unstated assumptions about the codebase or author intent.
|
|
1616
|
-
7. Speculative breakage is not enough \u2014 identify the other code that is provably affected.
|
|
1617
|
-
8. It is clearly not just an intentional change by the author.
|
|
1618
|
-
|
|
1619
|
-
When flagging an issue, include a short accompanying comment:
|
|
1620
|
-
|
|
1621
|
-
1. Clear about why it is a problem.
|
|
1622
|
-
2. Severity must match reality \u2014 do not inflate.
|
|
1623
|
-
3. Brief: at most one paragraph; avoid unnecessary line breaks in prose.
|
|
1624
|
-
4. No code chunks longer than 3 lines; wrap code in inline ticks or a fenced block.
|
|
1625
|
-
5. Call out scenarios / environments / inputs needed to hit the bug when severity depends on them.
|
|
1626
|
-
6. Matter-of-fact tone \u2014 helpful assistant, not accusatory or effusive.
|
|
1627
|
-
7. Skimmable on first read.
|
|
1628
|
-
8. No empty flattery (\u201CGreat job\u2026\u201D, \u201CThanks for\u2026\u201D).
|
|
1629
|
-
|
|
1630
|
-
HOW MANY FINDINGS TO RETURN:
|
|
1631
|
-
|
|
1632
|
-
List every finding the author would fix if they knew about it. If nothing qualifies, say so and still give the Recommendation. Do not stop at the first finding.
|
|
1633
|
-
|
|
1634
|
-
GUIDELINES:
|
|
1635
|
-
|
|
1636
|
-
- Ignore trivial style unless it obscures meaning or violates documented standards.
|
|
1637
|
-
- One comment per distinct issue (or a short multi-line range if needed).
|
|
1638
|
-
- Use \`\`\`suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block).
|
|
1639
|
-
- In every \`\`\`suggestion block, preserve the exact leading whitespace of the replaced lines (spaces vs tabs, number of spaces).
|
|
1640
|
-
- Do NOT introduce or remove outer indentation levels unless that is the actual fix.
|
|
1641
|
-
- Separate **blocking** findings from **nits**. Only blocking findings should drive Request changes.
|
|
1642
|
-
|
|
1643
|
-
The report appears in chat (and can become Sideboard diff comments). Avoid unnecessary location chatter in the body; keep line ranges as short as possible (prefer \u22645\u201310 lines).
|
|
1644
|
-
|
|
1645
|
-
## Getting the diff
|
|
1646
|
-
|
|
1647
|
-
Use Sideboard's diff for this thread's worktree. Prefer the \`get_diff\` MCP tool (pass this thread's ref) for a compact summary, then read specific files with Read/Glob as needed. In the Sideboard desktop app, the Changes panel shows the same worktree diff.
|
|
1648
|
-
|
|
1649
|
-
If the user asks you to address or read line comments they added in the Changes / file diff UI, those arrive as \`diff-comment\` attachments on the next turn \u2014 follow them precisely.
|
|
1650
|
-
|
|
1651
|
-
## Fallback: if you don't have access to the Sideboard diff tool
|
|
1652
|
-
|
|
1653
|
-
If you don't have access to \`get_diff\`, use the following git commands to get the diff:
|
|
1654
|
-
|
|
1655
|
-
\`\`\`bash
|
|
1656
|
-
# Get the merge base between this branch and the target
|
|
1657
|
-
MERGE_BASE=$(git merge-base origin/main HEAD)
|
|
1658
|
-
|
|
1659
|
-
# Get the committed diff against the merge base
|
|
1660
|
-
git diff $MERGE_BASE HEAD
|
|
1661
|
-
|
|
1662
|
-
# Get any uncommitted changes (staged and unstaged)
|
|
1663
|
-
git diff HEAD
|
|
1664
|
-
\`\`\`
|
|
1665
|
-
|
|
1666
|
-
Review the combination of both outputs: the first shows all committed changes on this branch relative to the target, and the second shows any uncommitted work in progress.
|
|
1667
|
-
|
|
1668
|
-
No need to mention in your report whether or not you used one of the fallback strategies; it's usually irrelevant.
|
|
1669
|
-
|
|
1670
|
-
## Output format
|
|
1671
|
-
|
|
1672
|
-
**1. Recommendation first** (required), then **2. Findings** (may be empty).
|
|
1673
|
-
|
|
1674
|
-
Only report ONE finding per unique issue.
|
|
1675
|
-
|
|
1676
|
-
<example>
|
|
1677
|
-
## Recommendation
|
|
1678
|
-
|
|
1679
|
-
**Request changes** \u2014 The empty-input crash on load will break first-run users; fix that before merge. The unused helper is a nit and can wait.
|
|
1680
|
-
|
|
1681
|
-
## Findings
|
|
1682
|
-
|
|
1683
|
-
### **#1 Empty input causes crash** (blocking)
|
|
1684
|
-
|
|
1685
|
-
If the input field is empty when the page loads, the app will crash.
|
|
1686
|
-
|
|
1687
|
-
File: src/client/frontends/desktop/ui/Input.tsx
|
|
1688
|
-
|
|
1689
|
-
### **#2 Dead code** (nit)
|
|
1690
|
-
|
|
1691
|
-
The getUserData function is now unused. It should be deleted.
|
|
1692
|
-
|
|
1693
|
-
File: src/client/frontends/desktop/core/UserData.ts
|
|
1694
|
-
</example>
|
|
1695
|
-
|
|
1696
|
-
<example>
|
|
1697
|
-
## Recommendation
|
|
1698
|
-
|
|
1699
|
-
**Approve** \u2014 Diff is scoped, behavior looks correct, and there are no blocking issues. Safe to merge.
|
|
1700
|
-
</example>
|
|
1701
|
-
|
|
1702
|
-
## Growing the rules
|
|
1703
|
-
|
|
1704
|
-
If a blocking issue is a missing or ambiguous repo rule that will recur, add one sentence to \`.claude/skills/review/SKILL.md\` when that skill already exists. Otherwise write it to \`.context/review.md\` (do not create a review skill). Do not only patch this diff when the same miss will happen again. Do not write new skills under \`.sideboard/skills/\`.
|
|
1705
|
-
`;
|
|
1706
|
-
var REVIEW_SKILL_PATH = ".claude/skills/review/SKILL.md";
|
|
1707
|
-
var REVIEW_SKILL_NAME = "review";
|
|
1708
|
-
|
|
1709
|
-
// src/review/request-review.ts
|
|
1710
|
-
var REPO_REVIEW_PATH = ".sideboard/review.md";
|
|
1711
|
-
var REPO_REVIEW_NAME = "review.md";
|
|
1712
|
-
var CONTEXT_REVIEW_PATH = ".context/review.md";
|
|
1713
|
-
var REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
|
|
1714
|
-
var LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
|
|
1715
|
-
var REVIEW_REQUEST_NAME = "Review request.md";
|
|
1716
|
-
var REVIEW_REQUEST_PREFILL = "Review changes in this workspace.";
|
|
1717
|
-
var LEGACY_REVIEW_TEMPLATE_MARKERS = [
|
|
1718
|
-
"You are acting as a reviewer for a proposed code change made by another engineer.",
|
|
1719
|
-
"HOW MANY FINDINGS TO RETURN:"
|
|
1720
|
-
];
|
|
1721
|
-
function shouldRefreshReviewRequestTemplate(content) {
|
|
1722
|
-
const trimmed = content.trim();
|
|
1723
|
-
if (!trimmed) return true;
|
|
1724
|
-
if (/##\s*Recommendation|ready to merge|Approve with nits|Request changes/i.test(trimmed)) {
|
|
1725
|
-
return false;
|
|
1726
|
-
}
|
|
1727
|
-
return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
|
|
1728
|
-
}
|
|
1729
|
-
function readTextIfPresent(abs) {
|
|
1730
|
-
if (!existsSync2(abs)) return null;
|
|
1731
|
-
try {
|
|
1732
|
-
const content = readFileSync(abs, "utf8");
|
|
1733
|
-
return content.trim() ? content : null;
|
|
1734
|
-
} catch {
|
|
1735
|
-
return null;
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
function readLocalGuidelines(worktreePath) {
|
|
1739
|
-
const localAbs = join3(worktreePath, REVIEW_REQUEST_PATH);
|
|
1740
|
-
const localContent = readTextIfPresent(localAbs);
|
|
1741
|
-
if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
|
|
1742
|
-
return { path: REVIEW_REQUEST_PATH, content: localContent };
|
|
1743
|
-
}
|
|
1744
|
-
const legacyAbs = join3(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
|
|
1745
|
-
const legacyContent = readTextIfPresent(legacyAbs);
|
|
1746
|
-
if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
|
|
1747
|
-
return { path: LEGACY_REVIEW_REQUEST_PATH, content: legacyContent };
|
|
1748
|
-
}
|
|
1749
|
-
return null;
|
|
1750
|
-
}
|
|
1751
|
-
function skillGuidelines(content, source) {
|
|
1752
|
-
return {
|
|
1753
|
-
path: REVIEW_SKILL_PATH,
|
|
1754
|
-
name: REVIEW_SKILL_NAME,
|
|
1755
|
-
content,
|
|
1756
|
-
source
|
|
1757
|
-
};
|
|
1758
|
-
}
|
|
1759
|
-
function contextGuidelines(content, source) {
|
|
1760
|
-
return {
|
|
1761
|
-
path: CONTEXT_REVIEW_PATH,
|
|
1762
|
-
name: REPO_REVIEW_NAME,
|
|
1763
|
-
content,
|
|
1764
|
-
source
|
|
1765
|
-
};
|
|
1766
|
-
}
|
|
1767
|
-
function writeContextGuidelines(worktreePath, content) {
|
|
1768
|
-
const abs = join3(worktreePath, CONTEXT_REVIEW_PATH);
|
|
1769
|
-
mkdirSync2(dirname(abs), { recursive: true });
|
|
1770
|
-
const body = content.endsWith("\n") ? content : `${content}
|
|
1771
|
-
`;
|
|
1772
|
-
writeFileSync(abs, body, "utf8");
|
|
1773
|
-
return body;
|
|
1774
|
-
}
|
|
1775
|
-
function readSideboardReview(worktreePath, repoPath) {
|
|
1776
|
-
const fromWorktree = readTextIfPresent(join3(worktreePath, REPO_REVIEW_PATH));
|
|
1777
|
-
if (fromWorktree) return fromWorktree;
|
|
1778
|
-
const repo = repoPath?.replace(/\/+$/, "");
|
|
1779
|
-
const wt = worktreePath.replace(/\/+$/, "");
|
|
1780
|
-
if (!repo || repo === wt) return null;
|
|
1781
|
-
return readTextIfPresent(join3(repo, REPO_REVIEW_PATH));
|
|
1782
|
-
}
|
|
1783
|
-
function ensureReviewGuidelinesFile(worktreePath, repoPath) {
|
|
1784
|
-
const skillContent = readTextIfPresent(join3(worktreePath, REVIEW_SKILL_PATH));
|
|
1785
|
-
if (skillContent) {
|
|
1786
|
-
return { ...skillGuidelines(skillContent, "skill"), wrote: false };
|
|
1787
|
-
}
|
|
1788
|
-
const contextContent = readTextIfPresent(join3(worktreePath, CONTEXT_REVIEW_PATH));
|
|
1789
|
-
if (contextContent && !shouldRefreshReviewRequestTemplate(contextContent)) {
|
|
1790
|
-
return { ...contextGuidelines(contextContent, "local"), wrote: false };
|
|
1791
|
-
}
|
|
1792
|
-
const fromSideboard = readSideboardReview(worktreePath, repoPath);
|
|
1793
|
-
if (fromSideboard && !shouldRefreshReviewRequestTemplate(fromSideboard)) {
|
|
1794
|
-
const body2 = writeContextGuidelines(worktreePath, fromSideboard);
|
|
1795
|
-
return { ...contextGuidelines(body2, "repo"), wrote: true };
|
|
1796
|
-
}
|
|
1797
|
-
const local = readLocalGuidelines(worktreePath);
|
|
1798
|
-
const body = writeContextGuidelines(worktreePath, local?.content ?? REVIEW_REQUEST_TEMPLATE);
|
|
1799
|
-
return {
|
|
1800
|
-
...contextGuidelines(body, local ? "local" : "stock"),
|
|
1801
|
-
wrote: true
|
|
1802
|
-
};
|
|
1803
|
-
}
|
|
1804
|
-
function resolveReviewGuidelines(worktreePath, repoPath) {
|
|
1805
|
-
return ensureReviewGuidelinesFile(worktreePath, repoPath);
|
|
1806
|
-
}
|
|
1807
|
-
function buildReviewRequestAttachment(content, opts) {
|
|
1808
|
-
const path = opts?.path ?? REVIEW_SKILL_PATH;
|
|
1809
|
-
const name = opts?.name ?? (path === REVIEW_SKILL_PATH ? REVIEW_SKILL_NAME : path === REPO_REVIEW_PATH || path === CONTEXT_REVIEW_PATH ? REPO_REVIEW_NAME : REVIEW_REQUEST_NAME);
|
|
1810
|
-
return {
|
|
1811
|
-
id: randomUUID2(),
|
|
1812
|
-
name,
|
|
1813
|
-
kind: "file",
|
|
1814
|
-
path,
|
|
1815
|
-
content
|
|
1816
|
-
};
|
|
1817
|
-
}
|
|
1818
|
-
function canReuseIdleWorktreeAgentForReview(from, siblings) {
|
|
1819
|
-
const live = siblings.filter((t) => t.status !== "archived");
|
|
1820
|
-
if (live.length !== 1) return false;
|
|
1821
|
-
const only = live[0];
|
|
1822
|
-
if (only.id !== from.id) return false;
|
|
1823
|
-
if (from.status !== "idle") return false;
|
|
1824
|
-
if ((from.queue?.length ?? 0) > 0) return false;
|
|
1825
|
-
return (from.messages?.length ?? 0) === 0 && (only.messages?.length ?? 0) === 0;
|
|
1826
|
-
}
|
|
1827
|
-
function reviewGuidelinesAttachment(from) {
|
|
1828
|
-
const guidelines = resolveReviewGuidelines(from.worktreePath, from.repoPath);
|
|
1829
|
-
return buildReviewRequestAttachment(guidelines.content, {
|
|
1830
|
-
path: guidelines.path,
|
|
1831
|
-
name: guidelines.name
|
|
1832
|
-
});
|
|
1833
|
-
}
|
|
1834
|
-
async function requestReview(threadRef, send) {
|
|
1835
|
-
const from = findThreadByRef(threadRef);
|
|
1836
|
-
if (!from) throw new Error(`Thread not found: ${threadRef}`);
|
|
1837
|
-
if (isOrchestratorThread(from)) {
|
|
1838
|
-
throw new Error(
|
|
1839
|
-
"request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
|
|
1840
|
-
);
|
|
1841
|
-
}
|
|
1842
|
-
if (from.status === "archived") {
|
|
1843
|
-
throw new Error(`Thread is archived: ${from.id}`);
|
|
1844
|
-
}
|
|
1845
|
-
const attachment = reviewGuidelinesAttachment(from);
|
|
1846
|
-
const siblings = threadsSharingWorktree(from.worktreePath);
|
|
1847
|
-
if (canReuseIdleWorktreeAgentForReview(from, siblings)) {
|
|
1848
|
-
const reused = updateThread(from.id, {
|
|
1849
|
-
title: REVIEW_TAB_TITLE,
|
|
1850
|
-
userSetTitle: true,
|
|
1851
|
-
attachments: [...from.attachments ?? [], attachment]
|
|
1852
|
-
});
|
|
1853
|
-
const started2 = await send(reused.id, REVIEW_REQUEST_PREFILL);
|
|
1854
|
-
return { tab: started2, from: reused };
|
|
1855
|
-
}
|
|
1856
|
-
const tab = createChatTab({
|
|
1857
|
-
fromThreadId: from.id,
|
|
1858
|
-
title: REVIEW_TAB_TITLE,
|
|
1859
|
-
attachments: [attachment]
|
|
1860
|
-
});
|
|
1861
|
-
const started = await send(tab.id, REVIEW_REQUEST_PREFILL);
|
|
1862
|
-
return { tab: started, from };
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
// src/hook/worktree-run-ports.ts
|
|
1866
|
-
import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1867
|
-
import { dirname as dirname2, join as join4 } from "path";
|
|
1868
|
-
var RUN_PORTS_REL = ".context/.sideboard/run-ports.json";
|
|
1869
|
-
function worktreeRunPortsPath(worktreePath) {
|
|
1870
|
-
return join4(worktreePath, RUN_PORTS_REL);
|
|
1871
|
-
}
|
|
1872
|
-
function isPort(value) {
|
|
1873
|
-
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65535;
|
|
1874
|
-
}
|
|
1875
|
-
function parseFile(raw) {
|
|
1876
|
-
try {
|
|
1877
|
-
const parsed = JSON.parse(raw);
|
|
1878
|
-
if (!parsed || typeof parsed !== "object") return {};
|
|
1879
|
-
const scripts = parsed.scripts;
|
|
1880
|
-
if (!scripts || typeof scripts !== "object") return {};
|
|
1881
|
-
const next = {};
|
|
1882
|
-
for (const [name, ports] of Object.entries(scripts)) {
|
|
1883
|
-
if (!name || !Array.isArray(ports)) continue;
|
|
1884
|
-
const cleaned = ports.filter(isPort);
|
|
1885
|
-
if (cleaned.length) next[name] = cleaned;
|
|
1886
|
-
}
|
|
1887
|
-
return { scripts: next };
|
|
1888
|
-
} catch {
|
|
1889
|
-
return {};
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
function loadWorktreeRunPorts(worktreePath, scriptName) {
|
|
1893
|
-
if (!worktreePath.trim() || !scriptName.trim()) return null;
|
|
1894
|
-
try {
|
|
1895
|
-
const raw = readFileSync2(worktreeRunPortsPath(worktreePath), "utf8");
|
|
1896
|
-
const ports = parseFile(raw).scripts?.[scriptName];
|
|
1897
|
-
return ports?.length ? ports : null;
|
|
1898
|
-
} catch {
|
|
1899
|
-
return null;
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
function saveWorktreeRunPorts(worktreePath, scriptName, ports) {
|
|
1903
|
-
if (!worktreePath.trim() || !scriptName.trim()) return;
|
|
1904
|
-
const cleaned = ports.filter(isPort);
|
|
1905
|
-
if (!cleaned.length) return;
|
|
1906
|
-
const path = worktreeRunPortsPath(worktreePath);
|
|
1907
|
-
let current = {};
|
|
1908
|
-
try {
|
|
1909
|
-
current = parseFile(readFileSync2(path, "utf8"));
|
|
1910
|
-
} catch {
|
|
1911
|
-
current = {};
|
|
1912
|
-
}
|
|
1913
|
-
const scripts = { ...current.scripts ?? {} };
|
|
1914
|
-
scripts[scriptName] = cleaned;
|
|
1915
|
-
mkdirSync3(dirname2(path), { recursive: true });
|
|
1916
|
-
writeFileSync2(path, `${JSON.stringify({ scripts }, null, 2)}
|
|
1917
|
-
`, "utf8");
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
// src/mcp/wait-for-job.ts
|
|
1921
|
-
import {
|
|
1922
|
-
appendFileSync as appendFileSync2,
|
|
1923
|
-
existsSync as existsSync3,
|
|
1924
|
-
mkdirSync as mkdirSync4,
|
|
1925
|
-
readdirSync,
|
|
1926
|
-
readFileSync as readFileSync3,
|
|
1927
|
-
writeFileSync as writeFileSync3
|
|
1928
|
-
} from "fs";
|
|
1929
|
-
import { dirname as dirname3, join as join5 } from "path";
|
|
1930
|
-
var MAX_JOB_CONTINUES = 8;
|
|
1931
|
-
var MAX_JOB_DELTA_LINES = 80;
|
|
1932
|
-
var MAX_JOB_DELTA_CHARS = 4e3;
|
|
1933
|
-
var DEFERRED_DONE_MAX_CHARS = 480;
|
|
1934
|
-
var GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
|
|
1935
|
-
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.";
|
|
1936
|
-
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.";
|
|
1937
|
-
var JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
|
|
1938
|
-
function mcpWaitForJobTimeoutMs(requested) {
|
|
1939
|
-
return mcpWaitForTurnTimeoutMs(requested);
|
|
1940
|
-
}
|
|
1941
|
-
function sanitizeDetachedJobId(id) {
|
|
1942
|
-
const s = id.trim();
|
|
1943
|
-
if (!JOB_ID_RE.test(s)) {
|
|
1944
|
-
throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
|
|
1945
|
-
}
|
|
1946
|
-
return s;
|
|
1947
|
-
}
|
|
1948
|
-
function jobAlive(pid) {
|
|
1949
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1950
|
-
try {
|
|
1951
|
-
process.kill(pid, 0);
|
|
1952
|
-
return true;
|
|
1953
|
-
} catch {
|
|
1954
|
-
return false;
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
function processGroupAlive(pgid) {
|
|
1958
|
-
if (process.platform === "win32") return false;
|
|
1959
|
-
if (!Number.isInteger(pgid) || pgid <= 1) return false;
|
|
1960
|
-
try {
|
|
1961
|
-
process.kill(-pgid, 0);
|
|
1962
|
-
return true;
|
|
1963
|
-
} catch (err) {
|
|
1964
|
-
return Boolean(err && typeof err === "object" && "code" in err && err.code === "EPERM");
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
function jobTreeAlive(pid) {
|
|
1968
|
-
if (pid == null) return false;
|
|
1969
|
-
return jobAlive(pid) || processGroupAlive(pid);
|
|
1970
|
-
}
|
|
1971
|
-
function readIntFile(file) {
|
|
1972
|
-
if (!existsSync3(file)) return null;
|
|
1973
|
-
const n = Number.parseInt(readFileSync3(file, "utf8").trim(), 10);
|
|
1974
|
-
return Number.isInteger(n) ? n : null;
|
|
1975
|
-
}
|
|
1976
|
-
function jobDir(root, id, legacy = false) {
|
|
1977
|
-
return join5(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
|
|
1978
|
-
}
|
|
1979
|
-
function resolveJobDir(root, id) {
|
|
1980
|
-
const modern = jobDir(root, id, false);
|
|
1981
|
-
if (existsSync3(modern)) return modern;
|
|
1982
|
-
const legacy = jobDir(root, id, true);
|
|
1983
|
-
if (existsSync3(legacy)) return legacy;
|
|
1984
|
-
return modern;
|
|
1985
|
-
}
|
|
1986
|
-
function listJobIdsIn(root, rel) {
|
|
1987
|
-
const dir = join5(root, rel);
|
|
1988
|
-
if (!existsSync3(dir)) return [];
|
|
1989
|
-
return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && JOB_ID_RE.test(e.name)).map((e) => e.name);
|
|
1990
|
-
}
|
|
1991
|
-
function listRunningDetachedJobs(worktreePath) {
|
|
1992
|
-
const root = worktreePath.trim();
|
|
1993
|
-
if (!root) return [];
|
|
1994
|
-
const ids = /* @__PURE__ */ new Set([
|
|
1995
|
-
...listJobIdsIn(root, DETACHED_JOBS_DIR),
|
|
1996
|
-
...listJobIdsIn(root, LEGACY_DETACHED_JOBS_DIR)
|
|
1997
|
-
]);
|
|
1998
|
-
const running = [];
|
|
1999
|
-
for (const id of ids) {
|
|
2000
|
-
const dir = resolveJobDir(root, id);
|
|
2001
|
-
const pid = readIntFile(join5(dir, "pid"));
|
|
2002
|
-
if (jobTreeAlive(pid)) running.push(id);
|
|
2003
|
-
}
|
|
2004
|
-
return running.sort();
|
|
2005
|
-
}
|
|
2006
|
-
function looksLikeDeferredDonePromise(text) {
|
|
2007
|
-
const t = (text ?? "").trim();
|
|
2008
|
-
if (!t || t.length > DEFERRED_DONE_MAX_CHARS) return false;
|
|
2009
|
-
const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
|
|
2010
|
-
if (!unquoted) return false;
|
|
2011
|
-
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);
|
|
2012
|
-
}
|
|
2013
|
-
function isJobContinuePrompt(text) {
|
|
2014
|
-
if (typeof text !== "string") return false;
|
|
2015
|
-
const t = text.trim();
|
|
2016
|
-
return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
|
|
2017
|
-
}
|
|
2018
|
-
function collapseGhRunWatchLog(text) {
|
|
2019
|
-
if (!text.includes(GH_WATCH_REFRESH)) return text;
|
|
2020
|
-
const frames = text.split(/(?=Refreshing run status every 3 seconds\.)/);
|
|
2021
|
-
const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
|
|
2022
|
-
if (watchFrames.length <= 1) return text;
|
|
2023
|
-
const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
|
|
2024
|
-
const last = watchFrames[watchFrames.length - 1] ?? "";
|
|
2025
|
-
const omitted = watchFrames.length - 1;
|
|
2026
|
-
const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
|
|
2027
|
-
`;
|
|
2028
|
-
return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
|
|
2029
|
-
}
|
|
2030
|
-
function capJobLogDelta(text) {
|
|
2031
|
-
const collapsed = collapseGhRunWatchLog(text);
|
|
2032
|
-
const lines = collapsed.split("\n");
|
|
2033
|
-
let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
|
|
2034
|
-
${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
|
|
2035
|
-
if (out.length > MAX_JOB_DELTA_CHARS) {
|
|
2036
|
-
out = `\u2026(truncated ${out.length} chars)
|
|
2037
|
-
${out.slice(-MAX_JOB_DELTA_CHARS)}`;
|
|
2038
|
-
}
|
|
2039
|
-
return out;
|
|
2040
|
-
}
|
|
2041
|
-
function formatJobStillRunningContinuePrompt(jobIds) {
|
|
2042
|
-
const ids = jobIds.join(", ");
|
|
2043
|
-
return [
|
|
2044
|
-
`Detached job still running: ${ids}.`,
|
|
2045
|
-
"Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
|
|
2046
|
-
"If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
|
|
2047
|
-
"Then report the result. Do not tell the user you will let them know later."
|
|
2048
|
-
].join(" ");
|
|
2049
|
-
}
|
|
2050
|
-
function formatDeferredDoneContinuePrompt() {
|
|
2051
|
-
return [
|
|
2052
|
-
"You ended the turn after promising to report later, but no detached job is running.",
|
|
2053
|
-
"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).",
|
|
2054
|
-
"Do not say you will let the user know later."
|
|
2055
|
-
].join(" ");
|
|
2056
|
-
}
|
|
2057
|
-
function turnWatchedDetachedJob(parts) {
|
|
2058
|
-
return parts.some((p) => {
|
|
2059
|
-
if (p.type !== "tool") return false;
|
|
2060
|
-
if (/wait_for_job$/i.test(p.name ?? "") || /stop_job$/i.test(p.name ?? "")) return true;
|
|
2061
|
-
const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
|
|
2062
|
-
return /detached-job\.(?:js|cjs)\b/i.test(blob);
|
|
2063
|
-
});
|
|
2064
|
-
}
|
|
2065
|
-
function planJobContinue(opts) {
|
|
2066
|
-
if (opts.isOrchestrator) return { action: "none" };
|
|
2067
|
-
if (opts.agent === "brightsy") return { action: "none" };
|
|
2068
|
-
if (opts.queueLength > 0) return { action: "none" };
|
|
2069
|
-
if (opts.continueCount >= MAX_JOB_CONTINUES) return { action: "none" };
|
|
2070
|
-
const farewell = looksLikeDeferredDonePromise(opts.chatText);
|
|
2071
|
-
if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
|
|
2072
|
-
return {
|
|
2073
|
-
action: "wait",
|
|
2074
|
-
jobIds: opts.runningJobIds,
|
|
2075
|
-
prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
|
|
2076
|
-
};
|
|
2077
|
-
}
|
|
2078
|
-
if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
|
|
2079
|
-
return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
|
|
2080
|
-
}
|
|
2081
|
-
return { action: "none" };
|
|
2082
|
-
}
|
|
2083
|
-
function tailProgress(logFile, maxLines = 12) {
|
|
2084
|
-
if (!existsSync3(logFile)) return "(no log yet)";
|
|
2085
|
-
const lines = readFileSync3(logFile, "utf8").split("\n");
|
|
2086
|
-
return lines.slice(-maxLines).join("\n");
|
|
2087
|
-
}
|
|
2088
|
-
function lastProgressLine(progress) {
|
|
2089
|
-
const last = progress.split("\n").map((l) => l.trim()).filter((l) => l && l !== "(no log yet)").at(-1);
|
|
2090
|
-
return last ? last.slice(0, 72) : void 0;
|
|
2091
|
-
}
|
|
2092
|
-
function readLogLines(file) {
|
|
2093
|
-
if (!existsSync3(file)) return [];
|
|
2094
|
-
const lines = readFileSync3(file, "utf8").split("\n");
|
|
2095
|
-
if (lines.length && lines[lines.length - 1] === "") lines.pop();
|
|
2096
|
-
return lines;
|
|
2097
|
-
}
|
|
2098
|
-
function takeDelta(logFile, cursorFile) {
|
|
2099
|
-
const lines = readLogLines(logFile);
|
|
2100
|
-
const cursor = readIntFile(cursorFile) ?? 0;
|
|
2101
|
-
const start = Math.min(Math.max(0, cursor), lines.length);
|
|
2102
|
-
return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
|
|
2103
|
-
}
|
|
2104
|
-
function snapshotJob(dir) {
|
|
2105
|
-
const pid = readIntFile(join5(dir, "pid"));
|
|
2106
|
-
const running = jobTreeAlive(pid);
|
|
2107
|
-
return {
|
|
2108
|
-
pid,
|
|
2109
|
-
running,
|
|
2110
|
-
exitCode: readIntFile(join5(dir, "exit")),
|
|
2111
|
-
log: join5(dir, "log"),
|
|
2112
|
-
cursor: join5(dir, "present.cursor"),
|
|
2113
|
-
progress: tailProgress(join5(dir, "log"))
|
|
2114
|
-
};
|
|
2115
|
-
}
|
|
2116
|
-
function toResult(id, snap, extra) {
|
|
2117
|
-
const failed = extra?.failed === true || !snap.running && snap.exitCode != null && snap.exitCode !== 0;
|
|
2118
|
-
const ok = !snap.running && snap.exitCode === 0;
|
|
2119
|
-
const stillRunning = snap.running && !ok;
|
|
2120
|
-
const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
|
|
2121
|
-
try {
|
|
2122
|
-
mkdirSync4(dirname3(snap.cursor), { recursive: true });
|
|
2123
|
-
writeFileSync3(snap.cursor, `${nextCursor}
|
|
2124
|
-
`);
|
|
2125
|
-
} catch {
|
|
2126
|
-
}
|
|
2127
|
-
const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
|
|
2128
|
-
const progress = extra?.progress ?? snap.progress;
|
|
2129
|
-
return {
|
|
2130
|
-
stillRunning,
|
|
2131
|
-
ok,
|
|
2132
|
-
failed: Boolean(failed && !stillRunning && !ok),
|
|
2133
|
-
status,
|
|
2134
|
-
id,
|
|
2135
|
-
pid: snap.pid,
|
|
2136
|
-
exitCode: snap.exitCode ?? void 0,
|
|
2137
|
-
phase: lastProgressLine(progress),
|
|
2138
|
-
delta,
|
|
2139
|
-
progress,
|
|
2140
|
-
hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
|
|
2141
|
-
};
|
|
2142
|
-
}
|
|
2143
|
-
async function sleepMs(ms) {
|
|
2144
|
-
await new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2145
|
-
}
|
|
2146
|
-
async function waitForDetachedJob(cwd, id, opts) {
|
|
2147
|
-
const jobId = sanitizeDetachedJobId(id);
|
|
2148
|
-
const root = cwd.trim() || process.cwd();
|
|
2149
|
-
const dir = resolveJobDir(root, jobId);
|
|
2150
|
-
const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
|
|
2151
|
-
const sleep = opts?.sleep ?? sleepMs;
|
|
2152
|
-
if (!existsSync3(dir)) {
|
|
2153
|
-
return {
|
|
2154
|
-
stillRunning: false,
|
|
2155
|
-
ok: false,
|
|
2156
|
-
failed: true,
|
|
2157
|
-
status: "failed",
|
|
2158
|
-
id: jobId,
|
|
2159
|
-
delta: "",
|
|
2160
|
-
progress: "No detached job. Start one first.",
|
|
2161
|
-
hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
|
|
2162
|
-
};
|
|
2163
|
-
}
|
|
2164
|
-
const deadline = Date.now() + timeoutMs;
|
|
2165
|
-
let snap = snapshotJob(dir);
|
|
2166
|
-
if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
|
|
2167
|
-
if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
|
|
2168
|
-
return toResult(jobId, snap, {
|
|
2169
|
-
failed: true,
|
|
2170
|
-
progress: "No detached job. Start one first."
|
|
2171
|
-
});
|
|
2172
|
-
}
|
|
2173
|
-
while (Date.now() < deadline) {
|
|
2174
|
-
snap = snapshotJob(dir);
|
|
2175
|
-
if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
|
|
2176
|
-
if (!snap.running) return toResult(jobId, snap, { failed: true });
|
|
2177
|
-
await sleep(Math.min(2e3, Math.max(50, deadline - Date.now())));
|
|
2178
|
-
}
|
|
2179
|
-
snap = snapshotJob(dir);
|
|
2180
|
-
return toResult(jobId, snap);
|
|
2181
|
-
}
|
|
2182
|
-
function killProcessTree(pid, signal) {
|
|
2183
|
-
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
2184
|
-
if (process.platform !== "win32") {
|
|
2185
|
-
try {
|
|
2186
|
-
process.kill(-pid, signal);
|
|
2187
|
-
return;
|
|
2188
|
-
} catch {
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
try {
|
|
2192
|
-
process.kill(pid, signal);
|
|
2193
|
-
} catch {
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
|
-
function appendJobLog(logFile, line) {
|
|
2197
|
-
try {
|
|
2198
|
-
mkdirSync4(dirname3(logFile), { recursive: true });
|
|
2199
|
-
const body = line.endsWith("\n") ? line : `${line}
|
|
2200
|
-
`;
|
|
2201
|
-
appendFileSync2(logFile, body);
|
|
2202
|
-
} catch {
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
async function stopDetachedJob(cwd, id, opts) {
|
|
2206
|
-
const jobId = sanitizeDetachedJobId(id);
|
|
2207
|
-
const root = cwd.trim() || process.cwd();
|
|
2208
|
-
const dir = resolveJobDir(root, jobId);
|
|
2209
|
-
const sleep = opts?.sleep ?? sleepMs;
|
|
2210
|
-
const why = opts?.reason?.trim() ?? "";
|
|
2211
|
-
if (!existsSync3(dir)) {
|
|
2212
|
-
return {
|
|
2213
|
-
stillRunning: false,
|
|
2214
|
-
ok: false,
|
|
2215
|
-
failed: true,
|
|
2216
|
-
status: "failed",
|
|
2217
|
-
id: jobId,
|
|
2218
|
-
delta: "",
|
|
2219
|
-
progress: "No detached job. Start one first.",
|
|
2220
|
-
hint: "Start with detached-job.cjs start <id> -- <command>, then wait_for_job or stop_job.",
|
|
2221
|
-
stopped: false,
|
|
2222
|
-
reason: "not-found"
|
|
2223
|
-
};
|
|
2224
|
-
}
|
|
2225
|
-
let snap = snapshotJob(dir);
|
|
2226
|
-
if (!snap.running) {
|
|
2227
|
-
const result2 = toResult(jobId, snap);
|
|
2228
|
-
return {
|
|
2229
|
-
...result2,
|
|
2230
|
-
stopped: false,
|
|
2231
|
-
reason: "not-running",
|
|
2232
|
-
hint: "That job is already finished. Read the log; do not wait again unless you start a new command."
|
|
2233
|
-
};
|
|
2234
|
-
}
|
|
2235
|
-
appendJobLog(snap.log, `$ stop${why ? `: ${why}` : ""}`);
|
|
2236
|
-
const targetPid = snap.pid;
|
|
2237
|
-
if (targetPid != null) killProcessTree(targetPid, "SIGTERM");
|
|
2238
|
-
const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
|
|
2239
|
-
const deadline = Date.now() + graceMs;
|
|
2240
|
-
while (Date.now() < deadline && jobTreeAlive(targetPid)) {
|
|
2241
|
-
await sleep(Math.min(100, Math.max(20, deadline - Date.now())));
|
|
2242
|
-
}
|
|
2243
|
-
if (targetPid != null) {
|
|
2244
|
-
killProcessTree(targetPid, "SIGKILL");
|
|
2245
|
-
await sleep(100);
|
|
2246
|
-
}
|
|
2247
|
-
const exitFile = join5(dir, "exit");
|
|
2248
|
-
if (!existsSync3(exitFile)) {
|
|
2249
|
-
try {
|
|
2250
|
-
writeFileSync3(exitFile, "1\n");
|
|
2251
|
-
} catch {
|
|
2252
|
-
}
|
|
2253
|
-
}
|
|
2254
|
-
snap = snapshotJob(dir);
|
|
2255
|
-
const stillRunning = jobTreeAlive(targetPid) || snap.running;
|
|
2256
|
-
const result = toResult(jobId, snap, { failed: true });
|
|
2257
|
-
const stopped = !stillRunning;
|
|
2258
|
-
return {
|
|
2259
|
-
...result,
|
|
2260
|
-
stillRunning,
|
|
2261
|
-
ok: false,
|
|
2262
|
-
failed: true,
|
|
2263
|
-
status: stillRunning ? "running" : "failed",
|
|
2264
|
-
stopped,
|
|
2265
|
-
reason: stopped ? "stopped" : "still-running",
|
|
2266
|
-
stopReason: why || void 0,
|
|
2267
|
-
hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
|
|
2268
|
-
};
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
// src/hook/conductor.ts
|
|
2272
|
-
var PORT_RANGE_SIZE = 10;
|
|
2273
|
-
var RECLAIM_RETRY_DELAY_MS = 40;
|
|
2274
|
-
var RECLAIM_TRIES = 8;
|
|
2275
|
-
function matchSimpleGlob(pattern, name) {
|
|
2276
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
2277
|
-
return new RegExp(`^${escaped}$`).test(name);
|
|
2278
|
-
}
|
|
2279
|
-
function readWorktreeInclude(repoPath) {
|
|
2280
|
-
const path = join6(repoPath, ".worktreeinclude");
|
|
2281
|
-
if (!existsSync4(path)) return [];
|
|
2282
|
-
return readFileSync4(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
2283
|
-
}
|
|
2284
|
-
function resolveFilesToCopy(repoPath) {
|
|
2285
|
-
const fromInclude = readWorktreeInclude(repoPath);
|
|
2286
|
-
if (fromInclude.length) return fromInclude;
|
|
2287
|
-
const settings = loadRepoSettings(repoPath);
|
|
2288
|
-
if (settings?.filesToCopy?.length) return settings.filesToCopy;
|
|
2289
|
-
if (settings?.fileIncludeGlobs?.length) {
|
|
2290
|
-
const matched = [];
|
|
2291
|
-
try {
|
|
2292
|
-
for (const entry of readdirSync2(repoPath, { withFileTypes: true })) {
|
|
2293
|
-
if (!entry.isFile()) continue;
|
|
2294
|
-
for (const glob of settings.fileIncludeGlobs) {
|
|
2295
|
-
if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob(basename(glob), entry.name)) {
|
|
2296
|
-
matched.push(entry.name);
|
|
2297
|
-
break;
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2300
|
-
}
|
|
2301
|
-
} catch {
|
|
2302
|
-
}
|
|
2303
|
-
if (matched.length) return [...new Set(matched)];
|
|
2304
|
-
}
|
|
2305
|
-
const defaults = [];
|
|
2306
|
-
try {
|
|
2307
|
-
for (const entry of readdirSync2(repoPath, { withFileTypes: true })) {
|
|
2308
|
-
if (entry.isFile() && entry.name.startsWith(".env")) {
|
|
2309
|
-
defaults.push(entry.name);
|
|
2310
|
-
}
|
|
2311
|
-
}
|
|
2312
|
-
} catch {
|
|
2313
|
-
}
|
|
2314
|
-
if (defaults.length) return defaults;
|
|
2315
|
-
return [".env.local", ".env"];
|
|
2316
|
-
}
|
|
2317
|
-
function copyConfiguredFiles(repoPath, worktreePath) {
|
|
2318
|
-
const patterns = resolveFilesToCopy(repoPath);
|
|
2319
|
-
const copied = [];
|
|
2320
|
-
for (const rel of patterns) {
|
|
2321
|
-
const src = join6(repoPath, rel);
|
|
2322
|
-
if (!existsSync4(src)) continue;
|
|
2323
|
-
const dest = join6(worktreePath, rel);
|
|
2324
|
-
mkdirSync5(dirname4(dest), { recursive: true });
|
|
2325
|
-
copyFileSync(src, dest);
|
|
2326
|
-
copied.push(rel);
|
|
2327
|
-
}
|
|
2328
|
-
return copied;
|
|
2329
|
-
}
|
|
2330
|
-
var cachedLoginEnv = null;
|
|
2331
|
-
async function captureLoginEnv() {
|
|
2332
|
-
if (cachedLoginEnv) return { ...cachedLoginEnv };
|
|
2333
|
-
const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
|
|
2334
|
-
try {
|
|
2335
|
-
const result = await execa2(shell, ["-l", "-c", "env -0"], {
|
|
2336
|
-
reject: false,
|
|
2337
|
-
timeout: 5e3
|
|
2338
|
-
});
|
|
2339
|
-
if (result.exitCode === 0 && result.stdout) {
|
|
2340
|
-
const env = { ...process.env };
|
|
2341
|
-
for (const entry of result.stdout.split("\0")) {
|
|
2342
|
-
if (!entry) continue;
|
|
2343
|
-
const eq = entry.indexOf("=");
|
|
2344
|
-
if (eq <= 0) continue;
|
|
2345
|
-
const key = entry.slice(0, eq);
|
|
2346
|
-
const value = entry.slice(eq + 1);
|
|
2347
|
-
if (key) env[key] = value;
|
|
2348
|
-
}
|
|
2349
|
-
cachedLoginEnv = env;
|
|
2350
|
-
return { ...env };
|
|
2351
|
-
}
|
|
2352
|
-
} catch {
|
|
2353
|
-
}
|
|
2354
|
-
cachedLoginEnv = { ...process.env };
|
|
2355
|
-
return { ...cachedLoginEnv };
|
|
2356
|
-
}
|
|
2357
|
-
function buildWorkspaceScriptEnv(opts, baseEnv) {
|
|
2358
|
-
const env = stripNestedElectronEnv({
|
|
2359
|
-
...baseEnv ?? process.env
|
|
2360
|
-
});
|
|
2361
|
-
const name = opts.workspaceName ?? basename(opts.worktreePath);
|
|
2362
|
-
const ports = opts.ports ?? [];
|
|
2363
|
-
const primary = ports[0];
|
|
2364
|
-
env.SIDEBOARD_WORKSPACE_NAME = name;
|
|
2365
|
-
env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
|
|
2366
|
-
env.SIDEBOARD_ROOT_PATH = opts.repoPath;
|
|
2367
|
-
env.SIDEBOARD_IS_LOCAL = "1";
|
|
2368
|
-
if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
|
|
2369
|
-
env.CONDUCTOR_WORKSPACE_NAME = name;
|
|
2370
|
-
env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
|
|
2371
|
-
env.CONDUCTOR_ROOT_PATH = opts.repoPath;
|
|
2372
|
-
env.CONDUCTOR_IS_LOCAL = "1";
|
|
2373
|
-
if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
|
|
2374
|
-
if (primary != null) {
|
|
2375
|
-
env.SIDEBOARD_PORT = String(primary);
|
|
2376
|
-
env.CONDUCTOR_PORT = String(primary);
|
|
2377
|
-
env.PORT = String(primary);
|
|
2378
|
-
for (let i = 1; i < ports.length; i++) {
|
|
2379
|
-
const p = ports[i];
|
|
2380
|
-
env[`SIDEBOARD_PORT_${i}`] = String(p);
|
|
2381
|
-
env[`CONDUCTOR_PORT_${i}`] = String(p);
|
|
2382
|
-
}
|
|
2383
|
-
}
|
|
2384
|
-
applyWorktreePkgCacheEnv(env, opts.worktreePath);
|
|
2385
|
-
return env;
|
|
2386
|
-
}
|
|
2387
|
-
function pipeLines(stream, onLine) {
|
|
2388
|
-
if (!stream || !onLine) return;
|
|
2389
|
-
const rl = createInterface2({ input: stream });
|
|
2390
|
-
rl.on("line", onLine);
|
|
2391
|
-
}
|
|
2392
|
-
function killableListenerPids(raw, selfPid = process.pid) {
|
|
2393
|
-
const pids = [];
|
|
2394
|
-
for (const token of raw.trim().split(/\s+/)) {
|
|
2395
|
-
const pid = Number(token);
|
|
2396
|
-
if (!Number.isFinite(pid) || pid <= 0 || pid === selfPid) continue;
|
|
2397
|
-
pids.push(pid);
|
|
2398
|
-
}
|
|
2399
|
-
return pids;
|
|
2400
|
-
}
|
|
2401
|
-
function listUnixListenerPids(port, selfPid) {
|
|
2402
|
-
try {
|
|
2403
|
-
const raw = execFileSync("lsof", [`-tiTCP:${port}`, "-sTCP:LISTEN"], {
|
|
2404
|
-
encoding: "utf8",
|
|
2405
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
2406
|
-
timeout: 2e3
|
|
2407
|
-
});
|
|
2408
|
-
return killableListenerPids(raw, selfPid);
|
|
2409
|
-
} catch {
|
|
2410
|
-
return [];
|
|
2411
|
-
}
|
|
2412
|
-
}
|
|
2413
|
-
function listenerCwd(pid) {
|
|
2414
|
-
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
2415
|
-
try {
|
|
2416
|
-
const raw = execFileSync("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
|
|
2417
|
-
encoding: "utf8",
|
|
2418
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
2419
|
-
timeout: 2e3
|
|
2420
|
-
});
|
|
2421
|
-
for (const line of raw.split("\n")) {
|
|
2422
|
-
if (line.startsWith("n") && line.length > 1) return line.slice(1);
|
|
2423
|
-
}
|
|
2424
|
-
} catch {
|
|
2425
|
-
}
|
|
2426
|
-
return null;
|
|
2427
|
-
}
|
|
2428
|
-
function realpathOrResolve(path) {
|
|
2429
|
-
try {
|
|
2430
|
-
return realpathSync(path);
|
|
2431
|
-
} catch {
|
|
2432
|
-
return resolve(path);
|
|
2433
|
-
}
|
|
2434
|
-
}
|
|
2435
|
-
function isOtherWorktreeListenerCwd(cwd, worktreePath) {
|
|
2436
|
-
const wt = realpathOrResolve(worktreePath);
|
|
2437
|
-
const c = realpathOrResolve(cwd);
|
|
2438
|
-
if (c === wt || c.startsWith(wt + sep)) return false;
|
|
2439
|
-
const parent = dirname4(wt);
|
|
2440
|
-
const thisName = basename(wt);
|
|
2441
|
-
if (!thisName || c === parent) return false;
|
|
2442
|
-
if (!c.startsWith(parent + sep)) return false;
|
|
2443
|
-
const first = c.slice(parent.length + 1).split(sep)[0];
|
|
2444
|
-
return Boolean(first) && first !== thisName;
|
|
2445
|
-
}
|
|
2446
|
-
function signalPid(pid, signal) {
|
|
2447
|
-
try {
|
|
2448
|
-
process.kill(pid, signal);
|
|
2449
|
-
} catch {
|
|
2450
|
-
}
|
|
2451
|
-
}
|
|
2452
|
-
function isThisWorktreeListenerCwd(cwd, worktreePath) {
|
|
2453
|
-
const wt = realpathOrResolve(worktreePath);
|
|
2454
|
-
const c = realpathOrResolve(cwd);
|
|
2455
|
-
return c === wt || c.startsWith(wt + sep);
|
|
2456
|
-
}
|
|
2457
|
-
function killDetachedRunTree(pid, worktreePath) {
|
|
2458
|
-
if (!Number.isFinite(pid) || pid <= 1 || pid === process.pid) return;
|
|
2459
|
-
const cwd = listenerCwd(pid);
|
|
2460
|
-
if (cwd) {
|
|
2461
|
-
if (!isThisWorktreeListenerCwd(cwd, worktreePath)) return;
|
|
2462
|
-
} else if (!processGroupAlive(pid)) {
|
|
2463
|
-
return;
|
|
2464
|
-
}
|
|
2465
|
-
try {
|
|
2466
|
-
process.kill(-pid, "SIGTERM");
|
|
2467
|
-
} catch {
|
|
2468
|
-
signalPid(pid, "SIGTERM");
|
|
2469
|
-
}
|
|
2470
|
-
}
|
|
2471
|
-
function killListenersOnPorts(ports) {
|
|
2472
|
-
const selfPid = process.pid;
|
|
2473
|
-
for (const port of ports) {
|
|
2474
|
-
if (!Number.isFinite(port) || port <= 0) continue;
|
|
2475
|
-
try {
|
|
2476
|
-
if (process.platform === "win32") {
|
|
2477
|
-
execFileSync(
|
|
2478
|
-
"powershell",
|
|
2479
|
-
[
|
|
2480
|
-
"-NoProfile",
|
|
2481
|
-
"-Command",
|
|
2482
|
-
`Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -ne ${selfPid} } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
|
|
2483
|
-
],
|
|
2484
|
-
{ stdio: "ignore", timeout: 3e3 }
|
|
2485
|
-
);
|
|
2486
|
-
continue;
|
|
2487
|
-
}
|
|
2488
|
-
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
2489
|
-
signalPid(pid, "SIGTERM");
|
|
2490
|
-
}
|
|
2491
|
-
} catch {
|
|
2492
|
-
}
|
|
2493
|
-
}
|
|
2494
|
-
}
|
|
2495
|
-
function killStaleWorktreeListeners(ports, worktreePath, signal = "SIGTERM") {
|
|
2496
|
-
if (!worktreePath.trim()) {
|
|
2497
|
-
killListenersOnPorts(ports);
|
|
2498
|
-
return;
|
|
2499
|
-
}
|
|
2500
|
-
if (process.platform === "win32") {
|
|
2501
|
-
killListenersOnPorts(ports);
|
|
2502
|
-
return;
|
|
2503
|
-
}
|
|
2504
|
-
const selfPid = process.pid;
|
|
2505
|
-
for (const port of ports) {
|
|
2506
|
-
if (!Number.isFinite(port) || port <= 0) continue;
|
|
2507
|
-
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
2508
|
-
const cwd = listenerCwd(pid);
|
|
2509
|
-
if (cwd && isOtherWorktreeListenerCwd(cwd, worktreePath)) continue;
|
|
2510
|
-
signalPid(pid, signal);
|
|
2511
|
-
}
|
|
2512
|
-
}
|
|
2513
|
-
}
|
|
2514
|
-
function killScriptTree(child, ports = []) {
|
|
2515
|
-
const pid = child.pid;
|
|
2516
|
-
if (pid) {
|
|
2517
|
-
try {
|
|
2518
|
-
if (process.platform === "win32") {
|
|
2519
|
-
void execa2("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
|
|
2520
|
-
} else {
|
|
2521
|
-
process.kill(-pid, "SIGTERM");
|
|
2522
|
-
setTimeout(() => {
|
|
2523
|
-
try {
|
|
2524
|
-
process.kill(-pid, "SIGKILL");
|
|
2525
|
-
} catch {
|
|
2526
|
-
}
|
|
2527
|
-
}, 2500).unref?.();
|
|
2528
|
-
}
|
|
2529
|
-
} catch {
|
|
2530
|
-
try {
|
|
2531
|
-
child.kill("SIGTERM");
|
|
2532
|
-
} catch {
|
|
2533
|
-
}
|
|
658
|
+
function agentGitPrompt(action, opts) {
|
|
659
|
+
switch (action) {
|
|
660
|
+
case "commit-push":
|
|
661
|
+
return "Commit and push.";
|
|
662
|
+
case "create-draft":
|
|
663
|
+
return "Commit, push, and open a draft PR.";
|
|
664
|
+
case "create-web":
|
|
665
|
+
return "Commit, push, and open a PR in the browser.";
|
|
666
|
+
case "resolve-conflicts": {
|
|
667
|
+
const base = opts?.prBase?.trim().replace(/^refs\/heads\//, "");
|
|
668
|
+
const named = base ? ` (${base})` : "";
|
|
669
|
+
return `Merge the remote branch${named} into your branch and resolve conflicts. Then, commit and push your changes.`;
|
|
2534
670
|
}
|
|
671
|
+
case "ready-for-review":
|
|
672
|
+
return "Ready for review.";
|
|
673
|
+
case "merge":
|
|
674
|
+
return "Merge PR.";
|
|
2535
675
|
}
|
|
2536
|
-
killListenersOnPorts(ports);
|
|
2537
676
|
}
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
repoPath: opts.repoPath,
|
|
2544
|
-
workspaceName: opts.workspaceName,
|
|
2545
|
-
defaultBranch: opts.defaultBranch,
|
|
2546
|
-
ports: opts.ports
|
|
2547
|
-
},
|
|
2548
|
-
loginEnv
|
|
2549
|
-
);
|
|
2550
|
-
try {
|
|
2551
|
-
mergeAgentGitAuthEnv(
|
|
2552
|
-
env,
|
|
2553
|
-
await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
|
|
2554
|
-
);
|
|
2555
|
-
} catch {
|
|
677
|
+
function resolveSidebarGitPrompt(action, opts) {
|
|
678
|
+
if (action === "create-pr" || action === "create-draft" || action === "create-web") {
|
|
679
|
+
const custom = opts?.createPr?.trim();
|
|
680
|
+
if (custom) return custom;
|
|
681
|
+
return agentGitPrompt(action === "create-pr" ? "create-draft" : action);
|
|
2556
682
|
}
|
|
2557
|
-
if (
|
|
2558
|
-
|
|
683
|
+
if (action === "resolve-conflicts") {
|
|
684
|
+
const custom = opts?.resolveMergeConflicts?.trim();
|
|
685
|
+
if (custom) return custom;
|
|
686
|
+
return agentGitPrompt("resolve-conflicts", opts);
|
|
2559
687
|
}
|
|
2560
|
-
|
|
2561
|
-
const child = execa2(shell, ["-lc", command], {
|
|
2562
|
-
cwd: opts.worktreePath,
|
|
2563
|
-
reject: false,
|
|
2564
|
-
env,
|
|
2565
|
-
// Own process group so Stop can tear down the whole tree (not just the shell).
|
|
2566
|
-
// There is no settings.toml `stop=` / teardown hook for run scripts.
|
|
2567
|
-
...process.platform === "win32" ? {} : { detached: true }
|
|
2568
|
-
});
|
|
2569
|
-
pipeLines(child.stdout, opts.onLine);
|
|
2570
|
-
pipeLines(child.stderr, opts.onLine);
|
|
2571
|
-
const ports = opts.ports ?? [];
|
|
2572
|
-
return {
|
|
2573
|
-
pid: child.pid,
|
|
2574
|
-
kill: () => killScriptTree(child, ports),
|
|
2575
|
-
done: child.then((r) => r.exitCode ?? null),
|
|
2576
|
-
child
|
|
2577
|
-
};
|
|
688
|
+
return agentGitPrompt(action, opts);
|
|
2578
689
|
}
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
690
|
+
var RESOLVE_CONFLICTS_RE = /^Merge the remote branch(?: \(([^)]+)\))? into your branch and resolve conflicts\. Then, commit and push your changes\.$/;
|
|
691
|
+
function gitActionExpansion(action, base) {
|
|
692
|
+
switch (action) {
|
|
693
|
+
case "commit-push":
|
|
694
|
+
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.";
|
|
695
|
+
case "create-draft":
|
|
696
|
+
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.";
|
|
697
|
+
case "create-web":
|
|
698
|
+
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.";
|
|
699
|
+
case "resolve-conflicts":
|
|
700
|
+
return `Fetch the PR base${base ? ` (\`${base}\`)` : ""}, merge it into this branch, resolve conflicts carefully, then commit and push until the PR is mergeable.`;
|
|
701
|
+
case "ready-for-review":
|
|
702
|
+
return "This phrase is the explicit ask to mark this thread's draft pull request ready for review on GitHub (`gh pr ready -R <origin-owner/name>` or `gh pr ready`). Update title/body if the purpose drifted. Do not merge.";
|
|
703
|
+
case "merge":
|
|
704
|
+
return "This phrase is the explicit ask to merge this thread's open pull request on GitHub. If `gh stack view` shows a stack, use `gh stack merge`; otherwise `gh pr merge` (respect repo defaults / squash vs merge). Do not force-push main/master or merge locally into the main checkout.";
|
|
2584
705
|
}
|
|
2585
|
-
return handle.done;
|
|
2586
|
-
}
|
|
2587
|
-
async function runSetupScript(repoPath, worktreePath, onLine, opts) {
|
|
2588
|
-
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
2589
|
-
if (!settings?.setup) return { ran: false, exitCode: null, source: null };
|
|
2590
|
-
const source = workspaceSettingsSourceLabel(worktreePath, repoPath);
|
|
2591
|
-
onLine?.(`[setup] ${source ?? "settings.toml"}`);
|
|
2592
|
-
const handle = await spawnWorkspaceScript(settings.setup, {
|
|
2593
|
-
worktreePath,
|
|
2594
|
-
repoPath,
|
|
2595
|
-
defaultBranch: opts?.defaultBranch,
|
|
2596
|
-
onLine
|
|
2597
|
-
});
|
|
2598
|
-
const exitCode = await attachAbort(handle, opts?.signal);
|
|
2599
|
-
return {
|
|
2600
|
-
ran: true,
|
|
2601
|
-
exitCode,
|
|
2602
|
-
source,
|
|
2603
|
-
kill: handle.kill
|
|
2604
|
-
};
|
|
2605
|
-
}
|
|
2606
|
-
async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
|
|
2607
|
-
const found = findConventionSetup(worktreePath, repoPath);
|
|
2608
|
-
if (!found) return { ran: false, exitCode: null, source: null };
|
|
2609
|
-
onLine?.(`[setup] ${found.source}`);
|
|
2610
|
-
const handle = await spawnWorkspaceScript(found.command, {
|
|
2611
|
-
worktreePath,
|
|
2612
|
-
repoPath,
|
|
2613
|
-
defaultBranch: opts?.defaultBranch,
|
|
2614
|
-
onLine
|
|
2615
|
-
});
|
|
2616
|
-
const exitCode = await attachAbort(handle, opts?.signal);
|
|
2617
|
-
return {
|
|
2618
|
-
ran: true,
|
|
2619
|
-
exitCode,
|
|
2620
|
-
source: found.source,
|
|
2621
|
-
kill: handle.kill
|
|
2622
|
-
};
|
|
2623
706
|
}
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
707
|
+
function expandCanonicalGitRequest(prompt) {
|
|
708
|
+
const trimmed = prompt.trim();
|
|
709
|
+
let action = null;
|
|
710
|
+
let base = null;
|
|
711
|
+
for (const candidate of AGENT_GIT_ACTIONS) {
|
|
712
|
+
if (candidate === "resolve-conflicts") continue;
|
|
713
|
+
if (agentGitPrompt(candidate) === trimmed) {
|
|
714
|
+
action = candidate;
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
2629
717
|
}
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
718
|
+
if (!action) {
|
|
719
|
+
const m = RESOLVE_CONFLICTS_RE.exec(trimmed);
|
|
720
|
+
if (m) {
|
|
721
|
+
action = "resolve-conflicts";
|
|
722
|
+
base = m[1] ?? null;
|
|
723
|
+
}
|
|
2633
724
|
}
|
|
2634
|
-
|
|
2635
|
-
|
|
725
|
+
let expansion = action ? gitActionExpansion(action, base) : null;
|
|
726
|
+
if (!expansion && trimmed === "Address review comments.") {
|
|
727
|
+
expansion = "Read the PR review feedback (`gh pr view --comments`, `gh api` review comments), make the requested changes, commit, and push.";
|
|
728
|
+
} else if (!expansion && FIX_CI_RE.test(trimmed)) {
|
|
729
|
+
expansion = "Investigate that failing check (`gh pr checks`, `gh run view --log-failed`), fix it, commit, and push. Loop only if a goal to keep going until it passes was given.";
|
|
2636
730
|
}
|
|
2637
|
-
return
|
|
731
|
+
if (!expansion) return prompt;
|
|
732
|
+
return `${trimmed}
|
|
733
|
+
|
|
734
|
+
(Sideboard git request \u2014 do not ask for clarification: ${expansion})`;
|
|
2638
735
|
}
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
repoPath,
|
|
2645
|
-
onLine
|
|
2646
|
-
});
|
|
2647
|
-
const exitCode = await handle.done;
|
|
2648
|
-
return { ran: true, exitCode };
|
|
2649
|
-
}
|
|
2650
|
-
function listRunScripts(worktreePath, repoPath) {
|
|
2651
|
-
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
2652
|
-
if (!settings?.runScripts.length) return [];
|
|
2653
|
-
return settings.runScripts.filter((s) => {
|
|
2654
|
-
if (!s.availableIn?.length) return true;
|
|
2655
|
-
return s.availableIn.includes("local");
|
|
2656
|
-
});
|
|
736
|
+
var FIX_CI_RE = /^Fix CI: .+\.$/;
|
|
737
|
+
|
|
738
|
+
// src/git/pr-merge-archive.ts
|
|
739
|
+
function normalizePrState(state) {
|
|
740
|
+
return (state ?? "").trim().toUpperCase();
|
|
2657
741
|
}
|
|
2658
|
-
function
|
|
2659
|
-
const
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
const
|
|
2665
|
-
|
|
2666
|
-
if (name) {
|
|
2667
|
-
return scripts.find((s) => s.name === name) ?? null;
|
|
2668
|
-
}
|
|
2669
|
-
return getDefaultRunScript(worktreePath, repoPath);
|
|
2670
|
-
}
|
|
2671
|
-
function getRunMode(worktreePath, repoPath) {
|
|
2672
|
-
return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
|
|
2673
|
-
}
|
|
2674
|
-
async function reservePort() {
|
|
2675
|
-
return new Promise((resolve2, reject) => {
|
|
2676
|
-
const server = createServer();
|
|
2677
|
-
let released = false;
|
|
2678
|
-
const release = () => new Promise((res) => {
|
|
2679
|
-
if (released) {
|
|
2680
|
-
res();
|
|
2681
|
-
return;
|
|
2682
|
-
}
|
|
2683
|
-
released = true;
|
|
2684
|
-
server.close(() => res());
|
|
2685
|
-
});
|
|
2686
|
-
server.listen(0, "127.0.0.1", () => {
|
|
2687
|
-
const addr = server.address();
|
|
2688
|
-
if (!addr || typeof addr === "string") {
|
|
2689
|
-
void release().then(() => reject(new Error("Failed to allocate port")));
|
|
2690
|
-
return;
|
|
2691
|
-
}
|
|
2692
|
-
resolve2({ port: addr.port, release });
|
|
2693
|
-
});
|
|
2694
|
-
server.on("error", reject);
|
|
2695
|
-
});
|
|
742
|
+
function samePrIdentity(a, b) {
|
|
743
|
+
const left = (a ?? "").trim().replace(/\/+$/, "");
|
|
744
|
+
const right = (b ?? "").trim().replace(/\/+$/, "");
|
|
745
|
+
if (!left || !right) return false;
|
|
746
|
+
if (left === right) return true;
|
|
747
|
+
const ln = left.match(/\/pull\/(\d+)/i)?.[1];
|
|
748
|
+
const rn = right.match(/\/pull\/(\d+)/i)?.[1];
|
|
749
|
+
return Boolean(ln && rn && ln === rn);
|
|
2696
750
|
}
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
const release = () => new Promise((res) => {
|
|
2703
|
-
if (released) {
|
|
2704
|
-
res();
|
|
2705
|
-
return;
|
|
2706
|
-
}
|
|
2707
|
-
released = true;
|
|
2708
|
-
server.close(() => res());
|
|
2709
|
-
});
|
|
2710
|
-
server.once("error", () => {
|
|
2711
|
-
try {
|
|
2712
|
-
server.close();
|
|
2713
|
-
} catch {
|
|
2714
|
-
}
|
|
2715
|
-
resolve2(null);
|
|
2716
|
-
});
|
|
2717
|
-
server.listen(port, "127.0.0.1", () => {
|
|
2718
|
-
resolve2({ port, release });
|
|
2719
|
-
});
|
|
2720
|
-
});
|
|
751
|
+
function shouldStopRunOnPrRetarget(previousPrUrl, nextPrUrl) {
|
|
752
|
+
const prev = (previousPrUrl ?? "").trim();
|
|
753
|
+
const next = (nextPrUrl ?? "").trim();
|
|
754
|
+
if (!prev || !next) return false;
|
|
755
|
+
return !samePrIdentity(prev, next);
|
|
2721
756
|
}
|
|
2722
|
-
function
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
const
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
if (out.length >= size) break;
|
|
2731
|
-
}
|
|
2732
|
-
return out;
|
|
757
|
+
function shouldPersistFetchedPrMeta(opts) {
|
|
758
|
+
const meta = opts.metaUrl?.trim() || "";
|
|
759
|
+
if (!meta) return false;
|
|
760
|
+
const head = opts.headPrUrl?.trim() || "";
|
|
761
|
+
if (head) return samePrIdentity(meta, head);
|
|
762
|
+
const live = opts.livePrUrl?.trim() || "";
|
|
763
|
+
if (live) return samePrIdentity(meta, live);
|
|
764
|
+
return true;
|
|
2733
765
|
}
|
|
2734
|
-
function
|
|
2735
|
-
|
|
766
|
+
function sameLoginList(a, b) {
|
|
767
|
+
const left = [...a ?? []].map((s) => s.trim().toLowerCase()).filter(Boolean).sort();
|
|
768
|
+
const right = [...b ?? []].map((s) => s.trim().toLowerCase()).filter(Boolean).sort();
|
|
769
|
+
return left.length === right.length && left.every((login, i) => login === right[i]);
|
|
2736
770
|
}
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
const
|
|
2740
|
-
const
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
held.push(next);
|
|
2750
|
-
taken.add(next.port);
|
|
2751
|
-
} else {
|
|
2752
|
-
const ephemeral = await reservePort();
|
|
2753
|
-
held.push(ephemeral);
|
|
2754
|
-
taken.add(ephemeral.port);
|
|
2755
|
-
}
|
|
2756
|
-
}
|
|
2757
|
-
return held;
|
|
2758
|
-
} catch (err) {
|
|
2759
|
-
await Promise.all(held.map((h) => h.release()));
|
|
2760
|
-
throw err;
|
|
771
|
+
function threadPrMetaPatch(thread, meta) {
|
|
772
|
+
const prevState = normalizePrState(thread.prState);
|
|
773
|
+
const nextState = normalizePrState(meta.state);
|
|
774
|
+
const patch = {};
|
|
775
|
+
if (meta.url && meta.url !== thread.prUrl) patch.prUrl = meta.url;
|
|
776
|
+
if (meta.title && meta.title !== thread.prTitle) patch.prTitle = meta.title;
|
|
777
|
+
if (nextState && nextState !== prevState) patch.prState = nextState;
|
|
778
|
+
const nextDraft = Boolean(meta.isDraft) && nextState !== "MERGED" && nextState !== "CLOSED";
|
|
779
|
+
if (nextDraft !== Boolean(thread.prIsDraft)) patch.prIsDraft = nextDraft;
|
|
780
|
+
const nextAuthor = meta.authorLogin?.trim() || "";
|
|
781
|
+
if (nextAuthor && nextAuthor !== (thread.prAuthorLogin ?? "").trim()) {
|
|
782
|
+
patch.prAuthorLogin = nextAuthor;
|
|
2761
783
|
}
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
for (const port of ports) {
|
|
2766
|
-
const next = await tryReservePort(port);
|
|
2767
|
-
if (next) held.push(next);
|
|
784
|
+
const nextReviewers = meta.reviewerLogins ?? [];
|
|
785
|
+
if (!sameLoginList(nextReviewers, thread.prReviewerLogins)) {
|
|
786
|
+
patch.prReviewerLogins = nextReviewers;
|
|
2768
787
|
}
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
const primary = ports[0];
|
|
2773
|
-
if (primary == null) return null;
|
|
2774
|
-
const takePrimary = async () => {
|
|
2775
|
-
const held2 = await tryReserveAvailablePorts(ports);
|
|
2776
|
-
if (held2.some((h) => h.port === primary)) return held2;
|
|
2777
|
-
await Promise.all(held2.map((h) => h.release()));
|
|
2778
|
-
return null;
|
|
2779
|
-
};
|
|
2780
|
-
let held = await takePrimary();
|
|
2781
|
-
if (held) return held;
|
|
2782
|
-
if (worktreePath) {
|
|
2783
|
-
killStaleWorktreeListeners(ports, worktreePath, "SIGTERM");
|
|
2784
|
-
} else {
|
|
2785
|
-
killListenersOnPorts(ports);
|
|
788
|
+
const nextDecision = meta.reviewDecision ?? null;
|
|
789
|
+
if ((thread.prReviewDecision ?? null) !== nextDecision) {
|
|
790
|
+
patch.prReviewDecision = nextDecision;
|
|
2786
791
|
}
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
if (held) return held;
|
|
2791
|
-
if (i === 2 && worktreePath) {
|
|
2792
|
-
killStaleWorktreeListeners(ports, worktreePath, "SIGKILL");
|
|
2793
|
-
}
|
|
792
|
+
const nextMergeable = meta.mergeable ?? null;
|
|
793
|
+
if ((thread.prMergeable ?? null) !== nextMergeable) {
|
|
794
|
+
patch.prMergeable = nextMergeable;
|
|
2794
795
|
}
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
}
|
|
2803
|
-
const
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
const
|
|
2808
|
-
if (
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
worktreePath
|
|
2813
|
-
});
|
|
2814
|
-
const ports = held.map((h) => h.port);
|
|
2815
|
-
saveWorktreeRunPorts(worktreePath, script.name, ports);
|
|
2816
|
-
try {
|
|
2817
|
-
const handle = await spawnWorkspaceScript(script.command, {
|
|
2818
|
-
worktreePath,
|
|
2819
|
-
repoPath,
|
|
2820
|
-
ports,
|
|
2821
|
-
heldPorts: held,
|
|
2822
|
-
defaultBranch: opts?.defaultBranch,
|
|
2823
|
-
onLine
|
|
2824
|
-
});
|
|
2825
|
-
return {
|
|
2826
|
-
pid: handle.pid,
|
|
2827
|
-
port: ports[0],
|
|
2828
|
-
ports,
|
|
2829
|
-
scriptName: script.name,
|
|
2830
|
-
kill: handle.kill,
|
|
2831
|
-
done: handle.done
|
|
2832
|
-
};
|
|
2833
|
-
} catch (err) {
|
|
2834
|
-
await Promise.all(held.map((h) => h.release()));
|
|
2835
|
-
throw err;
|
|
796
|
+
const nextMergeState = meta.mergeStateStatus ?? null;
|
|
797
|
+
if ((thread.prMergeStateStatus ?? null) !== nextMergeState) {
|
|
798
|
+
patch.prMergeStateStatus = nextMergeState;
|
|
799
|
+
}
|
|
800
|
+
const nextQueued = Boolean(meta.isInMergeQueue);
|
|
801
|
+
if (Boolean(thread.prIsInMergeQueue) !== nextQueued) {
|
|
802
|
+
patch.prIsInMergeQueue = nextQueued;
|
|
803
|
+
}
|
|
804
|
+
const nextLabels = meta.labels ?? [];
|
|
805
|
+
if (!sameLoginList(nextLabels, thread.prLabels)) {
|
|
806
|
+
patch.prLabels = nextLabels;
|
|
807
|
+
}
|
|
808
|
+
const mergeState = (nextMergeState ?? "").toUpperCase();
|
|
809
|
+
if (mergeState === "CLEAN" && thread.prChecksFailed) {
|
|
810
|
+
patch.prChecksFailed = false;
|
|
811
|
+
} else if ((mergeState === "UNSTABLE" || mergeState === "BLOCKED" && (nextDecision ?? "").toUpperCase() === "APPROVED") && !thread.prChecksFailed) {
|
|
812
|
+
patch.prChecksFailed = true;
|
|
2836
813
|
}
|
|
814
|
+
if (thread.skipAutoArchiveOnMerge && nextState && nextState !== "MERGED" && nextState !== "CLOSED") {
|
|
815
|
+
patch.skipAutoArchiveOnMerge = false;
|
|
816
|
+
}
|
|
817
|
+
return patch;
|
|
818
|
+
}
|
|
819
|
+
function threadPrChecksFailedPatch(thread, failed) {
|
|
820
|
+
if (thread.prChecksFailed === failed) return {};
|
|
821
|
+
return { prChecksFailed: failed };
|
|
822
|
+
}
|
|
823
|
+
function shouldAutoArchiveOnPrMerge(opts) {
|
|
824
|
+
if (!opts.autoArchiveEnabled) return false;
|
|
825
|
+
if (opts.isGlobal) return false;
|
|
826
|
+
if (opts.skipAutoArchiveOnMerge) return false;
|
|
827
|
+
if (opts.threadStatus === "archived") return false;
|
|
828
|
+
const next = normalizePrState(opts.nextPrState);
|
|
829
|
+
if (next !== "MERGED") return false;
|
|
830
|
+
return normalizePrState(opts.previousPrState) !== "MERGED";
|
|
2837
831
|
}
|
|
2838
832
|
|
|
2839
833
|
// src/store/setup-log.ts
|
|
2840
834
|
import { createHash } from "crypto";
|
|
2841
|
-
import { existsSync
|
|
2842
|
-
import { join as
|
|
835
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
836
|
+
import { join as join2 } from "path";
|
|
2843
837
|
var EMPTY = {
|
|
2844
838
|
output: "",
|
|
2845
839
|
running: false,
|
|
@@ -2881,7 +875,7 @@ function materialize(mem) {
|
|
|
2881
875
|
};
|
|
2882
876
|
}
|
|
2883
877
|
function setupLogPath(threadId) {
|
|
2884
|
-
return
|
|
878
|
+
return join2(threadsDir(), `${threadId}.setup.log.json`);
|
|
2885
879
|
}
|
|
2886
880
|
function setupLogKeyForWorktree(worktreePath) {
|
|
2887
881
|
const norm = normalizeWorktreePath(worktreePath);
|
|
@@ -2900,7 +894,7 @@ function persistNow(threadId) {
|
|
|
2900
894
|
const mem = memory.get(threadId);
|
|
2901
895
|
if (!mem) return;
|
|
2902
896
|
try {
|
|
2903
|
-
|
|
897
|
+
writeFileSync(
|
|
2904
898
|
setupLogPath(threadId),
|
|
2905
899
|
`${JSON.stringify(materialize(mem))}
|
|
2906
900
|
`,
|
|
@@ -2921,9 +915,9 @@ function schedulePersist(threadId) {
|
|
|
2921
915
|
}
|
|
2922
916
|
function loadFromDisk(threadId) {
|
|
2923
917
|
const path = setupLogPath(threadId);
|
|
2924
|
-
if (!
|
|
918
|
+
if (!existsSync(path)) return null;
|
|
2925
919
|
try {
|
|
2926
|
-
const raw = JSON.parse(
|
|
920
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
2927
921
|
return {
|
|
2928
922
|
output: typeof raw.output === "string" ? raw.output : "",
|
|
2929
923
|
running: Boolean(raw.running),
|
|
@@ -2982,8 +976,8 @@ function finishSetupLog(threadId, exitCode, source) {
|
|
|
2982
976
|
|
|
2983
977
|
// src/store/run-log.ts
|
|
2984
978
|
import { createHash as createHash2 } from "crypto";
|
|
2985
|
-
import { existsSync as
|
|
2986
|
-
import { join as
|
|
979
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
980
|
+
import { join as join3 } from "path";
|
|
2987
981
|
var EMPTY2 = {
|
|
2988
982
|
output: "",
|
|
2989
983
|
running: false,
|
|
@@ -3022,7 +1016,7 @@ function safeScriptToken(scriptName) {
|
|
|
3022
1016
|
return token || "dev";
|
|
3023
1017
|
}
|
|
3024
1018
|
function runLogPath(key) {
|
|
3025
|
-
return
|
|
1019
|
+
return join3(threadsDir(), `${key}.run.log.json`);
|
|
3026
1020
|
}
|
|
3027
1021
|
function runLogKeyForWorktree(worktreePath, scriptName) {
|
|
3028
1022
|
const norm = normalizeWorktreePath(worktreePath);
|
|
@@ -3051,7 +1045,7 @@ function persistNow2(key) {
|
|
|
3051
1045
|
const mem = memory2.get(key);
|
|
3052
1046
|
if (!mem) return;
|
|
3053
1047
|
try {
|
|
3054
|
-
|
|
1048
|
+
writeFileSync2(runLogPath(key), `${JSON.stringify(materialize2(mem))}
|
|
3055
1049
|
`, "utf8");
|
|
3056
1050
|
} catch {
|
|
3057
1051
|
}
|
|
@@ -3068,9 +1062,9 @@ function schedulePersist2(key) {
|
|
|
3068
1062
|
}
|
|
3069
1063
|
function loadFromDisk2(key) {
|
|
3070
1064
|
const path = runLogPath(key);
|
|
3071
|
-
if (!
|
|
1065
|
+
if (!existsSync2(path)) return null;
|
|
3072
1066
|
try {
|
|
3073
|
-
const raw = JSON.parse(
|
|
1067
|
+
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
3074
1068
|
return {
|
|
3075
1069
|
output: typeof raw.output === "string" ? raw.output : "",
|
|
3076
1070
|
running: Boolean(raw.running),
|
|
@@ -3130,8 +1124,8 @@ function flushRunLog(key) {
|
|
|
3130
1124
|
}
|
|
3131
1125
|
|
|
3132
1126
|
// src/git/orphan-cleanup.ts
|
|
3133
|
-
import { existsSync as
|
|
3134
|
-
import { join as
|
|
1127
|
+
import { existsSync as existsSync3, readdirSync, statSync } from "fs";
|
|
1128
|
+
import { join as join4 } from "path";
|
|
3135
1129
|
function isSideboardWorktreePath(path) {
|
|
3136
1130
|
return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
|
|
3137
1131
|
}
|
|
@@ -3142,9 +1136,9 @@ async function findOrphanWorktrees(repoPaths) {
|
|
|
3142
1136
|
repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
|
|
3143
1137
|
);
|
|
3144
1138
|
const homeRoot = sideboardWorkspacesDir();
|
|
3145
|
-
if (
|
|
1139
|
+
if (existsSync3(homeRoot)) {
|
|
3146
1140
|
try {
|
|
3147
|
-
for (const entry of
|
|
1141
|
+
for (const entry of readdirSync(homeRoot, { withFileTypes: true })) {
|
|
3148
1142
|
if (!entry.isDirectory()) continue;
|
|
3149
1143
|
void entry;
|
|
3150
1144
|
}
|
|
@@ -3154,7 +1148,7 @@ async function findOrphanWorktrees(repoPaths) {
|
|
|
3154
1148
|
const orphans = [];
|
|
3155
1149
|
const seen = /* @__PURE__ */ new Set();
|
|
3156
1150
|
for (const repoPath of repos) {
|
|
3157
|
-
if (!repoPath || !
|
|
1151
|
+
if (!repoPath || !existsSync3(repoPath)) continue;
|
|
3158
1152
|
try {
|
|
3159
1153
|
const wts = await listWorktrees(repoPath);
|
|
3160
1154
|
for (const wt of wts) {
|
|
@@ -3175,12 +1169,12 @@ async function findOrphanWorktrees(repoPaths) {
|
|
|
3175
1169
|
}
|
|
3176
1170
|
try {
|
|
3177
1171
|
const root = worktreesRoot(repoPath);
|
|
3178
|
-
if (
|
|
3179
|
-
for (const entry of
|
|
1172
|
+
if (existsSync3(root)) {
|
|
1173
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
3180
1174
|
if (!entry.isDirectory()) continue;
|
|
3181
|
-
const path =
|
|
1175
|
+
const path = join4(root, entry.name).replace(/\/$/, "");
|
|
3182
1176
|
if (known.has(path) || seen.has(path)) continue;
|
|
3183
|
-
if (!
|
|
1177
|
+
if (!existsSync3(join4(path, ".git"))) continue;
|
|
3184
1178
|
seen.add(path);
|
|
3185
1179
|
let mtimeMs = 0;
|
|
3186
1180
|
try {
|
|
@@ -3303,11 +1297,11 @@ function shouldRunHistoryCleanup(settings = loadAppSettings()) {
|
|
|
3303
1297
|
}
|
|
3304
1298
|
|
|
3305
1299
|
// src/git/apply-into-main.ts
|
|
3306
|
-
import { execa as
|
|
1300
|
+
import { execa as execa2 } from "execa";
|
|
3307
1301
|
async function applyThreadIntoMain(thread, opts) {
|
|
3308
1302
|
const method = opts?.method ?? "merge";
|
|
3309
1303
|
const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
|
|
3310
|
-
const status = await
|
|
1304
|
+
const status = await execa2("git", ["status", "--porcelain"], {
|
|
3311
1305
|
cwd: thread.repoPath,
|
|
3312
1306
|
reject: false
|
|
3313
1307
|
});
|
|
@@ -3316,7 +1310,7 @@ async function applyThreadIntoMain(thread, opts) {
|
|
|
3316
1310
|
`Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
|
|
3317
1311
|
);
|
|
3318
1312
|
}
|
|
3319
|
-
const checkout = await
|
|
1313
|
+
const checkout = await execa2("git", ["checkout", target], {
|
|
3320
1314
|
cwd: thread.repoPath,
|
|
3321
1315
|
reject: false
|
|
3322
1316
|
});
|
|
@@ -3326,7 +1320,7 @@ async function applyThreadIntoMain(thread, opts) {
|
|
|
3326
1320
|
);
|
|
3327
1321
|
}
|
|
3328
1322
|
if (method === "cherry-pick") {
|
|
3329
|
-
const log = await
|
|
1323
|
+
const log = await execa2(
|
|
3330
1324
|
"git",
|
|
3331
1325
|
["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
|
|
3332
1326
|
{ cwd: thread.repoPath, reject: false }
|
|
@@ -3341,12 +1335,12 @@ async function applyThreadIntoMain(thread, opts) {
|
|
|
3341
1335
|
};
|
|
3342
1336
|
}
|
|
3343
1337
|
for (const sha of shas) {
|
|
3344
|
-
const cp = await
|
|
1338
|
+
const cp = await execa2("git", ["cherry-pick", sha], {
|
|
3345
1339
|
cwd: thread.repoPath,
|
|
3346
1340
|
reject: false
|
|
3347
1341
|
});
|
|
3348
1342
|
if (cp.exitCode !== 0) {
|
|
3349
|
-
await
|
|
1343
|
+
await execa2("git", ["cherry-pick", "--abort"], {
|
|
3350
1344
|
cwd: thread.repoPath,
|
|
3351
1345
|
reject: false
|
|
3352
1346
|
});
|
|
@@ -3362,13 +1356,13 @@ async function applyThreadIntoMain(thread, opts) {
|
|
|
3362
1356
|
message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
|
|
3363
1357
|
};
|
|
3364
1358
|
}
|
|
3365
|
-
const merge = await
|
|
1359
|
+
const merge = await execa2(
|
|
3366
1360
|
"git",
|
|
3367
1361
|
["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
|
|
3368
1362
|
{ cwd: thread.repoPath, reject: false }
|
|
3369
1363
|
);
|
|
3370
1364
|
if (merge.exitCode !== 0) {
|
|
3371
|
-
await
|
|
1365
|
+
await execa2("git", ["merge", "--abort"], {
|
|
3372
1366
|
cwd: thread.repoPath,
|
|
3373
1367
|
reject: false
|
|
3374
1368
|
});
|
|
@@ -3385,25 +1379,25 @@ async function applyThreadIntoMain(thread, opts) {
|
|
|
3385
1379
|
}
|
|
3386
1380
|
|
|
3387
1381
|
// src/git/clone-repo.ts
|
|
3388
|
-
import { existsSync as
|
|
3389
|
-
import { basename
|
|
3390
|
-
import { execa as
|
|
1382
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1383
|
+
import { basename, join as join5 } from "path";
|
|
1384
|
+
import { execa as execa3 } from "execa";
|
|
3391
1385
|
async function cloneRepoIntoSideboard(opts) {
|
|
3392
1386
|
const url = opts.url.trim();
|
|
3393
1387
|
if (!url) throw new Error("Clone URL is required");
|
|
3394
1388
|
let name = opts.name?.trim();
|
|
3395
1389
|
if (!name) {
|
|
3396
|
-
const leaf =
|
|
1390
|
+
const leaf = basename(url.replace(/\/$/, "").replace(/\.git$/, ""));
|
|
3397
1391
|
name = leaf || "repo";
|
|
3398
1392
|
}
|
|
3399
1393
|
name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
3400
|
-
const dest =
|
|
3401
|
-
if (
|
|
1394
|
+
const dest = join5(sideboardReposDir(), name);
|
|
1395
|
+
if (existsSync4(dest)) {
|
|
3402
1396
|
const repoPath2 = await resolveRepoRoot(dest);
|
|
3403
1397
|
const workspace2 = await ensureWorkspace(repoPath2);
|
|
3404
1398
|
return { repoPath: repoPath2, workspace: workspace2 };
|
|
3405
1399
|
}
|
|
3406
|
-
const result = await
|
|
1400
|
+
const result = await execa3("git", ["clone", url, dest], { reject: false });
|
|
3407
1401
|
if (result.exitCode !== 0) {
|
|
3408
1402
|
throw new Error(
|
|
3409
1403
|
`git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
|
|
@@ -3420,14 +1414,14 @@ function isAdoptableRunScriptRequest(req) {
|
|
|
3420
1414
|
}
|
|
3421
1415
|
|
|
3422
1416
|
// src/store/desktop-host.ts
|
|
3423
|
-
import { readFileSync as
|
|
3424
|
-
import { join as
|
|
1417
|
+
import { readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1418
|
+
import { join as join6 } from "path";
|
|
3425
1419
|
function desktopHostPidPath() {
|
|
3426
|
-
return
|
|
1420
|
+
return join6(appDataDir(), "desktop-host.pid");
|
|
3427
1421
|
}
|
|
3428
1422
|
function readDesktopHostPid() {
|
|
3429
1423
|
try {
|
|
3430
|
-
const pid = Number.parseInt(
|
|
1424
|
+
const pid = Number.parseInt(readFileSync3(desktopHostPidPath(), "utf8").trim(), 10);
|
|
3431
1425
|
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
3432
1426
|
return pid;
|
|
3433
1427
|
} catch {
|
|
@@ -3523,7 +1517,7 @@ function agentPurposeStamp(input) {
|
|
|
3523
1517
|
}
|
|
3524
1518
|
|
|
3525
1519
|
// src/threads/create.ts
|
|
3526
|
-
import { existsSync as
|
|
1520
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3527
1521
|
|
|
3528
1522
|
// src/detect/detect.ts
|
|
3529
1523
|
var REQUIRE_AGENT_TIMEOUT_MS = 8e3;
|
|
@@ -4076,7 +2070,7 @@ function reuseLiveThread(input, repoPath, match) {
|
|
|
4076
2070
|
}
|
|
4077
2071
|
async function createThread(input, _onSetupLine) {
|
|
4078
2072
|
const repoPath = await resolveRepoRoot(input.repoPath);
|
|
4079
|
-
if (!
|
|
2073
|
+
if (!existsSync5(repoPath)) {
|
|
4080
2074
|
throw new Error(`Repo not found: ${repoPath}`);
|
|
4081
2075
|
}
|
|
4082
2076
|
const reused = reuseLiveThread(input, repoPath, {
|
|
@@ -4093,7 +2087,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
4093
2087
|
});
|
|
4094
2088
|
await requireAgent(resolved.agent);
|
|
4095
2089
|
if (input.cowboy) {
|
|
4096
|
-
const { cowboyModeEnabled } = await import("./app-settings-
|
|
2090
|
+
const { cowboyModeEnabled } = await import("./app-settings-RAC4PSSP.js");
|
|
4097
2091
|
if (!cowboyModeEnabled()) {
|
|
4098
2092
|
throw new Error(
|
|
4099
2093
|
"Cowboy mode is off. Enable it in Settings \u2192 Advanced."
|
|
@@ -4151,7 +2145,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
4151
2145
|
let sourceIsFork = false;
|
|
4152
2146
|
if (!input.cowboy && sourceType === "branch" && (!sourceRef || sourceRef === "HEAD" || sourceRef === "default")) {
|
|
4153
2147
|
if (isAbleTimeConnected() && getIssueSource() === "abletime") {
|
|
4154
|
-
const { ensureAbleTimeTask, issueAttachmentForAbleTimeTask } = await import("./abletime-
|
|
2148
|
+
const { ensureAbleTimeTask, issueAttachmentForAbleTimeTask } = await import("./abletime-4ZSDI4YP.js");
|
|
4155
2149
|
const task = await ensureAbleTimeTask({
|
|
4156
2150
|
title: title?.trim() || "Untitled work",
|
|
4157
2151
|
description: input.prompt?.trim() || void 0
|
|
@@ -4313,11 +2307,11 @@ function createLineCoalescer(onChunk, opts) {
|
|
|
4313
2307
|
|
|
4314
2308
|
// src/store/turn-live.ts
|
|
4315
2309
|
import {
|
|
4316
|
-
existsSync as
|
|
2310
|
+
existsSync as existsSync6,
|
|
4317
2311
|
renameSync,
|
|
4318
2312
|
unlinkSync as unlinkSync2,
|
|
4319
|
-
writeFileSync as
|
|
4320
|
-
readFileSync as
|
|
2313
|
+
writeFileSync as writeFileSync4,
|
|
2314
|
+
readFileSync as readFileSync4
|
|
4321
2315
|
} from "fs";
|
|
4322
2316
|
var buffers = /* @__PURE__ */ new Map();
|
|
4323
2317
|
var FLUSH_MS = 800;
|
|
@@ -4394,7 +2388,7 @@ function writeTurnLive(threadId, progress) {
|
|
|
4394
2388
|
const path = threadLivePath(threadId);
|
|
4395
2389
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
4396
2390
|
try {
|
|
4397
|
-
|
|
2391
|
+
writeFileSync4(tmp, JSON.stringify(progress), "utf8");
|
|
4398
2392
|
renameSync(tmp, path);
|
|
4399
2393
|
} catch {
|
|
4400
2394
|
try {
|
|
@@ -4405,9 +2399,9 @@ function writeTurnLive(threadId, progress) {
|
|
|
4405
2399
|
}
|
|
4406
2400
|
function readTurnLive(threadId) {
|
|
4407
2401
|
const path = threadLivePath(threadId);
|
|
4408
|
-
if (!
|
|
2402
|
+
if (!existsSync6(path)) return null;
|
|
4409
2403
|
try {
|
|
4410
|
-
const raw = JSON.parse(
|
|
2404
|
+
const raw = JSON.parse(readFileSync4(path, "utf8"));
|
|
4411
2405
|
if (!raw || typeof raw.summary !== "string") return null;
|
|
4412
2406
|
return raw;
|
|
4413
2407
|
} catch {
|
|
@@ -4419,7 +2413,7 @@ function clearTurnLive(threadId) {
|
|
|
4419
2413
|
if (buf?.timer) clearTimeout(buf.timer);
|
|
4420
2414
|
buffers.delete(threadId);
|
|
4421
2415
|
const path = threadLivePath(threadId);
|
|
4422
|
-
if (!
|
|
2416
|
+
if (!existsSync6(path)) return;
|
|
4423
2417
|
try {
|
|
4424
2418
|
unlinkSync2(path);
|
|
4425
2419
|
} catch {
|
|
@@ -4453,13 +2447,13 @@ function isStaleLastErrorDuringTurn(err) {
|
|
|
4453
2447
|
}
|
|
4454
2448
|
|
|
4455
2449
|
// src/threads/fork-worktree.ts
|
|
4456
|
-
function
|
|
2450
|
+
function requireThread(idOrRef) {
|
|
4457
2451
|
const thread = findThreadByRef(idOrRef) ?? null;
|
|
4458
2452
|
if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
|
|
4459
2453
|
return thread;
|
|
4460
2454
|
}
|
|
4461
2455
|
async function forkThreadWorktree(input, onSetupLine) {
|
|
4462
|
-
const from =
|
|
2456
|
+
const from = requireThread(input.threadId);
|
|
4463
2457
|
if (isOrchestratorThread(from)) {
|
|
4464
2458
|
throw new Error(
|
|
4465
2459
|
"fork_workspace targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
|
|
@@ -4496,7 +2490,7 @@ async function forkThreadWorktree(input, onSetupLine) {
|
|
|
4496
2490
|
}
|
|
4497
2491
|
|
|
4498
2492
|
// src/orchestrator/quota-failover.ts
|
|
4499
|
-
import { randomUUID
|
|
2493
|
+
import { randomUUID } from "crypto";
|
|
4500
2494
|
function managedKindForAgent(agent) {
|
|
4501
2495
|
return agent === "claude" || agent === "codex" ? agent : null;
|
|
4502
2496
|
}
|
|
@@ -4616,7 +2610,7 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
|
|
|
4616
2610
|
...instructions
|
|
4617
2611
|
].join("\n");
|
|
4618
2612
|
return {
|
|
4619
|
-
id:
|
|
2613
|
+
id: randomUUID(),
|
|
4620
2614
|
name: orch ? "Orchestration quota handoff.md" : "Quota handoff.md",
|
|
4621
2615
|
kind: "transcript",
|
|
4622
2616
|
content: body
|
|
@@ -4631,9 +2625,9 @@ var QUOTA_CONTINUE_ACCOUNT_PROMPT = (fromAgent, accountLabel) => [
|
|
|
4631
2625
|
"Proceed with the goal. Leave model Auto unless needed."
|
|
4632
2626
|
].join(" ");
|
|
4633
2627
|
var QUOTA_RESUME_PROMPT = "Session/usage limit window should have reset. Continue this chat from where you left off.";
|
|
4634
|
-
async function isolateQuotaFailover(
|
|
2628
|
+
async function isolateQuotaFailover(run) {
|
|
4635
2629
|
try {
|
|
4636
|
-
return await
|
|
2630
|
+
return await run();
|
|
4637
2631
|
} catch {
|
|
4638
2632
|
return true;
|
|
4639
2633
|
}
|
|
@@ -4659,29 +2653,29 @@ function createQuotaFailoverChat(from, fallbackAgent, limitText, opts) {
|
|
|
4659
2653
|
}
|
|
4660
2654
|
|
|
4661
2655
|
// src/threads/adopt.ts
|
|
4662
|
-
import { execFileSync
|
|
2656
|
+
import { execFileSync } from "child_process";
|
|
4663
2657
|
import {
|
|
4664
|
-
copyFileSync
|
|
4665
|
-
existsSync as
|
|
2658
|
+
copyFileSync,
|
|
2659
|
+
existsSync as existsSync7,
|
|
4666
2660
|
mkdtempSync,
|
|
4667
|
-
readdirSync as
|
|
4668
|
-
readFileSync as
|
|
2661
|
+
readdirSync as readdirSync2,
|
|
2662
|
+
readFileSync as readFileSync5,
|
|
4669
2663
|
rmSync
|
|
4670
2664
|
} from "fs";
|
|
4671
2665
|
import { tmpdir } from "os";
|
|
4672
|
-
import { join as
|
|
2666
|
+
import { join as join7 } from "path";
|
|
4673
2667
|
import { createRequire } from "module";
|
|
4674
|
-
var CONDUCTOR_APP_SUPPORT =
|
|
2668
|
+
var CONDUCTOR_APP_SUPPORT = join7(
|
|
4675
2669
|
process.env.HOME ?? "",
|
|
4676
2670
|
"Library",
|
|
4677
2671
|
"Application Support",
|
|
4678
2672
|
"com.conductor.app"
|
|
4679
2673
|
);
|
|
4680
|
-
var CONDUCTOR_DB =
|
|
4681
|
-
var CURSOR_SDK_STORE =
|
|
2674
|
+
var CONDUCTOR_DB = join7(CONDUCTOR_APP_SUPPORT, "conductor.db");
|
|
2675
|
+
var CURSOR_SDK_STORE = join7(CONDUCTOR_APP_SUPPORT, "cursor-sdk-store");
|
|
4682
2676
|
function thisModuleFile() {
|
|
4683
2677
|
const cjsFile = typeof __filename !== "undefined" ? __filename : "";
|
|
4684
|
-
return cjsFile || process.argv[1] ||
|
|
2678
|
+
return cjsFile || process.argv[1] || join7(process.cwd(), "package.json");
|
|
4685
2679
|
}
|
|
4686
2680
|
function openReadonlySqlite(file) {
|
|
4687
2681
|
const req = createRequire(thisModuleFile());
|
|
@@ -4699,21 +2693,21 @@ function mapAgentType(raw) {
|
|
|
4699
2693
|
return null;
|
|
4700
2694
|
}
|
|
4701
2695
|
function resolveConductorCursorAgentId(workspacePath) {
|
|
4702
|
-
if (!workspacePath || !
|
|
2696
|
+
if (!workspacePath || !existsSync7(CURSOR_SDK_STORE)) return null;
|
|
4703
2697
|
const normalized = workspacePath.replace(/\/$/, "");
|
|
4704
2698
|
let best = null;
|
|
4705
2699
|
let hashes;
|
|
4706
2700
|
try {
|
|
4707
|
-
hashes =
|
|
2701
|
+
hashes = readdirSync2(CURSOR_SDK_STORE);
|
|
4708
2702
|
} catch {
|
|
4709
2703
|
return null;
|
|
4710
2704
|
}
|
|
4711
2705
|
for (const hash of hashes) {
|
|
4712
|
-
const agentsFile =
|
|
4713
|
-
if (!
|
|
2706
|
+
const agentsFile = join7(CURSOR_SDK_STORE, hash, "agents.ndjson");
|
|
2707
|
+
if (!existsSync7(agentsFile)) continue;
|
|
4714
2708
|
let text;
|
|
4715
2709
|
try {
|
|
4716
|
-
text =
|
|
2710
|
+
text = readFileSync5(agentsFile, "utf8");
|
|
4717
2711
|
} catch {
|
|
4718
2712
|
continue;
|
|
4719
2713
|
}
|
|
@@ -4737,7 +2731,7 @@ function resolveConductorCursorAgentId(workspacePath) {
|
|
|
4737
2731
|
return best?.agentId ?? null;
|
|
4738
2732
|
}
|
|
4739
2733
|
async function adoptThread(input) {
|
|
4740
|
-
if (!
|
|
2734
|
+
if (!existsSync7(input.worktreePath)) {
|
|
4741
2735
|
throw new Error(`Worktree not found: ${input.worktreePath}`);
|
|
4742
2736
|
}
|
|
4743
2737
|
const repoPath = await resolveRepoRoot(input.worktreePath);
|
|
@@ -4756,25 +2750,25 @@ async function adoptThread(input) {
|
|
|
4756
2750
|
messages: input.messages ?? []
|
|
4757
2751
|
});
|
|
4758
2752
|
writeThread(thread);
|
|
4759
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
2753
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-SLP24Z3S.js");
|
|
4760
2754
|
await ensureWorkspace2(repoPath);
|
|
4761
|
-
const { ensureWorktreeSideboardIgnored
|
|
4762
|
-
await
|
|
2755
|
+
const { ensureWorktreeSideboardIgnored } = await import("./worktree-exclude-5N2KTYIT.js");
|
|
2756
|
+
await ensureWorktreeSideboardIgnored(input.worktreePath);
|
|
4763
2757
|
return thread;
|
|
4764
2758
|
}
|
|
4765
2759
|
function listConductorWorkspaces() {
|
|
4766
|
-
if (!
|
|
2760
|
+
if (!existsSync7(CONDUCTOR_DB)) {
|
|
4767
2761
|
throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
|
|
4768
2762
|
}
|
|
4769
|
-
const tmp = mkdtempSync(
|
|
4770
|
-
const snapshot =
|
|
2763
|
+
const tmp = mkdtempSync(join7(tmpdir(), "sideboard-conductor-"));
|
|
2764
|
+
const snapshot = join7(tmp, "conductor.db");
|
|
4771
2765
|
try {
|
|
4772
|
-
|
|
2766
|
+
copyFileSync(CONDUCTOR_DB, snapshot);
|
|
4773
2767
|
for (const suffix of ["-wal", "-shm"]) {
|
|
4774
2768
|
const src = `${CONDUCTOR_DB}${suffix}`;
|
|
4775
|
-
if (
|
|
2769
|
+
if (existsSync7(src)) {
|
|
4776
2770
|
try {
|
|
4777
|
-
|
|
2771
|
+
copyFileSync(src, `${snapshot}${suffix}`);
|
|
4778
2772
|
} catch {
|
|
4779
2773
|
}
|
|
4780
2774
|
}
|
|
@@ -4854,18 +2848,18 @@ function listConductorWorkspaces() {
|
|
|
4854
2848
|
}
|
|
4855
2849
|
}
|
|
4856
2850
|
function importConductorWorkspace(workspaceId) {
|
|
4857
|
-
if (!
|
|
2851
|
+
if (!existsSync7(CONDUCTOR_DB)) {
|
|
4858
2852
|
throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
|
|
4859
2853
|
}
|
|
4860
|
-
const tmp = mkdtempSync(
|
|
4861
|
-
const snapshot =
|
|
2854
|
+
const tmp = mkdtempSync(join7(tmpdir(), "sideboard-conductor-"));
|
|
2855
|
+
const snapshot = join7(tmp, "conductor.db");
|
|
4862
2856
|
try {
|
|
4863
|
-
|
|
2857
|
+
copyFileSync(CONDUCTOR_DB, snapshot);
|
|
4864
2858
|
for (const suffix of ["-wal", "-shm"]) {
|
|
4865
2859
|
const src = `${CONDUCTOR_DB}${suffix}`;
|
|
4866
|
-
if (
|
|
2860
|
+
if (existsSync7(src)) {
|
|
4867
2861
|
try {
|
|
4868
|
-
|
|
2862
|
+
copyFileSync(src, `${snapshot}${suffix}`);
|
|
4869
2863
|
} catch {
|
|
4870
2864
|
}
|
|
4871
2865
|
}
|
|
@@ -4883,7 +2877,7 @@ function importConductorWorkspace(workspaceId) {
|
|
|
4883
2877
|
).get(workspaceId);
|
|
4884
2878
|
if (!row) throw new Error(`Conductor workspace not found: ${workspaceId}`);
|
|
4885
2879
|
const worktreePath = String(row.workspacePath);
|
|
4886
|
-
if (!
|
|
2880
|
+
if (!existsSync7(worktreePath)) {
|
|
4887
2881
|
throw new Error(`Conductor worktree missing on disk: ${worktreePath}`);
|
|
4888
2882
|
}
|
|
4889
2883
|
let sessionId = null;
|
|
@@ -4923,7 +2917,7 @@ function importConductorWorkspace(workspaceId) {
|
|
|
4923
2917
|
}
|
|
4924
2918
|
let repoPath = worktreePath;
|
|
4925
2919
|
try {
|
|
4926
|
-
repoPath =
|
|
2920
|
+
repoPath = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
4927
2921
|
cwd: worktreePath,
|
|
4928
2922
|
encoding: "utf8"
|
|
4929
2923
|
}).trim();
|
|
@@ -4954,7 +2948,7 @@ async function importConductorWorkspaceAsync(workspaceId) {
|
|
|
4954
2948
|
}
|
|
4955
2949
|
|
|
4956
2950
|
// src/threads/stack-layers.ts
|
|
4957
|
-
import { existsSync as
|
|
2951
|
+
import { existsSync as existsSync8 } from "fs";
|
|
4958
2952
|
function stackIdFrom(stack) {
|
|
4959
2953
|
if (stack.stackNumber != null) return `gh-stack-${stack.stackNumber}`;
|
|
4960
2954
|
const key = stack.layers.map((l) => l.branchName).join("|");
|
|
@@ -4966,7 +2960,7 @@ function hashShort(s) {
|
|
|
4966
2960
|
for (let i = 0; i < s.length; i++) h = h * 31 + s.charCodeAt(i) | 0;
|
|
4967
2961
|
return Math.abs(h).toString(36);
|
|
4968
2962
|
}
|
|
4969
|
-
function
|
|
2963
|
+
function requireThread2(idOrRef) {
|
|
4970
2964
|
const thread = findThreadByRef(idOrRef) ?? readThread(idOrRef);
|
|
4971
2965
|
if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
|
|
4972
2966
|
return thread;
|
|
@@ -5017,7 +3011,7 @@ async function openStackLayer(input, _onSetupLine) {
|
|
|
5017
3011
|
let createdWorktree = false;
|
|
5018
3012
|
const trees = await listWorktrees(repoPath);
|
|
5019
3013
|
const checkedOut = trees.find((w) => w.branch === branchName);
|
|
5020
|
-
if (checkedOut?.path &&
|
|
3014
|
+
if (checkedOut?.path && existsSync8(checkedOut.path)) {
|
|
5021
3015
|
if (input.reuseExistingWorktree !== false) {
|
|
5022
3016
|
worktreePath = checkedOut.path;
|
|
5023
3017
|
} else {
|
|
@@ -5065,7 +3059,7 @@ async function openStackLayer(input, _onSetupLine) {
|
|
|
5065
3059
|
return { thread: readThread(thread.id) ?? thread, createdWorktree };
|
|
5066
3060
|
}
|
|
5067
3061
|
async function openPrStackLayers(input, onSetupLine) {
|
|
5068
|
-
const from =
|
|
3062
|
+
const from = requireThread2(input.threadRef);
|
|
5069
3063
|
if (!from.worktreePath?.trim() || !from.repoPath?.trim()) {
|
|
5070
3064
|
throw new Error("Thread has no worktree");
|
|
5071
3065
|
}
|
|
@@ -5109,7 +3103,7 @@ async function openPrStackLayers(input, onSetupLine) {
|
|
|
5109
3103
|
return { stack, threads, createdThreadIds };
|
|
5110
3104
|
}
|
|
5111
3105
|
async function addStackLayerFromThread(input, onSetupLine) {
|
|
5112
|
-
const from =
|
|
3106
|
+
const from = requireThread2(input.threadRef);
|
|
5113
3107
|
if (!from.worktreePath?.trim() || !from.repoPath?.trim()) {
|
|
5114
3108
|
throw new Error("Thread has no worktree");
|
|
5115
3109
|
}
|
|
@@ -5141,7 +3135,7 @@ async function addStackLayerFromThread(input, onSetupLine) {
|
|
|
5141
3135
|
return { stack, thread: thread.thread, createdWorktree: thread.createdWorktree };
|
|
5142
3136
|
}
|
|
5143
3137
|
async function initStackFromThread(input, onSetupLine) {
|
|
5144
|
-
const from =
|
|
3138
|
+
const from = requireThread2(input.threadRef);
|
|
5145
3139
|
if (!from.worktreePath?.trim() || !from.branchName?.trim() || !from.repoPath?.trim()) {
|
|
5146
3140
|
throw new Error("Thread has no worktree/branch");
|
|
5147
3141
|
}
|
|
@@ -5159,7 +3153,7 @@ async function initStackFromThread(input, onSetupLine) {
|
|
|
5159
3153
|
async function createPrStack(input, onSetupLine) {
|
|
5160
3154
|
await requireAgent(input.agent);
|
|
5161
3155
|
const repoPath = await resolveRepoRoot(input.repoPath);
|
|
5162
|
-
if (!
|
|
3156
|
+
if (!existsSync8(repoPath)) throw new Error(`Repo not found: ${repoPath}`);
|
|
5163
3157
|
if (!input.branches.length) throw new Error("At least one branch name required");
|
|
5164
3158
|
const status = await detectGhStack(repoPath);
|
|
5165
3159
|
if (!status.available) throw new Error(status.reason);
|
|
@@ -5226,7 +3220,7 @@ async function createPrStack(input, onSetupLine) {
|
|
|
5226
3220
|
}
|
|
5227
3221
|
}
|
|
5228
3222
|
const claimed = new Set(threads.map((t) => t.worktreePath));
|
|
5229
|
-
if (!claimed.has(bootstrap.worktreePath) &&
|
|
3223
|
+
if (!claimed.has(bootstrap.worktreePath) && existsSync8(bootstrap.worktreePath)) {
|
|
5230
3224
|
try {
|
|
5231
3225
|
await removeWorktree(repoPath, bootstrap.worktreePath, {
|
|
5232
3226
|
deleteBranch: bootstrap.branchName
|
|
@@ -5238,10 +3232,10 @@ async function createPrStack(input, onSetupLine) {
|
|
|
5238
3232
|
}
|
|
5239
3233
|
|
|
5240
3234
|
// src/diff/diff.ts
|
|
5241
|
-
import { existsSync as
|
|
5242
|
-
import { dirname
|
|
3235
|
+
import { existsSync as existsSync9, mkdirSync, readFileSync as readFileSync6, statSync as statSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
3236
|
+
import { dirname, join as join8 } from "path";
|
|
5243
3237
|
async function inspectGitWorktree(worktreePath) {
|
|
5244
|
-
if (!worktreePath || !
|
|
3238
|
+
if (!worktreePath || !existsSync9(worktreePath)) return "missing_worktree";
|
|
5245
3239
|
const check = await git(["rev-parse", "--is-inside-work-tree"], worktreePath, {
|
|
5246
3240
|
reject: false
|
|
5247
3241
|
});
|
|
@@ -5249,7 +3243,7 @@ async function inspectGitWorktree(worktreePath) {
|
|
|
5249
3243
|
return "ok";
|
|
5250
3244
|
}
|
|
5251
3245
|
async function initializeGitRepository(worktreePath) {
|
|
5252
|
-
if (!worktreePath || !
|
|
3246
|
+
if (!worktreePath || !existsSync9(worktreePath)) {
|
|
5253
3247
|
throw new Error("Worktree not found");
|
|
5254
3248
|
}
|
|
5255
3249
|
const status = await inspectGitWorktree(worktreePath);
|
|
@@ -5385,11 +3379,11 @@ new file mode 100644
|
|
|
5385
3379
|
};
|
|
5386
3380
|
}
|
|
5387
3381
|
async function untrackedPatch(worktreePath, path, maxHunk) {
|
|
5388
|
-
const abs =
|
|
3382
|
+
const abs = join8(worktreePath, path);
|
|
5389
3383
|
try {
|
|
5390
3384
|
const st = statSync2(abs);
|
|
5391
3385
|
if (st.isFile() && st.size > maxHunk) {
|
|
5392
|
-
const buf =
|
|
3386
|
+
const buf = readFileSync6(abs).subarray(0, maxHunk);
|
|
5393
3387
|
return syntheticAddPatch(path, buf.toString("utf8"), maxHunk);
|
|
5394
3388
|
}
|
|
5395
3389
|
} catch {
|
|
@@ -5857,8 +3851,8 @@ async function getDiff(worktreePath, repoPath, opts) {
|
|
|
5857
3851
|
}
|
|
5858
3852
|
function statWorktreePath(worktreePath, relativePath) {
|
|
5859
3853
|
assertSafeRelativePath(relativePath);
|
|
5860
|
-
const abs =
|
|
5861
|
-
if (!
|
|
3854
|
+
const abs = join8(worktreePath, relativePath);
|
|
3855
|
+
if (!existsSync9(abs)) return "missing";
|
|
5862
3856
|
const st = statSync2(abs);
|
|
5863
3857
|
if (st.isDirectory()) return "dir";
|
|
5864
3858
|
if (st.isFile()) return "file";
|
|
@@ -5899,7 +3893,7 @@ var DEFAULT_UPLOAD_MAX_BYTES = 5e7;
|
|
|
5899
3893
|
function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
|
|
5900
3894
|
assertSafeRelativePath(relativePath);
|
|
5901
3895
|
const maxBytes = opts?.maxBytes ?? DEFAULT_UPLOAD_MAX_BYTES;
|
|
5902
|
-
const abs =
|
|
3896
|
+
const abs = join8(worktreePath, relativePath);
|
|
5903
3897
|
const st = statSync2(abs);
|
|
5904
3898
|
if (!st.isFile()) {
|
|
5905
3899
|
throw new Error(`Not a file: ${relativePath}`);
|
|
@@ -5909,7 +3903,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
|
|
|
5909
3903
|
`File too large to upload (${st.size} bytes; max ${maxBytes})`
|
|
5910
3904
|
);
|
|
5911
3905
|
}
|
|
5912
|
-
const buf =
|
|
3906
|
+
const buf = readFileSync6(abs);
|
|
5913
3907
|
return {
|
|
5914
3908
|
path: relativePath,
|
|
5915
3909
|
contentBase64: buf.toString("base64"),
|
|
@@ -5919,12 +3913,12 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
|
|
|
5919
3913
|
function readWorktreeFile(worktreePath, relativePath, opts) {
|
|
5920
3914
|
assertSafeRelativePath(relativePath);
|
|
5921
3915
|
const maxBytes = opts?.maxBytes ?? 2e5;
|
|
5922
|
-
const abs =
|
|
3916
|
+
const abs = join8(worktreePath, relativePath);
|
|
5923
3917
|
const st = statSync2(abs);
|
|
5924
3918
|
if (!st.isFile()) {
|
|
5925
3919
|
throw new Error(`Not a file: ${relativePath}`);
|
|
5926
3920
|
}
|
|
5927
|
-
const buf =
|
|
3921
|
+
const buf = readFileSync6(abs);
|
|
5928
3922
|
if (isImageRelativePath(relativePath)) {
|
|
5929
3923
|
const maxImageBytes = Math.max(maxBytes, 15e6);
|
|
5930
3924
|
const truncated2 = buf.length > maxImageBytes;
|
|
@@ -5967,9 +3961,9 @@ function assertSafeRelativePath(relativePath) {
|
|
|
5967
3961
|
}
|
|
5968
3962
|
function writeWorktreeFile(worktreePath, relativePath, content) {
|
|
5969
3963
|
assertSafeRelativePath(relativePath);
|
|
5970
|
-
const abs =
|
|
5971
|
-
|
|
5972
|
-
|
|
3964
|
+
const abs = join8(worktreePath, relativePath);
|
|
3965
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
3966
|
+
writeFileSync5(abs, content, "utf8");
|
|
5973
3967
|
return { path: relativePath };
|
|
5974
3968
|
}
|
|
5975
3969
|
async function getWorktreeDirtyStat(worktreePath) {
|
|
@@ -6191,9 +4185,9 @@ async function confirmLand(thread, opts) {
|
|
|
6191
4185
|
}
|
|
6192
4186
|
|
|
6193
4187
|
// src/skills/discover.ts
|
|
6194
|
-
import { existsSync as
|
|
4188
|
+
import { existsSync as existsSync10, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
|
|
6195
4189
|
import { homedir } from "os";
|
|
6196
|
-
import { join as
|
|
4190
|
+
import { join as join9 } from "path";
|
|
6197
4191
|
|
|
6198
4192
|
// src/skills/bundled/long-running.ts
|
|
6199
4193
|
var LONG_RUNNING_SKILL_COMMAND = "long-running";
|
|
@@ -6330,7 +4324,7 @@ function parseFrontmatter(content) {
|
|
|
6330
4324
|
}
|
|
6331
4325
|
function readSkill(skillMd, source) {
|
|
6332
4326
|
try {
|
|
6333
|
-
const content =
|
|
4327
|
+
const content = readFileSync7(skillMd, "utf8");
|
|
6334
4328
|
const { name: fmName, description } = parseFrontmatter(content);
|
|
6335
4329
|
const dirName = skillMd.split("/").slice(-2, -1)[0] || "skill";
|
|
6336
4330
|
const name = fmName || dirName;
|
|
@@ -6349,17 +4343,17 @@ function readSkill(skillMd, source) {
|
|
|
6349
4343
|
}
|
|
6350
4344
|
}
|
|
6351
4345
|
function scanSkillsDir(dir, source, out) {
|
|
6352
|
-
if (!
|
|
4346
|
+
if (!existsSync10(dir)) return;
|
|
6353
4347
|
let entries;
|
|
6354
4348
|
try {
|
|
6355
|
-
entries =
|
|
4349
|
+
entries = readdirSync3(dir);
|
|
6356
4350
|
} catch {
|
|
6357
4351
|
return;
|
|
6358
4352
|
}
|
|
6359
4353
|
for (const entry of entries) {
|
|
6360
4354
|
if (entry.startsWith(".")) continue;
|
|
6361
|
-
const skillMd =
|
|
6362
|
-
if (!
|
|
4355
|
+
const skillMd = join9(dir, entry, "SKILL.md");
|
|
4356
|
+
if (!existsSync10(skillMd)) continue;
|
|
6363
4357
|
try {
|
|
6364
4358
|
if (!statSync3(skillMd).isFile()) continue;
|
|
6365
4359
|
} catch {
|
|
@@ -6370,22 +4364,22 @@ function scanSkillsDir(dir, source, out) {
|
|
|
6370
4364
|
}
|
|
6371
4365
|
}
|
|
6372
4366
|
function scanClaudePluginSkills(pluginsRoot, out) {
|
|
6373
|
-
if (!
|
|
4367
|
+
if (!existsSync10(pluginsRoot)) return;
|
|
6374
4368
|
const walk = (dir, depth, lookingForSkillsDir) => {
|
|
6375
4369
|
if (depth > 7) return;
|
|
6376
4370
|
let entries;
|
|
6377
4371
|
try {
|
|
6378
|
-
entries =
|
|
4372
|
+
entries = readdirSync3(dir);
|
|
6379
4373
|
} catch {
|
|
6380
4374
|
return;
|
|
6381
4375
|
}
|
|
6382
4376
|
if (lookingForSkillsDir && entries.includes("SKILL.md")) {
|
|
6383
|
-
const skill = readSkill(
|
|
4377
|
+
const skill = readSkill(join9(dir, "SKILL.md"), "cli");
|
|
6384
4378
|
if (skill) out.push(skill);
|
|
6385
4379
|
}
|
|
6386
4380
|
for (const entry of entries) {
|
|
6387
4381
|
if (entry === "node_modules" || entry === ".git") continue;
|
|
6388
|
-
const full =
|
|
4382
|
+
const full = join9(dir, entry);
|
|
6389
4383
|
try {
|
|
6390
4384
|
if (!statSync3(full).isDirectory()) continue;
|
|
6391
4385
|
} catch {
|
|
@@ -6405,17 +4399,17 @@ function discoverSkills(worktreePath) {
|
|
|
6405
4399
|
const home = homedir();
|
|
6406
4400
|
const collected = [];
|
|
6407
4401
|
for (const rel of [".claude/skills", ".cursor/skills", ".sideboard/skills", ".brightsy/skills", "skills"]) {
|
|
6408
|
-
scanSkillsDir(
|
|
4402
|
+
scanSkillsDir(join9(worktreePath, rel), "workspace", collected);
|
|
6409
4403
|
}
|
|
6410
4404
|
for (const abs of [
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
4405
|
+
join9(home, ".claude/skills"),
|
|
4406
|
+
join9(home, ".cursor/skills"),
|
|
4407
|
+
join9(home, ".sideboard/skills"),
|
|
4408
|
+
join9(home, ".brightsy/skills")
|
|
6415
4409
|
]) {
|
|
6416
4410
|
scanSkillsDir(abs, "user", collected);
|
|
6417
4411
|
}
|
|
6418
|
-
scanClaudePluginSkills(
|
|
4412
|
+
scanClaudePluginSkills(join9(home, ".claude/plugins"), collected);
|
|
6419
4413
|
collected.push(...bundledSkills());
|
|
6420
4414
|
const rank = {
|
|
6421
4415
|
workspace: 0,
|
|
@@ -6441,7 +4435,7 @@ function readSkillBody(skillPath, maxChars = 12e3) {
|
|
|
6441
4435
|
\u2026(truncated)` : body;
|
|
6442
4436
|
}
|
|
6443
4437
|
}
|
|
6444
|
-
const raw =
|
|
4438
|
+
const raw = readFileSync7(skillPath, "utf8");
|
|
6445
4439
|
if (raw.startsWith("---")) {
|
|
6446
4440
|
const end = raw.indexOf("\n---", 3);
|
|
6447
4441
|
if (end >= 0) {
|
|
@@ -6586,12 +4580,12 @@ function shiftQueuedItem(queue, queueAttachments) {
|
|
|
6586
4580
|
}
|
|
6587
4581
|
|
|
6588
4582
|
// src/agents/instructions.ts
|
|
6589
|
-
import { existsSync as
|
|
6590
|
-
import { join as
|
|
4583
|
+
import { existsSync as existsSync12 } from "fs";
|
|
4584
|
+
import { join as join11 } from "path";
|
|
6591
4585
|
|
|
6592
4586
|
// src/skills/detached-job-path.ts
|
|
6593
|
-
import { existsSync as
|
|
6594
|
-
import { dirname as
|
|
4587
|
+
import { existsSync as existsSync11 } from "fs";
|
|
4588
|
+
import { dirname as dirname2, join as join10 } from "path";
|
|
6595
4589
|
import { fileURLToPath } from "url";
|
|
6596
4590
|
var DETACHED_JOB_FILENAMES = ["detached-job.cjs", "detached-job.js"];
|
|
6597
4591
|
function compiledModuleDir() {
|
|
@@ -6600,7 +4594,7 @@ function compiledModuleDir() {
|
|
|
6600
4594
|
try {
|
|
6601
4595
|
const url = import.meta.url;
|
|
6602
4596
|
if (typeof url === "string" && url.length > 0) {
|
|
6603
|
-
return
|
|
4597
|
+
return dirname2(fileURLToPath(url));
|
|
6604
4598
|
}
|
|
6605
4599
|
} catch {
|
|
6606
4600
|
}
|
|
@@ -6608,8 +4602,8 @@ function compiledModuleDir() {
|
|
|
6608
4602
|
}
|
|
6609
4603
|
function detachedJobScriptIn(dir) {
|
|
6610
4604
|
for (const name of DETACHED_JOB_FILENAMES) {
|
|
6611
|
-
const script =
|
|
6612
|
-
if (
|
|
4605
|
+
const script = join10(dir, "scripts", name);
|
|
4606
|
+
if (existsSync11(script)) return script;
|
|
6613
4607
|
}
|
|
6614
4608
|
return null;
|
|
6615
4609
|
}
|
|
@@ -6626,7 +4620,7 @@ function resolveDetachedJobScript() {
|
|
|
6626
4620
|
for (let i = 0; i < 8; i++) {
|
|
6627
4621
|
const candidate = detachedJobScriptIn(dir);
|
|
6628
4622
|
if (candidate) return candidate;
|
|
6629
|
-
const parent =
|
|
4623
|
+
const parent = dirname2(dir);
|
|
6630
4624
|
if (parent === dir) break;
|
|
6631
4625
|
dir = parent;
|
|
6632
4626
|
}
|
|
@@ -6668,12 +4662,12 @@ function formatRenameBranchDirective(thread, opts) {
|
|
|
6668
4662
|
function localhostPreviewUrl(port) {
|
|
6669
4663
|
return `http://localhost:${port}`;
|
|
6670
4664
|
}
|
|
6671
|
-
function describeLiveDevRun(
|
|
6672
|
-
const url = localhostPreviewUrl(
|
|
6673
|
-
if (
|
|
6674
|
-
return `${
|
|
4665
|
+
function describeLiveDevRun(run) {
|
|
4666
|
+
const url = localhostPreviewUrl(run.port);
|
|
4667
|
+
if (run.preview === "window") {
|
|
4668
|
+
return `${run.scriptName} Electron window (renderer ${url})`;
|
|
6675
4669
|
}
|
|
6676
|
-
return `${
|
|
4670
|
+
return `${run.scriptName} ${url}`;
|
|
6677
4671
|
}
|
|
6678
4672
|
function formatDevAccessHint(runs) {
|
|
6679
4673
|
const live = (runs ?? []).filter(
|
|
@@ -6691,7 +4685,7 @@ function formatDevAccessHint(runs) {
|
|
|
6691
4685
|
const bits = live.map(describeLiveDevRun).join("; ");
|
|
6692
4686
|
return `This worktree's Dev apps: ${bits}. preview=url \u2192 open the URL; preview=window \u2192 Electron window is the app. MCP \`get_run_log\` reads the Run-tab terminal. Do not guess :3000.`;
|
|
6693
4687
|
}
|
|
6694
|
-
return "To open
|
|
4688
|
+
return "To open a project Dev app: MCP `run_dev_script` (same as the desktop Dev button; pass `name` for one script \u2014 several can run at once, each with its own port) returns `{url, port, preview}` (`SIDEBOARD_PORT`, not 3000). preview=url \u2192 open that URL (browser, curl, Playwright). preview=window \u2192 Electron: the native window is the app; url is renderer/HMR only (no preload/IPC in a browser). If already running, `list_run_scripts` `active[].url`. `get_run_log` reads that script\u2019s Run-tab terminal (tail). `stop_dev_script` stops one script; omit name to stop all.";
|
|
6695
4689
|
}
|
|
6696
4690
|
function formatWorktreeDirective(thread, opts) {
|
|
6697
4691
|
const worktree = normPath(thread.worktreePath);
|
|
@@ -6731,9 +4725,9 @@ function formatWorktreeDirective(thread, opts) {
|
|
|
6731
4725
|
);
|
|
6732
4726
|
} else {
|
|
6733
4727
|
const slug = opts?.githubSlug ?? "<origin-owner/name>";
|
|
6734
|
-
const
|
|
4728
|
+
const resolve = opts?.githubSlug ? "" : " Resolve `<origin-owner/name>` with `git remote get-url origin` in this worktree.";
|
|
6735
4729
|
lines.push(
|
|
6736
|
-
`- 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.${
|
|
4730
|
+
`- 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.${resolve} 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\`.`
|
|
6737
4731
|
);
|
|
6738
4732
|
}
|
|
6739
4733
|
lines.push(
|
|
@@ -6779,17 +4773,17 @@ function formatIssueToolsDirective(opts) {
|
|
|
6779
4773
|
];
|
|
6780
4774
|
if (opts.linear) {
|
|
6781
4775
|
lines.push(
|
|
6782
|
-
"- Linear: `linear_search_issues` (assignee=me; pass `updatedSince` for \u201Cany updates since yesterday?\u201D \u2014 new tickets, edits, and comments), `linear_get_issue` (comments; pass `include=full` if crush.truncated), `linear_comment`, `linear_update_issue` (state, cycle, labels, project, parent, relations), `linear_create_issue` (pass `parent` for spin-offs; call `linear_list_teams` first). Scope errors: reconnect Linear in Settings \u2192 Issues."
|
|
4776
|
+
"- Linear: `linear_search_issues` (assignee=me; pass `updatedSince` for \u201Cany updates since yesterday?\u201D \u2014 new tickets, edits, and comments), `linear_get_issue` (comments, attachments; pass `include=full` if crush.truncated), `linear_download_attachment` (saves to `.context/attachments/`), `linear_comment`, `linear_update_issue` (state, cycle, labels, project, parent, relations), `linear_create_issue` (pass `parent` for spin-offs; call `linear_list_teams` first). Scope errors: reconnect Linear in Settings \u2192 Issues."
|
|
6783
4777
|
);
|
|
6784
4778
|
}
|
|
6785
4779
|
if (github) {
|
|
6786
4780
|
lines.push(
|
|
6787
|
-
"- GitHub: `github_search_issues` (assignee=me; `updatedSince` for inbox updates + comments), `github_get_issue` (comments; pass `include=full` if crush.truncated), `github_comment`, `github_update_issue` (state open|closed, labels, project, parent, relations), `github_create_issue` (pass `parent` for spin-offs). Uses Account `gh`."
|
|
4781
|
+
"- GitHub: `github_search_issues` (assignee=me; `updatedSince` for inbox updates + comments), `github_get_issue` (comments, attachments; pass `include=full` if crush.truncated), `github_download_attachment` (saves to `.context/attachments/`), `github_comment`, `github_update_issue` (state open|closed, labels, project, parent, relations), `github_create_issue` (pass `parent` for spin-offs). Uses Account `gh`."
|
|
6788
4782
|
);
|
|
6789
4783
|
}
|
|
6790
4784
|
if (opts.abletime) {
|
|
6791
4785
|
lines.push(
|
|
6792
|
-
"- AbleTime: `abletime_list_tasks` / `abletime_search_tasks` (`updatedSince` for inbox updates + comments), `abletime_get_task` (comments; pass `include=full` if crush.truncated), `abletime_comment`, `abletime_update_task` (state, labels, project, parent, relations), `abletime_create_task` (pass `parent` for spin-offs; `abletime_list_projects` if needed)."
|
|
4786
|
+
"- AbleTime: `abletime_list_tasks` / `abletime_search_tasks` (`updatedSince` for inbox updates + comments), `abletime_get_task` (comments, attachments; pass `include=full` if crush.truncated), `abletime_download_attachment` (saves to `.context/attachments/`), `abletime_comment`, `abletime_update_task` (state, labels, project, parent, relations), `abletime_create_task` (pass `parent` for spin-offs; `abletime_list_projects` if needed)."
|
|
6793
4787
|
);
|
|
6794
4788
|
}
|
|
6795
4789
|
lines.push(
|
|
@@ -6827,9 +4821,9 @@ function formatPrGateDirective() {
|
|
|
6827
4821
|
}
|
|
6828
4822
|
function formatProcessGuideDirective(opts) {
|
|
6829
4823
|
const worktree = opts?.worktreePath?.trim();
|
|
6830
|
-
const hasGraphSkill = !!worktree &&
|
|
6831
|
-
const hasReviewSkill = !!worktree &&
|
|
6832
|
-
const hasInstallSkill = !!worktree &&
|
|
4824
|
+
const hasGraphSkill = !!worktree && existsSync12(join11(worktree, ".claude/skills/graph-engineering/SKILL.md"));
|
|
4825
|
+
const hasReviewSkill = !!worktree && existsSync12(join11(worktree, ".claude/skills/review/SKILL.md"));
|
|
4826
|
+
const hasInstallSkill = !!worktree && existsSync12(join11(worktree, ".claude/skills/worktree-install/SKILL.md"));
|
|
6833
4827
|
return [
|
|
6834
4828
|
"Process guides (recurring work only):",
|
|
6835
4829
|
hasGraphSkill ? "- Migrations, ports, batch fixes, and other fan-out: follow `.claude/skills/graph-engineering/SKILL.md` (`/graph-engineering`). Judge first; state on disk; grow the rulebook; do not patch around it." : null,
|
|
@@ -6849,7 +4843,7 @@ function formatLongRunningDirective(opts) {
|
|
|
6849
4843
|
"- Hanging, no useful output, or the wrong thing \u2192 `stop_job` (or the helper with `stop <id>`). Do not stop a pack/test/deploy that is clearly making progress.",
|
|
6850
4844
|
"- Stay in the loop until stillRunning is false (or you stopped it). ok \u2192 finish the task. failed / stopped \u2192 read the log, fix or narrow the command, start once.",
|
|
6851
4845
|
"State: `.context/.sideboard/detached-jobs/<id>/` (local scratch). Full guide: `/long-running` (always available).",
|
|
6852
|
-
"Project Dev / run scripts (`.sideboard` `[scripts.run.*]`, same as the desktop Dev button): use MCP `list_run_scripts` / `run_dev_script` / `stop_dev_script` / `get_run_log`. `get_run_log` is
|
|
4846
|
+
"Project Dev / run scripts (`.sideboard` `[scripts.run.*]`, same as the desktop Dev button): use MCP `list_run_scripts` / `run_dev_script` / `stop_dev_script` / `get_run_log`. Pass `name` to start one script; several can run at once, each with its own port. `get_run_log` is that script\u2019s Run-tab terminal (tail; raise `tail` only if truncated). Do not detached-job or shell-spawn that same command. `run_dev_script` returns `{url, port, preview}` (`SIDEBOARD_PORT`). preview=url \u2192 open that URL; preview=window \u2192 Electron window is the app (url is renderer/HMR only). Never guess :3000."
|
|
6853
4847
|
].join("\n");
|
|
6854
4848
|
}
|
|
6855
4849
|
function formatLongRunningReminder(opts) {
|
|
@@ -6866,7 +4860,7 @@ function formatArtifactDirective() {
|
|
|
6866
4860
|
].join("\n");
|
|
6867
4861
|
}
|
|
6868
4862
|
function formatUiReminder() {
|
|
6869
|
-
return "Sideboard UI: markdown table is enough to read data; present_schema if they ask to edit/filter (even after markdown); present_files for the file manager. html fence or present_artifact, not both for the same document. type=log appends (same artifact_id, new lines only). ask_user only for a real multiple-choice (not hellos, \u201Cwhat next?\u201D, or after a review) \u2014 reply in chat. ask_user wakes the parent orchestrator; notify_orchestrator if blocked without a picker. Do not say artifacts/CMS UI are unavailable. Dev / preview: run_dev_script (same as the Dev button) returns {url, port, preview} (SIDEBOARD_PORT \u2014 never guess :3000). preview=url \u2192 open url; preview=window \u2192 Electron window is the app, url is renderer/HMR only. list_run_scripts active[].url if already running; get_run_log for
|
|
4863
|
+
return "Sideboard UI: markdown table is enough to read data; present_schema if they ask to edit/filter (even after markdown); present_files for the file manager. html fence or present_artifact, not both for the same document. type=log appends (same artifact_id, new lines only). ask_user only for a real multiple-choice (not hellos, \u201Cwhat next?\u201D, or after a review) \u2014 reply in chat. ask_user wakes the parent orchestrator; notify_orchestrator if blocked without a picker. Do not say artifacts/CMS UI are unavailable. Dev / preview: run_dev_script (same as the Dev button; pass name \u2014 several scripts can run at once) returns {url, port, preview} (SIDEBOARD_PORT \u2014 never guess :3000). preview=url \u2192 open url; preview=window \u2192 Electron window is the app, url is renderer/HMR only. list_run_scripts active[].url if already running; get_run_log for that script\u2019s Run-tab terminal; stop_dev_script to stop one.";
|
|
6870
4864
|
}
|
|
6871
4865
|
|
|
6872
4866
|
// src/integrations/optional-services.ts
|
|
@@ -6993,9 +4987,9 @@ async function syncThreadBranchFromGit(threadId) {
|
|
|
6993
4987
|
}
|
|
6994
4988
|
|
|
6995
4989
|
// src/store/schedules.ts
|
|
6996
|
-
import { randomUUID as
|
|
6997
|
-
import { existsSync as
|
|
6998
|
-
import { join as
|
|
4990
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
4991
|
+
import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
|
|
4992
|
+
import { join as join12 } from "path";
|
|
6999
4993
|
import { Cron } from "croner";
|
|
7000
4994
|
var DURATION_RE = /^(\d+)(s|m|h|d)$/i;
|
|
7001
4995
|
var UNIT_MS = {
|
|
@@ -7005,7 +4999,7 @@ var UNIT_MS = {
|
|
|
7005
4999
|
d: 864e5
|
|
7006
5000
|
};
|
|
7007
5001
|
function schedulesPath() {
|
|
7008
|
-
return
|
|
5002
|
+
return join12(appDataDir(), "schedules.json");
|
|
7009
5003
|
}
|
|
7010
5004
|
function parseDurationMs(every) {
|
|
7011
5005
|
const m = every.trim().match(DURATION_RE);
|
|
@@ -7105,9 +5099,9 @@ function normalizeTask(raw) {
|
|
|
7105
5099
|
}
|
|
7106
5100
|
function readAll() {
|
|
7107
5101
|
const path = schedulesPath();
|
|
7108
|
-
if (!
|
|
5102
|
+
if (!existsSync13(path)) return [];
|
|
7109
5103
|
try {
|
|
7110
|
-
const parsed = JSON.parse(
|
|
5104
|
+
const parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
7111
5105
|
if (!Array.isArray(parsed)) return [];
|
|
7112
5106
|
return parsed.map((row) => {
|
|
7113
5107
|
try {
|
|
@@ -7132,7 +5126,7 @@ function buildTask(input, now) {
|
|
|
7132
5126
|
const ts = nowIso(now);
|
|
7133
5127
|
const agent = input.agent ? coerceOrchestratorAgent(input.agent) : null;
|
|
7134
5128
|
return {
|
|
7135
|
-
id:
|
|
5129
|
+
id: randomUUID2(),
|
|
7136
5130
|
name: input.name?.trim() || defaultScheduleName(prompt),
|
|
7137
5131
|
prompt,
|
|
7138
5132
|
enabled: input.enabled !== false,
|
|
@@ -7243,7 +5237,7 @@ function formatScheduledPrompt(name, prompt) {
|
|
|
7243
5237
|
${prompt}`;
|
|
7244
5238
|
}
|
|
7245
5239
|
async function defaultDeps() {
|
|
7246
|
-
const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-
|
|
5240
|
+
const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-MA3MHYIM.js");
|
|
7247
5241
|
const orch = getOrchestrator2();
|
|
7248
5242
|
return {
|
|
7249
5243
|
findThread: (id) => findThreadByRef(id),
|
|
@@ -7301,14 +5295,14 @@ function armOne(schedule) {
|
|
|
7301
5295
|
if (!schedule.enabled) return;
|
|
7302
5296
|
const at = new Date(schedule.nextRunAt);
|
|
7303
5297
|
if (Number.isNaN(at.getTime())) return;
|
|
7304
|
-
const
|
|
7305
|
-
if (
|
|
5298
|
+
const delay = Math.max(0, at.getTime() - Date.now());
|
|
5299
|
+
if (delay <= 250) {
|
|
7306
5300
|
void fireSchedule(schedule.id).then(() => {
|
|
7307
5301
|
if (isThisProcessDesktopHost()) armSchedules();
|
|
7308
5302
|
});
|
|
7309
5303
|
return;
|
|
7310
5304
|
}
|
|
7311
|
-
const capped = Math.min(
|
|
5305
|
+
const capped = Math.min(delay, SET_TIMEOUT_MAX);
|
|
7312
5306
|
const timer = setTimeout(() => {
|
|
7313
5307
|
timers.delete(schedule.id);
|
|
7314
5308
|
const live = getSchedule(schedule.id);
|
|
@@ -7376,8 +5370,8 @@ function writeLiveStatus(threadId, status, lastError) {
|
|
|
7376
5370
|
}
|
|
7377
5371
|
function collectActiveRunPorts(runs) {
|
|
7378
5372
|
const ports = /* @__PURE__ */ new Set();
|
|
7379
|
-
for (const
|
|
7380
|
-
const list =
|
|
5373
|
+
for (const run of runs) {
|
|
5374
|
+
const list = run.ports?.length ? run.ports : run.port != null ? [run.port] : [];
|
|
7381
5375
|
for (const p of list) {
|
|
7382
5376
|
if (Number.isFinite(p) && p > 0) ports.add(p);
|
|
7383
5377
|
}
|
|
@@ -7495,7 +5489,7 @@ var Orchestrator = class {
|
|
|
7495
5489
|
this.lightReconcileAt.set(repoPath, Date.now());
|
|
7496
5490
|
for (const thread of listThreads()) {
|
|
7497
5491
|
if (isGlobalThread(thread) || thread.repoPath !== repoPath) continue;
|
|
7498
|
-
if (!
|
|
5492
|
+
if (!existsSync14(thread.worktreePath)) {
|
|
7499
5493
|
setStatus(thread.id, "broken", "Worktree missing on disk");
|
|
7500
5494
|
this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
|
|
7501
5495
|
}
|
|
@@ -7524,7 +5518,7 @@ var Orchestrator = class {
|
|
|
7524
5518
|
}
|
|
7525
5519
|
continue;
|
|
7526
5520
|
}
|
|
7527
|
-
if (!
|
|
5521
|
+
if (!existsSync14(thread.worktreePath)) {
|
|
7528
5522
|
setStatus(thread.id, "broken", "Worktree missing on disk");
|
|
7529
5523
|
this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
|
|
7530
5524
|
continue;
|
|
@@ -7561,7 +5555,7 @@ var Orchestrator = class {
|
|
|
7561
5555
|
orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
|
|
7562
5556
|
});
|
|
7563
5557
|
}
|
|
7564
|
-
const { autoCleanupOrphansEnabled } = await import("./app-settings-
|
|
5558
|
+
const { autoCleanupOrphansEnabled } = await import("./app-settings-RAC4PSSP.js");
|
|
7565
5559
|
if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
|
|
7566
5560
|
await cleanupOrphanWorktrees({ repoPaths });
|
|
7567
5561
|
}
|
|
@@ -7692,8 +5686,8 @@ var Orchestrator = class {
|
|
|
7692
5686
|
scheduleQuotaResume(threadId, resumeAt) {
|
|
7693
5687
|
this.clearQuotaResumeTimer(threadId);
|
|
7694
5688
|
updateThread(threadId, { quotaResumeAt: resumeAt.toISOString() });
|
|
7695
|
-
const
|
|
7696
|
-
const capped = Math.min(
|
|
5689
|
+
const delay = Math.max(5e3, resumeAt.getTime() - Date.now());
|
|
5690
|
+
const capped = Math.min(delay, 2147483647);
|
|
7697
5691
|
const timer = setTimeout(() => {
|
|
7698
5692
|
this.quotaResumeTimers.delete(threadId);
|
|
7699
5693
|
void this.resumeAfterQuotaWait(threadId);
|
|
@@ -7950,7 +5944,7 @@ var Orchestrator = class {
|
|
|
7950
5944
|
}
|
|
7951
5945
|
await setup;
|
|
7952
5946
|
if (skipSetup) return;
|
|
7953
|
-
const { autoRunAfterSetupEnabled } = await import("./app-settings-
|
|
5947
|
+
const { autoRunAfterSetupEnabled } = await import("./app-settings-RAC4PSSP.js");
|
|
7954
5948
|
if (autoRunAfterSetupEnabled()) {
|
|
7955
5949
|
try {
|
|
7956
5950
|
await this.startDev(threadId);
|
|
@@ -8329,7 +6323,7 @@ var Orchestrator = class {
|
|
|
8329
6323
|
}
|
|
8330
6324
|
const isBrightsy = fresh.agent === "brightsy";
|
|
8331
6325
|
const isOrchestration = isOrchestratorThread(fresh);
|
|
8332
|
-
const { autoRenameBranchEnabled, getGithubGitAuthMode, gitBranchPrefixSetting } = await import("./app-settings-
|
|
6326
|
+
const { autoRenameBranchEnabled, getGithubGitAuthMode, gitBranchPrefixSetting } = await import("./app-settings-RAC4PSSP.js");
|
|
8333
6327
|
const gitAuthMode = getGithubGitAuthMode();
|
|
8334
6328
|
const worktreeDirective = isBrightsy || isOrchestration ? null : formatWorktreeDirective(fresh, {
|
|
8335
6329
|
githubSlug: await withTimeout(
|
|
@@ -8484,7 +6478,7 @@ var Orchestrator = class {
|
|
|
8484
6478
|
if (!this.stoppedTurns.has(threadId) && this.requireThread(threadId).agent === "cursor" && exitCode !== 0 && !assistantText && parts.length === 0) {
|
|
8485
6479
|
const sessionId = result.sessionId || this.requireThread(threadId).sessionId || "";
|
|
8486
6480
|
if (sessionId) {
|
|
8487
|
-
const { recoverFinishedCursorRun } = await import("./cursor-recover-
|
|
6481
|
+
const { recoverFinishedCursorRun } = await import("./cursor-recover-MB3T2K5T.js");
|
|
8488
6482
|
for (let i = 0; i < 8; i++) {
|
|
8489
6483
|
const recovered = recoverFinishedCursorRun({
|
|
8490
6484
|
agentId: sessionId,
|
|
@@ -8881,7 +6875,7 @@ var Orchestrator = class {
|
|
|
8881
6875
|
});
|
|
8882
6876
|
}
|
|
8883
6877
|
const latestShared = mergeWorktreeActiveRuns(threadsSharingWorktree(thread.worktreePath));
|
|
8884
|
-
const
|
|
6878
|
+
const run = {
|
|
8885
6879
|
scriptName: resolvedName,
|
|
8886
6880
|
port: handle.port,
|
|
8887
6881
|
ports: handle.ports,
|
|
@@ -8890,7 +6884,7 @@ var Orchestrator = class {
|
|
|
8890
6884
|
};
|
|
8891
6885
|
const nextRuns = [
|
|
8892
6886
|
...latestShared.activeRuns.filter((r) => r.scriptName !== resolvedName),
|
|
8893
|
-
|
|
6887
|
+
run
|
|
8894
6888
|
];
|
|
8895
6889
|
this.syncWorktreeRuns(
|
|
8896
6890
|
thread.worktreePath,
|
|
@@ -8936,7 +6930,7 @@ var Orchestrator = class {
|
|
|
8936
6930
|
const request = sameIntent ? pending : {
|
|
8937
6931
|
op,
|
|
8938
6932
|
scriptName: scriptName ?? null,
|
|
8939
|
-
requestId:
|
|
6933
|
+
requestId: randomUUID3(),
|
|
8940
6934
|
requestedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8941
6935
|
};
|
|
8942
6936
|
if (!sameIntent) {
|
|
@@ -8957,14 +6951,14 @@ var Orchestrator = class {
|
|
|
8957
6951
|
throw new Error(req.error);
|
|
8958
6952
|
}
|
|
8959
6953
|
const shared = mergeWorktreeActiveRuns(threadsSharingWorktree(latest.worktreePath));
|
|
8960
|
-
const
|
|
6954
|
+
const run = scriptName ? shared.activeRuns.find((r) => r.scriptName === scriptName) : shared.activeRuns[0];
|
|
8961
6955
|
const fulfilled = req?.requestId === request.requestId && Boolean(req.fulfilledAt);
|
|
8962
6956
|
const requestGone = !req || req.requestId !== request.requestId;
|
|
8963
6957
|
if (op === "start") {
|
|
8964
|
-
if (
|
|
8965
|
-
return { port:
|
|
6958
|
+
if (run && (fulfilled || requestGone || run.startedAt >= request.requestedAt)) {
|
|
6959
|
+
return { port: run.port, scriptName: run.scriptName, ports: run.ports };
|
|
8966
6960
|
}
|
|
8967
|
-
if (fulfilled && !
|
|
6961
|
+
if (fulfilled && !run) {
|
|
8968
6962
|
throw new Error("Desktop reported the run script started, but it is not active.");
|
|
8969
6963
|
}
|
|
8970
6964
|
} else if (fulfilled) {
|
|
@@ -9030,15 +7024,15 @@ var Orchestrator = class {
|
|
|
9030
7024
|
}
|
|
9031
7025
|
this.processes.delete(legacyKey);
|
|
9032
7026
|
}
|
|
9033
|
-
const
|
|
9034
|
-
if (!hadHandle2 &&
|
|
9035
|
-
if (typeof
|
|
9036
|
-
killDetachedRunTree(
|
|
7027
|
+
const run = shared.activeRuns.find((r) => r.scriptName === scriptName);
|
|
7028
|
+
if (!hadHandle2 && run) {
|
|
7029
|
+
if (typeof run.pid === "number") {
|
|
7030
|
+
killDetachedRunTree(run.pid, thread.worktreePath);
|
|
9037
7031
|
}
|
|
9038
|
-
killListenersOnPorts(collectActiveRunPorts([
|
|
7032
|
+
killListenersOnPorts(collectActiveRunPorts([run]));
|
|
9039
7033
|
}
|
|
9040
7034
|
const remaining = shared.activeRuns.filter((r) => r.scriptName !== scriptName);
|
|
9041
|
-
const isPrimary = shared.devPort != null &&
|
|
7035
|
+
const isPrimary = shared.devPort != null && run?.port === shared.devPort;
|
|
9042
7036
|
this.syncWorktreeRuns(
|
|
9043
7037
|
thread.worktreePath,
|
|
9044
7038
|
remaining,
|
|
@@ -9056,9 +7050,9 @@ var Orchestrator = class {
|
|
|
9056
7050
|
}
|
|
9057
7051
|
}
|
|
9058
7052
|
if (!hadHandle && shared.activeRuns.length > 0) {
|
|
9059
|
-
for (const
|
|
9060
|
-
if (typeof
|
|
9061
|
-
killDetachedRunTree(
|
|
7053
|
+
for (const run of shared.activeRuns) {
|
|
7054
|
+
if (typeof run.pid === "number") {
|
|
7055
|
+
killDetachedRunTree(run.pid, thread.worktreePath);
|
|
9062
7056
|
}
|
|
9063
7057
|
}
|
|
9064
7058
|
killListenersOnPorts(collectActiveRunPorts(shared.activeRuns));
|
|
@@ -9102,9 +7096,9 @@ var Orchestrator = class {
|
|
|
9102
7096
|
(k) => isWorktreeRunProcessKey(k, thread.worktreePath) || siblings.some((t) => k.startsWith(`${t.id}:run:`) || k === `${t.id}:dev`)
|
|
9103
7097
|
);
|
|
9104
7098
|
if (!force && hasLiveHandle) continue;
|
|
9105
|
-
for (const
|
|
9106
|
-
if (typeof
|
|
9107
|
-
killDetachedRunTree(
|
|
7099
|
+
for (const run of shared.activeRuns) {
|
|
7100
|
+
if (typeof run.pid === "number") {
|
|
7101
|
+
killDetachedRunTree(run.pid, thread.worktreePath);
|
|
9108
7102
|
}
|
|
9109
7103
|
}
|
|
9110
7104
|
killListenersOnPorts(collectActiveRunPorts(shared.activeRuns));
|
|
@@ -9313,7 +7307,7 @@ var Orchestrator = class {
|
|
|
9313
7307
|
return thread;
|
|
9314
7308
|
}
|
|
9315
7309
|
const start = Date.now();
|
|
9316
|
-
return new Promise((
|
|
7310
|
+
return new Promise((resolve, reject) => {
|
|
9317
7311
|
let settled = false;
|
|
9318
7312
|
let timer;
|
|
9319
7313
|
const finish = (fn) => {
|
|
@@ -9331,7 +7325,7 @@ var Orchestrator = class {
|
|
|
9331
7325
|
finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
|
|
9332
7326
|
return;
|
|
9333
7327
|
}
|
|
9334
|
-
finish(() =>
|
|
7328
|
+
finish(() => resolve(latest));
|
|
9335
7329
|
return;
|
|
9336
7330
|
}
|
|
9337
7331
|
if (event.type === "status_changed") {
|
|
@@ -9341,7 +7335,7 @@ var Orchestrator = class {
|
|
|
9341
7335
|
return;
|
|
9342
7336
|
}
|
|
9343
7337
|
if (!["running", "queued"].includes(latest.status)) {
|
|
9344
|
-
finish(() =>
|
|
7338
|
+
finish(() => resolve(latest));
|
|
9345
7339
|
}
|
|
9346
7340
|
}
|
|
9347
7341
|
});
|
|
@@ -9353,12 +7347,12 @@ var Orchestrator = class {
|
|
|
9353
7347
|
}
|
|
9354
7348
|
const current = this.healStaleReportedActivity(raw);
|
|
9355
7349
|
if (!this.threadLooksLive(current)) {
|
|
9356
|
-
finish(() =>
|
|
7350
|
+
finish(() => resolve(current));
|
|
9357
7351
|
return;
|
|
9358
7352
|
}
|
|
9359
7353
|
if (Date.now() - start > timeoutMs) {
|
|
9360
7354
|
if (opts?.resolveIfStillRunning) {
|
|
9361
|
-
finish(() =>
|
|
7355
|
+
finish(() => resolve(current));
|
|
9362
7356
|
} else {
|
|
9363
7357
|
finish(() => reject(new Error("wait_for_turn timed out")));
|
|
9364
7358
|
}
|
|
@@ -9639,7 +7633,7 @@ var Orchestrator = class {
|
|
|
9639
7633
|
if (Object.keys(patch).length === 0) continue;
|
|
9640
7634
|
updateThread(t.id, patch);
|
|
9641
7635
|
}
|
|
9642
|
-
const { autoArchiveOnMergeEnabled } = await import("./app-settings-
|
|
7636
|
+
const { autoArchiveOnMergeEnabled } = await import("./app-settings-RAC4PSSP.js");
|
|
9643
7637
|
const latest = this.requireThread(thread.id);
|
|
9644
7638
|
if (!shouldAutoArchiveOnPrMerge({
|
|
9645
7639
|
previousPrState: prevState || null,
|
|
@@ -9930,7 +7924,7 @@ var Orchestrator = class {
|
|
|
9930
7924
|
this.enforceHistoryRetention();
|
|
9931
7925
|
if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
|
|
9932
7926
|
try {
|
|
9933
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
7927
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-SLP24Z3S.js");
|
|
9934
7928
|
await ensureWorkspace2(thread.repoPath);
|
|
9935
7929
|
} catch {
|
|
9936
7930
|
}
|
|
@@ -9957,7 +7951,7 @@ var Orchestrator = class {
|
|
|
9957
7951
|
await runArchiveScript(thread.repoPath, thread.worktreePath);
|
|
9958
7952
|
} catch {
|
|
9959
7953
|
}
|
|
9960
|
-
const { deleteBranchOnPurgeEnabled } = await import("./app-settings-
|
|
7954
|
+
const { deleteBranchOnPurgeEnabled } = await import("./app-settings-RAC4PSSP.js");
|
|
9961
7955
|
const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
|
|
9962
7956
|
await removeWorktree(thread.repoPath, thread.worktreePath, {
|
|
9963
7957
|
deleteBranch: deleteBranch ? thread.branchName : void 0
|
|
@@ -9972,26 +7966,26 @@ var Orchestrator = class {
|
|
|
9972
7966
|
throw new Error("Thread is not archived");
|
|
9973
7967
|
}
|
|
9974
7968
|
if (isGlobalThread(thread)) {
|
|
9975
|
-
const { globalAgentCwd } = await import("./paths-
|
|
7969
|
+
const { globalAgentCwd } = await import("./paths-HYT5WOOX.js");
|
|
9976
7970
|
updateThread(thread.id, { worktreePath: globalAgentCwd() });
|
|
9977
7971
|
const restored2 = setStatus(thread.id, "idle");
|
|
9978
7972
|
this.emit({ type: "status_changed", threadId: restored2.id, status: restored2.status });
|
|
9979
7973
|
return restored2;
|
|
9980
7974
|
}
|
|
9981
|
-
if (!
|
|
7975
|
+
if (!existsSync14(thread.worktreePath)) {
|
|
9982
7976
|
if (isCowboyThread(thread) || isPrimaryCheckoutThread(thread)) {
|
|
9983
7977
|
throw new Error(
|
|
9984
7978
|
`Cowboy checkout missing: ${thread.worktreePath}. Re-add the project folder, then restore.`
|
|
9985
7979
|
);
|
|
9986
7980
|
}
|
|
9987
|
-
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-
|
|
7981
|
+
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-ACW23KJ5.js");
|
|
9988
7982
|
const slug = thread.worktreePath.split("/").pop();
|
|
9989
7983
|
const dest = thread.worktreePath;
|
|
9990
7984
|
await withRepoGitLock(thread.repoPath, async () => {
|
|
9991
7985
|
await git(["worktree", "add", dest, thread.branchName], thread.repoPath);
|
|
9992
7986
|
});
|
|
9993
|
-
const { ensureWorktreeSideboardIgnored
|
|
9994
|
-
await
|
|
7987
|
+
const { ensureWorktreeSideboardIgnored } = await import("./worktree-exclude-5N2KTYIT.js");
|
|
7988
|
+
await ensureWorktreeSideboardIgnored(dest);
|
|
9995
7989
|
void createThreadWorktree2;
|
|
9996
7990
|
void slug;
|
|
9997
7991
|
}
|
|
@@ -10094,7 +8088,7 @@ async function startOrchestration(opts) {
|
|
|
10094
8088
|
sourceRef: "default",
|
|
10095
8089
|
...createOpts
|
|
10096
8090
|
}).catch(async () => {
|
|
10097
|
-
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-
|
|
8091
|
+
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-ACW23KJ5.js");
|
|
10098
8092
|
const repo = await resolveRepoRoot2(repoPath);
|
|
10099
8093
|
const def = await resolveDefaultBranch2(repo);
|
|
10100
8094
|
return orch.createThread({
|
|
@@ -10104,7 +8098,7 @@ async function startOrchestration(opts) {
|
|
|
10104
8098
|
repoPath: repo
|
|
10105
8099
|
});
|
|
10106
8100
|
});
|
|
10107
|
-
const { updateThread: upd } = await import("./thread-store-
|
|
8101
|
+
const { updateThread: upd } = await import("./thread-store-B3ZM4QEZ.js");
|
|
10108
8102
|
const updated = upd(thread.id, {
|
|
10109
8103
|
sourceType: "orchestration",
|
|
10110
8104
|
sourceRef: goal
|
|
@@ -10117,15 +8111,7 @@ async function startOrchestration(opts) {
|
|
|
10117
8111
|
|
|
10118
8112
|
export {
|
|
10119
8113
|
isInjectedNoticeText,
|
|
10120
|
-
needsCoordinatorAction,
|
|
10121
|
-
mcpWaitForTurnTimeoutMs,
|
|
10122
|
-
mcpWaitTaskHint,
|
|
10123
|
-
waitForTurnToolResult,
|
|
10124
8114
|
AGENT_GIT_ACTIONS,
|
|
10125
|
-
threadsSharingWorktree,
|
|
10126
|
-
mcpWaitForJobTimeoutMs,
|
|
10127
|
-
waitForDetachedJob,
|
|
10128
|
-
stopDetachedJob,
|
|
10129
8115
|
MAX_RUN_LOG_CHARS,
|
|
10130
8116
|
DEFAULT_RUN_LOG_TAIL_CHARS,
|
|
10131
8117
|
sliceRunLogOutput,
|