@tt-a1i/hive 2.1.10 → 2.1.12

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 (69) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +13 -11
  3. package/README.zh.md +9 -8
  4. package/dist/src/cli/hive-defaults.d.ts +1 -0
  5. package/dist/src/cli/hive-defaults.js +1 -0
  6. package/dist/src/cli/hive-mcp.d.ts +62 -0
  7. package/dist/src/cli/hive-mcp.js +254 -0
  8. package/dist/src/cli/hive.d.ts +2 -0
  9. package/dist/src/cli/hive.js +12 -2
  10. package/dist/src/cli/team.d.ts +8 -0
  11. package/dist/src/cli/team.js +100 -0
  12. package/dist/src/server/agent-runtime-contract.d.ts +1 -0
  13. package/dist/src/server/agent-runtime.js +4 -0
  14. package/dist/src/server/agent-startup-instructions.js +1 -1
  15. package/dist/src/server/command-preset-defaults.d.ts +1 -1
  16. package/dist/src/server/command-preset-defaults.js +10 -0
  17. package/dist/src/server/diagnostics-support-bundle.d.ts +1 -1
  18. package/dist/src/server/external-goal-auth.d.ts +1 -0
  19. package/dist/src/server/external-goal-auth.js +1 -0
  20. package/dist/src/server/external-goal-bridge.d.ts +103 -0
  21. package/dist/src/server/external-goal-bridge.js +299 -0
  22. package/dist/src/server/external-goal-store.d.ts +59 -0
  23. package/dist/src/server/external-goal-store.js +165 -0
  24. package/dist/src/server/hive-team-guidance.js +1 -0
  25. package/dist/src/server/post-start-input-writer.js +28 -7
  26. package/dist/src/server/route-types.d.ts +30 -0
  27. package/dist/src/server/routes-external-goals.d.ts +2 -0
  28. package/dist/src/server/routes-external-goals.js +184 -0
  29. package/dist/src/server/routes-runtime.js +6 -1
  30. package/dist/src/server/routes-team-goals.d.ts +2 -0
  31. package/dist/src/server/routes-team-goals.js +60 -0
  32. package/dist/src/server/routes.js +4 -0
  33. package/dist/src/server/runtime-store-contract.d.ts +11 -0
  34. package/dist/src/server/runtime-store-external-goals.d.ts +47 -0
  35. package/dist/src/server/runtime-store-external-goals.js +24 -0
  36. package/dist/src/server/runtime-store-helpers.d.ts +3 -0
  37. package/dist/src/server/runtime-store-helpers.js +11 -1
  38. package/dist/src/server/runtime-store.js +6 -0
  39. package/dist/src/server/sqlite-schema-v37.d.ts +2 -0
  40. package/dist/src/server/sqlite-schema-v37.js +38 -0
  41. package/dist/src/server/sqlite-schema-v38.d.ts +2 -0
  42. package/dist/src/server/sqlite-schema-v38.js +26 -0
  43. package/dist/src/server/sqlite-schema.d.ts +1 -1
  44. package/dist/src/server/sqlite-schema.js +46 -1
  45. package/dist/src/server/team-authz.d.ts +1 -1
  46. package/dist/src/server/team-authz.js +1 -0
  47. package/dist/src/server/ui-auth.d.ts +2 -0
  48. package/dist/src/server/ui-auth.js +7 -0
  49. package/dist/src/server/workflow-cli-policy.d.ts +1 -1
  50. package/dist/src/server/workflow-cli-policy.js +1 -0
  51. package/package.json +3 -3
  52. package/web/dist/assets/{AddWorkerDialog-Dc0slXtx.js → AddWorkerDialog-BA0p5Pcr.js} +2 -2
  53. package/web/dist/assets/{AddWorkspaceFlow-AyzSdx5w.js → AddWorkspaceFlow-YO-HxdrH.js} +1 -1
  54. package/web/dist/assets/{FirstRunWizard-DGB5zWhl.js → FirstRunWizard-C5iyX5QF.js} +1 -1
  55. package/web/dist/assets/{MarketplaceDrawer-DAZ-crqq.js → MarketplaceDrawer-BIyJIDK4.js} +1 -1
  56. package/web/dist/assets/{TaskGraphDrawer-DYk-0uKD.js → TaskGraphDrawer-C0eOFQ8c.js} +1 -1
  57. package/web/dist/assets/{WhatsNewDialog-B5tu3MV2.js → WhatsNewDialog-Ci9pUKEP.js} +1 -1
  58. package/web/dist/assets/{WorkerModal-C73K7SUg.js → WorkerModal-DXHUZSN-.js} +1 -1
  59. package/web/dist/assets/WorkflowsDrawer-C5cS2jKY.js +1 -0
  60. package/web/dist/assets/{WorkspaceMemoryDrawer-CCz3HB4F.js → WorkspaceMemoryDrawer-BsAoM-lK.js} +1 -1
  61. package/web/dist/assets/{WorkspaceTaskDrawer-C7JzoRp8.js → WorkspaceTaskDrawer-rLaXBrNl.js} +1 -1
  62. package/web/dist/assets/index-CTJnf-r_.js +84 -0
  63. package/web/dist/assets/{search-Bx1xfpzr.js → search-CbyBNPSq.js} +1 -1
  64. package/web/dist/assets/{square-terminal-Dfr9QC1I.js → square-terminal-eIii6bbw.js} +1 -1
  65. package/web/dist/index.html +1 -1
  66. package/web/dist/manifest.webmanifest +1 -1
  67. package/web/dist/sw.js +1 -1
  68. package/web/dist/assets/WorkflowsDrawer-mrSAPqAh.js +0 -1
  69. package/web/dist/assets/index-CVQc8PLJ.js +0 -84
