@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
@@ -24,6 +24,7 @@ import {
24
24
  makeAgent,
25
25
  makeMinimalCtx,
26
26
  events,
27
+ resolveMockPiCallArgs,
27
28
  tryImport,
28
29
  } from "../support/helpers.ts";
29
30
  import registerSubagentExtension from "../../src/extension/index.ts";
@@ -37,7 +38,7 @@ import {
37
38
  type SubagentDelegationResponse,
38
39
  type SubagentDelegationStarted,
39
40
  } from "../../src/api/delegation.ts";
40
- import { CHAIN_RUNS_DIR, DIRS, INTERCOM_DETACH_REQUEST_EVENT, INTERCOM_DETACH_RESPONSE_EVENT, SUBAGENT_CONTROL_EVENT, TEMP_ARTIFACTS_DIR, type AsyncStatus, type ControlEvent, type SubagentState } from "../../src/shared/types.ts";
41
+ import { CHAIN_RUNS_DIR, DIRS, INTERCOM_DETACH_REQUEST_EVENT, INTERCOM_DETACH_RESPONSE_EVENT, SUBAGENT_CONTROL_EVENT, TEMP_ARTIFACTS_DIR, type AsyncStatus, type ChildWatchdogProgress, type ControlEvent, type SubagentState } from "../../src/shared/types.ts";
41
42
  import { ACTIVE_RUN_INDEX_DIR } from "../../src/runs/background/active-run-index.ts";
42
43
  import { listAsyncRuns } from "../../src/runs/background/async-status.ts";
43
44
  import { CHILD_WATCHDOG_STATUS_EVENT } from "../../src/watchdog/child-status.ts";
@@ -159,12 +160,14 @@ interface RunSyncResult {
159
160
  verifyRuns?: Array<{ status?: string }>;
160
161
  runtimeChecks?: Array<{ id?: string; status?: string; message?: string }>;
161
162
  };
163
+ watchdog?: ChildWatchdogProgress;
162
164
  launchResolvedExtensions?: LaunchResolvedExtensions;
163
165
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions;
164
166
  }
165
167
 
166
168
  interface MockPiCallRecord {
167
169
  args?: string[];
170
+ effectiveArgs?: string[];
168
171
  cwd?: string;
169
172
  systemPrompts?: Array<{ mode?: string; path?: string; text?: string; error?: string }>;
170
173
  }
@@ -205,7 +208,7 @@ async function withIsolatedWatchdogSettings<T>(projectDir: string, run: () => Pr
205
208
  }
206
209
  }
207
210
 
208
- function childWatchdogStatus(phase: "idle" | "reviewing" | "autofollow" | "settling" | "stale" | "failed", seq: number, followUpPending = false) {
211
+ function childWatchdogStatus(phase: "idle" | "reviewing" | "stale" | "failed", seq: number) {
209
212
  return {
210
213
  type: CHILD_WATCHDOG_STATUS_EVENT,
211
214
  runId: "watchdog-child-run",
@@ -215,7 +218,6 @@ function childWatchdogStatus(phase: "idle" | "reviewing" | "autofollow" | "settl
215
218
  seq,
216
219
  phase,
217
220
  ts: Date.now() + seq,
218
- followUpPending,
219
221
  };
220
222
  }
221
223
 
@@ -339,7 +341,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
339
341
  removeTempDir(tempDir);
340
342
  });
341
343
 
