@tt-a1i/hive 2.1.10 → 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 (56) hide show
  1. package/CHANGELOG.md +18 -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/diagnostics-support-bundle.d.ts +1 -1
  11. package/dist/src/server/external-goal-auth.d.ts +1 -0
  12. package/dist/src/server/external-goal-auth.js +1 -0
  13. package/dist/src/server/external-goal-bridge.d.ts +103 -0
  14. package/dist/src/server/external-goal-bridge.js +299 -0
  15. package/dist/src/server/external-goal-store.d.ts +59 -0
  16. package/dist/src/server/external-goal-store.js +165 -0
  17. package/dist/src/server/hive-team-guidance.js +1 -0
  18. package/dist/src/server/post-start-input-writer.js +23 -6
  19. package/dist/src/server/route-types.d.ts +30 -0
  20. package/dist/src/server/routes-external-goals.d.ts +2 -0
  21. package/dist/src/server/routes-external-goals.js +184 -0
  22. package/dist/src/server/routes-runtime.js +6 -1
  23. package/dist/src/server/routes-team-goals.d.ts +2 -0
  24. package/dist/src/server/routes-team-goals.js +60 -0
  25. package/dist/src/server/routes.js +4 -0
  26. package/dist/src/server/runtime-store-contract.d.ts +11 -0
  27. package/dist/src/server/runtime-store-external-goals.d.ts +47 -0
  28. package/dist/src/server/runtime-store-external-goals.js +24 -0
  29. package/dist/src/server/runtime-store-helpers.d.ts +3 -0
  30. package/dist/src/server/runtime-store-helpers.js +11 -1
  31. package/dist/src/server/runtime-store.js +6 -0
  32. package/dist/src/server/sqlite-schema-v37.d.ts +2 -0
  33. package/dist/src/server/sqlite-schema-v37.js +38 -0
  34. package/dist/src/server/sqlite-schema.d.ts +1 -1
  35. package/dist/src/server/sqlite-schema.js +41 -1
  36. package/dist/src/server/team-authz.d.ts +1 -1
  37. package/dist/src/server/team-authz.js +1 -0
  38. package/dist/src/server/ui-auth.d.ts +2 -0
  39. package/dist/src/server/ui-auth.js +7 -0
  40. package/package.json +1 -1
  41. package/web/dist/assets/{AddWorkerDialog-Dc0slXtx.js → AddWorkerDialog-CMQdmZJQ.js} +2 -2
  42. package/web/dist/assets/{AddWorkspaceFlow-AyzSdx5w.js → AddWorkspaceFlow-arGy-LVX.js} +1 -1
  43. package/web/dist/assets/{FirstRunWizard-DGB5zWhl.js → FirstRunWizard-PRHGFYry.js} +1 -1
  44. package/web/dist/assets/{MarketplaceDrawer-DAZ-crqq.js → MarketplaceDrawer-Dm-Z-YL8.js} +1 -1
  45. package/web/dist/assets/{TaskGraphDrawer-DYk-0uKD.js → TaskGraphDrawer-HaGRlpar.js} +1 -1
  46. package/web/dist/assets/{WhatsNewDialog-B5tu3MV2.js → WhatsNewDialog-9upxbs9b.js} +1 -1
  47. package/web/dist/assets/{WorkerModal-C73K7SUg.js → WorkerModal-DAC0fORb.js} +1 -1
  48. package/web/dist/assets/{WorkflowsDrawer-mrSAPqAh.js → WorkflowsDrawer-Dqv4vNZW.js} +1 -1
  49. package/web/dist/assets/{WorkspaceMemoryDrawer-CCz3HB4F.js → WorkspaceMemoryDrawer-QvVUM6qp.js} +1 -1
  50. package/web/dist/assets/{WorkspaceTaskDrawer-C7JzoRp8.js → WorkspaceTaskDrawer-CdiraIBo.js} +1 -1
  51. package/web/dist/assets/index-B1vvo0-M.js +84 -0
  52. package/web/dist/assets/{search-Bx1xfpzr.js → search-DgMNPsfM.js} +1 -1
  53. package/web/dist/assets/{square-terminal-Dfr9QC1I.js → square-terminal-CiDDFh5Z.js} +1 -1
  54. package/web/dist/index.html +1 -1
  55. package/web/dist/sw.js +1 -1
  56. package/web/dist/assets/index-CVQc8PLJ.js +0 -84
