@tt-a1i/hive 1.4.4 → 1.6.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 +47 -0
  2. package/README.en.md +21 -0
  3. package/README.md +16 -0
  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 +45 -17
  7. package/dist/src/cli/hive-update.js +63 -25
  8. package/dist/src/cli/hive.d.ts +25 -0
  9. package/dist/src/cli/hive.js +41 -3
  10. package/dist/src/cli/team.d.ts +1 -0
  11. package/dist/src/cli/team.js +216 -3
  12. package/dist/src/server/agent-command-resolver.js +3 -19
  13. package/dist/src/server/agent-manager-support.d.ts +2 -2
  14. package/dist/src/server/agent-manager-support.js +98 -24
  15. package/dist/src/server/agent-run-starter.d.ts +6 -1
  16. package/dist/src/server/agent-run-starter.js +9 -2
  17. package/dist/src/server/agent-run-store.d.ts +1 -1
  18. package/dist/src/server/agent-runtime-close.d.ts +1 -0
  19. package/dist/src/server/agent-runtime-close.js +25 -1
  20. package/dist/src/server/agent-runtime-contract.d.ts +12 -1
  21. package/dist/src/server/agent-runtime-stop-run.d.ts +1 -1
  22. package/dist/src/server/agent-runtime-stop-run.js +4 -1
  23. package/dist/src/server/agent-runtime.d.ts +2 -1
  24. package/dist/src/server/agent-runtime.js +14 -3
  25. package/dist/src/server/agent-startup-instructions.d.ts +7 -1
  26. package/dist/src/server/agent-startup-instructions.js +17 -9
  27. package/dist/src/server/agent-stdin-dispatcher.d.ts +25 -5
  28. package/dist/src/server/agent-stdin-dispatcher.js +141 -40
  29. package/dist/src/server/cron-util.d.ts +7 -0
  30. package/dist/src/server/cron-util.js +19 -0
  31. package/dist/src/server/dispatch-ledger-store.d.ts +22 -0
  32. package/dist/src/server/dispatch-ledger-store.js +51 -3
  33. package/dist/src/server/env-sync-message.js +9 -9
  34. package/dist/src/server/feature-flags.d.ts +42 -0
  35. package/dist/src/server/feature-flags.js +24 -0
  36. package/dist/src/server/fs-pick-folder.js +4 -0
  37. package/dist/src/server/fs-sandbox.js +36 -7
  38. package/dist/src/server/hive-team-guidance.d.ts +12 -6
  39. package/dist/src/server/hive-team-guidance.js +253 -71
  40. package/dist/src/server/live-run-registry.d.ts +1 -0
  41. package/dist/src/server/live-run-registry.js +1 -1
  42. package/dist/src/server/open-target-commands.js +5 -6
  43. package/dist/src/server/orchestrator-autostart.d.ts +12 -0
  44. package/dist/src/server/orchestrator-autostart.js +15 -13
  45. package/dist/src/server/path-canonicalization.d.ts +3 -0
  46. package/dist/src/server/path-canonicalization.js +29 -0
  47. package/dist/src/server/platform-path.d.ts +3 -0
  48. package/dist/src/server/platform-path.js +13 -0
  49. package/dist/src/server/post-start-input-writer.d.ts +1 -1
  50. package/dist/src/server/post-start-input-writer.js +110 -13
  51. package/dist/src/server/preset-launch-support.d.ts +1 -1
  52. package/dist/src/server/preset-launch-support.js +33 -2
  53. package/dist/src/server/recovery-summary.d.ts +5 -1
  54. package/dist/src/server/recovery-summary.js +18 -17
  55. package/dist/src/server/report-outbox-store.d.ts +36 -0
  56. package/dist/src/server/report-outbox-store.js +33 -0
  57. package/dist/src/server/restart-policy-support.d.ts +5 -1
  58. package/dist/src/server/restart-policy-support.js +9 -1
  59. package/dist/src/server/restart-policy.d.ts +6 -2
  60. package/dist/src/server/restart-policy.js +51 -31
  61. package/dist/src/server/role-template-store.d.ts +1 -0
  62. package/dist/src/server/role-template-store.js +11 -1
  63. package/dist/src/server/route-types.d.ts +43 -0
  64. package/dist/src/server/routes-runtime.js +2 -1
  65. package/dist/src/server/routes-settings.js +76 -0
  66. package/dist/src/server/routes-tasks.js +23 -0
  67. package/dist/src/server/routes-team.js +211 -1
  68. package/dist/src/server/routes-workflow-schedules.d.ts +2 -0
  69. package/dist/src/server/routes-workflow-schedules.js +58 -0
  70. package/dist/src/server/routes-workflows.d.ts +2 -0
  71. package/dist/src/server/routes-workflows.js +83 -0
  72. package/dist/src/server/routes-workspaces.js +5 -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 +2 -1
  76. package/dist/src/server/runtime-store-contract.d.ts +125 -0
  77. package/dist/src/server/runtime-store-contract.js +1 -0
  78. package/dist/src/server/runtime-store-helpers.d.ts +11 -0
  79. package/dist/src/server/runtime-store-helpers.js +106 -2
  80. package/dist/src/server/runtime-store-workflows.d.ts +6 -0
  81. package/dist/src/server/runtime-store-workflows.js +108 -0
  82. package/dist/src/server/runtime-store.d.ts +3 -72
  83. package/dist/src/server/runtime-store.js +71 -4
  84. package/dist/src/server/session-capture-codex.d.ts +3 -3
  85. package/dist/src/server/session-capture-codex.js +9 -7
  86. package/dist/src/server/session-capture-gemini.d.ts +1 -1
  87. package/dist/src/server/session-capture-gemini.js +6 -3
  88. package/dist/src/server/settings-store.d.ts +3 -0
  89. package/dist/src/server/settings-store.js +1 -0
  90. package/dist/src/server/sqlite-schema-v19.d.ts +2 -0
  91. package/dist/src/server/sqlite-schema-v19.js +17 -0
  92. package/dist/src/server/sqlite-schema-v20.d.ts +2 -0
  93. package/dist/src/server/sqlite-schema-v20.js +20 -0
  94. package/dist/src/server/sqlite-schema-v21.d.ts +2 -0
  95. package/dist/src/server/sqlite-schema-v21.js +20 -0
  96. package/dist/src/server/sqlite-schema.d.ts +1 -1
  97. package/dist/src/server/sqlite-schema.js +110 -1
  98. package/dist/src/server/system-message.d.ts +7 -0
  99. package/dist/src/server/system-message.js +8 -1
  100. package/dist/src/server/task-deps.d.ts +32 -0
  101. package/dist/src/server/task-deps.js +40 -0
  102. package/dist/src/server/tasks-file-watcher.d.ts +12 -1
  103. package/dist/src/server/tasks-file-watcher.js +128 -23
  104. package/dist/src/server/tasks-file.d.ts +3 -1
  105. package/dist/src/server/tasks-file.js +33 -9
  106. package/dist/src/server/tasks-websocket-server.js +13 -14
  107. package/dist/src/server/team-authz.d.ts +1 -1
  108. package/dist/src/server/team-authz.js +10 -1
  109. package/dist/src/server/team-autostaff.d.ts +16 -0
  110. package/dist/src/server/team-autostaff.js +16 -0
  111. package/dist/src/server/team-list-serializer.d.ts +1 -1
  112. package/dist/src/server/team-list-serializer.js +3 -1
  113. package/dist/src/server/team-operations.d.ts +21 -1
  114. package/dist/src/server/team-operations.js +183 -16
  115. package/dist/src/server/terminal-protocol.js +9 -3
  116. package/dist/src/server/terminal-stream-hub.js +16 -10
  117. package/dist/src/server/terminal-ws-server.js +10 -8
  118. package/dist/src/server/webhook-notifier.d.ts +34 -0
  119. package/dist/src/server/webhook-notifier.js +47 -0
  120. package/dist/src/server/websocket-upgrade-safety.d.ts +10 -0
  121. package/dist/src/server/websocket-upgrade-safety.js +35 -0
  122. package/dist/src/server/windows-command-line.d.ts +3 -0
  123. package/dist/src/server/windows-command-line.js +9 -0
  124. package/dist/src/server/windows-filename.d.ts +2 -0
  125. package/dist/src/server/windows-filename.js +33 -0
  126. package/dist/src/server/workflow-cli-policy.d.ts +60 -0
  127. package/dist/src/server/workflow-cli-policy.js +110 -0
  128. package/dist/src/server/workflow-dispatch-awaiter.d.ts +12 -0
  129. package/dist/src/server/workflow-dispatch-awaiter.js +80 -0
  130. package/dist/src/server/workflow-feature.d.ts +15 -0
  131. package/dist/src/server/workflow-feature.js +15 -0
  132. package/dist/src/server/workflow-http-serializers.d.ts +64 -0
  133. package/dist/src/server/workflow-http-serializers.js +58 -0
  134. package/dist/src/server/workflow-output-schema.d.ts +18 -0
  135. package/dist/src/server/workflow-output-schema.js +41 -0
  136. package/dist/src/server/workflow-run-log-store.d.ts +19 -0
  137. package/dist/src/server/workflow-run-log-store.js +45 -0
  138. package/dist/src/server/workflow-run-store.d.ts +50 -0
  139. package/dist/src/server/workflow-run-store.js +103 -0
  140. package/dist/src/server/workflow-runner.d.ts +147 -0
  141. package/dist/src/server/workflow-runner.js +411 -0
  142. package/dist/src/server/workflow-schedule-create.d.ts +14 -0
  143. package/dist/src/server/workflow-schedule-create.js +41 -0
  144. package/dist/src/server/workflow-schedule-store.d.ts +43 -0
  145. package/dist/src/server/workflow-schedule-store.js +112 -0
  146. package/dist/src/server/workflow-scheduler.d.ts +36 -0
  147. package/dist/src/server/workflow-scheduler.js +97 -0
  148. package/dist/src/server/workflow-script-loader.d.ts +34 -0
  149. package/dist/src/server/workflow-script-loader.js +106 -0
  150. package/dist/src/server/workspace-path-validation.js +16 -4
  151. package/dist/src/server/workspace-shell-runtime.d.ts +5 -0
  152. package/dist/src/server/workspace-shell-runtime.js +24 -2
  153. package/dist/src/server/workspace-store-contract.d.ts +4 -1
  154. package/dist/src/server/workspace-store-hydration.js +23 -7
  155. package/dist/src/server/workspace-store-mutations.js +2 -5
  156. package/dist/src/server/workspace-store-support.d.ts +4 -0
  157. package/dist/src/server/workspace-store-support.js +13 -1
  158. package/dist/src/server/workspace-store.js +38 -4
  159. package/dist/src/shared/types.d.ts +16 -1
  160. package/package.json +4 -2
  161. package/web/dist/assets/{AddWorkerDialog-DeZhTQLi.js → AddWorkerDialog-CGbaxu0T.js} +2 -2
  162. package/web/dist/assets/AddWorkspaceDialog-CNgExu6b.js +1 -0
  163. package/web/dist/assets/{FirstRunWizard-B5wLcat5.js → FirstRunWizard-DxGApUNc.js} +1 -1
  164. package/web/dist/assets/{MarketplaceDrawer-BC0eBOEW.js → MarketplaceDrawer-Bk6cpukn.js} +1 -1
  165. package/web/dist/assets/WhatsNewDialog-CSGzk-2U.js +1 -0
  166. package/web/dist/assets/WorkerModal-i2F3n3nZ.js +1 -0
  167. package/web/dist/assets/WorkspaceTaskDrawer-C_Ta_K13.js +1 -0
  168. package/web/dist/assets/WorkspaceTerminalPanels-VdDxtrQF.js +1 -0
  169. package/web/dist/assets/index-5zh61jMg.css +1 -0
  170. package/web/dist/assets/index-CAgGM6nb.js +75 -0
  171. package/web/dist/assets/path-join-7MR1s7b1.js +1 -0
  172. package/web/dist/index.html +2 -2
  173. package/web/dist/sw.js +1 -1
  174. package/web/dist/assets/AddWorkspaceDialog-DDpXNEKf.js +0 -1
  175. package/web/dist/assets/WorkerModal-BwMHq-Bi.js +0 -1
  176. package/web/dist/assets/WorkspaceTaskDrawer-CxvT4nqs.js +0 -1
  177. package/web/dist/assets/WorkspaceTerminalPanels-CvibsPSd.js +0 -1
  178. package/web/dist/assets/index-BEsTmfrO.css +0 -1
  179. package/web/dist/assets/index-Ddb7bDN5.js +0 -75
  180. package/web/dist/assets/path-join-S7qkXQtP.js +0 -1
