@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
@@ -9,18 +9,39 @@ export const PROTOCOL_RELATIVE_PATH = `${HIVE_DIR_NAME}/${PROTOCOL_FILE_NAME}`;
9
9
  export const getTasksFilePath = (workspacePath) => join(workspacePath, HIVE_DIR_NAME, TASKS_FILE_NAME);
10
10
  export const getProtocolFilePath = (workspacePath) => join(workspacePath, HIVE_DIR_NAME, PROTOCOL_FILE_NAME);
11
11
  const getLegacyTasksFilePath = (workspacePath) => join(workspacePath, TASKS_FILE_NAME);
12
+ const RETRYABLE_TASKS_FS_ERROR_CODES = new Set(['EACCES', 'EBUSY', 'EPERM']);
13
+ const TASKS_FS_RETRY_DELAYS_MS = [20, 50, 100];
14
+ const sleepSync = (ms) => {
15
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
16
+ };
17
+ const runRetryableTasksFileOperation = (operation) => {
18
+ for (let attempt = 0;; attempt += 1) {
19
+ try {
20
+ return operation();
21
+ }
22
+ catch (error) {
23
+ const code = error?.code;
24
+ const delay = TASKS_FS_RETRY_DELAYS_MS[attempt];
25
+ if (!code || !RETRYABLE_TASKS_FS_ERROR_CODES.has(code) || delay === undefined)
26
+ throw error;
27
+ sleepSync(delay);
28
+ }
29
+ }
30
+ };
12
31
  const ensureTasksDir = (workspacePath) => {
13
- mkdirSync(dirname(getTasksFilePath(workspacePath)), { recursive: true });
32
+ runRetryableTasksFileOperation(() => mkdirSync(dirname(getTasksFilePath(workspacePath)), { recursive: true }));
14
33
  };
15
34
  export const ensureTasksFile = (workspacePath) => {
16
35
  ensureTasksDir(workspacePath);
17
36
  const tasksFilePath = getTasksFilePath(workspacePath);
18
37
  if (existsSync(tasksFilePath)) {
19
- return readFileSync(tasksFilePath, 'utf8');
38
+ return runRetryableTasksFileOperation(() => readFileSync(tasksFilePath, 'utf8'));
20
39
  }
21
40
  const legacyTasksFilePath = getLegacyTasksFilePath(workspacePath);
22
- const content = existsSync(legacyTasksFilePath) ? readFileSync(legacyTasksFilePath, 'utf8') : '';
23
- writeFileSync(tasksFilePath, content, 'utf8');
41
+ const content = existsSync(legacyTasksFilePath)
42
+ ? runRetryableTasksFileOperation(() => readFileSync(legacyTasksFilePath, 'utf8'))
43
+ : '';
44
+ runRetryableTasksFileOperation(() => writeFileSync(tasksFilePath, content, 'utf8'));
24
45
  return content;
25
46
  };
26
47
  /**
@@ -29,14 +50,16 @@ export const ensureTasksFile = (workspacePath) => {
29
50
  * on every workspace open means a Hive version bump that changes the rules
30
51
  * propagates without manual intervention.
31
52
  */
32
- export const ensureProtocolFile = (workspacePath) => {
53
+ export const ensureProtocolFile = (workspacePath, cliPolicy, workflowsEnabled = false, autostaffEnabled = false) => {
33
54
  ensureTasksDir(workspacePath);
34
55
  const protocolFilePath = getProtocolFilePath(workspacePath);
35
- const desired = buildProtocolDoc();
36
- const current = existsSync(protocolFilePath) ? readFileSync(protocolFilePath, 'utf8') : null;
56
+ const desired = buildProtocolDoc(cliPolicy, workflowsEnabled, autostaffEnabled);
57
+ const current = existsSync(protocolFilePath)
58
+ ? runRetryableTasksFileOperation(() => readFileSync(protocolFilePath, 'utf8'))
59
+ : null;
37
60
  if (current === desired)
38
61
  return desired;
39
- writeFileSync(protocolFilePath, desired, 'utf8');
62
+ runRetryableTasksFileOperation(() => writeFileSync(protocolFilePath, desired, 'utf8'));
40
63
  return desired;
41
64
  };
42
65
  export const createTasksFileService = () => {
@@ -46,7 +69,7 @@ export const createTasksFileService = () => {
46
69
  },
47
70
  writeTasks(workspacePath, content) {
48
71
  ensureTasksDir(workspacePath);
49
- writeFileSync(getTasksFilePath(workspacePath), content, 'utf8');
72
+ runRetryableTasksFileOperation(() => writeFileSync(getTasksFilePath(workspacePath), content, 'utf8'));
50
73
  },
51
74
  };
52
75
  };
