@opengsd/gsd-pi 1.2.0-dev.955e4da0 → 1.2.0-dev.fb12b103

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 (378) hide show
  1. package/dist/cli-style.d.ts +17 -0
  2. package/dist/cli-style.js +28 -0
  3. package/dist/cli.js +1 -1
  4. package/dist/headless-events.d.ts +4 -2
  5. package/dist/headless-events.js +7 -29
  6. package/dist/models-resolver.d.ts +3 -13
  7. package/dist/models-resolver.js +3 -22
  8. package/dist/resource-loader.js +2 -14
  9. package/dist/resources/.managed-resources-content-hash +1 -1
  10. package/dist/resources/extensions/async-jobs/async-bash-tool.js +30 -64
  11. package/dist/resources/extensions/async-jobs/await-tool.js +80 -12
  12. package/dist/resources/extensions/async-jobs/index.js +65 -0
  13. package/dist/resources/extensions/async-jobs/job-manager.js +12 -1
  14. package/dist/resources/extensions/bg-shell/bg-shell-command.js +6 -6
  15. package/dist/resources/extensions/bg-shell/bg-shell-tool.js +10 -7
  16. package/dist/resources/extensions/bg-shell/overlay.js +9 -6
  17. package/dist/resources/extensions/bg-shell/process-manager.js +54 -25
  18. package/dist/resources/extensions/bg-shell/readiness-detector.js +11 -0
  19. package/dist/resources/extensions/bg-shell/utilities.js +3 -0
  20. package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +209 -88
  21. package/dist/resources/extensions/browser-tools/engine/selection.js +73 -5
  22. package/dist/resources/extensions/browser-tools/index.js +69 -12
  23. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +30 -4
  24. package/dist/resources/extensions/gsd/auto/orchestrator.js +7 -5
  25. package/dist/resources/extensions/gsd/auto-dispatch.js +12 -1
  26. package/dist/resources/extensions/gsd/auto-model-selection.js +25 -6
  27. package/dist/resources/extensions/gsd/auto-post-unit.js +11 -2
  28. package/dist/resources/extensions/gsd/auto-prompts.js +15 -10
  29. package/dist/resources/extensions/gsd/auto-start.js +15 -10
  30. package/dist/resources/extensions/gsd/auto-tool-tracking.js +18 -0
  31. package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +7 -16
  32. package/dist/resources/extensions/gsd/auto-worktree.js +30 -90
  33. package/dist/resources/extensions/gsd/auto.js +4 -13
  34. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +3 -2
  35. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +23 -6
  36. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +19 -0
  37. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +122 -20
  38. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +6 -2
  39. package/dist/resources/extensions/gsd/branch-patterns.js +2 -0
  40. package/dist/resources/extensions/gsd/browser-daemon-auto-prep.js +83 -0
  41. package/dist/resources/extensions/gsd/browser-evidence.js +8 -2
  42. package/dist/resources/extensions/gsd/captures.js +4 -6
  43. package/dist/resources/extensions/gsd/constants.js +0 -2
  44. package/dist/resources/extensions/gsd/crash-recovery.js +4 -12
  45. package/dist/resources/extensions/gsd/doctor-environment.js +2 -6
  46. package/dist/resources/extensions/gsd/doctor-format.js +9 -6
  47. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +13 -15
  48. package/dist/resources/extensions/gsd/error-classifier.js +9 -0
  49. package/dist/resources/extensions/gsd/exec-sandbox.js +30 -10
  50. package/dist/resources/extensions/gsd/guidance.js +98 -0
  51. package/dist/resources/extensions/gsd/guided-flow.js +17 -2
  52. package/dist/resources/extensions/gsd/mcp-filter.js +2 -19
  53. package/dist/resources/extensions/gsd/mcp-tool-name.js +5 -13
  54. package/dist/resources/extensions/gsd/memory-consolidation-scanner.js +1 -1
  55. package/dist/resources/extensions/gsd/migrate/safety.js +4 -1
  56. package/dist/resources/extensions/gsd/notification-store.js +11 -4
  57. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +6 -4
  58. package/dist/resources/extensions/gsd/paths.js +27 -0
  59. package/dist/resources/extensions/gsd/pre-execution-checks.js +91 -3
  60. package/dist/resources/extensions/gsd/preferences-models.js +14 -48
  61. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  62. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  63. package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  64. package/dist/resources/extensions/gsd/prompts/replan-slice.md +1 -1
  65. package/dist/resources/extensions/gsd/prompts/run-uat.md +1 -1
  66. package/dist/resources/extensions/gsd/prompts/system.md +5 -2
  67. package/dist/resources/extensions/gsd/provider-error-guidance.js +1 -5
  68. package/dist/resources/extensions/gsd/provider-switch-observer.js +1 -1
  69. package/dist/resources/extensions/gsd/publication.js +87 -0
  70. package/dist/resources/extensions/gsd/recovery-classification.js +37 -94
  71. package/dist/resources/extensions/gsd/safety/destructive-confirmation.js +108 -0
  72. package/dist/resources/extensions/gsd/state.js +1 -20
  73. package/dist/resources/extensions/gsd/stop-notice.js +57 -0
  74. package/dist/resources/extensions/gsd/tool-surface-readiness.js +56 -0
  75. package/dist/resources/extensions/gsd/tools/exec-tool.js +9 -7
  76. package/dist/resources/extensions/gsd/tools/plan-slice.js +12 -6
  77. package/dist/resources/extensions/gsd/uat-policy.js +2 -1
  78. package/dist/resources/extensions/gsd/unit-closeout.js +138 -0
  79. package/dist/resources/extensions/gsd/unit-context-composer.js +74 -1
  80. package/dist/resources/extensions/gsd/unit-context-manifest.js +4 -27
  81. package/dist/resources/extensions/gsd/unit-registry.js +337 -0
  82. package/dist/resources/extensions/gsd/unit-tool-contracts.js +9 -182
  83. package/dist/resources/extensions/gsd/web-app-uat.js +45 -8
  84. package/dist/resources/extensions/gsd/workflow-tool-surface.js +1 -1
  85. package/dist/resources/extensions/gsd/worktree-git-recovery.js +15 -9
  86. package/dist/resources/extensions/gsd/worktree-root.js +11 -0
  87. package/dist/resources/extensions/gsd/worktree-session-state.js +4 -5
  88. package/dist/resources/extensions/search-the-web/native-search.js +5 -3
  89. package/dist/resources/extensions/shared/browser-contract.js +59 -0
  90. package/dist/resources/extensions/shared/gsd-browser-cli.js +96 -5
  91. package/dist/resources/shared/package.json +3 -0
  92. package/dist/resources/skills/create-skill/references/executable-code.md +1 -1
  93. package/dist/resources/skills/create-skill/workflows/add-reference.md +8 -3
  94. package/dist/resources/skills/create-skill/workflows/add-script.md +4 -2
  95. package/dist/resources/skills/create-skill/workflows/add-template.md +3 -1
  96. package/dist/resources/skills/create-skill/workflows/add-workflow.md +8 -3
  97. package/dist/resources/skills/create-skill/workflows/upgrade-to-router.md +10 -5
  98. package/dist/resources/skills/create-skill/workflows/verify-skill.md +9 -4
  99. package/dist/resources/skills/spike-wrap-up/SKILL.md +9 -9
  100. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  101. package/dist/web/standalone/.next/BUILD_ID +1 -1
  102. package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
  103. package/dist/web/standalone/.next/build-manifest.json +3 -3
  104. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  105. package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
  106. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  107. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  108. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  109. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  110. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  111. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  112. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  113. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  114. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  115. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  116. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  117. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  118. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  119. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  120. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  121. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  122. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  123. package/dist/web/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  124. package/dist/web/standalone/.next/server/app/index.html +1 -1
  125. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  126. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  127. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  128. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  129. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  130. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  131. package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
  132. package/dist/web/standalone/.next/server/chunks/5124.js +1 -1
  133. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  134. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  135. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  136. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  137. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  138. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  139. package/dist/web/standalone/.next/static/chunks/{796.cf859a427a2cb2ac.js → 796.e0bdc932325d7e03.js} +1 -1
  140. package/dist/web/standalone/.next/static/chunks/{webpack-fbea77b5f9953368.js → webpack-f0285ce91d4ec9ef.js} +1 -1
  141. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  142. package/dist/web/standalone/node_modules/postcss/lib/container.js +18 -26
  143. package/dist/web/standalone/node_modules/postcss/lib/css-syntax-error.js +14 -47
  144. package/dist/web/standalone/node_modules/postcss/lib/declaration.js +4 -4
  145. package/dist/web/standalone/node_modules/postcss/lib/fromJSON.js +3 -3
  146. package/dist/web/standalone/node_modules/postcss/lib/input.js +29 -54
  147. package/dist/web/standalone/node_modules/postcss/lib/lazy-result.js +37 -47
  148. package/dist/web/standalone/node_modules/postcss/lib/map-generator.js +9 -26
  149. package/dist/web/standalone/node_modules/postcss/lib/no-work-result.js +55 -57
  150. package/dist/web/standalone/node_modules/postcss/lib/node.js +31 -99
  151. package/dist/web/standalone/node_modules/postcss/lib/parse.js +1 -1
  152. package/dist/web/standalone/node_modules/postcss/lib/parser.js +9 -10
  153. package/dist/web/standalone/node_modules/postcss/lib/postcss.js +12 -12
  154. package/dist/web/standalone/node_modules/postcss/lib/previous-map.js +11 -30
  155. package/dist/web/standalone/node_modules/postcss/lib/processor.js +7 -7
  156. package/dist/web/standalone/node_modules/postcss/lib/result.js +5 -5
  157. package/dist/web/standalone/node_modules/postcss/lib/rule.js +6 -6
  158. package/dist/web/standalone/node_modules/postcss/lib/stringifier.js +28 -69
  159. package/dist/web/standalone/node_modules/postcss/lib/tokenize.js +2 -6
  160. package/dist/web/standalone/node_modules/postcss/package.json +48 -48
  161. package/dist/web/standalone/package.json +1 -1
  162. package/dist/worktree-cli.js +3 -6
  163. package/dist/worktree-status-banner.js +7 -15
  164. package/package.json +1 -1
  165. package/packages/cloud-mcp-gateway/package.json +2 -2
  166. package/packages/contracts/dist/rpc.d.ts +1 -0
  167. package/packages/contracts/dist/rpc.d.ts.map +1 -1
  168. package/packages/contracts/dist/rpc.js.map +1 -1
  169. package/packages/contracts/dist/workflow.d.ts +4 -0
  170. package/packages/contracts/dist/workflow.d.ts.map +1 -1
  171. package/packages/contracts/dist/workflow.js.map +1 -1
  172. package/packages/contracts/package.json +1 -1
  173. package/packages/daemon/package.json +4 -4
  174. package/packages/gsd-agent-core/package.json +5 -5
  175. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts +5 -0
  176. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  177. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +5 -0
  178. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  179. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  180. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +7 -0
  181. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  182. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  183. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/input-controller.js +8 -1
  184. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  185. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.d.ts.map +1 -1
  186. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.js +11 -1
  187. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.js.map +1 -1
  188. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.d.ts.map +1 -1
  189. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.js +4 -4
  190. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.js.map +1 -1
  191. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  192. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-mode.js +3 -1
  193. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-mode.js.map +1 -1
  194. package/packages/gsd-agent-modes/package.json +7 -7
  195. package/packages/mcp-server/dist/cli.js +6 -3
  196. package/packages/mcp-server/dist/cli.js.map +1 -1
  197. package/packages/mcp-server/dist/workflow-tools.d.ts +8 -0
  198. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  199. package/packages/mcp-server/dist/workflow-tools.js +17 -1
  200. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  201. package/packages/mcp-server/package.json +3 -3
  202. package/packages/native/package.json +1 -1
  203. package/packages/pi-agent-core/dist/harness/env/nodejs.d.ts +1 -0
  204. package/packages/pi-agent-core/dist/harness/env/nodejs.d.ts.map +1 -1
  205. package/packages/pi-agent-core/dist/harness/env/nodejs.js +34 -3
  206. package/packages/pi-agent-core/dist/harness/env/nodejs.js.map +1 -1
  207. package/packages/pi-agent-core/dist/index.d.ts +1 -0
  208. package/packages/pi-agent-core/dist/index.d.ts.map +1 -1
  209. package/packages/pi-agent-core/dist/index.js +3 -0
  210. package/packages/pi-agent-core/dist/index.js.map +1 -1
  211. package/packages/pi-agent-core/package.json +1 -1
  212. package/packages/pi-ai/dist/models.generated.d.ts +94 -382
  213. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  214. package/packages/pi-ai/dist/models.generated.js +149 -422
  215. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  216. package/packages/pi-ai/package.json +1 -1
  217. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +2 -2
  218. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  219. package/packages/pi-coding-agent/dist/core/auth-storage.js +19 -13
  220. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  221. package/packages/pi-coding-agent/dist/core/provider-readiness.d.ts.map +1 -1
  222. package/packages/pi-coding-agent/dist/core/provider-readiness.js +13 -6
  223. package/packages/pi-coding-agent/dist/core/provider-readiness.js.map +1 -1
  224. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +11 -0
  225. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  226. package/packages/pi-coding-agent/dist/core/tools/bash.js +53 -11
  227. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  228. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  229. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  230. package/packages/pi-coding-agent/dist/index.js +1 -1
  231. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  232. package/packages/pi-coding-agent/dist/utils/shell.d.ts +28 -2
  233. package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
  234. package/packages/pi-coding-agent/dist/utils/shell.js +56 -10
  235. package/packages/pi-coding-agent/dist/utils/shell.js.map +1 -1
  236. package/packages/pi-coding-agent/package.json +7 -7
  237. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  238. package/packages/pi-tui/dist/tui.js +9 -0
  239. package/packages/pi-tui/dist/tui.js.map +1 -1
  240. package/packages/pi-tui/package.json +2 -2
  241. package/packages/rpc-client/package.json +2 -2
  242. package/pkg/package.json +1 -1
  243. package/src/resources/extensions/async-jobs/async-bash-cancel.test.ts +360 -0
  244. package/src/resources/extensions/async-jobs/async-bash-tool.ts +33 -56
  245. package/src/resources/extensions/async-jobs/await-tool.test.ts +139 -0
  246. package/src/resources/extensions/async-jobs/await-tool.ts +82 -12
  247. package/src/resources/extensions/async-jobs/index.ts +79 -0
  248. package/src/resources/extensions/async-jobs/job-manager.ts +21 -1
  249. package/src/resources/extensions/bg-shell/bg-shell-command.ts +6 -6
  250. package/src/resources/extensions/bg-shell/bg-shell-tool.ts +10 -6
  251. package/src/resources/extensions/bg-shell/overlay.ts +9 -5
  252. package/src/resources/extensions/bg-shell/process-manager.ts +50 -25
  253. package/src/resources/extensions/bg-shell/readiness-detector.ts +12 -0
  254. package/src/resources/extensions/bg-shell/tests/lifecycle-and-utilities.test.ts +48 -1
  255. package/src/resources/extensions/bg-shell/utilities.ts +3 -0
  256. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +265 -98
  257. package/src/resources/extensions/browser-tools/engine/selection.ts +90 -4
  258. package/src/resources/extensions/browser-tools/index.ts +71 -13
  259. package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +83 -13
  260. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +29 -1
  261. package/src/resources/extensions/browser-tools/tests/managed-gsd-browser-tools.test.mjs +136 -0
  262. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +34 -4
  263. package/src/resources/extensions/gsd/auto/orchestrator.ts +7 -5
  264. package/src/resources/extensions/gsd/auto-dispatch.ts +12 -0
  265. package/src/resources/extensions/gsd/auto-model-selection.ts +25 -5
  266. package/src/resources/extensions/gsd/auto-post-unit.ts +13 -2
  267. package/src/resources/extensions/gsd/auto-prompts.ts +40 -26
  268. package/src/resources/extensions/gsd/auto-start.ts +15 -10
  269. package/src/resources/extensions/gsd/auto-tool-tracking.ts +19 -0
  270. package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +10 -17
  271. package/src/resources/extensions/gsd/auto-worktree.ts +30 -93
  272. package/src/resources/extensions/gsd/auto.ts +8 -15
  273. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +3 -5
  274. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +23 -6
  275. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +24 -0
  276. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +151 -15
  277. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +6 -2
  278. package/src/resources/extensions/gsd/branch-patterns.ts +3 -0
  279. package/src/resources/extensions/gsd/browser-daemon-auto-prep.ts +108 -0
  280. package/src/resources/extensions/gsd/browser-evidence.ts +18 -2
  281. package/src/resources/extensions/gsd/captures.ts +4 -6
  282. package/src/resources/extensions/gsd/constants.ts +0 -3
  283. package/src/resources/extensions/gsd/crash-recovery.ts +3 -9
  284. package/src/resources/extensions/gsd/doctor-environment.ts +2 -7
  285. package/src/resources/extensions/gsd/doctor-format.ts +12 -7
  286. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +13 -15
  287. package/src/resources/extensions/gsd/error-classifier.ts +11 -0
  288. package/src/resources/extensions/gsd/exec-sandbox.ts +49 -9
  289. package/src/resources/extensions/gsd/guidance.ts +139 -0
  290. package/src/resources/extensions/gsd/guided-flow.ts +16 -2
  291. package/src/resources/extensions/gsd/mcp-filter.ts +2 -23
  292. package/src/resources/extensions/gsd/mcp-tool-name.ts +6 -11
  293. package/src/resources/extensions/gsd/memory-consolidation-scanner.ts +1 -1
  294. package/src/resources/extensions/gsd/migrate/safety.ts +4 -1
  295. package/src/resources/extensions/gsd/notification-store.ts +26 -3
  296. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +6 -4
  297. package/src/resources/extensions/gsd/paths.ts +33 -0
  298. package/src/resources/extensions/gsd/pre-execution-checks.ts +109 -3
  299. package/src/resources/extensions/gsd/preferences-models.ts +12 -47
  300. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  301. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  302. package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  303. package/src/resources/extensions/gsd/prompts/replan-slice.md +1 -1
  304. package/src/resources/extensions/gsd/prompts/run-uat.md +1 -1
  305. package/src/resources/extensions/gsd/prompts/system.md +5 -2
  306. package/src/resources/extensions/gsd/provider-error-guidance.ts +4 -9
  307. package/src/resources/extensions/gsd/provider-switch-observer.ts +1 -1
  308. package/src/resources/extensions/gsd/publication.ts +122 -0
  309. package/src/resources/extensions/gsd/recovery-classification.ts +42 -96
  310. package/src/resources/extensions/gsd/safety/destructive-confirmation.ts +134 -0
  311. package/src/resources/extensions/gsd/state.ts +4 -21
  312. package/src/resources/extensions/gsd/stop-notice.ts +75 -0
  313. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +22 -0
  314. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +16 -19
  315. package/src/resources/extensions/gsd/tests/browser-automation-contract-fixture.ts +39 -0
  316. package/src/resources/extensions/gsd/tests/browser-contract.test.ts +44 -0
  317. package/src/resources/extensions/gsd/tests/browser-daemon-auto-prep.test.ts +144 -0
  318. package/src/resources/extensions/gsd/tests/checkout-branch-stash-guard.test.ts +66 -1
  319. package/src/resources/extensions/gsd/tests/clear-stale-autostart.test.ts +22 -0
  320. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +8 -7
  321. package/src/resources/extensions/gsd/tests/destructive-confirmation.test.ts +303 -0
  322. package/src/resources/extensions/gsd/tests/dispatch-run-uat-browser-tools.test.ts +2 -1
  323. package/src/resources/extensions/gsd/tests/dynamic-bash-no-cap.test.ts +132 -0
  324. package/src/resources/extensions/gsd/tests/exec-graceful-kill.test.ts +193 -0
  325. package/src/resources/extensions/gsd/tests/exec-tool.test.ts +29 -1
  326. package/src/resources/extensions/gsd/tests/extension-bootstrap-isolation.test.ts +35 -1
  327. package/src/resources/extensions/gsd/tests/guidance.test.ts +125 -0
  328. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +53 -11
  329. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +73 -58
  330. package/src/resources/extensions/gsd/tests/integration/gsd-integration-fixture.ts +80 -0
  331. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +3 -1
  332. package/src/resources/extensions/gsd/tests/model-unittype-mapping.test.ts +32 -1
  333. package/src/resources/extensions/gsd/tests/notification-store.test.ts +32 -0
  334. package/src/resources/extensions/gsd/tests/oauth-api-model-routing.test.ts +167 -0
  335. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +193 -1
  336. package/src/resources/extensions/gsd/tests/provider-error-guidance.test.ts +3 -3
  337. package/src/resources/extensions/gsd/tests/publication.test.ts +120 -0
  338. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +157 -0
  339. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +1 -0
  340. package/src/resources/extensions/gsd/tests/stop-notice.test.ts +70 -0
  341. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +76 -0
  342. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +8 -0
  343. package/src/resources/extensions/gsd/tests/tool-surface-readiness.test.ts +155 -0
  344. package/src/resources/extensions/gsd/tests/uat-policy.test.ts +24 -29
  345. package/src/resources/extensions/gsd/tests/unit-closeout.test.ts +209 -0
  346. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +67 -2
  347. package/src/resources/extensions/gsd/tests/unit-registry.test.ts +163 -0
  348. package/src/resources/extensions/gsd/tests/web-app-uat.test.ts +44 -1
  349. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
  350. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +2 -2
  351. package/src/resources/extensions/gsd/tool-surface-readiness.ts +76 -0
  352. package/src/resources/extensions/gsd/tools/exec-tool.ts +8 -7
  353. package/src/resources/extensions/gsd/tools/plan-slice.ts +12 -6
  354. package/src/resources/extensions/gsd/uat-policy.ts +2 -1
  355. package/src/resources/extensions/gsd/unit-closeout.ts +201 -0
  356. package/src/resources/extensions/gsd/unit-context-composer.ts +111 -1
  357. package/src/resources/extensions/gsd/unit-context-manifest.ts +4 -28
  358. package/src/resources/extensions/gsd/unit-registry.ts +412 -0
  359. package/src/resources/extensions/gsd/unit-tool-contracts.ts +27 -192
  360. package/src/resources/extensions/gsd/web-app-uat.ts +51 -8
  361. package/src/resources/extensions/gsd/workflow-tool-surface.ts +4 -1
  362. package/src/resources/extensions/gsd/worktree-git-recovery.ts +15 -9
  363. package/src/resources/extensions/gsd/worktree-root.ts +12 -0
  364. package/src/resources/extensions/gsd/worktree-session-state.ts +3 -5
  365. package/src/resources/extensions/search-the-web/native-search.ts +5 -3
  366. package/src/resources/extensions/shared/browser-contract.ts +66 -0
  367. package/src/resources/extensions/shared/gsd-browser-cli.ts +119 -5
  368. package/src/resources/shared/package.json +3 -0
  369. package/src/resources/skills/create-skill/references/executable-code.md +1 -1
  370. package/src/resources/skills/create-skill/workflows/add-reference.md +8 -3
  371. package/src/resources/skills/create-skill/workflows/add-script.md +4 -2
  372. package/src/resources/skills/create-skill/workflows/add-template.md +3 -1
  373. package/src/resources/skills/create-skill/workflows/add-workflow.md +8 -3
  374. package/src/resources/skills/create-skill/workflows/upgrade-to-router.md +10 -5
  375. package/src/resources/skills/create-skill/workflows/verify-skill.md +9 -4
  376. package/src/resources/skills/spike-wrap-up/SKILL.md +9 -9
  377. /package/dist/web/standalone/.next/static/{C24pqUd-aru-l0Dp0gLZP → mU4QIDlpVHDdjDpeEKh5W}/_buildManifest.js +0 -0
  378. /package/dist/web/standalone/.next/static/{C24pqUd-aru-l0Dp0gLZP → mU4QIDlpVHDdjDpeEKh5W}/_ssgManifest.js +0 -0
