@coreplane/switchboard 1.218.0 → 1.220.0
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/assets/Dockerfile +16 -0
- package/dist/assets/config/config.example.yaml +4 -2
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +6 -3
- package/dist/assets/src/core/runEvents.ts +6 -0
- package/dist/assets/src/core/runFriction.ts +47 -1
- package/dist/assets/src/core/runRecord.ts +35 -0
- package/dist/cli.js +2352 -1551
- package/package.json +1 -1
package/dist/assets/Dockerfile
CHANGED
|
@@ -71,6 +71,22 @@ RUN apt-get update \
|
|
|
71
71
|
COPY --from=meat /go/bin/meat /usr/local/bin/meat
|
|
72
72
|
RUN command -v meat >/dev/null && meat -h >/dev/null 2>&1
|
|
73
73
|
|
|
74
|
+
# pi (docs/reference/specs/harness-pi.md item 12): the harness for a preset
|
|
75
|
+
# without a workspace (general, research, conductor) runs pi as a child of
|
|
76
|
+
# the bot itself, since such a run has no execution container. The same layer
|
|
77
|
+
# as the execution images', on this image's Node (the same 24 tag), installed
|
|
78
|
+
# globally as root before the switch to the bot's user, so `switchboard` finds
|
|
79
|
+
# it on PATH and cannot alter it; at an EXACT pin held equal across the three
|
|
80
|
+
# images by src/deploy/imagePiHarness.test.ts (the pnpm lesson: a floating tag
|
|
81
|
+
# would move the harness's protocol with the build date). Proven by the layer,
|
|
82
|
+
# so the BUILD fails, not a run. Dark until a deployment sets
|
|
83
|
+
# `harness: { general: pi }`: nothing here starts pi on its own, and a run's
|
|
84
|
+
# pi on this host holds none of pi's own shell or file tools.
|
|
85
|
+
RUN npm install -g @earendil-works/pi-coding-agent@0.85.1 \
|
|
86
|
+
&& npm cache clean --force \
|
|
87
|
+
&& pi --version | grep -qx '0.85.1' \
|
|
88
|
+
&& pi --help | grep -q -- '--mode <mode>'
|
|
89
|
+
|
|
74
90
|
# Non-root user; agents run bash with this user's (container-scoped) permissions.
|
|
75
91
|
RUN useradd -m -u 1001 switchboard
|
|
76
92
|
WORKDIR /app
|
|
@@ -320,8 +320,10 @@ workspaceDir: ./workspaces
|
|
|
320
320
|
# with the run's model-proxy bearer as its only key, every event bridged onto
|
|
321
321
|
# the run's own record, our tools and tool rules as its extension. Every
|
|
322
322
|
# preset declares `native` until the pi series moves it; a deployment moves a
|
|
323
|
-
# preset early here.
|
|
324
|
-
#
|
|
323
|
+
# preset early here. A preset with a workspace (coding, review, explore) runs
|
|
324
|
+
# pi in the run's container; a preset without one (general, research,
|
|
325
|
+
# conductor) runs it as a child of the bot, with none of pi's own shell or
|
|
326
|
+
# file tools. A preset the registry does not know, or a value that is
|
|
325
327
|
# neither harness, fails the load by name.
|
|
326
328
|
# harness:
|
|
327
329
|
# coding: pi
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.220.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.220.0",
|
|
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.
|
|
16713
|
+
"version": "1.220.0",
|
|
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.
|
|
3
|
+
"version": "1.220.0",
|
|
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
|
@@ -121,8 +121,9 @@ export interface AgentDef {
|
|
|
121
121
|
residentSystem?: string;
|
|
122
122
|
/** Which loop drives the preset's runs (`HARNESSES`): the native loop
|
|
123
123
|
* unless declared, and whatever a deployment's `harness.<preset>` says
|
|
124
|
-
* over that.
|
|
125
|
-
*
|
|
124
|
+
* over that. A preset with a workspace runs pi in the run's execution
|
|
125
|
+
* container; a preset without one (machine class `none`) runs it as a
|
|
126
|
+
* child of the bot, with none of pi's own tools. */
|
|
126
127
|
harness?: Harness;
|
|
127
128
|
}
|
|
128
129
|
|
|
@@ -435,6 +436,8 @@ TIME. Your budget is up to two hours — less when a boundary or the request's \
|
|
|
435
436
|
|
|
436
437
|
READ-ONLY: NEVER open a pull request, and never commit or push — no branch, no \`gh pr create\`, no PR or issue write of any kind. You hold a read credential and your job is to find out, not to change. If the investigation shows a change is needed, say exactly what and where in your write-up and point the user at \`agent:coding\`.
|
|
437
438
|
|
|
439
|
+
You cannot attach or post files: your whole answer is text. Never say a file is attached or below — name its path in the workspace and describe it (what it shows, its size) instead; a person who needs the file itself asks \`agent:coding\`, which can attach.
|
|
440
|
+
|
|
438
441
|
Maintain the user-facing status card with the update_status tool: post your plan as a checklist (○ pending) once you have it, and update items as they start (✱) and finish (✓ — only after they actually happened). Items are short outcomes ("Clone and install", "Time the full suite"), never commands.
|
|
439
442
|
|
|
440
443
|
${NOTEPAD}
|
|
@@ -600,7 +603,7 @@ const WORK_PRESETS = {
|
|
|
600
603
|
explore: {
|
|
601
604
|
name: "explore",
|
|
602
605
|
description:
|
|
603
|
-
"
|
|
606
|
+
"Read-only investigation of a repository in a cold sandbox: runs builds, suites and pipelines, searches the web, and reports a claim table with commands and numbers. Never opens a PR; cannot attach or post files.",
|
|
604
607
|
system: EXPLORE_SYSTEM,
|
|
605
608
|
toolset: "explore",
|
|
606
609
|
// Always a cold per-thread sandbox with the checkout, never the resident a
|
|
@@ -71,6 +71,11 @@ export type RunNoteKind =
|
|
|
71
71
|
* executor's bounded wait (docs/reference/specs/execution.md item 14). Capacity, not a
|
|
72
72
|
* dead sandbox: the run goes on and the model is told to retry or finish. */
|
|
73
73
|
| "fleet_busy"
|
|
74
|
+
/** The sandbox restarted under the run and came back (docs/reference/specs/
|
|
75
|
+
* resident-repos.md item 65): the executor waited for the resident's wake
|
|
76
|
+
* and re-attached; the interrupted call was settled with a synthetic
|
|
77
|
+
* result (run-loop.md item 19) and the run goes on. */
|
|
78
|
+
| "sandbox_restarted"
|
|
74
79
|
| "stop_requested"
|
|
75
80
|
| "stopped"
|
|
76
81
|
/** Setup spans the request's stream sink had to drop before this run was
|
|
@@ -154,6 +159,7 @@ export const RUN_NOTE_KINDS = [
|
|
|
154
159
|
"turn_budget_exhausted",
|
|
155
160
|
"sandbox_dead",
|
|
156
161
|
"fleet_busy",
|
|
162
|
+
"sandbox_restarted",
|
|
157
163
|
"stop_requested",
|
|
158
164
|
"stopped",
|
|
159
165
|
"spans_dropped",
|
|
@@ -38,7 +38,8 @@ export type FrictionCategory =
|
|
|
38
38
|
| "setup_install"
|
|
39
39
|
| "wrap_up"
|
|
40
40
|
| "budget_hit"
|
|
41
|
-
| "infra_failure"
|
|
41
|
+
| "infra_failure"
|
|
42
|
+
| "unkept_promise";
|
|
42
43
|
|
|
43
44
|
export const FRICTION_CATEGORIES: readonly FrictionCategory[] = [
|
|
44
45
|
"slow_tool",
|
|
@@ -49,6 +50,7 @@ export const FRICTION_CATEGORIES: readonly FrictionCategory[] = [
|
|
|
49
50
|
"wrap_up",
|
|
50
51
|
"budget_hit",
|
|
51
52
|
"infra_failure",
|
|
53
|
+
"unkept_promise",
|
|
52
54
|
];
|
|
53
55
|
|
|
54
56
|
/** Human labels — the verdict line, the report's table and its finding lines. */
|
|
@@ -61,6 +63,7 @@ export const CATEGORY_LABEL: Record<FrictionCategory, string> = {
|
|
|
61
63
|
wrap_up: "agent wind-down",
|
|
62
64
|
budget_hit: "budget hits",
|
|
63
65
|
infra_failure: "infra failures",
|
|
66
|
+
unkept_promise: "unkept promises",
|
|
64
67
|
};
|
|
65
68
|
|
|
66
69
|
/** What a category's time is a share OF: tool time for the categories whose
|
|
@@ -75,8 +78,17 @@ export const DENOMINATOR_OF: Record<FrictionCategory, "tool" | "run"> = {
|
|
|
75
78
|
wrap_up: "run",
|
|
76
79
|
budget_hit: "run",
|
|
77
80
|
infra_failure: "run",
|
|
81
|
+
unkept_promise: "run",
|
|
78
82
|
};
|
|
79
83
|
|
|
84
|
+
/** The ways a reply tells the person a file is attached — the promise `unkept_promise` holds a run to.
|
|
85
|
+
* One deliberate expression, case-insensitive: "attached below/here/above", "see (the) attached",
|
|
86
|
+
* "I've/I have attached", "is/are attached", "attachment(s) (is/are) below/here". A path such as
|
|
87
|
+
* `./attachments/1-clip.mp4` or the bare word "attachments/" names a place, not a promise, and
|
|
88
|
+
* does not match; nor does prose about attaching in an `assistant` turn — only the `answer` counts. */
|
|
89
|
+
export const ATTACHMENT_PROMISE =
|
|
90
|
+
/\b(?:attached (?:below|here|above)|see (?:the )?attached|i(?:'ve| have) attached|(?:is|are) attached\b|attachments? (?:(?:is|are) )?(?:below|here))/i;
|
|
91
|
+
|
|
80
92
|
export type FrictionSeverity = "low" | "medium" | "high";
|
|
81
93
|
|
|
82
94
|
export interface FrictionFinding {
|
|
@@ -382,6 +394,9 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
|
|
|
382
394
|
let sideFactEvents = 0; // skill_use / review_artifact / pr_description / pr_opened / review_posted / ship_round / route: facts about the run, not steps
|
|
383
395
|
let spanEvents = 0; // span_start / span_end (docs/reference/specs/tracing.md): timing records, not steps
|
|
384
396
|
let wrapUp: { index: number; at?: number } | undefined;
|
|
397
|
+
// The reply and the files the run sent out: what `unkept_promise` compares after the loop.
|
|
398
|
+
let answer: { index: number; text: string } | undefined;
|
|
399
|
+
let outboundArtifacts = 0;
|
|
385
400
|
events.forEach((ev, index) => {
|
|
386
401
|
flushTurns(index);
|
|
387
402
|
if (isSpanRecord(ev)) {
|
|
@@ -390,8 +405,10 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
|
|
|
390
405
|
}
|
|
391
406
|
if (isNarrative(ev)) {
|
|
392
407
|
narrativeEvents++; // the narrative and `run_meta` are neither steps nor findings
|
|
408
|
+
if (ev.type === "answer") answer = { index, text: ev.text };
|
|
393
409
|
return;
|
|
394
410
|
}
|
|
411
|
+
if (ev.type === "artifact" && ev.direction === "out") outboundArtifacts++;
|
|
395
412
|
// Side facts about the run, not steps: skill_use rides beside a use_skill
|
|
396
413
|
// call that already produced its own tool pair, and artifact beside the
|
|
397
414
|
// attach_file call (or the dispatcher's staging) that moved the file;
|
|
@@ -545,6 +562,17 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
|
|
|
545
562
|
eventIndex: index,
|
|
546
563
|
});
|
|
547
564
|
return;
|
|
565
|
+
case "sandbox_restarted":
|
|
566
|
+
// The container rolled under the run and came back: the run went on,
|
|
567
|
+
// but the minutes spent waiting for the wake are friction the deploy
|
|
568
|
+
// window owns.
|
|
569
|
+
findings.push({
|
|
570
|
+
category: "infra_failure",
|
|
571
|
+
severity: "medium",
|
|
572
|
+
summary: `sandbox restarted: ${ev.summary}`,
|
|
573
|
+
eventIndex: index,
|
|
574
|
+
});
|
|
575
|
+
return;
|
|
548
576
|
}
|
|
549
577
|
});
|
|
550
578
|
// A slow turn whose produced event is past the stream (a turn that ended the run).
|
|
@@ -582,6 +610,24 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
|
|
|
582
610
|
f.interval = { start: at, end: Math.max(at, windowEnd) };
|
|
583
611
|
}
|
|
584
612
|
}
|
|
613
|
+
// The reply promised a file the run never produced: no tool failed (a preset without
|
|
614
|
+
// attach_file makes no call), so the answer's own words are the only witness. Once per
|
|
615
|
+
// run, anchored to the answer, no extent; the line that made the promise is quoted.
|
|
616
|
+
if (answer !== undefined && outboundArtifacts === 0) {
|
|
617
|
+
const promised = ATTACHMENT_PROMISE.exec(answer.text);
|
|
618
|
+
if (promised) {
|
|
619
|
+
const line =
|
|
620
|
+
answer.text.slice(0, promised.index).split("\n").pop()! + answer.text.slice(promised.index).split("\n")[0]!;
|
|
621
|
+
const trimmed = line.trim();
|
|
622
|
+
const quoted = trimmed.length > 120 ? `…${trimmed.slice(trimmed.length - 119)}` : trimmed;
|
|
623
|
+
findings.push({
|
|
624
|
+
category: "unkept_promise",
|
|
625
|
+
severity: "low",
|
|
626
|
+
summary: `unkept promise: the reply says a file is attached but the run produced none — "${quoted}"`,
|
|
627
|
+
eventIndex: answer.index,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
585
631
|
|
|
586
632
|
// ---- totals ----------------------------------------------------------------
|
|
587
633
|
let toolTimeMs: number | undefined;
|
|
@@ -158,6 +158,39 @@ export interface RunRecord {
|
|
|
158
158
|
* on records written before the session log existed and on runs without
|
|
159
159
|
* a conversation of their own. */
|
|
160
160
|
session?: RunSession;
|
|
161
|
+
/** The pull request the run's post-step opened or edited (item 2): the last
|
|
162
|
+
* `pr_opened` event, folded at the assembly. Present only on a run that
|
|
163
|
+
* reached one. A follow-up in the thread continues on its head branch
|
|
164
|
+
* (docs/reference/specs/resident-repos.md item 29) — read off the record,
|
|
165
|
+
* never off the reply's text. */
|
|
166
|
+
pr?: RunPullRequest;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** A pull request as the record names it (item 2): its number and its GitHub
|
|
170
|
+
* URL, both from the `pr_opened` event the coding post-step published. */
|
|
171
|
+
export interface RunPullRequest {
|
|
172
|
+
number: number;
|
|
173
|
+
url: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The pull request a run's events say it opened or edited — the last
|
|
177
|
+
* `pr_opened` wins, as an edit after an open names the same PR — or nothing. */
|
|
178
|
+
export function prOfEvents(events: readonly RunEvent[]): RunPullRequest | undefined {
|
|
179
|
+
let pr: RunPullRequest | undefined;
|
|
180
|
+
for (const e of events) if (e.type === "pr_opened") pr = { number: e.number, url: e.url };
|
|
181
|
+
return pr;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isRunPullRequestShape(v: unknown): v is RunPullRequest {
|
|
185
|
+
if (typeof v !== "object" || v === null) return false;
|
|
186
|
+
const p = v as Record<string, unknown>;
|
|
187
|
+
return (
|
|
188
|
+
typeof p.number === "number" &&
|
|
189
|
+
Number.isInteger(p.number) &&
|
|
190
|
+
p.number > 0 &&
|
|
191
|
+
typeof p.url === "string" &&
|
|
192
|
+
p.url.length > 0
|
|
193
|
+
);
|
|
161
194
|
}
|
|
162
195
|
|
|
163
196
|
/** The three places a run's conversation can start (item 52): the thread's
|
|
@@ -574,6 +607,8 @@ export function isRunRecord(v: unknown): v is RunRecord {
|
|
|
574
607
|
if (r.dispositions !== undefined && !isFindingDispositionsShape(r.dispositions)) return false;
|
|
575
608
|
if (r.reviewPost !== undefined && !isReviewPostShape(r.reviewPost)) return false;
|
|
576
609
|
if (r.profile !== undefined && !isRunProfileRecord(r.profile)) return false;
|
|
610
|
+
// The pull request the run reached (item 2): a positive integer and a URL, or absent.
|
|
611
|
+
if (r.pr !== undefined && !isRunPullRequestShape(r.pr)) return false;
|
|
577
612
|
// A parent is named by a run id (item 46): the same shape as the record's own.
|
|
578
613
|
if (r.parentRunId !== undefined && (typeof r.parentRunId !== "string" || !RUN_ID_PATTERN.test(r.parentRunId)))
|
|
579
614
|
return false;
|