@tt-a1i/hive 1.4.3 → 1.5.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 (180) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.en.md +5 -4
  3. package/README.md +9 -1
  4. package/assets/qq-group.jpg +0 -0
  5. package/dist/bin/team.cmd +1 -0
  6. package/dist/src/cli/hive-update.d.ts +57 -0
  7. package/dist/src/cli/hive-update.js +92 -15
  8. package/dist/src/cli/hive.d.ts +57 -0
  9. package/dist/src/cli/hive.js +113 -20
  10. package/dist/src/cli/team.d.ts +1 -0
  11. package/dist/src/cli/team.js +215 -7
  12. package/dist/src/server/agent-command-resolver.d.ts +10 -1
  13. package/dist/src/server/agent-command-resolver.js +32 -4
  14. package/dist/src/server/agent-launch-resolver.js +9 -3
  15. package/dist/src/server/agent-manager-support.d.ts +28 -0
  16. package/dist/src/server/agent-manager-support.js +138 -10
  17. package/dist/src/server/agent-run-bootstrap.d.ts +17 -1
  18. package/dist/src/server/agent-run-bootstrap.js +30 -2
  19. package/dist/src/server/agent-run-starter.d.ts +7 -1
  20. package/dist/src/server/agent-run-starter.js +9 -2
  21. package/dist/src/server/agent-run-store.d.ts +1 -1
  22. package/dist/src/server/agent-runtime-close.d.ts +1 -0
  23. package/dist/src/server/agent-runtime-close.js +25 -1
  24. package/dist/src/server/agent-runtime-contract.d.ts +2 -1
  25. package/dist/src/server/agent-runtime.d.ts +1 -1
  26. package/dist/src/server/agent-runtime.js +8 -2
  27. package/dist/src/server/agent-startup-instructions.d.ts +8 -1
  28. package/dist/src/server/agent-startup-instructions.js +15 -9
  29. package/dist/src/server/agent-stdin-dispatcher.d.ts +12 -5
  30. package/dist/src/server/agent-stdin-dispatcher.js +129 -40
  31. package/dist/src/server/app.d.ts +1 -0
  32. package/dist/src/server/app.js +12 -2
  33. package/dist/src/server/cron-util.d.ts +7 -0
  34. package/dist/src/server/cron-util.js +19 -0
  35. package/dist/src/server/dispatch-ledger-store.d.ts +22 -0
  36. package/dist/src/server/dispatch-ledger-store.js +51 -3
  37. package/dist/src/server/env-sync-message.js +9 -9
  38. package/dist/src/server/fs-browse.d.ts +14 -1
  39. package/dist/src/server/fs-browse.js +48 -5
  40. package/dist/src/server/fs-pick-folder.js +58 -11
  41. package/dist/src/server/fs-sandbox.js +36 -7
  42. package/dist/src/server/hive-team-guidance.d.ts +11 -6
  43. package/dist/src/server/hive-team-guidance.js +252 -70
  44. package/dist/src/server/live-run-registry.d.ts +1 -0
  45. package/dist/src/server/live-run-registry.js +1 -1
  46. package/dist/src/server/open-target-commands.js +29 -4
  47. package/dist/src/server/orchestrator-autostart.d.ts +12 -0
  48. package/dist/src/server/orchestrator-autostart.js +15 -13
  49. package/dist/src/server/path-canonicalization.d.ts +3 -0
  50. package/dist/src/server/path-canonicalization.js +29 -0
  51. package/dist/src/server/platform-path.d.ts +3 -0
  52. package/dist/src/server/platform-path.js +13 -0
  53. package/dist/src/server/post-start-input-writer.d.ts +1 -1
  54. package/dist/src/server/post-start-input-writer.js +116 -16
  55. package/dist/src/server/preset-launch-support.d.ts +1 -1
  56. package/dist/src/server/preset-launch-support.js +33 -2
  57. package/dist/src/server/recovery-summary.d.ts +6 -1
  58. package/dist/src/server/recovery-summary.js +17 -17
  59. package/dist/src/server/restart-policy-support.d.ts +6 -1
  60. package/dist/src/server/restart-policy-support.js +9 -1
  61. package/dist/src/server/restart-policy.d.ts +2 -2
  62. package/dist/src/server/restart-policy.js +3 -1
  63. package/dist/src/server/role-template-store.d.ts +1 -0
  64. package/dist/src/server/role-template-store.js +11 -1
  65. package/dist/src/server/route-types.d.ts +43 -0
  66. package/dist/src/server/routes-runtime.js +2 -1
  67. package/dist/src/server/routes-settings.js +76 -0
  68. package/dist/src/server/routes-team.js +221 -2
  69. package/dist/src/server/routes-workflow-schedules.d.ts +2 -0
  70. package/dist/src/server/routes-workflow-schedules.js +58 -0
  71. package/dist/src/server/routes-workflows.d.ts +2 -0
  72. package/dist/src/server/routes-workflows.js +83 -0
  73. package/dist/src/server/routes.js +4 -0
  74. package/dist/src/server/runtime-restart-policy.d.ts +3 -1
  75. package/dist/src/server/runtime-restart-policy.js +3 -1
  76. package/dist/src/server/runtime-store-contract.d.ts +122 -0
  77. package/dist/src/server/runtime-store-contract.js +1 -0
  78. package/dist/src/server/runtime-store-helpers.d.ts +9 -0
  79. package/dist/src/server/runtime-store-helpers.js +101 -2
  80. package/dist/src/server/runtime-store-workflows.d.ts +6 -0
  81. package/dist/src/server/runtime-store-workflows.js +100 -0
  82. package/dist/src/server/runtime-store.d.ts +3 -70
  83. package/dist/src/server/runtime-store.js +70 -4
  84. package/dist/src/server/session-capture-claude.d.ts +23 -0
  85. package/dist/src/server/session-capture-claude.js +24 -1
  86. package/dist/src/server/session-capture-codex.d.ts +3 -3
  87. package/dist/src/server/session-capture-codex.js +9 -7
  88. package/dist/src/server/session-capture-gemini.d.ts +1 -1
  89. package/dist/src/server/session-capture-gemini.js +6 -3
  90. package/dist/src/server/session-capture-opencode.d.ts +18 -0
  91. package/dist/src/server/session-capture-opencode.js +27 -2
  92. package/dist/src/server/settings-store.d.ts +3 -0
  93. package/dist/src/server/settings-store.js +1 -0
  94. package/dist/src/server/sqlite-schema-v19.d.ts +2 -0
  95. package/dist/src/server/sqlite-schema-v19.js +17 -0
  96. package/dist/src/server/sqlite-schema-v20.d.ts +2 -0
  97. package/dist/src/server/sqlite-schema-v20.js +20 -0
  98. package/dist/src/server/sqlite-schema-v21.d.ts +2 -0
  99. package/dist/src/server/sqlite-schema-v21.js +20 -0
  100. package/dist/src/server/sqlite-schema.d.ts +1 -1
  101. package/dist/src/server/sqlite-schema.js +97 -1
  102. package/dist/src/server/startup-command-parser.d.ts +15 -0
  103. package/dist/src/server/startup-command-parser.js +33 -2
  104. package/dist/src/server/system-message.d.ts +7 -0
  105. package/dist/src/server/system-message.js +8 -1
  106. package/dist/src/server/tasks-file-watcher.d.ts +39 -1
  107. package/dist/src/server/tasks-file-watcher.js +155 -25
  108. package/dist/src/server/tasks-file.d.ts +2 -1
  109. package/dist/src/server/tasks-file.js +32 -9
  110. package/dist/src/server/tasks-websocket-server.js +13 -14
  111. package/dist/src/server/team-authz.d.ts +1 -1
  112. package/dist/src/server/team-authz.js +9 -1
  113. package/dist/src/server/team-autostaff.d.ts +16 -0
  114. package/dist/src/server/team-autostaff.js +16 -0
  115. package/dist/src/server/team-list-serializer.d.ts +1 -1
  116. package/dist/src/server/team-list-serializer.js +3 -1
  117. package/dist/src/server/team-operations.d.ts +20 -2
  118. package/dist/src/server/team-operations.js +160 -14
  119. package/dist/src/server/terminal-input-profile.js +2 -8
  120. package/dist/src/server/terminal-protocol.js +9 -3
  121. package/dist/src/server/terminal-stream-hub.js +16 -10
  122. package/dist/src/server/terminal-ws-server.js +36 -16
  123. package/dist/src/server/websocket-upgrade-safety.d.ts +10 -0
  124. package/dist/src/server/websocket-upgrade-safety.js +35 -0
  125. package/dist/src/server/windows-command-line.d.ts +3 -0
  126. package/dist/src/server/windows-command-line.js +9 -0
  127. package/dist/src/server/windows-filename.d.ts +2 -0
  128. package/dist/src/server/windows-filename.js +33 -0
  129. package/dist/src/server/workflow-cli-policy.d.ts +60 -0
  130. package/dist/src/server/workflow-cli-policy.js +110 -0
  131. package/dist/src/server/workflow-dispatch-awaiter.d.ts +12 -0
  132. package/dist/src/server/workflow-dispatch-awaiter.js +80 -0
  133. package/dist/src/server/workflow-feature.d.ts +15 -0
  134. package/dist/src/server/workflow-feature.js +15 -0
  135. package/dist/src/server/workflow-http-serializers.d.ts +64 -0
  136. package/dist/src/server/workflow-http-serializers.js +58 -0
  137. package/dist/src/server/workflow-run-log-store.d.ts +19 -0
  138. package/dist/src/server/workflow-run-log-store.js +45 -0
  139. package/dist/src/server/workflow-run-store.d.ts +50 -0
  140. package/dist/src/server/workflow-run-store.js +103 -0
  141. package/dist/src/server/workflow-runner.d.ts +147 -0
  142. package/dist/src/server/workflow-runner.js +401 -0
  143. package/dist/src/server/workflow-schedule-create.d.ts +14 -0
  144. package/dist/src/server/workflow-schedule-create.js +41 -0
  145. package/dist/src/server/workflow-schedule-store.d.ts +43 -0
  146. package/dist/src/server/workflow-schedule-store.js +112 -0
  147. package/dist/src/server/workflow-scheduler.d.ts +36 -0
  148. package/dist/src/server/workflow-scheduler.js +97 -0
  149. package/dist/src/server/workflow-script-loader.d.ts +34 -0
  150. package/dist/src/server/workflow-script-loader.js +106 -0
  151. package/dist/src/server/workspace-path-validation.js +16 -4
  152. package/dist/src/server/workspace-shell-runtime.d.ts +5 -0
  153. package/dist/src/server/workspace-shell-runtime.js +24 -2
  154. package/dist/src/server/workspace-store-contract.d.ts +4 -1
  155. package/dist/src/server/workspace-store-hydration.js +23 -7
  156. package/dist/src/server/workspace-store-mutations.js +2 -5
  157. package/dist/src/server/workspace-store-support.d.ts +4 -0
  158. package/dist/src/server/workspace-store-support.js +13 -1
  159. package/dist/src/server/workspace-store.js +38 -4
  160. package/dist/src/shared/types.d.ts +16 -1
  161. package/package.json +4 -2
  162. package/web/dist/assets/{AddWorkerDialog-DmkDOdp6.js → AddWorkerDialog-CcC-7kgG.js} +2 -2
  163. package/web/dist/assets/AddWorkspaceDialog-BDpOTfmt.js +1 -0
  164. package/web/dist/assets/{FirstRunWizard-SAd1wsH4.js → FirstRunWizard-BYX_ocQn.js} +1 -1
  165. package/web/dist/assets/{MarketplaceDrawer-B_8aG2uT.js → MarketplaceDrawer-DUxSk7db.js} +1 -1
  166. package/web/dist/assets/WhatsNewDialog-B_RlCXcV.js +1 -0
  167. package/web/dist/assets/WorkerModal-D9-7YfZZ.js +1 -0
  168. package/web/dist/assets/WorkspaceTaskDrawer-BCKoF7qc.js +1 -0
  169. package/web/dist/assets/{WorkspaceTerminalPanels-BReWh1YL.js → WorkspaceTerminalPanels-Dq8y91t2.js} +1 -1
  170. package/web/dist/assets/index-BiOvKIVw.css +1 -0
  171. package/web/dist/assets/index-DMRUklT3.js +73 -0
  172. package/web/dist/assets/path-join-7MR1s7b1.js +1 -0
  173. package/web/dist/index.html +2 -2
  174. package/web/dist/sw.js +1 -1
  175. package/web/dist/assets/AddWorkspaceDialog-BsVnH3Xe.js +0 -1
  176. package/web/dist/assets/WorkerModal-CQmjiPme.js +0 -1
  177. package/web/dist/assets/WorkspaceTaskDrawer-B0DmCWcV.js +0 -1
  178. package/web/dist/assets/chevron-right-CtLjVEl7.js +0 -1
  179. package/web/dist/assets/index-BEsTmfrO.css +0 -1
  180. package/web/dist/assets/index-Cn8X3get.js +0 -76
