@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
@@ -6,19 +6,27 @@ export interface DispatchRecord {
6
6
  deliveredAt: number | null;
7
7
  fromAgentId: string | null;
8
8
  id: string;
9
+ label: string | null;
10
+ phase: string | null;
9
11
  reportedAt: number | null;
10
12
  reportText: string | null;
11
13
  sequence: number | null;
12
14
  status: DispatchStatus;
15
+ stepIndex: number | null;
13
16
  submittedAt: number | null;
14
17
  text: string;
15
18
  toAgentId: string;
19
+ workflowRunId: string | null;
16
20
  workspaceId: string;
17
21
  }
18
22
  interface CreateDispatchInput {
19
23
  fromAgentId?: string;
24
+ label?: string;
25
+ phase?: string;
26
+ stepIndex?: number;
20
27
  text: string;
21
28
  toAgentId: string;
29
+ workflowRunId?: string;
22
30
  workspaceId: string;
23
31
  }
24
32
  interface ReportDispatchInput {
@@ -50,6 +58,12 @@ export declare const createDispatchLedgerStore: (db: Database) => {
50
58
  worker_id: string;
51
59
  workspace_id: string;
52
60
  }>;
61
+ listOpenDispatchIdsForRun: (runId: string) => string[];
62
+ listOpenWorkflowDispatchesForWorker: (workspaceId: string, workerId: string) => Array<{
63
+ dispatchId: string;
64
+ runId: string;
65
+ }>;
66
+ listWorkflowRunDispatches: (runId: string) => DispatchRecord[];
53
67
  listWorkspaceDispatches: (workspaceId: string, options?: ListDispatchesOptions) => DispatchRecord[];
54
68
  markCancelled: (input: CancelDispatchInput) => {
55
69
  reportedAt: number;
@@ -60,10 +74,14 @@ export declare const createDispatchLedgerStore: (db: Database) => {
60
74
  deliveredAt: number | null;
61
75
  fromAgentId: string | null;
62
76
  id: string;
77
+ label: string | null;
78
+ phase: string | null;
63
79
  sequence: number | null;
80
+ stepIndex: number | null;
64
81
  submittedAt: number | null;
65
82
  text: string;
66
83
  toAgentId: string;
84
+ workflowRunId: string | null;
67
85
  workspaceId: string;
68
86
  } | undefined;
69
87
  markReportedByWorker: (input: ReportDispatchInput) => {
@@ -75,10 +93,14 @@ export declare const createDispatchLedgerStore: (db: Database) => {
75
93
  deliveredAt: number | null;
76
94
  fromAgentId: string | null;
77
95
  id: string;
96
+ label: string | null;
97
+ phase: string | null;
78
98
  sequence: number | null;
99
+ stepIndex: number | null;
79
100
  submittedAt: number | null;
80
101
  text: string;
81
102
  toAgentId: string;
103
+ workflowRunId: string | null;
82
104
  workspaceId: string;
83
105
  } | undefined;
84
106
  markSubmitted: (dispatchId: string) => void;
@@ -18,13 +18,17 @@ const toRecord = (row) => ({
18
18
  deliveredAt: row.delivered_at,
19
19
  fromAgentId: row.from_agent_id,
20
20
  id: row.id,
21
+ label: row.label ?? null,
22
+ phase: row.phase ?? null,
21
23
  reportedAt: row.reported_at,
22
24
  reportText: row.report_text,
23
25
  sequence: row.sequence,
24
26
  status: row.status,
27
+ stepIndex: row.step_index,
25
28
  submittedAt: row.submitted_at,
26
29
  text: row.text,
27
30
  toAgentId: row.to_agent_id,
31
+ workflowRunId: row.workflow_run_id,
28
32
  workspaceId: row.workspace_id,
29
33
  });
30
34
  export const createDispatchLedgerStore = (db) => {
@@ -35,13 +39,17 @@ export const createDispatchLedgerStore = (db) => {
35
39
  deliveredAt: null,
36
40
  fromAgentId: input.fromAgentId ?? null,
37
41
  id: randomUUID(),
42
+ label: input.label ?? null,
43
+ phase: input.phase ?? null,
38
44
  reportedAt: null,
39
45
  reportText: null,
40
46
  sequence: null,
41
47
  status: 'queued',
48
+ stepIndex: input.stepIndex ?? null,
42
49
  submittedAt: null,
43
50
  text: input.text,
44
51
  toAgentId: input.toAgentId,
52
+ workflowRunId: input.workflowRunId ?? null,
45
53
  workspaceId: input.workspaceId,
46
54
  };
47
55
  db.prepare(`INSERT INTO dispatches (
@@ -56,8 +64,12 @@ export const createDispatchLedgerStore = (db) => {
56
64
  submitted_at,
57
65
  reported_at,
58
66
  report_text,
59
- artifacts
60
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(record.id, record.workspaceId, record.fromAgentId, record.toAgentId, record.text, record.status, record.createdAt, record.deliveredAt, record.submittedAt, record.reportedAt, record.reportText, JSON.stringify(record.artifacts));
67
+ artifacts,
68
+ workflow_run_id,
69
+ step_index,
70
+ phase,
71
+ label
72
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(record.id, record.workspaceId, record.fromAgentId, record.toAgentId, record.text, record.status, record.createdAt, record.deliveredAt, record.submittedAt, record.reportedAt, record.reportText, JSON.stringify(record.artifacts), record.workflowRunId, record.stepIndex, record.phase, record.label);
61
73
  return record;
62
74
  };
63
75
  const deleteDispatch = (dispatchId) => {
@@ -175,8 +187,41 @@ export const createDispatchLedgerStore = (db) => {
175
187
  db.prepare('DELETE FROM dispatches WHERE workspace_id = ?').run(workspaceId);
176
188
  };
177
189
  const deleteWorkerDispatches = (workspaceId, workerId) => {
178
- db.prepare('DELETE FROM dispatches WHERE workspace_id = ? AND to_agent_id = ?').run(workspaceId, workerId);
190
+ // Preserve workflow-run dispatch history (`workflow_run_id IS NOT NULL`)
191
+ // so the UI's run-detail timeline can still show what each ephemeral
192
+ // worker did after the worker itself is dismissed. Orchestrator-issued
193
+ // dispatches are tied to the worker identity and are dropped with it.
194
+ db.prepare(`DELETE FROM dispatches
195
+ WHERE workspace_id = ? AND to_agent_id = ? AND workflow_run_id IS NULL`).run(workspaceId, workerId);
179
196
  };
197
+ // Every dispatch fired by a workflow run carries the run id (M1-B added the
198
+ // column; M2-C plumbs it through). This is the timeline query the UI uses to
199
+ // explode a run row into per-worker activity.
200
+ const listWorkflowRunDispatches = (runId) => {
201
+ const rows = db
202
+ .prepare('SELECT * FROM dispatches WHERE workflow_run_id = ? ORDER BY sequence, created_at')
203
+ .all(runId);
204
+ return rows.map(toRecord);
205
+ };
206
+ // Open dispatch ids tied to a workflow run — drives the runner's stop path
207
+ // (each id gets a notifyCancel so the runner's await rejects).
208
+ const listOpenDispatchIdsForRun = (runId) => db
209
+ .prepare(`SELECT id FROM dispatches
210
+ WHERE workflow_run_id = ? AND status IN ('queued', 'submitted')`)
211
+ .all(runId).map((row) => row.id);
212
+ // Open workflow-tagged dispatches addressed to a specific worker. Drives the
213
+ // PTY-exit cancel path (TIER 1 #1): when a workflow-spawned worker dies
214
+ // without calling `team report`, the runner's `awaitReport` would otherwise
215
+ // hang for DEFAULT_TIMEOUT_MS (10 min). The exit handler enumerates these
216
+ // and `notifyCancel`s each so the surrounding `agent()`/`parallel`/`pipeline`
217
+ // sees an immediate reject.
218
+ const listOpenWorkflowDispatchesForWorker = (workspaceId, workerId) => db
219
+ .prepare(`SELECT id, workflow_run_id FROM dispatches
220
+ WHERE workspace_id = ?
221
+ AND to_agent_id = ?
222
+ AND workflow_run_id IS NOT NULL
223
+ AND status IN ('queued', 'submitted')`)
224
+ .all(workspaceId, workerId).map((row) => ({ dispatchId: row.id, runId: row.workflow_run_id }));
180
225
  return {
181
226
  createDispatch,
182
227
  deleteDispatch,
@@ -185,6 +230,9 @@ export const createDispatchLedgerStore = (db) => {
185
230
  findOpenDispatch,
186
231
  findOpenDispatchById,
187
232
  listOpenDispatchKinds,
233
+ listOpenDispatchIdsForRun,
234
+ listOpenWorkflowDispatchesForWorker,
235
+ listWorkflowRunDispatches,
188
236
  listWorkspaceDispatches,
189
237
  markCancelled,
190
238
  markReportedByWorker,
@@ -4,7 +4,7 @@ import { TASKS_RELATIVE_PATH } from './tasks-file.js';
4
4
  const TASKS_HEAD_LIMIT = 1024;
5
5
  const formatWorkers = (workers) => {
6
6
  if (workers.length === 0)
7
- return ['- 当前没有其他 worker'];
7
+ return ['- (no other workers)'];
8
8
  return workers.map((worker) => `- ${worker.name} (${worker.role}, ${worker.status}, pending_task_count: ${worker.pendingTaskCount})`);
9
9
  };
10
10
  const formatRestartWindow = (messages) => {
@@ -12,18 +12,18 @@ const formatRestartWindow = (messages) => {
12
12
  return message.type === 'send';
13
13
  });
14
14
  if (sends.length === 0)
15
- return ['- 重启期间未派新单'];
15
+ return ['- no new dispatches during the restart'];
16
16
  return sends.slice(-5).map((message) => `- send -> ${message.to}: ${message.text}`);
17
17
  };
18
18
  export const buildEnvSyncMessage = ({ agent, tasksContent, workers, workspace, restartWindowMessages, }) => wrapSystemMessage([
19
- '你刚被 Hive 重启了。期间环境变化:',
20
- `- 当前 workspace: ${workspace.name}`,
21
- '- 现有 worker:',
19
+ 'Hive just restarted you. Environment changes during the restart:',
20
+ `- Current workspace: ${workspace.name}`,
21
+ '- Existing workers:',
22
22
  ...formatWorkers(workers),
23
- `- ${TASKS_RELATIVE_PATH} 当前内容:`,
24
- tasksContent.slice(0, TASKS_HEAD_LIMIT) || '()',
23
+ `- Current ${TASKS_RELATIVE_PATH} contents:`,
24
+ tasksContent.slice(0, TASKS_HEAD_LIMIT) || '(empty)',
25
25
  ...formatRestartWindow(restartWindowMessages),
26
- agent.role === 'orchestrator' ? '- Hive worker 派单规则:' : '- Hive worker 边界:',
26
+ agent.role === 'orchestrator' ? '- Hive worker dispatch rules:' : '- Hive worker boundaries:',
27
27
  ...getHiveTeamRules(agent).map((rule) => ` - ${rule}`),
28
- `请继续。如果不确定,用 team list / Read ${TASKS_RELATIVE_PATH} 自查或问 user。`,
28
+ `Continue. If unsure, run team list / Read ${TASKS_RELATIVE_PATH} to self-check, or ask the user.`,
29
29
  ].join('\n'));
@@ -22,4 +22,17 @@ export interface FsProbeResponse {
22
22
  suggested_name: string;
23
23
  }
24
24
  export declare const browseDirectory: (requestedPath: string) => Promise<FsBrowseResponse>;
25
- export declare const probeDirectory: (requestedPath: string) => Promise<FsProbeResponse>;
25
+ export interface ProbeDirectoryOptions {
26
+ /**
27
+ * When `true` (default), probe rejects paths outside `$HOME` so the
28
+ * in-browser FS tree can't be tricked into revealing arbitrary disk
29
+ * contents via a hand-crafted path string.
30
+ *
31
+ * When `false`, the sandbox check is skipped — callers who already
32
+ * have a user-authorized path (e.g. paths returned by the OS-native
33
+ * folder picker) must use this so a Windows user picking `D:\projects`
34
+ * isn't rejected just because their `$HOME` lives on `C:`.
35
+ */
36
+ enforceSandbox?: boolean;
37
+ }
38
+ export declare const probeDirectory: (requestedPath: string, options?: ProbeDirectoryOptions) => Promise<FsProbeResponse>;
@@ -5,6 +5,29 @@ import { promisify } from 'node:util';
5
5
  import { getFsBrowseRoot, isPathWithinRoot } from './fs-sandbox.js';
6
6
  const execFileP = promisify(execFile);
7
7
  const GIT_BRANCH_TIMEOUT_MS = 800;
8
+ /**
9
+ * Map a filesystem rejection (from `readdir`, `stat`, etc.) to a string
10
+ * suitable for surfacing in the browse response. The common Windows
11
+ * failure paths — System Volume Information / $Recycle.Bin (EACCES),
12
+ * dangling junctions (EBUSY / EINVAL), paths past MAX_PATH on systems
13
+ * without long-path support (ENAMETOOLONG) — each get a recognizable
14
+ * prefix so the UI does not just show the raw errno.
15
+ */
16
+ const formatFilesystemError = (error) => {
17
+ if (!(error instanceof Error))
18
+ return 'Failed to read directory';
19
+ const code = error.code;
20
+ if (code === 'EACCES' || code === 'EPERM') {
21
+ return `Permission denied: ${error.message}`;
22
+ }
23
+ if (code === 'ENAMETOOLONG') {
24
+ return `Path is too long for this filesystem: ${error.message}`;
25
+ }
26
+ if (code === 'EBUSY' || code === 'EINVAL') {
27
+ return `Path is busy or unavailable: ${error.message}`;
28
+ }
29
+ return error.message;
30
+ };
8
31
  const detectGitRepository = async (entryPath) => {
9
32
  try {
10
33
  const info = await stat(resolve(entryPath, '.git'));
@@ -49,7 +72,7 @@ export const browseDirectory = async (requestedPath) => {
49
72
  return {
50
73
  current_path: candidate,
51
74
  entries: [],
52
- error: error instanceof Error ? error.message : 'Failed to stat directory',
75
+ error: formatFilesystemError(error),
53
76
  ok: false,
54
77
  parent_path: null,
55
78
  root_path: rootPath,
@@ -65,7 +88,24 @@ export const browseDirectory = async (requestedPath) => {
65
88
  root_path: rootPath,
66
89
  };
67
90
  }
68
- const rawEntries = await readdir(candidate, { withFileTypes: true });
91
+ let rawEntries;
92
+ try {
93
+ rawEntries = await readdir(candidate, { withFileTypes: true });
94
+ }
95
+ catch (error) {
96
+ // Windows hits this for System Volume Information, $Recycle.Bin,
97
+ // broken junctions, and long paths on hosts without long-path
98
+ // support. Returning ok:false lets the picker surface a readable
99
+ // message instead of crashing the HTTP handler.
100
+ return {
101
+ current_path: candidate,
102
+ entries: [],
103
+ error: formatFilesystemError(error),
104
+ ok: false,
105
+ parent_path: null,
106
+ root_path: rootPath,
107
+ };
108
+ }
69
109
  const directoryEntries = rawEntries
70
110
  .filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
71
111
  .sort((a, b) => a.name.localeCompare(b.name));
@@ -91,9 +131,12 @@ export const browseDirectory = async (requestedPath) => {
91
131
  root_path: rootPath,
92
132
  };
93
133
  };
94
- export const probeDirectory = async (requestedPath) => {
134
+ export const probeDirectory = async (requestedPath, options = {}) => {
135
+ const enforceSandbox = options.enforceSandbox ?? true;
95
136
  const rootPath = getFsBrowseRoot();
96
- const candidate = resolve(rootPath, requestedPath.trim());
137
+ const candidate = enforceSandbox
138
+ ? resolve(rootPath, requestedPath.trim())
139
+ : resolve(requestedPath.trim());
97
140
  const base = {
98
141
  current_branch: null,
99
142
  exists: false,
@@ -103,7 +146,7 @@ export const probeDirectory = async (requestedPath) => {
103
146
  path: candidate,
104
147
  suggested_name: candidate.split(/[\\/]/).filter(Boolean).pop() ?? '',
105
148
  };
106
- if (!isPathWithinRoot(rootPath, candidate)) {
149
+ if (enforceSandbox && !isPathWithinRoot(rootPath, candidate)) {
107
150
  return base;
108
151
  }
109
152
  try {
@@ -5,6 +5,13 @@ import { probeDirectory } from './fs-browse.js';
5
5
  const MACOS_CANCEL_PATTERNS = [/-128/, /-1743/, /user canceled/i, /execution error/i];
6
6
  // zenity documents exit code 1 on Cancel. kdialog uses exit code 1 as well.
7
7
  const LINUX_CANCEL_EXIT_CODES = new Set([1]);
8
+ // Cap how long we'll wait for a single picker invocation. A reasonable
9
+ // modal-dialog dwell time is well under this — the cap exists to catch
10
+ // genuinely wedged pickers (PowerShell startup hang under restricted
11
+ // execution policy, zenity hung on a missing DBus, osascript blocked on
12
+ // the macOS Accessibility prompt) so the HTTP request returns instead
13
+ // of pinning a connection forever.
14
+ const PICKER_TIMEOUT_MS = 5 * 60 * 1000;
8
15
  const defaultRunCommand = (command, args, options) => new Promise((resolve) => {
9
16
  const child = execFile(command, args, options, (error, stdout, stderr) => {
10
17
  const errno = error;
@@ -30,10 +37,15 @@ const emptyResponse = (overrides = {}) => ({
30
37
  ...overrides,
31
38
  });
32
39
  const finalizeWithProbe = async (path) => {
33
- const probe = await probeDirectory(path);
40
+ // The OS-native folder picker is itself a user-authorization surface
41
+ // — sandboxing again here would reject any drive other than the one
42
+ // hosting `$HOME` (a common Windows case: `D:\projects`, `E:\code`).
43
+ // The in-browser FS tree (fs-browse.ts:browseDirectory) keeps its
44
+ // own sandbox; only the native picker bypasses it.
45
+ const probe = await probeDirectory(path, { enforceSandbox: false });
34
46
  if (!probe.ok || !probe.is_dir) {
35
47
  return emptyResponse({
36
- error: 'Selected path is outside the Hive browse sandbox or is not a directory.',
48
+ error: 'Selected path is not a directory.',
37
49
  path,
38
50
  probe,
39
51
  });
@@ -42,7 +54,7 @@ const finalizeWithProbe = async (path) => {
42
54
  };
43
55
  const macOsPick = async (run) => {
44
56
  const script = 'POSIX path of (choose folder with prompt "Select Hive workspace")';
45
- const result = await run('osascript', ['-e', script], {});
57
+ const result = await run('osascript', ['-e', script], { timeout: PICKER_TIMEOUT_MS });
46
58
  if (result.spawnError?.code === 'ENOENT') {
47
59
  return emptyResponse({ error: 'osascript is unavailable on this host.', supported: false });
48
60
  }
@@ -65,7 +77,7 @@ const macOsPick = async (run) => {
65
77
  return finalizeWithProbe(picked);
66
78
  };
67
79
  const linuxPick = async (run) => {
68
- const result = await run('zenity', ['--file-selection', '--directory', '--title=Select Hive workspace'], {});
80
+ const result = await run('zenity', ['--file-selection', '--directory', '--title=Select Hive workspace'], { timeout: PICKER_TIMEOUT_MS });
69
81
  if (result.spawnError?.code === 'ENOENT') {
70
82
  return emptyResponse({
71
83
  error: 'zenity not installed. Install zenity or use Advanced: paste path.',
@@ -84,16 +96,51 @@ const linuxPick = async (run) => {
84
96
  return finalizeWithProbe(picked);
85
97
  };
86
98
  const windowsPick = async (run) => {
99
+ /* Hive's PowerShell child has no visible main window, so a bare
100
+ `$dialog.ShowDialog()` inherits the desktop as IWin32Window parent —
101
+ and ends up below the foreground browser in z-order. To the user
102
+ that looks like a hang ("Add Workspace pops up then nothing"); the
103
+ picker is open, just occluded.
104
+
105
+ Fix: build a TopMost invisible owner Form, `Show()` it so it has a
106
+ real HWND (an unshown Form has none, and ShowDialog silently falls
107
+ back to desktop-parent), then pass the owner to `ShowDialog($owner)`.
108
+ The owner inherits TopMost z-order onto the dialog. The owner itself
109
+ stays invisible — Opacity 0, parked at (-32000, -32000), 1x1 size,
110
+ no taskbar entry — so the user only sees the picker. Dispose in
111
+ `finally` to release the HWND each invocation.
112
+
113
+ `Add-Type -AssemblyName System.Drawing` is required because Point /
114
+ Size live in System.Drawing.dll, not System.Windows.Forms.dll. */
115
+ // PS 5.1 on zh-CN Windows defaults [Console]::OutputEncoding to cp936/GBK;
116
+ // Node decodes stdout as UTF-8 and CJK paths arrive mojibake'd. Force UTF-8
117
+ // before any output. No-op on PS 7+ which already defaults to UTF-8.
87
118
  const script = [
119
+ '[Console]::OutputEncoding = [System.Text.Encoding]::UTF8',
88
120
  'Add-Type -AssemblyName System.Windows.Forms',
89
- '$dialog = New-Object System.Windows.Forms.FolderBrowserDialog',
90
- '$dialog.Description = "Select Hive workspace"',
91
- '$dialog.ShowNewFolderButton = $false',
92
- '$result = $dialog.ShowDialog()',
93
- 'if ($result -eq [System.Windows.Forms.DialogResult]::OK) { [Console]::Out.WriteLine($dialog.SelectedPath); exit 0 }',
94
- 'exit 1',
121
+ 'Add-Type -AssemblyName System.Drawing',
122
+ '$owner = New-Object System.Windows.Forms.Form',
123
+ "$owner.FormBorderStyle = 'None'",
124
+ '$owner.Opacity = 0',
125
+ '$owner.ShowInTaskbar = $false',
126
+ "$owner.StartPosition = 'Manual'",
127
+ '$owner.Location = New-Object System.Drawing.Point(-32000, -32000)',
128
+ '$owner.Size = New-Object System.Drawing.Size(1, 1)',
129
+ '$owner.TopMost = $true',
130
+ '$owner.Show()',
131
+ 'try {',
132
+ ' $dialog = New-Object System.Windows.Forms.FolderBrowserDialog',
133
+ ' $dialog.Description = "Select Hive workspace"',
134
+ ' $dialog.ShowNewFolderButton = $false',
135
+ ' $result = $dialog.ShowDialog($owner)',
136
+ ' if ($result -eq [System.Windows.Forms.DialogResult]::OK) { [Console]::Out.WriteLine($dialog.SelectedPath); exit 0 }',
137
+ ' exit 1',
138
+ '} finally {',
139
+ ' $owner.Close()',
140
+ ' $owner.Dispose()',
141
+ '}',
95
142
  ].join('; ');
96
- const result = await run('powershell.exe', ['-NoProfile', '-STA', '-ExecutionPolicy', 'Bypass', '-Command', script], {});
143
+ const result = await run('powershell.exe', ['-NoProfile', '-STA', '-ExecutionPolicy', 'Bypass', '-Command', script], { timeout: PICKER_TIMEOUT_MS });
97
144
  if (result.spawnError?.code === 'ENOENT') {
98
145
  return emptyResponse({
99
146
  error: 'PowerShell is unavailable on this host. Use Advanced: paste path.',
@@ -1,5 +1,6 @@
1
1
  import { homedir } from 'node:os';
2
2
  import { isAbsolute, relative, resolve, sep } from 'node:path';
3
+ import { realpathNative } from './path-canonicalization.js';
3
4
  /**
4
5
  * Root directory the FS-browse API is allowed to reveal. We sandbox to
5
6
  * `$HOME` (override via `HIVE_FS_BROWSE_ROOT` for tests). Anything outside
@@ -7,7 +8,19 @@ import { isAbsolute, relative, resolve, sep } from 'node:path';
7
8
  */
8
9
  export const getFsBrowseRoot = () => {
9
10
  const override = process.env.HIVE_FS_BROWSE_ROOT;
10
- return override && override.length > 0 ? resolve(override) : resolve(homedir());
11
+ const root = override && override.length > 0 ? resolve(override) : resolve(homedir());
12
+ try {
13
+ return realpathNative(root);
14
+ }
15
+ catch {
16
+ return root;
17
+ }
18
+ };
19
+ const isResolvedPathWithinRoot = (rootPath, candidatePath) => {
20
+ if (candidatePath === rootPath)
21
+ return true;
22
+ const rel = relative(rootPath, candidatePath);
23
+ return rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
11
24
  };
12
25
  /**
13
26
  * True when `candidatePath` is `rootPath` itself or a descendant of it.
@@ -16,10 +29,26 @@ export const getFsBrowseRoot = () => {
16
29
  * isPathWithinRoot so the semantics match a project we already trust.
17
30
  */
18
31
  export const isPathWithinRoot = (rootPath, candidatePath) => {
19
- const resolvedRoot = resolve(rootPath);
20
- const resolvedCandidate = resolve(candidatePath);
21
- if (resolvedCandidate === resolvedRoot)
22
- return true;
23
- const rel = relative(resolvedRoot, resolvedCandidate);
24
- return rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
32
+ const lexicalRoot = resolve(rootPath);
33
+ const lexicalCandidate = resolve(candidatePath);
34
+ let resolvedRoot = lexicalRoot;
35
+ let resolvedCandidate = lexicalCandidate;
36
+ try {
37
+ resolvedRoot = realpathNative(resolvedRoot);
38
+ }
39
+ catch {
40
+ // Missing / inaccessible roots are handled by the caller's readdir/stat path.
41
+ }
42
+ try {
43
+ resolvedCandidate = realpathNative(resolvedCandidate);
44
+ }
45
+ catch {
46
+ // Non-existent children still need lexical sandboxing for "create later"
47
+ // probes; existing symlinks/junctions use the realpath branch above.
48
+ if (isResolvedPathWithinRoot(lexicalRoot, lexicalCandidate)) {
49
+ resolvedRoot = lexicalRoot;
50
+ resolvedCandidate = lexicalCandidate;
51
+ }
52
+ }
53
+ return isResolvedPathWithinRoot(resolvedRoot, resolvedCandidate);
25
54
  };
@@ -1,4 +1,5 @@
1
1
  import type { AgentSummary } from '../shared/types.js';
2
+ import { type WorkflowCliPolicy } from './workflow-cli-policy.js';
2
3
  /**
3
4
  * Tail reminder appended to every message that flows INTO the orchestrator
4
5
  * (worker reports, worker status updates, user chat input). Re-anchors the
@@ -11,10 +12,12 @@ import type { AgentSummary } from '../shared/types.js';
11
12
  * banner noise after a few occurrences, but `<...-system-reminder>` tags
12
13
  * mirror the out-of-band envelope LLMs are trained to attend to; placement
13
14
  * at the tail (right before the agent's reply turn) maximizes recency
14
- * weighting; phrasing as a two-option action menu is more actionable than
15
- * abstract identity restatement.
15
+ * weighting; phrasing as a short action menu is more actionable than abstract
16
+ * identity restatement. Kept deliberately SHORT — the full command syntax and
17
+ * the workflow DSL live in `.hive/PROTOCOL.md`, which agents re-read on demand.
18
+ * A long banner on every turn is itself the noise this envelope exists to beat.
16
19
  */
17
- export declare const ORCHESTRATOR_REMINDER_TAIL: string;
20
+ export declare const buildOrchestratorReminderTail: (workflowsEnabled: boolean) => string;
18
21
  /**
19
22
  * Tail reminder appended to dispatches sent TO a worker. Reinforces the
20
23
  * worker identity (so the agent does not regress into its normal CLI
@@ -22,12 +25,14 @@ export declare const ORCHESTRATOR_REMINDER_TAIL: string;
22
25
  * with dispatch_id pre-bound.
23
26
  */
24
27
  export declare const buildWorkerReminderTail: (dispatchId: string) => string;
25
- export declare const getHiveTeamRules: (agent: Pick<AgentSummary, "role">) => string[];
28
+ export declare const getHiveTeamRules: (agent: Pick<AgentSummary, "role">, workflowsEnabled?: boolean, autostaffEnabled?: boolean) => readonly string[];
26
29
  /**
27
30
  * Workspace-local protocol cheat sheet written to `.hive/PROTOCOL.md`. Agents
28
31
  * are explicitly trained to look at project root markdown when confused, so
29
32
  * keeping a single canonical doc next to `.hive/tasks.md` doubles as a
30
33
  * "cat-recover" path when both the startup prompt and the in-message
31
- * reminders fail to anchor.
34
+ * reminders fail to anchor. This is also the single home of the full command
35
+ * syntax and the workflow DSL reference — the always-on injections only carry
36
+ * the lean core rules and point here.
32
37
  */
33
- export declare const buildProtocolDoc: () => string;
38
+ export declare const buildProtocolDoc: (cliPolicy?: WorkflowCliPolicy, workflowsEnabled?: boolean, autostaffEnabled?: boolean) => string;