@tt-a1i/hive 1.6.0 → 2.0.1

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 (254) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.en.md +74 -11
  3. package/README.md +42 -8
  4. package/dist/src/cli/hive-remote.d.ts +46 -0
  5. package/dist/src/cli/hive-remote.js +257 -0
  6. package/dist/src/cli/hive-update.js +7 -2
  7. package/dist/src/cli/hive.d.ts +6 -0
  8. package/dist/src/cli/hive.js +64 -0
  9. package/dist/src/cli/team.d.ts +22 -0
  10. package/dist/src/cli/team.js +255 -5
  11. package/dist/src/server/agent-command-resolver.js +10 -3
  12. package/dist/src/server/agent-exit-classification.d.ts +6 -0
  13. package/dist/src/server/agent-exit-classification.js +6 -0
  14. package/dist/src/server/agent-manager-support.d.ts +2 -1
  15. package/dist/src/server/agent-manager-support.js +59 -15
  16. package/dist/src/server/agent-manager.d.ts +3 -0
  17. package/dist/src/server/agent-manager.js +22 -7
  18. package/dist/src/server/agent-run-bootstrap.d.ts +20 -1
  19. package/dist/src/server/agent-run-bootstrap.js +16 -6
  20. package/dist/src/server/agent-run-exit-handler.js +14 -8
  21. package/dist/src/server/agent-run-starter.d.ts +3 -1
  22. package/dist/src/server/agent-run-starter.js +37 -6
  23. package/dist/src/server/agent-run-sync.js +13 -5
  24. package/dist/src/server/agent-runtime-types.d.ts +1 -0
  25. package/dist/src/server/agent-runtime.d.ts +2 -1
  26. package/dist/src/server/agent-runtime.js +9 -2
  27. package/dist/src/server/agent-startup-instructions.d.ts +2 -1
  28. package/dist/src/server/agent-startup-instructions.js +8 -4
  29. package/dist/src/server/agent-stdin-dispatcher.d.ts +4 -2
  30. package/dist/src/server/agent-stdin-dispatcher.js +35 -3
  31. package/dist/src/server/command-preset-defaults.d.ts +6 -1
  32. package/dist/src/server/command-preset-defaults.js +68 -0
  33. package/dist/src/server/fs-browse.d.ts +2 -0
  34. package/dist/src/server/fs-browse.js +165 -31
  35. package/dist/src/server/fs-pick-folder.js +6 -69
  36. package/dist/src/server/fs-sandbox.d.ts +5 -3
  37. package/dist/src/server/fs-sandbox.js +5 -3
  38. package/dist/src/server/hive-team-guidance.js +18 -6
  39. package/dist/src/server/machine-name.d.ts +2 -0
  40. package/dist/src/server/machine-name.js +13 -0
  41. package/dist/src/server/open-target-commands.d.ts +1 -0
  42. package/dist/src/server/open-target-commands.js +4 -1
  43. package/dist/src/server/orchestrator-autostart.js +1 -1
  44. package/dist/src/server/platform-path.d.ts +1 -0
  45. package/dist/src/server/platform-path.js +14 -1
  46. package/dist/src/server/post-start-input-writer.js +50 -13
  47. package/dist/src/server/preset-launch-support.js +3 -1
  48. package/dist/src/server/recovery-summary.d.ts +2 -1
  49. package/dist/src/server/recovery-summary.js +2 -1
  50. package/dist/src/server/remote-audit-store.d.ts +51 -0
  51. package/dist/src/server/remote-audit-store.js +108 -0
  52. package/dist/src/server/remote-config-keys.d.ts +17 -0
  53. package/dist/src/server/remote-config-keys.js +27 -0
  54. package/dist/src/server/remote-control-constants.d.ts +30 -0
  55. package/dist/src/server/remote-control-constants.js +29 -0
  56. package/dist/src/server/remote-device-session.d.ts +40 -0
  57. package/dist/src/server/remote-device-session.js +22 -0
  58. package/dist/src/server/remote-device-store.d.ts +36 -0
  59. package/dist/src/server/remote-device-store.js +67 -0
  60. package/dist/src/server/remote-frame-bridge.d.ts +102 -0
  61. package/dist/src/server/remote-frame-bridge.js +791 -0
  62. package/dist/src/server/remote-gateway-client.d.ts +14 -0
  63. package/dist/src/server/remote-gateway-client.js +36 -0
  64. package/dist/src/server/remote-loopback-auth.d.ts +6 -0
  65. package/dist/src/server/remote-loopback-auth.js +112 -0
  66. package/dist/src/server/remote-pairing-tunnel.d.ts +59 -0
  67. package/dist/src/server/remote-pairing-tunnel.js +146 -0
  68. package/dist/src/server/remote-pairing.d.ts +58 -0
  69. package/dist/src/server/remote-pairing.js +237 -0
  70. package/dist/src/server/remote-tunnel.d.ts +113 -0
  71. package/dist/src/server/remote-tunnel.js +514 -0
  72. package/dist/src/server/restart-policy-support.d.ts +4 -1
  73. package/dist/src/server/restart-policy-support.js +3 -1
  74. package/dist/src/server/restart-policy.d.ts +1 -1
  75. package/dist/src/server/restart-policy.js +19 -3
  76. package/dist/src/server/route-types.d.ts +1 -1
  77. package/dist/src/server/routes-dispatches.js +1 -1
  78. package/dist/src/server/routes-fs.js +3 -3
  79. package/dist/src/server/routes-marketplace.js +2 -2
  80. package/dist/src/server/routes-open-workspace.js +1 -1
  81. package/dist/src/server/routes-remote.d.ts +2 -0
  82. package/dist/src/server/routes-remote.js +166 -0
  83. package/dist/src/server/routes-runtime.js +6 -6
  84. package/dist/src/server/routes-settings.js +16 -16
  85. package/dist/src/server/routes-tasks.js +2 -2
  86. package/dist/src/server/routes-team-memory.d.ts +2 -0
  87. package/dist/src/server/routes-team-memory.js +154 -0
  88. package/dist/src/server/routes-team-recall.d.ts +2 -0
  89. package/dist/src/server/routes-team-recall.js +119 -0
  90. package/dist/src/server/routes-team.js +31 -9
  91. package/dist/src/server/routes-ui.js +11 -1
  92. package/dist/src/server/routes-workflow-schedules.js +3 -3
  93. package/dist/src/server/routes-workflows.js +5 -5
  94. package/dist/src/server/routes-workspace-memory-dreams.d.ts +2 -0
  95. package/dist/src/server/routes-workspace-memory-dreams.js +105 -0
  96. package/dist/src/server/routes-workspace-memory.d.ts +2 -0
  97. package/dist/src/server/routes-workspace-memory.js +215 -0
  98. package/dist/src/server/routes-workspaces.js +9 -9
  99. package/dist/src/server/routes.js +10 -0
  100. package/dist/src/server/runtime-database.d.ts +1 -0
  101. package/dist/src/server/runtime-database.js +27 -2
  102. package/dist/src/server/runtime-restart-policy.d.ts +3 -1
  103. package/dist/src/server/runtime-restart-policy.js +2 -1
  104. package/dist/src/server/runtime-store-contract.d.ts +37 -0
  105. package/dist/src/server/runtime-store-dream.d.ts +23 -0
  106. package/dist/src/server/runtime-store-dream.js +16 -0
  107. package/dist/src/server/runtime-store-helpers.d.ts +20 -0
  108. package/dist/src/server/runtime-store-helpers.js +81 -7
  109. package/dist/src/server/runtime-store-memory.d.ts +33 -0
  110. package/dist/src/server/runtime-store-memory.js +37 -0
  111. package/dist/src/server/runtime-store-remote.d.ts +5 -0
  112. package/dist/src/server/runtime-store-remote.js +45 -0
  113. package/dist/src/server/runtime-store-workflows.js +2 -0
  114. package/dist/src/server/runtime-store.js +14 -3
  115. package/dist/src/server/session-capture-claude.d.ts +1 -1
  116. package/dist/src/server/session-capture-claude.js +7 -4
  117. package/dist/src/server/session-capture-codex.js +4 -5
  118. package/dist/src/server/session-capture-gemini.js +4 -5
  119. package/dist/src/server/session-capture-opencode.d.ts +4 -4
  120. package/dist/src/server/session-capture-opencode.js +20 -12
  121. package/dist/src/server/session-capture-qwen.d.ts +5 -0
  122. package/dist/src/server/session-capture-qwen.js +104 -0
  123. package/dist/src/server/session-capture.d.ts +23 -0
  124. package/dist/src/server/session-capture.js +48 -0
  125. package/dist/src/server/sqlite-schema-v22.d.ts +2 -0
  126. package/dist/src/server/sqlite-schema-v22.js +27 -0
  127. package/dist/src/server/sqlite-schema-v23.d.ts +2 -0
  128. package/dist/src/server/sqlite-schema-v23.js +43 -0
  129. package/dist/src/server/sqlite-schema-v24.d.ts +2 -0
  130. package/dist/src/server/sqlite-schema-v24.js +34 -0
  131. package/dist/src/server/sqlite-schema-v25.d.ts +2 -0
  132. package/dist/src/server/sqlite-schema-v25.js +127 -0
  133. package/dist/src/server/sqlite-schema-v26.d.ts +2 -0
  134. package/dist/src/server/sqlite-schema-v26.js +56 -0
  135. package/dist/src/server/sqlite-schema-v27.d.ts +6 -0
  136. package/dist/src/server/sqlite-schema-v27.js +92 -0
  137. package/dist/src/server/sqlite-schema-v28.d.ts +2 -0
  138. package/dist/src/server/sqlite-schema-v28.js +19 -0
  139. package/dist/src/server/sqlite-schema-v29.d.ts +2 -0
  140. package/dist/src/server/sqlite-schema-v29.js +27 -0
  141. package/dist/src/server/sqlite-schema-v30.d.ts +2 -0
  142. package/dist/src/server/sqlite-schema-v30.js +27 -0
  143. package/dist/src/server/sqlite-schema-v31.d.ts +2 -0
  144. package/dist/src/server/sqlite-schema-v31.js +30 -0
  145. package/dist/src/server/sqlite-schema.d.ts +1 -1
  146. package/dist/src/server/sqlite-schema.js +54 -1
  147. package/dist/src/server/startup-command-parser.js +5 -1
  148. package/dist/src/server/tasks-file-watcher.d.ts +2 -0
  149. package/dist/src/server/tasks-file-watcher.js +15 -6
  150. package/dist/src/server/tasks-file.js +30 -5
  151. package/dist/src/server/tasks-websocket-server.js +4 -0
  152. package/dist/src/server/team-authz.d.ts +1 -1
  153. package/dist/src/server/team-authz.js +13 -1
  154. package/dist/src/server/team-list-enrichment.js +3 -1
  155. package/dist/src/server/team-memory-digest.d.ts +52 -0
  156. package/dist/src/server/team-memory-digest.js +200 -0
  157. package/dist/src/server/team-memory-dream-applier.d.ts +5 -0
  158. package/dist/src/server/team-memory-dream-applier.js +234 -0
  159. package/dist/src/server/team-memory-dream-http-serializers.d.ts +13 -0
  160. package/dist/src/server/team-memory-dream-http-serializers.js +12 -0
  161. package/dist/src/server/team-memory-dream-ops.d.ts +40 -0
  162. package/dist/src/server/team-memory-dream-ops.js +153 -0
  163. package/dist/src/server/team-memory-dream-reverter.d.ts +22 -0
  164. package/dist/src/server/team-memory-dream-reverter.js +221 -0
  165. package/dist/src/server/team-memory-dream-run-store.d.ts +23 -0
  166. package/dist/src/server/team-memory-dream-run-store.js +211 -0
  167. package/dist/src/server/team-memory-dream-runner.d.ts +37 -0
  168. package/dist/src/server/team-memory-dream-runner.js +178 -0
  169. package/dist/src/server/team-memory-dream-scheduler.d.ts +32 -0
  170. package/dist/src/server/team-memory-dream-scheduler.js +115 -0
  171. package/dist/src/server/team-memory-dream-store.d.ts +19 -0
  172. package/dist/src/server/team-memory-dream-store.js +16 -0
  173. package/dist/src/server/team-memory-dream-types.d.ts +104 -0
  174. package/dist/src/server/team-memory-dream-types.js +23 -0
  175. package/dist/src/server/team-memory-export.d.ts +22 -0
  176. package/dist/src/server/team-memory-export.js +220 -0
  177. package/dist/src/server/team-memory-feature.d.ts +12 -0
  178. package/dist/src/server/team-memory-feature.js +12 -0
  179. package/dist/src/server/team-memory-http-serializers.d.ts +102 -0
  180. package/dist/src/server/team-memory-http-serializers.js +46 -0
  181. package/dist/src/server/team-memory-injection.d.ts +31 -0
  182. package/dist/src/server/team-memory-injection.js +49 -0
  183. package/dist/src/server/team-memory-store.d.ts +116 -0
  184. package/dist/src/server/team-memory-store.js +513 -0
  185. package/dist/src/server/team-operations.d.ts +5 -1
  186. package/dist/src/server/team-operations.js +46 -16
  187. package/dist/src/server/team-recall-store.d.ts +38 -0
  188. package/dist/src/server/team-recall-store.js +205 -0
  189. package/dist/src/server/terminal-input-profile.d.ts +1 -1
  190. package/dist/src/server/terminal-input-profile.js +8 -0
  191. package/dist/src/server/terminal-ws-server.js +6 -0
  192. package/dist/src/server/ui-auth-helpers.d.ts +1 -1
  193. package/dist/src/server/ui-auth-helpers.js +7 -1
  194. package/dist/src/server/ui-auth.d.ts +3 -0
  195. package/dist/src/server/ui-auth.js +21 -1
  196. package/dist/src/server/workflow-cli-policy.d.ts +2 -3
  197. package/dist/src/server/workflow-cli-policy.js +3 -3
  198. package/dist/src/server/workflow-runner.d.ts +1 -0
  199. package/dist/src/server/workflow-runner.js +9 -4
  200. package/dist/src/server/workspace-path-validation.js +6 -2
  201. package/dist/src/server/workspace-store.d.ts +1 -1
  202. package/dist/src/server/workspace-store.js +35 -9
  203. package/dist/src/shared/fs-browse.d.ts +1 -0
  204. package/dist/src/shared/fs-browse.js +1 -0
  205. package/dist/src/shared/path-input.d.ts +12 -0
  206. package/dist/src/shared/path-input.js +22 -0
  207. package/dist/src/shared/remote-bridge-routing.d.ts +19 -0
  208. package/dist/src/shared/remote-bridge-routing.js +141 -0
  209. package/dist/src/shared/remote-crypto.d.ts +138 -0
  210. package/dist/src/shared/remote-crypto.js +427 -0
  211. package/dist/src/shared/remote-pairing-code.d.ts +7 -0
  212. package/dist/src/shared/remote-pairing-code.js +47 -0
  213. package/dist/src/shared/remote-protocol.d.ts +160 -0
  214. package/dist/src/shared/remote-protocol.js +526 -0
  215. package/dist/src/shared/team-memory.d.ts +11 -0
  216. package/dist/src/shared/team-memory.js +10 -0
  217. package/dist/src/shared/team-recall.d.ts +1 -0
  218. package/dist/src/shared/team-recall.js +1 -0
  219. package/dist/src/shared/types.d.ts +5 -6
  220. package/package.json +12 -5
  221. package/scripts/postinstall-native-artifacts.mjs +113 -0
  222. package/web/dist/assets/AddWorkerDialog-C86CwNgQ.js +2 -0
  223. package/web/dist/assets/AddWorkspaceFlow-Bm2Jz34D.js +1 -0
  224. package/web/dist/assets/FirstRunWizard-XzBoEpA5.js +1 -0
  225. package/web/dist/assets/MarketplaceDrawer-BFfGT8hH.js +67 -0
  226. package/web/dist/assets/TaskGraphDrawer-_uVH_0C1.js +1 -0
  227. package/web/dist/assets/{WhatsNewDialog-CSGzk-2U.js → WhatsNewDialog-DkJHmkMs.js} +1 -1
  228. package/web/dist/assets/WorkerModal-BtMJEOG9.js +1 -0
  229. package/web/dist/assets/WorkflowsDrawer-CiIdHS6_.js +1 -0
  230. package/web/dist/assets/WorkspaceMemoryDrawer-C6sNocl_.js +1 -0
  231. package/web/dist/assets/WorkspaceTaskDrawer-CyhhEB1Z.js +1 -0
  232. package/web/dist/assets/index-BAiLYajK.css +1 -0
  233. package/web/dist/assets/index-K-GG8UwR.js +73 -0
  234. package/web/dist/assets/search-BtRkkEmS.js +1 -0
  235. package/web/dist/assets/square-terminal-lEeQUWb3.js +1 -0
  236. package/web/dist/cli-icons/agy.png +0 -0
  237. package/web/dist/cli-icons/cursor.ico +0 -0
  238. package/web/dist/cli-icons/grok.ico +0 -0
  239. package/web/dist/cli-icons/hermes.png +0 -0
  240. package/web/dist/cli-icons/qwen.png +0 -0
  241. package/web/dist/index.html +8 -3
  242. package/web/dist/sw.js +1 -1
  243. package/scripts/fix-runtime-artifacts.mjs +0 -33
  244. package/web/dist/assets/AddWorkerDialog-CGbaxu0T.js +0 -2
  245. package/web/dist/assets/AddWorkspaceDialog-CNgExu6b.js +0 -1
  246. package/web/dist/assets/FirstRunWizard-DxGApUNc.js +0 -1
  247. package/web/dist/assets/MarketplaceDrawer-Bk6cpukn.js +0 -76
  248. package/web/dist/assets/WorkerModal-i2F3n3nZ.js +0 -1
  249. package/web/dist/assets/WorkspaceTaskDrawer-C_Ta_K13.js +0 -1
  250. package/web/dist/assets/WorkspaceTerminalPanels-DDGTF8rc.css +0 -1
  251. package/web/dist/assets/WorkspaceTerminalPanels-VdDxtrQF.js +0 -1
  252. package/web/dist/assets/index-5zh61jMg.css +0 -1
  253. package/web/dist/assets/index-CAgGM6nb.js +0 -75
  254. package/web/dist/assets/path-join-7MR1s7b1.js +0 -1