342
- function readCall(): { args: string[]; cwd?: string; systemPrompts: NonNullable<MockPiCallRecord["systemPrompts"]> } {
344
+ function readCall(): { args: string[]; effectiveArgs?: string[]; cwd?: string; systemPrompts: NonNullable<MockPiCallRecord["systemPrompts"]> } {
343
345
  const callFile = fs.readdirSync(mockPi.dir)
344
346
  .filter((name) => name.startsWith("call-") && name.endsWith(".json"))
345
347
  .sort()
@@ -347,18 +349,22 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
347
349
  assert.ok(callFile, "expected a recorded mock pi call");
348
350
  const payload = JSON.parse(fs.readFileSync(path.join(mockPi.dir, callFile), "utf-8")) as MockPiCallRecord;
349
351
  assert.ok(Array.isArray(payload.args), "expected recorded args");
350
- return { args: payload.args, cwd: payload.cwd, systemPrompts: payload.systemPrompts ?? [] };
352
+ return { args: payload.args, effectiveArgs: payload.effectiveArgs, cwd: payload.cwd, systemPrompts: payload.systemPrompts ?? [] };
351
353
  }
352
354
 
353
355
  function readCallArgs(): string[] {
354
- return readCall().args;
356
+ const call = readCall();
357
+ return resolveMockPiCallArgs(call);
355
358
  }
356
359
 
357
- function readAllCallArgs(): string[][] {
360
+ function readAllCallArgs(effective = false): string[][] {
358
361
  return fs.readdirSync(mockPi.dir)
359
362
  .filter((name) => name.startsWith("call-") && name.endsWith(".json"))
360
363
  .sort()
361
- .map((name) => (JSON.parse(fs.readFileSync(path.join(mockPi.dir, name), "utf-8")) as MockPiCallRecord).args);
364
+ .map((name) => {
365
+ const call = JSON.parse(fs.readFileSync(path.join(mockPi.dir, name), "utf-8")) as MockPiCallRecord;
366
+ return effective ? resolveMockPiCallArgs(call) : call.args ?? [];
367
+ });
362
368
  }
363
369
 
364
370
  function makeExecutor(
@@ -623,6 +629,38 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
623
629
  assert.doesNotMatch(readCallArgs().join("\n"), /This path is authoritative for this run/);
624
630
  });
625
631
 
632
+ it("keeps escaped read-only delegate tasks from triggering the completion guard", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
633
+ mockPi.onCall({ output: "The exact user-facing response" });
634
+ const task = [
635
+ "This is a read-only skill compliance scenario, not an implementation assignment.",
636
+ "Read the supplied skill and write the exact user-facing response.",
637
+ "Do not edit files.",
638
+ "Use a scenario that discusses selection for an implementation task or closeout of an implementation assignment.",
639
+ ].join("\\n");
640
+ const result = await makeExecutor([makeAgent("delegate", {
641
+ tools: ["read", "grep", "find", "ls", "bash", "edit", "write", "contact_supervisor"],
642
+ inheritProjectContext: true,
643
+ systemPromptMode: "append",
644
+ })]).execute(
645
+ "workflow-read-only-delegate",
646
+ {
647
+ async: false,
648
+ acceptance: false,
649
+ preflight: { version: 1, coverage: "complete", lanes: [{ key: "main", mode: "review" }] },
650
+ workflowScript: `return runs.all([{ key: "main", agent: "delegate", task: ${JSON.stringify(task)} }]);`,
651
+ },
652
+ new AbortController().signal,
653
+ undefined,
654
+ makeMinimalCtx(tempDir),
655
+ );
656
+
657
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
658
+ const child = (result.details as { results?: Array<{ exitCode?: number; error?: string; output?: string }> } | undefined)?.results?.[0];
659
+ assert.equal(child?.exitCode, 0);
660
+ assert.equal(child?.error, undefined);
661
+ assert.match(result.content[0]?.text ?? "", /The exact user-facing response/);
662
+ });
663
+
626
664
  it("consumes one exact host-only workflow child permit before spawn", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
627
665
  const executor = makeExecutor([makeAgent("echo"), makeAgent("other"), makeAgent("external", { runner: { type: "external-cli", command: "external" } })]);
628
666
  const ctx = makeMinimalCtx(tempDir);
@@ -1848,7 +1886,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1848
1886
  it("notifies the parent when an async workflow child needs attention", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1849
1887
  mockPi.onCall({
1850
1888
  steps: [
1851
- { jsonl: [events.toolStart("read", { path: "src/example.ts" }), events.toolEnd("read"), events.toolResult("read", "contents")] },
1889
+ { jsonl: [events.toolStart("read", { path: "src/example.ts" }), events.toolEnd("read"), events.toolResult("read", "contents"), events.assistantMessage("Started")] },
1852
1890
  { delay: 2_500, jsonl: [events.assistantMessage("Done")] },
1853
1891
  ],
1854
1892
  });
@@ -2368,6 +2406,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2368
2406
  assert.match(result.content[0]?.text ?? "", /reviewed auth/);
2369
2407
  assert.equal(result.details.mode, "workflow");
2370
2408
  assert.equal(result.details.results.length, 2);
2409
+ assert.deepEqual(result.details.results.map((entry) => entry.workflowKey), ["scan", "review"]);
2371
2410
  assert.equal(result.details.workflow?.value && (result.details.workflow.value as { stateType?: unknown }).stateType, "object");
2372
2411
  assert.ok(result.details.missionId);
2373
2412
  const missionFiles = fs.readdirSync(path.join(agentDir, "missions", "projects"), { recursive: true })
@@ -4031,6 +4070,51 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
4031
4070
  assert.equal(mockPi.callCount(), 1);
4032
4071
  });
4033
4072
 
