@tt-a1i/hive 1.4.3 → 1.4.4

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 (54) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.en.md +5 -4
  3. package/README.md +1 -1
  4. package/dist/src/cli/hive-update.d.ts +29 -0
  5. package/dist/src/cli/hive-update.js +54 -15
  6. package/dist/src/cli/hive.d.ts +32 -0
  7. package/dist/src/cli/hive.js +72 -17
  8. package/dist/src/cli/team.js +17 -5
  9. package/dist/src/server/agent-command-resolver.d.ts +10 -1
  10. package/dist/src/server/agent-command-resolver.js +48 -4
  11. package/dist/src/server/agent-launch-resolver.js +9 -3
  12. package/dist/src/server/agent-manager-support.d.ts +28 -0
  13. package/dist/src/server/agent-manager-support.js +58 -4
  14. package/dist/src/server/agent-run-bootstrap.d.ts +17 -1
  15. package/dist/src/server/agent-run-bootstrap.js +30 -2
  16. package/dist/src/server/agent-startup-instructions.js +1 -1
  17. package/dist/src/server/app.d.ts +1 -0
  18. package/dist/src/server/app.js +12 -2
  19. package/dist/src/server/fs-browse.d.ts +14 -1
  20. package/dist/src/server/fs-browse.js +48 -5
  21. package/dist/src/server/fs-pick-folder.js +54 -11
  22. package/dist/src/server/hive-team-guidance.js +5 -4
  23. package/dist/src/server/open-target-commands.js +30 -4
  24. package/dist/src/server/post-start-input-writer.js +6 -3
  25. package/dist/src/server/routes-team.js +10 -1
  26. package/dist/src/server/runtime-store.d.ts +3 -1
  27. package/dist/src/server/session-capture-claude.d.ts +23 -0
  28. package/dist/src/server/session-capture-claude.js +24 -1
  29. package/dist/src/server/session-capture-opencode.d.ts +18 -0
  30. package/dist/src/server/session-capture-opencode.js +27 -2
  31. package/dist/src/server/startup-command-parser.d.ts +15 -0
  32. package/dist/src/server/startup-command-parser.js +33 -2
  33. package/dist/src/server/tasks-file-watcher.d.ts +26 -0
  34. package/dist/src/server/tasks-file-watcher.js +29 -3
  35. package/dist/src/server/team-operations.d.ts +5 -1
  36. package/dist/src/server/team-operations.js +44 -3
  37. package/dist/src/server/terminal-input-profile.js +2 -8
  38. package/dist/src/server/terminal-ws-server.js +26 -8
  39. package/package.json +2 -2
  40. package/web/dist/assets/{AddWorkerDialog-DmkDOdp6.js → AddWorkerDialog-DeZhTQLi.js} +2 -2
  41. package/web/dist/assets/AddWorkspaceDialog-DDpXNEKf.js +1 -0
  42. package/web/dist/assets/{FirstRunWizard-SAd1wsH4.js → FirstRunWizard-B5wLcat5.js} +1 -1
  43. package/web/dist/assets/{MarketplaceDrawer-B_8aG2uT.js → MarketplaceDrawer-BC0eBOEW.js} +1 -1
  44. package/web/dist/assets/{WorkerModal-CQmjiPme.js → WorkerModal-BwMHq-Bi.js} +1 -1
  45. package/web/dist/assets/WorkspaceTaskDrawer-CxvT4nqs.js +1 -0
  46. package/web/dist/assets/{WorkspaceTerminalPanels-BReWh1YL.js → WorkspaceTerminalPanels-CvibsPSd.js} +1 -1
  47. package/web/dist/assets/index-Ddb7bDN5.js +75 -0
  48. package/web/dist/assets/path-join-S7qkXQtP.js +1 -0
  49. package/web/dist/index.html +1 -1
  50. package/web/dist/sw.js +1 -1
  51. package/web/dist/assets/AddWorkspaceDialog-BsVnH3Xe.js +0 -1
  52. package/web/dist/assets/WorkspaceTaskDrawer-B0DmCWcV.js +0 -1
  53. package/web/dist/assets/chevron-right-CtLjVEl7.js +0 -1
  54. package/web/dist/assets/index-Cn8X3get.js +0 -76
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.4.4 - 2026-05-29
6
+
7
+ Windows portability and team protocol hardening.
8
+
9
+ - Fixes Windows `.cmd` / `.bat` launch handling for built-in and custom startup
10
+ commands, including quoted paths from nvm4w and `Program Files`.
11
+ - Improves Windows runtime shutdown by tearing down WebSocket connections before
12
+ closing the HTTP server and killing worker process trees with `taskkill /T /F`
13
+ before falling back to PTY termination.
14
+ - Makes `hive update`, open-in-editor commands, folder picking, filesystem
15
+ browsing, and port-in-use recovery friendlier on Windows.
16
+ - Preserves CRLF line endings in `.hive/tasks.md` mutations and makes the tasks
17
+ watcher more tolerant of atomic-save editors.
18
+ - Resolves OpenCode session data under `%LOCALAPPDATA%` on Windows and aligns
19
+ Claude session path encoding with Claude Code's project directory format.
20
+ - Hardens `team send` against stale worker names by returning a 409 with the
21
+ current roster and updates orchestrator guidance to refresh the member list
22
+ before dispatching.
23
+ - Expands Windows-focused unit and integration coverage across startup command
24
+ parsing, CLI shims, stdin protocol help, terminal profiles, filesystem
25
+ browsing, process cleanup, and path rendering.
26
+
5
27
  ## 1.4.3 - 2026-05-28
