@coreplane/switchboard 1.231.0 → 1.232.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/deploy/cloudflare/knownLength.ts +43 -0
- package/dist/assets/deploy/cloudflare/worker.ts +10 -2
- package/dist/assets/deploy/cloudflare-resident/worker.ts +64 -20
- 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/core/coordinator/driver.ts +33 -2
- package/dist/assets/src/core/ship/coordinator.ts +20 -3
- package/dist/assets/src/execution/residentReuse.ts +18 -3
- package/dist/assets/web/dist/.vite/manifest.json +18 -18
- package/dist/assets/web/dist/assets/{ResidentDetailPage-Df6D2emM.js → ResidentDetailPage-V4K35SYX.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-B0yECe2z.js → ResidentsIndexPage-3HuYaDwD.js} +1 -1
- package/dist/assets/web/dist/assets/{RunRoutePage-D4FG8JbC.js → RunRoutePage-Dr867-Ha.js} +4 -4
- package/dist/assets/web/dist/assets/{RunsIndexPage-qxjCAVwQ.js → RunsIndexPage-D_lpXsr8.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-B0xQSZIS.js → ScheduledPage-dfrjQ5E9.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-D-h9p7Iv.js → StatusDot-DexKtvUC.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-BuDnXjL7.js → Tooltip-DGQ2gu9M.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-BjLBNLkl.js → dist-boTdDLF4.js} +1 -1
- package/dist/assets/web/dist/assets/{main-BRrMt0Kr.js → main-BHhLVMQh.js} +2 -2
- package/dist/cli.js +269 -197
- package/package.json +1 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The container's answer, re-framed so its length survives to the browser.
|
|
2
|
+
//
|
|
3
|
+
// The bot writes `Content-Length` on every whole-object artifact answer
|
|
4
|
+
// (src/channels/liveView.ts `serveArtifact`), and locally the header is there.
|
|
5
|
+
// Live, a `200` for a 31 MB video reached the browser with no length while the
|
|
6
|
+
// `206` answers for the same bytes kept theirs: somewhere past the container
|
|
7
|
+
// the passthrough became a stream of unknown length. A player that falls back
|
|
8
|
+
// from ranges to a plain read of such a stream cannot seek and, with
|
|
9
|
+
// `preload="metadata"` on a file whose index sits at the end, never finds it.
|
|
10
|
+
//
|
|
11
|
+
// The Workers runtime emits `Content-Length` for a body it knows the length
|
|
12
|
+
// of, and `FixedLengthStream` is how a body declares one: this re-wraps the
|
|
13
|
+
// container's answer through a pipe sized by the container's own header. The
|
|
14
|
+
// pipe also refuses a body of another length, so a short or long upstream
|
|
15
|
+
// fails the response instead of lying about it. Pure over the Response and a
|
|
16
|
+
// pipe factory, so it is tested here with a fake pipe and wired in worker.ts
|
|
17
|
+
// with `new FixedLengthStream(size)`.
|
|
18
|
+
|
|
19
|
+
/** `new FixedLengthStream(size)` in workerd; a fake in tests. */
|
|
20
|
+
export type LengthPipe = (size: number) => {
|
|
21
|
+
readable: ReadableStream<Uint8Array>;
|
|
22
|
+
writable: WritableStream<Uint8Array>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** A response that could carry a WebSocket (workerd's `Response` does). */
|
|
26
|
+
type MaybeSocket = Response & { webSocket?: unknown };
|
|
27
|
+
|
|
28
|
+
/** The same answer with its body declared at the length the container named.
|
|
29
|
+
* Untouched: an answer with no body, one whose `Content-Length` is missing
|
|
30
|
+
* or not a byte count, a zero-length one, and a WebSocket upgrade — each is
|
|
31
|
+
* either already known-length or not a byte stream at all. */
|
|
32
|
+
export function withKnownLength(res: Response, lengthPipe: LengthPipe): Response {
|
|
33
|
+
if (res.body === null || (res as MaybeSocket).webSocket) return res;
|
|
34
|
+
const header = res.headers.get("content-length");
|
|
35
|
+
if (header === null || !/^\d{1,15}$/.test(header)) return res;
|
|
36
|
+
const size = Number(header);
|
|
37
|
+
if (size === 0) return res;
|
|
38
|
+
const { readable, writable } = lengthPipe(size);
|
|
39
|
+
// The pipe's own failure (a body of another length) ends the readable with
|
|
40
|
+
// that error; nothing here swallows it into a clean end.
|
|
41
|
+
void res.body.pipeTo(writable).catch(() => undefined);
|
|
42
|
+
return new Response(readable, res);
|
|
43
|
+
}
|
|
@@ -47,6 +47,7 @@ import { systemClock } from "../../src/core/trace/clock.ts";
|
|
|
47
47
|
import { createTracer } from "../../src/core/trace/tracer.ts";
|
|
48
48
|
import { shimRoute, stripTraceContext, withTraceContext, workerLogSink } from "../../src/core/trace/workerTrace.ts";
|
|
49
49
|
import { COPY_PATH, handleArtifactsCopy } from "./artifactsCopy.ts";
|
|
50
|
+
import { withKnownLength } from "./knownLength.ts";
|
|
50
51
|
import type { ShipCoordinatorParams } from "./coordinator";
|
|
51
52
|
import { INSTANCE, INTERNAL } from "./shared";
|
|
52
53
|
|
|
@@ -414,6 +415,13 @@ async function recordFiring(env: Env, firing: ScheduleFiring): Promise<void> {
|
|
|
414
415
|
if (!res.ok) console.error(`[schedule] ${firing.schedule}: recording the firing failed — ${res.reason}`);
|
|
415
416
|
}
|
|
416
417
|
|
|
418
|
+
/** The container's answer with the length it named declared to the runtime
|
|
419
|
+
* (knownLength.ts): live, a whole-object artifact `200` lost its
|
|
420
|
+
* `Content-Length` between the container and the browser while the `206`s
|
|
421
|
+
* kept theirs, and a player that falls back to a plain read of a stream it
|
|
422
|
+
* cannot size never finds an index at the end of the file. */
|
|
423
|
+
const withLength = (res: Response): Response => withKnownLength(res, (size) => new FixedLengthStream(size));
|
|
424
|
+
|
|
417
425
|
export default {
|
|
418
426
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
419
427
|
const pathname = new URL(request.url).pathname;
|
|
@@ -425,7 +433,7 @@ export default {
|
|
|
425
433
|
// authorize call below); a caller cannot be allowed to speak it.
|
|
426
434
|
const inbound = stripRestartSubject(stripTraceContext(request));
|
|
427
435
|
const route = shimRoute(pathname);
|
|
428
|
-
if (route === undefined) return getContainer(env.SWITCHBOARD, INSTANCE).fetch(inbound);
|
|
436
|
+
if (route === undefined) return withLength(await getContainer(env.SWITCHBOARD, INSTANCE).fetch(inbound));
|
|
429
437
|
const root = tracer.start("bot-shim.fetch", { sinks: traceSinks, attrs: { route } });
|
|
430
438
|
try {
|
|
431
439
|
const forwarded = withTraceContext(inbound, root);
|
|
@@ -454,7 +462,7 @@ export default {
|
|
|
454
462
|
fetch: (input, init) => fetch(input, init),
|
|
455
463
|
lengthPipe: (size) => new FixedLengthStream(size),
|
|
456
464
|
})
|
|
457
|
-
: await getContainer(env.SWITCHBOARD, INSTANCE).fetch(forwarded);
|
|
465
|
+
: withLength(await getContainer(env.SWITCHBOARD, INSTANCE).fetch(forwarded));
|
|
458
466
|
root.end(res.status >= 500 ? "error" : "ok", { httpStatus: res.status });
|
|
459
467
|
return res;
|
|
460
468
|
} catch (err) {
|
|
@@ -1110,9 +1110,12 @@ const NO_REF_HINT_REASON: RefHintReason = { ownPr: null, refByDefault: false };
|
|
|
1110
1110
|
type RebindOutcome =
|
|
1111
1111
|
| { kind: "none"; binding: ThreadBinding }
|
|
1112
1112
|
| { kind: "refuse"; refused: RebindRefused }
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1113
|
+
| { kind: "rebound"; moved: ThreadBinding; rebound: Rebound; keepTree: false }
|
|
1114
|
+
/** The move was the record alone — a dirty tree already on the branch, no
|
|
1115
|
+
* checkout run — with the verdict's `note` for the log line. The attach
|
|
1116
|
+
* that follows must keep that tree (`keepTree`, the worktree step's word
|
|
1117
|
+
* for it): the promise the note makes is the attach's to keep. */
|
|
1118
|
+
| { kind: "rebound"; moved: ThreadBinding; rebound: Rebound; keepTree: true; note: string };
|
|
1116
1119
|
|
|
1117
1120
|
/** Result of one /op test/build execution. `ok` is the command's
|
|
1118
1121
|
* verdict — a failing test run is a RESULT with ok:false, never an error. */
|
|
@@ -4570,6 +4573,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4570
4573
|
refHint,
|
|
4571
4574
|
wantSha,
|
|
4572
4575
|
reuse,
|
|
4576
|
+
keepTree: rebind.keepTree === true,
|
|
4573
4577
|
slug,
|
|
4574
4578
|
t0,
|
|
4575
4579
|
facts,
|
|
@@ -4595,16 +4599,32 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4595
4599
|
* mirror mutex so no other attach of the thread touches the tree meanwhile.
|
|
4596
4600
|
* The rest of the attach then runs on the moved ref: the mirror fetch for
|
|
4597
4601
|
* the PR head (item 51) and item 17's stale check, which keeps a tree whose
|
|
4598
|
-
* HEAD is the ref's tip or a descendant of it.
|
|
4599
|
-
*
|
|
4600
|
-
*
|
|
4602
|
+
* HEAD is the ref's tip or a descendant of it. When the verdict moved the
|
|
4603
|
+
* record alone — a dirty tree with the branch already checked out — the
|
|
4604
|
+
* answer says so (`keepTree`) and the worktree step keeps that tree, dirt
|
|
4605
|
+
* included, instead of item 17's dirty wipe: the promise not to touch it is
|
|
4606
|
+
* the whole attach's. The binding records the move (`rebound`); the answer
|
|
4607
|
+
* carries it, or the named refusal, so the bot can say where the follow-up
|
|
4608
|
+
* runs and why. A refusal never fails the attach. */
|
|
4601
4609
|
private async rebindToOwnPr(
|
|
4602
4610
|
prior: ThreadBinding | undefined,
|
|
4603
4611
|
ownPr: OwnPr | null,
|
|
4604
4612
|
reuse: boolean,
|
|
4605
4613
|
defaultRef: string,
|
|
4606
4614
|
slug: string,
|
|
4607
|
-
): Promise<
|
|
4615
|
+
): Promise<
|
|
4616
|
+
| {
|
|
4617
|
+
binding: ThreadBinding | undefined;
|
|
4618
|
+
rebound?: Rebound;
|
|
4619
|
+
rebindRefused?: RebindRefused;
|
|
4620
|
+
/** The move was the record alone — the tree is dirty with the branch
|
|
4621
|
+
* already checked out — and the attach must keep that tree as it
|
|
4622
|
+
* stands: the worktree step's item 17 wipe of a dirty tree would
|
|
4623
|
+
* destroy the very work the rebind declined to touch. */
|
|
4624
|
+
keepTree?: true;
|
|
4625
|
+
}
|
|
4626
|
+
| ThreadErr
|
|
4627
|
+
> {
|
|
4608
4628
|
const plan = rebindPlan({ ownPr, reuse, binding: prior, defaultRef });
|
|
4609
4629
|
if (plan.kind === "none" || prior === undefined) return { binding: prior };
|
|
4610
4630
|
if (plan.kind === "refuse") return { binding: prior, rebindRefused: plan.refused };
|
|
@@ -4704,8 +4724,8 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4704
4724
|
const moved: ThreadBinding = { ...current, ref: again.to, rebound };
|
|
4705
4725
|
await this.ctx.storage.put(key, moved);
|
|
4706
4726
|
return verdict.checkout
|
|
4707
|
-
? { kind: "rebound", moved, rebound }
|
|
4708
|
-
: { kind: "rebound", moved, rebound, note: verdict.note };
|
|
4727
|
+
? { kind: "rebound", moved, rebound, keepTree: false }
|
|
4728
|
+
: { kind: "rebound", moved, rebound, keepTree: true, note: verdict.note };
|
|
4709
4729
|
}, ATTACH_MUTEX_WAIT_MS)
|
|
4710
4730
|
).value;
|
|
4711
4731
|
} catch (err) {
|
|
@@ -4723,10 +4743,16 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4723
4743
|
return { binding: prior, rebindRefused: outcome.refused };
|
|
4724
4744
|
}
|
|
4725
4745
|
const { moved, rebound } = outcome;
|
|
4746
|
+
if (!outcome.keepTree) {
|
|
4747
|
+
console.log(
|
|
4748
|
+
`attach ${prior.threadKey}: rebound ${rebound.from} → ${rebound.to} (the thread's own pull request #${rebound.pr})`,
|
|
4749
|
+
);
|
|
4750
|
+
return { binding: moved, rebound };
|
|
4751
|
+
}
|
|
4726
4752
|
console.log(
|
|
4727
|
-
`attach ${prior.threadKey}: rebound ${rebound.from} → ${rebound.to} (the thread's own pull request #${rebound.pr})
|
|
4753
|
+
`attach ${prior.threadKey}: rebound ${rebound.from} → ${rebound.to} (the thread's own pull request #${rebound.pr}) — ${outcome.note}`,
|
|
4728
4754
|
);
|
|
4729
|
-
return { binding: moved, rebound };
|
|
4755
|
+
return { binding: moved, rebound, keepTree: true };
|
|
4730
4756
|
}
|
|
4731
4757
|
|
|
4732
4758
|
/** The move when the thread's tree is gone (item 16): the binding's ref
|
|
@@ -4781,7 +4807,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4781
4807
|
};
|
|
4782
4808
|
const moved: ThreadBinding = { ...current, ref: plan.to, rebound };
|
|
4783
4809
|
await this.ctx.storage.put(threadBindingKey(current.threadKey), moved);
|
|
4784
|
-
return { kind: "rebound", moved, rebound };
|
|
4810
|
+
return { kind: "rebound", moved, rebound, keepTree: false };
|
|
4785
4811
|
}
|
|
4786
4812
|
|
|
4787
4813
|
/** The release's word on what the run pushed (`/detach` `pushed`, item
|
|
@@ -4809,6 +4835,10 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4809
4835
|
refHint: string | null;
|
|
4810
4836
|
wantSha: string | null;
|
|
4811
4837
|
reuse: boolean;
|
|
4838
|
+
/** The rebind moved the record alone over a dirty tree already on the
|
|
4839
|
+
* branch (item 16) and promised not to touch it: the worktree step keeps
|
|
4840
|
+
* the tree as it stands instead of item 17's dirty wipe. */
|
|
4841
|
+
keepTree: boolean;
|
|
4812
4842
|
slug: string;
|
|
4813
4843
|
t0: number;
|
|
4814
4844
|
facts: RepoFacts;
|
|
@@ -4820,7 +4850,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4820
4850
|
rebound?: Rebound;
|
|
4821
4851
|
rebindRefused?: RebindRefused;
|
|
4822
4852
|
}): Promise<AttachOk | ThreadErr> {
|
|
4823
|
-
const { threadKey, refHint, wantSha, reuse, slug, t0, facts, record, binding, mode, rollback } = input;
|
|
4853
|
+
const { threadKey, refHint, wantSha, reuse, keepTree, slug, t0, facts, record, binding, mode, rollback } = input;
|
|
4824
4854
|
const { rebound, rebindRefused } = input;
|
|
4825
4855
|
|
|
4826
4856
|
// Command-level token mint — before the lock so mint latency
|
|
@@ -4899,6 +4929,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4899
4929
|
const recreated = await this.ensureThreadWorktree(binding, sha, mode.originUrl, mode.modeSwitch, {
|
|
4900
4930
|
detached: target.kind === "sha",
|
|
4901
4931
|
reuse,
|
|
4932
|
+
keepTree,
|
|
4902
4933
|
});
|
|
4903
4934
|
return { sha, threadLockKey, recreated };
|
|
4904
4935
|
}, ATTACH_MUTEX_WAIT_MS);
|
|
@@ -5003,10 +5034,13 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5003
5034
|
* trees; a reusing attach (`opts.reuse`, item 66) keeps a readable tree
|
|
5004
5035
|
* exactly as it stands (the dirt and the HEAD are the resumed run's own
|
|
5005
5036
|
* work) and throws `ReuseRefusedError` for one it cannot keep, touching
|
|
5006
|
-
* nothing
|
|
5007
|
-
*
|
|
5008
|
-
*
|
|
5009
|
-
*
|
|
5037
|
+
* nothing; a provisioning attach whose own rebind moved the record alone
|
|
5038
|
+
* over a dirty tree already on the branch (`opts.keepTree`, item 16) keeps
|
|
5039
|
+
* that tree as it stands too — the dirt is why the rebind declined to touch
|
|
5040
|
+
* it — and provisions one that turns out not to be there. The decision is
|
|
5041
|
+
* the pure `decideWorktree`; this method measures the facts and runs the
|
|
5042
|
+
* verdict. Returns true when the tree was (re)created. MUST be called
|
|
5043
|
+
* holding the mirror mutex: the clone reads the mirror.
|
|
5010
5044
|
*
|
|
5011
5045
|
* "Dirty" is tracked-file dirt (`status --porcelain -uno`): untracked
|
|
5012
5046
|
* scratch files are the thread's own state and survive re-attach.
|
|
@@ -5029,8 +5063,11 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5029
5063
|
/** `detached`: the bound ref is gone from the mirror and `sha` is the
|
|
5030
5064
|
* expected commit it still holds (item 51) — the tree is checked out at
|
|
5031
5065
|
* that commit, detached, instead of at a branch. `reuse`: a resumed
|
|
5032
|
-
* run's attach (item 66): keep the tree as it stands, or refuse.
|
|
5033
|
-
|
|
5066
|
+
* run's attach (item 66): keep the tree as it stands, or refuse.
|
|
5067
|
+
* `keepTree`: this attach's rebind moved the record alone over a dirty
|
|
5068
|
+
* tree already on the branch (item 16): keep a readable tree as it
|
|
5069
|
+
* stands, provision one that is not there. */
|
|
5070
|
+
opts: { detached: boolean; reuse: boolean; keepTree: boolean } = { detached: false, reuse: false, keepTree: false },
|
|
5034
5071
|
): Promise<boolean> {
|
|
5035
5072
|
const wt = binding.worktreePath;
|
|
5036
5073
|
const threadDir = parentDir(wt);
|
|
@@ -5067,7 +5104,14 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5067
5104
|
}
|
|
5068
5105
|
}
|
|
5069
5106
|
}
|
|
5070
|
-
const decision = decideWorktree({
|
|
5107
|
+
const decision = decideWorktree({
|
|
5108
|
+
reuse: opts.reuse,
|
|
5109
|
+
keepTree: opts.keepTree,
|
|
5110
|
+
modeSwitch,
|
|
5111
|
+
sha,
|
|
5112
|
+
worktreePath: wt,
|
|
5113
|
+
facts,
|
|
5114
|
+
});
|
|
5071
5115
|
if (decision.kind === "refuse") throw new ReuseRefusedError(decision.why);
|
|
5072
5116
|
if (decision.kind === "reuse") return false;
|
|
5073
5117
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.232.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.232.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"workspaces": [
|
|
12
12
|
"web",
|
|
@@ -20032,7 +20032,7 @@
|
|
|
20032
20032
|
},
|
|
20033
20033
|
"packages/switchboard": {
|
|
20034
20034
|
"name": "@coreplane/switchboard",
|
|
20035
|
-
"version": "1.
|
|
20035
|
+
"version": "1.232.0",
|
|
20036
20036
|
"license": "Apache-2.0",
|
|
20037
20037
|
"dependencies": {
|
|
20038
20038
|
"@earendil-works/pi-ai": "0.85.1",
|
package/dist/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.232.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
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
settleUnit,
|
|
45
45
|
startUnit,
|
|
46
46
|
type ChildFacts,
|
|
47
|
+
type MergeReadyFacts,
|
|
47
48
|
type CoordinatorAction,
|
|
48
49
|
type PlanGraph,
|
|
49
50
|
type PlanUnitNode,
|
|
@@ -278,7 +279,13 @@ function prCheckReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
278
279
|
return {
|
|
279
280
|
type: "pr-check",
|
|
280
281
|
step,
|
|
281
|
-
pr: {
|
|
282
|
+
pr: {
|
|
283
|
+
state: "open",
|
|
284
|
+
prNumber,
|
|
285
|
+
url,
|
|
286
|
+
...(typeof headSha === "string" ? { headSha } : {}),
|
|
287
|
+
...(typeof a.body.autoMergeEnabled === "boolean" ? { autoMergeEnabled: a.body.autoMergeEnabled } : {}),
|
|
288
|
+
},
|
|
282
289
|
at,
|
|
283
290
|
};
|
|
284
291
|
// A merged pull request is read whole or not at all: the merge commit and
|
|
@@ -448,12 +455,36 @@ async function runUnit(
|
|
|
448
455
|
const body = { ...tag, index: note.index, agent: note.agent, outcome: note.outcome };
|
|
449
456
|
await step.do(`${unit}/note/${++notes}`, STEP_CONFIG, () => call(bot, "round", body));
|
|
450
457
|
} else {
|
|
458
|
+
// A merge_ready ending names the pull request as it is at the APPROVED
|
|
459
|
+
// head (agent-ship item 9): one more pr-check reads the facts fresh —
|
|
460
|
+
// auto-merge may have been switched on since the round's check, or it
|
|
461
|
+
// may already have fired, in which case the answer is `merged` and the
|
|
462
|
+
// report says so rather than naming a gate that has passed. An
|
|
463
|
+
// unreadable answer just leaves the facts out.
|
|
464
|
+
let endFacts: MergeReadyFacts | undefined;
|
|
465
|
+
if (note.ending.kind === "merge_ready") {
|
|
466
|
+
try {
|
|
467
|
+
const check = prCheckReturn(
|
|
468
|
+
`${unit}/end/pr-facts`,
|
|
469
|
+
answerOf(
|
|
470
|
+
"pr-check",
|
|
471
|
+
await step.do(`${unit}/end/pr-facts`, STEP_CONFIG, () => call(bot, "pr-check", tag)),
|
|
472
|
+
),
|
|
473
|
+
);
|
|
474
|
+
if (check.type === "pr-check" && check.pr.state === "merged")
|
|
475
|
+
endFacts = { merged: { sha: check.pr.sha, mergedAt: check.pr.mergedAt } };
|
|
476
|
+
else if (check.type === "pr-check" && check.pr.state === "open" && check.pr.autoMergeEnabled !== undefined)
|
|
477
|
+
endFacts = { autoMergeEnabled: check.pr.autoMergeEnabled };
|
|
478
|
+
} catch {
|
|
479
|
+
// the report simply omits the fact
|
|
480
|
+
}
|
|
481
|
+
}
|
|
451
482
|
// The last coding child's run is named so the bot can put its handoff
|
|
452
483
|
// — the deviations it recorded — on the unit's board issue beside the
|
|
453
484
|
// ending (agent-ship item 14).
|
|
454
485
|
const body = {
|
|
455
486
|
...tag,
|
|
456
|
-
ending: { kind: note.ending.kind, report: renderUnitReport(state) },
|
|
487
|
+
ending: { kind: note.ending.kind, report: renderUnitReport(state, endFacts) },
|
|
457
488
|
...(state.pr !== undefined ? { pr: state.pr } : {}),
|
|
458
489
|
...(state.lastCodingRunId !== undefined ? { codingRunId: state.lastCodingRunId } : {}),
|
|
459
490
|
};
|
|
@@ -443,7 +443,7 @@ export type ChildFacts =
|
|
|
443
443
|
* with no open one — a merged one, `sha` the merge commit on the base. */
|
|
444
444
|
export type PrCheck =
|
|
445
445
|
| { state: "none" }
|
|
446
|
-
| { state: "open"; prNumber: number; url: string; headSha?: string }
|
|
446
|
+
| { state: "open"; prNumber: number; url: string; headSha?: string; autoMergeEnabled?: boolean }
|
|
447
447
|
| { state: "merged"; prNumber: number; url: string; sha: string; mergedAt: string };
|
|
448
448
|
|
|
449
449
|
/** What a step answered. Every bot answer carries `at`, the bot's clock — the machine's time. */
|
|
@@ -1159,7 +1159,17 @@ function splitReport(s: UnitPipelineState): string {
|
|
|
1159
1159
|
|
|
1160
1160
|
/** The thread's report for a unit's ending — the ship pipeline's own words
|
|
1161
1161
|
* for the endings it has, and the merge's for the ones it gains. */
|
|
1162
|
-
|
|
1162
|
+
/** What the driver read at the approved head when it composed a `merge_ready`
|
|
1163
|
+
* ending (agent-ship item 9): the pull request's own auto-merge fact, or the
|
|
1164
|
+
* merge that already happened — auto-merge or a person can merge between the
|
|
1165
|
+
* approval and the ending, and the report must describe the pull request as
|
|
1166
|
+
* it is, never a gate that has already passed. */
|
|
1167
|
+
export interface MergeReadyFacts {
|
|
1168
|
+
autoMergeEnabled?: boolean;
|
|
1169
|
+
merged?: { sha: string; mergedAt: string };
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts): string {
|
|
1163
1173
|
const e = s.ending;
|
|
1164
1174
|
if (!e) return "";
|
|
1165
1175
|
const rounds = `${e.reviewRounds} review round${e.reviewRounds === 1 ? "" : "s"}`;
|
|
@@ -1189,7 +1199,14 @@ export function renderUnitReport(s: UnitPipelineState): string {
|
|
|
1189
1199
|
`✅ Merge-ready after ${rounds}: ${e.pr.url}`,
|
|
1190
1200
|
verdictLine,
|
|
1191
1201
|
declinedLine,
|
|
1192
|
-
|
|
1202
|
+
// What the driver read at the approved head when it composed this
|
|
1203
|
+
// ending (agent-ship item 9): a merge that already happened is named
|
|
1204
|
+
// as such, else the pull request's own auto-merge fact, else the gate.
|
|
1205
|
+
facts?.merged
|
|
1206
|
+
? `Already merged: ${e.pr.url} (merge commit \`${facts.merged.sha.slice(0, 7)}\`, merged ${facts.merged.mergedAt}) — auto-merge or a person merged it after the approval; the runner merged nothing.`
|
|
1207
|
+
: facts?.autoMergeEnabled
|
|
1208
|
+
? "Auto-merge is on for this pull request: the approval merges it once checks pass."
|
|
1209
|
+
: "Remaining gate: a person's merge — the runner merges only when the instance's `merge` field says runner, and ship never approves.",
|
|
1193
1210
|
].join("\n");
|
|
1194
1211
|
case "merge_refused":
|
|
1195
1212
|
return join([
|
|
@@ -16,7 +16,11 @@
|
|
|
16
16
|
* PROVISIONS (a fresh one). A reusing attach keeps a readable tree exactly
|
|
17
17
|
* as it stands, dirt and stale HEAD included, and refuses by name a tree it
|
|
18
18
|
* cannot keep (gone, unreadable, built for the other mode) without touching
|
|
19
|
-
* it. A provisioning attach keeps the dirty/stale discipline byte for byte
|
|
19
|
+
* it. A provisioning attach keeps the dirty/stale discipline byte for byte —
|
|
20
|
+
* with one exception it is told about (`keepTree`): when its own rebind onto
|
|
21
|
+
* the thread's pull request branch found the tree dirty with that branch
|
|
22
|
+
* already checked out and moved the record alone, the dirt is the thread's
|
|
23
|
+
* own work and the tree is kept as it stands. */
|
|
20
24
|
|
|
21
25
|
export type ParsedReuse = { reuse: boolean } | { error: string };
|
|
22
26
|
|
|
@@ -58,6 +62,15 @@ export type WorktreeDecision =
|
|
|
58
62
|
export function decideWorktree(input: {
|
|
59
63
|
/** True for a resumed run's attach: keep the tree, never wipe it. */
|
|
60
64
|
reuse: boolean;
|
|
65
|
+
/** True when this attach's rebind moved the binding onto the branch the
|
|
66
|
+
* tree already had checked out, dirty, and promised not to touch it
|
|
67
|
+
* (residentRebind.ts, the `checkout: false` verdict): the dirt is the
|
|
68
|
+
* thread's own uncommitted work on its own pull request branch, so a
|
|
69
|
+
* readable tree is kept as it stands — dirt and HEAD included, like a
|
|
70
|
+
* resumed run's — while a tree that turns out missing, unreadable or built
|
|
71
|
+
* for the other mode has nothing to keep and is provisioned like any fresh
|
|
72
|
+
* attach's. Absent on every attach that did not make that move. */
|
|
73
|
+
keepTree?: boolean;
|
|
61
74
|
/** The tree was built for the other mode (read-only against writable). */
|
|
62
75
|
modeSwitch: boolean;
|
|
63
76
|
/** The commit a provisioning attach checks out: the ref's tip, or the expected head. */
|
|
@@ -65,7 +78,7 @@ export function decideWorktree(input: {
|
|
|
65
78
|
worktreePath: string;
|
|
66
79
|
facts: WorktreeFacts;
|
|
67
80
|
}): WorktreeDecision {
|
|
68
|
-
const { reuse, modeSwitch, sha, worktreePath, facts } = input;
|
|
81
|
+
const { reuse, keepTree = false, modeSwitch, sha, worktreePath, facts } = input;
|
|
69
82
|
if (modeSwitch) {
|
|
70
83
|
return reuse
|
|
71
84
|
? {
|
|
@@ -92,7 +105,9 @@ export function decideWorktree(input: {
|
|
|
92
105
|
}
|
|
93
106
|
// A reusing attach judges nothing past readability: the dirt and the HEAD are the run's own state.
|
|
94
107
|
if (reuse) return { kind: "reuse" };
|
|
95
|
-
|
|
108
|
+
// The rebind promised a dirty tree on its own branch would not be touched;
|
|
109
|
+
// the dirt is the reason it made that promise, so it is not a reason to wipe.
|
|
110
|
+
if (facts.dirty) return keepTree ? { kind: "reuse" } : { kind: "recreate", why: "dirty" };
|
|
96
111
|
if (facts.head !== sha && facts.descendsFromTip !== true) return { kind: "recreate", why: "stale" };
|
|
97
112
|
return { kind: "reuse" };
|
|
98
113
|
}
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"src": "../node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-vietnamese-wght-normal.woff2"
|
|
29
29
|
},
|
|
30
30
|
"../node_modules/@pierre/diffs/dist/index.js": {
|
|
31
|
-
"file": "assets/dist-
|
|
31
|
+
"file": "assets/dist-boTdDLF4.js",
|
|
32
32
|
"name": "dist",
|
|
33
33
|
"src": "../node_modules/@pierre/diffs/dist/index.js",
|
|
34
34
|
"isDynamicEntry": true,
|
|
@@ -2562,16 +2562,16 @@
|
|
|
2562
2562
|
"_seed-DeKSHrt2.js"
|
|
2563
2563
|
]
|
|
2564
2564
|
},
|
|
2565
|
-
"_StatusDot-
|
|
2566
|
-
"file": "assets/StatusDot-
|
|
2565
|
+
"_StatusDot-DexKtvUC.js": {
|
|
2566
|
+
"file": "assets/StatusDot-DexKtvUC.js",
|
|
2567
2567
|
"name": "StatusDot",
|
|
2568
2568
|
"imports": [
|
|
2569
2569
|
"_seed-DeKSHrt2.js",
|
|
2570
|
-
"_Tooltip-
|
|
2570
|
+
"_Tooltip-DGQ2gu9M.js"
|
|
2571
2571
|
]
|
|
2572
2572
|
},
|
|
2573
|
-
"_Tooltip-
|
|
2574
|
-
"file": "assets/Tooltip-
|
|
2573
|
+
"_Tooltip-DGQ2gu9M.js": {
|
|
2574
|
+
"file": "assets/Tooltip-DGQ2gu9M.js",
|
|
2575
2575
|
"name": "Tooltip",
|
|
2576
2576
|
"imports": [
|
|
2577
2577
|
"_AppShell-lYVcj-k7.js",
|
|
@@ -2789,7 +2789,7 @@
|
|
|
2789
2789
|
]
|
|
2790
2790
|
},
|
|
2791
2791
|
"src/main.ts": {
|
|
2792
|
-
"file": "assets/main-
|
|
2792
|
+
"file": "assets/main-BHhLVMQh.js",
|
|
2793
2793
|
"name": "main",
|
|
2794
2794
|
"src": "src/main.ts",
|
|
2795
2795
|
"isEntry": true,
|
|
@@ -2841,19 +2841,19 @@
|
|
|
2841
2841
|
]
|
|
2842
2842
|
},
|
|
2843
2843
|
"src/pages/ResidentDetailPage.vue": {
|
|
2844
|
-
"file": "assets/ResidentDetailPage-
|
|
2844
|
+
"file": "assets/ResidentDetailPage-V4K35SYX.js",
|
|
2845
2845
|
"name": "ResidentDetailPage",
|
|
2846
2846
|
"src": "src/pages/ResidentDetailPage.vue",
|
|
2847
2847
|
"isDynamicEntry": true,
|
|
2848
2848
|
"imports": [
|
|
2849
2849
|
"_AppShell-lYVcj-k7.js",
|
|
2850
2850
|
"_seed-DeKSHrt2.js",
|
|
2851
|
-
"_StatusDot-
|
|
2851
|
+
"_StatusDot-DexKtvUC.js",
|
|
2852
2852
|
"_residentDiskBudget-BMBKlYRH.js"
|
|
2853
2853
|
]
|
|
2854
2854
|
},
|
|
2855
2855
|
"src/pages/ResidentsIndexPage.vue": {
|
|
2856
|
-
"file": "assets/ResidentsIndexPage-
|
|
2856
|
+
"file": "assets/ResidentsIndexPage-3HuYaDwD.js",
|
|
2857
2857
|
"name": "ResidentsIndexPage",
|
|
2858
2858
|
"src": "src/pages/ResidentsIndexPage.vue",
|
|
2859
2859
|
"isDynamicEntry": true,
|
|
@@ -2861,20 +2861,20 @@
|
|
|
2861
2861
|
"_AppShell-lYVcj-k7.js",
|
|
2862
2862
|
"_seed-DeKSHrt2.js",
|
|
2863
2863
|
"_wallClock-61NpVkY6.js",
|
|
2864
|
-
"_StatusDot-
|
|
2864
|
+
"_StatusDot-DexKtvUC.js",
|
|
2865
2865
|
"_residentDiskBudget-BMBKlYRH.js",
|
|
2866
2866
|
"_indexFormat-B-pd4r7Y.js"
|
|
2867
2867
|
]
|
|
2868
2868
|
},
|
|
2869
2869
|
"src/pages/RunRoutePage.vue": {
|
|
2870
|
-
"file": "assets/RunRoutePage-
|
|
2870
|
+
"file": "assets/RunRoutePage-Dr867-Ha.js",
|
|
2871
2871
|
"name": "RunRoutePage",
|
|
2872
2872
|
"src": "src/pages/RunRoutePage.vue",
|
|
2873
2873
|
"isDynamicEntry": true,
|
|
2874
2874
|
"imports": [
|
|
2875
2875
|
"_AppShell-lYVcj-k7.js",
|
|
2876
2876
|
"_seed-DeKSHrt2.js",
|
|
2877
|
-
"_Tooltip-
|
|
2877
|
+
"_Tooltip-DGQ2gu9M.js",
|
|
2878
2878
|
"src/main.ts",
|
|
2879
2879
|
"_wallClock-61NpVkY6.js",
|
|
2880
2880
|
"_NotFoundPage-BzWS4Ca1.js",
|
|
@@ -2890,31 +2890,31 @@
|
|
|
2890
2890
|
]
|
|
2891
2891
|
},
|
|
2892
2892
|
"src/pages/RunsIndexPage.vue": {
|
|
2893
|
-
"file": "assets/RunsIndexPage-
|
|
2893
|
+
"file": "assets/RunsIndexPage-D_lpXsr8.js",
|
|
2894
2894
|
"name": "RunsIndexPage",
|
|
2895
2895
|
"src": "src/pages/RunsIndexPage.vue",
|
|
2896
2896
|
"isDynamicEntry": true,
|
|
2897
2897
|
"imports": [
|
|
2898
2898
|
"_AppShell-lYVcj-k7.js",
|
|
2899
2899
|
"_seed-DeKSHrt2.js",
|
|
2900
|
-
"_Tooltip-
|
|
2900
|
+
"_Tooltip-DGQ2gu9M.js",
|
|
2901
2901
|
"_wallClock-61NpVkY6.js",
|
|
2902
2902
|
"_RunsTabs-DEICQ4FY.js",
|
|
2903
|
-
"_StatusDot-
|
|
2903
|
+
"_StatusDot-DexKtvUC.js",
|
|
2904
2904
|
"_indexFormat-B-pd4r7Y.js",
|
|
2905
2905
|
"_favicon-CWPcvWvp.js",
|
|
2906
2906
|
"_localIso-L06jV29p.js"
|
|
2907
2907
|
]
|
|
2908
2908
|
},
|
|
2909
2909
|
"src/pages/ScheduledPage.vue": {
|
|
2910
|
-
"file": "assets/ScheduledPage-
|
|
2910
|
+
"file": "assets/ScheduledPage-dfrjQ5E9.js",
|
|
2911
2911
|
"name": "ScheduledPage",
|
|
2912
2912
|
"src": "src/pages/ScheduledPage.vue",
|
|
2913
2913
|
"isDynamicEntry": true,
|
|
2914
2914
|
"imports": [
|
|
2915
2915
|
"_AppShell-lYVcj-k7.js",
|
|
2916
2916
|
"_seed-DeKSHrt2.js",
|
|
2917
|
-
"_Tooltip-
|
|
2917
|
+
"_Tooltip-DGQ2gu9M.js",
|
|
2918
2918
|
"_wallClock-61NpVkY6.js",
|
|
2919
2919
|
"_RunsTabs-DEICQ4FY.js",
|
|
2920
2920
|
"_indexFormat-B-pd4r7Y.js",
|