@tt-a1i/hive 1.4.4 → 1.5.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 (164) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +8 -0
  3. package/assets/qq-group.jpg +0 -0
  4. package/dist/bin/team.cmd +1 -0
  5. package/dist/src/cli/hive-update.d.ts +45 -17
  6. package/dist/src/cli/hive-update.js +63 -25
  7. package/dist/src/cli/hive.d.ts +25 -0
  8. package/dist/src/cli/hive.js +41 -3
  9. package/dist/src/cli/team.d.ts +1 -0
  10. package/dist/src/cli/team.js +199 -3
  11. package/dist/src/server/agent-command-resolver.js +3 -19
  12. package/dist/src/server/agent-manager-support.d.ts +2 -2
  13. package/dist/src/server/agent-manager-support.js +98 -24
  14. package/dist/src/server/agent-run-starter.d.ts +7 -1
  15. package/dist/src/server/agent-run-starter.js +9 -2
  16. package/dist/src/server/agent-run-store.d.ts +1 -1
  17. package/dist/src/server/agent-runtime-close.d.ts +1 -0
  18. package/dist/src/server/agent-runtime-close.js +25 -1
  19. package/dist/src/server/agent-runtime-contract.d.ts +2 -1
  20. package/dist/src/server/agent-runtime.d.ts +1 -1
  21. package/dist/src/server/agent-runtime.js +8 -2
  22. package/dist/src/server/agent-startup-instructions.d.ts +8 -1
  23. package/dist/src/server/agent-startup-instructions.js +15 -9
  24. package/dist/src/server/agent-stdin-dispatcher.d.ts +12 -5
  25. package/dist/src/server/agent-stdin-dispatcher.js +129 -40
  26. package/dist/src/server/cron-util.d.ts +7 -0
  27. package/dist/src/server/cron-util.js +19 -0
  28. package/dist/src/server/dispatch-ledger-store.d.ts +22 -0
  29. package/dist/src/server/dispatch-ledger-store.js +51 -3
  30. package/dist/src/server/env-sync-message.js +9 -9
  31. package/dist/src/server/fs-pick-folder.js +4 -0
  32. package/dist/src/server/fs-sandbox.js +36 -7
  33. package/dist/src/server/hive-team-guidance.d.ts +11 -6
  34. package/dist/src/server/hive-team-guidance.js +252 -71
  35. package/dist/src/server/live-run-registry.d.ts +1 -0
  36. package/dist/src/server/live-run-registry.js +1 -1
  37. package/dist/src/server/open-target-commands.js +5 -6
  38. package/dist/src/server/orchestrator-autostart.d.ts +12 -0
  39. package/dist/src/server/orchestrator-autostart.js +15 -13
  40. package/dist/src/server/path-canonicalization.d.ts +3 -0
  41. package/dist/src/server/path-canonicalization.js +29 -0
  42. package/dist/src/server/platform-path.d.ts +3 -0
  43. package/dist/src/server/platform-path.js +13 -0
  44. package/dist/src/server/post-start-input-writer.d.ts +1 -1
  45. package/dist/src/server/post-start-input-writer.js +110 -13
  46. package/dist/src/server/preset-launch-support.d.ts +1 -1
  47. package/dist/src/server/preset-launch-support.js +33 -2
  48. package/dist/src/server/recovery-summary.d.ts +6 -1
  49. package/dist/src/server/recovery-summary.js +17 -17
  50. package/dist/src/server/restart-policy-support.d.ts +6 -1
  51. package/dist/src/server/restart-policy-support.js +9 -1
  52. package/dist/src/server/restart-policy.d.ts +2 -2
  53. package/dist/src/server/restart-policy.js +3 -1
  54. package/dist/src/server/role-template-store.d.ts +1 -0
  55. package/dist/src/server/role-template-store.js +11 -1
  56. package/dist/src/server/route-types.d.ts +43 -0
  57. package/dist/src/server/routes-runtime.js +2 -1
  58. package/dist/src/server/routes-settings.js +76 -0
  59. package/dist/src/server/routes-team.js +211 -1
  60. package/dist/src/server/routes-workflow-schedules.d.ts +2 -0
  61. package/dist/src/server/routes-workflow-schedules.js +58 -0
  62. package/dist/src/server/routes-workflows.d.ts +2 -0
  63. package/dist/src/server/routes-workflows.js +83 -0
  64. package/dist/src/server/routes.js +4 -0
  65. package/dist/src/server/runtime-restart-policy.d.ts +3 -1
  66. package/dist/src/server/runtime-restart-policy.js +3 -1
  67. package/dist/src/server/runtime-store-contract.d.ts +122 -0
  68. package/dist/src/server/runtime-store-contract.js +1 -0
  69. package/dist/src/server/runtime-store-helpers.d.ts +9 -0
  70. package/dist/src/server/runtime-store-helpers.js +101 -2
  71. package/dist/src/server/runtime-store-workflows.d.ts +6 -0
  72. package/dist/src/server/runtime-store-workflows.js +100 -0
  73. package/dist/src/server/runtime-store.d.ts +3 -72
  74. package/dist/src/server/runtime-store.js +70 -4
  75. package/dist/src/server/session-capture-codex.d.ts +3 -3
  76. package/dist/src/server/session-capture-codex.js +9 -7
  77. package/dist/src/server/session-capture-gemini.d.ts +1 -1
  78. package/dist/src/server/session-capture-gemini.js +6 -3
  79. package/dist/src/server/settings-store.d.ts +3 -0
  80. package/dist/src/server/settings-store.js +1 -0
  81. package/dist/src/server/sqlite-schema-v19.d.ts +2 -0
  82. package/dist/src/server/sqlite-schema-v19.js +17 -0
  83. package/dist/src/server/sqlite-schema-v20.d.ts +2 -0
  84. package/dist/src/server/sqlite-schema-v20.js +20 -0
  85. package/dist/src/server/sqlite-schema-v21.d.ts +2 -0
  86. package/dist/src/server/sqlite-schema-v21.js +20 -0
  87. package/dist/src/server/sqlite-schema.d.ts +1 -1
  88. package/dist/src/server/sqlite-schema.js +97 -1
  89. package/dist/src/server/system-message.d.ts +7 -0
  90. package/dist/src/server/system-message.js +8 -1
  91. package/dist/src/server/tasks-file-watcher.d.ts +13 -1
  92. package/dist/src/server/tasks-file-watcher.js +127 -23
  93. package/dist/src/server/tasks-file.d.ts +2 -1
  94. package/dist/src/server/tasks-file.js +32 -9
  95. package/dist/src/server/tasks-websocket-server.js +13 -14
  96. package/dist/src/server/team-authz.d.ts +1 -1
  97. package/dist/src/server/team-authz.js +9 -1
  98. package/dist/src/server/team-autostaff.d.ts +16 -0
  99. package/dist/src/server/team-autostaff.js +16 -0
  100. package/dist/src/server/team-list-serializer.d.ts +1 -1
  101. package/dist/src/server/team-list-serializer.js +3 -1
  102. package/dist/src/server/team-operations.d.ts +15 -1
  103. package/dist/src/server/team-operations.js +116 -11
  104. package/dist/src/server/terminal-protocol.js +9 -3
  105. package/dist/src/server/terminal-stream-hub.js +16 -10
  106. package/dist/src/server/terminal-ws-server.js +10 -8
  107. package/dist/src/server/websocket-upgrade-safety.d.ts +10 -0
  108. package/dist/src/server/websocket-upgrade-safety.js +35 -0
  109. package/dist/src/server/windows-command-line.d.ts +3 -0
  110. package/dist/src/server/windows-command-line.js +9 -0
  111. package/dist/src/server/windows-filename.d.ts +2 -0
  112. package/dist/src/server/windows-filename.js +33 -0
  113. package/dist/src/server/workflow-cli-policy.d.ts +60 -0
  114. package/dist/src/server/workflow-cli-policy.js +110 -0
  115. package/dist/src/server/workflow-dispatch-awaiter.d.ts +12 -0
  116. package/dist/src/server/workflow-dispatch-awaiter.js +80 -0
  117. package/dist/src/server/workflow-feature.d.ts +15 -0
  118. package/dist/src/server/workflow-feature.js +15 -0
  119. package/dist/src/server/workflow-http-serializers.d.ts +64 -0
  120. package/dist/src/server/workflow-http-serializers.js +58 -0
  121. package/dist/src/server/workflow-run-log-store.d.ts +19 -0
  122. package/dist/src/server/workflow-run-log-store.js +45 -0
  123. package/dist/src/server/workflow-run-store.d.ts +50 -0
  124. package/dist/src/server/workflow-run-store.js +103 -0
  125. package/dist/src/server/workflow-runner.d.ts +147 -0
  126. package/dist/src/server/workflow-runner.js +401 -0
  127. package/dist/src/server/workflow-schedule-create.d.ts +14 -0
  128. package/dist/src/server/workflow-schedule-create.js +41 -0
  129. package/dist/src/server/workflow-schedule-store.d.ts +43 -0
  130. package/dist/src/server/workflow-schedule-store.js +112 -0
  131. package/dist/src/server/workflow-scheduler.d.ts +36 -0
  132. package/dist/src/server/workflow-scheduler.js +97 -0
  133. package/dist/src/server/workflow-script-loader.d.ts +34 -0
  134. package/dist/src/server/workflow-script-loader.js +106 -0
  135. package/dist/src/server/workspace-path-validation.js +16 -4
  136. package/dist/src/server/workspace-shell-runtime.d.ts +5 -0
  137. package/dist/src/server/workspace-shell-runtime.js +24 -2
  138. package/dist/src/server/workspace-store-contract.d.ts +4 -1
  139. package/dist/src/server/workspace-store-hydration.js +23 -7
  140. package/dist/src/server/workspace-store-mutations.js +2 -5
  141. package/dist/src/server/workspace-store-support.d.ts +4 -0
  142. package/dist/src/server/workspace-store-support.js +13 -1
  143. package/dist/src/server/workspace-store.js +38 -4
  144. package/dist/src/shared/types.d.ts +16 -1
  145. package/package.json +4 -2
  146. package/web/dist/assets/{AddWorkerDialog-DeZhTQLi.js → AddWorkerDialog-CcC-7kgG.js} +2 -2
  147. package/web/dist/assets/AddWorkspaceDialog-BDpOTfmt.js +1 -0
  148. package/web/dist/assets/{FirstRunWizard-B5wLcat5.js → FirstRunWizard-BYX_ocQn.js} +1 -1
  149. package/web/dist/assets/{MarketplaceDrawer-BC0eBOEW.js → MarketplaceDrawer-DUxSk7db.js} +1 -1
  150. package/web/dist/assets/WhatsNewDialog-B_RlCXcV.js +1 -0
  151. package/web/dist/assets/WorkerModal-D9-7YfZZ.js +1 -0
  152. package/web/dist/assets/WorkspaceTaskDrawer-BCKoF7qc.js +1 -0
  153. package/web/dist/assets/{WorkspaceTerminalPanels-CvibsPSd.js → WorkspaceTerminalPanels-Dq8y91t2.js} +1 -1
  154. package/web/dist/assets/index-BiOvKIVw.css +1 -0
  155. package/web/dist/assets/index-DMRUklT3.js +73 -0
  156. package/web/dist/assets/path-join-7MR1s7b1.js +1 -0
  157. package/web/dist/index.html +2 -2
  158. package/web/dist/sw.js +1 -1
  159. package/web/dist/assets/AddWorkspaceDialog-DDpXNEKf.js +0 -1
  160. package/web/dist/assets/WorkerModal-BwMHq-Bi.js +0 -1
  161. package/web/dist/assets/WorkspaceTaskDrawer-CxvT4nqs.js +0 -1
  162. package/web/dist/assets/index-BEsTmfrO.css +0 -1
  163. package/web/dist/assets/index-Ddb7bDN5.js +0 -75
  164. package/web/dist/assets/path-join-S7qkXQtP.js +0 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable user-facing changes will be documented in this file.