@@ -0,0 +1,97 @@
1
+ import { CronExpressionParser } from 'cron-parser';
2
+ const defaultComputeNextRunAt = (cron, after) => {
3
+ // cron-parser 5.x: `currentDate` is the reference point; `next()` returns
4
+ // the first strictly-greater fire. UTC by default.
5
+ const expr = CronExpressionParser.parse(cron, { currentDate: after, tz: 'UTC' });
6
+ return expr.next().toDate().getTime();
7
+ };
8
+ const DEFAULT_TICK_INTERVAL_MS = 30_000;
9
+ export const createWorkflowScheduler = (deps) => {
10
+ const computeNext = deps.computeNextRunAt ?? defaultComputeNextRunAt;
11
+ let timer = null;
12
+ const scheduler = {
13
+ async tick(now = Date.now()) {
14
+ // Experimental gate: while workflows are disabled, hold all scheduled
15
+ // runs (don't fire, don't advance nextRunAt) so they resume cleanly
16
+ // once re-enabled.
17
+ if (deps.isWorkflowEnabled && !deps.isWorkflowEnabled())
18
+ return;
19
+ const due = deps.schedules.listDueSchedules(now);
20
+ for (const schedule of due) {
21
+ // TIER 1 #4 — self-heal orphan schedules: if a schedule survived a
22
+ // workspace-delete operation (shouldn't happen after the cascade
23
+ // fix, but defending against historical orphans), drop it now so
24
+ // it doesn't error-spam every tick from here forward.
25
+ if (deps.workspaceExists && !deps.workspaceExists(schedule.workspaceId)) {
26
+ console.warn('[hive] workflow-scheduler: deleting orphan schedule', {
27
+ scheduleId: schedule.id,
28
+ workspaceId: schedule.workspaceId,
29
+ });
30
+ deps.schedules.deleteSchedule(schedule.id);
31
+ continue;
32
+ }
33
+ // Compute the next fire BEFORE firing this one so a startWorkflow
34
+ // exception cannot keep us re-firing the same row each tick.
35
+ let nextRunAt;
36
+ try {
37
+ nextRunAt = computeNext(schedule.cron, new Date(now));
38
+ }
39
+ catch (error) {
40
+ console.error('[hive] workflow-scheduler: invalid cron, disabling schedule', {
41
+ scheduleId: schedule.id,
42
+ cron: schedule.cron,
43
+ error: error instanceof Error ? error.message : String(error),
44
+ });
45
+ deps.schedules.update(schedule.id, { enabled: false });
46
+ continue;
47
+ }
48
+ // TIER 1 #5 — compare-and-swap claim. If a previous tick is still
49
+ // running (slow esbuild / slow startWorkflow / many due schedules
50
+ // in one tick) and setInterval fires again, both ticks would
51
+ // otherwise see the same due schedule and fire it twice. CAS on
52
+ // the original next_run_at means only the first tick wins;
53
+ // losers see changes=0 and quietly skip.
54
+ const claimed = deps.schedules.claimDueSchedule({
55
+ id: schedule.id,
56
+ expectedNextRunAt: schedule.nextRunAt ?? 0,
57
+ newNextRunAt: nextRunAt,
58
+ lastRunAt: now,
59
+ });
60
+ if (!claimed)
61
+ continue;
62
+ try {
63
+ await deps.startWorkflow({
64
+ workspaceId: schedule.workspaceId,
65
+ scriptPath: schedule.scriptPath,
66
+ hivePort: deps.getHivePort?.() ?? '',
67
+ ...(schedule.args !== undefined && schedule.args !== null
68
+ ? { args: schedule.args }
69
+ : {}),
70
+ });
71
+ }
72
+ catch (error) {
73
+ console.error('[hive] workflow-scheduler: startWorkflow failed', {
74
+ scheduleId: schedule.id,
75
+ error: error instanceof Error ? error.message : String(error),
76
+ });
77
+ }
78
+ }
79
+ },
80
+ start({ tickIntervalMs = DEFAULT_TICK_INTERVAL_MS } = {}) {
81
+ if (timer)
82
+ clearInterval(timer);
83
+ timer = setInterval(() => {
84
+ scheduler.tick().catch((error) => {
85
+ console.error('[hive] swallowed:workflow-scheduler.tick', error);
86
+ });
87
+ }, tickIntervalMs);
88
+ },
89
+ close() {
90
+ if (timer) {
91
+ clearInterval(timer);
92
+ timer = null;
93
+ }
94
+ },
95
+ };
96
+ return scheduler;
97
+ };
@@ -0,0 +1,34 @@
1
+ export interface WorkflowMeta {
2
+ name: string;
3
+ description: string;
4
+ cron?: string;
5
+ phases?: Array<{
6
+ title: string;
7
+ detail?: string;
8
+ model?: string;
9
+ }>;
10
+ /** TIER 2 #11 — per-script budget overrides. Hard cap on total
11
+ * `agent()` calls (default 1000 — matches CC's lifetime cap) and on
12
+ * wall-clock duration in ms (default 60 min). Both apply to the
13
+ * ENTIRE run including nested workflow() calls; exceeding either
14
+ * rejects the offending agent() call and the run transitions to
15
+ * 'failed' (or 'stopped' if duration was the trigger). */
16
+ maxAgentCalls?: number;
17
+ maxDurationMs?: number;
18
+ }
19
+ export interface LoadedWorkflow {
20
+ meta: WorkflowMeta;
21
+ scriptPath: string;
22
+ scriptHash: string;
23
+ /** Transpiled `async function __wf(...) {…}` — the runner evals it via
24
+ * `new Function(source + '; return __wf')()` and calls it with the DSL. */
25
+ compiledFunctionSource: string;
26
+ }
27
+ interface ExtractedMeta {
28
+ meta: WorkflowMeta;
29
+ body: string;
30
+ }
31
+ export declare const extractMeta: (source: string) => ExtractedMeta;
32
+ export declare const loadWorkflowScriptSource: (source: string, scriptPath: string) => Promise<LoadedWorkflow>;
33
+ export declare const loadWorkflowScriptFile: (absPath: string) => Promise<LoadedWorkflow>;
34
+ export {};
@@ -0,0 +1,106 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ const DSL_PARAMS = 'agent, parallel, pipeline, phase, log, workflow, args';
4
+ // Find the matching close brace for the object literal whose '{' is at
5
+ // openIndex, ignoring braces inside '...' "..." `...` strings and // /* */
6
+ // comments so they don't miscount the depth.
7
+ const matchBrace = (source, openIndex) => {
8
+ let depth = 0;
9
+ let i = openIndex;
10
+ let str = null;
11
+ while (i < source.length) {
12
+ const ch = source[i];
13
+ const next = source[i + 1];
14
+ if (str) {
15
+ if (ch === '\\') {
16
+ i += 2;
17
+ continue;
18
+ }
19
+ if (ch === str)
20
+ str = null;
21
+ i += 1;
22
+ continue;
23
+ }
24
+ if (ch === '/' && next === '/') {
25
+ const nl = source.indexOf('\n', i);
26
+ i = nl === -1 ? source.length : nl;
27
+ continue;
28
+ }
29
+ if (ch === '/' && next === '*') {
30
+ const end = source.indexOf('*/', i + 2);
31
+ i = end === -1 ? source.length : end + 2;
32
+ continue;
33
+ }
34
+ if (ch === "'" || ch === '"' || ch === '`') {
35
+ str = ch;
36
+ i += 1;
37
+ continue;
38
+ }
39
+ if (ch === '{')
40
+ depth += 1;
41
+ else if (ch === '}') {
42
+ depth -= 1;
43
+ if (depth === 0)
44
+ return i;
45
+ }
46
+ i += 1;
47
+ }
48
+ throw new Error('workflow meta: unbalanced braces in `export const meta`');
49
+ };
50
+ export const extractMeta = (source) => {
51
+ const re = /export\s+const\s+meta\s*(?::[^=]+)?=\s*\{/;
52
+ const m = re.exec(source);
53
+ if (!m) {
54
+ throw new Error('workflow script must `export const meta = { name, description }`');
55
+ }
56
+ const braceStart = source.indexOf('{', m.index + m[0].length - 1);
57
+ const braceEnd = matchBrace(source, braceStart);
58
+ const literal = source.slice(braceStart, braceEnd + 1);
59
+ let meta;
60
+ try {
61
+ // meta MUST be a pure literal (no calls/vars); eval it in isolation.
62
+ meta = new Function(`return (${literal})`)();
63
+ }
64
+ catch (error) {
65
+ throw new Error(`workflow meta is not a plain literal: ${error instanceof Error ? error.message : String(error)}`);
66
+ }
67
+ if (!meta || typeof meta.name !== 'string' || !meta.name.trim()) {
68
+ throw new Error('workflow meta requires a non-empty `name`');
69
+ }
70
+ if (typeof meta.description !== 'string') {
71
+ throw new Error('workflow meta requires a `description`');
72
+ }
73
+ let after = braceEnd + 1;
74
+ const tailMatch = /^(\s*as\s+const)?\s*;?/.exec(source.slice(after));
75
+ if (tailMatch)
76
+ after += tailMatch[0].length;
77
+ const body = source.slice(0, m.index) + source.slice(after);
78
+ return { meta, body };
79
+ };
80
+ // Module-level cache for esbuild's dynamic import. esbuild's top-level
81
+ // invariant check requires `new TextEncoder().encode('') instanceof
82
+ // Uint8Array` — true in Node, FALSE in jsdom's realm. Workflow runs are not
83
+ // designed to be exercised end-to-end inside a jsdom test environment;
84
+ // jsdom-hosted tests stub the run step at the network layer.
85
+ let esbuildModulePromise = null;
86
+ const loadEsbuild = async () => {
87
+ if (!esbuildModulePromise)
88
+ esbuildModulePromise = import('esbuild');
89
+ return esbuildModulePromise;
90
+ };
91
+ export const loadWorkflowScriptSource = async (source, scriptPath) => {
92
+ if (/^\s*import\s/m.test(source)) {
93
+ throw new Error('workflow scripts may not use `import`; use the ambient DSL + inline schemas');
94
+ }
95
+ const { meta, body } = extractMeta(source);
96
+ const wrapped = `async function __wf(${DSL_PARAMS}) {\n${body}\n}`;
97
+ // Lazy-load esbuild: its native binary breaks under jsdom/worker contexts
98
+ // used by the web test suite, so importing it at module-load time would
99
+ // crash any test file that transitively pulls in the runtime store. The
100
+ // transpile path is only reached when a workflow actually runs.
101
+ const { transform } = await loadEsbuild();
102
+ const { code } = await transform(wrapped, { loader: 'ts', target: 'es2022' });
103
+ const scriptHash = createHash('sha256').update(code).digest('hex');
104
+ return { meta, scriptPath, scriptHash, compiledFunctionSource: code };
105
+ };
106
+ export const loadWorkflowScriptFile = async (absPath) => loadWorkflowScriptSource(await readFile(absPath, 'utf8'), absPath);
@@ -1,5 +1,6 @@
1
- import { realpathSync, statSync } from 'node:fs';
1
+ import { statSync } from 'node:fs';
2
2
  import { BadRequestError } from './http-errors.js';
3
+ import { realpathNative } from './path-canonicalization.js';
3
4
  export const validateWorkspacePath = (path) => {
4
5
  if (typeof path !== 'string' || path.trim().length === 0) {
5
6
  throw new BadRequestError('Workspace path is required');
@@ -7,16 +8,27 @@ export const validateWorkspacePath = (path) => {
7
8
  const candidate = path.trim();
8
9
  let resolved;
9
10
  try {
10
- resolved = realpathSync(candidate);
11
+ resolved = realpathNative(candidate);
11
12
  }
12
- catch {
13
+ catch (error) {
14
+ const code = error?.code;
15
+ if (code === 'EACCES' || code === 'EPERM') {
16
+ throw new BadRequestError(`Workspace path is not accessible: ${candidate}`);
17
+ }
18
+ if (code === 'ENAMETOOLONG') {
19
+ throw new BadRequestError(`Workspace path is too long: ${candidate}`);
20
+ }
13
21
  throw new BadRequestError(`Workspace path does not exist: ${candidate}`);
14
22
  }
15
23
  let stat;
16
24
  try {
17
25
  stat = statSync(resolved);
18
26
  }
19
- catch {
27
+ catch (error) {
28
+ const code = error?.code;
29
+ if (code === 'EACCES' || code === 'EPERM') {
30
+ throw new BadRequestError(`Workspace path is not accessible: ${candidate}`);
31
+ }
20
32
  throw new BadRequestError(`Workspace path does not exist: ${candidate}`);
21
33
  }
22
34
  if (!stat.isDirectory()) {
@@ -7,6 +7,11 @@ export declare const resolveWorkspaceShellLaunch: (env?: NodeJS.ProcessEnv, plat
7
7
  args: string[];
8
8
  command: string;
9
9
  };
10
+ export declare const resolveWorkspaceShellStart: (workspacePath: string, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform) => {
11
+ args: string[];
12
+ command: string;
13
+ cwd: string;
14
+ };
10
15
  export declare const createWorkspaceShellRuntime: (agentManager: AgentManager | undefined) => {
11
16
  close(): void;
12
17
  closeRun(workspaceId: string, runId: string): boolean;
@@ -1,4 +1,5 @@
1
1
  import { basename } from 'node:path';
2
+ import { escapeCmdToken } from './windows-command-line.js';
2
3
  const WORKSPACE_SHELL_SUFFIX = ':shell';
3
4
  const WORKSPACE_SHELL_LABEL = 'Shell';
4
5
  const EXITED_SHELL_RETENTION_MS = 5000;
@@ -20,6 +21,27 @@ export const resolveWorkspaceShellLaunch = (env = process.env, platform = proces
20
21
  const command = env.SHELL || '/bin/sh';
21
22
  return { command, args: shouldUseLoginShell(command) ? ['-l'] : [] };
22
23
  };
24
+ const isWindowsUncPath = (path, platform = process.platform) => platform === 'win32' && /^[\\/]{2}[^\\/]+[\\/]+[^\\/]+/u.test(path);
25
+ const getWindowsSafeShellCwd = (env = process.env) => {
26
+ const systemRoot = getEnvValue(env, 'SystemRoot', 'win32');
27
+ if (systemRoot)
28
+ return systemRoot;
29
+ const systemDrive = getEnvValue(env, 'SystemDrive', 'win32');
30
+ if (systemDrive)
31
+ return `${systemDrive}\\`;
32
+ return process.cwd();
33
+ };
34
+ export const resolveWorkspaceShellStart = (workspacePath, env = process.env, platform = process.platform) => {
35
+ const launch = resolveWorkspaceShellLaunch(env, platform);
36
+ if (isWindowsUncPath(workspacePath, platform)) {
37
+ return {
38
+ args: ['/d', '/s', '/k', `pushd ${escapeCmdToken(workspacePath)}`],
39
+ command: launch.command,
40
+ cwd: getWindowsSafeShellCwd(env),
41
+ };
42
+ }
43
+ return { ...launch, cwd: workspacePath };
44
+ };
23
45
  export const createWorkspaceShellRuntime = (agentManager) => {
24
46
  const labelsByRunId = new Map();
25
47
  const workspaceIdsByRunId = new Map();
@@ -176,12 +198,12 @@ export const createWorkspaceShellRuntime = (agentManager) => {
176
198
  },
177
199
  async start(workspace) {
178
200
  const startedAt = Date.now();
179
- const launch = resolveWorkspaceShellLaunch();
201
+ const launch = resolveWorkspaceShellStart(workspace.path);
180
202
  const run = await requireManager().startAgent({
181
203
  agentId: getWorkspaceShellAgentId(workspace.id),
182
204
  args: launch.args,
183
205
  command: launch.command,
184
- cwd: workspace.path,
206
+ cwd: launch.cwd,
185
207
  env: {
186
208
  COLORTERM: 'truecolor',
187
209
  FORCE_COLOR: '1',
@@ -1,4 +1,4 @@
1
- import type { AgentSummary, TeamListItem, WorkerRole, WorkspaceSummary } from '../shared/types.js';
1
+ import type { AgentSummary, TeamListItem, WorkerRole, WorkerSpawnSource, WorkspaceSummary } from '../shared/types.js';
2
2
  export interface WorkspaceRecord {
3
3
  summary: WorkspaceSummary;
4
4
  agents: AgentSummary[];
@@ -7,6 +7,8 @@ export interface WorkerInput {
7
7
  description?: string;
8
8
  name: string;
9
9
  role: WorkerRole;
10
+ ephemeral?: boolean;
11
+ spawnedBy?: WorkerSpawnSource;
10
12
  }
11
13
  export interface WorkspaceStore {
12
14
  addWorker: (workspaceId: string, input: WorkerInput) => AgentSummary;
@@ -19,6 +21,7 @@ export interface WorkspaceStore {
19
21
  getWorkerByName: (workspaceId: string, workerName: string) => AgentSummary;
20
22
  getWorkspaceSnapshot: (workspaceId: string) => WorkspaceRecord;
21
23
  hasAgent: (workspaceId: string, agentId: string) => boolean;
24
+ hasWorkspace: (workspaceId: string) => boolean;
22
25
  listWorkers: (workspaceId: string) => TeamListItem[];
23
26
  listWorkspaces: () => WorkspaceSummary[];
24
27
  markAgentStarted: (workspaceId: string, agentId: string) => void;
@@ -1,5 +1,5 @@
1
1
  import { getDefaultRoleDescription } from './role-templates.js';
2
- import { applyPendingTaskCount, createOrchestrator, isWorkerAgent, } from './workspace-store-support.js';
2
+ import { applyPendingTaskCount, createOrchestrator, createWorkflowAgent, isWorkerAgent, } from './workspace-store-support.js';
3
3
  const createWorkerSummary = (workspaceId, row) => ({
4
4
  id: row.id,
5
5
  workspaceId,
@@ -8,7 +8,25 @@ const createWorkerSummary = (workspaceId, row) => ({
8
8
  role: row.role,
9
9
  status: 'stopped',
10
10
  pendingTaskCount: 0,
11
+ ephemeral: row.ephemeral === 1,
12
+ spawnedBy: row.spawned_by ?? null,
11
13
  });
14
+ /**
15
+ * Build the worker SELECT defensively. The global runtime data dir
16
+ * (`~/.config/hive`) is shared across every Hive install on the machine, so a
17
+ * DB may have been migrated forward by a NEWER Hive whose `schema_version`
18
+ * already lists 19+. In that case our own v19 ALTER is skipped and the
19
+ * `workers` table can lack `ephemeral`/`spawned_by`. Selecting a non-existent
20
+ * column throws and crashes startup, so we only request the optional columns
21
+ * when they actually exist; `createWorkerSummary` defaults them otherwise.
22
+ */
23
+ const buildWorkerSelect = (db, whereClause) => {
24
+ const present = new Set(db.prepare('PRAGMA table_info(workers)').all().map((c) => c.name));
25
+ const optional = ['ephemeral', 'spawned_by'].filter((column) => present.has(column));
26
+ const columns = ['id', 'workspace_id', 'name', 'description', 'role', ...optional].join(', ');
27
+ const where = whereClause ? `${whereClause} ` : '';
28
+ return `SELECT ${columns} FROM workers ${where}ORDER BY created_at ASC`;
29
+ };
12
30
  const applyMessageKinds = (workspaces, messageKinds, workspaceId) => {
13
31
  for (const row of messageKinds) {
14
32
  if (workspaceId && row.workspace_id !== workspaceId) {
@@ -33,10 +51,10 @@ export const hydrateWorkspaceFromDb = (db, workspaces, messageKinds, workspaceId
33
51
  }
34
52
  workspaces.set(row.id, {
35
53
  summary: { id: row.id, name: row.name, path: row.path },
36
- agents: [createOrchestrator(row.id)],
54
+ agents: [createOrchestrator(row.id), createWorkflowAgent(row.id)],
37
55
  });
38
56
  for (const workerRow of db
39
- .prepare('SELECT id, workspace_id, name, description, role FROM workers WHERE workspace_id = ? ORDER BY created_at ASC')
57
+ .prepare(buildWorkerSelect(db, 'WHERE workspace_id = ?'))
40
58
  .all(workspaceId)) {
41
59
  workspaces.get(workspaceId)?.agents.push(createWorkerSummary(workerRow.workspace_id, workerRow));
42
60
  }
@@ -48,12 +66,10 @@ export const seedWorkspacesFromDb = (db, workspaces, messageKinds) => {
48
66
  .all()) {
49
67
  workspaces.set(row.id, {
50
68
  summary: { id: row.id, name: row.name, path: row.path },
51
- agents: [createOrchestrator(row.id)],
69
+ agents: [createOrchestrator(row.id), createWorkflowAgent(row.id)],
52
70
  });
53
71
  }
54
- for (const row of db
55
- .prepare('SELECT id, workspace_id, name, description, role FROM workers ORDER BY created_at ASC')
56
- .all()) {
72
+ for (const row of db.prepare(buildWorkerSelect(db, '')).all()) {
57
73
  workspaces.get(row.workspace_id)?.agents.push(createWorkerSummary(row.workspace_id, row));
58
74
  }
59
75
  applyMessageKinds(workspaces, messageKinds);
@@ -24,11 +24,8 @@ export const getWorkerByNameRecord = (workspaces, workspaceId, workerName) => {
24
24
  return worker;
25
25
  };
26
26
  export const markAgentStarted = (workspaces, workspaceId, agentId) => {
27
- // Worker status tracks "is this agent currently working", not "are there
28
- // pending tasks". A freshly started PTY hasn't done anything yet, even if
29
- // dispatch ledger replayed pendingTaskCount > 0 during hydration. The next
30
- // team send will flip status to 'working' via markTaskDispatched.
31
- getAgentRecord(workspaces, workspaceId, agentId).status = 'idle';
27
+ const agent = getAgentRecord(workspaces, workspaceId, agentId);
28
+ agent.status = isWorkerAgent(agent) ? getStatusFromPendingCount(agent.pendingTaskCount) : 'idle';
32
29
  };
33
30
  export const markAgentStopped = (workspaces, workspaceId, agentId) => {
34
31
  getAgentRecord(workspaces, workspaceId, agentId).status = 'stopped';
@@ -15,11 +15,15 @@ export interface WorkerRow {
15
15
  name: string;
16
16
  description: string | null;
17
17
  role: WorkerRole;
18
+ ephemeral: number;
19
+ spawned_by: string | null;
18
20
  }
19
21
  export interface WorkspaceSummaryRow extends WorkspaceRow {
20
22
  }
21
23
  export declare const getOrchestratorId: (workspaceId: string) => string;
22
24
  export declare const createOrchestrator: (workspaceId: string) => AgentSummary;
25
+ export declare const getWorkflowAgentId: (workspaceId: string) => string;
26
+ export declare const createWorkflowAgent: (workspaceId: string) => AgentSummary;
23
27
  export declare const isWorkerAgent: (agent: AgentSummary) => agent is AgentSummary & {
24
28
  role: WorkerRole;
25
29
  };
@@ -9,8 +9,20 @@ export const createOrchestrator = (workspaceId) => ({
9
9
  status: 'stopped',
10
10
  pendingTaskCount: 0,
11
11
  });
12
+ export const getWorkflowAgentId = (workspaceId) => `${workspaceId}:__workflow__`;
13
+ // In-memory pseudo-agent (no DB row, no PTY) that gives the deterministic
14
+ // workflow runner a dispatch identity — mirrors the orchestrator pseudo-agent.
15
+ export const createWorkflowAgent = (workspaceId) => ({
16
+ id: getWorkflowAgentId(workspaceId),
17
+ workspaceId,
18
+ name: 'Workflow',
19
+ description: 'Hive workflow runner — deterministic multi-agent orchestration driver.',
20
+ role: 'workflow',
21
+ status: 'stopped',
22
+ pendingTaskCount: 0,
23
+ });
12
24
  export const isWorkerAgent = (agent) => {
13
- return agent.role !== 'orchestrator';
25
+ return agent.role !== 'orchestrator' && agent.role !== 'workflow';
14
26
  };
15
27
  export const getStatusFromPendingCount = (pendingTaskCount) => {
16
28
  return pendingTaskCount > 0 ? 'working' : 'idle';
@@ -1,9 +1,10 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { ConflictError } from './http-errors.js';
3
+ import { sameFilesystemPath } from './path-canonicalization.js';
3
4
  import { getDefaultRoleDescription } from './role-templates.js';
4
5
  import { hydrateWorkspaceFromDb, seedWorkspacesFromDb } from './workspace-store-hydration.js';
5
6
  import { getAgentRecord, getWorkerByNameRecord, getWorkerRecord, markAgentStarted, markAgentStopped, markTaskCancelled, markTaskDispatched, markTaskReported, } from './workspace-store-mutations.js';
6
- import { createOrchestrator, isWorkerAgent, } from './workspace-store-support.js';
7
+ import { createOrchestrator, createWorkflowAgent, isWorkerAgent, } from './workspace-store-support.js';
7
8
  const normalizeWorkerName = (name) => {
8
9
  const trimmed = name.trim();
9
10
  if (!trimmed)
@@ -37,15 +38,23 @@ export const createWorkspaceStore = (db, messageKinds) => {
37
38
  role: input.role,
38
39
  status: 'stopped',
39
40
  pendingTaskCount: 0,
41
+ ephemeral: input.ephemeral ?? false,
42
+ spawnedBy: input.spawnedBy ?? null,
40
43
  };
41
- db.prepare('INSERT INTO workers (id, workspace_id, name, description, role, created_at) VALUES (?, ?, ?, ?, ?, ?)').run(worker.id, workspaceId, worker.name, worker.description, worker.role, Date.now());
44
+ db.prepare('INSERT INTO workers (id, workspace_id, name, description, role, ephemeral, spawned_by, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)').run(worker.id, workspaceId, worker.name, worker.description, worker.role, worker.ephemeral ? 1 : 0, worker.spawnedBy, Date.now());
42
45
  workspace.agents.push(worker);
43
46
  return worker;
44
47
  },
45
48
  createWorkspace(path, name) {
49
+ const existing = Array.from(workspaces.values()).find((workspace) => sameFilesystemPath(workspace.summary.path, path));
50
+ if (existing)
51
+ throw new ConflictError(`Workspace path already exists: ${path}`);
46
52
  const summary = { id: randomUUID(), name, path };
47
53
  db.prepare('INSERT INTO workspaces (id, name, path, created_at) VALUES (?, ?, ?, ?)').run(summary.id, name, path, Date.now());
48
- workspaces.set(summary.id, { summary, agents: [createOrchestrator(summary.id)] });
54
+ workspaces.set(summary.id, {
55
+ summary,
56
+ agents: [createOrchestrator(summary.id), createWorkflowAgent(summary.id)],
57
+ });
49
58
  return summary;
50
59
  },
51
60
  deleteWorkspace(workspaceId) {
@@ -59,6 +68,23 @@ export const createWorkspaceStore = (db, messageKinds) => {
59
68
  for (const agentId of agentIds)
60
69
  deleteAgentRuns.run(agentId);
61
70
  db.prepare('DELETE FROM workers WHERE workspace_id = ?').run(workspaceId);
71
+ // TIER 1 #4 — cascade workflow tables. Without this, the scheduler's
72
+ // listDueSchedules keeps firing schedules for the dead workspace
73
+ // every minute (the startWorkflow then crashes in
74
+ // getWorkflowAgentId / addWorkerWithLaunch and `nextRunAt` is
75
+ // rewritten to fire again next tick — a permanent error-spam
76
+ // loop). Orphan workflow_runs / dispatches would otherwise also
77
+ // accumulate forever. The dispatches DELETE in particular hits the
78
+ // workflow-tagged subset; non-workflow dispatches were already
79
+ // cleared via the deleteWorker cascade above.
80
+ db.prepare('DELETE FROM workflow_schedules WHERE workspace_id = ?').run(workspaceId);
81
+ // TIER 2 #3 — also wipe the log table; FK is on run_id, so we
82
+ // have to clear it BEFORE deleting workflow_runs (the lookup
83
+ // would otherwise miss the rows we're about to delete).
84
+ db.prepare(`DELETE FROM workflow_run_logs
85
+ WHERE run_id IN (SELECT id FROM workflow_runs WHERE workspace_id = ?)`).run(workspaceId);
86
+ db.prepare('DELETE FROM workflow_runs WHERE workspace_id = ?').run(workspaceId);
87
+ db.prepare('DELETE FROM dispatches WHERE workspace_id = ?').run(workspaceId);
62
88
  db.prepare('DELETE FROM workspaces WHERE id = ?').run(workspaceId);
63
89
  })();
64
90
  workspaces.delete(workspaceId);
@@ -99,17 +125,25 @@ export const createWorkspaceStore = (db, messageKinds) => {
99
125
  listWorkers(workspaceId) {
100
126
  return getWorkspace(workspaceId)
101
127
  .agents.filter(isWorkerAgent)
102
- .map(({ id, name, role, status, pendingTaskCount }) => ({
128
+ .map(({ id, name, role, status, pendingTaskCount, ephemeral, spawnedBy }) => ({
103
129
  id,
104
130
  name,
105
131
  role,
106
132
  status,
107
133
  pendingTaskCount,
134
+ // Carry the lifecycle marker through to the team panel so workflow-
135
+ // spawned ephemeral workers can be visually distinguished from the
136
+ // user's persistent team (M10).
137
+ ...(ephemeral === true ? { ephemeral: true } : {}),
138
+ ...(spawnedBy ? { spawnedBy } : {}),
108
139
  }));
109
140
  },
110
141
  listWorkspaces() {
111
142
  return Array.from(workspaces.values(), (workspace) => workspace.summary);
112
143
  },
144
+ hasWorkspace(workspaceId) {
145
+ return workspaces.has(workspaceId);
146
+ },
113
147
  markAgentStarted: (workspaceId, agentId) => markAgentStarted(workspaces, workspaceId, agentId),
114
148
  markAgentStopped: (workspaceId, agentId) => markAgentStopped(workspaces, workspaceId, agentId),
115
149
  markTaskDispatched: (workspaceId, workerId) => markTaskDispatched(workspaces, workspaceId, workerId),
@@ -1,6 +1,9 @@
1
1
  export declare const agentStatuses: readonly ["idle", "working", "stopped"];
2
2
  export type AgentStatus = (typeof agentStatuses)[number];
3
3
  export type WorkerRole = 'coder' | 'reviewer' | 'tester' | 'custom';
4
+ /** How a worker came to exist: spawned by a workflow run or by the orchestrator
5
+ * (via `team spawn`). Absent/null for user-added persistent workers. */
6
+ export type WorkerSpawnSource = 'workflow' | 'orchestrator';
4
7
  export interface WorkspaceSummary {
5
8
  id: string;
6
9
  name: string;
@@ -11,9 +14,11 @@ export interface AgentSummary {
11
14
  workspaceId: string;
12
15
  name: string;
13
16
  description: string;
14
- role: WorkerRole | 'orchestrator';
17
+ role: WorkerRole | 'orchestrator' | 'workflow';
15
18
  status: AgentStatus;
16
19
  pendingTaskCount: number;
20
+ ephemeral?: boolean;
21
+ spawnedBy?: WorkerSpawnSource | null;
17
22
  }
18
23
  export interface TeamListItem {
19
24
  id: string;
@@ -34,6 +39,14 @@ export interface TeamListItem {
34
39
  * the role-letter avatar.
35
40
  */
36
41
  commandPresetId?: string;
42
+ /** Lifecycle marker — true for workers spawned by `team spawn` or by the
43
+ * workflow runner (auto-dismissed after their dispatch). Drives the team
44
+ * panel's visual distinction so workflow-spawned agents read as the live
45
+ * workflow fleet, not as members of the user's persistent team (M10). */
46
+ ephemeral?: boolean;
47
+ /** When `ephemeral`, identifies the creator: 'orchestrator' (via `team spawn`)
48
+ * or 'workflow' (via the workflow runner's `agent()` call). */
49
+ spawnedBy?: 'orchestrator' | 'workflow';
37
50
  }
38
51
  /**
39
52
  * Wire payload shape for /api/workspaces/:id/team and worker-creation responses.
@@ -48,4 +61,6 @@ export interface TeamListItemPayload {
48
61
  pending_task_count: number;
49
62
  last_pty_line: string | null;
50
63
  command_preset_id: string | null;
64
+ ephemeral?: boolean;
65
+ spawned_by?: 'orchestrator' | 'workflow' | null;
51
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tt-a1i/hive",
3
- "version": "1.4.3",
3
+ "version": "1.5.0",
4
4
  "description": "Browser-native hive-mind for CLI coding agents — Claude Code, Codex, Gemini, and OpenCode collaborate as real PTY processes via a team protocol.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.30.3",
@@ -64,7 +64,7 @@
64
64
  "release:dry": "pnpm check && pnpm build && pnpm test && pnpm pack:check && pnpm pack:smoke",
65
65
  "sync:marketplace": "node scripts/sync-marketplace.mjs",
66
66
  "test": "vitest run",
67
- "test:windows": "vitest run tests/unit/agent-command-resolver.test.ts tests/unit/session-capture-multi-cli.test.ts tests/unit/claude-session-support.test.ts tests/unit/worker-name-generator.test.ts tests/unit/open-target-commands.test.ts tests/unit/sw-template-substitution.test.ts tests/unit/build-sw-plugin.test.ts tests/server/fs-pick-folder.test.ts tests/server/fs-browse.test.ts tests/server/schema-version.test.ts tests/server/runtime-rehydration.test.ts tests/server/open-workspace-route.test.ts tests/server/static-pwa.test.ts tests/web/workspace-picker.test.tsx tests/web/confirm-dialog.test.tsx tests/web/toast.test.tsx tests/web/open-workspace-button.test.tsx tests/web/register-service-worker.test.ts tests/web/use-shortcut-action.test.ts tests/web/update-available-toast.test.tsx tests/web/runtime-offline-page.test.tsx tests/web/use-terminal-panel-height.test.ts tests/web/use-terminal-panel-tabs.test.ts tests/web/terminal-tabs.test.tsx tests/web/terminal-bottom-panel.test.tsx --no-file-parallelism --maxWorkers=1 --testTimeout=60000 --hookTimeout=60000",
67
+ "test:windows": "node scripts/run-windows-tests.mjs",
68
68
  "test:watch": "vitest"
69
69
  },
70
70
  "dependencies": {
@@ -85,6 +85,8 @@
85
85
  "class-variance-authority": "^0.7.1",
86
86
  "clsx": "^2.1.1",
87
87
  "commander": "^14.0.0",
88
+ "cron-parser": "^5.5.0",
89
+ "esbuild": "^0.28.0",
88
90
  "gray-matter": "^4.0.3",
89
91
  "isomorphic-dompurify": "^3.14.0",
90
92
  "lucide-react": "^1.8.0",