@tt-a1i/hive 1.4.4 → 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 (164) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +8 -0
  3. package/assets/qq-group.jpg +0 -0
  4. package/dist/bin/team.cmd +1 -0
  5. package/dist/src/cli/hive-update.d.ts +45 -17
  6. package/dist/src/cli/hive-update.js +63 -25
  7. package/dist/src/cli/hive.d.ts +25 -0
  8. package/dist/src/cli/hive.js +41 -3
  9. package/dist/src/cli/team.d.ts +1 -0
  10. package/dist/src/cli/team.js +199 -3
  11. package/dist/src/server/agent-command-resolver.js +3 -19
  12. package/dist/src/server/agent-manager-support.d.ts +2 -2
  13. package/dist/src/server/agent-manager-support.js +98 -24
  14. package/dist/src/server/agent-run-starter.d.ts +7 -1
  15. package/dist/src/server/agent-run-starter.js +9 -2
  16. package/dist/src/server/agent-run-store.d.ts +1 -1
  17. package/dist/src/server/agent-runtime-close.d.ts +1 -0
  18. package/dist/src/server/agent-runtime-close.js +25 -1
  19. package/dist/src/server/agent-runtime-contract.d.ts +2 -1
  20. package/dist/src/server/agent-runtime.d.ts +1 -1
  21. package/dist/src/server/agent-runtime.js +8 -2
  22. package/dist/src/server/agent-startup-instructions.d.ts +8 -1
  23. package/dist/src/server/agent-startup-instructions.js +15 -9
  24. package/dist/src/server/agent-stdin-dispatcher.d.ts +12 -5
  25. package/dist/src/server/agent-stdin-dispatcher.js +129 -40
  26. package/dist/src/server/cron-util.d.ts +7 -0
  27. package/dist/src/server/cron-util.js +19 -0
  28. package/dist/src/server/dispatch-ledger-store.d.ts +22 -0
  29. package/dist/src/server/dispatch-ledger-store.js +51 -3
  30. package/dist/src/server/env-sync-message.js +9 -9
  31. package/dist/src/server/fs-pick-folder.js +4 -0
  32. package/dist/src/server/fs-sandbox.js +36 -7
  33. package/dist/src/server/hive-team-guidance.d.ts +11 -6
  34. package/dist/src/server/hive-team-guidance.js +252 -71
  35. package/dist/src/server/live-run-registry.d.ts +1 -0
  36. package/dist/src/server/live-run-registry.js +1 -1
  37. package/dist/src/server/open-target-commands.js +5 -6
  38. package/dist/src/server/orchestrator-autostart.d.ts +12 -0
  39. package/dist/src/server/orchestrator-autostart.js +15 -13
  40. package/dist/src/server/path-canonicalization.d.ts +3 -0
  41. package/dist/src/server/path-canonicalization.js +29 -0
  42. package/dist/src/server/platform-path.d.ts +3 -0
  43. package/dist/src/server/platform-path.js +13 -0
  44. package/dist/src/server/post-start-input-writer.d.ts +1 -1
  45. package/dist/src/server/post-start-input-writer.js +110 -13
  46. package/dist/src/server/preset-launch-support.d.ts +1 -1
  47. package/dist/src/server/preset-launch-support.js +33 -2
  48. package/dist/src/server/recovery-summary.d.ts +6 -1
  49. package/dist/src/server/recovery-summary.js +17 -17
  50. package/dist/src/server/restart-policy-support.d.ts +6 -1
  51. package/dist/src/server/restart-policy-support.js +9 -1
  52. package/dist/src/server/restart-policy.d.ts +2 -2
  53. package/dist/src/server/restart-policy.js +3 -1
  54. package/dist/src/server/role-template-store.d.ts +1 -0
  55. package/dist/src/server/role-template-store.js +11 -1
  56. package/dist/src/server/route-types.d.ts +43 -0
  57. package/dist/src/server/routes-runtime.js +2 -1
  58. package/dist/src/server/routes-settings.js +76 -0
  59. package/dist/src/server/routes-team.js +211 -1
  60. package/dist/src/server/routes-workflow-schedules.d.ts +2 -0
  61. package/dist/src/server/routes-workflow-schedules.js +58 -0
  62. package/dist/src/server/routes-workflows.d.ts +2 -0
  63. package/dist/src/server/routes-workflows.js +83 -0
  64. package/dist/src/server/routes.js +4 -0
  65. package/dist/src/server/runtime-restart-policy.d.ts +3 -1
  66. package/dist/src/server/runtime-restart-policy.js +3 -1
  67. package/dist/src/server/runtime-store-contract.d.ts +122 -0
  68. package/dist/src/server/runtime-store-contract.js +1 -0
  69. package/dist/src/server/runtime-store-helpers.d.ts +9 -0
  70. package/dist/src/server/runtime-store-helpers.js +101 -2
  71. package/dist/src/server/runtime-store-workflows.d.ts +6 -0
  72. package/dist/src/server/runtime-store-workflows.js +100 -0
  73. package/dist/src/server/runtime-store.d.ts +3 -72
  74. package/dist/src/server/runtime-store.js +70 -4
  75. package/dist/src/server/session-capture-codex.d.ts +3 -3
  76. package/dist/src/server/session-capture-codex.js +9 -7
  77. package/dist/src/server/session-capture-gemini.d.ts +1 -1
  78. package/dist/src/server/session-capture-gemini.js +6 -3
  79. package/dist/src/server/settings-store.d.ts +3 -0
  80. package/dist/src/server/settings-store.js +1 -0
  81. package/dist/src/server/sqlite-schema-v19.d.ts +2 -0
  82. package/dist/src/server/sqlite-schema-v19.js +17 -0
  83. package/dist/src/server/sqlite-schema-v20.d.ts +2 -0
  84. package/dist/src/server/sqlite-schema-v20.js +20 -0
  85. package/dist/src/server/sqlite-schema-v21.d.ts +2 -0
  86. package/dist/src/server/sqlite-schema-v21.js +20 -0
  87. package/dist/src/server/sqlite-schema.d.ts +1 -1
  88. package/dist/src/server/sqlite-schema.js +97 -1
  89. package/dist/src/server/system-message.d.ts +7 -0
  90. package/dist/src/server/system-message.js +8 -1
  91. package/dist/src/server/tasks-file-watcher.d.ts +13 -1
  92. package/dist/src/server/tasks-file-watcher.js +127 -23
  93. package/dist/src/server/tasks-file.d.ts +2 -1
  94. package/dist/src/server/tasks-file.js +32 -9
  95. package/dist/src/server/tasks-websocket-server.js +13 -14
  96. package/dist/src/server/team-authz.d.ts +1 -1
  97. package/dist/src/server/team-authz.js +9 -1
  98. package/dist/src/server/team-autostaff.d.ts +16 -0
  99. package/dist/src/server/team-autostaff.js +16 -0
  100. package/dist/src/server/team-list-serializer.d.ts +1 -1
  101. package/dist/src/server/team-list-serializer.js +3 -1
  102. package/dist/src/server/team-operations.d.ts +15 -1
  103. package/dist/src/server/team-operations.js +116 -11
  104. package/dist/src/server/terminal-protocol.js +9 -3
  105. package/dist/src/server/terminal-stream-hub.js +16 -10
  106. package/dist/src/server/terminal-ws-server.js +10 -8
  107. package/dist/src/server/websocket-upgrade-safety.d.ts +10 -0
  108. package/dist/src/server/websocket-upgrade-safety.js +35 -0
  109. package/dist/src/server/windows-command-line.d.ts +3 -0
  110. package/dist/src/server/windows-command-line.js +9 -0
  111. package/dist/src/server/windows-filename.d.ts +2 -0
  112. package/dist/src/server/windows-filename.js +33 -0
  113. package/dist/src/server/workflow-cli-policy.d.ts +60 -0
  114. package/dist/src/server/workflow-cli-policy.js +110 -0
  115. package/dist/src/server/workflow-dispatch-awaiter.d.ts +12 -0
  116. package/dist/src/server/workflow-dispatch-awaiter.js +80 -0
  117. package/dist/src/server/workflow-feature.d.ts +15 -0
  118. package/dist/src/server/workflow-feature.js +15 -0
  119. package/dist/src/server/workflow-http-serializers.d.ts +64 -0
  120. package/dist/src/server/workflow-http-serializers.js +58 -0
  121. package/dist/src/server/workflow-run-log-store.d.ts +19 -0
  122. package/dist/src/server/workflow-run-log-store.js +45 -0
  123. package/dist/src/server/workflow-run-store.d.ts +50 -0
  124. package/dist/src/server/workflow-run-store.js +103 -0
  125. package/dist/src/server/workflow-runner.d.ts +147 -0
  126. package/dist/src/server/workflow-runner.js +401 -0
  127. package/dist/src/server/workflow-schedule-create.d.ts +14 -0
  128. package/dist/src/server/workflow-schedule-create.js +41 -0
  129. package/dist/src/server/workflow-schedule-store.d.ts +43 -0
  130. package/dist/src/server/workflow-schedule-store.js +112 -0
  131. package/dist/src/server/workflow-scheduler.d.ts +36 -0
  132. package/dist/src/server/workflow-scheduler.js +97 -0
  133. package/dist/src/server/workflow-script-loader.d.ts +34 -0
  134. package/dist/src/server/workflow-script-loader.js +106 -0
  135. package/dist/src/server/workspace-path-validation.js +16 -4
  136. package/dist/src/server/workspace-shell-runtime.d.ts +5 -0
  137. package/dist/src/server/workspace-shell-runtime.js +24 -2
  138. package/dist/src/server/workspace-store-contract.d.ts +4 -1
  139. package/dist/src/server/workspace-store-hydration.js +23 -7
  140. package/dist/src/server/workspace-store-mutations.js +2 -5
  141. package/dist/src/server/workspace-store-support.d.ts +4 -0
  142. package/dist/src/server/workspace-store-support.js +13 -1
  143. package/dist/src/server/workspace-store.js +38 -4
  144. package/dist/src/shared/types.d.ts +16 -1
  145. package/package.json +4 -2
  146. package/web/dist/assets/{AddWorkerDialog-DeZhTQLi.js → AddWorkerDialog-CcC-7kgG.js} +2 -2
  147. package/web/dist/assets/AddWorkspaceDialog-BDpOTfmt.js +1 -0
  148. package/web/dist/assets/{FirstRunWizard-B5wLcat5.js → FirstRunWizard-BYX_ocQn.js} +1 -1
  149. package/web/dist/assets/{MarketplaceDrawer-BC0eBOEW.js → MarketplaceDrawer-DUxSk7db.js} +1 -1
  150. package/web/dist/assets/WhatsNewDialog-B_RlCXcV.js +1 -0
  151. package/web/dist/assets/WorkerModal-D9-7YfZZ.js +1 -0
  152. package/web/dist/assets/WorkspaceTaskDrawer-BCKoF7qc.js +1 -0
  153. package/web/dist/assets/{WorkspaceTerminalPanels-CvibsPSd.js → WorkspaceTerminalPanels-Dq8y91t2.js} +1 -1
  154. package/web/dist/assets/index-BiOvKIVw.css +1 -0
  155. package/web/dist/assets/index-DMRUklT3.js +73 -0
  156. package/web/dist/assets/path-join-7MR1s7b1.js +1 -0
  157. package/web/dist/index.html +2 -2
  158. package/web/dist/sw.js +1 -1
  159. package/web/dist/assets/AddWorkspaceDialog-DDpXNEKf.js +0 -1
  160. package/web/dist/assets/WorkerModal-BwMHq-Bi.js +0 -1
  161. package/web/dist/assets/WorkspaceTaskDrawer-CxvT4nqs.js +0 -1
  162. package/web/dist/assets/index-BEsTmfrO.css +0 -1
  163. package/web/dist/assets/index-Ddb7bDN5.js +0 -75
  164. package/web/dist/assets/path-join-S7qkXQtP.js +0 -1
