pi-subagents 0.80.0 → 0.82.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 (184) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/bundle/agents.js +18 -0
  3. package/bundle/background-work.js +16 -0
  4. package/bundle/capability-ceiling.js +22 -0
  5. package/bundle/child-tool-plan.js +11 -0
  6. package/bundle/chunk-43SCHIMD.js +57 -0
  7. package/bundle/chunk-4QFZZV3O.js +57 -0
  8. package/bundle/chunk-4SDU2BZX.js +1190 -0
  9. package/bundle/chunk-52UBALJL.js +753 -0
  10. package/bundle/chunk-5LFLVSTM.js +260 -0
  11. package/bundle/chunk-5TYMSZX3.js +235 -0
  12. package/bundle/chunk-62OCY2S4.js +14 -0
  13. package/bundle/chunk-7F6RKT6W.js +132 -0
  14. package/bundle/chunk-7IVSUMQQ.js +1337 -0
  15. package/bundle/chunk-7J4ZXMY7.js +18 -0
  16. package/bundle/chunk-7PCKZHR5.js +1234 -0
  17. package/bundle/chunk-7YMXOYEU.js +1373 -0
  18. package/bundle/chunk-B5EKXDHE.js +16870 -0
  19. package/bundle/chunk-CIZQXJ5Q.js +8619 -0
  20. package/bundle/chunk-CTPAE36Y.js +94 -0
  21. package/bundle/chunk-D4EHT5SJ.js +14 -0
  22. package/bundle/chunk-GC4DSPMH.js +946 -0
  23. package/bundle/chunk-GJECHSAR.js +179 -0
  24. package/bundle/chunk-M2CTFYT3.js +4164 -0
  25. package/bundle/chunk-MHZ4G23A.js +1296 -0
  26. package/bundle/chunk-N2BN7BF5.js +159 -0
  27. package/bundle/chunk-NF7IEKBH.js +490 -0
  28. package/bundle/chunk-NGGAFDL6.js +742 -0
  29. package/bundle/chunk-NIXWDZZV.js +9 -0
  30. package/bundle/chunk-NRQ75MGH.js +8327 -0
  31. package/bundle/chunk-OYF6AFDY.js +458 -0
  32. package/bundle/chunk-PLCCBLPY.js +526 -0
  33. package/bundle/chunk-RMQFMMAT.js +416 -0
  34. package/bundle/chunk-SIJ4S3EN.js +276 -0
  35. package/bundle/chunk-UKG4V45O.js +228 -0
  36. package/bundle/chunk-VH5OAWVG.js +4096 -0
  37. package/bundle/chunk-W2SZ22EM.js +5462 -0
  38. package/bundle/chunk-WVD4UEL4.js +9367 -0
  39. package/bundle/chunk-XSWOTIA3.js +175 -0
  40. package/bundle/chunk-YFCK7H2S.js +129 -0
  41. package/bundle/chunk-YKEWIB4M.js +380 -0
  42. package/bundle/chunk-YYREO77V.js +137 -0
  43. package/bundle/chunk-Z7KVHQHL.js +173 -0
  44. package/bundle/chunk-ZLINIYTP.js +8613 -0
  45. package/bundle/chunk-ZQGSDHL6.js +128 -0
  46. package/bundle/control-channel.js +11 -0
  47. package/bundle/delegation.js +14 -0
  48. package/bundle/external-job-provider.js +20 -0
  49. package/bundle/external-runs.js +21 -0
  50. package/bundle/index.js +30 -0
  51. package/bundle/inspectors.js +8 -0
  52. package/bundle/intercom-bridge.js +11 -0
  53. package/bundle/preflight.js +425 -0
  54. package/bundle/project-panes.js +28 -0
  55. package/bundle/required-child-extensions.js +6 -0
  56. package/bundle/shared-types.js +8 -0
  57. package/bundle/workflow-resources.js +7 -0
  58. package/docs/agents.md +31 -0
  59. package/docs/configuration.md +9 -1
  60. package/docs/extension-api.md +27 -0
  61. package/docs/observability.md +6 -2
  62. package/docs/tool-reference.md +3 -1
  63. package/docs/workflows.md +16 -0
  64. package/package.json +19 -19
  65. package/skills/pi-subagents/references/execution-controls.md +5 -4
  66. package/src/agents/agent-management.js +3 -3
  67. package/src/agents/agent-memory.js +3 -1
  68. package/src/agents/agents.js +6 -6
  69. package/src/agents/frontmatter.d.ts +4 -0
  70. package/src/agents/frontmatter.js +24 -3
  71. package/src/agents/skills.js +2 -1
  72. package/src/api/delegation.d.ts +20 -0
  73. package/src/extension/config.js +3 -0
  74. package/src/extension/herdr-pi-bridge.js +17 -10
  75. package/src/extension/index.js +109 -9
  76. package/src/extension/schemas.js +3 -3
  77. package/src/intercom/native-supervisor-channel.js +20 -0
  78. package/src/missions/lifecycle.js +7 -1
  79. package/src/runs/background/active-async-capacity.d.ts +1 -0
  80. package/src/runs/background/active-async-capacity.js +9 -3
  81. package/src/runs/background/async-execution.js +20 -10
  82. package/src/runs/background/async-job-tracker.d.ts +6 -1
  83. package/src/runs/background/async-job-tracker.js +9 -4
  84. package/src/runs/background/async-status.d.ts +2 -0
  85. package/src/runs/background/async-status.js +21 -2
  86. package/src/runs/background/await-async-run.js +3 -7
  87. package/src/runs/background/boot-id.d.ts +12 -0
  88. package/src/runs/background/boot-id.js +47 -0
  89. package/src/runs/background/interrupted-elsewhere.d.ts +22 -0
  90. package/src/runs/background/interrupted-elsewhere.js +68 -0
  91. package/src/runs/background/notify.d.ts +2 -0
  92. package/src/runs/background/notify.js +13 -0
  93. package/src/runs/background/result-delivery-ownership.d.ts +6 -0
  94. package/src/runs/background/result-delivery-ownership.js +19 -1
  95. package/src/runs/background/result-watcher.d.ts +3 -1
  96. package/src/runs/background/result-watcher.js +24 -8
  97. package/src/runs/background/run-child-session.d.ts +1 -6
  98. package/src/runs/background/run-child-session.js +19 -1
  99. package/src/runs/background/run-status.js +2 -1
  100. package/src/runs/background/stale-run-reconciler.d.ts +25 -0
  101. package/src/runs/background/stale-run-reconciler.js +54 -13
  102. package/src/runs/background/subagent-runner-bootstrap.js +3 -0
  103. package/src/runs/background/subagent-runner.d.ts +4 -0
  104. package/src/runs/background/subagent-runner.js +13 -5
  105. package/src/runs/background/subagent-wait.d.ts +1 -1
  106. package/src/runs/background/subagent-wait.js +6 -1
  107. package/src/runs/background/terminal-run-index.d.ts +1 -0
  108. package/src/runs/background/terminal-run-index.js +4 -2
  109. package/src/runs/foreground/async-stop-action.js +3 -1
  110. package/src/runs/foreground/execution.js +45 -5
  111. package/src/runs/foreground/foreground-history.d.ts +16 -2
  112. package/src/runs/foreground/foreground-history.js +133 -31
  113. package/src/runs/foreground/subagent-executor.d.ts +11 -0
  114. package/src/runs/foreground/subagent-executor.js +277 -74
  115. package/src/runs/shared/child-launch.d.ts +2 -2
  116. package/src/runs/shared/child-session.d.ts +3 -2
  117. package/src/runs/shared/child-session.js +51 -8
  118. package/src/runs/shared/child-tool-plan.d.ts +7 -0
  119. package/src/runs/shared/child-tool-plan.js +21 -6
  120. package/src/runs/shared/herdr-connection.d.ts +17 -8
  121. package/src/runs/shared/herdr-connection.js +19 -7
  122. package/src/runs/shared/herdr-external-adapters.d.ts +3 -2
  123. package/src/runs/shared/herdr-external-adapters.js +1 -1
  124. package/src/runs/shared/herdr-machine.d.ts +12 -2
  125. package/src/runs/shared/herdr-machine.js +55 -9
  126. package/src/runs/shared/herdr-pane-host.d.ts +88 -0
  127. package/src/runs/shared/herdr-pane-host.js +155 -0
  128. package/src/runs/shared/herdr-placed-run.d.ts +27 -150
  129. package/src/runs/shared/herdr-placed-run.js +48 -395
  130. package/src/runs/shared/local-machine-access.d.ts +5 -0
  131. package/src/runs/shared/local-machine-access.js +28 -0
  132. package/src/runs/shared/orca-progress-tabs.d.ts +0 -1
  133. package/src/runs/shared/orca-progress-tabs.js +1 -24
  134. package/src/runs/shared/parallel-utils.d.ts +1 -1
  135. package/src/runs/shared/placed-run-owner.d.ts +49 -0
  136. package/src/runs/shared/placed-run-owner.js +93 -0
  137. package/src/runs/shared/placement-host.d.ts +99 -0
  138. package/src/runs/shared/placement-host.js +157 -0
  139. package/src/runs/shared/rex-machine.d.ts +25 -0
  140. package/src/runs/shared/rex-machine.js +83 -0
  141. package/src/runs/shared/rex-pane-host.d.ts +62 -0
  142. package/src/runs/shared/rex-pane-host.js +327 -0
  143. package/src/runs/shared/structured-output.d.ts +1 -0
  144. package/src/runs/shared/structured-output.js +1 -0
  145. package/src/shared/accessible-dir.js +4 -1
  146. package/src/shared/completion-owner.d.ts +5 -0
  147. package/src/shared/completion-owner.js +5 -0
  148. package/src/shared/provider-diagnostics.d.ts +9 -0
  149. package/src/shared/provider-diagnostics.js +75 -0
  150. package/src/shared/session-identity.d.ts +2 -0
  151. package/src/shared/session-identity.js +26 -0
  152. package/src/shared/types.d.ts +48 -6
  153. package/src/slash/delegation-adapters.d.ts +3 -1
  154. package/src/slash/delegation-adapters.js +7 -0
  155. package/src/slash/prompt-template-bridge.js +3 -1
  156. package/src/slash/subagents-admin.d.ts +1 -0
  157. package/src/slash/subagents-admin.js +1 -1
  158. package/src/tui/fleet-layout.d.ts +20 -0
  159. package/src/tui/fleet-layout.js +17 -0
  160. package/src/tui/fleet-viewport.d.ts +12 -0
  161. package/src/tui/fleet-viewport.js +35 -0
  162. package/src/tui/fleet.d.ts +6 -6
  163. package/src/tui/fleet.js +93 -66
  164. package/src/tui/render.d.ts +6 -0
  165. package/src/tui/render.js +3 -3
  166. package/src/tui/subagent-messages.js +1 -0
  167. package/src/workflows/scripted-workflow.d.ts +1 -1
  168. package/src/workflows/structured-workflow-scripts.d.ts +1 -0
  169. package/src/workflows/structured-workflow-scripts.js +5 -4
  170. package/src/workflows/workflow-auto-resume.d.ts +32 -0
  171. package/src/workflows/workflow-auto-resume.js +102 -0
  172. package/src/workflows/workflow-launch-record.d.ts +27 -0
  173. package/src/workflows/workflow-launch-record.js +48 -0
  174. package/src/workflows/workflow-resources.js +2 -2
  175. package/src/workflows/workflow-resume-claim.d.ts +82 -0
  176. package/src/workflows/workflow-resume-claim.js +210 -0
  177. package/src/workflows/workflow-resume.d.ts +56 -0
  178. package/src/workflows/workflow-resume.js +106 -0
  179. package/src/workflows/workflow-reuse.d.ts +15 -9
  180. package/src/workflows/workflow-reuse.js +77 -14
  181. package/src/workflows/workflow-revival.d.ts +2 -0
  182. package/src/workflows/workflow-revival.js +2 -1
  183. package/src/workflows/workflow-stop-cause.d.ts +6 -0
  184. package/src/workflows/workflow-stop-cause.js +8 -0
