@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +34 -3
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/defaults.js +24 -0
  5. package/dist/commands/exec.js +28 -4
  6. package/dist/commands/secrets.d.ts +3 -2
  7. package/dist/commands/secrets.js +35 -25
  8. package/dist/commands/teams.d.ts +20 -1
  9. package/dist/commands/teams.js +105 -2
  10. package/dist/commands/versions.js +11 -3
  11. package/dist/commands/view.js +19 -4
  12. package/dist/lib/agents.d.ts +21 -0
  13. package/dist/lib/agents.js +28 -4
  14. package/dist/lib/daemon.d.ts +5 -5
  15. package/dist/lib/daemon.js +88 -17
  16. package/dist/lib/git.d.ts +9 -0
  17. package/dist/lib/git.js +12 -0
  18. package/dist/lib/hosts/dispatch.d.ts +21 -0
  19. package/dist/lib/hosts/dispatch.js +88 -5
  20. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  21. package/dist/lib/permissions.d.ts +19 -1
  22. package/dist/lib/permissions.js +137 -0
  23. package/dist/lib/project-root.d.ts +65 -0
  24. package/dist/lib/project-root.js +133 -0
  25. package/dist/lib/resources/permissions.js +2 -0
  26. package/dist/lib/resources/types.d.ts +1 -1
  27. package/dist/lib/secrets/agent.d.ts +48 -18
  28. package/dist/lib/secrets/agent.js +288 -165
  29. package/dist/lib/secrets/remote.js +1 -0
  30. package/dist/lib/session/active.d.ts +3 -0
  31. package/dist/lib/session/active.js +1 -0
  32. package/dist/lib/session/parse.js +38 -15
  33. package/dist/lib/session/state.d.ts +4 -1
  34. package/dist/lib/session/state.js +18 -1
  35. package/dist/lib/session/types.d.ts +8 -0
  36. package/dist/lib/staleness/detectors/permissions.js +42 -0
  37. package/dist/lib/staleness/detectors/subagents.js +30 -0
  38. package/dist/lib/staleness/writers/subagents.js +13 -1
  39. package/dist/lib/subagents.d.ts +22 -0
  40. package/dist/lib/subagents.js +146 -0
  41. package/dist/lib/teams/agents.d.ts +30 -0
  42. package/dist/lib/teams/agents.js +271 -42
  43. package/dist/lib/types.d.ts +13 -0
  44. package/dist/lib/versions.d.ts +39 -0
  45. package/dist/lib/versions.js +199 -12
  46. package/package.json +1 -1
  47. package/scripts/postinstall.js +26 -11
