@juspay/shooter 1.21.0 → 1.22.0

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 (207) hide show
  1. package/.claude/hooks/notifier.cjs +94 -1
  2. package/build/client/_app/immutable/assets/2.JWRrnR-w.css +1 -0
  3. package/build/client/_app/immutable/assets/2.JWRrnR-w.css.br +0 -0
  4. package/build/client/_app/immutable/assets/2.JWRrnR-w.css.gz +0 -0
  5. package/build/client/_app/immutable/chunks/BfbPKMXz.js +3 -0
  6. package/build/client/_app/immutable/chunks/BfbPKMXz.js.br +0 -0
  7. package/build/client/_app/immutable/chunks/BfbPKMXz.js.gz +0 -0
  8. package/build/client/_app/immutable/chunks/C4Hns_Wl.js +1 -0
  9. package/build/client/_app/immutable/chunks/C4Hns_Wl.js.br +0 -0
  10. package/build/client/_app/immutable/chunks/C4Hns_Wl.js.gz +0 -0
  11. package/build/client/_app/immutable/chunks/{BmfLecb1.js → CZg4kn4E.js} +1 -1
  12. package/build/client/_app/immutable/chunks/CZg4kn4E.js.br +0 -0
  13. package/build/client/_app/immutable/chunks/CZg4kn4E.js.gz +0 -0
  14. package/build/client/_app/immutable/chunks/{EqMAkEha.js → DhK7PwI_.js} +1 -1
  15. package/build/client/_app/immutable/chunks/DhK7PwI_.js.br +0 -0
  16. package/build/client/_app/immutable/chunks/DhK7PwI_.js.gz +0 -0
  17. package/build/client/_app/immutable/entry/{app.CeSxgGat.js → app.CTqz33nP.js} +2 -2
  18. package/build/client/_app/immutable/entry/app.CTqz33nP.js.br +0 -0
  19. package/build/client/_app/immutable/entry/app.CTqz33nP.js.gz +0 -0
  20. package/build/client/_app/immutable/entry/start.Dj-Kvgwo.js +1 -0
  21. package/build/client/_app/immutable/entry/start.Dj-Kvgwo.js.br +2 -0
  22. package/build/client/_app/immutable/entry/start.Dj-Kvgwo.js.gz +0 -0
  23. package/build/client/_app/immutable/nodes/{0.oaPwxh1O.js → 0.Qn7Ktiht.js} +1 -1
  24. package/build/client/_app/immutable/nodes/0.Qn7Ktiht.js.br +0 -0
  25. package/build/client/_app/immutable/nodes/0.Qn7Ktiht.js.gz +0 -0
  26. package/build/client/_app/immutable/nodes/{1.DMPyoM-M.js → 1.BxWOfNlo.js} +1 -1
  27. package/build/client/_app/immutable/nodes/1.BxWOfNlo.js.br +0 -0
  28. package/build/client/_app/immutable/nodes/1.BxWOfNlo.js.gz +0 -0
  29. package/build/client/_app/immutable/nodes/{10.Cbm7nQKK.js → 10.BGPYD1s1.js} +1 -1
  30. package/build/client/_app/immutable/nodes/10.BGPYD1s1.js.br +0 -0
  31. package/build/client/_app/immutable/nodes/10.BGPYD1s1.js.gz +0 -0
  32. package/build/client/_app/immutable/nodes/{11.CKmZjP_a.js → 11.BxY1PUjC.js} +1 -1
  33. package/build/client/_app/immutable/nodes/11.BxY1PUjC.js.br +0 -0
  34. package/build/client/_app/immutable/nodes/{11.CKmZjP_a.js.gz → 11.BxY1PUjC.js.gz} +0 -0
  35. package/build/client/_app/immutable/nodes/2.Bc2qALkX.js +23 -0
  36. package/build/client/_app/immutable/nodes/2.Bc2qALkX.js.br +0 -0
  37. package/build/client/_app/immutable/nodes/2.Bc2qALkX.js.gz +0 -0
  38. package/build/client/_app/immutable/nodes/{3.BgLpGnzb.js → 3.N2-A8noI.js} +1 -1
  39. package/build/client/_app/immutable/nodes/3.N2-A8noI.js.br +0 -0
  40. package/build/client/_app/immutable/nodes/3.N2-A8noI.js.gz +0 -0
  41. package/build/client/_app/immutable/nodes/{5.Avc1-gVb.js → 5.DziEu9rx.js} +1 -1
  42. package/build/client/_app/immutable/nodes/5.DziEu9rx.js.br +0 -0
  43. package/build/client/_app/immutable/nodes/5.DziEu9rx.js.gz +0 -0
  44. package/build/client/_app/immutable/nodes/{6.Dw2wEssJ.js → 6.BWF9Qx6F.js} +1 -1
  45. package/build/client/_app/immutable/nodes/6.BWF9Qx6F.js.br +0 -0
  46. package/build/client/_app/immutable/nodes/6.BWF9Qx6F.js.gz +0 -0
  47. package/build/client/_app/immutable/nodes/{7.DwKZjoBg.js → 7.DHuDIdpz.js} +1 -1
  48. package/build/client/_app/immutable/nodes/7.DHuDIdpz.js.br +0 -0
  49. package/build/client/_app/immutable/nodes/7.DHuDIdpz.js.gz +0 -0
  50. package/build/client/_app/immutable/nodes/{8.ZUAI6g5E.js → 8.D0Ijt9Vv.js} +1 -1
  51. package/build/client/_app/immutable/nodes/8.D0Ijt9Vv.js.br +0 -0
  52. package/build/client/_app/immutable/nodes/8.D0Ijt9Vv.js.gz +0 -0
  53. package/build/client/_app/immutable/nodes/{9.I_KGXPwB.js → 9.2Piwo35J.js} +1 -1
  54. package/build/client/_app/immutable/nodes/9.2Piwo35J.js.br +0 -0
  55. package/build/client/_app/immutable/nodes/9.2Piwo35J.js.gz +0 -0
  56. package/build/client/_app/version.json +1 -1
  57. package/build/client/_app/version.json.br +0 -0
  58. package/build/client/_app/version.json.gz +0 -0
  59. package/build/pty-holder.cjs +6 -0
  60. package/build/server/chunks/{0-vrTNAfZB.js → 0-CVGsyVKN.js} +2 -2
  61. package/build/server/chunks/{0-vrTNAfZB.js.map → 0-CVGsyVKN.js.map} +1 -1
  62. package/build/server/chunks/{1-nbr-bOoF.js → 1-BAlAsKdp.js} +2 -2
  63. package/build/server/chunks/{1-nbr-bOoF.js.map → 1-BAlAsKdp.js.map} +1 -1
  64. package/build/server/chunks/{10-ChyvvJ6w.js → 10-BUCX7Aqz.js} +2 -2
  65. package/build/server/chunks/{10-ChyvvJ6w.js.map → 10-BUCX7Aqz.js.map} +1 -1
  66. package/build/server/chunks/{11-6ZAjL3uU.js → 11-DHPvc2yA.js} +2 -2
  67. package/build/server/chunks/{11-6ZAjL3uU.js.map → 11-DHPvc2yA.js.map} +1 -1
  68. package/build/server/chunks/{2-DWFRVDWJ.js → 2-DLOMdCHW.js} +4 -4
  69. package/build/server/chunks/{2-DWFRVDWJ.js.map → 2-DLOMdCHW.js.map} +1 -1
  70. package/build/server/chunks/{3-CKANM_WM.js → 3-DCf69LYo.js} +2 -2
  71. package/build/server/chunks/{3-CKANM_WM.js.map → 3-DCf69LYo.js.map} +1 -1
  72. package/build/server/chunks/{5-BxVjs2qi.js → 5-D-Uv1voC.js} +2 -2
  73. package/build/server/chunks/{5-BxVjs2qi.js.map → 5-D-Uv1voC.js.map} +1 -1
  74. package/build/server/chunks/{6-Cbf1AAMQ.js → 6-DUrC2Naz.js} +2 -2
  75. package/build/server/chunks/{6-Cbf1AAMQ.js.map → 6-DUrC2Naz.js.map} +1 -1
  76. package/build/server/chunks/{7-CMK2quEf.js → 7-TXwjMHt2.js} +2 -2
  77. package/build/server/chunks/{7-CMK2quEf.js.map → 7-TXwjMHt2.js.map} +1 -1
  78. package/build/server/chunks/{8-DhdfkfDM.js → 8-D2X_jBsT.js} +2 -2
  79. package/build/server/chunks/{8-DhdfkfDM.js.map → 8-D2X_jBsT.js.map} +1 -1
  80. package/build/server/chunks/{9-CPpxtRM5.js → 9-DK0hH5Xa.js} +2 -2
  81. package/build/server/chunks/{9-CPpxtRM5.js.map → 9-DK0hH5Xa.js.map} +1 -1
  82. package/build/server/chunks/_page.svelte-8OFzwdNA.js +758 -0
  83. package/build/server/chunks/_page.svelte-8OFzwdNA.js.map +1 -0
  84. package/build/server/chunks/{_server.ts-BWVlO8iV.js → _server.ts-05JJOdcX.js} +15 -12
  85. package/build/server/chunks/_server.ts-05JJOdcX.js.map +1 -0
  86. package/build/server/chunks/{_server.ts-CC2K8-L2.js → _server.ts-B54Pvhgc.js} +3 -2
  87. package/build/server/chunks/_server.ts-B54Pvhgc.js.map +1 -0
  88. package/build/server/chunks/{_server.ts-BevnuePu.js → _server.ts-BCljU9Sg.js} +7 -3
  89. package/build/server/chunks/_server.ts-BCljU9Sg.js.map +1 -0
  90. package/build/server/chunks/{_server.ts-D-vgx5UZ.js → _server.ts-BTmknWpO.js} +2 -2
  91. package/build/server/chunks/{_server.ts-D-vgx5UZ.js.map → _server.ts-BTmknWpO.js.map} +1 -1
  92. package/build/server/chunks/{_server.ts-tChyh9FX.js → _server.ts-BXhmLZwN.js} +4 -2
  93. package/build/server/chunks/{_server.ts-tChyh9FX.js.map → _server.ts-BXhmLZwN.js.map} +1 -1
  94. package/build/server/chunks/{_server.ts-CvJKTS3Z.js → _server.ts-BbRSpB74.js} +4 -2
  95. package/build/server/chunks/{_server.ts-CvJKTS3Z.js.map → _server.ts-BbRSpB74.js.map} +1 -1
  96. package/build/server/chunks/{_server.ts-Dp-hXW_I.js → _server.ts-Bol54_Qo.js} +3 -2
  97. package/build/server/chunks/_server.ts-Bol54_Qo.js.map +1 -0
  98. package/build/server/chunks/{_server.ts-X1R7L_QI.js → _server.ts-C0PO_cAu.js} +3 -2
  99. package/build/server/chunks/{_server.ts-X1R7L_QI.js.map → _server.ts-C0PO_cAu.js.map} +1 -1
  100. package/build/server/chunks/_server.ts-C6NRpe7e.js +33 -0
  101. package/build/server/chunks/_server.ts-C6NRpe7e.js.map +1 -0
  102. package/build/server/chunks/_server.ts-CGqCOCdK.js +53 -0
  103. package/build/server/chunks/_server.ts-CGqCOCdK.js.map +1 -0
  104. package/build/server/chunks/{_server.ts-CA5KUENM.js → _server.ts-CZb-BI5H.js} +3 -2
  105. package/build/server/chunks/_server.ts-CZb-BI5H.js.map +1 -0
  106. package/build/server/chunks/{_server.ts-VzDcFFgy.js → _server.ts-DPHRUFYS.js} +4 -2
  107. package/build/server/chunks/_server.ts-DPHRUFYS.js.map +1 -0
  108. package/build/server/chunks/{_server.ts-D0zRDSx0.js → _server.ts-D_WRex0k.js} +4 -2
  109. package/build/server/chunks/_server.ts-D_WRex0k.js.map +1 -0
  110. package/build/server/chunks/{_server.ts-CD7JP3fz.js → _server.ts-DiBMY7Ho.js} +3 -2
  111. package/build/server/chunks/_server.ts-DiBMY7Ho.js.map +1 -0
  112. package/build/server/chunks/{library-apns-Dl3iRE2h.js → library-apns-D8RPINlv.js} +62 -7
  113. package/build/server/chunks/library-apns-D8RPINlv.js.map +1 -0
  114. package/build/server/chunks/{pending-requests-C9p57WoU.js → pending-requests-8rWjrF6d.js} +3 -2
  115. package/build/server/chunks/pending-requests-8rWjrF6d.js.map +1 -0
  116. package/build/server/chunks/presence-store-Bx_g0-Gd.js +23 -0
  117. package/build/server/chunks/presence-store-Bx_g0-Gd.js.map +1 -0
  118. package/build/server/chunks/{pty-manager-ZqXqa-6A.js → pty-manager-CoWVT56F.js} +26 -5
  119. package/build/server/chunks/pty-manager-CoWVT56F.js.map +1 -0
  120. package/build/server/chunks/shooter-home-4f_HkdGI.js +10 -0
  121. package/build/server/chunks/shooter-home-4f_HkdGI.js.map +1 -0
  122. package/build/server/index.js +1 -1
  123. package/build/server/index.js.map +1 -1
  124. package/build/server/manifest.js +38 -24
  125. package/build/server/manifest.js.map +1 -1
  126. package/package.json +2 -2
  127. package/src/lib/modules/client/common/index.ts +1 -0
  128. package/src/lib/modules/client/common/presence.ts +47 -0
  129. package/src/lib/modules/client/dashboard/AutopilotPanel.svelte +188 -4
  130. package/src/lib/modules/client/dashboard/autopilot-driver.svelte.ts +681 -0
  131. package/src/lib/modules/client/dashboard/decide-injection.ts +127 -0
  132. package/src/lib/modules/client/dashboard/store.svelte.ts +65 -24
  133. package/src/lib/modules/client/neurolink/fetch-proxy.ts +38 -1
  134. package/src/lib/modules/server/apn/apns-payload.ts +50 -0
  135. package/src/lib/modules/server/apn/library-apns.ts +50 -8
  136. package/src/lib/modules/server/apn/pending-requests.ts +3 -1
  137. package/src/lib/modules/server/sessions/autopilot-context.ts +57 -0
  138. package/src/lib/modules/server/sessions/autopilot-engine.ts +148 -43
  139. package/src/lib/modules/server/sessions/litellm-client.ts +90 -34
  140. package/src/lib/modules/server/sessions/next-step-consensus.ts +27 -2
  141. package/src/lib/modules/server/sessions/summary-store.ts +3 -1
  142. package/src/lib/modules/server/terminal/agent-launch.ts +26 -0
  143. package/src/lib/modules/server/terminal/pty-holder.cjs +6 -0
  144. package/src/lib/modules/server/terminal/pty-manager.ts +13 -3
  145. package/src/lib/modules/server/terminal/session-watcher.ts +12 -2
  146. package/src/lib/modules/server/terminal/terminal-store.ts +3 -1
  147. package/src/lib/modules/server/utils/shooter-home.ts +16 -0
  148. package/src/lib/modules/server/ws/presence-store.ts +50 -0
  149. package/src/lib/types/autopilot.ts +65 -0
  150. package/src/routes/api/autopilot/goal/+server.ts +72 -0
  151. package/src/routes/api/neurolink-proxy/+server.ts +8 -2
  152. package/src/routes/api/notify/+server.ts +22 -15
  153. package/src/routes/api/presence/+server.ts +39 -0
  154. package/src/routes/api/ws-status/+server.ts +8 -5
  155. package/build/client/_app/immutable/assets/2.BHi6pjT2.css +0 -1
  156. package/build/client/_app/immutable/assets/2.BHi6pjT2.css.br +0 -0
  157. package/build/client/_app/immutable/assets/2.BHi6pjT2.css.gz +0 -0
  158. package/build/client/_app/immutable/chunks/BmfLecb1.js.br +0 -0
  159. package/build/client/_app/immutable/chunks/BmfLecb1.js.gz +0 -0
  160. package/build/client/_app/immutable/chunks/CRkG7oE4.js +0 -1
  161. package/build/client/_app/immutable/chunks/CRkG7oE4.js.br +0 -0
  162. package/build/client/_app/immutable/chunks/CRkG7oE4.js.gz +0 -0
  163. package/build/client/_app/immutable/chunks/DOEXXmsh.js +0 -3
  164. package/build/client/_app/immutable/chunks/DOEXXmsh.js.br +0 -0
  165. package/build/client/_app/immutable/chunks/DOEXXmsh.js.gz +0 -0
  166. package/build/client/_app/immutable/chunks/EqMAkEha.js.br +0 -0
  167. package/build/client/_app/immutable/chunks/EqMAkEha.js.gz +0 -0
  168. package/build/client/_app/immutable/entry/app.CeSxgGat.js.br +0 -0
  169. package/build/client/_app/immutable/entry/app.CeSxgGat.js.gz +0 -0
  170. package/build/client/_app/immutable/entry/start.DrnJFwxA.js +0 -1
  171. package/build/client/_app/immutable/entry/start.DrnJFwxA.js.br +0 -2
  172. package/build/client/_app/immutable/entry/start.DrnJFwxA.js.gz +0 -0
  173. package/build/client/_app/immutable/nodes/0.oaPwxh1O.js.br +0 -0
  174. package/build/client/_app/immutable/nodes/0.oaPwxh1O.js.gz +0 -0
  175. package/build/client/_app/immutable/nodes/1.DMPyoM-M.js.br +0 -0
  176. package/build/client/_app/immutable/nodes/1.DMPyoM-M.js.gz +0 -0
  177. package/build/client/_app/immutable/nodes/10.Cbm7nQKK.js.br +0 -0
  178. package/build/client/_app/immutable/nodes/10.Cbm7nQKK.js.gz +0 -0
  179. package/build/client/_app/immutable/nodes/11.CKmZjP_a.js.br +0 -0
  180. package/build/client/_app/immutable/nodes/2.zlrdNFtH.js +0 -13
  181. package/build/client/_app/immutable/nodes/2.zlrdNFtH.js.br +0 -0
  182. package/build/client/_app/immutable/nodes/2.zlrdNFtH.js.gz +0 -0
  183. package/build/client/_app/immutable/nodes/3.BgLpGnzb.js.br +0 -0
  184. package/build/client/_app/immutable/nodes/3.BgLpGnzb.js.gz +0 -0
  185. package/build/client/_app/immutable/nodes/5.Avc1-gVb.js.br +0 -0
  186. package/build/client/_app/immutable/nodes/5.Avc1-gVb.js.gz +0 -0
  187. package/build/client/_app/immutable/nodes/6.Dw2wEssJ.js.br +0 -0
  188. package/build/client/_app/immutable/nodes/6.Dw2wEssJ.js.gz +0 -0
  189. package/build/client/_app/immutable/nodes/7.DwKZjoBg.js.br +0 -0
  190. package/build/client/_app/immutable/nodes/7.DwKZjoBg.js.gz +0 -0
  191. package/build/client/_app/immutable/nodes/8.ZUAI6g5E.js.br +0 -0
  192. package/build/client/_app/immutable/nodes/8.ZUAI6g5E.js.gz +0 -0
  193. package/build/client/_app/immutable/nodes/9.I_KGXPwB.js.br +0 -0
  194. package/build/client/_app/immutable/nodes/9.I_KGXPwB.js.gz +0 -0
  195. package/build/server/chunks/_page.svelte-tBuIq8Pg.js +0 -159
  196. package/build/server/chunks/_page.svelte-tBuIq8Pg.js.map +0 -1
  197. package/build/server/chunks/_server.ts-BWVlO8iV.js.map +0 -1
  198. package/build/server/chunks/_server.ts-BevnuePu.js.map +0 -1
  199. package/build/server/chunks/_server.ts-CA5KUENM.js.map +0 -1
  200. package/build/server/chunks/_server.ts-CC2K8-L2.js.map +0 -1
  201. package/build/server/chunks/_server.ts-CD7JP3fz.js.map +0 -1
  202. package/build/server/chunks/_server.ts-D0zRDSx0.js.map +0 -1
  203. package/build/server/chunks/_server.ts-Dp-hXW_I.js.map +0 -1
  204. package/build/server/chunks/_server.ts-VzDcFFgy.js.map +0 -1
  205. package/build/server/chunks/library-apns-Dl3iRE2h.js.map +0 -1
  206. package/build/server/chunks/pending-requests-C9p57WoU.js.map +0 -1
  207. package/build/server/chunks/pty-manager-ZqXqa-6A.js.map +0 -1