@@ -0,0 +1,59 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ export declare const EXTERNAL_GOAL_STATUSES: readonly ["open", "in_progress", "blocked", "done", "failed", "cancelled"];
3
+ export type ExternalGoalStatus = (typeof EXTERNAL_GOAL_STATUSES)[number];
4
+ export declare const EXTERNAL_GOAL_REPORT_STATUSES: readonly ["progress", "done", "blocked", "failed"];
5
+ export type ExternalGoalReportStatus = (typeof EXTERNAL_GOAL_REPORT_STATUSES)[number];
6
+ export declare const EXTERNAL_GOAL_EVENT_KINDS: readonly ["goal_started", "goal_continued", "goal_delivered", "progress_reported", "goal_done", "goal_blocked", "goal_failed", "goal_cancelled", "delivery_failed"];
7
+ export type ExternalGoalEventKind = (typeof EXTERNAL_GOAL_EVENT_KINDS)[number];
8
+ export interface ExternalGoalSession {
9
+ closedAt: number | null;
10
+ context: unknown;
11
+ createdAt: number;
12
+ goal: string;
13
+ id: string;
14
+ source: string;
15
+ status: ExternalGoalStatus;
16
+ summary: string | null;
17
+ title: string | null;
18
+ updatedAt: number;
19
+ workspaceId: string;
20
+ }
21
+ export interface ExternalGoalEvent {
22
+ artifacts: string[];
23
+ body: string;
24
+ createdAt: number;
25
+ goalId: string;
26
+ id: string;
27
+ kind: ExternalGoalEventKind;
28
+ sequence: number;
29
+ status: ExternalGoalReportStatus | ExternalGoalStatus | null;
30
+ workspaceId: string;
31
+ }
32
+ interface CreateExternalGoalInput {
33
+ context?: unknown;
34
+ goal: string;
35
+ source: string;
36
+ workspaceId: string;
37
+ }
38
+ interface AppendExternalGoalEventInput {
39
+ artifacts?: string[];
40
+ body: string;
41
+ goalId: string;
42
+ kind: ExternalGoalEventKind;
43
+ sessionStatus?: ExternalGoalStatus;
44
+ status?: ExternalGoalEvent['status'];
45
+ }
46
+ export declare const isExternalGoalReportStatus: (value: unknown) => value is ExternalGoalReportStatus;
47
+ export declare const createExternalGoalStore: (db: Database) => {
48
+ appendEvent: (input: AppendExternalGoalEventInput) => ExternalGoalEvent;
49
+ createSession: (input: CreateExternalGoalInput) => {
50
+ event: ExternalGoalEvent;
51
+ session: ExternalGoalSession;
52
+ };
53
+ deleteWorkspaceGoals: (workspaceId: string) => void;
54
+ getLatestSequence: (goalId: string) => number;
55
+ getSession: (goalId: string) => ExternalGoalSession | undefined;
56
+ listEventsAfter: (goalId: string, cursor?: number) => ExternalGoalEvent[];
57
+ };
58
+ export type ExternalGoalStore = ReturnType<typeof createExternalGoalStore>;
59
+ export {};
@@ -0,0 +1,165 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ export const EXTERNAL_GOAL_STATUSES = [
3
+ 'open',
4
+ 'in_progress',
5
+ 'blocked',
6
+ 'done',
7
+ 'failed',
8
+ 'cancelled',
9
+ ];
10
+ export const EXTERNAL_GOAL_REPORT_STATUSES = ['progress', 'done', 'blocked', 'failed'];
11
+ export const EXTERNAL_GOAL_EVENT_KINDS = [
12
+ 'goal_started',
13
+ 'goal_continued',
14
+ 'goal_delivered',
15
+ 'progress_reported',
16
+ 'goal_done',
17
+ 'goal_blocked',
18
+ 'goal_failed',
19
+ 'goal_cancelled',
20
+ 'delivery_failed',
21
+ ];
22
+ const CLOSED_STATUSES = new Set(['blocked', 'done', 'failed', 'cancelled']);
23
+ export const isExternalGoalReportStatus = (value) => typeof value === 'string' && EXTERNAL_GOAL_REPORT_STATUSES.includes(value);
24
+ const toSessionRecord = (row) => ({
25
+ closedAt: row.closed_at,
26
+ context: JSON.parse(row.context_json),
27
+ createdAt: row.created_at,
28
+ goal: row.goal,
29
+ id: row.id,
30
+ source: row.source,
31
+ status: row.status,
32
+ summary: row.summary,
33
+ title: row.title,
34
+ updatedAt: row.updated_at,
35
+ workspaceId: row.workspace_id,
36
+ });
37
+ const toEventRecord = (row) => ({
38
+ artifacts: JSON.parse(row.artifacts_json),
39
+ body: row.body,
40
+ createdAt: row.created_at,
41
+ goalId: row.goal_id,
42
+ id: row.id,
43
+ kind: row.kind,
44
+ sequence: row.sequence,
45
+ status: row.status,
46
+ workspaceId: row.workspace_id,
47
+ });
48
+ const deriveTitle = (goal) => {
49
+ const firstLine = goal
50
+ .split(/\r?\n/u)
51
+ .map((line) => line.trim())
52
+ .find(Boolean);
53
+ return (firstLine ?? 'External goal').slice(0, 120);
54
+ };
55
+ export const createExternalGoalStore = (db) => {
56
+ const getSession = (goalId) => {
57
+ const row = db.prepare('SELECT * FROM external_goal_sessions WHERE id = ?').get(goalId);
58
+ return row ? toSessionRecord(row) : undefined;
59
+ };
60
+ const listEventsAfter = (goalId, cursor = 0) => db
61
+ .prepare(`SELECT *
62
+ FROM external_goal_events
63
+ WHERE goal_id = ? AND sequence > ?
64
+ ORDER BY sequence ASC`)
65
+ .all(goalId, cursor).map(toEventRecord);
66
+ const getLatestSequence = (goalId) => {
67
+ const row = db
68
+ .prepare('SELECT COALESCE(MAX(sequence), 0) AS sequence FROM external_goal_events WHERE goal_id = ?')
69
+ .get(goalId);
70
+ return row?.sequence ?? 0;
71
+ };
72
+ const insertEvent = (session, input) => {
73
+ const sequence = getLatestSequence(input.goalId) + 1;
74
+ const now = Date.now();
75
+ const event = {
76
+ artifacts: input.artifacts ?? [],
77
+ body: input.body,
78
+ createdAt: now,
79
+ goalId: session.id,
80
+ id: randomUUID(),
81
+ kind: input.kind,
82
+ sequence,
83
+ status: input.status ?? null,
84
+ workspaceId: session.workspaceId,
85
+ };
86
+ db.prepare(`INSERT INTO external_goal_events (
87
+ id,
88
+ goal_id,
89
+ workspace_id,
90
+ sequence,
91
+ kind,
92
+ status,
93
+ body,
94
+ artifacts_json,
95
+ created_at
96
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(event.id, event.goalId, event.workspaceId, event.sequence, event.kind, event.status, event.body, JSON.stringify(event.artifacts), event.createdAt);
97
+ if (input.sessionStatus) {
98
+ db.prepare(`UPDATE external_goal_sessions
99
+ SET status = ?, updated_at = ?, closed_at = ?
100
+ WHERE id = ?`).run(input.sessionStatus, now, CLOSED_STATUSES.has(input.sessionStatus) ? now : null, session.id);
101
+ }
102
+ else {
103
+ db.prepare('UPDATE external_goal_sessions SET updated_at = ? WHERE id = ?').run(now, session.id);
104
+ }
105
+ return event;
106
+ };
107
+ const appendEvent = (input) => {
108
+ return db.transaction(() => {
109
+ const session = getSession(input.goalId);
110
+ if (!session)
111
+ throw new Error(`External goal not found: ${input.goalId}`);
112
+ return insertEvent(session, input);
113
+ })();
114
+ };
115
+ const createSession = (input) => {
116
+ return db.transaction(() => {
117
+ const now = Date.now();
118
+ const session = {
119
+ closedAt: null,
120
+ context: input.context ?? null,
121
+ createdAt: now,
122
+ goal: input.goal,
123
+ id: `goal_${randomUUID()}`,
124
+ source: input.source,
125
+ status: 'open',
126
+ summary: null,
127
+ title: deriveTitle(input.goal),
128
+ updatedAt: now,
129
+ workspaceId: input.workspaceId,
130
+ };
131
+ db.prepare(`INSERT INTO external_goal_sessions (
132
+ id,
133
+ workspace_id,
134
+ source,
135
+ status,
136
+ goal,
137
+ context_json,
138
+ title,
139
+ summary,
140
+ created_at,
141
+ updated_at,
142
+ closed_at
143
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(session.id, session.workspaceId, session.source, session.status, session.goal, JSON.stringify(session.context), session.title, session.summary, session.createdAt, session.updatedAt, session.closedAt);
144
+ const event = insertEvent(session, {
145
+ body: input.goal,
146
+ goalId: session.id,
147
+ kind: 'goal_started',
148
+ status: 'open',
149
+ });
150
+ return { event, session };
151
+ })();
152
+ };
153
+ const deleteWorkspaceGoals = (workspaceId) => {
154
+ db.prepare('DELETE FROM external_goal_events WHERE workspace_id = ?').run(workspaceId);
155
+ db.prepare('DELETE FROM external_goal_sessions WHERE workspace_id = ?').run(workspaceId);
156
+ };
157
+ return {
158
+ appendEvent,
159
+ createSession,
160
+ deleteWorkspaceGoals,
161
+ getLatestSequence,
162
+ getSession,
163
+ listEventsAfter,
164
+ };
165
+ };
@@ -313,6 +313,7 @@ export const buildProtocolGuide = (topic, cliPolicy = DEFAULT_WORKFLOW_CLI_POLIC
313
313
  '- `team list` — show current members/status and open dispatches.',
314
314
  '- `team send "<member-name>" "<task>"` — dispatch by member name, never id.',
315
315
  '- `team cancel --dispatch <id> "<reason>"` — cancel an obsolete open dispatch.',
316
+ '- `team goal report --goal <goal-id> --status progress|done|blocked|failed --stdin` — report external Supervisor goal events when an external-goal envelope asks for it.',
316
317
  `- \`team spawn <role> [--name <name>] [--cli <${BUILTIN_COMMAND_PRESET_CLI_LIST}>] [--ephemeral]\` — create a member only when authorized.`,
317
318
  '- `team dismiss <member-name>` — remove a member you are allowed to remove.',
318
319
  '',
@@ -39,6 +39,7 @@ const COMMANDS_WITH_BRACKETED_PASTE = new Set([
39
39
  'grok',
40
40
  'hermes',
41
41
  'opencode',
42
+ 'pi',
42
43
  ]);
43
44
  const COMMANDS_WAITING_FOR_PASTE_ACK = new Set(['claude', 'codex']);
44
45
  const BRACKETED_PASTE_END = '\u001b[201~';
@@ -48,6 +49,9 @@ const ESCAPE = String.fromCharCode(27);
48
49
  const BELL = String.fromCharCode(7);
49
50
  const TERMINAL_CONTROL_PATTERN = new RegExp(`${ESCAPE}\\[[0-?]*[ -/]*[@-~]|${ESCAPE}\\][^${BELL}${ESCAPE}]*(?:${BELL}|${ESCAPE}\\\\)`, 'gu');
50
51
  const HIVE_ACK_GATED_MESSAGE_PATTERN = /<(?:hive-system-message\b|hive-message\s+kind="(?:cancel|dispatch|heartbeat|startup)"(?:\s|>))/u;
52
+ const OPENCODE_VISIBLE_PROMPT_PATTERN = /\bAsk anything\.\.\./u;
53
+ const OPENCODE_COMPLETED_TURN_FOOTER_PATTERN = /^▣\s+[^·\n]+·\s+\S.*\s+·\s+\d+(?:\.\d+)?(?:ms|s)$/u;
54
+ const OPENCODE_INTERRUPT_STATUS_PATTERN = /\besc\s+interrupt\b/iu;
51
55
  export const toBracketedPasteSubmission = (text) => `\u001b[200~${text}\u001b[201~`;
52
56
  const toError = (error) => (error instanceof Error ? error : new Error(String(error)));
53
57
  const createRunInactiveError = (runId) => new Error(`Run became inactive before input was submitted: ${runId}`);
@@ -71,14 +75,17 @@ export const isInteractiveAgentCommand = (command) => {
71
75
  };
72
76
  const getPlainTerminalOutput = (output) => output.replace(/\r/g, '\n').replace(TERMINAL_CONTROL_PATTERN, '');
73
77
  const getPlainTerminalTail = (output) => getPlainTerminalOutput(output).slice(-PROMPT_READY_TAIL_CHARS);
78
+ const getRecentNonEmptyTerminalLines = (output) => getPlainTerminalTail(output)
79
+ .split('\n')
80
+ .map((line) => line.trim())
81
+ .filter(Boolean);
74
82
  const hasGeminiPromptReady = (output) => /\bType your message\b/u.test(output);
75
83
  const hasHermesPromptReady = (output) => {
76
84
  const plain = getPlainTerminalTail(output);
77
85
  return /\bWelcome to Hermes Agent![\s\S]*❯/u.test(plain);
78
86
  };
79
- const hasOpencodeVisiblePromptReady = (output) => /\bAsk anything\.\.\./u.test(getPlainTerminalTail(output));
80
- const hasOpencodeCompletedTurnReady = (output) => /(?:^|\s)▣\s+Build\s+·/u.test(getLastNonEmptyTerminalLine(output));
81
87
  const hasGrokPromptReady = (output) => /\b(?:Enter:send|Composer\s+\S+)/u.test(output);
88
+ const hasPiPromptReady = (output) => /\bpi\s+v\d+(?:\.\d+){1,3}[\s\S]*\b(?:escape\s+interrupt|ctrl\+c\/ctrl\+d\s+clear\/exit)\b/iu.test(getPlainTerminalTail(output));
82
89
  const hasAgyPromptReady = (output) => /(?:^|\n)\s*>\s*\n\s*(?:[─-]{8,}|\?\s*for shortcuts)/u.test(getPlainTerminalOutput(output));
83
90
  const getLastNonEmptyTerminalLine = (output) => {
84
91
  const normalized = getPlainTerminalOutput(output);
@@ -112,20 +119,33 @@ export const hasFirstRunSetupPrompt = (output) => {
112
119
  const recent = recentLines.join('\n');
113
120
  return SETUP_PROMPT_PATTERNS.some((pattern) => pattern.test(recent));
114
121
  };
122
+ const getOpencodePromptReadyState = (output) => {
123
+ const recentLines = getRecentNonEmptyTerminalLines(output);
124
+ for (let index = recentLines.length - 1; index >= 0; index--) {
125
+ const line = recentLines[index];
126
+ if (line === undefined)
127
+ continue;
128
+ if (OPENCODE_INTERRUPT_STATUS_PATTERN.test(line))
129
+ return 'ready';
130
+ if (OPENCODE_VISIBLE_PROMPT_PATTERN.test(line))
131
+ return 'ready';
132
+ if (OPENCODE_COMPLETED_TURN_FOOTER_PATTERN.test(line))
133
+ return 'ready-after-settle';
134
+ }
135
+ return 'not-ready';
136
+ };
115
137
  const getInteractivePromptReadyState = (output, command = '') => {
116
138
  const commandName = getCommandName(command);
117
139
  if (hasBarePromptLine(output) ||
118
140
  (commandName === 'agy' && hasAgyPromptReady(output)) ||
119
141
  (commandName === 'grok' && hasGrokPromptReady(output)) ||
120
142
  (commandName === 'hermes' && hasHermesPromptReady(output)) ||
143
+ (commandName === 'pi' && hasPiPromptReady(output)) ||
121
144
  ((commandName === 'gemini' || commandName === 'qwen') && hasGeminiPromptReady(output))) {
122
145
  return 'ready';
123
146
  }
124
147
  if (commandName === 'opencode') {
125
- if (hasOpencodeVisiblePromptReady(output))
126
- return 'ready';
127
- if (hasOpencodeCompletedTurnReady(output))
128
- return 'ready-after-settle';
148
+ return getOpencodePromptReadyState(output);
129
149
  }
130
150
  return 'not-ready';
131
151
  };
@@ -167,11 +187,12 @@ const canTimeoutBeforePromptReady = (command) => {
167
187
  commandName !== 'gemini' &&
168
188
  commandName !== 'hermes' &&
169
189
  commandName !== 'opencode' &&
190
+ commandName !== 'pi' &&
170
191
  commandName !== 'qwen');
171
192
  };
172
193
  const hasPromptReadyHardTimeout = (command) => {
173
194
  const commandName = getCommandName(command);
174
- return commandName === 'hermes' || commandName === 'opencode';
195
+ return commandName === 'hermes' || commandName === 'opencode' || commandName === 'pi';
175
196
  };
176
197
  const isWritableRunStatus = (status) => status === undefined || status === 'starting' || status === 'running';
177
198
  const writeIfRunWritable = (agentManager, runId, text) => {
@@ -67,6 +67,15 @@ export interface ReportTaskBody {
67
67
  status?: 'success' | 'failed';
68
68
  artifacts?: unknown[];
69
69
  }
70
+ export interface GoalReportBody {
71
+ artifacts?: unknown[];
72
+ goal_id?: string;
73
+ project_id: string;
74
+ from_agent_id: string;
75
+ token?: string;
76
+ result?: string;
77
+ status?: string;
78
+ }
70
79
  export interface CancelTaskBody {
71
80
  dispatch_id?: string;
72
81
  project_id: string;
@@ -74,6 +83,27 @@ export interface CancelTaskBody {
74
83
  token?: string;
75
84
  reason?: string;
76
85
  }
86
+ export interface ExternalGoalStartBody {
87
+ workspace_id?: string;
88
+ goal?: string;
89
+ context?: unknown;
90
+ timeout_hint_ms?: number;
91
+ source?: string;
92
+ }
93
+ export interface ExternalGoalWaitBody {
94
+ goal_id?: string;
95
+ cursor?: number;
96
+ timeout_ms?: number;
97
+ }
98
+ export interface ExternalGoalContinueBody {
99
+ goal_id?: string;
100
+ message?: string;
101
+ context?: unknown;
102
+ }
103
+ export interface ExternalGoalCancelBody {
104
+ goal_id?: string;
105
+ reason?: string;
106
+ }
77
107
  export interface CreateWorkspaceBody {
78
108
  path: string;
79
109
  name: string;
@@ -0,0 +1,2 @@
1
+ import type { RouteDefinition } from './route-types.js';
2
+ export declare const externalGoalRoutes: RouteDefinition[];
@@ -0,0 +1,184 @@
1
+ import { HIVE_SUPERVISOR_TOKEN_HEADER } from './external-goal-auth.js';
2
+ import { ExternalGoalDeliveryError } from './external-goal-bridge.js';
3
+ import { BadRequestError, ForbiddenError } from './http-errors.js';
4
+ import { getRequiredParam, readJsonBody, route, sendJson } from './route-helpers.js';
5
+ const BODY_MAX_CHARS = 40_000;
6
+ const SOURCE_MAX_CHARS = 80;
7
+ const requireNonEmptyString = (value, field, maxChars = BODY_MAX_CHARS) => {
8
+ if (typeof value !== 'string' || value.trim().length === 0) {
9
+ throw new BadRequestError(`Missing ${field}`);
10
+ }
11
+ if ([...value].length > maxChars) {
12
+ throw new BadRequestError(`${field} must be ${maxChars} characters or fewer`);
13
+ }
14
+ return value;
15
+ };
16
+ const optionalNonEmptyString = (value, field, maxChars = SOURCE_MAX_CHARS) => {
17
+ if (value === undefined || value === null)
18
+ return undefined;
19
+ return requireNonEmptyString(value, field, maxChars);
20
+ };
21
+ const serializeEvent = (event) => ({
22
+ artifacts: event.artifacts,
23
+ body: event.body,
24
+ created_at: event.createdAt,
25
+ goal_id: event.goalId,
26
+ id: event.id,
27
+ kind: event.kind,
28
+ sequence: event.sequence,
29
+ status: event.status,
30
+ workspace_id: event.workspaceId,
31
+ });
32
+ const serializeSession = (session) => ({
33
+ closed_at: session.closedAt,
34
+ created_at: session.createdAt,
35
+ goal: session.goal,
36
+ id: session.id,
37
+ source: session.source,
38
+ status: session.status,
39
+ summary: session.summary,
40
+ title: session.title,
41
+ updated_at: session.updatedAt,
42
+ workspace_id: session.workspaceId,
43
+ });
44
+ const serializeWaitResult = (result) => ({
45
+ cursor: result.cursor,
46
+ events: result.events.map(serializeEvent),
47
+ goal_id: result.goalId,
48
+ status: result.status,
49
+ });
50
+ const sendDeliveryError = (response, error) => {
51
+ sendJson(response, error.statusCode, {
52
+ cursor: error.cursor,
53
+ error: error.message,
54
+ goal_id: error.goalId,
55
+ status: error.status,
56
+ });
57
+ };
58
+ const requireExternalController = (store, request) => {
59
+ if (store.authorizeRemoteTunnelRequest(request)) {
60
+ throw new ForbiddenError('Supervisor token is not available over the remote tunnel');
61
+ }
62
+ const rawToken = request.headers[HIVE_SUPERVISOR_TOKEN_HEADER];
63
+ const token = Array.isArray(rawToken) ? rawToken[0] : rawToken;
64
+ if (!store.validateSupervisorToken(token)) {
65
+ throw new ForbiddenError('External goal endpoint requires valid Supervisor token');
66
+ }
67
+ };
68
+ export const externalGoalRoutes = [
69
+ route('GET', '/api/external-goals/session', ({ request, response, store }) => {
70
+ if (store.authorizeRemoteTunnelRequest(request)) {
71
+ throw new ForbiddenError('Supervisor token is not available over the remote tunnel');
72
+ }
73
+ sendJson(response, 200, {
74
+ token: store.getSupervisorToken(),
75
+ token_type: 'hive-supervisor',
76
+ });
77
+ }),
78
+ route('GET', '/api/external-goals/workspaces', ({ request, response, store }) => {
79
+ requireExternalController(store, request);
80
+ sendJson(response, 200, { workspaces: store.listExternalGoalWorkspaces() });
81
+ }),
82
+ route('GET', '/api/external-goals/workspaces/:workspaceId', ({ params, request, response, store }) => {
83
+ requireExternalController(store, request);
84
+ const workspaceId = getRequiredParam(response, params, 'workspaceId', 'Workspace id is required');
85
+ if (!workspaceId)
86
+ return;
87
+ sendJson(response, 200, store.inspectExternalGoalWorkspace({ workspaceId }));
88
+ }),
89
+ route('POST', '/api/external-goals/start', async ({ request, response, store }) => {
90
+ requireExternalController(store, request);
91
+ const body = await readJsonBody(request);
92
+ const workspaceId = requireNonEmptyString(body.workspace_id, 'workspace_id', 200);
93
+ const goal = requireNonEmptyString(body.goal, 'goal');
94
+ const source = optionalNonEmptyString(body.source, 'source') ?? 'hive-mcp';
95
+ let result;
96
+ try {
97
+ result = await store.startExternalGoal({
98
+ workspaceId,
99
+ goal,
100
+ source,
101
+ ...(body.context !== undefined ? { context: body.context } : {}),
102
+ ...(body.timeout_hint_ms !== undefined ? { timeoutHintMs: body.timeout_hint_ms } : {}),
103
+ });
104
+ }
105
+ catch (error) {
106
+ if (error instanceof ExternalGoalDeliveryError) {
107
+ sendDeliveryError(response, error);
108
+ return;
109
+ }
110
+ throw error;
111
+ }
112
+ sendJson(response, 202, {
113
+ cursor: result.cursor,
114
+ events: result.events.map(serializeEvent),
115
+ goal_id: result.goalId,
116
+ ok: true,
117
+ session: serializeSession(result.session),
118
+ status: result.status,
119
+ });
120
+ }),
121
+ route('POST', '/api/external-goals/wait', async ({ request, response, store }) => {
122
+ requireExternalController(store, request);
123
+ const body = await readJsonBody(request);
124
+ const goalId = requireNonEmptyString(body.goal_id, 'goal_id', 200);
125
+ const result = await store.waitExternalGoal({
126
+ goalId,
127
+ ...(body.cursor !== undefined ? { cursor: body.cursor } : {}),
128
+ ...(body.timeout_ms !== undefined ? { timeoutMs: body.timeout_ms } : {}),
129
+ });
130
+ sendJson(response, 200, serializeWaitResult(result));
131
+ }),
132
+ route('POST', '/api/external-goals/continue', async ({ request, response, store }) => {
133
+ requireExternalController(store, request);
134
+ const body = await readJsonBody(request);
135
+ const goalId = requireNonEmptyString(body.goal_id, 'goal_id', 200);
136
+ const message = requireNonEmptyString(body.message, 'message');
137
+ let result;
138
+ try {
139
+ result = await store.continueExternalGoal({
140
+ goalId,
141
+ message,
142
+ ...(body.context !== undefined ? { context: body.context } : {}),
143
+ });
144
+ }
145
+ catch (error) {
146
+ if (error instanceof ExternalGoalDeliveryError) {
147
+ sendDeliveryError(response, error);
148
+ return;
149
+ }
150
+ throw error;
151
+ }
152
+ sendJson(response, 202, {
153
+ cursor: result.cursor,
154
+ event: serializeEvent(result.event),
155
+ ok: true,
156
+ session: serializeSession(result.session),
157
+ status: result.status,
158
+ });
159
+ }),
160
+ route('POST', '/api/external-goals/cancel', async ({ request, response, store }) => {
161
+ requireExternalController(store, request);
162
+ const body = await readJsonBody(request);
163
+ const goalId = requireNonEmptyString(body.goal_id, 'goal_id', 200);
164
+ const reason = requireNonEmptyString(body.reason, 'reason');
165
+ let result;
166
+ try {
167
+ result = await store.cancelExternalGoal({ goalId, reason });
168
+ }
169
+ catch (error) {
170
+ if (error instanceof ExternalGoalDeliveryError) {
171
+ sendDeliveryError(response, error);
172
+ return;
173
+ }
174
+ throw error;
175
+ }
176
+ sendJson(response, 202, {
177
+ cursor: result.cursor,
178
+ event: serializeEvent(result.event),
179
+ ok: true,
180
+ session: serializeSession(result.session),
181
+ status: result.status,
182
+ });
183
+ }),
184
+ ];
@@ -74,6 +74,11 @@ export const runtimeRoutes = [
74
74
  return;
75
75
  }
76
76
  requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
77
- sendJson(response, 200, serializeLiveAgentRun(store.getLiveRun(runId)));
77
+ const run = store.findLiveRun(runId);
78
+ if (!run) {
79
+ sendJson(response, 404, { error: 'Run not found' });
80
+ return;
81
+ }
82
+ sendJson(response, 200, serializeLiveAgentRun(run));
78
83
  }),
79
84
  ];
@@ -0,0 +1,2 @@
1
+ import type { RouteDefinition } from './route-types.js';
2
+ export declare const teamGoalRoutes: RouteDefinition[];
@@ -0,0 +1,60 @@
1
+ import { isExternalGoalReportStatus } from './external-goal-store.js';
2
+ import { BadRequestError } from './http-errors.js';
3
+ import { readJsonBody, route, sendJson } from './route-helpers.js';
4
+ import { authenticateCliAgent, requireCommandForRole } from './team-authz.js';
5
+ const BODY_MAX_CHARS = 40_000;
6
+ const requireNonEmptyString = (value, field, maxChars = BODY_MAX_CHARS) => {
7
+ if (typeof value !== 'string' || value.trim().length === 0) {
8
+ throw new BadRequestError(`Missing ${field}`);
9
+ }
10
+ if ([...value].length > maxChars) {
11
+ throw new BadRequestError(`${field} must be ${maxChars} characters or fewer`);
12
+ }
13
+ return value;
14
+ };
15
+ const getArtifacts = (value) => Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
16
+ export const teamGoalRoutes = [
17
+ route('POST', '/api/team/goal/report', async ({ request, response, store }) => {
18
+ const body = await readJsonBody(request);
19
+ const projectId = requireNonEmptyString(body.project_id, 'project_id', 200);
20
+ const fromAgentId = requireNonEmptyString(body.from_agent_id, 'from_agent_id', 200);
21
+ const goalId = requireNonEmptyString(body.goal_id, 'goal_id', 200);
22
+ const resultText = requireNonEmptyString(body.result, 'result');
23
+ if (!isExternalGoalReportStatus(body.status)) {
24
+ throw new BadRequestError('Invalid status; expected progress, done, blocked, or failed');
25
+ }
26
+ const agent = authenticateCliAgent({
27
+ fromAgentId,
28
+ getAgent: store.getAgent,
29
+ token: body.token,
30
+ validateToken: store.validateAgentToken,
31
+ workspaceId: projectId,
32
+ });
33
+ requireCommandForRole(agent, 'goal_report');
34
+ const result = store.reportExternalGoal({
35
+ artifacts: getArtifacts(body.artifacts),
36
+ body: resultText,
37
+ fromAgentId,
38
+ goalId,
39
+ status: body.status,
40
+ workspaceId: projectId,
41
+ });
42
+ sendJson(response, 202, {
43
+ cursor: result.cursor,
44
+ event: {
45
+ artifacts: result.event.artifacts,
46
+ body: result.event.body,
47
+ created_at: result.event.createdAt,
48
+ goal_id: result.event.goalId,
49
+ id: result.event.id,
50
+ kind: result.event.kind,
51
+ sequence: result.event.sequence,
52
+ status: result.event.status,
53
+ workspace_id: result.event.workspaceId,
54
+ },
55
+ goal_id: goalId,
56
+ ok: true,
57
+ status: result.status,
58
+ });
59
+ }),
60
+ ];
@@ -2,6 +2,7 @@ import { matchPath } from './route-helpers.js';
2
2
  import { actionCenterRoutes } from './routes-action-center.js';
3
3
  import { diagnosticsRoutes } from './routes-diagnostics.js';
4
4
  import { dispatchRoutes } from './routes-dispatches.js';
5
+ import { externalGoalRoutes } from './routes-external-goals.js';
5
6
  import { fsRoutes } from './routes-fs.js';
6
7
  import { marketplaceRoutes } from './routes-marketplace.js';
7
8
  import { openWorkspaceRoutes } from './routes-open-workspace.js';
@@ -11,6 +12,7 @@ import { scenarioRoutes } from './routes-scenarios.js';
11
12
  import { settingsRoutes } from './routes-settings.js';
12
13
  import { taskRoutes } from './routes-tasks.js';
13
14
  import { teamRoutes } from './routes-team.js';
15
+ import { teamGoalRoutes } from './routes-team-goals.js';
14
16
  import { teamMemoryRoutes } from './routes-team-memory.js';
15
17
  import { teamRecallRoutes } from './routes-team-recall.js';
16
18
  import { uiRoutes } from './routes-ui.js';
@@ -36,8 +38,10 @@ const routes = [
36
38
  ...workspaceMemoryDreamRoutes,
37
39
  ...workspaceMemoryRoutes,
38
40
  ...runtimeRoutes,
41
+ ...externalGoalRoutes,
39
42
  ...teamRecallRoutes,
40
43
  ...teamMemoryRoutes,
44
+ ...teamGoalRoutes,
41
45
  ...teamRoutes,
42
46
  ...fsRoutes,
43
47
  ...marketplaceRoutes,