@tt-a1i/hive 2.0.2 → 2.1.0

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 (147) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.en.md +15 -6
  3. package/README.md +26 -4
  4. package/dist/src/cli/hive.d.ts +4 -0
  5. package/dist/src/cli/hive.js +25 -3
  6. package/dist/src/cli/team.d.ts +8 -1
  7. package/dist/src/cli/team.js +111 -11
  8. package/dist/src/server/action-center-summary.d.ts +193 -0
  9. package/dist/src/server/action-center-summary.js +188 -0
  10. package/dist/src/server/agent-command-resolver.d.ts +6 -0
  11. package/dist/src/server/agent-command-resolver.js +16 -0
  12. package/dist/src/server/agent-manager.js +11 -1
  13. package/dist/src/server/agent-run-starter.js +47 -6
  14. package/dist/src/server/agent-runtime-types.d.ts +4 -0
  15. package/dist/src/server/agent-startup-instructions.d.ts +4 -0
  16. package/dist/src/server/agent-startup-instructions.js +35 -9
  17. package/dist/src/server/agent-stdin-dispatcher.js +17 -9
  18. package/dist/src/server/diagnostics-support-bundle.d.ts +288 -0
  19. package/dist/src/server/diagnostics-support-bundle.js +179 -0
  20. package/dist/src/server/dispatch-ledger-store.d.ts +4 -1
  21. package/dist/src/server/dispatch-ledger-store.js +46 -6
  22. package/dist/src/server/hive-envelope-escape.d.ts +2 -0
  23. package/dist/src/server/hive-envelope-escape.js +2 -0
  24. package/dist/src/server/hive-team-guidance.d.ts +1 -1
  25. package/dist/src/server/hive-team-guidance.js +67 -25
  26. package/dist/src/server/message-log-store.d.ts +1 -1
  27. package/dist/src/server/post-start-input-writer.js +8 -2
  28. package/dist/src/server/preset-launch-support.d.ts +2 -0
  29. package/dist/src/server/preset-launch-support.js +65 -2
  30. package/dist/src/server/protocol-event-stats.d.ts +39 -0
  31. package/dist/src/server/protocol-event-stats.js +84 -0
  32. package/dist/src/server/recovery-summary.js +19 -14
  33. package/dist/src/server/role-template-store.d.ts +1 -1
  34. package/dist/src/server/role-templates.d.ts +1 -0
  35. package/dist/src/server/role-templates.js +43 -29
  36. package/dist/src/server/routes-action-center.d.ts +2 -0
  37. package/dist/src/server/routes-action-center.js +37 -0
  38. package/dist/src/server/routes-diagnostics.d.ts +2 -0
  39. package/dist/src/server/routes-diagnostics.js +17 -0
  40. package/dist/src/server/routes-scenarios.d.ts +25 -0
  41. package/dist/src/server/routes-scenarios.js +89 -0
  42. package/dist/src/server/routes-settings.js +2 -11
  43. package/dist/src/server/routes-team-memory.js +52 -0
  44. package/dist/src/server/routes-team.js +40 -20
  45. package/dist/src/server/routes-workspace-memory-dreams.js +8 -0
  46. package/dist/src/server/routes-workspace-uploads.d.ts +2 -0
  47. package/dist/src/server/routes-workspace-uploads.js +154 -0
  48. package/dist/src/server/routes-workspaces.js +29 -3
  49. package/dist/src/server/routes.js +8 -0
  50. package/dist/src/server/runtime-message-builders.d.ts +0 -1
  51. package/dist/src/server/runtime-message-builders.js +0 -8
  52. package/dist/src/server/runtime-store-contract.d.ts +15 -0
  53. package/dist/src/server/runtime-store-dream.d.ts +14 -1
  54. package/dist/src/server/runtime-store-dream.js +49 -1
  55. package/dist/src/server/runtime-store-helpers.d.ts +7 -0
  56. package/dist/src/server/runtime-store-helpers.js +85 -22
  57. package/dist/src/server/runtime-store-worker-mutations.d.ts +11 -0
  58. package/dist/src/server/runtime-store-worker-mutations.js +46 -0
  59. package/dist/src/server/runtime-store-workflows.js +10 -6
  60. package/dist/src/server/runtime-store.js +34 -42
  61. package/dist/src/server/scenario-presets.d.ts +25 -0
  62. package/dist/src/server/scenario-presets.js +35 -0
  63. package/dist/src/server/sentinel-heartbeat.d.ts +30 -0
  64. package/dist/src/server/sentinel-heartbeat.js +145 -0
  65. package/dist/src/server/spawn-cli-resolver.d.ts +37 -0
  66. package/dist/src/server/spawn-cli-resolver.js +70 -0
  67. package/dist/src/server/spawn-worker-defaults.d.ts +13 -0
  68. package/dist/src/server/spawn-worker-defaults.js +45 -0
  69. package/dist/src/server/sqlite-schema-v32.d.ts +2 -0
  70. package/dist/src/server/sqlite-schema-v32.js +17 -0
  71. package/dist/src/server/sqlite-schema-v33.d.ts +3 -0
  72. package/dist/src/server/sqlite-schema-v33.js +18 -0
  73. package/dist/src/server/sqlite-schema-v34.d.ts +11 -0
  74. package/dist/src/server/sqlite-schema-v34.js +19 -0
  75. package/dist/src/server/sqlite-schema-v35.d.ts +3 -0
  76. package/dist/src/server/sqlite-schema-v35.js +23 -0
  77. package/dist/src/server/sqlite-schema.d.ts +1 -1
  78. package/dist/src/server/sqlite-schema.js +35 -1
  79. package/dist/src/server/system-message.d.ts +5 -2
  80. package/dist/src/server/system-message.js +5 -2
  81. package/dist/src/server/tasks-file-watcher.d.ts +8 -0
  82. package/dist/src/server/tasks-file-watcher.js +31 -2
  83. package/dist/src/server/team-authz.d.ts +9 -1
  84. package/dist/src/server/team-authz.js +24 -0
  85. package/dist/src/server/team-list-serializer.d.ts +2 -2
  86. package/dist/src/server/team-list-serializer.js +2 -1
  87. package/dist/src/server/team-memory-digest.js +4 -4
  88. package/dist/src/server/team-memory-dream-applier.js +24 -3
  89. package/dist/src/server/team-memory-dream-prompt.d.ts +13 -0
  90. package/dist/src/server/team-memory-dream-prompt.js +91 -0
  91. package/dist/src/server/team-memory-dream-run-store.d.ts +2 -0
  92. package/dist/src/server/team-memory-dream-run-store.js +14 -4
  93. package/dist/src/server/team-memory-dream-runner.d.ts +2 -21
  94. package/dist/src/server/team-memory-dream-runner.js +3 -148
  95. package/dist/src/server/team-memory-dream-store.d.ts +1 -1
  96. package/dist/src/server/team-memory-dream-store.js +1 -1
  97. package/dist/src/server/team-operations.d.ts +18 -2
  98. package/dist/src/server/team-operations.js +222 -33
  99. package/dist/src/server/team-recap.d.ts +10 -0
  100. package/dist/src/server/team-recap.js +73 -0
  101. package/dist/src/server/terminal-input-profile.js +88 -9
  102. package/dist/src/server/upload-limits.d.ts +2 -0
  103. package/dist/src/server/upload-limits.js +2 -0
  104. package/dist/src/server/workflow-cli-policy.d.ts +7 -2
  105. package/dist/src/server/workflow-cli-policy.js +15 -3
  106. package/dist/src/server/workflow-run-store.d.ts +1 -0
  107. package/dist/src/server/workflow-run-store.js +11 -1
  108. package/dist/src/server/workflow-runner.d.ts +4 -1
  109. package/dist/src/server/workflow-runner.js +418 -118
  110. package/dist/src/server/workflow-script-loader.d.ts +3 -2
  111. package/dist/src/server/workflow-script-loader.js +161 -0
  112. package/dist/src/server/workspace-store-contract.d.ts +2 -0
  113. package/dist/src/server/workspace-store.d.ts +1 -1
  114. package/dist/src/server/workspace-store.js +40 -30
  115. package/dist/src/server/workspace-upload-store.d.ts +40 -0
  116. package/dist/src/server/workspace-upload-store.js +295 -0
  117. package/dist/src/shared/scenario-presets.d.ts +32 -0
  118. package/dist/src/shared/scenario-presets.js +69 -0
  119. package/dist/src/shared/types.d.ts +12 -1
  120. package/package.json +1 -1
  121. package/web/dist/assets/AddWorkerDialog-DBLhwb91.js +2 -0
  122. package/web/dist/assets/AddWorkspaceFlow-cxvhVAsT.js +1 -0
  123. package/web/dist/assets/FirstRunWizard-DlEPnWWw.js +1 -0
  124. package/web/dist/assets/{MarketplaceDrawer-Dd8WIA8T.js → MarketplaceDrawer-CfSiRi8e.js} +11 -11
  125. package/web/dist/assets/TaskGraphDrawer-C2JufcPs.js +1 -0
  126. package/web/dist/assets/WhatsNewDialog-vP7buLos.js +1 -0
  127. package/web/dist/assets/WorkerModal-CSorwcdP.js +1 -0
  128. package/web/dist/assets/{WorkflowsDrawer-Bjf4olbR.js → WorkflowsDrawer-BXS3w9Uq.js} +1 -1
  129. package/web/dist/assets/WorkspaceMemoryDrawer-D71ivohr.js +1 -0
  130. package/web/dist/assets/{WorkspaceTaskDrawer-BIWwISvA.js → WorkspaceTaskDrawer-CGCTSHKa.js} +1 -1
  131. package/web/dist/assets/index-BcwN8cCw.js +79 -0
  132. package/web/dist/assets/index-StXTPHls.css +1 -0
  133. package/web/dist/assets/{search-Bk2HQvO7.js → search-BZw4T67h.js} +1 -1
  134. package/web/dist/assets/{square-terminal-D93m9hfY.js → square-terminal-B7E57In1.js} +1 -1
  135. package/web/dist/index.html +2 -2
  136. package/web/dist/sw.js +1 -1
  137. package/dist/src/server/env-sync-message.d.ts +0 -9
  138. package/dist/src/server/env-sync-message.js +0 -29
  139. package/web/dist/assets/AddWorkerDialog-CbV75qUX.js +0 -2
  140. package/web/dist/assets/AddWorkspaceFlow-CwV-7wPx.js +0 -1
  141. package/web/dist/assets/FirstRunWizard-a6PWIK3x.js +0 -1
  142. package/web/dist/assets/TaskGraphDrawer-Bk5WFIk_.js +0 -1
  143. package/web/dist/assets/WhatsNewDialog-C2VZaip0.js +0 -1
  144. package/web/dist/assets/WorkerModal-DucW-9YT.js +0 -1
  145. package/web/dist/assets/WorkspaceMemoryDrawer-DglCy_5f.js +0 -1
  146. package/web/dist/assets/index-BAiLYajK.css +0 -1
  147. package/web/dist/assets/index-BV2k9Dts.js +0 -73