@@ -0,0 +1,360 @@
1
+ /**
2
+ * async-bash-cancel.test.ts — Tests for graceful async_bash cancellation.
3
+ *
4
+ * Proves that:
5
+ * 1. The killProcessTree re-export from @gsd/pi-coding-agent resolves correctly
6
+ * (loading async-bash-tool.ts will fail at import-time if the export is missing).
7
+ * 2. manager.cancel() sets status to 'cancelled' and returns 'cancelled'.
8
+ * 3. The job promise settles promptly (SIGTERM kills a well-behaved child).
9
+ * 4. The timeout path force-kills a SIGTERM-immune child via SIGKILL (regression:
10
+ * it must not be left running in the background after the timeout fires).
11
+ */
12
+
13
+ import test from "node:test";
14
+ import assert from "node:assert/strict";
15
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
18
+ import { createAsyncBashTool } from "./async-bash-tool.ts";
19
+ import { createAwaitTool } from "./await-tool.ts";
20
+ import { AsyncJobManager } from "./job-manager.ts";
21
+
22
+ function isAlive(pid: number): boolean {
23
+ try {
24
+ process.kill(pid, 0); // signal 0 probes existence without killing
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ // If killProcessTree is missing from the re-export the import above will throw
32
+ // at load time (async-bash-tool.ts destructures it from @gsd/pi-coding-agent).
33
+ // A load-time error surfaces as a test runner parse failure — no explicit
34
+ // assertion needed; the test file simply won't run.
35
+
36
+ const noopSignal = new AbortController().signal;
37
+
38
+ test("graceful cancel: manager.cancel returns 'cancelled' and job settles promptly", async () => {
39
+ const manager = new AsyncJobManager();
40
+ const tool = createAsyncBashTool(() => manager, () => process.cwd());
41
+
42
+ // Launch a long-running well-behaved process (responds to SIGTERM)
43
+ const result = await tool.execute(
44
+ "tc-cancel-01",
45
+ {
46
+ command: "sleep 30",
47
+ label: "cancel-test-sleep",
48
+ },
49
+ noopSignal,
50
+ () => {},
51
+ undefined as never,
52
+ );
53
+
54
+ const text = result.content.map((c: { type: string; text?: string }) => c.text ?? "").join("\n");
55
+ const jobId = text.match(/\*\*(bg_[a-f0-9]+)\*\*/)?.[1];
56
+ assert.ok(jobId, `Expected a job ID in result text, got: ${text}`);
57
+
58
+ const job = manager.getJob(jobId)!;
59
+ assert.ok(job, "Job should be registered in manager");
60
+ assert.equal(job.status, "running", "Job should be running before cancel");
61
+
62
+ // Cancel — should return 'cancelled' immediately
63
+ const cancelResult = manager.cancel(jobId);
64
+ assert.equal(cancelResult, "cancelled", `cancel() should return 'cancelled', got: ${cancelResult}`);
65
+ assert.equal(job.status, "cancelled", "Job status should flip to 'cancelled'");
66
+
67
+ // Job promise should settle promptly — SIGTERM kills sleep quickly
68
+ const start = Date.now();
69
+ await Promise.race([
70
+ job.promise,
71
+ new Promise<never>((_, reject) => {
72
+ const t = setTimeout(() => {
73
+ reject(new Error(
74
+ `Job promise did not settle within 5s after cancel ` +
75
+ `(${Date.now() - start}ms elapsed) — graceful cancel may be broken`,
76
+ ));
77
+ }, 5_000);
78
+ if (typeof t === "object" && "unref" in t) t.unref();
79
+ }),
80
+ ]);
81
+
82
+ const elapsed = Date.now() - start;
83
+ assert.ok(elapsed < 5_000, `Job promise should settle under 5s, took ${elapsed}ms`);
84
+
85
+ manager.shutdown();
86
+ });
87
+
88
+ test(
89
+ "graceful cancel: status STAYS 'cancelled' after the job promise settles (not clobbered to 'completed')",
90
+ async (t) => {
91
+ // Regression: async_bash resolves (not rejects) its run promise even when aborted —
92
+ // safeResolve returns "Command aborted" rather than throwing — so the job-manager
93
+ // .then branch used to overwrite status back to 'completed', mislabeling a job the
94
+ // user explicitly cancelled. A cancelled job must also deliver NO follow-up.
95
+ const delivered: string[] = [];
96
+ const manager = new AsyncJobManager({ onJobComplete: (j) => delivered.push(j.id) });
97
+ // Tear down via t.after() so a thrown assertion still cleans up the manager.
98
+ t.after(() => manager.shutdown());
99
+ const tool = createAsyncBashTool(() => manager, () => process.cwd());
100
+
101
+ const result = await tool.execute(
102
+ "tc-cancel-status",
103
+ { command: "sleep 30", label: "cancel-status-test" },
104
+ noopSignal,
105
+ () => {},
106
+ undefined as never,
107
+ );
108
+ const text = result.content.map((c: { type: string; text?: string }) => c.text ?? "").join("\n");
109
+ const jobId = text.match(/\*\*(bg_[a-f0-9]+)\*\*/)?.[1];
110
+ assert.ok(jobId, `Expected a job ID, got: ${text}`);
111
+
112
+ const job = manager.getJob(jobId)!;
113
+ assert.equal(manager.cancel(jobId), "cancelled");
114
+ assert.equal(job.status, "cancelled", "status should be 'cancelled' synchronously");
115
+
116
+ // Await the run promise settling (SIGTERM kills sleep quickly), THEN re-check status.
117
+ await job.promise;
118
+ // Let the deliverResult setTimeout(0) (if any) and microtasks flush.
119
+ await new Promise((r) => setTimeout(r, 20));
120
+
121
+ assert.equal(
122
+ job.status,
123
+ "cancelled",
124
+ `status must REMAIN 'cancelled' after the run promise settles, got '${job.status}' ` +
125
+ `(the .then branch clobbered the user-cancelled status)`,
126
+ );
127
+ assert.equal(
128
+ delivered.includes(jobId),
129
+ false,
130
+ "a cancelled job must not fire an onJobComplete follow-up",
131
+ );
132
+ },
133
+ );
134
+
135
+ test("graceful cancel: killProcessTree re-export resolves (load-time check)", async () => {
136
+ // This test is a belt-and-suspenders static check. If killProcessTree were
137
+ // not re-exported, the import at the top of this file would have already
138
+ // caused the entire test file to fail to load. We do an explicit runtime
139
+ // check here to make the intent clear and produce a readable assertion failure
140
+ // rather than a cryptic module-not-found error in the test runner output.
141
+ const mod = await import("@gsd/pi-coding-agent");
142
+ assert.ok(
143
+ typeof (mod as Record<string, unknown>).killProcessTree === "function",
144
+ "killProcessTree must be exported from @gsd/pi-coding-agent",
145
+ );
146
+ });
147
+
148
+ test(
149
+ "timeout path: SIGTERM-immune job is force-killed (SIGKILL), not left running in the background",
150
+ // Worst case ~12s (1s timeout + 5s grace + 3s hard-deadline + 3s poll); the
151
+ // explicit timeout prevents an infinite hang if force-resolve ever regresses.
152
+ { skip: process.platform === "win32" ? "Unix-primary graceful semantics" : false, timeout: 20_000 },
153
+ async (t) => {
154
+ // Regression: the timeout path previously called a local killTree that only
155
+ // ever sent SIGTERM (twice), so a `trap '' TERM` child survived its timeout
156
+ // and ran forever in the background. It now routes through killProcessTree,
157
+ // which escalates SIGTERM -> grace -> SIGKILL. This proves the child is
158
+ // actually dead shortly after the 5s grace window, not orphaned.
159
+ const dir = mkdtempSync(join(tmpdir(), "async-timeout-sigkill-"));
160
+ const pidFile = join(dir, "pgid.pid");
161
+ t.after(() => {
162
+ // Best-effort: kill the process group if the test failed and left it alive.
163
+ try {
164
+ const pgid = Number.parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
165
+ // Guard pgid > 0: process.kill(-0, ...) would signal the test runner's OWN
166
+ // process group (-0 === 0 === "caller's group" under POSIX).
167
+ if (Number.isFinite(pgid) && pgid > 0) process.kill(-pgid, "SIGKILL");
168
+ } catch {
169
+ /* already gone */
170
+ }
171
+ rmSync(dir, { recursive: true, force: true });
172
+ });
173
+
174
+ const manager = new AsyncJobManager();
175
+ const tool = createAsyncBashTool(() => manager, () => process.cwd());
176
+
177
+ // echo $$ records the detached shell's PID (process-group leader). The shell
178
+ // ignores SIGTERM and loops forever, so only SIGKILL can end it.
179
+ const command = `echo $$ > '${pidFile}'; trap '' TERM; while true; do sleep 1; done`;
180
+ const result = await tool.execute(
181
+ "tc-timeout-sigkill",
182
+ { command, label: "timeout-sigkill", timeout: 1 },
183
+ noopSignal,
184
+ () => {},
185
+ undefined as never,
186
+ );
187
+
188
+ const text = result.content.map((c: { type: string; text?: string }) => c.text ?? "").join("\n");
189
+ const jobId = text.match(/\*\*(bg_[a-f0-9]+)\*\*/)?.[1];
190
+ assert.ok(jobId, `Expected a job ID, got: ${text}`);
191
+
192
+ // The promise force-resolves at ~timeout + grace + hard-deadline (~1+5+3s).
193
+ await manager.getJob(jobId)!.promise;
194
+
195
+ const pgid = Number.parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
196
+ assert.ok(Number.isFinite(pgid) && pgid > 0, "child must have recorded a valid positive process-group PID");
197
+
198
+ // Poll briefly: SIGKILL was sent at timeout+grace (~6s); give it a moment to reap.
199
+ const deadlineMs = Date.now() + 3_000;
200
+ while (isAlive(pgid) && Date.now() < deadlineMs) {
201
+ await new Promise((r) => setTimeout(r, 100));
202
+ }
203
+ assert.equal(
204
+ isAlive(pgid),
205
+ false,
206
+ `SIGTERM-immune timed-out job (pgid ${pgid}) must be SIGKILLed, not left running`,
207
+ );
208
+
209
+ manager.shutdown();
210
+ },
211
+ );
212
+
213
+ test("graceful cancel: job promise settles even for node process on cancel", async () => {
214
+ const manager = new AsyncJobManager();
215
+ const tool = createAsyncBashTool(() => manager, () => process.cwd());
216
+
217
+ // node process that blocks for 30s — responds to SIGTERM
218
+ const result = await tool.execute(
219
+ "tc-cancel-02",
220
+ {
221
+ command: `node -e "setTimeout(()=>{}, 30000)"`,
222
+ label: "cancel-test-node",
223
+ },
224
+ noopSignal,
225
+ () => {},
226
+ undefined as never,
227
+ );
228
+
229
+ const text = result.content.map((c: { type: string; text?: string }) => c.text ?? "").join("\n");
230
+ const jobId = text.match(/\*\*(bg_[a-f0-9]+)\*\*/)?.[1];
231
+ assert.ok(jobId, "Expected a job ID");
232
+
233
+ const job = manager.getJob(jobId)!;
234
+ assert.equal(job.status, "running");
235
+
236
+ const cancelResult = manager.cancel(jobId);
237
+ assert.equal(cancelResult, "cancelled");
238
+
239
+ const start = Date.now();
240
+ await Promise.race([
241
+ job.promise,
242
+ new Promise<never>((_, reject) => {
243
+ const t = setTimeout(() => {
244
+ reject(new Error(`Job promise hung after cancel (${Date.now() - start}ms)`));
245
+ }, 5_000);
246
+ if (typeof t === "object" && "unref" in t) t.unref();
247
+ }),
248
+ ]);
249
+
250
+ assert.ok(Date.now() - start < 5_000, "Promise should settle quickly after cancel");
251
+ manager.shutdown();
252
+ });
253
+
254
+ test(
255
+ "cancel path: SIGTERM-immune job force-resolves via hard deadline, does not hang",
256
+ // Worst case ~8s (5s grace + 3s hard-deadline after cancel); explicit timeout
257
+ // guards against an infinite hang if the abort-path force-resolve regresses.
258
+ { skip: process.platform === "win32" ? "Unix-primary graceful semantics" : false, timeout: 20_000 },
259
+ async () => {
260
+ // Regression: onAbort (the /jobs cancel path) used to kill the child but arm no
261
+ // hard deadline, so cancelling a `trap '' TERM` child that never closes left the
262
+ // job promise dangling forever. It now arms the same hard-deadline force-resolve
263
+ // the timeout path uses.
264
+ const manager = new AsyncJobManager();
265
+ const tool = createAsyncBashTool(() => manager, () => process.cwd());
266
+
267
+ const result = await tool.execute(
268
+ "tc-cancel-dstate",
269
+ { command: `trap '' TERM; while true; do sleep 1; done`, label: "cancel-dstate" },
270
+ noopSignal,
271
+ () => {},
272
+ undefined as never,
273
+ );
274
+ const text = result.content.map((c: { type: string; text?: string }) => c.text ?? "").join("\n");
275
+ const jobId = text.match(/\*\*(bg_[a-f0-9]+)\*\*/)?.[1];
276
+ assert.ok(jobId, "Expected a job ID");
277
+
278
+ const job = manager.getJob(jobId)!;
279
+ assert.equal(job.status, "running");
280
+ assert.equal(manager.cancel(jobId), "cancelled");
281
+
282
+ // The promise must settle via the hard deadline (~8s) rather than hang.
283
+ const start = Date.now();
284
+ await Promise.race([
285
+ job.promise,
286
+ new Promise<never>((_, reject) => {
287
+ const t = setTimeout(() => {
288
+ reject(new Error(`Cancelled D-state job hung (${Date.now() - start}ms) — abort-path force-resolve missing`));
289
+ }, 15_000);
290
+ if (typeof t === "object" && "unref" in t) t.unref();
291
+ }),
292
+ ]);
293
+ assert.ok(Date.now() - start < 15_000, "cancelled D-state job must force-resolve, not hang");
294
+
295
+ manager.shutdown();
296
+ },
297
+ );
298
+
299
+ test(
300
+ "ESC during await_job ends the wait but leaves the real background process alive",
301
+ // End-to-end proof of the headline claim: a real OS subprocess (not a synthetic
302
+ // in-process job) must survive an aborted await_job. The await tool resolves on
303
+ // abort; we then confirm the child PID is still alive before cleaning it up.
304
+ { skip: process.platform === "win32" ? "Unix-primary graceful semantics" : false, timeout: 20_000 },
305
+ async (t) => {
306
+ const dir = mkdtempSync(join(tmpdir(), "await-esc-alive-"));
307
+ const pidFile = join(dir, "pgid.pid");
308
+ const manager = new AsyncJobManager();
309
+ t.after(() => {
310
+ try {
311
+ const pgid = Number.parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
312
+ if (Number.isFinite(pgid) && pgid > 0) process.kill(-pgid, "SIGKILL");
313
+ } catch {
314
+ /* already gone */
315
+ }
316
+ manager.shutdown();
317
+ rmSync(dir, { recursive: true, force: true });
318
+ });
319
+
320
+ const asyncBash = createAsyncBashTool(() => manager, () => process.cwd());
321
+ const awaitJob = createAwaitTool(() => manager);
322
+
323
+ // Start a real 60s background process that records its process-group PID.
324
+ const started = await asyncBash.execute(
325
+ "tc-await-esc",
326
+ { command: `echo $$ > '${pidFile}'; sleep 60`, label: "await-esc-sleeper" },
327
+ noopSignal,
328
+ () => {},
329
+ undefined as never,
330
+ );
331
+ const jobId = started.content.map((c: { type: string; text?: string }) => c.text ?? "").join("\n")
332
+ .match(/\*\*(bg_[a-f0-9]+)\*\*/)?.[1];
333
+ assert.ok(jobId, "expected a job id from async_bash");
334
+
335
+ // Give the child a moment to write its pidfile.
336
+ await new Promise((r) => setTimeout(r, 200));
337
+ const pgid = Number.parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
338
+ assert.ok(Number.isFinite(pgid) && pgid > 0, "child must record a valid pgid");
339
+ assert.equal(isAlive(pgid), true, "child should be alive before the await");
340
+
341
+ // await_job, then fire ESC (abort) ~100ms in. The wait must end promptly.
342
+ const ac = new AbortController();
343
+ const abortTimer = setTimeout(() => ac.abort(), 100);
344
+ if (typeof abortTimer === "object" && "unref" in abortTimer) (abortTimer as NodeJS.Timeout).unref();
345
+
346
+ const waitStart = Date.now();
347
+ const awaitResult = await awaitJob.execute("tc-await-esc-wait", { jobs: [jobId!] }, ac.signal, () => {}, undefined as never);
348
+ const elapsed = Date.now() - waitStart;
349
+ const awaitText = awaitResult.content.map((c: { type: string; text?: string }) => c.text ?? "").join("\n");
350
+
351
+ // (a) The wait ended promptly on ESC, not after the 120s default timeout.
352
+ assert.ok(elapsed < 5_000, `await should end promptly on ESC, took ${elapsed}ms`);
353
+ assert.match(awaitText, /interrupted/i, "aborted await should report the interruption");
354
+
355
+ // (b) The job and its real OS process are both STILL ALIVE — ESC ends the wait,
356
+ // it does not kill the job.
357
+ assert.equal(manager.getJob(jobId!)!.status, "running", "job should still be running after ESC");
358
+ assert.equal(isAlive(pgid), true, "the real background process must survive an aborted await_job");
359
+ },
360
+ );
@@ -10,11 +10,14 @@ import type { ToolDefinition } from "@gsd/pi-coding-agent";
10
10
  import {
11
11
  getShellConfig,
12
12
  sanitizeCommand,
13
+ killProcessTree,
14
+ SIGKILL_GRACE_MS,
15
+ HARD_DEADLINE_MS,
13
16
  DEFAULT_MAX_BYTES,
14
17
  DEFAULT_MAX_LINES,
15
18
  } from "@gsd/pi-coding-agent";
16
19
  import { Type } from "@sinclair/typebox";
17
- import { spawn, spawnSync } from "node:child_process";
20
+ import { spawn } from "node:child_process";
18
21
  import { createWriteStream } from "node:fs";
19
22
  import { tmpdir } from "node:os";
20
23
  import { join } from "node:path";
@@ -37,29 +40,6 @@ function getTempFilePath(): string {
37
40
  return join(tmpdir(), `pi-async-bash-${id}.log`);
38
41
  }
39
42
 
40
- /**
41
- * Kill a process and its children (cross-platform).
42
- * Uses process group kill on Unix; taskkill /F /T on Windows.
43
- */
44
- function killTree(pid: number): void {
45
- if (process.platform === "win32") {
46
- try {
47
- spawnSync("taskkill", ["/F", "/T", "/PID", String(pid)], {
48
- timeout: 5_000,
49
- stdio: "ignore",
50
- });
51
- } catch {
52
- try { process.kill(pid, "SIGTERM"); } catch { /* already exited */ }
53
- }
54
- } else {
55
- try {
56
- process.kill(-pid, "SIGTERM");
57
- } catch {
58
- try { process.kill(pid, "SIGTERM"); } catch { /* already exited */ }
59
- }
60
- }
61
- }
62
-
63
43
  export function createAsyncBashTool(
64
44
  getManager: () => AsyncJobManager,
65
45
  getCwd: () => string,
@@ -73,7 +53,7 @@ export function createAsyncBashTool(
73
53
  `Output is truncated to the last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB.`,
74
54
  promptSnippet: "Run a bash command in the background, returning a job ID immediately.",
75
55
  promptGuidelines: [
76
- "Use async_bash for commands that take more than a few seconds (builds, tests, installs, large git operations).",
56
+ "Use async_bash for long-running builds, tests, installs, or operations that should run in the background so you can continue other work (the job ID lets you await_job later). Sync bash is uncapped — use async_bash when you want non-blocking behavior, not because of a timeout concern.",
77
57
  "After starting async jobs, continue with other work and use await_job when you need the results.",
78
58
  "await_job has a configurable timeout (default 120s) to prevent indefinite blocking — if it times out, jobs keep running and you can check again later.",
79
59
  "For long-running processes (SSH, deploys, training) that may take minutes+, prefer async_bash with periodic await_job polling over a single long await.",
@@ -138,39 +118,27 @@ function executeBashInBackground(
138
118
 
139
119
  let timedOut = false;
140
120
  let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
141
- let sigkillHandle: ReturnType<typeof setTimeout> | undefined;
142
121
  let hardDeadlineHandle: ReturnType<typeof setTimeout> | undefined;
143
122
 
144
- /** Grace period (ms) between SIGTERM and SIGKILL. */
145
- const SIGKILL_GRACE_MS = 5_000;
146
- /** Hard deadline (ms) after SIGKILL to force-resolve the promise. */
147
- const HARD_DEADLINE_MS = 3_000;
148
-
149
123
  if (timeout !== undefined && timeout > 0) {
150
124
  timeoutHandle = setTimeout(() => {
151
125
  timedOut = true;
152
- if (child.pid) killTree(child.pid);
153
-
154
- // If the process ignores SIGTERM, escalate to SIGKILL
155
- sigkillHandle = setTimeout(() => {
156
- if (child.pid) {
157
- // killTree already uses taskkill /F /T on Windows
158
- killTree(child.pid);
159
- }
160
-
161
- // Hard deadline: if even SIGKILL doesn't trigger 'close',
162
- // force-resolve so the job doesn't hang forever (#2186).
163
- hardDeadlineHandle = setTimeout(() => {
164
- const output = Buffer.concat(chunks).toString("utf-8");
165
- safeResolve(
166
- output
167
- ? `${output}\n\nCommand timed out after ${timeout} seconds (force-killed)`
168
- : `Command timed out after ${timeout} seconds (force-killed)`,
169
- );
170
- }, HARD_DEADLINE_MS);
171
- if (typeof hardDeadlineHandle === "object" && "unref" in hardDeadlineHandle) hardDeadlineHandle.unref();
172
- }, SIGKILL_GRACE_MS);
173
- if (typeof sigkillHandle === "object" && "unref" in sigkillHandle) sigkillHandle.unref();
126
+ // killProcessTree owns the SIGTERM -> grace -> SIGKILL escalation, so a
127
+ // SIGTERM-immune child is actually force-killed rather than left running.
128
+ if (child.pid) killProcessTree(child.pid);
129
+
130
+ // Hard deadline: a D-state (uninterruptible-I/O) child never emits 'close'
131
+ // even after SIGKILL, so force-resolve the promise rather than hang (#2186).
132
+ // Fires after the full grace window so SIGKILL has had its chance first.
133
+ hardDeadlineHandle = setTimeout(() => {
134
+ const output = Buffer.concat(chunks).toString("utf-8");
135
+ safeResolve(
136
+ output
137
+ ? `${output}\n\nCommand timed out after ${timeout} seconds (force-killed)`
138
+ : `Command timed out after ${timeout} seconds (force-killed)`,
139
+ );
140
+ }, SIGKILL_GRACE_MS + HARD_DEADLINE_MS);
141
+ if (typeof hardDeadlineHandle === "object" && "unref" in hardDeadlineHandle) hardDeadlineHandle.unref();
174
142
  }, timeout * 1000);
175
143
  }
176
144
 
@@ -202,7 +170,18 @@ function executeBashInBackground(
202
170
  if (child.stderr) child.stderr.on("data", onData);
203
171
 
204
172
  const onAbort = () => {
205
- if (child.pid) killTree(child.pid);
173
+ if (child.pid) killProcessTree(child.pid);
174
+ // Arm the same hard-deadline force-resolve the timeout path uses, so a
175
+ // cancelled D-state child (never emits 'close' even after SIGKILL) can't
176
+ // hang the job promise forever. safeResolve is idempotent, so the real
177
+ // 'close' still wins if the child does exit.
178
+ if (!hardDeadlineHandle) {
179
+ hardDeadlineHandle = setTimeout(() => {
180
+ const output = Buffer.concat(chunks).toString("utf-8");
181
+ safeResolve(output ? `${output}\n\nCommand aborted (force-killed)` : "Command aborted (force-killed)");
182
+ }, SIGKILL_GRACE_MS + HARD_DEADLINE_MS);
183
+ if (typeof hardDeadlineHandle === "object" && "unref" in hardDeadlineHandle) hardDeadlineHandle.unref();
184
+ }
206
185
  };
207
186
 
208
187
  if (signal.aborted) {
@@ -213,7 +192,6 @@ function executeBashInBackground(
213
192
 
214
193
  child.on("error", (err) => {
215
194
  if (timeoutHandle) clearTimeout(timeoutHandle);
216
- if (sigkillHandle) clearTimeout(sigkillHandle);
217
195
  if (hardDeadlineHandle) clearTimeout(hardDeadlineHandle);
218
196
  signal.removeEventListener("abort", onAbort);
219
197
  safeReject(err);
@@ -221,7 +199,6 @@ function executeBashInBackground(
221
199
 
222
200
  child.on("close", (code) => {
223
201
  if (timeoutHandle) clearTimeout(timeoutHandle);
224
- if (sigkillHandle) clearTimeout(sigkillHandle);
225
202
  if (hardDeadlineHandle) clearTimeout(hardDeadlineHandle);
226
203
  signal.removeEventListener("abort", onAbort);
227
204
  if (spillStream) spillStream.end();
@@ -177,6 +177,145 @@ test("await_job suppresses follow-up for already-completed jobs (cross-turn case
177
177
  manager.shutdown();
178
178
  });
179
179
 
180
+ test("await_job aborts promptly when signal fires; job keeps running and is not suppressed", async () => {
181
+ const manager = new AsyncJobManager();
182
+ const tool = createAwaitTool(() => manager);
183
+
184
+ // Register a job that would run for 60s
185
+ const jobId = manager.register("bash", "long-job", async (_signal) => {
186
+ return new Promise<string>((resolve) => {
187
+ const timer = setTimeout(() => resolve("finally done"), 60_000);
188
+ if (typeof timer === "object" && "unref" in timer) timer.unref();
189
+ });
190
+ });
191
+
192
+ const ac = new AbortController();
193
+ // Abort after ~100ms
194
+ const abortTimer = setTimeout(() => ac.abort(), 100);
195
+ if (typeof abortTimer === "object" && "unref" in abortTimer) (abortTimer as NodeJS.Timeout).unref();
196
+
197
+ const start = Date.now();
198
+ const result = await tool.execute("tc_abort1", { jobs: [jobId], timeout: 60 }, ac.signal, () => {}, undefined as never);
199
+ const elapsed = Date.now() - start;
200
+ const text = getTextFromResult(result);
201
+
202
+ // Should abort quickly (within ~100ms + overhead), not wait for full timeout
203
+ assert.ok(elapsed < 5_000, `Expected abort in ~100ms but took ${elapsed}ms`);
204
+ assert.match(text, /interrupted/i);
205
+
206
+ // Job should still be running (not killed)
207
+ const job = manager.getJob(jobId)!;
208
+ assert.equal(job.status, "running", "Job should still be running after abort");
209
+ // Job should NOT be marked awaited — results must resurface later
210
+ assert.ok(!job.awaited, "Job should not be suppressed after abort");
211
+
212
+ // Cleanup
213
+ manager.cancel(jobId);
214
+ manager.shutdown();
215
+ });
216
+
217
+ test("after abort, a still-running job resurfaces via onJobComplete", async () => {
218
+ const followUps: string[] = [];
219
+ const manager = new AsyncJobManager({
220
+ onJobComplete: (job) => {
221
+ if (!job.awaited) followUps.push(job.id);
222
+ },
223
+ });
224
+ const tool = createAwaitTool(() => manager);
225
+
226
+ // Register a job that resolves after ~150ms
227
+ const jobId = manager.register("bash", "resurface-job", async () => {
228
+ return new Promise<string>((resolve) => {
229
+ const timer = setTimeout(() => resolve("done"), 150);
230
+ if (typeof timer === "object" && "unref" in timer) (timer as NodeJS.Timeout).unref();
231
+ });
232
+ });
233
+
234
+ const ac = new AbortController();
235
+ // Abort the await at ~50ms (before the job completes at ~150ms)
236
+ const abortTimer = setTimeout(() => ac.abort(), 50);
237
+ if (typeof abortTimer === "object" && "unref" in abortTimer) (abortTimer as NodeJS.Timeout).unref();
238
+
239
+ const result = await tool.execute("tc_abort2", { jobs: [jobId], timeout: 10 }, ac.signal, () => {}, undefined as never);
240
+ const text = getTextFromResult(result);
241
+ assert.match(text, /interrupted/i);
242
+
243
+ // Wait longer than the job's natural completion time (150ms) + delivery timer (0ms) + buffer
244
+ await new Promise((r) => setTimeout(r, 350));
245
+
246
+ // The job should have completed and the follow-up should have fired
247
+ // (because we did NOT suppress it on abort)
248
+ assert.ok(followUps.includes(jobId), `Expected job ${jobId} to resurface via onJobComplete, but got: ${followUps.join(", ")}`);
249
+
250
+ manager.shutdown();
251
+ });
252
+
253
+ test("await_job does not reprint a job whose follow-up was already delivered (no duplicate-in-context)", async () => {
254
+ // Real cross-turn timing: a job completes in a prior turn, its setTimeout(0)
255
+ // follow-up FIRES (delivered to context), and only THEN does await_job run in a
256
+ // later turn. The earlier #3787 test masked this by advancing with setImmediate,
257
+ // which races ahead of setTimeout(0); a real turn boundary does not. await_job
258
+ // must acknowledge the already-delivered job tersely instead of reprinting its
259
+ // full output (which would duplicate it in context).
260
+ const followUps: string[] = [];
261
+ const manager = new AsyncJobManager({
262
+ onJobComplete: (job) => {
263
+ if (!job.awaited) followUps.push(job.id);
264
+ },
265
+ });
266
+ const tool = createAwaitTool(() => manager);
267
+
268
+ const jobId = manager.register("bash", "already-delivered-job", async () => "THE_RESULT_TEXT");
269
+ await manager.getJob(jobId)!.promise;
270
+
271
+ // Real macrotask gap — generously long so the setTimeout(0) delivery fires even
272
+ // if the CI event loop is briefly starved (a tight 25ms could race the precondition).
273
+ await new Promise((r) => setTimeout(r, 100));
274
+ assert.equal(followUps.length, 1, "follow-up should have been delivered before the later-turn await_job");
275
+
276
+ const result = await tool.execute("tc_dup", { jobs: [jobId] }, noopSignal, () => {}, undefined as never);
277
+ const text = getTextFromResult(result);
278
+
279
+ // The full output must NOT be reprinted (it is already in context via the follow-up).
280
+ assert.ok(
281
+ !text.includes("THE_RESULT_TEXT"),
282
+ `await_job must not reprint already-delivered output, got:\n${text}`,
283
+ );
284
+ // It should still acknowledge the job so the agent knows the wait resolved.
285
+ assert.match(text, /already-delivered-job/);
286
+ // Single already-delivered job must use grammatical singular wording, not
287
+ // the plural form (regression: "These job ... their results ... they completed").
288
+ assert.match(text, /This job already finished and its result was shown above when it completed/);
289
+ assert.doesNotMatch(text, /These job\b/);
290
+
291
+ manager.shutdown();
292
+ });
293
+
294
+ test("await_job still renders full output for a job consumed within the same turn (not yet delivered)", async () => {
295
+ // Within-turn case: await_job wins the race against the delivery timer, so the
296
+ // follow-up is suppressed and never delivered. Here await_job IS the only place
297
+ // the result surfaces, so it must render the full output.
298
+ const followUps: string[] = [];
299
+ const manager = new AsyncJobManager({
300
+ onJobComplete: (job) => {
301
+ if (!job.awaited) followUps.push(job.id);
302
+ },
303
+ });
304
+ const tool = createAwaitTool(() => manager);
305
+
306
+ const jobId = manager.register("bash", "within-turn-job", async () => {
307
+ return new Promise<string>((resolve) => setTimeout(() => resolve("WITHIN_TURN_OUTPUT"), 40));
308
+ });
309
+
310
+ const result = await tool.execute("tc_within", { jobs: [jobId] }, noopSignal, () => {}, undefined as never);
311
+ const text = getTextFromResult(result);
312
+
313
+ assert.equal(followUps.length, 0, "within-turn await must suppress the follow-up");
314
+ assert.match(text, /WITHIN_TURN_OUTPUT/, "within-turn await must render the full output inline");
315
+
316
+ manager.shutdown();
317
+ });
318
+
180
319
  test("unawaited jobs still get follow-up delivery (#2248)", async () => {
181
320
  const followUps: string[] = [];
182
321
  const manager = new AsyncJobManager({