@phnx-labs/agents-cli 1.22.14 → 1.22.16

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 (76) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +7 -5
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/cloud.js +4 -6
  5. package/dist/commands/exec.js +96 -11
  6. package/dist/commands/humans.d.ts +10 -0
  7. package/dist/commands/humans.js +91 -0
  8. package/dist/commands/packages.js +90 -22
  9. package/dist/commands/projects.d.ts +2 -5
  10. package/dist/commands/projects.js +92 -65
  11. package/dist/commands/resume.d.ts +17 -0
  12. package/dist/commands/resume.js +80 -0
  13. package/dist/commands/routines.js +53 -71
  14. package/dist/commands/sessions.d.ts +13 -4
  15. package/dist/commands/sessions.js +37 -19
  16. package/dist/commands/setup-watchdog.d.ts +3 -0
  17. package/dist/commands/setup-watchdog.js +54 -0
  18. package/dist/commands/setup.js +8 -1
  19. package/dist/commands/ssh.d.ts +11 -0
  20. package/dist/commands/ssh.js +18 -4
  21. package/dist/commands/watchdog.d.ts +3 -4
  22. package/dist/commands/watchdog.js +40 -28
  23. package/dist/index.js +3 -2
  24. package/dist/lib/auto-dispatch.d.ts +8 -10
  25. package/dist/lib/auto-dispatch.js +20 -42
  26. package/dist/lib/channels/send.d.ts +4 -1
  27. package/dist/lib/channels/send.js +15 -7
  28. package/dist/lib/devices/doctor-findings.d.ts +14 -0
  29. package/dist/lib/devices/doctor-findings.js +92 -51
  30. package/dist/lib/exec.d.ts +6 -2
  31. package/dist/lib/exec.js +22 -4
  32. package/dist/lib/hooks.d.ts +4 -2
  33. package/dist/lib/hooks.js +173 -23
  34. package/dist/lib/humans.d.ts +25 -0
  35. package/dist/lib/humans.js +65 -0
  36. package/dist/lib/memory.js +2 -1
  37. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  38. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  39. package/dist/lib/migrate.d.ts +1 -1
  40. package/dist/lib/migrate.js +116 -7
  41. package/dist/lib/notify.js +6 -2
  42. package/dist/lib/permissions.js +12 -10
  43. package/dist/lib/project-import.d.ts +7 -46
  44. package/dist/lib/project-import.js +9 -108
  45. package/dist/lib/projects.d.ts +20 -3
  46. package/dist/lib/projects.js +36 -18
  47. package/dist/lib/registry.d.ts +1 -1
  48. package/dist/lib/registry.js +11 -0
  49. package/dist/lib/routine-activation.d.ts +14 -0
  50. package/dist/lib/routine-activation.js +78 -0
  51. package/dist/lib/routines-project.d.ts +1 -1
  52. package/dist/lib/routines-project.js +3 -9
  53. package/dist/lib/routines.d.ts +3 -1
  54. package/dist/lib/routines.js +32 -19
  55. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  56. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  57. package/dist/lib/secrets/bundles.d.ts +11 -0
  58. package/dist/lib/secrets/bundles.js +122 -5
  59. package/dist/lib/session/actor-sidecar.d.ts +5 -2
  60. package/dist/lib/session/actor-sidecar.js +4 -2
  61. package/dist/lib/session/db.d.ts +2 -1
  62. package/dist/lib/session/db.js +19 -3
  63. package/dist/lib/session/types.d.ts +4 -0
  64. package/dist/lib/staleness/writers/sources.d.ts +6 -1
  65. package/dist/lib/staleness/writers/sources.js +93 -7
  66. package/dist/lib/startup/command-registry.d.ts +2 -0
  67. package/dist/lib/startup/command-registry.js +5 -0
  68. package/dist/lib/state.d.ts +2 -0
  69. package/dist/lib/state.js +23 -10
  70. package/dist/lib/types.d.ts +64 -1
  71. package/dist/lib/versions.js +48 -14
  72. package/dist/lib/watchdog/rotate.d.ts +1 -1
  73. package/dist/lib/watchdog/rotate.js +1 -1
  74. package/package.json +1 -1
  75. package/dist/lib/watchdog/routine.d.ts +0 -44
  76. package/dist/lib/watchdog/routine.js +0 -69
