@selesai/code 0.5.20 → 0.5.21

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 (263) hide show
  1. package/dist/extensions/handoff-new.test.ts +8 -9
  2. package/dist/extensions/handoff-new.ts +6 -8
  3. package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
  4. package/dist/extensions/pi-subagents/README.md +602 -220
  5. package/dist/extensions/pi-subagents/banner.png +0 -0
  6. package/dist/extensions/pi-subagents/index.ts +1 -0
  7. package/dist/extensions/pi-subagents/install.mjs +3 -3
  8. package/dist/extensions/pi-subagents/package-lock.json +3405 -0
  9. package/dist/extensions/pi-subagents/package.json +26 -14
  10. package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
  11. package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
  12. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
  13. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
  14. package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
  15. package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
  16. package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
  17. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
  18. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
  19. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
  20. package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
  21. package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
  22. package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
  23. package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
  24. package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
  25. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
  26. package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
  27. package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
  28. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
  29. package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
  30. package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
  31. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
  32. package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
  33. package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
  34. package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
  35. package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
  36. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
  37. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
  38. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
  39. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
  40. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
  41. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
  42. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
  43. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
  44. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
  45. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
  46. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
  48. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
  49. package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
  50. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
  51. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
  52. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
  53. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
  54. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
  55. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
  56. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
  57. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
  58. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
  59. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
  60. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
  61. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
  62. package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
  63. package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
  64. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
  65. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
  66. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
  67. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
  68. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
  69. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
  70. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
  71. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
  72. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
  73. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
  74. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
  75. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
  76. package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
  77. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
  78. package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
  79. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  80. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
  81. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
  82. package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
  83. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
  84. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
  85. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
  86. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
  87. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
  88. package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
  89. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
  90. package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
  92. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
  93. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
  94. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
  95. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
  96. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
  97. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
  98. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
  99. package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
  100. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
  101. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
  102. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
  103. package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
  104. package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
  105. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
  106. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
  107. package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
  108. package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
  109. package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
  110. package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
  111. package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
  112. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
  113. package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
  114. package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
  115. package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
  116. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
  117. package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
  118. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
  119. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
  120. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
  121. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
  122. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
  123. package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
  124. package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
  125. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
  126. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
  127. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
  128. package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
  129. package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
  130. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
  131. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
  132. package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
  133. package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
  134. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
  135. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
  136. package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
  137. package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
  138. package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
  139. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
  140. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
  141. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
  142. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
  143. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
  144. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
  145. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
  146. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
  147. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
  148. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
  149. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
  150. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
  151. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
  152. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
  153. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
  154. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
  155. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
  156. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
  157. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
  158. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
  159. package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
  160. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
  161. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
  162. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
  163. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
  164. package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
  165. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
  166. package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
  167. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
  168. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
  169. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
  170. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
  171. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
  172. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
  173. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
  174. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
  175. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
  176. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
  177. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
  178. package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
  179. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
  180. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
  181. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
  182. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
  183. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
  184. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
  185. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
  186. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
  187. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
  188. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
  189. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
  190. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
  191. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
  192. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
  193. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
  194. package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
  195. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
  196. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
  197. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
  198. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
  199. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
  200. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
  201. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
  202. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
  203. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
  204. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
  205. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
  206. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
  207. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
  208. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
  209. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
  210. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
  211. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
  212. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
  213. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
  214. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
  215. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
  216. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
  217. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
  218. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
  219. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
  220. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
  221. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
  222. package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
  223. package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
  224. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
  225. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
  226. package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
  227. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
  228. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
  229. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
  230. package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
  231. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
  232. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
  233. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
  234. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
  235. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
  236. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
  237. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
  238. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
  239. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
  240. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
  241. package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
  242. package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
  243. package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
  244. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
  245. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
  246. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
  247. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
  248. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
  249. package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
  250. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
  251. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
  252. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
  253. package/dist/extensions/question/batch.ts +39 -0
  254. package/dist/extensions/question/dialog-adapter.ts +23 -1
  255. package/dist/extensions/question/index.ts +237 -7
  256. package/dist/extensions/question/schemas.ts +28 -15
  257. package/dist/extensions/question/tests/batch.test.ts +26 -0
  258. package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
  259. package/dist/extensions/question/tui-adapter.ts +2 -3
  260. package/dist/extensions/question/types.ts +37 -0
  261. package/dist/extensions/question/ui-protocol.ts +3 -3
  262. package/dist/skills/batch-grill-me/SKILL.md +3 -1
  263. package/package.json +1 -1
