@coreplane/switchboard 1.252.0 → 1.253.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 (131) hide show
  1. package/dist/assets/config/config.example.yaml +24 -0
  2. package/dist/assets/deploy/cloudflare-memory/runMetricsSink.ts +31 -0
  3. package/dist/assets/deploy/cloudflare-memory/worker.ts +146 -26
  4. package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +12 -0
  5. package/dist/assets/deploy/profile.example.json +1 -1
  6. package/dist/assets/package-lock.json +3 -3
  7. package/dist/assets/package.json +1 -1
  8. package/dist/assets/source.json +3 -3
  9. package/dist/assets/src/core/authz/policy.ts +8 -0
  10. package/dist/assets/src/core/budgets.ts +17 -0
  11. package/dist/assets/src/core/coordinator/contract.ts +51 -1
  12. package/dist/assets/src/core/coordinator/driver.ts +102 -11
  13. package/dist/assets/src/core/memory/engine.ts +25 -4
  14. package/dist/assets/src/core/memory/types.ts +6 -2
  15. package/dist/assets/src/core/runEvents.ts +31 -1
  16. package/dist/assets/src/core/runFriction.ts +1 -0
  17. package/dist/assets/src/core/runMetrics.ts +237 -0
  18. package/dist/assets/src/core/runRecord.ts +46 -0
  19. package/dist/assets/src/core/ship/contract.ts +8 -15
  20. package/dist/assets/src/core/ship/coordinator.ts +105 -2
  21. package/dist/assets/src/deploy/profile.ts +16 -0
  22. package/dist/assets/web/dist/.vite/manifest.json +440 -426
  23. package/dist/assets/web/dist/assets/{AppShell-DAtHiuI6.js → AppShell-Bmz8nzz3.js} +1 -1
  24. package/dist/assets/web/dist/assets/{CostsPage-BoKzYa4B.js → CostsPage-CxTKdpzU.js} +1 -1
  25. package/dist/assets/web/dist/assets/{DeliveryPage-3ELQWM0r.js → DeliveryPage-C7jaEJip.js} +1 -1
  26. package/dist/assets/web/dist/assets/HomePage-cWbupQzy.js +3 -0
  27. package/dist/assets/web/dist/assets/{InputMenu-C6aPpe30.js → InputMenu-C9E7YoII.js} +1 -1
  28. package/dist/assets/web/dist/assets/{NotFoundPage-DtE-GgTk.js → NotFoundPage-4nlrq7DP.js} +1 -1
  29. package/dist/assets/web/dist/assets/{PendingTurnRow-ChCQOLgZ.js → PendingTurnRow-C-pUgyyX.js} +1 -1
  30. package/dist/assets/web/dist/assets/PlanePage-BNy4tSgJ.js +1 -0
  31. package/dist/assets/web/dist/assets/{ResidentDetailPage-C9y3nbo8.js → ResidentDetailPage-Dbtiq6_J.js} +1 -1
  32. package/dist/assets/web/dist/assets/{ResidentsIndexPage-i1RG9e7g.js → ResidentsIndexPage-CKtz9H1o.js} +1 -1
  33. package/dist/assets/web/dist/assets/RunFoldRow-sYFtuK2G.js +1 -0
  34. package/dist/assets/web/dist/assets/RunRoutePage-Clh68nw9.js +9 -0
  35. package/dist/assets/web/dist/assets/RunsIndexPage-DPDbcOA_.js +1 -0
  36. package/dist/assets/web/dist/assets/{RunsTabs-YSbUu5py.js → RunsTabs-DotpuCSP.js} +1 -1
  37. package/dist/assets/web/dist/assets/{ScheduledPage-DvYwM2TE.js → ScheduledPage-C8FYoZy5.js} +1 -1
  38. package/dist/assets/web/dist/assets/{SettingSelect-BIzAsLk1.js → SettingSelect-CSiBQCSX.js} +1 -1
  39. package/dist/assets/web/dist/assets/{SettingsPage-Bo6yCyXZ.js → SettingsPage-CkYIhLUl.js} +1 -1
  40. package/dist/assets/web/dist/assets/{StatusDot-CAfS1AUi.js → StatusDot-C0TUgnC1.js} +1 -1
  41. package/dist/assets/web/dist/assets/{Tooltip-tZoum_T-.js → Tooltip-CN5cZdl3.js} +1 -1
  42. package/dist/assets/web/dist/assets/{UnitRoutePage-BmdOHwNn.js → UnitRoutePage-D_EtAl8r.js} +1 -1
  43. package/dist/assets/web/dist/assets/{angular-html-BjeQZdCq.js → angular-html-qW40ZL9x.js} +1 -1
  44. package/dist/assets/web/dist/assets/{angular-ts-ZGdORrL2.js → angular-ts-PUtZU8Cq.js} +1 -1
  45. package/dist/assets/web/dist/assets/{apl-DhvV_X93.js → apl-D3fC7Qjd.js} +1 -1
  46. package/dist/assets/web/dist/assets/{astro-DiRSE4Ug.js → astro-Bi3JFHhh.js} +1 -1
  47. package/dist/assets/web/dist/assets/{blade-D8Fr5Wvq.js → blade-CSpsf9Z9.js} +1 -1
  48. package/dist/assets/web/dist/assets/{c-BwbC64D4.js → c-DE_b1oWP.js} +1 -1
  49. package/dist/assets/web/dist/assets/{chapel-DgmqRK97.js → chapel-CWXd9giL.js} +1 -1
  50. package/dist/assets/web/dist/assets/{cobol-DJod2RNK.js → cobol-CnMOmWNF.js} +1 -1
  51. package/dist/assets/web/dist/assets/{coffee-D1Y2CgBY.js → coffee-B9WvURu8.js} +1 -1
  52. package/dist/assets/web/dist/assets/{cpp-CsO97YOM.js → cpp-BZAJXlWI.js} +1 -1
  53. package/dist/assets/web/dist/assets/{crystal-CJ7m2tOc.js → crystal-Bm48wAlb.js} +1 -1
  54. package/dist/assets/web/dist/assets/{css-B2M-NKoj.js → css-WtMeC3ab.js} +1 -1
  55. package/dist/assets/web/dist/assets/{dist-DfbEpHXR.js → dist-B1Mf2JbM.js} +2 -2
  56. package/dist/assets/web/dist/assets/{edge-CFVW-m9B.js → edge-BmwNa6Ss.js} +1 -1
  57. package/dist/assets/web/dist/assets/{elixir-B2mtRgCr.js → elixir-BotfP-Mn.js} +1 -1
  58. package/dist/assets/web/dist/assets/{elm-2teVAwCi.js → elm-DAFc6laN.js} +1 -1
  59. package/dist/assets/web/dist/assets/{erb-LAbeNqen.js → erb-QkOON3bB.js} +1 -1
  60. package/dist/assets/web/dist/assets/{git-rebase-DnhzH_sp.js → git-rebase-BPs-dd73.js} +1 -1
  61. package/dist/assets/web/dist/assets/{glimmer-js-DPKjXf1-.js → glimmer-js-D8eooepk.js} +1 -1
  62. package/dist/assets/web/dist/assets/{glimmer-ts-CABXnt5z.js → glimmer-ts-B0bRiDVg.js} +1 -1
  63. package/dist/assets/web/dist/assets/{glsl-Dz4-3-gJ.js → glsl-Bv6bMe1n.js} +1 -1
  64. package/dist/assets/web/dist/assets/{graphql-ByWUD1DL.js → graphql-Bh9y-n9o.js} +1 -1
  65. package/dist/assets/web/dist/assets/{hack-CdmOK2K-.js → hack-DSEbn7Ir.js} +1 -1
  66. package/dist/assets/web/dist/assets/{haml-DzegMpd2.js → haml-0NW1-z1f.js} +1 -1
  67. package/dist/assets/web/dist/assets/{handlebars-B5NeiP0e.js → handlebars-BfgCHFbK.js} +1 -1
  68. package/dist/assets/web/dist/assets/{html-Zg_NHLv9.js → html-CQHc0gQ5.js} +1 -1
  69. package/dist/assets/web/dist/assets/{html-derivative-DZf9JNzh.js → html-derivative-B0Tm7wLP.js} +1 -1
  70. package/dist/assets/web/dist/assets/{http-CVFrQZeN.js → http-GAuU3aOF.js} +1 -1
  71. package/dist/assets/web/dist/assets/{hurl-L5brWAJe.js → hurl-CrDFbF5B.js} +1 -1
  72. package/dist/assets/web/dist/assets/indexRow-BxKswdlK.js +1 -0
  73. package/dist/assets/web/dist/assets/{java-hFphyZ4R.js → java-DYv5KhUe.js} +1 -1
  74. package/dist/assets/web/dist/assets/{javascript-CAJO1WKX.js → javascript-32SiLGg0.js} +1 -1
  75. package/dist/assets/web/dist/assets/{jinja-N4A4srXI.js → jinja--gJDUGYa.js} +1 -1
  76. package/dist/assets/web/dist/assets/{jison-D3BZa_iU.js → jison-BlKLGzZH.js} +1 -1
  77. package/dist/assets/web/dist/assets/{json-DxD1Qh8W.js → json-BICanD1N.js} +1 -1
  78. package/dist/assets/web/dist/assets/{jsx-CHhag69S.js → jsx-CgW3-uFT.js} +1 -1
  79. package/dist/assets/web/dist/assets/{julia-CrhkZ6Tt.js → julia-BErRpwwL.js} +1 -1
  80. package/dist/assets/web/dist/assets/{just-D0LFOuhq.js → just-CordHv0h.js} +1 -1
  81. package/dist/assets/web/dist/assets/{latex-BHAlwCXK.js → latex-D7LVPLbi.js} +1 -1
  82. package/dist/assets/web/dist/assets/{liquid-CkaLfCWh.js → liquid-3ehz2i4U.js} +1 -1
  83. package/dist/assets/web/dist/assets/{lua-B1h4aAzP.js → lua-C9ppSVrm.js} +1 -1
  84. package/dist/assets/web/dist/assets/main-Cv0q9y56.js +28 -0
  85. package/dist/assets/web/dist/assets/main-DKFCM-sy.css +1 -0
  86. package/dist/assets/web/dist/assets/{marko-D6CG4hvX.js → marko-xBZLUBEJ.js} +1 -1
  87. package/dist/assets/web/dist/assets/{mdc-CVQzimAU.js → mdc-BJxGoVgD.js} +1 -1
  88. package/dist/assets/web/dist/assets/{nginx-h-9Ir54h.js → nginx-B-sZmpsb.js} +1 -1
  89. package/dist/assets/web/dist/assets/{nim-DHVI30sp.js → nim--s9wRABK.js} +1 -1
  90. package/dist/assets/web/dist/assets/{org-AasAXfMF.js → org-DoUvssoI.js} +1 -1
  91. package/dist/assets/web/dist/assets/{perl-BVi5aTEy.js → perl-B-vGHKOy.js} +1 -1
  92. package/dist/assets/web/dist/assets/{php-qOkCeUKr.js → php-CerWx_Xb.js} +1 -1
  93. package/dist/assets/web/dist/assets/{pug-8TnUGvEX.js → pug-CzKrBMb1.js} +1 -1
  94. package/dist/assets/web/dist/assets/{qml-CpJbhoxn.js → qml-Bs8L8I6k.js} +1 -1
  95. package/dist/assets/web/dist/assets/{r-JkUNcAJW.js → r-w6uG6mob.js} +1 -1
  96. package/dist/assets/web/dist/assets/{razor-CtU4ws5a.js → razor-dz2oj2bd.js} +1 -1
  97. package/dist/assets/web/dist/assets/{regexp-J2zA6ceN.js → regexp-CfQdprnY.js} +1 -1
  98. package/dist/assets/web/dist/assets/{rst-CMCvf-yZ.js → rst-DAOaX2tb.js} +1 -1
  99. package/dist/assets/web/dist/assets/{ruby-ChLBY_iR.js → ruby-QcCe4udB.js} +1 -1
  100. package/dist/assets/web/dist/assets/{sas-cwn4x2VY.js → sas-BrM4lAAc.js} +1 -1
  101. package/dist/assets/web/dist/assets/{scss-jSrJlzLI.js → scss-DVbJXoUb.js} +1 -1
  102. package/dist/assets/web/dist/assets/{shellscript-D5nAd0gs.js → shellscript-09fP2hZf.js} +1 -1
  103. package/dist/assets/web/dist/assets/{shellsession-BZY4-Tdh.js → shellsession-Qgr8xjZF.js} +1 -1
  104. package/dist/assets/web/dist/assets/{soy-COppPOKr.js → soy-DIYeNTuz.js} +1 -1
  105. package/dist/assets/web/dist/assets/{sql-Blc_6dOA.js → sql-CzLR9msV.js} +1 -1
  106. package/dist/assets/web/dist/assets/{sseReplay-DmyMXfRC.js → sseReplay-BYo2EZKy.js} +4 -4
  107. package/dist/assets/web/dist/assets/{stata-DRv3OY-h.js → stata-zCY1ZzH0.js} +1 -1
  108. package/dist/assets/web/dist/assets/{surrealql-qMv_eeJ6.js → surrealql-CJIjTRV4.js} +1 -1
  109. package/dist/assets/web/dist/assets/{svelte-d1Edb6Rz.js → svelte-C5OTPFZg.js} +1 -1
  110. package/dist/assets/web/dist/assets/{templ-CUMSIOXG.js → templ-DYj-UICF.js} +1 -1
  111. package/dist/assets/web/dist/assets/{tex-BGZQA-Ei.js → tex-xcie-3Gd.js} +1 -1
  112. package/dist/assets/web/dist/assets/{ts-tags-Z9ulxVsq.js → ts-tags-CoN86VTt.js} +1 -1
  113. package/dist/assets/web/dist/assets/{tsx-oI4bI3Vf.js → tsx-n3-No_Br.js} +1 -1
  114. package/dist/assets/web/dist/assets/{twig-D_RXwBIX.js → twig-DPiJ7Vp1.js} +1 -1
  115. package/dist/assets/web/dist/assets/{typescript-BgqQDYoN.js → typescript-BP6HBaOp.js} +1 -1
  116. package/dist/assets/web/dist/assets/{typst-5MW-KRDd.js → typst-Do9ZjsVP.js} +1 -1
  117. package/dist/assets/web/dist/assets/{vue-CZhHHsPm.js → vue-CzAeXNRY.js} +1 -1
  118. package/dist/assets/web/dist/assets/{vue-html-DlYy2d8v.js → vue-html-D7iYmYjd.js} +1 -1
  119. package/dist/assets/web/dist/assets/{vue-vine-CJDnx-qn.js → vue-vine-DtrMRsJx.js} +1 -1
  120. package/dist/assets/web/dist/assets/{xml-BRBDk65Q.js → xml-DGJHo1c0.js} +1 -1
  121. package/dist/assets/web/dist/assets/{xsl-BlCykPxo.js → xsl-Q00kvNgW.js} +1 -1
  122. package/dist/assets/web/dist/assets/{yaml-BqW48szf.js → yaml-F-UGKaiO.js} +1 -1
  123. package/dist/cli.js +19192 -17592
  124. package/package.json +1 -1
  125. package/dist/assets/web/dist/assets/HomePage-BG_ok-K2.js +0 -2
  126. package/dist/assets/web/dist/assets/RunFoldRow-D3wVpzBa.js +0 -1
  127. package/dist/assets/web/dist/assets/RunRoutePage-B3IirUVi.js +0 -9
  128. package/dist/assets/web/dist/assets/RunsIndexPage-DiFmtGaJ.js +0 -1
  129. package/dist/assets/web/dist/assets/indexRow-BT0cPVRw.js +0 -1
  130. package/dist/assets/web/dist/assets/main-5Gm_1Gv8.js +0 -28
  131. package/dist/assets/web/dist/assets/main-zbP_dTjR.css +0 -1