package/CHANGELOG.md CHANGED
@@ -1,5 +1,58 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.16
4
+
5
+ - **Resume exact sessions locally or across the fleet with `agents resume <id>` and `agents run <agent|auto> --resume <id>`.** Full IDs use the local SQLite index before any SSH fan-out; remote owners route to the recorded device and version home. Session metadata now records launch mode alongside harness, version, account, cwd, and machine so strict resume reconstructs the original run. Claude, Codex, Grok, Kimi, Droid, and Cursor use their verified version-specific native resume syntax; `run auto --resume` can select another healthy harness/account and continue through `/continue` when native resume is unavailable. Source: `apps/cli/src/commands/{exec,resume,sessions}.ts`, `apps/cli/src/lib/{exec,session/db}.ts`, `packages/session-tracker/src/hook.sh`.
6
+
7
+ - **Hooks: one-level event dirs (`hooks/<event-name>/<script>`) are first-class.**
8
+ System hooks organize by harness event (`session-start/`, `pre-tool-use/`, …).
9
+ Install names stay the file basename. Dirs with top-level scripts expand into
10
+ individual hooks; fixture-only dirs remain directory bundles. Manifest `script:`
11
+ may be a relative path under `hooks/`. Source: `apps/cli/src/lib/hooks.ts`,
12
+ `apps/cli/src/lib/staleness/writers/sources.ts`, `apps/cli/src/lib/versions.ts`,
13
+ `apps/cli/src/lib/__tests__/hooks-nested-groups.test.ts`.
14
+
15
+ - **`agents humans show owner [--json]`** — new command to display the owner config from `~/.agents/humans.yaml`. The file is written automatically on first run when `notify.owner` exists in `agents.yaml`. Source: `apps/cli/src/lib/humans.ts`, `apps/cli/src/commands/humans.ts`.
16
+
17
+ - **`humans.yaml` — typed, versioned owner config.** `~/.agents/humans.yaml` (`version: 1`) now stores owner identity (name, timezone, quiet hours, severity), notification channels, and escalation policy. `notify.owner` in `agents.yaml` is migrated into it on first run and the `notify.owner` key is removed from `agents.yaml`; unrelated keys are preserved. `agents send --to owner` / `agents notify` prefer `humans.yaml` with a fallback to `agents.yaml` during the migration window. Source: `apps/cli/src/lib/humans.ts`, `apps/cli/src/commands/humans.ts`, `apps/cli/src/lib/migrate.ts`.
18
+
19
+ - **`agents memory` ignores `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, and `MEMORY.md`.** These rule/index files lived in `~/.agents/memory/` but were incorrectly surfaced as memory facts. `isFactFile()` now excludes them by name (case-insensitive). Source: `apps/cli/src/lib/memory.ts`.
20
+
21
+ - **Permissions write path fixed — `groups/` subdirectory.** `installPermissionSet`, `removePermissionSet`, and `savePermissionSet` now all write to the `groups/` subdirectory (matching `discoverPermissionGroups()` which already reads from `groups/`). Source: `apps/cli/src/lib/permissions.ts`.
22
+
23
+ - **Stop eagerly creating webhooks directories.** `ensureAgentsDir()` no longer creates `~/.agents/webhooks/` or `~/.agents/.system/webhooks/` on startup — both dirs are created on first actual use. Source: `apps/cli/src/lib/state.ts`.
24
+
25
+ - **Terminals canonically under `.cache/`.** The stale migration comment that blocked `terminals/` from moving to `~/.agents/.cache/terminals/` is replaced by the actual move. Factory already writes to `.cache/terminals/` (`foreman.registry.ts:9`), so no app-level change is needed. Source: `apps/cli/src/lib/migrate.ts`.
26
+
27
+ - **Menu bar warns when a device is under high load (local or remote).** The
28
+ agents-cli menu bar now shows a `⚠ <device> — high load N%` row in NEEDS YOU when
29
+ a machine's load or memory crosses the `headroom()` "loaded" threshold (≥75%), and
30
+ a red `✕` when critical. The local machine is probed natively via `getloadavg`
31
+ (zero subprocess); fleet peers come from the daemon-warmed `.fleet-stats.json`
32
+ cache with a freshness guard — never the slow `agents doctor` path. Action-required
33
+ rows are now emphasized so items that need you stand out. Source:
34
+ `apps/cli/menubar/Sources/MenubarHelper/LocalState.swift`,
35
+ `apps/cli/menubar/Sources/MenubarHelper/StatusItemController.swift`.
36
+
37
+ - **Projects canonicalization contract.** `agents projects import --from-factory` / `--min-confidence` / `--all` are gone; import is `--from-linear` only, and `~/.agents/factory/projects.json` is never read or migrated. `agents projects list --json` returns definitions only (zero session scan / SSH); `--with-agents` is an explicit opt-in for local active counts. New `agents projects save --json` reads one complete `ProjectDef` from stdin, validates, writes atomically under `~/.agents/projects/`, and prints the saved def. `agents projects rm <name> --json` returns machine-readable success/error. Factory's `managedProjects.ts` shells only through `agents projects list|save|rm` — never reads or writes project YAML/JSON directly, never seeds or migrates legacy Factory state; errors stay explicit for inline UI display. Source: `apps/cli/src/commands/projects.ts`, `apps/cli/src/lib/projects.ts`, `apps/factory/src/core/managedProjects.ts`, `apps/cli/docs/11-projects.md`.
38
+
39
+ - **`ProjectDef` YAML gains `dispatch` block and `linear.name`.** `~/.agents/projects/<name>.yaml` now accepts a `dispatch:` block (`enabled`, `maxAgents`, `provider`, `host`) that opts a project into auto-dispatch and is read directly by `agents __auto-dispatch` — previously these fields lived only in Factory's own registry. `linear.name` stores the Linear project display name alongside the existing `projectId` and `url`. Both fields are optional; existing YAMLs are unchanged. Source: `apps/cli/src/lib/projects.ts`, `apps/cli/src/lib/auto-dispatch.ts`.
40
+
41
+ - **`agents secrets` no longer pops a generic "Agents CLI needs to authenticate" Touch ID sheet on every agent launch.** `listBundles` — which runs on essentially every secrets touch (session-title generation, `agents devices list`, every `agents run`, every remote launch that resolves secrets on the host) — could not ask the keychain for just the bundle *metadata* items: with hashed service names (#316) those names are opaque, so it fell back to a **broad `agents-cli.` keychain scan** that also matched the ACL'd secret *value* items. On machines where a bundle carries a biometric ACL (e.g. a `hold`-tier bundle holding an SSN or a password), macOS evaluated that value ACL during the attributes-only scan and raised a **generic, context-less** Touch ID prompt — on every launch, so a busy fleet felt like a machine-wide bombardment. Neither `kSecUseAuthenticationUIFail` nor `LAContext.interactionNotAllowed` can list the no-ACL items while skipping the ACL'd ones (both return nothing), so the fix is to stop doing the broad scan: a per-machine **no-ACL metadata-name index** (opaque hashes only, in the regenerable helpers dir — it leaks nothing #316 didn't) is read as a silent file instead. The write paths keep it current; an absent/stale index self-heals by rebuilding from the one-time scan, and a missing entry only makes `secrets list` cosmetically incomplete — it never affects a resolve-by-name. Your sensitive bundles keep their biometric gate on real value reads; only the bundle *listing* goes silent. Source: `apps/cli/src/lib/secrets/bundles.ts`.
42
+
43
+ ## 1.22.15
44
+
45
+ - **Separate routine definitions from device activation (#2023).** Enable a routine by listing its name in `~/.agents/devices/<hostname>/agents.yaml`; built-in Watchdog setup and `watchdog on|off` now update that host-owned manifest without rewriting the routine definition. Source: `apps/cli/src/lib/routine-activation.ts`, `apps/cli/src/commands/setup-watchdog.ts`.
46
+
47
+ - **`agents devices list` no longer shows the "Leased boxes" section by default — it moves behind a new `--all` flag (RUSH-2190).** Loading the section routes through crabbox's bundle auto-detect, which scans the keychain and can raise a macOS Touch ID sheet *after* the device table has printed, hanging non-interactive callers (observed: the `.agents-system` SessionStart topology hook). The default list now renders only registered devices, which are reachable without any secrets; the load/mem/headroom columns are unchanged (the stats probe was already broker-only). `agents devices list --all` restores the section; `--no-stats` remains a hard "instant, no provider calls" opt-out even with `--all`. Source: `apps/cli/src/commands/ssh.ts` (`showLeasedBoxesSection`), `apps/cli/src/commands/ssh.test.ts`.
48
+
49
+ - **Install: `plugin:` prefix on the unified path (Phase 5 packaging).**
50
+ `agents install plugin:<spec>` uses the same grammar and trust gate as
51
+ `agents plugins install` (`name@url`, local path, `--allow-exec-surfaces`).
52
+ Specialized verbs still work; `agents install` is the one add path for mcp,
53
+ skill, plugin, and GitHub sources. Source: `apps/cli/src/commands/packages.ts`,
54
+ `apps/cli/src/lib/registry.ts`.
55
+
3
56
  ## 1.22.14
4
57
 
5
58
  - **`agents secrets view <bundle> --reveal` now resolves a locked keychain bundle
package/README.md CHANGED
@@ -341,9 +341,11 @@ agents sessions resume # multi-select; packs two sessions pe
341
341
  agents sessions resume "auth middleware" # pre-filter the pool, then choose
342
342
  agents sessions resume --tmux # into persistent tmux — survives editor restarts
343
343
  agents sessions resume --host zion --tmux # resume on another machine over SSH
344
+ agents resume 019fd0c8-b3e9-77a2-a1a4-444698c4d897 # original harness/version/device/mode
345
+ agents run auto --resume 019fd0c8-b3e9-77a2-a1a4-444698c4d897 # adapt if its account is unavailable
344
346
  ```
