@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.
- package/CHANGELOG.md +35 -0
- package/dist/src/cli/hive-mcp.d.ts +62 -0
- package/dist/src/cli/hive-mcp.js +253 -0
- package/dist/src/cli/hive.js +8 -0
- package/dist/src/cli/team.d.ts +8 -0
- package/dist/src/cli/team.js +100 -0
- package/dist/src/server/agent-runtime-contract.d.ts +1 -0
- package/dist/src/server/agent-runtime.js +4 -0
- package/dist/src/server/agent-startup-instructions.js +1 -1
- package/dist/src/server/agent-stdin-dispatcher.js +1 -0
- package/dist/src/server/diagnostics-support-bundle.d.ts +1 -1
- package/dist/src/server/external-goal-auth.d.ts +1 -0
- package/dist/src/server/external-goal-auth.js +1 -0
- package/dist/src/server/external-goal-bridge.d.ts +103 -0
- package/dist/src/server/external-goal-bridge.js +299 -0
- package/dist/src/server/external-goal-store.d.ts +59 -0
- package/dist/src/server/external-goal-store.js +165 -0
- package/dist/src/server/hive-team-guidance.js +1 -0
- package/dist/src/server/post-start-input-writer.js +52 -6
- package/dist/src/server/route-types.d.ts +30 -0
- package/dist/src/server/routes-external-goals.d.ts +2 -0
- package/dist/src/server/routes-external-goals.js +184 -0
- package/dist/src/server/routes-runtime.js +6 -1
- package/dist/src/server/routes-scenarios.d.ts +12 -17
- package/dist/src/server/routes-scenarios.js +41 -17
- package/dist/src/server/routes-team-goals.d.ts +2 -0
- package/dist/src/server/routes-team-goals.js +60 -0
- package/dist/src/server/routes.js +4 -0
- package/dist/src/server/runtime-store-contract.d.ts +11 -0
- package/dist/src/server/runtime-store-external-goals.d.ts +47 -0
- package/dist/src/server/runtime-store-external-goals.js +24 -0
- package/dist/src/server/runtime-store-helpers.d.ts +3 -0
- package/dist/src/server/runtime-store-helpers.js +11 -1
- package/dist/src/server/runtime-store.js +6 -0
- package/dist/src/server/scenario-presets.d.ts +7 -4
- package/dist/src/server/scenario-presets.js +16 -6
- package/dist/src/server/sqlite-schema-v37.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v37.js +38 -0
- package/dist/src/server/sqlite-schema.d.ts +1 -1
- package/dist/src/server/sqlite-schema.js +41 -1
- package/dist/src/server/team-authz.d.ts +1 -1
- package/dist/src/server/team-authz.js +1 -0
- package/dist/src/server/ui-auth.d.ts +2 -0
- package/dist/src/server/ui-auth.js +7 -0
- package/dist/src/shared/random-worker-name.d.ts +9 -0
- package/dist/src/shared/random-worker-name.js +382 -0
- package/package.json +1 -1
- package/web/dist/assets/{AddWorkerDialog-QBN0m0eH.js → AddWorkerDialog-CMQdmZJQ.js} +2 -2
- package/web/dist/assets/{AddWorkspaceFlow-BJy-W2XI.js → AddWorkspaceFlow-arGy-LVX.js} +1 -1
- package/web/dist/assets/{FirstRunWizard-V-idMQB7.js → FirstRunWizard-PRHGFYry.js} +1 -1
- package/web/dist/assets/{MarketplaceDrawer-D4ldgvSM.js → MarketplaceDrawer-Dm-Z-YL8.js} +1 -1
- package/web/dist/assets/{TaskGraphDrawer-BSxRbr1h.js → TaskGraphDrawer-HaGRlpar.js} +1 -1
- package/web/dist/assets/{WhatsNewDialog-C-hpR8y4.js → WhatsNewDialog-9upxbs9b.js} +1 -1
- package/web/dist/assets/{WorkerModal-EuNxsCtr.js → WorkerModal-DAC0fORb.js} +1 -1
- package/web/dist/assets/{WorkflowsDrawer-CoT6kWcM.js → WorkflowsDrawer-Dqv4vNZW.js} +1 -1
- package/web/dist/assets/{WorkspaceMemoryDrawer-BXx4Z0kx.js → WorkspaceMemoryDrawer-QvVUM6qp.js} +1 -1
- package/web/dist/assets/{WorkspaceTaskDrawer-DKRRttxQ.js → WorkspaceTaskDrawer-CdiraIBo.js} +1 -1
- package/web/dist/assets/index-B1vvo0-M.js +84 -0
- package/web/dist/assets/{search-KbSLfFB8.js → search-DgMNPsfM.js} +1 -1
- package/web/dist/assets/{square-terminal-C89RIZhu.js → square-terminal-CiDDFh5Z.js} +1 -1
- package/web/dist/index.html +1 -1
- package/web/dist/sw.js +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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,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
|
-
|
|
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
|
-
|
|
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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
39
|
-
//
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,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;
|