@tt-a1i/hive 2.1.9 → 2.1.10

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 (26) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/src/server/agent-stdin-dispatcher.js +1 -0
  3. package/dist/src/server/post-start-input-writer.js +35 -6
  4. package/dist/src/server/routes-scenarios.d.ts +12 -17
  5. package/dist/src/server/routes-scenarios.js +41 -17
  6. package/dist/src/server/scenario-presets.d.ts +7 -4
  7. package/dist/src/server/scenario-presets.js +16 -6
  8. package/dist/src/shared/random-worker-name.d.ts +9 -0
  9. package/dist/src/shared/random-worker-name.js +382 -0
  10. package/package.json +1 -1
  11. package/web/dist/assets/{AddWorkerDialog-QBN0m0eH.js → AddWorkerDialog-Dc0slXtx.js} +2 -2
  12. package/web/dist/assets/{AddWorkspaceFlow-BJy-W2XI.js → AddWorkspaceFlow-AyzSdx5w.js} +1 -1
  13. package/web/dist/assets/{FirstRunWizard-V-idMQB7.js → FirstRunWizard-DGB5zWhl.js} +1 -1
  14. package/web/dist/assets/{MarketplaceDrawer-D4ldgvSM.js → MarketplaceDrawer-DAZ-crqq.js} +1 -1
  15. package/web/dist/assets/{TaskGraphDrawer-BSxRbr1h.js → TaskGraphDrawer-DYk-0uKD.js} +1 -1
  16. package/web/dist/assets/{WhatsNewDialog-C-hpR8y4.js → WhatsNewDialog-B5tu3MV2.js} +1 -1
  17. package/web/dist/assets/{WorkerModal-EuNxsCtr.js → WorkerModal-C73K7SUg.js} +1 -1
  18. package/web/dist/assets/{WorkflowsDrawer-CoT6kWcM.js → WorkflowsDrawer-mrSAPqAh.js} +1 -1
  19. package/web/dist/assets/{WorkspaceMemoryDrawer-BXx4Z0kx.js → WorkspaceMemoryDrawer-CCz3HB4F.js} +1 -1
  20. package/web/dist/assets/{WorkspaceTaskDrawer-DKRRttxQ.js → WorkspaceTaskDrawer-C7JzoRp8.js} +1 -1
  21. package/web/dist/assets/index-CVQc8PLJ.js +84 -0
  22. package/web/dist/assets/{search-KbSLfFB8.js → search-Bx1xfpzr.js} +1 -1
  23. package/web/dist/assets/{square-terminal-C89RIZhu.js → square-terminal-Dfr9QC1I.js} +1 -1
  24. package/web/dist/index.html +1 -1
  25. package/web/dist/sw.js +1 -1
  26. package/web/dist/assets/index-D43vHIy2.js +0 -84
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable user-facing changes will be documented in this file.
4
4
 
5
+ ## 2.1.10 - 2026-06-23
6
+
7
+ Scenario teams and terminal delivery polish.
8
+
9
+ - Starts members created from one-click scenarios immediately, so the team is
10
+ ready for the Orchestrator to assign work instead of sitting stopped.
11
+ - Uses the shared member-name pool for scenario-created members, matching the
12
+ Add Member dialog and avoiding generated stem-only names.
13
+ - Improves OpenCode dispatch delivery after startup by recognizing its completed
14
+ turn footer as a ready signal, while waiting briefly for the terminal output
15
+ to settle before injecting the next dispatch.
16
+ - Adds an explicit accepted-dispatch status instruction to member tasks, making
17
+ it easier to see that a member received a dispatch before it starts working.
18
+ - Keeps optimistic terminal panels tied to real run lifecycle events and
19
+ preserves Codex cursor-position repaint frames, improving terminal display
20
+ during editing and fast start/stop flows.
21
+
5
22
  ## 2.1.9 - 2026-06-23
6
23
 
7
24
  Lean Hive guidance and desktop terminal polish.