package/CHANGELOG.md CHANGED
@@ -2,10 +2,35 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.20.58
6
+
7
+ - **Self-updating agent CLIs are represented as one live installation.** `agents view` no longer invents version-home rows for single-binary installers such as Droid, Grok, Cursor, Kiro, Goose, and Hermes; it reports the version returned by the installed binary and folds away stale per-version directories. `agents add <agent>@<version>` now installs or keeps that agent's current release instead of rejecting an unsupported pinned install. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/versions.ts`, `apps/cli/src/commands/{versions,view}.ts`. (RUSH-1321)
8
+ - **Stopped teammate resumes are transactional from launch through persistence.** If a local or remote resume fails, the existing teammate record, directory, runtime metadata, stdout mirror, and log cursor are restored; any replacement wrapper and its descendants are terminated as one process group. A successful resume whose log was truncated restarts parsing at byte zero, and a secondary restore-write failure retains the original launch error as its cause. Source: `apps/cli/src/lib/teams/agents.ts`, `apps/cli/src/lib/hosts/dispatch.ts`. (#1104, #1108)
9
+ - **Wire allowlist support for Cursor CLI.** Cursor agent CLI stores allow/deny in `~/.cursor/cli-config.json` (`permissions.allow`/`deny` with Shell/Read/Write/WebFetch/Mcp). Flip `allowlist: true`, add `convertToCursorFormat` (Bash→Shell), and write via `applyPermissionsToVersion` + detector. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/permissions.ts`. (RUSH-1387)
10
+ - **GitHub Copilot CLI subagents now sync (RUSH-1390).** Installed subagents flatten into GitHub Copilot custom-agent profiles at `~/.copilot/agents/<name>.agent.md` (the Droid custom-droid format), gated to Copilot CLI ≥ 0.0.353. `agents subagents list/view` now surfaces synced Copilot agents and `agents subagents remove` soft-deletes their `.agent.md` files to trash — both previously skipped `copilot` entirely. Source: `apps/cli/src/lib/subagents.ts` (`listSubagentsForAgent`, `removeSubagentFromVersion`, `transformSubagentForCopilot`), `apps/cli/src/lib/staleness/writers/subagents.ts`, `apps/cli/src/lib/staleness/detectors/subagents.ts`, `apps/cli/src/lib/agents.ts`.
11
+ - **Menu-bar Quick Dispatch preserves typed drafts when focus is stolen (RUSH-1592).** If another app activates while the `Cmd-Shift-O` capture panel is open, the panel can hide without destroying the note; the next summon restores the draft text plus selected screenshots, action, and agents. Return submits and clears the draft; Escape clears without dispatching. Source: `apps/cli/menubar/Sources/MenubarHelper/PromptPanel.swift`, `apps/cli/docs/menubar.md`.
12
+ - **Wire subagents support for Kiro CLI.** Kiro custom agents are JSON files under `~/.kiro/agents/*.json` (introduced in kiro-cli v1.23.0). Flip Kiro's `subagents: { since: '1.23.0' }`, add `transformSubagentForKiro`, and wire the subagents writer, detector, install/remove, and orphan-detection paths. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/subagents.ts`, `apps/cli/src/lib/staleness/{writers,detectors}/subagents.ts`. (RUSH-1393)
13
+ - **Menu-bar ticket agents now carry every selected screenshot into the Linear issue (RUSH-1668).** `Cmd-Shift-O` already passed selected file paths to the ticket agent, but the prompt only asked it to inspect them, so agents could create text-only issues and stop. The brief now identifies every selected path as user-provided ticket material, requires each file to be uploaded, supplies the existing `linear update <id> --proof <path>` path as a reliable default, and leaves description/comment/other placement to the agent's judgment. Source: `apps/cli/menubar/Sources/MenubarHelper/{AgentsCLI,IssueSelfTest}.swift`, `apps/cli/docs/menubar.md`.
14
+ - **`agents sessions --active --json` now carries session attachment metadata for Factory previews (RUSH-1524).** Claude and Droid prompt image/document blocks that reference local files are preserved as `{ path, name, mediaType, sizeBytes }`, and the active-session state dedupes them into `attachments` so consumers can render screenshot thumbnails and open the original files instead of only seeing an attachment count. Source: `apps/cli/src/lib/session/parse.ts`, `apps/cli/src/lib/session/state.ts`, `apps/cli/src/lib/session/active.ts`.
15
+ - **Retired the standalone `com.phnx-labs.agents-secrets-agent` launchd service — the always-on daemon is now the sole broker host (#416, step 2).** `ensureAgentRunning()` no longer installs a separate launchd service: it retires any leftover plist via the new `retireLegacySecretsAgentService()` and relies on the daemon (Path 0), with a one-off detached broker as the only fallback. The upgrade migration (`scripts/postinstall.js` → `healLongRunningProcesses`) now `launchctl bootout`s the legacy service **first**, then (re)starts the daemon so it takes over the broker socket, instead of kickstarting the old service onto new code. `agents secrets start` is now a thin alias that brings the daemon up (and waits for the broker to answer); `agents secrets stop` locks all bundles and retires any leftover legacy service while leaving the always-on daemon running; `agents secrets status` reports broker reachability (daemon-hosted vs standalone) rather than "service installed". The stale broker teardown (version-skew self-heal) retires the legacy service instead of kickstarting it. Source: `apps/cli/src/lib/secrets/agent.ts` (`retireLegacySecretsAgentService`, `ensureAgentRunning`, `teardownStaleBroker`, `uninstallSecretsAgentService`; removed `installSecretsAgentService`/`kickstartSecretsAgentService`/`generateServicePlist`), `apps/cli/scripts/postinstall.js` (`healLongRunningProcesses`), `apps/cli/src/commands/secrets.ts` (`start`/`stop`/`status`).
16
+
17
+ - **Clarify the native escape hatch behind `--mode skip`.** The README and bundled `run` skill now discourage `skip`, list its exact direct-exec per-harness flag mappings and ACP `allow_always` behavior, replace an older recommendation of unsafe `full` for ordinary writes, and distinguish Codex `auto` (sandboxed `edit`, which can still prompt) from Codex `skip` (`--dangerously-bypass-approvals-and-sandbox`, equivalent to unsandboxed `--yolo`). Documentation only; runtime behavior is unchanged. Source: `README.md`, `skills/run/SKILL.md`.
18
+ - **Wire allowlist support for Kiro CLI.** Kiro 2.8.0+ permission groups now sync into `~/.kiro/settings/permissions.yaml` as v3 capability rules for shell, filesystem, and web access; existing user-authored rules are preserved and duplicate generated rules are removed. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/permissions.ts`, `apps/cli/src/lib/staleness/detectors/permissions.ts`. (RUSH-1392)
19
+ - **Fix: remote `agents secrets view <bundle>@host --reveal` no longer leaves a 60-second SSH control master behind.** The interactive `-tt` reveal path now opts out of default SSH multiplexing (`multiplex: false`), matching the transport guidance for one-shot commands that must not keep a `ControlPersist` socket open after a Touch ID/passphrase reveal. Source: `apps/cli/src/lib/secrets/remote.ts` (`remoteSecretsRaw`).
20
+ - **`agents run --host <host> --cwd <dir>` now sets the working directory ON the host (and a new `--project` shorthand jumps to a project by name).** Previously `--cwd` was silently dropped for `--host` runs — only the separate `--remote-cwd` flag worked — so `agents run claude --host s1 --cwd ~/src/foo` landed in the remote login-shell's default directory with no warning. `--cwd` is now forwarded as the host working directory, and a home-anchored path (`~/…`, `$HOME/…`, or a local-home absolute the shell already expanded like `/Users/me/…`) is re-rooted at the *remote* `$HOME` so it resolves correctly across machines with different home paths (`/Users/me` → `/home/me`). `--remote-cwd` remains as the explicit override. New `-P, --project <slug>[@worktree]` resolves a bare project name against your projects root (e.g. `~/src/github.com/<user>`) — auto-inferred from the repo you launch inside and cached in `agents.yaml`, or set/shown with `agents defaults project-root [path]`; `--project foo@fix` targets the `fix` git worktree. Works for local and `--host` runs. Verified end-to-end: `agents run claude --host yosemite-s1 --project agents-cli` runs the remote agent with `pwd` = `/home/muqsit/src/github.com/muqsitnawaz/agents-cli`. Source: `apps/cli/src/lib/project-root.ts` (new), `apps/cli/src/lib/hosts/dispatch.ts` (`remoteCdPrefix`), `apps/cli/src/commands/exec.ts` (`--project`/`--cwd` host wiring), `apps/cli/src/commands/defaults.ts` (`project-root`).
21
+ - **Fix daemon crash-looping when its pinned Node version is pruned (fleet-wide).** The routine daemon's launchd/systemd manifest hardcoded `~/.nvm/versions/node/v24.0.0/bin` on PATH, and launched the CLI entry bare when it was an extension-less shim or a `bin/agents → dist/index.js` symlink (an extension check on the link name missed it). The moment that exact nvm patch was upgraded away, the shim's `#!/usr/bin/env node` shebang fell through to an ancient system node (Node 18 → `SyntaxError: node:util has no export 'styleText'` from `@inquirer/core`), and the service crash-looped at import — observed at 100k+ restarts on Linux workers, silently killing all scheduled routines. `getDaemonLaunch` now detects Node-script entries by resolving symlinks and sniffing the shebang (not just the `.js`/`.cjs`/`.mjs` extension), so it pins them to `process.execPath`; and the generated PATH now leads with `path.dirname(process.execPath)` — the Node that installed the service — instead of a hardcoded nvm version, so both the shim and child routine processes always resolve a working runtime. Source: `apps/cli/src/lib/daemon.ts` (`getDaemonLaunch`, `isNodeScriptEntry`, `daemonNodeBinDir`, `generateSystemdUnit`, `generateLaunchdPlist`).
22
+ - **Fix global npm upgrades restarting the routines daemon through `scripts/postinstall.js`.** The postinstall process is itself `process.argv[1]`, so its daemon self-heal could stamp `node scripts/postinstall.js daemon _run` into launchd. Daemon startup now accepts an explicit CLI entry and postinstall passes the resolved signed native binary (or JavaScript entrypoint), with the same value threaded through launchd, systemd, and detached startup. Source: `apps/cli/scripts/postinstall.js`, `apps/cli/src/lib/daemon.ts`.
23
+ - **Fix a standalone secrets service stealing the daemon-hosted broker socket during postinstall.** The standalone and hosted brokers now bind through one race-safe owner arbitration path: an existing reachable broker wins without its socket being unlinked, a persistent losing service stays quiescent instead of triggering launchd restart churn, takes over if the owner stops, and releases its standby PID on service shutdown; only an unreachable stale socket is reclaimed. This covers the release ordering where postinstall restarts the daemon first and then kickstarts an installed standalone service. Source: `apps/cli/src/lib/secrets/agent.ts` (`bindBrokerSocket`, `runSecretsAgent`, `startHostedBroker`).
24
+
25
+ ## 1.20.57
26
+
27
+ - **`agents teams resume` / `agents teams message` — resume a stopped teammate with a follow-up message.** A teammate that ended its turn with more to do (PR open awaiting review, headless turn cap, a redirect after the fact) could not be reached: `agents message` resolves only *live* sessions, so a completed/stopped/failed teammate had no path back short of finishing the work by hand or spawning a fresh, context-less teammate. `teams resume <team> <teammate> <message>` re-enters the teammate's **own** session with the message as the next user turn, re-launching through the same backend (local process or remote host) in its original worktree and flipping it back to `running` so `teams status` tracks it live. `teams message` is the same command with automatic routing by reconciled status: a **running** teammate is steered via its mailbox (delivered at its next tool call, no re-launch); a **stopped** one is resumed; a **pending** one is refused with a pointer to `teams start`. Works for every harness — the resume delegates to `agents run --resume`, inheriting native resume for Claude/Codex and the universal `/continue` replay for the rest (OpenCode, Grok, Kimi, …); the resume target is the teammate's captured underlying session id (`remoteSessionId ?? agentId`), and a non-Claude teammate that died before emitting a session id is refused with a clear error rather than resumed into a fresh run. This also makes good on `teams stop`'s long-standing "can be restarted later" promise, which no code implemented. Source: `apps/cli/src/commands/teams.ts` (`message`/`resume` subcommands, `decideTeamMessageRoute`), `apps/cli/src/lib/teams/agents.ts` (`AgentManager.resumeTeammate`, resume-aware `buildRunArgv`/`buildCommand`/`launchProcess`/`launchRemoteProcess`).
28
+ - **The always-on daemon now hosts the secrets broker (socket-first) — one supervised backbone instead of a separate service (#416, step 1).** `runDaemon()` binds the broker via the new `startHostedBroker()` before the scheduler and the heavy browser/session-sync services, so `agents secrets` resolves within ms of daemon start. It serves the same socket + wire protocol as the standalone broker (no `PROTOCOL_VERSION` bump — `agentGetSync`/`agentPing`/`agentAutoLoadSync` are unchanged), but is daemon-safe: no pid-guard, no `process.exit`/signal handlers/self-heal-exit (which would take the daemon down), TTL-eviction only. `ensureAgentRunning()` gains a Path 0 that prefers the daemon and falls back to the standalone `com.phnx-labs.agents-secrets-agent` launchd service, and the daemon only hosts when no broker is already reachable, so a live standalone broker is never orphaned. Retiring the standalone service (a gated `launchctl bootout` migration) and child-spawning the heavy services are the follow-on (#417). Source: `apps/cli/src/lib/secrets/agent.ts` (`startHostedBroker`, `ensureAgentRunning` Path 0, `agentPing` exported), `apps/cli/src/lib/daemon.ts` (`runDaemon` broker host + shutdown).
29
+ - **Clarified `agents secrets list` POLICY column labels.** The column previously mixed policy names, runtime state, and implementation jargon (`daily · 7d left`, `always ask`, `never · NO ACL`). It now uses a consistent `policy · state` form: `daily`, `daily · held 7d`, `always · prompt`, and `never · no prompt`. Source: `apps/cli/src/commands/secrets.ts` (`renderPolicyCol`).
30
+
5
31
  ## 1.20.56
6
32
 
7
33
  - **Fix native routine schedulers rejecting the published CLI as a Bun virtual path.** Bun's standalone runtime reports the embedded `/$bunfs/root/agents` entry as existing at `process.argv[1]`, while the real physical executable lives at `process.execPath`. Daemon resolution now substitutes that physical executable before generating launchd/systemd manifests or detached launches; the existing virtual-path guard still rejects any virtual path that reaches supervision. Source: `apps/cli/src/lib/daemon.ts`.
8
-
9
34
  - **Fix: `agents teams`, `agents message`, and `agents profiles check` work again on the signed standalone binary (regression from #315).** When `agents` resolves to the bun-compiled Mach-O (shipped since 1.20.53), three self-spawn sites relaunched the CLI as `[process.execPath, process.argv[1], …]` — but under a bun standalone executable `process.argv[1]` is the virtual entry `/$bunfs/root/agents`, so the child died with `unknown command '/$bunfs/root/agents'` (or `/bin/sh: /$bunfs/root/agents: No such file or directory`). Every teammate spawned by a compiled-binary install failed in 0s. New shared `getAgentsInvocation(subArgs)` (`apps/cli/src/lib/daemon.ts`) resolves the real on-disk binary — mapping the `/$bunfs/root/…` virtual path to `process.execPath`, running a `.js` entry under node, and a native binary directly — and `teams/agents.ts`, `commands/message.ts`, and `commands/profiles.ts` route through it. Verified end-to-end: a teammate spawned by the freshly-compiled binary runs to `completed` with no `$bunfs` error. Source: `apps/cli/src/lib/daemon.ts` (`getAgentsInvocation`), `apps/cli/src/lib/teams/agents.ts`, `apps/cli/src/commands/{message,profiles}.ts`.
10
35
  ## 1.20.55
11
36
 
package/README.md CHANGED
@@ -155,7 +155,37 @@ agents run claude "Review PRs merged this week, summarize risks" \
155
155
  | agents run codex "Write regression tests for the top 3 risks"
156
156
  ```
157
157
 
158
- Supports plan (read-only) and edit modes, effort levels, JSON output for scripting, and timeout limits.
158
+ Supports plan (read-only), edit, auto, and skip modes, effort levels, JSON output for scripting, and timeout limits.
159
+
160
+ ### What does `--mode skip` actually do?
161
+
162
+ Treat `skip` as a last-resort escape hatch. In direct-exec runs (without `--acp`),
163
+ agents-cli forwards the harness's native no-prompt flag; it does not add another
164
+ safety layer. Prefer `auto` where the harness has a smart classifier (Claude Code and
165
+ GitHub Copilot), or `edit` everywhere else. Harnesses without a native bypass flag
166
+ reject direct-exec `skip`.
167
+
168
+ | Harness | Direct-exec `--mode skip` becomes |
169
+ |---|---|
170
+ | Claude Code | `--dangerously-skip-permissions` |
171
+ | Codex | `--dangerously-bypass-approvals-and-sandbox` (equivalent to `--yolo`) |
172
+ | Gemini | `--yolo` |
173
+ | Cursor | `-f` |
174
+ | OpenClaw | `--mode full` |
175
+ | GitHub Copilot | `--allow-all` (alias: `--yolo`) |
176
+ | Antigravity | `--dangerously-skip-permissions` |
177
+ | Grok | `--always-approve` |
178
+ | Kimi | `--yolo` interactively; no extra flag in headless `-p` runs, which already auto-approve |
179
+ | Droid | `--skip-permissions-unsafe` |
180
+
181
+ With `--acp`, these native flags are not used. agents-cli instead grants `skip`
182
+ permission requests at the ACP protocol layer with `allow_always`; the same
183
+ last-resort warning applies.
184
+
185
+ Codex has no native smart-classifier mode, so `agents run codex --mode auto` resolves
186
+ to sandboxed `edit` and can still prompt. `agents run codex --mode skip` is different:
187
+ it bypasses approvals **and** removes the sandbox. `full` remains an alias for `skip`,
188
+ but new scripts should use the explicit `skip` name.
159
189
 
160
190
  ### One protocol, every harness
161
191
 
@@ -432,7 +462,7 @@ tools:
432
462
  ---
433
463
  ```
434
464
 
435
- Workflows that need to write — post PR comments, edit files, send Slack — should run with `--mode edit` or `--mode full`. `agents run` defaults to `--mode plan` (read-only), which deadlocks at `ExitPlanMode` in headless runs.
465
+ Workflows that need to write — post PR comments, edit files, send Slack — should run with `--mode edit`, or `--mode auto` on Claude Code and GitHub Copilot. Reserve `--mode skip` (legacy alias: `full`) for last-resort bypasses. `agents run` defaults to `--mode plan` (read-only), which deadlocks at `ExitPlanMode` in headless runs.
436
466
 
437
467
  Resolution is project > user > system: a `<repo>/.agents/workflows/<name>/` overrides a same-named workflow in `~/.agents/workflows/`. Commit project workflows with your repo so teammates get the same pipeline.
438
468
 
@@ -825,7 +855,7 @@ Which DotAgents resources each agent CLI can load. Source of truth: [src/lib/age
825
855
  | OpenCode | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
826
856
  | Copilot | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
827
857
  | Amp | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
828
- | Kiro | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
858
+ | Kiro | yes | no | yes | >= 2.8.0 | yes | yes | no | no | `AGENTS.md` | no |
829
859
  | Goose | yes | no | yes | no | no | no | no | no | `AGENTS.md` | no |
830
860
  | Roo Code | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
831
861
 
@@ -853,6 +883,7 @@ Which DotAgents resources each agent CLI can load. Source of truth: [src/lib/age
853
883
  |------------|-------|------|
854
884
  | Hooks | Codex | >= 0.116.0 |
855
885
  | Hooks | Gemini | >= 0.26.0 |
886
+ | Permissions | Kiro | >= 2.8.0 |
856
887
  | File-based commands | Codex | < 0.117.0 (0.117+ uses command-as-skill) |
857
888
  | Plugins | Codex | >= 0.128.0 |
858
889
 
package/dist/bin/agents CHANGED
Binary file
@@ -6,6 +6,7 @@
6
6
  import chalk from 'chalk';
7
7
  import { setHelpSections } from '../lib/help.js';
8
8
  import { listRunDefaults, setRunDefault, unsetRunDefault, } from '../lib/run-defaults.js';
9
+ import { getProjectRoot, setProjectRoot } from '../lib/project-root.js';
9
10
  function formatRunDefault(entry) {
10
11
  const parts = [];
11
12
  if (entry.defaults.mode)
@@ -86,4 +87,27 @@ export function registerDefaultsCommands(program) {
86
87
  process.exit(1);
87
88
  }
88
89
  });
90
+ defaults
91
+ .command('project-root [path]')
92
+ .description('Show or set the projects root for `agents run --project` (auto-inferred when unset)')
93
+ .action((rootPath) => {
94
+ try {
95
+ if (!rootPath) {
96
+ const current = getProjectRoot();
97
+ if (current) {
98
+ console.log(`Projects root: ${chalk.white(current)}`);
99
+ }
100
+ else {
101
+ console.log(chalk.gray('Projects root not set — auto-inferred and cached on first `--project` use.'));
102
+ }
103
+ return;
104
+ }
105
+ const stored = setProjectRoot(rootPath);
106
+ console.log(chalk.green(`Set projects root: ${stored}`));
107
+ }
108
+ catch (err) {
109
+ console.error(chalk.red(err.message));
110
+ process.exit(1);
111
+ }
112
+ });
89
113
  }
@@ -211,7 +211,8 @@ export function registerRunCommand(program) {
211
211
  .option('--no-auto-secrets', 'Skip auto-injection of secrets declared by a workflow\'s frontmatter `secrets:` field. Has no effect on bare-agent runs.')
212
212
  .option('--secrets-keys <keys>', 'Inject only this comma-separated subset of keys from --secrets bundles (e.g. KEY1,KEY2). Missing keys are an error. Applies to all --secrets bundles on this run.')
213
213
  .option('--allow-expired', 'Inject secrets even if their expiry date has passed (overrides the pre-run expiry abort).')
214
- .option('--cwd <dir>', 'Working directory for the agent (defaults to current directory)')
214
+ .option('--cwd <dir>', 'Working directory for the agent (defaults to current directory). With --host, the directory ON the host.')
215
+ .option('-P, --project <ref>', 'Project shorthand <slug>[@worktree], resolved against your projects root (auto-inferred, cached). Sets the cwd locally or on --host.')
215
216
  .option('--add-dir <dir>', 'Grant access to an additional directory outside the project (Claude and Codex, repeatable)', (val, prev) => [...prev, val], [])
216
217
  .option('--json', 'Stream events as JSON lines (for parsing by other tools)')
217
218
  .option('--quiet', 'Suppress preamble (rotation banner, "Running:" line). Useful when piping JSON events to a parser.', false)
@@ -238,7 +239,7 @@ export function registerRunCommand(program) {
238
239
  .option('--interval <dur>', 'Loop delay between iterations ("0" back-to-back, "30m" paces). Loop only.')
239
240
  .option('--host <name>', 'Offload this run onto another machine over SSH instead of running locally — a device, a registered agent host, or user@host. See `agents devices` / `agents hosts`.')
240
241
  .option('--device <name>', 'Alias of --host: offload this run onto a registered device (from `agents devices`).')
241
- .option('--remote-cwd <dir>', 'Working directory on the host for --host runs.')
242
+ .option('--remote-cwd <dir>', 'Explicit host working directory for --host runs (overrides --cwd; usually --cwd suffices).')
242
243
  .option('--no-follow', 'With --host, dispatch detached and return immediately (track via `agents hosts ps/logs`).')
243
244
  .option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match.')
244
245
  .option('--lease [backend]', 'Invent a disposable cloud box for this run and tear it down after (via crabbox). Optional backend selects the cloud (hetzner/aws/do). Unlike --host, no machine is registered.')
@@ -454,6 +455,24 @@ export function registerRunCommand(program) {
454
455
  // --host/--on/--computer: offload this run onto a registered agent host
455
456
  // over SSH instead of running locally. The three flags are aliases.
456
457
  const hostGiven = [options.host, options.device, options.on, options.computer].filter((v) => !!v);
458
+ // --project <slug>[@worktree]: resolve the projects-root shorthand into a
459
+ // cwd. On a host run it resolves home-relative (`~/…`, so the host expands
460
+ // it); locally it becomes an absolute path. It owns the working directory,
461
+ // so it is mutually exclusive with both --cwd and --remote-cwd.
462
+ if (options.project) {
463
+ if (options.cwd || options.remoteCwd) {
464
+ console.error(chalk.red('Pass --project alone — not with --cwd or --remote-cwd.'));
465
+ process.exit(1);
466
+ }
467
+ const { resolveProjectRef } = await import('../lib/project-root.js');
468
+ try {
469
+ options.cwd = await resolveProjectRef(options.project, { forRemote: hostGiven.length > 0 });
470
+ }
471
+ catch (err) {
472
+ console.error(chalk.red(err.message));
473
+ process.exit(1);
474
+ }
475
+ }
457
476
  if (hostGiven.length > 0) {
458
477
  if (new Set(hostGiven).size > 1) {
459
478
  console.error(chalk.red('Conflicting --host/--device values — pass just one.'));
@@ -488,6 +507,11 @@ export function registerRunCommand(program) {
488
507
  }
489
508
  try {
490
509
  const runAgent = agentSpec.split('@')[0];
510
+ // Working directory on the host: an explicit --remote-cwd is used
511
+ // verbatim; --cwd/--project are made portable (a local-home absolute
512
+ // becomes `~/…` so the remote shell re-roots it at ITS home).
513
+ const { toRemotePortable } = await import('../lib/project-root.js');
514
+ const hostCwd = options.remoteCwd ?? (options.cwd ? toRemotePortable(options.cwd) : undefined);
491
515
  // `--resume [id]`: commander yields the string id, or `true` when the
492
516
  // flag is passed bare. A bare resume needs the interactive picker,
493
517
  // which can't run over a detached remote dispatch — only forward a
@@ -528,7 +552,7 @@ export function registerRunCommand(program) {
528
552
  prompt,
529
553
  mode: options.mode,
530
554
  model: options.model,
531
- remoteCwd: options.remoteCwd,
555
+ remoteCwd: hostCwd,
532
556
  sessionId: hostSessionId,
533
557
  name: options.name,
534
558
  resume: resumeId,
@@ -550,7 +574,7 @@ export function registerRunCommand(program) {
550
574
  prompt,
551
575
  mode: options.mode,
552
576
  model: options.model,
553
- remoteCwd: options.remoteCwd,
577
+ remoteCwd: hostCwd,
554
578
  sessionId: hostSessionId,
555
579
  name: options.name,
556
580
  resume: resumeId,
@@ -66,8 +66,9 @@ export declare function buildSecretsExecEnv(parentEnv: NodeJS.ProcessEnv, secret
66
66
  * parse, so multi-line values are rejected rather than silently corrupted.
67
67
  */
68
68
  export declare function bundleEnvToDotenv(env: Record<string, string>): string;
69
- /** The POLICY column for `secrets list`: the prompt policy, plus a "Nh left"
70
- * hint when a `daily` bundle is currently held by the secrets-agent. `held`
69
+ /** The POLICY column for `secrets list`: the prompt policy, plus a concise
70
+ * state hint. `daily` shows `held Nh` when the secrets-agent is currently
71
+ * caching the bundle; `always` and `never` show whether they prompt. `held`
71
72
  * maps bundle name → expiry epoch-ms (from agentStatus()). */
72
73
  export declare function renderPolicyCol(b: SecretsBundle, held?: Map<string, number>): string;
73
74
  /** Register the `agents secrets` command tree. */
@@ -12,14 +12,14 @@ import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/widt
12
12
  import * as fs from 'fs';
13
13
  import { SSH_TARGET_RE, assertValidSshTarget, sshExec } from '../lib/ssh-exec.js';
14
14
  import { quoteWin32ExecArg, composeWin32CommandLine } from '../lib/platform/index.js';
15
- import { ensureDaemonStarted } from '../lib/daemon.js';
15
+ import { ensureDaemonStarted, isDaemonRunning } from '../lib/daemon.js';
16
16
  import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveSshTarget, } from '../lib/secrets/remote.js';