@@ -0,0 +1,127 @@
1
+ // Pure, deterministic auto-inject safety logic for the phone-resident autonomous loop.
2
+ // No I/O, no LLM. Fully unit-testable (tests/decide-injection.test.cjs).
3
+ // See docs/superpowers/specs/2026-06-01-phone-autonomous-agent-design.md
4
+ // §"Auto-inject safety model".
5
+
6
+ import type {
7
+ CommandVerdict,
8
+ ConsensusResult,
9
+ GateDecision,
10
+ InjectionPolicy,
11
+ InjectionState,
12
+ } from '$lib/types';
13
+
14
+ /** Default thresholds. Conservative — auto-inject is dangerous, so err toward NOT acting. */
15
+ export const DEFAULT_INJECTION_POLICY: InjectionPolicy = {
16
+ humanGraceMs: 5_000,
17
+ injectConfidence: 0.7,
18
+ maxAutoActions: 8,
19
+ minIntervalMs: 30_000,
20
+ };
21
+
22
+ /** Max length of a single injected command. Longer → rejected (likely not one command). */
23
+ const MAX_COMMAND_LENGTH = 400;
24
+
25
+ /**
26
+ * Coarse dangerous-payload patterns. This is a SEATBELT, not a security boundary:
27
+ * auto-inject is the user's accepted risk. We only block the obviously catastrophic.
28
+ */
29
+ const DANGEROUS_PATTERNS: readonly RegExp[] = [
30
+ /\brm\s+-[a-z]*r[a-z]*f?\s+(\/|~|\/\*|\$home)/i, // rm -rf /, rm -rf ~, rm -rf /*
31
+ /\brm\s+-[a-z]*f[a-z]*r?\s+(\/|~|\/\*|\$home)/i, // rm -fr variants
32
+ /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/, // classic fork bomb :(){ :|:& };:
33
+ /\bdd\b[^\n]*\bof=\/dev\//i, // dd of=/dev/...
34
+ />\s*\/dev\/(sd|nvme|disk|hd)/i, // redirect onto a raw disk
35
+ /\bmkfs(\.[a-z0-9]+)?\b/i, // mkfs, mkfs.ext4, ...
36
+ /\b(shutdown|reboot|halt|poweroff)\b/i, // power state
37
+ ];
38
+
39
+ /**
40
+ * The GATE: given a per-terminal snapshot + the current consensus, decide whether the
41
+ * autonomous loop should act (produce + inject a command). Pure; `now` is passed in so
42
+ * the function stays deterministic and testable.
43
+ *
44
+ * Order matters — the first failing guard short-circuits with a reason.
45
+ */
46
+ export function decideInjection(
47
+ state: InjectionState,
48
+ consensus: ConsensusResult,
49
+ now: number,
50
+ policy: InjectionPolicy = DEFAULT_INJECTION_POLICY,
51
+ opts: { allowTentative?: boolean } = {}
52
+ ): GateDecision {
53
+ const top = consensus.steps[0];
54
+ if (!top) {
55
+ return { act: false, reason: 'no consensus step' };
56
+ }
57
+ // `tentative` = the 5 distinct lenses didn't reach quorum on the exact next step. That is a
58
+ // PRECISION filter, not a safety boundary (the real guards are the confidence floor, dedup,
59
+ // circuit breaker, and guardCommand). For AGENT terminals the injection is a natural-language
60
+ // PROMPT the agent re-grounds against its goal — not a raw command run verbatim — so a best-
61
+ // ranked-but-tentative next step is fine and keeps the autonomous loop live instead of stalling
62
+ // after step 1. Callers pass allowTentative only for agent terminals; shell terminals stay strict.
63
+ if (top.tentative && !opts.allowTentative) {
64
+ return { act: false, reason: 'consensus is tentative (no quorum)' };
65
+ }
66
+ if (!state.isManaged) {
67
+ return { act: false, reason: 'terminal is external / read-only (cannot inject)' };
68
+ }
69
+ if (state.lastEventType !== 'agent-idle') {
70
+ return { act: false, reason: `agent not idle (last event: ${state.lastEventType})` };
71
+ }
72
+ if (now - state.lastActivityAt < policy.humanGraceMs) {
73
+ return { act: false, reason: 'recent activity — within human grace window' };
74
+ }
75
+ if (now - state.lastInjectedAt < policy.minIntervalMs) {
76
+ return { act: false, reason: 'rate-limited (min inject interval)' };
77
+ }
78
+ if (state.autoActionCount >= policy.maxAutoActions) {
79
+ return { act: false, reason: 'circuit breaker — max consecutive auto-actions reached' };
80
+ }
81
+ if (top.confidence < policy.injectConfidence) {
82
+ return { act: false, reason: `confidence ${top.confidence.toFixed(2)} below floor` };
83
+ }
84
+ if (state.lastActedStep !== null && normalizeStep(top.text) === state.lastActedStep) {
85
+ return { act: false, reason: 'already acted on this step' };
86
+ }
87
+ return { act: true, reason: 'idle + high-confidence consensus', step: top };
88
+ }
89
+
90
+ /**
91
+ * Vet a CONCRETE command (produced by the decide step) before it is written to the PTY:
92
+ * single-line, length-bounded, not obviously catastrophic, not a duplicate of the last one.
93
+ */
94
+ export function guardCommand(command: string, lastInjectedCommand: null | string): CommandVerdict {
95
+ const trimmed = command.trim();
96
+ if (trimmed.length === 0) {
97
+ return { command: trimmed, reason: 'empty command', safe: false };
98
+ }
99
+ if (/[\r\n]/.test(command)) {
100
+ return {
101
+ command: trimmed,
102
+ reason: 'multi-line command rejected (single command only)',
103
+ safe: false,
104
+ };
105
+ }
106
+ if (trimmed.length > MAX_COMMAND_LENGTH) {
107
+ return { command: trimmed, reason: `command too long (> ${MAX_COMMAND_LENGTH})`, safe: false };
108
+ }
109
+ for (const pattern of DANGEROUS_PATTERNS) {
110
+ if (pattern.test(trimmed)) {
111
+ return { command: trimmed, reason: 'matches a dangerous-command pattern', safe: false };
112
+ }
113
+ }
114
+ if (lastInjectedCommand !== null && trimmed === lastInjectedCommand.trim()) {
115
+ return { command: trimmed, reason: 'duplicate of last injected command', safe: false };
116
+ }
117
+ return { command: trimmed, reason: 'ok', safe: true };
118
+ }
119
+
120
+ /** Normalize a step text for dedup comparison (mirror of the consensus normalizer). */
121
+ export function normalizeStep(text: string): string {
122
+ return text
123
+ .toLowerCase()
124
+ .trim()
125
+ .replace(/\s+/g, ' ')
126
+ .replace(/[.,;:!?]+$/, '');
127
+ }
@@ -238,6 +238,18 @@ async function connectWs(apiKey: string): Promise<void> {
238
238
  }
