@coreplane/switchboard 1.264.0 → 1.265.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.264.0",
3
+ "version": "1.265.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.264.0",
9
+ "version": "1.265.1",
10
10
  "license": "Apache-2.0",
11
11
  "workspaces": [
12
12
  "web",
@@ -21060,7 +21060,7 @@
21060
21060
  },
21061
21061
  "packages/switchboard": {
21062
21062
  "name": "@coreplane/switchboard",
21063
- "version": "1.264.0",
21063
+ "version": "1.265.1",
21064
21064
  "license": "Apache-2.0",
21065
21065
  "dependencies": {
21066
21066
  "@earendil-works/pi-ai": "0.85.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.264.0",
3
+ "version": "1.265.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",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.264.0",
3
- "commit": "3f965ac7812604e62bd3e7df613ccf72ee665615",
4
- "builtAt": "2026-09-26T02:38:42.181Z"
2
+ "version": "1.265.1",
3
+ "commit": "dccb10815d97823de2be35d9768379a2d9ff6aaf",
4
+ "builtAt": "2026-09-26T14:55:53.149Z"
5
5
  }
@@ -0,0 +1,173 @@
1
+ import type { ExistingPrPublicationBinding } from "./coordinator/contract.js";
2
+ import { COMMAND_CAP, TOOL_OUTPUT_CAP, type RunEvent } from "./runEvents.js";
3
+
4
+ export interface PublicationPushReceipt {
5
+ callId: string;
6
+ previousHeadSha: string;
7
+ repo: string;
8
+ pr: number;
9
+ owner: ExistingPrPublicationBinding["owner"];
10
+ }
11
+
12
+ type PushEvent = Extract<RunEvent, { type: "pushed_head" }>;
13
+ const fullSha = (s: string): boolean => /^[a-f0-9]{40}$/.test(s);
14
+ const refName = (s: string): boolean =>
15
+ /^[a-zA-Z0-9_/-]+(?:\.[a-zA-Z0-9_/-]+)*$/.test(s) && !s.includes("..") && !s.startsWith("-");
16
+ const shortRef = (s: string): string => s.replace(/^refs\/heads\//, "");
17
+
18
+ /** Deliberately not a shell interpreter. Only a standalone literal push can
19
+ * attest its result: pipelines, scripts, substitutions and compound commands
20
+ * cannot prove which operation produced stdout or the successful exit. */
21
+ export function leasedPushCommand(command: string): { ref: string; expectedHeadSha: string } | undefined {
22
+ if (command.length >= COMMAND_CAP) return;
23
+ const literal = command.replace(/[ \t]+2>&1[ \t]*$/, "");
24
+ if (!/^git push\s/.test(literal) || /[\n\r'"`$;|<>\\&]/.test(literal)) return;
25
+ const words = literal.trim().split(/\s+/).slice(2);
26
+ const leases = words.filter((w) => w.startsWith("--force-with-lease="));
27
+ if (leases.length !== 1) return;
28
+ const lease = /^--force-with-lease=refs\/heads\/([^:]+):([a-f0-9]{40})$/.exec(leases[0]!);
29
+ if (!lease || !refName(lease[1]!)) return;
30
+ const args = words.filter((w) => w !== leases[0] && w !== "-u" && w !== "--set-upstream");
31
+ if (args.length !== 2 || args[0] !== "origin") return;
32
+ const refs = args[1]!.split(":");
33
+ if (refs.length > 2 || shortRef(refs[0]!) !== lease[1] || shortRef(refs[1] ?? refs[0]!) !== lease[1]) return;
34
+ return { ref: lease[1]!, expectedHeadSha: lease[2]! };
35
+ }
36
+
37
+ /** A legacy record needs the complete pair, never its display summary, an
38
+ * assistant's prose or the remote head alone. More than one attempted push is
39
+ * intentionally ambiguous; historical reconciliation does not choose one. */
40
+ export function pairedPublicationPush(
41
+ events: readonly RunEvent[],
42
+ binding: ExistingPrPublicationBinding,
43
+ head: string,
44
+ callId?: string,
45
+ ): PushEvent | undefined {
46
+ if (!fullSha(head) || !fullSha(binding.expectedHeadSha) || head === binding.expectedHeadSha) return;
47
+ const calls = events.filter(
48
+ (e): e is Extract<RunEvent, { type: "tool_call" }> =>
49
+ e.type === "tool_call" &&
50
+ e.tool === "bash" &&
51
+ (callId === undefined ? /\bgit\s+push\b/.test(e.command ?? e.summary) : e.callId === callId),
52
+ );
53
+ if (calls.length !== 1) return;
54
+ const call = calls[0]!;
55
+ if (
56
+ !call.callId ||
57
+ !call.command ||
58
+ events.filter((e) => e.type === "tool_call" && e.callId === call.callId).length !== 1
59
+ )
60
+ return;
61
+ const command = leasedPushCommand(call.command);
62
+ if (command?.ref !== binding.publicationRef || command.expectedHeadSha !== binding.expectedHeadSha) return;
63
+ const results = events.filter(
64
+ (e): e is Extract<RunEvent, { type: "tool_result" }> => e.type === "tool_result" && e.callId === call.callId,
65
+ );
66
+ if (results.length !== 1) return;
67
+ const result = results[0]!;
68
+ if (
69
+ events.indexOf(result) <= events.indexOf(call) ||
70
+ result.tool !== "bash" ||
71
+ !result.ok ||
72
+ result.exitCode !== 0 ||
73
+ result.cut ||
74
+ !result.output ||
75
+ result.output.length >= TOOL_OUTPUT_CAP
76
+ )
77
+ return;
78
+ let lines = result.output.trim().split(/\r?\n/);
79
+ const tracking = `branch '${binding.publicationRef}' set up to track 'origin/${binding.publicationRef}'.`;
80
+ if (
81
+ /(?:^|\s)(?:-u|--set-upstream)(?:\s|$)/.test(call.command) &&
82
+ lines.filter((line) => line === tracking).length === 1
83
+ )
84
+ lines = lines.filter((line) => line !== tracking);
85
+ if (lines.length !== 2) return;
86
+ const remote = /^To (?:https:\/\/github\.com\/|git@github\.com:|github\.com:)([^\s]+?)(?:\.git)?$/.exec(lines[0]!);
87
+ const update = /^\s*\+?\s*([a-f0-9]{7,40})(\.{2,3})([a-f0-9]{7,40})\s+(\S+) -> (\S+)( \(forced update\))?$/.exec(
88
+ lines[1]!,
89
+ );
90
+ if (
91
+ !remote ||
92
+ remote[1]!.toLowerCase() !== binding.repo.toLowerCase() ||
93
+ !update ||
94
+ !binding.expectedHeadSha.startsWith(update[1]!) ||
95
+ !head.startsWith(update[3]!) ||
96
+ update[1] === update[3] ||
97
+ shortRef(update[4]!) !== binding.publicationRef ||
98
+ shortRef(update[5]!) !== binding.publicationRef
99
+ )
100
+ return;
101
+ return {
102
+ type: "pushed_head",
103
+ ref: binding.publicationRef,
104
+ sha: head,
105
+ by: "push",
106
+ receipt: {
107
+ callId: call.callId,
108
+ previousHeadSha: binding.expectedHeadSha,
109
+ repo: binding.repo,
110
+ pr: binding.pr,
111
+ owner: { ...binding.owner },
112
+ },
113
+ };
114
+ }
115
+
116
+ /** State is stored JSON, not an unchecked cast back into publication authority. */
117
+ export function publicationReceiptsFromState(value: unknown): PushEvent[] {
118
+ if (!Array.isArray(value)) return [];
119
+ const valid = value.every((v) => {
120
+ if (typeof v !== "object" || v === null) return false;
121
+ const r = v.receipt;
122
+ return (
123
+ v.type === "pushed_head" &&
124
+ v.by === "push" &&
125
+ typeof v.ref === "string" &&
126
+ refName(v.ref) &&
127
+ typeof v.sha === "string" &&
128
+ fullSha(v.sha) &&
129
+ typeof r === "object" &&
130
+ r !== null &&
131
+ typeof r.callId === "string" &&
132
+ r.callId.length > 0 &&
133
+ typeof r.previousHeadSha === "string" &&
134
+ fullSha(r.previousHeadSha) &&
135
+ typeof r.repo === "string" &&
136
+ Number.isSafeInteger(r.pr) &&
137
+ r.pr > 0 &&
138
+ typeof r.owner === "object" &&
139
+ r.owner !== null &&
140
+ typeof r.owner.instanceId === "string" &&
141
+ typeof r.owner.unit === "string"
142
+ );
143
+ });
144
+ return valid ? ([...value] as PushEvent[]) : [];
145
+ }
146
+
147
+ /** Only the run's typed, contiguous leased transitions can move its restart
148
+ * fence. The caller still verifies the fresh PR and attached workspace. */
149
+ export function restoredPublicationHead(
150
+ events: readonly RunEvent[],
151
+ binding: ExistingPrPublicationBinding,
152
+ ): string | undefined {
153
+ let head = binding.expectedHeadSha;
154
+ const seen = new Set<string>();
155
+ for (const e of events) {
156
+ if (e.type !== "pushed_head" || e.receipt === undefined) continue;
157
+ const r = e.receipt;
158
+ if (
159
+ e.ref !== binding.publicationRef ||
160
+ r.repo !== binding.repo ||
161
+ r.pr !== binding.pr ||
162
+ r.owner.instanceId !== binding.owner.instanceId ||
163
+ r.owner.unit !== binding.owner.unit ||
164
+ r.previousHeadSha !== head ||
165
+ !fullSha(e.sha) ||
166
+ seen.has(r.callId)
167
+ )
168
+ return;
169
+ seen.add(r.callId);
170
+ head = e.sha;
171
+ }
172
+ return head === binding.expectedHeadSha ? undefined : head;
173
+ }
@@ -944,12 +944,23 @@ export type RunEvent =
944
944
  ref: string;
945
945
  sha: string;
946
946
  by: "push" | "salvage";
947
+ /** Exact leased tool result, retained before any post-step can refuse. */
948
+ receipt?: import("./publicationPush.js").PublicationPushReceipt;
947
949
  /** No uncommitted or unpushed work at the push (record 0064): the fact
948
950
  * the plane's soft stop reads. Absent where the measure was missing. */
949
951
  clean?: boolean;
950
952
  seq?: number;
951
953
  at?: number;
952
954
  }
955
+ /** The server gate admitted this exact leased push, not a child's claim. */
956
+ | {
957
+ type: "publication_push_authorized";
958
+ callId: string;
959
+ ref: string;
960
+ expectedHeadSha: string;
961
+ seq?: number;
962
+ at?: number;
963
+ }
953
964
  /** The coordinator tag as a fact of the run (docs/reference/specs/run-history.md
954
965
  * item 48a): the instance the run is a child of, the unit its idempotency
955
966
  * key named, and the base branch its pull request targets — published by
@@ -1998,10 +1998,12 @@ function settleCoding(
1998
1998
  // final observed head. Only then may a fresh remote read decide readiness.
1999
1999
  if (round.kind === "findings" && facts.status === "completed") {
2000
2000
  const issued = next.findingsByRound[round.index] ?? [];
2001
- const matched = matchDispositions(issued, facts.dispositions ?? []).matched;
2001
+ const { matched, dropped } = matchDispositions(issued, facts.dispositions ?? []);
2002
2002
  const missingOutputs = issued
2003
2003
  .filter((finding) => !matched.some((disposition) => disposition.findingId === finding.id))
2004
2004
  .map((finding) => `missing disposition for ${finding.id}`);
2005
+ if (missingOutputs.length > 0 && dropped.length > 0)
2006
+ missingOutputs.push(`submitted IDs with no matching finding: ${dropped.join(", ")}`);
2005
2007
  if (facts.description !== true) missingOutputs.push("missing updated pull request description");
2006
2008
  const observedHead = fullHead(facts.headSha);
2007
2009
  if (observedHead === undefined) missingOutputs.push("missing final observed commit");
package/dist/cli.js CHANGED
@@ -36249,6 +36249,8 @@ function activityLine(e) {
36249
36249
  case "pr_opened":
36250
36250
  return "PR opened";
36251
36251
  // published straight to the registry — never arrives here
36252
+ case "publication_push_authorized":
36253
+ return "push authorized";
36252
36254
  case "pushed_head":
36253
36255
  return `\u2B06 pushed ${e.ref} @ ${e.sha.slice(0, 7)}`;
36254
36256
  case "coordinator_tag":
@@ -36722,13 +36724,16 @@ async function resolveRepoContext(msg, history = [], probe, residentSlugs, recor
36722
36724
  rejected = void 0;
36723
36725
  unverified = void 0;
36724
36726
  }
36727
+ const bareNumberText = !s.pr && repo === records?.pr?.repo ? /\b(?:PR|pull request)\s*#(\d+)\b/i.exec(unwrapSlack(msg.text).replace(CODE_SPAN, " "))?.[1] : void 0;
36728
+ const bareNumber = bareNumberText === void 0 ? void 0 : Number(bareNumberText);
36729
+ const namedPr = s.pr ?? (repo && bareNumber !== void 0 && Number.isSafeInteger(bareNumber) && bareNumber > 0 ? { repo, number: bareNumber } : void 0);
36725
36730
  let headSha;
36726
36731
  let baseRef;
36727
36732
  let prSize;
36728
36733
  let facts2;
36729
36734
  let closedPr;
36730
- if (s.pr && repo === s.pr.repo) {
36731
- const head = await prHead(s.pr).catch(() => void 0);
36735
+ if (namedPr && repo === namedPr.repo) {
36736
+ const head = await prHead(namedPr).catch(() => void 0);
36732
36737
  if (head?.ref && head.state === "open") {
36733
36738
  ref = head.ref;
36734
36739
  refFromPr = true;
@@ -36739,7 +36744,7 @@ async function resolveRepoContext(msg, history = [], probe, residentSlugs, recor
36739
36744
  facts2 = head?.facts;
36740
36745
  if (head?.state === "closed")
36741
36746
  closedPr = {
36742
- number: s.pr.number,
36747
+ number: namedPr.number,
36743
36748
  merged: head.merged,
36744
36749
  ...head.mergedAt !== void 0 ? { mergedAt: head.mergedAt } : {}
36745
36750
  };
@@ -36750,16 +36755,16 @@ async function resolveRepoContext(msg, history = [], probe, residentSlugs, recor
36750
36755
  else if (rejected) out.rejectedRepo = rejected;
36751
36756
  if (ref) out.ref = ref;
36752
36757
  if (refFromPr) out.refFromPr = true;
36753
- if (repo && s.pr && repo === s.pr.repo) {
36754
- out.pr = s.pr.number;
36758
+ if (repo && namedPr && repo === namedPr.repo) {
36759
+ out.pr = namedPr.number;
36755
36760
  out.prFromMessage = true;
36756
- if (isThreadOwnPr(records, repo, s.pr.number)) out.prIsThreadOwn = true;
36761
+ if (isThreadOwnPr(records, repo, namedPr.number)) out.prIsThreadOwn = true;
36757
36762
  if (headSha) out.headSha = headSha;
36758
36763
  if (baseRef) out.baseRef = baseRef;
36759
36764
  if (prSize) out.prSize = prSize;
36760
36765
  if (facts2) out.prDescription = facts2;
36761
36766
  if (closedPr) out.closedPr = closedPr;
36762
- } else if (repo && !s.pr) {
36767
+ } else if (repo && !namedPr) {
36763
36768
  const inherited = inheritedPr(thread, records, repo);
36764
36769
  if (inherited) {
36765
36770
  const head = await openPrHeadSha(inherited);
@@ -40793,6 +40798,7 @@ async function ownerOf2(runs, unitsOf, threadKey) {
40793
40798
  if (live !== void 0) return { kind: "live", run: live };
40794
40799
  const hosted = runs.find((r) => !r.finished && r.hosted === true);
40795
40800
  const instanceId = instanceOf(runs);
40801
+ let releasedPr;
40796
40802
  if (instanceId !== void 0) {
40797
40803
  const units = await unitsOf(instanceId).catch(() => []);
40798
40804
  const unit = units.find((u) => u.threadKey === threadKey && u.ending === void 0);
@@ -40807,11 +40813,19 @@ async function ownerOf2(runs, unitsOf, threadKey) {
40807
40813
  if (ended.length === 1 && ship !== void 0) return { kind: "pipeline", instanceId, run: ship, unit: ended[0] };
40808
40814
  if (ended.length > 1 && ship !== void 0)
40809
40815
  return { kind: "pipeline_ambiguous", instanceId, run: ship, units: ended };
40816
+ const released = units.filter((u) => {
40817
+ const binding = u.publication;
40818
+ return u.threadKey === threadKey && u.instanceId === instanceId && u.ending?.kind === "merge_ready" && binding !== void 0 && u.pr?.number === binding.pr && u.branch === binding.headRef && u.branch === binding.publicationRef && binding.owner.instanceId === instanceId && binding.owner.unit === u.unit && (u.lastPush === void 0 || u.lastPush === binding.expectedHeadSha);
40819
+ });
40820
+ if (released.length === 1) {
40821
+ const unit2 = released[0];
40822
+ releasedPr = { repo: unit2.publication.repo, number: unit2.publication.pr, at: unit2.ending.at };
40823
+ }
40810
40824
  }
40811
40825
  if (hosted !== void 0) return { kind: "live", run: hosted };
40812
40826
  const agent = stickyAgentOf(runs);
40813
- if (agent !== void 0) return { kind: "session", agent };
40814
- return { kind: "none" };
40827
+ if (agent !== void 0) return { kind: "session", agent, ...releasedPr ? { releasedPr } : {} };
40828
+ return { kind: "none", ...releasedPr ? { releasedPr } : {} };
40815
40829
  }
40816
40830
  async function shipRequestOf(service, runId3) {
40817
40831
  const result = await service.getRun(runId3, { include: "messages" }).catch(() => void 0);
@@ -46062,7 +46076,7 @@ var init_submit = __esm({
46062
46076
  submitDispositionsTool = {
46063
46077
  name: "submit_dispositions",
46064
46078
  failsInText: true,
46065
- description: "Record one disposition per review finding after addressing them: `fixed` (the finding is addressed in your pushed code) or `declined` (deliberately not doing it \u2014 the note says why). `findingId` is the finding's stable id from the review (F1, F2, \u2026) \u2014 use exactly those ids; an id the review never issued answers nothing and is dropped when the plan runner reads your record. Every finding gets exactly one entry, every severity included (nits too). Call it once with the complete set after your last push; a later call replaces the earlier one. The set rides this run's record, where the plan runner reads it for the re-review.",
46079
+ description: "Record one disposition per review finding after addressing them: `fixed` (the finding is addressed in your pushed code) or `declined` (deliberately not doing it \u2014 the note says why). `findingId` is the finding's stable id printed in the findings. Copy it exactly, including check ids such as `check:CI / checks (test)`; do not replace a check id with `F1`. An id the review never issued answers nothing and is dropped when the plan runner reads your record. Every finding gets exactly one entry, every severity included (nits too). Call it once with the complete set after your last push; a later call replaces the earlier one. The set rides this run's record, where the plan runner reads it for the re-review.",
46066
46080
  inputSchema: {
46067
46081
  type: "object",
46068
46082
  properties: {
@@ -46072,7 +46086,10 @@ var init_submit = __esm({
46072
46086
  items: {
46073
46087
  type: "object",
46074
46088
  properties: {
46075
- findingId: { type: "string", description: `The finding's stable id from the review (e.g. "F1")` },
46089
+ findingId: {
46090
+ type: "string",
46091
+ description: 'Copy the exact id in the findings (e.g. "F1" or "check:CI / checks (test)")'
46092
+ },
46076
46093
  disposition: { type: "string", enum: ["fixed", "declined"], description: "fixed | declined" },
46077
46094
  note: { type: "string", description: "One line: what was done, or why it was declined" }
46078
46095
  },
@@ -46088,7 +46105,7 @@ var init_submit = __esm({
46088
46105
  if (!ctx.onDispositions) return "no run is recording dispositions here";
46089
46106
  ctx.onDispositions(parsed.dispositions);
46090
46107
  const drops = parsed.dropped.length ? ` (dropped: ${parsed.dropped.join("; ")})` : "";
46091
- return `dispositions recorded: ${parsed.dispositions.length}${drops}; a later call replaces this one`;
46108
+ return `dispositions recorded: ${parsed.dispositions.length}${drops}; IDs are checked against the findings by the plan runner; a later call replaces this one`;
46092
46109
  }
46093
46110
  };
46094
46111
  submitPrDescriptionTool = {
@@ -50246,6 +50263,11 @@ function reachFor(identity2) {
50246
50263
  if (identity2 === "read") return READ_REACH;
50247
50264
  return NONE_REACH;
50248
50265
  }
50266
+ function publicationAttributionRefusal(rules, callId) {
50267
+ const pending2 = rules.publication?.attributingCallId;
50268
+ if (pending2 !== void 0 && pending2 !== callId)
50269
+ return "push attribution is still pending; no later tool may change the checkout until its result is recorded";
50270
+ }
50249
50271
  function openCodeToolWord(action) {
50250
50272
  const mapped = OPENCODE_ACTION_TO_TOOL_WORD[action];
50251
50273
  if (mapped !== void 0) return mapped;
@@ -50278,19 +50300,36 @@ function judgeBashCommand(command2, ctx) {
50278
50300
  if (MERGE_OR_APPROVE.test(command2)) {
50279
50301
  return refused4("merge/approve \u2014 a coding run never merges or approves a pull request");
50280
50302
  }
50303
+ const pushCommand = withoutShellContinuations(command2);
50281
50304
  if (ctx.identity !== "write") {
50282
- if (ANY_PUSH.test(command2)) return refused4("read-only \u2014 a read-identity run never pushes");
50305
+ if (ANY_PUSH.test(pushCommand)) return refused4("read-only \u2014 a read-identity run never pushes");
50283
50306
  if (GH_WRITE_VERB.test(command2) || GH_API_WRITE.test(command2) || CURL_GITHUB_WRITE.test(command2)) {
50284
50307
  return refused4("read-only \u2014 a read-identity run never writes to GitHub");
50285
50308
  }
50286
50309
  return allowed;
50287
50310
  }
50288
- for (const match of command2.matchAll(GIT_PUSH)) {
50311
+ for (const match of pushCommand.matchAll(GIT_PUSH)) {
50289
50312
  const verdict = judgePush(match[1], ctx);
50290
50313
  if (verdict.verdict !== "allowed") return verdict;
50291
50314
  }
50292
50315
  return allowed;
50293
50316
  }
50317
+ function withoutShellContinuations(command2) {
50318
+ let result = "";
50319
+ let quote3;
50320
+ for (let i = 0; i < command2.length; i++) {
50321
+ const char = command2[i];
50322
+ if (char === "\\" && quote3 !== "'" && i + 1 < command2.length) {
50323
+ const next = command2[++i];
50324
+ if (next !== "\n") result += char + next;
50325
+ continue;
50326
+ }
50327
+ if (char === quote3) quote3 = void 0;
50328
+ else if (quote3 === void 0 && (char === "'" || char === '"')) quote3 = char;
50329
+ result += char;
50330
+ }
50331
+ return result;
50332
+ }
50294
50333
  function judgeBashTimeout(timeout, ctx) {
50295
50334
  if (!ctx.loopEndsIn || typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0) return allowed;
50296
50335
  const leftMs = ctx.loopEndsIn();
@@ -50401,8 +50440,11 @@ var init_toolRules = __esm({
50401
50440
  ENV_DUMP = /(^|[;&|(]\s*)((printenv|env)(\s+-[A-Za-z0-9-]+)*|export\s+-p|declare\s+-[a-zA-Z]*[px][a-zA-Z]*|set)\s*($|[|;&>)])|\/proc\/(self|\d+)\/environ|process\.env(?![.[\w])/;
50402
50441
  CREDENTIAL_VAR = /\$\{?[A-Z][A-Z0-9_]*(_API_KEY|_TOKEN|_SECRET|_BEARER|_PASSWORD)\b|printenv\s+[A-Z][A-Z0-9_]*(_API_KEY|_TOKEN|_SECRET|_BEARER|_PASSWORD)\b/;
50403
50442
  MERGE_OR_APPROVE = /\bgh\s+pr\s+(merge|review)\b|\/pulls\/\d+\/(merge|reviews)\b/;
50404
- GIT_THEN_PUSH = String.raw`\bgit(?:\s+(?:-C\s+\S+|--git-dir=\S+|--work-tree=\S+|-c\s+\S+|--no-pager))*\s+push\b`;
50405
- GIT_PUSH = new RegExp(`${GIT_THEN_PUSH}((?:[^;&|]|(?<=[<>])&|&(?=>))*)`, "g");
50443
+ GIT_THEN_PUSH = String.raw`\bgit(?:[ \t]+(?:-C[ \t]+\S+|--git-dir=\S+|--work-tree=\S+|-c[ \t]+\S+|--no-pager))*[ \t]+push\b`;
50444
+ GIT_PUSH = new RegExp(
50445
+ GIT_THEN_PUSH + String.raw`((?:"(?:\\[\s\S]|[^"\\])*"|'[^']*'|\\[^\n]|[^\n;&|'"\\]|(?<=[<>])&|&(?=>))*)`,
50446
+ "g"
50447
+ );
50406
50448
  ANY_PUSH = new RegExp(GIT_THEN_PUSH);
50407
50449
  GH_WRITE_VERB = /\bgh\s+(pr|issue)\s+(comment|create|edit|close|reopen|ready|lock|unlock|review|merge|delete|transfer|pin|unpin)\b/;
50408
50450
  WRITE_METHOD = "(POST|post|PATCH|patch|PUT|put|DELETE|delete)";
@@ -50429,6 +50471,98 @@ var init_toolRules = __esm({
50429
50471
  }
50430
50472
  });
50431
50473
 
50474
+ // ../../src/core/publicationPush.ts
50475
+ function leasedPushCommand(command2) {
50476
+ if (command2.length >= COMMAND_CAP) return;
50477
+ const literal = command2.replace(/[ \t]+2>&1[ \t]*$/, "");
50478
+ if (!/^git push\s/.test(literal) || /[\n\r'"`$;|<>\\&]/.test(literal)) return;
50479
+ const words = literal.trim().split(/\s+/).slice(2);
50480
+ const leases = words.filter((w) => w.startsWith("--force-with-lease="));
50481
+ if (leases.length !== 1) return;
50482
+ const lease = /^--force-with-lease=refs\/heads\/([^:]+):([a-f0-9]{40})$/.exec(leases[0]);
50483
+ if (!lease || !refName(lease[1])) return;
50484
+ const args = words.filter((w) => w !== leases[0] && w !== "-u" && w !== "--set-upstream");
50485
+ if (args.length !== 2 || args[0] !== "origin") return;
50486
+ const refs = args[1].split(":");
50487
+ if (refs.length > 2 || shortRef(refs[0]) !== lease[1] || shortRef(refs[1] ?? refs[0]) !== lease[1]) return;
50488
+ return { ref: lease[1], expectedHeadSha: lease[2] };
50489
+ }
50490
+ function pairedPublicationPush(events, binding, head, callId) {
50491
+ if (!fullSha(head) || !fullSha(binding.expectedHeadSha) || head === binding.expectedHeadSha) return;
50492
+ const calls = events.filter(
50493
+ (e) => e.type === "tool_call" && e.tool === "bash" && (callId === void 0 ? /\bgit\s+push\b/.test(e.command ?? e.summary) : e.callId === callId)
50494
+ );
50495
+ if (calls.length !== 1) return;
50496
+ const call2 = calls[0];
50497
+ if (!call2.callId || !call2.command || events.filter((e) => e.type === "tool_call" && e.callId === call2.callId).length !== 1)
50498
+ return;
50499
+ const command2 = leasedPushCommand(call2.command);
50500
+ if (command2?.ref !== binding.publicationRef || command2.expectedHeadSha !== binding.expectedHeadSha) return;
50501
+ const results = events.filter(
50502
+ (e) => e.type === "tool_result" && e.callId === call2.callId
50503
+ );
50504
+ if (results.length !== 1) return;
50505
+ const result = results[0];
50506
+ if (events.indexOf(result) <= events.indexOf(call2) || result.tool !== "bash" || !result.ok || result.exitCode !== 0 || result.cut || !result.output || result.output.length >= TOOL_OUTPUT_CAP)
50507
+ return;
50508
+ let lines = result.output.trim().split(/\r?\n/);
50509
+ const tracking = `branch '${binding.publicationRef}' set up to track 'origin/${binding.publicationRef}'.`;
50510
+ if (/(?:^|\s)(?:-u|--set-upstream)(?:\s|$)/.test(call2.command) && lines.filter((line4) => line4 === tracking).length === 1)
50511
+ lines = lines.filter((line4) => line4 !== tracking);
50512
+ if (lines.length !== 2) return;
50513
+ const remote = /^To (?:https:\/\/github\.com\/|git@github\.com:|github\.com:)([^\s]+?)(?:\.git)?$/.exec(lines[0]);
50514
+ const update = /^\s*\+?\s*([a-f0-9]{7,40})(\.{2,3})([a-f0-9]{7,40})\s+(\S+) -> (\S+)( \(forced update\))?$/.exec(
50515
+ lines[1]
50516
+ );
50517
+ if (!remote || remote[1].toLowerCase() !== binding.repo.toLowerCase() || !update || !binding.expectedHeadSha.startsWith(update[1]) || !head.startsWith(update[3]) || update[1] === update[3] || shortRef(update[4]) !== binding.publicationRef || shortRef(update[5]) !== binding.publicationRef)
50518
+ return;
50519
+ return {
50520
+ type: "pushed_head",
50521
+ ref: binding.publicationRef,
50522
+ sha: head,
50523
+ by: "push",
50524
+ receipt: {
50525
+ callId: call2.callId,
50526
+ previousHeadSha: binding.expectedHeadSha,
50527
+ repo: binding.repo,
50528
+ pr: binding.pr,
50529
+ owner: { ...binding.owner }
50530
+ }
50531
+ };
50532
+ }
50533
+ function publicationReceiptsFromState(value) {
50534
+ if (!Array.isArray(value)) return [];
50535
+ const valid = value.every((v) => {
50536
+ if (typeof v !== "object" || v === null) return false;
50537
+ const r = v.receipt;
50538
+ return v.type === "pushed_head" && v.by === "push" && typeof v.ref === "string" && refName(v.ref) && typeof v.sha === "string" && fullSha(v.sha) && typeof r === "object" && r !== null && typeof r.callId === "string" && r.callId.length > 0 && typeof r.previousHeadSha === "string" && fullSha(r.previousHeadSha) && typeof r.repo === "string" && Number.isSafeInteger(r.pr) && r.pr > 0 && typeof r.owner === "object" && r.owner !== null && typeof r.owner.instanceId === "string" && typeof r.owner.unit === "string";
50539
+ });
50540
+ return valid ? [...value] : [];
50541
+ }
50542
+ function restoredPublicationHead(events, binding) {
50543
+ let head = binding.expectedHeadSha;
50544
+ const seen = /* @__PURE__ */ new Set();
50545
+ for (const e of events) {
50546
+ if (e.type !== "pushed_head" || e.receipt === void 0) continue;
50547
+ const r = e.receipt;
50548
+ if (e.ref !== binding.publicationRef || r.repo !== binding.repo || r.pr !== binding.pr || r.owner.instanceId !== binding.owner.instanceId || r.owner.unit !== binding.owner.unit || r.previousHeadSha !== head || !fullSha(e.sha) || seen.has(r.callId))
50549
+ return;
50550
+ seen.add(r.callId);
50551
+ head = e.sha;
50552
+ }
50553
+ return head === binding.expectedHeadSha ? void 0 : head;
50554
+ }
50555
+ var fullSha, refName, shortRef;
50556
+ var init_publicationPush = __esm({
50557
+ "../../src/core/publicationPush.ts"() {
50558
+ "use strict";
50559
+ init_runEvents();
50560
+ fullSha = (s) => /^[a-f0-9]{40}$/.test(s);
50561
+ refName = (s) => /^[a-zA-Z0-9_/-]+(?:\.[a-zA-Z0-9_/-]+)*$/.test(s) && !s.includes("..") && !s.startsWith("-");
50562
+ shortRef = (s) => s.replace(/^refs\/heads\//, "");
50563
+ }
50564
+ });
50565
+
50432
50566
  // ../../src/core/harness/pi/relay.ts
50433
50567
  function stillRunningNote(tool) {
50434
50568
  return `This ${tool} call is still running in the bot: the process that made it was relaunched while the call was in flight, and the call keeps running there rather than being re-run \u2014 do not run it again; its result is kept on the relay under the same call id.`;
@@ -50459,9 +50593,18 @@ function authorizeToolCall(harness, ask) {
50459
50593
  return { allow: false, reason: reason2 };
50460
50594
  };
50461
50595
  if (blocked2 !== void 0) return refuse3(blocked2);
50596
+ const pending2 = publicationAttributionRefusal(harness.rules, ask.toolCallId);
50597
+ if (pending2 !== void 0) return refuse3(pending2);
50462
50598
  if (harness.tools.some((t) => t.name === ask.tool)) return { allow: true };
50463
50599
  const verdict = judgeToolCall(ask.tool, ask.input, harness.rules);
50464
- if (verdict.verdict === "allowed") return { allow: true };
50600
+ if (verdict.verdict === "allowed") {
50601
+ const authority = harness.rules.publication?.authority;
50602
+ const input = ask.input;
50603
+ const push = ask.tool === "bash" && typeof input?.command === "string" ? leasedPushCommand(input.command) : void 0;
50604
+ if (push !== void 0 && authority !== void 0 && !("blocked" in authority) && push.ref === authority.ref && push.expectedHeadSha === authority.expectedHeadSha)
50605
+ harness.emit({ type: "publication_push_authorized", callId: ask.toolCallId, ...push });
50606
+ return { allow: true };
50607
+ }
50465
50608
  return refuse3(verdict.reason);
50466
50609
  }
50467
50610
  function answerCompaction(harness, ask) {
@@ -50528,6 +50671,7 @@ var init_relay = __esm({
50528
50671
  init_runEvents();
50529
50672
  init_compactionFallback();
50530
50673
  init_toolRules();
50674
+ init_publicationPush();
50531
50675
  HarnessRegistry = class {
50532
50676
  live = /* @__PURE__ */ new Map();
50533
50677
  /** The run drives a pi from here, its relayed calls kept beside it; the
@@ -52341,19 +52485,19 @@ var init_resume = __esm({
52341
52485
  // ../../src/core/existingPrPublication.ts
52342
52486
  function verifyExistingPrPublication(durable, local, remote) {
52343
52487
  if (durable === void 0) return blocked("the durable existing-PR binding is missing");
52344
- if (!fullSha(durable.expectedHeadSha)) return blocked("the durable expected head is not a full commit SHA");
52488
+ if (!fullSha2(durable.expectedHeadSha)) return blocked("the durable expected head is not a full commit SHA");
52345
52489
  if (durable.headRef !== durable.publicationRef)
52346
52490
  return blocked("the durable head ref and owned publication ref do not agree");
52347
52491
  if (local.repo !== durable.repo) return blocked("the resolved repository does not match the durable binding");
52348
52492
  if (local.pr !== durable.pr) return blocked("the resolved pull request does not match the durable binding");
52349
52493
  if (local.ref !== durable.headRef) return blocked("the resolved head ref does not match the durable binding");
52350
52494
  if (local.baseRef !== durable.baseRef) return blocked("the resolved base ref does not match the durable binding");
52351
- if (!fullSha(local.requestHeadSha)) return blocked("the request head is missing or not a full commit SHA");
52495
+ if (!fullSha2(local.requestHeadSha)) return blocked("the request head is missing or not a full commit SHA");
52352
52496
  if (local.requestHeadSha.toLowerCase() !== durable.expectedHeadSha.toLowerCase())
52353
52497
  return blocked("the request head does not match the durable expected commit");
52354
52498
  if (local.workspaceRef !== durable.publicationRef)
52355
52499
  return blocked("the checked-out publication ref does not match the durable binding");
52356
- if (!fullSha(local.workspaceHeadSha)) return blocked("the checked-out head is missing or not a full commit SHA");
52500
+ if (!fullSha2(local.workspaceHeadSha)) return blocked("the checked-out head is missing or not a full commit SHA");
52357
52501
  if (local.workspaceHeadSha.toLowerCase() !== durable.expectedHeadSha.toLowerCase())
52358
52502
  return blocked("the checked-out head does not match the durable expected commit");
52359
52503
  if (!sameOwner(local.owner, durable.owner)) return blocked("the publication owner changed");
@@ -52363,7 +52507,7 @@ function verifyExistingPrPublication(durable, local, remote) {
52363
52507
  if (remote.headBranchExists !== true) return blocked("the pull request head branch is missing or unknown");
52364
52508
  if (remote.headRef !== durable.headRef) return blocked("the pull request head ref moved");
52365
52509
  if (remote.baseRef !== durable.baseRef) return blocked("the pull request base ref moved");
52366
- if (!fullSha(remote.headSha)) return blocked("the fresh pull-request head is missing or not a full commit SHA");
52510
+ if (!fullSha2(remote.headSha)) return blocked("the fresh pull-request head is missing or not a full commit SHA");
52367
52511
  if (remote.headSha.toLowerCase() !== durable.expectedHeadSha.toLowerCase())
52368
52512
  return blocked("the pull request head moved from the durable expected commit");
52369
52513
  return {
@@ -52371,11 +52515,11 @@ function verifyExistingPrPublication(durable, local, remote) {
52371
52515
  publication: { ref: durable.publicationRef, expectedHeadSha: durable.expectedHeadSha.toLowerCase() }
52372
52516
  };
52373
52517
  }
52374
- var fullSha, sameOwner, blocked;
52518
+ var fullSha2, sameOwner, blocked;
52375
52519
  var init_existingPrPublication = __esm({
52376
52520
  "../../src/core/existingPrPublication.ts"() {
52377
52521
  "use strict";
52378
- fullSha = (value) => value !== void 0 && /^[0-9a-f]{40}$/i.test(value);
52522
+ fullSha2 = (value) => value !== void 0 && /^[0-9a-f]{40}$/i.test(value);
52379
52523
  sameOwner = (left, right) => left?.instanceId === right.instanceId && left.unit === right.unit;
52380
52524
  blocked = (reason2) => ({ ok: false, reason: reason2 });
52381
52525
  }
@@ -53402,6 +53546,14 @@ async function runLoop(deps, ctx) {
53402
53546
  let reviewHead = ctx.reviewHead;
53403
53547
  let lastActivityAt = ctx.loopStartedAt;
53404
53548
  const restored = resume?.row.state ?? {};
53549
+ const restoredReceipts = publicationReceiptsFromState(restored.publicationReceipts);
53550
+ const publicationReceipts = coordinator?.publication !== void 0 && restoredPublicationHead(restoredReceipts, coordinator.publication) !== void 0 ? restoredReceipts : [];
53551
+ const retainPublicationReceipts = () => {
53552
+ const history = registry.snapshot(run2.id, run2.token)?.events ?? [];
53553
+ const latest = publicationReceipts.at(-1);
53554
+ if (latest !== void 0 && !history.some((e) => e.type === "pushed_head" && e.ref === latest.ref))
53555
+ registry.publish(run2.id, latest);
53556
+ };
53405
53557
  let checklist = typeof restored.checklist === "string" ? restored.checklist : void 0;
53406
53558
  let lastActivity;
53407
53559
  let inFlightTool;
@@ -53438,6 +53590,23 @@ async function runLoop(deps, ctx) {
53438
53590
  let budgetEnded = false;
53439
53591
  let recordedPushedBranch;
53440
53592
  let modelBudgetEndsAt;
53593
+ const receiptAtToolResult = async (e) => {
53594
+ const binding2 = coordinator?.publication;
53595
+ const authority = existingPrPublicationFence?.authority;
53596
+ if (e.type !== "tool_result" || e.tool !== "bash" || !e.ok || e.exitCode !== 0 || e.cut || binding2 === void 0 || authority === void 0 || "blocked" in authority)
53597
+ return;
53598
+ const history = registry.snapshot(run2.id, run2.token)?.events ?? [];
53599
+ const gates = history.filter((event) => event.type === "publication_push_authorized" && event.callId === e.callId);
53600
+ if (gates.length !== 1 || gates[0]?.type !== "publication_push_authorized" || gates[0].ref !== authority.ref || gates[0].expectedHeadSha !== authority.expectedHeadSha)
53601
+ return;
53602
+ try {
53603
+ const head = (await executor.exec(`git rev-parse refs/heads/${authority.ref}`)).trim();
53604
+ const current = { ...binding2, expectedHeadSha: authority.expectedHeadSha };
53605
+ return pairedPublicationPush([...history, e], current, head, e.callId);
53606
+ } catch {
53607
+ return;
53608
+ }
53609
+ };
53441
53610
  const publishEvent = (e, startsRunBudget) => {
53442
53611
  registry.publish(run2.id, e);
53443
53612
  if (isSpanRecord(e)) return;
@@ -53462,12 +53631,16 @@ async function runLoop(deps, ctx) {
53462
53631
  card.update(currentFrame());
53463
53632
  };
53464
53633
  const onEvent = (e) => {
53634
+ if (e.type === "publication_push_authorized" && existingPrPublicationFence !== void 0)
53635
+ existingPrPublicationFence.attributingCallId = e.callId;
53465
53636
  const startsRunBudget = e.type === "lease";
53466
53637
  if (startsRunBudget) {
53467
53638
  modelBudgetEndsAt = e.endsAt;
53468
53639
  run2.control.startLease(() => e.endsAt - clock4());
53469
53640
  }
53470
53641
  void events.write(async () => {
53642
+ const receipt = await receiptAtToolResult(e);
53643
+ let receiptCommitted = false;
53471
53644
  if ((e.type === "tool_call" || e.type === "tool_result") && typeof registry.commitLiveState === "function") {
53472
53645
  const summary = registry.getById(run2.id);
53473
53646
  if (summary?.liveState?.state === "working") {
@@ -53481,12 +53654,16 @@ async function runLoop(deps, ctx) {
53481
53654
  state: "working",
53482
53655
  bound,
53483
53656
  detail: e.type === "tool_call" ? "running a tool" : "model turn",
53484
- sourceEvents: [{ ...e, seq: sourceSeq }]
53657
+ sourceEvents: [{ ...e, seq: sourceSeq }, ...receipt ? [{ ...receipt, seq: sourceSeq + 1, at }] : []],
53658
+ ...receipt ? { statePatch: { publicationReceipts: [...publicationReceipts, receipt] } } : {}
53485
53659
  };
53486
53660
  try {
53487
53661
  if (ledgerRun?.tracked()) {
53488
53662
  const committed = await ledgerRun.assignLiveState(assignment);
53489
- if (committed.ok) registry.commitLiveState(run2.id, committed);
53663
+ if (committed.ok) {
53664
+ registry.commitLiveState(run2.id, committed);
53665
+ receiptCommitted = true;
53666
+ }
53490
53667
  } else {
53491
53668
  const local = assignRunLiveState(summary.liveState, summary.liveStateSeq ?? 0, assignment);
53492
53669
  if (local.ok) registry.commitLiveState(run2.id, { ...local, liveStateSeq: sourceSeq });
@@ -53500,6 +53677,16 @@ async function runLoop(deps, ctx) {
53500
53677
  }
53501
53678
  }
53502
53679
  publishEvent(e, startsRunBudget);
53680
+ if (receipt !== void 0) {
53681
+ if (receiptCommitted) {
53682
+ publicationReceipts.push(receipt);
53683
+ publishEvent(receipt, false);
53684
+ existingPrPublication = { ref: receipt.ref, expectedHeadSha: receipt.sha };
53685
+ if (existingPrPublicationFence !== void 0) existingPrPublicationFence.authority = existingPrPublication;
53686
+ } else blockExistingPrPublication("the successful push receipt could not be committed durably");
53687
+ }
53688
+ if (e.type === "tool_result" && existingPrPublicationFence !== void 0 && existingPrPublicationFence.attributingCallId === e.callId)
53689
+ delete existingPrPublicationFence.attributingCallId;
53503
53690
  });
53504
53691
  };
53505
53692
  for (const s of mcpForRun?.servers ?? []) {
@@ -53651,6 +53838,8 @@ async function runLoop(deps, ctx) {
53651
53838
  };
53652
53839
  const preserveCodingChildWork = async (cue) => {
53653
53840
  if (workSalvageAttempted || !isCodingPrRun || coordinator === void 0) return;
53841
+ await events.drain();
53842
+ retainPublicationReceipts();
53654
53843
  if (!workspaceObserved) await observeWorkspaceNow();
53655
53844
  const target = coordinatorBranch === void 0 ? { skipped: "the ending checkpoint was skipped: the unit's branch is unavailable" } : salvageTargetOf({
53656
53845
  pushedBranch: pushes.branch(),
@@ -53804,6 +53993,40 @@ async function runLoop(deps, ctx) {
53804
53993
  };
53805
53994
  let workspaceBinding = workspaceBindingFor(round2.selection, profile.machine);
53806
53995
  try {
53996
+ const prBase = repoCtx.baseRef ?? await coordinatorBase;
53997
+ if (coordinator?.publication !== void 0) {
53998
+ const original = coordinator.publication;
53999
+ const restoredHead = resume === void 0 ? void 0 : restoredPublicationHead(publicationReceipts, original);
54000
+ const publication = restoredHead === void 0 ? original : { ...original, expectedHeadSha: restoredHead };
54001
+ const fresh = await (deps.fetchPrFacts ?? fetchPullRequestFacts)({
54002
+ repo: publication.repo,
54003
+ number: publication.pr
54004
+ }).catch(() => void 0);
54005
+ const verified = verifyExistingPrPublication(
54006
+ publication,
54007
+ {
54008
+ repo: repoCtx.repo,
54009
+ pr: repoCtx.pr,
54010
+ ref: repoCtx.ref,
54011
+ baseRef: prBase,
54012
+ requestHeadSha: restoredHead ?? repoCtx.headSha,
54013
+ workspaceRef: binding?.ref,
54014
+ workspaceHeadSha: binding?.sha,
54015
+ owner: {
54016
+ instanceId: coordinator.parentInstanceId,
54017
+ unit: unitOfIdempotencyKey(coordinator.idempotencyKey) ?? ""
54018
+ }
54019
+ },
54020
+ fresh
54021
+ );
54022
+ existingPrPublication = verified.ok ? verified.publication : { blocked: verified.reason };
54023
+ existingPrPublicationFence = { authority: existingPrPublication };
54024
+ if (!verified.ok)
54025
+ shell.note(
54026
+ "quiet",
54027
+ `Existing-PR publication is blocked: ${verified.reason}. Work stays on the bound checkout; no alternate branch or pull request will be published.`
54028
+ );
54029
+ }
53807
54030
  if (finish2) {
53808
54031
  onEvent({
53809
54032
  type: "run_note",
@@ -53869,38 +54092,6 @@ async function runLoop(deps, ctx) {
53869
54092
  const providerCfg = deps.config.config.providers[providerName];
53870
54093
  if (!providerCfg)
53871
54094
  throw new Error(`the ${harness.name} harness found no provider named ${providerName} in the config`);
53872
- const prBase = repoCtx.baseRef ?? await coordinatorBase;
53873
- if (coordinator?.publication !== void 0) {
53874
- const publication = coordinator.publication;
53875
- const fresh = await (deps.fetchPrFacts ?? fetchPullRequestFacts)({
53876
- repo: publication.repo,
53877
- number: publication.pr
53878
- }).catch(() => void 0);
53879
- const verified = verifyExistingPrPublication(
53880
- publication,
53881
- {
53882
- repo: repoCtx.repo,
53883
- pr: repoCtx.pr,
53884
- ref: repoCtx.ref,
53885
- baseRef: prBase,
53886
- requestHeadSha: repoCtx.headSha,
53887
- workspaceRef: binding?.ref,
53888
- workspaceHeadSha: binding?.sha,
53889
- owner: {
53890
- instanceId: coordinator.parentInstanceId,
53891
- unit: unitOfIdempotencyKey(coordinator.idempotencyKey) ?? ""
53892
- }
53893
- },
53894
- fresh
53895
- );
53896
- existingPrPublication = verified.ok ? verified.publication : { blocked: verified.reason };
53897
- existingPrPublicationFence = { authority: existingPrPublication };
53898
- if (!verified.ok)
53899
- shell.note(
53900
- "quiet",
53901
- `Existing-PR publication is blocked: ${verified.reason}. Work stays on the bound checkout; no alternate branch or pull request will be published.`
53902
- );
53903
- }
53904
54095
  const ownBranch = coordinator?.publication !== void 0 ? coordinator.publication.publicationRef : prBase !== void 0 ? binding?.ref ?? repoCtx.ref : void 0;
53905
54096
  const protectedBranches = [
53906
54097
  ...new Set(
@@ -54161,6 +54352,8 @@ async function runLoop(deps, ctx) {
54161
54352
  };
54162
54353
  const endingNeedsPreservation = isCodingPrRun && coordinator !== void 0 && (budgetEnded || run2.control.requested !== void 0);
54163
54354
  if (endingNeedsPreservation && run2.control.requested === "hard") await endHarness();
54355
+ await events.drain();
54356
+ retainPublicationReceipts();
54164
54357
  if (isCodingPrRun && !tailSkipped()) await observeWorkspaceNow();
54165
54358
  if (endingNeedsPreservation) await preserveCodingChildWork(budgetEnded ? "budget" : "ending");
54166
54359
  else if (isCodingPrRun && coordinator !== void 0 && !tailSkipped()) await preserveCodingChildWork("completion");
@@ -54376,6 +54569,7 @@ async function runLoop(deps, ctx) {
54376
54569
  } finally {
54377
54570
  clearInterval(heartbeat);
54378
54571
  await events.drain();
54572
+ retainPublicationReceipts();
54379
54573
  const status3 = statusNow();
54380
54574
  if (typeof registry.commitLiveState === "function") {
54381
54575
  const commitBoundary = async (state, at2) => {
@@ -54539,6 +54733,7 @@ var init_runLoop = __esm({
54539
54733
  init_githubPulls();
54540
54734
  init_repoContext();
54541
54735
  init_existingPrPublication();
54736
+ init_publicationPush();
54542
54737
  init_prDescription();
54543
54738
  init_handoff();
54544
54739
  init_reviewVerdict();
@@ -56782,7 +56977,8 @@ async function dispatch(deps, msg, io, opts = {}) {
56782
56977
  );
56783
56978
  };
56784
56979
  const stickyAgent = thread ? stickyAgentOf(thread) : void 0;
56785
- const threadPr = thread ? threadPrOf(thread) : void 0;
56980
+ const laterPr = (runPr, releasedPr) => releasedPr !== void 0 && (runPr === void 0 || releasedPr.at > runPr.at) ? releasedPr : runPr;
56981
+ let threadPr = laterPr(thread ? threadPrOf(thread) : void 0, pageOwner?.releasedPr);
56786
56982
  const inheritedRepo = operatorRepo ?? (thread ? newestFinishedRunOf(thread)?.repo : void 0) ?? deps.config.scopes(msg.channelId, msg.userId).channel.repo;
56787
56983
  const historicalRoutePreset = restart?.row.meta.route?.preset;
56788
56984
  const resolveCurrent = () => resolveRun(deps, {
@@ -56803,6 +56999,7 @@ async function dispatch(deps, msg, io, opts = {}) {
56803
56999
  const threadLive = admission.get(msg.threadKey) !== void 0 || !resume && !restart && deps.threadsElsewhere.get(msg.threadKey) !== void 0;
56804
57000
  if (thread && !threadLive && deps.coordinatorInstances !== void 0) {
56805
57001
  const owner = pageOwner ?? await ownerOf2(thread, (id) => deps.coordinatorInstances.listUnits(id), msg.threadKey);
57002
+ threadPr = laterPr(threadPr, owner.releasedPr);
56806
57003
  if (owner.kind === "live" && owner.run.hosted === true) {
56807
57004
  const units = owner.run.instanceId === void 0 ? [] : await deps.coordinatorInstances.listUnits(owner.run.instanceId).catch(() => []);
56808
57005
  const open2 = units.filter((u) => u.ending === void 0);
@@ -60811,7 +61008,7 @@ function isBudgetRefusal(error) {
60811
61008
  if (error.status !== void 0 && error.status !== 403) return false;
60812
61009
  return TURN_BUDGET_REFUSAL.test(error.message);
60813
61010
  }
60814
- function judgeOpenCodeAsk(action, resources, rules, relayedToolNames) {
61011
+ function judgeOpenCodeAsk(action, resources, rules, relayedToolNames, callId) {
60815
61012
  if (action === "doom_loop")
60816
61013
  return {
60817
61014
  reply: "reject",
@@ -60820,6 +61017,8 @@ function judgeOpenCodeAsk(action, resources, rules, relayedToolNames) {
60820
61017
  };
60821
61018
  const word = openCodeToolWord(action);
60822
61019
  const tool = TOOL_NAME_WORD[action] ?? word;
61020
+ const pending2 = publicationAttributionRefusal(rules, callId);
61021
+ if (pending2 !== void 0) return { reply: "reject", message: pending2, tool };
60823
61022
  if (relayedToolNames.has(action) || relayedToolNames.has(word)) return { reply: "once", tool: action };
60824
61023
  const refuse3 = (reason2) => ({ reply: "reject", message: reason2, tool });
60825
61024
  if (action === "external_directory") {
@@ -61640,6 +61839,7 @@ var init_bridge3 = __esm({
61640
61839
  "use strict";
61641
61840
  init_budgets();
61642
61841
  init_runEvents();
61842
+ init_publicationPush();
61643
61843
  init_contract4();
61644
61844
  init_container();
61645
61845
  init_reattach();
@@ -62507,9 +62707,14 @@ ${data.text}` : data.text;
62507
62707
  request2.action,
62508
62708
  Array.isArray(request2.resources) ? request2.resources : [],
62509
62709
  this.deps.rules,
62510
- this.deps.relayedToolNames
62710
+ this.deps.relayedToolNames,
62711
+ callId
62511
62712
  );
62512
62713
  if (this.answered.get(callId) !== "reject") this.answered.set(callId, verdict.reply);
62714
+ const authority = this.deps.rules.publication?.authority;
62715
+ const push = openCodeToolWord(request2.action) === "bash" && request2.resources?.length === 1 ? leasedPushCommand(request2.resources[0]) : void 0;
62716
+ if (this.answered.get(callId) === "once" && push !== void 0 && authority !== void 0 && !("blocked" in authority) && push.ref === authority.ref && push.expectedHeadSha === authority.expectedHeadSha)
62717
+ this.emit({ type: "publication_push_authorized", callId, ...push });
62513
62718
  this.decidedReplies.set(request2.id, { reply: verdict.reply, callId, echoed: false });
62514
62719
  const stepID = typeof request2.source?.messageID === "string" ? request2.source.messageID : void 0;
62515
62720
  if (verdict.reply === "reject") {
@@ -72943,7 +73148,7 @@ function findingsRequest(input) {
72943
73148
 
72944
73149
  A person answered the human-gated finding. Treat the finding and this answer together as the fix brief:
72945
73150
  ${input.answers.join("\n\n")}` : "";
72946
- return `The review of ${input.where} requested changes. Load the \`address-review-findings\` skill and address every finding below, nits included: record one disposition per finding with submit_dispositions (fixed or declined, with a note), squash to coherent commits, resubmit the pull request description with submit_pr_description, and push the branch. Never merge and never approve.
73151
+ return `The review of ${input.where} requested changes. Load the \`address-review-findings\` skill and address every finding below, nits included: record one disposition per finding with submit_dispositions (fixed or declined, with a note). Copy each finding ID exactly, including any \`check:\u2026\` ID; never substitute an \`F1\` ID for a check. Squash to coherent commits, resubmit the pull request description with submit_pr_description, and push the branch. Never merge and never approve.
72947
73152
 
72948
73153
  Findings:
72949
73154
  ${findings}${answers}
@@ -74011,6 +74216,36 @@ function publicationStepPrefix(row2) {
74011
74216
  }
74012
74217
  return prefix;
74013
74218
  }
74219
+ async function reconcileMissingFindingsPush(deps, instance, row2, runId3, head, siblings, after) {
74220
+ const binding = row2.publication;
74221
+ if (binding === void 0) return false;
74222
+ const read = await deps.runs.getRun(runId3, { include: "messages" });
74223
+ if (!read.ok) return false;
74224
+ const run2 = read.value;
74225
+ const complete2 = (r) => {
74226
+ const models = Object.values(r.usage?.byModel ?? {});
74227
+ return r.finished && r.persisted === true && !r.provisional && !r.restarting && r.truncated === false && r.events !== void 0 && r.eventCount === r.events.length && r.storedEventCount === r.events.length && r.events.every(
74228
+ (e) => e.type !== "tool_call" || e.tool !== "bash" || e.command !== void 0 && e.command.length < COMMAND_CAP && !e.command.includes("\u2026")
74229
+ ) && !r.events.some((e) => e.type === "tool_result" && e.cut || e.type === "pushed_head") && models.length > 0 && models.every((m) => typeof m.usd === "number" && Number.isFinite(m.usd) && m.usd >= 0);
74230
+ };
74231
+ const tags = run2.events?.filter((e) => e.type === "coordinator_tag") ?? [];
74232
+ const tag = tags.length === 1 ? tags[0] : void 0;
74233
+ if (!complete2(run2) || run2.status !== "completed" || (run2.pushed?.length ?? 0) !== 0 || run2.headSha !== head || run2.parentInstanceId !== instance.id || run2.userId !== instance.userId || run2.repo?.toLowerCase() !== instance.repo.toLowerCase() || run2.threadKey !== (row2.threadKey ?? instance.threadKey) || run2.agent !== "coding" || run2.startedAt < after || run2.finishedAt === void 0 || run2.finishedAt < run2.startedAt || tag?.parentInstanceId !== instance.id || tag.unit !== row2.unit || tag.base !== instance.base || !samePublicationBinding(tag.publication, binding) || run2.pr !== void 0 && (run2.pr.number !== binding.pr || run2.pr.head !== void 0 && run2.pr.head !== row2.branch) || pairedPublicationPush(run2.events, binding, head) === void 0)
74234
+ return false;
74235
+ for (const sibling of siblings.filter(
74236
+ (r) => r.id !== runId3 && r.agent === "coding" && (r.finishedAt === void 0 || r.finishedAt >= after)
74237
+ )) {
74238
+ if (!sibling.finished || sibling.status !== "failed" && sibling.status !== "interrupted" || sibling.parentInstanceId !== instance.id || sibling.userId !== instance.userId || sibling.repo?.toLowerCase() !== instance.repo.toLowerCase() || sibling.threadKey !== run2.threadKey || sibling.startedAt < after || sibling.finishedAt === void 0 || sibling.finishedAt > run2.startedAt || (sibling.pushed?.length ?? 0) > 0)
74239
+ return false;
74240
+ const prior = await deps.runs.getRun(sibling.id, { include: "messages" });
74241
+ if (!prior.ok || !complete2(prior.value) || prior.value.events.some(
74242
+ (e) => e.type === "pushed_head" || e.type === "tool_call" && e.tool === "bash" && /\bgit\s+push\b/.test(e.command ?? e.summary)
74243
+ ))
74244
+ return false;
74245
+ }
74246
+ const fresh = await deps.fetchPrFacts({ repo: instance.repo, number: binding.pr }).catch(() => void 0);
74247
+ return fresh?.state === "open" && fresh.sameRepoHead === true && fresh.headBranchExists === true && fresh.headRef === binding.headRef && fresh.baseRef === binding.baseRef && fresh.headSha === head && fresh.verifiedHead?.repo.toLowerCase() === instance.repo.toLowerCase() && fresh.verifiedHead.ref === binding.publicationRef && fresh.verifiedHead.sha === head;
74248
+ }
74014
74249
  async function advanceFindingsPublication(deps, instance, row2, facts2, runId3) {
74015
74250
  if (row2.publication === void 0 || facts2.headSha === row2.publication.expectedHeadSha) return row2;
74016
74251
  const child = await deps.runs.getRun(runId3, { include: "messages" });
@@ -74018,6 +74253,7 @@ async function advanceFindingsPublication(deps, instance, row2, facts2, runId3)
74018
74253
  const latestReview = [...row2.rounds].reverse().find((round2) => round2.agent === "review");
74019
74254
  const prefix = row2.recovery !== void 0 ? `${row2.unit}/recovery` : publicationStepPrefix(row2);
74020
74255
  const findingsPrefix = `${instance.id}:${prefix}/${latestReview?.index}/findings`;
74256
+ let reconciledPush = false;
74021
74257
  if (row2.recovery === void 0) {
74022
74258
  const tags = child.ok ? child.value.events?.filter((event) => event.type === "coordinator_tag") ?? [] : [];
74023
74259
  const tag = tags.length === 1 ? tags[0] : void 0;
@@ -74036,8 +74272,18 @@ async function advanceFindingsPublication(deps, instance, row2, facts2, runId3)
74036
74272
  (run2) => run2.id !== runId3 && !(run2.finished && (run2.status === "failed" || run2.status === "interrupted") && run2.agent === "coding" && run2.userId === instance.userId && run2.repo?.toLowerCase() === instance.repo.toLowerCase() && run2.threadKey === (row2.threadKey ?? instance.threadKey) && run2.startedAt >= latestReview.at && run2.finishedAt !== void 0 && run2.finishedAt >= run2.startedAt && run2.finishedAt <= child.value.startedAt && (run2.pushed?.length ?? 0) === 0)
74037
74273
  ))
74038
74274
  throw new PublicationBindingRefusal("publication_facts_mismatch");
74275
+ if (child.ok && (child.value.pushed?.length ?? 0) === 0 && fullHead(facts2.headSha))
74276
+ reconciledPush = await reconcileMissingFindingsPush(
74277
+ deps,
74278
+ instance,
74279
+ row2,
74280
+ runId3,
74281
+ facts2.headSha,
74282
+ listing.runs,
74283
+ latestReview.at
74284
+ );
74039
74285
  }
74040
- if (!child.ok || child.value.finished !== true || child.value.status !== "completed" || child.value.agent !== "coding" || child.value.parentInstanceId !== instance.id || child.value.userId !== instance.userId || child.value.repo?.toLowerCase() !== instance.repo.toLowerCase() || child.value.threadKey !== (row2.threadKey ?? instance.threadKey) || (row2.recovery !== void 0 ? child.value.idempotencyKey?.startsWith(`${instance.id}:${row2.unit}/recovery/`) !== true || !/\/findings(?:\/a[1-9][0-9]*)?$/.test(child.value.idempotencyKey ?? "") : !isStepAttempt(child.value.idempotencyKey, findingsPrefix)) || !fullHead(facts2.headSha) || child.value.headSha !== facts2.headSha || child.value.pushed?.some((push) => push.ref === row2.branch && push.sha === facts2.headSha) !== true || facts2.state !== "open" || facts2.sameRepoHead !== true || facts2.headBranchExists !== true || facts2.headRef !== row2.publication.headRef || facts2.baseRef !== row2.publication.baseRef || facts2.verifiedHead?.repo.toLowerCase() !== instance.repo.toLowerCase() || facts2.verifiedHead.ref !== row2.publication.publicationRef || facts2.verifiedHead.sha !== facts2.headSha || row2.publication.repo.toLowerCase() !== instance.repo.toLowerCase() || row2.publication.pr !== row2.pr?.number || row2.publication.baseRef !== instance.base || row2.publication.headRef !== row2.branch || row2.publication.publicationRef !== row2.branch || !samePublicationOwner(row2.publication.owner, owner))
74286
+ if (!child.ok || child.value.finished !== true || child.value.status !== "completed" || child.value.agent !== "coding" || child.value.parentInstanceId !== instance.id || child.value.userId !== instance.userId || child.value.repo?.toLowerCase() !== instance.repo.toLowerCase() || child.value.threadKey !== (row2.threadKey ?? instance.threadKey) || (row2.recovery !== void 0 ? child.value.idempotencyKey?.startsWith(`${instance.id}:${row2.unit}/recovery/`) !== true || !/\/findings(?:\/a[1-9][0-9]*)?$/.test(child.value.idempotencyKey ?? "") : !isStepAttempt(child.value.idempotencyKey, findingsPrefix)) || !fullHead(facts2.headSha) || child.value.headSha !== facts2.headSha || !reconciledPush && child.value.pushed?.some((push) => push.ref === row2.branch && push.sha === facts2.headSha) !== true || facts2.state !== "open" || facts2.sameRepoHead !== true || facts2.headBranchExists !== true || facts2.headRef !== row2.publication.headRef || facts2.baseRef !== row2.publication.baseRef || facts2.verifiedHead?.repo.toLowerCase() !== instance.repo.toLowerCase() || facts2.verifiedHead.ref !== row2.publication.publicationRef || facts2.verifiedHead.sha !== facts2.headSha || row2.publication.repo.toLowerCase() !== instance.repo.toLowerCase() || row2.publication.pr !== row2.pr?.number || row2.publication.baseRef !== instance.base || row2.publication.headRef !== row2.branch || row2.publication.publicationRef !== row2.branch || !samePublicationOwner(row2.publication.owner, owner))
74041
74287
  throw new PublicationBindingRefusal("publication_facts_mismatch");
74042
74288
  try {
74043
74289
  if (row2.recovery !== void 0 && deps.runnerOwnership?.claim(instance.repo, row2.publication.pr, owner) !== true)
@@ -74334,7 +74580,16 @@ async function recoverOriginalUnit(body, deps, caller) {
74334
74580
  const checkedAttempts = attempts.filter((run2) => run2.idempotencyKey === `${instance.id}:${checkedStep}`);
74335
74581
  const completed = checkedAttempts.length === 1 ? checkedAttempts[0] : void 0;
74336
74582
  const reviewFinishedAt = review.finishedAt;
74337
- if (reviewFinishedAt === void 0 || completed === void 0 || !completed.finished || completed.status !== "completed" || completed.agent !== "coding" || completed.userId !== instance.userId || completed.repo?.toLowerCase() !== instance.repo.toLowerCase() || completed.threadKey !== unitThreadKey || completed.pr !== void 0 && (completed.pr.number !== pr.number || completed.pr.head !== void 0 && completed.pr.head !== row2.branch) || completed.headSha !== facts2.headSha || expectedHead !== reviewedHead && expectedHead !== facts2.headSha || completed.pushed?.some((push) => push.ref !== row2.branch) === true || headMoved && completed.pushed?.some((push) => push.ref === row2.branch && push.sha === facts2.headSha) !== true || completed.startedAt < reviewFinishedAt || completed.finishedAt === void 0 || completed.finishedAt < completed.startedAt || completed.finishedAt > row2.ending.at || // Inspect every attempt, not just runs matching the current PR head:
74583
+ const reconciledPush = completed !== void 0 && headMoved && reviewFinishedAt !== void 0 && (completed.pushed?.length ?? 0) === 0 && await reconcileMissingFindingsPush(
74584
+ deps,
74585
+ instance,
74586
+ row2,
74587
+ completed.id,
74588
+ facts2.headSha,
74589
+ unitListing.runs,
74590
+ reviewFinishedAt
74591
+ );
74592
+ if (reviewFinishedAt === void 0 || completed === void 0 || !completed.finished || completed.status !== "completed" || completed.agent !== "coding" || completed.userId !== instance.userId || completed.repo?.toLowerCase() !== instance.repo.toLowerCase() || completed.threadKey !== unitThreadKey || completed.pr !== void 0 && (completed.pr.number !== pr.number || completed.pr.head !== void 0 && completed.pr.head !== row2.branch) || completed.headSha !== facts2.headSha || expectedHead !== reviewedHead && expectedHead !== facts2.headSha || completed.pushed?.some((push) => push.ref !== row2.branch) === true || headMoved && !reconciledPush && completed.pushed?.some((push) => push.ref === row2.branch && push.sha === facts2.headSha) !== true || completed.startedAt < reviewFinishedAt || completed.finishedAt === void 0 || completed.finishedAt < completed.startedAt || completed.finishedAt > row2.ending.at || // Inspect every attempt, not just runs matching the current PR head:
74338
74593
  // any competing success or push remains ambiguous after a later push.
74339
74594
  attempts.some(
74340
74595
  (run2) => run2.id !== completed.id && !(run2.finished && (run2.status === "failed" || run2.status === "interrupted") && run2.agent === "coding" && run2.userId === instance.userId && run2.repo?.toLowerCase() === instance.repo.toLowerCase() && run2.threadKey === unitThreadKey && run2.startedAt >= reviewFinishedAt && run2.finishedAt !== void 0 && run2.finishedAt >= run2.startedAt && run2.finishedAt <= completed.startedAt && (run2.pushed?.length ?? 0) === 0)
@@ -75998,6 +76253,7 @@ var init_adminCoordinator = __esm({
75998
76253
  "use strict";
75999
76254
  init_budgets();
76000
76255
  init_verbosity();
76256
+ init_publicationPush();
76001
76257
  init_prDescription();
76002
76258
  init_reply();
76003
76259
  init_registry();
@@ -76012,6 +76268,7 @@ var init_adminCoordinator = __esm({
76012
76268
  init_effort();
76013
76269
  init_reviewVerdict();
76014
76270
  init_runFriction();
76271
+ init_runEvents();
76015
76272
  init_runRecord();
76016
76273
  init_coordinator();
76017
76274
  init_renewal();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coreplane/switchboard",
3
- "version": "1.264.0",
3
+ "version": "1.265.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",