4
4
 
5
+ ## 1.5.0 - 2026-05-31
6
+
7
+ Workflow runtime, experimental team automation, and Codex reliability.
8
+
9
+ - Adds the experimental Hive workflow runtime: Orchestrators can author
10
+ multi-agent workflow scripts that fan out across real Hive PTY workers, show
11
+ runs in the Workflows drawer, stop runs, inspect run details, schedule
12
+ recurring workflows, and route workflow reports back into the Orchestrator.
13
+ - Adds workflow agent CLI policy settings so users can choose which CLI
14
+ workflow-created agents use by default and which CLIs are allowed.
15
+ - Adds the experimental auto-staff setting, letting the Orchestrator size the
16
+ worker roster to the task and prefer task-scoped ephemeral workers when it
17
+ needs temporary coders, testers, or reviewers.
18
+ - Adds an in-app What's New dialog so future upgrades can surface curated
19
+ release highlights without requiring users to read the changelog manually.
20
+ - Improves Codex reliability by waiting for pasted-content acknowledgements on
21
+ long dispatches while submitting short report/status injections quickly,
22
+ avoiding the several-second delay before reports reach the Orchestrator.
23
+ - Hardens Windows and runtime edge cases, including malformed WebSocket frames,
24
+ stale nvm4w Codex node entrypoints, workflow worker exits, and additional
25
+ workflow/runtime cleanup paths.
26
+
5
27
  ## 1.4.4 - 2026-05-29
