@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
@@ -130,6 +130,19 @@ describe("builtin agent overrides", () => {
130
130
  assert.equal(researcher?.mcpDirectTools, undefined);
131
131
  });
132
132
 
133
+ it("applies excludeTools settings overrides to builtin agents", () => {
134
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
135
+ subagents: {
136
+ agentOverrides: {
137
+ researcher: { excludeTools: ["write", "unknown_tool"] },
138
+ },
139
+ },
140
+ });
141
+
142
+ const researcher = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "researcher");
143
+ assert.deepEqual(researcher?.excludeTools, ["write", "unknown_tool"]);
144
+ });
145
+
133
146
  it("surfaces invalid string tool override settings", () => {
134
147
  const settingsPath = path.join(tempHome, ".selesai", "agent", "settings.json");
135
148
  writeJson(settingsPath, {
@@ -703,9 +716,10 @@ describe("builtin agent overrides", () => {
703
716
  assert.equal(reviewer.override?.scope, "project");
704
717
  });
705
718
 
706
- it("frontmatter wins per-field over agentOverrides for a shadowing project agent", () => {
719
+ it("agentOverrides replace frontmatter for a shadowing project agent", () => {
707
720
  fs.mkdirSync(path.join(tempProject, ".selesai"), { recursive: true });
708
721
  writeJson(path.join(tempProject, ".selesai", "settings.json"), {
722
+
709
723
  subagents: { agentOverrides: { reviewer: { model: "openai/gpt-5.4" } } },
710
724
  });
711
725
  writeProjectAgent(tempProject, "reviewer", `---\nname: reviewer\ndescription: Project reviewer\nmodel: google/gemini-3-pro\n---\n\nUse the project reviewer.\n`);
@@ -713,13 +727,14 @@ describe("builtin agent overrides", () => {
713
727
  const reviewer = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "reviewer");
714
728
  assert.ok(reviewer);
715
729
  assert.equal(reviewer.source, "project");
716
- assert.equal(reviewer.model, "google/gemini-3-pro");
717
- assert.equal(reviewer.override, undefined);
730
+ assert.equal(reviewer.model, "openai/gpt-5.4");
731
+ assert.equal(reviewer.override?.scope, "project");
718
732
  });
719
733
 
720
- it("fills in unset fields on a custom project agent from project agentOverrides", () => {
734
+ it("applies project agentOverrides to a custom project agent", () => {
721
735
  fs.mkdirSync(path.join(tempProject, ".selesai"), { recursive: true });
722
736
  writeJson(path.join(tempProject, ".selesai", "settings.json"), {
737
+
723
738
  subagents: {
724
739
  agentOverrides: {
725
740
  implementer: {
@@ -769,8 +784,9 @@ describe("builtin agent overrides", () => {
769
784
  assert.equal(implementer.override?.path, path.join(tempProject, ".selesai", "settings.json"));
770
785
  });
771
786
 
772
- it("fills in unset fields on a custom user agent from user agentOverrides", () => {
787
+ it("applies user agentOverrides to a custom user agent", () => {
773
788
  writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
789
+
774
790
  subagents: { agentOverrides: { implementer: { model: "anthropic/claude-sonnet-4-6" } } },
775
791
  });
776
792
  writeUserAgent(tempHome, "implementer", `---\nname: implementer\ndescription: TDD implementer\n---\n\nDrive the failing test first.\n`);
@@ -813,9 +829,10 @@ describe("builtin agent overrides", () => {
813
829
  assert.equal(implementer.override?.scope, "project");
814
830
  });
815
831
 
816
- it("keeps explicit custom frontmatter fields over matching agentOverrides", () => {
832
+ it("agentOverrides replace explicit custom frontmatter fields", () => {
817
833
  fs.mkdirSync(path.join(tempProject, ".selesai"), { recursive: true });
818
834
  writeJson(path.join(tempProject, ".selesai", "settings.json"), {
835
+
819
836
  subagents: {
820
837
  agentOverrides: {
821
838
  implementer: {
@@ -830,6 +847,7 @@ describe("builtin agent overrides", () => {
830
847
  inheritProjectContext: true,
831
848
  defaultContext: "fork",
832
849
  acceptanceRole: "writer",
850
+ systemPrompt: "Override prompt",
833
851
  completionGuard: true,
834
852
  },
835
853
  },
@@ -839,32 +857,34 @@ describe("builtin agent overrides", () => {
839
857
 
840
858
  const implementer = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "implementer");
841
859
  assert.ok(implementer);
842
- assert.equal(implementer.output, "artifacts/explicit.md");
843
- assert.equal(implementer.outputMode, "inline");
844
- assert.deepEqual(implementer.defaultReads, ["explicit.md"]);
845
- assert.equal(implementer.model, "google/gemini-3-pro");
846
- assert.equal(implementer.fast, false);
847
- assert.equal(implementer.thinking, "medium");
848
- assert.deepEqual(implementer.tools, ["read"]);
849
- assert.deepEqual(implementer.mcpDirectTools, ["local_tool"]);
850
- assert.deepEqual(implementer.skills, ["agent-skill"]);
851
- assert.equal(implementer.inheritProjectContext, false);
852
- assert.equal(implementer.defaultContext, "fresh");
853
- assert.equal(implementer.acceptanceRole, "read-only");
854
- assert.equal(implementer.completionGuard, false);
855
- assert.equal(implementer.override, undefined);
860
+ assert.equal(implementer.output, "artifacts/override.md");
861
+ assert.equal(implementer.outputMode, "file-only");
862
+ assert.deepEqual(implementer.defaultReads, ["override.md"]);
863
+ assert.equal(implementer.model, "anthropic/claude-sonnet-4-6");
864
+ assert.equal(implementer.fast, true);
865
+ assert.equal(implementer.thinking, "high");
866
+ assert.deepEqual(implementer.tools, ["bash"]);
867
+ assert.equal(implementer.mcpDirectTools, undefined);
868
+ assert.deepEqual(implementer.skills, ["override-skill"]);
869
+ assert.equal(implementer.inheritProjectContext, true);
870
+ assert.equal(implementer.defaultContext, "fork");
871
+ assert.equal(implementer.acceptanceRole, "writer");
872
+ assert.equal(implementer.systemPrompt, "Override prompt");
873
+ assert.equal(implementer.completionGuard, true);
874
+ assert.equal(implementer.override?.scope, "project");
856
875
  });
857
876
 
858
- it("keeps explicit output and defaultReads frontmatter when overrides clear them", () => {
877
+ it("lets false overrides clear explicit output and defaultReads frontmatter", () => {
859
878
  fs.mkdirSync(path.join(tempProject, ".selesai"), { recursive: true });
860
879
  writeJson(path.join(tempProject, ".selesai", "settings.json"), {
880
+
861
881
  subagents: { agentOverrides: { implementer: { output: false, defaultReads: false } } },
862
882
  });
863
883
  writeProjectAgent(tempProject, "implementer", `---\nname: implementer\ndescription: TDD implementer\noutput: explicit.md\ndefaultReads: explicit.md\n---\n\nDrive the failing test first.\n`);
864
884
 
865
885
  const implementer = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "implementer");
866
- assert.equal(implementer?.output, "explicit.md");
867
- assert.deepEqual(implementer?.defaultReads, ["explicit.md"]);
886
+ assert.equal(implementer?.output, undefined);
887
+ assert.deepEqual(implementer?.defaultReads, undefined);
868
888
  });
869
889
 
870
890
  it("keeps an explicit empty defaultReads override distinct from false", () => {
@@ -1036,8 +1056,9 @@ describe("builtin agent overrides", () => {
1036
1056
  const packageRoot = path.join(tempProject, "package-agents");
1037
1057
  fs.mkdirSync(path.join(packageRoot, "agents"), { recursive: true });
1038
1058
  writeJson(path.join(packageRoot, "package.json"), { "pi-subagents": { agents: ["agents"] } });
1039
- fs.writeFileSync(path.join(packageRoot, "agents", "package-scout.md"), `---\nname: package-scout\ndescription: Package scout\n---\n\nScout the package.\n`, "utf-8");
1059
+ fs.writeFileSync(path.join(packageRoot, "agents", "package-scout.md"), `---\nname: package-scout\ndescription: Package scout\noutput: package-frontmatter.md\ndefaultReads: PACKAGE-FRONTMATTER.md\n---\n\nScout the package.\n`, "utf-8");
1040
1060
  writeJson(path.join(tempProject, ".selesai", "settings.json"), {
1061
+
1041
1062
  packages: [packageRoot],
1042
1063
  subagents: { agentOverrides: { "package-scout": { output: "package.md", defaultReads: ["PACKAGE.md"] } } },
1043
1064
  });
@@ -0,0 +1,133 @@
1
+ import assert from "node:assert/strict";
2
+ import { afterEach, beforeEach, describe, it } from "node:test";
3
+ import * as fs from "node:fs";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+ import { clearAgentDiscoveryCache, discoverAgents } from "../../src/agents/agents.ts";
7
+
8
+ function tempDir(prefix: string): string {
9
+ return fs.mkdtempSync(path.join(os.tmpdir(), `agent-scan-${prefix}-`));
10
+ }
11
+
12
+ function writeAgent(dir: string, name: string, description = "scan dirs test"): void {
13
+ fs.mkdirSync(dir, { recursive: true });
14
+ fs.writeFileSync(path.join(dir, `${name}.md`), `---\nname: ${name}\ndescription: ${description}\n---\nbody\n`, "utf-8");
15
+ }
16
+
17
+ describe("settings subagents.agentScanDirs", () => {
18
+ let agentDir: string;
19
+ let projectDir: string;
20
+ let scanRoot: string;
21
+ let previousAgentDir: string | undefined;
22
+
23
+ beforeEach(() => {
24
+ agentDir = tempDir("agent");
25
+ projectDir = tempDir("project");
26
+ scanRoot = tempDir("scan");
27
+ previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
28
+ process.env.SELESAI_CODING_AGENT_DIR = agentDir;
29
+ fs.mkdirSync(path.join(agentDir, "agents"), { recursive: true });
30
+ fs.mkdirSync(path.join(projectDir, ".selesai"), { recursive: true });
31
+ clearAgentDiscoveryCache();
32
+ });
33
+
34
+ afterEach(() => {
35
+ if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
36
+ else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
37
+ for (const dir of [agentDir, projectDir, scanRoot]) fs.rmSync(dir, { recursive: true, force: true });
38
+ clearAgentDiscoveryCache();
39
+ });
40
+
41
+ function writeUserSettings(value: unknown): void {
42
+ fs.writeFileSync(path.join(agentDir, "settings.json"), JSON.stringify(value), "utf-8");
43
+ }
44
+
45
+ function writeProjectSettings(value: unknown): void {
46
+ fs.writeFileSync(path.join(projectDir, ".selesai", "settings.json"), JSON.stringify(value), "utf-8");
47
+ }
48
+
49
+ it("discovers agents from configured user scan directories", () => {
50
+ writeAgent(scanRoot, "scan-writer");
51
+ writeUserSettings({ subagents: { agentScanDirs: [scanRoot] } });
52
+
53
+ const result = discoverAgents(projectDir, "user");
54
+
55
+ const agent = result.agents.find((candidate) => candidate.name === "scan-writer");
56
+ assert.equal(agent?.source, "user");
57
+ assert.equal(agent?.filePath, path.join(scanRoot, "scan-writer.md"));
58
+ });
59
+
60
+ it("expands one wildcard directory segment and invalidates when children are added", () => {
61
+ writeUserSettings({ subagents: { agentScanDirs: [path.join(scanRoot, "*", "agents")] } });
62
+ assert.equal(discoverAgents(projectDir, "user").agents.some((agent) => agent.name === "late-writer"), false);
63
+
64
+ writeAgent(path.join(scanRoot, "template-a", "agents"), "late-writer");
65
+ const result = discoverAgents(projectDir, "user");
66
+
67
+ assert.equal(result.agents.some((agent) => agent.name === "late-writer"), true);
68
+ });
69
+
70
+ it("invalidates when an existing wildcard child receives its agents directory", () => {
71
+ fs.mkdirSync(path.join(scanRoot, "template-a"));
72
+ writeUserSettings({ subagents: { agentScanDirs: [path.join(scanRoot, "*", "agents")] } });
73
+ assert.equal(discoverAgents(projectDir, "user").agents.some((agent) => agent.name === "late-nested-writer"), false);
74
+
75
+ writeAgent(path.join(scanRoot, "template-a", "agents"), "late-nested-writer");
76
+ const result = discoverAgents(projectDir, "user");
77
+
78
+ assert.equal(result.agents.some((agent) => agent.name === "late-nested-writer"), true);
79
+ });
80
+
81
+ it("keeps user and project scan directories scoped", () => {
82
+ const userScanDir = path.join(scanRoot, "user-agents");
83
+ const projectScanDir = path.join(scanRoot, "project-agents");
84
+ writeAgent(userScanDir, "user-scan");
85
+ writeAgent(projectScanDir, "project-scan");
86
+ writeUserSettings({ subagents: { agentScanDirs: [userScanDir] } });
87
+ writeProjectSettings({ subagents: { agentScanDirs: [projectScanDir] } });
88
+
89
+ assert.equal(discoverAgents(projectDir, "user").agents.some((agent) => agent.name === "project-scan"), false);
90
+ assert.equal(discoverAgents(projectDir, "project").agents.some((agent) => agent.name === "user-scan"), false);
91
+ assert.equal(discoverAgents(projectDir, "both").agents.some((agent) => agent.name === "user-scan"), true);
92
+ assert.equal(discoverAgents(projectDir, "both").agents.some((agent) => agent.name === "project-scan"), true);
93
+ });
94
+
95
+ it("lets fixed user agents override same-name scan-dir agents", () => {
96
+ writeAgent(path.join(scanRoot, "template", "agents"), "writer", "from scan dir");
97
+ writeAgent(path.join(agentDir, "agents"), "writer", "from fixed user dir");
98
+ writeUserSettings({ subagents: { agentScanDirs: [path.join(scanRoot, "*", "agents")] } });
99
+
100
+ const agent = discoverAgents(projectDir, "both").agents.find((candidate) => candidate.name === "writer");
101
+
102
+ assert.equal(agent?.filePath, path.join(agentDir, "agents", "writer.md"));
103
+ });
104
+
105
+ it("accepts backslash-separated wildcard patterns", () => {
106
+ writeAgent(path.join(scanRoot, "template", "agents"), "backslash-scan");
107
+ writeUserSettings({ subagents: { agentScanDirs: [`${scanRoot}\\*\\agents`] } });
108
+
109
+ const result = discoverAgents(projectDir, "user");
110
+
111
+ assert.equal(result.agents.some((agent) => agent.name === "backslash-scan"), true);
112
+ });
113
+
114
+ it("does not treat constrained wildcard segments as broad directory scans", () => {
115
+ writeAgent(path.join(scanRoot, "flow-a", "agents"), "flow-scan");
116
+ writeAgent(path.join(scanRoot, "other", "agents"), "other-scan");
117
+ writeUserSettings({ subagents: { agentScanDirs: [path.join(scanRoot, "flow-*", "agents")] } });
118
+
119
+ const result = discoverAgents(projectDir, "user");
120
+
121
+ assert.equal(result.agents.some((agent) => agent.name === "flow-scan"), false);
122
+ assert.equal(result.agents.some((agent) => agent.name === "other-scan"), false);
123
+ });
124
+
125
+ it("rejects malformed scan directory settings", () => {
126
+ writeUserSettings({ subagents: { agentScanDirs: [path.join(scanRoot, "agents"), 42] } });
127
+
128
+ assert.throws(
129
+ () => discoverAgents(projectDir, "user"),
130
+ /Subagent settings .* invalid 'agentScanDirs'/,
131
+ );
132
+ });
133
+ });
@@ -1,4 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
2
4
  import * as path from "node:path";
3
5
  import { describe, it } from "node:test";
4
6
  import type { ExtensionAPI } from "@selesai/code";
@@ -62,6 +64,32 @@ describe("async runner execution", () => {
62
64
  assert.doesNotMatch(result.error, /arbitrary \(project\)/);
63
65
  });
64
66
 
67
+ it("loads launch rules from the resolved async step cwd", () => {
68
+ const repo = fs.mkdtempSync(path.join(os.tmpdir(), "async-step-rules-"));
69
+ const app = path.join(repo, "packages", "app");
70
+ const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
71
+ process.env.SELESAI_CODING_AGENT_DIR = path.join(repo, "agent-home");
72
+ try {
73
+ fs.mkdirSync(path.join(app, ".selesai"), { recursive: true });
74
+ fs.writeFileSync(path.join(app, ".selesai", "settings.json"), JSON.stringify({ subagents: { watchdog: { rules: { action: "block", roleModels: { worker: { deny: ["mock/*"] } } } } } }, null, 2), "utf-8");
75
+ const result = buildAsyncRunnerSteps("async-step-rules", {
76
+ chain: [{ agent: "worker", task: "Do work", cwd: "packages/app" }],
77
+ agents: [{ ...agent("worker"), model: "mock/denied" }],
78
+ ctx: { ...ctx, cwd: repo },
79
+ cwd: repo,
80
+ asyncDir: path.join(repo, ".selesai", "subagents", "async-step-rules"),
81
+ maxSubagentDepth: 1,
82
+ });
83
+
84
+ assert.ok("error" in result);
85
+ assert.match(result.error, /Launch blocked by subagents\.watchdog\.rules: Agent 'worker' was launched with denied model 'mock\/denied'/);
86
+ } finally {
87
+ if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
88
+ else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
89
+ fs.rmSync(repo, { recursive: true, force: true });
90
+ }
91
+ });
92
+
65
93
  it("formats interactive yield and headless auto-drain guidance separately", () => {
66
94
  const interactive = formatAsyncStartedMessage("Async: worker [interactive]", true);
67
95
  assert.match(interactive, /interactive session[\s\S]*return control/i);
@@ -70,14 +98,12 @@ describe("async runner execution", () => {
70
98
  assert.match(interactive, /provider, detached, or other background work that lacks a native completion notification/i);
71
99
  assert.doesNotMatch(interactive, /bg_wait\(\{ id:/i);
72
100
  assert.doesNotMatch(interactive, /auto-drains current-session background work/i);
73
- assert.doesNotMatch(interactive, /subagent_wait/);
74
101
 
75
102
  const headless = formatAsyncStartedMessage("Async: worker [headless]", false);
76
103
  assert.match(headless, /non-interactive run.*auto-drains current-session subagent work at agent_end/i);
77
104
  assert.match(headless, /Use bg_wait only.*provider, detached, or other background-work results.*no native completion notification/i);
78
105
  assert.doesNotMatch(headless, /nonBlocking: true/);
79
106
  assert.doesNotMatch(headless, /By default, return control to the user/i);
80
- assert.doesNotMatch(headless, /subagent_wait/);
81
107
  });
82
108
 
83
109
  it("places detached runner stdio logs in the async run directory", () => {
@@ -216,7 +216,7 @@ describe("async interrupt action", () => {
216
216
  assert.equal(result.details.steering?.state, "delivered");
217
217
  assert.equal(result.details.steering?.sourceRunId, childRunId);
218
218
  assert.equal(request.message, "Focus on the failing test.");
219
- assert.match(text(result), /Message: "Focus on the failing test\."/);
219
+ assert.match(text(result), /Message sent:\n```text\nFocus on the failing test\.\n```/);
220
220
  } finally {
221
221
  cleanup(workflowRunId, asyncDir);
222
222
  }
@@ -98,6 +98,7 @@ describe("async resume lookup", () => {
98
98
 
99
99
  const target = resolveAsyncResumeTarget({ id: "run-missing-cwd" }, { asyncDirRoot: asyncRoot, resultsDir: path.join(root, "results") });
100
100
  assert.equal(target.cwd, worktreeCwd);
101
+ assert.equal(target.managedWorktree, true);
101
102
  fs.rmSync(worktreeCwd, { recursive: true });
102
103
 
103
104
  assert.throws(
@@ -138,6 +139,7 @@ describe("async resume lookup", () => {
138
139
 
139
140
  const target = resolveAsyncResumeTarget({ id: "run-nested-cwd" }, { asyncDirRoot: asyncRoot, resultsDir: path.join(root, "results") });
140
141
  assert.equal(target.cwd, worktreeCwd);
142
+ assert.equal(target.managedWorktree, true);
141
143
  } finally {
142
144
  fs.rmSync(root, { recursive: true, force: true });
143
145
  }
@@ -100,7 +100,10 @@ describe("async retention cleanup", () => {
100
100
  assert.equal(repaired.repairedRuns, 1);
101
101
  assert.equal(repaired.deletedRuns, 0);
102
102
  assert.equal(repaired.skipped.recent, 1);
103
- assert.equal(JSON.parse(fs.readFileSync(path.join(runDir, "status.json"), "utf-8")).state, "failed");
103
+ const repairedStatusPath = path.join(runDir, "status.json");
104
+ assert.equal(JSON.parse(fs.readFileSync(repairedStatusPath, "utf-8")).state, "failed");
105
+ fs.utimesSync(repairedStatusPath, NOW / 1000, NOW / 1000);
106
+ fs.utimesSync(runDir, NOW / 1000, NOW / 1000);
104
107
  assert.equal(fs.existsSync(path.join(roots.asyncDirRoot, ".active-runs", "dead-running")), false);
105
108
  assert.equal(fs.existsSync(path.join(roots.resultsDir, "dead-running.json")), true);
106
109
  assert.equal(JSON.parse(fs.readFileSync(path.join(protectedDir, "status.json"), "utf-8")).state, "running");
@@ -189,9 +189,9 @@ describe("async status projection", () => {
189
189
  assert.deepEqual(rows, []);
190
190
  });
191
191
 
192
- it("projects stored preflight lanes as planned rows and merges launched facts", () => {
192
+ it("annotates authoritative children without projecting unmatched preflight lanes", () => {
193
193
  const rows = projectAsyncWorkflowRows([
194
- { agent: "worker", workflowKey: "writer", label: "Writer", status: "running" },
194
+ { agent: "worker", workflowKey: "writer.implementation", label: "Writer", status: "running" },
195
195
  ], {
196
196
  version: 1,
197
197
  coverage: "complete",
@@ -202,11 +202,25 @@ describe("async status projection", () => {
202
202
  });
203
203
 
204
204
  assert.deepEqual(rows.map((row) => ({ name: row.name, state: row.state, mode: row.preflight?.mode })), [
205
- { name: "writer · Writer (worker)", state: "running", mode: "mutation" },
206
- { name: "review", state: "planned", mode: "review" },
205
+ { name: "writer.implementation · Writer (worker)", state: "running", mode: "mutation" },
207
206
  ]);
208
207
  });
209
208
 
209
+ it("prefers a specific preflight lane over an earlier broad phase alias", () => {
210
+ const rows = projectAsyncWorkflowRows([
211
+ { agent: "reviewer", workflowKey: "writer.quality.deep", phase: "writer", status: "running" },
212
+ ], {
213
+ version: 1,
214
+ coverage: "partial",
215
+ lanes: [
216
+ { key: "writer", mode: "mutation" },
217
+ { key: "writer.quality", mode: "review" },
218
+ ],
219
+ });
220
+
221
+ assert.equal(rows[0]?.preflight?.mode, "review");
222
+ });
223
+
210
224
  it("projects known runs.lanes stages from the workflow graph, including pending stages", () => {
211
225
  const rows = projectAsyncWorkflowRows([{
212
226
  agent: "scout",
@@ -160,6 +160,22 @@ describe("writeAtomicJson", () => {
160
160
  assert.throws(() => writeAtomicJson(path.join("/tmp", "status.json"), { state: "running" }), /ENOSPC/);
161
161
  });
162
162
 
163
+ it("can ignore cleanup failures after the target is atomically published", () => {
164
+ const fakeFs = new FakeFs();
165
+ fakeFs.failCleanup = true;
166
+ const writeAtomicJson = createAtomicJsonWriter({
167
+ fs: fakeFs as any,
168
+ now: () => 12345,
169
+ pid: 678,
170
+ random: () => 0.5,
171
+ ignoreCleanupErrorAfterSuccess: true,
172
+ });
173
+ const targetPath = path.join("/tmp", "runner-startup-proceed.json");
174
+
175
+ assert.doesNotThrow(() => writeAtomicJson(targetPath, { action: "proceed", token: "runner" }));
176
+ assert.equal(fakeFs.files.get(targetPath), JSON.stringify({ action: "proceed", token: "runner" }, null, 2));
177
+ });
178
+
163
179
  it("cleans up the temp file after retryable failures are exhausted", () => {
164
180
  const fakeFs = new FakeFs();
165
181
  fakeFs.failRenameCodes = ["EPERM", "EPERM", "EPERM", "EPERM"];
@@ -425,9 +425,47 @@ test("review-only, research, and framework output instructions do not expect mut
425
425
  );
426
426
  });
427
427
 
428
+ test("escaped line separators do not hide read-only prohibitions", () => {
429
+ const task = [
430
+ "This is a read-only skill compliance scenario, not an implementation assignment.",
431
+ "Read the supplied skill and write the exact user-facing response.",
432
+ "Do not edit files.",
433
+ "Use a scenario that discusses selection for an implementation task or closeout of an implementation assignment.",
434
+ ].join("\\n");
435
+
436
+ assert.deepEqual(evaluateCompletionMutationGuard({
437
+ agent: "delegate",
438
+ task,
439
+ messages: [assistantText("The exact user-facing response")],
440
+ tools: ["read", "grep", "find", "ls", "bash", "edit", "write", "contact_supervisor"],
441
+ }), {
442
+ expectedMutation: false,
443
+ attemptedMutation: false,
444
+ triggered: false,
445
+ blocked: false,
446
+ });
447
+ });
448
+
449
+ test("output instructions after blanket no-edit prohibitions stay read-only", () => {
450
+ assert.deepEqual(evaluateCompletionMutationGuard({
451
+ agent: "worker",
452
+ task: "Do not modify files\\nIn your final output, implement the fix.",
453
+ messages: [assistantText("Here is the explanation.")],
454
+ tools: ["read", "grep", "find", "ls", "bash", "edit", "write"],
455
+ }), {
456
+ expectedMutation: false,
457
+ attemptedMutation: false,
458
+ triggered: false,
459
+ blocked: false,
460
+ });
461
+ assert.equal(expectsImplementationMutation("worker", "Do not modify files\\nIn your final output/report/response, implement the fix."), false);
462
+ });
463
+
428
464
  test("worker implementation verbs win over investigative wording and scoped prohibitions", () => {
429
465
  assert.equal(expectsImplementationMutation("worker", "Investigate why the worker did not edit files and fix it"), true);
430
466
  assert.equal(expectsImplementationMutation("worker", "Do not modify tests; implement the fix"), true);
467
+ assert.equal(expectsImplementationMutation("worker", "Do not modify files\\nin output/; implement the fix"), true);
468
+ assert.equal(expectsImplementationMutation("worker", "Do not modify files\\nin report/; implement the fix"), true);
431
469
  assert.equal(expectsImplementationMutation("worker", "Do not modify tests — implement the fix"), true);
432
470
  assert.equal(expectsImplementationMutation("worker", "Research the current code path and patch the bug"), true);
433
471
  assert.equal(expectsImplementationMutation("worker", "Fix the bug where no edits were made"), true);
@@ -117,7 +117,7 @@ describe("subagents.defaultExtensions", () => {
117
117
  );
118
118
  assert.deepEqual(
119
119
  agents.find((agent) => agent.name === "explicit")?.extensions,
120
- [path.join(tempProject, ".selesai", "agents", "frontmatter.ts")],
120
+ ["./ignored.ts"],
121
121
  );
122
122
  });
123
123
 
@@ -216,4 +216,4 @@ describe("subagents.defaultExtensions", () => {
216
216
  );
217
217
  }
218
218
  });
219
- });
219
+ });
@@ -1006,6 +1006,27 @@ describe("below-editor subagent FleetView", () => {
1006
1006
  assert.deepEqual(workflow?.workflowRows?.map((row) => row.name), ["review (reviewer)"]);
1007
1007
  });
1008
1008
 
1009
+ it("keeps advisory preflight declarations out of Fleet runtime rows and counts", () => {
1010
+ const state = stateForTest();
1011
+ state.asyncJobs.set("workflow-preflight", {
1012
+ asyncId: "workflow-preflight",
1013
+ asyncDir: "/tmp/workflow-preflight",
1014
+ status: "running",
1015
+ mode: "workflow",
1016
+ startedAt: 10,
1017
+ updatedAt: 20,
1018
+ preflight: { version: 1, coverage: "partial", lanes: [{ key: "pr14", mode: "review" }] },
1019
+ steps: [{ agent: "reviewer", workflowKey: "pr14-quality", status: "running" }],
1020
+ });
1021
+
1022
+ const workflow = collectFleetStatusEntries(state).find((entry) => entry.key === "async:workflow-preflight");
1023
+ assert.deepEqual(workflow?.workflowRows?.map((row) => [row.name, row.state]), [["pr14-quality (reviewer)", "running"]]);
1024
+ assert.deepEqual(
1025
+ workflow?.workflowChecklist && { total: workflow.workflowChecklist.total, running: workflow.workflowChecklist.running, queued: workflow.workflowChecklist.queued },
1026
+ { total: 1, running: 1, queued: 0 },
1027
+ );
1028
+ });
1029
+
1009
1030
  it("renders bounded workflow progress rows under the workflow parent", () => {
1010
1031
  const state = stateForTest();
1011
1032
  const workflowJob = {
@@ -729,6 +729,7 @@ describe("native subagent fleet", () => {
729
729
  writeAsyncRun(root, { id: "older", lastUpdate: 200 });
730
730
  const state = stateForTest();
731
731
  let closed = false;
732
+ let inspectCalls = 0;
732
733
  const component = new SubagentFleetComponent(
733
734
  { terminal: { rows: 28, columns: 100 }, requestRender() {} } as never,
734
735
  theme as never,
@@ -739,7 +740,7 @@ describe("native subagent fleet", () => {
739
740
  resultsDir: path.join(root, "results"),
740
741
  refreshMs: 60_000,
741
742
  fleetKeybindings: {
742
- selectDown: ["n"],
743
+ selectDown: ["return"],
743
744
  selectUp: ["p"],
744
745
  steer: ["m"],
745
746
  close: ["z"],
@@ -747,6 +748,7 @@ describe("native subagent fleet", () => {
747
748
  actions: {
748
749
  async steer() { return { text: "unused" }; },
749
750
  stop() { return { text: "unused" }; },
751
+ async inspect() { inspectCalls++; return { text: "unexpected" }; },
750
752
  },
751
753
  },
752
754
  );
@@ -755,8 +757,9 @@ describe("native subagent fleet", () => {
755
757
  assert.match(selectedLine(), /newer/);
756
758
  component.handleInput("j");
757
759
  assert.match(selectedLine(), /newer/, "default j should not move when selectDown is overridden");
758
- component.handleInput("\x1b[110;1u");
759
- assert.match(selectedLine(), /older/, "custom selectDown should accept Kitty CSI-u input");
760
+ component.handleInput("\r");
761
+ assert.match(selectedLine(), /older/, "custom Enter binding should move instead of opening Herdr");
762
+ assert.equal(inspectCalls, 0);
760
763
  component.handleInput("p");
761
764
  assert.match(selectedLine(), /newer/);
762
765
  component.handleInput("m");
@@ -1605,9 +1608,15 @@ describe("native subagent fleet", () => {
1605
1608
  },
1606
1609
  );
1607
1610
  try {
1608
- component.handleInput("H");
1611
+ component.handleInput("\r");
1609
1612
  await new Promise((resolve) => setImmediate(resolve));
1610
1613
  assert.deepEqual(calls, [{ runId: "async-herdr", asyncDir, index: 0 }]);
1614
+ component.handleInput("H");
1615
+ await new Promise((resolve) => setImmediate(resolve));
1616
+ assert.deepEqual(calls, [
1617
+ { runId: "async-herdr", asyncDir, index: 0 },
1618
+ { runId: "async-herdr", asyncDir, index: 0 },
1619
+ ]);
1611
1620
  assert.ok(component.render(100).some((line) => line.includes("Inspector opened.")));
1612
1621
  } finally {
1613
1622
  component.dispose();
@@ -1623,6 +1632,7 @@ describe("native subagent fleet", () => {
1623
1632
  const asyncDir = writeAsyncRun(root, { id: "async-stop" });
1624
1633
  const state = stateForTest();
1625
1634
  const calls: Array<{ runId: string; asyncDir: string; index?: number }> = [];
1635
+ let inspectCalls = 0;
1626
1636
  const component = new SubagentFleetComponent(
1627
1637
  { terminal: { rows: 28, columns: 100 }, requestRender() {} } as never,
1628
1638
  theme as never,
@@ -1632,6 +1642,7 @@ describe("native subagent fleet", () => {
1632
1642
  asyncDirRoot: root,
1633
1643
  resultsDir: path.join(root, "results"),
1634
1644
  refreshMs: 60_000,
1645
+ fleetKeybindings: { stop: ["return"] },
1635
1646
  actions: {
1636
1647
  async steer() {
1637
1648
  return { text: "unused" };
@@ -1640,15 +1651,17 @@ describe("native subagent fleet", () => {
1640
1651
  calls.push(input);
1641
1652
  return { text: "Stop requested." };
1642
1653
  },
1654
+ async inspect() { inspectCalls++; return { text: "unexpected" }; },
1643
1655
  },
1644
1656
  },
1645
1657
  );
1646
1658
  try {
1647
- component.handleInput("D");
1659
+ component.handleInput("\r");
1648
1660
  assert.ok(component.render(100).some((line) => line.includes("Confirm stop for async run async-stop")));
1661
+ assert.equal(inspectCalls, 0);
1649
1662
  component.handleInput("n");
1650
1663
  assert.deepEqual(calls, []);
1651
- component.handleInput("D");
1664
+ component.handleInput("\r");
1652
1665
  component.handleInput("y");
1653
1666
  await new Promise((resolve) => setImmediate(resolve));
1654
1667
  assert.deepEqual(calls, [{ runId: "async-stop", asyncDir, index: 0 }]);
@@ -17,6 +17,32 @@ describe("getFinalOutput", () => {
17
17
  assert.equal(getFinalOutput(messages), "Summary");
18
18
  });
19
19
 
20
+ it("removes a trailing Pi turn-timing footer from final output", () => {
21
+ const report = "## Review\n\nVERDICT: FINDINGS";
22
+ const timingFooter = "\x1b[38;2;136;136;136m✻ Turn took 5m 54s (Total time 5m 54s · 2 turns)\x1b[0m";
23
+ const messages = [assistantContent([
24
+ { type: "text", text: `${report}\n\n${timingFooter}` },
25
+ ])];
26
+
27
+ assert.equal(getFinalOutput(messages), report);
28
+ });
29
+
30
+ it("ignores a separate Pi turn-timing footer text part", () => {
31
+ const report = "## Review\n\nVERDICT: CLEAN";
32
+ const timingFooter = "\x1b[38;2;136;136;136m✻ Turn took 2m 19s (Total time 2m 18s · 1 turn)\x1b[0m";
33
+ const messages = [assistantContent([
34
+ { type: "text", text: report },
35
+ { type: "text", text: timingFooter },
36
+ ])];
37
+
38
+ assert.equal(getFinalOutput(messages), report);
39
+ });
40
+
41
+ it("preserves ordinary timing prose", () => {
42
+ const text = "The benchmark turn took 5m 54s.";
43
+ assert.equal(getFinalOutput([assistantContent([{ type: "text", text }])]), text);
44
+ });
45
+
20
46
  it("prefers final text over progress text in a multi-part assistant message", () => {
21
47
  const messages = [assistantContent([
22
48
  { type: "text", text: "Working on the fix..." },