4073
+ it("passes an agent-level tool budget to an async single child", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
4074
+ const toolBudget = { soft: 100, hard: 150, block: "*" as const };
4075
+ mockPi.onCall({ echoEnv: [TOOL_BUDGET_ENV] });
4076
+ const executor = makeExecutor([makeAgent("echo", { toolBudget })]);
4077
+ let asyncDir: string | undefined;
4078
+ let resultPath: string | undefined;
4079
+
4080
+ try {
4081
+ const result = await executor.execute(
4082
+ "agent-tool-budget-async-single",
4083
+ { agent: "echo", task: "Run the async budget probe", async: true },
4084
+ new AbortController().signal,
4085
+ undefined,
4086
+ makeMinimalCtx(tempDir),
4087
+ );
4088
+
4089
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "async launch failed");
4090
+ assert.ok(result.details.asyncId);
4091
+ asyncDir = result.details.asyncDir;
4092
+ resultPath = path.join(DIRS.results, `${result.details.asyncId}.json`);
4093
+
4094
+ let persisted: { state?: string; results?: Array<{ output?: string }> } = {};
4095
+ for (let attempt = 0; attempt < 200; attempt++) {
4096
+ if (fs.existsSync(resultPath)) persisted = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
4097
+ if (persisted.state === "complete" || persisted.state === "failed") break;
4098
+ await new Promise((resolve) => setTimeout(resolve, 20));
4099
+ }
4100
+ assert.equal(persisted.state, "complete");
4101
+ const childEnv = JSON.parse(persisted.results?.[0]?.output ?? "{}") as Record<string, string | null>;
4102
+ assert.deepEqual(JSON.parse(childEnv[TOOL_BUDGET_ENV] ?? "null"), toolBudget);
4103
+ assert.equal(mockPi.callCount(), 1);
4104
+ const processTerminalPath = path.join(asyncDir!, "process-terminal.json");
4105
+ let processTerminal: { state?: string } = {};
4106
+ for (let attempt = 0; attempt < 100; attempt++) {
4107
+ if (fs.existsSync(processTerminalPath)) processTerminal = JSON.parse(fs.readFileSync(processTerminalPath, "utf-8"));
4108
+ if (processTerminal.state && processTerminal.state !== "pending") break;
4109
+ await new Promise((resolve) => setTimeout(resolve, 20));
4110
+ }
4111
+ assert.match(processTerminal.state ?? "", /^(observed|unknown)$/);
4112
+ } finally {
4113
+ if (asyncDir) fs.rmSync(asyncDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
4114
+ if (resultPath) fs.rmSync(resultPath, { force: true });
4115
+ }
4116
+ });
4117
+
4034
4118
  it("keeps delegated agent and config tool budgets at a minimum of one", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
4035
4119
  const ctx = makeMinimalCtx(tempDir);
4036
4120
  const cases = [
@@ -4067,6 +4151,52 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
4067
4151
  assert.equal(mockPi.callCount(), 0);
4068
4152
  });
4069
4153
 
