@tt-a1i/hive 1.4.3 → 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 (180) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.en.md +5 -4
  3. package/README.md +9 -1
  4. package/assets/qq-group.jpg +0 -0
  5. package/dist/bin/team.cmd +1 -0
  6. package/dist/src/cli/hive-update.d.ts +57 -0
  7. package/dist/src/cli/hive-update.js +92 -15
  8. package/dist/src/cli/hive.d.ts +57 -0
  9. package/dist/src/cli/hive.js +113 -20
  10. package/dist/src/cli/team.d.ts +1 -0
  11. package/dist/src/cli/team.js +215 -7
  12. package/dist/src/server/agent-command-resolver.d.ts +10 -1
  13. package/dist/src/server/agent-command-resolver.js +32 -4
  14. package/dist/src/server/agent-launch-resolver.js +9 -3
  15. package/dist/src/server/agent-manager-support.d.ts +28 -0
  16. package/dist/src/server/agent-manager-support.js +138 -10
  17. package/dist/src/server/agent-run-bootstrap.d.ts +17 -1
  18. package/dist/src/server/agent-run-bootstrap.js +30 -2
  19. package/dist/src/server/agent-run-starter.d.ts +7 -1
  20. package/dist/src/server/agent-run-starter.js +9 -2
  21. package/dist/src/server/agent-run-store.d.ts +1 -1
  22. package/dist/src/server/agent-runtime-close.d.ts +1 -0
  23. package/dist/src/server/agent-runtime-close.js +25 -1
  24. package/dist/src/server/agent-runtime-contract.d.ts +2 -1
  25. package/dist/src/server/agent-runtime.d.ts +1 -1
  26. package/dist/src/server/agent-runtime.js +8 -2
  27. package/dist/src/server/agent-startup-instructions.d.ts +8 -1
  28. package/dist/src/server/agent-startup-instructions.js +15 -9
  29. package/dist/src/server/agent-stdin-dispatcher.d.ts +12 -5
  30. package/dist/src/server/agent-stdin-dispatcher.js +129 -40
  31. package/dist/src/server/app.d.ts +1 -0
  32. package/dist/src/server/app.js +12 -2
  33. package/dist/src/server/cron-util.d.ts +7 -0
  34. package/dist/src/server/cron-util.js +19 -0
  35. package/dist/src/server/dispatch-ledger-store.d.ts +22 -0
  36. package/dist/src/server/dispatch-ledger-store.js +51 -3
  37. package/dist/src/server/env-sync-message.js +9 -9
  38. package/dist/src/server/fs-browse.d.ts +14 -1
  39. package/dist/src/server/fs-browse.js +48 -5
  40. package/dist/src/server/fs-pick-folder.js +58 -11
  41. package/dist/src/server/fs-sandbox.js +36 -7
  42. package/dist/src/server/hive-team-guidance.d.ts +11 -6
  43. package/dist/src/server/hive-team-guidance.js +252 -70
  44. package/dist/src/server/live-run-registry.d.ts +1 -0
  45. package/dist/src/server/live-run-registry.js +1 -1
  46. package/dist/src/server/open-target-commands.js +29 -4
  47. package/dist/src/server/orchestrator-autostart.d.ts +12 -0
  48. package/dist/src/server/orchestrator-autostart.js +15 -13
  49. package/dist/src/server/path-canonicalization.d.ts +3 -0
  50. package/dist/src/server/path-canonicalization.js +29 -0
  51. package/dist/src/server/platform-path.d.ts +3 -0
  52. package/dist/src/server/platform-path.js +13 -0
  53. package/dist/src/server/post-start-input-writer.d.ts +1 -1
  54. package/dist/src/server/post-start-input-writer.js +116 -16
  55. package/dist/src/server/preset-launch-support.d.ts +1 -1
  56. package/dist/src/server/preset-launch-support.js +33 -2
  57. package/dist/src/server/recovery-summary.d.ts +6 -1
  58. package/dist/src/server/recovery-summary.js +17 -17
  59. package/dist/src/server/restart-policy-support.d.ts +6 -1
  60. package/dist/src/server/restart-policy-support.js +9 -1
  61. package/dist/src/server/restart-policy.d.ts +2 -2
  62. package/dist/src/server/restart-policy.js +3 -1
  63. package/dist/src/server/role-template-store.d.ts +1 -0
  64. package/dist/src/server/role-template-store.js +11 -1
  65. package/dist/src/server/route-types.d.ts +43 -0
  66. package/dist/src/server/routes-runtime.js +2 -1
  67. package/dist/src/server/routes-settings.js +76 -0
  68. package/dist/src/server/routes-team.js +221 -2
  69. package/dist/src/server/routes-workflow-schedules.d.ts +2 -0
  70. package/dist/src/server/routes-workflow-schedules.js +58 -0
  71. package/dist/src/server/routes-workflows.d.ts +2 -0
  72. package/dist/src/server/routes-workflows.js +83 -0
  73. package/dist/src/server/routes.js +4 -0
  74. package/dist/src/server/runtime-restart-policy.d.ts +3 -1
  75. package/dist/src/server/runtime-restart-policy.js +3 -1
  76. package/dist/src/server/runtime-store-contract.d.ts +122 -0
  77. package/dist/src/server/runtime-store-contract.js +1 -0
  78. package/dist/src/server/runtime-store-helpers.d.ts +9 -0
  79. package/dist/src/server/runtime-store-helpers.js +101 -2
  80. package/dist/src/server/runtime-store-workflows.d.ts +6 -0
  81. package/dist/src/server/runtime-store-workflows.js +100 -0
  82. package/dist/src/server/runtime-store.d.ts +3 -70
  83. package/dist/src/server/runtime-store.js +70 -4
  84. package/dist/src/server/session-capture-claude.d.ts +23 -0
  85. package/dist/src/server/session-capture-claude.js +24 -1
  86. package/dist/src/server/session-capture-codex.d.ts +3 -3
  87. package/dist/src/server/session-capture-codex.js +9 -7
  88. package/dist/src/server/session-capture-gemini.d.ts +1 -1
  89. package/dist/src/server/session-capture-gemini.js +6 -3
  90. package/dist/src/server/session-capture-opencode.d.ts +18 -0
  91. package/dist/src/server/session-capture-opencode.js +27 -2
  92. package/dist/src/server/settings-store.d.ts +3 -0
  93. package/dist/src/server/settings-store.js +1 -0
  94. package/dist/src/server/sqlite-schema-v19.d.ts +2 -0
  95. package/dist/src/server/sqlite-schema-v19.js +17 -0
  96. package/dist/src/server/sqlite-schema-v20.d.ts +2 -0
  97. package/dist/src/server/sqlite-schema-v20.js +20 -0
  98. package/dist/src/server/sqlite-schema-v21.d.ts +2 -0
  99. package/dist/src/server/sqlite-schema-v21.js +20 -0
  100. package/dist/src/server/sqlite-schema.d.ts +1 -1
  101. package/dist/src/server/sqlite-schema.js +97 -1
  102. package/dist/src/server/startup-command-parser.d.ts +15 -0
  103. package/dist/src/server/startup-command-parser.js +33 -2
  104. package/dist/src/server/system-message.d.ts +7 -0
  105. package/dist/src/server/system-message.js +8 -1
  106. package/dist/src/server/tasks-file-watcher.d.ts +39 -1
  107. package/dist/src/server/tasks-file-watcher.js +155 -25
  108. package/dist/src/server/tasks-file.d.ts +2 -1
  109. package/dist/src/server/tasks-file.js +32 -9
  110. package/dist/src/server/tasks-websocket-server.js +13 -14
  111. package/dist/src/server/team-authz.d.ts +1 -1
  112. package/dist/src/server/team-authz.js +9 -1
  113. package/dist/src/server/team-autostaff.d.ts +16 -0
  114. package/dist/src/server/team-autostaff.js +16 -0
  115. package/dist/src/server/team-list-serializer.d.ts +1 -1
  116. package/dist/src/server/team-list-serializer.js +3 -1
  117. package/dist/src/server/team-operations.d.ts +20 -2
  118. package/dist/src/server/team-operations.js +160 -14
  119. package/dist/src/server/terminal-input-profile.js +2 -8
  120. package/dist/src/server/terminal-protocol.js +9 -3
  121. package/dist/src/server/terminal-stream-hub.js +16 -10
  122. package/dist/src/server/terminal-ws-server.js +36 -16
  123. package/dist/src/server/websocket-upgrade-safety.d.ts +10 -0
  124. package/dist/src/server/websocket-upgrade-safety.js +35 -0
  125. package/dist/src/server/windows-command-line.d.ts +3 -0
  126. package/dist/src/server/windows-command-line.js +9 -0
  127. package/dist/src/server/windows-filename.d.ts +2 -0
  128. package/dist/src/server/windows-filename.js +33 -0
  129. package/dist/src/server/workflow-cli-policy.d.ts +60 -0
  130. package/dist/src/server/workflow-cli-policy.js +110 -0
  131. package/dist/src/server/workflow-dispatch-awaiter.d.ts +12 -0
  132. package/dist/src/server/workflow-dispatch-awaiter.js +80 -0
  133. package/dist/src/server/workflow-feature.d.ts +15 -0
  134. package/dist/src/server/workflow-feature.js +15 -0
  135. package/dist/src/server/workflow-http-serializers.d.ts +64 -0
  136. package/dist/src/server/workflow-http-serializers.js +58 -0
  137. package/dist/src/server/workflow-run-log-store.d.ts +19 -0
  138. package/dist/src/server/workflow-run-log-store.js +45 -0
  139. package/dist/src/server/workflow-run-store.d.ts +50 -0
  140. package/dist/src/server/workflow-run-store.js +103 -0
  141. package/dist/src/server/workflow-runner.d.ts +147 -0
  142. package/dist/src/server/workflow-runner.js +401 -0
  143. package/dist/src/server/workflow-schedule-create.d.ts +14 -0
  144. package/dist/src/server/workflow-schedule-create.js +41 -0
  145. package/dist/src/server/workflow-schedule-store.d.ts +43 -0
  146. package/dist/src/server/workflow-schedule-store.js +112 -0
  147. package/dist/src/server/workflow-scheduler.d.ts +36 -0
  148. package/dist/src/server/workflow-scheduler.js +97 -0
  149. package/dist/src/server/workflow-script-loader.d.ts +34 -0
  150. package/dist/src/server/workflow-script-loader.js +106 -0
  151. package/dist/src/server/workspace-path-validation.js +16 -4
  152. package/dist/src/server/workspace-shell-runtime.d.ts +5 -0
  153. package/dist/src/server/workspace-shell-runtime.js +24 -2
  154. package/dist/src/server/workspace-store-contract.d.ts +4 -1
  155. package/dist/src/server/workspace-store-hydration.js +23 -7
  156. package/dist/src/server/workspace-store-mutations.js +2 -5
  157. package/dist/src/server/workspace-store-support.d.ts +4 -0
  158. package/dist/src/server/workspace-store-support.js +13 -1
  159. package/dist/src/server/workspace-store.js +38 -4
  160. package/dist/src/shared/types.d.ts +16 -1
  161. package/package.json +4 -2
  162. package/web/dist/assets/{AddWorkerDialog-DmkDOdp6.js → AddWorkerDialog-CcC-7kgG.js} +2 -2
  163. package/web/dist/assets/AddWorkspaceDialog-BDpOTfmt.js +1 -0
  164. package/web/dist/assets/{FirstRunWizard-SAd1wsH4.js → FirstRunWizard-BYX_ocQn.js} +1 -1
  165. package/web/dist/assets/{MarketplaceDrawer-B_8aG2uT.js → MarketplaceDrawer-DUxSk7db.js} +1 -1
  166. package/web/dist/assets/WhatsNewDialog-B_RlCXcV.js +1 -0
  167. package/web/dist/assets/WorkerModal-D9-7YfZZ.js +1 -0
  168. package/web/dist/assets/WorkspaceTaskDrawer-BCKoF7qc.js +1 -0
  169. package/web/dist/assets/{WorkspaceTerminalPanels-BReWh1YL.js → WorkspaceTerminalPanels-Dq8y91t2.js} +1 -1
  170. package/web/dist/assets/index-BiOvKIVw.css +1 -0
  171. package/web/dist/assets/index-DMRUklT3.js +73 -0
  172. package/web/dist/assets/path-join-7MR1s7b1.js +1 -0
  173. package/web/dist/index.html +2 -2
  174. package/web/dist/sw.js +1 -1
  175. package/web/dist/assets/AddWorkspaceDialog-BsVnH3Xe.js +0 -1
  176. package/web/dist/assets/WorkerModal-CQmjiPme.js +0 -1
  177. package/web/dist/assets/WorkspaceTaskDrawer-B0DmCWcV.js +0 -1
  178. package/web/dist/assets/chevron-right-CtLjVEl7.js +0 -1
  179. package/web/dist/assets/index-BEsTmfrO.css +0 -1
  180. package/web/dist/assets/index-Cn8X3get.js +0 -76