@@ -0,0 +1,112 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ const parseArgs = (value) => {
3
+ if (!value)
4
+ return null;
5
+ try {
6
+ return JSON.parse(value);
7
+ }
8
+ catch {
9
+ return null;
10
+ }
11
+ };
12
+ const toRecord = (row) => ({
13
+ id: row.id,
14
+ workspaceId: row.workspace_id,
15
+ scriptPath: row.script_path,
16
+ cron: row.cron,
17
+ args: parseArgs(row.args),
18
+ enabled: row.enabled === 1,
19
+ lastRunAt: row.last_run_at,
20
+ nextRunAt: row.next_run_at,
21
+ createdAt: row.created_at,
22
+ updatedAt: row.updated_at,
23
+ });
24
+ export const createWorkflowScheduleStore = (db) => {
25
+ const create = (input) => {
26
+ const now = Date.now();
27
+ const record = {
28
+ id: randomUUID(),
29
+ workspaceId: input.workspaceId,
30
+ scriptPath: input.scriptPath,
31
+ cron: input.cron,
32
+ args: input.args ?? null,
33
+ enabled: input.enabled ?? true,
34
+ lastRunAt: null,
35
+ nextRunAt: input.nextRunAt,
36
+ createdAt: now,
37
+ updatedAt: now,
38
+ };
39
+ db.prepare(`INSERT INTO workflow_schedules (
40
+ id, workspace_id, script_path, cron, args, enabled,
41
+ last_run_at, next_run_at, created_at, updated_at
42
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(record.id, record.workspaceId, record.scriptPath, record.cron, record.args === null ? null : JSON.stringify(record.args), record.enabled ? 1 : 0, record.lastRunAt, record.nextRunAt, record.createdAt, record.updatedAt);
43
+ return record;
44
+ };
45
+ const update = (id, input) => {
46
+ const sets = [];
47
+ const values = [];
48
+ if (input.cron !== undefined) {
49
+ sets.push('cron = ?');
50
+ values.push(input.cron);
51
+ }
52
+ if (input.args !== undefined) {
53
+ sets.push('args = ?');
54
+ values.push(input.args === null ? null : JSON.stringify(input.args));
55
+ }
56
+ if (input.enabled !== undefined) {
57
+ sets.push('enabled = ?');
58
+ values.push(input.enabled ? 1 : 0);
59
+ }
60
+ if (input.lastRunAt !== undefined) {
61
+ sets.push('last_run_at = ?');
62
+ values.push(input.lastRunAt);
63
+ }
64
+ if (input.nextRunAt !== undefined) {
65
+ sets.push('next_run_at = ?');
66
+ values.push(input.nextRunAt);
67
+ }
68
+ if (sets.length === 0)
69
+ return;
70
+ sets.push('updated_at = ?');
71
+ values.push(Date.now());
72
+ values.push(id);
73
+ db.prepare(`UPDATE workflow_schedules SET ${sets.join(', ')} WHERE id = ?`).run(...values);
74
+ };
75
+ const get = (id) => {
76
+ const row = db.prepare('SELECT * FROM workflow_schedules WHERE id = ?').get(id);
77
+ return row ? toRecord(row) : undefined;
78
+ };
79
+ const listForWorkspace = (workspaceId) => db
80
+ .prepare('SELECT * FROM workflow_schedules WHERE workspace_id = ? ORDER BY created_at DESC, id DESC')
81
+ .all(workspaceId).map(toRecord);
82
+ // Enabled schedules whose next_run_at has arrived — the scheduler queries
83
+ // this every tick and fires each.
84
+ const listDueSchedules = (now) => db
85
+ .prepare('SELECT * FROM workflow_schedules WHERE enabled = 1 AND next_run_at <= ? ORDER BY next_run_at')
86
+ .all(now).map(toRecord);
87
+ const deleteSchedule = (id) => {
88
+ db.prepare('DELETE FROM workflow_schedules WHERE id = ?').run(id);
89
+ };
90
+ // TIER 1 #5 — compare-and-swap claim. If two scheduler ticks see the same
91
+ // due schedule (overlapping setInterval invocations, slow esbuild
92
+ // transpile in startWorkflow, or future multi-process), only the one
93
+ // whose UPDATE matches the still-original next_run_at wins. Returns
94
+ // true if the caller may proceed to fire the workflow.
95
+ const claimDueSchedule = (input) => {
96
+ const result = db
97
+ .prepare(`UPDATE workflow_schedules
98
+ SET next_run_at = ?, last_run_at = ?, updated_at = ?
99
+ WHERE id = ? AND next_run_at = ?`)
100
+ .run(input.newNextRunAt, input.lastRunAt, Date.now(), input.id, input.expectedNextRunAt);
101
+ return result.changes === 1;
102
+ };
103
+ return {
104
+ create,
105
+ update,
106
+ get,
107
+ listForWorkspace,
108
+ listDueSchedules,
109
+ deleteSchedule,
110
+ claimDueSchedule,
111
+ };
112
+ };
@@ -0,0 +1,36 @@
1
+ import type { WorkflowRunRecord } from './workflow-run-store.js';
2
+ import type { RunWorkflowInput } from './workflow-runner.js';
3
+ import type { createWorkflowScheduleStore } from './workflow-schedule-store.js';
4
+ type ScheduleStore = ReturnType<typeof createWorkflowScheduleStore>;
5
+ export interface WorkflowSchedulerDeps {
6
+ schedules: ScheduleStore;
7
+ startWorkflow: (input: RunWorkflowInput) => Promise<WorkflowRunRecord>;
8
+ /** Defensive guard against orphan schedules (TIER 1 #4). The workspace
9
+ * delete cascade clears workflow_schedules in the same transaction so
10
+ * fresh orphans cannot appear; this port catches any pre-existing or
11
+ * externally-introduced orphan and self-heals by deleting the schedule
12
+ * before it can fire and crash startWorkflow → re-fire next tick. */
13
+ workspaceExists?: (workspaceId: string) => boolean;
14
+ /** HivePort to pass into every fired workflow. Resolved lazily so the
15
+ * scheduler doesn't need to be reconstructed when the runtime listens
16
+ * on a different port (e.g. tests using port 0). */
17
+ getHivePort?: () => string;
18
+ /** Test seam: defaults to cron-parser. Returns next fire time in ms-epoch. */
19
+ computeNextRunAt?: (cron: string, after: Date) => number;
20
+ /** Experimental workflow gate. When provided and returns false, ticks fire
21
+ * nothing (scheduled runs are held, not dropped — they fire once re-enabled).
22
+ * Omitted → no gate (the scheduler unit tests rely on this default). */
23
+ isWorkflowEnabled?: () => boolean;
24
+ }
25
+ export interface WorkflowScheduler {
26
+ /** Run one tick using the given `now` (ms-epoch). For both production and tests. */
27
+ tick: (now?: number) => Promise<void>;
28
+ /** Arm a setInterval that calls tick() every `tickIntervalMs`. Returns void. */
29
+ start: (input?: {
30
+ tickIntervalMs?: number;
31
+ }) => void;
32
+ /** Clear the timer. Idempotent. */
33
+ close: () => void;
34
+ }
35
+ export declare const createWorkflowScheduler: (deps: WorkflowSchedulerDeps) => WorkflowScheduler;
36
+ export {};
@@ -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';