@coreplane/switchboard 1.260.2 → 1.260.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
@@ -2094,7 +2094,7 @@ function renderFirstInstruction(rebase) {
2094
2094
  const branch2 = rebase.branch ? `\`${rebase.branch}\`` : "the unit's branch";
2095
2095
  const onto = rebase.onto ? `\`${rebase.onto}\`` : "the merged parent";
2096
2096
  const gates = `${FAST_GATES_POINTER} ${GATE_RECEIPTS}`;
2097
- return `Rebase ${branch2} onto ${onto} before any other work \u2014 the parent unit has merged and the base has moved; the only writes are your own on that branch. A conflict ends the unit: report it as the handoff and stop. Push the branch as soon as the change exists and the fast gates pass \u2014 the project's full verification is CI's gate, run there after the push with any fix as a further commit; an unpushed tree does not survive the run's end. ${gates} ${REBASE_POINTER} At the wind-down note, commit and push what compiles, say what does not, then answer. ${TIMEOUT_ON_LONG_COMMANDS}`;
2097
+ return `Rebase ${branch2} onto ${onto} before any other work \u2014 the parent unit has merged and the base has moved; the only writes are your own on that branch. Push the branch as soon as the change exists and the fast gates pass \u2014 the project's full verification is CI's gate, run there after the push with any fix as a further commit; an unpushed tree does not survive the run's end. ${gates} ${REBASE_POINTER} At the wind-down note, commit and push what compiles, say what does not, then answer. ${TIMEOUT_ON_LONG_COMMANDS}`;
2098
2098
  }
