@tt-a1i/hive 1.4.4 → 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 (164) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +8 -0
  3. package/assets/qq-group.jpg +0 -0
  4. package/dist/bin/team.cmd +1 -0
  5. package/dist/src/cli/hive-update.d.ts +45 -17
  6. package/dist/src/cli/hive-update.js +63 -25
  7. package/dist/src/cli/hive.d.ts +25 -0
  8. package/dist/src/cli/hive.js +41 -3
  9. package/dist/src/cli/team.d.ts +1 -0
  10. package/dist/src/cli/team.js +199 -3
  11. package/dist/src/server/agent-command-resolver.js +3 -19
  12. package/dist/src/server/agent-manager-support.d.ts +2 -2
  13. package/dist/src/server/agent-manager-support.js +98 -24
  14. package/dist/src/server/agent-run-starter.d.ts +7 -1
  15. package/dist/src/server/agent-run-starter.js +9 -2
  16. package/dist/src/server/agent-run-store.d.ts +1 -1
  17. package/dist/src/server/agent-runtime-close.d.ts +1 -0
  18. package/dist/src/server/agent-runtime-close.js +25 -1
  19. package/dist/src/server/agent-runtime-contract.d.ts +2 -1
  20. package/dist/src/server/agent-runtime.d.ts +1 -1
  21. package/dist/src/server/agent-runtime.js +8 -2
  22. package/dist/src/server/agent-startup-instructions.d.ts +8 -1
  23. package/dist/src/server/agent-startup-instructions.js +15 -9
  24. package/dist/src/server/agent-stdin-dispatcher.d.ts +12 -5
  25. package/dist/src/server/agent-stdin-dispatcher.js +129 -40
  26. package/dist/src/server/cron-util.d.ts +7 -0
  27. package/dist/src/server/cron-util.js +19 -0
  28. package/dist/src/server/dispatch-ledger-store.d.ts +22 -0
  29. package/dist/src/server/dispatch-ledger-store.js +51 -3
  30. package/dist/src/server/env-sync-message.js +9 -9
  31. package/dist/src/server/fs-pick-folder.js +4 -0
  32. package/dist/src/server/fs-sandbox.js +36 -7
  33. package/dist/src/server/hive-team-guidance.d.ts +11 -6
  34. package/dist/src/server/hive-team-guidance.js +252 -71
  35. package/dist/src/server/live-run-registry.d.ts +1 -0
  36. package/dist/src/server/live-run-registry.js +1 -1
  37. package/dist/src/server/open-target-commands.js +5 -6
  38. package/dist/src/server/orchestrator-autostart.d.ts +12 -0
  39. package/dist/src/server/orchestrator-autostart.js +15 -13
  40. package/dist/src/server/path-canonicalization.d.ts +3 -0
  41. package/dist/src/server/path-canonicalization.js +29 -0
  42. package/dist/src/server/platform-path.d.ts +3 -0
  43. package/dist/src/server/platform-path.js +13 -0
  44. package/dist/src/server/post-start-input-writer.d.ts +1 -1
  45. package/dist/src/server/post-start-input-writer.js +110 -13
  46. package/dist/src/server/preset-launch-support.d.ts +1 -1
  47. package/dist/src/server/preset-launch-support.js +33 -2
  48. package/dist/src/server/recovery-summary.d.ts +6 -1
  49. package/dist/src/server/recovery-summary.js +17 -17
  50. package/dist/src/server/restart-policy-support.d.ts +6 -1
  51. package/dist/src/server/restart-policy-support.js +9 -1
  52. package/dist/src/server/restart-policy.d.ts +2 -2
  53. package/dist/src/server/restart-policy.js +3 -1
  54. package/dist/src/server/role-template-store.d.ts +1 -0
  55. package/dist/src/server/role-template-store.js +11 -1
  56. package/dist/src/server/route-types.d.ts +43 -0
  57. package/dist/src/server/routes-runtime.js +2 -1
  58. package/dist/src/server/routes-settings.js +76 -0
  59. package/dist/src/server/routes-team.js +211 -1
  60. package/dist/src/server/routes-workflow-schedules.d.ts +2 -0
  61. package/dist/src/server/routes-workflow-schedules.js +58 -0
  62. package/dist/src/server/routes-workflows.d.ts +2 -0
  63. package/dist/src/server/routes-workflows.js +83 -0
  64. package/dist/src/server/routes.js +4 -0
  65. package/dist/src/server/runtime-restart-policy.d.ts +3 -1
  66. package/dist/src/server/runtime-restart-policy.js +3 -1
  67. package/dist/src/server/runtime-store-contract.d.ts +122 -0
  68. package/dist/src/server/runtime-store-contract.js +1 -0
  69. package/dist/src/server/runtime-store-helpers.d.ts +9 -0
  70. package/dist/src/server/runtime-store-helpers.js +101 -2
  71. package/dist/src/server/runtime-store-workflows.d.ts +6 -0
  72. package/dist/src/server/runtime-store-workflows.js +100 -0
  73. package/dist/src/server/runtime-store.d.ts +3 -72
  74. package/dist/src/server/runtime-store.js +70 -4
  75. package/dist/src/server/session-capture-codex.d.ts +3 -3
  76. package/dist/src/server/session-capture-codex.js +9 -7
  77. package/dist/src/server/session-capture-gemini.d.ts +1 -1
  78. package/dist/src/server/session-capture-gemini.js +6 -3
  79. package/dist/src/server/settings-store.d.ts +3 -0
  80. package/dist/src/server/settings-store.js +1 -0
  81. package/dist/src/server/sqlite-schema-v19.d.ts +2 -0
  82. package/dist/src/server/sqlite-schema-v19.js +17 -0
  83. package/dist/src/server/sqlite-schema-v20.d.ts +2 -0
  84. package/dist/src/server/sqlite-schema-v20.js +20 -0
  85. package/dist/src/server/sqlite-schema-v21.d.ts +2 -0
  86. package/dist/src/server/sqlite-schema-v21.js +20 -0
  87. package/dist/src/server/sqlite-schema.d.ts +1 -1
  88. package/dist/src/server/sqlite-schema.js +97 -1
  89. package/dist/src/server/system-message.d.ts +7 -0
  90. package/dist/src/server/system-message.js +8 -1
  91. package/dist/src/server/tasks-file-watcher.d.ts +13 -1
  92. package/dist/src/server/tasks-file-watcher.js +127 -23
  93. package/dist/src/server/tasks-file.d.ts +2 -1
  94. package/dist/src/server/tasks-file.js +32 -9
  95. package/dist/src/server/tasks-websocket-server.js +13 -14
  96. package/dist/src/server/team-authz.d.ts +1 -1
  97. package/dist/src/server/team-authz.js +9 -1
  98. package/dist/src/server/team-autostaff.d.ts +16 -0
  99. package/dist/src/server/team-autostaff.js +16 -0
  100. package/dist/src/server/team-list-serializer.d.ts +1 -1
  101. package/dist/src/server/team-list-serializer.js +3 -1
  102. package/dist/src/server/team-operations.d.ts +15 -1
  103. package/dist/src/server/team-operations.js +116 -11
  104. package/dist/src/server/terminal-protocol.js +9 -3
  105. package/dist/src/server/terminal-stream-hub.js +16 -10
  106. package/dist/src/server/terminal-ws-server.js +10 -8
  107. package/dist/src/server/websocket-upgrade-safety.d.ts +10 -0
  108. package/dist/src/server/websocket-upgrade-safety.js +35 -0
  109. package/dist/src/server/windows-command-line.d.ts +3 -0
  110. package/dist/src/server/windows-command-line.js +9 -0
  111. package/dist/src/server/windows-filename.d.ts +2 -0
  112. package/dist/src/server/windows-filename.js +33 -0
  113. package/dist/src/server/workflow-cli-policy.d.ts +60 -0
  114. package/dist/src/server/workflow-cli-policy.js +110 -0
  115. package/dist/src/server/workflow-dispatch-awaiter.d.ts +12 -0
  116. package/dist/src/server/workflow-dispatch-awaiter.js +80 -0
  117. package/dist/src/server/workflow-feature.d.ts +15 -0
  118. package/dist/src/server/workflow-feature.js +15 -0
  119. package/dist/src/server/workflow-http-serializers.d.ts +64 -0
  120. package/dist/src/server/workflow-http-serializers.js +58 -0
  121. package/dist/src/server/workflow-run-log-store.d.ts +19 -0
  122. package/dist/src/server/workflow-run-log-store.js +45 -0
  123. package/dist/src/server/workflow-run-store.d.ts +50 -0
  124. package/dist/src/server/workflow-run-store.js +103 -0
  125. package/dist/src/server/workflow-runner.d.ts +147 -0
  126. package/dist/src/server/workflow-runner.js +401 -0
  127. package/dist/src/server/workflow-schedule-create.d.ts +14 -0
  128. package/dist/src/server/workflow-schedule-create.js +41 -0
  129. package/dist/src/server/workflow-schedule-store.d.ts +43 -0
  130. package/dist/src/server/workflow-schedule-store.js +112 -0
  131. package/dist/src/server/workflow-scheduler.d.ts +36 -0
  132. package/dist/src/server/workflow-scheduler.js +97 -0
  133. package/dist/src/server/workflow-script-loader.d.ts +34 -0
  134. package/dist/src/server/workflow-script-loader.js +106 -0
  135. package/dist/src/server/workspace-path-validation.js +16 -4
  136. package/dist/src/server/workspace-shell-runtime.d.ts +5 -0
  137. package/dist/src/server/workspace-shell-runtime.js +24 -2
  138. package/dist/src/server/workspace-store-contract.d.ts +4 -1
  139. package/dist/src/server/workspace-store-hydration.js +23 -7
  140. package/dist/src/server/workspace-store-mutations.js +2 -5
  141. package/dist/src/server/workspace-store-support.d.ts +4 -0
  142. package/dist/src/server/workspace-store-support.js +13 -1
  143. package/dist/src/server/workspace-store.js +38 -4
  144. package/dist/src/shared/types.d.ts +16 -1
  145. package/package.json +4 -2
  146. package/web/dist/assets/{AddWorkerDialog-DeZhTQLi.js → AddWorkerDialog-CcC-7kgG.js} +2 -2
  147. package/web/dist/assets/AddWorkspaceDialog-BDpOTfmt.js +1 -0
  148. package/web/dist/assets/{FirstRunWizard-B5wLcat5.js → FirstRunWizard-BYX_ocQn.js} +1 -1
  149. package/web/dist/assets/{MarketplaceDrawer-BC0eBOEW.js → MarketplaceDrawer-DUxSk7db.js} +1 -1
  150. package/web/dist/assets/WhatsNewDialog-B_RlCXcV.js +1 -0
  151. package/web/dist/assets/WorkerModal-D9-7YfZZ.js +1 -0
  152. package/web/dist/assets/WorkspaceTaskDrawer-BCKoF7qc.js +1 -0
  153. package/web/dist/assets/{WorkspaceTerminalPanels-CvibsPSd.js → WorkspaceTerminalPanels-Dq8y91t2.js} +1 -1
  154. package/web/dist/assets/index-BiOvKIVw.css +1 -0
  155. package/web/dist/assets/index-DMRUklT3.js +73 -0
  156. package/web/dist/assets/path-join-7MR1s7b1.js +1 -0
  157. package/web/dist/index.html +2 -2
  158. package/web/dist/sw.js +1 -1
  159. package/web/dist/assets/AddWorkspaceDialog-DDpXNEKf.js +0 -1
  160. package/web/dist/assets/WorkerModal-BwMHq-Bi.js +0 -1
  161. package/web/dist/assets/WorkspaceTaskDrawer-CxvT4nqs.js +0 -1
  162. package/web/dist/assets/index-BEsTmfrO.css +0 -1
  163. package/web/dist/assets/index-Ddb7bDN5.js +0 -75
  164. package/web/dist/assets/path-join-S7qkXQtP.js +0 -1