239
239
  }
240
240
 
241
+ // Claude Code injects these wrappers as "user" messages (slash-command caveats, system reminders,
242
+ // etc.); none is the real goal. Mirror of SYSTEM_TAG_PREFIXES in server/sessions/jsonl-reader.ts.
243
+ const HARNESS_TAG_PREFIXES = [
244
+ '<local-command',
245
+ '<command-name>',
246
+ '<command-message>',
247
+ '<command-args>',
248
+ '<system-reminder>',
249
+ '<task-notification>',
250
+ 'Caveat:',
251
+ ];
252
+
241
253
  function extractGoalText(content: string | { content?: string; type: string }[]): string {
242
254
  let text = '';
243
255
  if (typeof content === 'string') {
@@ -246,7 +258,10 @@ function extractGoalText(content: string | { content?: string; type: string }[])
246
258
  const textPart = content.find((p) => p.type === 'text');
247
259
  text = textPart?.content ?? '';
248
260
  }
249
- return text.slice(0, 200).trim();
261
+ const trimmed = text.slice(0, 200).trim();
262
+ // A harness wrapper is not the user's goal — skip it so the caller keeps scanning for the real one
263
+ // (this is what let "<local-command-caveat>Caveat: …" become the pinned goal).
264
+ return isHarnessText(trimmed) ? '' : trimmed;
250
265
  }