2099
2099
  function renderSpecRow(row2, dropText) {
2100
2100
  const head = `#### ${row2.spec} item ${row2.item}`;
@@ -2313,7 +2313,7 @@ EVERY PR includes one that already exists when you push \u2014 opened by a perso
2313
2313
  - **For agents** (\`agentNotes\`, optional, \u22642000, collapsed): what a reviewing agent needs that a person does not \u2014 the rebase you did, generated files to skip, the command that reproduces the bug.`;
2314
2314
  NEVER_MERGE = `NEVER merge a pull request and NEVER approve one \u2014 no merge or approve command, no merge/approve API call, no pushing to the default branch. Your deliverable is the pushed branch plus the submitted description; Switchboard's own GitHub writes are the PR open/edit, never an approval or a merge \u2014 a human decides what merges.`;
2315
2315
  CONTRACT_HEADINGS_LIST = Object.values(CONTRACT_SECTION_HEADINGS).map((h) => `\`${h}\``).join(", ");
2316
- UNIT_CONTRACT = `UNIT CONTRACT: when your first user turn carries a \`${CONTRACT_HEADING}\` block \u2014 its sub-headings, in this order: ${CONTRACT_HEADINGS_LIST} \u2014 it is the contract for one plan unit, rendered by Switchboard from the plan itself, and it outranks any free-text task beside it. Do its first instruction first: the rebase of the unit's branch onto the merged parent (a conflict ends the unit \u2014 report it and stop; never resolve it by force). Then implement the unit's section as written: every test scenario it lists is added as a test, every spec row it names is updated so its proof binding resolves, the agent rules are followed, and no guard it names is weakened. The review is handed the same block and checks the diff against it: a test scenario the unit listed and the diff did not add is a finding at minor severity \u2014 the same severity as a spec contradiction. Never edit the plan record itself; where the unit is wrong or a criterion could not be proven, say so in the handoff and in your final message.`;
2316
+ UNIT_CONTRACT = `UNIT CONTRACT: when your first user turn carries a \`${CONTRACT_HEADING}\` block \u2014 its sub-headings, in this order: ${CONTRACT_HEADINGS_LIST} \u2014 it is the contract for one plan unit, rendered by Switchboard from the plan itself, and it outranks any free-text task beside it. Do its first instruction first: the rebase of the unit's branch onto the merged parent. Then implement the unit's section as written: every test scenario it lists is added as a test, every spec row it names is updated so its proof binding resolves, the agent rules are followed, and no guard it names is weakened. The review is handed the same block and checks the diff against it: a test scenario the unit listed and the diff did not add is a finding at minor severity \u2014 the same severity as a spec contradiction. Never edit the plan record itself; where the unit is wrong or a criterion could not be proven, say so in the handoff and in your final message.`;
2317
2317
  UNIT_HANDOFF = `UNIT HANDOFF: when your first user turn carries a \`${CONTRACT_HEADING}\` block, call the submit_handoff tool once, after submit_pr_description and before your final message, with the typed handoff \u2014 deviations: where you departed from the unit as written (from, to, why); followUps: what you found and did not do, and where it belongs (what, where); unproven: which of the unit's test scenarios or criteria you could not prove, and why (criterion, why); landed (optional): what of the unit was already on the base when you began, and the pull request or commit that carries it (what, where) \u2014 when the whole unit is already there, push nothing of your own and open no pull request; the handoff's landed rows end the unit done. Switchboard records it on the run and posts it to the unit's board issue, where a person decides each row's disposition; you never edit the plan's ledger yourself. An empty handoff is submitted as three empty lists, never skipped \u2014 a missing handoff reads as an unfinished run, not as nothing to say. Without a \`${CONTRACT_HEADING}\` block, do not call it.`;
2318
2318
  IMAGE_TOOLCHAIN = `Node 24 with npm and pnpm; python3, make and g++ (native modules build); ffmpeg (frames out of a video \u2014 \`ffmpeg -i in.mp4 -vf fps=1 f_%03d.png\` \u2014 and video out of frames or a recording); and a headless Chromium through Playwright \u2014 \`playwright screenshot <url> out.png\`, \`playwright pdf <url> out.pdf\`, or \`require('playwright')\` for a scripted page and \`recordVideo\``;
2319
2319
  SANDBOX_TOOLCHAIN = `The sandbox image carries ${IMAGE_TOOLCHAIN}; and Docker (the engine starts on the first \`docker\` call).`;
@@ -2330,7 +2330,7 @@ Keep notes with the \`notes\` tool: one short document, replaced whole each time
2330
2330
  CHECKS_BY_COST = `CHECKS BY COST \u2014 push before the expensive ones. Every check you might run has a cost class: seconds (a formatter or a linter on the files you touched, one test file, a docs, link or spec check, the typecheck of one package) or minutes (the whole test suite, a build, a dependency install, an end-to-end or full verification). Know a command's class before you run it \u2014 from the project's own scripts and CI configuration, from how long it took last time, or by the class above when you have nothing better. Prove each change with the cheapest check that can prove it, matched to the change's scope and scoped to the changed set \u2014 the tests nearest your change, the touched project's typecheck, the changed files' formatting, never the whole tree: a documentation change gets the documentation checks, one module gets its own tests, a shared type gets the typecheck. Every CI pipeline runs the tests, the types, the formatting and the full verification on your push, so you never run them again: you validate and fix your own change before pushing, at the changed-set scope. Passing the full test suite and the full typecheck is NOT part of your criteria: CI is that gate and the only place they run \u2014 on a shared machine they cost minutes that every other run pays for. As soon as the change exists and those checks pass, commit and push \u2014 the pushed branch is the deliverable, and an unpushed tree does not survive the run's end. Beyond the changed set, use judgement about what this change needs rather than a checklist, fixing forward with further commits and pushes. Never start an operation whose expected duration does not fit the time you have left minus what a commit, a push and the description need: push what there is and say plainly what is unverified instead. At the wind-down note, commit and push what compiles, say what does not, then answer. ${TIMEOUT_ON_LONG_COMMANDS} The description's validation names exactly what ran; what did not run is CI's to gate, and you say so.`;
2331
2331
  TOUCHED_TESTS_COMMAND = "`npx vitest run` on the test files you touched, by name,";
2332
2332
  FAST_GATES_BEFORE_PUSH = `THE FAST GATES, before every push \u2014 each scoped to the changed set, never the whole project: ${TOUCHED_TESTS_COMMAND} once (never \`--changed\`, never a directory: on a moving base that is most of the suite), \`tsc --noEmit -p\` the touched tsconfig under \`NODE_OPTIONS=--max-old-space-size=6144\`, \`npx prettier --check\` on the changed files, \`npm run hygiene:check\` and \`npm run specs:check\` \u2014 then your judgement on what else this change needs, not a longer checklist. The full verification is CI's gate \u2014 \`npm run verify\` runs there on your push, never here: push a head early and let CI judge it, fixing forward with further commits and pushes.`;
2333
- REBASE_BEFORE_PUSH = "REBASE BEFORE EVERY PUSH \u2014 always, not configurable. Immediately before each push: fetch your base branch, rebase your branch onto it, resolve any conflict with the context you already have (the repository's AGENTS.md says how a generated file is regenerated \u2014 regenerate it, never hand-merge it), re-run THE FAST GATES on the rebased tree, and only then push. Every head that reaches review is then current with its base when it lands, and no unit ends merge-ready behind a sibling that merged first.";
2333
+ REBASE_BEFORE_PUSH = "REBASE BEFORE EVERY PUSH \u2014 always, not configurable. Immediately before each push: fetch your base branch, rebase your branch onto it, and resolve any conflict with the context you already have in one bounded model round \u2014 the thread's context and the repository's AGENTS.md (which says how a generated file is regenerated: regenerate it, never hand-merge it). If you cannot resolve it inside that bound, end the round; otherwise re-run THE FAST GATES on the rebased tree, and only then push. Every head that reaches review is then current with its base when it lands, and no unit ends merge-ready behind a sibling that merged first.";
2334
2334
  CODING_SYSTEM = `You are Switchboard's coding agent, operating from a Slack request.
2335
2335
 
2336
2336
  You work inside a dedicated workspace directory with bash, read_file, and write_file tools. ${SANDBOX_TOOLCHAIN}
@@ -3162,6 +3162,9 @@ var init_redact = __esm({
3162
3162
  function severityAtOrAbove(severity, level) {
3163
3163
  return isAddressSeverity(severity) && severityRank(severity) <= severityRank(level);
3164
3164
  }
3165
+ function findingsAtOrAbove(findings, level) {
3166
+ return findings.filter((f) => severityAtOrAbove(f.severity, level));
3167
+ }
3165
3168
  function resolveAddressSeverity(layers) {
3166
3169
  if (layers.run !== void 0) return { level: layers.run, source: "run" };
3167
3170
  if (layers.user !== void 0) return { level: layers.user, source: "user" };
@@ -3274,7 +3277,7 @@ function findingCounts(findings) {
3274
3277
  }
3275
3278
  function whereCell(f, target) {
3276
3279
  const location = f.line !== void 0 ? `${f.file}:${f.line}` : f.file;
3277
- const label = `\`${cell(location)}\``;
3280
+ const label = `\`${escapeMarkdownTableCell(location)}\``;
3278
3281
  if (!target || !isRepoPath(f.file)) return label;
3279
3282
  const path = encodeURI(f.file).replace(/#/g, "%23").replace(/\?/g, "%3F");
3280
3283
  const url = `https://github.com/${target.repo}/blob/${target.head}/${path}${f.line !== void 0 ? `#L${f.line}` : ""}`;
@@ -3290,6 +3293,7 @@ function verdictMarker(verdict, target) {
3290
3293
  id: f.id,
3291
3294
  severity: f.severity,
3292
3295
  file: f.file,
3296
+ title: f.title,
3293
3297
  ...f.line !== void 0 ? { line: f.line } : {},
3294
3298
  ...f.humanGated ? { humanGated: true } : {}
3295
3299
  }))
@@ -3311,7 +3315,9 @@ function buildReviewPostBody(answer, verdict, target) {
3311
3315
  [
3312
3316
  "| Severity | Finding | Where |",
3313
3317
  "| --- | --- | --- |",
3314
- ...findings.map((f) => `| ${f.severity} | **${cell(f.id)}** ${cell(f.title)} | ${whereCell(f, target)} |`)
3318
+ ...findings.map(
3319
+ (f) => `| ${f.severity} | **${escapeMarkdownTableCell(f.id)}** ${escapeMarkdownTableCell(f.title)} | ${whereCell(f, target)} |`
3320
+ )
3315
3321
  ].join("\n")
3316
3322
  );
3317
3323
  }
@@ -3412,7 +3418,7 @@ function redactVerdict(v, redact = redactSecrets) {
3412
3418
  function redactDispositions(dispositions, redact = redactSecrets) {
3413
3419
  return dispositions.map((d) => ({ findingId: d.findingId, disposition: d.disposition, note: redact(d.note) }));
3414
3420
  }
3415
- var FINDING_SEVERITIES, ADDRESS_SEVERITIES, DEFAULT_ADDRESS_SEVERITY, isAddressSeverity, severityRank, LGTM_TOKEN, CHANGES_TOKEN, NO_VERDICT_LINE, cell, isRepoPath, VERDICT_KINDS, DISPOSITION_KINDS, isRecordLike, REVIEW_POST_HEAD;
3421
+ var FINDING_SEVERITIES, ADDRESS_SEVERITIES, DEFAULT_ADDRESS_SEVERITY, isAddressSeverity, severityRank, LGTM_TOKEN, CHANGES_TOKEN, NO_VERDICT_LINE, escapeMarkdownTableCell, unescapeMarkdownTableCell, isRepoPath, VERDICT_KINDS, DISPOSITION_KINDS, isRecordLike, REVIEW_POST_HEAD;
3416
3422
  var init_reviewVerdict = __esm({
3417
3423
  "../../src/core/reviewVerdict.ts"() {
3418
3424
  "use strict";
@@ -3427,7 +3433,8 @@ var init_reviewVerdict = __esm({
3427
3433
  LGTM_TOKEN = "LGTM:";
3428
3434
  CHANGES_TOKEN = "Changes requested:";
3429
3435
  NO_VERDICT_LINE = "No verdict submitted \u2014 not approving.";
3430
- cell = (text) => oneLine2(text).replace(/\|/g, "\\|");
3436
+ escapeMarkdownTableCell = (text) => oneLine2(text).replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
3437
+ unescapeMarkdownTableCell = (text) => text.replace(/\\([\\|])/g, "$1");
3431
3438
  isRepoPath = (file) => /^[^\s`]+$/.test(file) && !/^[a-z][a-z0-9+.-]*:\/\//i.test(file);
3432
3439
  VERDICT_KINDS = ["approve", "request_changes"];
3433
3440
  DISPOSITION_KINDS = ["fixed", "declined"];
@@ -5299,7 +5306,7 @@ function redactHandoff(h, redact = redactSecrets) {
5299
5306
  function line(s) {
5300
5307
  return s.replace(/\s*\n\s*/g, " ");
5301
5308
  }
5302
- function cell2(s) {
5309
+ function cell(s) {
5303
5310
  return line(s.replace(/\\/g, "\\\\").replace(/\|/g, "\\|"));
5304
5311
  }
5305
5312
  function entryLines(h) {
@@ -5331,7 +5338,7 @@ function prLink(pr) {
5331
5338
  function renderHandoffLedgerRows(h, ctx) {
5332
5339
  const link = prLink(ctx.pr);
5333
5340
  const source2 = `${ctx.unitId} handoff${link ? ` (${link})` : ""}`;
5334
- return entryLines(h).map((e) => `| ${cell2(e.row)} | ${cell2(source2)} | open |`).join("\n");
5341
+ return entryLines(h).map((e) => `| ${cell(e.row)} | ${cell(source2)} | open |`).join("\n");
5335
5342
  }
5336
5343
  function renderHandoffComment(h, ctx) {
5337
5344
  if (isEmptyHandoff(h)) return void 0;
@@ -5514,6 +5521,18 @@ function isCoordinatorUnit(v) {
5514
5521
  if (r.startedAt !== void 0 && !isFinite(r.startedAt)) return false;
5515
5522
  return true;
5516
5523
  }
5524
+ async function sendUnitNudge(workflow, key) {
5525
+ const instance = key.instanceId;
5526
+ if (!workflow) return { kind: "no-binding", instance };
5527
+ const type = unitNudgeEventType(key);
5528
+ try {
5529
+ const handle2 = await workflow.get(instance);
5530
+ await handle2.sendEvent({ type, payload: {} });
5531
+ return { kind: "sent", instance, type };
5532
+ } catch (err2) {
5533
+ return { kind: "failed", instance, type, reason: err2 instanceof Error ? err2.message : String(err2) };
5534
+ }
5535
+ }
5517
5536
  async function sendChecksSettled(workflow, instance, headSha, settledAt) {
5518
5537
  if (!workflow) return { kind: "no-binding", instance };
5519
5538
  const type = checksSettledEventType(headSha);
@@ -5550,12 +5569,13 @@ async function sendChildSignal(workflow, signal2) {
5550
5569
  return { kind: "failed", instance, type, reason: err2 instanceof Error ? err2.message : String(err2) };
5551
5570
  }
5552
5571
  }
5553
- 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, isUnitIdle, isSegment, isUnitWakeAnswer;
5572
+ 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;
5554
5573
  var init_contract2 = __esm({
5555
5574
  "../../src/core/coordinator/contract.ts"() {
5556
5575
  "use strict";
5557
5576
  init_budgets();
5558
5577
  init_verbosity();
5578
+ init_reviewVerdict();
5559
5579
  init_coordinator();
5560
5580
  init_handoff();
5561
5581
  COORDINATOR_IDENTITY = "coordinator";
@@ -5591,13 +5611,14 @@ var init_contract2 = __esm({
5591
5611
  isResume = (v) => isObject(v) && isFinite(v.pr) && (v.headSha === void 0 || isText(v.headSha)) && (v.url === void 0 || isText(v.url, 2048));
5592
5612
  isThread = (v) => isObject(v) && isText(v.threadKey) && isOptionalText(v.sourceUrl);
5593
5613
  isRoundGate = (v) => isObject(v) && isAddressSeverity(v.level) && Array.isArray(v.findings) && v.findings.every((f) => typeof f === "string");
5594
- isUnitIdle = (v) => isObject(v) && isText(v.why, IDLE_WHY_MAX) && isFinite(v.at) && isCount(v.renewalsLeft) && (v.from === void 0 || isText(v.from)) && (v.runId === void 0 || isText(v.runId)) && (v.spendUsd === null || isFinite(v.spendUsd)) && (v.handoff === void 0 || isHandoffShape(v.handoff)) && isCount(v.wakes);
5614
+ 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));
5615
+ isUnitIdle = (v) => isObject(v) && isText(v.why, IDLE_WHY_MAX) && isFinite(v.at) && isCount(v.renewalsLeft) && (v.from === void 0 || isText(v.from)) && (v.runId === void 0 || isText(v.runId)) && (v.spendUsd === null || isFinite(v.spendUsd)) && (v.handoff === void 0 || isHandoffShape(v.handoff)) && (v.humanGate === void 0 || isHumanGatePending(v.humanGate)) && isCount(v.wakes);
5595
5616
  isSegment = (v) => isObject(v) && typeof v.index === "number" && Number.isInteger(v.index) && v.index >= 2 && (v.from === void 0 || isText(v.from)) && (v.runId === void 0 || isText(v.runId)) && typeof v.at === "number";
5596
5617
  isUnitWakeAnswer = (v) => {
5597
5618
  if (!isObject(v)) return false;
5598
5619
  if (v.kind === "answered") return typeof v.reply === "string";
5599
5620
  if (v.kind === "stopped" || v.kind === "expired") return true;
5600
- return v.kind === "segment" && typeof v.index === "number" && Number.isInteger(v.index) && v.index >= 1 && (v.from === void 0 || isText(v.from)) && (v.runId === void 0 || isText(v.runId)) && (v.spendUsd === null || isFinite(v.spendUsd)) && (v.handoff === void 0 || isHandoffShape(v.handoff)) && Array.isArray(v.texts) && v.texts.every((text) => typeof text === "string") && Array.isArray(v.senders) && v.senders.every((sender) => isText(sender)) && (v.leaseMs === void 0 || isFinite(v.leaseMs) && v.leaseMs > 0);
5621
+ return v.kind === "segment" && typeof v.index === "number" && Number.isInteger(v.index) && v.index >= 1 && (v.from === void 0 || isText(v.from)) && (v.runId === void 0 || isText(v.runId)) && (v.spendUsd === null || isFinite(v.spendUsd)) && (v.handoff === void 0 || isHandoffShape(v.handoff)) && Array.isArray(v.texts) && v.texts.every((text) => typeof text === "string") && Array.isArray(v.senders) && v.senders.every((sender) => isText(sender)) && (v.leaseMs === void 0 || isFinite(v.leaseMs) && v.leaseMs > 0) && (v.humanGate === void 0 || isHumanGatePending(v.humanGate));
5601
5622
  };
5602
5623
  }
5603
5624
  });
@@ -17465,20 +17486,83 @@ async function fetchPullRequestTitleBody(pr) {
17465
17486
  if (!data || typeof data.title !== "string") return void 0;
17466
17487
  return { title: data.title, body: typeof data.body === "string" ? data.body : "" };
17467
17488
  }
17468
- async function fetchPullRequestReviews(pr) {
17469
- const token2 = await resolveGithubToken().catch(() => null);
17470
- let res;
17489
+ function nextGithubPage(link) {
17490
+ for (const entry of link?.split(",") ?? []) {
17491
+ const target = /<([^>]+)>/.exec(entry)?.[1];
17492
+ const relations = /(?:^|;)\s*rel="([^"]+)"/.exec(entry)?.[1]?.split(/\s+/) ?? [];
17493
+ if (target !== void 0 && relations.includes("next")) return target;
17494
+ }
17495
+ return void 0;
17496
+ }
17497
+ function isGithubListPage(url, base) {
17498
+ let candidate;
17499
+ let requested;
17471
17500
  try {
17472
- res = await fetch(`https://api.github.com/repos/${pr.repo}/pulls/${pr.number}/reviews?per_page=100`, {
17473
- headers: apiHeaders(token2),
17474
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
17475
- });
17501
+ candidate = new URL(url);
17502
+ requested = new URL(base);
17476
17503
  } catch {
17477
- return void 0;
17504
+ return false;
17478
17505
  }
17479
- if (!res.ok) return void 0;
17480
- const rows = await res.json().catch(() => null);
17481
- if (!Array.isArray(rows)) return void 0;
17506
+ if (candidate.origin !== requested.origin || candidate.username !== "" || candidate.password !== "" || candidate.hash !== "")
17507
+ return false;
17508
+ if (candidate.pathname === requested.pathname) return true;
17509
+ const requestedParts = requested.pathname.split("/").filter(Boolean);
17510
+ const candidateParts = candidate.pathname.split("/").filter(Boolean);
17511
+ const resourcePath = requestedParts.slice(3);
17512
+ return requestedParts.length >= 4 && requestedParts[0] === "repos" && candidateParts.length === resourcePath.length + 2 && candidateParts[0] === "repositories" && /^\d+$/.test(candidateParts[1] ?? "") && candidateParts.slice(2).every((part, index) => part === resourcePath[index]);
17513
+ }
17514
+ async function fetchGithubListPages(base, token2) {
17515
+ const rows = [];
17516
+ const seen = /* @__PURE__ */ new Set();
17517
+ let url = `${base}?per_page=100`;
17518
+ while (url !== void 0) {
17519
+ if (!isGithubListPage(url, base) || seen.has(url)) return void 0;
17520
+ seen.add(url);
17521
+ let res;
17522
+ try {
17523
+ res = await fetch(url, {
17524
+ headers: apiHeaders(token2),
17525
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
17526
+ });
17527
+ } catch {
17528
+ return void 0;
17529
+ }
17530
+ if (!res.ok) return void 0;
17531
+ const page2 = await res.json().catch(() => null);
17532
+ if (!Array.isArray(page2)) return void 0;
17533
+ rows.push(...page2);
17534
+ url = nextGithubPage(res.headers.get("link"));
17535
+ }
17536
+ return rows;
17537
+ }
17538
+ async function fetchPullRequestComments(pr) {
17539
+ const token2 = await resolveGithubToken().catch(() => null);
17540
+ const base = `https://api.github.com/repos/${pr.repo}/issues/${pr.number}/comments`;
17541
+ const rows = await fetchGithubListPages(base, token2);
17542
+ if (rows === void 0) return void 0;
17543
+ return rows.flatMap((raw) => {
17544
+ const row2 = raw;
17545
+ if (typeof row2.id !== "number" || typeof row2.user?.login !== "string" || typeof row2.user.type !== "string" || typeof row2.created_at !== "string" || typeof row2.body !== "string")
17546
+ return [];
17547
+ return [
17548
+ {
17549
+ id: row2.id,
17550
+ author: {
17551
+ login: row2.user.login,
17552
+ ...typeof row2.user.id === "number" ? { id: row2.user.id } : {},
17553
+ type: row2.user.type
17554
+ },
17555
+ createdAt: row2.created_at,
17556
+ body: row2.body
17557
+ }
17558
+ ];
17559
+ });
17560
+ }
17561
+ async function fetchPullRequestReviews(pr) {
17562
+ const token2 = await resolveGithubToken().catch(() => null);
17563
+ const base = `https://api.github.com/repos/${pr.repo}/pulls/${pr.number}/reviews`;
17564
+ const rows = await fetchGithubListPages(base, token2);
17565
+ if (rows === void 0) return void 0;
17482
17566
  return rows.flatMap((r) => {
17483
17567
  const row2 = r;
17484
17568
  if (typeof row2.state !== "string") return [];
@@ -17492,6 +17576,7 @@ async function fetchPullRequestReviews(pr) {
17492
17576
  } : {},
17493
17577
  state: row2.state,
17494
17578
  ...typeof row2.commit_id === "string" ? { commitId: row2.commit_id } : {},
17579
+ ...typeof row2.submitted_at === "string" ? { submittedAt: row2.submitted_at } : {},
17495
17580
  body: typeof row2.body === "string" ? row2.body : ""
17496
17581
  }
17497
17582
  ];
@@ -19957,11 +20042,11 @@ function resolveRange(daysParam, now = new Date(systemClock())) {
19957
20042
  function buildBillerTieOuts(billers, invoices, cells, range) {
19958
20043
  return billers.map(({ name, invoice }) => {
19959
20044
  const attributed2 = /* @__PURE__ */ new Map();
19960
- for (const cell4 of cells) {
19961
- if (cell4.day < range.from || cell4.day > range.to) continue;
19962
- for (const [ref, m] of Object.entries(cell4.usage.byModel)) {
20045
+ for (const cell3 of cells) {
20046
+ if (cell3.day < range.from || cell3.day > range.to) continue;
20047
+ for (const [ref, m] of Object.entries(cell3.usage.byModel)) {
19963
20048
  if (billerOfRef(ref) !== name) continue;
19964
- const day2 = attributed2.get(cell4.day) ?? { usd: 0, fee: 0, hasFee: false, unpricedTokens: 0 };
20049
+ const day2 = attributed2.get(cell3.day) ?? { usd: 0, fee: 0, hasFee: false, unpricedTokens: 0 };
19965
20050
  if (typeof m.usd === "number") {
19966
20051
  day2.usd += m.usd;
19967
20052
  if (m.feeUsd !== void 0) {
@@ -19971,7 +20056,7 @@ function buildBillerTieOuts(billers, invoices, cells, range) {
19971
20056
  } else {
19972
20057
  day2.unpricedTokens += m.inputTokens + m.outputTokens + m.cacheReadTokens + m.cacheWriteTokens;
19973
20058
  }
19974
- attributed2.set(cell4.day, day2);
20059
+ attributed2.set(cell3.day, day2);
19975
20060
  }
19976
20061
  }
19977
20062
  const invoiced = /* @__PURE__ */ new Map();
@@ -35281,7 +35366,7 @@ var init_reply = __esm({
35281
35366
  repo_unverified: (p) => p.via === "github" ? `\u26A0\uFE0F I couldn't verify \`${p.slug}\` against GitHub \u2014 it didn't answer \u2014 so I did not start ${aRun(p.agent)} rather than guess which repository you meant. Try again in a minute.` : `\u26A0\uFE0F I couldn't verify that \`${p.slug}\` is an onboarded repo \u2014 the resident registry didn't answer \u2014 so I did not start a *${p.agent}* run rather than guess which repo you meant. Try again in a minute, or name the repository by URL (https://github.com/${p.slug}) to run in a cold per-thread sandbox.`,
35282
35367
  repo_not_onboarded: (p) => `\u{1F4E6} \`${p.slug}\` is not onboarded as a resident, so I did not start a *${p.agent}* run for it. ${p.onboardHint} for a warm, deps-ready environment, or name the repository by URL (https://github.com/${p.slug}) to run in a cold per-thread sandbox.`,
35283
35368
  repo_access: (p) => `\u{1F6AB} You're not on the allowlist for the \`${p.repo}\` repo environment. Ask ${p.adminsHint} for access.`,
35284
- which_branch: (p) => `\u{1F33F} Which branch of \`${p.repo}\` should this thread work on? No branch is bound yet \u2014 reply naming one (e.g. "on main" or "on branch fix/login") and I'll pick it up from there.`,
35369
+ which_branch: (p) => `\u{1F33F} Which branch of \`${p.repo}\` should this thread work on? No branch is bound yet; a branch token such as \`branch:main\` will bind it.`,
35285
35370
  ship_thread_live: () => "\u{1F6AB} A pipeline is already running in this thread \u2014 one pipeline per thread. Follow the one in flight here, or start this one in a thread of its own.",
35286
35371
  pipeline_thread_owned: (p) => `\u{1F6A6} This thread belongs to the live *${p.agent}* pipeline runner \u2014 nothing runs beside it here. ` + (p.units.length > 0 ? `Reply in the unit's own thread instead: ${p.units.map((u) => `${u.unit}${u.threadKey !== void 0 ? ` (\`${u.threadKey}\`)` : ""}`).join(", ")}.` : "Reply in the unit's own thread instead."),
35287
35372
  ship_budget: (p) => `\u{1F6AB} Ship cannot start under a ${p.maxMinutes}-minute budget: the loop it allows (${p.maxRounds} review rounds) needs ${p.need} minutes \u2014 ${p.provision} to provision, the coding child's ${p.coding}, and the reserve for the rounds after it at their floors. Widen the budget or the boundary that clipped it, or run \`agent:coding\` for a single pass without the review loop.`
@@ -35307,6 +35392,10 @@ function slugOf(token2) {
35307
35392
  if (!OWNER_RE.test(owner) || !NAME_RE.test(name) || /^\.+$/.test(name)) return void 0;
35308
35393
  return `${owner}/${name}`.toLowerCase();
35309
35394
  }
35395
+ function headBranchRef(text) {
35396
+ const match = /^\s*(?:(?:<@[^>\s]+>|[a-z]+:\S+)\s+)*(?:in\s+\S+\s+)?on\s+(?:the\s+)?branch\s+(\S+?)\s*:/i.exec(text);
35397
+ return match ? validRef(stripPunct(match[1])) : void 0;
35398
+ }
35310
35399
  function extractSignals(rawText) {
35311
35400
  const text = unwrapSlack(rawText);
35312
35401
  const out = {};
@@ -35315,8 +35404,9 @@ function extractSignals(rawText) {
35315
35404
  const slug = slugOf(`${stripPunct(prUrl3[1])}/${stripPunct(prUrl3[2])}`);
35316
35405
  if (slug) out.pr = { repo: slug, number: Number(prUrl3[3]) };
35317
35406
  }
35318
- const kw = /(?:^|\s)on\s+(?:the\s+)?branch\s+(\S+)/i.exec(text) ?? /(?:^|\s)branch[:=](\S+)/i.exec(text);
35319
- if (kw) out.ref = validRef(stripPunct(kw[1]));
35407
+ out.ref = headBranchRef(text);
35408
+ const branchToken = /(?:^|\s)branch:(\S+)/i.exec(text);
35409
+ if (!out.ref && branchToken) out.ref = validRef(stripPunct(branchToken[1]));
35320
35410
  const treeUrl = /https?:\/\/(?:www\.)?github\.com\/([^/\s]+)\/([^/\s#?]+)\/tree\/([^\s?#]+)/i.exec(text);
35321
35411
  if (treeUrl) {
35322
35412
  const slug = slugOf(`${stripPunct(treeUrl[1])}/${stripPunct(treeUrl[2])}`);
@@ -35360,9 +35450,7 @@ function extractSignals(rawText) {
35360
35450
  out.addressed = { name: t.toLowerCase() };
35361
35451
  }
35362
35452
  if (prev === "on") {
35363
- if (!out.ref && WELL_KNOWN_REFS.has(t)) out.ref = t;
35364
- else if (!out.onSlug && slugOf(t) && validRef(t)) out.onSlug = t;
35365
- else if (!out.ref && t.includes("/") && !slugOf(t)) out.ref = validRef(t);
35453
+ if (!out.onSlug && slugOf(t) && validRef(t)) out.onSlug = t;
35366
35454
  continue;
35367
35455
  }
35368
35456
  const prShort = /^([^/#\s]+)\/([^/#\s]+)#(\d+)$/.exec(t);
@@ -35465,13 +35553,9 @@ async function resolveRepoContext(msg, history = [], probe, residentSlugs, recor
35465
35553
  if (!repo && s.repo && await vet(s.repo)) repo = s.repo;
35466
35554
  let ref = s.ref;
35467
35555
  let refFromPr = false;
35468
- if (s.onSlug) {
35469
- if (repo && !ref) {
35470
- if (slugOf(s.onSlug) !== repo) ref = s.onSlug;
35471
- } else if (!repo) {
35472
- const cand = slugOf(s.onSlug);
35473
- if (cand && await vet(cand)) repo = cand;
35474
- }
35556
+ if (s.onSlug && !repo) {
35557
+ const cand = slugOf(s.onSlug);
35558
+ if (cand && await vet(cand)) repo = cand;
35475
35559
  }
35476
35560
  if (repo) {
35477
35561
  rejected = void 0;
@@ -35688,7 +35772,7 @@ function prFacts(title, body) {
35688
35772
  if (last >= 55296 && last <= 56319) end--;
35689
35773
  return { title, body: body.slice(0, end), truncated: true };
35690
35774
  }
35691
- var PR_BODY_CAP, OWNER_RE, NAME_RE, WELL_KNOWN_REFS, DETERMINERS, CODE_SPAN, threadSignalsCache, COMPARE_FILES_CAP;
35775
+ var PR_BODY_CAP, OWNER_RE, NAME_RE, DETERMINERS, CODE_SPAN, threadSignalsCache, COMPARE_FILES_CAP;
35692
35776
  var init_repoContext = __esm({
35693
35777
  "../../src/core/repoContext.ts"() {
35694
35778
  "use strict";
@@ -35699,7 +35783,6 @@ var init_repoContext = __esm({
35699
35783
  PR_BODY_CAP = 65536;
35700
35784
  OWNER_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
35701
35785
  NAME_RE = /^[A-Za-z0-9._-]{1,100}$/;
35702
- WELL_KNOWN_REFS = /* @__PURE__ */ new Set(["main", "master", "develop", "trunk"]);
35703
35786
  DETERMINERS = /* @__PURE__ */ new Set(["the", "a", "an", "our", "my", "this"]);
35704
35787
  CODE_SPAN = /```[\s\S]*?```|`[^`\n]*`/g;
35705
35788
  threadSignalsCache = /* @__PURE__ */ new WeakMap();
@@ -40661,8 +40744,8 @@ async function executeOperatorDecision(deps, ctx) {
40661
40744
  await recordOperatorDecision(deps, msg, event, ctx.ending, ctx.trace);
40662
40745
  return answered;
40663
40746
  }
40664
- const cell4 = decideExecution({ kind: "run_command", confirm: "at_or_above", mintable: false }, surface);
40665
- const text = cell4.cell === "refuse" && cell4.names === "typed_form" ? renderHandBackLine(bind.line) : mint !== void 0 && mint.kind === "unshowable" ? UNSHOWABLE_LINE : STORE_UNREACHABLE_LINE;
40747
+ const cell3 = decideExecution({ kind: "run_command", confirm: "at_or_above", mintable: false }, surface);
40748
+ const text = cell3.cell === "refuse" && cell3.names === "typed_form" ? renderHandBackLine(bind.line) : mint !== void 0 && mint.kind === "unshowable" ? UNSHOWABLE_LINE : STORE_UNREACHABLE_LINE;
40666
40749
  await io.reply(`${verbose ? `${receipt}
40667
40750
  ` : ""}${text}`);
40668
40751
  await recordOperatorDecision(deps, msg, event, ctx.ending, ctx.trace);
@@ -44108,7 +44191,7 @@ function encodeGithubPathSegments(path) {
44108
44191
  function anchorUrl(ctx, a) {
44109
44192
  return `https://github.com/${ctx.repo}/blob/${ctx.headSha}/${encodeGithubPathSegments(a.path)}#L${a.from}-L${a.to}`;
44110
44193
  }
44111
- function cell3(s) {
44194
+ function cell2(s) {
44112
44195
  return s.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\s*\n\s*/g, " ");
44113
44196
  }
44114
44197
  function linkLabel(s) {
@@ -44146,7 +44229,7 @@ function renderPrDescriptionMarkdown(desc, ctx) {
44146
44229
  ...details(FOLD_SUMMARIES.validation(desc.validation.criteria.length), [
44147
44230
  "| Criterion | Proof |",
44148
44231
  "|---|---|",
44149
- ...desc.validation.criteria.map((c) => `| ${cell3(c.criterion)} | ${cell3(c.proof)} |`)
44232
+ ...desc.validation.criteria.map((c) => `| ${cell2(c.criterion)} | ${cell2(c.proof)} |`)
44150
44233
  ])
44151
44234
  );
44152
44235
  const agentLines = [
@@ -50645,6 +50728,11 @@ async function salvageBudgetPush(executor, opts, span) {
50645
50728
  nothing: `the coding child ended with nothing to preserve: the tree is clean and \`${opts.branch}\` holds no unpushed commits`,
50646
50729
  pushedLead: "the coding child ended with interrupted work",
50647
50730
  failedLead: `the interrupted-work push to \`${opts.branch}\` failed`
50731
+ } : opts.cue === "completion" ? {
50732
+ commit: "wip: preserve unfinished coding work \u2014 work in progress, not reviewed",
50733
+ nothing: `the coding child ended with nothing to preserve: the tree is clean and \`${opts.branch}\` holds no unpushed commits`,
50734
+ pushedLead: "the coding child ended with unfinished work",
50735
+ failedLead: `the unfinished-work push to \`${opts.branch}\` failed`
50648
50736
  } : {
50649
50737
  commit: "wip: committed at the budget wind-down \u2014 work in progress, not reviewed",
50650
50738
  nothing: nothingToSalvageNote(opts.branch),
@@ -50653,7 +50741,7 @@ async function salvageBudgetPush(executor, opts, span) {
50653
50741
  };
50654
50742
  try {
50655
50743
  const dirty = (await run2("git status --porcelain")).trim() !== "";
50656
- const endingCheckpoint = opts.cue !== "compaction";
50744
+ const endingCheckpoint = opts.cue !== "compaction" && opts.cue !== "completion";
50657
50745
  if (dirty) {
50658
50746
  await run2("git add -A");
50659
50747
  await run2(`git commit -m ${shellQuote(words.commit)}`);
@@ -51618,8 +51706,6 @@ async function runLoop(deps, ctx) {
51618
51706
  const preserveCodingChildWork = async (cue) => {
51619
51707
  if (workSalvageAttempted || !isCodingPrRun || coordinator === void 0) return;
51620
51708
  if (!workspaceObserved) await observeWorkspaceNow();
51621
- workSalvageAttempted = true;
51622
- if (cue === "ending") endingSalvageAttempted = true;
51623
51709
  const target = salvageTargetOf({
51624
51710
  pushedBranch: pushes.branch(),
51625
51711
  checkedOut: observedCheckedOut,
@@ -51629,6 +51715,10 @@ async function runLoop(deps, ctx) {
51629
51715
  cue === "budget" ? "run.budget_salvage" : "run.work_salvage",
51630
51716
  (span) => salvageBudgetPush(executor, { branch: target.branch, cue }, span)
51631
51717
  );
51718
+ const cleanCompletion = cue === "completion" && !salvaged.pushed && (/ended with nothing to preserve/i.test(salvaged.summary) || "skipped" in target && (observedUncommitted ?? 0) === 0 && (observedUnpushed ?? 0) === 0);
51719
+ if (cleanCompletion) return;
51720
+ workSalvageAttempted = true;
51721
+ if (cue === "ending" || cue === "completion") endingSalvageAttempted = true;
51632
51722
  onEvent({
51633
51723
  type: "run_note",
51634
51724
  kind: cue === "budget" ? "budget_salvage" : "work_salvage",
@@ -52068,6 +52158,7 @@ async function runLoop(deps, ctx) {
52068
52158
  if (endingNeedsPreservation && run2.control.requested === "hard") await endHarness();
52069
52159
  if (isCodingPrRun && !tailSkipped()) await observeWorkspaceNow();
52070
52160
  if (endingNeedsPreservation) await preserveCodingChildWork(budgetEnded ? "budget" : "ending");
52161
+ else if (isCodingPrRun && coordinator !== void 0 && !tailSkipped()) await preserveCodingChildWork("completion");
52071
52162
  let descriptionTurnRan = false;
52072
52163
  if (isCodingPrRun && !tailSkipped() && !endingSalvageAttempted && prDescription === void 0) {
52073
52164
  const turnTarget = await descriptionTurnTarget({
@@ -52103,6 +52194,7 @@ async function runLoop(deps, ctx) {
52103
52194
  if (!run2.control.hardSignal.aborted) await observeWorkspaceNow();
52104
52195
  }
52105
52196
  }
52197
+ if (descriptionTurnRan) await preserveCodingChildWork("completion");
52106
52198
  await endHarness();
52107
52199
  const leftBehind = isCodingPrRun && !tailSkipped() && !workSalvageAttempted ? workLeftBehindOf({ uncommittedChanges: observedUncommitted, unpushedCommits: observedUnpushed }) : void 0;
52108
52200
  const endingFacts = () => {
@@ -52448,14 +52540,6 @@ function shipUnitText(requestText, repo) {
52448
52540
  t = t.replace(new RegExp(`^\\s*in\\s+${escapeRegExp3(repo)}\\s*:?`, "i"), " ");
52449
52541
  return t.replace(/\s+/g, " ").trim();
52450
52542
  }
52451
- function refNamedExplicitly(requestText, ref) {
52452
- const text = requestText.replace(SLACK_LINK, " $1 ");
52453
- const strip = (s) => s.replace(/^[("'`<[{*]+/, "").replace(/[)"'`>\]}.,;:!?*]+$/, "");
52454
- const kw = /(?:^|\s)on\s+(?:the\s+)?branch\s+(\S+)/i.exec(text) ?? /(?:^|\s)branch[:=](\S+)/i.exec(text);
52455
- if (kw && strip(kw[1]) === ref) return true;
52456
- const treeUrl = /https?:\/\/(?:www\.)?github\.com\/[^/\s]+\/[^/\s#?]+\/tree\/([^\s?#]+)/i.exec(text);
52457
- return treeUrl !== void 0 && treeUrl !== null && strip(treeUrl[1]) === ref;
52458
- }
52459
52543
  function noRepoGuess(input) {
52460
52544
  const candidates = input.repoCandidates ?? [];
52461
52545
  if (candidates.length === 0) return void 0;
@@ -52549,18 +52633,9 @@ async function shipPreflight(input) {
52549
52633
  );
52550
52634
  }
52551
52635
  const ownBase = facts2.baseRef ?? repoCtx.baseRef;
52552
- if (ownBase !== void 0 && input.refExists) {
52553
- const exists = await input.refExists(repo, ownBase).catch(() => void 0);
52554
- if (exists === false) {
52555
- return refuse2(
52556
- "ship_preflight_base_missing",
52557
- "base ref missing",
52558
- "not started (base ref missing)",
52559
- `\u{1F6AB} ${where}'s base branch \`${ownBase}\` does not exist on \`${repo}\` \u2014 refusing fail-closed. Retarget the pull request at an existing branch first.`
52560
- );
52561
- }
52562
- }
52563
- const base = resolveBaseRef([ownBase], info?.defaultBranch);
52636
+ const ownBaseExists = ownBase !== void 0 && input.refExists ? await input.refExists(repo, ownBase).catch(() => void 0) : void 0;
52637
+ const baseFallback = ownBase !== void 0 && ownBaseExists === false ? { requested: ownBase } : void 0;
52638
+ const base = resolveBaseRef([ownBaseExists === false ? void 0 : ownBase], info?.defaultBranch);
52564
52639
  const autoMerge = facts2.autoMergeEnabled !== void 0 ? { autoMergeEnabled: facts2.autoMergeEnabled } : {};
52565
52640
  if (task) {
52566
52641
  return {
@@ -52570,6 +52645,7 @@ async function shipPreflight(input) {
52570
52645
  branch: branch2,
52571
52646
  base,
52572
52647
  adopt: { pr: repoCtx.pr, ...facts2.htmlUrl !== void 0 ? { url: facts2.htmlUrl } : {} },
52648
+ ...baseFallback !== void 0 ? { baseFallback } : {},
52573
52649
  ...autoMerge
52574
52650
  }
52575
52651
  };
@@ -52585,6 +52661,7 @@ async function shipPreflight(input) {
52585
52661
  headSha: facts2.headSha ?? repoCtx.headSha,
52586
52662
  ...facts2.htmlUrl !== void 0 ? { url: facts2.htmlUrl } : {}
52587
52663
  },
52664
+ ...baseFallback !== void 0 ? { baseFallback } : {},
52588
52665
  ...autoMerge
52589
52666
  }
52590
52667
  };
@@ -52610,20 +52687,11 @@ async function shipPreflight(input) {
52610
52687
  const ref = repoCtx.ref && !repoCtx.refFromPr && !isUnitBranch(repoCtx.ref) && repoCtx.ref.toLowerCase() !== repo.toLowerCase() ? repoCtx.ref : void 0;
52611
52688
  if (ref !== void 0 && input.refExists) {
52612
52689
  const exists = await input.refExists(repo, ref).catch(() => void 0);
52613
- if (exists === false) {
52614
- if (refNamedExplicitly(input.requestText, ref)) {
52615
- return refuse2(
52616
- "ship_preflight_base_missing",
52617
- "base ref missing",
52618
- "not started (base ref missing)",
52619
- `\u{1F6AB} The branch \`${ref}\` does not exist on \`${repo}\` \u2014 nothing was started. Name an existing branch, or drop it to run on the default branch.`
52620
- );
52621
- }
52690
+ if (exists === false)
52622
52691
  return {
52623
52692
  ok: true,
52624
52693
  entry: { repo, base: info?.defaultBranch, baseFallback: { requested: ref } }
52625
52694
  };
52626
- }
52627
52695
  }
52628
52696
  return {
52629
52697
  ok: true,
@@ -54063,7 +54131,7 @@ async function answerUnitOwnedThread(deps, ctx) {
54063
54131
  ...msg.userName !== void 0 ? { senderName: msg.userName } : {},
54064
54132
  text: ctx.text,
54065
54133
  ...attachments.length > 0 ? { attachments } : {},
54066
- mode: "steer",
54134
+ mode: owner.unit.idle !== void 0 ? "wake" : "steer",
54067
54135
  at
54068
54136
  }).catch((err2) => {
54069
54137
  console.warn(
@@ -54361,7 +54429,7 @@ async function dispatch(deps, msg, io, opts = {}) {
54361
54429
  ({ sticky, resolved, agentSource } = settled);
54362
54430
  agentSource = "sticky";
54363
54431
  }
54364
- if (owner.kind === "unit" && directives.agent === void 0) {
54432
+ if (owner.kind === "unit" && (directives.agent === void 0 || owner.unit.idle?.humanGate !== void 0)) {
54365
54433
  if ((await authorizeAgent(deps, { msg, io, refuse: refuse3, agentName: "coding" })).kind === "refused") {
54366
54434
  await recordPendingOperator();
54367
54435
  return ended;
@@ -68876,14 +68944,14 @@ var init_resumeLaunch = __esm({
68876
68944
  });
68877
68945
 
68878
68946
  // ../../src/core/runnerOwnership.ts
68879
- async function recoverRunnerOwnedPulls(instanceIds, instances) {
68880
- const owned = /* @__PURE__ */ new Set();
68947
+ async function recoverRunnerOwnedPullOwners(instanceIds, instances) {
68948
+ const owned = /* @__PURE__ */ new Map();
68881
68949
  for (const instanceId of new Set(instanceIds)) {
68882
68950
  const instance = await instances.get(instanceId);
68883
68951
  if (instance === null) continue;
68884
68952
  for (const unit of await instances.listUnits(instanceId)) {
68885
68953
  if (unit.pr !== void 0 && unit.ending === void 0)
68886
- owned.add(runnerOwnedPullKey(instance.repo, unit.pr.number));
68954
+ owned.set(runnerOwnedPullKey(instance.repo, unit.pr.number), { instanceId, unit: unit.unit });
68887
68955
  }
68888
68956
  }
68889
68957
  return owned;
@@ -68896,24 +68964,35 @@ var init_runnerOwnership = __esm({
68896
68964
  RunnerOwnershipFence = class {
68897
68965
  claimed = /* @__PURE__ */ new Set();
68898
68966
  recovered = /* @__PURE__ */ new Set();
68967
+ claimedOwners = /* @__PURE__ */ new Map();
68968
+ recoveredOwners = /* @__PURE__ */ new Map();
68899
68969
  localRecoveredInstances = /* @__PURE__ */ new Set();
68900
68970
  recoveryComplete;
68901
68971
  constructor(requiresRecovery) {
68902
68972
  this.recoveryComplete = !requiresRecovery;
68903
68973
  }
68904
- claim(repo, prNumber) {
68905
- this.claimed.add(runnerOwnedPullKey(repo, prNumber));
68974
+ claim(repo, prNumber, owner) {
68975
+ const key = runnerOwnedPullKey(repo, prNumber);
68976
+ this.claimed.add(key);
68977
+ if (owner !== void 0) this.claimedOwners.set(key, owner);
68906
68978
  }
68907
68979
  release(repo, prNumber) {
68908
68980
  const key = runnerOwnedPullKey(repo, prNumber);
68909
68981
  this.claimed.delete(key);
68910
68982
  this.recovered.delete(key);
68983
+ this.claimedOwners.delete(key);
68984
+ this.recoveredOwners.delete(key);
68911
68985
  }
68912
68986
  owns(repo, prNumber) {
68913
68987
  if (!this.recoveryComplete) throw new Error("runner ownership recovery is still in progress");
68914
68988
  const key = runnerOwnedPullKey(repo, prNumber);
68915
68989
  return this.claimed.has(key) || this.recovered.has(key);
68916
68990
  }
68991
+ owner(repo, prNumber) {
68992
+ if (!this.recoveryComplete) throw new Error("runner ownership recovery is still in progress");
68993
+ const key = runnerOwnedPullKey(repo, prNumber);
68994
+ return this.claimedOwners.get(key) ?? this.recoveredOwners.get(key);
68995
+ }
68917
68996
  async recover(outcome, instances) {
68918
68997
  for (const run2 of outcome.resumable) {
68919
68998
  if (run2.kind === "rehost" && run2.hosting !== void 0) this.localRecoveredInstances.add(run2.hosting.instanceId);
@@ -68925,9 +69004,13 @@ var init_runnerOwnership = __esm({
68925
69004
  for (const run2 of outcome.liveElsewhere) {
68926
69005
  if (run2.hosting !== void 0) activeRunnerInstances.add(run2.hosting.instanceId);
68927
69006
  }
68928
- const recovered = await recoverRunnerOwnedPulls(activeRunnerInstances, instances);
69007
+ const recovered = await recoverRunnerOwnedPullOwners(activeRunnerInstances, instances);
68929
69008
  this.recovered.clear();
68930
- for (const key of recovered) this.recovered.add(key);
69009
+ this.recoveredOwners.clear();
69010
+ for (const [key, owner] of recovered) {
69011
+ this.recovered.add(key);
69012
+ this.recoveredOwners.set(key, owner);
69013
+ }
68931
69014
  this.recoveryComplete = true;
68932
69015
  }
68933
69016
  };
@@ -69400,10 +69483,14 @@ ${cont.texts.join("\n\n")}`);
69400
69483
  }
69401
69484
  function findingsRequest(input) {
69402
69485
  const findings = input.findings.map(formatFinding).join("\n") || "(the review listed no structured findings, address its prose)";
69486
+ const answers = input.answers !== void 0 && input.answers.length > 0 ? `
69487
+
69488
+ A person answered the human-gated finding. Treat the finding and this answer together as the fix brief:
69489
+ ${input.answers.join("\n\n")}` : "";
69403
69490
  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.
69404
69491
 
69405
69492
  Findings:
69406
- ${findings}
69493
+ ${findings}${answers}
69407
69494
 
69408
69495
  Review:
69409
69496
  ${input.review}`;
@@ -69422,7 +69509,8 @@ ${task}` : task;
69422
69509
  case "review": {
69423
69510
  let prior;
69424
69511
  if (brief.prior !== void 0) {
69425
- const findings = [...(await facts(readers, brief.prior.reviewRunId)).findings ?? [], ...brief.checks ?? []];
69512
+ const reviewFindings = brief.prior.findings ?? (brief.prior.reviewRunId !== void 0 ? (await facts(readers, brief.prior.reviewRunId)).findings ?? [] : []);
69513
+ const findings = [...reviewFindings, ...brief.checks ?? []];
69426
69514
  const recorded = brief.prior.codingRunId !== void 0 ? (await facts(readers, brief.prior.codingRunId)).dispositions ?? [] : [];
69427
69515
  const { matched, dropped } = matchDispositions(findings, recorded);
69428
69516
  prior = { findings, dispositions: matched, dropped };
@@ -69440,13 +69528,14 @@ ${task}` : task;
69440
69528
  ${turn}`, contract };
69441
69529
  }
69442
69530
  case "findings": {
69443
- const review = await facts(readers, brief.reviewRunId);
69531
+ const review = brief.reviewRunId !== void 0 ? brief.findings !== void 0 ? await readers.readRunFacts(brief.reviewRunId) ?? {} : await facts(readers, brief.reviewRunId) : {};
69444
69532
  return {
69445
69533
  preset: "coding",
69446
69534
  prompt: findingsRequest({
69447
69535
  where,
69448
- findings: [...review.findings ?? [], ...brief.checks ?? []],
69449
- review: review.finalReply ?? ""
69536
+ findings: [...brief.findings ?? review.findings ?? [], ...brief.checks ?? []],
69537
+ review: review.finalReply ?? "",
69538
+ ...brief.answers !== void 0 ? { answers: brief.answers } : {}
69450
69539
  }),
69451
69540
  ref: unit.branch
69452
69541
  };
@@ -69920,6 +70009,10 @@ ${foldThreadEvents(folded)}` };
69920
70009
  at
69921
70010
  });
69922
70011
  }
70012
+ function reviewAskedAtOf(events) {
70013
+ const posted = [...events ?? []].reverse().find((event) => event.type === "review_posted");
70014
+ return posted?.type === "review_posted" && typeof posted.at === "number" ? posted.at : void 0;
70015
+ }
69923
70016
  function finalReplyOf2(events) {
69924
70017
  const last = [...events ?? []].reverse().find((e) => e.type === "answer");
69925
70018
  return last && last.type === "answer" ? last.text : void 0;
@@ -69954,6 +70047,74 @@ async function reviewPostedAt(deps, pr, verdict, head) {
69954
70047
  (r) => r.author?.login === self.login && (r.author.id === void 0 || r.author.id === self.id) && r.commitId !== void 0 && sameCommit(r.commitId.toLowerCase(), head) && r.body.startsWith(token2)
69955
70048
  );
69956
70049
  }
70050
+ function legacyPostedFinding(value, body) {
70051
+ if (isFindingShape(value)) return value;
70052
+ if (typeof value !== "object" || value === null) return void 0;
70053
+ const row2 = value;
70054
+ if (row2.title !== void 0 || typeof row2.id !== "string" || typeof row2.severity !== "string") return void 0;
70055
+ const prefix = `| ${row2.severity} | **${escapeMarkdownTableCell(row2.id)}** `;
70056
+ const header4 = "| Severity | Finding | Where |\n| --- | --- | --- |\n";
70057
+ const tableStart = body.indexOf(header4);
70058
+ if (tableStart < 0) return void 0;
70059
+ const table = body.slice(tableStart + header4.length).split("\n\n", 1)[0];
70060
+ const titles = table.split("\n").filter((line4) => line4.startsWith(prefix)).flatMap((line4) => {
70061
+ const tail = line4.slice(prefix.length);
70062
+ const boundary = tail.lastIndexOf(" | ");
70063
+ return boundary > 0 ? [unescapeMarkdownTableCell(tail.slice(0, boundary)).trim()] : [];
70064
+ }).filter(Boolean);
70065
+ if (titles.length !== 1) return void 0;
70066
+ const recovered = { ...row2, title: titles[0] };
70067
+ return isFindingShape(recovered) ? recovered : void 0;
70068
+ }
70069
+ async function postedHumanGateAnswer(deps, pr, requester, addressSeverity) {
70070
+ if (deps.fetchPrComments === void 0) return void 0;
70071
+ const [reviews, comments, self] = await Promise.all([
70072
+ deps.fetchPrReviews(pr).catch(() => void 0),
70073
+ deps.fetchPrComments(pr).catch(() => void 0),
70074
+ deps.selfIdentity().catch(() => void 0)
70075
+ ]);
70076
+ if (reviews === void 0 || comments === void 0 || self === void 0) return void 0;
70077
+ const own = reviews.filter(
70078
+ (review) => review.author?.login === self.login && (review.author.id === void 0 || review.author.id === self.id) && review.submittedAt !== void 0 && Number.isFinite(Date.parse(review.submittedAt))
70079
+ ).sort((a, b) => Date.parse(a.submittedAt) - Date.parse(b.submittedAt)).at(-1);
70080
+ if (own === void 0) return void 0;
70081
+ const marker = /<!-- switchboard:verdict (\{[^\n]*\}) -->/.exec(own.body);
70082
+ if (marker === null) return void 0;
70083
+ let payload;
70084
+ try {
70085
+ payload = JSON.parse(marker[1]);
70086
+ } catch {
70087
+ return void 0;
70088
+ }
70089
+ if (typeof payload !== "object" || payload === null) return void 0;
70090
+ const row2 = payload;
70091
+ if (row2.verdict !== "approve" && row2.verdict !== "request_changes") return void 0;
70092
+ if (!Array.isArray(row2.findings)) return void 0;
70093
+ const postedFindings = row2.findings.map((finding) => legacyPostedFinding(finding, own.body));
70094
+ if (!postedFindings.every((finding) => finding !== void 0)) return void 0;
70095
+ const findings = row2.verdict === "approve" ? findingsAtOrAbove(postedFindings, addressSeverity) : postedFindings;
70096
+ if (findings.length === 0 || !findings.every((finding) => finding.humanGated === true)) return void 0;
70097
+ const candidates = comments.filter(
70098
+ (comment) => comment.author.type === "User" && comment.body.trim() !== "" && Number.isFinite(Date.parse(comment.createdAt)) && Date.parse(comment.createdAt) >= githubSecondStart(Date.parse(own.submittedAt))
70099
+ ).sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
70100
+ let answer;
70101
+ for (const candidate of candidates) {
70102
+ const authorized = await deps.commenterAuthorized(requester, candidate.author).catch(() => false);
70103
+ if (authorized) {
70104
+ answer = candidate;
70105
+ break;
70106
+ }
70107
+ }
70108
+ if (answer === void 0) return void 0;
70109
+ return {
70110
+ round: 1,
70111
+ findings,
70112
+ verdict: row2.verdict,
70113
+ answer: answer.body.trim(),
70114
+ author: answer.author.login,
70115
+ commentId: String(answer.id)
70116
+ };
70117
+ }
69957
70118
  async function reviewPostedAtPatiently(deps, pr, verdict, head) {
69958
70119
  const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve13) => setTimeout(resolve13, ms)));
69959
70120
  let answer;
@@ -69986,6 +70147,7 @@ async function readRecord(body, deps) {
69986
70147
  const full = await deps.runs.getRun(body.runId, { include: "messages" });
69987
70148
  const record2 = full.ok ? full.value : view;
69988
70149
  const finalReply = finalReplyOf2(record2.events);
70150
+ const reviewAskedAt = reviewAskedAtOf(record2.events);
69989
70151
  const pr = prOpenedOf(record2.events);
69990
70152
  const instanceRow = await deps.instances.get(id.value);
69991
70153
  if (view.status === "interrupted") {
@@ -70027,6 +70189,7 @@ async function readRecord(body, deps) {
70027
70189
  ...pr !== void 0 ? { pr } : {},
70028
70190
  ...record2.verdict !== void 0 ? { verdict: record2.verdict } : {},
70029
70191
  ...record2.reviewHead !== void 0 ? { reviewHead: record2.reviewHead } : {},
70192
+ ...reviewAskedAt !== void 0 ? { reviewAskedAt } : {},
70030
70193
  ...posted !== void 0 ? { reviewPosted: posted.reviewPosted } : {},
70031
70194
  ...posted?.reviewPostReason !== void 0 ? { reviewPostReason: posted.reviewPostReason } : {},
70032
70195
  ...record2.dispositions !== void 0 ? { dispositions: record2.dispositions } : {},
@@ -70172,11 +70335,20 @@ async function prCheck(body, deps) {
70172
70335
  const open2 = await deps.findOpenPrByHead(instance.repo, branch2);
70173
70336
  if (open2) {
70174
70337
  await remember({ number: open2.number, url: open2.htmlUrl });
70175
- deps.runnerOwnership?.claim(instance.repo, open2.number);
70338
+ deps.runnerOwnership?.claim(instance.repo, open2.number, {
70339
+ instanceId: instance.id,
70340
+ unit: unit.row?.unit ?? body.unit
70341
+ });
70176
70342
  const entryFacts = entry ? await deps.fetchPrFacts({ repo: instance.repo, number: open2.number }).catch(() => void 0) : void 0;
70177
70343
  const branchHead = entryFacts?.headSha;
70178
70344
  const entryHead = branchHead ?? open2.headSha;
70179
70345
  const approved = entry && entryHead !== void 0 ? await reviewPostedAt(deps, { repo: instance.repo, number: open2.number }, "approve", entryHead) : void 0;
70346
+ const humanGate = entry ? await postedHumanGateAnswer(
70347
+ deps,
70348
+ { repo: instance.repo, number: open2.number },
70349
+ instance.userId,
70350
+ instance.addressSeverity ?? DEFAULT_ADDRESS_SEVERITY
70351
+ ) : void 0;
70180
70352
  const entryChecks = entry && entryHead !== void 0 ? await deps.fetchCommitChecks(instance.repo, entryHead).catch(() => void 0) : void 0;
70181
70353
  const checks = body.checks === true && open2.headSha !== void 0 ? await deps.fetchCommitChecks(instance.repo, open2.headSha).catch(() => void 0) : entryChecks;
70182
70354
  const prRef = { repo: instance.repo, number: open2.number };
@@ -70192,6 +70364,7 @@ async function prCheck(body, deps) {
70192
70364
  ...open2.headSha !== void 0 ? { headSha: open2.headSha } : {},
70193
70365
  ...branchHead !== void 0 ? { branchHead } : {},
70194
70366
  ...approved !== void 0 ? { approved } : {},
70367
+ ...humanGate !== void 0 ? { humanGate } : {},
70195
70368
  // The pull request's own auto-merge fact (agent-ship item 9), so a
70196
70369
  // merge_ready ending can name it at the approved head.
70197
70370
  ...open2.autoMergeEnabled !== void 0 ? { autoMergeEnabled: open2.autoMergeEnabled } : {},
@@ -70551,6 +70724,7 @@ function parseIdle(ending, runId3, at) {
70551
70724
  ...typeof runId3 === "string" && RUN_ID_PATTERN.test(runId3) ? { runId: runId3 } : {},
70552
70725
  spendUsd: typeof ending.spendUsd === "number" && Number.isFinite(ending.spendUsd) ? ending.spendUsd : null,
70553
70726
  ...isHandoffShape(ending.handoff) ? { handoff: ending.handoff } : {},
70727
+ ...isHumanGatePending(ending.humanGate) ? { humanGate: ending.humanGate } : {},
70554
70728
  wakes: 0
70555
70729
  };
70556
70730
  }
@@ -70590,20 +70764,37 @@ async function unitWake(body, deps) {
70590
70764
  }
70591
70765
  const events = await deps.instances.listEvents({ instanceId: instance.id, unit: row2.unit }, true);
70592
70766
  const idle = row2.idle;
70767
+ const askedAt = idle.humanGate?.askedAt;
70768
+ const wakeEvents = askedAt !== void 0 ? events.filter(
70769
+ (event) => event.id?.startsWith("github:issue-comment:") ? event.at >= githubSecondStart(askedAt) : event.at > askedAt
70770
+ ) : events;
70593
70771
  const senderOf = (e) => e.senderName ?? e.sender;
70594
- const senders = [...new Set(events.map(senderOf))];
70595
- const requesterWoke = events.some((event) => event.sender === instance.userId);
70596
- const stopperWoke = events.some((event) => event.mode === "interrupt");
70772
+ const senders = [...new Set(wakeEvents.map(senderOf))];
70773
+ const requesterWoke = wakeEvents.some((event) => event.sender === instance.userId);
70774
+ const stopperWoke = wakeEvents.some((event) => event.mode === "interrupt");
70597
70775
  const grantFact = deps.shipGrantFor?.(instance) ?? {
70598
70776
  grant: instance.grant ?? DEFAULT_GRANT,
70599
70777
  source: instance.grantSource ?? "org"
70600
70778
  };
70601
70779
  const renewalsSpent = row2.segments?.length ?? 0;
70602
70780
  let answer;
70603
- if (events.length === 0) {
70781
+ if (wakeEvents.length === 0) {
70604
70782
  answer = { kind: "answered", reply: "Nothing new was waiting for this unit." };
70605
70783
  } else if (idle.wakes + 1 >= IDLE_WAKES_MAX) {
70606
70784
  answer = { kind: "expired" };
70785
+ } else if (idle.humanGate !== void 0) {
70786
+ answer = {
70787
+ kind: "segment",
70788
+ index: row2.segments?.at(-1)?.index ?? 1,
70789
+ ...idle.from !== void 0 ? { from: idle.from } : {},
70790
+ ...idle.runId !== void 0 ? { runId: idle.runId } : {},
70791
+ spendUsd: idle.spendUsd,
70792
+ ...idle.handoff !== void 0 ? { handoff: idle.handoff } : {},
70793
+ texts: wakeEvents.map(attributedText),
70794
+ senders,
70795
+ leaseMs: minutesToMs(instance.caps?.maxMinutes ?? resolveShipCaps(void 0).maxMinutes),
70796
+ humanGate: idle.humanGate
70797
+ };
70607
70798
  } else {
70608
70799
  const segmentStart = row2.segments?.at(-1)?.at ?? row2.startedAt ?? idle.at;
70609
70800
  const leaseMs = Math.max(
@@ -70618,7 +70809,7 @@ async function unitWake(body, deps) {
70618
70809
  ...idle.runId !== void 0 ? { runId: idle.runId } : {},
70619
70810
  spendUsd: idle.spendUsd,
70620
70811
  ...idle.handoff !== void 0 ? { handoff: idle.handoff } : {},
70621
- texts: events.map(attributedText),
70812
+ texts: wakeEvents.map(attributedText),
70622
70813
  senders,
70623
70814
  leaseMs
70624
70815
  };
@@ -70643,7 +70834,7 @@ async function unitWake(body, deps) {
70643
70834
  ...idle.runId !== void 0 ? { runId: idle.runId } : {},
70644
70835
  spendUsd: idle.spendUsd,
70645
70836
  ...idle.handoff !== void 0 ? { handoff: idle.handoff } : {},
70646
- texts: events.map(attributedText),
70837
+ texts: wakeEvents.map(attributedText),
70647
70838
  senders
70648
70839
  };
70649
70840
  } else {
@@ -70654,7 +70845,7 @@ async function unitWake(body, deps) {
70654
70845
  }
70655
70846
  }
70656
70847
  }
70657
- const countedIdle = events.length > 0 ? { ...idle, wakes: idle.wakes + 1 } : idle;
70848
+ const countedIdle = wakeEvents.length > 0 ? { ...idle, wakes: idle.wakes + 1 } : idle;
70658
70849
  const segments2 = row2.segments ?? [];
70659
70850
  const updated = {
70660
70851
  ...row2,
@@ -70837,12 +71028,14 @@ ${rendered}` : "");
70837
71028
  )
70838
71029
  );
70839
71030
  }
70840
- if (ending.kind === "held" && updated.pr !== void 0) {
70841
- await deps.github.commentIssue(instance.repo, updated.pr.number, `**Plan runner \u2014 ${row2.unit} held**
71031
+ const parkedHumanGate = ending.kind === "idle" && idle?.humanGate !== void 0;
71032
+ if ((ending.kind === "held" || parkedHumanGate) && updated.pr !== void 0) {
71033
+ const state = parkedHumanGate ? "waiting for a person" : "held";
71034
+ await deps.github.commentIssue(instance.repo, updated.pr.number, `**Plan runner \u2014 ${row2.unit} ${state}**
70842
71035
 
70843
71036
  ${ending.report}`).catch(
70844
71037
  (err2) => (deps.log ?? console.warn)(
70845
- `[coordinator] ${instance.id} ${row2.unit}: the held report could not be posted on the pull request: ${describe11(err2)}`
71038
+ `[coordinator] ${instance.id} ${row2.unit}: the human-gated report could not be posted on the pull request: ${describe11(err2)}`
70846
71039
  )
70847
71040
  );
70848
71041
  }
@@ -70877,7 +71070,10 @@ async function rebaseStep(body, deps) {
70877
71070
  if (!row2) return json3(404, { ok: false, error: "unknown_unit", at });
70878
71071
  if (row2.pr?.number !== void 0 && row2.pr.number !== body.prNumber)
70879
71072
  return json3(409, { ok: false, error: "pull_request_moved", at });
70880
- deps.runnerOwnership?.claim(instance.repo, body.prNumber);
71073
+ deps.runnerOwnership?.claim(instance.repo, body.prNumber, {
71074
+ instanceId: instance.id,
71075
+ unit: row2.unit
71076
+ });
70881
71077
  if (deps.runnerRebase === void 0)
70882
71078
  return json3(200, { ok: true, outcome: "refused", reason: "the runner's rebase resolver is unavailable", at });
70883
71079
  try {
@@ -71324,7 +71520,7 @@ function createAdminCoordinatorHandler(deps) {
71324
71520
  })();
71325
71521
  };
71326
71522
  }
71327
- var COORDINATOR_ADMIN_PREFIX, MAX_SPAWN_PROMPT_CHARS, FINISHED_LOOKBACK_PAGES, MAX_ADMIN_BODY_BYTES, UNIT_ID, PRESETS_OF_KIND, invalid, json3, describe11, isGenerated2, EVERY_RUN, REVIEW_POSTED_CHECKS, REVIEW_POSTED_RECHECK_MS, ROUND_OUTCOMES, ENDING_ICON, STEPS;
71523
+ var COORDINATOR_ADMIN_PREFIX, MAX_SPAWN_PROMPT_CHARS, FINISHED_LOOKBACK_PAGES, MAX_ADMIN_BODY_BYTES, UNIT_ID, PRESETS_OF_KIND, invalid, json3, describe11, isGenerated2, EVERY_RUN, REVIEW_POSTED_CHECKS, REVIEW_POSTED_RECHECK_MS, githubSecondStart, ROUND_OUTCOMES, ENDING_ICON, STEPS;
71328
71524
  var init_adminCoordinator = __esm({
71329
71525
  "../../src/channels/adminCoordinator.ts"() {
71330
71526
  "use strict";
@@ -71376,6 +71572,7 @@ var init_adminCoordinator = __esm({
71376
71572
  EVERY_RUN = { kind: "all" };
71377
71573
  REVIEW_POSTED_CHECKS = 3;
71378
71574
  REVIEW_POSTED_RECHECK_MS = 2e3;
71575
+ githubSecondStart = (at) => Math.floor(at / SECOND_MS) * SECOND_MS;
71379
71576
  ROUND_OUTCOMES = [
71380
71577
  "started",
71381
71578
  "pr_opened",
@@ -71479,6 +71676,56 @@ async function handleCheckRunIntake(headers, rawBody, deps) {
71479
71676
  body: { ok: true, settled: true, sent: sends.filter((s) => s.kind === "sent").length, of: instances.length }
71480
71677
  };
71481
71678
  }
71679
+ async function handleIssueCommentIntake(headers, rawBody, deps) {
71680
+ if (deps.secret === void 0 || deps.secret === "") return { status: 503, body: { error: "disabled" } };
71681
+ if (headers.signature === void 0 || !await verifyWebhookSignature(deps.secret, rawBody, headers.signature))
71682
+ return { status: 401, body: { error: "unauthorized" } };
71683
+ if (headers.event !== "issue_comment") return { status: 200, body: { ok: true, ignored: "event" } };
71684
+ let payload;
71685
+ try {
71686
+ payload = JSON.parse(rawBody);
71687
+ } catch {
71688
+ return { status: 400, body: { error: "invalid_json" } };
71689
+ }
71690
+ if (typeof payload !== "object" || payload === null) return { status: 400, body: { error: "invalid_body" } };
71691
+ const p = payload;
71692
+ if (p.action !== "created") return { status: 200, body: { ok: true, ignored: "action" } };
71693
+ if (p.issue?.pull_request === void 0) return { status: 200, body: { ok: true, ignored: "issue" } };
71694
+ const repo = p.repository?.full_name;
71695
+ const prNumber = p.issue.number;
71696
+ const comment = p.comment;
71697
+ if (typeof repo !== "string" || repo === "" || typeof prNumber !== "number" || !Number.isInteger(prNumber) || typeof comment?.id !== "number" || typeof comment.body !== "string" || typeof comment.created_at !== "string" || typeof comment.user?.login !== "string" || typeof comment.user.type !== "string")
71698
+ return { status: 400, body: { error: "invalid_body" } };
71699
+ if (comment.user.type !== "User") return { status: 200, body: { ok: true, ignored: "sender" } };
71700
+ let owner;
71701
+ try {
71702
+ owner = deps.ownerOf(repo, prNumber);
71703
+ } catch {
71704
+ return { status: 503, body: { error: "ownership_unavailable" } };
71705
+ }
71706
+ if (owner === void 0) return { status: 200, body: { ok: true, ignored: "unowned" } };
71707
+ const instance = await deps.instances.get(owner.instanceId);
71708
+ if (instance === null) return { status: 200, body: { ok: true, ignored: "ended" } };
71709
+ const authorized = await deps.commenterAuthorized(instance.userId, {
71710
+ login: comment.user.login,
71711
+ ...typeof comment.user.id === "number" ? { id: comment.user.id } : {}
71712
+ }).catch(() => false);
71713
+ if (!authorized) return { status: 200, body: { ok: true, ignored: "sender" } };
71714
+ const row2 = (await deps.instances.listUnits(owner.instanceId)).find((unit) => unit.unit === owner.unit);
71715
+ if (row2 === void 0 || row2.ending !== void 0) return { status: 200, body: { ok: true, ignored: "ended" } };
71716
+ const created = Date.parse(comment.created_at);
71717
+ const appended = await deps.instances.appendEvent(owner, {
71718
+ id: `github:issue-comment:${comment.id}`,
71719
+ sender: `github:${typeof comment.user.id === "number" ? comment.user.id : comment.user.login}`,
71720
+ senderName: comment.user.login,
71721
+ text: comment.body,
71722
+ mode: row2.idle !== void 0 ? "wake" : "steer",
71723
+ at: Number.isFinite(created) ? created : deps.now()
71724
+ });
71725
+ if (!appended.ok) return { status: 503, body: { error: "store_unavailable" } };
71726
+ const sent = await sendUnitNudge(deps.workflow, owner);
71727
+ return { status: 200, body: { ok: true, appended: true, seq: appended.seq, nudge: sent.kind } };
71728
+ }
71482
71729
  async function handlePushIntake(headers, rawBody, deps) {
71483
71730
  if (deps.secret === void 0 || deps.secret === "") return { status: 503, body: { error: "disabled" } };
71484
71731
  if (headers.signature === void 0 || !await verifyWebhookSignature(deps.secret, rawBody, headers.signature))
@@ -71528,7 +71775,14 @@ function createGithubWebhookHandler(deps) {
71528
71775
  event: one(req.headers[GITHUB_EVENT_HEADER]),
71529
71776
  signature: one(req.headers[GITHUB_SIGNATURE_HEADER])
71530
71777
  };
71531
- const result = headers.event === "push" ? await handlePushIntake(headers, body.body, { secret: deps.secret, watch: deps.watch }) : await handleCheckRunIntake(headers, body.body, deps);
71778
+ const result = headers.event === "push" ? await handlePushIntake(headers, body.body, { secret: deps.secret, watch: deps.watch }) : headers.event === "issue_comment" && deps.ownerOf !== void 0 && deps.instances !== void 0 && deps.commenterAuthorized !== void 0 && deps.now !== void 0 ? await handleIssueCommentIntake(headers, body.body, {
71779
+ secret: deps.secret,
71780
+ ownerOf: deps.ownerOf,
71781
+ instances: deps.instances,
71782
+ commenterAuthorized: deps.commenterAuthorized,
71783
+ workflow: deps.workflow,
71784
+ now: deps.now
71785
+ }) : await handleCheckRunIntake(headers, body.body, deps);
71532
71786
  answer(result.status, result.body);
71533
71787
  };
71534
71788
  }
@@ -72598,9 +72852,16 @@ async function runBot() {
72598
72852
  );
72599
72853
  }
72600
72854
  });
72855
+ const commenterAuthorized = async (requester, author) => {
72856
+ const binding = await bindingOf(requester, config);
72857
+ return binding !== void 0 && author.id === binding.id && author.login.toLowerCase() === binding.login.toLowerCase();
72858
+ };
72601
72859
  const githubWebhook = createGithubWebhookHandler({
72602
72860
  secret: processSecrets.get("GITHUB_WEBHOOK_SECRET")?.reveal(),
72603
72861
  watch: mergeWatch,
72862
+ ownerOf: (repo, prNumber) => runnerOwnership.owner(repo, prNumber),
72863
+ instances: coordinatorInstances,
72864
+ commenterAuthorized,
72604
72865
  checksSettled: async (repo, headSha) => {
72605
72866
  const checks = await fetchCommitChecks(repo, headSha);
72606
72867
  return checks !== void 0 && checks.total > 0 && checks.pending.length === 0;
@@ -72662,6 +72923,8 @@ async function runBot() {
72662
72923
  github: deps.githubApi ?? new RestGithubApi(),
72663
72924
  createBranchRef,
72664
72925
  fetchPrReviews: fetchPullRequestReviews,
72926
+ fetchPrComments: fetchPullRequestComments,
72927
+ commenterAuthorized,
72665
72928
  // The merge step (record 0031's merge grant): the pull request as GitHub has it, the checks at its head, the squash.
72666
72929
  fetchPrFacts: fetchPullRequestFacts,
72667
72930
  fetchCommitChecks,
@@ -73288,6 +73551,7 @@ var init_index = __esm({
73288
73551
  init_pullSweepWiring();
73289
73552
  init_githubApi();
73290
73553
  init_githubApp();
73554
+ init_authorBinding();
73291
73555
  init_identityRewrite();
73292
73556
  init_run2();
73293
73557
  init_record();