@@ -0,0 +1,387 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawnSync } from "node:child_process";
3
+ import * as fs from "node:fs";
4
+ import { createRequire, syncBuiltinESMExports } from "node:module";
5
+ import * as os from "node:os";
6
+ import * as path from "node:path";
7
+ import { describe, it } from "node:test";
8
+ import { computeWatchdogRepoChangeSignature } from "../../src/watchdog/change-signature.ts";
9
+
10
+ // The module under test hashes file content via `fs.readFileSync`, whose ESM
11
+ // binding cannot be reassigned (module namespaces are frozen) and mock.module is
12
+ // gated behind an experimental flag not passed by the runner. However, the ESM
13
+ // `readFileSync` dispatches through the CJS module's `openSync`, so patching
14
+ // `require("node:fs").openSync` deterministically forces the inner read error
15
+ // paths without touching lstat/readdir (which do not go through openSync).
16
+ const require = createRequire(import.meta.url);
17
+ const cjsFs = require("node:fs") as { openSync: typeof fs.openSync; lstatSync: typeof fs.lstatSync };
18
+
19
+ function withOpenSyncError<T>(code: string, run: () => T): T {
20
+ const previous = cjsFs.openSync;
21
+ cjsFs.openSync = () => {
22
+ throw Object.assign(new Error("forced read failure"), { code });
23
+ };
24
+ try {
25
+ return run();
26
+ } finally {
27
+ cjsFs.openSync = previous;
28
+ }
29
+ }
30
+
31
+ function withLstatSyncError<T>(code: string, run: () => T): T {
32
+ const previous = cjsFs.lstatSync;
33
+ cjsFs.lstatSync = () => {
34
+ throw Object.assign(new Error("forced stat failure"), { code });
35
+ };
36
+ syncBuiltinESMExports();
37
+ try {
38
+ return run();
39
+ } finally {
40
+ cjsFs.lstatSync = previous;
41
+ syncBuiltinESMExports();
42
+ }
43
+ }
44
+
45
+ function git(cwd: string, args: string[]): string {
46
+ const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8" });
47
+ if (result.status !== 0) throw new Error(result.stderr.trim() || result.stdout.trim() || `git ${args.join(" ")} failed`);
48
+ return result.stdout.trim();
49
+ }
50
+
51
+ function createRepo(prefix: string): string {
52
+ const repo = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
53
+ git(repo, ["init"]);
54
+ git(repo, ["config", "user.email", "watchdog@example.com"]);
55
+ git(repo, ["config", "user.name", "Watchdog Tests"]);
56
+ return repo;
57
+ }
58
+
59
+ describe("watchdog change signature", () => {
60
+ it("summarizes modified submodules as opaque Git worktrees", (t) => {
61
+ const childSource = createRepo("watchdog-child-");
62
+ const parent = createRepo("watchdog-parent-");
63
+ const checkout = path.join(parent, "vendor", "child");
64
+ const ignoredFile = path.join(checkout, "node_modules", "ignored.bin");
65
+ t.after(() => {
66
+ fs.rmSync(parent, { recursive: true, force: true });
67
+ fs.rmSync(childSource, { recursive: true, force: true });
68
+ });
69
+
70
+ fs.writeFileSync(path.join(childSource, ".gitignore"), "node_modules/\n", "utf-8");
71
+ fs.writeFileSync(path.join(childSource, "tracked.txt"), "one\n", "utf-8");
72
+ git(childSource, ["add", "-A"]);
73
+ git(childSource, ["commit", "-m", "initial"]);
74
+
75
+ git(parent, ["-c", "protocol.file.allow=always", "submodule", "add", childSource, "vendor/child"]);
76
+ git(parent, ["commit", "-am", "add submodule"]);
77
+ git(checkout, ["config", "user.email", "watchdog@example.com"]);
78
+ git(checkout, ["config", "user.name", "Watchdog Tests"]);
79
+
80
+ fs.mkdirSync(path.dirname(ignoredFile), { recursive: true });
81
+ fs.writeFileSync(ignoredFile, "ignored one\n", "utf-8");
82
+ fs.writeFileSync(path.join(checkout, "tracked.txt"), "two\n", "utf-8");
83
+
84
+ const first = computeWatchdogRepoChangeSignature(parent);
85
+ assert.deepEqual(first?.changedPaths, ["vendor/child"]);
86
+
87
+ fs.writeFileSync(ignoredFile, "ignored two\n", "utf-8");
88
+ const ignoredOnly = computeWatchdogRepoChangeSignature(parent);
89
+ assert.equal(ignoredOnly?.key, first?.key);
90
+
91
+ fs.writeFileSync(path.join(checkout, "tracked.txt"), "three\n", "utf-8");
92
+ const stillDirty = computeWatchdogRepoChangeSignature(parent);
93
+ assert.equal(stillDirty?.key, first?.key, "dirty submodule content is intentionally opaque");
94
+
95
+ git(checkout, ["add", "tracked.txt"]);
96
+ git(checkout, ["commit", "-m", "update tracked"]);
97
+ const headChange = computeWatchdogRepoChangeSignature(parent);
98
+ assert.notEqual(headChange?.key, first?.key, "submodule HEAD changes remain visible");
99
+ });
100
+
101
+ function setThreshold(bytes: number, t: { after: (fn: () => void) => void }): void {
102
+ const previous = process.env.SELESAI_SUBAGENTS_MAX_HASH_FILE_BYTES;
103
+ process.env.SELESAI_SUBAGENTS_MAX_HASH_FILE_BYTES = String(bytes);
104
+ t.after(() => {
105
+ if (previous === undefined) delete process.env.SELESAI_SUBAGENTS_MAX_HASH_FILE_BYTES;
106
+ else process.env.SELESAI_SUBAGENTS_MAX_HASH_FILE_BYTES = previous;
107
+ });
108
+ }
109
+
110
+ function setTotalThreshold(bytes: number, t: { after: (fn: () => void) => void }): void {
111
+ const previous = process.env.SELESAI_SUBAGENTS_MAX_HASH_TOTAL_BYTES;
112
+ process.env.SELESAI_SUBAGENTS_MAX_HASH_TOTAL_BYTES = String(bytes);
113
+ t.after(() => {
114
+ if (previous === undefined) delete process.env.SELESAI_SUBAGENTS_MAX_HASH_TOTAL_BYTES;
115
+ else process.env.SELESAI_SUBAGENTS_MAX_HASH_TOTAL_BYTES = previous;
116
+ });
117
+ }
118
+
119
+ function setEntryThreshold(entries: number, t: { after: (fn: () => void) => void }): void {
120
+ const previous = process.env.SELESAI_SUBAGENTS_MAX_HASH_ENTRIES;
121
+ process.env.SELESAI_SUBAGENTS_MAX_HASH_ENTRIES = String(entries);
122
+ t.after(() => {
123
+ if (previous === undefined) delete process.env.SELESAI_SUBAGENTS_MAX_HASH_ENTRIES;
124
+ else process.env.SELESAI_SUBAGENTS_MAX_HASH_ENTRIES = previous;
125
+ });
126
+ }
127
+
128
+ it("uses a metadata marker for files above the (lowered) threshold instead of hashing content", (t) => {
129
+ const repo = createRepo("watchdog-large-");
130
+ t.after(() => fs.rmSync(repo, { recursive: true, force: true }));
131
+ setThreshold(1024, t);
132
+
133
+ const filePath = path.join(repo, "big.txt");
134
+ fs.writeFileSync(filePath, Buffer.alloc(4096, 0x61)); // 4 KiB > 1 KiB threshold
135
+ const mtime = new Date(1_600_000_000_000);
136
+ fs.utimesSync(filePath, mtime, mtime);
137
+
138
+ const sig = computeWatchdogRepoChangeSignature(repo);
139
+ assert.ok(sig, "expected a signature");
140
+ assert.equal(typeof sig?.key, "string");
141
+ assert.ok(sig?.changedPaths.includes("big.txt"));
142
+
143
+ // The per-file hash is not exposed on the public signature, so prove the
144
+ // metadata marker (largeFileHash = "large:size:mtime") behaviorally: content
145
+ // is unchanged but the mtime changes, so a metadata-keyed hash yields a new
146
+ // key while a content SHA-256 (unfixed main) would produce the same key.
147
+ const laterMtime = new Date(1_600_000_100_000);
148
+ fs.utimesSync(filePath, laterMtime, laterMtime);
149
+ const afterMtime = computeWatchdogRepoChangeSignature(repo);
150
+ assert.notEqual(afterMtime?.key, sig?.key, "mtime-only change must alter the key for large files");
151
+ });
152
+
153
+ it("uses metadata markers after the total content hash budget", (t) => {
154
+ const repo = createRepo("watchdog-total-budget-");
155
+ t.after(() => fs.rmSync(repo, { recursive: true, force: true }));
156
+ setTotalThreshold(10, t);
157
+
158
+ const aPath = path.join(repo, "a.txt");
159
+ const bPath = path.join(repo, "b.txt");
160
+ const mtime = new Date(1_600_000_000_000);
161
+ fs.writeFileSync(aPath, "aaaaaaaa");
162
+ fs.writeFileSync(bPath, "bbbbbbbb");
163
+ fs.utimesSync(aPath, mtime, mtime);
164
+ fs.utimesSync(bPath, mtime, mtime);
165
+
166
+ const first = computeWatchdogRepoChangeSignature(repo);
167
+ assert.ok(first, "expected a signature");
168
+
169
+ fs.writeFileSync(bPath, "cccccccc");
170
+ fs.utimesSync(bPath, mtime, mtime);
171
+ assert.equal(computeWatchdogRepoChangeSignature(repo)?.key, first?.key, "over-budget files use size+mtime metadata instead of content");
172
+
173
+ fs.writeFileSync(aPath, "dddddddd");
174
+ fs.utimesSync(aPath, mtime, mtime);
175
+ assert.notEqual(computeWatchdogRepoChangeSignature(repo)?.key, first?.key, "within-budget files are still content hashed");
176
+ });
177
+
178
+ it("uses skipped markers after the entry budget", (t) => {
179
+ const repo = createRepo("watchdog-entry-budget-");
180
+ t.after(() => fs.rmSync(repo, { recursive: true, force: true }));
181
+ setEntryThreshold(3, t);
182
+
183
+ const dir = path.join(repo, "files");
184
+ fs.mkdirSync(dir);
185
+ for (const name of ["a.txt", "b.txt", "c.txt", "d.txt"]) {
186
+ fs.writeFileSync(path.join(dir, name), `${name}\n`, "utf-8");
187
+ }
188
+
189
+ const first = computeWatchdogRepoChangeSignature(repo);
190
+ assert.ok(first, "expected a signature");
191
+
192
+ fs.writeFileSync(path.join(dir, "d.txt"), "changed d\n", "utf-8");
193
+ assert.equal(computeWatchdogRepoChangeSignature(repo)?.key, first?.key, "over-budget entries are summarized instead of hashed individually");
194
+
195
+ fs.writeFileSync(path.join(dir, "a.txt"), "changed a\n", "utf-8");
196
+ assert.notEqual(computeWatchdogRepoChangeSignature(repo)?.key, first?.key, "within-budget entries are still content hashed");
197
+ });
198
+
199
+ it("produces deterministic keys keyed on size and mtime for large files", (t) => {
200
+ const repo = createRepo("watchdog-large-det-");
201
+ t.after(() => fs.rmSync(repo, { recursive: true, force: true }));
202
+ setThreshold(1024, t);
203
+
204
+ const filePath = path.join(repo, "big.txt");
205
+ fs.writeFileSync(filePath, Buffer.alloc(4096, 0x61));
206
+ const mtime = new Date(1_600_000_000_000);
207
+ fs.utimesSync(filePath, mtime, mtime);
208
+
209
+ const first = computeWatchdogRepoChangeSignature(repo);
210
+ const again = computeWatchdogRepoChangeSignature(repo);
211
+ assert.equal(again?.key, first?.key, "same size+mtime → same key");
212
+
213
+ const laterMtime = new Date(1_600_000_100_000);
214
+ fs.utimesSync(filePath, laterMtime, laterMtime);
215
+ const afterMtime = computeWatchdogRepoChangeSignature(repo);
216
+ assert.notEqual(afterMtime?.key, first?.key, "changed mtime → different key");
217
+
218
+ fs.writeFileSync(filePath, Buffer.alloc(8192, 0x61));
219
+ fs.utimesSync(filePath, laterMtime, laterMtime);
220
+ const afterSize = computeWatchdogRepoChangeSignature(repo);
221
+ assert.notEqual(afterSize?.key, afterMtime?.key, "changed size → different key");
222
+ });
223
+
224
+ it("handles a >2 GiB sparse file without throwing (default threshold)", { skip: process.platform === "win32" }, (t) => {
225
+ const repo = createRepo("watchdog-sparse-");
226
+ const filePath = path.join(repo, "huge.bin");
227
+ t.after(() => fs.rmSync(repo, { recursive: true, force: true }));
228
+
229
+ const fd = fs.openSync(filePath, "w");
230
+ try {
231
+ fs.ftruncateSync(fd, 2 * 1024 ** 3 + 1);
232
+ } finally {
233
+ fs.closeSync(fd);
234
+ }
235
+
236
+ const sig = computeWatchdogRepoChangeSignature(repo);
237
+ assert.ok(sig, "expected a signature");
238
+ assert.ok(sig?.changedPaths.includes("huge.bin"));
239
+ });
240
+
241
+ it("ignores node_modules below changed directories", (t) => {
242
+ const repo = createRepo("watchdog-nested-ignored-");
243
+ t.after(() => fs.rmSync(repo, { recursive: true, force: true }));
244
+
245
+ const subdir = path.join(repo, "nested");
246
+ fs.mkdirSync(path.join(subdir, "node_modules"), { recursive: true });
247
+ fs.writeFileSync(path.join(subdir, "keep.txt"), "one\n", "utf-8");
248
+ fs.writeFileSync(path.join(subdir, "node_modules", "cache.bin"), "ignored one\n", "utf-8");
249
+
250
+ const first = computeWatchdogRepoChangeSignature(repo);
251
+ assert.ok(first?.changedPaths.includes("nested/keep.txt"));
252
+ assert.ok(!first?.changedPaths.some((relPath) => relPath.includes("node_modules")));
253
+
254
+ fs.writeFileSync(path.join(subdir, "node_modules", "cache.bin"), "ignored two\n", "utf-8");
255
+ assert.equal(computeWatchdogRepoChangeSignature(repo)?.key, first?.key);
256
+
257
+ fs.writeFileSync(path.join(subdir, "keep.txt"), "two\n", "utf-8");
258
+ assert.notEqual(computeWatchdogRepoChangeSignature(repo)?.key, first?.key);
259
+ });
260
+
261
+ it("applies the threshold guard recursively into untracked subdirectories", (t) => {
262
+ const repo = createRepo("watchdog-recurse-");
263
+ t.after(() => fs.rmSync(repo, { recursive: true, force: true }));
264
+ setThreshold(1024, t);
265
+
266
+ const subdir = path.join(repo, "nested");
267
+ fs.mkdirSync(subdir, { recursive: true });
268
+ const filePath = path.join(subdir, "big.txt");
269
+ fs.writeFileSync(filePath, Buffer.alloc(4096, 0x62));
270
+ const mtime = new Date(1_600_000_000_000);
271
+ fs.utimesSync(filePath, mtime, mtime);
272
+
273
+ const sig = computeWatchdogRepoChangeSignature(repo);
274
+ assert.ok(sig, "expected a signature");
275
+ assert.ok(sig?.changedPaths.includes("nested/big.txt"));
276
+
277
+ // Same behavioral proof as above, for a file nested in a subdirectory.
278
+ const laterMtime = new Date(1_600_000_100_000);
279
+ fs.utimesSync(filePath, laterMtime, laterMtime);
280
+ const afterMtime = computeWatchdogRepoChangeSignature(repo);
281
+ assert.notEqual(afterMtime?.key, sig?.key, "mtime-only change must alter the key for large files in subdirs");
282
+ });
283
+
284
+ it("degrades to the metadata marker when a per-file read raises ERR_FS_FILE_TOO_LARGE", (t) => {
285
+ const repo = createRepo("watchdog-inner-toolarge-");
286
+ t.after(() => fs.rmSync(repo, { recursive: true, force: true }));
287
+
288
+ // Small file (default threshold) so it enters the content-hash try branch.
289
+ fs.writeFileSync(path.join(repo, "small.txt"), "hello\n", "utf-8");
290
+
291
+ const warnings: string[] = [];
292
+ const previousWarn = console.warn;
293
+ console.warn = (...args: unknown[]) => void warnings.push(args.join(" "));
294
+ let sig: ReturnType<typeof computeWatchdogRepoChangeSignature>;
295
+ try {
296
+ sig = withOpenSyncError("ERR_FS_FILE_TOO_LARGE", () => computeWatchdogRepoChangeSignature(repo));
297
+ } finally {
298
+ console.warn = previousWarn;
299
+ }
300
+
301
+ assert.ok(sig, "a single unreadable file must not discard the whole signature");
302
+ assert.equal(typeof sig?.key, "string");
303
+ assert.ok(sig?.changedPaths.includes("small.txt"));
304
+ assert.deepEqual(warnings, [], "the too-large case is expected and must stay quiet");
305
+ });
306
+
307
+ it("keeps a valid signature when a per-file read races into ENOENT", (t) => {
308
+ const repo = createRepo("watchdog-inner-enoent-");
309
+ t.after(() => fs.rmSync(repo, { recursive: true, force: true }));
310
+
311
+ fs.writeFileSync(path.join(repo, "small.txt"), "hello\n", "utf-8");
312
+
313
+ const sig = withOpenSyncError("ENOENT", () => computeWatchdogRepoChangeSignature(repo));
314
+ assert.ok(sig, "an ENOENT during read must degrade to the deleted marker, not undefined");
315
+ assert.equal(typeof sig?.key, "string");
316
+ assert.ok(sig?.changedPaths.includes("small.txt"));
317
+ });
318
+
319
+ it("warns and falls back to metadata for other per-file read errors", (t) => {
320
+ const repo = createRepo("watchdog-inner-eacces-");
321
+ t.after(() => fs.rmSync(repo, { recursive: true, force: true }));
322
+
323
+ fs.writeFileSync(path.join(repo, "small.txt"), "hello\n", "utf-8");
324
+
325
+ const warnings: string[] = [];
326
+ const previousWarn = console.warn;
327
+ console.warn = (...args: unknown[]) => void warnings.push(args.join(" "));
328
+ let sig: ReturnType<typeof computeWatchdogRepoChangeSignature>;
329
+ try {
330
+ sig = withOpenSyncError("EACCES", () => computeWatchdogRepoChangeSignature(repo));
331
+ } finally {
332
+ console.warn = previousWarn;
333
+ }
334
+
335
+ assert.ok(sig, "a single unreadable file must not discard the whole signature");
336
+ assert.equal(typeof sig?.key, "string");
337
+ assert.ok(warnings.some((line) => line.includes("hashFile fell back to metadata") && line.includes("small.txt")));
338
+ });
339
+
340
+ it("returns undefined instead of throwing when hashing a changed path raises", (t) => {
341
+ const repo = createRepo("watchdog-outer-");
342
+ t.after(() => fs.rmSync(repo, { recursive: true, force: true }));
343
+
344
+ // Use a deterministic stat failure to exercise the outer fail-safe on every
345
+ // platform without depending on POSIX permissions or ENOTDIR/ENOENT differences
346
+ // between Unix and Windows.
347
+ fs.writeFileSync(path.join(repo, "x.txt"), "changed\n", "utf-8");
348
+
349
+ const warnings: string[] = [];
350
+ const previousWarn = console.warn;
351
+ console.warn = (...args: unknown[]) => void warnings.push(args.join(" "));
352
+ let result: ReturnType<typeof computeWatchdogRepoChangeSignature>;
353
+ try {
354
+ assert.doesNotThrow(() => {
355
+ result = withLstatSyncError("EIO", () => computeWatchdogRepoChangeSignature(repo));
356
+ });
357
+ } finally {
358
+ console.warn = previousWarn;
359
+ }
360
+
361
+ assert.equal(result, undefined, "a throw in the hashing region must fail safe to undefined");
362
+ });
363
+
364
+ it("still hashes small files by content when under the threshold", (t) => {
365
+ const repo = createRepo("watchdog-small-");
366
+ t.after(() => fs.rmSync(repo, { recursive: true, force: true }));
367
+
368
+ const filePath = path.join(repo, "small.txt");
369
+ fs.writeFileSync(filePath, "hello\n", "utf-8");
370
+ const mtime = new Date(1_600_000_000_000);
371
+ fs.utimesSync(filePath, mtime, mtime);
372
+
373
+ const sig = computeWatchdogRepoChangeSignature(repo);
374
+ assert.ok(sig, "expected a signature");
375
+ assert.ok(sig?.changedPaths.includes("small.txt"));
376
+
377
+ // Content-hashed files ignore mtime: an mtime-only change must NOT alter the
378
+ // key (would fail if the metadata marker leaked into the normal path). A
379
+ // content change must alter it.
380
+ const laterMtime = new Date(1_600_000_100_000);
381
+ fs.utimesSync(filePath, laterMtime, laterMtime);
382
+ assert.equal(computeWatchdogRepoChangeSignature(repo)?.key, sig?.key, "mtime-only change must not alter key for small files");
383
+
384
+ fs.writeFileSync(filePath, "changed\n", "utf-8");
385
+ assert.notEqual(computeWatchdogRepoChangeSignature(repo)?.key, sig?.key, "content change must alter key");
386
+ });
387
+ });
@@ -0,0 +1,110 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ CHILD_WATCHDOG_STATUS_EVENT,
5
+ acceptChildWatchdogEvent,
6
+ childWatchdogIsActive,
7
+ decodeChildWatchdogConfig,
8
+ isChildWatchdogStatusEvent,
9
+ resolveChildWatchdogConfig,
10
+ } from "../../src/watchdog/child-status.ts";
11
+ import { DEFAULT_WATCHDOG_CONFIG } from "../../src/watchdog/settings.ts";
12
+
13
+ describe("child watchdog status helpers", () => {
14
+ it("preserves child model and explicit override thinking when resolving config", () => {
15
+ const config = resolveChildWatchdogConfig({
16
+ config: {
17
+ ...DEFAULT_WATCHDOG_CONFIG,
18
+ enabled: true,
19
+ children: {
20
+ ...DEFAULT_WATCHDOG_CONFIG.children,
21
+ enabled: true,
22
+ model: "openai/gpt-test-child",
23
+ thinking: "low",
24
+ overrides: {
25
+ worker: {
26
+ model: "anthropic/claude-test-worker",
27
+ thinking: false,
28
+ },
29
+ },
30
+ },
31
+ },
32
+ agent: "worker",
33
+ runId: "run-1",
34
+ childIndex: 0,
35
+ });
36
+
37
+ assert.equal(config?.model, "anthropic/claude-test-worker");
38
+ assert.equal(config?.thinking, false);
39
+ assert.deepEqual(config?.lsp, DEFAULT_WATCHDOG_CONFIG.lsp);
40
+ });
41
+
42
+ it("decodes child watchdog config and rejects malformed enabled payloads", () => {
43
+ const payload = {
44
+ enabled: true,
45
+ runId: "run-1",
46
+ agent: "worker",
47
+ childIndex: 1,
48
+ watchdogTailTimeoutMs: 100,
49
+ agentEndTimeoutMs: 200,
50
+ maxWarnings: null,
51
+ lsp: { enabled: false, timeoutMs: 50, maxFiles: 2, maxDiagnostics: 3 },
52
+ autoFollowBlockers: true,
53
+ autoFollowMaxAttempts: 2,
54
+ stalemateRepeats: 3,
55
+ };
56
+ const config = decodeChildWatchdogConfig(JSON.stringify(payload));
57
+
58
+ assert.deepEqual(config, payload);
59
+ assert.equal(decodeChildWatchdogConfig(JSON.stringify({ enabled: false })), undefined);
60
+ assert.throws(
61
+ () => decodeChildWatchdogConfig(JSON.stringify({ ...payload, lsp: { ...payload.lsp, timeoutMs: 0 } })),
62
+ /lsp\.timeoutMs/,
63
+ );
64
+ assert.throws(
65
+ () => decodeChildWatchdogConfig(JSON.stringify({ ...payload, autoFollowBlockers: undefined })),
66
+ /autoFollowBlockers/,
67
+ );
68
+ assert.throws(
69
+ () => decodeChildWatchdogConfig(JSON.stringify({ ...payload, autoFollowBlockers: "yes" })),
70
+ /autoFollowBlockers/,
71
+ );
72
+ assert.throws(
73
+ () => decodeChildWatchdogConfig(JSON.stringify({ ...payload, stalemateRepeats: 0 })),
74
+ /stalemateRepeats/,
75
+ );
76
+ });
77
+
78
+ it("accepts latest matching status events and drops stale or foreign events", () => {
79
+ const firstEvent = {
80
+ type: CHILD_WATCHDOG_STATUS_EVENT,
81
+ runId: "run-1",
82
+ agent: "worker",
83
+ childIndex: 0,
84
+ seq: 1,
85
+ phase: "reviewing",
86
+ ts: 10,
87
+ followUpPending: false,
88
+ } as const;
89
+
90
+ assert.equal(isChildWatchdogStatusEvent(firstEvent), true);
91
+ const first = acceptChildWatchdogEvent({ event: firstEvent, runId: "run-1", agent: "worker", childIndex: 0, current: undefined });
92
+ assert.deepEqual(first, { phase: "reviewing", seq: 1, lastUpdate: 10, followUpPending: false });
93
+ assert.equal(childWatchdogIsActive(first), true);
94
+ assert.equal(acceptChildWatchdogEvent({ event: firstEvent, current: first, runId: "run-1", agent: "worker", childIndex: 0 }), undefined);
95
+ assert.equal(acceptChildWatchdogEvent({ event: { ...firstEvent, seq: 2, runId: undefined }, current: first, runId: "run-1", agent: "worker", childIndex: 0 }), undefined);
96
+ assert.equal(acceptChildWatchdogEvent({ event: { ...firstEvent, seq: 2, agent: undefined }, current: first, runId: "run-1", agent: "worker", childIndex: 0 }), undefined);
97
+ assert.equal(acceptChildWatchdogEvent({ event: { ...firstEvent, seq: 2, childIndex: undefined }, current: first, runId: "run-1", agent: "worker", childIndex: 0 }), undefined);
98
+ assert.equal(acceptChildWatchdogEvent({ event: { ...firstEvent, seq: 2, agent: "other" }, current: first, runId: "run-1", agent: "worker", childIndex: 0 }), undefined);
99
+
100
+ const settled = acceptChildWatchdogEvent({
101
+ event: { ...firstEvent, seq: 2, phase: "idle", ts: 20 },
102
+ current: first,
103
+ runId: "run-1",
104
+ agent: "worker",
105
+ childIndex: 0,
106
+ });
107
+ assert.equal(settled?.phase, "idle");
108
+ assert.equal(childWatchdogIsActive(settled), false);
109
+ });
110
+ });
@@ -0,0 +1,62 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { WatchdogEmissionGuard, normalizeWatchdogEmissionText } from "../../src/watchdog/emission-guard.ts";
4
+ import type { WatchdogWarning } from "../../src/watchdog/types.ts";
5
+
6
+ function warning(overrides: Partial<WatchdogWarning> = {}): WatchdogWarning {
7
+ return {
8
+ severity: "concern",
9
+ summary: "Unvalidated file write",
10
+ evidence: "The implementation writes settings without a test.",
11
+ recommendedAction: "Add a focused settings write regression test.",
12
+ ...overrides,
13
+ };
14
+ }
15
+
16
+ describe("watchdog emission guard", () => {
17
+ it("normalizes text with NFKC, punctuation folding, lowercase, and whitespace collapse", () => {
18
+ assert.equal(normalizeWatchdogEmissionText(" Fix—THE\tBug!!! "), "fix the bug");
19
+ });
20
+
21
+ it("suppresses content-free and empty emissions", () => {
22
+ assert.deepEqual(new WatchdogEmissionGuard().evaluate(warning({ summary: "No concerns." })), { accepted: false, reason: "content-free" });
23
+ assert.deepEqual(new WatchdogEmissionGuard().evaluate(warning({ evidence: "LGTM" })), { accepted: false, reason: "content-free" });
24
+ assert.deepEqual(new WatchdogEmissionGuard().evaluate(warning({ recommendedAction: "" })), { accepted: false, reason: "content-free" });
25
+ });
26
+
27
+ it("dedupes normalized repeated warnings across model updates", () => {
28
+ const guard = new WatchdogEmissionGuard();
29
+ assert.equal(guard.evaluate(warning({ summary: "Fix Foo!!!", evidence: "Line 1: bad whitespace." })).accepted, true);
30
+ guard.startModelUpdate();
31
+ const duplicate = guard.evaluate(warning({ summary: "fix foo", evidence: "line 1 bad whitespace" }));
32
+
33
+ assert.equal(duplicate.accepted, false);
34
+ assert.equal(duplicate.reason, "duplicate");
35
+ });
36
+
37
+ it("allows concern to blocker escalation for the same issue despite per-update budget", () => {
38
+ const guard = new WatchdogEmissionGuard();
39
+ const first = guard.evaluate(warning({ severity: "concern", summary: "Tests missing", evidence: "No unit covers the parser." }));
40
+ const escalation = guard.evaluate(warning({ severity: "blocker", summary: "Tests missing", evidence: "No unit covers the parser." }));
41
+ const extra = guard.evaluate(warning({ severity: "blocker", summary: "Docs missing", evidence: "No README section exists." }));
42
+
43
+ assert.equal(first.accepted, true);
44
+ assert.equal(escalation.accepted, true);
45
+ assert.equal(escalation.escalation, true);
46
+ assert.equal(extra.accepted, false);
47
+ assert.equal(extra.reason, "update-budget");
48
+ });
49
+
50
+ it("enforces max warnings while still allowing escalation of an accepted concern", () => {
51
+ const guard = new WatchdogEmissionGuard({ maxWarnings: 1 });
52
+ assert.equal(guard.evaluate(warning({ severity: "concern", summary: "Parser gap", evidence: "Unknown fields are ignored." })).accepted, true);
53
+ guard.startModelUpdate();
54
+ const capped = guard.evaluate(warning({ severity: "concern", summary: "Renderer gap", evidence: "Stale state is hidden." }));
55
+ const escalation = guard.evaluate(warning({ severity: "blocker", summary: "Parser gap", evidence: "Unknown fields are ignored." }));
56
+
57
+ assert.equal(capped.accepted, false);
58
+ assert.equal(capped.reason, "max-warnings");
59
+ assert.equal(escalation.accepted, true);
60
+ assert.equal(escalation.escalation, true);
61
+ });
62
+ });