@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.
- package/CHANGELOG.md +44 -0
- package/README.en.md +5 -4
- package/README.md +9 -1
- package/assets/qq-group.jpg +0 -0
- package/dist/bin/team.cmd +1 -0
- package/dist/src/cli/hive-update.d.ts +57 -0
- package/dist/src/cli/hive-update.js +92 -15
- package/dist/src/cli/hive.d.ts +57 -0
- package/dist/src/cli/hive.js +113 -20
- package/dist/src/cli/team.d.ts +1 -0
- package/dist/src/cli/team.js +215 -7
- package/dist/src/server/agent-command-resolver.d.ts +10 -1
- package/dist/src/server/agent-command-resolver.js +32 -4
- package/dist/src/server/agent-launch-resolver.js +9 -3
- package/dist/src/server/agent-manager-support.d.ts +28 -0
- package/dist/src/server/agent-manager-support.js +138 -10
- package/dist/src/server/agent-run-bootstrap.d.ts +17 -1
- package/dist/src/server/agent-run-bootstrap.js +30 -2
- package/dist/src/server/agent-run-starter.d.ts +7 -1
- package/dist/src/server/agent-run-starter.js +9 -2
- package/dist/src/server/agent-run-store.d.ts +1 -1
- package/dist/src/server/agent-runtime-close.d.ts +1 -0
- package/dist/src/server/agent-runtime-close.js +25 -1
- package/dist/src/server/agent-runtime-contract.d.ts +2 -1
- package/dist/src/server/agent-runtime.d.ts +1 -1
- package/dist/src/server/agent-runtime.js +8 -2
- package/dist/src/server/agent-startup-instructions.d.ts +8 -1
- package/dist/src/server/agent-startup-instructions.js +15 -9
- package/dist/src/server/agent-stdin-dispatcher.d.ts +12 -5
- package/dist/src/server/agent-stdin-dispatcher.js +129 -40
- package/dist/src/server/app.d.ts +1 -0
- package/dist/src/server/app.js +12 -2
- package/dist/src/server/cron-util.d.ts +7 -0
- package/dist/src/server/cron-util.js +19 -0
- package/dist/src/server/dispatch-ledger-store.d.ts +22 -0
- package/dist/src/server/dispatch-ledger-store.js +51 -3
- package/dist/src/server/env-sync-message.js +9 -9
- package/dist/src/server/fs-browse.d.ts +14 -1
- package/dist/src/server/fs-browse.js +48 -5
- package/dist/src/server/fs-pick-folder.js +58 -11
- package/dist/src/server/fs-sandbox.js +36 -7
- package/dist/src/server/hive-team-guidance.d.ts +11 -6
- package/dist/src/server/hive-team-guidance.js +252 -70
- package/dist/src/server/live-run-registry.d.ts +1 -0
- package/dist/src/server/live-run-registry.js +1 -1
- package/dist/src/server/open-target-commands.js +29 -4
- package/dist/src/server/orchestrator-autostart.d.ts +12 -0
- package/dist/src/server/orchestrator-autostart.js +15 -13
- package/dist/src/server/path-canonicalization.d.ts +3 -0
- package/dist/src/server/path-canonicalization.js +29 -0
- package/dist/src/server/platform-path.d.ts +3 -0
- package/dist/src/server/platform-path.js +13 -0
- package/dist/src/server/post-start-input-writer.d.ts +1 -1
- package/dist/src/server/post-start-input-writer.js +116 -16
- package/dist/src/server/preset-launch-support.d.ts +1 -1
- package/dist/src/server/preset-launch-support.js +33 -2
- package/dist/src/server/recovery-summary.d.ts +6 -1
- package/dist/src/server/recovery-summary.js +17 -17
- package/dist/src/server/restart-policy-support.d.ts +6 -1
- package/dist/src/server/restart-policy-support.js +9 -1
- package/dist/src/server/restart-policy.d.ts +2 -2
- package/dist/src/server/restart-policy.js +3 -1
- package/dist/src/server/role-template-store.d.ts +1 -0
- package/dist/src/server/role-template-store.js +11 -1
- package/dist/src/server/route-types.d.ts +43 -0
- package/dist/src/server/routes-runtime.js +2 -1
- package/dist/src/server/routes-settings.js +76 -0
- package/dist/src/server/routes-team.js +221 -2
- package/dist/src/server/routes-workflow-schedules.d.ts +2 -0
- package/dist/src/server/routes-workflow-schedules.js +58 -0
- package/dist/src/server/routes-workflows.d.ts +2 -0
- package/dist/src/server/routes-workflows.js +83 -0
- package/dist/src/server/routes.js +4 -0
- package/dist/src/server/runtime-restart-policy.d.ts +3 -1
- package/dist/src/server/runtime-restart-policy.js +3 -1
- package/dist/src/server/runtime-store-contract.d.ts +122 -0
- package/dist/src/server/runtime-store-contract.js +1 -0
- package/dist/src/server/runtime-store-helpers.d.ts +9 -0
- package/dist/src/server/runtime-store-helpers.js +101 -2
- package/dist/src/server/runtime-store-workflows.d.ts +6 -0
- package/dist/src/server/runtime-store-workflows.js +100 -0
- package/dist/src/server/runtime-store.d.ts +3 -70
- package/dist/src/server/runtime-store.js +70 -4
- package/dist/src/server/session-capture-claude.d.ts +23 -0
- package/dist/src/server/session-capture-claude.js +24 -1
- package/dist/src/server/session-capture-codex.d.ts +3 -3
- package/dist/src/server/session-capture-codex.js +9 -7
- package/dist/src/server/session-capture-gemini.d.ts +1 -1
- package/dist/src/server/session-capture-gemini.js +6 -3
- package/dist/src/server/session-capture-opencode.d.ts +18 -0
- package/dist/src/server/session-capture-opencode.js +27 -2
- package/dist/src/server/settings-store.d.ts +3 -0
- package/dist/src/server/settings-store.js +1 -0
- package/dist/src/server/sqlite-schema-v19.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v19.js +17 -0
- package/dist/src/server/sqlite-schema-v20.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v20.js +20 -0
- package/dist/src/server/sqlite-schema-v21.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v21.js +20 -0
- package/dist/src/server/sqlite-schema.d.ts +1 -1
- package/dist/src/server/sqlite-schema.js +97 -1
- package/dist/src/server/startup-command-parser.d.ts +15 -0
- package/dist/src/server/startup-command-parser.js +33 -2
- package/dist/src/server/system-message.d.ts +7 -0
- package/dist/src/server/system-message.js +8 -1
- package/dist/src/server/tasks-file-watcher.d.ts +39 -1
- package/dist/src/server/tasks-file-watcher.js +155 -25
- package/dist/src/server/tasks-file.d.ts +2 -1
- package/dist/src/server/tasks-file.js +32 -9
- package/dist/src/server/tasks-websocket-server.js +13 -14
- package/dist/src/server/team-authz.d.ts +1 -1
- package/dist/src/server/team-authz.js +9 -1
- package/dist/src/server/team-autostaff.d.ts +16 -0
- package/dist/src/server/team-autostaff.js +16 -0
- package/dist/src/server/team-list-serializer.d.ts +1 -1
- package/dist/src/server/team-list-serializer.js +3 -1
- package/dist/src/server/team-operations.d.ts +20 -2
- package/dist/src/server/team-operations.js +160 -14
- package/dist/src/server/terminal-input-profile.js +2 -8
- package/dist/src/server/terminal-protocol.js +9 -3
- package/dist/src/server/terminal-stream-hub.js +16 -10
- package/dist/src/server/terminal-ws-server.js +36 -16
- package/dist/src/server/websocket-upgrade-safety.d.ts +10 -0
- package/dist/src/server/websocket-upgrade-safety.js +35 -0
- package/dist/src/server/windows-command-line.d.ts +3 -0
- package/dist/src/server/windows-command-line.js +9 -0
- package/dist/src/server/windows-filename.d.ts +2 -0
- package/dist/src/server/windows-filename.js +33 -0
- package/dist/src/server/workflow-cli-policy.d.ts +60 -0
- package/dist/src/server/workflow-cli-policy.js +110 -0
- package/dist/src/server/workflow-dispatch-awaiter.d.ts +12 -0
- package/dist/src/server/workflow-dispatch-awaiter.js +80 -0
- package/dist/src/server/workflow-feature.d.ts +15 -0
- package/dist/src/server/workflow-feature.js +15 -0
- package/dist/src/server/workflow-http-serializers.d.ts +64 -0
- package/dist/src/server/workflow-http-serializers.js +58 -0
- package/dist/src/server/workflow-run-log-store.d.ts +19 -0
- package/dist/src/server/workflow-run-log-store.js +45 -0
- package/dist/src/server/workflow-run-store.d.ts +50 -0
- package/dist/src/server/workflow-run-store.js +103 -0
- package/dist/src/server/workflow-runner.d.ts +147 -0
- package/dist/src/server/workflow-runner.js +401 -0
- package/dist/src/server/workflow-schedule-create.d.ts +14 -0
- package/dist/src/server/workflow-schedule-create.js +41 -0
- package/dist/src/server/workflow-schedule-store.d.ts +43 -0
- package/dist/src/server/workflow-schedule-store.js +112 -0
- package/dist/src/server/workflow-scheduler.d.ts +36 -0
- package/dist/src/server/workflow-scheduler.js +97 -0
- package/dist/src/server/workflow-script-loader.d.ts +34 -0
- package/dist/src/server/workflow-script-loader.js +106 -0
- package/dist/src/server/workspace-path-validation.js +16 -4
- package/dist/src/server/workspace-shell-runtime.d.ts +5 -0
- package/dist/src/server/workspace-shell-runtime.js +24 -2
- package/dist/src/server/workspace-store-contract.d.ts +4 -1
- package/dist/src/server/workspace-store-hydration.js +23 -7
- package/dist/src/server/workspace-store-mutations.js +2 -5
- package/dist/src/server/workspace-store-support.d.ts +4 -0
- package/dist/src/server/workspace-store-support.js +13 -1
- package/dist/src/server/workspace-store.js +38 -4
- package/dist/src/shared/types.d.ts +16 -1
- package/package.json +4 -2
- package/web/dist/assets/{AddWorkerDialog-DmkDOdp6.js → AddWorkerDialog-CcC-7kgG.js} +2 -2
- package/web/dist/assets/AddWorkspaceDialog-BDpOTfmt.js +1 -0
- package/web/dist/assets/{FirstRunWizard-SAd1wsH4.js → FirstRunWizard-BYX_ocQn.js} +1 -1
- package/web/dist/assets/{MarketplaceDrawer-B_8aG2uT.js → MarketplaceDrawer-DUxSk7db.js} +1 -1
- package/web/dist/assets/WhatsNewDialog-B_RlCXcV.js +1 -0
- package/web/dist/assets/WorkerModal-D9-7YfZZ.js +1 -0
- package/web/dist/assets/WorkspaceTaskDrawer-BCKoF7qc.js +1 -0
- package/web/dist/assets/{WorkspaceTerminalPanels-BReWh1YL.js → WorkspaceTerminalPanels-Dq8y91t2.js} +1 -1
- package/web/dist/assets/index-BiOvKIVw.css +1 -0
- package/web/dist/assets/index-DMRUklT3.js +73 -0
- package/web/dist/assets/path-join-7MR1s7b1.js +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/sw.js +1 -1
- package/web/dist/assets/AddWorkspaceDialog-BsVnH3Xe.js +0 -1
- package/web/dist/assets/WorkerModal-CQmjiPme.js +0 -1
- package/web/dist/assets/WorkspaceTaskDrawer-B0DmCWcV.js +0 -1
- package/web/dist/assets/chevron-right-CtLjVEl7.js +0 -1
- package/web/dist/assets/index-BEsTmfrO.css +0 -1
- 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 {
|
|
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 =
|
|
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 =
|
|
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:
|
|
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(
|
|
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
|
-
|
|
28
|
-
|
|
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, {
|
|
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.
|
|
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": "
|
|
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",
|