@coreplane/switchboard 1.260.2 → 1.260.4

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 (129) hide show
  1. package/dist/assets/deploy/cloudflare-memory/backgroundTasks.ts +27 -3
  2. package/dist/assets/deploy/cloudflare-memory/testDiagnostics.ts +227 -0
  3. package/dist/assets/deploy/cloudflare-memory/testDiagnosticsProtocol.ts +22 -0
  4. package/dist/assets/deploy/cloudflare-memory/testDiagnosticsReporter.ts +88 -0
  5. package/dist/assets/deploy/cloudflare-memory/tsconfig.json +14 -2
  6. package/dist/assets/deploy/cloudflare-resident/Dockerfile +11 -2
  7. package/dist/assets/package-lock.json +3 -3
  8. package/dist/assets/package.json +4 -3
  9. package/dist/assets/project.json +4 -0
  10. package/dist/assets/source.json +3 -3
  11. package/dist/assets/src/agents/registry.ts +6 -5
  12. package/dist/assets/src/core/coordinator/contract.ts +48 -2
  13. package/dist/assets/src/core/coordinator/driver.ts +40 -5
  14. package/dist/assets/src/core/reviewVerdict.ts +15 -5
  15. package/dist/assets/src/core/runFriction.ts +1 -1
  16. package/dist/assets/src/core/ship/contract.ts +2 -2
  17. package/dist/assets/src/core/ship/coordinator.ts +236 -108
  18. package/dist/assets/src/core/ship/renewal.ts +3 -1
  19. package/dist/assets/src/execution/bashTimeout.ts +1 -1
  20. package/dist/assets/src/execution/sandboxErrors.ts +6 -6
  21. package/dist/assets/web/dist/.vite/manifest.json +488 -488
  22. package/dist/assets/web/dist/assets/{AppShell-CSXDCV4x.js → AppShell-DPJVhUGP.js} +1 -1
  23. package/dist/assets/web/dist/assets/{CostChart-DsuPS2g7.js → CostChart-4TeDdbvJ.js} +1 -1
  24. package/dist/assets/web/dist/assets/CostsPage-8HDgvuWB.js +1 -0
  25. package/dist/assets/web/dist/assets/{DeliveryPage-62qCSLvk.js → DeliveryPage-BaemWKW8.js} +1 -1
  26. package/dist/assets/web/dist/assets/{HomePage-Dk3HRBc3.js → HomePage-fgKQHLaO.js} +1 -1
  27. package/dist/assets/web/dist/assets/{InputMenu-BdZXnibG.js → InputMenu-BRV35UWP.js} +1 -1
  28. package/dist/assets/web/dist/assets/{MetricsPage-o5fSaCTe.js → MetricsPage-BQBuIuYU.js} +1 -1
  29. package/dist/assets/web/dist/assets/{NotFoundPage-DHuWyjlq.js → NotFoundPage-CDjMfCcj.js} +1 -1
  30. package/dist/assets/web/dist/assets/{PendingTurnRow-CeTGOXUy.js → PendingTurnRow-vuWiDzvz.js} +1 -1
  31. package/dist/assets/web/dist/assets/{PlanePage-RD0M6O6W.js → PlanePage-DXMJf8FB.js} +1 -1
  32. package/dist/assets/web/dist/assets/{ResidentDetailPage-Dl2KId95.js → ResidentDetailPage-DdC8yU_S.js} +1 -1
  33. package/dist/assets/web/dist/assets/{ResidentsIndexPage-DBy43I1R.js → ResidentsIndexPage-CSM-KODv.js} +1 -1
  34. package/dist/assets/web/dist/assets/{RunFoldRow-CwHDL-Mo.js → RunFoldRow-Csik3wD6.js} +1 -1
  35. package/dist/assets/web/dist/assets/{RunRoutePage-Du1HqLZs.js → RunRoutePage-jo6NZxsP.js} +3 -3
  36. package/dist/assets/web/dist/assets/{RunsIndexPage-BZphy3Hr.js → RunsIndexPage-Dx1Y4sSq.js} +1 -1
  37. package/dist/assets/web/dist/assets/{RunsTabs-kHbbxVHx.js → RunsTabs-C7Ua3XX5.js} +1 -1
  38. package/dist/assets/web/dist/assets/{ScheduledPage-B7jh898t.js → ScheduledPage-BuB_NZyR.js} +1 -1
  39. package/dist/assets/web/dist/assets/{SettingSelect-BfID5nF4.js → SettingSelect-CEeA4iuC.js} +1 -1
  40. package/dist/assets/web/dist/assets/{SettingsPage-3uLEsiiE.js → SettingsPage-Cn96jB6Q.js} +1 -1
  41. package/dist/assets/web/dist/assets/{SilentTurn-CKxSxnQC.js → SilentTurn-D2cJfpEq.js} +1 -1
  42. package/dist/assets/web/dist/assets/{SlackMark-CtiHrNjr.js → SlackMark-DB0MGiF5.js} +1 -1
  43. package/dist/assets/web/dist/assets/{StatusDot-x7vcK0iE.js → StatusDot-DDHUCPVJ.js} +1 -1
  44. package/dist/assets/web/dist/assets/{Tooltip-wVXCWhMp.js → Tooltip-B4VIHkAI.js} +1 -1
  45. package/dist/assets/web/dist/assets/{UnitRoutePage-V5BXK7FH.js → UnitRoutePage-C-2DfjLI.js} +1 -1
  46. package/dist/assets/web/dist/assets/{angular-html-C8b5YTHf.js → angular-html-BRRhsdQE.js} +1 -1
  47. package/dist/assets/web/dist/assets/{angular-ts-C3f4WYNe.js → angular-ts-BFj-mqCm.js} +1 -1
  48. package/dist/assets/web/dist/assets/{apl-DSHxUJc3.js → apl-CsBuy1IL.js} +1 -1
  49. package/dist/assets/web/dist/assets/{astro-BSGCCBP6.js → astro-cVaqxBZq.js} +1 -1
  50. package/dist/assets/web/dist/assets/{blade-DFeYrMKk.js → blade-MEP5mWha.js} +1 -1
  51. package/dist/assets/web/dist/assets/{browser-B6T5hjfR.js → browser-CCImen5O.js} +1 -1
  52. package/dist/assets/web/dist/assets/{c-L_QOnFVv.js → c-D4NGQ_Pd.js} +1 -1
  53. package/dist/assets/web/dist/assets/{chapel-DLQWUwhu.js → chapel-BkWImsIy.js} +1 -1
  54. package/dist/assets/web/dist/assets/{cobol-1DlMmrEO.js → cobol-RPDSPSET.js} +1 -1
  55. package/dist/assets/web/dist/assets/{coffee-BTB6os6N.js → coffee-DZ5z7oVX.js} +1 -1
  56. package/dist/assets/web/dist/assets/{cpp-BvJI2z3u.js → cpp-CrBtIaRZ.js} +1 -1
  57. package/dist/assets/web/dist/assets/{crystal-B11wGW_h.js → crystal-DJesYi6f.js} +1 -1
  58. package/dist/assets/web/dist/assets/{css-BiIWx5Pw.js → css-DO9mjau-.js} +1 -1
  59. package/dist/assets/web/dist/assets/{dist-C-XGnvM4.js → dist-Cf1CmCSK.js} +2 -2
  60. package/dist/assets/web/dist/assets/{edge-nSglEXaD.js → edge-C3VzbSW0.js} +1 -1
  61. package/dist/assets/web/dist/assets/{elixir-x7o8qMl8.js → elixir-BkD_Vhfe.js} +1 -1
  62. package/dist/assets/web/dist/assets/{elm-DGrRyqTQ.js → elm-KIM0mFPk.js} +1 -1
  63. package/dist/assets/web/dist/assets/{erb--_dh58Nw.js → erb-D2ruCqb2.js} +1 -1
  64. package/dist/assets/web/dist/assets/{eventSource-Bj_dZf6M.js → eventSource-Cv0AFlMi.js} +1 -1
  65. package/dist/assets/web/dist/assets/{git-rebase-DbpUXwyg.js → git-rebase-CnfbQTxA.js} +1 -1
  66. package/dist/assets/web/dist/assets/{glimmer-js-BG0CuZG5.js → glimmer-js-CbiwFXWH.js} +1 -1
  67. package/dist/assets/web/dist/assets/{glimmer-ts-CQIl5qyR.js → glimmer-ts-DV8ii0Pv.js} +1 -1
  68. package/dist/assets/web/dist/assets/{glsl-9PJNQTcw.js → glsl-CgmznWkA.js} +1 -1
  69. package/dist/assets/web/dist/assets/{graphql-CzVTzjEI.js → graphql-BK-bBYfH.js} +1 -1
  70. package/dist/assets/web/dist/assets/{hack-BhCmAz3Q.js → hack-DnwTTP5n.js} +1 -1
  71. package/dist/assets/web/dist/assets/{haml-BmI12COR.js → haml-DAVJsWwT.js} +1 -1
  72. package/dist/assets/web/dist/assets/{handlebars-Cib-ISTS.js → handlebars-voOVfxFL.js} +1 -1
  73. package/dist/assets/web/dist/assets/{html-D3lDapvI.js → html-3FTqwaPk.js} +1 -1
  74. package/dist/assets/web/dist/assets/{html-derivative-DRPY8vOK.js → html-derivative-CSlhLDQn.js} +1 -1
  75. package/dist/assets/web/dist/assets/{http-B9VttK6m.js → http-Vx4ilPfS.js} +1 -1
  76. package/dist/assets/web/dist/assets/{hurl-C0saNJYj.js → hurl-DxyZDE3R.js} +1 -1
  77. package/dist/assets/web/dist/assets/{java-BCFbtYAj.js → java-nWqdjNJG.js} +1 -1
  78. package/dist/assets/web/dist/assets/{javascript-nwBjnrhh.js → javascript-Dr82HOwp.js} +1 -1
  79. package/dist/assets/web/dist/assets/{jinja-CMYO9byN.js → jinja-BhjLrPOt.js} +1 -1
  80. package/dist/assets/web/dist/assets/{jison-Cko8HnE_.js → jison-DilGIUa8.js} +1 -1
  81. package/dist/assets/web/dist/assets/{json-Dzv__YsX.js → json-CrOcvmhJ.js} +1 -1
  82. package/dist/assets/web/dist/assets/{jsx-CTFxagih.js → jsx-BW_RX-fo.js} +1 -1
  83. package/dist/assets/web/dist/assets/{julia-_Xvten1-.js → julia-CWH071DB.js} +1 -1
  84. package/dist/assets/web/dist/assets/{just-bLps4qOu.js → just-DkhhoWBy.js} +1 -1
  85. package/dist/assets/web/dist/assets/{latex-D6UaYsUP.js → latex-cuHJ4TUT.js} +1 -1
  86. package/dist/assets/web/dist/assets/{liquid-CoDIeULa.js → liquid-DG6afYQN.js} +1 -1
  87. package/dist/assets/web/dist/assets/{lua-D1KFRnVf.js → lua-C5CMnCPS.js} +1 -1
  88. package/dist/assets/web/dist/assets/{main-DAL--hyj.js → main-DxPqaFu9.js} +3 -3
  89. package/dist/assets/web/dist/assets/{marko-B5N5XcOw.js → marko-Dy2_DuC4.js} +1 -1
  90. package/dist/assets/web/dist/assets/{mdc-uxzjQO3q.js → mdc-BMKb_EeA.js} +1 -1
  91. package/dist/assets/web/dist/assets/{nginx-BMJxs2TK.js → nginx-og0RgMKf.js} +1 -1
  92. package/dist/assets/web/dist/assets/{nim-DqQ3-8p1.js → nim-CQxTQQP0.js} +1 -1
  93. package/dist/assets/web/dist/assets/{org-DsY6LK59.js → org-b5lkq_Pl.js} +1 -1
  94. package/dist/assets/web/dist/assets/{perl--pq9iJFp.js → perl-D9X5_i_J.js} +1 -1
  95. package/dist/assets/web/dist/assets/{php-RHCMbsM2.js → php-CwUxvLX0.js} +1 -1
  96. package/dist/assets/web/dist/assets/{pug-CFNq_FCp.js → pug-uOIOExr0.js} +1 -1
  97. package/dist/assets/web/dist/assets/{qml-C9Acx9E8.js → qml-g5RoADf-.js} +1 -1
  98. package/dist/assets/web/dist/assets/{r-DxlU8FBd.js → r-5LdAopD3.js} +1 -1
  99. package/dist/assets/web/dist/assets/{razor-CLJ2kKih.js → razor-DwrvFLYQ.js} +1 -1
  100. package/dist/assets/web/dist/assets/{regexp-CzmdGoEp.js → regexp-dGtAaxC-.js} +1 -1
  101. package/dist/assets/web/dist/assets/{rst-D9yuxBET.js → rst-wFdW0brr.js} +1 -1
  102. package/dist/assets/web/dist/assets/{ruby-CIRcEvnD.js → ruby-DPglN6I9.js} +1 -1
  103. package/dist/assets/web/dist/assets/{sas-LOl-uUPh.js → sas-CUbCs40d.js} +1 -1
  104. package/dist/assets/web/dist/assets/{scss-BX2_sZHY.js → scss-B-DUiI1c.js} +1 -1
  105. package/dist/assets/web/dist/assets/{shellscript-4d-QvKkY.js → shellscript-lGOV7Ram.js} +1 -1
  106. package/dist/assets/web/dist/assets/{shellsession-DhB16UGy.js → shellsession-BDWtAAjr.js} +1 -1
  107. package/dist/assets/web/dist/assets/{soy-Cm1xTUBr.js → soy-zod_dcOy.js} +1 -1
  108. package/dist/assets/web/dist/assets/{sql-BsfcBJdu.js → sql-BG0HSSoM.js} +1 -1
  109. package/dist/assets/web/dist/assets/{sseReplay-BqgOggyQ.js → sseReplay-BzoeVxZB.js} +1 -1
  110. package/dist/assets/web/dist/assets/{stata-Dwat3BSw.js → stata-BVUMLp1W.js} +1 -1
  111. package/dist/assets/web/dist/assets/{surrealql-P4rtxzAW.js → surrealql-DiSUbHKF.js} +1 -1
  112. package/dist/assets/web/dist/assets/{svelte-DTcsi8S8.js → svelte-DZenT45Y.js} +1 -1
  113. package/dist/assets/web/dist/assets/{templ-D-kP0Tg5.js → templ-BWKRNM0-.js} +1 -1
  114. package/dist/assets/web/dist/assets/{tex-D-4vuedK.js → tex-CbMzBlkK.js} +1 -1
  115. package/dist/assets/web/dist/assets/{ts-tags-jEEWYxsu.js → ts-tags-CZsVorDN.js} +1 -1
  116. package/dist/assets/web/dist/assets/{tsx-CFnlrVzT.js → tsx-R1r3mH4W.js} +1 -1
  117. package/dist/assets/web/dist/assets/{twig-D5rGrPjs.js → twig-BjPMyjKH.js} +1 -1
  118. package/dist/assets/web/dist/assets/{typescript-nYIQ35or.js → typescript-Bx-66VN_.js} +1 -1
  119. package/dist/assets/web/dist/assets/{typst-3-UX8dna.js → typst-fW037Eo6.js} +1 -1
  120. package/dist/assets/web/dist/assets/{vue-CLIPIX03.js → vue-D1V_GNxW.js} +1 -1
  121. package/dist/assets/web/dist/assets/{vue-html-Ci9CpkHU.js → vue-html-C-4pTZ-s.js} +1 -1
  122. package/dist/assets/web/dist/assets/{vue-vine-DL5pEYYk.js → vue-vine-C-QsX3hR.js} +1 -1
  123. package/dist/assets/web/dist/assets/{wallClock-DmaC1sKA.js → wallClock-x0hjr0J_.js} +1 -1
  124. package/dist/assets/web/dist/assets/{xml-bO80qikC.js → xml-BJTJXPgC.js} +1 -1
  125. package/dist/assets/web/dist/assets/{xsl-BPze60Ej.js → xsl-Xc9lukqe.js} +1 -1
  126. package/dist/assets/web/dist/assets/{yaml-BQUSon_o.js → yaml-Dw-FMsQa.js} +1 -1
  127. package/dist/cli.js +505 -253
  128. package/package.json +1 -1
  129. package/dist/assets/web/dist/assets/CostsPage-BaeWnm-o.js +0 -1