package/src/tui/fleet.js CHANGED
@@ -2,6 +2,8 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
4
4
  import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
5
+ import { fleetContains, fleetLayout } from "./fleet-layout.js";
6
+ import { FleetViewport } from "./fleet-viewport.js";
5
7
  import { snapshotExternalRuns } from "../api/external-runs.js";
6
8
  import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.js";
7
9
  import { formatDuration, formatModelThinking, formatTokens, formatTokenUsage, shortenPath } from "../shared/formatters.js";
@@ -14,7 +16,9 @@ import { steerAsyncRun } from "../runs/foreground/async-steering-action.js";
14
16
  import { stopAsyncRun } from "../runs/foreground/async-stop-action.js";
15
17
  import { resolveWorkflowForegroundSteeringTarget, steerWorkflowForegroundTarget } from "../runs/foreground/workflow-foreground-steering.js";
16
18
  import { contextModeBadge, contextModeLabel } from "../runs/shared/context-mode.js";
19
+ import { foregroundChildStateLabel } from "../runs/foreground/foreground-history.js";
17
20
  import { FLEET_STATUS_WIDGET_KEY, reportFleetWarning } from "./fleet-status.js";
21
+ import { stepTaskSummary } from "./render.js";
18
22
  import { readFleetTranscript, renderFleetTranscript } from "./fleet-transcript.js";