@@ -1,3 +1,4 @@
1
+ import { BUILTIN_COMMAND_PRESET_CLI_LIST } from './command-preset-defaults.js';
1
2
  import { FEATURE_FLAGS_ALL_OFF } from './feature-flags.js';
2
3
  import { DEFAULT_WORKFLOW_CLI_POLICY } from './workflow-cli-policy.js';
3
4
  /**
@@ -20,7 +21,7 @@ import { DEFAULT_WORKFLOW_CLI_POLICY } from './workflow-cli-policy.js';
20
21
  export const buildOrchestratorReminderTail = ({ workflowsEnabled }) => {
21
22
  const body = 'You are the Hive Orchestrator. Reply with one of: ' +
22
23
  '(a) `team send "<worker-name>" "<task>"` to dispatch — run `team list` first if the roster may have changed since your last view (Hive does not push membership changes; stale names fail). ' +
23
- 'If no worker fits or the roster is empty, `team spawn <role> [--cli claude|codex|opencode|gemini]` to create one (add `--ephemeral` for a one-shot), then dispatch; do not ask the user to add workers. ' +
24
+ `If no worker fits or the roster is empty, \`team spawn <role> [--cli <${BUILTIN_COMMAND_PRESET_CLI_LIST}>]\` to create one (add \`--ephemeral\` for a one-shot), then dispatch; do not ask the user to add workers. ` +
24
25
  '(b) `team cancel --dispatch <id> "<reason>"` to close an obsolete dispatch. ' +
25
26
  (workflowsEnabled
26
27
  ? '(c) `team workflow run --stdin` to fan out across 3+ workers or run a staged review→fix — never a loop of `team send` (no barrier, no UI group, no stop button). (d) plain text to the user. '
@@ -53,10 +54,11 @@ const CORE_ORCHESTRATOR_RULES = [
53
54
  'Small, low-risk tasks you can finish in a couple of minutes: do them yourself; do not dispatch for the sake of form. Reach for `team send` when the work needs parallelism, long execution, independent review/test, a dedicated role, or the user explicitly asked for a worker.',
54
55
  'If exactly one worker is available, dispatch to it directly with `team send <worker-name> "<task>"` — do not bounce the choice back to the user.',
55
56
  'When the user says "have a worker do X", dispatch it with `team send <worker-name> "<task>"`.',
56
- 'If the roster is empty or lacks the role the task needs, build the team yourself: `team spawn <role> [--name <n>] [--cli claude|codex|opencode|gemini]`, then immediately dispatch. Do not stop to ask the user to add members — that call is yours.',
57
+ `If the roster is empty or lacks the role the task needs, build the team yourself: \`team spawn <role> [--name <n>] [--cli <${BUILTIN_COMMAND_PRESET_CLI_LIST}>]\`, then immediately dispatch. Do not stop to ask the user to add members — that call is yours.`,
57
58
  '`team spawn` is PERSISTENT by default (a normal member that stays in the workspace); add `--ephemeral` for a one-shot worker that auto-dismisses after its first `team report`. Rule of thumb: will you reuse this role in the next 10 minutes? Yes → persistent; no → `--ephemeral`.',
58
59
  'When the user cancels or changes direction on an open dispatch, close it explicitly with `team cancel --dispatch <id> "<reason>"` — do not just say "cancel" in prose.',
59
60
  'Each `team send` opens a SEPARATE dispatch with its own pending count and its own required report — it is not a way to tack context onto a dispatch already in flight. Send the same worker twice and it owes you TWO reports; one report closes only one dispatch (the one its `--dispatch <id>` names, or the oldest open one if the flag is omitted), so the other stays open and the worker is pinned on `working`. To change or extend an in-flight task, `team cancel --dispatch <id> "<reason>"` the stale dispatch and re-send the whole task, or wait for the worker to report and dispatch the follow-up after.',
61
+ 'Only orchestrators can add memory directly; workers should report durable findings for the orchestrator or Dream to capture. Search memory before adding: use `team memory search "<query>"` to avoid duplicates. Use `team memory add "<body>"` only for rare, evidence-backed workspace facts, decisions, and pitfalls that should help future Hive agents across sessions. Nothing worth saving is a normal outcome; do not add memory just to prove you used it. Use `team memory forget <memory-id>` only to archive obsolete memory.',
60
62
  "Never substitute your CLI's own subagent / workflow tools (e.g. Task / Explore / Workflow / Agent) for Hive workers or Hive workflows — they run inside your CLI process, bypass Hive's PTY fleet, never appear in the UI or `team list`, and the stop button cannot reach them.",
61
63
  'In `team list`, `last_pty_line` is raw terminal output (stdout / help / control-sequence noise), NOT a worker\'s report. A real report arrives only as an injected "report from @<name>" / "status from @<name>" system message — treat only those as replies.',
62
64
  ];
@@ -96,6 +98,8 @@ const WORKER_RULES = [
96
98
  "Do not call `team send`, and do not launch your own CLI's subagent tools to do the work for you — finish it yourself.",
97
99
  'When an assigned task is done, blocked, or has failed, you MUST report to the Orchestrator with `team report`.',
98
100
  'When you have no active dispatch and only want to report readiness, environment, or status, use `team status "<state>"`.',
101
+ 'Use `team recall "<query>"` when prior team messages or reports may contain useful evidence.',
102
+ 'Use `team memory search "<query>"` to inspect active workspace memory. Include durable findings in `team report`; workers cannot add or forget memory.',
99
103
  '`team --help` only prints command syntax — it is NOT a way to report; its output never reaches the Orchestrator. You still owe a real `team report` / `team status` afterward.',
100
104
  'If `team report` / `team status` errors, it also prints USAGE — fix the arguments per USAGE and retry; do not use `team --help` as a stand-in for reporting.',
101
105
  ];
@@ -121,7 +125,7 @@ Host functions injected into the script: \`agent(prompt, opts)\`,
121
125
  \`parallel(thunks)\`, \`pipeline(items, ...stages)\`, \`phase(title)\`,
122
126
  \`log(msg)\`, plus the \`args\` global.
123
127
 
124
- \`agent(prompt, opts)\` opts: \`{ agentType?: "coder"|"reviewer"|"tester"|"custom"|<custom-role-name>, cli?: "claude"|"codex"|"opencode"|"gemini", model?: string, outputSchema?: object, label?: string, timeoutMs?: number }\` — other fields are silently ignored. \`agentType\` also accepts the name of a workspace custom role template (case-insensitive); a typo throws rather than silently falling back to coder. \`model\` is passed through to the worker launch config (\`--model <id>\`), so a 100-way fan-out can use a cheap model and the synthesizer a strong one. \`outputSchema\` makes \`agent()\` resolve to a parsed object instead of a string: the worker is told to end its report with a fenced \`\`\`json block whose keys match the schema. On a parse miss it falls back to \`{ text: "<raw report>" }\`, so ALWAYS treat a missing field as the SAFE default (never assume success). The worker auto-dismisses when its \`agent()\` call resolves — you never dismiss it.
128
+ \`agent(prompt, opts)\` opts: \`{ agentType?: "coder"|"reviewer"|"tester"|"custom"|<custom-role-name>, cli?: "${BUILTIN_COMMAND_PRESET_CLI_LIST.replace(/\|/gu, '"|"')}", model?: string, outputSchema?: object, label?: string, timeoutMs?: number }\` — other fields are silently ignored. \`agentType\` also accepts the name of a workspace custom role template (case-insensitive); a typo throws rather than silently falling back to coder. \`model\` is passed through to the worker launch config (\`--model <id>\`), so a 100-way fan-out can use a cheap model and the synthesizer a strong one. \`outputSchema\` makes \`agent()\` resolve to a parsed object instead of a string: the worker is told to end its report with a fenced \`\`\`json block whose keys match the schema. On a parse miss it falls back to \`{ text: "<raw report>" }\`, so ALWAYS treat a missing field as the SAFE default (never assume success). The worker auto-dismisses when its \`agent()\` call resolves — you never dismiss it.
125
129
 
126
130
  \`parallel()\` takes an array of THUNKS (\`() => agent(...)\`), NOT already-started promises: \`parallel([agent(...), agent(...)])\` degrades to unordered concurrency counted as a single step (a no-op grouping), because the promises already started at construction time. \`pipeline(items, ...stages)\` stages are also functions, shape \`(prev, item, i) => agent(...)\`.
127
131
 
@@ -240,7 +244,7 @@ export const buildProtocolDoc = (cliPolicy = DEFAULT_WORKFLOW_CLI_POLICY, flags
240
244
  '## You are running inside Hive',
241
245
  '',
242
246
  'Hive is a multi-CLI-agent workbench. Each agent in this workspace is a',
243
- 'real CLI process (Claude Code / Codex / OpenCode / Gemini). All',
247
+ 'real CLI process (Antigravity CLI / Claude Code / Codex / OpenCode / Gemini / Hermes / Qwen Code / Cursor CLI / Grok Build). All',
244
248
  'inter-agent communication goes through the `team` CLI binary on your',
245
249
  'PATH.',
246
250
  '',
@@ -252,8 +256,13 @@ export const buildProtocolDoc = (cliPolicy = DEFAULT_WORKFLOW_CLI_POLICY, flags
252
256
  '## `team` CLI — orchestrator',
253
257
  '',
254
258
  '- `team list` — show workspace members and their status',
259
+ '- `team recall "<query>" [--limit <n>] [--window <n>]` — search prior team messages/reports in this workspace',
260
+ '- `team memory add "<body>" [--kind fact|preference|decision|pitfall|procedure_ref] [--tag <tag>]` — save durable workspace memory (orchestrator entries become active)',
261
+ '- `team memory show <memory-id>` — inspect a memory entry and evidence snapshots',
262
+ '- `team memory search "<query>" [--limit <n>]` — search active workspace memory',
263
+ '- `team memory forget <memory-id>` — archive obsolete memory (orchestrator only; does not physically delete evidence)',
255
264
  '- `team send "<worker-name>" "<task>"` — dispatch to a worker by name (never id)',
256
- '- `team spawn <role> [--name <name>] [--cli claude|codex|opencode|gemini]` — create a PERSISTENT member when none fits (or when the roster is empty)',
265
+ `- \`team spawn <role> [--name <name>] [--cli <${BUILTIN_COMMAND_PRESET_CLI_LIST}>]\` — create a PERSISTENT member when none fits (or when the roster is empty)`,
257
266
  '- `team spawn <role> --ephemeral [other-flags]` — create a one-shot worker that auto-dismisses after its first `team report`',
258
267
  '- `team dismiss <worker-name>` — remove an ephemeral worker you spawned',
259
268
  '- `team cancel --dispatch <id> "<reason>"` — cancel an obsolete open dispatch',
@@ -261,8 +270,11 @@ export const buildProtocolDoc = (cliPolicy = DEFAULT_WORKFLOW_CLI_POLICY, flags
261
270
  '',
262
271
  '## `team` CLI — worker',
263
272
  '',
273
+ '- `team recall "<query>" [--limit <n>] [--window <n>]` — search prior team messages/reports in this workspace',
274
+ '- `team memory show <memory-id>` — inspect a memory entry and evidence snapshots',
275
+ '- `team memory search "<query>" [--limit <n>]` — search active workspace memory',
264
276
  '- `team report "<result>" --dispatch <id>` — report task outcome',
265
- '- `team report --stdin --dispatch <id>` — same, body from stdin (pipe content in via your shell — POSIX heredoc, `type file |`, or whatever your environment supports)',
277
+ '- `team report --stdin --dispatch <id>` — same, body from stdin (pipe content in via your shell — POSIX heredoc, Windows cmd `type file |`, PowerShell `Get-Content -Raw -Encoding utf8 file |`, or portable stdin redirection `< file`)',
266
278
  '- `team status "<state>"` — update orchestrator when no dispatch is active',
267
279
  '',
268
280
  '## Orchestrator rules',
@@ -0,0 +1,2 @@
1
+ export declare function cleanMachineName(raw: string): string | null;
2
+ export declare function getMachineName(): string | null;
@@ -0,0 +1,13 @@
1
+ import { hostname } from 'node:os';
2
+ const MAX_LEN = 64;
3
+ // Trim surrounding whitespace, strip a trailing '.local' suffix (common on macOS),
4
+ // and cap at 64 chars. Returns null if the result is empty.
5
+ export function cleanMachineName(raw) {
6
+ const cleaned = raw.trim().replace(/\.local$/i, '');
7
+ if (cleaned.length === 0)
8
+ return null;
9
+ return cleaned.slice(0, MAX_LEN);
10
+ }
11
+ export function getMachineName() {
12
+ return cleanMachineName(hostname());
13
+ }
@@ -6,6 +6,7 @@ export declare const resolveOpenTargetPlatform: (platform: NodeJS.Platform) => O
6
6
  export interface OpenAttempt {
7
7
  command: string;
8
8
  args: string[];
9
+ options?: ExecFileOptions;
9
10
  }
10
11
  /**
11
12
  * Returns the ordered list of commands to try. First success wins; remaining
@@ -58,6 +58,7 @@ const linuxAttempts = (targetId, path) => {
58
58
  const cmdExeShimAttempt = (bin, path) => ({
59
59
  command: 'cmd.exe',
60
60
  args: ['/d', '/s', '/c', buildCmdCallCommand(bin, [path])],
61
+ options: { windowsHide: true },
61
62
  });
62
63
  const windowsAttempts = (targetId, path) => {
63
64
  switch (targetId) {
@@ -123,6 +124,8 @@ const APP_NOT_INSTALLED_PATTERNS = [
123
124
  const classifyFailure = (result) => {
124
125
  if (result.spawnError?.code === 'ENOENT')
125
126
  return 'command-not-in-path';
127
+ if (result.status === 9009)
128
+ return 'app-not-installed';
126
129
  const stderr = result.stderr.toLowerCase();
127
130
  if (APP_NOT_INSTALLED_PATTERNS.some((re) => re.test(stderr)))
128
131
  return 'app-not-installed';
@@ -168,7 +171,7 @@ export const openWorkspace = async (input, options = {}) => {
168
171
  const attempts = buildOpenAttempts(input.targetId, input.path, platform);
169
172
  let lastFailure = null;
170
173
  for (const attempt of attempts) {
171
- const result = await run(attempt.command, attempt.args, {});
174
+ const result = await run(attempt.command, attempt.args, attempt.options ?? {});
172
175
  // Windows `explorer.exe` returns exit code 1 even on success — checking
173
176
  // exit code here would surface a spurious error to the user on every
174
177
  // File Explorer open. spawnError still catches the "explorer not on PATH"
@@ -4,7 +4,7 @@ import { getStartupCommandExecutable } from './startup-command-parser.js';
4
4
  // "command not found" then dies with exit 127 (POSIX) or 9009 (Windows)
5
5
  // — typically <100ms in practice. 800ms balances reliability vs the perceived
6
6
  // workspace-create latency cost.
7
- const SETTLE_WAIT_MS = 800;
7
+ const SETTLE_WAIT_MS = process.platform === 'win32' ? 2000 : 800;
8
8
  const POLL_INTERVAL_MS = 25;
9
9
  // Shells emit a "command not found" exit code when the requested binary is
10
10
  // missing on PATH. node-pty does NOT raise a synchronous spawn error for that
@@ -1,3 +1,4 @@
1
+ export declare const expandHomePath: (path: string) => string;
1
2
  export declare const arePathsEqual: (left: string, right: string, platform?: NodeJS.Platform) => boolean;
2
3
  export declare const containsPathMarker: (haystack: string, marker: string, platform?: NodeJS.Platform) => boolean;
3
4
  export declare const indexOfPathMarker: (haystack: string, marker: string, platform?: NodeJS.Platform) => number;
@@ -1,5 +1,18 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
1
3
  const toForwardSlashes = (path) => path.replace(/\\/g, '/');
2
4
  const normalizeForWin32 = (path) => toForwardSlashes(path).toLowerCase();
5
+ export const expandHomePath = (path) => {
6
+ if (path === '~')
7
+ return homedir();
8
+ const match = /^~[\\/](.*)$/u.exec(path);
9
+ if (!match)
10
+ return path;
11
+ const rest = match[1] ?? '';
12
+ if (!rest)
13
+ return homedir();
14
+ return join(homedir(), ...rest.split(/[\\/]+/u).filter(Boolean));
15
+ };
3
16
  export const arePathsEqual = (left, right, platform = process.platform) => {
4
17
  if (platform === 'win32')
5
18
  return normalizeForWin32(left) === normalizeForWin32(right);
@@ -8,6 +21,6 @@ export const arePathsEqual = (left, right, platform = process.platform) => {
8
21
  export const containsPathMarker = (haystack, marker, platform = process.platform) => indexOfPathMarker(haystack, marker, platform) !== -1;
9
22
  export const indexOfPathMarker = (haystack, marker, platform = process.platform) => {
10
23
  if (platform === 'win32')
11
- return toForwardSlashes(haystack).indexOf(marker);
24
+ return normalizeForWin32(haystack).indexOf(normalizeForWin32(marker));
12
25
  return haystack.indexOf(marker);
13
26
  };
@@ -1,5 +1,5 @@
1
+ import { BUILTIN_INTERACTIVE_COMMANDS } from './command-preset-defaults.js';
1
2
  import { normalizeExecutableToken } from './startup-command-parser.js';
2
- const INTERACTIVE_COMMANDS = new Set(['claude', 'codex', 'gemini', 'opencode']);
3
3
  const READY_CHECK_INTERVAL_MS = 50;
4
4
  const READY_TIMEOUT_MS = 3000;
5
5
  const MIN_SUBMIT_AFTER_PASTE_DELAY_MS = 600;
@@ -19,25 +19,57 @@ const CODEX_PASTE_ACK_TIMEOUT_MS = 10000;
19
19
  // timeout before Enter is sent.
20
20
  const CODEX_PASTE_ACK_MIN_CHARS = 2000;
21
21
  const CODEX_SUBMIT_RETRY_DELAY_MS = 500;
22
- const COMMANDS_WITH_BRACKETED_PASTE = new Set(['claude', 'codex', 'opencode']);
22
+ const GROK_SUBMIT_AFTER_PASTE_DELAY_MS = 100;
23
+ const COMMANDS_WITH_BRACKETED_PASTE = new Set([
24
+ 'agy',
25
+ 'claude',
26
+ 'codex',
27
+ 'grok',
28
+ 'hermes',
29
+ 'opencode',
30
+ ]);
23
31
  const COMMANDS_WAITING_FOR_PASTE_ACK = new Set(['claude', 'codex']);
24
32
  const BRACKETED_PASTE_END = '\u001b[201~';
25
33
  const PASTE_ACK_PATTERN = /(?:^|[\r\n])\s*(?:[❯›]\s*)?\[(?:Pasted text #\d+[^\]]*|Pasted Content [\d,]+ chars)\]/u;
26
34
  const PASTE_ACK_ONLY_DELTA_PATTERN = /^[\s\r\n]*(?:[❯›]\s*)?\[(?:Pasted text #\d+[^\]]*|Pasted Content [\d,]+ chars)\]\s*(?:[❯›]\s*)?$/u;
35
+ const ESCAPE = String.fromCharCode(27);
36
+ const BELL = String.fromCharCode(7);
37
+ const TERMINAL_CONTROL_PATTERN = new RegExp(`${ESCAPE}\\[[0-?]*[ -/]*[@-~]|${ESCAPE}\\][^${BELL}${ESCAPE}]*(?:${BELL}|${ESCAPE}\\\\)`, 'gu');
27
38
  export const toBracketedPasteSubmission = (text) => `\u001b[200~${text}\u001b[201~`;
28
39
  const toError = (error) => (error instanceof Error ? error : new Error(String(error)));
29
40
  const createRunInactiveError = (runId) => new Error(`Run became inactive before input was submitted: ${runId}`);
30
- const getSubmitAfterPasteDelayMs = (text) => Math.min(MAX_SUBMIT_AFTER_PASTE_DELAY_MS, Math.max(MIN_SUBMIT_AFTER_PASTE_DELAY_MS, Math.ceil(text.length / PASTE_CHARS_PER_DELAY_MS)));
41
+ const getSubmitAfterPasteDelayMs = (command, text) => {
42
+ if (getCommandName(command) === 'grok')
43
+ return GROK_SUBMIT_AFTER_PASTE_DELAY_MS;
44
+ return Math.min(MAX_SUBMIT_AFTER_PASTE_DELAY_MS, Math.max(MIN_SUBMIT_AFTER_PASTE_DELAY_MS, Math.ceil(text.length / PASTE_CHARS_PER_DELAY_MS)));
45
+ };
46
+ const getCommandName = (command) => normalizeExecutableToken(command) ?? '';
31
47
  export const isInteractiveAgentCommand = (command) => {
32
48
  const brand = normalizeExecutableToken(command);
33
- return brand !== null && INTERACTIVE_COMMANDS.has(brand);
49
+ return brand !== null && BUILTIN_INTERACTIVE_COMMANDS.has(brand);
34
50
  };
35
- const getCommandName = (command) => normalizeExecutableToken(command) ?? '';
36
51
  const hasGeminiPromptReady = (output) => /\bType your message\b/u.test(output);
52
+ const hasOpencodePromptReady = (output) => /\bAsk anything\.\.\./u.test(output);
53
+ const hasGrokPromptReady = (output) => /\b(?:Enter:send|Composer\s+\S+)/u.test(output);
54
+ const getPlainTerminalOutput = (output) => output.replace(/\r/g, '\n').replace(TERMINAL_CONTROL_PATTERN, '');
55
+ const hasAgyPromptReady = (output) => /(?:^|\n)\s*>\s*\n\s*(?:[─-]{8,}|\?\s*for shortcuts)/u.test(getPlainTerminalOutput(output));
56
+ const getLastNonEmptyTerminalLine = (output) => {
57
+ const normalized = getPlainTerminalOutput(output);
58
+ for (const line of normalized.split('\n').reverse()) {
59
+ if (line.trim().length > 0)
60
+ return line.trim();
61
+ }
62
+ return '';
63
+ };
64
+ const hasBarePromptLine = (output) => /^[❯›]$/u.test(getLastNonEmptyTerminalLine(output));
65
+ const hasCodexChoicePrompt = (output, command) => getCommandName(command) === 'codex' && /^[❯›]\s+\S/u.test(getLastNonEmptyTerminalLine(output));
37
66
  export const hasInteractivePromptReady = (output, command = '') => {
38
67
  const commandName = getCommandName(command);
39
- return (/(?:^|[\r\n])\s*[❯›]\s*/u.test(output) ||
40
- (commandName === 'gemini' && hasGeminiPromptReady(output)));
68
+ return (hasBarePromptLine(output) ||
69
+ (commandName === 'agy' && hasAgyPromptReady(output)) ||
70
+ (commandName === 'grok' && hasGrokPromptReady(output)) ||
71
+ ((commandName === 'gemini' || commandName === 'qwen') && hasGeminiPromptReady(output)) ||
72
+ (commandName === 'opencode' && hasOpencodePromptReady(output)));
41
73
  };
