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