@selesai/code 0.13.12 → 0.13.14

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 (203) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/extensions/pi-subagents/CHANGELOG.md +77 -1
  3. package/dist/extensions/pi-subagents/VISION.md +12 -1
  4. package/dist/extensions/pi-subagents/docs/agents.md +13 -6
  5. package/dist/extensions/pi-subagents/docs/configuration.md +35 -9
  6. package/dist/extensions/pi-subagents/docs/extension-api.md +1 -1
  7. package/dist/extensions/pi-subagents/docs/missions.md +1 -1
  8. package/dist/extensions/pi-subagents/docs/models.md +6 -6
  9. package/dist/extensions/pi-subagents/docs/observability.md +6 -3
  10. package/dist/extensions/pi-subagents/docs/tool-reference.md +3 -3
  11. package/dist/extensions/pi-subagents/docs/watchdog.md +92 -114
  12. package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
  13. package/dist/extensions/pi-subagents/install.mjs +1 -2
  14. package/dist/extensions/pi-subagents/package.json +1 -1
  15. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  16. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +6 -6
  17. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  18. package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  19. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +4 -4
  20. package/dist/extensions/pi-subagents/skills/pi-subagents/references/review-and-validation.md +1 -1
  21. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +41 -4
  22. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +3 -0
  23. package/dist/extensions/pi-subagents/src/agents/agents.ts +120 -124
  24. package/dist/extensions/pi-subagents/src/agents/runtime-agent-registry.ts +5 -1
  25. package/dist/extensions/pi-subagents/src/api/preflight.ts +4 -0
  26. package/dist/extensions/pi-subagents/src/api/shared-types.ts +3 -0
  27. package/dist/extensions/pi-subagents/src/extension/config.ts +20 -0
  28. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +1 -0
  29. package/dist/extensions/pi-subagents/src/extension/schemas.ts +6 -2
  30. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +1 -1
  31. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +19 -13
  32. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +26 -8
  33. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +103 -23
  34. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +6 -2
  35. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -2
  36. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +54 -3
  37. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +16 -0
  38. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +22 -2
  39. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +63 -6
  40. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +4 -1
  41. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +69 -12
  42. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +13 -0
  43. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +3 -1
  44. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +21 -8
  45. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +59 -6
  46. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +95 -18
  47. package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +11 -43
  48. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +1 -0
  49. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
  50. package/dist/extensions/pi-subagents/src/runs/shared/lane-metadata.ts +24 -3
  51. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +4 -0
  52. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -6
  53. package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +1 -0
  54. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +33 -18
  55. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +73 -37
  56. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +33 -6
  57. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +4 -2
  58. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +20 -3
  59. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +21 -7
  60. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +1 -1
  61. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +467 -63
  62. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +3 -1
  63. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +0 -12
  64. package/dist/extensions/pi-subagents/src/shared/fork-session-cwd.ts +27 -0
  65. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +3 -0
  66. package/dist/extensions/pi-subagents/src/shared/types.ts +42 -4
  67. package/dist/extensions/pi-subagents/src/shared/utils.ts +18 -7
  68. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
  69. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +26 -12
  70. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +61 -2
  71. package/dist/extensions/pi-subagents/src/tui/fleet.ts +12 -7
  72. package/dist/extensions/pi-subagents/src/tui/render.ts +227 -14
  73. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +56 -34
  74. package/dist/extensions/pi-subagents/src/watchdog/diff-tool.ts +77 -0
  75. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +5 -3
  76. package/dist/extensions/pi-subagents/src/watchdog/guidance.ts +20 -0
  77. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +16 -13
  78. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +10 -9
  79. package/dist/extensions/pi-subagents/src/watchdog/render.ts +4 -5
  80. package/dist/extensions/pi-subagents/src/watchdog/review.ts +15 -4
  81. package/dist/extensions/pi-subagents/src/watchdog/rules.ts +70 -0
  82. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +75 -92
  83. package/dist/extensions/pi-subagents/src/watchdog/scope.ts +2 -1
  84. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +48 -104
  85. package/dist/extensions/pi-subagents/src/watchdog/types.ts +18 -32
  86. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +0 -1
  87. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +3 -2
  88. package/dist/extensions/pi-subagents/src/workflows/workflow-checklist.ts +439 -0
  89. package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +28 -1
  90. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +335 -21
  91. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +4 -1
  92. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +1 -1
  93. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +22 -18
  94. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +0 -1
  95. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +21 -18
  96. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +307 -22
  97. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -0
  98. package/dist/extensions/pi-subagents/test/support/helpers.ts +25 -0
  99. package/dist/extensions/pi-subagents/test/support/isolated-temp-root.mjs +15 -4
  100. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +17 -2
  101. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +96 -15
  102. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +53 -5
  103. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +20 -0
  104. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +221 -8
  105. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +45 -24
  106. package/dist/extensions/pi-subagents/test/unit/agent-scan-dirs.test.ts +133 -0
  107. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +28 -2
  108. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +1 -1
  109. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  110. package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +4 -1
  111. package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +18 -4
  112. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +16 -0
  113. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +38 -0
  114. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +2 -2
  115. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +21 -0
  116. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +19 -6
  117. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +26 -0
  118. package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +22 -1
  119. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +2 -6
  120. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +66 -1
  121. package/dist/extensions/pi-subagents/test/unit/pi-args-permission-system.test.ts +1 -1
  122. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +116 -10
  123. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +136 -18
  124. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +20 -0
  125. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +22 -0
  126. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +1 -0
  127. package/dist/extensions/pi-subagents/test/unit/project-panes-public-api.test.ts +8 -0
  128. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +127 -48
  129. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +68 -0
  130. package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +17 -0
  131. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +88 -0
  132. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -2
  133. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +9 -9
  134. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -2
  135. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +9 -3
  136. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +22 -9
  137. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +91 -8
  138. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +27 -0
  139. package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +50 -0
  140. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +0 -1
  141. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +2 -2
  142. package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +34 -0
  143. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +1 -2
  144. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +73 -13
  145. package/dist/extensions/pi-subagents/test/unit/watchdog-diff-tool.test.ts +79 -0
  146. package/dist/extensions/pi-subagents/test/unit/watchdog-permission-arbiter.test.ts +2 -4
  147. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +7 -8
  148. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +48 -3
  149. package/dist/extensions/pi-subagents/test/unit/watchdog-rules.test.ts +37 -0
  150. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +36 -118
  151. package/dist/extensions/pi-subagents/test/unit/watchdog-scope.test.ts +1 -1
  152. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +37 -14
  153. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +45 -26
  154. package/dist/extensions/pi-subagents/test/unit/workflow-checklist.test.ts +223 -0
  155. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +50 -1
  156. package/dist/extensions/pi-subagents/test/unit/workflow-preflight.test.ts +22 -0
  157. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +136 -4
  158. package/dist/extensions/pi-zentui/README.md +19 -5
  159. package/dist/extensions/pi-zentui/docs/configuration.md +37 -9
  160. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +29 -0
  161. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +31 -2
  162. package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +55 -11
  163. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +115 -3
  164. package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +119 -2
  165. package/dist/extensions/pi-zentui/extensions/zentui/thinking-experimental.ts +1768 -0
  166. package/dist/extensions/pi-zentui/extensions/zentui/thinking-status.ts +69 -0
  167. package/dist/extensions/pi-zentui/extensions/zentui/thinking-steps.ts +155 -0
  168. package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +2 -1
  169. package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +16 -2
  170. package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +1 -1
  171. package/dist/extensions/pi-zentui/test/config-load-lifecycle.test.ts +124 -0
  172. package/dist/extensions/pi-zentui/test/config.test.ts +82 -2
  173. package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +19 -4
  174. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +58 -9
  175. package/dist/extensions/pi-zentui/test/package-contents.mjs +33 -0
  176. package/dist/extensions/pi-zentui/test/prototype-patch-registry.test.ts +131 -0
  177. package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +4 -4
  178. package/dist/extensions/pi-zentui/test/settings-command.test.ts +289 -7
  179. package/dist/extensions/pi-zentui/test/settings-previews.test.ts +106 -0
  180. package/dist/extensions/pi-zentui/test/thinking-experimental-docs.test.ts +51 -0
  181. package/dist/extensions/pi-zentui/test/thinking-experimental-loader-diagnostics.mjs +54 -0
  182. package/dist/extensions/pi-zentui/test/thinking-experimental-missing-export.test.ts +44 -0
  183. package/dist/extensions/pi-zentui/test/thinking-experimental-rows.test.ts +297 -0
  184. package/dist/extensions/pi-zentui/test/thinking-experimental-tui.mjs +2329 -0
  185. package/dist/extensions/pi-zentui/test/thinking-experimental.test.ts +1668 -0
  186. package/dist/extensions/pi-zentui/test/thinking-status.test.ts +85 -0
  187. package/dist/extensions/pi-zentui/test/thinking-steps.test.ts +122 -0
  188. package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +58 -0
  189. package/dist/skills/brandkit/SKILL.md +0 -2
  190. package/dist/skills/{industrial-brutalist-ui → brutalist}/SKILL.md +1 -3
  191. package/dist/skills/gpt-taste/SKILL.md +0 -2
  192. package/dist/skills/image-to-code/SKILL.md +0 -2
  193. package/dist/skills/imagegen-frontend-mobile/SKILL.md +0 -2
  194. package/dist/skills/imagegen-frontend-web/SKILL.md +0 -2
  195. package/dist/skills/{minimalist-ui → minimalist}/SKILL.md +1 -3
  196. package/dist/skills/{full-output-enforcement → output}/SKILL.md +1 -3
  197. package/dist/skills/{redesign-existing-projects → redesign}/SKILL.md +1 -3
  198. package/dist/skills/{high-end-visual-design → soft}/SKILL.md +1 -3
  199. package/dist/skills/stitch/DESIGN.md +121 -0
  200. package/dist/skills/{stitch-design-taste → stitch}/SKILL.md +1 -3
  201. package/dist/skills/{design-taste-frontend → taste}/SKILL.md +1 -3
  202. package/dist/skills/{design-taste-frontend-v1 → taste-v1}/SKILL.md +2 -4
  203. package/package.json +1 -1