17
17
  import { remoteShellFor, buildWindowsStdinImportCommand } from '../lib/hosts/remote-cmd.js';
18
18
  import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
19
19
  import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
20
20
  import { getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
21
21
  import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
22
- import { DEFAULT_TTL_MS, agentLoad, agentLock, agentStatus, ensureAgentRunning, installSecretsAgentService, runAgentLoadFromStdin, runSecretsAgent, secretsAgentServiceInstalled, uninstallSecretsAgentService, } from '../lib/secrets/agent.js';
22
+ import { DEFAULT_TTL_MS, agentLoad, agentLock, agentPing, agentStatus, ensureAgentRunning, runAgentLoadFromStdin, runSecretsAgent, uninstallSecretsAgentService, } from '../lib/secrets/agent.js';
23
23
  import { parseDuration } from '../lib/hooks/cache.js';
24
24
  import { emit } from '../lib/events.js';
25
25
  import { registerCommandGroups, setHelpSections } from '../lib/help.js';
@@ -387,17 +387,18 @@ function compactRemaining(expiresAt) {
387
387
  return `${hours}h`;
388
388
  return `${Math.round(hours / 24)}d`;
389
389
  }
390
- /** The POLICY column for `secrets list`: the prompt policy, plus a "Nh left"
391
- * hint when a `daily` bundle is currently held by the secrets-agent. `held`
390
+ /** The POLICY column for `secrets list`: the prompt policy, plus a concise
391
+ * state hint. `daily` shows `held Nh` when the secrets-agent is currently
392
+ * caching the bundle; `always` and `never` show whether they prompt. `held`
392
393
  * maps bundle name → expiry epoch-ms (from agentStatus()). */
393
394
  export function renderPolicyCol(b, held) {
394
395
  // `never` is loud on purpose — it's the only tier with no user-presence gate.
395
396
  if (bundlePolicy(b) === 'never')
396
- return chalk.red.bold('never · NO ACL');
397
+ return chalk.red.bold('never · no prompt');
397
398
  if (bundlePolicy(b) === 'always')
398
- return chalk.yellow('always ask');
399
+ return chalk.yellow('always · prompt');
399
400
  const exp = held?.get(b.name);
400
- return exp ? chalk.green(`daily · ${compactRemaining(exp)} left`) : chalk.gray('daily');
401
+ return exp ? chalk.green(`daily · held ${compactRemaining(exp)}`) : chalk.gray('daily');
401
402
  }
402
403
  /** Below this width the fixed date columns no longer fit; `list` uses cards. */
403
404
  const SECRETS_WIDE = 96;
@@ -657,7 +658,7 @@ export function registerSecretsCommands(program) {
657
658
  return;
658
659
  }
659
660
  // Cross-reference the secrets-agent so `daily` bundles that are currently
660
- // held can show "· Nh left". Soft-fails to no hint if the broker is down.
661
+ // held can show "· held Nh". Soft-fails to no hint if the broker is down.
661
662
  const held = new Map();
662
663
  if (process.platform === 'darwin') {
663
664
  try {
@@ -1769,13 +1770,13 @@ Examples:
1769
1770
  ttlMs = secs * 1000;
1770
1771
  }
1771
1772
  if (!(await ensureAgentRunning())) {
1772
- console.error(chalk.red('Could not start the secrets-agent.'));
1773
+ console.error(chalk.red('Could not start the secrets broker.'));
1773
1774
  process.exit(1);
1774
1775
  }
1775
1776
  // #415: the daemon should be always-on for any background need, not only
1776
- // after `routines add`. A user who only ever unlocks secrets still gets
1777
- // the daemon installed + running here. `ensureAgentRunning` above only
1778
- // brings up the standalone secrets broker, not the daemon. Idempotent
1777
+ // after `routines add`. `ensureAgentRunning` prefers the daemon (it hosts
1778
+ // the broker, #416), but can fall back to a one-off broker spawn when the
1779
+ // daemon can't come up so ensure the daemon is up regardless. Idempotent
1779
1780
  // (single-instance start lock, #414) and best-effort — never blocks unlock.
1780
1781
  ensureDaemonStarted();
1781
1782
  let loaded = 0;
@@ -1829,13 +1830,14 @@ Examples:
1829
1830
  console.log(chalk.gray('secrets-agent is macOS-only.'));
1830
1831
  return;
1831
1832
  }
1832
- console.log(chalk.gray('service: ') +
1833
- (secretsAgentServiceInstalled()
1834
- ? chalk.green('installed (persistent)')
1835
- : chalk.yellow('not installed run `agents secrets start` for a persistent broker')));
1833
+ const brokerUp = (await agentPing()).reachable;
1834
+ console.log(chalk.gray('broker: ') +
1835
+ (brokerUp
1836
+ ? chalk.green('running') + chalk.gray(isDaemonRunning() ? ' (hosted by the daemon)' : ' (standalone)')
1837
+ : chalk.yellow('not running — starts on demand, or run `agents secrets start` to bring the daemon up now')));
1836
1838
  const entries = await agentStatus();
1837
1839
  if (entries.length === 0) {
1838
- console.log(chalk.gray('No bundles unlocked. The secrets-agent is idle or not running.'));
1840
+ console.log(chalk.gray('No bundles unlocked. The secrets broker is idle or not running.'));
1839
1841
  console.log(chalk.gray('Try: agents secrets unlock <bundle>'));
1840
1842
  return;
1841
1843
  }
@@ -1886,29 +1888,37 @@ Examples:
1886
1888
  });
