@phnx-labs/agents-cli 1.22.8 → 1.22.10

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 (65) hide show
  1. package/CHANGELOG.md +64 -4
  2. package/README.md +5 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/browser.js +2 -2
  5. package/dist/commands/exec.js +41 -9
  6. package/dist/commands/feed.js +38 -1
  7. package/dist/commands/harness.d.ts +40 -1
  8. package/dist/commands/harness.js +316 -76
  9. package/dist/commands/profiles.d.ts +42 -0
  10. package/dist/commands/profiles.js +91 -3
  11. package/dist/commands/secrets.js +38 -39
  12. package/dist/commands/sessions-picker.js +3 -3
  13. package/dist/commands/sessions-render.d.ts +12 -0
  14. package/dist/commands/sessions-render.js +124 -0
  15. package/dist/commands/sessions.js +28 -1
  16. package/dist/commands/ssh.js +30 -8
  17. package/dist/commands/teams.js +12 -1
  18. package/dist/commands/workflows.js +1 -1
  19. package/dist/index.js +9 -2
  20. package/dist/lib/crabbox/cli.d.ts +35 -0
  21. package/dist/lib/crabbox/cli.js +46 -0
  22. package/dist/lib/crabbox/config.d.ts +21 -0
  23. package/dist/lib/crabbox/config.js +43 -0
  24. package/dist/lib/crabbox/lease.d.ts +15 -5
  25. package/dist/lib/crabbox/lease.js +57 -17
  26. package/dist/lib/daemon.js +12 -11
  27. package/dist/lib/devices/resolve-target.d.ts +4 -3
  28. package/dist/lib/devices/resolve-target.js +4 -3
  29. package/dist/lib/hosts/passthrough.js +18 -1
  30. package/dist/lib/hosts/registry.d.ts +4 -0
  31. package/dist/lib/hosts/registry.js +16 -0
  32. package/dist/lib/hosts/remote-cmd.js +1 -0
  33. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  34. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  35. package/dist/lib/observe-aliases.d.ts +30 -0
  36. package/dist/lib/observe-aliases.js +56 -0
  37. package/dist/lib/plugins.d.ts +14 -5
  38. package/dist/lib/plugins.js +29 -5
  39. package/dist/lib/redact.js +4 -0
  40. package/dist/lib/resources/types.d.ts +2 -1
  41. package/dist/lib/resources/workflows.d.ts +1 -1
  42. package/dist/lib/resources/workflows.js +22 -20
  43. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  44. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  45. package/dist/lib/secrets/bundles.d.ts +9 -0
  46. package/dist/lib/secrets/bundles.js +9 -23
  47. package/dist/lib/secrets/headless.d.ts +3 -15
  48. package/dist/lib/secrets/headless.js +5 -33
  49. package/dist/lib/secrets/index.d.ts +8 -0
  50. package/dist/lib/secrets/index.js +16 -36
  51. package/dist/lib/session/parse.d.ts +5 -1
  52. package/dist/lib/session/parse.js +23 -13
  53. package/dist/lib/session/prompt.js +5 -0
  54. package/dist/lib/session/render.d.ts +3 -0
  55. package/dist/lib/session/render.js +33 -10
  56. package/dist/lib/share/config.js +5 -7
  57. package/dist/lib/startup/command-registry.js +15 -1
  58. package/dist/lib/types.d.ts +6 -0
  59. package/dist/lib/usage-refresh.d.ts +19 -11
  60. package/dist/lib/usage-refresh.js +75 -43
  61. package/dist/lib/usage.d.ts +12 -0
  62. package/dist/lib/usage.js +37 -6
  63. package/dist/lib/workflows.d.ts +19 -4
  64. package/dist/lib/workflows.js +81 -7
  65. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,65 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.10