@@ -1,17 +1,15 @@
1
1
  import { WebSocketServer } from 'ws';
2
2
  import { getLocalRequestRejection } from './local-request-guard.js';
3
3
  import { readCookie } from './ui-auth-helpers.js';
4
+ import { attachRawSocketErrorHandler, attachWebSocketErrorHandler, attachWebSocketServerErrorHandler, rejectWebSocketUpgrade, sendWebSocketMessage, } from './websocket-upgrade-safety.js';
4
5
  const matchTasksPath = (pathname) => {
5
6
  const match = /^\/ws\/tasks\/(?<workspaceId>[^/]+)$/.exec(pathname);
6
7
  const workspaceId = match?.groups?.workspaceId;
7
8
  return workspaceId ? decodeURIComponent(workspaceId) : null;
8
9
  };
9
- const rejectUpgrade = (socket, status) => {
10
- socket.write(`HTTP/1.1 ${status}\r\n\r\n`);
11
- socket.destroy();
12
- };
13
10
  export const createTasksWebSocketServer = (server, store, tasksFileService) => {
14
11
  const wss = new WebSocketServer({ noServer: true });
12
+ attachWebSocketServerErrorHandler(wss, 'tasks');
15
13
  const socketsByWorkspaceId = new Map();
16
14
  const validateUpgradeSession = (request) => {
17
15
  const cookieHeader = Array.isArray(request.headers.cookie)
@@ -25,12 +23,13 @@ export const createTasksWebSocketServer = (server, store, tasksFileService) => {
25
23
  const workspaceId = matchTasksPath(url.pathname);
26
24
  if (!workspaceId)
27
25
  return;
26
+ const detachRawSocketErrorHandler = attachRawSocketErrorHandler(socket, 'tasks upgrade');
28
27
  if (getLocalRequestRejection(request)) {
29
- rejectUpgrade(socket, '403 Forbidden');
28
+ rejectWebSocketUpgrade(socket, '403 Forbidden');
30
29
  return;
31
30
  }
32
31
  if (!validateUpgradeSession(request)) {
33
- rejectUpgrade(socket, '401 Unauthorized');
32
+ rejectWebSocketUpgrade(socket, '401 Unauthorized');
34
33
  return;
35
34
  }
36
35
  let workspacePath = '';
@@ -38,10 +37,12 @@ export const createTasksWebSocketServer = (server, store, tasksFileService) => {
38
37
  workspacePath = store.getWorkspaceSnapshot(workspaceId).summary.path;
39
38
  }
40
39
  catch {
41
- rejectUpgrade(socket, '404 Not Found');
40
+ rejectWebSocketUpgrade(socket, '404 Not Found');
42
41
  return;
43
42
  }
44
43
  wss.handleUpgrade(request, socket, head, (ws) => {
44
+ detachRawSocketErrorHandler();
45
+ attachWebSocketErrorHandler(ws, `tasks ${workspaceId}`);
45
46
  const sockets = socketsByWorkspaceId.get(workspaceId) ?? new Set();
46
47
  sockets.add(ws);
47
48
  socketsByWorkspaceId.set(workspaceId, sockets);
@@ -55,14 +56,14 @@ export const createTasksWebSocketServer = (server, store, tasksFileService) => {
55
56
  if (ws.readyState !== ws.OPEN)
56
57
  return;
57
58
  try {
58
- ws.send(JSON.stringify({
59
+ sendWebSocketMessage(ws, JSON.stringify({
59
60
  type: 'tasks-snapshot',
60
61
  content: tasksFileService.readTasks(workspacePath),
61
- }));
62
+ }), `tasks ${workspaceId} snapshot`);
62
63
  }
63
64
  catch {
64
65
  if (ws.readyState === ws.OPEN) {
65
- ws.send(JSON.stringify({ type: 'tasks-snapshot', content: '' }));
66
+ sendWebSocketMessage(ws, JSON.stringify({ type: 'tasks-error', error: 'Failed to read tasks file' }), `tasks ${workspaceId} snapshot error`);
66
67
  }
67
68
  }
68
69
  });
@@ -72,7 +73,7 @@ export const createTasksWebSocketServer = (server, store, tasksFileService) => {
72
73
  close: () => {
73
74
  for (const sockets of socketsByWorkspaceId.values()) {
74
75
  for (const socket of sockets)
75
- socket.close();
76
+ socket.terminate();
76
77
  }
77
78
  socketsByWorkspaceId.clear();
78
79
  wss.close();
@@ -83,9 +84,7 @@ export const createTasksWebSocketServer = (server, store, tasksFileService) => {
83
84
  return;
84
85
  const payload = JSON.stringify({ type: 'tasks-updated', content });
85
86
  for (const socket of sockets) {
86
- if (socket.readyState === socket.OPEN) {
87
- socket.send(payload);
88
- }
87
+ sendWebSocketMessage(socket, payload, `tasks ${workspaceId} publish`);
89
88
  }
90
89
  },
91
90
  };
@@ -1,5 +1,5 @@
1
1
  import type { AgentSummary } from '../shared/types.js';
2
- export type TeamCommand = 'send' | 'list' | 'report' | 'status' | 'cancel' | 'help';
2
+ export type TeamCommand = 'send' | 'list' | 'report' | 'status' | 'cancel' | 'help' | 'spawn' | 'dismiss' | 'workflow';
3
3
  export declare const commandAllowedForRole: (role: AgentSummary["role"], command: TeamCommand) => boolean;
4
4
  interface AuthenticateInput {
5
5
  fromAgentId: string | undefined;
@@ -1,5 +1,13 @@
1
1
  import { ForbiddenError, UnauthorizedError } from './http-errors.js';
2
- const ORCHESTRATOR_COMMANDS = new Set(['send', 'list', 'cancel', 'help']);
2
+ const ORCHESTRATOR_COMMANDS = new Set([
3
+ 'send',
4
+ 'list',
5
+ 'cancel',
6
+ 'help',
7
+ 'spawn',
8
+ 'dismiss',
9
+ 'workflow',
10
+ ]);
3
11
  const WORKER_COMMANDS = new Set(['report', 'status', 'help']);
4
12
  const WORKER_ROLES = new Set(['coder', 'reviewer', 'tester', 'custom']);
5
13
  export const commandAllowedForRole = (role, command) => {
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Auto-staff experimental feature gate.
3
+ *
4
+ * The orchestrator can already create workers (`team spawn`); auto-staff is a
5
+ * guidance layer that grants + encourages it to size the team to the task up
6
+ * front (e.g. 2 coders + 1 reviewer + 1 tester in one go) rather than adding
7
+ * workers one at a time. It's purely additive orchestrator guidance — no new
8
+ * execution path — so unlike workflows it ships ON by default; a user opts
9
+ * OUT from Settings.
10
+ *
11
+ * Stored GLOBALLY in `app_state` under AUTOSTAFF_ENABLED_KEY. Only the exact
12
+ * string "false" disables it; absent / anything else reads back as enabled.
13
+ */
14
+ export declare const AUTOSTAFF_ENABLED_KEY = "team.autostaff";
15
+ export declare const readAutostaffEnabled: (raw: string | null | undefined) => boolean;
16
+ export declare const serializeAutostaffEnabled: (enabled: boolean) => string;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Auto-staff experimental feature gate.
3
+ *
4
+ * The orchestrator can already create workers (`team spawn`); auto-staff is a
5
+ * guidance layer that grants + encourages it to size the team to the task up
6
+ * front (e.g. 2 coders + 1 reviewer + 1 tester in one go) rather than adding
7
+ * workers one at a time. It's purely additive orchestrator guidance — no new
8
+ * execution path — so unlike workflows it ships ON by default; a user opts
9
+ * OUT from Settings.
10
+ *
11
+ * Stored GLOBALLY in `app_state` under AUTOSTAFF_ENABLED_KEY. Only the exact
12
+ * string "false" disables it; absent / anything else reads back as enabled.
13
+ */
14
+ export const AUTOSTAFF_ENABLED_KEY = 'team.autostaff';
15
+ export const readAutostaffEnabled = (raw) => raw !== 'false';
16
+ export const serializeAutostaffEnabled = (enabled) => (enabled ? 'true' : 'false');
@@ -1,2 +1,2 @@
1
1
  import type { TeamListItem, TeamListItemPayload } from '../shared/types.js';
2
- export declare const serializeTeamListItem: ({ commandPresetId, id, lastPtyLine, name, pendingTaskCount, role, status, }: TeamListItem) => TeamListItemPayload;
2
+ export declare const serializeTeamListItem: ({ commandPresetId, ephemeral, id, lastPtyLine, name, pendingTaskCount, role, spawnedBy, status, }: TeamListItem) => TeamListItemPayload;
@@ -1,4 +1,4 @@
1
- export const serializeTeamListItem = ({ commandPresetId, id, lastPtyLine, name, pendingTaskCount, role, status, }) => ({
1
+ export const serializeTeamListItem = ({ commandPresetId, ephemeral, id, lastPtyLine, name, pendingTaskCount, role, spawnedBy, status, }) => ({
2
2
  id,
3
3
  name,
4
4
  role,
@@ -6,4 +6,6 @@ export const serializeTeamListItem = ({ commandPresetId, id, lastPtyLine, name,
6
6
  pending_task_count: pendingTaskCount,
7
7
  last_pty_line: lastPtyLine ?? null,
8
8
  command_preset_id: commandPresetId ?? null,
9
+ ...(ephemeral === true ? { ephemeral: true } : {}),
10
+ ...(spawnedBy ? { spawned_by: spawnedBy } : {}),
9
11
  });
@@ -1,13 +1,20 @@
1
+ import type { TeamListItem } from '../shared/types.js';
1
2
  import type { AgentRuntime } from './agent-runtime.js';
2
3
  import type { DispatchRecord } from './dispatch-ledger-store.js';
3
4
  import type { MessageLogHandle, MessageLogRecord } from './message-log-store.js';
5
+ import type { WorkflowDispatchAwaiter } from './workflow-dispatch-awaiter.js';
4
6
  import type { WorkspaceStore } from './workspace-store.js';
7
+ export declare const formatUnknownWorkerError: (workerName: string, roster: readonly TeamListItem[]) => string;
5
8
  export interface TeamOperationsInput {
6
9
  agentRuntime: AgentRuntime;
7
10
  createDispatch: (input: {
8
11
  fromAgentId?: string;
12
+ label?: string;
13
+ phase?: string;
14
+ stepIndex?: number;
9
15
  text: string;
10
16
  toAgentId: string;
17
+ workflowRunId?: string;
11
18
  workspaceId: string;
12
19
  }) => DispatchRecord;
13
20
  deleteDispatch: (dispatchId: string) => void;
@@ -28,11 +35,20 @@ export interface TeamOperationsInput {
28
35
  workspaceId: string;
29
36
  }) => DispatchRecord | undefined;
30
37
  markDispatchSubmitted: (dispatchId: string) => void;
38
+ workflowDispatchAwaiter: WorkflowDispatchAwaiter;
31
39
  workspaceStore: WorkspaceStore;
40
+ /** Auto-dismiss an ephemeral orchestrator-spawned worker after its
41
+ * dispatch report. Wired in runtime-store-helpers to remove the worker
42
+ * completely (stop run, drop launch config, drop the row). M11. */
43
+ dismissEphemeralWorker?: (workspaceId: string, workerId: string) => void;
32
44
  }
33
45
  export interface DispatchTaskInput {
34
46
  fromAgentId?: string;
35
47
  hivePort?: string;
48
+ workflowRunId?: string;
49
+ stepIndex?: number;
50
+ phase?: string;
51
+ label?: string;
36
52
  }
37
53
  export interface ReportTaskInput {
38
54
  artifacts?: string[];
@@ -55,14 +71,16 @@ export interface ReportTaskResult {
55
71
  forwardError: string | null;
56
72
  forwarded: boolean;
57
73
  }
58
- export declare const createTeamOperations: ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, workspaceStore, }: TeamOperationsInput) => {
74
+ export declare const createTeamOperations: ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, workflowDispatchAwaiter, workspaceStore, dismissEphemeralWorker, }: TeamOperationsInput) => {
59
75
  cancelTask(workspaceId: string, dispatchId: string, input: CancelTaskInput): {
60
76
  dispatch: DispatchRecord;
61
77
  forwardError: string | null;
62
78
  forwarded: boolean;
63
79
  };
64
80
  dispatchTask: (workspaceId: string, workerId: string, text: string, input?: DispatchTaskInput) => Promise<DispatchRecord>;
65
- dispatchTaskByWorkerName(workspaceId: string, workerName: string, text: string, input?: DispatchTaskInput): Promise<DispatchRecord>;
81
+ dispatchTaskByWorkerName(workspaceId: string, workerName: string, text: string, input?: DispatchTaskInput): Promise<DispatchRecord & {
82
+ restartedWorker: boolean;
83
+ }>;
66
84
  recordUserInput(workspaceId: string, orchestratorId: string, text: string): void;
67
85
  statusTask(workspaceId: string, workerId: string, input?: StatusTaskInput): {
68
86
  dispatch: null;
@@ -1,7 +1,30 @@
1
1
  import { ConflictError, PtyInactiveError } from './http-errors.js';
2
2
  import { createReportMessage, createSendMessage, createStatusMessage, createUserInputMessage, } from './runtime-message-builders.js';
3
+ import { getWorkflowAgentId } from './workspace-store-support.js';
4
+ /* Roster snapshot embedded in the 409 the orchestrator sees when it
5
+ dispatches to a missing name. Format prioritizes the orchestrator's
6
+ parsing path: bullet-per-member with role + live status + pending
7
+ count, plus an explicit retry hint that names both fix-up paths
8
+ (direct send to a listed name, or re-`team list` if it doubts the
9
+ snapshot). Keeping role/status/pending inline saves a follow-up
10
+ `team list` round-trip — the orchestrator can re-pick in the same
11
+ turn. */
12
+ export const formatUnknownWorkerError = (workerName, roster) => {
13
+ if (roster.length === 0) {
14
+ return [
15
+ `Unknown worker "${workerName}": this workspace currently has no workers.`,
16
+ 'Ask the user to add a worker in the Hive UI (Add Worker), then run `team list` and retry.',
17
+ ].join('\n');
18
+ }
19
+ const lines = roster.map((entry) => ` - ${entry.name} (${entry.role}, ${entry.status}, ${entry.pendingTaskCount} pending)`);
20
+ return [
21
+ `Unknown worker "${workerName}" in this workspace. Current members:`,
22
+ ...lines,
23
+ 'Retry with `team send "<one of the names above>" "<task>"`, or run `team list` to refresh the roster.',
24
+ ].join('\n');
25
+ };
3
26
  const reportForwardErrorMessage = (error) => error instanceof Error ? error.message : String(error);
4
- export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, workspaceStore, }) => {
27
+ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, workflowDispatchAwaiter, workspaceStore, dismissEphemeralWorker, }) => {
5
28
  const ensureWorkerRun = async (workspaceId, workerId, hivePort) => {
6
29
  if (agentRuntime.getActiveRunByAgentId(workspaceId, workerId)) {
7
30
  return;
@@ -23,10 +46,25 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
23
46
  throw error;
24
47
  }
25
48
  };
49
+ const cancelUndeliveredDispatch = (workspaceId, workerId, dispatchId, reason, workflowRunId) => {
50
+ const cancelled = markDispatchCancelled({ dispatchId, reason, workspaceId });
51
+ if (!cancelled)
52
+ return;
53
+ try {
54
+ workspaceStore.markTaskCancelled(workspaceId, workerId);
55
+ }
56
+ catch (error) {
57
+ console.error('[hive] swallowed:teamDispatch.markTaskCancelled', error);
58
+ }
59
+ if (workflowRunId !== undefined) {
60
+ workflowDispatchAwaiter.notifyCancel(dispatchId, reason);
61
+ }
62
+ };
26
63
  const dispatchTask = async (workspaceId, workerId, text, input = {}) => {
27
64
  const message = createSendMessage(workspaceId, workerId, text, input.fromAgentId);
28
65
  const messageHandle = insertMessage(message);
29
66
  let dispatch;
67
+ let pendingMarked = false;
30
68
  try {
31
69
  const dispatchInput = {
32
70
  text,
@@ -35,18 +73,69 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
35
73
  };
36
74
  if (input.fromAgentId)
37
75
  dispatchInput.fromAgentId = input.fromAgentId;
76
+ if (input.workflowRunId !== undefined)
77
+ dispatchInput.workflowRunId = input.workflowRunId;
78
+ if (input.stepIndex !== undefined)
79
+ dispatchInput.stepIndex = input.stepIndex;
80
+ if (input.phase !== undefined)
81
+ dispatchInput.phase = input.phase;
82
+ if (input.label !== undefined)
83
+ dispatchInput.label = input.label;
38
84
  dispatch = createDispatch(dispatchInput);
85
+ const dispatchId = dispatch.id;
39
86
  if (input.fromAgentId) {
40
87
  const sender = workspaceStore.getAgent(workspaceId, input.fromAgentId);
41
88
  await ensureWorkerRun(workspaceId, workerId, input.hivePort ?? '');
42
89
  const worker = workspaceStore.getWorker(workspaceId, workerId);
43
- markDispatchSubmitted(dispatch.id);
44
- agentRuntime.writeSendPrompt(workspaceId, workerId, dispatch.id, sender.name, worker.description, text);
90
+ const isWorkflowDispatch = input.workflowRunId !== undefined || input.fromAgentId === getWorkflowAgentId(workspaceId);
91
+ markDispatchSubmitted(dispatchId);
92
+ workspaceStore.markTaskDispatched(workspaceId, workerId);
93
+ pendingMarked = true;
94
+ try {
95
+ const writePrompt = agentRuntime.writeSendPrompt(workspaceId, workerId, dispatchId, sender.name, worker.description, text);
96
+ void writePrompt.catch((error) => {
97
+ // `team send` is intentionally asynchronous (§3.3). A worker that
98
+ // exits during paste-submit did not receive actionable work, so
99
+ // close the open dispatch instead of leaving a fake pending task.
100
+ try {
101
+ cancelUndeliveredDispatch(workspaceId, workerId, dispatchId, reportForwardErrorMessage(error), input.workflowRunId);
102
+ }
103
+ catch (cancelError) {
104
+ if (!isWorkflowDispatch)
105
+ console.error('[hive] swallowed:teamDispatch.cancelUndelivered', cancelError);
106
+ }
107
+ if (!isWorkflowDispatch)
108
+ console.error('[hive] swallowed:teamDispatch.writePrompt', error);
109
+ });
110
+ }
111
+ catch (error) {
112
+ try {
113
+ cancelUndeliveredDispatch(workspaceId, workerId, dispatchId, reportForwardErrorMessage(error), input.workflowRunId);
114
+ }
115
+ catch (cancelError) {
116
+ if (!isWorkflowDispatch)
117
+ console.error('[hive] swallowed:teamDispatch.cancelUndelivered', cancelError);
118
+ }
119
+ if (!isWorkflowDispatch)
120
+ console.error('[hive] swallowed:teamDispatch.writePrompt', error);
121
+ }
122
+ }
123
+ else {
124
+ workspaceStore.markTaskDispatched(workspaceId, workerId);
125
+ pendingMarked = true;
45
126
  }
46
- workspaceStore.markTaskDispatched(workspaceId, workerId);
47
127
  return dispatch;
48
128
  }
49
129
  catch (error) {
130
+ if (pendingMarked) {
131
+ try {
132
+ workspaceStore.markTaskCancelled(workspaceId, workerId);
133
+ }
134
+ catch {
135
+ // Best-effort compensation for the in-memory pending count; the
136
+ // durable send message is deleted below.
137
+ }
138
+ }
50
139
  if (dispatch)
51
140
  deleteDispatch(dispatch.id);
52
141
  deleteMessage(messageHandle);
@@ -82,9 +171,28 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
82
171
  return { dispatch, forwardError, forwarded };
83
172
  },
84
173
  dispatchTask,
85
- dispatchTaskByWorkerName(workspaceId, workerName, text, input = {}) {
86
- const worker = workspaceStore.getWorkerByName(workspaceId, workerName);
87
- return dispatchTask(workspaceId, worker.id, text, input);
174
+ async dispatchTaskByWorkerName(workspaceId, workerName, text, input = {}) {
175
+ /* Build the roster once so a missing-name path can surface it without
176
+ a second store call. We deliberately don't go through
177
+ `getWorkerByName` because its underlying record helper throws a
178
+ bare Error — that bubbles as HTTP 500 and looks like a server bug
179
+ to the orchestrator, instead of the self-healing 409 we want. */
180
+ const roster = workspaceStore.listWorkers(workspaceId);
181
+ const worker = roster.find((entry) => entry.name === workerName);
182
+ if (!worker) {
183
+ throw new ConflictError(formatUnknownWorkerError(workerName, roster));
184
+ }
185
+ /* Capture the active-run state *before* dispatchTask runs, because
186
+ dispatchTask calls ensureWorkerRun on authenticated team-send calls
187
+ and may silently auto-start a stopped worker. Surfacing
188
+ `restartedWorker=true` to the orchestrator turns that silent state
189
+ change into a transparent one. Internal calls without `fromAgentId`
190
+ only queue work and do not wake a PTY, so they must not report a
191
+ restart. */
192
+ const restartedWorker = input.fromAgentId !== undefined &&
193
+ !agentRuntime.getActiveRunByAgentId(workspaceId, worker.id);
194
+ const dispatch = await dispatchTask(workspaceId, worker.id, text, input);
195
+ return Object.assign(dispatch, { restartedWorker });
88
196
  },
89
197
  recordUserInput(workspaceId, orchestratorId, text) {
90
198
  workspaceStore.getAgent(workspaceId, orchestratorId);
@@ -123,17 +231,19 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
123
231
  const status = input.status;
124
232
  const artifacts = input.artifacts ?? [];
125
233
  const worker = workspaceStore.getWorker(workspaceId, workerId);
126
- if (input.requireActiveRun === true &&
127
- !agentRuntime.getActiveRunByAgentId(workspaceId, `${workspaceId}:orchestrator`)) {
128
- throw new PtyInactiveError(`No active run for agent: ${workspaceId}:orchestrator`);
129
- }
130
234
  const openDispatch = findOpenDispatch(workspaceId, workerId, input.dispatchId);
131
- if (!openDispatch && input.dispatchId) {
132
- throw new ConflictError(`No open dispatch for worker: ${worker.name}`);
133
- }
134
235
  if (!openDispatch) {
135
236
  throw new ConflictError(`No open dispatch for worker: ${worker.name}`);
136
237
  }
238
+ const isWorkflowDispatch = openDispatch.fromAgentId === getWorkflowAgentId(workspaceId);
239
+ // Pre-check the orchestrator PTY only when the report is heading there.
240
+ // Workflow-sourced dispatches don't need an orchestrator: the runner
241
+ // is in-process and will resolve its awaiter directly.
242
+ if (input.requireActiveRun === true && !isWorkflowDispatch) {
243
+ if (!agentRuntime.getActiveRunByAgentId(workspaceId, `${workspaceId}:orchestrator`)) {
244
+ throw new PtyInactiveError(`No active run for agent: ${workspaceId}:orchestrator`);
245
+ }
246
+ }
137
247
  const messageHandle = insertMessage(createReportMessage(workspaceId, workerId, text, status, artifacts));
138
248
  try {
139
249
  const dispatch = markDispatchReportedByWorker({
@@ -149,6 +259,24 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
149
259
  workspaceStore.markTaskReported(workspaceId, workerId);
150
260
  let forwardError = null;
151
261
  let forwarded = false;
262
+ // Workflow-sourced dispatches: the source is the in-process runner, not a
263
+ // PTY. Resolve its awaiting Promise instead of injecting into orchestrator
264
+ // stdin (which would do nothing — `__workflow__` has no PTY).
265
+ if (dispatch.fromAgentId === getWorkflowAgentId(workspaceId)) {
266
+ try {
267
+ workflowDispatchAwaiter.notifyReport(dispatch.id, {
268
+ artifacts,
269
+ text,
270
+ ...(status ? { status } : {}),
271
+ });
272
+ forwarded = true;
273
+ }
274
+ catch (error) {
275
+ forwardError = reportForwardErrorMessage(error);
276
+ console.error('[hive] swallowed:teamReport.workflowForward', error);
277
+ }
278
+ return { dispatch, forwardError, forwarded };
279
+ }
152
280
  if (input.requireActiveRun === true) {
153
281
  try {
154
282
  agentRuntime.writeReportPrompt(workspaceId, worker.name, workerId, text, artifacts, {
@@ -161,6 +289,24 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
161
289
  console.error('[hive] swallowed:teamReport.forward', error);
162
290
  }
163
291
  }
292
+ // M11: if this worker was spawned with `team spawn --ephemeral`, this
293
+ // first successful report is its trigger to auto-dismiss. Deferred via
294
+ // queueMicrotask so the orchestrator's forward write lands BEFORE the
295
+ // worker's PTY is torn down (otherwise the inject + dismiss race).
296
+ // Skipped for workflow dispatches — workflow workers are managed by
297
+ // the runner's own finally block.
298
+ if (worker.ephemeral === true &&
299
+ worker.spawnedBy === 'orchestrator' &&
300
+ dismissEphemeralWorker) {
301
+ queueMicrotask(() => {
302
+ try {
303
+ dismissEphemeralWorker(workspaceId, workerId);
304
+ }
305
+ catch (error) {
306
+ console.error('[hive] swallowed:teamReport.ephemeralDismiss', error);
307
+ }
308
+ });
309
+ }
164
310
  return { dispatch, forwardError, forwarded };
165
311
  }
166
312
  catch (error) {
@@ -1,15 +1,9 @@
1
- import { basename } from 'node:path';
2
- const normalizeExecutable = (value) => {
3
- if (!value)
4
- return null;
5
- const normalized = basename(value).toLowerCase();
6
- return normalized.replace(/\.(cmd|exe)$/u, '');
7
- };
1
+ import { normalizeExecutableToken } from './startup-command-parser.js';
8
2
  export const resolveTerminalInputProfile = (config) => {
9
3
  if (!config)
10
4
  return 'default';
11
5
  if (config.commandPresetId === 'opencode')
12
6
  return 'opencode';
13
- const executable = normalizeExecutable(config.interactiveCommand) ?? normalizeExecutable(config.command);
7
+ const executable = normalizeExecutableToken(config.interactiveCommand) ?? normalizeExecutableToken(config.command);
14
8
  return executable === 'opencode' ? 'opencode' : 'default';
15
9
  };
@@ -14,7 +14,11 @@ export const parseTerminalControlMessage = (raw) => {
14
14
  }
15
15
  const resizeCols = asInteger(cols);
16
16
  const resizeRows = asInteger(rows);
17
- if (parsed.type === 'resize' && resizeCols !== undefined && resizeRows !== undefined) {
17
+ if (parsed.type === 'resize' &&
18
+ resizeCols !== undefined &&
19
+ resizeRows !== undefined &&
20
+ resizeCols > 0 &&
21
+ resizeRows > 0) {
18
22
  const message = {
19
23
  type: 'resize',
20
24
  cols: resizeCols,
@@ -22,10 +26,12 @@ export const parseTerminalControlMessage = (raw) => {
22
26
  };
23
27
  const parsedPixelWidth = asInteger(pixelWidth);
24
28
  const parsedPixelHeight = asInteger(pixelHeight);
25
- if (parsedPixelWidth !== undefined)
29
+ if (parsedPixelWidth !== undefined && parsedPixelWidth >= 0) {
26
30
  message.pixelWidth = parsedPixelWidth;
27
- if (parsedPixelHeight !== undefined)
31
+ }
32
+ if (parsedPixelHeight !== undefined && parsedPixelHeight >= 0) {
28
33
  message.pixelHeight = parsedPixelHeight;
34
+ }
29
35
  return message;
30
36
  }
31
37
  throw new Error('Invalid terminal control message');