@tt-a1i/hive 1.5.0 → 1.7.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 (82) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.en.md +23 -1
  3. package/README.md +10 -1
  4. package/dist/src/cli/team.js +19 -2
  5. package/dist/src/server/agent-run-bootstrap.d.ts +6 -1
  6. package/dist/src/server/agent-run-bootstrap.js +5 -2
  7. package/dist/src/server/agent-run-starter.d.ts +6 -7
  8. package/dist/src/server/agent-run-starter.js +18 -4
  9. package/dist/src/server/agent-runtime-contract.d.ts +10 -0
  10. package/dist/src/server/agent-runtime-stop-run.d.ts +1 -1
  11. package/dist/src/server/agent-runtime-stop-run.js +4 -1
  12. package/dist/src/server/agent-runtime.d.ts +2 -1
  13. package/dist/src/server/agent-runtime.js +10 -5
  14. package/dist/src/server/agent-startup-instructions.d.ts +7 -8
  15. package/dist/src/server/agent-startup-instructions.js +6 -4
  16. package/dist/src/server/agent-stdin-dispatcher.d.ts +20 -7
  17. package/dist/src/server/agent-stdin-dispatcher.js +22 -10
  18. package/dist/src/server/command-preset-defaults.js +12 -0
  19. package/dist/src/server/feature-flags.d.ts +42 -0
  20. package/dist/src/server/feature-flags.js +24 -0
  21. package/dist/src/server/hive-team-guidance.d.ts +4 -3
  22. package/dist/src/server/hive-team-guidance.js +17 -16
  23. package/dist/src/server/post-start-input-writer.js +2 -2
  24. package/dist/src/server/preset-launch-support.js +2 -1
  25. package/dist/src/server/recovery-summary.d.ts +5 -6
  26. package/dist/src/server/recovery-summary.js +3 -2
  27. package/dist/src/server/report-outbox-store.d.ts +36 -0
  28. package/dist/src/server/report-outbox-store.js +33 -0
  29. package/dist/src/server/restart-policy-support.d.ts +4 -5
  30. package/dist/src/server/restart-policy.d.ts +5 -1
  31. package/dist/src/server/restart-policy.js +51 -33
  32. package/dist/src/server/routes-settings.js +3 -3
  33. package/dist/src/server/routes-tasks.js +23 -0
  34. package/dist/src/server/routes-workspaces.js +5 -0
  35. package/dist/src/server/runtime-restart-policy.d.ts +3 -3
  36. package/dist/src/server/runtime-restart-policy.js +2 -3
  37. package/dist/src/server/runtime-store-contract.d.ts +3 -0
  38. package/dist/src/server/runtime-store-helpers.d.ts +2 -0
  39. package/dist/src/server/runtime-store-helpers.js +14 -9
  40. package/dist/src/server/runtime-store-workflows.js +8 -0
  41. package/dist/src/server/runtime-store.js +1 -0
  42. package/dist/src/server/session-capture.d.ts +6 -0
  43. package/dist/src/server/session-capture.js +32 -0
  44. package/dist/src/server/sqlite-schema-v22.d.ts +2 -0
  45. package/dist/src/server/sqlite-schema-v22.js +27 -0
  46. package/dist/src/server/sqlite-schema.d.ts +1 -1
  47. package/dist/src/server/sqlite-schema.js +19 -1
  48. package/dist/src/server/task-deps.d.ts +32 -0
  49. package/dist/src/server/task-deps.js +40 -0
  50. package/dist/src/server/tasks-file-watcher.d.ts +6 -7
  51. package/dist/src/server/tasks-file-watcher.js +3 -2
  52. package/dist/src/server/tasks-file.d.ts +2 -1
  53. package/dist/src/server/tasks-file.js +3 -2
  54. package/dist/src/server/team-authz.d.ts +1 -1
  55. package/dist/src/server/team-authz.js +1 -0
  56. package/dist/src/server/team-operations.d.ts +7 -1
  57. package/dist/src/server/team-operations.js +81 -19
  58. package/dist/src/server/webhook-notifier.d.ts +34 -0
  59. package/dist/src/server/webhook-notifier.js +47 -0
  60. package/dist/src/server/workflow-cli-policy.d.ts +1 -1
  61. package/dist/src/server/workflow-cli-policy.js +1 -1
  62. package/dist/src/server/workflow-output-schema.d.ts +18 -0
  63. package/dist/src/server/workflow-output-schema.js +41 -0
  64. package/dist/src/server/workflow-runner.js +12 -2
  65. package/dist/src/shared/types.d.ts +2 -2
  66. package/package.json +1 -1
  67. package/web/dist/assets/{AddWorkerDialog-CcC-7kgG.js → AddWorkerDialog-BRUxpa3f.js} +2 -2
  68. package/web/dist/assets/{AddWorkspaceDialog-BDpOTfmt.js → AddWorkspaceDialog-D56x5JCb.js} +1 -1
  69. package/web/dist/assets/{FirstRunWizard-BYX_ocQn.js → FirstRunWizard-BFVaMIsE.js} +1 -1
  70. package/web/dist/assets/{MarketplaceDrawer-DUxSk7db.js → MarketplaceDrawer-DeEZ35dN.js} +1 -1
  71. package/web/dist/assets/{WhatsNewDialog-B_RlCXcV.js → WhatsNewDialog-CHkZeINH.js} +1 -1
  72. package/web/dist/assets/{WorkerModal-D9-7YfZZ.js → WorkerModal-BBCuMLIa.js} +1 -1
  73. package/web/dist/assets/{WorkspaceTaskDrawer-BCKoF7qc.js → WorkspaceTaskDrawer-CpZHAcj1.js} +1 -1
  74. package/web/dist/assets/WorkspaceTerminalPanels-7If2mDyp.js +1 -0
  75. package/web/dist/assets/index-5zh61jMg.css +1 -0
  76. package/web/dist/assets/index-CxNL0O-C.js +73 -0
  77. package/web/dist/cli-icons/hermes.png +0 -0
  78. package/web/dist/index.html +2 -2
  79. package/web/dist/sw.js +1 -1
  80. package/web/dist/assets/WorkspaceTerminalPanels-Dq8y91t2.js +0 -1
  81. package/web/dist/assets/index-BiOvKIVw.css +0 -1
  82. package/web/dist/assets/index-DMRUklT3.js +0 -73
