@coreplane/switchboard 1.261.1 → 1.261.3

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/cli.js CHANGED
@@ -4186,6 +4186,7 @@ var init_runEvents = __esm({
4186
4186
  "ledger_untracked",
4187
4187
  "rebind_refused",
4188
4188
  "work_left_behind",
4189
+ "publication_blocked",
4189
4190
  "workspace_torn_down",
4190
4191
  "pr_not_opened",
4191
4192
  "pr_head_unverified",
@@ -5792,6 +5793,7 @@ function isCoordinatorUnit(v) {
5792
5793
  if (r.issue !== void 0 && !isFinite(r.issue)) return false;
5793
5794
  if (r.pr !== void 0 && !isPr(r.pr)) return false;
5794
5795
  if (r.resume !== void 0 && !isResume(r.resume)) return false;
5796
+ if (r.publication !== void 0 && !isPublication(r.publication)) return false;
5795
5797
  if (r.record !== void 0 && (typeof r.record !== "string" || !/^\d{4}$/.test(r.record))) return false;
5796
5798
  if (r.lastPush !== void 0 && !isText(r.lastPush)) return false;
5797
5799
  if (r.segments !== void 0 && (!Array.isArray(r.segments) || !r.segments.every(isSegment))) return false;
@@ -5855,7 +5857,7 @@ async function sendChildSignal(workflow, signal2) {
5855
5857
  return { kind: "failed", instance, type, reason: err2 instanceof Error ? err2.message : String(err2) };
5856
5858
  }
5857
5859
  }
5858
- var COORDINATOR_IDENTITY, COORDINATOR_STEP_ACTION, PLAN_MERGE_ACTION, COORDINATOR_STEP_PATH_PREFIX, INSTANCE_ID_PATTERN, STEP_NAME_PATTERN, IDEMPOTENCY_KEY_PATTERN, UNIT_PATTERN, UNIT_KEY_PATTERN, CHILD_INTERRUPTED_EVENT_PREFIX, CHILD_RESUMED_EVENT_PREFIX, THREAD_EVENT_MAX_BYTES, serializedBytes, isThreadEventStagedFile, isThreadEventAttachment, isThreadEventMode, UNIT_NUDGE_EVENT_PREFIX, CHECKS_SETTLED_EVENT_PREFIX, PULL_MERGED_EVENT_PREFIX, IDLE_WHY_MAX, REPO_SLUG, MAX_TEXT, MAX_REPORT, MAX_ROUNDS, isText, isOptionalText, isFinite, isCount, isObject, isPr, isResume, isThread, isRoundGate, isHumanGatePending, isUnitIdle, isSegment, isUnitWakeAnswer;
5860
+ var COORDINATOR_IDENTITY, COORDINATOR_STEP_ACTION, PLAN_MERGE_ACTION, COORDINATOR_STEP_PATH_PREFIX, INSTANCE_ID_PATTERN, STEP_NAME_PATTERN, IDEMPOTENCY_KEY_PATTERN, UNIT_PATTERN, UNIT_KEY_PATTERN, CHILD_INTERRUPTED_EVENT_PREFIX, CHILD_RESUMED_EVENT_PREFIX, THREAD_EVENT_MAX_BYTES, serializedBytes, isThreadEventStagedFile, isThreadEventAttachment, isThreadEventMode, UNIT_NUDGE_EVENT_PREFIX, CHECKS_SETTLED_EVENT_PREFIX, PULL_MERGED_EVENT_PREFIX, IDLE_WHY_MAX, REPO_SLUG, MAX_TEXT, MAX_REPORT, MAX_ROUNDS, isText, isOptionalText, isFinite, isCount, isObject, isPr, isResume, isFullSha, isPublication, isThread, isRoundGate, isHumanGatePending, isUnitIdle, isSegment, isUnitWakeAnswer;
5859
5861
  var init_contract2 = __esm({
5860
5862
  "../../src/core/coordinator/contract.ts"() {
5861
5863
  "use strict";
@@ -5895,6 +5897,8 @@ var init_contract2 = __esm({
5895
5897
  isObject = (v) => typeof v === "object" && v !== null;
5896
5898
  isPr = (v) => isObject(v) && isFinite(v.number) && isText(v.url, 2048);
5897
5899
  isResume = (v) => isObject(v) && isFinite(v.pr) && (v.headSha === void 0 || isText(v.headSha)) && (v.url === void 0 || isText(v.url, 2048));
5900
+ isFullSha = (v) => typeof v === "string" && /^[0-9a-f]{40}$/i.test(v);
5901
+ isPublication = (v) => isObject(v) && typeof v.repo === "string" && REPO_SLUG.test(v.repo) && typeof v.pr === "number" && Number.isInteger(v.pr) && v.pr > 0 && isText(v.headRef) && isText(v.baseRef) && isFullSha(v.expectedHeadSha) && isText(v.publicationRef) && isObject(v.owner) && typeof v.owner.instanceId === "string" && INSTANCE_ID_PATTERN.test(v.owner.instanceId) && typeof v.owner.unit === "string" && UNIT_PATTERN.test(v.owner.unit);
5898
5902
  isThread = (v) => isObject(v) && isText(v.threadKey) && isOptionalText(v.sourceUrl);
5899
5903
  isRoundGate = (v) => isObject(v) && isAddressSeverity(v.level) && Array.isArray(v.findings) && v.findings.every((f) => typeof f === "string");
5900
5904
  isHumanGatePending = (v) => isObject(v) && isObject(v.pr) && typeof v.pr.number === "number" && Number.isInteger(v.pr.number) && v.pr.number > 0 && isText(v.pr.url) && typeof v.round === "number" && Number.isInteger(v.round) && v.round >= 1 && Array.isArray(v.findings) && v.findings.every((finding) => isFindingShape(finding)) && (v.verdict === "approve" || v.verdict === "request_changes") && (v.reviewRunId === void 0 || isText(v.reviewRunId)) && (v.headSha === void 0 || isText(v.headSha)) && (v.askedAt === void 0 || isFinite(v.askedAt));
@@ -7128,9 +7132,9 @@ function makeSsrfLookup(resolve13) {
7128
7132
  const family = (ip) => ip.includes(":") ? 6 : 4;
7129
7133
  resolve13(hostname3).then(
7130
7134
  (ips) => {
7131
- const blocked = ips.find((ip) => ipInBlockedRange(ip));
7132
- if (blocked) {
7133
- cb(new BlockedUrlError(`host ${hostname3} resolves to blocked address ${blocked}`));
7135
+ const blocked2 = ips.find((ip) => ipInBlockedRange(ip));
7136
+ if (blocked2) {
7137
+ cb(new BlockedUrlError(`host ${hostname3} resolves to blocked address ${blocked2}`));
7134
7138
  return;
7135
7139
  }
7136
7140
  if (ips.length === 0) {
@@ -34683,6 +34687,9 @@ var init_instanceStore = __esm({
34683
34687
  async putUnits(_units) {
34684
34688
  return { ok: false, reason: "unavailable" };
34685
34689
  }
34690
+ async claimLegacyContinuation(_expected, _recovered) {
34691
+ return { ok: false, reason: "unavailable" };
34692
+ }
34686
34693
  async listUnits(_instanceId) {
34687
34694
  return [];
34688
34695
  }
@@ -34757,6 +34764,15 @@ var init_instanceStore = __esm({
34757
34764
  if (d.ok === true) return { ok: true };
34758
34765
  throw new Error(`coordinator store /runs/coordinator/units/put: unexpected answer (HTTP ${r.status})`);
34759
34766
  }
34767
+ async claimLegacyContinuation(expected, recovered) {
34768
+ const r = await this.post("/runs/coordinator/units/claim-legacy-continuation", { expected, recovered });
34769
+ const d = r.data;
34770
+ if (r.status === 409 && d.reason === "stale") return { ok: false, reason: "stale" };
34771
+ if (d.ok === true) return { ok: true };
34772
+ throw new Error(
34773
+ `coordinator store /runs/coordinator/units/claim-legacy-continuation: unexpected answer (HTTP ${r.status})`
34774
+ );
34775
+ }
34760
34776
  async listUnits(instanceId) {
34761
34777
  const r = await this.post("/runs/coordinator/units/list", { instanceId });
34762
34778
  const d = r.data;
@@ -36133,7 +36149,7 @@ function cardActivity(e) {
36133
36149
  if (e.type === "tool_call" && e.command !== void 0) return { kind: "command", tool: e.tool, command: e.command };
36134
36150
  return { kind: "line", text: activityLine(e) };
36135
36151
  }
36136
- function quietActivity(activity) {
36152
+ function compactActivity(activity) {
36137
36153
  if (activity?.kind === "command") return { kind: "line", text: `\u2192 ${activity.tool}` };
36138
36154
  return activity;
36139
36155
  }
@@ -36238,7 +36254,9 @@ function cardLines(trace, opts) {
36238
36254
  });
36239
36255
  return { ...shape ? { shape } : {}, ...opts.queued ? { queued: opts.queued } : {} };
36240
36256
  }
36241
- async function replyCommandOutput(io, parsed, text) {
36257
+ async function replyCommandOutput(io, parsed, text, display = {}) {
36258
+ if (parsed.kind === "invoke" && parsed.id === "steer.run" && display.ok === true && !shows(display.verbosity ?? "quiet", "verbose"))
36259
+ return;
36242
36260
  if (!io.attach || text.length <= LONG_COMMAND_REPLY_CHARS) return io.reply(text);
36243
36261
  const nl = text.indexOf("\n");
36244
36262
  const lead = nl === -1 ? text : text.slice(0, nl);
@@ -38783,6 +38801,79 @@ var init_commandChat = __esm({
38783
38801
  }
38784
38802
  });
38785
38803
 
38804
+ // ../../src/directives.ts
38805
+ function parseBudgetMinutes(value) {
38806
+ if (!/^\d+$/.test(value)) return void 0;
38807
+ const minutes = Number(value);
38808
+ return minutes >= MIN_BOUNDARY_MINUTES ? minutes : void 0;
38809
+ }
38810
+ function lastThreadDirectives(history) {
38811
+ const out = {};
38812
+ for (const h of history) {
38813
+ if (h.role !== "user") continue;
38814
+ for (const m of h.text.matchAll(DIRECTIVE_RE)) {
38815
+ if (m[1] === "model") out.model = m[2];
38816
+ else if (m[1] === "effort" && isEffort(m[2])) out.effort = m[2];
38817
+ else if (m[1] === "verbosity" && isVerbosity(m[2])) out.verbosity = m[2];
38818
+ }
38819
+ }
38820
+ return out;
38821
+ }
38822
+ function parseRenewals(value) {
38823
+ if (!/^\d+$/.test(value)) return void 0;
38824
+ const count = Number(value);
38825
+ return count <= GRANT_RENEWALS_MAX ? count : void 0;
38826
+ }
38827
+ function parseDirectives(input) {
38828
+ const out = { text: input };
38829
+ let text = input;
38830
+ const head = HEAD_AGENT_RE.exec(input);
38831
+ if (head !== null && AGENTS[head[1]] !== void 0) {
38832
+ out.agent = head[1];
38833
+ text = text.replace(head[0], " ");
38834
+ }
38835
+ for (const m of input.matchAll(DIRECTIVE_RE)) {
38836
+ const [token2, key, value] = m;
38837
+ if (key === "model") {
38838
+ out.model = value;
38839
+ } else if (key === "effort") {
38840
+ if (!isEffort(value)) continue;
38841
+ out.effort = value;
38842
+ } else if (key === "budget") {
38843
+ const minutes = parseBudgetMinutes(value);
38844
+ if (minutes === void 0) continue;
38845
+ out.budget = minutes;
38846
+ } else if (key === "severity") {
38847
+ if (!isAddressSeverity(value)) continue;
38848
+ out.severity = value;
38849
+ } else if (key === "renewals") {
38850
+ const count = parseRenewals(value);
38851
+ if (count === void 0) continue;
38852
+ out.renewals = count;
38853
+ } else {
38854
+ if (!isVerbosity(value)) continue;
38855
+ out.verbosity = value;
38856
+ }
38857
+ text = text.replace(token2, " ");
38858
+ }
38859
+ out.text = text.replace(/\s+/g, " ").trim();
38860
+ return out;
38861
+ }
38862
+ var DIRECTIVE_RE, HEAD_AGENT_RE;
38863
+ var init_directives = __esm({
38864
+ "../../src/directives.ts"() {
38865
+ "use strict";
38866
+ init_registry();
38867
+ init_validate();
38868
+ init_budgets();
38869
+ init_effort();
38870
+ init_shipPipeline();
38871
+ init_verbosity();
38872
+ DIRECTIVE_RE = /(?:^|\s)(model|effort|budget|severity|renewals|verbosity)[:=](\S+)/g;
38873
+ HEAD_AGENT_RE = /^\s*agent[:=](\S+)/;
38874
+ }
38875
+ });
38876
+
38786
38877
  // ../../src/core/runRegistry/runControl.ts
38787
38878
  var RunControl;
38788
38879
  var init_runControl = __esm({
@@ -39782,6 +39873,7 @@ var init_commandRun = __esm({
39782
39873
  async function answerChatCommand(deps, ctx) {
39783
39874
  const { msg, io, ending, trace } = ctx;
39784
39875
  const root = trace.root;
39876
+ const verbosity2 = deps.config.verbosityFor(msg.channelId, msg.userId, parseDirectives(msg.text).verbosity);
39785
39877
  if (deps.commands) {
39786
39878
  const chatCmd = parseChatCommand(msg.text, deps.commands);
39787
39879
  if (chatCmd) {
@@ -39801,7 +39893,7 @@ async function answerChatCommand(deps, ctx) {
39801
39893
  confirmations: deps.confirmations
39802
39894
  })
39803
39895
  );
39804
- } : () => root.span("post.reply", () => replyCommandOutput(io, chatCmd, res.text));
39896
+ } : () => root.span("post.reply", () => replyCommandOutput(io, chatCmd, res.text, { verbosity: verbosity2, ok: res.ok }));
39805
39897
  await ending.sealAfterReply(async () => {
39806
39898
  }, replyFn);
39807
39899
  if (res.followUp) postSettledOutcome(res.followUp, io, root);
@@ -39817,6 +39909,7 @@ var init_fastPath = __esm({
39817
39909
  init_commandChat();
39818
39910
  init_reply();
39819
39911
  init_refusal();
39912
+ init_directives();
39820
39913
  init_commandRun();
39821
39914
  init_commandRun();
39822
39915
  }
@@ -40110,79 +40203,6 @@ var init_references = __esm({
40110
40203
  }
40111
40204
  });
40112
40205
 
40113
- // ../../src/directives.ts
40114
- function parseBudgetMinutes(value) {
40115
- if (!/^\d+$/.test(value)) return void 0;
40116
- const minutes = Number(value);
40117
- return minutes >= MIN_BOUNDARY_MINUTES ? minutes : void 0;
40118
- }
40119
- function lastThreadDirectives(history) {
40120
- const out = {};
40121
- for (const h of history) {
40122
- if (h.role !== "user") continue;
40123
- for (const m of h.text.matchAll(DIRECTIVE_RE)) {
40124
- if (m[1] === "model") out.model = m[2];
40125
- else if (m[1] === "effort" && isEffort(m[2])) out.effort = m[2];
40126
- else if (m[1] === "verbosity" && isVerbosity(m[2])) out.verbosity = m[2];
40127
- }
40128
- }
40129
- return out;
40130
- }
40131
- function parseRenewals(value) {
40132
- if (!/^\d+$/.test(value)) return void 0;
40133
- const count = Number(value);
40134
- return count <= GRANT_RENEWALS_MAX ? count : void 0;
40135
- }
40136
- function parseDirectives(input) {
40137
- const out = { text: input };
40138
- let text = input;
40139
- const head = HEAD_AGENT_RE.exec(input);
40140
- if (head !== null && AGENTS[head[1]] !== void 0) {
40141
- out.agent = head[1];
40142
- text = text.replace(head[0], " ");
40143
- }
40144
- for (const m of input.matchAll(DIRECTIVE_RE)) {
40145
- const [token2, key, value] = m;
40146
- if (key === "model") {
40147
- out.model = value;
40148
- } else if (key === "effort") {
40149
- if (!isEffort(value)) continue;
40150
- out.effort = value;
40151
- } else if (key === "budget") {
40152
- const minutes = parseBudgetMinutes(value);
40153
- if (minutes === void 0) continue;
40154
- out.budget = minutes;
40155
- } else if (key === "severity") {
40156
- if (!isAddressSeverity(value)) continue;
40157
- out.severity = value;
40158
- } else if (key === "renewals") {
40159
- const count = parseRenewals(value);
40160
- if (count === void 0) continue;
40161
- out.renewals = count;
40162
- } else {
40163
- if (!isVerbosity(value)) continue;
40164
- out.verbosity = value;
40165
- }
40166
- text = text.replace(token2, " ");
40167
- }
40168
- out.text = text.replace(/\s+/g, " ").trim();
40169
- return out;
40170
- }
40171
- var DIRECTIVE_RE, HEAD_AGENT_RE;
40172
- var init_directives = __esm({
40173
- "../../src/directives.ts"() {
40174
- "use strict";
40175
- init_registry();
40176
- init_validate();
40177
- init_budgets();
40178
- init_effort();
40179
- init_shipPipeline();
40180
- init_verbosity();
40181
- DIRECTIVE_RE = /(?:^|\s)(model|effort|budget|severity|renewals|verbosity)[:=](\S+)/g;
40182
- HEAD_AGENT_RE = /^\s*agent[:=](\S+)/;
40183
- }
40184
- });
40185
-
40186
40206
  // ../../src/execution/githubRepoProbe.ts
40187
40207
  function githubRepoProbe(opts) {
40188
40208
  const fetchImpl = opts.fetch ?? fetch;
@@ -41563,10 +41583,8 @@ function presetRequestOf(line4) {
41563
41583
  async function executeOperatorDecision(deps, ctx) {
41564
41584
  const { event, io, msg } = ctx;
41565
41585
  const surface = io.offer ? "chat" : "typed";
41566
- const verbose = shows(
41567
- deps.config.verbosityFor(msg.channelId, msg.userId, parseDirectives(msg.text).verbosity),
41568
- "verbose"
41569
- );
41586
+ const verbosity2 = deps.config.verbosityFor(msg.channelId, msg.userId, parseDirectives(msg.text).verbosity);
41587
+ const verbose = shows(verbosity2, "verbose");
41570
41588
  const answered = { kind: "answered" };
41571
41589
  if (ctx.owner?.kind === "pipeline") return { kind: "fold" };
41572
41590
  if (event.outcome === "question") {
@@ -41673,7 +41691,7 @@ async function executeOperatorDecision(deps, ctx) {
41673
41691
  if (verbose) await io.reply(receipt);
41674
41692
  const res = await runChatCommand(deps, msg, io, invocation, ctx.ending, ctx.trace, { operator: executedEvent });
41675
41693
  carried = true;
41676
- if (res.text.length > 0) await io.reply(res.text);
41694
+ if (res.text.length > 0) await replyCommandOutput(io, invocation, res.text, { verbosity: verbosity2, ok: res.ok });
41677
41695
  }
41678
41696
  if (!carried) await recordOperatorDecision(deps, msg, event, ctx.ending, ctx.trace);
41679
41697
  return answered;
@@ -42352,7 +42370,7 @@ async function openAckCard(deps, ctx) {
42352
42370
  });
42353
42371
  if (route2) shell.note("debug", routeReasonLabel(route2.reason, route2.collapsed));
42354
42372
  const card = coalesceStatus(
42355
- await root.span("dispatch.ack_card", () => io.status(shell.ack())),
42373
+ await root.span("dispatch.ack_card", () => io.status(shell.ack(), { verbosity: resolved.verbosity })),
42356
42374
  deps.statusUpdateMinMs ?? STATUS_UPDATE_MIN_MS
42357
42375
  );
42358
42376
  trace.bindCard({ setupLabel: (label) => shell.setSetupLabel(label) });
@@ -44079,7 +44097,7 @@ function watchedChild(io, on) {
44079
44097
  on.replied(text);
44080
44098
  await io.reply(text);
44081
44099
  },
44082
- status: (initial) => io.status(initial),
44100
+ status: (initial, display) => io.status(initial, display),
44083
44101
  history: () => io.history(),
44084
44102
  runStarted: (started) => {
44085
44103
  io.runStarted?.(started);
@@ -46164,6 +46182,7 @@ async function claimRun(deps, ctx) {
46164
46182
  parentInstanceId: coordinator.parentInstanceId,
46165
46183
  ...unit !== void 0 ? { unit } : {},
46166
46184
  ...coordinator.base !== void 0 ? { base: coordinator.base } : {},
46185
+ ...coordinator.publication !== void 0 ? { publication: coordinator.publication } : {},
46167
46186
  at: clock4()
46168
46187
  });
46169
46188
  }
@@ -47446,7 +47465,7 @@ function established(facts2) {
47446
47465
  else if (w?.kind === "salvaged")
47447
47466
  tree = `What the tree held was pushed to \`${w.branch}\`${w.head ? ` at \`${shortSha2(w.head)}\`` : ""} by the budget salvage, unreviewed \u2014 a follow-up starts from it.`;
47448
47467
  else if (w?.kind === "left")
47449
- tree = w.fate === "kept" ? `${counted(w)} sit in the workspace, kept for this thread until it idles out \u2014 a follow-up here reuses them.` : w.fate === "discarded" ? `${counted(w)} were left in the tree and discarded at the run's end.` : `${counted(w)} were left in the tree, which is torn down since a command may still be running in it.`;
47468
+ tree = w.fate === "kept" ? `${counted(w)} sit in the workspace, kept for this thread until it idles out \u2014 a follow-up here reuses them.` : w.fate === "retained_unverified" ? `${counted(w)} remain in the attached workspace after the existing-PR push was blocked; retention beyond this run is unverified, so no continuation is promised.` : w.fate === "discarded" ? `${counted(w)} were left in the tree and discarded at the run's end.` : `${counted(w)} were left in the tree, which is torn down since a command may still be running in it.`;
47450
47469
  const description = facts2?.description === "submitted" ? "The PR description was submitted." : facts2?.description === "not_submitted" ? "No PR description was submitted." : "";
47451
47470
  return [tree, description].filter((s) => s !== "").join(" ");
47452
47471
  }
@@ -49979,10 +49998,28 @@ function judgeBashTimeout(timeout, ctx) {
49979
49998
  return refused4(commandPastLoopEndRefusal(Math.round(timeout), Math.max(0, Math.floor(leftMs / 1e3))));
49980
49999
  }
49981
50000
  function judgePush(tail, ctx) {
49982
- const [remote, refspec] = pushArguments(tail);
50001
+ const [remote, refspec, ...additionalRefspecs] = pushArguments(tail);
49983
50002
  if (remote !== void 0 && remote !== "origin") {
49984
50003
  return refused4(`repo:use \u2014 push to remote \`${remote}\`, not the run's repository (origin)`);
49985
50004
  }
50005
+ if (ctx.publication !== void 0) {
50006
+ const publication = ctx.publication.authority;
50007
+ if ("blocked" in publication) return refused4(`repo:use \u2014 existing-PR publication blocked: ${publication.blocked}`);
50008
+ if (refspec === void 0)
50009
+ return refused4("repo:use \u2014 existing-PR publication requires the explicit owned destination");
50010
+ if (additionalRefspecs.length > 0)
50011
+ return refused4("repo:use \u2014 existing-PR publication allows exactly one owned destination");
50012
+ const destination2 = refspec.replace(/^\+/, "").split(":").pop() ?? refspec;
50013
+ const branch3 = destination2 === "HEAD" ? ctx.branch : destination2.replace(/^refs\/heads\//, "");
50014
+ if (branch3 !== publication.ref)
50015
+ return refused4(`repo:use \u2014 push to \`${branch3}\`, not the owned publication ref ${publication.ref}`);
50016
+ const lease = `--force-with-lease=refs/heads/${publication.ref}:${publication.expectedHeadSha}`;
50017
+ if (!tail.split(/\s+/).includes(lease))
50018
+ return refused4(
50019
+ `repo:use \u2014 existing-PR publication requires \`${lease}\` so concurrent movement fails atomically`
50020
+ );
50021
+ return allowed;
50022
+ }
49986
50023
  if (refspec === void 0) return allowed;
49987
50024
  const destination = refspec.replace(/^\+/, "").split(":").pop() ?? refspec;
49988
50025
  if (ctx.branch !== void 0) {
@@ -50102,7 +50139,7 @@ function relayedToolDefinitions(harness) {
50102
50139
  }
50103
50140
  function authorizeToolCall(harness, ask) {
50104
50141
  harness.gateSaw(ask.toolCallId);
50105
- const blocked = harness.toolsBlocked();
50142
+ const blocked2 = harness.toolsBlocked();
50106
50143
  const refuse3 = (reason2) => {
50107
50144
  harness.emit({
50108
50145
  type: "run_note",
@@ -50111,7 +50148,7 @@ function authorizeToolCall(harness, ask) {
50111
50148
  });
50112
50149
  return { allow: false, reason: reason2 };
50113
50150
  };
50114
- if (blocked !== void 0) return refuse3(blocked);
50151
+ if (blocked2 !== void 0) return refuse3(blocked2);
50115
50152
  if (harness.tools.some((t) => t.name === ask.tool)) return { allow: true };
50116
50153
  const verdict = judgeToolCall(ask.tool, ask.input, harness.rules);
50117
50154
  if (verdict.verdict === "allowed") return { allow: true };
@@ -51991,6 +52028,49 @@ var init_resume = __esm({
51991
52028
  }
51992
52029
  });
51993
52030
 
52031
+ // ../../src/core/existingPrPublication.ts
52032
+ function verifyExistingPrPublication(durable, local, remote) {
52033
+ if (durable === void 0) return blocked("the durable existing-PR binding is missing");
52034
+ if (!fullSha(durable.expectedHeadSha)) return blocked("the durable expected head is not a full commit SHA");
52035
+ if (durable.headRef !== durable.publicationRef)
52036
+ return blocked("the durable head ref and owned publication ref do not agree");
52037
+ if (local.repo !== durable.repo) return blocked("the resolved repository does not match the durable binding");
52038
+ if (local.pr !== durable.pr) return blocked("the resolved pull request does not match the durable binding");
52039
+ if (local.ref !== durable.headRef) return blocked("the resolved head ref does not match the durable binding");
52040
+ if (local.baseRef !== durable.baseRef) return blocked("the resolved base ref does not match the durable binding");
52041
+ if (!fullSha(local.requestHeadSha)) return blocked("the request head is missing or not a full commit SHA");
52042
+ if (local.requestHeadSha.toLowerCase() !== durable.expectedHeadSha.toLowerCase())
52043
+ return blocked("the request head does not match the durable expected commit");
52044
+ if (local.workspaceRef !== durable.publicationRef)
52045
+ return blocked("the checked-out publication ref does not match the durable binding");
52046
+ if (!fullSha(local.workspaceHeadSha)) return blocked("the checked-out head is missing or not a full commit SHA");
52047
+ if (local.workspaceHeadSha.toLowerCase() !== durable.expectedHeadSha.toLowerCase())
52048
+ return blocked("the checked-out head does not match the durable expected commit");
52049
+ if (!sameOwner(local.owner, durable.owner)) return blocked("the publication owner changed");
52050
+ if (remote === void 0) return blocked("the fresh pull-request read is unavailable");
52051
+ if (remote.state !== "open") return blocked("the bound pull request is no longer open");
52052
+ if (remote.sameRepoHead !== true) return blocked("the pull request head is not in the bound repository");
52053
+ if (remote.headBranchExists !== true) return blocked("the pull request head branch is missing or unknown");
52054
+ if (remote.headRef !== durable.headRef) return blocked("the pull request head ref moved");
52055
+ if (remote.baseRef !== durable.baseRef) return blocked("the pull request base ref moved");
52056
+ if (!fullSha(remote.headSha)) return blocked("the fresh pull-request head is missing or not a full commit SHA");
52057
+ if (remote.headSha.toLowerCase() !== durable.expectedHeadSha.toLowerCase())
52058
+ return blocked("the pull request head moved from the durable expected commit");
52059
+ return {
52060
+ ok: true,
52061
+ publication: { ref: durable.publicationRef, expectedHeadSha: durable.expectedHeadSha.toLowerCase() }
52062
+ };
52063
+ }
52064
+ var fullSha, sameOwner, blocked;
52065
+ var init_existingPrPublication = __esm({
52066
+ "../../src/core/existingPrPublication.ts"() {
52067
+ "use strict";
52068
+ fullSha = (value) => value !== void 0 && /^[0-9a-f]{40}$/i.test(value);
52069
+ sameOwner = (left, right) => left?.instanceId === right.instanceId && left.unit === right.unit;
52070
+ blocked = (reason2) => ({ ok: false, reason: reason2 });
52071
+ }
52072
+ });
52073
+
51994
52074
  // ../../src/execution/githubComments.ts
51995
52075
  async function postReviewComment(target, body) {
51996
52076
  const codePoints = [...body].length;
@@ -52234,7 +52314,26 @@ async function salvageBudgetPush(executor, opts, span) {
52234
52314
  }
52235
52315
  const unpushed = parseCountOutput(await run2("git rev-list --count HEAD --not --remotes")) ?? 0;
52236
52316
  if (!dirty && !endingCheckpoint && unpushed === 0) return { pushed: false, summary: words.nothing };
52237
- await run2(`git push origin ${shellQuote(`HEAD:refs/heads/${opts.branch}`)}`);
52317
+ if (opts.publication !== void 0 && "blocked" in opts.publication)
52318
+ return {
52319
+ pushed: false,
52320
+ summary: `${words.failedLead}: existing-PR publication is blocked (${opts.publication.blocked}) \u2014 the commit remains unpublished in the bound workspace; no alternate ref was created`
52321
+ };
52322
+ const lease = opts.publication !== void 0 ? ` --force-with-lease=${shellQuote(`refs/heads/${opts.publication.ref}:${opts.publication.expectedHeadSha}`)}` : "";
52323
+ try {
52324
+ await run2(`git push${lease} origin ${shellQuote(`HEAD:refs/heads/${opts.branch}`)}`);
52325
+ } catch (err2) {
52326
+ if (opts.publication !== void 0) {
52327
+ const detail = err2 instanceof Error ? err2.message : String(err2);
52328
+ const publicationBlocked = `the atomic leased push was rejected: ${detail}`;
52329
+ return {
52330
+ pushed: false,
52331
+ publicationBlocked,
52332
+ summary: `${words.failedLead}: existing-PR publication is blocked (${publicationBlocked}) \u2014 the commit remains unpublished in the bound workspace; no alternate ref was created`
52333
+ };
52334
+ }
52335
+ throw err2;
52336
+ }
52238
52337
  const head = parseRevParseOutput(await probe("git rev-parse HEAD"));
52239
52338
  return {
52240
52339
  pushed: true,
@@ -52961,6 +53060,12 @@ async function runLoop(deps, ctx) {
52961
53060
  return void 0;
52962
53061
  }
52963
53062
  })();
53063
+ let existingPrPublication;
53064
+ let existingPrPublicationFence;
53065
+ const blockExistingPrPublication = (reason2) => {
53066
+ existingPrPublication = { blocked: reason2 };
53067
+ if (existingPrPublicationFence !== void 0) existingPrPublicationFence.authority = existingPrPublication;
53068
+ };
52964
53069
  const identitySeam = deps.identityRewrite;
52965
53070
  const startStateRead = identitySeam !== void 0 && isCodingPrRun && repoCtx.repo !== void 0 ? (async () => {
52966
53071
  const repo = repoCtx.repo;
@@ -52997,14 +53102,14 @@ async function runLoop(deps, ctx) {
52997
53102
  );
52998
53103
  return pace ? ` \xB7 ${pace}` : "";
52999
53104
  };
53105
+ const showsActivity = shows(resolved.verbosity, "verbose");
53000
53106
  const chatty = shows(resolved.verbosity, "debug");
53001
53107
  const showBookkeeping = shows(resolved.verbosity, "verbose");
53002
53108
  const currentFrame = () => shell.live({
53003
- // the pace mark rides every level (the stall signal is a fact, not chatter); the quiet suffix is debug's
53004
- suffix: chatty ? `${quietSuffix(clock4() - lastActivityAt, inFlightTool)}${cardPace()}` : cardPace(),
53109
+ suffix: showsActivity ? chatty ? `${quietSuffix(clock4() - lastActivityAt, inFlightTool)}${cardPace()}` : cardPace() : "",
53005
53110
  notice: shutdownNotice(),
53006
53111
  detail: [checklist],
53007
- activity: chatty ? lastActivity : quietActivity(lastActivity)
53112
+ activity: showsActivity ? chatty ? lastActivity : compactActivity(lastActivity) : void 0
53008
53113
  });
53009
53114
  const checklistAsLeft = () => checklist;
53010
53115
  const checklistCheckedOff = () => checklist?.replace(/^(\s*)[○✱](?=\s)/gm, "$1\u2713");
@@ -53239,8 +53344,18 @@ async function runLoop(deps, ctx) {
53239
53344
  });
53240
53345
  const salvaged = "skipped" in target ? { pushed: false, summary: target.skipped } : await root.span(
53241
53346
  cue === "budget" ? "run.budget_salvage" : "run.work_salvage",
53242
- (span) => salvageBudgetPush(executor, { branch: target.branch, cue }, span)
53347
+ (span) => salvageBudgetPush(
53348
+ executor,
53349
+ {
53350
+ branch: target.branch,
53351
+ cue,
53352
+ ...existingPrPublication !== void 0 ? { publication: existingPrPublication } : {}
53353
+ },
53354
+ span
53355
+ )
53243
53356
  );
53357
+ if ("publicationBlocked" in salvaged && salvaged.publicationBlocked !== void 0)
53358
+ blockExistingPrPublication(salvaged.publicationBlocked);
53244
53359
  const cleanCompletion = cue === "completion" && !salvaged.pushed && (/ended with nothing to preserve/i.test(salvaged.summary) || "skipped" in target && (observedUncommitted ?? 0) === 0 && (observedUnpushed ?? 0) === 0);
53245
53360
  if (cleanCompletion) return;
53246
53361
  workSalvageAttempted = true;
@@ -53255,7 +53370,10 @@ async function runLoop(deps, ctx) {
53255
53370
  salvagedTo = { branch: target.branch, head: salvaged.head };
53256
53371
  } else if (salvaged.pushed && !("skipped" in target)) salvagedTo = { branch: target.branch };
53257
53372
  if (salvaged.pushed) await observeWorkspaceNow();
53258
- else if ("skipped" in target || /failed|not attempted/i.test(salvaged.summary))
53373
+ else if (existingPrPublication !== void 0 && "blocked" in existingPrPublication) {
53374
+ await observeWorkspaceNow();
53375
+ shell.note("quiet", salvaged.summary);
53376
+ } else if ("skipped" in target || /failed|not attempted/i.test(salvaged.summary))
53259
53377
  shell.note("quiet", salvaged.summary);
53260
53378
  };
53261
53379
  let relaunchEndedRun = false;
@@ -53281,7 +53399,9 @@ async function runLoop(deps, ctx) {
53281
53399
  return true;
53282
53400
  };
53283
53401
  const tailSkipped = () => run2.control.requested === "hard" || relaunchEndedRun || latchReviewStoppedBeforeStart();
53402
+ const keepBlockedPublicationWorkspace = () => existingPrPublication !== void 0 && "blocked" in existingPrPublication && run2.control.requested !== "hard" && !commandInFlight && !gateBypassed && ((observedUncommitted ?? 0) > 0 || (observedUnpushed ?? 0) > 0);
53284
53403
  const releaseWorkspace = (span) => {
53404
+ if (keepBlockedPublicationWorkspace()) return Promise.resolve();
53285
53405
  const events2 = recordEvents();
53286
53406
  const pushed = pushedBranchesOf(events2);
53287
53407
  return round2.release({
@@ -53434,7 +53554,38 @@ async function runLoop(deps, ctx) {
53434
53554
  if (!providerCfg)
53435
53555
  throw new Error(`the ${harness.name} harness found no provider named ${providerName} in the config`);
53436
53556
  const prBase = repoCtx.baseRef ?? await coordinatorBase;
53437
- const ownBranch = prBase !== void 0 ? binding?.ref ?? repoCtx.ref : void 0;
53557
+ if (coordinator?.publication !== void 0) {
53558
+ const publication = coordinator.publication;
53559
+ const fresh = await (deps.fetchPrFacts ?? fetchPullRequestFacts)({
53560
+ repo: publication.repo,
53561
+ number: publication.pr
53562
+ }).catch(() => void 0);
53563
+ const verified = verifyExistingPrPublication(
53564
+ publication,
53565
+ {
53566
+ repo: repoCtx.repo,
53567
+ pr: repoCtx.pr,
53568
+ ref: repoCtx.ref,
53569
+ baseRef: prBase,
53570
+ requestHeadSha: repoCtx.headSha,
53571
+ workspaceRef: binding?.ref,
53572
+ workspaceHeadSha: binding?.sha,
53573
+ owner: {
53574
+ instanceId: coordinator.parentInstanceId,
53575
+ unit: unitOfIdempotencyKey(coordinator.idempotencyKey) ?? ""
53576
+ }
53577
+ },
53578
+ fresh
53579
+ );
53580
+ existingPrPublication = verified.ok ? verified.publication : { blocked: verified.reason };
53581
+ existingPrPublicationFence = { authority: existingPrPublication };
53582
+ if (!verified.ok)
53583
+ shell.note(
53584
+ "quiet",
53585
+ `Existing-PR publication is blocked: ${verified.reason}. Work stays on the bound checkout; no alternate branch or pull request will be published.`
53586
+ );
53587
+ }
53588
+ const ownBranch = coordinator?.publication !== void 0 ? coordinator.publication.publicationRef : prBase !== void 0 ? binding?.ref ?? repoCtx.ref : void 0;
53438
53589
  const protectedBranches = [
53439
53590
  ...new Set(
53440
53591
  (prBase !== void 0 ? [prBase] : [binding?.ref, repoCtx.ref]).filter(
@@ -53494,8 +53645,18 @@ async function runLoop(deps, ctx) {
53494
53645
  const branch2 = ownBranch;
53495
53646
  const salvaged = await root.span(
53496
53647
  "run.compaction_salvage",
53497
- (span) => salvageBudgetPush(executor, { branch: branch2, cue: "compaction" }, span)
53648
+ (span) => salvageBudgetPush(
53649
+ executor,
53650
+ {
53651
+ branch: branch2,
53652
+ cue: "compaction",
53653
+ ...existingPrPublication !== void 0 ? { publication: existingPrPublication } : {}
53654
+ },
53655
+ span
53656
+ )
53498
53657
  );
53658
+ if (salvaged.publicationBlocked !== void 0)
53659
+ blockExistingPrPublication(salvaged.publicationBlocked);
53499
53660
  onEvent({
53500
53661
  type: "run_note",
53501
53662
  kind: "compaction_salvage",
@@ -53508,7 +53669,8 @@ async function runLoop(deps, ctx) {
53508
53669
  rules: {
53509
53670
  checkout: binding?.workspace ?? "/workspace",
53510
53671
  ...ownBranch !== void 0 ? { branch: ownBranch } : {},
53511
- protectedBranches
53672
+ protectedBranches,
53673
+ ...existingPrPublicationFence !== void 0 ? { publication: existingPrPublicationFence } : {}
53512
53674
  },
53513
53675
  ...round2.selection.backend ? { backend: round2.selection.backend } : {},
53514
53676
  span: root,
@@ -53724,6 +53886,12 @@ async function runLoop(deps, ctx) {
53724
53886
  if (descriptionTurnRan) await preserveCodingChildWork("completion");
53725
53887
  await endHarness();
53726
53888
  const leftBehind = isCodingPrRun && !tailSkipped() && !workSalvageAttempted ? workLeftBehindOf({ uncommittedChanges: observedUncommitted, unpushedCommits: observedUnpushed }) : void 0;
53889
+ const blockedCheckpoint = keepBlockedPublicationWorkspace() && existingPrPublication !== void 0 && "blocked" in existingPrPublication && observedUncommitted !== void 0 && observedUnpushed !== void 0 && (observedUncommitted > 0 || observedUnpushed > 0) ? {
53890
+ reason: existingPrPublication.blocked,
53891
+ uncommitted: observedUncommitted,
53892
+ unpushed: observedUnpushed,
53893
+ ...observedHead !== void 0 ? { head: observedHead } : {}
53894
+ } : void 0;
53727
53895
  const endingFacts = () => {
53728
53896
  if (profile.machine === "none") return { workspace: { kind: "none" } };
53729
53897
  if (!isCodingPrRun || tailSkipped()) return { workspace: { kind: "unread" } };
@@ -53742,11 +53910,16 @@ async function runLoop(deps, ctx) {
53742
53910
  kind: "left",
53743
53911
  uncommitted: observedUncommitted,
53744
53912
  unpushed: observedUnpushed,
53745
- fate: commandInFlight ? "torn_down" : profile.machine === "repo-resident" ? "discarded" : "kept"
53913
+ fate: blockedCheckpoint !== void 0 ? "retained_unverified" : commandInFlight ? "torn_down" : profile.machine === "repo-resident" ? "discarded" : "kept"
53746
53914
  };
53747
53915
  return { workspace, description: prDescription !== void 0 ? "submitted" : "not_submitted" };
53748
53916
  };
53749
- if (leftBehind) {
53917
+ if (blockedCheckpoint !== void 0) {
53918
+ const checkpoint = blockedCheckpoint.head ? ` at ${blockedCheckpoint.head.slice(0, 7)}` : "";
53919
+ const summary = `the existing-PR push is blocked (${blockedCheckpoint.reason}); kept the local checkpoint${checkpoint} with ${blockedCheckpoint.uncommitted} uncommitted change(s) and ${blockedCheckpoint.unpushed} unpushed commit(s) in the run workspace; no alternate ref or pull request was created, and retention beyond this run is unverified`;
53920
+ events.publish({ type: "run_note", kind: "publication_blocked", summary: oneLine(summary), at: clock4() });
53921
+ shell.note("quiet", `\u26A0\uFE0F ${summary}`);
53922
+ } else if (leftBehind) {
53750
53923
  events.publish({
53751
53924
  type: "run_note",
53752
53925
  kind: "work_left_behind",
@@ -53762,7 +53935,7 @@ async function runLoop(deps, ctx) {
53762
53935
  at: clock4()
53763
53936
  });
53764
53937
  }
53765
- if (isCodingPrRun && !tailSkipped() && !endingSalvageAttempted) {
53938
+ if (isCodingPrRun && !tailSkipped() && !endingSalvageAttempted && !(existingPrPublication !== void 0 && "blocked" in existingPrPublication)) {
53766
53939
  const instance = coordinator && deps.coordinatorInstances ? await deps.coordinatorInstances.get(coordinator.parentInstanceId).catch(() => void 0) : void 0;
53767
53940
  const requester = instance?.userId === msg.userId ? instance : msg;
53768
53941
  const requestedLogin = identitySeam !== void 0 ? await identitySeam.requestedLogin(requester.userId).catch(() => void 0) : void 0;
@@ -54033,6 +54206,7 @@ var init_runLoop = __esm({
54033
54206
  "use strict";
54034
54207
  init_actor();
54035
54208
  init_predicate();
54209
+ init_contract2();
54036
54210
  init_decisionRecordReservation();
54037
54211
  init_profile();
54038
54212
  init_provider();
@@ -54048,6 +54222,7 @@ var init_runLoop = __esm({
54048
54222
  init_resume();
54049
54223
  init_githubPulls();
54050
54224
  init_repoContext();
54225
+ init_existingPrPublication();
54051
54226
  init_prDescription();
54052
54227
  init_handoff();
54053
54228
  init_reviewVerdict();
@@ -54202,7 +54377,7 @@ async function shipPreflight(input) {
54202
54377
  `\u{1F6AB} ${where}'s head branch lives on a fork, not on \`${repo}\` \u2014 ship cannot drive it.`
54203
54378
  );
54204
54379
  }
54205
- const branch2 = facts2.headRef ?? repoCtx.ref;
54380
+ const branch2 = facts2.headRef;
54206
54381
  if (!branch2) {
54207
54382
  return refuse2(
54208
54383
  "ship_preflight_head_unknown",
@@ -54211,10 +54386,34 @@ async function shipPreflight(input) {
54211
54386
  `\u{1F6AB} Could not determine ${where}'s head branch, so ship cannot bind the thread's worktree to it \u2014 refusing fail-closed.`
54212
54387
  );
54213
54388
  }
54214
- const ownBase = facts2.baseRef ?? repoCtx.baseRef;
54389
+ const headSha = facts2.headSha;
54390
+ if (headSha === void 0 || !/^[0-9a-f]{40}$/i.test(headSha)) {
54391
+ return refuse2(
54392
+ "ship_preflight_head_unknown",
54393
+ "full head commit unknown",
54394
+ "not started (head commit unknown)",
54395
+ `\u{1F6AB} Could not determine ${where}'s full head commit, so ship cannot fence publication to it \u2014 refusing fail-closed.`
54396
+ );
54397
+ }
54398
+ if (facts2.baseRef === void 0) {
54399
+ return refuse2(
54400
+ "ship_preflight_head_unknown",
54401
+ "base branch unknown",
54402
+ "not started (base branch unknown)",
54403
+ `\u{1F6AB} Could not determine ${where}'s base branch, so ship cannot bind publication to it \u2014 refusing fail-closed.`
54404
+ );
54405
+ }
54406
+ const ownBase = facts2.baseRef;
54215
54407
  const ownBaseExists = ownBase !== void 0 && input.refExists ? await input.refExists(repo, ownBase).catch(() => void 0) : void 0;
54216
- const baseFallback = ownBase !== void 0 && ownBaseExists === false ? { requested: ownBase } : void 0;
54217
- const base = resolveBaseRef([ownBaseExists === false ? void 0 : ownBase], info?.defaultBranch);
54408
+ if (ownBaseExists === false) {
54409
+ return refuse2(
54410
+ "ship_preflight_head_unknown",
54411
+ "pull request base branch missing",
54412
+ "not started (base branch missing)",
54413
+ `\u{1F6AB} ${where}'s base branch \`${ownBase}\` is not present in \`${repo}\`, so ship cannot establish an exact publication binding \u2014 refusing fail-closed.`
54414
+ );
54415
+ }
54416
+ const base = resolveBaseRef([ownBase], info?.defaultBranch);
54218
54417
  const autoMerge = facts2.autoMergeEnabled !== void 0 ? { autoMergeEnabled: facts2.autoMergeEnabled } : {};
54219
54418
  if (task) {
54220
54419
  return {
@@ -54223,8 +54422,7 @@ async function shipPreflight(input) {
54223
54422
  repo,
54224
54423
  branch: branch2,
54225
54424
  base,
54226
- adopt: { pr: repoCtx.pr, ...facts2.htmlUrl !== void 0 ? { url: facts2.htmlUrl } : {} },
54227
- ...baseFallback !== void 0 ? { baseFallback } : {},
54425
+ adopt: { pr: repoCtx.pr, headSha, ...facts2.htmlUrl !== void 0 ? { url: facts2.htmlUrl } : {} },
54228
54426
  ...autoMerge
54229
54427
  }
54230
54428
  };
@@ -54237,10 +54435,9 @@ async function shipPreflight(input) {
54237
54435
  base,
54238
54436
  resume: {
54239
54437
  pr: repoCtx.pr,
54240
- headSha: facts2.headSha ?? repoCtx.headSha,
54438
+ headSha,
54241
54439
  ...facts2.htmlUrl !== void 0 ? { url: facts2.htmlUrl } : {}
54242
54440
  },
54243
- ...baseFallback !== void 0 ? { baseFallback } : {},
54244
54441
  ...autoMerge
54245
54442
  }
54246
54443
  };
@@ -54432,15 +54629,27 @@ async function rowsFor(deps, p, selected, instanceId, carried) {
54432
54629
  const previous = carried?.get(u.id);
54433
54630
  const taskKey = p.recordTasks?.[u.id];
54434
54631
  const record3 = taskKey !== void 0 ? await reserve(p.identity.repo, taskKey, previous?.record) : previous?.record;
54632
+ const branch2 = p.entryBranch ?? u.branch;
54633
+ const existing = p.adopt ?? p.resume;
54634
+ const publication = existing !== void 0 && p.entryBranch !== void 0 ? {
54635
+ repo: p.identity.repo,
54636
+ pr: existing.pr,
54637
+ headRef: p.entryBranch,
54638
+ baseRef: p.base,
54639
+ expectedHeadSha: existing.headSha,
54640
+ publicationRef: p.entryBranch,
54641
+ owner: { instanceId, unit: u.id }
54642
+ } : void 0;
54435
54643
  return {
54436
54644
  instanceId,
54437
54645
  unit: u.id,
54438
54646
  slug: u.slug,
54439
54647
  title: u.title,
54440
- branch: p.entryBranch ?? u.branch,
54648
+ branch: branch2,
54441
54649
  dependsOn: u.dependsOn,
54442
54650
  rounds: [],
54443
54651
  ...p.resume !== void 0 ? { resume: p.resume } : {},
54652
+ ...publication !== void 0 ? { publication } : {},
54444
54653
  ...previous?.lastPush !== void 0 ? { lastPush: previous.lastPush } : {},
54445
54654
  ...record3 !== void 0 ? { record: record3 } : {}
54446
54655
  };
@@ -54577,75 +54786,103 @@ async function handOffToCoordinatorUnchecked(deps, input) {
54577
54786
  }
54578
54787
  async function start(deps, input, instance, units, where, write) {
54579
54788
  const log = deps.log ?? console.log;
54580
- const put = write === "replace" ? await deps.instances.replace(instance) : await deps.instances.put(instance);
54581
- if (!put.ok) {
54582
- if (put.reason === "unavailable")
54789
+ const reservation = await input.beforeStart?.();
54790
+ if (reservation?.ok === false)
54791
+ return { status: "aborted", reply: reservation.refusal.text, refusal: reservation.refusal };
54792
+ let started = false;
54793
+ try {
54794
+ const put = write === "replace" ? await deps.instances.replace(instance) : await deps.instances.put(instance);
54795
+ if (!put.ok) {
54796
+ if (put.reason === "unavailable")
54797
+ return refused5(
54798
+ "plan_history_unavailable",
54799
+ "\u26A0\uFE0F The plan runner needs run history on the state Worker (`runHistory.worker`): the instance record could not be written, so nothing ran."
54800
+ );
54583
54801
  return refused5(
54584
- "plan_history_unavailable",
54585
- "\u26A0\uFE0F The plan runner needs run history on the state Worker (`runHistory.worker`): the instance record could not be written, so nothing ran."
54802
+ "plan_runner_conflict",
54803
+ `\u{1F6AB} A runner for \`${instance.id}\` was just recorded by another request, so this request started nothing; the recorded runner owns the pipeline.`
54586
54804
  );
54587
- return refused5(
54588
- "plan_runner_conflict",
54589
- `\u{1F6AB} A runner for \`${instance.id}\` was just recorded by another request, so this request started nothing; the recorded runner owns the pipeline.`
54590
- );
54591
- }
54592
- const rows = await deps.instances.putUnits(units);
54593
- if (!rows.ok)
54594
- return refused5(
54595
- "plan_history_unavailable",
54596
- "\u26A0\uFE0F The plan runner needs run history on the state Worker: the unit rows could not be written, so nothing ran."
54597
- );
54598
- const accepted = [...input.msg.images ?? [], ...input.msg.documents ?? []];
54599
- if (instance.plan?.path === void 0 && accepted.length > 0) {
54600
- const unit = units[0];
54601
- const seeded = await deps.instances.appendEvent(
54602
- { instanceId: instance.id, unit: unit.unit },
54603
- {
54604
- id: `${instance.id}:${unit.unit}:ship-request`,
54605
- sender: input.msg.userId,
54606
- ...input.msg.userName !== void 0 ? { senderName: input.msg.userName } : {},
54607
- text: "Attachments from the ship request.",
54608
- attachments: accepted,
54609
- mode: "steer",
54610
- at: input.now
54611
- }
54612
- );
54613
- if (!seeded.ok)
54805
+ }
54806
+ const rows = await deps.instances.putUnits(units);
54807
+ if (!rows.ok)
54614
54808
  return refused5(
54615
54809
  "plan_history_unavailable",
54616
- "\u26A0\uFE0F The plan runner needs run history on the state Worker: the ship request's attachments could not be written, so nothing ran."
54810
+ "\u26A0\uFE0F The plan runner needs run history on the state Worker: the unit rows could not be written, so nothing ran."
54617
54811
  );
54618
- }
54619
- let answer;
54620
- try {
54621
- answer = await deps.create(instance.id);
54622
- } catch (err2) {
54623
- answer = { kind: "unanswered", reason: describe7(err2) };
54624
- }
54625
- switch (answer.kind) {
54626
- case "created": {
54627
- log(`[ship] ${input.msg.threadKey}: handed to the plan runner ${instance.id} (${units.length} unit(s))`);
54628
- const replaced = write === "replace" ? ["the records of an earlier attempt that never started were replaced"] : [];
54629
- return {
54630
- status: "completed",
54631
- reply: handedOff([...where, ...replaced]),
54632
- instanceId: instance.id
54633
- };
54634
- }
54635
- case "duplicate": {
54636
- const status3 = answer.status !== void 0 ? `, status: ${answer.status}` : "";
54637
- return refused5(
54638
- "plan_instance_orphaned",
54639
- `\u{1F6AB} A Workflow instance \`${instance.id}\` already exists on the platform${status3} but the state Worker knew nothing of it \u2014 a person decides; re-issuing will not resume it.`
54812
+ const accepted = [...input.msg.images ?? [], ...input.msg.documents ?? []];
54813
+ if (instance.plan?.path === void 0 && accepted.length > 0) {
54814
+ const unit = units[0];
54815
+ const seeded = await deps.instances.appendEvent(
54816
+ { instanceId: instance.id, unit: unit.unit },
54817
+ {
54818
+ id: `${instance.id}:${unit.unit}:ship-request`,
54819
+ sender: input.msg.userId,
54820
+ ...input.msg.userName !== void 0 ? { senderName: input.msg.userName } : {},
54821
+ text: "Attachments from the ship request.",
54822
+ attachments: accepted,
54823
+ mode: "steer",
54824
+ at: input.now
54825
+ }
54640
54826
  );
54827
+ if (!seeded.ok)
54828
+ return refused5(
54829
+ "plan_history_unavailable",
54830
+ "\u26A0\uFE0F The plan runner needs run history on the state Worker: the ship request's attachments could not be written, so nothing ran."
54831
+ );
54641
54832
  }
54642
- case "failed":
54643
- case "unanswered":
54644
- log(`[ship] ${input.msg.threadKey}: the plan runner ${instance.id} could not be started \u2014 ${answer.reason}`);
54645
- return refused5(
54646
- "plan_start_failed",
54647
- `\u26A0\uFE0F This is a bug: the plan runner could not be started (${answer.reason}), nothing ran, and no automatic start retry was scheduled.`
54648
- );
54833
+ if (reservation?.ok === true) {
54834
+ const unit = units[0];
54835
+ if (unit === void 0)
54836
+ return refused5(
54837
+ "setup_failed",
54838
+ "\u26A0\uFE0F The plan runner had no durable unit to own at its final start gate, so nothing ran."
54839
+ );
54840
+ try {
54841
+ await reservation.commit({ instanceId: instance.id, unit: unit.unit });
54842
+ } catch (err2) {
54843
+ return refused5(
54844
+ "setup_failed",
54845
+ `\u26A0\uFE0F The plan runner could not transfer existing-pull-request ownership to its new attempt (${describe7(err2)}), so nothing ran.`
54846
+ );
54847
+ }
54848
+ }
54849
+ let answer;
54850
+ try {
54851
+ answer = await deps.create(instance.id);
54852
+ } catch (err2) {
54853
+ answer = { kind: "unanswered", reason: describe7(err2) };
54854
+ }
54855
+ switch (answer.kind) {
54856
+ case "created": {
54857
+ if (reservation?.ok === true) reservation.complete();
54858
+ started = true;
54859
+ log(`[ship] ${input.msg.threadKey}: handed to the plan runner ${instance.id} (${units.length} unit(s))`);
54860
+ const replaced = write === "replace" ? ["the records of an earlier attempt that never started were replaced"] : [];
54861
+ return {
54862
+ status: "completed",
54863
+ reply: handedOff([...where, ...replaced]),
54864
+ instanceId: instance.id
54865
+ };
54866
+ }
54867
+ case "duplicate": {
54868
+ const status3 = answer.status !== void 0 ? `, status: ${answer.status}` : "";
54869
+ return refused5(
54870
+ "plan_instance_orphaned",
54871
+ `\u{1F6AB} A Workflow instance \`${instance.id}\` already exists on the platform${status3} but the state Worker knew nothing of it \u2014 a person decides; re-issuing will not resume it.`
54872
+ );
54873
+ }
54874
+ case "failed":
54875
+ case "unanswered":
54876
+ log(`[ship] ${input.msg.threadKey}: the plan runner ${instance.id} could not be started \u2014 ${answer.reason}`);
54877
+ return refused5(
54878
+ "plan_start_failed",
54879
+ `\u26A0\uFE0F This is a bug: the plan runner could not be started (${answer.reason}), nothing ran, and no automatic start retry was scheduled.`
54880
+ );
54881
+ }
54882
+ const unreachable3 = answer;
54883
+ return unreachable3;
54884
+ } finally {
54885
+ if (!started && reservation?.ok === true) await reservation.abort();
54649
54886
  }
54650
54887
  }
54651
54888
  function handedOff(facts2) {
@@ -55006,6 +55243,7 @@ async function runShipBranch(deps, msg, io, ctx) {
55006
55243
  ...msg.authenticatedAs !== void 0 ? { authenticatedAs: msg.authenticatedAs } : {},
55007
55244
  ...msg.postedBy !== void 0 ? { postedBy: msg.postedBy } : {},
55008
55245
  ...entry.resume !== void 0 ? { pr: entry.resume.pr } : {},
55246
+ verbosity: ctx.verbosity,
55009
55247
  profile
55010
55248
  },
55011
55249
  card: card.handle ?? null,
@@ -55098,6 +55336,7 @@ async function runShipBranch(deps, msg, io, ctx) {
55098
55336
  entry,
55099
55337
  requestText: directives.text,
55100
55338
  ...ctx.reissuePlanId !== void 0 ? { reissuePlanId: ctx.reissuePlanId } : {},
55339
+ ...ctx.beforeCoordinatorStart !== void 0 ? { beforeStart: ctx.beforeCoordinatorStart } : {},
55101
55340
  msg,
55102
55341
  agentSource: ctx.agentSource,
55103
55342
  runId: run2.id,
@@ -55349,7 +55588,13 @@ function carriedCoordinatorTag(row2, events) {
55349
55588
  if (typeof parentInstanceId !== "string" || typeof idempotencyKey !== "string") return void 0;
55350
55589
  const tag = events.find((e) => e.type === "coordinator_tag");
55351
55590
  const base = tag?.type === "coordinator_tag" ? tag.base : void 0;
55352
- return { parentInstanceId, idempotencyKey, ...base !== void 0 ? { base } : {} };
55591
+ const publication = tag?.type === "coordinator_tag" ? tag.publication : void 0;
55592
+ return {
55593
+ parentInstanceId,
55594
+ idempotencyKey,
55595
+ ...base !== void 0 ? { base } : {},
55596
+ ...publication !== void 0 ? { publication } : {}
55597
+ };
55353
55598
  }
55354
55599
  function lostWorkspaceNote(why, restarts) {
55355
55600
  const lost = `resumed after a restart: the run's workspace could not be re-attached (${why})`;
@@ -55885,6 +56130,29 @@ function continuationBudgetOf(instance, unit) {
55885
56130
  if (remainingRounds <= 0) return { kind: "exhausted", budget: "review-round" };
55886
56131
  return { kind: "remaining", caps: { maxRounds: remainingRounds, maxMinutes: remainingMinutes } };
55887
56132
  }
56133
+ async function legacyReviewedHeadOf(runs, instance, unit, prNumber) {
56134
+ const listed = await runs.listUnitRuns(unitKeyOf({ instanceId: instance.id, unit: unit.unit }), { kind: "all" }).catch(() => void 0);
56135
+ if (listed === void 0 || !listed.ok || listed.value.unit !== unitKeyOf({ instanceId: instance.id, unit: unit.unit }) || listed.value.instanceId !== instance.id || listed.value.id !== unit.unit || listed.value.instance.id !== instance.id || listed.value.instance.repo.toLowerCase() !== instance.repo.toLowerCase())
56136
+ return void 0;
56137
+ const prefix = `${instance.id}:${unit.unit}/`;
56138
+ const owned = listed.value.runs.filter(
56139
+ (run2) => run2.finished && run2.status === "completed" && run2.parentInstanceId === instance.id && run2.idempotencyKey?.startsWith(prefix) === true && run2.userId === instance.userId && run2.repo?.toLowerCase() === instance.repo.toLowerCase()
56140
+ );
56141
+ const coding = new Set(
56142
+ owned.flatMap(
56143
+ (run2) => run2.agent === "coding" && run2.pr?.number === prNumber && typeof run2.headSha === "string" && FULL_SHA2.test(run2.headSha) ? [run2.headSha.toLowerCase()] : []
56144
+ )
56145
+ );
56146
+ const approved = new Set(
56147
+ owned.flatMap((run2) => {
56148
+ const head = run2.reviewHead;
56149
+ const post = run2.reviewPost;
56150
+ return run2.agent === "review" && typeof head === "string" && FULL_SHA2.test(head) && run2.verdict?.verdict === "approve" && run2.verdict.head?.toLowerCase() === head.toLowerCase() && post?.posted === true && post.target.repo.toLowerCase() === instance.repo.toLowerCase() && post.target.number === prNumber && post.head.toLowerCase() === head.toLowerCase() && post.verdict === "approve" ? [head.toLowerCase()] : [];
56151
+ })
56152
+ );
56153
+ const exact = [...approved].filter((head) => coding.has(head));
56154
+ return exact.length === 1 ? exact[0] : void 0;
56155
+ }
55888
56156
  async function dispatch(deps, msg, io, opts = {}) {
55889
56157
  const ended = { status: "completed" };
55890
56158
  const resume = opts.resume;
@@ -55969,6 +56237,7 @@ async function dispatch(deps, msg, io, opts = {}) {
55969
56237
  let stoppedWhileAttaching;
55970
56238
  let resumeRowClosed = false;
55971
56239
  let restartRequest;
56240
+ let releaseLegacyOwnership;
55972
56241
  const reservationHooks = {
55973
56242
  onStop: (mode) => void registered?.control.requestStop(mode),
55974
56243
  onFenced: () => {
@@ -55987,6 +56256,8 @@ async function dispatch(deps, msg, io, opts = {}) {
55987
56256
  let operatorRepo;
55988
56257
  let reissuePlanId;
55989
56258
  let reissueCaps;
56259
+ let beforeCoordinatorStart;
56260
+ let reserveLegacyOwnership;
55990
56261
  let operatorThread;
55991
56262
  let pageOwner;
55992
56263
  if (operatorMode !== "off") {
@@ -56204,7 +56475,19 @@ async function dispatch(deps, msg, io, opts = {}) {
56204
56475
  await recordPendingOperator();
56205
56476
  return ended;
56206
56477
  }
56207
- const expectedHead = owner.unit.lastPush;
56478
+ let expectedHead = owner.unit.lastPush;
56479
+ const recoverLegacyBinding = owner.unit.ending?.kind === "merge_ready" && (expectedHead === void 0 || owner.unit.publication === void 0);
56480
+ if (recoverLegacyBinding) {
56481
+ const recovered = await legacyReviewedHeadOf(runsService, instance, owner.unit, recordedPr.number);
56482
+ if (recovered === void 0 || expectedHead !== void 0 && expectedHead.toLowerCase() !== recovered) {
56483
+ await io.reply(
56484
+ `Unit ${owner.unit.unit} has no single exact reviewed head in its durable child records, so continuation did not start. Nothing else ran.`
56485
+ );
56486
+ await recordPendingOperator();
56487
+ return ended;
56488
+ }
56489
+ expectedHead = recovered;
56490
+ }
56208
56491
  if (expectedHead === void 0) {
56209
56492
  await io.reply(
56210
56493
  `Unit ${owner.unit.unit} does not retain the pull request's durable expected head, so continuation did not start. Nothing else ran.`
@@ -56243,6 +56526,140 @@ async function dispatch(deps, msg, io, opts = {}) {
56243
56526
  await recordPendingOperator();
56244
56527
  return ended;
56245
56528
  }
56529
+ if (recoverLegacyBinding) {
56530
+ const baseRef = instance.base;
56531
+ if (baseRef === void 0 || facts2.baseRef !== baseRef) {
56532
+ await io.reply(
56533
+ `${instance.repo}#${recordedPr.number} no longer has the pipeline's verifiable base ref, so continuation did not start. Nothing else ran.`
56534
+ );
56535
+ await recordPendingOperator();
56536
+ return ended;
56537
+ }
56538
+ const existingBinding = owner.unit.publication;
56539
+ if (existingBinding !== void 0 && (existingBinding.repo.toLowerCase() !== instance.repo.toLowerCase() || existingBinding.pr !== recordedPr.number || existingBinding.headRef !== owner.unit.branch || existingBinding.baseRef !== baseRef || existingBinding.expectedHeadSha.toLowerCase() !== expectedHead || existingBinding.publicationRef !== owner.unit.branch || existingBinding.owner.instanceId !== owner.instanceId || existingBinding.owner.unit !== owner.unit.unit)) {
56540
+ await io.reply(
56541
+ `Unit ${owner.unit.unit}'s durable publication binding no longer matches this pipeline, so continuation did not start. Nothing else ran.`
56542
+ );
56543
+ await recordPendingOperator();
56544
+ return ended;
56545
+ }
56546
+ let currentOwner;
56547
+ const runnerOwnership = deps.runnerOwnership;
56548
+ try {
56549
+ if (runnerOwnership === void 0) throw new Error("runner ownership is unavailable");
56550
+ currentOwner = runnerOwnership.owner(instance.repo, recordedPr.number);
56551
+ } catch {
56552
+ await io.reply(
56553
+ `Runner ownership for ${instance.repo}#${recordedPr.number} could not be verified, so continuation did not start. Nothing else ran.`
56554
+ );
56555
+ await recordPendingOperator();
56556
+ return ended;
56557
+ }
56558
+ if (currentOwner !== void 0 && (currentOwner.instanceId !== owner.instanceId || currentOwner.unit !== owner.unit.unit)) {
56559
+ await io.reply(
56560
+ `${instance.repo}#${recordedPr.number} is owned by another runner, so continuation of ${owner.instanceId}:${owner.unit.unit} did not start. Nothing else ran.`
56561
+ );
56562
+ await recordPendingOperator();
56563
+ return ended;
56564
+ }
56565
+ const legacyUnit = owner.unit;
56566
+ const coordinatorInstances = deps.coordinatorInstances;
56567
+ const reservationOwner = { instanceId: owner.instanceId, unit: owner.unit.unit };
56568
+ const recoveredUnit = {
56569
+ ...legacyUnit,
56570
+ lastPush: expectedHead,
56571
+ publication: {
56572
+ repo: instance.repo,
56573
+ pr: recordedPr.number,
56574
+ headRef: legacyUnit.branch,
56575
+ baseRef,
56576
+ expectedHeadSha: expectedHead,
56577
+ publicationRef: legacyUnit.branch,
56578
+ owner: reservationOwner
56579
+ }
56580
+ };
56581
+ let token2;
56582
+ let transferredOwner;
56583
+ releaseLegacyOwnership = () => {
56584
+ if (token2 !== void 0) {
56585
+ const released = runnerOwnership.releaseReservation(instance.repo, recordedPr.number, token2);
56586
+ if (released) token2 = void 0;
56587
+ return released;
56588
+ }
56589
+ if (transferredOwner !== void 0) {
56590
+ const released = runnerOwnership.release(instance.repo, recordedPr.number, transferredOwner);
56591
+ if (released) transferredOwner = void 0;
56592
+ return released;
56593
+ }
56594
+ return true;
56595
+ };
56596
+ reserveLegacyOwnership = () => {
56597
+ token2 = runnerOwnership.reserve(instance.repo, recordedPr.number, reservationOwner);
56598
+ if (token2 !== void 0) return void 0;
56599
+ const competing = runnerOwnership.owner(instance.repo, recordedPr.number);
56600
+ const sameContinuation = competing?.instanceId === reservationOwner.instanceId && competing.unit === reservationOwner.unit;
56601
+ return refusalOf(
56602
+ "setup_failed",
56603
+ sameContinuation ? `Unit ${legacyUnit.unit} changed while its legacy continuation was being reserved, so continuation did not start. Nothing else ran.` : `${instance.repo}#${recordedPr.number} is owned by another runner, so continuation of ${owner.instanceId}:${legacyUnit.unit} did not start. Nothing else ran.`
56604
+ );
56605
+ };
56606
+ beforeCoordinatorStart = async () => {
56607
+ const activeToken = token2;
56608
+ if (activeToken === void 0)
56609
+ return {
56610
+ ok: false,
56611
+ refusal: refusalOf(
56612
+ "setup_failed",
56613
+ `Unit ${legacyUnit.unit}'s ownership reservation was lost, so continuation did not start. Nothing else ran.`
56614
+ )
56615
+ };
56616
+ const persisted = await coordinatorInstances.claimLegacyContinuation(legacyUnit, recoveredUnit).catch(() => void 0);
56617
+ if (persisted?.ok !== true) {
56618
+ const released = releaseLegacyOwnership?.() ?? true;
56619
+ const reason2 = persisted?.reason === "stale" ? `Unit ${legacyUnit.unit} changed while its legacy continuation was being reserved, so continuation did not start.` : `Unit ${legacyUnit.unit}'s recovered continuation binding could not be stored, so continuation did not start.`;
56620
+ const text = released ? `${reason2} Nothing else ran.` : `${reason2} Its ownership reservation could not be released. Nothing else ran.`;
56621
+ return { ok: false, refusal: refusalOf("setup_failed", text) };
56622
+ }
56623
+ const reservedOwner = runnerOwnership.owner(instance.repo, recordedPr.number);
56624
+ if (reservedOwner?.instanceId !== reservationOwner.instanceId || reservedOwner.unit !== reservationOwner.unit) {
56625
+ const restored = await coordinatorInstances.claimLegacyContinuation(recoveredUnit, legacyUnit).catch(() => void 0);
56626
+ const released = releaseLegacyOwnership?.() ?? true;
56627
+ const rollbackFailure = restored?.ok === true ? void 0 : restored?.reason ?? "unavailable";
56628
+ const cleanupFailure = released ? "" : " and its ownership reservation could not be released";
56629
+ const text = rollbackFailure === void 0 ? `${instance.repo}#${recordedPr.number} changed runner ownership while its legacy continuation was being reserved${cleanupFailure}, so continuation did not start. Nothing else ran.` : `${instance.repo}#${recordedPr.number} changed runner ownership while its legacy continuation was being reserved, and the exact legacy row could not be restored (${rollbackFailure})${cleanupFailure}; continuation did not start. Nothing else ran.`;
56630
+ return { ok: false, refusal: refusalOf("setup_failed", text) };
56631
+ }
56632
+ owner.unit = recoveredUnit;
56633
+ return {
56634
+ ok: true,
56635
+ commit: async (nextOwner) => {
56636
+ if (!runnerOwnership.transferReservation(
56637
+ instance.repo,
56638
+ recordedPr.number,
56639
+ activeToken,
56640
+ reservationOwner,
56641
+ nextOwner
56642
+ ))
56643
+ throw new Error("legacy continuation ownership reservation or current owner changed");
56644
+ token2 = void 0;
56645
+ transferredOwner = nextOwner;
56646
+ },
56647
+ complete: () => {
56648
+ transferredOwner = void 0;
56649
+ },
56650
+ abort: async () => {
56651
+ const restored = await coordinatorInstances.claimLegacyContinuation(recoveredUnit, legacyUnit).catch(() => void 0);
56652
+ const released = releaseLegacyOwnership?.() ?? true;
56653
+ owner.unit = legacyUnit;
56654
+ const failures = [
56655
+ ...restored?.ok === true ? [] : [`legacy continuation rollback failed (${restored?.reason ?? "unavailable"})`],
56656
+ ...released ? [] : ["legacy continuation ownership cleanup failed"]
56657
+ ];
56658
+ if (failures.length > 0) throw new Error(failures.join("; "));
56659
+ }
56660
+ };
56661
+ };
56662
+ }
56246
56663
  reissuePlanId = instance.plan.id;
56247
56664
  reissueCaps = continuationBudget.caps;
56248
56665
  const durableRequest = parseDirectives(original);
@@ -56295,6 +56712,12 @@ async function dispatch(deps, msg, io, opts = {}) {
56295
56712
  });
56296
56713
  if (profileGate.kind === "refused") return ended;
56297
56714
  const { profile } = profileGate;
56715
+ const legacyOwnershipRefusal = reserveLegacyOwnership?.();
56716
+ if (legacyOwnershipRefusal !== void 0) {
56717
+ await io.reply(legacyOwnershipRefusal.text);
56718
+ await recordPendingOperator();
56719
+ return ended;
56720
+ }
56298
56721
  const earlyRepoTarget = agent.name === "review" ? resolveRepoTarget(deps, {
56299
56722
  msg,
56300
56723
  history,
@@ -56473,6 +56896,7 @@ async function dispatch(deps, msg, io, opts = {}) {
56473
56896
  card,
56474
56897
  directives,
56475
56898
  ...reissuePlanId !== void 0 ? { reissuePlanId } : {},
56899
+ ...beforeCoordinatorStart !== void 0 ? { beforeCoordinatorStart } : {},
56476
56900
  ...reissueCaps !== void 0 ? { reissueCaps: { ...reissueCaps, maxMinutes: Math.min(reissueCaps.maxMinutes, profile.minutes) } } : {},
56477
56901
  sticky,
56478
56902
  history,
@@ -57185,6 +57609,7 @@ async function dispatch(deps, msg, io, opts = {}) {
57185
57609
  });
57186
57610
  } finally {
57187
57611
  clearInterval(setupHeartbeat);
57612
+ releaseLegacyOwnership?.();
57188
57613
  ending.drain(void 0);
57189
57614
  if (registered) deps.runBearers?.revoke(registered.id);
57190
57615
  if (resume && ledgerRun && !runLoopStarted && !resumeRowClosed) {
@@ -57372,7 +57797,7 @@ async function dispatchClick(deps, click) {
57372
57797
  activeRuns--;
57373
57798
  }
57374
57799
  }
57375
- var activeRuns;
57800
+ var activeRuns, FULL_SHA2;
57376
57801
  var init_dispatcher = __esm({
57377
57802
  "../../src/core/dispatcher.ts"() {
57378
57803
  "use strict";
@@ -57438,6 +57863,7 @@ var init_dispatcher = __esm({
57438
57863
  init_types5();
57439
57864
  init_decisionRecordReservation();
57440
57865
  activeRuns = 0;
57866
+ FULL_SHA2 = /^[0-9a-f]{40}$/i;
57441
57867
  }
57442
57868
  });
57443
57869
 
@@ -65712,7 +66138,8 @@ async function handle(deps, { client, statusClient }, ev, intake) {
65712
66138
  span,
65713
66139
  {
65714
66140
  staging: deps.artifacts !== void 0,
65715
- maxBytesPerMessage: deps.config.config.artifacts?.inbound?.maxBytesPerMessage ?? ARTIFACT_DEFAULTS.maxBytesPerMessage
66141
+ maxBytesPerMessage: deps.config.config.artifacts?.inbound?.maxBytesPerMessage ?? ARTIFACT_DEFAULTS.maxBytesPerMessage,
66142
+ verbosityFor: (channelId, userId, request2) => deps.config.verbosityFor(channelId, userId, request2)
65716
66143
  },
65717
66144
  relayAppsOf(deps),
65718
66145
  intake
@@ -65794,10 +66221,25 @@ async function receiveSlackMessage(client, ev, span, policy, relayApps, intake)
65794
66221
  threadRuns = gated.thread;
65795
66222
  }
65796
66223
  }
65797
- client.reactions.add({ channel: ev.channel, timestamp: ev.ts, name: ACK_EMOJI }).catch((err2) => {
65798
- if (!err2.message.includes("already_reacted")) console.error(`[ack] ${err2.message}`);
65799
- });
65800
- const delayNote = ev.caughtUp ? new SlackIO(client, ev).reply(catchUpDelayNote(ev.ts, systemClock())).catch((err2) => {
66224
+ const requester = await resolveSlackRequester(
66225
+ client,
66226
+ {
66227
+ channel: ev.channel,
66228
+ ts: ev.ts,
66229
+ threadTs: ev.threadTs,
66230
+ ...ev.user !== void 0 ? { user: ev.user } : {},
66231
+ text: ev.rawText ?? ev.text,
66232
+ ...ev.poster !== void 0 ? { poster: ev.poster } : {}
66233
+ },
66234
+ relayApps
66235
+ );
66236
+ span.setAttrs({ requester: requester.resolvedBy });
66237
+ const verbosity2 = policy.verbosityFor?.(`${PLATFORM3}:${ev.channel}`, requester.userId, parseDirectives(ev.text).verbosity) ?? policy.verbosity ?? DEFAULT_VERBOSITY;
66238
+ if (shows(verbosity2, "verbose"))
66239
+ client.reactions.add({ channel: ev.channel, timestamp: ev.ts, name: ACK_EMOJI }).catch((err2) => {
66240
+ if (!err2.message.includes("already_reacted")) console.error(`[ack] ${err2.message}`);
66241
+ });
66242
+ const delayNote = ev.caughtUp && shows(verbosity2, "verbose") ? new SlackIO(client, ev).reply(catchUpDelayNote(ev.ts, systemClock())).catch((err2) => {
65801
66243
  console.error(`[catch-up] ${ev.channel}:${ev.ts} delay note failed: ${err2.message}`);
65802
66244
  }) : Promise.resolve();
65803
66245
  const [imagePass, documentPass] = await Promise.all([
@@ -65820,19 +66262,6 @@ async function receiveSlackMessage(client, ev, span, policy, relayApps, intake)
65820
66262
  });
65821
66263
  const unsupported = imagePass.skippedFiles.filter((f) => documentPass.skippedFiles.includes(f));
65822
66264
  const { staged, skipped } = stagedFiles(unsupported, { ...policy, messageId: ev.ts });
65823
- const requester = await resolveSlackRequester(
65824
- client,
65825
- {
65826
- channel: ev.channel,
65827
- ts: ev.ts,
65828
- threadTs: ev.threadTs,
65829
- ...ev.user !== void 0 ? { user: ev.user } : {},
65830
- text: ev.rawText ?? ev.text,
65831
- ...ev.poster !== void 0 ? { poster: ev.poster } : {}
65832
- },
65833
- relayApps
65834
- );
65835
- span.setAttrs({ requester: requester.resolvedBy });
65836
66265
  const [channelName, userName, team] = await Promise.all([
65837
66266
  resolveChannelName(client, ev.channel),
65838
66267
  requester.slackUserId !== void 0 ? resolveUserName(client, requester.slackUserId) : Promise.resolve(requester.userName),
@@ -66152,6 +66581,8 @@ var init_slack = __esm({
66152
66581
  init_intake();
66153
66582
  init_thread();
66154
66583
  init_operator();
66584
+ init_directives();
66585
+ init_verbosity();
66155
66586
  init_confirmations();
66156
66587
  init_threadTurns();
66157
66588
  init_statusBudget();
@@ -66337,14 +66768,15 @@ ${file.text}`));
66337
66768
  io
66338
66769
  };
66339
66770
  }
66340
- async status(initial) {
66771
+ async status(initial, display = {}) {
66341
66772
  const statusClient = this.opts.statusClient ?? this.client;
66342
66773
  const budget = this.opts.statusBudget ?? processStatusBudget;
66343
- const setShimmer = () => statusClient.assistant.threads.setStatus({
66774
+ const shimmerVisible = shows(display.verbosity ?? DEFAULT_VERBOSITY, "verbose");
66775
+ const setShimmer = (status3 = LOADING_PHRASES[0]) => statusClient.assistant.threads.setStatus({
66344
66776
  channel_id: this.ev.channel,
66345
66777
  thread_ts: this.ev.threadTs,
66346
- status: LOADING_PHRASES[0],
66347
- loading_messages: LOADING_PHRASES
66778
+ status: status3,
66779
+ ...status3 === "" ? {} : { loading_messages: LOADING_PHRASES }
66348
66780
  }).catch((err2) => console.error(`[shimmer] ${err2.message}`));
66349
66781
  let ts;
66350
66782
  if (this.opts.existingCard) {
@@ -66369,11 +66801,17 @@ ${file.text}`));
66369
66801
  liveCards.add(liveCardKey(this.ev.channel, ts));
66370
66802
  const card = `${this.ev.channel}:${ts}`;
66371
66803
  const shimmerKey = `${this.ev.channel}:${this.ev.threadTs}`;
66372
- shimmerOwners.set(shimmerKey, card);
66373
- await setShimmer();
66374
- const shimmerTimer = setInterval(() => {
66375
- if (shimmerOwners.get(shimmerKey) === card) void setShimmer();
66376
- }, 75e3);
66804
+ let shimmerTimer;
66805
+ if (shimmerVisible) {
66806
+ shimmerOwners.set(shimmerKey, card);
66807
+ await setShimmer();
66808
+ shimmerTimer = setInterval(() => {
66809
+ if (shimmerOwners.get(shimmerKey) === card) void setShimmer();
66810
+ }, 75e3);
66811
+ } else {
66812
+ shimmerOwners.delete(shimmerKey);
66813
+ await setShimmer("");
66814
+ }
66377
66815
  budget.open(card);
66378
66816
  const edit = (frame2) => statusClient.chat.update({ channel: this.ev.channel, ts, ...render2(frame2) });
66379
66817
  let dropped = 0;
@@ -66402,7 +66840,7 @@ ${file.text}`));
66402
66840
  });
66403
66841
  },
66404
66842
  done: async (frame2) => {
66405
- clearInterval(shimmerTimer);
66843
+ if (shimmerTimer !== void 0) clearInterval(shimmerTimer);
66406
66844
  liveCards.delete(liveCardKey(this.ev.channel, ts));
66407
66845
  budget.close(card);
66408
66846
  const waitMs = budget.takeTerminal(this.ev.channel);
@@ -66414,7 +66852,7 @@ ${file.text}`));
66414
66852
  if (dropped > 0) console.log(`[slack] card ${card}: ${dropped} progress frames dropped by the status budget`);
66415
66853
  if (shimmerOwners.get(shimmerKey) !== card) return;
66416
66854
  shimmerOwners.delete(shimmerKey);
66417
- await statusClient.assistant.threads.setStatus({ channel_id: this.ev.channel, thread_ts: this.ev.threadTs, status: "" }).catch(() => {
66855
+ await setShimmer("").catch(() => {
66418
66856
  });
66419
66857
  }
66420
66858
  };
@@ -71187,7 +71625,12 @@ async function launchResumes(deps, resumable, opts) {
71187
71625
  startedAt: row2.startedAt,
71188
71626
  now: deps.clock ?? Date.now
71189
71627
  });
71190
- await io2.status(shell.live({ detail: [rehostCardDetail(run2)] }));
71628
+ const verbosity2 = row2.meta.verbosity ?? deps.config.verbosityFor(
71629
+ row2.meta.channelId,
71630
+ row2.meta.userId,
71631
+ parseDirectives(inputTextOf(run2.events)).verbosity
71632
+ );
71633
+ await io2.status(shell.live({ detail: [rehostCardDetail(run2)] }), { verbosity: verbosity2 });
71191
71634
  } catch (err2) {
71192
71635
  warn(
71193
71636
  `[resume] ${row2.runId} ${row2.threadKey}: restart card could not be written: ${err2 instanceof Error ? err2.message : String(err2)}`
@@ -71266,6 +71709,7 @@ var PI_READ_ONLY_TOOLS;
71266
71709
  var init_resumeLaunch = __esm({
71267
71710
  "../../src/core/resumeLaunch.ts"() {
71268
71711
  "use strict";
71712
+ init_directives();
71269
71713
  init_inboxMessage();
71270
71714
  init_resume();
71271
71715
  init_runRegistry();
@@ -71300,22 +71744,73 @@ var init_runnerOwnership = __esm({
71300
71744
  recovered = /* @__PURE__ */ new Set();
71301
71745
  claimedOwners = /* @__PURE__ */ new Map();
71302
71746
  recoveredOwners = /* @__PURE__ */ new Map();
71747
+ reservations = /* @__PURE__ */ new Map();
71303
71748
  localRecoveredInstances = /* @__PURE__ */ new Set();
71304
71749
  recoveryComplete;
71305
71750
  constructor(requiresRecovery) {
71306
71751
  this.recoveryComplete = !requiresRecovery;
71307
71752
  }
71753
+ /** Claim without displacing a different live runner. The read and write are
71754
+ * synchronous, so a caller owns the process-local fence before its next
71755
+ * awaited durable transition. Repeating the same owner's claim is idempotent. */
71308
71756
  claim(repo, prNumber, owner) {
71309
71757
  const key = runnerOwnedPullKey(repo, prNumber);
71758
+ const current = this.claimedOwners.get(key) ?? this.recoveredOwners.get(key);
71759
+ const alreadyOwned = this.claimed.has(key) || this.recovered.has(key);
71760
+ if (this.reservations.has(key)) return false;
71761
+ if (alreadyOwned && (owner === void 0 || current === void 0 || current.instanceId !== owner.instanceId || current.unit !== owner.unit))
71762
+ return false;
71310
71763
  this.claimed.add(key);
71311
71764
  if (owner !== void 0) this.claimedOwners.set(key, owner);
71765
+ return true;
71312
71766
  }
71313
- release(repo, prNumber) {
71767
+ /** Exclusively reserve an unowned pull request across an awaited durable
71768
+ * transition. Even the same runner identity cannot reserve it twice: the
71769
+ * returned token, not the owner fields, identifies the one caller. */
71770
+ reserve(repo, prNumber, owner) {
71771
+ const key = runnerOwnedPullKey(repo, prNumber);
71772
+ if (this.claimed.has(key) || this.recovered.has(key) || this.reservations.has(key)) return void 0;
71773
+ const token2 = Symbol(key);
71774
+ this.reservations.set(key, token2);
71775
+ this.claimed.add(key);
71776
+ this.claimedOwners.set(key, owner);
71777
+ return token2;
71778
+ }
71779
+ /** Move an exclusive recovery reservation to the durable attempt that is
71780
+ * about to start. Both the unforgeable token and the currently recorded
71781
+ * owner must still match; a stale caller can neither displace nor adopt a
71782
+ * successor. The transfer consumes the reservation but retains ownership. */
71783
+ transferReservation(repo, prNumber, token2, currentOwner, nextOwner) {
71314
71784
  const key = runnerOwnedPullKey(repo, prNumber);
71785
+ const current = this.claimedOwners.get(key);
71786
+ if (this.reservations.get(key) !== token2 || !this.claimed.has(key) || current?.instanceId !== currentOwner.instanceId || current.unit !== currentOwner.unit)
71787
+ return false;
71788
+ this.reservations.delete(key);
71789
+ this.claimedOwners.set(key, nextOwner);
71790
+ return true;
71791
+ }
71792
+ releaseReservation(repo, prNumber, token2) {
71793
+ const key = runnerOwnedPullKey(repo, prNumber);
71794
+ if (this.reservations.get(key) !== token2) return false;
71795
+ this.reservations.delete(key);
71796
+ this.claimed.delete(key);
71797
+ this.claimedOwners.delete(key);
71798
+ return true;
71799
+ }
71800
+ /** Release unconditionally for the runner's ordinary end, or conditionally
71801
+ * for a provisional claim whose cleanup must not erase a successor. */
71802
+ release(repo, prNumber, owner) {
71803
+ const key = runnerOwnedPullKey(repo, prNumber);
71804
+ if (owner !== void 0) {
71805
+ const current = this.claimedOwners.get(key) ?? this.recoveredOwners.get(key);
71806
+ if (current?.instanceId !== owner.instanceId || current.unit !== owner.unit) return false;
71807
+ }
71808
+ this.reservations.delete(key);
71315
71809
  this.claimed.delete(key);
71316
71810
  this.recovered.delete(key);
71317
71811
  this.claimedOwners.delete(key);
71318
71812
  this.recoveredOwners.delete(key);
71813
+ return true;
71319
71814
  }
71320
71815
  owns(repo, prNumber) {
71321
71816
  if (!this.recoveryComplete) throw new Error("runner ownership recovery is still in progress");
@@ -72242,7 +72737,7 @@ function watched(io, on) {
72242
72737
  on.replied(text);
72243
72738
  await io.reply(text);
72244
72739
  },
72245
- status: (initial) => io.status(initial),
72740
+ status: (initial, display) => io.status(initial, display),
72246
72741
  history: () => io.history(),
72247
72742
  runStarted: (started) => {
72248
72743
  io.runStarted?.(started);
@@ -72316,6 +72811,33 @@ The thread since the last step:
72316
72811
  ${foldThreadEvents(folded)}` };
72317
72812
  }
72318
72813
  const carried = foldThreadAttachments(folded);
72814
+ const existingPrCodingTarget = req.preset === "coding" && row2 !== void 0 && (row2.publication !== void 0 || row2.pr !== void 0 || row2.resume !== void 0 || parsePlanBranch(row2.branch) === void 0);
72815
+ if (existingPrCodingTarget) {
72816
+ if (row2.publication === void 0)
72817
+ return json3(409, {
72818
+ ok: false,
72819
+ error: "publication_binding_missing",
72820
+ message: "the existing pull request has no durable publication binding",
72821
+ at
72822
+ });
72823
+ try {
72824
+ const owner = deps.runnerOwnership?.owner(instance.repo, row2.publication.pr);
72825
+ if (owner === void 0 || owner.instanceId !== row2.publication.owner.instanceId || owner.unit !== row2.publication.owner.unit)
72826
+ return json3(409, {
72827
+ ok: false,
72828
+ error: "publication_ownership_changed",
72829
+ message: "the durable existing-PR publication owner is no longer the runner's sole owner",
72830
+ at
72831
+ });
72832
+ } catch (err2) {
72833
+ return json3(409, {
72834
+ ok: false,
72835
+ error: "publication_ownership_unknown",
72836
+ message: describe12(err2),
72837
+ at
72838
+ });
72839
+ }
72840
+ }
72319
72841
  const msg = {
72320
72842
  channelId: instance.channelId,
72321
72843
  userId: instance.userId,
@@ -72357,10 +72879,16 @@ ${foldThreadEvents(folded)}` };
72357
72879
  lastReply = text;
72358
72880
  }
72359
72881
  });
72882
+ const roundExpectedHead = req.brief?.kind === "findings" || req.brief?.kind === "rebase" ? req.brief.headSha : req.brief?.kind === "contract" ? req.brief.continue?.from : void 0;
72883
+ const publication = req.preset === "coding" && row2?.publication !== void 0 ? {
72884
+ ...row2.publication,
72885
+ ...roundExpectedHead !== void 0 ? { expectedHeadSha: roundExpectedHead } : {}
72886
+ } : void 0;
72360
72887
  const tag = {
72361
72888
  parentInstanceId: instance.id,
72362
72889
  idempotencyKey: key,
72363
- ...instance.base !== void 0 ? { base: instance.base } : {}
72890
+ ...instance.base !== void 0 ? { base: instance.base } : {},
72891
+ ...publication !== void 0 ? { publication } : {}
72364
72892
  };
72365
72893
  let dispatching;
72366
72894
  try {
@@ -72851,9 +73379,19 @@ async function prCheck(body, deps) {
72851
73379
  const unit = await unitRowOf(deps, instance, body.unit);
72852
73380
  if (!unit.ok) return unit.response;
72853
73381
  const branch2 = unit.row?.branch ?? instance.branch;
72854
- const remember = async (pr) => {
72855
- if (unit.row && (unit.row.pr?.number !== pr.number || unit.row.pr.url !== pr.url))
72856
- await deps.instances.putUnits([{ ...unit.row, pr }]);
73382
+ const remember = async (pr, facts2) => {
73383
+ if (!unit.row) return;
73384
+ const publication = unit.row.publication ?? (facts2?.state === "open" && facts2.sameRepoHead === true && facts2.headBranchExists === true && typeof facts2.headRef === "string" && typeof facts2.baseRef === "string" && typeof facts2.headSha === "string" && /^[0-9a-f]{40}$/i.test(facts2.headSha) ? {
73385
+ repo: instance.repo,
73386
+ pr: pr.number,
73387
+ headRef: facts2.headRef,
73388
+ baseRef: facts2.baseRef,
73389
+ expectedHeadSha: facts2.headSha,
73390
+ publicationRef: facts2.headRef,
73391
+ owner: { instanceId: instance.id, unit: unit.row.unit }
73392
+ } : void 0);
73393
+ if (unit.row.pr?.number !== pr.number || unit.row.pr.url !== pr.url || unit.row.publication === void 0 && publication !== void 0)
73394
+ await deps.instances.putUnits([{ ...unit.row, pr, ...publication !== void 0 ? { publication } : {} }]);
72857
73395
  };
72858
73396
  try {
72859
73397
  if (follow !== void 0) {
@@ -72862,8 +73400,12 @@ async function prCheck(body, deps) {
72862
73400
  const fallbackUrl = unit.row?.pr?.url ?? `https://github.com/${instance.repo}/pull/${follow}`;
72863
73401
  const state2 = pullRequestState(follow, fallbackUrl, facts2);
72864
73402
  if (state2 === void 0) throw new Error(headBranchStateError(instance.repo, follow));
72865
- await remember({ number: follow, url: String(state2.url) });
73403
+ await remember({ number: follow, url: String(state2.url) }, facts2);
72866
73404
  if (state2.state !== "open") return json3(200, { ok: true, ...state2, at });
73405
+ deps.runnerOwnership?.claim(instance.repo, follow, {
73406
+ instanceId: instance.id,
73407
+ unit: unit.row?.unit ?? body.unit
73408
+ });
72867
73409
  const prRef = { repo: instance.repo, number: follow };
72868
73410
  const headSha = facts2.headSha;
72869
73411
  const approved = entry && headSha !== void 0 ? await reviewPostedAt(deps, prRef, "approve", headSha) : void 0;
@@ -72894,13 +73436,13 @@ async function prCheck(body, deps) {
72894
73436
  }
72895
73437
  const open2 = await deps.findOpenPrByHead(instance.repo, branch2);
72896
73438
  if (open2) {
72897
- await remember({ number: open2.number, url: open2.htmlUrl });
72898
73439
  deps.runnerOwnership?.claim(instance.repo, open2.number, {
72899
73440
  instanceId: instance.id,
72900
73441
  unit: unit.row?.unit ?? body.unit
72901
73442
  });
72902
73443
  const liveFacts = await deps.fetchPrFacts({ repo: instance.repo, number: open2.number });
72903
73444
  if (liveFacts === void 0) throw new Error(`could not read ${instance.repo}#${open2.number}`);
73445
+ await remember({ number: open2.number, url: open2.htmlUrl }, liveFacts);
72904
73446
  const current = pullRequestState(open2.number, open2.htmlUrl, liveFacts);
72905
73447
  if (current === void 0) throw new Error(headBranchStateError(instance.repo, open2.number));
72906
73448
  if (current.state !== "open") return json3(200, { ok: true, ...current, at });
@@ -73209,12 +73751,13 @@ async function drawCard(deps, instance, units, close) {
73209
73751
  source: instance.addressSeveritySource ?? "org"
73210
73752
  } : void 0
73211
73753
  );
73754
+ const display = { verbosity: instance.verbosity ?? DEFAULT_VERBOSITY };
73212
73755
  if (!close) {
73213
- await io.status(shell.live({ detail }));
73756
+ await io.status(shell.live({ detail }), display);
73214
73757
  return;
73215
73758
  }
73216
73759
  shell.freeze(clock4());
73217
- const handle2 = await io.status(shell.live({ detail }));
73760
+ const handle2 = await io.status(shell.live({ detail }), display);
73218
73761
  await handle2.done(shell.close({ kind: "done", icon: close.icon, detail: detail.join("\n") }));
73219
73762
  }
73220
73763
  function parseGate(raw) {
@@ -75274,6 +75817,7 @@ async function runBot() {
75274
75817
  ) : new NullMetricsService();
75275
75818
  const sweepState = { roundSpent: /* @__PURE__ */ new Set() };
75276
75819
  const runnerOwnership = new RunnerOwnershipFence(capabilities.runLedger);
75820
+ deps.runnerOwnership = runnerOwnership;
75277
75821
  const pullsWiring = /* @__PURE__ */ (() => {
75278
75822
  const state = sweepState;
75279
75823
  const chains = /* @__PURE__ */ new Map();