1887
1889
  cmd
1888
1890
  .command('start')
1889
- .description('Install + start the secrets-agent as a persistent background service (macOS). Survives heavy load; reads connect instantly.')
1891
+ .description('Bring up the always-on daemon that hosts the secrets broker (macOS). Survives heavy load; reads connect instantly.')
1890
1892
  .action(async () => {
1891
1893
  if (process.platform !== 'darwin') {
1892
- console.error(chalk.red('secrets-agent service is macOS-only.'));
1894
+ console.error(chalk.red('The secrets broker is macOS-only.'));
1893
1895
  process.exit(1);
1894
1896
  }
1895
- process.stdout.write(chalk.gray('Installing launchd service…\n'));
1896
- if (await installSecretsAgentService()) {
1897
- console.log(chalk.green('secrets-agent service running.') + chalk.gray(' It stays up across your macOS login session; unlock/auto-cache now connect instantly.'));
1897
+ process.stdout.write(chalk.gray('Starting the daemon…\n'));
1898
+ ensureDaemonStarted();
1899
+ // The daemon hosts the broker socket-first; wait briefly for it to answer.
1900
+ const deadline = Date.now() + 10000;
1901
+ let reachable = (await agentPing()).reachable;
1902
+ while (!reachable && Date.now() < deadline) {
1903
+ await new Promise((r) => setTimeout(r, 200));
1904
+ reachable = (await agentPing()).reachable;
1905
+ }
1906
+ if (reachable) {
1907
+ console.log(chalk.green('secrets broker running.') + chalk.gray(' Hosted by the always-on daemon; unlock/auto-cache now connect instantly.'));
1898
1908
  }
1899
1909
  else {
1900
- console.error(chalk.red('Service installed but did not become reachable in time (machine may be heavily loaded — launchd will keep retrying).'));
1910
+ console.error(chalk.red('Daemon started but the broker did not become reachable in time (machine may be heavily loaded — it will keep retrying).'));
1901
1911
  process.exit(1);
1902
1912
  }
1903
1913
  });
1904
1914
  cmd
1905
1915
  .command('stop')
1906
- .description('Stop + remove the persistent secrets-agent service and wipe what it held.')
1916
+ .description('Lock all bundles and retire any legacy standalone service. The always-on daemon (which hosts the broker) is left running.')
1907
1917
  .action(async () => {
1908
1918
  if (process.platform !== 'darwin')
1909
1919
  return;
1910
1920
  await uninstallSecretsAgentService();
1911
- console.log(chalk.green('secrets-agent service stopped and removed.'));
1921
+ console.log(chalk.green('Locked all bundles.') + chalk.gray(' The broker stays hosted by the always-on daemon; a legacy standalone service, if any, was retired.'));
1912
1922
  });
1913
1923
  cmd
1914
1924
  .command('_agent-run', { hidden: true })
@@ -6,7 +6,26 @@
6
6
  * dependencies between teammates, and clean up when work is done.
7
7
  */
8
8
  import type { Command } from 'commander';
9
- import { AgentManager } from '../lib/teams/agents.js';
9
+ import { AgentManager, AgentStatus } from '../lib/teams/agents.js';
10
+ /** Where `teams message`/`teams resume` routes a follow-up, by teammate status. */
11
+ export type TeamMessageRoute = {
12
+ kind: 'steer';
13
+ } | {
14
+ kind: 'resume';
15
+ } | {
16
+ kind: 'need-message';
17
+ } | {
18
+ kind: 'not-started';
19
+ };
20
+ /**
21
+ * Decide how a follow-up to a teammate is delivered from its reconciled status.
22
+ * Pure — the source of truth for the routing table, unit-tested without I/O.
23
+ * - pending -> not-started (tell them to `teams start`)
24
+ * - running + message -> steer (mailbox)
25
+ * - stopped/etc + message -> resume (re-enter session)
26
+ * - any actionable + no message -> need-message
27
+ */
28
+ export declare function decideTeamMessageRoute(status: AgentStatus, hasMessage: boolean): TeamMessageRoute;
10
29
  /**
11
30
  * Register the generic cloud dispatcher — staged cloud teammates get
12
31
  * dispatched when their --after deps resolve, using repo/branch stored on
@@ -3,7 +3,8 @@ import { die, relTime, truncate, isJsonMode, padRight } from '../lib/format.js';
3
3
  import * as fs from 'fs/promises';
4
4
  import { addHostOption } from '../lib/hosts/option.js';
5
5
  import * as path from 'path';
6
- import { AgentManager, checkCliSignedIn, collectTeamsDoctorData, getAgentsDir, VALID_TASK_TYPES, } from '../lib/teams/agents.js';
6
+ import { AgentManager, AgentStatus, checkCliSignedIn, collectTeamsDoctorData, getAgentsDir, VALID_TASK_TYPES, } from '../lib/teams/agents.js';
7
+ import { mailboxDir, enqueue } from '../lib/mailbox.js';
7
8
  import { resolveProvider } from '../lib/cloud/registry.js';
8
9
  import { emit } from '../lib/events.js';
9
10
  import { runSupervisor } from '../lib/teams/supervisor.js';
@@ -126,6 +127,23 @@ function parseTeammate(spec) {
126
127
  function shortId(id) {
127
128
  return id.slice(0, 8);
128
129
  }
130
+ /**
131
+ * Decide how a follow-up to a teammate is delivered from its reconciled status.
132
+ * Pure — the source of truth for the routing table, unit-tested without I/O.
133
+ * - pending -> not-started (tell them to `teams start`)
134
+ * - running + message -> steer (mailbox)
135
+ * - stopped/etc + message -> resume (re-enter session)
136
+ * - any actionable + no message -> need-message
137
+ */
138
+ export function decideTeamMessageRoute(status, hasMessage) {
139
+ if (status === AgentStatus.PENDING)
140
+ return { kind: 'not-started' };
141
+ if (!hasMessage)
142
+ return { kind: 'need-message' };
143
+ if (status === AgentStatus.RUNNING)
144
+ return { kind: 'steer' };
145
+ return { kind: 'resume' };
146
+ }
129
147
  /**
130
148
  * Preamble injected into every factory worker's prompt. Tells the worker
131
149
  * which team + teammate name + task-type it is, and how to file new tasks.
@@ -816,6 +834,12 @@ export function registerTeamsCommands(program) {
816
834
  # Delta-poll status without rereading everything
817
835
  agents teams status pricing-page --since 2026-04-24T09:00:00-07:00
818
836
 
837
+ # Nudge a teammate that stopped with more to do — resumes its own session
838
+ agents teams resume pricing-page backend "Review's in — rebase-merge the PR, then release"
839
+
840
+ # Steer a still-running teammate mid-flight (delivered at its next tool call)
841
+ agents teams message pricing-page qa "Skip the flaky screenshot test for now"
842
+
819
843
  # Wind everyone down when shipped
820
844
  agents teams disband pricing-page
821
845
  `,
@@ -1646,7 +1670,7 @@ export function registerTeamsCommands(program) {
1646
1670
  });
1647
1671
  // stop
1648
1672
  addHostOption(teams.command('stop [team] [teammate]'))
1649
- .description('Stop a running teammate. Can be restarted later. Cleans up worktree if no uncommitted changes.')
1673
+ .description('Stop a running teammate. Resume it later with `agents teams resume`. Cleans up worktree if no uncommitted changes.')
1650
1674
  .option('--json', 'Output machine-readable JSON')
1651
1675
  .action(async (team, ref, opts) => {
1652
1676
  const mgr = mkManager();
@@ -1733,6 +1757,85 @@ export function registerTeamsCommands(program) {
1733
1757
  console.log(chalk.yellow(`Worktree '${agent.worktreeName}' has uncommitted changes. Keeping it at: ${agent.worktreePath}`));
1734
1758
  }
1735
1759
  });
1760
+ // message / resume — send a follow-up message to a teammate. Routes by the
1761
+ // teammate's reconciled status: a RUNNING teammate is STEERED via its mailbox
1762
+ // (delivered at its next tool call); a STOPPED one (completed/failed/stopped)
1763
+ // is RESUMED — re-entering its own session with the message as the next user
1764
+ // turn, re-attaching it to the team as live.
1765
+ async function teamMessageAction(team, ref, message, opts) {
1766
+ const mgr = mkManager();
1767
+ const lookup = await mgr.resolveAgentIdInTask(team, ref);
1768
+ if (lookup.kind === 'none')
1769
+ die(`No teammate matching '${ref}' in team ${team}`, 2);
1770
+ if (lookup.kind === 'ambiguous') {
1771
+ const shorts = lookup.matches.map(shortId).join(', ');
1772
+ die(`'${ref}' matches multiple teammates: ${shorts}. Use more characters or a name.`, 2);
1773
+ }
1774
+ const agentId = lookup.agentId;
1775
+ // mgr.get reconciles the teammate's status (PID + start-time guard / remote
1776
+ // .exit sentinel / exit-code reap) before we branch — so running-vs-stopped
1777
+ // is a fact, not a guess.
1778
+ const agent = await mgr.get(agentId);
1779
+ if (!agent)
1780
+ die(`Teammate ${shortId(agentId)} vanished from team ${team}.`);
1781
+ const display = agent.name || shortId(agentId);
1782
+ const status = agent.status;
1783
+ const hasMessage = message != null && message.trim().length > 0;
1784
+ const route = decideTeamMessageRoute(status, hasMessage);
1785
+ switch (route.kind) {
1786
+ case 'not-started':
1787
+ die(`Teammate '${display}' hasn't started yet (waiting on --after deps). Run \`agents teams start ${team}\` to launch it.`);
1788
+ return;
1789
+ case 'need-message':
1790
+ if (status === AgentStatus.RUNNING) {
1791
+ die(`Teammate '${display}' is running — pass a message to steer it.`);
1792
+ }
1793
+ die(`Teammate '${display}' is ${status} — pass a message to resume it: \`agents teams resume ${team} ${display} "<message>"\`.`);
1794
+ return;
1795
+ case 'steer': {
1796
+ // Running -> steer via mailbox; never re-launch (that forks a 2nd session).
1797
+ enqueue(mailboxDir(agentId), { to: agentId, text: message, from: opts.from });
1798
+ if (isJsonMode(opts)) {
1799
+ console.log(JSON.stringify({ team, agent_id: agentId, name: agent.name ?? null, action: 'steer', status }, null, 2));
1800
+ return;
1801
+ }
1802
+ console.log(chalk.green(`Steering ${chalk.cyan(display)} (running) — `) +
1803
+ chalk.dim('message queued; it will see it at its next tool call.'));
1804
+ return;
1805
+ }
1806
+ case 'resume': {
1807
+ // Stopped / completed / failed -> resume its own session with the message.
1808
+ try {
1809
+ await mgr.resumeTeammate(agentId, message);
1810
+ }
1811
+ catch (err) {
1812
+ die(err.message);
1813
+ }
1814
+ if (isJsonMode(opts)) {
1815
+ console.log(JSON.stringify({ team, agent_id: agentId, name: agent.name ?? null, action: 'resume', prior_status: status }, null, 2));
1816
+ return;
1817
+ }
1818
+ console.log(chalk.green(`Resuming ${chalk.cyan(display)} `) +
1819
+ chalk.dim(`(was ${status}) in team ${team} — re-entering its session with your message.`));
1820
+ console.log(chalk.dim(`Track it with \`agents teams status ${team}\`.`));
1821
+ return;
1822
+ }
1823
+ }
1824
+ }
1825
+ addHostOption(teams.command('message <team> <teammate> <message>'))
1826
+ .description('Send a follow-up message to a teammate. A running teammate is steered via its mailbox; a stopped one is resumed — re-entering its own session with the message.')
1827
+ .option('--from <who>', 'Label recorded as the sender of this message')
1828
+ .option('--json', 'Output machine-readable JSON')
1829
+ .action(async (team, ref, message, opts) => {
1830
+ await teamMessageAction(team, ref, message, opts);
1831
+ });
1832
+ addHostOption(teams.command('resume <team> <teammate> [message]'))
1833
+ .description("Resume a stopped teammate (completed/failed/stopped) by re-entering its own session with a message as the next user turn. If the teammate is still running, the message is steered via its mailbox instead.")
1834
+ .option('--from <who>', 'Label recorded as the sender of this message')
1835
+ .option('--json', 'Output machine-readable JSON')
1836
+ .action(async (team, ref, message, opts) => {
1837
+ await teamMessageAction(team, ref, message, opts);
1838
+ });
1736
1839
  // remove
1737
1840
  teams
1738
1841
  .command('remove [team] [teammate]')
@@ -4,7 +4,7 @@ import ora from 'ora';
4
4
  import * as fs from 'fs';
5
5
  import * as path from 'path';
6
6
  import { select, confirm, checkbox } from '@inquirer/prompts';
7
- import { AGENTS, ALL_AGENT_IDS, getAccountEmail, getAccountInfo, agentLabel, warnAgentDeprecated, } from '../lib/agents.js';
7
+ import { AGENTS, ALL_AGENT_IDS, getAccountEmail, getAccountInfo, agentLabel, warnAgentDeprecated, isSelfUpdatingAgent, } from '../lib/agents.js';
8
8
  import { formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
9
9
  import { viewAction } from './view.js';
10
10
  import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
@@ -67,7 +67,7 @@ async function setDefaultVersion(agent, installedVersion) {
67
67
  createVersionedAlias(agent, installedVersion);
68
68
  const symlinkResult = await switchConfigSymlink(agent, installedVersion);
69
69
  if (symlinkResult.success) {
70
- console.log(chalk.green(' Set as default'));
70
+ console.log(chalk.green(isSelfUpdatingAgent(agent) ? ' Set as active config profile' : ' Set as default'));
71
71
  if (symlinkResult.backupPath) {
72
72
  console.log(chalk.gray(` Backed up existing config to: ${symlinkResult.backupPath}`));
73
73
  }
@@ -752,7 +752,15 @@ export function registerVersionsCommands(program) {
752
752
  warnIfShimShadowed(agentId);
753
753
  const useEmail = await getAccountEmail(agentId, getVersionHomePath(agentId, finalVersion));
754
754
  const useEmailStr = useEmail ? chalk.cyan(` (${useEmail})`) : '';
755
- console.log(chalk.green(`Set ${agentLabel(agentConfig.id)}@${finalVersion} as global default`) + useEmailStr);
755
+ // Self-updating agents are one binary; `use` only swaps the config
756
+ // symlink (the profile), it does not change which binary runs — so say
757
+ // "profile", not "version", to match what actually happened.
758
+ if (isSelfUpdatingAgent(agentId)) {
759
+ console.log(chalk.green(`Switched ${agentLabel(agentConfig.id)} to config profile ${finalVersion}`) + useEmailStr);
760
+ }
761
+ else {
762
+ console.log(chalk.green(`Set ${agentLabel(agentConfig.id)}@${finalVersion} as global default`) + useEmailStr);
763
+ }
756
764
  }
757
765
  }
758
766
  catch (err) {