@phnx-labs/agents-cli 1.20.50 → 1.20.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/commands/browser-picker.js +1 -18
- package/dist/commands/cloud.js +1 -25
- package/dist/commands/computer.d.ts +1 -0
- package/dist/commands/computer.js +129 -8
- package/dist/commands/exec.js +49 -6
- package/dist/commands/factory.js +1 -4
- package/dist/commands/inspect.js +1 -11
- package/dist/commands/mcp.js +2 -6
- package/dist/commands/message.js +1 -4
- package/dist/commands/profiles.js +1 -18
- package/dist/commands/repo.js +33 -14
- package/dist/commands/resource-view.d.ts +1 -0
- package/dist/commands/resource-view.js +5 -17
- package/dist/commands/secrets.d.ts +1 -0
- package/dist/commands/secrets.js +1 -28
- package/dist/commands/sessions-picker.js +1 -18
- package/dist/commands/sessions.js +6 -8
- package/dist/commands/teams-picker.js +1 -32
- package/dist/commands/teams.js +1 -27
- package/dist/commands/tmux.js +1 -3
- package/dist/commands/view.js +1 -9
- package/dist/commands/worktree.js +1 -4
- package/dist/lib/agents.d.ts +0 -4
- package/dist/lib/agents.js +20 -33
- package/dist/lib/auto-dispatch-linear.d.ts +18 -0
- package/dist/lib/auto-dispatch-linear.js +107 -0
- package/dist/lib/auto-dispatch-provider.d.ts +10 -0
- package/dist/lib/auto-dispatch-provider.js +25 -0
- package/dist/lib/auto-dispatch.d.ts +87 -0
- package/dist/lib/auto-dispatch.js +142 -0
- package/dist/lib/browser/cdp.js +11 -2
- package/dist/lib/browser/drivers/ssh.d.ts +28 -10
- package/dist/lib/browser/drivers/ssh.js +57 -18
- package/dist/lib/browser/refs.js +1 -5
- package/dist/lib/cli-resources.d.ts +0 -2
- package/dist/lib/cli-resources.js +30 -13
- package/dist/lib/cloud/rush.d.ts +0 -24
- package/dist/lib/cloud/rush.js +0 -31
- package/dist/lib/crabbox/cli.js +4 -1
- package/dist/lib/crabbox/lease.js +29 -1
- package/dist/lib/daemon.js +41 -0
- package/dist/lib/exec.js +31 -14
- package/dist/lib/format.d.ts +38 -0
- package/dist/lib/format.js +108 -0
- package/dist/lib/git.d.ts +21 -0
- package/dist/lib/git.js +92 -0
- package/dist/lib/hooks/cache.d.ts +9 -2
- package/dist/lib/hooks/cache.js +220 -8
- package/dist/lib/hooks.js +17 -8
- package/dist/lib/platform/exec.d.ts +4 -1
- package/dist/lib/platform/exec.js +8 -2
- package/dist/lib/resources.d.ts +0 -8
- package/dist/lib/resources.js +0 -10
- package/dist/lib/runner.js +10 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/session/active.d.ts +11 -1
- package/dist/lib/session/active.js +3 -0
- package/dist/lib/session/db.d.ts +1 -4
- package/dist/lib/session/db.js +20 -25
- package/dist/lib/session/discover.d.ts +2 -2
- package/dist/lib/session/discover.js +61 -48
- package/dist/lib/session/parse.js +35 -34
- package/dist/lib/session/render.d.ts +7 -3
- package/dist/lib/session/render.js +15 -9
- package/dist/lib/session/state.d.ts +55 -0
- package/dist/lib/session/state.js +87 -10
- package/dist/lib/session/types.d.ts +9 -0
- package/dist/lib/shims.d.ts +9 -3
- package/dist/lib/shims.js +44 -8
- package/dist/lib/ssh-tunnel.d.ts +33 -2
- package/dist/lib/ssh-tunnel.js +94 -7
- package/dist/lib/staleness/types.d.ts +0 -1
- package/dist/lib/types.d.ts +14 -1
- package/dist/lib/versions.d.ts +0 -26
- package/dist/lib/versions.js +2 -145
- package/dist/lib/warn-unpushed.d.ts +40 -0
- package/dist/lib/warn-unpushed.js +128 -0
- package/package.json +3 -1
- package/dist/lib/resources/index.d.ts +0 -53
- package/dist/lib/resources/index.js +0 -76
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.20.51
|
|
6
|
+
|
|
7
|
+
- **Fix: `agents run --lease` bootstraps a fresh crabbox image and no longer leaks the box after the run.** Three failures compounded on a stock Hetzner lease (Ubuntu 24.04, no node preinstalled): (1) the bootstrap's `npm install -g @phnx-labs/agents-cli` ran with no node/npm on the box and swallowed the failure with `|| true`, so every run died deep in the script with `agents: command not found` (exit 127) and no hint why; (2) even with the CLI installed, a fresh install refuses `agents run` with "agents-cli is not set up" until `agents setup` has run; (3) teardown called `crabbox stop --id <slug>`, but crabbox's `stop` takes a positional target (unlike `status`/`run`/`ssh`) and died with `flag provided but not defined: -id` — silently, because `crabboxStop` is best-effort — so every one-shot lease box was **kept, billed, and left carrying the run's working data** until someone noticed (`Box … kept` instead of destroyed). The bootstrap now: exports `~/.local/bin` onto PATH, installs node user-level from the official `latest-v22.x` tarball when missing (arch-aware, satisfies `engines.node >=22.5.0`, no sudo needed), points the npm prefix at `~/.local`, fails loud with exit 96 and a diagnostic when the CLI still isn't runnable, and runs `agents setup` behind the same `[ ! -d ~/.agents/.system ]` first-run guard the hosts bootstrap uses; `crabboxStop` passes the slug positionally. Verified live on a fresh Hetzner cpx62 by the run's own progression across builds: the pre-fix lease exited 127 (`agents: command not found`) with `Box … kept`; after the node/npm fix it reached `agents-cli is not set up`; after the setup fix it reached the agent's login check (`Not logged in`); and every post-fix run ends with `Box <slug> destroyed.` instead of leaking. Source: `apps/cli/src/lib/crabbox/lease.ts` (`ENSURE_AGENTS_CLI`, `buildBootstrapScript`), `apps/cli/src/lib/crabbox/cli.ts` (`crabboxStop`). Known follow-up: leasing a **Claude** runtime from a Mac whose Claude Code credential lives in the login Keychain (the default install, and any agents-cli managed home — service name is hash-suffixed) still lands "Not logged in" on the box, because the picker copies `~/.claude.json` (config/state) but not the OAuth token, and extracting the token from the Keychain needs an interactive ACL approval; tracked separately.
|
|
8
|
+
- **`agents repo pull user <git-url>` now git-backs a plain `~/.agents` instead of silently skipping it — fixing config sync on Windows/fresh machines.** Setup only ever git-clones the *system* repo (`~/.agents/.system/`); the user repo is created as a bare directory (`state.ts ensureAgentsDir`), so `~/.agents` is git-backed only where it was cloned by hand as a dotfiles step. On a box where that never happened (a fresh install, or Windows), `agents repo pull` just printed `user: not a git repo, skipping` and the machine silently fell out of config sync — no `rules/`, no `agents sync` of shared resources. Now, passing your config remote once — `agents repo pull user git@github.com:you/.agents.git` — **adopts** the existing directory in place: it clones your remote and moves the `.git` in without deleting anything, materializes the tracked resources it was missing, and **backs up any locally-modified tracked file** (e.g. a machine-specific `agents.yaml`) to a sibling `~/.agents.pre-adopt-backup/` before overwriting it. Untracked runtime state (`.cache/`, `.history/`, `.system/` — all gitignored) is never touched. Every subsequent `agents repo pull` / `agents sync` is plain (the remote is now `origin`). No new command; the URL is only needed the first time. SSH transport is preserved (a `git@…` URL clones over SSH, not a rewritten https that would hang on a private-repo credential prompt), and git never prompts (`GIT_TERMINAL_PROMPT=0`). Source: `apps/cli/src/lib/git.ts` (`adoptRepo`), `apps/cli/src/commands/repo.ts`.
|
|
9
|
+
- **`agents run` now warns when a headless run leaves committed-but-unpushed work, instead of stranding it silently.** A headless `agents run` in a writable mode (`edit`/`skip`/`auto`) could end with the agent having committed on a branch but never pushed it — the run's exit path did no git work, so those commits sat invisible in a worktree until someone audited the box (exactly how a batch dispatch loop can quietly lose a verified fix). After a non-interactive, writable run the CLI now inspects the cwd for commits on the current branch that haven't reached any remote (`git log HEAD --not --remotes`, correct even when no upstream is set — work already on an `origin/*` ref is not flagged) and prints a loud stderr warning naming the branch, the unpushed commits, and the exact `git push` / `gh pr create` commands. Advisory only: it never pushes, never mutates the repo, and never throws (a 5s git timeout plus full error-swallowing guarantee it can't delay or break the run's exit). The check is wired into every headless exit path — single run, `--loop`, `--acp`, `--resume-checkpoint`, and the crash/catch path — and gated by `shouldWarnUnpushed(mode, interactive)` so it stays silent for interactive runs (the human sees their shell) and read-only `plan` mode. Source: `apps/cli/src/lib/warn-unpushed.ts`, `apps/cli/src/commands/exec.ts`. (#868)
|
|
10
|
+
- **Codex mode flags now match what the mode names promise — only `--mode skip` is yolo.** `--mode edit` used to append `--dangerously-bypass-approvals-and-sandbox` (Codex's `--yolo`) alongside `--sandbox workspace-write`, and the bypass flag wins — so "edit" silently ran Codex with **no sandbox and no approvals**, verified against codex 0.142.5's own session banner (`sandbox: danger-full-access`). And `--mode plan` mapped to `workspace-write` (writable!) because the template predated Codex's `read-only` sandbox. Now: `plan` → `--sandbox read-only`, `edit` → `--sandbox workspace-write -c sandbox_workspace_write.network_access=true` (sandboxed writes, network on so git/gh/installs keep working, no approval bypass), `skip` → `--dangerously-bypass-approvals-and-sandbox` (unchanged — skip IS the gnarly mode, equivalent to `codex --yolo`). Same fix in routine jobs (`runner.ts`) and in headless `codex exec resume`, which used to get the bypass for ANY non-plan resume — it now maps plan/edit through `-c sandbox_mode=…` and reserves the bypass for skip; interactive `codex resume` now carries the mode's sandbox flags instead of none. Verified live per mode against codex 0.142.5 session banners: skip = `approval: never / sandbox: danger-full-access`, edit = `sandbox: workspace-write (network access enabled)`, plan = `sandbox: read-only`. Source: `apps/cli/src/lib/exec.ts` (`AGENT_COMMANDS.codex`, resume block), `apps/cli/src/lib/runner.ts` (`buildJobCommand`).
|
|
11
|
+
- **`--add-dir` is now forwarded to Codex (it was silently dropped).** `agents teams` passes `--add-dir ~/.agents` so Codex teammates can run `agents teams add`, but `buildExecCommand` emitted `--add-dir` for Claude only — the grant never reached Codex, masked until now by edit mode's accidental sandbox bypass. Codex takes `--add-dir` natively (widens the workspace-write sandbox); it is now forwarded for fresh runs and skipped on resume (`codex exec resume` rejects it). Source: `apps/cli/src/lib/exec.ts`.
|
|
12
|
+
- **Fix: the documented `agents run <agent> [prompt] -- <native flags>` passthrough works again.** commander ≥13 rejects excess operands by default, so any post-`--` token (e.g. `agents run codex -- --yolo`) died with `too many arguments` before the run started. The run command now allows excess operands, re-derives the `--` boundary from argv (a post-`--` token can never be mis-parsed as the prompt — `agents run codex -- --yolo` launches the TUI with `--yolo`, it doesn't headless-run the "prompt" `--yolo`), and still errors, with a hint to quote the prompt, on excess operands NOT behind `--`. Verified live: `agents run codex "…" -- --yolo` forwards `--yolo` and codex reports `sandbox: danger-full-access`. Source: `apps/cli/src/commands/exec.ts`.
|
|
13
|
+
|
|
14
|
+
- **Fix: grok launch shims resolve the binary from the versioned home before the global `~/.grok/downloads`, so a pinned grok that installed into the versioned home no longer dies with "grok@<version> not installed."** Grok ships a native binary (not an npm package), and it lands in the versioned home's `.grok/downloads` whenever the installer runs with `GROK_HOME` set — via the shim, a correct `agents add grok`, or a grok self-update from within the shim. Both generated shims (the dispatcher in `generateShimScript` and the `grok@<version>` versioned alias in `generateVersionedAliasScript`) checked only `$HOME/.grok/downloads`, which was often empty, so they fell through to the "not installed" error even though the binary existed in the versioned home. `getBinaryPath` already checked the versioned home first, so `agents view` and the shims disagreed. Both shim blocks now check `$VERSION_DIR/home/.grok/downloads` first and fall back to the global `$HOME/.grok/downloads` for pre-fix installs, then the existing adopted-launcher/PATH last resort. Bumps `SHIM_SCHEMA_VERSION` 25→26 and `VERSIONED_ALIAS_SCHEMA_VERSION` 12→13 so existing on-disk grok shims regenerate. Supersedes the pre-monorepo #830. Source: `apps/cli/src/lib/shims.ts` (`generateShimScript` grok dispatcher block, `generateVersionedAliasScript` `binaryResolution`).
|
|
15
|
+
- **Fix: `browser stop --host <windows>` tree-kills the remote browser — relaunches never wedge on a stale `SingletonLock`.** The kill script used `Stop-Process` on the CDP port owner only; orphaned Chromium child processes survived, kept the profile's `SingletonLock` held, and the next `browser start --host` against the same profile exited immediately as a second instance. The script now uses `taskkill /PID <owner> /T /F` to take down the whole process tree. Source: `apps/cli/src/lib/browser/drivers/ssh.ts` (`buildWindowsKillScript`). (GitHub #561)
|
|
16
|
+
- **Fix: `agents browser start --host <windows>` actually serves CDP now — the remote browser launches in the user's interactive session instead of session 0.** The Windows launch used WMI `Win32_Process.Create` (chosen so the browser outlives the ssh session), but a WMI-created process lands in session 0, where Edge binds the debugging port yet its DevTools server never initializes — every `/json/version` probe hung forever and `DevToolsActivePort` was never written, so `browser start --host` failed with a connection error on every attempt. The launch is now a one-shot scheduled task registered and started by the logged-on user: it survives ssh disconnect the same way, runs in the interactive session where DevTools comes up normally, and is unregistered immediately after start. The launch args also gained the same automation-modal suppressors the local launcher has (`--no-first-run --no-default-browser-check --hide-crash-restore-bubble --disable-session-crashed-bubble`) — without them a relaunch against a previously hard-killed profile triggers session-restore churn that closes the CDP page target mid-command. Verified live against win-mini (Edg/150). Source: `apps/cli/src/lib/browser/drivers/ssh.ts` (`buildWindowsLaunchScript`). (GitHub #561)
|
|
17
|
+
- **Fix: remote CDP no longer dies on large payloads — screenshots of content-rich pages over `browser --host` work.** The CDP client rode the platform (undici) WebSocket, which enforces a non-configurable max decompressed message size; a `Page.captureScreenshot` response for a content-rich page blew past it and the socket closed with 1006 ("Max decompressed message size exceeded") while the command was pending, surfacing as "CDP connection closed". The websocket transport now uses the `ws` client (no permessage-deflate offer by default, explicit 256MB `maxPayload`); the local pipe transport is unchanged. Source: `apps/cli/src/lib/browser/cdp.ts`. (GitHub #561)
|
|
18
|
+
- **`agents sessions <id> --json` now exposes the ExitPlanMode plan markdown as a top-level field, and the shape changed from a bare event array to `{ session, events }`.** The session-state engine already detected plan-review (`awaitingReason: 'plan_review'`) off a trailing `ExitPlanMode` tool call, but the plan markdown itself was dropped on the floor — forcing every consumer that wanted it (the Factory NEEDS-YOU panel via `parsePlanFromClaudeJsonl`, external dashboards) to re-open the raw JSONL and scan for the same tool call. That "extension re-implements the session engine" gap now closes at the source: the state engine surfaces `state.plan` alongside `awaitingReason`, the Claude scanner captures the plan text at scan time and persists it to `sessions.db` (schema v11, additive, rescan-on-migrate), it's exposed as `plan` on `SessionMeta` in every `agents sessions --json` row, and `agents sessions <id> --json` now emits `{ session: SessionMeta, events: SessionEvent[] }` so the plan is one top-level `output.session.plan` read instead of a needle-in-haystack scan. Verified live against a real Claude session with an ExitPlanMode event: `agents sessions 74464df7 --json` prints the plan markdown at `.session.plan`. Source: `apps/cli/src/lib/session/{state,discover,db,render,types}.ts`, `apps/cli/src/lib/session/active.ts`, `apps/cli/src/commands/sessions.ts`. (issue #743 / RUSH-1505)
|
|
19
|
+
- **`agents computer` Windows parity: scoped screenshots, `get-text --max-chars`, `status`/`reload --host`, and honest `--background`/`--require-frontmost` handling (#548).** Four params the CLI already sent were silently ignored by the Windows daemon. (1) **Screenshots are now pid-scoped like macOS** — `screenshot --list` enumerates the target pid's top-level windows (`window_id` is the Win32 HWND, the same id `raise --window-id` takes), the default capture crops to the pid's largest on-screen window, `--window-id` shoots one window, and `--display` captures the display the app is on; previously every capture was the whole virtual desktop. Verified live on win-mini: window capture 2097x984/28KB vs full display 2560x1440/380KB. (2) **`get-text --max-chars` is honored** (default stays 20k, ceiling 200k like macOS) — `--max-chars 100` now returns exactly 100 chars. (3) **`status --host <device>` and `reload --host <device>`** — status reports the recorded tunnel plus a live daemon probe (previously it misreported macOS-local install state for a remote Windows daemon); reload restarts the daemon's scheduled task (the way to pick up a freshly pushed exe) and confirms it answers through the tunnel. (4) **`--require-frontmost` is enforced on Windows** — `SendInput` lands in the *focused* window, so `type-text`/`key` now report `frontmost` (feeding the existing CLI warning) and the flag hard-fails with `not_frontmost` when the target isn't foreground; **`--background` is rejected** with `action_unsupported` instead of silently no-oping (macOS postToPid delivery has no Win32 analogue — element-mode clicks via UIA patterns are the focus-safe path). Source: `native/computer-win/{Screenshot,Automation}.cs`, `apps/cli/src/commands/computer.ts`, `apps/cli/docs/computer.md`.
|
|
20
|
+
- **The `never` prompt-policy is now live — the signed keychain helper was rebuilt, re-notarized, and re-pinned.** `agents secrets create --policy never --i-understand` stores bundle values with no biometry ACL (`kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`, device-local, non-synchronizable) so headless automation can read them with zero Touch ID prompts. The Swift `set-no-acl` path shipped in #682 but the pinned helper binary predated it, so the policy failed against the shipped helper; the helper is rebuilt from current source, notarized (Apple submission `a2373c91-7fc2-4894-a801-b37c111597aa`, status Accepted, stapled, Gatekeeper `Notarized Developer ID`), and `scripts/Agents CLI.app.sha256` re-pinned to the new binary. (GitHub #421)
|
|
21
|
+
- **Consolidated ~30 copy-pasted terminal-formatting helpers into one shared `apps/cli/src/lib/format.ts`, fixing three user-visible drifts at the source.** `die`, `truncate`, `relTime`, `humanDuration`, `visibleWidth`, `padRight`/`padVisible`, `isJsonMode`, `readStdinSync`, and `termLink` had drifted into per-command copies with different behavior; every consumer now imports the single canonical version. Three normalizations are user-visible: (1) the truncation ellipsis is now the single glyph `…` everywhere — `agents cloud` task lists, `agents sessions` overflow, and session prompt/tool summaries previously showed ASCII `...` or a bare `.`; (2) `agents cloud` relative timestamps switch from the long "5 minutes ago" form to the compact "5m ago" form already used by `agents teams`; (3) the `agents teams` picker duration cell gains a space ("2h5m" → "2h 5m") to match the sessions/browser pickers. It also fixes a latent bug: `agents repo`'s divergence-table column alignment used a `visibleWidth` regex missing its `\x1b` escape, so ANSI-colored cells were mis-measured and columns could misalign — the canonical `visibleWidth` strips the full SGR sequence. `lib/events.ts`'s `truncate` (a distinct nullable, exported helper that truncates persisted event payloads) and the domain-specific `statusColor` copies (different status vocabularies with conflicting color assignments) are deliberately left in place. Internal refactor plus the noted string normalizations; no other behavior change. Source: `apps/cli/src/lib/format.ts` and consumers across `apps/cli/src/commands/` and `apps/cli/src/lib/`. (GitHub #753 / RUSH-1515)
|
|
22
|
+
- **`agents computer setup --host` now works from a plain `npm i -g` install — the Windows helper exe downloads on demand from GitHub releases.** The ~157MB `computer-helper-win.exe` never shipped in the npm tarball, so setup died with "Windows helper exe not built. Run: bash scripts/build-win.sh" for anyone without a repo checkout. On `v*` tags the `computer-helper-win.yml` workflow now builds the self-contained exe, smoke-tests it, and uploads it plus a `.sha256` as GitHub release assets; `setup --host` resolves a local build first, then downloads the asset for the exact running CLI version, verifies its sha256 against the published checksum, and caches it under `~/.agents/.cache/computer/win-helper/v<version>/`. A tag with no asset is a hard error naming that tag — never a silent fallback to a different release. (GitHub #547) Source: `apps/cli/src/lib/ssh-tunnel.ts` (`ensureWinHelperExe`, `downloadWinHelperExe`), `.github/workflows/computer-helper-win.yml` (`release-exe`).
|
|
23
|
+
- **`registerMcp` HTTP transport now routes through the capability table instead of an inline agent-id allowlist.** MCP-over-HTTP support and MCP-header support were gated by hardcoded `agentId !== 'claude' && agentId !== 'codex' && agentId !== 'gemini'` / `agentId !== 'claude'` checks in `apps/cli/src/lib/agents.ts`, bypassing the `capabilities.ts`/`supports()` table that is the single source of truth for "which agent supports what." A newly-added agent would silently get the wrong HTTP-MCP behavior with no compile-time signal. Two new capabilities land on the `AgentConfig` matrix — `mcpHttp` (Claude/Codex/Gemini today) and `mcpHeaders` (Claude only) — and both inline allowlists are replaced with `supports(agentId, cap)` calls. Pure refactor: `capableAgents('mcpHttp')` is `['claude','codex','gemini']` and `capableAgents('mcpHeaders')` is `['claude']`, matching the pre-change behavior exactly. Source: `apps/cli/src/lib/{agents,capabilities,types}.ts`, `apps/cli/src/lib/{agents,capabilities}.test.ts`, `apps/cli/src/lib/__tests__/capabilities.test.ts`. (issue #742 / RUSH-1504)
|
|
24
|
+
- **Hook `matches:` predicates are now enforced at fire time — the documented gating was inert.** A hook manifest entry could declare `matches:` predicates (`prompt_contains` / `prompt_matches` / `tool_name` / `tool_args_match` / `cwd_includes` / `project_has` / `git_dirty`) to gate when it fires, and the docs described the gate ("all predicates AND together; an empty block always fires"), but `shouldFire()` (the evaluator in `src/lib/hooks/match.ts`) had **zero runtime callers**: the agent execs the registered command directly and nothing evaluated `matches:`, so any hook with a `matches:` block fired unconditionally. A hook that declares `matches:` (with or without `cache:`) is now registered as a generated wrapper shim that evaluates the predicates against the event JSON on stdin before running the script — a non-matching event exits 0 without running the hook body (logged as `cache:"skip"`), a matching event runs it. Matches-only hooks (no `cache:`) get a gate-only pass-through shim; cached hooks apply the gate before the cache. The shim gate is a faithful port of `shouldFire()` (same AND semantics, same ReDoS guard) and is pinned to it by a 20-case conformance test so the two can't drift. Gating is fail-open: a garbled predicate runs the hook rather than silently disabling a safety hook (e.g. `git-guard`). No installed or bundled hook currently declares `matches:`, so this changes no existing hook's behavior — it activates a documented feature for authors who add one. Verified end-to-end by generating a shim from a `matches: { tool_name: Bash, tool_args_match: "rm -rf" }` manifest and firing it: a `Read` event and a `Bash`+`ls` event were skipped, a `Bash`+`rm -rf` event ran the body. Source: `apps/cli/src/lib/hooks/cache.ts` (`renderShim` gate + pass-through tail), `apps/cli/src/lib/hooks.ts` (`resolveHookCommand`), `apps/cli/docs/hooks.md`. (RUSH-1506)
|
|
25
|
+
- **Browser-over-SSH no longer hangs on an unreachable remote host — it fails fast (~10s).** The raw-`ssh` spawns in the browser SSH driver (`ensureRemoteBrowser`, `runSSHCommand`) passed only `-o BatchMode=yes`, with no `ConnectTimeout`, so a dropped SYN to a down host stalled on the OS default TCP timeout (~127s) instead of erroring. Both call sites now compose the shared hardened baseline `SSH_OPTS` from `ssh-exec.ts` (`BatchMode` + `ConnectTimeout=10` + `ServerAlive` keepalive) rather than re-listing options — the same baseline `sshExec` and the `-L` tunnel already use. The options now also precede the target (matching `sshExec`); on macOS/BSD `getopt` an option placed after the target is swallowed into the remote command instead of applied. Verified against TEST-NET `203.0.113.1` (guaranteed unreachable): `-o BatchMode=yes` alone was still hanging at a 30s cap (en route to ~127s), while the `SSH_OPTS` set failed in `10.04s` with `connect to host 203.0.113.1 port 22: Connection timed out`. Source: `apps/cli/src/lib/browser/drivers/ssh.ts`. (RUSH-1508)
|
|
26
|
+
- **Fix: OpenCode sessions now load on Windows.** Reading OpenCode sessions shelled out to the `sqlite3` CLI at three call sites — `parseOpenCode` (transcript parse) plus the session scan and active-account lookup in discovery — and that binary is absent on Windows, so OpenCode sessions silently never appeared in `agents sessions` there. All three now read through the same runtime-aware node/bun `Database` wrapper the Antigravity parser already uses (`bun:sqlite`/`node:sqlite`, no native addon, no CLI), and the OpenCode transcript query binds the session id as a parameter instead of interpolating it. No behavior change on macOS/Linux. Source: `apps/cli/src/lib/session/parse.ts` (`parseOpenCode`), `apps/cli/src/lib/session/discover.ts` (`scanOpenCodeIncremental`, `getOpenCodeAccount`). (RUSH-1513)
|
|
27
|
+
- **`agents sessions --active --json` now carries the agent's actual decision, not a truncated status line.** A session waiting on you used to collapse everything to a one-line `preview` — an `AskUserQuestion` became the generic `"Asked you a question"` (throwing away the options that are already in the tool input), and a trailing thinking block masked the real turn as `"thinking…"`. The state engine now emits a structured `question` object (`{ text, reason, options: [{ label, description, key }] }`) for every waiting path — `AskUserQuestion` (with each option's 1-based select key), plan review, permission (Approve=`1` / Deny=`esc`), and a trailing prose question — plus a short assistant `tail` for context, and `preview` no longer degrades to `"thinking…"` when a real turn exists. Every consumer (the Factory NEEDS-YOU panel, teams, cloud) now gets the real "what does it want from me" instead of re-deriving it from prose. Verified live: the blocked session in the screenshot now reports `awaitingReason: question` with the real question text. Source: `apps/cli/src/lib/session/state.ts` (`structuredQuestionFromAsk`, `inferActivity`), `apps/cli/src/lib/session/active.ts`. (RUSH-453)
|
|
28
|
+
|
|
5
29
|
## 1.20.50
|
|
6
30
|
|
|
7
31
|
- **Distributed agent teams: teammates can now run on different machines across your fleet, not just the box running `teams start`.** A single team can place the backend teammate on a Linux box and the UI teammate on a Mac while one orchestrator still drives the DAG, polls status, and cleans up. One vocabulary, all optional (omit it and teams stay 100% local as before): `teams create --devices a,b,c` (alias `--hosts`) declares a pool the team may auto-schedule onto, `--repo <url|path>` (defaults to the local checkout's `origin`) says how each device gets the code, and `teams add --device X` (alias `--host`) pins one teammate to a host — which needs **no** pool, so "send just one teammate elsewhere" is zero-setup. Placement resolves top-down at launch: explicit `--device` pin → single-device pool (whole team there) → multi-device pool (least-loaded auto-schedule) → local. Remote teammates dispatch over SSH via the existing `agents devices`/host machinery (a third teammate backend beside local and cloud), are monitored by offset-tailing the remote log + `.exit` sentinel, and get the repo auto-provisioned per device (reuse an existing checkout, else clone into `~/.agents/repos/<team>`) with an optional per-teammate git worktree on the host. `teams status`/`teams logs` show each teammate's host and stream its output back with the local mirror capped (~512KB rolling tail) so a 10+-teammate fleet can't blow up the orchestrator. POSIX hosts only in v1 (Windows rejected with a clear message). Source: `apps/cli/src/lib/teams/{scheduler,remoteWorktree,agents,api,supervisor,registry}.ts`, `apps/cli/src/lib/hosts/{progress,passthrough}.ts`, `apps/cli/src/commands/teams.ts`, `apps/cli/docs/teams.md`.
|
|
@@ -5,29 +5,12 @@
|
|
|
5
5
|
* Shows task name, profile, domains, and tab count; preview pane shows full tab list.
|
|
6
6
|
*/
|
|
7
7
|
import chalk from 'chalk';
|
|
8
|
+
import { truncate, humanDuration } from '../lib/format.js';
|
|
8
9
|
import { itemPicker } from '../lib/picker.js';
|
|
9
10
|
const DOT = chalk.gray(' · ');
|
|
10
|
-
function humanDuration(ms) {
|
|
11
|
-
const s = Math.floor(ms / 1000);
|
|
12
|
-
if (s < 60)
|
|
13
|
-
return `${s}s`;
|
|
14
|
-
const m = Math.floor(s / 60);
|
|
15
|
-
if (m < 60)
|
|
16
|
-
return `${m}m`;
|
|
17
|
-
const h = Math.floor(m / 60);
|
|
18
|
-
const mm = m % 60;
|
|
19
|
-
if (h < 24)
|
|
20
|
-
return mm ? `${h}h ${mm}m` : `${h}h`;
|
|
21
|
-
const d = Math.floor(h / 24);
|
|
22
|
-
const hh = h % 24;
|
|
23
|
-
return hh ? `${d}d ${hh}h` : `${d}d`;
|
|
24
|
-
}
|
|
25
11
|
function formatAge(ms) {
|
|
26
12
|
return humanDuration(Date.now() - ms) + ' ago';
|
|
27
13
|
}
|
|
28
|
-
function truncate(s, max) {
|
|
29
|
-
return s.length > max ? s.slice(0, max - 1) + '…' : s;
|
|
30
|
-
}
|
|
31
14
|
/** Build the preview pane for a browser task. */
|
|
32
15
|
export function buildBrowserPreview(item) {
|
|
33
16
|
const { task, profile } = item;
|
package/dist/commands/cloud.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { die, relTime, truncate, isJsonMode } from '../lib/format.js';
|
|
2
3
|
import * as fs from 'fs';
|
|
3
4
|
import * as path from 'path';
|
|
4
5
|
import ora from 'ora';
|
|
@@ -35,28 +36,6 @@ function parseSkillRef(raw) {
|
|
|
35
36
|
return { id: raw };
|
|
36
37
|
}
|
|
37
38
|
/** Print an error message to stderr and exit. */
|
|
38
|
-
function die(msg, code = 1) {
|
|
39
|
-
console.error(chalk.red(msg));
|
|
40
|
-
process.exit(code);
|
|
41
|
-
}
|
|
42
|
-
/** Format an ISO timestamp as a human-readable relative time string. */
|
|
43
|
-
function relTime(iso) {
|
|
44
|
-
const secs = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
|
|
45
|
-
if (secs < 10)
|
|
46
|
-
return 'just now';
|
|
47
|
-
if (secs < 60)
|
|
48
|
-
return `${secs}s ago`;
|
|
49
|
-
if (secs < 3600)
|
|
50
|
-
return `${Math.floor(secs / 60)} minutes ago`;
|
|
51
|
-
if (secs < 86400)
|
|
52
|
-
return `${Math.floor(secs / 3600)} hours ago`;
|
|
53
|
-
return `${Math.floor(secs / 86400)} days ago`;
|
|
54
|
-
}
|
|
55
|
-
function truncate(s, n) {
|
|
56
|
-
if (s.length <= n)
|
|
57
|
-
return s;
|
|
58
|
-
return s.slice(0, n - 1) + '...';
|
|
59
|
-
}
|
|
60
39
|
/** Return a chalk color function appropriate for the given task status. */
|
|
61
40
|
function statusColor(status) {
|
|
62
41
|
switch (status) {
|
|
@@ -70,9 +49,6 @@ function statusColor(status) {
|
|
|
70
49
|
default: return chalk.white;
|
|
71
50
|
}
|
|
72
51
|
}
|
|
73
|
-
function isJsonMode(opts) {
|
|
74
|
-
return Boolean(opts.json) || !process.stdout.isTTY;
|
|
75
|
-
}
|
|
76
52
|
/**
|
|
77
53
|
* After a `MissingTargetError`, try to resolve the target interactively.
|
|
78
54
|
* Returns the chosen id, or undefined when no interactive resolution is
|
|
@@ -37,5 +37,6 @@ export declare function reconcileScreenshotExt(outPath: string, buf: Buffer): {
|
|
|
37
37
|
};
|
|
38
38
|
export declare function registerComputerCommand(program: Command): void;
|
|
39
39
|
export declare function registerComputerSubcommands(program: Command): void;
|
|
40
|
+
export declare function buildRestartTaskScript(taskName: string, exeName: string): string;
|
|
40
41
|
export { resolveHelperExec as resolveHelperPath };
|
|
41
42
|
export { resolveSocketPath };
|
|
@@ -4,7 +4,9 @@ import * as os from 'os';
|
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import { registerCommandGroups } from '../lib/help.js';
|
|
6
6
|
import { openComputerClient, resolveHelperApp, resolveHelperExec, resolveSocketPath, resolveLogPath, resolvePolicyPath, resolvePeersPath, resolveTcpEndpoint, loadComputerAllowList, loadDefaultPeers, writeComputerPolicy, writeComputerPeers, } from '../lib/computer-rpc.js';
|
|
7
|
-
import { setupRemoteHelper, startRemoteTunnel, stopRemoteHelper, hydrateRemoteEnvFromState, } from '../lib/ssh-tunnel.js';
|
|
7
|
+
import { setupRemoteHelper, startRemoteTunnel, stopRemoteHelper, hydrateRemoteEnvFromState, readRemoteState, resolveRemoteDevice, REMOTE_TASK_NAME, WIN_HELPER_EXE, } from '../lib/ssh-tunnel.js';
|
|
8
|
+
import { sshExec } from '../lib/ssh-exec.js';
|
|
9
|
+
import { encodePowershell } from '../lib/hosts/remote-cmd.js';
|
|
8
10
|
import { registerActionCommands, withClient, unwrap, pickTarget } from './computer-actions.js';
|
|
9
11
|
import { runComputerLoop } from '../lib/computer/loop.js';
|
|
10
12
|
import { makeVerbDispatcher } from '../lib/computer/dispatch.js';
|
|
@@ -18,9 +20,10 @@ const COMPUTER_HELP_GROUPS = [
|
|
|
18
20
|
{ title: 'Interact', names: ['launch', 'raise', 'click', 'right-click', 'type', 'type-text', 'key', 'drag', 'scroll', 'ax-action', 'focus', 'wait'] },
|
|
19
21
|
];
|
|
20
22
|
// Subcommands that manage the `--host` remote path themselves (provisioning /
|
|
21
|
-
// tunnel lifecycle
|
|
22
|
-
//
|
|
23
|
-
|
|
23
|
+
// tunnel lifecycle, or daemon-state reporting that must degrade gracefully
|
|
24
|
+
// when no tunnel is recorded). Every other `--host`-bearing subcommand is a
|
|
25
|
+
// plain verb that just needs the TCP endpoint hydrated before it runs.
|
|
26
|
+
const REMOTE_LIFECYCLE = new Set(['setup', 'start', 'stop', 'status', 'reload']);
|
|
24
27
|
/**
|
|
25
28
|
* Pure platform gate. The computer subsystem is macOS-only for LOCAL driving
|
|
26
29
|
* (Accessibility / launchctl). It is NOT blocked off macOS when a remote daemon
|
|
@@ -111,8 +114,13 @@ export function registerComputerSubcommands(program) {
|
|
|
111
114
|
function registerStatusCommand(program) {
|
|
112
115
|
program
|
|
113
116
|
.command('status')
|
|
114
|
-
.description('Report install state, daemon state, and Accessibility trust')
|
|
115
|
-
.
|
|
117
|
+
.description('Report install state, daemon state, and Accessibility trust — or a remote Windows daemon with --host')
|
|
118
|
+
.option('--host <device>', 'Report the remote Windows daemon (tunnel + liveness) instead of the local helper')
|
|
119
|
+
.action(async (opts) => {
|
|
120
|
+
if (opts.host) {
|
|
121
|
+
await reportRemoteStatus(opts.host);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
116
124
|
const socketPath = resolveSocketPath();
|
|
117
125
|
const installed = fs.existsSync(HELPER_APP_DEST);
|
|
118
126
|
const socketUp = fs.existsSync(socketPath);
|
|
@@ -159,6 +167,108 @@ function registerStatusCommand(program) {
|
|
|
159
167
|
}
|
|
160
168
|
});
|
|
161
169
|
}
|
|
170
|
+
// status --host: the local checks (app install, launchd socket, policy files)
|
|
171
|
+
// are macOS concepts — a remote Windows daemon is reported from what actually
|
|
172
|
+
// exists for it: the recorded tunnel and a live trust_status probe through it.
|
|
173
|
+
async function reportRemoteStatus(host) {
|
|
174
|
+
console.log(`host: ${host}`);
|
|
175
|
+
const state = readRemoteState(host);
|
|
176
|
+
if (!state) {
|
|
177
|
+
console.log('tunnel: none');
|
|
178
|
+
console.log('daemon: unknown (no tunnel to probe through)');
|
|
179
|
+
console.log('');
|
|
180
|
+
console.log(`Run: agents computer start --host ${host}`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
console.log(`tunnel: 127.0.0.1:${state.localPort} -> ${state.target} (127.0.0.1:${state.remotePort})`);
|
|
184
|
+
hydrateRemoteEnvFromState(host);
|
|
185
|
+
try {
|
|
186
|
+
const client = openComputerClient();
|
|
187
|
+
try {
|
|
188
|
+
const r = await client.call('trust_status');
|
|
189
|
+
if (r.error) {
|
|
190
|
+
console.error(`error: ${r.error.code}: ${r.error.message}`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
console.log('daemon: running');
|
|
194
|
+
console.log(`trust: ${r.result?.trusted ? 'granted' : 'denied'} (Windows UIAutomation needs no per-app grant)`);
|
|
195
|
+
if (typeof r.result?.pid === 'number')
|
|
196
|
+
console.log(`pid: ${r.result.pid}`);
|
|
197
|
+
if (typeof r.result?.path === 'string' && r.result.path)
|
|
198
|
+
console.log(`exe: ${r.result.path}`);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
await client.close();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
console.log('daemon: unreachable');
|
|
206
|
+
console.log(` ${err.message}`);
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log(`Run: agents computer start --host ${host}`);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// PowerShell to bounce the remote daemon: kill the running exe (tolerating
|
|
213
|
+
// "not running"), then start the LOGON scheduled task that owns its
|
|
214
|
+
// lifecycle. Pure so tests can assert the exact script (mirrors the
|
|
215
|
+
// ssh-tunnel script builders).
|
|
216
|
+
export function buildRestartTaskScript(taskName, exeName) {
|
|
217
|
+
const procName = exeName.replace(/\.exe$/i, '');
|
|
218
|
+
return [
|
|
219
|
+
`$ErrorActionPreference = 'Stop'`,
|
|
220
|
+
`Stop-Process -Name '${procName}' -Force -ErrorAction SilentlyContinue`,
|
|
221
|
+
`Start-ScheduledTask -TaskName '${taskName}'`,
|
|
222
|
+
`Write-Output 'restarted'`,
|
|
223
|
+
].join('; ');
|
|
224
|
+
}
|
|
225
|
+
// reload --host: the Windows daemon has no policy file to re-read (it
|
|
226
|
+
// enforces no allow-list — see TrustStatus in native/computer-win/Rpc.cs), so
|
|
227
|
+
// reload means bounce the daemon via its scheduled task — the way to pick up
|
|
228
|
+
// a freshly pushed exe — then prove it answers through the recorded tunnel.
|
|
229
|
+
async function reloadRemoteHelper(host) {
|
|
230
|
+
const { target } = await resolveRemoteDevice(host);
|
|
231
|
+
const script = buildRestartTaskScript(REMOTE_TASK_NAME, WIN_HELPER_EXE);
|
|
232
|
+
const res = sshExec(target, `powershell -NoProfile -NonInteractive -EncodedCommand ${encodePowershell(script)}`, { timeoutMs: 60_000 });
|
|
233
|
+
if (res.code !== 0) {
|
|
234
|
+
const msg = (res.stderr || res.stdout || '').trim();
|
|
235
|
+
console.error(`restart failed on ${target}${res.timedOut ? ' (timed out)' : ''}${msg ? `: ${msg}` : ''}`);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
console.log(`task: restarted "${REMOTE_TASK_NAME}" on ${target}`);
|
|
239
|
+
const state = readRemoteState(host);
|
|
240
|
+
if (!state) {
|
|
241
|
+
console.log(`(no tunnel recorded — run \`agents computer start --host ${host}\` to drive it)`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
hydrateRemoteEnvFromState(host);
|
|
245
|
+
// The relaunched daemon needs a beat to rebind its port; poll through the
|
|
246
|
+
// tunnel until it answers.
|
|
247
|
+
const deadline = Date.now() + 15_000;
|
|
248
|
+
let lastErr = '';
|
|
249
|
+
while (Date.now() < deadline) {
|
|
250
|
+
try {
|
|
251
|
+
const client = openComputerClient();
|
|
252
|
+
try {
|
|
253
|
+
const r = await client.call('trust_status');
|
|
254
|
+
if (!r.error && r.result) {
|
|
255
|
+
console.log(`reloaded: daemon answering (pid ${r.result.pid ?? '?'})`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
lastErr = r.error ? `${r.error.code}: ${r.error.message}` : 'empty result';
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
await client.close();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
lastErr = err.message;
|
|
266
|
+
}
|
|
267
|
+
await sleep(500);
|
|
268
|
+
}
|
|
269
|
+
console.error(`daemon did not answer within 15s after restart${lastErr ? ` (${lastErr})` : ''}`);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
162
272
|
// run — the embedded observe -> act -> verify agent loop. A reasoning model
|
|
163
273
|
// (Claude API by default, or any Anthropic-shaped endpoint via --base-url for
|
|
164
274
|
// Ollama / vLLM / LiteLLM) drives the EXISTING computer verbs as tools over the
|
|
@@ -560,8 +670,19 @@ function registerStartCommand(program) {
|
|
|
560
670
|
function registerReloadCommand(program) {
|
|
561
671
|
program
|
|
562
672
|
.command('reload')
|
|
563
|
-
.description('Reload the allow-list policy
|
|
564
|
-
.
|
|
673
|
+
.description('Reload the allow-list policy (SIGHUP the local daemon) — or restart a remote Windows daemon with --host')
|
|
674
|
+
.option('--host <device>', 'Restart the remote Windows daemon (its scheduled task) instead of SIGHUPing the local one')
|
|
675
|
+
.action(async (opts) => {
|
|
676
|
+
if (opts.host) {
|
|
677
|
+
try {
|
|
678
|
+
await reloadRemoteHelper(opts.host);
|
|
679
|
+
}
|
|
680
|
+
catch (err) {
|
|
681
|
+
console.error(`error: ${err.message}`);
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
565
686
|
const socketPath = resolveSocketPath();
|
|
566
687
|
if (!fs.existsSync(socketPath)) {
|
|
567
688
|
console.error(`daemon not running (no socket at ${socketPath})`);
|
package/dist/commands/exec.js
CHANGED
|
@@ -11,6 +11,7 @@ import { setHelpSections } from '../lib/help.js';
|
|
|
11
11
|
import { parseLoopInterval } from '../lib/loop.js';
|
|
12
12
|
import { AGENTS } from '../lib/agents.js';
|
|
13
13
|
import { recordDispatchedRun } from '../lib/audit/log.js';
|
|
14
|
+
import { warnUnpushedWork, shouldWarnUnpushed } from '../lib/warn-unpushed.js';
|
|
14
15
|
import * as fs from 'fs';
|
|
15
16
|
import * as path from 'path';
|
|
16
17
|
import * as os from 'os';
|
|
@@ -211,7 +212,7 @@ export function registerRunCommand(program) {
|
|
|
211
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.')
|
|
212
213
|
.option('--allow-expired', 'Inject secrets even if their expiry date has passed (overrides the pre-run expiry abort).')
|
|
213
214
|
.option('--cwd <dir>', 'Working directory for the agent (defaults to current directory)')
|
|
214
|
-
.option('--add-dir <dir>', 'Grant access to an additional directory outside the project (Claude
|
|
215
|
+
.option('--add-dir <dir>', 'Grant access to an additional directory outside the project (Claude and Codex, repeatable)', (val, prev) => [...prev, val], [])
|
|
215
216
|
.option('--json', 'Stream events as JSON lines (for parsing by other tools)')
|
|
216
217
|
.option('--quiet', 'Suppress preamble (rotation banner, "Running:" line). Useful when piping JSON events to a parser.', false)
|
|
217
218
|
.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)
|
|
@@ -245,6 +246,12 @@ export function registerRunCommand(program) {
|
|
|
245
246
|
// `--on` and `--computer` are hidden aliases of `--host` — same behavior.
|
|
246
247
|
runCmd.addOption(new Option('--on <name>', 'Alias of --host.').hideHelp());
|
|
247
248
|
runCmd.addOption(new Option('--computer <name>', 'Alias of --host.').hideHelp());
|
|
249
|
+
// Required for the documented `agents run <agent> [prompt] -- <native flags>`
|
|
250
|
+
// passthrough: commander >=13 rejects excess operands by default, so any
|
|
251
|
+
// post-`--` token died with "too many arguments" before the action ran. The
|
|
252
|
+
// action re-derives the `--` boundary from rawArgs and still errors on excess
|
|
253
|
+
// operands that are NOT behind `--`.
|
|
254
|
+
runCmd.allowExcessArguments(true);
|
|
248
255
|
setHelpSections(runCmd, {
|
|
249
256
|
examples: `
|
|
250
257
|
# Headless, read-only: investigate or summarize without writing files
|
|
@@ -293,10 +300,25 @@ export function registerRunCommand(program) {
|
|
|
293
300
|
`,
|
|
294
301
|
});
|
|
295
302
|
runCmd.action(async (agentSpec, prompt, options, command) => {
|
|
296
|
-
// Capture everything after -- as passthrough args forwarded verbatim to the
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
303
|
+
// Capture everything after -- as passthrough args forwarded verbatim to the
|
|
304
|
+
// underlying CLI. Commander strips the literal `--` and folds what follows
|
|
305
|
+
// into the positional operands (so `agents run codex -- --yolo` would parse
|
|
306
|
+
// `--yolo` as the PROMPT) — recover the boundary from rawArgs instead of
|
|
307
|
+
// operand counts. Excess operands not behind `--` are still an error (an
|
|
308
|
+
// unquoted prompt must not silently become agent flags).
|
|
309
|
+
const rawArgs = process.argv;
|
|
310
|
+
const separatorIdx = rawArgs.indexOf('--');
|
|
311
|
+
const passthroughArgs = separatorIdx === -1 ? [] : rawArgs.slice(separatorIdx + 1);
|
|
312
|
+
const operandsBeforeSeparator = command.args.length - passthroughArgs.length;
|
|
313
|
+
if (operandsBeforeSeparator > 2) {
|
|
314
|
+
console.error(chalk.red(`Too many arguments for 'run'. Quote the prompt ("fix the bug"), and put agent-native flags after -- (agents run codex -- --yolo).`));
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
if (prompt !== undefined && operandsBeforeSeparator < 2) {
|
|
318
|
+
// The token commander assigned to [prompt] came from behind `--` — it is
|
|
319
|
+
// a native flag, not a prompt. Run interactively.
|
|
320
|
+
prompt = undefined;
|
|
321
|
+
}
|
|
300
322
|
// --lease: invent a disposable cloud box for this run (via crabbox), run
|
|
301
323
|
// the agent there, then tear it down. Unlike --host, nothing is registered.
|
|
302
324
|
if (options.lease) {
|
|
@@ -342,7 +364,7 @@ export function registerRunCommand(program) {
|
|
|
342
364
|
secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE,
|
|
343
365
|
keep: options.keepBox,
|
|
344
366
|
});
|
|
345
|
-
console.error(chalk.gray(toreDown ? `Box ${box.slug} destroyed.` : `Box ${box.slug} kept${box.ip ? ` (${box.ip})` : ''}. Stop it: crabbox stop
|
|
367
|
+
console.error(chalk.gray(toreDown ? `Box ${box.slug} destroyed.` : `Box ${box.slug} kept${box.ip ? ` (${box.ip})` : ''}. Stop it: crabbox stop ${box.slug}`));
|
|
346
368
|
process.exit(exitCode === null ? 1 : exitCode);
|
|
347
369
|
}
|
|
348
370
|
catch (err) {
|
|
@@ -568,6 +590,9 @@ export function registerRunCommand(program) {
|
|
|
568
590
|
cwd: resumeExec.cwd ?? process.cwd(),
|
|
569
591
|
exitCode: resumeExit,
|
|
570
592
|
});
|
|
593
|
+
// A resumed loop is always headless; surface any commits it left unpushed.
|
|
594
|
+
if (shouldWarnUnpushed(resumeExec.mode ?? 'auto', false))
|
|
595
|
+
await warnUnpushedWork(resumeExec.cwd ?? process.cwd());
|
|
571
596
|
process.exit(resumeExit);
|
|
572
597
|
}
|
|
573
598
|
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported }, { splitBundleRef, resolveSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports },] = await Promise.all([
|
|
@@ -1279,6 +1304,9 @@ export function registerRunCommand(program) {
|
|
|
1279
1304
|
// Governance chokepoint (#347): the --acp path exits here, bypassing
|
|
1280
1305
|
// the normal finalize below — record its one audit entry.
|
|
1281
1306
|
recordDispatchedRun({ agent, version: defaultVersion ?? 'unknown', mode, cwd, exitCode });
|
|
1307
|
+
// ACP headless run always has a prompt; surface any unpushed commits.
|
|
1308
|
+
if (shouldWarnUnpushed(mode, false))
|
|
1309
|
+
await warnUnpushedWork(cwd);
|
|
1282
1310
|
process.exit(exitCode);
|
|
1283
1311
|
}
|
|
1284
1312
|
catch (err) {
|
|
@@ -1435,6 +1463,9 @@ export function registerRunCommand(program) {
|
|
|
1435
1463
|
// the normal finalize below — record its one audit entry.
|
|
1436
1464
|
const loopExit = loopExitCode(result.stoppedBy);
|
|
1437
1465
|
recordDispatchedRun({ agent, version: defaultVersion ?? 'unknown', mode, cwd, exitCode: loopExit });
|
|
1466
|
+
// A loop is always headless; surface any commits it left unpushed.
|
|
1467
|
+
if (shouldWarnUnpushed(mode, false))
|
|
1468
|
+
await warnUnpushedWork(cwd);
|
|
1438
1469
|
process.exit(loopExit);
|
|
1439
1470
|
}
|
|
1440
1471
|
catch (err) {
|
|
@@ -1462,6 +1493,13 @@ export function registerRunCommand(program) {
|
|
|
1462
1493
|
}
|
|
1463
1494
|
cleanupWorkflowMcpConfig();
|
|
1464
1495
|
cleanupWorkflowSubagents();
|
|
1496
|
+
// Surface committed-but-unpushed work a headless writable run left
|
|
1497
|
+
// behind, so it isn't silently stranded in a worktree. Advisory only,
|
|
1498
|
+
// never throws; skipped for interactive runs (the human sees the shell)
|
|
1499
|
+
// and read-only plan mode (can't commit).
|
|
1500
|
+
if (shouldWarnUnpushed(mode, resolveInteractive(execOptions))) {
|
|
1501
|
+
await warnUnpushedWork(cwd);
|
|
1502
|
+
}
|
|
1465
1503
|
// Governance chokepoint (#347): every dispatched run finalizes here.
|
|
1466
1504
|
// ONE tamper-evident audit record per run — non-fatal by contract.
|
|
1467
1505
|
recordDispatchedRun({ agent: ranAgent, version: ranVersion ?? 'unknown', mode, cwd, exitCode });
|
|
@@ -1470,6 +1508,11 @@ export function registerRunCommand(program) {
|
|
|
1470
1508
|
catch (err) {
|
|
1471
1509
|
cleanupWorkflowMcpConfig();
|
|
1472
1510
|
cleanupWorkflowSubagents();
|
|
1511
|
+
// An agent that committed then crashed is the case where stranded work
|
|
1512
|
+
// matters most — warn before exiting the error path too.
|
|
1513
|
+
if (shouldWarnUnpushed(mode, resolveInteractive(execOptions))) {
|
|
1514
|
+
await warnUnpushedWork(cwd);
|
|
1515
|
+
}
|
|
1473
1516
|
console.error(chalk.red(`Failed to execute ${agent}: ${err.message}`));
|
|
1474
1517
|
process.exit(1);
|
|
1475
1518
|
}
|
package/dist/commands/factory.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { die } from '../lib/format.js';
|
|
2
3
|
import * as fs from 'fs';
|
|
3
4
|
import * as path from 'path';
|
|
4
5
|
import { homedir } from 'os';
|
|
5
6
|
import { betaEnableHint, isBetaEnabled } from '../lib/beta.js';
|
|
6
7
|
import { insertTask } from '../lib/cloud/store.js';
|
|
7
|
-
function die(msg, code = 1) {
|
|
8
|
-
console.error(chalk.red(msg));
|
|
9
|
-
process.exit(code);
|
|
10
|
-
}
|
|
11
8
|
function requireFactoryUrl() {
|
|
12
9
|
const url = process.env.FACTORY_FLOOR_URL;
|
|
13
10
|
if (!url) {
|
package/dist/commands/inspect.js
CHANGED
|
@@ -17,6 +17,7 @@ import * as fs from 'fs';
|
|
|
17
17
|
import * as os from 'os';
|
|
18
18
|
import * as path from 'path';
|
|
19
19
|
import chalk from 'chalk';
|
|
20
|
+
import { truncate, termLink } from '../lib/format.js';
|
|
20
21
|
import * as yaml from 'yaml';
|
|
21
22
|
import { AGENTS, getCliState, resolveAgentName } from '../lib/agents.js';
|
|
22
23
|
import { supports } from '../lib/capabilities.js';
|
|
@@ -1216,12 +1217,6 @@ function readFirstProseLine(p) {
|
|
|
1216
1217
|
return '';
|
|
1217
1218
|
}
|
|
1218
1219
|
// ─── OSC-8 + path helpers ────────────────────────────────────────────────────
|
|
1219
|
-
function termLink(text, filePath) {
|
|
1220
|
-
if (!filePath || !process.stdout.isTTY)
|
|
1221
|
-
return text;
|
|
1222
|
-
const url = `file://${filePath}`;
|
|
1223
|
-
return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
|
|
1224
|
-
}
|
|
1225
1220
|
function linkTarget(p) {
|
|
1226
1221
|
if (!p)
|
|
1227
1222
|
return '';
|
|
@@ -1271,8 +1266,3 @@ function safeCountSessions(agent) {
|
|
|
1271
1266
|
return 0;
|
|
1272
1267
|
}
|
|
1273
1268
|
}
|
|
1274
|
-
function truncate(s, n) {
|
|
1275
|
-
if (s.length <= n)
|
|
1276
|
-
return s;
|
|
1277
|
-
return s.slice(0, n - 1) + '…';
|
|
1278
|
-
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { truncate } from '../lib/format.js';
|
|
2
3
|
import ora from 'ora';
|
|
3
4
|
import { checkbox } from '@inquirer/prompts';
|
|
4
5
|
import { capableAgents, isCapable } from '../lib/capabilities.js';
|
|
@@ -764,7 +765,7 @@ function buildMcpRows(opts) {
|
|
|
764
765
|
const displayCommand = declaredCommand || firstCommand;
|
|
765
766
|
rows.push({
|
|
766
767
|
name,
|
|
767
|
-
description: displayCommand ?
|
|
768
|
+
description: displayCommand ? truncate(displayCommand, 60) : '',
|
|
768
769
|
extra: source,
|
|
769
770
|
targets,
|
|
770
771
|
buildDetail: () => formatMcpDetail(name, source, centralConfig, manifestConfig, displayCommand, targets),
|
|
@@ -811,8 +812,3 @@ function formatMcpDetail(name, source, centralConfig, manifestConfig, command, t
|
|
|
811
812
|
lines.push(buildTargetsSection(targets));
|
|
812
813
|
return lines.join('\n');
|
|
813
814
|
}
|
|
814
|
-
function truncateString(s, max) {
|
|
815
|
-
if (s.length <= max)
|
|
816
|
-
return s;
|
|
817
|
-
return s.slice(0, max - 1) + '…';
|
|
818
|
-
}
|
package/dist/commands/message.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { die } from '../lib/format.js';
|
|
2
3
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
3
4
|
import { getTaskById, updateTaskStatus } from '../lib/cloud/store.js';
|
|
4
5
|
import { resolveProvider } from '../lib/cloud/registry.js';
|
|
5
6
|
import { mailboxDir, enqueue } from '../lib/mailbox.js';
|
|
6
7
|
import { resolveMessageTarget } from '../lib/mailbox-target.js';
|
|
7
|
-
function die(msg, code = 1) {
|
|
8
|
-
console.error(chalk.red(msg));
|
|
9
|
-
process.exit(code);
|
|
10
|
-
}
|
|
11
8
|
export function registerMessageCommand(program) {
|
|
12
9
|
program
|
|
13
10
|
.command('message <target> <text>')
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* through a standard agent CLI with no local proxy.
|
|
8
8
|
*/
|
|
9
9
|
import chalk from 'chalk';
|
|
10
|
-
import
|
|
10
|
+
import { readStdinSync } from '../lib/format.js';
|
|
11
11
|
import { spawn } from 'child_process';
|
|
12
12
|
import { listProfiles, readProfile, writeProfile, deleteProfile, profileExists, profileFromPreset, validateProfileName, getPresetForProfile, } from '../lib/profiles.js';
|
|
13
13
|
import { getPreset, listPresets, expandPreset } from '../lib/profiles-presets.js';
|
|
@@ -41,23 +41,6 @@ async function promptForSecret(message) {
|
|
|
41
41
|
return await password({ message, mask: true });
|
|
42
42
|
}
|
|
43
43
|
/** Read all available data from stdin synchronously, trimmed. */
|
|
44
|
-
function readStdinSync() {
|
|
45
|
-
const chunks = [];
|
|
46
|
-
const buf = Buffer.alloc(65536);
|
|
47
|
-
while (true) {
|
|
48
|
-
let bytesRead;
|
|
49
|
-
try {
|
|
50
|
-
bytesRead = fs.readSync(0, buf, 0, buf.length, null);
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
|
-
if (bytesRead === 0)
|
|
56
|
-
break;
|
|
57
|
-
chunks.push(Buffer.from(buf.subarray(0, bytesRead)));
|
|
58
|
-
}
|
|
59
|
-
return Buffer.concat(chunks).toString('utf-8').trim();
|
|
60
|
-
}
|
|
61
44
|
/** Ensure a provider API key exists in keychain, prompting or reading stdin if missing. */
|
|
62
45
|
async function ensureProviderToken(provider, signupUrl, fromStdin) {
|
|
63
46
|
const item = keychainItemName(provider);
|