@@ -0,0 +1,20 @@
1
+ export const applySchemaVersion21 = (db) => {
2
+ db.exec(`
3
+ CREATE TABLE IF NOT EXISTS workflow_schedules (
4
+ id TEXT PRIMARY KEY,
5
+ workspace_id TEXT NOT NULL,
6
+ script_path TEXT NOT NULL,
7
+ cron TEXT NOT NULL,
8
+ args TEXT,
9
+ enabled INTEGER NOT NULL DEFAULT 1,
10
+ last_run_at INTEGER,
11
+ next_run_at INTEGER NOT NULL,
12
+ created_at INTEGER NOT NULL,
13
+ updated_at INTEGER NOT NULL
14
+ );
15
+ CREATE INDEX IF NOT EXISTS idx_workflow_schedules_due
16
+ ON workflow_schedules (enabled, next_run_at);
17
+ CREATE INDEX IF NOT EXISTS idx_workflow_schedules_workspace
18
+ ON workflow_schedules (workspace_id, created_at);
19
+ `);
20
+ };
@@ -1,3 +1,3 @@
1
1
  import type { Database } from 'better-sqlite3';
2
- export declare const CURRENT_SCHEMA_VERSION = 18;
2
+ export declare const CURRENT_SCHEMA_VERSION = 21;
3
3
  export declare const initializeRuntimeDatabase: (db: Database) => void;