6
28
 
7
29
  Windows portability and team protocol hardening.
package/README.md CHANGED
@@ -241,6 +241,14 @@ pnpm release:dry
241
241
 
242
242
  Hive 目前处于 alpha 阶段,核心流程已可用。当前重点是继续打磨多 Agent 协作体验、Windows 支持和更清晰的调度可观测性。欢迎试用、提 issue——反馈会直接影响后续节奏。
243
243
 
244
+ ## 交流群
245
+
246
+ 有问题、想反馈,或者就想聊聊 Agent 协作,欢迎进 QQ 群:**Ai Native 交流群**(群号 `1098836554`)。
247
+
248
+ <p align="center">
249
+ <img src="./assets/qq-group.jpg" width="240" alt="Ai Native 交流群 QQ 群二维码,群号 1098836554" />
250
+ </p>
251
+
244
252
  ## 在路上:跨 Agent 的长时记忆
245
253
 
246
254
  <p align="center">
Binary file
package/dist/bin/team.cmd CHANGED
@@ -1,2 +1,3 @@
1
1
  @echo off
2
+ setlocal DisableDelayedExpansion
2
3
  node "%~dp0..\src\cli\team.js" %*
@@ -4,26 +4,36 @@ export interface RunUpdateResult {
4
4
  spawnError?: Error;
5
5
  }
6
6
  export type RunUpdate = (command: string, args: readonly string[]) => Promise<RunUpdateResult>;
7
+ export interface SpawnInvocationPlan {
8
+ command: string;
9
+ args: string[];
10
+ options: {
11
+ stdio: 'inherit';
12
+ windowsHide?: boolean;
13
+ };
14
+ }
7
15
  /**
8
- * Build the spawn options for the upgrade child. The non-obvious part is
9
- * Windows: npm ships as `npm.cmd` (a batch shim), and Node 22+ refuses to
10
- * spawn `.cmd` / `.bat` files without `shell: true` after CVE-2024-27980.
11
- * Detect by file extension rather than by `process.platform` so the same
12
- * code path works for both real Windows runs and our cross-platform unit
13
- * tests (which inject `platform: 'win32'` so that `getNpmCommand` returns
14
- * `npm.cmd`).
16
+ * Plan how `defaultRunUpdate` should hand the npm invocation to
17
+ * `child_process.spawn`. Two non-obvious cases collapse here.
18
+ *
19
+ * 1. Node 22+ refuses to spawn `.cmd` / `.bat` files directly after
20
+ * CVE-2024-27980 unless `shell: true` is passed. We do not want
21
+ * `shell: true` though its arg-stringification path joins argv
22
+ * without quoting, so an install prefix containing spaces (the
23
+ * common Windows case `C:\Program Files\nodejs`) gets word-split
24
+ * by cmd.exe and `--prefix` only sees the first token, so npm
25
+ * silently installs hive to the wrong directory.
26
+ * 2. Wrapping with `cmd.exe /d /s /c <npm.cmd> <args>` solves both:
27
+ * spawning `cmd.exe` (a native exe) avoids the .cmd refusal, and
28
+ * Node's own argv-quoting builds an lpCommandLine where each
29
+ * space-containing arg is wrapped in double quotes that cmd.exe
30
+ * then re-parses correctly. `/d` skips AutoRun, `/s` keeps the
31
+ * quote-handling consistent with `/c`.
15
32
  *
16
- * Known limitation: with `shell: true` the args are stringified through
17
- * cmd.exe without quoting, so an install prefix containing spaces (e.g.
18
- * `C:\Program Files\nodejs`) will be tokenized incorrectly. The common
19
- * Windows prefix `%APPDATA%\npm` does not have this problem; fixing the
20
- * spaces case requires a verbatim `cmd.exe /d /s /c call npm.cmd …`
21
- * wrapper similar to `agent-command-resolver` and is tracked separately.
33
+ * Detect by filename suffix instead of `process.platform` so unit
34
+ * tests can inject `platform: 'win32'` and exercise the wrap.
22
35
  */
23
- export declare const buildSpawnOptionsForCommand: (command: string) => {
24
- shell: boolean;
25
- stdio: "inherit";
26
- };
36
+ export declare const planSpawnInvocation: (command: string, args: readonly string[], platform?: NodeJS.Platform) => SpawnInvocationPlan;
27
37
  /**
28
38
  * Signals the upgrade child should receive when the parent runtime is
29
39
  * interrupted. Beyond the POSIX-only SIGTERM/SIGINT, SIGHUP is what
@@ -33,6 +43,24 @@ export declare const buildSpawnOptionsForCommand: (command: string) => {
33
43
  * Windows exit paths.
34
44
  */