@@ -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>]',
@@ -17,10 +24,13 @@ const TEAM_USAGE = [
17
24
  ' team status --stdin [--artifact <path>]',
18
25
  '',
19
26
  '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',
27
+ 'The body comes from stdin use whatever your shell supports:',
28
+ " POSIX: team report --stdin --dispatch <id> <<'EOF'",
29
+ ' ... long report ...',
30
+ ' EOF',
31
+ ' Windows cmd: type body.txt | team report --stdin --dispatch <id>',
32
+ ' PowerShell: Get-Content body.txt | team report --stdin --dispatch <id>',
33
+ ' Portable: team report --stdin --dispatch <id> < body.txt',
24
34
  '',
25
35
  'For role rules, workflow, and recovery instructions, see .hive/PROTOCOL.md',
26
36
  ].join('\n');
@@ -32,6 +42,15 @@ const getHiveEnv = () => {
32
42
  return values;
33
43
  };
34
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
+ };
35
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;
36
55
  const describeFetchError = (baseUrl, error) => {
37
56
  const cause = error instanceof Error && error.cause instanceof Error ? ` (${error.cause.message})` : '';
@@ -174,6 +193,20 @@ export const parseCancelArgs = (args) => {
174
193
  }
175
194
  return { dispatchId, reason };
176
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
+ };
177
210
  export const readStdinToString = async (command = 'report') => {
178
211
  if (process.stdin.isTTY) {
179
212
  throw new Error(withUsage('--stdin requires piped input, but stdin is a TTY. Did you forget to pipe content in?', command));
@@ -182,7 +215,7 @@ export const readStdinToString = async (command = 'report') => {
182
215
  for await (const chunk of process.stdin) {
183
216
  chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
184
217
  }
185
- const content = Buffer.concat(chunks).toString('utf8');
218
+ const content = decodeStdinBuffer(Buffer.concat(chunks));
186
219
  if (!content.trim()) {
187
220
  throw new Error(withUsage('--stdin received empty input', command));
188
221
  }
@@ -226,9 +259,184 @@ export const runTeamCommand = async (argv) => {
226
259
  to: workerName,
227
260
  text: task,
228
261
  });
262
+ const payload = (await response.json());
263
+ /* When the dispatch happened to also auto-wake a stopped worker
264
+ (PTY had no active run), make the silent restart visible. Stderr
265
+ is the right channel because the JSON on stdout is the
266
+ machine-readable payload; the human-readable narration goes
267
+ beside it so it doesn't corrupt parsers. */
268
+ if (payload.restarted_worker === true) {
269
+ console.error(`Hive woke up worker "${workerName}" before dispatching.`);
270
+ }
271
+ console.log(JSON.stringify(payload));
272
+ return;
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
+ });
229
294
  console.log(JSON.stringify(await response.json()));
