@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
@@ -252,10 +252,23 @@ describe("Herdr inspector", () => {
252
252
  assert.match(dashboard, /decision-1: Choose UX/);
253
253
  assert.match(dashboard, /closing it does not stop the run/i);
254
254
 
255
- assert.match(submitInspectorControl({ asyncDir, runId: "run-123", refreshMs: 1_500 }, "steer keep going"), /Steering queued for run run-123\. Message: "keep going"/);
255
+ assert.match(submitInspectorControl({ asyncDir, runId: "run-123", refreshMs: 1_500 }, "steer keep going"), /Steering queued for run run-123\.\n\nMessage sent:\n```text\nkeep going\n```/);
256
256
  assert.deepEqual(consumeSteerRequests(asyncDir).map((request) => ({ message: request.message, targetIndex: request.targetIndex, source: request.source })), [
257
257
  { message: "keep going", targetIndex: 0, source: "herdr-inspector" },
258
258
  ]);
259
+ assert.match(submitInspectorControl({ asyncDir, runId: "run-123", index: 0, refreshMs: 1_500 }, "keep going without a prefix"), /Steering queued for run run-123/);
260
+ assert.deepEqual(consumeSteerRequests(asyncDir).map((request) => ({ message: request.message, targetIndex: request.targetIndex, source: request.source })), [
261
+ { message: "keep going without a prefix", targetIndex: 0, source: "herdr-inspector" },
262
+ ]);
263
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({ ...status, mode: "parallel", steps: [...status.steps!, { agent: "reviewer", status: "running" }] }), "utf-8");
264
+ const aggregateDashboard = formatInspectorDashboard({ status: { ...status, mode: "parallel", steps: [...status.steps!, { agent: "reviewer", status: "running" }] }, asyncDir });
265
+ assert.match(aggregateDashboard, /Controls: steer <message> \| stop \| status/);
266
+ assert.doesNotMatch(aggregateDashboard, /type guidance/);
267
+ assert.throws(() => submitInspectorControl({ asyncDir, runId: "run-123", refreshMs: 1_500 }, "ambiguous plain guidance"), /Plain guidance requires a child-specific inspector/);
268
+ assert.match(submitInspectorControl({ asyncDir, runId: "run-123", refreshMs: 1_500 }, "steer broadcast guidance"), /Steering queued for run run-123/);
269
+ assert.deepEqual(consumeSteerRequests(asyncDir).map((request) => ({ message: request.message, targetIndexes: request.targetIndexes, source: request.source })), [
270
+ { message: "broadcast guidance", targetIndexes: [0, 1], source: "herdr-inspector" },
271
+ ]);
259
272
  assert.match(submitInspectorControl({ asyncDir, runId: "run-123", refreshMs: 1_500 }, "stop"), /Stop requested/);
260
273
  assert.equal(consumeStopRequest(asyncDir), true);
261
274
  assert.throws(() => submitInspectorControl({ asyncDir, runId: "run-123", refreshMs: 1_500 }, "reply decision-1 yes"), /parent Pi session/);
@@ -369,6 +382,8 @@ describe("Herdr inspector", () => {
369
382
 
370
383
  it("fails closed when an idle-only project pane close sees active work", async () => {
371
384
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-herdr-project-pane-busy-"));
385
+ const previousPiBinary = process.env[SELESAI_SUBAGENT_SELESAI_BINARY_ENV];
386
+ process.env[SELESAI_SUBAGENT_SELESAI_BINARY_ENV] = path.join(root, "pi-bin");
372
387
  try {
373
388
  const projectRoot = fs.realpathSync(root);
374
389
  const calls: string[][] = [];
@@ -394,12 +409,16 @@ describe("Herdr inspector", () => {
394
409
  assert.equal(calls.some((args) => args.join(" ") === "pane close w1:p11"), false);
395
410
  assert.equal(readHerdrProjectPaneBinding(root)?.paneId, "w1:p11");
396
411
  } finally {
412
+ if (previousPiBinary === undefined) delete process.env[SELESAI_SUBAGENT_SELESAI_BINARY_ENV];
413
+ else process.env[SELESAI_SUBAGENT_SELESAI_BINARY_ENV] = previousPiBinary;
397
414
  fs.rmSync(root, { recursive: true, force: true });
398
415
  }
399
416
  });
400
417
 
401
418
  it("preserves legacy model-facing recovery for transient and opaque pane inspection results", async () => {
402
419
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-herdr-project-pane-legacy-"));
420
+ const previousPiBinary = process.env[SELESAI_SUBAGENT_SELESAI_BINARY_ENV];
421
+ process.env[SELESAI_SUBAGENT_SELESAI_BINARY_ENV] = path.join(root, "pi-bin");
403
422
  try {
404
423
  let splitCount = 0;
405
424
  let getMode: "valid" | "timeout" | "opaque" = "valid";
@@ -436,6 +455,8 @@ describe("Herdr inspector", () => {
436
455
  assert.match(text(closed), /INVALID_PANE_RESPONSE/);
437
456
  assert.equal(readHerdrProjectPaneBinding(root)?.paneId, "w1:p32");
438
457
  } finally {
458
+ if (previousPiBinary === undefined) delete process.env[SELESAI_SUBAGENT_SELESAI_BINARY_ENV];
459
+ else process.env[SELESAI_SUBAGENT_SELESAI_BINARY_ENV] = previousPiBinary;
439
460
  fs.rmSync(root, { recursive: true, force: true });
440
461
  }
441
462
  });
@@ -485,7 +485,7 @@ describe("subagent extension child mode", () => {
485
485
  }
486
486
  });
487
487
 
488
- it("registers bg_wait and its compatibility alias and honors waitTool disabled config", () => {
488
+ it("registers bg_wait and honors waitTool disabled config", () => {
489
489
  const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-wait-tool-config-"));
490
490
  try {
491
491
  const configDir = path.join(agentDir, "extensions", "subagent");
@@ -496,13 +496,11 @@ describe("subagent extension child mode", () => {
496
496
  import registerSubagentExtension from "./index.ts";
497
497
  const events = { on() { return () => {}; }, emit() {} };
498
498
  let bgWaitTool;
499
- let subagentWaitTool;
500
499
  let legacyWaitRegistered = false;
501
500
  const fakePi = new Proxy({
502
501
  events,
503
502
  registerTool(tool) {
504
503
  if (tool.name === "bg_wait") bgWaitTool = tool;
505
- if (tool.name === "subagent_wait") subagentWaitTool = tool;
506
504
  if (tool.name === "wait") legacyWaitRegistered = true;
507
505
  },
508
506
  registerCommand() {},
@@ -517,11 +515,9 @@ describe("subagent extension child mode", () => {
517
515
  },
518
516
  });
519
517
  registerSubagentExtension(fakePi);
520
- if (!bgWaitTool || !subagentWaitTool) throw new Error("bg_wait and subagent_wait tools not registered");
518
+ if (!bgWaitTool) throw new Error("bg_wait tool not registered");
521
519
  if (legacyWaitRegistered) throw new Error("legacy wait tool must not be registered");
522
520
  const result = await bgWaitTool.execute("bg-wait-disabled", {}, new AbortController().signal, undefined, {});
523
- const aliasResult = await subagentWaitTool.execute("subagent-wait-disabled", {}, new AbortController().signal, undefined, {});
524
- if (aliasResult.content[0].text !== result.content[0].text) throw new Error("subagent_wait alias behavior differs from bg_wait");
525
521
  process.stdout.write(JSON.stringify(result.content[0].text));
526
522
  `;
527
523
 
@@ -709,6 +709,20 @@ describe("completion formatting helpers", () => {
709
709
  assert.equal(details.agent, "unknown");
710
710
  assert.equal(details.status, "completed");
711
711
  });
712
+
713
+ it("surfaces direct structured output when the completion has no text output", () => {
714
+ const details = buildCompletionDetails({
715
+ id: "structured-run",
716
+ agent: "delegate",
717
+ success: true,
718
+ summary: "delegate:\n(no output)",
719
+ results: [{ agent: "delegate", output: "", structuredOutput: { payload: { ok: true } }, success: true }],
720
+ });
721
+
722
+ assert.match(details.resultPreview, /Structured output:/);
723
+ assert.match(details.resultPreview, /"ok": true/);
724
+ assert.match(formatSingleCompletion(details), /"ok": true/);
725
+ });
712
726
  });
713
727
 
714
728
  describe("scheduled completions", () => {
@@ -748,4 +762,55 @@ describe("scheduled completions", () => {
748
762
  assert.equal(parsed?.scheduleOrigin, undefined);
749
763
  assert.equal(parsed?.resultPreview, "Workflow completed with 1 child run(s).");
750
764
  });
751
- });
765
+ });
766
+
767
+ describe("watchdog blockers in completion notices", () => {
768
+ it("collects child blockers into details and round-trips them through the notice text", () => {
769
+ const details = buildCompletionDetails({
770
+ id: "run-1",
771
+ agent: "workflow",
772
+ mode: "workflow",
773
+ runId: "run-1",
774
+ success: false,
775
+ summary: "worker:\nPatched billing.",
776
+ exitCode: 1,
777
+ results: [
778
+ {
779
+ runId: "child-1",
780
+ agent: "worker",
781
+ success: false,
782
+ watchdog: {
783
+ phase: "idle",
784
+ seq: 3,
785
+ lastUpdate: 1,
786
+ warnings: [
787
+ { severity: "blocker", category: "test-gap", summary: "Claims tests passed without running them", evidence: "e", recommendedAction: "r", addressed: false, stalemate: false },
788
+ { severity: "concern", category: "other", summary: "Concern is not listed", evidence: "e", recommendedAction: "r", addressed: true, stalemate: false },
789
+ { severity: "blocker", category: "scope-drift", summary: "Kept editing after being told to stop", evidence: "e", recommendedAction: "r", addressed: false, stalemate: true },
790
+ ],
791
+ },
792
+ },
793
+ { runId: "child-2", agent: "reviewer", success: true, output: "clean" },
794
+ ],
795
+ sessionId: "session-1",
796
+ });
797
+
798
+ assert.deepEqual(details.watchdogBlockers, [
799
+ { agent: "worker", summary: "Claims tests passed without running them", addressed: false, stalemate: false },
800
+ { agent: "worker", summary: "Kept editing after being told to stop", addressed: false, stalemate: true },
801
+ ]);
802
+
803
+ const content = formatSingleCompletion(details);
804
+ assert.match(content, /\nWatchdog blockers:\n- worker: Claims tests passed without running them \(unaddressed\)\n- worker: Kept editing after being told to stop \(stalemate\)\n/);
805
+ const parsed = parseSubagentNotifyContent(content);
806
+ assert.deepEqual(parsed?.watchdogBlockers, details.watchdogBlockers);
807
+ assert.match(parsed?.resultPreview ?? "", /^worker:\nPatched billing\./);
808
+ assert.doesNotMatch(parsed?.resultPreview ?? "", /Watchdog blockers/);
809
+ assert.equal(parsed?.workflowRunId, "run-1");
810
+
811
+ const grouped = formatGroupedCompletion([details, { agent: "scout", status: "completed", resultPreview: "ok" }]);
812
+ assert.match(grouped, /Watchdog blockers:\n- worker: Claims tests passed without running them \(unaddressed\)/);
813
+ assert.equal(grouped.split("Watchdog blockers:").length, 2);
814
+
815
+ });
816
+ });
@@ -56,7 +56,7 @@ afterEach(() => {
56
56
  function readPromptFile(result: { tempDir?: string }): string {
57
57
  assert.ok(result.tempDir, "expected a temp dir from buildPiArgs");
58
58
  const files = fs.readdirSync(result.tempDir);
59
- const promptFile = files.find((f: string) => f.endsWith(".md"));
59
+ const promptFile = files.find((f: string) => f.endsWith(".md") && f !== "task.md");
60
60
  assert.ok(promptFile, "expected a prompt.md file");
61
61
  return fs.readFileSync(path.join(result.tempDir, promptFile), "utf-8");
62
62
  }
@@ -16,6 +16,8 @@ import {
16
16
  } from "../../src/runs/shared/tool-budget.ts";
17
17
  import { WAIT_TOOL_DEFAULT_TIMEOUT_MS_ENV, WAIT_TOOL_ENABLED_ENV } from "../../src/runs/background/wait-config.ts";
18
18
  import { SELESAI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../src/shared/utils.ts";
19
+ import { STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV, STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV } from "../../src/runs/shared/structured-output.ts";
20
+
19
21
  import {
20
22
  CHILD_TOOL_DIAGNOSTIC_PATH_ENV,
21
23
  MCP_DIRECT_CHILD_TOOLS_ENV,
@@ -46,6 +48,7 @@ import {
46
48
  buildPiArgs,
47
49
  projectLaunchResolvedChildExtensions,
48
50
  resolvePiLaunchToolPlan,
51
+ shouldDeliverTaskViaFile,
49
52
  } from "../../src/runs/shared/pi-args.ts";
50
53
 
51
54
  const originalEnv = {
@@ -457,7 +460,6 @@ describe("buildPiArgs session wiring", () => {
457
460
  inheritProjectContext: false,
458
461
  inheritSkills: false,
459
462
  childWatchdog: {
460
- enabled: true,
461
463
  runId: "run-1",
462
464
  agent: "worker",
463
465
  childIndex: 2,
@@ -465,15 +467,13 @@ describe("buildPiArgs session wiring", () => {
465
467
  agentEndTimeoutMs: 500,
466
468
  maxWarnings: 1,
467
469
  lsp: { enabled: false, timeoutMs: 50, maxFiles: 2, maxDiagnostics: 3 },
468
- autoFollowBlockers: true,
469
- autoFollowMaxAttempts: 3,
470
470
  stalemateRepeats: 2,
471
+ cadence: { everyNTools: null },
471
472
  },
472
473
  });
473
474
  const encoded = withWatchdog.env[CHILD_WATCHDOG_CONFIG_ENV];
474
475
  assert.equal(typeof encoded, "string");
475
476
  assert.deepEqual(JSON.parse(encoded ?? "{}"), {
476
- enabled: true,
477
477
  runId: "run-1",
478
478
  agent: "worker",
479
479
  childIndex: 2,
@@ -481,10 +481,67 @@ describe("buildPiArgs session wiring", () => {
481
481
  agentEndTimeoutMs: 500,
482
482
  maxWarnings: 1,
483
483
  lsp: { enabled: false, timeoutMs: 50, maxFiles: 2, maxDiagnostics: 3 },
484
- autoFollowBlockers: true,
485
- autoFollowMaxAttempts: 3,
486
484
  stalemateRepeats: 2,
485
+ cadence: { everyNTools: null },
486
+ });
487
+ });
488
+ });
489
+
490
+ describe("excludeTools resolution", () => {
491
+ it("filters excluded names from explicit allowlists and runtime-injected tools", () => {
492
+ const plan = resolvePiLaunchToolPlan({
493
+ tools: ["read", "write"],
494
+ excludeTools: ["write", "structured_output"],
495
+ structuredOutput: true,
496
+ });
497
+ assert.deepEqual(plan.declaredBuiltinTools, ["read", "write"]);
498
+ assert.deepEqual(plan.excludeTools, ["write", "structured_output"]);
499
+ assert.deepEqual(plan.effectiveToolAllowlist, ["read"]);
500
+ assert.deepEqual(plan.internalTools, []);
501
+ assert.deepEqual(plan.requiredChildTools, ["read"]);
502
+
503
+ const built = buildPiArgs({
504
+ baseArgs: [],
505
+ task: "hello",
506
+ sessionEnabled: true,
507
+ inheritProjectContext: true,
508
+ inheritSkills: true,
509
+ tools: ["read", "write"],
510
+ excludeTools: ["write"],
511
+ });
512
+ assert.deepEqual(built.args.slice(0, 2), ["--tools", "read"]);
513
+ assert.equal(built.args.includes("--exclude-tools"), false);
514
+ });
515
+
516
+ it("forwards exclusions to Pi without inventing an ambient allowlist", () => {
517
+ const built = buildPiArgs({
518
+ baseArgs: [],
519
+ task: "hello",
520
+ sessionEnabled: true,
521
+ inheritProjectContext: true,
522
+ inheritSkills: true,
523
+ excludeTools: ["write", "unknown_tool"],
524
+ });
525
+ assert.deepEqual(built.args.slice(0, 2), ["--exclude-tools", "write,unknown_tool"]);
526
+ assert.equal(built.args.includes("--tools"), false);
527
+ assert.equal(built.args.includes("--no-tools"), false);
528
+ });
529
+
530
+ it("keeps empty tool sets all-off and makes excluded nested fanout unavailable", () => {
531
+ const empty = buildPiArgs({
532
+ baseArgs: [],
533
+ task: "hello",
534
+ sessionEnabled: true,
535
+ inheritProjectContext: true,
536
+ inheritSkills: true,
537
+ tools: [],
538
+ excludeTools: ["write"],
487
539
  });
540
+ assert.equal(empty.args.includes("--no-tools"), true);
541
+ assert.equal(empty.args.includes("--exclude-tools"), false);
542
+
543
+ const plan = resolvePiLaunchToolPlan({ allowNestedSubagents: true, excludeTools: ["subagent"] });
544
+ assert.equal(plan.fanoutAuthorized, false);
488
545
  });
489
546
  });
490
547
 
@@ -634,7 +691,7 @@ describe("buildPiArgs task delivery", () => {
634
691
  return ref ? ref.slice(1) : undefined;
635
692
  }
636
693
 
637
- it("delivers short tasks inline by default", () => {
694
+ it("delivers short tasks through the platform default", () => {
638
695
  const { args } = buildPiArgs({
639
696
  baseArgs: ["-p"],
640
697
  task: "hello",
@@ -643,8 +700,13 @@ describe("buildPiArgs task delivery", () => {
643
700
  inheritSkills: false,
644
701
  });
645
702
 
646
- assert.ok(args.includes("Task: hello"));
647
- assert.equal(taskFileFromArgs(args), undefined);
703
+ if (process.platform === "darwin") {
704
+ assert.ok(taskFileFromArgs(args), "expected an @task.md argv reference on macOS");
705
+ assert.ok(!args.includes("Task: hello"));
706
+ } else {
707
+ assert.ok(args.includes("Task: hello"));
708
+ assert.equal(taskFileFromArgs(args), undefined);
709
+ }
648
710
  });
649
711
 
650
712
  it("delivers tasks over the argv limit via a temp file by default", () => {
@@ -681,6 +743,7 @@ describe("buildPiArgs task delivery", () => {
681
743
 
682
744
  it("falls back to auto when SELESAI_SUBAGENT_TASK_DELIVERY is invalid", () => {
683
745
  process.env.SELESAI_SUBAGENT_TASK_DELIVERY = "carrier-pigeon";
746
+
684
747
  const { args } = buildPiArgs({
685
748
  baseArgs: ["-p"],
686
749
  task: longTask,
@@ -706,6 +769,13 @@ describe("buildPiArgs task delivery", () => {
706
769
  assert.ok(taskFileFromArgs(args), "expected an @task.md argv reference");
707
770
  assert.ok(!args.includes("Task: hello"));
708
771
  });
772
+
773
+ it("selects file delivery for macOS or over-limit tasks", () => {
774
+ assert.equal(shouldDeliverTaskViaFile("short", "auto", "darwin"), true);
775
+ assert.equal(shouldDeliverTaskViaFile("short", "auto", "linux"), false);
776
+ assert.equal(shouldDeliverTaskViaFile("short", "auto", "win32"), false);
777
+ assert.equal(shouldDeliverTaskViaFile("x".repeat(8001), "auto", "linux"), true);
778
+ });
709
779
  });
710
780
 
711
781
  describe("buildPiArgs system prompt mode wiring", () => {
@@ -974,6 +1044,27 @@ describe("buildPiArgs system prompt mode wiring", () => {
974
1044
  assert.equal(env[CHILD_TOOL_DIAGNOSTIC_PATH_ENV], toolDiagnosticPath);
975
1045
  });
976
1046
 
1047
+ it("clears inherited tool diagnostics for nested zero-tool children", () => {
1048
+ const inheritedDiagnosticPath = "/tmp/parent-tool-diagnostic.json";
1049
+ const { env, toolDiagnosticPath } = buildPiArgs({
1050
+ baseArgs: ["-p"],
1051
+ task: "repair retained output",
1052
+ sessionEnabled: false,
1053
+ inheritProjectContext: false,
1054
+ inheritSkills: false,
1055
+ tools: [],
1056
+ });
1057
+ const spawnEnv = {
1058
+ [REQUIRED_CHILD_TOOLS_ENV]: JSON.stringify(["read", "bash"]),
1059
+ [CHILD_TOOL_DIAGNOSTIC_PATH_ENV]: inheritedDiagnosticPath,
1060
+ ...env,
1061
+ };
1062
+
1063
+ assert.equal(toolDiagnosticPath, undefined);
1064
+ assert.equal(spawnEnv[REQUIRED_CHILD_TOOLS_ENV], undefined);
1065
+ assert.equal(spawnEnv[CHILD_TOOL_DIAGNOSTIC_PATH_ENV], undefined);
1066
+ });
1067
+
977
1068
  it("strips the legacy supervisor pairing from requirements", () => {
978
1069
  const { args, env } = buildPiArgs({
979
1070
  baseArgs: ["-p"],
@@ -1042,6 +1133,8 @@ describe("buildPiArgs system prompt mode wiring", () => {
1042
1133
  schema: { type: "object", properties: {}, additionalProperties: false },
1043
1134
  schemaPath: "/tmp/schema.json",
1044
1135
  outputPath: "/tmp/output.json",
1136
+ acceptanceReportPath: "/tmp/acceptance.json",
1137
+ acceptanceReportRequired: true,
1045
1138
  },
1046
1139
  });
1047
1140
 
@@ -1054,6 +1147,18 @@ describe("buildPiArgs system prompt mode wiring", () => {
1054
1147
  "fixture_search",
1055
1148
  "structured_output",
1056
1149
  ]);
1150
+ assert.equal(env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV], "/tmp/acceptance.json");
1151
+ assert.equal(env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV], "1");
1152
+ });
1153
+
1154
+ it("clears inherited acceptance capture for launches without outputSchema", () => {
1155
+ const { env } = buildPiArgs({
1156
+ baseArgs: ["-p"], task: "hello", sessionEnabled: false,
1157
+ inheritProjectContext: false, inheritSkills: false,
1158
+ });
1159
+
1160
+ assert.equal(env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV], undefined);
1161
+ assert.equal(env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV], undefined);
1057
1162
  });
1058
1163
 
1059
1164
  it("forwards the Pi package root to child processes for host peer resolution", () => {
@@ -1267,7 +1372,8 @@ describe("buildPiArgs system prompt mode wiring", () => {
1267
1372
  assert.deepEqual(requestedNames, [serverName, serverName]);
1268
1373
  assert.equal(serverLaunch.args[serverLaunch.args.indexOf("--tools") + 1], "read,runtime-github_search_repositories,runtime-github_create_issue");
1269
1374
  assert.equal(toolLaunch.args[toolLaunch.args.indexOf("--tools") + 1], "read,runtime-github_search_repositories");
1270
- assert.ok(serverLaunch.args.indexOf("--mcp-config") < serverLaunch.args.indexOf("Task: hello"));
1375
+ const serverTaskArgIndex = serverLaunch.args.findIndex((arg) => arg === "Task: hello" || arg.endsWith("task.md"));
1376
+ assert.ok(serverLaunch.args.indexOf("--mcp-config") < serverTaskArgIndex);
1271
1377
  for (const launch of [serverLaunch, toolLaunch]) {
1272
1378
  const configPath = launch.args[launch.args.indexOf("--mcp-config") + 1];
1273
1379
  assert.ok(configPath);
@@ -8,6 +8,7 @@ import {
8
8
  resolvePiCliScript,
9
9
  type SelesaiSpawnDeps,
10
10
  } from "../../src/runs/shared/pi-spawn.ts";
11
+ import { SELESAI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../src/shared/utils.ts";
11
12
 
12
13
  function makeDeps(input: {
13
14
  platform?: NodeJS.Platform;
@@ -73,6 +74,33 @@ describe("getPiSpawnCommand", () => {
73
74
  assert.deepEqual(result, { command: "selesai", args });
74
75
  });
75
76
 
77
+ for (const extension of ["js", "mjs", "cjs"] as const) {
78
+ it(`runs a Windows ${extension} SELESAI_SUBAGENT_SELESAI_BINARY override through Node`, () => {
79
+ const piBinary = `C:\\Program Files\\pi\\bin\\pi-cli.${extension}`;
80
+ const args = ["--mode", "json", "Task: check output"];
81
+ const result = getPiSpawnCommand(args, {
82
+ platform: "win32",
83
+ execPath: "C:\\Program Files\\nodejs\\node.exe",
84
+ env: { SELESAI_SUBAGENT_SELESAI_BINARY: piBinary },
85
+ });
86
+ assert.deepEqual(result, {
87
+ command: "C:\\Program Files\\nodejs\\node.exe",
88
+ args: [piBinary, ...args],
89
+ });
90
+ });
91
+ }
92
+
93
+ it("keeps a JavaScript SELESAI_SUBAGENT_SELESAI_BINARY override direct on POSIX", () => {
94
+ const piBinary = "/opt/pi/bin/pi-cli.mjs";
95
+ const args = ["--mode", "json", "Task: check output"];
96
+ const result = getPiSpawnCommand(args, {
97
+ platform: "darwin",
98
+ execPath: "/usr/local/bin/node",
99
+ env: { SELESAI_SUBAGENT_SELESAI_BINARY: piBinary },
100
+ });
101
+ assert.deepEqual(result, { command: piBinary, args });
102
+ });
103
+
76
104
  for (const [platform, execPath] of [
77
105
  ["darwin", "/opt/selesai/selesai"],
78
106
  ["linux", "/opt/selesai/selesai"],
@@ -89,7 +117,8 @@ describe("getPiSpawnCommand", () => {
89
117
  execPath,
90
118
  argv1: "/missing/host.js",
91
119
  packageJsonPath,
92
- packageJsonContent: JSON.stringify({ bin: { selesai: "dist/cli.js" } }),
120
+ packageJsonContent: JSON.stringify({ name: "@selesai/code", bin: { selesai: "dist/cli.js" } }),
121
+
93
122
  existing: [packageJsonPath, cliPath],
94
123
  });
95
124
  const args = ["--mode", "json", "-p", "Task: review diff"];
@@ -141,7 +170,8 @@ describe("getPiSpawnCommand", () => {
141
170
  execPath: "/usr/local/bin/node",
142
171
  argv1: "/opt/selesai/subagent-runner.ts",
143
172
  packageJsonPath,
144
- packageJsonContent: JSON.stringify({ bin: { selesai: "dist/cli/index.js" } }),
173
+ packageJsonContent: JSON.stringify({ name: "@selesai/code", bin: { selesai: "dist/cli/index.js" } }),
174
+
145
175
  existing: [packageJsonPath, cliPath],
146
176
  });
147
177
  const args = ["-p", "Task: hello"];
@@ -152,7 +182,103 @@ describe("getPiSpawnCommand", () => {
152
182
  });
153
183
  });
154
184
 
185
+ it("fails closed until the Windows CLI script becomes available", () => {
186
+ const packageJsonPath = "/opt/pi/package.json";
187
+ const cliPath = path.resolve(
188
+ path.dirname(packageJsonPath),
189
+ "dist/cli/index.js",
190
+ );
191
+ let cliAvailable = false;
192
+ const deps = makeDeps({
193
+ platform: "win32",
194
+ execPath: "C:\\Program Files\\nodejs\\node.exe",
195
+ argv1: "/opt/pi-web/dist/server.js",
196
+ packageJsonPath,
197
+ packageJsonContent: JSON.stringify({ name: "@selesai/code", bin: { selesai: "dist/cli/index.js" } }),
198
+ existing: [packageJsonPath],
199
+ });
200
+ deps.existsSync = (filePath) =>
201
+ filePath === packageJsonPath || (cliAvailable && filePath === cliPath);
202
+ const args = ["-p", "Task: hello"];
203
+
204
+ assert.throws(
205
+ () => getPiSpawnCommand(args, deps),
206
+ /Could not resolve the Pi CLI on Windows/,
207
+ );
208
+ cliAvailable = true;
209
+ assert.deepEqual(getPiSpawnCommand(args, deps), {
210
+ command: "C:\\Program Files\\nodejs\\node.exe",
211
+ args: [cliPath, ...args],
212
+ });
213
+ });
214
+
215
+ it("resolves the Windows CLI from the forwarded package-root env for wrapper hosts", () => {
216
+ const packageJsonPath = "/opt/pi/package.json";
217
+ const cliPath = path.resolve(
218
+ path.dirname(packageJsonPath),
219
+ "dist/cli/index.js",
220
+ );
221
+ const args = ["-p", "Task: hello"];
222
+ const result = getPiSpawnCommand(args, {
223
+ platform: "win32",
224
+ execPath: "C:\\Program Files\\nodejs\\node.exe",
225
+ argv1: "/opt/pi-web/dist/server.js",
226
+ env: { [SELESAI_CODING_AGENT_PACKAGE_ROOT_ENV]: "/opt/pi" },
227
+ existsSync: (filePath) => filePath === packageJsonPath || filePath === cliPath,
228
+ readFileSync: () => JSON.stringify({
229
+ name: "@selesai/code",
230
+ bin: { selesai: "dist/cli/index.js" },
231
+ }),
232
+ });
233
+
234
+ assert.deepEqual(result, {
235
+ command: "C:\\Program Files\\nodejs\\node.exe",
236
+ args: [cliPath, ...args],
237
+ });
238
+ });
239
+
240
+ it("skips an unverified forwarded package-root env before resolving a verified Pi package", () => {
241
+ const tempDir = fs.mkdtempSync(
242
+ path.join(os.tmpdir(), "pi-spawn-unverified-env-root-"),
243
+ );
244
+ try {
245
+ const wrongRoot = path.join(tempDir, "not-selesai");
246
+ const wrongCli = path.join(wrongRoot, "bin", "evil.js");
247
+ const selesaiRoot = path.join(tempDir, "node_modules", "@selesai", "code");
248
+ const selesaiEntry = path.join(selesaiRoot, "dist", "index.js");
249
+ const selesaiCli = path.join(selesaiRoot, "dist", "cli.js");
250
+ fs.mkdirSync(path.dirname(wrongCli), { recursive: true });
251
+ fs.mkdirSync(path.dirname(selesaiCli), { recursive: true });
252
+ fs.writeFileSync(wrongCli, "console.log('evil');\n");
253
+ fs.writeFileSync(path.join(wrongRoot, "package.json"), JSON.stringify({ name: "not-selesai", bin: { selesai: "bin/evil.js" } }));
254
+ fs.writeFileSync(selesaiEntry, "export {};\n");
255
+ fs.writeFileSync(selesaiCli, "#!/usr/bin/env node\n");
256
+ fs.writeFileSync(path.join(selesaiRoot, "package.json"), JSON.stringify({ name: "@selesai/code", bin: { selesai: "dist/cli.js" } }));
257
+
258
+ const result = getPiSpawnCommand(["-p", "Task: hello"], {
259
+ platform: "win32",
260
+ execPath: "C:\\Program Files\\nodejs\\node.exe",
261
+ argv1: "/opt/pi-web/dist/server.js",
262
+ // The Selesai host repo root is itself named @selesai/code, so
263
+ // resolvePiPackageRoot() would find it before the temp verified
264
+ // package; pin the verified temp package to keep the test
265
+ // deterministic while still exercising the unverified env root.
266
+ piPackageRoot: selesaiRoot,
267
+ env: { [SELESAI_CODING_AGENT_PACKAGE_ROOT_ENV]: wrongRoot },
268
+ resolvePackageEntry: () => selesaiEntry,
269
+ });
270
+
271
+ assert.deepEqual(result, {
272
+ command: "C:\\Program Files\\nodejs\\node.exe",
273
+ args: [selesaiCli, "-p", "Task: hello"],
274
+ });
275
+ } finally {
276
+ fs.rmSync(tempDir, { recursive: true, force: true });
277
+ }
278
+ });
279
+
155
280
  it("falls back to plain selesai command on POSIX when CLI script cannot be resolved", () => {
281
+
156
282
  const args = ["--mode", "json", "Task: check output"];
157
283
  const result = getPiSpawnCommand(args, {
158
284
  platform: "darwin",
@@ -281,7 +407,8 @@ describe("getPiSpawnCommand", () => {
281
407
  execPath: "/usr/local/bin/node",
282
408
  argv1: "/opt/selesai/subagent-runner.ts",
283
409
  packageJsonPath,
284
- packageJsonContent: JSON.stringify({ bin: { selesai: "dist/cli/index.js" } }),
410
+ packageJsonContent: JSON.stringify({ name: "@selesai/code", bin: { selesai: "dist/cli/index.js" } }),
411
+
285
412
  existing: [packageJsonPath, cliPath],
286
413
  });
287
414
  const result = getPiSpawnCommand(["-p", "Task: hello"], deps);
@@ -289,16 +416,6 @@ describe("getPiSpawnCommand", () => {
289
416
  assert.equal(result.args[0], cliPath);
290
417
  });
291
418
 
292
- it("falls back to selesai when Windows CLI script cannot be resolved", () => {
293
- const deps = makeDeps({
294
- platform: "win32",
295
- argv1: "/opt/selesai/subagent-runner.ts",
296
- existing: [],
297
- });
298
- const args = ["-p", "Task: hello"];
299
- const result = getPiSpawnCommand(args, deps);
300
- assert.deepEqual(result, { command: "selesai", args });
301
- });
302
419
 
303
420
  it("walks from package main entry to resolve package bin", () => {
304
421
  const tempDir = fs.mkdtempSync(
@@ -334,8 +451,8 @@ describe("getPiSpawnCommand", () => {
334
451
  });
335
452
  });
336
453
 
337
- describe("getPiSpawnCommand with selesaiPackageRoot", () => {
338
- it("resolves CLI script via selesaiPackageRoot when argv1 is not runnable", () => {
454
+ describe("getPiSpawnCommand with piPackageRoot", () => {
455
+ it("resolves CLI script via piPackageRoot when argv1 is not runnable", () => {
339
456
  const packageJsonPath = "/opt/selesai/package.json";
340
457
  const cliPath = path.resolve(
341
458
  path.dirname(packageJsonPath),
@@ -346,10 +463,11 @@ describe("getPiSpawnCommand with selesaiPackageRoot", () => {
346
463
  execPath: "/usr/local/bin/node",
347
464
  argv1: "/opt/selesai/subagent-runner.ts",
348
465
  packageJsonPath,
349
- packageJsonContent: JSON.stringify({ bin: { selesai: "dist/cli/index.js" } }),
466
+ packageJsonContent: JSON.stringify({ name: "@selesai/code", bin: { selesai: "dist/cli/index.js" } }),
467
+
350
468
  existing: [packageJsonPath, cliPath],
351
469
  });
352
- deps.selesaiPackageRoot = "/opt/pi";
470
+ deps.piPackageRoot = "/opt/pi";
353
471
  const result = getPiSpawnCommand(["-p", "Task: hello"], deps);
354
472
  assert.equal(result.command, "/usr/local/bin/node");
355
473
  assert.equal(result.args[0], cliPath);
@@ -367,7 +485,7 @@ describe("resolvePiCliScript", () => {
367
485
  platform: "win32",
368
486
  argv1: "/opt/selesai/subagent-runner.ts",
369
487
  packageJsonPath,
370
- packageJsonContent: JSON.stringify({ bin: "dist/cli/index.mjs" }),
488
+ packageJsonContent: JSON.stringify({ name: "@selesai/code", bin: "dist/cli/index.mjs" }),
371
489
  existing: [packageJsonPath, cliPath],
372
490
  });
373
491
  assert.equal(resolvePiCliScript(deps), cliPath);
@@ -804,6 +804,26 @@ Project prompt.
804
804
  assert.ok(result.contract.tools.extensionArgs.includes("/tmp/subagent-only.ts"));
805
805
  });
806
806
 
807
+ it("projects per-agent tool exclusions and binds them into launch identity", async () => {
808
+ const cwd = path.join(tempDir, "exclude-tools-repo");
809
+ fs.mkdirSync(cwd, { recursive: true });
810
+ writeAgent(path.join(cwd, ".selesai", "agents", "worker.md"), `---
811
+ name: worker
812
+ description: Project worker
813
+ tools: read, write
814
+ excludeTools: write, unknown_tool
815
+ ---
816
+ Project prompt.
817
+ `);
818
+
819
+ const result = await resolveSubagentLaunchContract({ agent: "worker", cwd, task: "Inspect" });
820
+ assert.equal(result.ok, true);
821
+ if (!result.ok) return;
822
+ assert.deepEqual(result.contract.tools.excludeTools, ["write", "unknown_tool"]);
823
+ assert.deepEqual(result.contract.tools.effectiveAllowlist, ["read"]);
824
+ assert.match(result.contract.launchContractDigest, /^[a-f0-9]{64}$/);
825
+ });
826
+
807
827
  it("falls back implicit default fork to fresh when the parent session is not forkable", async () => {
808
828
  const cwd = path.join(tempDir, "repo-implicit-fork");
809
829
  fs.mkdirSync(cwd, { recursive: true });