@coreplane/switchboard 1.255.0 → 1.256.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/assets/deploy/cloudflare-resident/drain.ts +55 -1
  2. package/dist/assets/deploy/cloudflare-resident/worker.ts +177 -32
  3. package/dist/assets/package-lock.json +3 -3
  4. package/dist/assets/package.json +1 -1
  5. package/dist/assets/source.json +3 -3
  6. package/dist/assets/src/agents/registry.ts +31 -0
  7. package/dist/assets/src/core/coordinator/driver.ts +11 -1
  8. package/dist/assets/src/core/pipelineStanding.ts +57 -0
  9. package/dist/assets/src/core/reviewVerdict.ts +49 -0
  10. package/dist/assets/src/core/ship/contract.ts +20 -30
  11. package/dist/assets/src/core/ship/coordinator.ts +195 -68
  12. package/dist/assets/web/dist/.vite/manifest.json +31 -31
  13. package/dist/assets/web/dist/assets/{HomePage-Be7jLLnU.js → HomePage-BEiStFwW.js} +1 -1
  14. package/dist/assets/web/dist/assets/{ResidentDetailPage-D_RD6wLo.js → ResidentDetailPage-jYhEeeyu.js} +1 -1
  15. package/dist/assets/web/dist/assets/{ResidentsIndexPage-BLkSuCxo.js → ResidentsIndexPage-DvFmlV4U.js} +1 -1
  16. package/dist/assets/web/dist/assets/RunFoldRow-DvWzR1JQ.js +1 -0
  17. package/dist/assets/web/dist/assets/RunRoutePage-CvZ-TOT3.js +9 -0
  18. package/dist/assets/web/dist/assets/{RunsIndexPage-DW-HHuZa.js → RunsIndexPage-JqVSDOok.js} +1 -1
  19. package/dist/assets/web/dist/assets/{ScheduledPage-3aYsDf-q.js → ScheduledPage-Viim-bus.js} +1 -1
  20. package/dist/assets/web/dist/assets/{SettingsPage-DG-p5Xy1.js → SettingsPage-CYUy8McC.js} +1 -1
  21. package/dist/assets/web/dist/assets/{StatusDot-CEnGlyAL.js → StatusDot-Dv6UMaPy.js} +1 -1
  22. package/dist/assets/web/dist/assets/{Tooltip-CiunVowT.js → Tooltip-Bd-5Rypv.js} +1 -1
  23. package/dist/assets/web/dist/assets/UnitRoutePage-B81wEnKN.js +1 -0
  24. package/dist/assets/web/dist/assets/{dist-luhv3YSo.js → dist-Nl3uaxrP.js} +1 -1
  25. package/dist/assets/web/dist/assets/{main-mAKx_zo9.js → main-DRxWlffc.js} +2 -2
  26. package/dist/cli.js +319 -64
  27. package/package.json +1 -1
  28. package/dist/assets/web/dist/assets/RunFoldRow-CfuZqf_O.js +0 -1
  29. package/dist/assets/web/dist/assets/RunRoutePage-CmWYGR36.js +0 -9
  30. package/dist/assets/web/dist/assets/UnitRoutePage-iOYnmTcQ.js +0 -1
package/dist/cli.js CHANGED
@@ -1933,7 +1933,7 @@ function hasApproach(unit) {
1933
1933
  function renderFirstInstruction(rebase) {
1934
1934
  const branch2 = rebase.branch ? `\`${rebase.branch}\`` : "the unit's branch";
1935
1935
  const onto = rebase.onto ? `\`${rebase.onto}\`` : "the merged parent";
1936
- const gates = FAST_GATES_BEFORE_PUSH;
1936
+ const gates = `${FAST_GATES_POINTER} ${GATE_RECEIPTS}`;
1937
1937
  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} Right before each push, fetch ${onto} again and rebase once more if it moved while you worked, so the pull request is not born conflicting. At the wind-down note, commit and push what compiles, say what does not, then answer. ${TIMEOUT_ON_LONG_COMMANDS}`;
1938
1938
  }
1939
1939
  function renderSpecRow(row2, dropText) {
@@ -2019,7 +2019,7 @@ ${renderGuards(contract.guards)}`
2019
2019
  }
2020
2020
  return { text, chars: text.length, dropped, overBudget: text.length > maxChars };
2021
2021
  }