42
74
  export const hasBracketedPasteAcknowledgement = (output, baselineLength) => {
43
75
  const outputAfterPaste = output.slice(baselineLength);
@@ -58,7 +90,10 @@ const retriesSubmit = (command) => getCommandName(command) === 'codex';
58
90
  const getPasteAckSettleDelayMs = (command) => getCommandName(command) === 'codex' ? CODEX_PASTE_ACK_SETTLE_DELAY_MS : PASTE_ACK_SETTLE_DELAY_MS;
59
91
  const getPasteAckTimeoutMs = (command) => getCommandName(command) === 'codex' ? CODEX_PASTE_ACK_TIMEOUT_MS : PASTE_ACK_TIMEOUT_MS;
60
92
  const usesBracketedPaste = (command) => COMMANDS_WITH_BRACKETED_PASTE.has(getCommandName(command));
61
- const canTimeoutBeforePromptReady = (command) => getCommandName(command) !== 'gemini';
93
+ const canTimeoutBeforePromptReady = (command) => {
94
+ const commandName = getCommandName(command);
95
+ return commandName !== 'agy' && commandName !== 'gemini';
96
+ };
62
97
  const isWritableRunStatus = (status) => status === undefined || status === 'starting' || status === 'running';
63
98
  const writeIfRunWritable = (agentManager, runId, text) => {
64
99
  let run;
@@ -73,9 +108,9 @@ const writeIfRunWritable = (agentManager, runId, text) => {
73
108
  agentManager.writeInput(runId, text);
74
109
  return true;
75
110
  };
76
- const submitPastedInteractiveInput = (agentManager, runId, text, baselineLength, waitForPasteAck, pasteAckSettleDelayMs, pasteAckTimeoutMs, retrySubmit, onDone, onError) => {
111
+ const submitPastedInteractiveInput = (agentManager, runId, command, text, baselineLength, waitForPasteAck, pasteAckSettleDelayMs, pasteAckTimeoutMs, retrySubmit, onDone, onError) => {
77
112
  const pastedAt = Date.now();
78
- const minDelay = getSubmitAfterPasteDelayMs(text);
113
+ const minDelay = getSubmitAfterPasteDelayMs(command, text);
79
114
  let acknowledgedAt = null;
80
115
  const getWritableOutput = () => {
81
116
  try {
@@ -164,7 +199,7 @@ export const createPostStartInputWriter = (agentManager, command) => {
164
199
  return (runId, text) => {
165
200
  // Synchronous write; an EPIPE/inactive failure still throws synchronously
166
201
  // (before the promise is returned), preserving the dispatcher's contract.
167
- if (!writeIfRunWritable(agentManager, runId, `${text}\n`)) {
202
+ if (!writeIfRunWritable(agentManager, runId, `${text}\r`)) {
168
203
  throw createRunInactiveError(runId);
169
204
  }
170
205
  return Promise.resolve();
@@ -194,7 +229,9 @@ export const createPostStartInputWriter = (agentManager, command) => {
194
229
  return;
195
230
  }
196
231
  if (hasInteractivePromptReady(output, command) ||
197
- (canTimeoutBeforePromptReady(command) && Date.now() - startedAt >= READY_TIMEOUT_MS)) {
232
+ (!hasCodexChoicePrompt(output, command) &&
233
+ canTimeoutBeforePromptReady(command) &&
234
+ Date.now() - startedAt >= READY_TIMEOUT_MS)) {
198
235
  const baselineLength = output.length;
199
236
  const input = usesBracketedPaste(command) ? toBracketedPasteSubmission(text) : text;
200
237
  try {
@@ -209,7 +246,7 @@ export const createPostStartInputWriter = (agentManager, command) => {
209
246
  rejectDone(toError(error));
210
247
  return;
211
248
  }
212
- submitPastedInteractiveInput(agentManager, runId, text, baselineLength, shouldWaitForPasteAck(command, text), getPasteAckSettleDelayMs(command), getPasteAckTimeoutMs(command), retriesSubmit(command), resolveDone, rejectDone);
249
+ submitPastedInteractiveInput(agentManager, runId, command, text, baselineLength, shouldWaitForPasteAck(command, text), getPasteAckSettleDelayMs(command), getPasteAckTimeoutMs(command), retriesSubmit(command), resolveDone, rejectDone);
213
250
  return;
214
251
  }
215
252
  setTimeout(tryWrite, READY_CHECK_INTERVAL_MS);
@@ -63,7 +63,9 @@ const shouldVerifySessionBeforeResume = (capture) => {
63
63
  const supportsPresetResume = (capture) => capture?.source === 'claude_project_jsonl_dir' ||
64
64
  capture?.source === 'codex_session_jsonl_dir' ||
65
65
  capture?.source === 'gemini_session_json_dir' ||
66
- capture?.source === 'opencode_session_db';
66
+ capture?.source === 'opencode_session_db' ||
67
+ capture?.source === 'qwen_session_json_dir' ||
68
+ capture?.source === 'stdout_regex';
67
69
  export const withPresetResumeArgs = (config, preset, lastSessionId, cwd, discriminator, onInvalidSessionId) => {
68
70
  const launchConfig = normalizeCodexNodeEntrypoint(config, preset);
69
71
  let nextConfig = withPresetYoloArgs(launchConfig, preset);
@@ -1,9 +1,10 @@
1
1
  import type { AgentSummary, WorkspaceSummary } from '../shared/types.js';
2
2
  import { type FeatureFlags } from './feature-flags.js';
3
3
  import type { RecoveryMessage } from './message-log-store.js';
4
- export declare const buildRecoverySummary: ({ agent, allTaskMessages, messages, tasksContent, workers, workspace, flags, }: {
4
+ export declare const buildRecoverySummary: ({ agent, allTaskMessages, memoryDigest, messages, tasksContent, workers, workspace, flags, }: {
5
5
  agent: AgentSummary;
6
6
  allTaskMessages?: RecoveryMessage[];
7
+ memoryDigest?: string | null | undefined;
7
8
  messages: RecoveryMessage[];
8
9
  tasksContent: string;
9
10
  workers: AgentSummary[];
@@ -65,7 +65,7 @@ const formatWorkers = (workers) => {
65
65
  return workers.map((worker) => `- ${worker.name} (${worker.role}, ${worker.status}, pending_task_count: ${worker.pendingTaskCount})`);
66
66
  };
67
67
  const getTaskSectionTitle = (agent) => agent.role === 'orchestrator' ? '## Tasks you dispatched' : '## Tasks recently sent to you';
68
- export const buildRecoverySummary = ({ agent, allTaskMessages, messages, tasksContent, workers, workspace, flags = FEATURE_FLAGS_ALL_OFF, }) => wrapSystemMessage([
68
+ export const buildRecoverySummary = ({ agent, allTaskMessages, memoryDigest, messages, tasksContent, workers, workspace, flags = FEATURE_FLAGS_ALL_OFF, }) => wrapSystemMessage([
69
69
  `You are ${agent.name} (${agent.role}) in workspace ${workspace.name}.`,
70
70
  'Hive just restarted you and could not recover via native session resume. Here is the handover context.',
71
71
  '',
@@ -84,6 +84,7 @@ export const buildRecoverySummary = ({ agent, allTaskMessages, messages, tasksCo
84
84
  '## Active workers',
85
85
  ...formatWorkers(workers),
86
86
  '',
87
+ ...(memoryDigest ? ['## Hive memory digest', memoryDigest, ''] : []),
87
88
  agent.role === 'orchestrator' ? '## Hive worker dispatch rules' : '## Hive worker boundaries',
88
89
  ...getHiveTeamRules(agent, flags),
89
90
  '',
@@ -0,0 +1,51 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ export type RemoteAuditResult = 'ok' | 'rejected' | 'error';
3
+ export type RemoteAuditAction = 'http' | 'ws_input' | 'ws_open' | 'session_open' | 'session_close' | 'revoke' | 'reject';
4
+ export interface RemoteAuditEvent {
5
+ /** M1 session device id. Audit/revocation tag only — never a permission branch. */
6
+ deviceId?: string | null;
7
+ /** Coarse category (see RemoteAuditAction). */
8
+ action: RemoteAuditAction;
9
+ /** Whitelisted path for http/ws actions (`/api/...`, `/ws/...`); omit otherwise. */
10
+ endpoint?: string | null;
11
+ /** Affected workspace, when the action is scoped to one. */
12
+ workspaceId?: string | null;
13
+ result: RemoteAuditResult;
14
+ /** Required-in-spirit on a rejection: the concrete reason (off-whitelist, revoked, …). */
15
+ rejectReason?: string | null;
16
+ /** WS-input byte count. The full chunk is NEVER stored. */
17
+ byteCount?: number | null;
18
+ /** Short truncated preview of WS input. Bounded here, not by the caller. */
19
+ preview?: string | null;
20
+ }
21
+ export interface RemoteAuditRecord {
22
+ id: number;
23
+ deviceId: string | null;
24
+ ts: number;
25
+ workspaceId: string | null;
26
+ action: string;
27
+ endpoint: string | null;
28
+ result: string;
29
+ rejectReason: string | null;
30
+ byteCount: number | null;
31
+ preview: string | null;
32
+ }
33
+ export declare const AUDIT_PREVIEW_MAX = 120;
34
+ export declare const createRemoteAuditStore: (db: Database) => {
35
+ /**
36
+ * Record an audit event. Returns immediately; the row is written on a
37
+ * later microtask. This is the ONLY method the tunnel calls on the hot path.
38
+ */
39
+ enqueue(event: RemoteAuditEvent, ts?: number): void;
40
+ /**
41
+ * Drain the buffer synchronously and await the pending write. Tests await
42
+ * this to assert rows deterministically; shutdown calls it to avoid losing
43
+ * the tail of the buffer. Idempotent when there's nothing pending.
44
+ */
45
+ flush(): Promise<void>;
46
+ /** Newest-first audit rows, capped at `limit`. Backs the Settings stream. */
47
+ list(limit?: number): RemoteAuditRecord[];
48
+ /** Newest-first rows for one device. Used by the device-detail view. */
49
+ listForDevice(deviceId: string, limit?: number): RemoteAuditRecord[];
50
+ };
51
+ export type RemoteAuditStore = ReturnType<typeof createRemoteAuditStore>;
@@ -0,0 +1,108 @@
1
+ // stdin previews are bounded so a paste of a megabyte of secrets can't end up in
2
+ // the audit table. byte_count carries the real size; preview is just enough to
3
+ // recognise the input in the Settings stream.
4
+ export const AUDIT_PREVIEW_MAX = 120;
5
+ const truncatePreview = (preview) => {
6
+ if (preview === null || preview === undefined)
7
+ return null;
8
+ if (preview.length <= AUDIT_PREVIEW_MAX)
9
+ return preview;
10
+ return `${preview.slice(0, AUDIT_PREVIEW_MAX)}…`;
11
+ };
12
+ export const createRemoteAuditStore = (db) => {
13
+ const insert = db.prepare(`INSERT INTO remote_audit
14
+ (remote_device_id, ts, workspace_id, action, endpoint, result, reject_reason, byte_count, preview)
15
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
16
+ const listStmt = db.prepare(`SELECT id, remote_device_id, ts, workspace_id, action, endpoint, result, reject_reason, byte_count, preview
17
+ FROM remote_audit
18
+ ORDER BY id DESC
19
+ LIMIT ?`);
20
+ const listForDeviceStmt = db.prepare(`SELECT id, remote_device_id, ts, workspace_id, action, endpoint, result, reject_reason, byte_count, preview
21
+ FROM remote_audit
22
+ WHERE remote_device_id = ?
23
+ ORDER BY id DESC
24
+ LIMIT ?`);
25
+ // Pending events buffered between flushes. enqueue() never touches SQLite
26
+ // directly — it appends here and arms a single microtask drain, so a burst of
27
+ // terminal keystrokes coalesces into one transaction instead of N.
28
+ const pending = [];
29
+ let flushScheduled = false;
30
+ let draining = null;
31
+ const writePending = () => {
32
+ if (pending.length === 0)
33
+ return;
34
+ const batch = pending.splice(0, pending.length);
35
+ const tx = db.transaction((events) => {
36
+ for (const e of events) {
37
+ insert.run(e.deviceId ?? null, e.ts, e.workspaceId ?? null, e.action, e.endpoint ?? null, e.result, e.rejectReason ?? null, typeof e.byteCount === 'number' ? e.byteCount : null, truncatePreview(e.preview));
38
+ }
39
+ });
40
+ tx(batch);
41
+ };
42
+ const scheduleFlush = () => {
43
+ if (flushScheduled)
44
+ return;
45
+ flushScheduled = true;
46
+ draining = new Promise((resolve) => {
47
+ // queueMicrotask keeps the write off the forwarding call stack while
48
+ // still landing before the event loop yields to I/O, so audit rows for a
49
+ // request are durable well before the response round-trips.
50
+ queueMicrotask(() => {
51
+ flushScheduled = false;
52
+ try {
53
+ writePending();
54
+ }
55
+ catch {
56
+ // A failed audit write must not crash the tunnel. Drop the batch and
57
+ // keep forwarding; losing an audit row is strictly better than
58
+ // dropping a user's terminal input.
59
+ }
60
+ resolve();
61
+ });
62
+ });
63
+ };
64
+ const mapRow = (row) => ({
65
+ id: row.id,
66
+ deviceId: row.remote_device_id,
67
+ ts: row.ts,
68
+ workspaceId: row.workspace_id,
69
+ action: row.action,
70
+ endpoint: row.endpoint,
71
+ result: row.result,
72
+ rejectReason: row.reject_reason,
73
+ byteCount: row.byte_count,
74
+ preview: row.preview,
75
+ });
76
+ return {
77
+ /**
78
+ * Record an audit event. Returns immediately; the row is written on a
79
+ * later microtask. This is the ONLY method the tunnel calls on the hot path.
80
+ */
81
+ enqueue(event, ts = Date.now()) {
82
+ pending.push({ ...event, ts });
83
+ scheduleFlush();
84
+ },
85
+ /**
86
+ * Drain the buffer synchronously and await the pending write. Tests await
87
+ * this to assert rows deterministically; shutdown calls it to avoid losing
88
+ * the tail of the buffer. Idempotent when there's nothing pending.
89
+ */
90
+ async flush() {
91
+ // Settle any already-scheduled drain first, then force-write whatever the
92
+ // caller enqueued after it was scheduled (or never scheduled at all).
93
+ if (draining)
94
+ await draining;
95
+ writePending();
96
+ },
97
+ /** Newest-first audit rows, capped at `limit`. Backs the Settings stream. */
98
+ list(limit = 100) {
99
+ writePending();
100
+ return listStmt.all(limit).map(mapRow);
101
+ },
102
+ /** Newest-first rows for one device. Used by the device-detail view. */
103
+ listForDevice(deviceId, limit = 100) {
104
+ writePending();
105
+ return listForDeviceStmt.all(deviceId, limit).map(mapRow);
106
+ },
107
+ };
108
+ };
@@ -0,0 +1,17 @@
1
+ export declare const REMOTE_GATEWAY_URL_KEY = "remote_gateway_url";
2
+ export declare const REMOTE_DAEMON_TOKEN_KEY = "remote_daemon_token";
3
+ export declare const REMOTE_DAEMON_ID_KEY = "remote_daemon_id";
4
+ export declare const REMOTE_ENABLED_KEY = "remote_enabled";
5
+ export declare const DEFAULT_GATEWAY_URL = "https://app.hivehq.dev";
6
+ export interface RemoteAppStateReader {
7
+ get(key: string): {
8
+ value: string | null;
9
+ } | undefined;
10
+ }
11
+ export interface RemoteConfigSource {
12
+ isEnabled(): boolean;
13
+ getGatewayUrl(): string | null;
14
+ getDaemonToken(): string | null;
15
+ getDaemonId(): string | null;
16
+ }
17
+ export declare const createRemoteConfigSource: (store: RemoteAppStateReader) => RemoteConfigSource;
@@ -0,0 +1,27 @@
1
+ // Remote-access config lives in the existing app_state KV. These are the
2
+ // canonical key literals — the single source of truth for BOTH sides of the
3
+ // feature: `hive remote login/logout` (src/cli/hive-remote.ts) writes them, and
4
+ // the daemon-side tunnel reads them. The CLI re-exports these so there is one
5
+ // definition, not two that can drift (a rename on only one side would silently
6
+ // stop login from reaching the tunnel).
7
+ //
8
+ // snake_case to match the existing app_state keys (active_workspace_id, …).
9
+ export const REMOTE_GATEWAY_URL_KEY = 'remote_gateway_url';
10
+ export const REMOTE_DAEMON_TOKEN_KEY = 'remote_daemon_token';
11
+ export const REMOTE_DAEMON_ID_KEY = 'remote_daemon_id';
12
+ export const REMOTE_ENABLED_KEY = 'remote_enabled';
13
+ export const DEFAULT_GATEWAY_URL = 'https://app.hivehq.dev';
14
+ // remote_enabled is ON only when it is exactly the string 'true'. Everything
15
+ // else — absent, '', 'false', '1', 'yes' — is OFF. This is invariant 4 (off ==
16
+ // zero behavior change): a truthy check would let a stray value silently arm
17
+ // the outbound tunnel, so the test pins the exact-string semantics.
18
+ const ENABLED_VALUE = 'true';
19
+ export const createRemoteConfigSource = (store) => {
20
+ const read = (key) => store.get(key)?.value ?? null;
21
+ return {
22
+ isEnabled: () => read(REMOTE_ENABLED_KEY) === ENABLED_VALUE,
23
+ getGatewayUrl: () => read(REMOTE_GATEWAY_URL_KEY),
24
+ getDaemonToken: () => read(REMOTE_DAEMON_TOKEN_KEY),
25
+ getDaemonId: () => read(REMOTE_DAEMON_ID_KEY),
26
+ };
27
+ };
@@ -0,0 +1,30 @@
1
+ export declare const GW_CONTROL_PREFIX = "\0gw:";
2
+ export declare const RelayCloseCode: {
3
+ readonly Normal: 1000;
4
+ readonly ProtocolError: 4400;
5
+ readonly Unauthorized: 4401;
6
+ readonly Forbidden: 4403;
7
+ readonly DaemonOffline: 4404;
8
+ readonly Replaced: 4409;
9
+ readonly Revoked: 4410;
10
+ readonly InternalError: 4500;
11
+ };
12
+ export type RelayCloseCode = (typeof RelayCloseCode)[keyof typeof RelayCloseCode];
13
+ export type GatewayControl = {
14
+ t: 'peer-online';
15
+ role: 'daemon' | 'device' | 'pair';
16
+ jti?: string;
17
+ } | {
18
+ t: 'peer-offline';
19
+ role: 'daemon' | 'device' | 'pair';
20
+ } | {
21
+ t: 'revoked';
22
+ reason: string;
23
+ } | {
24
+ t: 'error';
25
+ code: number;
26
+ message: string;
27
+ };
28
+ export declare const HB_PING = "hb:ping";
29
+ export declare const HB_PONG = "hb:pong";
30
+ export declare const isAuthFatalCloseCode: (code: number) => boolean;
@@ -0,0 +1,29 @@
1
+ // Daemon-side re-declaration of the gateway's control-band wire contract.
2
+ //
3
+ // The gateway lives in gateway/src/relay-do.ts — a separate Cloudflare-Workers package that is NOT
4
+ // import-reachable from src/ (importing it would drag in cloudflare:workers). So the daemon mirrors
5
+ // the few values it has to agree on, and tests/unit/remote-control-constants.test.ts pins each one
6
+ // to the literal gateway value so the wire format can never silently drift between the two sides.
7
+ // Control-frame sentinel. The gateway prefixes every control message (peer presence, revocation)
8
+ // with this so it can never be confused with an opaque binary E2E frame. relay-do.ts:29.
9
+ export const GW_CONTROL_PREFIX = '\x00gw:';
10
+ // WebSocket close codes (4xxx app range). relay-do.ts:32-41.
11
+ export const RelayCloseCode = {
12
+ Normal: 1000,
13
+ ProtocolError: 4400,
14
+ Unauthorized: 4401,
15
+ Forbidden: 4403,
16
+ DaemonOffline: 4404,
17
+ Replaced: 4409,
18
+ Revoked: 4410,
19
+ InternalError: 4500,
20
+ };
21
+ // Idle keepalive: the daemon SENDS this app-level string and the DO auto-replies HB_PONG WITHOUT
22
+ // waking (setWebSocketAutoResponse, relay-do.ts:144). A protocol-level ws.ping() would wake the DO
23
+ // and burn duration — the heartbeat MUST be this string, never ws.ping().
24
+ export const HB_PING = 'hb:ping';
25
+ export const HB_PONG = 'hb:pong';
26
+ // Only these two close codes are authoritative credential death: the daemon must LATCH (stop
27
+ // retrying until refresh() re-reads config). Every other close — including 4404 DaemonOffline,
28
+ // 4409 Replaced, and the transport 1006 — is transient and backs off + retries.
29
+ export const isAuthFatalCloseCode = (code) => code === RelayCloseCode.Unauthorized || code === RelayCloseCode.Revoked;