@tt-a1i/hive 2.1.9 → 2.1.11

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 (63) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/src/cli/hive-mcp.d.ts +62 -0
  3. package/dist/src/cli/hive-mcp.js +253 -0
  4. package/dist/src/cli/hive.js +8 -0
  5. package/dist/src/cli/team.d.ts +8 -0
  6. package/dist/src/cli/team.js +100 -0
  7. package/dist/src/server/agent-runtime-contract.d.ts +1 -0
  8. package/dist/src/server/agent-runtime.js +4 -0
  9. package/dist/src/server/agent-startup-instructions.js +1 -1
  10. package/dist/src/server/agent-stdin-dispatcher.js +1 -0
  11. package/dist/src/server/diagnostics-support-bundle.d.ts +1 -1
  12. package/dist/src/server/external-goal-auth.d.ts +1 -0
  13. package/dist/src/server/external-goal-auth.js +1 -0
  14. package/dist/src/server/external-goal-bridge.d.ts +103 -0
  15. package/dist/src/server/external-goal-bridge.js +299 -0
  16. package/dist/src/server/external-goal-store.d.ts +59 -0
  17. package/dist/src/server/external-goal-store.js +165 -0
  18. package/dist/src/server/hive-team-guidance.js +1 -0
  19. package/dist/src/server/post-start-input-writer.js +52 -6
  20. package/dist/src/server/route-types.d.ts +30 -0
  21. package/dist/src/server/routes-external-goals.d.ts +2 -0
  22. package/dist/src/server/routes-external-goals.js +184 -0
  23. package/dist/src/server/routes-runtime.js +6 -1
  24. package/dist/src/server/routes-scenarios.d.ts +12 -17
  25. package/dist/src/server/routes-scenarios.js +41 -17
  26. package/dist/src/server/routes-team-goals.d.ts +2 -0
  27. package/dist/src/server/routes-team-goals.js +60 -0
  28. package/dist/src/server/routes.js +4 -0
  29. package/dist/src/server/runtime-store-contract.d.ts +11 -0
  30. package/dist/src/server/runtime-store-external-goals.d.ts +47 -0
  31. package/dist/src/server/runtime-store-external-goals.js +24 -0
  32. package/dist/src/server/runtime-store-helpers.d.ts +3 -0
  33. package/dist/src/server/runtime-store-helpers.js +11 -1
  34. package/dist/src/server/runtime-store.js +6 -0
  35. package/dist/src/server/scenario-presets.d.ts +7 -4
  36. package/dist/src/server/scenario-presets.js +16 -6
  37. package/dist/src/server/sqlite-schema-v37.d.ts +2 -0
  38. package/dist/src/server/sqlite-schema-v37.js +38 -0
  39. package/dist/src/server/sqlite-schema.d.ts +1 -1
  40. package/dist/src/server/sqlite-schema.js +41 -1
  41. package/dist/src/server/team-authz.d.ts +1 -1
  42. package/dist/src/server/team-authz.js +1 -0
  43. package/dist/src/server/ui-auth.d.ts +2 -0
  44. package/dist/src/server/ui-auth.js +7 -0
  45. package/dist/src/shared/random-worker-name.d.ts +9 -0
  46. package/dist/src/shared/random-worker-name.js +382 -0
  47. package/package.json +1 -1
  48. package/web/dist/assets/{AddWorkerDialog-QBN0m0eH.js → AddWorkerDialog-CMQdmZJQ.js} +2 -2
  49. package/web/dist/assets/{AddWorkspaceFlow-BJy-W2XI.js → AddWorkspaceFlow-arGy-LVX.js} +1 -1
  50. package/web/dist/assets/{FirstRunWizard-V-idMQB7.js → FirstRunWizard-PRHGFYry.js} +1 -1
  51. package/web/dist/assets/{MarketplaceDrawer-D4ldgvSM.js → MarketplaceDrawer-Dm-Z-YL8.js} +1 -1
  52. package/web/dist/assets/{TaskGraphDrawer-BSxRbr1h.js → TaskGraphDrawer-HaGRlpar.js} +1 -1
  53. package/web/dist/assets/{WhatsNewDialog-C-hpR8y4.js → WhatsNewDialog-9upxbs9b.js} +1 -1
  54. package/web/dist/assets/{WorkerModal-EuNxsCtr.js → WorkerModal-DAC0fORb.js} +1 -1
  55. package/web/dist/assets/{WorkflowsDrawer-CoT6kWcM.js → WorkflowsDrawer-Dqv4vNZW.js} +1 -1
  56. package/web/dist/assets/{WorkspaceMemoryDrawer-BXx4Z0kx.js → WorkspaceMemoryDrawer-QvVUM6qp.js} +1 -1
  57. package/web/dist/assets/{WorkspaceTaskDrawer-DKRRttxQ.js → WorkspaceTaskDrawer-CdiraIBo.js} +1 -1
  58. package/web/dist/assets/index-B1vvo0-M.js +84 -0
  59. package/web/dist/assets/{search-KbSLfFB8.js → search-DgMNPsfM.js} +1 -1
  60. package/web/dist/assets/{square-terminal-C89RIZhu.js → square-terminal-CiDDFh5Z.js} +1 -1
  61. package/web/dist/index.html +1 -1
  62. package/web/dist/sw.js +1 -1
  63. package/web/dist/assets/index-D43vHIy2.js +0 -84