345
347
 
346
- `agents sessions resume` reopens sessions in whatever terminal you're in -- auto-detected across iTerm, Ghostty, tmux, and the VSCodium agent-terminal, or forced with `--iterm` / `--ghostty` / `--tmux` / `--vscodium`. Back them with **tmux** and the runs turn durable: detach, close your editor, reboot the GUI -- the session is still alive to `agents tmux attach`. The whole `agents tmux` subsystem (persistent multiplexer sessions that survive editor restarts and can be shared with other tools) sits underneath.
348
+ `agents sessions resume` reopens several sessions in whatever terminal you're in -- auto-detected across iTerm, Ghostty, tmux, and the VSCodium agent-terminal, or forced with `--iterm` / `--ghostty` / `--tmux` / `--vscodium`. `agents resume <id>` resumes one session without requiring you to name its harness: exact IDs take a local SQLite fast path, then resolve fleet-wide and restore the source harness, version, device, cwd, and recorded launch mode. Back them with **tmux** and the runs turn durable: detach, close your editor, reboot the GUI -- the session is still alive to `agents tmux attach`. The whole `agents tmux` subsystem (persistent multiplexer sessions that survive editor restarts and can be shared with other tools) sits underneath.
347
349
 
348
350
  ### Send an agent to the background — and bring it back
349
351
 
@@ -396,7 +398,7 @@ agents watchdog --nudge # actually inject "Continue." into the stalled split
396
398
  agents watchdog --watch # daemon loop: a tick every --interval
