@coreplane/switchboard 1.243.0 → 1.244.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 (133) hide show
  1. package/dist/assets/deploy/cloudflare-memory/worker.ts +148 -0
  2. package/dist/assets/deploy/cloudflare-resident/gc.ts +8 -1
  3. package/dist/assets/deploy/cloudflare-resident/worker.ts +433 -117
  4. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +13 -2
  5. package/dist/assets/package-lock.json +3 -3
  6. package/dist/assets/package.json +1 -1
  7. package/dist/assets/source.json +3 -3
  8. package/dist/assets/src/agents/registry.ts +1 -1
  9. package/dist/assets/src/core/authz/types.ts +10 -0
  10. package/dist/assets/src/core/budgets.ts +36 -0
  11. package/dist/assets/src/core/coordinator/contract.ts +32 -0
  12. package/dist/assets/src/core/coordinator/driver.ts +75 -12
  13. package/dist/assets/src/core/delivery.ts +1 -1
  14. package/dist/assets/src/core/runEvents.ts +50 -7
  15. package/dist/assets/src/core/runFriction.ts +2 -1
  16. package/dist/assets/src/core/runRecord.ts +31 -0
  17. package/dist/assets/src/core/ship/contract.ts +2 -1
  18. package/dist/assets/src/core/ship/coordinator.ts +211 -13
  19. package/dist/assets/src/core/ship/renewal.ts +155 -0
  20. package/dist/assets/src/core/trace/attrs.ts +11 -0
  21. package/dist/assets/src/core/trace/types.ts +4 -3
  22. package/dist/assets/src/execution/residentRefresh.ts +24 -4
  23. package/dist/assets/src/execution/residentText.ts +8 -3
  24. package/dist/assets/src/mcp/registry.ts +3 -0
  25. package/dist/assets/web/dist/.vite/manifest.json +417 -417
  26. package/dist/assets/web/dist/assets/{AppShell-IDMGG6yi.js → AppShell-Ck4hSZDi.js} +1 -1
  27. package/dist/assets/web/dist/assets/CostsPage-DvhjDMka.js +2 -0
  28. package/dist/assets/web/dist/assets/DeliveryPage-DBlI_Yq2.js +1 -0
  29. package/dist/assets/web/dist/assets/HomePage-XFWPxDiJ.js +2 -0
  30. package/dist/assets/web/dist/assets/{NotFoundPage-C97EhJcQ.js → NotFoundPage-CiGrVOyv.js} +1 -1
  31. package/dist/assets/web/dist/assets/{PendingTurnRow-BT9RhFZ7.js → PendingTurnRow-BHCSpsPf.js} +1 -1
  32. package/dist/assets/web/dist/assets/{ResidentDetailPage-DACalNVF.js → ResidentDetailPage-C5USV_8P.js} +1 -1
  33. package/dist/assets/web/dist/assets/{ResidentsIndexPage-SHPdu6uZ.js → ResidentsIndexPage-CSKisWnX.js} +1 -1
  34. package/dist/assets/web/dist/assets/RunFoldRow-BPMQxTcl.js +1 -0
  35. package/dist/assets/web/dist/assets/RunRoutePage-CZe9Yodw.js +9 -0
  36. package/dist/assets/web/dist/assets/RunsIndexPage-BfA333Qy.js +1 -0
  37. package/dist/assets/web/dist/assets/{RunsTabs-BUfdk0lH.js → RunsTabs-BWRHzhtT.js} +1 -1
  38. package/dist/assets/web/dist/assets/{ScheduledPage-DJ8HiCPt.js → ScheduledPage-DBaChcJZ.js} +1 -1
  39. package/dist/assets/web/dist/assets/SettingsPage-CqVi9-Ac.js +1 -0
  40. package/dist/assets/web/dist/assets/{StatusDot-Dw0T1M-P.js → StatusDot-_Q_bmReh.js} +1 -1
  41. package/dist/assets/web/dist/assets/{Tooltip-BbLuIAiS.js → Tooltip-Cafsgd4m.js} +1 -1
  42. package/dist/assets/web/dist/assets/{UnitRoutePage-DicUG96U.js → UnitRoutePage-CdoBZwtf.js} +1 -1
  43. package/dist/assets/web/dist/assets/{angular-html-oBNfPJR0.js → angular-html-Dail07rr.js} +1 -1
  44. package/dist/assets/web/dist/assets/{angular-ts-BvNwsyWA.js → angular-ts-BReExio8.js} +1 -1
  45. package/dist/assets/web/dist/assets/{apl-CNUdRlYf.js → apl-DhNO6Yyx.js} +1 -1
  46. package/dist/assets/web/dist/assets/{astro-Zb0NriSe.js → astro-BYotTY4C.js} +1 -1
  47. package/dist/assets/web/dist/assets/{blade-qPRVheqq.js → blade-mUxTtD1X.js} +1 -1
  48. package/dist/assets/web/dist/assets/{c-D8Awx4YO.js → c-DhuiwcSQ.js} +1 -1
  49. package/dist/assets/web/dist/assets/{chapel-Bt72Mhsx.js → chapel-GD2vFtRi.js} +1 -1
  50. package/dist/assets/web/dist/assets/{cobol-BOBacexg.js → cobol-BhFDwkes.js} +1 -1
  51. package/dist/assets/web/dist/assets/{coffee-E4u0liHW.js → coffee-DWUgbE0r.js} +1 -1
  52. package/dist/assets/web/dist/assets/{cpp-q2sLNlul.js → cpp-CO9XFjGW.js} +1 -1
  53. package/dist/assets/web/dist/assets/{crystal-DyWqUnlb.js → crystal-B9pVurpn.js} +1 -1
  54. package/dist/assets/web/dist/assets/{css-CQY0hFsD.js → css-CFVVV6lI.js} +1 -1
  55. package/dist/assets/web/dist/assets/{dist-twkFmSUY.js → dist-BtPs2AkQ.js} +2 -2
  56. package/dist/assets/web/dist/assets/{edge-C1MwhJkX.js → edge-BDhIDQIb.js} +1 -1
  57. package/dist/assets/web/dist/assets/{elixir-Bb3YbHfn.js → elixir-DG3J8WoD.js} +1 -1
  58. package/dist/assets/web/dist/assets/{elm-DAN9IGQw.js → elm-FiOdiaOE.js} +1 -1
  59. package/dist/assets/web/dist/assets/{erb-BEB8Xlsj.js → erb-vxbtW503.js} +1 -1
  60. package/dist/assets/web/dist/assets/{git-rebase-tqpjRfxO.js → git-rebase-DINz8M3H.js} +1 -1
  61. package/dist/assets/web/dist/assets/{glimmer-js-Ccbo65zR.js → glimmer-js-yann6r9i.js} +1 -1
  62. package/dist/assets/web/dist/assets/{glimmer-ts-B6WBVMpE.js → glimmer-ts-CXTbrZn_.js} +1 -1
  63. package/dist/assets/web/dist/assets/{glsl-tRec3Fcu.js → glsl-DNYC-hq0.js} +1 -1
  64. package/dist/assets/web/dist/assets/{graphql-P8kbxT4F.js → graphql-CwzhHzi9.js} +1 -1
  65. package/dist/assets/web/dist/assets/{hack-H9Zhkagy.js → hack-BBXF1cwR.js} +1 -1
  66. package/dist/assets/web/dist/assets/{haml-DtEnpn7Z.js → haml-oEqBcsTy.js} +1 -1
  67. package/dist/assets/web/dist/assets/{handlebars-DkgPfoAz.js → handlebars-YE5thkT1.js} +1 -1
  68. package/dist/assets/web/dist/assets/{html-D30RXpIs.js → html-Dp4jRQ1j.js} +1 -1
  69. package/dist/assets/web/dist/assets/{html-derivative-DwozLrEx.js → html-derivative-DCT1GYtQ.js} +1 -1
  70. package/dist/assets/web/dist/assets/{http-DXuzBAPm.js → http-BPcGbmzJ.js} +1 -1
  71. package/dist/assets/web/dist/assets/{hurl-d1UUJIt_.js → hurl-CQnNHwyd.js} +1 -1
  72. package/dist/assets/web/dist/assets/{indexRow-B_s5tKyq.js → indexRow-BmK74Vp1.js} +1 -1
  73. package/dist/assets/web/dist/assets/{java-DL0gWf34.js → java-Dm8Ovj6W.js} +1 -1
  74. package/dist/assets/web/dist/assets/{javascript-Cl7vavnS.js → javascript-Btm9oJK9.js} +1 -1
  75. package/dist/assets/web/dist/assets/{jinja-CfQOWMX9.js → jinja-VZYjWF_U.js} +1 -1
  76. package/dist/assets/web/dist/assets/{jison-KdYirlqm.js → jison-Dcmjxa9x.js} +1 -1
  77. package/dist/assets/web/dist/assets/{json-C9cDQ-Qj.js → json-DoWiOGZ8.js} +1 -1
  78. package/dist/assets/web/dist/assets/{jsx-CRx5NItd.js → jsx-Dvmo55ma.js} +1 -1
  79. package/dist/assets/web/dist/assets/{julia-CmsQsZQl.js → julia-d8AtfV26.js} +1 -1
  80. package/dist/assets/web/dist/assets/{just-CsM3Q8TE.js → just-Cc5EStA-.js} +1 -1
  81. package/dist/assets/web/dist/assets/{latex-BXCh5YRX.js → latex-B5fKKIH7.js} +1 -1
  82. package/dist/assets/web/dist/assets/{liquid-BF2vwK8p.js → liquid-DUYI9JND.js} +1 -1
  83. package/dist/assets/web/dist/assets/{lua-DcMBATrl.js → lua-BhMwKWpw.js} +1 -1
  84. package/dist/assets/web/dist/assets/main-C4366PZF.css +1 -0
  85. package/dist/assets/web/dist/assets/main-DlTNAC6c.js +28 -0
  86. package/dist/assets/web/dist/assets/{marko-88MndKvG.js → marko-Cnf2cPAi.js} +1 -1
  87. package/dist/assets/web/dist/assets/{mdc-DJ4kVAd8.js → mdc-FDDXoir9.js} +1 -1
  88. package/dist/assets/web/dist/assets/{nginx-CP6mRgtV.js → nginx-q-8U_1jR.js} +1 -1
  89. package/dist/assets/web/dist/assets/{nim-QQfj3fpF.js → nim-DIA0TUWs.js} +1 -1
  90. package/dist/assets/web/dist/assets/{org-Bke3eBzc.js → org-CR88hy7S.js} +1 -1
  91. package/dist/assets/web/dist/assets/{perl-Bkh0N7Kz.js → perl-Bkd8N7ja.js} +1 -1
  92. package/dist/assets/web/dist/assets/{php-Dnv1Piya.js → php-BOs84mvy.js} +1 -1
  93. package/dist/assets/web/dist/assets/{pug-D6peFFI7.js → pug-BbpZBb3w.js} +1 -1
  94. package/dist/assets/web/dist/assets/{qml-D8PEs-C-.js → qml-D2jQpjFE.js} +1 -1
  95. package/dist/assets/web/dist/assets/{r-B1EL9b_j.js → r-DKzcAQ22.js} +1 -1
  96. package/dist/assets/web/dist/assets/{razor-BsMTIh2b.js → razor-SOQMWcC9.js} +1 -1
  97. package/dist/assets/web/dist/assets/{regexp-HhvC8spD.js → regexp-B6KK4W8d.js} +1 -1
  98. package/dist/assets/web/dist/assets/{rst-D5paAxpg.js → rst-kPSg-NPf.js} +1 -1
  99. package/dist/assets/web/dist/assets/{ruby-BrQwhLrl.js → ruby-cKS-JYLH.js} +1 -1
  100. package/dist/assets/web/dist/assets/{sas-afot2B1o.js → sas-DNxzE5gs.js} +1 -1
  101. package/dist/assets/web/dist/assets/{scss-Efm-mwuG.js → scss-DGcHQ5OO.js} +1 -1
  102. package/dist/assets/web/dist/assets/{shellscript-BK0Vv5fT.js → shellscript-CRZeZBa5.js} +1 -1
  103. package/dist/assets/web/dist/assets/{shellsession-BGqlMC7N.js → shellsession-DSNEL0HX.js} +1 -1
  104. package/dist/assets/web/dist/assets/{soy-M0b4UwGM.js → soy-zisNbkDO.js} +1 -1
  105. package/dist/assets/web/dist/assets/{sql-sxe6IE9j.js → sql-MG06KMMb.js} +1 -1
  106. package/dist/assets/web/dist/assets/{sseReplay-C9m_EB8J.js → sseReplay-DaRtc7nI.js} +4 -4
  107. package/dist/assets/web/dist/assets/{stata-nPF_ddLP.js → stata-CB_wKwv9.js} +1 -1
  108. package/dist/assets/web/dist/assets/{surrealql-D_GrC6u7.js → surrealql-DYdSt7re.js} +1 -1
  109. package/dist/assets/web/dist/assets/{svelte-DwL1AtNP.js → svelte-DHO-F7c7.js} +1 -1
  110. package/dist/assets/web/dist/assets/{templ-7s7LTDkc.js → templ-CUL5L4Lr.js} +1 -1
  111. package/dist/assets/web/dist/assets/{tex-Bx-5fMxe.js → tex-_JNEKjVW.js} +1 -1
  112. package/dist/assets/web/dist/assets/{ts-tags-DUMJnke_.js → ts-tags--9EpEV_8.js} +1 -1
  113. package/dist/assets/web/dist/assets/{tsx-BN8biPDe.js → tsx-BcNq2x2u.js} +1 -1
  114. package/dist/assets/web/dist/assets/{twig-8nIu84TN.js → twig-EsY5Dk_D.js} +1 -1
  115. package/dist/assets/web/dist/assets/{typescript-BooSPq_S.js → typescript-CyA_HiUH.js} +1 -1
  116. package/dist/assets/web/dist/assets/{typst-CTBiBsem.js → typst-SzaHlDt4.js} +1 -1
  117. package/dist/assets/web/dist/assets/{vue-C6Ft4Lea.js → vue-D3cn5acV.js} +1 -1
  118. package/dist/assets/web/dist/assets/{vue-html-Ba36dD5D.js → vue-html-6MxhUxOa.js} +1 -1
  119. package/dist/assets/web/dist/assets/{vue-vine-NkFexVo2.js → vue-vine-qJnqhgo9.js} +1 -1
  120. package/dist/assets/web/dist/assets/{xml-C_THnHXZ.js → xml-DyRpC7xv.js} +1 -1
  121. package/dist/assets/web/dist/assets/{xsl-D8G5xqjY.js → xsl-BXMRPAsg.js} +1 -1
  122. package/dist/assets/web/dist/assets/{yaml-jAMIJzge.js → yaml-BNmPCtAN.js} +1 -1
  123. package/dist/cli.js +2990 -706
  124. package/package.json +1 -1
  125. package/dist/assets/web/dist/assets/CostsPage-CwXOmkeQ.js +0 -2
  126. package/dist/assets/web/dist/assets/DeliveryPage-n1tz5I_v.js +0 -1
  127. package/dist/assets/web/dist/assets/HomePage-PRxjQiGG.js +0 -2
  128. package/dist/assets/web/dist/assets/RunFoldRow-0SdOmOr5.js +0 -1
  129. package/dist/assets/web/dist/assets/RunRoutePage-BaFS2p8I.js +0 -9
  130. package/dist/assets/web/dist/assets/RunsIndexPage-DYI-iALj.js +0 -1
  131. package/dist/assets/web/dist/assets/SettingsPage-DLiN5IgY.js +0 -1
  132. package/dist/assets/web/dist/assets/main-B4kEF3Sg.css +0 -1
  133. package/dist/assets/web/dist/assets/main-d-w-tIKt.js +0 -28