@@ -4,6 +4,7 @@ const READY_CHECK_INTERVAL_MS = 50;
4
4
  const READY_TIMEOUT_MS = 3000;
5
5
  const PROMPT_READY_HARD_TIMEOUT_MS = 30000;
6
6
  const PROMPT_READY_TAIL_CHARS = 8000;
7
+ const OPENCODE_COMPLETION_READY_SETTLE_MS = 1000;
7
8
  const MIN_SUBMIT_AFTER_PASTE_DELAY_MS = 600;
8
9
  const MAX_SUBMIT_AFTER_PASTE_DELAY_MS = 1500;
9
10
  const PASTE_CHARS_PER_DELAY_MS = 4;
@@ -47,6 +48,9 @@ const ESCAPE = String.fromCharCode(27);
47
48
  const BELL = String.fromCharCode(7);
48
49
  const TERMINAL_CONTROL_PATTERN = new RegExp(`${ESCAPE}\\[[0-?]*[ -/]*[@-~]|${ESCAPE}\\][^${BELL}${ESCAPE}]*(?:${BELL}|${ESCAPE}\\\\)`, 'gu');
49
50
  const HIVE_ACK_GATED_MESSAGE_PATTERN = /<(?:hive-system-message\b|hive-message\s+kind="(?:cancel|dispatch|heartbeat|startup)"(?:\s|>))/u;
51
+ const OPENCODE_VISIBLE_PROMPT_PATTERN = /\bAsk anything\.\.\./u;
52
+ const OPENCODE_COMPLETED_TURN_FOOTER_PATTERN = /^▣\s+[^·\n]+·\s+\S.*\s+·\s+\d+(?:\.\d+)?(?:ms|s)$/u;
53
+ const OPENCODE_INTERRUPT_STATUS_PATTERN = /\besc\s+interrupt\b/iu;
50
54
  export const toBracketedPasteSubmission = (text) => `\u001b[200~${text}\u001b[201~`;
51
55
  const toError = (error) => (error instanceof Error ? error : new Error(String(error)));
52
56
  const createRunInactiveError = (runId) => new Error(`Run became inactive before input was submitted: ${runId}`);
@@ -70,12 +74,15 @@ export const isInteractiveAgentCommand = (command) => {
70
74
  };
71
75
  const getPlainTerminalOutput = (output) => output.replace(/\r/g, '\n').replace(TERMINAL_CONTROL_PATTERN, '');
72
76
  const getPlainTerminalTail = (output) => getPlainTerminalOutput(output).slice(-PROMPT_READY_TAIL_CHARS);
77
+ const getRecentNonEmptyTerminalLines = (output) => getPlainTerminalTail(output)
78
+ .split('\n')
79
+ .map((line) => line.trim())
80
+ .filter(Boolean);
73
81
  const hasGeminiPromptReady = (output) => /\bType your message\b/u.test(output);
74
82
  const hasHermesPromptReady = (output) => {
75
83
  const plain = getPlainTerminalTail(output);
76
84
  return /\bWelcome to Hermes Agent![\s\S]*❯/u.test(plain);
77
85
  };
78
- const hasOpencodePromptReady = (output) => /\bAsk anything\.\.\./u.test(getPlainTerminalTail(output));
79
86
  const hasGrokPromptReady = (output) => /\b(?:Enter:send|Composer\s+\S+)/u.test(output);
