@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
package/dist/src/cli/team.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { realpathSync } from 'node:fs';
|
|
2
1
|
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { sameFilesystemPath } from '../server/path-canonicalization.js';
|
|
3
3
|
const REQUIRED_ENV_KEYS = [
|
|
4
4
|
'HIVE_PORT',
|
|
5
5
|
'HIVE_PROJECT_ID',
|
|
@@ -10,6 +10,13 @@ const TEAM_USAGE = [
|
|
|
10
10
|
'Usage:',
|
|
11
11
|
' team list',
|
|
12
12
|
' team send <worker-name> "<task>"',
|
|
13
|
+
' team spawn <role> [--name <name>] [--cli <claude|codex|opencode|gemini>] [--ephemeral]',
|
|
14
|
+
' team dismiss <worker-name>',
|
|
15
|
+
" team workflow run --stdin [--args '<JSON>'] (script from stdin — for multi-line scripts)",
|
|
16
|
+
' team workflow run --inline "<source>" [--args \'<JSON>\']',
|
|
17
|
+
' team workflow stop <run-id>',
|
|
18
|
+
' team workflow show <run-id> (full per-agent transcript for one run)',
|
|
19
|
+
' team workflow schedule --cron "<cron>" --name <n> --stdin (register a recurring run)',
|
|
13
20
|
' team cancel --dispatch <dispatch-id> "<reason>"',
|
|
14
21
|
' team report "<result>" [--dispatch <dispatch-id>] [--artifact <path>]',
|
|
15
22
|
' team report --stdin [--dispatch <dispatch-id>] [--artifact <path>]',
|
|
@@ -17,10 +24,13 @@ const TEAM_USAGE = [
|
|
|
17
24
|
' team status --stdin [--artifact <path>]',
|
|
18
25
|
'',
|
|
19
26
|
'Flags can appear in any order. Use --stdin to pipe long bodies and avoid shell-escaping issues.',
|
|
20
|
-
|
|
21
|
-
" team report --stdin --dispatch <id> <<'EOF'",
|
|
22
|
-
'
|
|
23
|
-
'
|
|
27
|
+
'The body comes from stdin — use whatever your shell supports:',
|
|
28
|
+
" POSIX: team report --stdin --dispatch <id> <<'EOF'",
|
|
29
|
+
' ... long report ...',
|
|
30
|
+
' EOF',
|
|
31
|
+
' Windows cmd: type body.txt | team report --stdin --dispatch <id>',
|
|
32
|
+
' PowerShell: Get-Content body.txt | team report --stdin --dispatch <id>',
|
|
33
|
+
' Portable: team report --stdin --dispatch <id> < body.txt',
|
|
24
34
|
'',
|
|
25
35
|
'For role rules, workflow, and recovery instructions, see .hive/PROTOCOL.md',
|
|
26
36
|
].join('\n');
|
|
@@ -32,6 +42,15 @@ const getHiveEnv = () => {
|
|
|
32
42
|
return values;
|
|
33
43
|
};
|
|
34
44
|
const getBaseUrl = (env) => `http://127.0.0.1:${env.HIVE_PORT}`;
|
|
45
|
+
// Read `--flag value` from an argv slice; returns undefined when absent or
|
|
46
|
+
// when the flag is the last token with no following value.
|
|
47
|
+
const readFlag = (args, flag) => {
|
|
48
|
+
const index = args.indexOf(flag);
|
|
49
|
+
if (index === -1)
|
|
50
|
+
return undefined;
|
|
51
|
+
const value = args[index + 1];
|
|
52
|
+
return value && !value.startsWith('--') ? value : undefined;
|
|
53
|
+
};
|
|
35
54
|
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
36
55
|
const describeFetchError = (baseUrl, error) => {
|
|
37
56
|
const cause = error instanceof Error && error.cause instanceof Error ? ` (${error.cause.message})` : '';
|
|
@@ -174,6 +193,20 @@ export const parseCancelArgs = (args) => {
|
|
|
174
193
|
}
|
|
175
194
|
return { dispatchId, reason };
|
|
176
195
|
};
|
|
196
|
+
export const decodeStdinBuffer = (buffer) => {
|
|
197
|
+
if (buffer.length >= 3 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) {
|
|
198
|
+
return buffer.subarray(3).toString('utf8');
|
|
199
|
+
}
|
|
200
|
+
if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) {
|
|
201
|
+
return buffer.subarray(2).toString('utf16le');
|
|
202
|
+
}
|
|
203
|
+
if (buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff) {
|
|
204
|
+
const swapped = Buffer.from(buffer.subarray(2));
|
|
205
|
+
swapped.swap16();
|
|
206
|
+
return swapped.toString('utf16le');
|
|
207
|
+
}
|
|
208
|
+
return buffer.toString('utf8');
|
|
209
|
+
};
|
|
177
210
|
export const readStdinToString = async (command = 'report') => {
|
|
178
211
|
if (process.stdin.isTTY) {
|
|
179
212
|
throw new Error(withUsage('--stdin requires piped input, but stdin is a TTY. Did you forget to pipe content in?', command));
|
|
@@ -182,7 +215,7 @@ export const readStdinToString = async (command = 'report') => {
|
|
|
182
215
|
for await (const chunk of process.stdin) {
|
|
183
216
|
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
|
184
217
|
}
|
|
185
|
-
const content = Buffer.concat(chunks)
|
|
218
|
+
const content = decodeStdinBuffer(Buffer.concat(chunks));
|
|
186
219
|
if (!content.trim()) {
|
|
187
220
|
throw new Error(withUsage('--stdin received empty input', command));
|
|
188
221
|
}
|
|
@@ -226,9 +259,184 @@ export const runTeamCommand = async (argv) => {
|
|
|
226
259
|
to: workerName,
|
|
227
260
|
text: task,
|
|
228
261
|
});
|
|
262
|
+
const payload = (await response.json());
|
|
263
|
+
/* When the dispatch happened to also auto-wake a stopped worker
|
|
264
|
+
(PTY had no active run), make the silent restart visible. Stderr
|
|
265
|
+
is the right channel because the JSON on stdout is the
|
|
266
|
+
machine-readable payload; the human-readable narration goes
|
|
267
|
+
beside it so it doesn't corrupt parsers. */
|
|
268
|
+
if (payload.restarted_worker === true) {
|
|
269
|
+
console.error(`Hive woke up worker "${workerName}" before dispatching.`);
|
|
270
|
+
}
|
|
271
|
+
console.log(JSON.stringify(payload));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (command === 'spawn') {
|
|
275
|
+
const role = args[0];
|
|
276
|
+
if (!role || role.startsWith('--')) {
|
|
277
|
+
throw new Error('Usage: team spawn <role> [--name <name>] [--cli <claude|codex|opencode|gemini>] [--ephemeral]\n' +
|
|
278
|
+
' Default: persistent member (lives until you `team dismiss` it).\n' +
|
|
279
|
+
' --ephemeral: auto-dismiss after the next dispatch report (one-shot worker).');
|
|
280
|
+
}
|
|
281
|
+
const name = readFlag(args, '--name');
|
|
282
|
+
const cli = readFlag(args, '--cli');
|
|
283
|
+
const ephemeral = args.includes('--ephemeral');
|
|
284
|
+
const env = getHiveEnv();
|
|
285
|
+
const response = await postJson(getBaseUrl(env), '/api/team/spawn', {
|
|
286
|
+
project_id: env.HIVE_PROJECT_ID,
|
|
287
|
+
from_agent_id: env.HIVE_AGENT_ID,
|
|
288
|
+
token: env.HIVE_AGENT_TOKEN,
|
|
289
|
+
role,
|
|
290
|
+
...(name ? { name } : {}),
|
|
291
|
+
...(cli ? { cli } : {}),
|
|
292
|
+
...(ephemeral ? { ephemeral: true } : {}),
|
|
293
|
+
});
|
|
229
294
|
console.log(JSON.stringify(await response.json()));
|
|
230
295
|
return;
|
|
231
296
|
}
|
|
297
|
+
if (command === 'dismiss') {
|
|
298
|
+
const workerName = args[0];
|
|
299
|
+
if (!workerName || workerName.startsWith('--')) {
|
|
300
|
+
throw new Error('Usage: team dismiss <worker-name>');
|
|
301
|
+
}
|
|
302
|
+
const env = getHiveEnv();
|
|
303
|
+
const response = await postJson(getBaseUrl(env), '/api/team/dismiss', {
|
|
304
|
+
project_id: env.HIVE_PROJECT_ID,
|
|
305
|
+
from_agent_id: env.HIVE_AGENT_ID,
|
|
306
|
+
token: env.HIVE_AGENT_TOKEN,
|
|
307
|
+
name: workerName,
|
|
308
|
+
});
|
|
309
|
+
console.log(JSON.stringify(await response.json()));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (command === 'workflow') {
|
|
313
|
+
const sub = args[0];
|
|
314
|
+
const rest = args.slice(1);
|
|
315
|
+
if (sub === 'run') {
|
|
316
|
+
const inlineFlag = rest.indexOf('--inline');
|
|
317
|
+
const stdinFlag = rest.includes('--stdin');
|
|
318
|
+
const name = readFlag(rest, '--name');
|
|
319
|
+
// TIER 2 #8 — `--args '<JSON>'` makes the script's `args` global a
|
|
320
|
+
// real value instead of always undefined. Parses lazily so a bad
|
|
321
|
+
// JSON gives a clear local error before the HTTP round-trip.
|
|
322
|
+
const rawArgs = readFlag(rest, '--args');
|
|
323
|
+
let parsedArgs;
|
|
324
|
+
if (rawArgs !== undefined) {
|
|
325
|
+
try {
|
|
326
|
+
parsedArgs = JSON.parse(rawArgs);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
throw new Error(`Usage: team workflow run … --args '<JSON>'\n --args must be valid JSON; got: ${error instanceof Error ? error.message : String(error)}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
let source;
|
|
333
|
+
if (inlineFlag !== -1) {
|
|
334
|
+
const literal = rest[inlineFlag + 1];
|
|
335
|
+
if (!literal)
|
|
336
|
+
throw new Error('Usage: team workflow run --inline "<script-source>"');
|
|
337
|
+
source = literal;
|
|
338
|
+
}
|
|
339
|
+
else if (stdinFlag) {
|
|
340
|
+
source = await readStdinToString('workflow run');
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
throw new Error('Usage: team workflow run --stdin | team workflow run --inline "<script-source>"\n' +
|
|
344
|
+
' Pass workflow source via stdin (POSIX heredoc / `type x.ts |`) or as one inline arg.\n' +
|
|
345
|
+
" Optional: --args '<JSON>' makes the script's `args` global a real value.");
|
|
346
|
+
}
|
|
347
|
+
const env = getHiveEnv();
|
|
348
|
+
const response = await postJson(getBaseUrl(env), '/api/team/workflow/run', {
|
|
349
|
+
project_id: env.HIVE_PROJECT_ID,
|
|
350
|
+
from_agent_id: env.HIVE_AGENT_ID,
|
|
351
|
+
token: env.HIVE_AGENT_TOKEN,
|
|
352
|
+
source,
|
|
353
|
+
...(name ? { name } : {}),
|
|
354
|
+
...(parsedArgs !== undefined ? { args: parsedArgs } : {}),
|
|
355
|
+
});
|
|
356
|
+
console.log(JSON.stringify(await response.json()));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (sub === 'stop') {
|
|
360
|
+
const runId = rest[0];
|
|
361
|
+
if (!runId)
|
|
362
|
+
throw new Error('Usage: team workflow stop <run-id>');
|
|
363
|
+
const env = getHiveEnv();
|
|
364
|
+
const response = await postJson(getBaseUrl(env), '/api/team/workflow/stop', {
|
|
365
|
+
project_id: env.HIVE_PROJECT_ID,
|
|
366
|
+
from_agent_id: env.HIVE_AGENT_ID,
|
|
367
|
+
token: env.HIVE_AGENT_TOKEN,
|
|
368
|
+
run_id: runId,
|
|
369
|
+
});
|
|
370
|
+
console.log(JSON.stringify(await response.json()));
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (sub === 'show') {
|
|
374
|
+
const runId = rest[0];
|
|
375
|
+
if (!runId)
|
|
376
|
+
throw new Error('Usage: team workflow show <run-id>');
|
|
377
|
+
const env = getHiveEnv();
|
|
378
|
+
const response = await postJson(getBaseUrl(env), '/api/team/workflow/show', {
|
|
379
|
+
project_id: env.HIVE_PROJECT_ID,
|
|
380
|
+
from_agent_id: env.HIVE_AGENT_ID,
|
|
381
|
+
token: env.HIVE_AGENT_TOKEN,
|
|
382
|
+
run_id: runId,
|
|
383
|
+
});
|
|
384
|
+
console.log(JSON.stringify(await response.json()));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (sub === 'schedule') {
|
|
388
|
+
const usage = 'Usage: team workflow schedule --cron "<5-field cron>" --name <name> --stdin\n' +
|
|
389
|
+
' team workflow schedule --cron "<cron>" --name <name> --inline "<source>" [--args \'<JSON>\']\n' +
|
|
390
|
+
' Registers a recurring run. Source is persisted so cron can fire it with no orchestrator present.';
|
|
391
|
+
const inlineFlag = rest.indexOf('--inline');
|
|
392
|
+
const stdinFlag = rest.includes('--stdin');
|
|
393
|
+
const cron = readFlag(rest, '--cron');
|
|
394
|
+
const name = readFlag(rest, '--name');
|
|
395
|
+
if (!cron || !name)
|
|
396
|
+
throw new Error(usage);
|
|
397
|
+
const rawArgs = readFlag(rest, '--args');
|
|
398
|
+
let parsedArgs;
|
|
399
|
+
if (rawArgs !== undefined) {
|
|
400
|
+
try {
|
|
401
|
+
parsedArgs = JSON.parse(rawArgs);
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
throw new Error(`Usage: team workflow schedule … --args '<JSON>'\n --args must be valid JSON; got: ${error instanceof Error ? error.message : String(error)}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
let source;
|
|
408
|
+
if (inlineFlag !== -1) {
|
|
409
|
+
const literal = rest[inlineFlag + 1];
|
|
410
|
+
if (!literal)
|
|
411
|
+
throw new Error(usage);
|
|
412
|
+
source = literal;
|
|
413
|
+
}
|
|
414
|
+
else if (stdinFlag) {
|
|
415
|
+
source = await readStdinToString('workflow schedule');
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
throw new Error(usage);
|
|
419
|
+
}
|
|
420
|
+
const env = getHiveEnv();
|
|
421
|
+
const response = await postJson(getBaseUrl(env), '/api/team/workflow/schedule', {
|
|
422
|
+
project_id: env.HIVE_PROJECT_ID,
|
|
423
|
+
from_agent_id: env.HIVE_AGENT_ID,
|
|
424
|
+
token: env.HIVE_AGENT_TOKEN,
|
|
425
|
+
source,
|
|
426
|
+
name,
|
|
427
|
+
cron,
|
|
428
|
+
...(parsedArgs !== undefined ? { args: parsedArgs } : {}),
|
|
429
|
+
});
|
|
430
|
+
console.log(JSON.stringify(await response.json()));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
throw new Error('Usage:\n' +
|
|
434
|
+
" team workflow run --stdin [--args '<JSON>'] (read script from stdin)\n" +
|
|
435
|
+
' team workflow run --inline "<source>" [--args ...] (one-arg form)\n' +
|
|
436
|
+
' team workflow stop <run-id> (cancel a running workflow)\n' +
|
|
437
|
+
' team workflow show <run-id> (full per-agent transcript)\n' +
|
|
438
|
+
' team workflow schedule --cron "<cron>" --name <n> --stdin (register a recurring run)');
|
|
439
|
+
}
|
|
232
440
|
if (command === 'cancel') {
|
|
233
441
|
const cancel = parseCancelArgs(args);
|
|
234
442
|
const env = getHiveEnv();
|
|
@@ -282,7 +490,7 @@ export const runTeamCommand = async (argv) => {
|
|
|
282
490
|
throw new Error('Unsupported team command');
|
|
283
491
|
};
|
|
284
492
|
const isMainModule = process.argv[1]
|
|
285
|
-
? fileURLToPath(import.meta.url)
|
|
493
|
+
? sameFilesystemPath(fileURLToPath(import.meta.url), process.argv[1])
|
|
286
494
|
: false;
|
|
287
495
|
if (isMainModule) {
|
|
288
496
|
void runTeamCommand(process.argv.slice(2)).catch((error) => {
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
interface ResolvedSpawnCommand {
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* `args` is a `string[]` for plain executables (node-pty's serializer is
|
|
4
|
+
* fine for them) and a verbatim `string` for Windows `.cmd`/`.bat` shim
|
|
5
|
+
* launches. The verbatim form bypasses node-pty's `argsToCommandLine`,
|
|
6
|
+
* because that function backslash-escapes any embedded `"` and cmd.exe
|
|
7
|
+
* does NOT recognize `\"` as an escape — it treats `\` as literal, which
|
|
8
|
+
* leaves cmd looking up a program name containing literal quote chars.
|
|
9
|
+
* See `node-pty/src/windowsPtyAgent.ts` `argsToCommandLine` for the rule.
|
|
10
|
+
*/
|
|
11
|
+
args: string | string[];
|
|
3
12
|
command: string;
|
|
4
13
|
}
|
|
5
14
|
export declare const resolveCommandPath: (command: string, cwd: string, env: NodeJS.ProcessEnv, platform?: NodeJS.Platform) => string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { accessSync, constants } from 'node:fs';
|
|
2
|
-
import { delimiter, extname, isAbsolute, join } from 'node:path';
|
|
2
|
+
import { basename, delimiter, extname, isAbsolute, join } from 'node:path';
|
|
3
|
+
import { buildCmdCallCommand } from './windows-command-line.js';
|
|
3
4
|
const hasPathSeparator = (command) => command.includes('/') || command.includes('\\');
|
|
4
5
|
const canExecute = (path, platform = process.platform) => {
|
|
5
6
|
try {
|
|
@@ -54,16 +55,43 @@ const isWindowsBatchFile = (command) => {
|
|
|
54
55
|
const extension = extname(command).toLowerCase();
|
|
55
56
|
return extension === '.cmd' || extension === '.bat';
|
|
56
57
|
};
|
|
57
|
-
const
|
|
58
|
-
|
|
58
|
+
const buildWindowsBatchCommandLine = (command, args) => {
|
|
59
|
+
// `call` is cmd's built-in batch invocation; it handles quoted .cmd / .bat
|
|
60
|
+
// paths reliably (this is the same pattern Node.js's child_process uses
|
|
61
|
+
// internally on Windows since the CVE-2024-27980 fix).
|
|
62
|
+
return `/d /s /c ${buildCmdCallCommand(command, args)}`;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Recognize the exact shape that `createStartupCommandLaunch` produces on
|
|
66
|
+
* Windows: `cmd.exe` with args `['/d', '/s', '/c', '<raw user command>']`.
|
|
67
|
+
* Pinned to length 4 so this branch only fires for that single contract;
|
|
68
|
+
* any other cmd.exe invocation (e.g. someone explicitly composing custom
|
|
69
|
+
* shell args via the launch config) keeps the default node-pty path.
|
|
70
|
+
*
|
|
71
|
+
* We need this repackaging because the user's raw command often contains `"`
|
|
72
|
+
* (Windows users habitually wrap paths) and node-pty's `argsToCommandLine`
|
|
73
|
+
* backslash-escapes those — cmd.exe then sees `\"...\"` and looks up a
|
|
74
|
+
* program whose name starts with `\`.
|
|
75
|
+
*/
|
|
76
|
+
const isCmdExeShellLaunch = (resolvedCommand, args) => basename(resolvedCommand).toLowerCase() === 'cmd.exe' &&
|
|
77
|
+
args.length === 4 &&
|
|
78
|
+
args[0] === '/d' &&
|
|
79
|
+
args[1] === '/s' &&
|
|
80
|
+
(args[2] === '/c' || args[2] === '/k');
|
|
59
81
|
export const resolveSpawnCommand = (command, cwd, env, args = [], platform = process.platform) => {
|
|
60
82
|
const resolvedCommand = resolveCommandPath(command, cwd, env, platform);
|
|
61
83
|
if (platform === 'win32' && isWindowsBatchFile(resolvedCommand)) {
|
|
62
84
|
return {
|
|
63
|
-
args:
|
|
85
|
+
args: buildWindowsBatchCommandLine(resolvedCommand, args),
|
|
64
86
|
command: getEnvValue(env, 'ComSpec', platform) ?? 'cmd.exe',
|
|
65
87
|
};
|
|
66
88
|
}
|
|
89
|
+
if (platform === 'win32' && isCmdExeShellLaunch(resolvedCommand, args)) {
|
|
90
|
+
return {
|
|
91
|
+
args: args.join(' '),
|
|
92
|
+
command: resolvedCommand,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
67
95
|
return { args, command: resolvedCommand };
|
|
68
96
|
};
|
|
69
97
|
export const assertCommandIsExecutable = (command, cwd, env) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createStartupCommandLaunch, getStartupCommandExecutable, } from './startup-command-parser.js';
|
|
1
|
+
import { createStartupCommandLaunch, getStartupCommandExecutable, normalizeExecutableToken, } from './startup-command-parser.js';
|
|
2
2
|
export const resolveCommandPresetLaunchConfig = (settings, commandPresetId) => {
|
|
3
3
|
const preset = settings.getCommandPreset(commandPresetId);
|
|
4
4
|
if (!preset)
|
|
@@ -12,8 +12,14 @@ export const resolveCommandPresetLaunchConfig = (settings, commandPresetId) => {
|
|
|
12
12
|
const findPresetForStartupCommand = (settings, startupCommand, commandPresetId) => {
|
|
13
13
|
if (commandPresetId)
|
|
14
14
|
return settings.getCommandPreset(commandPresetId);
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// Reduce the raw token (which may be a bare command, an absolute path,
|
|
16
|
+
// or a Windows path with spaces and a .cmd suffix) to the canonical
|
|
17
|
+
// brand id before looking up the preset. Without this normalization
|
|
18
|
+
// step `getCommandPreset` only matched bare command names — Windows
|
|
19
|
+
// users typing the full nvm4w path lost CLI brand identification,
|
|
20
|
+
// session capture, and post-start input strategy in one swoop.
|
|
21
|
+
const brandId = normalizeExecutableToken(getStartupCommandExecutable(startupCommand));
|
|
22
|
+
return brandId ? settings.getCommandPreset(brandId) : undefined;
|
|
17
23
|
};
|
|
18
24
|
export const resolveStartupCommandLaunchConfig = (settings, startupCommand, commandPresetId = null) => {
|
|
19
25
|
const trimmedStartupCommand = startupCommand.trim();
|
|
@@ -2,6 +2,34 @@ import type { IPty } from 'node-pty';
|
|
|
2
2
|
import type { AgentRunRecord, AgentRunSnapshot } from './agent-manager.js';
|
|
3
3
|
import type { PtyOutputBus } from './pty-output-bus.js';
|
|
4
4
|
export declare const MAX_RUN_OUTPUT_LENGTH = 1000000;
|
|
5
|
+
type ExecRunner = (cmd: string, args: readonly string[], done: (success: boolean) => void) => void;
|
|
6
|
+
/**
|
|
7
|
+
* Windows analogue of POSIX `process.kill(-pgid, SIGKILL)`. node-pty on
|
|
8
|
+
* Windows hands `pty.kill()` to TerminateProcess against the PTY's main
|
|
9
|
+
* process only — children that the worker spawned (npm install, build
|
|
10
|
+
* scripts, custom tooling) become orphans and keep writing to the
|
|
11
|
+
* filesystem after the worker card flips to stopped.
|
|
12
|
+
*
|
|
13
|
+
* `taskkill /pid <pid> /t /f` walks the process tree (`/t`) and forces
|
|
14
|
+
* termination (`/f`), matching what task manager would do.
|
|
15
|
+
*
|
|
16
|
+
* IMPORTANT — call this BEFORE any other termination of the parent
|
|
17
|
+
* process. taskkill /T builds the tree by querying the parent for its
|
|
18
|
+
* descendants; if the parent is already gone (e.g. pty.kill() ran
|
|
19
|
+
* first) the enumeration returns empty and the children become
|
|
20
|
+
* orphans. /F also terminates the parent itself, so a parent-kill
|
|
21
|
+
* after this call is only useful as a fallback when taskkill itself
|
|
22
|
+
* failed (taskkill missing from PATH, restricted PowerShell, etc.).
|
|
23
|
+
*
|
|
24
|
+
* Best-effort: non-zero exits (process already gone, taskkill missing
|
|
25
|
+
* from PATH, access denied) are swallowed and surface as a `false`
|
|
26
|
+
* return. The caller is expected to fall back to pty.kill().
|
|
27
|
+
*
|
|
28
|
+
* Exported for unit testing — the `runner` parameter lets tests assert
|
|
29
|
+
* the exact argv without mocking node:child_process.
|
|
30
|
+
*/
|
|
31
|
+
export declare const taskkillProcessTree: (pid: number, platform?: NodeJS.Platform, runner?: ExecRunner, onFailure?: () => void) => boolean;
|
|
5
32
|
export declare const toAgentRunSnapshot: (run: AgentRunRecord) => AgentRunSnapshot;
|
|
6
33
|
export declare const finishAgentRun: (run: AgentRunRecord, exitCode: number | null, ptyOutputBus: PtyOutputBus) => void;
|
|
7
34
|
export declare const attachAgentPty: (run: AgentRunRecord, pty: IPty, ptyOutputBus: PtyOutputBus) => void;
|
|
35
|
+
export {};
|
|
@@ -1,6 +1,63 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
1
|
+
import { execFile, execFileSync } from 'node:child_process';
|
|
2
2
|
export const MAX_RUN_OUTPUT_LENGTH = 1_000_000;
|
|
3
3
|
const FORCE_KILL_DELAY_MS = 750;
|
|
4
|
+
const TASKKILL_TIMEOUT_MS = 3000;
|
|
5
|
+
const PTY_READ_EOF_EXIT_GRACE_MS = 1000;
|
|
6
|
+
const isPtyReadEofError = (error) => {
|
|
7
|
+
const candidate = error;
|
|
8
|
+
return process.platform !== 'win32' && candidate?.code === 'EIO' && candidate.syscall === 'read';
|
|
9
|
+
};
|
|
10
|
+
const serializePtyInput = (input) => Buffer.isBuffer(input) ? input.toString('latin1') : input;
|
|
11
|
+
const defaultExecRunner = (cmd, args, done) => {
|
|
12
|
+
let settled = false;
|
|
13
|
+
const settle = (success) => {
|
|
14
|
+
if (settled)
|
|
15
|
+
return;
|
|
16
|
+
settled = true;
|
|
17
|
+
done(success);
|
|
18
|
+
};
|
|
19
|
+
const child = execFile(cmd, [...args], { maxBuffer: 64 * 1024, timeout: TASKKILL_TIMEOUT_MS, windowsHide: true }, (error) => settle(!error));
|
|
20
|
+
child.once('error', () => settle(false));
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Windows analogue of POSIX `process.kill(-pgid, SIGKILL)`. node-pty on
|
|
24
|
+
* Windows hands `pty.kill()` to TerminateProcess against the PTY's main
|
|
25
|
+
* process only — children that the worker spawned (npm install, build
|
|
26
|
+
* scripts, custom tooling) become orphans and keep writing to the
|
|
27
|
+
* filesystem after the worker card flips to stopped.
|
|
28
|
+
*
|
|
29
|
+
* `taskkill /pid <pid> /t /f` walks the process tree (`/t`) and forces
|
|
30
|
+
* termination (`/f`), matching what task manager would do.
|
|
31
|
+
*
|
|
32
|
+
* IMPORTANT — call this BEFORE any other termination of the parent
|
|
33
|
+
* process. taskkill /T builds the tree by querying the parent for its
|
|
34
|
+
* descendants; if the parent is already gone (e.g. pty.kill() ran
|
|
35
|
+
* first) the enumeration returns empty and the children become
|
|
36
|
+
* orphans. /F also terminates the parent itself, so a parent-kill
|
|
37
|
+
* after this call is only useful as a fallback when taskkill itself
|
|
38
|
+
* failed (taskkill missing from PATH, restricted PowerShell, etc.).
|
|
39
|
+
*
|
|
40
|
+
* Best-effort: non-zero exits (process already gone, taskkill missing
|
|
41
|
+
* from PATH, access denied) are swallowed and surface as a `false`
|
|
42
|
+
* return. The caller is expected to fall back to pty.kill().
|
|
43
|
+
*
|
|
44
|
+
* Exported for unit testing — the `runner` parameter lets tests assert
|
|
45
|
+
* the exact argv without mocking node:child_process.
|
|
46
|
+
*/
|
|
47
|
+
export const taskkillProcessTree = (pid, platform = process.platform, runner = defaultExecRunner, onFailure) => {
|
|
48
|
+
if (platform !== 'win32' || pid <= 0)
|
|
49
|
+
return false;
|
|
50
|
+
try {
|
|
51
|
+
runner('taskkill', ['/pid', String(pid), '/t', '/f'], (success) => {
|
|
52
|
+
if (!success)
|
|
53
|
+
onFailure?.();
|
|
54
|
+
});
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
4
61
|
export const toAgentRunSnapshot = (run) => ({
|
|
5
62
|
runId: run.runId,
|
|
6
63
|
agentId: run.agentId,
|
|
@@ -22,6 +79,7 @@ export const finishAgentRun = (run, exitCode, ptyOutputBus) => {
|
|
|
22
79
|
export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
23
80
|
let stdinClosed = false;
|
|
24
81
|
let forceKillTimer;
|
|
82
|
+
let ptyReadEofTimer;
|
|
25
83
|
const resolveProcessGroupId = () => {
|
|
26
84
|
if (process.platform === 'win32' || pty.pid <= 0)
|
|
27
85
|
return null;
|
|
@@ -60,16 +118,29 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
|
60
118
|
ignoreBestEffortGroupKillError(error);
|
|
61
119
|
}
|
|
62
120
|
};
|
|
63
|
-
const
|
|
121
|
+
const killPtyDirect = (signal) => {
|
|
64
122
|
try {
|
|
65
|
-
|
|
66
|
-
pty.kill();
|
|
67
|
-
else
|
|
68
|
-
pty.kill(signal);
|
|
123
|
+
pty.kill(signal);
|
|
69
124
|
}
|
|
70
125
|
catch (error) {
|
|
71
126
|
ignoreMissingProcess(error);
|
|
72
127
|
}
|
|
128
|
+
};
|
|
129
|
+
const killPty = (signal) => {
|
|
130
|
+
if (process.platform === 'win32') {
|
|
131
|
+
// taskkill /pid <pid> /t /f walks the parent's process tree
|
|
132
|
+
// BEFORE terminating it — so we have to run it while the parent
|
|
133
|
+
// is still alive. Calling pty.kill() first (the previous
|
|
134
|
+
// ordering) detaches the children: taskkill /T then fails with
|
|
135
|
+
// "process not found" and the npm-installs / build scripts
|
|
136
|
+
// become orphans. taskkill /f also terminates the parent, so
|
|
137
|
+
// pty.kill() is the fallback for the rare case where taskkill
|
|
138
|
+
// is missing from PATH or refused (e.g. restricted PowerShell).
|
|
139
|
+
if (!taskkillProcessTree(pty.pid, process.platform, defaultExecRunner, () => killPtyDirect()))
|
|
140
|
+
killPtyDirect();
|
|
141
|
+
}
|
|
142
|
+
else
|
|
143
|
+
killPtyDirect(signal);
|
|
73
144
|
killProcessGroup(signal);
|
|
74
145
|
};
|
|
75
146
|
const clearForceKillTimer = () => {
|
|
@@ -78,8 +149,34 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
|
78
149
|
clearTimeout(forceKillTimer);
|
|
79
150
|
forceKillTimer = undefined;
|
|
80
151
|
};
|
|
152
|
+
const clearPtyReadEofTimer = () => {
|
|
153
|
+
if (!ptyReadEofTimer)
|
|
154
|
+
return;
|
|
155
|
+
clearTimeout(ptyReadEofTimer);
|
|
156
|
+
ptyReadEofTimer = undefined;
|
|
157
|
+
};
|
|
158
|
+
const schedulePtyReadEofExitGuard = (error) => {
|
|
159
|
+
if (ptyReadEofTimer)
|
|
160
|
+
return;
|
|
161
|
+
ptyReadEofTimer = setTimeout(() => {
|
|
162
|
+
ptyReadEofTimer = undefined;
|
|
163
|
+
if (stopped())
|
|
164
|
+
return;
|
|
165
|
+
console.error(`[hive] PTY read EOF without exit for run ${run.runId}`, error);
|
|
166
|
+
finishAgentRun(run, null, ptyOutputBus);
|
|
167
|
+
try {
|
|
168
|
+
killPty('SIGTERM');
|
|
169
|
+
scheduleForceKill();
|
|
170
|
+
}
|
|
171
|
+
catch (killError) {
|
|
172
|
+
ignoreMissingProcess(killError);
|
|
173
|
+
}
|
|
174
|
+
}, PTY_READ_EOF_EXIT_GRACE_MS);
|
|
175
|
+
ptyReadEofTimer.unref?.();
|
|
176
|
+
};
|
|
81
177
|
const cleanupProcessGroup = () => {
|
|
82
178
|
clearForceKillTimer();
|
|
179
|
+
clearPtyReadEofTimer();
|
|
83
180
|
killProcessGroup('SIGKILL');
|
|
84
181
|
};
|
|
85
182
|
const scheduleForceKill = () => {
|
|
@@ -88,10 +185,15 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
|
88
185
|
forceKillTimer = setTimeout(() => {
|
|
89
186
|
forceKillTimer = undefined;
|
|
90
187
|
try {
|
|
91
|
-
if (process.platform === 'win32')
|
|
92
|
-
|
|
188
|
+
if (process.platform === 'win32') {
|
|
189
|
+
// Same ordering as killPty(): tree-kill before terminating the
|
|
190
|
+
// parent, so taskkill /T can still enumerate the process tree.
|
|
191
|
+
// pty.kill() is the fallback for taskkill-missing hosts.
|
|
192
|
+
if (!taskkillProcessTree(pty.pid, process.platform, defaultExecRunner, () => killPtyDirect()))
|
|
193
|
+
killPtyDirect();
|
|
194
|
+
}
|
|
93
195
|
else
|
|
94
|
-
|
|
196
|
+
killPtyDirect('SIGKILL');
|
|
95
197
|
}
|
|
96
198
|
catch (error) {
|
|
97
199
|
ignoreMissingProcess(error);
|
|
@@ -109,6 +211,9 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
|
109
211
|
},
|
|
110
212
|
pid: pty.pid,
|
|
111
213
|
resize(cols, rows) {
|
|
214
|
+
if (!Number.isInteger(cols) || !Number.isInteger(rows) || cols <= 0 || rows <= 0) {
|
|
215
|
+
throw new Error(`Invalid terminal size for run: ${run.runId}`);
|
|
216
|
+
}
|
|
112
217
|
pty.resize(cols, rows);
|
|
113
218
|
},
|
|
114
219
|
resume() {
|
|
@@ -119,6 +224,7 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
|
119
224
|
cleanupProcessGroup();
|
|
120
225
|
return;
|
|
121
226
|
}
|
|
227
|
+
clearPtyReadEofTimer();
|
|
122
228
|
killPty('SIGTERM');
|
|
123
229
|
stdinClosed = true;
|
|
124
230
|
scheduleForceKill();
|
|
@@ -127,7 +233,7 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
|
127
233
|
if (stdinClosed || run.status === 'exited' || run.status === 'error') {
|
|
128
234
|
throw new Error(`PTY is not active for run: ${run.runId}`);
|
|
129
235
|
}
|
|
130
|
-
pty.write(text);
|
|
236
|
+
pty.write(serializePtyInput(text));
|
|
131
237
|
},
|
|
132
238
|
};
|
|
133
239
|
pty.onData((chunk) => {
|
|
@@ -138,8 +244,30 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
|
138
244
|
run.output = run.output.slice(-MAX_RUN_OUTPUT_LENGTH);
|
|
139
245
|
ptyOutputBus.publish(run.runId, chunk);
|
|
140
246
|
});
|
|
247
|
+
pty.on?.('error', (error) => {
|
|
248
|
+
if (stopped())
|
|
249
|
+
return;
|
|
250
|
+
if (isPtyReadEofError(error)) {
|
|
251
|
+
// Unix PTYs can surface a closed slave as read/EIO just before
|
|
252
|
+
// node-pty delivers the real onExit event. Treat it as EOF, not
|
|
253
|
+
// as the run's terminal status.
|
|
254
|
+
stdinClosed = true;
|
|
255
|
+
schedulePtyReadEofExitGuard(error);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
console.error(`[hive] PTY error for run ${run.runId}`, error);
|
|
259
|
+
stdinClosed = true;
|
|
260
|
+
finishAgentRun(run, null, ptyOutputBus);
|
|
261
|
+
try {
|
|
262
|
+
killPty('SIGTERM');
|
|
263
|
+
}
|
|
264
|
+
catch (killError) {
|
|
265
|
+
ignoreMissingProcess(killError);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
141
268
|
pty.onExit((event) => {
|
|
142
269
|
stdinClosed = true;
|
|
270
|
+
clearPtyReadEofTimer();
|
|
143
271
|
cleanupProcessGroup();
|
|
144
272
|
finishAgentRun(run, event.exitCode, ptyOutputBus);
|
|
145
273
|
});
|
|
@@ -3,6 +3,23 @@ import type { AgentLaunchConfigInput } from './agent-run-store.js';
|
|
|
3
3
|
import type { AgentSessionStorePort } from './agent-runtime-ports.js';
|
|
4
4
|
import type { CommandPresetRecord } from './command-preset-store.js';
|
|
5
5
|
import { type SessionCaptureSnapshot } from './session-capture.js';
|
|
6
|
+
/**
|
|
7
|
+
* Builds a `{ <PATH-key>: <new-value> }` object for the spawn env override.
|
|
8
|
+
* Critical on Windows: the OS env block reports PATH under its native casing
|
|
9
|
+
* (typically `Path`). Writing to a literal `PATH` key would, after spread
|
|
10
|
+
* with `process.env`, leave two entries — `Path` carrying the original value
|
|
11
|
+
* and `PATH` carrying our prepend. CreateProcess then sees both and the
|
|
12
|
+
* effective lookup order is undefined; in practice the child PTY often falls
|
|
13
|
+
* back to the original `Path` and never sees `HIVE_BIN_DIR`, breaking every
|
|
14
|
+
* `team` shim resolution.
|
|
15
|
+
*
|
|
16
|
+
* We detect the existing key (case-insensitive on Windows) and overwrite IT,
|
|
17
|
+
* so the merge produces exactly one PATH entry.
|
|
18
|
+
*
|
|
19
|
+
* Exported for unit testing — `buildAgentRunBootstrap` is the only in-tree
|
|
20
|
+
* caller.
|
|
21
|
+
*/
|
|
22
|
+
export declare const buildSpawnPathEnvEntry: (parentEnv: NodeJS.ProcessEnv, hiveBinDir: string, platform: NodeJS.Platform) => NodeJS.ProcessEnv;
|
|
6
23
|
export declare const buildAgentRunBootstrap: (workspace: WorkspaceSummary, agentId: string, config: AgentLaunchConfigInput, sessionStore: AgentSessionStorePort, getCommandPreset: (id: string) => CommandPresetRecord | undefined, agent?: AgentSummary) => {
|
|
7
24
|
sessionCaptureSnapshot: {
|
|
8
25
|
discriminator?: {
|
|
@@ -50,7 +67,6 @@ export declare const buildAgentRunBootstrap: (workspace: WorkspaceSummary, agent
|
|
|
50
67
|
HIVE_PROJECT_ID: string;
|
|
51
68
|
HIVE_AGENT_ID: string;
|
|
52
69
|
HIVE_AGENT_TOKEN: string;
|
|
53
|
-
PATH: string;
|
|
54
70
|
};
|
|
55
71
|
};
|
|
56
72
|
export declare const startAgentRunCapture: ({ agentId, sessionCaptureSnapshot, sessionStore, startConfig, workspace, }: {
|