@@ -6,6 +6,7 @@ import test from "node:test";
6
6
  import { ACTIVE_RUN_INDEX_DIR, updateActiveRunIndex } from "../../src/runs/background/active-run-index.ts";
7
7
  import {
8
8
  finalizeProcessTerminal,
9
+ initializeProcessTerminal,
9
10
  processTerminalPath,
10
11
  readProcessTerminal,
11
12
  sanitizeProcessTerminal,
@@ -59,6 +60,27 @@ test("process-terminal proof requires the matching runner instance and writer cl
59
60
  }
60
61
  });
61
62
 
63
+ test("startup process-terminal candidate does not certify unobserved writers", () => {
64
+ const asyncDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-process-terminal-startup-"));
65
+ try {
66
+ initializeProcessTerminal(asyncDir, "run-startup", "runner-startup");
67
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({ runId: "run-startup", state: "running", lifecycleArtifactVersion: 3, steps: [{ agent: "worker", status: "running" }] }));
68
+ fs.writeFileSync(path.join(asyncDir, "events.jsonl"), "");
69
+
70
+ const proof = finalizeProcessTerminal(asyncDir, "run-startup", {
71
+ processInstanceId: "runner-startup",
72
+ closeObservedAt: 20,
73
+ exitCode: 1,
74
+ signal: null,
75
+ });
76
+
77
+ assert.equal(proof.state, "unknown");
78
+ assert.equal(proof.reason, "writer-close-unverified");
79
+ } finally {
80
+ fs.rmSync(asyncDir, { recursive: true, force: true });
81
+ }
82
+ });
83
+
62
84
  test("process-terminal rejects missing writer close evidence", () => {
63
85
  const asyncDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-process-terminal-"));
64
86
  try {
@@ -37,6 +37,7 @@ describe("profiles helpers", () => {
37
37
  homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subprofiles-home-"));
38
38
  process.env.HOME = homeDir;
39
39
  process.env.USERPROFILE = homeDir;
40
+ process.env[SELESAI_SUBAGENT_SELESAI_BINARY_ENV] = path.join(homeDir, "pi-test-bin");
40
41
  });
41
42
 
42
43
  afterEach(() => {
@@ -30,6 +30,8 @@ describe("public project-panes package export", () => {
30
30
 
31
31
  it("runs the structured lifecycle and verifies pane ownership before idle close", async () => {
32
32
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-public-project-pane-"));
33
+ const previousPiBinary = process.env.SELESAI_SUBAGENT_SELESAI_BINARY;
34
+ process.env.SELESAI_SUBAGENT_SELESAI_BINARY = path.join(root, "pi-bin");
33
35
  try {
34
36
  const projectRoot = fs.realpathSync(root);
35
37
  const calls: string[][] = [];
@@ -76,6 +78,8 @@ describe("public project-panes package export", () => {
76
78
  assert.ok(calls.some((args) => args.join(" ") === "tab focus tab-20"));
77
79
  assert.ok(calls.some((args) => args.join(" ") === "pane close w1:p20"));
78
80
  } finally {
81
+ if (previousPiBinary === undefined) delete process.env.SELESAI_SUBAGENT_SELESAI_BINARY;
82
+ else process.env.SELESAI_SUBAGENT_SELESAI_BINARY = previousPiBinary;
79
83
  fs.rmSync(root, { recursive: true, force: true });
80
84
  }
81
85
  });
@@ -131,6 +135,8 @@ describe("public project-panes package export", () => {
131
135
 
132
136
  it("returns a structured error and closes the pane when binding persistence fails", async () => {
133
137
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-public-project-pane-write-failure-"));
138
+ const previousPiBinary = process.env.SELESAI_SUBAGENT_SELESAI_BINARY;
139
+ process.env.SELESAI_SUBAGENT_SELESAI_BINARY = path.join(root, "pi-bin");
134
140
  try {
135
141
  fs.mkdirSync(path.join(root, ".selesai"));
136
142
  fs.writeFileSync(path.join(root, ".pi-subagents"), "directory collision");
@@ -152,6 +158,8 @@ describe("public project-panes package export", () => {
152
158
  }
153
159
  assert.ok(calls.some((args) => args.join(" ") === "pane close w1:p24"));
154
160
  } finally {
161
+ if (previousPiBinary === undefined) delete process.env.SELESAI_SUBAGENT_SELESAI_BINARY;
162
+ else process.env.SELESAI_SUBAGENT_SELESAI_BINARY = previousPiBinary;
155
163
  fs.rmSync(root, { recursive: true, force: true });
156
164
  }
157
165
  });
@@ -33,37 +33,6 @@ function result(agent: string, output: string) {
33
33
  };
34
34
  }
35
35
 
36
- function countedWorkflowJob(onNodesRead: () => void): AsyncJobState {
37
- const nodes = [
38
- { id: "step-0", kind: "step", agent: "scout", label: "Scout", status: "completed", flatIndex: 0, stepIndex: 0 },
39
- { id: "step-1", kind: "step", agent: "reviewer", label: "Review", status: "running", flatIndex: 1, stepIndex: 1 },
40
- ] as const;
41
- return {
42
- asyncId: "workflow-run",
43
- asyncDir: "/tmp/workflow-run",
44
- status: "running",
45
- mode: "workflow",
46
- agents: ["scout", "reviewer"],
47
- currentStep: 1,
48
- startedAt: 0,
49
- updatedAt: 1000,
50
- steps: [
51
- { index: 0, agent: "scout", status: "completed", workflowKey: "step-0" },
52
- { index: 1, agent: "reviewer", status: "running", workflowKey: "step-1" },
53
- ],
54
- workflowGraph: {
55
- runId: "workflow-run",
56
- mode: "workflow",
57
- phases: [],
58
- currentNodeId: "step-1",
59
- get nodes() {
60
- onNodesRead();
61
- return nodes;
62
- },
63
- } as never,
64
- };
65
- }
66
-
67
36
  test("row clips content to the available width", () => {
68
37
  const rendered = row("abcdef", 6, theme as any);
69
38
  assert.equal(visibleWidth(rendered), 6);
@@ -317,6 +286,133 @@ test("compact multi-result cards prefer bounded workflow labels over raw tasks",
317
286
  assert.match(text, /Ctrl\+Alt\+F Fleet/);
318
287
  });
319
288
 
289
+ test("workflow checklist does not map child-local result indexes onto graph nodes", () => {
290
+ const text = componentText(renderSubagentResult({
291
+ content: [{ type: "text", text: "failed" }],
292
+ details: {
293
+ mode: "workflow",
294
+ results: [
295
+ { ...result("scout", "ok"), index: 0 },
296
+ { ...result("writer", "failed"), index: 0, exitCode: 1 },
297
+ ],
298
+ workflowGraph: {
299
+ runId: "workflow-local-indexes",
300
+ mode: "workflow",
301
+ phases: [{ title: "inventory", nodeIds: ["inventory"] }, { title: "write", nodeIds: ["writer"] }],
302
+ nodes: [
303
+ { id: "inventory", kind: "agent", agent: "scout", label: "Inventory", status: "completed", flatIndex: 0 },
304
+ { id: "writer", kind: "agent", agent: "writer", label: "Writer", status: "failed", flatIndex: 1 },
305
+ ],
306
+ },
307
+ },
308
+ }, { expanded: true }, theme as any));
309
+
310
+ assert.match(text, /Checklist 1\/2 done · 1 failed/);
311
+ assert.match(text, /✗ write 1 failed/);
312
+ assert.match(text, /✗ Writer · writer · failed/);
313
+ });
314
+
315
+ test("collapsed workflow widgets lead with checklist phases while expanded widgets keep child detail", () => {
316
+ const now = 50_000;
317
+ const workflowGraph = {
318
+ runId: "workflow-collapsed",
319
+ mode: "workflow" as const,
320
+ phases: [
321
+ { title: "inventory", nodeIds: ["inventory"] },
322
+ { title: "writers", nodeIds: ["writer-a", "writer-b"] },
323
+ { title: "reviews", nodeIds: ["review"] },
324
+ { title: "gates", nodeIds: ["gate"] },
325
+ ],
326
+ nodes: [
327
+ { id: "inventory", kind: "step" as const, label: "inventory", agent: "scout", status: "completed" as const, flatIndex: 0 },
328
+ { id: "writer-a", kind: "agent" as const, label: "writer-a", agent: "writer", status: "running" as const, flatIndex: 1 },
329
+ { id: "writer-b", kind: "agent" as const, label: "writer-b", agent: "writer", status: "pending" as const, flatIndex: 2 },
330
+ { id: "review", kind: "step" as const, label: "review", agent: "reviewer", status: "pending" as const, flatIndex: 3 },
331
+ { id: "gate", kind: "step" as const, label: "gate", agent: "reviewer", status: "pending" as const, flatIndex: 4 },
332
+ ],
333
+ };
334
+ const job: AsyncJobState = {
335
+ asyncId: "workflow-collapsed",
336
+ asyncDir: "/tmp/workflow-collapsed",
337
+ cwd: "/tmp/workflow",
338
+ status: "running",
339
+ mode: "workflow",
340
+ agents: ["scout", "writer", "reviewer"],
341
+ stepsTotal: 5,
342
+ currentStep: 1,
343
+ toolCount: 38,
344
+ startedAt: 1_000,
345
+ updatedAt: now,
346
+ workflowGraph,
347
+ steps: [
348
+ { index: 0, workflowKey: "inventory", agent: "scout", status: "complete", description: "Inspect the repository" },
349
+ { index: 1, workflowKey: "writer-a", agent: "writer", status: "running", description: "Review auth flow", currentTool: "edit", currentToolStartedAt: now - 2_000, toolCount: 19, outputName: "review-a.md" },
350
+ { index: 2, workflowKey: "writer-b", agent: "writer", status: "pending", description: "Review billing flow", outputName: "review-b.md" },
351
+ ],
352
+ };
353
+
354
+ const collapsed = buildWidgetLines([job], theme, 240).join("\n");
355
+ assert.match(collapsed, /1\/5 done · 1 active · 3 queued/);
356
+ assert.match(collapsed, /✓ inventory/);
357
+ assert.match(collapsed, /writers 1 active · 1 queued/);
358
+ assert.match(collapsed, /◦ reviews 1 queued/);
359
+ assert.match(collapsed, /◦ gates 1 queued/);
360
+ assert.match(collapsed, /bottleneck/);
361
+ assert.match(collapsed, /Press configured-expand-key for live detail · Ctrl\+Alt\+F Fleet/);
362
+ assert.equal((collapsed.match(/1\/5 done · 1 active · 3 queued/g) ?? []).length, 1);
363
+ assert.doesNotMatch(collapsed, /Step \d\/\d|task:|workspace:|ref:|out(?:put)?:/i);
364
+
365
+ const expanded = buildWidgetLines([job], theme, 240, true).join("\n");
366
+ assert.match(expanded, /Stage 2\/5/);
367
+ assert.match(expanded, /task: .*Review auth flow/);
368
+ assert.match(expanded, /workspace:\/tmp\/workflow/);
369
+ assert.match(expanded, /out:review-a\.md/);
370
+ assert.match(expanded, /output: [\\/]tmp[\\/]workflow-collapsed[\\/]output-1\.log/);
371
+
372
+ const generic = buildWidgetLines([{
373
+ asyncId: "workflow-generic",
374
+ asyncDir: "/tmp/workflow-generic",
375
+ status: "running",
376
+ mode: "workflow",
377
+ agents: ["reviewer", "scout"],
378
+ stepsTotal: 2,
379
+ steps: [
380
+ { index: 0, agent: "reviewer", status: "running" },
381
+ { index: 1, agent: "scout", status: "running" },
382
+ ],
383
+ } as AsyncJobState], theme, 240).join("\n");
384
+ assert.match(generic, /Workflow 2 active/);
385
+ });
386
+
387
+ test("compact foreground workflow results use checklist phases instead of child rows", () => {
388
+ const workflowGraph = {
389
+ runId: "foreground-workflow",
390
+ mode: "workflow" as const,
391
+ phases: [{ title: "inventory", nodeIds: ["inventory"] }, { title: "writers", nodeIds: ["writer"] }],
392
+ nodes: [
393
+ { id: "inventory", kind: "step" as const, label: "inventory", agent: "scout", status: "completed" as const, flatIndex: 0 },
394
+ { id: "writer", kind: "agent" as const, label: "writer", agent: "writer", status: "running" as const, flatIndex: 1 },
395
+ ],
396
+ };
397
+ const text = componentText(renderSubagentResult({
398
+ content: [{ type: "text", text: "running" }],
399
+ details: {
400
+ mode: "workflow",
401
+ results: [
402
+ { ...result("scout", "done"), workflowKey: "inventory" },
403
+ { ...result("writer", ""), workflowKey: "writer", task: "raw task that should stay in expanded detail", progress: { status: "running", index: 1, agent: "writer", toolCount: 7, tokens: 0, durationMs: 2_000 } },
404
+ ],
405
+ workflowGraph,
406
+ },
407
+ } as never, { expanded: false }, theme as any));
408
+
409
+ assert.match(text, /1\/2 done · 1 active/);
410
+ assert.match(text, /✓ inventory/);
411
+ assert.match(text, /writers 1 active/);
412
+ assert.match(text, /Press configured-expand-key for live detail · Ctrl\+Alt\+F Fleet/);
413
+ assert.doesNotMatch(text, /Step \d\/\d|task:|workspace:|ref:|out(?:put)?:/i);
414
+ });
415
+
320
416
  test("compact chain rendering uses workflow graph labels and parallel groups", () => {
321
417
  const component = renderSubagentResult({
322
418
  content: [{ type: "text", text: "done" }],
@@ -357,23 +453,6 @@ test("compact chain rendering uses workflow graph labels and parallel groups", (
357
453
  assert.match(text, /Step 3\/3: Writer/);
358
454
  });
359
455
 
360
- test("widget rendering reuses one staged workflow projection", () => {
361
- let nodeReads = 0;
362
- const lines = buildWidgetLines([countedWorkflowJob(() => nodeReads++)], theme as any, 120, false, 0);
363
-
364
- assert.equal(nodeReads, 1);
365
- assert.match(lines.join("\n"), /staged lane · stage 2\/2 · Review · reviewer · running/);
366
- assert.match(lines.join("\n"), /Stage 2\/2: Review/);
367
- });
368
-
369
- test("widget render keys reuse one staged workflow projection", () => {
370
- let nodeReads = 0;
371
-
372
- widgetRenderKey(countedWorkflowJob(() => nodeReads++));
373
-
374
- assert.equal(nodeReads, 1);
375
- });
376
-
377
456
  test("compact chain rendering shows failed zero-child dynamic fanout groups", () => {
378
457
  const component = renderSubagentResult({
379
458
  content: [{ type: "text", text: "failed" }],
@@ -360,6 +360,70 @@ describe("async run status inspection", () => {
360
360
  }
361
361
  });
362
362
 
363
+ it("shows host steps in exact workflow status checklist", () => {
364
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-workflow-host-checklist-"));
365
+ try {
366
+ const asyncRoot = path.join(root, "runs");
367
+ const asyncDir = path.join(asyncRoot, "run-workflow-host");
368
+ fs.mkdirSync(asyncDir, { recursive: true });
369
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
370
+ runId: "run-workflow-host",
371
+ mode: "workflow",
372
+ state: "running",
373
+ startedAt: 100,
374
+ lastUpdate: 200,
375
+ workflowGraph: { runId: "run-workflow-host", mode: "workflow", phases: [], nodes: [{ id: "ci", kind: "host-step", label: "CI", status: "running", hostStep: { version: 1, kind: "host-step", monitorKind: "ci", id: "ci", label: "CI", state: "running", updatedAt: 200 } }] },
376
+ }, null, 2), "utf-8");
377
+
378
+ const result = inspectSubagentStatus({ id: "run-workflow-host" }, {
379
+ asyncDirRoot: asyncRoot,
380
+ resultsDir: path.join(root, "results"),
381
+ kill: () => true,
382
+ now: () => 250,
383
+ });
384
+
385
+ const text = textContent(result);
386
+ assert.equal(result.isError, undefined);
387
+ assert.match(text, /Workflow checklist: 0\/1 done · 1 active/);
388
+ assert.match(text, /CI 1 active/);
389
+ } finally {
390
+ fs.rmSync(root, { recursive: true, force: true });
391
+ }
392
+ });
393
+
394
+ it("keeps preflight as a plan hint outside the runtime checklist", () => {
395
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-workflow-preflight-"));
396
+ try {
397
+ const asyncRoot = path.join(root, "runs");
398
+ const asyncDir = path.join(asyncRoot, "run-workflow-preflight");
399
+ fs.mkdirSync(asyncDir, { recursive: true });
400
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
401
+ runId: "run-workflow-preflight",
402
+ mode: "workflow",
403
+ state: "running",
404
+ startedAt: 100,
405
+ lastUpdate: 200,
406
+ preflight: { version: 1, lanes: [{ key: "pr14", mode: "review" }] },
407
+ steps: [{ agent: "reviewer", workflowKey: "pr14-quality", status: "running" }],
408
+ }, null, 2), "utf-8");
409
+
410
+ const result = inspectSubagentStatus({ id: "run-workflow-preflight" }, {
411
+ asyncDirRoot: asyncRoot,
412
+ resultsDir: path.join(root, "results"),
413
+ kill: () => true,
414
+ now: () => 250,
415
+ });
416
+
417
+ const text = textContent(result);
418
+ assert.equal(result.isError, undefined);
419
+ assert.match(text, /Plan: 1 lane · pr14/);
420
+ assert.match(text, /Workflow checklist: 0\/1 done · 1 active/);
421
+ assert.doesNotMatch(text, /1 queued/);
422
+ } finally {
423
+ fs.rmSync(root, { recursive: true, force: true });
424
+ }
425
+ });
426
+
363
427
  it("refuses to tail status outputFile paths outside the async directory", () => {
364
428
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-transcript-escape-"));
365
429
  try {
@@ -1446,6 +1510,8 @@ describe("async run status inspection", () => {
1446
1510
  results: [{
1447
1511
  agent: "worker",
1448
1512
  success: false,
1513
+ structuredOutput: { payload: { ok: true } },
1514
+ structuredOutputPath: "/runs/structured-output/output.json",
1449
1515
  timeoutRecovery: {
1450
1516
  termination: "timed-out",
1451
1517
  changedFiles: ["input.md"],
@@ -1470,6 +1536,8 @@ describe("async run status inspection", () => {
1470
1536
  assert.match(text, /Recovery needed: review the diff and artifacts before resuming or launching dependent stages\./);
1471
1537
  assert.match(text, /requested report: missing/);
1472
1538
  assert.match(text, /changed tracked files: input\.md/);
1539
+ assert.match(text, /Structured output: \{"payload":\{"ok":true\}\}/);
1540
+ assert.match(text, /Structured output path: \/runs\/structured-output\/output\.json/);
1473
1541
  } finally {
1474
1542
  fs.rmSync(root, { recursive: true, force: true });
1475
1543
  }
@@ -123,6 +123,23 @@ describe("runtime agent registration", () => {
123
123
  registration.dispose();
124
124
  });
125
125
 
126
+ it("preserves excludeTools on runtime agents", () => {
127
+ const registration = registerAgent({
128
+ pi,
129
+ name: "runtime-exclude-helper",
130
+ definition: {
131
+ description: "Runtime exclude helper",
132
+ systemPrompt: "Help at runtime.",
133
+ tools: ["read", "write"],
134
+ excludeTools: ["write", "unknown_tool"],
135
+ },
136
+ });
137
+
138
+ const agent = mergeRuntimeAgents(pi, discoverAgents(tempProject, "both")).agents.find((candidate) => candidate.name === "runtime-exclude-helper");
139
+ assert.deepEqual(agent?.excludeTools, ["write", "unknown_tool"]);
140
+ registration.dispose();
141
+ });
142
+
126
143
  it("registers through the owner runtime when consumer and owner API objects differ", () => {
127
144
  const events = makeEventBus();
128
145
  const ownerPi = makePiWithEvents(events);
@@ -97,6 +97,10 @@ function text(result: Awaited<ReturnType<ScheduledRunManager["handleToolCall"]>>
97
97
  return result.content[0]?.type === "text" ? result.content[0].text : "";
98
98
  }
99
99
 
100
+ function detailRecords(result: Awaited<ReturnType<ScheduledRunManager["handleToolCall"]>>): Array<Record<string, unknown>> {
101
+ return result.details?.schedules?.records as Array<Record<string, unknown>> ?? [];
102
+ }
103
+
100
104
  async function flush(): Promise<void> {
101
105
  for (let i = 0; i < 8; i++) await Promise.resolve();
102
106
  }
@@ -158,6 +162,85 @@ describe("project schedule management", () => {
158
162
  assert.match(text(shown), /Night review/);
159
163
  });
160
164
 
165
+ it("restores session-only schedules only in the creating session", async () => {
166
+ const owner = harness({ sessionId: "owner-session" });
167
+ const created = await owner.manager.handleToolCall({
168
+ action: "schedule.create",
169
+ id: "owner-only",
170
+ name: "Owner only",
171
+ every: "1h",
172
+ cwd: owner.ctx.cwd,
173
+ sessionOnly: true,
174
+ workflowScript: "return 1",
175
+ }, owner.ctx);
176
+ assert.equal(created.isError, undefined);
177
+ assert.equal(detailRecords(created)[0]?.sessionOnly, true);
178
+ assert.equal("ownerSessionFile" in detailRecords(created)[0]!, false);
179
+
180
+ const root = scheduledRunStorePath(owner.ctx.cwd, undefined, path.join(owner.root, "stores"));
181
+ const [record] = listScheduledRunSummaries(owner.ctx.cwd, path.join(owner.root, "stores"));
182
+ assert.equal(record?.sessionOnly, true);
183
+ assert.equal(record?.ownerSessionFile, owner.ctx.sessionManager.getSessionFile());
184
+
185
+ owner.manager.stop();
186
+ const otherTimers = new FakeTimers();
187
+ const otherLaunches: Launch[] = [];
188
+ const other = createScheduledRunManager({
189
+ config: { scheduledRuns: { enabled: true } },
190
+ storeRoot: path.join(owner.root, "stores"),
191
+ now: () => owner.clock.now,
192
+ timers: otherTimers,
193
+ launch: (params, launchCtx) => new Promise((resolve) => otherLaunches.push({ params: params as Record<string, unknown>, ctx: launchCtx, resolve: resolve as Launch["resolve"] })) as never,
194
+ });
195
+ const otherContext = context(owner.ctx.cwd, "other-session");
196
+ other.bindSession(otherContext);
197
+ assert.equal(otherTimers.values.size, 0, "non-owner sessions must not arm session-only schedules");
198
+
199
+ const manual = await other.handleToolCall({ action: "schedule.run", id: "owner-only" }, otherContext);
200
+ assert.match(text(manual), /not its owner/);
201
+ assert.equal("ownerSessionFile" in detailRecords(manual)[0]!, false);
202
+ const shownToOther = await other.handleToolCall({ action: "schedule.show", id: "owner-only" }, otherContext);
203
+ assert.equal("ownerSessionFile" in detailRecords(shownToOther)[0]!, false);
204
+ const listedToOther = await other.handleToolCall({ action: "schedule.list" }, otherContext);
205
+ assert.equal("ownerSessionFile" in detailRecords(listedToOther)[0]!, false);
206
+ assert.equal(otherLaunches.length, 0);
207
+ assert.equal(listScheduledRunSummaries(owner.ctx.cwd, path.join(owner.root, "stores"))[0]?.ownerSessionFile, owner.ctx.sessionManager.getSessionFile());
208
+ assert.equal(fs.existsSync(path.join(root, "owner-only", "active.lock")), false);
209
+
210
+ const ownerTimers = new FakeTimers();
211
+ const ownerAgain = createScheduledRunManager({
212
+ config: { scheduledRuns: { enabled: true } },
213
+ storeRoot: path.join(owner.root, "stores"),
214
+ now: () => owner.clock.now,
215
+ timers: ownerTimers,
216
+ launch: async () => ({ content: [{ type: "text", text: "unused" }], details: { mode: "management", results: [] } }),
217
+ });
218
+ ownerAgain.bindSession(context(owner.ctx.cwd, "owner-session"));
219
+ assert.equal(ownerTimers.values.size, 1, "the creating session must restore its schedule");
220
+ });
221
+
222
+ it("re-arms an owner timer after a non-owner context consumes it", async () => {
223
+ const h = harness({ sessionId: "owner-session" });
224
+ await h.manager.handleToolCall({ action: "schedule.create", id: "owner-only", cwd: h.ctx.cwd, every: "1h", sessionOnly: true, workflowScript: "return runs.run('main', { agent: 'worker' })" }, h.ctx);
225
+ const nextRunAt = listScheduledRunSummaries(h.ctx.cwd, path.join(h.root, "stores"))[0]?.trigger.nextRunAt;
226
+ h.manager.bindSession(context(h.ctx.cwd, "other-session"));
227
+ h.clock.now += 3_600_000;
228
+ h.timers.fireAll();
229
+ await flush();
230
+ assert.equal(h.launches.length, 0, "a non-owner context must not launch the schedule");
231
+ assert.equal(h.timers.values.size, 0, "the consumed non-owner timer is not left spinning");
232
+ assert.equal(listScheduledRunSummaries(h.ctx.cwd, path.join(h.root, "stores"))[0]?.trigger.nextRunAt, nextRunAt, "a non-owner context must not advance the schedule");
233
+
234
+ h.manager.bindSession(h.ctx);
235
+ assert.equal(h.timers.values.size, 1, "rebinding the owner must restore the timer");
236
+ h.timers.fireAll();
237
+ await flush();
238
+ assert.equal(h.launches.length, 1);
239
+ assert.equal(h.launches[0]!.ctx.sessionManager.getSessionId(), "owner-session");
240
+ h.launches[0]!.resolve({ content: [{ type: "text", text: "Async" }], details: { mode: "single", results: [], asyncId: "owner-async" } });
241
+ await flush();
242
+ });
243
+
161
244
  it("does not let completed one-shot schedules consume maxPending capacity", async () => {
162
245
  const h = harness({ config: { scheduledRuns: { enabled: true, maxPending: 1 } } });
163
246
  await h.manager.handleToolCall({ action: "schedule.create", id: "completed", at: "+1h", workflowScript: "return runs.run('main', { agent: 'worker' })" }, h.ctx);
@@ -668,6 +751,11 @@ describe("recurring schedule execution", () => {
668
751
  const targetCtx = context(target, "target-session");
669
752
  h.manager.bindSession(targetCtx);
670
753
  h.manager.bindSession(h.ctx);
754
+ const sessionOnly = await h.manager.handleToolCall({ action: "schedule.create", id: "session-only-targeted", cwd: target, every: "1h", sessionOnly: true, workflowScript: "return runs.run('main', { agent: 'worker' })" }, h.ctx);
755
+ assert.equal(sessionOnly.isError, true);
756
+ assert.match(text(sessionOnly), /sessionOnly schedules cannot use an explicit cross-project cwd/);
757
+ assert.deepEqual(listScheduledRunSummaries(target, path.join(h.root, "stores")), [], "rejected session-only schedules must not dead-end in the target project");
758
+
671
759
  await h.manager.handleToolCall({ action: "schedule.create", id: "targeted", cwd: target, every: "1h", workflowScript: "return runs.run('main', { agent: 'worker' })" }, h.ctx);
672
760
  h.clock.now += 3_600_000;
673
761
  h.timers.fireAll();
@@ -596,7 +596,10 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
596
596
  const reviewedRecoveryBranch = acceptanceStringBranches.find((branch) => Array.isArray(branch.enum) && branch.enum.includes("reviewed"));
597
597
  assert.deepEqual(reviewedRecoveryBranch?.enum, ["reviewed"]);
598
598
  assert.equal(reviewedRecoveryBranch?.deprecated, true);
599
+ assert.equal(acceptanceStringBranches.some((branch) => branch.enum === undefined), true, "acceptance should tolerate JSON-encoded object strings");
599
600
  assert.match(String(acceptanceSchema.description ?? ""), /reviewer\/read-only calls, omit acceptance/i);
601
+ assert.match(String(acceptanceSchema.description ?? ""), /prefer an inline JSON object/i);
602
+ assert.match(String(acceptanceSchema.description ?? ""), /JSON-encoded object strings are tolerated only during input normalization/i);
600
603
  assert.match(String(acceptanceSchema.description ?? ""), /acceptance\.review\.required/);
601
604
  const acceptanceObjectBranch = anyOfBranches(acceptanceSchema).find((branch) => branch.type === "object");
602
605
  assert.ok(acceptanceObjectBranch, "acceptance should support object config");
@@ -621,11 +624,10 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
621
624
  { action: "not-a-real-action" },
622
625
  { config: { name: "reviewer", description: "Review things" } },
623
626
  { config: JSON.stringify({ name: "reviewer", description: "Review things" }) },
627
+ { agent: "worker", task: "Fix", acceptance: JSON.stringify({ level: "checked", evidence: ["commands-run"] }) },
624
628
  ];
625
629
  const invalidValues = [
626
630
  { skill: 123 },
627
- { agent: "worker", task: "Fix", acceptance: "none" },
628
- { agent: "worker", task: "Fix", acceptance: "verified" },
629
631
  { skill: [123] },
630
632
  { output: 123 },
631
633
  { timeoutMs: 0 },
@@ -1429,28 +1429,28 @@ describe("scripted workflow runtime", () => {
1429
1429
  );
1430
1430
  });
1431
1431
 
1432
- it("passes per-child worktree controls through runs.run and runs.all", async () => {
1433
- const launches: Array<{ key: string; worktree: unknown }> = [];
1432
+ it("passes per-child workflow controls through runs.run and runs.all", async () => {
1433
+ const launches: Array<{ key: string; worktree: unknown; control: unknown }> = [];
1434
1434
  await runWorkflowScript({
1435
1435
  script: `
1436
- const one = await runs.run("one", { agent: "worker", task: "one", worktree: true });
1436
+ const one = await runs.run("one", { agent: "worker", task: "one", worktree: true, control: { needsAttentionAfterMs: 111 } });
1437
1437
  const rest = await runs.all([
1438
- { key: "two", agent: "worker", task: "two", worktree: true },
1439
- { key: "three", agent: "reviewer", task: "three", worktree: false }
1438
+ { key: "two", agent: "worker", task: "two", worktree: true, control: { activeNoticeAfterMs: 222 } },
1439
+ { key: "three", agent: "reviewer", task: "three", worktree: false, control: { enabled: false } }
1440
1440
  ]);
1441
1441
  return [one.key, ...rest.map((entry) => entry.key)];
1442
1442
  `,
1443
1443
  timeoutMs: 2_000,
1444
1444
  async launch(key, params) {
1445
- launches.push({ key, worktree: params.worktree });
1445
+ launches.push({ key, worktree: params.worktree, control: params.control });
1446
1446
  return { key, ok: true, output: key, artifactPaths: [], results: [] };
1447
1447
  },
1448
1448
  async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
1449
1449
  });
1450
1450
  assert.deepEqual(launches, [
1451
- { key: "one", worktree: true },
1452
- { key: "two", worktree: true },
1453
- { key: "three", worktree: false },
1451
+ { key: "one", worktree: true, control: { needsAttentionAfterMs: 111 } },
1452
+ { key: "two", worktree: true, control: { activeNoticeAfterMs: 222 } },
1453
+ { key: "three", worktree: false, control: { enabled: false } },
1454
1454
  ]);
1455
1455
  });
1456
1456
 
@@ -139,7 +139,7 @@ describe("acknowledged steering action", () => {
139
139
  assert.equal(result.isError, undefined);
140
140
  assert.equal(result.details.steering?.state, "delivered");
141
141
  assert.match(result.content[0]!.text, /Steering delivered/);
142
- assert.match(result.content[0]!.text, /Message: "correct course"/);
142
+ assert.match(result.content[0]!.text, /Message sent:\n```text\ncorrect course\n```/);
143
143
  } finally {
144
144
  removeAsyncDir(asyncDir);
145
145
  }
@@ -330,7 +330,7 @@ describe("acknowledged steering action", () => {
330
330
  const result = await action;
331
331
  assert.equal(result.isError, undefined);
332
332
  assert.equal(result.details.steering?.state, "recovered");
333
- assert.match(result.content[0]!.text, /Message: "correct course"/);
333
+ assert.match(result.content[0]!.text, /Message sent:\n```text\ncorrect course\n```/);
334
334
  assert.equal(result.details.steering?.replacementRunId, "replacement");
335
335
  assert.ok(result.details.steering?.targets[0]?.lateDeliveredAt);
336
336
  const limits = receivedLimits as { timeoutMs: number; absoluteDeadlineAt: number; toolBudget: unknown };
@@ -3,17 +3,23 @@ import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { describe, it } from "node:test";
6
- import { actionResultFromSteeringStatus, claimSteeringRecovery, createSteeringStatus, recordSteeringRequest, remainingSteeringRecoveryLimits, steeringMessagePreview, terminalSteeringNoticeState, updateSteeringTarget } from "../../src/runs/background/steering.ts";
6
+ import { actionResultFromSteeringStatus, claimSteeringRecovery, createSteeringStatus, recordSteeringRequest, remainingSteeringRecoveryLimits, steeringMessagePreview, steeringReceipt, terminalSteeringNoticeState, updateSteeringTarget } from "../../src/runs/background/steering.ts";
7
7
  import { applySteeringRecoveryAgentConfig } from "../../src/runs/background/async-resume.ts";
8
8
  import type { AgentConfig } from "../../src/agents/agents.ts";
9
9
 
10
10
  describe("steering lifecycle ledger", () => {
11
- it("redacts and bounds steering message previews", () => {
11
+ it("redacts, bounds, and separates steering message previews", () => {
12
12
  const secret = "ghp_1234567890abcdef";
13
- const preview = steeringMessagePreview(`Use ${secret}\n${"x".repeat(200)}`);
13
+ const message = `Use ${secret}\n${"x".repeat(200)}`;
14
+ const preview = steeringMessagePreview(message);
15
+ const receipt = steeringReceipt(message, "Steering delivered.");
14
16
  assert.ok(preview.length <= 160);
15
17
  assert.match(preview, /\[redacted\]/);
16
18
  assert.doesNotMatch(preview, new RegExp(secret));
19
+ assert.match(receipt, /\[redacted\]/);
20
+ assert.doesNotMatch(receipt, new RegExp(secret));
21
+ assert.match(receipt, /^Steering delivered\.\n\nMessage sent:\n```text\n[\s\S]+\n```$/);
22
+ assert.match(steeringReceipt("review this:\n```\ntext\n```", "Steering queued."), /^Steering queued\.\n\nMessage sent:\n````text\n[\s\S]+\n````$/);
17
23
  });
18
24
 
19
25
  it("retains 20 recent requests while aggregate totals remain monotonic", () => {