@sideboard-ai/core 0.1.263 → 0.1.266
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.cjs +6 -2
- package/dist/agents/cursor-runner.js +13 -7
- 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 +350 -68
- package/dist/index.d.ts +350 -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,26 +1,65 @@
|
|
|
1
1
|
import {
|
|
2
2
|
adjustBrightsyTurnExit,
|
|
3
3
|
allAdapters,
|
|
4
|
+
getAdapter,
|
|
5
|
+
isSessionQuotaLimit,
|
|
6
|
+
parseBrightsyCliLine,
|
|
7
|
+
parseSessionQuotaResetAt,
|
|
8
|
+
planModeTurnInstruction,
|
|
9
|
+
resolveQuotaFallbackAgent
|
|
10
|
+
} from "./chunk-3VBS6MES.js";
|
|
11
|
+
import {
|
|
12
|
+
REVIEW_TAB_TITLE,
|
|
13
|
+
applyWorktreePkgCacheEnv,
|
|
14
|
+
buildBrightsySessionSeed,
|
|
15
|
+
buildForkTranscriptAttachment,
|
|
16
|
+
buildSessionSeed,
|
|
17
|
+
copyConfiguredFiles,
|
|
18
|
+
createChatTab,
|
|
19
|
+
deriveTaskState,
|
|
20
|
+
forkChatTab,
|
|
21
|
+
forkMessageSlice,
|
|
22
|
+
formatReviewWriteGateDirective,
|
|
23
|
+
formatReviewWriteGateReminder,
|
|
24
|
+
getDefaultRunScript,
|
|
25
|
+
getRunMode,
|
|
26
|
+
isJobContinuePrompt,
|
|
27
|
+
isReviewWriteGatedThread,
|
|
28
|
+
isWorktreeRunProcessKey,
|
|
29
|
+
killDetachedRunTree,
|
|
30
|
+
killListenersOnPorts,
|
|
31
|
+
lastActivityAtForWait,
|
|
32
|
+
listRunScripts,
|
|
33
|
+
listRunningDetachedJobs,
|
|
34
|
+
maybeCompactContext,
|
|
35
|
+
mergeWorktreeActiveRuns,
|
|
36
|
+
planJobContinue,
|
|
37
|
+
requestReview,
|
|
38
|
+
resolveWorktreeSetupLog,
|
|
39
|
+
runArchiveScript,
|
|
40
|
+
runWorkspaceSetup,
|
|
41
|
+
startDevServer,
|
|
42
|
+
threadsSharingWorktree,
|
|
43
|
+
turnWatchedDetachedJob,
|
|
44
|
+
worktreeDevProcessKey,
|
|
45
|
+
worktreeRunProcessKey,
|
|
46
|
+
worktreeSetupProcessKey
|
|
47
|
+
} from "./chunk-X7RP3TQB.js";
|
|
48
|
+
import {
|
|
4
49
|
applyAgentEvent,
|
|
5
50
|
clipAgentEventForPaint,
|
|
6
|
-
compactSummaryPart,
|
|
7
51
|
finalizeParts,
|
|
8
|
-
getAdapter,
|
|
9
52
|
isBrightsyNdjsonLine,
|
|
10
53
|
isInternalAgentStatusText,
|
|
11
54
|
isPollWrapperToolName,
|
|
12
|
-
isSessionQuotaLimit,
|
|
13
55
|
lastAssistantMessageText,
|
|
14
56
|
liveActivitySummary,
|
|
57
|
+
messagePartParentId,
|
|
15
58
|
normalizeParseResult,
|
|
16
|
-
parseBrightsyCliLine,
|
|
17
|
-
parseSessionQuotaResetAt,
|
|
18
59
|
partsToAssistantText,
|
|
19
|
-
planModeTurnInstruction,
|
|
20
|
-
resolveQuotaFallbackAgent,
|
|
21
60
|
stripBrightsyNdjsonNoise,
|
|
22
61
|
toolDescription
|
|
23
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-FAGK33E2.js";
|
|
24
63
|
import {
|
|
25
64
|
titleFromPrompt
|
|
26
65
|
} from "./chunk-7H655NEW.js";
|
|
@@ -40,22 +79,19 @@ import {
|
|
|
40
79
|
ensureGlobalCoordinatorCwd,
|
|
41
80
|
ensureWorkspace,
|
|
42
81
|
healOrchestrationSoccerTitles,
|
|
43
|
-
isCloudCoordinatorThread,
|
|
44
82
|
isGlobalRepoPath,
|
|
45
83
|
isGlobalThread,
|
|
46
84
|
isOrchestratorThread,
|
|
47
|
-
isSlackCoordinatorThread,
|
|
48
85
|
orchestratorSessionPoisonedByBuiltins,
|
|
49
86
|
persistPendingFileAttachments,
|
|
50
87
|
removeWorkspace,
|
|
51
88
|
stageAbsolutePathsAsAttachments,
|
|
52
89
|
stageBuffersAsAttachments,
|
|
53
90
|
syncWorkspacesFromThreads
|
|
54
|
-
} from "./chunk-
|
|
91
|
+
} from "./chunk-DB7MVDYS.js";
|
|
55
92
|
import {
|
|
56
93
|
addPrStackLayer,
|
|
57
94
|
agentNicknameFromWorktreePath,
|
|
58
|
-
allocateTeamName,
|
|
59
95
|
allocateTeamSlug,
|
|
60
96
|
canonicalizeRepoPath,
|
|
61
97
|
ciChecksFailed,
|
|
@@ -97,7 +133,6 @@ import {
|
|
|
97
133
|
resolvePrSelector,
|
|
98
134
|
resolveRepoRoot,
|
|
99
135
|
switchWorktreeBranch,
|
|
100
|
-
takenSlugsFromThread,
|
|
101
136
|
teamSlugFromName,
|
|
102
137
|
ticketSlugForBranch,
|
|
103
138
|
withRepoGitLock,
|
|
@@ -105,7 +140,7 @@ import {
|
|
|
105
140
|
worktreeIdentityTitles,
|
|
106
141
|
worktreeNameFromPath,
|
|
107
142
|
worktreeSlugForTicket
|
|
108
|
-
} from "./chunk-
|
|
143
|
+
} from "./chunk-USBZ7PH4.js";
|
|
109
144
|
import {
|
|
110
145
|
appendMessage,
|
|
111
146
|
createEmptyThread,
|
|
@@ -118,9 +153,6 @@ import {
|
|
|
118
153
|
withThreadLock,
|
|
119
154
|
writeThread
|
|
120
155
|
} from "./chunk-FWNL62VA.js";
|
|
121
|
-
import {
|
|
122
|
-
ensureWorktreeSideboardIgnored
|
|
123
|
-
} from "./chunk-2P4XKWV2.js";
|
|
124
156
|
import {
|
|
125
157
|
httpFetch
|
|
126
158
|
} from "./chunk-N7WZ6NIN.js";
|
|
@@ -129,34 +161,28 @@ import {
|
|
|
129
161
|
} from "./chunk-AXQ4ZWHK.js";
|
|
130
162
|
import {
|
|
131
163
|
applyAgentRunnerHeapEnv,
|
|
132
|
-
|
|
164
|
+
packagedMcpDir
|
|
165
|
+
} from "./chunk-FIBZEFKA.js";
|
|
166
|
+
import {
|
|
133
167
|
fallbackTurnFailDetail,
|
|
134
168
|
formatAgentErrorContinuePrompt,
|
|
135
169
|
formatTurnExitError,
|
|
136
170
|
looksLikeAgentFailureMessage,
|
|
137
171
|
looksLikeInvalidAgentSession,
|
|
138
172
|
looksLikeV8Oom,
|
|
139
|
-
packagedMcpDir,
|
|
140
173
|
pushTurnStderr,
|
|
141
174
|
shouldFeedErrorBackToAgent,
|
|
142
175
|
shouldRetryCodexPluginIsolate,
|
|
143
176
|
shouldRetryFailedAgentTurn,
|
|
144
177
|
summarizeTurnStderr,
|
|
145
178
|
turnFailChatText
|
|
146
|
-
} from "./chunk-
|
|
147
|
-
import {
|
|
148
|
-
ATTACHMENTS_DIR,
|
|
149
|
-
DETACHED_JOBS_DIR,
|
|
150
|
-
LEGACY_ATTACHMENTS_DIR,
|
|
151
|
-
LEGACY_DETACHED_JOBS_DIR
|
|
152
|
-
} from "./chunk-6K64FJDD.js";
|
|
179
|
+
} from "./chunk-5N4Q46SG.js";
|
|
153
180
|
import {
|
|
154
181
|
clearStaleIndexLocks,
|
|
155
182
|
enrichPathWithNpmGlobalBin,
|
|
156
183
|
ensureAgentPath,
|
|
157
184
|
git,
|
|
158
|
-
resolveGitDirsForLockRecovery
|
|
159
|
-
run
|
|
185
|
+
resolveGitDirsForLockRecovery
|
|
160
186
|
} from "./chunk-QVN2WOKJ.js";
|
|
161
187
|
import {
|
|
162
188
|
HISTORY_CLEANUP_INTERVAL_HOURS_DEFAULT,
|
|
@@ -166,7 +192,6 @@ import {
|
|
|
166
192
|
caffeinateWhileSchedulesEnabled,
|
|
167
193
|
childEnvWithAppSettings,
|
|
168
194
|
coerceOrchestratorAgent,
|
|
169
|
-
contextTokens,
|
|
170
195
|
followUpBehavior,
|
|
171
196
|
formatViewerContextDirective,
|
|
172
197
|
formatViewerContextReminder,
|
|
@@ -190,10 +215,7 @@ import {
|
|
|
190
215
|
updateAdvancedSettings,
|
|
191
216
|
usageOnLimit,
|
|
192
217
|
withTurnScopedCost
|
|
193
|
-
} from "./chunk-
|
|
194
|
-
import {
|
|
195
|
-
stripNestedElectronEnv
|
|
196
|
-
} from "./chunk-4TR3HZFT.js";
|
|
218
|
+
} from "./chunk-PAH7KDL2.js";
|
|
197
219
|
import {
|
|
198
220
|
examplePrefixedBranch,
|
|
199
221
|
sanitizeGitBranchPrefix
|
|
@@ -213,15 +235,11 @@ import {
|
|
|
213
235
|
import {
|
|
214
236
|
annotateDevAccessRuns,
|
|
215
237
|
appDataDir,
|
|
216
|
-
findConventionSetup,
|
|
217
|
-
loadRepoSettings,
|
|
218
238
|
loadWorkspaceSettings,
|
|
219
|
-
runCursorWorktreeSetup,
|
|
220
239
|
sideboardReposDir,
|
|
221
240
|
sideboardWorkspacesDir,
|
|
222
241
|
threadLivePath,
|
|
223
242
|
threadsDir,
|
|
224
|
-
workspaceSettingsSourceLabel,
|
|
225
243
|
worktreesRoot
|
|
226
244
|
} from "./chunk-EHT2DRT4.js";
|
|
227
245
|
|
|
@@ -266,95 +284,9 @@ function formatInjectedNoticesForTurn(notices) {
|
|
|
266
284
|
].join("\n\n");
|
|
267
285
|
}
|
|
268
286
|
|
|
269
|
-
// src/orchestrator/task-state.ts
|
|
270
|
-
function isTerminalTaskState(state) {
|
|
271
|
-
return state !== "submitted" && state !== "working";
|
|
272
|
-
}
|
|
273
|
-
function isIncompleteTaskState(state) {
|
|
274
|
-
return state === "failed" || state === "canceled";
|
|
275
|
-
}
|
|
276
|
-
function needsCoordinatorAction(state) {
|
|
277
|
-
return isIncompleteTaskState(state) || state === "input-required";
|
|
278
|
-
}
|
|
279
|
-
var ASK_USER_TOOL = /(^|[_.:/])ask_user$/i;
|
|
280
|
-
function endedOnAskUser(parts) {
|
|
281
|
-
if (!parts?.length) return false;
|
|
282
|
-
for (let i = parts.length - 1; i >= 0; i--) {
|
|
283
|
-
const p = parts[i];
|
|
284
|
-
if (p.type === "tool" && !p.parentId) {
|
|
285
|
-
return ASK_USER_TOOL.test(p.name);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
290
|
-
function deriveTaskState(input) {
|
|
291
|
-
if (input.stillRunning) {
|
|
292
|
-
return input.status === "queued" ? "submitted" : "working";
|
|
293
|
-
}
|
|
294
|
-
switch (input.status) {
|
|
295
|
-
case "error":
|
|
296
|
-
case "broken":
|
|
297
|
-
return "failed";
|
|
298
|
-
case "stopped":
|
|
299
|
-
return "canceled";
|
|
300
|
-
default:
|
|
301
|
-
return endedOnAskUser(input.lastAgentParts) ? "input-required" : "completed";
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// src/mcp/wait-for-turn.ts
|
|
306
|
-
var MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
|
|
307
|
-
function mcpWaitForTurnTimeoutMs(requested) {
|
|
308
|
-
const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
|
|
309
|
-
if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
|
|
310
|
-
return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
|
|
311
|
-
}
|
|
312
|
-
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.";
|
|
313
|
-
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.";
|
|
314
|
-
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.";
|
|
315
|
-
var MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
|
|
316
|
-
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.";
|
|
317
|
-
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.";
|
|
318
|
-
function mcpWaitTaskHint(taskState, status) {
|
|
319
|
-
switch (taskState) {
|
|
320
|
-
case "submitted":
|
|
321
|
-
return MCP_WAIT_QUEUED_HINT;
|
|
322
|
-
case "working":
|
|
323
|
-
return MCP_WAIT_STILL_RUNNING_HINT;
|
|
324
|
-
case "input-required":
|
|
325
|
-
return MCP_WAIT_INPUT_REQUIRED_HINT;
|
|
326
|
-
case "canceled":
|
|
327
|
-
return MCP_WAIT_STOPPED_HINT;
|
|
328
|
-
case "failed":
|
|
329
|
-
return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
|
|
330
|
-
default:
|
|
331
|
-
return void 0;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
function nonemptyIso(value) {
|
|
335
|
-
const trimmed = value?.trim();
|
|
336
|
-
return trimmed ? trimmed : null;
|
|
337
|
-
}
|
|
338
|
-
function lastActivityAtForWait(stillRunning, liveUpdatedAt, fallbacks) {
|
|
339
|
-
if (stillRunning) return nonemptyIso(liveUpdatedAt);
|
|
340
|
-
for (const value of fallbacks) {
|
|
341
|
-
const iso = nonemptyIso(value);
|
|
342
|
-
if (iso) return iso;
|
|
343
|
-
}
|
|
344
|
-
return null;
|
|
345
|
-
}
|
|
346
|
-
function waitForTurnToolResult(input) {
|
|
347
|
-
return {
|
|
348
|
-
...input,
|
|
349
|
-
usage: input.usage ?? null,
|
|
350
|
-
hint: mcpWaitTaskHint(input.taskState, input.status),
|
|
351
|
-
incomplete: needsCoordinatorAction(input.taskState)
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
|
|
355
287
|
// src/orchestrator/orchestrator.ts
|
|
356
|
-
import { randomUUID as
|
|
357
|
-
import { existsSync as
|
|
288
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
289
|
+
import { existsSync as existsSync14 } from "fs";
|
|
358
290
|
|
|
359
291
|
// src/agents/spawn.ts
|
|
360
292
|
import { createInterface } from "readline";
|
|
@@ -388,6 +320,49 @@ async function withTimeout(promise, ms, label) {
|
|
|
388
320
|
}
|
|
389
321
|
}
|
|
390
322
|
|
|
323
|
+
// src/agents/stdout-replay.ts
|
|
324
|
+
var REPLAY_MIN_CHARS = 80;
|
|
325
|
+
function textSoFar(parts, parentId) {
|
|
326
|
+
const parent = parentId?.trim() || "";
|
|
327
|
+
let out = "";
|
|
328
|
+
for (const part of parts) {
|
|
329
|
+
if (part.type !== "text") continue;
|
|
330
|
+
if ((messagePartParentId(part) ?? "") !== parent) continue;
|
|
331
|
+
out += part.text;
|
|
332
|
+
}
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
function replaySized(text) {
|
|
336
|
+
return text.trim().length >= REPLAY_MIN_CHARS;
|
|
337
|
+
}
|
|
338
|
+
function tailAfterAccumulated(accumulated, incoming) {
|
|
339
|
+
if (incoming.startsWith(accumulated)) return incoming.slice(accumulated.length);
|
|
340
|
+
const acc = accumulated.trim();
|
|
341
|
+
const inc = incoming.trim();
|
|
342
|
+
if (!acc || !inc.startsWith(acc)) return null;
|
|
343
|
+
const at = incoming.indexOf(acc);
|
|
344
|
+
if (at < 0) return null;
|
|
345
|
+
return incoming.slice(at + acc.length);
|
|
346
|
+
}
|
|
347
|
+
function foldDuplicateStdout(accumulated, incoming) {
|
|
348
|
+
if (!incoming) return null;
|
|
349
|
+
if (!accumulated) return incoming;
|
|
350
|
+
const accTrim = accumulated.trim();
|
|
351
|
+
const incTrim = incoming.trim();
|
|
352
|
+
if (!incTrim) return incoming;
|
|
353
|
+
if (incTrim === accTrim) return null;
|
|
354
|
+
if (!replaySized(incoming)) return incoming;
|
|
355
|
+
if (accTrim.includes(incTrim)) return null;
|
|
356
|
+
let tail = tailAfterAccumulated(accumulated, incoming);
|
|
357
|
+
if (tail == null) return incoming;
|
|
358
|
+
for (let i = 0; i < 3 && tail.trim(); i++) {
|
|
359
|
+
const next = tailAfterAccumulated(accumulated, tail);
|
|
360
|
+
if (next == null) break;
|
|
361
|
+
tail = next;
|
|
362
|
+
}
|
|
363
|
+
return tail.trim() ? tail : null;
|
|
364
|
+
}
|
|
365
|
+
|
|
391
366
|
// src/hook/agent-host-env.ts
|
|
392
367
|
import { delimiter } from "path";
|
|
393
368
|
var DROP_EXACT = /* @__PURE__ */ new Set([
|
|
@@ -444,56 +419,6 @@ function sanitizeAgentHostEnv(env, cwd) {
|
|
|
444
419
|
return env;
|
|
445
420
|
}
|
|
446
421
|
|
|
447
|
-
// src/hook/worktree-pkg-cache.ts
|
|
448
|
-
import { existsSync, mkdirSync } from "fs";
|
|
449
|
-
import { join as join2 } from "path";
|
|
450
|
-
var WORKTREE_PKG_CACHE_REL = ".context/.sideboard/pkg-cache";
|
|
451
|
-
function worktreePkgCacheRoot(worktreePath) {
|
|
452
|
-
return join2(worktreePath, WORKTREE_PKG_CACHE_REL);
|
|
453
|
-
}
|
|
454
|
-
function worktreePkgCacheEnv(worktreePath) {
|
|
455
|
-
const root = worktreePkgCacheRoot(worktreePath);
|
|
456
|
-
const store = join2(root, "pnpm-store");
|
|
457
|
-
const pnpmCache = join2(root, "pnpm");
|
|
458
|
-
const npmCache = join2(root, "npm");
|
|
459
|
-
const nodeGyp = join2(root, "node-gyp");
|
|
460
|
-
const electron = join2(root, "electron");
|
|
461
|
-
return {
|
|
462
|
-
npm_config_store_dir: store,
|
|
463
|
-
PNPM_STORE_DIR: store,
|
|
464
|
-
npm_config_cache_dir: pnpmCache,
|
|
465
|
-
npm_config_cache: npmCache,
|
|
466
|
-
npm_config_devdir: nodeGyp,
|
|
467
|
-
// @electron/get — do not set ELECTRON_CACHE (stripNestedElectronEnv).
|
|
468
|
-
electron_config_cache: electron
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
function isSet(value) {
|
|
472
|
-
return value != null && value.trim() !== "";
|
|
473
|
-
}
|
|
474
|
-
function fillGroup(env, keys, values) {
|
|
475
|
-
if (keys.some((key) => isSet(env[key]))) return;
|
|
476
|
-
for (const key of keys) {
|
|
477
|
-
const value = values[key];
|
|
478
|
-
if (value) env[key] = value;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
function ensureWorktreePkgCacheDirs(worktreePath) {
|
|
482
|
-
if (!existsSync(worktreePath)) return;
|
|
483
|
-
mkdirSync(worktreePkgCacheRoot(worktreePath), { recursive: true });
|
|
484
|
-
}
|
|
485
|
-
function applyWorktreePkgCacheEnv(env, worktreePath) {
|
|
486
|
-
const cwd = worktreePath.trim();
|
|
487
|
-
if (!cwd) return;
|
|
488
|
-
const next = worktreePkgCacheEnv(cwd);
|
|
489
|
-
fillGroup(env, ["npm_config_store_dir", "PNPM_STORE_DIR"], next);
|
|
490
|
-
fillGroup(env, ["npm_config_cache_dir"], next);
|
|
491
|
-
fillGroup(env, ["npm_config_cache"], next);
|
|
492
|
-
fillGroup(env, ["npm_config_devdir"], next);
|
|
493
|
-
fillGroup(env, ["electron_config_cache"], next);
|
|
494
|
-
ensureWorktreePkgCacheDirs(cwd);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
422
|
// src/agents/spawn.ts
|
|
498
423
|
function applyPromptCacheTtlEnv(agent, env) {
|
|
499
424
|
if (agent === "claude" && env.FORCE_PROMPT_CACHING_5M !== "1") {
|
|
@@ -512,9 +437,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
512
437
|
`Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
|
|
513
438
|
);
|
|
514
439
|
}
|
|
515
|
-
const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-
|
|
440
|
+
const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-EEWJOMED.js");
|
|
516
441
|
if (isGlobalThread2(thread)) {
|
|
517
|
-
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-
|
|
442
|
+
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-BKRQDZFT.js");
|
|
518
443
|
ensureGlobalCoordinatorCwd2(
|
|
519
444
|
isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
|
|
520
445
|
);
|
|
@@ -621,7 +546,8 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
621
546
|
events = normalizeParseResult(parseBrightsyCliLine(line));
|
|
622
547
|
}
|
|
623
548
|
}
|
|
624
|
-
for (const
|
|
549
|
+
for (const event of events) {
|
|
550
|
+
let parsed = event;
|
|
625
551
|
if (parsed.type === "session_id") {
|
|
626
552
|
sessionId = parsed.data;
|
|
627
553
|
outbound.push(parsed);
|
|
@@ -637,9 +563,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
637
563
|
if (thread.agent === "brightsy" && isBrightsyNdjsonLine(parsed.data)) {
|
|
638
564
|
continue;
|
|
639
565
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
566
|
+
const folded = foldDuplicateStdout(textSoFar(parts, parsed.parentId), parsed.data);
|
|
567
|
+
if (folded == null) continue;
|
|
568
|
+
if (folded !== parsed.data) parsed = { ...parsed, data: folded };
|
|
643
569
|
}
|
|
644
570
|
parts = applyAgentEvent(parts, parsed);
|
|
645
571
|
if (parsed.type === "stdout") {
|
|
@@ -725,2149 +651,141 @@ function gitActionExpansion(action, base) {
|
|
|
725
651
|
case "resolve-conflicts":
|
|
726
652
|
return `Fetch the PR base${base ? ` (\`${base}\`)` : ""}, merge it into this branch, resolve conflicts carefully, then commit and push until the PR is mergeable.`;
|
|
727
653
|
case "ready-for-review":
|
|
728
|
-
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.";
|
|
729
|
-
case "merge":
|
|
730
|
-
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.";
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
function expandCanonicalGitRequest(prompt) {
|
|
734
|
-
const trimmed = prompt.trim();
|
|
735
|
-
let action = null;
|
|
736
|
-
let base = null;
|
|
737
|
-
for (const candidate of AGENT_GIT_ACTIONS) {
|
|
738
|
-
if (candidate === "resolve-conflicts") continue;
|
|
739
|
-
if (agentGitPrompt(candidate) === trimmed) {
|
|
740
|
-
action = candidate;
|
|
741
|
-
break;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
if (!action) {
|
|
745
|
-
const m = RESOLVE_CONFLICTS_RE.exec(trimmed);
|
|
746
|
-
if (m) {
|
|
747
|
-
action = "resolve-conflicts";
|
|
748
|
-
base = m[1] ?? null;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
let expansion = action ? gitActionExpansion(action, base) : null;
|
|
752
|
-
if (!expansion && trimmed === "Address review comments.") {
|
|
753
|
-
expansion = "Read the PR review feedback (`gh pr view --comments`, `gh api` review comments), make the requested changes, commit, and push.";
|
|
754
|
-
} else if (!expansion && FIX_CI_RE.test(trimmed)) {
|
|
755
|
-
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.";
|
|
756
|
-
}
|
|
757
|
-
if (!expansion) return prompt;
|
|
758
|
-
return `${trimmed}
|
|
759
|
-
|
|
760
|
-
(Sideboard git request \u2014 do not ask for clarification: ${expansion})`;
|
|
761
|
-
}
|
|
762
|
-
var FIX_CI_RE = /^Fix CI: .+\.$/;
|
|
763
|
-
|
|
764
|
-
// src/git/pr-merge-archive.ts
|
|
765
|
-
function normalizePrState(state) {
|
|
766
|
-
return (state ?? "").trim().toUpperCase();
|
|
767
|
-
}
|
|
768
|
-
function samePrIdentity(a, b) {
|
|
769
|
-
const left = (a ?? "").trim().replace(/\/+$/, "");
|
|
770
|
-
const right = (b ?? "").trim().replace(/\/+$/, "");
|
|
771
|
-
if (!left || !right) return false;
|
|
772
|
-
if (left === right) return true;
|
|
773
|
-
const ln = left.match(/\/pull\/(\d+)/i)?.[1];
|
|
774
|
-
const rn = right.match(/\/pull\/(\d+)/i)?.[1];
|
|
775
|
-
return Boolean(ln && rn && ln === rn);
|
|
776
|
-
}
|
|
777
|
-
function shouldStopRunOnPrRetarget(previousPrUrl, nextPrUrl) {
|
|
778
|
-
const prev = (previousPrUrl ?? "").trim();
|
|
779
|
-
const next = (nextPrUrl ?? "").trim();
|
|
780
|
-
if (!prev || !next) return false;
|
|
781
|
-
return !samePrIdentity(prev, next);
|
|
782
|
-
}
|
|
783
|
-
function shouldPersistFetchedPrMeta(opts) {
|
|
784
|
-
const meta = opts.metaUrl?.trim() || "";
|
|
785
|
-
if (!meta) return false;
|
|
786
|
-
const head = opts.headPrUrl?.trim() || "";
|
|
787
|
-
if (head) return samePrIdentity(meta, head);
|
|
788
|
-
const live = opts.livePrUrl?.trim() || "";
|
|
789
|
-
if (live) return samePrIdentity(meta, live);
|
|
790
|
-
return true;
|
|
791
|
-
}
|
|
792
|
-
function sameLoginList(a, b) {
|
|
793
|
-
const left = [...a ?? []].map((s) => s.trim().toLowerCase()).filter(Boolean).sort();
|
|
794
|
-
const right = [...b ?? []].map((s) => s.trim().toLowerCase()).filter(Boolean).sort();
|
|
795
|
-
return left.length === right.length && left.every((login, i) => login === right[i]);
|
|
796
|
-
}
|
|
797
|
-
function threadPrMetaPatch(thread, meta) {
|
|
798
|
-
const prevState = normalizePrState(thread.prState);
|
|
799
|
-
const nextState = normalizePrState(meta.state);
|
|
800
|
-
const patch = {};
|
|
801
|
-
if (meta.url && meta.url !== thread.prUrl) patch.prUrl = meta.url;
|
|
802
|
-
if (meta.title && meta.title !== thread.prTitle) patch.prTitle = meta.title;
|
|
803
|
-
if (nextState && nextState !== prevState) patch.prState = nextState;
|
|
804
|
-
const nextDraft = Boolean(meta.isDraft) && nextState !== "MERGED" && nextState !== "CLOSED";
|
|
805
|
-
if (nextDraft !== Boolean(thread.prIsDraft)) patch.prIsDraft = nextDraft;
|
|
806
|
-
const nextAuthor = meta.authorLogin?.trim() || "";
|
|
807
|
-
if (nextAuthor && nextAuthor !== (thread.prAuthorLogin ?? "").trim()) {
|
|
808
|
-
patch.prAuthorLogin = nextAuthor;
|
|
809
|
-
}
|
|
810
|
-
const nextReviewers = meta.reviewerLogins ?? [];
|
|
811
|
-
if (!sameLoginList(nextReviewers, thread.prReviewerLogins)) {
|
|
812
|
-
patch.prReviewerLogins = nextReviewers;
|
|
813
|
-
}
|
|
814
|
-
const nextDecision = meta.reviewDecision ?? null;
|
|
815
|
-
if ((thread.prReviewDecision ?? null) !== nextDecision) {
|
|
816
|
-
patch.prReviewDecision = nextDecision;
|
|
817
|
-
}
|
|
818
|
-
const nextMergeable = meta.mergeable ?? null;
|
|
819
|
-
if ((thread.prMergeable ?? null) !== nextMergeable) {
|
|
820
|
-
patch.prMergeable = nextMergeable;
|
|
821
|
-
}
|
|
822
|
-
const nextMergeState = meta.mergeStateStatus ?? null;
|
|
823
|
-
if ((thread.prMergeStateStatus ?? null) !== nextMergeState) {
|
|
824
|
-
patch.prMergeStateStatus = nextMergeState;
|
|
825
|
-
}
|
|
826
|
-
const nextQueued = Boolean(meta.isInMergeQueue);
|
|
827
|
-
if (Boolean(thread.prIsInMergeQueue) !== nextQueued) {
|
|
828
|
-
patch.prIsInMergeQueue = nextQueued;
|
|
829
|
-
}
|
|
830
|
-
const nextLabels = meta.labels ?? [];
|
|
831
|
-
if (!sameLoginList(nextLabels, thread.prLabels)) {
|
|
832
|
-
patch.prLabels = nextLabels;
|
|
833
|
-
}
|
|
834
|
-
const mergeState = (nextMergeState ?? "").toUpperCase();
|
|
835
|
-
if (mergeState === "CLEAN" && thread.prChecksFailed) {
|
|
836
|
-
patch.prChecksFailed = false;
|
|
837
|
-
} else if ((mergeState === "UNSTABLE" || mergeState === "BLOCKED" && (nextDecision ?? "").toUpperCase() === "APPROVED") && !thread.prChecksFailed) {
|
|
838
|
-
patch.prChecksFailed = true;
|
|
839
|
-
}
|
|
840
|
-
if (thread.skipAutoArchiveOnMerge && nextState && nextState !== "MERGED" && nextState !== "CLOSED") {
|
|
841
|
-
patch.skipAutoArchiveOnMerge = false;
|
|
842
|
-
}
|
|
843
|
-
return patch;
|
|
844
|
-
}
|
|
845
|
-
function threadPrChecksFailedPatch(thread, failed) {
|
|
846
|
-
if (thread.prChecksFailed === failed) return {};
|
|
847
|
-
return { prChecksFailed: failed };
|
|
848
|
-
}
|
|
849
|
-
function shouldAutoArchiveOnPrMerge(opts) {
|
|
850
|
-
if (!opts.autoArchiveEnabled) return false;
|
|
851
|
-
if (opts.isGlobal) return false;
|
|
852
|
-
if (opts.skipAutoArchiveOnMerge) return false;
|
|
853
|
-
if (opts.threadStatus === "archived") return false;
|
|
854
|
-
const next = normalizePrState(opts.nextPrState);
|
|
855
|
-
if (next !== "MERGED") return false;
|
|
856
|
-
return normalizePrState(opts.previousPrState) !== "MERGED";
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
// src/hook/conductor.ts
|
|
860
|
-
import {
|
|
861
|
-
copyFileSync,
|
|
862
|
-
existsSync as existsSync4,
|
|
863
|
-
mkdirSync as mkdirSync5,
|
|
864
|
-
readdirSync as readdirSync2,
|
|
865
|
-
readFileSync as readFileSync4,
|
|
866
|
-
realpathSync
|
|
867
|
-
} from "fs";
|
|
868
|
-
import { execFileSync } from "child_process";
|
|
869
|
-
import { createServer } from "net";
|
|
870
|
-
import { basename, dirname as dirname4, join as join6, resolve, sep } from "path";
|
|
871
|
-
import { execa as execa2 } from "execa";
|
|
872
|
-
import { createInterface as createInterface2 } from "readline";
|
|
873
|
-
|
|
874
|
-
// src/review/request-review.ts
|
|
875
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
876
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
877
|
-
import { dirname, join as join3 } from "path";
|
|
878
|
-
|
|
879
|
-
// src/threads/chat-tabs.ts
|
|
880
|
-
import { randomUUID } from "crypto";
|
|
881
|
-
|
|
882
|
-
// src/composer/context-estimate.ts
|
|
883
|
-
var CHARS_PER_CONTEXT_TOKEN = 4;
|
|
884
|
-
function estimateMessageChars(message) {
|
|
885
|
-
let n = message.text.length + 16;
|
|
886
|
-
for (const part of message.parts ?? []) {
|
|
887
|
-
n += estimatePartChars(part);
|
|
888
|
-
}
|
|
889
|
-
return n;
|
|
890
|
-
}
|
|
891
|
-
function estimatePartChars(part) {
|
|
892
|
-
switch (part.type) {
|
|
893
|
-
case "text":
|
|
894
|
-
case "thinking":
|
|
895
|
-
return part.text.length;
|
|
896
|
-
case "tool": {
|
|
897
|
-
const input = part.input ? JSON.stringify(part.input) : "";
|
|
898
|
-
return part.name.length + (part.description?.length ?? 0) + (part.detail?.length ?? 0) + (part.result?.length ?? 0) + input.length + 32;
|
|
899
|
-
}
|
|
900
|
-
default:
|
|
901
|
-
return 0;
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
function estimateThreadChars(messages) {
|
|
905
|
-
return messages.reduce((sum, m) => sum + estimateMessageChars(m), 0);
|
|
906
|
-
}
|
|
907
|
-
function estimateOccupancyTokens(messages) {
|
|
908
|
-
return Math.ceil(estimateThreadChars(messages) / CHARS_PER_CONTEXT_TOKEN);
|
|
909
|
-
}
|
|
910
|
-
function threadHasCompactedContext(messages) {
|
|
911
|
-
return messages.some((m) => m.role === "summary");
|
|
912
|
-
}
|
|
913
|
-
function forwardOccupancyTokens(messages, usage, windowTokens = 1e6) {
|
|
914
|
-
const estimated = estimateOccupancyTokens(messages);
|
|
915
|
-
if (threadHasCompactedContext(messages) && estimated > 0) return estimated;
|
|
916
|
-
if (usage) {
|
|
917
|
-
const last = contextTokens(usage);
|
|
918
|
-
if (last > 0 && last <= windowTokens) return last;
|
|
919
|
-
}
|
|
920
|
-
return estimated;
|
|
921
|
-
}
|
|
922
|
-
function forwardContextUsage(usage, messages) {
|
|
923
|
-
if (!usage) return null;
|
|
924
|
-
if (!threadHasCompactedContext(messages)) return usage;
|
|
925
|
-
const remaining = estimateOccupancyTokens(messages);
|
|
926
|
-
const current = contextTokens(usage);
|
|
927
|
-
if (remaining <= 0 || remaining >= current) return usage;
|
|
928
|
-
return { ...usage, lastRequestTokens: remaining };
|
|
929
|
-
}
|
|
930
|
-
function applyForwardOccupancy(messages) {
|
|
931
|
-
const tokens = estimateOccupancyTokens(messages);
|
|
932
|
-
if (tokens <= 0) return messages;
|
|
933
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
934
|
-
const m = messages[i];
|
|
935
|
-
if (m?.role !== "agent" || !m.usage) continue;
|
|
936
|
-
const current = contextTokens(m.usage);
|
|
937
|
-
if (tokens >= current) return messages;
|
|
938
|
-
const next = messages.slice();
|
|
939
|
-
next[i] = { ...m, usage: { ...m.usage, lastRequestTokens: tokens } };
|
|
940
|
-
return next;
|
|
941
|
-
}
|
|
942
|
-
return messages;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
// src/composer/summarize.ts
|
|
946
|
-
var MAX_TRANSCRIPT_CHARS = 12e4;
|
|
947
|
-
var MAX_SUMMARY_CHARS = 6e3;
|
|
948
|
-
var CLAUDE_SUMMARY_TIMEOUT_MS = 2e4;
|
|
949
|
-
async function summarizeConversation(transcript, opts) {
|
|
950
|
-
const clipped = transcript.length > MAX_TRANSCRIPT_CHARS ? `${transcript.slice(0, MAX_TRANSCRIPT_CHARS)}
|
|
951
|
-
|
|
952
|
-
[\u2026truncated\u2026]` : transcript;
|
|
953
|
-
const claudeSummary = await tryClaudeSummary(clipped, opts);
|
|
954
|
-
if (claudeSummary) {
|
|
955
|
-
return { summary: clipSummary(claudeSummary), method: "claude" };
|
|
956
|
-
}
|
|
957
|
-
return { summary: clipSummary(extractiveSummary(clipped)), method: "extractive" };
|
|
958
|
-
}
|
|
959
|
-
async function tryClaudeSummary(transcript, opts) {
|
|
960
|
-
ensureAgentPath();
|
|
961
|
-
const prompt = [
|
|
962
|
-
"Summarize this coding-agent conversation for continuity in a fresh session.",
|
|
963
|
-
"Use concise markdown bullets covering:",
|
|
964
|
-
"- Goal / request",
|
|
965
|
-
"- Key decisions and constraints",
|
|
966
|
-
"- Files / areas touched",
|
|
967
|
-
"- Current state and open tasks",
|
|
968
|
-
"- Anything the next turn must remember",
|
|
969
|
-
"Do not invent details. No preamble.",
|
|
970
|
-
"",
|
|
971
|
-
"Transcript:",
|
|
972
|
-
transcript
|
|
973
|
-
].join("\n");
|
|
974
|
-
try {
|
|
975
|
-
const { stdout, exitCode } = await run(
|
|
976
|
-
"claude",
|
|
977
|
-
[
|
|
978
|
-
"-p",
|
|
979
|
-
prompt,
|
|
980
|
-
"--output-format",
|
|
981
|
-
"text",
|
|
982
|
-
"--permission-mode",
|
|
983
|
-
"bypassPermissions",
|
|
984
|
-
"--model",
|
|
985
|
-
"haiku"
|
|
986
|
-
],
|
|
987
|
-
{
|
|
988
|
-
cwd: opts?.cwd,
|
|
989
|
-
reject: false,
|
|
990
|
-
timeoutMs: opts?.timeoutMs ?? CLAUDE_SUMMARY_TIMEOUT_MS
|
|
991
|
-
}
|
|
992
|
-
);
|
|
993
|
-
if (exitCode !== 0) return null;
|
|
994
|
-
const text = stdout.trim();
|
|
995
|
-
if (text.length < 40) return null;
|
|
996
|
-
return text;
|
|
997
|
-
} catch {
|
|
998
|
-
return null;
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
function extractiveSummary(transcript) {
|
|
1002
|
-
const lines = transcript.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1003
|
-
const users = [];
|
|
1004
|
-
const tools = [];
|
|
1005
|
-
const agents = [];
|
|
1006
|
-
let mode = "none";
|
|
1007
|
-
for (const line of lines) {
|
|
1008
|
-
if (line.startsWith("### User")) {
|
|
1009
|
-
mode = "user";
|
|
1010
|
-
continue;
|
|
1011
|
-
}
|
|
1012
|
-
if (line.startsWith("### Agent") || line.startsWith("## Prior")) {
|
|
1013
|
-
mode = "agent";
|
|
1014
|
-
continue;
|
|
1015
|
-
}
|
|
1016
|
-
if (line.startsWith("- ") && line.includes(":")) {
|
|
1017
|
-
tools.push(line.replace(/^- /, ""));
|
|
1018
|
-
continue;
|
|
1019
|
-
}
|
|
1020
|
-
if (mode === "user" && users.length < 8) {
|
|
1021
|
-
users.push(line.slice(0, 240));
|
|
1022
|
-
} else if (mode === "agent" && agents.length < 6) {
|
|
1023
|
-
agents.push(line.slice(0, 240));
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
const uniqueTools = [...new Set(tools)].slice(0, 20);
|
|
1027
|
-
const parts = [
|
|
1028
|
-
"## Compacted context (extractive)",
|
|
1029
|
-
"",
|
|
1030
|
-
"### User goals",
|
|
1031
|
-
...users.length ? users.map((u) => `- ${u}`) : ["- (none captured)"],
|
|
1032
|
-
"",
|
|
1033
|
-
"### Recent agent notes",
|
|
1034
|
-
...agents.length ? agents.map((a) => `- ${a}`) : ["- (none captured)"]
|
|
1035
|
-
];
|
|
1036
|
-
if (uniqueTools.length) {
|
|
1037
|
-
parts.push("", "### Tools / files", ...uniqueTools.map((t) => `- ${t}`));
|
|
1038
|
-
}
|
|
1039
|
-
parts.push(
|
|
1040
|
-
"",
|
|
1041
|
-
"_Automatic extractive summary \u2014 Claude summarizer was unavailable._"
|
|
1042
|
-
);
|
|
1043
|
-
return parts.join("\n");
|
|
1044
|
-
}
|
|
1045
|
-
function clipSummary(text) {
|
|
1046
|
-
if (text.length <= MAX_SUMMARY_CHARS) return text;
|
|
1047
|
-
return `${text.slice(0, MAX_SUMMARY_CHARS)}
|
|
1048
|
-
|
|
1049
|
-
[\u2026summary truncated\u2026]`;
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
// src/composer/context-compact.ts
|
|
1053
|
-
var CONTEXT_COMPACT_CHARS = 4e5;
|
|
1054
|
-
var CONTEXT_KEEP_RECENT_CHARS = 24e3;
|
|
1055
|
-
var CONTEXT_KEEP_RECENT_MESSAGES = 12;
|
|
1056
|
-
var CONTEXT_MIN_MESSAGES = 10;
|
|
1057
|
-
var SESSION_RESET_OCCUPANCY_TOKENS = 75e4;
|
|
1058
|
-
function shouldCompactContext(messages, thresholds = {}) {
|
|
1059
|
-
const maxChars = thresholds.maxChars ?? CONTEXT_COMPACT_CHARS;
|
|
1060
|
-
const minMessages = thresholds.minMessages ?? CONTEXT_MIN_MESSAGES;
|
|
1061
|
-
if (messages.length < minMessages) return false;
|
|
1062
|
-
return estimateThreadChars(messages) >= maxChars;
|
|
1063
|
-
}
|
|
1064
|
-
function splitForCompaction(messages, thresholds = {}) {
|
|
1065
|
-
const keepChars = thresholds.keepRecentChars ?? CONTEXT_KEEP_RECENT_CHARS;
|
|
1066
|
-
const keepCount = thresholds.keepRecentMessages ?? CONTEXT_KEEP_RECENT_MESSAGES;
|
|
1067
|
-
if (messages.length === 0) return { older: [], recent: [] };
|
|
1068
|
-
let recentChars = 0;
|
|
1069
|
-
let cut = messages.length;
|
|
1070
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1071
|
-
const next = recentChars + estimateMessageChars(messages[i]);
|
|
1072
|
-
const kept = messages.length - i;
|
|
1073
|
-
if (kept >= keepCount && next > keepChars) break;
|
|
1074
|
-
recentChars = next;
|
|
1075
|
-
cut = i;
|
|
1076
|
-
}
|
|
1077
|
-
const minRecent = Math.min(keepCount, messages.length);
|
|
1078
|
-
cut = Math.min(cut, messages.length - minRecent);
|
|
1079
|
-
if (cut <= 0) return { older: [], recent: messages };
|
|
1080
|
-
return {
|
|
1081
|
-
older: messages.slice(0, cut),
|
|
1082
|
-
recent: messages.slice(cut)
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
|
-
function formatToolPart(t, detail) {
|
|
1086
|
-
if (detail === "summary") {
|
|
1087
|
-
const label = t.description || t.detail || t.name;
|
|
1088
|
-
const path = t.filePath ? ` (${t.filePath})` : "";
|
|
1089
|
-
return `- ${t.name}: ${label}${path}`;
|
|
1090
|
-
}
|
|
1091
|
-
const lines = [`#### Tool: ${t.name}`, `Status: ${t.status}`];
|
|
1092
|
-
if (t.description) lines.push(`Description: ${t.description}`);
|
|
1093
|
-
if (t.filePath) lines.push(`Path: ${t.filePath}`);
|
|
1094
|
-
if (t.input && Object.keys(t.input).length > 0) {
|
|
1095
|
-
lines.push("Input:");
|
|
1096
|
-
lines.push("```json");
|
|
1097
|
-
lines.push(clipToolResultForStore(JSON.stringify(t.input, null, 2)) ?? "");
|
|
1098
|
-
lines.push("```");
|
|
1099
|
-
} else if (t.detail) {
|
|
1100
|
-
lines.push(`Detail: ${t.detail}`);
|
|
1101
|
-
}
|
|
1102
|
-
if (t.result != null && t.result !== "") {
|
|
1103
|
-
lines.push("Result:");
|
|
1104
|
-
lines.push("```");
|
|
1105
|
-
lines.push(t.result);
|
|
1106
|
-
lines.push("```");
|
|
1107
|
-
}
|
|
1108
|
-
return lines.join("\n");
|
|
1109
|
-
}
|
|
1110
|
-
function formatMessagesAsTranscript(messages, opts) {
|
|
1111
|
-
const tools = opts?.tools ?? "full";
|
|
1112
|
-
const thinking = opts?.thinking ?? true;
|
|
1113
|
-
return messages.map((m) => formatMessageBlock(m, { tools, thinking })).join("\n\n");
|
|
1114
|
-
}
|
|
1115
|
-
function formatMessageBlock(m, opts) {
|
|
1116
|
-
if (m.role === "summary") return `## Prior summary
|
|
1117
|
-
${m.text}`;
|
|
1118
|
-
if (m.role === "user") return `### User
|
|
1119
|
-
${m.text}`;
|
|
1120
|
-
const bits = [];
|
|
1121
|
-
if (m.text?.trim()) {
|
|
1122
|
-
bits.push(`### Agent
|
|
1123
|
-
${m.text}`);
|
|
1124
|
-
} else {
|
|
1125
|
-
bits.push("### Agent");
|
|
1126
|
-
}
|
|
1127
|
-
if (opts.thinking) {
|
|
1128
|
-
for (const th of m.parts ?? []) {
|
|
1129
|
-
if (th.type !== "thinking" || !th.text.trim()) continue;
|
|
1130
|
-
bits.push("Thinking:");
|
|
1131
|
-
bits.push(th.text);
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
const toolParts = (m.parts ?? []).filter(
|
|
1135
|
-
(p) => p.type === "tool"
|
|
1136
|
-
);
|
|
1137
|
-
if (opts.tools !== "none" && toolParts.length > 0) {
|
|
1138
|
-
if (opts.tools === "summary") bits.push("Tools:");
|
|
1139
|
-
for (const t of toolParts) {
|
|
1140
|
-
bits.push(formatToolPart(t, opts.tools));
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
return bits.join("\n");
|
|
1144
|
-
}
|
|
1145
|
-
var SEED_MAX_CHARS = 6e4;
|
|
1146
|
-
var SEED_FULL_TOOL_MESSAGES = 6;
|
|
1147
|
-
function budgetSeedBlocks(messages, blocks, maxChars) {
|
|
1148
|
-
const keep = new Array(blocks.length).fill(false);
|
|
1149
|
-
let used = 0;
|
|
1150
|
-
for (let i = 0; i < blocks.length; i++) {
|
|
1151
|
-
if (messages[i].role === "summary") {
|
|
1152
|
-
keep[i] = true;
|
|
1153
|
-
used += blocks[i].length + 2;
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
1157
|
-
if (keep[i]) continue;
|
|
1158
|
-
const next = used + blocks[i].length + 2;
|
|
1159
|
-
if (next > maxChars && used > 0) continue;
|
|
1160
|
-
keep[i] = true;
|
|
1161
|
-
used = next;
|
|
1162
|
-
}
|
|
1163
|
-
const dropped = keep.filter((k) => !k).length;
|
|
1164
|
-
const marker = `_(${dropped} older message${dropped === 1 ? "" : "s"} omitted for length)_`;
|
|
1165
|
-
const kept = [];
|
|
1166
|
-
let markerPlaced = false;
|
|
1167
|
-
for (let i = 0; i < blocks.length; i++) {
|
|
1168
|
-
if (!keep[i]) continue;
|
|
1169
|
-
if (dropped > 0 && !markerPlaced && messages[i].role !== "summary") {
|
|
1170
|
-
kept.push(marker);
|
|
1171
|
-
markerPlaced = true;
|
|
1172
|
-
}
|
|
1173
|
-
kept.push(blocks[i]);
|
|
1174
|
-
}
|
|
1175
|
-
if (dropped > 0 && !markerPlaced) kept.push(marker);
|
|
1176
|
-
return kept.join("\n\n");
|
|
1177
|
-
}
|
|
1178
|
-
function buildSessionSeed(messages, opts) {
|
|
1179
|
-
if (messages.length === 0) return null;
|
|
1180
|
-
const tools = opts?.tools ?? "full";
|
|
1181
|
-
const fullCount = opts?.fullToolMessages ?? SEED_FULL_TOOL_MESSAGES;
|
|
1182
|
-
const maxChars = opts?.maxChars ?? SEED_MAX_CHARS;
|
|
1183
|
-
const olderTools = tools === "none" ? "none" : "summary";
|
|
1184
|
-
const fullFrom = Math.max(0, messages.length - fullCount);
|
|
1185
|
-
const blocks = messages.map(
|
|
1186
|
-
(m, i) => formatMessageBlock(m, {
|
|
1187
|
-
tools: i >= fullFrom ? tools : olderTools,
|
|
1188
|
-
thinking: false
|
|
1189
|
-
})
|
|
1190
|
-
);
|
|
1191
|
-
const body = budgetSeedBlocks(messages, blocks, maxChars);
|
|
1192
|
-
if (!body.trim()) return null;
|
|
1193
|
-
return [
|
|
1194
|
-
"Sideboard conversation context (restored after compaction or a new session):",
|
|
1195
|
-
"",
|
|
1196
|
-
body,
|
|
1197
|
-
"",
|
|
1198
|
-
"Continue from this context. Do not repeat the summary unless asked."
|
|
1199
|
-
].join("\n");
|
|
1200
|
-
}
|
|
1201
|
-
var BRIGHTSY_SUMMARIZE_CONTEXT_TOOL = "summarize_context";
|
|
1202
|
-
function extractBrightsyContextSummary(result) {
|
|
1203
|
-
if (!result?.trim()) return null;
|
|
1204
|
-
const trimmed = result.trim();
|
|
1205
|
-
const lower = trimmed.toLowerCase();
|
|
1206
|
-
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")) {
|
|
1207
|
-
return null;
|
|
1208
|
-
}
|
|
1209
|
-
try {
|
|
1210
|
-
const parsed = JSON.parse(trimmed);
|
|
1211
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1212
|
-
if (parsed.error != null && typeof parsed.context_summary !== "string") {
|
|
1213
|
-
return null;
|
|
1214
|
-
}
|
|
1215
|
-
if (typeof parsed.context_summary === "string" && parsed.context_summary.trim()) {
|
|
1216
|
-
return parsed.context_summary.trim();
|
|
1217
|
-
}
|
|
1218
|
-
return null;
|
|
1219
|
-
}
|
|
1220
|
-
} catch {
|
|
1221
|
-
}
|
|
1222
|
-
if (trimmed.includes('"error"') && !trimmed.includes("context_summary")) {
|
|
1223
|
-
return null;
|
|
1224
|
-
}
|
|
1225
|
-
return trimmed;
|
|
1226
|
-
}
|
|
1227
|
-
function findLastBrightsyContextSummary(messages) {
|
|
1228
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1229
|
-
const message = messages[i];
|
|
1230
|
-
if (message?.role !== "agent") continue;
|
|
1231
|
-
for (const part of message.parts ?? []) {
|
|
1232
|
-
if (part.type !== "tool" || part.name !== BRIGHTSY_SUMMARIZE_CONTEXT_TOOL) {
|
|
1233
|
-
continue;
|
|
1234
|
-
}
|
|
1235
|
-
if (part.status === "error") continue;
|
|
1236
|
-
const text = extractBrightsyContextSummary(part.result);
|
|
1237
|
-
if (text) return { index: i, text };
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
return null;
|
|
1241
|
-
}
|
|
1242
|
-
function messagesSinceLastBrightsyContextSummary(messages) {
|
|
1243
|
-
const match = findLastBrightsyContextSummary(messages);
|
|
1244
|
-
if (!match) return messages;
|
|
1245
|
-
return messages.slice(match.index + 1);
|
|
1246
|
-
}
|
|
1247
|
-
function buildBrightsySessionSeed(messages, opts) {
|
|
1248
|
-
const match = findLastBrightsyContextSummary(messages);
|
|
1249
|
-
const tail = match ? messages.slice(match.index + 1) : messages;
|
|
1250
|
-
const budget = Math.max(1e3, (opts?.maxChars ?? SEED_MAX_CHARS) - (match?.text.length ?? 0));
|
|
1251
|
-
const tailBlocks = tail.map((m) => formatMessageBlock(m, { tools: "none", thinking: false }));
|
|
1252
|
-
const body = budgetSeedBlocks(tail, tailBlocks, budget);
|
|
1253
|
-
if (!match && !body.trim()) return null;
|
|
1254
|
-
const blocks = [
|
|
1255
|
-
"Sideboard conversation context (restored after compaction or a new session):",
|
|
1256
|
-
""
|
|
1257
|
-
];
|
|
1258
|
-
if (match) {
|
|
1259
|
-
blocks.push(`## Prior summary
|
|
1260
|
-
${match.text}`, "");
|
|
1261
|
-
}
|
|
1262
|
-
if (body.trim()) {
|
|
1263
|
-
blocks.push(body, "");
|
|
1264
|
-
}
|
|
1265
|
-
blocks.push("Continue from this context. Do not repeat the summary unless asked.");
|
|
1266
|
-
return blocks.join("\n");
|
|
1267
|
-
}
|
|
1268
|
-
function compactArchiveProse(message) {
|
|
1269
|
-
if (message.origin === "continue") return null;
|
|
1270
|
-
if (message.role === "summary") return null;
|
|
1271
|
-
let text = (message.text ?? "").trim();
|
|
1272
|
-
if (!text && message.role === "agent") {
|
|
1273
|
-
text = lastAssistantMessageText(message.parts).trim();
|
|
1274
|
-
}
|
|
1275
|
-
if (!text || isInternalAgentStatusText(text)) return null;
|
|
1276
|
-
return { role: message.role, text, ts: message.ts };
|
|
1277
|
-
}
|
|
1278
|
-
function messagesForCompactArchive(older) {
|
|
1279
|
-
const out = [];
|
|
1280
|
-
for (const message of older) {
|
|
1281
|
-
const prose = compactArchiveProse(message);
|
|
1282
|
-
if (prose) out.push(prose);
|
|
1283
|
-
}
|
|
1284
|
-
return out;
|
|
1285
|
-
}
|
|
1286
|
-
function appendCompactedMessages(existing, older) {
|
|
1287
|
-
const next = messagesForCompactArchive(older);
|
|
1288
|
-
if (!existing?.length) return next;
|
|
1289
|
-
if (!next.length) return existing;
|
|
1290
|
-
return [...existing, ...next];
|
|
1291
|
-
}
|
|
1292
|
-
function applyCompaction(messages, summaryText, thresholds = {}) {
|
|
1293
|
-
const { older, recent } = splitForCompaction(messages, thresholds);
|
|
1294
|
-
if (older.length === 0) return messages;
|
|
1295
|
-
const text = summaryText.trim();
|
|
1296
|
-
const summary = {
|
|
1297
|
-
role: "summary",
|
|
1298
|
-
text,
|
|
1299
|
-
parts: [compactSummaryPart(text)],
|
|
1300
|
-
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
1301
|
-
};
|
|
1302
|
-
return [summary, ...recent];
|
|
1303
|
-
}
|
|
1304
|
-
function lastRequestOccupancy(thread) {
|
|
1305
|
-
for (let i = thread.messages.length - 1; i >= 0; i--) {
|
|
1306
|
-
const usage = thread.messages[i]?.usage;
|
|
1307
|
-
if (usage) return contextTokens(usage);
|
|
1308
|
-
}
|
|
1309
|
-
return 0;
|
|
1310
|
-
}
|
|
1311
|
-
function shouldResetSessionForOccupancy(thread, occupancyTokens = SESSION_RESET_OCCUPANCY_TOKENS) {
|
|
1312
|
-
return lastRequestOccupancy(thread) >= occupancyTokens;
|
|
1313
|
-
}
|
|
1314
|
-
async function maybeCompactContext(thread, thresholds = {}, summarize = summarizeConversation) {
|
|
1315
|
-
if (!shouldCompactContext(thread.messages, thresholds)) {
|
|
1316
|
-
return { didCompact: false, thread };
|
|
1317
|
-
}
|
|
1318
|
-
const { older } = splitForCompaction(thread.messages, thresholds);
|
|
1319
|
-
if (older.length === 0) {
|
|
1320
|
-
return { didCompact: false, thread };
|
|
1321
|
-
}
|
|
1322
|
-
const transcript = formatMessagesAsTranscript(older, { tools: "summary" });
|
|
1323
|
-
const { summary, method } = await summarize(transcript, {
|
|
1324
|
-
cwd: thread.worktreePath
|
|
1325
|
-
});
|
|
1326
|
-
let messages = applyCompaction(thread.messages, summary, thresholds);
|
|
1327
|
-
const compactedMessages = appendCompactedMessages(thread.compactedMessages, older);
|
|
1328
|
-
const resetSession = shouldResetSessionForOccupancy({ messages: thread.messages });
|
|
1329
|
-
if (resetSession) {
|
|
1330
|
-
messages = applyForwardOccupancy(messages);
|
|
1331
|
-
}
|
|
1332
|
-
const next = {
|
|
1333
|
-
...thread,
|
|
1334
|
-
messages,
|
|
1335
|
-
...compactedMessages.length ? { compactedMessages } : { compactedMessages: void 0 },
|
|
1336
|
-
sessionId: resetSession ? null : thread.sessionId,
|
|
1337
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1338
|
-
};
|
|
1339
|
-
return {
|
|
1340
|
-
didCompact: true,
|
|
1341
|
-
thread: next,
|
|
1342
|
-
summary,
|
|
1343
|
-
method,
|
|
1344
|
-
olderCount: older.length
|
|
1345
|
-
};
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
// src/threads/worktree-runtime.ts
|
|
1349
|
-
function worktreeProcessPrefix(worktreePath) {
|
|
1350
|
-
return `wt:${normalizeWorktreePath(worktreePath)}`;
|
|
1351
|
-
}
|
|
1352
|
-
function worktreeSetupProcessKey(worktreePath) {
|
|
1353
|
-
return `${worktreeProcessPrefix(worktreePath)}:setup`;
|
|
1354
|
-
}
|
|
1355
|
-
function worktreeRunProcessKey(worktreePath, scriptName) {
|
|
1356
|
-
return `${worktreeProcessPrefix(worktreePath)}:run:${scriptName}`;
|
|
1357
|
-
}
|
|
1358
|
-
function worktreeDevProcessKey(worktreePath) {
|
|
1359
|
-
return `${worktreeProcessPrefix(worktreePath)}:dev`;
|
|
1360
|
-
}
|
|
1361
|
-
function isWorktreeRunProcessKey(key, worktreePath) {
|
|
1362
|
-
const prefix = worktreeProcessPrefix(worktreePath);
|
|
1363
|
-
return key.startsWith(`${prefix}:run:`) || key === `${prefix}:dev`;
|
|
1364
|
-
}
|
|
1365
|
-
function pickRichestSetupLog(snaps) {
|
|
1366
|
-
let best;
|
|
1367
|
-
for (const snap of snaps) {
|
|
1368
|
-
if (!snap.output && !snap.running && snap.exitCode == null) continue;
|
|
1369
|
-
if (!best) {
|
|
1370
|
-
best = snap;
|
|
1371
|
-
continue;
|
|
1372
|
-
}
|
|
1373
|
-
if (snap.running && !best.running) {
|
|
1374
|
-
best = snap;
|
|
1375
|
-
continue;
|
|
1376
|
-
}
|
|
1377
|
-
if (snap.running === best.running && snap.output.length > best.output.length) {
|
|
1378
|
-
best = snap;
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
return best;
|
|
1382
|
-
}
|
|
1383
|
-
function resolveWorktreeSetupLog(shared, legacyChatLogs) {
|
|
1384
|
-
if (shared.output || shared.running || shared.exitCode != null) return shared;
|
|
1385
|
-
return pickRichestSetupLog(legacyChatLogs);
|
|
1386
|
-
}
|
|
1387
|
-
function mergeWorktreeActiveRuns(threads) {
|
|
1388
|
-
const byName = /* @__PURE__ */ new Map();
|
|
1389
|
-
let devPort = null;
|
|
1390
|
-
for (const thread of threads) {
|
|
1391
|
-
if (thread.devPort != null && devPort == null) devPort = thread.devPort;
|
|
1392
|
-
for (const run2 of thread.activeRuns ?? []) {
|
|
1393
|
-
if (!byName.has(run2.scriptName)) byName.set(run2.scriptName, run2);
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
return { activeRuns: [...byName.values()], devPort };
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
// src/threads/chat-tabs.ts
|
|
1400
|
-
function sameWorktreePath(a, b) {
|
|
1401
|
-
return normalizeWorktreePath(a) === normalizeWorktreePath(b);
|
|
1402
|
-
}
|
|
1403
|
-
function takenTeamSlugsForChatTab(worktreePath) {
|
|
1404
|
-
const key = normalizeWorktreePath(worktreePath);
|
|
1405
|
-
const taken = /* @__PURE__ */ new Set();
|
|
1406
|
-
const dir = worktreeNameFromPath(key);
|
|
1407
|
-
if (dir) taken.add(dir.toLowerCase());
|
|
1408
|
-
for (const sibling of threadsSharingWorktree(key)) {
|
|
1409
|
-
for (const slug of takenSlugsFromThread(sibling)) {
|
|
1410
|
-
taken.add(slug);
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
return [...taken];
|
|
1414
|
-
}
|
|
1415
|
-
function requireThread(idOrRef) {
|
|
1416
|
-
const thread = findThreadByRef(idOrRef) ?? null;
|
|
1417
|
-
if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
|
|
1418
|
-
return thread;
|
|
1419
|
-
}
|
|
1420
|
-
function worktreeBindingFrom(from) {
|
|
1421
|
-
return {
|
|
1422
|
-
worktreePath: normalizeWorktreePath(from.worktreePath),
|
|
1423
|
-
repoPath: from.repoPath,
|
|
1424
|
-
branchName: from.branchName,
|
|
1425
|
-
sourceRef: from.sourceRef,
|
|
1426
|
-
// Legacy pinned-repo orchestration: new tabs become normal worktree agents.
|
|
1427
|
-
// Global: keep orchestration so chat tabs stay fleet orchestrators (MCP + Bash).
|
|
1428
|
-
sourceType: isGlobalThread(from) ? "orchestration" : from.sourceType === "orchestration" ? "branch" : from.sourceType,
|
|
1429
|
-
sourceIsFork: from.sourceIsFork,
|
|
1430
|
-
parentThreadId: from.parentThreadId,
|
|
1431
|
-
prUrl: from.prUrl,
|
|
1432
|
-
prTitle: from.prTitle,
|
|
1433
|
-
prState: from.prState,
|
|
1434
|
-
prIsDraft: from.prIsDraft,
|
|
1435
|
-
prAuthorLogin: from.prAuthorLogin,
|
|
1436
|
-
prReviewerLogins: from.prReviewerLogins,
|
|
1437
|
-
prReviewDecision: from.prReviewDecision,
|
|
1438
|
-
prMergeable: from.prMergeable,
|
|
1439
|
-
prMergeStateStatus: from.prMergeStateStatus,
|
|
1440
|
-
prIsInMergeQueue: from.prIsInMergeQueue,
|
|
1441
|
-
prLabels: from.prLabels,
|
|
1442
|
-
prChecksFailed: from.prChecksFailed,
|
|
1443
|
-
skipAutoArchiveOnMerge: from.skipAutoArchiveOnMerge,
|
|
1444
|
-
stackId: from.stackId,
|
|
1445
|
-
stackLayer: from.stackLayer,
|
|
1446
|
-
cowboy: from.cowboy
|
|
1447
|
-
};
|
|
1448
|
-
}
|
|
1449
|
-
function threadsSharingWorktree(worktreePath) {
|
|
1450
|
-
if (!worktreePath) return [];
|
|
1451
|
-
const key = normalizeWorktreePath(worktreePath);
|
|
1452
|
-
return listThreads({ includeArchived: true }).filter(
|
|
1453
|
-
(t) => Boolean(t.worktreePath) && sameWorktreePath(t.worktreePath, key) && t.status !== "archived"
|
|
1454
|
-
).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
1455
|
-
}
|
|
1456
|
-
function formatTranscriptMarkdown(title, messages) {
|
|
1457
|
-
const body = formatMessagesAsTranscript(messages, { tools: "full" });
|
|
1458
|
-
return [`# Transcript: ${title}`, "", `Forked ${(/* @__PURE__ */ new Date()).toISOString()}`, "", body].join("\n");
|
|
1459
|
-
}
|
|
1460
|
-
function forkMessageSlice(from, throughIndex) {
|
|
1461
|
-
const through = throughIndex === void 0 ? from.messages.length - 1 : Math.min(throughIndex, from.messages.length - 1);
|
|
1462
|
-
return through >= 0 ? from.messages.slice(0, through + 1) : [];
|
|
1463
|
-
}
|
|
1464
|
-
function buildForkTranscriptAttachment(baseTitle, messages) {
|
|
1465
|
-
const title = baseTitle || "Chat";
|
|
1466
|
-
return {
|
|
1467
|
-
id: randomUUID(),
|
|
1468
|
-
name: `Transcript of ${title}.md`,
|
|
1469
|
-
kind: "transcript",
|
|
1470
|
-
content: formatTranscriptMarkdown(title, messages)
|
|
1471
|
-
};
|
|
1472
|
-
}
|
|
1473
|
-
function createChatTab(input) {
|
|
1474
|
-
const from = requireThread(input.fromThreadId);
|
|
1475
|
-
const binding = worktreeBindingFrom(from);
|
|
1476
|
-
const explicitTitle = input.title?.trim();
|
|
1477
|
-
const title = explicitTitle || allocateTeamName(takenTeamSlugsForChatTab(binding.worktreePath)).name;
|
|
1478
|
-
const nextAgent = input.agent ?? from.agent;
|
|
1479
|
-
if (isOrchestratorThread(from) || binding.sourceType === "orchestration") {
|
|
1480
|
-
assertOrchestratorCapableAgent(nextAgent);
|
|
1481
|
-
}
|
|
1482
|
-
const singletonInbox = isSlackCoordinatorThread(from) || isCloudCoordinatorThread(from);
|
|
1483
|
-
const thread = createEmptyThread({
|
|
1484
|
-
title,
|
|
1485
|
-
// Chat-tab nicknames (soccer team or explicit) must stick. Post-turn
|
|
1486
|
-
// syncThreadBranchFromGit would otherwise rewrite every sibling to the
|
|
1487
|
-
// shared worktree folder name (e.g. fork "Arsenal" → "Monaco").
|
|
1488
|
-
userSetTitle: true,
|
|
1489
|
-
...binding,
|
|
1490
|
-
// Slack / Brightsy cloud identity stays on the original chat. A + tab
|
|
1491
|
-
// that copies `slack:team:user` would steal inbound DMs after you close
|
|
1492
|
-
// the connected chat.
|
|
1493
|
-
sourceRef: singletonInbox ? title : binding.sourceRef,
|
|
1494
|
-
agent: nextAgent,
|
|
1495
|
-
model: input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model,
|
|
1496
|
-
effort: input.effort !== void 0 ? input.effort : from.effort,
|
|
1497
|
-
fast: input.fast !== void 0 ? Boolean(input.fast) : from.fast,
|
|
1498
|
-
planMode: from.planMode,
|
|
1499
|
-
autonomy: input.autonomy ?? from.autonomy,
|
|
1500
|
-
attachments: input.attachments ?? [],
|
|
1501
|
-
status: "idle",
|
|
1502
|
-
...input.accountId !== void 0 ? { accountId: input.accountId } : {},
|
|
1503
|
-
...mergeWorktreeActiveRuns(threadsSharingWorktree(binding.worktreePath))
|
|
1504
|
-
});
|
|
1505
|
-
writeThread(thread);
|
|
1506
|
-
return thread;
|
|
1507
|
-
}
|
|
1508
|
-
function forkChatTab(input) {
|
|
1509
|
-
const from = requireThread(input.threadId);
|
|
1510
|
-
const slice = forkMessageSlice(from, input.throughIndex);
|
|
1511
|
-
const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
|
|
1512
|
-
const tab = createChatTab({
|
|
1513
|
-
fromThreadId: input.threadId,
|
|
1514
|
-
agent: input.agent ?? from.agent,
|
|
1515
|
-
model: input.model,
|
|
1516
|
-
title: input.title?.trim() || void 0,
|
|
1517
|
-
attachments: [attachment]
|
|
1518
|
-
});
|
|
1519
|
-
if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
|
|
1520
|
-
const next = { ...tab, parentThreadId: from.id };
|
|
1521
|
-
writeThread(next);
|
|
1522
|
-
return next;
|
|
1523
|
-
}
|
|
1524
|
-
return tab;
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
// src/review/review-write-gate.ts
|
|
1528
|
-
var REVIEW_TAB_TITLE = "Review";
|
|
1529
|
-
function hasLiveReviewSibling(thread, siblings = []) {
|
|
1530
|
-
const selfId = thread.id;
|
|
1531
|
-
return siblings.some(
|
|
1532
|
-
(t) => t.id !== selfId && t.status !== "archived" && t.title.trim() === REVIEW_TAB_TITLE
|
|
1533
|
-
);
|
|
1534
|
-
}
|
|
1535
|
-
function isReviewWriteGatedThread(thread, siblings) {
|
|
1536
|
-
if (thread.sourceType === "pr" || thread.title.trim() === REVIEW_TAB_TITLE) {
|
|
1537
|
-
return true;
|
|
1538
|
-
}
|
|
1539
|
-
return hasLiveReviewSibling(thread, siblings);
|
|
1540
|
-
}
|
|
1541
|
-
function formatReviewWriteGateDirective() {
|
|
1542
|
-
return [
|
|
1543
|
-
"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.",
|
|
1544
|
-
"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.",
|
|
1545
|
-
"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.",
|
|
1546
|
-
"Do not request reviewers, change labels, or assign yourself unless they asked \u2014 those also notify the author."
|
|
1547
|
-
].join("\n");
|
|
1548
|
-
}
|
|
1549
|
-
function formatReviewWriteGateReminder() {
|
|
1550
|
-
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.";
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
// src/review/review-request-template.ts
|
|
1554
|
-
var REVIEW_REQUEST_TEMPLATE = `# Review guidelines:
|
|
1555
|
-
|
|
1556
|
-
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.
|
|
1557
|
-
|
|
1558
|
-
## Required outcome
|
|
1559
|
-
|
|
1560
|
-
Start your reply with a **Recommendation** section using exactly one of:
|
|
1561
|
-
|
|
1562
|
-
- **Approve** \u2014 ready to merge as-is (or with only trivial nits the author can ignore).
|
|
1563
|
-
- **Approve with nits** \u2014 ready to merge; list only optional polish that should not block.
|
|
1564
|
-
- **Request changes** \u2014 not ready; blocking issues must be fixed first.
|
|
1565
|
-
- **Needs more information** \u2014 cannot judge readiness yet (missing context, incomplete diff, unclear intent).
|
|
1566
|
-
|
|
1567
|
-
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.
|
|
1568
|
-
|
|
1569
|
-
People running this review are asking \u201Ccan we ship this?\u201D Treat that as the question you answer first.
|
|
1570
|
-
|
|
1571
|
-
## Confirm before posting
|
|
1572
|
-
|
|
1573
|
-
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.
|
|
1574
|
-
|
|
1575
|
-
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.
|
|
1576
|
-
|
|
1577
|
-
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.
|
|
1578
|
-
|
|
1579
|
-
## Findings
|
|
1580
|
-
|
|
1581
|
-
Below are guidelines for determining whether an issue is worth flagging to the original author.
|
|
1582
|
-
|
|
1583
|
-
These are not the final word. More specific guidelines elsewhere (developer message, user message, a file, etc.) override these.
|
|
1584
|
-
|
|
1585
|
-
Flag something as a bug / blocking finding only when:
|
|
1586
|
-
|
|
1587
|
-
1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
|
|
1588
|
-
2. The bug is discrete and actionable (not a vague codebase-wide complaint or a bundle of unrelated issues).
|
|
1589
|
-
3. Fixing it does not demand rigor absent from the rest of the codebase.
|
|
1590
|
-
4. The issue was introduced by this change (do not flag pre-existing bugs unless they are newly exposed by this PR).
|
|
1591
|
-
5. The author would likely fix it if made aware.
|
|
1592
|
-
6. It does not rely on unstated assumptions about the codebase or author intent.
|
|
1593
|
-
7. Speculative breakage is not enough \u2014 identify the other code that is provably affected.
|
|
1594
|
-
8. It is clearly not just an intentional change by the author.
|
|
1595
|
-
|
|
1596
|
-
When flagging an issue, include a short accompanying comment:
|
|
1597
|
-
|
|
1598
|
-
1. Clear about why it is a problem.
|
|
1599
|
-
2. Severity must match reality \u2014 do not inflate.
|
|
1600
|
-
3. Brief: at most one paragraph; avoid unnecessary line breaks in prose.
|
|
1601
|
-
4. No code chunks longer than 3 lines; wrap code in inline ticks or a fenced block.
|
|
1602
|
-
5. Call out scenarios / environments / inputs needed to hit the bug when severity depends on them.
|
|
1603
|
-
6. Matter-of-fact tone \u2014 helpful assistant, not accusatory or effusive.
|
|
1604
|
-
7. Skimmable on first read.
|
|
1605
|
-
8. No empty flattery (\u201CGreat job\u2026\u201D, \u201CThanks for\u2026\u201D).
|
|
1606
|
-
|
|
1607
|
-
HOW MANY FINDINGS TO RETURN:
|
|
1608
|
-
|
|
1609
|
-
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.
|
|
1610
|
-
|
|
1611
|
-
GUIDELINES:
|
|
1612
|
-
|
|
1613
|
-
- Ignore trivial style unless it obscures meaning or violates documented standards.
|
|
1614
|
-
- One comment per distinct issue (or a short multi-line range if needed).
|
|
1615
|
-
- Use \`\`\`suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block).
|
|
1616
|
-
- In every \`\`\`suggestion block, preserve the exact leading whitespace of the replaced lines (spaces vs tabs, number of spaces).
|
|
1617
|
-
- Do NOT introduce or remove outer indentation levels unless that is the actual fix.
|
|
1618
|
-
- Separate **blocking** findings from **nits**. Only blocking findings should drive Request changes.
|
|
1619
|
-
|
|
1620
|
-
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).
|
|
1621
|
-
|
|
1622
|
-
## Getting the diff
|
|
1623
|
-
|
|
1624
|
-
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.
|
|
1625
|
-
|
|
1626
|
-
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.
|
|
1627
|
-
|
|
1628
|
-
## Fallback: if you don't have access to the Sideboard diff tool
|
|
1629
|
-
|
|
1630
|
-
If you don't have access to \`get_diff\`, use the following git commands to get the diff:
|
|
1631
|
-
|
|
1632
|
-
\`\`\`bash
|
|
1633
|
-
# Get the merge base between this branch and the target
|
|
1634
|
-
MERGE_BASE=$(git merge-base origin/main HEAD)
|
|
1635
|
-
|
|
1636
|
-
# Get the committed diff against the merge base
|
|
1637
|
-
git diff $MERGE_BASE HEAD
|
|
1638
|
-
|
|
1639
|
-
# Get any uncommitted changes (staged and unstaged)
|
|
1640
|
-
git diff HEAD
|
|
1641
|
-
\`\`\`
|
|
1642
|
-
|
|
1643
|
-
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.
|
|
1644
|
-
|
|
1645
|
-
No need to mention in your report whether or not you used one of the fallback strategies; it's usually irrelevant.
|
|
1646
|
-
|
|
1647
|
-
## Output format
|
|
1648
|
-
|
|
1649
|
-
**1. Recommendation first** (required), then **2. Findings** (may be empty).
|
|
1650
|
-
|
|
1651
|
-
Only report ONE finding per unique issue.
|
|
1652
|
-
|
|
1653
|
-
<example>
|
|
1654
|
-
## Recommendation
|
|
1655
|
-
|
|
1656
|
-
**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.
|
|
1657
|
-
|
|
1658
|
-
## Findings
|
|
1659
|
-
|
|
1660
|
-
### **#1 Empty input causes crash** (blocking)
|
|
1661
|
-
|
|
1662
|
-
If the input field is empty when the page loads, the app will crash.
|
|
1663
|
-
|
|
1664
|
-
File: src/client/frontends/desktop/ui/Input.tsx
|
|
1665
|
-
|
|
1666
|
-
### **#2 Dead code** (nit)
|
|
1667
|
-
|
|
1668
|
-
The getUserData function is now unused. It should be deleted.
|
|
1669
|
-
|
|
1670
|
-
File: src/client/frontends/desktop/core/UserData.ts
|
|
1671
|
-
</example>
|
|
1672
|
-
|
|
1673
|
-
<example>
|
|
1674
|
-
## Recommendation
|
|
1675
|
-
|
|
1676
|
-
**Approve** \u2014 Diff is scoped, behavior looks correct, and there are no blocking issues. Safe to merge.
|
|
1677
|
-
</example>
|
|
1678
|
-
|
|
1679
|
-
## Growing the rules
|
|
1680
|
-
|
|
1681
|
-
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/\`.
|
|
1682
|
-
`;
|
|
1683
|
-
var REVIEW_SKILL_PATH = ".claude/skills/review/SKILL.md";
|
|
1684
|
-
var REVIEW_SKILL_NAME = "review";
|
|
1685
|
-
var REVIEW_SKILL_FRONTMATTER = `---
|
|
1686
|
-
name: review
|
|
1687
|
-
description: >-
|
|
1688
|
-
Merge-readiness review for this repository. Use when reviewing a PR, worktree
|
|
1689
|
-
diff, or Sideboard Review (Approve / Approve with nits / Request changes).
|
|
1690
|
-
---
|
|
1691
|
-
|
|
1692
|
-
`;
|
|
1693
|
-
function wrapReviewSkillMarkdown(body) {
|
|
1694
|
-
const trimmed = body.replace(/^\uFEFF/, "").trim();
|
|
1695
|
-
const source = trimmed || REVIEW_REQUEST_TEMPLATE.trim();
|
|
1696
|
-
if (/^---\s*\n[\s\S]*?\n---\s*\n/.test(source)) {
|
|
1697
|
-
return source.endsWith("\n") ? source : `${source}
|
|
1698
|
-
`;
|
|
1699
|
-
}
|
|
1700
|
-
return `${REVIEW_SKILL_FRONTMATTER}${source}
|
|
1701
|
-
`;
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
// src/review/request-review.ts
|
|
1705
|
-
var REPO_REVIEW_PATH = ".sideboard/review.md";
|
|
1706
|
-
var REPO_REVIEW_NAME = "review.md";
|
|
1707
|
-
var CONTEXT_REVIEW_PATH = ".context/review.md";
|
|
1708
|
-
var REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
|
|
1709
|
-
var LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
|
|
1710
|
-
var REVIEW_REQUEST_NAME = "Review request.md";
|
|
1711
|
-
var REVIEW_REQUEST_PREFILL = "Review changes in this workspace.";
|
|
1712
|
-
var LEGACY_REVIEW_TEMPLATE_MARKERS = [
|
|
1713
|
-
"You are acting as a reviewer for a proposed code change made by another engineer.",
|
|
1714
|
-
"HOW MANY FINDINGS TO RETURN:"
|
|
1715
|
-
];
|
|
1716
|
-
function shouldRefreshReviewRequestTemplate(content) {
|
|
1717
|
-
const trimmed = content.trim();
|
|
1718
|
-
if (!trimmed) return true;
|
|
1719
|
-
if (/##\s*Recommendation|ready to merge|Approve with nits|Request changes/i.test(trimmed)) {
|
|
1720
|
-
return false;
|
|
1721
|
-
}
|
|
1722
|
-
return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
|
|
1723
|
-
}
|
|
1724
|
-
function readTextIfPresent(abs) {
|
|
1725
|
-
if (!existsSync2(abs)) return null;
|
|
1726
|
-
try {
|
|
1727
|
-
const content = readFileSync(abs, "utf8");
|
|
1728
|
-
return content.trim() ? content : null;
|
|
1729
|
-
} catch {
|
|
1730
|
-
return null;
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
function readLocalGuidelines(worktreePath) {
|
|
1734
|
-
const localAbs = join3(worktreePath, REVIEW_REQUEST_PATH);
|
|
1735
|
-
const localContent = readTextIfPresent(localAbs);
|
|
1736
|
-
if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
|
|
1737
|
-
return { path: REVIEW_REQUEST_PATH, content: localContent };
|
|
1738
|
-
}
|
|
1739
|
-
const legacyAbs = join3(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
|
|
1740
|
-
const legacyContent = readTextIfPresent(legacyAbs);
|
|
1741
|
-
if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
|
|
1742
|
-
return { path: LEGACY_REVIEW_REQUEST_PATH, content: legacyContent };
|
|
1743
|
-
}
|
|
1744
|
-
return null;
|
|
1745
|
-
}
|
|
1746
|
-
function skillGuidelines(content, source) {
|
|
1747
|
-
return {
|
|
1748
|
-
path: REVIEW_SKILL_PATH,
|
|
1749
|
-
name: REVIEW_SKILL_NAME,
|
|
1750
|
-
content,
|
|
1751
|
-
source
|
|
1752
|
-
};
|
|
1753
|
-
}
|
|
1754
|
-
function contextGuidelines(content, source) {
|
|
1755
|
-
return {
|
|
1756
|
-
path: CONTEXT_REVIEW_PATH,
|
|
1757
|
-
name: REPO_REVIEW_NAME,
|
|
1758
|
-
content,
|
|
1759
|
-
source
|
|
1760
|
-
};
|
|
1761
|
-
}
|
|
1762
|
-
function writeContextGuidelines(worktreePath, content) {
|
|
1763
|
-
const abs = join3(worktreePath, CONTEXT_REVIEW_PATH);
|
|
1764
|
-
mkdirSync2(dirname(abs), { recursive: true });
|
|
1765
|
-
const body = content.endsWith("\n") ? content : `${content}
|
|
1766
|
-
`;
|
|
1767
|
-
writeFileSync(abs, body, "utf8");
|
|
1768
|
-
return body;
|
|
1769
|
-
}
|
|
1770
|
-
function readSideboardReview(worktreePath, repoPath) {
|
|
1771
|
-
const fromWorktree = readTextIfPresent(join3(worktreePath, REPO_REVIEW_PATH));
|
|
1772
|
-
if (fromWorktree) return fromWorktree;
|
|
1773
|
-
const repo = repoPath?.replace(/\/+$/, "");
|
|
1774
|
-
const wt = worktreePath.replace(/\/+$/, "");
|
|
1775
|
-
if (!repo || repo === wt) return null;
|
|
1776
|
-
return readTextIfPresent(join3(repo, REPO_REVIEW_PATH));
|
|
1777
|
-
}
|
|
1778
|
-
function ensureReviewGuidelinesFile(worktreePath, repoPath) {
|
|
1779
|
-
const skillContent = readTextIfPresent(join3(worktreePath, REVIEW_SKILL_PATH));
|
|
1780
|
-
if (skillContent) {
|
|
1781
|
-
return { ...skillGuidelines(skillContent, "skill"), wrote: false };
|
|
1782
|
-
}
|
|
1783
|
-
const contextContent = readTextIfPresent(join3(worktreePath, CONTEXT_REVIEW_PATH));
|
|
1784
|
-
if (contextContent && !shouldRefreshReviewRequestTemplate(contextContent)) {
|
|
1785
|
-
return { ...contextGuidelines(contextContent, "local"), wrote: false };
|
|
1786
|
-
}
|
|
1787
|
-
const fromSideboard = readSideboardReview(worktreePath, repoPath);
|
|
1788
|
-
if (fromSideboard && !shouldRefreshReviewRequestTemplate(fromSideboard)) {
|
|
1789
|
-
const body2 = writeContextGuidelines(worktreePath, fromSideboard);
|
|
1790
|
-
return { ...contextGuidelines(body2, "repo"), wrote: true };
|
|
1791
|
-
}
|
|
1792
|
-
const local = readLocalGuidelines(worktreePath);
|
|
1793
|
-
const body = writeContextGuidelines(worktreePath, local?.content ?? REVIEW_REQUEST_TEMPLATE);
|
|
1794
|
-
return {
|
|
1795
|
-
...contextGuidelines(body, local ? "local" : "stock"),
|
|
1796
|
-
wrote: true
|
|
1797
|
-
};
|
|
1798
|
-
}
|
|
1799
|
-
function ensureReviewSkillFile(worktreePath, repoPath) {
|
|
1800
|
-
const g = ensureReviewGuidelinesFile(worktreePath, repoPath);
|
|
1801
|
-
return { path: g.path, content: g.content, wrote: g.wrote };
|
|
1802
|
-
}
|
|
1803
|
-
function resolveReviewGuidelines(worktreePath, repoPath) {
|
|
1804
|
-
return ensureReviewGuidelinesFile(worktreePath, repoPath);
|
|
1805
|
-
}
|
|
1806
|
-
function ensureReviewRequestFile(worktreePath, repoPath) {
|
|
1807
|
-
return ensureReviewGuidelinesFile(worktreePath, repoPath);
|
|
1808
|
-
}
|
|
1809
|
-
function buildReviewRequestAttachment(content, opts) {
|
|
1810
|
-
const path = opts?.path ?? REVIEW_SKILL_PATH;
|
|
1811
|
-
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);
|
|
1812
|
-
return {
|
|
1813
|
-
id: randomUUID2(),
|
|
1814
|
-
name,
|
|
1815
|
-
kind: "file",
|
|
1816
|
-
path,
|
|
1817
|
-
content
|
|
1818
|
-
};
|
|
1819
|
-
}
|
|
1820
|
-
function readExistingReviewRequestFile(worktreePath) {
|
|
1821
|
-
return readTextIfPresent(join3(worktreePath, REVIEW_SKILL_PATH)) ?? readTextIfPresent(join3(worktreePath, CONTEXT_REVIEW_PATH)) ?? readTextIfPresent(join3(worktreePath, REPO_REVIEW_PATH)) ?? readTextIfPresent(join3(worktreePath, REVIEW_REQUEST_PATH)) ?? readTextIfPresent(join3(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
|
|
1822
|
-
}
|
|
1823
|
-
function canReuseIdleWorktreeAgentForReview(from, siblings) {
|
|
1824
|
-
const live = siblings.filter((t) => t.status !== "archived");
|
|
1825
|
-
if (live.length !== 1) return false;
|
|
1826
|
-
const only = live[0];
|
|
1827
|
-
if (only.id !== from.id) return false;
|
|
1828
|
-
if (from.status !== "idle") return false;
|
|
1829
|
-
if ((from.queue?.length ?? 0) > 0) return false;
|
|
1830
|
-
return (from.messages?.length ?? 0) === 0 && (only.messages?.length ?? 0) === 0;
|
|
1831
|
-
}
|
|
1832
|
-
function reviewGuidelinesAttachment(from) {
|
|
1833
|
-
const guidelines = resolveReviewGuidelines(from.worktreePath, from.repoPath);
|
|
1834
|
-
return buildReviewRequestAttachment(guidelines.content, {
|
|
1835
|
-
path: guidelines.path,
|
|
1836
|
-
name: guidelines.name
|
|
1837
|
-
});
|
|
1838
|
-
}
|
|
1839
|
-
async function requestReview(threadRef, send) {
|
|
1840
|
-
const from = findThreadByRef(threadRef);
|
|
1841
|
-
if (!from) throw new Error(`Thread not found: ${threadRef}`);
|
|
1842
|
-
if (isOrchestratorThread(from)) {
|
|
1843
|
-
throw new Error(
|
|
1844
|
-
"request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
|
|
1845
|
-
);
|
|
1846
|
-
}
|
|
1847
|
-
if (from.status === "archived") {
|
|
1848
|
-
throw new Error(`Thread is archived: ${from.id}`);
|
|
1849
|
-
}
|
|
1850
|
-
const attachment = reviewGuidelinesAttachment(from);
|
|
1851
|
-
const siblings = threadsSharingWorktree(from.worktreePath);
|
|
1852
|
-
if (canReuseIdleWorktreeAgentForReview(from, siblings)) {
|
|
1853
|
-
const reused = updateThread(from.id, {
|
|
1854
|
-
title: REVIEW_TAB_TITLE,
|
|
1855
|
-
userSetTitle: true,
|
|
1856
|
-
attachments: [...from.attachments ?? [], attachment]
|
|
1857
|
-
});
|
|
1858
|
-
const started2 = await send(reused.id, REVIEW_REQUEST_PREFILL);
|
|
1859
|
-
return { tab: started2, from: reused };
|
|
1860
|
-
}
|
|
1861
|
-
const tab = createChatTab({
|
|
1862
|
-
fromThreadId: from.id,
|
|
1863
|
-
title: REVIEW_TAB_TITLE,
|
|
1864
|
-
attachments: [attachment]
|
|
1865
|
-
});
|
|
1866
|
-
const started = await send(tab.id, REVIEW_REQUEST_PREFILL);
|
|
1867
|
-
return { tab: started, from };
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
// src/hook/worktree-run-ports.ts
|
|
1871
|
-
import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1872
|
-
import { dirname as dirname2, join as join4 } from "path";
|
|
1873
|
-
var RUN_PORTS_REL = ".context/.sideboard/run-ports.json";
|
|
1874
|
-
function worktreeRunPortsPath(worktreePath) {
|
|
1875
|
-
return join4(worktreePath, RUN_PORTS_REL);
|
|
1876
|
-
}
|
|
1877
|
-
function isPort(value) {
|
|
1878
|
-
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65535;
|
|
1879
|
-
}
|
|
1880
|
-
function parseFile(raw) {
|
|
1881
|
-
try {
|
|
1882
|
-
const parsed = JSON.parse(raw);
|
|
1883
|
-
if (!parsed || typeof parsed !== "object") return {};
|
|
1884
|
-
const scripts = parsed.scripts;
|
|
1885
|
-
if (!scripts || typeof scripts !== "object") return {};
|
|
1886
|
-
const next = {};
|
|
1887
|
-
for (const [name, ports] of Object.entries(scripts)) {
|
|
1888
|
-
if (!name || !Array.isArray(ports)) continue;
|
|
1889
|
-
const cleaned = ports.filter(isPort);
|
|
1890
|
-
if (cleaned.length) next[name] = cleaned;
|
|
1891
|
-
}
|
|
1892
|
-
return { scripts: next };
|
|
1893
|
-
} catch {
|
|
1894
|
-
return {};
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
function loadWorktreeRunPorts(worktreePath, scriptName) {
|
|
1898
|
-
if (!worktreePath.trim() || !scriptName.trim()) return null;
|
|
1899
|
-
try {
|
|
1900
|
-
const raw = readFileSync2(worktreeRunPortsPath(worktreePath), "utf8");
|
|
1901
|
-
const ports = parseFile(raw).scripts?.[scriptName];
|
|
1902
|
-
return ports?.length ? ports : null;
|
|
1903
|
-
} catch {
|
|
1904
|
-
return null;
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
function saveWorktreeRunPorts(worktreePath, scriptName, ports) {
|
|
1908
|
-
if (!worktreePath.trim() || !scriptName.trim()) return;
|
|
1909
|
-
const cleaned = ports.filter(isPort);
|
|
1910
|
-
if (!cleaned.length) return;
|
|
1911
|
-
const path = worktreeRunPortsPath(worktreePath);
|
|
1912
|
-
let current = {};
|
|
1913
|
-
try {
|
|
1914
|
-
current = parseFile(readFileSync2(path, "utf8"));
|
|
1915
|
-
} catch {
|
|
1916
|
-
current = {};
|
|
1917
|
-
}
|
|
1918
|
-
const scripts = { ...current.scripts ?? {} };
|
|
1919
|
-
scripts[scriptName] = cleaned;
|
|
1920
|
-
mkdirSync3(dirname2(path), { recursive: true });
|
|
1921
|
-
writeFileSync2(path, `${JSON.stringify({ scripts }, null, 2)}
|
|
1922
|
-
`, "utf8");
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
// src/mcp/wait-for-job.ts
|
|
1926
|
-
import {
|
|
1927
|
-
appendFileSync as appendFileSync2,
|
|
1928
|
-
existsSync as existsSync3,
|
|
1929
|
-
mkdirSync as mkdirSync4,
|
|
1930
|
-
readdirSync,
|
|
1931
|
-
readFileSync as readFileSync3,
|
|
1932
|
-
writeFileSync as writeFileSync3
|
|
1933
|
-
} from "fs";
|
|
1934
|
-
import { dirname as dirname3, join as join5 } from "path";
|
|
1935
|
-
var MAX_JOB_CONTINUES = 8;
|
|
1936
|
-
var MAX_JOB_DELTA_LINES = 80;
|
|
1937
|
-
var MAX_JOB_DELTA_CHARS = 4e3;
|
|
1938
|
-
var DEFERRED_DONE_MAX_CHARS = 480;
|
|
1939
|
-
var GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
|
|
1940
|
-
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.";
|
|
1941
|
-
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.";
|
|
1942
|
-
var JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
|
|
1943
|
-
function mcpWaitForJobTimeoutMs(requested) {
|
|
1944
|
-
return mcpWaitForTurnTimeoutMs(requested);
|
|
1945
|
-
}
|
|
1946
|
-
function sanitizeDetachedJobId(id) {
|
|
1947
|
-
const s = id.trim();
|
|
1948
|
-
if (!JOB_ID_RE.test(s)) {
|
|
1949
|
-
throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
|
|
1950
|
-
}
|
|
1951
|
-
return s;
|
|
1952
|
-
}
|
|
1953
|
-
function jobAlive(pid) {
|
|
1954
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1955
|
-
try {
|
|
1956
|
-
process.kill(pid, 0);
|
|
1957
|
-
return true;
|
|
1958
|
-
} catch {
|
|
1959
|
-
return false;
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
function processGroupAlive(pgid) {
|
|
1963
|
-
if (process.platform === "win32") return false;
|
|
1964
|
-
if (!Number.isInteger(pgid) || pgid <= 1) return false;
|
|
1965
|
-
try {
|
|
1966
|
-
process.kill(-pgid, 0);
|
|
1967
|
-
return true;
|
|
1968
|
-
} catch (err) {
|
|
1969
|
-
return Boolean(err && typeof err === "object" && "code" in err && err.code === "EPERM");
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
function jobTreeAlive(pid) {
|
|
1973
|
-
if (pid == null) return false;
|
|
1974
|
-
return jobAlive(pid) || processGroupAlive(pid);
|
|
1975
|
-
}
|
|
1976
|
-
function readIntFile(file) {
|
|
1977
|
-
if (!existsSync3(file)) return null;
|
|
1978
|
-
const n = Number.parseInt(readFileSync3(file, "utf8").trim(), 10);
|
|
1979
|
-
return Number.isInteger(n) ? n : null;
|
|
1980
|
-
}
|
|
1981
|
-
function jobDir(root, id, legacy = false) {
|
|
1982
|
-
return join5(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
|
|
1983
|
-
}
|
|
1984
|
-
function resolveJobDir(root, id) {
|
|
1985
|
-
const modern = jobDir(root, id, false);
|
|
1986
|
-
if (existsSync3(modern)) return modern;
|
|
1987
|
-
const legacy = jobDir(root, id, true);
|
|
1988
|
-
if (existsSync3(legacy)) return legacy;
|
|
1989
|
-
return modern;
|
|
1990
|
-
}
|
|
1991
|
-
function listJobIdsIn(root, rel) {
|
|
1992
|
-
const dir = join5(root, rel);
|
|
1993
|
-
if (!existsSync3(dir)) return [];
|
|
1994
|
-
return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && JOB_ID_RE.test(e.name)).map((e) => e.name);
|
|
1995
|
-
}
|
|
1996
|
-
function listRunningDetachedJobs(worktreePath) {
|
|
1997
|
-
const root = worktreePath.trim();
|
|
1998
|
-
if (!root) return [];
|
|
1999
|
-
const ids = /* @__PURE__ */ new Set([
|
|
2000
|
-
...listJobIdsIn(root, DETACHED_JOBS_DIR),
|
|
2001
|
-
...listJobIdsIn(root, LEGACY_DETACHED_JOBS_DIR)
|
|
2002
|
-
]);
|
|
2003
|
-
const running = [];
|
|
2004
|
-
for (const id of ids) {
|
|
2005
|
-
const dir = resolveJobDir(root, id);
|
|
2006
|
-
const pid = readIntFile(join5(dir, "pid"));
|
|
2007
|
-
if (jobTreeAlive(pid)) running.push(id);
|
|
2008
|
-
}
|
|
2009
|
-
return running.sort();
|
|
2010
|
-
}
|
|
2011
|
-
function looksLikeDeferredDonePromise(text) {
|
|
2012
|
-
const t = (text ?? "").trim();
|
|
2013
|
-
if (!t || t.length > DEFERRED_DONE_MAX_CHARS) return false;
|
|
2014
|
-
const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
|
|
2015
|
-
if (!unquoted) return false;
|
|
2016
|
-
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);
|
|
2017
|
-
}
|
|
2018
|
-
function isJobContinuePrompt(text) {
|
|
2019
|
-
if (typeof text !== "string") return false;
|
|
2020
|
-
const t = text.trim();
|
|
2021
|
-
return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
|
|
2022
|
-
}
|
|
2023
|
-
function collapseGhRunWatchLog(text) {
|
|
2024
|
-
if (!text.includes(GH_WATCH_REFRESH)) return text;
|
|
2025
|
-
const frames = text.split(/(?=Refreshing run status every 3 seconds\.)/);
|
|
2026
|
-
const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
|
|
2027
|
-
if (watchFrames.length <= 1) return text;
|
|
2028
|
-
const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
|
|
2029
|
-
const last = watchFrames[watchFrames.length - 1] ?? "";
|
|
2030
|
-
const omitted = watchFrames.length - 1;
|
|
2031
|
-
const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
|
|
2032
|
-
`;
|
|
2033
|
-
return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
|
|
2034
|
-
}
|
|
2035
|
-
function capJobLogDelta(text) {
|
|
2036
|
-
const collapsed = collapseGhRunWatchLog(text);
|
|
2037
|
-
const lines = collapsed.split("\n");
|
|
2038
|
-
let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
|
|
2039
|
-
${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
|
|
2040
|
-
if (out.length > MAX_JOB_DELTA_CHARS) {
|
|
2041
|
-
out = `\u2026(truncated ${out.length} chars)
|
|
2042
|
-
${out.slice(-MAX_JOB_DELTA_CHARS)}`;
|
|
2043
|
-
}
|
|
2044
|
-
return out;
|
|
2045
|
-
}
|
|
2046
|
-
function formatJobStillRunningContinuePrompt(jobIds) {
|
|
2047
|
-
const ids = jobIds.join(", ");
|
|
2048
|
-
return [
|
|
2049
|
-
`Detached job still running: ${ids}.`,
|
|
2050
|
-
"Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
|
|
2051
|
-
"If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
|
|
2052
|
-
"Then report the result. Do not tell the user you will let them know later."
|
|
2053
|
-
].join(" ");
|
|
2054
|
-
}
|
|
2055
|
-
function formatDeferredDoneContinuePrompt() {
|
|
2056
|
-
return [
|
|
2057
|
-
"You ended the turn after promising to report later, but no detached job is running.",
|
|
2058
|
-
"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).",
|
|
2059
|
-
"Do not say you will let the user know later."
|
|
2060
|
-
].join(" ");
|
|
2061
|
-
}
|
|
2062
|
-
function turnWatchedDetachedJob(parts) {
|
|
2063
|
-
return parts.some((p) => {
|
|
2064
|
-
if (p.type !== "tool") return false;
|
|
2065
|
-
if (/wait_for_job$/i.test(p.name ?? "") || /stop_job$/i.test(p.name ?? "")) return true;
|
|
2066
|
-
const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
|
|
2067
|
-
return /detached-job\.(?:js|cjs)\b/i.test(blob);
|
|
2068
|
-
});
|
|
2069
|
-
}
|
|
2070
|
-
function planJobContinue(opts) {
|
|
2071
|
-
if (opts.isOrchestrator) return { action: "none" };
|
|
2072
|
-
if (opts.agent === "brightsy") return { action: "none" };
|
|
2073
|
-
if (opts.queueLength > 0) return { action: "none" };
|
|
2074
|
-
if (opts.continueCount >= MAX_JOB_CONTINUES) return { action: "none" };
|
|
2075
|
-
const farewell = looksLikeDeferredDonePromise(opts.chatText);
|
|
2076
|
-
if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
|
|
2077
|
-
return {
|
|
2078
|
-
action: "wait",
|
|
2079
|
-
jobIds: opts.runningJobIds,
|
|
2080
|
-
prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
|
|
2081
|
-
};
|
|
2082
|
-
}
|
|
2083
|
-
if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
|
|
2084
|
-
return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
|
|
2085
|
-
}
|
|
2086
|
-
return { action: "none" };
|
|
2087
|
-
}
|
|
2088
|
-
function tailProgress(logFile, maxLines = 12) {
|
|
2089
|
-
if (!existsSync3(logFile)) return "(no log yet)";
|
|
2090
|
-
const lines = readFileSync3(logFile, "utf8").split("\n");
|
|
2091
|
-
return lines.slice(-maxLines).join("\n");
|
|
2092
|
-
}
|
|
2093
|
-
function lastProgressLine(progress) {
|
|
2094
|
-
const last = progress.split("\n").map((l) => l.trim()).filter((l) => l && l !== "(no log yet)").at(-1);
|
|
2095
|
-
return last ? last.slice(0, 72) : void 0;
|
|
2096
|
-
}
|
|
2097
|
-
function readLogLines(file) {
|
|
2098
|
-
if (!existsSync3(file)) return [];
|
|
2099
|
-
const lines = readFileSync3(file, "utf8").split("\n");
|
|
2100
|
-
if (lines.length && lines[lines.length - 1] === "") lines.pop();
|
|
2101
|
-
return lines;
|
|
2102
|
-
}
|
|
2103
|
-
function takeDelta(logFile, cursorFile) {
|
|
2104
|
-
const lines = readLogLines(logFile);
|
|
2105
|
-
const cursor = readIntFile(cursorFile) ?? 0;
|
|
2106
|
-
const start = Math.min(Math.max(0, cursor), lines.length);
|
|
2107
|
-
return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
|
|
2108
|
-
}
|
|
2109
|
-
function snapshotJob(dir) {
|
|
2110
|
-
const pid = readIntFile(join5(dir, "pid"));
|
|
2111
|
-
const running = jobTreeAlive(pid);
|
|
2112
|
-
return {
|
|
2113
|
-
pid,
|
|
2114
|
-
running,
|
|
2115
|
-
exitCode: readIntFile(join5(dir, "exit")),
|
|
2116
|
-
log: join5(dir, "log"),
|
|
2117
|
-
cursor: join5(dir, "present.cursor"),
|
|
2118
|
-
progress: tailProgress(join5(dir, "log"))
|
|
2119
|
-
};
|
|
2120
|
-
}
|
|
2121
|
-
function toResult(id, snap, extra) {
|
|
2122
|
-
const failed = extra?.failed === true || !snap.running && snap.exitCode != null && snap.exitCode !== 0;
|
|
2123
|
-
const ok = !snap.running && snap.exitCode === 0;
|
|
2124
|
-
const stillRunning = snap.running && !ok;
|
|
2125
|
-
const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
|
|
2126
|
-
try {
|
|
2127
|
-
mkdirSync4(dirname3(snap.cursor), { recursive: true });
|
|
2128
|
-
writeFileSync3(snap.cursor, `${nextCursor}
|
|
2129
|
-
`);
|
|
2130
|
-
} catch {
|
|
2131
|
-
}
|
|
2132
|
-
const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
|
|
2133
|
-
const progress = extra?.progress ?? snap.progress;
|
|
2134
|
-
return {
|
|
2135
|
-
stillRunning,
|
|
2136
|
-
ok,
|
|
2137
|
-
failed: Boolean(failed && !stillRunning && !ok),
|
|
2138
|
-
status,
|
|
2139
|
-
id,
|
|
2140
|
-
pid: snap.pid,
|
|
2141
|
-
exitCode: snap.exitCode ?? void 0,
|
|
2142
|
-
phase: lastProgressLine(progress),
|
|
2143
|
-
delta,
|
|
2144
|
-
progress,
|
|
2145
|
-
hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
|
|
2146
|
-
};
|
|
2147
|
-
}
|
|
2148
|
-
async function sleepMs(ms) {
|
|
2149
|
-
await new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2150
|
-
}
|
|
2151
|
-
async function waitForDetachedJob(cwd, id, opts) {
|
|
2152
|
-
const jobId = sanitizeDetachedJobId(id);
|
|
2153
|
-
const root = cwd.trim() || process.cwd();
|
|
2154
|
-
const dir = resolveJobDir(root, jobId);
|
|
2155
|
-
const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
|
|
2156
|
-
const sleep = opts?.sleep ?? sleepMs;
|
|
2157
|
-
if (!existsSync3(dir)) {
|
|
2158
|
-
return {
|
|
2159
|
-
stillRunning: false,
|
|
2160
|
-
ok: false,
|
|
2161
|
-
failed: true,
|
|
2162
|
-
status: "failed",
|
|
2163
|
-
id: jobId,
|
|
2164
|
-
delta: "",
|
|
2165
|
-
progress: "No detached job. Start one first.",
|
|
2166
|
-
hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
|
|
2167
|
-
};
|
|
2168
|
-
}
|
|
2169
|
-
const deadline = Date.now() + timeoutMs;
|
|
2170
|
-
let snap = snapshotJob(dir);
|
|
2171
|
-
if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
|
|
2172
|
-
if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
|
|
2173
|
-
return toResult(jobId, snap, {
|
|
2174
|
-
failed: true,
|
|
2175
|
-
progress: "No detached job. Start one first."
|
|
2176
|
-
});
|
|
2177
|
-
}
|
|
2178
|
-
while (Date.now() < deadline) {
|
|
2179
|
-
snap = snapshotJob(dir);
|
|
2180
|
-
if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
|
|
2181
|
-
if (!snap.running) return toResult(jobId, snap, { failed: true });
|
|
2182
|
-
await sleep(Math.min(2e3, Math.max(50, deadline - Date.now())));
|
|
2183
|
-
}
|
|
2184
|
-
snap = snapshotJob(dir);
|
|
2185
|
-
return toResult(jobId, snap);
|
|
2186
|
-
}
|
|
2187
|
-
function killProcessTree(pid, signal) {
|
|
2188
|
-
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
2189
|
-
if (process.platform !== "win32") {
|
|
2190
|
-
try {
|
|
2191
|
-
process.kill(-pid, signal);
|
|
2192
|
-
return;
|
|
2193
|
-
} catch {
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
|
-
try {
|
|
2197
|
-
process.kill(pid, signal);
|
|
2198
|
-
} catch {
|
|
2199
|
-
}
|
|
2200
|
-
}
|
|
2201
|
-
function appendJobLog(logFile, line) {
|
|
2202
|
-
try {
|
|
2203
|
-
mkdirSync4(dirname3(logFile), { recursive: true });
|
|
2204
|
-
const body = line.endsWith("\n") ? line : `${line}
|
|
2205
|
-
`;
|
|
2206
|
-
appendFileSync2(logFile, body);
|
|
2207
|
-
} catch {
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
async function stopDetachedJob(cwd, id, opts) {
|
|
2211
|
-
const jobId = sanitizeDetachedJobId(id);
|
|
2212
|
-
const root = cwd.trim() || process.cwd();
|
|
2213
|
-
const dir = resolveJobDir(root, jobId);
|
|
2214
|
-
const sleep = opts?.sleep ?? sleepMs;
|
|
2215
|
-
const why = opts?.reason?.trim() ?? "";
|
|
2216
|
-
if (!existsSync3(dir)) {
|
|
2217
|
-
return {
|
|
2218
|
-
stillRunning: false,
|
|
2219
|
-
ok: false,
|
|
2220
|
-
failed: true,
|
|
2221
|
-
status: "failed",
|
|
2222
|
-
id: jobId,
|
|
2223
|
-
delta: "",
|
|
2224
|
-
progress: "No detached job. Start one first.",
|
|
2225
|
-
hint: "Start with detached-job.cjs start <id> -- <command>, then wait_for_job or stop_job.",
|
|
2226
|
-
stopped: false,
|
|
2227
|
-
reason: "not-found"
|
|
2228
|
-
};
|
|
2229
|
-
}
|
|
2230
|
-
let snap = snapshotJob(dir);
|
|
2231
|
-
if (!snap.running) {
|
|
2232
|
-
const result2 = toResult(jobId, snap);
|
|
2233
|
-
return {
|
|
2234
|
-
...result2,
|
|
2235
|
-
stopped: false,
|
|
2236
|
-
reason: "not-running",
|
|
2237
|
-
hint: "That job is already finished. Read the log; do not wait again unless you start a new command."
|
|
2238
|
-
};
|
|
2239
|
-
}
|
|
2240
|
-
appendJobLog(snap.log, `$ stop${why ? `: ${why}` : ""}`);
|
|
2241
|
-
const targetPid = snap.pid;
|
|
2242
|
-
if (targetPid != null) killProcessTree(targetPid, "SIGTERM");
|
|
2243
|
-
const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
|
|
2244
|
-
const deadline = Date.now() + graceMs;
|
|
2245
|
-
while (Date.now() < deadline && jobTreeAlive(targetPid)) {
|
|
2246
|
-
await sleep(Math.min(100, Math.max(20, deadline - Date.now())));
|
|
2247
|
-
}
|
|
2248
|
-
if (targetPid != null) {
|
|
2249
|
-
killProcessTree(targetPid, "SIGKILL");
|
|
2250
|
-
await sleep(100);
|
|
2251
|
-
}
|
|
2252
|
-
const exitFile = join5(dir, "exit");
|
|
2253
|
-
if (!existsSync3(exitFile)) {
|
|
2254
|
-
try {
|
|
2255
|
-
writeFileSync3(exitFile, "1\n");
|
|
2256
|
-
} catch {
|
|
2257
|
-
}
|
|
2258
|
-
}
|
|
2259
|
-
snap = snapshotJob(dir);
|
|
2260
|
-
const stillRunning = jobTreeAlive(targetPid) || snap.running;
|
|
2261
|
-
const result = toResult(jobId, snap, { failed: true });
|
|
2262
|
-
const stopped = !stillRunning;
|
|
2263
|
-
return {
|
|
2264
|
-
...result,
|
|
2265
|
-
stillRunning,
|
|
2266
|
-
ok: false,
|
|
2267
|
-
failed: true,
|
|
2268
|
-
status: stillRunning ? "running" : "failed",
|
|
2269
|
-
stopped,
|
|
2270
|
-
reason: stopped ? "stopped" : "still-running",
|
|
2271
|
-
stopReason: why || void 0,
|
|
2272
|
-
hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
|
|
2273
|
-
};
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
// src/hook/conductor.ts
|
|
2277
|
-
var PORT_RANGE_SIZE = 10;
|
|
2278
|
-
var RECLAIM_RETRY_DELAY_MS = 40;
|
|
2279
|
-
var RECLAIM_TRIES = 8;
|
|
2280
|
-
function matchSimpleGlob(pattern, name) {
|
|
2281
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
2282
|
-
return new RegExp(`^${escaped}$`).test(name);
|
|
2283
|
-
}
|
|
2284
|
-
function readWorktreeInclude(repoPath) {
|
|
2285
|
-
const path = join6(repoPath, ".worktreeinclude");
|
|
2286
|
-
if (!existsSync4(path)) return [];
|
|
2287
|
-
return readFileSync4(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
2288
|
-
}
|
|
2289
|
-
function resolveFilesToCopy(repoPath) {
|
|
2290
|
-
const fromInclude = readWorktreeInclude(repoPath);
|
|
2291
|
-
if (fromInclude.length) return fromInclude;
|
|
2292
|
-
const settings = loadRepoSettings(repoPath);
|
|
2293
|
-
if (settings?.filesToCopy?.length) return settings.filesToCopy;
|
|
2294
|
-
if (settings?.fileIncludeGlobs?.length) {
|
|
2295
|
-
const matched = [];
|
|
2296
|
-
try {
|
|
2297
|
-
for (const entry of readdirSync2(repoPath, { withFileTypes: true })) {
|
|
2298
|
-
if (!entry.isFile()) continue;
|
|
2299
|
-
for (const glob of settings.fileIncludeGlobs) {
|
|
2300
|
-
if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob(basename(glob), entry.name)) {
|
|
2301
|
-
matched.push(entry.name);
|
|
2302
|
-
break;
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2305
|
-
}
|
|
2306
|
-
} catch {
|
|
2307
|
-
}
|
|
2308
|
-
if (matched.length) return [...new Set(matched)];
|
|
2309
|
-
}
|
|
2310
|
-
const defaults = [];
|
|
2311
|
-
try {
|
|
2312
|
-
for (const entry of readdirSync2(repoPath, { withFileTypes: true })) {
|
|
2313
|
-
if (entry.isFile() && entry.name.startsWith(".env")) {
|
|
2314
|
-
defaults.push(entry.name);
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2317
|
-
} catch {
|
|
2318
|
-
}
|
|
2319
|
-
if (defaults.length) return defaults;
|
|
2320
|
-
return [".env.local", ".env"];
|
|
2321
|
-
}
|
|
2322
|
-
function copyConfiguredFiles(repoPath, worktreePath) {
|
|
2323
|
-
const patterns = resolveFilesToCopy(repoPath);
|
|
2324
|
-
const copied = [];
|
|
2325
|
-
for (const rel of patterns) {
|
|
2326
|
-
const src = join6(repoPath, rel);
|
|
2327
|
-
if (!existsSync4(src)) continue;
|
|
2328
|
-
const dest = join6(worktreePath, rel);
|
|
2329
|
-
mkdirSync5(dirname4(dest), { recursive: true });
|
|
2330
|
-
copyFileSync(src, dest);
|
|
2331
|
-
copied.push(rel);
|
|
2332
|
-
}
|
|
2333
|
-
return copied;
|
|
2334
|
-
}
|
|
2335
|
-
var cachedLoginEnv = null;
|
|
2336
|
-
async function captureLoginEnv() {
|
|
2337
|
-
if (cachedLoginEnv) return { ...cachedLoginEnv };
|
|
2338
|
-
const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
|
|
2339
|
-
try {
|
|
2340
|
-
const result = await execa2(shell, ["-l", "-c", "env -0"], {
|
|
2341
|
-
reject: false,
|
|
2342
|
-
timeout: 5e3
|
|
2343
|
-
});
|
|
2344
|
-
if (result.exitCode === 0 && result.stdout) {
|
|
2345
|
-
const env = { ...process.env };
|
|
2346
|
-
for (const entry of result.stdout.split("\0")) {
|
|
2347
|
-
if (!entry) continue;
|
|
2348
|
-
const eq = entry.indexOf("=");
|
|
2349
|
-
if (eq <= 0) continue;
|
|
2350
|
-
const key = entry.slice(0, eq);
|
|
2351
|
-
const value = entry.slice(eq + 1);
|
|
2352
|
-
if (key) env[key] = value;
|
|
2353
|
-
}
|
|
2354
|
-
cachedLoginEnv = env;
|
|
2355
|
-
return { ...env };
|
|
2356
|
-
}
|
|
2357
|
-
} catch {
|
|
2358
|
-
}
|
|
2359
|
-
cachedLoginEnv = { ...process.env };
|
|
2360
|
-
return { ...cachedLoginEnv };
|
|
2361
|
-
}
|
|
2362
|
-
function buildWorkspaceScriptEnv(opts, baseEnv) {
|
|
2363
|
-
const env = stripNestedElectronEnv({
|
|
2364
|
-
...baseEnv ?? process.env
|
|
2365
|
-
});
|
|
2366
|
-
const name = opts.workspaceName ?? basename(opts.worktreePath);
|
|
2367
|
-
const ports = opts.ports ?? [];
|
|
2368
|
-
const primary = ports[0];
|
|
2369
|
-
env.SIDEBOARD_WORKSPACE_NAME = name;
|
|
2370
|
-
env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
|
|
2371
|
-
env.SIDEBOARD_ROOT_PATH = opts.repoPath;
|
|
2372
|
-
env.SIDEBOARD_IS_LOCAL = "1";
|
|
2373
|
-
if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
|
|
2374
|
-
env.CONDUCTOR_WORKSPACE_NAME = name;
|
|
2375
|
-
env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
|
|
2376
|
-
env.CONDUCTOR_ROOT_PATH = opts.repoPath;
|
|
2377
|
-
env.CONDUCTOR_IS_LOCAL = "1";
|
|
2378
|
-
if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
|
|
2379
|
-
if (primary != null) {
|
|
2380
|
-
env.SIDEBOARD_PORT = String(primary);
|
|
2381
|
-
env.CONDUCTOR_PORT = String(primary);
|
|
2382
|
-
env.PORT = String(primary);
|
|
2383
|
-
for (let i = 1; i < ports.length; i++) {
|
|
2384
|
-
const p = ports[i];
|
|
2385
|
-
env[`SIDEBOARD_PORT_${i}`] = String(p);
|
|
2386
|
-
env[`CONDUCTOR_PORT_${i}`] = String(p);
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
applyWorktreePkgCacheEnv(env, opts.worktreePath);
|
|
2390
|
-
return env;
|
|
2391
|
-
}
|
|
2392
|
-
function pipeLines(stream, onLine) {
|
|
2393
|
-
if (!stream || !onLine) return;
|
|
2394
|
-
const rl = createInterface2({ input: stream });
|
|
2395
|
-
rl.on("line", onLine);
|
|
2396
|
-
}
|
|
2397
|
-
function killableListenerPids(raw, selfPid = process.pid) {
|
|
2398
|
-
const pids = [];
|
|
2399
|
-
for (const token of raw.trim().split(/\s+/)) {
|
|
2400
|
-
const pid = Number(token);
|
|
2401
|
-
if (!Number.isFinite(pid) || pid <= 0 || pid === selfPid) continue;
|
|
2402
|
-
pids.push(pid);
|
|
2403
|
-
}
|
|
2404
|
-
return pids;
|
|
2405
|
-
}
|
|
2406
|
-
function listUnixListenerPids(port, selfPid) {
|
|
2407
|
-
try {
|
|
2408
|
-
const raw = execFileSync("lsof", [`-tiTCP:${port}`, "-sTCP:LISTEN"], {
|
|
2409
|
-
encoding: "utf8",
|
|
2410
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
2411
|
-
timeout: 2e3
|
|
2412
|
-
});
|
|
2413
|
-
return killableListenerPids(raw, selfPid);
|
|
2414
|
-
} catch {
|
|
2415
|
-
return [];
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2418
|
-
function listenerCwd(pid) {
|
|
2419
|
-
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
2420
|
-
try {
|
|
2421
|
-
const raw = execFileSync("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
|
|
2422
|
-
encoding: "utf8",
|
|
2423
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
2424
|
-
timeout: 2e3
|
|
2425
|
-
});
|
|
2426
|
-
for (const line of raw.split("\n")) {
|
|
2427
|
-
if (line.startsWith("n") && line.length > 1) return line.slice(1);
|
|
2428
|
-
}
|
|
2429
|
-
} catch {
|
|
2430
|
-
}
|
|
2431
|
-
return null;
|
|
2432
|
-
}
|
|
2433
|
-
function realpathOrResolve(path) {
|
|
2434
|
-
try {
|
|
2435
|
-
return realpathSync(path);
|
|
2436
|
-
} catch {
|
|
2437
|
-
return resolve(path);
|
|
2438
|
-
}
|
|
2439
|
-
}
|
|
2440
|
-
function isOtherWorktreeListenerCwd(cwd, worktreePath) {
|
|
2441
|
-
const wt = realpathOrResolve(worktreePath);
|
|
2442
|
-
const c = realpathOrResolve(cwd);
|
|
2443
|
-
if (c === wt || c.startsWith(wt + sep)) return false;
|
|
2444
|
-
const parent = dirname4(wt);
|
|
2445
|
-
const thisName = basename(wt);
|
|
2446
|
-
if (!thisName || c === parent) return false;
|
|
2447
|
-
if (!c.startsWith(parent + sep)) return false;
|
|
2448
|
-
const first = c.slice(parent.length + 1).split(sep)[0];
|
|
2449
|
-
return Boolean(first) && first !== thisName;
|
|
2450
|
-
}
|
|
2451
|
-
function signalPid(pid, signal) {
|
|
2452
|
-
try {
|
|
2453
|
-
process.kill(pid, signal);
|
|
2454
|
-
} catch {
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2457
|
-
function isThisWorktreeListenerCwd(cwd, worktreePath) {
|
|
2458
|
-
const wt = realpathOrResolve(worktreePath);
|
|
2459
|
-
const c = realpathOrResolve(cwd);
|
|
2460
|
-
return c === wt || c.startsWith(wt + sep);
|
|
2461
|
-
}
|
|
2462
|
-
function killDetachedRunTree(pid, worktreePath) {
|
|
2463
|
-
if (!Number.isFinite(pid) || pid <= 1 || pid === process.pid) return;
|
|
2464
|
-
const cwd = listenerCwd(pid);
|
|
2465
|
-
if (cwd) {
|
|
2466
|
-
if (!isThisWorktreeListenerCwd(cwd, worktreePath)) return;
|
|
2467
|
-
} else if (!processGroupAlive(pid)) {
|
|
2468
|
-
return;
|
|
2469
|
-
}
|
|
2470
|
-
try {
|
|
2471
|
-
process.kill(-pid, "SIGTERM");
|
|
2472
|
-
} catch {
|
|
2473
|
-
signalPid(pid, "SIGTERM");
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
|
-
function killListenersOnPorts(ports) {
|
|
2477
|
-
const selfPid = process.pid;
|
|
2478
|
-
for (const port of ports) {
|
|
2479
|
-
if (!Number.isFinite(port) || port <= 0) continue;
|
|
2480
|
-
try {
|
|
2481
|
-
if (process.platform === "win32") {
|
|
2482
|
-
execFileSync(
|
|
2483
|
-
"powershell",
|
|
2484
|
-
[
|
|
2485
|
-
"-NoProfile",
|
|
2486
|
-
"-Command",
|
|
2487
|
-
`Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -ne ${selfPid} } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
|
|
2488
|
-
],
|
|
2489
|
-
{ stdio: "ignore", timeout: 3e3 }
|
|
2490
|
-
);
|
|
2491
|
-
continue;
|
|
2492
|
-
}
|
|
2493
|
-
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
2494
|
-
signalPid(pid, "SIGTERM");
|
|
2495
|
-
}
|
|
2496
|
-
} catch {
|
|
2497
|
-
}
|
|
654
|
+
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.";
|
|
655
|
+
case "merge":
|
|
656
|
+
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.";
|
|
2498
657
|
}
|
|
2499
658
|
}
|
|
2500
|
-
function
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
const selfPid = process.pid;
|
|
2510
|
-
for (const port of ports) {
|
|
2511
|
-
if (!Number.isFinite(port) || port <= 0) continue;
|
|
2512
|
-
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
2513
|
-
const cwd = listenerCwd(pid);
|
|
2514
|
-
if (cwd && isOtherWorktreeListenerCwd(cwd, worktreePath)) continue;
|
|
2515
|
-
signalPid(pid, signal);
|
|
659
|
+
function expandCanonicalGitRequest(prompt) {
|
|
660
|
+
const trimmed = prompt.trim();
|
|
661
|
+
let action = null;
|
|
662
|
+
let base = null;
|
|
663
|
+
for (const candidate of AGENT_GIT_ACTIONS) {
|
|
664
|
+
if (candidate === "resolve-conflicts") continue;
|
|
665
|
+
if (agentGitPrompt(candidate) === trimmed) {
|
|
666
|
+
action = candidate;
|
|
667
|
+
break;
|
|
2516
668
|
}
|
|
2517
669
|
}
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
if (process.platform === "win32") {
|
|
2524
|
-
void execa2("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
|
|
2525
|
-
} else {
|
|
2526
|
-
process.kill(-pid, "SIGTERM");
|
|
2527
|
-
setTimeout(() => {
|
|
2528
|
-
try {
|
|
2529
|
-
process.kill(-pid, "SIGKILL");
|
|
2530
|
-
} catch {
|
|
2531
|
-
}
|
|
2532
|
-
}, 2500).unref?.();
|
|
2533
|
-
}
|
|
2534
|
-
} catch {
|
|
2535
|
-
try {
|
|
2536
|
-
child.kill("SIGTERM");
|
|
2537
|
-
} catch {
|
|
2538
|
-
}
|
|
670
|
+
if (!action) {
|
|
671
|
+
const m = RESOLVE_CONFLICTS_RE.exec(trimmed);
|
|
672
|
+
if (m) {
|
|
673
|
+
action = "resolve-conflicts";
|
|
674
|
+
base = m[1] ?? null;
|
|
2539
675
|
}
|
|
2540
676
|
}
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
{
|
|
2547
|
-
worktreePath: opts.worktreePath,
|
|
2548
|
-
repoPath: opts.repoPath,
|
|
2549
|
-
workspaceName: opts.workspaceName,
|
|
2550
|
-
defaultBranch: opts.defaultBranch,
|
|
2551
|
-
ports: opts.ports
|
|
2552
|
-
},
|
|
2553
|
-
loginEnv
|
|
2554
|
-
);
|
|
2555
|
-
try {
|
|
2556
|
-
mergeAgentGitAuthEnv(
|
|
2557
|
-
env,
|
|
2558
|
-
await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
|
|
2559
|
-
);
|
|
2560
|
-
} catch {
|
|
2561
|
-
}
|
|
2562
|
-
if (opts.heldPorts?.length) {
|
|
2563
|
-
await Promise.all(opts.heldPorts.map((h) => h.release()));
|
|
677
|
+
let expansion = action ? gitActionExpansion(action, base) : null;
|
|
678
|
+
if (!expansion && trimmed === "Address review comments.") {
|
|
679
|
+
expansion = "Read the PR review feedback (`gh pr view --comments`, `gh api` review comments), make the requested changes, commit, and push.";
|
|
680
|
+
} else if (!expansion && FIX_CI_RE.test(trimmed)) {
|
|
681
|
+
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.";
|
|
2564
682
|
}
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
env,
|
|
2570
|
-
// Own process group so Stop can tear down the whole tree (not just the shell).
|
|
2571
|
-
// There is no settings.toml `stop=` / teardown hook for run scripts.
|
|
2572
|
-
...process.platform === "win32" ? {} : { detached: true }
|
|
2573
|
-
});
|
|
2574
|
-
pipeLines(child.stdout, opts.onLine);
|
|
2575
|
-
pipeLines(child.stderr, opts.onLine);
|
|
2576
|
-
const ports = opts.ports ?? [];
|
|
2577
|
-
return {
|
|
2578
|
-
pid: child.pid,
|
|
2579
|
-
kill: () => killScriptTree(child, ports),
|
|
2580
|
-
done: child.then((r) => r.exitCode ?? null),
|
|
2581
|
-
child
|
|
2582
|
-
};
|
|
683
|
+
if (!expansion) return prompt;
|
|
684
|
+
return `${trimmed}
|
|
685
|
+
|
|
686
|
+
(Sideboard git request \u2014 do not ask for clarification: ${expansion})`;
|
|
2583
687
|
}
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
}
|
|
2590
|
-
return handle.done;
|
|
688
|
+
var FIX_CI_RE = /^Fix CI: .+\.$/;
|
|
689
|
+
|
|
690
|
+
// src/git/pr-merge-archive.ts
|
|
691
|
+
function normalizePrState(state) {
|
|
692
|
+
return (state ?? "").trim().toUpperCase();
|
|
2591
693
|
}
|
|
2592
|
-
|
|
2593
|
-
const
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
const
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
defaultBranch: opts?.defaultBranch,
|
|
2601
|
-
onLine
|
|
2602
|
-
});
|
|
2603
|
-
const exitCode = await attachAbort(handle, opts?.signal);
|
|
2604
|
-
return {
|
|
2605
|
-
ran: true,
|
|
2606
|
-
exitCode,
|
|
2607
|
-
source,
|
|
2608
|
-
kill: handle.kill
|
|
2609
|
-
};
|
|
694
|
+
function samePrIdentity(a, b) {
|
|
695
|
+
const left = (a ?? "").trim().replace(/\/+$/, "");
|
|
696
|
+
const right = (b ?? "").trim().replace(/\/+$/, "");
|
|
697
|
+
if (!left || !right) return false;
|
|
698
|
+
if (left === right) return true;
|
|
699
|
+
const ln = left.match(/\/pull\/(\d+)/i)?.[1];
|
|
700
|
+
const rn = right.match(/\/pull\/(\d+)/i)?.[1];
|
|
701
|
+
return Boolean(ln && rn && ln === rn);
|
|
2610
702
|
}
|
|
2611
|
-
|
|
2612
|
-
const
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
source: found.source,
|
|
2626
|
-
kill: handle.kill
|
|
2627
|
-
};
|
|
703
|
+
function shouldStopRunOnPrRetarget(previousPrUrl, nextPrUrl) {
|
|
704
|
+
const prev = (previousPrUrl ?? "").trim();
|
|
705
|
+
const next = (nextPrUrl ?? "").trim();
|
|
706
|
+
if (!prev || !next) return false;
|
|
707
|
+
return !samePrIdentity(prev, next);
|
|
708
|
+
}
|
|
709
|
+
function shouldPersistFetchedPrMeta(opts) {
|
|
710
|
+
const meta = opts.metaUrl?.trim() || "";
|
|
711
|
+
if (!meta) return false;
|
|
712
|
+
const head = opts.headPrUrl?.trim() || "";
|
|
713
|
+
if (head) return samePrIdentity(meta, head);
|
|
714
|
+
const live = opts.livePrUrl?.trim() || "";
|
|
715
|
+
if (live) return samePrIdentity(meta, live);
|
|
716
|
+
return true;
|
|
2628
717
|
}
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
const
|
|
2632
|
-
|
|
2633
|
-
|
|
718
|
+
function sameLoginList(a, b) {
|
|
719
|
+
const left = [...a ?? []].map((s) => s.trim().toLowerCase()).filter(Boolean).sort();
|
|
720
|
+
const right = [...b ?? []].map((s) => s.trim().toLowerCase()).filter(Boolean).sort();
|
|
721
|
+
return left.length === right.length && left.every((login, i) => login === right[i]);
|
|
722
|
+
}
|
|
723
|
+
function threadPrMetaPatch(thread, meta) {
|
|
724
|
+
const prevState = normalizePrState(thread.prState);
|
|
725
|
+
const nextState = normalizePrState(meta.state);
|
|
726
|
+
const patch = {};
|
|
727
|
+
if (meta.url && meta.url !== thread.prUrl) patch.prUrl = meta.url;
|
|
728
|
+
if (meta.title && meta.title !== thread.prTitle) patch.prTitle = meta.title;
|
|
729
|
+
if (nextState && nextState !== prevState) patch.prState = nextState;
|
|
730
|
+
const nextDraft = Boolean(meta.isDraft) && nextState !== "MERGED" && nextState !== "CLOSED";
|
|
731
|
+
if (nextDraft !== Boolean(thread.prIsDraft)) patch.prIsDraft = nextDraft;
|
|
732
|
+
const nextAuthor = meta.authorLogin?.trim() || "";
|
|
733
|
+
if (nextAuthor && nextAuthor !== (thread.prAuthorLogin ?? "").trim()) {
|
|
734
|
+
patch.prAuthorLogin = nextAuthor;
|
|
2634
735
|
}
|
|
2635
|
-
|
|
2636
|
-
if (!
|
|
2637
|
-
|
|
736
|
+
const nextReviewers = meta.reviewerLogins ?? [];
|
|
737
|
+
if (!sameLoginList(nextReviewers, thread.prReviewerLogins)) {
|
|
738
|
+
patch.prReviewerLogins = nextReviewers;
|
|
2638
739
|
}
|
|
2639
|
-
|
|
2640
|
-
|
|
740
|
+
const nextDecision = meta.reviewDecision ?? null;
|
|
741
|
+
if ((thread.prReviewDecision ?? null) !== nextDecision) {
|
|
742
|
+
patch.prReviewDecision = nextDecision;
|
|
2641
743
|
}
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
2646
|
-
if (!settings?.archive) return { ran: false, exitCode: null };
|
|
2647
|
-
const handle = await spawnWorkspaceScript(settings.archive, {
|
|
2648
|
-
worktreePath,
|
|
2649
|
-
repoPath,
|
|
2650
|
-
onLine
|
|
2651
|
-
});
|
|
2652
|
-
const exitCode = await handle.done;
|
|
2653
|
-
return { ran: true, exitCode };
|
|
2654
|
-
}
|
|
2655
|
-
function listRunScripts(worktreePath, repoPath) {
|
|
2656
|
-
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
2657
|
-
if (!settings?.runScripts.length) return [];
|
|
2658
|
-
return settings.runScripts.filter((s) => {
|
|
2659
|
-
if (!s.availableIn?.length) return true;
|
|
2660
|
-
return s.availableIn.includes("local");
|
|
2661
|
-
});
|
|
2662
|
-
}
|
|
2663
|
-
function getDefaultRunScript(worktreePath, repoPath) {
|
|
2664
|
-
const scripts = listRunScripts(worktreePath, repoPath);
|
|
2665
|
-
if (!scripts.length) return null;
|
|
2666
|
-
return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
|
|
2667
|
-
}
|
|
2668
|
-
function getRunScript(worktreePath, repoPath, name) {
|
|
2669
|
-
const scripts = listRunScripts(worktreePath, repoPath);
|
|
2670
|
-
if (!scripts.length) return null;
|
|
2671
|
-
if (name) {
|
|
2672
|
-
return scripts.find((s) => s.name === name) ?? null;
|
|
2673
|
-
}
|
|
2674
|
-
return getDefaultRunScript(worktreePath, repoPath);
|
|
2675
|
-
}
|
|
2676
|
-
function getRunMode(worktreePath, repoPath) {
|
|
2677
|
-
return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
|
|
2678
|
-
}
|
|
2679
|
-
async function reservePort() {
|
|
2680
|
-
return new Promise((resolve2, reject) => {
|
|
2681
|
-
const server = createServer();
|
|
2682
|
-
let released = false;
|
|
2683
|
-
const release = () => new Promise((res) => {
|
|
2684
|
-
if (released) {
|
|
2685
|
-
res();
|
|
2686
|
-
return;
|
|
2687
|
-
}
|
|
2688
|
-
released = true;
|
|
2689
|
-
server.close(() => res());
|
|
2690
|
-
});
|
|
2691
|
-
server.listen(0, "127.0.0.1", () => {
|
|
2692
|
-
const addr = server.address();
|
|
2693
|
-
if (!addr || typeof addr === "string") {
|
|
2694
|
-
void release().then(() => reject(new Error("Failed to allocate port")));
|
|
2695
|
-
return;
|
|
2696
|
-
}
|
|
2697
|
-
resolve2({ port: addr.port, release });
|
|
2698
|
-
});
|
|
2699
|
-
server.on("error", reject);
|
|
2700
|
-
});
|
|
2701
|
-
}
|
|
2702
|
-
async function allocatePort() {
|
|
2703
|
-
const held = await reservePort();
|
|
2704
|
-
await held.release();
|
|
2705
|
-
return held.port;
|
|
2706
|
-
}
|
|
2707
|
-
async function tryReservePort(port) {
|
|
2708
|
-
if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
|
|
2709
|
-
return new Promise((resolve2) => {
|
|
2710
|
-
const server = createServer();
|
|
2711
|
-
let released = false;
|
|
2712
|
-
const release = () => new Promise((res) => {
|
|
2713
|
-
if (released) {
|
|
2714
|
-
res();
|
|
2715
|
-
return;
|
|
2716
|
-
}
|
|
2717
|
-
released = true;
|
|
2718
|
-
server.close(() => res());
|
|
2719
|
-
});
|
|
2720
|
-
server.once("error", () => {
|
|
2721
|
-
try {
|
|
2722
|
-
server.close();
|
|
2723
|
-
} catch {
|
|
2724
|
-
}
|
|
2725
|
-
resolve2(null);
|
|
2726
|
-
});
|
|
2727
|
-
server.listen(port, "127.0.0.1", () => {
|
|
2728
|
-
resolve2({ port, release });
|
|
2729
|
-
});
|
|
2730
|
-
});
|
|
2731
|
-
}
|
|
2732
|
-
async function tryReservePorts(ports) {
|
|
2733
|
-
const held = [];
|
|
2734
|
-
for (const port of ports) {
|
|
2735
|
-
const next = await tryReservePort(port);
|
|
2736
|
-
if (!next) {
|
|
2737
|
-
await Promise.all(held.map((h) => h.release()));
|
|
2738
|
-
return null;
|
|
2739
|
-
}
|
|
2740
|
-
held.push(next);
|
|
744
|
+
const nextMergeable = meta.mergeable ?? null;
|
|
745
|
+
if ((thread.prMergeable ?? null) !== nextMergeable) {
|
|
746
|
+
patch.prMergeable = nextMergeable;
|
|
2741
747
|
}
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
if (!ports?.length) return [];
|
|
2746
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2747
|
-
const out = [];
|
|
2748
|
-
for (const p of ports) {
|
|
2749
|
-
if (!Number.isInteger(p) || p <= 0 || p > 65535 || seen.has(p)) continue;
|
|
2750
|
-
seen.add(p);
|
|
2751
|
-
out.push(p);
|
|
2752
|
-
if (out.length >= size) break;
|
|
748
|
+
const nextMergeState = meta.mergeStateStatus ?? null;
|
|
749
|
+
if ((thread.prMergeStateStatus ?? null) !== nextMergeState) {
|
|
750
|
+
patch.prMergeStateStatus = nextMergeState;
|
|
2753
751
|
}
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2758
|
-
}
|
|
2759
|
-
async function fillPortRange(held, size) {
|
|
2760
|
-
if (held.length >= size) return held;
|
|
2761
|
-
const base = held[0]?.port;
|
|
2762
|
-
const taken = new Set(held.map((h) => h.port));
|
|
2763
|
-
try {
|
|
2764
|
-
let offset = 0;
|
|
2765
|
-
while (held.length < size) {
|
|
2766
|
-
const candidate = base != null ? base + offset : null;
|
|
2767
|
-
offset += 1;
|
|
2768
|
-
if (candidate != null && taken.has(candidate)) continue;
|
|
2769
|
-
const next = candidate != null ? await tryReservePort(candidate) : null;
|
|
2770
|
-
if (next) {
|
|
2771
|
-
held.push(next);
|
|
2772
|
-
taken.add(next.port);
|
|
2773
|
-
} else {
|
|
2774
|
-
const ephemeral = await reservePort();
|
|
2775
|
-
held.push(ephemeral);
|
|
2776
|
-
taken.add(ephemeral.port);
|
|
2777
|
-
}
|
|
2778
|
-
}
|
|
2779
|
-
return held;
|
|
2780
|
-
} catch (err) {
|
|
2781
|
-
await Promise.all(held.map((h) => h.release()));
|
|
2782
|
-
throw err;
|
|
752
|
+
const nextQueued = Boolean(meta.isInMergeQueue);
|
|
753
|
+
if (Boolean(thread.prIsInMergeQueue) !== nextQueued) {
|
|
754
|
+
patch.prIsInMergeQueue = nextQueued;
|
|
2783
755
|
}
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
for (const port of ports) {
|
|
2788
|
-
const next = await tryReservePort(port);
|
|
2789
|
-
if (next) held.push(next);
|
|
756
|
+
const nextLabels = meta.labels ?? [];
|
|
757
|
+
if (!sameLoginList(nextLabels, thread.prLabels)) {
|
|
758
|
+
patch.prLabels = nextLabels;
|
|
2790
759
|
}
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
const takePrimary = async () => {
|
|
2797
|
-
const held2 = await tryReserveAvailablePorts(ports);
|
|
2798
|
-
if (held2.some((h) => h.port === primary)) return held2;
|
|
2799
|
-
await Promise.all(held2.map((h) => h.release()));
|
|
2800
|
-
return null;
|
|
2801
|
-
};
|
|
2802
|
-
let held = await takePrimary();
|
|
2803
|
-
if (held) return held;
|
|
2804
|
-
if (worktreePath) {
|
|
2805
|
-
killStaleWorktreeListeners(ports, worktreePath, "SIGTERM");
|
|
2806
|
-
} else {
|
|
2807
|
-
killListenersOnPorts(ports);
|
|
760
|
+
const mergeState = (nextMergeState ?? "").toUpperCase();
|
|
761
|
+
if (mergeState === "CLEAN" && thread.prChecksFailed) {
|
|
762
|
+
patch.prChecksFailed = false;
|
|
763
|
+
} else if ((mergeState === "UNSTABLE" || mergeState === "BLOCKED" && (nextDecision ?? "").toUpperCase() === "APPROVED") && !thread.prChecksFailed) {
|
|
764
|
+
patch.prChecksFailed = true;
|
|
2808
765
|
}
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
held = await takePrimary();
|
|
2812
|
-
if (held) return held;
|
|
2813
|
-
if (i === 2 && worktreePath) {
|
|
2814
|
-
killStaleWorktreeListeners(ports, worktreePath, "SIGKILL");
|
|
2815
|
-
}
|
|
766
|
+
if (thread.skipAutoArchiveOnMerge && nextState && nextState !== "MERGED" && nextState !== "CLOSED") {
|
|
767
|
+
patch.skipAutoArchiveOnMerge = false;
|
|
2816
768
|
}
|
|
2817
|
-
return
|
|
769
|
+
return patch;
|
|
2818
770
|
}
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
await Promise.all(held.map((h) => h.release()));
|
|
2832
|
-
return ports;
|
|
2833
|
-
}
|
|
2834
|
-
async function startDevServer(repoPath, worktreePath, onLine, opts) {
|
|
2835
|
-
const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
|
|
2836
|
-
if (!script) return null;
|
|
2837
|
-
const preferred = loadWorktreeRunPorts(worktreePath, script.name) ?? opts?.preferredPorts;
|
|
2838
|
-
const held = await reservePortRange(PORT_RANGE_SIZE, {
|
|
2839
|
-
preferred: preferred ?? void 0,
|
|
2840
|
-
worktreePath
|
|
2841
|
-
});
|
|
2842
|
-
const ports = held.map((h) => h.port);
|
|
2843
|
-
saveWorktreeRunPorts(worktreePath, script.name, ports);
|
|
2844
|
-
try {
|
|
2845
|
-
const handle = await spawnWorkspaceScript(script.command, {
|
|
2846
|
-
worktreePath,
|
|
2847
|
-
repoPath,
|
|
2848
|
-
ports,
|
|
2849
|
-
heldPorts: held,
|
|
2850
|
-
defaultBranch: opts?.defaultBranch,
|
|
2851
|
-
onLine
|
|
2852
|
-
});
|
|
2853
|
-
return {
|
|
2854
|
-
pid: handle.pid,
|
|
2855
|
-
port: ports[0],
|
|
2856
|
-
ports,
|
|
2857
|
-
scriptName: script.name,
|
|
2858
|
-
kill: handle.kill,
|
|
2859
|
-
done: handle.done
|
|
2860
|
-
};
|
|
2861
|
-
} catch (err) {
|
|
2862
|
-
await Promise.all(held.map((h) => h.release()));
|
|
2863
|
-
throw err;
|
|
2864
|
-
}
|
|
771
|
+
function threadPrChecksFailedPatch(thread, failed) {
|
|
772
|
+
if (thread.prChecksFailed === failed) return {};
|
|
773
|
+
return { prChecksFailed: failed };
|
|
774
|
+
}
|
|
775
|
+
function shouldAutoArchiveOnPrMerge(opts) {
|
|
776
|
+
if (!opts.autoArchiveEnabled) return false;
|
|
777
|
+
if (opts.isGlobal) return false;
|
|
778
|
+
if (opts.skipAutoArchiveOnMerge) return false;
|
|
779
|
+
if (opts.threadStatus === "archived") return false;
|
|
780
|
+
const next = normalizePrState(opts.nextPrState);
|
|
781
|
+
if (next !== "MERGED") return false;
|
|
782
|
+
return normalizePrState(opts.previousPrState) !== "MERGED";
|
|
2865
783
|
}
|
|
2866
784
|
|
|
2867
785
|
// src/store/setup-log.ts
|
|
2868
786
|
import { createHash } from "crypto";
|
|
2869
|
-
import { existsSync
|
|
2870
|
-
import { join as
|
|
787
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
788
|
+
import { join as join2 } from "path";
|
|
2871
789
|
var EMPTY = {
|
|
2872
790
|
output: "",
|
|
2873
791
|
running: false,
|
|
@@ -2909,7 +827,7 @@ function materialize(mem) {
|
|
|
2909
827
|
};
|
|
2910
828
|
}
|
|
2911
829
|
function setupLogPath(threadId) {
|
|
2912
|
-
return
|
|
830
|
+
return join2(threadsDir(), `${threadId}.setup.log.json`);
|
|
2913
831
|
}
|
|
2914
832
|
function setupLogKeyForWorktree(worktreePath) {
|
|
2915
833
|
const norm = normalizeWorktreePath(worktreePath);
|
|
@@ -2936,7 +854,7 @@ function persistNow(threadId) {
|
|
|
2936
854
|
const mem = memory.get(threadId);
|
|
2937
855
|
if (!mem) return;
|
|
2938
856
|
try {
|
|
2939
|
-
|
|
857
|
+
writeFileSync(
|
|
2940
858
|
setupLogPath(threadId),
|
|
2941
859
|
`${JSON.stringify(materialize(mem))}
|
|
2942
860
|
`,
|
|
@@ -2957,9 +875,9 @@ function schedulePersist(threadId) {
|
|
|
2957
875
|
}
|
|
2958
876
|
function loadFromDisk(threadId) {
|
|
2959
877
|
const path = setupLogPath(threadId);
|
|
2960
|
-
if (!
|
|
878
|
+
if (!existsSync(path)) return null;
|
|
2961
879
|
try {
|
|
2962
|
-
const raw = JSON.parse(
|
|
880
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
2963
881
|
return {
|
|
2964
882
|
output: typeof raw.output === "string" ? raw.output : "",
|
|
2965
883
|
running: Boolean(raw.running),
|
|
@@ -3018,8 +936,8 @@ function finishSetupLog(threadId, exitCode, source) {
|
|
|
3018
936
|
|
|
3019
937
|
// src/store/run-log.ts
|
|
3020
938
|
import { createHash as createHash2 } from "crypto";
|
|
3021
|
-
import { existsSync as
|
|
3022
|
-
import { join as
|
|
939
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
940
|
+
import { join as join3 } from "path";
|
|
3023
941
|
var EMPTY2 = {
|
|
3024
942
|
output: "",
|
|
3025
943
|
running: false,
|
|
@@ -3058,7 +976,7 @@ function safeScriptToken(scriptName) {
|
|
|
3058
976
|
return token || "dev";
|
|
3059
977
|
}
|
|
3060
978
|
function runLogPath(key) {
|
|
3061
|
-
return
|
|
979
|
+
return join3(threadsDir(), `${key}.run.log.json`);
|
|
3062
980
|
}
|
|
3063
981
|
function runLogKeyForWorktree(worktreePath, scriptName) {
|
|
3064
982
|
const norm = normalizeWorktreePath(worktreePath);
|
|
@@ -3087,7 +1005,7 @@ function persistNow2(key) {
|
|
|
3087
1005
|
const mem = memory2.get(key);
|
|
3088
1006
|
if (!mem) return;
|
|
3089
1007
|
try {
|
|
3090
|
-
|
|
1008
|
+
writeFileSync2(runLogPath(key), `${JSON.stringify(materialize2(mem))}
|
|
3091
1009
|
`, "utf8");
|
|
3092
1010
|
} catch {
|
|
3093
1011
|
}
|
|
@@ -3104,9 +1022,9 @@ function schedulePersist2(key) {
|
|
|
3104
1022
|
}
|
|
3105
1023
|
function loadFromDisk2(key) {
|
|
3106
1024
|
const path = runLogPath(key);
|
|
3107
|
-
if (!
|
|
1025
|
+
if (!existsSync2(path)) return null;
|
|
3108
1026
|
try {
|
|
3109
|
-
const raw = JSON.parse(
|
|
1027
|
+
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
3110
1028
|
return {
|
|
3111
1029
|
output: typeof raw.output === "string" ? raw.output : "",
|
|
3112
1030
|
running: Boolean(raw.running),
|
|
@@ -3166,8 +1084,8 @@ function flushRunLog(key) {
|
|
|
3166
1084
|
}
|
|
3167
1085
|
|
|
3168
1086
|
// src/git/orphan-cleanup.ts
|
|
3169
|
-
import { existsSync as
|
|
3170
|
-
import { join as
|
|
1087
|
+
import { existsSync as existsSync3, readdirSync, statSync } from "fs";
|
|
1088
|
+
import { join as join4 } from "path";
|
|
3171
1089
|
function isSideboardWorktreePath(path) {
|
|
3172
1090
|
return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
|
|
3173
1091
|
}
|
|
@@ -3178,9 +1096,9 @@ async function findOrphanWorktrees(repoPaths) {
|
|
|
3178
1096
|
repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
|
|
3179
1097
|
);
|
|
3180
1098
|
const homeRoot = sideboardWorkspacesDir();
|
|
3181
|
-
if (
|
|
1099
|
+
if (existsSync3(homeRoot)) {
|
|
3182
1100
|
try {
|
|
3183
|
-
for (const entry of
|
|
1101
|
+
for (const entry of readdirSync(homeRoot, { withFileTypes: true })) {
|
|
3184
1102
|
if (!entry.isDirectory()) continue;
|
|
3185
1103
|
void entry;
|
|
3186
1104
|
}
|
|
@@ -3190,7 +1108,7 @@ async function findOrphanWorktrees(repoPaths) {
|
|
|
3190
1108
|
const orphans = [];
|
|
3191
1109
|
const seen = /* @__PURE__ */ new Set();
|
|
3192
1110
|
for (const repoPath of repos) {
|
|
3193
|
-
if (!repoPath || !
|
|
1111
|
+
if (!repoPath || !existsSync3(repoPath)) continue;
|
|
3194
1112
|
try {
|
|
3195
1113
|
const wts = await listWorktrees(repoPath);
|
|
3196
1114
|
for (const wt of wts) {
|
|
@@ -3211,12 +1129,12 @@ async function findOrphanWorktrees(repoPaths) {
|
|
|
3211
1129
|
}
|
|
3212
1130
|
try {
|
|
3213
1131
|
const root = worktreesRoot(repoPath);
|
|
3214
|
-
if (
|
|
3215
|
-
for (const entry of
|
|
1132
|
+
if (existsSync3(root)) {
|
|
1133
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
3216
1134
|
if (!entry.isDirectory()) continue;
|
|
3217
|
-
const path =
|
|
1135
|
+
const path = join4(root, entry.name).replace(/\/$/, "");
|
|
3218
1136
|
if (known.has(path) || seen.has(path)) continue;
|
|
3219
|
-
if (!
|
|
1137
|
+
if (!existsSync3(join4(path, ".git"))) continue;
|
|
3220
1138
|
seen.add(path);
|
|
3221
1139
|
let mtimeMs = 0;
|
|
3222
1140
|
try {
|
|
@@ -3348,11 +1266,11 @@ function shouldRunHistoryCleanup(settings = loadAppSettings()) {
|
|
|
3348
1266
|
}
|
|
3349
1267
|
|
|
3350
1268
|
// src/git/apply-into-main.ts
|
|
3351
|
-
import { execa as
|
|
1269
|
+
import { execa as execa2 } from "execa";
|
|
3352
1270
|
async function applyThreadIntoMain(thread, opts) {
|
|
3353
1271
|
const method = opts?.method ?? "merge";
|
|
3354
1272
|
const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
|
|
3355
|
-
const status = await
|
|
1273
|
+
const status = await execa2("git", ["status", "--porcelain"], {
|
|
3356
1274
|
cwd: thread.repoPath,
|
|
3357
1275
|
reject: false
|
|
3358
1276
|
});
|
|
@@ -3361,7 +1279,7 @@ async function applyThreadIntoMain(thread, opts) {
|
|
|
3361
1279
|
`Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
|
|
3362
1280
|
);
|
|
3363
1281
|
}
|
|
3364
|
-
const checkout = await
|
|
1282
|
+
const checkout = await execa2("git", ["checkout", target], {
|
|
3365
1283
|
cwd: thread.repoPath,
|
|
3366
1284
|
reject: false
|
|
3367
1285
|
});
|
|
@@ -3371,7 +1289,7 @@ async function applyThreadIntoMain(thread, opts) {
|
|
|
3371
1289
|
);
|
|
3372
1290
|
}
|
|
3373
1291
|
if (method === "cherry-pick") {
|
|
3374
|
-
const log = await
|
|
1292
|
+
const log = await execa2(
|
|
3375
1293
|
"git",
|
|
3376
1294
|
["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
|
|
3377
1295
|
{ cwd: thread.repoPath, reject: false }
|
|
@@ -3386,12 +1304,12 @@ async function applyThreadIntoMain(thread, opts) {
|
|
|
3386
1304
|
};
|
|
3387
1305
|
}
|
|
3388
1306
|
for (const sha of shas) {
|
|
3389
|
-
const cp = await
|
|
1307
|
+
const cp = await execa2("git", ["cherry-pick", sha], {
|
|
3390
1308
|
cwd: thread.repoPath,
|
|
3391
1309
|
reject: false
|
|
3392
1310
|
});
|
|
3393
1311
|
if (cp.exitCode !== 0) {
|
|
3394
|
-
await
|
|
1312
|
+
await execa2("git", ["cherry-pick", "--abort"], {
|
|
3395
1313
|
cwd: thread.repoPath,
|
|
3396
1314
|
reject: false
|
|
3397
1315
|
});
|
|
@@ -3407,13 +1325,13 @@ async function applyThreadIntoMain(thread, opts) {
|
|
|
3407
1325
|
message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
|
|
3408
1326
|
};
|
|
3409
1327
|
}
|
|
3410
|
-
const merge = await
|
|
1328
|
+
const merge = await execa2(
|
|
3411
1329
|
"git",
|
|
3412
1330
|
["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
|
|
3413
1331
|
{ cwd: thread.repoPath, reject: false }
|
|
3414
1332
|
);
|
|
3415
1333
|
if (merge.exitCode !== 0) {
|
|
3416
|
-
await
|
|
1334
|
+
await execa2("git", ["merge", "--abort"], {
|
|
3417
1335
|
cwd: thread.repoPath,
|
|
3418
1336
|
reject: false
|
|
3419
1337
|
});
|
|
@@ -3430,25 +1348,25 @@ async function applyThreadIntoMain(thread, opts) {
|
|
|
3430
1348
|
}
|
|
3431
1349
|
|
|
3432
1350
|
// src/git/clone-repo.ts
|
|
3433
|
-
import { existsSync as
|
|
3434
|
-
import { basename
|
|
3435
|
-
import { execa as
|
|
1351
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1352
|
+
import { basename, join as join5 } from "path";
|
|
1353
|
+
import { execa as execa3 } from "execa";
|
|
3436
1354
|
async function cloneRepoIntoSideboard(opts) {
|
|
3437
1355
|
const url = opts.url.trim();
|
|
3438
1356
|
if (!url) throw new Error("Clone URL is required");
|
|
3439
1357
|
let name = opts.name?.trim();
|
|
3440
1358
|
if (!name) {
|
|
3441
|
-
const leaf =
|
|
1359
|
+
const leaf = basename(url.replace(/\/$/, "").replace(/\.git$/, ""));
|
|
3442
1360
|
name = leaf || "repo";
|
|
3443
1361
|
}
|
|
3444
1362
|
name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
3445
|
-
const dest =
|
|
3446
|
-
if (
|
|
1363
|
+
const dest = join5(sideboardReposDir(), name);
|
|
1364
|
+
if (existsSync4(dest)) {
|
|
3447
1365
|
const repoPath2 = await resolveRepoRoot(dest);
|
|
3448
1366
|
const workspace2 = await ensureWorkspace(repoPath2);
|
|
3449
1367
|
return { repoPath: repoPath2, workspace: workspace2 };
|
|
3450
1368
|
}
|
|
3451
|
-
const result = await
|
|
1369
|
+
const result = await execa3("git", ["clone", url, dest], { reject: false });
|
|
3452
1370
|
if (result.exitCode !== 0) {
|
|
3453
1371
|
throw new Error(
|
|
3454
1372
|
`git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
|
|
@@ -3468,13 +1386,13 @@ function isAdoptableRunScriptRequest(req) {
|
|
|
3468
1386
|
}
|
|
3469
1387
|
|
|
3470
1388
|
// src/store/desktop-host.ts
|
|
3471
|
-
import { readFileSync as
|
|
3472
|
-
import { join as
|
|
1389
|
+
import { readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1390
|
+
import { join as join6 } from "path";
|
|
3473
1391
|
function desktopHostPidPath() {
|
|
3474
|
-
return
|
|
1392
|
+
return join6(appDataDir(), "desktop-host.pid");
|
|
3475
1393
|
}
|
|
3476
1394
|
function claimDesktopHost(pid = process.pid) {
|
|
3477
|
-
|
|
1395
|
+
writeFileSync3(desktopHostPidPath(), `${pid}
|
|
3478
1396
|
`, "utf8");
|
|
3479
1397
|
}
|
|
3480
1398
|
function releaseDesktopHost(pid = process.pid) {
|
|
@@ -3486,7 +1404,7 @@ function releaseDesktopHost(pid = process.pid) {
|
|
|
3486
1404
|
}
|
|
3487
1405
|
function readDesktopHostPid() {
|
|
3488
1406
|
try {
|
|
3489
|
-
const pid = Number.parseInt(
|
|
1407
|
+
const pid = Number.parseInt(readFileSync3(desktopHostPidPath(), "utf8").trim(), 10);
|
|
3490
1408
|
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
3491
1409
|
return pid;
|
|
3492
1410
|
} catch {
|
|
@@ -3582,7 +1500,7 @@ function agentPurposeStamp(input) {
|
|
|
3582
1500
|
}
|
|
3583
1501
|
|
|
3584
1502
|
// src/threads/create.ts
|
|
3585
|
-
import { existsSync as
|
|
1503
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3586
1504
|
|
|
3587
1505
|
// src/detect/detect.ts
|
|
3588
1506
|
async function detectAgents() {
|
|
@@ -4273,7 +2191,7 @@ function reuseLiveThread(input, repoPath, match) {
|
|
|
4273
2191
|
}
|
|
4274
2192
|
async function createThread(input, _onSetupLine) {
|
|
4275
2193
|
const repoPath = await resolveRepoRoot(input.repoPath);
|
|
4276
|
-
if (!
|
|
2194
|
+
if (!existsSync5(repoPath)) {
|
|
4277
2195
|
throw new Error(`Repo not found: ${repoPath}`);
|
|
4278
2196
|
}
|
|
4279
2197
|
const reused = reuseLiveThread(input, repoPath, {
|
|
@@ -4290,7 +2208,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
4290
2208
|
});
|
|
4291
2209
|
await requireAgent(resolved.agent);
|
|
4292
2210
|
if (input.cowboy) {
|
|
4293
|
-
const { cowboyModeEnabled } = await import("./app-settings-
|
|
2211
|
+
const { cowboyModeEnabled } = await import("./app-settings-L5HGYXWJ.js");
|
|
4294
2212
|
if (!cowboyModeEnabled()) {
|
|
4295
2213
|
throw new Error(
|
|
4296
2214
|
"Cowboy mode is off. Enable it in Settings \u2192 Advanced."
|
|
@@ -4348,7 +2266,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
4348
2266
|
let sourceIsFork = false;
|
|
4349
2267
|
if (!input.cowboy && sourceType === "branch" && (!sourceRef || sourceRef === "HEAD" || sourceRef === "default")) {
|
|
4350
2268
|
if (isAbleTimeConnected() && getIssueSource() === "abletime") {
|
|
4351
|
-
const { ensureAbleTimeTask, issueAttachmentForAbleTimeTask } = await import("./abletime-
|
|
2269
|
+
const { ensureAbleTimeTask, issueAttachmentForAbleTimeTask } = await import("./abletime-JNQAHLHH.js");
|
|
4352
2270
|
const task = await ensureAbleTimeTask({
|
|
4353
2271
|
title: title?.trim() || "Untitled work",
|
|
4354
2272
|
description: input.prompt?.trim() || void 0
|
|
@@ -4458,7 +2376,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
4458
2376
|
return readThread(thread.id) ?? thread;
|
|
4459
2377
|
}
|
|
4460
2378
|
async function listLinearIssues(agent, repoPath) {
|
|
4461
|
-
const { getAdapter: getAdapter2 } = await import("./agents-
|
|
2379
|
+
const { getAdapter: getAdapter2 } = await import("./agents-G3JTULM2.js");
|
|
4462
2380
|
await requireAgent(agent, { requireLinear: true });
|
|
4463
2381
|
const adapter = getAdapter2(agent);
|
|
4464
2382
|
if (!adapter.listLinearIssues) {
|
|
@@ -4519,11 +2437,11 @@ function createLineCoalescer(onChunk, opts) {
|
|
|
4519
2437
|
|
|
4520
2438
|
// src/store/turn-live.ts
|
|
4521
2439
|
import {
|
|
4522
|
-
existsSync as
|
|
2440
|
+
existsSync as existsSync6,
|
|
4523
2441
|
renameSync,
|
|
4524
2442
|
unlinkSync as unlinkSync2,
|
|
4525
|
-
writeFileSync as
|
|
4526
|
-
readFileSync as
|
|
2443
|
+
writeFileSync as writeFileSync4,
|
|
2444
|
+
readFileSync as readFileSync4
|
|
4527
2445
|
} from "fs";
|
|
4528
2446
|
var buffers = /* @__PURE__ */ new Map();
|
|
4529
2447
|
var FLUSH_MS = 800;
|
|
@@ -4600,7 +2518,7 @@ function writeTurnLive(threadId, progress) {
|
|
|
4600
2518
|
const path = threadLivePath(threadId);
|
|
4601
2519
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
4602
2520
|
try {
|
|
4603
|
-
|
|
2521
|
+
writeFileSync4(tmp, JSON.stringify(progress), "utf8");
|
|
4604
2522
|
renameSync(tmp, path);
|
|
4605
2523
|
} catch {
|
|
4606
2524
|
try {
|
|
@@ -4611,9 +2529,9 @@ function writeTurnLive(threadId, progress) {
|
|
|
4611
2529
|
}
|
|
4612
2530
|
function readTurnLive(threadId) {
|
|
4613
2531
|
const path = threadLivePath(threadId);
|
|
4614
|
-
if (!
|
|
2532
|
+
if (!existsSync6(path)) return null;
|
|
4615
2533
|
try {
|
|
4616
|
-
const raw = JSON.parse(
|
|
2534
|
+
const raw = JSON.parse(readFileSync4(path, "utf8"));
|
|
4617
2535
|
if (!raw || typeof raw.summary !== "string") return null;
|
|
4618
2536
|
return raw;
|
|
4619
2537
|
} catch {
|
|
@@ -4625,7 +2543,7 @@ function clearTurnLive(threadId) {
|
|
|
4625
2543
|
if (buf?.timer) clearTimeout(buf.timer);
|
|
4626
2544
|
buffers.delete(threadId);
|
|
4627
2545
|
const path = threadLivePath(threadId);
|
|
4628
|
-
if (!
|
|
2546
|
+
if (!existsSync6(path)) return;
|
|
4629
2547
|
try {
|
|
4630
2548
|
unlinkSync2(path);
|
|
4631
2549
|
} catch {
|
|
@@ -4662,13 +2580,13 @@ function isStaleLastErrorDuringTurn(err) {
|
|
|
4662
2580
|
}
|
|
4663
2581
|
|
|
4664
2582
|
// src/threads/fork-worktree.ts
|
|
4665
|
-
function
|
|
2583
|
+
function requireThread(idOrRef) {
|
|
4666
2584
|
const thread = findThreadByRef(idOrRef) ?? null;
|
|
4667
2585
|
if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
|
|
4668
2586
|
return thread;
|
|
4669
2587
|
}
|
|
4670
2588
|
async function forkThreadWorktree(input, onSetupLine) {
|
|
4671
|
-
const from =
|
|
2589
|
+
const from = requireThread(input.threadId);
|
|
4672
2590
|
if (isOrchestratorThread(from)) {
|
|
4673
2591
|
throw new Error(
|
|
4674
2592
|
"fork_workspace targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
|
|
@@ -4705,7 +2623,7 @@ async function forkThreadWorktree(input, onSetupLine) {
|
|
|
4705
2623
|
}
|
|
4706
2624
|
|
|
4707
2625
|
// src/orchestrator/quota-failover.ts
|
|
4708
|
-
import { randomUUID
|
|
2626
|
+
import { randomUUID } from "crypto";
|
|
4709
2627
|
function managedKindForAgent(agent) {
|
|
4710
2628
|
return agent === "claude" || agent === "codex" ? agent : null;
|
|
4711
2629
|
}
|
|
@@ -4825,7 +2743,7 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
|
|
|
4825
2743
|
...instructions
|
|
4826
2744
|
].join("\n");
|
|
4827
2745
|
return {
|
|
4828
|
-
id:
|
|
2746
|
+
id: randomUUID(),
|
|
4829
2747
|
name: orch ? "Orchestration quota handoff.md" : "Quota handoff.md",
|
|
4830
2748
|
kind: "transcript",
|
|
4831
2749
|
content: body
|
|
@@ -4840,9 +2758,9 @@ var QUOTA_CONTINUE_ACCOUNT_PROMPT = (fromAgent, accountLabel) => [
|
|
|
4840
2758
|
"Proceed with the goal. Leave model Auto unless needed."
|
|
4841
2759
|
].join(" ");
|
|
4842
2760
|
var QUOTA_RESUME_PROMPT = "Session/usage limit window should have reset. Continue this chat from where you left off.";
|
|
4843
|
-
async function isolateQuotaFailover(
|
|
2761
|
+
async function isolateQuotaFailover(run) {
|
|
4844
2762
|
try {
|
|
4845
|
-
return await
|
|
2763
|
+
return await run();
|
|
4846
2764
|
} catch {
|
|
4847
2765
|
return true;
|
|
4848
2766
|
}
|
|
@@ -4868,29 +2786,29 @@ function createQuotaFailoverChat(from, fallbackAgent, limitText, opts) {
|
|
|
4868
2786
|
}
|
|
4869
2787
|
|
|
4870
2788
|
// src/threads/adopt.ts
|
|
4871
|
-
import { execFileSync
|
|
2789
|
+
import { execFileSync } from "child_process";
|
|
4872
2790
|
import {
|
|
4873
|
-
copyFileSync
|
|
4874
|
-
existsSync as
|
|
2791
|
+
copyFileSync,
|
|
2792
|
+
existsSync as existsSync7,
|
|
4875
2793
|
mkdtempSync,
|
|
4876
|
-
readdirSync as
|
|
4877
|
-
readFileSync as
|
|
2794
|
+
readdirSync as readdirSync2,
|
|
2795
|
+
readFileSync as readFileSync5,
|
|
4878
2796
|
rmSync
|
|
4879
2797
|
} from "fs";
|
|
4880
2798
|
import { tmpdir } from "os";
|
|
4881
|
-
import { join as
|
|
2799
|
+
import { join as join7 } from "path";
|
|
4882
2800
|
import { createRequire } from "module";
|
|
4883
|
-
var CONDUCTOR_APP_SUPPORT =
|
|
2801
|
+
var CONDUCTOR_APP_SUPPORT = join7(
|
|
4884
2802
|
process.env.HOME ?? "",
|
|
4885
2803
|
"Library",
|
|
4886
2804
|
"Application Support",
|
|
4887
2805
|
"com.conductor.app"
|
|
4888
2806
|
);
|
|
4889
|
-
var CONDUCTOR_DB =
|
|
4890
|
-
var CURSOR_SDK_STORE =
|
|
2807
|
+
var CONDUCTOR_DB = join7(CONDUCTOR_APP_SUPPORT, "conductor.db");
|
|
2808
|
+
var CURSOR_SDK_STORE = join7(CONDUCTOR_APP_SUPPORT, "cursor-sdk-store");
|
|
4891
2809
|
function thisModuleFile() {
|
|
4892
2810
|
const cjsFile = typeof __filename !== "undefined" ? __filename : "";
|
|
4893
|
-
return cjsFile || process.argv[1] ||
|
|
2811
|
+
return cjsFile || process.argv[1] || join7(process.cwd(), "package.json");
|
|
4894
2812
|
}
|
|
4895
2813
|
function openReadonlySqlite(file) {
|
|
4896
2814
|
const req = createRequire(thisModuleFile());
|
|
@@ -4908,21 +2826,21 @@ function mapAgentType(raw) {
|
|
|
4908
2826
|
return null;
|
|
4909
2827
|
}
|
|
4910
2828
|
function resolveConductorCursorAgentId(workspacePath) {
|
|
4911
|
-
if (!workspacePath || !
|
|
2829
|
+
if (!workspacePath || !existsSync7(CURSOR_SDK_STORE)) return null;
|
|
4912
2830
|
const normalized = workspacePath.replace(/\/$/, "");
|
|
4913
2831
|
let best = null;
|
|
4914
2832
|
let hashes;
|
|
4915
2833
|
try {
|
|
4916
|
-
hashes =
|
|
2834
|
+
hashes = readdirSync2(CURSOR_SDK_STORE);
|
|
4917
2835
|
} catch {
|
|
4918
2836
|
return null;
|
|
4919
2837
|
}
|
|
4920
2838
|
for (const hash of hashes) {
|
|
4921
|
-
const agentsFile =
|
|
4922
|
-
if (!
|
|
2839
|
+
const agentsFile = join7(CURSOR_SDK_STORE, hash, "agents.ndjson");
|
|
2840
|
+
if (!existsSync7(agentsFile)) continue;
|
|
4923
2841
|
let text;
|
|
4924
2842
|
try {
|
|
4925
|
-
text =
|
|
2843
|
+
text = readFileSync5(agentsFile, "utf8");
|
|
4926
2844
|
} catch {
|
|
4927
2845
|
continue;
|
|
4928
2846
|
}
|
|
@@ -4946,7 +2864,7 @@ function resolveConductorCursorAgentId(workspacePath) {
|
|
|
4946
2864
|
return best?.agentId ?? null;
|
|
4947
2865
|
}
|
|
4948
2866
|
async function adoptThread(input) {
|
|
4949
|
-
if (!
|
|
2867
|
+
if (!existsSync7(input.worktreePath)) {
|
|
4950
2868
|
throw new Error(`Worktree not found: ${input.worktreePath}`);
|
|
4951
2869
|
}
|
|
4952
2870
|
const repoPath = await resolveRepoRoot(input.worktreePath);
|
|
@@ -4965,28 +2883,28 @@ async function adoptThread(input) {
|
|
|
4965
2883
|
messages: input.messages ?? []
|
|
4966
2884
|
});
|
|
4967
2885
|
writeThread(thread);
|
|
4968
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
2886
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-F33Z4CEY.js");
|
|
4969
2887
|
await ensureWorkspace2(repoPath);
|
|
4970
|
-
const { ensureWorktreeSideboardIgnored
|
|
4971
|
-
await
|
|
2888
|
+
const { ensureWorktreeSideboardIgnored } = await import("./worktree-exclude-RXZPTCNH.js");
|
|
2889
|
+
await ensureWorktreeSideboardIgnored(input.worktreePath);
|
|
4972
2890
|
return thread;
|
|
4973
2891
|
}
|
|
4974
2892
|
function conductorDbPath() {
|
|
4975
2893
|
return CONDUCTOR_DB;
|
|
4976
2894
|
}
|
|
4977
2895
|
function listConductorWorkspaces() {
|
|
4978
|
-
if (!
|
|
2896
|
+
if (!existsSync7(CONDUCTOR_DB)) {
|
|
4979
2897
|
throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
|
|
4980
2898
|
}
|
|
4981
|
-
const tmp = mkdtempSync(
|
|
4982
|
-
const snapshot =
|
|
2899
|
+
const tmp = mkdtempSync(join7(tmpdir(), "sideboard-conductor-"));
|
|
2900
|
+
const snapshot = join7(tmp, "conductor.db");
|
|
4983
2901
|
try {
|
|
4984
|
-
|
|
2902
|
+
copyFileSync(CONDUCTOR_DB, snapshot);
|
|
4985
2903
|
for (const suffix of ["-wal", "-shm"]) {
|
|
4986
2904
|
const src = `${CONDUCTOR_DB}${suffix}`;
|
|
4987
|
-
if (
|
|
2905
|
+
if (existsSync7(src)) {
|
|
4988
2906
|
try {
|
|
4989
|
-
|
|
2907
|
+
copyFileSync(src, `${snapshot}${suffix}`);
|
|
4990
2908
|
} catch {
|
|
4991
2909
|
}
|
|
4992
2910
|
}
|
|
@@ -5066,18 +2984,18 @@ function listConductorWorkspaces() {
|
|
|
5066
2984
|
}
|
|
5067
2985
|
}
|
|
5068
2986
|
function importConductorWorkspace(workspaceId) {
|
|
5069
|
-
if (!
|
|
2987
|
+
if (!existsSync7(CONDUCTOR_DB)) {
|
|
5070
2988
|
throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
|
|
5071
2989
|
}
|
|
5072
|
-
const tmp = mkdtempSync(
|
|
5073
|
-
const snapshot =
|
|
2990
|
+
const tmp = mkdtempSync(join7(tmpdir(), "sideboard-conductor-"));
|
|
2991
|
+
const snapshot = join7(tmp, "conductor.db");
|
|
5074
2992
|
try {
|
|
5075
|
-
|
|
2993
|
+
copyFileSync(CONDUCTOR_DB, snapshot);
|
|
5076
2994
|
for (const suffix of ["-wal", "-shm"]) {
|
|
5077
2995
|
const src = `${CONDUCTOR_DB}${suffix}`;
|
|
5078
|
-
if (
|
|
2996
|
+
if (existsSync7(src)) {
|
|
5079
2997
|
try {
|
|
5080
|
-
|
|
2998
|
+
copyFileSync(src, `${snapshot}${suffix}`);
|
|
5081
2999
|
} catch {
|
|
5082
3000
|
}
|
|
5083
3001
|
}
|
|
@@ -5095,7 +3013,7 @@ function importConductorWorkspace(workspaceId) {
|
|
|
5095
3013
|
).get(workspaceId);
|
|
5096
3014
|
if (!row) throw new Error(`Conductor workspace not found: ${workspaceId}`);
|
|
5097
3015
|
const worktreePath = String(row.workspacePath);
|
|
5098
|
-
if (!
|
|
3016
|
+
if (!existsSync7(worktreePath)) {
|
|
5099
3017
|
throw new Error(`Conductor worktree missing on disk: ${worktreePath}`);
|
|
5100
3018
|
}
|
|
5101
3019
|
let sessionId = null;
|
|
@@ -5135,7 +3053,7 @@ function importConductorWorkspace(workspaceId) {
|
|
|
5135
3053
|
}
|
|
5136
3054
|
let repoPath = worktreePath;
|
|
5137
3055
|
try {
|
|
5138
|
-
repoPath =
|
|
3056
|
+
repoPath = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
5139
3057
|
cwd: worktreePath,
|
|
5140
3058
|
encoding: "utf8"
|
|
5141
3059
|
}).trim();
|
|
@@ -5166,7 +3084,7 @@ async function importConductorWorkspaceAsync(workspaceId) {
|
|
|
5166
3084
|
}
|
|
5167
3085
|
|
|
5168
3086
|
// src/threads/stack-layers.ts
|
|
5169
|
-
import { existsSync as
|
|
3087
|
+
import { existsSync as existsSync8 } from "fs";
|
|
5170
3088
|
function stackIdFrom(stack) {
|
|
5171
3089
|
if (stack.stackNumber != null) return `gh-stack-${stack.stackNumber}`;
|
|
5172
3090
|
const key = stack.layers.map((l) => l.branchName).join("|");
|
|
@@ -5178,7 +3096,7 @@ function hashShort(s) {
|
|
|
5178
3096
|
for (let i = 0; i < s.length; i++) h = h * 31 + s.charCodeAt(i) | 0;
|
|
5179
3097
|
return Math.abs(h).toString(36);
|
|
5180
3098
|
}
|
|
5181
|
-
function
|
|
3099
|
+
function requireThread2(idOrRef) {
|
|
5182
3100
|
const thread = findThreadByRef(idOrRef) ?? readThread(idOrRef);
|
|
5183
3101
|
if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
|
|
5184
3102
|
return thread;
|
|
@@ -5229,7 +3147,7 @@ async function openStackLayer(input, _onSetupLine) {
|
|
|
5229
3147
|
let createdWorktree = false;
|
|
5230
3148
|
const trees = await listWorktrees(repoPath);
|
|
5231
3149
|
const checkedOut = trees.find((w) => w.branch === branchName);
|
|
5232
|
-
if (checkedOut?.path &&
|
|
3150
|
+
if (checkedOut?.path && existsSync8(checkedOut.path)) {
|
|
5233
3151
|
if (input.reuseExistingWorktree !== false) {
|
|
5234
3152
|
worktreePath = checkedOut.path;
|
|
5235
3153
|
} else {
|
|
@@ -5277,7 +3195,7 @@ async function openStackLayer(input, _onSetupLine) {
|
|
|
5277
3195
|
return { thread: readThread(thread.id) ?? thread, createdWorktree };
|
|
5278
3196
|
}
|
|
5279
3197
|
async function openPrStackLayers(input, onSetupLine) {
|
|
5280
|
-
const from =
|
|
3198
|
+
const from = requireThread2(input.threadRef);
|
|
5281
3199
|
if (!from.worktreePath?.trim() || !from.repoPath?.trim()) {
|
|
5282
3200
|
throw new Error("Thread has no worktree");
|
|
5283
3201
|
}
|
|
@@ -5321,7 +3239,7 @@ async function openPrStackLayers(input, onSetupLine) {
|
|
|
5321
3239
|
return { stack, threads, createdThreadIds };
|
|
5322
3240
|
}
|
|
5323
3241
|
async function addStackLayerFromThread(input, onSetupLine) {
|
|
5324
|
-
const from =
|
|
3242
|
+
const from = requireThread2(input.threadRef);
|
|
5325
3243
|
if (!from.worktreePath?.trim() || !from.repoPath?.trim()) {
|
|
5326
3244
|
throw new Error("Thread has no worktree");
|
|
5327
3245
|
}
|
|
@@ -5353,7 +3271,7 @@ async function addStackLayerFromThread(input, onSetupLine) {
|
|
|
5353
3271
|
return { stack, thread: thread.thread, createdWorktree: thread.createdWorktree };
|
|
5354
3272
|
}
|
|
5355
3273
|
async function initStackFromThread(input, onSetupLine) {
|
|
5356
|
-
const from =
|
|
3274
|
+
const from = requireThread2(input.threadRef);
|
|
5357
3275
|
if (!from.worktreePath?.trim() || !from.branchName?.trim() || !from.repoPath?.trim()) {
|
|
5358
3276
|
throw new Error("Thread has no worktree/branch");
|
|
5359
3277
|
}
|
|
@@ -5371,7 +3289,7 @@ async function initStackFromThread(input, onSetupLine) {
|
|
|
5371
3289
|
async function createPrStack(input, onSetupLine) {
|
|
5372
3290
|
await requireAgent(input.agent);
|
|
5373
3291
|
const repoPath = await resolveRepoRoot(input.repoPath);
|
|
5374
|
-
if (!
|
|
3292
|
+
if (!existsSync8(repoPath)) throw new Error(`Repo not found: ${repoPath}`);
|
|
5375
3293
|
if (!input.branches.length) throw new Error("At least one branch name required");
|
|
5376
3294
|
const status = await detectGhStack(repoPath);
|
|
5377
3295
|
if (!status.available) throw new Error(status.reason);
|
|
@@ -5438,7 +3356,7 @@ async function createPrStack(input, onSetupLine) {
|
|
|
5438
3356
|
}
|
|
5439
3357
|
}
|
|
5440
3358
|
const claimed = new Set(threads.map((t) => t.worktreePath));
|
|
5441
|
-
if (!claimed.has(bootstrap.worktreePath) &&
|
|
3359
|
+
if (!claimed.has(bootstrap.worktreePath) && existsSync8(bootstrap.worktreePath)) {
|
|
5442
3360
|
try {
|
|
5443
3361
|
await removeWorktree(repoPath, bootstrap.worktreePath, {
|
|
5444
3362
|
deleteBranch: bootstrap.branchName
|
|
@@ -5460,10 +3378,10 @@ function stackAgentDefaultsFrom(input) {
|
|
|
5460
3378
|
}
|
|
5461
3379
|
|
|
5462
3380
|
// src/diff/diff.ts
|
|
5463
|
-
import { existsSync as
|
|
5464
|
-
import { dirname
|
|
3381
|
+
import { existsSync as existsSync9, mkdirSync, readFileSync as readFileSync6, statSync as statSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
3382
|
+
import { dirname, join as join8 } from "path";
|
|
5465
3383
|
async function inspectGitWorktree(worktreePath) {
|
|
5466
|
-
if (!worktreePath || !
|
|
3384
|
+
if (!worktreePath || !existsSync9(worktreePath)) return "missing_worktree";
|
|
5467
3385
|
const check = await git(["rev-parse", "--is-inside-work-tree"], worktreePath, {
|
|
5468
3386
|
reject: false
|
|
5469
3387
|
});
|
|
@@ -5471,7 +3389,7 @@ async function inspectGitWorktree(worktreePath) {
|
|
|
5471
3389
|
return "ok";
|
|
5472
3390
|
}
|
|
5473
3391
|
async function initializeGitRepository(worktreePath) {
|
|
5474
|
-
if (!worktreePath || !
|
|
3392
|
+
if (!worktreePath || !existsSync9(worktreePath)) {
|
|
5475
3393
|
throw new Error("Worktree not found");
|
|
5476
3394
|
}
|
|
5477
3395
|
const status = await inspectGitWorktree(worktreePath);
|
|
@@ -5607,11 +3525,11 @@ new file mode 100644
|
|
|
5607
3525
|
};
|
|
5608
3526
|
}
|
|
5609
3527
|
async function untrackedPatch(worktreePath, path, maxHunk) {
|
|
5610
|
-
const abs =
|
|
3528
|
+
const abs = join8(worktreePath, path);
|
|
5611
3529
|
try {
|
|
5612
3530
|
const st = statSync2(abs);
|
|
5613
3531
|
if (st.isFile() && st.size > maxHunk) {
|
|
5614
|
-
const buf =
|
|
3532
|
+
const buf = readFileSync6(abs).subarray(0, maxHunk);
|
|
5615
3533
|
return syntheticAddPatch(path, buf.toString("utf8"), maxHunk);
|
|
5616
3534
|
}
|
|
5617
3535
|
} catch {
|
|
@@ -6079,8 +3997,8 @@ async function getDiff(worktreePath, repoPath, opts) {
|
|
|
6079
3997
|
}
|
|
6080
3998
|
function statWorktreePath(worktreePath, relativePath) {
|
|
6081
3999
|
assertSafeRelativePath(relativePath);
|
|
6082
|
-
const abs =
|
|
6083
|
-
if (!
|
|
4000
|
+
const abs = join8(worktreePath, relativePath);
|
|
4001
|
+
if (!existsSync9(abs)) return "missing";
|
|
6084
4002
|
const st = statSync2(abs);
|
|
6085
4003
|
if (st.isDirectory()) return "dir";
|
|
6086
4004
|
if (st.isFile()) return "file";
|
|
@@ -6121,7 +4039,7 @@ var DEFAULT_UPLOAD_MAX_BYTES = 5e7;
|
|
|
6121
4039
|
function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
|
|
6122
4040
|
assertSafeRelativePath(relativePath);
|
|
6123
4041
|
const maxBytes = opts?.maxBytes ?? DEFAULT_UPLOAD_MAX_BYTES;
|
|
6124
|
-
const abs =
|
|
4042
|
+
const abs = join8(worktreePath, relativePath);
|
|
6125
4043
|
const st = statSync2(abs);
|
|
6126
4044
|
if (!st.isFile()) {
|
|
6127
4045
|
throw new Error(`Not a file: ${relativePath}`);
|
|
@@ -6131,7 +4049,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
|
|
|
6131
4049
|
`File too large to upload (${st.size} bytes; max ${maxBytes})`
|
|
6132
4050
|
);
|
|
6133
4051
|
}
|
|
6134
|
-
const buf =
|
|
4052
|
+
const buf = readFileSync6(abs);
|
|
6135
4053
|
return {
|
|
6136
4054
|
path: relativePath,
|
|
6137
4055
|
contentBase64: buf.toString("base64"),
|
|
@@ -6141,12 +4059,12 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
|
|
|
6141
4059
|
function readWorktreeFile(worktreePath, relativePath, opts) {
|
|
6142
4060
|
assertSafeRelativePath(relativePath);
|
|
6143
4061
|
const maxBytes = opts?.maxBytes ?? 2e5;
|
|
6144
|
-
const abs =
|
|
4062
|
+
const abs = join8(worktreePath, relativePath);
|
|
6145
4063
|
const st = statSync2(abs);
|
|
6146
4064
|
if (!st.isFile()) {
|
|
6147
4065
|
throw new Error(`Not a file: ${relativePath}`);
|
|
6148
4066
|
}
|
|
6149
|
-
const buf =
|
|
4067
|
+
const buf = readFileSync6(abs);
|
|
6150
4068
|
if (isImageRelativePath(relativePath)) {
|
|
6151
4069
|
const maxImageBytes = Math.max(maxBytes, 15e6);
|
|
6152
4070
|
const truncated2 = buf.length > maxImageBytes;
|
|
@@ -6189,9 +4107,9 @@ function assertSafeRelativePath(relativePath) {
|
|
|
6189
4107
|
}
|
|
6190
4108
|
function writeWorktreeFile(worktreePath, relativePath, content) {
|
|
6191
4109
|
assertSafeRelativePath(relativePath);
|
|
6192
|
-
const abs =
|
|
6193
|
-
|
|
6194
|
-
|
|
4110
|
+
const abs = join8(worktreePath, relativePath);
|
|
4111
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
4112
|
+
writeFileSync5(abs, content, "utf8");
|
|
6195
4113
|
return { path: relativePath };
|
|
6196
4114
|
}
|
|
6197
4115
|
async function getWorktreeDirtyStat(worktreePath) {
|
|
@@ -6413,9 +4331,9 @@ async function confirmLand(thread, opts) {
|
|
|
6413
4331
|
}
|
|
6414
4332
|
|
|
6415
4333
|
// src/skills/discover.ts
|
|
6416
|
-
import { existsSync as
|
|
4334
|
+
import { existsSync as existsSync10, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
|
|
6417
4335
|
import { homedir } from "os";
|
|
6418
|
-
import { join as
|
|
4336
|
+
import { join as join9 } from "path";
|
|
6419
4337
|
|
|
6420
4338
|
// src/skills/bundled/long-running.ts
|
|
6421
4339
|
var LONG_RUNNING_SKILL_COMMAND = "long-running";
|
|
@@ -6552,7 +4470,7 @@ function parseFrontmatter(content) {
|
|
|
6552
4470
|
}
|
|
6553
4471
|
function readSkill(skillMd, source) {
|
|
6554
4472
|
try {
|
|
6555
|
-
const content =
|
|
4473
|
+
const content = readFileSync7(skillMd, "utf8");
|
|
6556
4474
|
const { name: fmName, description } = parseFrontmatter(content);
|
|
6557
4475
|
const dirName = skillMd.split("/").slice(-2, -1)[0] || "skill";
|
|
6558
4476
|
const name = fmName || dirName;
|
|
@@ -6571,17 +4489,17 @@ function readSkill(skillMd, source) {
|
|
|
6571
4489
|
}
|
|
6572
4490
|
}
|
|
6573
4491
|
function scanSkillsDir(dir, source, out) {
|
|
6574
|
-
if (!
|
|
4492
|
+
if (!existsSync10(dir)) return;
|
|
6575
4493
|
let entries;
|
|
6576
4494
|
try {
|
|
6577
|
-
entries =
|
|
4495
|
+
entries = readdirSync3(dir);
|
|
6578
4496
|
} catch {
|
|
6579
4497
|
return;
|
|
6580
4498
|
}
|
|
6581
4499
|
for (const entry of entries) {
|
|
6582
4500
|
if (entry.startsWith(".")) continue;
|
|
6583
|
-
const skillMd =
|
|
6584
|
-
if (!
|
|
4501
|
+
const skillMd = join9(dir, entry, "SKILL.md");
|
|
4502
|
+
if (!existsSync10(skillMd)) continue;
|
|
6585
4503
|
try {
|
|
6586
4504
|
if (!statSync3(skillMd).isFile()) continue;
|
|
6587
4505
|
} catch {
|
|
@@ -6592,22 +4510,22 @@ function scanSkillsDir(dir, source, out) {
|
|
|
6592
4510
|
}
|
|
6593
4511
|
}
|
|
6594
4512
|
function scanClaudePluginSkills(pluginsRoot, out) {
|
|
6595
|
-
if (!
|
|
4513
|
+
if (!existsSync10(pluginsRoot)) return;
|
|
6596
4514
|
const walk = (dir, depth, lookingForSkillsDir) => {
|
|
6597
4515
|
if (depth > 7) return;
|
|
6598
4516
|
let entries;
|
|
6599
4517
|
try {
|
|
6600
|
-
entries =
|
|
4518
|
+
entries = readdirSync3(dir);
|
|
6601
4519
|
} catch {
|
|
6602
4520
|
return;
|
|
6603
4521
|
}
|
|
6604
4522
|
if (lookingForSkillsDir && entries.includes("SKILL.md")) {
|
|
6605
|
-
const skill = readSkill(
|
|
4523
|
+
const skill = readSkill(join9(dir, "SKILL.md"), "cli");
|
|
6606
4524
|
if (skill) out.push(skill);
|
|
6607
4525
|
}
|
|
6608
4526
|
for (const entry of entries) {
|
|
6609
4527
|
if (entry === "node_modules" || entry === ".git") continue;
|
|
6610
|
-
const full =
|
|
4528
|
+
const full = join9(dir, entry);
|
|
6611
4529
|
try {
|
|
6612
4530
|
if (!statSync3(full).isDirectory()) continue;
|
|
6613
4531
|
} catch {
|
|
@@ -6627,17 +4545,17 @@ function discoverSkills(worktreePath) {
|
|
|
6627
4545
|
const home = homedir();
|
|
6628
4546
|
const collected = [];
|
|
6629
4547
|
for (const rel of [".claude/skills", ".cursor/skills", ".sideboard/skills", ".brightsy/skills", "skills"]) {
|
|
6630
|
-
scanSkillsDir(
|
|
4548
|
+
scanSkillsDir(join9(worktreePath, rel), "workspace", collected);
|
|
6631
4549
|
}
|
|
6632
4550
|
for (const abs of [
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
4551
|
+
join9(home, ".claude/skills"),
|
|
4552
|
+
join9(home, ".cursor/skills"),
|
|
4553
|
+
join9(home, ".sideboard/skills"),
|
|
4554
|
+
join9(home, ".brightsy/skills")
|
|
6637
4555
|
]) {
|
|
6638
4556
|
scanSkillsDir(abs, "user", collected);
|
|
6639
4557
|
}
|
|
6640
|
-
scanClaudePluginSkills(
|
|
4558
|
+
scanClaudePluginSkills(join9(home, ".claude/plugins"), collected);
|
|
6641
4559
|
collected.push(...bundledSkills());
|
|
6642
4560
|
const rank = {
|
|
6643
4561
|
workspace: 0,
|
|
@@ -6663,7 +4581,7 @@ function readSkillBody(skillPath, maxChars = 12e3) {
|
|
|
6663
4581
|
\u2026(truncated)` : body;
|
|
6664
4582
|
}
|
|
6665
4583
|
}
|
|
6666
|
-
const raw =
|
|
4584
|
+
const raw = readFileSync7(skillPath, "utf8");
|
|
6667
4585
|
if (raw.startsWith("---")) {
|
|
6668
4586
|
const end = raw.indexOf("\n---", 3);
|
|
6669
4587
|
if (end >= 0) {
|
|
@@ -6808,12 +4726,12 @@ function shiftQueuedItem(queue, queueAttachments) {
|
|
|
6808
4726
|
}
|
|
6809
4727
|
|
|
6810
4728
|
// src/agents/instructions.ts
|
|
6811
|
-
import { existsSync as
|
|
6812
|
-
import { join as
|
|
4729
|
+
import { existsSync as existsSync12 } from "fs";
|
|
4730
|
+
import { join as join11 } from "path";
|
|
6813
4731
|
|
|
6814
4732
|
// src/skills/detached-job-path.ts
|
|
6815
|
-
import { existsSync as
|
|
6816
|
-
import { dirname as
|
|
4733
|
+
import { existsSync as existsSync11 } from "fs";
|
|
4734
|
+
import { dirname as dirname2, join as join10 } from "path";
|
|
6817
4735
|
import { fileURLToPath } from "url";
|
|
6818
4736
|
var DETACHED_JOB_FILENAMES = ["detached-job.cjs", "detached-job.js"];
|
|
6819
4737
|
function compiledModuleDir() {
|
|
@@ -6822,7 +4740,7 @@ function compiledModuleDir() {
|
|
|
6822
4740
|
try {
|
|
6823
4741
|
const url = import.meta.url;
|
|
6824
4742
|
if (typeof url === "string" && url.length > 0) {
|
|
6825
|
-
return
|
|
4743
|
+
return dirname2(fileURLToPath(url));
|
|
6826
4744
|
}
|
|
6827
4745
|
} catch {
|
|
6828
4746
|
}
|
|
@@ -6830,8 +4748,8 @@ function compiledModuleDir() {
|
|
|
6830
4748
|
}
|
|
6831
4749
|
function detachedJobScriptIn(dir) {
|
|
6832
4750
|
for (const name of DETACHED_JOB_FILENAMES) {
|
|
6833
|
-
const script =
|
|
6834
|
-
if (
|
|
4751
|
+
const script = join10(dir, "scripts", name);
|
|
4752
|
+
if (existsSync11(script)) return script;
|
|
6835
4753
|
}
|
|
6836
4754
|
return null;
|
|
6837
4755
|
}
|
|
@@ -6848,7 +4766,7 @@ function resolveDetachedJobScript() {
|
|
|
6848
4766
|
for (let i = 0; i < 8; i++) {
|
|
6849
4767
|
const candidate = detachedJobScriptIn(dir);
|
|
6850
4768
|
if (candidate) return candidate;
|
|
6851
|
-
const parent =
|
|
4769
|
+
const parent = dirname2(dir);
|
|
6852
4770
|
if (parent === dir) break;
|
|
6853
4771
|
dir = parent;
|
|
6854
4772
|
}
|
|
@@ -6890,12 +4808,12 @@ function formatRenameBranchDirective(thread, opts) {
|
|
|
6890
4808
|
function localhostPreviewUrl(port) {
|
|
6891
4809
|
return `http://localhost:${port}`;
|
|
6892
4810
|
}
|
|
6893
|
-
function describeLiveDevRun(
|
|
6894
|
-
const url = localhostPreviewUrl(
|
|
6895
|
-
if (
|
|
6896
|
-
return `${
|
|
4811
|
+
function describeLiveDevRun(run) {
|
|
4812
|
+
const url = localhostPreviewUrl(run.port);
|
|
4813
|
+
if (run.preview === "window") {
|
|
4814
|
+
return `${run.scriptName} Electron window (renderer ${url})`;
|
|
6897
4815
|
}
|
|
6898
|
-
return `${
|
|
4816
|
+
return `${run.scriptName} ${url}`;
|
|
6899
4817
|
}
|
|
6900
4818
|
function formatDevAccessHint(runs) {
|
|
6901
4819
|
const live = (runs ?? []).filter(
|
|
@@ -6913,7 +4831,7 @@ function formatDevAccessHint(runs) {
|
|
|
6913
4831
|
const bits = live.map(describeLiveDevRun).join("; ");
|
|
6914
4832
|
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.`;
|
|
6915
4833
|
}
|
|
6916
|
-
return "To open
|
|
4834
|
+
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.";
|
|
6917
4835
|
}
|
|
6918
4836
|
function formatWorktreeDirective(thread, opts) {
|
|
6919
4837
|
const worktree = normPath(thread.worktreePath);
|
|
@@ -6953,9 +4871,9 @@ function formatWorktreeDirective(thread, opts) {
|
|
|
6953
4871
|
);
|
|
6954
4872
|
} else {
|
|
6955
4873
|
const slug = opts?.githubSlug ?? "<origin-owner/name>";
|
|
6956
|
-
const
|
|
4874
|
+
const resolve = opts?.githubSlug ? "" : " Resolve `<origin-owner/name>` with `git remote get-url origin` in this worktree.";
|
|
6957
4875
|
lines.push(
|
|
6958
|
-
`- 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.${
|
|
4876
|
+
`- 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\`.`
|
|
6959
4877
|
);
|
|
6960
4878
|
}
|
|
6961
4879
|
lines.push(
|
|
@@ -7005,17 +4923,17 @@ function formatIssueToolsDirective(opts) {
|
|
|
7005
4923
|
];
|
|
7006
4924
|
if (opts.linear) {
|
|
7007
4925
|
lines.push(
|
|
7008
|
-
"- 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."
|
|
4926
|
+
"- 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."
|
|
7009
4927
|
);
|
|
7010
4928
|
}
|
|
7011
4929
|
if (github) {
|
|
7012
4930
|
lines.push(
|
|
7013
|
-
"- 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`."
|
|
4931
|
+
"- 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`."
|
|
7014
4932
|
);
|
|
7015
4933
|
}
|
|
7016
4934
|
if (opts.abletime) {
|
|
7017
4935
|
lines.push(
|
|
7018
|
-
"- 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)."
|
|
4936
|
+
"- 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)."
|
|
7019
4937
|
);
|
|
7020
4938
|
}
|
|
7021
4939
|
lines.push(
|
|
@@ -7071,9 +4989,9 @@ function formatPrGateDirective() {
|
|
|
7071
4989
|
}
|
|
7072
4990
|
function formatProcessGuideDirective(opts) {
|
|
7073
4991
|
const worktree = opts?.worktreePath?.trim();
|
|
7074
|
-
const hasGraphSkill = !!worktree &&
|
|
7075
|
-
const hasReviewSkill = !!worktree &&
|
|
7076
|
-
const hasInstallSkill = !!worktree &&
|
|
4992
|
+
const hasGraphSkill = !!worktree && existsSync12(join11(worktree, ".claude/skills/graph-engineering/SKILL.md"));
|
|
4993
|
+
const hasReviewSkill = !!worktree && existsSync12(join11(worktree, ".claude/skills/review/SKILL.md"));
|
|
4994
|
+
const hasInstallSkill = !!worktree && existsSync12(join11(worktree, ".claude/skills/worktree-install/SKILL.md"));
|
|
7077
4995
|
return [
|
|
7078
4996
|
"Process guides (recurring work only):",
|
|
7079
4997
|
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,
|
|
@@ -7093,7 +5011,7 @@ function formatLongRunningDirective(opts) {
|
|
|
7093
5011
|
"- 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.",
|
|
7094
5012
|
"- 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.",
|
|
7095
5013
|
"State: `.context/.sideboard/detached-jobs/<id>/` (local scratch). Full guide: `/long-running` (always available).",
|
|
7096
|
-
"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
|
|
5014
|
+
"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."
|
|
7097
5015
|
].join("\n");
|
|
7098
5016
|
}
|
|
7099
5017
|
function formatLongRunningReminder(opts) {
|
|
@@ -7110,7 +5028,7 @@ function formatArtifactDirective() {
|
|
|
7110
5028
|
].join("\n");
|
|
7111
5029
|
}
|
|
7112
5030
|
function formatUiReminder() {
|
|
7113
|
-
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
|
|
5031
|
+
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.";
|
|
7114
5032
|
}
|
|
7115
5033
|
|
|
7116
5034
|
// src/integrations/optional-services.ts
|
|
@@ -7399,9 +5317,9 @@ async function syncThreadBranchFromGit(threadId) {
|
|
|
7399
5317
|
}
|
|
7400
5318
|
|
|
7401
5319
|
// src/store/schedules.ts
|
|
7402
|
-
import { randomUUID as
|
|
7403
|
-
import { existsSync as
|
|
7404
|
-
import { join as
|
|
5320
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
5321
|
+
import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
|
|
5322
|
+
import { join as join12 } from "path";
|
|
7405
5323
|
import { Cron } from "croner";
|
|
7406
5324
|
var DURATION_RE = /^(\d+)(s|m|h|d)$/i;
|
|
7407
5325
|
var UNIT_MS = {
|
|
@@ -7411,7 +5329,7 @@ var UNIT_MS = {
|
|
|
7411
5329
|
d: 864e5
|
|
7412
5330
|
};
|
|
7413
5331
|
function schedulesPath() {
|
|
7414
|
-
return
|
|
5332
|
+
return join12(appDataDir(), "schedules.json");
|
|
7415
5333
|
}
|
|
7416
5334
|
function parseDurationMs(every) {
|
|
7417
5335
|
const m = every.trim().match(DURATION_RE);
|
|
@@ -7517,9 +5435,9 @@ function normalizeTask(raw) {
|
|
|
7517
5435
|
}
|
|
7518
5436
|
function readAll() {
|
|
7519
5437
|
const path = schedulesPath();
|
|
7520
|
-
if (!
|
|
5438
|
+
if (!existsSync13(path)) return [];
|
|
7521
5439
|
try {
|
|
7522
|
-
const parsed = JSON.parse(
|
|
5440
|
+
const parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
7523
5441
|
if (!Array.isArray(parsed)) return [];
|
|
7524
5442
|
return parsed.map((row) => {
|
|
7525
5443
|
try {
|
|
@@ -7544,7 +5462,7 @@ function buildTask(input, now) {
|
|
|
7544
5462
|
const ts = nowIso(now);
|
|
7545
5463
|
const agent = input.agent ? coerceOrchestratorAgent(input.agent) : null;
|
|
7546
5464
|
return {
|
|
7547
|
-
id:
|
|
5465
|
+
id: randomUUID2(),
|
|
7548
5466
|
name: input.name?.trim() || defaultScheduleName(prompt),
|
|
7549
5467
|
prompt,
|
|
7550
5468
|
enabled: input.enabled !== false,
|
|
@@ -7658,7 +5576,7 @@ function formatScheduledPrompt(name, prompt) {
|
|
|
7658
5576
|
${prompt}`;
|
|
7659
5577
|
}
|
|
7660
5578
|
async function defaultDeps() {
|
|
7661
|
-
const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-
|
|
5579
|
+
const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-6ZQ2CYS4.js");
|
|
7662
5580
|
const orch = getOrchestrator2();
|
|
7663
5581
|
return {
|
|
7664
5582
|
findThread: (id) => findThreadByRef(id),
|
|
@@ -7716,14 +5634,14 @@ function armOne(schedule) {
|
|
|
7716
5634
|
if (!schedule.enabled) return;
|
|
7717
5635
|
const at = new Date(schedule.nextRunAt);
|
|
7718
5636
|
if (Number.isNaN(at.getTime())) return;
|
|
7719
|
-
const
|
|
7720
|
-
if (
|
|
5637
|
+
const delay = Math.max(0, at.getTime() - Date.now());
|
|
5638
|
+
if (delay <= 250) {
|
|
7721
5639
|
void fireSchedule(schedule.id).then(() => {
|
|
7722
5640
|
if (isThisProcessDesktopHost()) armSchedules();
|
|
7723
5641
|
});
|
|
7724
5642
|
return;
|
|
7725
5643
|
}
|
|
7726
|
-
const capped = Math.min(
|
|
5644
|
+
const capped = Math.min(delay, SET_TIMEOUT_MAX);
|
|
7727
5645
|
const timer = setTimeout(() => {
|
|
7728
5646
|
timers.delete(schedule.id);
|
|
7729
5647
|
const live = getSchedule(schedule.id);
|
|
@@ -7791,8 +5709,8 @@ function writeLiveStatus(threadId, status, lastError) {
|
|
|
7791
5709
|
}
|
|
7792
5710
|
function collectActiveRunPorts(runs) {
|
|
7793
5711
|
const ports = /* @__PURE__ */ new Set();
|
|
7794
|
-
for (const
|
|
7795
|
-
const list =
|
|
5712
|
+
for (const run of runs) {
|
|
5713
|
+
const list = run.ports?.length ? run.ports : run.port != null ? [run.port] : [];
|
|
7796
5714
|
for (const p of list) {
|
|
7797
5715
|
if (Number.isFinite(p) && p > 0) ports.add(p);
|
|
7798
5716
|
}
|
|
@@ -7910,7 +5828,7 @@ var Orchestrator = class {
|
|
|
7910
5828
|
this.lightReconcileAt.set(repoPath, Date.now());
|
|
7911
5829
|
for (const thread of listThreads()) {
|
|
7912
5830
|
if (isGlobalThread(thread) || thread.repoPath !== repoPath) continue;
|
|
7913
|
-
if (!
|
|
5831
|
+
if (!existsSync14(thread.worktreePath)) {
|
|
7914
5832
|
setStatus(thread.id, "broken", "Worktree missing on disk");
|
|
7915
5833
|
this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
|
|
7916
5834
|
}
|
|
@@ -7939,7 +5857,7 @@ var Orchestrator = class {
|
|
|
7939
5857
|
}
|
|
7940
5858
|
continue;
|
|
7941
5859
|
}
|
|
7942
|
-
if (!
|
|
5860
|
+
if (!existsSync14(thread.worktreePath)) {
|
|
7943
5861
|
setStatus(thread.id, "broken", "Worktree missing on disk");
|
|
7944
5862
|
this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
|
|
7945
5863
|
continue;
|
|
@@ -7976,7 +5894,7 @@ var Orchestrator = class {
|
|
|
7976
5894
|
orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
|
|
7977
5895
|
});
|
|
7978
5896
|
}
|
|
7979
|
-
const { autoCleanupOrphansEnabled } = await import("./app-settings-
|
|
5897
|
+
const { autoCleanupOrphansEnabled } = await import("./app-settings-L5HGYXWJ.js");
|
|
7980
5898
|
if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
|
|
7981
5899
|
await cleanupOrphanWorktrees({ repoPaths });
|
|
7982
5900
|
}
|
|
@@ -8107,8 +6025,8 @@ var Orchestrator = class {
|
|
|
8107
6025
|
scheduleQuotaResume(threadId, resumeAt) {
|
|
8108
6026
|
this.clearQuotaResumeTimer(threadId);
|
|
8109
6027
|
updateThread(threadId, { quotaResumeAt: resumeAt.toISOString() });
|
|
8110
|
-
const
|
|
8111
|
-
const capped = Math.min(
|
|
6028
|
+
const delay = Math.max(5e3, resumeAt.getTime() - Date.now());
|
|
6029
|
+
const capped = Math.min(delay, 2147483647);
|
|
8112
6030
|
const timer = setTimeout(() => {
|
|
8113
6031
|
this.quotaResumeTimers.delete(threadId);
|
|
8114
6032
|
void this.resumeAfterQuotaWait(threadId);
|
|
@@ -8365,7 +6283,7 @@ var Orchestrator = class {
|
|
|
8365
6283
|
}
|
|
8366
6284
|
await setup;
|
|
8367
6285
|
if (skipSetup) return;
|
|
8368
|
-
const { autoRunAfterSetupEnabled } = await import("./app-settings-
|
|
6286
|
+
const { autoRunAfterSetupEnabled } = await import("./app-settings-L5HGYXWJ.js");
|
|
8369
6287
|
if (autoRunAfterSetupEnabled()) {
|
|
8370
6288
|
try {
|
|
8371
6289
|
await this.startDev(threadId);
|
|
@@ -8744,7 +6662,7 @@ var Orchestrator = class {
|
|
|
8744
6662
|
}
|
|
8745
6663
|
const isBrightsy = fresh.agent === "brightsy";
|
|
8746
6664
|
const isOrchestration = isOrchestratorThread(fresh);
|
|
8747
|
-
const { autoRenameBranchEnabled, getGithubGitAuthMode, gitBranchPrefixSetting } = await import("./app-settings-
|
|
6665
|
+
const { autoRenameBranchEnabled, getGithubGitAuthMode, gitBranchPrefixSetting } = await import("./app-settings-L5HGYXWJ.js");
|
|
8748
6666
|
const gitAuthMode = getGithubGitAuthMode();
|
|
8749
6667
|
const worktreeDirective = isBrightsy || isOrchestration ? null : formatWorktreeDirective(fresh, {
|
|
8750
6668
|
githubSlug: await withTimeout(
|
|
@@ -9296,7 +7214,7 @@ var Orchestrator = class {
|
|
|
9296
7214
|
});
|
|
9297
7215
|
}
|
|
9298
7216
|
const latestShared = mergeWorktreeActiveRuns(threadsSharingWorktree(thread.worktreePath));
|
|
9299
|
-
const
|
|
7217
|
+
const run = {
|
|
9300
7218
|
scriptName: resolvedName,
|
|
9301
7219
|
port: handle.port,
|
|
9302
7220
|
ports: handle.ports,
|
|
@@ -9305,7 +7223,7 @@ var Orchestrator = class {
|
|
|
9305
7223
|
};
|
|
9306
7224
|
const nextRuns = [
|
|
9307
7225
|
...latestShared.activeRuns.filter((r) => r.scriptName !== resolvedName),
|
|
9308
|
-
|
|
7226
|
+
run
|
|
9309
7227
|
];
|
|
9310
7228
|
this.syncWorktreeRuns(
|
|
9311
7229
|
thread.worktreePath,
|
|
@@ -9351,7 +7269,7 @@ var Orchestrator = class {
|
|
|
9351
7269
|
const request = sameIntent ? pending : {
|
|
9352
7270
|
op,
|
|
9353
7271
|
scriptName: scriptName ?? null,
|
|
9354
|
-
requestId:
|
|
7272
|
+
requestId: randomUUID3(),
|
|
9355
7273
|
requestedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9356
7274
|
};
|
|
9357
7275
|
if (!sameIntent) {
|
|
@@ -9372,14 +7290,14 @@ var Orchestrator = class {
|
|
|
9372
7290
|
throw new Error(req.error);
|
|
9373
7291
|
}
|
|
9374
7292
|
const shared = mergeWorktreeActiveRuns(threadsSharingWorktree(latest.worktreePath));
|
|
9375
|
-
const
|
|
7293
|
+
const run = scriptName ? shared.activeRuns.find((r) => r.scriptName === scriptName) : shared.activeRuns[0];
|
|
9376
7294
|
const fulfilled = req?.requestId === request.requestId && Boolean(req.fulfilledAt);
|
|
9377
7295
|
const requestGone = !req || req.requestId !== request.requestId;
|
|
9378
7296
|
if (op === "start") {
|
|
9379
|
-
if (
|
|
9380
|
-
return { port:
|
|
7297
|
+
if (run && (fulfilled || requestGone || run.startedAt >= request.requestedAt)) {
|
|
7298
|
+
return { port: run.port, scriptName: run.scriptName, ports: run.ports };
|
|
9381
7299
|
}
|
|
9382
|
-
if (fulfilled && !
|
|
7300
|
+
if (fulfilled && !run) {
|
|
9383
7301
|
throw new Error("Desktop reported the run script started, but it is not active.");
|
|
9384
7302
|
}
|
|
9385
7303
|
} else if (fulfilled) {
|
|
@@ -9445,15 +7363,15 @@ var Orchestrator = class {
|
|
|
9445
7363
|
}
|
|
9446
7364
|
this.processes.delete(legacyKey);
|
|
9447
7365
|
}
|
|
9448
|
-
const
|
|
9449
|
-
if (!hadHandle2 &&
|
|
9450
|
-
if (typeof
|
|
9451
|
-
killDetachedRunTree(
|
|
7366
|
+
const run = shared.activeRuns.find((r) => r.scriptName === scriptName);
|
|
7367
|
+
if (!hadHandle2 && run) {
|
|
7368
|
+
if (typeof run.pid === "number") {
|
|
7369
|
+
killDetachedRunTree(run.pid, thread.worktreePath);
|
|
9452
7370
|
}
|
|
9453
|
-
killListenersOnPorts(collectActiveRunPorts([
|
|
7371
|
+
killListenersOnPorts(collectActiveRunPorts([run]));
|
|
9454
7372
|
}
|
|
9455
7373
|
const remaining = shared.activeRuns.filter((r) => r.scriptName !== scriptName);
|
|
9456
|
-
const isPrimary = shared.devPort != null &&
|
|
7374
|
+
const isPrimary = shared.devPort != null && run?.port === shared.devPort;
|
|
9457
7375
|
this.syncWorktreeRuns(
|
|
9458
7376
|
thread.worktreePath,
|
|
9459
7377
|
remaining,
|
|
@@ -9471,9 +7389,9 @@ var Orchestrator = class {
|
|
|
9471
7389
|
}
|
|
9472
7390
|
}
|
|
9473
7391
|
if (!hadHandle && shared.activeRuns.length > 0) {
|
|
9474
|
-
for (const
|
|
9475
|
-
if (typeof
|
|
9476
|
-
killDetachedRunTree(
|
|
7392
|
+
for (const run of shared.activeRuns) {
|
|
7393
|
+
if (typeof run.pid === "number") {
|
|
7394
|
+
killDetachedRunTree(run.pid, thread.worktreePath);
|
|
9477
7395
|
}
|
|
9478
7396
|
}
|
|
9479
7397
|
killListenersOnPorts(collectActiveRunPorts(shared.activeRuns));
|
|
@@ -9517,9 +7435,9 @@ var Orchestrator = class {
|
|
|
9517
7435
|
(k) => isWorktreeRunProcessKey(k, thread.worktreePath) || siblings.some((t) => k.startsWith(`${t.id}:run:`) || k === `${t.id}:dev`)
|
|
9518
7436
|
);
|
|
9519
7437
|
if (!force && hasLiveHandle) continue;
|
|
9520
|
-
for (const
|
|
9521
|
-
if (typeof
|
|
9522
|
-
killDetachedRunTree(
|
|
7438
|
+
for (const run of shared.activeRuns) {
|
|
7439
|
+
if (typeof run.pid === "number") {
|
|
7440
|
+
killDetachedRunTree(run.pid, thread.worktreePath);
|
|
9523
7441
|
}
|
|
9524
7442
|
}
|
|
9525
7443
|
killListenersOnPorts(collectActiveRunPorts(shared.activeRuns));
|
|
@@ -9728,7 +7646,7 @@ var Orchestrator = class {
|
|
|
9728
7646
|
return thread;
|
|
9729
7647
|
}
|
|
9730
7648
|
const start = Date.now();
|
|
9731
|
-
return new Promise((
|
|
7649
|
+
return new Promise((resolve, reject) => {
|
|
9732
7650
|
let settled = false;
|
|
9733
7651
|
let timer;
|
|
9734
7652
|
const finish = (fn) => {
|
|
@@ -9746,7 +7664,7 @@ var Orchestrator = class {
|
|
|
9746
7664
|
finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
|
|
9747
7665
|
return;
|
|
9748
7666
|
}
|
|
9749
|
-
finish(() =>
|
|
7667
|
+
finish(() => resolve(latest));
|
|
9750
7668
|
return;
|
|
9751
7669
|
}
|
|
9752
7670
|
if (event.type === "status_changed") {
|
|
@@ -9756,7 +7674,7 @@ var Orchestrator = class {
|
|
|
9756
7674
|
return;
|
|
9757
7675
|
}
|
|
9758
7676
|
if (!["running", "queued"].includes(latest.status)) {
|
|
9759
|
-
finish(() =>
|
|
7677
|
+
finish(() => resolve(latest));
|
|
9760
7678
|
}
|
|
9761
7679
|
}
|
|
9762
7680
|
});
|
|
@@ -9768,12 +7686,12 @@ var Orchestrator = class {
|
|
|
9768
7686
|
}
|
|
9769
7687
|
const current = this.healStaleReportedActivity(raw);
|
|
9770
7688
|
if (!this.threadLooksLive(current)) {
|
|
9771
|
-
finish(() =>
|
|
7689
|
+
finish(() => resolve(current));
|
|
9772
7690
|
return;
|
|
9773
7691
|
}
|
|
9774
7692
|
if (Date.now() - start > timeoutMs) {
|
|
9775
7693
|
if (opts?.resolveIfStillRunning) {
|
|
9776
|
-
finish(() =>
|
|
7694
|
+
finish(() => resolve(current));
|
|
9777
7695
|
} else {
|
|
9778
7696
|
finish(() => reject(new Error("wait_for_turn timed out")));
|
|
9779
7697
|
}
|
|
@@ -10054,7 +7972,7 @@ var Orchestrator = class {
|
|
|
10054
7972
|
if (Object.keys(patch).length === 0) continue;
|
|
10055
7973
|
updateThread(t.id, patch);
|
|
10056
7974
|
}
|
|
10057
|
-
const { autoArchiveOnMergeEnabled } = await import("./app-settings-
|
|
7975
|
+
const { autoArchiveOnMergeEnabled } = await import("./app-settings-L5HGYXWJ.js");
|
|
10058
7976
|
const latest = this.requireThread(thread.id);
|
|
10059
7977
|
if (!shouldAutoArchiveOnPrMerge({
|
|
10060
7978
|
previousPrState: prevState || null,
|
|
@@ -10345,7 +8263,7 @@ var Orchestrator = class {
|
|
|
10345
8263
|
this.enforceHistoryRetention();
|
|
10346
8264
|
if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
|
|
10347
8265
|
try {
|
|
10348
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
8266
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-F33Z4CEY.js");
|
|
10349
8267
|
await ensureWorkspace2(thread.repoPath);
|
|
10350
8268
|
} catch {
|
|
10351
8269
|
}
|
|
@@ -10372,7 +8290,7 @@ var Orchestrator = class {
|
|
|
10372
8290
|
await runArchiveScript(thread.repoPath, thread.worktreePath);
|
|
10373
8291
|
} catch {
|
|
10374
8292
|
}
|
|
10375
|
-
const { deleteBranchOnPurgeEnabled } = await import("./app-settings-
|
|
8293
|
+
const { deleteBranchOnPurgeEnabled } = await import("./app-settings-L5HGYXWJ.js");
|
|
10376
8294
|
const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
|
|
10377
8295
|
await removeWorktree(thread.repoPath, thread.worktreePath, {
|
|
10378
8296
|
deleteBranch: deleteBranch ? thread.branchName : void 0
|
|
@@ -10393,20 +8311,20 @@ var Orchestrator = class {
|
|
|
10393
8311
|
this.emit({ type: "status_changed", threadId: restored2.id, status: restored2.status });
|
|
10394
8312
|
return restored2;
|
|
10395
8313
|
}
|
|
10396
|
-
if (!
|
|
8314
|
+
if (!existsSync14(thread.worktreePath)) {
|
|
10397
8315
|
if (isCowboyThread(thread) || isPrimaryCheckoutThread(thread)) {
|
|
10398
8316
|
throw new Error(
|
|
10399
8317
|
`Cowboy checkout missing: ${thread.worktreePath}. Re-add the project folder, then restore.`
|
|
10400
8318
|
);
|
|
10401
8319
|
}
|
|
10402
|
-
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-
|
|
8320
|
+
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-C7RPVHZ7.js");
|
|
10403
8321
|
const slug = thread.worktreePath.split("/").pop();
|
|
10404
8322
|
const dest = thread.worktreePath;
|
|
10405
8323
|
await withRepoGitLock(thread.repoPath, async () => {
|
|
10406
8324
|
await git(["worktree", "add", dest, thread.branchName], thread.repoPath);
|
|
10407
8325
|
});
|
|
10408
|
-
const { ensureWorktreeSideboardIgnored
|
|
10409
|
-
await
|
|
8326
|
+
const { ensureWorktreeSideboardIgnored } = await import("./worktree-exclude-RXZPTCNH.js");
|
|
8327
|
+
await ensureWorktreeSideboardIgnored(dest);
|
|
10410
8328
|
void createThreadWorktree2;
|
|
10411
8329
|
void slug;
|
|
10412
8330
|
}
|
|
@@ -10509,7 +8427,7 @@ async function startOrchestration(opts) {
|
|
|
10509
8427
|
sourceRef: "default",
|
|
10510
8428
|
...createOpts
|
|
10511
8429
|
}).catch(async () => {
|
|
10512
|
-
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-
|
|
8430
|
+
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-C7RPVHZ7.js");
|
|
10513
8431
|
const repo = await resolveRepoRoot2(repoPath);
|
|
10514
8432
|
const def = await resolveDefaultBranch2(repo);
|
|
10515
8433
|
return orch.createThread({
|
|
@@ -10590,116 +8508,11 @@ export {
|
|
|
10590
8508
|
pendingInjectedNotices,
|
|
10591
8509
|
lastAgentReply,
|
|
10592
8510
|
formatInjectedNoticesForTurn,
|
|
10593
|
-
isTerminalTaskState,
|
|
10594
|
-
isIncompleteTaskState,
|
|
10595
|
-
needsCoordinatorAction,
|
|
10596
|
-
endedOnAskUser,
|
|
10597
|
-
deriveTaskState,
|
|
10598
|
-
mcpWaitForTurnTimeoutMs,
|
|
10599
|
-
mcpWaitTaskHint,
|
|
10600
|
-
waitForTurnToolResult,
|
|
10601
8511
|
applyPromptCacheTtlEnv,
|
|
10602
8512
|
spawnAgentTurn,
|
|
10603
8513
|
AGENT_GIT_ACTIONS,
|
|
10604
8514
|
agentGitPrompt,
|
|
10605
8515
|
resolveSidebarGitPrompt,
|
|
10606
|
-
CHARS_PER_CONTEXT_TOKEN,
|
|
10607
|
-
estimateMessageChars,
|
|
10608
|
-
estimateThreadChars,
|
|
10609
|
-
estimateOccupancyTokens,
|
|
10610
|
-
threadHasCompactedContext,
|
|
10611
|
-
forwardOccupancyTokens,
|
|
10612
|
-
forwardContextUsage,
|
|
10613
|
-
applyForwardOccupancy,
|
|
10614
|
-
CLAUDE_SUMMARY_TIMEOUT_MS,
|
|
10615
|
-
summarizeConversation,
|
|
10616
|
-
extractiveSummary,
|
|
10617
|
-
CONTEXT_COMPACT_CHARS,
|
|
10618
|
-
CONTEXT_KEEP_RECENT_CHARS,
|
|
10619
|
-
CONTEXT_KEEP_RECENT_MESSAGES,
|
|
10620
|
-
CONTEXT_MIN_MESSAGES,
|
|
10621
|
-
SESSION_RESET_OCCUPANCY_TOKENS,
|
|
10622
|
-
shouldCompactContext,
|
|
10623
|
-
splitForCompaction,
|
|
10624
|
-
formatMessagesAsTranscript,
|
|
10625
|
-
SEED_MAX_CHARS,
|
|
10626
|
-
SEED_FULL_TOOL_MESSAGES,
|
|
10627
|
-
buildSessionSeed,
|
|
10628
|
-
BRIGHTSY_SUMMARIZE_CONTEXT_TOOL,
|
|
10629
|
-
extractBrightsyContextSummary,
|
|
10630
|
-
findLastBrightsyContextSummary,
|
|
10631
|
-
messagesSinceLastBrightsyContextSummary,
|
|
10632
|
-
buildBrightsySessionSeed,
|
|
10633
|
-
compactArchiveProse,
|
|
10634
|
-
messagesForCompactArchive,
|
|
10635
|
-
appendCompactedMessages,
|
|
10636
|
-
applyCompaction,
|
|
10637
|
-
lastRequestOccupancy,
|
|
10638
|
-
shouldResetSessionForOccupancy,
|
|
10639
|
-
maybeCompactContext,
|
|
10640
|
-
sameWorktreePath,
|
|
10641
|
-
takenTeamSlugsForChatTab,
|
|
10642
|
-
threadsSharingWorktree,
|
|
10643
|
-
formatTranscriptMarkdown,
|
|
10644
|
-
forkMessageSlice,
|
|
10645
|
-
buildForkTranscriptAttachment,
|
|
10646
|
-
createChatTab,
|
|
10647
|
-
forkChatTab,
|
|
10648
|
-
REVIEW_REQUEST_TEMPLATE,
|
|
10649
|
-
REVIEW_SKILL_PATH,
|
|
10650
|
-
REVIEW_SKILL_NAME,
|
|
10651
|
-
wrapReviewSkillMarkdown,
|
|
10652
|
-
REPO_REVIEW_PATH,
|
|
10653
|
-
REPO_REVIEW_NAME,
|
|
10654
|
-
CONTEXT_REVIEW_PATH,
|
|
10655
|
-
REVIEW_REQUEST_PATH,
|
|
10656
|
-
LEGACY_REVIEW_REQUEST_PATH,
|
|
10657
|
-
REVIEW_REQUEST_NAME,
|
|
10658
|
-
REVIEW_REQUEST_PREFILL,
|
|
10659
|
-
shouldRefreshReviewRequestTemplate,
|
|
10660
|
-
ensureReviewGuidelinesFile,
|
|
10661
|
-
ensureReviewSkillFile,
|
|
10662
|
-
resolveReviewGuidelines,
|
|
10663
|
-
ensureReviewRequestFile,
|
|
10664
|
-
buildReviewRequestAttachment,
|
|
10665
|
-
readExistingReviewRequestFile,
|
|
10666
|
-
canReuseIdleWorktreeAgentForReview,
|
|
10667
|
-
requestReview,
|
|
10668
|
-
RUN_PORTS_REL,
|
|
10669
|
-
worktreeRunPortsPath,
|
|
10670
|
-
loadWorktreeRunPorts,
|
|
10671
|
-
saveWorktreeRunPorts,
|
|
10672
|
-
mcpWaitForJobTimeoutMs,
|
|
10673
|
-
waitForDetachedJob,
|
|
10674
|
-
stopDetachedJob,
|
|
10675
|
-
PORT_RANGE_SIZE,
|
|
10676
|
-
readWorktreeInclude,
|
|
10677
|
-
resolveFilesToCopy,
|
|
10678
|
-
copyConfiguredFiles,
|
|
10679
|
-
captureLoginEnv,
|
|
10680
|
-
buildWorkspaceScriptEnv,
|
|
10681
|
-
killableListenerPids,
|
|
10682
|
-
listenerCwd,
|
|
10683
|
-
isOtherWorktreeListenerCwd,
|
|
10684
|
-
isThisWorktreeListenerCwd,
|
|
10685
|
-
killDetachedRunTree,
|
|
10686
|
-
killListenersOnPorts,
|
|
10687
|
-
killStaleWorktreeListeners,
|
|
10688
|
-
runSetupScript,
|
|
10689
|
-
runConventionSetup,
|
|
10690
|
-
runWorkspaceSetup,
|
|
10691
|
-
runArchiveScript,
|
|
10692
|
-
listRunScripts,
|
|
10693
|
-
getDefaultRunScript,
|
|
10694
|
-
getRunScript,
|
|
10695
|
-
getRunMode,
|
|
10696
|
-
reservePort,
|
|
10697
|
-
allocatePort,
|
|
10698
|
-
tryReservePort,
|
|
10699
|
-
tryReservePorts,
|
|
10700
|
-
reservePortRange,
|
|
10701
|
-
allocatePortRange,
|
|
10702
|
-
startDevServer,
|
|
10703
8516
|
findOrphanWorktrees,
|
|
10704
8517
|
cleanupOrphanWorktrees,
|
|
10705
8518
|
shouldRunWorktreeCleanup,
|