397
399
  ```
398
400
 
399
- `agents watchdog` detects a stalled session, resolves the *exact* terminal split it lives in (tmux, iTerm, VSCodium, or a raw pty), and injects a nudge -- `Continue.` by default, or set `--text`. It's dry by default; `--nudge` acts on a single tick, and `agents watchdog enable` flips global auto-nudge on so `--watch` injects on its own. Steer a single run with `agents watchdog policy <id> off | keep | handsoff`.
401
+ `agents watchdog` detects a stalled session, resolves the *exact* terminal split it lives in (tmux, iTerm, VSCodium, or a raw pty), and injects a nudge -- `Continue.` by default, or set `--text`. It's dry by default; `--nudge` acts on a single tick, and `agents watchdog on|off` enables or disables the built-in routine on this device. `agents setup watchdog` chooses devices. Steer a single run with `agents watchdog policy <id> off | keep | handsoff`.
400
402
 
401
403
  A stalled session whose tail shows a hard account limit ("You've hit your weekly limit · resets …") is **rotated in place** instead of nudged: the watchdog gates on the same healthy-account selection `agents run auto` makes (zero healthy → one skip event per cooldown window, terminal untouched), injects the harness's exit sequence, relaunches `agents run auto --interactive --session-id <uuid>` in the *same* tab, then replays the old session's resume once the new TUI is live. Default on; `agents watchdog rotate off` disables it (nudging stays on).
402
404
 
@@ -921,11 +923,11 @@ agents routines run daily-digest # Test it now, ignore the schedule
921
923
  agents routines logs daily-digest # Last execution — status + report (add --full for raw stdout)
922
924
  agents routines stats # Run count, failed, missed, avg/p50/p95 duration — per job or all
923
925
 
924
- # Routines sync to every device; restrict to an allowlist with --devices
926
+ # Definitions sync to every device; activation is stored per hostname
925
927
  agents routines add nightly-drain --schedule "0 3 * * *" --agent claude \
926
- --devices yosemite-s0,mac-mini --prompt "Drain the local work queue"
928
+ --prompt "Drain the local work queue"
927
929
 
928
- agents routines devices nightly-drain --set yosemite-s0,mac-mini # update allowlist
930
+ agents routines devices nightly-drain --set yosemite-s0,mac-mini # enable on both hosts
929
931
  agents routines list --host yosemite-s0 # query another device
930
932
 
931
933
  # Signed webhook trigger: Linear issue labeled "agent" fires a routine
package/dist/bin/agents CHANGED
Binary file
@@ -7,8 +7,7 @@ import { resolveProvider, getAllProviders, getDefaultProviderId } from '../lib/c
7
7
  import { insertTask, updateTaskStatus, getTaskById, listTasks as listStoredTasks, listActiveTasks } from '../lib/cloud/store.js';
8
8
  import { renderStream } from '../lib/cloud/stream.js';
9
9
  import { MissingTargetError, MAX_IMAGES_PER_DISPATCH } from '../lib/cloud/types.js';
10
- import { normalizeTriggerEvent, validateTrigger, writeJob, jobExists, GITHUB_TRIGGER_EVENTS } from '../lib/routines.js';
11
- import { machineId } from '../lib/machine-id.js';
10
+ import { normalizeTriggerEvent, validateTrigger, writeJob, setJobEnabled, jobExists, GITHUB_TRIGGER_EVENTS } from '../lib/routines.js';
12
11
  import { emit } from '../lib/events.js';
13
12
  import { shareRuntimeEnv } from '../lib/share/config.js';
14
13
  /** Map a supported image file extension to its wire mimeType. Rejects anything else. */
@@ -310,16 +309,15 @@ Examples:
310
309
  };
311
310
  if (repoValues[0])
312
311
  routine.repo = repoValues[0];
313
- // --host with --on: the webhook-fired run places on that machine (the
314
- // routine carries the placement, and firing pins to THIS device so a
315
- // fleet of receivers can't each dispatch a duplicate).
312
+ // --host controls where the enabled local receiver dispatches the run.
313
+ // Device activation remains in this receiver's device manifest.
316
314
  if (options.host) {
317
315
  routine.host = options.host;
318
316
  if (options.remoteCwd)
319
317
  routine.remoteCwd = options.remoteCwd;
320
- routine.devices = [machineId()];
321
318
  }
322
319
  writeJob(routine);
320
+ setJobEnabled(routine.name, true);
323
321
  if (json) {
324
322
  console.log(JSON.stringify({ ok: true, registered: routineName, trigger }, null, 2));
325
323
  }
@@ -532,7 +532,7 @@ export function registerRunCommand(program) {
532
532
  .option('--headless', 'Force headless mode. Auto-enabled when a prompt is provided; pass explicitly to stay headless with no prompt (reads the prompt from stdin).', false)
533
533
  .option('--no-auth-check', 'Skip the pre-launch "looks logged out" warning on an interactive run (advisory; never blocks anyway). Also silenced by AGENTS_NO_AUTH_CHECK=1.')
534
534
  .option('-i, --interactive', 'Force interactive mode even when a prompt is provided. Mutually exclusive with --headless.')
535
- .option('--resume [id]', 'Resume a previous conversation. Accepts a full or partial session id (prefix-matched against the index); omit the id to pick from recent sessions interactively. Resumes under the version that started the session. claude/codex resume natively; other agents replay via a /continue first message. Pair with a prompt to continue headlessly.')
535
+ .option('--resume [id]', 'Resume a previous conversation. Full IDs resolve locally first, then fleet-wide. Claude, Codex, Grok, Kimi, Droid, and Cursor use version-gated native resume; other agents replay via /continue. Pair with a prompt to continue headlessly.')
536
536
  .option('--session-id <id>', 'Force a NEW conversation to use this exact session UUID (Claude only). This CREATES a session — to resume an existing one, use --resume.')
537
537
  .option('--name <slug>', 'Name the run — seeds the session label so it shows up as `<name>` in `agents sessions` and resolves by it (and `agents hosts logs <name>` for --host runs) instead of an opaque id. An agent-generated title later refines the label; your name shows until then. Optional.')
538
538
  .option('--notify', 'Post a desktop notification when a headless run finishes. Fired by this process on exit, so it survives whatever launched the run (the menu bar dispatching it, a terminal you closed).')
@@ -668,7 +668,7 @@ export function registerRunCommand(program) {
668
668
 
669
669
  Fallback: --fallback codex,antigravity retries on rate-limit failure via /continue handoff. Each entry accepts @version.
670
670
 
671
- Resume: --resume <id> continues a prior conversation (full or partial id; omit to pick interactively). claude/codex resume natively; others replay via a /continue first message. Add a prompt to continue headlessly.
671
+ Resume: --resume <id> resolves full IDs locally first, then fleet-wide, and restores the source version/device/mode. Claude, Codex, Grok, Kimi, Droid, and Cursor use version-gated native resume; others replay via /continue. agents resume <id> infers the harness too.
672
672
 
673
673
  Passthrough: everything after -- is forwarded verbatim to the underlying agent CLI.
674
674
  agents run kimi -- --plan --some-native-flag value
@@ -781,7 +781,89 @@ export function registerRunCommand(program) {
781
781
  `Pin a concrete harness instead: agents run <harness>@<version>.`));
782
782
  process.exit(1);
783
783
  }
784
- const autoHarnessRequested = normalizedAgentSpec === RUN_AUTO_KEYWORD;
784
+ let autoHarnessRequested = normalizedAgentSpec === RUN_AUTO_KEYWORD;
785
+ let resolvedResumeSource;
786
+ // Concrete resume ids resolve BEFORE placement. Full UUIDs take the local
787
+ // SQLite fast path; only a local miss fans out to the fleet. This lets a
788
+ // command entered on zion discover that the owning version-home is on a
789
+ // worker, while a command entered on that worker never pays for SSH.
790
+ if (typeof options.resume === 'string' && options.resume.trim()) {
791
+ const selector = options.resume.trim();
792
+ const injectedSource = (() => {
793
+ try {
794
+ const parsed = JSON.parse(process.env.AGENTS_RESUME_SOURCE_JSON ?? 'null');
795
+ return parsed?.id === selector ? parsed : undefined;
796
+ }
797
+ catch {
798
+ return undefined;
799
+ }
800
+ })();
801
+ delete process.env.AGENTS_RESUME_SOURCE_JSON;
802
+ const outcome = injectedSource
803
+ ? { kind: 'resolved', session: injectedSource }
804
+ : await (await import('./sessions.js')).resolveSessionMetadataValue(selector);
805
+ if (outcome.kind === 'partial') {
806
+ console.error(chalk.red(`Could not resolve session while these devices were unavailable: ${outcome.failedPeers.join(', ')}`));
807
+ process.exit(2);
808
+ }
809
+ if (outcome.kind === 'not-found') {
810
+ console.error(chalk.red(`No session matching "${selector}".`));
811
+ process.exit(1);
812
+ }
813
+ if (outcome.kind === 'ambiguous') {
814
+ console.error(chalk.red(`"${selector}" matches ${outcome.candidates.length} sessions. Pass the full session id.`));
815
+ process.exit(1);
816
+ }
817
+ resolvedResumeSource = outcome.session;
818
+ const [requestedAgent, requestedVersion] = normalizedAgentSpec.split('@');
819
+ if (!autoHarnessRequested && requestedAgent !== resolvedResumeSource.agent) {
820
+ console.error(chalk.red(`Session ${resolvedResumeSource.shortId} belongs to ${resolvedResumeSource.agent}, not ${requestedAgent}. ` +
821
+ `Use: agents resume ${resolvedResumeSource.id}`));
822
+ process.exit(1);
823
+ }
824
+ if (!autoHarnessRequested && requestedVersion && requestedVersion !== resolvedResumeSource.version) {
825
+ console.error(chalk.red(`Session ${resolvedResumeSource.shortId} started with ${resolvedResumeSource.agent}@${resolvedResumeSource.version ?? 'unknown'}, ` +
826
+ `not @${requestedVersion}.`));
827
+ process.exit(1);
828
+ }
829
+ // Omitted --mode inherits the effective source mode. Commander keeps
830
+ // the normal new-run default as a real value, so consult its provenance
831
+ // rather than mistaking the default for an explicit override.
832
+ if (command.getOptionValueSource('mode') === 'default') {
833
+ if (resolvedResumeSource.mode)
834
+ options.mode = resolvedResumeSource.mode;
835
+ else if (!options.quiet)
836
+ process.stderr.write(chalk.yellow(`[agents] session ${resolvedResumeSource.shortId} predates stored launch modes; using --mode ${options.mode}\n`));
837
+ }
838
+ const { machineId } = await import('../lib/machine-id.js');
839
+ const sourceMachine = resolvedResumeSource.machine;
840
+ const explicitPlacement = hostTargetGiven(options).length > 0;
841
+ if (sourceMachine && sourceMachine !== machineId() && !explicitPlacement) {
842
+ options.host = sourceMachine;
843
+ }
844
+ else if (!autoHarnessRequested && sourceMachine && sourceMachine !== machineId() && explicitPlacement && !hostTargetGiven(options).includes(sourceMachine)) {
845
+ console.error(chalk.red(`Strict resume must run on ${sourceMachine}, where session ${resolvedResumeSource.shortId} is owned. ` +
846
+ `Use agents run auto --resume ${resolvedResumeSource.id} to hand off elsewhere.`));
847
+ process.exit(1);
848
+ }
849
+ // On the owning machine, `auto` first asks the existing account router
850
+ // whether the exact source version is healthy. If it is, native resume
851
+ // wins. If not, keep `auto` so the normal harness/account router selects
852
+ // a healthy local target and the later resume block performs /continue.
853
+ if (autoHarnessRequested && (!sourceMachine || sourceMachine === machineId())) {
854
+ const sourceAgent = resolvedResumeSource.agent;
855
+ if (sourceAgent in AGENTS && resolvedResumeSource.version) {
856
+ const { resolveRunVersion } = await import('../lib/rotate.js');
857
+ const health = await resolveRunVersion(sourceAgent, 'available', resolvedResumeSource.cwd ?? process.cwd());
858
+ if (!health.exhausted && health.version === resolvedResumeSource.version) {
859
+ normalizedAgentSpec = `${sourceAgent}@${resolvedResumeSource.version}`;
860
+ autoHarnessRequested = false;
861
+ if (!options.quiet)
862
+ process.stderr.write(chalk.gray(`[agents] auto resume → native ${normalizedAgentSpec} on ${sourceMachine ?? machineId()}\n`));
863
+ }
864
+ }
865
+ }
866
+ }
785
867
  if (autoHarnessRequested) {
786
868
  // `auto` is reserved. If a future harness registers that id, the
787
869
  // keyword collides — fail loud rather than silently shadow the harness.
@@ -797,7 +879,7 @@ export function registerRunCommand(program) {
797
879
  // Host layer: with no explicit --host/--device, default to the
798
880
  // affinity pick. Skipped on a host-dispatched run — its dispatcher
799
881
  // already resolved this layer (see runAutoDefaultsToAffinity).
800
- if (runAutoDefaultsToAffinity(options))
882
+ if (!resolvedResumeSource && runAutoDefaultsToAffinity(options))
801
883
  options.device = 'auto';
802
884
  }
803
885
  // --device auto / --host auto (and deprecated --smart): affinity-pick host.
@@ -2013,7 +2095,8 @@ export function registerRunCommand(program) {
2013
2095
  // AgentId is wider than SessionAgentId (amp/kiro/goose/copilot keep no transcripts);
2014
2096
  // those simply yield no matches and fall through to the not-found error.
2015
2097
  const sessionAgent = agent;
2016
- await discoverSessions({ agent: sessionAgent, version });
2098
+ if (!resolvedResumeSource)
2099
+ await discoverSessions({ agent: sessionAgent, version });
2017
2100
  // Resume is interactive unless a follow-on prompt makes it headless.
2018
2101
  const wantsInteractive = resolveInteractive({ interactive: options.interactive, headless: options.headless, prompt });
2019
2102
  const idArg = typeof options.resume === 'string' ? options.resume.trim() : '';
@@ -2024,9 +2107,9 @@ export function registerRunCommand(program) {
2024
2107
  catch {
2025
2108
  scopeCwd = cwd;
2026
2109
  }
2027
- let session;
2110
+ let session = resolvedResumeSource;
2028
2111
  if (idArg) {
2029
- let matches = findSessionsById(idArg, { agent: sessionAgent, version, cwd: scopeCwd });
2112
+ let matches = session ? [session] : findSessionsById(idArg, { agent: sessionAgent, version, cwd: scopeCwd });
2030
2113
  if (matches.length === 0) {
2031
2114
  const wide = findSessionsById(idArg, { agent: sessionAgent, version });
2032
2115
  if (wide.length > 0) {
@@ -2076,10 +2159,12 @@ export function registerRunCommand(program) {
2076
2159
  session = picked.session;
2077
2160
  forceInteractive = true; // bare resume always lands in the agent's TUI
2078
2161
  }
2079
- // Pin to the chosen session's own version (the isolated HOME the transcript
2080
- // lives in) and route by tier.
2081
- version = session.version;
2082
- if (nativeResume(agent)) {
2162
+ // Native resume is valid only for the source harness + exact isolated
2163
+ // version. `auto` may have selected another healthy harness/account; in
2164
+ // that case keep the target version and hand off through /continue.
2165
+ const canResumeNatively = session.agent === agent && nativeResume(agent, session.version);
2166
+ if (canResumeNatively) {
2167
+ version = session.version;
2083
2168
  resumeNative = true;
2084
2169
  resumeSessionId = session.id;
2085
2170
  // Native `--resume` (claude/codex) resolves the transcript relative to the
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `agents humans` — owner identity and notification channel management.
3
+ *
4
+ * Reads from ~/.agents/humans.yaml (created by migration from owner.md and
5
+ * agents.yaml notify.owner). Provides inspection commands for the current
6
+ * owner config.
7
+ */
8
+ import type { Command } from 'commander';
9
+ /** Register the `agents humans` command tree. */
10
+ export declare function registerHumansCommands(program: Command): void;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * `agents humans` — owner identity and notification channel management.
3
+ *
4
+ * Reads from ~/.agents/humans.yaml (created by migration from owner.md and
5
+ * agents.yaml notify.owner). Provides inspection commands for the current
6
+ * owner config.
7
+ */
8
+ import { setHelpSections } from '../lib/help.js';
9
+ import { readHumans, getOwnerFromHumans } from '../lib/humans.js';
10
+ /** Register the `agents humans` command tree. */
11
+ export function registerHumansCommands(program) {
12
+ const humansCmd = program
13
+ .command('humans')
14
+ .description('Inspect owner identity and notification channel config (humans.yaml)');
15
+ setHelpSections(humansCmd, {
16
+ examples: `
17
+ # Show owner identity and channel config as JSON
18
+ agents humans show owner --json
19
+
20
+ # Show owner identity in human-readable form
21
+ agents humans show owner
22
+ `,
23
+ notes: `
24
+ humans.yaml is the canonical owner identity file at ~/.agents/humans.yaml.
25
+ It is populated by migrating notify.owner from agents.yaml and frontmatter
26
+ from owner.md. Use \`agents humans show owner\` to inspect the current config.
27
+ `,
28
+ });
29
+ const showCmd = humansCmd
30
+ .command('show')
31
+ .description('Show config from humans.yaml');
32
+ setHelpSections(showCmd, {
33
+ examples: `
34
+ agents humans show owner
35
+ agents humans show owner --json
36
+ `,
37
+ });
38
+ showCmd
39
+ .command('owner')
40
+ .description('Show the configured owner identity and notification channels')
41
+ .option('--json', 'Output as JSON')
42
+ .action((opts) => {
43
+ const humans = readHumans();
44
+ if (!humans) {
45
+ if (opts.json) {
46
+ process.stdout.write(JSON.stringify(null) + '\n');
47
+ }
48
+ else {
49
+ process.stderr.write('humans.yaml not found — run `agents` once to trigger migration\n');
50
+ process.exitCode = 1;
51
+ }
52
+ return;
53
+ }
54
+ const owner = getOwnerFromHumans();
55
+ if (opts.json) {
56
+ process.stdout.write(JSON.stringify(owner ?? null, null, 2) + '\n');
57
+ }
58
+ else {
59
+ if (!owner) {
60
+ process.stdout.write('No owner configured in humans.yaml\n');
61
+ return;
62
+ }
63
+ if (owner.name)
64
+ process.stdout.write(`Name: ${owner.name}\n`);
65
+ if (owner.timezone)
66
+ process.stdout.write(`Timezone: ${owner.timezone}\n`);
67
+ if (owner.quiet_hours)
68
+ process.stdout.write(`Quiet: ${owner.quiet_hours}\n`);
69
+ if (owner.default_severity)
70
+ process.stdout.write(`Severity: ${owner.default_severity}\n`);
71
+ if (owner.notify) {
72
+ process.stdout.write(`Notify: channel=${owner.notify.channel} to=${owner.notify.to}\n`);
73
+ }
74
+ if (owner.channels?.length) {
75
+ process.stdout.write(`Channels:\n`);
76
+ for (const ch of owner.channels) {
77
+ const extra = [ch.transport, ch.intrusive ? 'intrusive' : null].filter(Boolean).join(', ');
78
+ process.stdout.write(` - ${ch.id}${extra ? ` (${extra})` : ''}\n`);
79
+ }
80
+ }
81
+ if (owner.policy) {
82
+ process.stdout.write(`Policy:\n`);
83
+ for (const [sev, channels] of Object.entries(owner.policy)) {
84
+ if (Array.isArray(channels) && channels.length) {
85
+ process.stdout.write(` ${sev}: ${channels.join(', ')}\n`);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ });
91
+ }
@@ -22,7 +22,7 @@ import { discoverWorkflowsFromRepo, installWorkflowCentrally, } from '../lib/wor
22
22
  import { discoverSubagentsFromRepo, installSubagentCentrally, } from '../lib/subagents.js';
23
23
  import { discoverPermissionsFromRepo, installPermissionSet, } from '../lib/permissions.js';
24
24
  import { listInstalledVersions, resolveConfiguredAgentTargets, syncResourcesToVersion, } from '../lib/versions.js';
25
- import { isInteractiveTerminal, isPromptCancelled, parseCommaSeparatedList, requireDestructiveArg, requireInteractiveSelection, resolveInstalledAgentTargetsAutoInstalling, } from './utils.js';
25
+ import { formatPath, isInteractiveTerminal, isPromptCancelled, parseCommaSeparatedList, requireDestructiveArg, requireInteractiveSelection, resolveInstalledAgentTargetsAutoInstalling, } from './utils.js';
26
26
  import { itemPicker } from '../lib/picker.js';
27
27
  import { registerMcpCommandToTargets, discoverMcpConfigsFromRepo, installMcpConfigCentrally, } from '../lib/mcp.js';
28
28
  export function buildMcpPackageCommand(pkg) {
@@ -435,37 +435,30 @@ the sha256 in the index and abort on mismatch.
435
435
  // ==========================================================================
436
436
  program
437
437
  .command('install <identifier>')
438
- .description('Install a package by registry name (mcp:notion), GitHub URL (gh:user/repo), or skill identifier')
438
+ .description('Install a package: mcp:, skill:, plugin:, or GitHub (gh:user/repo) — one install path (Phase 5)')
439
439
  .option('-a, --agents <list>', 'Targets: claude, codex@0.116.0, or cursor@default')
440
440
  .option('--types <list>', 'When source is a repo: comma-separated resource types to install (skills,workflows,commands,hooks,permissions,subagents,mcp)')
441
441
  .option('--names <list>', 'When source is a repo: comma-separated resource names within the selected types')
442
442
  .option('-y, --yes', 'Auto-install any missing agent versions without prompting')
443
+ .option('--allow-exec-surfaces', 'With plugin: allow plugins that ship hooks/mcp/bin (same as agents plugins install)')
443
444
  .addHelpText('after', `
444
- Install resolves the package type (MCP server, skill, command, hook) and installs to the specified agents. Packages can come from registries (mcp:, skill:), GitHub (gh:user/repo), or direct URLs.
445
+ Install is the unified add path (Phase 5 packaging). Prefix the identifier:
446
+
447
+ mcp:<name> MCP server from a registry
448
+ skill:<name> skill from a registry (or gh: fallback)
449
+ plugin:<spec> plugin — same grammar as agents plugins install (name@url or path)
450
+ gh:user/repo clone a DotAgents / multi-resource repo and install selected types
445
451
 
446
452
  Examples:
447
- # Install an MCP server from a registry
448
453
  agents install mcp:notion --agents claude
449
-
450
- # Install skills and commands from GitHub
454
+ agents install skill:animator --agents claude,codex
455
+ agents install plugin:my-plugin@https://github.com/user/my-plugin.git
456
+ agents install plugin:~/Projects/rush-toolkit
451
457
  agents install gh:anthropics/skills --agents codex,claude
452
-
453
- # Install using GitHub shorthand
454
- agents install gh:user/repo --agents claude@2.1.112
455
-
456
- # Install only specific resource types from a multi-resource repo
457
458
  agents install gh:phnx-labs/.agents-system --types skills,workflows --agents claude@all
458
459
 
459
- # Install specific resources by name
460
- agents install gh:phnx-labs/.agents-system --types skills --names animator,composer --agents claude@all
461
-
462
- # Install to all installed agents (uses defaults or prompts)
463
- agents install mcp:postgres
464
-
465
- When to use:
466
- - After search: 'agents search notion' then 'agents install mcp:notion'
467
- - Team setup: 'agents install gh:team/resources' to sync everyone's tooling
468
- - Quick MCP add: 'agents install mcp:<name>' when you know the package name
460
+ Specialized verbs still work (agents plugins install, agents skills add) and
461
+ delegate to the same underlying installers.
469
462
  `)
470
463
  .action(async (identifier, options) => {
471
464
  const spinner = ora('Resolving package...').start();
@@ -473,10 +466,85 @@ When to use:
473
466
  const resolved = await resolvePackage(identifier);
474
467
  if (!resolved) {
475
468
  spinner.fail('Package not found');
476
- console.log(chalk.gray('\nTip: Use explicit prefix (mcp:, skill:, gh:) or check the identifier.'));
469
+ console.log(chalk.gray('\nTip: Use explicit prefix (mcp:, skill:, plugin:, gh:) or check the identifier.'));
477
470
  process.exit(1);
478
471
  }
479
472
  spinner.succeed(`Found ${resolved.type} package`);
473
+ if (resolved.type === 'plugin') {
474
+ spinner.stop();
475
+ const spec = resolved.pluginSpec ?? resolved.source;
476
+ console.log(chalk.gray(`Installing plugin from: ${spec}`));
477
+ const { installPlugin, getPlugin, inspectPluginCapabilities, pluginCapabilityLabels, parseInstallSpec, checkPluginDependencies, hasPluginExecSurfaces, pluginSupportsAgent, syncPluginToVersion, pluginResourceGroups, } = await import('../lib/plugins.js');
478
+ const { listInstalledVersions, getGlobalDefault, getVersionHomePath, syncResourcesToVersion } = await import('../lib/versions.js');
479
+ const { agentLabel } = await import('../lib/agents.js');
480
+ let name;
481
+ let root;
482
+ try {
483
+ const result = await installPlugin(spec);
484
+ name = result.name;
485
+ root = result.root;
486
+ }
487
+ catch (err) {
488
+ console.log(chalk.red(`Install failed: ${err.message}`));
489
+ process.exit(1);
490
+ }
491
+ const plugin = getPlugin(name);
492
+ if (!plugin) {
493
+ console.log(chalk.red(`Installed but could not load plugin '${name}'`));
494
+ process.exit(1);
495
+ }
496
+ const capabilities = inspectPluginCapabilities(root);
497
+ const allowExec = options.allowExecSurfaces === true;
498
+ if (hasPluginExecSurfaces(capabilities) && !allowExec) {
499
+ const source = parseInstallSpec(spec).source;
500
+ console.error(chalk.red('Install refused: plugin ships executable surfaces:'));
501
+ for (const label of pluginCapabilityLabels(capabilities)) {
502
+ console.error(` ${label}`);
503
+ }
504
+ console.error(`Re-run with --allow-exec-surfaces if you trust the source: ${source}@HEAD`);
505
+ fs.rmSync(root, { recursive: true, force: true });
506
+ process.exit(1);
507
+ }
508
+ const missingDeps = checkPluginDependencies(plugin.manifest);
509
+ if (missingDeps.length > 0) {
510
+ console.log(chalk.yellow(`Warning: missing dependencies: ${missingDeps.join(', ')}`));
511
+ console.log(chalk.gray('Install them with: agents install plugin:<name>@<source>'));
512
+ }
513
+ // Same sync loop as `agents plugins install` (default version per harness).
514
+ console.log();
515
+ let synced = 0;
516
+ for (const agentId of capableAgents('plugins')) {
517
+ if (!pluginSupportsAgent(plugin, agentId))
518
+ continue;
519
+ const versions = listInstalledVersions(agentId);
520
+ if (versions.length === 0)
521
+ continue;
522
+ const defaultVer = getGlobalDefault(agentId);
523
+ const targetVersions = defaultVer ? [defaultVer] : [versions[versions.length - 1]];
524
+ for (const version of targetVersions) {
525
+ const didSync = allowExec
526
+ ? syncPluginToVersion(plugin, agentId, getVersionHomePath(agentId, version), {
527
+ allowExecSurfaces: true,
528
+ }).success
529
+ : syncResourcesToVersion(agentId, version, { plugins: [name] }).plugins.length > 0;
530
+ if (didSync) {
531
+ console.log(chalk.green(` Synced to ${agentLabel(agentId)}@${version}`));
532
+ synced++;
533
+ }
534
+ }
535
+ }
536
+ if (synced === 0) {
537
+ console.log(chalk.gray(' No supported agent versions installed — run "agents use <agent>@<version>" to sync.'));
538
+ }
539
+ console.log(chalk.bold(`\nInstalled ${plugin.name} v${plugin.manifest.version} to ${formatPath(root)}`));
540
+ const groups = pluginResourceGroups(plugin);
541
+ if (groups.length > 0) {
542
+ for (const g of groups) {
543
+ console.log(chalk.gray(` ${g.label}: ${g.items.slice(0, 8).join(', ')}${g.items.length > 8 ? '…' : ''}`));
544
+ }
545
+ }
546
+ return;
547
+ }
480
548
  if (resolved.type === 'mcp') {
481
549
  // Install MCP server
482
550
  const entry = resolved.mcpEntry;
@@ -24,11 +24,8 @@ export declare function formatFleetSkippedNote(skipped: string[]): string;
24
24
  *
25
25
  * `stderr: 'ignore'` is load-bearing, not tidiness. A checkout with no origin
26
26
  * makes git print `error: No such remote 'origin'` on ITS stderr, which is the
27
- * terminal's — the catch below never sees it. That was invisible while this was
28
- * called once from `add` inside a real repo, and became noise the moment
29
- * `import --from-factory` started calling it per row: importing 12 registry
30
- * rows printed two raw git errors between the progress lines. Absence of a
31
- * remote is an expected answer here (`undefined`), not something to report.
27
+ * terminal's — the catch below never sees it. Absence of a remote is an
28
+ * expected answer here (`undefined`), not something to report.
32
29
  */
33
30
  export declare function originSlug(cwd: string): string | undefined;
34
31
  /** One `projects list` row, pre-render. */