@@ -1,3 +1,5 @@
1
+ import { type FeatureFlags } from './feature-flags.js';
2
+ import type { WorkflowCliPolicy } from './workflow-cli-policy.js';
1
3
  interface TasksFileService {
2
4
  readTasks: (workspacePath: string) => string;
3
5
  writeTasks: (workspacePath: string, content: string) => void;
@@ -16,6 +18,6 @@ export declare const ensureTasksFile: (workspacePath: string) => string;
16
18
  * on every workspace open means a Hive version bump that changes the rules
17
19
  * propagates without manual intervention.
18
20
  */
19
- export declare const ensureProtocolFile: (workspacePath: string) => string;
21
+ export declare const ensureProtocolFile: (workspacePath: string, cliPolicy?: WorkflowCliPolicy, flags?: FeatureFlags) => string;
20
22
  export declare const createTasksFileService: () => TasksFileService;
21
23
  export type { TasksFileService };
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
+ import { FEATURE_FLAGS_ALL_OFF } from './feature-flags.js';
3
4
  import { buildProtocolDoc } from './hive-team-guidance.js';
4
5
  export const HIVE_DIR_NAME = '.hive';
5
6
  export const TASKS_FILE_NAME = 'tasks.md';
@@ -9,18 +10,39 @@ export const PROTOCOL_RELATIVE_PATH = `${HIVE_DIR_NAME}/${PROTOCOL_FILE_NAME}`;
9
10
  export const getTasksFilePath = (workspacePath) => join(workspacePath, HIVE_DIR_NAME, TASKS_FILE_NAME);
10
11
  export const getProtocolFilePath = (workspacePath) => join(workspacePath, HIVE_DIR_NAME, PROTOCOL_FILE_NAME);
11
12
  const getLegacyTasksFilePath = (workspacePath) => join(workspacePath, TASKS_FILE_NAME);
13
+ const RETRYABLE_TASKS_FS_ERROR_CODES = new Set(['EACCES', 'EBUSY', 'EPERM']);
14
+ const TASKS_FS_RETRY_DELAYS_MS = [20, 50, 100];
15
+ const sleepSync = (ms) => {
16
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
17
+ };
18
+ const runRetryableTasksFileOperation = (operation) => {
19
+ for (let attempt = 0;; attempt += 1) {
20
+ try {
21
+ return operation();
22
+ }
23
+ catch (error) {
24
+ const code = error?.code;
25
+ const delay = TASKS_FS_RETRY_DELAYS_MS[attempt];
26
+ if (!code || !RETRYABLE_TASKS_FS_ERROR_CODES.has(code) || delay === undefined)
27
+ throw error;
28
+ sleepSync(delay);
29
+ }
30
+ }
31
+ };
12
32
  const ensureTasksDir = (workspacePath) => {
13
- mkdirSync(dirname(getTasksFilePath(workspacePath)), { recursive: true });
33
+ runRetryableTasksFileOperation(() => mkdirSync(dirname(getTasksFilePath(workspacePath)), { recursive: true }));
14
34
  };
15
35
  export const ensureTasksFile = (workspacePath) => {
16
36
  ensureTasksDir(workspacePath);
17
37
  const tasksFilePath = getTasksFilePath(workspacePath);
18
38
  if (existsSync(tasksFilePath)) {
19
- return readFileSync(tasksFilePath, 'utf8');
39
+ return runRetryableTasksFileOperation(() => readFileSync(tasksFilePath, 'utf8'));
20
40
  }
21
41
  const legacyTasksFilePath = getLegacyTasksFilePath(workspacePath);
22
- const content = existsSync(legacyTasksFilePath) ? readFileSync(legacyTasksFilePath, 'utf8') : '';
23
- writeFileSync(tasksFilePath, content, 'utf8');
42
+ const content = existsSync(legacyTasksFilePath)
43
+ ? runRetryableTasksFileOperation(() => readFileSync(legacyTasksFilePath, 'utf8'))
44
+ : '';
45
+ runRetryableTasksFileOperation(() => writeFileSync(tasksFilePath, content, 'utf8'));
24
46
  return content;
25
47
  };
26
48
  /**
@@ -29,14 +51,16 @@ export const ensureTasksFile = (workspacePath) => {
29
51
  * on every workspace open means a Hive version bump that changes the rules
30
52
  * propagates without manual intervention.
31
53
  */
32
- export const ensureProtocolFile = (workspacePath) => {
54
+ export const ensureProtocolFile = (workspacePath, cliPolicy, flags = FEATURE_FLAGS_ALL_OFF) => {
33
55
  ensureTasksDir(workspacePath);
34
56
  const protocolFilePath = getProtocolFilePath(workspacePath);
35
- const desired = buildProtocolDoc();
36
- const current = existsSync(protocolFilePath) ? readFileSync(protocolFilePath, 'utf8') : null;
57
+ const desired = buildProtocolDoc(cliPolicy, flags);
58
+ const current = existsSync(protocolFilePath)
59
+ ? runRetryableTasksFileOperation(() => readFileSync(protocolFilePath, 'utf8'))
60
+ : null;
37
61
  if (current === desired)
38
62
  return desired;
39
- writeFileSync(protocolFilePath, desired, 'utf8');
63
+ runRetryableTasksFileOperation(() => writeFileSync(protocolFilePath, desired, 'utf8'));
40
64
  return desired;
41
65
  };
42
66
  export const createTasksFileService = () => {
@@ -46,7 +70,7 @@ export const createTasksFileService = () => {
46
70
  },
47
71
  writeTasks(workspacePath, content) {
48
72
  ensureTasksDir(workspacePath);
49
- writeFileSync(getTasksFilePath(workspacePath), content, 'utf8');
73
+ runRetryableTasksFileOperation(() => writeFileSync(getTasksFilePath(workspacePath), content, 'utf8'));
50
74
  },
51
75
  };
52
76
  };
@@ -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' | 'next' | '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,14 @@
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
+ 'next',
6
+ 'cancel',
7
+ 'help',
8
+ 'spawn',
9
+ 'dismiss',
10
+ 'workflow',
11
+ ]);
3
12
  const WORKER_COMMANDS = new Set(['report', 'status', 'help']);