@@ -11,7 +11,19 @@ import { applySchemaVersion15 } from './sqlite-schema-v15.js';
11
11
  import { applySchemaVersion16 } from './sqlite-schema-v16.js';
12
12
  import { applySchemaVersion17 } from './sqlite-schema-v17.js';
13
13
  import { applySchemaVersion18 } from './sqlite-schema-v18.js';
14
- export const CURRENT_SCHEMA_VERSION = 18;
14
+ import { applySchemaVersion19 } from './sqlite-schema-v19.js';
15
+ import { applySchemaVersion20 } from './sqlite-schema-v20.js';
16
+ import { applySchemaVersion21 } from './sqlite-schema-v21.js';
17
+ export const CURRENT_SCHEMA_VERSION = 21;
18
+ // Idempotent column-add helper. SQLite doesn't have `ALTER TABLE … ADD COLUMN
19
+ // IF NOT EXISTS`, so PRAGMA-check first. Safe to call on every init; required
20
+ // for foreign-built DBs where the version-gated migration is skipped.
21
+ const ensureColumn = (db, table, column, definition) => {
22
+ const present = new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((r) => r.name));
23
+ if (!present.has(column)) {
24
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
25
+ }
26
+ };
15
27
  export const initializeRuntimeDatabase = (db) => {
16
28
  db.exec(`
17
29
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -105,7 +117,79 @@ export const initializeRuntimeDatabase = (db) => {
105
117
 
106
118
  CREATE INDEX IF NOT EXISTS idx_dispatches_open_by_worker
107
119
  ON dispatches (workspace_id, to_agent_id, status, sequence);
120
+
121
+ CREATE TABLE IF NOT EXISTS workflow_runs (
122
+ id TEXT PRIMARY KEY,
123
+ workspace_id TEXT NOT NULL,
124
+ script_path TEXT NOT NULL,
125
+ script_hash TEXT,
126
+ name TEXT NOT NULL,
127
+ status TEXT NOT NULL,
128
+ phase TEXT,
129
+ args TEXT,
130
+ started_at INTEGER NOT NULL,
131
+ finished_at INTEGER,
132
+ error TEXT,
133
+ created_at INTEGER NOT NULL
134
+ );
135
+
136
+ CREATE INDEX IF NOT EXISTS idx_workflow_runs_workspace
137
+ ON workflow_runs (workspace_id, created_at);
138
+
139
+ CREATE TABLE IF NOT EXISTS workflow_schedules (
140
+ id TEXT PRIMARY KEY,
141
+ workspace_id TEXT NOT NULL,
142
+ script_path TEXT NOT NULL,
143
+ cron TEXT NOT NULL,
144
+ args TEXT,
145
+ enabled INTEGER NOT NULL DEFAULT 1,
146
+ last_run_at INTEGER,
147
+ next_run_at INTEGER NOT NULL,
148
+ created_at INTEGER NOT NULL,
149
+ updated_at INTEGER NOT NULL
150
+ );
151
+
152
+ CREATE INDEX IF NOT EXISTS idx_workflow_schedules_due
153
+ ON workflow_schedules (enabled, next_run_at);
154
+ CREATE INDEX IF NOT EXISTS idx_workflow_schedules_workspace
155
+ ON workflow_schedules (workspace_id, created_at);
108
156
  `);
157
+ // Idempotent column additions — run on every init regardless of
158
+ // schema_version. The v19 migration adds these columns but is gated on
159
+ // !appliedVersions.has(19); a foreign-built DB whose v19 was a DIFFERENT
160
+ // migration leaves these absent. PRAGMA + ALTER here makes the writes safe.
161
+ ensureColumn(db, 'workers', 'ephemeral', 'INTEGER NOT NULL DEFAULT 0');
162
+ ensureColumn(db, 'workers', 'spawned_by', 'TEXT');
163
+ ensureColumn(db, 'dispatches', 'workflow_run_id', 'TEXT');
164
+ ensureColumn(db, 'dispatches', 'step_index', 'INTEGER');
165
+ // M9 — phase + label on dispatches so the workflow UI can render the
166
+ // phase tree + agent fleet view (mirrors Claude Code's /workflows view).
167
+ ensureColumn(db, 'dispatches', 'phase', 'TEXT');
168
+ ensureColumn(db, 'dispatches', 'label', 'TEXT');
169
+ // M10: capture the workflow script's `return` value so the UI can render
170
+ // a single canonical "Result" panel and the orchestrator notification can
171
+ // include it.
172
+ ensureColumn(db, 'workflow_runs', 'result', 'TEXT');
173
+ // TIER 2 #5: parent_run_id lets the Drawer render nested workflow() calls
174
+ // as a tree (child runs indented under their parent). Without it,
175
+ // nested runs were flat and the user couldn't tell which child
176
+ // belonged to which parent. Null on top-level runs.
177
+ ensureColumn(db, 'workflow_runs', 'parent_run_id', 'TEXT');
178
+ // TIER 2 #3: log() narrator pipeline. Authors call `log('Discovered 47
179
+ // endpoints')` from a workflow script; rows live here and stream to
180
+ // the Drawer's narrator lane + the last few lines are appended to
181
+ // the orchestrator's completion notification.
182
+ db.exec(`
183
+ CREATE TABLE IF NOT EXISTS workflow_run_logs (
184
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
185
+ run_id TEXT NOT NULL,
186
+ ts INTEGER NOT NULL,
187
+ message TEXT NOT NULL
188
+ );
189
+ CREATE INDEX IF NOT EXISTS idx_workflow_run_logs_run
190
+ ON workflow_run_logs (run_id, id);
191
+ `);
192
+ db.exec('CREATE INDEX IF NOT EXISTS idx_dispatches_workflow ON dispatches (workflow_run_id, step_index)');
109
193
  const versions = db
110
194
  .prepare('SELECT version FROM schema_version ORDER BY version ASC')
111
195
  .all();
@@ -203,4 +287,16 @@ export const initializeRuntimeDatabase = (db) => {
203
287
  applySchemaVersion18(db);
204
288
  db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(18, Date.now());
205
289
  }
290
+ if (!appliedVersions.has(19)) {
291
+ applySchemaVersion19(db);
292
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(19, Date.now());
293
+ }
294
+ if (!appliedVersions.has(20)) {
295
+ applySchemaVersion20(db);
296
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(20, Date.now());
297
+ }
298
+ if (!appliedVersions.has(21)) {
299
+ applySchemaVersion21(db);
300
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(21, Date.now());
301
+ }
206
302
  };
