@phnx-labs/agents-cli 1.20.56 → 1.20.58
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 +26 -1
- package/README.md +34 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/defaults.js +24 -0
- package/dist/commands/exec.js +28 -4
- package/dist/commands/secrets.d.ts +3 -2
- package/dist/commands/secrets.js +35 -25
- package/dist/commands/teams.d.ts +20 -1
- package/dist/commands/teams.js +105 -2
- package/dist/commands/versions.js +11 -3
- package/dist/commands/view.js +19 -4
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +28 -4
- package/dist/lib/daemon.d.ts +5 -5
- package/dist/lib/daemon.js +88 -17
- package/dist/lib/git.d.ts +9 -0
- package/dist/lib/git.js +12 -0
- package/dist/lib/hosts/dispatch.d.ts +21 -0
- package/dist/lib/hosts/dispatch.js +88 -5
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/permissions.d.ts +19 -1
- package/dist/lib/permissions.js +137 -0
- package/dist/lib/project-root.d.ts +65 -0
- package/dist/lib/project-root.js +133 -0
- package/dist/lib/resources/permissions.js +2 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/secrets/agent.d.ts +48 -18
- package/dist/lib/secrets/agent.js +288 -165
- package/dist/lib/secrets/remote.js +1 -0
- package/dist/lib/session/active.d.ts +3 -0
- package/dist/lib/session/active.js +1 -0
- package/dist/lib/session/parse.js +38 -15
- package/dist/lib/session/state.d.ts +4 -1
- package/dist/lib/session/state.js +18 -1
- package/dist/lib/session/types.d.ts +8 -0
- package/dist/lib/staleness/detectors/permissions.js +42 -0
- package/dist/lib/staleness/detectors/subagents.js +30 -0
- package/dist/lib/staleness/writers/subagents.js +13 -1
- package/dist/lib/subagents.d.ts +22 -0
- package/dist/lib/subagents.js +146 -0
- package/dist/lib/teams/agents.d.ts +30 -0
- package/dist/lib/teams/agents.js +271 -42
- package/dist/lib/types.d.ts +13 -0
- package/dist/lib/versions.d.ts +39 -0
- package/dist/lib/versions.js +199 -12
- package/package.json +1 -1
- package/scripts/postinstall.js +26 -11
package/dist/lib/teams/agents.js
CHANGED
|
@@ -27,7 +27,7 @@ import { recordRunName } from '../session/run-names.js';
|
|
|
27
27
|
import { sshExec, shellQuote } from '../ssh-exec.js';
|
|
28
28
|
import { resolveHost } from '../hosts/registry.js';
|
|
29
29
|
import { sshTargetFor } from '../hosts/types.js';
|
|
30
|
-
import { dispatchAgentsCommand } from '../hosts/dispatch.js';
|
|
30
|
+
import { dispatchAgentsCommand, terminateDispatchedTask } from '../hosts/dispatch.js';
|
|
31
31
|
import { ensureHostReady } from '../hosts/ready.js';
|
|
32
32
|
import { remoteShellFor } from '../hosts/remote-cmd.js';
|
|
33
33
|
import { resolveRemoteOsSync } from '../hosts/remote-os.js';
|
|
@@ -746,6 +746,16 @@ export class AgentProcess {
|
|
|
746
746
|
}
|
|
747
747
|
}
|
|
748
748
|
}
|
|
749
|
+
/** Reset the local stdout cursor for a newly truncated resume log. */
|
|
750
|
+
resetLogReadPosition() {
|
|
751
|
+
const previous = this.lastReadPos;
|
|
752
|
+
this.lastReadPos = 0;
|
|
753
|
+
return previous;
|
|
754
|
+
}
|
|
755
|
+
/** Restore the cursor when a resume transaction puts the prior log back. */
|
|
756
|
+
restoreLogReadPosition(position) {
|
|
757
|
+
this.lastReadPos = position;
|
|
758
|
+
}
|
|
749
759
|
async readNewEvents() {
|
|
750
760
|
// Distributed teammate: mirror the host's new log bytes locally first, then
|
|
751
761
|
// fall through to the identical local read+parse below.
|
|
@@ -1107,6 +1117,62 @@ export class AgentProcess {
|
|
|
1107
1117
|
}
|
|
1108
1118
|
}
|
|
1109
1119
|
}
|
|
1120
|
+
export async function beginResumeLogTransaction(agent) {
|
|
1121
|
+
const stdoutPath = await agent.getStdoutPath();
|
|
1122
|
+
const backupPath = `${stdoutPath}.resume-backup-${randomUUID()}`;
|
|
1123
|
+
let hadOriginal = false;
|
|
1124
|
+
try {
|
|
1125
|
+
await fs.rename(stdoutPath, backupPath);
|
|
1126
|
+
hadOriginal = true;
|
|
1127
|
+
}
|
|
1128
|
+
catch (err) {
|
|
1129
|
+
if (err?.code !== 'ENOENT')
|
|
1130
|
+
throw err;
|
|
1131
|
+
}
|
|
1132
|
+
const previousReadPos = agent.resetLogReadPosition();
|
|
1133
|
+
return { agent, stdoutPath, backupPath, hadOriginal, previousReadPos };
|
|
1134
|
+
}
|
|
1135
|
+
export async function commitResumeLogTransaction(transaction) {
|
|
1136
|
+
if (transaction.hadOriginal)
|
|
1137
|
+
await fs.rm(transaction.backupPath, { force: true });
|
|
1138
|
+
}
|
|
1139
|
+
async function rollbackResumeLogTransaction(transaction) {
|
|
1140
|
+
try {
|
|
1141
|
+
await fs.rm(transaction.stdoutPath, { force: true });
|
|
1142
|
+
if (transaction.hadOriginal) {
|
|
1143
|
+
await fs.rename(transaction.backupPath, transaction.stdoutPath);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
finally {
|
|
1147
|
+
transaction.agent.restoreLogReadPosition(transaction.previousReadPos);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
export async function terminateSpawnedProcess(pid) {
|
|
1151
|
+
try {
|
|
1152
|
+
process.kill(-pid, 'SIGTERM');
|
|
1153
|
+
}
|
|
1154
|
+
catch (err) {
|
|
1155
|
+
if (err?.code === 'ESRCH')
|
|
1156
|
+
return;
|
|
1157
|
+
throw err;
|
|
1158
|
+
}
|
|
1159
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
1160
|
+
try {
|
|
1161
|
+
process.kill(-pid, 0);
|
|
1162
|
+
}
|
|
1163
|
+
catch (err) {
|
|
1164
|
+
if (err?.code === 'ESRCH')
|
|
1165
|
+
return;
|
|
1166
|
+
throw err;
|
|
1167
|
+
}
|
|
1168
|
+
try {
|
|
1169
|
+
process.kill(-pid, 'SIGKILL');
|
|
1170
|
+
}
|
|
1171
|
+
catch (err) {
|
|
1172
|
+
if (err?.code !== 'ESRCH')
|
|
1173
|
+
throw err;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1110
1176
|
export class AgentManager {
|
|
1111
1177
|
agents = new Map();
|
|
1112
1178
|
maxAgents;
|
|
@@ -1357,11 +1423,95 @@ export class AgentManager {
|
|
|
1357
1423
|
await this.cleanupOldAgents();
|
|
1358
1424
|
return agent;
|
|
1359
1425
|
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Resume a STOPPED teammate (completed / failed / stopped) by re-entering its
|
|
1428
|
+
* own session with `message` as the next user turn. Re-launches through the
|
|
1429
|
+
* SAME backend the teammate first used (local process or remote host), reusing
|
|
1430
|
+
* its stored cwd / worktree / host / version / model / effort, and flips it
|
|
1431
|
+
* back to RUNNING so the team tracks it live again.
|
|
1432
|
+
*
|
|
1433
|
+
* The resume target is the teammate's underlying agent session id: for Claude
|
|
1434
|
+
* that IS its agent_id (unified identity, pinned via --session-id at first
|
|
1435
|
+
* launch); other harnesses only expose their session/thread id after their
|
|
1436
|
+
* first stream event, captured as `remoteSessionId`.
|
|
1437
|
+
*
|
|
1438
|
+
* Callers branch on status first — a RUNNING teammate is steered via its
|
|
1439
|
+
* mailbox, never re-launched — so this method assumes a non-running teammate.
|
|
1440
|
+
*/
|
|
1441
|
+
async resumeTeammate(agentId, message) {
|
|
1442
|
+
await this.initialize();
|
|
1443
|
+
const agent = await this.get(agentId);
|
|
1444
|
+
if (!agent)
|
|
1445
|
+
throw new Error(`No teammate with id ${agentId}`);
|
|
1446
|
+
const who = agent.name ?? agent.agentId.slice(0, 8);
|
|
1447
|
+
// The message rides as `agents run`'s prompt positional. A leading '-' makes
|
|
1448
|
+
// commander parse it as an (unknown) flag, exiting the child non-zero — the
|
|
1449
|
+
// teammate would silently land FAILED. `--` can't rescue it: `agents run`
|
|
1450
|
+
// treats post-`--` tokens as native passthrough and unsets the prompt. Fail
|
|
1451
|
+
// loud and early instead. (Steer/mailbox delivery has no such limit.)
|
|
1452
|
+
if (message.startsWith('-')) {
|
|
1453
|
+
throw new Error(`Resume message can't start with '-' — \`agents run\` would parse it as a flag. ` +
|
|
1454
|
+
`Rephrase so it leads with a word (e.g. "Please ${message}").`);
|
|
1455
|
+
}
|
|
1456
|
+
// Cloud-backed teammates run on remote provider infrastructure with no local
|
|
1457
|
+
// or host process to re-launch; continuing them goes through the provider.
|
|
1458
|
+
if (agent.cloudProvider) {
|
|
1459
|
+
throw new Error(`Teammate '${who}' is a ${agent.cloudProvider} cloud task — resume it with ` +
|
|
1460
|
+
`\`agents message ${agent.cloudSessionId ?? agent.agentId} "<message>"\` instead.`);
|
|
1461
|
+
}
|
|
1462
|
+
// For non-Claude teammates the agent_id is NOT the harness session id — that
|
|
1463
|
+
// is only known once the agent emitted its first stream event. If it never
|
|
1464
|
+
// did (e.g. it failed before its first turn), there is no resumable handle.
|
|
1465
|
+
if (agent.agentType !== 'claude' && !agent.remoteSessionId) {
|
|
1466
|
+
throw new Error(`No resumable session id was captured for ${agent.agentType} teammate '${who}' — ` +
|
|
1467
|
+
`its session id is discovered from the agent's own output, which never arrived ` +
|
|
1468
|
+
`(it may have failed before its first turn). Start a fresh teammate instead.`);
|
|
1469
|
+
}
|
|
1470
|
+
const resume = { id: agent.remoteSessionId ?? agent.agentId, message };
|
|
1471
|
+
const priorRuntime = {
|
|
1472
|
+
status: agent.status,
|
|
1473
|
+
completedAt: agent.completedAt,
|
|
1474
|
+
pid: agent.pid,
|
|
1475
|
+
startTime: agent.startTime,
|
|
1476
|
+
startedAt: agent.startedAt,
|
|
1477
|
+
remotePid: agent.remotePid,
|
|
1478
|
+
remoteLog: agent.remoteLog,
|
|
1479
|
+
remoteExit: agent.remoteExit,
|
|
1480
|
+
remoteLogOffset: agent.remoteLogOffset,
|
|
1481
|
+
worktreePath: agent.worktreePath,
|
|
1482
|
+
};
|
|
1483
|
+
// Flip to RUNNING up front so a concurrent status poll can't reap the
|
|
1484
|
+
// teammate between the exit-sentinel clear and the new PID landing; the
|
|
1485
|
+
// launch re-persists with the fresh pid/startTime. If relaunch fails before
|
|
1486
|
+
// that happens, restore the stopped lifecycle state and keep its existing
|
|
1487
|
+
// metadata/log directory intact so the user can retry.
|
|
1488
|
+
agent.status = AgentStatus.RUNNING;
|
|
1489
|
+
agent.completedAt = null;
|
|
1490
|
+
try {
|
|
1491
|
+
if (agent.hostName) {
|
|
1492
|
+
await this.launchRemoteProcess(agent, resume);
|
|
1493
|
+
}
|
|
1494
|
+
else {
|
|
1495
|
+
await this.launchProcess(agent, resume);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
catch (err) {
|
|
1499
|
+
Object.assign(agent, priorRuntime);
|
|
1500
|
+
try {
|
|
1501
|
+
await agent.saveMeta();
|
|
1502
|
+
}
|
|
1503
|
+
catch (restoreErr) {
|
|
1504
|
+
throw new Error(`Failed to resume teammate: ${err.message}; restoring stopped state also failed: ${restoreErr.message}`, { cause: err });
|
|
1505
|
+
}
|
|
1506
|
+
throw err;
|
|
1507
|
+
}
|
|
1508
|
+
return agent;
|
|
1509
|
+
}
|
|
1360
1510
|
/**
|
|
1361
1511
|
* Actually spawn the OS process for a teammate. Extracted from spawn() so
|
|
1362
1512
|
* staged teammates can be launched later by startReady().
|
|
1363
1513
|
*/
|
|
1364
|
-
async launchProcess(agent) {
|
|
1514
|
+
async launchProcess(agent, resume) {
|
|
1365
1515
|
const running = await this.listRunning();
|
|
1366
1516
|
warnIfMemoryLow(running.length);
|
|
1367
1517
|
const effort = agent.effort ?? 'medium';
|
|
@@ -1369,11 +1519,26 @@ export class AgentManager {
|
|
|
1369
1519
|
// forwarded). Effort is a separate knob wired into buildReasoningFlags
|
|
1370
1520
|
// inside buildCommand.
|
|
1371
1521
|
const resolvedModel = agent.model ?? null;
|
|
1372
|
-
const cmd = this.buildCommand(agent.agentType, agent.prompt, agent.mode, resolvedModel, agent.cwd, agent.agentId, effort, agent.version, agent.profileName);
|
|
1373
|
-
debug(`Launching ${agent.agentType} agent ${agent.agentId} [${agent.mode}]: ${cmd.slice(0, 3).join(' ')}...`);
|
|
1522
|
+
const cmd = this.buildCommand(agent.agentType, agent.prompt, agent.mode, resolvedModel, agent.cwd, agent.agentId, effort, agent.version, agent.profileName, resume);
|
|
1523
|
+
debug(`Launching ${agent.agentType} agent ${agent.agentId} [${agent.mode}]${resume ? ' (resume)' : ''}: ${cmd.slice(0, 3).join(' ')}...`);
|
|
1524
|
+
let childProcess = null;
|
|
1525
|
+
let stdoutFile = null;
|
|
1526
|
+
let resumeLog = null;
|
|
1374
1527
|
try {
|
|
1375
|
-
|
|
1376
|
-
|
|
1528
|
+
if (resume)
|
|
1529
|
+
resumeLog = await beginResumeLogTransaction(agent);
|
|
1530
|
+
const stdoutPath = resumeLog?.stdoutPath ?? await agent.getStdoutPath();
|
|
1531
|
+
// Always TRUNCATE — including on resume. The status reader re-reads the
|
|
1532
|
+
// whole log from byte 0 every poll (lastReadPos is in-memory, not
|
|
1533
|
+
// persisted) and marks terminal status from the last `result` event it
|
|
1534
|
+
// sees, with no liveness guard. If the resumed turn's stream were appended
|
|
1535
|
+
// after the prior turn's `result:success`, that stale event would win for
|
|
1536
|
+
// the entire duration of the new (still-running) turn — reporting the
|
|
1537
|
+
// teammate COMPLETED while it works, and steering a second follow-up into
|
|
1538
|
+
// a forked session. Truncating keeps exactly one turn in the log, so the
|
|
1539
|
+
// re-read is always correct. The authoritative transcript lives in the
|
|
1540
|
+
// agent's own session (resumed via --resume), not this stdout mirror.
|
|
1541
|
+
stdoutFile = await fs.open(stdoutPath, 'w');
|
|
1377
1542
|
const stdoutFd = stdoutFile.fd;
|
|
1378
1543
|
// Wrap the teammate command in a shell that records the underlying CLI's
|
|
1379
1544
|
// exit code to a sentinel file. Detached + unref()'d children can't be
|
|
@@ -1387,7 +1552,7 @@ export class AgentManager {
|
|
|
1387
1552
|
const wrappedCmd = buildSentinelCommand(cmd, exitCodePath);
|
|
1388
1553
|
// detached:true makes the shell the process-group leader, so stop()'s
|
|
1389
1554
|
// `kill(-pid)` still reaches the underlying CLI through the group.
|
|
1390
|
-
|
|
1555
|
+
childProcess = spawn('/bin/sh', ['-c', wrappedCmd], {
|
|
1391
1556
|
stdio: ['ignore', stdoutFd, stdoutFd],
|
|
1392
1557
|
cwd: agent.cwd || undefined,
|
|
1393
1558
|
detached: true,
|
|
@@ -1395,8 +1560,13 @@ export class AgentManager {
|
|
|
1395
1560
|
? { ...sanitizeProcessEnv(process.env), ...agent.envOverrides }
|
|
1396
1561
|
: sanitizeProcessEnv(process.env),
|
|
1397
1562
|
});
|
|
1563
|
+
await new Promise((resolve, reject) => {
|
|
1564
|
+
childProcess.once('spawn', resolve);
|
|
1565
|
+
childProcess.once('error', reject);
|
|
1566
|
+
});
|
|
1398
1567
|
childProcess.unref();
|
|
1399
|
-
stdoutFile.close()
|
|
1568
|
+
await stdoutFile.close();
|
|
1569
|
+
stdoutFile = null;
|
|
1400
1570
|
agent.pid = childProcess.pid || null;
|
|
1401
1571
|
// Capture start-time NOW, while we know the PID is ours. Once the
|
|
1402
1572
|
// OS reuses this PID slot, /proc and `ps` will report a different
|
|
@@ -1406,9 +1576,21 @@ export class AgentManager {
|
|
|
1406
1576
|
agent.status = AgentStatus.RUNNING;
|
|
1407
1577
|
agent.startedAt = new Date();
|
|
1408
1578
|
await agent.saveMeta();
|
|
1579
|
+
if (resumeLog)
|
|
1580
|
+
await commitResumeLogTransaction(resumeLog);
|
|
1409
1581
|
}
|
|
1410
1582
|
catch (err) {
|
|
1411
|
-
|
|
1583
|
+
if (stdoutFile)
|
|
1584
|
+
await stdoutFile.close().catch(() => { });
|
|
1585
|
+
if (childProcess?.pid)
|
|
1586
|
+
await terminateSpawnedProcess(childProcess.pid);
|
|
1587
|
+
if (resumeLog)
|
|
1588
|
+
await rollbackResumeLogTransaction(resumeLog);
|
|
1589
|
+
// Fresh spawns own a newly-created directory, so a failed launch removes
|
|
1590
|
+
// that partial record. A resume reuses an existing teammate: its caller
|
|
1591
|
+
// restores the prior terminal state and preserves the directory for retry.
|
|
1592
|
+
if (!resume)
|
|
1593
|
+
await this.cleanupPartialAgent(agent);
|
|
1412
1594
|
console.error(`Failed to spawn agent ${agent.agentId}:`, err);
|
|
1413
1595
|
throw new Error(`Failed to spawn agent: ${err.message}`);
|
|
1414
1596
|
}
|
|
@@ -1424,7 +1606,7 @@ export class AgentManager {
|
|
|
1424
1606
|
* created ON THE HOST off the freshly-fetched default branch; the teammate runs
|
|
1425
1607
|
* there. Otherwise it runs in the host repo path directly.
|
|
1426
1608
|
*/
|
|
1427
|
-
async launchRemoteProcess(agent) {
|
|
1609
|
+
async launchRemoteProcess(agent, resume) {
|
|
1428
1610
|
if (!agent.hostName || !agent.hostTarget || !agent.repoPath) {
|
|
1429
1611
|
throw new Error(`Remote teammate ${agent.agentId} is missing host placement (host/target/repo).`);
|
|
1430
1612
|
}
|
|
@@ -1448,33 +1630,74 @@ export class AgentManager {
|
|
|
1448
1630
|
}
|
|
1449
1631
|
// Worktree isolation on the host, if the team enables it. createRemoteWorktree
|
|
1450
1632
|
// fetches origin and branches off origin/<default>, returning the host path.
|
|
1633
|
+
// On RESUME the worktree already exists from the original launch — reuse it
|
|
1634
|
+
// (its path is persisted) instead of re-creating (which would fail on the
|
|
1635
|
+
// existing branch and would also discard the teammate's in-progress work).
|
|
1451
1636
|
let remoteCwd = agent.repoPath;
|
|
1452
1637
|
if (agent.worktreeName) {
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1638
|
+
if (resume && agent.worktreePath) {
|
|
1639
|
+
remoteCwd = agent.worktreePath;
|
|
1640
|
+
}
|
|
1641
|
+
else {
|
|
1642
|
+
const worktreePath = createRemoteWorktree(agent.hostTarget, agent.repoPath, agent.worktreeName);
|
|
1643
|
+
agent.worktreePath = worktreePath;
|
|
1644
|
+
remoteCwd = worktreePath;
|
|
1645
|
+
}
|
|
1456
1646
|
}
|
|
1457
1647
|
// Same run argv the local path builds (shared buildRunArgv keeps the prompt
|
|
1458
1648
|
// scaffolding + flags from drifting); dispatched non-blocking (follow:false)
|
|
1459
1649
|
// — the supervisor polls the host, we don't block here.
|
|
1460
1650
|
const effort = agent.effort ?? 'medium';
|
|
1461
|
-
const forwardedArgs = this.buildRunArgv(agent.agentType, agent.prompt, agent.mode, agent.model ?? null, effort, agent.version, agent.profileName);
|
|
1651
|
+
const forwardedArgs = this.buildRunArgv(agent.agentType, agent.prompt, agent.mode, agent.model ?? null, effort, agent.version, agent.profileName, resume);
|
|
1652
|
+
let dispatchedTask = null;
|
|
1653
|
+
let resumeLog = null;
|
|
1462
1654
|
try {
|
|
1655
|
+
if (resume) {
|
|
1656
|
+
resumeLog = await beginResumeLogTransaction(agent);
|
|
1657
|
+
await fs.writeFile(resumeLog.stdoutPath, '');
|
|
1658
|
+
}
|
|
1463
1659
|
const { task } = await dispatchAgentsCommand(host, {
|
|
1464
1660
|
forwardedArgs,
|
|
1465
1661
|
remoteCwd,
|
|
1466
1662
|
follow: false,
|
|
1467
1663
|
});
|
|
1664
|
+
dispatchedTask = task;
|
|
1468
1665
|
agent.remotePid = task.pid ?? null;
|
|
1469
1666
|
agent.remoteLog = task.remoteLog ?? null;
|
|
1470
1667
|
agent.remoteExit = task.remoteExit ?? null;
|
|
1471
1668
|
agent.remoteLogOffset = 0;
|
|
1669
|
+
// On resume the offset resets to 0 against a FRESH remote log, and
|
|
1670
|
+
// syncRemoteMirror appends the delta onto the local mirror. Truncate that
|
|
1671
|
+
// mirror first so the prior turn's terminal event can't linger and get
|
|
1672
|
+
// re-read as the current status (same hazard the local path truncates for).
|
|
1472
1673
|
agent.status = AgentStatus.RUNNING;
|
|
1473
1674
|
agent.startedAt = new Date();
|
|
1474
1675
|
await agent.saveMeta();
|
|
1676
|
+
if (resumeLog)
|
|
1677
|
+
await commitResumeLogTransaction(resumeLog);
|
|
1475
1678
|
}
|
|
1476
1679
|
catch (err) {
|
|
1680
|
+
let cleanupError = null;
|
|
1681
|
+
if (dispatchedTask) {
|
|
1682
|
+
try {
|
|
1683
|
+
terminateDispatchedTask(dispatchedTask);
|
|
1684
|
+
}
|
|
1685
|
+
catch (cleanupErr) {
|
|
1686
|
+
cleanupError = cleanupErr;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
if (resumeLog) {
|
|
1690
|
+
try {
|
|
1691
|
+
await rollbackResumeLogTransaction(resumeLog);
|
|
1692
|
+
}
|
|
1693
|
+
catch (cleanupErr) {
|
|
1694
|
+
cleanupError = cleanupError ?? cleanupErr;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1477
1697
|
console.error(`Failed to launch remote teammate ${agent.agentId} on ${agent.hostName}:`, err);
|
|
1698
|
+
if (cleanupError) {
|
|
1699
|
+
throw new Error(`Failed to launch remote teammate: ${err.message}; cleanup failed: ${cleanupError.message}`, { cause: err });
|
|
1700
|
+
}
|
|
1478
1701
|
throw new Error(`Failed to launch remote teammate: ${err.message}`);
|
|
1479
1702
|
}
|
|
1480
1703
|
debug(`Launched remote agent ${agent.agentId} on ${agent.hostName} (remote pid ${agent.remotePid})`);
|
|
@@ -1670,38 +1893,46 @@ export class AgentManager {
|
|
|
1670
1893
|
* before invoking `agents`. `sessionId` is likewise local-only (the remote run
|
|
1671
1894
|
* mints its own session on the host).
|
|
1672
1895
|
*/
|
|
1673
|
-
buildRunArgv(agentType, prompt, mode, model, effort, version, profileName) {
|
|
1674
|
-
// Compose the prompt
|
|
1675
|
-
//
|
|
1676
|
-
//
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1896
|
+
buildRunArgv(agentType, prompt, mode, model, effort, version, profileName, resume) {
|
|
1897
|
+
// Compose the prompt. On RESUME the message is the teammate's next user turn,
|
|
1898
|
+
// not a fresh brief — so skip the original brief and the plan-mode prefix, but
|
|
1899
|
+
// keep PROMPT_SUFFIX so the resumed run still emits a final summary the team
|
|
1900
|
+
// parser reads. On a fresh launch, add the plan-mode prefix for Claude and the
|
|
1901
|
+
// universal summary suffix. These are team-specific prompt scaffolding —
|
|
1902
|
+
// `agents run` does not apply them.
|
|
1903
|
+
let fullPrompt;
|
|
1904
|
+
if (resume) {
|
|
1905
|
+
fullPrompt = resume.message + PROMPT_SUFFIX;
|
|
1906
|
+
}
|
|
1907
|
+
else {
|
|
1908
|
+
fullPrompt = prompt + PROMPT_SUFFIX;
|
|
1909
|
+
if (agentType === 'claude' && mode === 'plan') {
|
|
1910
|
+
fullPrompt = CLAUDE_PLAN_MODE_PREFIX + fullPrompt;
|
|
1911
|
+
}
|
|
1680
1912
|
}
|
|
1681
1913
|
// Profile target takes precedence — `agents run <profile>` resolves the
|
|
1682
1914
|
// host harness, version pin, and env injection in one place. Plain
|
|
1683
1915
|
// version pins only apply when no profile is selected.
|
|
1684
1916
|
const target = profileName ?? (version ? `${agentType}@${version}` : agentType);
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
'--
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
];
|
|
1917
|
+
// Keep the prompt as the first positional (right after target), matching the
|
|
1918
|
+
// fresh-launch shape, and add `--resume <id>` among the flags. `agents run`
|
|
1919
|
+
// continues the teammate's own session natively (claude `--resume`, codex
|
|
1920
|
+
// `resume`) or via the universal `/continue` replay for other harnesses.
|
|
1921
|
+
const args = ['run', target, fullPrompt];
|
|
1922
|
+
if (resume) {
|
|
1923
|
+
args.push('--resume', resume.id);
|
|
1924
|
+
}
|
|
1925
|
+
args.push('--mode', mode, '--effort', effort, '--json', '--headless', '--quiet');
|
|
1695
1926
|
if (model)
|
|
1696
1927
|
args.push('--model', model);
|
|
1697
1928
|
args.push('--env', 'AGENTS_RUNTIME=teams');
|
|
1698
1929
|
return args;
|
|
1699
1930
|
}
|
|
1700
|
-
buildCommand(agentType, prompt, mode, model, cwd = null, sessionId = null, effort = 'medium', version = null, profileName = null) {
|
|
1931
|
+
buildCommand(agentType, prompt, mode, model, cwd = null, sessionId = null, effort = 'medium', version = null, profileName = null, resume) {
|
|
1701
1932
|
// Route through getAgentsInvocation so a teammate launched by the compiled
|
|
1702
1933
|
// standalone binary (#315) doesn't relaunch as `agents /$bunfs/root/agents …`
|
|
1703
1934
|
// (process.argv[1] is the bun virtual entry there) → "unknown command".
|
|
1704
|
-
const inv = getAgentsInvocation(this.buildRunArgv(agentType, prompt, mode, model, effort, version, profileName));
|
|
1935
|
+
const inv = getAgentsInvocation(this.buildRunArgv(agentType, prompt, mode, model, effort, version, profileName, resume));
|
|
1705
1936
|
const cmd = [inv.command, ...inv.args];
|
|
1706
1937
|
if (cwd)
|
|
1707
1938
|
cmd.push('--cwd', cwd);
|
|
@@ -1709,7 +1940,9 @@ export class AgentManager {
|
|
|
1709
1940
|
// AGENTS_MAILBOX_DIR by the same id mailboxIdForActiveSession returns.
|
|
1710
1941
|
// Claude also forwards --session-id to its CLI (unified identity);
|
|
1711
1942
|
// other agents ignore the flag but still get the correct mailbox dir.
|
|
1712
|
-
|
|
1943
|
+
// On RESUME we continue an existing session — `--session-id` CREATES one and
|
|
1944
|
+
// `agents run` rejects it alongside `--resume`, so it must be omitted.
|
|
1945
|
+
if (sessionId && !resume) {
|
|
1713
1946
|
cmd.push('--session-id', sessionId);
|
|
1714
1947
|
}
|
|
1715
1948
|
// Claude: grant access to the teammate's working directory.
|
|
@@ -1817,17 +2050,13 @@ export class AgentManager {
|
|
|
1817
2050
|
if (!agent) {
|
|
1818
2051
|
return false;
|
|
1819
2052
|
}
|
|
1820
|
-
// Distributed teammate: no local PID — signal
|
|
1821
|
-
//
|
|
1822
|
-
//
|
|
1823
|
-
// `nohup bash -lc … &` under a non-interactive shell where job control is off,
|
|
1824
|
-
// so `&` may NOT open a new group — fall back to signalling the wrapper pid
|
|
1825
|
-
// directly. Best-effort either way; the `.exit` sentinel is the durable
|
|
1826
|
-
// terminal-status source if a grandchild lingers.
|
|
2053
|
+
// Distributed teammate: no local PID — signal the dedicated process group
|
|
2054
|
+
// created by dispatch.ts. The persisted PID is the group leader, so one
|
|
2055
|
+
// negative-PID signal reaches the login-shell wrapper and every descendant.
|
|
1827
2056
|
if (agent.hostName && agent.status === AgentStatus.RUNNING) {
|
|
1828
2057
|
if (agent.hostTarget && agent.remotePid) {
|
|
1829
2058
|
try {
|
|
1830
|
-
sshExec(agent.hostTarget, `kill -TERM -- -${agent.remotePid} 2>/dev/null
|
|
2059
|
+
sshExec(agent.hostTarget, `kill -TERM -- -${agent.remotePid} 2>/dev/null`, {
|
|
1831
2060
|
timeoutMs: 10000,
|
|
1832
2061
|
multiplex: true,
|
|
1833
2062
|
});
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -643,6 +643,12 @@ export interface Meta {
|
|
|
643
643
|
registries?: Record<RegistryType, Record<string, RegistryConfig>>;
|
|
644
644
|
versions?: Partial<Record<AgentId, Record<string, VersionResources>>>;
|
|
645
645
|
source?: string;
|
|
646
|
+
/**
|
|
647
|
+
* Projects root for the `agents run --project <slug>` shorthand, e.g.
|
|
648
|
+
* `~/src/github.com/<user>`. Auto-inferred from the repo you launch inside and
|
|
649
|
+
* cached here; stored home-relative (`~/…`) so it resolves on remote hosts too.
|
|
650
|
+
*/
|
|
651
|
+
projectRoot?: string;
|
|
646
652
|
/**
|
|
647
653
|
* Extra DotAgent repos merged after ~/.agents/. Managed clones live as peer
|
|
648
654
|
* dirs at ~/.agents-<alias>/; user-owned repos can point at arbitrary paths
|
|
@@ -771,6 +777,13 @@ export interface ClaudePermissions {
|
|
|
771
777
|
additionalDirectories?: string[];
|
|
772
778
|
};
|
|
773
779
|
}
|
|
780
|
+
/** Cursor CLI native format in ~/.cursor/cli-config.json (Shell/Read/Write/WebFetch/Mcp). */
|
|
781
|
+
export interface CursorPermissions {
|
|
782
|
+
permissions: {
|
|
783
|
+
allow: string[];
|
|
784
|
+
deny: string[];
|
|
785
|
+
};
|
|
786
|
+
}
|
|
774
787
|
/** OpenCode's native permission format (per-command allow/deny/ask). */
|
|
775
788
|
export interface OpenCodePermissions {
|
|
776
789
|
permission: {
|
package/dist/lib/versions.d.ts
CHANGED
|
@@ -114,6 +114,41 @@ export declare function getVersionDir(agent: AgentId, version: string): string;
|
|
|
114
114
|
* Get the binary path for a specific agent version.
|
|
115
115
|
*/
|
|
116
116
|
export declare function getBinaryPath(agent: AgentId, version: string): string;
|
|
117
|
+
/**
|
|
118
|
+
* Does this agent resolve to ONE global binary that is the same file regardless
|
|
119
|
+
* of the `version` argument? (droid → always `~/.local/bin/droid`.) Computed
|
|
120
|
+
* generically by probing `getBinaryPath` with two distinct versions rather than
|
|
121
|
+
* hardcoding an agent id, so it stays correct if another global-binary agent is
|
|
122
|
+
* added.
|
|
123
|
+
*
|
|
124
|
+
* This is the narrower cousin of `isSelfUpdatingAgent`: every global-binary
|
|
125
|
+
* agent is self-updating, but grok is self-updating WITHOUT a global binary — it
|
|
126
|
+
* stores a real per-version binary copy under each version-home
|
|
127
|
+
* (`versions/grok/<v>/home/.grok/downloads/grok-<v>`), so its version-homes are
|
|
128
|
+
* genuinely distinct and must NOT be collapsed. Gate the single-binary
|
|
129
|
+
* collapse/live-version logic on THIS predicate; gate pin-refusal / "switch
|
|
130
|
+
* profile" copy on `isSelfUpdatingAgent`.
|
|
131
|
+
*/
|
|
132
|
+
export declare function isGlobalBinaryAgent(agent: AgentId): boolean;
|
|
133
|
+
/** Drop the live-version cache (call after an install/remove that changes the
|
|
134
|
+
* running binary, e.g. `agents add droid@latest`). */
|
|
135
|
+
export declare function invalidateLiveVersionCache(agent?: AgentId): void;
|
|
136
|
+
/**
|
|
137
|
+
* Resolve the version the ONE globally-installed binary actually reports via
|
|
138
|
+
* `<cli> --version`, cached for {@link LIVE_VERSION_TTL_MS}. For a self-updating
|
|
139
|
+
* global-binary agent (droid) this is the single source of truth for "which
|
|
140
|
+
* version is installed" — the on-disk version-dir NAMES are just stale labels
|
|
141
|
+
* left behind by successive `agents add`/self-update cycles. Returns null when
|
|
142
|
+
* the binary isn't on PATH or the probe fails.
|
|
143
|
+
*/
|
|
144
|
+
export declare function getLiveVersion(agent: AgentId): Promise<string | null>;
|
|
145
|
+
/**
|
|
146
|
+
* Synchronous, non-blocking read of the live-version cache — returns the value
|
|
147
|
+
* only if a recent {@link getLiveVersion} call already warmed it, else null.
|
|
148
|
+
* `listInstalledVersions` is sync and must not shell out, so it prefers this
|
|
149
|
+
* warm value (accurate) and otherwise falls back to the newest on-disk dir.
|
|
150
|
+
*/
|
|
151
|
+
export declare function getCachedLiveVersion(agent: AgentId): string | null;
|
|
117
152
|
/**
|
|
118
153
|
* Get the isolated HOME directory for a specific agent version.
|
|
119
154
|
* Each version has its own config isolation (like jobs sandbox).
|
|
@@ -155,6 +190,10 @@ export declare function isOldestInstalled(agent: AgentId): Promise<{
|
|
|
155
190
|
export declare function invalidateInstalledVersionsCache(agent?: AgentId): void;
|
|
156
191
|
/**
|
|
157
192
|
* List all installed versions for an agent (cached by versions-dir mtime).
|
|
193
|
+
*
|
|
194
|
+
* For a self-updating global-binary agent (droid) every version dir resolves to
|
|
195
|
+
* the SAME binary, so this collapses them to a single canonical entry — one
|
|
196
|
+
* install, one row in `agents view`, never the phantom set of semver dir names.
|
|
158
197
|
*/
|
|
159
198
|
export declare function listInstalledVersions(agent: AgentId): string[];
|
|
160
199
|
/**
|