19
23
  import { runningTone } from "./running-tone.js";
20
24
  import { childThinkingLevel } from "../shared/model-info.js";
@@ -108,18 +112,22 @@ function asyncItems(run, description) {
108
112
  if (run.steps.length === 0 || run.mode === "workflow") {
109
113
  return [{ key: `async:${run.id}`, kind: "async", runId: run.id, agent: run.mode, state: run.state, updatedAt, run, ...(description ? { description } : {}) }];
110
114
  }
111
- return run.steps.map((step) => ({
112
- key: `async:${run.id}:${step.index}`,
113
- kind: "async",
114
- runId: run.id,
115
- index: step.index,
116
- agent: step.label ? `${step.label} (${step.agent})` : step.agent,
117
- state: step.status,
118
- updatedAt: step.lastActivityAt ?? updatedAt,
119
- run,
120
- step,
121
- ...(description ? { description } : {}),
122
- }));
115
+ return run.steps.map((step) => {
116
+ const task = stepTaskSummary(step, step.agent);
117
+ return {
118
+ key: `async:${run.id}:${step.index}`,
119
+ kind: "async",
120
+ runId: run.id,
121
+ index: step.index,
122
+ agent: step.label ? `${step.label} (${step.agent})` : step.agent,
123
+ state: step.status,
124
+ updatedAt: step.lastActivityAt ?? updatedAt,
125
+ run,
126
+ step,
127
+ ...(task ? { task } : {}),
128
+ ...(description ? { description } : {}),
129
+ };
130
+ });
123
131
  }