35
45
  export declare const FORWARDED_UPDATE_SIGNALS: readonly NodeJS.Signals[];
46
+ /**
47
+ * Forward a parent-process signal to the spawned npm child. On POSIX
48
+ * we hand the signal straight to the child; on Windows there are no
49
+ * real signals, so `child.kill(SIGTERM)` resolves to TerminateProcess
50
+ * against cmd.exe (our wrapper) only — npm itself, plus any install
51
+ * scripts it spawned, become orphans. `taskkill /pid <pid> /t /f`
52
+ * walks the wrapper's process tree so the whole branch dies together.
53
+ * If taskkill is unavailable (restricted PATH, locked-down policy)
54
+ * we fall back to `child.kill` so the wrapper at least exits.
55
+ *
56
+ * Exported so the win32 path can be unit-tested by injecting a stub
57
+ * `killTree` runner — the real `taskkillProcessTree` shells out, and
58
+ * we don't want that running during the test suite.
59
+ */
60
+ export declare const killUpdateChild: (child: {
61
+ pid?: number | undefined;
62
+ kill: (signal: NodeJS.Signals) => boolean;
63
+ }, signal: NodeJS.Signals, platform?: NodeJS.Platform, killTree?: (pid: number, onFailure?: () => void) => boolean) => void;
36
64
  export declare const defaultRunUpdate: RunUpdate;
37
65
  export declare const resolveHiveUpdateInstallArgs: (moduleUrl?: string) => string[];