4
13
  const WORKER_ROLES = new Set(['coder', 'reviewer', 'tester', 'custom']);
5
14
  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
  });
@@ -2,14 +2,21 @@ import type { TeamListItem } from '../shared/types.js';
2
2
  import type { AgentRuntime } from './agent-runtime.js';
3
3
  import type { DispatchRecord } from './dispatch-ledger-store.js';
4
4
  import type { MessageLogHandle, MessageLogRecord } from './message-log-store.js';
5
+ import type { ReportOutboxStore } from './report-outbox-store.js';
6
+ import type { WebhookEvent } from './webhook-notifier.js';
7
+ import type { WorkflowDispatchAwaiter } from './workflow-dispatch-awaiter.js';
5
8
  import type { WorkspaceStore } from './workspace-store.js';
6
9
  export declare const formatUnknownWorkerError: (workerName: string, roster: readonly TeamListItem[]) => string;
7
10
  export interface TeamOperationsInput {
8
11
  agentRuntime: AgentRuntime;
9
12
  createDispatch: (input: {
10
13
  fromAgentId?: string;
14
+ label?: string;
15
+ phase?: string;
16
+ stepIndex?: number;
11
17
  text: string;
12
18
  toAgentId: string;
19
+ workflowRunId?: string;
13
20
  workspaceId: string;
14
21
  }) => DispatchRecord;
15
22
  deleteDispatch: (dispatchId: string) => void;
@@ -30,11 +37,23 @@ export interface TeamOperationsInput {
30
37
  workspaceId: string;
31
38
  }) => DispatchRecord | undefined;
