@coreplane/switchboard 1.220.0 → 1.220.1
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.
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.220.
|
|
3
|
+
"version": "1.220.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.220.
|
|
9
|
+
"version": "1.220.1",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"workspaces": [
|
|
12
12
|
"web",
|
|
@@ -16710,7 +16710,7 @@
|
|
|
16710
16710
|
},
|
|
16711
16711
|
"packages/switchboard": {
|
|
16712
16712
|
"name": "@coreplane/switchboard",
|
|
16713
|
-
"version": "1.220.
|
|
16713
|
+
"version": "1.220.1",
|
|
16714
16714
|
"license": "Apache-2.0",
|
|
16715
16715
|
"dependencies": {
|
|
16716
16716
|
"@anthropic-ai/sdk": "^0.124.0",
|
package/dist/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.220.
|
|
3
|
+
"version": "1.220.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/dist/assets/source.json
CHANGED
|
@@ -622,6 +622,19 @@ export function parseExitPrefix(output: string): { failed: boolean; exitCode?: n
|
|
|
622
622
|
return /^\d+$/.test(m[1]) ? { failed: true, exitCode: Number(m[1]) } : { failed: true };
|
|
623
623
|
}
|
|
624
624
|
|
|
625
|
+
/** A tool that could not do what it was asked answers the model with a text
|
|
626
|
+
* that opens `error:` (`attach_file`, the `submit_*` tools, the run tools —
|
|
627
|
+
* each declares `failsInText` on its `RunnableTool`) instead of throwing, so
|
|
628
|
+
* the model can read the reason and go on. The record must call that result
|
|
629
|
+
* what the model reads it as: `ok:false`. Both loops (the native runner and
|
|
630
|
+
* the pi bridge) derive `ok` for such a tool through this one reader; bash
|
|
631
|
+
* keeps `parseExitPrefix`; a tool relaying content it did not write is never
|
|
632
|
+
* read this way. Ordinary output that merely contains the word later on is a
|
|
633
|
+
* success. */
|
|
634
|
+
export function toolTextFailed(output: string): boolean {
|
|
635
|
+
return /^\s*error:/i.test(stripAnsi(output));
|
|
636
|
+
}
|
|
637
|
+
|
|
625
638
|
/** Upper bound on a `tool_result.output` — large enough for a test run or a
|
|
626
639
|
* diff to read in full on the run page, small enough that a run of ordinary
|
|
627
640
|
* length fits the registry's backlog whole (the backlog is bounded by count AND
|
package/dist/cli.js
CHANGED
|
@@ -408,8 +408,11 @@ var init_store = __esm({
|
|
|
408
408
|
const res = await this.fetchImpl(signed);
|
|
409
409
|
if (res.status === 404) return null;
|
|
410
410
|
if (!res.ok) throw new Error(`artifact store: HEAD ${key} answered HTTP ${res.status}`);
|
|
411
|
-
const
|
|
412
|
-
|
|
411
|
+
const length = res.headers.get("content-length");
|
|
412
|
+
const size = length === null ? Number.NaN : Number(length);
|
|
413
|
+
if (!Number.isFinite(size) || size < 0) {
|
|
414
|
+
throw new Error(`artifact store: HEAD ${key} answered HTTP ${res.status} without a length`);
|
|
415
|
+
}
|
|
413
416
|
return { size, contentType: res.headers.get("content-type") ?? "application/octet-stream" };
|
|
414
417
|
}
|
|
415
418
|
async get(key) {
|
|
@@ -7993,6 +7996,9 @@ function parseExitPrefix(output) {
|
|
|
7993
7996
|
if (!m) return { failed: false, exitCode: 0 };
|
|
7994
7997
|
return /^\d+$/.test(m[1]) ? { failed: true, exitCode: Number(m[1]) } : { failed: true };
|
|
7995
7998
|
}
|
|
7999
|
+
function toolTextFailed(output) {
|
|
8000
|
+
return /^\s*error:/i.test(stripAnsi(output));
|
|
8001
|
+
}
|
|
7996
8002
|
function capClean(clean) {
|
|
7997
8003
|
const text = clean.trim();
|
|
7998
8004
|
if (text.length <= TOOL_OUTPUT_CAP) return text;
|
|
@@ -25933,7 +25939,11 @@ function fileNameOf2(path) {
|
|
|
25933
25939
|
return segments2[segments2.length - 1] ?? path;
|
|
25934
25940
|
}
|
|
25935
25941
|
function putCommandFor(path, contentType, url) {
|
|
25936
|
-
return `curl -fsS -T ${shellQuote(path)} -H ${shellQuote(`Content-Type: ${contentType}`)} ${shellQuote(url)}`;
|
|
25942
|
+
return `curl -fsS -T ${shellQuote(path)} -H ${shellQuote(`Content-Type: ${contentType}`)} -o /dev/null -w ${shellQuote(PUT_REPORT_FORMAT)} ${shellQuote(url)}`;
|
|
25943
|
+
}
|
|
25944
|
+
function parsePutReport(output) {
|
|
25945
|
+
const m = /(\d{3}) (\d{1,15})\s*$/.exec(output);
|
|
25946
|
+
return m ? { httpCode: Number(m[1]), sentBytes: Number(m[2]) } : null;
|
|
25937
25947
|
}
|
|
25938
25948
|
function postCommandFor(path, url) {
|
|
25939
25949
|
return `curl -fsS --upload-file ${shellQuote(path)} -X POST ${shellQuote(url)}`;
|
|
@@ -25968,11 +25978,14 @@ async function attachThroughStore(path, name, lead, artifacts, ctx) {
|
|
|
25968
25978
|
if (parseExitPrefix(putOut).failed) {
|
|
25969
25979
|
return `error: the upload of ${name} to the artifact store failed: ${putOut.trim()}`;
|
|
25970
25980
|
}
|
|
25981
|
+
const report2 = parsePutReport(putOut);
|
|
25982
|
+
const curlSaid = report2 ? `; curl reported HTTP ${report2.httpCode} after sending ${report2.sentBytes} bytes` : "";
|
|
25971
25983
|
const head = await artifacts.store.head(key);
|
|
25972
|
-
if (!head)
|
|
25973
|
-
return `error: the artifact store holds nothing under ${key} after the upload of ${name} \u2014 nothing was posted`;
|
|
25984
|
+
if (!head) {
|
|
25985
|
+
return `error: the artifact store holds nothing under ${key} after the upload of ${name}${curlSaid} \u2014 nothing was posted`;
|
|
25986
|
+
}
|
|
25974
25987
|
if (head.size !== size) {
|
|
25975
|
-
return `error: the artifact store holds ${head.size} bytes for ${name}, not the ${size} measured \u2014 nothing was posted`;
|
|
25988
|
+
return `error: the artifact store holds ${head.size} bytes for ${name}, not the ${size} measured${curlSaid} \u2014 nothing was posted`;
|
|
25976
25989
|
}
|
|
25977
25990
|
ctx.publish?.({ type: "artifact", direction: "out", key, name, size, contentType });
|
|
25978
25991
|
if (!ctx.uploadTicket) {
|
|
@@ -26000,7 +26013,7 @@ async function attachThroughStore(path, name, lead, artifacts, ctx) {
|
|
|
26000
26013
|
}
|
|
26001
26014
|
return `attached ${name} (${size} bytes) to the conversation and the run page`;
|
|
26002
26015
|
}
|
|
26003
|
-
var MAX_ARTIFACT_BYTES, TRANSFER_FLOOR_BYTES_PER_MS, TRANSFER_SETUP_MS, STAT_TIMEOUT_MS, describe2, secs, attachFileTool;
|
|
26016
|
+
var MAX_ARTIFACT_BYTES, TRANSFER_FLOOR_BYTES_PER_MS, TRANSFER_SETUP_MS, STAT_TIMEOUT_MS, describe2, secs, PUT_REPORT_FORMAT, attachFileTool;
|
|
26004
26017
|
var init_attach = __esm({
|
|
26005
26018
|
"../../src/tools/attach.ts"() {
|
|
26006
26019
|
"use strict";
|
|
@@ -26016,8 +26029,10 @@ var init_attach = __esm({
|
|
|
26016
26029
|
STAT_TIMEOUT_MS = 3e4;
|
|
26017
26030
|
describe2 = (err2) => err2 instanceof Error ? err2.message : String(err2);
|
|
26018
26031
|
secs = (ms) => Math.max(0, Math.round(ms / 1e3));
|
|
26032
|
+
PUT_REPORT_FORMAT = "\\n%{http_code} %{size_upload}";
|
|
26019
26033
|
attachFileTool = {
|
|
26020
26034
|
name: "attach_file",
|
|
26035
|
+
failsInText: true,
|
|
26021
26036
|
description: `Post a file from the workspace into the conversation so the person sees it inline \u2014 a screenshot (e.g. from \`playwright screenshot\`), a rendered PDF, a recording, a log. Use it whenever you produce an image worth showing: a link to a file is not a picture. Whole files only: up to ${MAX_ARTIFACT_BYTES} bytes (1 GiB) where the artifact store is configured, ${MAX_READ_BYTES} bytes otherwise \u2014 the result says which.`,
|
|
26022
26037
|
inputSchema: {
|
|
26023
26038
|
type: "object",
|
|
@@ -26557,6 +26572,7 @@ var init_runs2 = __esm({
|
|
|
26557
26572
|
CHILDREN_PAGES_MAX = 25;
|
|
26558
26573
|
spawnRunTool = {
|
|
26559
26574
|
name: "spawn_run",
|
|
26575
|
+
failsInText: true,
|
|
26560
26576
|
description: `Start a child run as the person who asked you: an ordinary Switchboard run of the named preset, in a thread of its own in this channel, under their permissions \u2014 what they could start by hand with \`agent:<preset>\`. The child is a reader of this conversation: it starts from this conversation's text so far (every user and assistant turn, never your tool calls or their results) plus \`prompt\`, its one new turn \u2014 so say what it should do. Presets a child can run: ${CHILD_PRESETS().join(", ")}; a preset that writes (${WRITE_PRESETS().join(", ")}) is refused \`spawn_identity\`. \`repo\` (owner/name) for a preset that works in a repository (${REPO_PRESETS().join(", ")}); \`budget\` narrows the child's wall clock in whole minutes (at least 2; it is also capped by what is left of yours). Returns the child's run id, its thread and a link, or a refusal by name: a preset or repository the requester may not use, a boundary the child's profile exceeds, the fan-out cap, or a channel that cannot open a thread. A child cannot spawn children.`,
|
|
26561
26577
|
inputSchema: {
|
|
26562
26578
|
type: "object",
|
|
@@ -26599,6 +26615,7 @@ var init_runs2 = __esm({
|
|
|
26599
26615
|
};
|
|
26600
26616
|
sendToRunTool = {
|
|
26601
26617
|
name: "send_to_run",
|
|
26618
|
+
failsInText: true,
|
|
26602
26619
|
description: "Steer one of your live children: `text` reaches it as a follow-up at its next step, the way a reply in its thread would \u2014 sent as the person who asked you, and recorded on the child's run page as coming from this run. Use it when the request changed or a child is heading the wrong way. Refused by name: `not_found` (an unknown id, or a run the requester may not read), `not_child` (a run you did not spawn), `not_live` (it already ended \u2014 its status is given, nothing is sent; get_run_status has its reply), or the child's agent's allowlist. A child that finishes before its next step never reads it.",
|
|
26603
26620
|
inputSchema: {
|
|
26604
26621
|
type: "object",
|
|
@@ -26638,6 +26655,7 @@ var init_runs2 = __esm({
|
|
|
26638
26655
|
AWAIT_IDS_MAX = 50;
|
|
26639
26656
|
awaitRunsTool = {
|
|
26640
26657
|
name: "await_runs",
|
|
26658
|
+
failsInText: true,
|
|
26641
26659
|
description: "Wait for runs \u2014 normally your children \u2014 to end, and get each one's end as data: its terminal status (completed, failed, refused, stopped_soft, stopped_hard, interrupted) with its final reply wrapped as untrusted content; `running` for one still live when the wait was cut; `not_found` for an unknown id or one the requester may not read. The wait ends at the first of: every named run ended; `timeoutMinutes` (optional, whole minutes); the edge of your own budget (a minute before your clock runs out \u2014 write up what came back and name what is still running, which keeps running); a stop; a follow-up landing in this thread (it rides your next turn). `ended` says which. An interrupted child is reported, never restarted. A child is its thread: when a person's reply in a child's thread started a later run there, the child's row follows that run \u2014 `continuedBy` names it, and `status` and `finalReply` are its \u2014 never the child's first reply alone.",
|
|
26642
26660
|
inputSchema: {
|
|
26643
26661
|
type: "object",
|
|
@@ -27037,6 +27055,7 @@ ${raw.trim()}`;
|
|
|
27037
27055
|
};
|
|
27038
27056
|
submitVerdictTool = {
|
|
27039
27057
|
name: "submit_verdict",
|
|
27058
|
+
failsInText: true,
|
|
27040
27059
|
description: "Record your review verdict. REQUIRED before your final message when reviewing a PR: `approve` when there are no blocking issues (nits alone are not blocking), `request_changes` otherwise. Switchboard writes the verdict as the first line of the GitHub comment itself (`LGTM:` only for approve); a review with no submitted verdict is posted as NOT approving. Call it once, after your analysis; a later call replaces the earlier one. `head` is the commit you reviewed \u2014 run `git rev-parse HEAD` in the checkout you read and tested and pass its output; Switchboard posts to the PR only if that commit IS the PR's head, so a review of the wrong branch can never land on a PR. Enumerate EVERY issue you report in `findings` with STABLE ids assigned in order (F1, F2, \u2026) \u2014 a fix round references findings by these ids, so never renumber them. Severity is exactly one of blocking|major|minor|nit; the entry carries the file (plus line when it points at one) and a one-line title, while the full explanation stays in your review text keyed by the same ids. An `approve` carrying a `blocking` finding is downgraded to `request_changes` \u2014 approve only when nothing blocking remains.",
|
|
27041
27060
|
inputSchema: {
|
|
27042
27061
|
type: "object",
|
|
@@ -27087,6 +27106,7 @@ ${raw.trim()}`;
|
|
|
27087
27106
|
};
|
|
27088
27107
|
submitDispositionsTool = {
|
|
27089
27108
|
name: "submit_dispositions",
|
|
27109
|
+
failsInText: true,
|
|
27090
27110
|
description: "Record one disposition per review finding after addressing them: `fixed` (the finding is addressed in your pushed code) or `declined` (deliberately not doing it \u2014 the note says why). `findingId` is the finding's stable id from the review (F1, F2, \u2026) \u2014 use exactly those ids; an id the review never issued answers nothing and is dropped when the plan runner reads your record. Every finding gets exactly one entry, every severity included (nits too). Call it once with the complete set after your last push; a later call replaces the earlier one. The set rides this run's record, where the plan runner reads it for the re-review.",
|
|
27091
27111
|
inputSchema: {
|
|
27092
27112
|
type: "object",
|
|
@@ -27118,6 +27138,7 @@ ${raw.trim()}`;
|
|
|
27118
27138
|
};
|
|
27119
27139
|
submitPrDescriptionTool = {
|
|
27120
27140
|
name: "submit_pr_description",
|
|
27141
|
+
failsInText: true,
|
|
27121
27142
|
description: "Submit the PR description as a typed object. REQUIRED after pushing your branch: Switchboard renders the GitHub PR body from this object at the pushed head and opens (or updates) the pull request itself \u2014 never open a PR yourself. Fields map 1:1 to the rendered sections; `title` becomes the PR's title; tour anchors are (path, from, to) line ranges at your pushed head. Call it after your last push; if you push again afterwards, call it again \u2014 the last valid call wins. An invalid object returns an error naming the field to fix; correct it and resubmit.",
|
|
27122
27143
|
inputSchema: {
|
|
27123
27144
|
type: "object",
|
|
@@ -27211,6 +27232,7 @@ ${raw.trim()}`;
|
|
|
27211
27232
|
});
|
|
27212
27233
|
submitHandoffTool = {
|
|
27213
27234
|
name: "submit_handoff",
|
|
27235
|
+
failsInText: true,
|
|
27214
27236
|
description: "Submit the unit handoff as a typed object \u2014 REQUIRED when your first user turn carries a `## Contract` block: once, after submit_pr_description and before your final message. `deviations`: where you departed from the unit as written (from, to, why); `followUps`: what you found and did not do, and where it belongs (what, where); `unproven`: which of the unit's test scenarios or criteria you could not prove, and why (criterion, why). Switchboard records it on the run and posts it to the unit's board issue, where a person decides each row. Submit empty lists when there is nothing to say \u2014 never skip it. A later call replaces the earlier one; an invalid object returns an error naming the field to fix.",
|
|
27215
27237
|
inputSchema: {
|
|
27216
27238
|
type: "object",
|
|
@@ -27994,17 +28016,17 @@ ${stagedLine}` : prompt }];
|
|
|
27994
28016
|
try {
|
|
27995
28017
|
const output = await untilHardStop(tool.run(tu.input ?? {}, ctx));
|
|
27996
28018
|
const text2 = toolResultText(output);
|
|
27997
|
-
const exit = tu.name === "bash" ? parseExitPrefix(text2) :
|
|
28019
|
+
const exit = tu.name === "bash" ? parseExitPrefix(text2) : { failed: tool.failsInText === true && toolTextFailed(text2) };
|
|
27998
28020
|
emit({
|
|
27999
28021
|
type: "tool_result",
|
|
28000
28022
|
tool: tu.name,
|
|
28001
|
-
ok: !exit
|
|
28023
|
+
ok: !exit.failed,
|
|
28002
28024
|
callId: tu.id,
|
|
28003
|
-
...exit
|
|
28025
|
+
...exit.exitCode !== void 0 ? { exitCode: exit.exitCode } : {},
|
|
28004
28026
|
...prepareToolResult(text2),
|
|
28005
28027
|
...spanId
|
|
28006
28028
|
});
|
|
28007
|
-
settle2(!exit
|
|
28029
|
+
settle2(!exit.failed, exit.exitCode !== void 0 ? { exitCode: exit.exitCode } : {});
|
|
28008
28030
|
return { type: "tool_result", toolUseId: tu.id, content: capToolResultContent(output) };
|
|
28009
28031
|
} catch (err2) {
|
|
28010
28032
|
if (err2 instanceof HardStopError) throw err2;
|
|
@@ -31289,7 +31311,7 @@ var init_bridge2 = __esm({
|
|
|
31289
31311
|
const tool = open2?.tool ?? str5(event.toolName);
|
|
31290
31312
|
const isError = event.isError === true;
|
|
31291
31313
|
const text = piResultText(event.result);
|
|
31292
|
-
const exit = tool === "bash" ? piBashExit(text, isError) : { failed: isError
|
|
31314
|
+
const exit = tool === "bash" ? piBashExit(text, isError) : { failed: isError || this.deps.textFailing?.has(tool) === true && toolTextFailed(text) };
|
|
31293
31315
|
const ok2 = !exit.failed;
|
|
31294
31316
|
this.emit({
|
|
31295
31317
|
type: "tool_result",
|
|
@@ -31739,7 +31761,13 @@ async function runPiHarness(deps, run2) {
|
|
|
31739
31761
|
run2.onProgress?.(summary);
|
|
31740
31762
|
emit({ type: "run_note", kind, summary, ...mode ? { mode } : {} });
|
|
31741
31763
|
};
|
|
31742
|
-
const bridge = new PiBridge({
|
|
31764
|
+
const bridge = new PiBridge({
|
|
31765
|
+
emit,
|
|
31766
|
+
onProgress: run2.onProgress,
|
|
31767
|
+
agentSpan,
|
|
31768
|
+
clock: clock3,
|
|
31769
|
+
textFailing: new Set(run2.tools.filter((t) => t.failsInText).map((t) => t.name))
|
|
31770
|
+
});
|
|
31743
31771
|
let paths;
|
|
31744
31772
|
const remainingMs = run2.resume?.remainingMs ?? run2.agent.maxMinutes * 6e4;
|
|
31745
31773
|
const deadline = now() + remainingMs;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coreplane/switchboard",
|
|
3
|
-
"version": "1.220.
|
|
3
|
+
"version": "1.220.1",
|
|
4
4
|
"description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://openswitchboard.dev",
|