38
66
  interface RunHiveUpdateOptions {
@@ -2,7 +2,9 @@ import { spawn } from 'node:child_process';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { basename, dirname, join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import { taskkillProcessTree } from '../server/agent-manager-support.js';
5
6
  import { getNpmCommand, INSTALL_COMMAND_ARGS, INSTALL_COMMAND_DISPLAY, PACKAGE_NAME, } from '../server/package-version.js';
7
+ import { buildCmdCallCommand } from '../server/windows-command-line.js';
6
8
  export const HIVE_UPDATE_USAGE = [
7
9
  'Usage:',
8
10
  ' hive update',
@@ -20,25 +22,36 @@ export const HIVE_UPDATE_USAGE = [
20
22
  ' -h, --help Print this help.',
21
23
  ].join('\n');
22
24
  /**
23
- * Build the spawn options for the upgrade child. The non-obvious part is
24
- * Windows: npm ships as `npm.cmd` (a batch shim), and Node 22+ refuses to
25
- * spawn `.cmd` / `.bat` files without `shell: true` after CVE-2024-27980.
26
- * Detect by file extension rather than by `process.platform` so the same
27
- * code path works for both real Windows runs and our cross-platform unit
28
- * tests (which inject `platform: 'win32'` so that `getNpmCommand` returns
29
- * `npm.cmd`).
25
+ * Plan how `defaultRunUpdate` should hand the npm invocation to
26
+ * `child_process.spawn`. Two non-obvious cases collapse here.
30
27
  *
31
- * Known limitation: with `shell: true` the args are stringified through
32
- * cmd.exe without quoting, so an install prefix containing spaces (e.g.
33
- * `C:\Program Files\nodejs`) will be tokenized incorrectly. The common
34
- * Windows prefix `%APPDATA%\npm` does not have this problem; fixing the
35
- * spaces case requires a verbatim `cmd.exe /d /s /c call npm.cmd …`
36
- * wrapper similar to `agent-command-resolver` and is tracked separately.
28
+ * 1. Node 22+ refuses to spawn `.cmd` / `.bat` files directly after
29
+ * CVE-2024-27980 unless `shell: true` is passed. We do not want
30
+ * `shell: true` though its arg-stringification path joins argv
31
+ * without quoting, so an install prefix containing spaces (the
32
+ * common Windows case `C:\Program Files\nodejs`) gets word-split
33
+ * by cmd.exe and `--prefix` only sees the first token, so npm
34
+ * silently installs hive to the wrong directory.
35
+ * 2. Wrapping with `cmd.exe /d /s /c <npm.cmd> <args>` solves both:
36
+ * spawning `cmd.exe` (a native exe) avoids the .cmd refusal, and
37
+ * Node's own argv-quoting builds an lpCommandLine where each
38
+ * space-containing arg is wrapped in double quotes that cmd.exe
39
+ * then re-parses correctly. `/d` skips AutoRun, `/s` keeps the
40
+ * quote-handling consistent with `/c`.
41
+ *
42
+ * Detect by filename suffix instead of `process.platform` so unit
43
+ * tests can inject `platform: 'win32'` and exercise the wrap.
37
44
  */
38
- export const buildSpawnOptionsForCommand = (command) => ({
39
- shell: /\.(cmd|bat)$/i.test(command),
40
- stdio: 'inherit',
41
- });
45
+ export const planSpawnInvocation = (command, args, platform = process.platform) => {
46
+ if (platform === 'win32' && /\.(cmd|bat)$/i.test(command)) {
47
+ return {
48
+ command: 'cmd.exe',
49
+ args: ['/d', '/s', '/c', buildCmdCallCommand(command, args)],
50
+ options: { stdio: 'inherit', windowsHide: false },
51
+ };
52
+ }
53
+ return { command, args: [...args], options: { stdio: 'inherit' } };
54
+ };
42
55
  /**
43
56
  * Signals the upgrade child should receive when the parent runtime is
44
57
  * interrupted. Beyond the POSIX-only SIGTERM/SIGINT, SIGHUP is what
@@ -53,21 +66,46 @@ export const FORWARDED_UPDATE_SIGNALS = [
53
66
  'SIGHUP',
54
67
  'SIGBREAK',
55
68
  ];
69
+ /**
70
+ * Forward a parent-process signal to the spawned npm child. On POSIX
71
+ * we hand the signal straight to the child; on Windows there are no
72
+ * real signals, so `child.kill(SIGTERM)` resolves to TerminateProcess
73
+ * against cmd.exe (our wrapper) only — npm itself, plus any install
74
+ * scripts it spawned, become orphans. `taskkill /pid <pid> /t /f`
75
+ * walks the wrapper's process tree so the whole branch dies together.
76
+ * If taskkill is unavailable (restricted PATH, locked-down policy)
77
+ * we fall back to `child.kill` so the wrapper at least exits.
78
+ *
79
+ * Exported so the win32 path can be unit-tested by injecting a stub
80
+ * `killTree` runner — the real `taskkillProcessTree` shells out, and
81
+ * we don't want that running during the test suite.
82
+ */
83
+ export const killUpdateChild = (child, signal, platform = process.platform, killTree = (pid, onFailure) => taskkillProcessTree(pid, platform, undefined, onFailure)) => {
84
+ const fallback = () => {
85
+ try {
86
+ child.kill(signal);
87
+ }
88
+ catch {
89
+ // child.kill on Windows throws if the signal name isn't
90
+ // implemented; we forward what we can and ignore the rest.
91
+ }
92
+ };
93
+ if (platform === 'win32' && typeof child.pid === 'number' && child.pid > 0) {
94
+ if (killTree(child.pid, fallback))
95
+ return;
96
+ }
97
+ fallback();
98
+ };
56
99
  export const defaultRunUpdate = (command, args) => new Promise((resolve) => {
57
- const child = spawn(command, [...args], buildSpawnOptionsForCommand(command));
100
+ const plan = planSpawnInvocation(command, args);
101
+ const child = spawn(plan.command, plan.args, plan.options);
58
102
  let resolved = false;
59
103
  // Handlers are registered with `once` so they don't accumulate
60
104
  // across invocations and explicitly removed at finalize().
61
105
  const handlers = new Map();
62
106
  for (const signal of FORWARDED_UPDATE_SIGNALS) {
63
107
  const handler = () => {
64
- try {
65
- child.kill(signal);
66
- }
67
- catch {
68
- // child.kill on Windows throws if the signal name isn't
69
- // implemented; we forward what we can and ignore the rest.
70
- }
108
+ killUpdateChild(child, signal);
71
109
  };
72
110
  handlers.set(signal, handler);
73
111
  process.once(signal, handler);
@@ -33,6 +33,31 @@ type RunHiveCommandOptions = {
33
33
  export declare const SHUTDOWN_SIGNALS: readonly ["SIGINT", "SIGTERM", "SIGHUP", "SIGBREAK"];
34
34
  export declare const HIVE_USAGE: string;
35
35
  export declare const handleHiveInfoCommand: (argv: string[]) => boolean;
36
+ /**
37
+ * Resolve the directory where Hive persists its SQLite DB and supporting
38
+ * state. Platform-aware because `~/.config/hive` is a hidden dot-directory
39
+ * convention that Windows Explorer treats as second-class — Windows users
40
+ * can't navigate there from the address bar without typing the full path,
41
+ * and the standard roaming-profile location is `%APPDATA%\<app>` instead.
42
+ *
43
+ * Resolution order:
44
+ * 1. HIVE_DATA_DIR override (any platform, for tests / opinionated users).
45
+ * 2. Windows: %APPDATA%\hive — roaming user state, follows the user
46
+ * across machines on a domain profile. APPDATA, not LOCALAPPDATA,
47
+ * because Hive's DB is user data, not a machine-local cache.
48
+ * Falls back to homedir()\AppData\Roaming\hive when APPDATA is
49
+ * stripped from the env (some Windows Task Scheduler configs do this).
50
+ * 3. POSIX: $XDG_CONFIG_HOME/hive, falling back to ~/.config/hive.
51
+ *
52
+ * Migration: pre-fix Windows installs wrote to ~/.config/hive. When that
53
+ * legacy directory exists but the new %APPDATA%\hive does not, prefer the
54
+ * legacy path so an upgrade does not surface as an empty workspace list.
55
+ * This is a one-way ratchet — once the new location is populated, it wins.
56
+ *
57
+ * Exported so the resolution rules are unit-testable without touching env
58
+ * or the real filesystem.
59
+ */
60
+ export declare const resolveDataDir: (platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv, pathExists?: (path: string) => boolean) => string;
36
61
  /**
37
62
  * Recovery hint formatter for the "port already in use" error. Platform-aware
38
63
  * because the lsof / xargs / kill pipeline is POSIX-only; on Windows a user
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { once } from 'node:events';
3
- import { realpathSync } from 'node:fs';
3
+ import { existsSync } from 'node:fs';
4
4
  import { homedir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { createAgentManager } from '../server/agent-manager.js';
8
8
  import { createApp } from '../server/app.js';
9
9
  import { readPackageVersion } from '../server/package-version.js';
10
+ import { sameFilesystemPath } from '../server/path-canonicalization.js';
10
11
  import { createRuntimeStore } from '../server/runtime-store.js';
11
12
  import { createVersionService } from '../server/version-service.js';
12
13
  import { runHiveUpdateCommand } from './hive-update.js';
@@ -80,7 +81,44 @@ const parsePort = (argv) => {
80
81
  }
81
82
  return parsedPort ?? 3000;
82
83
  };
83
- const resolveDataDir = () => process.env.HIVE_DATA_DIR || join(homedir(), '.config', 'hive');
84
+ /**
85
+ * Resolve the directory where Hive persists its SQLite DB and supporting
86
+ * state. Platform-aware because `~/.config/hive` is a hidden dot-directory
87
+ * convention that Windows Explorer treats as second-class — Windows users
88
+ * can't navigate there from the address bar without typing the full path,
89
+ * and the standard roaming-profile location is `%APPDATA%\<app>` instead.
90
+ *
91
+ * Resolution order:
92
+ * 1. HIVE_DATA_DIR override (any platform, for tests / opinionated users).
93
+ * 2. Windows: %APPDATA%\hive — roaming user state, follows the user
94
+ * across machines on a domain profile. APPDATA, not LOCALAPPDATA,
95
+ * because Hive's DB is user data, not a machine-local cache.
96
+ * Falls back to homedir()\AppData\Roaming\hive when APPDATA is
97
+ * stripped from the env (some Windows Task Scheduler configs do this).
98
+ * 3. POSIX: $XDG_CONFIG_HOME/hive, falling back to ~/.config/hive.
99
+ *
100
+ * Migration: pre-fix Windows installs wrote to ~/.config/hive. When that
101
+ * legacy directory exists but the new %APPDATA%\hive does not, prefer the
102
+ * legacy path so an upgrade does not surface as an empty workspace list.
103
+ * This is a one-way ratchet — once the new location is populated, it wins.
104
+ *
105
+ * Exported so the resolution rules are unit-testable without touching env
106
+ * or the real filesystem.
107
+ */
108
+ export const resolveDataDir = (platform = process.platform, env = process.env, pathExists = existsSync) => {
109
+ const override = env.HIVE_DATA_DIR;
110
+ if (override)
111
+ return override;
112
+ if (platform === 'win32') {
113
+ const appData = env.APPDATA ?? join(homedir(), 'AppData', 'Roaming');
114
+ const target = join(appData, 'hive');
115
+ const legacy = join(homedir(), '.config', 'hive');
116
+ if (!pathExists(target) && pathExists(legacy))
117
+ return legacy;
118
+ return target;
119
+ }
120
+ return join(env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'hive');
121
+ };
84
122
  const maybePrintUpdateHint = async (versionService) => {
85
123
  const info = await versionService.getVersionInfo();
86
124
  if (!info.update_available)
@@ -210,7 +248,7 @@ export const runHiveCommand = async (argv, options = {}) => {
210
248
  };
211
249
  };
212
250
  const isMainModule = process.argv[1]
213
- ? fileURLToPath(import.meta.url) === realpathSync(process.argv[1])
251
+ ? sameFilesystemPath(fileURLToPath(import.meta.url), process.argv[1])
214
252
  : false;
215
253
  if (isMainModule) {
216
254
  const argv = process.argv.slice(2);
@@ -10,6 +10,7 @@ export interface ParsedReportArgs {
10
10
  }
11
11
  export declare const parseReportArgs: (args: string[], command?: string) => ParsedReportArgs;
12
12
  export declare const parseCancelArgs: (args: string[]) => ParsedCancelArgs;
13
+ export declare const decodeStdinBuffer: (buffer: Buffer) => string;
13
14
  export declare const readStdinToString: (command?: string) => Promise<string>;
14
15
  export declare const runTeamCommand: (argv: string[]) => Promise<void>;
15
16
  export {};
@@ -1,5 +1,5 @@
1
- import { realpathSync } from 'node:fs';
2
1
  import { fileURLToPath } from 'node:url';
2
+ import { sameFilesystemPath } from '../server/path-canonicalization.js';
3
3
  const REQUIRED_ENV_KEYS = [
4
4
  'HIVE_PORT',
5
5
  'HIVE_PROJECT_ID',
@@ -10,6 +10,13 @@ const TEAM_USAGE = [
10
10
  'Usage:',
11
11
  ' team list',
12
12
  ' team send <worker-name> "<task>"',
13
+ ' team spawn <role> [--name <name>] [--cli <claude|codex|opencode|gemini>] [--ephemeral]',
14
+ ' team dismiss <worker-name>',
15
+ " team workflow run --stdin [--args '<JSON>'] (script from stdin — for multi-line scripts)",
16
+ ' team workflow run --inline "<source>" [--args \'<JSON>\']',
17
+ ' team workflow stop <run-id>',
18
+ ' team workflow show <run-id> (full per-agent transcript for one run)',
19
+ ' team workflow schedule --cron "<cron>" --name <n> --stdin (register a recurring run)',
13
20
  ' team cancel --dispatch <dispatch-id> "<reason>"',
14
21
  ' team report "<result>" [--dispatch <dispatch-id>] [--artifact <path>]',
15
22
  ' team report --stdin [--dispatch <dispatch-id>] [--artifact <path>]',
@@ -35,6 +42,15 @@ const getHiveEnv = () => {
35
42
  return values;
36
43
  };
37
44
  const getBaseUrl = (env) => `http://127.0.0.1:${env.HIVE_PORT}`;
45
+ // Read `--flag value` from an argv slice; returns undefined when absent or
46
+ // when the flag is the last token with no following value.
47
+ const readFlag = (args, flag) => {
48
+ const index = args.indexOf(flag);
49
+ if (index === -1)
50
+ return undefined;
51
+ const value = args[index + 1];
52
+ return value && !value.startsWith('--') ? value : undefined;
53
+ };
38
54
  const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
39
55
  const describeFetchError = (baseUrl, error) => {
40
56
  const cause = error instanceof Error && error.cause instanceof Error ? ` (${error.cause.message})` : '';
@@ -177,6 +193,20 @@ export const parseCancelArgs = (args) => {
177
193
  }
178
194
  return { dispatchId, reason };
179
195
  };
196
+ export const decodeStdinBuffer = (buffer) => {
197
+ if (buffer.length >= 3 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) {
198
+ return buffer.subarray(3).toString('utf8');
199
+ }
200
+ if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) {
201
+ return buffer.subarray(2).toString('utf16le');
202
+ }
203
+ if (buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff) {
204
+ const swapped = Buffer.from(buffer.subarray(2));
205
+ swapped.swap16();
206
+ return swapped.toString('utf16le');
207
+ }
208
+ return buffer.toString('utf8');
209
+ };
180
210
  export const readStdinToString = async (command = 'report') => {
181
211
  if (process.stdin.isTTY) {
182
212
  throw new Error(withUsage('--stdin requires piped input, but stdin is a TTY. Did you forget to pipe content in?', command));
@@ -185,7 +215,7 @@ export const readStdinToString = async (command = 'report') => {
185
215
  for await (const chunk of process.stdin) {
186
216
  chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
187
217
  }
188
- const content = Buffer.concat(chunks).toString('utf8');
218
+ const content = decodeStdinBuffer(Buffer.concat(chunks));
189
219
  if (!content.trim()) {
190
220
  throw new Error(withUsage('--stdin received empty input', command));
191
221
  }
@@ -241,6 +271,172 @@ export const runTeamCommand = async (argv) => {
241
271
  console.log(JSON.stringify(payload));
242
272
  return;
243
273
  }
274
+ if (command === 'spawn') {
275
+ const role = args[0];
276
+ if (!role || role.startsWith('--')) {
277
+ throw new Error('Usage: team spawn <role> [--name <name>] [--cli <claude|codex|opencode|gemini>] [--ephemeral]\n' +
278
+ ' Default: persistent member (lives until you `team dismiss` it).\n' +
279
+ ' --ephemeral: auto-dismiss after the next dispatch report (one-shot worker).');
280
+ }
281
+ const name = readFlag(args, '--name');
282
+ const cli = readFlag(args, '--cli');
283
+ const ephemeral = args.includes('--ephemeral');
284
+ const env = getHiveEnv();
285
+ const response = await postJson(getBaseUrl(env), '/api/team/spawn', {
286
+ project_id: env.HIVE_PROJECT_ID,
287
+ from_agent_id: env.HIVE_AGENT_ID,
288
+ token: env.HIVE_AGENT_TOKEN,
289
+ role,
290
+ ...(name ? { name } : {}),
291
+ ...(cli ? { cli } : {}),
292
+ ...(ephemeral ? { ephemeral: true } : {}),
293
+ });
294
+ console.log(JSON.stringify(await response.json()));
295
+ return;
296
+ }
297
+ if (command === 'dismiss') {
298
+ const workerName = args[0];
299
+ if (!workerName || workerName.startsWith('--')) {
300
+ throw new Error('Usage: team dismiss <worker-name>');
301
+ }
302
+ const env = getHiveEnv();
303
+ const response = await postJson(getBaseUrl(env), '/api/team/dismiss', {
304
+ project_id: env.HIVE_PROJECT_ID,
305
+ from_agent_id: env.HIVE_AGENT_ID,
306
+ token: env.HIVE_AGENT_TOKEN,
307
+ name: workerName,
308
+ });
309
+ console.log(JSON.stringify(await response.json()));
310
+ return;
311
+ }
312
+ if (command === 'workflow') {
313
+ const sub = args[0];
314
+ const rest = args.slice(1);
315
+ if (sub === 'run') {
316
+ const inlineFlag = rest.indexOf('--inline');
317
+ const stdinFlag = rest.includes('--stdin');
318
+ const name = readFlag(rest, '--name');
319
+ // TIER 2 #8 — `--args '<JSON>'` makes the script's `args` global a
320
+ // real value instead of always undefined. Parses lazily so a bad
321
+ // JSON gives a clear local error before the HTTP round-trip.
322
+ const rawArgs = readFlag(rest, '--args');
323
+ let parsedArgs;
324
+ if (rawArgs !== undefined) {
325
+ try {
326
+ parsedArgs = JSON.parse(rawArgs);
327
+ }
328
+ catch (error) {
329
+ throw new Error(`Usage: team workflow run … --args '<JSON>'\n --args must be valid JSON; got: ${error instanceof Error ? error.message : String(error)}`);
330
+ }
331
+ }
332
+ let source;
333
+ if (inlineFlag !== -1) {
334
+ const literal = rest[inlineFlag + 1];
335
+ if (!literal)
336
+ throw new Error('Usage: team workflow run --inline "<script-source>"');
337
+ source = literal;
338
+ }
339
+ else if (stdinFlag) {
340
+ source = await readStdinToString('workflow run');
341
+ }
342
+ else {
343
+ throw new Error('Usage: team workflow run --stdin | team workflow run --inline "<script-source>"\n' +
344
+ ' Pass workflow source via stdin (POSIX heredoc / `type x.ts |`) or as one inline arg.\n' +
345
+ " Optional: --args '<JSON>' makes the script's `args` global a real value.");
346
+ }
347
+ const env = getHiveEnv();
348
+ const response = await postJson(getBaseUrl(env), '/api/team/workflow/run', {
349
+ project_id: env.HIVE_PROJECT_ID,
350
+ from_agent_id: env.HIVE_AGENT_ID,
351
+ token: env.HIVE_AGENT_TOKEN,
352
+ source,
353
+ ...(name ? { name } : {}),
354
+ ...(parsedArgs !== undefined ? { args: parsedArgs } : {}),
355
+ });
356
+ console.log(JSON.stringify(await response.json()));
357
+ return;
358
+ }
359
+ if (sub === 'stop') {
360
+ const runId = rest[0];
361
+ if (!runId)
362
+ throw new Error('Usage: team workflow stop <run-id>');
363
+ const env = getHiveEnv();
364
+ const response = await postJson(getBaseUrl(env), '/api/team/workflow/stop', {
365
+ project_id: env.HIVE_PROJECT_ID,
366
+ from_agent_id: env.HIVE_AGENT_ID,
367
+ token: env.HIVE_AGENT_TOKEN,
368
+ run_id: runId,
369
+ });
370
+ console.log(JSON.stringify(await response.json()));
371
+ return;
372
+ }
373
+ if (sub === 'show') {
374
+ const runId = rest[0];
375
+ if (!runId)
376
+ throw new Error('Usage: team workflow show <run-id>');
377
+ const env = getHiveEnv();
378
+ const response = await postJson(getBaseUrl(env), '/api/team/workflow/show', {
379
+ project_id: env.HIVE_PROJECT_ID,
380
+ from_agent_id: env.HIVE_AGENT_ID,
381
+ token: env.HIVE_AGENT_TOKEN,
382
+ run_id: runId,
383
+ });
384
+ console.log(JSON.stringify(await response.json()));
385
+ return;
386
+ }
387
+ if (sub === 'schedule') {
388
+ const usage = 'Usage: team workflow schedule --cron "<5-field cron>" --name <name> --stdin\n' +
389
+ ' team workflow schedule --cron "<cron>" --name <name> --inline "<source>" [--args \'<JSON>\']\n' +
390
+ ' Registers a recurring run. Source is persisted so cron can fire it with no orchestrator present.';
391
+ const inlineFlag = rest.indexOf('--inline');
392
+ const stdinFlag = rest.includes('--stdin');
393
+ const cron = readFlag(rest, '--cron');
394
+ const name = readFlag(rest, '--name');
395
+ if (!cron || !name)
396
+ throw new Error(usage);
397
+ const rawArgs = readFlag(rest, '--args');
398
+ let parsedArgs;
399
+ if (rawArgs !== undefined) {
400
+ try {
401
+ parsedArgs = JSON.parse(rawArgs);
402
+ }
403
+ catch (error) {
404
+ throw new Error(`Usage: team workflow schedule … --args '<JSON>'\n --args must be valid JSON; got: ${error instanceof Error ? error.message : String(error)}`);
405
+ }
406
+ }
407
+ let source;
408
+ if (inlineFlag !== -1) {
409
+ const literal = rest[inlineFlag + 1];
410
+ if (!literal)
411
+ throw new Error(usage);
412
+ source = literal;
413
+ }
414
+ else if (stdinFlag) {
415
+ source = await readStdinToString('workflow schedule');
416
+ }
417
+ else {
418
+ throw new Error(usage);
419
+ }
420
+ const env = getHiveEnv();
421
+ const response = await postJson(getBaseUrl(env), '/api/team/workflow/schedule', {
422
+ project_id: env.HIVE_PROJECT_ID,
423
+ from_agent_id: env.HIVE_AGENT_ID,
424
+ token: env.HIVE_AGENT_TOKEN,
425
+ source,
426
+ name,
427
+ cron,
428
+ ...(parsedArgs !== undefined ? { args: parsedArgs } : {}),
429
+ });
430
+ console.log(JSON.stringify(await response.json()));
431
+ return;
432
+ }
433
+ throw new Error('Usage:\n' +
434
+ " team workflow run --stdin [--args '<JSON>'] (read script from stdin)\n" +
435
+ ' team workflow run --inline "<source>" [--args ...] (one-arg form)\n' +
436
+ ' team workflow stop <run-id> (cancel a running workflow)\n' +
437
+ ' team workflow show <run-id> (full per-agent transcript)\n' +
438
+ ' team workflow schedule --cron "<cron>" --name <n> --stdin (register a recurring run)');
439
+ }
244
440
  if (command === 'cancel') {
245
441
  const cancel = parseCancelArgs(args);
246
442
  const env = getHiveEnv();
@@ -294,7 +490,7 @@ export const runTeamCommand = async (argv) => {
294
490
  throw new Error('Unsupported team command');
295
491
  };
296
492
  const isMainModule = process.argv[1]
297
- ? fileURLToPath(import.meta.url) === realpathSync(process.argv[1])
493
+ ? sameFilesystemPath(fileURLToPath(import.meta.url), process.argv[1])
298
494
  : false;
299
495
  if (isMainModule) {
300
496
  void runTeamCommand(process.argv.slice(2)).catch((error) => {
@@ -1,5 +1,6 @@
1
1
  import { accessSync, constants } from 'node:fs';
2
2
  import { basename, delimiter, extname, isAbsolute, join } from 'node:path';
3
+ import { buildCmdCallCommand } from './windows-command-line.js';
3
4
  const hasPathSeparator = (command) => command.includes('/') || command.includes('\\');
4
5
  const canExecute = (path, platform = process.platform) => {
5
6
  try {
@@ -54,28 +55,11 @@ const isWindowsBatchFile = (command) => {
54
55
  const extension = extname(command).toLowerCase();
55
56
  return extension === '.cmd' || extension === '.bat';
56
57
  };
57
- /**
58
- * cmd.exe-style escape: doubles inner `"` (cmd's only literal-quote idiom)
59
- * and only wraps in `"..."` when the token contains whitespace or a cmd
60
- * metachar. Plain alnum/path tokens stay unquoted, which is the form
61
- * cmd.exe parses most predictably.
62
- *
63
- * Crucially, we do NOT use the `\"` form that the previous implementation
64
- * used and that node-pty's `argsToCommandLine` produces: cmd doesn't honor
65
- * backslash-quote escapes in its own command-line parsing.
66
- */
67
- const escapeCmdToken = (value) => {
68
- if (value.length === 0)
69
- return '""';
70
- const doubled = value.replace(/"/g, '""');
71
- return /[\s"&<>|^()]/.test(value) ? `"${doubled}"` : doubled;
72
- };
73
58
  const buildWindowsBatchCommandLine = (command, args) => {
74
- const tokens = [command, ...args].map(escapeCmdToken).join(' ');
75
59
  // `call` is cmd's built-in batch invocation; it handles quoted .cmd / .bat
76
60
  // paths reliably (this is the same pattern Node.js's child_process uses
77
61
  // internally on Windows since the CVE-2024-27980 fix).
78
- return `/d /s /c call ${tokens}`;
62
+ return `/d /s /c ${buildCmdCallCommand(command, args)}`;
79
63
  };
80
64
  /**
81
65
  * Recognize the exact shape that `createStartupCommandLaunch` produces on
@@ -93,7 +77,7 @@ const isCmdExeShellLaunch = (resolvedCommand, args) => basename(resolvedCommand)
93
77
  args.length === 4 &&
94
78
  args[0] === '/d' &&
95
79
  args[1] === '/s' &&
96
- args[2] === '/c';
80
+ (args[2] === '/c' || args[2] === '/k');
97
81
  export const resolveSpawnCommand = (command, cwd, env, args = [], platform = process.platform) => {
98
82
  const resolvedCommand = resolveCommandPath(command, cwd, env, platform);
99
83
  if (platform === 'win32' && isWindowsBatchFile(resolvedCommand)) {