@@ -0,0 +1,299 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { escapeHiveEnvelopeAttribute, escapeHiveEnvelopeText } from './hive-envelope-escape.js';
3
+ import { BadRequestError, ConflictError, ForbiddenError, HttpError, PtyInactiveError, } from './http-errors.js';
4
+ import { serializeTeamListItem } from './team-list-serializer.js';
5
+ import { getOrchestratorId } from './workspace-store-support.js';
6
+ const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
7
+ const MAX_WAIT_TIMEOUT_MS = 120_000;
8
+ export class ExternalGoalDeliveryError extends PtyInactiveError {
9
+ cursor;
10
+ goalId;
11
+ status;
12
+ constructor(input) {
13
+ super(input.message);
14
+ this.name = 'ExternalGoalDeliveryError';
15
+ this.cursor = input.cursor;
16
+ this.goalId = input.goalId;
17
+ this.status = input.status;
18
+ }
19
+ }
20
+ const reportEventKind = (status) => {
21
+ if (status === 'progress')
22
+ return 'progress_reported';
23
+ if (status === 'done')
24
+ return 'goal_done';
25
+ if (status === 'blocked')
26
+ return 'goal_blocked';
27
+ return 'goal_failed';
28
+ };
29
+ const reportSessionStatus = (status) => {
30
+ if (status === 'progress')
31
+ return 'in_progress';
32
+ return status;
33
+ };
34
+ const closedForReport = new Set([
35
+ 'blocked',
36
+ 'done',
37
+ 'failed',
38
+ 'cancelled',
39
+ ]);
40
+ const boundedTimeout = (value) => {
41
+ if (value === undefined)
42
+ return DEFAULT_WAIT_TIMEOUT_MS;
43
+ if (!Number.isFinite(value) || value < 0) {
44
+ throw new BadRequestError('timeout_ms must be a non-negative number');
45
+ }
46
+ return Math.min(Math.floor(value), MAX_WAIT_TIMEOUT_MS);
47
+ };
48
+ const normalizedCursor = (value) => {
49
+ if (value === undefined)
50
+ return 0;
51
+ if (!Number.isInteger(value) || value < 0) {
52
+ throw new BadRequestError('cursor must be a non-negative integer');
53
+ }
54
+ return value;
55
+ };
56
+ const requireSession = (goalStore, goalId) => {
57
+ const session = goalStore.getSession(goalId);
58
+ if (!session)
59
+ throw new HttpError(404, `External goal not found: ${goalId}`);
60
+ return session;
61
+ };
62
+ const contextLines = (context) => {
63
+ if (context === undefined || context === null)
64
+ return [];
65
+ const body = typeof context === 'string' ? context : JSON.stringify(context, null, 2);
66
+ if (!body.trim())
67
+ return [];
68
+ return ['', 'Context:', escapeHiveEnvelopeText(body)];
69
+ };
70
+ const buildExternalGoalPayload = (session) => [
71
+ `<hive-message kind="external-goal" source="${escapeHiveEnvelopeAttribute(session.source)}" goal_id="${escapeHiveEnvelopeAttribute(session.id)}">`,
72
+ '',
73
+ 'You received an external Supervisor goal.',
74
+ '',
75
+ 'Rules:',
76
+ '- You are still the Hive Orchestrator for this workspace.',
77
+ '- Use current Hive members through `team list` and `team send`.',
78
+ '- Do not report final completion to the user directly; report to the external Supervisor with:',
79
+ ` team goal report --goal ${session.id} --status done --stdin`,
80
+ '- Use status `progress` for meaningful phase updates.',
81
+ '- Use status `blocked` when you need an external decision.',
82
+ '',
83
+ 'Goal:',
84
+ escapeHiveEnvelopeText(session.goal),
85
+ ...contextLines(session.context),
86
+ '</hive-message>',
87
+ '',
88
+ ].join('\n');
89
+ const buildExternalGoalContinuePayload = (session, message, context) => [
90
+ `<hive-message kind="external-goal-continue" goal_id="${escapeHiveEnvelopeAttribute(session.id)}">`,
91
+ '',
92
+ 'The external Supervisor added context for this goal.',
93
+ '',
94
+ 'Continue coordinating through Hive members as needed. Report meaningful updates with:',
95
+ ` team goal report --goal ${session.id} --status progress --stdin`,
96
+ '',
97
+ 'Message:',
98
+ escapeHiveEnvelopeText(message),
99
+ ...contextLines(context),
100
+ '</hive-message>',
101
+ '',
102
+ ].join('\n');
103
+ const buildExternalGoalCancelPayload = (session, reason) => [
104
+ `<hive-message kind="external-goal-cancel" goal_id="${escapeHiveEnvelopeAttribute(session.id)}">`,
105
+ '',
106
+ 'Stop coordinating this external goal. Do not send more goal reports for it.',
107
+ '',
108
+ 'MVP note: Hive did not automatically cancel member dispatches. Decide whether any open dispatches should be cancelled with `team cancel`.',
109
+ '',
110
+ 'Reason:',
111
+ escapeHiveEnvelopeText(reason),
112
+ '</hive-message>',
113
+ '',
114
+ ].join('\n');
115
+ export const createExternalGoalBridge = ({ deliverToOrchestrator, getActiveRunByAgentId, getAgent, getWorkspaceSnapshot, goalStore, listWorkers, listWorkspaces, }) => {
116
+ const events = new EventEmitter();
117
+ events.setMaxListeners(200);
118
+ const notify = (goalId) => events.emit(goalId);
119
+ const waitForNewEvent = (goalId, timeoutMs) => {
120
+ if (timeoutMs <= 0)
121
+ return Promise.resolve();
122
+ return new Promise((resolve) => {
123
+ const onEvent = () => {
124
+ clearTimeout(timer);
125
+ resolve();
126
+ };
127
+ const timer = setTimeout(() => {
128
+ events.off(goalId, onEvent);
129
+ resolve();
130
+ }, timeoutMs);
131
+ timer.unref?.();
132
+ events.once(goalId, onEvent);
133
+ });
134
+ };
135
+ const appendAndNotify = (input) => {
136
+ const event = goalStore.appendEvent(input);
137
+ notify(input.goalId);
138
+ return event;
139
+ };
140
+ const deliverOrMarkFailed = async (session, text) => {
141
+ try {
142
+ await deliverToOrchestrator(session.workspaceId, text);
143
+ }
144
+ catch (error) {
145
+ const event = appendAndNotify({
146
+ body: error instanceof Error ? error.message : String(error),
147
+ goalId: session.id,
148
+ kind: 'delivery_failed',
149
+ sessionStatus: 'failed',
150
+ status: 'failed',
151
+ });
152
+ throw new ExternalGoalDeliveryError({
153
+ cursor: event.sequence,
154
+ goalId: session.id,
155
+ message: `orchestrator_not_running: could not deliver external goal ${session.id}`,
156
+ status: 'failed',
157
+ });
158
+ }
159
+ };
160
+ return {
161
+ listWorkspaces,
162
+ inspectWorkspace(input) {
163
+ const snapshot = getWorkspaceSnapshot(input.workspaceId);
164
+ const orchestratorId = getOrchestratorId(input.workspaceId);
165
+ const orchestrator = getAgent(input.workspaceId, orchestratorId);
166
+ return {
167
+ workspace: snapshot.summary,
168
+ orchestrator: {
169
+ id: orchestrator.id,
170
+ name: orchestrator.name,
171
+ status: orchestrator.status,
172
+ active_run: Boolean(getActiveRunByAgentId(input.workspaceId, orchestrator.id)),
173
+ },
174
+ members: listWorkers(input.workspaceId).map((worker) => serializeTeamListItem(worker)),
175
+ };
176
+ },
177
+ async startGoal(input) {
178
+ getWorkspaceSnapshot(input.workspaceId);
179
+ const { session } = goalStore.createSession({
180
+ context: input.context,
181
+ goal: input.goal,
182
+ source: input.source,
183
+ workspaceId: input.workspaceId,
184
+ });
185
+ notify(session.id);
186
+ await deliverOrMarkFailed(session, buildExternalGoalPayload(session));
187
+ const delivered = appendAndNotify({
188
+ body: 'External goal delivered to Orchestrator.',
189
+ goalId: session.id,
190
+ kind: 'goal_delivered',
191
+ sessionStatus: 'in_progress',
192
+ status: 'in_progress',
193
+ });
194
+ const current = requireSession(goalStore, session.id);
195
+ return {
196
+ cursor: delivered.sequence,
197
+ events: goalStore.listEventsAfter(session.id, 0),
198
+ goalId: session.id,
199
+ session: current,
200
+ status: current.status,
201
+ };
202
+ },
203
+ async continueGoal(input) {
204
+ const session = requireSession(goalStore, input.goalId);
205
+ if (session.status === 'cancelled') {
206
+ throw new ConflictError(`External goal is cancelled: ${input.goalId}`);
207
+ }
208
+ const event = appendAndNotify({
209
+ body: input.message,
210
+ goalId: session.id,
211
+ kind: 'goal_continued',
212
+ sessionStatus: 'in_progress',
213
+ status: 'in_progress',
214
+ });
215
+ await deliverOrMarkFailed({ ...session, status: 'in_progress' }, buildExternalGoalContinuePayload(session, input.message, input.context));
216
+ const current = requireSession(goalStore, session.id);
217
+ return { cursor: event.sequence, event, session: current, status: current.status };
218
+ },
219
+ reportGoal(input) {
220
+ const session = requireSession(goalStore, input.goalId);
221
+ if (session.workspaceId !== input.workspaceId) {
222
+ throw new HttpError(404, `External goal not found in workspace: ${input.goalId}`);
223
+ }
224
+ if (input.fromAgentId !== getOrchestratorId(input.workspaceId)) {
225
+ throw new ForbiddenError('Only the workspace Orchestrator can report external goals');
226
+ }
227
+ if (closedForReport.has(session.status)) {
228
+ throw new ConflictError(`External goal is not accepting reports: ${input.goalId}`);
229
+ }
230
+ const event = appendAndNotify({
231
+ body: input.body,
232
+ goalId: session.id,
233
+ kind: reportEventKind(input.status),
234
+ sessionStatus: reportSessionStatus(input.status),
235
+ status: input.status,
236
+ ...(input.artifacts !== undefined ? { artifacts: input.artifacts } : {}),
237
+ });
238
+ const current = requireSession(goalStore, session.id);
239
+ return { cursor: event.sequence, event, session: current, status: current.status };
240
+ },
241
+ async waitGoal(input) {
242
+ const session = requireSession(goalStore, input.goalId);
243
+ const cursor = normalizedCursor(input.cursor);
244
+ const existing = goalStore.listEventsAfter(session.id, cursor);
245
+ if (existing.length > 0) {
246
+ const current = requireSession(goalStore, session.id);
247
+ return {
248
+ cursor: existing.at(-1)?.sequence ?? cursor,
249
+ events: existing,
250
+ goalId: session.id,
251
+ status: current.status,
252
+ };
253
+ }
254
+ await waitForNewEvent(session.id, boundedTimeout(input.timeoutMs));
255
+ const eventsAfterWait = goalStore.listEventsAfter(session.id, cursor);
256
+ const current = requireSession(goalStore, session.id);
257
+ return {
258
+ cursor: eventsAfterWait.at(-1)?.sequence ??
259
+ Math.max(cursor, goalStore.getLatestSequence(session.id)),
260
+ events: eventsAfterWait,
261
+ goalId: session.id,
262
+ status: current.status,
263
+ };
264
+ },
265
+ async cancelGoal(input) {
266
+ const session = requireSession(goalStore, input.goalId);
267
+ if (closedForReport.has(session.status) && session.status !== 'blocked') {
268
+ throw new ConflictError(`External goal cannot be cancelled from ${session.status}: ${input.goalId}`);
269
+ }
270
+ const event = appendAndNotify({
271
+ body: input.reason,
272
+ goalId: session.id,
273
+ kind: 'goal_cancelled',
274
+ sessionStatus: 'cancelled',
275
+ status: 'cancelled',
276
+ });
277
+ try {
278
+ await deliverToOrchestrator(session.workspaceId, buildExternalGoalCancelPayload(session, input.reason));
279
+ }
280
+ catch (error) {
281
+ const deliveryFailure = appendAndNotify({
282
+ body: error instanceof Error ? error.message : String(error),
283
+ goalId: session.id,
284
+ kind: 'delivery_failed',
285
+ sessionStatus: 'cancelled',
286
+ status: 'cancelled',
287
+ });
288
+ throw new ExternalGoalDeliveryError({
289
+ cursor: deliveryFailure.sequence,
290
+ goalId: session.id,
291
+ message: `orchestrator_not_running: cancelled external goal ${session.id}, but could not deliver the cancellation notice`,
292
+ status: 'cancelled',
293
+ });
294
+ }
295
+ const current = requireSession(goalStore, session.id);
296
+ return { cursor: event.sequence, event, session: current, status: current.status };
297
+ },
298
+ };
299
+ };
@@ -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
  '',