@@ -588,9 +588,20 @@ export class SwitchboardSandbox extends Sandbox<Env> {
588
588
  await Promise.race([settled, new Promise<void>((r) => setTimeout(r, maxMs))]);
589
589
  }
590
590
 
591
- /** `du -sk` over the paths, summed, in KiB; null when nothing could be measured. */
591
+ /** `du -sk` over the paths, summed, in KiB; null when nothing could be
592
+ * measured — including a `du` that did not answer inside its own limit
593
+ * (a mount mid-restore under load): the judge treats a sample the
594
+ * container could not take as no evidence either way, never as the
595
+ * restore's failure. The restore's own promise still reports a runtime
596
+ * that went away. */
592
597
  private async duKiB(paths: string[]): Promise<number | null> {
593
- const r = await this.runRoot(["du", "-sk", ...paths], 30_000);
598
+ let r: { stdout: string; exitCode: number };
599
+ try {
600
+ r = await this.runRoot(["du", "-sk", ...paths], 30_000);
601
+ } catch {
602
+ return null;
603
+ }
604
+ if (r.exitCode === 124) return null;
594
605
  let total = 0;
595
606
  let seen = false;
596
607
  for (const line of r.stdout.split("\n")) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.243.0",
3
+ "version": "1.244.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.243.0",
9
+ "version": "1.244.0",
10
10
  "license": "Apache-2.0",
11
11
  "workspaces": [
12
12
  "web",
@@ -20445,7 +20445,7 @@
20445
20445
  },
20446
20446
  "packages/switchboard": {
20447
20447
  "name": "@coreplane/switchboard",
20448
- "version": "1.243.0",
20448
+ "version": "1.244.0",
20449
20449
  "license": "Apache-2.0",
20450
20450
  "dependencies": {
20451
20451
  "@earendil-works/pi-ai": "0.85.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.243.0",
3
+ "version": "1.244.0",
4
4
  "private": true,
5
5
  "description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
6
6
  "license": "Apache-2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.243.0",
3
- "commit": "abfb780fe3a276f303d13d597ad7c549ba8f49c0",
4
- "builtAt": "2026-09-17T07:58:14.271Z"
2
+ "version": "1.244.0",
3
+ "commit": "de7fddf53659568639ad877e935a8b9c0177debd",
4
+ "builtAt": "2026-09-17T15:03:14.350Z"
5
5
  }
@@ -225,7 +225,7 @@ Keep notes with the \`notes\` tool: one short document, replaced whole each time
225
225
  // project's most expensive checks first; the rule is the runner's to hold, not
226
226
  // a line every requester remembers to paste. Stack-agnostic on purpose — the
227
227
  // classes are by duration, the project's own scripts and CI say which is which.
228
- export const CHECKS_BY_COST = `CHECKS BY COST — 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 — 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: a documentation change gets the documentation checks, one module gets its own tests, a shared type gets the typecheck. As soon as the change exists and those checks pass, commit and push — the pushed branch is the deliverable, and an unpushed tree does not survive the run's end. Only then run the expensive checks, once, and fix 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. The description's validation names exactly what ran; what did not run is CI's to gate, and you say so.`;
228
+ export const CHECKS_BY_COST = `CHECKS BY COST — 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 — 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: a documentation change gets the documentation checks, one module gets its own tests, a shared type gets the typecheck. As soon as the change exists and those checks pass, commit and push — the pushed branch is the deliverable, and an unpushed tree does not survive the run's end. Only then run the expensive checks, once, and fix 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. The description's validation names exactly what ran; what did not run is CI's to gate, and you say so.`;
229
229
 
230
230
  const CODING_SYSTEM = `You are Switchboard's coding agent, operating from a Slack request.
231
231
 
@@ -196,4 +196,14 @@ export interface ChannelDirectory {
196
196
  /** Every channel the actor is in, platform-namespaced — what the resolver puts on
197
197
  * `Actor.memberOf`; `unknown` when the adapter cannot say (fail-closed). */
198
198
  channelsOf(actorId: string): Promise<ReadonlySet<string> | "unknown">;
199
+ /** Every channel the bot itself is in, with its visibility — the channels a person can
200
+ * pick a scope or an MCP tier for (settings-page.md item 7); `unknown` when the adapter
201
+ * cannot list them, and the surfaces fall back to the channels that already carry a scope. */
202
+ channels(): Promise<readonly ListedChannel[] | "unknown">;
203
+ }
204
+
205
+ /** One channel the bot is in, as the directory lists it. */
206
+ export interface ListedChannel {
207
+ id: string;
208
+ visibility: ChannelVisibility;
199
209
  }
@@ -18,6 +18,18 @@
18
18
 
19
19
  export const MINUTE_MS = 60_000;
20
20
 
21
+ /** One calendar day in milliseconds: the unit the daily cost and delivery ranges
22
+ * step by (`dayOf`, the day count of a range). A day is not a lease, but it is
23
+ * a duration, and every duration is read from this table rather than written
24
+ * as a literal where it is used. */
25
+ export const DAY_MS = 24 * 60 * MINUTE_MS;
26
+
27
+ /** How long the confirmation a routed write is offered as stays pending
28
+ * (docs/decisions/0044; docs/reference/specs/routing-and-config.md item 25):
29
+ * the connect ticket's ten minutes. The bot passes it to the config object,
30
+ * which stamps the expiry on its own clock. */
31
+ export const CONFIRMATION_TTL_MS = 10 * MINUTE_MS;
32
+
21
33
  /** The presets that run the tool loop, and the one pipeline preset. */
22
34
  export const LOOP_PRESETS = ["general", "coding", "review", "research", "explore", "conductor"] as const;
23
35
  export type LoopPreset = (typeof LOOP_PRESETS)[number];
@@ -311,3 +323,27 @@ export function fit(pipeline: Pipeline): { ok: boolean; need: number; have: numb
311
323
  const need = ALLOWANCES.provision + ASKS.coding + reserveMinutes({ kind: "coding", index: 0 }, pipeline);
312
324
  return { ok: pipeline.maxMinutes >= need, need, have: pipeline.maxMinutes };
313
325
  }
326
+
327
+ // ---- the grant (decision 0046, Renewal: the grant decides, the lease continues) ----
328
+
329
+ /** What the request authorizes for the whole problem beyond one lease: a count
330
+ * of renewals and a cost cap in dollars. A lease is sized by the infrastructure
331
+ * that must outlive it; the grant is sized by the person — a scope's word or
332
+ * the request's `renewals:` directive — and the runner spends it one segment at
333
+ * a time. Zero renewals and no cap by default, so nothing renews until someone
334
+ * says so; `costCapUsd` absent is no cap, never zero. */
335
+ export interface Grant {
336
+ renewals: number;
337
+ costCapUsd?: number;
338
+ }
339
+
340
+ /** Which layer's word the grant is: the request's directive, the user's scope,
341
+ * the channel's, or the org's `ship.grant` — the default counts as the org's. */
342
+ export type GrantSource = "org" | "channel" | "user" | "run";
343
+
344
+ export const DEFAULT_GRANT: Grant = { renewals: 0 };
345
+
346
+ /** The most renewals one request may carry: thirteen segments of the ship
347
+ * preset's ask are a day, the longest problem a person hands over in one
348
+ * message before a plan should carry it. */
349
+ export const GRANT_RENEWALS_MAX = 12;
@@ -8,6 +8,7 @@
8
8
  // parent record the spawn route reads the requester from, and the names the
9
9
  // routes decide on.
10
10
  //
11
+ import type { Grant, GrantSource } from "../budgets.js";
11
12
  import type { AddressSeverity, AddressSeveritySource } from "../ship/coordinator.js";
12
13
 
13
14
  // A coordinator is a Workflow instance in the shim Worker whose children are
@@ -181,6 +182,13 @@ export interface CoordinatorInstance {
181
182
  * the machine reads one value; absent reads as the default (`minor`, org). */
182
183
  addressSeverity?: AddressSeverity;
183
184
  addressSeveritySource?: AddressSeveritySource;
185
+ /** The grant (decision 0046, the renewable lease): the renewals and cost cap
186
+ * the request carries, resolved once by the ship fork (directive count >
187
+ * user > channel > org) and written here beside `merge`; absent reads as
188
+ * zero renewals and no cap, the org's. Nothing renews until the renewal
189
+ * decision reads it. */
190
+ grant?: Grant;
191
+ grantSource?: GrantSource;
184
192
  /** The pipeline's caps as the profile gate clipped them: the rounds cap and the wall clock per unit. */
185
193
  caps?: { maxRounds: number; maxMinutes: number };
186
194
  /** The status card in the requesting thread, when the channel has one — what
@@ -195,6 +203,13 @@ export interface CoordinatorInstance {
195
203
  attempt?: number;
196
204
  }
197
205
 
206
+ export interface UnitSegment {
207
+ index: number;
208
+ from?: string;
209
+ runId?: string;
210
+ at: number;
211
+ }
212
+
198
213
  /** One unit of the plan an instance runs (a task string is a generated plan of
199
214
  * one unit, `U1`): its branch, the units it waits on, and — as the runner
200
215
  * reaches it — its thread, its pull request, the round boundaries the card
@@ -229,6 +244,13 @@ export interface CoordinatorUnit {
229
244
  * opened or updated): the next attempt's pre-check starts at the review
230
245
  * round when the open pull request still heads exactly here. */
231
246
  lastPush?: string;
247
+ /** The renewals the unit spent (decision 0046, Renewal): one row per segment
248
+ * the grant opened after the first, keyed by the segment's index — written
249
+ * by `unit-end` on a `continued` ending before the segment runs, so a runner
250
+ * reclaimed between a segment's end and its renewal finds the row and never
251
+ * renews the same segment twice. `from` is the sha the segment continues
252
+ * from, `runId` the coding run whose write-up briefs it. */
253
+ segments?: UnitSegment[];
232
254
  /** The round boundaries the coordinator reported, oldest first (the `ship_round` vocabulary). */
233
255
  rounds: Array<{ index: number; agent: string; outcome: string; at: number }>;
234
256
  /** How the unit ended: the ending's kind and the thread's report, when it has. */
@@ -281,6 +303,15 @@ export function isCoordinatorInstance(v: unknown): v is CoordinatorInstance {
281
303
  }
282
304
 
283
305
  /** Structural check on a unit row from outside the process. */
306
+ const isSegment = (v: unknown): boolean =>
307
+ isObject(v) &&
308
+ typeof v.index === "number" &&
309
+ Number.isInteger(v.index) &&
310
+ v.index >= 2 &&
311
+ (v.from === undefined || isText(v.from)) &&
312
+ (v.runId === undefined || isText(v.runId)) &&
313
+ typeof v.at === "number";
314
+
284
315
  export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
285
316
  if (!isObject(v)) return false;
286
317
  const r = v;
@@ -293,6 +324,7 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
293
324
  if (r.pr !== undefined && !isPr(r.pr)) return false;
294
325
  if (r.resume !== undefined && !isResume(r.resume)) return false;
295
326
  if (r.lastPush !== undefined && !isText(r.lastPush)) return false;
327
+ if (r.segments !== undefined && (!Array.isArray(r.segments) || !r.segments.every(isSegment))) return false;
296
328
  if (
297
329
  !Array.isArray(r.rounds) ||
298
330
  r.rounds.length > MAX_ROUNDS ||
@@ -33,6 +33,7 @@
33
33
  // plan's re-issue. A task string's ship branch waits for a person. Node-free:
34
34
  // the shim Worker imports this by relative path.
35
35
 
36
+ import { DEFAULT_GRANT, GRANT_RENEWALS_MAX, type Grant, type GrantSource } from "../budgets.js";
36
37
  import {
37
38
  applyReturn,
38
39
  cursorFinished,
@@ -55,6 +56,8 @@ import {
55
56
  type StepReturn,
56
57
  type UnitEnding,
57
58
  type UnitPipelineState,
59
+ stepPrefixOf,
60
+ type UnitSession,
58
61
  } from "../ship/coordinator.js";
59
62
  import { checksSettledEventType, isCoordinatorUnit, runFinishedEventType, type CoordinatorUnit } from "./contract.js";
60
63
 
@@ -164,6 +167,9 @@ interface PlanFacts {
164
167
  /** The severity to address, beside `merge`: the level an approve's findings are held to, and which layer set it. */
165
168
  addressSeverity: AddressSeverity;
166
169
  addressSeveritySource: AddressSeveritySource;
170
+ /** The grant beside them (decision 0046): what a renewal could spend, and which layer granted it. */
171
+ grant: Grant;
172
+ grantSource: GrantSource;
167
173
  /** The instance's mark as the plan route answers it: a generated one-unit plan (a `plan` with no `path`). */
168
174
  generated: boolean;
169
175
  repo: string;
@@ -176,6 +182,21 @@ const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === "obj
176
182
  const isMinutes = (v: unknown): v is Record<string, number> =>
177
183
  isRecord(v) && Object.values(v).every((n) => typeof n === "number" && Number.isFinite(n));
178
184
 
185
+ /** The grant as the plan route answers it; anything unreadable — a count
186
+ * outside the module's ceiling included — is the default: nothing renews on a guess. */
187
+ function readGrant(raw: unknown): Grant {
188
+ if (
189
+ !isRecord(raw) ||
190
+ typeof raw.renewals !== "number" ||
191
+ !Number.isInteger(raw.renewals) ||
192
+ raw.renewals < 0 ||
193
+ raw.renewals > GRANT_RENEWALS_MAX
194
+ )
195
+ return DEFAULT_GRANT;
196
+ const cap = raw.costCapUsd;
197
+ return { renewals: raw.renewals, ...(typeof cap === "number" && cap > 0 ? { costCapUsd: cap } : {}) };
198
+ }
199
+
179
200
  function readPlan(a: BotAnswer): PlanFacts {
180
201
  const b = a.body;
181
202
  if (b.ok !== true) throw new UnreadableAnswer("plan", a, "not ok");
@@ -192,6 +213,9 @@ function readPlan(a: BotAnswer): PlanFacts {
192
213
  b.addressSeveritySource === "run" || b.addressSeveritySource === "user" || b.addressSeveritySource === "channel"
193
214
  ? b.addressSeveritySource
194
215
  : "org",
216
+ grant: readGrant(b.grant),
217
+ grantSource:
218
+ b.grantSource === "run" || b.grantSource === "user" || b.grantSource === "channel" ? b.grantSource : "org",
195
219
  generated: b.generated === true,
196
220
  repo: b.repo,
197
221
  base: b.base,
@@ -252,6 +276,10 @@ function readRecordReturn(step: string, a: BotAnswer): StepReturn {
252
276
  reviewHead,
253
277
  dispositions,
254
278
  handoff,
279
+ pushed,
280
+ leaseStartedAt,
281
+ costUsd,
282
+ handoffLists,
255
283
  } = facts;
256
284
  return {
257
285
  type: "read-record",
@@ -269,6 +297,11 @@ function readRecordReturn(step: string, a: BotAnswer): StepReturn {
269
297
  ...(reviewHead !== undefined ? { reviewHead } : {}),
270
298
  ...(dispositions !== undefined ? { dispositions } : {}),
271
299
  ...(handoff !== undefined ? { handoff } : {}),
300
+ // The renewal's facts (decision 0046), as the record carries them.
301
+ ...(Array.isArray(pushed) ? { pushed } : {}),
302
+ ...(typeof leaseStartedAt === "number" ? { leaseStartedAt } : {}),
303
+ ...(typeof costUsd === "number" || costUsd === null ? { costUsd } : {}),
304
+ ...(handoffLists !== undefined ? { handoffLists } : {}),
272
305
  },
273
306
  at: a.body.at,
274
307
  };
@@ -457,11 +490,16 @@ async function runUnit(
457
490
  instanceId: string,
458
491
  node: PlanUnitNode,
459
492
  plan: PlanFacts,
493
+ session?: UnitSession,
460
494
  ): Promise<UnitEnding> {
461
495
  const unit = node.id;
496
+ // A renewal's segment names its steps under the segment (`U10/s2/…`), so
497
+ // the Workflow's durable step cache never answers segment two with segment
498
+ // one's results (decision 0046).
499
+ const prefix = stepPrefixOf(unit, session);
462
500
  const tag = { parentInstanceId: instanceId, unit };
463
501
  const start = readUnitStart(
464
- answerOf("unit-start", await step.do(`${unit}/start`, STEP_CONFIG, () => call(bot, "unit-start", tag))),
502
+ answerOf("unit-start", await step.do(`${prefix}/start`, STEP_CONFIG, () => call(bot, "unit-start", tag))),
465
503
  );
466
504
  // A resume at review (agent-ship item 10) rides the unit's row: the pull
467
505
  // request of ship's own the requester named opens the pipeline at its first
@@ -484,9 +522,12 @@ async function runUnit(
484
522
  merge: plan.merge,
485
523
  addressSeverity: plan.addressSeverity,
486
524
  addressSeveritySource: plan.addressSeveritySource,
525
+ grant: plan.grant,
526
+ grantSource: plan.grantSource,
487
527
  generated: plan.generated,
488
528
  ...(resume !== undefined ? { resume } : {}),
489
529
  ...(lastPush !== undefined ? { lastPush } : {}),
530
+ ...(session !== undefined ? { session } : {}),
490
531
  },
491
532
  start.at,
492
533
  );
@@ -499,7 +540,7 @@ async function runUnit(
499
540
  for (const note of transition.notes) {
500
541
  if (note.type === "round") {
501
542
  const body = { ...tag, index: note.index, agent: note.agent, outcome: note.outcome };
502
- await step.do(`${unit}/note/${++notes}`, STEP_CONFIG, () => call(bot, "round", body));
543
+ await step.do(`${prefix}/note/${++notes}`, STEP_CONFIG, () => call(bot, "round", body));
503
544
  } else {
504
545
  // A merge_ready ending names the pull request as it is at the APPROVED
505
546
  // head (agent-ship item 9): one more pr-check reads the facts fresh —
@@ -511,10 +552,10 @@ async function runUnit(
511
552
  if (note.ending.kind === "merge_ready") {
512
553
  try {
513
554
  const check = prCheckReturn(
514
- `${unit}/end/pr-facts`,
555
+ `${prefix}/end/pr-facts`,
515
556
  answerOf(
516
557
  "pr-check",
517
- await step.do(`${unit}/end/pr-facts`, STEP_CONFIG, () => call(bot, "pr-check", tag)),
558
+ await step.do(`${prefix}/end/pr-facts`, STEP_CONFIG, () => call(bot, "pr-check", tag)),
518
559
  ),
519
560
  );
520
561
  if (check.type === "pr-check" && check.pr.state === "merged")
@@ -538,8 +579,20 @@ async function runUnit(
538
579
  ? { headSha: note.ending.headSha }
539
580
  : {}),
540
581
  ...(state.lastCodingRunId !== undefined ? { codingRunId: state.lastCodingRunId } : {}),
582
+ // A continued ending is a segment's end, not the unit's: the bot
583
+ // writes the renewal as a row keyed by the next segment's index
584
+ // (decision 0046), so a runner reclaimed here never renews twice.
585
+ ...(note.ending.kind === "continued"
586
+ ? {
587
+ segment: {
588
+ index: note.ending.segment,
589
+ ...(note.ending.from !== undefined ? { from: note.ending.from } : {}),
590
+ runId: note.ending.runId,
591
+ },
592
+ }
593
+ : {}),
541
594
  };
542
- await step.do(`${unit}/end`, STEP_CONFIG, () => call(bot, "unit-end", body));
595
+ await step.do(`${prefix}/end`, STEP_CONFIG, () => call(bot, "unit-end", body));
543
596
  }
544
597
  }
545
598
  }
@@ -573,13 +626,23 @@ async function walk(step: StepRunner, bot: CoordinatorBot, instanceId: string):
573
626
  const [next] = readyUnits(graph, cursor);
574
627
  if (next === undefined) break;
575
628
  cursor = startUnit(graph, cursor, next);
576
- const ending = await runUnit(
577
- step,
578
- bot,
579
- instanceId,
580
- graph.units.find((u) => u.id === next)!,
581
- plan,
582
- );
629
+ const node = graph.units.find((u) => u.id === next)!;
630
+ let ending = await runUnit(step, bot, instanceId, node, plan);
631
+ // The lease continues while the grant renews (decision 0046): each
632
+ // `continued` ending opens the next segment of the same unit — a fresh
633
+ // pipeline under a fresh lease, from the recorded sha, briefed with the
634
+ // previous segment's write-up — until the unit ends some other way.
635
+ while (ending.kind === "continued") {
636
+ const c = ending;
637
+ ending = await runUnit(step, bot, instanceId, node, plan, {
638
+ segment: c.segment,
639
+ renewalsSpent: c.segment - 1,
640
+ spendUsd: c.spendUsd,
641
+ ...(c.from !== undefined ? { continueFrom: c.from } : {}),
642
+ previousRunId: c.runId,
643
+ ...(c.handoff !== undefined ? { previousHandoff: c.handoff } : {}),
644
+ });
645
+ }
583
646
  endings[next] = ending.kind;
584
647
  cursor = settleUnit(graph, cursor, next, ending.kind === "merged" ? "done" : "failed");
585
648
  }
@@ -1,4 +1,5 @@
1
1
  import { snapshotAgeText } from "./snapshotAge.js";
2
+ import { DAY_MS } from "./budgets.js";
2
3
  import { systemClock } from "./trace/clock.js";
3
4
 
4
5
  // Delivery indicators: what the run history and the pull requests' own facts
@@ -225,7 +226,6 @@ export function weekIncomplete(report: Pick<DeliveryReport, "truncated" | "compl
225
226
 
226
227
  // ---- the arithmetic ---------------------------------------------------------------------
227
228
 
228
- const DAY_MS = 86_400_000;
229
229
  const HOUR_MS = 3_600_000;
230
230
 
231
231
  /** Add `n` calendar days to a UTC date (`YYYY-MM-DD`). */
@@ -196,6 +196,20 @@ export type RunNoteKind =
196
196
  * item 7): the summary names the tool and the rule; the model read the same
197
197
  * reason as the tool's result. Published by the bot's authorize route. */
198
198
  | "tool_refused"
199
+ /** An OpenCode tool settled under a step this loop never saw start and the
200
+ * settle was set aside (harness.md item 13): an earlier execution's late
201
+ * result — the pinned binary's ordinary shape after a hung call's interrupt
202
+ * — or a step lost with the tailer's stream. The summary names the call and
203
+ * the step. Information, not a failure of the harness: nothing ran that the
204
+ * gate did not decide. Published by the OpenCode bridge in its own mode. */
205
+ | "settle_set_aside"
206
+ /** OpenCode's store refill carried a permission over something other than a
207
+ * tool (a directory outside the project, a repeating session) for a call
208
+ * the record never saw, and the store held no part yet to name the call's
209
+ * tool by: the call was opened under the permission's own name. The summary
210
+ * names the call and the step. Information, not a failure: the tool ran
211
+ * under the gate's decision either way. Published by the OpenCode bridge. */
212
+ | "tool_unnamed"
199
213
  /** A ship coding child's budget ended with work still in the tree: the run
200
214
  * loop committed and pushed it to the unit's branch (or says plainly that
201
215
  * there was nothing to push), so a re-issue starts from the partial work
@@ -235,6 +249,8 @@ export const RUN_NOTE_KINDS = [
235
249
  "harness_error",
236
250
  "policy_refusal",
237
251
  "tool_refused",
252
+ "settle_set_aside",
253
+ "tool_unnamed",
238
254
  "budget_salvage",
239
255
  "stuck_loop",
240
256
  ] as const satisfies readonly RunNoteKind[];
@@ -293,7 +309,17 @@ export function sanitizeActor(actor: RunActor): RunActor {
293
309
  * settles a round: caps end the pipeline BETWEEN rounds, visible as the
294
310
  * absence of a next `started` boundary plus the answer's cap report. */
295
311
  export type ShipRoundOutcome =
296
- "started" | "pr_opened" | "completed" | "approve" | "request_changes" | "no_verdict" | "aborted" | "stopped";
312
+ | "started"
313
+ | "pr_opened"
314
+ | "completed"
315
+ | "approve"
316
+ | "request_changes"
317
+ | "no_verdict"
318
+ | "aborted"
319
+ | "stopped"
320
+ /** The coding round ended at its lease with the unit unfinished and the row
321
+ * showing progress, and the grant renewed: the next segment opens (decision 0046). */
322
+ | "continued";
297
323
 
298
324
  /**
299
325
  * One event in a run's stream. `seq` is stamped by `RunRegistry.publish` — a
@@ -393,10 +419,14 @@ export type RouteInputValue = RouteInputLeafOrList | RouteInputObject3;
393
419
 
394
420
  /** How a door decision about a state change ended (docs/decisions/0044-a-routed-write-is-confirmed-in-proportion-to-its-blast-radius.md):
395
421
  * `hand_back` — the router bound a state-changing command and the door answered
396
- * the line to paste, nothing invoked; `pasted` — the typed line that followed
397
- * a hand-back in its thread, with the same receipt, ran. A routed read
398
- * carries no outcome: it is not a decision about a state change. */
399
- export type RouteOutcome = "hand_back" | "pasted";
422
+ * the line to paste, nothing invoked; `offered` — the same decision on a
423
+ * channel that can show a confirmation: the line, its risk and a button, a
424
+ * row stored in the config object, nothing invoked; `confirmed` — the stored
425
+ * input ran at the click, as the requester, through the typed line's path;
426
+ * `pasted` — the typed line that followed a hand-back in its thread, with the
427
+ * same receipt, ran. A routed read carries no outcome: it is not a decision
428
+ * about a state change. */
429
+ export type RouteOutcome = "hand_back" | "offered" | "confirmed" | "pasted";
400
430
 
401
431
  export type RunEvent =
402
432
  /** `callId` is the provider's tool_use id — the explicit pair key between a
@@ -561,6 +591,10 @@ export type RunEvent =
561
591
  * the instance's units. A child's instance rides the record's
562
592
  * `parentInstanceId`, never here. */
563
593
  instanceId?: string;
594
+ /** The grant the ship request carried (decision 0046, the renewable
595
+ * lease): renewals and a cost cap — what a renewal could spend. Written
596
+ * on the pipeline's own record beside `instanceId`; absent elsewhere. */
597
+ grant?: { renewals: number; costCapUsd?: number };
564
598
  seq?: number;
565
599
  at?: number;
566
600
  }
@@ -691,6 +725,12 @@ export type RunEvent =
691
725
  * start of the loop; a resumed run carries the original. Head material,
692
726
  * like `run_meta`. Additive: unknown → ignored. */
693
727
  | { type: "lease"; startedAt: number; endsAt: number; loopEndsAt: number; seq?: number; at?: number }
728
+ /** A head the run pushed (docs/reference/specs/run-history.md item 2; decision
729
+ * 0046): the branch and the sha the coding post-step observed on the remote
730
+ * (`by: "push"`), or the budget-end salvage pushed (`by: "salvage"`),
731
+ * whether or not a pull request follows — the fact renewal reads. Published
732
+ * straight to the registry like `pr_opened`. Additive: unknown → ignored. */
733
+ | { type: "pushed_head"; ref: string; sha: string; by: "push" | "salvage"; seq?: number; at?: number }
694
734
  /** The coordinator tag as a fact of the run (docs/reference/specs/run-history.md
695
735
  * item 48a): the instance the run is a child of, the unit its idempotency
696
736
  * key named, and the base branch its pull request targets — published by
@@ -755,8 +795,11 @@ export type RunEvent =
755
795
  * how the invoke ended is the run's own status and `answer`. A door
756
796
  * decision about a state change (record 0044) is a command run too, with
757
797
  * `outcome` saying which: a hand-back invoked nothing and its `answer` is
758
- * the line to paste; a paste is the typed line that followed, whatever its
759
- * command, `handBackRunId` naming the hand-back's record. */
798
+ * the line to paste; an offer invoked nothing and its `answer` is the offer
799
+ * as the channel shows it; a confirmation is the stored input run at the
800
+ * click, its `answer` the command's own text as a routed read's is; a paste is
801
+ * the typed line that followed a hand-back, whatever its command,
802
+ * `handBackRunId` naming the hand-back's record. */
760
803
  | {
761
804
  type: "route";
762
805
  preset: string;
@@ -426,7 +426,8 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
426
426
  ev.type === "ship_round" ||
427
427
  ev.type === "route" ||
428
428
  ev.type === "reference" ||
429
- ev.type === "lease"
429
+ ev.type === "lease" ||
430
+ ev.type === "pushed_head"
430
431
  ) {
431
432
  sideFactEvents++;
432
433
  return;
@@ -213,6 +213,10 @@ export interface RunRecord {
213
213
  * `lease` event, folded at the assembly. Present only on a run whose
214
214
  * harness started a loop; a record written before the event lacks it. */
215
215
  lease?: RunLease;
216
+ /** The heads the run pushed (item 2; decision 0046): one per branch, the
217
+ * last `pushed_head` event's sha winning — the fact a renewal reads to know
218
+ * the run made progress. Present only on a run that pushed. */
219
+ pushed?: PushedHead[];
216
220
  /** What the run cost in tokens, per model, summed from its `model.turn`
217
221
  * spans at finish (`usageOfEvents`; docs/reference/specs/costs.md, cost by user).
218
222
  * Every record written since carries it (zero turns included); one written
@@ -238,6 +242,21 @@ export interface RunLease {
238
242
  loopEndsAt: number;
239
243
  }
240
244
 
245
+ /** A head a run pushed, as the record names it (item 2): the branch and the sha. */
246
+ export interface PushedHead {
247
+ ref: string;
248
+ sha: string;
249
+ }
250
+
251
+ /** The heads a run's events say it pushed — one per branch, the last event's
252
+ * sha winning, in first-seen order — or nothing when it pushed none. */
253
+ export function pushedHeadsOf(events: readonly RunEvent[]): PushedHead[] | undefined {
254
+ const byRef = new Map<string, string>();
255
+ for (const e of events) if (e.type === "pushed_head") byRef.set(e.ref, e.sha);
256
+ if (byRef.size === 0) return undefined;
257
+ return [...byRef].map(([ref, sha]) => ({ ref, sha }));
258
+ }
259
+
241
260
  /** The lease a run's events say its harness started — the first `lease`
242
261
  * event; a resumed run's later generations publish none — or nothing. */
243
262
  export function leaseOfEvents(events: readonly RunEvent[]): RunLease | undefined {
@@ -798,6 +817,18 @@ export function isRunRecord(v: unknown): v is RunRecord {
798
817
  if (r.profile !== undefined && !isRunProfileRecord(r.profile)) return false;
799
818
  // The pull request the run reached (item 2): a positive integer and a URL, or absent.
800
819
  if (r.pr !== undefined && !isRunPullRequestShape(r.pr)) return false;
820
+ if (
821
+ r.pushed !== undefined &&
822
+ !(
823
+ Array.isArray(r.pushed) &&
824
+ r.pushed.every((h) => {
825
+ if (typeof h !== "object" || h === null) return false;
826
+ const { ref, sha } = h as Record<string, unknown>;
827
+ return typeof ref === "string" && ref.length > 0 && typeof sha === "string" && /^[0-9a-f]{7,40}$/.test(sha);
828
+ })
829
+ )
830
+ )
831
+ return false;
801
832
  // A parent is named by a run id (item 46): the same shape as the record's own.
802
833
  if (r.parentRunId !== undefined && (typeof r.parentRunId !== "string" || !RUN_ID_PATTERN.test(r.parentRunId)))
803
834
  return false;
@@ -431,7 +431,8 @@ function renderFirstInstruction(rebase: ChildContract["rebase"]): string {
431
431
  `Push the branch as soon as the change exists and its cheapest proving checks pass — before the project's ` +
432
432
  `full verification, which runs after that push with any fix as a further commit; an unpushed tree does not ` +
433
433
  `survive the run's end. Right before each push, fetch ${onto} again and rebase once more if it moved while ` +
434
- `you worked, so the pull request is not born conflicting.`
434
+ `you worked, so the pull request is not born conflicting. At the wind-down note, commit and push what compiles, ` +
435
+ `say what does not, then answer.`
435
436
  );
436
437
  }
437
438