@@ -2,21 +2,22 @@ import { createAgentRunStore } from './agent-run-store.js';
2
2
  import { createAgentRuntime } from './agent-runtime.js';
3
3
  import { createAgentSessionStore } from './agent-session-store.js';
4
4
  import { createDispatchLedgerStore } from './dispatch-ledger-store.js';
5
+ import { readFeatureFlags } from './feature-flags.js';
5
6
  import { createMessageLogStore } from './message-log-store.js';
6
7
  import { seedOrchestratorLaunchConfig } from './orchestrator-launch.js';
8
+ import { createReportOutboxStore } from './report-outbox-store.js';
7
9
  import { openRuntimeDatabase } from './runtime-database.js';
8
10
  import { buildRuntimeRestartPolicy } from './runtime-restart-policy.js';
9
11
  import { createSettingsStore } from './settings-store.js';
10
12
  import { createTasksFileService } from './tasks-file.js';
11
13
  import { createTasksFileWatcher } from './tasks-file-watcher.js';
12
- import { AUTOSTAFF_ENABLED_KEY, readAutostaffEnabled } from './team-autostaff.js';
13
14
  import { createTeamOperations } from './team-operations.js';
14
15
  import { resolveTerminalInputProfile } from './terminal-input-profile.js';
15
16
  import { createUiAuth } from './ui-auth.js';
17
+ import { createWebhookNotifier, WEBHOOK_URL_KEY } from './webhook-notifier.js';
16
18
  import { createWorkerOutputTracker } from './worker-output-tracker.js';
17
19
  import { readWorkflowCliPolicy, WORKFLOW_CLI_POLICY_KEY } from './workflow-cli-policy.js';
18
20
  import { createWorkflowDispatchAwaiter, } from './workflow-dispatch-awaiter.js';
19
- import { readWorkflowEnabled, WORKFLOW_ENABLED_KEY } from './workflow-feature.js';
20
21
  import { createWorkflowRunLogStore } from './workflow-run-log-store.js';
21
22
  import { createWorkflowRunStore } from './workflow-run-store.js';
22
23
  import { createWorkflowScheduleStore } from './workflow-schedule-store.js';