4
+
5
+ - **Plugins package workflows (Phase 5 packaging slice).** A plugin’s `workflows/<name>/WORKFLOW.md` is discovered and resolved by `agents run <name>` with precedence project > user > plugin > extra > system — no separate install into `~/.agents/workflows/` required. Plugin inventory / resource groups list `workflows`. Source: `apps/cli/src/lib/workflows.ts`, `apps/cli/src/lib/plugins.ts`, `apps/cli/src/lib/resources/workflows.ts`.
6
+
7
+ - **`scripts/release.sh` routes the home-base publish hop via `agents ssh`.** Plain `ssh mac-mini` fails host-key checks on headless Linux workers; `agents ssh` uses the devices registry and brokered credentials. Falls back to plain ssh only when `agents` is not on PATH. Source: `apps/cli/scripts/release.sh`.
8
+
9
+ - **Touch ID is now raised in exactly one place — `agents secrets unlock`.** `agents secrets list`, `agents run <agent>`, `secrets get`/`export`/`view`, and every background read resolve from the secrets broker / durable session / no-ACL layer and never raise a biometric sheet; a locked keychain bundle fails with an actionable "run `agents secrets unlock <bundle>`" hint instead of prompting. The `AGENTS_SECRETS_NO_PROMPT` environment override and the "a human at a TTY, so prompting is fine" heuristic are deleted — the prompt decision is structural, not an ambient env var. The macOS keychain `list`/`list-synced` enumeration queries now pass `kSecUseAuthenticationUISkip` (enumeration itself was evaluating the biometry ACL, so `agents secrets list` prompted and silently dropped keychain bundles when the sheet was cancelled), and the one-time hash-rekey + metadata-ACL heal run only inside the single `unlock` sheet so nothing on the run/list path can storm. Source: `apps/cli/src/lib/secrets/keychain-helper.swift`, `apps/cli/src/lib/secrets/index.ts`, `apps/cli/src/lib/secrets/bundles.ts`, `apps/cli/src/lib/secrets/headless.ts`, `apps/cli/src/commands/secrets.ts`.
10
+ - **The Factory VS Code extension (`swarm-ext`) no longer decrypts secrets or shells raw `ssh`.** Device health, reachability, and sync route every remote command through `agents ssh <host>` (broker-owned credentials, no prompt); the extension's own secret-resolution path (`resolveSecret`/`discoverSecretsReadCmd`/`extractCredentials`) is removed, so rendering the devices list never raises Touch ID. Source: `apps/factory/src/vscode/deviceHealth.vscode.ts`, `apps/factory/src/vscode/settings.vscode.ts`, `apps/factory/src/vscode/extension.ts`.
11
+
12
+ ## 1.22.9
13
+
14
+ - **`agents ssh auto` and `agents teams add --device auto` no longer reject with "Unknown device 'auto'" (RUSH-2185).** The `auto` affinity sentinel was a `run`-only preprocessing step (`applyDeviceAutoToOptions` in `smart-launch.ts`, wired only from `agents run`'s exec path) — every other `--host`/`--device` caller went straight to the shared resolver, which had no idea what `auto` meant and reported it as an unregistered device. `matchHost` (the one core every `--host`/`--device` caller shares) now resolves `auto` directly via the same `resolveDeviceAffinity` engine `run` uses, so `agents ssh`, `agents teams add`, and anything else routed through `matchHost`/`resolveHost` (including the generic `--host`/`--device` passthrough) pick a device the same way. `agents teams add --device auto` landing on the local machine now just runs the teammate locally, matching `run`'s "null pick = local" outcome; `agents ssh auto` refuses a local pick with a clear message instead of self-SSHing, since `agents ssh` exists to dial OUT to a remote box. Source: `apps/cli/src/lib/hosts/registry.ts`, `apps/cli/src/lib/devices/resolve-target.ts`, `apps/cli/src/commands/ssh.ts`, `apps/cli/src/commands/teams.ts`, `apps/cli/docs/00-concepts.md`, `apps/cli/docs/hosts.md`, `apps/cli/docs/teams.md`.
15
+
16
+ - **`agents harness edit` gains `--auth-provider`, `--fallback-model`, and `--from-secrets`; `add`/`fork` gain an interactive wizard and `--from-secrets`.** `edit` (already shipped with `--model`/`--base-url`/`--version`/`--description`) now also repoints auth at a different keychain-backed provider, sets or clears (`--fallback-model ""`) the same-host fallback model retried on a rate limit, and — like `add`/`fork` — accepts `--from-secrets <bundle>[:<key>]` to copy a value out of an existing `agents secrets` bundle into the harness's own keychain item once, instead of retyping a key already stored elsewhere (the item it writes to, `agents-cli.<provider>.token`, is never gated behind the biometry-required prefixes, so later reads stay silent). `agents harness add`/`fork` now accept `[name]`/`[source] [name]` as optional positionals — run either with insufficient flags in an interactive terminal and a picker (fork from a native host or existing harness → a built-in preset or "build custom" → the harness's name, pre-filled with the preset's own name → how to get the key) replaces the old hard error; flags remain fully supported for scripts, and a non-interactive shell still gets the original error. Source: `apps/cli/src/commands/harness.ts`, `apps/cli/src/commands/profiles.ts`, `apps/cli/docs/profiles.md`.
17
+
18
+ - **`agents run --lease` is reuse-first against the crabbox profile pool, with a
19
+ `--fresh` opt-out.** A bare `--lease` used to always lease a brand-new box, so
20
+ bursts of runs (e.g. resumed sessions) stacked up idle `keep=true` boxes at
21
+ full monthly cost. Now, before warming a new box, the run looks for a warm box
22
+ carrying the same `profile` label the warmup would use (read from the repo's
23
+ `.crabbox.yaml`, matching `scripts/sandbox.sh`'s `pick_ready_box`) and the same
24
+ network mode — a tailnet box is never handed to a public run or vice versa —
25
+ and reuses the first one `crabbox status` reports SSH-ready, keeping it after
26
+ the run. A not-ready pool box is skipped, never stopped. `--fresh` forces the
27
+ old behavior (brand-new box, torn down after the run); `--box <slug>` is
28
+ unchanged. Source: `apps/cli/src/lib/crabbox/lease.ts`,
29
+ `apps/cli/src/lib/crabbox/cli.ts`, `apps/cli/src/lib/crabbox/config.ts`,
30
+ `apps/cli/src/commands/exec.ts`.
31
+
32
+ - **The menu bar now notices and reports when the scheduler dies — instead of
33
+ staying silent forever.** The only proactive "routines overdue / scheduler
34
+ down" signal was `notifyOverdue` (`src/lib/overdue.ts`), fired from inside
35
+ `runDaemon()` — so it could never fire while the daemon itself was down, the
36
+ exact outage it exists to report. `MenubarHelper` is a separate launchd
37
+ KeepAlive service that stays alive when the daemon dies, so its 10s tick now
38
+ polls daemon liveness independently of the dropdown ever being opened; once
39
+ it has been continuously unreachable for ~30s (debounced past a routine
40
+ restart blip), it fires one native notification ("Scheduler stopped —
41
+ routines won't run") through its own `NSUserNotificationCenter` delivery —
42
+ no daemon, no CLI spawn required — and lights the always-visible menu-bar
43
+ badge (`⏻`) until the scheduler comes back. Source:
44
+ `apps/cli/menubar/Sources/MenubarHelper/StatusItemController.swift`.
45
+
46
+ - **Observe umbrella aliases: `inbox`, `timeline`, `roster` (Phase 3 surface consolidation).**
47
+ Thin doors onto existing readers (no store merge): `agents inbox` ≡ `feed`,
48
+ `agents timeline` ≡ `feed --filter updates`, `agents roster` ≡ `sessions --active`.
49
+ Root help gains an Observe section; `agents audit` stays the tamper-evident run
50
+ log (not an events alias). Source: `apps/cli/src/lib/observe-aliases.ts`,
51
+ `apps/cli/src/commands/feed.ts`, `apps/cli/src/commands/sessions.ts`.
52
+
53
+ - **Daemon usage refresh is a fixed 5-minute per-host schedule, concurrency-safe, and Touch-ID-free.**
54
+ Each machine's daemon still owns its own usage cache (no fleet-wide store). Account live
55
+ fetches are now scheduled every **5 minutes** (was adaptive 90s–15m), with a 60s wake to
56
+ notice due accounts after backoff ends. Cache writes use a file lock + atomic rename so a
57
+ concurrent `agents view` background refresh cannot tear or drop rows. The daemon path
58
+ loads Claude credentials with **`fileOnly`** (setup-token / no-ACL cache / `.credentials.json`
59
+ only) and never opens the ACL-bound macOS keychain item, so a background tick cannot pop
60
+ Touch ID. Refresh still skips a provider under 429 backoff and still never rotates
61
+ single-use Claude refresh tokens.
62
+
3
63
  ## 1.22.8
4
64
 
5
65
  - **`agents browser` now gates cross-machine drives behind per-device consent.** `agents browser <cmd> --host <device>` already routes a browser command to another fleet machine over SSH and drives its browser — but nothing asked that machine's permission, so any box you could SSH to, you could drive. A new device-local `browser.remote-control` setting (off by default, never synced) fixes that: a fleet-remote `browser --host <this-machine> start` is refused with an actionable message until the owner runs `agents browser remote-control on` here. Local starts (no `--host`) are never gated. The fleet passthrough marks every `--host` dispatch with `AGENTS_FLEET_REMOTE` so the far side can tell a cross-machine drive from a local one. New command: `agents browser remote-control [on|off]` (no arg prints status; `--json` supported). Source: `apps/cli/src/lib/browser/remote-control.ts`, `apps/cli/src/commands/browser.ts`, `apps/cli/src/lib/hosts/passthrough.ts`, `apps/cli/src/lib/device-config.ts`, `apps/cli/docs/browser.md`.
@@ -983,6 +1043,8 @@
983
1043
 
984
1044
  ## 1.20.92
985
1045
 
1046
+ - **`agents sessions render <id...>` produces shareable, redacted Markdown instead of raw harness JSONL.** Claude, Codex, Kimi, Grok, Cursor, and Droid transcripts flow through the existing normalized `SessionEvent[]` parsers, then render with the same session-browser preview at the top, ordered user/assistant turns, fenced shell commands, JSON tool arguments, and explicitly truncated tool results. Redaction remains on by default through the canonical redactor, now also masking Unix/macOS/Windows home-directory identities and live secret values; `--no-redact` is local-only. Reasoning defaults to omitted and can be folded or included explicitly. One or several selected sessions can be written to a mode-`0600` Markdown file, and `--json` exposes the rendered documents for machine callers. The repo session skill and transcript-sharing guidance now require rendering `.md` before creating a confidential gist. Source: `apps/cli/src/commands/sessions-render.ts`, `apps/cli/src/lib/session/render.ts`, `apps/cli/src/lib/redact.ts`, `.agents/skills/sessions/SKILL.md`.
1047
+
986
1048
  - **`agents sessions` rows show creation time as well as last activity (RUSH-2107).**
987
1049
  The trailing time cell used to carry one unlabeled "X ago" — last activity — so a
988
1050
  row could not say when the session began or how long it had been alive. It now
@@ -2855,10 +2917,8 @@
2855
2917
  `agentOnly` guard; and `isHeadlessSecretsContext` recognized the `headless` and
2856
2918
  `teams` runtimes but not `terminal`, which is what an interactive run sets. Agent
2857
2919
  launches now resolve broker-only and a locked bundle fails fast naming
2858
- `agents secrets unlock <bundle>`; `AGENTS_SECRETS_NO_PROMPT=1` is no longer needed
2859
- as a workaround. `agents secrets get/export/exec` typed in a **plain shell** still
2860
- prompts — it carries no `AGENTS_RUNTIME`, so the guard does not apply. Run beneath
2861
- an agent it refuses, because there the agent is the caller. This narrows the
2920
+ `agents secrets unlock <bundle>`. Direct read commands use the same broker-only
2921
+ path even from a plain shell; only an explicit unlock may authenticate. This narrows the
2862
2922
  agent-triggered approval added in RUSH-2032, which is unreleased.
2863
2923
 
2864
2924
  - **`release.sh` now borrows the npm token from a primary device when the local box
package/README.md CHANGED
@@ -255,6 +255,9 @@ agents sessions --project my-app
255
255
  # Read a full conversation
256
256
  agents sessions a1b2c3d4 --markdown
257
257
 
258
+ # Render a shareable, redacted Markdown transcript with the session preview on top
259
+ agents sessions render a1b2c3d4 -o session.md
260
+
258
261
  # Just the last 3 turns, user messages only
259
262
  agents sessions a1b2c3d4 --last 3 --include user
260
263
 
@@ -323,6 +326,8 @@ Each live session resolves to `working`, `waiting_input` (with why -- a question
323
326
 
324
327
  Landing on a session cold? `agents sessions <id>` prints a catch-up digest: an inferred title, files changed grouped by directory (created / modified / deleted), a histogram of which tools did the work (including parsed Bash commands -- `git`, `npm`, `ffmpeg`, `ssh`, and so on), and the last test verdict -- the signals to reload a task in seconds.
325
328
 
329
+ Sharing a session uses `agents sessions render <id> -o session.md`, not the raw harness JSONL. The document starts with that same preview, then presents user and assistant turns, fenced commands, structured tool arguments, and bounded tool output. Credential-shaped values and local home paths are redacted by default; `--no-redact` is for local-only inspection.
330
+
326
331
  ### Resume anywhere — and stay resumed
327
332
 
328
333
  Pick up any past conversation and drop it back into a terminal:
package/dist/bin/agents CHANGED
Binary file
@@ -5,7 +5,7 @@ import { updateMeta } from '../lib/state.js';
5
5
  import { resolveActor } from '../lib/actor.js';
6
6
  import { loginsForProfile, profilesLoggedInto, serviceForUrl, loginsWithAccountsForProfile, accountsForProfile, credKeysForService, AUTH_SIGNATURES, } from '../lib/browser/login-detection.js';
7
7
  import { parseSecretRef } from '../lib/browser/secret-ref.js';
8
- import { readAndResolveBundleEnv, isHeadlessSecretsContext, bundleExists, readBundle, describeBundle } from '../lib/secrets/bundles.js';
8
+ import { readAndResolveBundleEnv, bundleExists, readBundle, describeBundle } from '../lib/secrets/bundles.js';
9
9
  import { findBrowserPath, getPortOccupant, isLauncherScript } from '../lib/browser/chrome.js';
10
10
  import { listProfileCacheDirs, removeProfileCache, listAllProfileSnapshots, } from '../lib/browser/runtime-state.js';
11
11
  import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
@@ -1481,7 +1481,7 @@ function registerTaskCommands(browser) {
1481
1481
  process.exit(1);
1482
1482
  }
1483
1483
  try {
1484
- const { env } = readAndResolveBundleEnv(parsed.bundle, { caller: 'browser type', keys: [parsed.key], keyMode: 'storage', agentOnly: isHeadlessSecretsContext() });
1484
+ const { env } = readAndResolveBundleEnv(parsed.bundle, { caller: 'browser type', keys: [parsed.key], keyMode: 'storage', agentOnly: true });
1485
1485
  if (!(parsed.key in env)) {
1486
1486
  console.error(`Key "${parsed.key}" not in bundle "${parsed.bundle}".`);
1487
1487
  process.exit(1);
@@ -560,9 +560,10 @@ export function registerRunCommand(program) {
560
560
  .option('--no-follow', 'With --host, dispatch detached and return immediately (track via `agents hosts ps/logs`).')
561
561
  .option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match.')
562
562
  .option('--copy-creds', 'With --host, copy the picked runtime credentials (and Claude OAuth token) to the host, then shred them after the run. Opt-in per run.')
563
- .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). Same as --where lease[:backend]. Unlike --host, no machine is registered.')
563
+ .option('--lease [backend]', "Run on a cloud box (via crabbox) and tear it down after — reuses a warm box from the repo's profile pool when one is ready (--fresh forces a new box). Optional backend selects the cloud (hetzner/aws/do). Same as --where lease[:backend]. Unlike --host, no machine is registered.")
564
564
  .option('--box <slug>', 'Reuse an existing warm crabbox box for this run instead of provisioning a disposable --lease box.')
565
565
  .option('--keep-box', 'With --lease, keep the box after the run instead of stopping it.')
566
+ .option('--fresh', "With --lease, always provision a brand-new box (skip the warm profile-pool reuse) and tear it down after the run.")
566
567
  .option('--reuse', 'With --lease, reuse the most-recently-used warm box if one exists (else provision fresh). The scriptable form of the interactive reuse picker.')
567
568
  .option('--bare', 'With --lease, skip copying your local ~/.agents setup (skills/hooks/commands/MCP) onto the box.')
568
569
  .option('--tailscale', 'Lease the box onto your tailnet (reachable only over Tailscale) rather than a public IP.')
@@ -621,6 +622,14 @@ export function registerRunCommand(program) {
621
622
  # Inject a keychain-backed secrets bundle
622
623
  agents run claude "deploy the worker" --secrets prod --mode edit
623
624
 
625
+ # Run on a cloud box — reuses a warm box from the repo's profile pool when
626
+ # one is ready (kept after the run), else leases a fresh box, torn down after
627
+ agents run claude "fix the failing tests" --lease
628
+
629
+ # Force a brand-new box (destroyed after), or target a warm box by slug
630
+ agents run claude "fix the failing tests" --lease --fresh
631
+ agents run claude "fix the failing tests" --box warm-one
632
+
624
633
  # Pass arbitrary native flags to the underlying CLI via -- separator
625
634
  agents run kimi -- --plan --some-kimi-option value
626
635
  agents run claude "fix the bug" -- --custom-flag
@@ -824,6 +833,14 @@ export function registerRunCommand(program) {
824
833
  console.error(chalk.red('Pass either --lease to provision a disposable box, or --box <slug> to reuse a warm box — not both.'));
825
834
  process.exit(1);
826
835
  }
836
+ if (options.fresh && options.box) {
837
+ console.error(chalk.red('--fresh forces a brand-new box; it cannot be combined with --box <slug> (which reuses one).'));
838
+ process.exit(1);
839
+ }
840
+ if (options.fresh && options.reuse) {
841
+ console.error(chalk.red('--fresh forces a brand-new box; it cannot be combined with --reuse.'));
842
+ process.exit(1);
843
+ }
827
844
  const backend = typeof options.lease === 'string' ? options.lease : undefined;
828
845
  // crabbox syncs this directory to the box via `git ls-files`; outside a
829
846
  // git repo that fails with "build sync file list: exit status 128" — but
@@ -848,12 +865,21 @@ export function registerRunCommand(program) {
848
865
  // ── F3 reuse (RUSH-1922) + F5 net-mode (RUSH-1924) ───────────────────
849
866
  // Resolve which box this run targets and how it is networked BEFORE any
850
867
  // provisioning. `--box` is an explicit reuse; otherwise, on an
851
- // interactive tty, offer the warm boxes as a reuse picker (headless /
852
- // --json never blocks — it provisions fresh unless --reuse/--box).
868
+ // interactive tty, offer the warm boxes as a reuse picker. Headless runs
869
+ // never block: leaseAndRun itself is reuse-first against the profile
870
+ // pool (a ready pool box is reused; none ready → warm a fresh one).
871
+ // `--fresh` opts out of every reuse path.
853
872
  const leaseSecretsBundle = process.env.AGENTS_LEASE_SECRETS_BUNDLE;
854
873
  const nowSecs = Math.floor(Date.now() / 1000);
855
874
  let reuseSlug = options.box;
856
- if (options.lease && !reuseSlug) {
875
+ // The profile this run's pool/box carries: the repo's .crabbox.yaml
876
+ // `profile:` when declared (what crabbox warmup would label a fresh box
877
+ // with), else crabbox's default. Passing it to leaseAndRun makes the
878
+ // pool-reuse match interchangeable with a fresh warmup.
879
+ const repoRoot = gitToplevel(leaseCwd);
880
+ const { readCrabboxRepoProfile } = await import('../lib/crabbox/config.js');
881
+ const poolProfile = repoRoot ? readCrabboxRepoProfile(repoRoot) : undefined;
882
+ if (options.lease && !reuseSlug && !options.fresh) {
857
883
  const { crabboxList } = await import('../lib/crabbox/cli.js');
858
884
  const { reusableBoxes, formatBoxRow } = await import('./lease.js');
859
885
  let warm = [];
@@ -861,9 +887,8 @@ export function registerRunCommand(program) {
861
887
  warm = reusableBoxes(crabboxList({ secretsBundle: leaseSecretsBundle }), nowSecs);
862
888
  }
863
889
  catch {
864
- warm = []; // crabbox unavailable / no creds → just provision fresh
890
+ warm = []; // crabbox unavailable / no creds → the pool check in leaseAndRun decides
865
891
  }
866
- const repoRoot = gitToplevel(leaseCwd);
867
892
  const alwaysFresh = repoRoot ? isAlwaysFreshRepo(readAlwaysFreshRepos(), repoRoot) : false;
868
893
  if (warm.length > 0 && !alwaysFresh) {
869
894
  if (options.reuse) {
@@ -896,7 +921,8 @@ export function registerRunCommand(program) {
896
921
  console.error(chalk.yellow('Selection cancelled — provisioning a fresh box.'));
897
922
  }
898
923
  }
899
- // Headless with no --reuse falls through here → provision fresh.
924
+ // Headless with no --reuse falls through here → leaseAndRun's
925
+ // profile-pool check decides (reuse a ready pool box, else warm fresh).
900
926
  }
901
927
  else if (options.reuse && warm.length > 0) {
902
928
  // --reuse still honors a warm box even when the picker is suppressed.
@@ -1015,12 +1041,16 @@ export function registerRunCommand(program) {
1015
1041
  : `${runtime} credentials`;
1016
1042
  const boxLifecycle = reuseSlug
1017
1043
  ? `Reusing crabbox box ${reuseSlug}`
1018
- : `Leasing a ${backend ?? 'hetzner'} box${netMode === 'tailscale' ? ' on your tailnet' : ''}`;
1044
+ : options.fresh
1045
+ ? `Leasing a fresh ${backend ?? 'hetzner'} box${netMode === 'tailscale' ? ' on your tailnet' : ''}`
1046
+ : `Leasing a ${backend ?? 'hetzner'} box${netMode === 'tailscale' ? ' on your tailnet' : ''} (a ready box from the '${poolProfile ?? 'default'}' pool is reused when one exists)`;
1019
1047
  const boxAfterRun = reuseSlug
1020
1048
  ? 'the box is kept after the run'
1021
1049
  : options.keepBox
1022
1050
  ? 'the box is kept after the run'
1023
- : 'the box is destroyed after the run';
1051
+ : options.fresh
1052
+ ? 'the box is destroyed after the run'
1053
+ : 'a fresh box is destroyed after the run; a reused pool box is kept';
1024
1054
  console.error(chalk.gray(`${boxLifecycle} · shipping ${whatShips}${credentialRuntimes.length > 0 && leaseEmail ? ` (${leaseEmail})` : ''}; ${boxAfterRun}.`));
1025
1055
  // Read the Claude OAuth token from the local Keychain (silent) so it can be
1026
1056
  // written to ~/.claude/.credentials.json on the box — otherwise Claude boots
@@ -1100,6 +1130,8 @@ export function registerRunCommand(program) {
1100
1130
  secretsBundle: leaseSecretsBundle,
1101
1131
  keep: options.keepBox,
1102
1132
  reuseBox: reuseSlug,
1133
+ fresh: options.fresh,
1134
+ profile: poolProfile,
1103
1135
  copySetup,
1104
1136
  netMode,
1105
1137
  onData: (chunk) => router.push(chunk),
@@ -222,7 +222,7 @@ export function sessionHintsFromActive(sessions) {
222
222
  export function registerFeedCommand(program) {
223
223
  const feed = program
224
224
  .command('feed')
225
- .description('Open blocks (needs you) + agent status posts (feed post)')
225
+ .description('Operator inbox + agent status posts (aliases: inbox = needs-you; timeline = --filter updates)')
226
226
  .option('--json', 'Output as JSON (each block stamped with its outcome + ask class)')
227
227
  .option('--filter <view>', 'What to show: needs (default) · updates · all', 'needs')
228
228
  .option('--flat', 'List one block per agent instead of grouping by outcome')
@@ -572,6 +572,43 @@ docs/06-observability.md.
572
572
  renderOutcomeGroup(g, self);
573
573
  await renderTrailingActivity();
574
574
  });
575
+ // Observe-umbrella aliases (Phase 3): intentional second names, not deprecations.
576
+ // Re-parse into `feed` so flags/help stay single-sourced. See lib/observe-aliases.ts.
577
+ registerFeedObserveAliases(program);
578
+ }
579
+ /**
580
+ * `inbox` / `timeline` → feed. Loaded with the feed module so lazy COMMAND_LOADERS
581
+ * for those names also get the real `feed` command registered for re-parse.
582
+ */
583
+ function registerFeedObserveAliases(program) {
584
+ const reparse = async (alias) => {
585
+ const { expandObserveAlias } = await import('../lib/observe-aliases.js');
586
+ const rest = process.argv.slice(3);
587
+ const expanded = expandObserveAlias(alias, rest);
588
+ if (!expanded) {
589
+ console.error(chalk.red(`Unknown observe alias: ${alias}`));
590
+ process.exit(1);
591
+ }
592
+ if (process.stderr.isTTY)
593
+ process.stderr.write(chalk.gray(`${expanded.note}\n`));
594
+ await program.parseAsync(['node', 'agents', ...expanded.argv]);
595
+ };
596
+ program
597
+ .command('inbox')
598
+ .description('Needs-you inbox (alias of `agents feed`). Open blocks waiting on you.')
599
+ .allowUnknownOption()
600
+ .allowExcessArguments()
601
+ .action(async () => {
602
+ await reparse('inbox');
603
+ });
604
+ program
605
+ .command('timeline')
606
+ .description('Agent progress stream (alias of `agents feed --filter updates`). What agents posted recently.')
607
+ .allowUnknownOption()
608
+ .allowExcessArguments()
609
+ .action(async () => {
610
+ await reparse('timeline');
611
+ });
575
612
  }
576
613
  /**
577
614
  * Mirror a written post to the configured sinks (`feed.broadcast` in
@@ -11,7 +11,8 @@
11
11
  * native harness registry. The `agents profiles` tree stays unchanged.
12
12
  */
13
13
  import type { Command } from 'commander';
14
- import { type Profile } from '../lib/profiles.js';
14
+ import { type AddProfileOptions } from './profiles.js';
15
+ import { type Profile, type ForkProfileOptions } from '../lib/profiles.js';
15
16
  /**
16
17
  * Print one custom harness. Shared by `agents harness view <name>` and by
17
18
  * `agents view <name>` — a custom harness resolves as an agent type there, so
@@ -25,9 +26,25 @@ export interface ForkOptions {
25
26
  authProvider?: string;
26
27
  version?: string;
27
28
  description?: string;
29
+ /** `<bundle>` or `<bundle>:<key>` — see {@link applyFromSecrets} in ./profiles.js. */
30
+ fromSecrets?: string;
28
31
  keyStdin?: boolean;
29
32
  force?: boolean;
30
33
  }
34
+ /** Options accepted by `agents harness edit`. */
35
+ export interface EditOptions {
36
+ model?: string;
37
+ baseUrl?: string;
38
+ authProvider?: string;
39
+ /** Empty string ('') unpins the host CLI version. */
40
+ version?: string;
41
+ description?: string;
42
+ /** Empty string ('') clears the fallback model. */
43
+ fallbackModel?: string;
44
+ /** `<bundle>` or `<bundle>:<key>` — see {@link applyFromSecrets} in ./profiles.js. */
45
+ fromSecrets?: string;
46
+ keyStdin?: boolean;
47
+ }
31
48
  /**
32
49
  * Build the new harness for `agents harness fork <source> <name>`.
33
50
  *
@@ -37,4 +54,26 @@ export interface ForkOptions {
37
54
  * copy a model from.
38
55
  */
39
56
  export declare function buildFork(source: string, name: string, opts: ForkOptions): Profile;
57
+ /** Map `agents harness edit` flags onto {@link ForkProfileOptions} — the same
58
+ * override shape `editProfile`/`forkProfile` apply. `--version ''` (unpin) is
59
+ * handled by the caller ({@link buildEdit}), not here: `forkProfile`'s own
60
+ * ternary treats an empty string as "no override" and would otherwise inherit
61
+ * the source's version instead of clearing it. */
62
+ export declare function buildEditOverrides(opts: EditOptions): ForkProfileOptions;
63
+ /** True when at least one recognized edit flag was given. */
64
+ export declare function hasEditFlags(opts: EditOptions): boolean;
65
+ /**
66
+ * Build the edited harness for `agents harness edit <name>` — the profile-shape
67
+ * transform only; the caller applies `--from-secrets`/`--auth-provider` (async
68
+ * keychain side effects) and persists with `writeProfile`. Mirrors
69
+ * {@link buildFork}'s split between a pure builder and the action's IO.
70
+ */
71
+ export declare function buildEdit(name: string, opts: EditOptions): Profile;
72
+ /** True when `agents harness fork` was given too little to proceed without the wizard. */
73
+ export declare function forkNeedsWizard(source: string | undefined, name: string | undefined, opts: ForkOptions): boolean;
74
+ /** True when `agents harness add` was given too little to proceed without the wizard.
75
+ * Mirrors {@link addProfile}'s own error condition in ./profiles.js so a bare
76
+ * `agents harness add <preset-name>` (no flags) still resolves via the preset
77
+ * fallback instead of being routed into the wizard. */
78
+ export declare function addNeedsWizard(name: string | undefined, opts: AddProfileOptions): boolean;
40
79
  export declare function registerHarnessCommands(program: Command): void;