@@ -1 +1,8 @@
1
+ /**
2
+ * Wraps a system-injected message body in an out-of-band XML envelope so the
3
+ * agent attends to it as system content rather than mistaking it for user
4
+ * input or its own output. `<hive-system-reminder>` is reserved for the short
5
+ * re-anchoring action menu appended at a message tail; this `<hive-system-message>`
6
+ * tag carries larger injected bodies (crash-recovery / restart env-sync).
7
+ */
1
8
  export declare const wrapSystemMessage: (content: string) => string;
@@ -1 +1,8 @@
1
- export const wrapSystemMessage = (content) => `[Hive 系统消息:${content}]`;
1
+ /**
2
+ * Wraps a system-injected message body in an out-of-band XML envelope so the
3
+ * agent attends to it as system content rather than mistaking it for user
4
+ * input or its own output. `<hive-system-reminder>` is reserved for the short
5
+ * re-anchoring action menu appended at a message tail; this `<hive-system-message>`
6
+ * tag carries larger injected bodies (crash-recovery / restart env-sync).
7
+ */
8
+ export const wrapSystemMessage = (content) => `<hive-system-message>\n${content}\n</hive-system-message>`;
@@ -1,4 +1,5 @@
1
1
  import { type ChokidarOptions } from 'chokidar';