251
266
 
252
267
  async function fetchTerminals(apiKey: string): Promise<void> {
@@ -291,8 +306,6 @@ function getApiKey(): string {
291
306
  return '';
292
307
  }
293
308
 
294
- // -- Public API -----------------------------------------------------------
295
-
296
309
  function handleWsMessage(raw: RawEvent): void {
297
310
  const type = raw.type as string | undefined;
298
311
  if (!type || type === 'welcome') {
@@ -367,6 +380,12 @@ function handleWsMessage(raw: RawEvent): void {
367
380
  sessions = sortSessions(sessions);
368
381
  }
369
382
 
383
+ function isHarnessText(text: string): boolean {
384
+ return HARNESS_TAG_PREFIXES.some((p) => text.startsWith(p));
385
+ }
386
+
387
+ // -- Public API -----------------------------------------------------------
388
+
370
389
  function makeSessionState(t: DashboardTerminalRecord): SessionState {
371
390
  return {
372
391
  command: t.command,
@@ -435,14 +454,17 @@ function mergeSessions(
435
454
  if (prev.status !== 'error') {
436
455
  prev.status = mapStatus(t.status);
437
456
  }
438
- // Schedule goal extraction if still missing
439
- if (!prev.goal) {
457
+ // Schedule goal extraction if still missing — but never for an exited terminal (its socket
458
+ // would open, get a session-end, and the poll would keep reopening it).
459
+ if (!prev.goal && t.exitedAt === null && t.status !== 'exited') {
440
460
  void openSessionSocket(t.id);
441
461
  }
442
462
  } else {
443
463
  map.set(t.id, makeSessionState(t));
444
- // Open a session WS for goal extraction on newly discovered terminals
445
- void openSessionSocket(t.id);
464
+ // Open a session WS for goal extraction on newly discovered (live) terminals only
465
+ if (t.exitedAt === null && t.status !== 'exited') {
466
+ void openSessionSocket(t.id);
467
+ }
446
468
  }
447
469
  }
448
470
 
@@ -495,6 +517,12 @@ async function openSessionSocket(terminalId: string): Promise<void> {
495
517
  }
496
518
  const data = raw as Record<string, unknown>;
497
519
 
520
+ // Session ended — stop watching (no goal will ever arrive on this socket).
521
+ if (data.type === 'session-end') {
522
+ closeSessionSocket(terminalId);
523
+ return;
524
+ }
525
+
498
526
  // Check if we already have a goal for this terminal
499
527
  const currentSession = sessions.find((s) => s.terminalId === terminalId);
500
528
  const hasGoal = currentSession?.goal && currentSession.goal.length > 0;
@@ -517,25 +545,23 @@ async function openSessionSocket(terminalId: string): Promise<void> {
517
545
  `[DashboardStore] Found ${messages.length} messages, looking for first user message...`
518
546
  );
519
547
 
520
- // Find first user message and extract goal
548
+ // Find the FIRST non-harness user message and use it as the goal. Keep scanning past
549
+ // harness-only messages (slash-command caveats, system reminders) so they never become
550
+ // the goal — and do NOT close the socket when none is found yet: closing here made the
551
+ // 15s poll reopen it every cycle (the session-watcher churn).
521
552
  for (const m of messages) {
522
- if (m.role === 'user') {
523
- const goal = extractGoalText(m.content);
524
- console.log(
525
- `[DashboardStore] Extracted goal for ${terminalId}: "${goal.substring(0, 50)}..."`
526
- );
527
-
528
- if (goal) {
529
- updateSessionGoal(terminalId, goal);
530
- }
531
- // Goal extracted — close the socket, we no longer need it
553
+ if (m.role !== 'user') {
554
+ continue;
555
+ }
556
+ const goal = extractGoalText(m.content);
557
+ if (goal) {
558
+ updateSessionGoal(terminalId, goal);
559
+ syncEngineGoal(terminalId, goal);
532
560
  closeSessionSocket(terminalId);
533
561
  return;
534
562
  }
535
563
  }
536
-
537
- console.log(`[DashboardStore] No user message found in history for ${terminalId}`);
538
- // No user message yet — keep socket open for incoming messages
564
+ // No real user goal in history yet — keep the socket open for incoming live messages.
539
565
  return;
540
566
  }
541
567
 
@@ -546,10 +572,8 @@ async function openSessionSocket(terminalId: string): Promise<void> {
546
572
  data.content as string | { content?: string; type: string }[]
547
573
  );
548
574
  if (goal) {
549
- console.log(
550
- `[DashboardStore] Extracted goal from message for ${terminalId}: "${goal.substring(0, 50)}..."`
551
- );
552
575
  updateSessionGoal(terminalId, goal);
576
+ syncEngineGoal(terminalId, goal);
553
577
  closeSessionSocket(terminalId);
554
578
  }
555
579
  return;
@@ -625,6 +649,23 @@ function sortSessions(list: SessionState[]): SessionState[] {
625
649
  });
626
650
  }
627
651
 
652
+ // Push a freshly-extracted goal to the SERVER-side autopilot engine so its per-cycle LLM context is
653
+ // anchored to the real user intent. Without this the engine always ran goal-less: its goal store
654
+ // was only reachable via this route, which nothing ever called (the client only updated local
655
+ // state). Best-effort — the engine may not be running (503); the next extraction retries.
656
+ function syncEngineGoal(terminalId: string, goal: string): void {
657
+ if (!storedApiKey) {
658
+ return;
659
+ }
660
+ void fetch('/api/autopilot/goal', {
661
+ body: JSON.stringify({ goal, terminalId }),
662
+ headers: { Authorization: `Bearer ${storedApiKey}`, 'Content-Type': 'application/json' },
663
+ method: 'POST',
664
+ }).catch(() => {
665
+ // best-effort; nothing to do on failure
666
+ });
667
+ }
668
+
628
669
  function triggerSummarization(session: SessionState): void {
629
670
  session.isSummarizing = true;
630
671
 
@@ -25,7 +25,7 @@ export function installFetchProxy(): void {
25
25
  globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
26
26
  const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
27
27
 
28
- const provider = Object.entries(PROXY_PREFIXES).find(([prefix]) => url.startsWith(prefix))?.[1];
28
+ const provider = resolveProvider(url);
29
29
 
30
30
  if (!provider) {
31
31
  return originalFetch(input, init);
@@ -68,3 +68,40 @@ export function installFetchProxy(): void {
68
68
  });
69
69
  };
70
70
  }
71
+
72
+ /**
73
+ * The runtime LiteLLM base URL injected by the root layout into window.process.env, or ''.
74
+ * Read via window['process'] (bracket access) so the bundler does NOT constant-fold it: a
75
+ * direct `globalThis.process.env` gets frozen to Node's build-time env by Vite/Rollup, which
76
+ * never contains the runtime-injected value.
77
+ */
78
+ function litellmBaseUrl(): string {
79
+ if (typeof window === 'undefined') {
80
+ return '';
81
+ }
82
+ // eslint-disable-next-line @typescript-eslint/dot-notation -- bracket access is deliberate: it stops the bundler constant-folding process.env to the build-time value
83
+ const proc = (window as unknown as Record<string, unknown>)['process'] as
84
+ | undefined
85
+ | { env?: Record<string, string | undefined> };
86
+ const base = proc?.env?.LITELLM_BASE_URL;
87
+ return typeof base === 'string' ? base : '';
88
+ }
89
+
90
+ /**
91
+ * Resolve which proxy provider (if any) a URL routes through. Static cloud prefixes plus the
92
+ * runtime LiteLLM base URL — LiteLLM is self-hosted at a configurable origin, so its prefix
93
+ * is not known at build time and must be read from the injected env.
94
+ */
95
+ function resolveProvider(url: string): string | undefined {
96
+ const staticMatch = Object.entries(PROXY_PREFIXES).find(([prefix]) =>
97
+ url.startsWith(prefix)
98
+ )?.[1];
99
+ if (staticMatch) {
100
+ return staticMatch;
101
+ }
102
+ const base = litellmBaseUrl();
103
+ if (base && url.startsWith(base)) {
104
+ return 'litellm';
105
+ }
106
+ return undefined;
107
+ }
@@ -0,0 +1,50 @@
1
+ // Pure: cap an APNs JSON payload to APNs' size limit by truncating the alert body (then the
2
+ // subtitle) so a long agent message can't blow past the ~4 KB cap and 413 / fail to deliver.
3
+ // No imports — unit-tested in isolation (tests/apns-payload.test.cjs).
4
+
5
+ /** APNs caps an alert payload at 4096 bytes; leave headroom for transport framing. */
6
+ export const APNS_MAX_BYTES = 3900;
7
+
8
+ const ELLIPSIS = '…';
9
+
10
+ /**
11
+ * Shrink an APNs payload so its JSON fits under `maxBytes`. Truncates `alert.body` first (the
12
+ * agent's last message — the usual culprit), then `alert.subtitle`, marking cuts with an
13
+ * ellipsis. Mutates and returns `body`. Title and custom data are preserved.
14
+ */
15
+ export function fitApnsPayload(
16
+ body: Record<string, unknown>,
17
+ maxBytes: number = APNS_MAX_BYTES
18
+ ): Record<string, unknown> {
19
+ if (payloadBytes(body) <= maxBytes) {
20
+ return body;
21
+ }
22
+ const aps = body.aps as Record<string, unknown> | undefined;
23
+ const alert = aps?.alert as Record<string, unknown> | undefined;
24
+ if (!alert) {
25
+ return body; // nothing safely trimmable (e.g. silent push) — leave as-is
26
+ }
27
+ for (const field of ['body', 'subtitle'] as const) {
28
+ if (payloadBytes(body) <= maxBytes) {
29
+ break;
30
+ }
31
+ const value = alert[field];
32
+ if (typeof value !== 'string' || value.length === 0) {
33
+ continue;
34
+ }
35
+ let text = value;
36
+ while (payloadBytes(body) > maxBytes && text.length > 0) {
37
+ const cut = Math.max(1, Math.ceil(text.length * 0.12));
38
+ text = text.slice(0, text.length - cut);
39
+ // When a field is fully truncated away, OMIT it (undefined → dropped by JSON.stringify)
40
+ // rather than leaving an empty string — applies to body as well as subtitle.
41
+ alert[field] = text.length > 0 ? text + ELLIPSIS : undefined;
42
+ }
43
+ }
44
+ return body;
45
+ }
46
+
47
+ /** UTF-8 byte length of the JSON-serialised payload. */
48
+ function payloadBytes(body: Record<string, unknown>): number {
49
+ return Buffer.byteLength(JSON.stringify(body), 'utf8');
50
+ }
@@ -6,6 +6,7 @@ import jwt from 'jsonwebtoken';
6
6
  import { promisify } from 'util';
7
7
 
8
8
  import { toErrorMessage } from '../utils/error';
9
+ import { fitApnsPayload } from './apns-payload.js';
9
10
 
10
11
  // APNs delivery via curl. Replaces @parse/node-apn (which times out on Node 24
11
12
  // regardless of version: 7.1.0, 8.1.0, both reproduce). Node's native http2 +
@@ -109,7 +110,48 @@ export class LibraryAPNsService {
109
110
  void _ignoredAps;
110
111
  Object.assign(body, customData);
111
112
  }
112
- const bodyJson = JSON.stringify(body);
113
+ return this.deliver(deviceToken, body, 'alert', '10');
114
+ }
115
+
116
+ /**
117
+ * Send a SILENT (content-available) background push to wake a backgrounded app without
118
+ * showing an alert — used to wake the phone-resident agent loop so it can run a burst.
119
+ * Uses apns-push-type:background + apns-priority:5 (required by APNs for silent pushes).
120
+ * iOS throttles these (a few per hour); delivery is best-effort.
121
+ */
122
+ async sendSilentNotification(
123
+ deviceToken: string,
124
+ data?: Record<string, unknown>
125
+ ): Promise<APNsSendResult> {
126
+ if (!this.configured) {
127
+ throw new Error('APNs service not configured properly');
128
+ }
129
+ if (!deviceToken) {
130
+ throw new Error('Device token is required');
131
+ }
132
+ const body: Record<string, unknown> = { aps: { 'content-available': 1 } };
133
+ if (data) {
134
+ const { aps: _ignoredAps, ...customData } = data;
135
+ void _ignoredAps;
136
+ Object.assign(body, customData);
137
+ }
138
+ return this.deliver(deviceToken, body, 'background', '5');
139
+ }
140
+
141
+ shutdown(): void {
142
+ // No persistent state to release.
143
+ }
144
+
145
+ /** Shared curl/HTTP-2 delivery. `pushType` is 'alert' | 'background', `priority` '10' | '5'. */
146
+ private async deliver(
147
+ deviceToken: string,
148
+ body: Record<string, unknown>,
149
+ pushType: 'alert' | 'background',
150
+ priority: '5' | '10'
151
+ ): Promise<APNsSendResult> {
152
+ // Cap the payload to APNs' size limit. A long agent message in alert.body otherwise blows
153
+ // past ~4 KB → APNs 413 PayloadTooLarge (or curl E2BIG), so the notification never arrives.
154
+ const bodyJson = JSON.stringify(fitApnsPayload(body));
113
155
  const jwtToken = this.getJwt();
114
156
  const url = `https://${this.host}/3/device/${deviceToken}`;
115
157
 
@@ -127,9 +169,9 @@ export class LibraryAPNsService {
127
169
  '-H',
128
170
  `authorization: bearer ${jwtToken}`,
129
171
  '-H',
130
- 'apns-push-type: alert',
172
+ `apns-push-type: ${pushType}`,
131
173
  '-H',
132
- 'apns-priority: 10',
174
+ `apns-priority: ${priority}`,
133
175
  '-d',
134
176
  bodyJson,
135
177
  '-w',
@@ -171,16 +213,16 @@ export class LibraryAPNsService {
171
213
  console.error(`[apns] Delivery failed (status=${status}): ${reason}`);
172
214
  return { error: reason, failed: 1, sent: 0, success: false };
173
215
  } catch (err) {
174
- const msg = toErrorMessage(err);
216
+ // A failed execFile echoes the whole curl command — which carries the APNs JWT and the
217
+ // device token. Redact both before logging or returning so they don't leak into logs/responses.
218
+ const msg = toErrorMessage(err)
219
+ .replace(/bearer\s+[A-Za-z0-9._-]+/gi, 'bearer [REDACTED]')
220
+ .replace(/device\/[A-Fa-f0-9]+/g, 'device/[REDACTED]');
175
221
  console.error(`[apns] curl transport error: ${msg}`);
176
222
  return { error: msg, failed: 1, sent: 0, success: false };
177
223
  }
178
224
  }
179
225
 
180
- shutdown(): void {
181
- // No persistent state to release.
182
- }
183
-
184
226
  private getJwt(): string {
185
227
  const now = Date.now();
186
228
  if (this.cachedJwt && now - this.cachedJwtAt < JWT_REFRESH_INTERVAL_MS) {
@@ -29,11 +29,13 @@ import Database from 'better-sqlite3';
29
29
  import * as fs from 'fs';
30
30
  import * as path from 'path';
31
31
 
32
+ import { shooterDataDir } from '../utils/shooter-home.js';
33
+
32
34
  export type { PendingRequest };
33
35
 
34
36
  const MAX_AGE_MS = 5 * 60 * 1000;
35
37
 
36
- const DB_DIR = path.join(process.env.HOME || '', '.shooter');
38
+ const DB_DIR = shooterDataDir();
37
39
  const DB_PATH = path.join(DB_DIR, 'shooter.db');
38
40
 
39
41
  export class PendingRequestsStore {
@@ -0,0 +1,57 @@
1
+ // Pure context-building + goal store for the autopilot engine.
2
+ //
3
+ // Kept free of server imports (no pty-manager, no SQLite) so it is unit-testable in isolation,
4
+ // the same way next-step-consensus.ts and decide-injection.ts are.
5
+ //
6
+ // WHY a goal store: the engine builds its LLM context from the last ~12 session events, with no
7
+ // memory of what the session is trying to achieve. After a few autonomous cycles the original
8
+ // goal scrolls out of that window and consensus drifts toward "what the agent just did". Pinning
9
+ // a per-terminal goal and prepending it to every context keeps the lenses (and thus the injected
10
+ // next-steps) anchored. It is also the literal "set it as a goal" the autonomous loop needs.
11
+
12
+ // Per-terminal goal store. Lives in the engine's module graph; the /api/autopilot/goal route
13
+ // reaches setEngineGoal via the globalThis control object (it never imports the engine directly).
14
+ const goals = new Map<string, string>();
15
+
16
+ /**
17
+ * Build the LLM context string for one pipeline run. When a goal is present it leads the string
18
+ * so the summary + every lens see it first; otherwise the format is unchanged from before.
19
+ */
20
+ export function buildEngineContext(input: {
21
+ errorCount: number;
22
+ events: string[];
23
+ goal?: string;
24
+ projectName: string;
25
+ status: string;
26
+ toolCallCount: number;
27
+ trigger: string;
28
+ }): string {
29
+ const goalLine = input.goal && input.goal.trim().length > 0 ? `Goal: ${input.goal.trim()}\n` : '';
30
+ return (
31
+ `${goalLine}Project: ${input.projectName}\nStatus: ${input.status}\n` +
32
+ `Errors: ${input.errorCount}\nTool calls: ${input.toolCallCount}\n` +
33
+ `Recent events: ${input.events.slice(-12).join('; ')}\nTrigger: ${input.trigger}`
34
+ );
35
+ }
36
+
37
+ /** Drop a terminal's goal (called when the terminal exits). */
38
+ export function clearEngineGoal(terminalId: string): void {
39
+ goals.delete(terminalId);
40
+ }
41
+
42
+ /** The pinned goal for a terminal, or undefined if none is set. */
43
+ export function getEngineGoal(terminalId: string): string | undefined {
44
+ return goals.get(terminalId);
45
+ }
46
+
47
+ /** Set (or, with a blank value, clear) the goal that anchors a terminal's autopilot context. */
48
+ export function setEngineGoal(terminalId: string, goal: string): void {
49
+ const trimmed = goal.trim();
50
+ if (trimmed.length > 0) {
51
+ // Cap defensively: the goal is prepended to EVERY engine LLM context, so an unbounded string
52
+ // would permanently bloat the prompt. The /api/autopilot/goal route also rejects > 500.
53
+ goals.set(terminalId, trimmed.slice(0, 500));
54
+ } else {
55
+ goals.delete(terminalId);
56
+ }
57
+ }