@@ -48,6 +48,9 @@ const ESCAPE = String.fromCharCode(27);
48
48
  const BELL = String.fromCharCode(7);
49
49
  const TERMINAL_CONTROL_PATTERN = new RegExp(`${ESCAPE}\\[[0-?]*[ -/]*[@-~]|${ESCAPE}\\][^${BELL}${ESCAPE}]*(?:${BELL}|${ESCAPE}\\\\)`, 'gu');
50
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;
51
54
  export const toBracketedPasteSubmission = (text) => `\u001b[200~${text}\u001b[201~`;
52
55
  const toError = (error) => (error instanceof Error ? error : new Error(String(error)));
53
56
  const createRunInactiveError = (runId) => new Error(`Run became inactive before input was submitted: ${runId}`);
@@ -71,13 +74,15 @@ export const isInteractiveAgentCommand = (command) => {
71
74
  };
72
75
  const getPlainTerminalOutput = (output) => output.replace(/\r/g, '\n').replace(TERMINAL_CONTROL_PATTERN, '');
73
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);
74
81
  const hasGeminiPromptReady = (output) => /\bType your message\b/u.test(output);
75
82
  const hasHermesPromptReady = (output) => {
76
83
  const plain = getPlainTerminalTail(output);
77
84
  return /\bWelcome to Hermes Agent![\s\S]*❯/u.test(plain);
78
85
  };