@@ -0,0 +1,145 @@
1
+ // Sentinel heartbeat — feeds a periodic, read-only workspace snapshot into any RUNNING
2
+ // sentinel-role worker's PTY so the agent can patrol without polling tools itself.
3
+ //
4
+ // INVARIANT (design §3.6): the runtime still detects nothing and decides nothing. This service
5
+ // computes the same kind of read-only view the action center already renders, formats it as text,
6
+ // and hands it to an OBSERVER AGENT; agent states never change (writeSystemMessageToAgent is not a
7
+ // protocol event), no dispatch is touched, and "orphan"/"quiet" lines are hints with generous grace
8
+ // windows — the sentinel (and ultimately the user) judges them.
9
+ import { escapeHiveEnvelopeText } from './hive-envelope-escape.js';
10
+ export const SENTINEL_HEARTBEAT_INTERVAL_MS = 30 * 60 * 1000;
11
+ export const SENTINEL_HEARTBEAT_TICK_MS = 60 * 1000;
12
+ // A dispatch open on a STOPPED worker is only flagged after this grace — restarts happen.
13
+ export const SENTINEL_ORPHAN_GRACE_MS = 15 * 60 * 1000;
14
+ // A submitted dispatch with no report is only mentioned after this — big tasks are slow by nature.
15
+ export const SENTINEL_QUIET_GRACE_MS = 30 * 60 * 1000;
16
+ const dispatchAge = (dispatch, now) => now - (dispatch.submittedAt ?? dispatch.createdAt);
17
+ const minutes = (ms) => `${Math.max(0, Math.floor(ms / 60_000))}m`;
18
+ const shortId = (id) => (id.length > 8 ? id.slice(0, 8) : id);
19
+ export const buildSentinelHeartbeat = (input) => {
20
+ const { now, workers, openDispatches } = input;
21
+ const workersById = new Map(workers.map((worker) => [worker.id, worker]));
22
+ const teammates = workers.filter((worker) => worker.role !== 'sentinel');
23
+ const workerLines = teammates.map((worker) => {
24
+ const queued = worker.pendingTaskCount > 0 ? `, ${worker.pendingTaskCount} queued` : '';
25
+ return `- @${escapeHiveEnvelopeText(worker.name)} (${worker.role}) ${worker.status}${queued}`;
26
+ });
27
+ const dispatchLines = openDispatches.map((dispatch) => {
28
+ const target = workersById.get(dispatch.toAgentId);
29
+ const name = target
30
+ ? `@${escapeHiveEnvelopeText(target.name)}`
31
+ : escapeHiveEnvelopeText(dispatch.toAgentId);
32
+ return `- ${shortId(dispatch.id)} -> ${name}, ${dispatch.status} for ${minutes(dispatchAge(dispatch, now))}`;
33
+ });
34
+ const orphanLines = openDispatches
35
+ .filter((dispatch) => {
36
+ const target = workersById.get(dispatch.toAgentId);
37
+ return target?.status === 'stopped' && dispatchAge(dispatch, now) >= SENTINEL_ORPHAN_GRACE_MS;
38
+ })
39
+ .map((dispatch) => {
40
+ const target = workersById.get(dispatch.toAgentId);
41
+ return `- ${shortId(dispatch.id)} -> @${escapeHiveEnvelopeText(target?.name ?? dispatch.toAgentId)} (worker stopped, open for ${minutes(dispatchAge(dispatch, now))})`;
42
+ });
43
+ const quietLines = openDispatches
44
+ .filter((dispatch) => {
45
+ const target = workersById.get(dispatch.toAgentId);
46
+ return (target !== undefined &&
47
+ target.status !== 'stopped' &&
48
+ dispatch.submittedAt !== null &&
49
+ now - dispatch.submittedAt >= SENTINEL_QUIET_GRACE_MS);
50
+ })
51
+ .map((dispatch) => {
52
+ const target = workersById.get(dispatch.toAgentId);
53
+ return `- ${shortId(dispatch.id)} -> @${escapeHiveEnvelopeText(target?.name ?? dispatch.toAgentId)}, no report for ${minutes(now - (dispatch.submittedAt ?? now))} (may be a normal long task)`;
54
+ });
55
+ const lines = [
56
+ '<hive-message kind="heartbeat">',
57
+ `Sentinel snapshot for "${escapeHiveEnvelopeText(input.workspaceName)}" (auto-generated; observe-only).`,
58
+ '',
59
+ `Workers (${teammates.length}):`,
60
+ ...(workerLines.length > 0 ? workerLines : ['- (none)']),
61
+ '',
62
+ `Open dispatches (${openDispatches.length}):`,
63
+ ...(dispatchLines.length > 0 ? dispatchLines : ['- (none)']),
64
+ ];
65
+ if (orphanLines.length > 0) {
66
+ lines.push('', `Possible orphans (worker stopped, open >= ${minutes(SENTINEL_ORPHAN_GRACE_MS)}):`);
67
+ lines.push(...orphanLines);
68
+ }
69
+ if (quietLines.length > 0) {
70
+ lines.push('', `Long-quiet (no report >= ${minutes(SENTINEL_QUIET_GRACE_MS)}):`);
71
+ lines.push(...quietLines);
72
+ }
73
+ if (orphanLines.length === 0 && quietLines.length === 0) {
74
+ lines.push('', 'Nothing unusual detected.');
75
+ }
76
+ lines.push('', "Cross-check against your own observations. If something needs the Orchestrator's attention,", 'summarize it with: team status "<finding>" — otherwise stay quiet.', 'Do not modify files, run side-effecting commands, or dispatch work.', '</hive-message>', '');
77
+ return lines.join('\n');
78
+ };
79
+ export const createSentinelHeartbeatService = (deps) => {
80
+ const intervalMs = deps.intervalMs ?? SENTINEL_HEARTBEAT_INTERVAL_MS;
81
+ const tickMs = deps.tickMs ?? SENTINEL_HEARTBEAT_TICK_MS;
82
+ const now = deps.now ?? Date.now;
83
+ // Keyed by `${workspaceId}:${agentId}`. A sentinel that stops and restarts keeps its slot —
84
+ // the cadence is "at most one heartbeat per interval", not "one per run".
85
+ const lastSentAt = new Map();
86
+ let timer;
87
+ const tickOnce = () => {
88
+ const ts = now();
89
+ for (const workspace of safeList(deps.listWorkspaces)) {
90
+ let workers;
91
+ try {
92
+ workers = deps.listWorkers(workspace.id);
93
+ }
94
+ catch {
95
+ continue;
96
+ }
97
+ const sentinels = workers.filter((worker) => worker.role === 'sentinel' && deps.hasActiveRun(workspace.id, worker.id));
98
+ if (sentinels.length === 0)
99
+ continue;
100
+ for (const sentinel of sentinels) {
101
+ const key = `${workspace.id}:${sentinel.id}`;
102
+ const last = lastSentAt.get(key);
103
+ if (last !== undefined && ts - last < intervalMs)
104
+ continue;
105
+ try {
106
+ const text = buildSentinelHeartbeat({
107
+ now: ts,
108
+ openDispatches: deps.listOpenDispatches(workspace.id),
109
+ workers,
110
+ workspaceName: workspace.name,
111
+ });
112
+ deps.deliver(workspace.id, sentinel.id, text);
113
+ lastSentAt.set(key, ts);
114
+ }
115
+ catch {
116
+ // Never let one workspace's failure kill the patrol loop; retry next interval.
117
+ }
118
+ }
119
+ }
120
+ };
121
+ return {
122
+ start() {
123
+ if (timer)
124
+ return;
125
+ timer = setInterval(tickOnce, tickMs);
126
+ timer.unref?.();
127
+ },
128
+ /** Test seam + deterministic cadence driver. */
129
+ tickOnce,
130
+ close() {
131
+ if (!timer)
132
+ return;
133
+ clearInterval(timer);
134
+ timer = undefined;
135
+ },
136
+ };
137
+ };
138
+ const safeList = (listWorkspaces) => {
139
+ try {
140
+ return listWorkspaces();
141
+ }
142
+ catch {
143
+ return [];
144
+ }
145
+ };
@@ -0,0 +1,37 @@
1
+ import type { AgentLaunchConfigInput } from './agent-run-store.js';
2
+ /**
3
+ * CLI selection for `team spawn` (growth research 2026-06-11, P0-B2).
4
+ *
5
+ * The old behavior hardcoded the default to 'claude': a Codex/Gemini-only
6
+ * user whose orchestrator asked for `team spawn coder` got a worker that
7
+ * died instantly on a missing `claude` binary. The default now:
8
+ * 1. inherits the workspace orchestrator's CLI brand (preset id first,
9
+ * else the brand normalized from its launch command), if available;
10
+ * 2. else picks the first built-in preset whose command is on PATH;
11
+ * 3. else falls back to 'claude' as the last resort (old behavior).
12
+ * An EXPLICIT `--cli` that is not on PATH is now a 400 with a suggestion
13
+ * instead of a silently-broken worker.
14
+ *
15
+ * The PATH probe is injectable so unit tests never touch the real PATH.
16
+ */
17
+ export interface SpawnCliPresetRecord {
18
+ id: string;
19
+ command: string;
20
+ args: string[];
21
+ env: Record<string, string>;
22
+ }
23
+ export type CliAvailabilityProbe = (command: string, env: Record<string, string>) => boolean;
24
+ export interface SpawnCliResolverPorts {
25
+ /** Settings lookup — covers built-in presets (possibly user-edited) and custom ones. */
26
+ getCommandPreset: (id: string) => SpawnCliPresetRecord | undefined;
27
+ /** Launch config of this workspace's orchestrator, if it was ever configured. */
28
+ getOrchestratorLaunchConfig: () => Pick<AgentLaunchConfigInput, 'command' | 'commandPresetId' | 'interactiveCommand'> | undefined;
29
+ /** PATH probe — defaults to the real probe; tests inject a stub. */
30
+ isCommandAvailable?: CliAvailabilityProbe;
31
+ }
32
+ /** Built-in preset ids whose command is actually visible on PATH, in built-in order. */
33
+ export declare const listAvailableBuiltinCliIds: (ports: SpawnCliResolverPorts) => string[];
34
+ /** Default-CLI selection when `team spawn` omits `--cli`. */
35
+ export declare const resolveDefaultSpawnCliLaunchConfig: (ports: SpawnCliResolverPorts) => AgentLaunchConfigInput;
36
+ /** Explicit `--cli <id>`: unknown id or a command missing from PATH is a 400. */
37
+ export declare const resolveExplicitSpawnCliLaunchConfig: (ports: SpawnCliResolverPorts, cliId: string) => AgentLaunchConfigInput;
@@ -0,0 +1,70 @@
1
+ import { isCommandAvailableOnPath } from './agent-command-resolver.js';
2
+ import { BUILTIN_COMMAND_PRESET_IDS, getBuiltinCommandPresetByCommand, } from './command-preset-defaults.js';
3
+ import { BadRequestError } from './http-errors.js';
4
+ import { normalizeExecutableToken } from './startup-command-parser.js';
5
+ const toLaunchConfig = (preset) => ({
6
+ args: preset.args,
7
+ command: preset.command,
8
+ commandPresetId: preset.id,
9
+ });
10
+ const getProbe = (ports) => ports.isCommandAvailable ?? isCommandAvailableOnPath;
11
+ const getAvailablePreset = (ports, presetId) => {
12
+ const preset = ports.getCommandPreset(presetId);
13
+ if (!preset)
14
+ return undefined;
15
+ return getProbe(ports)(preset.command, preset.env) ? preset : undefined;
16
+ };
17
+ /**
18
+ * The orchestrator's CLI brand: explicit preset id when the launch config
19
+ * carries one, else the brand normalized from the launch command (for
20
+ * startup-command launches the shell is in `command` and the real CLI in
21
+ * `interactiveCommand`, so prefer the latter). `cursor-agent`-style commands
22
+ * map back to their preset id via the built-in command table.
23
+ */
24
+ const inheritedOrchestratorPresetId = (ports) => {
25
+ const config = ports.getOrchestratorLaunchConfig();
26
+ if (!config)
27
+ return undefined;
28
+ if (config.commandPresetId)
29
+ return config.commandPresetId;
30
+ const brand = normalizeExecutableToken(config.interactiveCommand ?? config.command);
31
+ if (!brand)
32
+ return undefined;
33
+ return getBuiltinCommandPresetByCommand(brand)?.id ?? brand;
34
+ };
35
+ /** Built-in preset ids whose command is actually visible on PATH, in built-in order. */
36
+ export const listAvailableBuiltinCliIds = (ports) => BUILTIN_COMMAND_PRESET_IDS.filter((id) => getAvailablePreset(ports, id) !== undefined);
37
+ /** Default-CLI selection when `team spawn` omits `--cli`. */
38
+ export const resolveDefaultSpawnCliLaunchConfig = (ports) => {
39
+ const inheritedId = inheritedOrchestratorPresetId(ports);
40
+ if (inheritedId) {
41
+ const inherited = getAvailablePreset(ports, inheritedId);
42
+ if (inherited)
43
+ return toLaunchConfig(inherited);
44
+ }
45
+ for (const id of BUILTIN_COMMAND_PRESET_IDS) {
46
+ const preset = getAvailablePreset(ports, id);
47
+ if (preset)
48
+ return toLaunchConfig(preset);
49
+ }
50
+ // Last resort: nothing probed as available (or probing is impossible in
51
+ // this environment) — keep the historical 'claude' fallback so spawn never
52
+ // hard-fails on the default path.
53
+ const claude = ports.getCommandPreset('claude');
54
+ return claude ? toLaunchConfig(claude) : { args: [], command: 'claude' };
55
+ };
56
+ /** Explicit `--cli <id>`: unknown id or a command missing from PATH is a 400. */
57
+ export const resolveExplicitSpawnCliLaunchConfig = (ports, cliId) => {
58
+ const preset = ports.getCommandPreset(cliId);
59
+ if (!preset) {
60
+ throw new BadRequestError(`Unsupported cli '${cliId}'`);
61
+ }
62
+ if (!getProbe(ports)(preset.command, preset.env)) {
63
+ const available = listAvailableBuiltinCliIds(ports);
64
+ const hint = available.length > 0
65
+ ? `Install it, or retry with a CLI that is available on this machine: \`team spawn <role> --cli ${available[0]}\` (available: ${available.join(', ')}).`
66
+ : 'Install it and make sure it is on PATH, then retry.';
67
+ throw new BadRequestError(`CLI '${cliId}' is not usable here: its command '${preset.command}' is not visible on PATH. ${hint}`);
68
+ }
69
+ return toLaunchConfig(preset);
70
+ };
@@ -0,0 +1,13 @@
1
+ import type { WorkerRole } from '../shared/types.js';
2
+ export interface SpawnWorkerDefaults {
3
+ role: WorkerRole;
4
+ name: string;
5
+ /** Present only when an unknown role label was mapped to 'custom' — keeps
6
+ the requested label visible instead of the generic custom placeholder. */
7
+ description?: string;
8
+ }
9
+ export declare const resolveSpawnWorkerDefaults: (input: {
10
+ requestedRole: string | undefined;
11
+ requestedName: string | undefined;
12
+ takenNames: ReadonlySet<string>;
13
+ }) => SpawnWorkerDefaults;
@@ -0,0 +1,45 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ /**
3
+ * Role + name defaults for `team spawn` (routes-team /api/team/spawn).
4
+ *
5
+ * The natural orchestrator flow is `team spawn researcher` followed by
6
+ * `team send researcher "..."` — `team send` matches the worker NAME
7
+ * exactly, so the spawn defaults must keep that flow working:
8
+ *
9
+ * - When `--name` is omitted, the worker is named after the requested role
10
+ * label verbatim (`researcher`), as long as that name is free. Only on a
11
+ * roster collision do we fall back to the old `<label>-<uuid>` form —
12
+ * the spawn response echoes the final name either way.
13
+ * - A role label outside the built-in set maps to 'custom' (NOT 'coder':
14
+ * silently relabeling `researcher` as a coder hid the coercion from the
15
+ * orchestrator and broke send-by-role). The requested label survives in
16
+ * the worker name and in a generated role description.
17
+ */
18
+ const WORKER_ROLES = new Set(['coder', 'reviewer', 'tester', 'sentinel', 'custom']);
19
+ /* The built-in pseudo-agents ('Orchestrator' / 'Workflow') are not workers,
20
+ so the store's duplicate-name check can't protect them. Don't let a bare
21
+ `team spawn orchestrator` mint a second roster entry that reads like the
22
+ queen — those labels take the uuid fallback instead. */
23
+ const RESERVED_BARE_NAMES = new Set(['orchestrator', 'workflow']);
24
+ /* workspace-store normalizeWorkerName caps names at 64 chars; keep room for
25
+ the '-' + uuid (37 chars) so the fallback never trips that limit. */
26
+ const MAX_NAME_LENGTH = 64;
27
+ const MAX_LABEL_IN_FALLBACK = MAX_NAME_LENGTH - 37;
28
+ export const resolveSpawnWorkerDefaults = (input) => {
29
+ const label = input.requestedRole?.trim() || 'coder';
30
+ const role = WORKER_ROLES.has(label) ? label : 'custom';
31
+ const description = role === 'custom' && label !== 'custom'
32
+ ? [
33
+ `You are ${label}. Work from the task instructions dispatched by the Orchestrator.`,
34
+ `你是 ${label},按 Orchestrator 派发的任务说明工作。`,
35
+ 'When done, use `team report` to report results, risks, and blockers. / 完成后用 `team report` 汇报结果、风险和阻塞。',
36
+ ].join('\n')
37
+ : undefined;
38
+ const requestedName = input.requestedName?.trim();
39
+ const bareLabelAvailable = label.length <= MAX_NAME_LENGTH &&
40
+ !RESERVED_BARE_NAMES.has(label.toLowerCase()) &&
41
+ !input.takenNames.has(label);
42
+ const name = requestedName ||
43
+ (bareLabelAvailable ? label : `${label.slice(0, MAX_LABEL_IN_FALLBACK)}-${randomUUID()}`);
44
+ return description === undefined ? { role, name } : { role, name, description };
45
+ };
@@ -0,0 +1,2 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ export declare const applySchemaVersion32: (db: Database) => void;
@@ -0,0 +1,17 @@
1
+ export const applySchemaVersion32 = (db) => {
2
+ db.exec(`
3
+ CREATE TABLE IF NOT EXISTS workspace_uploads (
4
+ id TEXT PRIMARY KEY,
5
+ workspace_id TEXT NOT NULL,
6
+ remote_device_id TEXT,
7
+ original_name TEXT NOT NULL,
8
+ mime_type TEXT NOT NULL,
9
+ size_bytes INTEGER NOT NULL,
10
+ storage_key TEXT NOT NULL,
11
+ created_at INTEGER NOT NULL
12
+ );
13
+
14
+ CREATE INDEX IF NOT EXISTS idx_workspace_uploads_workspace_created
15
+ ON workspace_uploads (workspace_id, created_at DESC, id DESC);
16
+ `);
17
+ };
@@ -0,0 +1,3 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ /** v33: built-in Sentinel role template — a read-only patrol observer that reports via `team status`. */
3
+ export declare const applySchemaVersion33: (db: Database) => void;
@@ -0,0 +1,18 @@
1
+ import { SENTINEL_ROLE_DESCRIPTION } from './role-templates.js';
2
+ /** v33: built-in Sentinel role template — a read-only patrol observer that reports via `team status`. */
3
+ export const applySchemaVersion33 = (db) => {
4
+ // Runs unguarded on every boot (v30+ pattern), so it must tolerate a
5
+ // foreign-built DB where role_templates was never created — same
6
+ // sqlite_master guard v12 uses for the same table.
7
+ const hasRoleTemplates = db
8
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'role_templates'")
9
+ .get();
10
+ if (!hasRoleTemplates)
11
+ return;
12
+ const now = Date.now();
13
+ db.prepare(`INSERT INTO role_templates (
14
+ id, name, role_type, description, default_command, default_args, default_env,
15
+ is_builtin, created_at, updated_at
16
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
17
+ ON CONFLICT(id) DO NOTHING`).run('sentinel', 'Sentinel', 'sentinel', SENTINEL_ROLE_DESCRIPTION, 'claude', '[]', '{}', now, now);
18
+ };
@@ -0,0 +1,11 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ /**
3
+ * v34: local retention signals (issue #23) — per-day protocol event counters.
4
+ *
5
+ * Deliberately a dedicated append-only counter table instead of aggregating
6
+ * `messages` / `dispatches` at read time: both of those are pruned when
7
+ * workers or workspaces are deleted (ephemeral workers especially), so
8
+ * historical activity would silently shrink. Counters are local-only —
9
+ * nothing here is ever transmitted anywhere.
10
+ */
11
+ export declare const applySchemaVersion34: (db: Database) => void;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * v34: local retention signals (issue #23) — per-day protocol event counters.
3
+ *
4
+ * Deliberately a dedicated append-only counter table instead of aggregating
5
+ * `messages` / `dispatches` at read time: both of those are pruned when
6
+ * workers or workspaces are deleted (ephemeral workers especially), so
7
+ * historical activity would silently shrink. Counters are local-only —
8
+ * nothing here is ever transmitted anywhere.
9
+ */
10
+ export const applySchemaVersion34 = (db) => {
11
+ db.exec(`
12
+ CREATE TABLE IF NOT EXISTS protocol_event_daily (
13
+ day TEXT NOT NULL,
14
+ event TEXT NOT NULL,
15
+ count INTEGER NOT NULL DEFAULT 0,
16
+ PRIMARY KEY (day, event)
17
+ );
18
+ `);
19
+ };
@@ -0,0 +1,3 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ /** v35: refresh built-in role contracts to be English-first bilingual prompts. */
3
+ export declare const applySchemaVersion35: (db: Database) => void;
@@ -0,0 +1,23 @@
1
+ import { CODER_ROLE_DESCRIPTION, ORCHESTRATOR_ROLE_DESCRIPTION, REVIEWER_ROLE_DESCRIPTION, SENTINEL_ROLE_DESCRIPTION, TESTER_ROLE_DESCRIPTION, } from './role-templates.js';
2
+ const BUILTIN_ROLE_DESCRIPTIONS = [
3
+ ['orchestrator', ORCHESTRATOR_ROLE_DESCRIPTION],
4
+ ['coder', CODER_ROLE_DESCRIPTION],
5
+ ['reviewer', REVIEWER_ROLE_DESCRIPTION],
6
+ ['tester', TESTER_ROLE_DESCRIPTION],
7
+ ['sentinel', SENTINEL_ROLE_DESCRIPTION],
8
+ ];
9
+ /** v35: refresh built-in role contracts to be English-first bilingual prompts. */
10
+ export const applySchemaVersion35 = (db) => {
11
+ const table = db
12
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'role_templates'")
13
+ .get();
14
+ if (!table)
15
+ return;
16
+ const now = Date.now();
17
+ const updateTemplate = db.prepare(`UPDATE role_templates
18
+ SET description = ?, updated_at = ?
19
+ WHERE id = ? AND is_builtin = 1`);
20
+ for (const [id, description] of BUILTIN_ROLE_DESCRIPTIONS) {
21
+ updateTemplate.run(description, now, id);
22
+ }
23
+ };
@@ -1,3 +1,3 @@
1
1
  import type { Database } from 'better-sqlite3';
2
- export declare const CURRENT_SCHEMA_VERSION = 31;
2
+ export declare const CURRENT_SCHEMA_VERSION = 35;
3
3
  export declare const initializeRuntimeDatabase: (db: Database) => void;
@@ -24,7 +24,11 @@ import { applySchemaVersion28 } from './sqlite-schema-v28.js';
24
24
  import { applySchemaVersion29 } from './sqlite-schema-v29.js';
25
25
  import { applySchemaVersion30 } from './sqlite-schema-v30.js';
26
26
  import { applySchemaVersion31 } from './sqlite-schema-v31.js';
27
- export const CURRENT_SCHEMA_VERSION = 31;
27
+ import { applySchemaVersion32 } from './sqlite-schema-v32.js';
28
+ import { applySchemaVersion33 } from './sqlite-schema-v33.js';
29
+ import { applySchemaVersion34 } from './sqlite-schema-v34.js';
30
+ import { applySchemaVersion35 } from './sqlite-schema-v35.js';
31
+ export const CURRENT_SCHEMA_VERSION = 35;
28
32
  // Idempotent column-add helper. SQLite doesn't have `ALTER TABLE … ADD COLUMN
29
33
  // IF NOT EXISTS`, so PRAGMA-check first. Safe to call on every init; required
30
34
  // for foreign-built DBs where the version-gated migration is skipped.
@@ -176,6 +180,20 @@ export const initializeRuntimeDatabase = (db) => {
176
180
  ON workflow_schedules (enabled, next_run_at);
177
181
  CREATE INDEX IF NOT EXISTS idx_workflow_schedules_workspace
178
182
  ON workflow_schedules (workspace_id, created_at);
183
+
184
+ CREATE TABLE IF NOT EXISTS workspace_uploads (
185
+ id TEXT PRIMARY KEY,
186
+ workspace_id TEXT NOT NULL,
187
+ remote_device_id TEXT,
188
+ original_name TEXT NOT NULL,
189
+ mime_type TEXT NOT NULL,
190
+ size_bytes INTEGER NOT NULL,
191
+ storage_key TEXT NOT NULL,
192
+ created_at INTEGER NOT NULL
193
+ );
194
+
195
+ CREATE INDEX IF NOT EXISTS idx_workspace_uploads_workspace_created
196
+ ON workspace_uploads (workspace_id, created_at DESC, id DESC);
179
197
  `);
180
198
  // Idempotent column additions — run on every init regardless of
181
199
  // schema_version. The v19 migration adds these columns but is gated on
@@ -365,4 +383,20 @@ export const initializeRuntimeDatabase = (db) => {
365
383
  if (!appliedVersions.has(31)) {
366
384
  db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(31, Date.now());
367
385
  }
386
+ applySchemaVersion32(db);
387
+ if (!appliedVersions.has(32)) {
388
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(32, Date.now());
389
+ }
390
+ applySchemaVersion33(db);
391
+ if (!appliedVersions.has(33)) {
392
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(33, Date.now());
393
+ }
394
+ applySchemaVersion34(db);
395
+ if (!appliedVersions.has(34)) {
396
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(34, Date.now());
397
+ }
398
+ if (!appliedVersions.has(35)) {
399
+ applySchemaVersion35(db);
400
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(35, Date.now());
401
+ }
368
402
  };
@@ -3,6 +3,9 @@
3
3
  * agent attends to it as system content rather than mistaking it for user
4
4
  * input or its own output. `<hive-system-reminder>` is reserved for the short
5
5
  * re-anchoring action menu appended at a message tail; this `<hive-system-message>`
6
- * tag carries larger injected bodies (crash-recovery / restart env-sync).
6
+ * tag carries larger injected bodies.
7
+ *
8
+ * The content must already be escaped or intentionally contain trusted Hive
9
+ * sub-envelopes such as `<hive-memory>`.
7
10
  */
8
- export declare const wrapSystemMessage: (content: string) => string;
11
+ export declare const wrapRawSystemMessage: (content: string) => string;
@@ -3,6 +3,9 @@
3
3
  * agent attends to it as system content rather than mistaking it for user
4
4
  * input or its own output. `<hive-system-reminder>` is reserved for the short
5
5
  * re-anchoring action menu appended at a message tail; this `<hive-system-message>`
6
- * tag carries larger injected bodies (crash-recovery / restart env-sync).
6
+ * tag carries larger injected bodies.
7
+ *
8
+ * The content must already be escaped or intentionally contain trusted Hive
9
+ * sub-envelopes such as `<hive-memory>`.
7
10
  */
8
- export const wrapSystemMessage = (content) => `<hive-system-message>\n${content}\n</hive-system-message>`;
11
+ export const wrapRawSystemMessage = (content) => `<hive-system-message>\n${content}\n</hive-system-message>`;
@@ -23,6 +23,14 @@ import type { WorkflowCliPolicy } from './workflow-cli-policy.js';
23
23
  * `atomic` option alone covers atomic-save; if a future workspace
24
24
  * needs stronger settling behaviour we can promote it then.
25
25
  *
26
+ * Keep the directory walk shallow. We watch the `.hive` parent directory
27
+ * instead of the `tasks.md` file so atomic-save editors can replace the
28
+ * file without making the watcher go deaf, but recursing through
29
+ * `.hive/reports/assets` or other generated folders can consume a file
30
+ * descriptor per asset and starve later PTY spawns. `tasks.md` is a
31
+ * direct child of `.hive`, so depth 0 preserves the needed events without
32
+ * walking binary artifact trees.
33
+ *
26
34
  * Exported so the configuration is testable in isolation.
27
35
  */
28
36
  export declare const TASKS_WATCHER_OPTIONS: ChokidarOptions;
@@ -1,9 +1,9 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { readFile } from 'node:fs/promises';
3
- import { basename, dirname, normalize } from 'node:path';
3
+ import { basename, dirname, normalize, win32 } from 'node:path';
4
4
  import chokidar from 'chokidar';
5
5
  import { FEATURE_FLAGS_ALL_OFF } from './feature-flags.js';
6
- import { ensureProtocolFile, ensureTasksFile, getTasksFilePath, TASKS_FILE_NAME, } from './tasks-file.js';
6
+ import { ensureProtocolFile, ensureTasksFile, getTasksFilePath, HIVE_DIR_NAME, TASKS_FILE_NAME, } from './tasks-file.js';
7
7
  const DEBOUNCE_MS = 100;
8
8
  const WATCHER_RETRY_MS = 5000;
9
9
  const WATCHER_CLOSE_TIMEOUT_MS = 2000;
@@ -31,15 +31,44 @@ const WINDOWS_WATCHER_READY_TIMEOUT_MS = 60000;
31
31
  * `atomic` option alone covers atomic-save; if a future workspace
32
32
  * needs stronger settling behaviour we can promote it then.
33
33
  *
34
+ * Keep the directory walk shallow. We watch the `.hive` parent directory
35
+ * instead of the `tasks.md` file so atomic-save editors can replace the
36
+ * file without making the watcher go deaf, but recursing through
37
+ * `.hive/reports/assets` or other generated folders can consume a file
38
+ * descriptor per asset and starve later PTY spawns. `tasks.md` is a
39
+ * direct child of `.hive`, so depth 0 preserves the needed events without
40
+ * walking binary artifact trees.
41
+ *
34
42
  * Exported so the configuration is testable in isolation.
35
43
  */
36
44
  export const TASKS_WATCHER_OPTIONS = {
37
45
  atomic: 100,
46
+ depth: 0,
38
47
  ignoreInitial: true,
39
48
  };
40
49
  const isWindowsUncPath = (path, platform = process.platform) => platform === 'win32' && /^[\\/]{2}[^\\/]+[\\/]+[^\\/]+/u.test(path);
50
+ const normalizeWatchPath = (path, platform) => {
51
+ const normalized = platform === 'win32' ? win32.normalize(path) : normalize(path);
52
+ return platform === 'win32' ? normalized.toLowerCase() : normalized;
53
+ };
54
+ const getTasksWatchPath = (workspacePath, platform) => platform === 'win32'
55
+ ? win32.join(workspacePath, HIVE_DIR_NAME, TASKS_FILE_NAME)
56
+ : getTasksFilePath(workspacePath);
57
+ const createTasksWatcherIgnored = (workspacePath, platform) => {
58
+ const tasksPath = getTasksWatchPath(workspacePath, platform);
59
+ const hiveDir = platform === 'win32' ? win32.dirname(tasksPath) : dirname(tasksPath);
60
+ const hiveKey = normalizeWatchPath(hiveDir, platform);
61
+ const tasksKey = normalizeWatchPath(tasksPath, platform);
62
+ return (path) => {
63
+ const pathKey = normalizeWatchPath(path, platform);
64
+ if (pathKey === hiveKey || pathKey === tasksKey)
65
+ return false;
66
+ return pathKey.startsWith(`${hiveKey}/`) || pathKey.startsWith(`${hiveKey}\\`);
67
+ };
68
+ };
41
69
  export const buildTasksWatcherOptions = (workspacePath, platform = process.platform) => ({
42
70
  ...TASKS_WATCHER_OPTIONS,
71
+ ignored: createTasksWatcherIgnored(workspacePath, platform),
43
72
  ...(platform === 'win32' || isWindowsUncPath(workspacePath, platform)
44
73
  ? { interval: 500, usePolling: true }
45
74
  : {}),
@@ -1,6 +1,14 @@
1
1
  import type { AgentSummary } from '../shared/types.js';
2
- export type TeamCommand = 'send' | 'list' | 'next' | 'report' | 'recall' | 'memory_add' | 'memory_forget' | 'memory_search' | 'memory_show' | 'status' | 'cancel' | 'help' | 'spawn' | 'dismiss' | 'workflow';
2
+ export type TeamCommand = 'send' | 'list' | 'next' | 'report' | 'recall' | 'memory_add' | 'memory_apply' | 'memory_dream_show' | 'memory_forget' | 'memory_search' | 'memory_show' | 'status' | 'cancel' | 'help' | 'spawn' | 'dismiss' | 'workflow';
3
3
  export declare const commandAllowedForRole: (role: AgentSummary["role"], command: TeamCommand) => boolean;
4
+ /**
5
+ * Dispatch-target gate: the sentinel is an observer, so handing it a dispatch
6
+ * would strand the task forever (it cannot `team report` to close it).
7
+ */
8
+ export declare const assertWorkerDispatchable: (worker: {
9
+ name: string;
10
+ role: string;
11
+ }) => void;
4
12
  interface AuthenticateInput {
5
13
  fromAgentId: string | undefined;
6
14
  getAgent: (workspaceId: string, agentId: string) => AgentSummary;
@@ -7,6 +7,8 @@ const ORCHESTRATOR_COMMANDS = new Set([
7
7
  'help',
8
8
  'recall',
9
9
  'memory_add',
10
+ 'memory_apply',
11
+ 'memory_dream_show',
10
12
  'memory_forget',
11
13
  'memory_search',
12
14
  'memory_show',
@@ -19,17 +21,39 @@ const WORKER_COMMANDS = new Set([
19
21
  'status',
20
22
  'help',
21
23
  'recall',
24
+ 'memory_dream_show',
22
25
  'memory_search',
23
26
  'memory_show',
24
27
  ]);
25
28
  const WORKER_ROLES = new Set(['coder', 'reviewer', 'tester', 'custom']);
29
+ // Observe-only: a sentinel reports findings via `team status` and may read team
30
+ // memory, but it can NEVER `team report` (it is never assigned dispatch work —
31
+ // see assertWorkerDispatchable) and never dispatches or spawns.
32
+ const SENTINEL_COMMANDS = new Set([
33
+ 'status',
34
+ 'help',
35
+ 'recall',
36
+ 'memory_search',
37
+ 'memory_show',
38
+ ]);
26
39
  export const commandAllowedForRole = (role, command) => {
27
40
  if (role === 'orchestrator')
28
41
  return ORCHESTRATOR_COMMANDS.has(command);
42
+ if (role === 'sentinel')
43
+ return SENTINEL_COMMANDS.has(command);
29
44
  if (WORKER_ROLES.has(role))
30
45
  return WORKER_COMMANDS.has(command);
31
46
  return false;
32
47
  };
48
+ /**
49
+ * Dispatch-target gate: the sentinel is an observer, so handing it a dispatch
50
+ * would strand the task forever (it cannot `team report` to close it).
51
+ */
52
+ export const assertWorkerDispatchable = (worker) => {
53
+ if (worker.role === 'sentinel') {
54
+ throw new ForbiddenError(`@${worker.name} is a sentinel (observe-only); it does not take dispatches`);
55
+ }
56
+ };
33
57
  export const authenticateCliAgent = ({ fromAgentId, getAgent, token, validateToken, workspaceId, }) => {
34
58
  if (!fromAgentId) {
35
59
  throw new UnauthorizedError('Missing agent identity');