6
28
 
7
29
  Update hardening for multi-Node installs.
package/README.en.md CHANGED
@@ -107,10 +107,11 @@ a separate app from `hive --port 3000`. To uninstall, visit `chrome://apps`,
107
107
  right-click the Hive tile, and choose **Remove from Chrome…**.
108
108
 
109
109
  Hive asks the browser to confirm before closing the tab or PWA window so an
110
- accidental Cmd-W doesn't drop your session. Modern browsers gate that prompt
111
- on prior page interaction if you open the PWA and immediately press Cmd-W
112
- without clicking or typing anywhere first, it still closes cleanly. That's a
113
- browser policy, not a Hive bug.
110
+ accidental close shortcut (Cmd-W on macOS, Ctrl-W on Windows/Linux) doesn't
111
+ drop your session. Modern browsers gate that prompt on prior page interaction
112
+ if you open the PWA and immediately press the close shortcut without
113
+ clicking or typing anywhere first, it still closes cleanly. That's a browser
114
+ policy, not a Hive bug.
114
115
 
115
116
  First-run flow:
116
117
 
package/README.md CHANGED
@@ -74,7 +74,7 @@ hive update
74
74
 
75
75
  PWA 只是 UI 壳,Hive 后端仍需要在终端里跑着。如果启动 PWA 时后端没起,会看到 “Hive 后端未启动” 页面,等你跑起 `hive` 后会自动刷新。PWA 的 install scope 按 origin(含端口)划分,所以 `hive --port 4011` 跟 `hive --port 3000` 在浏览器看来是两个独立应用。卸载方法:浏览器地址栏访问 `chrome://apps`,右键 Hive 图标,选 **从 Chrome 中移除…**。
76
76
 
77
- 关闭 PWA 窗口或 tab 时 Hive 会主动请求浏览器弹原生确认对话框,避免 Cmd+W 误关丢失会话。但现代浏览器要求你跟页面"交互过"(点击 / 滚动 / 输入)才会真的弹这个对话框——刚打开 PWA 立刻按 Cmd+W 仍会直接关闭,这是浏览器策略,不是 Hive 的 bug。
77
+ 关闭 PWA 窗口或 tab 时 Hive 会主动请求浏览器弹原生确认对话框,避免关闭快捷键(macOS 上是 Cmd+W、Windows / Linux 上是 Ctrl+W)误关丢失会话。但现代浏览器要求你跟页面"交互过"(点击 / 滚动 / 输入)才会真的弹这个对话框——刚打开 PWA 立刻按关闭快捷键仍会直接关闭,这是浏览器策略,不是 Hive 的 bug。
78
78
 
79
79
  首次使用流程:
80
80
 
@@ -4,6 +4,35 @@ export interface RunUpdateResult {
4
4
  spawnError?: Error;
5
5
  }
6
6
  export type RunUpdate = (command: string, args: readonly string[]) => Promise<RunUpdateResult>;
7
+ /**
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`).
15
+ *
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.
22
+ */
23
+ export declare const buildSpawnOptionsForCommand: (command: string) => {
24
+ shell: boolean;
25
+ stdio: "inherit";
26
+ };
27
+ /**
28
+ * Signals the upgrade child should receive when the parent runtime is
29
+ * interrupted. Beyond the POSIX-only SIGTERM/SIGINT, SIGHUP is what
30
+ * libuv synthesises from Windows CTRL_CLOSE_EVENT (window X close),
31
+ * and SIGBREAK comes from Windows Ctrl+Break. Without forwarding
32
+ * those two the npm child outlives the runtime on the most common
33
+ * Windows exit paths.
34
+ */
35
+ export declare const FORWARDED_UPDATE_SIGNALS: readonly NodeJS.Signals[];
7
36
  export declare const defaultRunUpdate: RunUpdate;
8
37
  export declare const resolveHiveUpdateInstallArgs: (moduleUrl?: string) => string[];