@@ -42,6 +42,7 @@ export const buildWorkerDispatchPayload = (fromAgentName, workerDescription, dis
42
42
  `Your role: ${escapeHiveEnvelopeText(workerDescription)}`,
43
43
  '',
44
44
  'You must:',
45
+ `- Immediately acknowledge receipt by running \`team status "accepted dispatch ${dispatchId}"\` before doing the task`,
45
46
  `- When the task is done, failed, blocked, or partially done, run \`team report "<result>" --dispatch ${dispatchId}\``,
46
47
  '- Do not do unrelated work; report as soon as you are done',
47
48
  '- Never stop to wait for terminal input — if you need a decision, report the question as blocked instead',
@@ -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;
@@ -75,7 +76,8 @@ const hasHermesPromptReady = (output) => {
75
76
  const plain = getPlainTerminalTail(output);
76
77
  return /\bWelcome to Hermes Agent![\s\S]*❯/u.test(plain);
77
78
  };
78
- const hasOpencodePromptReady = (output) => /\bAsk anything\.\.\./u.test(getPlainTerminalTail(output));
79
+ const hasOpencodeVisiblePromptReady = (output) => /\bAsk anything\.\.\./u.test(getPlainTerminalTail(output));
80
+ const hasOpencodeCompletedTurnReady = (output) => /(?:^|\s)▣\s+Build\s+·/u.test(getLastNonEmptyTerminalLine(output));
79
81
  const hasGrokPromptReady = (output) => /\b(?:Enter:send|Composer\s+\S+)/u.test(output);
80
82
  const hasAgyPromptReady = (output) => /(?:^|\n)\s*>\s*\n\s*(?:[─-]{8,}|\?\s*for shortcuts)/u.test(getPlainTerminalOutput(output));
81
83
  const getLastNonEmptyTerminalLine = (output) => {
@@ -110,15 +112,24 @@ export const hasFirstRunSetupPrompt = (output) => {
110
112
  const recent = recentLines.join('\n');
111
113
  return SETUP_PROMPT_PATTERNS.some((pattern) => pattern.test(recent));
112
114
  };
113
- export const hasInteractivePromptReady = (output, command = '') => {
115
+ const getInteractivePromptReadyState = (output, command = '') => {
114
116
  const commandName = getCommandName(command);
115
- return (hasBarePromptLine(output) ||
117
+ if (hasBarePromptLine(output) ||
116
118
  (commandName === 'agy' && hasAgyPromptReady(output)) ||
117
119
  (commandName === 'grok' && hasGrokPromptReady(output)) ||
118
120
  (commandName === 'hermes' && hasHermesPromptReady(output)) ||
119
- ((commandName === 'gemini' || commandName === 'qwen') && hasGeminiPromptReady(output)) ||
120
- (commandName === 'opencode' && hasOpencodePromptReady(output)));
121
+ ((commandName === 'gemini' || commandName === 'qwen') && hasGeminiPromptReady(output))) {
122
+ return 'ready';
123
+ }
124
+ if (commandName === 'opencode') {
125
+ if (hasOpencodeVisiblePromptReady(output))
126
+ return 'ready';
127
+ if (hasOpencodeCompletedTurnReady(output))
128
+ return 'ready-after-settle';
129
+ }
130
+ return 'not-ready';
121
131
  };
132
+ export const hasInteractivePromptReady = (output, command = '') => getInteractivePromptReadyState(output, command) !== 'not-ready';
122
133
  export const hasBracketedPasteAcknowledgement = (output, baselineLength) => {
123
134
  const outputAfterPaste = output.slice(baselineLength);
124
135
  const pasteEndIndex = outputAfterPaste.lastIndexOf(BRACKETED_PASTE_END);
@@ -282,6 +293,24 @@ export const createPostStartInputWriter = (agentManager, command) => {
282
293
  });
283
294
  const startedAt = Date.now();
284
295
  let isInitialAttempt = true;
296
+ let settleCandidate = null;
297
+ const isReadyToWrite = (output) => {
298
+ const promptReadyState = getInteractivePromptReadyState(output, command);
299
+ if (promptReadyState === 'ready') {
300
+ settleCandidate = null;
301
+ return true;
302
+ }
303
+ if (promptReadyState !== 'ready-after-settle') {
304
+ settleCandidate = null;
305
+ return false;
306
+ }
307
+ const tail = getPlainTerminalTail(output);
308
+ if (!settleCandidate || settleCandidate.tail !== tail) {
309
+ settleCandidate = { since: Date.now(), tail };
310
+ return false;
311
+ }
312
+ return Date.now() - settleCandidate.since >= OPENCODE_COMPLETION_READY_SETTLE_MS;
313
+ };
285
314
  const tryWrite = () => {
286
315
  let output;
287
316
  try {
@@ -297,7 +326,7 @@ export const createPostStartInputWriter = (agentManager, command) => {
297
326
  return;
298
327
  }
299
328
  const firstRunSetupPromptVisible = hasFirstRunSetupPrompt(output);
300
- if ((!firstRunSetupPromptVisible && hasInteractivePromptReady(output, command)) ||
329
+ if ((!firstRunSetupPromptVisible && isReadyToWrite(output)) ||
301
330
  (!firstRunSetupPromptVisible &&
302
331
  canTimeoutBeforePromptReady(command) &&
303
332
  Date.now() - startedAt >= READY_TIMEOUT_MS)) {
@@ -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
+ };
@@ -3,11 +3,14 @@ import type { WorkerRole } from '../shared/types.js';
3
3
  import type { UiLanguage } from '../shared/ui-language.js';
4
4
  export { getScenarioPreset, getScenarioWorkerDescription, SCENARIO_PRESETS, type ScenarioId, type ScenarioPreset, type ScenarioWorkerSpec, } from '../shared/scenario-presets.js';
5
5
  /**
6
- * `<stem>-<4 base36 chars>` short, `team send`-safe (no spaces, lowercase),
7
- * and unlikely to collide. The caller passes an `isTaken` probe against the
8
- * live roster; on the (rare) collision we just roll again.
6
+ * Scenario member names come from the same role-flavored historical-figure
7
+ * pool the Add Member dialog uses. When a pool is exhausted in a busy
8
+ * workspace, append a short suffix instead of failing the whole scenario.
9
9
  */
10
- export declare const buildScenarioWorkerName: (spec: Pick<ScenarioWorkerSpec, "nameStem">, isTaken: (name: string) => boolean, maxAttempts?: number) => string;
10
+ export declare const buildScenarioWorkerName: (spec: Pick<ScenarioWorkerSpec, "nameStem" | "role">, usedNames: ReadonlySet<string>, input?: {
11
+ language?: UiLanguage;
12
+ nextUint32?: () => number;
13
+ }, maxAttempts?: number) => string;
11
14
  export interface ScenarioKickoffInput {
12
15
  scenarioId: string;
13
16
  goal: string;
@@ -1,15 +1,25 @@
1
1
  import { randomBytes } from 'node:crypto';
2
+ import { generateWorkerName } from '../shared/random-worker-name.js';
2
3
  export { getScenarioPreset, getScenarioWorkerDescription, SCENARIO_PRESETS, } from '../shared/scenario-presets.js';
3
4
  /**
4
- * `<stem>-<4 base36 chars>` short, `team send`-safe (no spaces, lowercase),
5
- * and unlikely to collide. The caller passes an `isTaken` probe against the
6
- * live roster; on the (rare) collision we just roll again.
5
+ * Scenario member names come from the same role-flavored historical-figure
6
+ * pool the Add Member dialog uses. When a pool is exhausted in a busy
7
+ * workspace, append a short suffix instead of failing the whole scenario.
7
8
  */
8
- export const buildScenarioWorkerName = (spec, isTaken, maxAttempts = 16) => {
9
+ export const buildScenarioWorkerName = (spec, usedNames, input = {}, maxAttempts = 16) => {
10
+ const candidate = generateWorkerName({
11
+ role: spec.role,
12
+ usedNames,
13
+ ...(input.language !== undefined ? { language: input.language } : {}),
14
+ ...(input.nextUint32 !== undefined ? { nextUint32: input.nextUint32 } : {}),
15
+ });
16
+ if (!usedNames.has(candidate))
17
+ return candidate;
18
+ const base = candidate || spec.nameStem;
9
19
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
10
20
  const suffix = randomBytes(3).readUIntBE(0, 3).toString(36).padStart(4, '0').slice(-4);
11
- const name = `${spec.nameStem}-${suffix}`;
12
- if (!isTaken(name))
21
+ const name = `${base}-${suffix}`;
22
+ if (!usedNames.has(name))
13
23
  return name;
14
24
  }
15
25
  throw new Error(`Could not generate a unique member name for: ${spec.nameStem}`);
@@ -0,0 +1,9 @@
1
+ import type { WorkerRole } from './types.js';
2
+ import type { UiLanguage } from './ui-language.js';
3
+ export interface GenerateWorkerNameOptions {
4
+ language?: UiLanguage;
5
+ role?: WorkerRole;
6
+ usedNames?: ReadonlySet<string>;
7
+ nextUint32?: () => number;
8
+ }
9
+ export declare const generateWorkerName: ({ language, role, usedNames, nextUint32, }?: GenerateWorkerNameOptions) => string;