@phnx-labs/agents-cli 1.22.68 → 1.22.69
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 +30 -0
- package/README.md +14 -6
- package/dist/bootstrap.js +3 -0
- package/dist/commands/exec.js +31 -21
- package/dist/commands/feed.js +20 -7
- package/dist/commands/monitors.js +3 -0
- package/dist/commands/projects.d.ts +26 -6
- package/dist/commands/projects.js +55 -22
- package/dist/commands/send.js +29 -2
- package/dist/commands/sessions-inject.d.ts +58 -0
- package/dist/commands/sessions-inject.js +143 -7
- package/dist/commands/sessions-picker.js +1 -0
- package/dist/commands/share.js +43 -14
- package/dist/commands/ssh.js +205 -2
- package/dist/lib/accounting/usage.d.ts +7 -2
- package/dist/lib/accounting/usage.js +142 -10
- package/dist/lib/boot-profile.d.ts +14 -0
- package/dist/lib/boot-profile.js +66 -0
- package/dist/lib/channels/providers/desktop.d.ts +5 -4
- package/dist/lib/channels/providers/desktop.js +5 -4
- package/dist/lib/claude-account-token.js +108 -4
- package/dist/lib/devices/health.d.ts +38 -2
- package/dist/lib/devices/health.js +43 -5
- package/dist/lib/devices/worker-pick.d.ts +1 -1
- package/dist/lib/devices/worker-pick.js +4 -1
- package/dist/lib/exec.js +4 -0
- package/dist/lib/feed-broadcast.d.ts +64 -5
- package/dist/lib/feed-broadcast.js +124 -22
- package/dist/lib/monitors/engine.js +18 -0
- package/dist/lib/monitors/sources/command.js +13 -3
- package/dist/lib/monitors/sources/failure.d.ts +32 -0
- package/dist/lib/monitors/sources/failure.js +52 -0
- package/dist/lib/monitors/sources/types.d.ts +9 -0
- package/dist/lib/owner-message.d.ts +12 -0
- package/dist/lib/owner-message.js +44 -0
- package/dist/lib/run-trace-sync.d.ts +15 -0
- package/dist/lib/run-trace-sync.js +43 -21
- package/dist/lib/secrets/filestore.d.ts +4 -0
- package/dist/lib/secrets/filestore.js +164 -3
- package/dist/lib/session/active.d.ts +10 -0
- package/dist/lib/session/active.js +3 -0
- package/dist/lib/session/db.d.ts +12 -1
- package/dist/lib/session/db.js +20 -1
- package/dist/lib/session/discover.js +81 -1
- package/dist/lib/session/linear.d.ts +13 -0
- package/dist/lib/session/linear.js +44 -0
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/parse.js +2 -3
- package/dist/lib/session/prompt.d.ts +7 -1
- package/dist/lib/session/prompt.js +12 -2
- package/dist/lib/session/recovery.d.ts +21 -12
- package/dist/lib/session/recovery.js +29 -11
- package/dist/lib/session/remote/watch.js +5 -2
- package/dist/lib/session/state.js +11 -13
- package/dist/lib/share/backend.d.ts +2 -2
- package/dist/lib/share/backend.js +20 -9
- package/dist/lib/share/delete.d.ts +5 -1
- package/dist/lib/share/delete.js +7 -2
- package/dist/lib/share/http-error.d.ts +52 -0
- package/dist/lib/share/http-error.js +65 -0
- package/dist/lib/share/publish.d.ts +13 -3
- package/dist/lib/share/publish.js +19 -15
- package/dist/lib/share/worker-template.js +5 -1
- package/dist/lib/smart-launch.js +27 -4
- package/dist/lib/storage/index.d.ts +14 -0
- package/dist/lib/storage/index.js +14 -0
- package/dist/lib/storage/selection.d.ts +48 -0
- package/dist/lib/storage/selection.js +39 -0
- package/dist/lib/storage/visibility.d.ts +82 -0
- package/dist/lib/storage/visibility.js +99 -0
- package/dist/lib/teams/agents.js +3 -1
- package/dist/lib/teams/placement-probe.js +1 -0
- package/dist/lib/teams/scheduler.d.ts +8 -1
- package/dist/lib/teams/scheduler.js +4 -1
- package/dist/lib/traces/backend.js +13 -2
- package/dist/lib/worktree/held.d.ts +166 -0
- package/dist/lib/worktree/held.js +368 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.69
|
|
4
|
+
|
|
5
|
+
- **`agents monitors` no longer fires an action on a poll FAILURE (PHNX-3510).** An `[on-change]` monitor treated a failing poll — a `gh pr list … | jq` that intermittently printed `GraphQL: API rate limit already exceeded …` — as a value change: the empty→error→empty flap read as two changes and dispatched a full agent run each time on a premise that was false (~10 sessions burned re-verifying two already-merged PRs). A command/poll source now flags a snapshot that exited non-zero, or whose output matches a transport/auth/rate-limit error shape (caught even on exit 0, since a pipe to jq swallows the failing half's exit code), as an *observation failure*: the engine skips it, leaves watched-state untouched, does not fire, and records it as a failed check so a sustained streak escalates to the owner as a drought. `agents monitors test` labels a failed poll and reports `Would fire: no`. Note: a non-zero exit is now an observation failure rather than a watchable value — a monitor that wants to watch a command's success/failure should emit a stable token (`… && echo UP || echo DOWN`, always exit 0) so the state it cares about diffs as a value; sustained failures still reach the owner via the drought escalation. Source: `cli/src/lib/monitors/sources/failure.ts`, `cli/src/lib/monitors/sources/command.ts`, `cli/src/lib/monitors/engine.ts`, `cli/docs/automation.md`.
|
|
6
|
+
|
|
7
|
+
- **`agents fleet worktrees` surfaces the worktree-sweep's held set as buckets, not a silent count (PHNX-3520).** The nightly `worktree-sweep` (PHNX-3503, in `phnx-labs/.agents`) reclaims merged agent worktrees and correctly HOLDS anything dirty, unmerged, or undeterminable — but it only ever emitted a `held=<n>` count, so the held set became a permanent, growing residue nothing resolved. Measured 2026-08-30 it is consistently LARGER than the reclaimed set (~800 worktrees fleet-wide), and one bucket inside it is real stranded work: a worktree whose branch carries commits on no remote (the PHNX-2951 / PHNX-2732 class — an agent finished, its commits never reached a remote, nothing surfaced them). The new read-only command classifies every held worktree into its three buckets — **`unmerged-commits`** (the one that matters), **`uncommitted-changes`**, and **`undeterminable`** (`status-unreadable` / `merge-state-unknown`, failing closed exactly as the sweep does) — and reports each with device/repo/worktree/branch/reason/age/size. `--json` is the structured form for machine callers; `--fleet` fans out over every online device and aggregates; `--bucket <name>` filters. The safe recovery action for the stranded bucket is `--push`: it PUBLISHES each `unmerged-commits` branch that is on no remote (re-reading the merge state immediately before pushing and refusing anything already on origin), making the work visible — it never removes a worktree or a branch. The destructive reclaim stays in the sweep; this is the surfacing half the sweep can consume. Source: `cli/src/lib/worktree/held.ts`, `cli/src/commands/ssh.ts`, `cli/src/lib/worktree/held.test.ts`.
|
|
8
|
+
|
|
9
|
+
- **`agents run` starts ~0.6s faster on a box with many secrets bundles (PHNX-3585).** The AGI EXT "New Claude" boot runs `agents run claude --interactive`, and the whole wrapper cost is spent BEFORE the harness prints anything. Profiling that pre-exec window (new `AGENTS_PROFILE_BOOT=1` per-stage tracer, `cli/src/lib/boot-profile.ts`) showed the dominant stage was `--strategy balanced` account rotation: `readAccountRegistry` → `listBundles` decrypted EVERY file-store bundle's metadata to find the account bundles, and each decrypt runs a fresh scrypt KDF (~12ms) because every `.enc` file carries its own salt — ~690ms of pure KDF on a box with ~40 bundles, on every launch. Bundle metadata is non-secret by contract (it holds `keychain:`/`env:`/literal refs, never the secret bytes, and is already stored no-ACL so `secrets list` enumerates without Touch ID), so the file store now caches the decrypted metadata JSON keyed by each `.enc` file's `(mtime,size)` plus a passphrase fingerprint. Any bundle write (new mtime/size) or passphrase change (new fingerprint; rotation re-writes every file) misses and re-derives — no staleness, self-healing on any miss — and secret VALUE items never enter the cache. Measured on a worker box: `agents run claude` pre-exec `resolve-version` dropped from ~690ms to ~31ms (warm), total pre-exec ~1040ms → ~370ms. A committed `cli/scripts/bench-boot.sh` keeps it measured. Source: `cli/src/lib/secrets/filestore.ts`.
|
|
10
|
+
|
|
11
|
+
- **Grok's cheap scan now fills `firstUserMessage` from a bounded `chat_history.jsonl` prefix read (PHNX-3621).** The summary-only path never parsed the log. The prefix reader skips Grok `<user_info>` / `synthetic_reason` scaffolding, prefers the first `prompt_index` user turn, and unwraps `<user_query>`. Existing Grok rows backfill through `CONTENT_INDEX_VERSION` v4. Source: `cli/src/lib/session/discover.ts`.
|
|
12
|
+
|
|
13
|
+
- **Worker homes provisioned before seed-on-attach self-heal their account identity at resolve time (PHNX-3660).** `resolveClaudeSetupToken` required the version home's `.claude.json` to carry `oauthAccount.emailAddress`; homes whose `.oauth_token` was written by a pre-fix `accounts attach` had none, so exec-env injection and per-account usage reads were silently inert for them (they authenticated only via the shim's `.oauth_token` fallback). The resolver now recovers the email by matching the home's `.oauth_token` value against the `auth` bundle (re-encode-guarded slug decode, so a lossy or unmatched token can never invent a mapping), writes it back with `seedClaudeWorkerHomeIdentity` so the home converges after one read, and still fails closed — writing nothing — when the token matches no bundle key. Ambient no-home resolves never rewrite the operator's real `~/.claude.json`. Source: `cli/src/lib/claude-account-token.ts`.
|
|
14
|
+
|
|
15
|
+
- **Resume `/continue` no longer launches the exhausted origin login when the pick is a provider account (PHNX-3674).** After native-first rotation (PHNX-3626), a usage-limited origin whose transcript is not in the origin home (trash/backup/reinstall, or local fallback from an unreachable peer) fell through to `/continue` on the provider-inclusive pool without carrying `RecoveryAccount`, so exec authenticated as the rate-limited native login. That continue target now injects the healthy provider the same way native rotation does. Source: `cli/src/lib/session/recovery.ts`, `cli/src/commands/exec.ts`.
|
|
16
|
+
|
|
17
|
+
- **Private share OG covers are no longer cacheable by shared proxies (PHNX-3676).** Token-gated `visibility=private` pages already served HTML as `Cache-Control: private, no-store`, but the generated `/user/slug.png` cover used `public, max-age=31536000, immutable` after the token gate. RFC 9111 lets a shared cache reuse a `public` Authorization response for later unauthenticated requests keyed on the unauthenticated URL, so a Bearer fetch of the cover could leak the preview. `managedCoverHeaders` now treats `private` like `me`/`org` (`private, no-store` + `X-Robots-Tag: noindex`). Source: `cli/src/lib/share/worker-template.ts`.
|
|
18
|
+
|
|
19
|
+
- **`agents sessions inject` can nudge a live remote tmux session that has no session id, and no longer crashes on `--pane --device` (PHNX-3688).** Two bugs are fixed. (1) A live session whose `agents sessions --active` id column shows `-` — only a tmux label like `ag-claude-214edaae:0.0` is known — was untargetable: `inject <shortid>` failed `No active session matches`. The lookup now matches the `ag-<agent>-<shortid>` tmux name's suffix (and the full name / pane id), the only selector such a row exposes, and the discovered row carries its `tmuxName` so the match has something to key on. (2) `inject _ "text" --pane %122 --device <box>` crashed with `TypeError: host.startsWith is not a function`: under `optsWithGlobals()` the parent `sessions` command's variadic `-D, --device <target...>` shadows this subcommand's scalar `--device`, so a single `--device box` arrived as `['box']` and the array flowed straight into `sshExec`. It is now coerced to one host (and fails loud on more than one, since inject delivers to exactly one terminal). A bare session id combined with `--device` now resolves ON that device — its tmux panes live there, not on the box you typed the command from — by re-running the inject over SSH, the tool-native form of the old `agents ssh <box> "agents sessions inject <id> …"` workaround. Source: `cli/src/commands/sessions-inject.ts`, `cli/src/lib/session/active.ts`.
|
|
20
|
+
|
|
21
|
+
- **`agents` releases no longer wedge waiting for a human to mint an attestation (PHNX-3696).** RUSH-2666 made an exact-tree attestation for the *release commit* mandatory but shipped no producer for it, so every `release.sh --apply` since 2026-08-15 stopped at `missing exact attestation key` and required an operator to hand-run `release-attestation-produce.sh` and copy the tarball into the store. `attest-main.yml` already attests every push to `main`; the release commit differs from that base only by the version bump, the folded changelog, and the regenerated command index — exactly the set `release-attestation.sh derive` allowlists — so `release.sh` now derives the record itself (`derive_release_attestation`, inheriting the green base's suite via `--inherit-suite-from`, no suite re-run). Soundness is unchanged: `derive` still fails closed if the tree diff touches anything outside that allowlist, so a code change can never inherit a stale pass, and the derive is best-effort — any failure falls through to the previous poll-then-`require`, which still fails loud (the call site guards it with `|| true`, which is load-bearing under `set -euo pipefail`: a bare call returning non-zero would abort the release before that fallback ran). The regression survived review because `release.test.ts` asserted against `release.sh` as *text*; the new coverage executes the real `derive_release_attestation` body against a real git repo and store. Source: `cli/scripts/release.sh`, `cli/scripts/release.test.ts`, `cli/AGENTS.md`.
|
|
22
|
+
|
|
23
|
+
- **Owner phone pings are short and tappable — but only where a sink can render a link (PHNX-3698).** Every owner-bound message goes through one composer (`composeBroadcastMessage`), and the shared `{message}` now surfaces its links per-sink instead of dumping naked URLs after the footer. A **Slack `channel:` sink** gets mrkdwn labeled links `<url|label>`: the `Sent from claude/6fc1db18 on zion` crumb becomes `<https://prix.dev/console/sessions/<full-id>|claude/6fc1db18>` (an 8-char crumb is first upgraded to the full indexed id, since a truncated id would 404), and every `TEAM-N` key the title or body *names* — not just the session's own `ticketId`, so a ping that only mentions `PHNX-3689` in prose is still tappable — becomes `<https://linear.app/<workspace>/issue/<KEY>|<KEY>>` in place. **iMessage, the owner-scoped rush message, `command:` sinks, and every other channel stay plain** — they cannot render a labeled link and a dumped naked URL reads as noise, so the message is the human sentence with no URLs (keys and crumb as text); there is never a trailing URL line on any sink. `agents notify` and `agents send --to owner` route through that same composer on the owner-scoped (plain) path instead of shipping the raw body dump; a non-owner `agents send` is still delivered verbatim. An important/`--blocked` post (and an owner send) fires a best-effort, opt-in background `agents traces sync` so the linked console page exists when a Slack crumb is tapped (gated exactly like the run-exit arm; `AGENTS_NO_TRACE_SYNC=1` opts out). The Linear-key detector is canonical in one place shared with transcript ticket detection (`session/linear.ts`). Source: `cli/src/lib/feed-broadcast.ts`, `cli/src/lib/owner-message.ts`, `cli/src/lib/session/linear.ts`, `cli/src/lib/session/db.ts`, `cli/src/lib/run-trace-sync.ts`, `cli/src/commands/send.ts`, `cli/src/commands/feed.ts`.
|
|
24
|
+
|
|
25
|
+
- **An ordinary CLI release no longer signs or notarizes anything on macOS, and the macOS attestation producer works again (PHNX-3699, PHNX-3631).** `release-attestation-produce.sh` gated its sign+notarize block on `uname == Darwin` alone, with no `--with-helpers` condition — so a CLI-only attestation produced on a Mac codesigned the CLI binary and rebuilt + signed both helper `.app`s, none of which ship in the tarball (RUSH-3026 removed the binary, RUSH-3100 the bundles). That violates the R3 requirement ("no signing, no notarization on the ordinary path") and, because `agents secrets exec apple.com` cannot unlock a Touch-ID-gated bundle headlessly, it *killed* the release outright — hit live cutting 1.22.69 right after PHNX-3696 made `release.sh` produce this attestation itself. The block is now gated on `--with-helpers`, so cutting a helper release still signs and an ordinary CLI release does not. Shipped alongside the **PHNX-3631** fix in the same path: `ATTEST_TMP` used `mktemp ".../agents-cli-attest.XXXXXX.json"`, but BSD/macOS `mktemp` only substitutes trailing X's and treats a template with a suffix after them as a literal filename — so the first call created that exact file and every later call died with `mkstemp failed … File exists`. Now `agents-cli-attest.json.XXXXXX`, which GNU accepts identically; the same pattern in `remote-sign-mac.sh` is fixed too. Together these take `release-attestation-produce.test.ts` on macOS from 22-of-29 failing to 31/31 passing. Source: `cli/scripts/release-attestation-produce.sh`, `cli/scripts/remote-sign-mac.sh`.
|
|
26
|
+
|
|
27
|
+
- **`agents projects view <path>` auto-detects the cwd's project; `projects for-cwd` is gone (PHNX-3704).** `agents projects view [nameOrPath]` now accepts a DIRECTORY as well as a project name. A path-shaped argument — `.`, `..`, a `~`-prefixed value, a `/`-containing value, or an explicit `--path [dir]` (bare `--path` = cwd) — auto-detects the project that contains that directory via the existing path-based `projectNameForCwd` (def-root containment, longest wins); a bare token stays a project name. `view <path> --json` prints `{name, linear:{name,projectId}, root}` in ONE call, so a consumer gets the project name AND its Linear binding without a second `projects list` round-trip. Fail-open: an all-null shape when nothing contains the path, exit 0, never throws. The `projects for-cwd` subcommand and its `formatForCwdOutput` helper are deleted — `view <path> --json` subsumes them. Source: `cli/src/commands/projects.ts`.
|
|
28
|
+
|
|
29
|
+
- **`agents artifacts share` is private by default when signed in, and storage + visibility now live in one shared module.** A publish with no `--visibility` flag is now `me` (owner-only, Phoenix-gated) instead of `public` — signed in, your share is private unless you opt into `--visibility org` (everyone at your email domain, no org to create) or `--visibility public` (the gallery). A BYO (signed-out) publish still defaults to `public`, since the Worker refuses `me`/`org` without a Phoenix owner. The managed-vs-BYO selection policy and the visibility model are now the single `cli/src/lib/storage/` module (`selectStorageBackendKind` + `publishVisibility`/`defaultVisibilityForBackend`), consumed by both `agents artifacts share` (`lib/share/backend.ts`) and `agents traces sync` (`lib/traces/backend.ts`) instead of each re-deriving it — the seam the `sessions` backup surface reuses next. Failed share calls (publish/list/revisions/delete) now surface the Worker's own error — a 400/413/429 shows its `{"error":"…"}` reason and a 429's `Retry-After` — via one bounded extractor (`lib/share/http-error.ts`) that never dumps an arbitrary body. Source: `cli/src/lib/storage/`, `cli/src/lib/share/{backend,publish,delete,http-error}.ts`, `cli/src/commands/share.ts`.
|
|
30
|
+
|
|
31
|
+
- **`agents view grok` shows the last-known usage % again instead of a numberless "run grok once to refresh usage".** Two bugs compounded. (1) `getGrokUsageInfo` read `unified.jsonl` from the home it was given, but `agents view` fetches usage per INSTALLED VERSION, passing each version's isolated home (`~/.agents/.history/versions/grok/<ver>`) — whose `.grok/logs/unified.jsonl` never exists, because Grok writes its billing log only to the user's shared real home `~/.grok`. So every version fell back to the refresh hint while the real reading (e.g. week 42%) sat unread in the shared home. Grok usage now resolves the requested home's log first and falls back to the shared `~/.grok` log where Grok actually writes (a per-version log, if one ever appears, still wins). The shared last line is attributed only to the identity that owns `~/.grok` (matching `auth.json`, or the real home itself) — other version-scoped Grok accounts stay as no-recent-usage rather than inheriting that meter. (2) The usage cache serializer persisted only fresh `windows`, dropping the `staleWindows` Grok's collector pre-partitions an ended-period reading onto — so the daemon-refreshed cache the plain `agents view grok` reads rendered the plan alone (no bar), even right after a `--refresh` had shown "W: 42% · stale". The serializer now persists the union of fresh and stale windows; `deserializeClaudeUsageSnapshot` re-runs the freshness gate on read, so the round-trip is preserved for every collector (Claude, which returns raw windows, is unchanged). Source: `cli/src/lib/accounting/usage.ts`.
|
|
32
|
+
|
|
3
33
|
## 1.22.68
|
|
4
34
|
|
|
5
35
|
Fix OpenCode session history to index and backfill the genuine first user request.
|
package/README.md
CHANGED
|
@@ -416,11 +416,11 @@ agents sessions resume 019fd0c8-b3e9-77a2-a1a4-444698c4d897 # original harness/
|
|
|
416
416
|
agents run auto --resume 019fd0c8-b3e9-77a2-a1a4-444698c4d897 # adapt if its account is unavailable
|
|
417
417
|
```
|
|
418
418
|
|
|
419
|
-
`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 sessions 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 recover on the source device. If the origin version is installed, signed in, healthy, and still owns the indexed transcript, its isolated home performs native resume. Claude launches that native resume from the original project directory recorded before the first turn, so its `projects/<cwd-key>` lookup reaches the conversation even when the session later changed directories.
|
|
419
|
+
`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 sessions 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 recover on the source device. If the origin version is installed, signed in, healthy, and still owns the indexed transcript, its isolated home performs native resume. Claude launches that native resume from the original project directory recorded before the first turn, so its `projects/<cwd-key>` lookup reaches the conversation even when the session later changed directories. When that origin login is usage-limited but the home still owns the transcript, resume stays native and rotates to a healthy injectable provider account of the **same harness**. `/continue <id>` is the last-resort fallback — a signed-out, revoked, trashed, backup-only, or same-number-reinstalled origin, or a limited origin whose transcript is no longer in that home — and still authenticates as a healthy same-harness account (injecting a provider credential when that is the pick, so it never launches the exhausted native login). It never native-resumes from a different isolated home. 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.
|
|
420
420
|
|
|
421
421
|
### Send an agent to the background — and bring it back
|
|
422
422
|
|
|
423
|
-
Running 30 agents and drowning in terminal tabs? `agents sessions detach <id>` stops a session's interactive process and keeps it working **headless** in the background -- it drives its task to done unattended, no tab, lower cost. `agents sessions resume <id>` brings it back through the same origin-device recovery decision: native resume in the exact
|
|
423
|
+
Running 30 agents and drowning in terminal tabs? `agents sessions detach <id>` stops a session's interactive process and keeps it working **headless** in the background -- it drives its task to done unattended, no tab, lower cost. `agents sessions resume <id>` brings it back through the same origin-device recovery decision: native resume in the exact origin home (healthy login, or a rotated injectable account on a usage limit), or same-harness `/continue` as last resort when that home is unavailable, with the full indexed history (including whatever it did while backgrounded).
|
|
424
424
|
|
|
425
425
|
```
|
|
426
426
|
agents sessions detach a1b2c3d4 # go headless in the background, keep working
|
|
@@ -658,6 +658,9 @@ agents fleet status # online/offline rollup + NEEDS ATTENTIO
|
|
|
658
658
|
agents fleet status --verbose # full per-device auth/CLI/sync/version grid
|
|
659
659
|
agents fleet status --live # force a live resource probe (alias of --refresh)
|
|
660
660
|
agents fleet status --json --strict # scriptable fleet health gate
|
|
661
|
+
agents fleet worktrees # held agent worktrees by bucket: stranded (unmerged) · dirty · undeterminable
|
|
662
|
+
agents fleet worktrees --fleet # aggregate the held set across every online device
|
|
663
|
+
agents fleet worktrees --push # publish stranded on-no-remote branches (recovers work; never deletes)
|
|
661
664
|
agents devices harnesses # per device: agent@version · account · signed · quota · ready
|
|
662
665
|
agents devices accounts # same, one row per account (which harnesses share it)
|
|
663
666
|
agents devices harnesses --agents claude,codex --json # scoped, machine-readable
|
|
@@ -1322,6 +1325,7 @@ Sources: a command's stdout (`--watch` / `--poll`), an HTTP endpoint (`--poll-ht
|
|
|
1322
1325
|
# Signed in? Just publish — no Cloudflare setup.
|
|
1323
1326
|
agents auth login
|
|
1324
1327
|
agents artifacts share plan.html --visibility unlisted # → https://share.agents-cli.sh/<handle>/<slug>-<id>
|
|
1328
|
+
agents artifacts share secret.html --protected # → https://share.agents-cli.sh/<handle>/<slug>-<id>?k=<token>
|
|
1325
1329
|
|
|
1326
1330
|
# Or provision your own Cloudflare R2 (~$0).
|
|
1327
1331
|
agents artifacts setup # once: provision bucket + Worker on your CF
|
|
@@ -1331,7 +1335,7 @@ agents artifacts share plan.html --label "Q3 fleet plan" --meta kind=plan # hu
|
|
|
1331
1335
|
agents artifacts share plan.html --json # URL object for plan-render hooks
|
|
1332
1336
|
agents artifacts share list --agent claude # public gallery, filterable
|
|
1333
1337
|
agents artifacts share list --meta kind=plan # exact, repeatable metadata filters
|
|
1334
|
-
agents artifacts share list --all # include hidden unlisted/me/org pages
|
|
1338
|
+
agents artifacts share list --all # include hidden unlisted/private/me/org pages
|
|
1335
1339
|
agents artifacts share list --scope me # just the owner-only pages
|
|
1336
1340
|
agents artifacts share edit fleet --label "Final fleet plan" --meta status=final
|
|
1337
1341
|
agents artifacts share revisions fleet # prior versions kept under a slug
|
|
@@ -1353,7 +1357,10 @@ Worker lazily renders and caches the branded 1200×630 Open Graph card at `<slug
|
|
|
1353
1357
|
so publishing from Linux does not require a local Chromium; BYO endpoints keep the
|
|
1354
1358
|
local screenshot fallback. `--visibility unlisted` (hidden aliases
|
|
1355
1359
|
`--unlisted` / `--private`) is a capability URL: GET still works, the gallery hides it,
|
|
1356
|
-
and the Worker sends `X-Robots-Tag: noindex`. `--visibility
|
|
1360
|
+
and the Worker sends `X-Robots-Tag: noindex`. `--protected` (`--visibility private`)
|
|
1361
|
+
is token-gated: the published URL carries a secret `?k=` key (`Authorization: Bearer`
|
|
1362
|
+
is accepted too) and GET returns 404 without it — treat the whole link as a secret.
|
|
1363
|
+
`--visibility me` is visible only to
|
|
1357
1364
|
you (the signed-in owner); `--visibility org` is visible to anyone at your email
|
|
1358
1365
|
**domain** — derived from your own address, so it needs a **workspace** Google
|
|
1359
1366
|
account and is refused on a public-inbox domain (`gmail.com`, `outlook.com`,
|
|
@@ -1373,8 +1380,9 @@ single **Phoenix ID** (Google-only device-code OAuth, `agents auth login`) — s
|
|
|
1373
1380
|
a Cloudflare API token from your `cloudflare.com` secrets bundle (or `--token`), creates
|
|
1374
1381
|
an R2 bucket, uploads a tiny Worker, and enables the free `*.workers.dev` subdomain (or
|
|
1375
1382
|
maps `--domain share.example.com` when the token owns the zone). Writes are bearer-gated
|
|
1376
|
-
**through** the Worker (Phoenix bearer or static `WRITE_TOKEN`);
|
|
1377
|
-
a link outlives the agent
|
|
1383
|
+
**through** the Worker (Phoenix bearer or static `WRITE_TOKEN`); public and unlisted
|
|
1384
|
+
reads are **public** so a link outlives the agent, while `--protected` token-gates
|
|
1385
|
+
reads on BYO too. R2 has zero egress + a 10 GB free tier, so BYO is still
|
|
1378
1386
|
effectively free.
|
|
1379
1387
|
|
|
1380
1388
|
**Fleet mode:** provision one endpoint, then every fleet / cloud / ephemeral agent
|
package/dist/bootstrap.js
CHANGED
|
@@ -19,6 +19,7 @@ import * as path from 'path';
|
|
|
19
19
|
import { fileURLToPath } from 'url';
|
|
20
20
|
import { detectDevBuild } from './lib/startup/dev-build.js';
|
|
21
21
|
import { configureRootCommand } from './lib/startup/root-command.js';
|
|
22
|
+
import { bootMark } from './lib/boot-profile.js';
|
|
22
23
|
// `ora`, `@inquirer/prompts`, `./commands/utils.js`, and the agents/versions/shims
|
|
23
24
|
// modules are imported dynamically at their use sites: they are needed only on
|
|
24
25
|
// interactive / update / shim-repair paths, never for fast commands like
|
|
@@ -1029,6 +1030,7 @@ if (helpAllRequested) {
|
|
|
1029
1030
|
// immediately: skip the update check (PATH scan + cache read) and the detached
|
|
1030
1031
|
// background sync (spawns a child process) that every other invocation runs.
|
|
1031
1032
|
if (!isDocumentationRequest) {
|
|
1033
|
+
bootMark('bootstrap:evaluated');
|
|
1032
1034
|
// Run update check before parsing so the upgrade notice/prompt precedes output.
|
|
1033
1035
|
await checkForUpdates();
|
|
1034
1036
|
// Fire-and-forget the background sync. System repo gets a real fast-forward
|
|
@@ -1157,6 +1159,7 @@ if (passedArgs.length === 0) {
|
|
|
1157
1159
|
}
|
|
1158
1160
|
try {
|
|
1159
1161
|
await maybeBootstrapShimIntegration(requestedCommand, isDocumentationRequest, verboseStartup);
|
|
1162
|
+
bootMark('bootstrap:pre-parse');
|
|
1160
1163
|
await program.parseAsync();
|
|
1161
1164
|
}
|
|
1162
1165
|
catch (err) {
|
package/dist/commands/exec.js
CHANGED
|
@@ -27,6 +27,7 @@ import { randomUUID } from 'crypto';
|
|
|
27
27
|
import { isSessionTrackedAgent } from '../lib/session/types.js';
|
|
28
28
|
import { applyActiveRulesPresetAtRun } from '../lib/rules/run-sync.js';
|
|
29
29
|
import { handleBroadcast } from './run-broadcast.js';
|
|
30
|
+
import { bootMark } from '../lib/boot-profile.js';
|
|
30
31
|
/** Distinguish a terminal account-picker marker from an explicit @version pin. */
|
|
31
32
|
export function parseRunAccountPickerRequest(agentSpec) {
|
|
32
33
|
const requested = agentSpec.endsWith('@');
|
|
@@ -690,6 +691,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
690
691
|
`,
|
|
691
692
|
});
|
|
692
693
|
runCmd.action(async (agentSpec, prompt, options, command) => {
|
|
694
|
+
bootMark('run-action:enter');
|
|
693
695
|
// Capture everything after -- as passthrough args forwarded verbatim to the
|
|
694
696
|
// underlying CLI. Commander strips the literal `--` and folds what follows
|
|
695
697
|
// into the positional operands (so `agents run codex -- --yolo` would parse
|
|
@@ -1806,6 +1808,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1806
1808
|
import('../lib/capabilities.js'),
|
|
1807
1809
|
import('../lib/share/config.js'),
|
|
1808
1810
|
]);
|
|
1811
|
+
bootMark('run-deps:imported');
|
|
1809
1812
|
const isValidAgent = (agent) => ALL_AGENT_IDS.includes(agent);
|
|
1810
1813
|
// Parse agent@version#label. The label selects native auth without pinning
|
|
1811
1814
|
// the binary version; --account remains the equivalent flag form.
|
|
@@ -2345,6 +2348,25 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2345
2348
|
process.exit(1);
|
|
2346
2349
|
}
|
|
2347
2350
|
version = resolvedRecoveryTarget.version;
|
|
2351
|
+
// Account rotation (PHNX-3626 / PHNX-3674): recovery picked a healthy
|
|
2352
|
+
// provider of the SAME harness. Inject that credential through the
|
|
2353
|
+
// `--account` path so spawn does not authenticate as the version home's
|
|
2354
|
+
// native login (the exhausted origin when the continue pick is a
|
|
2355
|
+
// provider). An explicit --account (configuredAccount) always wins.
|
|
2356
|
+
const rotatedAccount = resolvedRecoveryTarget.account;
|
|
2357
|
+
if (rotatedAccount && !configuredAccount) {
|
|
2358
|
+
try {
|
|
2359
|
+
const picked = resolveSpawnAccount(rotatedAccount.providerAccount, agent, version, readMeta(), { useDefault: false });
|
|
2360
|
+
if (picked?.kind === 'provider')
|
|
2361
|
+
accountEnv = picked.env;
|
|
2362
|
+
}
|
|
2363
|
+
catch {
|
|
2364
|
+
// Account-registry errors can carry provider credential material;
|
|
2365
|
+
// never relay them onto the terminal from this automatic path.
|
|
2366
|
+
console.error(chalk.red('Could not prepare the rotated provider account for session recovery.'));
|
|
2367
|
+
process.exit(1);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2348
2370
|
if (resolvedRecoveryTarget.mode === 'native') {
|
|
2349
2371
|
version = session.version;
|
|
2350
2372
|
resumeNative = true;
|
|
@@ -2355,27 +2377,8 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2355
2377
|
// differ from the earlier cwd that selected projects/<cwd-key>.
|
|
2356
2378
|
if (!options.cwd && resolvedRecoveryTarget.cwd)
|
|
2357
2379
|
options.cwd = resolvedRecoveryTarget.cwd;
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
// SAME harness. Inject that credential through the same `--account`
|
|
2361
|
-
// path an explicit selection uses, so exec authenticates as the
|
|
2362
|
-
// rotated account while reading the origin transcript. An explicit
|
|
2363
|
-
// --account (configuredAccount) always wins — never rotate over it.
|
|
2364
|
-
const rotatedAccount = resolvedRecoveryTarget.account;
|
|
2365
|
-
if (rotatedAccount && !configuredAccount) {
|
|
2366
|
-
try {
|
|
2367
|
-
const picked = resolveSpawnAccount(rotatedAccount.providerAccount, agent, version, readMeta(), { useDefault: false });
|
|
2368
|
-
if (picked?.kind === 'provider')
|
|
2369
|
-
accountEnv = picked.env;
|
|
2370
|
-
}
|
|
2371
|
-
catch {
|
|
2372
|
-
// Account-registry errors can carry provider credential material;
|
|
2373
|
-
// never relay them onto the terminal from this automatic path.
|
|
2374
|
-
console.error(chalk.red('Could not prepare the rotated provider account for native resume.'));
|
|
2375
|
-
process.exit(1);
|
|
2376
|
-
}
|
|
2377
|
-
if (!options.quiet)
|
|
2378
|
-
process.stderr.write(chalk.gray(`[agents] origin ${agent} account limited → rotated to ${rotatedAccount.label} (native resume)\n`));
|
|
2380
|
+
if (rotatedAccount && !configuredAccount && !options.quiet) {
|
|
2381
|
+
process.stderr.write(chalk.gray(`[agents] origin ${agent} account limited → rotated to ${rotatedAccount.label} (native resume)\n`));
|
|
2379
2382
|
}
|
|
2380
2383
|
if (!options.quiet)
|
|
2381
2384
|
process.stderr.write(chalk.gray(`Resuming ${agent} ${session.shortId} (native)${version ? ` @${version}` : ''} in ${options.cwd ?? cwd}\n`));
|
|
@@ -2386,6 +2389,9 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2386
2389
|
prompt = buildContinuePrompt(session.id, prompt);
|
|
2387
2390
|
if (prompt.trim() === `/continue ${session.id}`)
|
|
2388
2391
|
forceInteractive = true;
|
|
2392
|
+
if (rotatedAccount && !configuredAccount && !options.quiet) {
|
|
2393
|
+
process.stderr.write(chalk.gray(`[agents] origin ${agent} account limited → rotated to ${rotatedAccount.label} (/continue)\n`));
|
|
2394
|
+
}
|
|
2389
2395
|
if (!options.quiet)
|
|
2390
2396
|
process.stderr.write(chalk.gray(`Resuming ${agent} ${session.shortId} (/continue replay)${version ? ` @${version}` : ''}\n`));
|
|
2391
2397
|
}
|
|
@@ -2443,7 +2449,9 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2443
2449
|
// ones sitting in a version home (RUSH-3182). Run-path only — the
|
|
2444
2450
|
// picker's other callers keep the native-only collector.
|
|
2445
2451
|
const { collectRunCandidatesForRun } = await import('../lib/accounting/account-pool-collect.js');
|
|
2452
|
+
bootMark('resolve-version:start');
|
|
2446
2453
|
const resolved = await resolveRunVersion(agent, strategy, cwd, collectRunCandidatesForRun);
|
|
2454
|
+
bootMark('resolve-version:done');
|
|
2447
2455
|
if (resolved.exhausted) {
|
|
2448
2456
|
// Zero healthy accounts splits two ways, and conflating them is what
|
|
2449
2457
|
// stranded a logged-out harness with no way in at all (RUSH-2334):
|
|
@@ -2610,6 +2618,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2610
2618
|
version = healed;
|
|
2611
2619
|
}
|
|
2612
2620
|
}
|
|
2621
|
+
bootMark('ensure-runnable:done');
|
|
2613
2622
|
// The harness may simply not be on this machine. The self-heal above only
|
|
2614
2623
|
// runs when a managed version resolved, so with nothing installed we used
|
|
2615
2624
|
// to fall through and spawn the bare `cliCommand`, which dies as
|
|
@@ -2646,6 +2655,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2646
2655
|
if (defaultVersion) {
|
|
2647
2656
|
applyActiveRulesPresetAtRun(agent, defaultVersion, getVersionHomePath(agent, defaultVersion));
|
|
2648
2657
|
}
|
|
2658
|
+
bootMark('rules-sync:done');
|
|
2649
2659
|
// Login preflight (advisory, warn + continue). On a local INTERACTIVE
|
|
2650
2660
|
// launch, probe whether this agent's account has a credential and print a
|
|
2651
2661
|
// one-line warning if it looks logged out — so you find out BEFORE the TUI
|
package/dist/commands/feed.js
CHANGED
|
@@ -5,7 +5,8 @@ import { projectKeyFromCwd } from '../lib/project-key.js';
|
|
|
5
5
|
import { postFeedStatus } from '../lib/feed-post.js';
|
|
6
6
|
import { linearIssueUrl } from '../lib/session/linear.js';
|
|
7
7
|
import { parseFeedPostLevel, planFeedBroadcast, runFeedBroadcast, effectiveBroadcastConfig, withDesktopNotify, blockBroadcastContext, blockDeliveryFailure, } from '../lib/feed-broadcast.js';
|
|
8
|
-
import { getSessionById } from '../lib/session/db.js';
|
|
8
|
+
import { getSessionById, resolveFullSessionId } from '../lib/session/db.js';
|
|
9
|
+
import { fireTraceSyncInBackground } from '../lib/run-trace-sync.js';
|
|
9
10
|
import { readMeta } from '../lib/state.js';
|
|
10
11
|
import { enrichBlocksFromSessions, groupBlocksByOutcome, isUnambiguousOutcomeAnswer, openBlocksForOutcome, stampBlockOutcomes, } from '../lib/feed-outcome.js';
|
|
11
12
|
import { classifyBlock, filterBlocksForFeed, suppressionDigest, } from '../lib/ask-classifier.js';
|
|
@@ -692,7 +693,10 @@ async function broadcastPostedEvent(event, level, meta, notify = false) {
|
|
|
692
693
|
const config = withDesktopNotify(effectiveBroadcastConfig(meta.feed?.broadcast, level, meta), notify);
|
|
693
694
|
if (!config)
|
|
694
695
|
return [];
|
|
695
|
-
|
|
696
|
+
// Upgrade an 8-char footer crumb to the full indexed id so the console session
|
|
697
|
+
// URL resolves instead of 404ing, and so the ticket join hits the right row.
|
|
698
|
+
const session = resolveFullSessionId(event.sessionId) ?? event.sessionId;
|
|
699
|
+
const ticket = getSessionById(session)?.ticketId;
|
|
696
700
|
const planned = planFeedBroadcast(config, {
|
|
697
701
|
title: event.title,
|
|
698
702
|
text: event.detail ?? '',
|
|
@@ -702,11 +706,16 @@ async function broadcastPostedEvent(event, level, meta, notify = false) {
|
|
|
702
706
|
project: event.project,
|
|
703
707
|
agent: event.agent,
|
|
704
708
|
host: event.host,
|
|
705
|
-
session
|
|
709
|
+
session,
|
|
706
710
|
links: (event.attachments ?? [])
|
|
707
711
|
.map((a) => a.href)
|
|
708
712
|
.filter((href) => /^https?:\/\//i.test(href)),
|
|
709
|
-
});
|
|
713
|
+
}, meta);
|
|
714
|
+
// An important post links the session's console page; fire the trace sync now
|
|
715
|
+
// so that page exists when the owner taps it (trace sync otherwise waits for
|
|
716
|
+
// run exit — PHNX-3628/PHNX-3698). Gated + best-effort, never blocks the post.
|
|
717
|
+
if (level === 'important')
|
|
718
|
+
fireTraceSyncInBackground();
|
|
710
719
|
return runFeedBroadcast(planned, meta);
|
|
711
720
|
}
|
|
712
721
|
/**
|
|
@@ -722,9 +731,13 @@ async function broadcastBlock(block, extras, meta, notify = false) {
|
|
|
722
731
|
const config = withDesktopNotify(effectiveBroadcastConfig(meta.feed?.broadcast, 'important', meta), notify);
|
|
723
732
|
if (!config)
|
|
724
733
|
return [];
|
|
725
|
-
const
|
|
726
|
-
const
|
|
727
|
-
|
|
734
|
+
const sessionId = resolveFullSessionId(block.sessionId) ?? block.sessionId;
|
|
735
|
+
const ticket = getSessionById(sessionId)?.ticketId;
|
|
736
|
+
const ctx = blockBroadcastContext({ ...block, sessionId, ticket: block.ticket ?? ticket }, extras);
|
|
737
|
+
// A block is always important and links the session's console page — fire the
|
|
738
|
+
// trace sync so the page exists when tapped (best-effort, gated, non-blocking).
|
|
739
|
+
fireTraceSyncInBackground();
|
|
740
|
+
return runFeedBroadcast(planFeedBroadcast(config, ctx, meta), meta);
|
|
728
741
|
}
|
|
729
742
|
/** One line per sink that ran. Silent when nothing is configured. */
|
|
730
743
|
function reportBroadcast(outcomes) {
|
|
@@ -947,6 +947,9 @@ export function registerMonitorsCommands(program) {
|
|
|
947
947
|
console.log(observation.raw.split('\n').slice(0, 20).map((l) => ` ${l}`).join('\n'));
|
|
948
948
|
if (observation.meta)
|
|
949
949
|
console.log(chalk.gray(` meta: ${JSON.stringify(observation.meta)}`));
|
|
950
|
+
if (observation.failed) {
|
|
951
|
+
console.log(chalk.yellow(` poll failed (${observation.failureReason ?? 'observation failure'}) — not a value change; skipped`));
|
|
952
|
+
}
|
|
950
953
|
console.log('');
|
|
951
954
|
console.log(`Would fire: ${wouldFire ? chalk.green('yes') : chalk.gray('no')}`);
|
|
952
955
|
if (decision?.event) {
|
|
@@ -13,13 +13,33 @@ import { type LinearMilestone } from '../lib/linear-project-counts.js';
|
|
|
13
13
|
/** Recursion guard: a peer answering a probe fan-out never re-fans-out itself. */
|
|
14
14
|
export declare const PROJECTS_NO_FANOUT_ENV = "AGENTS_PROJECTS_LOCAL";
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* should be printed.
|
|
16
|
+
* A path-shaped `agents projects view` argument — `.`, `..`, a `~`-prefixed
|
|
17
|
+
* value, or anything containing a path separator — means "auto-detect the
|
|
18
|
+
* project for this DIRECTORY", not "a project named literally this". A bare
|
|
19
|
+
* token (no separators) stays a project name, exactly as before.
|
|
21
20
|
*/
|
|
22
|
-
export declare function
|
|
21
|
+
export declare function looksLikePath(token: string): boolean;
|
|
22
|
+
/** Machine-readable shape of `agents projects view <path> --json`. */
|
|
23
|
+
export interface ViewDetection {
|
|
24
|
+
/** Detected project name, or null when no definition contains the path. */
|
|
25
|
+
name: string | null;
|
|
26
|
+
/** The detected project's Linear binding; fields are null when unbound. */
|
|
27
|
+
linear: {
|
|
28
|
+
name: string | null;
|
|
29
|
+
projectId: string | null;
|
|
30
|
+
};
|
|
31
|
+
/** The detected project's repo/monorepo root (home-relative), or null. */
|
|
32
|
+
root: string | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Best-effort cwd->project detection for `agents projects view <path>`. Delegates
|
|
36
|
+
* to the shared {@link projectNameForCwd} (def-root containment, longest wins),
|
|
37
|
+
* then reads the matched definition's Linear binding and root so ONE call yields
|
|
38
|
+
* the name AND the Linear projectId. Fail-open: an all-null shape when nothing
|
|
39
|
+
* contains the path — never throws, so a scripted caller stays unscoped rather
|
|
40
|
+
* than crashing. This subsumes the removed `agents projects for-cwd`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function detectProjectForPath(cwd: string, defs: ProjectDef[]): ViewDetection;
|
|
23
43
|
/**
|
|
24
44
|
* One compact trailing note for peers that didn't answer the `--fleet`
|
|
25
45
|
* fan-out — unreachable, running an agents-cli too old to carry `projects
|
|
@@ -35,16 +35,33 @@ export const PROJECTS_NO_FANOUT_ENV = 'AGENTS_PROJECTS_LOCAL';
|
|
|
35
35
|
/** Max peers named in the skipped note before the rest collapse to `+N`. */
|
|
36
36
|
const SKIPPED_NAME_LIMIT = 4;
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* should be printed.
|
|
38
|
+
* A path-shaped `agents projects view` argument — `.`, `..`, a `~`-prefixed
|
|
39
|
+
* value, or anything containing a path separator — means "auto-detect the
|
|
40
|
+
* project for this DIRECTORY", not "a project named literally this". A bare
|
|
41
|
+
* token (no separators) stays a project name, exactly as before.
|
|
43
42
|
*/
|
|
44
|
-
export function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
export function looksLikePath(token) {
|
|
44
|
+
return token === '.' || token === '..' || token.startsWith('~') || token.includes('/');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Best-effort cwd->project detection for `agents projects view <path>`. Delegates
|
|
48
|
+
* to the shared {@link projectNameForCwd} (def-root containment, longest wins),
|
|
49
|
+
* then reads the matched definition's Linear binding and root so ONE call yields
|
|
50
|
+
* the name AND the Linear projectId. Fail-open: an all-null shape when nothing
|
|
51
|
+
* contains the path — never throws, so a scripted caller stays unscoped rather
|
|
52
|
+
* than crashing. This subsumes the removed `agents projects for-cwd`.
|
|
53
|
+
*/
|
|
54
|
+
export function detectProjectForPath(cwd, defs) {
|
|
55
|
+
const name = projectNameForCwd(cwd, defs);
|
|
56
|
+
const def = name ? defs.find((d) => d.name === name) : undefined;
|
|
57
|
+
return {
|
|
58
|
+
name: name ?? null,
|
|
59
|
+
linear: {
|
|
60
|
+
name: def?.linear?.name ?? null,
|
|
61
|
+
projectId: def?.linear?.projectId ?? null,
|
|
62
|
+
},
|
|
63
|
+
root: def?.root ?? null,
|
|
64
|
+
};
|
|
48
65
|
}
|
|
49
66
|
/**
|
|
50
67
|
* One compact trailing note for peers that didn't answer the `--fleet`
|
|
@@ -570,17 +587,6 @@ export function registerProjectsCommands(program) {
|
|
|
570
587
|
console.log(` ${chalk.bold(row.name.padEnd(w.name))} ${chalk.dim(row.path.padEnd(w.path))} ${chalk.cyan(row.repo.padEnd(w.repo))}${agentsSuffix}`);
|
|
571
588
|
}
|
|
572
589
|
});
|
|
573
|
-
// ---- for-cwd ----
|
|
574
|
-
projects
|
|
575
|
-
.command('for-cwd [cwd]')
|
|
576
|
-
.description('Resolve a directory to its defined project name (root or a repos[].path/subpath match). Defaults to the current directory.')
|
|
577
|
-
.option('--json', 'Machine-readable output: {"name": string | null}')
|
|
578
|
-
.action((cwdArg, opts) => {
|
|
579
|
-
const name = projectNameForCwd(cwdArg ?? process.cwd(), listProjectDefs());
|
|
580
|
-
const out = formatForCwdOutput(name, !!opts.json);
|
|
581
|
-
if (out)
|
|
582
|
-
console.log(out);
|
|
583
|
-
});
|
|
584
590
|
// ---- add ----
|
|
585
591
|
projects
|
|
586
592
|
.command('add <name>')
|
|
@@ -763,15 +769,42 @@ export function registerProjectsCommands(program) {
|
|
|
763
769
|
process.stdout.write(formatFleetSkippedNote(fleetSkipped));
|
|
764
770
|
}
|
|
765
771
|
projects
|
|
766
|
-
.command('status [
|
|
772
|
+
.command('status [nameOrPath]')
|
|
767
773
|
.alias('view')
|
|
768
|
-
.description('Progress card for every project across the whole fleet, or one named project (alias: view). Named form also prints every milestone and the stored definition.')
|
|
774
|
+
.description('Progress card for every project across the whole fleet, or one named project (alias: view). Named form also prints every milestone and the stored definition. A path argument (., .., a ~-prefixed value, a /-containing value, or --path) auto-detects the project that CONTAINS that directory.')
|
|
769
775
|
.option('--json', 'Machine-readable output')
|
|
776
|
+
.option('--path [dir]', 'Treat the argument as a DIRECTORY and auto-detect the project that contains it (bare --path uses the cwd). With --json prints {name, linear:{name,projectId}, root}, all-null when nothing matches (fail-open, exit 0).')
|
|
770
777
|
.option('--window <days>', 'Window for merged PRs, artifacts, and focus areas', '7')
|
|
771
778
|
.option('--no-remote', 'Skip the GitHub and Linear lookups; faster, offline')
|
|
772
779
|
.option('--device <name...>', 'Scope fleet status to one or more devices (repeatable)')
|
|
773
780
|
.option('--devices <names>', 'Scope fleet status to a comma-separated list of devices')
|
|
774
781
|
.action(async (name, rawOpts) => {
|
|
782
|
+
// Path mode: an explicit --path, or a path-shaped positional (., .., a
|
|
783
|
+
// ~-prefixed value, or a /-containing value) means "auto-detect the
|
|
784
|
+
// project for this DIRECTORY" rather than "a project named literally
|
|
785
|
+
// this". projectNameForCwd resolves the dir (expandLocalHome +
|
|
786
|
+
// path.resolve), so `.`, `~/…`, and relative paths all normalize. This
|
|
787
|
+
// subsumes the removed `agents projects for-cwd`.
|
|
788
|
+
let detectDir;
|
|
789
|
+
if (typeof rawOpts.path === 'string')
|
|
790
|
+
detectDir = rawOpts.path;
|
|
791
|
+
else if (rawOpts.path === true)
|
|
792
|
+
detectDir = process.cwd();
|
|
793
|
+
else if (name !== undefined && looksLikePath(name))
|
|
794
|
+
detectDir = name;
|
|
795
|
+
if (detectDir !== undefined) {
|
|
796
|
+
const detection = detectProjectForPath(detectDir, listProjectDefs());
|
|
797
|
+
if (rawOpts.json) {
|
|
798
|
+
console.log(JSON.stringify(detection));
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (!detection.name) {
|
|
802
|
+
console.log(chalk.gray(`No defined project contains ${detectDir}`));
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
// Non-JSON: fall through and render the full card for the detected project.
|
|
806
|
+
name = detection.name;
|
|
807
|
+
}
|
|
775
808
|
// Named invocation = `view` depth (all milestones + definition). Unnamed
|
|
776
809
|
// stays the scannable multi-project rollup. `view` is a commander alias
|
|
777
810
|
// of this same command, so there is only one implementation.
|
package/dist/commands/send.js
CHANGED
|
@@ -2,7 +2,9 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { die } from '../lib/format.js';
|
|
3
3
|
import { setHelpSections } from '../lib/help.js';
|
|
4
4
|
import { readMeta } from '../lib/state.js';
|
|
5
|
-
import { sendMessage } from '../lib/channels/send.js';
|
|
5
|
+
import { sendMessage, isOwnerAlias } from '../lib/channels/send.js';
|
|
6
|
+
import { composeOwnerMessage } from '../lib/owner-message.js';
|
|
7
|
+
import { fireTraceSyncInBackground } from '../lib/run-trace-sync.js';
|
|
6
8
|
function mergeAttachments(opts) {
|
|
7
9
|
const list = [...(opts.attach ?? []), ...(opts.attachment ?? [])];
|
|
8
10
|
return list.length ? list : undefined;
|
|
@@ -23,7 +25,28 @@ function toInput(positionalText, opts, ownerMode) {
|
|
|
23
25
|
}
|
|
24
26
|
async function runSend(positionalText, opts, ownerMode) {
|
|
25
27
|
const meta = readMeta();
|
|
26
|
-
|
|
28
|
+
let input = toInput(positionalText, opts, ownerMode);
|
|
29
|
+
// An owner-bound ping (`agents notify`, `agents send --to owner`) goes through
|
|
30
|
+
// the SAME composer as an important `feed post` (PHNX-3698): short-shaped body,
|
|
31
|
+
// TEAM-N keys linkified, session crumb as a tappable console URL — instead of a
|
|
32
|
+
// raw dump. A non-owner send (explicit --channel/--to) is delivered verbatim.
|
|
33
|
+
if (ownerMode || isOwnerAlias(opts.to)) {
|
|
34
|
+
const flagged = opts.text?.trim() ?? '';
|
|
35
|
+
const positional = (positionalText ?? '').trim();
|
|
36
|
+
const raw = flagged || positional;
|
|
37
|
+
// When both forms are given and disagree, leave it to sendMessage to fail
|
|
38
|
+
// loud with the "pass the message once" error rather than composing a guess.
|
|
39
|
+
const bothDiffer = flagged !== '' && positional !== '' && flagged !== positional;
|
|
40
|
+
if (raw && !bothDiffer) {
|
|
41
|
+
input = { ...input, text: composeOwnerMessage(raw), positionalText: undefined };
|
|
42
|
+
// The console URL in the composed body only resolves once this session's
|
|
43
|
+
// trace shard is uploaded; fire that now so the tapped link isn't a 404.
|
|
44
|
+
// A --dry-run resolves + composes but MUST NOT act (its documented contract),
|
|
45
|
+
// so it never spawns the sync — it just shows what would be sent.
|
|
46
|
+
fireTraceSyncInBackground({ disabled: Boolean(opts.dryRun) });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const out = await sendMessage(input, meta);
|
|
27
50
|
if ('error' in out) {
|
|
28
51
|
die(out.error);
|
|
29
52
|
}
|
|
@@ -127,6 +150,10 @@ export function registerSendCommand(program) {
|
|
|
127
150
|
Set owner.channels + owner.policy.normal in humans.yaml once per fleet.
|
|
128
151
|
Every channel listed in the normal policy receives an owner-addressed send.
|
|
129
152
|
|
|
153
|
+
Owner sends go through the same composer as "feed post": the body is
|
|
154
|
+
short-shaped, any TEAM-N key becomes a Linear URL, and the session crumb
|
|
155
|
+
becomes a tappable https://prix.dev/console/sessions/<id> link.
|
|
156
|
+
|
|
130
157
|
${SHARED_NOTES}
|
|
131
158
|
`,
|
|
132
159
|
});
|
|
@@ -13,5 +13,63 @@
|
|
|
13
13
|
* `--pane`/`--pty` target a backend directly when the handle is already known.
|
|
14
14
|
*/
|
|
15
15
|
import type { Command } from 'commander';
|
|
16
|
+
import { type ActiveSession } from '../lib/session/active.js';
|
|
17
|
+
interface InjectOptions {
|
|
18
|
+
pane?: string;
|
|
19
|
+
socket?: string;
|
|
20
|
+
pty?: string;
|
|
21
|
+
/**
|
|
22
|
+
* The remote device. A single `--device box` arrives here as `['box']` because
|
|
23
|
+
* the parent `sessions` command's variadic `-D, --device <target...>` shadows
|
|
24
|
+
* this subcommand's scalar option under `optsWithGlobals()` — normalize it with
|
|
25
|
+
* {@link normalizeInjectDevice} before use (PHNX-3688).
|
|
26
|
+
*/
|
|
27
|
+
device?: string | string[];
|
|
28
|
+
enter?: boolean;
|
|
29
|
+
combined?: boolean;
|
|
30
|
+
json?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether an active session is the one `sessions inject <token>` means. Matches
|
|
34
|
+
* a resolvable session id (exact or unique prefix) AND — for a tmux-hosted row
|
|
35
|
+
* whose full id never resolved (`sessionId` absent) — the `ag-<agent>-<shortid>`
|
|
36
|
+
* tmux name's `shortid` suffix (exact or prefix), the full tmux name, and the
|
|
37
|
+
* pane id. Those are the only selectors an id-less remote tmux row exposes, so
|
|
38
|
+
* without this an operator has no tool-native way to nudge it (PHNX-3688).
|
|
39
|
+
*/
|
|
40
|
+
export declare function matchInjectSelector(session: ActiveSession, token: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The `--device` selector, normalized to a single host string. `optsWithGlobals()`
|
|
43
|
+
* merges the parent `sessions` command's variadic `-D, --device <target...>` over
|
|
44
|
+
* this subcommand's scalar `--device`, so a single `--device box` arrives as
|
|
45
|
+
* `['box']` — which flowed straight into `sshExec` and crashed on
|
|
46
|
+
* `host.startsWith` (PHNX-3688). Coerce the array to its one element; fail loud on
|
|
47
|
+
* several, since inject delivers to exactly one terminal (a fan-out spelling is a
|
|
48
|
+
* user error, not a first-of-list guess).
|
|
49
|
+
*/
|
|
50
|
+
export declare function normalizeInjectDevice(value: string | string[] | undefined): string | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* The `agents sessions inject` argv to re-run ON a device (its tmux panes live
|
|
53
|
+
* there, so resolution must happen there). Every flag rides along EXCEPT
|
|
54
|
+
* `--device`: the command runs on the device, resolving locally. Pure so the
|
|
55
|
+
* forwarded invocation is asserted without an SSH hop (PHNX-3688).
|
|
56
|
+
*/
|
|
57
|
+
export declare function buildRemoteInjectArgv(sessionId: string, text: string, options: InjectOptions): string[];
|
|
58
|
+
/**
|
|
59
|
+
* Resolve `device` (registry alias or `user@host`) to an ssh target and re-run
|
|
60
|
+
* `agents sessions inject` there, so a bare session id + `--device` resolves on
|
|
61
|
+
* the box that actually holds the session's tmux panes. The tool-native form of
|
|
62
|
+
* the `agents ssh <device> "agents sessions inject <id> …"` workaround (PHNX-3688).
|
|
63
|
+
*/
|
|
64
|
+
/**
|
|
65
|
+
* Resolve `--device` to an ssh target. A registered device becomes its
|
|
66
|
+
* `user@dnsName`; a bare unknown name (an ad-hoc `user@host` or ssh_config alias)
|
|
67
|
+
* is handed to ssh verbatim (`resolveHost` returns null for it). A registered
|
|
68
|
+
* device we CANNOT dial — password-auth, addressless — throws its typed error and
|
|
69
|
+
* is NOT degraded to the raw name, which could ssh a coincidentally-matching but
|
|
70
|
+
* unrelated `~/.ssh/config` Host (PHNX-3688 review).
|
|
71
|
+
*/
|
|
72
|
+
export declare function resolveInjectSshTarget(device: string): Promise<string>;
|
|
16
73
|
/** Attach the `inject` subcommand to an existing `sessions` command. */
|
|
17
74
|
export declare function registerSessionsInjectCommand(sessionsCmd: Command): void;
|
|
75
|
+
export {};
|