@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,220 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+
6
+ const IGNORED_CHANGE_PREFIXES = [".pi-subagents/", "tmp/", "node_modules/"];
7
+ const IGNORED_CHANGE_PATHS = new Set([".pi-subagents", "tmp", "node_modules"]);
8
+ const IGNORED_CHANGE_SEGMENTS = new Set([".git", ".pi-subagents", "node_modules"]);
9
+
10
+ const DEFAULT_MAX_HASH_FILE_BYTES = 64 * 1024 * 1024;
11
+ const DEFAULT_MAX_HASH_TOTAL_BYTES = 64 * 1024 * 1024;
12
+ const DEFAULT_MAX_HASH_ENTRIES = 2_000;
13
+
14
+ function positiveEnvNumber(name: string, fallback: number): number {
15
+ const parsed = Number(process.env[name]);
16
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
17
+ }
18
+
19
+ // Read at call time (not module load) so tests can override env guards after
20
+ // this module is imported.
21
+ function maxHashFileBytes(): number {
22
+ return positiveEnvNumber("SELESAI_SUBAGENTS_MAX_HASH_FILE_BYTES", DEFAULT_MAX_HASH_FILE_BYTES);
23
+ }
24
+
25
+ function maxHashTotalBytes(): number {
26
+ return positiveEnvNumber("SELESAI_SUBAGENTS_MAX_HASH_TOTAL_BYTES", DEFAULT_MAX_HASH_TOTAL_BYTES);
27
+ }
28
+
29
+ function maxHashEntries(): number {
30
+ return positiveEnvNumber("SELESAI_SUBAGENTS_MAX_HASH_ENTRIES", DEFAULT_MAX_HASH_ENTRIES);
31
+ }
32
+
33
+ export interface WatchdogRepoChangeSignature {
34
+ root: string;
35
+ key: string;
36
+ changedPaths: string[];
37
+ }
38
+
39
+ function git(cwd: string, args: string[]): string | undefined {
40
+ const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
41
+ if (result.status !== 0) return undefined;
42
+ return result.stdout;
43
+ }
44
+
45
+ function normalizeRelPath(value: string): string {
46
+ return value.replaceAll(path.sep, "/").replace(/^\.\//, "");
47
+ }
48
+
49
+ function ignoredRelPath(relPath: string): boolean {
50
+ const normalized = normalizeRelPath(relPath);
51
+ return IGNORED_CHANGE_PATHS.has(normalized)
52
+ || IGNORED_CHANGE_PREFIXES.some((prefix) => normalized.startsWith(prefix))
53
+ || normalized.split("/").some((segment) => IGNORED_CHANGE_SEGMENTS.has(segment));
54
+ }
55
+
56
+ interface HashBudget {
57
+ entries: number;
58
+ bytes: number;
59
+ maxEntries: number;
60
+ maxBytes: number;
61
+ }
62
+
63
+ function useHashEntryBudget(budget: HashBudget): boolean {
64
+ if (budget.entries >= budget.maxEntries) return false;
65
+ budget.entries++;
66
+ return true;
67
+ }
68
+
69
+ function hashFile(filePath: string): string {
70
+ return createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
71
+ }
72
+
73
+ function largeFileHash(stat: fs.Stats): string {
74
+ return "large:" + stat.size + ":" + Math.floor(stat.mtimeMs);
75
+ }
76
+
77
+ function hashFileEntry(normalized: string, fullPath: string, stat: fs.Stats, budget: HashBudget): unknown {
78
+ let hash: string;
79
+ if (stat.size > maxHashFileBytes() || budget.bytes + stat.size > budget.maxBytes) {
80
+ hash = largeFileHash(stat);
81
+ } else {
82
+ try {
83
+ hash = hashFile(fullPath);
84
+ budget.bytes += stat.size;
85
+ } catch (error) {
86
+ const code = (error as NodeJS.ErrnoException).code;
87
+ // A file racing away between lstat and read: mirror the lstat ENOENT path.
88
+ if (code === "ENOENT") return { path: normalized, state: "deleted" };
89
+ // Any other read failure (too-large, EACCES, EISDIR, ...) degrades to the
90
+ // metadata marker so one unreadable file never discards the whole signature.
91
+ hash = largeFileHash(stat);
92
+ if (code !== "ERR_FS_FILE_TOO_LARGE") {
93
+ console.warn("[pi-subagents] watchdog hashFile fell back to metadata for", normalized + ":", (error as Error)?.message);
94
+ }
95
+ }
96
+ }
97
+ return { path: normalized, state: "file", mode: stat.mode & 0o777, size: stat.size, hash };
98
+ }
99
+
100
+ function gitWorktreeEntry(normalized: string, fullPath: string): unknown {
101
+ const status = git(fullPath, ["status", "--porcelain=v1", "-z", "--untracked-files=no"]);
102
+ return {
103
+ path: normalized,
104
+ state: "git-worktree",
105
+ head: git(fullPath, ["rev-parse", "HEAD"])?.trim(),
106
+ dirty: Boolean(status),
107
+ statusKey: status ? createHash("sha256").update(status).digest("hex") : undefined,
108
+ };
109
+ }
110
+
111
+ function hashPath(root: string, relPath: string, budget: HashBudget): unknown {
112
+ const normalized = normalizeRelPath(relPath);
113
+ if (!useHashEntryBudget(budget)) return { path: normalized, state: "skipped", reason: "entry-limit" };
114
+ const fullPath = path.join(root, normalized);
115
+ let stat: fs.Stats;
116
+ try {
117
+ stat = fs.lstatSync(fullPath);
118
+ } catch (error) {
119
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return { path: normalized, state: "deleted" };
120
+ throw error;
121
+ }
122
+ if (stat.isSymbolicLink()) {
123
+ return { path: normalized, state: "symlink", target: fs.readlinkSync(fullPath) };
124
+ }
125
+ if (stat.isDirectory()) {
126
+ if (fs.existsSync(path.join(fullPath, ".git"))) return gitWorktreeEntry(normalized, fullPath);
127
+ const entries = fs.readdirSync(fullPath)
128
+ .map((entry) => normalizeRelPath(path.posix.join(normalized, entry)))
129
+ .filter((entry) => !ignoredRelPath(entry))
130
+ .sort();
131
+ const remainingEntries = Math.max(0, budget.maxEntries - budget.entries);
132
+ const selectedEntries = entries.slice(0, remainingEntries);
133
+ const childEntries = selectedEntries.map((entry) => hashPath(root, entry, budget));
134
+ if (selectedEntries.length < entries.length) {
135
+ childEntries.push({ path: normalized, state: "skipped-children", reason: "entry-limit", count: entries.length - selectedEntries.length });
136
+ }
137
+ return { path: normalized, state: "dir", entries: childEntries };
138
+ }
139
+ if (stat.isFile()) return hashFileEntry(normalized, fullPath, stat, budget);
140
+ return { path: normalized, state: "other", mode: stat.mode };
141
+ }
142
+
143
+ function parsePorcelainZ(raw: string): Array<{ status: string; paths: string[] }> {
144
+ const tokens = raw.split("\0").filter(Boolean);
145
+ const entries: Array<{ status: string; paths: string[] }> = [];
146
+ for (let index = 0; index < tokens.length; index++) {
147
+ const token = tokens[index]!;
148
+ if (token.length < 4) continue;
149
+ const status = token.slice(0, 2);
150
+ const relPath = token.slice(3);
151
+ const paths = [relPath];
152
+ if (status[0] === "R" || status[0] === "C") {
153
+ const originalPath = tokens[++index];
154
+ if (originalPath) paths.push(originalPath);
155
+ }
156
+ entries.push({ status, paths });
157
+ }
158
+ return entries;
159
+ }
160
+
161
+ function buildRepoChangeSignature(root: string, statusOutput: string): WatchdogRepoChangeSignature {
162
+ const entries = parsePorcelainZ(statusOutput)
163
+ .map((entry) => ({
164
+ status: entry.status,
165
+ paths: entry.paths.map(normalizeRelPath).filter((relPath) => !ignoredRelPath(relPath)),
166
+ }))
167
+ .filter((entry) => entry.paths.length > 0)
168
+ .sort((a, b) => `${a.status} ${a.paths.join("\0")}`.localeCompare(`${b.status} ${b.paths.join("\0")}`));
169
+ const changedPaths = [...new Set(entries.flatMap((entry) => entry.paths))].sort();
170
+ const budget: HashBudget = { entries: 0, bytes: 0, maxEntries: maxHashEntries(), maxBytes: maxHashTotalBytes() };
171
+ const payload = entries.map((entry) => ({
172
+ status: entry.status,
173
+ paths: entry.paths,
174
+ content: entry.paths.map((relPath) => hashPath(root, relPath, budget)),
175
+ }));
176
+ const key = createHash("sha256").update(JSON.stringify(payload)).digest("hex");
177
+ return { root, key, changedPaths };
178
+ }
179
+
180
+ export function computeWatchdogRepoChangeSignature(cwd: string): WatchdogRepoChangeSignature | undefined {
181
+ const root = git(cwd, ["rev-parse", "--show-toplevel"])?.trim();
182
+ if (!root) return undefined;
183
+ const statusOutput = git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
184
+ if (statusOutput === undefined) return undefined;
185
+ try {
186
+ return buildRepoChangeSignature(root, statusOutput);
187
+ } catch (error) {
188
+ console.warn("[pi-subagents] watchdog repo change signature failed:", (error as Error)?.message);
189
+ return undefined;
190
+ }
191
+ }
192
+
193
+ function toolNameFromMessage(message: Record<string, unknown>): string {
194
+ const value = message.toolName ?? message.name;
195
+ return typeof value === "string" ? value : "";
196
+ }
197
+
198
+ function toolResultSucceeded(message: Record<string, unknown>): boolean {
199
+ return message.isError !== true && message.error === undefined;
200
+ }
201
+
202
+ function messageIndicatesRepoEdit(message: unknown): boolean {
203
+ if (!message || typeof message !== "object") return false;
204
+ const input = message as Record<string, unknown>;
205
+ const role = input.role;
206
+ if (role !== "toolResult" && role !== "tool") return false;
207
+ const toolName = toolNameFromMessage(input);
208
+ return (toolName === "edit" || toolName === "write") && toolResultSucceeded(input);
209
+ }
210
+
211
+ export function eventIndicatesRepoEdit(event: unknown): boolean {
212
+ if (!event || typeof event !== "object") return false;
213
+ const input = event as Record<string, unknown>;
214
+ if (input.type === "turn_end" || input.event === "turn_end") {
215
+ return [input.message, ...(Array.isArray(input.toolResults) ? input.toolResults : [])].some(messageIndicatesRepoEdit);
216
+ }
217
+ if (input.type === "tool_result" || input.event === "tool_result") return messageIndicatesRepoEdit({ role: "toolResult", ...input });
218
+ if (input.type !== "tool_result_end" && input.event !== "tool_result_end") return false;
219
+ return messageIndicatesRepoEdit(input.message);
220
+ }
@@ -0,0 +1,205 @@
1
+ import type { ResolvedWatchdogConfig, WatchdogLspConfig } from "./types.ts";
2
+
3
+ export const CHILD_WATCHDOG_CONFIG_ENV = "SELESAI_SUBAGENT_WATCHDOG_CHILD_CONFIG";
4
+ export const CHILD_WATCHDOG_STATUS_EVENT = "subagent.watchdog.status";
5
+
6
+ export const CHILD_WATCHDOG_PHASES = ["idle", "reviewing", "autofollow", "settling", "stale", "failed"] as const;
7
+ export type ChildWatchdogPhase = typeof CHILD_WATCHDOG_PHASES[number];
8
+
9
+ export interface ChildWatchdogConfig {
10
+ enabled: boolean;
11
+ runId?: string;
12
+ agent?: string;
13
+ childIndex?: number;
14
+ watchdogTailTimeoutMs: number;
15
+ agentEndTimeoutMs: number;
16
+ maxWarnings: number | null;
17
+ model?: string;
18
+ thinking?: string | false;
19
+ lsp: WatchdogLspConfig;
20
+ autoFollowBlockers: boolean;
21
+ autoFollowMaxAttempts: number | null;
22
+ stalemateRepeats: number;
23
+ }
24
+
25
+ export interface ChildWatchdogStatusEvent {
26
+ type: typeof CHILD_WATCHDOG_STATUS_EVENT;
27
+ runId?: string;
28
+ agent?: string;
29
+ childIndex?: number;
30
+ stepIndex?: number;
31
+ seq: number;
32
+ phase: ChildWatchdogPhase;
33
+ ts: number;
34
+ followUpPending: boolean;
35
+ reason?: string;
36
+ }
37
+
38
+ export interface ChildWatchdogStateSnapshot {
39
+ phase: ChildWatchdogPhase;
40
+ seq: number;
41
+ lastUpdate: number;
42
+ followUpPending: boolean;
43
+ reason?: string;
44
+ timedOut?: boolean;
45
+ }
46
+
47
+ export function resolveChildWatchdogConfig(input: {
48
+ config: ResolvedWatchdogConfig;
49
+ agent?: string;
50
+ runId?: string;
51
+ childIndex?: number;
52
+ }): ChildWatchdogConfig | undefined {
53
+ const override = input.agent ? input.config.children.overrides[input.agent] : undefined;
54
+ const enabled = input.config.enabled && (override?.enabled ?? input.config.children.enabled);
55
+ if (!enabled) return undefined;
56
+ const model = override?.model ?? input.config.children.model;
57
+ const thinking = override?.thinking ?? input.config.children.thinking;
58
+ return {
59
+ enabled: true,
60
+ ...(input.runId ? { runId: input.runId } : {}),
61
+ ...(input.agent ? { agent: input.agent } : {}),
62
+ ...(input.childIndex !== undefined ? { childIndex: input.childIndex } : {}),
63
+ watchdogTailTimeoutMs: input.config.children.watchdogTailTimeoutMs,
64
+ agentEndTimeoutMs: input.config.agentEndTimeoutMs,
65
+ maxWarnings: input.config.maxWarnings,
66
+ ...(model ? { model } : {}),
67
+ ...(thinking !== undefined ? { thinking } : {}),
68
+ lsp: { ...input.config.lsp },
69
+ autoFollowBlockers: input.config.children.autoFollow.blockers,
70
+ autoFollowMaxAttempts: input.config.children.autoFollow.maxAttempts,
71
+ stalemateRepeats: input.config.children.autoFollow.stalemateRepeats,
72
+ };
73
+ }
74
+
75
+ export function encodeChildWatchdogConfig(config: ChildWatchdogConfig | undefined): string | undefined {
76
+ return config ? JSON.stringify(config) : undefined;
77
+ }
78
+
79
+ function childConfigObject(value: unknown, field: string): Record<string, unknown> {
80
+ if (value && typeof value === "object" && !Array.isArray(value)) return value as Record<string, unknown>;
81
+ throw new Error(`Invalid child watchdog config: ${field} must be an object.`);
82
+ }
83
+
84
+ function childConfigOptionalString(input: Record<string, unknown>, field: string): string | undefined {
85
+ if (!(field in input)) return undefined;
86
+ const value = input[field];
87
+ if (typeof value === "string" && value.trim()) return value;
88
+ throw new Error(`Invalid child watchdog config: ${field} must be a non-empty string.`);
89
+ }
90
+
91
+ function childConfigOptionalIndex(input: Record<string, unknown>, field: string): number | undefined {
92
+ if (!(field in input)) return undefined;
93
+ const value = input[field];
94
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
95
+ throw new Error(`Invalid child watchdog config: ${field} must be a non-negative integer.`);
96
+ }
97
+
98
+ function childConfigPositiveInteger(input: Record<string, unknown>, field: string): number {
99
+ const value = input[field];
100
+ if (typeof value === "number" && Number.isInteger(value) && value >= 1) return value;
101
+ throw new Error(`Invalid child watchdog config: ${field} must be a positive integer.`);
102
+ }
103
+
104
+ function childConfigNullableNonNegativeInteger(input: Record<string, unknown>, field: string): number | null {
105
+ const value = input[field];
106
+ if (value === null) return null;
107
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
108
+ throw new Error(`Invalid child watchdog config: ${field} must be null or a non-negative integer.`);
109
+ }
110
+
111
+ function childConfigBoolean(input: Record<string, unknown>, field: string): boolean {
112
+ const value = input[field];
113
+ if (typeof value === "boolean") return value;
114
+ throw new Error(`Invalid child watchdog config: ${field} must be a boolean.`);
115
+ }
116
+
117
+ function childConfigLsp(value: unknown): WatchdogLspConfig {
118
+ const input = childConfigObject(value, "lsp");
119
+ if (typeof input.enabled !== "boolean") throw new Error("Invalid child watchdog config: lsp.enabled must be a boolean.");
120
+ if (typeof input.timeoutMs !== "number" || !Number.isInteger(input.timeoutMs) || input.timeoutMs < 1) {
121
+ throw new Error("Invalid child watchdog config: lsp.timeoutMs must be a positive integer.");
122
+ }
123
+ if (typeof input.maxFiles !== "number" || !Number.isInteger(input.maxFiles) || input.maxFiles < 1) {
124
+ throw new Error("Invalid child watchdog config: lsp.maxFiles must be a positive integer.");
125
+ }
126
+ if (typeof input.maxDiagnostics !== "number" || !Number.isInteger(input.maxDiagnostics) || input.maxDiagnostics < 0) {
127
+ throw new Error("Invalid child watchdog config: lsp.maxDiagnostics must be a non-negative integer.");
128
+ }
129
+ return {
130
+ enabled: input.enabled,
131
+ timeoutMs: input.timeoutMs,
132
+ maxFiles: input.maxFiles,
133
+ maxDiagnostics: input.maxDiagnostics,
134
+ };
135
+ }
136
+
137
+ export function decodeChildWatchdogConfig(raw: string | undefined): ChildWatchdogConfig | undefined {
138
+ if (!raw) return undefined;
139
+ const parsed = childConfigObject(JSON.parse(raw), "root");
140
+ if (parsed.enabled === false) return undefined;
141
+ if (parsed.enabled !== true) throw new Error("Invalid child watchdog config: enabled must be true or false.");
142
+ const thinking = parsed.thinking;
143
+ if (thinking !== undefined && typeof thinking !== "string" && thinking !== false) {
144
+ throw new Error("Invalid child watchdog config: thinking must be a string or false.");
145
+ }
146
+ const runId = childConfigOptionalString(parsed, "runId");
147
+ const agent = childConfigOptionalString(parsed, "agent");
148
+ const childIndex = childConfigOptionalIndex(parsed, "childIndex");
149
+ const model = childConfigOptionalString(parsed, "model");
150
+ return {
151
+ enabled: true,
152
+ ...(runId ? { runId } : {}),
153
+ ...(agent ? { agent } : {}),
154
+ ...(childIndex !== undefined ? { childIndex } : {}),
155
+ watchdogTailTimeoutMs: childConfigPositiveInteger(parsed, "watchdogTailTimeoutMs"),
156
+ agentEndTimeoutMs: childConfigPositiveInteger(parsed, "agentEndTimeoutMs"),
157
+ maxWarnings: childConfigNullableNonNegativeInteger(parsed, "maxWarnings"),
158
+ ...(model ? { model } : {}),
159
+ ...(thinking !== undefined ? { thinking: thinking as string | false } : {}),
160
+ lsp: childConfigLsp(parsed.lsp),
161
+ autoFollowBlockers: childConfigBoolean(parsed, "autoFollowBlockers"),
162
+ autoFollowMaxAttempts: childConfigNullableNonNegativeInteger(parsed, "autoFollowMaxAttempts"),
163
+ stalemateRepeats: childConfigPositiveInteger(parsed, "stalemateRepeats"),
164
+ };
165
+ }
166
+
167
+ export function isChildWatchdogStatusEvent(value: unknown): value is ChildWatchdogStatusEvent {
168
+ if (!value || typeof value !== "object") return false;
169
+ const event = value as Partial<ChildWatchdogStatusEvent>;
170
+ return event.type === CHILD_WATCHDOG_STATUS_EVENT
171
+ && typeof event.seq === "number"
172
+ && Number.isInteger(event.seq)
173
+ && event.seq >= 0
174
+ && typeof event.ts === "number"
175
+ && Number.isFinite(event.ts)
176
+ && typeof event.followUpPending === "boolean"
177
+ && typeof event.phase === "string"
178
+ && (CHILD_WATCHDOG_PHASES as readonly string[]).includes(event.phase);
179
+ }
180
+
181
+ export function childWatchdogIsActive(snapshot: ChildWatchdogStateSnapshot | undefined): boolean {
182
+ if (!snapshot) return false;
183
+ return snapshot.followUpPending || snapshot.phase === "reviewing" || snapshot.phase === "autofollow" || snapshot.phase === "settling";
184
+ }
185
+
186
+ export function acceptChildWatchdogEvent(input: {
187
+ current: ChildWatchdogStateSnapshot | undefined;
188
+ event: ChildWatchdogStatusEvent;
189
+ runId?: string;
190
+ agent?: string;
191
+ childIndex?: number;
192
+ }): ChildWatchdogStateSnapshot | undefined {
193
+ if (input.runId !== undefined && input.event.runId !== input.runId) return undefined;
194
+ if (input.agent !== undefined && input.event.agent !== input.agent) return undefined;
195
+ const eventIndex = input.event.childIndex ?? input.event.stepIndex;
196
+ if (input.childIndex !== undefined && eventIndex !== input.childIndex) return undefined;
197
+ if (input.current && input.event.seq <= input.current.seq) return undefined;
198
+ return {
199
+ phase: input.event.phase,
200
+ seq: input.event.seq,
201
+ lastUpdate: input.event.ts,
202
+ followUpPending: input.event.followUpPending,
203
+ ...(input.event.reason ? { reason: input.event.reason } : {}),
204
+ };
205
+ }
@@ -0,0 +1,123 @@
1
+ import type { WatchdogSeverity, WatchdogWarning } from "./types.ts";
2
+
3
+ export type WatchdogEmissionSuppressionReason = "content-free" | "duplicate" | "max-warnings" | "update-budget";
4
+
5
+ export type WatchdogEmissionDecision = {
6
+ accepted: true;
7
+ identity: string;
8
+ underlyingIdentity: string;
9
+ escalation: boolean;
10
+ } | {
11
+ accepted: false;
12
+ reason: WatchdogEmissionSuppressionReason;
13
+ identity?: string;
14
+ underlyingIdentity?: string;
15
+ };
16
+
17
+ export interface WatchdogEmissionGuardOptions {
18
+ maxWarnings?: number | null;
19
+ dedupeHistoryLimit?: number;
20
+ }
21
+
22
+ const CONTENT_FREE_PHRASES = new Set([
23
+ "stop",
24
+ "done",
25
+ "complete",
26
+ "completed",
27
+ "no issue",
28
+ "no issues",
29
+ "no concern",
30
+ "no concerns",
31
+ "nothing to add",
32
+ "lgtm",
33
+ "looks good",
34
+ "looks good to me",
35
+ "all good",
36
+ "ok",
37
+ "okay",
38
+ "none",
39
+ "n a",
40
+ ]);
41
+
42
+ export function normalizeWatchdogEmissionText(value: string): string {
43
+ return value
44
+ .normalize("NFKC")
45
+ .toLowerCase()
46
+ .replace(/[’'`]/g, "")
47
+ .replace(/[^\p{L}\p{N}]+/gu, " ")
48
+ .replace(/\s+/g, " ")
49
+ .trim();
50
+ }
51
+
52
+ export function watchdogWarningUnderlyingIdentity(warning: Pick<WatchdogWarning, "summary" | "evidence">): string {
53
+ return [normalizeWatchdogEmissionText(warning.summary), normalizeWatchdogEmissionText(warning.evidence)].join("\n");
54
+ }
55
+
56
+ export function watchdogWarningIdentity(warning: Pick<WatchdogWarning, "severity" | "summary" | "evidence">): string {
57
+ return [warning.severity, watchdogWarningUnderlyingIdentity(warning)].join("\n");
58
+ }
59
+
60
+ function isContentFree(value: string): boolean {
61
+ const normalized = normalizeWatchdogEmissionText(value);
62
+ return !normalized || CONTENT_FREE_PHRASES.has(normalized);
63
+ }
64
+
65
+ export class WatchdogEmissionGuard {
66
+ private maxWarnings: number | null;
67
+ private dedupeHistoryLimit: number;
68
+ private acceptedCount = 0;
69
+ private acceptedByUnderlyingIdentity = new Map<string, WatchdogSeverity>();
70
+ private historyOrder: string[] = [];
71
+ private updateAcceptedUnderlyingIdentity: string | undefined;
72
+ private updateAcceptedSeverity: WatchdogSeverity | undefined;
73
+
74
+ constructor(options: WatchdogEmissionGuardOptions = {}) {
75
+ this.maxWarnings = options.maxWarnings ?? null;
76
+ this.dedupeHistoryLimit = options.dedupeHistoryLimit ?? 200;
77
+ }
78
+
79
+ startModelUpdate(): void {
80
+ this.updateAcceptedUnderlyingIdentity = undefined;
81
+ this.updateAcceptedSeverity = undefined;
82
+ }
83
+
84
+ reset(): void {
85
+ this.acceptedCount = 0;
86
+ this.acceptedByUnderlyingIdentity.clear();
87
+ this.historyOrder = [];
88
+ this.startModelUpdate();
89
+ }
90
+
91
+ evaluate(warning: WatchdogWarning): WatchdogEmissionDecision {
92
+ if (isContentFree(warning.summary) || isContentFree(warning.evidence) || isContentFree(warning.recommendedAction)) {
93
+ return { accepted: false, reason: "content-free" };
94
+ }
95
+ const underlyingIdentity = watchdogWarningUnderlyingIdentity(warning);
96
+ const identity = watchdogWarningIdentity(warning);
97
+ const priorSeverity = this.acceptedByUnderlyingIdentity.get(underlyingIdentity);
98
+ const escalation = priorSeverity === "concern" && warning.severity === "blocker";
99
+ if (this.updateAcceptedUnderlyingIdentity !== undefined) {
100
+ const updateEscalation = this.updateAcceptedUnderlyingIdentity === underlyingIdentity
101
+ && this.updateAcceptedSeverity === "concern"
102
+ && warning.severity === "blocker";
103
+ if (!updateEscalation) return { accepted: false, reason: "update-budget", identity, underlyingIdentity };
104
+ }
105
+ if (priorSeverity !== undefined && !escalation) return { accepted: false, reason: "duplicate", identity, underlyingIdentity };
106
+ if (this.maxWarnings !== null && this.acceptedCount >= this.maxWarnings && !escalation) {
107
+ return { accepted: false, reason: "max-warnings", identity, underlyingIdentity };
108
+ }
109
+
110
+ this.acceptedByUnderlyingIdentity.set(underlyingIdentity, warning.severity);
111
+ if (!priorSeverity) {
112
+ this.acceptedCount++;
113
+ this.historyOrder.push(underlyingIdentity);
114
+ }
115
+ while (this.historyOrder.length > this.dedupeHistoryLimit) {
116
+ const stale = this.historyOrder.shift();
117
+ if (stale) this.acceptedByUnderlyingIdentity.delete(stale);
118
+ }
119
+ this.updateAcceptedUnderlyingIdentity = underlyingIdentity;
120
+ this.updateAcceptedSeverity = warning.severity;
121
+ return { accepted: true, identity, underlyingIdentity, escalation };
122
+ }
123
+ }