@@ -35,6 +36,7 @@ export const createRuntimeStoreServices = (options = {}) => {
35
36
  const db = openRuntimeDatabase(options.dataDir);
36
37
  const messageLogStore = createMessageLogStore(db);
37
38
  const dispatchLedgerStore = createDispatchLedgerStore(db);
39
+ const reportOutbox = createReportOutboxStore(db);
38
40
  const workflowDispatchAwaiter = createWorkflowDispatchAwaiter();
39
41
  const workflowRunStore = createWorkflowRunStore(db);
40
42
  const workflowRunLogStore = createWorkflowRunLogStore(db);
@@ -42,8 +44,10 @@ export const createRuntimeStoreServices = (options = {}) => {
42
44
  const agentRunStore = createAgentRunStore(db);
43
45
  const agentSessionStore = createAgentSessionStore(db);
44
46
  const settings = createSettingsStore(db);
45
- const getWorkflowsEnabled = () => readWorkflowEnabled(settings.getAppState(WORKFLOW_ENABLED_KEY)?.value ?? null);
46
- const getAutostaffEnabled = () => readAutostaffEnabled(settings.getAppState(AUTOSTAFF_ENABLED_KEY)?.value ?? null);
47
+ const getFlags = () => readFeatureFlags(settings);
48
+ const webhookNotifier = createWebhookNotifier({
49
+ getUrl: () => settings.getAppState(WEBHOOK_URL_KEY)?.value ?? null,
50
+ });
47
51
  const tasksFileService = createTasksFileService();
48
52
  const tasksFileWatchCallbacks = new Set();
49
53
  const tasksFileWatcher = createTasksFileWatcher({
@@ -51,8 +55,7 @@ export const createRuntimeStoreServices = (options = {}) => {
51
55
  notifyTasksUpdated(tasksFileWatchCallbacks, workspaceId, content);
52
56
  },
53
57
  getWorkflowCliPolicy: () => readWorkflowCliPolicy(settings.getAppState(WORKFLOW_CLI_POLICY_KEY)?.value ?? null),
54
- getWorkflowsEnabled,
55
- getAutostaffEnabled,
58
+ getFlags,
56
59
  });
57
60
  const uiAuth = createUiAuth();
58
61
  const shellRuntime = createWorkspaceShellRuntime(options.agentManager);
@@ -71,8 +74,7 @@ export const createRuntimeStoreServices = (options = {}) => {
71
74
  messageLogStore,
72
75
  tasksFileService,
73
76
  workspaceStore,
74
- getWorkflowsEnabled,
75
- getAutostaffEnabled,
77
+ getFlags,
76
78
  });
77
79
  const workerOutputTracker = options.agentManager
78
80
  ? createWorkerOutputTracker(options.agentManager.getOutputBus())
@@ -119,7 +121,7 @@ export const createRuntimeStoreServices = (options = {}) => {
119
121
  for (const child of children)
120
122
  removeWorkerCompletely(workspaceId, child.id);
121
123
  }
122
- }, restartPolicy, (workspaceId, agentId) => workspaceStore.getAgent(workspaceId, agentId), getWorkflowsEnabled, getAutostaffEnabled);
124
+ }, restartPolicy, (workspaceId, agentId) => workspaceStore.getAgent(workspaceId, agentId), getFlags);
123
125
  // Mirrors runtime-store.deleteWorker (stop run → drop launch config → drop
124
126
  // dispatches → drop worker row, transactionally). Hoisted `function` so the
125
127
  // onAgentExit closure above can reference it; only invoked at runtime, after
