@tt-a1i/hive 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.en.md +21 -0
  3. package/README.md +8 -0
  4. package/dist/src/cli/team.js +17 -0
  5. package/dist/src/server/agent-run-starter.d.ts +6 -7
  6. package/dist/src/server/agent-run-starter.js +3 -3
  7. package/dist/src/server/agent-runtime-contract.d.ts +10 -0
  8. package/dist/src/server/agent-runtime-stop-run.d.ts +1 -1
  9. package/dist/src/server/agent-runtime-stop-run.js +4 -1
  10. package/dist/src/server/agent-runtime.d.ts +2 -1
  11. package/dist/src/server/agent-runtime.js +10 -5
  12. package/dist/src/server/agent-startup-instructions.d.ts +7 -8
  13. package/dist/src/server/agent-startup-instructions.js +5 -3
  14. package/dist/src/server/agent-stdin-dispatcher.d.ts +20 -7
  15. package/dist/src/server/agent-stdin-dispatcher.js +22 -10
  16. package/dist/src/server/feature-flags.d.ts +42 -0
  17. package/dist/src/server/feature-flags.js +24 -0
  18. package/dist/src/server/hive-team-guidance.d.ts +4 -3
  19. package/dist/src/server/hive-team-guidance.js +14 -13
  20. package/dist/src/server/recovery-summary.d.ts +5 -6
  21. package/dist/src/server/recovery-summary.js +3 -2
  22. package/dist/src/server/report-outbox-store.d.ts +36 -0
  23. package/dist/src/server/report-outbox-store.js +33 -0
  24. package/dist/src/server/restart-policy-support.d.ts +4 -5
  25. package/dist/src/server/restart-policy.d.ts +5 -1
  26. package/dist/src/server/restart-policy.js +51 -33
  27. package/dist/src/server/routes-settings.js +3 -3
  28. package/dist/src/server/routes-tasks.js +23 -0
  29. package/dist/src/server/routes-workspaces.js +5 -0
  30. package/dist/src/server/runtime-restart-policy.d.ts +3 -3
  31. package/dist/src/server/runtime-restart-policy.js +2 -3
  32. package/dist/src/server/runtime-store-contract.d.ts +3 -0
  33. package/dist/src/server/runtime-store-helpers.d.ts +2 -0
  34. package/dist/src/server/runtime-store-helpers.js +14 -9
  35. package/dist/src/server/runtime-store-workflows.js +8 -0
  36. package/dist/src/server/runtime-store.js +1 -0
  37. package/dist/src/server/sqlite-schema.js +13 -0
  38. package/dist/src/server/task-deps.d.ts +32 -0
  39. package/dist/src/server/task-deps.js +40 -0
  40. package/dist/src/server/tasks-file-watcher.d.ts +6 -7
  41. package/dist/src/server/tasks-file-watcher.js +3 -2
  42. package/dist/src/server/tasks-file.d.ts +2 -1
  43. package/dist/src/server/tasks-file.js +3 -2
  44. package/dist/src/server/team-authz.d.ts +1 -1
  45. package/dist/src/server/team-authz.js +1 -0
  46. package/dist/src/server/team-operations.d.ts +7 -1
  47. package/dist/src/server/team-operations.js +81 -19
  48. package/dist/src/server/webhook-notifier.d.ts +34 -0
  49. package/dist/src/server/webhook-notifier.js +47 -0
  50. package/dist/src/server/workflow-output-schema.d.ts +18 -0
  51. package/dist/src/server/workflow-output-schema.js +41 -0
  52. package/dist/src/server/workflow-runner.js +11 -1
  53. package/package.json +1 -1
  54. package/web/dist/assets/{AddWorkerDialog-CcC-7kgG.js → AddWorkerDialog-CGbaxu0T.js} +2 -2
  55. package/web/dist/assets/{AddWorkspaceDialog-BDpOTfmt.js → AddWorkspaceDialog-CNgExu6b.js} +1 -1
  56. package/web/dist/assets/{FirstRunWizard-BYX_ocQn.js → FirstRunWizard-DxGApUNc.js} +1 -1
  57. package/web/dist/assets/{MarketplaceDrawer-DUxSk7db.js → MarketplaceDrawer-Bk6cpukn.js} +1 -1
  58. package/web/dist/assets/{WhatsNewDialog-B_RlCXcV.js → WhatsNewDialog-CSGzk-2U.js} +1 -1
  59. package/web/dist/assets/{WorkerModal-D9-7YfZZ.js → WorkerModal-i2F3n3nZ.js} +1 -1
  60. package/web/dist/assets/{WorkspaceTaskDrawer-BCKoF7qc.js → WorkspaceTaskDrawer-C_Ta_K13.js} +1 -1
  61. package/web/dist/assets/WorkspaceTerminalPanels-VdDxtrQF.js +1 -0
  62. package/web/dist/assets/index-5zh61jMg.css +1 -0
  63. package/web/dist/assets/index-CAgGM6nb.js +75 -0
  64. package/web/dist/index.html +2 -2
  65. package/web/dist/sw.js +1 -1
  66. package/web/dist/assets/WorkspaceTerminalPanels-Dq8y91t2.js +0 -1
  67. package/web/dist/assets/index-BiOvKIVw.css +0 -1
  68. package/web/dist/assets/index-DMRUklT3.js +0 -73
