@sideboard-ai/core 0.1.107 → 0.1.109
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/agents/cursor-runner.js +2 -2
- package/dist/{agents-T4RHL5UX.js → agents-XB332VUJ.js} +5 -5
- package/dist/{agents-4XQANGFO.js → agents-YIMHDI46.js} +7 -7
- package/dist/{app-settings-2WH6DVOW.js → app-settings-EBCVMFF6.js} +1 -1
- package/dist/{app-settings-6AXBWM3K.js → app-settings-IYSSVZHH.js} +2 -2
- package/dist/{chunk-7ZTQHC2Q.js → chunk-4LKD3PGO.js} +1 -1
- package/dist/{chunk-TLPJHLLM.js → chunk-4TR3HZFT.js} +1 -1
- package/dist/{chunk-SMWVSSE5.js → chunk-756EO4GK.js} +3 -3
- package/dist/{chunk-XOYH3OMI.js → chunk-AY2EQI2P.js} +16 -13
- package/dist/{chunk-PZVA2VDF.js → chunk-EW7COXYE.js} +1 -1
- package/dist/{chunk-VTTTV2LM.js → chunk-FBV6NK62.js} +2 -2
- package/dist/{chunk-GYFNFU62.js → chunk-I2OJ2YSP.js} +8 -16
- package/dist/{chunk-6JJTMI7G.js → chunk-JVIW55KT.js} +1 -1
- package/dist/{chunk-6HRR4T4P.js → chunk-KKHDPV45.js} +15 -14
- package/dist/{chunk-XEFHG6VG.js → chunk-LTPX5N6K.js} +1310 -1265
- package/dist/{chunk-EX4NZN3O.js → chunk-NE4TOOKS.js} +1 -1
- package/dist/{chunk-LRO7YVLJ.js → chunk-OQDIYVDD.js} +2 -2
- package/dist/{chunk-3SIFRHJ3.js → chunk-Q6B3NRCT.js} +1457 -1431
- package/dist/{chunk-THXSFETX.js → chunk-TROJ3PVH.js} +3 -3
- package/dist/{chunk-5XUKY2JY.js → chunk-VRTKGKUW.js} +8 -16
- package/dist/{chunk-HUKCGRAT.js → chunk-WD2TARQS.js} +7 -2
- package/dist/{coordinator-prompt-4TUQUEHY.js → coordinator-prompt-JKOU6BWV.js} +4 -4
- package/dist/{coordinator-prompt-YQWRXCPX.js → coordinator-prompt-V66BYTUV.js} +3 -3
- package/dist/{global-workspace-V6TMECDU.js → global-workspace-NCTBE7G7.js} +5 -5
- package/dist/{global-workspace-H37LFFXI.js → global-workspace-WBQWQQQY.js} +4 -4
- package/dist/index.cjs +1697 -1661
- package/dist/index.d.cts +65 -24
- package/dist/index.d.ts +65 -24
- package/dist/index.js +107 -85
- package/dist/mcp/run-stdio.cjs +1660 -1619
- package/dist/mcp/run-stdio.js +82 -79
- package/dist/{orchestrator-DEHSKERK.js → orchestrator-6W7EKSPO.js} +9 -9
- package/dist/{orchestrator-2P7DX44Q.js → orchestrator-JVGVPKLI.js} +7 -7
- package/dist/{workspaces-JU3P6FHB.js → workspaces-I3554R3F.js} +5 -5
- package/dist/{workspaces-UPEVANU4.js → workspaces-SUBEDSGB.js} +6 -6
- package/dist/{worktree-NR5YJG2E.js → worktree-2UWO7VEK.js} +3 -3
- package/dist/{worktree-6MFKE465.js → worktree-7H6HB6GW.js} +2 -2
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -555,7 +555,7 @@ function isElectronLikeCommand(command) {
|
|
|
555
555
|
const name = command.trim();
|
|
556
556
|
if (!name) return false;
|
|
557
557
|
if (process.versions.electron && name === process.execPath) return true;
|
|
558
|
-
return /(?:^|[/\\])Electron(?:\.exe)?$/i.test(name) || /
|
|
558
|
+
return /(?:^|[/\\])Electron(?:\.exe)?$/i.test(name) || /(?:^|[/\\])Sideboard(?:\.exe)?$/i.test(name) || /(?:Sideboard|Electron)\.app[/\\]Contents[/\\]MacOS[/\\]/i.test(name);
|
|
559
559
|
}
|
|
560
560
|
function unwrapStrippedElectronLaunch(command, args) {
|
|
561
561
|
if (!isStrippedElectronLaunch(command, args) || !args || args.length < 4) {
|
|
@@ -5496,19 +5496,11 @@ function coordinatorTurnReminder(opts) {
|
|
|
5496
5496
|
const goal = opts.goal?.trim();
|
|
5497
5497
|
return [
|
|
5498
5498
|
"Sideboard Orchestration (mandatory):",
|
|
5499
|
-
"- You oversee
|
|
5500
|
-
|
|
5501
|
-
"- Registered workspaces / child threads are the fleet you manage via Sideboard MCP.",
|
|
5502
|
-
`- YOUR orchestration thread id is ${opts.parentId}`,
|
|
5503
|
-
`- Always pass parentThreadId="${opts.parentId}" on create_thread (never invent or reuse another uuid).`,
|
|
5504
|
-
`- Or omit parentThreadId \u2014 Sideboard MCP already binds children to ${opts.parentId}.`,
|
|
5499
|
+
"- You oversee worktree agents from a synthetic empty cwd (not a git repo). Follow AGENTS.md / CLAUDE.md.",
|
|
5500
|
+
`- YOUR orchestration thread id is ${opts.parentId} \u2014 pass parentThreadId="${opts.parentId}" on create_thread, or omit it.`,
|
|
5505
5501
|
goal ? `- Goal / title: ${goal}` : null,
|
|
5506
5502
|
accountDefaultsPlaybookLine(),
|
|
5507
|
-
|
|
5508
|
-
"- Existing repo: create_thread on a repoPath \u2192 send_to_thread \u2192 wait_for_turn. If stillRunning, the child is working (progress is tools/thinking) \u2014 wait_for_turn again; do not ping it. If status is error, lastError/text is the failure \u2014 adapt (switch agent, tell the user). Commit/push/draft PR with ask_git on the child, then wait_for_turn \u2014 never git/gh from this cwd. Call ask_git merge only if the user explicitly asked to merge.",
|
|
5509
|
-
"- New repo: Bash (clone or gh repo create under ~/sideboard/repos/<name>) \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft.",
|
|
5510
|
-
"- When naming threads for the user, link them as `[Title](sideboard://thread/<id>)`.",
|
|
5511
|
-
"- If they will wait on Slack, leave the Mac, or rely on overnight schedules, call set_caffeinate enabled=true (or they can enable Settings \u2192 Advanced \u2192 Caffeinate while schedules are enabled). When they say they are done / wrapping up / going to sleep, call set_caffeinate enabled=false. Closing this chat also turns it off."
|
|
5503
|
+
"- Status: list_threads. Link chats as `[Title](sideboard://thread/<id>)`. Merge only if the user asked."
|
|
5512
5504
|
].filter(Boolean).join("\n");
|
|
5513
5505
|
}
|
|
5514
5506
|
function ensureGlobalCoordinatorCwd(opts) {
|
|
@@ -5637,14 +5629,14 @@ var init_coordinator_prompt = __esm({
|
|
|
5637
5629
|
"- list_run_scripts / run_dev_script / stop_dev_script \u2014 start/stop named run scripts",
|
|
5638
5630
|
"Inspect / review / PRs:",
|
|
5639
5631
|
"- get_diff \u2014 compact diff summary",
|
|
5640
|
-
'- request_review \u2014 open a Review chat tab on a worktree thread (attaches .
|
|
5632
|
+
'- request_review \u2014 open a Review chat tab on a worktree thread (attaches .claude/skills/review/SKILL.md when present, else seeds that skill / local guidelines; sends "Review changes in this workspace."); then wait_for_turn (loop while stillRunning) / get_turn_result on the returned id',
|
|
5641
5633
|
"- ask_git \u2014 commit & push, open a draft PR, resolve conflicts, or merge. When the worktree is clean, Sideboard pushes / opens the PR itself. When dirty, it queues the worktree agent \u2014 then wait_for_turn (loop while stillRunning). Prefer this over paraphrasing.",
|
|
5642
5634
|
'- 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.',
|
|
5643
5635
|
'- Or send_to_thread with those exact phrases: "Commit and push.", "Commit, push, and open a draft PR.", "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.',
|
|
5644
5636
|
"Process guides:",
|
|
5645
5637
|
"- 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.",
|
|
5646
|
-
"- Recurring shapes: have the worktree agent write `.claude/skills/<kebab-name>/SKILL.md` (commit it) so later threads and native Claude Code / attach see it. Do not use `.sideboard/skills
|
|
5647
|
-
"- One-offs: no guide. Same miss across threads: edit the skill or `.
|
|
5638
|
+
"- Recurring shapes: have the worktree agent write `.claude/skills/<kebab-name>/SKILL.md` (commit it) so later threads and native Claude Code / attach see it. Merge-review sentences go in `.claude/skills/review/SKILL.md` (create it \u2014 that is allowed). Do not use `.sideboard/skills/` (that folder only). Codex/OpenCode: one line in AGENTS.md pointing at that file.",
|
|
5639
|
+
"- One-offs: no guide. Same miss across threads: edit the skill or `.claude/skills/review/SKILL.md`, then rerun the batch \u2014 do not patch three threads.",
|
|
5648
5640
|
"Human-only (do not attempt): ready-for-review land (confirm_land), purge_thread.",
|
|
5649
5641
|
"Thread links in replies: when mentioning a chat/thread for the user, include a markdown link `[Title](sideboard://thread/<id>)` using the full id (or the link field from create_thread / list_threads). Sideboard renders these as clickable opens.",
|
|
5650
5642
|
"Bash / Read / etc: allowed for (1) inspecting target worktrees / registered repo paths from MCP, and (2) greenfield setup under ~/sideboard/repos (git clone, gh repo create, git init+remote). Never git init/clone *inside* this synthetic home cwd \u2014 emptiness here is expected, not a bug."
|
|
@@ -6582,6 +6574,8 @@ function summarizeTurnStderr(tail, maxChars = 500) {
|
|
|
6582
6574
|
if (tail.length === 0) return "";
|
|
6583
6575
|
const cursorStartup = [...tail].reverse().find((line) => /cursor startup failed:/i.test(line));
|
|
6584
6576
|
if (cursorStartup) return clipStderr(cursorStartup, maxChars);
|
|
6577
|
+
const cursorRunFailed = [...tail].reverse().find((line) => /^cursor run failed\b/i.test(line.trim()));
|
|
6578
|
+
if (cursorRunFailed) return clipStderr(cursorRunFailed, maxChars);
|
|
6585
6579
|
if (tail.some(looksLikeNestedElectronCrash)) return NESTED_ELECTRON_SUMMARY;
|
|
6586
6580
|
if (tail.some(looksLikeHomebrewLibuvCrash)) return HOMEBREW_LIBUV_SUMMARY;
|
|
6587
6581
|
if (tail.some(
|
|
@@ -6612,7 +6606,7 @@ function looksLikeRetryableRunnerCrash(text4) {
|
|
|
6612
6606
|
const lower = text4.trim().toLowerCase();
|
|
6613
6607
|
if (/cannot find (?:package|module)|err_module_not_found/.test(lower)) return false;
|
|
6614
6608
|
if (!lower) return true;
|
|
6615
|
-
return /uv_run|spineventloopinternal|libuv|homebrew node \+ shared libuv|hascustomhostobject|electroninitializeicuandstartnode|nested chromium|truncated crash dump|sig(?:segv|abrt|ill)|segmentation fault|illegal instruction|fatal error/.test(
|
|
6609
|
+
return /uv_run|spineventloopinternal|libuv|homebrew node \+ shared libuv|hascustomhostobject|electroninitializeicuandstartnode|nested chromium|truncated crash dump|connection stalled|sig(?:segv|abrt|ill)|segmentation fault|illegal instruction|fatal error/.test(
|
|
6616
6610
|
lower
|
|
6617
6611
|
);
|
|
6618
6612
|
}
|
|
@@ -6670,6 +6664,9 @@ function humanizeAgentFailDetail(detail) {
|
|
|
6670
6664
|
if (/corrupt local agent checkpoint|missing root blob|truncated crash dump/.test(lower)) {
|
|
6671
6665
|
return `${raw} \u2014 retry the turn (Sideboard will start a fresh Cursor session).`;
|
|
6672
6666
|
}
|
|
6667
|
+
if (/connection stalled/.test(lower)) {
|
|
6668
|
+
return /retry the turn/i.test(raw) ? raw : `${raw} \u2014 retry the turn (Sideboard will start a fresh Cursor session).`;
|
|
6669
|
+
}
|
|
6673
6670
|
return raw;
|
|
6674
6671
|
}
|
|
6675
6672
|
function turnFailChatText(opts) {
|
|
@@ -7538,11 +7535,18 @@ var init_claude_mcp = __esm({
|
|
|
7538
7535
|
function sideboardMcpProfile(env = process.env) {
|
|
7539
7536
|
return env[SIDEBOARD_MCP_PROFILE_ENV]?.trim().toLowerCase() === "worktree" ? "worktree" : "orchestration";
|
|
7540
7537
|
}
|
|
7541
|
-
var SIDEBOARD_MCP_PROFILE_ENV;
|
|
7538
|
+
var SIDEBOARD_MCP_PROFILE_ENV, WORKTREE_MCP_TOOLS;
|
|
7542
7539
|
var init_profile = __esm({
|
|
7543
7540
|
"src/mcp/profile.ts"() {
|
|
7544
7541
|
"use strict";
|
|
7545
7542
|
SIDEBOARD_MCP_PROFILE_ENV = "SIDEBOARD_MCP_PROFILE";
|
|
7543
|
+
WORKTREE_MCP_TOOLS = [
|
|
7544
|
+
"present_artifact",
|
|
7545
|
+
"ask_user",
|
|
7546
|
+
"present_plan",
|
|
7547
|
+
"present_schema",
|
|
7548
|
+
"present_files"
|
|
7549
|
+
];
|
|
7546
7550
|
}
|
|
7547
7551
|
});
|
|
7548
7552
|
|
|
@@ -8063,14 +8067,7 @@ var init_injected_mcp = __esm({
|
|
|
8063
8067
|
"mcp__sideboard__present_schema",
|
|
8064
8068
|
"mcp__sideboard__present_files",
|
|
8065
8069
|
"mcp__sideboard__ask_user",
|
|
8066
|
-
"mcp__sideboard__present_plan"
|
|
8067
|
-
"mcp__sideboard__list_teams",
|
|
8068
|
-
"mcp__sideboard__slack_list_channels",
|
|
8069
|
-
"mcp__sideboard__slack_list_users",
|
|
8070
|
-
"mcp__sideboard__slack_search",
|
|
8071
|
-
"mcp__sideboard__slack_read",
|
|
8072
|
-
"mcp__sideboard__slack_post",
|
|
8073
|
-
"mcp__sideboard__slack_replies"
|
|
8070
|
+
"mcp__sideboard__present_plan"
|
|
8074
8071
|
];
|
|
8075
8072
|
BRIGHTSY_MCP_ALLOWED_TOOLS = [
|
|
8076
8073
|
"mcp__brightsy",
|
|
@@ -10358,6 +10355,14 @@ var init_cursor_stream_coalesce = __esm({
|
|
|
10358
10355
|
});
|
|
10359
10356
|
|
|
10360
10357
|
// src/agents/spawn.ts
|
|
10358
|
+
function applyPromptCacheTtlEnv(agent, env) {
|
|
10359
|
+
if (agent === "claude" && env.FORCE_PROMPT_CACHING_5M !== "1") {
|
|
10360
|
+
env.ENABLE_PROMPT_CACHING_1H ??= "1";
|
|
10361
|
+
}
|
|
10362
|
+
if (agent === "opencode" && env.OPENCODE_ANTHROPIC_FORCE_PROMPT_CACHING_5M !== "1") {
|
|
10363
|
+
env.OPENCODE_ANTHROPIC_PROMPT_CACHING_1H ??= "1";
|
|
10364
|
+
}
|
|
10365
|
+
}
|
|
10361
10366
|
async function spawnAgentTurn(thread, input, onEvent) {
|
|
10362
10367
|
ensureAgentPath();
|
|
10363
10368
|
if (!thread.worktreePath?.trim()) {
|
|
@@ -10383,6 +10388,7 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
10383
10388
|
);
|
|
10384
10389
|
}
|
|
10385
10390
|
const env = childEnvWithAppSettings(cmd.env);
|
|
10391
|
+
applyPromptCacheTtlEnv(thread.agent, env);
|
|
10386
10392
|
try {
|
|
10387
10393
|
if (isOrchestratorThread(thread)) {
|
|
10388
10394
|
mergeAgentGitAuthEnv(env, await resolveAgentGitAuthEnv(env));
|
|
@@ -10628,1707 +10634,1722 @@ var init_pr_merge_archive = __esm({
|
|
|
10628
10634
|
}
|
|
10629
10635
|
});
|
|
10630
10636
|
|
|
10631
|
-
// src/
|
|
10632
|
-
function
|
|
10633
|
-
const
|
|
10634
|
-
|
|
10635
|
-
|
|
10636
|
-
|
|
10637
|
-
|
|
10638
|
-
|
|
10639
|
-
return (0, import_node_fs29.readFileSync)(path2, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
10640
|
-
}
|
|
10641
|
-
function resolveFilesToCopy(repoPath) {
|
|
10642
|
-
const fromInclude = readWorktreeInclude(repoPath);
|
|
10643
|
-
if (fromInclude.length) return fromInclude;
|
|
10644
|
-
const settings = loadRepoSettings(repoPath);
|
|
10645
|
-
if (settings?.filesToCopy?.length) return settings.filesToCopy;
|
|
10646
|
-
if (settings?.fileIncludeGlobs?.length) {
|
|
10647
|
-
const matched = [];
|
|
10648
|
-
try {
|
|
10649
|
-
for (const entry of (0, import_node_fs29.readdirSync)(repoPath, { withFileTypes: true })) {
|
|
10650
|
-
if (!entry.isFile()) continue;
|
|
10651
|
-
for (const glob of settings.fileIncludeGlobs) {
|
|
10652
|
-
if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob((0, import_node_path28.basename)(glob), entry.name)) {
|
|
10653
|
-
matched.push(entry.name);
|
|
10654
|
-
break;
|
|
10655
|
-
}
|
|
10656
|
-
}
|
|
10657
|
-
}
|
|
10658
|
-
} catch {
|
|
10659
|
-
}
|
|
10660
|
-
if (matched.length) return [...new Set(matched)];
|
|
10661
|
-
}
|
|
10662
|
-
const defaults = [];
|
|
10663
|
-
try {
|
|
10664
|
-
for (const entry of (0, import_node_fs29.readdirSync)(repoPath, { withFileTypes: true })) {
|
|
10665
|
-
if (entry.isFile() && entry.name.startsWith(".env")) {
|
|
10666
|
-
defaults.push(entry.name);
|
|
10667
|
-
}
|
|
10668
|
-
}
|
|
10669
|
-
} catch {
|
|
10670
|
-
}
|
|
10671
|
-
if (defaults.length) return defaults;
|
|
10672
|
-
return [".env.local", ".env"];
|
|
10673
|
-
}
|
|
10674
|
-
function copyConfiguredFiles(repoPath, worktreePath) {
|
|
10675
|
-
const patterns = resolveFilesToCopy(repoPath);
|
|
10676
|
-
const copied = [];
|
|
10677
|
-
for (const rel of patterns) {
|
|
10678
|
-
const src = (0, import_node_path28.join)(repoPath, rel);
|
|
10679
|
-
if (!(0, import_node_fs29.existsSync)(src)) continue;
|
|
10680
|
-
const dest = (0, import_node_path28.join)(worktreePath, rel);
|
|
10681
|
-
(0, import_node_fs29.mkdirSync)((0, import_node_path28.dirname)(dest), { recursive: true });
|
|
10682
|
-
(0, import_node_fs29.copyFileSync)(src, dest);
|
|
10683
|
-
copied.push(rel);
|
|
10637
|
+
// src/composer/summarize.ts
|
|
10638
|
+
async function summarizeConversation(transcript, opts) {
|
|
10639
|
+
const clipped = transcript.length > MAX_TRANSCRIPT_CHARS ? `${transcript.slice(0, MAX_TRANSCRIPT_CHARS)}
|
|
10640
|
+
|
|
10641
|
+
[\u2026truncated\u2026]` : transcript;
|
|
10642
|
+
const claudeSummary = await tryClaudeSummary(clipped, opts);
|
|
10643
|
+
if (claudeSummary) {
|
|
10644
|
+
return { summary: clipSummary(claudeSummary), method: "claude" };
|
|
10684
10645
|
}
|
|
10685
|
-
return
|
|
10646
|
+
return { summary: clipSummary(extractiveSummary(clipped)), method: "extractive" };
|
|
10686
10647
|
}
|
|
10687
|
-
async function
|
|
10688
|
-
|
|
10689
|
-
const
|
|
10648
|
+
async function tryClaudeSummary(transcript, opts) {
|
|
10649
|
+
ensureAgentPath();
|
|
10650
|
+
const prompt = [
|
|
10651
|
+
"Summarize this coding-agent conversation for continuity in a fresh session.",
|
|
10652
|
+
"Use concise markdown bullets covering:",
|
|
10653
|
+
"- Goal / request",
|
|
10654
|
+
"- Key decisions and constraints",
|
|
10655
|
+
"- Files / areas touched",
|
|
10656
|
+
"- Current state and open tasks",
|
|
10657
|
+
"- Anything the next turn must remember",
|
|
10658
|
+
"Do not invent details. No preamble.",
|
|
10659
|
+
"",
|
|
10660
|
+
"Transcript:",
|
|
10661
|
+
transcript
|
|
10662
|
+
].join("\n");
|
|
10690
10663
|
try {
|
|
10691
|
-
const
|
|
10692
|
-
|
|
10693
|
-
|
|
10694
|
-
|
|
10695
|
-
|
|
10696
|
-
|
|
10697
|
-
|
|
10698
|
-
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
|
|
10703
|
-
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
|
|
10664
|
+
const { stdout, exitCode } = await run(
|
|
10665
|
+
"claude",
|
|
10666
|
+
[
|
|
10667
|
+
"-p",
|
|
10668
|
+
prompt,
|
|
10669
|
+
"--output-format",
|
|
10670
|
+
"text",
|
|
10671
|
+
"--permission-mode",
|
|
10672
|
+
"bypassPermissions",
|
|
10673
|
+
"--model",
|
|
10674
|
+
"haiku"
|
|
10675
|
+
],
|
|
10676
|
+
{ cwd: opts?.cwd, reject: false }
|
|
10677
|
+
);
|
|
10678
|
+
if (exitCode !== 0) return null;
|
|
10679
|
+
const text4 = stdout.trim();
|
|
10680
|
+
if (text4.length < 40) return null;
|
|
10681
|
+
return text4;
|
|
10708
10682
|
} catch {
|
|
10683
|
+
return null;
|
|
10709
10684
|
}
|
|
10710
|
-
cachedLoginEnv = { ...process.env };
|
|
10711
|
-
return { ...cachedLoginEnv };
|
|
10712
10685
|
}
|
|
10713
|
-
function
|
|
10714
|
-
const
|
|
10715
|
-
|
|
10716
|
-
|
|
10717
|
-
const
|
|
10718
|
-
|
|
10719
|
-
const
|
|
10720
|
-
|
|
10721
|
-
|
|
10722
|
-
|
|
10723
|
-
|
|
10724
|
-
|
|
10725
|
-
|
|
10726
|
-
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
|
|
10730
|
-
|
|
10731
|
-
|
|
10732
|
-
|
|
10733
|
-
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
env[`SIDEBOARD_PORT_${i}`] = String(p);
|
|
10737
|
-
env[`CONDUCTOR_PORT_${i}`] = String(p);
|
|
10686
|
+
function extractiveSummary(transcript) {
|
|
10687
|
+
const lines = transcript.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
10688
|
+
const users = [];
|
|
10689
|
+
const tools = [];
|
|
10690
|
+
const agents = [];
|
|
10691
|
+
let mode = "none";
|
|
10692
|
+
for (const line of lines) {
|
|
10693
|
+
if (line.startsWith("### User")) {
|
|
10694
|
+
mode = "user";
|
|
10695
|
+
continue;
|
|
10696
|
+
}
|
|
10697
|
+
if (line.startsWith("### Agent") || line.startsWith("## Prior")) {
|
|
10698
|
+
mode = "agent";
|
|
10699
|
+
continue;
|
|
10700
|
+
}
|
|
10701
|
+
if (line.startsWith("- ") && line.includes(":")) {
|
|
10702
|
+
tools.push(line.replace(/^- /, ""));
|
|
10703
|
+
continue;
|
|
10704
|
+
}
|
|
10705
|
+
if (mode === "user" && users.length < 8) {
|
|
10706
|
+
users.push(line.slice(0, 240));
|
|
10707
|
+
} else if (mode === "agent" && agents.length < 6) {
|
|
10708
|
+
agents.push(line.slice(0, 240));
|
|
10738
10709
|
}
|
|
10739
10710
|
}
|
|
10740
|
-
|
|
10711
|
+
const uniqueTools = [...new Set(tools)].slice(0, 20);
|
|
10712
|
+
const parts = [
|
|
10713
|
+
"## Compacted context (extractive)",
|
|
10714
|
+
"",
|
|
10715
|
+
"### User goals",
|
|
10716
|
+
...users.length ? users.map((u) => `- ${u}`) : ["- (none captured)"],
|
|
10717
|
+
"",
|
|
10718
|
+
"### Recent agent notes",
|
|
10719
|
+
...agents.length ? agents.map((a) => `- ${a}`) : ["- (none captured)"]
|
|
10720
|
+
];
|
|
10721
|
+
if (uniqueTools.length) {
|
|
10722
|
+
parts.push("", "### Tools / files", ...uniqueTools.map((t) => `- ${t}`));
|
|
10723
|
+
}
|
|
10724
|
+
parts.push(
|
|
10725
|
+
"",
|
|
10726
|
+
"_Automatic extractive summary \u2014 Claude summarizer was unavailable._"
|
|
10727
|
+
);
|
|
10728
|
+
return parts.join("\n");
|
|
10741
10729
|
}
|
|
10742
|
-
function
|
|
10743
|
-
if (
|
|
10744
|
-
|
|
10745
|
-
|
|
10730
|
+
function clipSummary(text4) {
|
|
10731
|
+
if (text4.length <= MAX_SUMMARY_CHARS) return text4;
|
|
10732
|
+
return `${text4.slice(0, MAX_SUMMARY_CHARS)}
|
|
10733
|
+
|
|
10734
|
+
[\u2026summary truncated\u2026]`;
|
|
10746
10735
|
}
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10755
|
-
setTimeout(() => {
|
|
10756
|
-
try {
|
|
10757
|
-
process.kill(-pid, "SIGKILL");
|
|
10758
|
-
} catch {
|
|
10759
|
-
}
|
|
10760
|
-
}, 2500).unref?.();
|
|
10761
|
-
}
|
|
10762
|
-
} catch {
|
|
10763
|
-
try {
|
|
10764
|
-
child.kill("SIGTERM");
|
|
10765
|
-
} catch {
|
|
10766
|
-
}
|
|
10767
|
-
}
|
|
10736
|
+
var MAX_TRANSCRIPT_CHARS, MAX_SUMMARY_CHARS;
|
|
10737
|
+
var init_summarize = __esm({
|
|
10738
|
+
"src/composer/summarize.ts"() {
|
|
10739
|
+
"use strict";
|
|
10740
|
+
init_run();
|
|
10741
|
+
init_path();
|
|
10742
|
+
MAX_TRANSCRIPT_CHARS = 12e4;
|
|
10743
|
+
MAX_SUMMARY_CHARS = 6e3;
|
|
10768
10744
|
}
|
|
10769
|
-
|
|
10770
|
-
|
|
10771
|
-
|
|
10772
|
-
|
|
10773
|
-
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
|
|
10777
|
-
|
|
10778
|
-
|
|
10779
|
-
|
|
10780
|
-
|
|
10781
|
-
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
|
|
10785
|
-
|
|
10786
|
-
);
|
|
10745
|
+
});
|
|
10746
|
+
|
|
10747
|
+
// src/composer/context-compact.ts
|
|
10748
|
+
function estimateMessageChars(message) {
|
|
10749
|
+
let n = message.text.length + 16;
|
|
10750
|
+
for (const part of message.parts ?? []) {
|
|
10751
|
+
n += estimatePartChars(part);
|
|
10752
|
+
}
|
|
10753
|
+
return n;
|
|
10754
|
+
}
|
|
10755
|
+
function estimatePartChars(part) {
|
|
10756
|
+
switch (part.type) {
|
|
10757
|
+
case "text":
|
|
10758
|
+
case "thinking":
|
|
10759
|
+
return part.text.length;
|
|
10760
|
+
case "tool": {
|
|
10761
|
+
const input = part.input ? JSON.stringify(part.input) : "";
|
|
10762
|
+
return part.name.length + (part.description?.length ?? 0) + (part.detail?.length ?? 0) + (part.result?.length ?? 0) + input.length + 32;
|
|
10787
10763
|
}
|
|
10764
|
+
default:
|
|
10765
|
+
return 0;
|
|
10788
10766
|
}
|
|
10789
10767
|
}
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
const env = buildWorkspaceScriptEnv(
|
|
10793
|
-
{
|
|
10794
|
-
worktreePath: opts.worktreePath,
|
|
10795
|
-
repoPath: opts.repoPath,
|
|
10796
|
-
workspaceName: opts.workspaceName,
|
|
10797
|
-
defaultBranch: opts.defaultBranch,
|
|
10798
|
-
ports: opts.ports
|
|
10799
|
-
},
|
|
10800
|
-
loginEnv
|
|
10801
|
-
);
|
|
10802
|
-
try {
|
|
10803
|
-
mergeAgentGitAuthEnv(
|
|
10804
|
-
env,
|
|
10805
|
-
await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
|
|
10806
|
-
);
|
|
10807
|
-
} catch {
|
|
10808
|
-
}
|
|
10809
|
-
const shell = process.platform === "darwin" ? "zsh" : "bash";
|
|
10810
|
-
const child = (0, import_execa4.execa)(shell, ["-lc", command], {
|
|
10811
|
-
cwd: opts.worktreePath,
|
|
10812
|
-
reject: false,
|
|
10813
|
-
env,
|
|
10814
|
-
// Own process group so Stop can tear down the whole tree (not just the shell).
|
|
10815
|
-
// There is no settings.toml `stop=` / teardown hook for run scripts.
|
|
10816
|
-
...process.platform === "win32" ? {} : { detached: true }
|
|
10817
|
-
});
|
|
10818
|
-
pipeLines(child.stdout, opts.onLine);
|
|
10819
|
-
pipeLines(child.stderr, opts.onLine);
|
|
10820
|
-
const ports = opts.ports ?? [];
|
|
10821
|
-
return {
|
|
10822
|
-
pid: child.pid,
|
|
10823
|
-
kill: () => killScriptTree(child, ports),
|
|
10824
|
-
done: child.then((r) => r.exitCode ?? null),
|
|
10825
|
-
child
|
|
10826
|
-
};
|
|
10827
|
-
}
|
|
10828
|
-
async function attachAbort(handle, signal) {
|
|
10829
|
-
if (signal) {
|
|
10830
|
-
const onAbort = () => handle.kill();
|
|
10831
|
-
if (signal.aborted) onAbort();
|
|
10832
|
-
else signal.addEventListener("abort", onAbort, { once: true });
|
|
10833
|
-
}
|
|
10834
|
-
return handle.done;
|
|
10768
|
+
function estimateThreadChars(messages) {
|
|
10769
|
+
return messages.reduce((sum, m) => sum + estimateMessageChars(m), 0);
|
|
10835
10770
|
}
|
|
10836
|
-
|
|
10837
|
-
const
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
repoPath,
|
|
10842
|
-
defaultBranch: opts?.defaultBranch,
|
|
10843
|
-
onLine
|
|
10844
|
-
});
|
|
10845
|
-
const exitCode = await attachAbort(handle, opts?.signal);
|
|
10846
|
-
return {
|
|
10847
|
-
ran: true,
|
|
10848
|
-
exitCode,
|
|
10849
|
-
source: workspaceSettingsSourceLabel(worktreePath, repoPath),
|
|
10850
|
-
kill: handle.kill
|
|
10851
|
-
};
|
|
10771
|
+
function shouldCompactContext(messages, thresholds = {}) {
|
|
10772
|
+
const maxChars = thresholds.maxChars ?? CONTEXT_COMPACT_CHARS;
|
|
10773
|
+
const minMessages = thresholds.minMessages ?? CONTEXT_MIN_MESSAGES;
|
|
10774
|
+
if (messages.length < minMessages) return false;
|
|
10775
|
+
return estimateThreadChars(messages) >= maxChars;
|
|
10852
10776
|
}
|
|
10853
|
-
|
|
10854
|
-
const
|
|
10855
|
-
|
|
10856
|
-
|
|
10857
|
-
|
|
10858
|
-
|
|
10859
|
-
|
|
10860
|
-
|
|
10861
|
-
|
|
10862
|
-
|
|
10863
|
-
|
|
10777
|
+
function splitForCompaction(messages, thresholds = {}) {
|
|
10778
|
+
const keepChars = thresholds.keepRecentChars ?? CONTEXT_KEEP_RECENT_CHARS;
|
|
10779
|
+
const keepCount = thresholds.keepRecentMessages ?? CONTEXT_KEEP_RECENT_MESSAGES;
|
|
10780
|
+
if (messages.length === 0) return { older: [], recent: [] };
|
|
10781
|
+
let recentChars = 0;
|
|
10782
|
+
let cut = messages.length;
|
|
10783
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
10784
|
+
const next = recentChars + estimateMessageChars(messages[i]);
|
|
10785
|
+
const kept = messages.length - i;
|
|
10786
|
+
if (kept >= keepCount && next > keepChars) break;
|
|
10787
|
+
recentChars = next;
|
|
10788
|
+
cut = i;
|
|
10789
|
+
}
|
|
10790
|
+
const minRecent = Math.min(keepCount, messages.length);
|
|
10791
|
+
cut = Math.min(cut, messages.length - minRecent);
|
|
10792
|
+
if (cut <= 0) return { older: [], recent: messages };
|
|
10864
10793
|
return {
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
source: found.source,
|
|
10868
|
-
kill: handle.kill
|
|
10794
|
+
older: messages.slice(0, cut),
|
|
10795
|
+
recent: messages.slice(cut)
|
|
10869
10796
|
};
|
|
10870
10797
|
}
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
|
|
10798
|
+
function formatToolPart(t, detail) {
|
|
10799
|
+
if (detail === "summary") {
|
|
10800
|
+
const label = t.description || t.detail || t.name;
|
|
10801
|
+
const path2 = t.filePath ? ` (${t.filePath})` : "";
|
|
10802
|
+
return `- ${t.name}: ${label}${path2}`;
|
|
10875
10803
|
}
|
|
10876
|
-
|
|
10877
|
-
|
|
10804
|
+
const lines = [`#### Tool: ${t.name}`, `Status: ${t.status}`];
|
|
10805
|
+
if (t.description) lines.push(`Description: ${t.description}`);
|
|
10806
|
+
if (t.filePath) lines.push(`Path: ${t.filePath}`);
|
|
10807
|
+
if (t.input && Object.keys(t.input).length > 0) {
|
|
10808
|
+
lines.push("Input:");
|
|
10809
|
+
lines.push("```json");
|
|
10810
|
+
lines.push(JSON.stringify(t.input, null, 2));
|
|
10811
|
+
lines.push("```");
|
|
10812
|
+
} else if (t.detail) {
|
|
10813
|
+
lines.push(`Detail: ${t.detail}`);
|
|
10878
10814
|
}
|
|
10879
|
-
|
|
10815
|
+
if (t.result != null && t.result !== "") {
|
|
10816
|
+
lines.push("Result:");
|
|
10817
|
+
lines.push("```");
|
|
10818
|
+
lines.push(t.result);
|
|
10819
|
+
lines.push("```");
|
|
10820
|
+
}
|
|
10821
|
+
return lines.join("\n");
|
|
10880
10822
|
}
|
|
10881
|
-
|
|
10882
|
-
const
|
|
10883
|
-
|
|
10884
|
-
const
|
|
10885
|
-
|
|
10886
|
-
|
|
10887
|
-
|
|
10888
|
-
|
|
10889
|
-
|
|
10890
|
-
|
|
10823
|
+
function formatMessagesAsTranscript(messages, opts) {
|
|
10824
|
+
const tools = opts?.tools ?? "full";
|
|
10825
|
+
const blocks = [];
|
|
10826
|
+
for (const m of messages) {
|
|
10827
|
+
if (m.role === "summary") {
|
|
10828
|
+
blocks.push(`## Prior summary
|
|
10829
|
+
${m.text}`);
|
|
10830
|
+
continue;
|
|
10831
|
+
}
|
|
10832
|
+
if (m.role === "user") {
|
|
10833
|
+
blocks.push(`### User
|
|
10834
|
+
${m.text}`);
|
|
10835
|
+
continue;
|
|
10836
|
+
}
|
|
10837
|
+
const bits = [];
|
|
10838
|
+
if (m.text?.trim()) {
|
|
10839
|
+
bits.push(`### Agent
|
|
10840
|
+
${m.text}`);
|
|
10841
|
+
} else {
|
|
10842
|
+
bits.push("### Agent");
|
|
10843
|
+
}
|
|
10844
|
+
const thinking = (m.parts ?? []).filter((p) => p.type === "thinking");
|
|
10845
|
+
for (const th of thinking) {
|
|
10846
|
+
if (th.type !== "thinking" || !th.text.trim()) continue;
|
|
10847
|
+
bits.push("Thinking:");
|
|
10848
|
+
bits.push(th.text);
|
|
10849
|
+
}
|
|
10850
|
+
const toolParts = (m.parts ?? []).filter(
|
|
10851
|
+
(p) => p.type === "tool"
|
|
10852
|
+
);
|
|
10853
|
+
if (tools !== "none" && toolParts.length > 0) {
|
|
10854
|
+
if (tools === "summary") bits.push("Tools:");
|
|
10855
|
+
for (const t of toolParts) {
|
|
10856
|
+
bits.push(formatToolPart(t, tools));
|
|
10857
|
+
}
|
|
10858
|
+
}
|
|
10859
|
+
blocks.push(bits.join("\n"));
|
|
10860
|
+
}
|
|
10861
|
+
return blocks.join("\n\n");
|
|
10891
10862
|
}
|
|
10892
|
-
function
|
|
10893
|
-
|
|
10894
|
-
|
|
10895
|
-
|
|
10896
|
-
|
|
10897
|
-
|
|
10898
|
-
|
|
10863
|
+
function buildSessionSeed(messages, opts) {
|
|
10864
|
+
if (messages.length === 0) return null;
|
|
10865
|
+
const body = formatMessagesAsTranscript(messages, { tools: opts?.tools ?? "full" });
|
|
10866
|
+
if (!body.trim()) return null;
|
|
10867
|
+
return [
|
|
10868
|
+
"Sideboard conversation context (restored after compaction or a new session):",
|
|
10869
|
+
"",
|
|
10870
|
+
body,
|
|
10871
|
+
"",
|
|
10872
|
+
"Continue from this context. Do not repeat the summary unless asked."
|
|
10873
|
+
].join("\n");
|
|
10899
10874
|
}
|
|
10900
|
-
function
|
|
10901
|
-
const
|
|
10902
|
-
if (
|
|
10903
|
-
|
|
10875
|
+
function applyCompaction(messages, summaryText, thresholds = {}) {
|
|
10876
|
+
const { older, recent } = splitForCompaction(messages, thresholds);
|
|
10877
|
+
if (older.length === 0) return messages;
|
|
10878
|
+
const summary = {
|
|
10879
|
+
role: "summary",
|
|
10880
|
+
text: summaryText.trim(),
|
|
10881
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
10882
|
+
};
|
|
10883
|
+
return [summary, ...recent];
|
|
10904
10884
|
}
|
|
10905
|
-
function
|
|
10906
|
-
|
|
10907
|
-
|
|
10908
|
-
|
|
10909
|
-
return scripts.find((s) => s.name === name) ?? null;
|
|
10885
|
+
function lastRequestOccupancy(thread) {
|
|
10886
|
+
for (let i = thread.messages.length - 1; i >= 0; i--) {
|
|
10887
|
+
const usage = thread.messages[i]?.usage;
|
|
10888
|
+
if (usage) return contextTokens(usage);
|
|
10910
10889
|
}
|
|
10911
|
-
return
|
|
10890
|
+
return 0;
|
|
10912
10891
|
}
|
|
10913
|
-
function
|
|
10914
|
-
return
|
|
10892
|
+
function shouldResetSessionForOccupancy(thread, occupancyTokens = SESSION_RESET_OCCUPANCY_TOKENS) {
|
|
10893
|
+
return lastRequestOccupancy(thread) >= occupancyTokens;
|
|
10915
10894
|
}
|
|
10916
|
-
async function
|
|
10917
|
-
|
|
10918
|
-
|
|
10919
|
-
|
|
10920
|
-
|
|
10921
|
-
|
|
10922
|
-
|
|
10923
|
-
|
|
10924
|
-
|
|
10925
|
-
|
|
10926
|
-
|
|
10927
|
-
server.close((err) => err ? reject(err) : resolve(port));
|
|
10928
|
-
});
|
|
10929
|
-
server.on("error", reject);
|
|
10895
|
+
async function maybeCompactContext(thread, thresholds = {}, summarize = summarizeConversation) {
|
|
10896
|
+
if (!shouldCompactContext(thread.messages, thresholds)) {
|
|
10897
|
+
return { didCompact: false, thread };
|
|
10898
|
+
}
|
|
10899
|
+
const { older } = splitForCompaction(thread.messages, thresholds);
|
|
10900
|
+
if (older.length === 0) {
|
|
10901
|
+
return { didCompact: false, thread };
|
|
10902
|
+
}
|
|
10903
|
+
const transcript = formatMessagesAsTranscript(older, { tools: "summary" });
|
|
10904
|
+
const { summary, method } = await summarize(transcript, {
|
|
10905
|
+
cwd: thread.worktreePath
|
|
10930
10906
|
});
|
|
10907
|
+
const messages = applyCompaction(thread.messages, summary, thresholds);
|
|
10908
|
+
const resetSession = shouldResetSessionForOccupancy({ messages: thread.messages });
|
|
10909
|
+
const next = {
|
|
10910
|
+
...thread,
|
|
10911
|
+
messages,
|
|
10912
|
+
sessionId: resetSession ? null : thread.sessionId,
|
|
10913
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10914
|
+
};
|
|
10915
|
+
return {
|
|
10916
|
+
didCompact: true,
|
|
10917
|
+
thread: next,
|
|
10918
|
+
summary,
|
|
10919
|
+
method,
|
|
10920
|
+
olderCount: older.length
|
|
10921
|
+
};
|
|
10931
10922
|
}
|
|
10932
|
-
|
|
10933
|
-
|
|
10934
|
-
|
|
10935
|
-
for (let i = 1; i < size; i++) {
|
|
10936
|
-
const candidate = base + i;
|
|
10937
|
-
const free = await new Promise((resolve) => {
|
|
10938
|
-
const server = (0, import_node_net.createServer)();
|
|
10939
|
-
server.once("error", () => resolve(false));
|
|
10940
|
-
server.listen(candidate, "127.0.0.1", () => {
|
|
10941
|
-
server.close(() => resolve(true));
|
|
10942
|
-
});
|
|
10943
|
-
});
|
|
10944
|
-
if (free) {
|
|
10945
|
-
ports.push(candidate);
|
|
10946
|
-
} else {
|
|
10947
|
-
ports.push(await allocatePort());
|
|
10948
|
-
}
|
|
10949
|
-
}
|
|
10950
|
-
return ports;
|
|
10951
|
-
}
|
|
10952
|
-
async function startDevServer(repoPath, worktreePath, onLine, opts) {
|
|
10953
|
-
const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
|
|
10954
|
-
if (!script) return null;
|
|
10955
|
-
const ports = await allocatePortRange(PORT_RANGE_SIZE);
|
|
10956
|
-
const handle = await spawnWorkspaceScript(script.command, {
|
|
10957
|
-
worktreePath,
|
|
10958
|
-
repoPath,
|
|
10959
|
-
ports,
|
|
10960
|
-
defaultBranch: opts?.defaultBranch,
|
|
10961
|
-
onLine
|
|
10962
|
-
});
|
|
10963
|
-
return {
|
|
10964
|
-
pid: handle.pid,
|
|
10965
|
-
port: ports[0],
|
|
10966
|
-
ports,
|
|
10967
|
-
scriptName: script.name,
|
|
10968
|
-
kill: handle.kill,
|
|
10969
|
-
done: handle.done
|
|
10970
|
-
};
|
|
10971
|
-
}
|
|
10972
|
-
var import_node_fs29, import_node_net, import_node_path28, import_execa4, import_node_readline3, PORT_RANGE_SIZE, cachedLoginEnv;
|
|
10973
|
-
var init_conductor = __esm({
|
|
10974
|
-
"src/hook/conductor.ts"() {
|
|
10923
|
+
var CONTEXT_COMPACT_CHARS, CONTEXT_KEEP_RECENT_CHARS, CONTEXT_KEEP_RECENT_MESSAGES, CONTEXT_MIN_MESSAGES, SESSION_RESET_OCCUPANCY_TOKENS;
|
|
10924
|
+
var init_context_compact = __esm({
|
|
10925
|
+
"src/composer/context-compact.ts"() {
|
|
10975
10926
|
"use strict";
|
|
10976
|
-
|
|
10977
|
-
|
|
10978
|
-
|
|
10979
|
-
|
|
10980
|
-
|
|
10981
|
-
|
|
10982
|
-
|
|
10983
|
-
init_convention_setup();
|
|
10984
|
-
init_cursor_worktrees();
|
|
10985
|
-
init_git_auth_mode();
|
|
10986
|
-
init_nested_electron_env();
|
|
10987
|
-
PORT_RANGE_SIZE = 10;
|
|
10988
|
-
cachedLoginEnv = null;
|
|
10927
|
+
init_usage();
|
|
10928
|
+
init_summarize();
|
|
10929
|
+
CONTEXT_COMPACT_CHARS = 4e5;
|
|
10930
|
+
CONTEXT_KEEP_RECENT_CHARS = 24e3;
|
|
10931
|
+
CONTEXT_KEEP_RECENT_MESSAGES = 12;
|
|
10932
|
+
CONTEXT_MIN_MESSAGES = 10;
|
|
10933
|
+
SESSION_RESET_OCCUPANCY_TOKENS = 75e4;
|
|
10989
10934
|
}
|
|
10990
10935
|
});
|
|
10991
10936
|
|
|
10992
|
-
// src/
|
|
10993
|
-
function
|
|
10994
|
-
return
|
|
10937
|
+
// src/threads/chat-tabs.ts
|
|
10938
|
+
function sameWorktreePath(a, b) {
|
|
10939
|
+
return normalizeWorktreePath(a) === normalizeWorktreePath(b);
|
|
10995
10940
|
}
|
|
10996
|
-
|
|
10997
|
-
const
|
|
10998
|
-
const
|
|
10999
|
-
const
|
|
11000
|
-
|
|
11001
|
-
)
|
|
11002
|
-
|
|
11003
|
-
|
|
11004
|
-
try {
|
|
11005
|
-
for (const entry of (0, import_node_fs30.readdirSync)(homeRoot, { withFileTypes: true })) {
|
|
11006
|
-
if (!entry.isDirectory()) continue;
|
|
11007
|
-
void entry;
|
|
11008
|
-
}
|
|
11009
|
-
} catch {
|
|
11010
|
-
}
|
|
11011
|
-
}
|
|
11012
|
-
const orphans = [];
|
|
11013
|
-
const seen = /* @__PURE__ */ new Set();
|
|
11014
|
-
for (const repoPath of repos) {
|
|
11015
|
-
if (!repoPath || !(0, import_node_fs30.existsSync)(repoPath)) continue;
|
|
11016
|
-
try {
|
|
11017
|
-
const wts = await listWorktrees(repoPath);
|
|
11018
|
-
for (const wt of wts) {
|
|
11019
|
-
const path2 = wt.path.replace(/\/$/, "");
|
|
11020
|
-
if (!isSideboardWorktreePath(path2)) continue;
|
|
11021
|
-
if (known.has(path2)) continue;
|
|
11022
|
-
if (seen.has(path2)) continue;
|
|
11023
|
-
seen.add(path2);
|
|
11024
|
-
let mtimeMs = 0;
|
|
11025
|
-
try {
|
|
11026
|
-
mtimeMs = (0, import_node_fs30.statSync)(path2).mtimeMs;
|
|
11027
|
-
} catch {
|
|
11028
|
-
mtimeMs = 0;
|
|
11029
|
-
}
|
|
11030
|
-
orphans.push({ path: path2, repoPath, mtimeMs });
|
|
11031
|
-
}
|
|
11032
|
-
} catch {
|
|
11033
|
-
}
|
|
11034
|
-
try {
|
|
11035
|
-
const root = worktreesRoot(repoPath);
|
|
11036
|
-
if ((0, import_node_fs30.existsSync)(root)) {
|
|
11037
|
-
for (const entry of (0, import_node_fs30.readdirSync)(root, { withFileTypes: true })) {
|
|
11038
|
-
if (!entry.isDirectory()) continue;
|
|
11039
|
-
const path2 = (0, import_node_path29.join)(root, entry.name).replace(/\/$/, "");
|
|
11040
|
-
if (known.has(path2) || seen.has(path2)) continue;
|
|
11041
|
-
if (!(0, import_node_fs30.existsSync)((0, import_node_path29.join)(path2, ".git"))) continue;
|
|
11042
|
-
seen.add(path2);
|
|
11043
|
-
let mtimeMs = 0;
|
|
11044
|
-
try {
|
|
11045
|
-
mtimeMs = (0, import_node_fs30.statSync)(path2).mtimeMs;
|
|
11046
|
-
} catch {
|
|
11047
|
-
mtimeMs = Date.now();
|
|
11048
|
-
}
|
|
11049
|
-
orphans.push({ path: path2, repoPath, mtimeMs });
|
|
11050
|
-
}
|
|
11051
|
-
}
|
|
11052
|
-
} catch {
|
|
10941
|
+
function takenTeamSlugsForChatTab(worktreePath) {
|
|
10942
|
+
const key = normalizeWorktreePath(worktreePath);
|
|
10943
|
+
const taken = /* @__PURE__ */ new Set();
|
|
10944
|
+
const dir = worktreeNameFromPath(key);
|
|
10945
|
+
if (dir) taken.add(dir.toLowerCase());
|
|
10946
|
+
for (const sibling of threadsSharingWorktree(key)) {
|
|
10947
|
+
for (const slug of takenSlugsFromThread(sibling)) {
|
|
10948
|
+
taken.add(slug);
|
|
11053
10949
|
}
|
|
11054
10950
|
}
|
|
11055
|
-
return
|
|
10951
|
+
return [...taken];
|
|
11056
10952
|
}
|
|
11057
|
-
|
|
11058
|
-
const
|
|
11059
|
-
|
|
11060
|
-
|
|
11061
|
-
const activeCount = listThreads().filter((t) => t.status !== "archived").length;
|
|
11062
|
-
const budget = Math.max(0, maxCount - activeCount);
|
|
11063
|
-
const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
11064
|
-
const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
|
|
11065
|
-
const removed = [];
|
|
11066
|
-
const kept = [];
|
|
11067
|
-
for (const orphan of orphans) {
|
|
11068
|
-
if (keep.has(orphan.path)) {
|
|
11069
|
-
kept.push(orphan.path);
|
|
11070
|
-
continue;
|
|
11071
|
-
}
|
|
11072
|
-
if (opts?.dryRun) {
|
|
11073
|
-
removed.push(orphan.path);
|
|
11074
|
-
continue;
|
|
11075
|
-
}
|
|
11076
|
-
try {
|
|
11077
|
-
await removeWorktree(orphan.repoPath, orphan.path);
|
|
11078
|
-
removed.push(orphan.path);
|
|
11079
|
-
} catch {
|
|
11080
|
-
kept.push(orphan.path);
|
|
11081
|
-
}
|
|
11082
|
-
}
|
|
11083
|
-
if (!opts?.dryRun) {
|
|
11084
|
-
updateAdvancedSettings({
|
|
11085
|
-
worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11086
|
-
});
|
|
11087
|
-
}
|
|
11088
|
-
return { removed, kept, orphans };
|
|
10953
|
+
function requireThread(idOrRef) {
|
|
10954
|
+
const thread = findThreadByRef(idOrRef) ?? null;
|
|
10955
|
+
if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
|
|
10956
|
+
return thread;
|
|
11089
10957
|
}
|
|
11090
|
-
function
|
|
11091
|
-
|
|
11092
|
-
|
|
11093
|
-
|
|
11094
|
-
|
|
11095
|
-
|
|
10958
|
+
function worktreeBindingFrom(from) {
|
|
10959
|
+
return {
|
|
10960
|
+
worktreePath: normalizeWorktreePath(from.worktreePath),
|
|
10961
|
+
repoPath: from.repoPath,
|
|
10962
|
+
branchName: from.branchName,
|
|
10963
|
+
sourceRef: from.sourceRef,
|
|
10964
|
+
// Legacy pinned-repo orchestration: new tabs become normal worktree agents.
|
|
10965
|
+
// Global: keep orchestration so chat tabs stay fleet orchestrators (MCP + Bash).
|
|
10966
|
+
sourceType: isGlobalThread(from) ? "orchestration" : from.sourceType === "orchestration" ? "branch" : from.sourceType,
|
|
10967
|
+
sourceIsFork: from.sourceIsFork,
|
|
10968
|
+
parentThreadId: from.parentThreadId,
|
|
10969
|
+
prUrl: from.prUrl,
|
|
10970
|
+
prTitle: from.prTitle,
|
|
10971
|
+
prState: from.prState,
|
|
10972
|
+
skipAutoArchiveOnMerge: from.skipAutoArchiveOnMerge,
|
|
10973
|
+
stackId: from.stackId,
|
|
10974
|
+
stackLayer: from.stackLayer
|
|
10975
|
+
};
|
|
11096
10976
|
}
|
|
11097
|
-
function
|
|
11098
|
-
|
|
10977
|
+
function threadsSharingWorktree(worktreePath) {
|
|
10978
|
+
if (!worktreePath) return [];
|
|
10979
|
+
const key = normalizeWorktreePath(worktreePath);
|
|
10980
|
+
return listThreads({ includeArchived: true }).filter(
|
|
10981
|
+
(t) => Boolean(t.worktreePath) && sameWorktreePath(t.worktreePath, key) && t.status !== "archived"
|
|
10982
|
+
).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
10983
|
+
}
|
|
10984
|
+
function formatTranscriptMarkdown(title, messages) {
|
|
10985
|
+
const body = formatMessagesAsTranscript(messages, { tools: "full" });
|
|
10986
|
+
return [`# Transcript: ${title}`, "", `Forked ${(/* @__PURE__ */ new Date()).toISOString()}`, "", body].join("\n");
|
|
10987
|
+
}
|
|
10988
|
+
function forkMessageSlice(from, throughIndex) {
|
|
10989
|
+
const through = throughIndex === void 0 ? from.messages.length - 1 : Math.min(throughIndex, from.messages.length - 1);
|
|
10990
|
+
return through >= 0 ? from.messages.slice(0, through + 1) : [];
|
|
10991
|
+
}
|
|
10992
|
+
function buildForkTranscriptAttachment(baseTitle, messages) {
|
|
10993
|
+
const title = baseTitle || "Chat";
|
|
11099
10994
|
return {
|
|
11100
|
-
|
|
11101
|
-
|
|
11102
|
-
|
|
11103
|
-
|
|
10995
|
+
id: (0, import_node_crypto5.randomUUID)(),
|
|
10996
|
+
name: `Transcript of ${title}.md`,
|
|
10997
|
+
kind: "transcript",
|
|
10998
|
+
content: formatTranscriptMarkdown(title, messages)
|
|
11104
10999
|
};
|
|
11105
11000
|
}
|
|
11106
|
-
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
init_paths();
|
|
11115
|
-
init_app_settings();
|
|
11116
|
-
}
|
|
11117
|
-
});
|
|
11118
|
-
|
|
11119
|
-
// src/git/apply-into-main.ts
|
|
11120
|
-
async function applyThreadIntoMain(thread, opts) {
|
|
11121
|
-
const method = opts?.method ?? "merge";
|
|
11122
|
-
const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
|
|
11123
|
-
const status = await (0, import_execa5.execa)("git", ["status", "--porcelain"], {
|
|
11124
|
-
cwd: thread.repoPath,
|
|
11125
|
-
reject: false
|
|
11126
|
-
});
|
|
11127
|
-
if (status.stdout.trim()) {
|
|
11128
|
-
throw new Error(
|
|
11129
|
-
`Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
|
|
11130
|
-
);
|
|
11131
|
-
}
|
|
11132
|
-
const checkout = await (0, import_execa5.execa)("git", ["checkout", target], {
|
|
11133
|
-
cwd: thread.repoPath,
|
|
11134
|
-
reject: false
|
|
11135
|
-
});
|
|
11136
|
-
if (checkout.exitCode !== 0) {
|
|
11137
|
-
throw new Error(
|
|
11138
|
-
`Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
|
|
11139
|
-
);
|
|
11001
|
+
function createChatTab(input) {
|
|
11002
|
+
const from = requireThread(input.fromThreadId);
|
|
11003
|
+
const binding = worktreeBindingFrom(from);
|
|
11004
|
+
const explicitTitle = input.title?.trim();
|
|
11005
|
+
const title = explicitTitle || allocateTeamName(takenTeamSlugsForChatTab(binding.worktreePath)).name;
|
|
11006
|
+
const nextAgent = input.agent ?? from.agent;
|
|
11007
|
+
if (isOrchestratorThread(from) || binding.sourceType === "orchestration") {
|
|
11008
|
+
assertOrchestratorCapableAgent(nextAgent);
|
|
11140
11009
|
}
|
|
11141
|
-
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
|
|
11145
|
-
|
|
11146
|
-
)
|
|
11147
|
-
|
|
11148
|
-
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
reject: false
|
|
11165
|
-
});
|
|
11166
|
-
throw new Error(
|
|
11167
|
-
`Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
|
|
11168
|
-
);
|
|
11169
|
-
}
|
|
11170
|
-
}
|
|
11171
|
-
return {
|
|
11172
|
-
applied: true,
|
|
11173
|
-
method,
|
|
11174
|
-
targetBranch: target,
|
|
11175
|
-
message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
|
|
11176
|
-
};
|
|
11177
|
-
}
|
|
11178
|
-
const merge = await (0, import_execa5.execa)(
|
|
11179
|
-
"git",
|
|
11180
|
-
["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
|
|
11181
|
-
{ cwd: thread.repoPath, reject: false }
|
|
11182
|
-
);
|
|
11183
|
-
if (merge.exitCode !== 0) {
|
|
11184
|
-
await (0, import_execa5.execa)("git", ["merge", "--abort"], {
|
|
11185
|
-
cwd: thread.repoPath,
|
|
11186
|
-
reject: false
|
|
11187
|
-
});
|
|
11188
|
-
throw new Error(
|
|
11189
|
-
`Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
|
|
11190
|
-
);
|
|
11191
|
-
}
|
|
11192
|
-
return {
|
|
11193
|
-
applied: true,
|
|
11194
|
-
method: "merge",
|
|
11195
|
-
targetBranch: target,
|
|
11196
|
-
message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
|
|
11197
|
-
};
|
|
11198
|
-
}
|
|
11199
|
-
var import_execa5;
|
|
11200
|
-
var init_apply_into_main = __esm({
|
|
11201
|
-
"src/git/apply-into-main.ts"() {
|
|
11202
|
-
"use strict";
|
|
11203
|
-
import_execa5 = require("execa");
|
|
11204
|
-
init_worktree();
|
|
11205
|
-
}
|
|
11206
|
-
});
|
|
11207
|
-
|
|
11208
|
-
// src/store/workspaces.ts
|
|
11209
|
-
var workspaces_exports = {};
|
|
11210
|
-
__export(workspaces_exports, {
|
|
11211
|
-
addWorkspace: () => addWorkspace,
|
|
11212
|
-
ensureWorkspace: () => ensureWorkspace,
|
|
11213
|
-
listWorkspaces: () => listWorkspaces,
|
|
11214
|
-
removeWorkspace: () => removeWorkspace,
|
|
11215
|
-
syncWorkspacesFromThreads: () => syncWorkspacesFromThreads
|
|
11216
|
-
});
|
|
11217
|
-
function workspacesFile() {
|
|
11218
|
-
return (0, import_node_path30.join)(appDataDir(), "workspaces.json");
|
|
11219
|
-
}
|
|
11220
|
-
function removedWorkspacesFile() {
|
|
11221
|
-
return (0, import_node_path30.join)(appDataDir(), "removed-workspaces.json");
|
|
11222
|
-
}
|
|
11223
|
-
function readAll2() {
|
|
11224
|
-
const path2 = workspacesFile();
|
|
11225
|
-
if (!(0, import_node_fs31.existsSync)(path2)) return [];
|
|
11226
|
-
try {
|
|
11227
|
-
const raw = JSON.parse((0, import_node_fs31.readFileSync)(path2, "utf8"));
|
|
11228
|
-
return Array.isArray(raw) ? raw : [];
|
|
11229
|
-
} catch {
|
|
11230
|
-
return [];
|
|
11231
|
-
}
|
|
11232
|
-
}
|
|
11233
|
-
function writeAll2(list) {
|
|
11234
|
-
(0, import_node_fs31.mkdirSync)(appDataDir(), { recursive: true });
|
|
11235
|
-
(0, import_node_fs31.writeFileSync)(workspacesFile(), JSON.stringify(list, null, 2), "utf8");
|
|
11236
|
-
}
|
|
11237
|
-
function readRemoved() {
|
|
11238
|
-
const path2 = removedWorkspacesFile();
|
|
11239
|
-
if (!(0, import_node_fs31.existsSync)(path2)) return /* @__PURE__ */ new Set();
|
|
11240
|
-
try {
|
|
11241
|
-
const raw = JSON.parse((0, import_node_fs31.readFileSync)(path2, "utf8"));
|
|
11242
|
-
return new Set(Array.isArray(raw) ? raw.filter((p) => typeof p === "string") : []);
|
|
11243
|
-
} catch {
|
|
11244
|
-
return /* @__PURE__ */ new Set();
|
|
11245
|
-
}
|
|
11246
|
-
}
|
|
11247
|
-
function writeRemoved(paths) {
|
|
11248
|
-
(0, import_node_fs31.mkdirSync)(appDataDir(), { recursive: true });
|
|
11249
|
-
(0, import_node_fs31.writeFileSync)(removedWorkspacesFile(), JSON.stringify([...paths].sort(), null, 2), "utf8");
|
|
11250
|
-
}
|
|
11251
|
-
function rememberRemoved(repoPath) {
|
|
11252
|
-
const next = readRemoved();
|
|
11253
|
-
next.add(repoPath);
|
|
11254
|
-
writeRemoved(next);
|
|
11255
|
-
}
|
|
11256
|
-
function forgetRemoved(repoPath) {
|
|
11257
|
-
const next = readRemoved();
|
|
11258
|
-
if (!next.delete(repoPath)) return;
|
|
11259
|
-
writeRemoved(next);
|
|
11260
|
-
}
|
|
11261
|
-
function listWorkspaces() {
|
|
11262
|
-
const all = readAll2();
|
|
11263
|
-
const valid = all.filter(
|
|
11264
|
-
(w) => Boolean(w.path) && w.path !== "/" && w.path !== "." && !isGlobalRepoPath(w.path)
|
|
11265
|
-
);
|
|
11266
|
-
if (valid.length !== all.length) writeAll2(valid);
|
|
11267
|
-
return valid.sort((a, b) => a.name.localeCompare(b.name));
|
|
11268
|
-
}
|
|
11269
|
-
async function addWorkspace(repoPath) {
|
|
11270
|
-
const root = await resolveRepoRoot(repoPath);
|
|
11271
|
-
if (!root || root === "/") throw new Error(`Invalid repo path: ${repoPath}`);
|
|
11272
|
-
if (!(0, import_node_fs31.existsSync)(root)) throw new Error(`Repo not found: ${root}`);
|
|
11273
|
-
forgetRemoved(root);
|
|
11274
|
-
await ensureGhPreferOrigin(root);
|
|
11275
|
-
const current = readAll2();
|
|
11276
|
-
const existing = current.find((w) => w.path === root);
|
|
11277
|
-
if (existing) return existing;
|
|
11278
|
-
const next = {
|
|
11279
|
-
path: root,
|
|
11280
|
-
name: (0, import_node_path30.basename)(root),
|
|
11281
|
-
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11282
|
-
};
|
|
11283
|
-
writeAll2([...current, next]);
|
|
11284
|
-
return next;
|
|
11285
|
-
}
|
|
11286
|
-
function removeWorkspace(repoPath) {
|
|
11287
|
-
writeAll2(readAll2().filter((w) => w.path !== repoPath));
|
|
11288
|
-
rememberRemoved(repoPath);
|
|
11289
|
-
}
|
|
11290
|
-
async function ensureWorkspace(repoPath) {
|
|
11291
|
-
return addWorkspace(repoPath);
|
|
11010
|
+
const singletonInbox = isSlackCoordinatorThread(from) || isCloudCoordinatorThread(from);
|
|
11011
|
+
const thread = createEmptyThread({
|
|
11012
|
+
title,
|
|
11013
|
+
// Chat-tab nicknames (soccer team or explicit) must stick. Post-turn
|
|
11014
|
+
// syncThreadBranchFromGit would otherwise rewrite every sibling to the
|
|
11015
|
+
// shared worktree folder name (e.g. fork "Arsenal" → "Monaco").
|
|
11016
|
+
userSetTitle: true,
|
|
11017
|
+
...binding,
|
|
11018
|
+
// Slack / Brightsy cloud identity stays on the original chat. A + tab
|
|
11019
|
+
// that copies `slack:team:user` would steal inbound DMs after you close
|
|
11020
|
+
// the connected chat.
|
|
11021
|
+
sourceRef: singletonInbox ? title : binding.sourceRef,
|
|
11022
|
+
agent: nextAgent,
|
|
11023
|
+
model: input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model,
|
|
11024
|
+
effort: input.effort !== void 0 ? input.effort : from.effort,
|
|
11025
|
+
fast: input.fast !== void 0 ? Boolean(input.fast) : from.fast,
|
|
11026
|
+
planMode: from.planMode,
|
|
11027
|
+
autonomy: input.autonomy ?? from.autonomy,
|
|
11028
|
+
attachments: input.attachments ?? [],
|
|
11029
|
+
status: "idle"
|
|
11030
|
+
});
|
|
11031
|
+
writeThread(thread);
|
|
11032
|
+
return thread;
|
|
11292
11033
|
}
|
|
11293
|
-
function
|
|
11294
|
-
const
|
|
11295
|
-
const
|
|
11296
|
-
const
|
|
11297
|
-
|
|
11298
|
-
|
|
11299
|
-
|
|
11300
|
-
|
|
11301
|
-
|
|
11302
|
-
|
|
11303
|
-
|
|
11304
|
-
|
|
11305
|
-
|
|
11306
|
-
|
|
11307
|
-
|
|
11308
|
-
byPath.set(path2, ws);
|
|
11309
|
-
dirty = true;
|
|
11034
|
+
function forkChatTab(input) {
|
|
11035
|
+
const from = requireThread(input.threadId);
|
|
11036
|
+
const slice = forkMessageSlice(from, input.throughIndex);
|
|
11037
|
+
const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
|
|
11038
|
+
const tab = createChatTab({
|
|
11039
|
+
fromThreadId: input.threadId,
|
|
11040
|
+
agent: input.agent ?? from.agent,
|
|
11041
|
+
model: input.model,
|
|
11042
|
+
title: input.title?.trim() || void 0,
|
|
11043
|
+
attachments: [attachment]
|
|
11044
|
+
});
|
|
11045
|
+
if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
|
|
11046
|
+
const next = { ...tab, parentThreadId: from.id };
|
|
11047
|
+
writeThread(next);
|
|
11048
|
+
return next;
|
|
11310
11049
|
}
|
|
11311
|
-
|
|
11312
|
-
if (dirty) writeAll2(next);
|
|
11313
|
-
return next.sort((a, b) => a.name.localeCompare(b.name));
|
|
11050
|
+
return tab;
|
|
11314
11051
|
}
|
|
11315
|
-
var
|
|
11316
|
-
var
|
|
11317
|
-
"src/
|
|
11052
|
+
var import_node_crypto5;
|
|
11053
|
+
var init_chat_tabs = __esm({
|
|
11054
|
+
"src/threads/chat-tabs.ts"() {
|
|
11318
11055
|
"use strict";
|
|
11319
|
-
|
|
11320
|
-
|
|
11321
|
-
|
|
11056
|
+
import_node_crypto5 = require("crypto");
|
|
11057
|
+
init_context_compact();
|
|
11058
|
+
init_teams();
|
|
11059
|
+
init_worktree_labels();
|
|
11322
11060
|
init_global_workspace();
|
|
11323
|
-
|
|
11061
|
+
init_orchestrator_capable();
|
|
11062
|
+
init_thread_store();
|
|
11063
|
+
init_worktree_labels();
|
|
11324
11064
|
}
|
|
11325
11065
|
});
|
|
11326
11066
|
|
|
11327
|
-
// src/
|
|
11328
|
-
|
|
11329
|
-
const
|
|
11330
|
-
|
|
11331
|
-
|
|
11332
|
-
|
|
11333
|
-
|
|
11334
|
-
name = leaf || "repo";
|
|
11335
|
-
}
|
|
11336
|
-
name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
11337
|
-
const dest = (0, import_node_path31.join)(sideboardReposDir(), name);
|
|
11338
|
-
if ((0, import_node_fs32.existsSync)(dest)) {
|
|
11339
|
-
const repoPath2 = await resolveRepoRoot(dest);
|
|
11340
|
-
const workspace2 = await ensureWorkspace(repoPath2);
|
|
11341
|
-
return { repoPath: repoPath2, workspace: workspace2 };
|
|
11342
|
-
}
|
|
11343
|
-
const result = await (0, import_execa6.execa)("git", ["clone", url, dest], { reject: false });
|
|
11344
|
-
if (result.exitCode !== 0) {
|
|
11345
|
-
throw new Error(
|
|
11346
|
-
`git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
|
|
11347
|
-
);
|
|
11067
|
+
// src/review/review-request-template.ts
|
|
11068
|
+
function wrapReviewSkillMarkdown(body) {
|
|
11069
|
+
const trimmed = body.replace(/^\uFEFF/, "").trim();
|
|
11070
|
+
const source = trimmed || REVIEW_REQUEST_TEMPLATE.trim();
|
|
11071
|
+
if (/^---\s*\n[\s\S]*?\n---\s*\n/.test(source)) {
|
|
11072
|
+
return source.endsWith("\n") ? source : `${source}
|
|
11073
|
+
`;
|
|
11348
11074
|
}
|
|
11349
|
-
|
|
11350
|
-
|
|
11351
|
-
return { repoPath, workspace };
|
|
11075
|
+
return `${REVIEW_SKILL_FRONTMATTER}${source}
|
|
11076
|
+
`;
|
|
11352
11077
|
}
|
|
11353
|
-
var
|
|
11354
|
-
var
|
|
11355
|
-
"src/
|
|
11078
|
+
var REVIEW_REQUEST_TEMPLATE, REVIEW_SKILL_PATH, REVIEW_SKILL_NAME, REVIEW_SKILL_FRONTMATTER;
|
|
11079
|
+
var init_review_request_template = __esm({
|
|
11080
|
+
"src/review/review-request-template.ts"() {
|
|
11356
11081
|
"use strict";
|
|
11357
|
-
|
|
11358
|
-
import_node_path31 = require("path");
|
|
11359
|
-
import_execa6 = require("execa");
|
|
11360
|
-
init_paths();
|
|
11361
|
-
init_workspaces2();
|
|
11362
|
-
init_worktree();
|
|
11363
|
-
}
|
|
11364
|
-
});
|
|
11082
|
+
REVIEW_REQUEST_TEMPLATE = `# Review guidelines:
|
|
11365
11083
|
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
|
|
11369
|
-
|
|
11370
|
-
|
|
11371
|
-
|
|
11372
|
-
|
|
11373
|
-
|
|
11374
|
-
|
|
11375
|
-
|
|
11376
|
-
|
|
11377
|
-
|
|
11378
|
-
|
|
11379
|
-
|
|
11380
|
-
|
|
11381
|
-
|
|
11382
|
-
|
|
11383
|
-
|
|
11384
|
-
|
|
11385
|
-
|
|
11386
|
-
|
|
11387
|
-
|
|
11388
|
-
|
|
11084
|
+
You are reviewing a proposed code change so a human can decide whether it is **ready to merge / land**. Findings matter, but the primary deliverable is a clear readiness recommendation \u2014 not a laundry list of style notes.
|
|
11085
|
+
|
|
11086
|
+
## Required outcome
|
|
11087
|
+
|
|
11088
|
+
Start your reply with a **Recommendation** section using exactly one of:
|
|
11089
|
+
|
|
11090
|
+
- **Approve** \u2014 ready to merge as-is (or with only trivial nits the author can ignore).
|
|
11091
|
+
- **Approve with nits** \u2014 ready to merge; list only optional polish that should not block.
|
|
11092
|
+
- **Request changes** \u2014 not ready; blocking issues must be fixed first.
|
|
11093
|
+
- **Needs more information** \u2014 cannot judge readiness yet (missing context, incomplete diff, unclear intent).
|
|
11094
|
+
|
|
11095
|
+
In 1\u20133 sentences, say **why** \u2014 grounded in correctness, risk, test coverage, and scope \u2014 not vibes. If you request changes, name the blockers explicitly.
|
|
11096
|
+
|
|
11097
|
+
People running this review are asking \u201Ccan we ship this?\u201D Treat that as the question you answer first.
|
|
11098
|
+
|
|
11099
|
+
## Findings
|
|
11100
|
+
|
|
11101
|
+
Below are guidelines for determining whether an issue is worth flagging to the original author.
|
|
11102
|
+
|
|
11103
|
+
These are not the final word. More specific guidelines elsewhere (developer message, user message, a file, etc.) override these.
|
|
11104
|
+
|
|
11105
|
+
Flag something as a bug / blocking finding only when:
|
|
11106
|
+
|
|
11107
|
+
1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
|
|
11108
|
+
2. The bug is discrete and actionable (not a vague codebase-wide complaint or a bundle of unrelated issues).
|
|
11109
|
+
3. Fixing it does not demand rigor absent from the rest of the codebase.
|
|
11110
|
+
4. The issue was introduced by this change (do not flag pre-existing bugs unless they are newly exposed by this PR).
|
|
11111
|
+
5. The author would likely fix it if made aware.
|
|
11112
|
+
6. It does not rely on unstated assumptions about the codebase or author intent.
|
|
11113
|
+
7. Speculative breakage is not enough \u2014 identify the other code that is provably affected.
|
|
11114
|
+
8. It is clearly not just an intentional change by the author.
|
|
11115
|
+
|
|
11116
|
+
When flagging an issue, include a short accompanying comment:
|
|
11117
|
+
|
|
11118
|
+
1. Clear about why it is a problem.
|
|
11119
|
+
2. Severity must match reality \u2014 do not inflate.
|
|
11120
|
+
3. Brief: at most one paragraph; avoid unnecessary line breaks in prose.
|
|
11121
|
+
4. No code chunks longer than 3 lines; wrap code in inline ticks or a fenced block.
|
|
11122
|
+
5. Call out scenarios / environments / inputs needed to hit the bug when severity depends on them.
|
|
11123
|
+
6. Matter-of-fact tone \u2014 helpful assistant, not accusatory or effusive.
|
|
11124
|
+
7. Skimmable on first read.
|
|
11125
|
+
8. No empty flattery (\u201CGreat job\u2026\u201D, \u201CThanks for\u2026\u201D).
|
|
11126
|
+
|
|
11127
|
+
HOW MANY FINDINGS TO RETURN:
|
|
11128
|
+
|
|
11129
|
+
List every finding the author would fix if they knew about it. If nothing qualifies, say so and still give the Recommendation. Do not stop at the first finding.
|
|
11130
|
+
|
|
11131
|
+
GUIDELINES:
|
|
11132
|
+
|
|
11133
|
+
- Ignore trivial style unless it obscures meaning or violates documented standards.
|
|
11134
|
+
- One comment per distinct issue (or a short multi-line range if needed).
|
|
11135
|
+
- Use \`\`\`suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block).
|
|
11136
|
+
- In every \`\`\`suggestion block, preserve the exact leading whitespace of the replaced lines (spaces vs tabs, number of spaces).
|
|
11137
|
+
- Do NOT introduce or remove outer indentation levels unless that is the actual fix.
|
|
11138
|
+
- Separate **blocking** findings from **nits**. Only blocking findings should drive Request changes.
|
|
11139
|
+
|
|
11140
|
+
The report appears in chat (and can become Sideboard diff comments). Avoid unnecessary location chatter in the body; keep line ranges as short as possible (prefer \u22645\u201310 lines).
|
|
11141
|
+
|
|
11142
|
+
## Getting the diff
|
|
11143
|
+
|
|
11144
|
+
Use Sideboard's diff for this thread's worktree. Prefer the \`get_diff\` MCP tool (pass this thread's ref) for a compact summary, then read specific files with Read/Glob as needed. In the Sideboard desktop app, the Changes panel shows the same worktree diff.
|
|
11145
|
+
|
|
11146
|
+
If the user asks you to address or read line comments they added in the Changes / file diff UI, those arrive as \`diff-comment\` attachments on the next turn \u2014 follow them precisely.
|
|
11147
|
+
|
|
11148
|
+
## Fallback: if you don't have access to the Sideboard diff tool
|
|
11149
|
+
|
|
11150
|
+
If you don't have access to \`get_diff\`, use the following git commands to get the diff:
|
|
11151
|
+
|
|
11152
|
+
\`\`\`bash
|
|
11153
|
+
# Get the merge base between this branch and the target
|
|
11154
|
+
MERGE_BASE=$(git merge-base origin/main HEAD)
|
|
11155
|
+
|
|
11156
|
+
# Get the committed diff against the merge base
|
|
11157
|
+
git diff $MERGE_BASE HEAD
|
|
11158
|
+
|
|
11159
|
+
# Get any uncommitted changes (staged and unstaged)
|
|
11160
|
+
git diff HEAD
|
|
11161
|
+
\`\`\`
|
|
11162
|
+
|
|
11163
|
+
Review the combination of both outputs: the first shows all committed changes on this branch relative to the target, and the second shows any uncommitted work in progress.
|
|
11164
|
+
|
|
11165
|
+
No need to mention in your report whether or not you used one of the fallback strategies; it's usually irrelevant.
|
|
11166
|
+
|
|
11167
|
+
## Output format
|
|
11168
|
+
|
|
11169
|
+
**1. Recommendation first** (required), then **2. Findings** (may be empty).
|
|
11170
|
+
|
|
11171
|
+
Only report ONE finding per unique issue.
|
|
11172
|
+
|
|
11173
|
+
<example>
|
|
11174
|
+
## Recommendation
|
|
11175
|
+
|
|
11176
|
+
**Request changes** \u2014 The empty-input crash on load will break first-run users; fix that before merge. The unused helper is a nit and can wait.
|
|
11177
|
+
|
|
11178
|
+
## Findings
|
|
11179
|
+
|
|
11180
|
+
### **#1 Empty input causes crash** (blocking)
|
|
11181
|
+
|
|
11182
|
+
If the input field is empty when the page loads, the app will crash.
|
|
11183
|
+
|
|
11184
|
+
File: src/client/frontends/desktop/ui/Input.tsx
|
|
11185
|
+
|
|
11186
|
+
### **#2 Dead code** (nit)
|
|
11187
|
+
|
|
11188
|
+
The getUserData function is now unused. It should be deleted.
|
|
11189
|
+
|
|
11190
|
+
File: src/client/frontends/desktop/core/UserData.ts
|
|
11191
|
+
</example>
|
|
11192
|
+
|
|
11193
|
+
<example>
|
|
11194
|
+
## Recommendation
|
|
11195
|
+
|
|
11196
|
+
**Approve** \u2014 Diff is scoped, behavior looks correct, and there are no blocking issues. Safe to merge.
|
|
11197
|
+
</example>
|
|
11198
|
+
|
|
11199
|
+
## Growing the rules
|
|
11200
|
+
|
|
11201
|
+
If a blocking issue is a missing or ambiguous repo rule that will recur, add one sentence to \`.claude/skills/review/SKILL.md\` (create the skill if it is missing \u2014 that is allowed and should be committed). Do not only patch this diff when the same miss will happen again. Do not write new skills under \`.sideboard/skills/\`. Do not use \`.sideboard/review.md\` for new notes.
|
|
11202
|
+
`;
|
|
11203
|
+
REVIEW_SKILL_PATH = ".claude/skills/review/SKILL.md";
|
|
11204
|
+
REVIEW_SKILL_NAME = "review";
|
|
11205
|
+
REVIEW_SKILL_FRONTMATTER = `---
|
|
11206
|
+
name: review
|
|
11207
|
+
description: >-
|
|
11208
|
+
Merge-readiness review for this repository. Use when reviewing a PR, worktree
|
|
11209
|
+
diff, or Sideboard Review (Approve / Approve with nits / Request changes).
|
|
11210
|
+
---
|
|
11211
|
+
|
|
11212
|
+
`;
|
|
11389
11213
|
}
|
|
11390
|
-
|
|
11391
|
-
|
|
11392
|
-
|
|
11393
|
-
|
|
11214
|
+
});
|
|
11215
|
+
|
|
11216
|
+
// src/review/request-review.ts
|
|
11217
|
+
function shouldRefreshReviewRequestTemplate(content) {
|
|
11218
|
+
const trimmed = content.trim();
|
|
11219
|
+
if (!trimmed) return true;
|
|
11220
|
+
if (/##\s*Recommendation|ready to merge|Approve with nits|Request changes/i.test(trimmed)) {
|
|
11221
|
+
return false;
|
|
11394
11222
|
}
|
|
11395
|
-
return
|
|
11223
|
+
return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
|
|
11396
11224
|
}
|
|
11397
|
-
|
|
11398
|
-
|
|
11399
|
-
|
|
11400
|
-
|
|
11401
|
-
|
|
11402
|
-
|
|
11225
|
+
function readTextIfPresent(abs) {
|
|
11226
|
+
if (!(0, import_node_fs29.existsSync)(abs)) return null;
|
|
11227
|
+
try {
|
|
11228
|
+
const content = (0, import_node_fs29.readFileSync)(abs, "utf8");
|
|
11229
|
+
return content.trim() ? content : null;
|
|
11230
|
+
} catch {
|
|
11231
|
+
return null;
|
|
11403
11232
|
}
|
|
11404
|
-
}
|
|
11405
|
-
|
|
11406
|
-
|
|
11407
|
-
|
|
11408
|
-
|
|
11409
|
-
|
|
11410
|
-
model: input.model,
|
|
11411
|
-
effort: input.effort,
|
|
11412
|
-
fast: input.fast
|
|
11413
|
-
});
|
|
11414
|
-
await requireAgent(resolved.agent);
|
|
11415
|
-
const repoPath = await resolveRepoRoot(input.repoPath);
|
|
11416
|
-
if (!(0, import_node_fs33.existsSync)(repoPath)) {
|
|
11417
|
-
throw new Error(`Repo not found: ${repoPath}`);
|
|
11233
|
+
}
|
|
11234
|
+
function readLocalGuidelines(worktreePath) {
|
|
11235
|
+
const localAbs = (0, import_node_path28.join)(worktreePath, REVIEW_REQUEST_PATH);
|
|
11236
|
+
const localContent = readTextIfPresent(localAbs);
|
|
11237
|
+
if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
|
|
11238
|
+
return { path: REVIEW_REQUEST_PATH, content: localContent };
|
|
11418
11239
|
}
|
|
11419
|
-
|
|
11420
|
-
|
|
11421
|
-
|
|
11422
|
-
|
|
11423
|
-
if (input.sourceType === "pr") {
|
|
11424
|
-
const num2 = Number(input.sourceRef.replace(/^#/, ""));
|
|
11425
|
-
if (!Number.isFinite(num2)) throw new Error(`Invalid PR number: ${input.sourceRef}`);
|
|
11426
|
-
const pr = await getPr(repoPath, num2);
|
|
11427
|
-
if (!pr) throw new Error(`PR #${num2} not found`);
|
|
11428
|
-
sourceIsFork = pr.isCrossRepository;
|
|
11429
|
-
prUrl = pr.url;
|
|
11430
|
-
const localFetchBranch = `sideboard-pr-${pr.number}`;
|
|
11431
|
-
await fetchPrHead(repoPath, pr.number, localFetchBranch);
|
|
11432
|
-
sourceRef = localFetchBranch;
|
|
11433
|
-
} else if (input.sourceType === "ticket") {
|
|
11434
|
-
sourceRef = await resolveDefaultBranch(repoPath);
|
|
11435
|
-
} else if (input.sourceType === "branch") {
|
|
11436
|
-
if (!sourceRef || sourceRef === "HEAD" || sourceRef === "default") {
|
|
11437
|
-
sourceRef = await resolveDefaultBranch(repoPath);
|
|
11438
|
-
} else {
|
|
11439
|
-
const existing = await getPrForHeadBranch(repoPath, sourceRef);
|
|
11440
|
-
if (existing?.url) {
|
|
11441
|
-
prUrl = existing.url;
|
|
11442
|
-
prTitle = existing.title;
|
|
11443
|
-
}
|
|
11444
|
-
}
|
|
11445
|
-
} else if (input.sourceType === "adopt") {
|
|
11446
|
-
throw new Error("Use adoptThread() for adopt sources");
|
|
11240
|
+
const legacyAbs = (0, import_node_path28.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
|
|
11241
|
+
const legacyContent = readTextIfPresent(legacyAbs);
|
|
11242
|
+
if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
|
|
11243
|
+
return { path: LEGACY_REVIEW_REQUEST_PATH, content: legacyContent };
|
|
11447
11244
|
}
|
|
11448
|
-
|
|
11449
|
-
const { branchName, worktreePath } = await createThreadWorktree({
|
|
11450
|
-
repoPath,
|
|
11451
|
-
sourceRef,
|
|
11452
|
-
slug: team.slug
|
|
11453
|
-
});
|
|
11454
|
-
copyConfiguredFiles(repoPath, worktreePath);
|
|
11455
|
-
const explicitTitle = input.title?.trim();
|
|
11456
|
-
const thread = createEmptyThread({
|
|
11457
|
-
title: explicitTitle || team.name,
|
|
11458
|
-
userSetTitle: Boolean(explicitTitle),
|
|
11459
|
-
sourceType: input.sourceType,
|
|
11460
|
-
sourceRef: input.sourceRef === "default" ? sourceRef : input.sourceRef,
|
|
11461
|
-
branchName,
|
|
11462
|
-
worktreePath,
|
|
11463
|
-
repoPath,
|
|
11464
|
-
agent: resolved.agent,
|
|
11465
|
-
autonomy: input.autonomy ?? "default",
|
|
11466
|
-
model: resolved.model,
|
|
11467
|
-
effort: resolved.effort,
|
|
11468
|
-
fast: resolved.fast,
|
|
11469
|
-
planMode: Boolean(input.planMode),
|
|
11470
|
-
attachments: input.attachments ?? [],
|
|
11471
|
-
sourceIsFork,
|
|
11472
|
-
parentThreadId: input.parentThreadId ?? null,
|
|
11473
|
-
status: "idle",
|
|
11474
|
-
prUrl,
|
|
11475
|
-
prTitle
|
|
11476
|
-
});
|
|
11477
|
-
writeThread(thread);
|
|
11478
|
-
await ensureWorkspace(repoPath);
|
|
11479
|
-
return readThread(thread.id) ?? thread;
|
|
11245
|
+
return null;
|
|
11480
11246
|
}
|
|
11481
|
-
|
|
11482
|
-
|
|
11483
|
-
|
|
11484
|
-
|
|
11485
|
-
|
|
11486
|
-
|
|
11247
|
+
function skillGuidelines(content, source) {
|
|
11248
|
+
return {
|
|
11249
|
+
path: REVIEW_SKILL_PATH,
|
|
11250
|
+
name: REVIEW_SKILL_NAME,
|
|
11251
|
+
content,
|
|
11252
|
+
source
|
|
11253
|
+
};
|
|
11254
|
+
}
|
|
11255
|
+
function ensureReviewSkillFile(worktreePath) {
|
|
11256
|
+
const abs = (0, import_node_path28.join)(worktreePath, REVIEW_SKILL_PATH);
|
|
11257
|
+
const existing = readTextIfPresent(abs);
|
|
11258
|
+
if (existing) {
|
|
11259
|
+
return { path: REVIEW_SKILL_PATH, content: existing, wrote: false };
|
|
11487
11260
|
}
|
|
11488
|
-
|
|
11261
|
+
const fromRepo = readTextIfPresent((0, import_node_path28.join)(worktreePath, REPO_REVIEW_PATH));
|
|
11262
|
+
const fromLocal = readLocalGuidelines(worktreePath)?.content ?? null;
|
|
11263
|
+
const content = wrapReviewSkillMarkdown(fromRepo ?? fromLocal ?? REVIEW_REQUEST_TEMPLATE);
|
|
11264
|
+
(0, import_node_fs29.mkdirSync)((0, import_node_path28.dirname)(abs), { recursive: true });
|
|
11265
|
+
(0, import_node_fs29.writeFileSync)(abs, content, "utf8");
|
|
11266
|
+
return { path: REVIEW_SKILL_PATH, content, wrote: true };
|
|
11489
11267
|
}
|
|
11490
|
-
|
|
11491
|
-
|
|
11492
|
-
"
|
|
11493
|
-
|
|
11494
|
-
|
|
11495
|
-
|
|
11496
|
-
|
|
11497
|
-
|
|
11498
|
-
|
|
11499
|
-
|
|
11500
|
-
|
|
11501
|
-
}
|
|
11502
|
-
});
|
|
11503
|
-
|
|
11504
|
-
// src/composer/summarize.ts
|
|
11505
|
-
async function summarizeConversation(transcript, opts) {
|
|
11506
|
-
const clipped = transcript.length > MAX_TRANSCRIPT_CHARS ? `${transcript.slice(0, MAX_TRANSCRIPT_CHARS)}
|
|
11507
|
-
|
|
11508
|
-
[\u2026truncated\u2026]` : transcript;
|
|
11509
|
-
const claudeSummary = await tryClaudeSummary(clipped, opts);
|
|
11510
|
-
if (claudeSummary) {
|
|
11511
|
-
return { summary: clipSummary(claudeSummary), method: "claude" };
|
|
11268
|
+
function resolveReviewGuidelines(worktreePath) {
|
|
11269
|
+
const skillContent = readTextIfPresent((0, import_node_path28.join)(worktreePath, REVIEW_SKILL_PATH));
|
|
11270
|
+
if (skillContent) return skillGuidelines(skillContent, "skill");
|
|
11271
|
+
const local = readLocalGuidelines(worktreePath);
|
|
11272
|
+
if (local) {
|
|
11273
|
+
return {
|
|
11274
|
+
path: local.path,
|
|
11275
|
+
name: REVIEW_REQUEST_NAME,
|
|
11276
|
+
content: local.content,
|
|
11277
|
+
source: "local"
|
|
11278
|
+
};
|
|
11512
11279
|
}
|
|
11513
|
-
|
|
11280
|
+
const seeded = ensureReviewSkillFile(worktreePath);
|
|
11281
|
+
return skillGuidelines(seeded.content, seeded.wrote ? "stock" : "skill");
|
|
11514
11282
|
}
|
|
11515
|
-
|
|
11516
|
-
|
|
11517
|
-
|
|
11518
|
-
"Summarize this coding-agent conversation for continuity in a fresh session.",
|
|
11519
|
-
"Use concise markdown bullets covering:",
|
|
11520
|
-
"- Goal / request",
|
|
11521
|
-
"- Key decisions and constraints",
|
|
11522
|
-
"- Files / areas touched",
|
|
11523
|
-
"- Current state and open tasks",
|
|
11524
|
-
"- Anything the next turn must remember",
|
|
11525
|
-
"Do not invent details. No preamble.",
|
|
11526
|
-
"",
|
|
11527
|
-
"Transcript:",
|
|
11528
|
-
transcript
|
|
11529
|
-
].join("\n");
|
|
11530
|
-
try {
|
|
11531
|
-
const { stdout, exitCode } = await run(
|
|
11532
|
-
"claude",
|
|
11533
|
-
[
|
|
11534
|
-
"-p",
|
|
11535
|
-
prompt,
|
|
11536
|
-
"--output-format",
|
|
11537
|
-
"text",
|
|
11538
|
-
"--permission-mode",
|
|
11539
|
-
"bypassPermissions",
|
|
11540
|
-
"--model",
|
|
11541
|
-
"haiku"
|
|
11542
|
-
],
|
|
11543
|
-
{ cwd: opts?.cwd, reject: false }
|
|
11544
|
-
);
|
|
11545
|
-
if (exitCode !== 0) return null;
|
|
11546
|
-
const text4 = stdout.trim();
|
|
11547
|
-
if (text4.length < 40) return null;
|
|
11548
|
-
return text4;
|
|
11549
|
-
} catch {
|
|
11550
|
-
return null;
|
|
11551
|
-
}
|
|
11283
|
+
function ensureReviewRequestFile(worktreePath) {
|
|
11284
|
+
const seeded = ensureReviewSkillFile(worktreePath);
|
|
11285
|
+
return skillGuidelines(seeded.content, "skill");
|
|
11552
11286
|
}
|
|
11553
|
-
function
|
|
11554
|
-
const
|
|
11555
|
-
const
|
|
11556
|
-
|
|
11557
|
-
|
|
11558
|
-
|
|
11559
|
-
|
|
11560
|
-
|
|
11561
|
-
|
|
11562
|
-
|
|
11563
|
-
|
|
11564
|
-
|
|
11565
|
-
|
|
11566
|
-
|
|
11567
|
-
|
|
11568
|
-
|
|
11569
|
-
|
|
11570
|
-
|
|
11571
|
-
|
|
11572
|
-
|
|
11573
|
-
|
|
11574
|
-
} else if (mode === "agent" && agents.length < 6) {
|
|
11575
|
-
agents.push(line.slice(0, 240));
|
|
11576
|
-
}
|
|
11287
|
+
function buildReviewRequestAttachment(content, opts) {
|
|
11288
|
+
const path2 = opts?.path ?? REVIEW_SKILL_PATH;
|
|
11289
|
+
const name = opts?.name ?? (path2 === REVIEW_SKILL_PATH ? REVIEW_SKILL_NAME : path2 === REPO_REVIEW_PATH ? REPO_REVIEW_NAME : REVIEW_REQUEST_NAME);
|
|
11290
|
+
return {
|
|
11291
|
+
id: (0, import_node_crypto6.randomUUID)(),
|
|
11292
|
+
name,
|
|
11293
|
+
kind: "file",
|
|
11294
|
+
path: path2,
|
|
11295
|
+
content
|
|
11296
|
+
};
|
|
11297
|
+
}
|
|
11298
|
+
function readExistingReviewRequestFile(worktreePath) {
|
|
11299
|
+
return readTextIfPresent((0, import_node_path28.join)(worktreePath, REVIEW_SKILL_PATH)) ?? readTextIfPresent((0, import_node_path28.join)(worktreePath, REPO_REVIEW_PATH)) ?? readTextIfPresent((0, import_node_path28.join)(worktreePath, REVIEW_REQUEST_PATH)) ?? readTextIfPresent((0, import_node_path28.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
|
|
11300
|
+
}
|
|
11301
|
+
async function requestReview(threadRef, send2) {
|
|
11302
|
+
const from = findThreadByRef(threadRef);
|
|
11303
|
+
if (!from) throw new Error(`Thread not found: ${threadRef}`);
|
|
11304
|
+
if (isOrchestratorThread(from)) {
|
|
11305
|
+
throw new Error(
|
|
11306
|
+
"request_review targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
|
|
11307
|
+
);
|
|
11577
11308
|
}
|
|
11578
|
-
|
|
11579
|
-
|
|
11580
|
-
"## Compacted context (extractive)",
|
|
11581
|
-
"",
|
|
11582
|
-
"### User goals",
|
|
11583
|
-
...users.length ? users.map((u) => `- ${u}`) : ["- (none captured)"],
|
|
11584
|
-
"",
|
|
11585
|
-
"### Recent agent notes",
|
|
11586
|
-
...agents.length ? agents.map((a) => `- ${a}`) : ["- (none captured)"]
|
|
11587
|
-
];
|
|
11588
|
-
if (uniqueTools.length) {
|
|
11589
|
-
parts.push("", "### Tools / files", ...uniqueTools.map((t) => `- ${t}`));
|
|
11309
|
+
if (from.status === "archived") {
|
|
11310
|
+
throw new Error(`Thread is archived: ${from.id}`);
|
|
11590
11311
|
}
|
|
11591
|
-
|
|
11592
|
-
|
|
11593
|
-
|
|
11594
|
-
|
|
11595
|
-
|
|
11596
|
-
|
|
11597
|
-
|
|
11598
|
-
|
|
11599
|
-
|
|
11600
|
-
|
|
11601
|
-
|
|
11312
|
+
const guidelines = resolveReviewGuidelines(from.worktreePath);
|
|
11313
|
+
const tab = createChatTab({
|
|
11314
|
+
fromThreadId: from.id,
|
|
11315
|
+
title: "Review",
|
|
11316
|
+
attachments: [
|
|
11317
|
+
buildReviewRequestAttachment(guidelines.content, {
|
|
11318
|
+
path: guidelines.path,
|
|
11319
|
+
name: guidelines.name
|
|
11320
|
+
})
|
|
11321
|
+
]
|
|
11322
|
+
});
|
|
11323
|
+
const started = await send2(tab.id, REVIEW_REQUEST_PREFILL);
|
|
11324
|
+
return { tab: started, from };
|
|
11602
11325
|
}
|
|
11603
|
-
var
|
|
11604
|
-
var
|
|
11605
|
-
"src/
|
|
11326
|
+
var import_node_crypto6, import_node_fs29, import_node_path28, REPO_REVIEW_PATH, REPO_REVIEW_NAME, REVIEW_REQUEST_PATH, LEGACY_REVIEW_REQUEST_PATH, REVIEW_REQUEST_NAME, REVIEW_REQUEST_PREFILL, LEGACY_REVIEW_TEMPLATE_MARKERS;
|
|
11327
|
+
var init_request_review = __esm({
|
|
11328
|
+
"src/review/request-review.ts"() {
|
|
11606
11329
|
"use strict";
|
|
11607
|
-
|
|
11608
|
-
|
|
11609
|
-
|
|
11610
|
-
|
|
11330
|
+
import_node_crypto6 = require("crypto");
|
|
11331
|
+
import_node_fs29 = require("fs");
|
|
11332
|
+
import_node_path28 = require("path");
|
|
11333
|
+
init_global_workspace();
|
|
11334
|
+
init_chat_tabs();
|
|
11335
|
+
init_thread_store();
|
|
11336
|
+
init_review_request_template();
|
|
11337
|
+
init_workspace_scratch();
|
|
11338
|
+
REPO_REVIEW_PATH = ".sideboard/review.md";
|
|
11339
|
+
REPO_REVIEW_NAME = "review.md";
|
|
11340
|
+
REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
|
|
11341
|
+
LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
|
|
11342
|
+
REVIEW_REQUEST_NAME = "Review request.md";
|
|
11343
|
+
REVIEW_REQUEST_PREFILL = "Review changes in this workspace.";
|
|
11344
|
+
LEGACY_REVIEW_TEMPLATE_MARKERS = [
|
|
11345
|
+
"You are acting as a reviewer for a proposed code change made by another engineer.",
|
|
11346
|
+
"HOW MANY FINDINGS TO RETURN:"
|
|
11347
|
+
];
|
|
11611
11348
|
}
|
|
11612
11349
|
});
|
|
11613
11350
|
|
|
11614
|
-
// src/
|
|
11615
|
-
function
|
|
11616
|
-
|
|
11617
|
-
|
|
11618
|
-
n += estimatePartChars(part);
|
|
11619
|
-
}
|
|
11620
|
-
return n;
|
|
11351
|
+
// src/hook/conductor.ts
|
|
11352
|
+
function matchSimpleGlob(pattern, name) {
|
|
11353
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
11354
|
+
return new RegExp(`^${escaped}$`).test(name);
|
|
11621
11355
|
}
|
|
11622
|
-
function
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
11356
|
+
function readWorktreeInclude(repoPath) {
|
|
11357
|
+
const path2 = (0, import_node_path29.join)(repoPath, ".worktreeinclude");
|
|
11358
|
+
if (!(0, import_node_fs30.existsSync)(path2)) return [];
|
|
11359
|
+
return (0, import_node_fs30.readFileSync)(path2, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
11360
|
+
}
|
|
11361
|
+
function resolveFilesToCopy(repoPath) {
|
|
11362
|
+
const fromInclude = readWorktreeInclude(repoPath);
|
|
11363
|
+
if (fromInclude.length) return fromInclude;
|
|
11364
|
+
const settings = loadRepoSettings(repoPath);
|
|
11365
|
+
if (settings?.filesToCopy?.length) return settings.filesToCopy;
|
|
11366
|
+
if (settings?.fileIncludeGlobs?.length) {
|
|
11367
|
+
const matched = [];
|
|
11368
|
+
try {
|
|
11369
|
+
for (const entry of (0, import_node_fs30.readdirSync)(repoPath, { withFileTypes: true })) {
|
|
11370
|
+
if (!entry.isFile()) continue;
|
|
11371
|
+
for (const glob of settings.fileIncludeGlobs) {
|
|
11372
|
+
if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob((0, import_node_path29.basename)(glob), entry.name)) {
|
|
11373
|
+
matched.push(entry.name);
|
|
11374
|
+
break;
|
|
11375
|
+
}
|
|
11376
|
+
}
|
|
11377
|
+
}
|
|
11378
|
+
} catch {
|
|
11630
11379
|
}
|
|
11631
|
-
|
|
11632
|
-
return 0;
|
|
11380
|
+
if (matched.length) return [...new Set(matched)];
|
|
11633
11381
|
}
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
11640
|
-
|
|
11641
|
-
|
|
11642
|
-
return estimateThreadChars(messages) >= maxChars;
|
|
11643
|
-
}
|
|
11644
|
-
function splitForCompaction(messages, thresholds = {}) {
|
|
11645
|
-
const keepChars = thresholds.keepRecentChars ?? CONTEXT_KEEP_RECENT_CHARS;
|
|
11646
|
-
const keepCount = thresholds.keepRecentMessages ?? CONTEXT_KEEP_RECENT_MESSAGES;
|
|
11647
|
-
if (messages.length === 0) return { older: [], recent: [] };
|
|
11648
|
-
let recentChars = 0;
|
|
11649
|
-
let cut = messages.length;
|
|
11650
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
11651
|
-
const next = recentChars + estimateMessageChars(messages[i]);
|
|
11652
|
-
const kept = messages.length - i;
|
|
11653
|
-
if (kept >= keepCount && next > keepChars) break;
|
|
11654
|
-
recentChars = next;
|
|
11655
|
-
cut = i;
|
|
11382
|
+
const defaults = [];
|
|
11383
|
+
try {
|
|
11384
|
+
for (const entry of (0, import_node_fs30.readdirSync)(repoPath, { withFileTypes: true })) {
|
|
11385
|
+
if (entry.isFile() && entry.name.startsWith(".env")) {
|
|
11386
|
+
defaults.push(entry.name);
|
|
11387
|
+
}
|
|
11388
|
+
}
|
|
11389
|
+
} catch {
|
|
11656
11390
|
}
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
if (cut <= 0) return { older: [], recent: messages };
|
|
11660
|
-
return {
|
|
11661
|
-
older: messages.slice(0, cut),
|
|
11662
|
-
recent: messages.slice(cut)
|
|
11663
|
-
};
|
|
11391
|
+
if (defaults.length) return defaults;
|
|
11392
|
+
return [".env.local", ".env"];
|
|
11664
11393
|
}
|
|
11665
|
-
function
|
|
11666
|
-
|
|
11667
|
-
|
|
11668
|
-
|
|
11669
|
-
|
|
11394
|
+
function copyConfiguredFiles(repoPath, worktreePath) {
|
|
11395
|
+
const patterns = resolveFilesToCopy(repoPath);
|
|
11396
|
+
const copied = [];
|
|
11397
|
+
for (const rel of patterns) {
|
|
11398
|
+
const src = (0, import_node_path29.join)(repoPath, rel);
|
|
11399
|
+
if (!(0, import_node_fs30.existsSync)(src)) continue;
|
|
11400
|
+
const dest = (0, import_node_path29.join)(worktreePath, rel);
|
|
11401
|
+
(0, import_node_fs30.mkdirSync)((0, import_node_path29.dirname)(dest), { recursive: true });
|
|
11402
|
+
(0, import_node_fs30.copyFileSync)(src, dest);
|
|
11403
|
+
copied.push(rel);
|
|
11670
11404
|
}
|
|
11671
|
-
|
|
11672
|
-
if (t.description) lines.push(`Description: ${t.description}`);
|
|
11673
|
-
if (t.filePath) lines.push(`Path: ${t.filePath}`);
|
|
11674
|
-
if (t.input && Object.keys(t.input).length > 0) {
|
|
11675
|
-
lines.push("Input:");
|
|
11676
|
-
lines.push("```json");
|
|
11677
|
-
lines.push(JSON.stringify(t.input, null, 2));
|
|
11678
|
-
lines.push("```");
|
|
11679
|
-
} else if (t.detail) {
|
|
11680
|
-
lines.push(`Detail: ${t.detail}`);
|
|
11681
|
-
}
|
|
11682
|
-
if (t.result != null && t.result !== "") {
|
|
11683
|
-
lines.push("Result:");
|
|
11684
|
-
lines.push("```");
|
|
11685
|
-
lines.push(t.result);
|
|
11686
|
-
lines.push("```");
|
|
11687
|
-
}
|
|
11688
|
-
return lines.join("\n");
|
|
11405
|
+
return copied;
|
|
11689
11406
|
}
|
|
11690
|
-
function
|
|
11691
|
-
|
|
11692
|
-
const
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
|
|
11696
|
-
|
|
11697
|
-
|
|
11698
|
-
|
|
11699
|
-
|
|
11700
|
-
|
|
11701
|
-
|
|
11702
|
-
|
|
11703
|
-
|
|
11704
|
-
|
|
11705
|
-
|
|
11706
|
-
|
|
11707
|
-
${m.text}`);
|
|
11708
|
-
} else {
|
|
11709
|
-
bits.push("### Agent");
|
|
11710
|
-
}
|
|
11711
|
-
const thinking = (m.parts ?? []).filter((p) => p.type === "thinking");
|
|
11712
|
-
for (const th of thinking) {
|
|
11713
|
-
if (th.type !== "thinking" || !th.text.trim()) continue;
|
|
11714
|
-
bits.push("Thinking:");
|
|
11715
|
-
bits.push(th.text);
|
|
11716
|
-
}
|
|
11717
|
-
const toolParts = (m.parts ?? []).filter(
|
|
11718
|
-
(p) => p.type === "tool"
|
|
11719
|
-
);
|
|
11720
|
-
if (tools !== "none" && toolParts.length > 0) {
|
|
11721
|
-
if (tools === "summary") bits.push("Tools:");
|
|
11722
|
-
for (const t of toolParts) {
|
|
11723
|
-
bits.push(formatToolPart(t, tools));
|
|
11407
|
+
async function captureLoginEnv() {
|
|
11408
|
+
if (cachedLoginEnv) return { ...cachedLoginEnv };
|
|
11409
|
+
const shell = process.env.SHELL?.trim() || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
|
|
11410
|
+
try {
|
|
11411
|
+
const result = await (0, import_execa4.execa)(shell, ["-l", "-c", "env -0"], {
|
|
11412
|
+
reject: false,
|
|
11413
|
+
timeout: 5e3
|
|
11414
|
+
});
|
|
11415
|
+
if (result.exitCode === 0 && result.stdout) {
|
|
11416
|
+
const env = { ...process.env };
|
|
11417
|
+
for (const entry of result.stdout.split("\0")) {
|
|
11418
|
+
if (!entry) continue;
|
|
11419
|
+
const eq = entry.indexOf("=");
|
|
11420
|
+
if (eq <= 0) continue;
|
|
11421
|
+
const key = entry.slice(0, eq);
|
|
11422
|
+
const value = entry.slice(eq + 1);
|
|
11423
|
+
if (key) env[key] = value;
|
|
11724
11424
|
}
|
|
11425
|
+
cachedLoginEnv = env;
|
|
11426
|
+
return { ...env };
|
|
11725
11427
|
}
|
|
11726
|
-
|
|
11428
|
+
} catch {
|
|
11727
11429
|
}
|
|
11728
|
-
|
|
11430
|
+
cachedLoginEnv = { ...process.env };
|
|
11431
|
+
return { ...cachedLoginEnv };
|
|
11729
11432
|
}
|
|
11730
|
-
function
|
|
11731
|
-
|
|
11732
|
-
|
|
11733
|
-
|
|
11734
|
-
|
|
11735
|
-
|
|
11736
|
-
|
|
11737
|
-
|
|
11738
|
-
|
|
11739
|
-
|
|
11740
|
-
|
|
11433
|
+
function buildWorkspaceScriptEnv(opts, baseEnv) {
|
|
11434
|
+
const env = stripNestedElectronEnv({
|
|
11435
|
+
...baseEnv ?? process.env
|
|
11436
|
+
});
|
|
11437
|
+
const name = opts.workspaceName ?? (0, import_node_path29.basename)(opts.worktreePath);
|
|
11438
|
+
const ports = opts.ports ?? [];
|
|
11439
|
+
const primary = ports[0];
|
|
11440
|
+
env.SIDEBOARD_WORKSPACE_NAME = name;
|
|
11441
|
+
env.SIDEBOARD_WORKSPACE_PATH = opts.worktreePath;
|
|
11442
|
+
env.SIDEBOARD_ROOT_PATH = opts.repoPath;
|
|
11443
|
+
env.SIDEBOARD_IS_LOCAL = "1";
|
|
11444
|
+
if (opts.defaultBranch) env.SIDEBOARD_DEFAULT_BRANCH = opts.defaultBranch;
|
|
11445
|
+
env.CONDUCTOR_WORKSPACE_NAME = name;
|
|
11446
|
+
env.CONDUCTOR_WORKSPACE_PATH = opts.worktreePath;
|
|
11447
|
+
env.CONDUCTOR_ROOT_PATH = opts.repoPath;
|
|
11448
|
+
env.CONDUCTOR_IS_LOCAL = "1";
|
|
11449
|
+
if (opts.defaultBranch) env.CONDUCTOR_DEFAULT_BRANCH = opts.defaultBranch;
|
|
11450
|
+
if (primary != null) {
|
|
11451
|
+
env.SIDEBOARD_PORT = String(primary);
|
|
11452
|
+
env.CONDUCTOR_PORT = String(primary);
|
|
11453
|
+
env.PORT = String(primary);
|
|
11454
|
+
for (let i = 1; i < ports.length; i++) {
|
|
11455
|
+
const p = ports[i];
|
|
11456
|
+
env[`SIDEBOARD_PORT_${i}`] = String(p);
|
|
11457
|
+
env[`CONDUCTOR_PORT_${i}`] = String(p);
|
|
11458
|
+
}
|
|
11459
|
+
}
|
|
11460
|
+
return env;
|
|
11741
11461
|
}
|
|
11742
|
-
function
|
|
11743
|
-
|
|
11744
|
-
|
|
11745
|
-
|
|
11746
|
-
role: "summary",
|
|
11747
|
-
text: summaryText.trim(),
|
|
11748
|
-
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
11749
|
-
};
|
|
11750
|
-
return [summary, ...recent];
|
|
11462
|
+
function pipeLines(stream, onLine) {
|
|
11463
|
+
if (!stream || !onLine) return;
|
|
11464
|
+
const rl = (0, import_node_readline3.createInterface)({ input: stream });
|
|
11465
|
+
rl.on("line", onLine);
|
|
11751
11466
|
}
|
|
11752
|
-
|
|
11753
|
-
|
|
11754
|
-
|
|
11467
|
+
function killScriptTree(child, ports = []) {
|
|
11468
|
+
const pid = child.pid;
|
|
11469
|
+
if (pid) {
|
|
11470
|
+
try {
|
|
11471
|
+
if (process.platform === "win32") {
|
|
11472
|
+
void (0, import_execa4.execa)("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
|
|
11473
|
+
} else {
|
|
11474
|
+
process.kill(-pid, "SIGTERM");
|
|
11475
|
+
setTimeout(() => {
|
|
11476
|
+
try {
|
|
11477
|
+
process.kill(-pid, "SIGKILL");
|
|
11478
|
+
} catch {
|
|
11479
|
+
}
|
|
11480
|
+
}, 2500).unref?.();
|
|
11481
|
+
}
|
|
11482
|
+
} catch {
|
|
11483
|
+
try {
|
|
11484
|
+
child.kill("SIGTERM");
|
|
11485
|
+
} catch {
|
|
11486
|
+
}
|
|
11487
|
+
}
|
|
11755
11488
|
}
|
|
11756
|
-
const
|
|
11757
|
-
|
|
11758
|
-
|
|
11489
|
+
for (const port of ports) {
|
|
11490
|
+
if (!Number.isFinite(port) || port <= 0) continue;
|
|
11491
|
+
if (process.platform === "win32") {
|
|
11492
|
+
void (0, import_execa4.execa)(
|
|
11493
|
+
"powershell",
|
|
11494
|
+
[
|
|
11495
|
+
"-NoProfile",
|
|
11496
|
+
"-Command",
|
|
11497
|
+
`Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
|
|
11498
|
+
],
|
|
11499
|
+
{ reject: false }
|
|
11500
|
+
);
|
|
11501
|
+
} else {
|
|
11502
|
+
void (0, import_execa4.execa)(
|
|
11503
|
+
"zsh",
|
|
11504
|
+
["-lc", `pids=$(lsof -tiTCP:${port} -sTCP:LISTEN 2>/dev/null); [ -n "$pids" ] && kill -TERM $pids 2>/dev/null; true`],
|
|
11505
|
+
{ reject: false }
|
|
11506
|
+
);
|
|
11507
|
+
}
|
|
11759
11508
|
}
|
|
11760
|
-
|
|
11761
|
-
|
|
11762
|
-
|
|
11509
|
+
}
|
|
11510
|
+
async function spawnWorkspaceScript(command, opts) {
|
|
11511
|
+
const loginEnv = await captureLoginEnv();
|
|
11512
|
+
const env = buildWorkspaceScriptEnv(
|
|
11513
|
+
{
|
|
11514
|
+
worktreePath: opts.worktreePath,
|
|
11515
|
+
repoPath: opts.repoPath,
|
|
11516
|
+
workspaceName: opts.workspaceName,
|
|
11517
|
+
defaultBranch: opts.defaultBranch,
|
|
11518
|
+
ports: opts.ports
|
|
11519
|
+
},
|
|
11520
|
+
loginEnv
|
|
11521
|
+
);
|
|
11522
|
+
try {
|
|
11523
|
+
mergeAgentGitAuthEnv(
|
|
11524
|
+
env,
|
|
11525
|
+
await resolveAgentGitAuthEnv(env, { cwd: opts.worktreePath })
|
|
11526
|
+
);
|
|
11527
|
+
} catch {
|
|
11528
|
+
}
|
|
11529
|
+
const shell = process.platform === "darwin" ? "zsh" : "bash";
|
|
11530
|
+
const child = (0, import_execa4.execa)(shell, ["-lc", command], {
|
|
11531
|
+
cwd: opts.worktreePath,
|
|
11532
|
+
reject: false,
|
|
11533
|
+
env,
|
|
11534
|
+
// Own process group so Stop can tear down the whole tree (not just the shell).
|
|
11535
|
+
// There is no settings.toml `stop=` / teardown hook for run scripts.
|
|
11536
|
+
...process.platform === "win32" ? {} : { detached: true }
|
|
11763
11537
|
});
|
|
11764
|
-
|
|
11765
|
-
|
|
11766
|
-
|
|
11767
|
-
messages,
|
|
11768
|
-
sessionId: null,
|
|
11769
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11770
|
-
};
|
|
11538
|
+
pipeLines(child.stdout, opts.onLine);
|
|
11539
|
+
pipeLines(child.stderr, opts.onLine);
|
|
11540
|
+
const ports = opts.ports ?? [];
|
|
11771
11541
|
return {
|
|
11772
|
-
|
|
11773
|
-
|
|
11774
|
-
|
|
11775
|
-
|
|
11776
|
-
olderCount: older.length
|
|
11542
|
+
pid: child.pid,
|
|
11543
|
+
kill: () => killScriptTree(child, ports),
|
|
11544
|
+
done: child.then((r) => r.exitCode ?? null),
|
|
11545
|
+
child
|
|
11777
11546
|
};
|
|
11778
11547
|
}
|
|
11779
|
-
|
|
11780
|
-
|
|
11781
|
-
|
|
11782
|
-
|
|
11783
|
-
|
|
11784
|
-
CONTEXT_COMPACT_CHARS = 1e5;
|
|
11785
|
-
CONTEXT_KEEP_RECENT_CHARS = 24e3;
|
|
11786
|
-
CONTEXT_KEEP_RECENT_MESSAGES = 12;
|
|
11787
|
-
CONTEXT_MIN_MESSAGES = 10;
|
|
11788
|
-
}
|
|
11789
|
-
});
|
|
11790
|
-
|
|
11791
|
-
// src/threads/chat-tabs.ts
|
|
11792
|
-
function sameWorktreePath(a, b) {
|
|
11793
|
-
return normalizeWorktreePath(a) === normalizeWorktreePath(b);
|
|
11794
|
-
}
|
|
11795
|
-
function takenTeamSlugsForChatTab(worktreePath) {
|
|
11796
|
-
const key = normalizeWorktreePath(worktreePath);
|
|
11797
|
-
const taken = /* @__PURE__ */ new Set();
|
|
11798
|
-
const dir = worktreeNameFromPath(key);
|
|
11799
|
-
if (dir) taken.add(dir.toLowerCase());
|
|
11800
|
-
for (const sibling of threadsSharingWorktree(key)) {
|
|
11801
|
-
for (const slug of takenSlugsFromThread(sibling)) {
|
|
11802
|
-
taken.add(slug);
|
|
11803
|
-
}
|
|
11548
|
+
async function attachAbort(handle, signal) {
|
|
11549
|
+
if (signal) {
|
|
11550
|
+
const onAbort = () => handle.kill();
|
|
11551
|
+
if (signal.aborted) onAbort();
|
|
11552
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
11804
11553
|
}
|
|
11805
|
-
return
|
|
11806
|
-
}
|
|
11807
|
-
function requireThread(idOrRef) {
|
|
11808
|
-
const thread = findThreadByRef(idOrRef) ?? null;
|
|
11809
|
-
if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
|
|
11810
|
-
return thread;
|
|
11554
|
+
return handle.done;
|
|
11811
11555
|
}
|
|
11812
|
-
function
|
|
11556
|
+
async function runSetupScript(repoPath, worktreePath, onLine, opts) {
|
|
11557
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
11558
|
+
if (!settings?.setup) return { ran: false, exitCode: null, source: null };
|
|
11559
|
+
const handle = await spawnWorkspaceScript(settings.setup, {
|
|
11560
|
+
worktreePath,
|
|
11561
|
+
repoPath,
|
|
11562
|
+
defaultBranch: opts?.defaultBranch,
|
|
11563
|
+
onLine
|
|
11564
|
+
});
|
|
11565
|
+
const exitCode = await attachAbort(handle, opts?.signal);
|
|
11813
11566
|
return {
|
|
11814
|
-
|
|
11815
|
-
|
|
11816
|
-
|
|
11817
|
-
|
|
11818
|
-
// Legacy pinned-repo orchestration: new tabs become normal worktree agents.
|
|
11819
|
-
// Global: keep orchestration so chat tabs stay fleet orchestrators (MCP + Bash).
|
|
11820
|
-
sourceType: isGlobalThread(from) ? "orchestration" : from.sourceType === "orchestration" ? "branch" : from.sourceType,
|
|
11821
|
-
sourceIsFork: from.sourceIsFork,
|
|
11822
|
-
parentThreadId: from.parentThreadId,
|
|
11823
|
-
prUrl: from.prUrl,
|
|
11824
|
-
prTitle: from.prTitle,
|
|
11825
|
-
prState: from.prState,
|
|
11826
|
-
skipAutoArchiveOnMerge: from.skipAutoArchiveOnMerge,
|
|
11827
|
-
stackId: from.stackId,
|
|
11828
|
-
stackLayer: from.stackLayer
|
|
11567
|
+
ran: true,
|
|
11568
|
+
exitCode,
|
|
11569
|
+
source: workspaceSettingsSourceLabel(worktreePath, repoPath),
|
|
11570
|
+
kill: handle.kill
|
|
11829
11571
|
};
|
|
11830
11572
|
}
|
|
11831
|
-
function
|
|
11832
|
-
|
|
11833
|
-
|
|
11834
|
-
|
|
11835
|
-
|
|
11836
|
-
|
|
11837
|
-
|
|
11838
|
-
|
|
11839
|
-
|
|
11840
|
-
|
|
11841
|
-
|
|
11842
|
-
function forkMessageSlice(from, throughIndex) {
|
|
11843
|
-
const through = throughIndex === void 0 ? from.messages.length - 1 : Math.min(throughIndex, from.messages.length - 1);
|
|
11844
|
-
return through >= 0 ? from.messages.slice(0, through + 1) : [];
|
|
11845
|
-
}
|
|
11846
|
-
function buildForkTranscriptAttachment(baseTitle, messages) {
|
|
11847
|
-
const title = baseTitle || "Chat";
|
|
11573
|
+
async function runConventionSetup(repoPath, worktreePath, onLine, opts) {
|
|
11574
|
+
const found = findConventionSetup(worktreePath, repoPath);
|
|
11575
|
+
if (!found) return { ran: false, exitCode: null, source: null };
|
|
11576
|
+
onLine?.(`[setup] ${found.source}`);
|
|
11577
|
+
const handle = await spawnWorkspaceScript(found.command, {
|
|
11578
|
+
worktreePath,
|
|
11579
|
+
repoPath,
|
|
11580
|
+
defaultBranch: opts?.defaultBranch,
|
|
11581
|
+
onLine
|
|
11582
|
+
});
|
|
11583
|
+
const exitCode = await attachAbort(handle, opts?.signal);
|
|
11848
11584
|
return {
|
|
11849
|
-
|
|
11850
|
-
|
|
11851
|
-
|
|
11852
|
-
|
|
11585
|
+
ran: true,
|
|
11586
|
+
exitCode,
|
|
11587
|
+
source: found.source,
|
|
11588
|
+
kill: handle.kill
|
|
11853
11589
|
};
|
|
11854
11590
|
}
|
|
11855
|
-
function
|
|
11856
|
-
const
|
|
11857
|
-
|
|
11858
|
-
|
|
11859
|
-
|
|
11860
|
-
|
|
11861
|
-
if (isOrchestratorThread(from) || binding.sourceType === "orchestration") {
|
|
11862
|
-
assertOrchestratorCapableAgent(nextAgent);
|
|
11591
|
+
async function runWorkspaceSetup(repoPath, worktreePath, onLine, opts) {
|
|
11592
|
+
const reviewSkill = ensureReviewSkillFile(worktreePath);
|
|
11593
|
+
if (reviewSkill.wrote) {
|
|
11594
|
+
onLine?.(
|
|
11595
|
+
`[setup] wrote ${REVIEW_SKILL_PATH} \u2014 commit it so later worktrees inherit Review guidelines`
|
|
11596
|
+
);
|
|
11863
11597
|
}
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
-
|
|
11870
|
-
|
|
11871
|
-
|
|
11872
|
-
|
|
11873
|
-
|
|
11874
|
-
|
|
11875
|
-
|
|
11876
|
-
|
|
11877
|
-
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
planMode: from.planMode,
|
|
11881
|
-
autonomy: input.autonomy ?? from.autonomy,
|
|
11882
|
-
attachments: input.attachments ?? [],
|
|
11883
|
-
status: "idle"
|
|
11598
|
+
let setup = await runSetupScript(repoPath, worktreePath, onLine, opts);
|
|
11599
|
+
if (!setup.ran) {
|
|
11600
|
+
setup = await runCursorWorktreeSetup(repoPath, worktreePath, onLine);
|
|
11601
|
+
}
|
|
11602
|
+
if (!setup.ran) {
|
|
11603
|
+
setup = await runConventionSetup(repoPath, worktreePath, onLine, opts);
|
|
11604
|
+
}
|
|
11605
|
+
return setup;
|
|
11606
|
+
}
|
|
11607
|
+
async function runArchiveScript(repoPath, worktreePath, onLine) {
|
|
11608
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
11609
|
+
if (!settings?.archive) return { ran: false, exitCode: null };
|
|
11610
|
+
const handle = await spawnWorkspaceScript(settings.archive, {
|
|
11611
|
+
worktreePath,
|
|
11612
|
+
repoPath,
|
|
11613
|
+
onLine
|
|
11884
11614
|
});
|
|
11885
|
-
|
|
11886
|
-
return
|
|
11615
|
+
const exitCode = await handle.done;
|
|
11616
|
+
return { ran: true, exitCode };
|
|
11887
11617
|
}
|
|
11888
|
-
function
|
|
11889
|
-
const
|
|
11890
|
-
|
|
11891
|
-
|
|
11892
|
-
|
|
11893
|
-
|
|
11894
|
-
agent: input.agent ?? from.agent,
|
|
11895
|
-
model: input.model,
|
|
11896
|
-
title: input.title?.trim() || void 0,
|
|
11897
|
-
attachments: [attachment]
|
|
11618
|
+
function listRunScripts(worktreePath, repoPath) {
|
|
11619
|
+
const settings = loadWorkspaceSettings(worktreePath, repoPath);
|
|
11620
|
+
if (!settings?.runScripts.length) return [];
|
|
11621
|
+
return settings.runScripts.filter((s) => {
|
|
11622
|
+
if (!s.availableIn?.length) return true;
|
|
11623
|
+
return s.availableIn.includes("local");
|
|
11898
11624
|
});
|
|
11899
|
-
if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
|
|
11900
|
-
const next = { ...tab, parentThreadId: from.id };
|
|
11901
|
-
writeThread(next);
|
|
11902
|
-
return next;
|
|
11903
|
-
}
|
|
11904
|
-
return tab;
|
|
11905
11625
|
}
|
|
11906
|
-
|
|
11907
|
-
|
|
11908
|
-
|
|
11909
|
-
|
|
11910
|
-
import_node_crypto5 = require("crypto");
|
|
11911
|
-
init_context_compact();
|
|
11912
|
-
init_teams();
|
|
11913
|
-
init_worktree_labels();
|
|
11914
|
-
init_global_workspace();
|
|
11915
|
-
init_orchestrator_capable();
|
|
11916
|
-
init_thread_store();
|
|
11917
|
-
init_worktree_labels();
|
|
11918
|
-
}
|
|
11919
|
-
});
|
|
11920
|
-
|
|
11921
|
-
// src/store/turn-live.ts
|
|
11922
|
-
function trimParts(parts) {
|
|
11923
|
-
if (parts.length <= MAX_PARTS) return parts;
|
|
11924
|
-
return parts.slice(-Math.floor(MAX_PARTS / 2));
|
|
11626
|
+
function getDefaultRunScript(worktreePath, repoPath) {
|
|
11627
|
+
const scripts = listRunScripts(worktreePath, repoPath);
|
|
11628
|
+
if (!scripts.length) return null;
|
|
11629
|
+
return scripts.find((s) => s.default === true) ?? scripts.find((s) => s.name === "dev") ?? scripts.find((s) => s.name !== "all") ?? scripts[0] ?? null;
|
|
11925
11630
|
}
|
|
11926
|
-
function
|
|
11927
|
-
|
|
11928
|
-
|
|
11929
|
-
|
|
11631
|
+
function getRunScript(worktreePath, repoPath, name) {
|
|
11632
|
+
const scripts = listRunScripts(worktreePath, repoPath);
|
|
11633
|
+
if (!scripts.length) return null;
|
|
11634
|
+
if (name) {
|
|
11635
|
+
return scripts.find((s) => s.name === name) ?? null;
|
|
11930
11636
|
}
|
|
11931
|
-
return
|
|
11637
|
+
return getDefaultRunScript(worktreePath, repoPath);
|
|
11932
11638
|
}
|
|
11933
|
-
function
|
|
11934
|
-
|
|
11935
|
-
|
|
11936
|
-
|
|
11937
|
-
|
|
11938
|
-
|
|
11939
|
-
|
|
11940
|
-
|
|
11941
|
-
|
|
11942
|
-
|
|
11943
|
-
|
|
11944
|
-
|
|
11945
|
-
|
|
11946
|
-
|
|
11947
|
-
|
|
11948
|
-
|
|
11949
|
-
|
|
11950
|
-
}
|
|
11951
|
-
|
|
11639
|
+
function getRunMode(worktreePath, repoPath) {
|
|
11640
|
+
return loadWorkspaceSettings(worktreePath, repoPath)?.runMode ?? "concurrent";
|
|
11641
|
+
}
|
|
11642
|
+
async function allocatePort() {
|
|
11643
|
+
return new Promise((resolve, reject) => {
|
|
11644
|
+
const server = (0, import_node_net.createServer)();
|
|
11645
|
+
server.listen(0, "127.0.0.1", () => {
|
|
11646
|
+
const addr = server.address();
|
|
11647
|
+
if (!addr || typeof addr === "string") {
|
|
11648
|
+
server.close();
|
|
11649
|
+
reject(new Error("Failed to allocate port"));
|
|
11650
|
+
return;
|
|
11651
|
+
}
|
|
11652
|
+
const port = addr.port;
|
|
11653
|
+
server.close((err) => err ? reject(err) : resolve(port));
|
|
11654
|
+
});
|
|
11655
|
+
server.on("error", reject);
|
|
11656
|
+
});
|
|
11657
|
+
}
|
|
11658
|
+
async function allocatePortRange(size = PORT_RANGE_SIZE) {
|
|
11659
|
+
const base = await allocatePort();
|
|
11660
|
+
const ports = [base];
|
|
11661
|
+
for (let i = 1; i < size; i++) {
|
|
11662
|
+
const candidate = base + i;
|
|
11663
|
+
const free = await new Promise((resolve) => {
|
|
11664
|
+
const server = (0, import_node_net.createServer)();
|
|
11665
|
+
server.once("error", () => resolve(false));
|
|
11666
|
+
server.listen(candidate, "127.0.0.1", () => {
|
|
11667
|
+
server.close(() => resolve(true));
|
|
11668
|
+
});
|
|
11669
|
+
});
|
|
11670
|
+
if (free) {
|
|
11671
|
+
ports.push(candidate);
|
|
11672
|
+
} else {
|
|
11673
|
+
ports.push(await allocatePort());
|
|
11674
|
+
}
|
|
11952
11675
|
}
|
|
11676
|
+
return ports;
|
|
11677
|
+
}
|
|
11678
|
+
async function startDevServer(repoPath, worktreePath, onLine, opts) {
|
|
11679
|
+
const script = getRunScript(worktreePath, repoPath, opts?.scriptName);
|
|
11680
|
+
if (!script) return null;
|
|
11681
|
+
const ports = await allocatePortRange(PORT_RANGE_SIZE);
|
|
11682
|
+
const handle = await spawnWorkspaceScript(script.command, {
|
|
11683
|
+
worktreePath,
|
|
11684
|
+
repoPath,
|
|
11685
|
+
ports,
|
|
11686
|
+
defaultBranch: opts?.defaultBranch,
|
|
11687
|
+
onLine
|
|
11688
|
+
});
|
|
11953
11689
|
return {
|
|
11954
|
-
|
|
11955
|
-
|
|
11956
|
-
|
|
11957
|
-
|
|
11958
|
-
|
|
11690
|
+
pid: handle.pid,
|
|
11691
|
+
port: ports[0],
|
|
11692
|
+
ports,
|
|
11693
|
+
scriptName: script.name,
|
|
11694
|
+
kill: handle.kill,
|
|
11695
|
+
done: handle.done
|
|
11959
11696
|
};
|
|
11960
11697
|
}
|
|
11961
|
-
|
|
11962
|
-
|
|
11963
|
-
|
|
11964
|
-
|
|
11965
|
-
|
|
11966
|
-
|
|
11698
|
+
var import_node_fs30, import_node_net, import_node_path29, import_execa4, import_node_readline3, PORT_RANGE_SIZE, cachedLoginEnv;
|
|
11699
|
+
var init_conductor = __esm({
|
|
11700
|
+
"src/hook/conductor.ts"() {
|
|
11701
|
+
"use strict";
|
|
11702
|
+
import_node_fs30 = require("fs");
|
|
11703
|
+
import_node_net = require("net");
|
|
11704
|
+
import_node_path29 = require("path");
|
|
11705
|
+
import_execa4 = require("execa");
|
|
11706
|
+
import_node_readline3 = require("readline");
|
|
11707
|
+
init_settings();
|
|
11708
|
+
init_nested_electron_env();
|
|
11709
|
+
init_convention_setup();
|
|
11710
|
+
init_cursor_worktrees();
|
|
11711
|
+
init_git_auth_mode();
|
|
11712
|
+
init_request_review();
|
|
11713
|
+
init_nested_electron_env();
|
|
11714
|
+
PORT_RANGE_SIZE = 10;
|
|
11715
|
+
cachedLoginEnv = null;
|
|
11967
11716
|
}
|
|
11968
|
-
|
|
11969
|
-
|
|
11970
|
-
|
|
11971
|
-
|
|
11972
|
-
|
|
11973
|
-
if (buf.timer) return;
|
|
11974
|
-
buf.timer = setTimeout(() => {
|
|
11975
|
-
const live = buffers.get(threadId);
|
|
11976
|
-
if (live) live.timer = null;
|
|
11977
|
-
flush(threadId);
|
|
11978
|
-
}, FLUSH_MS);
|
|
11717
|
+
});
|
|
11718
|
+
|
|
11719
|
+
// src/git/orphan-cleanup.ts
|
|
11720
|
+
function isSideboardWorktreePath(path2) {
|
|
11721
|
+
return path2.includes("/.sideboard/worktrees/") || path2.includes("/sideboard/workspaces/");
|
|
11979
11722
|
}
|
|
11980
|
-
function
|
|
11981
|
-
|
|
11982
|
-
|
|
11723
|
+
async function findOrphanWorktrees(repoPaths) {
|
|
11724
|
+
const threads = listThreads({ includeArchived: true });
|
|
11725
|
+
const known = new Set(threads.map((t) => t.worktreePath.replace(/\/$/, "")));
|
|
11726
|
+
const repos = new Set(
|
|
11727
|
+
repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
|
|
11728
|
+
);
|
|
11729
|
+
const homeRoot = sideboardWorkspacesDir();
|
|
11730
|
+
if ((0, import_node_fs31.existsSync)(homeRoot)) {
|
|
11731
|
+
try {
|
|
11732
|
+
for (const entry of (0, import_node_fs31.readdirSync)(homeRoot, { withFileTypes: true })) {
|
|
11733
|
+
if (!entry.isDirectory()) continue;
|
|
11734
|
+
void entry;
|
|
11735
|
+
}
|
|
11736
|
+
} catch {
|
|
11737
|
+
}
|
|
11983
11738
|
}
|
|
11984
|
-
const
|
|
11985
|
-
|
|
11986
|
-
|
|
11987
|
-
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
11992
|
-
|
|
11739
|
+
const orphans = [];
|
|
11740
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11741
|
+
for (const repoPath of repos) {
|
|
11742
|
+
if (!repoPath || !(0, import_node_fs31.existsSync)(repoPath)) continue;
|
|
11743
|
+
try {
|
|
11744
|
+
const wts = await listWorktrees(repoPath);
|
|
11745
|
+
for (const wt of wts) {
|
|
11746
|
+
const path2 = wt.path.replace(/\/$/, "");
|
|
11747
|
+
if (!isSideboardWorktreePath(path2)) continue;
|
|
11748
|
+
if (known.has(path2)) continue;
|
|
11749
|
+
if (seen.has(path2)) continue;
|
|
11750
|
+
seen.add(path2);
|
|
11751
|
+
let mtimeMs = 0;
|
|
11752
|
+
try {
|
|
11753
|
+
mtimeMs = (0, import_node_fs31.statSync)(path2).mtimeMs;
|
|
11754
|
+
} catch {
|
|
11755
|
+
mtimeMs = 0;
|
|
11756
|
+
}
|
|
11757
|
+
orphans.push({ path: path2, repoPath, mtimeMs });
|
|
11758
|
+
}
|
|
11759
|
+
} catch {
|
|
11760
|
+
}
|
|
11761
|
+
try {
|
|
11762
|
+
const root = worktreesRoot(repoPath);
|
|
11763
|
+
if ((0, import_node_fs31.existsSync)(root)) {
|
|
11764
|
+
for (const entry of (0, import_node_fs31.readdirSync)(root, { withFileTypes: true })) {
|
|
11765
|
+
if (!entry.isDirectory()) continue;
|
|
11766
|
+
const path2 = (0, import_node_path30.join)(root, entry.name).replace(/\/$/, "");
|
|
11767
|
+
if (known.has(path2) || seen.has(path2)) continue;
|
|
11768
|
+
if (!(0, import_node_fs31.existsSync)((0, import_node_path30.join)(path2, ".git"))) continue;
|
|
11769
|
+
seen.add(path2);
|
|
11770
|
+
let mtimeMs = 0;
|
|
11771
|
+
try {
|
|
11772
|
+
mtimeMs = (0, import_node_fs31.statSync)(path2).mtimeMs;
|
|
11773
|
+
} catch {
|
|
11774
|
+
mtimeMs = Date.now();
|
|
11775
|
+
}
|
|
11776
|
+
orphans.push({ path: path2, repoPath, mtimeMs });
|
|
11777
|
+
}
|
|
11778
|
+
}
|
|
11779
|
+
} catch {
|
|
11780
|
+
}
|
|
11993
11781
|
}
|
|
11994
|
-
|
|
11995
|
-
if (immediate) flush(threadId);
|
|
11996
|
-
else scheduleFlush(threadId);
|
|
11782
|
+
return orphans.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
11997
11783
|
}
|
|
11998
|
-
function
|
|
11999
|
-
const
|
|
12000
|
-
const
|
|
12001
|
-
|
|
12002
|
-
|
|
12003
|
-
|
|
12004
|
-
|
|
11784
|
+
async function cleanupOrphanWorktrees(opts) {
|
|
11785
|
+
const settings = loadAppSettings();
|
|
11786
|
+
const maxCount = opts?.maxCount ?? settings.advanced.worktreeMaxCount ?? 25;
|
|
11787
|
+
const orphans = await findOrphanWorktrees(opts?.repoPaths);
|
|
11788
|
+
const activeCount = listThreads().filter((t) => t.status !== "archived").length;
|
|
11789
|
+
const budget = Math.max(0, maxCount - activeCount);
|
|
11790
|
+
const sortedNewestFirst = [...orphans].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
11791
|
+
const keep = new Set(sortedNewestFirst.slice(0, budget).map((o) => o.path));
|
|
11792
|
+
const removed = [];
|
|
11793
|
+
const kept = [];
|
|
11794
|
+
for (const orphan of orphans) {
|
|
11795
|
+
if (keep.has(orphan.path)) {
|
|
11796
|
+
kept.push(orphan.path);
|
|
11797
|
+
continue;
|
|
11798
|
+
}
|
|
11799
|
+
if (opts?.dryRun) {
|
|
11800
|
+
removed.push(orphan.path);
|
|
11801
|
+
continue;
|
|
11802
|
+
}
|
|
12005
11803
|
try {
|
|
12006
|
-
(
|
|
11804
|
+
await removeWorktree(orphan.repoPath, orphan.path);
|
|
11805
|
+
removed.push(orphan.path);
|
|
12007
11806
|
} catch {
|
|
11807
|
+
kept.push(orphan.path);
|
|
12008
11808
|
}
|
|
12009
11809
|
}
|
|
12010
|
-
|
|
12011
|
-
|
|
12012
|
-
|
|
12013
|
-
|
|
12014
|
-
try {
|
|
12015
|
-
const raw = JSON.parse((0, import_node_fs34.readFileSync)(path2, "utf8"));
|
|
12016
|
-
if (!raw || typeof raw.summary !== "string") return null;
|
|
12017
|
-
return raw;
|
|
12018
|
-
} catch {
|
|
12019
|
-
return null;
|
|
11810
|
+
if (!opts?.dryRun) {
|
|
11811
|
+
updateAdvancedSettings({
|
|
11812
|
+
worktreeLastCleanupAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11813
|
+
});
|
|
12020
11814
|
}
|
|
11815
|
+
return { removed, kept, orphans };
|
|
12021
11816
|
}
|
|
12022
|
-
function
|
|
12023
|
-
const
|
|
12024
|
-
|
|
12025
|
-
|
|
12026
|
-
const
|
|
12027
|
-
|
|
12028
|
-
try {
|
|
12029
|
-
(0, import_node_fs34.unlinkSync)(path2);
|
|
12030
|
-
} catch {
|
|
12031
|
-
}
|
|
11817
|
+
function shouldRunWorktreeCleanup(settings = loadAppSettings()) {
|
|
11818
|
+
const intervalHours = settings.advanced.worktreeCleanupIntervalHours ?? 6;
|
|
11819
|
+
const last = settings.advanced.worktreeLastCleanupAt;
|
|
11820
|
+
if (!last) return true;
|
|
11821
|
+
const elapsed = Date.now() - Date.parse(last);
|
|
11822
|
+
return elapsed >= intervalHours * 36e5;
|
|
12032
11823
|
}
|
|
12033
|
-
|
|
12034
|
-
|
|
12035
|
-
|
|
11824
|
+
function worktreeCleanupSettings() {
|
|
11825
|
+
const a = loadAppSettings().advanced;
|
|
11826
|
+
return {
|
|
11827
|
+
worktreeMaxCount: a.worktreeMaxCount,
|
|
11828
|
+
worktreeCleanupIntervalHours: a.worktreeCleanupIntervalHours,
|
|
11829
|
+
worktreeLastCleanupAt: a.worktreeLastCleanupAt,
|
|
11830
|
+
autoCleanupOrphans: a.autoCleanupOrphans
|
|
11831
|
+
};
|
|
11832
|
+
}
|
|
11833
|
+
var import_node_fs31, import_node_path30;
|
|
11834
|
+
var init_orphan_cleanup = __esm({
|
|
11835
|
+
"src/git/orphan-cleanup.ts"() {
|
|
12036
11836
|
"use strict";
|
|
12037
|
-
|
|
12038
|
-
|
|
11837
|
+
import_node_fs31 = require("fs");
|
|
11838
|
+
import_node_path30 = require("path");
|
|
11839
|
+
init_worktree();
|
|
11840
|
+
init_thread_store();
|
|
12039
11841
|
init_paths();
|
|
12040
|
-
|
|
12041
|
-
FLUSH_MS = 800;
|
|
12042
|
-
MAX_PARTS = 80;
|
|
11842
|
+
init_app_settings();
|
|
12043
11843
|
}
|
|
12044
11844
|
});
|
|
12045
11845
|
|
|
12046
|
-
// src/
|
|
12047
|
-
|
|
12048
|
-
|
|
12049
|
-
|
|
12050
|
-
|
|
12051
|
-
|
|
12052
|
-
|
|
12053
|
-
|
|
12054
|
-
|
|
12055
|
-
|
|
12056
|
-
|
|
12057
|
-
|
|
12058
|
-
|
|
12059
|
-
|
|
12060
|
-
|
|
12061
|
-
|
|
12062
|
-
|
|
12063
|
-
|
|
12064
|
-
|
|
12065
|
-
|
|
12066
|
-
|
|
12067
|
-
|
|
12068
|
-
|
|
12069
|
-
|
|
12070
|
-
|
|
12071
|
-
|
|
12072
|
-
|
|
12073
|
-
|
|
12074
|
-
|
|
12075
|
-
|
|
12076
|
-
|
|
12077
|
-
|
|
12078
|
-
|
|
12079
|
-
|
|
12080
|
-
|
|
12081
|
-
|
|
12082
|
-
|
|
12083
|
-
|
|
12084
|
-
|
|
12085
|
-
|
|
12086
|
-
|
|
12087
|
-
|
|
12088
|
-
|
|
12089
|
-
|
|
12090
|
-
|
|
12091
|
-
|
|
12092
|
-
|
|
12093
|
-
|
|
12094
|
-
|
|
12095
|
-
|
|
12096
|
-
|
|
12097
|
-
|
|
12098
|
-
|
|
12099
|
-
|
|
12100
|
-
|
|
12101
|
-
|
|
12102
|
-
-
|
|
12103
|
-
|
|
12104
|
-
|
|
12105
|
-
|
|
12106
|
-
|
|
12107
|
-
-
|
|
12108
|
-
|
|
12109
|
-
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
12115
|
-
|
|
12116
|
-
|
|
12117
|
-
|
|
12118
|
-
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
12123
|
-
|
|
12124
|
-
|
|
12125
|
-
|
|
12126
|
-
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
|
|
12131
|
-
|
|
12132
|
-
|
|
12133
|
-
|
|
12134
|
-
No need to mention in your report whether or not you used one of the fallback strategies; it's usually irrelevant.
|
|
12135
|
-
|
|
12136
|
-
## Output format
|
|
12137
|
-
|
|
12138
|
-
**1. Recommendation first** (required), then **2. Findings** (may be empty).
|
|
12139
|
-
|
|
12140
|
-
Only report ONE finding per unique issue.
|
|
12141
|
-
|
|
12142
|
-
<example>
|
|
12143
|
-
## Recommendation
|
|
12144
|
-
|
|
12145
|
-
**Request changes** \u2014 The empty-input crash on load will break first-run users; fix that before merge. The unused helper is a nit and can wait.
|
|
12146
|
-
|
|
12147
|
-
## Findings
|
|
12148
|
-
|
|
12149
|
-
### **#1 Empty input causes crash** (blocking)
|
|
12150
|
-
|
|
12151
|
-
If the input field is empty when the page loads, the app will crash.
|
|
12152
|
-
|
|
12153
|
-
File: src/client/frontends/desktop/ui/Input.tsx
|
|
12154
|
-
|
|
12155
|
-
### **#2 Dead code** (nit)
|
|
12156
|
-
|
|
12157
|
-
The getUserData function is now unused. It should be deleted.
|
|
12158
|
-
|
|
12159
|
-
File: src/client/frontends/desktop/core/UserData.ts
|
|
12160
|
-
</example>
|
|
11846
|
+
// src/git/apply-into-main.ts
|
|
11847
|
+
async function applyThreadIntoMain(thread, opts) {
|
|
11848
|
+
const method = opts?.method ?? "merge";
|
|
11849
|
+
const target = opts?.targetBranch?.trim() || await resolveDefaultBranch(thread.repoPath);
|
|
11850
|
+
const status = await (0, import_execa5.execa)("git", ["status", "--porcelain"], {
|
|
11851
|
+
cwd: thread.repoPath,
|
|
11852
|
+
reject: false
|
|
11853
|
+
});
|
|
11854
|
+
if (status.stdout.trim()) {
|
|
11855
|
+
throw new Error(
|
|
11856
|
+
`Main checkout is dirty \u2014 commit or stash before apply-into-main (${thread.repoPath})`
|
|
11857
|
+
);
|
|
11858
|
+
}
|
|
11859
|
+
const checkout = await (0, import_execa5.execa)("git", ["checkout", target], {
|
|
11860
|
+
cwd: thread.repoPath,
|
|
11861
|
+
reject: false
|
|
11862
|
+
});
|
|
11863
|
+
if (checkout.exitCode !== 0) {
|
|
11864
|
+
throw new Error(
|
|
11865
|
+
`Failed to checkout ${target}: ${checkout.stderr.trim() || checkout.stdout.trim()}`
|
|
11866
|
+
);
|
|
11867
|
+
}
|
|
11868
|
+
if (method === "cherry-pick") {
|
|
11869
|
+
const log = await (0, import_execa5.execa)(
|
|
11870
|
+
"git",
|
|
11871
|
+
["log", "--reverse", "--format=%H", `${target}..${thread.branchName}`],
|
|
11872
|
+
{ cwd: thread.repoPath, reject: false }
|
|
11873
|
+
);
|
|
11874
|
+
const shas = log.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
11875
|
+
if (!shas.length) {
|
|
11876
|
+
return {
|
|
11877
|
+
applied: false,
|
|
11878
|
+
method,
|
|
11879
|
+
targetBranch: target,
|
|
11880
|
+
message: `No commits on ${thread.branchName} not already in ${target}`
|
|
11881
|
+
};
|
|
11882
|
+
}
|
|
11883
|
+
for (const sha of shas) {
|
|
11884
|
+
const cp = await (0, import_execa5.execa)("git", ["cherry-pick", sha], {
|
|
11885
|
+
cwd: thread.repoPath,
|
|
11886
|
+
reject: false
|
|
11887
|
+
});
|
|
11888
|
+
if (cp.exitCode !== 0) {
|
|
11889
|
+
await (0, import_execa5.execa)("git", ["cherry-pick", "--abort"], {
|
|
11890
|
+
cwd: thread.repoPath,
|
|
11891
|
+
reject: false
|
|
11892
|
+
});
|
|
11893
|
+
throw new Error(
|
|
11894
|
+
`Cherry-pick ${sha.slice(0, 8)} failed: ${cp.stderr.trim() || cp.stdout.trim()}`
|
|
11895
|
+
);
|
|
11896
|
+
}
|
|
11897
|
+
}
|
|
11898
|
+
return {
|
|
11899
|
+
applied: true,
|
|
11900
|
+
method,
|
|
11901
|
+
targetBranch: target,
|
|
11902
|
+
message: `Cherry-picked ${shas.length} commit(s) from ${thread.branchName} into ${target}`
|
|
11903
|
+
};
|
|
11904
|
+
}
|
|
11905
|
+
const merge = await (0, import_execa5.execa)(
|
|
11906
|
+
"git",
|
|
11907
|
+
["merge", "--no-ff", "-m", `Apply Sideboard thread ${thread.branchName}`, thread.branchName],
|
|
11908
|
+
{ cwd: thread.repoPath, reject: false }
|
|
11909
|
+
);
|
|
11910
|
+
if (merge.exitCode !== 0) {
|
|
11911
|
+
await (0, import_execa5.execa)("git", ["merge", "--abort"], {
|
|
11912
|
+
cwd: thread.repoPath,
|
|
11913
|
+
reject: false
|
|
11914
|
+
});
|
|
11915
|
+
throw new Error(
|
|
11916
|
+
`Merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
|
|
11917
|
+
);
|
|
11918
|
+
}
|
|
11919
|
+
return {
|
|
11920
|
+
applied: true,
|
|
11921
|
+
method: "merge",
|
|
11922
|
+
targetBranch: target,
|
|
11923
|
+
message: `Merged ${thread.branchName} into ${target} at ${thread.repoPath}`
|
|
11924
|
+
};
|
|
11925
|
+
}
|
|
11926
|
+
var import_execa5;
|
|
11927
|
+
var init_apply_into_main = __esm({
|
|
11928
|
+
"src/git/apply-into-main.ts"() {
|
|
11929
|
+
"use strict";
|
|
11930
|
+
import_execa5 = require("execa");
|
|
11931
|
+
init_worktree();
|
|
11932
|
+
}
|
|
11933
|
+
});
|
|
12161
11934
|
|
|
12162
|
-
|
|
12163
|
-
|
|
11935
|
+
// src/store/workspaces.ts
|
|
11936
|
+
var workspaces_exports = {};
|
|
11937
|
+
__export(workspaces_exports, {
|
|
11938
|
+
addWorkspace: () => addWorkspace,
|
|
11939
|
+
ensureWorkspace: () => ensureWorkspace,
|
|
11940
|
+
listWorkspaces: () => listWorkspaces,
|
|
11941
|
+
removeWorkspace: () => removeWorkspace,
|
|
11942
|
+
syncWorkspacesFromThreads: () => syncWorkspacesFromThreads
|
|
11943
|
+
});
|
|
11944
|
+
function workspacesFile() {
|
|
11945
|
+
return (0, import_node_path31.join)(appDataDir(), "workspaces.json");
|
|
11946
|
+
}
|
|
11947
|
+
function removedWorkspacesFile() {
|
|
11948
|
+
return (0, import_node_path31.join)(appDataDir(), "removed-workspaces.json");
|
|
11949
|
+
}
|
|
11950
|
+
function readAll2() {
|
|
11951
|
+
const path2 = workspacesFile();
|
|
11952
|
+
if (!(0, import_node_fs32.existsSync)(path2)) return [];
|
|
11953
|
+
try {
|
|
11954
|
+
const raw = JSON.parse((0, import_node_fs32.readFileSync)(path2, "utf8"));
|
|
11955
|
+
return Array.isArray(raw) ? raw : [];
|
|
11956
|
+
} catch {
|
|
11957
|
+
return [];
|
|
11958
|
+
}
|
|
11959
|
+
}
|
|
11960
|
+
function writeAll2(list) {
|
|
11961
|
+
(0, import_node_fs32.mkdirSync)(appDataDir(), { recursive: true });
|
|
11962
|
+
(0, import_node_fs32.writeFileSync)(workspacesFile(), JSON.stringify(list, null, 2), "utf8");
|
|
11963
|
+
}
|
|
11964
|
+
function readRemoved() {
|
|
11965
|
+
const path2 = removedWorkspacesFile();
|
|
11966
|
+
if (!(0, import_node_fs32.existsSync)(path2)) return /* @__PURE__ */ new Set();
|
|
11967
|
+
try {
|
|
11968
|
+
const raw = JSON.parse((0, import_node_fs32.readFileSync)(path2, "utf8"));
|
|
11969
|
+
return new Set(Array.isArray(raw) ? raw.filter((p) => typeof p === "string") : []);
|
|
11970
|
+
} catch {
|
|
11971
|
+
return /* @__PURE__ */ new Set();
|
|
11972
|
+
}
|
|
11973
|
+
}
|
|
11974
|
+
function writeRemoved(paths) {
|
|
11975
|
+
(0, import_node_fs32.mkdirSync)(appDataDir(), { recursive: true });
|
|
11976
|
+
(0, import_node_fs32.writeFileSync)(removedWorkspacesFile(), JSON.stringify([...paths].sort(), null, 2), "utf8");
|
|
11977
|
+
}
|
|
11978
|
+
function rememberRemoved(repoPath) {
|
|
11979
|
+
const next = readRemoved();
|
|
11980
|
+
next.add(repoPath);
|
|
11981
|
+
writeRemoved(next);
|
|
11982
|
+
}
|
|
11983
|
+
function forgetRemoved(repoPath) {
|
|
11984
|
+
const next = readRemoved();
|
|
11985
|
+
if (!next.delete(repoPath)) return;
|
|
11986
|
+
writeRemoved(next);
|
|
11987
|
+
}
|
|
11988
|
+
function listWorkspaces() {
|
|
11989
|
+
const all = readAll2();
|
|
11990
|
+
const valid = all.filter(
|
|
11991
|
+
(w) => Boolean(w.path) && w.path !== "/" && w.path !== "." && !isGlobalRepoPath(w.path)
|
|
11992
|
+
);
|
|
11993
|
+
if (valid.length !== all.length) writeAll2(valid);
|
|
11994
|
+
return valid.sort((a, b) => a.name.localeCompare(b.name));
|
|
11995
|
+
}
|
|
11996
|
+
async function addWorkspace(repoPath) {
|
|
11997
|
+
const root = await resolveRepoRoot(repoPath);
|
|
11998
|
+
if (!root || root === "/") throw new Error(`Invalid repo path: ${repoPath}`);
|
|
11999
|
+
if (!(0, import_node_fs32.existsSync)(root)) throw new Error(`Repo not found: ${root}`);
|
|
12000
|
+
forgetRemoved(root);
|
|
12001
|
+
await ensureGhPreferOrigin(root);
|
|
12002
|
+
const current = readAll2();
|
|
12003
|
+
const existing = current.find((w) => w.path === root);
|
|
12004
|
+
if (existing) return existing;
|
|
12005
|
+
const next = {
|
|
12006
|
+
path: root,
|
|
12007
|
+
name: (0, import_node_path31.basename)(root),
|
|
12008
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12009
|
+
};
|
|
12010
|
+
writeAll2([...current, next]);
|
|
12011
|
+
return next;
|
|
12012
|
+
}
|
|
12013
|
+
function removeWorkspace(repoPath) {
|
|
12014
|
+
writeAll2(readAll2().filter((w) => w.path !== repoPath));
|
|
12015
|
+
rememberRemoved(repoPath);
|
|
12016
|
+
}
|
|
12017
|
+
async function ensureWorkspace(repoPath) {
|
|
12018
|
+
return addWorkspace(repoPath);
|
|
12019
|
+
}
|
|
12020
|
+
function syncWorkspacesFromThreads(repoPaths) {
|
|
12021
|
+
const current = readAll2();
|
|
12022
|
+
const removed = readRemoved();
|
|
12023
|
+
const byPath = new Map(current.map((w) => [w.path, w]));
|
|
12024
|
+
let dirty = false;
|
|
12025
|
+
for (const path2 of repoPaths) {
|
|
12026
|
+
if (!path2 || path2 === "/" || isGlobalRepoPath(path2) || byPath.has(path2) || removed.has(path2)) {
|
|
12027
|
+
continue;
|
|
12028
|
+
}
|
|
12029
|
+
if (!(0, import_node_fs32.existsSync)(path2)) continue;
|
|
12030
|
+
const ws = {
|
|
12031
|
+
path: path2,
|
|
12032
|
+
name: (0, import_node_path31.basename)(path2),
|
|
12033
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12034
|
+
};
|
|
12035
|
+
byPath.set(path2, ws);
|
|
12036
|
+
dirty = true;
|
|
12037
|
+
}
|
|
12038
|
+
const next = [...byPath.values()];
|
|
12039
|
+
if (dirty) writeAll2(next);
|
|
12040
|
+
return next.sort((a, b) => a.name.localeCompare(b.name));
|
|
12041
|
+
}
|
|
12042
|
+
var import_node_fs32, import_node_path31;
|
|
12043
|
+
var init_workspaces2 = __esm({
|
|
12044
|
+
"src/store/workspaces.ts"() {
|
|
12045
|
+
"use strict";
|
|
12046
|
+
import_node_fs32 = require("fs");
|
|
12047
|
+
import_node_path31 = require("path");
|
|
12048
|
+
init_paths();
|
|
12049
|
+
init_global_workspace();
|
|
12050
|
+
init_worktree();
|
|
12051
|
+
}
|
|
12052
|
+
});
|
|
12164
12053
|
|
|
12165
|
-
|
|
12166
|
-
|
|
12054
|
+
// src/git/clone-repo.ts
|
|
12055
|
+
async function cloneRepoIntoSideboard(opts) {
|
|
12056
|
+
const url = opts.url.trim();
|
|
12057
|
+
if (!url) throw new Error("Clone URL is required");
|
|
12058
|
+
let name = opts.name?.trim();
|
|
12059
|
+
if (!name) {
|
|
12060
|
+
const leaf = (0, import_node_path32.basename)(url.replace(/\/$/, "").replace(/\.git$/, ""));
|
|
12061
|
+
name = leaf || "repo";
|
|
12062
|
+
}
|
|
12063
|
+
name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
12064
|
+
const dest = (0, import_node_path32.join)(sideboardReposDir(), name);
|
|
12065
|
+
if ((0, import_node_fs33.existsSync)(dest)) {
|
|
12066
|
+
const repoPath2 = await resolveRepoRoot(dest);
|
|
12067
|
+
const workspace2 = await ensureWorkspace(repoPath2);
|
|
12068
|
+
return { repoPath: repoPath2, workspace: workspace2 };
|
|
12069
|
+
}
|
|
12070
|
+
const result = await (0, import_execa6.execa)("git", ["clone", url, dest], { reject: false });
|
|
12071
|
+
if (result.exitCode !== 0) {
|
|
12072
|
+
throw new Error(
|
|
12073
|
+
`git clone failed: ${result.stderr?.trim() || result.stdout?.trim() || `exit ${result.exitCode}`}`
|
|
12074
|
+
);
|
|
12075
|
+
}
|
|
12076
|
+
const repoPath = await resolveRepoRoot(dest);
|
|
12077
|
+
const workspace = await ensureWorkspace(repoPath);
|
|
12078
|
+
return { repoPath, workspace };
|
|
12079
|
+
}
|
|
12080
|
+
var import_node_fs33, import_node_path32, import_execa6;
|
|
12081
|
+
var init_clone_repo = __esm({
|
|
12082
|
+
"src/git/clone-repo.ts"() {
|
|
12083
|
+
"use strict";
|
|
12084
|
+
import_node_fs33 = require("fs");
|
|
12085
|
+
import_node_path32 = require("path");
|
|
12086
|
+
import_execa6 = require("execa");
|
|
12087
|
+
init_paths();
|
|
12088
|
+
init_workspaces2();
|
|
12089
|
+
init_worktree();
|
|
12090
|
+
}
|
|
12091
|
+
});
|
|
12167
12092
|
|
|
12168
|
-
|
|
12093
|
+
// src/detect/detect.ts
|
|
12094
|
+
async function detectAgents() {
|
|
12095
|
+
ensureAgentPath();
|
|
12096
|
+
enrichPathWithNpmGlobalBin();
|
|
12097
|
+
return Promise.all(allAdapters().map((a) => a.detect()));
|
|
12098
|
+
}
|
|
12099
|
+
async function requireAgent(agent, opts) {
|
|
12100
|
+
ensureAgentPath();
|
|
12101
|
+
enrichPathWithNpmGlobalBin();
|
|
12102
|
+
const status = await Promise.race([
|
|
12103
|
+
getAdapter(agent).detect(),
|
|
12104
|
+
new Promise((_, reject) => {
|
|
12105
|
+
setTimeout(() => {
|
|
12106
|
+
reject(
|
|
12107
|
+
new Error(
|
|
12108
|
+
`${agent} detect timed out after ${REQUIRE_AGENT_TIMEOUT_MS / 1e3}s`
|
|
12109
|
+
)
|
|
12110
|
+
);
|
|
12111
|
+
}, REQUIRE_AGENT_TIMEOUT_MS);
|
|
12112
|
+
})
|
|
12113
|
+
]);
|
|
12114
|
+
if (!status.installed || !status.authenticated) {
|
|
12115
|
+
throw new Error(status.reason ?? `${agent} is not available`);
|
|
12116
|
+
}
|
|
12117
|
+
if (opts?.requireLinear && !status.linearMcp) {
|
|
12118
|
+
throw new Error(
|
|
12119
|
+
`${agent} has no Linear MCP server configured \u2014 ticket sources require it`
|
|
12120
|
+
);
|
|
12121
|
+
}
|
|
12122
|
+
return status;
|
|
12123
|
+
}
|
|
12124
|
+
var REQUIRE_AGENT_TIMEOUT_MS;
|
|
12125
|
+
var init_detect = __esm({
|
|
12126
|
+
"src/detect/detect.ts"() {
|
|
12127
|
+
"use strict";
|
|
12128
|
+
init_agents();
|
|
12129
|
+
REQUIRE_AGENT_TIMEOUT_MS = 8e3;
|
|
12130
|
+
}
|
|
12131
|
+
});
|
|
12169
12132
|
|
|
12170
|
-
|
|
12171
|
-
|
|
12133
|
+
// src/threads/create.ts
|
|
12134
|
+
async function createThread(input, _onSetupLine) {
|
|
12135
|
+
const resolved = resolveNewThreadOptions({
|
|
12136
|
+
agent: input.agent,
|
|
12137
|
+
model: input.model,
|
|
12138
|
+
effort: input.effort,
|
|
12139
|
+
fast: input.fast
|
|
12140
|
+
});
|
|
12141
|
+
await requireAgent(resolved.agent);
|
|
12142
|
+
const repoPath = await resolveRepoRoot(input.repoPath);
|
|
12143
|
+
if (!(0, import_node_fs34.existsSync)(repoPath)) {
|
|
12144
|
+
throw new Error(`Repo not found: ${repoPath}`);
|
|
12145
|
+
}
|
|
12146
|
+
let sourceRef = input.sourceRef;
|
|
12147
|
+
let sourceIsFork = false;
|
|
12148
|
+
let prUrl = null;
|
|
12149
|
+
let prTitle = null;
|
|
12150
|
+
if (input.sourceType === "pr") {
|
|
12151
|
+
const num2 = Number(input.sourceRef.replace(/^#/, ""));
|
|
12152
|
+
if (!Number.isFinite(num2)) throw new Error(`Invalid PR number: ${input.sourceRef}`);
|
|
12153
|
+
const pr = await getPr(repoPath, num2);
|
|
12154
|
+
if (!pr) throw new Error(`PR #${num2} not found`);
|
|
12155
|
+
sourceIsFork = pr.isCrossRepository;
|
|
12156
|
+
prUrl = pr.url;
|
|
12157
|
+
const localFetchBranch = `sideboard-pr-${pr.number}`;
|
|
12158
|
+
await fetchPrHead(repoPath, pr.number, localFetchBranch);
|
|
12159
|
+
sourceRef = localFetchBranch;
|
|
12160
|
+
} else if (input.sourceType === "ticket") {
|
|
12161
|
+
sourceRef = await resolveDefaultBranch(repoPath);
|
|
12162
|
+
} else if (input.sourceType === "branch") {
|
|
12163
|
+
if (!sourceRef || sourceRef === "HEAD" || sourceRef === "default") {
|
|
12164
|
+
sourceRef = await resolveDefaultBranch(repoPath);
|
|
12165
|
+
} else {
|
|
12166
|
+
const existing = await getPrForHeadBranch(repoPath, sourceRef);
|
|
12167
|
+
if (existing?.url) {
|
|
12168
|
+
prUrl = existing.url;
|
|
12169
|
+
prTitle = existing.title;
|
|
12170
|
+
}
|
|
12171
|
+
}
|
|
12172
|
+
} else if (input.sourceType === "adopt") {
|
|
12173
|
+
throw new Error("Use adoptThread() for adopt sources");
|
|
12174
|
+
}
|
|
12175
|
+
const team = allocateTeamSlug(repoPath);
|
|
12176
|
+
const { branchName, worktreePath } = await createThreadWorktree({
|
|
12177
|
+
repoPath,
|
|
12178
|
+
sourceRef,
|
|
12179
|
+
slug: team.slug
|
|
12180
|
+
});
|
|
12181
|
+
copyConfiguredFiles(repoPath, worktreePath);
|
|
12182
|
+
const explicitTitle = input.title?.trim();
|
|
12183
|
+
const thread = createEmptyThread({
|
|
12184
|
+
title: explicitTitle || team.name,
|
|
12185
|
+
userSetTitle: Boolean(explicitTitle),
|
|
12186
|
+
sourceType: input.sourceType,
|
|
12187
|
+
sourceRef: input.sourceRef === "default" ? sourceRef : input.sourceRef,
|
|
12188
|
+
branchName,
|
|
12189
|
+
worktreePath,
|
|
12190
|
+
repoPath,
|
|
12191
|
+
agent: resolved.agent,
|
|
12192
|
+
autonomy: input.autonomy ?? "default",
|
|
12193
|
+
model: resolved.model,
|
|
12194
|
+
effort: resolved.effort,
|
|
12195
|
+
fast: resolved.fast,
|
|
12196
|
+
planMode: Boolean(input.planMode),
|
|
12197
|
+
attachments: input.attachments ?? [],
|
|
12198
|
+
sourceIsFork,
|
|
12199
|
+
parentThreadId: input.parentThreadId ?? null,
|
|
12200
|
+
status: "idle",
|
|
12201
|
+
prUrl,
|
|
12202
|
+
prTitle
|
|
12203
|
+
});
|
|
12204
|
+
writeThread(thread);
|
|
12205
|
+
await ensureWorkspace(repoPath);
|
|
12206
|
+
return readThread(thread.id) ?? thread;
|
|
12207
|
+
}
|
|
12208
|
+
async function listLinearIssues(agent, repoPath) {
|
|
12209
|
+
const { getAdapter: getAdapter2 } = await Promise.resolve().then(() => (init_agents(), agents_exports));
|
|
12210
|
+
await requireAgent(agent, { requireLinear: true });
|
|
12211
|
+
const adapter = getAdapter2(agent);
|
|
12212
|
+
if (!adapter.listLinearIssues) {
|
|
12213
|
+
throw new Error(`${agent} does not support Linear issue listing`);
|
|
12214
|
+
}
|
|
12215
|
+
return adapter.listLinearIssues(repoPath);
|
|
12216
|
+
}
|
|
12217
|
+
var import_node_fs34;
|
|
12218
|
+
var init_create = __esm({
|
|
12219
|
+
"src/threads/create.ts"() {
|
|
12220
|
+
"use strict";
|
|
12221
|
+
import_node_fs34 = require("fs");
|
|
12222
|
+
init_detect();
|
|
12223
|
+
init_worktree();
|
|
12224
|
+
init_conductor();
|
|
12225
|
+
init_app_settings();
|
|
12226
|
+
init_thread_store();
|
|
12227
|
+
init_workspaces2();
|
|
12172
12228
|
}
|
|
12173
12229
|
});
|
|
12174
12230
|
|
|
12175
|
-
// src/
|
|
12176
|
-
function
|
|
12177
|
-
|
|
12178
|
-
|
|
12179
|
-
if (/##\s*Recommendation|ready to merge|Approve with nits|Request changes/i.test(trimmed)) {
|
|
12180
|
-
return false;
|
|
12181
|
-
}
|
|
12182
|
-
return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
|
|
12231
|
+
// src/store/turn-live.ts
|
|
12232
|
+
function trimParts(parts) {
|
|
12233
|
+
if (parts.length <= MAX_PARTS) return parts;
|
|
12234
|
+
return parts.slice(-Math.floor(MAX_PARTS / 2));
|
|
12183
12235
|
}
|
|
12184
|
-
function
|
|
12185
|
-
|
|
12186
|
-
|
|
12187
|
-
|
|
12188
|
-
return content.trim() ? content : null;
|
|
12189
|
-
} catch {
|
|
12190
|
-
return null;
|
|
12236
|
+
function lastText(parts, type) {
|
|
12237
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
12238
|
+
const p = parts[i];
|
|
12239
|
+
if (p.type === type && p.text.trim()) return p.text.trim();
|
|
12191
12240
|
}
|
|
12241
|
+
return "";
|
|
12192
12242
|
}
|
|
12193
|
-
function
|
|
12194
|
-
const
|
|
12195
|
-
|
|
12196
|
-
|
|
12197
|
-
|
|
12198
|
-
|
|
12199
|
-
|
|
12200
|
-
const
|
|
12201
|
-
const
|
|
12202
|
-
|
|
12203
|
-
|
|
12204
|
-
|
|
12205
|
-
|
|
12206
|
-
|
|
12207
|
-
|
|
12208
|
-
|
|
12209
|
-
|
|
12210
|
-
|
|
12211
|
-
|
|
12212
|
-
if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
|
|
12213
|
-
return {
|
|
12214
|
-
path: REVIEW_REQUEST_PATH,
|
|
12215
|
-
name: REVIEW_REQUEST_NAME,
|
|
12216
|
-
content: localContent,
|
|
12217
|
-
source: "local"
|
|
12218
|
-
};
|
|
12219
|
-
}
|
|
12220
|
-
const legacyAbs = (0, import_node_path32.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
|
|
12221
|
-
const legacyContent = readTextIfPresent(legacyAbs);
|
|
12222
|
-
if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
|
|
12223
|
-
return {
|
|
12224
|
-
path: LEGACY_REVIEW_REQUEST_PATH,
|
|
12225
|
-
name: REVIEW_REQUEST_NAME,
|
|
12226
|
-
content: legacyContent,
|
|
12227
|
-
source: "local"
|
|
12228
|
-
};
|
|
12243
|
+
function summarizeTurnLive(parts) {
|
|
12244
|
+
const tools = parts.filter(
|
|
12245
|
+
(p) => p.type === "tool"
|
|
12246
|
+
);
|
|
12247
|
+
const lastTool = tools[tools.length - 1];
|
|
12248
|
+
const lastToolLabel = lastTool ? lastTool.description || toolDescription(lastTool.name, lastTool.input) || lastTool.name : void 0;
|
|
12249
|
+
const running = lastTool?.status === "running";
|
|
12250
|
+
const thinking = lastText(parts, "thinking");
|
|
12251
|
+
const text4 = lastText(parts, "text");
|
|
12252
|
+
const excerptRaw = thinking || text4;
|
|
12253
|
+
const excerpt = excerptRaw.length > 280 ? `${excerptRaw.slice(-280)}` : excerptRaw || void 0;
|
|
12254
|
+
let summary = "Working\u2026";
|
|
12255
|
+
if (lastToolLabel) {
|
|
12256
|
+
const verb = running ? lastToolLabel : `Finished ${lastToolLabel}`;
|
|
12257
|
+
summary = tools.length > 1 ? `${verb} (${tools.length} tools)` : verb;
|
|
12258
|
+
} else if (thinking) {
|
|
12259
|
+
summary = "Thinking\u2026";
|
|
12260
|
+
} else if (text4) {
|
|
12261
|
+
summary = "Writing reply\u2026";
|
|
12229
12262
|
}
|
|
12230
|
-
ensureAttachmentsGitignore(worktreePath);
|
|
12231
|
-
(0, import_node_fs35.mkdirSync)((0, import_node_path32.dirname)(localAbs), { recursive: true });
|
|
12232
|
-
(0, import_node_fs35.writeFileSync)(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
|
|
12233
12263
|
return {
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
|
|
12237
|
-
|
|
12264
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12265
|
+
summary,
|
|
12266
|
+
lastTool: lastToolLabel,
|
|
12267
|
+
toolCount: tools.length,
|
|
12268
|
+
excerpt
|
|
12238
12269
|
};
|
|
12239
12270
|
}
|
|
12240
|
-
function
|
|
12241
|
-
const
|
|
12242
|
-
|
|
12243
|
-
if (
|
|
12244
|
-
|
|
12245
|
-
|
|
12246
|
-
name: REPO_REVIEW_NAME,
|
|
12247
|
-
content: repoContent,
|
|
12248
|
-
source: "repo"
|
|
12249
|
-
};
|
|
12250
|
-
}
|
|
12251
|
-
const localAbs = (0, import_node_path32.join)(worktreePath, REVIEW_REQUEST_PATH);
|
|
12252
|
-
const localContent = readTextIfPresent(localAbs) ?? readTextIfPresent((0, import_node_path32.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
|
|
12253
|
-
if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
|
|
12254
|
-
const path2 = (0, import_node_fs35.existsSync)(localAbs) ? REVIEW_REQUEST_PATH : LEGACY_REVIEW_REQUEST_PATH;
|
|
12255
|
-
return {
|
|
12256
|
-
path: path2,
|
|
12257
|
-
name: REVIEW_REQUEST_NAME,
|
|
12258
|
-
content: localContent,
|
|
12259
|
-
source: "local"
|
|
12260
|
-
};
|
|
12271
|
+
function flush(threadId) {
|
|
12272
|
+
const buf = buffers.get(threadId);
|
|
12273
|
+
if (!buf) return;
|
|
12274
|
+
if (buf.timer) {
|
|
12275
|
+
clearTimeout(buf.timer);
|
|
12276
|
+
buf.timer = null;
|
|
12261
12277
|
}
|
|
12262
|
-
(
|
|
12263
|
-
(0, import_node_fs35.writeFileSync)(repoAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
|
|
12264
|
-
return {
|
|
12265
|
-
path: REPO_REVIEW_PATH,
|
|
12266
|
-
name: REPO_REVIEW_NAME,
|
|
12267
|
-
content: REVIEW_REQUEST_TEMPLATE,
|
|
12268
|
-
source: "repo"
|
|
12269
|
-
};
|
|
12278
|
+
writeTurnLive(threadId, summarizeTurnLive(buf.parts));
|
|
12270
12279
|
}
|
|
12271
|
-
function
|
|
12272
|
-
const
|
|
12273
|
-
|
|
12274
|
-
return
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
|
|
12279
|
-
|
|
12280
|
-
};
|
|
12280
|
+
function scheduleFlush(threadId) {
|
|
12281
|
+
const buf = buffers.get(threadId);
|
|
12282
|
+
if (!buf) return;
|
|
12283
|
+
if (buf.timer) return;
|
|
12284
|
+
buf.timer = setTimeout(() => {
|
|
12285
|
+
const live = buffers.get(threadId);
|
|
12286
|
+
if (live) live.timer = null;
|
|
12287
|
+
flush(threadId);
|
|
12288
|
+
}, FLUSH_MS);
|
|
12281
12289
|
}
|
|
12282
|
-
function
|
|
12283
|
-
|
|
12290
|
+
function noteTurnLiveEvent(threadId, event) {
|
|
12291
|
+
if (event.type === "session_id" || event.type === "usage" || event.type === "stderr") {
|
|
12292
|
+
return;
|
|
12293
|
+
}
|
|
12294
|
+
const prev = buffers.get(threadId) ?? { parts: [], timer: null };
|
|
12295
|
+
prev.parts = trimParts(applyAgentEvent(prev.parts, event));
|
|
12296
|
+
buffers.set(threadId, prev);
|
|
12297
|
+
if (event.type === "exit") {
|
|
12298
|
+
flush(threadId);
|
|
12299
|
+
const cur = buffers.get(threadId);
|
|
12300
|
+
if (cur?.timer) clearTimeout(cur.timer);
|
|
12301
|
+
buffers.delete(threadId);
|
|
12302
|
+
return;
|
|
12303
|
+
}
|
|
12304
|
+
const immediate = event.type === "tool_use" || event.type === "tool_result";
|
|
12305
|
+
if (immediate) flush(threadId);
|
|
12306
|
+
else scheduleFlush(threadId);
|
|
12284
12307
|
}
|
|
12285
|
-
|
|
12286
|
-
const
|
|
12287
|
-
|
|
12288
|
-
|
|
12289
|
-
|
|
12290
|
-
|
|
12291
|
-
|
|
12308
|
+
function writeTurnLive(threadId, progress) {
|
|
12309
|
+
const path2 = threadLivePath(threadId);
|
|
12310
|
+
const tmp = `${path2}.${process.pid}.tmp`;
|
|
12311
|
+
try {
|
|
12312
|
+
(0, import_node_fs35.writeFileSync)(tmp, JSON.stringify(progress), "utf8");
|
|
12313
|
+
(0, import_node_fs35.renameSync)(tmp, path2);
|
|
12314
|
+
} catch {
|
|
12315
|
+
try {
|
|
12316
|
+
(0, import_node_fs35.unlinkSync)(tmp);
|
|
12317
|
+
} catch {
|
|
12318
|
+
}
|
|
12292
12319
|
}
|
|
12293
|
-
|
|
12294
|
-
|
|
12320
|
+
}
|
|
12321
|
+
function readTurnLive(threadId) {
|
|
12322
|
+
const path2 = threadLivePath(threadId);
|
|
12323
|
+
if (!(0, import_node_fs35.existsSync)(path2)) return null;
|
|
12324
|
+
try {
|
|
12325
|
+
const raw = JSON.parse((0, import_node_fs35.readFileSync)(path2, "utf8"));
|
|
12326
|
+
if (!raw || typeof raw.summary !== "string") return null;
|
|
12327
|
+
return raw;
|
|
12328
|
+
} catch {
|
|
12329
|
+
return null;
|
|
12295
12330
|
}
|
|
12296
|
-
const guidelines = resolveReviewGuidelines(from.worktreePath);
|
|
12297
|
-
const tab = createChatTab({
|
|
12298
|
-
fromThreadId: from.id,
|
|
12299
|
-
title: "Review",
|
|
12300
|
-
attachments: [
|
|
12301
|
-
buildReviewRequestAttachment(guidelines.content, {
|
|
12302
|
-
path: guidelines.path,
|
|
12303
|
-
name: guidelines.name
|
|
12304
|
-
})
|
|
12305
|
-
]
|
|
12306
|
-
});
|
|
12307
|
-
const started = await send2(tab.id, REVIEW_REQUEST_PREFILL);
|
|
12308
|
-
return { tab: started, from };
|
|
12309
12331
|
}
|
|
12310
|
-
|
|
12311
|
-
|
|
12312
|
-
|
|
12332
|
+
function clearTurnLive(threadId) {
|
|
12333
|
+
const buf = buffers.get(threadId);
|
|
12334
|
+
if (buf?.timer) clearTimeout(buf.timer);
|
|
12335
|
+
buffers.delete(threadId);
|
|
12336
|
+
const path2 = threadLivePath(threadId);
|
|
12337
|
+
if (!(0, import_node_fs35.existsSync)(path2)) return;
|
|
12338
|
+
try {
|
|
12339
|
+
(0, import_node_fs35.unlinkSync)(path2);
|
|
12340
|
+
} catch {
|
|
12341
|
+
}
|
|
12342
|
+
}
|
|
12343
|
+
var import_node_fs35, buffers, FLUSH_MS, MAX_PARTS;
|
|
12344
|
+
var init_turn_live = __esm({
|
|
12345
|
+
"src/store/turn-live.ts"() {
|
|
12313
12346
|
"use strict";
|
|
12314
|
-
import_node_crypto6 = require("crypto");
|
|
12315
12347
|
import_node_fs35 = require("fs");
|
|
12316
|
-
|
|
12317
|
-
|
|
12318
|
-
|
|
12319
|
-
|
|
12320
|
-
|
|
12321
|
-
init_workspace_scratch();
|
|
12322
|
-
REPO_REVIEW_PATH = ".sideboard/review.md";
|
|
12323
|
-
REPO_REVIEW_NAME = "review.md";
|
|
12324
|
-
REVIEW_REQUEST_PATH = `${ATTACHMENTS_DIR}/Review request.md`;
|
|
12325
|
-
LEGACY_REVIEW_REQUEST_PATH = `${LEGACY_ATTACHMENTS_DIR}/Review request.md`;
|
|
12326
|
-
REVIEW_REQUEST_NAME = "Review request.md";
|
|
12327
|
-
REVIEW_REQUEST_PREFILL = "Review changes in this workspace.";
|
|
12328
|
-
LEGACY_REVIEW_TEMPLATE_MARKERS = [
|
|
12329
|
-
"You are acting as a reviewer for a proposed code change made by another engineer.",
|
|
12330
|
-
"HOW MANY FINDINGS TO RETURN:"
|
|
12331
|
-
];
|
|
12348
|
+
init_message_parts();
|
|
12349
|
+
init_paths();
|
|
12350
|
+
buffers = /* @__PURE__ */ new Map();
|
|
12351
|
+
FLUSH_MS = 800;
|
|
12352
|
+
MAX_PARTS = 80;
|
|
12332
12353
|
}
|
|
12333
12354
|
});
|
|
12334
12355
|
|
|
@@ -14520,8 +14541,9 @@ function formatProcessGuideDirective() {
|
|
|
14520
14541
|
"Process guides (recurring work only):",
|
|
14521
14542
|
"- If `.claude/skills/graph-engineering/SKILL.md` exists, follow it (`/graph-engineering`) for migrations, ports, batch fixes, and other fan-out. Judge first; state on disk; grow the rulebook; do not patch around it.",
|
|
14522
14543
|
"- If this same shape of work will happen again, write `.claude/skills/<kebab-name>/SKILL.md` in this worktree (Claude Code project skill). Sideboard `/name`, Claude Code, and `attach` all load that path. Do not leave the method only in chat.",
|
|
14523
|
-
"-
|
|
14524
|
-
"-
|
|
14544
|
+
"- Merge-readiness notes: create or edit `.claude/skills/review/SKILL.md` and commit it. That file is allowed. Do not use `.sideboard/review.md` for new notes (legacy; setup copies it into the skill).",
|
|
14545
|
+
"- Do not write new skills under `.sideboard/skills/` (that folder only \u2014 other `.sideboard/` files such as settings.toml are fine). Point Codex/OpenCode at the Claude skill from `AGENTS.md`. Optional: symlink `.cursor/skills/<name>` to the Claude skill.",
|
|
14546
|
+
"- Skip a guide for a one-off. If a matching skill exists, follow it. Same miss twice \u2192 edit the skill, do not patch around it."
|
|
14525
14547
|
].join("\n");
|
|
14526
14548
|
}
|
|
14527
14549
|
function formatArtifactDirective() {
|
|
@@ -14548,15 +14570,7 @@ function formatArtifactDirective() {
|
|
|
14548
14570
|
].join("\n");
|
|
14549
14571
|
}
|
|
14550
14572
|
function formatUiReminder() {
|
|
14551
|
-
return
|
|
14552
|
-
"Sideboard side column (important):",
|
|
14553
|
-
"There is no claude.ai Artifact tool here \u2014 that is normal.",
|
|
14554
|
-
"HTML/SVG/markdown: ```html fence or MCP present_artifact.",
|
|
14555
|
-
"CMS forms/tables: MCP present_schema (Brightsy resource_id or inline schema+schemaUi).",
|
|
14556
|
-
"Files column: MCP present_files (brightsy account storage or memory).",
|
|
14557
|
-
"Do not say artifacts/CMS UI are unavailable.",
|
|
14558
|
-
"ask_user: only when blocked on a real multiple-choice (approach fork, which API). Never for hellos, check-ins, or \u201Cwhat next?\u201D menus \u2014 reply in chat."
|
|
14559
|
-
].join(" ");
|
|
14573
|
+
return "Sideboard UI: html fence or present_artifact / present_schema / present_files; ask_user only for a real multiple-choice (not hellos or \u201Cwhat next?\u201D) \u2014 reply in chat. Do not say artifacts/CMS UI are unavailable.";
|
|
14560
14574
|
}
|
|
14561
14575
|
function loadAgentInstructions(worktreePath, agent) {
|
|
14562
14576
|
const candidates = FILES_BY_AGENT[agent] ?? FILES_BY_AGENT.claude;
|
|
@@ -14703,7 +14717,7 @@ __export(plan_file_exports, {
|
|
|
14703
14717
|
resolvePlanMarkdown: () => resolvePlanMarkdown,
|
|
14704
14718
|
writePlanFile: () => writePlanFile
|
|
14705
14719
|
});
|
|
14706
|
-
function
|
|
14720
|
+
function ensureAttachmentsGitignore(worktreePath) {
|
|
14707
14721
|
const gitignoreAbs = (0, import_node_path38.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
|
|
14708
14722
|
if ((0, import_node_fs42.existsSync)(gitignoreAbs)) return;
|
|
14709
14723
|
(0, import_node_fs42.mkdirSync)((0, import_node_path38.dirname)(gitignoreAbs), { recursive: true });
|
|
@@ -14725,7 +14739,7 @@ function readPlanFile(worktreePath) {
|
|
|
14725
14739
|
return readTextIfPresent2(planFileAbs(worktreePath)) ?? readTextIfPresent2((0, import_node_path38.join)(worktreePath, `${LEGACY_ATTACHMENTS_DIR}/plan.md`)) ?? readTextIfPresent2((0, import_node_path38.join)(worktreePath, LEGACY_PLAN_FILE_REL));
|
|
14726
14740
|
}
|
|
14727
14741
|
function writePlanFile(worktreePath, content) {
|
|
14728
|
-
|
|
14742
|
+
ensureAttachmentsGitignore(worktreePath);
|
|
14729
14743
|
const abs = planFileAbs(worktreePath);
|
|
14730
14744
|
(0, import_node_fs42.mkdirSync)((0, import_node_path38.dirname)(abs), { recursive: true });
|
|
14731
14745
|
const body = content.trimEnd() + (content.endsWith("\n") ? "" : "\n");
|
|
@@ -15480,7 +15494,7 @@ var init_orchestrator = __esm({
|
|
|
15480
15494
|
if (compact.didCompact) {
|
|
15481
15495
|
thread = updateThread(threadId, {
|
|
15482
15496
|
messages: compact.thread.messages,
|
|
15483
|
-
sessionId:
|
|
15497
|
+
sessionId: compact.thread.sessionId
|
|
15484
15498
|
});
|
|
15485
15499
|
this.emit({
|
|
15486
15500
|
type: "context_compacted",
|
|
@@ -16897,6 +16911,9 @@ __export(index_exports, {
|
|
|
16897
16911
|
REVIEW_REQUEST_PATH: () => REVIEW_REQUEST_PATH,
|
|
16898
16912
|
REVIEW_REQUEST_PREFILL: () => REVIEW_REQUEST_PREFILL,
|
|
16899
16913
|
REVIEW_REQUEST_TEMPLATE: () => REVIEW_REQUEST_TEMPLATE,
|
|
16914
|
+
REVIEW_SKILL_NAME: () => REVIEW_SKILL_NAME,
|
|
16915
|
+
REVIEW_SKILL_PATH: () => REVIEW_SKILL_PATH,
|
|
16916
|
+
SESSION_RESET_OCCUPANCY_TOKENS: () => SESSION_RESET_OCCUPANCY_TOKENS,
|
|
16900
16917
|
SIDEBOARD_FORCE_STOP: () => SIDEBOARD_FORCE_STOP,
|
|
16901
16918
|
SIDEBOARD_MCP_ALLOWED_TOOLS: () => SIDEBOARD_MCP_ALLOWED_TOOLS,
|
|
16902
16919
|
SIDEBOARD_MCP_PROFILE_ENV: () => SIDEBOARD_MCP_PROFILE_ENV,
|
|
@@ -16911,6 +16928,7 @@ __export(index_exports, {
|
|
|
16911
16928
|
SlackOAuthCancelledError: () => SlackOAuthCancelledError,
|
|
16912
16929
|
SlackRelayHub: () => SlackRelayHub,
|
|
16913
16930
|
THINKING_EFFORTS: () => THINKING_EFFORTS,
|
|
16931
|
+
WORKTREE_MCP_TOOLS: () => WORKTREE_MCP_TOOLS,
|
|
16914
16932
|
ackSlackInboundSeen: () => ackSlackInboundSeen,
|
|
16915
16933
|
addPrStackLayer: () => addPrStackLayer,
|
|
16916
16934
|
addStackLayerFromThread: () => addStackLayerFromThread,
|
|
@@ -16930,6 +16948,7 @@ __export(index_exports, {
|
|
|
16930
16948
|
applyAppEnvironment: () => applyAppEnvironment,
|
|
16931
16949
|
applyCompaction: () => applyCompaction,
|
|
16932
16950
|
applyGithubGitAuthEnv: () => applyGithubGitAuthEnv,
|
|
16951
|
+
applyPromptCacheTtlEnv: () => applyPromptCacheTtlEnv,
|
|
16933
16952
|
applyThreadIntoMain: () => applyThreadIntoMain,
|
|
16934
16953
|
applyTurnUsage: () => applyTurnUsage,
|
|
16935
16954
|
armSchedules: () => armSchedules,
|
|
@@ -17029,6 +17048,7 @@ __export(index_exports, {
|
|
|
17029
17048
|
ensureGhPreferOrigin: () => ensureGhPreferOrigin,
|
|
17030
17049
|
ensureGlobalCoordinatorCwd: () => ensureGlobalCoordinatorCwd,
|
|
17031
17050
|
ensureReviewRequestFile: () => ensureReviewRequestFile,
|
|
17051
|
+
ensureReviewSkillFile: () => ensureReviewSkillFile,
|
|
17032
17052
|
ensureSlackCoordinator: () => ensureSlackCoordinator,
|
|
17033
17053
|
ensureSlackDeviceIdentity: () => ensureSlackDeviceIdentity,
|
|
17034
17054
|
ensureWorkspace: () => ensureWorkspace,
|
|
@@ -17167,6 +17187,7 @@ __export(index_exports, {
|
|
|
17167
17187
|
isThisProcessDesktopHost: () => isThisProcessDesktopHost,
|
|
17168
17188
|
isThreadCaffeinated: () => isThreadCaffeinated,
|
|
17169
17189
|
isWorkspaceScratchPath: () => isWorkspaceScratchPath,
|
|
17190
|
+
lastRequestOccupancy: () => lastRequestOccupancy,
|
|
17170
17191
|
linearAuthorizationHeader: () => linearAuthorizationHeader,
|
|
17171
17192
|
linearGraphql: () => linearGraphql,
|
|
17172
17193
|
linearOAuthAuthorizeUrl: () => linearOAuthAuthorizeUrl,
|
|
@@ -17334,6 +17355,7 @@ __export(index_exports, {
|
|
|
17334
17355
|
shouldCompactContext: () => shouldCompactContext,
|
|
17335
17356
|
shouldInjectBrightsyMcp: () => shouldInjectBrightsyMcp,
|
|
17336
17357
|
shouldRefreshReviewRequestTemplate: () => shouldRefreshReviewRequestTemplate,
|
|
17358
|
+
shouldResetSessionForOccupancy: () => shouldResetSessionForOccupancy,
|
|
17337
17359
|
shouldRunWorktreeCleanup: () => shouldRunWorktreeCleanup,
|
|
17338
17360
|
sideboardHomeDir: () => sideboardHomeDir,
|
|
17339
17361
|
sideboardMcpProfile: () => sideboardMcpProfile,
|
|
@@ -17411,6 +17433,7 @@ __export(index_exports, {
|
|
|
17411
17433
|
worktreeDisplayLabelForGroup: () => worktreeDisplayLabelForGroup,
|
|
17412
17434
|
worktreeNameFromPath: () => worktreeNameFromPath,
|
|
17413
17435
|
worktreesRoot: () => worktreesRoot,
|
|
17436
|
+
wrapReviewSkillMarkdown: () => wrapReviewSkillMarkdown,
|
|
17414
17437
|
writeInjectedMcpConfig: () => writeInjectedMcpConfig,
|
|
17415
17438
|
writePlanFile: () => writePlanFile,
|
|
17416
17439
|
writeThread: () => writeThread,
|
|
@@ -19218,74 +19241,77 @@ async function startMcpServer() {
|
|
|
19218
19241
|
name: "sideboard",
|
|
19219
19242
|
version: "0.1.0"
|
|
19220
19243
|
});
|
|
19221
|
-
|
|
19222
|
-
|
|
19223
|
-
|
|
19224
|
-
|
|
19225
|
-
|
|
19226
|
-
|
|
19227
|
-
|
|
19228
|
-
workspaces.
|
|
19229
|
-
|
|
19230
|
-
|
|
19231
|
-
|
|
19232
|
-
|
|
19233
|
-
|
|
19234
|
-
|
|
19235
|
-
|
|
19236
|
-
|
|
19237
|
-
|
|
19238
|
-
|
|
19239
|
-
|
|
19240
|
-
|
|
19241
|
-
|
|
19242
|
-
|
|
19243
|
-
|
|
19244
|
-
|
|
19245
|
-
|
|
19246
|
-
|
|
19247
|
-
const
|
|
19244
|
+
const worktreeProfile = sideboardMcpProfile() === "worktree";
|
|
19245
|
+
if (!worktreeProfile) {
|
|
19246
|
+
server.tool(
|
|
19247
|
+
"list_workspaces",
|
|
19248
|
+
"List registered Sideboard workspaces (repos). Each line is name, path, and github:owner/repo when resolvable \u2014 use path as repoPath for list_branches/list_prs/list_issues/create_thread.",
|
|
19249
|
+
{},
|
|
19250
|
+
async () => {
|
|
19251
|
+
const workspaces = orch.listWorkspaces();
|
|
19252
|
+
const lines = await Promise.all(
|
|
19253
|
+
workspaces.map(async (w) => {
|
|
19254
|
+
const slug = await resolveGithubRepoSlug(w.path).catch(() => null);
|
|
19255
|
+
return slug ? `${w.name} ${w.path} github:${slug}` : `${w.name} ${w.path}`;
|
|
19256
|
+
})
|
|
19257
|
+
);
|
|
19258
|
+
return {
|
|
19259
|
+
content: [
|
|
19260
|
+
{ type: "text", text: lines.join("\n") || "(no workspaces)" }
|
|
19261
|
+
]
|
|
19262
|
+
};
|
|
19263
|
+
}
|
|
19264
|
+
);
|
|
19265
|
+
server.tool(
|
|
19266
|
+
"list_threads",
|
|
19267
|
+
"List Sideboard threads across all workspaces (one summary line each \u2014 token-frugal). Each line ends with sideboard://thread/<id> \u2014 use that URL in markdown links so the UI can open the chat.",
|
|
19268
|
+
{},
|
|
19269
|
+
async () => {
|
|
19270
|
+
const threads = orch.getThreads(true);
|
|
19271
|
+
const lines = threads.map((t) => {
|
|
19272
|
+
const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path40.basename)(t.repoPath) || t.repoPath;
|
|
19273
|
+
const live = t.status === "running" || t.status === "queued" ? readTurnLive(t.id) : null;
|
|
19274
|
+
const progress = live?.summary ? ` ${live.summary}` : "";
|
|
19275
|
+
return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title} sideboard://thread/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}${progress}`;
|
|
19276
|
+
});
|
|
19277
|
+
return {
|
|
19278
|
+
content: [{ type: "text", text: lines.join("\n") || "(no threads)" }]
|
|
19279
|
+
};
|
|
19280
|
+
}
|
|
19281
|
+
);
|
|
19282
|
+
server.tool(
|
|
19283
|
+
"get_thread",
|
|
19284
|
+
"Get a compact thread summary by id/ref. While running, includes progress (last tool/thinking) and lastActivityAt.",
|
|
19285
|
+
{ ref: import_zod4.z.string() },
|
|
19286
|
+
async ({ ref }) => {
|
|
19287
|
+
const t = orch.getThread(ref);
|
|
19288
|
+
if (!t) {
|
|
19289
|
+
return { content: [{ type: "text", text: `Thread not found: ${ref}` }], isError: true };
|
|
19290
|
+
}
|
|
19248
19291
|
const live = t.status === "running" || t.status === "queued" ? readTurnLive(t.id) : null;
|
|
19249
|
-
const
|
|
19250
|
-
|
|
19251
|
-
|
|
19252
|
-
|
|
19253
|
-
|
|
19254
|
-
|
|
19255
|
-
|
|
19256
|
-
|
|
19257
|
-
|
|
19258
|
-
|
|
19259
|
-
|
|
19260
|
-
|
|
19261
|
-
|
|
19262
|
-
|
|
19263
|
-
|
|
19264
|
-
|
|
19265
|
-
|
|
19266
|
-
|
|
19267
|
-
|
|
19268
|
-
|
|
19269
|
-
|
|
19270
|
-
|
|
19271
|
-
|
|
19272
|
-
sourceType: t.sourceType,
|
|
19273
|
-
sourceRef: t.sourceRef,
|
|
19274
|
-
branchName: t.branchName,
|
|
19275
|
-
worktreePath: t.worktreePath,
|
|
19276
|
-
sessionId: t.sessionId,
|
|
19277
|
-
queueLength: t.queue.length,
|
|
19278
|
-
messageCount: t.messages.length,
|
|
19279
|
-
devPort: t.devPort,
|
|
19280
|
-
prUrl: t.prUrl,
|
|
19281
|
-
lastError: t.lastError ?? null,
|
|
19282
|
-
stillRunning: t.status === "running" || t.status === "queued",
|
|
19283
|
-
progress: live?.summary ?? null,
|
|
19284
|
-
lastActivityAt: live?.updatedAt ?? null
|
|
19285
|
-
};
|
|
19286
|
-
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
19287
|
-
}
|
|
19288
|
-
);
|
|
19292
|
+
const summary = {
|
|
19293
|
+
id: t.id,
|
|
19294
|
+
title: t.title,
|
|
19295
|
+
status: t.status,
|
|
19296
|
+
agent: t.agent,
|
|
19297
|
+
sourceType: t.sourceType,
|
|
19298
|
+
sourceRef: t.sourceRef,
|
|
19299
|
+
branchName: t.branchName,
|
|
19300
|
+
worktreePath: t.worktreePath,
|
|
19301
|
+
sessionId: t.sessionId,
|
|
19302
|
+
queueLength: t.queue.length,
|
|
19303
|
+
messageCount: t.messages.length,
|
|
19304
|
+
devPort: t.devPort,
|
|
19305
|
+
prUrl: t.prUrl,
|
|
19306
|
+
lastError: t.lastError ?? null,
|
|
19307
|
+
stillRunning: t.status === "running" || t.status === "queued",
|
|
19308
|
+
progress: live?.summary ?? null,
|
|
19309
|
+
lastActivityAt: live?.updatedAt ?? null
|
|
19310
|
+
};
|
|
19311
|
+
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
19312
|
+
}
|
|
19313
|
+
);
|
|
19314
|
+
}
|
|
19289
19315
|
server.tool(
|
|
19290
19316
|
"present_artifact",
|
|
19291
19317
|
"Show an HTML, SVG, markdown, or React document in Sideboard\u2019s Claude-style side column (desktop). Use instead of claude.ai\u2019s artifact tool \u2014 pass the full document content. Prefer type=html for interactive pages. For type=react, pass a single component module that `export default`s a component (JSX/TSX ok) \u2014 Sideboard bootstraps React/ReactDOM/Babel and renders it; only `react`/`react-dom` imports are available, no other npm packages.",
|
|
@@ -19414,9 +19440,9 @@ async function startMcpServer() {
|
|
|
19414
19440
|
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
19415
19441
|
}
|
|
19416
19442
|
);
|
|
19417
|
-
|
|
19418
|
-
|
|
19419
|
-
|
|
19443
|
+
if (!worktreeProfile) {
|
|
19444
|
+
registerSlackTools(server);
|
|
19445
|
+
registerLinearTools(server);
|
|
19420
19446
|
registerScheduleTools(server);
|
|
19421
19447
|
const { getCaffeinateHold: getCaffeinateHold2, setCaffeinateHold: setCaffeinateHold2 } = await Promise.resolve().then(() => (init_caffeinate_hold(), caffeinate_hold_exports));
|
|
19422
19448
|
const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await Promise.resolve().then(() => (init_app_settings(), app_settings_exports));
|
|
@@ -19770,7 +19796,7 @@ async function startMcpServer() {
|
|
|
19770
19796
|
);
|
|
19771
19797
|
server.tool(
|
|
19772
19798
|
"request_review",
|
|
19773
|
-
'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). Opens a new Review chat tab, attaches .
|
|
19799
|
+
'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). Opens a new Review chat tab, attaches .claude/skills/review/SKILL.md when present (else seeds that skill from .sideboard/review.md or the stock template), and sends "Review changes in this workspace." Expect Approve / Approve with nits / Request changes / Needs more information. Pass a worktree thread ref \u2014 not the orchestrator. Then wait_for_turn (loop while stillRunning) / get_turn_result on the returned review tab id.',
|
|
19774
19800
|
{ ref: import_zod4.z.string().describe("Worktree thread id/ref to review") },
|
|
19775
19801
|
async ({ ref }) => {
|
|
19776
19802
|
try {
|
|
@@ -21550,10 +21576,11 @@ function slackReplyThreadTs(msg) {
|
|
|
21550
21576
|
}
|
|
21551
21577
|
var SLACK_PROGRESS_DELAY_MS = 2e4;
|
|
21552
21578
|
var SLACK_PROGRESS_EDIT_MS = 15e3;
|
|
21579
|
+
var SLACK_PROGRESS_PLACEHOLDER = "Thinking\u2026";
|
|
21553
21580
|
function formatSlackWorkingText(summary) {
|
|
21554
21581
|
const line = (summary ?? "").trim();
|
|
21555
|
-
if (!line || /^working/i.test(line)) return
|
|
21556
|
-
return
|
|
21582
|
+
if (!line || /^(working|thinking)/i.test(line)) return SLACK_PROGRESS_PLACEHOLDER;
|
|
21583
|
+
return line;
|
|
21557
21584
|
}
|
|
21558
21585
|
var SLACK_SEEN_REACTION = "eyes";
|
|
21559
21586
|
function writeTokenForTeam(teamId) {
|
|
@@ -21689,7 +21716,7 @@ function startSlackTurnProgress(msg, threadId, opts) {
|
|
|
21689
21716
|
};
|
|
21690
21717
|
const postWorking = async () => {
|
|
21691
21718
|
if (!stillLive()) return;
|
|
21692
|
-
const summary = readTurnLive(threadId())?.summary ??
|
|
21719
|
+
const summary = readTurnLive(threadId())?.summary ?? SLACK_PROGRESS_PLACEHOLDER;
|
|
21693
21720
|
const body = formatSlackWorkingText(summary);
|
|
21694
21721
|
try {
|
|
21695
21722
|
const posted = await postSlackText(target, signForThisMac(body), opts);
|
|
@@ -21707,7 +21734,7 @@ function startSlackTurnProgress(msg, threadId, opts) {
|
|
|
21707
21734
|
};
|
|
21708
21735
|
const editWorking = async () => {
|
|
21709
21736
|
if (!postedTs || !stillLive()) return;
|
|
21710
|
-
const summary = readTurnLive(threadId())?.summary ??
|
|
21737
|
+
const summary = readTurnLive(threadId())?.summary ?? SLACK_PROGRESS_PLACEHOLDER;
|
|
21711
21738
|
const body = formatSlackWorkingText(summary);
|
|
21712
21739
|
if (body === lastBody) return;
|
|
21713
21740
|
lastBody = body;
|
|
@@ -22615,6 +22642,9 @@ init_outbound_watch();
|
|
|
22615
22642
|
REVIEW_REQUEST_PATH,
|
|
22616
22643
|
REVIEW_REQUEST_PREFILL,
|
|
22617
22644
|
REVIEW_REQUEST_TEMPLATE,
|
|
22645
|
+
REVIEW_SKILL_NAME,
|
|
22646
|
+
REVIEW_SKILL_PATH,
|
|
22647
|
+
SESSION_RESET_OCCUPANCY_TOKENS,
|
|
22618
22648
|
SIDEBOARD_FORCE_STOP,
|
|
22619
22649
|
SIDEBOARD_MCP_ALLOWED_TOOLS,
|
|
22620
22650
|
SIDEBOARD_MCP_PROFILE_ENV,
|
|
@@ -22629,6 +22659,7 @@ init_outbound_watch();
|
|
|
22629
22659
|
SlackOAuthCancelledError,
|
|
22630
22660
|
SlackRelayHub,
|
|
22631
22661
|
THINKING_EFFORTS,
|
|
22662
|
+
WORKTREE_MCP_TOOLS,
|
|
22632
22663
|
ackSlackInboundSeen,
|
|
22633
22664
|
addPrStackLayer,
|
|
22634
22665
|
addStackLayerFromThread,
|
|
@@ -22648,6 +22679,7 @@ init_outbound_watch();
|
|
|
22648
22679
|
applyAppEnvironment,
|
|
22649
22680
|
applyCompaction,
|
|
22650
22681
|
applyGithubGitAuthEnv,
|
|
22682
|
+
applyPromptCacheTtlEnv,
|
|
22651
22683
|
applyThreadIntoMain,
|
|
22652
22684
|
applyTurnUsage,
|
|
22653
22685
|
armSchedules,
|
|
@@ -22747,6 +22779,7 @@ init_outbound_watch();
|
|
|
22747
22779
|
ensureGhPreferOrigin,
|
|
22748
22780
|
ensureGlobalCoordinatorCwd,
|
|
22749
22781
|
ensureReviewRequestFile,
|
|
22782
|
+
ensureReviewSkillFile,
|
|
22750
22783
|
ensureSlackCoordinator,
|
|
22751
22784
|
ensureSlackDeviceIdentity,
|
|
22752
22785
|
ensureWorkspace,
|
|
@@ -22885,6 +22918,7 @@ init_outbound_watch();
|
|
|
22885
22918
|
isThisProcessDesktopHost,
|
|
22886
22919
|
isThreadCaffeinated,
|
|
22887
22920
|
isWorkspaceScratchPath,
|
|
22921
|
+
lastRequestOccupancy,
|
|
22888
22922
|
linearAuthorizationHeader,
|
|
22889
22923
|
linearGraphql,
|
|
22890
22924
|
linearOAuthAuthorizeUrl,
|
|
@@ -23052,6 +23086,7 @@ init_outbound_watch();
|
|
|
23052
23086
|
shouldCompactContext,
|
|
23053
23087
|
shouldInjectBrightsyMcp,
|
|
23054
23088
|
shouldRefreshReviewRequestTemplate,
|
|
23089
|
+
shouldResetSessionForOccupancy,
|
|
23055
23090
|
shouldRunWorktreeCleanup,
|
|
23056
23091
|
sideboardHomeDir,
|
|
23057
23092
|
sideboardMcpProfile,
|
|
@@ -23129,6 +23164,7 @@ init_outbound_watch();
|
|
|
23129
23164
|
worktreeDisplayLabelForGroup,
|
|
23130
23165
|
worktreeNameFromPath,
|
|
23131
23166
|
worktreesRoot,
|
|
23167
|
+
wrapReviewSkillMarkdown,
|
|
23132
23168
|
writeInjectedMcpConfig,
|
|
23133
23169
|
writePlanFile,
|
|
23134
23170
|
writeThread,
|