@@ -157,6 +159,8 @@ export const createRuntimeStoreServices = (options = {}) => {
157
159
  markDispatchCancelled: dispatchLedgerStore.markCancelled,
158
160
  markDispatchReportedByWorker: dispatchLedgerStore.markReportedByWorker,
159
161
  markDispatchSubmitted: dispatchLedgerStore.markSubmitted,
162
+ reportOutbox,
163
+ notifyWebhook: webhookNotifier.notify,
160
164
  workflowDispatchAwaiter,
161
165
  workspaceStore,
162
166
  dismissEphemeralWorker: (workspaceId, workerId) => removeWorkerCompletely(workspaceId, workerId),
@@ -176,6 +180,7 @@ export const createRuntimeStoreServices = (options = {}) => {
176
180
  tasksFileService,
177
181
  teamOps,
178
182
  uiAuth,
183
+ webhookNotifier,
179
184
  workerOutputTracker,
180
185
  workflowDispatchAwaiter,
181
186
  workflowRunLogStore,
@@ -78,6 +78,14 @@ export const createRuntimeStoreWorkflowRuntime = (services, store) => {
78
78
  catch (error) {
79
79
  console.error('[hive] workflow.notifyTrigger failed', error);
80
80
  }
81
+ // Outbound completion webhook (best-effort) so the user is pinged when a
82
+ // long fan-out finishes without watching the drawer.
83
+ services.webhookNotifier.notify({
84
+ type: 'workflow_finished',
85
+ workspaceId,
86
+ summary: `${finalRecord.name}: ${finalRecord.status}${finalRecord.error ? ` — ${finalRecord.error}` : ''}`,
87
+ at: Date.now(),
88
+ });
81
89
  },
82
90
  store: {
83
91
  addWorkerWithLaunch: (ws, input, launch) => store.addWorkerWithLaunch(ws, input, launch),
@@ -93,6 +93,7 @@ export const createRuntimeStore = (options = {}) => {
93
93
  dispatchTask: services.teamOps.dispatchTask,
94
94
  dispatchTaskByWorkerName: services.teamOps.dispatchTaskByWorkerName,
95
95
  reportTask: services.teamOps.reportTask,
96
+ drainReportOutbox: services.teamOps.drainReportOutbox,
96
97
  statusTask: services.teamOps.statusTask,
97
98
  listDispatches: services.dispatchLedgerStore.listWorkspaceDispatches,
98
99
  listWorkers: (workspaceId) => services.workspaceStore.listWorkers(workspaceId),
@@ -21,6 +21,7 @@ export interface SessionCaptureSnapshot {
21
21
  discriminator?: {
22
22
  contentIncludes: string | readonly string[];
23
23
  };
24
+ getOutput?: () => string | null;
24
25
  knownSessionIds: Set<string>;
25
26
  env?: Record<string, string>;
26
27
  root?: string;
@@ -65,7 +66,12 @@ export declare const snapshotSessionIdsForCapture: (cwd: string, capture: Sessio
65
66
  };
66
67
  knownSessionIds: Set<string>;
67
68
  root: string;
69
+ } | {
70
+ knownSessionIds: Set<string>;
71
+ env?: never;
72
+ root?: never;
68
73
  } | undefined;
69
74
  export declare const getSessionCaptureEnvironment: (snapshot: SessionCaptureSnapshot | undefined) => Record<string, string>;
70
75
  export declare const captureSessionIdForCapture: (cwd: string, capture: SessionIdCaptureConfig, snapshot: SessionCaptureSnapshot, onCapture: (sessionId: string) => void, timeoutMs?: number, intervalMs?: number) => Promise<void>;
71
76
  export declare const doesCapturedSessionExist: (cwd: string, capture: SessionIdCaptureConfig, sessionId: string, discriminator?: SessionCaptureSnapshot["discriminator"]) => boolean;
77
+ export declare const captureStdoutRegexSessionId: (pattern: string, getOutput: (() => string | null) | undefined, knownSessionIds: Set<string>, onCapture: (sessionId: string) => void, timeoutMs?: number, intervalMs?: number) => Promise<void>;
@@ -57,6 +57,11 @@ export const snapshotSessionIdsForCapture = (cwd, capture, discriminator) => {
57
57
  root: dbPath,
58
58
  };
59
59
  }
60
+ if (capture.source === 'stdout_regex') {
61
+ return {
62
+ knownSessionIds: new Set(),
63
+ };
64
+ }
60
65
  return undefined;
61
66
  };
62
67
  export const getSessionCaptureEnvironment = (snapshot) => snapshot?.env ?? {};
@@ -73,6 +78,9 @@ export const captureSessionIdForCapture = async (cwd, capture, snapshot, onCaptu
73
78
  if (capture.source === 'opencode_session_db') {
74
79
  await captureOpenCodeSessionId(cwd, snapshot.knownSessionIds, onCapture, timeoutMs, intervalMs, snapshot.root);
75
80
  }
81
+ if (capture.source === 'stdout_regex') {
82
+ await captureStdoutRegexSessionId(capture.pattern, snapshot.getOutput, snapshot.knownSessionIds, onCapture, timeoutMs, intervalMs);
83
+ }
76
84
  };
77
85
  export const doesCapturedSessionExist = (cwd, capture, sessionId, discriminator) => {
78
86
  if (capture.source === 'claude_project_jsonl_dir') {
@@ -89,3 +97,27 @@ export const doesCapturedSessionExist = (cwd, capture, sessionId, discriminator)
89
97
  }
90
98
  return false;
91
99
  };
100
+ const compileCaptureRegex = (pattern) => {
101
+ try {
102
+ return new RegExp(pattern, 'u');
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ };
108
+ export const captureStdoutRegexSessionId = async (pattern, getOutput, knownSessionIds, onCapture, timeoutMs = 5000, intervalMs = 100) => {
109
+ const regex = compileCaptureRegex(pattern);
110
+ if (!regex || !getOutput)
111
+ return;
112
+ const deadline = Date.now() + timeoutMs;
113
+ while (Date.now() <= deadline) {
114
+ const output = getOutput();
115
+ const match = output ? regex.exec(output) : null;
116
+ const sessionId = match?.[1];
117
+ if (sessionId && !knownSessionIds.has(sessionId)) {
118
+ onCapture(sessionId);
119
+ return;
120
+ }
121
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
122
+ }
123
+ };
@@ -0,0 +1,2 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ export declare const applySchemaVersion22: (db: Database) => void;
@@ -0,0 +1,27 @@
1
+ const HERMES_SESSION_ID_CAPTURE = {
2
+ pattern: String.raw `Session:\s*([A-Za-z0-9_-]+)`,
3
+ source: 'stdout_regex',
4
+ };
5
+ const HERMES_YOLO_ARGS = ['--yolo'];
6
+ export const applySchemaVersion22 = (db) => {
7
+ const hasCommandPresets = db
8
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'command_presets'")
9
+ .get();
10
+ if (!hasCommandPresets)
11
+ return;
12
+ const now = Date.now();
13
+ db.prepare(`INSERT INTO command_presets (
14
+ id, display_name, command, args, env, resume_args_template, session_id_capture,
15
+ yolo_args_template, is_builtin, created_at, updated_at
16
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
17
+ ON CONFLICT(id) DO UPDATE SET
18
+ display_name = excluded.display_name,
19
+ command = excluded.command,
20
+ args = excluded.args,
21
+ env = excluded.env,
22
+ resume_args_template = excluded.resume_args_template,
23
+ session_id_capture = excluded.session_id_capture,
24
+ yolo_args_template = excluded.yolo_args_template,
25
+ updated_at = excluded.updated_at
26
+ WHERE command_presets.is_builtin = 1`).run('hermes', 'Hermes', 'hermes', '[]', '{}', '--resume {session_id}', JSON.stringify(HERMES_SESSION_ID_CAPTURE), JSON.stringify(HERMES_YOLO_ARGS), now, now);
27
+ };
@@ -1,3 +1,3 @@
1
1
  import type { Database } from 'better-sqlite3';
2
- export declare const CURRENT_SCHEMA_VERSION = 21;
2
+ export declare const CURRENT_SCHEMA_VERSION = 22;
3
3
  export declare const initializeRuntimeDatabase: (db: Database) => void;
@@ -14,7 +14,8 @@ import { applySchemaVersion18 } from './sqlite-schema-v18.js';
14
14
  import { applySchemaVersion19 } from './sqlite-schema-v19.js';
15
15
  import { applySchemaVersion20 } from './sqlite-schema-v20.js';
16
16
  import { applySchemaVersion21 } from './sqlite-schema-v21.js';
17
- export const CURRENT_SCHEMA_VERSION = 21;
17
+ import { applySchemaVersion22 } from './sqlite-schema-v22.js';
18
+ export const CURRENT_SCHEMA_VERSION = 22;
18
19
  // Idempotent column-add helper. SQLite doesn't have `ALTER TABLE … ADD COLUMN
19
20
  // IF NOT EXISTS`, so PRAGMA-check first. Safe to call on every init; required
20
21
  // for foreign-built DBs where the version-gated migration is skipped.
@@ -118,6 +119,19 @@ export const initializeRuntimeDatabase = (db) => {
118
119
  CREATE INDEX IF NOT EXISTS idx_dispatches_open_by_worker
119
120
  ON dispatches (workspace_id, to_agent_id, status, sequence);
120
121
 
122
+ CREATE TABLE IF NOT EXISTS report_outbox (
123
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
124
+ workspace_id TEXT NOT NULL,
125
+ target_agent_id TEXT NOT NULL,
126
+ dispatch_id TEXT NOT NULL UNIQUE,
127
+ payload TEXT NOT NULL,
128
+ created_at INTEGER NOT NULL,
129
+ delivered_at INTEGER
130
+ );
131
+
132
+ CREATE INDEX IF NOT EXISTS idx_report_outbox_pending
133
+ ON report_outbox (workspace_id, target_agent_id, delivered_at, created_at);
134
+
121
135
  CREATE TABLE IF NOT EXISTS workflow_runs (
122
136
  id TEXT PRIMARY KEY,
123
137
  workspace_id TEXT NOT NULL,
@@ -299,4 +313,8 @@ export const initializeRuntimeDatabase = (db) => {
299
313
  applySchemaVersion21(db);
300
314
  db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(21, Date.now());
301
315
  }
316
+ if (!appliedVersions.has(22)) {
317
+ applySchemaVersion22(db);
318
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(22, Date.now());
319
+ }
302
320
  };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Optional, read-only task-dependency support for `.hive/tasks.md`.
3
+ *
4
+ * A task line may carry a trailing `[needs: #2, #5]` annotation declaring that
5
+ * it depends on other tasks (referenced by their 1-based position among the
6
+ * GFM task-list items). `team next` uses this to surface the tasks that are
7
+ * currently runnable — not done, and with every dependency already checked off.
8
+ *
9
+ * Deliberately scoped: parsing happens here on the server only and is purely a
10
+ * READ — Hive never auto-dispatches a runnable task and never writes the
11
+ * annotation back into tasks.md (that file stays a human/orchestrator-edited,
12
+ * git-mergeable artifact). The web task renderer is intentionally left
13
+ * untouched, so the annotation just shows as literal text in the UI.
14
+ */
15
+ export interface ParsedTaskLine {
16
+ /** 1-based position among task-list items — the `#n` that `[needs:]` uses. */
17
+ index: number;
18
+ done: boolean;
19
+ text: string;
20
+ needs: number[];
21
+ }
22
+ export interface RunnableTask {
23
+ index: number;
24
+ text: string;
25
+ }
26
+ export declare const parseTasksWithDeps: (markdown: string) => ParsedTaskLine[];
27
+ /**
28
+ * Tasks that can be worked right now: not yet done, and every `[needs:]`
29
+ * dependency is a task that exists AND is checked off. A dependency on a
30
+ * non-existent `#n`, or on a task that isn't done, withholds the task.
31
+ */
32
+ export declare const computeRunnableTasks: (markdown: string) => RunnableTask[];
@@ -0,0 +1,40 @@
1
+ const TASK_LINE = /^\s*[-*]\s+\[([ xX])\]\s+(.*)$/;
2
+ const NEEDS = /\[needs:\s*([#\d,\s]+)\]\s*$/i;
3
+ export const parseTasksWithDeps = (markdown) => {
4
+ const tasks = [];
5
+ let index = 0;
6
+ for (const line of markdown.split('\n')) {
7
+ const match = TASK_LINE.exec(line);
8
+ if (!match)
9
+ continue;
10
+ index += 1;
11
+ const done = (match[1] ?? ' ').toLowerCase() === 'x';
12
+ let text = (match[2] ?? '').trim();
13
+ const needs = [];
14
+ const needsMatch = NEEDS.exec(text);
15
+ if (needsMatch) {
16
+ for (const token of (needsMatch[1] ?? '').split(',')) {
17
+ const parsed = Number.parseInt(token.replace('#', '').trim(), 10);
18
+ if (Number.isInteger(parsed) && parsed > 0)
19
+ needs.push(parsed);
20
+ }
21
+ // Drop the trailing annotation from the human-readable text.
22
+ text = text.slice(0, needsMatch.index).trim();
23
+ }
24
+ tasks.push({ index, done, text, needs });
25
+ }
26
+ return tasks;
27
+ };
28
+ /**
29
+ * Tasks that can be worked right now: not yet done, and every `[needs:]`
30
+ * dependency is a task that exists AND is checked off. A dependency on a
31
+ * non-existent `#n`, or on a task that isn't done, withholds the task.
32
+ */
33
+ export const computeRunnableTasks = (markdown) => {
34
+ const tasks = parseTasksWithDeps(markdown);
35
+ const doneByIndex = new Map(tasks.map((task) => [task.index, task.done]));
36
+ return tasks
37
+ .filter((task) => !task.done)
38
+ .filter((task) => task.needs.every((dep) => doneByIndex.get(dep) === true))
39
+ .map((task) => ({ index: task.index, text: task.text }));
40
+ };
@@ -1,4 +1,5 @@
1
1
  import { type ChokidarOptions } from 'chokidar';
2
+ import { type FeatureFlags } from './feature-flags.js';
2
3
  import type { WorkflowCliPolicy } from './workflow-cli-policy.js';
3
4
  /**
4
5
  * Watcher configuration. The atomic-save option matters on Windows: VS
@@ -31,16 +32,14 @@ export interface TasksFileWatcher {
31
32
  start: (workspaceId: string, workspacePath: string) => Promise<void>;
32
33
  stop: (workspaceId: string) => Promise<void>;
33
34
  }
34
- export declare const createTasksFileWatcher: ({ onTasksUpdated, getWorkflowCliPolicy, getWorkflowsEnabled, getAutostaffEnabled, }: {
35
+ export declare const createTasksFileWatcher: ({ onTasksUpdated, getWorkflowCliPolicy, getFlags, }: {
35
36
  onTasksUpdated: (workspaceId: string, content: string) => void;
36
37
  /** Lets the freshly-written `.hive/PROTOCOL.md` state the workspace's
37
38
  * workflow CLI default + allowlist. Optional: omitted → the doc renders
38
39
  * the unrestricted default. */
39
40
  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;
41
+ /** Resolves the live experimental flags. PROTOCOL.md omits the workflow DSL
42
+ * + `team workflow` commands when `workflowsEnabled` is off, and the
43
+ * team-sizing rule when `autostaffEnabled` is off. Omitted → all off. */
44
+ getFlags?: () => FeatureFlags;
46
45
  }) => TasksFileWatcher;
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { basename, dirname, normalize } from 'node:path';
4
4
  import chokidar from 'chokidar';
5
+ import { FEATURE_FLAGS_ALL_OFF } from './feature-flags.js';
5
6
  import { ensureProtocolFile, ensureTasksFile, getTasksFilePath, TASKS_FILE_NAME, } from './tasks-file.js';
6
7
  const DEBOUNCE_MS = 100;
7
8
  const WATCHER_RETRY_MS = 5000;
@@ -66,7 +67,7 @@ const isTasksFileEvent = (tasksPath, changedPath) => {
66
67
  const text = Buffer.isBuffer(changedPath) ? changedPath.toString() : changedPath;
67
68
  return normalize(text) === normalize(tasksPath) || basename(text) === TASKS_FILE_NAME;
68
69
  };
69
- export const createTasksFileWatcher = ({ onTasksUpdated, getWorkflowCliPolicy, getWorkflowsEnabled, getAutostaffEnabled, }) => {
70
+ export const createTasksFileWatcher = ({ onTasksUpdated, getWorkflowCliPolicy, getFlags, }) => {
70
71
  const watchers = new Map();
71
72
  const timers = new Map();
72
73
  const retryTimers = new Map();
@@ -145,7 +146,7 @@ export const createTasksFileWatcher = ({ onTasksUpdated, getWorkflowCliPolicy, g
145
146
  closed = false;
146
147
  await stop(workspaceId);
147
148
  ensureTasksFile(workspacePath);
148
- ensureProtocolFile(workspacePath, getWorkflowCliPolicy?.(), getWorkflowsEnabled?.() ?? false, getAutostaffEnabled?.() ?? false);
149
+ ensureProtocolFile(workspacePath, getWorkflowCliPolicy?.(), getFlags?.() ?? FEATURE_FLAGS_ALL_OFF);
149
150
  const tasksPath = getTasksFilePath(workspacePath);
150
151
  const watcher = chokidar.watch(dirname(tasksPath), buildTasksWatcherOptions(workspacePath));
151
152
  const scheduleEmit = (changedPath) => {
@@ -1,3 +1,4 @@
1
+ import { type FeatureFlags } from './feature-flags.js';
1
2
  import type { WorkflowCliPolicy } from './workflow-cli-policy.js';
2
3
  interface TasksFileService {
3
4
  readTasks: (workspacePath: string) => string;
@@ -17,6 +18,6 @@ export declare const ensureTasksFile: (workspacePath: string) => string;
17
18
  * on every workspace open means a Hive version bump that changes the rules
18
19
  * propagates without manual intervention.
19
20
  */
20
- export declare const ensureProtocolFile: (workspacePath: string, cliPolicy?: WorkflowCliPolicy, workflowsEnabled?: boolean, autostaffEnabled?: boolean) => string;
21
+ export declare const ensureProtocolFile: (workspacePath: string, cliPolicy?: WorkflowCliPolicy, flags?: FeatureFlags) => string;
21
22
  export declare const createTasksFileService: () => TasksFileService;
22
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';
@@ -50,10 +51,10 @@ export const ensureTasksFile = (workspacePath) => {
50
51
  * on every workspace open means a Hive version bump that changes the rules
51
52
  * propagates without manual intervention.
52
53
  */
53
- export const ensureProtocolFile = (workspacePath, cliPolicy, workflowsEnabled = false, autostaffEnabled = false) => {
54
+ export const ensureProtocolFile = (workspacePath, cliPolicy, flags = FEATURE_FLAGS_ALL_OFF) => {
54
55
  ensureTasksDir(workspacePath);
55
56
  const protocolFilePath = getProtocolFilePath(workspacePath);
56
- const desired = buildProtocolDoc(cliPolicy, workflowsEnabled, autostaffEnabled);
57
+ const desired = buildProtocolDoc(cliPolicy, flags);
57
58
  const current = existsSync(protocolFilePath)
58
59
  ? runRetryableTasksFileOperation(() => readFileSync(protocolFilePath, 'utf8'))
59
60
  : null;
@@ -1,5 +1,5 @@
1
1
  import type { AgentSummary } from '../shared/types.js';
2
- export type TeamCommand = 'send' | 'list' | 'report' | 'status' | 'cancel' | 'help' | 'spawn' | 'dismiss' | 'workflow';
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;
@@ -2,6 +2,7 @@ import { ForbiddenError, UnauthorizedError } from './http-errors.js';
2
2
  const ORCHESTRATOR_COMMANDS = new Set([
3
3
  'send',
4
4
  'list',
5
+ 'next',
5
6
  'cancel',
6
7
  'help',
7
8
  'spawn',
@@ -2,6 +2,8 @@ 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';
5
7
  import type { WorkflowDispatchAwaiter } from './workflow-dispatch-awaiter.js';
6
8
  import type { WorkspaceStore } from './workspace-store.js';
7
9
  export declare const formatUnknownWorkerError: (workerName: string, roster: readonly TeamListItem[]) => string;
@@ -35,6 +37,9 @@ export interface TeamOperationsInput {
35
37
  workspaceId: string;
36
38
  }) => DispatchRecord | undefined;
37
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;
38
43
  workflowDispatchAwaiter: WorkflowDispatchAwaiter;
39
44
  workspaceStore: WorkspaceStore;
40
45
  /** Auto-dismiss an ephemeral orchestrator-spawned worker after its
@@ -71,13 +76,14 @@ export interface ReportTaskResult {
71
76
  forwardError: string | null;
72
77
  forwarded: boolean;
73
78
  }
74
- export declare const createTeamOperations: ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, workflowDispatchAwaiter, workspaceStore, dismissEphemeralWorker, }: TeamOperationsInput) => {
79
+ export declare const createTeamOperations: ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, reportOutbox, notifyWebhook, workflowDispatchAwaiter, workspaceStore, dismissEphemeralWorker, }: TeamOperationsInput) => {
75
80
  cancelTask(workspaceId: string, dispatchId: string, input: CancelTaskInput): {
76
81
  dispatch: DispatchRecord;
77
82
  forwardError: string | null;
78
83
  forwarded: boolean;
79
84
  };
80
85
  dispatchTask: (workspaceId: string, workerId: string, text: string, input?: DispatchTaskInput) => Promise<DispatchRecord>;
86
+ drainReportOutbox: (workspaceId: string) => void;
81
87
  dispatchTaskByWorkerName(workspaceId: string, workerName: string, text: string, input?: DispatchTaskInput): Promise<DispatchRecord & {
82
88
  restartedWorker: boolean;
83
89
  }>;
@@ -1,4 +1,5 @@
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';
3
4
  import { getWorkflowAgentId } from './workspace-store-support.js';
4
5
  /* Roster snapshot embedded in the 409 the orchestrator sees when it
@@ -24,7 +25,27 @@ export const formatUnknownWorkerError = (workerName, roster) => {
24
25
  ].join('\n');
25
26
  };
26
27
  const reportForwardErrorMessage = (error) => error instanceof Error ? error.message : String(error);
27
- export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, workflowDispatchAwaiter, workspaceStore, dismissEphemeralWorker, }) => {
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
+ };
28
49
  const ensureWorkerRun = async (workspaceId, workerId, hivePort) => {
29
50
  if (agentRuntime.getActiveRunByAgentId(workspaceId, workerId)) {
30
51
  return;
@@ -171,6 +192,7 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
171
192
  return { dispatch, forwardError, forwarded };
172
193
  },
173
194
  dispatchTask,
195
+ drainReportOutbox,
174
196
  async dispatchTaskByWorkerName(workspaceId, workerName, text, input = {}) {
175
197
  /* Build the roster once so a missing-name path can surface it without
176
198
  a second store call. We deliberately don't go through
@@ -235,15 +257,6 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
235
257
  if (!openDispatch) {
236
258
  throw new ConflictError(`No open dispatch for worker: ${worker.name}`);
237
259
  }
238
- const isWorkflowDispatch = openDispatch.fromAgentId === getWorkflowAgentId(workspaceId);
239
- // Pre-check the orchestrator PTY only when the report is heading there.
240
- // Workflow-sourced dispatches don't need an orchestrator: the runner
241
- // is in-process and will resolve its awaiter directly.
242
- if (input.requireActiveRun === true && !isWorkflowDispatch) {
243
- if (!agentRuntime.getActiveRunByAgentId(workspaceId, `${workspaceId}:orchestrator`)) {
244
- throw new PtyInactiveError(`No active run for agent: ${workspaceId}:orchestrator`);
245
- }
246
- }
247
260
  const messageHandle = insertMessage(createReportMessage(workspaceId, workerId, text, status, artifacts));
248
261
  try {
249
262
  const dispatch = markDispatchReportedByWorker({
@@ -277,16 +290,65 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
277
290
  }
278
291
  return { dispatch, forwardError, forwarded };
279
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
+ });
280
303
  if (input.requireActiveRun === true) {
281
- try {
282
- agentRuntime.writeReportPrompt(workspaceId, worker.name, workerId, text, artifacts, {
283
- requireActiveRun: input.requireActiveRun,
284
- });
285
- forwarded = 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
+ }
286
340
  }
287
- catch (error) {
288
- forwardError = reportForwardErrorMessage(error);
289
- console.error('[hive] swallowed:teamReport.forward', error);
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
+ });
290
352
  }
291
353
  }
292
354
  // M11: if this worker was spawned with `team spawn --ephemeral`, this