@coreplane/switchboard 1.207.0 → 1.207.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.207.
|
|
3
|
+
"version": "1.207.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.207.
|
|
9
|
+
"version": "1.207.1",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"workspaces": [
|
|
12
12
|
"web",
|
|
@@ -18999,7 +18999,7 @@
|
|
|
18999
18999
|
},
|
|
19000
19000
|
"packages/switchboard": {
|
|
19001
19001
|
"name": "@coreplane/switchboard",
|
|
19002
|
-
"version": "1.207.
|
|
19002
|
+
"version": "1.207.1",
|
|
19003
19003
|
"license": "Apache-2.0",
|
|
19004
19004
|
"dependencies": {
|
|
19005
19005
|
"@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.207.
|
|
3
|
+
"version": "1.207.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
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
// dependencies are done. `done` is merged: a plan branch's pull request is the
|
|
25
25
|
// runner's to squash (the `merge` step, under `plan:merge`) once the review
|
|
26
26
|
// approved at its head and the checks are green, so its dependents start on a
|
|
27
|
-
// base that carries it
|
|
27
|
+
// base that carries it — or was found merged already by the machine's first
|
|
28
|
+
// `pr-check` (a re-issued plan whose unit a person, or an earlier attempt,
|
|
29
|
+
// merged), which ends the unit with no branch and no child; a unit that ended
|
|
30
|
+
// any other way — a refused merge, a
|
|
28
31
|
// cap, a stop — blocks its dependents, each told so as its own ending, and the
|
|
29
32
|
// plan finishes `failed` so the summary says which units are left for the
|
|
30
33
|
// plan's re-issue. A task string's ship branch waits for a person. Node-free:
|
|
@@ -252,7 +255,7 @@ function readRecordReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
252
255
|
}
|
|
253
256
|
|
|
254
257
|
function prCheckReturn(step: string, a: BotAnswer): StepReturn {
|
|
255
|
-
const { ok, state, prNumber, url, headSha, at } = a.body;
|
|
258
|
+
const { ok, state, prNumber, url, headSha, sha, mergedAt, at } = a.body;
|
|
256
259
|
if (ok === true && state === "none") return { type: "pr-check", step, pr: { state: "none" }, at };
|
|
257
260
|
if (ok === true && state === "open" && typeof prNumber === "number" && typeof url === "string")
|
|
258
261
|
return {
|
|
@@ -261,6 +264,17 @@ function prCheckReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
261
264
|
pr: { state: "open", prNumber, url, ...(typeof headSha === "string" ? { headSha } : {}) },
|
|
262
265
|
at,
|
|
263
266
|
};
|
|
267
|
+
// A merged pull request is read whole or not at all: the merge commit and
|
|
268
|
+
// the time are what the unit's ending and its report carry.
|
|
269
|
+
if (
|
|
270
|
+
ok === true &&
|
|
271
|
+
state === "merged" &&
|
|
272
|
+
typeof prNumber === "number" &&
|
|
273
|
+
typeof url === "string" &&
|
|
274
|
+
typeof sha === "string" &&
|
|
275
|
+
typeof mergedAt === "string"
|
|
276
|
+
)
|
|
277
|
+
return { type: "pr-check", step, pr: { state: "merged", prNumber, url, sha, mergedAt }, at };
|
|
264
278
|
throw new UnreadableAnswer("pr-check", a, "state");
|
|
265
279
|
}
|
|
266
280
|
|
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
// approve and the merge: one thread and one head branch per unit
|
|
17
17
|
// (`plan/<plan-id>/<unit-slug>`), every child a `dispatch()` run the bot
|
|
18
18
|
// starts as the requesting user, every step retry-safe under the key
|
|
19
|
-
// `<instance>:<unit>/<round>/<kind>`.
|
|
19
|
+
// `<instance>:<unit>/<round>/<kind>`. Its first step asks what already heads
|
|
20
|
+
// the branch: a pull request merged before the attempt reached the unit — a
|
|
21
|
+
// person's merge, or an earlier attempt's — ends the unit `merged` with no
|
|
22
|
+
// branch and no child, so a re-issued plan walks past its done units instead
|
|
23
|
+
// of aborting them; the same answer after a round ends it the same way, since
|
|
24
|
+
// a merge can land while a child runs. Nothing here reads a clock: the bot
|
|
20
25
|
// answers every step with its own `at`, and that is the machine's time. Nothing
|
|
21
26
|
// here carries a task's text or a thread's contents: a spawn's brief names the
|
|
22
27
|
// unit and the runs whose records the bot reads to compose the child's turn.
|
|
@@ -348,7 +353,12 @@ export type ChildFacts =
|
|
|
348
353
|
handoff?: boolean;
|
|
349
354
|
};
|
|
350
355
|
|
|
351
|
-
|
|
356
|
+
/** What heads the unit's branch on GitHub: nothing, an open pull request, or —
|
|
357
|
+
* with no open one — a merged one, `sha` the merge commit on the base. */
|
|
358
|
+
export type PrCheck =
|
|
359
|
+
| { state: "none" }
|
|
360
|
+
| { state: "open"; prNumber: number; url: string; headSha?: string }
|
|
361
|
+
| { state: "merged"; prNumber: number; url: string; sha: string; mergedAt: string };
|
|
352
362
|
|
|
353
363
|
/** What a step answered. Every bot answer carries `at`, the bot's clock — the machine's time. */
|
|
354
364
|
export type StepReturn =
|
|
@@ -366,11 +376,14 @@ export type StepReturn =
|
|
|
366
376
|
| { type: "sleep"; step: string };
|
|
367
377
|
|
|
368
378
|
/** How one unit's pipeline ended — the truthful vocabulary the in-process loop
|
|
369
|
-
* has, plus the merge's own: `merged` by the runner,
|
|
370
|
-
* person,
|
|
371
|
-
*
|
|
379
|
+
* has, plus the merge's own: `merged` by the runner, or found merged (`by:
|
|
380
|
+
* other` — a person's merge, or an earlier attempt's that died after it, so
|
|
381
|
+
* the runner merged nothing), `merge_ready` for a person, `merge_refused` by
|
|
382
|
+
* the guards; `interrupted` a child the ledger closed; `refused` a child the
|
|
383
|
+
* authorize stage never started. */
|
|
372
384
|
export type UnitEnding =
|
|
373
|
-
| { kind: "merged"; pr: PrRef; sha: string; reviewRounds: number }
|
|
385
|
+
| { kind: "merged"; by: "runner"; pr: PrRef; sha: string; reviewRounds: number }
|
|
386
|
+
| { kind: "merged"; by: "other"; pr: PrRef; sha: string; mergedAt: string; reviewRounds: number }
|
|
374
387
|
| { kind: "merge_ready"; pr: PrRef; reviewRounds: number }
|
|
375
388
|
| { kind: "merge_refused"; pr: PrRef; reason: string; reviewRounds: number }
|
|
376
389
|
| { kind: "round_cap"; maxRounds: number; reviewRounds: number }
|
|
@@ -411,6 +424,8 @@ export interface UnitPipelineInput {
|
|
|
411
424
|
}
|
|
412
425
|
|
|
413
426
|
type Phase =
|
|
427
|
+
/** Before anything is created: what already heads the branch — a merged pull request ends the unit here. */
|
|
428
|
+
| { at: "pre-check" }
|
|
414
429
|
| { at: "branch" }
|
|
415
430
|
| { at: "spawn"; round: RoundRef; busy: number }
|
|
416
431
|
| { at: "busy-wait"; round: RoundRef; runId?: string; n: number }
|
|
@@ -467,7 +482,7 @@ export function openUnitPipeline(input: UnitPipelineInput, at: number): UnitPipe
|
|
|
467
482
|
input,
|
|
468
483
|
startedAt: at,
|
|
469
484
|
clock: at,
|
|
470
|
-
phase: { at: "
|
|
485
|
+
phase: { at: "pre-check" },
|
|
471
486
|
reviewRounds: 0,
|
|
472
487
|
findingsByRound: {},
|
|
473
488
|
dispositionsByRound: {},
|
|
@@ -531,6 +546,8 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
531
546
|
const unit = s.input.unit.id;
|
|
532
547
|
const p = s.phase;
|
|
533
548
|
switch (p.at) {
|
|
549
|
+
case "pre-check":
|
|
550
|
+
return { type: "pr-check", step: `${unit}/pr-check` };
|
|
534
551
|
case "branch":
|
|
535
552
|
return { type: "branch", step: `${unit}/branch`, branch: s.input.unit.branch, from: s.input.base };
|
|
536
553
|
case "spawn": {
|
|
@@ -753,9 +770,30 @@ function settleReview(
|
|
|
753
770
|
return enterRound(next, { index: round.index, kind: "fix" }, notes);
|
|
754
771
|
}
|
|
755
772
|
|
|
773
|
+
/** The unit's ending when its pull request is found merged — by a person, or
|
|
774
|
+
* by an earlier attempt of the plan that died after its merge: the unit is
|
|
775
|
+
* done and its dependents run on a base that carries it, and the runner
|
|
776
|
+
* merged nothing, so the ending says so (`by: other`). */
|
|
777
|
+
function foundMerged(
|
|
778
|
+
s: UnitPipelineState,
|
|
779
|
+
pr: Extract<PrCheck, { state: "merged" }>,
|
|
780
|
+
notes: CoordinatorNote[] = [],
|
|
781
|
+
): Transition {
|
|
782
|
+
const ref: PrRef = { number: pr.prNumber, url: pr.url };
|
|
783
|
+
return end(
|
|
784
|
+
{ ...s, pr: ref },
|
|
785
|
+
{ kind: "merged", by: "other", pr: ref, sha: pr.sha, mergedAt: pr.mergedAt, reviewRounds: s.reviewRounds },
|
|
786
|
+
notes,
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
|
|
756
790
|
/** The pull request heading the branch after a coding or fix round. */
|
|
757
791
|
function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-check" }>, pr: PrCheck): Transition {
|
|
758
792
|
const { round } = phase;
|
|
793
|
+
// The merge landed during the round: the child found nothing left to ship
|
|
794
|
+
// (or shipped into a pull request a person merged under it). The round
|
|
795
|
+
// completed without a pull request of its own, and the unit is done.
|
|
796
|
+
if (pr.state === "merged") return foundMerged(s, pr, [roundNote(round, "completed")]);
|
|
759
797
|
if (pr.state === "none") {
|
|
760
798
|
const reason =
|
|
761
799
|
round.index === 0
|
|
@@ -817,6 +855,16 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
817
855
|
const clocked: UnitPipelineState = "at" in ret ? { ...s, clock: ret.at } : s;
|
|
818
856
|
const p = s.phase;
|
|
819
857
|
switch (p.at) {
|
|
858
|
+
case "pre-check": {
|
|
859
|
+
// A pull request merged before this attempt reached the unit — a
|
|
860
|
+
// person's merge, or an earlier attempt's that died after it — makes the
|
|
861
|
+
// unit done before a branch or a child: nothing to run. Anything else is
|
|
862
|
+
// round 0's to work on: an open pull request is rebased and re-described
|
|
863
|
+
// by the coding child and adopted at the round's own pr-check.
|
|
864
|
+
const r = ret as Extract<StepReturn, { type: "pr-check" }>;
|
|
865
|
+
if (r.pr.state === "merged") return foundMerged(clocked, r.pr);
|
|
866
|
+
return { state: { ...clocked, phase: { at: "branch" } }, notes: [] };
|
|
867
|
+
}
|
|
820
868
|
case "branch": {
|
|
821
869
|
const r = ret as Extract<StepReturn, { type: "branch" }>;
|
|
822
870
|
if (r.ok) return enterRound(clocked, { index: 0, kind: "coding" });
|
|
@@ -914,7 +962,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
|
|
|
914
962
|
case "merge": {
|
|
915
963
|
const r = ret as Extract<StepReturn, { type: "merge" }>;
|
|
916
964
|
if (r.outcome === "merged")
|
|
917
|
-
return end(clocked, { kind: "merged", pr: p.pr, sha: r.sha, reviewRounds: s.reviewRounds });
|
|
965
|
+
return end(clocked, { kind: "merged", by: "runner", pr: p.pr, sha: r.sha, reviewRounds: s.reviewRounds });
|
|
918
966
|
if (r.outcome === "refused")
|
|
919
967
|
return end(clocked, { kind: "merge_refused", pr: p.pr, reason: r.reason, reviewRounds: s.reviewRounds });
|
|
920
968
|
const waited = r.at - p.since;
|
|
@@ -1000,6 +1048,8 @@ export function renderUnitReport(s: UnitPipelineState): string {
|
|
|
1000
1048
|
const join = (parts: Array<string | undefined>) => parts.filter(Boolean).join("\n\n");
|
|
1001
1049
|
switch (e.kind) {
|
|
1002
1050
|
case "merged":
|
|
1051
|
+
if (e.by === "other")
|
|
1052
|
+
return `✅ Already merged: ${e.pr.url} (merge commit \`${e.sha.slice(0, 7)}\`, merged ${e.mergedAt}) — the pull request heading \`${s.input.unit.branch}\` was merged before this attempt reached it, by a person or by an earlier attempt of this plan; the runner merged nothing. The unit is done and its dependents start on a base that carries it.`;
|
|
1003
1053
|
return [
|
|
1004
1054
|
`✅ Merged after ${rounds}: ${e.pr.url} (squash \`${e.sha.slice(0, 7)}\`) — merged by the plan runner under \`plan:merge\`: the review approved at this head and the guards were green.`,
|
|
1005
1055
|
verdictLine,
|
package/dist/cli.js
CHANGED
|
@@ -21130,6 +21130,25 @@ async function findOpenPrByHead(repo, branch2) {
|
|
|
21130
21130
|
...first.head?.sha ? { headSha: first.head.sha } : {}
|
|
21131
21131
|
};
|
|
21132
21132
|
}
|
|
21133
|
+
async function findMergedPrByHead(repo, branch2) {
|
|
21134
|
+
const token = await requireToken();
|
|
21135
|
+
const owner = repo.split("/")[0];
|
|
21136
|
+
const head = encodeURIComponent(`${owner}:${branch2}`);
|
|
21137
|
+
const res = await fetch(
|
|
21138
|
+
`https://api.github.com/repos/${repo}/pulls?state=closed&head=${head}&sort=updated&direction=desc`,
|
|
21139
|
+
{ headers: apiHeaders(token), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS5) }
|
|
21140
|
+
);
|
|
21141
|
+
if (!res.ok) {
|
|
21142
|
+
const text = await res.text().catch(() => "");
|
|
21143
|
+
throw new Error(`PR lookup failed: HTTP ${res.status} ${redactAndCap(text, 300)}`);
|
|
21144
|
+
}
|
|
21145
|
+
const rows = await res.json();
|
|
21146
|
+
if (!Array.isArray(rows)) return null;
|
|
21147
|
+
const merged = rows.flatMap(
|
|
21148
|
+
(r) => typeof r.merged_at === "string" && typeof r.merge_commit_sha === "string" && /^[0-9a-f]{40}$/.test(r.merge_commit_sha) ? [{ number: r.number, htmlUrl: r.html_url, sha: r.merge_commit_sha, mergedAt: r.merged_at }] : []
|
|
21149
|
+
).sort((a, b) => a.mergedAt < b.mergedAt ? 1 : a.mergedAt > b.mergedAt ? -1 : 0);
|
|
21150
|
+
return merged[0] ?? null;
|
|
21151
|
+
}
|
|
21133
21152
|
async function openPullRequest(target) {
|
|
21134
21153
|
const existing = await findOpenPrByHead(target.repo, target.headBranch);
|
|
21135
21154
|
if (existing) {
|
|
@@ -37930,17 +37949,33 @@ async function prCheck(body, deps) {
|
|
|
37930
37949
|
const unit = await unitRowOf(deps, instance, body.unit);
|
|
37931
37950
|
if (!unit.ok) return unit.response;
|
|
37932
37951
|
const branch2 = unit.row?.branch ?? instance.branch;
|
|
37952
|
+
const remember = async (pr) => {
|
|
37953
|
+
if (unit.row && (unit.row.pr?.number !== pr.number || unit.row.pr.url !== pr.url))
|
|
37954
|
+
await deps.instances.putUnits([{ ...unit.row, pr }]);
|
|
37955
|
+
};
|
|
37933
37956
|
try {
|
|
37934
|
-
const
|
|
37935
|
-
if (
|
|
37936
|
-
|
|
37937
|
-
|
|
37957
|
+
const open = await deps.findOpenPrByHead(instance.repo, branch2);
|
|
37958
|
+
if (open) {
|
|
37959
|
+
await remember({ number: open.number, url: open.htmlUrl });
|
|
37960
|
+
return json(200, {
|
|
37961
|
+
ok: true,
|
|
37962
|
+
state: "open",
|
|
37963
|
+
prNumber: open.number,
|
|
37964
|
+
url: open.htmlUrl,
|
|
37965
|
+
...open.headSha !== void 0 ? { headSha: open.headSha } : {},
|
|
37966
|
+
at
|
|
37967
|
+
});
|
|
37968
|
+
}
|
|
37969
|
+
const merged = await deps.findMergedPrByHead(instance.repo, branch2);
|
|
37970
|
+
if (!merged) return json(200, { ok: true, state: "none", at });
|
|
37971
|
+
await remember({ number: merged.number, url: merged.htmlUrl });
|
|
37938
37972
|
return json(200, {
|
|
37939
37973
|
ok: true,
|
|
37940
|
-
state: "
|
|
37941
|
-
prNumber:
|
|
37942
|
-
url:
|
|
37943
|
-
|
|
37974
|
+
state: "merged",
|
|
37975
|
+
prNumber: merged.number,
|
|
37976
|
+
url: merged.htmlUrl,
|
|
37977
|
+
sha: merged.sha,
|
|
37978
|
+
mergedAt: merged.mergedAt,
|
|
37944
37979
|
at
|
|
37945
37980
|
});
|
|
37946
37981
|
} catch (err2) {
|
|
@@ -38923,6 +38958,7 @@ async function runBot() {
|
|
|
38923
38958
|
dispatch: (msg, io, opts) => dispatch(deps, msg, io, opts),
|
|
38924
38959
|
ioFor: (thread) => threadIoFor(thread),
|
|
38925
38960
|
findOpenPrByHead,
|
|
38961
|
+
findMergedPrByHead,
|
|
38926
38962
|
// The App's GitHub reads for the plan, the specs, the rules and a unit's
|
|
38927
38963
|
// board issue; the branch create; the reviews and the identity the merge
|
|
38928
38964
|
// gate's "the verdict stands" question is answered from.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coreplane/switchboard",
|
|
3
|
-
"version": "1.207.
|
|
3
|
+
"version": "1.207.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",
|