79
- const hasOpencodeVisiblePromptReady = (output) => /\bAsk anything\.\.\./u.test(getPlainTerminalTail(output));
80
- const hasOpencodeCompletedTurnReady = (output) => /(?:^|\s)▣\s+Build\s+·/u.test(getLastNonEmptyTerminalLine(output));
81
86
  const hasGrokPromptReady = (output) => /\b(?:Enter:send|Composer\s+\S+)/u.test(output);
82
87
  const hasAgyPromptReady = (output) => /(?:^|\n)\s*>\s*\n\s*(?:[─-]{8,}|\?\s*for shortcuts)/u.test(getPlainTerminalOutput(output));
83
88
  const getLastNonEmptyTerminalLine = (output) => {
@@ -112,6 +117,21 @@ export const hasFirstRunSetupPrompt = (output) => {
112
117
  const recent = recentLines.join('\n');
113
118
  return SETUP_PROMPT_PATTERNS.some((pattern) => pattern.test(recent));
114
119
  };
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
+ };
115
135
  const getInteractivePromptReadyState = (output, command = '') => {
116
136
  const commandName = getCommandName(command);
117
137
  if (hasBarePromptLine(output) ||
@@ -122,10 +142,7 @@ const getInteractivePromptReadyState = (output, command = '') => {
122
142
  return 'ready';
123
143
  }
124
144
  if (commandName === 'opencode') {
125
- if (hasOpencodeVisiblePromptReady(output))
126
- return 'ready';
127
- if (hasOpencodeCompletedTurnReady(output))
128
- return 'ready-after-settle';
145
+ return getOpencodePromptReadyState(output);
129
146
  }
130
147
  return 'not-ready';
131
148
  };
@@ -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[];