@@ -59,6 +59,7 @@ import { parsePlanUnit, planUnitIds } from "./contract.js";
59
59
  import {
60
60
  carve,
61
61
  DEFAULT_GRANT,
62
+ IDLE_DAYS_MAX,
62
63
  loopPosition,
63
64
  MERGE_WAIT_ASK_MINUTES,
64
65
  MINUTE_MS,
@@ -136,11 +137,11 @@ export function shipInterruptedNote(prUrl?: string, cause?: InterruptionCause, i
136
137
  const stands = prUrl
137
138
  ? `Its work stands on GitHub: ${prUrl}.`
138
139
  : "Whatever it pushed stands on its pipeline branch; no PR was opened yet.";
139
- const reissue = idle
140
- ? "To continue, reply in this thread to continue."
140
+ const continuation = idle
141
+ ? "The next reply in this thread continues the unit."
141
142
  : prUrl
142
- ? `To continue the review loop, re-issue \`agent:ship\` in this thread with only the PR URL (${prUrl}).`
143
- : "To continue, re-issue `agent:ship` in this thread with the task — round 0 runs again on the same branch.";
143
+ ? `The pipeline kept its task, branch and pull request; the next reply in this thread continues the review loop at ${prUrl}.`
144
+ : "The pipeline kept its task and branch; the next reply in this thread starts round 0 again on that branch.";
144
145
  const opening =
145
146
  cause === "container_replaced"
146
147
  ? "⚠️ 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."
@@ -149,7 +150,7 @@ export function shipInterruptedNote(prUrl?: string, cause?: InterruptionCause, i
149
150
  : cause === "bot_restart"
150
151
  ? "⚠️ The bot restarted while this ship pipeline was running, so the pipeline stopped."
151
152
  : "⚠️ This ship pipeline's child was interrupted and could not resume, so the pipeline stopped.";
152
- return `${opening} ${stands} ${reissue}`;
153
+ return `${opening} ${stands} ${continuation}`;
153
154
  }
154
155
 
155
156
  // ---- the plan graph --------------------------------------------------------------------------------
@@ -494,8 +495,10 @@ export type Brief =
494
495
  pr: number;
495
496
  headSha?: string;
496
497
  round: number;
497
- /** The previous review round's run and the coding run that answered its findings, for a re-review. */
498
- prior?: { reviewRunId: string; codingRunId?: string };
498
+ /** The previous review round and the coding run that answered it. A
499
+ * recovered posted verdict carries findings by value when no run row
500
+ * belongs to this attempt. */
501
+ prior?: { reviewRunId?: string; codingRunId?: string; findings?: Finding[] };
499
502
  /** The previous round's check findings (record 0055): they sit on no
500
503
  * run's record, so the brief carries them beside `prior` by value. */
501
504
  checks?: Finding[];
@@ -504,7 +507,12 @@ export type Brief =
504
507
  kind: "findings";
505
508
  unit: string;
506
509
  pr: number;
507
- reviewRunId: string;
510
+ /** The review run normally supplies the findings. An adopted attempt
511
+ * can recover them from the last posted verdict marker instead. */
512
+ reviewRunId?: string;
513
+ findings?: Finding[];
514
+ /** Human replies that answered a parked finding, attributed at intake. */
515
+ answers?: string[];
508
516
  /** The round's check findings (record 0055), carried by value — the
509
517
  * review run's record holds only the reviewer's own findings. */
510
518
  checks?: Finding[];
@@ -522,6 +530,20 @@ export interface PrRef {
522
530
  url: string;
523
531
  }
524
532
 
533
+ /** A human-gated review question parked on its live unit. The review's typed
534
+ * findings and the pull request survive the wait; `reviewRunId` is absent when
535
+ * an adopted attempt recovered the question from GitHub's verdict marker. */
536
+ export interface HumanGatePending {
537
+ pr: PrRef;
538
+ round: number;
539
+ findings: Finding[];
540
+ verdict: "approve" | "request_changes";
541
+ reviewRunId?: string;
542
+ headSha?: string;
543
+ /** When the verdict asked the question, for fencing older unit events. */
544
+ askedAt?: number;
545
+ }
546
+
525
547
  /** What the coordinator asks for next. `step` is the Workflow step's name, and
526
548
  * for a spawn the second half of the child's idempotency key. */
527
549
  export type CoordinatorAction =
@@ -577,6 +599,10 @@ export type ChildFacts =
577
599
  | {
578
600
  finished: true;
579
601
  status: RunStatus;
602
+ /** The terminal record time; fallback fence for an older review record. */
603
+ finishedAt?: number;
604
+ /** When the posted verdict became the person's question. */
605
+ reviewAskedAt?: number;
580
606
  finalReply?: string;
581
607
  /** A coding child's `pr_opened`. */
582
608
  pr?: { number: number; url: string; created: boolean };
@@ -659,6 +685,16 @@ export type PrCheck =
659
685
  * read beside the checks on the ending's facts read, so a `merge_ready`
660
686
  * report tells the person their merge is queued, never a direct one. */
661
687
  baseHasMergeQueue?: boolean;
688
+ /** A person's newest pull-request comment answered the last posted
689
+ * human-gated verdict. Entry starts its fix round before any review. */
690
+ humanGate?: {
691
+ round: number;
692
+ findings: Finding[];
693
+ verdict: "approve" | "request_changes";
694
+ answer: string;
695
+ author: string;
696
+ commentId: string;
697
+ };
662
698
  }
663
699
  | { state: "merged"; prNumber: number; url: string; sha: string; mergedAt: string };
664
700
 
@@ -793,12 +829,9 @@ export type UnitEnding =
793
829
  * The unit is done and its dependents start on a base that carries it. */
794
830
  | { kind: "already_landed"; landed: HandoffLanded[]; round: RoundRef; runId: string; reviewRounds: number }
795
831
  | { kind: "merge_ready"; pr: PrRef; reviewRounds: number }
796
- /** Every finding the round would act on is human-gated — a receipt only a
797
- * person can produce (issue 1990; the reviewer set the flag through
798
- * `submit_verdict`, agent-review item 5) — so a fix round could change
799
- * nothing: the unit ends held for a person, the ending carrying the
800
- * human-gated rows, and a re-issue with the pull request resumes at the
801
- * review round once the receipt is posted (item 10's resume path). */
832
+ /** The report nested inside a parked human-gated question. The unit never
833
+ * publishes this as its terminal kind: `parkHumanGate` carries it under an
834
+ * idle ending until a person's answer opens the fix round. */
802
835
  | {
803
836
  kind: "held";
804
837
  cause?: undefined;
@@ -938,6 +971,10 @@ export type UnitEnding =
938
971
  handoff?: Handoff;
939
972
  round?: RoundRef;
940
973
  reviewRounds: number;
974
+ /** A parked human-gated review is always an idle, independent of the
975
+ * optional idle policy. Its question is the unit's pending state. */
976
+ humanGate?: HumanGatePending;
977
+ parkDays?: number;
941
978
  };
942
979
 
943
980
  /** The kinds that idle: every ending but the ended ones — the unit is
@@ -945,10 +982,9 @@ export type UnitEnding =
945
982
  * a reply could continue it. `merged`, `already_landed`, `merge_ready` and
946
983
  * `refused` never idle: the first two are done, merge-ready waits only for a
947
984
  * person's merge, and a refused child would be refused again. A gated or
948
- * draft `held` never idles either — it waits only for a person's receipt at
949
- * the pull request, which no thread reply can produce — but a blocked hold
950
- * (issue 2086) waits for exactly the person's word the idle's wake carries,
951
- * so it alone idles; `idleEnding` draws that line by cause. */
985
+ * draft `held` never idles either. A blocked hold (issue 2086) follows the
986
+ * configured idle policy; a human-gated hold takes `parkHumanGate` instead,
987
+ * which always creates the pending idle question. */
952
988
  export type IdleWhy =
953
989
  Exclude<UnitEnding["kind"], "idle" | "merged" | "already_landed" | "merge_ready" | "refused"> | RenewalWhy;
954
990
 
@@ -1014,6 +1050,9 @@ export interface LeaseSegmentProgress {
1014
1050
  previousHandoff?: Handoff;
1015
1051
  /** Words that woke an idle segment, already attributed in arrival order. */
1016
1052
  texts?: string[];
1053
+ /** The human-gated question those words answer. It resumes as a findings
1054
+ * round before another review, without spending a renewal. */
1055
+ humanGate?: HumanGatePending;
1017
1056
  /** A stopped segment reopens under the remainder of its cut lease. Each
1018
1057
  * reopen gets a fresh attempt so its Workflow steps cannot replay the
1019
1058
  * stopped attempt's cached answers. */
@@ -1165,6 +1204,8 @@ export interface UnitPipelineState {
1165
1204
  * ids are unique within one round only. A disposition naming an id the review
1166
1205
  * never issued is dropped at the match (`matchDispositions`). */
1167
1206
  readonly findingsByRound: Readonly<Record<number, Finding[]>>;
1207
+ /** Human answers folded into a recovered/parked findings brief. */
1208
+ readonly humanAnswersByRound: Readonly<Record<number, string[]>>;
1168
1209
  readonly dispositionsByRound: Readonly<Record<number, FindingDisposition[]>>;
1169
1210
  readonly reviewRunByRound: Readonly<Record<number, string>>;
1170
1211
  /** The coding run each round's findings step dispatched. */
@@ -1208,20 +1249,29 @@ export const BUSY_RETRY_MS = SHIP_WAIT.busyRetryMinutes * MIN;
1208
1249
  // ---- the unit pipeline: opening and the next action -----------------------------------------------
1209
1250
 
1210
1251
  export function openUnitPipeline(input: UnitPipelineInput, at: number): UnitPipelineState {
1252
+ const pending = input.session?.humanGate;
1211
1253
  const base: UnitPipelineState = {
1212
1254
  input,
1213
1255
  startedAt: at,
1214
1256
  clock: at,
1215
1257
  phase: { at: "pre-check" },
1216
- reviewRounds: 0,
1258
+ reviewRounds: pending?.round ?? 0,
1217
1259
  rebaseAttempts: 0,
1218
1260
  spentMs: { coding: 0, review: 0, waiting: 0 },
1219
1261
  spendUsd: input.session?.spendUsd ?? 0,
1220
- findingsByRound: {},
1262
+ findingsByRound: pending ? { [pending.round]: pending.findings } : {},
1263
+ humanAnswersByRound: pending && input.session?.texts !== undefined ? { [pending.round]: input.session.texts } : {},
1221
1264
  dispositionsByRound: {},
1222
- reviewRunByRound: {},
1265
+ reviewRunByRound: pending?.reviewRunId !== undefined ? { [pending.round]: pending.reviewRunId } : {},
1223
1266
  findingsRunByRound: {},
1267
+ ...(pending !== undefined
1268
+ ? {
1269
+ pr: pending.pr,
1270
+ ...(pending.headSha !== undefined ? { lastReviewHead: pending.headSha } : {}),
1271
+ }
1272
+ : {}),
1224
1273
  };
1274
+ if (pending !== undefined) return enterRound(base, { index: pending.round, kind: "findings" }).state;
1225
1275
  if (!input.resume) return base;
1226
1276
  const url = input.resume.url ?? `https://github.com/${input.repo}/pull/${input.resume.pr}`;
1227
1277
  const resumed: UnitPipelineState = {
@@ -1294,26 +1344,40 @@ function briefFor(s: UnitPipelineState, round: RoundRef): Brief {
1294
1344
  return { kind: "rebase", unit, pr, headSha: s.lastReviewHead!, base: s.input.base };
1295
1345
  }
1296
1346
  if (round.kind === "findings") {
1297
- const checks = checkFindingsOf(s.findingsByRound[round.index]);
1347
+ const issued = s.findingsByRound[round.index] ?? [];
1348
+ const checks = checkFindingsOf(issued);
1349
+ const reviewRunId = s.reviewRunByRound[round.index];
1350
+ const answers = s.humanAnswersByRound[round.index];
1298
1351
  return {
1299
1352
  kind: "findings",
1300
1353
  unit,
1301
1354
  pr,
1302
- reviewRunId: s.reviewRunByRound[round.index]!,
1355
+ ...(reviewRunId !== undefined ? { reviewRunId } : {}),
1356
+ ...(reviewRunId === undefined || (answers !== undefined && answers.length > 0) ? { findings: issued } : {}),
1357
+ ...(answers !== undefined && answers.length > 0 ? { answers } : {}),
1303
1358
  ...(checks.length > 0 ? { checks } : {}),
1304
1359
  };
1305
1360
  }
1361
+ const priorFindings = s.findingsByRound[round.index - 1] ?? [];
1306
1362
  const priorReview = s.reviewRunByRound[round.index - 1];
1307
1363
  const priorCoding = s.findingsRunByRound[round.index - 1];
1308
- const priorChecks = checkFindingsOf(s.findingsByRound[round.index - 1]);
1364
+ const priorChecks = checkFindingsOf(priorFindings);
1309
1365
  return {
1310
1366
  kind: "review",
1311
1367
  unit,
1312
1368
  pr,
1313
1369
  ...(s.lastReviewHead !== undefined ? { headSha: s.lastReviewHead } : {}),
1314
1370
  round: round.index,
1315
- ...(priorReview !== undefined
1316
- ? { prior: { reviewRunId: priorReview, ...(priorCoding !== undefined ? { codingRunId: priorCoding } : {}) } }
1371
+ ...(priorReview !== undefined || (priorCoding !== undefined && priorFindings.length > 0)
1372
+ ? {
1373
+ prior: {
1374
+ ...(priorReview !== undefined ? { reviewRunId: priorReview } : {}),
1375
+ ...(priorReview === undefined || s.humanAnswersByRound[round.index - 1] !== undefined
1376
+ ? { findings: priorFindings }
1377
+ : {}),
1378
+ ...(priorCoding !== undefined ? { codingRunId: priorCoding } : {}),
1379
+ },
1380
+ }
1317
1381
  : {}),
1318
1382
  ...(priorChecks.length > 0 ? { checks: priorChecks } : {}),
1319
1383
  };
@@ -1454,9 +1518,9 @@ function end(s: UnitPipelineState, ending: UnitEnding, notes: CoordinatorNote[]
1454
1518
  function idleEnding(s: UnitPipelineState, ending: UnitEnding): Extract<UnitEnding, { kind: "idle" }> | undefined {
1455
1519
  if ((s.input.idleDays ?? 0) <= 0) return undefined;
1456
1520
  if (NEVER_IDLES.has(ending.kind)) return undefined;
1457
- // A gated or draft hold waits only for a person's receipt at the pull
1458
- // request — no thread reply can continue it — while a blocked hold (issue
1459
- // 2086) waits for exactly the person's word the wake carries, so it idles.
1521
+ // A draft hold is not continued by a reply. A blocked hold (issue 2086)
1522
+ // waits for the person's word. Human-gated questions bypass this mapping:
1523
+ // `parkHumanGate` always parks them with their typed pending state.
1460
1524
  if (ending.kind === "held" && ending.cause !== "blocked") return undefined;
1461
1525
  const old = ending as Exclude<UnitEnding, { kind: "idle" | "merged" | "already_landed" | "merge_ready" | "refused" }>;
1462
1526
  const grant = s.input.grant ?? DEFAULT_GRANT;
@@ -1512,7 +1576,7 @@ const heldEnding = (
1512
1576
  round: RoundRef,
1513
1577
  findings: Finding[],
1514
1578
  verdict: "approve" | "request_changes",
1515
- ): UnitEnding => ({
1579
+ ): Extract<UnitEnding, { kind: "held" }> => ({
1516
1580
  kind: "held",
1517
1581
  ...(s.pr !== undefined ? { pr: s.pr } : {}),
1518
1582
  round,
@@ -1521,6 +1585,48 @@ const heldEnding = (
1521
1585
  reviewRounds: s.reviewRounds,
1522
1586
  });
1523
1587
 
1588
+ /** A human-gated finding is a question, not a terminal hold. Park the live
1589
+ * unit in record 0051's idle state regardless of the optional idle policy, so
1590
+ * either a thread reply or a pull-request comment can become its fix brief. */
1591
+ function parkHumanGate(
1592
+ s: UnitPipelineState,
1593
+ round: RoundRef,
1594
+ findings: Finding[],
1595
+ verdict: "approve" | "request_changes",
1596
+ askedAt: number,
1597
+ notes: CoordinatorNote[],
1598
+ ): Transition {
1599
+ const held = heldEnding(s, round, findings, verdict);
1600
+ const humanGate: HumanGatePending = {
1601
+ pr: s.pr!,
1602
+ round: round.index,
1603
+ findings,
1604
+ verdict,
1605
+ ...(s.reviewRunByRound[round.index] !== undefined ? { reviewRunId: s.reviewRunByRound[round.index] } : {}),
1606
+ ...(s.lastReviewHead !== undefined ? { headSha: s.lastReviewHead } : {}),
1607
+ askedAt,
1608
+ };
1609
+ const grant = s.input.grant ?? DEFAULT_GRANT;
1610
+ const ending: Extract<UnitEnding, { kind: "idle" }> = {
1611
+ kind: "idle",
1612
+ why: "held",
1613
+ idled: held,
1614
+ renewalsLeft: Math.max(0, grant.renewals - (s.input.session?.renewalsSpent ?? 0)),
1615
+ ...(s.lastReviewHead !== undefined ? { from: s.lastReviewHead } : {}),
1616
+ ...(s.lastCodingRunId !== undefined ? { runId: s.lastCodingRunId } : {}),
1617
+ spendUsd: s.spendUsd,
1618
+ ...(s.lastCodingHandoff !== undefined ? { handoff: s.lastCodingHandoff } : {}),
1619
+ round,
1620
+ reviewRounds: s.reviewRounds,
1621
+ humanGate,
1622
+ parkDays: IDLE_DAYS_MAX,
1623
+ };
1624
+ return {
1625
+ state: { ...s, phase: ENDED, ending },
1626
+ notes: [...notes, { type: "ended", ending }],
1627
+ };
1628
+ }
1629
+
1524
1630
  const roundNote = (round: RoundRef, outcome: ShipRoundOutcome): RoundNote => ({
1525
1631
  type: "round",
1526
1632
  index: round.index,
@@ -1564,10 +1670,11 @@ function enterRound(s: UnitPipelineState, round: RoundRef, notes: CoordinatorNot
1564
1670
  };
1565
1671
  }
1566
1672
 
1567
- /** A rebase changed the approved patch, so review is mandatory even when
1568
- * the ordinary findings-round count is spent. The run's remaining lease is
1569
- * the bound: `enterRound` refuses when another review no longer fits. */
1570
- function reviewAfterRebase(s: UnitPipelineState, notes: CoordinatorNote[] = []): Transition {
1673
+ /** A rebase changed the approved patch, or a person's answer enabled a
1674
+ * human-gated fix, so review is mandatory even when the ordinary findings-
1675
+ * round count is spent. The run's remaining lease is the bound: `enterRound`
1676
+ * refuses when another review no longer fits. */
1677
+ function mandatoryReview(s: UnitPipelineState, notes: CoordinatorNote[] = []): Transition {
1571
1678
  return enterRound(s, { index: s.reviewRounds + 1, kind: "review" }, notes);
1572
1679
  }
1573
1680
 
@@ -1634,18 +1741,21 @@ function settleCoding(
1634
1741
  [roundNote(round, "stopped")],
1635
1742
  );
1636
1743
  // A mechanical WIP push means the child did not declare the work ready for
1637
- // review. Keep the unit branch as the resumption point and abort this
1638
- // attempt; an ordinary push still takes the established recover-PR path.
1639
- if (facts.status === "failed" && checkpoint !== undefined) {
1744
+ // review, whatever terminal status its own answer produced. Keep the unit
1745
+ // branch as the resumption point and abort this attempt; an ordinary push
1746
+ // still takes the established recover-PR path.
1747
+ if (checkpoint !== undefined) {
1640
1748
  const cause =
1641
1749
  facts.failure?.kind === "provider_transient"
1642
1750
  ? "the model provider's transport retry budget was spent"
1643
- : "it failed before finishing";
1751
+ : facts.status === "failed"
1752
+ ? "it failed before finishing"
1753
+ : "it ended before its work was ready for review";
1644
1754
  return end(
1645
1755
  next,
1646
1756
  {
1647
1757
  kind: "aborted",
1648
- reason: `⚠️ The coding child ended because ${cause}; the branch carries the interrupted work at \`${checkpoint.sha.slice(0, 7)}\` on \`${checkpoint.branch}\`. Re-issue the request to resume from it instead of starting over.`,
1758
+ reason: `⚠️ The coding child ended because ${cause}; the branch carries the interrupted work at \`${checkpoint.sha.slice(0, 7)}\` on \`${checkpoint.branch}\`. The next reply in this thread resumes from that checkpoint instead of starting over.`,
1649
1759
  round,
1650
1760
  reviewRounds: next.reviewRounds,
1651
1761
  },
@@ -1766,9 +1876,10 @@ function settleReview(
1766
1876
  },
1767
1877
  notes,
1768
1878
  );
1769
- // Every gated finding is human-gated (issue 1990): a fix round could
1770
- // change nothing, so the unit ends held for a person instead.
1771
- if (allHumanGated(gated)) return end(next, heldEnding(next, round, gated, "approve"), notes);
1879
+ // Every gated finding is human-gated: ask a person and park the live
1880
+ // unit; an answer later opens the fix round with the question attached.
1881
+ if (allHumanGated(gated))
1882
+ return parkHumanGate(next, round, gated, "approve", facts.reviewAskedAt ?? facts.finishedAt ?? s.clock, notes);
1772
1883
  if (next.reviewRounds >= next.input.caps.maxRounds)
1773
1884
  return end(
1774
1885
  next,
@@ -1797,11 +1908,18 @@ function settleReview(
1797
1908
  },
1798
1909
  notes,
1799
1910
  );
1800
- // Every finding of the round is human-gated (issue 1990): no fix round can
1801
- // change anything, so the unit ends held for a person's receipt instead of
1802
- // spending a coding child — or the round cap — on it.
1911
+ // Every finding of the round is human-gated: no fix round can change
1912
+ // anything yet, so park the live unit for a person's answer before asking
1913
+ // the round cap.
1803
1914
  if (allHumanGated(verdict.findings ?? []))
1804
- return end(next, heldEnding(next, round, verdict.findings ?? [], "request_changes"), notes);
1915
+ return parkHumanGate(
1916
+ next,
1917
+ round,
1918
+ verdict.findings ?? [],
1919
+ "request_changes",
1920
+ facts.reviewAskedAt ?? facts.finishedAt ?? s.clock,
1921
+ notes,
1922
+ );
1805
1923
  if (next.reviewRounds >= next.input.caps.maxRounds)
1806
1924
  return end(
1807
1925
  next,
@@ -2303,12 +2421,14 @@ function roundOnOpenPr(
2303
2421
  },
2304
2422
  [roundNote(round, "aborted")],
2305
2423
  );
2306
- return reviewAfterRebase({ ...next, lastReviewHead: pushed }, [roundNote(round, "pr_opened")]);
2424
+ return mandatoryReview({ ...next, lastReviewHead: pushed }, [roundNote(round, "pr_opened")]);
2307
2425
  }
2426
+ const hasHumanAnswer = round.kind === "findings" && (s.humanAnswersByRound[round.index]?.length ?? 0) > 0;
2308
2427
  if (round.kind === "findings") {
2309
2428
  // Nothing repushed → nothing to re-review, unless every finding of the
2310
2429
  // last review was declined on the record: that re-review verifies the
2311
- // arguments and may concede.
2430
+ // arguments and may concede. A person's answer is itself new evidence for
2431
+ // the mandatory re-review, even when accepting it changes no code.
2312
2432
  const codingHead = normalizeHead(head);
2313
2433
  const reviewedAt = normalizeHead(s.lastReviewHead);
2314
2434
  if (codingHead !== undefined && reviewedAt !== undefined && sameCommit(codingHead, reviewedAt)) {
@@ -2339,27 +2459,30 @@ function roundOnOpenPr(
2339
2459
  },
2340
2460
  [roundNote(round, "aborted")],
2341
2461
  );
2342
- const findings = s.findingsByRound[round.index] ?? [];
2343
- const dispositions = s.dispositionsByRound[round.index] ?? [];
2344
- const allDeclined =
2345
- findings.length > 0 &&
2346
- findings.every((f) => dispositions.find((d) => d.findingId === f.id)?.disposition === "declined");
2347
- if (!allDeclined)
2348
- return end(
2349
- next,
2350
- {
2351
- kind: "aborted",
2352
- reason: `⚠️ Round ${round.index}'s findings step produced no new head — the branch still sits at \`${codingHead.slice(0, 7)}\`, the commit the review already read, and not every finding was declined on the record, so there is nothing new to re-review.`,
2353
- round,
2354
- reviewRounds: next.reviewRounds,
2355
- },
2356
- [roundNote(round, "aborted")],
2357
- );
2462
+ if (!hasHumanAnswer) {
2463
+ const findings = s.findingsByRound[round.index] ?? [];
2464
+ const dispositions = s.dispositionsByRound[round.index] ?? [];
2465
+ const allDeclined =
2466
+ findings.length > 0 &&
2467
+ findings.every((f) => dispositions.find((d) => d.findingId === f.id)?.disposition === "declined");
2468
+ if (!allDeclined)
2469
+ return end(
2470
+ next,
2471
+ {
2472
+ kind: "aborted",
2473
+ reason: `⚠️ Round ${round.index}'s findings step produced no new head — the branch still sits at \`${codingHead.slice(0, 7)}\`, the commit the review already read, and not every finding was declined on the record, so there is nothing new to re-review.`,
2474
+ round,
2475
+ reviewRounds: next.reviewRounds,
2476
+ },
2477
+ [roundNote(round, "aborted")],
2478
+ );
2479
+ }
2358
2480
  }
2359
2481
  }
2360
- return nextReview({ ...next, ...(head !== undefined ? { lastReviewHead: head } : {}) }, [
2361
- roundNote(round, "pr_opened"),
2362
- ]);
2482
+ const reviewed: UnitPipelineState = { ...next, ...(head !== undefined ? { lastReviewHead: head } : {}) };
2483
+ const notes = [roundNote(round, "pr_opened")];
2484
+ if (hasHumanAnswer) return mandatoryReview(reviewed, notes);
2485
+ return nextReview(reviewed, notes);
2363
2486
  }
2364
2487
 
2365
2488
  /** Move the clock and charge the elapsed time to the budget's bucket: a phase
@@ -2421,6 +2544,20 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
2421
2544
  pr: { number: r.pr.prNumber, url: r.pr.url },
2422
2545
  lastReviewHead: entryHead,
2423
2546
  };
2547
+ const answered = r.pr.humanGate;
2548
+ if (answered !== undefined) {
2549
+ const round = Math.max(1, answered.round);
2550
+ const withAnswer: UnitPipelineState = {
2551
+ ...adopted,
2552
+ reviewRounds: round,
2553
+ findingsByRound: { ...adopted.findingsByRound, [round]: answered.findings },
2554
+ humanAnswersByRound: {
2555
+ ...adopted.humanAnswersByRound,
2556
+ [round]: [`${answered.author}: ${answered.answer}`],
2557
+ },
2558
+ };
2559
+ return enterRound(withAnswer, { index: round, kind: "findings" });
2560
+ }
2424
2561
  const checks = r.pr.checks;
2425
2562
  const green =
2426
2563
  checks !== undefined && checks.total > 0 && checks.failed.length === 0 && checks.pending.length === 0;
@@ -2733,7 +2870,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
2733
2870
  reviewRounds: s.reviewRounds,
2734
2871
  });
2735
2872
  const rebased: UnitPipelineState = { ...clocked, lastReviewHead: r.headSha };
2736
- if (r.outcome === "changed") return reviewAfterRebase(rebased);
2873
+ if (r.outcome === "changed") return mandatoryReview(rebased);
2737
2874
  return enterChecks(rebased, { index: s.reviewRounds, kind: "review" }, []);
2738
2875
  }
2739
2876
  case "merge-wait":
@@ -2785,7 +2922,7 @@ function dispositionFor(s: UnitPipelineState, finding: Finding, round: number):
2785
2922
  function writeUpPointer(s: UnitPipelineState, kind: RoundKind, runId: string | undefined): string | undefined {
2786
2923
  if (runId === undefined) return undefined;
2787
2924
  const base = s.input.runPageBase;
2788
- const at = base !== undefined ? `${base.replace(/\/+$/, "")}/${encodeURIComponent(runId)}` : `run ${runId}`;
2925
+ const at = base !== undefined ? `${base.replace(/\/+$/, "")}/${encodeURIComponent(runId)}` : `the run ${runId}`;
2789
2926
  return `The ${presetOf(kind)} child's write-up is its own message above in this thread — the single copy of the detail (${at}).`;
2790
2927
  }
2791
2928
 
@@ -2899,9 +3036,9 @@ export function renderUnitReport(
2899
3036
  // The re-issue line keys on the instance's mark, never on who merges: a
2900
3037
  // generated plan is re-issued with the request's own text, a seeded one by
2901
3038
  // its plan path — and a seeded plan can be a person's merge too.
2902
- const reissue = s.input.generated
2903
- ? `To continue, re-issue \`agent:ship\` in this thread with the same text${prUrl ? ` and include the PR URL (${prUrl})` : " — include the PR URL if a PR exists"}.`
2904
- : `The unit's dependents in this plan stay blocked; the unit runs again when the plan is re-issued.`;
3039
+ const continuation = s.input.generated
3040
+ ? `The pipeline kept this unit's task and branch; the next reply in this thread continues it${prUrl ? ` from the open pull request (${prUrl})` : " from the branch head"}.`
3041
+ : `The unit's dependents in this plan stay blocked; the next run of this plan recognizes the unit's branch and pull request.`;
2905
3042
  const declined = [...(s.dispositionsByRound[e.reviewRounds - 1] ?? [])].filter((d) => d.disposition === "declined");
2906
3043
  const declinedLine = `Declined findings: ${declined.length > 0 ? declined.map((d) => `${d.findingId}${d.note ? ` — ${d.note}` : ""}`).join("; ") : "none"}`;
2907
3044
  const verdictLine = `Verdict: LGTM${s.lastVerdictSummary ? ` — ${s.lastVerdictSummary}` : ""}`;
@@ -2926,7 +3063,7 @@ export function renderUnitReport(
2926
3063
  const checkpointLine = (checkpoint: { branch: string; sha: string } | undefined) =>
2927
3064
  checkpoint === undefined
2928
3065
  ? undefined
2929
- : `The branch carries the interrupted work at \`${checkpoint.sha.slice(0, 7)}\` on \`${checkpoint.branch}\`; re-issue to resume from it instead of starting over.`;
3066
+ : `The branch carries the interrupted work at \`${checkpoint.sha.slice(0, 7)}\` on \`${checkpoint.branch}\`; the next reply in this thread resumes from it instead of starting over.`;
2930
3067
  switch (e.kind) {
2931
3068
  case "merged":
2932
3069
  if (e.by === "other")
@@ -2982,13 +3119,13 @@ export function renderUnitReport(
2982
3119
  // the cause and the person's exact next step.
2983
3120
  if (e.cause === "draft") {
2984
3121
  const link = e.pr !== undefined ? ` — ${e.pr.url}` : "";
2985
- if (!shows(verbosity, "verbose")) return `⏸️ Held: draft — mark it ready to continue${link}`;
2986
- const draftReissue = s.input.generated
2987
- ? `To continue, mark it ready and re-issue \`agent:ship\` in this thread with only the PR URL${e.pr !== undefined ? ` (${e.pr.url})` : ""} — no new task text; the re-issued pipeline resumes at the review round.`
2988
- : `To continue, mark it ready; ${reissue.charAt(0).toLowerCase()}${reissue.slice(1)}`;
3122
+ if (!shows(verbosity, "verbose")) return `⏸️ Held: draft — GitHub still marks the pull request as draft${link}`;
3123
+ const draftContinuation = s.input.generated
3124
+ ? `The pipeline kept the task, branch and pull request${e.pr !== undefined ? ` (${e.pr.url})` : ""}; after GitHub marks it ready, the next reply in this thread resumes at the review round.`
3125
+ : `The unit stays held while GitHub marks the pull request as a draft; ${continuation.charAt(0).toLowerCase()}${continuation.slice(1)}`;
2989
3126
  return join([
2990
3127
  `⏸️ Held after ${rounds}${e.pr !== undefined ? `: ${e.pr.url}` : ""} — the pull request is a draft, so nothing can merge and no fix round would change anything.`,
2991
- draftReissue,
3128
+ draftContinuation,
2992
3129
  ]);
2993
3130
  }
2994
3131
  // The blocked hold (issue 2086): the coding child concluded its round
@@ -3001,28 +3138,19 @@ export function renderUnitReport(
3001
3138
  return join([
3002
3139
  `⏸️ Held after ${rounds}: the coding child of round ${e.round.index} concluded its round blocked instead of opening a pull request — ${e.reason}.${prLine}`,
3003
3140
  writeUpPointer(s, e.round.kind, e.runId),
3004
- `No renewal was spent and no second coding child ran — a concluded round is not renewed (a renewal continues a budget that ran out mid-work). Next step: your word in this thread — answer what the child raised, and the unit continues from there. ${reissue}`,
3141
+ `No renewal was spent and no second coding child ran — a concluded round is not renewed (a renewal continues a budget that ran out mid-work). The child's answer remains the thread's open question; the next reply is treated as its answer. ${continuation}`,
3005
3142
  ]);
3006
3143
  }
3007
- // The person's next step is the report's whole point (issue 1990): the
3008
- // human-gated rows are named with the reviewer's own words, and the
3009
- // re-issue line says the attempt resumes at the review round — item 10's
3010
- // resume path — once the receipt stands on the pull request. That path
3011
- // fires only when the invocation carries NO new task text: re-issuing
3012
- // with the task would ADOPT the pull request and run a coding round
3013
- // first (item 10), the very round this ending exists to avoid — so the
3014
- // held case renders its own re-issue line instead of the shared one.
3144
+ // A human-gated row is a question to a person. The enclosing idle keeps
3145
+ // this unit live; either answer surface wakes a fix round carrying both
3146
+ // the typed finding and the attributed answer.
3015
3147
  const rows = e.findings.map((f) => `${f.id} (${f.severity}) — ${f.title}`).join("; ");
3016
- // The quiet copy is the ending in the user's words (record 0066): the
3017
- // held row named, the pull request linked, nothing about the machinery.
3018
- if (!shows(verbosity, "verbose")) return `⏸️ Held: ${rows}${e.pr !== undefined ? ` — ${e.pr.url}` : ""}`;
3019
- const heldReissue = s.input.generated
3020
- ? `To continue, re-issue \`agent:ship\` in this thread with only the PR URL${e.pr !== undefined ? ` (${e.pr.url})` : ""} — no new task text.`
3021
- : reissue;
3148
+ if (!shows(verbosity, "verbose"))
3149
+ return `⏸️ Waiting for you: ${rows}${e.pr !== undefined ? ` — ${e.pr.url}` : ""}`;
3022
3150
  return join([
3023
- `⏸️ ${e.verdict === "approve" ? "Approved but held" : "Changes requested but held"} after ${rounds}${e.pr !== undefined ? `: ${e.pr.url}` : ""} — every finding of review round ${e.round.index} is human-gated, a receipt only a person can produce: ${rows}. No fix round was opened: a coding child cannot produce the receipt.`,
3151
+ `⏸️ Waiting for a person after ${rounds}${e.pr !== undefined ? `: ${e.pr.url}` : ""} — every finding of review round ${e.round.index} is human-gated, a receipt only a person can produce: ${rows}. The unit stays live; no unanswered fix round was opened.`,
3024
3152
  levelLine,
3025
- `Next step: produce the receipt each finding names and post it on the pull request. ${heldReissue} The re-issued pipeline resumes at the review round — no coding round runs first.`,
3153
+ `The next reply in this unit thread or authorized pull request comment becomes the fix round's answer; review then runs again.`,
3026
3154
  ]);
3027
3155
  }
3028
3156
  case "merge_refused":
@@ -3038,8 +3166,8 @@ export function renderUnitReport(
3038
3166
  return join([
3039
3167
  `⚠️ The review approved ${e.pr.url} but the pipeline did not merge it: ${e.reason}. A person decides what becomes of the pull request.`,
3040
3168
  s.input.generated
3041
- ? reissue
3042
- : `The approved work is on the branch: rebase or fix it, push, and merge it by hand. Then re-issue the plan naming the remaining units — a unit whose pull request has merged is recognized and not run again, and its dependents start from there.`,
3169
+ ? continuation
3170
+ : "The approved work remains on the branch. This is a bug: the pipeline has no automatic recovery for this refused merge, so the unit's dependents remain blocked.",
3043
3171
  ]);
3044
3172
  case "round_cap": {
3045
3173
  // The quiet copy counts the last review's open findings, only the
@@ -3058,7 +3186,7 @@ export function renderUnitReport(
3058
3186
  failedChecks.length > 0
3059
3187
  ? `🧢 Ship stopped at a cap: the ${e.maxRounds}-round cap — the review of round ${e.reviewRounds} approved, but ${failedChecks.map((f) => `\`${f.id}\``).join(", ")} failed at the approved head and no fix round remains.${prLine}`
3060
3188
  : `🧢 Ship stopped at a cap: the ${e.maxRounds}-round cap — no approval after ${rounds}.${prLine}`;
3061
- return join([headline, splitReport(s), reissue]);
3189
+ return join([headline, splitReport(s), continuation]);
3062
3190
  }
3063
3191
  case "wall_clock_cap":
3064
3192
  if (!shows(verbosity, "verbose")) return `🧢 Out of budget — no approval after ${rounds}.${prLine}`;
@@ -3066,13 +3194,13 @@ export function renderUnitReport(
3066
3194
  `🧢 Ship stopped at a cap: the remaining pipeline time (~${Math.max(0, Math.round(e.remainingMs / MIN))} min of the ${s.input.caps.maxMinutes}-minute budget) cannot hold another round${e.refused ? ` (the ${e.refused.round} round would get ${e.refused.minutes} min, under its floor of ${e.refused.floor})` : ""} — no approval after ${rounds}.${prLine}`,
3067
3195
  budgetSplitLine(e.spent, s.input.caps.maxMinutes),
3068
3196
  splitReport(s),
3069
- reissue,
3197
+ continuation,
3070
3198
  ]);
3071
3199
  case "review_pending":
3072
3200
  return join([
3073
3201
  `⏳ Review pending: the coding child shipped ${e.pr.url}${e.headSha !== undefined ? ` (head \`${e.headSha.slice(0, 7)}\`)` : ""} but the remaining pipeline time cannot hold the review round — the work stands, only the review is missing. The next pipeline starts at the review round while the pull request still heads at the child's own last push.`,
3074
3202
  aside(budgetSplitLine(e.spent, s.input.caps.maxMinutes)),
3075
- aside(reissue),
3203
+ aside(continuation),
3076
3204
  ]);
3077
3205
  case "stopped":
3078
3206
  if (!shows(verbosity, "verbose"))
@@ -3088,7 +3216,7 @@ export function renderUnitReport(
3088
3216
  e.postedReview
3089
3217
  ? "ℹ️ A changes-requested review was posted this round before the stop — its findings stand on the PR."
3090
3218
  : undefined,
3091
- s.input.idleDays && s.input.idleDays > 0 ? "Next step: reply in this thread to continue." : reissue,
3219
+ continuation,
3092
3220
  ]);
3093
3221
  case "aborted":
3094
3222
  if (!shows(verbosity, "verbose")) return `⚠️ Aborted after ${rounds}: ${e.reason}${prLine}`;
@@ -3101,7 +3229,7 @@ export function renderUnitReport(
3101
3229
  ),
3102
3230
  e.renewal !== undefined ? `🔁 Not renewed: ${e.renewal.line}.` : undefined,
3103
3231
  `⚠️ Ship aborted after ${rounds}.`,
3104
- reissue,
3232
+ continuation,
3105
3233
  ]);
3106
3234
  case "continued":
3107
3235
  // A segment boundary is the runner continuing — an acknowledgement, verbose
@@ -3111,18 +3239,18 @@ export function renderUnitReport(
3111
3239
  return join([
3112
3240
  writeUpPointer(s, e.round.kind, e.runId),
3113
3241
  s.input.idleDays && s.input.idleDays > 0
3114
- ? `🔁 The unit's budget ran out with the unit unfinished — ${e.line}. Next step: reply in this thread to continue; ${e.renewalsLeft} renewal${e.renewalsLeft === 1 ? "" : "s"} remain${e.spendUsd !== null ? `, $${e.spendUsd.toFixed(2)} spent so far` : ""}.`
3242
+ ? `🔁 The unit's budget ran out with the unit unfinished — ${e.line}. The next reply in this thread continues it; ${e.renewalsLeft} renewal${e.renewalsLeft === 1 ? "" : "s"} remain${e.spendUsd !== null ? `, $${e.spendUsd.toFixed(2)} spent so far` : ""}.`
3115
3243
  : `🔁 The unit's budget ran out with the unit unfinished — ${e.line}. A fresh ${s.input.caps.maxMinutes}-minute budget opens in this thread${e.from !== undefined ? ` from \`${e.from.slice(0, 7)}\`` : ""}, with the last run's write-up as its request; ${e.renewalsLeft} renewal${e.renewalsLeft === 1 ? "" : "s"} remain${e.spendUsd !== null ? `, $${e.spendUsd.toFixed(2)} spent so far` : ""}.`,
3116
3244
  aside(budgetSplitLine(e.spent, s.input.caps.maxMinutes)),
3117
3245
  ]);
3118
3246
  case "transient":
3119
3247
  if (!shows(verbosity, "verbose"))
3120
- return `⚠️ Aborted after ${rounds}: the model provider failed twice; re-issue once it settles.${prLine}`;
3248
+ return `⚠️ Aborted after ${rounds}: this is a bug — the model provider failed twice and no automatic retry remains.${prLine}`;
3121
3249
  return join([
3122
3250
  `⚠️ The coding child of round ${e.round.index} (run ${e.runId}) died on a provider transient — a model-gateway 5xx, a cut stream or a gateway timeout past the harness's retry ladder — with nothing pushed, after the round was already re-run once for the same reason. The task itself was never the problem.`,
3123
3251
  writeUpPointer(s, e.round.kind, s.lastCodingRunId),
3124
- `⚠️ Ship ended after ${rounds}; re-issue once the provider settles.`,
3125
- reissue,
3252
+ `⚠️ This is a bug: ship ended after ${rounds} because its automatic provider retry was spent.`,
3253
+ continuation,
3126
3254
  ]);
3127
3255
  case "no_verdict":
3128
3256
  if (!shows(verbosity, "verbose"))
@@ -3131,7 +3259,7 @@ export function renderUnitReport(
3131
3259
  `⚠️ Review round ${e.round.index} ended without a submitted verdict (budget, refusal, or stop) — ship never converts that into a request for changes, so no findings step ran.`,
3132
3260
  writeUpPointer(s, e.round.kind, s.reviewRunByRound[e.round.index]),
3133
3261
  `⚠️ Ship aborted after ${rounds}.`,
3134
- reissue,
3262
+ continuation,
3135
3263
  ]);
3136
3264
  case "interrupted":
3137
3265
  return join([shipInterruptedNote(prUrl, e.cause, (s.input.idleDays ?? 0) > 0), checkpointLine(e.checkpoint)]);
@@ -3140,7 +3268,7 @@ export function renderUnitReport(
3140
3268
  case "refused":
3141
3269
  return join([
3142
3270
  `🚫 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.`,
3143
- aside(reissue),
3271
+ aside(continuation),
3144
3272
  ]);
3145
3273
  case "idle":
3146
3274
  // The old kind's sentence at this copy's level — the report is unchanged