32
39
  markDispatchSubmitted: (dispatchId: string) => void;
40
+ reportOutbox: ReportOutboxStore;
41
+ /** Fire an outbound completion webhook (best-effort) when a worker reports. */
42
+ notifyWebhook?: (event: WebhookEvent) => void;
43
+ workflowDispatchAwaiter: WorkflowDispatchAwaiter;
33
44
  workspaceStore: WorkspaceStore;
45
+ /** Auto-dismiss an ephemeral orchestrator-spawned worker after its
46
+ * dispatch report. Wired in runtime-store-helpers to remove the worker
47
+ * completely (stop run, drop launch config, drop the row). M11. */
48
+ dismissEphemeralWorker?: (workspaceId: string, workerId: string) => void;
34
49
  }
35
50
  export interface DispatchTaskInput {
36
51
  fromAgentId?: string;
37
52
  hivePort?: string;
53
+ workflowRunId?: string;
54
+ stepIndex?: number;
55
+ phase?: string;
56
+ label?: string;
38
57
  }
39
58
  export interface ReportTaskInput {
40
59
  artifacts?: string[];
@@ -57,13 +76,14 @@ export interface ReportTaskResult {
57
76
  forwardError: string | null;
58
77
  forwarded: boolean;
59
78
  }
60
- export declare const createTeamOperations: ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, workspaceStore, }: TeamOperationsInput) => {
79
+ export declare const createTeamOperations: ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, reportOutbox, notifyWebhook, workflowDispatchAwaiter, workspaceStore, dismissEphemeralWorker, }: TeamOperationsInput) => {
61
80
  cancelTask(workspaceId: string, dispatchId: string, input: CancelTaskInput): {
62
81
  dispatch: DispatchRecord;
63
82
  forwardError: string | null;
64
83
  forwarded: boolean;
65
84
  };
66
85
  dispatchTask: (workspaceId: string, workerId: string, text: string, input?: DispatchTaskInput) => Promise<DispatchRecord>;
86
+ drainReportOutbox: (workspaceId: string) => void;
67
87
  dispatchTaskByWorkerName(workspaceId: string, workerName: string, text: string, input?: DispatchTaskInput): Promise<DispatchRecord & {
68
88
  restartedWorker: boolean;
69
89
  }>;
@@ -1,5 +1,7 @@
1
- import { ConflictError, PtyInactiveError } from './http-errors.js';
1
+ import { buildOrchestratorReportPayload } from './agent-stdin-dispatcher.js';
2
+ import { ConflictError } from './http-errors.js';
2
3
  import { createReportMessage, createSendMessage, createStatusMessage, createUserInputMessage, } from './runtime-message-builders.js';
4
+ import { getWorkflowAgentId } from './workspace-store-support.js';
3
5
  /* Roster snapshot embedded in the 409 the orchestrator sees when it
4
6
  dispatches to a missing name. Format prioritizes the orchestrator's
5
7
  parsing path: bullet-per-member with role + live status + pending
@@ -23,7 +25,27 @@ export const formatUnknownWorkerError = (workerName, roster) => {
23
25
  ].join('\n');
24
26
  };
25
27
  const reportForwardErrorMessage = (error) => error instanceof Error ? error.message : String(error);
26
- export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, workspaceStore, }) => {
28
+ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, reportOutbox, notifyWebhook, workflowDispatchAwaiter, workspaceStore, dismissEphemeralWorker, }) => {
29
+ // Best-effort redelivery of reports a prior orchestrator outage stranded.
30
+ // Called when a fresh report confirms the orchestrator is reachable and
31
+ // when the orchestrator polls `team list` (its natural post-restart wakeup).
32
+ // An entry is marked delivered only after its PTY write actually resolves,
33
+ // so a still-down orchestrator just leaves the backlog pending.
34
+ const drainReportOutbox = (workspaceId) => {
35
+ const orchestratorId = `${workspaceId}:orchestrator`;
36
+ if (!agentRuntime.getActiveRunByAgentId(workspaceId, orchestratorId))
37
+ return;
38
+ for (const entry of reportOutbox.listPending(workspaceId, orchestratorId)) {
39
+ void agentRuntime
40
+ .deliverSystemMessageToAgent(workspaceId, orchestratorId, entry.payload, {
41
+ requireActiveRun: true,
42
+ })
43
+ .then(() => reportOutbox.markDelivered(entry.id))
44
+ .catch((error) => {
45
+ console.error('[hive] swallowed:teamReport.outboxDrain', error);
46
+ });
47
+ }
48
+ };
27
49
  const ensureWorkerRun = async (workspaceId, workerId, hivePort) => {
28
50
  if (agentRuntime.getActiveRunByAgentId(workspaceId, workerId)) {
29
51
  return;
@@ -45,10 +67,25 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
45
67
  throw error;
46
68
  }
47
69
  };
70
+ const cancelUndeliveredDispatch = (workspaceId, workerId, dispatchId, reason, workflowRunId) => {
71
+ const cancelled = markDispatchCancelled({ dispatchId, reason, workspaceId });
72
+ if (!cancelled)
73
+ return;
74
+ try {
75
+ workspaceStore.markTaskCancelled(workspaceId, workerId);
76
+ }
77
+ catch (error) {
78
+ console.error('[hive] swallowed:teamDispatch.markTaskCancelled', error);
79
+ }
80
+ if (workflowRunId !== undefined) {
81
+ workflowDispatchAwaiter.notifyCancel(dispatchId, reason);
82
+ }
83
+ };
48
84
  const dispatchTask = async (workspaceId, workerId, text, input = {}) => {
49
85
  const message = createSendMessage(workspaceId, workerId, text, input.fromAgentId);
50
86
  const messageHandle = insertMessage(message);
51
87
  let dispatch;
88
+ let pendingMarked = false;
52
89
  try {
53
90
  const dispatchInput = {
54
91
  text,
@@ -57,18 +94,69 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
57
94
  };
58
95
  if (input.fromAgentId)
59
96
  dispatchInput.fromAgentId = input.fromAgentId;
97
+ if (input.workflowRunId !== undefined)
98
+ dispatchInput.workflowRunId = input.workflowRunId;
99
+ if (input.stepIndex !== undefined)
100
+ dispatchInput.stepIndex = input.stepIndex;
101
+ if (input.phase !== undefined)
102
+ dispatchInput.phase = input.phase;
103
+ if (input.label !== undefined)
104
+ dispatchInput.label = input.label;
60
105
  dispatch = createDispatch(dispatchInput);
106
+ const dispatchId = dispatch.id;
61
107
  if (input.fromAgentId) {
62
108
  const sender = workspaceStore.getAgent(workspaceId, input.fromAgentId);
63
109
  await ensureWorkerRun(workspaceId, workerId, input.hivePort ?? '');
64
110
  const worker = workspaceStore.getWorker(workspaceId, workerId);
65
- markDispatchSubmitted(dispatch.id);
66
- agentRuntime.writeSendPrompt(workspaceId, workerId, dispatch.id, sender.name, worker.description, text);
111
+ const isWorkflowDispatch = input.workflowRunId !== undefined || input.fromAgentId === getWorkflowAgentId(workspaceId);
112
+ markDispatchSubmitted(dispatchId);
113
+ workspaceStore.markTaskDispatched(workspaceId, workerId);
114
+ pendingMarked = true;
115
+ try {
116
+ const writePrompt = agentRuntime.writeSendPrompt(workspaceId, workerId, dispatchId, sender.name, worker.description, text);
117
+ void writePrompt.catch((error) => {
118
+ // `team send` is intentionally asynchronous (§3.3). A worker that
119
+ // exits during paste-submit did not receive actionable work, so
120
+ // close the open dispatch instead of leaving a fake pending task.
121
+ try {
122
+ cancelUndeliveredDispatch(workspaceId, workerId, dispatchId, reportForwardErrorMessage(error), input.workflowRunId);
123
+ }
124
+ catch (cancelError) {
125
+ if (!isWorkflowDispatch)
126
+ console.error('[hive] swallowed:teamDispatch.cancelUndelivered', cancelError);
127
+ }
128
+ if (!isWorkflowDispatch)
129
+ console.error('[hive] swallowed:teamDispatch.writePrompt', error);
130
+ });
131
+ }
132
+ catch (error) {
133
+ try {
134
+ cancelUndeliveredDispatch(workspaceId, workerId, dispatchId, reportForwardErrorMessage(error), input.workflowRunId);
135
+ }
136
+ catch (cancelError) {
137
+ if (!isWorkflowDispatch)
138
+ console.error('[hive] swallowed:teamDispatch.cancelUndelivered', cancelError);
139
+ }
140
+ if (!isWorkflowDispatch)
141
+ console.error('[hive] swallowed:teamDispatch.writePrompt', error);
142
+ }
143
+ }
144
+ else {
145
+ workspaceStore.markTaskDispatched(workspaceId, workerId);
146
+ pendingMarked = true;
67
147
  }
68
- workspaceStore.markTaskDispatched(workspaceId, workerId);
69
148
  return dispatch;
70
149
  }
71
150
  catch (error) {
151
+ if (pendingMarked) {
152
+ try {
153
+ workspaceStore.markTaskCancelled(workspaceId, workerId);
154
+ }
155
+ catch {
156
+ // Best-effort compensation for the in-memory pending count; the
157
+ // durable send message is deleted below.
158
+ }
159
+ }
72
160
  if (dispatch)
73
161
  deleteDispatch(dispatch.id);
74
162
  deleteMessage(messageHandle);
@@ -104,6 +192,7 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
104
192
  return { dispatch, forwardError, forwarded };
105
193
  },
106
194
  dispatchTask,
195
+ drainReportOutbox,
107
196
  async dispatchTaskByWorkerName(workspaceId, workerName, text, input = {}) {
108
197
  /* Build the roster once so a missing-name path can surface it without
109
198
  a second store call. We deliberately don't go through
@@ -164,14 +253,7 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
164
253
  const status = input.status;
165
254
  const artifacts = input.artifacts ?? [];
166
255
  const worker = workspaceStore.getWorker(workspaceId, workerId);
167
- if (input.requireActiveRun === true &&
168
- !agentRuntime.getActiveRunByAgentId(workspaceId, `${workspaceId}:orchestrator`)) {
169
- throw new PtyInactiveError(`No active run for agent: ${workspaceId}:orchestrator`);
170
- }
171
256
  const openDispatch = findOpenDispatch(workspaceId, workerId, input.dispatchId);
172
- if (!openDispatch && input.dispatchId) {
173
- throw new ConflictError(`No open dispatch for worker: ${worker.name}`);
174
- }
175
257
  if (!openDispatch) {
176
258
  throw new ConflictError(`No open dispatch for worker: ${worker.name}`);
177
259
  }
@@ -190,17 +272,102 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
190
272
  workspaceStore.markTaskReported(workspaceId, workerId);
191
273
  let forwardError = null;
192
274
  let forwarded = false;
193
- if (input.requireActiveRun === true) {
275
+ // Workflow-sourced dispatches: the source is the in-process runner, not a
276
+ // PTY. Resolve its awaiting Promise instead of injecting into orchestrator
277
+ // stdin (which would do nothing — `__workflow__` has no PTY).
278
+ if (dispatch.fromAgentId === getWorkflowAgentId(workspaceId)) {
194
279
  try {
195
- agentRuntime.writeReportPrompt(workspaceId, worker.name, workerId, text, artifacts, {
196
- requireActiveRun: input.requireActiveRun,
280
+ workflowDispatchAwaiter.notifyReport(dispatch.id, {
281
+ artifacts,
282
+ text,
283
+ ...(status ? { status } : {}),
197
284
  });
198
285
  forwarded = true;
199
286
  }
200
287
  catch (error) {
201
288
  forwardError = reportForwardErrorMessage(error);
202
- console.error('[hive] swallowed:teamReport.forward', error);
289
+ console.error('[hive] swallowed:teamReport.workflowForward', error);
203
290
  }
291
+ return { dispatch, forwardError, forwarded };
292
+ }
293
+ // Real worker reported (not a workflow-internal step) — fire the
294
+ // outbound completion webhook. Best-effort; never blocks the report.
295
+ notifyWebhook?.({
296
+ type: 'report_received',
297
+ workspaceId,
298
+ agentId: workerId,
299
+ agentName: worker.name,
300
+ summary: text.slice(0, 280),
301
+ at: Date.now(),
302
+ });
303
+ if (input.requireActiveRun === true) {
304
+ const orchestratorId = `${workspaceId}:orchestrator`;
305
+ // A fresh report proves the orchestrator is reachable — flush any
306
+ // backlog a prior outage stranded first, in arrival order, before
307
+ // this one (both ride the dispatcher's per-agent serial queue).
308
+ drainReportOutbox(workspaceId);
309
+ const payload = buildOrchestratorReportPayload(worker.name, text, artifacts);
310
+ if (agentRuntime.getActiveRunByAgentId(workspaceId, orchestratorId)) {
311
+ try {
312
+ const delivery = agentRuntime.deliverReportToOrchestrator(workspaceId, worker.name, text, artifacts, { requireActiveRun: true });
313
+ forwarded = true;
314
+ // The dispatch is already marked reported. If the PTY dies
315
+ // mid-write the ledger would say "reported" while the
316
+ // orchestrator never received it — persist for redelivery so it
317
+ // isn't silently lost.
318
+ void delivery.catch((error) => {
319
+ console.error('[hive] swallowed:teamReport.forward', error);
320
+ reportOutbox.enqueue({
321
+ workspaceId,
322
+ targetAgentId: orchestratorId,
323
+ dispatchId: dispatch.id,
324
+ payload,
325
+ });
326
+ });
327
+ }
328
+ catch (error) {
329
+ // TOCTOU: orchestrator vanished between the active-run check and
330
+ // the write. Same fix — queue it.
331
+ console.error('[hive] swallowed:teamReport.forward', error);
332
+ forwardError = reportForwardErrorMessage(error);
333
+ reportOutbox.enqueue({
334
+ workspaceId,
335
+ targetAgentId: orchestratorId,
336
+ dispatchId: dispatch.id,
337
+ payload,
338
+ });
339
+ }
340
+ }
341
+ else {
342
+ // Orchestrator is down. Queue the report instead of dropping it;
343
+ // the CLI surfaces forwarded:false and the backlog drains on the
344
+ // orchestrator's next `team list` after it restarts.
345
+ forwardError = 'Orchestrator is not running; report queued for delivery.';
346
+ reportOutbox.enqueue({
347
+ workspaceId,
348
+ targetAgentId: orchestratorId,
349
+ dispatchId: dispatch.id,
350
+ payload,
351
+ });
352
+ }
353
+ }
354
+ // M11: if this worker was spawned with `team spawn --ephemeral`, this
355
+ // first successful report is its trigger to auto-dismiss. Deferred via
356
+ // queueMicrotask so the orchestrator's forward write lands BEFORE the
357
+ // worker's PTY is torn down (otherwise the inject + dismiss race).
358
+ // Skipped for workflow dispatches — workflow workers are managed by
359
+ // the runner's own finally block.
360
+ if (worker.ephemeral === true &&
361
+ worker.spawnedBy === 'orchestrator' &&
362
+ dismissEphemeralWorker) {
363
+ queueMicrotask(() => {
364
+ try {
365
+ dismissEphemeralWorker(workspaceId, workerId);
366
+ }
367
+ catch (error) {
368
+ console.error('[hive] swallowed:teamReport.ephemeralDismiss', error);
369
+ }
370
+ });
204
371
  }
205
372
  return { dispatch, forwardError, forwarded };
206
373
  }
@@ -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');