124
132
  function orderFleetAsyncRuns(runs, terminalLimit, trackedJobs) {
125
133
  const updatedAt = (run) => run.lastUpdate ?? run.endedAt ?? run.startedAt;
@@ -443,7 +451,7 @@ function foregroundRecentDetail(item, state) {
443
451
  const lines = [
444
452
  `Run: ${item.runId}`,
445
453
  "Source: foreground",
446
- `State: ${child.status}`,
454
+ `State: ${foregroundChildStateLabel(run, child)}`,
447
455
  `Mode: ${run.mode}`,
448
456
  `Child: ${child.index} (${child.agent})${contextModeLabel(child.context) ? ` ${contextModeLabel(child.context)}` : ""}`,
449
457
  modelThinking ? `Model: ${modelThinking}` : undefined,
@@ -746,11 +754,8 @@ export class SubagentFleetComponent {
746
754
  snapshot = { items: [] };
747
755
  selected = 0;
748
756
  selectedKey;
749
- detailScroll = 0;
750
- detailAutoFollow = true;
751
- detailLineCount = 0;
752
- detailViewportHeight = 8;
753
- bodyHeight = 8;
757
+ viewport;
758
+ renderedLayout;
754
759
  expandedTools = false;
755
760
  promptAuditOpen = false;
756
761
  promptAuditView = "authored";
@@ -773,6 +778,7 @@ export class SubagentFleetComponent {
773
778
  keybindings;
774
779
  constructor(tui, theme, state, done, options = {}) {
775
780
  this.tui = tui;
781
+ this.viewport = new FleetViewport(() => this.tui.requestRender());
776
782
  this.theme = theme;
777
783
  this.markdownTheme = options.markdownTheme ?? getMarkdownTheme();
778
784
  this.state = state;
@@ -820,11 +826,14 @@ export class SubagentFleetComponent {
820
826
  this.selectedKey = this.snapshot.items[this.selected]?.key;
821
827
  }
822
828
  moveSelection(delta) {
823
- if (this.snapshot.items.length === 0)
829
+ this.selectItem(Math.max(0, Math.min(this.snapshot.items.length - 1, this.selected + delta)));
830
+ }
831
+ selectItem(index) {
832
+ if (!this.snapshot.items[index])
824
833
  return;
825
- this.selected = Math.max(0, Math.min(this.snapshot.items.length - 1, this.selected + delta));
834
+ this.selected = index;
826
835
  this.selectedKey = this.snapshot.items[this.selected]?.key;
827
- this.detailAutoFollow = true;
836
+ this.viewport.follow();
828
837
  this.resetActionInput();
829
838
  this.tui.requestRender();
830
839
  }
@@ -862,8 +871,7 @@ export class SubagentFleetComponent {
862
871
  return;
863
872
  this.selected = this.snapshot.items.findIndex((item) => item.key === target.key);
864
873
  this.selectedKey = target.key;
865
- this.detailScroll = 0;
866
- this.detailAutoFollow = false;
874
+ this.viewport.resetTop();
867
875
  this.tui.requestRender();
868
876
  }
869
877
  resetActionInput() {
@@ -951,8 +959,7 @@ export class SubagentFleetComponent {
951
959
  setActionNotice(result) {
952
960
  this.actionNotice = result;
953
961
  this.resetActionInput();
954
- this.detailAutoFollow = false;
955
- this.detailScroll = 0;
962
+ this.viewport.resetTop();
956
963
  this.refresh();
957
964
  this.tui.requestRender();
958
965
  }
@@ -972,10 +979,35 @@ export class SubagentFleetComponent {
972
979
  });
973
980
  }
974
981
  scrollDetail(delta) {
975
- const maxScroll = Math.max(0, this.detailLineCount - this.detailViewportHeight);
976
- this.detailScroll = Math.max(0, Math.min(maxScroll, this.detailScroll + delta));
977
- this.detailAutoFollow = this.detailScroll >= maxScroll;
978
- this.tui.requestRender();
982
+ this.viewport.scroll.scrollBy(delta);
983
+ }
984
+ handleMouse(event) {
985
+ // Block pointer fallback as well as inspector actions while a text/confirmation modal owns input.
986
+ if (this.steerDraft !== undefined || this.redoGuidanceDraft !== undefined || this.stopConfirming || this.actionBusy) {
987
+ return { handled: true, render: false };
988
+ }
989
+ const layout = this.renderedLayout;
990
+ if (!layout || layout.width !== event.width || layout.rows !== (this.tui.terminal?.rows ?? 32)) {
991
+ // Never use hit regions from a different size; wheel must not escape to the transcript.
992
+ return event.type === "wheel" ? { handled: true, render: false } : undefined;
993
+ }
994
+ if (event.type === "wheel" && fleetContains(layout.output, event.x, event.y)) {
995
+ this.scrollDetail(event.wheelDelta ?? 0);
996
+ return { handled: true, render: false };
997
+ }
998
+ if (this.promptAuditOpen)
999
+ return { handled: true, render: false };
1000
+ if (event.type === "click" && event.button === "left" && !event.shift && !event.alt && !event.ctrl
1001
+ && fleetContains(layout.roster, event.x, event.y)) {
1002
+ const key = layout.rosterKeys[event.y - layout.roster.y];
1003
+ const index = key === undefined ? -1 : this.snapshot.items.findIndex((item) => item.key === key);
1004
+ if (index < 0)
1005
+ return undefined;
1006
+ this.selectItem(index);
1007
+ return { handled: true, render: false };
1008
+ }
1009
+ // Unowned wheel inside the overlay would otherwise scroll the transcript behind it.
1010
+ return event.type === "wheel" ? { handled: true, render: false } : undefined;
979
1011
  }
980
1012
  handleInput(data) {
981
1013
  if (this.promptAuditOpen && this.redoGuidanceDraft !== undefined) {
@@ -1012,7 +1044,7 @@ export class SubagentFleetComponent {
1012
1044
  if (this.promptAuditOpen) {
1013
1045
  if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
1014
1046
  this.promptAuditOpen = false;
1015
- this.detailAutoFollow = true;
1047
+ this.viewport.follow();
1016
1048
  this.tui.requestRender();
1017
1049
  return;
1018
1050
  }
@@ -1022,7 +1054,7 @@ export class SubagentFleetComponent {
1022
1054
  return this.movePromptSelection(-1);
1023
1055
  if (data === "1" || data === "2" || data === "3") {
1024
1056
  this.promptAuditView = data === "1" ? "authored" : data === "2" ? "runtime" : "effective";
1025
- this.detailScroll = 0;
1057
+ this.viewport.resetTop();
1026
1058
  this.tui.requestRender();
1027
1059
  return;
1028
1060
  }
@@ -1037,8 +1069,7 @@ export class SubagentFleetComponent {
1037
1069
  else {
1038
1070
  this.actionNotice = undefined;
1039
1071
  this.redoGuidanceDraft = "";
1040
- this.detailAutoFollow = false;
1041
- this.detailScroll = 0;
1072
+ this.viewport.resetTop();
1042
1073
  this.tui.requestRender();
1043
1074
  }
1044
1075
  return;
@@ -1061,9 +1092,9 @@ export class SubagentFleetComponent {
1061
1092
  if (matchesFleetAction(data, this.keybindings, "scrollDown"))
1062
1093
  return this.scrollDetail(1);
1063
1094
  if (matchesFleetAction(data, this.keybindings, "pageUp"))
1064
- return this.scrollDetail(-this.detailViewportHeight);
1095
+ return this.scrollDetail(-this.viewport.scroll.viewportHeight);
1065
1096
  if (matchesFleetAction(data, this.keybindings, "pageDown"))
1066
- return this.scrollDetail(this.detailViewportHeight);
1097
+ return this.scrollDetail(this.viewport.scroll.viewportHeight);
1067
1098
  return;
1068
1099
  }
1069
1100
  if (this.steerDraft !== undefined) {
@@ -1137,9 +1168,9 @@ export class SubagentFleetComponent {
1137
1168
  if (matchesFleetAction(data, this.keybindings, "selectLast"))
1138
1169
  return this.moveSelection(this.snapshot.items.length);
1139
1170
  if (matchesFleetAction(data, this.keybindings, "pageUp"))
1140
- return this.scrollDetail(-this.detailViewportHeight);
1171
+ return this.scrollDetail(-this.viewport.scroll.viewportHeight);
1141
1172
  if (matchesFleetAction(data, this.keybindings, "pageDown"))
1142
- return this.scrollDetail(this.detailViewportHeight);
1173
+ return this.scrollDetail(this.viewport.scroll.viewportHeight);
1143
1174
  if (matchesFleetAction(data, this.keybindings, "refresh")) {
1144
1175
  this.transcriptCache = undefined;
1145
1176
  this.refresh();
@@ -1152,8 +1183,7 @@ export class SubagentFleetComponent {
1152
1183
  else {
1153
1184
  this.promptAuditOpen = true;
1154
1185
  this.promptAuditView = "authored";
1155
- this.detailScroll = 0;
1156
- this.detailAutoFollow = false;
1186
+ this.viewport.resetTop();
1157
1187
  this.actionNotice = undefined;
1158
1188
  this.tui.requestRender();
1159
1189
  }
@@ -1166,8 +1196,7 @@ export class SubagentFleetComponent {
1166
1196
  else {
1167
1197
  this.actionNotice = undefined;
1168
1198
  this.steerDraft = "";
1169
- this.detailAutoFollow = false;
1170
- this.detailScroll = 0;
1199
+ this.viewport.resetTop();
1171
1200
  this.tui.requestRender();
1172
1201
  }
1173
1202
  return;
@@ -1179,8 +1208,7 @@ export class SubagentFleetComponent {
1179
1208
  else {
1180
1209
  this.actionNotice = undefined;
1181
1210
  this.stopConfirming = true;
1182
- this.detailAutoFollow = false;
1183
- this.detailScroll = 0;
1211
+ this.viewport.resetTop();
1184
1212
  this.tui.requestRender();
1185
1213
  }
1186
1214
  return;
@@ -1196,18 +1224,21 @@ export class SubagentFleetComponent {
1196
1224
  return;
1197
1225
  }
1198
1226
  }
1199
- rosterLines(width) {
1227
+ rosterLines(layout) {
1200
1228
  if (this.snapshot.items.length === 0)
1201
1229
  return [this.theme.fg("dim", "No tracked children")];
1202
- const start = Math.max(0, Math.min(this.selected - this.bodyHeight + 1, Math.max(0, this.snapshot.items.length - this.bodyHeight)));
1203
- return this.snapshot.items.slice(start, start + this.bodyHeight).map((item, offset) => {
1204
- const index = start + offset;
1230
+ return layout.rosterKeys.map((_key, offset) => {
1231
+ const index = layout.rosterStart + offset;
1232
+ const item = this.snapshot.items[index];
1205
1233
  const marker = index === this.selected ? this.theme.fg("accent", "›") : " ";
1206
1234
  const context = item.kind === "async" ? contextModeBadge(this.theme, item.step?.context ?? item.run.context) : item.kind === "foreground-recent" ? contextModeBadge(this.theme, item.child.context) : "";
1207
- const agent = index === this.selected ? this.theme.bold(item.agent) : item.agent;
1208
- const identity = item.runId.slice(0, 8);
1235
+ // A child's task tells rows of one run apart; the run id stays in the detail header.
1236
+ const task = item.kind === "async" ? item.task : undefined;
1237
+ const name = task && item.kind === "async" && item.step ? item.step.agent : item.agent;
1238
+ const agent = index === this.selected ? this.theme.bold(name) : name;
1239
+ const identity = task ?? item.runId.slice(0, 8);
1209
1240
  const left = `${marker} ${statusGlyph(item, this.theme)} ${agent}${context} ${this.theme.fg("dim", `· ${identity}`)}`;
1210
- return rightAligned(left, this.theme.fg("dim", item.state), width);
1241
+ return rightAligned(left, this.theme.fg("dim", item.state), layout.roster.width);
1211
1242
  });
1212
1243
  }
1213
1244
  renderedTranscript(target, width) {
@@ -1305,26 +1336,21 @@ export class SubagentFleetComponent {
1305
1336
  return { header: [], body: this.withActionLines(lines) };
1306
1337
  }
1307
1338
  render(width) {
1308
- if (width < 36)
1339
+ const layout = fleetLayout(width, this.tui.terminal?.rows ?? 32, this.selected, this.snapshot.items);
1340
+ this.renderedLayout = layout;
1341
+ if (!layout)
1309
1342
  return [truncateToWidth("Subagent fleet needs at least 36 columns. Esc closes.", width)];
1310
1343
  const innerWidth = width - 2;
1311
- const rows = this.tui.terminal?.rows ?? 32;
1312
- this.bodyHeight = Math.max(2, Math.floor(rows * 0.85) - 6);
1313
- const rosterWidth = Math.max(22, Math.min(46, Math.floor((innerWidth - 1) * 0.38)));
1314
- const detailWidth = Math.max(1, innerWidth - rosterWidth - 1);
1315
- const roster = this.rosterLines(rosterWidth);
1344
+ const { bodyHeight } = layout;
1345
+ const rosterWidth = layout.roster.width;
1346
+ const detailWidth = layout.output.width;
1347
+ const roster = this.rosterLines(layout);
1316
1348
  const detail = this.wrappedDetail(detailWidth);
1317
- const detailHeader = detail.header.slice(0, Math.max(0, this.bodyHeight - 1));
1318
- this.detailViewportHeight = Math.max(1, this.bodyHeight - detailHeader.length);
1319
- this.detailLineCount = detail.body.length;
1320
- const maxDetailScroll = Math.max(0, detail.body.length - this.detailViewportHeight);
1321
- if (this.detailAutoFollow)
1322
- this.detailScroll = maxDetailScroll;
1323
- else if (this.detailScroll > maxDetailScroll)
1324
- this.detailScroll = maxDetailScroll;
1349
+ const detailHeader = detail.header.slice(0, Math.max(0, bodyHeight - 1));
1350
+ const outputBodyHeight = bodyHeight - detailHeader.length;
1325
1351
  const visibleDetails = [
1326
1352
  ...detailHeader,
1327
- ...detail.body.slice(this.detailScroll, this.detailScroll + this.detailViewportHeight),
1353
+ ...this.viewport.render(detail.body, detailWidth, outputBodyHeight),
1328
1354
  ];
1329
1355
  const lines = [this.theme.fg("border", `╭${"─".repeat(innerWidth)}╮`)];
1330
1356
  const selected = this.snapshot.items[this.selected];
@@ -1336,7 +1362,7 @@ export class SubagentFleetComponent {
1336
1362
  : this.theme.fg("dim", "no children ");
1337
1363
  lines.push(this.theme.fg("border", "│") + rightAligned(title, selectedStatus, innerWidth) + this.theme.fg("border", "│"));
1338
1364
  lines.push(this.theme.fg("border", `├${"─".repeat(rosterWidth)}┬${"─".repeat(detailWidth)}┤`));
1339
- for (let index = 0; index < this.bodyHeight; index++) {
1365
+ for (let index = 0; index < bodyHeight; index++) {
1340
1366
  lines.push(this.theme.fg("border", "│")
1341
1367
  + fit(roster[index] ?? "", rosterWidth)
1342
1368
  + this.theme.fg("border", "│")
@@ -1355,6 +1381,7 @@ export class SubagentFleetComponent {
1355
1381
  return lines.map((line) => truncateToWidth(line, width));
1356
1382
  }
1357
1383
  invalidate() {
1384
+ this.renderedLayout = undefined;
1358
1385
  this.transcriptCache = undefined;
1359
1386
  this.refresh();
1360
1387
  }
@@ -39,6 +39,12 @@ export interface AsyncLaneProjection {
39
39
  /** Project already-loaded async status facts into one bounded, render-only lane row. */
40
40
  export declare function projectAsyncLane(job: AsyncJobState, ...args: [selectedStep?: AsyncJobStep]): AsyncLaneProjection | undefined;
41
41
  export declare function widgetRenderKey(job: AsyncJobState, expanded?: boolean): string;
42
+ /** A child's task in one line: its label and task, else its session name without the repeated agent name. */
43
+ export declare function stepTaskSummary(step: {
44
+ description?: string;
45
+ label?: string;
46
+ sessionName?: string;
47
+ }, agentName: string | undefined): string | undefined;
42
48
  /** Structural identity of the workflow rows that Fleet can cover. */
43
49
  export declare function inlineWorkflowRenderKey(job: AsyncJobState, children: AsyncJobState[]): string;
44
50
  /** Presentation-only coverage from the mounted inline Fleet roster, never configuration. */
package/src/tui/render.js CHANGED
@@ -1104,14 +1104,14 @@ function shouldCollapseSingleChildDetails(job, step) {
1104
1104
  return isCompletedWidgetStepStatus(step.status);
1105
1105
  return false;
1106
1106
  }
1107
- function singleChildTask(job, step) {
1107
+ /** A child's task in one line: its label and task, else its session name without the repeated agent name. */
1108
+ export function stepTaskSummary(step, agentName) {
1108
1109
  const task = compactTaskText(step.description, step.label);
1109
1110
  if (task)
1110
1111
  return task;
1111
1112
  const sessionName = step.sessionName?.trim();
1112
1113
  if (!sessionName)
1113
1114
  return undefined;
1114
- const agentName = singleChildAgentName(job, step);
1115
1115
  const sessionTask = stripRepeatedAgentPrefix(sessionName, agentName);
1116
1116
  return sessionTask === agentName ? undefined : compactTaskText(sessionTask);
1117
1117
  }
@@ -2223,7 +2223,7 @@ function foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index, tota
2223
2223
  const lane = projectAsyncLane(job, step);
2224
2224
  if (lane)
2225
2225
  lines.push(...formatLaneProjectionLines(lane, theme, detailIndent));
2226
- const task = collapseDetails ? singleChildTask(job, step) : compactTaskText(step.description, step.label);
2226
+ const task = collapseDetails ? stepTaskSummary(step, displayName) : compactTaskText(step.description, step.label);
2227
2227
  if (task)
2228
2228
  lines.push(`${detailIndent}${theme.fg("dim", `task: ${task}`)}`);
2229
2229
  const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
@@ -183,6 +183,7 @@ const MESSAGE_VIEWS = new Map([
183
183
  ["subagent-wait-subscription", readsContent("result", (message) => waitSubscriptionHeadline(message.details))],
184
184
  ["subagent-incremental-child-notify", readsContent("result", workflowChildHeadline)],
185
185
  ["subagent-workflow-result-write-failed", readsContent("result", () => ["workflow result ", { status: "save failed" }])],
186
+ ["subagent-auto-resume", readsContent("result", () => ["workflow auto-resume ", { status: "failed" }])],
186
187
  [SUBAGENT_CONTROL_MESSAGE_TYPE, readsContent("attention", (message) => controlNoticeHeadline(message.details))],
187
188
  [SUBAGENT_STEERING_MESSAGE_TYPE, readsContent("attention", (message) => steeringNoticeHeadline(message.details))],
188
189
  [SUBAGENT_WATCHDOG_WARNING_TYPE, (message) => watchdogWarningView(message.details, contentText(message.content))],
@@ -60,7 +60,7 @@ export interface WorkflowScriptChildResult {
60
60
  };
61
61
  artifactPaths: string[];
62
62
  results?: SingleResult[];
63
- /** Came from a runtime-replaced run of the same script (and args, unless named by options.reuseFrom): its saved result, or its still-running child re-attached; this run launched nothing. */
63
+ /** Came from a runtime-replaced or owner-exited run of the same script (and args, unless named by options.reuseFrom): its saved result, or its still-running child re-attached; this run launched nothing. */
64
64
  reused?: boolean;
65
65
  }
66
66
  export interface WorkflowScriptTraceEntry {
@@ -4,6 +4,7 @@
4
4
  * and never becomes code. Scripts run on the ordinary workflow runtime.
5
5
  */
6
6
  export type StructuredWorkflowKind = "tasks" | "chain";
7
+ export declare const TOP_LEVEL_CONTROLS_HINT = " Put worktree and other child controls on the subagent call itself; they apply to every child.";
7
8
  export declare function isPlainRecord(value: unknown): value is Record<string, unknown>;
8
9
  /** Validate exact data shapes and expand them into a package-owned workflow script. */
9
10
  export declare function buildStructuredWorkflowScript(kind: StructuredWorkflowKind, steps: unknown, originalTask: string | undefined): string;
@@ -7,6 +7,7 @@ const OUTPUT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
7
7
  const PLACEHOLDER_PATTERN = /\{(?:task|previous|outputs\.([A-Za-z_][A-Za-z0-9_]*))\}/g;
8
8
  /** Optional per-child launch fields copied verbatim onto the generated runs.all item. */
9
9
  const CHILD_FIELDS = ["cwd", "model"];
10
+ export const TOP_LEVEL_CONTROLS_HINT = " Put worktree and other child controls on the subagent call itself; they apply to every child.";
10
11
  export function isPlainRecord(value) {
11
12
  if (!value || typeof value !== "object" || Array.isArray(value))
12
13
  return false;
@@ -97,7 +98,7 @@ function buildTasksScript(steps, originalTask) {
97
98
  throw new Error("tasks must be a non-empty array of { agent, task } items.");
98
99
  const children = steps.map((item, index) => {
99
100
  const path = `tasks[${index}]`;
100
- const record = exactFields(item, path, ["agent", "task", ...CHILD_FIELDS], "Each tasks item accepts agent and task, plus optional cwd and model.");
101
+ const record = exactFields(item, path, ["agent", "task", ...CHILD_FIELDS], `Each tasks item accepts agent and task, plus optional cwd and model.${TOP_LEVEL_CONTROLS_HINT}`);
101
102
  const agent = nonEmptyString(record.agent, `${path}.agent`);
102
103
  const task = nonEmptyString(record.task, `${path}.task`);
103
104
  const taskExpression = compileTemplate(task, {
@@ -131,20 +132,20 @@ function buildChainScript(steps, originalTask) {
131
132
  let stepChildren;
132
133
  let outputName;
133
134
  if (isPlainRecord(step) && Object.hasOwn(step, "parallel")) {
134
- const record = exactFields(step, path, ["parallel"], "A parallel step accepts only parallel; as is supported on sequential steps only.");
135
+ const record = exactFields(step, path, ["parallel"], `A parallel step accepts only parallel; as is supported on sequential steps only.${TOP_LEVEL_CONTROLS_HINT}`);
135
136
  const items = record.parallel;
136
137
  if (!Array.isArray(items) || items.length === 0)
137
138
  throw new Error(`${path}.parallel must be a non-empty array of { agent, task } items.`);
138
139
  stepChildren = items.map((item, itemIndex) => {
139
140
  const itemPath = `${path}.parallel[${itemIndex}]`;
140
- const itemRecord = exactFields(item, itemPath, ["agent", "task", ...CHILD_FIELDS], "Each parallel item accepts agent and task, plus optional cwd and model.");
141
+ const itemRecord = exactFields(item, itemPath, ["agent", "task", ...CHILD_FIELDS], `Each parallel item accepts agent and task, plus optional cwd and model.${TOP_LEVEL_CONTROLS_HINT}`);
141
142
  const agent = nonEmptyString(itemRecord.agent, `${itemPath}.agent`);
142
143
  const task = nonEmptyString(itemRecord.task, `${itemPath}.task`);
143
144
  return { key: `step-${number}-${itemIndex + 1}`, agent, fields: childFields(itemRecord, itemPath), taskExpression: compileTemplate(task, { path: `${itemPath}.task`, hasOriginalTask, previous, outputs }) };
144
145
  });
145
146
  }
146
147
  else {
147
- const record = exactFields(step, path, ["agent", "task", "as", ...CHILD_FIELDS], "A sequential step accepts agent, task, as, cwd and model; a parallel step accepts only parallel.");
148
+ const record = exactFields(step, path, ["agent", "task", "as", ...CHILD_FIELDS], `A sequential step accepts agent, task, as, cwd and model; a parallel step accepts only parallel.${TOP_LEVEL_CONTROLS_HINT}`);
148
149
  const agent = nonEmptyString(record.agent, `${path}.agent`);
149
150
  let task;
150
151
  if (record.task === undefined) {
@@ -0,0 +1,32 @@
1
+ import { type PreparedWorkflowResume, type WorkflowResumeRefusalReason } from "./workflow-resume.ts";
2
+ /** Custom message posted when an auto-resume launch did not start. */
3
+ export declare const WORKFLOW_AUTO_RESUME_MESSAGE_TYPE = "subagent-auto-resume";
4
+ /** One planned auto-resume: its claim is held from session start until the launch starts or is cancelled. */
5
+ export interface PendingWorkflowAutoResume {
6
+ readonly sessionId: string;
7
+ /** The extension runtime that planned it. */
8
+ readonly owner: object;
9
+ readonly prepared: PreparedWorkflowResume;
10
+ /** `cancelled` before it started: its run id is reused by the next scan. */
11
+ state: "pending" | "started" | "cancelled";
12
+ }
13
+ /** Cancels matching launches that have not started and gives their claims back, synchronously. */
14
+ export declare function cancelPendingWorkflowAutoResumes(match: (entry: PendingWorkflowAutoResume) => boolean): void;
15
+ /** A cancelled entry stays recorded until its session's next scan reuses its run id. */
16
+ export declare function finishWorkflowAutoResume(entry: PendingWorkflowAutoResume): void;
17
+ /**
18
+ * Claims this session's interrupted top-level workflows for auto-resume. Synchronous and bounded:
19
+ * one listing of the session's terminal index, reads only for runs that ended in the last 24 h,
20
+ * then the cheap filters before any claim. A refusal is recorded in the run, so each run gets one
21
+ * automatic attempt; the launch itself happens later, off session start. Launches of this session
22
+ * that another runtime (or an earlier session start) planned but did not start are cancelled first,
23
+ * so their claims do not hide the runs from this scan.
24
+ */
25
+ export declare function planWorkflowAutoResumes(input: {
26
+ asyncDirRoot: string;
27
+ currentSessionId: string;
28
+ now: number;
29
+ owner: object;
30
+ }): PendingWorkflowAutoResume[];
31
+ /** The one line posted when an auto-resume launch did not start. */
32
+ export declare function autoResumeFailureText(sourceRunId: string, refusal: Pick<WorkflowResumeRefusalReason, "reason" | "byHand">): string;
@@ -0,0 +1,102 @@
1
+ import * as path from "node:path";
2
+ import { readRecentTerminalRunIndex } from "../runs/background/terminal-run-index.js";
3
+ import { readStatus } from "../shared/utils.js";
4
+ import { hasWorkflowResumeRefusal, recordWorkflowResumeRefusal, refusalSentence, releaseWorkflowResumeClaim, workflowResumeClaimState } from "./workflow-resume-claim.js";
5
+ import { prepareWorkflowResume } from "./workflow-resume.js";
6
+ import { isReusableWorkflowStopCause, WORKFLOW_STOP_CAUSE_OWNER_EXITED } from "./workflow-stop-cause.js";
7
+ /** Custom message posted when an auto-resume launch did not start. */
8
+ export const WORKFLOW_AUTO_RESUME_MESSAGE_TYPE = "subagent-auto-resume";
9
+ /** Interrupted workflows older than this are left for a resume by hand. */
10
+ const AUTO_RESUME_MAX_AGE_MS = 24 * 60 * 60 * 1000;
11
+ const AUTO_RESUME_SCAN_LIMIT = 50;
12
+ /** When the run was interrupted: the dead owner's last update, or the runtime replacement. */
13
+ function interruptedAt(status) {
14
+ return status.workflow?.stopCause === WORKFLOW_STOP_CAUSE_OWNER_EXITED ? status.workflow.ownerExitedAt : status.endedAt;
15
+ }
16
+ // Shared by every runtime in the process: a reload's new runtime must see the old runtime's pending claims.
17
+ const registryKey = Symbol.for("pi-subagents.workflow-auto-resume.v1");
18
+ const registryGlobal = globalThis;
19
+ const pendingAutoResumes = registryGlobal[registryKey] ?? (registryGlobal[registryKey] = new Set());
20
+ /** Cancels matching launches that have not started and gives their claims back, synchronously. */
21
+ export function cancelPendingWorkflowAutoResumes(match) {
22
+ for (const entry of pendingAutoResumes) {
23
+ if (entry.state !== "pending" || !match(entry))
24
+ continue;
25
+ // It stays in the set, so the next scan of its session claims the run again under the same id and
26
+ // a notice that already said `Auto-resumed as <id>.` stays true.
27
+ entry.state = "cancelled";
28
+ try {
29
+ releaseWorkflowResumeClaim(entry.prepared.sourceAsyncDir, entry.prepared.newRunId);
30
+ }
31
+ catch (error) {
32
+ console.error(`Failed to give back the auto-resume claim of workflow ${entry.prepared.sourceRunId}:`, error);
33
+ }
34
+ }
35
+ }
36
+ /** A cancelled entry stays recorded until its session's next scan reuses its run id. */
37
+ export function finishWorkflowAutoResume(entry) {
38
+ if (entry.state !== "cancelled")
39
+ pendingAutoResumes.delete(entry);
40
+ }
41
+ /**
42
+ * Claims this session's interrupted top-level workflows for auto-resume. Synchronous and bounded:
43
+ * one listing of the session's terminal index, reads only for runs that ended in the last 24 h,
44
+ * then the cheap filters before any claim. A refusal is recorded in the run, so each run gets one
45
+ * automatic attempt; the launch itself happens later, off session start. Launches of this session
46
+ * that another runtime (or an earlier session start) planned but did not start are cancelled first,
47
+ * so their claims do not hide the runs from this scan.
48
+ */
49
+ export function planWorkflowAutoResumes(input) {
50
+ const { asyncDirRoot, currentSessionId, now } = input;
51
+ cancelPendingWorkflowAutoResumes((entry) => entry.sessionId === currentSessionId);
52
+ const reusableRunIds = new Map();
53
+ for (const entry of pendingAutoResumes) {
54
+ if (entry.sessionId !== currentSessionId || entry.state !== "cancelled")
55
+ continue;
56
+ reusableRunIds.set(entry.prepared.sourceRunId, entry.prepared.newRunId);
57
+ pendingAutoResumes.delete(entry);
58
+ }
59
+ let runIds;
60
+ try {
61
+ runIds = readRecentTerminalRunIndex(asyncDirRoot, { sessionId: currentSessionId, limit: AUTO_RESUME_SCAN_LIMIT, endedAfter: now - AUTO_RESUME_MAX_AGE_MS });
62
+ }
63
+ catch (error) {
64
+ console.error("Failed to read interrupted workflows for auto-resume:", error);
65
+ return [];
66
+ }
67
+ const planned = [];
68
+ for (const runId of runIds) {
69
+ const asyncDir = path.join(asyncDirRoot, runId);
70
+ try {
71
+ const status = readStatus(asyncDir);
72
+ if (!status || status.mode !== "workflow" || status.parentWorkflowRunId !== undefined || status.sessionId !== currentSessionId)
73
+ continue;
74
+ if (!isReusableWorkflowStopCause(status.workflow?.stopCause))
75
+ continue;
76
+ const at = interruptedAt(status);
77
+ if (at === undefined || now - at > AUTO_RESUME_MAX_AGE_MS)
78
+ continue;
79
+ if (workflowResumeClaimState(asyncDir).kind !== "none" || hasWorkflowResumeRefusal(asyncDir))
80
+ continue;
81
+ const preparation = prepareWorkflowResume({ asyncDirRoot, sourceRunId: runId, trigger: "auto", now, currentSessionId, newRunId: reusableRunIds.get(runId) });
82
+ if (preparation.ok) {
83
+ const entry = { sessionId: currentSessionId, owner: input.owner, prepared: preparation.prepared, state: "pending" };
84
+ pendingAutoResumes.add(entry);
85
+ planned.push(entry);
86
+ continue;
87
+ }
88
+ // Another claimant continues it; nothing to record.
89
+ if (preparation.code === "claim-busy" || preparation.code === "claim-resumed")
90
+ continue;
91
+ recordWorkflowResumeRefusal(asyncDir, { reason: preparation.reason, byHand: preparation.byHand }, now);
92
+ }
93
+ catch (error) {
94
+ console.error(`Failed to prepare auto-resume of workflow ${runId}:`, error);
95
+ }
96
+ }
97
+ return planned;
98
+ }
99
+ /** The one line posted when an auto-resume launch did not start. */
100
+ export function autoResumeFailureText(sourceRunId, refusal) {
101
+ return `Workflow ${sourceRunId} was not auto-resumed: ${refusalSentence(refusal.reason, refusal.byHand)}`;
102
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The public launch of an async top-level workflow, saved in its run directory so the run can be
3
+ * relaunched later. A named resource is saved by name (its `runs.host` permit cannot be saved);
4
+ * structured tasks/chain launches save no params, so they cannot be relaunched from this record.
5
+ */
6
+ export type WorkflowLaunch = {
7
+ source: "script" | "resource";
8
+ params: Record<string, unknown>;
9
+ } | {
10
+ source: "structured";
11
+ };
12
+ export type WorkflowLaunchRecord = WorkflowLaunch & {
13
+ version: 1;
14
+ scriptDigest: string;
15
+ };
16
+ type WorkflowLaunchRecordRead = {
17
+ ok: true;
18
+ record: WorkflowLaunchRecord;
19
+ } | {
20
+ ok: false;
21
+ missing: boolean;
22
+ error: string;
23
+ };
24
+ /** Throws on a write failure; the launch must fail rather than start a run it cannot relaunch. */
25
+ export declare function writeWorkflowLaunchRecord(asyncDir: string, record: WorkflowLaunchRecord): void;
26
+ export declare function readWorkflowLaunchRecord(asyncDir: string): WorkflowLaunchRecordRead;
27
+ export {};
@@ -0,0 +1,48 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { writeAtomicJson } from "../shared/atomic-json.js";
4
+ const WORKFLOW_LAUNCH_RECORD_FILE = "launch.json";
5
+ function launchRecordPath(asyncDir) {
6
+ return path.join(asyncDir, WORKFLOW_LAUNCH_RECORD_FILE);
7
+ }
8
+ /** Throws on a write failure; the launch must fail rather than start a run it cannot relaunch. */
9
+ export function writeWorkflowLaunchRecord(asyncDir, record) {
10
+ writeAtomicJson(launchRecordPath(asyncDir), record);
11
+ }
12
+ function isPlainObject(value) {
13
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
14
+ }
15
+ function invalid(error) {
16
+ return { ok: false, missing: false, error };
17
+ }
18
+ export function readWorkflowLaunchRecord(asyncDir) {
19
+ let text;
20
+ try {
21
+ text = fs.readFileSync(launchRecordPath(asyncDir), "utf-8");
22
+ }
23
+ catch (error) {
24
+ if (error.code === "ENOENT")
25
+ return { ok: false, missing: true, error: "launch.json is missing" };
26
+ return invalid(`could not be read: ${error instanceof Error ? error.message : String(error)}`);
27
+ }
28
+ let value;
29
+ try {
30
+ value = JSON.parse(text);
31
+ }
32
+ catch (error) {
33
+ return invalid(`not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
34
+ }
35
+ if (!isPlainObject(value))
36
+ return invalid("not a JSON object");
37
+ if (value.version !== 1)
38
+ return invalid("version must be 1");
39
+ if (typeof value.scriptDigest !== "string" || !value.scriptDigest)
40
+ return invalid("scriptDigest must be a non-empty string");
41
+ if (value.source === "structured")
42
+ return { ok: true, record: { version: 1, source: "structured", scriptDigest: value.scriptDigest } };
43
+ if (value.source !== "script" && value.source !== "resource")
44
+ return invalid("source must be \"script\", \"resource\", or \"structured\"");
45
+ if (!isPlainObject(value.params))
46
+ return invalid("params must be an object");
47
+ return { ok: true, record: { version: 1, source: value.source, params: value.params, scriptDigest: value.scriptDigest } };
48
+ }