@@ -184,6 +184,13 @@ export interface RunRecord {
184
184
  * reading the events. Absent for a preset a person, a scope or the default
185
185
  * chose, and on records written before it existed. */
186
186
  route?: RunRouteDecision;
187
+ /** The operator's shadow decision beside the routed request (record 0057;
188
+ * the one-door plan's operator unit; run-history item 60): what the operator
189
+ * bound for the same event, redacted like the receipt — never the message
190
+ * text — with the intake gate's verdict when the gate was present. The
191
+ * agreement row reads it from here; it follows the record's retention.
192
+ * Absent under `routing.operator: off` and on records written before it. */
193
+ operator?: RunOperatorDecision;
187
194
  /** The effective profile the run was admitted with (docs/decisions/0026-capability-profiles-and-request-routing.md):
188
195
  * the preset, its machine class and identity, the minutes it ran on and —
189
196
  * when a boundary clipped the budget — the scope that did, so a reader can
@@ -404,6 +411,45 @@ export function routeOfEvents(events: readonly RunEvent[]): RunRouteDecision | u
404
411
  };
405
412
  }
406
413
 
414
+ /** The operator's decision as a record carries it — the `operator` event's
415
+ * fields less the stream bookkeeping (record 0057; run-history item 60). */
416
+ export interface RunOperatorDecision {
417
+ mode: "shadow" | "on";
418
+ outcome: "binds" | "question" | "refusal";
419
+ reason: string;
420
+ binds?: { line: string; reason: string }[];
421
+ question?: string;
422
+ /** A question's proposed line — what the next turn's "yes" binds. */
423
+ proposal?: string;
424
+ refusalCause?: string;
425
+ refusalText?: string;
426
+ intake?: { verdict: string; reason: string };
427
+ latencyMs?: number;
428
+ outputTokens?: number;
429
+ }
430
+
431
+ /** The operator's decision the run's events carry: the one `operator` event
432
+ * (record 0057), whatever the run's own agent source — the shadow decision
433
+ * rides routed and unrouted runs alike, or there would be no disagreement to
434
+ * count. Undefined on a run the operator never saw. */
435
+ export function operatorOfEvents(events: readonly RunEvent[]): RunOperatorDecision | undefined {
436
+ const e = events.find((e) => e.type === "operator");
437
+ if (e?.type !== "operator") return undefined;
438
+ return {
439
+ mode: e.mode,
440
+ outcome: e.outcome,
441
+ reason: e.reason,
442
+ ...(e.binds ? { binds: e.binds.map((b) => ({ line: b.line, reason: b.reason })) } : {}),
443
+ ...(e.question !== undefined ? { question: e.question } : {}),
444
+ ...(e.proposal !== undefined ? { proposal: e.proposal } : {}),
445
+ ...(e.refusalCause !== undefined ? { refusalCause: e.refusalCause } : {}),
446
+ ...(e.refusalText !== undefined ? { refusalText: e.refusalText } : {}),
447
+ ...(e.intake ? { intake: { verdict: e.intake.verdict, reason: e.intake.reason } } : {}),
448
+ ...(e.latencyMs !== undefined ? { latencyMs: e.latencyMs } : {}),
449
+ ...(e.outputTokens !== undefined ? { outputTokens: e.outputTokens } : {}),
450
+ };
451
+ }
452
+
407
453
  function isRunRouteShape(v: unknown): v is RunRouteDecision {
408
454
  if (typeof v !== "object" || v === null) return false;
409
455
  const r = v as Record<string, unknown>;
@@ -451,16 +451,16 @@ export const TIMEOUT_ON_LONG_COMMANDS =
451
451
  * exit line goes into the PR description's validation table as the row's proof
452
452
  * (the handoff has no verified list; parseHandoff carries deviations, followUps,
453
453
  * unproven and landed), and a gate the child could not run goes under the
454
- * handoff's unproven list, never claimed clean. The tests compare against the
455
- * pull request's own base — `origin/<base>` — which the render substitutes
456
- * from the rebase's `onto` when the contract knows it. */
457
- /** The changed-set test command as the contract renders it before the base is
458
- * known; the render substitutes the unit's base for `<base>` (`renderFirstInstruction`). */
459
- export const CHANGED_SET_TEST_PLACEHOLDER = "`npx vitest run --changed origin/<base>`";
454
+ * handoff's unproven list, never claimed clean. The test gate is the touched
455
+ * files by name, never a changed-set or directory run. */
456
+ /** The test command the contract hands a child: the touched files by name, once.
457
+ * Never `--changed`: against a base that moves, it selects most of the suite, and on the
458
+ * shared resident that is the memory incident the coding contract exists to prevent. */
459
+ export const TOUCHED_TESTS_COMMAND = "`npx vitest run` on the test files you touched, by name,";
460
460
 
461
461
  export const FAST_GATES_BEFORE_PUSH =
462
462
  "The fast gates, before every push — each scoped to the changed set, never the whole project: " +
463
- `${CHANGED_SET_TEST_PLACEHOLDER} (the pull request's base; or the touched test files), ` +
463
+ `${TOUCHED_TESTS_COMMAND} once (never \`--changed\`, never a directory: on a moving base that is most of the suite), ` +
464
464
  "`tsc --noEmit -p` the touched tsconfig under `NODE_OPTIONS=--max-old-space-size=6144`, " +
465
465
  "`npx prettier --check` on the changed files, `npm run hygiene:check` and `npm run specs:check` — " +
466
466
  "then your judgement on what else this change needs, not a longer checklist. Every CI pipeline runs the " +
@@ -474,14 +474,7 @@ export const FAST_GATES_BEFORE_PUSH =
474
474
  function renderFirstInstruction(rebase: ChildContract["rebase"]): string {
475
475
  const branch = rebase.branch ? `\`${rebase.branch}\`` : "the unit's branch";
476
476
  const onto = rebase.onto ? `\`${rebase.onto}\`` : "the merged parent";
477
- // the changed-set test run compares against the unit's own base, which the
478
- // contract knows as the rebase target; unknown, the placeholder stands
479
- // split/join, never String.replace: a `$` in a branch name is literal text here
480
- const gates = rebase.onto
481
- ? FAST_GATES_BEFORE_PUSH.split(`${CHANGED_SET_TEST_PLACEHOLDER} (the pull request's base; or`).join(
482
- `\`npx vitest run --changed origin/${rebase.onto}\` (or`,
483
- )
484
- : FAST_GATES_BEFORE_PUSH;
477
+ const gates = FAST_GATES_BEFORE_PUSH;
485
478
  return (
486
479
  `Rebase ${branch} onto ${onto} before any other work — the parent unit has merged and the base has moved; ` +
487
480
  `the only writes are your own on that branch. A conflict ends the unit: report it as the handoff and stop. ` +
@@ -696,7 +696,46 @@ export type UnitEnding =
696
696
  }
697
697
  | { kind: "no_verdict"; round: RoundRef; reviewRounds: number; finalReply?: string }
698
698
  | { kind: "interrupted"; round: RoundRef; runId: string; reviewRounds: number }
699
- | { kind: "refused"; refusal: string; message?: string; round: RoundRef; reviewRounds: number };
699
+ | { kind: "refused"; refusal: string; message?: string; round: RoundRef; reviewRounds: number }
700
+ /** The unit idles instead of ending (record 0051): with the resolved
701
+ * `ship.idleDays` above zero, `end()` wraps an idling kind — every kind but
702
+ * the four ended ones (`merged`, `already_landed`, `merge_ready`,
703
+ * `refused`) — in this ending: the old kind as `why`, the old kind's
704
+ * report unchanged, and what a continuation needs (the renewals the grant
705
+ * still holds — unspent: the wake spends one, this plan's fifth unit — the
706
+ * head to continue from, the last coding child's run, the spend and its
707
+ * handoff). `runId` is absent when the pipeline capped before any coding
708
+ * child ran. */
709
+ | {
710
+ kind: "idle";
711
+ why: IdleWhy;
712
+ /** The ending the idle stands in for, whole: its report is rendered per
713
+ * copy at that copy's level — the row's and the board's in full, the
714
+ * thread's at the request's verbosity — never once at mapping time. */
715
+ idled: Exclude<UnitEnding, { kind: "idle" }>;
716
+ renewalsLeft: number;
717
+ from?: string;
718
+ runId?: string;
719
+ spendUsd: number | null;
720
+ handoff?: Handoff;
721
+ round?: RoundRef;
722
+ reviewRounds: number;
723
+ };
724
+
725
+ /** The kinds that idle: every ending but the four ended ones — the unit is
726
+ * unfinished (a cap, a stop, an abort, a refused merge, a segment's end) and
727
+ * a reply could continue it. `merged`, `already_landed`, `merge_ready` and
728
+ * `refused` never idle: the first two are done, merge-ready waits only for a
729
+ * person's merge, and a refused child would be refused again. */
730
+ export type IdleWhy = Exclude<UnitEnding["kind"], "idle" | "merged" | "already_landed" | "merge_ready" | "refused">;
731
+
732
+ const NEVER_IDLES: ReadonlySet<UnitEnding["kind"]> = new Set([
733
+ "idle",
734
+ "merged",
735
+ "already_landed",
736
+ "merge_ready",
737
+ "refused",
738
+ ]);
700
739
 
701
740
  /** What a transition tells the driver beyond the next action: a round boundary
702
741
  * the card draws (`shipRoundHeader`) and the run stream records, and the end. */
@@ -783,6 +822,11 @@ export interface UnitPipelineInput {
783
822
  * `plan` with an id and no `path` — whose unit runs in the requesting
784
823
  * thread and is re-issued with the request's own text, never a plan path. */
785
824
  generated: boolean;
825
+ /** The idle flag (record 0051; agent-ship item 8): `ship.idleDays` as the
826
+ * ship fork resolved it onto the instance — above zero, an idling kind ends
827
+ * `idle` instead; zero or absent is today's behavior byte for byte. The
828
+ * wait itself lands with this plan's fifth unit. */
829
+ idleDays?: number;
786
830
  /** Resume at review: an open pull request of ship's own the requester named. */
787
831
  resume?: { pr: number; headSha?: string; url?: string };
788
832
  /** The head the previous attempt's coding child last pushed (a
@@ -876,6 +920,10 @@ export interface UnitPipelineState {
876
920
  readonly findingsRunByRound: Readonly<Record<number, string>>;
877
921
  /** The last coding run (round 0's child or a findings step's): its record carries the unit's handoff. */
878
922
  readonly lastCodingRunId?: string;
923
+ /** The head the last coding child left the branch at, and its handoff — the
924
+ * continuation facts an `idle` ending carries (record 0051). */
925
+ readonly lastChildHead?: string;
926
+ readonly lastCodingHandoff?: Handoff;
879
927
  /** How the budget went so far, accrued as each answer moves the clock. */
880
928
  readonly spentMs: ShipBudgetSpent;
881
929
  /** The session's dollars so far: the input's from earlier segments plus each
@@ -1117,7 +1165,55 @@ function addSpend(sum: number | null, cost: number | null | undefined): number |
1117
1165
  }
1118
1166
 
1119
1167
  function end(s: UnitPipelineState, ending: UnitEnding, notes: CoordinatorNote[] = []): Transition {
1120
- return { state: { ...s, phase: ENDED, ending }, notes: [...notes, { type: "ended", ending }] };
1168
+ const idled = idleEnding(s, ending);
1169
+ const final = idled ?? ending;
1170
+ return { state: { ...s, phase: ENDED, ending: final }, notes: [...notes, { type: "ended", ending: final }] };
1171
+ }
1172
+
1173
+ /** The idle mapping (record 0051; agent-ship item 8): with the input's
1174
+ * `idleDays` above zero an idling kind ends `idle` — the old kind as `why`,
1175
+ * the old ending itself for the report, and the continuation facts off the state
1176
+ * (a `continued` ending's own where it carries them). At zero, undefined:
1177
+ * every ending is byte for byte today's. The round notes keep the old kind's
1178
+ * outcome — the row and the card say what happened. */
1179
+ function idleEnding(s: UnitPipelineState, ending: UnitEnding): Extract<UnitEnding, { kind: "idle" }> | undefined {
1180
+ if ((s.input.idleDays ?? 0) <= 0) return undefined;
1181
+ if (NEVER_IDLES.has(ending.kind)) return undefined;
1182
+ const old = ending as Exclude<UnitEnding, { kind: "idle" | "merged" | "already_landed" | "merge_ready" | "refused" }>;
1183
+ const grant = s.input.grant ?? DEFAULT_GRANT;
1184
+ // Unspent: an idle spends no renewal — the wake's segment does (this plan's
1185
+ // fifth unit) — so the row says what the grant still holds.
1186
+ const renewalsLeft = Math.max(0, grant.renewals - (s.input.session?.renewalsSpent ?? 0));
1187
+ // The head to continue from: a segment's own; a review_pending's reviewed
1188
+ // head — the pull request's, known even when the coding record carried none,
1189
+ // so the re-issue's resume-at-review fact survives the idle; else the last
1190
+ // coding child's recorded head.
1191
+ const from =
1192
+ old.kind === "continued"
1193
+ ? old.from
1194
+ : old.kind === "review_pending"
1195
+ ? (old.headSha ?? s.lastChildHead)
1196
+ : s.lastChildHead;
1197
+ // The last coding child's run: a segment's own, a dead coding child's own —
1198
+ // a review child that died names nothing here, the coding run before it does.
1199
+ const runId =
1200
+ old.kind === "continued" || (old.kind === "interrupted" && old.round.kind !== "review")
1201
+ ? old.runId
1202
+ : s.lastCodingRunId;
1203
+ const handoff = old.kind === "continued" ? old.handoff : s.lastCodingHandoff;
1204
+ const round = "round" in old ? old.round : undefined;
1205
+ return {
1206
+ kind: "idle",
1207
+ why: old.kind,
1208
+ idled: old,
1209
+ renewalsLeft,
1210
+ ...(from !== undefined ? { from } : {}),
1211
+ ...(runId !== undefined ? { runId } : {}),
1212
+ spendUsd: s.spendUsd,
1213
+ ...(handoff !== undefined ? { handoff } : {}),
1214
+ ...(round !== undefined ? { round } : {}),
1215
+ reviewRounds: s.reviewRounds,
1216
+ };
1121
1217
  }
1122
1218
 
1123
1219
  /** A gated finding as the gate note names it: `F1 (minor)`. */
@@ -1190,6 +1286,9 @@ function settleCoding(
1190
1286
  let next: UnitPipelineState = {
1191
1287
  ...s,
1192
1288
  spendUsd: addSpend(s.spendUsd, facts.costUsd),
1289
+ // The continuation facts an idle ending reads off the state (record 0051).
1290
+ ...(facts.headSha !== undefined ? { lastChildHead: facts.headSha } : {}),
1291
+ ...(facts.handoffLists !== undefined ? { lastCodingHandoff: facts.handoffLists } : {}),
1193
1292
  ...(facts.pr !== undefined ? { pr: { number: facts.pr.number, url: facts.pr.url } } : {}),
1194
1293
  ...(round.kind === "findings" && facts.dispositions !== undefined
1195
1294
  ? {
@@ -2242,5 +2341,9 @@ export function renderUnitReport(
2242
2341
  `🚫 The ${presetOf(e.round.kind)} child of round ${e.round.index} was refused by the authorize stage (${e.refusal})${e.message ? `: ${e.message}` : ""} — every child is authorized as the requesting user, so the pipeline ends here.`,
2243
2342
  reissue,
2244
2343
  ]);
2344
+ case "idle":
2345
+ // The old kind's sentence at this copy's level — the report is unchanged
2346
+ // by the idle at every verbosity (record 0051).
2347
+ return renderUnitReport({ ...s, ending: e.idled }, facts, verbosity);
2245
2348
  }
2246
2349
  }
@@ -100,6 +100,22 @@ export const profileSchema = z.object({
100
100
  .regex(/^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/, "an R2 bucket name: 3–63 lowercase letters, digits and hyphens"),
101
101
  })
102
102
  .optional(),
103
+ /** The run-metrics dataset (docs/reference/specs/run-metrics.md item 6), when the installation
104
+ * writes points: the state Worker's template binds it as `RUN_METRICS` with the name beside it
105
+ * in `RUN_METRICS_DATASET`, and `deploy plan` names the binding on the memory step. Nothing is
106
+ * created ahead of the deploy — the platform creates a dataset on first write. The bot's runtime
107
+ * config (`metrics.dataset`) must name the same dataset; the two are held together by the bot's
108
+ * boot warning, not by the profile. Analytics Engine's own dataset-name rules. */
109
+ metrics: z
110
+ .object({
111
+ dataset: z
112
+ .string()
113
+ .regex(
114
+ /^[A-Za-z_][A-Za-z0-9_]{0,63}$/,
115
+ "an Analytics Engine dataset name: 1–64 letters, digits and underscores, not starting with a digit",
116
+ ),
117
+ })
118
+ .optional(),
103
119
  });
104
120
 
105
121
  export type DeploymentProfile = z.infer<typeof profileSchema>;