@sideboard-ai/core 0.1.190 → 0.1.192
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{abletime-J3EGHIAV.js → abletime-53T7G667.js} +3 -1
- package/dist/{abletime-CAFQ4GEG.js → abletime-JFMBCSIQ.js} +3 -1
- package/dist/{agents-5OBZETUV.js → agents-T5VBB6X3.js} +2 -2
- package/dist/{agents-DTPVTDB5.js → agents-TXGQIK2E.js} +2 -2
- package/dist/{chunk-TEJU6UJY.js → chunk-4M2AK3O5.js} +126 -20
- package/dist/{chunk-XXEIF2IL.js → chunk-7NSXGL6L.js} +3 -3
- package/dist/{chunk-IAOXW6RI.js → chunk-FZ3G5TXM.js} +23 -16
- package/dist/{chunk-Z6TW3A5P.js → chunk-NF6ENGHS.js} +24 -17
- package/dist/{chunk-OLDZXO53.js → chunk-TWSCTJCR.js} +2 -2
- package/dist/{chunk-KEVY4GEK.js → chunk-UBQIELFS.js} +123 -20
- package/dist/{chunk-VIODP5PT.js → chunk-VV7UT3E2.js} +3 -3
- package/dist/{chunk-GBWKMWUQ.js → chunk-WHD4JNTN.js} +2 -2
- package/dist/{coordinator-prompt-LWKOL3AZ.js → coordinator-prompt-PNUDN4DW.js} +1 -1
- package/dist/{coordinator-prompt-MSC44OOM.js → coordinator-prompt-S4HHFSLI.js} +1 -1
- package/dist/{global-workspace-6G5JJH2E.js → global-workspace-2BVCJUAL.js} +1 -1
- package/dist/{global-workspace-XATJTV3N.js → global-workspace-JTBBH2K5.js} +1 -1
- package/dist/index.cjs +565 -57
- package/dist/index.d.cts +96 -6
- package/dist/index.d.ts +96 -6
- package/dist/index.js +422 -30
- package/dist/mcp/run-stdio.cjs +543 -57
- package/dist/mcp/run-stdio.js +407 -30
- package/dist/{orchestrator-BHJELEFM.js → orchestrator-4G76BOHH.js} +5 -3
- package/dist/{orchestrator-HWNGCDGW.js → orchestrator-LWEZ74RK.js} +5 -3
- package/dist/{workspaces-LMFCRXTT.js → workspaces-OY7D5RPU.js} +1 -1
- package/dist/{workspaces-FWKPWSXV.js → workspaces-ZYPME3YD.js} +1 -1
- package/package.json +1 -1
package/dist/mcp/run-stdio.cjs
CHANGED
|
@@ -7973,8 +7973,8 @@ var init_coordinator_prompt = __esm({
|
|
|
7973
7973
|
"- start_board_card \u2014 same as create_thread for a ticket/PR/named branch (attaches issue text when resolvable). Then send_to_thread.",
|
|
7974
7974
|
"- fork_worktree \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.",
|
|
7975
7975
|
"- fork_chat \u2014 fork a worktree chat (same worktree tab) OR a Global orchestration chat (new orchestration tab); optional agent; leave model unset (Auto) unless you have a reason. Remote coordinators: use this to continue another orchestration chat on a different agent after session limits.",
|
|
7976
|
-
"- send_to_thread \u2014
|
|
7977
|
-
"- 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). wait_for_turn returns within ~45s even if the child is still working (MCP clients kill longer tool calls). stillRunning is the source of truth \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 is true, progress is a live snapshot of tools/thinking \u2014 call wait_for_turn again. status=queued with no lastActivityAt means the child has not started yet (concurrency cap) \u2014 keep waiting; do not force_stop or
|
|
7976
|
+
"- send_to_thread \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)",
|
|
7977
|
+
"- 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). wait_for_turn returns within ~45s even if the child is still working (MCP clients kill longer tool calls). stillRunning is the source of truth \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 is true, progress is a live snapshot of tools/thinking \u2014 call wait_for_turn again. status=queued with no lastActivityAt means the child has not started yet (concurrency cap) \u2014 keep waiting; do not force_stop or send_to_thread a check-in (that steers / interrupts). Do not send \u201Care you stuck?\u201D or assume a hang while lastActivityAt is recent. If stillRunning stays true across many waits with the same lastActivityAt, the child is stuck \u2014 tell the user; do not invent a gate. On status error, lastError (and text) is the failure \u2014 switch agent, tell the user, or retry; do not treat empty text as success. On status stopped or broken (or incomplete=true), the child was interrupted or died \u2014 resume with send_to_thread or tell the user; never treat stopped as a finished turn. Sideboard also injects a notice into this chat when a child stops unexpectedly.",
|
|
7978
7978
|
"- stop_thread \u2014 force-stop: kill in-flight turn AND clear queued prompts (do not leave stale queue after an interrupt)",
|
|
7979
7979
|
"- archive_thread / restore_thread \u2014 archive (tears down worktree when last tab) or restore",
|
|
7980
7980
|
"Setup / run:",
|
|
@@ -7984,7 +7984,7 @@ var init_coordinator_prompt = __esm({
|
|
|
7984
7984
|
"- get_diff \u2014 compact diff summary",
|
|
7985
7985
|
"- get_pr_checks \u2014 snapshot of a worktree thread's PR checks (null = no PR). Use this to inspect status. If the user gave a goal, the worktree agent watches with `gh pr checks --watch` \u2014 do not poll for the human.",
|
|
7986
7986
|
'- request_review \u2014 open a Review chat tab on a worktree thread (attaches .claude/skills/review/SKILL.md when present, else .context/review.md copied from .sideboard/review.md / stock; sends "Review changes in this workspace."); then wait_for_turn (loop while stillRunning) / get_turn_result on the returned id. The review stays in that chat so they can read it and type next steps \u2014 do not tell the child to comment or update the PR or ticket.',
|
|
7987
|
-
"- ask_git \u2014 commit & push, open a draft PR, mark ready for review, resolve conflicts, or merge. When the worktree is clean, Sideboard pushes / opens the PR itself. When dirty, it
|
|
7987
|
+
"- ask_git \u2014 commit & push, open a draft PR, mark ready for review, resolve conflicts, or merge. When the worktree is clean, Sideboard pushes / opens the PR itself. When dirty, it steers the worktree agent \u2014 then wait_for_turn (loop while stillRunning). Prefer this over paraphrasing. If ask_git / get_thread lastError says the GraphQL/PR body is too long, the branch is already pushed \u2014 send_to_thread so the worktree agent runs `gh pr create --body-file` with a short description. Do not invent SSH or auth failures from that error. If the user gave a goal (Greptile 5/5, CI green), send_to_thread that goal so the worktree agent enters the watch-fix-push loop \u2014 do not tell the human to poll.",
|
|
7988
7988
|
'- Merge (`ask_git` action=merge / send_to_thread "Merge PR.") only when the user explicitly asked to merge that PR. Do not merge because the work looks done, CI is green, or a typical flow includes it.',
|
|
7989
7989
|
'- Or send_to_thread with those exact phrases: "Commit and push.", "Commit, push, and open a draft PR.", "Ready for review.", "Merge the remote branch (main) into your branch and resolve conflicts. Then, commit and push your changes.", "Merge PR." (draft PRs: `gh pr create --draft -R <origin-owner/name>` using the workspace `github:` slug \u2014 never upstream). Never run git/gh from this orchestration cwd, and never merge the PR yourself.',
|
|
7990
7990
|
"Process guides:",
|
|
@@ -14303,8 +14303,8 @@ var init_wait_for_turn = __esm({
|
|
|
14303
14303
|
"src/mcp/wait-for-turn.ts"() {
|
|
14304
14304
|
"use strict";
|
|
14305
14305
|
MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
|
|
14306
|
-
MCP_WAIT_STILL_RUNNING_HINT = "Child is still working. Call wait_for_turn again. Do not
|
|
14307
|
-
MCP_WAIT_QUEUED_HINT = "Child is queued waiting for a concurrency slot \u2014 it has not started yet. Call wait_for_turn again. Do not
|
|
14306
|
+
MCP_WAIT_STILL_RUNNING_HINT = "Child is still working. Call wait_for_turn again. Do not send_to_thread a check-in (that steers / interrupts) or assume a hang while progress is updating.";
|
|
14307
|
+
MCP_WAIT_QUEUED_HINT = "Child is queued waiting for a concurrency slot \u2014 it has not started yet. Call wait_for_turn again. Do not send_to_thread a check-in (that steers / interrupts), force_stop, or assume it failed to start.";
|
|
14308
14308
|
MCP_WAIT_STOPPED_HINT = "Child was stopped before the turn finished. Do not treat this as success. send_to_thread to resume, or tell the user.";
|
|
14309
14309
|
MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
|
|
14310
14310
|
MCP_WAIT_ERROR_HINT = "Child turn failed. lastError/text is the failure \u2014 switch agent, tell the user, or retry. Do not treat empty text as success.";
|
|
@@ -15193,13 +15193,17 @@ var init_home_board = __esm({
|
|
|
15193
15193
|
});
|
|
15194
15194
|
|
|
15195
15195
|
// src/integrations/abletime-mcp.ts
|
|
15196
|
+
function abletimeToolUsesPm(name) {
|
|
15197
|
+
return ABLETIME_PM_TOOL_NAMES.includes(name);
|
|
15198
|
+
}
|
|
15196
15199
|
function normalizeAbleTimeHost(raw) {
|
|
15197
15200
|
const trimmed = raw?.trim() || DEFAULT_ABLETIME_HOST;
|
|
15198
15201
|
const withScheme = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
15199
15202
|
return withScheme.replace(/\/+$/, "");
|
|
15200
15203
|
}
|
|
15201
|
-
function abletimeMcpUrl(host) {
|
|
15202
|
-
|
|
15204
|
+
function abletimeMcpUrl(host, opts) {
|
|
15205
|
+
const path = opts?.pm ? ABLETIME_MCP_PM_PATH : ABLETIME_MCP_PATH;
|
|
15206
|
+
return `${normalizeAbleTimeHost(host)}${path}`;
|
|
15203
15207
|
}
|
|
15204
15208
|
function rewriteAbleTimeError(message) {
|
|
15205
15209
|
if (/INTEGRATION_AGENT_ACCESS_DISABLED|agent access/i.test(message)) {
|
|
@@ -15277,7 +15281,8 @@ async function abletimeMcpRequest(method, params, opts) {
|
|
|
15277
15281
|
throw new Error("AbleTime is not connected \u2014 paste a personal access token in Account settings");
|
|
15278
15282
|
}
|
|
15279
15283
|
const url = abletimeMcpUrl(
|
|
15280
|
-
opts?.host ?? (opts?.token ? DEFAULT_ABLETIME_HOST : getAbleTimeHost())
|
|
15284
|
+
opts?.host ?? (opts?.token ? DEFAULT_ABLETIME_HOST : getAbleTimeHost()),
|
|
15285
|
+
{ pm: opts?.pm }
|
|
15281
15286
|
);
|
|
15282
15287
|
const res = await httpFetch(url, {
|
|
15283
15288
|
method: "POST",
|
|
@@ -15341,7 +15346,8 @@ async function callAbleTimeTool(name, args = {}, opts) {
|
|
|
15341
15346
|
if (!opts?.token && !isAbleTimeConnected()) {
|
|
15342
15347
|
throw new Error("AbleTime is not connected \u2014 paste a personal access token in Account settings");
|
|
15343
15348
|
}
|
|
15344
|
-
const
|
|
15349
|
+
const requestOpts = { ...opts, pm: opts?.pm ?? abletimeToolUsesPm(name) };
|
|
15350
|
+
const call = () => abletimeMcpRequest("tools/call", { name, arguments: args }, requestOpts).then((result) => {
|
|
15345
15351
|
return unwrapToolResult(result);
|
|
15346
15352
|
});
|
|
15347
15353
|
try {
|
|
@@ -15349,13 +15355,13 @@ async function callAbleTimeTool(name, args = {}, opts) {
|
|
|
15349
15355
|
} catch (err) {
|
|
15350
15356
|
const message = err instanceof Error ? err.message : String(err);
|
|
15351
15357
|
if (/initialize|session|not initialized|-32000/i.test(message)) {
|
|
15352
|
-
await initializeIfNeeded(
|
|
15358
|
+
await initializeIfNeeded(requestOpts);
|
|
15353
15359
|
return await call();
|
|
15354
15360
|
}
|
|
15355
15361
|
throw err instanceof Error ? new Error(rewriteAbleTimeError(err.message)) : err;
|
|
15356
15362
|
}
|
|
15357
15363
|
}
|
|
15358
|
-
var DEFAULT_ABLETIME_HOST, ABLETIME_MCP_PATH, MCP_PROTOCOL_VERSION;
|
|
15364
|
+
var DEFAULT_ABLETIME_HOST, ABLETIME_MCP_PATH, ABLETIME_MCP_PM_PATH, MCP_PROTOCOL_VERSION, ABLETIME_PM_TOOL_NAMES;
|
|
15359
15365
|
var init_abletime_mcp = __esm({
|
|
15360
15366
|
"src/integrations/abletime-mcp.ts"() {
|
|
15361
15367
|
"use strict";
|
|
@@ -15363,13 +15369,27 @@ var init_abletime_mcp = __esm({
|
|
|
15363
15369
|
init_app_settings();
|
|
15364
15370
|
DEFAULT_ABLETIME_HOST = "https://track.abletime.com";
|
|
15365
15371
|
ABLETIME_MCP_PATH = "/api/public/v2/mcp";
|
|
15372
|
+
ABLETIME_MCP_PM_PATH = "/api/public/v2/mcp/pm";
|
|
15366
15373
|
MCP_PROTOCOL_VERSION = "2025-03-26";
|
|
15374
|
+
ABLETIME_PM_TOOL_NAMES = [
|
|
15375
|
+
"set_task_assignee",
|
|
15376
|
+
"set_task_priority",
|
|
15377
|
+
"set_task_stage",
|
|
15378
|
+
"set_task_dependency",
|
|
15379
|
+
"set_task_blocked",
|
|
15380
|
+
"set_task_locked",
|
|
15381
|
+
"set_task_schedule",
|
|
15382
|
+
"clear_task_schedule",
|
|
15383
|
+
"archive_task",
|
|
15384
|
+
"delete_task"
|
|
15385
|
+
];
|
|
15367
15386
|
}
|
|
15368
15387
|
});
|
|
15369
15388
|
|
|
15370
15389
|
// src/integrations/abletime.ts
|
|
15371
15390
|
var abletime_exports = {};
|
|
15372
15391
|
__export(abletime_exports, {
|
|
15392
|
+
ableTimeDependencyTarget: () => ableTimeDependencyTarget,
|
|
15373
15393
|
commentAbleTimeTask: () => commentAbleTimeTask,
|
|
15374
15394
|
createAbleTimeTask: () => createAbleTimeTask,
|
|
15375
15395
|
ensureAbleTimeTask: () => ensureAbleTimeTask,
|
|
@@ -15626,39 +15646,122 @@ async function commentAbleTimeTask(input, opts) {
|
|
|
15626
15646
|
body: rec ? firstString(rec, ["body", "text", "comment"]) || body : body
|
|
15627
15647
|
};
|
|
15628
15648
|
}
|
|
15649
|
+
function isAbleTimeNoneToken(value) {
|
|
15650
|
+
if (value == null) return true;
|
|
15651
|
+
const s = value.trim();
|
|
15652
|
+
return !s || /^(none|clear|unschedule|null)$/i.test(s);
|
|
15653
|
+
}
|
|
15654
|
+
function taskIdArgs(id) {
|
|
15655
|
+
return { id, task_id: id, task: id };
|
|
15656
|
+
}
|
|
15657
|
+
function ableTimeDependencyTarget(thisId, type, relatedId) {
|
|
15658
|
+
const s = type.trim().toLowerCase().replace(/[\s_-]+/g, "");
|
|
15659
|
+
if (s === "none" || s === "clear" || s === "null") {
|
|
15660
|
+
return { taskId: thisId, dependsOn: null };
|
|
15661
|
+
}
|
|
15662
|
+
if (s === "blockedby" || s === "blocked" || s === "dependson" || s === "waiton") {
|
|
15663
|
+
return { taskId: thisId, dependsOn: relatedId };
|
|
15664
|
+
}
|
|
15665
|
+
if (s === "blocks" || s === "blocking") {
|
|
15666
|
+
return { taskId: relatedId, dependsOn: thisId };
|
|
15667
|
+
}
|
|
15668
|
+
throw new Error(
|
|
15669
|
+
`AbleTime relation type must be blocks or blockedBy (got ${type}). AbleTime models one wait-on dependency.`
|
|
15670
|
+
);
|
|
15671
|
+
}
|
|
15672
|
+
async function setAbleTimeTaskDependency(taskId, dependsOn, opts) {
|
|
15673
|
+
const args = {
|
|
15674
|
+
...taskIdArgs(taskId)
|
|
15675
|
+
};
|
|
15676
|
+
if (dependsOn) {
|
|
15677
|
+
args.depends_on = dependsOn;
|
|
15678
|
+
args.depends_on_id = dependsOn;
|
|
15679
|
+
args.dependency = dependsOn;
|
|
15680
|
+
args.dependency_id = dependsOn;
|
|
15681
|
+
args.blocked_by = dependsOn;
|
|
15682
|
+
args.wait_on = dependsOn;
|
|
15683
|
+
args.related_task_id = dependsOn;
|
|
15684
|
+
} else {
|
|
15685
|
+
args.depends_on = null;
|
|
15686
|
+
args.dependency = null;
|
|
15687
|
+
args.blocked_by = null;
|
|
15688
|
+
args.clear = true;
|
|
15689
|
+
}
|
|
15690
|
+
await callAbleTimeTool("set_task_dependency", args, opts);
|
|
15691
|
+
}
|
|
15629
15692
|
async function updateAbleTimeTask(input, opts) {
|
|
15630
15693
|
const id = input.id.trim();
|
|
15631
15694
|
if (!id) throw new Error("AbleTime task id is required");
|
|
15632
15695
|
const title = input.title?.trim();
|
|
15633
15696
|
const description = input.description;
|
|
15634
15697
|
const state = input.state?.trim();
|
|
15635
|
-
|
|
15636
|
-
|
|
15698
|
+
const addRelations = input.relations?.length ? input.relations : void 0;
|
|
15699
|
+
const removeRelations = input.removeRelations?.length ? input.removeRelations : void 0;
|
|
15700
|
+
const updateArgs = { ...taskIdArgs(id) };
|
|
15701
|
+
if (title) updateArgs.title = title;
|
|
15702
|
+
if (description !== void 0) updateArgs.description = description;
|
|
15703
|
+
if (input.labels !== void 0) {
|
|
15704
|
+
updateArgs.tags = input.labels;
|
|
15705
|
+
updateArgs.labels = input.labels;
|
|
15706
|
+
updateArgs.tag_names = input.labels;
|
|
15707
|
+
}
|
|
15708
|
+
if (input.project !== void 0) {
|
|
15709
|
+
const projectName = (input.project ?? "").trim();
|
|
15710
|
+
if (isAbleTimeNoneToken(projectName)) {
|
|
15711
|
+
throw new Error("AbleTime tasks stay in a project \u2014 pass a project name or id");
|
|
15712
|
+
}
|
|
15713
|
+
const project = await resolveAbleTimeProject(projectName, opts);
|
|
15714
|
+
updateArgs.project = project.id;
|
|
15715
|
+
updateArgs.project_id = project.id;
|
|
15716
|
+
}
|
|
15717
|
+
if (input.parent !== void 0) {
|
|
15718
|
+
if (isAbleTimeNoneToken(input.parent)) {
|
|
15719
|
+
updateArgs.parent = null;
|
|
15720
|
+
updateArgs.parent_id = null;
|
|
15721
|
+
updateArgs.related_task_id = null;
|
|
15722
|
+
} else {
|
|
15723
|
+
const parent = (input.parent ?? "").trim();
|
|
15724
|
+
updateArgs.parent = parent;
|
|
15725
|
+
updateArgs.parent_id = parent;
|
|
15726
|
+
updateArgs.related_task_id = parent;
|
|
15727
|
+
}
|
|
15728
|
+
}
|
|
15729
|
+
const hasUpdateFields = Boolean(title) || description !== void 0 || input.labels !== void 0 || input.project !== void 0 || input.parent !== void 0;
|
|
15730
|
+
if (!hasUpdateFields && !state && !addRelations && !removeRelations) {
|
|
15731
|
+
throw new Error(
|
|
15732
|
+
"abletime_update_task needs at least one of title, description, state, labels, project, parent, relations, removeRelations"
|
|
15733
|
+
);
|
|
15637
15734
|
}
|
|
15638
15735
|
if (state) {
|
|
15639
15736
|
await callAbleTimeTool(
|
|
15640
15737
|
"set_task_state",
|
|
15641
|
-
{ id,
|
|
15738
|
+
{ ...taskIdArgs(id), state },
|
|
15642
15739
|
opts
|
|
15643
15740
|
).catch(async () => {
|
|
15644
15741
|
await callAbleTimeTool(
|
|
15645
15742
|
"update_task",
|
|
15646
|
-
{
|
|
15743
|
+
{ ...updateArgs, state },
|
|
15647
15744
|
opts
|
|
15648
15745
|
);
|
|
15649
15746
|
});
|
|
15650
15747
|
}
|
|
15651
|
-
if (
|
|
15652
|
-
await callAbleTimeTool(
|
|
15653
|
-
|
|
15654
|
-
|
|
15655
|
-
|
|
15656
|
-
|
|
15657
|
-
|
|
15658
|
-
|
|
15659
|
-
|
|
15660
|
-
|
|
15661
|
-
);
|
|
15748
|
+
if (hasUpdateFields) {
|
|
15749
|
+
await callAbleTimeTool("update_task", updateArgs, opts);
|
|
15750
|
+
}
|
|
15751
|
+
for (const rel of removeRelations ?? []) {
|
|
15752
|
+
const related = rel.issue.trim() || id;
|
|
15753
|
+
const target = ableTimeDependencyTarget(id, rel.type, related);
|
|
15754
|
+
await setAbleTimeTaskDependency(target.taskId, null, opts);
|
|
15755
|
+
}
|
|
15756
|
+
for (const rel of addRelations ?? []) {
|
|
15757
|
+
const related = rel.issue.trim();
|
|
15758
|
+
if (!related) throw new Error("AbleTime relations.issue is required (id or CRM-232)");
|
|
15759
|
+
const target = ableTimeDependencyTarget(id, rel.type, related);
|
|
15760
|
+
if (!target.dependsOn) {
|
|
15761
|
+
await setAbleTimeTaskDependency(target.taskId, null, opts);
|
|
15762
|
+
continue;
|
|
15763
|
+
}
|
|
15764
|
+
await setAbleTimeTaskDependency(target.taskId, target.dependsOn, opts);
|
|
15662
15765
|
}
|
|
15663
15766
|
return getAbleTimeTask(id, opts);
|
|
15664
15767
|
}
|
|
@@ -18307,17 +18410,17 @@ function formatIssueToolsDirective(opts) {
|
|
|
18307
18410
|
];
|
|
18308
18411
|
if (opts.linear) {
|
|
18309
18412
|
lines.push(
|
|
18310
|
-
"- Linear: `linear_search_issues` (assignee=me; pass `updatedSince` for \u201Cany updates since yesterday?\u201D \u2014 new tickets, edits, and comments), `linear_get_issue` (comments; pass `include=full` if crush.truncated), `linear_comment`, `linear_update_issue` (state, cycle), `linear_create_issue` (pass `parent` for spin-offs; call `linear_list_teams` first). Scope errors: reconnect Linear in Settings \u2192 Issues."
|
|
18413
|
+
"- Linear: `linear_search_issues` (assignee=me; pass `updatedSince` for \u201Cany updates since yesterday?\u201D \u2014 new tickets, edits, and comments), `linear_get_issue` (comments; pass `include=full` if crush.truncated), `linear_comment`, `linear_update_issue` (state, cycle, labels, project, parent, relations), `linear_create_issue` (pass `parent` for spin-offs; call `linear_list_teams` first). Scope errors: reconnect Linear in Settings \u2192 Issues."
|
|
18311
18414
|
);
|
|
18312
18415
|
}
|
|
18313
18416
|
if (github) {
|
|
18314
18417
|
lines.push(
|
|
18315
|
-
"- GitHub: `github_search_issues` (assignee=me; `updatedSince` for inbox updates + comments), `github_get_issue` (comments; pass `include=full` if crush.truncated), `github_comment`, `github_update_issue` (state open|closed), `github_create_issue` (pass `parent` for spin-offs). Uses Account `gh`."
|
|
18418
|
+
"- GitHub: `github_search_issues` (assignee=me; `updatedSince` for inbox updates + comments), `github_get_issue` (comments; pass `include=full` if crush.truncated), `github_comment`, `github_update_issue` (state open|closed, labels, project, parent, relations), `github_create_issue` (pass `parent` for spin-offs). Uses Account `gh`."
|
|
18316
18419
|
);
|
|
18317
18420
|
}
|
|
18318
18421
|
if (opts.abletime) {
|
|
18319
18422
|
lines.push(
|
|
18320
|
-
"- AbleTime: `abletime_list_tasks` / `abletime_search_tasks` (`updatedSince` for inbox updates + comments), `abletime_get_task` (comments; pass `include=full` if crush.truncated), `abletime_comment`, `abletime_update_task` (state), `abletime_create_task` (pass `parent` for spin-offs; `abletime_list_projects` if needed)."
|
|
18423
|
+
"- AbleTime: `abletime_list_tasks` / `abletime_search_tasks` (`updatedSince` for inbox updates + comments), `abletime_get_task` (comments; pass `include=full` if crush.truncated), `abletime_comment`, `abletime_update_task` (state, labels, project, parent, relations), `abletime_create_task` (pass `parent` for spin-offs; `abletime_list_projects` if needed)."
|
|
18321
18424
|
);
|
|
18322
18425
|
}
|
|
18323
18426
|
lines.push(
|
|
@@ -19356,6 +19459,7 @@ __export(orchestrator_exports, {
|
|
|
19356
19459
|
Orchestrator: () => Orchestrator,
|
|
19357
19460
|
getOrchestrator: () => getOrchestrator,
|
|
19358
19461
|
isPidAlive: () => isPidAlive,
|
|
19462
|
+
resolveOrchChildFollowUp: () => resolveOrchChildFollowUp,
|
|
19359
19463
|
resolveSendFollowUp: () => resolveSendFollowUp,
|
|
19360
19464
|
startOrchestration: () => startOrchestration,
|
|
19361
19465
|
waitForPidExit: () => waitForPidExit
|
|
@@ -19386,6 +19490,10 @@ function resolveSendFollowUp(thread, requested) {
|
|
|
19386
19490
|
if (requested === "steer" || requested === "queue") return requested;
|
|
19387
19491
|
return isOrchestratorThread(thread) ? followUpBehavior() : "queue";
|
|
19388
19492
|
}
|
|
19493
|
+
function resolveOrchChildFollowUp(requested) {
|
|
19494
|
+
if (requested === "steer" || requested === "queue") return requested;
|
|
19495
|
+
return followUpBehavior();
|
|
19496
|
+
}
|
|
19389
19497
|
function writeLiveStatus(threadId, status, lastError) {
|
|
19390
19498
|
const latest = readThread(threadId);
|
|
19391
19499
|
if (!latest || latest.status === "archived") return latest;
|
|
@@ -21326,7 +21434,7 @@ var init_orchestrator = __esm({
|
|
|
21326
21434
|
/**
|
|
21327
21435
|
* Desktop git buttons + MCP `ask_git`.
|
|
21328
21436
|
* When the worktree is clean, push / open the PR here (HTTPS via `gh` if SSH
|
|
21329
|
-
* is missing). When dirty,
|
|
21437
|
+
* is missing). When dirty, steer the worktree agent to commit first.
|
|
21330
21438
|
*/
|
|
21331
21439
|
async askGit(threadRef, action) {
|
|
21332
21440
|
if (!AGENT_GIT_ACTIONS.includes(action)) {
|
|
@@ -21363,7 +21471,9 @@ var init_orchestrator = __esm({
|
|
|
21363
21471
|
} catch {
|
|
21364
21472
|
}
|
|
21365
21473
|
}
|
|
21366
|
-
return this.send(threadRef, agentGitPrompt(action, { prBase })
|
|
21474
|
+
return this.send(threadRef, agentGitPrompt(action, { prBase }), {
|
|
21475
|
+
followUp: resolveOrchChildFollowUp()
|
|
21476
|
+
});
|
|
21367
21477
|
}
|
|
21368
21478
|
/** Push origin (gh HTTPS fallback) and create/update the PR when requested. */
|
|
21369
21479
|
async pushAndMaybeOpenPr(thread, action) {
|
|
@@ -21903,13 +22013,78 @@ async function commentGitHubIssue(input, opts) {
|
|
|
21903
22013
|
const url = stdout.trim().split(/\s+/).find((part) => /^https?:\/\//i.test(part));
|
|
21904
22014
|
return { body, url };
|
|
21905
22015
|
}
|
|
22016
|
+
function isGitHubNoneToken(value) {
|
|
22017
|
+
if (value == null) return true;
|
|
22018
|
+
const s = value.trim();
|
|
22019
|
+
return !s || /^(none|clear|unschedule|null)$/i.test(s);
|
|
22020
|
+
}
|
|
22021
|
+
function githubRelationEditFlag(type, remove = false) {
|
|
22022
|
+
const s = type.trim().toLowerCase().replace(/[\s_-]+/g, "");
|
|
22023
|
+
if (s === "blockedby" || s === "blocked" || s === "dependson") {
|
|
22024
|
+
return remove ? "--remove-blocked-by" : "--add-blocked-by";
|
|
22025
|
+
}
|
|
22026
|
+
if (s === "blocks" || s === "blocking") {
|
|
22027
|
+
return remove ? "--remove-blocking" : "--add-blocking";
|
|
22028
|
+
}
|
|
22029
|
+
throw new Error(
|
|
22030
|
+
`GitHub relation type must be blocks or blockedBy (got ${type}). Parent is a separate parent= field.`
|
|
22031
|
+
);
|
|
22032
|
+
}
|
|
22033
|
+
function labelsEqual(a, b) {
|
|
22034
|
+
return a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
22035
|
+
}
|
|
22036
|
+
function parseProjectTitles(raw) {
|
|
22037
|
+
const out = [];
|
|
22038
|
+
const seen = /* @__PURE__ */ new Set();
|
|
22039
|
+
const push = (name) => {
|
|
22040
|
+
const title = name.trim();
|
|
22041
|
+
const key = title.toLowerCase();
|
|
22042
|
+
if (!title || seen.has(key)) return;
|
|
22043
|
+
seen.add(key);
|
|
22044
|
+
out.push(title);
|
|
22045
|
+
};
|
|
22046
|
+
const fromItems = raw.projectItems ?? raw.projects ?? raw.projectItemsV2;
|
|
22047
|
+
if (Array.isArray(fromItems)) {
|
|
22048
|
+
for (const item of fromItems) {
|
|
22049
|
+
if (typeof item === "string") {
|
|
22050
|
+
push(item);
|
|
22051
|
+
continue;
|
|
22052
|
+
}
|
|
22053
|
+
if (!item || typeof item !== "object") continue;
|
|
22054
|
+
const rec = item;
|
|
22055
|
+
const nested = rec.project && typeof rec.project === "object" ? rec.project : rec;
|
|
22056
|
+
const title = String(nested.title ?? nested.name ?? rec.title ?? rec.name ?? "").trim();
|
|
22057
|
+
if (title) push(title);
|
|
22058
|
+
}
|
|
22059
|
+
}
|
|
22060
|
+
return out;
|
|
22061
|
+
}
|
|
22062
|
+
async function listGitHubIssueProjects(number, cwd, repoArgs) {
|
|
22063
|
+
const result = await gh(
|
|
22064
|
+
["issue", "view", String(number), ...repoArgs, "--json", "projectItems"],
|
|
22065
|
+
cwd,
|
|
22066
|
+
{ reject: false }
|
|
22067
|
+
);
|
|
22068
|
+
if (result.exitCode !== 0 || !result.stdout.trim()) return [];
|
|
22069
|
+
try {
|
|
22070
|
+
const parsed = JSON.parse(result.stdout);
|
|
22071
|
+
return parseProjectTitles(parsed);
|
|
22072
|
+
} catch {
|
|
22073
|
+
return [];
|
|
22074
|
+
}
|
|
22075
|
+
}
|
|
21906
22076
|
async function updateGitHubIssue(input, opts) {
|
|
21907
22077
|
const number = parseGitHubIssueNumber(input.id);
|
|
21908
22078
|
const title = input.title?.trim();
|
|
21909
22079
|
const body = input.body;
|
|
21910
22080
|
const state = input.state?.trim().toLowerCase();
|
|
21911
|
-
|
|
21912
|
-
|
|
22081
|
+
const addRelations = input.relations?.length ? input.relations : void 0;
|
|
22082
|
+
const removeRelations = input.removeRelations?.length ? input.removeRelations : void 0;
|
|
22083
|
+
const hasEditFields = Boolean(title) || body !== void 0 || input.labels !== void 0 || input.project !== void 0 || input.parent !== void 0 || Boolean(addRelations) || Boolean(removeRelations);
|
|
22084
|
+
if (!hasEditFields && !state) {
|
|
22085
|
+
throw new Error(
|
|
22086
|
+
"github_update_issue needs at least one of title, body, state, labels, project, parent, relations, removeRelations"
|
|
22087
|
+
);
|
|
21913
22088
|
}
|
|
21914
22089
|
const { cwd, repoArgs } = await resolveGitHubIssueRepo(opts?.repoPath);
|
|
21915
22090
|
if (state === "closed" || state === "close") {
|
|
@@ -21925,10 +22100,46 @@ async function updateGitHubIssue(input, opts) {
|
|
|
21925
22100
|
} else if (state) {
|
|
21926
22101
|
throw new Error(`GitHub issue state must be open or closed (got ${input.state})`);
|
|
21927
22102
|
}
|
|
21928
|
-
|
|
21929
|
-
|
|
21930
|
-
|
|
21931
|
-
|
|
22103
|
+
const args = ["issue", "edit", String(number), ...repoArgs];
|
|
22104
|
+
if (title) args.push("--title", title);
|
|
22105
|
+
if (body !== void 0) args.push("--body", body);
|
|
22106
|
+
if (input.labels !== void 0) {
|
|
22107
|
+
const existing = await getGitHubIssue(String(number), opts);
|
|
22108
|
+
const desired = input.labels.map((name) => name.trim()).filter(Boolean);
|
|
22109
|
+
const add = desired.filter((name) => !existing.labels.some((cur) => labelsEqual(cur, name)));
|
|
22110
|
+
const remove = existing.labels.filter((cur) => !desired.some((name) => labelsEqual(cur, name)));
|
|
22111
|
+
if (add.length) args.push("--add-label", add.join(","));
|
|
22112
|
+
if (remove.length) args.push("--remove-label", remove.join(","));
|
|
22113
|
+
}
|
|
22114
|
+
if (input.project !== void 0) {
|
|
22115
|
+
const current = await listGitHubIssueProjects(number, cwd, repoArgs);
|
|
22116
|
+
if (isGitHubNoneToken(input.project)) {
|
|
22117
|
+
for (const name of current) args.push("--remove-project", name);
|
|
22118
|
+
} else {
|
|
22119
|
+
const wanted = (input.project ?? "").trim();
|
|
22120
|
+
if (!current.some((name) => labelsEqual(name, wanted))) {
|
|
22121
|
+
args.push("--add-project", wanted);
|
|
22122
|
+
}
|
|
22123
|
+
for (const name of current) {
|
|
22124
|
+
if (!labelsEqual(name, wanted)) args.push("--remove-project", name);
|
|
22125
|
+
}
|
|
22126
|
+
}
|
|
22127
|
+
}
|
|
22128
|
+
if (input.parent !== void 0) {
|
|
22129
|
+
if (isGitHubNoneToken(input.parent)) {
|
|
22130
|
+
args.push("--remove-parent");
|
|
22131
|
+
} else {
|
|
22132
|
+
args.push("--parent", String(parseGitHubIssueNumber(input.parent ?? "")));
|
|
22133
|
+
}
|
|
22134
|
+
}
|
|
22135
|
+
for (const rel of removeRelations ?? []) {
|
|
22136
|
+
args.push(githubRelationEditFlag(rel.type, true), String(parseGitHubIssueNumber(rel.issue)));
|
|
22137
|
+
}
|
|
22138
|
+
for (const rel of addRelations ?? []) {
|
|
22139
|
+
args.push(githubRelationEditFlag(rel.type, false), String(parseGitHubIssueNumber(rel.issue)));
|
|
22140
|
+
}
|
|
22141
|
+
const editChanged = args.length > 3 + repoArgs.length;
|
|
22142
|
+
if (editChanged) {
|
|
21932
22143
|
requireGhOk(await gh(args, cwd, { reject: false }), `GitHub edit #${number}`);
|
|
21933
22144
|
}
|
|
21934
22145
|
return getGitHubIssue(String(number), opts);
|
|
@@ -22105,12 +22316,14 @@ var ISSUE_FIELDS = `
|
|
|
22105
22316
|
children(first: 25) { nodes { ${ISSUE_REF_FIELDS} } }
|
|
22106
22317
|
relations(first: 25) {
|
|
22107
22318
|
nodes {
|
|
22319
|
+
id
|
|
22108
22320
|
type
|
|
22109
22321
|
relatedIssue { ${ISSUE_REF_FIELDS} }
|
|
22110
22322
|
}
|
|
22111
22323
|
}
|
|
22112
22324
|
inverseRelations(first: 25) {
|
|
22113
22325
|
nodes {
|
|
22326
|
+
id
|
|
22114
22327
|
type
|
|
22115
22328
|
issue { ${ISSUE_REF_FIELDS} }
|
|
22116
22329
|
}
|
|
@@ -22230,6 +22443,45 @@ mutation SideboardCommentCreate($input: CommentCreateInput!) {
|
|
|
22230
22443
|
}
|
|
22231
22444
|
}
|
|
22232
22445
|
`;
|
|
22446
|
+
var LABELS_QUERY = `
|
|
22447
|
+
query SideboardLabels($first: Int!) {
|
|
22448
|
+
issueLabels(first: $first) {
|
|
22449
|
+
nodes { id name }
|
|
22450
|
+
}
|
|
22451
|
+
}
|
|
22452
|
+
`;
|
|
22453
|
+
var TEAM_LABELS_QUERY = `
|
|
22454
|
+
query SideboardTeamLabels($id: String!, $first: Int!, $after: String) {
|
|
22455
|
+
team(id: $id) {
|
|
22456
|
+
labels(first: $first, after: $after) {
|
|
22457
|
+
nodes { id name }
|
|
22458
|
+
pageInfo { hasNextPage endCursor }
|
|
22459
|
+
}
|
|
22460
|
+
}
|
|
22461
|
+
}
|
|
22462
|
+
`;
|
|
22463
|
+
var PROJECTS_QUERY = `
|
|
22464
|
+
query SideboardProjects($first: Int!, $filter: ProjectFilter) {
|
|
22465
|
+
projects(first: $first, filter: $filter) {
|
|
22466
|
+
nodes { id name slugId }
|
|
22467
|
+
}
|
|
22468
|
+
}
|
|
22469
|
+
`;
|
|
22470
|
+
var ISSUE_RELATION_CREATE = `
|
|
22471
|
+
mutation SideboardIssueRelationCreate($input: IssueRelationCreateInput!) {
|
|
22472
|
+
issueRelationCreate(input: $input) {
|
|
22473
|
+
success
|
|
22474
|
+
issueRelation { id type }
|
|
22475
|
+
}
|
|
22476
|
+
}
|
|
22477
|
+
`;
|
|
22478
|
+
var ISSUE_RELATION_DELETE = `
|
|
22479
|
+
mutation SideboardIssueRelationDelete($id: String!) {
|
|
22480
|
+
issueRelationDelete(id: $id) {
|
|
22481
|
+
success
|
|
22482
|
+
}
|
|
22483
|
+
}
|
|
22484
|
+
`;
|
|
22233
22485
|
function rewriteLinearError(message) {
|
|
22234
22486
|
if (/scope|permission|not (authorized|allowed)|unauthorized|insufficient/i.test(message)) {
|
|
22235
22487
|
return `${message} \u2014 Disconnect and Connect Linear in Account settings to grant write access.`;
|
|
@@ -22349,12 +22601,18 @@ function mapRelations(node) {
|
|
|
22349
22601
|
for (const rel of node.relations?.nodes ?? []) {
|
|
22350
22602
|
const issue = mapIssueRef(rel.relatedIssue);
|
|
22351
22603
|
if (!issue) continue;
|
|
22352
|
-
|
|
22604
|
+
const id = rel.id?.trim();
|
|
22605
|
+
out.push({ type: String(rel.type ?? "related"), issue, ...id ? { id } : {} });
|
|
22353
22606
|
}
|
|
22354
22607
|
for (const rel of node.inverseRelations?.nodes ?? []) {
|
|
22355
22608
|
const issue = mapIssueRef(rel.issue);
|
|
22356
22609
|
if (!issue) continue;
|
|
22357
|
-
|
|
22610
|
+
const id = rel.id?.trim();
|
|
22611
|
+
out.push({
|
|
22612
|
+
type: flipLinearRelationType(String(rel.type ?? "related")),
|
|
22613
|
+
issue,
|
|
22614
|
+
...id ? { id } : {}
|
|
22615
|
+
});
|
|
22358
22616
|
}
|
|
22359
22617
|
return out;
|
|
22360
22618
|
}
|
|
@@ -22515,6 +22773,171 @@ function resolveLinearCycle(team, cycle) {
|
|
|
22515
22773
|
}
|
|
22516
22774
|
return found.id;
|
|
22517
22775
|
}
|
|
22776
|
+
var LINEAR_NONE_RE = /^(none|clear|unschedule|null)$/i;
|
|
22777
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
22778
|
+
function isLinearNoneToken(value) {
|
|
22779
|
+
if (value == null) return true;
|
|
22780
|
+
const s = value.trim();
|
|
22781
|
+
return !s || LINEAR_NONE_RE.test(s);
|
|
22782
|
+
}
|
|
22783
|
+
function resolveLinearLabel(labels, name) {
|
|
22784
|
+
const s = name.trim();
|
|
22785
|
+
if (!s) throw new Error("Linear label is empty");
|
|
22786
|
+
const lower = s.toLowerCase();
|
|
22787
|
+
const found = labels.find((x) => x.id === s) || labels.find((x) => x.name.toLowerCase() === lower);
|
|
22788
|
+
if (!found) {
|
|
22789
|
+
const available = labels.map((x) => x.name).filter(Boolean).slice(0, 20).join(", ") || "(none)";
|
|
22790
|
+
throw new Error(`Linear label not found: ${name}. Available: ${available}`);
|
|
22791
|
+
}
|
|
22792
|
+
return found;
|
|
22793
|
+
}
|
|
22794
|
+
function resolveLinearProject(projects, project) {
|
|
22795
|
+
if (isLinearNoneToken(project)) return null;
|
|
22796
|
+
const s = (project ?? "").trim();
|
|
22797
|
+
const lower = s.toLowerCase();
|
|
22798
|
+
const found = projects.find((x) => x.id === s) || projects.find((x) => x.name.toLowerCase() === lower) || projects.find((x) => x.slugId?.toLowerCase() === lower);
|
|
22799
|
+
if (!found) {
|
|
22800
|
+
const available = projects.map((x) => x.name).filter(Boolean).slice(0, 20).join(", ") || "(none)";
|
|
22801
|
+
throw new Error(`Linear project not found: ${project}. Available: ${available}`);
|
|
22802
|
+
}
|
|
22803
|
+
return found.id;
|
|
22804
|
+
}
|
|
22805
|
+
function normalizeLinearRelationType(type) {
|
|
22806
|
+
const s = type.trim().toLowerCase().replace(/[\s_-]+/g, "");
|
|
22807
|
+
if (s === "blocks" || s === "block") return "blocks";
|
|
22808
|
+
if (s === "blockedby" || s === "blocked") return "blockedBy";
|
|
22809
|
+
if (s === "related" || s === "relates" || s === "relatesto") return "related";
|
|
22810
|
+
if (s === "duplicate" || s === "dup") return "duplicate";
|
|
22811
|
+
if (s === "duplicateof" || s === "original") return "duplicateOf";
|
|
22812
|
+
if (s === "similar") return "similar";
|
|
22813
|
+
throw new Error(
|
|
22814
|
+
`Linear relation type must be blocks, blockedBy, related, duplicate, or duplicateOf (got ${type})`
|
|
22815
|
+
);
|
|
22816
|
+
}
|
|
22817
|
+
function linearRelationCreateInput(thisIssueId, type, relatedIssueId) {
|
|
22818
|
+
const view = normalizeLinearRelationType(type);
|
|
22819
|
+
if (view === "blockedBy") {
|
|
22820
|
+
return { issueId: relatedIssueId, relatedIssueId: thisIssueId, type: "blocks" };
|
|
22821
|
+
}
|
|
22822
|
+
if (view === "duplicateOf") {
|
|
22823
|
+
return { issueId: relatedIssueId, relatedIssueId: thisIssueId, type: "duplicate" };
|
|
22824
|
+
}
|
|
22825
|
+
return { issueId: thisIssueId, relatedIssueId, type: view };
|
|
22826
|
+
}
|
|
22827
|
+
function relationMatches(rel, type, issueRef) {
|
|
22828
|
+
if (rel.type.toLowerCase() !== type.toLowerCase()) return false;
|
|
22829
|
+
const ref = issueRef.trim().toLowerCase();
|
|
22830
|
+
return rel.issue.id.toLowerCase() === ref || rel.issue.identifier.toLowerCase() === ref;
|
|
22831
|
+
}
|
|
22832
|
+
function formatRelationList(relations) {
|
|
22833
|
+
if (!relations.length) return "(none)";
|
|
22834
|
+
return relations.map((r) => `${r.type} ${r.issue.identifier || r.issue.id}`).join(", ");
|
|
22835
|
+
}
|
|
22836
|
+
function mapLabelNodes(nodes) {
|
|
22837
|
+
return (nodes ?? []).flatMap((node) => {
|
|
22838
|
+
const id = String(node.id ?? "").trim();
|
|
22839
|
+
const name = String(node.name ?? "").trim();
|
|
22840
|
+
if (!id) return [];
|
|
22841
|
+
return [{ id, name }];
|
|
22842
|
+
});
|
|
22843
|
+
}
|
|
22844
|
+
async function listLinearWorkspaceLabels(opts) {
|
|
22845
|
+
const json = await linearGraphql(LABELS_QUERY, { first: 250 }, opts);
|
|
22846
|
+
return mapLabelNodes(json.issueLabels?.nodes);
|
|
22847
|
+
}
|
|
22848
|
+
async function listLinearLabels(opts) {
|
|
22849
|
+
const teamId = opts?.teamId?.trim();
|
|
22850
|
+
if (teamId) {
|
|
22851
|
+
const out = [];
|
|
22852
|
+
let after;
|
|
22853
|
+
for (let page = 0; page < 4; page++) {
|
|
22854
|
+
const json = await linearGraphql(
|
|
22855
|
+
TEAM_LABELS_QUERY,
|
|
22856
|
+
after ? { id: teamId, first: 250, after } : { id: teamId, first: 250 },
|
|
22857
|
+
opts
|
|
22858
|
+
);
|
|
22859
|
+
const conn = json.team?.labels;
|
|
22860
|
+
out.push(...mapLabelNodes(conn?.nodes));
|
|
22861
|
+
if (!conn?.pageInfo?.hasNextPage || !conn.pageInfo.endCursor) break;
|
|
22862
|
+
after = conn.pageInfo.endCursor;
|
|
22863
|
+
}
|
|
22864
|
+
if (out.length) return out;
|
|
22865
|
+
}
|
|
22866
|
+
return listLinearWorkspaceLabels(opts);
|
|
22867
|
+
}
|
|
22868
|
+
async function listLinearProjects(filter, opts) {
|
|
22869
|
+
const json = await linearGraphql(PROJECTS_QUERY, { first: 50, filter: filter ?? {} }, opts);
|
|
22870
|
+
return (json.projects?.nodes ?? []).flatMap((node) => {
|
|
22871
|
+
const id = String(node.id ?? "").trim();
|
|
22872
|
+
if (!id) return [];
|
|
22873
|
+
return [
|
|
22874
|
+
{
|
|
22875
|
+
id,
|
|
22876
|
+
name: String(node.name ?? ""),
|
|
22877
|
+
...node.slugId ? { slugId: String(node.slugId) } : {}
|
|
22878
|
+
}
|
|
22879
|
+
];
|
|
22880
|
+
});
|
|
22881
|
+
}
|
|
22882
|
+
async function resolveLinearProjectId(project, opts) {
|
|
22883
|
+
if (isLinearNoneToken(project)) return null;
|
|
22884
|
+
const s = (project ?? "").trim();
|
|
22885
|
+
const filter = UUID_RE.test(s) ? { id: { eq: s } } : { name: { eqIgnoreCase: s } };
|
|
22886
|
+
const matched = await listLinearProjects(filter, opts);
|
|
22887
|
+
try {
|
|
22888
|
+
return resolveLinearProject(matched, s);
|
|
22889
|
+
} catch {
|
|
22890
|
+
if (UUID_RE.test(s)) return s;
|
|
22891
|
+
const listed = await listLinearProjects(void 0, opts);
|
|
22892
|
+
return resolveLinearProject(listed, s);
|
|
22893
|
+
}
|
|
22894
|
+
}
|
|
22895
|
+
async function resolveLinearLabelIds(names, opts) {
|
|
22896
|
+
if (names.length === 0) return [];
|
|
22897
|
+
const labels = await listLinearLabels(opts);
|
|
22898
|
+
return names.map((name) => {
|
|
22899
|
+
const s = name.trim();
|
|
22900
|
+
try {
|
|
22901
|
+
return resolveLinearLabel(labels, s).id;
|
|
22902
|
+
} catch (error) {
|
|
22903
|
+
if (UUID_RE.test(s)) return s;
|
|
22904
|
+
throw error;
|
|
22905
|
+
}
|
|
22906
|
+
});
|
|
22907
|
+
}
|
|
22908
|
+
async function applyLinearIssueRelations(issue, add, remove, opts) {
|
|
22909
|
+
const current = [...issue.relations];
|
|
22910
|
+
for (const rel of remove ?? []) {
|
|
22911
|
+
const type = normalizeLinearRelationType(rel.type);
|
|
22912
|
+
const issueRef = rel.issue.trim();
|
|
22913
|
+
if (!issueRef) throw new Error("Linear removeRelations.issue is required (uuid or ENG-123)");
|
|
22914
|
+
const match = current.find((r) => relationMatches(r, type, issueRef));
|
|
22915
|
+
if (!match?.id) {
|
|
22916
|
+
throw new Error(
|
|
22917
|
+
`Linear relation not found: ${type} ${issueRef}. Existing: ${formatRelationList(current)}`
|
|
22918
|
+
);
|
|
22919
|
+
}
|
|
22920
|
+
const json = await linearGraphql(ISSUE_RELATION_DELETE, { id: match.id }, opts);
|
|
22921
|
+
if (!json.issueRelationDelete?.success) {
|
|
22922
|
+
throw new Error(`Linear issueRelationDelete failed (${type} ${issueRef})`);
|
|
22923
|
+
}
|
|
22924
|
+
const idx = current.indexOf(match);
|
|
22925
|
+
if (idx >= 0) current.splice(idx, 1);
|
|
22926
|
+
}
|
|
22927
|
+
for (const rel of add ?? []) {
|
|
22928
|
+
const type = normalizeLinearRelationType(rel.type);
|
|
22929
|
+
const issueRef = rel.issue.trim();
|
|
22930
|
+
if (!issueRef) throw new Error("Linear relations.issue is required (uuid or ENG-123)");
|
|
22931
|
+
if (current.some((r) => relationMatches(r, type, issueRef))) continue;
|
|
22932
|
+
const related = await getLinearIssue(issueRef, opts);
|
|
22933
|
+
const input = linearRelationCreateInput(issue.id, type, related.id);
|
|
22934
|
+
const json = await linearGraphql(ISSUE_RELATION_CREATE, { input }, opts);
|
|
22935
|
+
if (!json.issueRelationCreate?.success) {
|
|
22936
|
+
throw new Error(`Linear issueRelationCreate failed (${type} ${related.identifier})`);
|
|
22937
|
+
}
|
|
22938
|
+
current.push({ type, issue: related, id: void 0 });
|
|
22939
|
+
}
|
|
22940
|
+
}
|
|
22518
22941
|
function normalizePriority(priority) {
|
|
22519
22942
|
if (priority == null) return void 0;
|
|
22520
22943
|
if (!Number.isInteger(priority) || priority < 0 || priority > 4) {
|
|
@@ -22522,7 +22945,6 @@ function normalizePriority(priority) {
|
|
|
22522
22945
|
}
|
|
22523
22946
|
return priority;
|
|
22524
22947
|
}
|
|
22525
|
-
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
22526
22948
|
function assigneeFilterKey(assignee) {
|
|
22527
22949
|
return (assignee ?? "").trim();
|
|
22528
22950
|
}
|
|
@@ -22729,8 +23151,10 @@ async function updateLinearIssue(input, opts) {
|
|
|
22729
23151
|
if (input.description !== void 0) {
|
|
22730
23152
|
mutationInput.description = input.description;
|
|
22731
23153
|
}
|
|
22732
|
-
const
|
|
22733
|
-
const
|
|
23154
|
+
const addRelations = input.relations?.length ? input.relations : void 0;
|
|
23155
|
+
const removeRelations = input.removeRelations?.length ? input.removeRelations : void 0;
|
|
23156
|
+
const needsIssue = Boolean(input.state?.trim()) || input.cycle !== void 0 || input.labels !== void 0 || Boolean(addRelations) || Boolean(removeRelations);
|
|
23157
|
+
const existing = needsIssue ? await getLinearIssue(issueId, opts) : void 0;
|
|
22734
23158
|
if (input.state?.trim()) {
|
|
22735
23159
|
const { teams } = await listLinearTeams(opts);
|
|
22736
23160
|
const teamRef = existing?.team?.key || existing?.team?.id;
|
|
@@ -22763,14 +23187,47 @@ async function updateLinearIssue(input, opts) {
|
|
|
22763
23187
|
if (input.priority !== void 0) {
|
|
22764
23188
|
mutationInput.priority = normalizePriority(input.priority);
|
|
22765
23189
|
}
|
|
22766
|
-
if (
|
|
22767
|
-
|
|
23190
|
+
if (input.labels !== void 0) {
|
|
23191
|
+
mutationInput.labelIds = await resolveLinearLabelIds(input.labels, {
|
|
23192
|
+
...opts,
|
|
23193
|
+
teamId: existing?.team?.id
|
|
23194
|
+
});
|
|
23195
|
+
}
|
|
23196
|
+
if (input.project !== void 0) {
|
|
23197
|
+
mutationInput.projectId = await resolveLinearProjectId(input.project, opts);
|
|
23198
|
+
}
|
|
23199
|
+
if (input.parent !== void 0) {
|
|
23200
|
+
if (isLinearNoneToken(input.parent)) {
|
|
23201
|
+
mutationInput.parentId = null;
|
|
23202
|
+
} else {
|
|
23203
|
+
const parent = await getLinearIssue((input.parent ?? "").trim(), opts);
|
|
23204
|
+
mutationInput.parentId = parent.id;
|
|
23205
|
+
}
|
|
23206
|
+
}
|
|
23207
|
+
const hasIssueFields = Object.keys(mutationInput).length > 0;
|
|
23208
|
+
const hasRelationWrites = Boolean(addRelations || removeRelations);
|
|
23209
|
+
if (!hasIssueFields && !hasRelationWrites) {
|
|
23210
|
+
throw new Error(
|
|
23211
|
+
"linear_update_issue needs at least one of title, description, state, assignee, priority, cycle, labels, project, parent, relations, removeRelations"
|
|
23212
|
+
);
|
|
22768
23213
|
}
|
|
22769
|
-
|
|
22770
|
-
if (
|
|
22771
|
-
|
|
23214
|
+
let updated;
|
|
23215
|
+
if (hasIssueFields) {
|
|
23216
|
+
const json = await linearGraphql(ISSUE_UPDATE, { id: issueId, input: mutationInput }, opts);
|
|
23217
|
+
if (!json.issueUpdate?.success || !json.issueUpdate.issue) {
|
|
23218
|
+
throw new Error("Linear issueUpdate failed");
|
|
23219
|
+
}
|
|
23220
|
+
updated = mapIssue(json.issueUpdate.issue);
|
|
23221
|
+
}
|
|
23222
|
+
if (hasRelationWrites) {
|
|
23223
|
+
const base = updated ?? existing;
|
|
23224
|
+
if (!base) {
|
|
23225
|
+
throw new Error(`Linear issue not found: ${issueId}`);
|
|
23226
|
+
}
|
|
23227
|
+
await applyLinearIssueRelations(base, addRelations, removeRelations, opts);
|
|
23228
|
+
return getLinearIssue(base.id || issueId, opts);
|
|
22772
23229
|
}
|
|
22773
|
-
return
|
|
23230
|
+
return updated;
|
|
22774
23231
|
}
|
|
22775
23232
|
async function commentLinearIssue(input, opts) {
|
|
22776
23233
|
const issueId = input.id.trim();
|
|
@@ -24038,14 +24495,23 @@ function registerAbleTimeTools(server) {
|
|
|
24038
24495
|
}
|
|
24039
24496
|
}
|
|
24040
24497
|
);
|
|
24498
|
+
const relationInputSchema = import_zod4.z.object({
|
|
24499
|
+
type: import_zod4.z.string().describe("blockedBy (this waits on issue) or blocks (issue waits on this)."),
|
|
24500
|
+
issue: import_zod4.z.string().describe("Related task id or reference (CRM-232).")
|
|
24501
|
+
});
|
|
24041
24502
|
server.tool(
|
|
24042
24503
|
"abletime_update_task",
|
|
24043
|
-
|
|
24504
|
+
'Update an AbleTime task (id or CRM-232). Pass title, description, state, labels, project, parent, and/or relations. labels replaces tags. project is name or id (tasks stay in a project). parent is CRM-232 or "none". relations sets the wait-on dependency (blockedBy/blocks); removeRelations clears it. When reviewing a PR or ticket, wait until they type a next step before changing the task \u2014 do not ask_user after the review. The author is notified.',
|
|
24044
24505
|
{
|
|
24045
24506
|
id: import_zod4.z.string(),
|
|
24046
24507
|
title: import_zod4.z.string().optional(),
|
|
24047
24508
|
description: import_zod4.z.string().optional(),
|
|
24048
|
-
state: import_zod4.z.string().optional()
|
|
24509
|
+
state: import_zod4.z.string().optional(),
|
|
24510
|
+
labels: import_zod4.z.array(import_zod4.z.string()).optional().describe("Replace tags/labels. Empty array clears when the host accepts it."),
|
|
24511
|
+
project: import_zod4.z.string().nullable().optional().describe("Project name or id. AbleTime tasks stay in a project."),
|
|
24512
|
+
parent: import_zod4.z.string().nullable().optional().describe('Parent task id or reference (CRM-232), or "none" to unset.'),
|
|
24513
|
+
relations: import_zod4.z.array(relationInputSchema).optional().describe("Set a wait-on dependency: blockedBy or blocks."),
|
|
24514
|
+
removeRelations: import_zod4.z.array(relationInputSchema).optional().describe("Clear a wait-on dependency.")
|
|
24049
24515
|
},
|
|
24050
24516
|
async (args) => {
|
|
24051
24517
|
try {
|
|
@@ -24180,14 +24646,23 @@ function registerGithubIssueTools(server) {
|
|
|
24180
24646
|
}
|
|
24181
24647
|
}
|
|
24182
24648
|
);
|
|
24649
|
+
const relationInputSchema = import_zod5.z.object({
|
|
24650
|
+
type: import_zod5.z.string().describe("blocks or blockedBy (this issue's view)."),
|
|
24651
|
+
issue: import_zod5.z.string().describe("Related issue #123 or URL.")
|
|
24652
|
+
});
|
|
24183
24653
|
server.tool(
|
|
24184
24654
|
"github_update_issue",
|
|
24185
|
-
|
|
24655
|
+
'Update a GitHub issue (#123). Pass title, body, state (open|closed), labels, project, parent, and/or relations. labels replaces all names (pass [] to clear). project is a Project title, or "none" to remove. parent is #123 or "none". relations adds blockedBy/blocks; removeRelations drops them (`gh` 2.94+). When reviewing a PR or ticket, wait until they type a next step before changing the issue \u2014 do not ask_user after the review. The author is notified.',
|
|
24186
24656
|
{
|
|
24187
24657
|
id: import_zod5.z.string(),
|
|
24188
24658
|
title: import_zod5.z.string().optional(),
|
|
24189
24659
|
body: import_zod5.z.string().optional(),
|
|
24190
24660
|
state: import_zod5.z.string().optional().describe("open or closed"),
|
|
24661
|
+
labels: import_zod5.z.array(import_zod5.z.string()).optional().describe("Replace all labels. Empty array clears. Call github_get_issue first to keep existing."),
|
|
24662
|
+
project: import_zod5.z.string().nullable().optional().describe('GitHub Project title, or "none" to remove current projects.'),
|
|
24663
|
+
parent: import_zod5.z.string().nullable().optional().describe('Parent issue #123 or URL, or "none" to unset.'),
|
|
24664
|
+
relations: import_zod5.z.array(relationInputSchema).optional().describe("Add blockedBy or blocks relations (issue #123 or URL)."),
|
|
24665
|
+
removeRelations: import_zod5.z.array(relationInputSchema).optional().describe("Remove blockedBy or blocks relations."),
|
|
24191
24666
|
repoPath: repoPathSchema
|
|
24192
24667
|
},
|
|
24193
24668
|
async (args) => {
|
|
@@ -24317,9 +24792,13 @@ function registerLinearTools(server) {
|
|
|
24317
24792
|
}
|
|
24318
24793
|
}
|
|
24319
24794
|
);
|
|
24795
|
+
const relationInputSchema = import_zod6.z.object({
|
|
24796
|
+
type: import_zod6.z.string().describe("blocks, blockedBy, related, duplicate, or duplicateOf (this issue's view)."),
|
|
24797
|
+
issue: import_zod6.z.string().describe("Related issue uuid or identifier (ENG-123).")
|
|
24798
|
+
});
|
|
24320
24799
|
server.tool(
|
|
24321
24800
|
"linear_update_issue",
|
|
24322
|
-
'Update a Linear issue (uuid or ENG-123). Pass title, description, state, assignee, priority, and/or
|
|
24801
|
+
'Update a Linear issue (uuid or ENG-123). Pass title, description, state, assignee, priority, cycle, labels, project, parent, and/or relations. Cycle/project/parent: name or id, or "none" to clear. labels replaces all names (pass [] to clear; get the issue first to keep existing). relations adds; removeRelations removes by type + issue. When reviewing a PR or ticket, wait until they type a next step before changing the ticket \u2014 do not ask_user after the review. The author is notified.',
|
|
24323
24802
|
{
|
|
24324
24803
|
id: import_zod6.z.string(),
|
|
24325
24804
|
title: import_zod6.z.string().optional(),
|
|
@@ -24327,7 +24806,12 @@ function registerLinearTools(server) {
|
|
|
24327
24806
|
state: import_zod6.z.string().optional(),
|
|
24328
24807
|
assignee: import_zod6.z.string().nullable().optional(),
|
|
24329
24808
|
priority: prioritySchema,
|
|
24330
|
-
cycle: import_zod6.z.string().nullable().optional().describe('Cycle name, number, "current"/"active", or "none" to unschedule.')
|
|
24809
|
+
cycle: import_zod6.z.string().nullable().optional().describe('Cycle name, number, "current"/"active", or "none" to unschedule.'),
|
|
24810
|
+
labels: import_zod6.z.array(import_zod6.z.string()).optional().describe("Replace all labels (names or ids). Empty array clears. Call linear_get_issue first to keep existing."),
|
|
24811
|
+
project: import_zod6.z.string().nullable().optional().describe('Project name, id, slug, or "none" to unassign.'),
|
|
24812
|
+
parent: import_zod6.z.string().nullable().optional().describe('Parent issue uuid or identifier (ENG-123), or "none" to unset.'),
|
|
24813
|
+
relations: import_zod6.z.array(relationInputSchema).optional().describe("Add relations from this issue's view (blocks, blockedBy, related, duplicate, duplicateOf)."),
|
|
24814
|
+
removeRelations: import_zod6.z.array(relationInputSchema).optional().describe("Remove relations by type + related issue (uuid or ENG-123).")
|
|
24331
24815
|
},
|
|
24332
24816
|
async (args) => {
|
|
24333
24817
|
try {
|
|
@@ -25568,7 +26052,7 @@ async function startMcpServer() {
|
|
|
25568
26052
|
);
|
|
25569
26053
|
server.tool(
|
|
25570
26054
|
"send_to_thread",
|
|
25571
|
-
'
|
|
26055
|
+
'Steer a prompt on a worktree thread chat (Settings \u2192 Follow-up, default steer: interrupt and start now, skip the inbox). Use after create_thread to start or continue a conversation. For commit/push/PR, prefer ask_git (canonical desktop-button phrases). Send "Merge PR." / ask_git merge only when the user explicitly asked to merge. force_stop=true kills the in-flight turn and clears the queue before this prompt \u2014 only when the current request is wrong and must be replaced. Do not send_to_thread to check in, resume after a halt notice, or because wait_for_turn returned stillRunning; that interrupts the child mid-thought. Call wait_for_turn again instead.',
|
|
25572
26056
|
{
|
|
25573
26057
|
ref: import_zod9.z.string(),
|
|
25574
26058
|
prompt: import_zod9.z.string(),
|
|
@@ -25581,7 +26065,9 @@ async function startMcpServer() {
|
|
|
25581
26065
|
orch.stop(ref, { clearQueue: true, notifyParent: false });
|
|
25582
26066
|
}
|
|
25583
26067
|
}
|
|
25584
|
-
const thread = await orch.send(ref, prompt
|
|
26068
|
+
const thread = await orch.send(ref, prompt, {
|
|
26069
|
+
followUp: resolveOrchChildFollowUp()
|
|
26070
|
+
});
|
|
25585
26071
|
return mcpJson({
|
|
25586
26072
|
id: thread.id,
|
|
25587
26073
|
status: thread.status,
|
|
@@ -25592,7 +26078,7 @@ async function startMcpServer() {
|
|
|
25592
26078
|
);
|
|
25593
26079
|
server.tool(
|
|
25594
26080
|
"wait_for_turn",
|
|
25595
|
-
"Wait until the thread 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. stillRunning is the source of truth \u2014 if false, the child is not working (do not say it is waiting for a gate). If stillRunning is true, progress is tools/thinking (or \u201Cqueued, waiting for a concurrency slot\u201D if it has not started). Call wait_for_turn again. Do not
|
|
26081
|
+
"Wait until the thread 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. stillRunning is the source of truth \u2014 if false, the child is not working (do not say it is waiting for a gate). If stillRunning is true, progress is tools/thinking (or \u201Cqueued, waiting for a concurrency slot\u201D if it has not started). Call wait_for_turn again. Do not send_to_thread a check-in (that steers / interrupts), force_stop, or assume a hang. On status error, lastError/text is the failure. On status stopped or broken, the child did not finish \u2014 resume with send_to_thread or tell the user; do not treat that as success. When finished, usage is the last agent turn\u2019s tokens + costUsd (when the provider reported cost).",
|
|
25596
26082
|
{
|
|
25597
26083
|
ref: import_zod9.z.string(),
|
|
25598
26084
|
timeoutMs: import_zod9.z.number().optional()
|
|
@@ -25749,7 +26235,7 @@ async function startMcpServer() {
|
|
|
25749
26235
|
);
|
|
25750
26236
|
server.tool(
|
|
25751
26237
|
"ask_git",
|
|
25752
|
-
"Commit & push, open a draft PR, mark ready for review, resolve conflicts, or merge \u2014 same actions as the desktop git buttons. When the worktree is clean, Sideboard pushes / opens the PR itself (HTTPS via `gh` even when origin is SSH / Settings \u2192 Git is SSH). When dirty,
|
|
26238
|
+
"Commit & push, open a draft PR, mark ready for review, resolve conflicts, or merge \u2014 same actions as the desktop git buttons. When the worktree is clean, Sideboard pushes / opens the PR itself (HTTPS via `gh` even when origin is SSH / Settings \u2192 Git is SSH). When dirty, steers the worktree agent to commit (Settings \u2192 Follow-up, default interrupt now); then wait_for_turn (loop while stillRunning). Do not start a checks loop on a plain push \u2014 only if the user gave a goal (Greptile 5/5, CI green). Pass a worktree thread ref (not the orchestrator). action=ready-for-review and action=merge only when the user explicitly asked (or clicked the desktop button). Do not run git or gh from the orchestration cwd. If this tool errors that the GraphQL/PR body is too long, the branch is already pushed \u2014 have the worktree agent retry `gh pr create --body-file` with a short description (GitHub limit 65,536 characters). Do not invent SSH/auth failures from that error.",
|
|
25753
26239
|
{
|
|
25754
26240
|
ref: import_zod9.z.string().describe("Worktree thread id/ref"),
|
|
25755
26241
|
action: import_zod9.z.enum(AGENT_GIT_ACTIONS).describe(
|