80
87
  const hasAgyPromptReady = (output) => /(?:^|\n)\s*>\s*\n\s*(?:[─-]{8,}|\?\s*for shortcuts)/u.test(getPlainTerminalOutput(output));
81
88
  const getLastNonEmptyTerminalLine = (output) => {
@@ -110,15 +117,36 @@ export const hasFirstRunSetupPrompt = (output) => {
110
117
  const recent = recentLines.join('\n');
111
118
  return SETUP_PROMPT_PATTERNS.some((pattern) => pattern.test(recent));
112
119
  };
113
- export const hasInteractivePromptReady = (output, command = '') => {
120
+ const getOpencodePromptReadyState = (output) => {
121
+ const recentLines = getRecentNonEmptyTerminalLines(output);
122
+ for (let index = recentLines.length - 1; index >= 0; index--) {
123
+ const line = recentLines[index];
124
+ if (line === undefined)
125
+ continue;
126
+ if (OPENCODE_INTERRUPT_STATUS_PATTERN.test(line))
127
+ return 'ready';
128
+ if (OPENCODE_VISIBLE_PROMPT_PATTERN.test(line))
129
+ return 'ready';
130
+ if (OPENCODE_COMPLETED_TURN_FOOTER_PATTERN.test(line))
131
+ return 'ready-after-settle';
132
+ }
133
+ return 'not-ready';
134
+ };
135
+ const getInteractivePromptReadyState = (output, command = '') => {
114
136
  const commandName = getCommandName(command);
115
- return (hasBarePromptLine(output) ||
137
+ if (hasBarePromptLine(output) ||
116
138
  (commandName === 'agy' && hasAgyPromptReady(output)) ||
117
139
  (commandName === 'grok' && hasGrokPromptReady(output)) ||
118
140
  (commandName === 'hermes' && hasHermesPromptReady(output)) ||
119
- ((commandName === 'gemini' || commandName === 'qwen') && hasGeminiPromptReady(output)) ||
120
- (commandName === 'opencode' && hasOpencodePromptReady(output)));
141
+ ((commandName === 'gemini' || commandName === 'qwen') && hasGeminiPromptReady(output))) {
142
+ return 'ready';
143
+ }
144
+ if (commandName === 'opencode') {
145
+ return getOpencodePromptReadyState(output);
146
+ }
147
+ return 'not-ready';
121
148
  };
149
+ export const hasInteractivePromptReady = (output, command = '') => getInteractivePromptReadyState(output, command) !== 'not-ready';
122
150
  export const hasBracketedPasteAcknowledgement = (output, baselineLength) => {
123
151
  const outputAfterPaste = output.slice(baselineLength);
124
152
  const pasteEndIndex = outputAfterPaste.lastIndexOf(BRACKETED_PASTE_END);
@@ -282,6 +310,24 @@ export const createPostStartInputWriter = (agentManager, command) => {
282
310
  });
283
311
  const startedAt = Date.now();
284
312
  let isInitialAttempt = true;
313
+ let settleCandidate = null;
314
+ const isReadyToWrite = (output) => {
315
+ const promptReadyState = getInteractivePromptReadyState(output, command);
316
+ if (promptReadyState === 'ready') {
317
+ settleCandidate = null;
318
+ return true;
319
+ }
320
+ if (promptReadyState !== 'ready-after-settle') {
321
+ settleCandidate = null;
322
+ return false;
323
+ }
324
+ const tail = getPlainTerminalTail(output);
325
+ if (!settleCandidate || settleCandidate.tail !== tail) {
326
+ settleCandidate = { since: Date.now(), tail };
327
+ return false;
328
+ }
329
+ return Date.now() - settleCandidate.since >= OPENCODE_COMPLETION_READY_SETTLE_MS;
330
+ };
285
331
  const tryWrite = () => {
286
332
  let output;
287
333
  try {
@@ -297,7 +343,7 @@ export const createPostStartInputWriter = (agentManager, command) => {
297
343
  return;
298
344
  }
299
345
  const firstRunSetupPromptVisible = hasFirstRunSetupPrompt(output);
300
- if ((!firstRunSetupPromptVisible && hasInteractivePromptReady(output, command)) ||
346
+ if ((!firstRunSetupPromptVisible && isReadyToWrite(output)) ||
301
347
  (!firstRunSetupPromptVisible &&
302
348
  canTimeoutBeforePromptReady(command) &&
303
349
  Date.now() - startedAt >= READY_TIMEOUT_MS)) {
@@ -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
  ];
@@ -4,23 +4,18 @@ import type { RuntimeStore } from './runtime-store-contract.js';
4
4
  import { type ScenarioPreset } from './scenario-presets.js';
5
5
  import { type CliAvailabilityProbe } from './spawn-cli-resolver.js';
6
6
  import { type UiLanguage } from './workspace-ui-language.js';
7
- export declare const scenarioRoutes: RouteDefinition[];
8
- /**
9
- * Materialize the scenario team and hand the goal to the orchestrator.
10
- * Exported for direct testing (the route harness has no PTY, so the
11
- * active-run gate above would mask this logic).
12
- *
13
- * Workers derive a FRESH launch config from the orchestrator's CLI brand via
14
- * the same resolver `team spawn` uses — never a clone of the orchestrator's
15
- * own config, which may carry session-resume arguments that would make every
16
- * worker resume the orchestrator's session.
17
- *
18
- * Worker creation is sequential and NOT transactional across workers: each
19
- * addWorkerWithLaunch is atomic, but a mid-loop failure (e.g. a name race)
20
- * leaves earlier workers in place and surfaces the error to the caller.
21
- */
22
- export declare const applyScenario: (store: RuntimeStore, workspaceId: string, scenario: ScenarioPreset, goal: string, isCommandAvailable?: CliAvailabilityProbe, language?: UiLanguage) => Array<{
7
+ type CreatedScenarioWorker = {
23
8
  id: string;
24
9
  name: string;
25
10
  role: WorkerRole;
26
- }>;
11
+ };
12
+ type StartedScenarioWorker = CreatedScenarioWorker & {
13
+ start: {
14
+ ok: true;
15
+ run_id: string;
16
+ };
17
+ };
18
+ export declare const scenarioRoutes: RouteDefinition[];
19
+ export declare const applyScenario: (store: RuntimeStore, workspaceId: string, scenario: ScenarioPreset, goal: string, isCommandAvailable?: CliAvailabilityProbe, language?: UiLanguage) => CreatedScenarioWorker[];
20
+ export declare const applyAndStartScenario: (store: RuntimeStore, workspaceId: string, scenario: ScenarioPreset, goal: string, hivePort: string, isCommandAvailable?: CliAvailabilityProbe, language?: UiLanguage) => Promise<StartedScenarioWorker[]>;
21
+ export {};
@@ -5,12 +5,13 @@ import { resolveDefaultSpawnCliLaunchConfig, } from './spawn-cli-resolver.js';
5
5
  import { requireUiTokenFromRequest } from './ui-auth-helpers.js';
6
6
  import { getOrchestratorId } from './workspace-store-support.js';
7
7
  import { resolveWorkspaceUiLanguage, writeWorkspaceUiLanguage, } from './workspace-ui-language.js';
8
+ const getRuntimePort = (request) => String(request.socket.localPort ?? '');
8
9
  export const scenarioRoutes = [
9
10
  /**
10
- * One-click team assembly: materialize a scenario preset's workers (each
11
- * inheriting the orchestrator's launch config) and hand the user's goal to
12
- * the orchestrator as a normal user message. Deliberately NOT dispatching —
13
- * splitting the goal and calling `team send` stays the orchestrator's job.
11
+ * One-click team assembly: materialize a scenario preset's members, start
12
+ * their PTYs, then hand the user's goal to the orchestrator as a normal user
13
+ * message. Deliberately NOT dispatching — splitting the goal and calling
14
+ * `team send` stays the orchestrator's job.
14
15
  */
15
16
  route('POST', '/api/workspaces/:workspaceId/scenarios/:scenarioId/apply', async ({ params, request, response, store }) => {
16
17
  const workspaceId = getRequiredParam(response, params, 'workspaceId', 'Workspace id and scenario id are required');
@@ -35,13 +36,12 @@ export const scenarioRoutes = [
35
36
  }
36
37
  const language = resolveWorkspaceUiLanguage(store.settings, workspaceId, body.locale);
37
38
  writeWorkspaceUiLanguage(store.settings, workspaceId, language);
38
- // The kickoff is delivered to the orchestrator's live terminal and is NOT
39
- // persisted for replay, so an offline orchestrator would silently lose the
40
- // goal. Gate up front: no live run, no team assembly.
39
+ // The kickoff must land in the orchestrator's live terminal. Gate up
40
+ // front so a stopped orchestrator never gets a half-created scenario.
41
41
  if (!store.getActiveRunByAgentId(workspaceId, getOrchestratorId(workspaceId))) {
42
42
  throw new ConflictError('Start the Orchestrator first — the scenario goal is handed to its terminal');
43
43
  }
44
- const created = applyScenario(store, workspaceId, scenario, goal, undefined, language);
44
+ const created = await applyAndStartScenario(store, workspaceId, scenario, goal, getRuntimePort(request), undefined, language);
45
45
  sendJson(response, 201, {
46
46
  created_workers: created,
47
47
  injected: true,
@@ -51,7 +51,7 @@ export const scenarioRoutes = [
51
51
  /**
52
52
  * Materialize the scenario team and hand the goal to the orchestrator.
53
53
  * Exported for direct testing (the route harness has no PTY, so the
54
- * active-run gate above would mask this logic).
54
+ * active-run/start behavior would mask this logic).
55
55
  *
56
56
  * Workers derive a FRESH launch config from the orchestrator's CLI brand via
57
57
  * the same resolver `team spawn` uses — never a clone of the orchestrator's
@@ -62,7 +62,7 @@ export const scenarioRoutes = [
62
62
  * addWorkerWithLaunch is atomic, but a mid-loop failure (e.g. a name race)
63
63
  * leaves earlier workers in place and surfaces the error to the caller.
64
64
  */
65
- export const applyScenario = (store, workspaceId, scenario, goal, isCommandAvailable, language = 'en') => {
65
+ const createScenarioWorkers = (store, workspaceId, scenario, isCommandAvailable, language = 'en') => {
66
66
  const ports = {
67
67
  getCommandPreset: (id) => store.settings.getCommandPreset(id),
68
68
  getOrchestratorLaunchConfig: () => store.peekAgentLaunchConfig(workspaceId, getOrchestratorId(workspaceId)),
@@ -72,7 +72,7 @@ export const applyScenario = (store, workspaceId, scenario, goal, isCommandAvail
72
72
  const takenNames = new Set(store.listWorkers(workspaceId).map((worker) => worker.name));
73
73
  const created = [];
74
74
  for (const spec of scenario.workers) {
75
- const name = buildScenarioWorkerName(spec, (candidate) => takenNames.has(candidate));
75
+ const name = buildScenarioWorkerName(spec, takenNames, { language });
76
76
  takenNames.add(name);
77
77
  const description = spec.descriptionOverride !== undefined
78
78
  ? getScenarioWorkerDescription(spec, language)
@@ -84,11 +84,35 @@ export const applyScenario = (store, workspaceId, scenario, goal, isCommandAvail
84
84
  }, launchConfig);
85
85
  created.push({ id: worker.id, name: worker.name, role: spec.role });
86
86
  }
87
- store.recordUserInput(workspaceId, getOrchestratorId(workspaceId), buildScenarioKickoffMessage({
88
- scenarioId: scenario.id,
89
- goal,
90
- language,
91
- workers: created.map((worker) => ({ name: worker.name, role: worker.role })),
92
- }));
93
87
  return created;
94
88
  };
89
+ const deliverScenarioKickoff = async (store, workspaceId, scenario, goal, language, created) => {
90
+ await store.deliverUserInput(workspaceId, getOrchestratorId(workspaceId), buildScenarioKickoffText(scenario, goal, language, created));
91
+ };
92
+ const buildScenarioKickoffText = (scenario, goal, language, created) => buildScenarioKickoffMessage({
93
+ scenarioId: scenario.id,
94
+ goal,
95
+ language,
96
+ workers: created.map((worker) => ({ name: worker.name, role: worker.role })),
97
+ });
98
+ const recordScenarioKickoff = (store, workspaceId, scenario, goal, language, created) => {
99
+ store.recordUserInput(workspaceId, getOrchestratorId(workspaceId), buildScenarioKickoffText(scenario, goal, language, created));
100
+ };
101
+ export const applyScenario = (store, workspaceId, scenario, goal, isCommandAvailable, language = 'en') => {
102
+ const created = createScenarioWorkers(store, workspaceId, scenario, isCommandAvailable, language);
103
+ recordScenarioKickoff(store, workspaceId, scenario, goal, language, created);
104
+ return created;
105
+ };
106
+ const startScenarioWorkers = async (store, workspaceId, hivePort, created) => Promise.all(created.map(async (worker) => {
107
+ const run = await store.startAgent(workspaceId, worker.id, { hivePort });
108
+ if (run.status === 'error') {
109
+ throw new ConflictError(`Failed to start scenario member: ${worker.name}`);
110
+ }
111
+ return { ...worker, start: { ok: true, run_id: run.runId } };
112
+ }));
113
+ export const applyAndStartScenario = async (store, workspaceId, scenario, goal, hivePort, isCommandAvailable, language = 'en') => {
114
+ const created = createScenarioWorkers(store, workspaceId, scenario, isCommandAvailable, language);
115
+ const started = await startScenarioWorkers(store, workspaceId, hivePort, created);
116
+ await deliverScenarioKickoff(store, workspaceId, scenario, goal, language, created);
117
+ return started;
118
+ };
@@ -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,
@@ -4,6 +4,7 @@ import type { AgentManager } from './agent-manager.js';
4
4
  import type { AgentLaunchConfigInput, PersistedAgentRun } from './agent-run-store.js';
5
5
  import type { LiveAgentRun } from './agent-runtime-types.js';
6
6
  import type { DispatchRecord, ListDispatchesOptions } from './dispatch-ledger-store.js';
7
+ import type { ExternalGoalBridge, ExternalGoalCancelInput, ExternalGoalContinueInput, ExternalGoalReportInput, ExternalGoalStartInput, ExternalGoalWaitInput } from './external-goal-bridge.js';
7
8
  import type { RecoveryMessage } from './message-log-store.js';
8
9
  import type { PtyOutputBus } from './pty-output-bus.js';
9
10
  import type { RemoteAuditStore } from './remote-audit-store.js';
@@ -50,6 +51,13 @@ export interface RuntimeStore {
50
51
  listDispatches: (workspaceId: string, options?: ListDispatchesOptions) => DispatchRecord[];
51
52
  listOpenDispatches: (workspaceId: string) => DispatchRecord[];
52
53
  listRecentDispatches: (workspaceId: string, limit?: number) => DispatchRecord[];
54
+ listExternalGoalWorkspaces: ExternalGoalBridge['listWorkspaces'];
55
+ inspectExternalGoalWorkspace: ExternalGoalBridge['inspectWorkspace'];
56
+ startExternalGoal: (input: ExternalGoalStartInput) => ReturnType<ExternalGoalBridge['startGoal']>;
57
+ continueExternalGoal: (input: ExternalGoalContinueInput) => ReturnType<ExternalGoalBridge['continueGoal']>;
58
+ reportExternalGoal: (input: ExternalGoalReportInput) => ReturnType<ExternalGoalBridge['reportGoal']>;
59
+ waitExternalGoal: (input: ExternalGoalWaitInput) => ReturnType<ExternalGoalBridge['waitGoal']>;
60
+ cancelExternalGoal: (input: ExternalGoalCancelInput) => ReturnType<ExternalGoalBridge['cancelGoal']>;
53
61
  listWorkers: (workspaceId: string) => TeamListItem[];
54
62
  getLastPtyLineForAgent: (workspaceId: string, agentId: string) => string | null;
55
63
  getWorkspaceSnapshot: (workspaceId: string) => WorkspaceRecord;
@@ -70,6 +78,7 @@ export interface RuntimeStore {
70
78
  workspace_id: string;
71
79
  }>>;
72
80
  startWorkspaceWatch: (workspaceId: string) => Promise<void>;
81
+ findLiveRun: (runId: string) => LiveAgentRun | undefined;
73
82
  getLiveRun: (runId: string) => LiveAgentRun;
74
83
  waitForRunExit: (runId: string, timeoutMs: number) => Promise<boolean>;
75
84
  getActiveRunByAgentId: (workspaceId: string, agentId: string) => LiveAgentRun | undefined;
@@ -102,9 +111,11 @@ export interface RuntimeStore {
102
111
  settings: SettingsStore;
103
112
  writeRunInput: (runId: string, input: Buffer | string) => void;
104
113
  getUiToken: () => string;
114
+ getSupervisorToken: () => string;
105
115
  stopAgentRun: (runId: string) => void;
106
116
  validateAgentToken: (agentId: string, token: string | undefined) => boolean;
107
117
  validateUiToken: (token: string | undefined) => boolean;
118
+ validateSupervisorToken: (token: string | undefined) => boolean;
108
119
  authorizeRemoteTunnelRequest: (request: IncomingMessage) => boolean;
109
120
  getRemoteTunnelSecret: () => string;
110
121
  getRemoteAuditStore: () => RemoteAuditStore;