2022
- var PR_TITLE_GUARD, GUARDS, CONTRACT_HEADING, CONTRACT_SECTION_HEADINGS, DEFAULT_CONTRACT_MAX_CHARS, PLAN_MAX_CHARS, UNIT_HEADING, ANY_HEADING, BULLET, SPEC_MENTION, ITEM_LIST, NAMED_REF, ANY_MD_MENTION, SPEC_ITEM, TITLE_MAX, APPROACH_DROPPED, TIMEOUT_ON_LONG_COMMANDS, TOUCHED_TESTS_COMMAND, FAST_GATES_BEFORE_PUSH, DROP_NOTE;
2022
+ var PR_TITLE_GUARD, GUARDS, CONTRACT_HEADING, CONTRACT_SECTION_HEADINGS, DEFAULT_CONTRACT_MAX_CHARS, PLAN_MAX_CHARS, UNIT_HEADING, ANY_HEADING, BULLET, SPEC_MENTION, ITEM_LIST, NAMED_REF, ANY_MD_MENTION, SPEC_ITEM, TITLE_MAX, APPROACH_DROPPED, TIMEOUT_ON_LONG_COMMANDS, FAST_GATES_POINTER, GATE_RECEIPTS, DROP_NOTE;
2023
2023
  var init_contract = __esm({
2024
2024
  "../../src/core/ship/contract.ts"() {
2025
2025
  "use strict";
@@ -2067,8 +2067,8 @@ var init_contract = __esm({
2067
2067
  TITLE_MAX = 80;
2068
2068
  APPROACH_DROPPED = "- **Approach**: (dropped to fit the contract's budget \u2014 read it in the plan)";
2069
2069
  TIMEOUT_ON_LONG_COMMANDS = "State a timeout on any command you expect to run longer than a minute: a timeout that reaches past the loop's end is refused before the command runs, never cut midway.";
2070
- TOUCHED_TESTS_COMMAND = "`npx vitest run` on the test files you touched, by name,";
2071
- 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. 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 resident they cost minutes that every other run pays for. Paste each command's exit line into the PR description's validation table as the row's proof; a gate you could not run goes under the handoff's unproven list and is never claimed clean.`;
2070
+ FAST_GATES_POINTER = "The fast gates are the ones your preset instructions name (THE FAST GATES): the changed-set forms, never the whole project \u2014 the full suite, the full typecheck and the full verification are CI's, never yours to run.";
2071
+ GATE_RECEIPTS = "Paste each command's exit line into the PR description's validation table as the row's proof; a gate you could not run goes under the handoff's unproven list and is never claimed clean.";
2072
2072
  DROP_NOTE = {
2073
2073
  approach: "the unit's Approach text was dropped (read it in the plan)",
2074
2074
  agentRules: "the agent rules were dropped (read the file at the head)",
@@ -2129,7 +2129,7 @@ function getAgent(name) {
2129
2129
  }
2130
2130
  return a;
2131
2131
  }
2132
- var MACHINE_CLASSES, IDENTITIES, PR_DESCRIPTION_TEMPLATE, NEVER_MERGE, CONTRACT_HEADINGS_LIST, UNIT_CONTRACT, UNIT_HANDOFF, IMAGE_TOOLCHAIN, SANDBOX_TOOLCHAIN, RESIDENT_TOOLCHAIN, SHOW_FILES, BREVITY_RULE, FENCED_CONTENT_RULE, NOTEPAD, CHECKS_BY_COST, CODING_SYSTEM, CODING_SYSTEM_RESIDENT, CODING_SYSTEM_SEEDED, REVIEW_FINAL_MESSAGE, REVIEW_VERDICT_INSTRUCTION, REVIEW_SPEC_CHECK, REVIEW_UNIT_CONTRACT, REVIEW_WHOLE_CHANGE, REVIEW_SYSTEM, REVIEW_SYSTEM_RESIDENT, REVIEW_SYSTEM_SEEDED, RESEARCH_SYSTEM, GENERAL_SYSTEM, EXPLORE_SYSTEM, COMPOUND_PRESET, WORK_PRESETS, AGENTS;
2132
+ var MACHINE_CLASSES, IDENTITIES, PR_DESCRIPTION_TEMPLATE, NEVER_MERGE, CONTRACT_HEADINGS_LIST, UNIT_CONTRACT, UNIT_HANDOFF, IMAGE_TOOLCHAIN, SANDBOX_TOOLCHAIN, RESIDENT_TOOLCHAIN, SHOW_FILES, BREVITY_RULE, FENCED_CONTENT_RULE, NOTEPAD, CHECKS_BY_COST, TOUCHED_TESTS_COMMAND, FAST_GATES_BEFORE_PUSH, CODING_SYSTEM, CODING_SYSTEM_RESIDENT, CODING_SYSTEM_SEEDED, REVIEW_FINAL_MESSAGE, REVIEW_VERDICT_INSTRUCTION, REVIEW_SPEC_CHECK, REVIEW_UNIT_CONTRACT, REVIEW_WHOLE_CHANGE, REVIEW_SYSTEM, REVIEW_SYSTEM_RESIDENT, REVIEW_SYSTEM_SEEDED, RESEARCH_SYSTEM, GENERAL_SYSTEM, EXPLORE_SYSTEM, COMPOUND_PRESET, WORK_PRESETS, AGENTS;
2133
2133
  var init_registry = __esm({
2134
2134
  "../../src/agents/registry.ts"() {
2135
2135
  "use strict";
@@ -2167,6 +2167,8 @@ Text stays in your message; do not attach what you can say.`;
2167
2167
  NOTEPAD = `YOUR NOTES AND YOUR REACH BACK. This thread's conversation outlives your context window and this run: every turn \u2014 yours, the person's, every tool call and its output, from this run and the runs before it in this thread \u2014 is kept in a log you can search with the \`recall\` tool (words \u2192 the matching turns with their numbers; a turn number \u2192 that turn whole). When something you need is no longer in front of you, recall it instead of redoing the work or guessing.
2168
2168
  Keep notes with the \`notes\` tool: one short document, replaced whole each time, at most 8 KiB \u2014 decisions and their reasons, the names of things you found (files, tests, commits, the head your tests were green at), what is not yet proven. They are the one thing sure to survive a compaction and to reach the next run in this thread: they ride your system prompt at its start and come back to you right after a compaction. A person reads them too, on the run's page, so write them as a document and never as one paragraph: Markdown, a \`##\` heading per section \u2014 \`Done\`, \`In progress\`, \`Next\`, \`Facts\` (names, ids, heads, the reasons behind decisions), leaving out a section with nothing in it \u2014 one bullet per item, one line per bullet, no prose walls. Write them when you decide something worth keeping, not only at the end.`;
2169
2169
  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.`;
2170
+ TOUCHED_TESTS_COMMAND = "`npx vitest run` on the test files you touched, by name,";
2171
+ 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.`;
2170
2172
  CODING_SYSTEM = `You are Switchboard's coding agent, operating from a Slack request.
2171
2173
 
2172
2174
  You work inside a dedicated workspace directory with bash, read_file, and write_file tools. ${SANDBOX_TOOLCHAIN}
@@ -2189,6 +2191,8 @@ Workflow for shipping a PR:
2189
2191
 
2190
2192
  ${CHECKS_BY_COST}
2191
2193
 
2194
+ ${FAST_GATES_BEFORE_PUSH}
2195
+
2192
2196
  ${NEVER_MERGE}
2193
2197
 
2194
2198
  ${UNIT_CONTRACT}
@@ -2230,6 +2234,8 @@ Workflow for shipping a change:
2230
2234
 
2231
2235
  ${CHECKS_BY_COST}
2232
2236
 
2237
+ ${FAST_GATES_BEFORE_PUSH}
2238
+
2233
2239
  ${NEVER_MERGE}
2234
2240
 
2235
2241
  ${UNIT_CONTRACT}
@@ -2266,6 +2272,8 @@ Workflow for shipping a change:
2266
2272
 
2267
2273
  ${CHECKS_BY_COST}
2268
2274
 
2275
+ ${FAST_GATES_BEFORE_PUSH}
2276
+
2269
2277
  ${NEVER_MERGE}
2270
2278
 
2271
2279
  ${UNIT_CONTRACT}
@@ -2969,6 +2977,19 @@ function verdictLine(verdict) {
2969
2977
  const summary = oneLine2(verdict.summary);
2970
2978
  return summary ? `${token2} ${summary}` : token2;
2971
2979
  }
2980
+ function severityCount(sev, n2) {
2981
+ if (sev === "blocking") return `${n2} blocker${n2 === 1 ? "" : "s"}`;
2982
+ if (sev === "nit") return `${n2} nit${n2 === 1 ? "" : "s"}`;
2983
+ return `${n2} ${sev}`;
2984
+ }
2985
+ function verdictCountsLine(verdict) {
2986
+ if (verdict.verdict === "approve") return "LGTM";
2987
+ const counts = severityCounts(verdict.findings ?? []);
2988
+ return counts ? `Changes requested: ${counts}` : "Changes requested";
2989
+ }
2990
+ function severityCounts(findings) {
2991
+ return FINDING_SEVERITIES.map((sev) => [sev, findings.filter((f) => f.severity === sev).length]).filter(([, n2]) => n2 > 0).map(([sev, n2]) => severityCount(sev, n2)).join(", ");
2992
+ }
2972
2993
  function formatFinding(f) {
2973
2994
  const location = f.line !== void 0 ? `${f.file}:${f.line}` : f.file;
2974
2995
  return `[${f.severity}] ${f.id} ${location} \u2014 ${f.title}${f.humanGated ? " (human-gated)" : ""}`;
@@ -3045,6 +3066,9 @@ ${prose2}
3045
3066
  }
3046
3067
  function buildReviewChannelReply(input) {
3047
3068
  const { answer, verdict, posted, liveUrl } = input;
3069
+ if (!shows(input.verbosity ?? "verbose", "verbose") && verdict !== void 0 && verdict.findings !== void 0 && posted !== void 0) {
3070
+ return `${verdictCountsLine(verdict)} \u2014 https://github.com/${posted.repo}/pull/${posted.number}`;
3071
+ }
3048
3072
  const tail = [
3049
3073
  ...posted && verdict ? [`Posted to ${posted.repo}#${posted.number}`] : [],
3050
3074
  ...liveUrl ? [`[Live run](${liveUrl})`] : []
@@ -3133,6 +3157,7 @@ var init_reviewVerdict = __esm({
3133
3157
  "use strict";
3134
3158
  init_reviewedHead();
3135
3159
  init_redact();
3160
+ init_verbosity();
3136
3161
  FINDING_SEVERITIES = ["blocking", "major", "minor", "nit"];
3137
3162
  ADDRESS_SEVERITIES = FINDING_SEVERITIES;
3138
3163
  DEFAULT_ADDRESS_SEVERITY = "minor";
@@ -3151,10 +3176,20 @@ var init_reviewVerdict = __esm({
3151
3176
  });
3152
3177
 
3153
3178
  // ../../src/core/ship/coordinator.ts
3154
- function shipInterruptedNote(prUrl2) {
3179
+ function interruptionCauseOfWords(words) {
3180
+ if (/\bcontainer (?:was )?replaced\b|\breplac(?:ed|ement) container\b|\bworkspace lost\b|\bcould not be re-attached\b/i.test(
3181
+ words
3182
+ ))
3183
+ return "container_replaced";
3184
+ if (/\bsandbox (?:recycled|worker|exec|runtime)\b/i.test(words)) return "sandbox_fault";
3185
+ if (/\bbot restart(?:ed|s)?\b|\b(?:next|previous|another) generation\b/i.test(words)) return "bot_restart";
3186
+ return void 0;
3187
+ }
3188
+ function shipInterruptedNote(prUrl2, cause) {
3155
3189
  const stands = prUrl2 ? `Its work stands on GitHub: ${prUrl2}.` : "Whatever it pushed stands on its pipeline branch; no PR was opened yet.";
3156
3190
  const reissue = prUrl2 ? `To continue the review loop, re-issue \`agent:ship\` in this thread with only the PR URL (${prUrl2}).` : "To continue, re-issue `agent:ship` in this thread with the task \u2014 round 0 runs again on the same branch.";
3157
- return `\u26A0\uFE0F The bot restarted while this ship pipeline was running, so the pipeline stopped. ${stands} ${reissue}`;
3191
+ const opening = cause === "container_replaced" ? "\u26A0\uFE0F The resident container running this pipeline's child was replaced (a deploy's image swap) and the child could not resume, so the pipeline stopped." : cause === "sandbox_fault" ? "\u26A0\uFE0F The sandbox running this pipeline's child failed and the child could not resume, so the pipeline stopped." : cause === "bot_restart" ? "\u26A0\uFE0F The bot restarted while this ship pipeline was running, so the pipeline stopped." : "\u26A0\uFE0F This ship pipeline's child was interrupted and could not resume, so the pipeline stopped.";
3192
+ return `${opening} ${stands} ${reissue}`;
3158
3193
  }
3159
3194
  function planIdOf(planPath) {
3160
3195
  const base = planPath.split("/").pop() ?? "";
@@ -5247,6 +5282,12 @@ var init_contract2 = __esm({
5247
5282
  });
5248
5283
 
5249
5284
  // ../../src/core/pipelineStanding.ts
5285
+ function endingWordOf(kind) {
5286
+ return ENDING_WORDS[kind] ?? kind;
5287
+ }
5288
+ function roundOutcomeWordOf(outcome) {
5289
+ return ROUND_OUTCOME_WORDS[outcome] ?? outcome;
5290
+ }
5250
5291
  function isShipEvent(e) {
5251
5292
  return e.type === "ship_round" || e.type === "ship_unit";
5252
5293
  }
@@ -5367,7 +5408,7 @@ function isPipelineSummaryShape(v) {
5367
5408
  }
5368
5409
  return true;
5369
5410
  }
5370
- var STAGES, OPEN_STAGES, ROUND_STAGE, ENDING_STAGE, isFinite_;
5411
+ var STAGES, OPEN_STAGES, ROUND_STAGE, ENDING_STAGE, ENDING_WORDS, ROUND_OUTCOME_WORDS, isFinite_;
5371
5412
  var init_pipelineStanding = __esm({
5372
5413
  "../../src/core/pipelineStanding.ts"() {
5373
5414
  "use strict";
@@ -5417,6 +5458,41 @@ var init_pipelineStanding = __esm({
5417
5458
  idle: "idle",
5418
5459
  blocked: "ended"
5419
5460
  };
5461
+ ENDING_WORDS = {
5462
+ merged: "merged",
5463
+ already_landed: "merged",
5464
+ merge_ready: "merge-ready",
5465
+ held: "held",
5466
+ merge_refused: "not merged",
5467
+ round_cap: "round cap reached",
5468
+ wall_clock_cap: "out of budget",
5469
+ review_pending: "review pending",
5470
+ stopped: "stopped",
5471
+ aborted: "aborted",
5472
+ transient: "aborted",
5473
+ continued: "continuing",
5474
+ no_verdict: "no verdict",
5475
+ interrupted: "interrupted",
5476
+ refused: "refused",
5477
+ idle: "idle",
5478
+ blocked: "blocked"
5479
+ };
5480
+ ROUND_OUTCOME_WORDS = {
5481
+ started: "started",
5482
+ pr_opened: "pull request opened",
5483
+ completed: "done",
5484
+ approve: "approved",
5485
+ request_changes: "changes requested",
5486
+ no_verdict: "no verdict",
5487
+ checks_failed: "checks failed",
5488
+ transient: "retried",
5489
+ enqueued: "queued to merge",
5490
+ dequeued: "removed from the merge queue",
5491
+ aborted: "aborted",
5492
+ stopped: "stopped",
5493
+ continued: "continuing",
5494
+ idle: "idle"
5495
+ };
5420
5496
  isFinite_ = (v) => typeof v === "number" && Number.isFinite(v);
5421
5497
  }
5422
5498
  });
@@ -8659,6 +8735,9 @@ function drainSkippedLine(step, tokenEnv, tag = "deploy:all") {
8659
8735
  }
8660
8736
  function drainLiftedLine(step, answer, drain, tag = "deploy:all") {
8661
8737
  const ok2 = !("error" in answer) && answer.status >= 200 && answer.status < 300;
8738
+ const held2 = !("error" in answer) && answer.body.cleared === false && Array.isArray(answer.body.held) ? answer.body.held.filter((h) => typeof h === "string") : [];
8739
+ if (ok2 && held2.length > 0)
8740
+ return `[${tag}] ${step}: fleet stays closed \u2014 ${held2.join(", ")} still report${held2.length === 1 ? "s" : ""} the pre-deploy image; it reopens by itself on the last container's new-image report${drain.until ? ` (backstop ${drain.until})` : ""}`;
8662
8741
  if (drain.drained) {
8663
8742
  if (ok2) return `[${tag}] ${step}: fleet reopened`;
8664
8743
  return `[${tag}] ${step}: fleet NOT reopened (${answerWords(answer)}) \u2014 it reopens by itself${drain.until ? ` at ${drain.until}` : " when the drain ends"}; \`POST /undrain\` with the drain or admin bearer reopens it now`;
@@ -8674,10 +8753,10 @@ function reconcileLine(step, answer, tag = "deploy:all") {
8674
8753
  if (!ok2)
8675
8754
  return `[${tag}] ${step}: the fleet could NOT be reconciled onto the new image (${answerWords(answer)}) \u2014 a stale container restarts on its next quiet attach or refresh instead`;
8676
8755
  const rows = Array.isArray(answer.body.reconciled) ? answer.body.reconciled : [];
8677
- const incomplete = rows.filter((r) => r.result === "deferred" || r.result === "error");
8678
- if (incomplete.length > 0)
8679
- return `[${tag}] ${step}: fleet reconciled onto the new image with exceptions (${reconcileWords(rows)}) \u2014 a deferred or failed resident restarts on its next quiet attach or refresh`;
8680
- return `[${tag}] ${step}: fleet reconciled onto the new image (${reconcileWords(rows) || "no residents"})`;
8756
+ const unverified = rows.filter((r) => r.verified !== true);
8757
+ if (unverified.length > 0)
8758
+ return `[${tag}] ${step}: fleet reconciled, but not every container is verified on the new image yet (${reconcileWords(rows)}) \u2014 the drain holds for the unverified until each reports`;
8759
+ return `[${tag}] ${step}: fleet reconciled and every container verified on the new image (${reconcileWords(rows) || "no residents"})`;
8681
8760
  }
8682
8761
  async function postJson(url, bearer, body) {
8683
8762
  try {
@@ -8707,7 +8786,9 @@ var init_residentDrain = __esm({
8707
8786
  RESIDENT_DRAIN_TOKEN_ENV = "RESIDENT_DRAIN_TOKEN";
8708
8787
  RESIDENT_DRAINED_WAIT_MAX_MS = DRAIN.deployWaitMaxMs;
8709
8788
  DRAIN_MARGIN_MINUTES = DRAIN.marginMinutes;
8710
- reconcileWords = (rows) => rows.map((r) => `${typeof r.resource === "string" ? r.resource : "?"} ${String(r.result ?? "?")}`).join(", ");
8789
+ reconcileWords = (rows) => rows.map(
8790
+ (r) => `${typeof r.resource === "string" ? r.resource : "?"} ${String(r.result ?? "?")}${r.verified === true ? "" : " (unverified)"}`
8791
+ ).join(", ");
8711
8792
  DRAINED_GAVE_UP_SUFFIX = " \u2014 the fleet was drained for the whole wait, so what refused was already in flight when the drain began and outlived the budget; the drain is lifted";
8712
8793
  }
8713
8794
  });
@@ -33514,7 +33595,8 @@ async function deliverAnswer(ctx) {
33514
33595
  answer,
33515
33596
  verdict: ctx.verdict,
33516
33597
  posted: ctx.reviewPost?.posted ? ctx.reviewPost.target : void 0,
33517
- liveUrl
33598
+ liveUrl,
33599
+ ...ctx.verbosity !== void 0 ? { verbosity: ctx.verbosity } : {}
33518
33600
  }) : answer;
33519
33601
  await ending.sealAfterReply(
33520
33602
  () => root.span(
@@ -35847,6 +35929,10 @@ var init_commandChat = __esm({
35847
35929
  });
35848
35930
 
35849
35931
  // ../../src/core/dispatch/handBack.ts
35932
+ function renderHandBackLine(line3) {
35933
+ return `${HAND_BACK_PREFIX}
35934
+ \`${line3}\``;
35935
+ }
35850
35936
  var HAND_BACK_PREFIX;
35851
35937
  var init_handBack = __esm({
35852
35938
  "../../src/core/dispatch/handBack.ts"() {
@@ -40105,24 +40191,11 @@ ${text}` : text;
40105
40191
  if (res.ok && res.followUp) postSettledOutcome(res.followUp, io, root);
40106
40192
  return { kind: "command", command: def.id, outcome: res.ok ? "ok" : "error" };
40107
40193
  }
40108
- async function answerHandBack(branch2, msg, def, input, route3, receipt, confirm, root) {
40109
- const { deps, io, ending, trace } = branch2;
40110
- const answer = async (text, outcome, post) => {
40111
- await recordRoutedDecision(deps, msg, io, def, { ...route3, outcome }, text, ending, trace);
40112
- await ending.sealAfterReply(
40113
- async () => {
40114
- },
40115
- () => root.span("post.reply", post)
40116
- );
40117
- return { kind: "command", command: def.id, outcome };
40118
- };
40119
- const cut = receipt.length > ROUTE_RECEIPT_CAP;
40120
- const handBack = cut ? `${HAND_BACK_PREFIX} ${receipt}
40121
- ${HAND_BACK_CUT_NOTE}` : `${HAND_BACK_PREFIX} ${receipt}`;
40122
- const store = deps.confirmations;
40123
- if (!io.offer || !store) return answer(handBack, "hand_back", () => io.reply(handBack));
40194
+ async function mintConfirmationOffer(args) {
40195
+ const { io, store, msg, def, input, receipt, model } = args;
40196
+ if (!io.offer || !store) return { kind: "no_click" };
40124
40197
  const line3 = chatInvocation(def, input);
40125
- if (redactSecrets(line3) !== line3) return answer(UNSHOWABLE_LINE, "hand_back", () => io.reply(UNSHOWABLE_LINE));
40198
+ if (redactSecrets(line3) !== line3) return { kind: "unshowable" };
40126
40199
  const risk = def.annotations?.risk?.(input) ?? "";
40127
40200
  let row2;
40128
40201
  try {
@@ -40135,7 +40208,7 @@ ${HAND_BACK_CUT_NOTE}` : `${HAND_BACK_PREFIX} ${receipt}`;
40135
40208
  input,
40136
40209
  receipt,
40137
40210
  risk,
40138
- model: route3.model
40211
+ model
40139
40212
  },
40140
40213
  CONFIRMATION_TTL_MS
40141
40214
  );
@@ -40143,13 +40216,43 @@ ${HAND_BACK_CUT_NOTE}` : `${HAND_BACK_PREFIX} ${receipt}`;
40143
40216
  console.warn(
40144
40217
  `[route] ${msg.threadKey} confirmation store unreachable at mint, handing back: ${err2 instanceof Error ? err2.message : String(err2)}`
40145
40218
  );
40219
+ return { kind: "store_unreachable" };
40220
+ }
40221
+ return { kind: "offered", shown: { id: row2.id, line: line3, risk, expiresAt: row2.expiresAt } };
40222
+ }
40223
+ async function answerHandBack(branch2, msg, def, input, route3, receipt, confirm, root) {
40224
+ const { deps, io, ending, trace } = branch2;
40225
+ const answer = async (text, outcome, post) => {
40226
+ await recordRoutedDecision(deps, msg, io, def, { ...route3, outcome }, text, ending, trace);
40227
+ await ending.sealAfterReply(
40228
+ async () => {
40229
+ },
40230
+ () => root.span("post.reply", post)
40231
+ );
40232
+ return { kind: "command", command: def.id, outcome };
40233
+ };
40234
+ const cut = receipt.length > ROUTE_RECEIPT_CAP;
40235
+ const handBack = cut ? `${HAND_BACK_PREFIX} ${receipt}
40236
+ ${HAND_BACK_CUT_NOTE}` : `${HAND_BACK_PREFIX} ${receipt}`;
40237
+ const mint = await mintConfirmationOffer({
40238
+ io,
40239
+ store: deps.confirmations,
40240
+ msg,
40241
+ def,
40242
+ input,
40243
+ receipt,
40244
+ model: route3.model
40245
+ });
40246
+ if (mint.kind === "no_click") return answer(handBack, "hand_back", () => io.reply(handBack));
40247
+ if (mint.kind === "unshowable") return answer(UNSHOWABLE_LINE, "hand_back", () => io.reply(UNSHOWABLE_LINE));
40248
+ if (mint.kind === "store_unreachable") {
40146
40249
  const text = `${handBack}
40147
40250
  ${STORE_UNREACHABLE_NOTE}`;
40148
40251
  return answer(text, "hand_back", () => io.reply(text));
40149
40252
  }
40150
- const shown = { id: row2.id, line: line3, risk, expiresAt: row2.expiresAt };
40253
+ const shown = mint.shown;
40151
40254
  console.log(
40152
- `[route] ${msg.threadKey} offered ${def.id} as confirmation ${row2.id} (confirm asked by ${confirm.scope})`
40255
+ `[route] ${msg.threadKey} offered ${def.id} as confirmation ${shown.id} (confirm asked by ${confirm.scope})`
40153
40256
  );
40154
40257
  return answer(renderOffer(shown), "offered", () => renderConfirmationOffer(io, shown));
40155
40258
  }
@@ -41953,6 +42056,17 @@ var init_seed = __esm({
41953
42056
  });
41954
42057
 
41955
42058
  // ../../src/core/dispatch/operator.ts
42059
+ function ownedProjection(p) {
42060
+ return {
42061
+ presets: [],
42062
+ commands: p.commands.filter((c) => c.id === "steer.run" || c.effect === "read")
42063
+ };
42064
+ }
42065
+ function ownerNote(owner) {
42066
+ const who2 = owner.kind === "live" ? `a live run${owner.runId !== void 0 ? ` (\`${owner.runId}\`)` : ""}` : `the unfinished plan unit ${owner.unit}`;
42067
+ const steer = owner.kind === "live" && owner.runId !== void 0 ? ` bind \`steer run ${owner.runId} <words>\` to deliver it,` : "";
42068
+ return `This thread is owned by ${who2}: the request below is a follow-up for that owner. To act on it,${steer} bind a read command, or ask a question. Any other decision \u2014 a refusal, a preset, a write \u2014 folds the whole message into the owner unchanged and posts no answer.`;
42069
+ }
41956
42070
  function operatorProjection(input) {
41957
42071
  return {
41958
42072
  presets: input.presets.filter((p) => input.allowedPresets.includes(p.name)),
@@ -41960,7 +42074,7 @@ function operatorProjection(input) {
41960
42074
  };
41961
42075
  }
41962
42076
  function buildOperatorPrompt(input) {
41963
- const { projection } = input;
42077
+ const projection = input.owner ? ownedProjection(input.projection) : input.projection;
41964
42078
  const commandList = projection.commands.map((c) => `- \`${c.tool.name}\`: ${oneLine(c.tool.description ?? c.id)}`).join("\n");
41965
42079
  const system = [
41966
42080
  // 1. Rules.
@@ -41979,6 +42093,7 @@ function buildOperatorPrompt(input) {
41979
42093
  // 4. Tail, oldest first.
41980
42094
  ...input.tail.length > 0 ? ["The thread so far, oldest first:", ...input.tail.map((t) => `<turn>${quoteTurn(t.text)}</turn>`), ""] : [],
41981
42095
  ...input.pendingQuestion ? [`A question is pending: ${OPERATOR_QUESTION_MARKER} \`${input.pendingQuestion.proposal}\``, ""] : [],
42096
+ ...input.owner ? [ownerNote(input.owner), ""] : [],
41982
42097
  // 5. Request.
41983
42098
  "<request>",
41984
42099
  quoteRequest(input.text),
@@ -42050,7 +42165,8 @@ function bindViolationOf(line3, ordinal2, guard) {
42050
42165
  const preset = presetBindOf(line3, guard.presets);
42051
42166
  if (preset !== void 0) {
42052
42167
  const words = oneLine(guard.requestText).trim();
42053
- if (words.length > 0 && !oneLine(line3).includes(words))
42168
+ const stripped = oneLine(stripDirectiveHead(guard.requestText, preset)).trim();
42169
+ if (words.length > 0 && !oneLine(line3).includes(words) && !oneLine(line3).includes(stripped))
42054
42170
  return `bind ${ordinal2} names the preset "${preset}" but drops the request's own words; bind the preset on the request verbatim`;
42055
42171
  return void 0;
42056
42172
  }
@@ -42107,6 +42223,17 @@ function parseOperatorDecision(answer, guard) {
42107
42223
  if (typeof text !== "string" || text.trim().length === 0) return refused7("a refusal with no text");
42108
42224
  return { kind: "refusal", cause, text: redactAndCap(text, ROUTE_RECEIPT_CAP), reason: tidy(reason2) };
42109
42225
  }
42226
+ function ownedDecisionRuns(event, owner, commands) {
42227
+ if (event.outcome !== "binds") return false;
42228
+ return (event.binds ?? []).every((bind) => {
42229
+ const parsed = commands ? parseChatCommand(bind.line, commands) : null;
42230
+ if (parsed?.kind !== "invoke") return false;
42231
+ const def = commands.list().find((c) => c.id === parsed.id);
42232
+ if (!def) return false;
42233
+ if (def.id === "steer.run") return !(owner.kind === "live" && owner.runId === void 0);
42234
+ return boundBlastRadius(def, parsed.input) === "read";
42235
+ });
42236
+ }
42110
42237
  function bindFromAnswer(text, pending2) {
42111
42238
  if (!/^yes[.!]?$/i.test(text.trim())) return void 0;
42112
42239
  return { line: operatorLine(pending2.proposal), reason: "yes to the pending question's proposal", confirmed: true };
@@ -42253,6 +42380,7 @@ async function operatorStage(deps, ctx) {
42253
42380
  projection,
42254
42381
  tail,
42255
42382
  ...pending2 ? { pendingQuestion: pending2 } : {},
42383
+ ...ctx.owner ? { owner: ctx.owner } : {},
42256
42384
  ...commands ? { registryParses: (line3) => parseChatCommand(line3, commands)?.kind === "invoke" } : {}
42257
42385
  },
42258
42386
  model
@@ -42273,6 +42401,10 @@ function presetBindOf(line3, presets) {
42273
42401
  const name = head ? head[1] : /^(\S+)/.exec(trimmed)?.[1];
42274
42402
  return name !== void 0 && presets.includes(name) ? name : void 0;
42275
42403
  }
42404
+ function stripDirectiveHead(text, preset) {
42405
+ const m = /^([^\s:]+):(\S+)\s+(\S[\s\S]*)$/.exec(text.trim());
42406
+ return m && m[2] === preset ? m[3].trim() : text;
42407
+ }
42276
42408
  function presetRequestOf(line3) {
42277
42409
  const tail = /^(?:agent:\S+|\S+)\s+(.*\S)\s*$/s.exec(line3.trim());
42278
42410
  return tail ? tail[1] : void 0;
@@ -42319,12 +42451,24 @@ function verifierModelOf(deps) {
42319
42451
  }
42320
42452
  async function executeOperatorDecision(deps, ctx) {
42321
42453
  const { event, io, msg } = ctx;
42454
+ const verbose = shows(
42455
+ deps.config.verbosityFor(msg.channelId, msg.userId, parseDirectives(msg.text).verbosity),
42456
+ "verbose"
42457
+ );
42322
42458
  const answered = { kind: "answered" };
42323
42459
  if (event.outcome === "question") {
42324
42460
  await io.reply(event.question ?? "");
42325
42461
  await recordOperatorDecision(deps, msg, event, ctx.ending, ctx.trace);
42326
42462
  return answered;
42327
42463
  }
42464
+ if (ctx.owner !== void 0 && !ownedDecisionRuns(event, ctx.owner, deps.commands)) {
42465
+ const confirmed = (event.binds ?? []).find((b) => b.confirmed);
42466
+ const request2 = confirmed !== void 0 ? presetBindOf(
42467
+ confirmed.line,
42468
+ routablePresets().map((p) => p.name)
42469
+ ) !== void 0 ? presetRequestOf(confirmed.line) ?? confirmed.line : confirmed.line : void 0;
42470
+ return { kind: "fold", ...request2 !== void 0 ? { request: request2 } : {} };
42471
+ }
42328
42472
  if (event.outcome === "refusal") {
42329
42473
  await io.reply(event.refusalText ?? "");
42330
42474
  await recordOperatorDecision(deps, msg, event, ctx.ending, ctx.trace);
@@ -42346,13 +42490,18 @@ async function executeOperatorDecision(deps, ctx) {
42346
42490
  const def = parsed?.kind === "invoke" ? commands?.list().find((c) => c.id === parsed.id) : void 0;
42347
42491
  const bound = parsed?.kind === "invoke" && def ? { def, radius: boundBlastRadius(def, parsed.input) } : void 0;
42348
42492
  const preset = bound ? void 0 : presetBindOf(bind.line, presetNames);
42349
- const runs = preset !== void 0 ? bind.confirmed ? bind.line : redactSecrets(`agent:${preset} ${msg.text}`) : bind.line;
42493
+ const requestWords = preset !== void 0 && !bind.confirmed ? stripDirectiveHead(msg.text, preset) : void 0;
42494
+ const runs = preset !== void 0 ? bind.confirmed ? bind.line : redactSecrets(`agent:${preset} ${requestWords}`) : bind.line;
42350
42495
  const line3 = preset !== void 0 ? operatorLine(runs) : bind.line;
42351
42496
  let verified;
42352
42497
  if (verifierHolds(bind.line, bound, preset !== void 0)) {
42353
42498
  const model = verifierModelOf(deps);
42354
42499
  const verdict = model ? await verifyOperatorBind(await authorTurnsOnce(), runs, model) : { agrees: false, reason: "no model to verify on" };
42355
42500
  if (!verdict.agrees) {
42501
+ if (bound === void 0 && !bind.confirmed) {
42502
+ for (const rest of binds.slice(i + 1)) await io.reply(renderHandBackLine(rest.line));
42503
+ return { kind: "fallback", reason: `the verifier held the bind: ${verdict.reason}`, carried };
42504
+ }
42356
42505
  await io.reply(renderVerifierHandBack(bind.line, verdict.reason));
42357
42506
  continue;
42358
42507
  }
@@ -42361,17 +42510,16 @@ async function executeOperatorDecision(deps, ctx) {
42361
42510
  if (preset !== void 0) {
42362
42511
  const identity2 = presets.find((p) => p.name === preset)?.identity;
42363
42512
  const radius2 = identity2 === "write" ? "write" : "read";
42364
- await io.reply(`${renderOperatorReceipt(line3, radius2, bind.reason)}${verified ? `
42513
+ if (verbose)
42514
+ await io.reply(`${renderOperatorReceipt(line3, radius2, bind.reason)}${verified ? `
42365
42515
  ${verified}` : ""}`);
42366
- for (const rest of binds.slice(i + 1)) await io.reply(`${HAND_BACK_PREFIX}
42367
- \`${rest.line}\``);
42368
- const request2 = bind.confirmed ? presetRequestOf(bind.line) : void 0;
42516
+ for (const rest of binds.slice(i + 1)) await io.reply(renderHandBackLine(rest.line));
42517
+ const request2 = bind.confirmed ? presetRequestOf(bind.line) : requestWords !== void 0 && requestWords !== msg.text ? requestWords : void 0;
42369
42518
  return { kind: "route", preset, ...request2 !== void 0 ? { request: request2 } : {}, carried };
42370
42519
  }
42371
42520
  if (!parsed || parsed.kind !== "invoke" || !def || !bound) {
42372
- await io.reply(`${verified ? `${verified}
42373
- ` : ""}${HAND_BACK_PREFIX}
42374
- \`${bind.line}\``);
42521
+ await io.reply(`${verbose && verified ? `${verified}
42522
+ ` : ""}${renderHandBackLine(bind.line)}`);
42375
42523
  continue;
42376
42524
  }
42377
42525
  const radius = bound.radius;
@@ -42379,12 +42527,34 @@ ${verified}` : ""}`);
42379
42527
  const receipt = `${renderOperatorReceipt(bind.line, radius, bind.reason)}${verified ? `
42380
42528
  ${verified}` : ""}`;
42381
42529
  if (!runsNow) {
42382
- await io.reply(`${receipt}
42383
- ${HAND_BACK_PREFIX}
42384
- \`${bind.line}\``);
42530
+ const mint = await mintConfirmationOffer({
42531
+ io,
42532
+ store: deps.confirmations,
42533
+ msg,
42534
+ def: bound.def,
42535
+ input: parsed.input,
42536
+ receipt: routeReceipt(bound.def, parsed.input),
42537
+ // The row's model is the decider's, as the routed offer stores the
42538
+ // router's: the operator runs on the ref behind `defaults.models.general`.
42539
+ model: deps.config.config.defaults.models["general"] ?? ""
42540
+ });
42541
+ if (mint.kind === "offered") {
42542
+ if (verbose) await io.reply(receipt);
42543
+ await renderConfirmationOffer(io, mint.shown);
42544
+ continue;
42545
+ }
42546
+ if (mint.kind === "unshowable") {
42547
+ await io.reply(`${verbose ? `${receipt}
42548
+ ` : ""}${UNSHOWABLE_LINE}`);
42549
+ continue;
42550
+ }
42551
+ const note = mint.kind === "store_unreachable" ? `
42552
+ ${STORE_UNREACHABLE_NOTE}` : "";
42553
+ await io.reply(`${verbose ? `${receipt}
42554
+ ` : ""}${renderHandBackLine(bind.line)}${note}`);
42385
42555
  continue;
42386
42556
  }
42387
- await io.reply(receipt);
42557
+ if (verbose) await io.reply(receipt);
42388
42558
  const res = await runChatCommand(deps, msg, io, parsed, ctx.ending, ctx.trace, carried ? {} : { operator: event });
42389
42559
  carried = true;
42390
42560
  if (res.text.length > 0) await io.reply(res.text);
@@ -42397,6 +42567,8 @@ var init_operator = __esm({
42397
42567
  "../../src/core/dispatch/operator.ts"() {
42398
42568
  "use strict";
42399
42569
  init_provider();
42570
+ init_directives();
42571
+ init_verbosity();
42400
42572
  init_redact();
42401
42573
  init_sessionLog();
42402
42574
  init_actor();
@@ -42404,6 +42576,7 @@ var init_operator = __esm({
42404
42576
  init_commandRegistry();
42405
42577
  init_commandChat();
42406
42578
  init_handBack();
42579
+ init_confirmations();
42407
42580
  init_structured();
42408
42581
  init_commandRun();
42409
42582
  init_reply();
@@ -51750,7 +51923,7 @@ async function abandonLostWorkspace(ctx) {
51750
51923
  registry,
51751
51924
  runId: run2.id,
51752
51925
  coordinator,
51753
- kind: "interrupted",
51926
+ kind: restored ? "resumed" : "interrupted",
51754
51927
  reason: summary,
51755
51928
  clock: clock4,
51756
51929
  ...workflow ? { workflow } : {}
@@ -52278,16 +52451,31 @@ async function dispatch(deps, msg, io, opts = {}) {
52278
52451
  let operatorPreset;
52279
52452
  let operatorRequest;
52280
52453
  let operatorThread;
52454
+ let pageOwner;
52281
52455
  if (operatorMode !== "off") {
52282
52456
  const runsService2 = deps.runs ?? createRunsService({ registry, store: deps.runStore });
52283
52457
  operatorThread = opts.thread ?? await readThread(runsService2, msg.threadKey);
52458
+ const slot = admission.get(msg.threadKey);
52459
+ const liveElsewhere = deps.threadsElsewhere.get(msg.threadKey) !== void 0;
52460
+ if (slot === void 0 && !liveElsewhere && operatorThread !== void 0 && deps.coordinatorInstances !== void 0)
52461
+ pageOwner = await ownerOf2(operatorThread, (id) => deps.coordinatorInstances.listUnits(id), msg.threadKey);
52462
+ const threadOwner = slot !== void 0 || liveElsewhere ? { kind: "live", ...slot?.runId !== void 0 ? { runId: slot.runId } : {} } : pageOwner?.kind === "live" ? (
52463
+ // A hosted pipeline runner takes no inbox (thread-admission item
52464
+ // 9's seed rule): a steer offered here would queue words nothing
52465
+ // drains, so the owner rides without a run id — the prompt
52466
+ // offers no steer line, the executor folds a steer bind like any
52467
+ // other, and the fold runs on to the seed refusal below, which
52468
+ // names the unit threads to reply in.
52469
+ pageOwner.run.instanceId !== void 0 ? { kind: "live" } : { kind: "live", runId: pageOwner.run.id }
52470
+ ) : pageOwner?.kind === "unit" ? { kind: "unit", unit: pageOwner.unit.unit } : void 0;
52284
52471
  operatorEvent = await root.span(
52285
52472
  "dispatch.operator",
52286
52473
  () => operatorStage(deps, {
52287
52474
  msg,
52288
52475
  mode: operatorMode,
52289
52476
  ...operatorThread ? { thread: operatorThread } : {},
52290
- ...opts.intake ? { intake: opts.intake } : {}
52477
+ ...opts.intake ? { intake: opts.intake } : {},
52478
+ ...threadOwner ? { owner: threadOwner } : {}
52291
52479
  })
52292
52480
  );
52293
52481
  const live = operatorEvent ? admission.get(msg.threadKey) : void 0;
@@ -52304,13 +52492,19 @@ async function dispatch(deps, msg, io, opts = {}) {
52304
52492
  ending,
52305
52493
  trace,
52306
52494
  event: operatorEvent,
52307
- ...operatorThread ? { thread: operatorThread } : {}
52495
+ ...operatorThread ? { thread: operatorThread } : {},
52496
+ ...threadOwner ? { owner: threadOwner } : {}
52308
52497
  })
52309
52498
  );
52310
52499
  if (execution.kind === "answered") return ended;
52311
- operatorPreset = execution.preset;
52312
- operatorRequest = execution.request;
52313
- if (execution.carried) operatorEvent = void 0;
52500
+ if (execution.kind !== "fold" && execution.carried) operatorEvent = void 0;
52501
+ if (execution.kind === "route") {
52502
+ operatorPreset = execution.preset;
52503
+ operatorRequest = execution.request;
52504
+ } else if (execution.kind === "fallback" && operatorEvent !== void 0) {
52505
+ operatorEvent = { ...operatorEvent, reason: execution.reason };
52506
+ }
52507
+ if (execution.kind === "fold" && execution.request !== void 0) operatorRequest = execution.request;
52314
52508
  }
52315
52509
  }
52316
52510
  const recordPendingOperator = async () => {
@@ -52351,7 +52545,7 @@ async function dispatch(deps, msg, io, opts = {}) {
52351
52545
  let route3;
52352
52546
  const threadLive = admission.get(msg.threadKey) !== void 0 || !resume && !restart && deps.threadsElsewhere.get(msg.threadKey) !== void 0;
52353
52547
  if (thread && !threadLive && deps.coordinatorInstances !== void 0) {
52354
- const owner = await ownerOf2(thread, (id) => deps.coordinatorInstances.listUnits(id), msg.threadKey);
52548
+ const owner = pageOwner ?? await ownerOf2(thread, (id) => deps.coordinatorInstances.listUnits(id), msg.threadKey);
52355
52549
  if (owner.kind === "live" && owner.run.instanceId !== void 0) {
52356
52550
  const units = await deps.coordinatorInstances.listUnits(owner.run.instanceId).catch(() => []);
52357
52551
  const open2 = units.filter((u) => u.ending === void 0);
@@ -53044,6 +53238,7 @@ async function dispatch(deps, msg, io, opts = {}) {
53044
53238
  answer,
53045
53239
  verdict: ran.verdict,
53046
53240
  reviewPost: ran.reviewPost,
53241
+ verbosity: resolved.verbosity,
53047
53242
  liveUrl,
53048
53243
  prNote,
53049
53244
  stopped,
@@ -65892,7 +66087,7 @@ var init_processMetrics = __esm({
65892
66087
 
65893
66088
  // ../../src/core/boot.ts
65894
66089
  function closureNote(agent, prUrl2) {
65895
- if (agent === "ship") return shipInterruptedNote(prUrl2);
66090
+ if (agent === "ship") return shipInterruptedNote(prUrl2, "bot_restart");
65896
66091
  return "The bot restarted while this run was in flight and it could not be resumed, so this card stopped updating. Re-send your request to run it again.";
65897
66092
  }
65898
66093
  function prUrlOf(events) {
@@ -67317,6 +67512,17 @@ async function readRecord(body, deps) {
67317
67512
  const finalReply = finalReplyOf2(record2.events);
67318
67513
  const pr = prOpenedOf(record2.events);
67319
67514
  const instanceRow = await deps.instances.get(id.value);
67515
+ if (view.status === "interrupted") {
67516
+ const successor = await restartedChildOf(deps, id.value, view).catch(() => void 0);
67517
+ if (successor !== void 0)
67518
+ return json3(200, {
67519
+ ok: true,
67520
+ ...instanceRow?.stop !== void 0 ? { stopped: true } : {},
67521
+ run: { id: successor, finished: false },
67522
+ restartedAs: successor,
67523
+ at
67524
+ });
67525
+ }
67320
67526
  let posted;
67321
67527
  if (record2.verdict !== void 0 && record2.reviewHead !== void 0 && typeof body.unit === "string") {
67322
67528
  const instance = instanceRow;
@@ -67352,19 +67558,64 @@ async function readRecord(body, deps) {
67352
67558
  ...record2.handoff !== void 0 ? { handoffLists: record2.handoff } : {},
67353
67559
  // The failure by name (run-history item 57): a `provider_transient` lets
67354
67560
  // the machine re-run a round-0 child that pushed nothing (issue 1932).
67355
- ...record2.failure !== void 0 ? { failure: record2.failure } : {}
67561
+ ...record2.failure !== void 0 ? { failure: record2.failure } : {},
67562
+ // What ended an interrupted child (issue 1876), off its record's own
67563
+ // events: the unit's ending names the actual cause in the user's nouns.
67564
+ ...view.status === "interrupted" ? interruptionOf(record2.events) : {}
67356
67565
  },
67357
67566
  at
67358
67567
  });
67359
67568
  }
67569
+ function interruptionOf(events) {
67570
+ if (events === void 0) return {};
67571
+ for (let i = events.length - 1; i >= 0; i--) {
67572
+ const e = events[i];
67573
+ const words = e.type === "child_interrupted" ? e.reason : e.type === "run_note" && (e.kind === "resumed" || e.kind === "sandbox_restarted" || e.kind === "harness_error") ? e.summary : void 0;
67574
+ if (words === void 0) continue;
67575
+ const cause = interruptionCauseOfWords(words);
67576
+ if (cause !== void 0) return { interruption: cause };
67577
+ }
67578
+ return {};
67579
+ }
67580
+ async function restartedChildOf(deps, instanceId, view) {
67581
+ if (view.threadKey === void 0 || view.idempotencyKey === void 0) return void 0;
67582
+ const listing = await deps.runs.listRuns({
67583
+ status: "all",
67584
+ visibleTo: { kind: "all" },
67585
+ threadKey: view.threadKey,
67586
+ limit: RUN_LIST_MAX_LIMIT
67587
+ });
67588
+ const successor = listing.runs.find(
67589
+ (r) => r.id !== view.id && r.parentInstanceId === instanceId && r.idempotencyKey === view.idempotencyKey && r.startedAt >= view.startedAt
67590
+ );
67591
+ return successor?.id;
67592
+ }
67360
67593
  function isEmptyBranchRefusal(err2) {
67361
67594
  const message4 = err2 instanceof Error ? err2.message : String(err2);
67362
67595
  return /HTTP 422\b/.test(message4) && /no commits between/i.test(message4);
67363
67596
  }
67597
+ function cutAtWordBoundary(line3, max, after) {
67598
+ const tidy2 = (s) => s.replace(/[\s.,;:—–-]+$/u, "");
67599
+ if (line3.length <= max) return tidy2(line3);
67600
+ const head = line3.slice(0, max + 1);
67601
+ const space = head.lastIndexOf(" ");
67602
+ return tidy2(space > after ? head.slice(0, space) : line3.slice(0, max));
67603
+ }
67604
+ function recoveredFallbackTitle(row2, branch2, planId) {
67605
+ const raw = (row2?.title ?? `${row2?.unit ?? "the unit"} \u2014 ${branch2}`).trim();
67606
+ if (checkPrTitle(raw, prTitleVocabulary_default).ok) return raw;
67607
+ const words = raw.split(/\s+/u);
67608
+ const first = (words[0] ?? "").toLowerCase().replace(/:$/u, "");
67609
+ const type = prTitleVocabulary_default.types.includes(first) ? first : "chore";
67610
+ const rest = (type === first ? words.slice(1) : words).join(" ");
67611
+ const area = planId?.split("-").find((seg) => prTitleVocabulary_default.scopes.includes(seg) && !BOT_SCOPES.includes(seg));
67612
+ const prefix = `${type}${area !== void 0 ? `(${area})` : ""}: `;
67613
+ return cutAtWordBoundary(`${prefix}${rest === "" ? branch2 : rest}`, TITLE_MAX_LENGTH, prefix.length);
67614
+ }
67364
67615
  async function recoverPushedBranch(deps, instance, row2, branch2, runId3) {
67365
67616
  if (instance.base === void 0) return { kind: "none", why: "no_base" };
67366
67617
  const unitName = row2?.unit ?? "the unit";
67367
- let title = row2?.title !== void 0 ? `${row2.unit}: ${row2.title}` : `${unitName} \u2014 ${branch2}`;
67618
+ let title = recoveredFallbackTitle(row2, branch2, instance.plan?.id ?? parsePlanBranch(branch2)?.planId);
67368
67619
  let prBody = `Opened by the plan runner from the pushed branch \`${branch2}\`: the coding run ${runId3} of ${unitName} ended before it could open the pull request or submit its description. The review round asks for the description.`;
67369
67620
  try {
67370
67621
  const full = await deps.runs.getRun(runId3, { include: "messages" });
@@ -67682,10 +67933,10 @@ async function branch(body, deps) {
67682
67933
  function unitLines(units, generated, severity) {
67683
67934
  return units.map((u) => {
67684
67935
  const last = u.rounds.at(-1);
67685
- const state = u.ending ? u.ending.kind : u.idle ? (
67686
- // The idle line names the old kind (record 0051): `idle · wall_clock_cap`.
67687
- `idle \xB7 ${u.idle.why}`
67688
- ) : last ? `${shipRoundHeader({ index: last.index, agent: last.agent }, severity)} \xB7 ${last.outcome}${last.gate ? ` \xB7 \u26A0\uFE0F gate fired: ${last.gate.findings.join(", ")} at or above ${last.gate.level}` : ""}` : u.threadKey ? "starting" : "waiting";
67936
+ const state = u.ending ? endingWordOf(u.ending.kind) : u.idle ? (
67937
+ // The idle line names the old kind's word (record 0051): `idle · out of budget`.
67938
+ `idle \xB7 ${endingWordOf(u.idle.why)}`
67939
+ ) : last ? `${shipRoundHeader({ index: last.index, agent: last.agent }, severity)} \xB7 ${roundOutcomeWordOf(last.outcome)}${last.gate ? ` \xB7 \u26A0\uFE0F gate fired: ${last.gate.findings.join(", ")} at or above ${last.gate.level}` : ""}` : u.threadKey ? "starting" : "waiting";
67689
67940
  const seg = u.ending === void 0 && u.segments !== void 0 && u.segments.length > 0 ? `segment ${u.segments[u.segments.length - 1].index} \xB7 ` : "";
67690
67941
  return generated ? `${seg}${state}` : `${u.unit} \xB7 ${seg}${state}`;
67691
67942
  });
@@ -68177,9 +68428,10 @@ async function checksStep(body, deps) {
68177
68428
  }
68178
68429
  function planSummary(units, generated = false) {
68179
68430
  const lines = units.map((u) => {
68180
- const how = u.ending ? u.ending.kind : u.threadKey ? "unfinished" : "not started";
68431
+ const kind = u.ending ? u.ending.kind : u.threadKey ? "unfinished" : "not started";
68432
+ const how = u.ending ? endingWordOf(u.ending.kind) : kind;
68181
68433
  const pr = u.pr ? ` \u2014 ${u.pr.url}` : "";
68182
- return generated ? `${ENDING_ICON[how] ?? "\u2022"} ${how}${pr}` : `${ENDING_ICON[how] ?? "\u2022"} ${u.unit} \u2014 ${how}${pr}`;
68434
+ return generated ? `${ENDING_ICON[kind] ?? "\u2022"} ${how}${pr}` : `${ENDING_ICON[kind] ?? "\u2022"} ${u.unit} \u2014 ${how}${pr}`;
68183
68435
  });
68184
68436
  return lines.join("\n");
68185
68437
  }
@@ -68384,8 +68636,11 @@ var init_adminCoordinator = __esm({
68384
68636
  init_runFriction();
68385
68637
  init_runRecord();
68386
68638
  init_coordinator();
68639
+ init_prTitle();
68640
+ init_prTitleVocabulary();
68387
68641
  init_handoff();
68388
68642
  init_reviewedHead();
68643
+ init_pipelineStanding();
68389
68644
  init_shipPipeline();
68390
68645
  init_statusCardFrame();
68391
68646
  init_clock();