@sideboard-ai/core 0.1.242 → 0.1.243
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{abletime-VSV6AUBV.js → abletime-CWIJ7INC.js} +4 -4
- package/dist/{abletime-UX4VUENQ.js → abletime-OI6FNSXX.js} +4 -4
- package/dist/{abletime-oauth-DKBUZV2C.js → abletime-oauth-CHVTUZBR.js} +3 -3
- package/dist/{abletime-oauth-QMDV45JC.js → abletime-oauth-PUIPZTA4.js} +3 -3
- package/dist/{abletime-rest-NL56UVK5.js → abletime-rest-32NIVXAY.js} +3 -3
- package/dist/{abletime-rest-VPBC2XS5.js → abletime-rest-APJUKFKL.js} +3 -3
- package/dist/{agents-CDQCQVWU.js → agents-CPME6O2H.js} +9 -5
- package/dist/{agents-PBRIHUYM.js → agents-VKO5UZO6.js} +9 -5
- package/dist/{app-settings-AGDLF75Z.js → app-settings-652J57CX.js} +1 -1
- package/dist/{app-settings-I27UBVI4.js → app-settings-VOPXJMGV.js} +1 -1
- package/dist/{chunk-BILEJG3V.js → chunk-3P5OCI2X.js} +3 -3
- package/dist/{chunk-3PGFAVFL.js → chunk-43F44EKX.js} +2 -2
- package/dist/{chunk-3AJUH66R.js → chunk-6I5YQTNK.js} +2 -1
- package/dist/{chunk-NJ5OXOTN.js → chunk-7HOKP4GR.js} +2 -1
- package/dist/{chunk-TB6UMHIF.js → chunk-7UCZ4DRF.js} +2 -2
- package/dist/{chunk-DXY4K5MP.js → chunk-BL2UGKWU.js} +3 -3
- package/dist/{chunk-CWHMLCLN.js → chunk-C64YQAA6.js} +3 -3
- package/dist/{chunk-O3UAQAYA.js → chunk-CCD2QIFK.js} +2 -2
- package/dist/{chunk-LRMJ6V6U.js → chunk-DARSRQUE.js} +1503 -1462
- package/dist/{chunk-OKHOYI53.js → chunk-ECZTXNYR.js} +3 -3
- package/dist/{chunk-CRAXJAYJ.js → chunk-G45QP2ZD.js} +3 -3
- package/dist/{chunk-3Q4PVZ3U.js → chunk-HR4RNC73.js} +32 -5
- package/dist/{chunk-YXOOSFAF.js → chunk-IMO7AZI4.js} +2 -2
- package/dist/{chunk-QXYWLO6W.js → chunk-ISZERVDT.js} +28 -5
- package/dist/{chunk-R3YKDTVE.js → chunk-NEOAHPMK.js} +1569 -1526
- package/dist/{chunk-DJU54RYW.js → chunk-PGPSIEMW.js} +13 -0
- package/dist/{chunk-P3DCZRER.js → chunk-PPJ5JTWA.js} +1 -0
- package/dist/{chunk-5ELC3KK3.js → chunk-QNJXJIWJ.js} +5 -5
- package/dist/{chunk-JFOPOMK7.js → chunk-WDDUIGFK.js} +13 -0
- package/dist/{chunk-IVXPXYH2.js → chunk-WURF67ID.js} +5 -5
- package/dist/{chunk-QLWMHQPH.js → chunk-YLGBQVRK.js} +1 -0
- package/dist/{coordinator-prompt-5UINTEA6.js → coordinator-prompt-AJ3E6LBX.js} +4 -4
- package/dist/{coordinator-prompt-ATK73HYU.js → coordinator-prompt-PYIRQGU7.js} +4 -4
- package/dist/{global-workspace-TD5SARNW.js → global-workspace-GIQ6F6IJ.js} +4 -4
- package/dist/{global-workspace-ZJBBICFJ.js → global-workspace-GQM653DP.js} +4 -4
- package/dist/index.cjs +574 -464
- package/dist/index.d.cts +43 -3
- package/dist/index.d.ts +43 -3
- package/dist/index.js +70 -38
- package/dist/mcp/run-stdio.cjs +1633 -1544
- package/dist/mcp/run-stdio.js +50 -36
- package/dist/{orchestrator-66ZFQHW5.js → orchestrator-3IOISDFS.js} +7 -7
- package/dist/{orchestrator-VYNPNHY4.js → orchestrator-YDPDMXUY.js} +7 -7
- package/dist/{plan-file-UUKCZEYP.js → plan-file-7MMIPV7Y.js} +9 -1
- package/dist/{plan-file-2EPHYRX7.js → plan-file-CKXUFTQ2.js} +9 -1
- package/dist/{thread-store-YOPKY74K.js → thread-store-JQB7KZJR.js} +1 -1
- package/dist/{thread-store-AYPBEZT5.js → thread-store-SCV2IZYS.js} +1 -1
- package/dist/{workspaces-4SFSCYKT.js → workspaces-OIBSD5B6.js} +4 -4
- package/dist/{workspaces-JX5R4V24.js → workspaces-WYC5TLC3.js} +4 -4
- package/dist/{worktree-Y5FES5D6.js → worktree-5VRZVGP3.js} +3 -3
- package/dist/{worktree-AGVPKDBH.js → worktree-DGDM7LGV.js} +3 -3
- package/package.json +1 -1
package/dist/mcp/run-stdio.cjs
CHANGED
|
@@ -1660,7 +1660,8 @@ function normalizeThread(raw) {
|
|
|
1660
1660
|
runScriptRequest: normalizeRunScriptRequest(raw.runScriptRequest),
|
|
1661
1661
|
quotaResumeAt: raw.quotaResumeAt ?? null,
|
|
1662
1662
|
quotaContinuedFromId: raw.quotaContinuedFromId ?? null,
|
|
1663
|
-
archivedAt: typeof raw.archivedAt === "string" && raw.archivedAt.trim() ? raw.archivedAt.trim() : void 0
|
|
1663
|
+
archivedAt: typeof raw.archivedAt === "string" && raw.archivedAt.trim() ? raw.archivedAt.trim() : void 0,
|
|
1664
|
+
compactedMessages: Array.isArray(raw.compactedMessages) ? raw.compactedMessages : void 0
|
|
1664
1665
|
};
|
|
1665
1666
|
}
|
|
1666
1667
|
function createEmptyThread(partial) {
|
|
@@ -3400,6 +3401,7 @@ function archivedUpdatedMs(thread) {
|
|
|
3400
3401
|
}
|
|
3401
3402
|
function isHistoryRetentionStub(thread) {
|
|
3402
3403
|
if ((thread.attachments?.length ?? 0) > 0) return false;
|
|
3404
|
+
if ((thread.compactedMessages?.length ?? 0) > 0) return false;
|
|
3403
3405
|
const messages = thread.messages ?? [];
|
|
3404
3406
|
if (messages.length === 0) return true;
|
|
3405
3407
|
if (messages.length > 1) return false;
|
|
@@ -9704,6 +9706,10 @@ var init_node_launch = __esm({
|
|
|
9704
9706
|
// src/agents/injected-mcp.ts
|
|
9705
9707
|
function sideboardWorktreeAllowedTools(opts) {
|
|
9706
9708
|
const out = [...SIDEBOARD_ARTIFACT_MCP_ALLOWED_TOOLS];
|
|
9709
|
+
if (!opts?.planMode) {
|
|
9710
|
+
const idx = out.indexOf("mcp__sideboard__present_plan");
|
|
9711
|
+
if (idx >= 0) out.splice(idx, 1);
|
|
9712
|
+
}
|
|
9707
9713
|
if (opts?.github !== false) out.push(...SIDEBOARD_GITHUB_MCP_ALLOWED_TOOLS);
|
|
9708
9714
|
if (opts?.linear) out.push(...SIDEBOARD_LINEAR_MCP_ALLOWED_TOOLS);
|
|
9709
9715
|
if (opts?.abletime) out.push(...SIDEBOARD_ABLETIME_MCP_ALLOWED_TOOLS);
|
|
@@ -10564,6 +10570,9 @@ function skillToolFromTextPart(part, id) {
|
|
|
10564
10570
|
updatedAt: Date.now()
|
|
10565
10571
|
};
|
|
10566
10572
|
}
|
|
10573
|
+
function compactSummaryPart(text7, id = "compact-summary") {
|
|
10574
|
+
return compactToolPart(text7, id, { status: "done" });
|
|
10575
|
+
}
|
|
10567
10576
|
function compactToolPart(text7, id, opts) {
|
|
10568
10577
|
const input = { trigger: opts?.trigger ?? "auto" };
|
|
10569
10578
|
return withPartTimes(
|
|
@@ -10841,6 +10850,17 @@ function partsToAssistantText(parts) {
|
|
|
10841
10850
|
(p) => p.type === "text" && !messagePartParentId(p)
|
|
10842
10851
|
).map((p) => p.text).join("").trim();
|
|
10843
10852
|
}
|
|
10853
|
+
function lastAssistantMessageText(parts) {
|
|
10854
|
+
if (!parts?.length) return "";
|
|
10855
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
10856
|
+
const p = parts[i];
|
|
10857
|
+
if (!p || p.type !== "text" || messagePartParentId(p)) continue;
|
|
10858
|
+
const text7 = p.text.trim();
|
|
10859
|
+
if (!text7 || isInternalAgentStatusText(text7)) continue;
|
|
10860
|
+
return text7;
|
|
10861
|
+
}
|
|
10862
|
+
return "";
|
|
10863
|
+
}
|
|
10844
10864
|
function stripBrightsyNdjsonNoise(text7) {
|
|
10845
10865
|
if (!text7 || !text7.includes('"type"')) return text7;
|
|
10846
10866
|
let out = text7;
|
|
@@ -10878,6 +10898,9 @@ var init_message_parts = __esm({
|
|
|
10878
10898
|
});
|
|
10879
10899
|
|
|
10880
10900
|
// src/agents/types.ts
|
|
10901
|
+
function planModeTurnInstruction(planMode) {
|
|
10902
|
+
return planMode ? PLAN_MODE_INSTRUCTION : PLAN_MODE_OFF_INSTRUCTION;
|
|
10903
|
+
}
|
|
10881
10904
|
function permissionMode(thread) {
|
|
10882
10905
|
if (thread.sourceType === "orchestration") {
|
|
10883
10906
|
return {
|
|
@@ -10916,11 +10939,12 @@ function permissionMode(thread) {
|
|
|
10916
10939
|
codexSandbox: "workspace-write"
|
|
10917
10940
|
};
|
|
10918
10941
|
}
|
|
10919
|
-
var PLAN_MODE_INSTRUCTION;
|
|
10942
|
+
var PLAN_MODE_INSTRUCTION, PLAN_MODE_OFF_INSTRUCTION;
|
|
10920
10943
|
var init_types = __esm({
|
|
10921
10944
|
"src/agents/types.ts"() {
|
|
10922
10945
|
"use strict";
|
|
10923
10946
|
PLAN_MODE_INSTRUCTION = "Plan mode is active and must remain active until the user turns Plan mode off in the UI (or Approves / Hands off the plan). Analyze the codebase, search and read files as needed, and produce or refine a clear implementation plan. Do not modify, create, or delete any project files except via Sideboard MCP present_plan (writes .context/attachments/plan.md). When you need a clarifying decision (approach forks, auth choice, scope) \u2014 not greetings, check-ins, or an invented task menu: (1) first write a short chat message that explains the decision and what each option means (tradeoffs, when to pick it) \u2014 do not leave the user staring at bare labels; (2) then call Sideboard MCP ask_user with the same options, including a description on every option. If one option is the obvious default, proceed without asking. Sideboard shows questions in the composer and mirrors them in chat. After ask_user, wait for the user's next message with their answers before finalizing the plan. When the plan is ready for approval: (1) call present_plan with the full markdown plan (title + content) so Sideboard saves .context/attachments/plan.md and shows it in chat for Approve / Hand off / Copy; (2) Claude should also call ExitPlanMode after present_plan. Do not skip present_plan \u2014 the plan must be a markdown file, not only chat prose.";
|
|
10947
|
+
PLAN_MODE_OFF_INSTRUCTION = "Plan mode is off. Do not call present_plan or ExitPlanMode \u2014 those only work when the user turns Plan on in the composer. If you need to propose an approach, write it in chat.";
|
|
10924
10948
|
}
|
|
10925
10949
|
});
|
|
10926
10950
|
|
|
@@ -11433,7 +11457,8 @@ var init_claude = __esm({
|
|
|
11433
11457
|
...sideboardWorktreeAllowedTools({
|
|
11434
11458
|
github: true,
|
|
11435
11459
|
linear: isLinearConnected(),
|
|
11436
|
-
abletime: isAbleTimeConnected()
|
|
11460
|
+
abletime: isAbleTimeConnected(),
|
|
11461
|
+
planMode: thread.planMode
|
|
11437
11462
|
}),
|
|
11438
11463
|
...brightsyMcpAllowedTools(injectedBrightsyNames)
|
|
11439
11464
|
];
|
|
@@ -13983,12 +14008,38 @@ ${match.text}`, "");
|
|
|
13983
14008
|
blocks.push("Continue from this context. Do not repeat the summary unless asked.");
|
|
13984
14009
|
return blocks.join("\n");
|
|
13985
14010
|
}
|
|
14011
|
+
function compactArchiveProse(message) {
|
|
14012
|
+
if (message.origin === "continue") return null;
|
|
14013
|
+
if (message.role === "summary") return null;
|
|
14014
|
+
let text7 = (message.text ?? "").trim();
|
|
14015
|
+
if (!text7 && message.role === "agent") {
|
|
14016
|
+
text7 = lastAssistantMessageText(message.parts).trim();
|
|
14017
|
+
}
|
|
14018
|
+
if (!text7 || isInternalAgentStatusText(text7)) return null;
|
|
14019
|
+
return { role: message.role, text: text7, ts: message.ts };
|
|
14020
|
+
}
|
|
14021
|
+
function messagesForCompactArchive(older) {
|
|
14022
|
+
const out = [];
|
|
14023
|
+
for (const message of older) {
|
|
14024
|
+
const prose = compactArchiveProse(message);
|
|
14025
|
+
if (prose) out.push(prose);
|
|
14026
|
+
}
|
|
14027
|
+
return out;
|
|
14028
|
+
}
|
|
14029
|
+
function appendCompactedMessages(existing, older) {
|
|
14030
|
+
const next = messagesForCompactArchive(older);
|
|
14031
|
+
if (!existing?.length) return next;
|
|
14032
|
+
if (!next.length) return existing;
|
|
14033
|
+
return [...existing, ...next];
|
|
14034
|
+
}
|
|
13986
14035
|
function applyCompaction(messages, summaryText, thresholds = {}) {
|
|
13987
14036
|
const { older, recent } = splitForCompaction(messages, thresholds);
|
|
13988
14037
|
if (older.length === 0) return messages;
|
|
14038
|
+
const text7 = summaryText.trim();
|
|
13989
14039
|
const summary = {
|
|
13990
14040
|
role: "summary",
|
|
13991
|
-
text:
|
|
14041
|
+
text: text7,
|
|
14042
|
+
parts: [compactSummaryPart(text7)],
|
|
13992
14043
|
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
13993
14044
|
};
|
|
13994
14045
|
return [summary, ...recent];
|
|
@@ -14016,6 +14067,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
|
|
|
14016
14067
|
cwd: thread.worktreePath
|
|
14017
14068
|
});
|
|
14018
14069
|
let messages = applyCompaction(thread.messages, summary, thresholds);
|
|
14070
|
+
const compactedMessages = appendCompactedMessages(thread.compactedMessages, older);
|
|
14019
14071
|
const resetSession = shouldResetSessionForOccupancy({ messages: thread.messages });
|
|
14020
14072
|
if (resetSession) {
|
|
14021
14073
|
messages = applyForwardOccupancy(messages);
|
|
@@ -14023,6 +14075,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
|
|
|
14023
14075
|
const next = {
|
|
14024
14076
|
...thread,
|
|
14025
14077
|
messages,
|
|
14078
|
+
...compactedMessages.length ? { compactedMessages } : { compactedMessages: void 0 },
|
|
14026
14079
|
sessionId: resetSession ? null : thread.sessionId,
|
|
14027
14080
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14028
14081
|
};
|
|
@@ -14038,6 +14091,7 @@ var CONTEXT_COMPACT_CHARS, CONTEXT_KEEP_RECENT_CHARS, CONTEXT_KEEP_RECENT_MESSAG
|
|
|
14038
14091
|
var init_context_compact = __esm({
|
|
14039
14092
|
"src/composer/context-compact.ts"() {
|
|
14040
14093
|
"use strict";
|
|
14094
|
+
init_message_parts();
|
|
14041
14095
|
init_error_detail();
|
|
14042
14096
|
init_usage();
|
|
14043
14097
|
init_context_estimate();
|
|
@@ -14596,1708 +14650,1714 @@ var init_worktree_run_ports = __esm({
|
|
|
14596
14650
|
}
|
|
14597
14651
|
});
|
|
14598
14652
|
|
|
14599
|
-
// src/
|
|
14600
|
-
function
|
|
14601
|
-
const
|
|
14602
|
-
|
|
14603
|
-
|
|
14604
|
-
function readWorktreeInclude(repoPath) {
|
|
14605
|
-
const path = (0, import_node_path39.join)(repoPath, ".worktreeinclude");
|
|
14606
|
-
if (!(0, import_node_fs39.existsSync)(path)) return [];
|
|
14607
|
-
return (0, import_node_fs39.readFileSync)(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
14653
|
+
// src/mcp/wait-for-turn.ts
|
|
14654
|
+
function mcpWaitForTurnTimeoutMs(requested) {
|
|
14655
|
+
const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
|
|
14656
|
+
if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
|
|
14657
|
+
return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
|
|
14608
14658
|
}
|
|
14609
|
-
function
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
14618
|
-
|
|
14619
|
-
|
|
14620
|
-
|
|
14621
|
-
|
|
14622
|
-
|
|
14623
|
-
}
|
|
14624
|
-
}
|
|
14625
|
-
}
|
|
14626
|
-
} catch {
|
|
14627
|
-
}
|
|
14628
|
-
if (matched.length) return [...new Set(matched)];
|
|
14659
|
+
function mcpWaitTaskHint(taskState, status) {
|
|
14660
|
+
switch (taskState) {
|
|
14661
|
+
case "submitted":
|
|
14662
|
+
return MCP_WAIT_QUEUED_HINT;
|
|
14663
|
+
case "working":
|
|
14664
|
+
return MCP_WAIT_STILL_RUNNING_HINT;
|
|
14665
|
+
case "input-required":
|
|
14666
|
+
return MCP_WAIT_INPUT_REQUIRED_HINT;
|
|
14667
|
+
case "canceled":
|
|
14668
|
+
return MCP_WAIT_STOPPED_HINT;
|
|
14669
|
+
case "failed":
|
|
14670
|
+
return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
|
|
14671
|
+
default:
|
|
14672
|
+
return void 0;
|
|
14629
14673
|
}
|
|
14630
|
-
|
|
14631
|
-
|
|
14632
|
-
|
|
14633
|
-
|
|
14634
|
-
|
|
14635
|
-
|
|
14636
|
-
|
|
14637
|
-
|
|
14674
|
+
}
|
|
14675
|
+
var MCP_WAIT_FOR_TURN_MAX_MS, MCP_WAIT_STILL_RUNNING_HINT, MCP_WAIT_QUEUED_HINT, MCP_WAIT_STOPPED_HINT, MCP_WAIT_BROKEN_HINT, MCP_WAIT_ERROR_HINT, MCP_WAIT_INPUT_REQUIRED_HINT;
|
|
14676
|
+
var init_wait_for_turn = __esm({
|
|
14677
|
+
"src/mcp/wait-for-turn.ts"() {
|
|
14678
|
+
"use strict";
|
|
14679
|
+
MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
|
|
14680
|
+
MCP_WAIT_STILL_RUNNING_HINT = "Child is still working. Call wait_for_turn again. Do not send_to_thread a check-in (that steers / interrupts) or assume a hang while progress is updating.";
|
|
14681
|
+
MCP_WAIT_QUEUED_HINT = "Child is queued waiting for a concurrency slot \u2014 it has not started yet. Call wait_for_turn again. Do not send_to_thread a check-in (that steers / interrupts), force_stop, or assume it failed to start.";
|
|
14682
|
+
MCP_WAIT_STOPPED_HINT = "Child was stopped before the turn finished. Do not treat this as success. send_to_thread to resume, or tell the user.";
|
|
14683
|
+
MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
|
|
14684
|
+
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.";
|
|
14685
|
+
MCP_WAIT_INPUT_REQUIRED_HINT = "Child asked the user a question (ask_user). taskState is input-required. Wait for the user to answer in that chat \u2014 do not send_to_thread a check-in.";
|
|
14638
14686
|
}
|
|
14639
|
-
|
|
14640
|
-
|
|
14687
|
+
});
|
|
14688
|
+
|
|
14689
|
+
// src/mcp/wait-for-job.ts
|
|
14690
|
+
function mcpWaitForJobTimeoutMs(requested) {
|
|
14691
|
+
return mcpWaitForTurnTimeoutMs(requested);
|
|
14641
14692
|
}
|
|
14642
|
-
function
|
|
14643
|
-
const
|
|
14644
|
-
|
|
14645
|
-
|
|
14646
|
-
const src = (0, import_node_path39.join)(repoPath, rel);
|
|
14647
|
-
if (!(0, import_node_fs39.existsSync)(src)) continue;
|
|
14648
|
-
const dest = (0, import_node_path39.join)(worktreePath, rel);
|
|
14649
|
-
(0, import_node_fs39.mkdirSync)((0, import_node_path39.dirname)(dest), { recursive: true });
|
|
14650
|
-
(0, import_node_fs39.copyFileSync)(src, dest);
|
|
14651
|
-
copied.push(rel);
|
|
14693
|
+
function sanitizeDetachedJobId(id) {
|
|
14694
|
+
const s = id.trim();
|
|
14695
|
+
if (!JOB_ID_RE.test(s)) {
|
|
14696
|
+
throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
|
|
14652
14697
|
}
|
|
14653
|
-
return
|
|
14698
|
+
return s;
|
|
14654
14699
|
}
|
|
14655
|
-
|
|
14656
|
-
if (
|
|
14657
|
-
const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
|
|
14700
|
+
function jobAlive(pid) {
|
|
14701
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
14658
14702
|
try {
|
|
14659
|
-
|
|
14660
|
-
|
|
14661
|
-
timeout: 5e3
|
|
14662
|
-
});
|
|
14663
|
-
if (result.exitCode === 0 && result.stdout) {
|
|
14664
|
-
const env = { ...process.env };
|
|
14665
|
-
for (const entry of result.stdout.split("\0")) {
|
|
14666
|
-
if (!entry) continue;
|
|
14667
|
-
const eq = entry.indexOf("=");
|
|
14668
|
-
if (eq <= 0) continue;
|
|
14669
|
-
const key = entry.slice(0, eq);
|
|
14670
|
-
const value = entry.slice(eq + 1);
|
|
14671
|
-
if (key) env[key] = value;
|
|
14672
|
-
}
|
|
14673
|
-
cachedLoginEnv = env;
|
|
14674
|
-
return { ...env };
|
|
14675
|
-
}
|
|
14703
|
+
process.kill(pid, 0);
|
|
14704
|
+
return true;
|
|
14676
14705
|
} catch {
|
|
14706
|
+
return false;
|
|
14677
14707
|
}
|
|
14678
|
-
cachedLoginEnv = { ...process.env };
|
|
14679
|
-
return { ...cachedLoginEnv };
|
|
14680
14708
|
}
|
|
14681
|
-
function
|
|
14682
|
-
|
|
14683
|
-
|
|
14684
|
-
|
|
14685
|
-
|
|
14686
|
-
|
|
14687
|
-
|
|
14688
|
-
|
|
14689
|
-
env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
|
|
14690
|
-
env.SIDEBOARD_ROOT_PATH = opts.repoPath;
|
|
14691
|
-
env.SIDEBOARD_IS_LOCAL = "1";
|
|
14692
|
-
if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
|
|
14693
|
-
env.CONDUCTOR_WORKSPACE_NAME = name;
|
|
14694
|
-
env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
|
|
14695
|
-
env.CONDUCTOR_ROOT_PATH = opts.repoPath;
|
|
14696
|
-
env.CONDUCTOR_IS_LOCAL = "1";
|
|
14697
|
-
if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
|
|
14698
|
-
if (primary != null) {
|
|
14699
|
-
env.SIDEBOARD_PORT = String(primary);
|
|
14700
|
-
env.CONDUCTOR_PORT = String(primary);
|
|
14701
|
-
env.PORT = String(primary);
|
|
14702
|
-
for (let i = 1; i < ports.length; i++) {
|
|
14703
|
-
const p = ports[i];
|
|
14704
|
-
env[`SIDEBOARD_PORT_${i}`] = String(p);
|
|
14705
|
-
env[`CONDUCTOR_PORT_${i}`] = String(p);
|
|
14706
|
-
}
|
|
14709
|
+
function processGroupAlive(pgid) {
|
|
14710
|
+
if (process.platform === "win32") return false;
|
|
14711
|
+
if (!Number.isInteger(pgid) || pgid <= 1) return false;
|
|
14712
|
+
try {
|
|
14713
|
+
process.kill(-pgid, 0);
|
|
14714
|
+
return true;
|
|
14715
|
+
} catch (err) {
|
|
14716
|
+
return err && typeof err === "object" && "code" in err && err.code === "EPERM";
|
|
14707
14717
|
}
|
|
14708
|
-
applyWorktreePkgCacheEnv(env, opts.worktreePath);
|
|
14709
|
-
return env;
|
|
14710
|
-
}
|
|
14711
|
-
function pipeLines(stream, onLine) {
|
|
14712
|
-
if (!stream || !onLine) return;
|
|
14713
|
-
const rl = (0, import_node_readline3.createInterface)({ input: stream });
|
|
14714
|
-
rl.on("line", onLine);
|
|
14715
14718
|
}
|
|
14716
|
-
function
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
const pid = Number(token);
|
|
14720
|
-
if (!Number.isFinite(pid) || pid <= 0 || pid === selfPid) continue;
|
|
14721
|
-
pids.push(pid);
|
|
14722
|
-
}
|
|
14723
|
-
return pids;
|
|
14719
|
+
function jobTreeAlive(pid) {
|
|
14720
|
+
if (pid == null) return false;
|
|
14721
|
+
return jobAlive(pid) || processGroupAlive(pid);
|
|
14724
14722
|
}
|
|
14725
|
-
function
|
|
14726
|
-
|
|
14727
|
-
|
|
14728
|
-
|
|
14729
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
14730
|
-
timeout: 2e3
|
|
14731
|
-
});
|
|
14732
|
-
return killableListenerPids(raw, selfPid);
|
|
14733
|
-
} catch {
|
|
14734
|
-
return [];
|
|
14735
|
-
}
|
|
14723
|
+
function readIntFile(file) {
|
|
14724
|
+
if (!(0, import_node_fs39.existsSync)(file)) return null;
|
|
14725
|
+
const n = Number.parseInt((0, import_node_fs39.readFileSync)(file, "utf8").trim(), 10);
|
|
14726
|
+
return Number.isInteger(n) ? n : null;
|
|
14736
14727
|
}
|
|
14737
|
-
function
|
|
14738
|
-
|
|
14739
|
-
try {
|
|
14740
|
-
const raw = (0, import_node_child_process4.execFileSync)("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
|
|
14741
|
-
encoding: "utf8",
|
|
14742
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
14743
|
-
timeout: 2e3
|
|
14744
|
-
});
|
|
14745
|
-
for (const line of raw.split("\n")) {
|
|
14746
|
-
if (line.startsWith("n") && line.length > 1) return line.slice(1);
|
|
14747
|
-
}
|
|
14748
|
-
} catch {
|
|
14749
|
-
}
|
|
14750
|
-
return null;
|
|
14728
|
+
function jobDir(root, id, legacy = false) {
|
|
14729
|
+
return (0, import_node_path39.join)(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
|
|
14751
14730
|
}
|
|
14752
|
-
function
|
|
14753
|
-
|
|
14754
|
-
|
|
14755
|
-
|
|
14756
|
-
|
|
14757
|
-
|
|
14731
|
+
function resolveJobDir(root, id) {
|
|
14732
|
+
const modern = jobDir(root, id, false);
|
|
14733
|
+
if ((0, import_node_fs39.existsSync)(modern)) return modern;
|
|
14734
|
+
const legacy = jobDir(root, id, true);
|
|
14735
|
+
if ((0, import_node_fs39.existsSync)(legacy)) return legacy;
|
|
14736
|
+
return modern;
|
|
14758
14737
|
}
|
|
14759
|
-
function
|
|
14760
|
-
const
|
|
14761
|
-
|
|
14762
|
-
|
|
14763
|
-
const parent = (0, import_node_path39.dirname)(wt);
|
|
14764
|
-
const thisName = (0, import_node_path39.basename)(wt);
|
|
14765
|
-
if (!thisName || c === parent) return false;
|
|
14766
|
-
if (!c.startsWith(parent + import_node_path39.sep)) return false;
|
|
14767
|
-
const first = c.slice(parent.length + 1).split(import_node_path39.sep)[0];
|
|
14768
|
-
return Boolean(first) && first !== thisName;
|
|
14738
|
+
function listJobIdsIn(root, rel) {
|
|
14739
|
+
const dir = (0, import_node_path39.join)(root, rel);
|
|
14740
|
+
if (!(0, import_node_fs39.existsSync)(dir)) return [];
|
|
14741
|
+
return (0, import_node_fs39.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && JOB_ID_RE.test(e.name)).map((e) => e.name);
|
|
14769
14742
|
}
|
|
14770
|
-
function
|
|
14771
|
-
|
|
14772
|
-
|
|
14773
|
-
|
|
14743
|
+
function listRunningDetachedJobs(worktreePath) {
|
|
14744
|
+
const root = worktreePath.trim();
|
|
14745
|
+
if (!root) return [];
|
|
14746
|
+
const ids = /* @__PURE__ */ new Set([
|
|
14747
|
+
...listJobIdsIn(root, DETACHED_JOBS_DIR),
|
|
14748
|
+
...listJobIdsIn(root, LEGACY_DETACHED_JOBS_DIR)
|
|
14749
|
+
]);
|
|
14750
|
+
const running = [];
|
|
14751
|
+
for (const id of ids) {
|
|
14752
|
+
const dir = resolveJobDir(root, id);
|
|
14753
|
+
const pid = readIntFile((0, import_node_path39.join)(dir, "pid"));
|
|
14754
|
+
if (jobTreeAlive(pid)) running.push(id);
|
|
14774
14755
|
}
|
|
14756
|
+
return running.sort();
|
|
14775
14757
|
}
|
|
14776
|
-
function
|
|
14777
|
-
const
|
|
14778
|
-
|
|
14779
|
-
|
|
14758
|
+
function looksLikeDeferredDonePromise(text7) {
|
|
14759
|
+
const t = (text7 ?? "").trim();
|
|
14760
|
+
if (!t || t.length > DEFERRED_DONE_MAX_CHARS) return false;
|
|
14761
|
+
const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
|
|
14762
|
+
if (!unquoted) return false;
|
|
14763
|
+
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);
|
|
14780
14764
|
}
|
|
14781
|
-
function
|
|
14782
|
-
if (
|
|
14783
|
-
const
|
|
14784
|
-
|
|
14785
|
-
try {
|
|
14786
|
-
process.kill(-pid, "SIGTERM");
|
|
14787
|
-
} catch {
|
|
14788
|
-
signalPid(pid, "SIGTERM");
|
|
14789
|
-
}
|
|
14765
|
+
function isJobContinuePrompt(text7) {
|
|
14766
|
+
if (typeof text7 !== "string") return false;
|
|
14767
|
+
const t = text7.trim();
|
|
14768
|
+
return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
|
|
14790
14769
|
}
|
|
14791
|
-
function
|
|
14792
|
-
|
|
14793
|
-
|
|
14794
|
-
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
|
|
14802
|
-
`Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -ne ${selfPid} } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
|
|
14803
|
-
],
|
|
14804
|
-
{ stdio: "ignore", timeout: 3e3 }
|
|
14805
|
-
);
|
|
14806
|
-
continue;
|
|
14807
|
-
}
|
|
14808
|
-
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
14809
|
-
signalPid(pid, "SIGTERM");
|
|
14810
|
-
}
|
|
14811
|
-
} catch {
|
|
14812
|
-
}
|
|
14813
|
-
}
|
|
14770
|
+
function collapseGhRunWatchLog(text7) {
|
|
14771
|
+
if (!text7.includes(GH_WATCH_REFRESH)) return text7;
|
|
14772
|
+
const frames = text7.split(/(?=Refreshing run status every 3 seconds\.)/);
|
|
14773
|
+
const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
|
|
14774
|
+
if (watchFrames.length <= 1) return text7;
|
|
14775
|
+
const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
|
|
14776
|
+
const last = watchFrames[watchFrames.length - 1] ?? "";
|
|
14777
|
+
const omitted = watchFrames.length - 1;
|
|
14778
|
+
const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
|
|
14779
|
+
`;
|
|
14780
|
+
return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
|
|
14814
14781
|
}
|
|
14815
|
-
function
|
|
14816
|
-
|
|
14817
|
-
|
|
14818
|
-
|
|
14819
|
-
|
|
14820
|
-
if (
|
|
14821
|
-
|
|
14822
|
-
|
|
14823
|
-
}
|
|
14824
|
-
const selfPid = process.pid;
|
|
14825
|
-
for (const port of ports) {
|
|
14826
|
-
if (!Number.isFinite(port) || port <= 0) continue;
|
|
14827
|
-
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
14828
|
-
const cwd = listenerCwd(pid);
|
|
14829
|
-
if (cwd && isOtherWorktreeListenerCwd(cwd, worktreePath)) continue;
|
|
14830
|
-
signalPid(pid, signal);
|
|
14831
|
-
}
|
|
14782
|
+
function capJobLogDelta(text7) {
|
|
14783
|
+
const collapsed = collapseGhRunWatchLog(text7);
|
|
14784
|
+
const lines = collapsed.split("\n");
|
|
14785
|
+
let out = lines.length > MAX_JOB_DELTA_LINES ? `\u2026(${lines.length - MAX_JOB_DELTA_LINES} earlier lines omitted)
|
|
14786
|
+
${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
|
|
14787
|
+
if (out.length > MAX_JOB_DELTA_CHARS) {
|
|
14788
|
+
out = `\u2026(truncated ${out.length} chars)
|
|
14789
|
+
${out.slice(-MAX_JOB_DELTA_CHARS)}`;
|
|
14832
14790
|
}
|
|
14791
|
+
return out;
|
|
14833
14792
|
}
|
|
14834
|
-
function
|
|
14835
|
-
const
|
|
14836
|
-
|
|
14837
|
-
|
|
14838
|
-
|
|
14839
|
-
|
|
14840
|
-
|
|
14841
|
-
|
|
14842
|
-
setTimeout(() => {
|
|
14843
|
-
try {
|
|
14844
|
-
process.kill(-pid, "SIGKILL");
|
|
14845
|
-
} catch {
|
|
14846
|
-
}
|
|
14847
|
-
}, 2500).unref?.();
|
|
14848
|
-
}
|
|
14849
|
-
} catch {
|
|
14850
|
-
try {
|
|
14851
|
-
child.kill("SIGTERM");
|
|
14852
|
-
} catch {
|
|
14853
|
-
}
|
|
14854
|
-
}
|
|
14855
|
-
}
|
|
14856
|
-
killListenersOnPorts(ports);
|
|
14793
|
+
function formatJobStillRunningContinuePrompt(jobIds) {
|
|
14794
|
+
const ids = jobIds.join(", ");
|
|
14795
|
+
return [
|
|
14796
|
+
`Detached job still running: ${ids}.`,
|
|
14797
|
+
"Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
|
|
14798
|
+
"If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
|
|
14799
|
+
"Then report the result. Do not tell the user you will let them know later."
|
|
14800
|
+
].join(" ");
|
|
14857
14801
|
}
|
|
14858
|
-
|
|
14859
|
-
|
|
14860
|
-
|
|
14861
|
-
|
|
14862
|
-
|
|
14863
|
-
|
|
14864
|
-
|
|
14865
|
-
|
|
14866
|
-
|
|
14867
|
-
|
|
14868
|
-
|
|
14869
|
-
|
|
14870
|
-
|
|
14871
|
-
mergeAgentGitAuthEnv(
|
|
14872
|
-
env,
|
|
14873
|
-
await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
|
|
14874
|
-
);
|
|
14875
|
-
} catch {
|
|
14876
|
-
}
|
|
14877
|
-
if (opts.heldPorts?.length) {
|
|
14878
|
-
await Promise.all(opts.heldPorts.map((h) => h.release()));
|
|
14879
|
-
}
|
|
14880
|
-
const shell = process.platform === "darwin" ? "zsh" : "bash";
|
|
14881
|
-
const child = (0, import_execa4.execa)(shell, ["-lc", command], {
|
|
14882
|
-
cwd: opts.worktreePath,
|
|
14883
|
-
reject: false,
|
|
14884
|
-
env,
|
|
14885
|
-
// Own process group so Stop can tear down the whole tree (not just the shell).
|
|
14886
|
-
// There is no settings.toml `stop=` / teardown hook for run scripts.
|
|
14887
|
-
...process.platform === "win32" ? {} : { detached: true }
|
|
14802
|
+
function formatDeferredDoneContinuePrompt() {
|
|
14803
|
+
return [
|
|
14804
|
+
"You ended the turn after promising to report later, but no detached job is running.",
|
|
14805
|
+
"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).",
|
|
14806
|
+
"Do not say you will let the user know later."
|
|
14807
|
+
].join(" ");
|
|
14808
|
+
}
|
|
14809
|
+
function turnWatchedDetachedJob(parts) {
|
|
14810
|
+
return parts.some((p) => {
|
|
14811
|
+
if (p.type !== "tool") return false;
|
|
14812
|
+
if (/wait_for_job$/i.test(p.name ?? "") || /stop_job$/i.test(p.name ?? "")) return true;
|
|
14813
|
+
const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
|
|
14814
|
+
return /detached-job\.(?:js|cjs)\b/i.test(blob);
|
|
14888
14815
|
});
|
|
14889
|
-
pipeLines(child.stdout, opts.onLine);
|
|
14890
|
-
pipeLines(child.stderr, opts.onLine);
|
|
14891
|
-
const ports = opts.ports ?? [];
|
|
14892
|
-
return {
|
|
14893
|
-
pid: child.pid,
|
|
14894
|
-
kill: () => killScriptTree(child, ports),
|
|
14895
|
-
done: child.then((r) => r.exitCode ?? null),
|
|
14896
|
-
child
|
|
14897
|
-
};
|
|
14898
14816
|
}
|
|
14899
|
-
|
|
14900
|
-
if (
|
|
14901
|
-
|
|
14902
|
-
|
|
14903
|
-
|
|
14817
|
+
function planJobContinue(opts) {
|
|
14818
|
+
if (opts.isOrchestrator) return { action: "none" };
|
|
14819
|
+
if (opts.agent === "brightsy") return { action: "none" };
|
|
14820
|
+
if (opts.queueLength > 0) return { action: "none" };
|
|
14821
|
+
if (opts.continueCount >= MAX_JOB_CONTINUES) return { action: "none" };
|
|
14822
|
+
const farewell = looksLikeDeferredDonePromise(opts.chatText);
|
|
14823
|
+
if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
|
|
14824
|
+
return {
|
|
14825
|
+
action: "wait",
|
|
14826
|
+
jobIds: opts.runningJobIds,
|
|
14827
|
+
prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
|
|
14828
|
+
};
|
|
14904
14829
|
}
|
|
14905
|
-
|
|
14830
|
+
if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
|
|
14831
|
+
return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
|
|
14832
|
+
}
|
|
14833
|
+
return { action: "none" };
|
|
14906
14834
|
}
|
|
14907
|
-
|
|
14908
|
-
|
|
14909
|
-
|
|
14910
|
-
|
|
14911
|
-
|
|
14912
|
-
|
|
14913
|
-
|
|
14914
|
-
|
|
14915
|
-
|
|
14916
|
-
|
|
14917
|
-
|
|
14918
|
-
const
|
|
14835
|
+
function tailProgress(logFile, maxLines = 12) {
|
|
14836
|
+
if (!(0, import_node_fs39.existsSync)(logFile)) return "(no log yet)";
|
|
14837
|
+
const lines = (0, import_node_fs39.readFileSync)(logFile, "utf8").split("\n");
|
|
14838
|
+
return lines.slice(-maxLines).join("\n");
|
|
14839
|
+
}
|
|
14840
|
+
function lastProgressLine(progress) {
|
|
14841
|
+
const last = progress.split("\n").map((l) => l.trim()).filter((l) => l && l !== "(no log yet)").at(-1);
|
|
14842
|
+
return last ? last.slice(0, 72) : void 0;
|
|
14843
|
+
}
|
|
14844
|
+
function readLogLines(file) {
|
|
14845
|
+
if (!(0, import_node_fs39.existsSync)(file)) return [];
|
|
14846
|
+
const lines = (0, import_node_fs39.readFileSync)(file, "utf8").split("\n");
|
|
14847
|
+
if (lines.length && lines[lines.length - 1] === "") lines.pop();
|
|
14848
|
+
return lines;
|
|
14849
|
+
}
|
|
14850
|
+
function takeDelta(logFile, cursorFile) {
|
|
14851
|
+
const lines = readLogLines(logFile);
|
|
14852
|
+
const cursor = readIntFile(cursorFile) ?? 0;
|
|
14853
|
+
const start = Math.min(Math.max(0, cursor), lines.length);
|
|
14854
|
+
return { delta: capJobLogDelta(lines.slice(start).join("\n")), nextCursor: lines.length };
|
|
14855
|
+
}
|
|
14856
|
+
function snapshotJob(dir) {
|
|
14857
|
+
const pid = readIntFile((0, import_node_path39.join)(dir, "pid"));
|
|
14858
|
+
const running = jobTreeAlive(pid);
|
|
14919
14859
|
return {
|
|
14920
|
-
|
|
14921
|
-
|
|
14922
|
-
|
|
14923
|
-
|
|
14860
|
+
pid,
|
|
14861
|
+
running,
|
|
14862
|
+
exitCode: readIntFile((0, import_node_path39.join)(dir, "exit")),
|
|
14863
|
+
log: (0, import_node_path39.join)(dir, "log"),
|
|
14864
|
+
cursor: (0, import_node_path39.join)(dir, "present.cursor"),
|
|
14865
|
+
progress: tailProgress((0, import_node_path39.join)(dir, "log"))
|
|
14924
14866
|
};
|
|
14925
14867
|
}
|
|
14926
|
-
|
|
14927
|
-
const
|
|
14928
|
-
|
|
14929
|
-
|
|
14930
|
-
const
|
|
14931
|
-
|
|
14932
|
-
|
|
14933
|
-
|
|
14934
|
-
|
|
14935
|
-
}
|
|
14936
|
-
|
|
14868
|
+
function toResult(id, snap, extra) {
|
|
14869
|
+
const failed = extra?.failed === true || !snap.running && snap.exitCode != null && snap.exitCode !== 0;
|
|
14870
|
+
const ok = !snap.running && snap.exitCode === 0;
|
|
14871
|
+
const stillRunning = snap.running && !ok;
|
|
14872
|
+
const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
|
|
14873
|
+
try {
|
|
14874
|
+
(0, import_node_fs39.mkdirSync)((0, import_node_path39.dirname)(snap.cursor), { recursive: true });
|
|
14875
|
+
(0, import_node_fs39.writeFileSync)(snap.cursor, `${nextCursor}
|
|
14876
|
+
`);
|
|
14877
|
+
} catch {
|
|
14878
|
+
}
|
|
14879
|
+
const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
|
|
14880
|
+
const progress = extra?.progress ?? snap.progress;
|
|
14937
14881
|
return {
|
|
14938
|
-
|
|
14939
|
-
|
|
14940
|
-
|
|
14941
|
-
|
|
14882
|
+
stillRunning,
|
|
14883
|
+
ok,
|
|
14884
|
+
failed: Boolean(failed && !stillRunning && !ok),
|
|
14885
|
+
status,
|
|
14886
|
+
id,
|
|
14887
|
+
pid: snap.pid,
|
|
14888
|
+
exitCode: snap.exitCode ?? void 0,
|
|
14889
|
+
phase: lastProgressLine(progress),
|
|
14890
|
+
delta,
|
|
14891
|
+
progress,
|
|
14892
|
+
hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
|
|
14942
14893
|
};
|
|
14943
14894
|
}
|
|
14944
|
-
async function
|
|
14945
|
-
await
|
|
14946
|
-
const review = ensureReviewGuidelinesFile(worktreePath, repoPath);
|
|
14947
|
-
if (review.wrote) {
|
|
14948
|
-
onLine?.(`[setup] wrote ${review.path} \u2014 local Review guidelines`);
|
|
14949
|
-
}
|
|
14950
|
-
let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
|
|
14951
|
-
if (!setup.ran) {
|
|
14952
|
-
setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
|
|
14953
|
-
}
|
|
14954
|
-
if (!setup.ran) {
|
|
14955
|
-
setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
|
|
14956
|
-
}
|
|
14957
|
-
return setup;
|
|
14895
|
+
async function sleepMs(ms) {
|
|
14896
|
+
await new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
14958
14897
|
}
|
|
14959
|
-
async function
|
|
14960
|
-
const
|
|
14961
|
-
|
|
14962
|
-
const
|
|
14963
|
-
|
|
14964
|
-
|
|
14965
|
-
|
|
14966
|
-
|
|
14967
|
-
|
|
14968
|
-
|
|
14969
|
-
|
|
14970
|
-
|
|
14971
|
-
|
|
14972
|
-
|
|
14973
|
-
|
|
14974
|
-
|
|
14975
|
-
|
|
14976
|
-
});
|
|
14977
|
-
}
|
|
14978
|
-
function getDefaultRunScript(worktreePath, repoPath) {
|
|
14979
|
-
const scripts = listRunScripts(worktreePath, repoPath);
|
|
14980
|
-
if (!scripts.length) return null;
|
|
14981
|
-
return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
|
|
14982
|
-
}
|
|
14983
|
-
function getRunScript(worktreePath, repoPath, name) {
|
|
14984
|
-
const scripts = listRunScripts(worktreePath, repoPath);
|
|
14985
|
-
if (!scripts.length) return null;
|
|
14986
|
-
if (name) {
|
|
14987
|
-
return scripts.find((s) => s.name === name) ?? null;
|
|
14898
|
+
async function waitForDetachedJob(cwd, id, opts) {
|
|
14899
|
+
const jobId = sanitizeDetachedJobId(id);
|
|
14900
|
+
const root = cwd.trim() || process.cwd();
|
|
14901
|
+
const dir = resolveJobDir(root, jobId);
|
|
14902
|
+
const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
|
|
14903
|
+
const sleep = opts?.sleep ?? sleepMs;
|
|
14904
|
+
if (!(0, import_node_fs39.existsSync)(dir)) {
|
|
14905
|
+
return {
|
|
14906
|
+
stillRunning: false,
|
|
14907
|
+
ok: false,
|
|
14908
|
+
failed: true,
|
|
14909
|
+
status: "failed",
|
|
14910
|
+
id: jobId,
|
|
14911
|
+
delta: "",
|
|
14912
|
+
progress: "No detached job. Start one first.",
|
|
14913
|
+
hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
|
|
14914
|
+
};
|
|
14988
14915
|
}
|
|
14989
|
-
|
|
14990
|
-
|
|
14991
|
-
|
|
14992
|
-
|
|
14993
|
-
|
|
14994
|
-
|
|
14995
|
-
|
|
14996
|
-
const server = (0, import_node_net.createServer)();
|
|
14997
|
-
let released = false;
|
|
14998
|
-
const release = () => new Promise((res) => {
|
|
14999
|
-
if (released) {
|
|
15000
|
-
res();
|
|
15001
|
-
return;
|
|
15002
|
-
}
|
|
15003
|
-
released = true;
|
|
15004
|
-
server.close(() => res());
|
|
15005
|
-
});
|
|
15006
|
-
server.listen(0, "127.0.0.1", () => {
|
|
15007
|
-
const addr = server.address();
|
|
15008
|
-
if (!addr || typeof addr === "string") {
|
|
15009
|
-
void release().then(() => reject(new Error("Failed to allocate port")));
|
|
15010
|
-
return;
|
|
15011
|
-
}
|
|
15012
|
-
resolve2({ port: addr.port, release });
|
|
15013
|
-
});
|
|
15014
|
-
server.on("error", reject);
|
|
15015
|
-
});
|
|
15016
|
-
}
|
|
15017
|
-
async function tryReservePort(port) {
|
|
15018
|
-
if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
|
|
15019
|
-
return new Promise((resolve2) => {
|
|
15020
|
-
const server = (0, import_node_net.createServer)();
|
|
15021
|
-
let released = false;
|
|
15022
|
-
const release = () => new Promise((res) => {
|
|
15023
|
-
if (released) {
|
|
15024
|
-
res();
|
|
15025
|
-
return;
|
|
15026
|
-
}
|
|
15027
|
-
released = true;
|
|
15028
|
-
server.close(() => res());
|
|
15029
|
-
});
|
|
15030
|
-
server.once("error", () => {
|
|
15031
|
-
try {
|
|
15032
|
-
server.close();
|
|
15033
|
-
} catch {
|
|
15034
|
-
}
|
|
15035
|
-
resolve2(null);
|
|
15036
|
-
});
|
|
15037
|
-
server.listen(port, "127.0.0.1", () => {
|
|
15038
|
-
resolve2({ port, release });
|
|
14916
|
+
const deadline = Date.now() + timeoutMs;
|
|
14917
|
+
let snap = snapshotJob(dir);
|
|
14918
|
+
if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
|
|
14919
|
+
if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
|
|
14920
|
+
return toResult(jobId, snap, {
|
|
14921
|
+
failed: true,
|
|
14922
|
+
progress: "No detached job. Start one first."
|
|
15039
14923
|
});
|
|
15040
|
-
});
|
|
15041
|
-
}
|
|
15042
|
-
function normalizePreferredPorts(ports, size) {
|
|
15043
|
-
if (!ports?.length) return [];
|
|
15044
|
-
const seen = /* @__PURE__ */ new Set();
|
|
15045
|
-
const out = [];
|
|
15046
|
-
for (const p of ports) {
|
|
15047
|
-
if (!Number.isInteger(p) || p <= 0 || p > 65535 || seen.has(p)) continue;
|
|
15048
|
-
seen.add(p);
|
|
15049
|
-
out.push(p);
|
|
15050
|
-
if (out.length >= size) break;
|
|
15051
14924
|
}
|
|
15052
|
-
|
|
15053
|
-
|
|
15054
|
-
|
|
15055
|
-
|
|
14925
|
+
while (Date.now() < deadline) {
|
|
14926
|
+
snap = snapshotJob(dir);
|
|
14927
|
+
if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
|
|
14928
|
+
if (!snap.running) return toResult(jobId, snap, { failed: true });
|
|
14929
|
+
await sleep(Math.min(2e3, Math.max(50, deadline - Date.now())));
|
|
14930
|
+
}
|
|
14931
|
+
snap = snapshotJob(dir);
|
|
14932
|
+
return toResult(jobId, snap);
|
|
15056
14933
|
}
|
|
15057
|
-
|
|
15058
|
-
if (
|
|
15059
|
-
|
|
15060
|
-
|
|
15061
|
-
|
|
15062
|
-
|
|
15063
|
-
|
|
15064
|
-
const candidate = base != null ? base + offset : null;
|
|
15065
|
-
offset += 1;
|
|
15066
|
-
if (candidate != null && taken.has(candidate)) continue;
|
|
15067
|
-
const next = candidate != null ? await tryReservePort(candidate) : null;
|
|
15068
|
-
if (next) {
|
|
15069
|
-
held.push(next);
|
|
15070
|
-
taken.add(next.port);
|
|
15071
|
-
} else {
|
|
15072
|
-
const ephemeral = await reservePort();
|
|
15073
|
-
held.push(ephemeral);
|
|
15074
|
-
taken.add(ephemeral.port);
|
|
15075
|
-
}
|
|
14934
|
+
function killProcessTree(pid, signal) {
|
|
14935
|
+
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
14936
|
+
if (process.platform !== "win32") {
|
|
14937
|
+
try {
|
|
14938
|
+
process.kill(-pid, signal);
|
|
14939
|
+
return;
|
|
14940
|
+
} catch {
|
|
15076
14941
|
}
|
|
15077
|
-
return held;
|
|
15078
|
-
} catch (err) {
|
|
15079
|
-
await Promise.all(held.map((h) => h.release()));
|
|
15080
|
-
throw err;
|
|
15081
14942
|
}
|
|
15082
|
-
|
|
15083
|
-
|
|
15084
|
-
|
|
15085
|
-
for (const port of ports) {
|
|
15086
|
-
const next = await tryReservePort(port);
|
|
15087
|
-
if (next) held.push(next);
|
|
14943
|
+
try {
|
|
14944
|
+
process.kill(pid, signal);
|
|
14945
|
+
} catch {
|
|
15088
14946
|
}
|
|
15089
|
-
return held;
|
|
15090
14947
|
}
|
|
15091
|
-
|
|
15092
|
-
|
|
15093
|
-
|
|
15094
|
-
|
|
15095
|
-
|
|
15096
|
-
|
|
15097
|
-
|
|
15098
|
-
return null;
|
|
15099
|
-
};
|
|
15100
|
-
let held = await takePrimary();
|
|
15101
|
-
if (held) return held;
|
|
15102
|
-
if (worktreePath) {
|
|
15103
|
-
killStaleWorktreeListeners(ports, worktreePath, "SIGTERM");
|
|
15104
|
-
} else {
|
|
15105
|
-
killListenersOnPorts(ports);
|
|
15106
|
-
}
|
|
15107
|
-
for (let i = 0; i < RECLAIM_TRIES; i++) {
|
|
15108
|
-
await delay(RECLAIM_RETRY_DELAY_MS);
|
|
15109
|
-
held = await takePrimary();
|
|
15110
|
-
if (held) return held;
|
|
15111
|
-
if (i === 2 && worktreePath) {
|
|
15112
|
-
killStaleWorktreeListeners(ports, worktreePath, "SIGKILL");
|
|
15113
|
-
}
|
|
14948
|
+
function appendJobLog(logFile, line) {
|
|
14949
|
+
try {
|
|
14950
|
+
(0, import_node_fs39.mkdirSync)((0, import_node_path39.dirname)(logFile), { recursive: true });
|
|
14951
|
+
const body = line.endsWith("\n") ? line : `${line}
|
|
14952
|
+
`;
|
|
14953
|
+
(0, import_node_fs39.appendFileSync)(logFile, body);
|
|
14954
|
+
} catch {
|
|
15114
14955
|
}
|
|
15115
|
-
return null;
|
|
15116
14956
|
}
|
|
15117
|
-
async function
|
|
15118
|
-
const
|
|
15119
|
-
|
|
15120
|
-
|
|
15121
|
-
|
|
14957
|
+
async function stopDetachedJob(cwd, id, opts) {
|
|
14958
|
+
const jobId = sanitizeDetachedJobId(id);
|
|
14959
|
+
const root = cwd.trim() || process.cwd();
|
|
14960
|
+
const dir = resolveJobDir(root, jobId);
|
|
14961
|
+
const sleep = opts?.sleep ?? sleepMs;
|
|
14962
|
+
const why = opts?.reason?.trim() ?? "";
|
|
14963
|
+
if (!(0, import_node_fs39.existsSync)(dir)) {
|
|
14964
|
+
return {
|
|
14965
|
+
stillRunning: false,
|
|
14966
|
+
ok: false,
|
|
14967
|
+
failed: true,
|
|
14968
|
+
status: "failed",
|
|
14969
|
+
id: jobId,
|
|
14970
|
+
delta: "",
|
|
14971
|
+
progress: "No detached job. Start one first.",
|
|
14972
|
+
hint: "Start with detached-job.cjs start <id> -- <command>, then wait_for_job or stop_job.",
|
|
14973
|
+
stopped: false,
|
|
14974
|
+
reason: "not-found"
|
|
14975
|
+
};
|
|
15122
14976
|
}
|
|
15123
|
-
|
|
15124
|
-
|
|
15125
|
-
|
|
15126
|
-
async function startDevServer(repoPath, worktreePath, onLine, opts) {
|
|
15127
|
-
const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
|
|
15128
|
-
if (!script) return null;
|
|
15129
|
-
const preferred = loadWorktreeRunPorts(worktreePath, script.name) ?? opts?.preferredPorts;
|
|
15130
|
-
const held = await reservePortRange(PORT_RANGE_SIZE, {
|
|
15131
|
-
preferred: preferred ?? void 0,
|
|
15132
|
-
worktreePath
|
|
15133
|
-
});
|
|
15134
|
-
const ports = held.map((h) => h.port);
|
|
15135
|
-
saveWorktreeRunPorts(worktreePath, script.name, ports);
|
|
15136
|
-
try {
|
|
15137
|
-
const handle = await spawnWorkspaceScript(script.command, {
|
|
15138
|
-
worktreePath,
|
|
15139
|
-
repoPath,
|
|
15140
|
-
ports,
|
|
15141
|
-
heldPorts: held,
|
|
15142
|
-
defaultBranch: opts?.defaultBranch,
|
|
15143
|
-
onLine
|
|
15144
|
-
});
|
|
14977
|
+
let snap = snapshotJob(dir);
|
|
14978
|
+
if (!snap.running) {
|
|
14979
|
+
const result2 = toResult(jobId, snap);
|
|
15145
14980
|
return {
|
|
15146
|
-
|
|
15147
|
-
|
|
15148
|
-
|
|
15149
|
-
|
|
15150
|
-
kill: handle.kill,
|
|
15151
|
-
done: handle.done
|
|
14981
|
+
...result2,
|
|
14982
|
+
stopped: false,
|
|
14983
|
+
reason: "not-running",
|
|
14984
|
+
hint: "That job is already finished. Read the log; do not wait again unless you start a new command."
|
|
15152
14985
|
};
|
|
15153
|
-
} catch (err) {
|
|
15154
|
-
await Promise.all(held.map((h) => h.release()));
|
|
15155
|
-
throw err;
|
|
15156
14986
|
}
|
|
15157
|
-
}
|
|
15158
|
-
|
|
15159
|
-
|
|
15160
|
-
|
|
14987
|
+
appendJobLog(snap.log, `$ stop${why ? `: ${why}` : ""}`);
|
|
14988
|
+
const targetPid = snap.pid;
|
|
14989
|
+
if (targetPid != null) killProcessTree(targetPid, "SIGTERM");
|
|
14990
|
+
const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
|
|
14991
|
+
const deadline = Date.now() + graceMs;
|
|
14992
|
+
while (Date.now() < deadline && jobTreeAlive(targetPid)) {
|
|
14993
|
+
await sleep(Math.min(100, Math.max(20, deadline - Date.now())));
|
|
14994
|
+
}
|
|
14995
|
+
if (targetPid != null) {
|
|
14996
|
+
killProcessTree(targetPid, "SIGKILL");
|
|
14997
|
+
await sleep(100);
|
|
14998
|
+
}
|
|
14999
|
+
const exitFile = (0, import_node_path39.join)(dir, "exit");
|
|
15000
|
+
if (!(0, import_node_fs39.existsSync)(exitFile)) {
|
|
15001
|
+
try {
|
|
15002
|
+
(0, import_node_fs39.writeFileSync)(exitFile, "1\n");
|
|
15003
|
+
} catch {
|
|
15004
|
+
}
|
|
15005
|
+
}
|
|
15006
|
+
snap = snapshotJob(dir);
|
|
15007
|
+
const stillRunning = jobTreeAlive(targetPid) || snap.running;
|
|
15008
|
+
const result = toResult(jobId, snap, { failed: true });
|
|
15009
|
+
const stopped = !stillRunning;
|
|
15010
|
+
return {
|
|
15011
|
+
...result,
|
|
15012
|
+
stillRunning,
|
|
15013
|
+
ok: false,
|
|
15014
|
+
failed: true,
|
|
15015
|
+
status: stillRunning ? "running" : "failed",
|
|
15016
|
+
stopped,
|
|
15017
|
+
reason: stopped ? "stopped" : "still-running",
|
|
15018
|
+
stopReason: why || void 0,
|
|
15019
|
+
hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
|
|
15020
|
+
};
|
|
15021
|
+
}
|
|
15022
|
+
var import_node_fs39, import_node_path39, MAX_JOB_CONTINUES, MAX_JOB_DELTA_LINES, MAX_JOB_DELTA_CHARS, DEFERRED_DONE_MAX_CHARS, GH_WATCH_REFRESH, MCP_WAIT_JOB_STILL_RUNNING_HINT, MCP_STOP_JOB_HINT, JOB_ID_RE;
|
|
15023
|
+
var init_wait_for_job = __esm({
|
|
15024
|
+
"src/mcp/wait-for-job.ts"() {
|
|
15161
15025
|
"use strict";
|
|
15162
15026
|
import_node_fs39 = require("fs");
|
|
15163
|
-
import_node_child_process4 = require("child_process");
|
|
15164
|
-
import_node_net = require("net");
|
|
15165
15027
|
import_node_path39 = require("path");
|
|
15166
|
-
|
|
15167
|
-
|
|
15168
|
-
|
|
15169
|
-
|
|
15170
|
-
|
|
15171
|
-
|
|
15172
|
-
|
|
15173
|
-
|
|
15174
|
-
|
|
15175
|
-
|
|
15176
|
-
init_worktree_run_ports();
|
|
15177
|
-
init_worktree_run_ports();
|
|
15178
|
-
init_nested_electron_env();
|
|
15179
|
-
PORT_RANGE_SIZE = 10;
|
|
15180
|
-
RECLAIM_RETRY_DELAY_MS = 40;
|
|
15181
|
-
RECLAIM_TRIES = 8;
|
|
15182
|
-
cachedLoginEnv = null;
|
|
15028
|
+
init_workspace_scratch();
|
|
15029
|
+
init_wait_for_turn();
|
|
15030
|
+
MAX_JOB_CONTINUES = 8;
|
|
15031
|
+
MAX_JOB_DELTA_LINES = 80;
|
|
15032
|
+
MAX_JOB_DELTA_CHARS = 4e3;
|
|
15033
|
+
DEFERRED_DONE_MAX_CHARS = 480;
|
|
15034
|
+
GH_WATCH_REFRESH = "Refreshing run status every 3 seconds.";
|
|
15035
|
+
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.";
|
|
15036
|
+
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.";
|
|
15037
|
+
JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
|
|
15183
15038
|
}
|
|
15184
15039
|
});
|
|
15185
15040
|
|
|
15186
|
-
// src/
|
|
15187
|
-
function
|
|
15188
|
-
const
|
|
15189
|
-
|
|
15190
|
-
if (next.length <= max) return next;
|
|
15191
|
-
const cut = next.slice(next.length - max);
|
|
15192
|
-
const nl = cut.indexOf("\n");
|
|
15193
|
-
return nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
|
|
15041
|
+
// src/hook/conductor.ts
|
|
15042
|
+
function matchSimpleGlob(pattern, name) {
|
|
15043
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
15044
|
+
return new RegExp(`^${escaped}$`).test(name);
|
|
15194
15045
|
}
|
|
15195
|
-
function
|
|
15196
|
-
|
|
15197
|
-
|
|
15198
|
-
|
|
15199
|
-
running: snap.running,
|
|
15200
|
-
exitCode: snap.exitCode,
|
|
15201
|
-
source: snap.source
|
|
15202
|
-
};
|
|
15046
|
+
function readWorktreeInclude(repoPath) {
|
|
15047
|
+
const path = (0, import_node_path40.join)(repoPath, ".worktreeinclude");
|
|
15048
|
+
if (!(0, import_node_fs40.existsSync)(path)) return [];
|
|
15049
|
+
return (0, import_node_fs40.readFileSync)(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
15203
15050
|
}
|
|
15204
|
-
function
|
|
15205
|
-
|
|
15206
|
-
if (
|
|
15207
|
-
|
|
15208
|
-
|
|
15209
|
-
|
|
15051
|
+
function resolveFilesToCopy(repoPath) {
|
|
15052
|
+
const fromInclude = readWorktreeInclude(repoPath);
|
|
15053
|
+
if (fromInclude.length) return fromInclude;
|
|
15054
|
+
const settings = loadRepoSettings(repoPath);
|
|
15055
|
+
if (settings?.filesToCopy?.length) return settings.filesToCopy;
|
|
15056
|
+
if (settings?.fileIncludeGlobs?.length) {
|
|
15057
|
+
const matched = [];
|
|
15058
|
+
try {
|
|
15059
|
+
for (const entry of (0, import_node_fs40.readdirSync)(repoPath, { withFileTypes: true })) {
|
|
15060
|
+
if (!entry.isFile()) continue;
|
|
15061
|
+
for (const glob of settings.fileIncludeGlobs) {
|
|
15062
|
+
if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob((0, import_node_path40.basename)(glob), entry.name)) {
|
|
15063
|
+
matched.push(entry.name);
|
|
15064
|
+
break;
|
|
15065
|
+
}
|
|
15066
|
+
}
|
|
15067
|
+
}
|
|
15068
|
+
} catch {
|
|
15069
|
+
}
|
|
15070
|
+
if (matched.length) return [...new Set(matched)];
|
|
15210
15071
|
}
|
|
15211
|
-
|
|
15212
|
-
|
|
15213
|
-
|
|
15214
|
-
|
|
15215
|
-
|
|
15216
|
-
|
|
15072
|
+
const defaults = [];
|
|
15073
|
+
try {
|
|
15074
|
+
for (const entry of (0, import_node_fs40.readdirSync)(repoPath, { withFileTypes: true })) {
|
|
15075
|
+
if (entry.isFile() && entry.name.startsWith(".env")) {
|
|
15076
|
+
defaults.push(entry.name);
|
|
15077
|
+
}
|
|
15078
|
+
}
|
|
15079
|
+
} catch {
|
|
15080
|
+
}
|
|
15081
|
+
if (defaults.length) return defaults;
|
|
15082
|
+
return [".env.local", ".env"];
|
|
15217
15083
|
}
|
|
15218
|
-
function
|
|
15219
|
-
|
|
15084
|
+
function copyConfiguredFiles(repoPath, worktreePath) {
|
|
15085
|
+
const patterns = resolveFilesToCopy(repoPath);
|
|
15086
|
+
const copied = [];
|
|
15087
|
+
for (const rel of patterns) {
|
|
15088
|
+
const src = (0, import_node_path40.join)(repoPath, rel);
|
|
15089
|
+
if (!(0, import_node_fs40.existsSync)(src)) continue;
|
|
15090
|
+
const dest = (0, import_node_path40.join)(worktreePath, rel);
|
|
15091
|
+
(0, import_node_fs40.mkdirSync)((0, import_node_path40.dirname)(dest), { recursive: true });
|
|
15092
|
+
(0, import_node_fs40.copyFileSync)(src, dest);
|
|
15093
|
+
copied.push(rel);
|
|
15094
|
+
}
|
|
15095
|
+
return copied;
|
|
15220
15096
|
}
|
|
15221
|
-
function
|
|
15222
|
-
|
|
15223
|
-
|
|
15097
|
+
async function captureLoginEnv() {
|
|
15098
|
+
if (cachedLoginEnv) return { ...cachedLoginEnv };
|
|
15099
|
+
const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
|
|
15100
|
+
try {
|
|
15101
|
+
const result = await (0, import_execa4.execa)(shell, ["-l", "-c", "env -0"], {
|
|
15102
|
+
reject: false,
|
|
15103
|
+
timeout: 5e3
|
|
15104
|
+
});
|
|
15105
|
+
if (result.exitCode === 0 && result.stdout) {
|
|
15106
|
+
const env = { ...process.env };
|
|
15107
|
+
for (const entry of result.stdout.split("\0")) {
|
|
15108
|
+
if (!entry) continue;
|
|
15109
|
+
const eq = entry.indexOf("=");
|
|
15110
|
+
if (eq <= 0) continue;
|
|
15111
|
+
const key = entry.slice(0, eq);
|
|
15112
|
+
const value = entry.slice(eq + 1);
|
|
15113
|
+
if (key) env[key] = value;
|
|
15114
|
+
}
|
|
15115
|
+
cachedLoginEnv = env;
|
|
15116
|
+
return { ...env };
|
|
15117
|
+
}
|
|
15118
|
+
} catch {
|
|
15119
|
+
}
|
|
15120
|
+
cachedLoginEnv = { ...process.env };
|
|
15121
|
+
return { ...cachedLoginEnv };
|
|
15224
15122
|
}
|
|
15225
|
-
function
|
|
15226
|
-
|
|
15123
|
+
function buildWorkspaceScriptEnv(opts, baseEnv) {
|
|
15124
|
+
const env = stripNestedElectronEnv({
|
|
15125
|
+
...baseEnv ?? process.env
|
|
15126
|
+
});
|
|
15127
|
+
const name = opts.workspaceName ?? (0, import_node_path40.basename)(opts.worktreePath);
|
|
15128
|
+
const ports = opts.ports ?? [];
|
|
15129
|
+
const primary = ports[0];
|
|
15130
|
+
env.SIDEBOARD_WORKSPACE_NAME = name;
|
|
15131
|
+
env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
|
|
15132
|
+
env.SIDEBOARD_ROOT_PATH = opts.repoPath;
|
|
15133
|
+
env.SIDEBOARD_IS_LOCAL = "1";
|
|
15134
|
+
if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
|
|
15135
|
+
env.CONDUCTOR_WORKSPACE_NAME = name;
|
|
15136
|
+
env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
|
|
15137
|
+
env.CONDUCTOR_ROOT_PATH = opts.repoPath;
|
|
15138
|
+
env.CONDUCTOR_IS_LOCAL = "1";
|
|
15139
|
+
if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
|
|
15140
|
+
if (primary != null) {
|
|
15141
|
+
env.SIDEBOARD_PORT = String(primary);
|
|
15142
|
+
env.CONDUCTOR_PORT = String(primary);
|
|
15143
|
+
env.PORT = String(primary);
|
|
15144
|
+
for (let i = 1; i < ports.length; i++) {
|
|
15145
|
+
const p = ports[i];
|
|
15146
|
+
env[`SIDEBOARD_PORT_${i}`] = String(p);
|
|
15147
|
+
env[`CONDUCTOR_PORT_${i}`] = String(p);
|
|
15148
|
+
}
|
|
15149
|
+
}
|
|
15150
|
+
applyWorktreePkgCacheEnv(env, opts.worktreePath);
|
|
15151
|
+
return env;
|
|
15227
15152
|
}
|
|
15228
|
-
function
|
|
15229
|
-
|
|
15230
|
-
|
|
15231
|
-
|
|
15232
|
-
|
|
15153
|
+
function pipeLines(stream, onLine) {
|
|
15154
|
+
if (!stream || !onLine) return;
|
|
15155
|
+
const rl = (0, import_node_readline3.createInterface)({ input: stream });
|
|
15156
|
+
rl.on("line", onLine);
|
|
15157
|
+
}
|
|
15158
|
+
function killableListenerPids(raw, selfPid = process.pid) {
|
|
15159
|
+
const pids = [];
|
|
15160
|
+
for (const token of raw.trim().split(/\s+/)) {
|
|
15161
|
+
const pid = Number(token);
|
|
15162
|
+
if (!Number.isFinite(pid) || pid <= 0 || pid === selfPid) continue;
|
|
15163
|
+
pids.push(pid);
|
|
15233
15164
|
}
|
|
15234
|
-
|
|
15235
|
-
|
|
15165
|
+
return pids;
|
|
15166
|
+
}
|
|
15167
|
+
function listUnixListenerPids(port, selfPid) {
|
|
15236
15168
|
try {
|
|
15237
|
-
(0,
|
|
15238
|
-
|
|
15239
|
-
|
|
15240
|
-
|
|
15241
|
-
|
|
15242
|
-
);
|
|
15169
|
+
const raw = (0, import_node_child_process4.execFileSync)("lsof", [`-tiTCP:${port}`, "-sTCP:LISTEN"], {
|
|
15170
|
+
encoding: "utf8",
|
|
15171
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
15172
|
+
timeout: 2e3
|
|
15173
|
+
});
|
|
15174
|
+
return killableListenerPids(raw, selfPid);
|
|
15243
15175
|
} catch {
|
|
15176
|
+
return [];
|
|
15244
15177
|
}
|
|
15245
15178
|
}
|
|
15246
|
-
function
|
|
15247
|
-
if (
|
|
15248
|
-
persistTimers.set(
|
|
15249
|
-
threadId,
|
|
15250
|
-
setTimeout(() => {
|
|
15251
|
-
persistTimers.delete(threadId);
|
|
15252
|
-
persistNow(threadId);
|
|
15253
|
-
}, PERSIST_DEBOUNCE_MS)
|
|
15254
|
-
);
|
|
15255
|
-
}
|
|
15256
|
-
function loadFromDisk(threadId) {
|
|
15257
|
-
const path = setupLogPath(threadId);
|
|
15258
|
-
if (!(0, import_node_fs40.existsSync)(path)) return null;
|
|
15179
|
+
function listenerCwd(pid) {
|
|
15180
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
15259
15181
|
try {
|
|
15260
|
-
const raw =
|
|
15261
|
-
|
|
15262
|
-
|
|
15263
|
-
|
|
15264
|
-
|
|
15265
|
-
|
|
15266
|
-
|
|
15182
|
+
const raw = (0, import_node_child_process4.execFileSync)("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
|
|
15183
|
+
encoding: "utf8",
|
|
15184
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
15185
|
+
timeout: 2e3
|
|
15186
|
+
});
|
|
15187
|
+
for (const line of raw.split("\n")) {
|
|
15188
|
+
if (line.startsWith("n") && line.length > 1) return line.slice(1);
|
|
15189
|
+
}
|
|
15267
15190
|
} catch {
|
|
15268
|
-
return null;
|
|
15269
15191
|
}
|
|
15192
|
+
return null;
|
|
15270
15193
|
}
|
|
15271
|
-
function
|
|
15272
|
-
const cached = memory.get(threadId);
|
|
15273
|
-
if (cached) return cached;
|
|
15274
|
-
const mem = fromSnapshot(loadFromDisk(threadId) ?? emptySetupLog());
|
|
15275
|
-
memory.set(threadId, mem);
|
|
15276
|
-
return mem;
|
|
15277
|
-
}
|
|
15278
|
-
function readSetupLog(threadId) {
|
|
15279
|
-
const cached = memory.get(threadId);
|
|
15280
|
-
if (cached) return materialize(cached);
|
|
15281
|
-
const disk = loadFromDisk(threadId);
|
|
15282
|
-
if (disk) {
|
|
15283
|
-
memory.set(threadId, fromSnapshot(disk));
|
|
15284
|
-
return disk;
|
|
15285
|
-
}
|
|
15286
|
-
return emptySetupLog();
|
|
15287
|
-
}
|
|
15288
|
-
function beginSetupLog(threadId) {
|
|
15289
|
-
const mem = {
|
|
15290
|
-
chunks: [],
|
|
15291
|
-
length: 0,
|
|
15292
|
-
running: true,
|
|
15293
|
-
exitCode: null,
|
|
15294
|
-
source: null
|
|
15295
|
-
};
|
|
15296
|
-
memory.set(threadId, mem);
|
|
15297
|
-
persistNow(threadId);
|
|
15298
|
-
return materialize(mem);
|
|
15299
|
-
}
|
|
15300
|
-
function appendSetupLog(threadId, line) {
|
|
15301
|
-
const mem = memFor(threadId);
|
|
15302
|
-
mem.chunks.push(line);
|
|
15303
|
-
mem.length += line.length + 1;
|
|
15304
|
-
mem.running = true;
|
|
15305
|
-
if (mem.length > MAX_SETUP_LOG_CHARS * 2) materialize(mem);
|
|
15306
|
-
schedulePersist(threadId);
|
|
15307
|
-
}
|
|
15308
|
-
function finishSetupLog(threadId, exitCode, source) {
|
|
15309
|
-
const mem = memFor(threadId);
|
|
15310
|
-
mem.running = false;
|
|
15311
|
-
mem.exitCode = exitCode;
|
|
15312
|
-
mem.source = source ?? mem.source;
|
|
15313
|
-
persistNow(threadId);
|
|
15314
|
-
return materialize(mem);
|
|
15315
|
-
}
|
|
15316
|
-
var import_node_crypto8, import_node_fs40, import_node_path40, EMPTY, MAX_SETUP_LOG_CHARS, memory, persistTimers, PERSIST_DEBOUNCE_MS;
|
|
15317
|
-
var init_setup_log = __esm({
|
|
15318
|
-
"src/store/setup-log.ts"() {
|
|
15319
|
-
"use strict";
|
|
15320
|
-
import_node_crypto8 = require("crypto");
|
|
15321
|
-
import_node_fs40 = require("fs");
|
|
15322
|
-
import_node_path40 = require("path");
|
|
15323
|
-
init_worktree_labels();
|
|
15324
|
-
init_paths();
|
|
15325
|
-
EMPTY = {
|
|
15326
|
-
output: "",
|
|
15327
|
-
running: false,
|
|
15328
|
-
exitCode: null,
|
|
15329
|
-
source: null
|
|
15330
|
-
};
|
|
15331
|
-
MAX_SETUP_LOG_CHARS = 256e3;
|
|
15332
|
-
memory = /* @__PURE__ */ new Map();
|
|
15333
|
-
persistTimers = /* @__PURE__ */ new Map();
|
|
15334
|
-
PERSIST_DEBOUNCE_MS = 500;
|
|
15335
|
-
}
|
|
15336
|
-
});
|
|
15337
|
-
|
|
15338
|
-
// src/store/run-log.ts
|
|
15339
|
-
function fromSnapshot2(snap) {
|
|
15340
|
-
return {
|
|
15341
|
-
chunks: snap.output ? [snap.output] : [],
|
|
15342
|
-
length: snap.output.length,
|
|
15343
|
-
running: snap.running,
|
|
15344
|
-
exitCode: snap.exitCode,
|
|
15345
|
-
scriptName: snap.scriptName
|
|
15346
|
-
};
|
|
15347
|
-
}
|
|
15348
|
-
function materialize2(mem) {
|
|
15349
|
-
let output = mem.chunks.join("\n");
|
|
15350
|
-
if (output.length > MAX_RUN_LOG_CHARS) {
|
|
15351
|
-
output = appendSetupOutput("", output, MAX_RUN_LOG_CHARS);
|
|
15352
|
-
mem.chunks = [output];
|
|
15353
|
-
mem.length = output.length;
|
|
15354
|
-
}
|
|
15355
|
-
return {
|
|
15356
|
-
output,
|
|
15357
|
-
running: mem.running,
|
|
15358
|
-
exitCode: mem.exitCode,
|
|
15359
|
-
scriptName: mem.scriptName
|
|
15360
|
-
};
|
|
15361
|
-
}
|
|
15362
|
-
function safeScriptToken(scriptName) {
|
|
15363
|
-
const token = scriptName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
|
|
15364
|
-
return token || "dev";
|
|
15365
|
-
}
|
|
15366
|
-
function runLogPath(key) {
|
|
15367
|
-
return (0, import_node_path41.join)(threadsDir(), `${key}.run.log.json`);
|
|
15368
|
-
}
|
|
15369
|
-
function runLogKeyForWorktree(worktreePath, scriptName) {
|
|
15370
|
-
const norm = normalizeWorktreePath(worktreePath);
|
|
15371
|
-
const hash = (0, import_node_crypto9.createHash)("sha1").update(norm).digest("hex").slice(0, 16);
|
|
15372
|
-
return `wt-${hash}-run-${safeScriptToken(scriptName)}`;
|
|
15373
|
-
}
|
|
15374
|
-
function emptyRunLog() {
|
|
15375
|
-
return { ...EMPTY2 };
|
|
15376
|
-
}
|
|
15377
|
-
function sliceRunLogOutput(output, tailChars = DEFAULT_RUN_LOG_TAIL_CHARS) {
|
|
15378
|
-
const max = Math.max(1, tailChars);
|
|
15379
|
-
const outputChars = output.length;
|
|
15380
|
-
if (outputChars <= max) return { output, truncated: false, outputChars };
|
|
15381
|
-
const cut = output.slice(outputChars - max);
|
|
15382
|
-
const nl = cut.indexOf("\n");
|
|
15383
|
-
const text7 = nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
|
|
15384
|
-
return { output: text7, truncated: true, outputChars };
|
|
15385
|
-
}
|
|
15386
|
-
function persistNow2(key) {
|
|
15387
|
-
const timer = persistTimers2.get(key);
|
|
15388
|
-
if (timer) {
|
|
15389
|
-
clearTimeout(timer);
|
|
15390
|
-
persistTimers2.delete(key);
|
|
15391
|
-
}
|
|
15392
|
-
const mem = memory2.get(key);
|
|
15393
|
-
if (!mem) return;
|
|
15194
|
+
function realpathOrResolve(path) {
|
|
15394
15195
|
try {
|
|
15395
|
-
(0,
|
|
15396
|
-
`, "utf8");
|
|
15196
|
+
return (0, import_node_fs40.realpathSync)(path);
|
|
15397
15197
|
} catch {
|
|
15198
|
+
return (0, import_node_path40.resolve)(path);
|
|
15398
15199
|
}
|
|
15399
15200
|
}
|
|
15400
|
-
function
|
|
15401
|
-
|
|
15402
|
-
|
|
15403
|
-
|
|
15404
|
-
|
|
15405
|
-
|
|
15406
|
-
|
|
15407
|
-
|
|
15408
|
-
);
|
|
15201
|
+
function isOtherWorktreeListenerCwd(cwd, worktreePath) {
|
|
15202
|
+
const wt = realpathOrResolve(worktreePath);
|
|
15203
|
+
const c = realpathOrResolve(cwd);
|
|
15204
|
+
if (c === wt || c.startsWith(wt + import_node_path40.sep)) return false;
|
|
15205
|
+
const parent = (0, import_node_path40.dirname)(wt);
|
|
15206
|
+
const thisName = (0, import_node_path40.basename)(wt);
|
|
15207
|
+
if (!thisName || c === parent) return false;
|
|
15208
|
+
if (!c.startsWith(parent + import_node_path40.sep)) return false;
|
|
15209
|
+
const first = c.slice(parent.length + 1).split(import_node_path40.sep)[0];
|
|
15210
|
+
return Boolean(first) && first !== thisName;
|
|
15409
15211
|
}
|
|
15410
|
-
function
|
|
15411
|
-
const path = runLogPath(key);
|
|
15412
|
-
if (!(0, import_node_fs41.existsSync)(path)) return null;
|
|
15212
|
+
function signalPid(pid, signal) {
|
|
15413
15213
|
try {
|
|
15414
|
-
|
|
15415
|
-
return {
|
|
15416
|
-
output: typeof raw.output === "string" ? raw.output : "",
|
|
15417
|
-
running: Boolean(raw.running),
|
|
15418
|
-
exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
|
|
15419
|
-
scriptName: typeof raw.scriptName === "string" ? raw.scriptName : null
|
|
15420
|
-
};
|
|
15214
|
+
process.kill(pid, signal);
|
|
15421
15215
|
} catch {
|
|
15422
|
-
return null;
|
|
15423
15216
|
}
|
|
15424
15217
|
}
|
|
15425
|
-
function
|
|
15426
|
-
const
|
|
15427
|
-
|
|
15428
|
-
|
|
15429
|
-
memory2.set(key, mem);
|
|
15430
|
-
return mem;
|
|
15218
|
+
function isThisWorktreeListenerCwd(cwd, worktreePath) {
|
|
15219
|
+
const wt = realpathOrResolve(worktreePath);
|
|
15220
|
+
const c = realpathOrResolve(cwd);
|
|
15221
|
+
return c === wt || c.startsWith(wt + import_node_path40.sep);
|
|
15431
15222
|
}
|
|
15432
|
-
function
|
|
15433
|
-
|
|
15434
|
-
|
|
15435
|
-
|
|
15436
|
-
|
|
15437
|
-
|
|
15438
|
-
return
|
|
15223
|
+
function killDetachedRunTree(pid, worktreePath) {
|
|
15224
|
+
if (!Number.isFinite(pid) || pid <= 1 || pid === process.pid) return;
|
|
15225
|
+
const cwd = listenerCwd(pid);
|
|
15226
|
+
if (cwd) {
|
|
15227
|
+
if (!isThisWorktreeListenerCwd(cwd, worktreePath)) return;
|
|
15228
|
+
} else if (!processGroupAlive(pid)) {
|
|
15229
|
+
return;
|
|
15439
15230
|
}
|
|
15440
|
-
|
|
15441
|
-
|
|
15442
|
-
|
|
15443
|
-
|
|
15444
|
-
chunks: [],
|
|
15445
|
-
length: 0,
|
|
15446
|
-
running: true,
|
|
15447
|
-
exitCode: null,
|
|
15448
|
-
scriptName
|
|
15449
|
-
};
|
|
15450
|
-
memory2.set(key, mem);
|
|
15451
|
-
persistNow2(key);
|
|
15452
|
-
return materialize2(mem);
|
|
15453
|
-
}
|
|
15454
|
-
function appendRunLog(key, line) {
|
|
15455
|
-
const mem = memFor2(key);
|
|
15456
|
-
mem.chunks.push(line);
|
|
15457
|
-
mem.length += line.length + 1;
|
|
15458
|
-
mem.running = true;
|
|
15459
|
-
if (mem.length > MAX_RUN_LOG_CHARS * 2) materialize2(mem);
|
|
15460
|
-
schedulePersist2(key);
|
|
15461
|
-
}
|
|
15462
|
-
function finishRunLog(key, exitCode) {
|
|
15463
|
-
const mem = memFor2(key);
|
|
15464
|
-
mem.running = false;
|
|
15465
|
-
mem.exitCode = exitCode;
|
|
15466
|
-
persistNow2(key);
|
|
15467
|
-
return materialize2(mem);
|
|
15468
|
-
}
|
|
15469
|
-
function flushRunLog(key) {
|
|
15470
|
-
persistNow2(key);
|
|
15471
|
-
}
|
|
15472
|
-
var import_node_crypto9, import_node_fs41, import_node_path41, EMPTY2, MAX_RUN_LOG_CHARS, DEFAULT_RUN_LOG_TAIL_CHARS, memory2, persistTimers2, PERSIST_DEBOUNCE_MS2;
|
|
15473
|
-
var init_run_log = __esm({
|
|
15474
|
-
"src/store/run-log.ts"() {
|
|
15475
|
-
"use strict";
|
|
15476
|
-
import_node_crypto9 = require("crypto");
|
|
15477
|
-
import_node_fs41 = require("fs");
|
|
15478
|
-
import_node_path41 = require("path");
|
|
15479
|
-
init_worktree_labels();
|
|
15480
|
-
init_setup_log();
|
|
15481
|
-
init_paths();
|
|
15482
|
-
EMPTY2 = {
|
|
15483
|
-
output: "",
|
|
15484
|
-
running: false,
|
|
15485
|
-
exitCode: null,
|
|
15486
|
-
scriptName: null
|
|
15487
|
-
};
|
|
15488
|
-
MAX_RUN_LOG_CHARS = 256e3;
|
|
15489
|
-
DEFAULT_RUN_LOG_TAIL_CHARS = 16e3;
|
|
15490
|
-
memory2 = /* @__PURE__ */ new Map();
|
|
15491
|
-
persistTimers2 = /* @__PURE__ */ new Map();
|
|
15492
|
-
PERSIST_DEBOUNCE_MS2 = 500;
|
|
15231
|
+
try {
|
|
15232
|
+
process.kill(-pid, "SIGTERM");
|
|
15233
|
+
} catch {
|
|
15234
|
+
signalPid(pid, "SIGTERM");
|
|
15493
15235
|
}
|
|
15494
|
-
});
|
|
15495
|
-
|
|
15496
|
-
// src/git/orphan-cleanup.ts
|
|
15497
|
-
function isSideboardWorktreePath(path) {
|
|
15498
|
-
return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
|
|
15499
15236
|
}
|
|
15500
|
-
|
|
15501
|
-
const
|
|
15502
|
-
const
|
|
15503
|
-
|
|
15504
|
-
repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
|
|
15505
|
-
);
|
|
15506
|
-
const homeRoot = sideboardWorkspacesDir();
|
|
15507
|
-
if ((0, import_node_fs42.existsSync)(homeRoot)) {
|
|
15237
|
+
function killListenersOnPorts(ports) {
|
|
15238
|
+
const selfPid = process.pid;
|
|
15239
|
+
for (const port of ports) {
|
|
15240
|
+
if (!Number.isFinite(port) || port <= 0) continue;
|
|
15508
15241
|
try {
|
|
15509
|
-
|
|
15510
|
-
|
|
15511
|
-
|
|
15242
|
+
if (process.platform === "win32") {
|
|
15243
|
+
(0, import_node_child_process4.execFileSync)(
|
|
15244
|
+
"powershell",
|
|
15245
|
+
[
|
|
15246
|
+
"-NoProfile",
|
|
15247
|
+
"-Command",
|
|
15248
|
+
`Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -ne ${selfPid} } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
|
|
15249
|
+
],
|
|
15250
|
+
{ stdio: "ignore", timeout: 3e3 }
|
|
15251
|
+
);
|
|
15252
|
+
continue;
|
|
15253
|
+
}
|
|
15254
|
+
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
15255
|
+
signalPid(pid, "SIGTERM");
|
|
15512
15256
|
}
|
|
15513
15257
|
} catch {
|
|
15514
15258
|
}
|
|
15515
15259
|
}
|
|
15516
|
-
|
|
15517
|
-
|
|
15518
|
-
|
|
15519
|
-
|
|
15520
|
-
|
|
15521
|
-
|
|
15522
|
-
|
|
15523
|
-
|
|
15524
|
-
|
|
15525
|
-
|
|
15526
|
-
|
|
15527
|
-
|
|
15528
|
-
|
|
15529
|
-
|
|
15530
|
-
|
|
15531
|
-
|
|
15532
|
-
|
|
15533
|
-
}
|
|
15534
|
-
orphans.push({ path, repoPath, mtimeMs });
|
|
15535
|
-
}
|
|
15536
|
-
} catch {
|
|
15260
|
+
}
|
|
15261
|
+
function killStaleWorktreeListeners(ports, worktreePath, signal = "SIGTERM") {
|
|
15262
|
+
if (!worktreePath.trim()) {
|
|
15263
|
+
killListenersOnPorts(ports);
|
|
15264
|
+
return;
|
|
15265
|
+
}
|
|
15266
|
+
if (process.platform === "win32") {
|
|
15267
|
+
killListenersOnPorts(ports);
|
|
15268
|
+
return;
|
|
15269
|
+
}
|
|
15270
|
+
const selfPid = process.pid;
|
|
15271
|
+
for (const port of ports) {
|
|
15272
|
+
if (!Number.isFinite(port) || port <= 0) continue;
|
|
15273
|
+
for (const pid of listUnixListenerPids(port, selfPid)) {
|
|
15274
|
+
const cwd = listenerCwd(pid);
|
|
15275
|
+
if (cwd && isOtherWorktreeListenerCwd(cwd, worktreePath)) continue;
|
|
15276
|
+
signalPid(pid, signal);
|
|
15537
15277
|
}
|
|
15278
|
+
}
|
|
15279
|
+
}
|
|
15280
|
+
function killScriptTree(child, ports = []) {
|
|
15281
|
+
const pid = child.pid;
|
|
15282
|
+
if (pid) {
|
|
15538
15283
|
try {
|
|
15539
|
-
|
|
15540
|
-
|
|
15541
|
-
|
|
15542
|
-
|
|
15543
|
-
|
|
15544
|
-
if (known.has(path) || seen.has(path)) continue;
|
|
15545
|
-
if (!(0, import_node_fs42.existsSync)((0, import_node_path42.join)(path, ".git"))) continue;
|
|
15546
|
-
seen.add(path);
|
|
15547
|
-
let mtimeMs = 0;
|
|
15284
|
+
if (process.platform === "win32") {
|
|
15285
|
+
void (0, import_execa4.execa)("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
|
|
15286
|
+
} else {
|
|
15287
|
+
process.kill(-pid, "SIGTERM");
|
|
15288
|
+
setTimeout(() => {
|
|
15548
15289
|
try {
|
|
15549
|
-
|
|
15290
|
+
process.kill(-pid, "SIGKILL");
|
|
15550
15291
|
} catch {
|
|
15551
|
-
mtimeMs = Date.now();
|
|
15552
15292
|
}
|
|
15553
|
-
|
|
15554
|
-
}
|
|
15293
|
+
}, 2500).unref?.();
|
|
15555
15294
|
}
|
|
15556
15295
|
} catch {
|
|
15296
|
+
try {
|
|
15297
|
+
child.kill("SIGTERM");
|
|
15298
|
+
} catch {
|
|
15299
|
+
}
|
|
15557
15300
|
}
|
|
15558
15301
|
}
|
|
15559
|
-
|
|
15560
|
-
}
|
|
15561
|
-
async function cleanupOrphanWorktrees(opts) {
|
|
15562
|
-
const settings = loadAppSettings();
|
|
15563
|
-
const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
|
|
15564
|
-
const orphans = await findOrphanWorktrees(opts?.repoPaths);
|
|
15565
|
-
const activeCount = listThreads().filter((t) => t.status !== "archived").length;
|
|
15566
|
-
const budget = Math.max(0, maxCount - activeCount);
|
|
15567
|
-
const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
15568
|
-
const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
|
|
15569
|
-
const removed = [];
|
|
15570
|
-
const kept = [];
|
|
15571
|
-
for (const orphan of orphans) {
|
|
15572
|
-
if (keep.has(orphan.path)) {
|
|
15573
|
-
kept.push(orphan.path);
|
|
15574
|
-
continue;
|
|
15575
|
-
}
|
|
15576
|
-
if (opts?.dryRun) {
|
|
15577
|
-
removed.push(orphan.path);
|
|
15578
|
-
continue;
|
|
15579
|
-
}
|
|
15580
|
-
try {
|
|
15581
|
-
await removeWorktree(orphan.repoPath, orphan.path);
|
|
15582
|
-
removed.push(orphan.path);
|
|
15583
|
-
} catch {
|
|
15584
|
-
kept.push(orphan.path);
|
|
15585
|
-
}
|
|
15586
|
-
}
|
|
15587
|
-
if (!opts?.dryRun) {
|
|
15588
|
-
updateAdvancedSettings({
|
|
15589
|
-
worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15590
|
-
});
|
|
15591
|
-
}
|
|
15592
|
-
return { removed, kept, orphans };
|
|
15593
|
-
}
|
|
15594
|
-
function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
|
|
15595
|
-
const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
|
|
15596
|
-
const last = settings.advanced.worktreeLastCleanupAt;
|
|
15597
|
-
if (!last) return true;
|
|
15598
|
-
const elapsed = Date.now() - Date.parse(last);
|
|
15599
|
-
return elapsed >= intervalHours * 36e5;
|
|
15302
|
+
killListenersOnPorts(ports);
|
|
15600
15303
|
}
|
|
15601
|
-
|
|
15602
|
-
|
|
15603
|
-
|
|
15604
|
-
"use strict";
|
|
15605
|
-
import_node_fs42 = require("fs");
|
|
15606
|
-
import_node_path42 = require("path");
|
|
15607
|
-
init_worktree();
|
|
15608
|
-
init_thread_store();
|
|
15609
|
-
init_paths();
|
|
15610
|
-
init_app_settings();
|
|
15611
|
-
}
|
|
15612
|
-
});
|
|
15613
|
-
|
|
15614
|
-
// src/store/history-cleanup.ts
|
|
15615
|
-
function stripArchivedTranscript(thread) {
|
|
15616
|
-
writeThread(
|
|
15304
|
+
async function spawnWorkspaceScript(command, opts) {
|
|
15305
|
+
const loginEnv = await captureLoginEnv();
|
|
15306
|
+
const env = buildWorkspaceScriptEnv(
|
|
15617
15307
|
{
|
|
15618
|
-
|
|
15619
|
-
|
|
15620
|
-
|
|
15621
|
-
|
|
15622
|
-
|
|
15308
|
+
worktreePath: opts.worktreePath,
|
|
15309
|
+
repoPath: opts.repoPath,
|
|
15310
|
+
workspaceName: opts.workspaceName,
|
|
15311
|
+
defaultBranch: opts.defaultBranch,
|
|
15312
|
+
ports: opts.ports
|
|
15623
15313
|
},
|
|
15624
|
-
|
|
15314
|
+
loginEnv
|
|
15625
15315
|
);
|
|
15626
|
-
|
|
15627
|
-
|
|
15628
|
-
|
|
15629
|
-
|
|
15630
|
-
|
|
15316
|
+
try {
|
|
15317
|
+
mergeAgentGitAuthEnv(
|
|
15318
|
+
env,
|
|
15319
|
+
await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
|
|
15320
|
+
);
|
|
15321
|
+
} catch {
|
|
15631
15322
|
}
|
|
15632
|
-
|
|
15633
|
-
|
|
15634
|
-
|
|
15635
|
-
|
|
15636
|
-
|
|
15637
|
-
|
|
15323
|
+
if (opts.heldPorts?.length) {
|
|
15324
|
+
await Promise.all(opts.heldPorts.map((h) => h.release()));
|
|
15325
|
+
}
|
|
15326
|
+
const shell = process.platform === "darwin" ? "zsh" : "bash";
|
|
15327
|
+
const child = (0, import_execa4.execa)(shell, ["-lc", command], {
|
|
15328
|
+
cwd: opts.worktreePath,
|
|
15329
|
+
reject: false,
|
|
15330
|
+
env,
|
|
15331
|
+
// Own process group so Stop can tear down the whole tree (not just the shell).
|
|
15332
|
+
// There is no settings.toml `stop=` / teardown hook for run scripts.
|
|
15333
|
+
...process.platform === "win32" ? {} : { detached: true }
|
|
15638
15334
|
});
|
|
15639
|
-
|
|
15640
|
-
|
|
15641
|
-
|
|
15642
|
-
|
|
15335
|
+
pipeLines(child.stdout, opts.onLine);
|
|
15336
|
+
pipeLines(child.stderr, opts.onLine);
|
|
15337
|
+
const ports = opts.ports ?? [];
|
|
15338
|
+
return {
|
|
15339
|
+
pid: child.pid,
|
|
15340
|
+
kill: () => killScriptTree(child, ports),
|
|
15341
|
+
done: child.then((r) => r.exitCode ?? null),
|
|
15342
|
+
child
|
|
15343
|
+
};
|
|
15344
|
+
}
|
|
15345
|
+
async function attachAbort(handle, signal) {
|
|
15346
|
+
if (signal) {
|
|
15347
|
+
const onAbort = () => handle.kill();
|
|
15348
|
+
if (signal.aborted) onAbort();
|
|
15349
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
15643
15350
|
}
|
|
15644
|
-
|
|
15645
|
-
|
|
15646
|
-
|
|
15647
|
-
|
|
15648
|
-
|
|
15649
|
-
|
|
15650
|
-
|
|
15651
|
-
|
|
15351
|
+
return handle.done;
|
|
15352
|
+
}
|
|
15353
|
+
async function runSetupScript(repoPath, worktreePath, onLine, opts) {
|
|
15354
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
15355
|
+
if (!settings?.setup) return { ran: false, exitCode: null, source: null };
|
|
15356
|
+
const source = workspaceSettingsSourceLabel(worktreePath, repoPath);
|
|
15357
|
+
onLine?.(`[setup] ${source ?? "settings.toml"}`);
|
|
15358
|
+
const handle = await spawnWorkspaceScript(settings.setup, {
|
|
15359
|
+
worktreePath,
|
|
15360
|
+
repoPath,
|
|
15361
|
+
defaultBranch: opts?.defaultBranch,
|
|
15362
|
+
onLine
|
|
15363
|
+
});
|
|
15364
|
+
const exitCode = await attachAbort(handle, opts?.signal);
|
|
15365
|
+
return {
|
|
15366
|
+
ran: true,
|
|
15367
|
+
exitCode,
|
|
15368
|
+
source,
|
|
15369
|
+
kill: handle.kill
|
|
15370
|
+
};
|
|
15371
|
+
}
|
|
15372
|
+
async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
|
|
15373
|
+
const found = findConventionSetup(worktreePath, repoPath);
|
|
15374
|
+
if (!found) return { ran: false, exitCode: null, source: null };
|
|
15375
|
+
onLine?.(`[setup] ${found.source}`);
|
|
15376
|
+
const handle = await spawnWorkspaceScript(found.command, {
|
|
15377
|
+
worktreePath,
|
|
15378
|
+
repoPath,
|
|
15379
|
+
defaultBranch: opts?.defaultBranch,
|
|
15380
|
+
onLine
|
|
15381
|
+
});
|
|
15382
|
+
const exitCode = await attachAbort(handle, opts?.signal);
|
|
15383
|
+
return {
|
|
15384
|
+
ran: true,
|
|
15385
|
+
exitCode,
|
|
15386
|
+
source: found.source,
|
|
15387
|
+
kill: handle.kill
|
|
15388
|
+
};
|
|
15389
|
+
}
|
|
15390
|
+
async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
|
|
15391
|
+
await ensureWorktreeSideboardIgnored(worktreePath);
|
|
15392
|
+
const review = ensureReviewGuidelinesFile(worktreePath, repoPath);
|
|
15393
|
+
if (review.wrote) {
|
|
15394
|
+
onLine?.(`[setup] wrote ${review.path} \u2014 local Review guidelines`);
|
|
15652
15395
|
}
|
|
15653
|
-
|
|
15654
|
-
|
|
15655
|
-
|
|
15656
|
-
try {
|
|
15657
|
-
deleteThreadRecord(id);
|
|
15658
|
-
purged.push(id);
|
|
15659
|
-
} catch {
|
|
15660
|
-
}
|
|
15396
|
+
let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
|
|
15397
|
+
if (!setup.ran) {
|
|
15398
|
+
setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
|
|
15661
15399
|
}
|
|
15662
|
-
if (
|
|
15663
|
-
|
|
15664
|
-
historyLastCleanupAt: new Date(nowMs).toISOString()
|
|
15665
|
-
});
|
|
15400
|
+
if (!setup.ran) {
|
|
15401
|
+
setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
|
|
15666
15402
|
}
|
|
15667
|
-
return
|
|
15403
|
+
return setup;
|
|
15668
15404
|
}
|
|
15669
|
-
function
|
|
15670
|
-
|
|
15671
|
-
|
|
15672
|
-
const
|
|
15673
|
-
|
|
15674
|
-
|
|
15675
|
-
|
|
15676
|
-
}
|
|
15677
|
-
var init_history_cleanup = __esm({
|
|
15678
|
-
"src/store/history-cleanup.ts"() {
|
|
15679
|
-
"use strict";
|
|
15680
|
-
init_app_settings();
|
|
15681
|
-
init_history_retention();
|
|
15682
|
-
init_thread_store();
|
|
15683
|
-
}
|
|
15684
|
-
});
|
|
15685
|
-
|
|
15686
|
-
// src/git/apply-into-main.ts
|
|
15687
|
-
async function applyThreadIntoMain(thread, opts) {
|
|
15688
|
-
const method = opts?.method ?? "merge";
|
|
15689
|
-
const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
|
|
15690
|
-
const status = await (0, import_execa5.execa)("git", ["status", "--porcelain"], {
|
|
15691
|
-
cwd: thread.repoPath,
|
|
15692
|
-
reject: false
|
|
15405
|
+
async function runArchiveScript(repoPath, worktreePath, onLine) {
|
|
15406
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
15407
|
+
if (!settings?.archive) return { ran: false, exitCode: null };
|
|
15408
|
+
const handle = await spawnWorkspaceScript(settings.archive, {
|
|
15409
|
+
worktreePath,
|
|
15410
|
+
repoPath,
|
|
15411
|
+
onLine
|
|
15693
15412
|
});
|
|
15694
|
-
|
|
15695
|
-
|
|
15696
|
-
|
|
15697
|
-
|
|
15698
|
-
|
|
15699
|
-
|
|
15700
|
-
|
|
15701
|
-
|
|
15413
|
+
const exitCode = await handle.done;
|
|
15414
|
+
return { ran: true, exitCode };
|
|
15415
|
+
}
|
|
15416
|
+
function listRunScripts(worktreePath, repoPath) {
|
|
15417
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
15418
|
+
if (!settings?.runScripts.length) return [];
|
|
15419
|
+
return settings.runScripts.filter((s) => {
|
|
15420
|
+
if (!s.availableIn?.length) return true;
|
|
15421
|
+
return s.availableIn.includes("local");
|
|
15702
15422
|
});
|
|
15703
|
-
|
|
15704
|
-
|
|
15705
|
-
|
|
15706
|
-
|
|
15423
|
+
}
|
|
15424
|
+
function getDefaultRunScript(worktreePath, repoPath) {
|
|
15425
|
+
const scripts = listRunScripts(worktreePath, repoPath);
|
|
15426
|
+
if (!scripts.length) return null;
|
|
15427
|
+
return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
|
|
15428
|
+
}
|
|
15429
|
+
function getRunScript(worktreePath, repoPath, name) {
|
|
15430
|
+
const scripts = listRunScripts(worktreePath, repoPath);
|
|
15431
|
+
if (!scripts.length) return null;
|
|
15432
|
+
if (name) {
|
|
15433
|
+
return scripts.find((s) => s.name === name) ?? null;
|
|
15707
15434
|
}
|
|
15708
|
-
|
|
15709
|
-
|
|
15710
|
-
|
|
15711
|
-
|
|
15712
|
-
|
|
15713
|
-
|
|
15714
|
-
|
|
15715
|
-
|
|
15716
|
-
|
|
15717
|
-
|
|
15718
|
-
|
|
15719
|
-
|
|
15720
|
-
|
|
15721
|
-
};
|
|
15722
|
-
}
|
|
15723
|
-
for (const sha of shas) {
|
|
15724
|
-
const cp = await (0, import_execa5.execa)("git", ["cherry-pick", sha], {
|
|
15725
|
-
cwd: thread.repoPath,
|
|
15726
|
-
reject: false
|
|
15727
|
-
});
|
|
15728
|
-
if (cp.exitCode !== 0) {
|
|
15729
|
-
await (0, import_execa5.execa)("git", ["cherry-pick", "--abort"], {
|
|
15730
|
-
cwd: thread.repoPath,
|
|
15731
|
-
reject: false
|
|
15732
|
-
});
|
|
15733
|
-
throw new Error(
|
|
15734
|
-
`Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
|
|
15735
|
-
);
|
|
15435
|
+
return getDefaultRunScript(worktreePath, repoPath);
|
|
15436
|
+
}
|
|
15437
|
+
function getRunMode(worktreePath, repoPath) {
|
|
15438
|
+
return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
|
|
15439
|
+
}
|
|
15440
|
+
async function reservePort() {
|
|
15441
|
+
return new Promise((resolve2, reject) => {
|
|
15442
|
+
const server = (0, import_node_net.createServer)();
|
|
15443
|
+
let released = false;
|
|
15444
|
+
const release = () => new Promise((res) => {
|
|
15445
|
+
if (released) {
|
|
15446
|
+
res();
|
|
15447
|
+
return;
|
|
15736
15448
|
}
|
|
15737
|
-
|
|
15738
|
-
|
|
15739
|
-
applied: true,
|
|
15740
|
-
method,
|
|
15741
|
-
targetBranch: target,
|
|
15742
|
-
message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
|
|
15743
|
-
};
|
|
15744
|
-
}
|
|
15745
|
-
const merge = await (0, import_execa5.execa)(
|
|
15746
|
-
"git",
|
|
15747
|
-
["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
|
|
15748
|
-
{ cwd: thread.repoPath, reject: false }
|
|
15749
|
-
);
|
|
15750
|
-
if (merge.exitCode !== 0) {
|
|
15751
|
-
await (0, import_execa5.execa)("git", ["merge", "--abort"], {
|
|
15752
|
-
cwd: thread.repoPath,
|
|
15753
|
-
reject: false
|
|
15449
|
+
released = true;
|
|
15450
|
+
server.close(() => res());
|
|
15754
15451
|
});
|
|
15755
|
-
|
|
15756
|
-
|
|
15757
|
-
|
|
15758
|
-
|
|
15759
|
-
|
|
15760
|
-
|
|
15761
|
-
|
|
15762
|
-
|
|
15763
|
-
|
|
15764
|
-
};
|
|
15452
|
+
server.listen(0, "127.0.0.1", () => {
|
|
15453
|
+
const addr = server.address();
|
|
15454
|
+
if (!addr || typeof addr === "string") {
|
|
15455
|
+
void release().then(() => reject(new Error("Failed to allocate port")));
|
|
15456
|
+
return;
|
|
15457
|
+
}
|
|
15458
|
+
resolve2({ port: addr.port, release });
|
|
15459
|
+
});
|
|
15460
|
+
server.on("error", reject);
|
|
15461
|
+
});
|
|
15765
15462
|
}
|
|
15766
|
-
|
|
15767
|
-
|
|
15768
|
-
|
|
15769
|
-
|
|
15770
|
-
|
|
15771
|
-
|
|
15772
|
-
|
|
15773
|
-
|
|
15774
|
-
|
|
15775
|
-
|
|
15776
|
-
|
|
15777
|
-
|
|
15778
|
-
|
|
15779
|
-
|
|
15780
|
-
|
|
15781
|
-
|
|
15782
|
-
|
|
15783
|
-
|
|
15784
|
-
|
|
15785
|
-
|
|
15786
|
-
|
|
15787
|
-
|
|
15788
|
-
|
|
15789
|
-
|
|
15790
|
-
}
|
|
15791
|
-
const result = await (0, import_execa6.execa)("git", ["clone", url, dest], { reject: false });
|
|
15792
|
-
if (result.exitCode !== 0) {
|
|
15793
|
-
throw new Error(
|
|
15794
|
-
`git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
|
|
15795
|
-
);
|
|
15796
|
-
}
|
|
15797
|
-
const repoPath = await resolveRepoRoot(dest);
|
|
15798
|
-
const workspace = await ensureWorkspace(repoPath);
|
|
15799
|
-
return { repoPath, workspace };
|
|
15463
|
+
async function tryReservePort(port) {
|
|
15464
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
|
|
15465
|
+
return new Promise((resolve2) => {
|
|
15466
|
+
const server = (0, import_node_net.createServer)();
|
|
15467
|
+
let released = false;
|
|
15468
|
+
const release = () => new Promise((res) => {
|
|
15469
|
+
if (released) {
|
|
15470
|
+
res();
|
|
15471
|
+
return;
|
|
15472
|
+
}
|
|
15473
|
+
released = true;
|
|
15474
|
+
server.close(() => res());
|
|
15475
|
+
});
|
|
15476
|
+
server.once("error", () => {
|
|
15477
|
+
try {
|
|
15478
|
+
server.close();
|
|
15479
|
+
} catch {
|
|
15480
|
+
}
|
|
15481
|
+
resolve2(null);
|
|
15482
|
+
});
|
|
15483
|
+
server.listen(port, "127.0.0.1", () => {
|
|
15484
|
+
resolve2({ port, release });
|
|
15485
|
+
});
|
|
15486
|
+
});
|
|
15800
15487
|
}
|
|
15801
|
-
|
|
15802
|
-
|
|
15803
|
-
|
|
15804
|
-
|
|
15805
|
-
|
|
15806
|
-
|
|
15807
|
-
|
|
15808
|
-
|
|
15809
|
-
|
|
15810
|
-
init_worktree();
|
|
15488
|
+
function normalizePreferredPorts(ports, size) {
|
|
15489
|
+
if (!ports?.length) return [];
|
|
15490
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15491
|
+
const out = [];
|
|
15492
|
+
for (const p of ports) {
|
|
15493
|
+
if (!Number.isInteger(p) || p <= 0 || p > 65535 || seen.has(p)) continue;
|
|
15494
|
+
seen.add(p);
|
|
15495
|
+
out.push(p);
|
|
15496
|
+
if (out.length >= size) break;
|
|
15811
15497
|
}
|
|
15812
|
-
|
|
15813
|
-
|
|
15814
|
-
// src/types/thread.ts
|
|
15815
|
-
function isAdoptableRunScriptRequest(req) {
|
|
15816
|
-
return Boolean(req && !req.fulfilledAt && !req.error);
|
|
15498
|
+
return out;
|
|
15817
15499
|
}
|
|
15818
|
-
|
|
15819
|
-
|
|
15820
|
-
"use strict";
|
|
15821
|
-
}
|
|
15822
|
-
});
|
|
15823
|
-
|
|
15824
|
-
// src/store/desktop-host.ts
|
|
15825
|
-
function desktopHostPidPath() {
|
|
15826
|
-
return (0, import_node_path44.join)(appDataDir(), "desktop-host.pid");
|
|
15500
|
+
function delay(ms) {
|
|
15501
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
15827
15502
|
}
|
|
15828
|
-
function
|
|
15503
|
+
async function fillPortRange(held, size) {
|
|
15504
|
+
if (held.length >= size) return held;
|
|
15505
|
+
const base = held[0]?.port;
|
|
15506
|
+
const taken = new Set(held.map((h) => h.port));
|
|
15829
15507
|
try {
|
|
15830
|
-
|
|
15831
|
-
|
|
15832
|
-
|
|
15833
|
-
|
|
15834
|
-
|
|
15508
|
+
let offset = 0;
|
|
15509
|
+
while (held.length < size) {
|
|
15510
|
+
const candidate = base != null ? base + offset : null;
|
|
15511
|
+
offset += 1;
|
|
15512
|
+
if (candidate != null && taken.has(candidate)) continue;
|
|
15513
|
+
const next = candidate != null ? await tryReservePort(candidate) : null;
|
|
15514
|
+
if (next) {
|
|
15515
|
+
held.push(next);
|
|
15516
|
+
taken.add(next.port);
|
|
15517
|
+
} else {
|
|
15518
|
+
const ephemeral = await reservePort();
|
|
15519
|
+
held.push(ephemeral);
|
|
15520
|
+
taken.add(ephemeral.port);
|
|
15521
|
+
}
|
|
15522
|
+
}
|
|
15523
|
+
return held;
|
|
15524
|
+
} catch (err) {
|
|
15525
|
+
await Promise.all(held.map((h) => h.release()));
|
|
15526
|
+
throw err;
|
|
15835
15527
|
}
|
|
15836
15528
|
}
|
|
15837
|
-
function
|
|
15838
|
-
|
|
15839
|
-
|
|
15840
|
-
|
|
15841
|
-
|
|
15842
|
-
return false;
|
|
15529
|
+
async function tryReserveAvailablePorts(ports) {
|
|
15530
|
+
const held = [];
|
|
15531
|
+
for (const port of ports) {
|
|
15532
|
+
const next = await tryReservePort(port);
|
|
15533
|
+
if (next) held.push(next);
|
|
15843
15534
|
}
|
|
15535
|
+
return held;
|
|
15844
15536
|
}
|
|
15845
|
-
function
|
|
15846
|
-
const
|
|
15847
|
-
|
|
15848
|
-
|
|
15849
|
-
|
|
15850
|
-
|
|
15851
|
-
|
|
15852
|
-
|
|
15853
|
-
|
|
15854
|
-
|
|
15855
|
-
|
|
15856
|
-
|
|
15857
|
-
|
|
15858
|
-
|
|
15859
|
-
|
|
15860
|
-
import_node_fs44 = require("fs");
|
|
15861
|
-
import_node_path44 = require("path");
|
|
15862
|
-
init_paths();
|
|
15537
|
+
async function reclaimPreferredPorts(ports, worktreePath) {
|
|
15538
|
+
const primary = ports[0];
|
|
15539
|
+
if (primary == null) return null;
|
|
15540
|
+
const takePrimary = async () => {
|
|
15541
|
+
const held2 = await tryReserveAvailablePorts(ports);
|
|
15542
|
+
if (held2.some((h) => h.port === primary)) return held2;
|
|
15543
|
+
await Promise.all(held2.map((h) => h.release()));
|
|
15544
|
+
return null;
|
|
15545
|
+
};
|
|
15546
|
+
let held = await takePrimary();
|
|
15547
|
+
if (held) return held;
|
|
15548
|
+
if (worktreePath) {
|
|
15549
|
+
killStaleWorktreeListeners(ports, worktreePath, "SIGTERM");
|
|
15550
|
+
} else {
|
|
15551
|
+
killListenersOnPorts(ports);
|
|
15863
15552
|
}
|
|
15864
|
-
|
|
15865
|
-
|
|
15866
|
-
|
|
15867
|
-
|
|
15868
|
-
|
|
15869
|
-
|
|
15870
|
-
|
|
15871
|
-
|
|
15872
|
-
|
|
15873
|
-
const why = child.lastError?.trim();
|
|
15874
|
-
const extra = why ? ` lastError: ${why}` : "";
|
|
15875
|
-
return [
|
|
15876
|
-
`Sideboard: child worktree ${link} ${status} before finishing (status=${status}).${extra}`,
|
|
15877
|
-
"This is information \u2014 not a user command. Resume with send_to_thread or tell the user. Do not treat this as a successful turn."
|
|
15878
|
-
].join("\n");
|
|
15879
|
-
}
|
|
15880
|
-
function shouldNotifyParentAfterTurnError(opts) {
|
|
15881
|
-
return !opts.quotaFailoverHandled && !opts.crashContinued;
|
|
15882
|
-
}
|
|
15883
|
-
function shouldNotifyParentOfChildHalt(opts) {
|
|
15884
|
-
if (!isIncompleteChildStatus(opts.status)) return false;
|
|
15885
|
-
if (!opts.child.parentThreadId) return false;
|
|
15886
|
-
if (!opts.parent || opts.parent.status === "archived") return false;
|
|
15887
|
-
if (opts.parent.id === opts.child.id) return false;
|
|
15888
|
-
return isOrchestratorThread(opts.parent);
|
|
15553
|
+
for (let i = 0; i < RECLAIM_TRIES; i++) {
|
|
15554
|
+
await delay(RECLAIM_RETRY_DELAY_MS);
|
|
15555
|
+
held = await takePrimary();
|
|
15556
|
+
if (held) return held;
|
|
15557
|
+
if (i === 2 && worktreePath) {
|
|
15558
|
+
killStaleWorktreeListeners(ports, worktreePath, "SIGKILL");
|
|
15559
|
+
}
|
|
15560
|
+
}
|
|
15561
|
+
return null;
|
|
15889
15562
|
}
|
|
15890
|
-
function
|
|
15891
|
-
|
|
15563
|
+
async function reservePortRange(size = PORT_RANGE_SIZE, opts) {
|
|
15564
|
+
const preferred = normalizePreferredPorts(opts?.preferred, size);
|
|
15565
|
+
if (preferred.length) {
|
|
15566
|
+
const reused = await reclaimPreferredPorts(preferred, opts?.worktreePath);
|
|
15567
|
+
if (reused) return fillPortRange(reused, size);
|
|
15568
|
+
}
|
|
15569
|
+
const held = [await reservePort()];
|
|
15570
|
+
return fillPortRange(held, size);
|
|
15892
15571
|
}
|
|
15893
|
-
function
|
|
15894
|
-
const
|
|
15895
|
-
if (!
|
|
15896
|
-
const
|
|
15897
|
-
|
|
15898
|
-
|
|
15899
|
-
|
|
15900
|
-
void send(parentId, childHaltNotice(child, status)).catch(() => {
|
|
15901
|
-
notified.delete(key);
|
|
15572
|
+
async function startDevServer(repoPath, worktreePath, onLine, opts) {
|
|
15573
|
+
const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
|
|
15574
|
+
if (!script) return null;
|
|
15575
|
+
const preferred = loadWorktreeRunPorts(worktreePath, script.name) ?? opts?.preferredPorts;
|
|
15576
|
+
const held = await reservePortRange(PORT_RANGE_SIZE, {
|
|
15577
|
+
preferred: preferred ?? void 0,
|
|
15578
|
+
worktreePath
|
|
15902
15579
|
});
|
|
15903
|
-
|
|
15580
|
+
const ports = held.map((h) => h.port);
|
|
15581
|
+
saveWorktreeRunPorts(worktreePath, script.name, ports);
|
|
15582
|
+
try {
|
|
15583
|
+
const handle = await spawnWorkspaceScript(script.command, {
|
|
15584
|
+
worktreePath,
|
|
15585
|
+
repoPath,
|
|
15586
|
+
ports,
|
|
15587
|
+
heldPorts: held,
|
|
15588
|
+
defaultBranch: opts?.defaultBranch,
|
|
15589
|
+
onLine
|
|
15590
|
+
});
|
|
15591
|
+
return {
|
|
15592
|
+
pid: handle.pid,
|
|
15593
|
+
port: ports[0],
|
|
15594
|
+
ports,
|
|
15595
|
+
scriptName: script.name,
|
|
15596
|
+
kill: handle.kill,
|
|
15597
|
+
done: handle.done
|
|
15598
|
+
};
|
|
15599
|
+
} catch (err) {
|
|
15600
|
+
await Promise.all(held.map((h) => h.release()));
|
|
15601
|
+
throw err;
|
|
15602
|
+
}
|
|
15904
15603
|
}
|
|
15905
|
-
var
|
|
15906
|
-
var
|
|
15907
|
-
"src/
|
|
15604
|
+
var import_node_fs40, import_node_child_process4, import_node_net, import_node_path40, import_execa4, import_node_readline3, PORT_RANGE_SIZE, RECLAIM_RETRY_DELAY_MS, RECLAIM_TRIES, cachedLoginEnv;
|
|
15605
|
+
var init_conductor = __esm({
|
|
15606
|
+
"src/hook/conductor.ts"() {
|
|
15908
15607
|
"use strict";
|
|
15909
|
-
|
|
15910
|
-
|
|
15911
|
-
|
|
15912
|
-
|
|
15608
|
+
import_node_fs40 = require("fs");
|
|
15609
|
+
import_node_child_process4 = require("child_process");
|
|
15610
|
+
import_node_net = require("net");
|
|
15611
|
+
import_node_path40 = require("path");
|
|
15612
|
+
import_execa4 = require("execa");
|
|
15613
|
+
import_node_readline3 = require("readline");
|
|
15614
|
+
init_settings();
|
|
15615
|
+
init_nested_electron_env();
|
|
15616
|
+
init_worktree_pkg_cache();
|
|
15617
|
+
init_convention_setup();
|
|
15618
|
+
init_cursor_worktrees();
|
|
15619
|
+
init_git_auth_mode();
|
|
15620
|
+
init_request_review();
|
|
15621
|
+
init_worktree_exclude();
|
|
15622
|
+
init_worktree_run_ports();
|
|
15623
|
+
init_wait_for_job();
|
|
15624
|
+
init_worktree_run_ports();
|
|
15625
|
+
init_nested_electron_env();
|
|
15626
|
+
PORT_RANGE_SIZE = 10;
|
|
15627
|
+
RECLAIM_RETRY_DELAY_MS = 40;
|
|
15628
|
+
RECLAIM_TRIES = 8;
|
|
15629
|
+
cachedLoginEnv = null;
|
|
15913
15630
|
}
|
|
15914
15631
|
});
|
|
15915
15632
|
|
|
15916
|
-
// src/
|
|
15917
|
-
function
|
|
15918
|
-
const
|
|
15919
|
-
|
|
15920
|
-
|
|
15633
|
+
// src/store/setup-log.ts
|
|
15634
|
+
function appendSetupOutput(prev, line, max = MAX_SETUP_LOG_CHARS) {
|
|
15635
|
+
const next = prev ? `${prev}
|
|
15636
|
+
${line}` : line;
|
|
15637
|
+
if (next.length <= max) return next;
|
|
15638
|
+
const cut = next.slice(next.length - max);
|
|
15639
|
+
const nl = cut.indexOf("\n");
|
|
15640
|
+
return nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
|
|
15921
15641
|
}
|
|
15922
|
-
function
|
|
15923
|
-
|
|
15924
|
-
|
|
15925
|
-
|
|
15926
|
-
|
|
15927
|
-
|
|
15928
|
-
|
|
15929
|
-
|
|
15930
|
-
case "canceled":
|
|
15931
|
-
return MCP_WAIT_STOPPED_HINT;
|
|
15932
|
-
case "failed":
|
|
15933
|
-
return status === "broken" ? MCP_WAIT_BROKEN_HINT : MCP_WAIT_ERROR_HINT;
|
|
15934
|
-
default:
|
|
15935
|
-
return void 0;
|
|
15936
|
-
}
|
|
15642
|
+
function fromSnapshot(snap) {
|
|
15643
|
+
return {
|
|
15644
|
+
chunks: snap.output ? [snap.output] : [],
|
|
15645
|
+
length: snap.output.length,
|
|
15646
|
+
running: snap.running,
|
|
15647
|
+
exitCode: snap.exitCode,
|
|
15648
|
+
source: snap.source
|
|
15649
|
+
};
|
|
15937
15650
|
}
|
|
15938
|
-
|
|
15939
|
-
|
|
15940
|
-
|
|
15941
|
-
"
|
|
15942
|
-
|
|
15943
|
-
|
|
15944
|
-
MCP_WAIT_QUEUED_HINT = "Child is queued waiting for a concurrency slot \u2014 it has not started yet. Call wait_for_turn again. Do not send_to_thread a check-in (that steers / interrupts), force_stop, or assume it failed to start.";
|
|
15945
|
-
MCP_WAIT_STOPPED_HINT = "Child was stopped before the turn finished. Do not treat this as success. send_to_thread to resume, or tell the user.";
|
|
15946
|
-
MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
|
|
15947
|
-
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.";
|
|
15948
|
-
MCP_WAIT_INPUT_REQUIRED_HINT = "Child asked the user a question (ask_user). taskState is input-required. Wait for the user to answer in that chat \u2014 do not send_to_thread a check-in.";
|
|
15651
|
+
function materialize(mem) {
|
|
15652
|
+
let output = mem.chunks.join("\n");
|
|
15653
|
+
if (output.length > MAX_SETUP_LOG_CHARS) {
|
|
15654
|
+
output = appendSetupOutput("", output);
|
|
15655
|
+
mem.chunks = [output];
|
|
15656
|
+
mem.length = output.length;
|
|
15949
15657
|
}
|
|
15950
|
-
|
|
15951
|
-
|
|
15952
|
-
|
|
15953
|
-
|
|
15954
|
-
|
|
15658
|
+
return {
|
|
15659
|
+
output,
|
|
15660
|
+
running: mem.running,
|
|
15661
|
+
exitCode: mem.exitCode,
|
|
15662
|
+
source: mem.source
|
|
15663
|
+
};
|
|
15955
15664
|
}
|
|
15956
|
-
function
|
|
15957
|
-
|
|
15958
|
-
if (!JOB_ID_RE.test(s)) {
|
|
15959
|
-
throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
|
|
15960
|
-
}
|
|
15961
|
-
return s;
|
|
15665
|
+
function setupLogPath(threadId) {
|
|
15666
|
+
return (0, import_node_path41.join)(threadsDir(), `${threadId}.setup.log.json`);
|
|
15962
15667
|
}
|
|
15963
|
-
function
|
|
15964
|
-
|
|
15668
|
+
function setupLogKeyForWorktree(worktreePath) {
|
|
15669
|
+
const norm = normalizeWorktreePath(worktreePath);
|
|
15670
|
+
return `wt-${(0, import_node_crypto8.createHash)("sha1").update(norm).digest("hex").slice(0, 16)}`;
|
|
15671
|
+
}
|
|
15672
|
+
function emptySetupLog() {
|
|
15673
|
+
return { ...EMPTY };
|
|
15674
|
+
}
|
|
15675
|
+
function persistNow(threadId) {
|
|
15676
|
+
const timer = persistTimers.get(threadId);
|
|
15677
|
+
if (timer) {
|
|
15678
|
+
clearTimeout(timer);
|
|
15679
|
+
persistTimers.delete(threadId);
|
|
15680
|
+
}
|
|
15681
|
+
const mem = memory.get(threadId);
|
|
15682
|
+
if (!mem) return;
|
|
15965
15683
|
try {
|
|
15966
|
-
|
|
15967
|
-
|
|
15684
|
+
(0, import_node_fs41.writeFileSync)(
|
|
15685
|
+
setupLogPath(threadId),
|
|
15686
|
+
`${JSON.stringify(materialize(mem))}
|
|
15687
|
+
`,
|
|
15688
|
+
"utf8"
|
|
15689
|
+
);
|
|
15968
15690
|
} catch {
|
|
15969
|
-
return false;
|
|
15970
15691
|
}
|
|
15971
15692
|
}
|
|
15972
|
-
function
|
|
15973
|
-
if (
|
|
15974
|
-
|
|
15693
|
+
function schedulePersist(threadId) {
|
|
15694
|
+
if (persistTimers.has(threadId)) return;
|
|
15695
|
+
persistTimers.set(
|
|
15696
|
+
threadId,
|
|
15697
|
+
setTimeout(() => {
|
|
15698
|
+
persistTimers.delete(threadId);
|
|
15699
|
+
persistNow(threadId);
|
|
15700
|
+
}, PERSIST_DEBOUNCE_MS)
|
|
15701
|
+
);
|
|
15702
|
+
}
|
|
15703
|
+
function loadFromDisk(threadId) {
|
|
15704
|
+
const path = setupLogPath(threadId);
|
|
15705
|
+
if (!(0, import_node_fs41.existsSync)(path)) return null;
|
|
15975
15706
|
try {
|
|
15976
|
-
|
|
15977
|
-
return
|
|
15978
|
-
|
|
15979
|
-
|
|
15707
|
+
const raw = JSON.parse((0, import_node_fs41.readFileSync)(path, "utf8"));
|
|
15708
|
+
return {
|
|
15709
|
+
output: typeof raw.output === "string" ? raw.output : "",
|
|
15710
|
+
running: Boolean(raw.running),
|
|
15711
|
+
exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
|
|
15712
|
+
source: typeof raw.source === "string" ? raw.source : null
|
|
15713
|
+
};
|
|
15714
|
+
} catch {
|
|
15715
|
+
return null;
|
|
15980
15716
|
}
|
|
15981
15717
|
}
|
|
15982
|
-
function
|
|
15983
|
-
|
|
15984
|
-
|
|
15718
|
+
function memFor(threadId) {
|
|
15719
|
+
const cached = memory.get(threadId);
|
|
15720
|
+
if (cached) return cached;
|
|
15721
|
+
const mem = fromSnapshot(loadFromDisk(threadId) ?? emptySetupLog());
|
|
15722
|
+
memory.set(threadId, mem);
|
|
15723
|
+
return mem;
|
|
15985
15724
|
}
|
|
15986
|
-
function
|
|
15987
|
-
|
|
15988
|
-
|
|
15989
|
-
|
|
15725
|
+
function readSetupLog(threadId) {
|
|
15726
|
+
const cached = memory.get(threadId);
|
|
15727
|
+
if (cached) return materialize(cached);
|
|
15728
|
+
const disk = loadFromDisk(threadId);
|
|
15729
|
+
if (disk) {
|
|
15730
|
+
memory.set(threadId, fromSnapshot(disk));
|
|
15731
|
+
return disk;
|
|
15732
|
+
}
|
|
15733
|
+
return emptySetupLog();
|
|
15990
15734
|
}
|
|
15991
|
-
function
|
|
15992
|
-
|
|
15735
|
+
function beginSetupLog(threadId) {
|
|
15736
|
+
const mem = {
|
|
15737
|
+
chunks: [],
|
|
15738
|
+
length: 0,
|
|
15739
|
+
running: true,
|
|
15740
|
+
exitCode: null,
|
|
15741
|
+
source: null
|
|
15742
|
+
};
|
|
15743
|
+
memory.set(threadId, mem);
|
|
15744
|
+
persistNow(threadId);
|
|
15745
|
+
return materialize(mem);
|
|
15993
15746
|
}
|
|
15994
|
-
function
|
|
15995
|
-
const
|
|
15996
|
-
|
|
15997
|
-
|
|
15998
|
-
|
|
15999
|
-
|
|
15747
|
+
function appendSetupLog(threadId, line) {
|
|
15748
|
+
const mem = memFor(threadId);
|
|
15749
|
+
mem.chunks.push(line);
|
|
15750
|
+
mem.length += line.length + 1;
|
|
15751
|
+
mem.running = true;
|
|
15752
|
+
if (mem.length > MAX_SETUP_LOG_CHARS * 2) materialize(mem);
|
|
15753
|
+
schedulePersist(threadId);
|
|
16000
15754
|
}
|
|
16001
|
-
function
|
|
16002
|
-
const
|
|
16003
|
-
|
|
16004
|
-
|
|
15755
|
+
function finishSetupLog(threadId, exitCode, source) {
|
|
15756
|
+
const mem = memFor(threadId);
|
|
15757
|
+
mem.running = false;
|
|
15758
|
+
mem.exitCode = exitCode;
|
|
15759
|
+
mem.source = source ?? mem.source;
|
|
15760
|
+
persistNow(threadId);
|
|
15761
|
+
return materialize(mem);
|
|
16005
15762
|
}
|
|
16006
|
-
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
|
|
16013
|
-
|
|
16014
|
-
|
|
16015
|
-
|
|
16016
|
-
|
|
16017
|
-
|
|
15763
|
+
var import_node_crypto8, import_node_fs41, import_node_path41, EMPTY, MAX_SETUP_LOG_CHARS, memory, persistTimers, PERSIST_DEBOUNCE_MS;
|
|
15764
|
+
var init_setup_log = __esm({
|
|
15765
|
+
"src/store/setup-log.ts"() {
|
|
15766
|
+
"use strict";
|
|
15767
|
+
import_node_crypto8 = require("crypto");
|
|
15768
|
+
import_node_fs41 = require("fs");
|
|
15769
|
+
import_node_path41 = require("path");
|
|
15770
|
+
init_worktree_labels();
|
|
15771
|
+
init_paths();
|
|
15772
|
+
EMPTY = {
|
|
15773
|
+
output: "",
|
|
15774
|
+
running: false,
|
|
15775
|
+
exitCode: null,
|
|
15776
|
+
source: null
|
|
15777
|
+
};
|
|
15778
|
+
MAX_SETUP_LOG_CHARS = 256e3;
|
|
15779
|
+
memory = /* @__PURE__ */ new Map();
|
|
15780
|
+
persistTimers = /* @__PURE__ */ new Map();
|
|
15781
|
+
PERSIST_DEBOUNCE_MS = 500;
|
|
16018
15782
|
}
|
|
16019
|
-
|
|
15783
|
+
});
|
|
15784
|
+
|
|
15785
|
+
// src/store/run-log.ts
|
|
15786
|
+
function fromSnapshot2(snap) {
|
|
15787
|
+
return {
|
|
15788
|
+
chunks: snap.output ? [snap.output] : [],
|
|
15789
|
+
length: snap.output.length,
|
|
15790
|
+
running: snap.running,
|
|
15791
|
+
exitCode: snap.exitCode,
|
|
15792
|
+
scriptName: snap.scriptName
|
|
15793
|
+
};
|
|
16020
15794
|
}
|
|
16021
|
-
function
|
|
16022
|
-
|
|
16023
|
-
if (
|
|
16024
|
-
|
|
16025
|
-
|
|
16026
|
-
|
|
15795
|
+
function materialize2(mem) {
|
|
15796
|
+
let output = mem.chunks.join("\n");
|
|
15797
|
+
if (output.length > MAX_RUN_LOG_CHARS) {
|
|
15798
|
+
output = appendSetupOutput("", output, MAX_RUN_LOG_CHARS);
|
|
15799
|
+
mem.chunks = [output];
|
|
15800
|
+
mem.length = output.length;
|
|
15801
|
+
}
|
|
15802
|
+
return {
|
|
15803
|
+
output,
|
|
15804
|
+
running: mem.running,
|
|
15805
|
+
exitCode: mem.exitCode,
|
|
15806
|
+
scriptName: mem.scriptName
|
|
15807
|
+
};
|
|
16027
15808
|
}
|
|
16028
|
-
function
|
|
16029
|
-
|
|
16030
|
-
|
|
16031
|
-
return t.startsWith("Detached job still running:") || t.startsWith("You ended the turn after promising to report later");
|
|
15809
|
+
function safeScriptToken(scriptName) {
|
|
15810
|
+
const token = scriptName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
|
|
15811
|
+
return token || "dev";
|
|
16032
15812
|
}
|
|
16033
|
-
function
|
|
16034
|
-
|
|
16035
|
-
const frames = text7.split(/(?=Refreshing run status every 3 seconds\.)/);
|
|
16036
|
-
const watchFrames = frames.filter((f) => f.includes(GH_WATCH_REFRESH));
|
|
16037
|
-
if (watchFrames.length <= 1) return text7;
|
|
16038
|
-
const prefix = frames[0]?.includes(GH_WATCH_REFRESH) ? "" : frames[0] ?? "";
|
|
16039
|
-
const last = watchFrames[watchFrames.length - 1] ?? "";
|
|
16040
|
-
const omitted = watchFrames.length - 1;
|
|
16041
|
-
const note = `(${omitted} identical gh run watch refresh${omitted === 1 ? "" : "es"} omitted)
|
|
16042
|
-
`;
|
|
16043
|
-
return [prefix.trimEnd(), note, last.trimStart()].filter(Boolean).join("\n");
|
|
15813
|
+
function runLogPath(key) {
|
|
15814
|
+
return (0, import_node_path42.join)(threadsDir(), `${key}.run.log.json`);
|
|
16044
15815
|
}
|
|
16045
|
-
function
|
|
16046
|
-
const
|
|
16047
|
-
const
|
|
16048
|
-
|
|
16049
|
-
${lines.slice(-MAX_JOB_DELTA_LINES).join("\n")}` : collapsed;
|
|
16050
|
-
if (out.length > MAX_JOB_DELTA_CHARS) {
|
|
16051
|
-
out = `\u2026(truncated ${out.length} chars)
|
|
16052
|
-
${out.slice(-MAX_JOB_DELTA_CHARS)}`;
|
|
16053
|
-
}
|
|
16054
|
-
return out;
|
|
15816
|
+
function runLogKeyForWorktree(worktreePath, scriptName) {
|
|
15817
|
+
const norm = normalizeWorktreePath(worktreePath);
|
|
15818
|
+
const hash = (0, import_node_crypto9.createHash)("sha1").update(norm).digest("hex").slice(0, 16);
|
|
15819
|
+
return `wt-${hash}-run-${safeScriptToken(scriptName)}`;
|
|
16055
15820
|
}
|
|
16056
|
-
function
|
|
16057
|
-
|
|
16058
|
-
return [
|
|
16059
|
-
`Detached job still running: ${ids}.`,
|
|
16060
|
-
"Do not end this turn. Loop wait_for_job (same id) until stillRunning is false. The log pane updates from each wait result.",
|
|
16061
|
-
"If the job is hanging or doing the wrong thing, call stop_job (same id) instead of looping forever.",
|
|
16062
|
-
"Then report the result. Do not tell the user you will let them know later."
|
|
16063
|
-
].join(" ");
|
|
15821
|
+
function emptyRunLog() {
|
|
15822
|
+
return { ...EMPTY2 };
|
|
16064
15823
|
}
|
|
16065
|
-
function
|
|
16066
|
-
|
|
16067
|
-
|
|
16068
|
-
|
|
16069
|
-
|
|
16070
|
-
|
|
15824
|
+
function sliceRunLogOutput(output, tailChars = DEFAULT_RUN_LOG_TAIL_CHARS) {
|
|
15825
|
+
const max = Math.max(1, tailChars);
|
|
15826
|
+
const outputChars = output.length;
|
|
15827
|
+
if (outputChars <= max) return { output, truncated: false, outputChars };
|
|
15828
|
+
const cut = output.slice(outputChars - max);
|
|
15829
|
+
const nl = cut.indexOf("\n");
|
|
15830
|
+
const text7 = nl >= 0 && nl < cut.length - 1 ? cut.slice(nl + 1) : cut;
|
|
15831
|
+
return { output: text7, truncated: true, outputChars };
|
|
16071
15832
|
}
|
|
16072
|
-
function
|
|
16073
|
-
|
|
16074
|
-
|
|
16075
|
-
|
|
16076
|
-
|
|
16077
|
-
|
|
16078
|
-
|
|
15833
|
+
function persistNow2(key) {
|
|
15834
|
+
const timer = persistTimers2.get(key);
|
|
15835
|
+
if (timer) {
|
|
15836
|
+
clearTimeout(timer);
|
|
15837
|
+
persistTimers2.delete(key);
|
|
15838
|
+
}
|
|
15839
|
+
const mem = memory2.get(key);
|
|
15840
|
+
if (!mem) return;
|
|
15841
|
+
try {
|
|
15842
|
+
(0, import_node_fs42.writeFileSync)(runLogPath(key), `${JSON.stringify(materialize2(mem))}
|
|
15843
|
+
`, "utf8");
|
|
15844
|
+
} catch {
|
|
15845
|
+
}
|
|
16079
15846
|
}
|
|
16080
|
-
function
|
|
16081
|
-
if (
|
|
16082
|
-
|
|
16083
|
-
|
|
16084
|
-
|
|
16085
|
-
|
|
16086
|
-
|
|
15847
|
+
function schedulePersist2(key) {
|
|
15848
|
+
if (persistTimers2.has(key)) return;
|
|
15849
|
+
persistTimers2.set(
|
|
15850
|
+
key,
|
|
15851
|
+
setTimeout(() => {
|
|
15852
|
+
persistTimers2.delete(key);
|
|
15853
|
+
persistNow2(key);
|
|
15854
|
+
}, PERSIST_DEBOUNCE_MS2)
|
|
15855
|
+
);
|
|
15856
|
+
}
|
|
15857
|
+
function loadFromDisk2(key) {
|
|
15858
|
+
const path = runLogPath(key);
|
|
15859
|
+
if (!(0, import_node_fs42.existsSync)(path)) return null;
|
|
15860
|
+
try {
|
|
15861
|
+
const raw = JSON.parse((0, import_node_fs42.readFileSync)(path, "utf8"));
|
|
16087
15862
|
return {
|
|
16088
|
-
|
|
16089
|
-
|
|
16090
|
-
|
|
15863
|
+
output: typeof raw.output === "string" ? raw.output : "",
|
|
15864
|
+
running: Boolean(raw.running),
|
|
15865
|
+
exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
|
|
15866
|
+
scriptName: typeof raw.scriptName === "string" ? raw.scriptName : null
|
|
16091
15867
|
};
|
|
15868
|
+
} catch {
|
|
15869
|
+
return null;
|
|
16092
15870
|
}
|
|
16093
|
-
if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
|
|
16094
|
-
return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
|
|
16095
|
-
}
|
|
16096
|
-
return { action: "none" };
|
|
16097
15871
|
}
|
|
16098
|
-
function
|
|
16099
|
-
|
|
16100
|
-
|
|
16101
|
-
|
|
15872
|
+
function memFor2(key) {
|
|
15873
|
+
const cached = memory2.get(key);
|
|
15874
|
+
if (cached) return cached;
|
|
15875
|
+
const mem = fromSnapshot2(loadFromDisk2(key) ?? emptyRunLog());
|
|
15876
|
+
memory2.set(key, mem);
|
|
15877
|
+
return mem;
|
|
16102
15878
|
}
|
|
16103
|
-
function
|
|
16104
|
-
const
|
|
16105
|
-
|
|
15879
|
+
function readRunLog(key) {
|
|
15880
|
+
const cached = memory2.get(key);
|
|
15881
|
+
if (cached) return materialize2(cached);
|
|
15882
|
+
const disk = loadFromDisk2(key);
|
|
15883
|
+
if (disk) {
|
|
15884
|
+
memory2.set(key, fromSnapshot2(disk));
|
|
15885
|
+
return disk;
|
|
15886
|
+
}
|
|
15887
|
+
return emptyRunLog();
|
|
16106
15888
|
}
|
|
16107
|
-
function
|
|
16108
|
-
|
|
16109
|
-
|
|
16110
|
-
|
|
16111
|
-
|
|
15889
|
+
function beginRunLog(key, scriptName) {
|
|
15890
|
+
const mem = {
|
|
15891
|
+
chunks: [],
|
|
15892
|
+
length: 0,
|
|
15893
|
+
running: true,
|
|
15894
|
+
exitCode: null,
|
|
15895
|
+
scriptName
|
|
15896
|
+
};
|
|
15897
|
+
memory2.set(key, mem);
|
|
15898
|
+
persistNow2(key);
|
|
15899
|
+
return materialize2(mem);
|
|
16112
15900
|
}
|
|
16113
|
-
function
|
|
16114
|
-
const
|
|
16115
|
-
|
|
16116
|
-
|
|
16117
|
-
|
|
15901
|
+
function appendRunLog(key, line) {
|
|
15902
|
+
const mem = memFor2(key);
|
|
15903
|
+
mem.chunks.push(line);
|
|
15904
|
+
mem.length += line.length + 1;
|
|
15905
|
+
mem.running = true;
|
|
15906
|
+
if (mem.length > MAX_RUN_LOG_CHARS * 2) materialize2(mem);
|
|
15907
|
+
schedulePersist2(key);
|
|
16118
15908
|
}
|
|
16119
|
-
function
|
|
16120
|
-
const
|
|
16121
|
-
|
|
16122
|
-
|
|
16123
|
-
|
|
16124
|
-
|
|
16125
|
-
exitCode: readIntFile((0, import_node_path45.join)(dir, "exit")),
|
|
16126
|
-
log: (0, import_node_path45.join)(dir, "log"),
|
|
16127
|
-
cursor: (0, import_node_path45.join)(dir, "present.cursor"),
|
|
16128
|
-
progress: tailProgress((0, import_node_path45.join)(dir, "log"))
|
|
16129
|
-
};
|
|
15909
|
+
function finishRunLog(key, exitCode) {
|
|
15910
|
+
const mem = memFor2(key);
|
|
15911
|
+
mem.running = false;
|
|
15912
|
+
mem.exitCode = exitCode;
|
|
15913
|
+
persistNow2(key);
|
|
15914
|
+
return materialize2(mem);
|
|
16130
15915
|
}
|
|
16131
|
-
function
|
|
16132
|
-
|
|
16133
|
-
|
|
16134
|
-
|
|
16135
|
-
|
|
16136
|
-
|
|
16137
|
-
|
|
16138
|
-
(
|
|
16139
|
-
|
|
16140
|
-
|
|
15916
|
+
function flushRunLog(key) {
|
|
15917
|
+
persistNow2(key);
|
|
15918
|
+
}
|
|
15919
|
+
var import_node_crypto9, import_node_fs42, import_node_path42, EMPTY2, MAX_RUN_LOG_CHARS, DEFAULT_RUN_LOG_TAIL_CHARS, memory2, persistTimers2, PERSIST_DEBOUNCE_MS2;
|
|
15920
|
+
var init_run_log = __esm({
|
|
15921
|
+
"src/store/run-log.ts"() {
|
|
15922
|
+
"use strict";
|
|
15923
|
+
import_node_crypto9 = require("crypto");
|
|
15924
|
+
import_node_fs42 = require("fs");
|
|
15925
|
+
import_node_path42 = require("path");
|
|
15926
|
+
init_worktree_labels();
|
|
15927
|
+
init_setup_log();
|
|
15928
|
+
init_paths();
|
|
15929
|
+
EMPTY2 = {
|
|
15930
|
+
output: "",
|
|
15931
|
+
running: false,
|
|
15932
|
+
exitCode: null,
|
|
15933
|
+
scriptName: null
|
|
15934
|
+
};
|
|
15935
|
+
MAX_RUN_LOG_CHARS = 256e3;
|
|
15936
|
+
DEFAULT_RUN_LOG_TAIL_CHARS = 16e3;
|
|
15937
|
+
memory2 = /* @__PURE__ */ new Map();
|
|
15938
|
+
persistTimers2 = /* @__PURE__ */ new Map();
|
|
15939
|
+
PERSIST_DEBOUNCE_MS2 = 500;
|
|
16141
15940
|
}
|
|
16142
|
-
|
|
16143
|
-
|
|
16144
|
-
|
|
16145
|
-
|
|
16146
|
-
|
|
16147
|
-
failed: Boolean(failed && !stillRunning && !ok),
|
|
16148
|
-
status,
|
|
16149
|
-
id,
|
|
16150
|
-
pid: snap.pid,
|
|
16151
|
-
exitCode: snap.exitCode ?? void 0,
|
|
16152
|
-
phase: lastProgressLine(progress),
|
|
16153
|
-
delta,
|
|
16154
|
-
progress,
|
|
16155
|
-
hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
|
|
16156
|
-
};
|
|
15941
|
+
});
|
|
15942
|
+
|
|
15943
|
+
// src/git/orphan-cleanup.ts
|
|
15944
|
+
function isSideboardWorktreePath(path) {
|
|
15945
|
+
return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
|
|
16157
15946
|
}
|
|
16158
|
-
async function
|
|
16159
|
-
|
|
15947
|
+
async function findOrphanWorktrees(repoPaths) {
|
|
15948
|
+
const threads = listThreads({ includeArchived: true });
|
|
15949
|
+
const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
|
|
15950
|
+
const repos = new Set(
|
|
15951
|
+
repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
|
|
15952
|
+
);
|
|
15953
|
+
const homeRoot = sideboardWorkspacesDir();
|
|
15954
|
+
if ((0, import_node_fs43.existsSync)(homeRoot)) {
|
|
15955
|
+
try {
|
|
15956
|
+
for (const entry of (0, import_node_fs43.readdirSync)(homeRoot, { withFileTypes: true })) {
|
|
15957
|
+
if (!entry.isDirectory()) continue;
|
|
15958
|
+
void entry;
|
|
15959
|
+
}
|
|
15960
|
+
} catch {
|
|
15961
|
+
}
|
|
15962
|
+
}
|
|
15963
|
+
const orphans = [];
|
|
15964
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15965
|
+
for (const repoPath of repos) {
|
|
15966
|
+
if (!repoPath || !(0, import_node_fs43.existsSync)(repoPath)) continue;
|
|
15967
|
+
try {
|
|
15968
|
+
const wts = await listWorktrees(repoPath);
|
|
15969
|
+
for (const wt of wts) {
|
|
15970
|
+
const path = wt.path.replace(/\/$/, "");
|
|
15971
|
+
if (!isSideboardWorktreePath(path)) continue;
|
|
15972
|
+
if (known.has(path)) continue;
|
|
15973
|
+
if (seen.has(path)) continue;
|
|
15974
|
+
seen.add(path);
|
|
15975
|
+
let mtimeMs = 0;
|
|
15976
|
+
try {
|
|
15977
|
+
mtimeMs = (0, import_node_fs43.statSync)(path).mtimeMs;
|
|
15978
|
+
} catch {
|
|
15979
|
+
mtimeMs = 0;
|
|
15980
|
+
}
|
|
15981
|
+
orphans.push({ path, repoPath, mtimeMs });
|
|
15982
|
+
}
|
|
15983
|
+
} catch {
|
|
15984
|
+
}
|
|
15985
|
+
try {
|
|
15986
|
+
const root = worktreesRoot(repoPath);
|
|
15987
|
+
if ((0, import_node_fs43.existsSync)(root)) {
|
|
15988
|
+
for (const entry of (0, import_node_fs43.readdirSync)(root, { withFileTypes: true })) {
|
|
15989
|
+
if (!entry.isDirectory()) continue;
|
|
15990
|
+
const path = (0, import_node_path43.join)(root, entry.name).replace(/\/$/, "");
|
|
15991
|
+
if (known.has(path) || seen.has(path)) continue;
|
|
15992
|
+
if (!(0, import_node_fs43.existsSync)((0, import_node_path43.join)(path, ".git"))) continue;
|
|
15993
|
+
seen.add(path);
|
|
15994
|
+
let mtimeMs = 0;
|
|
15995
|
+
try {
|
|
15996
|
+
mtimeMs = (0, import_node_fs43.statSync)(path).mtimeMs;
|
|
15997
|
+
} catch {
|
|
15998
|
+
mtimeMs = Date.now();
|
|
15999
|
+
}
|
|
16000
|
+
orphans.push({ path, repoPath, mtimeMs });
|
|
16001
|
+
}
|
|
16002
|
+
}
|
|
16003
|
+
} catch {
|
|
16004
|
+
}
|
|
16005
|
+
}
|
|
16006
|
+
return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
16160
16007
|
}
|
|
16161
|
-
async function
|
|
16162
|
-
const
|
|
16163
|
-
const
|
|
16164
|
-
const
|
|
16165
|
-
const
|
|
16166
|
-
const
|
|
16167
|
-
|
|
16008
|
+
async function cleanupOrphanWorktrees(opts) {
|
|
16009
|
+
const settings = loadAppSettings();
|
|
16010
|
+
const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
|
|
16011
|
+
const orphans = await findOrphanWorktrees(opts?.repoPaths);
|
|
16012
|
+
const activeCount = listThreads().filter((t) => t.status !== "archived").length;
|
|
16013
|
+
const budget = Math.max(0, maxCount - activeCount);
|
|
16014
|
+
const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
16015
|
+
const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
|
|
16016
|
+
const removed = [];
|
|
16017
|
+
const kept = [];
|
|
16018
|
+
for (const orphan of orphans) {
|
|
16019
|
+
if (keep.has(orphan.path)) {
|
|
16020
|
+
kept.push(orphan.path);
|
|
16021
|
+
continue;
|
|
16022
|
+
}
|
|
16023
|
+
if (opts?.dryRun) {
|
|
16024
|
+
removed.push(orphan.path);
|
|
16025
|
+
continue;
|
|
16026
|
+
}
|
|
16027
|
+
try {
|
|
16028
|
+
await removeWorktree(orphan.repoPath, orphan.path);
|
|
16029
|
+
removed.push(orphan.path);
|
|
16030
|
+
} catch {
|
|
16031
|
+
kept.push(orphan.path);
|
|
16032
|
+
}
|
|
16033
|
+
}
|
|
16034
|
+
if (!opts?.dryRun) {
|
|
16035
|
+
updateAdvancedSettings({
|
|
16036
|
+
worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16037
|
+
});
|
|
16038
|
+
}
|
|
16039
|
+
return { removed, kept, orphans };
|
|
16040
|
+
}
|
|
16041
|
+
function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
|
|
16042
|
+
const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
|
|
16043
|
+
const last = settings.advanced.worktreeLastCleanupAt;
|
|
16044
|
+
if (!last) return true;
|
|
16045
|
+
const elapsed = Date.now() - Date.parse(last);
|
|
16046
|
+
return elapsed >= intervalHours * 36e5;
|
|
16047
|
+
}
|
|
16048
|
+
var import_node_fs43, import_node_path43;
|
|
16049
|
+
var init_orphan_cleanup = __esm({
|
|
16050
|
+
"src/git/orphan-cleanup.ts"() {
|
|
16051
|
+
"use strict";
|
|
16052
|
+
import_node_fs43 = require("fs");
|
|
16053
|
+
import_node_path43 = require("path");
|
|
16054
|
+
init_worktree();
|
|
16055
|
+
init_thread_store();
|
|
16056
|
+
init_paths();
|
|
16057
|
+
init_app_settings();
|
|
16058
|
+
}
|
|
16059
|
+
});
|
|
16060
|
+
|
|
16061
|
+
// src/store/history-cleanup.ts
|
|
16062
|
+
function stripArchivedTranscript(thread) {
|
|
16063
|
+
writeThread(
|
|
16064
|
+
{
|
|
16065
|
+
...thread,
|
|
16066
|
+
attachments: [],
|
|
16067
|
+
pendingTurnAttachments: [],
|
|
16068
|
+
queueAttachments: [],
|
|
16069
|
+
compactedMessages: void 0,
|
|
16070
|
+
messages: [historyRetentionStubMessage(thread)]
|
|
16071
|
+
},
|
|
16072
|
+
{ touch: false }
|
|
16073
|
+
);
|
|
16074
|
+
}
|
|
16075
|
+
function cleanupArchivedHistory(opts = {}) {
|
|
16076
|
+
const settings = loadAppSettings();
|
|
16077
|
+
if (!opts.force && opts.purgeOlderThanDays == null && !autoCleanupHistoryEnabled(settings)) {
|
|
16078
|
+
return { stripped: [], purged: [] };
|
|
16079
|
+
}
|
|
16080
|
+
const archived = listThreads({ includeArchived: true }).filter((t) => t.status === "archived");
|
|
16081
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
16082
|
+
const plan = opts.purgeOlderThanDays != null ? { stripIds: [], purgeIds: planHistoryAgePurge(archived, opts.purgeOlderThanDays, nowMs) } : planHistoryRetention(archived, {
|
|
16083
|
+
maxCount: opts.maxCount ?? historyMaxCount(settings),
|
|
16084
|
+
maxDays: opts.maxDays ?? historyMaxDays(settings),
|
|
16085
|
+
nowMs
|
|
16086
|
+
});
|
|
16087
|
+
const stripped = [];
|
|
16088
|
+
const purged = [];
|
|
16089
|
+
if (opts.dryRun) {
|
|
16090
|
+
return { stripped: plan.stripIds, purged: plan.purgeIds };
|
|
16091
|
+
}
|
|
16092
|
+
for (const id of plan.stripIds) {
|
|
16093
|
+
const thread = readThread(id);
|
|
16094
|
+
if (!thread || thread.status !== "archived") continue;
|
|
16095
|
+
try {
|
|
16096
|
+
stripArchivedTranscript(thread);
|
|
16097
|
+
stripped.push(id);
|
|
16098
|
+
} catch {
|
|
16099
|
+
}
|
|
16100
|
+
}
|
|
16101
|
+
for (const id of plan.purgeIds) {
|
|
16102
|
+
const thread = readThread(id);
|
|
16103
|
+
if (!thread || thread.status !== "archived") continue;
|
|
16104
|
+
try {
|
|
16105
|
+
deleteThreadRecord(id);
|
|
16106
|
+
purged.push(id);
|
|
16107
|
+
} catch {
|
|
16108
|
+
}
|
|
16109
|
+
}
|
|
16110
|
+
if (stripped.length > 0 || purged.length > 0) {
|
|
16111
|
+
updateAdvancedSettings({
|
|
16112
|
+
historyLastCleanupAt: new Date(nowMs).toISOString()
|
|
16113
|
+
});
|
|
16114
|
+
}
|
|
16115
|
+
return { stripped, purged };
|
|
16116
|
+
}
|
|
16117
|
+
function shouldRunHistoryCleanup(settings = loadAppSettings()) {
|
|
16118
|
+
if (!autoCleanupHistoryEnabled(settings)) return false;
|
|
16119
|
+
const intervalHours = settings.advanced.historyCleanupIntervalHours ?? HISTORY_CLEANUP_INTERVAL_HOURS_DEFAULT;
|
|
16120
|
+
const last = settings.advanced.historyLastCleanupAt;
|
|
16121
|
+
if (!last) return true;
|
|
16122
|
+
const elapsed = Date.now() - Date.parse(last);
|
|
16123
|
+
return Number.isFinite(elapsed) && elapsed >= intervalHours * 36e5;
|
|
16124
|
+
}
|
|
16125
|
+
var init_history_cleanup = __esm({
|
|
16126
|
+
"src/store/history-cleanup.ts"() {
|
|
16127
|
+
"use strict";
|
|
16128
|
+
init_app_settings();
|
|
16129
|
+
init_history_retention();
|
|
16130
|
+
init_thread_store();
|
|
16131
|
+
}
|
|
16132
|
+
});
|
|
16133
|
+
|
|
16134
|
+
// src/git/apply-into-main.ts
|
|
16135
|
+
async function applyThreadIntoMain(thread, opts) {
|
|
16136
|
+
const method = opts?.method ?? "merge";
|
|
16137
|
+
const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
|
|
16138
|
+
const status = await (0, import_execa5.execa)("git", ["status", "--porcelain"], {
|
|
16139
|
+
cwd: thread.repoPath,
|
|
16140
|
+
reject: false
|
|
16141
|
+
});
|
|
16142
|
+
if (status.stdout.trim()) {
|
|
16143
|
+
throw new Error(
|
|
16144
|
+
`Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
|
|
16145
|
+
);
|
|
16146
|
+
}
|
|
16147
|
+
const checkout = await (0, import_execa5.execa)("git", ["checkout", target], {
|
|
16148
|
+
cwd: thread.repoPath,
|
|
16149
|
+
reject: false
|
|
16150
|
+
});
|
|
16151
|
+
if (checkout.exitCode !== 0) {
|
|
16152
|
+
throw new Error(
|
|
16153
|
+
`Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
|
|
16154
|
+
);
|
|
16155
|
+
}
|
|
16156
|
+
if (method === "cherry-pick") {
|
|
16157
|
+
const log = await (0, import_execa5.execa)(
|
|
16158
|
+
"git",
|
|
16159
|
+
["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
|
|
16160
|
+
{ cwd: thread.repoPath, reject: false }
|
|
16161
|
+
);
|
|
16162
|
+
const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
16163
|
+
if (!shas.length) {
|
|
16164
|
+
return {
|
|
16165
|
+
applied: false,
|
|
16166
|
+
method,
|
|
16167
|
+
targetBranch: target,
|
|
16168
|
+
message: `No commits on ${thread.branchName} not already in ${target}`
|
|
16169
|
+
};
|
|
16170
|
+
}
|
|
16171
|
+
for (const sha of shas) {
|
|
16172
|
+
const cp = await (0, import_execa5.execa)("git", ["cherry-pick", sha], {
|
|
16173
|
+
cwd: thread.repoPath,
|
|
16174
|
+
reject: false
|
|
16175
|
+
});
|
|
16176
|
+
if (cp.exitCode !== 0) {
|
|
16177
|
+
await (0, import_execa5.execa)("git", ["cherry-pick", "--abort"], {
|
|
16178
|
+
cwd: thread.repoPath,
|
|
16179
|
+
reject: false
|
|
16180
|
+
});
|
|
16181
|
+
throw new Error(
|
|
16182
|
+
`Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
|
|
16183
|
+
);
|
|
16184
|
+
}
|
|
16185
|
+
}
|
|
16168
16186
|
return {
|
|
16169
|
-
|
|
16170
|
-
|
|
16171
|
-
|
|
16172
|
-
|
|
16173
|
-
id: jobId,
|
|
16174
|
-
delta: "",
|
|
16175
|
-
progress: "No detached job. Start one first.",
|
|
16176
|
-
hint: "Start with detached-job.cjs start <id> -- <command>, then call wait_for_job again."
|
|
16187
|
+
applied: true,
|
|
16188
|
+
method,
|
|
16189
|
+
targetBranch: target,
|
|
16190
|
+
message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
|
|
16177
16191
|
};
|
|
16178
16192
|
}
|
|
16179
|
-
const
|
|
16180
|
-
|
|
16181
|
-
|
|
16182
|
-
|
|
16183
|
-
|
|
16184
|
-
|
|
16185
|
-
|
|
16186
|
-
|
|
16193
|
+
const merge = await (0, import_execa5.execa)(
|
|
16194
|
+
"git",
|
|
16195
|
+
["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
|
|
16196
|
+
{ cwd: thread.repoPath, reject: false }
|
|
16197
|
+
);
|
|
16198
|
+
if (merge.exitCode !== 0) {
|
|
16199
|
+
await (0, import_execa5.execa)("git", ["merge", "--abort"], {
|
|
16200
|
+
cwd: thread.repoPath,
|
|
16201
|
+
reject: false
|
|
16202
|
+
});
|
|
16203
|
+
throw new Error(
|
|
16204
|
+
`Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
|
|
16205
|
+
);
|
|
16206
|
+
}
|
|
16207
|
+
return {
|
|
16208
|
+
applied: true,
|
|
16209
|
+
method: "merge",
|
|
16210
|
+
targetBranch: target,
|
|
16211
|
+
message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
|
|
16212
|
+
};
|
|
16213
|
+
}
|
|
16214
|
+
var import_execa5;
|
|
16215
|
+
var init_apply_into_main = __esm({
|
|
16216
|
+
"src/git/apply-into-main.ts"() {
|
|
16217
|
+
"use strict";
|
|
16218
|
+
import_execa5 = require("execa");
|
|
16219
|
+
init_worktree();
|
|
16220
|
+
}
|
|
16221
|
+
});
|
|
16222
|
+
|
|
16223
|
+
// src/git/clone-repo.ts
|
|
16224
|
+
async function cloneRepoIntoSideboard(opts) {
|
|
16225
|
+
const url = opts.url.trim();
|
|
16226
|
+
if (!url) throw new Error("Clone URL is required");
|
|
16227
|
+
let name = opts.name?.trim();
|
|
16228
|
+
if (!name) {
|
|
16229
|
+
const leaf = (0, import_node_path44.basename)(url.replace(/\/$/, "").replace(/\.git$/, ""));
|
|
16230
|
+
name = leaf || "repo";
|
|
16187
16231
|
}
|
|
16188
|
-
|
|
16189
|
-
|
|
16190
|
-
|
|
16191
|
-
|
|
16192
|
-
|
|
16232
|
+
name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
16233
|
+
const dest = (0, import_node_path44.join)(sideboardReposDir(), name);
|
|
16234
|
+
if ((0, import_node_fs44.existsSync)(dest)) {
|
|
16235
|
+
const repoPath2 = await resolveRepoRoot(dest);
|
|
16236
|
+
const workspace2 = await ensureWorkspace(repoPath2);
|
|
16237
|
+
return { repoPath: repoPath2, workspace: workspace2 };
|
|
16193
16238
|
}
|
|
16194
|
-
|
|
16195
|
-
|
|
16239
|
+
const result = await (0, import_execa6.execa)("git", ["clone", url, dest], { reject: false });
|
|
16240
|
+
if (result.exitCode !== 0) {
|
|
16241
|
+
throw new Error(
|
|
16242
|
+
`git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
|
|
16243
|
+
);
|
|
16244
|
+
}
|
|
16245
|
+
const repoPath = await resolveRepoRoot(dest);
|
|
16246
|
+
const workspace = await ensureWorkspace(repoPath);
|
|
16247
|
+
return { repoPath, workspace };
|
|
16196
16248
|
}
|
|
16197
|
-
|
|
16198
|
-
|
|
16199
|
-
|
|
16200
|
-
|
|
16201
|
-
|
|
16202
|
-
|
|
16203
|
-
|
|
16204
|
-
|
|
16249
|
+
var import_node_fs44, import_node_path44, import_execa6;
|
|
16250
|
+
var init_clone_repo = __esm({
|
|
16251
|
+
"src/git/clone-repo.ts"() {
|
|
16252
|
+
"use strict";
|
|
16253
|
+
import_node_fs44 = require("fs");
|
|
16254
|
+
import_node_path44 = require("path");
|
|
16255
|
+
import_execa6 = require("execa");
|
|
16256
|
+
init_paths();
|
|
16257
|
+
init_workspaces();
|
|
16258
|
+
init_worktree();
|
|
16259
|
+
}
|
|
16260
|
+
});
|
|
16261
|
+
|
|
16262
|
+
// src/types/thread.ts
|
|
16263
|
+
function isAdoptableRunScriptRequest(req) {
|
|
16264
|
+
return Boolean(req && !req.fulfilledAt && !req.error);
|
|
16265
|
+
}
|
|
16266
|
+
var init_thread = __esm({
|
|
16267
|
+
"src/types/thread.ts"() {
|
|
16268
|
+
"use strict";
|
|
16205
16269
|
}
|
|
16270
|
+
});
|
|
16271
|
+
|
|
16272
|
+
// src/store/desktop-host.ts
|
|
16273
|
+
function desktopHostPidPath() {
|
|
16274
|
+
return (0, import_node_path45.join)(appDataDir(), "desktop-host.pid");
|
|
16275
|
+
}
|
|
16276
|
+
function readDesktopHostPid() {
|
|
16206
16277
|
try {
|
|
16207
|
-
|
|
16278
|
+
const pid = Number.parseInt((0, import_node_fs45.readFileSync)(desktopHostPidPath(), "utf8").trim(), 10);
|
|
16279
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
16280
|
+
return pid;
|
|
16208
16281
|
} catch {
|
|
16282
|
+
return null;
|
|
16209
16283
|
}
|
|
16210
16284
|
}
|
|
16211
|
-
function
|
|
16285
|
+
function pidAlive(pid) {
|
|
16212
16286
|
try {
|
|
16213
|
-
(
|
|
16214
|
-
|
|
16215
|
-
`;
|
|
16216
|
-
(0, import_node_fs45.appendFileSync)(logFile, body);
|
|
16287
|
+
process.kill(pid, 0);
|
|
16288
|
+
return true;
|
|
16217
16289
|
} catch {
|
|
16290
|
+
return false;
|
|
16218
16291
|
}
|
|
16219
16292
|
}
|
|
16220
|
-
|
|
16221
|
-
const
|
|
16222
|
-
|
|
16223
|
-
const dir = resolveJobDir(root, jobId);
|
|
16224
|
-
const sleep = opts?.sleep ?? sleepMs;
|
|
16225
|
-
const why = opts?.reason?.trim() ?? "";
|
|
16226
|
-
if (!(0, import_node_fs45.existsSync)(dir)) {
|
|
16227
|
-
return {
|
|
16228
|
-
stillRunning: false,
|
|
16229
|
-
ok: false,
|
|
16230
|
-
failed: true,
|
|
16231
|
-
status: "failed",
|
|
16232
|
-
id: jobId,
|
|
16233
|
-
delta: "",
|
|
16234
|
-
progress: "No detached job. Start one first.",
|
|
16235
|
-
hint: "Start with detached-job.cjs start <id> -- <command>, then wait_for_job or stop_job.",
|
|
16236
|
-
stopped: false,
|
|
16237
|
-
reason: "not-found"
|
|
16238
|
-
};
|
|
16239
|
-
}
|
|
16240
|
-
let snap = snapshotJob(dir);
|
|
16241
|
-
if (!snap.running) {
|
|
16242
|
-
const result2 = toResult(jobId, snap);
|
|
16243
|
-
return {
|
|
16244
|
-
...result2,
|
|
16245
|
-
stopped: false,
|
|
16246
|
-
reason: "not-running",
|
|
16247
|
-
hint: "That job is already finished. Read the log; do not wait again unless you start a new command."
|
|
16248
|
-
};
|
|
16249
|
-
}
|
|
16250
|
-
appendJobLog(snap.log, `$ stop${why ? `: ${why}` : ""}`);
|
|
16251
|
-
const targetPid = snap.pid;
|
|
16252
|
-
if (targetPid != null) killProcessTree(targetPid, "SIGTERM");
|
|
16253
|
-
const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
|
|
16254
|
-
const deadline = Date.now() + graceMs;
|
|
16255
|
-
while (Date.now() < deadline && jobTreeAlive(targetPid)) {
|
|
16256
|
-
await sleep(Math.min(100, Math.max(20, deadline - Date.now())));
|
|
16257
|
-
}
|
|
16258
|
-
if (targetPid != null) {
|
|
16259
|
-
killProcessTree(targetPid, "SIGKILL");
|
|
16260
|
-
await sleep(100);
|
|
16261
|
-
}
|
|
16262
|
-
const exitFile = (0, import_node_path45.join)(dir, "exit");
|
|
16263
|
-
if (!(0, import_node_fs45.existsSync)(exitFile)) {
|
|
16264
|
-
try {
|
|
16265
|
-
(0, import_node_fs45.writeFileSync)(exitFile, "1\n");
|
|
16266
|
-
} catch {
|
|
16267
|
-
}
|
|
16268
|
-
}
|
|
16269
|
-
snap = snapshotJob(dir);
|
|
16270
|
-
const stillRunning = jobTreeAlive(targetPid) || snap.running;
|
|
16271
|
-
const result = toResult(jobId, snap, { failed: true });
|
|
16272
|
-
const stopped = !stillRunning;
|
|
16273
|
-
return {
|
|
16274
|
-
...result,
|
|
16275
|
-
stillRunning,
|
|
16276
|
-
ok: false,
|
|
16277
|
-
failed: true,
|
|
16278
|
-
status: stillRunning ? "running" : "failed",
|
|
16279
|
-
stopped,
|
|
16280
|
-
reason: stopped ? "stopped" : "still-running",
|
|
16281
|
-
stopReason: why || void 0,
|
|
16282
|
-
hint: stopped ? MCP_STOP_JOB_HINT : MCP_WAIT_JOB_STILL_RUNNING_HINT
|
|
16283
|
-
};
|
|
16293
|
+
function isDesktopHostAlive() {
|
|
16294
|
+
const pid = readDesktopHostPid();
|
|
16295
|
+
return pid != null && pidAlive(pid);
|
|
16284
16296
|
}
|
|
16285
|
-
|
|
16286
|
-
|
|
16287
|
-
|
|
16297
|
+
function isThisProcessDesktopHost() {
|
|
16298
|
+
return readDesktopHostPid() === process.pid && pidAlive(process.pid);
|
|
16299
|
+
}
|
|
16300
|
+
function thisProcessShouldDrainAgentQueues() {
|
|
16301
|
+
if (isThisProcessDesktopHost()) return true;
|
|
16302
|
+
return !isDesktopHostAlive();
|
|
16303
|
+
}
|
|
16304
|
+
var import_node_fs45, import_node_path45;
|
|
16305
|
+
var init_desktop_host = __esm({
|
|
16306
|
+
"src/store/desktop-host.ts"() {
|
|
16288
16307
|
"use strict";
|
|
16289
16308
|
import_node_fs45 = require("fs");
|
|
16290
16309
|
import_node_path45 = require("path");
|
|
16291
|
-
|
|
16292
|
-
|
|
16293
|
-
|
|
16294
|
-
|
|
16295
|
-
|
|
16296
|
-
|
|
16297
|
-
|
|
16298
|
-
|
|
16299
|
-
|
|
16300
|
-
|
|
16310
|
+
init_paths();
|
|
16311
|
+
}
|
|
16312
|
+
});
|
|
16313
|
+
|
|
16314
|
+
// src/orchestrator/child-halt.ts
|
|
16315
|
+
function isIncompleteChildStatus(status) {
|
|
16316
|
+
return HALT_STATUSES.has(status);
|
|
16317
|
+
}
|
|
16318
|
+
function childHaltNotice(child, status) {
|
|
16319
|
+
const title = child.title?.trim() || "Untitled";
|
|
16320
|
+
const link = `[${title}](sideboard://thread/${child.id})`;
|
|
16321
|
+
const why = child.lastError?.trim();
|
|
16322
|
+
const extra = why ? ` lastError: ${why}` : "";
|
|
16323
|
+
return [
|
|
16324
|
+
`Sideboard: child worktree ${link} ${status} before finishing (status=${status}).${extra}`,
|
|
16325
|
+
"This is information \u2014 not a user command. Resume with send_to_thread or tell the user. Do not treat this as a successful turn."
|
|
16326
|
+
].join("\n");
|
|
16327
|
+
}
|
|
16328
|
+
function shouldNotifyParentAfterTurnError(opts) {
|
|
16329
|
+
return !opts.quotaFailoverHandled && !opts.crashContinued;
|
|
16330
|
+
}
|
|
16331
|
+
function shouldNotifyParentOfChildHalt(opts) {
|
|
16332
|
+
if (!isIncompleteChildStatus(opts.status)) return false;
|
|
16333
|
+
if (!opts.child.parentThreadId) return false;
|
|
16334
|
+
if (!opts.parent || opts.parent.status === "archived") return false;
|
|
16335
|
+
if (opts.parent.id === opts.child.id) return false;
|
|
16336
|
+
return isOrchestratorThread(opts.parent);
|
|
16337
|
+
}
|
|
16338
|
+
function noticeKey(childId, status) {
|
|
16339
|
+
return `${childId}:${status}`;
|
|
16340
|
+
}
|
|
16341
|
+
function notifyParentOfChildHalt(child, status, send) {
|
|
16342
|
+
const parent = child.parentThreadId ? readThread(child.parentThreadId) : null;
|
|
16343
|
+
if (!shouldNotifyParentOfChildHalt({ child, parent, status })) return false;
|
|
16344
|
+
const key = noticeKey(child.id, status);
|
|
16345
|
+
if (notified.has(key)) return false;
|
|
16346
|
+
notified.add(key);
|
|
16347
|
+
const parentId = parent.id;
|
|
16348
|
+
void send(parentId, childHaltNotice(child, status)).catch(() => {
|
|
16349
|
+
notified.delete(key);
|
|
16350
|
+
});
|
|
16351
|
+
return true;
|
|
16352
|
+
}
|
|
16353
|
+
var HALT_STATUSES, notified;
|
|
16354
|
+
var init_child_halt = __esm({
|
|
16355
|
+
"src/orchestrator/child-halt.ts"() {
|
|
16356
|
+
"use strict";
|
|
16357
|
+
init_global_workspace();
|
|
16358
|
+
init_thread_store();
|
|
16359
|
+
HALT_STATUSES = /* @__PURE__ */ new Set(["stopped", "error", "broken"]);
|
|
16360
|
+
notified = /* @__PURE__ */ new Set();
|
|
16301
16361
|
}
|
|
16302
16362
|
});
|
|
16303
16363
|
|
|
@@ -21364,6 +21424,13 @@ function isPresentPlanToolName(name) {
|
|
|
21364
21424
|
if (!name) return false;
|
|
21365
21425
|
return /present_plan$/i.test(name) || /^mcp__sideboard__present_plan$/i.test(name);
|
|
21366
21426
|
}
|
|
21427
|
+
function threadMayPresentPlan(thread) {
|
|
21428
|
+
return Boolean(thread?.planMode);
|
|
21429
|
+
}
|
|
21430
|
+
function shouldRegisterPresentPlan(thread) {
|
|
21431
|
+
if (thread == null) return true;
|
|
21432
|
+
return threadMayPresentPlan(thread);
|
|
21433
|
+
}
|
|
21367
21434
|
function extractPresentedPlan(parts) {
|
|
21368
21435
|
if (!parts?.length) return null;
|
|
21369
21436
|
for (let i = parts.length - 1; i >= 0; i--) {
|
|
@@ -21401,7 +21468,7 @@ function resolvePlanMarkdown(opts) {
|
|
|
21401
21468
|
}
|
|
21402
21469
|
return null;
|
|
21403
21470
|
}
|
|
21404
|
-
var PLAN_FILE_REL, PLAN_FILE_NAME, LEGACY_PLAN_FILE_REL;
|
|
21471
|
+
var PLAN_FILE_REL, PLAN_FILE_NAME, LEGACY_PLAN_FILE_REL, PRESENT_PLAN_TOOL_DESCRIPTION, PRESENT_PLAN_REQUIRES_PLAN_MODE;
|
|
21405
21472
|
var init_plan_present = __esm({
|
|
21406
21473
|
"src/plan/plan-present.ts"() {
|
|
21407
21474
|
"use strict";
|
|
@@ -21409,6 +21476,8 @@ var init_plan_present = __esm({
|
|
|
21409
21476
|
PLAN_FILE_REL = `${ATTACHMENTS_DIR}/plan.md`;
|
|
21410
21477
|
PLAN_FILE_NAME = "plan.md";
|
|
21411
21478
|
LEGACY_PLAN_FILE_REL = ".sideboard/plan.md";
|
|
21479
|
+
PRESENT_PLAN_TOOL_DESCRIPTION = "Only when Sideboard Plan mode is on (composer Plan chip). Saves the implementation plan to .context/attachments/plan.md and shows Approve / Hand off / Copy in chat. Pass the full plan body in content. Then Claude should call ExitPlanMode. If Plan mode is off, do not call this \u2014 write the plan in chat.";
|
|
21480
|
+
PRESENT_PLAN_REQUIRES_PLAN_MODE = "Plan mode is off. Do not call present_plan or ExitPlanMode. Write the plan in chat. The user can turn Plan on in the composer.";
|
|
21412
21481
|
}
|
|
21413
21482
|
});
|
|
21414
21483
|
|
|
@@ -21418,11 +21487,15 @@ __export(plan_file_exports, {
|
|
|
21418
21487
|
LEGACY_PLAN_FILE_REL: () => LEGACY_PLAN_FILE_REL,
|
|
21419
21488
|
PLAN_FILE_NAME: () => PLAN_FILE_NAME,
|
|
21420
21489
|
PLAN_FILE_REL: () => PLAN_FILE_REL,
|
|
21490
|
+
PRESENT_PLAN_REQUIRES_PLAN_MODE: () => PRESENT_PLAN_REQUIRES_PLAN_MODE,
|
|
21491
|
+
PRESENT_PLAN_TOOL_DESCRIPTION: () => PRESENT_PLAN_TOOL_DESCRIPTION,
|
|
21421
21492
|
extractPresentedPlan: () => extractPresentedPlan,
|
|
21422
21493
|
isPresentPlanToolName: () => isPresentPlanToolName,
|
|
21423
21494
|
planFileAbs: () => planFileAbs,
|
|
21424
21495
|
readPlanFile: () => readPlanFile,
|
|
21425
21496
|
resolvePlanMarkdown: () => resolvePlanMarkdown,
|
|
21497
|
+
shouldRegisterPresentPlan: () => shouldRegisterPresentPlan,
|
|
21498
|
+
threadMayPresentPlan: () => threadMayPresentPlan,
|
|
21426
21499
|
writePlanFile: () => writePlanFile
|
|
21427
21500
|
});
|
|
21428
21501
|
function ensureAttachmentsGitignore(worktreePath) {
|
|
@@ -23090,7 +23163,8 @@ var init_orchestrator = __esm({
|
|
|
23090
23163
|
if (compact.didCompact) {
|
|
23091
23164
|
thread = updateThread(threadId, {
|
|
23092
23165
|
messages: compact.thread.messages,
|
|
23093
|
-
sessionId: compact.thread.sessionId
|
|
23166
|
+
sessionId: compact.thread.sessionId,
|
|
23167
|
+
compactedMessages: compact.thread.compactedMessages
|
|
23094
23168
|
});
|
|
23095
23169
|
this.emit({
|
|
23096
23170
|
type: "context_compacted",
|
|
@@ -23104,9 +23178,6 @@ var init_orchestrator = __esm({
|
|
|
23104
23178
|
this.emit({ type: "error", threadId, message: `context compact failed: ${message}` });
|
|
23105
23179
|
thread = this.requireThread(threadId);
|
|
23106
23180
|
}
|
|
23107
|
-
if (this.stoppedTurns.has(threadId)) {
|
|
23108
|
-
return;
|
|
23109
|
-
}
|
|
23110
23181
|
const promptText = typeof prompt === "string" ? prompt : "";
|
|
23111
23182
|
const autoContinue = isJobContinuePrompt(promptText) || promptText.startsWith("The previous agent process ended before it finished.");
|
|
23112
23183
|
const parked = takePendingTurnAttachments(thread);
|
|
@@ -23123,6 +23194,14 @@ var init_orchestrator = __esm({
|
|
|
23123
23194
|
agent: thread.agent,
|
|
23124
23195
|
orch: isOrchestratorThread(thread)
|
|
23125
23196
|
});
|
|
23197
|
+
if (this.stoppedTurns.has(threadId)) {
|
|
23198
|
+
const stopped = writeLiveStatus(threadId, "stopped");
|
|
23199
|
+
if (stopped?.status === "stopped") {
|
|
23200
|
+
this.emit({ type: "status_changed", threadId, status: "stopped" });
|
|
23201
|
+
}
|
|
23202
|
+
this.emit({ type: "turn_finished", threadId, exitCode: 1 });
|
|
23203
|
+
return;
|
|
23204
|
+
}
|
|
23126
23205
|
thread = this.requireThread(threadId);
|
|
23127
23206
|
const gitPrompt = expandCanonicalGitRequest(promptText);
|
|
23128
23207
|
const { agentPrompt: expandedPrompt } = expandComposerPrompt(
|
|
@@ -23181,7 +23260,7 @@ var init_orchestrator = __esm({
|
|
|
23181
23260
|
].filter(Boolean).join("\n\n") || void 0;
|
|
23182
23261
|
const prGateDirective = thread.agent !== "brightsy" && !isOrchestratorThread(thread) && mentionsPrGoal(promptText) ? formatPrGateDirective() : null;
|
|
23183
23262
|
const agentPrompt = [
|
|
23184
|
-
thread.planMode
|
|
23263
|
+
planModeTurnInstruction(thread.planMode),
|
|
23185
23264
|
orchestrationReminder,
|
|
23186
23265
|
prGateDirective,
|
|
23187
23266
|
injectedNoticeContext,
|
|
@@ -26609,6 +26688,7 @@ function mcpArchiveBlockedReason(thread) {
|
|
|
26609
26688
|
|
|
26610
26689
|
// src/mcp/server.ts
|
|
26611
26690
|
init_profile();
|
|
26691
|
+
init_plan_present();
|
|
26612
26692
|
|
|
26613
26693
|
// src/mcp/run-script-ref.ts
|
|
26614
26694
|
init_chat_tabs();
|
|
@@ -29539,31 +29619,40 @@ async function startMcpServer() {
|
|
|
29539
29619
|
return mcpJson(payload);
|
|
29540
29620
|
}
|
|
29541
29621
|
);
|
|
29542
|
-
|
|
29543
|
-
|
|
29544
|
-
|
|
29545
|
-
|
|
29546
|
-
|
|
29547
|
-
|
|
29548
|
-
|
|
29549
|
-
|
|
29550
|
-
|
|
29551
|
-
|
|
29552
|
-
|
|
29553
|
-
|
|
29554
|
-
const
|
|
29622
|
+
const callingThreadId = process.env[SIDEBOARD_THREAD_ID_ENV]?.trim() || "";
|
|
29623
|
+
const callingThread = callingThreadId ? orch.getThread(callingThreadId) : null;
|
|
29624
|
+
if (shouldRegisterPresentPlan(callingThread)) {
|
|
29625
|
+
server.tool(
|
|
29626
|
+
"present_plan",
|
|
29627
|
+
PRESENT_PLAN_TOOL_DESCRIPTION,
|
|
29628
|
+
{
|
|
29629
|
+
title: import_zod9.z.string().optional().describe("Short plan title (defaults to Plan)"),
|
|
29630
|
+
content: import_zod9.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
|
|
29631
|
+
thread_id: import_zod9.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
|
|
29632
|
+
},
|
|
29633
|
+
async ({ title, content, thread_id }) => {
|
|
29634
|
+
const { writePlanFile: writePlanFile2 } = await Promise.resolve().then(() => (init_plan_file(), plan_file_exports));
|
|
29635
|
+
const ref = thread_id?.trim() || callingThreadId;
|
|
29636
|
+
const t = ref ? orch.getThread(ref) : null;
|
|
29637
|
+
if (t && !threadMayPresentPlan(t)) {
|
|
29638
|
+
return mcpJson(
|
|
29639
|
+
{ ok: false, error: PRESENT_PLAN_REQUIRES_PLAN_MODE },
|
|
29640
|
+
true
|
|
29641
|
+
);
|
|
29642
|
+
}
|
|
29643
|
+
let root = process.cwd();
|
|
29555
29644
|
if (t?.worktreePath?.trim()) root = t.worktreePath;
|
|
29645
|
+
const path = writePlanFile2(root, content);
|
|
29646
|
+
const payload = {
|
|
29647
|
+
ok: true,
|
|
29648
|
+
path,
|
|
29649
|
+
title: title?.trim() || "Plan",
|
|
29650
|
+
message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
|
|
29651
|
+
};
|
|
29652
|
+
return mcpJson(payload);
|
|
29556
29653
|
}
|
|
29557
|
-
|
|
29558
|
-
|
|
29559
|
-
ok: true,
|
|
29560
|
-
path,
|
|
29561
|
-
title: title?.trim() || "Plan",
|
|
29562
|
-
message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
|
|
29563
|
-
};
|
|
29564
|
-
return mcpJson(payload);
|
|
29565
|
-
}
|
|
29566
|
-
);
|
|
29654
|
+
);
|
|
29655
|
+
}
|
|
29567
29656
|
server.tool(
|
|
29568
29657
|
"present_schema",
|
|
29569
29658
|
"Open Sideboard\u2019s schema-driven side column (filterable table and/or form) when the user needs to filter, edit, publish, or persist records. Do not call this just to re-display rows you already wrote as a markdown table. If the user asks for an editable / interactive table, call this even if chat already showed those rows. Pass JSON Schema + optional schemaUi. Prefer datasource=inline with embedded resource/records. Use datasource=brightsy with resource_id only when the user is logged into Brightsy.",
|