9
38
  interface RunHiveUpdateOptions {
@@ -19,27 +19,66 @@ export const HIVE_UPDATE_USAGE = [
19
19
  'Options:',
20
20
  ' -h, --help Print this help.',
21
21
  ].join('\n');
22
+ /**
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`).
30
+ *
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.
37
+ */
38
+ export const buildSpawnOptionsForCommand = (command) => ({
39
+ shell: /\.(cmd|bat)$/i.test(command),
40
+ stdio: 'inherit',
41
+ });
42
+ /**
43
+ * Signals the upgrade child should receive when the parent runtime is
44
+ * interrupted. Beyond the POSIX-only SIGTERM/SIGINT, SIGHUP is what
45
+ * libuv synthesises from Windows CTRL_CLOSE_EVENT (window X close),
46
+ * and SIGBREAK comes from Windows Ctrl+Break. Without forwarding
47
+ * those two the npm child outlives the runtime on the most common
48
+ * Windows exit paths.
49
+ */
50
+ export const FORWARDED_UPDATE_SIGNALS = [
51
+ 'SIGINT',
52
+ 'SIGTERM',
53
+ 'SIGHUP',
54
+ 'SIGBREAK',
55
+ ];
22
56
  export const defaultRunUpdate = (command, args) => new Promise((resolve) => {
23
- const child = spawn(command, [...args], { stdio: 'inherit' });
57
+ const child = spawn(command, [...args], buildSpawnOptionsForCommand(command));
24
58
  let resolved = false;
25
- // Forward Ctrl+C / SIGTERM to the npm child so it can clean up rather
26
- // than getting orphaned mid-install. The handlers are registered with
27
- // `once` so they don't accumulate across invocations, and we also
28
- // explicitly remove them when the child exits in case the user only
29
- // sent one signal (Node would otherwise keep the handler alive).
30
- const handleSignal = (signal) => () => {
31
- child.kill(signal);
32
- };
33
- const handleSigint = handleSignal('SIGINT');
34
- const handleSigterm = handleSignal('SIGTERM');
35
- process.once('SIGINT', handleSigint);
36
- process.once('SIGTERM', handleSigterm);
59
+ // Handlers are registered with `once` so they don't accumulate
60
+ // across invocations and explicitly removed at finalize().
61
+ const handlers = new Map();
62
+ for (const signal of FORWARDED_UPDATE_SIGNALS) {
63
+ 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
+ }
71
+ };
72
+ handlers.set(signal, handler);
73
+ process.once(signal, handler);
74
+ }
37
75
  const finalize = (result) => {
38
76
  if (resolved)
39
77
  return;
40
78
  resolved = true;
41
- process.off('SIGINT', handleSigint);
42
- process.off('SIGTERM', handleSigterm);
79
+ for (const [signal, handler] of handlers) {
80
+ process.off(signal, handler);
81
+ }
43
82
  resolve(result);
44
83
  };
45
84
  child.on('error', (error) => {
@@ -9,7 +9,39 @@ interface RunHiveCommandResult {
9
9
  type RunHiveCommandOptions = {
10
10
  versionService?: VersionService;
11
11
  };
12
+ /**
13
+ * Signals that should drive a graceful shutdown. The interesting ones:
14
+ *
15
+ * SIGINT — Ctrl+C in the runtime terminal (all platforms).
16
+ * SIGTERM — `kill <pid>` on POSIX. Never delivered on Windows.
17
+ * SIGHUP — POSIX: parent shell exits. On Windows libuv synthesises
18
+ * SIGHUP from `CTRL_CLOSE_EVENT`, i.e. the user clicking
19
+ * the X on the runtime's cmd / Terminal window. Without
20
+ * this listener that close path skips the graceful path
21
+ * entirely on Windows.
22
+ * SIGBREAK — Windows: Ctrl+Break. Less common than Ctrl+C but kit
23
+ * scripts and CI hosts still send it.
24
+ *
25
+ * Stale agent_runs from a non-graceful exit are reconciled at next
26
+ * startup via `agentRunStore.markUnfinishedRunsStale()`
27
+ * (runtime-store-helpers.ts), so a dropped signal does not leave the
28
+ * database in an inconsistent state — only the PTY children miss the
29
+ * forwarded SIGTERM. On Windows there's no graceful equivalent for
30
+ * those children anyway (pty.kill is TerminateProcess), so this
31
+ * registration is mostly about giving SQLite a chance to checkpoint.
32
+ */
33
+ export declare const SHUTDOWN_SIGNALS: readonly ["SIGINT", "SIGTERM", "SIGHUP", "SIGBREAK"];
12
34
  export declare const HIVE_USAGE: string;
13
35
  export declare const handleHiveInfoCommand: (argv: string[]) => boolean;
36
+ /**
37
+ * Recovery hint formatter for the "port already in use" error. Platform-aware
38
+ * because the lsof / xargs / kill pipeline is POSIX-only; on Windows a user
39
+ * pasting that command into cmd or PowerShell gets nothing useful. The
40
+ * Windows path swaps in `netstat -ano | findstr` + `taskkill /F /PID` which
41
+ * is the documented Microsoft workflow for the same problem.
42
+ *
43
+ * Exported for unit testing.
44
+ */
45
+ export declare const formatPortInUseMessage: (port: number, platform?: NodeJS.Platform) => string;
14
46
  export declare const runHiveCommand: (argv: string[], options?: RunHiveCommandOptions) => Promise<RunHiveCommandResult>;
15
47
  export type { RunHiveCommandResult };
@@ -10,6 +10,28 @@ import { readPackageVersion } from '../server/package-version.js';
10
10
  import { createRuntimeStore } from '../server/runtime-store.js';
11
11
  import { createVersionService } from '../server/version-service.js';
12
12
  import { runHiveUpdateCommand } from './hive-update.js';
13
+ /**
14
+ * Signals that should drive a graceful shutdown. The interesting ones:
15
+ *
16
+ * SIGINT — Ctrl+C in the runtime terminal (all platforms).
17
+ * SIGTERM — `kill <pid>` on POSIX. Never delivered on Windows.
18
+ * SIGHUP — POSIX: parent shell exits. On Windows libuv synthesises
19
+ * SIGHUP from `CTRL_CLOSE_EVENT`, i.e. the user clicking
20
+ * the X on the runtime's cmd / Terminal window. Without
21
+ * this listener that close path skips the graceful path
22
+ * entirely on Windows.
23
+ * SIGBREAK — Windows: Ctrl+Break. Less common than Ctrl+C but kit
24
+ * scripts and CI hosts still send it.
25
+ *
26
+ * Stale agent_runs from a non-graceful exit are reconciled at next
27
+ * startup via `agentRunStore.markUnfinishedRunsStale()`
28
+ * (runtime-store-helpers.ts), so a dropped signal does not leave the
29
+ * database in an inconsistent state — only the PTY children miss the
30
+ * forwarded SIGTERM. On Windows there's no graceful equivalent for
31
+ * those children anyway (pty.kill is TerminateProcess), so this
32
+ * registration is mostly about giving SQLite a chance to checkpoint.
33
+ */
34
+ export const SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGBREAK'];
13
35
  export const HIVE_USAGE = [
14
36
  'Usage:',
15
37
  ' hive [--port <port>]',
@@ -66,19 +88,39 @@ const maybePrintUpdateHint = async (versionService) => {
66
88
  console.log(`Hive update available: ${info.current_version} -> ${info.latest_version}. Run: ${info.install_hint}`);
67
89
  };
68
90
  const isListenError = (error) => error instanceof Error && typeof error.code === 'string';
69
- const formatPortInUseMessage = (port) => [
70
- `Hive could not start because port ${port} is already in use.`,
71
- '',
72
- 'Another Hive instance may already be running:',
73
- ` http://127.0.0.1:${port}`,
74
- '',
75
- 'Options:',
76
- ' - Open the existing Hive window.',
77
- ' - Stop the process using that port:',
78
- ` lsof -tiTCP:${port} -sTCP:LISTEN | xargs kill`,
79
- ' - Start Hive on another port:',
80
- ` hive --port ${port + 1}`,
81
- ].join('\n');
91
+ /**
92
+ * Recovery hint formatter for the "port already in use" error. Platform-aware
93
+ * because the lsof / xargs / kill pipeline is POSIX-only; on Windows a user
94
+ * pasting that command into cmd or PowerShell gets nothing useful. The
95
+ * Windows path swaps in `netstat -ano | findstr` + `taskkill /F /PID` which
96
+ * is the documented Microsoft workflow for the same problem.
97
+ *
98
+ * Exported for unit testing.
99
+ */
100
+ export const formatPortInUseMessage = (port, platform = process.platform) => {
101
+ const stopHint = platform === 'win32'
102
+ ? [
103
+ ' - Stop the process using that port:',
104
+ ` netstat -ano | findstr ":${port}"`,
105
+ ' taskkill /PID <pid> /F',
106
+ ]
107
+ : [
108
+ ' - Stop the process using that port:',
109
+ ` lsof -tiTCP:${port} -sTCP:LISTEN | xargs kill`,
110
+ ];
111
+ return [
112
+ `Hive could not start because port ${port} is already in use.`,
113
+ '',
114
+ 'Another Hive instance may already be running:',
115
+ ` http://127.0.0.1:${port}`,
116
+ '',
117
+ 'Options:',
118
+ ' - Open the existing Hive window.',
119
+ ...stopHint,
120
+ ' - Start Hive on another port:',
121
+ ` hive --port ${port + 1}`,
122
+ ].join('\n');
123
+ };
82
124
  const formatListenError = (error, requestedPort) => {
83
125
  if (isListenError(error) && error.code === 'EADDRINUSE') {
84
126
  return new Error(formatPortInUseMessage(error.port ?? requestedPort));
@@ -119,8 +161,20 @@ export const runHiveCommand = async (argv, options = {}) => {
119
161
  return closePromise;
120
162
  }
121
163
  closePromise = (async () => {
122
- process.off('SIGTERM', gracefulShutdown);
123
- process.off('SIGINT', gracefulShutdown);
164
+ for (const signal of SHUTDOWN_SIGNALS) {
165
+ process.off(signal, gracefulShutdown);
166
+ }
167
+ // Tear down the WebSocket layer FIRST. `app.server.close()` waits
168
+ // on every existing socket, including upgraded WebSocket clients
169
+ // that never go idle on their own; `server.closeAllConnections()`
170
+ // alone does NOT terminate already-upgraded WS — only sockets
171
+ // still in the HTTP request/response state machine. The Windows
172
+ // symptom of skipping this step is Ctrl+C in the runtime cmd
173
+ // window hanging the process as long as any browser tab is
174
+ // connected to /ws/terminal/<runId> or /ws/tasks/<workspaceId>.
175
+ app.closeWebSockets();
176
+ // Then force-close any remaining plain-HTTP keep-alive sockets.
177
+ app.server.closeAllConnections();
124
178
  await new Promise((resolve, reject) => {
125
179
  app.server.close((error) => {
126
180
  if (error) {
@@ -144,8 +198,9 @@ export const runHiveCommand = async (argv, options = {}) => {
144
198
  process.exit(1);
145
199
  });
146
200
  };
147
- process.once('SIGTERM', gracefulShutdown);
148
- process.once('SIGINT', gracefulShutdown);
201
+ for (const signal of SHUTDOWN_SIGNALS) {
202
+ process.once(signal, gracefulShutdown);
203
+ }
149
204
  console.log(`Hive running at http://127.0.0.1:${address.port}`);
150
205
  void maybePrintUpdateHint(versionService).catch(() => { });
151
206
  return {
@@ -17,10 +17,13 @@ const TEAM_USAGE = [
17
17
  ' team status --stdin [--artifact <path>]',
18
18
  '',
19
19
  'Flags can appear in any order. Use --stdin to pipe long bodies and avoid shell-escaping issues.',
20
- "Use a quoted heredoc (<<'EOF') so $vars, backticks, and command substitutions stay literal:",
21
- " team report --stdin --dispatch <id> <<'EOF'",
22
- ' ... long report ...',
23
- ' EOF',
20
+ 'The body comes from stdin use whatever your shell supports:',
21
+ " POSIX: team report --stdin --dispatch <id> <<'EOF'",
22
+ ' ... long report ...',
23
+ ' EOF',
24
+ ' Windows cmd: type body.txt | team report --stdin --dispatch <id>',
25
+ ' PowerShell: Get-Content body.txt | team report --stdin --dispatch <id>',
26
+ ' Portable: team report --stdin --dispatch <id> < body.txt',
24
27
  '',
25
28
  'For role rules, workflow, and recovery instructions, see .hive/PROTOCOL.md',
26
29
  ].join('\n');
@@ -226,7 +229,16 @@ export const runTeamCommand = async (argv) => {
226
229
  to: workerName,
227
230
  text: task,
228
231
  });
229
- console.log(JSON.stringify(await response.json()));
232
+ const payload = (await response.json());
233
+ /* When the dispatch happened to also auto-wake a stopped worker
234
+ (PTY had no active run), make the silent restart visible. Stderr
235
+ is the right channel because the JSON on stdout is the
236
+ machine-readable payload; the human-readable narration goes
237
+ beside it so it doesn't corrupt parsers. */
238
+ if (payload.restarted_worker === true) {
239
+ console.error(`Hive woke up worker "${workerName}" before dispatching.`);
240
+ }
241
+ console.log(JSON.stringify(payload));
230
242
  return;
231
243
  }
232
244
  if (command === 'cancel') {
@@ -1,5 +1,14 @@
1
1
  interface ResolvedSpawnCommand {
2
- args: string[];
2
+ /**
3
+ * `args` is a `string[]` for plain executables (node-pty's serializer is
4
+ * fine for them) and a verbatim `string` for Windows `.cmd`/`.bat` shim
5
+ * launches. The verbatim form bypasses node-pty's `argsToCommandLine`,
6
+ * because that function backslash-escapes any embedded `"` and cmd.exe
7
+ * does NOT recognize `\"` as an escape — it treats `\` as literal, which
8
+ * leaves cmd looking up a program name containing literal quote chars.
9
+ * See `node-pty/src/windowsPtyAgent.ts` `argsToCommandLine` for the rule.
10
+ */
11
+ args: string | string[];
3
12
  command: string;
4
13
  }
5
14
  export declare const resolveCommandPath: (command: string, cwd: string, env: NodeJS.ProcessEnv, platform?: NodeJS.Platform) => string;
@@ -1,5 +1,5 @@
1
1
  import { accessSync, constants } from 'node:fs';
2
- import { delimiter, extname, isAbsolute, join } from 'node:path';
2
+ import { basename, delimiter, extname, isAbsolute, join } from 'node:path';
3
3
  const hasPathSeparator = (command) => command.includes('/') || command.includes('\\');
4
4
  const canExecute = (path, platform = process.platform) => {
5
5
  try {
@@ -54,16 +54,60 @@ const isWindowsBatchFile = (command) => {
54
54
  const extension = extname(command).toLowerCase();
55
55
  return extension === '.cmd' || extension === '.bat';
56
56
  };
57
- const quoteWindowsCommandArgument = (value) => `"${value.replace(/"/g, '\\"')}"`;
58
- const createWindowsCommandLine = (command, args) => [command, ...args].map(quoteWindowsCommandArgument).join(' ');
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
+ const buildWindowsBatchCommandLine = (command, args) => {
74
+ const tokens = [command, ...args].map(escapeCmdToken).join(' ');
75
+ // `call` is cmd's built-in batch invocation; it handles quoted .cmd / .bat
76
+ // paths reliably (this is the same pattern Node.js's child_process uses
77
+ // internally on Windows since the CVE-2024-27980 fix).
78
+ return `/d /s /c call ${tokens}`;
79
+ };
80
+ /**
81
+ * Recognize the exact shape that `createStartupCommandLaunch` produces on
82
+ * Windows: `cmd.exe` with args `['/d', '/s', '/c', '<raw user command>']`.
83
+ * Pinned to length 4 so this branch only fires for that single contract;
84
+ * any other cmd.exe invocation (e.g. someone explicitly composing custom
85
+ * shell args via the launch config) keeps the default node-pty path.
86
+ *
87
+ * We need this repackaging because the user's raw command often contains `"`
88
+ * (Windows users habitually wrap paths) and node-pty's `argsToCommandLine`
89
+ * backslash-escapes those — cmd.exe then sees `\"...\"` and looks up a
90
+ * program whose name starts with `\`.
91
+ */
92
+ const isCmdExeShellLaunch = (resolvedCommand, args) => basename(resolvedCommand).toLowerCase() === 'cmd.exe' &&
93
+ args.length === 4 &&
94
+ args[0] === '/d' &&
95
+ args[1] === '/s' &&
96
+ args[2] === '/c';
59
97
  export const resolveSpawnCommand = (command, cwd, env, args = [], platform = process.platform) => {
60
98
  const resolvedCommand = resolveCommandPath(command, cwd, env, platform);
61
99
  if (platform === 'win32' && isWindowsBatchFile(resolvedCommand)) {
62
100
  return {
63
- args: ['/d', '/s', '/c', createWindowsCommandLine(resolvedCommand, args)],
101
+ args: buildWindowsBatchCommandLine(resolvedCommand, args),
64
102
  command: getEnvValue(env, 'ComSpec', platform) ?? 'cmd.exe',
65
103
  };
66
104
  }
105
+ if (platform === 'win32' && isCmdExeShellLaunch(resolvedCommand, args)) {
106
+ return {
107
+ args: args.join(' '),
108
+ command: resolvedCommand,
109
+ };
110
+ }
67
111
  return { args, command: resolvedCommand };
68
112
  };
69
113
  export const assertCommandIsExecutable = (command, cwd, env) => {
@@ -1,4 +1,4 @@
1
- import { createStartupCommandLaunch, getStartupCommandExecutable, } from './startup-command-parser.js';
1
+ import { createStartupCommandLaunch, getStartupCommandExecutable, normalizeExecutableToken, } from './startup-command-parser.js';
2
2
  export const resolveCommandPresetLaunchConfig = (settings, commandPresetId) => {
3
3
  const preset = settings.getCommandPreset(commandPresetId);
4
4
  if (!preset)
@@ -12,8 +12,14 @@ export const resolveCommandPresetLaunchConfig = (settings, commandPresetId) => {
12
12
  const findPresetForStartupCommand = (settings, startupCommand, commandPresetId) => {
13
13
  if (commandPresetId)
14
14
  return settings.getCommandPreset(commandPresetId);
15
- const executable = getStartupCommandExecutable(startupCommand);
16
- return executable ? settings.getCommandPreset(executable) : undefined;
15
+ // Reduce the raw token (which may be a bare command, an absolute path,
16
+ // or a Windows path with spaces and a .cmd suffix) to the canonical
17
+ // brand id before looking up the preset. Without this normalization
18
+ // step `getCommandPreset` only matched bare command names — Windows
19
+ // users typing the full nvm4w path lost CLI brand identification,
20
+ // session capture, and post-start input strategy in one swoop.
21
+ const brandId = normalizeExecutableToken(getStartupCommandExecutable(startupCommand));
22
+ return brandId ? settings.getCommandPreset(brandId) : undefined;
17
23
  };
18
24
  export const resolveStartupCommandLaunchConfig = (settings, startupCommand, commandPresetId = null) => {
19
25
  const trimmedStartupCommand = startupCommand.trim();
@@ -2,6 +2,34 @@ import type { IPty } from 'node-pty';
2
2
  import type { AgentRunRecord, AgentRunSnapshot } from './agent-manager.js';
3
3
  import type { PtyOutputBus } from './pty-output-bus.js';
4
4
  export declare const MAX_RUN_OUTPUT_LENGTH = 1000000;
5
+ type ExecRunner = (cmd: string, args: readonly string[]) => void;
6
+ /**
7
+ * Windows analogue of POSIX `process.kill(-pgid, SIGKILL)`. node-pty on
8
+ * Windows hands `pty.kill()` to TerminateProcess against the PTY's main
9
+ * process only — children that the worker spawned (npm install, build
10
+ * scripts, custom tooling) become orphans and keep writing to the
11
+ * filesystem after the worker card flips to stopped.
12
+ *
13
+ * `taskkill /pid <pid> /t /f` walks the process tree (`/t`) and forces
14
+ * termination (`/f`), matching what task manager would do.
15
+ *
16
+ * IMPORTANT — call this BEFORE any other termination of the parent
17
+ * process. taskkill /T builds the tree by querying the parent for its
18
+ * descendants; if the parent is already gone (e.g. pty.kill() ran
19
+ * first) the enumeration returns empty and the children become
20
+ * orphans. /F also terminates the parent itself, so a parent-kill
21
+ * after this call is only useful as a fallback when taskkill itself
22
+ * failed (taskkill missing from PATH, restricted PowerShell, etc.).
23
+ *
24
+ * Best-effort: non-zero exits (process already gone, taskkill missing
25
+ * from PATH, access denied) are swallowed and surface as a `false`
26
+ * return. The caller is expected to fall back to pty.kill().
27
+ *
28
+ * Exported for unit testing — the `runner` parameter lets tests assert
29
+ * the exact argv without mocking node:child_process.
30
+ */
31
+ export declare const taskkillProcessTree: (pid: number, platform?: NodeJS.Platform, runner?: ExecRunner) => boolean;
5
32
  export declare const toAgentRunSnapshot: (run: AgentRunRecord) => AgentRunSnapshot;
6
33
  export declare const finishAgentRun: (run: AgentRunRecord, exitCode: number | null, ptyOutputBus: PtyOutputBus) => void;
7
34
  export declare const attachAgentPty: (run: AgentRunRecord, pty: IPty, ptyOutputBus: PtyOutputBus) => void;
35
+ export {};
@@ -1,6 +1,45 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  export const MAX_RUN_OUTPUT_LENGTH = 1_000_000;
3
3
  const FORCE_KILL_DELAY_MS = 750;
4
+ const defaultExecRunner = (cmd, args) => {
5
+ execFileSync(cmd, [...args], { stdio: 'ignore', windowsHide: true });
6
+ };
7
+ /**
8
+ * Windows analogue of POSIX `process.kill(-pgid, SIGKILL)`. node-pty on
9
+ * Windows hands `pty.kill()` to TerminateProcess against the PTY's main
10
+ * process only — children that the worker spawned (npm install, build
11
+ * scripts, custom tooling) become orphans and keep writing to the
12
+ * filesystem after the worker card flips to stopped.
13
+ *
14
+ * `taskkill /pid <pid> /t /f` walks the process tree (`/t`) and forces
15
+ * termination (`/f`), matching what task manager would do.
16
+ *
17
+ * IMPORTANT — call this BEFORE any other termination of the parent
18
+ * process. taskkill /T builds the tree by querying the parent for its
19
+ * descendants; if the parent is already gone (e.g. pty.kill() ran
20
+ * first) the enumeration returns empty and the children become
21
+ * orphans. /F also terminates the parent itself, so a parent-kill
22
+ * after this call is only useful as a fallback when taskkill itself
23
+ * failed (taskkill missing from PATH, restricted PowerShell, etc.).
24
+ *
25
+ * Best-effort: non-zero exits (process already gone, taskkill missing
26
+ * from PATH, access denied) are swallowed and surface as a `false`
27
+ * return. The caller is expected to fall back to pty.kill().
28
+ *
29
+ * Exported for unit testing — the `runner` parameter lets tests assert
30
+ * the exact argv without mocking node:child_process.
31
+ */
32
+ export const taskkillProcessTree = (pid, platform = process.platform, runner = defaultExecRunner) => {
33
+ if (platform !== 'win32' || pid <= 0)
34
+ return false;
35
+ try {
36
+ runner('taskkill', ['/pid', String(pid), '/t', '/f']);
37
+ return true;
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ };
4
43
  export const toAgentRunSnapshot = (run) => ({
5
44
  runId: run.runId,
6
45
  agentId: run.agentId,
@@ -62,8 +101,18 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
62
101
  };
63
102
  const killPty = (signal) => {
64
103
  try {
65
- if (process.platform === 'win32')
66
- pty.kill();
104
+ if (process.platform === 'win32') {
105
+ // taskkill /pid <pid> /t /f walks the parent's process tree
106
+ // BEFORE terminating it — so we have to run it while the parent
107
+ // is still alive. Calling pty.kill() first (the previous
108
+ // ordering) detaches the children: taskkill /T then fails with
109
+ // "process not found" and the npm-installs / build scripts
110
+ // become orphans. taskkill /f also terminates the parent, so
111
+ // pty.kill() is the fallback for the rare case where taskkill
112
+ // is missing from PATH or refused (e.g. restricted PowerShell).
113
+ if (!taskkillProcessTree(pty.pid))
114
+ pty.kill();
115
+ }
67
116
  else
68
117
  pty.kill(signal);
69
118
  }
@@ -88,8 +137,13 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
88
137
  forceKillTimer = setTimeout(() => {
89
138
  forceKillTimer = undefined;
90
139
  try {
91
- if (process.platform === 'win32')
92
- pty.kill();
140
+ if (process.platform === 'win32') {
141
+ // Same ordering as killPty(): tree-kill before terminating the
142
+ // parent, so taskkill /T can still enumerate the process tree.
143
+ // pty.kill() is the fallback for taskkill-missing hosts.
144
+ if (!taskkillProcessTree(pty.pid))
145
+ pty.kill();
146
+ }
93
147
  else
94
148
  pty.kill('SIGKILL');
95
149
  }
@@ -3,6 +3,23 @@ import type { AgentLaunchConfigInput } from './agent-run-store.js';
3
3
  import type { AgentSessionStorePort } from './agent-runtime-ports.js';
4
4
  import type { CommandPresetRecord } from './command-preset-store.js';
5
5
  import { type SessionCaptureSnapshot } from './session-capture.js';
6
+ /**
7
+ * Builds a `{ <PATH-key>: <new-value> }` object for the spawn env override.
8
+ * Critical on Windows: the OS env block reports PATH under its native casing
9
+ * (typically `Path`). Writing to a literal `PATH` key would, after spread
10
+ * with `process.env`, leave two entries — `Path` carrying the original value
11
+ * and `PATH` carrying our prepend. CreateProcess then sees both and the
12
+ * effective lookup order is undefined; in practice the child PTY often falls
13
+ * back to the original `Path` and never sees `HIVE_BIN_DIR`, breaking every
14
+ * `team` shim resolution.
15
+ *
16
+ * We detect the existing key (case-insensitive on Windows) and overwrite IT,
17
+ * so the merge produces exactly one PATH entry.
18
+ *
19
+ * Exported for unit testing — `buildAgentRunBootstrap` is the only in-tree
20
+ * caller.
21
+ */
22
+ export declare const buildSpawnPathEnvEntry: (parentEnv: NodeJS.ProcessEnv, hiveBinDir: string, platform: NodeJS.Platform) => NodeJS.ProcessEnv;
6
23
  export declare const buildAgentRunBootstrap: (workspace: WorkspaceSummary, agentId: string, config: AgentLaunchConfigInput, sessionStore: AgentSessionStorePort, getCommandPreset: (id: string) => CommandPresetRecord | undefined, agent?: AgentSummary) => {
7
24
  sessionCaptureSnapshot: {
8
25
  discriminator?: {
@@ -50,7 +67,6 @@ export declare const buildAgentRunBootstrap: (workspace: WorkspaceSummary, agent
50
67
  HIVE_PROJECT_ID: string;
51
68
  HIVE_AGENT_ID: string;
52
69
  HIVE_AGENT_TOKEN: string;
53
- PATH: string;
54
70
  };
55
71
  };
56
72
  export declare const startAgentRunCapture: ({ agentId, sessionCaptureSnapshot, sessionStore, startConfig, workspace, }: {