4154
+ it("accepts JSON-encoded acceptance objects and diagnoses malformed strings", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
4155
+ const acceptance = {
4156
+ level: "checked" as const,
4157
+ criteria: [{ id: "criterion-1", must: "Return required evidence" }],
4158
+ };
4159
+ const acceptedOutput = [
4160
+ "done",
4161
+ "```acceptance-report",
4162
+ JSON.stringify({
4163
+ criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "implemented" }],
4164
+ changedFiles: ["src/file.ts"],
4165
+ testsAddedOrUpdated: ["test/file.test.ts"],
4166
+ commandsRun: [{ command: "npm test", result: "passed", summary: "passed" }],
4167
+ validationOutput: ["tests passed"],
4168
+ residualRisks: [],
4169
+ noStagedFiles: true,
4170
+ }),
4171
+ "```",
4172
+ ].join("\n");
4173
+ const executor = makeExecutor([makeAgent("echo")]);
4174
+ for (const [index, input] of [acceptance, JSON.stringify(acceptance)].entries()) {
4175
+ mockPi.onCall({ output: acceptedOutput });
4176
+ const result = await executor.execute(
4177
+ `acceptance-object-string-${index}`,
4178
+ { async: false, agent: "echo", task: "Return the required evidence", acceptance: input as never },
4179
+ new AbortController().signal,
4180
+ undefined,
4181
+ makeMinimalCtx(tempDir),
4182
+ );
4183
+
4184
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "acceptance run failed");
4185
+ assert.equal(result.details.results[0]?.acceptance?.status, "checked");
4186
+ }
4187
+
4188
+ const malformed = await executor.execute(
4189
+ "malformed-acceptance-object-string",
4190
+ { async: false, agent: "echo", task: "Do not spawn", acceptance: '{"level":"checked"' as never },
4191
+ new AbortController().signal,
4192
+ undefined,
4193
+ makeMinimalCtx(tempDir),
4194
+ );
4195
+ assert.equal(malformed.isError, true);
4196
+ assert.match(malformed.content[0]?.text ?? "", /acceptance JSON string must encode a valid acceptance object/i);
4197
+ assert.equal(mockPi.callCount(), 2);
4198
+ });
4199
+
4070
4200
  it("rejects invalid verified acceptance before spawning", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
4071
4201
  const executor = makeExecutor([makeAgent("echo")]);
4072
4202
  const invalidPolicies = [
@@ -4995,6 +5125,16 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
4995
5125
  assert.doesNotMatch(call.args.join("\n"), /## Acceptance Contract/);
4996
5126
  });
4997
5127
 
5128
+ it("does not inject inferred acceptance into reviewer prompts", async () => {
5129
+ mockPi.onCall({ output: "VERDICT: PASS" });
5130
+ const result = await runSync(tempDir, [makeAgent("reviewer", { tools: ["read"], completionGuard: false })], "reviewer", "Review the diff and return findings only.", {
5131
+ runId: "reviewer-inferred-acceptance",
5132
+ });
5133
+
5134
+ assert.equal(result.exitCode, 0);
5135
+ assert.doesNotMatch(readCall().args.join("\n"), /## Acceptance Contract/);
5136
+ });
5137
+
4998
5138
  it("agent contract v1 keeps acceptance rejection out of execution status", async () => {
4999
5139
  mockPi.onCall({ output: "Done\n```acceptance-report\n{\"criteriaSatisfied\":[{\"id\":\"criterion-1\",\"status\":\"not-satisfied\",\"evidence\":\"no proof\"}]}\n```" });
5000
5140
  const agents = [makeAgent("worker", { tools: ["read"], completionGuard: false })];
@@ -5288,7 +5428,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
5288
5428
  "workflow-schema-acceptance-sidecar",
5289
5429
  {
5290
5430
  async: false,
5291
- acceptance: { level: "checked", criteria: [{ id: "proof", must: "Return required proof" }] },
5431
+ acceptance: { level: "checked", report: "on", criteria: [{ id: "proof", must: "Return required proof" }] },
5292
5432
  workflowScript: `
5293
5433
  const child = await runs.run("schema", {
5294
5434
  agent: "echo",
@@ -5326,7 +5466,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
5326
5466
  "workflow-schema-acceptance-missing-sidecar",
5327
5467
  {
5328
5468
  async: false,
5329
- acceptance: { level: "checked", criteria: [{ id: "proof", must: "Return required proof" }] },
5469
+ acceptance: { level: "checked", report: "on", criteria: [{ id: "proof", must: "Return required proof" }] },
5330
5470
  workflowScript: `
5331
5471
  const child = await runs.run("schema", {
5332
5472
  agent: "echo",
@@ -5350,6 +5490,38 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
5350
5490
  assert.match(result.content[0]?.text ?? "", /acceptance/i);
5351
5491
  });
5352
5492
 
5493
+ it("uses fenced acceptance reports when outputSchema acceptance report capture is off", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
5494
+ const acceptanceReport = {
5495
+ criteriaSatisfied: [{ id: "proof", status: "satisfied", evidence: "fenced proof" }],
5496
+ changedFiles: [], testsAddedOrUpdated: [],
5497
+ commandsRun: [{ command: "mock", result: "passed", summary: "passed" }],
5498
+ validationOutput: ["validated"], residualRisks: [], noStagedFiles: true,
5499
+ };
5500
+ mockPi.onCall({
5501
+ matchArgIncludes: "Finish with a fenced JSON block tagged `acceptance-report`",
5502
+ stdoutRaw: [
5503
+ events.assistantMessage(`done\n\`\`\`acceptance-report\n${JSON.stringify(acceptanceReport)}\n\`\`\``),
5504
+ { type: "tool_execution_start", toolName: "structured_output", args: { value: { ok: true } } },
5505
+ { type: "tool_result_end", message: { role: "toolResult", toolName: "structured_output", content: [{ type: "text", text: "Structured output captured." }] } },
5506
+ { type: "tool_execution_end", toolName: "structured_output" },
5507
+ ].map((entry) => JSON.stringify(entry)).join("\n") + "\n",
5508
+ structuredOutputCapture: { ok: true },
5509
+ });
5510
+
5511
+ const result = await makeExecutor([makeAgent("echo")]).execute(
5512
+ "single-schema-fenced-acceptance",
5513
+ {
5514
+ agent: "echo", task: "Return structured data",
5515
+ outputSchema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" } } },
5516
+ acceptance: { level: "checked", report: "off", criteria: [{ id: "proof", must: "Return required proof" }] },
5517
+ },
5518
+ new AbortController().signal, undefined, makeMinimalCtx(tempDir),
5519
+ );
5520
+
5521
+ assert.equal(result.isError, undefined, result.content[0]?.text);
5522
+ assert.equal(result.details.results[0]?.acceptance?.status, "checked");
5523
+ });
5524
+
5353
5525
  it("surfaces corrupt outputSchema acceptance sidecar read errors", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
5354
5526
  mockPi.onCall({
5355
5527
  stdoutRaw: [
@@ -5366,7 +5538,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
5366
5538
  "workflow-schema-acceptance-corrupt-sidecar",
5367
5539
  {
5368
5540
  async: false,
5369
- acceptance: { level: "checked", criteria: [{ id: "proof", must: "Return required proof" }] },
5541
+ acceptance: { level: "checked", report: "on", criteria: [{ id: "proof", must: "Return required proof" }] },
5370
5542
  workflowScript: `
5371
5543
  const child = await runs.run("schema", {
5372
5544
  agent: "echo",
@@ -5798,7 +5970,12 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
5798
5970
  assert.equal(result.finalOutput, "Recovered via file delivery");
5799
5971
  assert.equal(mockPi.callCount(), 2);
5800
5972
  const [firstArgs, retryArgs] = readAllCallArgs();
5801
- assert.ok(firstArgs?.includes("Task: Do work"), "first attempt should deliver the task inline");
5973
+ const firstTaskArg = firstArgs?.find((arg) => arg === "Task: Do work" || arg.endsWith("task.md"));
5974
+ if (process.platform === "darwin") {
5975
+ assert.ok(firstTaskArg?.startsWith("@"), "first attempt should deliver the task through a file on macOS");
5976
+ } else {
5977
+ assert.equal(firstTaskArg, "Task: Do work", "first attempt should deliver the task inline");
5978
+ }
5802
5979
  const retryTaskArg = retryArgs?.at(-1) ?? "";
5803
5980
  assert.ok(
5804
5981
  retryTaskArg.startsWith("@") && retryTaskArg.endsWith("task.md"),
@@ -6616,7 +6793,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
6616
6793
  const [firstArgs, resumedArgs] = readAllCallArgs();
6617
6794
  assert.equal(firstArgs?.[firstArgs.indexOf("--session") + 1], sessionFile);
6618
6795
  assert.equal(resumedArgs?.[resumedArgs.indexOf("--session") + 1], sessionFile);
6619
- assert.match(resumedArgs?.at(-1) ?? "", /Continue from the current files and transcript/);
6796
+ assert.match(readAllCallArgs(true)[1]?.at(-1) ?? "", /Continue from the current files and transcript/);
6620
6797
  assert.equal(fs.readFileSync(path.join(tempDir, "side-effect.txt"), "utf-8"), "done");
6621
6798
  });
6622
6799
 
@@ -6689,7 +6866,12 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
6689
6866
  it("delivers delegated activity-state transitions despite heartbeat suppression", async () => {
6690
6867
  const attentionUpdates: Array<{ details?: { progress?: ProgressSummary[] } }> = [];
6691
6868
  const attentionReleasePath = path.join(tempDir, "release-delegated-attention");
6692
- mockPi.onCall({ output: "Done", waitForPath: attentionReleasePath });
6869
+ mockPi.onCall({
6870
+ steps: [
6871
+ { jsonl: [events.assistantMessage("Started")] },
6872
+ { waitForPath: attentionReleasePath, jsonl: [events.assistantMessage("Done")] },
6873
+ ],
6874
+ });
6693
6875
  const attentionRun = runSync!(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
6694
6876
  suppressUnchangedDelegationUpdates: true,
6695
6877
  controlConfig: {
@@ -6704,7 +6886,13 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
6704
6886
  onUpdate: (update: { details?: { progress?: ProgressSummary[] } }) => attentionUpdates.push(update),
6705
6887
  });
6706
6888
  try {
6707
- const deadline = Date.now() + 5_000;
6889
+ const turnDeadline = Date.now() + 15_000;
6890
+ while (!attentionUpdates.some((update) => (update.details?.progress?.[0]?.turnCount ?? 0) > 0) && Date.now() < turnDeadline) {
6891
+ await new Promise((resolve) => setTimeout(resolve, 50));
6892
+ }
6893
+ assert.ok(attentionUpdates.some((update) => (update.details?.progress?.[0]?.turnCount ?? 0) > 0), "test fixture should complete one assistant turn before waiting for idle attention");
6894
+
6895
+ const deadline = Date.now() + 15_000;
6708
6896
  while (!attentionUpdates.some((update) => update.details?.progress?.[0]?.activityState === "needs_attention") && Date.now() < deadline) {
6709
6897
  await new Promise((resolve) => setTimeout(resolve, 50));
6710
6898
  }
@@ -6748,6 +6936,31 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
6748
6936
  }
6749
6937
  });
6750
6938
 
6939
+ it("does not deliver idle attention before a child completes its first assistant turn", async () => {
6940
+ const updates: Array<{ details?: { progress?: ProgressSummary[] } }> = [];
6941
+ const releasePath = path.join(tempDir, "release-zero-turn-attention");
6942
+ mockPi.onCall({ output: "Done", waitForPath: releasePath });
6943
+
6944
+ const runPromise = runSync!(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
6945
+ controlConfig: {
6946
+ enabled: true,
6947
+ needsAttentionAfterMs: 200,
6948
+ activeNoticeAfterMs: 999_999,
6949
+ notifyOn: ["needs_attention"],
6950
+ notifyChannels: ["event"],
6951
+ },
6952
+ onUpdate: (update: { details?: { progress?: ProgressSummary[] } }) => updates.push(update),
6953
+ });
6954
+ try {
6955
+ await new Promise((resolve) => setTimeout(resolve, 500));
6956
+ assert.equal(updates.some((update) => update.details?.progress?.[0]?.activityState === "needs_attention"), false);
6957
+ } finally {
6958
+ fs.writeFileSync(releasePath, "release", "utf-8");
6959
+ const result = await runPromise;
6960
+ assert.equal(result.exitCode, 0);
6961
+ }
6962
+ });
6963
+
6751
6964
  it("reports foreground context window usage without changing cumulative spend", async () => {
6752
6965
  mockPi.onCall({
6753
6966
  jsonl: [{
@@ -7161,7 +7374,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
7161
7374
  );
7162
7375
 
7163
7376
  const call = readCall();
7164
- const taskArg = call.args.at(-1) ?? "";
7377
+ const taskArg = resolveMockPiCallArgs(call).at(-1) ?? "";
7165
7378
  const systemPrompt = call.systemPrompts[0]?.text ?? "";
7166
7379
  assert.equal(result.isError, undefined);
7167
7380
  assert.match(taskArg, new RegExp(`Write your findings to exactly this path: ${escapeRegExp(overridePath)}`));
@@ -7188,7 +7401,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
7188
7401
  );
7189
7402
 
7190
7403
  const call = readCall();
7191
- const taskArg = call.args.at(-1) ?? "";
7404
+ const taskArg = resolveMockPiCallArgs(call).at(-1) ?? "";
7192
7405
  const systemPrompt = call.systemPrompts[0]?.text ?? "";
7193
7406
  assert.equal(result.isError, undefined);
7194
7407
  assert.equal(fs.readFileSync(outputPath, "utf-8"), "complete read-only analysis");
@@ -7838,7 +8051,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
7838
8051
  assert.ok(elapsed < 5000, `unconfigured watchdog status should not delay final drain, took ${elapsed}ms`);
7839
8052
  assert.equal(result.exitCode, 0);
7840
8053
  assert.equal(result.finalOutput, "done-without-watchdog-config");
7841
- assert.equal((result as RunSyncResult & { watchdog?: unknown }).watchdog, undefined);
8054
+ assert.equal(result.watchdog, undefined);
7842
8055
  });
7843
8056
  });
7844
8057
 
@@ -7862,7 +8075,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
7862
8075
  assert.ok(elapsed < 6000, `settled watchdog should still allow cleanup, took ${elapsed}ms`);
7863
8076
  assert.equal(result.exitCode, 0);
7864
8077
  assert.equal(result.finalOutput, "done-before-watchdog");
7865
- assert.equal((result as RunSyncResult & { watchdog?: { phase?: string } }).watchdog?.phase, "idle");
8078
+ assert.equal(result.watchdog?.phase, "idle");
7866
8079
  });
7867
8080
  });
7868
8081
 
@@ -7882,12 +8095,84 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
7882
8095
  assert.ok(elapsed < 5000, `watchdog tail fallback should not hang, took ${elapsed}ms`);
7883
8096
  assert.equal(result.exitCode, 0);
7884
8097
  assert.equal(result.finalOutput, "done-before-watchdog-timeout");
7885
- const watchdog = (result as RunSyncResult & { watchdog?: { phase?: string; timedOut?: boolean } }).watchdog;
8098
+ const watchdog = result.watchdog;
7886
8099
  assert.equal(watchdog?.phase, "stale");
7887
8100
  assert.equal(watchdog?.timedOut, true);
7888
8101
  });
7889
8102
  });
7890
8103
 
8104
+ it("blocks or warns on launches that violate configured watchdog rules", async () => {
8105
+ await withIsolatedWatchdogSettings(tempDir, async () => {
8106
+ const settingsPath = path.join(tempDir, ".selesai", "settings.json");
8107
+ const writeRules = (action: "warn" | "block") => {
8108
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
8109
+ fs.writeFileSync(settingsPath, JSON.stringify({ subagents: { watchdog: { rules: { action, roleModels: { echo: { deny: ["mock/*"], note: "echo must not use mock models" } } } } } }, null, 2), "utf-8");
8110
+ };
8111
+ const sent: unknown[] = [];
8112
+ const watchdog = new MainWatchdogRuntime({ cwd: tempDir, displayWarning: (details) => { sent.push(details); } });
8113
+ const executor = createSubagentExecutor!({
8114
+ pi: { events: createEventBus(), getSessionName: () => undefined },
8115
+ state: { baseCwd: tempDir, currentSessionId: null, asyncJobs: new Map(), foregroundControls: new Map(), lastForegroundControlId: null },
8116
+ config: {},
8117
+ asyncByDefault: false,
8118
+ watchdog,
8119
+ tempArtifactsDir: tempDir,
8120
+ getSubagentSessionRoot: () => path.join(tempDir, ".pi-subagents", "sessions"),
8121
+ expandTilde: (value: string) => value,
8122
+ discoverAgents: () => ({ agents: [makeAgent("echo")] }),
8123
+ allowMutatingManagementActions: true,
8124
+ } as never);
8125
+ const callsBefore = mockPi.callCount();
8126
+
8127
+ writeRules("block");
8128
+ const blocked = await executor.execute("rules-block", { async: false, agent: "echo", task: "Do work", model: "mock/test-model" }, new AbortController().signal, undefined, makeMinimalCtx(tempDir));
8129
+ assert.equal(blocked.isError, true);
8130
+ assert.match(blocked.content[0]?.text ?? "", /Launch blocked by subagents\.watchdog\.rules: Agent 'echo' was launched with denied model 'mock\/test-model'/);
8131
+ assert.equal(mockPi.callCount(), callsBefore, "a blocked launch never starts the child");
8132
+ assert.equal(sent.length, 0);
8133
+
8134
+ const nestedSettings = path.join(tempDir, "packages", "app", ".selesai", "settings.json");
8135
+ fs.mkdirSync(path.dirname(nestedSettings), { recursive: true });
8136
+ fs.writeFileSync(nestedSettings, JSON.stringify({ subagents: { watchdog: { rules: { action: "block", roleModels: { echo: { deny: ["mock/*"] } } } } } }, null, 2), "utf-8");
8137
+ writeRules("warn");
8138
+ const nestedBlocked = await executor.execute("rules-workflow-cwd", { async: false, cwd: "packages/app", workflowScript: `return runs.run("one", { agent: "echo", task: "Do work", model: "mock/test-model" });` }, new AbortController().signal, undefined, makeMinimalCtx(tempDir));
8139
+ assert.equal(nestedBlocked.isError, true, "rules load from the resolved workflow cwd");
8140
+ assert.equal(mockPi.callCount(), callsBefore);
8141
+
8142
+ mockPi.onCall({ output: "warned but ran" });
8143
+ const warned = await executor.execute("rules-warn", { async: false, agent: "echo", task: "Do work", model: "mock/test-model" }, new AbortController().signal, undefined, makeMinimalCtx(tempDir));
8144
+ assert.equal(warned.isError, undefined, warned.content[0]?.text);
8145
+ assert.equal(mockPi.callCount(), callsBefore + 1);
8146
+ assert.equal(sent.length, 1);
8147
+ assert.match((sent[0] as { summary?: string }).summary ?? "", /denied model 'mock\/test-model'/);
8148
+ assert.match((sent[0] as { evidence?: string }).evidence ?? "", /echo must not use mock models/);
8149
+ });
8150
+ });
8151
+
8152
+ it("fails explicit acceptance on an unaddressed child watchdog blocker and passes once a turn follows it", async () => {
8153
+ await withIsolatedWatchdogSettings(tempDir, async () => {
8154
+ writeWatchdogSettings(tempDir);
8155
+ const agents = makeAgentConfigs(["echo"]);
8156
+ const acceptance = { level: "checked" as const, criteria: ["Ship it"] };
8157
+ const blockerCheck = (result: RunSyncResult) => result.acceptance?.runtimeChecks?.find((entry) => entry.id === "watchdog-blocker");
8158
+
8159
+ mockPi.onCall({ jsonl: [events.watchdogWarning("concern", "Minor naming concern"), events.acceptanceReport(), events.watchdogWarning("blocker", "Claims tests passed without running them")] });
8160
+ const unaddressed = await runSync(tempDir, agents, "echo", "Task", { runId: "watchdog-child-run", acceptance });
8161
+ assert.deepEqual(unaddressed.watchdog?.warnings?.map((warning) => [warning.severity, warning.addressed]), [["concern", true], ["blocker", false]]);
8162
+ assert.equal(blockerCheck(unaddressed)?.status, "failed");
8163
+ assert.match(blockerCheck(unaddressed)?.message ?? "", /Unresolved watchdog blocker: Claims tests passed without running them/);
8164
+ assert.equal(unaddressed.acceptance?.status, "rejected");
8165
+ assert.equal(unaddressed.exitCode, 1);
8166
+ assert.match(unaddressed.error ?? "", /Unresolved watchdog blocker/);
8167
+
8168
+ mockPi.onCall({ jsonl: [events.assistantMessage("first pass"), events.watchdogWarning("blocker", "Claims tests passed without running them"), events.acceptanceReport()] });
8169
+ const addressed = await runSync(tempDir, agents, "echo", "Task", { runId: "watchdog-child-run-2", acceptance });
8170
+ assert.equal(addressed.watchdog?.warnings?.[0]?.addressed, true);
8171
+ assert.equal(blockerCheck(addressed)?.status, "passed");
8172
+ assert.equal(addressed.exitCode, 0, addressed.error);
8173
+ });
8174
+ });
8175
+
7891
8176
  it("treats forced drain after final assistant output as cleanup success", async () => {
7892
8177
  mockPi.onCall({
7893
8178
  jsonl: [events.assistantMessage("done-before-drain")],
@@ -8012,7 +8297,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
8012
8297
  });
8013
8298
 
8014
8299
  const result = await runSync(tempDir, makeAgentConfigs(["slow"]), "slow", "Slow task", {
8015
- timeoutMs: 150,
8300
+ timeoutMs: 1000,
8016
8301
  outputPath: reportPath,
8017
8302
  outputMode: "file-only",
8018
8303
  acceptance: false,
@@ -292,6 +292,7 @@ describe("subagents watchdog slash command", { skip: !available ? "watchdog comm
292
292
  assert.match(content, /Subagent watchdog/);
293
293
  assert.match(content, /Main: off \(default off\)/);
294
294
  assert.match(content, /Runtime: idle/);
295
+ assert.match(content, /Rules: none/);
295
296
  assert.match(content, /Review model call: real model review/);
296
297
  assert.match(content, /Sources:/);
297
298
  });
@@ -13,6 +13,10 @@ export function createMockPi(): MockPi {
13
13
  return _createMockPi();
14
14
  }
15
15
 
16
+ export function resolveMockPiCallArgs(call: { args?: readonly string[]; effectiveArgs?: readonly string[] }): string[] {
17
+ return [...(call.effectiveArgs ?? call.args ?? [])];
18
+ }
19
+
16
20
  export function createTempDir(prefix = "pi-subagent-test-"): string {
17
21
  return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
18
22
  }
@@ -176,6 +180,27 @@ export const events = {
176
180
  };
177
181
  },
178
182
 
183
+ /** Final assistant turn carrying a satisfied acceptance report. */
184
+ acceptanceReport(): object {
185
+ const report = { criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "implemented" }], changedFiles: ["src/file.ts"], testsAddedOrUpdated: ["test/file.test.ts"], commandsRun: [{ command: "npm test", result: "passed", summary: "passed" }], validationOutput: ["tests passed"], residualRisks: [], noStagedFiles: true };
186
+ return events.assistantMessage(["done", "```acceptance-report", JSON.stringify(report), "```"].join("\n"));
187
+ },
188
+
189
+ /** A child watchdog warning custom message as the child watchdog writes it into the child JSONL. */
190
+ watchdogWarning(severity: "concern" | "blocker", summary: string, overrides: Record<string, unknown> = {}): object {
191
+ return {
192
+ type: "message_end",
193
+ message: {
194
+ role: "custom",
195
+ customType: "subagent_watchdog_warning",
196
+ content: `<subagent_watchdog severity="${severity}">${summary}</subagent_watchdog>`,
197
+ display: true,
198
+ details: { severity, category: "test-gap", source: "child", agent: "worker", summary, evidence: "The transcript claims tests passed but no test command ran.", recommendedAction: "Run the focused test before finishing.", state: "displayed", displayedAt: new Date().toISOString(), ...overrides },
199
+ timestamp: Date.now(),
200
+ },
201
+ };
202
+ },
203
+
179
204
  /** Assistant write tool call plus its successful tool result, as one completed write. */
180
205
  completedWrite(filePath: string, content: string, model = "mock/test-model"): object[] {
181
206
  const id = `write-${++writeCallSeq}`;
@@ -2,8 +2,19 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
 
5
- if (!process.env.SELESAI_SUBAGENTS_TEMP_ROOT) {
6
- const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-test-root-"));
7
- process.env.SELESAI_SUBAGENTS_TEMP_ROOT = tempRoot;
5
+ const configuredTempRoot = process.env.SELESAI_SUBAGENTS_TEMP_ROOT?.trim();
6
+ const tempRoot = configuredTempRoot
7
+ ? path.resolve(configuredTempRoot)
8
+ : fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-test-root-"));
9
+ process.env.SELESAI_SUBAGENTS_TEMP_ROOT = tempRoot;
10
+
11
+ const nestedTestProcess = process.env.SELESAI_SUBAGENTS_TEST_LOADER === "1";
12
+ const isolatedHome = path.join(tempRoot, "home");
13
+ process.env.HOME = isolatedHome;
14
+ process.env.USERPROFILE = isolatedHome;
15
+ if (!nestedTestProcess) delete process.env.SELESAI_CODING_AGENT_DIR;
16
+ process.env.SELESAI_SUBAGENTS_TEST_LOADER = "1";
17
+
18
+ if (!configuredTempRoot) {
8
19
  process.on("exit", () => fs.rmSync(tempRoot, { recursive: true, force: true }));
9
- }
20
+ }
@@ -56,12 +56,26 @@ function hasArgMatcher(response) {
56
56
  return Object.prototype.hasOwnProperty.call(response ?? {}, "matchArgIncludes");
57
57
  }
58
58
 
59
+ function resolveTaskFileArg(arg) {
60
+ if (typeof arg !== "string" || !arg.startsWith("@") || !arg.endsWith("task.md")) return arg;
61
+ try {
62
+ return fs.readFileSync(arg.slice(1), "utf-8");
63
+ } catch {
64
+ return arg;
65
+ }
66
+ }
67
+
68
+ function resolveTaskFileArgs(args) {
69
+ return args.map(resolveTaskFileArg);
70
+ }
71
+
59
72
  function responseMatchesArgs(response, args) {
60
73
  const matcher = response?.matchArgIncludes;
61
74
  if (matcher === undefined) return true;
62
75
  const needles = Array.isArray(matcher) ? matcher : [matcher];
63
76
  if (needles.length === 0) return true;
64
- const haystack = args.join("\n");
77
+ const effectiveArgs = resolveTaskFileArgs(args);
78
+ const haystack = `${args.join("\n")}\n${effectiveArgs.join("\n")}`;
65
79
  return needles.every((needle) => typeof needle === "string" && haystack.includes(needle));
66
80
  }
67
81
 
@@ -360,9 +374,10 @@ async function main() {
360
374
  }
361
375
  writeSessionFile(args);
362
376
  writeToolDiagnostic(response);
377
+ const effectiveArgs = resolveTaskFileArgs(args);
363
378
  const callPath = path.join(queueDir, `call-${Date.now()}-${process.pid}-${Math.random().toString(16).slice(2)}.json`);
364
379
  const callTempPath = `${callPath}.tmp-${process.pid}-${Date.now()}`;
365
- fs.writeFileSync(callTempPath, JSON.stringify({ args, cwd: process.cwd(), systemPrompts: readSystemPromptRecords(args), requiredChildTools: readRequiredToolsEnv() }), "utf-8");
380
+ fs.writeFileSync(callTempPath, JSON.stringify({ args, effectiveArgs, cwd: process.cwd(), systemPrompts: readSystemPromptRecords(args), requiredChildTools: readRequiredToolsEnv() }), "utf-8");
366
381
  fs.renameSync(callTempPath, callPath);
367
382
 
368
383
  if (typeof response.delay === "number" && response.delay > 0) {