@@ -1,3 +1,4 @@
1
+ import { FEATURE_FLAGS_ALL_OFF } from './feature-flags.js';
1
2
  import { DEFAULT_WORKFLOW_CLI_POLICY } from './workflow-cli-policy.js';
2
3
  /**
3
4
  * Tail reminder appended to every message that flows INTO the orchestrator
@@ -16,7 +17,7 @@ import { DEFAULT_WORKFLOW_CLI_POLICY } from './workflow-cli-policy.js';
16
17
  * the workflow DSL live in `.hive/PROTOCOL.md`, which agents re-read on demand.
17
18
  * A long banner on every turn is itself the noise this envelope exists to beat.
18
19
  */
19
- export const buildOrchestratorReminderTail = (workflowsEnabled) => {
20
+ export const buildOrchestratorReminderTail = ({ workflowsEnabled }) => {
20
21
  const body = 'You are the Hive Orchestrator. Reply with one of: ' +
21
22
  '(a) `team send "<worker-name>" "<task>"` to dispatch — run `team list` first if the roster may have changed since your last view (Hive does not push membership changes; stale names fail). ' +
22
23
  'If no worker fits or the roster is empty, `team spawn <role> [--cli claude|codex|opencode|gemini]` to create one (add `--ephemeral` for a one-shot), then dispatch; do not ask the user to add workers. ' +
@@ -85,7 +86,7 @@ const buildAutostaffRule = (workflowsEnabled) => 'You may size the team to the t
85
86
  (workflowsEnabled
86
87
  ? ' When the work is a one-shot fan-out across 3+ workers or a staged review→fix that runs to completion, prefer `team workflow run` (barrier, phase tree, stop button) over a batch of `team spawn`; reserve up-front staffing for a standing team you direct interactively across several turns.'
87
88
  : '');
88
- const orchestratorRules = (workflowsEnabled, autostaffEnabled) => [
89
+ const orchestratorRules = ({ workflowsEnabled, autostaffEnabled, }) => [
89
90
  ...CORE_ORCHESTRATOR_RULES,
90
91
  ...(workflowsEnabled ? WORKFLOW_ORCHESTRATOR_RULES : []),
91
92
  ...(autostaffEnabled ? [buildAutostaffRule(workflowsEnabled)] : []),
@@ -98,9 +99,7 @@ const WORKER_RULES = [
98
99
  '`team --help` only prints command syntax — it is NOT a way to report; its output never reaches the Orchestrator. You still owe a real `team report` / `team status` afterward.',
99
100
  'If `team report` / `team status` errors, it also prints USAGE — fix the arguments per USAGE and retry; do not use `team --help` as a stand-in for reporting.',
100
101
  ];
101
- export const getHiveTeamRules = (agent, workflowsEnabled = false, autostaffEnabled = false) => agent.role === 'orchestrator'
102
- ? orchestratorRules(workflowsEnabled, autostaffEnabled)
103
- : WORKER_RULES;
102
+ export const getHiveTeamRules = (agent, flags = FEATURE_FLAGS_ALL_OFF) => (agent.role === 'orchestrator' ? orchestratorRules(flags) : WORKER_RULES);
104
103
  const renderRules = (rules) => rules.map((line) => `- ${line}`).join('\n');
105
104
  /**
106
105
  * The workflow DSL teaching: agent()/parallel()/pipeline() semantics, the
@@ -122,7 +121,7 @@ Host functions injected into the script: \`agent(prompt, opts)\`,
122
121
  \`parallel(thunks)\`, \`pipeline(items, ...stages)\`, \`phase(title)\`,
123
122
  \`log(msg)\`, plus the \`args\` global.
124
123
 
125
- \`agent(prompt, opts)\` opts: \`{ agentType?: "coder"|"reviewer"|"tester"|"custom"|<custom-role-name>, cli?: "claude"|"codex"|"opencode"|"gemini", model?: string, label?: string, timeoutMs?: number }\` — other fields are silently ignored. \`agentType\` also accepts the name of a workspace custom role template (case-insensitive); a typo throws rather than silently falling back to coder. \`model\` is passed through to the worker launch config (\`--model <id>\`), so a 100-way fan-out can use a cheap model and the synthesizer a strong one. The worker auto-dismisses when its \`agent()\` call resolves — you never dismiss it.
124
+ \`agent(prompt, opts)\` opts: \`{ agentType?: "coder"|"reviewer"|"tester"|"custom"|<custom-role-name>, cli?: "claude"|"codex"|"opencode"|"gemini", model?: string, outputSchema?: object, label?: string, timeoutMs?: number }\` — other fields are silently ignored. \`agentType\` also accepts the name of a workspace custom role template (case-insensitive); a typo throws rather than silently falling back to coder. \`model\` is passed through to the worker launch config (\`--model <id>\`), so a 100-way fan-out can use a cheap model and the synthesizer a strong one. \`outputSchema\` makes \`agent()\` resolve to a parsed object instead of a string: the worker is told to end its report with a fenced \`\`\`json block whose keys match the schema. On a parse miss it falls back to \`{ text: "<raw report>" }\`, so ALWAYS treat a missing field as the SAFE default (never assume success). The worker auto-dismisses when its \`agent()\` call resolves — you never dismiss it.
126
125
 
127
126
  \`parallel()\` takes an array of THUNKS (\`() => agent(...)\`), NOT already-started promises: \`parallel([agent(...), agent(...)])\` degrades to unordered concurrency counted as a single step (a no-op grouping), because the promises already started at construction time. \`pipeline(items, ...stages)\` stages are also functions, shape \`(prev, item, i) => agent(...)\`.
128
127
 
@@ -176,15 +175,16 @@ while (dry < 2) {
176
175
  }
177
176
  return confirmed
178
177
  \`\`\`
179
- - **judge panel** — N independent verifiers per finding; majority confirms. Catches plausible-but-wrong findings:
178
+ - **judge panel** — N independent verifiers per finding; majority confirms. \`outputSchema\` makes each vote a typed object instead of fragile yes/no string-matching. Note the safe default: a parse miss has no \`refuted\` key, so \`v.refuted === false\` is false → the finding stays refuted and a malformed reply can never flip it to "confirmed":
180
179
  \`\`\`
181
180
  phase('Verify')
182
181
  const votes = await parallel([
183
- () => agent(\`Try to REFUTE: \${claim}. Default refuted=true if uncertain. Reply yes/no.\`),
184
- () => agent(\`Independently judge: \${claim}. Reply yes/no.\`),
185
- () => agent(\`Repro test: \${claim}. Did you confirm? yes/no.\`),
182
+ () => agent(\`Try to REFUTE: \${claim}. Default refuted=true if uncertain.\`, { outputSchema: { refuted: 'boolean' } }),
183
+ () => agent(\`Independently judge: \${claim}.\`, { outputSchema: { refuted: 'boolean' } }),
184
+ () => agent(\`Repro test: \${claim}. Did it reproduce?\`, { outputSchema: { refuted: 'boolean' } }),
186
185
  ])
187
- const real = votes.filter((v) => v?.toLowerCase().startsWith('y')).length >= 2
186
+ // Only an explicit refuted:false counts as "survived"; missing/unparseable stays safely refuted.
187
+ const real = votes.filter((v) => v && v.refuted === false).length >= 2
188
188
  \`\`\`
189
189
  - **pipeline** — multi-item, multi-stage, no barrier between stages (item A can be in stage 3 while item B is in stage 1); wall-clock = slowest single item, not sum-of-slowest-per-stage. Each stage gets \`(prevResult, originalItem, index)\`; a stage that throws drops that item to null and skips its remaining stages.
190
190
 
@@ -198,7 +198,8 @@ On completion Hive injects \`<hive-system-reminder>Hive workflow ... finished: s
198
198
  * syntax and the workflow DSL reference — the always-on injections only carry
199
199
  * the lean core rules and point here.
200
200
  */
201
- export const buildProtocolDoc = (cliPolicy = DEFAULT_WORKFLOW_CLI_POLICY, workflowsEnabled = false, autostaffEnabled = false) => {
201
+ export const buildProtocolDoc = (cliPolicy = DEFAULT_WORKFLOW_CLI_POLICY, flags = FEATURE_FLAGS_ALL_OFF) => {
202
+ const { workflowsEnabled } = flags;
202
203
  // The `team workflow …` subcommands + the DSL reference + the per-workspace
203
204
  // CLI-policy section only appear when the experimental workflow feature is
204
205
  // ON. While off, the orchestrator's canonical reference never mentions
@@ -266,7 +267,7 @@ export const buildProtocolDoc = (cliPolicy = DEFAULT_WORKFLOW_CLI_POLICY, workfl
266
267
  '',
267
268
  '## Orchestrator rules',
268
269
  '',
269
- renderRules(orchestratorRules(workflowsEnabled, autostaffEnabled)),
270
+ renderRules(orchestratorRules(flags)),
270
271
  '',
271
272
  '## Worker rules',
272
273
  '',
@@ -1,15 +1,14 @@
1
1
  import type { AgentSummary, WorkspaceSummary } from '../shared/types.js';
2
+ import { type FeatureFlags } from './feature-flags.js';
2
3
  import type { RecoveryMessage } from './message-log-store.js';
3
- export declare const buildRecoverySummary: ({ agent, allTaskMessages, messages, tasksContent, workers, workspace, workflowsEnabled, autostaffEnabled, }: {
4
+ export declare const buildRecoverySummary: ({ agent, allTaskMessages, messages, tasksContent, workers, workspace, flags, }: {
4
5
  agent: AgentSummary;
5
6
  allTaskMessages?: RecoveryMessage[];
6
7
  messages: RecoveryMessage[];
7
8
  tasksContent: string;
8
9
  workers: AgentSummary[];
9
10
  workspace: WorkspaceSummary;
10
- /** Experimental workflow gatekeeps the recovered handover prompt
11
- * consistent with the live flag (omits the workflow rule when off). */
12
- workflowsEnabled?: boolean;
13
- /** Experimental auto-staff gate (default on) — same, for the team-sizing rule. */
14
- autostaffEnabled?: boolean;
11
+ /** Live experimental flagskeep the recovered handover prompt consistent
12
+ * with what a fresh startup would inject (workflow + team-sizing rules). */
13
+ flags?: FeatureFlags;
15
14
  }) => string;
@@ -1,3 +1,4 @@
1
+ import { FEATURE_FLAGS_ALL_OFF } from './feature-flags.js';
1
2
  import { getHiveTeamRules } from './hive-team-guidance.js';
2
3
  import { wrapSystemMessage } from './system-message.js';
3
4
  import { TASKS_RELATIVE_PATH } from './tasks-file.js';
@@ -64,7 +65,7 @@ const formatWorkers = (workers) => {
64
65
  return workers.map((worker) => `- ${worker.name} (${worker.role}, ${worker.status}, pending_task_count: ${worker.pendingTaskCount})`);
65
66
  };
66
67
  const getTaskSectionTitle = (agent) => agent.role === 'orchestrator' ? '## Tasks you dispatched' : '## Tasks recently sent to you';
67
- export const buildRecoverySummary = ({ agent, allTaskMessages, messages, tasksContent, workers, workspace, workflowsEnabled = false, autostaffEnabled = false, }) => wrapSystemMessage([
68
+ export const buildRecoverySummary = ({ agent, allTaskMessages, messages, tasksContent, workers, workspace, flags = FEATURE_FLAGS_ALL_OFF, }) => wrapSystemMessage([
68
69
  `You are ${agent.name} (${agent.role}) in workspace ${workspace.name}.`,
69
70
  'Hive just restarted you and could not recover via native session resume. Here is the handover context.',
70
71
  '',
@@ -84,7 +85,7 @@ export const buildRecoverySummary = ({ agent, allTaskMessages, messages, tasksCo
84
85
  ...formatWorkers(workers),
85
86
  '',
86
87
  agent.role === 'orchestrator' ? '## Hive worker dispatch rules' : '## Hive worker boundaries',
87
- ...getHiveTeamRules(agent, workflowsEnabled, autostaffEnabled),
88
+ ...getHiveTeamRules(agent, flags),
88
89
  '',
89
90
  'Continue from here. If unsure, ask the user.',
90
91
  ].join('\n'));
@@ -0,0 +1,36 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ /**
3
+ * Durable redelivery queue for worker reports the orchestrator could not
4
+ * receive. `team report` persists the dispatch as reported and forwards the
5
+ * report into the orchestrator's stdin best-effort; if that write fails (the
6
+ * PTY exited / is mid-restart, or the orchestrator was down at report time)
7
+ * the report would otherwise be lost — the ledger says "reported" yet the
8
+ * orchestrator waits forever, indistinguishable from a hang.
9
+ *
10
+ * Entries are keyed by dispatch id (UNIQUE) so a report enqueues at most once,
11
+ * and drain marks them delivered only after the orchestrator PTY write
12
+ * actually resolves, so a failed redelivery stays pending for the next drain.
13
+ */
14
+ export interface ReportOutboxEntry {
15
+ id: number;
16
+ workspaceId: string;
17
+ targetAgentId: string;
18
+ dispatchId: string;
19
+ payload: string;
20
+ createdAt: number;
21
+ deliveredAt: number | null;
22
+ }
23
+ interface EnqueueInput {
24
+ workspaceId: string;
25
+ targetAgentId: string;
26
+ dispatchId: string;
27
+ payload: string;
28
+ }
29
+ export declare const createReportOutboxStore: (db: Database) => {
30
+ enqueue: (input: EnqueueInput) => void;
31
+ listPending: (workspaceId: string, targetAgentId: string) => ReportOutboxEntry[];
32
+ markDelivered: (id: number) => void;
33
+ pendingCount: (workspaceId: string, targetAgentId: string) => number;
34
+ };
35
+ export type ReportOutboxStore = ReturnType<typeof createReportOutboxStore>;
36
+ export {};
@@ -0,0 +1,33 @@
1
+ const rowToEntry = (row) => ({
2
+ id: row.id,
3
+ workspaceId: row.workspace_id,
4
+ targetAgentId: row.target_agent_id,
5
+ dispatchId: row.dispatch_id,
6
+ payload: row.payload,
7
+ createdAt: row.created_at,
8
+ deliveredAt: row.delivered_at,
9
+ });
10
+ export const createReportOutboxStore = (db) => {
11
+ // INSERT OR IGNORE on the UNIQUE dispatch_id: a dispatch reports once, so a
12
+ // second enqueue for the same dispatch (e.g. a retry path) is a no-op rather
13
+ // than a duplicate the orchestrator would see twice.
14
+ const enqueue = (input) => {
15
+ db.prepare(`INSERT OR IGNORE INTO report_outbox
16
+ (workspace_id, target_agent_id, dispatch_id, payload, created_at)
17
+ VALUES (?, ?, ?, ?, ?)`).run(input.workspaceId, input.targetAgentId, input.dispatchId, input.payload, Date.now());
18
+ };
19
+ const listPending = (workspaceId, targetAgentId) => db
20
+ .prepare(`SELECT id, workspace_id, target_agent_id, dispatch_id, payload, created_at, delivered_at
21
+ FROM report_outbox
22
+ WHERE workspace_id = ? AND target_agent_id = ? AND delivered_at IS NULL
23
+ ORDER BY created_at ASC, id ASC`)
24
+ .all(workspaceId, targetAgentId).map(rowToEntry);
25
+ const markDelivered = (id) => {
26
+ db.prepare(`UPDATE report_outbox SET delivered_at = ? WHERE id = ? AND delivered_at IS NULL`).run(Date.now(), id);
27
+ };
28
+ const pendingCount = (workspaceId, targetAgentId) => db
29
+ .prepare(`SELECT COUNT(*) AS n FROM report_outbox
30
+ WHERE workspace_id = ? AND target_agent_id = ? AND delivered_at IS NULL`)
31
+ .get(workspaceId, targetAgentId).n;
32
+ return { enqueue, listPending, markDelivered, pendingCount };
33
+ };
@@ -1,5 +1,6 @@
1
1
  import type { AgentSummary, WorkspaceSummary } from '../shared/types.js';
2
2
  import type { PersistedAgentRun } from './agent-run-store.js';
3
+ import type { FeatureFlags } from './feature-flags.js';
3
4
  import type { MessageLogHandle, MessageLogRecord, RecoveryMessage } from './message-log-store.js';
4
5
  export interface RestartPolicyInput {
5
6
  deleteMessage: (handle: MessageLogHandle) => void;
@@ -11,11 +12,9 @@ export interface RestartPolicyInput {
11
12
  listAgentRuns: (agentId: string) => PersistedAgentRun[];
12
13
  listMessagesForRecovery: (workspaceId: string, sinceMs: number) => RecoveryMessage[];
13
14
  readTasks: (workspacePath: string) => string;
14
- /** Experimental workflow gate threaded into the recovery handover prompt
15
- * so it matches the live flag. Optional; omitted → off. */
16
- getWorkflowsEnabled?: () => boolean;
17
- /** Experimental auto-staff gate (default on) — same. */
18
- getAutostaffEnabled?: () => boolean;
15
+ /** Resolves the live experimental flags, threaded into the recovery handover
16
+ * prompt so it matches a fresh startup. Optional; omitted → all off. */
17
+ getFlags?: () => FeatureFlags;
19
18
  }
20
19
  export declare const findPreviousRun: (runs: PersistedAgentRun[], currentRunId: string) => PersistedAgentRun | undefined;
21
20
  export declare const writeSystemMessage: ({ deleteMessage, insertMessage, record, runId, text, writeToRun, }: {
@@ -9,6 +9,10 @@ export interface RestartPolicy {
9
9
  workspace: WorkspaceSummary;
10
10
  writeToRun: (runId: string, text: string) => Promise<void>;
11
11
  }) => boolean;
12
+ /** Record that `runId` was killed at the user's request (Stop button), so a
13
+ * subsequent restart does not inject the crash-recovery handover for a run
14
+ * the user deliberately ended. */
15
+ markUserStopped: (runId: string) => void;
12
16
  }
13
17
  export declare const createNoopRestartPolicy: () => RestartPolicy;
14
- export declare const createRestartPolicy: ({ deleteMessage, getWorkspaceSnapshot, insertMessage, listAgentRuns, listMessagesForRecovery, readTasks, getWorkflowsEnabled, getAutostaffEnabled, }: RestartPolicyInput) => RestartPolicy;
18
+ export declare const createRestartPolicy: ({ deleteMessage, getWorkspaceSnapshot, insertMessage, listAgentRuns, listMessagesForRecovery, readTasks, getFlags, }: RestartPolicyInput) => RestartPolicy;
@@ -1,3 +1,4 @@
1
+ import { FEATURE_FLAGS_ALL_OFF } from './feature-flags.js';
1
2
  import { buildRecoverySummary } from './recovery-summary.js';
2
3
  import { findPreviousRun, writeSystemMessage, } from './restart-policy-support.js';
3
4
  import { createSystemRecoverySummaryMessage } from './runtime-message-builders.js';
@@ -6,38 +7,55 @@ export const createNoopRestartPolicy = () => ({
6
7
  injectPostStartMessage() {
7
8
  return false;
8
9
  },
10
+ markUserStopped() { },
9
11
  });
10
- export const createRestartPolicy = ({ deleteMessage, getWorkspaceSnapshot, insertMessage, listAgentRuns, listMessagesForRecovery, readTasks, getWorkflowsEnabled, getAutostaffEnabled, }) => ({
11
- injectPostStartMessage({ agentId, runId, startConfig, workspace, writeToRun }) {
12
- const previousRun = findPreviousRun(listAgentRuns(agentId), runId);
13
- if (!previousRun)
14
- return false;
15
- const snapshot = getWorkspaceSnapshot(workspace.id);
16
- const agent = snapshot.agents.find((item) => item.id === agentId);
17
- if (!agent)
18
- return false;
19
- const workers = snapshot.agents.filter((item) => item.role !== 'orchestrator' && item.id !== agentId);
20
- const tasksContent = readTasks(snapshot.summary.path);
21
- if (startConfig.resumedSessionId)
12
+ export const createRestartPolicy = ({ deleteMessage, getWorkspaceSnapshot, insertMessage, listAgentRuns, listMessagesForRecovery, readTasks, getFlags, }) => {
13
+ // Runs the user killed via the Stop button. A deliberate stop is otherwise
14
+ // byte-identical to a crash (both end status 'error'), so without this a
15
+ // stop+Restart would inject the "could not recover" handover with stale open
16
+ // tasks the user may have meant to abandon. In-process only: after a runtime
17
+ // restart this is empty, which is correct — that case IS a recovery.
18
+ const userStoppedRuns = new Set();
19
+ return {
20
+ markUserStopped(runId) {
21
+ userStoppedRuns.add(runId);
22
+ },
23
+ injectPostStartMessage({ agentId, runId, startConfig, workspace, writeToRun }) {
24
+ const previousRun = findPreviousRun(listAgentRuns(agentId), runId);
25
+ if (!previousRun)
26
+ return false;
27
+ // Consume the marker whether or not we end up skipping, so it never
28
+ // lingers to suppress a later genuine crash on the same run id.
29
+ const wasUserStopped = userStoppedRuns.delete(previousRun.runId);
30
+ const snapshot = getWorkspaceSnapshot(workspace.id);
31
+ const agent = snapshot.agents.find((item) => item.id === agentId);
32
+ if (!agent)
33
+ return false;
34
+ const workers = snapshot.agents.filter((item) => item.role !== 'orchestrator' && item.id !== agentId);
35
+ const tasksContent = readTasks(snapshot.summary.path);
36
+ if (startConfig.resumedSessionId)
37
+ return true;
38
+ // Deliberate stop + restart: start fresh, no crash handover.
39
+ if (wasUserStopped)
40
+ return false;
41
+ const text = buildRecoverySummary({
42
+ agent,
43
+ allTaskMessages: listMessagesForRecovery(workspace.id, 0),
44
+ messages: listMessagesForRecovery(workspace.id, Date.now() - RECOVERY_WINDOW_MS),
45
+ tasksContent,
46
+ workers,
47
+ workspace,
48
+ flags: getFlags?.() ?? FEATURE_FLAGS_ALL_OFF,
49
+ });
50
+ writeSystemMessage({
51
+ deleteMessage,
52
+ insertMessage,
53
+ record: createSystemRecoverySummaryMessage(workspace.id, agentId, text),
54
+ runId,
55
+ text,
56
+ writeToRun,
57
+ });
22
58
  return true;
23
- const text = buildRecoverySummary({
24
- agent,
25
- allTaskMessages: listMessagesForRecovery(workspace.id, 0),
26
- messages: listMessagesForRecovery(workspace.id, Date.now() - RECOVERY_WINDOW_MS),
27
- tasksContent,
28
- workers,
29
- workspace,
30
- workflowsEnabled: getWorkflowsEnabled?.() ?? false,
31
- autostaffEnabled: getAutostaffEnabled?.() ?? false,
32
- });
33
- writeSystemMessage({
34
- deleteMessage,
35
- insertMessage,
36
- record: createSystemRecoverySummaryMessage(workspace.id, agentId, text),
37
- runId,
38
- text,
39
- writeToRun,
40
- });
41
- return true;
42
- },
43
- });
59
+ },
60
+ };
61
+ };
@@ -1,4 +1,5 @@
1
1
  import { resolveCommandPath } from './agent-command-resolver.js';
2
+ import { readFeatureFlags } from './feature-flags.js';
2
3
  import { BadRequestError } from './http-errors.js';
3
4
  import { getRequiredParam, readJsonBody, route, sendJson } from './route-helpers.js';
4
5
  import { ensureProtocolFile } from './tasks-file.js';
@@ -49,11 +50,10 @@ const serializeRoleTemplate = (template) => ({
49
50
  */
50
51
  const refreshWorkflowProtocolDocs = (store) => {
51
52
  const policy = readWorkflowCliPolicy(store.settings.getAppState(WORKFLOW_CLI_POLICY_KEY)?.value ?? null);
52
- const enabled = readWorkflowEnabled(store.settings.getAppState(WORKFLOW_ENABLED_KEY)?.value ?? null);
53
- const autostaff = readAutostaffEnabled(store.settings.getAppState(AUTOSTAFF_ENABLED_KEY)?.value ?? null);
53
+ const flags = readFeatureFlags(store.settings);
54
54
  for (const workspace of store.listWorkspaces()) {
55
55
  try {
56
- ensureProtocolFile(workspace.path, policy, enabled, autostaff);
56
+ ensureProtocolFile(workspace.path, policy, flags);
57
57
  }
58
58
  catch (error) {
59
59
  console.error('[hive] swallowed:settings.refreshProtocol', error);
@@ -1,6 +1,29 @@
1
1
  import { getRequiredParam, readJsonBody, route, sendJson } from './route-helpers.js';
2
+ import { computeRunnableTasks } from './task-deps.js';
3
+ import { authenticateCliAgent, requireCommandForRole } from './team-authz.js';
2
4
  import { requireUiTokenFromRequest } from './ui-auth-helpers.js';
3
5
  export const taskRoutes = [
6
+ route('GET', '/api/workspaces/:workspaceId/tasks/next', ({ params, request, response, store, tasksFileService }) => {
7
+ const workspaceId = getRequiredParam(response, params, 'workspaceId', 'Workspace id is required');
8
+ if (!workspaceId) {
9
+ return;
10
+ }
11
+ // CLI-agent auth (mirrors `team list`): `team next` is an orchestrator
12
+ // planning query, not a UI call.
13
+ const agentId = request.headers['x-hive-agent-id'];
14
+ const token = request.headers['x-hive-agent-token'];
15
+ const agent = authenticateCliAgent({
16
+ fromAgentId: typeof agentId === 'string' ? agentId : undefined,
17
+ getAgent: store.getAgent,
18
+ token: typeof token === 'string' ? token : undefined,
19
+ validateToken: store.validateAgentToken,
20
+ workspaceId,
21
+ });
22
+ requireCommandForRole(agent, 'next');
23
+ const workspace = store.getWorkspaceSnapshot(workspaceId);
24
+ const tasks = computeRunnableTasks(tasksFileService.readTasks(workspace.summary.path));
25
+ sendJson(response, 200, { tasks });
26
+ }),
4
27
  route('GET', '/api/workspaces/:workspaceId/tasks', ({ params, request, response, store, tasksFileService }) => {
5
28
  const workspaceId = getRequiredParam(response, params, 'workspaceId', 'Workspace id is required');
6
29
  if (!workspaceId) {
@@ -78,6 +78,11 @@ export const workspaceRoutes = [
78
78
  workspaceId,
79
79
  });
80
80
  requireCommandForRole(agent, 'list');
81
+ // The orchestrator polling `team list` is its natural post-restart wakeup:
82
+ // flush any reports a prior outage stranded back into its stdin now that
83
+ // it is reachable again. Cheap (indexed, usually empty) and a safe no-op
84
+ // for non-orchestrator callers.
85
+ store.drainReportOutbox(workspaceId);
81
86
  sendJson(response, 200, enrichTeamList(workspaceId, store, store.listWorkers(workspaceId)).map(serializeTeamListItem));
82
87
  }),
83
88
  route('POST', '/api/workspaces/:workspaceId/workers', async ({ params, request, response, store }) => {
@@ -1,8 +1,9 @@
1
1
  import type { AgentRunStorePort } from './agent-runtime-ports.js';
2
+ import type { FeatureFlags } from './feature-flags.js';
2
3
  import type { MessageLogHandle, MessageLogRecord, RecoveryMessage } from './message-log-store.js';
3
4
  import type { TasksFileService } from './tasks-file.js';
4
5
  import type { WorkspaceStore } from './workspace-store.js';
5
- export declare const buildRuntimeRestartPolicy: ({ agentRunStore, messageLogStore, tasksFileService, workspaceStore, getWorkflowsEnabled, getAutostaffEnabled, }: {
6
+ export declare const buildRuntimeRestartPolicy: ({ agentRunStore, messageLogStore, tasksFileService, workspaceStore, getFlags, }: {
6
7
  agentRunStore: Pick<AgentRunStorePort, "listAgentRuns">;
7
8
  messageLogStore: {
8
9
  deleteMessage: (handle: MessageLogHandle) => void;
@@ -11,6 +12,5 @@ export declare const buildRuntimeRestartPolicy: ({ agentRunStore, messageLogStor
11
12
  };
12
13
  tasksFileService: Pick<TasksFileService, "readTasks">;
13
14
  workspaceStore: Pick<WorkspaceStore, "getWorkspaceSnapshot">;
14
- getWorkflowsEnabled?: () => boolean;
15
- getAutostaffEnabled?: () => boolean;
15
+ getFlags?: () => FeatureFlags;
16
16
  }) => import("./restart-policy.js").RestartPolicy;
@@ -1,12 +1,11 @@
1
1
  import { createRestartPolicy } from './restart-policy.js';
2
2
  // Narrow helper keeps runtime-store under the hard line cap.
3
- export const buildRuntimeRestartPolicy = ({ agentRunStore, messageLogStore, tasksFileService, workspaceStore, getWorkflowsEnabled, getAutostaffEnabled, }) => createRestartPolicy({
3
+ export const buildRuntimeRestartPolicy = ({ agentRunStore, messageLogStore, tasksFileService, workspaceStore, getFlags, }) => createRestartPolicy({
4
4
  deleteMessage: messageLogStore.deleteMessage,
5
5
  getWorkspaceSnapshot: workspaceStore.getWorkspaceSnapshot,
6
6
  insertMessage: messageLogStore.insertMessage,
7
7
  listAgentRuns: agentRunStore.listAgentRuns,
8
8
  listMessagesForRecovery: messageLogStore.listMessagesForRecovery,
9
9
  readTasks: tasksFileService.readTasks,
10
- ...(getWorkflowsEnabled ? { getWorkflowsEnabled } : {}),
11
- ...(getAutostaffEnabled ? { getAutostaffEnabled } : {}),
10
+ ...(getFlags ? { getFlags } : {}),
12
11
  });
@@ -28,6 +28,9 @@ export interface RuntimeStore {
28
28
  restartedWorker: boolean;
29
29
  }>;
30
30
  reportTask: (workspaceId: string, workerId: string, input?: ReportTaskInput) => ReportTaskResult;
31
+ /** Flush any reports stranded by a prior orchestrator outage. Safe no-op
32
+ * when the orchestrator is down or the outbox is empty. */
33
+ drainReportOutbox: (workspaceId: string) => void;
31
34
  statusTask: (workspaceId: string, workerId: string, input?: StatusTaskInput) => ReportTaskResult;
32
35
  cancelTask: (workspaceId: string, dispatchId: string, input: CancelTaskInput) => ReportTaskResult;
33
36
  listDispatches: (workspaceId: string, options?: ListDispatchesOptions) => DispatchRecord[];
@@ -11,6 +11,7 @@ import { createTasksFileService } from './tasks-file.js';
11
11
  import { createTasksFileWatcher } from './tasks-file-watcher.js';
12
12
  import { createTeamOperations } from './team-operations.js';
13
13
  import { createUiAuth } from './ui-auth.js';
14
+ import { createWebhookNotifier } from './webhook-notifier.js';
14
15
  import { type WorkerOutputTracker } from './worker-output-tracker.js';
15
16
  import { type WorkflowDispatchAwaiter } from './workflow-dispatch-awaiter.js';
16
17
  import { createWorkflowRunLogStore } from './workflow-run-log-store.js';
@@ -31,6 +32,7 @@ export interface RuntimeStoreServices {
31
32
  tasksFileService: ReturnType<typeof createTasksFileService>;
32
33
  teamOps: ReturnType<typeof createTeamOperations>;
33
34
  uiAuth: ReturnType<typeof createUiAuth>;
35
+ webhookNotifier: ReturnType<typeof createWebhookNotifier>;
34
36
  workerOutputTracker: WorkerOutputTracker | null;
35
37
  workflowDispatchAwaiter: WorkflowDispatchAwaiter;
36
38
  workflowRunLogStore: ReturnType<typeof createWorkflowRunLogStore>;
@@ -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),
@@ -118,6 +118,19 @@ export const initializeRuntimeDatabase = (db) => {
118
118
  CREATE INDEX IF NOT EXISTS idx_dispatches_open_by_worker
119
119
  ON dispatches (workspace_id, to_agent_id, status, sequence);
120
120
 
121
+ CREATE TABLE IF NOT EXISTS report_outbox (
122
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
123
+ workspace_id TEXT NOT NULL,
124
+ target_agent_id TEXT NOT NULL,
125
+ dispatch_id TEXT NOT NULL UNIQUE,
126
+ payload TEXT NOT NULL,
127
+ created_at INTEGER NOT NULL,
128
+ delivered_at INTEGER
129
+ );
130
+
131
+ CREATE INDEX IF NOT EXISTS idx_report_outbox_pending
132
+ ON report_outbox (workspace_id, target_agent_id, delivered_at, created_at);
133
+
121
134
  CREATE TABLE IF NOT EXISTS workflow_runs (
122
135
  id TEXT PRIMARY KEY,
123
136
  workspace_id TEXT NOT NULL,
@@ -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[];