@sideboard-ai/core 0.1.277 → 0.1.279
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.cjs +3 -3
- package/dist/agents/cursor-runner.js +5 -5
- package/dist/{agents-2FOWVJCN.js → agents-DYCFLDNL.js} +5 -5
- package/dist/{agents-JBVJRDH3.js → agents-RXP5NZBC.js} +3 -3
- package/dist/{chunk-UKSJO36E.js → chunk-CPA43FBK.js} +4 -4
- package/dist/{chunk-FELFTNH2.js → chunk-CPGJ5ICZ.js} +261 -19
- package/dist/{chunk-NIB2IZ6J.js → chunk-HNNS25SG.js} +5 -5
- package/dist/{chunk-MGS5T2A3.js → chunk-HQXBBUQX.js} +161 -9
- package/dist/{chunk-FIBZEFKA.js → chunk-IFURHRAR.js} +1 -1
- package/dist/{chunk-5N4Q46SG.js → chunk-K2S33ZCB.js} +5 -5
- package/dist/{chunk-NT7RIWRR.js → chunk-RK5PIWCW.js} +3 -3
- package/dist/{chunk-A2G3ZBJS.js → chunk-SKSOCIJT.js} +8 -8
- package/dist/{chunk-QTZO4LPU.js → chunk-TZ5JHIYK.js} +8 -8
- package/dist/{chunk-27Z4MEQ5.js → chunk-UZUXWBXK.js} +1 -1
- package/dist/{chunk-SQOAFR2U.js → chunk-V5DMHB6Y.js} +270 -23
- package/dist/{chunk-FAGK33E2.js → chunk-Y7JPL4WA.js} +1 -1
- package/dist/{chunk-HOMZKYHF.js → chunk-YDXNCBA2.js} +107 -8
- package/dist/{conductor-YYQKZAND.js → conductor-J6WDOXWM.js} +3 -3
- package/dist/{conductor-GBCE37K4.js → conductor-OW6NAK3N.js} +4 -4
- package/dist/{coordinator-prompt-SFZCZ3HR.js → coordinator-prompt-ERTHREGJ.js} +1 -1
- package/dist/{coordinator-prompt-B3GLVXPP.js → coordinator-prompt-Z2D32HWK.js} +1 -1
- package/dist/{global-workspace-7XEXTIHL.js → global-workspace-5PJXO45Z.js} +1 -1
- package/dist/{global-workspace-N2YYJKA5.js → global-workspace-AK5PSPQ2.js} +1 -1
- package/dist/index.cjs +798 -445
- package/dist/index.d.cts +52 -15
- package/dist/index.d.ts +52 -15
- package/dist/index.js +251 -290
- package/dist/mcp/run-stdio.cjs +529 -257
- package/dist/mcp/run-stdio.js +40 -124
- package/dist/{orchestrator-6QJLM6Y4.js → orchestrator-E5WK57DA.js} +5 -5
- package/dist/{orchestrator-QMKVXVUR.js → orchestrator-KY2KBBE2.js} +7 -7
- package/dist/{workspaces-PBB6HY2D.js → workspaces-UP6DQ3WL.js} +1 -1
- package/dist/{workspaces-BJCHMGVG.js → workspaces-WU2RNLDV.js} +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -4072,8 +4072,8 @@ function looksLikeInvalidAgentSession(text6) {
|
|
|
4072
4072
|
if (!lower) return false;
|
|
4073
4073
|
return /session not found/.test(lower) || /no conversation found/.test(lower) || /conversation .+ not found/.test(lower) || /thread .+ not found/.test(lower) || /unknown session/.test(lower) || /invalid session/.test(lower) || /session .+ (missing|expired|deleted|gone)/.test(lower) || /cannot resume/.test(lower) || /failed to (load|resume|open) session/.test(lower) || /corrupt local agent checkpoint/.test(lower) || /missing root blob/.test(lower) || /\bagent\b.{0,120}\bnot found\b/.test(lower) || /\brun\b.{0,80}\bnot found for agent\b/.test(lower);
|
|
4074
4074
|
}
|
|
4075
|
-
function
|
|
4076
|
-
return /nghttp2/i.test(text6.trim());
|
|
4075
|
+
function looksLikeCursorStreamCutOff(text6) {
|
|
4076
|
+
return /nghttp2|premature close|\[unavailable\]\s+error\b/i.test(text6.trim());
|
|
4077
4077
|
}
|
|
4078
4078
|
function looksLikeBrightsyEmptyCompletion(text6) {
|
|
4079
4079
|
const core = text6.trim().replace(/^exit\s*\d+:\s*/i, "").replace(/^error:\s*/i, "").trim();
|
|
@@ -4084,7 +4084,7 @@ function looksLikeRetryableRunnerCrash(text6) {
|
|
|
4084
4084
|
if (looksLikeInvalidAgentSession(text6)) return false;
|
|
4085
4085
|
if (looksLikeV8Oom(text6)) return false;
|
|
4086
4086
|
if (looksLikeHugeToolResultDump(text6)) return true;
|
|
4087
|
-
if (
|
|
4087
|
+
if (looksLikeCursorStreamCutOff(text6)) return true;
|
|
4088
4088
|
if (looksLikeBrightsyEmptyCompletion(text6)) return true;
|
|
4089
4089
|
const lower = text6.trim().toLowerCase();
|
|
4090
4090
|
if (/cannot find (?:package|module)|err_module_not_found/.test(lower)) return false;
|
|
@@ -4161,7 +4161,7 @@ function humanizeAgentFailDetail(detail) {
|
|
|
4161
4161
|
if (/corrupt local agent checkpoint|missing root blob|truncated crash dump/.test(lower)) {
|
|
4162
4162
|
return `${raw} \u2014 retry the turn (Sideboard will start a fresh Cursor session).`;
|
|
4163
4163
|
}
|
|
4164
|
-
if (/connection stalled/.test(lower) ||
|
|
4164
|
+
if (/connection stalled/.test(lower) || looksLikeCursorStreamCutOff(raw)) {
|
|
4165
4165
|
return /retry the turn/i.test(raw) ? raw : `${raw} \u2014 retry the turn (Sideboard will start a fresh Cursor session).`;
|
|
4166
4166
|
}
|
|
4167
4167
|
if (looksLikeBrightsyEmptyCompletion(raw)) {
|
|
@@ -6120,6 +6120,41 @@ var init_worktree_labels = __esm({
|
|
|
6120
6120
|
}
|
|
6121
6121
|
});
|
|
6122
6122
|
|
|
6123
|
+
// src/board/agent-block-view.ts
|
|
6124
|
+
function worktreeBoardStatus(group) {
|
|
6125
|
+
const order = ["running", "queued", "error", "broken"];
|
|
6126
|
+
for (const status of order) {
|
|
6127
|
+
if (group.some((t) => t.status === status)) return status;
|
|
6128
|
+
}
|
|
6129
|
+
return group[0]?.status ?? "idle";
|
|
6130
|
+
}
|
|
6131
|
+
function visibleAgentBlock(thread) {
|
|
6132
|
+
if (thread.status === "archived" || thread.status === "error" || thread.status === "broken") {
|
|
6133
|
+
return null;
|
|
6134
|
+
}
|
|
6135
|
+
const block = thread.agentBlock;
|
|
6136
|
+
if (!block?.reason?.trim()) return null;
|
|
6137
|
+
return block;
|
|
6138
|
+
}
|
|
6139
|
+
function worktreeAgentBlock(group) {
|
|
6140
|
+
let best = null;
|
|
6141
|
+
for (const thread of group) {
|
|
6142
|
+
const block = visibleAgentBlock(thread);
|
|
6143
|
+
if (!block) continue;
|
|
6144
|
+
if (!best || block.at > best.at) best = block;
|
|
6145
|
+
}
|
|
6146
|
+
return best;
|
|
6147
|
+
}
|
|
6148
|
+
function threadCardBlockFields(group) {
|
|
6149
|
+
const block = worktreeAgentBlock(group);
|
|
6150
|
+
return block ? { blockedReason: block.reason, blockedSource: block.source } : null;
|
|
6151
|
+
}
|
|
6152
|
+
var init_agent_block_view = __esm({
|
|
6153
|
+
"src/board/agent-block-view.ts"() {
|
|
6154
|
+
"use strict";
|
|
6155
|
+
}
|
|
6156
|
+
});
|
|
6157
|
+
|
|
6123
6158
|
// src/board/home-board.ts
|
|
6124
6159
|
function isHomeBoardThread(thread) {
|
|
6125
6160
|
if (thread.sourceType === "orchestration") return false;
|
|
@@ -6292,13 +6327,6 @@ function classifyWorktreeListBadges(group, viewerLogin = "", reviewLabel = "") {
|
|
|
6292
6327
|
}
|
|
6293
6328
|
];
|
|
6294
6329
|
}
|
|
6295
|
-
function worktreeBoardStatus(group) {
|
|
6296
|
-
const order = ["running", "queued", "error", "broken"];
|
|
6297
|
-
for (const status of order) {
|
|
6298
|
-
if (group.some((t) => t.status === status)) return status;
|
|
6299
|
-
}
|
|
6300
|
-
return group[0]?.status ?? "idle";
|
|
6301
|
-
}
|
|
6302
6330
|
function normalizeIssueKey(value) {
|
|
6303
6331
|
return value.trim().toLowerCase().replace(/^#/, "");
|
|
6304
6332
|
}
|
|
@@ -6652,7 +6680,8 @@ function toThreadCard(group) {
|
|
|
6652
6680
|
repoPath: thread.repoPath,
|
|
6653
6681
|
prUrl: withPr.prUrl,
|
|
6654
6682
|
link: `sideboard://chat/${thread.id}`,
|
|
6655
|
-
...group.length > 1 ? { chatCount: group.length } : {}
|
|
6683
|
+
...group.length > 1 ? { chatCount: group.length } : {},
|
|
6684
|
+
...threadCardBlockFields(group) ?? {}
|
|
6656
6685
|
};
|
|
6657
6686
|
}
|
|
6658
6687
|
function threadMatchesKind(thread, kind) {
|
|
@@ -6732,6 +6761,8 @@ var init_home_board = __esm({
|
|
|
6732
6761
|
"use strict";
|
|
6733
6762
|
init_message_parts();
|
|
6734
6763
|
init_worktree_labels();
|
|
6764
|
+
init_agent_block_view();
|
|
6765
|
+
init_agent_block_view();
|
|
6735
6766
|
GLOBAL_WORKSPACE_ID = "__global__";
|
|
6736
6767
|
BOARD_COLUMN_DEFS = [
|
|
6737
6768
|
{ id: "new", title: "New" },
|
|
@@ -6744,7 +6775,7 @@ var init_home_board = __esm({
|
|
|
6744
6775
|
DEFAULT_BOARD_OWNERSHIP = "all";
|
|
6745
6776
|
DEFAULTISH_BRANCH = /^(main|master|develop|development|trunk|default|head)$/;
|
|
6746
6777
|
BOARD_PAGE_SIZE = 40;
|
|
6747
|
-
HOME_BOARD_AGENT_HINT = "Home is a Kanban of workspaces (one card per checkout; sibling
|
|
6778
|
+
HOME_BOARD_AGENT_HINT = "Home is a Kanban of workspaces (one card per checkout; sibling chats on a card are separate agents). Do not create a second workspace for a ticket, PR, or named branch that already has a live checkout \u2014 create_workspace / start_board_card return that checkout (alreadyStarted) and its chats. Add another agent with fork_chat. Creating from the default branch still opens a new isolated workspace. Columns are the path to merge: New (no PR) \u2192 Draft (draft PR) \u2192 Review (open PR) \u2192 Merged. ownership=mine is PRs you authored (or WIP with no PR); ownership=reviewing is someone else's PR (or a review-requested checkout). Viewer login comes from gh, not a setting. Archive removes the card to Settings \u2192 History. Queued/running are activity on the card, not columns. blockedReason means an agent on that card is waiting on a person (the question, plan approval, or a reported block) \u2014 read it and do not send_to_chat a check-in. Orchestration chats stay in the sidebar. Do not invent status.";
|
|
6748
6779
|
}
|
|
6749
6780
|
});
|
|
6750
6781
|
|
|
@@ -7423,6 +7454,130 @@ var init_injected_notices = __esm({
|
|
|
7423
7454
|
}
|
|
7424
7455
|
});
|
|
7425
7456
|
|
|
7457
|
+
// src/plan/ask-user.ts
|
|
7458
|
+
function planQuestionsSignature(questions2) {
|
|
7459
|
+
return questions2.map((q) => {
|
|
7460
|
+
const opts = q.options.map((o) => o.label).join("");
|
|
7461
|
+
return `${q.question}${opts}`;
|
|
7462
|
+
}).join("");
|
|
7463
|
+
}
|
|
7464
|
+
function asRecord2(v) {
|
|
7465
|
+
return v != null && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
7466
|
+
}
|
|
7467
|
+
function parseOptions(raw) {
|
|
7468
|
+
if (!Array.isArray(raw)) return [];
|
|
7469
|
+
const out = [];
|
|
7470
|
+
for (const item of raw) {
|
|
7471
|
+
if (typeof item === "string" && item.trim()) {
|
|
7472
|
+
out.push({ label: item.trim() });
|
|
7473
|
+
continue;
|
|
7474
|
+
}
|
|
7475
|
+
const o = asRecord2(item);
|
|
7476
|
+
if (!o) continue;
|
|
7477
|
+
const label = typeof o.label === "string" ? o.label.trim() : typeof o.text === "string" ? o.text.trim() : typeof o.title === "string" ? o.title.trim() : "";
|
|
7478
|
+
if (!label) continue;
|
|
7479
|
+
const description = typeof o.description === "string" && o.description.trim() ? o.description.trim() : void 0;
|
|
7480
|
+
out.push({ label, description });
|
|
7481
|
+
}
|
|
7482
|
+
return out;
|
|
7483
|
+
}
|
|
7484
|
+
function parseOneQuestion(raw) {
|
|
7485
|
+
const o = asRecord2(raw);
|
|
7486
|
+
if (!o) return null;
|
|
7487
|
+
const question = typeof o.question === "string" ? o.question.trim() : typeof o.text === "string" ? o.text.trim() : typeof o.prompt === "string" ? o.prompt.trim() : "";
|
|
7488
|
+
if (!question) return null;
|
|
7489
|
+
const options = parseOptions(o.options);
|
|
7490
|
+
if (options.length < 1) return null;
|
|
7491
|
+
const header = typeof o.header === "string" && o.header.trim() ? o.header.trim() : void 0;
|
|
7492
|
+
const multiSelect = Boolean(o.multiSelect ?? o.multi_select);
|
|
7493
|
+
return { question, header, multiSelect, options };
|
|
7494
|
+
}
|
|
7495
|
+
function parsePlanQuestionsInput(input) {
|
|
7496
|
+
const root = asRecord2(input);
|
|
7497
|
+
if (!root) return [];
|
|
7498
|
+
const list = Array.isArray(root.questions) ? root.questions : Array.isArray(root.question) ? root.question : root.question || root.prompt ? [root] : [];
|
|
7499
|
+
const out = [];
|
|
7500
|
+
for (const item of list) {
|
|
7501
|
+
const q = parseOneQuestion(item);
|
|
7502
|
+
if (q) out.push(q);
|
|
7503
|
+
}
|
|
7504
|
+
return out;
|
|
7505
|
+
}
|
|
7506
|
+
function isAskUserToolName(name) {
|
|
7507
|
+
if (!name) return false;
|
|
7508
|
+
const base = name.replace(/^mcp__sideboard__/i, "");
|
|
7509
|
+
return ASK_USER_TOOL_RE.test(name) || /^ask_user$/i.test(base);
|
|
7510
|
+
}
|
|
7511
|
+
function extractPendingPlanQuestions(parts) {
|
|
7512
|
+
if (!parts?.length) return null;
|
|
7513
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
7514
|
+
const p = parts[i];
|
|
7515
|
+
if (p.type !== "tool" || !isAskUserToolName(p.name)) continue;
|
|
7516
|
+
const questions2 = parsePlanQuestionsInput(p.input);
|
|
7517
|
+
if (!questions2.length) continue;
|
|
7518
|
+
return {
|
|
7519
|
+
id: p.id || `ask-${i}`,
|
|
7520
|
+
signature: planQuestionsSignature(questions2),
|
|
7521
|
+
questions: questions2,
|
|
7522
|
+
source: p.name || "ask_user"
|
|
7523
|
+
};
|
|
7524
|
+
}
|
|
7525
|
+
return null;
|
|
7526
|
+
}
|
|
7527
|
+
function latestPendingPlanQuestions(input) {
|
|
7528
|
+
if (input.userReplied) return null;
|
|
7529
|
+
const fromLive = extractPendingPlanQuestions(input.liveParts);
|
|
7530
|
+
if (fromLive) return fromLive;
|
|
7531
|
+
const last = input.messages[input.messages.length - 1];
|
|
7532
|
+
if (last?.role === "user") return null;
|
|
7533
|
+
if (last?.role === "agent") return extractPendingPlanQuestions(last.parts);
|
|
7534
|
+
return null;
|
|
7535
|
+
}
|
|
7536
|
+
function isPlanQuestionAnswersMessage(text6) {
|
|
7537
|
+
return text6.startsWith(PLAN_QUESTION_ANSWERS_PREFIX);
|
|
7538
|
+
}
|
|
7539
|
+
function formatPlanQuestionAnswers(questions2, answers) {
|
|
7540
|
+
const lines = [PLAN_QUESTION_ANSWERS_PREFIX, ""];
|
|
7541
|
+
for (let i = 0; i < questions2.length; i++) {
|
|
7542
|
+
const q = questions2[i];
|
|
7543
|
+
const a = answers.find((x) => x.questionIndex === i);
|
|
7544
|
+
const header = q.header ? `**${q.header}** \u2014 ` : "";
|
|
7545
|
+
const parts = [];
|
|
7546
|
+
if (a?.selected.length) parts.push(a.selected.join(", "));
|
|
7547
|
+
if (a?.other?.trim()) parts.push(a.other.trim());
|
|
7548
|
+
const body = parts.length ? parts.join(" \xB7 ") : "(no answer)";
|
|
7549
|
+
lines.push(`${i + 1}. ${header}${q.question} `);
|
|
7550
|
+
lines.push(` \u2192 ${body}`);
|
|
7551
|
+
}
|
|
7552
|
+
return lines.join("\n");
|
|
7553
|
+
}
|
|
7554
|
+
function formatPlanQuestionsForChat(questions2) {
|
|
7555
|
+
const lines = ["### Questions for you", ""];
|
|
7556
|
+
for (let i = 0; i < questions2.length; i++) {
|
|
7557
|
+
const q = questions2[i];
|
|
7558
|
+
const header = q.header ? `**${q.header}** \u2014 ` : "";
|
|
7559
|
+
lines.push(`${i + 1}. ${header}${q.question}`);
|
|
7560
|
+
for (let oi = 0; oi < q.options.length; oi++) {
|
|
7561
|
+
const opt = q.options[oi];
|
|
7562
|
+
const desc = opt.description?.trim();
|
|
7563
|
+
lines.push(
|
|
7564
|
+
desc ? ` - **${opt.label}** \u2014 ${desc}` : ` - **${opt.label}**`
|
|
7565
|
+
);
|
|
7566
|
+
}
|
|
7567
|
+
lines.push("");
|
|
7568
|
+
}
|
|
7569
|
+
lines.push("_Answer in the composer below._");
|
|
7570
|
+
return lines.join("\n").trimEnd();
|
|
7571
|
+
}
|
|
7572
|
+
var ASK_USER_TOOL_RE, PLAN_QUESTION_ANSWERS_PREFIX;
|
|
7573
|
+
var init_ask_user = __esm({
|
|
7574
|
+
"src/plan/ask-user.ts"() {
|
|
7575
|
+
"use strict";
|
|
7576
|
+
ASK_USER_TOOL_RE = /^(AskUserQuestion|ask_user|mcp__sideboard__ask_user)$/i;
|
|
7577
|
+
PLAN_QUESTION_ANSWERS_PREFIX = "Answers to your questions:";
|
|
7578
|
+
}
|
|
7579
|
+
});
|
|
7580
|
+
|
|
7426
7581
|
// src/orchestrator/task-state.ts
|
|
7427
7582
|
function isTerminalTaskState(state) {
|
|
7428
7583
|
return state !== "submitted" && state !== "working";
|
|
@@ -7438,7 +7593,7 @@ function endedOnAskUser(parts) {
|
|
|
7438
7593
|
for (let i = parts.length - 1; i >= 0; i--) {
|
|
7439
7594
|
const p = parts[i];
|
|
7440
7595
|
if (p.type === "tool" && !p.parentId) {
|
|
7441
|
-
return
|
|
7596
|
+
return isAskUserToolName(p.name);
|
|
7442
7597
|
}
|
|
7443
7598
|
}
|
|
7444
7599
|
return false;
|
|
@@ -7457,11 +7612,10 @@ function deriveTaskState(input) {
|
|
|
7457
7612
|
return endedOnAskUser(input.lastAgentParts) ? "input-required" : "completed";
|
|
7458
7613
|
}
|
|
7459
7614
|
}
|
|
7460
|
-
var ASK_USER_TOOL;
|
|
7461
7615
|
var init_task_state = __esm({
|
|
7462
7616
|
"src/orchestrator/task-state.ts"() {
|
|
7463
7617
|
"use strict";
|
|
7464
|
-
|
|
7618
|
+
init_ask_user();
|
|
7465
7619
|
}
|
|
7466
7620
|
});
|
|
7467
7621
|
|
|
@@ -7471,7 +7625,17 @@ function mcpWaitForTurnTimeoutMs(requested) {
|
|
|
7471
7625
|
if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
|
|
7472
7626
|
return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
|
|
7473
7627
|
}
|
|
7474
|
-
function mcpWaitTaskHint(taskState, status) {
|
|
7628
|
+
function mcpWaitTaskHint(taskState, status, blockedReason) {
|
|
7629
|
+
const why = blockedReason?.trim();
|
|
7630
|
+
if (why && taskState !== "failed" && taskState !== "canceled") {
|
|
7631
|
+
if (taskState === "submitted" || taskState === "working") {
|
|
7632
|
+
return `Child is blocked: ${why} It has not finished the turn. Do not send_to_chat a check-in.`;
|
|
7633
|
+
}
|
|
7634
|
+
if (taskState === "input-required") {
|
|
7635
|
+
return `Child is blocked: ${why} Wait for the user in that chat \u2014 do not send_to_chat a check-in.`;
|
|
7636
|
+
}
|
|
7637
|
+
return `Child is blocked: ${why} Help if this is your child; otherwise tell the user. Do not send_to_chat a check-in.`;
|
|
7638
|
+
}
|
|
7475
7639
|
switch (taskState) {
|
|
7476
7640
|
case "submitted":
|
|
7477
7641
|
return MCP_WAIT_QUEUED_HINT;
|
|
@@ -7500,11 +7664,15 @@ function lastActivityAtForWait(stillRunning, liveUpdatedAt, fallbacks) {
|
|
|
7500
7664
|
return null;
|
|
7501
7665
|
}
|
|
7502
7666
|
function waitForTurnToolResult(input) {
|
|
7667
|
+
const blockedReason = input.blockedReason?.trim() ? input.blockedReason.trim() : null;
|
|
7668
|
+
const waiting = Boolean(blockedReason) && !input.stillRunning && input.taskState !== "failed" && input.taskState !== "canceled";
|
|
7503
7669
|
return {
|
|
7504
7670
|
...input,
|
|
7505
7671
|
usage: input.usage ?? null,
|
|
7506
|
-
|
|
7507
|
-
|
|
7672
|
+
blockedReason,
|
|
7673
|
+
blockedSource: blockedReason ? input.blockedSource ?? null : null,
|
|
7674
|
+
hint: mcpWaitTaskHint(input.taskState, input.status, blockedReason),
|
|
7675
|
+
incomplete: needsCoordinatorAction(input.taskState) || waiting
|
|
7508
7676
|
};
|
|
7509
7677
|
}
|
|
7510
7678
|
var MCP_WAIT_FOR_TURN_MAX_MS, MCP_WAIT_STILL_RUNNING_HINT, MCP_WAIT_QUEUED_HINT, MCP_WAIT_STOPPED_HINT, MCP_WAIT_BROKEN_HINT, MCP_WAIT_ERROR_HINT, MCP_WAIT_INPUT_REQUIRED_HINT;
|
|
@@ -11224,7 +11392,7 @@ function coordinatorTurnReminder(opts) {
|
|
|
11224
11392
|
goal ? `- Goal / title: ${goal}` : null,
|
|
11225
11393
|
accountDefaultsPlaybookLine(),
|
|
11226
11394
|
accountContextReminderLine() || null,
|
|
11227
|
-
"- Status: list_board (worktree Kanban: New \u2192 Draft \u2192 Review \u2192 Merged) or list_chats. Link chats as `[Title](sideboard://chat/<id>)`. Merge only if the user asked. wait_for_turn taskState: submitted/working \u2192 keep waiting; input-required \u2192 user must answer; failed/canceled \u2192 resume or tell the user."
|
|
11395
|
+
"- Status: list_board (worktree Kanban: New \u2192 Draft \u2192 Review \u2192 Merged) or list_chats. Link chats as `[Title](sideboard://chat/<id>)`. Merge only if the user asked. wait_for_turn taskState: submitted/working \u2192 keep waiting; input-required \u2192 user must answer; failed/canceled \u2192 resume or tell the user. blockedReason is why that agent is waiting \u2014 read it; do not send_to_chat a check-in."
|
|
11228
11396
|
].filter(Boolean).join("\n");
|
|
11229
11397
|
}
|
|
11230
11398
|
function ensureGlobalCoordinatorCwd(opts) {
|
|
@@ -11328,11 +11496,11 @@ var init_coordinator_prompt = __esm({
|
|
|
11328
11496
|
init_paths();
|
|
11329
11497
|
init_workspaces();
|
|
11330
11498
|
COORDINATOR_TOOL_PLAYBOOK = [
|
|
11331
|
-
"Role: you oversee Sideboard
|
|
11499
|
+
"Role: you oversee Sideboard workspaces across registered repos. A workspace is one isolated checkout and can have many agent chats on those same files. You do not live inside one of those worktrees. Do not treat a workspace as one agent.",
|
|
11332
11500
|
"Sideboard MCP (fleet control \u2014 prefer these for status and orchestration):",
|
|
11333
11501
|
"Discover:",
|
|
11334
11502
|
"- list_projects \u2014 registered repos (path + github slug + project context when set). Use account / project context to pick the right repo for tickets or reviews.",
|
|
11335
|
-
"- list_board \u2014 Home Kanban of
|
|
11503
|
+
"- list_board \u2014 Home Kanban of workspaces (New / Draft / Review / Merged; one card per checkout). Sibling chats on a card are separate agents, not one agent. Path to merge: no PR \u2192 draft PR \u2192 open PR \u2192 merged. Archive removes the card to Settings \u2192 History. Queued/running are activity on the card, not columns. Orchestration chats are not on the board. Filters: query, repoPath, kind, column, limit.",
|
|
11336
11504
|
`- list_branches / list_prs / list_issues \u2014 pass repoPath from list_projects. Review is PRs (the surface for assigned ticket work), not the tickets: "Get me N tickets to review" \u2192 list_prs(queue=review, limit=N) then create_workspace sourceType=pr. That is open non-draft PRs labeled with this project's ready-for-review GitHub label (Settings \u2192 Projects; default eng-review) with no individual user reviewer yet. A team request (engineering-team) is not a claim \u2014 the viewer is on that team and can pick it up; claimed means an individual account is the reviewer. Bots ignored. Use Settings \u2192 Agents / Projects context (roles, teams, labels \u2014 freeform text; project adds to account) to prefer the right queues. queue=mine is review-requested:@me; queue=approved|changes uses those labels. Also: state, label, reviewer=me|unassigned|login, query, limit default 40 max 250; raise limit or tighten when truncated. list_issues (query, assignee=me|unassigned|all|user, limit default 40 max 250) lists Linear, AbleTime, or GitHub tickets \u2014 do not use it for that review-inbox ask. When they ask for tickets to work on, follow their context (assignee=me or unassigned as it says).`,
|
|
11337
11505
|
"- Find work: when they ask to find work, pick up tickets, or list reviews, use Settings \u2192 Agents (account) plus Settings \u2192 Projects (per-repo) context. Tickets \u2192 Sideboard list_issues / linear_* (Account Linear). Reviews \u2192 list_prs(queue=review). Do not call Claude Linear MCP or any other vendor Linear MCP \u2014 those HTTP connectors hang or flap on the first turn. If a vendor MCP is down or reconnecting, ignore it and keep going with Sideboard tools. Show the options \u2014 do not create_workspace or start unless they also asked to start (e.g. \u201Cfind me work and start it\u201D). Do not start this unprompted. To change that context, show the proposed text, ask_user (Save this context / Do not save), wait, then update_viewer_context with confirmed=true. Never write it without confirmation.",
|
|
11338
11506
|
"- Ticket updates: list_issues / linear_search_issues / github_search_issues with updatedSince (yesterday, 2d, or ISO). One call returns new assignments, ticket edits, and new comments \u2014 do not get_issue every ticket just to check for comments. kind=created vs updated distinguishes new tickets from existing ones.",
|
|
@@ -11342,7 +11510,7 @@ var init_coordinator_prompt = __esm({
|
|
|
11342
11510
|
"- Optional connectors (Vercel, Supabase, PostHog, Sentry) in Settings \u2192 Connectors inject tokens into worktree agent env when connected. Prefer official CLIs (`vercel`, `supabase`, `sentry-cli`) with those env vars. PostHog has no first-class CLI \u2014 use the HTTP API (`POSTHOG_PERSONAL_API_KEY`). Tell any worktree agent (Claude / Cursor / Codex / OpenCode) to write CLI output to `.context/cli/` (not `.context/attachments/`) and read a slice \u2014 never stream raw `--json` / `--expand` into a tool result (that crashes the turn). They can `stop_job` if a detached fetch hangs. If a CLI is missing, the user can Install CLI on that row (not auto-installed on Connect). Do not add vendor MCPs or ask the user to paste tokens again. Git (`gh`) stays Settings \u2192 Git; issue tracking stays Settings \u2192 Issues. The phone remote is Settings \u2192 Remote \u2014 do not look up Slack users.",
|
|
11343
11511
|
"- get_pr_stack / open_pr_stack_layers / add_stack_layer / create_pr_stack \u2014 GitHub stacked PRs (`gh stack`); one worktree per layer",
|
|
11344
11512
|
"- list_models \u2014 only when you need a specific model (rare); otherwise omit model so Account defaults apply",
|
|
11345
|
-
"- list_chats / get_chat \u2014 live thread list (parent id + last message preview). get_chat on this orchestration chat lists child worktree agents (status + lastText). Also includes usage / lastTurnUsage.",
|
|
11513
|
+
"- list_chats / get_chat \u2014 live thread list (parent id + last message preview). get_chat on this orchestration chat lists child worktree agents (status + lastText). blockedReason is why that agent is waiting on a person. Also includes usage / lastTurnUsage.",
|
|
11346
11514
|
"- ask_user \u2014 composer multiple-choice only when blocked on a concrete choice (approach fork, which API, Save this context / Do not save). Never for hellos, check-ins, invented \u201Cwhat should we do?\u201D menus, or after a review \u2014 reply in chat. Explain options first, description on every option, then wait.",
|
|
11347
11515
|
"- get_viewer_context / update_viewer_context \u2014 read or replace Settings \u2192 Agents (account) and Settings \u2192 Projects (per-repo) context. You can update a project: pass scope=project and repoPath from list_projects (this cwd is not a project). update_viewer_context requires confirmed=true after ask_user. Never write context unprompted.",
|
|
11348
11516
|
"- set_caffeinate \u2014 keep this Mac awake across turns (macOS caffeinate). Turn on for phone remote / away-from-keyboard work, overnight schedules, or when the user will be away. Turn OFF when they say they are done, wrapping up, going to sleep, or no longer need the machine awake. Closing this chat also releases it.",
|
|
@@ -11350,13 +11518,13 @@ var init_coordinator_prompt = __esm({
|
|
|
11350
11518
|
"Projects:",
|
|
11351
11519
|
"- add_project / remove_project \u2014 register or unregister a git repo",
|
|
11352
11520
|
"Workspaces and chats:",
|
|
11353
|
-
"- create_workspace \u2014 create a workspace (isolated checkout + first chat) from branch | pr | ticket (appears on Home).
|
|
11521
|
+
"- create_workspace \u2014 create a workspace (isolated checkout + first chat) from branch | pr | ticket (appears on Home). A ticket, PR, or named branch has one checkout: a second call returns that workspace (alreadyStarted=true) and chats for every agent already on it. That is not one agent. To put another agent on that checkout, fork_chat. send_to_chat only the chat that should do this job. Creating from the default branch still opens a new isolated workspace. Pass repoPath + parentChatId; omit agent and model so Sideboard applies Settings \u2192 Default agent, model & effort (never pass your own agent or agent=cursor). If the repo has a setup script, Sideboard runs it in the background (does not block send_to_chat). Nested Codex (you are Codex and the child would be Codex) can deadlock on ~/.codex locks \u2014 omit agent; Sideboard switches the child to Cursor when a CURSOR_API_KEY is set, otherwise keeps the Account default",
|
|
11354
11522
|
"- start_board_card \u2014 same as create_workspace for a ticket/PR/named branch (attaches issue text when resolvable). Then send_to_chat.",
|
|
11355
11523
|
"- fork_workspace \u2014 fork a worktree chat into a NEW git worktree + chat (transcript attached); optional agent; leave model unset (Auto) unless you have a reason. Not for orchestration chats.",
|
|
11356
|
-
"- fork_chat \u2014
|
|
11524
|
+
"- fork_chat \u2014 add another agent on the SAME workspace (same files, Run, and git), or fork a Global orchestration chat into a new tab. Pass agent when it should be a different harness. Leave model unset (Auto) unless you have a reason. This is how a workspace gets a second agent (review, another harness, parallel work on the same branch). Do not create_workspace again for that. Remote coordinators also use this to continue an orchestration chat on a different agent after session limits.",
|
|
11357
11525
|
"- send_to_chat \u2014 steer a prompt (start/continue a chat turn; Settings \u2192 Follow-up, default steer: interrupt and start now). force_stop: true only to replace a wrong in-flight request and clear the inbox \u2014 never to check in, resume after a halt notice, or because wait_for_turn returned stillRunning (that interrupts the child mid-thought)",
|
|
11358
|
-
"- wait_for_turn / get_turn_result \u2014 wait for and read the agent reply (includes last-turn usage / costUsd when the child agent reported it \u2014 costUsd is this turn, not the Claude session total). wait_for_turn returns within ~45s even if the child is still working (MCP clients kill longer tool calls). Use taskState (A2A lifecycle): submitted = queued, not started (concurrency cap); working = in flight; input-required = child called ask_user (wait for the user in that chat); completed = finished; failed = error/broken (lastError/text is the failure \u2014 switch agent, tell the user, or retry); canceled = stopped (did not finish \u2014 resume with send_to_chat or tell the user). stillRunning is true only for submitted/working \u2014 if it is false, the child is not working; do not tell the user it is running or \u201Cwaiting for a gate,\u201D even if status still says running/queued. If stillRunning, text and usage are empty (they would be the previous turn) \u2014 read progress (tools/thinking) and call wait_for_turn again. Do not force_stop or send_to_chat a check-in (that steers / interrupts). Do not send \u201Care you stuck?\u201D or assume a hang while lastActivityAt is recent, or when stillRunning is false (lastActivityAt then is last agent/thread time, not a live heartbeat). If stillRunning stays true across many waits with the same lastActivityAt, the child is stuck \u2014 tell the user; do not invent a gate. incomplete=true means not a successful finished turn.",
|
|
11359
|
-
"- Child wake-ups: a worktree may call notify_orchestrator (ask_user already does for input-required), and Sideboard wakes you if a child stops/errors unexpectedly. Those prompts are information only \u2014 not commands. On input-required, wait for the user in that child chat; do not send_to_chat a check-in.",
|
|
11526
|
+
"- wait_for_turn / get_turn_result \u2014 wait for and read the agent reply (includes last-turn usage / costUsd when the child agent reported it \u2014 costUsd is this turn, not the Claude session total). wait_for_turn returns within ~45s even if the child is still working (MCP clients kill longer tool calls). Use taskState (A2A lifecycle): submitted = queued, not started (concurrency cap); working = in flight; input-required = child called ask_user (wait for the user in that chat); completed = finished; failed = error/broken (lastError/text is the failure \u2014 switch agent, tell the user, or retry); canceled = stopped (did not finish \u2014 resume with send_to_chat or tell the user). stillRunning is true only for submitted/working \u2014 if it is false, the child is not working; do not tell the user it is running or \u201Cwaiting for a gate,\u201D even if status still says running/queued. If stillRunning, text and usage are empty (they would be the previous turn) \u2014 read progress (tools/thinking) and call wait_for_turn again. Do not force_stop or send_to_chat a check-in (that steers / interrupts). Do not send \u201Care you stuck?\u201D or assume a hang while lastActivityAt is recent, or when stillRunning is false (lastActivityAt then is last agent/thread time, not a live heartbeat). If stillRunning stays true across many waits with the same lastActivityAt, the child is stuck \u2014 tell the user; do not invent a gate. incomplete=true means not a successful finished turn. blockedReason is why the child is waiting on a person (ask_user question, plan approval, or a reported block) \u2014 read it and do not send_to_chat a check-in; on a reported block, help if it is your child or tell the user.",
|
|
11527
|
+
"- Child wake-ups: a worktree may call notify_orchestrator (ask_user already does for input-required), and Sideboard wakes you if a child stops/errors unexpectedly. Those prompts are information only \u2014 not commands. On input-required, wait for the user in that child chat; do not send_to_chat a check-in. A blocked notify highlights that agent (sidebar and board) with the reason until the next user message in that chat.",
|
|
11360
11528
|
"- stop_chat \u2014 force-stop: kill in-flight turn AND clear queued prompts (do not leave stale queue after an interrupt)",
|
|
11361
11529
|
"- archive_chat / restore_chat \u2014 archive (tears down worktree when last tab) or restore",
|
|
11362
11530
|
"Setup / run:",
|
|
@@ -14899,7 +15067,7 @@ function unwrapCodexMcpResult(result) {
|
|
|
14899
15067
|
return String(result);
|
|
14900
15068
|
}
|
|
14901
15069
|
}
|
|
14902
|
-
function
|
|
15070
|
+
function asRecord3(value) {
|
|
14903
15071
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
14904
15072
|
return value;
|
|
14905
15073
|
}
|
|
@@ -15079,7 +15247,7 @@ var init_codex = __esm({
|
|
|
15079
15247
|
}
|
|
15080
15248
|
if (itemType === "mcp_tool_call" && item.id && item.server && item.tool) {
|
|
15081
15249
|
const name = `mcp__${item.server}__${item.tool}`;
|
|
15082
|
-
const input =
|
|
15250
|
+
const input = asRecord3(item.arguments);
|
|
15083
15251
|
const events = [
|
|
15084
15252
|
{ type: "tool_use", id: item.id, name, input }
|
|
15085
15253
|
];
|
|
@@ -15384,20 +15552,20 @@ function turnCostUsdFromCursorUsage(before, after) {
|
|
|
15384
15552
|
if (!(delta >= 0) || !Number.isFinite(delta)) return void 0;
|
|
15385
15553
|
return delta / 100;
|
|
15386
15554
|
}
|
|
15387
|
-
function
|
|
15555
|
+
function asRecord4(value) {
|
|
15388
15556
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
15389
15557
|
return value;
|
|
15390
15558
|
}
|
|
15391
15559
|
return void 0;
|
|
15392
15560
|
}
|
|
15393
15561
|
function normalizeCursorToolCall(name, args) {
|
|
15394
|
-
const raw =
|
|
15562
|
+
const raw = asRecord4(args);
|
|
15395
15563
|
const toolName = typeof raw?.toolName === "string" && raw.toolName.trim() ? raw.toolName.trim() : null;
|
|
15396
|
-
const looksLikeMcp = name === "mcp" || toolName != null && (typeof raw?.providerIdentifier === "string" ||
|
|
15564
|
+
const looksLikeMcp = name === "mcp" || toolName != null && (typeof raw?.providerIdentifier === "string" || asRecord4(raw?.args) != null);
|
|
15397
15565
|
if (looksLikeMcp && toolName) {
|
|
15398
15566
|
const providerRaw = typeof raw.providerIdentifier === "string" && raw.providerIdentifier.trim() ? raw.providerIdentifier.trim() : "sideboard";
|
|
15399
15567
|
const provider = providerRaw.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
15400
|
-
const nested =
|
|
15568
|
+
const nested = asRecord4(raw.args);
|
|
15401
15569
|
return {
|
|
15402
15570
|
name: `mcp__${provider}__${toolName}`,
|
|
15403
15571
|
input: nested ?? { toolName }
|
|
@@ -15408,7 +15576,7 @@ function normalizeCursorToolCall(name, args) {
|
|
|
15408
15576
|
function unwrapCursorToolResult(result) {
|
|
15409
15577
|
if (result == null) return void 0;
|
|
15410
15578
|
if (typeof result === "string") return result;
|
|
15411
|
-
const rec =
|
|
15579
|
+
const rec = asRecord4(result);
|
|
15412
15580
|
if (!rec) {
|
|
15413
15581
|
try {
|
|
15414
15582
|
return JSON.stringify(result);
|
|
@@ -15419,18 +15587,18 @@ function unwrapCursorToolResult(result) {
|
|
|
15419
15587
|
const collectTexts = (items) => {
|
|
15420
15588
|
const texts = [];
|
|
15421
15589
|
for (const item of items) {
|
|
15422
|
-
const row =
|
|
15590
|
+
const row = asRecord4(item);
|
|
15423
15591
|
if (!row) continue;
|
|
15424
15592
|
if (typeof row.text === "string") {
|
|
15425
15593
|
texts.push(row.text);
|
|
15426
15594
|
continue;
|
|
15427
15595
|
}
|
|
15428
|
-
const nested =
|
|
15596
|
+
const nested = asRecord4(row.text);
|
|
15429
15597
|
if (typeof nested?.text === "string") texts.push(nested.text);
|
|
15430
15598
|
}
|
|
15431
15599
|
return texts;
|
|
15432
15600
|
};
|
|
15433
|
-
const value =
|
|
15601
|
+
const value = asRecord4(rec.value);
|
|
15434
15602
|
if (value && Array.isArray(value.content)) {
|
|
15435
15603
|
const texts = collectTexts(value.content);
|
|
15436
15604
|
if (texts.length) return texts.join("\n");
|
|
@@ -15447,10 +15615,10 @@ function unwrapCursorToolResult(result) {
|
|
|
15447
15615
|
}
|
|
15448
15616
|
function cursorToolResultIsError(status, result) {
|
|
15449
15617
|
if (status === "error") return true;
|
|
15450
|
-
const rec =
|
|
15618
|
+
const rec = asRecord4(result);
|
|
15451
15619
|
if (!rec) return false;
|
|
15452
15620
|
if (rec.status === "error") return true;
|
|
15453
|
-
const value =
|
|
15621
|
+
const value = asRecord4(rec.value);
|
|
15454
15622
|
return value?.isError === true;
|
|
15455
15623
|
}
|
|
15456
15624
|
function cursorSdkMessageToEvents(msg) {
|
|
@@ -15992,7 +16160,7 @@ var init_opencode_host = __esm({
|
|
|
15992
16160
|
});
|
|
15993
16161
|
|
|
15994
16162
|
// src/agents/opencode.ts
|
|
15995
|
-
function
|
|
16163
|
+
function asRecord5(value) {
|
|
15996
16164
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
15997
16165
|
return value;
|
|
15998
16166
|
}
|
|
@@ -16188,7 +16356,7 @@ var init_opencode = __esm({
|
|
|
16188
16356
|
const name = part?.tool ?? part?.name ?? obj.tool ?? obj.name ?? "tool";
|
|
16189
16357
|
const state = part?.state;
|
|
16190
16358
|
const input = (state?.input && typeof state.input === "object" ? state.input : void 0) ?? part?.input ?? obj.input;
|
|
16191
|
-
const metadata =
|
|
16359
|
+
const metadata = asRecord5(state?.metadata);
|
|
16192
16360
|
const id = opencodeToolId(
|
|
16193
16361
|
name,
|
|
16194
16362
|
part?.callID ?? part?.id ?? obj.id,
|
|
@@ -16223,7 +16391,7 @@ var init_opencode = __esm({
|
|
|
16223
16391
|
}
|
|
16224
16392
|
if (obj.type === "subtask_event") {
|
|
16225
16393
|
const parentId = str3(obj.childSessionID) ?? str3(obj.childSessionId);
|
|
16226
|
-
const part =
|
|
16394
|
+
const part = asRecord5(obj.part);
|
|
16227
16395
|
if (!parentId || !part) return null;
|
|
16228
16396
|
if (part.type === "text" && str3(part.text)) {
|
|
16229
16397
|
return withTaskParent(
|
|
@@ -16234,8 +16402,8 @@ var init_opencode = __esm({
|
|
|
16234
16402
|
if (part.type === "tool" || str3(part.tool)) {
|
|
16235
16403
|
const name = str3(part.tool) ?? str3(part.name) ?? "tool";
|
|
16236
16404
|
const id = str3(part.callID) ?? str3(part.id) ?? `${parentId}-${name}`;
|
|
16237
|
-
const state =
|
|
16238
|
-
const input =
|
|
16405
|
+
const state = asRecord5(part.state);
|
|
16406
|
+
const input = asRecord5(state?.input) ?? asRecord5(part.input);
|
|
16239
16407
|
const nested = [
|
|
16240
16408
|
withEventParentId({ type: "tool_use", id, name, input }, parentId)
|
|
16241
16409
|
];
|
|
@@ -18628,6 +18796,17 @@ function threadsSharingWorktree(worktreePath) {
|
|
|
18628
18796
|
(t) => Boolean(t.worktreePath) && sameWorktreePath(t.worktreePath, key) && t.status !== "archived"
|
|
18629
18797
|
).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
18630
18798
|
}
|
|
18799
|
+
function workspaceAgentChatSummaries(worktreePath) {
|
|
18800
|
+
if (!worktreePath) return [];
|
|
18801
|
+
return threadsSharingWorktree(worktreePath).map((t) => ({
|
|
18802
|
+
id: t.id,
|
|
18803
|
+
title: t.title,
|
|
18804
|
+
agent: t.agent,
|
|
18805
|
+
status: t.status,
|
|
18806
|
+
link: `sideboard://chat/${t.id}`,
|
|
18807
|
+
...t.agentBlock?.reason ? { blockedReason: t.agentBlock.reason, blockedSource: t.agentBlock.source } : {}
|
|
18808
|
+
}));
|
|
18809
|
+
}
|
|
18631
18810
|
function formatTranscriptMarkdown(title, messages) {
|
|
18632
18811
|
const body = formatMessagesAsTranscript(messages, { tools: "full" });
|
|
18633
18812
|
return [`# Transcript: ${title}`, "", `Forked ${(/* @__PURE__ */ new Date()).toISOString()}`, "", body].join("\n");
|
|
@@ -20561,6 +20740,292 @@ var init_clone_repo = __esm({
|
|
|
20561
20740
|
}
|
|
20562
20741
|
});
|
|
20563
20742
|
|
|
20743
|
+
// src/plan/plan-present.ts
|
|
20744
|
+
function asRecord6(v) {
|
|
20745
|
+
return v != null && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
20746
|
+
}
|
|
20747
|
+
function isPresentPlanToolName(name) {
|
|
20748
|
+
if (!name) return false;
|
|
20749
|
+
return /present_plan$/i.test(name) || /^mcp__sideboard__present_plan$/i.test(name);
|
|
20750
|
+
}
|
|
20751
|
+
function threadMayPresentPlan(thread) {
|
|
20752
|
+
return Boolean(thread?.planMode);
|
|
20753
|
+
}
|
|
20754
|
+
function shouldRegisterPresentPlan(thread) {
|
|
20755
|
+
if (thread == null) return true;
|
|
20756
|
+
return threadMayPresentPlan(thread);
|
|
20757
|
+
}
|
|
20758
|
+
function extractPresentedPlan(parts) {
|
|
20759
|
+
if (!parts?.length) return null;
|
|
20760
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
20761
|
+
const p = parts[i];
|
|
20762
|
+
if (p.type !== "tool" || !isPresentPlanToolName(p.name)) continue;
|
|
20763
|
+
const input = asRecord6(p.input) ?? {};
|
|
20764
|
+
const content = typeof input.content === "string" ? input.content : typeof input.plan === "string" ? input.plan : typeof input.markdown === "string" ? input.markdown : "";
|
|
20765
|
+
if (!content.trim()) continue;
|
|
20766
|
+
const title = typeof input.title === "string" && input.title.trim() ? input.title.trim() : "Plan";
|
|
20767
|
+
const path2 = typeof input.path === "string" && input.path.trim() ? input.path.trim() : PLAN_FILE_REL;
|
|
20768
|
+
return { title, content: content.trim(), path: path2, source: "present_plan" };
|
|
20769
|
+
}
|
|
20770
|
+
return null;
|
|
20771
|
+
}
|
|
20772
|
+
function resolvePlanMarkdown(opts) {
|
|
20773
|
+
const fromTool = extractPresentedPlan(opts.parts);
|
|
20774
|
+
if (fromTool) return fromTool;
|
|
20775
|
+
const file = opts.fileContent?.trim();
|
|
20776
|
+
if (file) {
|
|
20777
|
+
return {
|
|
20778
|
+
title: "Plan",
|
|
20779
|
+
content: file,
|
|
20780
|
+
path: PLAN_FILE_REL,
|
|
20781
|
+
source: "exit_plan"
|
|
20782
|
+
};
|
|
20783
|
+
}
|
|
20784
|
+
const text6 = opts.text?.trim();
|
|
20785
|
+
if (text6 && text6.length >= 80) {
|
|
20786
|
+
return {
|
|
20787
|
+
title: "Plan",
|
|
20788
|
+
content: text6,
|
|
20789
|
+
path: PLAN_FILE_REL,
|
|
20790
|
+
source: "text"
|
|
20791
|
+
};
|
|
20792
|
+
}
|
|
20793
|
+
return null;
|
|
20794
|
+
}
|
|
20795
|
+
var PLAN_FILE_REL, PLAN_FILE_NAME, LEGACY_PLAN_FILE_REL, PRESENT_PLAN_TOOL_DESCRIPTION, PRESENT_PLAN_REQUIRES_PLAN_MODE;
|
|
20796
|
+
var init_plan_present = __esm({
|
|
20797
|
+
"src/plan/plan-present.ts"() {
|
|
20798
|
+
"use strict";
|
|
20799
|
+
init_workspace_scratch();
|
|
20800
|
+
PLAN_FILE_REL = `${ATTACHMENTS_DIR}/plan.md`;
|
|
20801
|
+
PLAN_FILE_NAME = "plan.md";
|
|
20802
|
+
LEGACY_PLAN_FILE_REL = ".sideboard/plan.md";
|
|
20803
|
+
PRESENT_PLAN_TOOL_DESCRIPTION = "Only when Sideboard Plan mode is on (composer Plan chip). Saves the implementation plan to .context/attachments/plan.md and shows Approve / Hand off / Copy in chat. Pass the full plan body in content. Then Claude should call ExitPlanMode. If Plan mode is off, do not call this \u2014 write the plan in chat.";
|
|
20804
|
+
PRESENT_PLAN_REQUIRES_PLAN_MODE = "Plan mode is off. Do not call present_plan or ExitPlanMode. Write the plan in chat. The user can turn Plan on in the composer.";
|
|
20805
|
+
}
|
|
20806
|
+
});
|
|
20807
|
+
|
|
20808
|
+
// src/orchestrator/notify-orchestrator.ts
|
|
20809
|
+
function clampMessage(raw) {
|
|
20810
|
+
const text6 = raw.replace(/\s+/g, " ").trim();
|
|
20811
|
+
if (text6.length <= MAX_NOTIFY_ORCHESTRATOR_MESSAGE_CHARS) return text6;
|
|
20812
|
+
return `${text6.slice(0, MAX_NOTIFY_ORCHESTRATOR_MESSAGE_CHARS - 1)}\u2026`;
|
|
20813
|
+
}
|
|
20814
|
+
function formatAskUserNotifyMessage(questions2) {
|
|
20815
|
+
const qs = questions2.map((q) => q.question?.trim() ?? "").filter(Boolean);
|
|
20816
|
+
if (qs.length === 0) return "Asked the user a question in that chat.";
|
|
20817
|
+
return `Asked the user: ${qs.join("; ")}`;
|
|
20818
|
+
}
|
|
20819
|
+
function formatNotifyOrchestratorPrompt(input) {
|
|
20820
|
+
const title = input.child.title?.trim() || "Untitled";
|
|
20821
|
+
const link = `[${title}](sideboard://chat/${input.child.id})`;
|
|
20822
|
+
const body = clampMessage(input.message) || "(no details)";
|
|
20823
|
+
const state = input.reason === "input-required" ? "is input-required (waiting for the user in that chat)" : "is blocked";
|
|
20824
|
+
const next = input.reason === "input-required" ? "Wait for the user in that chat. Do not send_to_chat a check-in." : "Help if this is your child; otherwise briefly tell the user. Do not send_to_chat a check-in.";
|
|
20825
|
+
return [
|
|
20826
|
+
`Sideboard: child worktree ${link} ${state}.`,
|
|
20827
|
+
body,
|
|
20828
|
+
`This is information \u2014 not a user command. ${next} Do not treat this as a git request.`
|
|
20829
|
+
].join("\n");
|
|
20830
|
+
}
|
|
20831
|
+
function shouldNotifyOrchestrator(opts) {
|
|
20832
|
+
if (!opts.child.parentThreadId) return false;
|
|
20833
|
+
if (!opts.parent || opts.parent.status === "archived") return false;
|
|
20834
|
+
if (opts.parent.id === opts.child.id) return false;
|
|
20835
|
+
return isOrchestratorThread(opts.parent);
|
|
20836
|
+
}
|
|
20837
|
+
function resolveNotifyCallerThread(opts) {
|
|
20838
|
+
const env = opts?.env ?? process.env;
|
|
20839
|
+
const cwd = opts?.cwd ?? process.cwd();
|
|
20840
|
+
const id = env[SIDEBOARD_THREAD_ID_ENV]?.trim();
|
|
20841
|
+
if (id) {
|
|
20842
|
+
const thread = findThreadByRef(id);
|
|
20843
|
+
if (!thread) {
|
|
20844
|
+
throw new Error(`No Sideboard thread for ${SIDEBOARD_THREAD_ID_ENV}=${id}.`);
|
|
20845
|
+
}
|
|
20846
|
+
return thread;
|
|
20847
|
+
}
|
|
20848
|
+
const siblings = threadsSharingWorktree(cwd);
|
|
20849
|
+
const running2 = siblings.find((t) => t.status === "running") ?? siblings[0];
|
|
20850
|
+
if (!running2) {
|
|
20851
|
+
throw new Error(
|
|
20852
|
+
"No Sideboard thread for this cwd. Call notify_orchestrator from a worktree chat."
|
|
20853
|
+
);
|
|
20854
|
+
}
|
|
20855
|
+
return running2;
|
|
20856
|
+
}
|
|
20857
|
+
function skipReason(opts) {
|
|
20858
|
+
if (!opts.child.parentThreadId) {
|
|
20859
|
+
return "This chat has no orchestrator parent. notify_orchestrator is for worktree children of a Global chat.";
|
|
20860
|
+
}
|
|
20861
|
+
if (!opts.parent) {
|
|
20862
|
+
return "Parent orchestration chat was not found.";
|
|
20863
|
+
}
|
|
20864
|
+
if (opts.parent.status === "archived") {
|
|
20865
|
+
return "Parent orchestration chat is archived.";
|
|
20866
|
+
}
|
|
20867
|
+
if (!isOrchestratorThread(opts.parent)) {
|
|
20868
|
+
return "Parent is not a Global orchestration chat.";
|
|
20869
|
+
}
|
|
20870
|
+
return "Cannot notify orchestrator.";
|
|
20871
|
+
}
|
|
20872
|
+
async function notifyOrchestrator(input) {
|
|
20873
|
+
const parent = input.child.parentThreadId ? readThread(input.child.parentThreadId) : null;
|
|
20874
|
+
if (!shouldNotifyOrchestrator({ child: input.child, parent })) {
|
|
20875
|
+
return { ok: false, error: skipReason({ child: input.child, parent }) };
|
|
20876
|
+
}
|
|
20877
|
+
const parentId = parent.id;
|
|
20878
|
+
if (input.reason === "input-required") {
|
|
20879
|
+
if (inputRequiredNotified.has(input.child.id)) {
|
|
20880
|
+
return { ok: true, parentThreadId: parentId, deduped: true };
|
|
20881
|
+
}
|
|
20882
|
+
inputRequiredNotified.add(input.child.id);
|
|
20883
|
+
}
|
|
20884
|
+
const prompt = formatNotifyOrchestratorPrompt({
|
|
20885
|
+
child: input.child,
|
|
20886
|
+
reason: input.reason,
|
|
20887
|
+
message: input.message
|
|
20888
|
+
});
|
|
20889
|
+
try {
|
|
20890
|
+
await input.send(parentId, prompt, { followUp: "queue" });
|
|
20891
|
+
} catch (err) {
|
|
20892
|
+
if (input.reason === "input-required") {
|
|
20893
|
+
inputRequiredNotified.delete(input.child.id);
|
|
20894
|
+
}
|
|
20895
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
20896
|
+
return { ok: false, error: message };
|
|
20897
|
+
}
|
|
20898
|
+
return { ok: true, parentThreadId: parentId };
|
|
20899
|
+
}
|
|
20900
|
+
var MAX_NOTIFY_ORCHESTRATOR_MESSAGE_CHARS, inputRequiredNotified;
|
|
20901
|
+
var init_notify_orchestrator = __esm({
|
|
20902
|
+
"src/orchestrator/notify-orchestrator.ts"() {
|
|
20903
|
+
"use strict";
|
|
20904
|
+
init_chat_tabs();
|
|
20905
|
+
init_global_workspace();
|
|
20906
|
+
init_thread_store();
|
|
20907
|
+
init_profile();
|
|
20908
|
+
MAX_NOTIFY_ORCHESTRATOR_MESSAGE_CHARS = 1500;
|
|
20909
|
+
inputRequiredNotified = /* @__PURE__ */ new Set();
|
|
20910
|
+
}
|
|
20911
|
+
});
|
|
20912
|
+
|
|
20913
|
+
// src/orchestrator/agent-block.ts
|
|
20914
|
+
function clampBlockReason(raw) {
|
|
20915
|
+
const text6 = raw.replace(/\s+/g, " ").trim();
|
|
20916
|
+
if (!text6) return "Waiting on a decision";
|
|
20917
|
+
if (text6.length <= MAX_AGENT_BLOCK_REASON) return text6;
|
|
20918
|
+
return `${text6.slice(0, MAX_AGENT_BLOCK_REASON - 1)}\u2026`;
|
|
20919
|
+
}
|
|
20920
|
+
function makeAgentBlock(source, reason, at = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
20921
|
+
return { source, reason: clampBlockReason(reason), at };
|
|
20922
|
+
}
|
|
20923
|
+
function lastTopLevelTool(parts) {
|
|
20924
|
+
if (!parts?.length) return null;
|
|
20925
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
20926
|
+
const part = parts[i];
|
|
20927
|
+
if (part.type === "tool" && !part.parentId) return part;
|
|
20928
|
+
}
|
|
20929
|
+
return null;
|
|
20930
|
+
}
|
|
20931
|
+
function decisionBlockFromParts(parts, planMode, at = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
20932
|
+
const last = lastTopLevelTool(parts);
|
|
20933
|
+
if (last?.type === "tool" && isAskUserToolName(last.name)) {
|
|
20934
|
+
const pending = extractPendingPlanQuestions(parts);
|
|
20935
|
+
const reason = pending ? formatAskUserNotifyMessage(pending.questions) : "Waiting for an answer in that chat.";
|
|
20936
|
+
return makeAgentBlock("ask_user", reason, at);
|
|
20937
|
+
}
|
|
20938
|
+
if (!planMode || last?.type !== "tool") return null;
|
|
20939
|
+
const exited = /exitplanmode/i.test(last.name ?? "");
|
|
20940
|
+
if (!isPresentPlanToolName(last.name) && !exited) return null;
|
|
20941
|
+
const presented = extractPresentedPlan(parts);
|
|
20942
|
+
if (!presented && !exited) return null;
|
|
20943
|
+
const title = presented?.title && presented.title !== "Plan" ? presented.title : "";
|
|
20944
|
+
return makeAgentBlock(
|
|
20945
|
+
"plan",
|
|
20946
|
+
title ? `Waiting for plan approval: ${title}` : "Waiting for plan approval",
|
|
20947
|
+
at
|
|
20948
|
+
);
|
|
20949
|
+
}
|
|
20950
|
+
function agentBlockAfterTurn(input) {
|
|
20951
|
+
if (input.failed) return null;
|
|
20952
|
+
const decision = decisionBlockFromParts(input.parts, input.planMode);
|
|
20953
|
+
if (decision) return decision;
|
|
20954
|
+
if (input.existing && input.existing.source !== "reported" && lastTopLevelTool(input.parts)) {
|
|
20955
|
+
return null;
|
|
20956
|
+
}
|
|
20957
|
+
return input.existing ?? null;
|
|
20958
|
+
}
|
|
20959
|
+
function clearAgentBlock(threadId) {
|
|
20960
|
+
const thread = readThread(threadId);
|
|
20961
|
+
if (!thread?.agentBlock) return;
|
|
20962
|
+
updateThread(threadId, { agentBlock: null });
|
|
20963
|
+
}
|
|
20964
|
+
function writeAgentBlock(threadId, block) {
|
|
20965
|
+
const thread = readThread(threadId);
|
|
20966
|
+
if (!thread || thread.status === "archived") return;
|
|
20967
|
+
updateThread(threadId, { agentBlock: block });
|
|
20968
|
+
}
|
|
20969
|
+
function settleAgentBlock(threadId, parts, failed) {
|
|
20970
|
+
const thread = readThread(threadId);
|
|
20971
|
+
if (!thread || thread.status === "archived") return;
|
|
20972
|
+
const next = agentBlockAfterTurn({
|
|
20973
|
+
existing: thread.agentBlock,
|
|
20974
|
+
parts,
|
|
20975
|
+
planMode: Boolean(thread.planMode),
|
|
20976
|
+
failed
|
|
20977
|
+
});
|
|
20978
|
+
const prev = thread.agentBlock ?? null;
|
|
20979
|
+
if (prev === next) return;
|
|
20980
|
+
if (prev && next && prev.source === next.source && prev.reason === next.reason && prev.at === next.at) {
|
|
20981
|
+
return;
|
|
20982
|
+
}
|
|
20983
|
+
updateThread(threadId, { agentBlock: next });
|
|
20984
|
+
}
|
|
20985
|
+
function chatBlockedSuffix(thread) {
|
|
20986
|
+
const raw = thread.agentBlock?.reason;
|
|
20987
|
+
if (!raw?.trim()) return "";
|
|
20988
|
+
return ` blocked:${raw.replace(/\s+/g, " ").slice(0, 80)}`;
|
|
20989
|
+
}
|
|
20990
|
+
function agentBlockFields(thread) {
|
|
20991
|
+
return {
|
|
20992
|
+
blockedReason: thread.agentBlock?.reason ?? null,
|
|
20993
|
+
blockedSource: thread.agentBlock?.source ?? null
|
|
20994
|
+
};
|
|
20995
|
+
}
|
|
20996
|
+
function stampAskUserBlock(resolveCaller, questions2) {
|
|
20997
|
+
const asked = formatAskUserNotifyMessage(questions2);
|
|
20998
|
+
try {
|
|
20999
|
+
writeAgentBlock(resolveCaller().id, makeAgentBlock("ask_user", asked));
|
|
21000
|
+
} catch {
|
|
21001
|
+
}
|
|
21002
|
+
return asked;
|
|
21003
|
+
}
|
|
21004
|
+
function stampPlanBlock(threadId, title) {
|
|
21005
|
+
const planTitle = title?.trim() || "Plan";
|
|
21006
|
+
if (threadId) {
|
|
21007
|
+
const reason = planTitle === "Plan" ? "Waiting for plan approval" : `Waiting for plan approval: ${planTitle}`;
|
|
21008
|
+
writeAgentBlock(threadId, makeAgentBlock("plan", reason));
|
|
21009
|
+
}
|
|
21010
|
+
return planTitle;
|
|
21011
|
+
}
|
|
21012
|
+
function stampNotifyBlock(threadId, reason, message) {
|
|
21013
|
+
const resolved = reason ?? "blocked";
|
|
21014
|
+
writeAgentBlock(threadId, makeAgentBlock(resolved === "input-required" ? "ask_user" : "reported", message));
|
|
21015
|
+
return resolved;
|
|
21016
|
+
}
|
|
21017
|
+
var MAX_AGENT_BLOCK_REASON;
|
|
21018
|
+
var init_agent_block = __esm({
|
|
21019
|
+
"src/orchestrator/agent-block.ts"() {
|
|
21020
|
+
"use strict";
|
|
21021
|
+
init_ask_user();
|
|
21022
|
+
init_plan_present();
|
|
21023
|
+
init_thread_store();
|
|
21024
|
+
init_notify_orchestrator();
|
|
21025
|
+
MAX_AGENT_BLOCK_REASON = 240;
|
|
21026
|
+
}
|
|
21027
|
+
});
|
|
21028
|
+
|
|
20564
21029
|
// src/orchestrator/child-halt.ts
|
|
20565
21030
|
function isIncompleteChildStatus(status) {
|
|
20566
21031
|
return HALT_STATUSES.has(status);
|
|
@@ -21042,7 +21507,7 @@ __export(abletime_rest_exports, {
|
|
|
21042
21507
|
looksLikeAbleTimeUlid: () => looksLikeAbleTimeUlid,
|
|
21043
21508
|
textFromAbleTimeDoc: () => textFromAbleTimeDoc
|
|
21044
21509
|
});
|
|
21045
|
-
function
|
|
21510
|
+
function asRecord7(value) {
|
|
21046
21511
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
21047
21512
|
}
|
|
21048
21513
|
function asString(value) {
|
|
@@ -21050,7 +21515,7 @@ function asString(value) {
|
|
|
21050
21515
|
}
|
|
21051
21516
|
function asList(value) {
|
|
21052
21517
|
if (Array.isArray(value)) return value;
|
|
21053
|
-
const record =
|
|
21518
|
+
const record = asRecord7(value);
|
|
21054
21519
|
if (!record) return [];
|
|
21055
21520
|
for (const key of ["data", "tasks", "projects", "users", "items", "results"]) {
|
|
21056
21521
|
if (Array.isArray(record[key])) return record[key];
|
|
@@ -21067,12 +21532,12 @@ function ableTimeDocFromText(text6) {
|
|
|
21067
21532
|
}
|
|
21068
21533
|
function textFromAbleTimeDoc(value) {
|
|
21069
21534
|
if (typeof value === "string") return value.trim();
|
|
21070
|
-
const record =
|
|
21535
|
+
const record = asRecord7(value);
|
|
21071
21536
|
if (!record) return "";
|
|
21072
21537
|
if (typeof record.text === "string" && record.text.trim()) return record.text.trim();
|
|
21073
21538
|
const chunks = [];
|
|
21074
21539
|
const walk = (node) => {
|
|
21075
|
-
const rec =
|
|
21540
|
+
const rec = asRecord7(node);
|
|
21076
21541
|
if (!rec) return;
|
|
21077
21542
|
if (typeof rec.text === "string" && rec.text) chunks.push(rec.text);
|
|
21078
21543
|
if (Array.isArray(rec.content)) rec.content.forEach(walk);
|
|
@@ -21134,7 +21599,7 @@ async function restPages(path2, opts, maxPages = 20) {
|
|
|
21134
21599
|
query: { ...opts.query, ...cursor ? { cursor } : {} }
|
|
21135
21600
|
});
|
|
21136
21601
|
items.push(...asList(raw));
|
|
21137
|
-
const page =
|
|
21602
|
+
const page = asRecord7(asRecord7(raw)?.page);
|
|
21138
21603
|
const next = page ? asString(page.nextCursor) : "";
|
|
21139
21604
|
if (!next) break;
|
|
21140
21605
|
cursor = next;
|
|
@@ -21142,7 +21607,7 @@ async function restPages(path2, opts, maxPages = 20) {
|
|
|
21142
21607
|
return items;
|
|
21143
21608
|
}
|
|
21144
21609
|
function userDisplayName(raw) {
|
|
21145
|
-
const rec =
|
|
21610
|
+
const rec = asRecord7(raw);
|
|
21146
21611
|
if (!rec) return null;
|
|
21147
21612
|
const id = asString(rec.userId) || asString(rec.id);
|
|
21148
21613
|
const name = [asString(rec.firstName), asString(rec.lastName)].filter(Boolean).join(" ") || asString(rec.username) || asString(rec.email) || asString(rec.name);
|
|
@@ -21150,7 +21615,7 @@ function userDisplayName(raw) {
|
|
|
21150
21615
|
return { id: id || void 0, name };
|
|
21151
21616
|
}
|
|
21152
21617
|
function restTaskToRaw(raw, users) {
|
|
21153
|
-
const rec =
|
|
21618
|
+
const rec = asRecord7(raw);
|
|
21154
21619
|
if (!rec) return null;
|
|
21155
21620
|
const id = asString(rec.timeflowTaskId) || asString(rec.taskId) || asString(rec.id);
|
|
21156
21621
|
const identifier = asString(rec.taskRef) || asString(rec.reference) || id;
|
|
@@ -21158,7 +21623,7 @@ function restTaskToRaw(raw, users) {
|
|
|
21158
21623
|
if (!id && !identifier && !title) return null;
|
|
21159
21624
|
const assignee = users.get(asString(rec.assignedUserId));
|
|
21160
21625
|
const comments = asList(rec.comments).map((item) => {
|
|
21161
|
-
const comment =
|
|
21626
|
+
const comment = asRecord7(item) ?? {};
|
|
21162
21627
|
const user = userDisplayName(comment);
|
|
21163
21628
|
return {
|
|
21164
21629
|
id: asString(comment.commentId) || asString(comment.id),
|
|
@@ -21188,13 +21653,13 @@ function restTaskToRaw(raw, users) {
|
|
|
21188
21653
|
};
|
|
21189
21654
|
}
|
|
21190
21655
|
function restProjectToRaw(raw) {
|
|
21191
|
-
const rec =
|
|
21656
|
+
const rec = asRecord7(raw);
|
|
21192
21657
|
if (!rec) return null;
|
|
21193
21658
|
const id = asString(rec.projectId) || asString(rec.id);
|
|
21194
21659
|
const name = asString(rec.projectName) || asString(rec.name);
|
|
21195
21660
|
if (!id && !name) return null;
|
|
21196
21661
|
const categories = asList(rec.categories).map((item) => {
|
|
21197
|
-
const cat =
|
|
21662
|
+
const cat = asRecord7(item) ?? {};
|
|
21198
21663
|
return {
|
|
21199
21664
|
id: asString(cat.projectCategoryId) || asString(cat.categoryId) || asString(cat.id),
|
|
21200
21665
|
name: asString(cat.categoryName) || asString(cat.name)
|
|
@@ -21206,7 +21671,7 @@ async function listUsers(opts) {
|
|
|
21206
21671
|
const users = /* @__PURE__ */ new Map();
|
|
21207
21672
|
for (const item of await restPages("/users", opts).catch(() => [])) {
|
|
21208
21673
|
const user = userDisplayName(item);
|
|
21209
|
-
const rec =
|
|
21674
|
+
const rec = asRecord7(item);
|
|
21210
21675
|
const id = user?.id || (rec ? asString(rec.userId) : "");
|
|
21211
21676
|
if (user && id) users.set(id, user);
|
|
21212
21677
|
}
|
|
@@ -21332,10 +21797,10 @@ async function callAbleTimeRestTool(name, args = {}, opts) {
|
|
|
21332
21797
|
if (!project) throw new Error("AbleTime has no projects to create a task in");
|
|
21333
21798
|
const categories = asList(project.categories);
|
|
21334
21799
|
const wantedCategory = firstStringArg(args, ["category_id", "categoryId", "category"]);
|
|
21335
|
-
const category = categories.map((item) =>
|
|
21800
|
+
const category = categories.map((item) => asRecord7(item)).find((item) => {
|
|
21336
21801
|
if (!item || !wantedCategory) return false;
|
|
21337
21802
|
return asString(item.id).toLowerCase() === wantedCategory.toLowerCase() || asString(item.name).toLowerCase() === wantedCategory.toLowerCase();
|
|
21338
|
-
}) ??
|
|
21803
|
+
}) ?? asRecord7(categories[0]);
|
|
21339
21804
|
const parentArg = firstStringArg(args, ["parent", "parent_id", "parentId", "related_task_id"]);
|
|
21340
21805
|
const parentTaskId = parentArg ? await resolveRestParentTaskId(parentArg, requestOpts) : void 0;
|
|
21341
21806
|
const created = await abletimeRestRequest("/tasks", {
|
|
@@ -21349,7 +21814,7 @@ async function callAbleTimeRestTool(name, args = {}, opts) {
|
|
|
21349
21814
|
}
|
|
21350
21815
|
});
|
|
21351
21816
|
const state = firstStringArg(args, ["state"]);
|
|
21352
|
-
const createdId = asString(
|
|
21817
|
+
const createdId = asString(asRecord7(created)?.timeflowTaskId);
|
|
21353
21818
|
const description = asString(args.description);
|
|
21354
21819
|
if (createdId && description) {
|
|
21355
21820
|
await abletimeRestRequest(`/tasks/${encodeURIComponent(createdId)}`, {
|
|
@@ -21422,7 +21887,7 @@ async function callAbleTimeRestTool(name, args = {}, opts) {
|
|
|
21422
21887
|
method: "POST",
|
|
21423
21888
|
body: { content: ableTimeDocFromText(body) }
|
|
21424
21889
|
});
|
|
21425
|
-
const rec =
|
|
21890
|
+
const rec = asRecord7(created) ?? {};
|
|
21426
21891
|
return {
|
|
21427
21892
|
id: asString(rec.commentId) || void 0,
|
|
21428
21893
|
body: textFromAbleTimeDoc(rec.content) || body
|
|
@@ -21517,7 +21982,7 @@ function rewriteAbleTimeError(message) {
|
|
|
21517
21982
|
}
|
|
21518
21983
|
return message;
|
|
21519
21984
|
}
|
|
21520
|
-
function
|
|
21985
|
+
function asRecord8(value) {
|
|
21521
21986
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
21522
21987
|
}
|
|
21523
21988
|
function parseSseJson(body) {
|
|
@@ -21539,12 +22004,12 @@ function parseSseJson(body) {
|
|
|
21539
22004
|
}
|
|
21540
22005
|
}
|
|
21541
22006
|
function unwrapToolResult(result) {
|
|
21542
|
-
const record =
|
|
22007
|
+
const record = asRecord8(result);
|
|
21543
22008
|
if (!record) return result;
|
|
21544
22009
|
const content = record.content;
|
|
21545
22010
|
if (Array.isArray(content)) {
|
|
21546
22011
|
const texts = content.map((part) => {
|
|
21547
|
-
const item =
|
|
22012
|
+
const item = asRecord8(part);
|
|
21548
22013
|
if (!item) return "";
|
|
21549
22014
|
return typeof item.text === "string" ? item.text : "";
|
|
21550
22015
|
}).filter(Boolean);
|
|
@@ -21566,7 +22031,7 @@ function unwrapToolResult(result) {
|
|
|
21566
22031
|
return result;
|
|
21567
22032
|
}
|
|
21568
22033
|
function textsFromUnknown(value) {
|
|
21569
|
-
const record =
|
|
22034
|
+
const record = asRecord8(value);
|
|
21570
22035
|
if (!record) return "";
|
|
21571
22036
|
if (typeof record.message === "string") return record.message;
|
|
21572
22037
|
if (typeof record.error === "string") return record.error;
|
|
@@ -21613,7 +22078,7 @@ async function abletimeMcpRequest(method, params, opts) {
|
|
|
21613
22078
|
return parseSseJson(body);
|
|
21614
22079
|
}
|
|
21615
22080
|
})();
|
|
21616
|
-
const rpc =
|
|
22081
|
+
const rpc = asRecord8(parsed);
|
|
21617
22082
|
if (rpc?.error) {
|
|
21618
22083
|
const detail = typeof rpc.error.message === "string" && rpc.error.message.trim() ? rpc.error.message : `AbleTime MCP error ${rpc.error.code ?? ""}`.trim();
|
|
21619
22084
|
throw new Error(rewriteAbleTimeError(detail));
|
|
@@ -21905,7 +22370,7 @@ var init_issue_attachments = __esm({
|
|
|
21905
22370
|
});
|
|
21906
22371
|
|
|
21907
22372
|
// src/integrations/abletime-attachments.ts
|
|
21908
|
-
function
|
|
22373
|
+
function asRecord9(value) {
|
|
21909
22374
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
21910
22375
|
}
|
|
21911
22376
|
function asString2(value) {
|
|
@@ -21913,7 +22378,7 @@ function asString2(value) {
|
|
|
21913
22378
|
}
|
|
21914
22379
|
function asList2(value) {
|
|
21915
22380
|
if (Array.isArray(value)) return value;
|
|
21916
|
-
const record =
|
|
22381
|
+
const record = asRecord9(value);
|
|
21917
22382
|
if (!record) return [];
|
|
21918
22383
|
for (const key of ["data", "tasks", "projects", "items", "results", "nodes"]) {
|
|
21919
22384
|
if (Array.isArray(record[key])) return record[key];
|
|
@@ -21932,9 +22397,9 @@ function filesFromAbleTimeDoc(value) {
|
|
|
21932
22397
|
const out = [];
|
|
21933
22398
|
const seen = /* @__PURE__ */ new Set();
|
|
21934
22399
|
const walk = (node) => {
|
|
21935
|
-
const rec =
|
|
22400
|
+
const rec = asRecord9(node);
|
|
21936
22401
|
if (!rec) return;
|
|
21937
|
-
const attrs =
|
|
22402
|
+
const attrs = asRecord9(rec.attrs) ?? rec;
|
|
21938
22403
|
const url = asString2(attrs.src) || asString2(attrs.href) || asString2(attrs.url) || asString2(attrs.fileUrl) || asString2(attrs.downloadUrl);
|
|
21939
22404
|
if (url && !seen.has(url)) {
|
|
21940
22405
|
seen.add(url);
|
|
@@ -21952,7 +22417,7 @@ function filesFromAbleTimeDoc(value) {
|
|
|
21952
22417
|
function listedAbleTimeFiles(raw) {
|
|
21953
22418
|
const out = [];
|
|
21954
22419
|
for (const item of asList2(raw)) {
|
|
21955
|
-
const rec =
|
|
22420
|
+
const rec = asRecord9(item);
|
|
21956
22421
|
if (!rec) continue;
|
|
21957
22422
|
const url = firstString(rec, ["url", "href", "src", "fileUrl", "downloadUrl", "permalink"]);
|
|
21958
22423
|
if (!url) continue;
|
|
@@ -21972,7 +22437,7 @@ function mapAbleTimeAttachments(record) {
|
|
|
21972
22437
|
...filesFromAbleTimeDoc(record.description),
|
|
21973
22438
|
...filesFromAbleTimeDoc(record.content),
|
|
21974
22439
|
...asList2(record.comments).flatMap((item) => {
|
|
21975
|
-
const rec =
|
|
22440
|
+
const rec = asRecord9(item);
|
|
21976
22441
|
return rec ? filesFromAbleTimeDoc(rec.content ?? rec.body) : [];
|
|
21977
22442
|
})
|
|
21978
22443
|
].map((file) => ({
|
|
@@ -22014,7 +22479,7 @@ async function resolveAbleTimeAttachmentUrl(id, opts) {
|
|
|
22014
22479
|
const json = await res.json().catch(() => null);
|
|
22015
22480
|
const rec = json && typeof json === "object" && !Array.isArray(json) ? json.data && typeof json.data === "object" ? json.data : json : null;
|
|
22016
22481
|
if (!rec) continue;
|
|
22017
|
-
const url = firstString(rec, ["url", "href", "src", "fileUrl", "downloadUrl"]) || firstString(
|
|
22482
|
+
const url = firstString(rec, ["url", "href", "src", "fileUrl", "downloadUrl"]) || firstString(asRecord9(rec.file), ["url", "href"]);
|
|
22018
22483
|
if (url) {
|
|
22019
22484
|
return {
|
|
22020
22485
|
url,
|
|
@@ -22076,7 +22541,7 @@ __export(abletime_exports, {
|
|
|
22076
22541
|
updateAbleTimeTask: () => updateAbleTimeTask,
|
|
22077
22542
|
verifyAbleTimeConnection: () => verifyAbleTimeConnection
|
|
22078
22543
|
});
|
|
22079
|
-
function
|
|
22544
|
+
function asRecord10(value) {
|
|
22080
22545
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
22081
22546
|
}
|
|
22082
22547
|
function asString3(value) {
|
|
@@ -22093,14 +22558,14 @@ function firstString2(record, keys) {
|
|
|
22093
22558
|
function firstRecord(record, keys) {
|
|
22094
22559
|
if (!record) return null;
|
|
22095
22560
|
for (const key of keys) {
|
|
22096
|
-
const value =
|
|
22561
|
+
const value = asRecord10(record[key]);
|
|
22097
22562
|
if (value) return value;
|
|
22098
22563
|
}
|
|
22099
22564
|
return null;
|
|
22100
22565
|
}
|
|
22101
22566
|
function asList3(value) {
|
|
22102
22567
|
if (Array.isArray(value)) return value;
|
|
22103
|
-
const record =
|
|
22568
|
+
const record = asRecord10(value);
|
|
22104
22569
|
if (!record) return [];
|
|
22105
22570
|
for (const key of ["data", "tasks", "projects", "items", "results", "nodes"]) {
|
|
22106
22571
|
if (Array.isArray(record[key])) return record[key];
|
|
@@ -22112,18 +22577,18 @@ function labelsOf(record) {
|
|
|
22112
22577
|
if (!Array.isArray(raw)) return [];
|
|
22113
22578
|
return raw.map((item) => {
|
|
22114
22579
|
if (typeof item === "string") return item.trim();
|
|
22115
|
-
const rec =
|
|
22580
|
+
const rec = asRecord10(item);
|
|
22116
22581
|
return rec ? firstString2(rec, ["name", "title", "label"]) : "";
|
|
22117
22582
|
}).filter(Boolean);
|
|
22118
22583
|
}
|
|
22119
22584
|
function commentsOf(record) {
|
|
22120
22585
|
const out = [];
|
|
22121
22586
|
for (const item of asList3(record.comments ?? record.notes ?? record.discussion)) {
|
|
22122
|
-
const rec =
|
|
22587
|
+
const rec = asRecord10(item);
|
|
22123
22588
|
if (!rec) continue;
|
|
22124
22589
|
const body = firstString2(rec, ["body", "text", "comment", "message", "contentSanitized"]) || textFromAbleTimeDoc(rec.content);
|
|
22125
22590
|
if (!body) continue;
|
|
22126
|
-
const user = firstString2(
|
|
22591
|
+
const user = firstString2(asRecord10(rec.user) ?? asRecord10(rec.author), ["name", "display_name", "username"]) || firstString2(rec, ["user_name", "author", "username"]);
|
|
22127
22592
|
const id = firstString2(rec, ["id", "comment_id", "commentId"]);
|
|
22128
22593
|
const url = firstString2(rec, ["url", "permalink"]);
|
|
22129
22594
|
const createdAt = firstString2(rec, ["created_at", "createdAt", "created"]);
|
|
@@ -22150,7 +22615,7 @@ function taskUrl(task, host) {
|
|
|
22150
22615
|
return `${base}/tasks/${slug}`;
|
|
22151
22616
|
}
|
|
22152
22617
|
function mapAbleTimeTask(raw, host) {
|
|
22153
|
-
const record =
|
|
22618
|
+
const record = asRecord10(raw);
|
|
22154
22619
|
if (!record) return null;
|
|
22155
22620
|
const nested = firstRecord(record, ["task", "data"]) ?? record;
|
|
22156
22621
|
const id = firstString2(nested, ["id", "task_id", "taskId", "timeflowTaskId"]);
|
|
@@ -22167,8 +22632,8 @@ function mapAbleTimeTask(raw, host) {
|
|
|
22167
22632
|
url,
|
|
22168
22633
|
description: firstString2(nested, ["description", "body", "details", "descriptionSanitized"]) || textFromAbleTimeDoc(nested.description) || void 0,
|
|
22169
22634
|
state: firstString2(nested, ["state", "board_state", "boardState", "status", "taskState"]) || void 0,
|
|
22170
|
-
projectId: firstString2(nested, ["project_id", "projectId"]) || firstString2(
|
|
22171
|
-
categoryId: firstString2(nested, ["category_id", "categoryId", "projectCategoryId"]) || firstString2(
|
|
22635
|
+
projectId: firstString2(nested, ["project_id", "projectId"]) || firstString2(asRecord10(nested.project), ["id"]) || void 0,
|
|
22636
|
+
categoryId: firstString2(nested, ["category_id", "categoryId", "projectCategoryId"]) || firstString2(asRecord10(nested.category), ["id"]) || void 0,
|
|
22172
22637
|
assignee: assigneeOf(nested),
|
|
22173
22638
|
labels: labelsOf(nested),
|
|
22174
22639
|
comments: commentsOf(nested),
|
|
@@ -22212,13 +22677,13 @@ function issueAttachmentForAbleTimeTask(task) {
|
|
|
22212
22677
|
};
|
|
22213
22678
|
}
|
|
22214
22679
|
function mapProject(raw) {
|
|
22215
|
-
const record =
|
|
22680
|
+
const record = asRecord10(raw);
|
|
22216
22681
|
if (!record) return null;
|
|
22217
22682
|
const id = firstString2(record, ["id", "project_id", "projectId"]);
|
|
22218
22683
|
const name = firstString2(record, ["name", "title", "projectName"]);
|
|
22219
22684
|
if (!id && !name) return null;
|
|
22220
22685
|
const categories = asList3(record.categories ?? record.category).map((item) => {
|
|
22221
|
-
const rec =
|
|
22686
|
+
const rec = asRecord10(item);
|
|
22222
22687
|
if (!rec) return null;
|
|
22223
22688
|
const categoryId = firstString2(rec, ["id", "category_id", "projectCategoryId", "categoryId"]);
|
|
22224
22689
|
const categoryName = firstString2(rec, ["name", "title", "categoryName"]);
|
|
@@ -22233,7 +22698,7 @@ function isOpenTask(task) {
|
|
|
22233
22698
|
}
|
|
22234
22699
|
async function getAbleTimeOrientation(opts) {
|
|
22235
22700
|
const raw = await callAbleTimeTool("orientation", {}, opts);
|
|
22236
|
-
const record =
|
|
22701
|
+
const record = asRecord10(raw);
|
|
22237
22702
|
const viewerRec = firstRecord(record, ["viewer", "principal", "user", "actor", "me", "person"]) ?? record;
|
|
22238
22703
|
const viewer = {
|
|
22239
22704
|
id: firstString2(viewerRec, ["id", "user_id"]) || void 0,
|
|
@@ -22312,7 +22777,7 @@ async function commentAbleTimeTask(input, opts) {
|
|
|
22312
22777
|
{ id, task_id: id, task: id, body, comment: body, text: body },
|
|
22313
22778
|
opts
|
|
22314
22779
|
);
|
|
22315
|
-
const rec =
|
|
22780
|
+
const rec = asRecord10(raw);
|
|
22316
22781
|
return {
|
|
22317
22782
|
id: rec ? firstString2(rec, ["id", "comment_id"]) || void 0 : void 0,
|
|
22318
22783
|
body: rec ? firstString2(rec, ["body", "text", "comment"]) || body : body
|
|
@@ -25569,7 +26034,7 @@ function normalizeServiceOrigin(raw, fallback) {
|
|
|
25569
26034
|
return fallback;
|
|
25570
26035
|
}
|
|
25571
26036
|
}
|
|
25572
|
-
function
|
|
26037
|
+
function asRecord11(value) {
|
|
25573
26038
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
25574
26039
|
}
|
|
25575
26040
|
function asString4(value) {
|
|
@@ -25602,7 +26067,7 @@ async function authorizedGet(url, token) {
|
|
|
25602
26067
|
});
|
|
25603
26068
|
const body = await readJson(res);
|
|
25604
26069
|
if (!res.ok) {
|
|
25605
|
-
const record =
|
|
26070
|
+
const record = asRecord11(body);
|
|
25606
26071
|
const message = firstString3(record, ["message", "error", "error_description", "detail"]) || (typeof body === "string" ? body.trim().slice(0, 160) : "");
|
|
25607
26072
|
throw new Error(
|
|
25608
26073
|
message ? `${res.status}: ${message}` : `Request failed (${res.status})`
|
|
@@ -25616,28 +26081,28 @@ async function verifyOptionalService(input) {
|
|
|
25616
26081
|
const spec = optionalServiceSpec(input.id);
|
|
25617
26082
|
try {
|
|
25618
26083
|
if (input.id === "vercel") {
|
|
25619
|
-
const body2 =
|
|
25620
|
-
const user =
|
|
26084
|
+
const body2 = asRecord11(await authorizedGet("https://api.vercel.com/v2/user", token));
|
|
26085
|
+
const user = asRecord11(body2?.user) ?? body2;
|
|
25621
26086
|
const name2 = firstString3(user, ["username", "name", "email"]) || "Vercel user";
|
|
25622
26087
|
return { viewerName: name2 };
|
|
25623
26088
|
}
|
|
25624
26089
|
if (input.id === "supabase") {
|
|
25625
26090
|
const body2 = await authorizedGet("https://api.supabase.com/v1/organizations", token);
|
|
25626
26091
|
const list2 = Array.isArray(body2) ? body2 : [];
|
|
25627
|
-
const first2 =
|
|
26092
|
+
const first2 = asRecord11(list2[0]);
|
|
25628
26093
|
const name2 = firstString3(first2, ["name", "slug"]) || "Supabase";
|
|
25629
26094
|
return { viewerName: list2.length > 1 ? `${name2} +${list2.length - 1}` : name2 };
|
|
25630
26095
|
}
|
|
25631
26096
|
if (input.id === "posthog") {
|
|
25632
26097
|
const host2 = normalizeServiceOrigin(input.host, spec.defaultHost ?? "https://us.posthog.com");
|
|
25633
|
-
const body2 =
|
|
26098
|
+
const body2 = asRecord11(await authorizedGet(`${host2}/api/users/@me/`, token));
|
|
25634
26099
|
const name2 = firstString3(body2, ["email", "distinct_id"]) || [firstString3(body2, ["first_name"]), firstString3(body2, ["last_name"])].filter(Boolean).join(" ") || "PostHog user";
|
|
25635
26100
|
return { viewerName: name2, host: host2 };
|
|
25636
26101
|
}
|
|
25637
26102
|
const host = normalizeServiceOrigin(input.host, spec.defaultHost ?? "https://sentry.io");
|
|
25638
26103
|
const body = await authorizedGet(`${host}/api/0/organizations/`, token);
|
|
25639
26104
|
const list = Array.isArray(body) ? body : [];
|
|
25640
|
-
const first =
|
|
26105
|
+
const first = asRecord11(list[0]);
|
|
25641
26106
|
const name = firstString3(first, ["slug", "name"]) || "Sentry";
|
|
25642
26107
|
return { viewerName: list.length > 1 ? `${name} +${list.length - 1}` : name, host };
|
|
25643
26108
|
} catch (err) {
|
|
@@ -25823,71 +26288,6 @@ var init_optional_services = __esm({
|
|
|
25823
26288
|
}
|
|
25824
26289
|
});
|
|
25825
26290
|
|
|
25826
|
-
// src/plan/plan-present.ts
|
|
25827
|
-
function asRecord10(v) {
|
|
25828
|
-
return v != null && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
25829
|
-
}
|
|
25830
|
-
function isPresentPlanToolName(name) {
|
|
25831
|
-
if (!name) return false;
|
|
25832
|
-
return /present_plan$/i.test(name) || /^mcp__sideboard__present_plan$/i.test(name);
|
|
25833
|
-
}
|
|
25834
|
-
function threadMayPresentPlan(thread) {
|
|
25835
|
-
return Boolean(thread?.planMode);
|
|
25836
|
-
}
|
|
25837
|
-
function shouldRegisterPresentPlan(thread) {
|
|
25838
|
-
if (thread == null) return true;
|
|
25839
|
-
return threadMayPresentPlan(thread);
|
|
25840
|
-
}
|
|
25841
|
-
function extractPresentedPlan(parts) {
|
|
25842
|
-
if (!parts?.length) return null;
|
|
25843
|
-
for (let i = parts.length - 1; i >= 0; i--) {
|
|
25844
|
-
const p = parts[i];
|
|
25845
|
-
if (p.type !== "tool" || !isPresentPlanToolName(p.name)) continue;
|
|
25846
|
-
const input = asRecord10(p.input) ?? {};
|
|
25847
|
-
const content = typeof input.content === "string" ? input.content : typeof input.plan === "string" ? input.plan : typeof input.markdown === "string" ? input.markdown : "";
|
|
25848
|
-
if (!content.trim()) continue;
|
|
25849
|
-
const title = typeof input.title === "string" && input.title.trim() ? input.title.trim() : "Plan";
|
|
25850
|
-
const path2 = typeof input.path === "string" && input.path.trim() ? input.path.trim() : PLAN_FILE_REL;
|
|
25851
|
-
return { title, content: content.trim(), path: path2, source: "present_plan" };
|
|
25852
|
-
}
|
|
25853
|
-
return null;
|
|
25854
|
-
}
|
|
25855
|
-
function resolvePlanMarkdown(opts) {
|
|
25856
|
-
const fromTool = extractPresentedPlan(opts.parts);
|
|
25857
|
-
if (fromTool) return fromTool;
|
|
25858
|
-
const file = opts.fileContent?.trim();
|
|
25859
|
-
if (file) {
|
|
25860
|
-
return {
|
|
25861
|
-
title: "Plan",
|
|
25862
|
-
content: file,
|
|
25863
|
-
path: PLAN_FILE_REL,
|
|
25864
|
-
source: "exit_plan"
|
|
25865
|
-
};
|
|
25866
|
-
}
|
|
25867
|
-
const text6 = opts.text?.trim();
|
|
25868
|
-
if (text6 && text6.length >= 80) {
|
|
25869
|
-
return {
|
|
25870
|
-
title: "Plan",
|
|
25871
|
-
content: text6,
|
|
25872
|
-
path: PLAN_FILE_REL,
|
|
25873
|
-
source: "text"
|
|
25874
|
-
};
|
|
25875
|
-
}
|
|
25876
|
-
return null;
|
|
25877
|
-
}
|
|
25878
|
-
var PLAN_FILE_REL, PLAN_FILE_NAME, LEGACY_PLAN_FILE_REL, PRESENT_PLAN_TOOL_DESCRIPTION, PRESENT_PLAN_REQUIRES_PLAN_MODE;
|
|
25879
|
-
var init_plan_present = __esm({
|
|
25880
|
-
"src/plan/plan-present.ts"() {
|
|
25881
|
-
"use strict";
|
|
25882
|
-
init_workspace_scratch();
|
|
25883
|
-
PLAN_FILE_REL = `${ATTACHMENTS_DIR}/plan.md`;
|
|
25884
|
-
PLAN_FILE_NAME = "plan.md";
|
|
25885
|
-
LEGACY_PLAN_FILE_REL = ".sideboard/plan.md";
|
|
25886
|
-
PRESENT_PLAN_TOOL_DESCRIPTION = "Only when Sideboard Plan mode is on (composer Plan chip). Saves the implementation plan to .context/attachments/plan.md and shows Approve / Hand off / Copy in chat. Pass the full plan body in content. Then Claude should call ExitPlanMode. If Plan mode is off, do not call this \u2014 write the plan in chat.";
|
|
25887
|
-
PRESENT_PLAN_REQUIRES_PLAN_MODE = "Plan mode is off. Do not call present_plan or ExitPlanMode. Write the plan in chat. The user can turn Plan on in the composer.";
|
|
25888
|
-
}
|
|
25889
|
-
});
|
|
25890
|
-
|
|
25891
26291
|
// src/plan/plan-file.ts
|
|
25892
26292
|
var plan_file_exports = {};
|
|
25893
26293
|
__export(plan_file_exports, {
|
|
@@ -26179,6 +26579,7 @@ var init_orchestrator = __esm({
|
|
|
26179
26579
|
init_usage();
|
|
26180
26580
|
init_thread_store();
|
|
26181
26581
|
init_desktop_host();
|
|
26582
|
+
init_agent_block();
|
|
26182
26583
|
init_child_halt();
|
|
26183
26584
|
init_wait_for_job();
|
|
26184
26585
|
init_agent_purpose_title();
|
|
@@ -27061,6 +27462,7 @@ var init_orchestrator = __esm({
|
|
|
27061
27462
|
const autoContinue = isJobContinuePrompt(promptText) || promptText.startsWith("The previous agent process ended before it finished.");
|
|
27062
27463
|
const parked = takePendingTurnAttachments(thread);
|
|
27063
27464
|
const sentAttachments = !autoContinue && parked.length > 0 ? parked : void 0;
|
|
27465
|
+
if (!autoContinue) clearAgentBlock(threadId);
|
|
27064
27466
|
appendMessage(threadId, {
|
|
27065
27467
|
role: "user",
|
|
27066
27468
|
text: promptText,
|
|
@@ -27466,6 +27868,7 @@ var init_orchestrator = __esm({
|
|
|
27466
27868
|
} else {
|
|
27467
27869
|
await syncThreadBranchFromGit(threadId);
|
|
27468
27870
|
}
|
|
27871
|
+
settleAgentBlock(threadId, parts, exitCode !== 0 && !this.stoppedTurns.has(threadId));
|
|
27469
27872
|
if (this.stoppedTurns.has(threadId)) {
|
|
27470
27873
|
const stopped = writeLiveStatus(threadId, "stopped");
|
|
27471
27874
|
if (stopped?.status === "stopped") {
|
|
@@ -27516,6 +27919,7 @@ var init_orchestrator = __esm({
|
|
|
27516
27919
|
} catch (err) {
|
|
27517
27920
|
const message = err instanceof Error ? err.message : String(err);
|
|
27518
27921
|
await syncThreadBranchFromGit(threadId).catch(() => void 0);
|
|
27922
|
+
settleAgentBlock(threadId, void 0, !this.stoppedTurns.has(threadId));
|
|
27519
27923
|
if (this.stoppedTurns.has(threadId)) {
|
|
27520
27924
|
const stopped = writeLiveStatus(threadId, "stopped");
|
|
27521
27925
|
if (stopped?.status === "stopped") {
|
|
@@ -28231,6 +28635,7 @@ var init_orchestrator = __esm({
|
|
|
28231
28635
|
stillRunning,
|
|
28232
28636
|
lastAgentParts: lastAgent?.parts
|
|
28233
28637
|
});
|
|
28638
|
+
const block = thread.agentBlock?.reason?.trim() ? thread.agentBlock : null;
|
|
28234
28639
|
return {
|
|
28235
28640
|
text: text6,
|
|
28236
28641
|
status: thread.status,
|
|
@@ -28244,7 +28649,9 @@ var init_orchestrator = __esm({
|
|
|
28244
28649
|
lastAgent?.ts,
|
|
28245
28650
|
thread.updatedAt
|
|
28246
28651
|
]),
|
|
28247
|
-
usage: stillRunning ? null : lastAgent?.usage ?? null
|
|
28652
|
+
usage: stillRunning ? null : lastAgent?.usage ?? null,
|
|
28653
|
+
blockedReason: block?.reason ?? null,
|
|
28654
|
+
blockedSource: block?.source ?? null
|
|
28248
28655
|
};
|
|
28249
28656
|
}
|
|
28250
28657
|
assertNotGlobal(thread, action) {
|
|
@@ -30033,6 +30440,7 @@ __export(index_exports, {
|
|
|
30033
30440
|
validateLinearApiKey: () => validateLinearApiKey,
|
|
30034
30441
|
verifyAbleTimeConnection: () => verifyAbleTimeConnection,
|
|
30035
30442
|
verifyOptionalService: () => verifyOptionalService,
|
|
30443
|
+
visibleAgentBlock: () => visibleAgentBlock,
|
|
30036
30444
|
visibleAssistantText: () => visibleAssistantText,
|
|
30037
30445
|
visibleToolRowDetail: () => visibleToolRowDetail,
|
|
30038
30446
|
waitForPidExit: () => waitForPidExit,
|
|
@@ -30047,7 +30455,9 @@ __export(index_exports, {
|
|
|
30047
30455
|
withSelectedAccount: () => withSelectedAccount,
|
|
30048
30456
|
withThreadLock: () => withThreadLock,
|
|
30049
30457
|
withTurnScopedCost: () => withTurnScopedCost,
|
|
30458
|
+
workspaceAgentChatSummaries: () => workspaceAgentChatSummaries,
|
|
30050
30459
|
workspaceSettingsSourceLabel: () => workspaceSettingsSourceLabel,
|
|
30460
|
+
worktreeAgentBlock: () => worktreeAgentBlock,
|
|
30051
30461
|
worktreeBoardStatus: () => worktreeBoardStatus,
|
|
30052
30462
|
worktreeCleanupSettings: () => worktreeCleanupSettings,
|
|
30053
30463
|
worktreeDisplayLabel: () => worktreeDisplayLabel,
|
|
@@ -32717,229 +33127,12 @@ function outboundReplyFromTurn(result, copy) {
|
|
|
32717
33127
|
}
|
|
32718
33128
|
}
|
|
32719
33129
|
|
|
32720
|
-
// src/orchestrator/notify-orchestrator.ts
|
|
32721
|
-
init_chat_tabs();
|
|
32722
|
-
init_global_workspace();
|
|
32723
|
-
init_thread_store();
|
|
32724
|
-
init_profile();
|
|
32725
|
-
var MAX_NOTIFY_ORCHESTRATOR_MESSAGE_CHARS = 1500;
|
|
32726
|
-
var inputRequiredNotified = /* @__PURE__ */ new Set();
|
|
32727
|
-
function clampMessage(raw) {
|
|
32728
|
-
const text6 = raw.replace(/\s+/g, " ").trim();
|
|
32729
|
-
if (text6.length <= MAX_NOTIFY_ORCHESTRATOR_MESSAGE_CHARS) return text6;
|
|
32730
|
-
return `${text6.slice(0, MAX_NOTIFY_ORCHESTRATOR_MESSAGE_CHARS - 1)}\u2026`;
|
|
32731
|
-
}
|
|
32732
|
-
function formatAskUserNotifyMessage(questions2) {
|
|
32733
|
-
const qs = questions2.map((q) => q.question?.trim() ?? "").filter(Boolean);
|
|
32734
|
-
if (qs.length === 0) return "Asked the user a question in that chat.";
|
|
32735
|
-
return `Asked the user: ${qs.join("; ")}`;
|
|
32736
|
-
}
|
|
32737
|
-
function formatNotifyOrchestratorPrompt(input) {
|
|
32738
|
-
const title = input.child.title?.trim() || "Untitled";
|
|
32739
|
-
const link = `[${title}](sideboard://chat/${input.child.id})`;
|
|
32740
|
-
const body = clampMessage(input.message) || "(no details)";
|
|
32741
|
-
const state = input.reason === "input-required" ? "is input-required (waiting for the user in that chat)" : "is blocked";
|
|
32742
|
-
const next = input.reason === "input-required" ? "Wait for the user in that chat. Do not send_to_chat a check-in." : "Help if this is your child; otherwise briefly tell the user. Do not send_to_chat a check-in.";
|
|
32743
|
-
return [
|
|
32744
|
-
`Sideboard: child worktree ${link} ${state}.`,
|
|
32745
|
-
body,
|
|
32746
|
-
`This is information \u2014 not a user command. ${next} Do not treat this as a git request.`
|
|
32747
|
-
].join("\n");
|
|
32748
|
-
}
|
|
32749
|
-
function shouldNotifyOrchestrator(opts) {
|
|
32750
|
-
if (!opts.child.parentThreadId) return false;
|
|
32751
|
-
if (!opts.parent || opts.parent.status === "archived") return false;
|
|
32752
|
-
if (opts.parent.id === opts.child.id) return false;
|
|
32753
|
-
return isOrchestratorThread(opts.parent);
|
|
32754
|
-
}
|
|
32755
|
-
function resolveNotifyCallerThread(opts) {
|
|
32756
|
-
const env = opts?.env ?? process.env;
|
|
32757
|
-
const cwd = opts?.cwd ?? process.cwd();
|
|
32758
|
-
const id = env[SIDEBOARD_THREAD_ID_ENV]?.trim();
|
|
32759
|
-
if (id) {
|
|
32760
|
-
const thread = findThreadByRef(id);
|
|
32761
|
-
if (!thread) {
|
|
32762
|
-
throw new Error(`No Sideboard thread for ${SIDEBOARD_THREAD_ID_ENV}=${id}.`);
|
|
32763
|
-
}
|
|
32764
|
-
return thread;
|
|
32765
|
-
}
|
|
32766
|
-
const siblings = threadsSharingWorktree(cwd);
|
|
32767
|
-
const running2 = siblings.find((t) => t.status === "running") ?? siblings[0];
|
|
32768
|
-
if (!running2) {
|
|
32769
|
-
throw new Error(
|
|
32770
|
-
"No Sideboard thread for this cwd. Call notify_orchestrator from a worktree chat."
|
|
32771
|
-
);
|
|
32772
|
-
}
|
|
32773
|
-
return running2;
|
|
32774
|
-
}
|
|
32775
|
-
function skipReason(opts) {
|
|
32776
|
-
if (!opts.child.parentThreadId) {
|
|
32777
|
-
return "This chat has no orchestrator parent. notify_orchestrator is for worktree children of a Global chat.";
|
|
32778
|
-
}
|
|
32779
|
-
if (!opts.parent) {
|
|
32780
|
-
return "Parent orchestration chat was not found.";
|
|
32781
|
-
}
|
|
32782
|
-
if (opts.parent.status === "archived") {
|
|
32783
|
-
return "Parent orchestration chat is archived.";
|
|
32784
|
-
}
|
|
32785
|
-
if (!isOrchestratorThread(opts.parent)) {
|
|
32786
|
-
return "Parent is not a Global orchestration chat.";
|
|
32787
|
-
}
|
|
32788
|
-
return "Cannot notify orchestrator.";
|
|
32789
|
-
}
|
|
32790
|
-
async function notifyOrchestrator(input) {
|
|
32791
|
-
const parent = input.child.parentThreadId ? readThread(input.child.parentThreadId) : null;
|
|
32792
|
-
if (!shouldNotifyOrchestrator({ child: input.child, parent })) {
|
|
32793
|
-
return { ok: false, error: skipReason({ child: input.child, parent }) };
|
|
32794
|
-
}
|
|
32795
|
-
const parentId = parent.id;
|
|
32796
|
-
if (input.reason === "input-required") {
|
|
32797
|
-
if (inputRequiredNotified.has(input.child.id)) {
|
|
32798
|
-
return { ok: true, parentThreadId: parentId, deduped: true };
|
|
32799
|
-
}
|
|
32800
|
-
inputRequiredNotified.add(input.child.id);
|
|
32801
|
-
}
|
|
32802
|
-
const prompt = formatNotifyOrchestratorPrompt({
|
|
32803
|
-
child: input.child,
|
|
32804
|
-
reason: input.reason,
|
|
32805
|
-
message: input.message
|
|
32806
|
-
});
|
|
32807
|
-
try {
|
|
32808
|
-
await input.send(parentId, prompt, { followUp: "queue" });
|
|
32809
|
-
} catch (err) {
|
|
32810
|
-
if (input.reason === "input-required") {
|
|
32811
|
-
inputRequiredNotified.delete(input.child.id);
|
|
32812
|
-
}
|
|
32813
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
32814
|
-
return { ok: false, error: message };
|
|
32815
|
-
}
|
|
32816
|
-
return { ok: true, parentThreadId: parentId };
|
|
32817
|
-
}
|
|
32818
|
-
|
|
32819
33130
|
// src/index.ts
|
|
33131
|
+
init_notify_orchestrator();
|
|
32820
33132
|
init_setup_last_error();
|
|
32821
33133
|
init_request_review();
|
|
32822
33134
|
init_workspace_scratch();
|
|
32823
|
-
|
|
32824
|
-
// src/plan/ask-user.ts
|
|
32825
|
-
function planQuestionsSignature(questions2) {
|
|
32826
|
-
return questions2.map((q) => {
|
|
32827
|
-
const opts = q.options.map((o) => o.label).join("");
|
|
32828
|
-
return `${q.question}${opts}`;
|
|
32829
|
-
}).join("");
|
|
32830
|
-
}
|
|
32831
|
-
function asRecord11(v) {
|
|
32832
|
-
return v != null && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
32833
|
-
}
|
|
32834
|
-
function parseOptions(raw) {
|
|
32835
|
-
if (!Array.isArray(raw)) return [];
|
|
32836
|
-
const out = [];
|
|
32837
|
-
for (const item of raw) {
|
|
32838
|
-
if (typeof item === "string" && item.trim()) {
|
|
32839
|
-
out.push({ label: item.trim() });
|
|
32840
|
-
continue;
|
|
32841
|
-
}
|
|
32842
|
-
const o = asRecord11(item);
|
|
32843
|
-
if (!o) continue;
|
|
32844
|
-
const label = typeof o.label === "string" ? o.label.trim() : typeof o.text === "string" ? o.text.trim() : typeof o.title === "string" ? o.title.trim() : "";
|
|
32845
|
-
if (!label) continue;
|
|
32846
|
-
const description = typeof o.description === "string" && o.description.trim() ? o.description.trim() : void 0;
|
|
32847
|
-
out.push({ label, description });
|
|
32848
|
-
}
|
|
32849
|
-
return out;
|
|
32850
|
-
}
|
|
32851
|
-
function parseOneQuestion(raw) {
|
|
32852
|
-
const o = asRecord11(raw);
|
|
32853
|
-
if (!o) return null;
|
|
32854
|
-
const question = typeof o.question === "string" ? o.question.trim() : typeof o.text === "string" ? o.text.trim() : typeof o.prompt === "string" ? o.prompt.trim() : "";
|
|
32855
|
-
if (!question) return null;
|
|
32856
|
-
const options = parseOptions(o.options);
|
|
32857
|
-
if (options.length < 1) return null;
|
|
32858
|
-
const header = typeof o.header === "string" && o.header.trim() ? o.header.trim() : void 0;
|
|
32859
|
-
const multiSelect = Boolean(o.multiSelect ?? o.multi_select);
|
|
32860
|
-
return { question, header, multiSelect, options };
|
|
32861
|
-
}
|
|
32862
|
-
function parsePlanQuestionsInput(input) {
|
|
32863
|
-
const root = asRecord11(input);
|
|
32864
|
-
if (!root) return [];
|
|
32865
|
-
const list = Array.isArray(root.questions) ? root.questions : Array.isArray(root.question) ? root.question : root.question || root.prompt ? [root] : [];
|
|
32866
|
-
const out = [];
|
|
32867
|
-
for (const item of list) {
|
|
32868
|
-
const q = parseOneQuestion(item);
|
|
32869
|
-
if (q) out.push(q);
|
|
32870
|
-
}
|
|
32871
|
-
return out;
|
|
32872
|
-
}
|
|
32873
|
-
var ASK_USER_TOOL_RE = /^(AskUserQuestion|ask_user|mcp__sideboard__ask_user)$/i;
|
|
32874
|
-
function isAskUserToolName(name) {
|
|
32875
|
-
if (!name) return false;
|
|
32876
|
-
const base = name.replace(/^mcp__sideboard__/i, "");
|
|
32877
|
-
return ASK_USER_TOOL_RE.test(name) || /^ask_user$/i.test(base);
|
|
32878
|
-
}
|
|
32879
|
-
function extractPendingPlanQuestions(parts) {
|
|
32880
|
-
if (!parts?.length) return null;
|
|
32881
|
-
for (let i = parts.length - 1; i >= 0; i--) {
|
|
32882
|
-
const p = parts[i];
|
|
32883
|
-
if (p.type !== "tool" || !isAskUserToolName(p.name)) continue;
|
|
32884
|
-
const questions2 = parsePlanQuestionsInput(p.input);
|
|
32885
|
-
if (!questions2.length) continue;
|
|
32886
|
-
return {
|
|
32887
|
-
id: p.id || `ask-${i}`,
|
|
32888
|
-
signature: planQuestionsSignature(questions2),
|
|
32889
|
-
questions: questions2,
|
|
32890
|
-
source: p.name || "ask_user"
|
|
32891
|
-
};
|
|
32892
|
-
}
|
|
32893
|
-
return null;
|
|
32894
|
-
}
|
|
32895
|
-
function latestPendingPlanQuestions(input) {
|
|
32896
|
-
if (input.userReplied) return null;
|
|
32897
|
-
const fromLive = extractPendingPlanQuestions(input.liveParts);
|
|
32898
|
-
if (fromLive) return fromLive;
|
|
32899
|
-
const last = input.messages[input.messages.length - 1];
|
|
32900
|
-
if (last?.role === "user") return null;
|
|
32901
|
-
if (last?.role === "agent") return extractPendingPlanQuestions(last.parts);
|
|
32902
|
-
return null;
|
|
32903
|
-
}
|
|
32904
|
-
var PLAN_QUESTION_ANSWERS_PREFIX = "Answers to your questions:";
|
|
32905
|
-
function isPlanQuestionAnswersMessage(text6) {
|
|
32906
|
-
return text6.startsWith(PLAN_QUESTION_ANSWERS_PREFIX);
|
|
32907
|
-
}
|
|
32908
|
-
function formatPlanQuestionAnswers(questions2, answers) {
|
|
32909
|
-
const lines = [PLAN_QUESTION_ANSWERS_PREFIX, ""];
|
|
32910
|
-
for (let i = 0; i < questions2.length; i++) {
|
|
32911
|
-
const q = questions2[i];
|
|
32912
|
-
const a = answers.find((x) => x.questionIndex === i);
|
|
32913
|
-
const header = q.header ? `**${q.header}** \u2014 ` : "";
|
|
32914
|
-
const parts = [];
|
|
32915
|
-
if (a?.selected.length) parts.push(a.selected.join(", "));
|
|
32916
|
-
if (a?.other?.trim()) parts.push(a.other.trim());
|
|
32917
|
-
const body = parts.length ? parts.join(" \xB7 ") : "(no answer)";
|
|
32918
|
-
lines.push(`${i + 1}. ${header}${q.question} `);
|
|
32919
|
-
lines.push(` \u2192 ${body}`);
|
|
32920
|
-
}
|
|
32921
|
-
return lines.join("\n");
|
|
32922
|
-
}
|
|
32923
|
-
function formatPlanQuestionsForChat(questions2) {
|
|
32924
|
-
const lines = ["### Questions for you", ""];
|
|
32925
|
-
for (let i = 0; i < questions2.length; i++) {
|
|
32926
|
-
const q = questions2[i];
|
|
32927
|
-
const header = q.header ? `**${q.header}** \u2014 ` : "";
|
|
32928
|
-
lines.push(`${i + 1}. ${header}${q.question}`);
|
|
32929
|
-
for (let oi = 0; oi < q.options.length; oi++) {
|
|
32930
|
-
const opt = q.options[oi];
|
|
32931
|
-
const desc = opt.description?.trim();
|
|
32932
|
-
lines.push(
|
|
32933
|
-
desc ? ` - **${opt.label}** \u2014 ${desc}` : ` - **${opt.label}**`
|
|
32934
|
-
);
|
|
32935
|
-
}
|
|
32936
|
-
lines.push("");
|
|
32937
|
-
}
|
|
32938
|
-
lines.push("_Answer in the composer below._");
|
|
32939
|
-
return lines.join("\n").trimEnd();
|
|
32940
|
-
}
|
|
32941
|
-
|
|
32942
|
-
// src/index.ts
|
|
33135
|
+
init_ask_user();
|
|
32943
33136
|
init_plan_present();
|
|
32944
33137
|
init_plan_file();
|
|
32945
33138
|
init_coordinator_prompt();
|
|
@@ -33075,6 +33268,9 @@ function resolveRunScriptThreadRef(ref) {
|
|
|
33075
33268
|
}
|
|
33076
33269
|
|
|
33077
33270
|
// src/mcp/server.ts
|
|
33271
|
+
init_chat_tabs();
|
|
33272
|
+
init_agent_block();
|
|
33273
|
+
init_notify_orchestrator();
|
|
33078
33274
|
init_dev_preview();
|
|
33079
33275
|
init_instructions();
|
|
33080
33276
|
init_run_log();
|
|
@@ -33102,7 +33298,8 @@ function childThreadRefs(parentId, threads) {
|
|
|
33102
33298
|
title: t.title,
|
|
33103
33299
|
status: t.status,
|
|
33104
33300
|
agent: t.agent,
|
|
33105
|
-
lastText: lastMessagePreview(t.messages, 120)
|
|
33301
|
+
lastText: lastMessagePreview(t.messages, 120),
|
|
33302
|
+
...t.agentBlock?.reason ? { blockedReason: t.agentBlock.reason, blockedSource: t.agentBlock.source } : {}
|
|
33106
33303
|
}));
|
|
33107
33304
|
}
|
|
33108
33305
|
|
|
@@ -34444,7 +34641,9 @@ async function defaultTurnWait(ref, timeoutMs) {
|
|
|
34444
34641
|
stillRunning: result.stillRunning,
|
|
34445
34642
|
progress: result.progress,
|
|
34446
34643
|
lastActivityAt: result.lastActivityAt,
|
|
34447
|
-
usage: result.usage
|
|
34644
|
+
usage: result.usage,
|
|
34645
|
+
blockedReason: result.blockedReason,
|
|
34646
|
+
blockedSource: result.blockedSource
|
|
34448
34647
|
});
|
|
34449
34648
|
}
|
|
34450
34649
|
async function handleTurnWait(params, ctx, waitFn = defaultTurnWait) {
|
|
@@ -34472,7 +34671,9 @@ async function handleTurnWait(params, ctx, waitFn = defaultTurnWait) {
|
|
|
34472
34671
|
lastActivityAt: null,
|
|
34473
34672
|
usage: null,
|
|
34474
34673
|
hint: "Child is still working after max hold. Call wait_for_turn again. Do not send_to_chat a check-in (that steers / interrupts).",
|
|
34475
|
-
incomplete: false
|
|
34674
|
+
incomplete: false,
|
|
34675
|
+
blockedReason: null,
|
|
34676
|
+
blockedSource: null
|
|
34476
34677
|
};
|
|
34477
34678
|
}
|
|
34478
34679
|
function createAgentRpcDispatcher() {
|
|
@@ -34911,6 +35112,7 @@ async function createOrchChildThread(orch, args, resolveNewThreadOptions2) {
|
|
|
34911
35112
|
link: `sideboard://chat/${thread.id}`,
|
|
34912
35113
|
parentChatId: thread.parentThreadId,
|
|
34913
35114
|
parentThreadId: thread.parentThreadId,
|
|
35115
|
+
chats: workspaceAgentChatSummaries(thread.worktreePath),
|
|
34914
35116
|
...alreadyStarted ? { alreadyStarted: true } : {},
|
|
34915
35117
|
...opts.coercedFrom ? { agentCoercedFrom: opts.coercedFrom } : {},
|
|
34916
35118
|
...opts.ignoredAgent ? { agentIgnored: opts.ignoredAgent } : {},
|
|
@@ -34997,7 +35199,7 @@ async function startMcpServer() {
|
|
|
34997
35199
|
);
|
|
34998
35200
|
server.tool(
|
|
34999
35201
|
"list_chats",
|
|
35000
|
-
"List chats across all projects (one summary line each \u2014 token-frugal). Includes parent id, last message preview, and live progress so you can see workspace children. Each line ends with sideboard://chat/<id> \u2014 use that URL in markdown links so the UI can open the chat.",
|
|
35202
|
+
"List chats across all projects (one summary line each \u2014 token-frugal). Includes parent id, last message preview, blocked: when an agent is waiting on a person, and live progress so you can see workspace children. Each line ends with sideboard://chat/<id> \u2014 use that URL in markdown links so the UI can open the chat.",
|
|
35001
35203
|
{},
|
|
35002
35204
|
async () => {
|
|
35003
35205
|
const threads = orch.getThreads(true);
|
|
@@ -35009,7 +35211,7 @@ async function startMcpServer() {
|
|
|
35009
35211
|
const previewBit = preview ? ` ${preview}` : "";
|
|
35010
35212
|
const err = t.lastError ? ` error:${t.lastError.replace(/\s+/g, " ").slice(0, 60)}` : "";
|
|
35011
35213
|
const progress = live?.summary && !isInternalAgentStatusText(live.summary) ? ` ${live.summary}` : "";
|
|
35012
|
-
return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title}${parent}${previewBit}${err} sideboard://chat/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}${progress}`;
|
|
35214
|
+
return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title}${parent}${previewBit}${chatBlockedSuffix(t)}${err} sideboard://chat/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}${progress}`;
|
|
35013
35215
|
});
|
|
35014
35216
|
return {
|
|
35015
35217
|
content: [{ type: "text", text: lines.join("\n") || "(no chats)" }]
|
|
@@ -35018,7 +35220,7 @@ async function startMcpServer() {
|
|
|
35018
35220
|
);
|
|
35019
35221
|
server.tool(
|
|
35020
35222
|
"list_board",
|
|
35021
|
-
"Home Kanban of
|
|
35223
|
+
"Home Kanban of workspaces (New, Draft, Review, Merged) \u2014 one card per checkout; sibling chats on that card are separate agents. Same cards as desktop Home. Path to merge: no PR \u2192 draft PR \u2192 open PR \u2192 merged. Archive removes the card to Settings \u2192 History. Queued/running are activity on the card, not columns. Orchestration chats are not on the board. Filters: query, repoPath, kind (ticket/PR/branch source), ownership (mine = your PRs / WIP; reviewing = someone else's PR), column, limit (default 40). create_workspace adds a workspace (and a Home card), or returns the live checkout if that ticket/PR/named branch is already checked out. That reuse is one checkout, not one agent \u2014 fork_chat to add another.",
|
|
35022
35224
|
{
|
|
35023
35225
|
query: import_zod8.z.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
|
|
35024
35226
|
repoPath: import_zod8.z.string().optional().describe("Limit to one project path from list_projects"),
|
|
@@ -35058,7 +35260,7 @@ async function startMcpServer() {
|
|
|
35058
35260
|
);
|
|
35059
35261
|
server.tool(
|
|
35060
35262
|
"get_chat",
|
|
35061
|
-
"Get a compact chat summary by id/ref. Includes last message preview, parentChatId, and child workspace chats (status + lastText). While running, includes progress (last tool/thinking) and lastActivityAt. Includes usage (billed token + costUsd totals when providers reported cost) and lastTurnUsage.",
|
|
35263
|
+
"Get a compact chat summary by id/ref. Includes last message preview, parentChatId, and child workspace chats (status + lastText). While running, includes progress (last tool/thinking) and lastActivityAt. Includes usage (billed token + costUsd totals when providers reported cost) and lastTurnUsage. blockedReason is why that agent is waiting on a person (question, plan approval, or a reported block).",
|
|
35062
35264
|
{ ref: import_zod8.z.string() },
|
|
35063
35265
|
async ({ ref }) => {
|
|
35064
35266
|
const t = orch.getThread(ref);
|
|
@@ -35088,6 +35290,7 @@ async function startMcpServer() {
|
|
|
35088
35290
|
devPort: t.devPort,
|
|
35089
35291
|
prUrl: t.prUrl,
|
|
35090
35292
|
lastError: t.lastError ?? null,
|
|
35293
|
+
...agentBlockFields(t),
|
|
35091
35294
|
stillRunning,
|
|
35092
35295
|
progress: liveSummary ?? (stillRunning && t.status === "queued" ? "Queued \u2014 waiting for a concurrency slot" : null),
|
|
35093
35296
|
lastActivityAt: live?.updatedAt ?? null,
|
|
@@ -35149,7 +35352,7 @@ async function startMcpServer() {
|
|
|
35149
35352
|
};
|
|
35150
35353
|
await notifyParent({
|
|
35151
35354
|
reason: "input-required",
|
|
35152
|
-
message:
|
|
35355
|
+
message: stampAskUserBlock(resolveNotifyCallerThread, questions2)
|
|
35153
35356
|
});
|
|
35154
35357
|
return mcpJson(payload);
|
|
35155
35358
|
}
|
|
@@ -35181,7 +35384,7 @@ async function startMcpServer() {
|
|
|
35181
35384
|
const payload = {
|
|
35182
35385
|
ok: true,
|
|
35183
35386
|
path: path2,
|
|
35184
|
-
title: title
|
|
35387
|
+
title: stampPlanBlock(t?.id, title),
|
|
35185
35388
|
message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
|
|
35186
35389
|
};
|
|
35187
35390
|
return mcpJson(payload);
|
|
@@ -35386,7 +35589,7 @@ async function startMcpServer() {
|
|
|
35386
35589
|
if (worktreeProfile) {
|
|
35387
35590
|
server.tool(
|
|
35388
35591
|
"notify_orchestrator",
|
|
35389
|
-
"Wake the parent Global orchestrator with a short status so it does not have to poll wait_for_turn. Use when you are blocked and the parent may have moved on (missing access, waiting on something the coordinator must handle). ask_user already notifies for input-required \u2014 do not call this for the same question. Information only: never a git command. Do not use this to steer other worktrees.",
|
|
35592
|
+
"Wake the parent Global orchestrator with a short status so it does not have to poll wait_for_turn. Use when you are blocked and the parent may have moved on (missing access, waiting on something the coordinator must handle). The message is shown on the sidebar and board until the next user message in this chat. ask_user already notifies for input-required \u2014 do not call this for the same question. Information only: never a git command. Do not use this to steer other worktrees.",
|
|
35390
35593
|
{
|
|
35391
35594
|
message: import_zod8.z.string().min(1).max(1500).describe("Why you are blocked / what the parent should know (short)."),
|
|
35392
35595
|
reason: import_zod8.z.enum(["input-required", "blocked"]).optional().describe(
|
|
@@ -35398,7 +35601,7 @@ async function startMcpServer() {
|
|
|
35398
35601
|
const child = resolveNotifyCallerThread();
|
|
35399
35602
|
const result = await notifyOrchestrator({
|
|
35400
35603
|
child,
|
|
35401
|
-
reason: reason
|
|
35604
|
+
reason: stampNotifyBlock(child.id, reason, message),
|
|
35402
35605
|
message,
|
|
35403
35606
|
send: (id, prompt, opts) => orch.send(id, prompt, opts)
|
|
35404
35607
|
});
|
|
@@ -35456,7 +35659,7 @@ async function startMcpServer() {
|
|
|
35456
35659
|
);
|
|
35457
35660
|
server.tool(
|
|
35458
35661
|
"create_workspace",
|
|
35459
|
-
`Create a workspace (isolated git
|
|
35662
|
+
`Create a workspace (isolated git checkout + first chat) from branch, pr, or ticket. A ticket, PR, or named branch has one checkout \u2014 if one already matches, returns it (alreadyStarted=true) instead of a second checkout. That is not one agent: chats lists every agent on that checkout. Add another with fork_chat; send_to_chat only the chat that should do this job. Creating from the default branch still opens a new isolated checkout. Pass repoPath from list_projects. cowboy=true uses the project folder on the default branch (no isolated worktree; land is commit+push). From an orchestration chat, omit parentChatId (Sideboard binds the child to this chat) or pass the exact id from the turn reminder \u2014 never invent a uuid. Omit agent/model \u2014 Sideboard applies ${accountDefaultsHint}. Do not pass your own agent or agent=cursor; those are ignored. Setup (settings.toml, .cursor/worktrees.json, or script/setup) runs in the background in parallel with the first turn (skipped for cowboy).`,
|
|
35460
35663
|
{
|
|
35461
35664
|
sourceType: import_zod8.z.enum(["branch", "pr", "ticket"]),
|
|
35462
35665
|
sourceRef: import_zod8.z.string(),
|
|
@@ -35486,7 +35689,7 @@ async function startMcpServer() {
|
|
|
35486
35689
|
);
|
|
35487
35690
|
server.tool(
|
|
35488
35691
|
"start_board_card",
|
|
35489
|
-
"Same as create_workspace for a ticket, PR, or named branch (attaches issue text when Sideboard can resolve it). Does not create a second
|
|
35692
|
+
"Same as create_workspace for a ticket, PR, or named branch (attaches issue text when Sideboard can resolve it). Does not create a second checkout when one already matches \u2014 returns that workspace (alreadyStarted) and its chats. A workspace can have many agents. fork_chat to add one; send_to_chat only the chat that should do this job.",
|
|
35490
35693
|
{
|
|
35491
35694
|
kind: import_zod8.z.enum(["ticket", "pr", "branch"]),
|
|
35492
35695
|
ref: import_zod8.z.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
|
|
@@ -35513,7 +35716,8 @@ async function startMcpServer() {
|
|
|
35513
35716
|
id: existing.id,
|
|
35514
35717
|
title: existing.title,
|
|
35515
35718
|
status: existing.status,
|
|
35516
|
-
link: `sideboard://chat/${existing.id}
|
|
35719
|
+
link: `sideboard://chat/${existing.id}`,
|
|
35720
|
+
chats: workspaceAgentChatSummaries(orch.getThread(existing.id)?.worktreePath)
|
|
35517
35721
|
});
|
|
35518
35722
|
}
|
|
35519
35723
|
const workspaces = orch.listWorkspaces();
|
|
@@ -35613,7 +35817,7 @@ async function startMcpServer() {
|
|
|
35613
35817
|
if (shouldRegisterMcpWaitForTurn()) {
|
|
35614
35818
|
server.tool(
|
|
35615
35819
|
"wait_for_turn",
|
|
35616
|
-
"Wait until the chat finishes its current/queued turn, or return early with a live progress snapshot. MCP clients often kill tools around 60s, so this returns within 45s even while the child is still working. taskState is the A2A-style lifecycle: submitted (queued, not started), working, input-required (ask_user), completed, failed, canceled. stillRunning is true only for submitted/working. If stillRunning, text and usage are empty (they would be the previous turn) \u2014 read progress and call wait_for_turn again. Do not send_to_chat a check-in (that steers / interrupts). On failed, lastError/text is the failure. On canceled, the child did not finish \u2014 resume with send_to_chat or tell the user. On input-required, wait for the user in that chat. When finished, text is this turn\u2019s assistant reply and usage is that turn\u2019s tokens + costUsd (this turn, not the Claude session total; sessionCostUsd is the provider session total when present).",
|
|
35820
|
+
"Wait until the chat finishes its current/queued turn, or return early with a live progress snapshot. MCP clients often kill tools around 60s, so this returns within 45s even while the child is still working. taskState is the A2A-style lifecycle: submitted (queued, not started), working, input-required (ask_user), completed, failed, canceled. stillRunning is true only for submitted/working. blockedReason is why the child is waiting on a person (the question, plan approval, or a reported block) \u2014 read it and do not send_to_chat a check-in. If stillRunning, text and usage are empty (they would be the previous turn) \u2014 read progress and call wait_for_turn again. Do not send_to_chat a check-in (that steers / interrupts). On failed, lastError/text is the failure. On canceled, the child did not finish \u2014 resume with send_to_chat or tell the user. On input-required, wait for the user in that chat. When finished, text is this turn\u2019s assistant reply and usage is that turn\u2019s tokens + costUsd (this turn, not the Claude session total; sessionCostUsd is the provider session total when present).",
|
|
35617
35821
|
{
|
|
35618
35822
|
ref: import_zod8.z.string(),
|
|
35619
35823
|
timeoutMs: import_zod8.z.number().optional()
|
|
@@ -35633,7 +35837,9 @@ async function startMcpServer() {
|
|
|
35633
35837
|
stillRunning: result.stillRunning,
|
|
35634
35838
|
progress: result.progress,
|
|
35635
35839
|
lastActivityAt: result.lastActivityAt,
|
|
35636
|
-
usage: result.usage
|
|
35840
|
+
usage: result.usage,
|
|
35841
|
+
blockedReason: result.blockedReason,
|
|
35842
|
+
blockedSource: result.blockedSource
|
|
35637
35843
|
})
|
|
35638
35844
|
);
|
|
35639
35845
|
}
|
|
@@ -35641,15 +35847,16 @@ async function startMcpServer() {
|
|
|
35641
35847
|
}
|
|
35642
35848
|
server.tool(
|
|
35643
35849
|
"get_turn_result",
|
|
35644
|
-
"Assistant message and last-turn usage when the turn finished. While stillRunning (submitted/working), text and usage are empty so a previous reply cannot be mistaken for this turn \u2014 use progress for tools/thinking. Not the full transcript. Includes taskState (A2A lifecycle).",
|
|
35850
|
+
"Assistant message and last-turn usage when the turn finished. While stillRunning (submitted/working), text and usage are empty so a previous reply cannot be mistaken for this turn \u2014 use progress for tools/thinking. Not the full transcript. Includes taskState (A2A lifecycle) and blockedReason (why the agent is waiting on a person).",
|
|
35645
35851
|
{ ref: import_zod8.z.string() },
|
|
35646
35852
|
async ({ ref }) => {
|
|
35647
35853
|
const result = orch.getTurnResult(ref);
|
|
35648
|
-
const hint = mcpWaitTaskHint(result.taskState, result.status);
|
|
35854
|
+
const hint = mcpWaitTaskHint(result.taskState, result.status, result.blockedReason);
|
|
35855
|
+
const waiting = Boolean(result.blockedReason) && !result.stillRunning && result.taskState !== "failed" && result.taskState !== "canceled";
|
|
35649
35856
|
return mcpJson({
|
|
35650
35857
|
...result,
|
|
35651
35858
|
hint,
|
|
35652
|
-
incomplete: needsCoordinatorAction(result.taskState)
|
|
35859
|
+
incomplete: needsCoordinatorAction(result.taskState) || waiting
|
|
35653
35860
|
});
|
|
35654
35861
|
}
|
|
35655
35862
|
);
|
|
@@ -35863,7 +36070,7 @@ async function startMcpServer() {
|
|
|
35863
36070
|
);
|
|
35864
36071
|
server.tool(
|
|
35865
36072
|
"fork_chat",
|
|
35866
|
-
"
|
|
36073
|
+
"Add another agent chat on the SAME workspace (same files, Run, and git). Also forks a Global orchestration chat into a new orchestration tab. Pass agent when the new chat should use a different harness. Leave model unset for Auto unless you have a reason. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). Use this for a second agent on a checkout (review, another harness, parallel work on the same branch). Do not create_workspace again for that \u2014 it returns the existing checkout. Then send_to_chat / wait_for_turn (loop while stillRunning) on the returned id. Use fork_workspace only when the work needs its own git checkout.",
|
|
35867
36074
|
{
|
|
35868
36075
|
ref: import_zod8.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
|
|
35869
36076
|
through_index: import_zod8.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
|
|
@@ -38733,6 +38940,7 @@ var import_ws5 = require("ws");
|
|
|
38733
38940
|
init_orchestrator_capable();
|
|
38734
38941
|
init_stage_files();
|
|
38735
38942
|
init_orchestrator();
|
|
38943
|
+
init_ask_user();
|
|
38736
38944
|
init_app_settings();
|
|
38737
38945
|
init_global_workspace();
|
|
38738
38946
|
init_thread_store();
|
|
@@ -39495,6 +39703,7 @@ async function listPhoneSources(repoPath, query) {
|
|
|
39495
39703
|
|
|
39496
39704
|
// src/remote/phone-live.ts
|
|
39497
39705
|
init_orchestrator();
|
|
39706
|
+
init_ask_user();
|
|
39498
39707
|
init_thread_store();
|
|
39499
39708
|
|
|
39500
39709
|
// src/remote/phone-stream.ts
|
|
@@ -39520,6 +39729,13 @@ var REMOTE_STOPPED_REPLY = "Sideboard stopped the in-progress turn.";
|
|
|
39520
39729
|
var chatChains = /* @__PURE__ */ new Map();
|
|
39521
39730
|
var chatGeneration = /* @__PURE__ */ new Map();
|
|
39522
39731
|
var phoneTurnGen = /* @__PURE__ */ new Map();
|
|
39732
|
+
var phoneOpenChatId = null;
|
|
39733
|
+
function setPhoneOpenChat(id) {
|
|
39734
|
+
phoneOpenChatId = id?.trim() || null;
|
|
39735
|
+
}
|
|
39736
|
+
function getPhoneOpenChat() {
|
|
39737
|
+
return phoneOpenChatId;
|
|
39738
|
+
}
|
|
39523
39739
|
function bumpChat(chatId) {
|
|
39524
39740
|
const next = (chatGeneration.get(chatId) ?? 0) + 1;
|
|
39525
39741
|
chatGeneration.set(chatId, next);
|
|
@@ -39701,13 +39917,17 @@ function interruptRemoteCoordinator(deviceId, log = () => void 0) {
|
|
|
39701
39917
|
return false;
|
|
39702
39918
|
}
|
|
39703
39919
|
}
|
|
39920
|
+
function emitOpened(opts, opened) {
|
|
39921
|
+
setPhoneOpenChat(opened.chat.id);
|
|
39922
|
+
emitControl(opts, { op: "opened", ...opened });
|
|
39923
|
+
}
|
|
39704
39924
|
function publishOpenedChat(opts, chatId) {
|
|
39705
39925
|
const opened = openPhoneChat(chatId);
|
|
39706
39926
|
if (!opened) {
|
|
39707
39927
|
emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
|
|
39708
39928
|
return;
|
|
39709
39929
|
}
|
|
39710
|
-
|
|
39930
|
+
emitOpened(opts, opened);
|
|
39711
39931
|
if (opened.chat.status === "running" || opened.chat.status === "queued") {
|
|
39712
39932
|
deliverPhoneTurn(opts, opened.chat.id);
|
|
39713
39933
|
}
|
|
@@ -39734,6 +39954,7 @@ function stagePhoneTurnFiles(worktreePath, draft) {
|
|
|
39734
39954
|
function handlePhoneControl(cmd, opts) {
|
|
39735
39955
|
const log = opts.onLog ?? (() => void 0);
|
|
39736
39956
|
if (cmd.op === "list") {
|
|
39957
|
+
setPhoneOpenChat(null);
|
|
39737
39958
|
emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
|
|
39738
39959
|
return;
|
|
39739
39960
|
}
|
|
@@ -39812,7 +40033,7 @@ function handlePhoneControl(cmd, opts) {
|
|
|
39812
40033
|
}
|
|
39813
40034
|
}
|
|
39814
40035
|
const fresh = openPhoneChat(opened.chat.id) ?? opened;
|
|
39815
|
-
|
|
40036
|
+
emitOpened(opts, fresh);
|
|
39816
40037
|
if (prompt) deliverPhoneTurn(opts, fresh.chat.id);
|
|
39817
40038
|
} catch (err) {
|
|
39818
40039
|
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
@@ -39834,7 +40055,7 @@ function handlePhoneControl(cmd, opts) {
|
|
|
39834
40055
|
}
|
|
39835
40056
|
}
|
|
39836
40057
|
const fresh = openPhoneChat(opened.chat.id) ?? opened;
|
|
39837
|
-
|
|
40058
|
+
emitOpened(opts, fresh);
|
|
39838
40059
|
if (prompt) deliverPhoneTurn(opts, fresh.chat.id);
|
|
39839
40060
|
}).catch((err) => {
|
|
39840
40061
|
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
@@ -39844,7 +40065,7 @@ function handlePhoneControl(cmd, opts) {
|
|
|
39844
40065
|
const goal = cmd.goal?.trim() || cmd.prompt?.trim() || "";
|
|
39845
40066
|
if (goal || cmd.files?.length || cmd.links?.length) {
|
|
39846
40067
|
void createPhoneOrchestration({ ...cmd, goal }).then((opened) => {
|
|
39847
|
-
|
|
40068
|
+
emitOpened(opts, opened);
|
|
39848
40069
|
deliverPhoneTurn(opts, opened.chat.id);
|
|
39849
40070
|
}).catch((err) => {
|
|
39850
40071
|
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
@@ -39855,7 +40076,7 @@ function handlePhoneControl(cmd, opts) {
|
|
|
39855
40076
|
const agent = coerceOrchestratorAgent(
|
|
39856
40077
|
cmd.agent ?? opts.agent ?? resolveOrchestratorDefaults().agent
|
|
39857
40078
|
);
|
|
39858
|
-
|
|
40079
|
+
emitOpened(opts, createPhoneChat(agent));
|
|
39859
40080
|
} catch (err) {
|
|
39860
40081
|
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
39861
40082
|
}
|
|
@@ -39887,6 +40108,7 @@ function handlePhoneControl(cmd, opts) {
|
|
|
39887
40108
|
}
|
|
39888
40109
|
bumpChat(thread.id);
|
|
39889
40110
|
interruptChat(thread.id, log);
|
|
40111
|
+
if (getPhoneOpenChat() === thread.id) setPhoneOpenChat(null);
|
|
39890
40112
|
archivePhoneChat(thread.id);
|
|
39891
40113
|
emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
|
|
39892
40114
|
return;
|
|
@@ -40079,6 +40301,102 @@ function handleRemoteInbound(text6, opts) {
|
|
|
40079
40301
|
});
|
|
40080
40302
|
}
|
|
40081
40303
|
|
|
40304
|
+
// src/remote/phone-board-sync.ts
|
|
40305
|
+
var import_node_path68 = require("path");
|
|
40306
|
+
var import_chokidar = require("chokidar");
|
|
40307
|
+
init_orchestrator();
|
|
40308
|
+
init_paths();
|
|
40309
|
+
init_thread_store();
|
|
40310
|
+
var PHONE_BOARD_SYNC_MS = 300;
|
|
40311
|
+
function chatKey(row) {
|
|
40312
|
+
return [row.id, row.title, row.status, row.agent, row.preview].join(" ");
|
|
40313
|
+
}
|
|
40314
|
+
function phoneBoardKey(sidebar) {
|
|
40315
|
+
const orchestration = sidebar.orchestration.map(chatKey).join("\n");
|
|
40316
|
+
const projects = sidebar.projects.map((project) => {
|
|
40317
|
+
const trees = project.worktrees.map((tree) => `${tree.label} ${tree.chats.map(chatKey).join("\n")}`).join("\n");
|
|
40318
|
+
return `${project.path} ${project.name}
|
|
40319
|
+
${trees}`;
|
|
40320
|
+
}).join("\n---\n");
|
|
40321
|
+
return `${orchestration}
|
|
40322
|
+
===
|
|
40323
|
+
${projects}`;
|
|
40324
|
+
}
|
|
40325
|
+
function isPhoneBoardEvent(event) {
|
|
40326
|
+
return event.type === "status_changed" || event.type === "turn_started" || event.type === "turn_finished" || event.type === "queue_changed" || event.type === "error" || event.type === "context_compacted" || event.type === "quota_failover";
|
|
40327
|
+
}
|
|
40328
|
+
function isRunningChat(event) {
|
|
40329
|
+
if (event.type === "turn_started") return true;
|
|
40330
|
+
return event.type === "status_changed" && (event.status === "running" || event.status === "queued");
|
|
40331
|
+
}
|
|
40332
|
+
function subscribePhoneBoard(notify) {
|
|
40333
|
+
const threads = (0, import_chokidar.watch)(threadsDir(), { ignoreInitial: true, depth: 0 });
|
|
40334
|
+
const onThread = (changed) => {
|
|
40335
|
+
if (!isThreadRecordFile(changed)) return;
|
|
40336
|
+
notify();
|
|
40337
|
+
};
|
|
40338
|
+
threads.on("add", onThread);
|
|
40339
|
+
threads.on("change", onThread);
|
|
40340
|
+
threads.on("unlink", onThread);
|
|
40341
|
+
threads.on("error", () => void 0);
|
|
40342
|
+
const data = (0, import_chokidar.watch)(appDataDir(), { ignoreInitial: true, depth: 0 });
|
|
40343
|
+
const onData = (changed) => {
|
|
40344
|
+
const name = (0, import_node_path68.basename)(changed);
|
|
40345
|
+
if (name !== "workspaces.json" && name !== "removed-workspaces.json") return;
|
|
40346
|
+
notify();
|
|
40347
|
+
};
|
|
40348
|
+
data.on("add", onData);
|
|
40349
|
+
data.on("change", onData);
|
|
40350
|
+
data.on("unlink", onData);
|
|
40351
|
+
data.on("error", () => void 0);
|
|
40352
|
+
return () => {
|
|
40353
|
+
void threads.close();
|
|
40354
|
+
void data.close();
|
|
40355
|
+
};
|
|
40356
|
+
}
|
|
40357
|
+
function startPhoneBoardSync(opts) {
|
|
40358
|
+
const list = opts.list ?? listPhoneSidebar;
|
|
40359
|
+
const debounceMs = opts.debounceMs ?? PHONE_BOARD_SYNC_MS;
|
|
40360
|
+
let timer = null;
|
|
40361
|
+
let lastKey = "";
|
|
40362
|
+
let stopped = false;
|
|
40363
|
+
const push = () => {
|
|
40364
|
+
timer = null;
|
|
40365
|
+
if (stopped) return;
|
|
40366
|
+
let sidebar;
|
|
40367
|
+
try {
|
|
40368
|
+
sidebar = list();
|
|
40369
|
+
} catch {
|
|
40370
|
+
return;
|
|
40371
|
+
}
|
|
40372
|
+
const key = phoneBoardKey(sidebar);
|
|
40373
|
+
if (key === lastKey) return;
|
|
40374
|
+
lastKey = key;
|
|
40375
|
+
opts.onSidebar(sidebar);
|
|
40376
|
+
};
|
|
40377
|
+
const schedule = () => {
|
|
40378
|
+
if (stopped) return;
|
|
40379
|
+
if (timer) clearTimeout(timer);
|
|
40380
|
+
timer = setTimeout(push, debounceMs);
|
|
40381
|
+
};
|
|
40382
|
+
const handle = (event) => {
|
|
40383
|
+
if (isPhoneBoardEvent(event)) schedule();
|
|
40384
|
+
if (!isRunningChat(event)) return;
|
|
40385
|
+
const open = opts.openChatId?.();
|
|
40386
|
+
if (open && open === event.threadId) opts.onRunningChat?.(event.threadId);
|
|
40387
|
+
};
|
|
40388
|
+
const unsubscribe = (opts.subscribe ?? subscribePhoneBoard)(schedule);
|
|
40389
|
+
const offEvents = (opts.events ?? ((listener) => getOrchestrator().on(listener)))(handle);
|
|
40390
|
+
push();
|
|
40391
|
+
return () => {
|
|
40392
|
+
stopped = true;
|
|
40393
|
+
if (timer) clearTimeout(timer);
|
|
40394
|
+
timer = null;
|
|
40395
|
+
unsubscribe();
|
|
40396
|
+
offEvents();
|
|
40397
|
+
};
|
|
40398
|
+
}
|
|
40399
|
+
|
|
40082
40400
|
// src/remote/client.ts
|
|
40083
40401
|
var BACKOFF_START_MS3 = 1e3;
|
|
40084
40402
|
var BACKOFF_MAX_MS3 = 3e4;
|
|
@@ -40125,6 +40443,24 @@ function runRemoteHost(opts) {
|
|
|
40125
40443
|
let wantPair = false;
|
|
40126
40444
|
let deviceLabel = opts.deviceLabel;
|
|
40127
40445
|
let keepalive = null;
|
|
40446
|
+
let connectionGen = 0;
|
|
40447
|
+
let stopBoard = null;
|
|
40448
|
+
const stopBoardSync = () => {
|
|
40449
|
+
stopBoard?.();
|
|
40450
|
+
stopBoard = null;
|
|
40451
|
+
};
|
|
40452
|
+
const startBoardSync = () => {
|
|
40453
|
+
stopBoardSync();
|
|
40454
|
+
stopBoard = startPhoneBoardSync({
|
|
40455
|
+
openChatId: getPhoneOpenChat,
|
|
40456
|
+
onSidebar: (sidebar) => {
|
|
40457
|
+
emitControl({ onOutbound: emit }, { op: "sidebar", ...sidebar });
|
|
40458
|
+
},
|
|
40459
|
+
onRunningChat: (chatId) => {
|
|
40460
|
+
deliverPhoneTurn({ onOutbound: emit }, chatId);
|
|
40461
|
+
}
|
|
40462
|
+
});
|
|
40463
|
+
};
|
|
40128
40464
|
const send3 = (msg) => {
|
|
40129
40465
|
if (!socket || socket.readyState !== socket.OPEN) return;
|
|
40130
40466
|
socket.send(JSON.stringify(msg));
|
|
@@ -40161,12 +40497,19 @@ function runRemoteHost(opts) {
|
|
|
40161
40497
|
}
|
|
40162
40498
|
};
|
|
40163
40499
|
const connectOnce = () => new Promise((resolve2, reject) => {
|
|
40500
|
+
const gen = ++connectionGen;
|
|
40501
|
+
stopBoardSync();
|
|
40502
|
+
setPhoneOpenChat(null);
|
|
40164
40503
|
const ws = new import_ws5.WebSocket(opts.url, { lookup: lookupPreferPublicDns });
|
|
40165
40504
|
socket = ws;
|
|
40166
40505
|
let settled = false;
|
|
40167
40506
|
const finish = (err) => {
|
|
40168
40507
|
if (settled) return;
|
|
40169
40508
|
settled = true;
|
|
40509
|
+
if (gen === connectionGen) {
|
|
40510
|
+
stopBoardSync();
|
|
40511
|
+
setPhoneOpenChat(null);
|
|
40512
|
+
}
|
|
40170
40513
|
keepalive?.stop();
|
|
40171
40514
|
keepalive = null;
|
|
40172
40515
|
opts.signal?.removeEventListener("abort", onAbort);
|
|
@@ -40204,6 +40547,13 @@ function runRemoteHost(opts) {
|
|
|
40204
40547
|
const raw = typeof data === "string" ? data : data.toString("utf8");
|
|
40205
40548
|
const msg = parseRemoteServerMessage(raw);
|
|
40206
40549
|
if (!msg) return;
|
|
40550
|
+
if (gen === connectionGen) {
|
|
40551
|
+
if (msg.type === "phone_joined") startBoardSync();
|
|
40552
|
+
if (msg.type === "phone_left") {
|
|
40553
|
+
setPhoneOpenChat(null);
|
|
40554
|
+
stopBoardSync();
|
|
40555
|
+
}
|
|
40556
|
+
}
|
|
40207
40557
|
handleServer(msg);
|
|
40208
40558
|
});
|
|
40209
40559
|
ws.on("close", () => finish());
|
|
@@ -40289,12 +40639,12 @@ var import_node_crypto27 = require("crypto");
|
|
|
40289
40639
|
var import_node_crypto26 = require("crypto");
|
|
40290
40640
|
var import_node_fs70 = require("fs");
|
|
40291
40641
|
var import_node_module7 = require("module");
|
|
40292
|
-
var
|
|
40642
|
+
var import_node_path69 = require("path");
|
|
40293
40643
|
var GITHUB_ACCOUNT_ISSUER = "https://github.com";
|
|
40294
40644
|
var RELAY_DEVICE_CAP = 3;
|
|
40295
40645
|
function thisModuleFile3() {
|
|
40296
40646
|
const cjsFile = typeof __filename !== "undefined" ? __filename : "";
|
|
40297
|
-
return cjsFile || process.argv[1] || (0,
|
|
40647
|
+
return cjsFile || process.argv[1] || (0, import_node_path69.join)(process.cwd(), "package.json");
|
|
40298
40648
|
}
|
|
40299
40649
|
function openDatabase(filePath) {
|
|
40300
40650
|
const req = (0, import_node_module7.createRequire)(thisModuleFile3());
|
|
@@ -40331,7 +40681,7 @@ function assertRelayAccountsDurable(filePath) {
|
|
|
40331
40681
|
if (process.env.SIDEBOARD_RELAY_ACCOUNTS_EPHEMERAL === "1") return;
|
|
40332
40682
|
if (process.env.NODE_ENV !== "production") return;
|
|
40333
40683
|
if (process.platform !== "linux") return;
|
|
40334
|
-
const dir = (0, import_node_fs70.realpathSync)((0,
|
|
40684
|
+
const dir = (0, import_node_fs70.realpathSync)((0, import_node_path69.dirname)(filePath));
|
|
40335
40685
|
const mounts = (0, import_node_fs70.readFileSync)("/proc/mounts", "utf8");
|
|
40336
40686
|
const point = longestMountPoint(dir, mounts);
|
|
40337
40687
|
if (point === "/") {
|
|
@@ -40343,7 +40693,7 @@ function assertRelayAccountsDurable(filePath) {
|
|
|
40343
40693
|
var RelayAccountStore = class {
|
|
40344
40694
|
db;
|
|
40345
40695
|
constructor(filePath) {
|
|
40346
|
-
(0, import_node_fs70.mkdirSync)((0,
|
|
40696
|
+
(0, import_node_fs70.mkdirSync)((0, import_node_path69.dirname)(filePath), { recursive: true });
|
|
40347
40697
|
this.db = openDatabase(filePath);
|
|
40348
40698
|
this.db.exec(`
|
|
40349
40699
|
CREATE TABLE IF NOT EXISTS accounts (
|
|
@@ -41330,7 +41680,7 @@ var import_ws6 = require("ws");
|
|
|
41330
41680
|
// src/relay/static.ts
|
|
41331
41681
|
var import_node_fs71 = require("fs");
|
|
41332
41682
|
var import_promises = require("fs/promises");
|
|
41333
|
-
var
|
|
41683
|
+
var import_node_path70 = __toESM(require("path"), 1);
|
|
41334
41684
|
var TYPES = {
|
|
41335
41685
|
".css": "text/css; charset=utf-8",
|
|
41336
41686
|
".html": "text/html; charset=utf-8",
|
|
@@ -41361,9 +41711,9 @@ function resolveStaticPath(root, requestUrl) {
|
|
|
41361
41711
|
return null;
|
|
41362
41712
|
}
|
|
41363
41713
|
if (!pathname.startsWith("/") || pathname.includes("\0")) return null;
|
|
41364
|
-
const rootResolved =
|
|
41365
|
-
const candidate =
|
|
41366
|
-
if (candidate !== rootResolved && !candidate.startsWith(rootResolved +
|
|
41714
|
+
const rootResolved = import_node_path70.default.resolve(root);
|
|
41715
|
+
const candidate = import_node_path70.default.resolve(rootResolved, `.${pathname}`);
|
|
41716
|
+
if (candidate !== rootResolved && !candidate.startsWith(rootResolved + import_node_path70.default.sep)) {
|
|
41367
41717
|
return null;
|
|
41368
41718
|
}
|
|
41369
41719
|
return candidate;
|
|
@@ -41377,7 +41727,7 @@ async function fileSize(file) {
|
|
|
41377
41727
|
}
|
|
41378
41728
|
}
|
|
41379
41729
|
function sendFile(req, res, file, size) {
|
|
41380
|
-
const ext =
|
|
41730
|
+
const ext = import_node_path70.default.extname(file).toLowerCase();
|
|
41381
41731
|
res.writeHead(200, {
|
|
41382
41732
|
"Content-Type": TYPES[ext] ?? "application/octet-stream",
|
|
41383
41733
|
"Content-Length": size,
|
|
@@ -41396,7 +41746,7 @@ async function tryServeStatic(req, res, root) {
|
|
|
41396
41746
|
if (!candidate) return false;
|
|
41397
41747
|
const direct = await fileSize(candidate);
|
|
41398
41748
|
if (direct != null) return sendFile(req, res, candidate, direct);
|
|
41399
|
-
const asIndex =
|
|
41749
|
+
const asIndex = import_node_path70.default.join(candidate, "index.html");
|
|
41400
41750
|
const indexSize = await fileSize(asIndex);
|
|
41401
41751
|
if (indexSize != null) return sendFile(req, res, asIndex, indexSize);
|
|
41402
41752
|
return false;
|
|
@@ -41758,7 +42108,7 @@ async function startRelayServer(opts) {
|
|
|
41758
42108
|
|
|
41759
42109
|
// src/slack/outbound-watch.ts
|
|
41760
42110
|
var import_node_fs72 = require("fs");
|
|
41761
|
-
var
|
|
42111
|
+
var import_node_path71 = require("path");
|
|
41762
42112
|
init_paths();
|
|
41763
42113
|
init_private_file();
|
|
41764
42114
|
init_secure_file();
|
|
@@ -41771,7 +42121,7 @@ var POLL_INTERVAL_MS = 12e3;
|
|
|
41771
42121
|
var lastPollMs = 0;
|
|
41772
42122
|
var nameCache = /* @__PURE__ */ new Map();
|
|
41773
42123
|
function storePath4() {
|
|
41774
|
-
return (0,
|
|
42124
|
+
return (0, import_node_path71.join)(appDataDir(), "slack-outbound-watch.json");
|
|
41775
42125
|
}
|
|
41776
42126
|
function watchId(teamId, channelId, ts) {
|
|
41777
42127
|
return `${teamId}:${channelId}:${ts}`;
|
|
@@ -41852,10 +42202,10 @@ function formatOwnerSlackFyi(userName, text6) {
|
|
|
41852
42202
|
return `${who} replied in Slack:
|
|
41853
42203
|
${body}`;
|
|
41854
42204
|
}
|
|
41855
|
-
function sameSlackConversation(
|
|
41856
|
-
if (
|
|
41857
|
-
const targetThread = target.threadTs?.trim() ||
|
|
41858
|
-
return targetThread ===
|
|
42205
|
+
function sameSlackConversation(watch2, target) {
|
|
42206
|
+
if (watch2.channelId !== target.channelId) return false;
|
|
42207
|
+
const targetThread = target.threadTs?.trim() || watch2.threadTs;
|
|
42208
|
+
return targetThread === watch2.threadTs;
|
|
41859
42209
|
}
|
|
41860
42210
|
async function relayExternalReply(opts) {
|
|
41861
42211
|
const threadId = opts.watch.sourceThreadId?.trim();
|
|
@@ -41931,15 +42281,15 @@ function recordSlackOutboundWatch(input) {
|
|
|
41931
42281
|
writeStore3(pruneWatches(watches));
|
|
41932
42282
|
return next;
|
|
41933
42283
|
}
|
|
41934
|
-
function isHumanReply(msg,
|
|
42284
|
+
function isHumanReply(msg, watch2) {
|
|
41935
42285
|
const ts = msg.ts?.trim();
|
|
41936
|
-
if (!ts || ts ===
|
|
41937
|
-
if (!tsNewer(ts,
|
|
42286
|
+
if (!ts || ts === watch2.ts) return false;
|
|
42287
|
+
if (!tsNewer(ts, watch2.lastSeenTs)) return false;
|
|
41938
42288
|
if (msg.bot_id) return false;
|
|
41939
42289
|
if (msg.subtype) return false;
|
|
41940
42290
|
const user = msg.user?.trim();
|
|
41941
42291
|
if (!user) return false;
|
|
41942
|
-
if (
|
|
42292
|
+
if (watch2.ownerUserId && user === watch2.ownerUserId) return false;
|
|
41943
42293
|
return true;
|
|
41944
42294
|
}
|
|
41945
42295
|
function displayName(user) {
|
|
@@ -41971,16 +42321,16 @@ async function resolvePermalink(token, channelId, ts, fetchImpl) {
|
|
|
41971
42321
|
}
|
|
41972
42322
|
return slackArchiveUrl(channelId, ts);
|
|
41973
42323
|
}
|
|
41974
|
-
async function fetchMessages(token,
|
|
42324
|
+
async function fetchMessages(token, watch2, fetchImpl) {
|
|
41975
42325
|
const out = [];
|
|
41976
42326
|
try {
|
|
41977
42327
|
const data = await slackApi(
|
|
41978
42328
|
token,
|
|
41979
42329
|
"conversations.replies",
|
|
41980
42330
|
{
|
|
41981
|
-
channel:
|
|
41982
|
-
ts:
|
|
41983
|
-
oldest:
|
|
42331
|
+
channel: watch2.channelId,
|
|
42332
|
+
ts: watch2.threadTs,
|
|
42333
|
+
oldest: watch2.lastSeenTs,
|
|
41984
42334
|
inclusive: false,
|
|
41985
42335
|
limit: 50
|
|
41986
42336
|
},
|
|
@@ -41989,14 +42339,14 @@ async function fetchMessages(token, watch, fetchImpl) {
|
|
|
41989
42339
|
out.push(...data.messages ?? []);
|
|
41990
42340
|
} catch {
|
|
41991
42341
|
}
|
|
41992
|
-
if (
|
|
42342
|
+
if (watch2.kind === "dm" || watch2.channelId.startsWith("D")) {
|
|
41993
42343
|
try {
|
|
41994
42344
|
const data = await slackApi(
|
|
41995
42345
|
token,
|
|
41996
42346
|
"conversations.history",
|
|
41997
42347
|
{
|
|
41998
|
-
channel:
|
|
41999
|
-
oldest:
|
|
42348
|
+
channel: watch2.channelId,
|
|
42349
|
+
oldest: watch2.lastSeenTs,
|
|
42000
42350
|
inclusive: false,
|
|
42001
42351
|
limit: 50
|
|
42002
42352
|
},
|
|
@@ -42016,8 +42366,8 @@ async function pollSlackOutboundWatches(opts) {
|
|
|
42016
42366
|
let watches = pruneWatches(existing, now);
|
|
42017
42367
|
let changed = watches.length !== existing.length;
|
|
42018
42368
|
for (let i = 0; i < watches.length; i++) {
|
|
42019
|
-
const
|
|
42020
|
-
const ws = getSlackWorkspace(
|
|
42369
|
+
const watch2 = watches[i];
|
|
42370
|
+
const ws = getSlackWorkspace(watch2.teamId);
|
|
42021
42371
|
if (!ws) continue;
|
|
42022
42372
|
let token;
|
|
42023
42373
|
try {
|
|
@@ -42025,19 +42375,19 @@ async function pollSlackOutboundWatches(opts) {
|
|
|
42025
42375
|
} catch {
|
|
42026
42376
|
continue;
|
|
42027
42377
|
}
|
|
42028
|
-
const messages = await fetchMessages(token,
|
|
42029
|
-
const replies = messages.filter((m) => isHumanReply(m,
|
|
42378
|
+
const messages = await fetchMessages(token, watch2, opts?.fetchImpl);
|
|
42379
|
+
const replies = messages.filter((m) => isHumanReply(m, watch2)).sort((a, b) => Number(a.ts) - Number(b.ts));
|
|
42030
42380
|
if (replies.length === 0) continue;
|
|
42031
|
-
const injected2 = new Set(
|
|
42032
|
-
const collected = [...
|
|
42033
|
-
let lastSeenTs =
|
|
42381
|
+
const injected2 = new Set(watch2.injectedReplyTs ?? []);
|
|
42382
|
+
const collected = [...watch2.replies ?? []];
|
|
42383
|
+
let lastSeenTs = watch2.lastSeenTs;
|
|
42034
42384
|
let latestName;
|
|
42035
|
-
let latestPermalink =
|
|
42385
|
+
let latestPermalink = watch2.permalink;
|
|
42036
42386
|
let newlyInjected = 0;
|
|
42037
42387
|
for (const msg of replies) {
|
|
42038
42388
|
const ts = msg.ts.trim();
|
|
42039
42389
|
const user = msg.user.trim();
|
|
42040
|
-
const fallback =
|
|
42390
|
+
const fallback = watch2.toUserId === user ? watch2.toLabel : user;
|
|
42041
42391
|
const replyUserName = await resolveUserName(
|
|
42042
42392
|
token,
|
|
42043
42393
|
user,
|
|
@@ -42046,7 +42396,7 @@ async function pollSlackOutboundWatches(opts) {
|
|
|
42046
42396
|
);
|
|
42047
42397
|
const permalink = await resolvePermalink(
|
|
42048
42398
|
token,
|
|
42049
|
-
|
|
42399
|
+
watch2.channelId,
|
|
42050
42400
|
ts,
|
|
42051
42401
|
opts?.fetchImpl
|
|
42052
42402
|
);
|
|
@@ -42064,7 +42414,7 @@ async function pollSlackOutboundWatches(opts) {
|
|
|
42064
42414
|
continue;
|
|
42065
42415
|
}
|
|
42066
42416
|
const delivered = await relayExternalReply({
|
|
42067
|
-
watch,
|
|
42417
|
+
watch: watch2,
|
|
42068
42418
|
reply,
|
|
42069
42419
|
permalink,
|
|
42070
42420
|
fetchImpl: opts?.fetchImpl
|
|
@@ -42074,19 +42424,19 @@ async function pollSlackOutboundWatches(opts) {
|
|
|
42074
42424
|
injected2.add(ts);
|
|
42075
42425
|
lastSeenTs = ts;
|
|
42076
42426
|
}
|
|
42077
|
-
if (newlyInjected > 0 &&
|
|
42427
|
+
if (newlyInjected > 0 && watch2.sourceThreadId?.trim()) {
|
|
42078
42428
|
await continueSourceThread(
|
|
42079
|
-
|
|
42429
|
+
watch2.sourceThreadId.trim(),
|
|
42080
42430
|
formatSlackReplyContinuePrompt({
|
|
42081
|
-
userName: latestName ||
|
|
42082
|
-
kind:
|
|
42083
|
-
toLabel:
|
|
42431
|
+
userName: latestName || watch2.toLabel,
|
|
42432
|
+
kind: watch2.kind,
|
|
42433
|
+
toLabel: watch2.toLabel,
|
|
42084
42434
|
count: newlyInjected
|
|
42085
42435
|
})
|
|
42086
42436
|
);
|
|
42087
42437
|
}
|
|
42088
42438
|
watches[i] = {
|
|
42089
|
-
...
|
|
42439
|
+
...watch2,
|
|
42090
42440
|
lastSeenTs,
|
|
42091
42441
|
permalink: latestPermalink,
|
|
42092
42442
|
injectedReplyTs: [...injected2],
|
|
@@ -43117,6 +43467,7 @@ async function pollSlackOutboundWatches(opts) {
|
|
|
43117
43467
|
validateLinearApiKey,
|
|
43118
43468
|
verifyAbleTimeConnection,
|
|
43119
43469
|
verifyOptionalService,
|
|
43470
|
+
visibleAgentBlock,
|
|
43120
43471
|
visibleAssistantText,
|
|
43121
43472
|
visibleToolRowDetail,
|
|
43122
43473
|
waitForPidExit,
|
|
@@ -43131,7 +43482,9 @@ async function pollSlackOutboundWatches(opts) {
|
|
|
43131
43482
|
withSelectedAccount,
|
|
43132
43483
|
withThreadLock,
|
|
43133
43484
|
withTurnScopedCost,
|
|
43485
|
+
workspaceAgentChatSummaries,
|
|
43134
43486
|
workspaceSettingsSourceLabel,
|
|
43487
|
+
worktreeAgentBlock,
|
|
43135
43488
|
worktreeBoardStatus,
|
|
43136
43489
|
worktreeCleanupSettings,
|
|
43137
43490
|
worktreeDisplayLabel,
|