2
+ import type { WorkflowCliPolicy } from './workflow-cli-policy.js';
2
3
  /**
3
4
  * Watcher configuration. The atomic-save option matters on Windows: VS
4
5
  * Code, Cursor, Notepad++, and the editor inside Hive itself all save
@@ -24,11 +25,22 @@ import { type ChokidarOptions } from 'chokidar';
24
25
  * Exported so the configuration is testable in isolation.
25
26
  */
26
27
  export declare const TASKS_WATCHER_OPTIONS: ChokidarOptions;
28
+ export declare const buildTasksWatcherOptions: (workspacePath: string, platform?: NodeJS.Platform) => ChokidarOptions;
27
29
  export interface TasksFileWatcher {
28
30
  close: () => Promise<void>;
29
31
  start: (workspaceId: string, workspacePath: string) => Promise<void>;
30
32
  stop: (workspaceId: string) => Promise<void>;
31
33
  }
32
- export declare const createTasksFileWatcher: ({ onTasksUpdated, }: {
34
+ export declare const createTasksFileWatcher: ({ onTasksUpdated, getWorkflowCliPolicy, getWorkflowsEnabled, getAutostaffEnabled, }: {
33
35
  onTasksUpdated: (workspaceId: string, content: string) => void;
36
+ /** Lets the freshly-written `.hive/PROTOCOL.md` state the workspace's
37
+ * workflow CLI default + allowlist. Optional: omitted → the doc renders
38
+ * the unrestricted default. */
39
+ getWorkflowCliPolicy?: () => WorkflowCliPolicy;
40
+ /** Whether the experimental workflow feature is on. Off → PROTOCOL.md omits
41
+ * the workflow DSL + `team workflow` commands entirely. Defaults to off. */
42
+ getWorkflowsEnabled?: () => boolean;
43
+ /** Whether the experimental auto-staff feature is on (default on). Off →
44
+ * PROTOCOL.md omits the team-sizing rule. */
45
+ getAutostaffEnabled?: () => boolean;
34
46
  }) => TasksFileWatcher;
@@ -1,8 +1,12 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { readFile } from 'node:fs/promises';
3
+ import { basename, dirname, normalize } from 'node:path';
3
4
  import chokidar from 'chokidar';
4
- import { ensureProtocolFile, ensureTasksFile, getTasksFilePath } from './tasks-file.js';
5
+ import { ensureProtocolFile, ensureTasksFile, getTasksFilePath, TASKS_FILE_NAME, } from './tasks-file.js';
5
6
  const DEBOUNCE_MS = 100;
7
+ const WATCHER_RETRY_MS = 5000;
8
+ const WATCHER_CLOSE_TIMEOUT_MS = 2000;
9
+ const WATCHER_READY_TIMEOUT_MS = 15000;
6
10
  /**
7
11
  * Watcher configuration. The atomic-save option matters on Windows: VS
8
12
  * Code, Cursor, Notepad++, and the editor inside Hive itself all save
@@ -31,9 +35,45 @@ export const TASKS_WATCHER_OPTIONS = {
31
35
  atomic: 100,
32
36
  ignoreInitial: true,
33
37
  };
34
- export const createTasksFileWatcher = ({ onTasksUpdated, }) => {
38
+ const isWindowsUncPath = (path, platform = process.platform) => platform === 'win32' && /^[\\/]{2}[^\\/]+[\\/]+[^\\/]+/u.test(path);
39
+ export const buildTasksWatcherOptions = (workspacePath, platform = process.platform) => ({
40
+ ...TASKS_WATCHER_OPTIONS,
41
+ ...(platform === 'win32' || isWindowsUncPath(workspacePath, platform)
42
+ ? { interval: 500, usePolling: true }
43
+ : {}),
44
+ });
45
+ const closeWatcherWithTimeout = async (watcher) => {
46
+ if (!watcher)
47
+ return;
48
+ let timer;
49
+ try {
50
+ await Promise.race([
51
+ watcher.close(),
52
+ new Promise((resolve) => {
53
+ timer = setTimeout(resolve, WATCHER_CLOSE_TIMEOUT_MS);
54
+ timer.unref?.();
55
+ }),
56
+ ]);
57
+ }
58
+ finally {
59
+ if (timer)
60
+ clearTimeout(timer);
61
+ }
62
+ };
63
+ const isTasksFileEvent = (tasksPath, changedPath) => {
64
+ if (!changedPath)
65
+ return true;
66
+ const text = Buffer.isBuffer(changedPath) ? changedPath.toString() : changedPath;
67
+ return normalize(text) === normalize(tasksPath) || basename(text) === TASKS_FILE_NAME;
68
+ };
69
+ export const createTasksFileWatcher = ({ onTasksUpdated, getWorkflowCliPolicy, getWorkflowsEnabled, getAutostaffEnabled, }) => {
35
70
  const watchers = new Map();
36
71
  const timers = new Map();
72
+ const retryTimers = new Map();
73
+ let closed = false;
74
+ const logWatcherError = (workspaceId, error) => {
75
+ console.error(`[hive] tasks watcher error for workspace ${workspaceId}`, error);
76
+ };
37
77
  const clearTimer = (workspaceId) => {
38
78
  const timer = timers.get(workspaceId);
39
79
  if (!timer)
@@ -41,6 +81,13 @@ export const createTasksFileWatcher = ({ onTasksUpdated, }) => {
41
81
  clearTimeout(timer);
42
82
  timers.delete(workspaceId);
43
83
  };
84
+ const clearRetryTimer = (workspaceId) => {
85
+ const timer = retryTimers.get(workspaceId);
86
+ if (!timer)
87
+ return;
88
+ clearTimeout(timer);
89
+ retryTimers.delete(workspaceId);
90
+ };
44
91
  const emitCurrentContent = async (workspaceId, workspacePath) => {
45
92
  const tasksPath = getTasksFilePath(workspacePath);
46
93
  try {
@@ -48,39 +95,96 @@ export const createTasksFileWatcher = ({ onTasksUpdated, }) => {
48
95
  onTasksUpdated(workspaceId, content);
49
96
  }
50
97
  catch (error) {
51
- if (error.code !== 'ENOENT')
52
- throw error;
98
+ if (error.code !== 'ENOENT') {
99
+ logWatcherError(workspaceId, error);
100
+ return;
101
+ }
53
102
  onTasksUpdated(workspaceId, '');
54
103
  }
55
104
  };
56
105
  const stop = async (workspaceId) => {
57
106
  clearTimer(workspaceId);
107
+ clearRetryTimer(workspaceId);
58
108
  const watcher = watchers.get(workspaceId);
59
109
  watchers.delete(workspaceId);
60
- await watcher?.close();
110
+ await closeWatcherWithTimeout(watcher);
111
+ };
112
+ const scheduleRetry = (workspaceId, workspacePath) => {
113
+ if (closed || retryTimers.has(workspaceId))
114
+ return;
115
+ const timer = setTimeout(() => {
116
+ retryTimers.delete(workspaceId);
117
+ void start(workspaceId, workspacePath).catch((error) => logWatcherError(workspaceId, error));
118
+ }, WATCHER_RETRY_MS);
119
+ timer.unref?.();
120
+ retryTimers.set(workspaceId, timer);
121
+ };
122
+ const waitForReady = async (watcher) => await new Promise((resolve, reject) => {
123
+ const cleanup = () => {
124
+ watcher.off('ready', handleReady);
125
+ watcher.off('error', handleError);
126
+ clearTimeout(timeout);
127
+ };
128
+ const handleReady = () => {
129
+ cleanup();
130
+ resolve();
131
+ };
132
+ const handleError = (error) => {
133
+ cleanup();
134
+ reject(error);
135
+ };
136
+ const timeout = setTimeout(() => {
137
+ cleanup();
138
+ reject(new Error(`Timed out waiting for tasks watcher ready after ${WATCHER_READY_TIMEOUT_MS}ms`));
139
+ }, WATCHER_READY_TIMEOUT_MS);
140
+ timeout.unref?.();
141
+ watcher.once('ready', handleReady);
142
+ watcher.once('error', handleError);
143
+ });
144
+ const start = async (workspaceId, workspacePath) => {
145
+ closed = false;
146
+ await stop(workspaceId);
147
+ ensureTasksFile(workspacePath);
148
+ ensureProtocolFile(workspacePath, getWorkflowCliPolicy?.(), getWorkflowsEnabled?.() ?? false, getAutostaffEnabled?.() ?? false);
149
+ const tasksPath = getTasksFilePath(workspacePath);
150
+ const watcher = chokidar.watch(dirname(tasksPath), buildTasksWatcherOptions(workspacePath));
151
+ const scheduleEmit = (changedPath) => {
152
+ if (!isTasksFileEvent(tasksPath, changedPath))
153
+ return;
154
+ clearTimer(workspaceId);
155
+ timers.set(workspaceId, setTimeout(() => {
156
+ timers.delete(workspaceId);
157
+ void emitCurrentContent(workspaceId, workspacePath);
158
+ }, DEBOUNCE_MS));
159
+ };
160
+ watcher.on('add', scheduleEmit);
161
+ watcher.on('change', scheduleEmit);
162
+ watcher.on('unlink', scheduleEmit);
163
+ watcher.on('error', (error) => {
164
+ logWatcherError(workspaceId, error);
165
+ void stop(workspaceId)
166
+ .catch((closeError) => logWatcherError(workspaceId, closeError))
167
+ .finally(() => scheduleRetry(workspaceId, workspacePath));
168
+ });
169
+ watchers.set(workspaceId, watcher);
170
+ try {
171
+ await waitForReady(watcher);
172
+ }
173
+ catch (error) {
174
+ watchers.delete(workspaceId);
175
+ await closeWatcherWithTimeout(watcher);
176
+ scheduleRetry(workspaceId, workspacePath);
177
+ throw error;
178
+ }
61
179
  };
62
180
  return {
63
181
  close: async () => {
182
+ closed = true;
183
+ for (const workspaceId of retryTimers.keys())
184
+ clearRetryTimer(workspaceId);
64
185
  await Promise.all(Array.from(watchers.keys(), (workspaceId) => stop(workspaceId)));
65
186
  },
66
- start: async (workspaceId, workspacePath) => {
67
- await stop(workspaceId);
68
- ensureTasksFile(workspacePath);
69
- ensureProtocolFile(workspacePath);
70
- const watcher = chokidar.watch(getTasksFilePath(workspacePath), TASKS_WATCHER_OPTIONS);
71
- const scheduleEmit = () => {
72
- clearTimer(workspaceId);
73
- timers.set(workspaceId, setTimeout(() => {
74
- timers.delete(workspaceId);
75
- void emitCurrentContent(workspaceId, workspacePath);
76
- }, DEBOUNCE_MS));
77
- };
78
- watcher.on('add', scheduleEmit);
79
- watcher.on('change', scheduleEmit);
80
- watcher.on('unlink', scheduleEmit);
81
- watchers.set(workspaceId, watcher);
82
- await new Promise((resolve) => watcher.once('ready', () => resolve()));
83
- },
187
+ start,
84
188
  stop,
85
189
  };
86
190
  };
@@ -1,3 +1,4 @@
1
+ import type { WorkflowCliPolicy } from './workflow-cli-policy.js';
1
2
  interface TasksFileService {
2
3
  readTasks: (workspacePath: string) => string;
3
4
  writeTasks: (workspacePath: string, content: string) => void;
@@ -16,6 +17,6 @@ export declare const ensureTasksFile: (workspacePath: string) => string;
16
17
  * on every workspace open means a Hive version bump that changes the rules
17
18
  * propagates without manual intervention.
18
19
  */
19
- export declare const ensureProtocolFile: (workspacePath: string) => string;
20
+ export declare const ensureProtocolFile: (workspacePath: string, cliPolicy?: WorkflowCliPolicy, workflowsEnabled?: boolean, autostaffEnabled?: boolean) => string;
20
21
  export declare const createTasksFileService: () => TasksFileService;
21
22
  export type { TasksFileService };
@@ -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
  });