230
295
  return;
231
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
+ }
232
440
  if (command === 'cancel') {
233
441
  const cancel = parseCancelArgs(args);
234
442
  const env = getHiveEnv();
@@ -282,7 +490,7 @@ export const runTeamCommand = async (argv) => {
282
490
  throw new Error('Unsupported team command');
283
491
  };
284
492
  const isMainModule = process.argv[1]
285
- ? fileURLToPath(import.meta.url) === realpathSync(process.argv[1])
493
+ ? sameFilesystemPath(fileURLToPath(import.meta.url), process.argv[1])
286
494
  : false;
287
495
  if (isMainModule) {
288
496
  void runTeamCommand(process.argv.slice(2)).catch((error) => {
@@ -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,6 @@
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
+ 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,16 +55,43 @@ const isWindowsBatchFile = (command) => {
54
55
  const extension = extname(command).toLowerCase();
55
56
  return extension === '.cmd' || extension === '.bat';
56
57
  };
57
- const quoteWindowsCommandArgument = (value) => `"${value.replace(/"/g, '\\"')}"`;
58
- const createWindowsCommandLine = (command, args) => [command, ...args].map(quoteWindowsCommandArgument).join(' ');
58
+ const buildWindowsBatchCommandLine = (command, args) => {
59
+ // `call` is cmd's built-in batch invocation; it handles quoted .cmd / .bat
60
+ // paths reliably (this is the same pattern Node.js's child_process uses
61
+ // internally on Windows since the CVE-2024-27980 fix).
62
+ return `/d /s /c ${buildCmdCallCommand(command, args)}`;
63
+ };
64
+ /**
65
+ * Recognize the exact shape that `createStartupCommandLaunch` produces on
66
+ * Windows: `cmd.exe` with args `['/d', '/s', '/c', '<raw user command>']`.
67
+ * Pinned to length 4 so this branch only fires for that single contract;
68
+ * any other cmd.exe invocation (e.g. someone explicitly composing custom
69
+ * shell args via the launch config) keeps the default node-pty path.
70
+ *
71
+ * We need this repackaging because the user's raw command often contains `"`
72
+ * (Windows users habitually wrap paths) and node-pty's `argsToCommandLine`
73
+ * backslash-escapes those — cmd.exe then sees `\"...\"` and looks up a
74
+ * program whose name starts with `\`.
75
+ */
76
+ const isCmdExeShellLaunch = (resolvedCommand, args) => basename(resolvedCommand).toLowerCase() === 'cmd.exe' &&
77
+ args.length === 4 &&
78
+ args[0] === '/d' &&
79
+ args[1] === '/s' &&
80
+ (args[2] === '/c' || args[2] === '/k');
59
81
  export const resolveSpawnCommand = (command, cwd, env, args = [], platform = process.platform) => {
60
82
  const resolvedCommand = resolveCommandPath(command, cwd, env, platform);
61
83
  if (platform === 'win32' && isWindowsBatchFile(resolvedCommand)) {
62
84
  return {
63
- args: ['/d', '/s', '/c', createWindowsCommandLine(resolvedCommand, args)],
85
+ args: buildWindowsBatchCommandLine(resolvedCommand, args),
64
86
  command: getEnvValue(env, 'ComSpec', platform) ?? 'cmd.exe',
65
87
  };
66
88
  }
89
+ if (platform === 'win32' && isCmdExeShellLaunch(resolvedCommand, args)) {
90
+ return {
91
+ args: args.join(' '),
92
+ command: resolvedCommand,
93
+ };
94
+ }
67
95
  return { args, command: resolvedCommand };
68
96
  };
69
97
  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[], done: (success: boolean) => void) => 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, onFailure?: () => void) => 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,63 @@
1
- import { execFileSync } from 'node:child_process';
1
+ import { execFile, 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 TASKKILL_TIMEOUT_MS = 3000;
5
+ const PTY_READ_EOF_EXIT_GRACE_MS = 1000;
6
+ const isPtyReadEofError = (error) => {
7
+ const candidate = error;
8
+ return process.platform !== 'win32' && candidate?.code === 'EIO' && candidate.syscall === 'read';
9
+ };
10
+ const serializePtyInput = (input) => Buffer.isBuffer(input) ? input.toString('latin1') : input;
11
+ const defaultExecRunner = (cmd, args, done) => {
12
+ let settled = false;
13
+ const settle = (success) => {
14
+ if (settled)
15
+ return;
16
+ settled = true;
17
+ done(success);
18
+ };
19
+ const child = execFile(cmd, [...args], { maxBuffer: 64 * 1024, timeout: TASKKILL_TIMEOUT_MS, windowsHide: true }, (error) => settle(!error));
20
+ child.once('error', () => settle(false));
21
+ };
22
+ /**
23
+ * Windows analogue of POSIX `process.kill(-pgid, SIGKILL)`. node-pty on
24
+ * Windows hands `pty.kill()` to TerminateProcess against the PTY's main
25
+ * process only — children that the worker spawned (npm install, build
26
+ * scripts, custom tooling) become orphans and keep writing to the
27
+ * filesystem after the worker card flips to stopped.
28
+ *
29
+ * `taskkill /pid <pid> /t /f` walks the process tree (`/t`) and forces
30
+ * termination (`/f`), matching what task manager would do.
31
+ *
32
+ * IMPORTANT — call this BEFORE any other termination of the parent
33
+ * process. taskkill /T builds the tree by querying the parent for its
34
+ * descendants; if the parent is already gone (e.g. pty.kill() ran
35
+ * first) the enumeration returns empty and the children become
36
+ * orphans. /F also terminates the parent itself, so a parent-kill
37
+ * after this call is only useful as a fallback when taskkill itself
38
+ * failed (taskkill missing from PATH, restricted PowerShell, etc.).
39
+ *
40
+ * Best-effort: non-zero exits (process already gone, taskkill missing
41
+ * from PATH, access denied) are swallowed and surface as a `false`
42
+ * return. The caller is expected to fall back to pty.kill().
43
+ *
44
+ * Exported for unit testing — the `runner` parameter lets tests assert
45
+ * the exact argv without mocking node:child_process.
46
+ */
47
+ export const taskkillProcessTree = (pid, platform = process.platform, runner = defaultExecRunner, onFailure) => {
48
+ if (platform !== 'win32' || pid <= 0)
49
+ return false;
50
+ try {
51
+ runner('taskkill', ['/pid', String(pid), '/t', '/f'], (success) => {
52
+ if (!success)
53
+ onFailure?.();
54
+ });
55
+ return true;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ };
4
61
  export const toAgentRunSnapshot = (run) => ({
5
62
  runId: run.runId,
6
63
  agentId: run.agentId,
@@ -22,6 +79,7 @@ export const finishAgentRun = (run, exitCode, ptyOutputBus) => {
22
79
  export const attachAgentPty = (run, pty, ptyOutputBus) => {
23
80
  let stdinClosed = false;
24
81
  let forceKillTimer;
82
+ let ptyReadEofTimer;
25
83
  const resolveProcessGroupId = () => {
26
84
  if (process.platform === 'win32' || pty.pid <= 0)
27
85
  return null;
@@ -60,16 +118,29 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
60
118
  ignoreBestEffortGroupKillError(error);
61
119
  }
62
120
  };
63
- const killPty = (signal) => {
121
+ const killPtyDirect = (signal) => {
64
122
  try {
65
- if (process.platform === 'win32')
66
- pty.kill();
67
- else
68
- pty.kill(signal);
123
+ pty.kill(signal);
69
124
  }
70
125
  catch (error) {
71
126
  ignoreMissingProcess(error);
72
127
  }
128
+ };
129
+ const killPty = (signal) => {
130
+ if (process.platform === 'win32') {
131
+ // taskkill /pid <pid> /t /f walks the parent's process tree
132
+ // BEFORE terminating it — so we have to run it while the parent
133
+ // is still alive. Calling pty.kill() first (the previous
134
+ // ordering) detaches the children: taskkill /T then fails with
135
+ // "process not found" and the npm-installs / build scripts
136
+ // become orphans. taskkill /f also terminates the parent, so
137
+ // pty.kill() is the fallback for the rare case where taskkill
138
+ // is missing from PATH or refused (e.g. restricted PowerShell).
139
+ if (!taskkillProcessTree(pty.pid, process.platform, defaultExecRunner, () => killPtyDirect()))
140
+ killPtyDirect();
141
+ }
142
+ else
143
+ killPtyDirect(signal);
73
144
  killProcessGroup(signal);
74
145
  };
75
146
  const clearForceKillTimer = () => {
@@ -78,8 +149,34 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
78
149
  clearTimeout(forceKillTimer);
79
150
  forceKillTimer = undefined;
80
151
  };
152
+ const clearPtyReadEofTimer = () => {
153
+ if (!ptyReadEofTimer)
154
+ return;
155
+ clearTimeout(ptyReadEofTimer);
156
+ ptyReadEofTimer = undefined;
157
+ };
158
+ const schedulePtyReadEofExitGuard = (error) => {
159
+ if (ptyReadEofTimer)
160
+ return;
161
+ ptyReadEofTimer = setTimeout(() => {
162
+ ptyReadEofTimer = undefined;
163
+ if (stopped())
164
+ return;
165
+ console.error(`[hive] PTY read EOF without exit for run ${run.runId}`, error);
166
+ finishAgentRun(run, null, ptyOutputBus);
167
+ try {
168
+ killPty('SIGTERM');
169
+ scheduleForceKill();
170
+ }
171
+ catch (killError) {
172
+ ignoreMissingProcess(killError);
173
+ }
174
+ }, PTY_READ_EOF_EXIT_GRACE_MS);
175
+ ptyReadEofTimer.unref?.();
176
+ };
81
177
  const cleanupProcessGroup = () => {
82
178
  clearForceKillTimer();
179
+ clearPtyReadEofTimer();
83
180
  killProcessGroup('SIGKILL');
84
181
  };
85
182
  const scheduleForceKill = () => {
@@ -88,10 +185,15 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
88
185
  forceKillTimer = setTimeout(() => {
89
186
  forceKillTimer = undefined;
90
187
  try {
91
- if (process.platform === 'win32')
92
- pty.kill();
188
+ if (process.platform === 'win32') {
189
+ // Same ordering as killPty(): tree-kill before terminating the
190
+ // parent, so taskkill /T can still enumerate the process tree.
191
+ // pty.kill() is the fallback for taskkill-missing hosts.
192
+ if (!taskkillProcessTree(pty.pid, process.platform, defaultExecRunner, () => killPtyDirect()))
193
+ killPtyDirect();
194
+ }
93
195
  else
94
- pty.kill('SIGKILL');
196
+ killPtyDirect('SIGKILL');
95
197
  }
96
198
  catch (error) {
97
199
  ignoreMissingProcess(error);
@@ -109,6 +211,9 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
109
211
  },
110
212
  pid: pty.pid,
111
213
  resize(cols, rows) {
214
+ if (!Number.isInteger(cols) || !Number.isInteger(rows) || cols <= 0 || rows <= 0) {
215
+ throw new Error(`Invalid terminal size for run: ${run.runId}`);
216
+ }
112
217
  pty.resize(cols, rows);
113
218
  },
114
219
  resume() {
@@ -119,6 +224,7 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
119
224
  cleanupProcessGroup();
120
225
  return;
121
226
  }
227
+ clearPtyReadEofTimer();
122
228
  killPty('SIGTERM');
123
229
  stdinClosed = true;
124
230
  scheduleForceKill();
@@ -127,7 +233,7 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
127
233
  if (stdinClosed || run.status === 'exited' || run.status === 'error') {
128
234
  throw new Error(`PTY is not active for run: ${run.runId}`);
129
235
  }
130
- pty.write(text);
236
+ pty.write(serializePtyInput(text));
131
237
  },
132
238
  };
133
239
  pty.onData((chunk) => {
@@ -138,8 +244,30 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
138
244
  run.output = run.output.slice(-MAX_RUN_OUTPUT_LENGTH);
139
245
  ptyOutputBus.publish(run.runId, chunk);
140
246
  });
247
+ pty.on?.('error', (error) => {
248
+ if (stopped())
249
+ return;
250
+ if (isPtyReadEofError(error)) {
251
+ // Unix PTYs can surface a closed slave as read/EIO just before
252
+ // node-pty delivers the real onExit event. Treat it as EOF, not
253
+ // as the run's terminal status.
254
+ stdinClosed = true;
255
+ schedulePtyReadEofExitGuard(error);
256
+ return;
257
+ }
258
+ console.error(`[hive] PTY error for run ${run.runId}`, error);
259
+ stdinClosed = true;
260
+ finishAgentRun(run, null, ptyOutputBus);
261
+ try {
262
+ killPty('SIGTERM');
263
+ }
264
+ catch (killError) {
265
+ ignoreMissingProcess(killError);
266
+ }
267
+ });
141
268
  pty.onExit((event) => {
142
269
  stdinClosed = true;
270
+ clearPtyReadEofTimer();
143
271
  cleanupProcessGroup();
144
272
  finishAgentRun(run, event.exitCode, ptyOutputBus);
145
273
  });
@@ -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, }: {