@coreplane/switchboard 1.231.1 → 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-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/cli.js +240 -192
- package/package.json +1 -1
|
@@ -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
|
}
|
package/dist/cli.js
CHANGED
|
@@ -9369,6 +9369,13 @@ function wakeWaitBudget(commandBudgetMs) {
|
|
|
9369
9369
|
if (commandBudgetMs === void 0 || !Number.isFinite(commandBudgetMs)) return WAKE_WAIT_MAX_MS;
|
|
9370
9370
|
return Math.max(0, Math.min(WAKE_WAIT_MAX_MS, Math.trunc(commandBudgetMs)));
|
|
9371
9371
|
}
|
|
9372
|
+
function saysContainerGone(data) {
|
|
9373
|
+
if (data.reason === "runtime-replaced") return true;
|
|
9374
|
+
return data.needs === "attach" && typeof data.error === "string" && data.error.startsWith("worktree-missing:");
|
|
9375
|
+
}
|
|
9376
|
+
function containerGoneMessage(error) {
|
|
9377
|
+
return `${error.trim()}; the command was not run again`;
|
|
9378
|
+
}
|
|
9372
9379
|
function sandboxRestartedMessage(woke) {
|
|
9373
9380
|
const secs2 = Math.round(woke.waitedMs / 1e3);
|
|
9374
9381
|
return `the resident container exited under a rollout and woke from its snapshot after ${secs2}s; the worktree was recreated at ${woke.ref}@${woke.sha.slice(0, 7)}, so uncommitted changes and unpushed commits from earlier in this run are gone`;
|
|
@@ -9629,10 +9636,11 @@ var init_resident = __esm({
|
|
|
9629
9636
|
this.opts = opts;
|
|
9630
9637
|
}
|
|
9631
9638
|
opts;
|
|
9632
|
-
/** Consecutive `runtime-replaced`
|
|
9633
|
-
* them. One is a deploy that swapped
|
|
9634
|
-
*
|
|
9635
|
-
*
|
|
9639
|
+
/** Consecutive `runtime-replaced` answers on the idempotent routes (/read,
|
|
9640
|
+
* /write) with no successful op between them. One is a deploy that swapped
|
|
9641
|
+
* the resident isolate under a call (routine: re-attach and re-issue); two
|
|
9642
|
+
* in a row is a flapping resident and becomes infra. /exec never counts
|
|
9643
|
+
* here: its `runtime-replaced` is the typed restart (`opWithReattach`). */
|
|
9636
9644
|
runtimeReplacedStreak = 0;
|
|
9637
9645
|
lastBinding;
|
|
9638
9646
|
/** The thread's binding as the resident answered it on the most recent
|
|
@@ -9807,15 +9815,20 @@ var init_resident = __esm({
|
|
|
9807
9815
|
return { released: false, reason: err2 instanceof Error ? err2.message : String(err2) };
|
|
9808
9816
|
}
|
|
9809
9817
|
}
|
|
9810
|
-
/** Run a route; on needs:"attach"
|
|
9818
|
+
/** Run a route; on needs:"attach" for an evicted worktree (in-body for
|
|
9811
9819
|
* /exec, 409 for /read //write) re-attach ONCE and retry, then fail legibly.
|
|
9812
9820
|
*
|
|
9813
|
-
*
|
|
9814
|
-
*
|
|
9815
|
-
*
|
|
9816
|
-
*
|
|
9817
|
-
*
|
|
9818
|
-
*
|
|
9821
|
+
* An answer saying the container under the thread is gone
|
|
9822
|
+
* (`saysContainerGone`: `reason:"runtime-replaced"`, the isolate was swapped
|
|
9823
|
+
* by a deploy while the op ran; `worktree-missing`, the container disk was
|
|
9824
|
+
* recycled since the last attach) is, on /exec, the typed
|
|
9825
|
+
* `ExecSandboxRestartedError` at once — the word the pi harness keys on
|
|
9826
|
+
* (harness-pi item 16) — before any recovery, so nothing the recovery
|
|
9827
|
+
* meets can hide it; the command may have started and is never re-issued.
|
|
9828
|
+
* On the idempotent routes (/read, /write) a `runtime-replaced` re-attaches
|
|
9829
|
+
* once — proving the new isolate serves this thread — and re-issues the op;
|
|
9830
|
+
* two such answers with no success between them are infra (a flapping
|
|
9831
|
+
* resident).
|
|
9819
9832
|
*
|
|
9820
9833
|
* A refusal naming a container that just exited (item 65) waits for the
|
|
9821
9834
|
* wake first (`awaitWake`): the refusal came before the command started, so
|
|
@@ -9834,6 +9847,10 @@ var init_resident = __esm({
|
|
|
9834
9847
|
),
|
|
9835
9848
|
{ kind: "infra", code: "attach" }
|
|
9836
9849
|
);
|
|
9850
|
+
const goneUnderThread = (data) => {
|
|
9851
|
+
if (route3 === "/exec" && saysContainerGone(data))
|
|
9852
|
+
throw new ExecSandboxRestartedError(containerGoneMessage(String(data.error)), 0);
|
|
9853
|
+
};
|
|
9837
9854
|
let r = await this.call(route3, body, callTimeoutMs, signal2, span);
|
|
9838
9855
|
if (isContainerRolling(r.data.error)) {
|
|
9839
9856
|
const woke = await this.awaitWake(route3, String(r.data.error), { signal: signal2, budgetMs: opts.waitBudgetMs, span });
|
|
@@ -9848,19 +9865,19 @@ var init_resident = __esm({
|
|
|
9848
9865
|
);
|
|
9849
9866
|
}
|
|
9850
9867
|
}
|
|
9868
|
+
goneUnderThread(r.data);
|
|
9851
9869
|
if (r.data.needs === "attach") {
|
|
9852
9870
|
await this.attach(span);
|
|
9853
9871
|
r = await this.call(route3, body, callTimeoutMs, signal2, span);
|
|
9872
|
+
goneUnderThread(r.data);
|
|
9854
9873
|
if (r.data.needs === "attach") throw stillGone(r.data);
|
|
9855
9874
|
}
|
|
9856
9875
|
if (r.data.reason === "runtime-replaced") {
|
|
9857
9876
|
this.noteRuntimeReplaced(route3, r.data);
|
|
9858
9877
|
await this.attach(span);
|
|
9859
|
-
|
|
9860
|
-
|
|
9861
|
-
|
|
9862
|
-
if (r.data.needs === "attach") throw stillGone(r.data);
|
|
9863
|
-
}
|
|
9878
|
+
r = await this.call(route3, body, callTimeoutMs, signal2, span);
|
|
9879
|
+
if (r.data.reason === "runtime-replaced") this.noteRuntimeReplaced(route3, r.data);
|
|
9880
|
+
if (r.data.needs === "attach") throw stillGone(r.data);
|
|
9864
9881
|
}
|
|
9865
9882
|
if (typeof r.data.error !== "string" || !r.data.error) this.runtimeReplacedStreak = 0;
|
|
9866
9883
|
return r;
|
|
@@ -9944,10 +9961,6 @@ var init_resident = __esm({
|
|
|
9944
9961
|
if (status3 === 400 && typeof data.error === "string" && data.error && data.needs === void 0) {
|
|
9945
9962
|
throw new Error(`resident /exec: ${data.error}`);
|
|
9946
9963
|
}
|
|
9947
|
-
if (data.reason === "runtime-replaced") {
|
|
9948
|
-
return `${String(data.error)}
|
|
9949
|
-
The command may have started; re-check its effects (e.g. git status, the files it writes) before re-running it.`;
|
|
9950
|
-
}
|
|
9951
9964
|
if (typeof data.error === "string" && data.error) {
|
|
9952
9965
|
throw classifyError(new ExecInfraError(`resident /exec: ${data.error}`), { kind: "infra" });
|
|
9953
9966
|
}
|
|
@@ -25072,7 +25085,8 @@ async function findOpenPrByHead(repo, branch2) {
|
|
|
25072
25085
|
return {
|
|
25073
25086
|
number: first.number,
|
|
25074
25087
|
htmlUrl: first.html_url,
|
|
25075
|
-
...first.head?.sha ? { headSha: first.head.sha } : {}
|
|
25088
|
+
...first.head?.sha ? { headSha: first.head.sha } : {},
|
|
25089
|
+
..."auto_merge" in first ? { autoMergeEnabled: first.auto_merge !== null } : {}
|
|
25076
25090
|
};
|
|
25077
25091
|
}
|
|
25078
25092
|
async function findMergedPrByHead(repo, branch2) {
|
|
@@ -25182,7 +25196,6 @@ async function fetchRepoShipInfo(repo) {
|
|
|
25182
25196
|
const data = await res.json().catch(() => null);
|
|
25183
25197
|
if (!data || typeof data !== "object") return void 0;
|
|
25184
25198
|
return {
|
|
25185
|
-
...typeof data.allow_auto_merge === "boolean" ? { allowAutoMerge: data.allow_auto_merge } : {},
|
|
25186
25199
|
...typeof data.default_branch === "string" && data.default_branch ? { defaultBranch: data.default_branch } : {}
|
|
25187
25200
|
};
|
|
25188
25201
|
}
|
|
@@ -25247,7 +25260,8 @@ async function fetchPullRequestFacts(pr) {
|
|
|
25247
25260
|
sameRepoHead,
|
|
25248
25261
|
...typeof data.base?.ref === "string" && data.base.ref ? { baseRef: data.base.ref } : {},
|
|
25249
25262
|
...typeof data.html_url === "string" ? { htmlUrl: data.html_url } : {},
|
|
25250
|
-
...typeof data.title === "string" && data.title ? { title: data.title } : {}
|
|
25263
|
+
...typeof data.title === "string" && data.title ? { title: data.title } : {},
|
|
25264
|
+
..."auto_merge" in data ? { autoMergeEnabled: data.auto_merge !== null } : {}
|
|
25251
25265
|
};
|
|
25252
25266
|
}
|
|
25253
25267
|
async function mergePullRequest(pr, opts) {
|
|
@@ -25584,7 +25598,7 @@ async function resolveRepoContext(msg, history = [], probe, residentSlugs, recor
|
|
|
25584
25598
|
const inherited = inheritedPr(thread, records, repo);
|
|
25585
25599
|
if (inherited) {
|
|
25586
25600
|
const head = await openPrHeadSha(inherited);
|
|
25587
|
-
if ("
|
|
25601
|
+
if (!("reason" in head)) {
|
|
25588
25602
|
out.pr = inherited.number;
|
|
25589
25603
|
out.headSha = head.sha;
|
|
25590
25604
|
if (head.base) out.baseRef = head.base;
|
|
@@ -25599,6 +25613,9 @@ async function resolveRepoContext(msg, history = [], probe, residentSlugs, recor
|
|
|
25599
25613
|
}
|
|
25600
25614
|
} else {
|
|
25601
25615
|
out.prUnpostable = { number: inherited.number, reason: head.reason };
|
|
25616
|
+
if (inherited.source === "record" && head.reason === "closed" && head.sha !== void 0) {
|
|
25617
|
+
out.closedRecordPr = { number: inherited.number, headSha: head.sha, merged: head.merged };
|
|
25618
|
+
}
|
|
25602
25619
|
}
|
|
25603
25620
|
}
|
|
25604
25621
|
}
|
|
@@ -25609,8 +25626,14 @@ function ownPrOf(ctx) {
|
|
|
25609
25626
|
return { number: ctx.pr, ref: ctx.ref };
|
|
25610
25627
|
}
|
|
25611
25628
|
function recordPrOf(ctx) {
|
|
25612
|
-
if (
|
|
25613
|
-
|
|
25629
|
+
if (ctx.prFromRecord && ctx.pr !== void 0 && ctx.headSha !== void 0) {
|
|
25630
|
+
return { number: ctx.pr, headSha: ctx.headSha, state: "open" };
|
|
25631
|
+
}
|
|
25632
|
+
if (ctx.closedRecordPr !== void 0) {
|
|
25633
|
+
const { number, headSha, merged } = ctx.closedRecordPr;
|
|
25634
|
+
return { number, headSha, state: merged ? "merged" : "closed" };
|
|
25635
|
+
}
|
|
25636
|
+
return void 0;
|
|
25614
25637
|
}
|
|
25615
25638
|
function inheritedPr(thread, records, repo) {
|
|
25616
25639
|
const turn = thread.pr && thread.pr.repo === repo ? { repo, number: thread.pr.number, source: "turn", at: thread.prAt } : void 0;
|
|
@@ -25620,7 +25643,8 @@ function inheritedPr(thread, records, repo) {
|
|
|
25620
25643
|
}
|
|
25621
25644
|
async function openPrHeadSha(pr) {
|
|
25622
25645
|
const head = await prHead(pr).catch(() => void 0);
|
|
25623
|
-
if (head?.state === "closed")
|
|
25646
|
+
if (head?.state === "closed")
|
|
25647
|
+
return { reason: "closed", ...head.sha ? { sha: head.sha } : {}, merged: head.merged };
|
|
25624
25648
|
if (head?.state === "open" && head.sha) {
|
|
25625
25649
|
return {
|
|
25626
25650
|
sha: head.sha,
|
|
@@ -25686,9 +25710,17 @@ async function prHead(pr) {
|
|
|
25686
25710
|
const headRepo = data.head?.repo?.full_name?.toLowerCase();
|
|
25687
25711
|
const ref = data.head?.ref && headRepo === pr.repo ? validRef(data.head.ref) : void 0;
|
|
25688
25712
|
const state = data.state === "open" || data.state === "closed" ? data.state : void 0;
|
|
25689
|
-
const sha = ref !== void 0 ? preferRefTip(`${pr.repo}#${pr.number}`, prSha, await headRefTipSha(pr.repo, ref, headers), ref) : prSha;
|
|
25713
|
+
const sha = ref !== void 0 && state !== "closed" ? preferRefTip(`${pr.repo}#${pr.number}`, prSha, await headRefTipSha(pr.repo, ref, headers), ref) : prSha;
|
|
25690
25714
|
const size = sha === prSha ? prSizeOf(data) : void 0;
|
|
25691
|
-
return {
|
|
25715
|
+
return {
|
|
25716
|
+
ref,
|
|
25717
|
+
sha,
|
|
25718
|
+
base,
|
|
25719
|
+
...size ? { size } : {},
|
|
25720
|
+
state,
|
|
25721
|
+
merged: data.merged === true,
|
|
25722
|
+
...facts2 ? { facts: facts2 } : {}
|
|
25723
|
+
};
|
|
25692
25724
|
}
|
|
25693
25725
|
function prSizeOf(data) {
|
|
25694
25726
|
const n2 = (x) => typeof x === "number" && Number.isInteger(x) && x >= 0;
|
|
@@ -32445,13 +32477,13 @@ function removeScript(dir) {
|
|
|
32445
32477
|
return `rm -rf ${shellQuote(dir)}`;
|
|
32446
32478
|
}
|
|
32447
32479
|
function stdoutOf(operation, out) {
|
|
32448
|
-
|
|
32449
|
-
if (
|
|
32480
|
+
if (RUNTIME_WORD.test(out)) throw new PiContainerRuntimeReplacedError(operation, out);
|
|
32481
|
+
if (parseExitPrefix(out).failed) throw new PiContainerError(operation, out);
|
|
32450
32482
|
const cut = out.indexOf(STDERR_MARK);
|
|
32451
32483
|
const stdout = cut >= 0 ? out.slice(0, cut) : out;
|
|
32452
32484
|
return stdout === "(no output)" ? "" : stdout;
|
|
32453
32485
|
}
|
|
32454
|
-
var PiContainerError, WRITE_CHUNK_CHARS, INLINE_LINE_CHARS, LOG_READ_BYTES, MESSAGE_UPDATE_MARK, LOG_FILTER, IDENTITY_WORD, STDERR_MARK, RUNTIME_WORD, OP_TIMEOUT_MS, ExecPiContainer;
|
|
32486
|
+
var PiContainerError, WRITE_CHUNK_CHARS, INLINE_LINE_CHARS, LOG_READ_BYTES, MESSAGE_UPDATE_MARK, LOG_FILTER, IDENTITY_WORD, STDERR_MARK, RUNTIME_WORD, PiContainerRuntimeReplacedError, OP_TIMEOUT_MS, ExecPiContainer;
|
|
32455
32487
|
var init_container = __esm({
|
|
32456
32488
|
"../../src/core/harness/pi/container.ts"() {
|
|
32457
32489
|
"use strict";
|
|
@@ -32473,7 +32505,13 @@ var init_container = __esm({
|
|
|
32473
32505
|
LOG_FILTER = `grep --line-buffered -v ${shellQuote(MESSAGE_UPDATE_MARK)}`;
|
|
32474
32506
|
IDENTITY_WORD = /^[A-Za-z0-9-]{1,64}$/;
|
|
32475
32507
|
STDERR_MARK = "\n--- stderr ---\n";
|
|
32476
|
-
RUNTIME_WORD =
|
|
32508
|
+
RUNTIME_WORD = /\bruntime-(?:unreachable|replaced)\b/;
|
|
32509
|
+
PiContainerRuntimeReplacedError = class extends PiContainerError {
|
|
32510
|
+
constructor(operation, detail) {
|
|
32511
|
+
super(operation, detail);
|
|
32512
|
+
this.name = "PiContainerRuntimeReplacedError";
|
|
32513
|
+
}
|
|
32514
|
+
};
|
|
32477
32515
|
OP_TIMEOUT_MS = 6e4;
|
|
32478
32516
|
ExecPiContainer = class {
|
|
32479
32517
|
constructor(executor) {
|
|
@@ -33614,10 +33652,8 @@ function settlementAnswer(s) {
|
|
|
33614
33652
|
return { content: [{ type: "text", text: settlementText(s) }], isError: true };
|
|
33615
33653
|
}
|
|
33616
33654
|
function saysContainerReplaced(err2) {
|
|
33617
|
-
if (err2 instanceof ExecSandboxRestartedError) return true;
|
|
33618
|
-
|
|
33619
|
-
const text = err2.message.replace(/^pi container: [a-z]+ failed — /, "").replace(/^exit \d+: /, "");
|
|
33620
|
-
return /^(?:runtime-unreachable:|runtime-replaced)/.test(text);
|
|
33655
|
+
if (err2 instanceof ExecSandboxRestartedError || err2 instanceof PiContainerRuntimeReplacedError) return true;
|
|
33656
|
+
return err2 instanceof Error && RUNTIME_WORD.test(err2.message);
|
|
33621
33657
|
}
|
|
33622
33658
|
function replacedCallNote(tool) {
|
|
33623
33659
|
return `The container running pi was replaced while this ${tool} call was in flight; its result was lost \u2014 re-check its effects before re-running it.`;
|
|
@@ -34254,6 +34290,7 @@ var init_harness = __esm({
|
|
|
34254
34290
|
init_runEvents();
|
|
34255
34291
|
init_threadAdmission();
|
|
34256
34292
|
init_bridge2();
|
|
34293
|
+
init_container();
|
|
34257
34294
|
init_mirror();
|
|
34258
34295
|
init_process();
|
|
34259
34296
|
init_protocol();
|
|
@@ -34719,14 +34756,30 @@ async function runCodingPrPostStep(input) {
|
|
|
34719
34756
|
const candidates = [target.baseRef, target.bindingRef, target.resolvedRef];
|
|
34720
34757
|
const base = prDescription ? await resolveBaseRefLazy(candidates, repo, input.fetchRepoInfo) : resolveBaseRef(candidates, void 0);
|
|
34721
34758
|
const pushedBranch = branch2 !== void 0 && branch2 !== base && pushed;
|
|
34722
|
-
|
|
34723
|
-
|
|
34759
|
+
const ownPr = target.ownPr;
|
|
34760
|
+
const pushedPastOwnPr = ownPr !== void 0 && pushed && headSha !== void 0 && !sameCommit2(headSha, ownPr.headSha) ? headSha : void 0;
|
|
34761
|
+
if (prDescription && ownPr !== void 0 && ownPr.state !== "open" && pushedPastOwnPr !== void 0 && branch2 !== void 0) {
|
|
34762
|
+
const url = `https://github.com/${repo}/pull/${ownPr.number}`;
|
|
34763
|
+
console.log(
|
|
34764
|
+
`[pr-post] ${logKey} skipped: ${branchLog} was pushed past the thread's ${ownPr.state} ${repo}#${ownPr.number} (its head ${ownPr.headSha.slice(0, 7)}, the workspace at ${pushedPastOwnPr.slice(0, 7)}) \u2014 a new PR is needed`
|
|
34765
|
+
);
|
|
34766
|
+
input.publish({
|
|
34767
|
+
type: "run_note",
|
|
34768
|
+
kind: "pr_not_opened",
|
|
34769
|
+
summary: `no PR opened: ${branch2} was pushed past the thread's ${ownPr.state} pull request #${ownPr.number}; the new commits need a new pull request`,
|
|
34770
|
+
at: systemClock()
|
|
34771
|
+
});
|
|
34772
|
+
return `\u26A0\uFE0F A PR description was submitted and \`${branch2}\`${branchNote} was pushed to \`${pushedPastOwnPr.slice(0, 7)}\`, but this thread's pull request ${url} is ${ownPr.state} and that branch is its head \u2014 the new commits need a new pull request, so nothing was edited: compare & open manually: ${compareUrl}`;
|
|
34773
|
+
}
|
|
34774
|
+
if (prDescription && ownPr !== void 0 && (branch2 === base || ownPr.state !== "open")) {
|
|
34775
|
+
const { number, headSha: prHead2, state } = ownPr;
|
|
34724
34776
|
const url = `https://github.com/${repo}/pull/${number}`;
|
|
34777
|
+
const standing = state === "open" ? "" : ` (${state})`;
|
|
34725
34778
|
try {
|
|
34726
34779
|
const body = renderPrDescriptionMarkdown(prDescription, { repo, headSha: prHead2 });
|
|
34727
34780
|
await input.updatePullRequest(repo, number, { title: prDescription.title, body });
|
|
34728
34781
|
console.log(
|
|
34729
|
-
`[pr-post] ${logKey} updated ${repo}#${number} \u2014 the thread's own pull request, nothing pushed (workspace on ${branchLog}; rendered at its head ${prHead2.slice(0, 7)})`
|
|
34782
|
+
`[pr-post] ${logKey} updated ${repo}#${number} \u2014 the thread's own pull request${standing}, nothing pushed past its head (workspace on ${branchLog}; rendered at its head ${prHead2.slice(0, 7)})`
|
|
34730
34783
|
);
|
|
34731
34784
|
input.publish({ type: "pr_opened", url, number, created: false, at: systemClock() });
|
|
34732
34785
|
input.publish({
|
|
@@ -34734,7 +34787,7 @@ async function runCodingPrPostStep(input) {
|
|
|
34734
34787
|
...submittedPrDescriptionArtifact(prDescription, { repo, pr: number, headSha: prHead2, body }),
|
|
34735
34788
|
at: systemClock()
|
|
34736
34789
|
});
|
|
34737
|
-
return `\u{1F500} PR updated: ${url} \u2014 body re-rendered at \`${prHead2.slice(0, 7)}
|
|
34790
|
+
return state === "open" ? `\u{1F500} PR updated: ${url} \u2014 body re-rendered at \`${prHead2.slice(0, 7)}\`` : `\u{1F500} PR updated: ${url} \u2014 body re-rendered at \`${prHead2.slice(0, 7)}\` (the pull request is ${state}; its description was edited in place)`;
|
|
34738
34791
|
} catch (err2) {
|
|
34739
34792
|
const reason2 = err2 instanceof Error ? err2.message : String(err2);
|
|
34740
34793
|
console.error(`[pr-post] ${logKey} update failed for ${repo}#${number}: ${reason2}`);
|
|
@@ -35677,151 +35730,6 @@ var init_runLoop = __esm({
|
|
|
35677
35730
|
}
|
|
35678
35731
|
});
|
|
35679
35732
|
|
|
35680
|
-
// ../../src/core/ship/preflight.ts
|
|
35681
|
-
function shipTaskText(requestText, repo) {
|
|
35682
|
-
let t = requestText.replace(/<((?:https?):\/\/[^|>\s]+)(?:\|[^>]*)?>/gi, " $1 ");
|
|
35683
|
-
t = t.replace(/https?:\/\/\S+/gi, " ");
|
|
35684
|
-
const slug = repo.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
35685
|
-
t = t.replace(new RegExp(`\\bin\\s+${slug}\\s*:?`, "gi"), " ");
|
|
35686
|
-
t = t.replace(new RegExp(`\\b${slug}(#\\d+)?\\b`, "gi"), " ");
|
|
35687
|
-
t = t.replace(/\b[a-z0-9][\w.-]*\/[\w.-]+#\d+\b/gi, " ");
|
|
35688
|
-
return t.replace(/\s+/g, " ").trim().replace(/^[:,\-—.\s]+/, "").trim();
|
|
35689
|
-
}
|
|
35690
|
-
async function shipPreflight(input) {
|
|
35691
|
-
const { repoCtx } = input;
|
|
35692
|
-
if (!input.channelId.startsWith("slack:") && !input.channelId.startsWith("cli:")) {
|
|
35693
|
-
const base = input.runsBase?.trim();
|
|
35694
|
-
const page2 = base ? `${base.replace(/\/+$/, "")}/runs` : "the bot's /runs page";
|
|
35695
|
-
return refuse2(
|
|
35696
|
-
"channel",
|
|
35697
|
-
"not started (Slack/CLI only)",
|
|
35698
|
-
`\u{1F6AB} \`agent:ship\` runs only from Slack or the CLI \u2014 this adapter is single-shot and cannot hold a pipeline-length run. Start it there instead, and watch pipelines on the run page (${page2}).`
|
|
35699
|
-
);
|
|
35700
|
-
}
|
|
35701
|
-
const missing = ["ship", "coding", "review"].filter((a) => !input.gates.canRunAgent(a));
|
|
35702
|
-
if (missing.length > 0) {
|
|
35703
|
-
return refuse2(
|
|
35704
|
-
`permission (${missing.join(", ")})`,
|
|
35705
|
-
"not started (permissions)",
|
|
35706
|
-
`\u{1F6AB} Running \`ship\` drives \`coding\` and \`review\` child rounds, and you're not on the allowlist for ${missing.map((a) => `\`${a}\``).join(", ")}. Ask ${input.gates.adminsHint()} for access.`
|
|
35707
|
-
);
|
|
35708
|
-
}
|
|
35709
|
-
const repo = repoCtx.repo;
|
|
35710
|
-
if (!repo) {
|
|
35711
|
-
return refuse2(
|
|
35712
|
-
"no repo",
|
|
35713
|
-
"not started (no repository)",
|
|
35714
|
-
"\u{1F6AB} `agent:ship` needs a target repository \u2014 name it in the request, e.g. `agent:ship in owner/repo: <task>`."
|
|
35715
|
-
);
|
|
35716
|
-
}
|
|
35717
|
-
const info = await input.repoInfo(repo).catch(() => void 0);
|
|
35718
|
-
if (info?.allowAutoMerge !== false) {
|
|
35719
|
-
const reason2 = info?.allowAutoMerge === true ? `auto-merge is enabled on \`${repo}\` \u2014 ship's approving LGTM triggers the org auto-approve workflow, and with auto-merge on the PR would merge with no human. Disable auto-merge on the repo to use ship.` : `could not verify that auto-merge is disabled on \`${repo}\` (the repository lookup failed or answered without the setting) \u2014 refusing fail-closed.`;
|
|
35720
|
-
return refuse2("auto-merge", "not started (auto-merge)", `\u{1F6AB} ${reason2}`);
|
|
35721
|
-
}
|
|
35722
|
-
const task = shipTaskText(input.requestText, repo);
|
|
35723
|
-
if (repoCtx.prUnpostable?.reason === "unreachable") {
|
|
35724
|
-
return refuse2(
|
|
35725
|
-
"thread PR unreachable",
|
|
35726
|
-
"not started (PR unverifiable)",
|
|
35727
|
-
`\u{1F6AB} This thread names PR ${repo}#${repoCtx.prUnpostable.number} but it could not be fetched to run ship's entry checks \u2014 refusing fail-closed. Retry in a moment, or check the PR on GitHub.`
|
|
35728
|
-
);
|
|
35729
|
-
}
|
|
35730
|
-
if (repoCtx.pr !== void 0) {
|
|
35731
|
-
const where = `${repo}#${repoCtx.pr}`;
|
|
35732
|
-
const facts2 = await input.prFacts({ repo, number: repoCtx.pr }).catch(() => void 0);
|
|
35733
|
-
const fallThrough = !facts2 && task !== "" && repoCtx.prFromMessage === true;
|
|
35734
|
-
if (!facts2 && !fallThrough) {
|
|
35735
|
-
return refuse2(
|
|
35736
|
-
"PR facts unavailable",
|
|
35737
|
-
"not started (PR unverifiable)",
|
|
35738
|
-
`\u{1F6AB} Could not fetch ${where} to run ship's entry checks (open? bot-authored? same-repo head?) \u2014 refusing fail-closed. Retry in a moment.`
|
|
35739
|
-
);
|
|
35740
|
-
}
|
|
35741
|
-
if (facts2) {
|
|
35742
|
-
const self = facts2.state === "open" ? await input.selfIdentity().catch(() => void 0) : void 0;
|
|
35743
|
-
if (facts2.state === "open" && !self) {
|
|
35744
|
-
return refuse2(
|
|
35745
|
-
"own identity unavailable",
|
|
35746
|
-
"not started (identity unverifiable)",
|
|
35747
|
-
`\u{1F6AB} Could not resolve the GitHub identity this bot acts as, so whether ${where} is ship's to drive cannot be judged \u2014 refusing fail-closed. Retry in a moment.`
|
|
35748
|
-
);
|
|
35749
|
-
}
|
|
35750
|
-
const author = facts2.author;
|
|
35751
|
-
const shipAuthored = self !== void 0 && author?.login === self.login && author?.id === self.id;
|
|
35752
|
-
if (facts2.state === "open" && !(task && !shipAuthored)) {
|
|
35753
|
-
if (task) {
|
|
35754
|
-
return refuse2(
|
|
35755
|
-
"new task over open PR",
|
|
35756
|
-
"not started (open PR)",
|
|
35757
|
-
`\u{1F6AB} This thread's PR ${where} is still open \u2014 a new task over it is refused. Re-issue \`agent:ship\` with only the PR URL to resume its review loop, or finish/close ${where} and start the new task in a fresh thread.`
|
|
35758
|
-
);
|
|
35759
|
-
}
|
|
35760
|
-
if (!shipAuthored) {
|
|
35761
|
-
return refuse2(
|
|
35762
|
-
"human-authored PR",
|
|
35763
|
-
"not started (not ship's PR)",
|
|
35764
|
-
`\u{1F6AB} ${where} was not authored by \`${self.login}\` (this bot) \u2014 it is not ship's to drive. Use \`agent:review\` for a one-off review, or drive the loop manually.`
|
|
35765
|
-
);
|
|
35766
|
-
}
|
|
35767
|
-
if (!facts2.sameRepoHead) {
|
|
35768
|
-
return refuse2(
|
|
35769
|
-
"fork-head PR",
|
|
35770
|
-
"not started (fork head)",
|
|
35771
|
-
`\u{1F6AB} ${where}'s head branch lives on a fork, not on \`${repo}\` \u2014 ship cannot drive it.`
|
|
35772
|
-
);
|
|
35773
|
-
}
|
|
35774
|
-
const branch2 = facts2.headRef ?? repoCtx.ref;
|
|
35775
|
-
if (!branch2) {
|
|
35776
|
-
return refuse2(
|
|
35777
|
-
"head branch unknown",
|
|
35778
|
-
"not started (head branch unknown)",
|
|
35779
|
-
`\u{1F6AB} Could not determine ${where}'s head branch, so ship cannot bind the thread's worktree to it \u2014 refusing fail-closed.`
|
|
35780
|
-
);
|
|
35781
|
-
}
|
|
35782
|
-
return {
|
|
35783
|
-
ok: true,
|
|
35784
|
-
entry: {
|
|
35785
|
-
repo,
|
|
35786
|
-
branch: branch2,
|
|
35787
|
-
// The PR's OWN base wins on resume: a ship PR opened against a
|
|
35788
|
-
// non-default base must not run its re-reviews (or re-open a closed
|
|
35789
|
-
// PR) against the default branch. repoCtx.baseRef carries the same
|
|
35790
|
-
// fact when the thread context resolved the PR; the default branch
|
|
35791
|
-
// is the last resort.
|
|
35792
|
-
base: resolveBaseRef([facts2.baseRef, repoCtx.baseRef], info.defaultBranch),
|
|
35793
|
-
resume: {
|
|
35794
|
-
pr: repoCtx.pr,
|
|
35795
|
-
headSha: facts2.headSha ?? repoCtx.headSha,
|
|
35796
|
-
...facts2.htmlUrl !== void 0 ? { url: facts2.htmlUrl } : {}
|
|
35797
|
-
}
|
|
35798
|
-
}
|
|
35799
|
-
};
|
|
35800
|
-
}
|
|
35801
|
-
}
|
|
35802
|
-
}
|
|
35803
|
-
if (!task) {
|
|
35804
|
-
return refuse2(
|
|
35805
|
-
"no task",
|
|
35806
|
-
"not started (no task)",
|
|
35807
|
-
`\u{1F6AB} Nothing to ship: give ship a task (\`agent:ship in ${repo}: <task>\`), or name an open ship PR by URL to resume its review loop.`
|
|
35808
|
-
);
|
|
35809
|
-
}
|
|
35810
|
-
const ref = repoCtx.ref && !repoCtx.refFromPr && repoCtx.ref.toLowerCase() !== repo.toLowerCase() ? repoCtx.ref : void 0;
|
|
35811
|
-
return {
|
|
35812
|
-
ok: true,
|
|
35813
|
-
entry: { repo, base: resolveBaseRef([ref], info.defaultBranch) }
|
|
35814
|
-
};
|
|
35815
|
-
}
|
|
35816
|
-
var refuse2;
|
|
35817
|
-
var init_preflight = __esm({
|
|
35818
|
-
"../../src/core/ship/preflight.ts"() {
|
|
35819
|
-
"use strict";
|
|
35820
|
-
init_githubPulls();
|
|
35821
|
-
refuse2 = (where, card, reply) => ({ ok: false, where, card, reply });
|
|
35822
|
-
}
|
|
35823
|
-
});
|
|
35824
|
-
|
|
35825
35733
|
// ../../src/core/ship/coordinator.ts
|
|
35826
35734
|
function shipInterruptedNote(prUrl2) {
|
|
35827
35735
|
const stands = prUrl2 ? `Its work stands on GitHub: ${prUrl2}.` : "Whatever it pushed stands on its pipeline branch; no PR was opened yet.";
|
|
@@ -36072,6 +35980,142 @@ var init_coordinator = __esm({
|
|
|
36072
35980
|
}
|
|
36073
35981
|
});
|
|
36074
35982
|
|
|
35983
|
+
// ../../src/core/ship/preflight.ts
|
|
35984
|
+
function shipTaskText(requestText, repo) {
|
|
35985
|
+
let t = requestText.replace(/<((?:https?):\/\/[^|>\s]+)(?:\|[^>]*)?>/gi, " $1 ");
|
|
35986
|
+
t = t.replace(/https?:\/\/\S+/gi, " ");
|
|
35987
|
+
const slug = repo.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
35988
|
+
t = t.replace(new RegExp(`\\bin\\s+${slug}\\s*:?`, "gi"), " ");
|
|
35989
|
+
t = t.replace(new RegExp(`\\b${slug}(#\\d+)?\\b`, "gi"), " ");
|
|
35990
|
+
t = t.replace(/\b[a-z0-9][\w.-]*\/[\w.-]+#\d+\b/gi, " ");
|
|
35991
|
+
return t.replace(/\s+/g, " ").trim().replace(/^[:,\-—.\s]+/, "").trim();
|
|
35992
|
+
}
|
|
35993
|
+
async function shipPreflight(input) {
|
|
35994
|
+
const { repoCtx } = input;
|
|
35995
|
+
if (!input.channelId.startsWith("slack:") && !input.channelId.startsWith("cli:")) {
|
|
35996
|
+
const base = input.runsBase?.trim();
|
|
35997
|
+
const page2 = base ? `${base.replace(/\/+$/, "")}/runs` : "the bot's /runs page";
|
|
35998
|
+
return refuse2(
|
|
35999
|
+
"channel",
|
|
36000
|
+
"not started (Slack/CLI only)",
|
|
36001
|
+
`\u{1F6AB} \`agent:ship\` runs only from Slack or the CLI \u2014 this adapter is single-shot and cannot hold a pipeline-length run. Start it there instead, and watch pipelines on the run page (${page2}).`
|
|
36002
|
+
);
|
|
36003
|
+
}
|
|
36004
|
+
const missing = ["ship", "coding", "review"].filter((a) => !input.gates.canRunAgent(a));
|
|
36005
|
+
if (missing.length > 0) {
|
|
36006
|
+
return refuse2(
|
|
36007
|
+
`permission (${missing.join(", ")})`,
|
|
36008
|
+
"not started (permissions)",
|
|
36009
|
+
`\u{1F6AB} Running \`ship\` drives \`coding\` and \`review\` child rounds, and you're not on the allowlist for ${missing.map((a) => `\`${a}\``).join(", ")}. Ask ${input.gates.adminsHint()} for access.`
|
|
36010
|
+
);
|
|
36011
|
+
}
|
|
36012
|
+
const repo = repoCtx.repo;
|
|
36013
|
+
if (!repo) {
|
|
36014
|
+
return refuse2(
|
|
36015
|
+
"no repo",
|
|
36016
|
+
"not started (no repository)",
|
|
36017
|
+
"\u{1F6AB} `agent:ship` needs a target repository \u2014 name it in the request, e.g. `agent:ship in owner/repo: <task>`."
|
|
36018
|
+
);
|
|
36019
|
+
}
|
|
36020
|
+
const info = await input.repoInfo(repo).catch(() => void 0);
|
|
36021
|
+
const task = shipTaskText(input.requestText, repo);
|
|
36022
|
+
const seeded = parseShipPlanRequest(task) !== void 0;
|
|
36023
|
+
if (!seeded && repoCtx.prUnpostable?.reason === "unreachable") {
|
|
36024
|
+
return refuse2(
|
|
36025
|
+
"thread PR unreachable",
|
|
36026
|
+
"not started (PR unverifiable)",
|
|
36027
|
+
`\u{1F6AB} This thread names PR ${repo}#${repoCtx.prUnpostable.number} but it could not be fetched to run ship's entry checks \u2014 refusing fail-closed. Retry in a moment, or check the PR on GitHub.`
|
|
36028
|
+
);
|
|
36029
|
+
}
|
|
36030
|
+
if (repoCtx.pr !== void 0 && !seeded) {
|
|
36031
|
+
const where = `${repo}#${repoCtx.pr}`;
|
|
36032
|
+
const contextCase = task !== "" && repoCtx.prFromMessage === true;
|
|
36033
|
+
if (!contextCase) {
|
|
36034
|
+
const facts2 = await input.prFacts({ repo, number: repoCtx.pr }).catch(() => void 0);
|
|
36035
|
+
if (!facts2) {
|
|
36036
|
+
return refuse2(
|
|
36037
|
+
"PR facts unavailable",
|
|
36038
|
+
"not started (PR unverifiable)",
|
|
36039
|
+
`\u{1F6AB} Could not fetch ${where} to run ship's entry checks (open? same-repo head?) \u2014 refusing fail-closed. Retry in a moment.`
|
|
36040
|
+
);
|
|
36041
|
+
}
|
|
36042
|
+
if (facts2.state === "open") {
|
|
36043
|
+
if (!facts2.sameRepoHead) {
|
|
36044
|
+
return refuse2(
|
|
36045
|
+
"fork-head PR",
|
|
36046
|
+
"not started (fork head)",
|
|
36047
|
+
`\u{1F6AB} ${where}'s head branch lives on a fork, not on \`${repo}\` \u2014 ship cannot drive it.`
|
|
36048
|
+
);
|
|
36049
|
+
}
|
|
36050
|
+
const branch2 = facts2.headRef ?? repoCtx.ref;
|
|
36051
|
+
if (!branch2) {
|
|
36052
|
+
return refuse2(
|
|
36053
|
+
"head branch unknown",
|
|
36054
|
+
"not started (head branch unknown)",
|
|
36055
|
+
`\u{1F6AB} Could not determine ${where}'s head branch, so ship cannot bind the thread's worktree to it \u2014 refusing fail-closed.`
|
|
36056
|
+
);
|
|
36057
|
+
}
|
|
36058
|
+
const base = resolveBaseRef([facts2.baseRef, repoCtx.baseRef], info?.defaultBranch);
|
|
36059
|
+
const autoMerge = facts2.autoMergeEnabled !== void 0 ? { autoMergeEnabled: facts2.autoMergeEnabled } : {};
|
|
36060
|
+
if (task) {
|
|
36061
|
+
return {
|
|
36062
|
+
ok: true,
|
|
36063
|
+
entry: {
|
|
36064
|
+
repo,
|
|
36065
|
+
branch: branch2,
|
|
36066
|
+
base,
|
|
36067
|
+
adopt: { pr: repoCtx.pr, ...facts2.htmlUrl !== void 0 ? { url: facts2.htmlUrl } : {} },
|
|
36068
|
+
...autoMerge
|
|
36069
|
+
}
|
|
36070
|
+
};
|
|
36071
|
+
}
|
|
36072
|
+
return {
|
|
36073
|
+
ok: true,
|
|
36074
|
+
entry: {
|
|
36075
|
+
repo,
|
|
36076
|
+
branch: branch2,
|
|
36077
|
+
base,
|
|
36078
|
+
resume: {
|
|
36079
|
+
pr: repoCtx.pr,
|
|
36080
|
+
headSha: facts2.headSha ?? repoCtx.headSha,
|
|
36081
|
+
...facts2.htmlUrl !== void 0 ? { url: facts2.htmlUrl } : {}
|
|
36082
|
+
},
|
|
36083
|
+
...autoMerge
|
|
36084
|
+
}
|
|
36085
|
+
};
|
|
36086
|
+
}
|
|
36087
|
+
if (!task) {
|
|
36088
|
+
return refuse2(
|
|
36089
|
+
"closed resume target",
|
|
36090
|
+
"not started (PR closed)",
|
|
36091
|
+
`\u{1F6AB} ${where} is closed \u2014 there is no review loop to resume. Give ship a task to start fresh work.`
|
|
36092
|
+
);
|
|
36093
|
+
}
|
|
36094
|
+
}
|
|
36095
|
+
}
|
|
36096
|
+
if (!task) {
|
|
36097
|
+
return refuse2(
|
|
36098
|
+
"no task",
|
|
36099
|
+
"not started (no task)",
|
|
36100
|
+
`\u{1F6AB} Nothing to ship: give ship a task (\`agent:ship in ${repo}: <task>\`), or name an open ship PR by URL to resume its review loop.`
|
|
36101
|
+
);
|
|
36102
|
+
}
|
|
36103
|
+
const ref = repoCtx.ref && !repoCtx.refFromPr && repoCtx.ref.toLowerCase() !== repo.toLowerCase() ? repoCtx.ref : void 0;
|
|
36104
|
+
return {
|
|
36105
|
+
ok: true,
|
|
36106
|
+
entry: { repo, base: resolveBaseRef([ref], info?.defaultBranch) }
|
|
36107
|
+
};
|
|
36108
|
+
}
|
|
36109
|
+
var refuse2;
|
|
36110
|
+
var init_preflight = __esm({
|
|
36111
|
+
"../../src/core/ship/preflight.ts"() {
|
|
36112
|
+
"use strict";
|
|
36113
|
+
init_githubPulls();
|
|
36114
|
+
init_coordinator();
|
|
36115
|
+
refuse2 = (where, card, reply) => ({ ok: false, where, card, reply });
|
|
36116
|
+
}
|
|
36117
|
+
});
|
|
36118
|
+
|
|
36075
36119
|
// ../../src/core/coordinator/handOff.ts
|
|
36076
36120
|
async function plan(deps, input) {
|
|
36077
36121
|
const { entry, msg } = input;
|
|
@@ -36116,7 +36160,9 @@ async function plan(deps, input) {
|
|
|
36116
36160
|
identity: identity2,
|
|
36117
36161
|
merge: "person",
|
|
36118
36162
|
...entry.resume !== void 0 ? { resume: entry.resume } : {},
|
|
36119
|
-
...entry.
|
|
36163
|
+
...entry.adopt !== void 0 ? { adopt: entry.adopt } : {},
|
|
36164
|
+
...entry.branch !== void 0 ? { entryBranch: entry.branch } : {},
|
|
36165
|
+
...entry.autoMergeEnabled !== void 0 ? { autoMergeEnabled: entry.autoMergeEnabled } : {}
|
|
36120
36166
|
}
|
|
36121
36167
|
};
|
|
36122
36168
|
}
|
|
@@ -36173,9 +36219,10 @@ function planWhere(p, units, mergedBefore) {
|
|
|
36173
36219
|
if (p.path !== void 0)
|
|
36174
36220
|
return `${head} Each unit runs in a thread of its own in this channel under your grants; this card follows the plan and its summary lands in this thread.`;
|
|
36175
36221
|
const branch2 = units[0]?.branch ?? "";
|
|
36176
|
-
const
|
|
36177
|
-
const runs = p.resume !== void 0 ? `the review loop of ${
|
|
36178
|
-
|
|
36222
|
+
const url = (of) => of?.url ?? (of !== void 0 ? `https://github.com/${p.identity.repo}/pull/${of.pr}` : void 0);
|
|
36223
|
+
const runs = p.resume !== void 0 ? `the review loop of ${url(p.resume)} resumes at its next review round on \`${branch2}\` in this thread under your grants \u2014 no new coding round first` : p.adopt !== void 0 ? `the unit adopts ${url(p.adopt)}: it runs on \`${branch2}\` \u2014 the pull request's own head \u2014 in this thread under your grants, no new branch` : `the unit runs on \`${branch2}\` in this thread under your grants`;
|
|
36224
|
+
const autoMerge = p.autoMergeEnabled ? " Auto-merge is on for this pull request: the approval merges it once checks pass." : "";
|
|
36225
|
+
return `${head} ${runs}; this card follows it and the report lands here.${autoMerge}`;
|
|
36179
36226
|
}
|
|
36180
36227
|
async function handOffToCoordinator(deps, input) {
|
|
36181
36228
|
const log = deps.log ?? console.log;
|
|
@@ -36452,7 +36499,6 @@ async function runShipBranch(deps, msg, io, ctx) {
|
|
|
36452
36499
|
gates: { canRunAgent: (a) => deps.config.canRunAgent(msg.userId, a), adminsHint: () => deps.config.adminsHint() },
|
|
36453
36500
|
repoInfo: deps.fetchRepoShipInfo ?? fetchRepoShipInfo,
|
|
36454
36501
|
prFacts: deps.fetchPrFacts ?? fetchPullRequestFacts,
|
|
36455
|
-
selfIdentity: deps.fetchSelfIdentity ?? resolveGithubIdentity,
|
|
36456
36502
|
runsBase: process.env.PUBLIC_BASE_URL
|
|
36457
36503
|
})
|
|
36458
36504
|
);
|
|
@@ -36682,7 +36728,6 @@ var init_ship = __esm({
|
|
|
36682
36728
|
"use strict";
|
|
36683
36729
|
init_trace();
|
|
36684
36730
|
init_githubPulls();
|
|
36685
|
-
init_githubApp();
|
|
36686
36731
|
init_secrets2();
|
|
36687
36732
|
init_handOff();
|
|
36688
36733
|
init_instancesClient();
|
|
@@ -45417,6 +45462,9 @@ async function prCheck(body, deps) {
|
|
|
45417
45462
|
prNumber: open2.number,
|
|
45418
45463
|
url: open2.htmlUrl,
|
|
45419
45464
|
...open2.headSha !== void 0 ? { headSha: open2.headSha } : {},
|
|
45465
|
+
// The pull request's own auto-merge fact (agent-ship item 9), so a
|
|
45466
|
+
// merge_ready ending can name it at the approved head.
|
|
45467
|
+
...open2.autoMergeEnabled !== void 0 ? { autoMergeEnabled: open2.autoMergeEnabled } : {},
|
|
45420
45468
|
at
|
|
45421
45469
|
});
|
|
45422
45470
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coreplane/switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.232.0",
|
|
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",
|