@phnx-labs/agents-cli 1.20.91 → 1.20.92
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 +155 -0
- package/README.md +1 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +72 -6
- package/dist/commands/activity.js +198 -49
- package/dist/commands/beta.js +1 -0
- package/dist/commands/doctor.js +4 -2
- package/dist/commands/exec.d.ts +14 -0
- package/dist/commands/exec.js +144 -14
- package/dist/commands/projects.d.ts +12 -0
- package/dist/commands/projects.js +358 -0
- package/dist/commands/sessions-picker.d.ts +15 -0
- package/dist/commands/sessions-picker.js +37 -12
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +9 -1
- package/dist/commands/sessions.d.ts +10 -5
- package/dist/commands/sessions.js +65 -27
- package/dist/index.js +2 -1
- package/dist/lib/activity.d.ts +69 -12
- package/dist/lib/activity.js +417 -74
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/devices/registry.d.ts +14 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/feed-post.js +8 -2
- package/dist/lib/hosts/remote-cmd.js +4 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +14 -4
- package/dist/lib/menubar/install-menubar.js +20 -6
- package/dist/lib/project-key.d.ts +44 -0
- package/dist/lib/project-key.js +79 -0
- package/dist/lib/project-root.js +16 -0
- package/dist/lib/project-status.d.ts +69 -0
- package/dist/lib/project-status.js +101 -0
- package/dist/lib/projects.d.ts +138 -0
- package/dist/lib/projects.js +301 -0
- package/dist/lib/remote-agents-json.d.ts +9 -0
- package/dist/lib/remote-agents-json.js +11 -5
- 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/bash-command.d.ts +53 -0
- package/dist/lib/session/bash-command.js +364 -0
- package/dist/lib/session/digest.d.ts +6 -0
- package/dist/lib/session/digest.js +19 -0
- package/dist/lib/session/relative-time.d.ts +23 -0
- package/dist/lib/session/relative-time.js +60 -8
- package/dist/lib/session/remote-list.js +5 -2
- package/dist/lib/session/render.d.ts +2 -9
- package/dist/lib/session/render.js +25 -56
- package/dist/lib/ssh-exec.d.ts +6 -0
- package/dist/lib/ssh-exec.js +10 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +5 -0
- package/dist/lib/terminal/backends/index.d.ts +10 -2
- package/dist/lib/terminal/backends/index.js +14 -2
- package/dist/lib/terminal/backends/terminal-app.d.ts +13 -0
- package/dist/lib/terminal/backends/terminal-app.js +73 -0
- package/dist/lib/terminal/index.d.ts +2 -1
- package/dist/lib/terminal/index.js +2 -1
- package/dist/lib/terminal/preferred.d.ts +89 -0
- package/dist/lib/terminal/preferred.js +87 -0
- package/dist/lib/terminal/run-surface.d.ts +82 -0
- package/dist/lib/terminal/run-surface.js +146 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/types.d.ts +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,149 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.20.92
|
|
4
|
+
|
|
5
|
+
- **`agents sessions` rows show creation time as well as last activity (RUSH-2107).**
|
|
6
|
+
The trailing time cell used to carry one unlabeled "X ago" — last activity — so a
|
|
7
|
+
row could not say when the session began or how long it had been alive. It now
|
|
8
|
+
reads `3d → 1 hour ago`: the compact creation age, then the last-activity label the
|
|
9
|
+
listing sorts by. Both the interactive picker and the flat/tree listings render it.
|
|
10
|
+
A session that ran for under a minute keeps a single field (the two halves would
|
|
11
|
+
name the same moment), and a terminal too narrow for both drops the creation age
|
|
12
|
+
rather than squeezing the topic below its floor, so rows never wrap. The picker's
|
|
13
|
+
detail pane spells the same facts out as `created X ago · last active Y ago ·
|
|
14
|
+
lasted Z`, and now derives them from the indexed session metadata when no local
|
|
15
|
+
transcript exists — so **remote** and not-yet-indexed sessions report their timing
|
|
16
|
+
instead of showing none. Source: `apps/cli/src/lib/session/relative-time.ts`,
|
|
17
|
+
`apps/cli/src/commands/sessions.ts`, `apps/cli/src/commands/sessions-picker.ts`.
|
|
18
|
+
|
|
19
|
+
- **Stop fleet health probes from orphaning remote processes on timeout (RUSH-2114).** `sshExecAsync` now uses a direct ssh connection whenever a `timeoutMs` is set, because a control-master outlives the local client and keeps the remote command running after we kill it. `agents doctor` also normalizes host names before excluding the local machine, so `zion.local` can no longer be self-SSH'd. Source: `apps/cli/src/lib/ssh-exec.ts`, `apps/cli/src/commands/doctor.ts`.
|
|
20
|
+
- **Harden menubar install against Gatekeeper rejection.** `ensureValidSignature` now checks `spctl --assess`; a Developer-ID-signed but un-notarized bundle is stripped of quarantine and re-signed ad-hoc so the launchd service does not crash-loop with "app is damaged". The release build script gained optional notarization via `MENUBAR_HELPER_NOTARIZE` and `MENUBAR_HELPER_NOTARIZE_KEYCHAIN_PROFILE`. Source: `apps/cli/src/lib/menubar/install-menubar.ts`, `apps/cli/menubar/scripts/build.sh`.
|
|
21
|
+
|
|
22
|
+
- **`agents activity` now shows the whole fleet, grouped by project.** The
|
|
23
|
+
question the command answers is "what are my agents doing", and agents run on
|
|
24
|
+
every box — but it read only the local logs unless you remembered
|
|
25
|
+
`--devices-all`, and printed one flat newest-first stream. Both defaults are
|
|
26
|
+
inverted: every run fans `activity --json` out to each reachable device and
|
|
27
|
+
merges the peers' streams host-tagged, then buckets them by project, one level,
|
|
28
|
+
no sub-grouping. `--local` scopes back to this machine, `-H/--host` to named
|
|
29
|
+
boxes, and `--flat` (or `--group-by none`) restores the single stream;
|
|
30
|
+
`--devices-all`/`--hosts-all` remain accepted so existing scripts keep working.
|
|
31
|
+
A peer answering the fan-out still carries the recursion guard, so it never
|
|
32
|
+
re-fans the fleet.
|
|
33
|
+
|
|
34
|
+
- **Each project header names the machines its work ran on.** A bucket reads
|
|
35
|
+
`▸ agents-cli 12 events · 4 milestones · zion, yosemite-s0` — up to three
|
|
36
|
+
machines by name plus a `+N` tail, so a project touched by a dozen boxes stays
|
|
37
|
+
one scannable line; individual rows keep their own `[host]` tag. Peers that
|
|
38
|
+
never answered are reported once at the end (`· 2 devices unreachable: …`)
|
|
39
|
+
rather than a line each above the timeline, so a missing machine is visible but
|
|
40
|
+
not noisy.
|
|
41
|
+
|
|
42
|
+
- **A project is now the repository, not whatever directory the agent sat in.**
|
|
43
|
+
A cwd resolves to the git repository containing it, so `<repo>/apps/cli` files
|
|
44
|
+
under `<repo>` instead of `cli`, and a worktree under
|
|
45
|
+
`<repo>/.agents/worktrees/<slug>` folds back into the repo it branched from.
|
|
46
|
+
A directory in no repo groups as itself, and a dotfiles repo at `$HOME` is not
|
|
47
|
+
treated as a project. The `agents sessions` overview and `agents feed post` now
|
|
48
|
+
share this one resolver (`lib/project-key.ts`), so a project reads identically
|
|
49
|
+
everywhere instead of each view folding cwds its own way.
|
|
50
|
+
|
|
51
|
+
- **`--limit` is spent on milestones, not on collapsed churn.** The default view
|
|
52
|
+
rolls routine `file.edited` work up to a count, so a plain slice let one busy
|
|
53
|
+
machine's 40 file edits hide every other device's PRs behind a single
|
|
54
|
+
`file edited ×40` line. The cap now bounds the milestones shown, with the
|
|
55
|
+
routine events inside that window riding along for the counts. `--all` still
|
|
56
|
+
shows routine work inline and caps every event.
|
|
57
|
+
|
|
58
|
+
- **The activity header no longer carries other subsystems' hook warnings.**
|
|
59
|
+
Registering the activity-log hooks surfaced every unresolved entry in the hook
|
|
60
|
+
manifest — a missing `inject-session-id` script, someone else's half-installed
|
|
61
|
+
plugin — printing five wrapped yellow lines above the timeline on every run.
|
|
62
|
+
Those are `agents doctor`'s job; only a failure that would leave the activity
|
|
63
|
+
log unwritten is reported here.
|
|
64
|
+
|
|
65
|
+
- **The menu bar's New Session opens in the terminal you actually work in.** It
|
|
66
|
+
hardcoded AppleScript at Terminal.app, so a Ghostty or iTerm user got a
|
|
67
|
+
Terminal.app window every time. It now shells `agents run <agent> --terminal`,
|
|
68
|
+
and the CLI resolves the terminal from the user's own live sessions — the host
|
|
69
|
+
app `agents sessions --active` already attributes every session to
|
|
70
|
+
(`ActiveSession.host`). Order: the terminal the caller is in, then the host of
|
|
71
|
+
the most recent live session, then the first available backend. Hosts map to
|
|
72
|
+
backends only where the engine can really drive them, so an undrivable host
|
|
73
|
+
(Warp, kitty, Cursor) falls through instead of opening the wrong app. A
|
|
74
|
+
tmux-hosted session (every interactive `agents run`) resolves to the app its
|
|
75
|
+
attached tmux client is in, via the same resolver behind
|
|
76
|
+
`agents sessions`' "viewing in Ghostty tab 2" — without that it would name the
|
|
77
|
+
multiplexer and no terminal at all. Source:
|
|
78
|
+
`apps/cli/src/lib/terminal/preferred.ts`,
|
|
79
|
+
`apps/cli/src/lib/terminal/backends/terminal-app.ts`,
|
|
80
|
+
`apps/cli/menubar/Sources/MenubarHelper/AgentsCLI.swift`.
|
|
81
|
+
|
|
82
|
+
- **`agents run <agent> --terminal` opens a run in a real terminal tab.** For a
|
|
83
|
+
caller that cannot host a TUI (the menu bar, a script). Without a value the
|
|
84
|
+
terminal is detected as above; `--terminal <backend>` forces one
|
|
85
|
+
(`iterm | ghostty | terminal | tmux | vscodium-agent`) and errors on an unknown
|
|
86
|
+
id rather than silently auto-detecting. The tab re-invokes the same argv with
|
|
87
|
+
the flag stripped, so `--mode`, `--cwd`, and a `--` passthrough ride along.
|
|
88
|
+
Cannot combine with `--host`. Source: `apps/cli/src/lib/terminal/run-surface.ts`,
|
|
89
|
+
`apps/cli/src/commands/exec.ts`.
|
|
90
|
+
|
|
91
|
+
- **Terminal.app is a real launch backend now (`terminal`).** Registered last, so
|
|
92
|
+
it is the every-Mac floor without outranking a terminal the user chose to
|
|
93
|
+
install, and reported unavailable over SSH where `osascript` cannot reach the
|
|
94
|
+
GUI login. It has no scriptable split, so a split request opens a tab, and
|
|
95
|
+
`agents sessions resume --splits` now says so instead of quietly producing
|
|
96
|
+
tabs. `detectCurrentBackend` also recognizes `TERM_PROGRAM=Apple_Terminal`.
|
|
97
|
+
Source: `apps/cli/src/lib/terminal/backends/terminal-app.ts`.
|
|
98
|
+
|
|
99
|
+
- **`agents sessions resume` / `sessions focus` reach Terminal.app too.** Adding
|
|
100
|
+
it to the backend registry changes both: on a Mac with neither iTerm, Ghostty,
|
|
101
|
+
nor VSCodium installed they used to fall back to resuming in the current
|
|
102
|
+
process, and now open a Terminal.app tab; `resume`'s interactive picker gains a
|
|
103
|
+
Terminal row, and `--terminal-app` forces it (named apart from
|
|
104
|
+
`run --terminal`, which means something different). Source:
|
|
105
|
+
`apps/cli/src/commands/sessions-resume.ts`, `apps/cli/src/commands/focus.ts`.
|
|
106
|
+
|
|
107
|
+
- **New Task… in the menu bar.** A row above New Session that opens the
|
|
108
|
+
quick-dispatch bar — the same panel as `Cmd-Shift-O`, now reachable without the
|
|
109
|
+
chord (and without the Accessibility grant the chord needs). The status item
|
|
110
|
+
owns the one panel instance, so an interrupted capture is restored whichever
|
|
111
|
+
entry point you return through. Source:
|
|
112
|
+
`apps/cli/menubar/Sources/MenubarHelper/StatusItemController.swift`.
|
|
113
|
+
|
|
114
|
+
- **Activity, feed posts, and the sessions overview now speak defined project
|
|
115
|
+
names.** One resolver (`resolveProjectNameForCwd`, `lib/projects.ts`) backs all
|
|
116
|
+
three: a cwd inside a defined project (`~/.agents/projects/<name>.yaml`) reads
|
|
117
|
+
as the project's name — a multi-repo project is a single bucket in `agents
|
|
118
|
+
activity`, not one per repo — and anything else falls back to the
|
|
119
|
+
repository-level key, so nothing changes without definitions. Each peer
|
|
120
|
+
resolves its own cwds against its synced definitions before events cross the
|
|
121
|
+
wire. Source: `apps/cli/src/lib/projects.ts`, `apps/cli/src/commands/activity.ts`.
|
|
122
|
+
|
|
123
|
+
- **`agents activity --project <name>`** narrows the fleet stream to one project,
|
|
124
|
+
exact-matched on the resolved label — one project's PRs, plans, and worktrees
|
|
125
|
+
across every box without the rest of the fleet's noise. Source:
|
|
126
|
+
`apps/cli/src/lib/activity.ts` (`filterActivityByProject`).
|
|
127
|
+
|
|
128
|
+
- **`agents projects` — named multi-repo projects with a project progress rollup (beta).**
|
|
129
|
+
Define a project once in `~/.agents/projects/<name>.yaml` (name, home-relative
|
|
130
|
+
root/defaultPath, multiple repos with monorepo subpaths, described `contexts[]`
|
|
131
|
+
starting points, external `integrations[]`, Linear link) and `agents run --project
|
|
132
|
+
<name>` resolves the definition before the old `<root>/<slug>` convention — undefined
|
|
133
|
+
slugs behave exactly as before. The headline is `agents projects status`: instead of a
|
|
134
|
+
per-agent activity line, it renders one card per project — live agents by state, plan
|
|
135
|
+
completion, open **and** recently-merged PRs, tickets in flight, and the artifacts
|
|
136
|
+
agents produced — by rolling up signals already on disk (live agents matched to a project
|
|
137
|
+
by this machine's session cwd; the merged-PR count is repo-global via `gh`). `--window
|
|
138
|
+
<days>` and `--no-remote`
|
|
139
|
+
tune the PR/artifact lookup. Also `list` / `add` (infers root + origin slug) / `show` /
|
|
140
|
+
`edit` / `import --from-factory` (absorbs the Factory `projects.json` registry) / `rm`.
|
|
141
|
+
Enable with `agents beta enable projects`. Source: `apps/cli/src/lib/projects.ts`,
|
|
142
|
+
`apps/cli/src/lib/project-status.ts`, `apps/cli/src/commands/projects.ts`,
|
|
143
|
+
`apps/cli/src/lib/project-root.ts`.
|
|
144
|
+
|
|
145
|
+
- **The cross-fleet session sweep no longer hides sessions on manually-registered devices.** `agents sessions` fan-out (and therefore `--resolve`, cross-machine resume, and `--active`) picked peers with a strict `tailscale.online === true` test. A device registered with `address.via: "manual"` never gets a Tailscale peer entry at all, so its `online` stayed `undefined` and the sweep skipped it **permanently** — every session on that box was invisible and could not be resolved or resumed from any other machine. Peer selection is now `isDialableDevice`, a union of both liveness signals: a device with no Tailscale block is unknown-not-offline (the rule `ssh.ts` `renderDeviceTable` and Factory's `isDeviceOnline` already used, so the picker and the sweep finally agree on who exists), and a positive live SSH probe (`DeviceProfile.reachability`, RUSH-1965) additionally rescues a device whose snapshot says offline. A **failed** probe deliberately does not remove a peer — the probe runs on a short SSH budget and returns false negatives on a congested tailnet (observed calling the local machine unreachable), and letting that shrink the sweep would hide sessions on healthy boxes. Applied to both sweeps that share this shape. Source: `apps/cli/src/lib/devices/registry.ts` (`isDialableDevice`), `apps/cli/src/lib/session/remote-list.ts`, `apps/cli/src/lib/remote-agents-json.ts`.
|
|
146
|
+
|
|
3
147
|
## 1.20.91
|
|
4
148
|
|
|
5
149
|
- **An agent can now say it is stuck: `agents feed post --blocked` (RUSH-2110).** The
|
|
@@ -123,6 +267,17 @@
|
|
|
123
267
|
|
|
124
268
|
## 1.20.90
|
|
125
269
|
|
|
270
|
+
- **Bash commands are now parsed and classified for richer activity summaries.** The
|
|
271
|
+
`11-activity-log.py` hook tokenizes every Bash tool call and emits a structured
|
|
272
|
+
`bash.executed` activity record with `category`, `bashTool`, and `bashAction`.
|
|
273
|
+
High-signal commands also raise milestones: `video.rendered`/`video.converted`
|
|
274
|
+
for `ffmpeg`, `image.upscaled` for `realesrgan`/`waifu2x`/`swin2sr`, and
|
|
275
|
+
`metadata.edited` for `exiftool`/`id3v2`/`metaflac`/`vorbiscomment`. The session
|
|
276
|
+
renderer and digest use the shared `lib/session/bash-command.ts` classifier.
|
|
277
|
+
Source: `apps/cli/src/lib/session/bash-command.ts`,
|
|
278
|
+
`apps/cli/src/lib/activity.ts`, `apps/cli/src/lib/session/digest.ts`,
|
|
279
|
+
`apps/cli/src/lib/session/render.ts`.
|
|
280
|
+
|
|
126
281
|
- **`agents sessions --active` now shows one row per agent, not one per directory.**
|
|
127
282
|
A live tmux agent pane whose durable identity records were missing (the common case
|
|
128
283
|
once meta/pid-registry entries age out) was dropped, then re-surfaced by the ps-scan
|
package/README.md
CHANGED
|
@@ -288,7 +288,7 @@ Filters **stack** (they AND together), the active set shows in the header, and t
|
|
|
288
288
|
|
|
289
289
|
Each live session resolves to `working`, `waiting_input` (with why -- a question, a plan review, or a permission prompt), or `idle`, alongside badges for the PR it opened, the worktree it sits in, and the ticket it's working. `agents sessions focus [id]` attaches the live pane in place -- the tmux split locally or over SSH, or its Ghostty tab -- and falls back to a fresh tab + resume when the terminal is gone.
|
|
290
290
|
|
|
291
|
-
Landing on a session cold? `agents sessions <id>` prints a catch-up digest: an inferred title, files changed grouped by directory (created / modified / deleted), a histogram of which tools did the work, and the last test verdict -- the signals to reload a task in seconds.
|
|
291
|
+
Landing on a session cold? `agents sessions <id>` prints a catch-up digest: an inferred title, files changed grouped by directory (created / modified / deleted), a histogram of which tools did the work (including parsed Bash commands -- `git`, `npm`, `ffmpeg`, `ssh`, and so on), and the last test verdict -- the signals to reload a task in seconds.
|
|
292
292
|
|
|
293
293
|
### Resume anywhere — and stay resumed
|
|
294
294
|
|
package/dist/bin/agents
CHANGED
|
Binary file
|
|
@@ -8,14 +8,80 @@
|
|
|
8
8
|
* it tails the logs and collapses routine work to counts, surfacing milestones
|
|
9
9
|
* individually and newest-first.
|
|
10
10
|
*
|
|
11
|
-
* Fleet-wide
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* Fleet-wide and project-grouped BY DEFAULT: the question this command answers
|
|
12
|
+
* is "what are my agents doing", and agents run on every box, so a local-only
|
|
13
|
+
* flat stream answered a narrower question than anyone asked. Each run fans the
|
|
14
|
+
* same `activity --json` payload out across the fleet the way `agents feed`
|
|
15
|
+
* does, merges every peer's stream host-tagged, joins each item to live sessions
|
|
16
|
+
* (project / ticket / execution host), and buckets it by project -- one level,
|
|
17
|
+
* no sub-grouping, with the machines named in each project's header.
|
|
18
|
+
*
|
|
19
|
+
* `--local` scopes back to this machine, `--host <box>` to named peers, and
|
|
20
|
+
* `--flat` (or `--group-by none`) restores the single newest-first stream.
|
|
17
21
|
*/
|
|
18
22
|
import type { Command } from 'commander';
|
|
23
|
+
import { type ActivityGroupBy } from '../lib/activity.js';
|
|
19
24
|
/** Recursion guard: a peer answering a fan-out never re-fans-out itself. */
|
|
20
25
|
export declare const ACTIVITY_NO_FANOUT_ENV = "AGENTS_ACTIVITY_LOCAL";
|
|
26
|
+
interface ActivityOpts {
|
|
27
|
+
json?: boolean;
|
|
28
|
+
all?: boolean;
|
|
29
|
+
milestones?: boolean;
|
|
30
|
+
limit: number;
|
|
31
|
+
since?: number;
|
|
32
|
+
local?: boolean;
|
|
33
|
+
host?: string[];
|
|
34
|
+
device?: string[];
|
|
35
|
+
devicesAll?: boolean;
|
|
36
|
+
hostsAll?: boolean;
|
|
37
|
+
groupBy?: string;
|
|
38
|
+
flat?: boolean;
|
|
39
|
+
project?: string;
|
|
40
|
+
filter?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Does this hook-registration error concern one of the activity-log hooks?
|
|
44
|
+
* `registerHooksToSettings` prefixes each error with the manifest hook name
|
|
45
|
+
* (`<name>: script not found`), so the entry names are the discriminator.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isActivityHookError(error: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Is this error attributable to a named manifest hook at all? Errors that name
|
|
50
|
+
* no hook — `Failed to parse settings.json`, `Failed to write
|
|
51
|
+
* agents-cli-hooks.ts: …` — are agent-level aborts, not per-hook noise, and
|
|
52
|
+
* must never be filtered out.
|
|
53
|
+
*/
|
|
54
|
+
export declare function isManifestHookError(error: string, manifestNames: string[]): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* One compact trailing note for peers that didn't answer the fan-out. Now that
|
|
57
|
+
* every run dials the fleet, an offline box is routine -- but never silent: the
|
|
58
|
+
* reader has to know the timeline is missing a machine, not that the machine
|
|
59
|
+
* was idle. Empty string when everything answered.
|
|
60
|
+
*/
|
|
61
|
+
export declare function formatUnreachableNote(skipped: string[]): string;
|
|
62
|
+
/**
|
|
63
|
+
* Resolve `--group-by` / `--flat` into the grouping dimension, or `undefined`
|
|
64
|
+
* for a flat stream. Throws on an unknown value so the caller can exit 1 --
|
|
65
|
+
* a mistyped dimension must not silently fall back to the default.
|
|
66
|
+
*/
|
|
67
|
+
export declare function resolveActivityGrouping(opts: Pick<ActivityOpts, 'groupBy' | 'flat'>): ActivityGroupBy | undefined;
|
|
68
|
+
/** Which machines an invocation reads from. Pure -- no SSH, no device registry. */
|
|
69
|
+
export interface ActivityScope {
|
|
70
|
+
/** Read this box's own activity logs. */
|
|
71
|
+
includeLocal: boolean;
|
|
72
|
+
/** Dial peers over SSH. */
|
|
73
|
+
wantRemote: boolean;
|
|
74
|
+
/** Peers to dial; `undefined` means "every reachable device". */
|
|
75
|
+
remoteHosts?: string[];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Decide the read scope. Fleet-wide is the DEFAULT -- agents run on every box,
|
|
79
|
+
* so "what are my agents doing" is a fleet question -- and three things narrow
|
|
80
|
+
* it: `--local`, an explicit `--host`/`--device` list, and the recursion guard
|
|
81
|
+
* a peer answering a fan-out carries (else every peer would re-fan the fleet).
|
|
82
|
+
* `--devices-all`/`--hosts-all` are kept as explicit no-ops for scripts that
|
|
83
|
+
* already pass them.
|
|
84
|
+
*/
|
|
85
|
+
export declare function resolveActivityScope(opts: Pick<ActivityOpts, 'local' | 'host'>, self: string, noFanoutEnv?: boolean): ActivityScope;
|
|
21
86
|
export declare function registerActivityCommand(program: Command): void;
|
|
87
|
+
export {};
|
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { collapseActivity, enrichActivityEvents, ensureActivityLogHook, filterActivityEvents,
|
|
2
|
+
import { ACTIVITY_HOOK_DEFINITIONS, collapseActivity, enrichActivityEvents, capActivityEvents, ensureActivityLogHook, filterActivityByProject, filterActivityEvents, formatActivityGroupMeta, formatEnrichedActivityLine, formatProgressUpdate, groupActivity, mergeActivityEvents, parseActivityPayload, readRecentActivity, styleForEvent, tierForEvent, } from '../lib/activity.js';
|
|
3
3
|
import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
|
|
4
|
+
import { resolveProjectKey } from '../lib/project-key.js';
|
|
5
|
+
import { listProjectDefs, projectNameForCwd, resolveProjectNameForCwd } from '../lib/projects.js';
|
|
4
6
|
import { machineId, normalizeHost } from '../lib/machine-id.js';
|
|
5
7
|
import { shouldIncludeLocalFeed, remoteFeedHostsToDial } from './feed.js';
|
|
6
8
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
7
9
|
/** Recursion guard: a peer answering a fan-out never re-fans-out itself. */
|
|
8
10
|
export const ACTIVITY_NO_FANOUT_ENV = 'AGENTS_ACTIVITY_LOCAL';
|
|
11
|
+
/** `--group-by` values; `none` is the opt-out that `--flat` also selects. */
|
|
9
12
|
const GROUP_BY_VALUES = ['project', 'device', 'agent'];
|
|
10
|
-
|
|
13
|
+
const GROUP_BY_NONE = 'none';
|
|
14
|
+
/** Grouping when neither `--group-by` nor `--flat` is given. */
|
|
15
|
+
const DEFAULT_GROUP_BY = 'project';
|
|
16
|
+
/**
|
|
17
|
+
* How much further back than `--limit` the LOCAL log read goes, so routine
|
|
18
|
+
* churn on this box can't crowd the fleet's milestones out of the window
|
|
19
|
+
* ({@link capActivityEvents} does the real capping).
|
|
20
|
+
*/
|
|
21
|
+
const LOCAL_READ_HEADROOM = 5;
|
|
22
|
+
/**
|
|
23
|
+
* Wire the activity-log hooks into each hooks-capable version's settings.
|
|
24
|
+
*
|
|
25
|
+
* Only a failure that leaves THIS command's log unwritten is worth a warning.
|
|
26
|
+
* `registerHooksToSettings` reports every unresolved hook in the manifest — a
|
|
27
|
+
* missing `inject-session-id` script, someone else's half-installed plugin —
|
|
28
|
+
* and echoing all of them printed five wrapped lines of unrelated noise above
|
|
29
|
+
* the timeline on every run. Those belong to `agents doctor`, which exists to
|
|
30
|
+
* find them; here they are dropped. What still surfaces: an activity-hook
|
|
31
|
+
* failure, and any error attributable to NO manifest hook — a corrupt
|
|
32
|
+
* settings.json or an unwritable hooks file aborts registration for the whole
|
|
33
|
+
* agent, which is exactly a failure that leaves the activity log unwritten.
|
|
34
|
+
*/
|
|
11
35
|
async function installActivityHooks() {
|
|
12
36
|
const warnings = [];
|
|
13
37
|
const hookInstall = ensureActivityLogHook();
|
|
@@ -20,22 +44,45 @@ async function installActivityHooks() {
|
|
|
20
44
|
import('../lib/versions.js'),
|
|
21
45
|
]);
|
|
22
46
|
const manifest = parseHookManifest({ warn: false });
|
|
47
|
+
const manifestNames = Object.keys(manifest);
|
|
23
48
|
for (const { agent, version } of iterHooksCapableVersions({ agent: 'claude' })) {
|
|
24
49
|
const result = registerHooksToSettings(agent, getVersionHomePath(agent, version), manifest);
|
|
25
|
-
|
|
26
|
-
|
|
50
|
+
const ours = result.errors.filter((e) => isActivityHookError(e) || !isManifestHookError(e, manifestNames));
|
|
51
|
+
if (ours.length > 0)
|
|
52
|
+
warnings.push(`${agent}@${version}: ${ours.join('; ')}`);
|
|
27
53
|
}
|
|
28
54
|
return warnings;
|
|
29
55
|
}
|
|
30
|
-
/**
|
|
31
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Does this hook-registration error concern one of the activity-log hooks?
|
|
58
|
+
* `registerHooksToSettings` prefixes each error with the manifest hook name
|
|
59
|
+
* (`<name>: script not found`), so the entry names are the discriminator.
|
|
60
|
+
*/
|
|
61
|
+
export function isActivityHookError(error) {
|
|
62
|
+
return Object.keys(ACTIVITY_HOOK_DEFINITIONS).some((name) => error.startsWith(`${name}:`));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Is this error attributable to a named manifest hook at all? Errors that name
|
|
66
|
+
* no hook — `Failed to parse settings.json`, `Failed to write
|
|
67
|
+
* agents-cli-hooks.ts: …` — are agent-level aborts, not per-hook noise, and
|
|
68
|
+
* must never be filtered out.
|
|
69
|
+
*/
|
|
70
|
+
export function isManifestHookError(error, manifestNames) {
|
|
71
|
+
return manifestNames.some((name) => error.startsWith(`${name}:`));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Session facts (project / ticket / execution host) keyed for the read-time
|
|
75
|
+
* join. These sessions are this machine's, so their cwds get the canonical
|
|
76
|
+
* defined-project-first resolution rather than the bare cwd fold.
|
|
77
|
+
*/
|
|
78
|
+
function activityHintsFromSessions(sessions, resolveProject = resolveProjectKey) {
|
|
32
79
|
return sessions.map((s) => ({
|
|
33
80
|
sessionId: s.sessionId,
|
|
34
81
|
ticket: s.ticket?.id,
|
|
35
82
|
// Where the process actually lives — the SSH-resolved provenance host, else
|
|
36
83
|
// the cross-machine `machine` id. Normalized to match the event `host` form.
|
|
37
84
|
executionHost: s.provenance?.host ? normalizeHost(s.provenance.host) : s.machine,
|
|
38
|
-
project:
|
|
85
|
+
project: resolveProject(s.cwd),
|
|
39
86
|
}));
|
|
40
87
|
}
|
|
41
88
|
/**
|
|
@@ -70,6 +117,23 @@ function renderFlat(events, opts) {
|
|
|
70
117
|
}
|
|
71
118
|
process.stdout.write('\n');
|
|
72
119
|
}
|
|
120
|
+
/** Max peers named in the unreachable note before the rest collapse to `+N`. */
|
|
121
|
+
const UNREACHABLE_NAME_LIMIT = 4;
|
|
122
|
+
/**
|
|
123
|
+
* One compact trailing note for peers that didn't answer the fan-out. Now that
|
|
124
|
+
* every run dials the fleet, an offline box is routine -- but never silent: the
|
|
125
|
+
* reader has to know the timeline is missing a machine, not that the machine
|
|
126
|
+
* was idle. Empty string when everything answered.
|
|
127
|
+
*/
|
|
128
|
+
export function formatUnreachableNote(skipped) {
|
|
129
|
+
if (skipped.length === 0)
|
|
130
|
+
return '';
|
|
131
|
+
const named = skipped.slice(0, UNREACHABLE_NAME_LIMIT);
|
|
132
|
+
const rest = skipped.length - named.length;
|
|
133
|
+
const list = rest > 0 ? `${named.join(', ')} +${rest}` : named.join(', ');
|
|
134
|
+
const noun = skipped.length === 1 ? 'device' : 'devices';
|
|
135
|
+
return chalk.gray(` · ${skipped.length} ${noun} unreachable: ${list}\n\n`);
|
|
136
|
+
}
|
|
73
137
|
/** Print the bucketed view (`--group-by project|device|agent`). */
|
|
74
138
|
function renderGrouped(events, by, opts) {
|
|
75
139
|
const groups = groupActivity(events, by);
|
|
@@ -78,7 +142,11 @@ function renderGrouped(events, by, opts) {
|
|
|
78
142
|
const showProject = by !== 'project';
|
|
79
143
|
process.stdout.write(chalk.bold(`\n activity by ${by}\n`));
|
|
80
144
|
for (const group of groups) {
|
|
81
|
-
|
|
145
|
+
// `▸ <label> <meta>` -- label leads in cyan so projects scan down the
|
|
146
|
+
// left edge (mirrors the `agents sessions` overview), the counts and the
|
|
147
|
+
// machines that touched this project trail behind it in gray.
|
|
148
|
+
const meta = formatActivityGroupMeta(group, { showDevices: by !== 'device' });
|
|
149
|
+
process.stdout.write(`\n ${chalk.cyan('▸')} ${chalk.cyan.bold(group.label)} ${chalk.gray(meta)}\n`);
|
|
82
150
|
const { milestones, counts, subagentCount } = collapseActivity(group.events);
|
|
83
151
|
const shown = opts.all ? group.events : milestones;
|
|
84
152
|
for (const ev of shown) {
|
|
@@ -97,89 +165,168 @@ function renderGrouped(events, by, opts) {
|
|
|
97
165
|
}
|
|
98
166
|
process.stdout.write('\n');
|
|
99
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Resolve `--group-by` / `--flat` into the grouping dimension, or `undefined`
|
|
170
|
+
* for a flat stream. Throws on an unknown value so the caller can exit 1 --
|
|
171
|
+
* a mistyped dimension must not silently fall back to the default.
|
|
172
|
+
*/
|
|
173
|
+
export function resolveActivityGrouping(opts) {
|
|
174
|
+
if (opts.flat)
|
|
175
|
+
return undefined;
|
|
176
|
+
if (opts.groupBy === undefined)
|
|
177
|
+
return DEFAULT_GROUP_BY;
|
|
178
|
+
if (opts.groupBy === GROUP_BY_NONE)
|
|
179
|
+
return undefined;
|
|
180
|
+
if (!GROUP_BY_VALUES.includes(opts.groupBy)) {
|
|
181
|
+
throw new Error(`--group-by must be one of: ${[...GROUP_BY_VALUES, GROUP_BY_NONE].join(', ')}`);
|
|
182
|
+
}
|
|
183
|
+
return opts.groupBy;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Decide the read scope. Fleet-wide is the DEFAULT -- agents run on every box,
|
|
187
|
+
* so "what are my agents doing" is a fleet question -- and three things narrow
|
|
188
|
+
* it: `--local`, an explicit `--host`/`--device` list, and the recursion guard
|
|
189
|
+
* a peer answering a fan-out carries (else every peer would re-fan the fleet).
|
|
190
|
+
* `--devices-all`/`--hosts-all` are kept as explicit no-ops for scripts that
|
|
191
|
+
* already pass them.
|
|
192
|
+
*/
|
|
193
|
+
export function resolveActivityScope(opts, self, noFanoutEnv = process.env[ACTIVITY_NO_FANOUT_ENV] === '1') {
|
|
194
|
+
if (opts.local === true || noFanoutEnv)
|
|
195
|
+
return { includeLocal: true, wantRemote: false };
|
|
196
|
+
const explicitHosts = opts.host;
|
|
197
|
+
if (explicitHosts && explicitHosts.length > 0) {
|
|
198
|
+
const remoteHosts = remoteFeedHostsToDial(explicitHosts, self) ?? [];
|
|
199
|
+
return {
|
|
200
|
+
includeLocal: shouldIncludeLocalFeed(explicitHosts, self),
|
|
201
|
+
wantRemote: remoteHosts.length > 0,
|
|
202
|
+
remoteHosts,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return { includeLocal: true, wantRemote: true };
|
|
206
|
+
}
|
|
100
207
|
export function registerActivityCommand(program) {
|
|
101
208
|
program
|
|
102
209
|
.command('activity')
|
|
103
|
-
.description('Recent agent activity -- plans, PRs, worktrees, sub-agents
|
|
210
|
+
.description('Recent agent activity across the fleet -- plans, PRs, worktrees, sub-agents, by project')
|
|
104
211
|
.option('--json', 'Emit the (enriched) event list as JSON')
|
|
105
212
|
.option('--all', 'Include routine activity (file edits) inline, not collapsed')
|
|
106
213
|
.option('--milestones', 'Only milestone events (plans, PRs, worktrees, sub-agents)')
|
|
107
|
-
.option('-n, --limit <n>', 'Cap the
|
|
214
|
+
.option('-n, --limit <n>', 'Cap the milestones shown (routine work rides along as counts); with --all, caps every event', (v) => parseInt(v, 10), 40)
|
|
108
215
|
.option('--since <minutes>', 'Only events within the last N minutes', (v) => parseInt(v, 10))
|
|
109
216
|
.option('--local', 'Only this machine -- skip the cross-machine SSH fan-out')
|
|
110
217
|
.option('-H, --host <target...>', 'Scope to remote machine(s) over SSH; repeatable')
|
|
111
218
|
.option('--device <target...>', 'Alias for --host; repeatable')
|
|
112
|
-
.option('--devices-all', 'Fan out to every reachable device
|
|
219
|
+
.option('--devices-all', 'Fan out to every reachable device (the default; kept for scripts)')
|
|
113
220
|
.option('--hosts-all', 'Alias for --devices-all')
|
|
114
|
-
.option('--group-by <field>', 'Bucket the stream by project | device | agent')
|
|
221
|
+
.option('--group-by <field>', 'Bucket the stream by project | device | agent | none (default: project)')
|
|
222
|
+
.option('--flat', 'One newest-first stream instead of project buckets')
|
|
223
|
+
.option('--project <name>', 'Only items under one project; exact match on the resolved (defined-project-first) label')
|
|
115
224
|
.option('--filter <text>', 'Narrow to items matching a project / device / agent / event / ticket')
|
|
116
225
|
.addHelpText('after', `
|
|
117
226
|
Examples:
|
|
118
|
-
agents activity #
|
|
119
|
-
agents activity --
|
|
227
|
+
agents activity # the whole fleet, by project
|
|
228
|
+
agents activity --local # just this machine
|
|
229
|
+
agents activity --flat # one newest-first stream
|
|
120
230
|
agents activity --host yosemite-s1 # one box over SSH
|
|
121
|
-
agents activity --
|
|
122
|
-
agents activity --
|
|
231
|
+
agents activity --project rush # one defined project, fleet-wide
|
|
232
|
+
agents activity --filter RUSH-2100 # one ticket, fleet-wide
|
|
233
|
+
agents activity --group-by device # by machine instead of project
|
|
123
234
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
235
|
+
Fleet-wide and project-grouped by default: the same 'activity --json' runs on
|
|
236
|
+
every reachable peer and the streams merge host-tagged. Each item is enriched by
|
|
237
|
+
JOINING to live sessions (project, execution host, Linear ticket) -- never by
|
|
238
|
+
re-parsing transcripts. Project labels are canonical: a cwd inside a defined
|
|
239
|
+
project (~/.agents/projects/<name>.yaml) reads as that project's name, so a
|
|
240
|
+
multi-repo project is one bucket; anything else folds to its repository. Each
|
|
241
|
+
project header names the machines its work ran on.
|
|
127
242
|
`)
|
|
128
243
|
.action(async (opts) => {
|
|
129
244
|
if (opts.device?.length)
|
|
130
245
|
opts.host = [...(opts.host ?? []), ...opts.device];
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
246
|
+
let groupBy;
|
|
247
|
+
try {
|
|
248
|
+
groupBy = resolveActivityGrouping(opts);
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
process.stderr.write(chalk.red(`${err.message}\n`));
|
|
134
252
|
process.exitCode = 1;
|
|
135
253
|
return;
|
|
136
254
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (wantRemote && explicitHosts && explicitHosts.length > 0) {
|
|
145
|
-
includeLocal = shouldIncludeLocalFeed(explicitHosts, self);
|
|
255
|
+
// `parseInt` yields NaN for a non-numeric -n, and every downstream slice
|
|
256
|
+
// against NaN is empty — an unreadable flag would otherwise render as
|
|
257
|
+
// "No recent agent activity", which is a lie about the fleet.
|
|
258
|
+
if (!Number.isFinite(opts.limit) || opts.limit <= 0) {
|
|
259
|
+
process.stderr.write(chalk.red('--limit must be a positive number\n'));
|
|
260
|
+
process.exitCode = 1;
|
|
261
|
+
return;
|
|
146
262
|
}
|
|
263
|
+
const self = machineId();
|
|
264
|
+
const { includeLocal, wantRemote, remoteHosts } = resolveActivityScope(opts, self);
|
|
147
265
|
const warnings = includeLocal ? await installActivityHooks() : [];
|
|
148
266
|
const sinceMs = typeof opts.since === 'number' && opts.since > 0
|
|
149
267
|
? Date.now() - opts.since * 60_000
|
|
150
268
|
: undefined;
|
|
269
|
+
// Each peer contributes its own newest `-n` window, so the merged pool is
|
|
270
|
+
// already several times the cap. The local read is the one source that
|
|
271
|
+
// would otherwise be capped twice, and reading further back costs nothing
|
|
272
|
+
// (the files are tailed either way) — so it gets the headroom.
|
|
151
273
|
let events = includeLocal
|
|
152
|
-
? readRecentActivity({ sinceMs, limit: opts.limit })
|
|
274
|
+
? readRecentActivity({ sinceMs, limit: opts.limit * LOCAL_READ_HEADROOM })
|
|
153
275
|
: [];
|
|
276
|
+
// Peers that were dialed but never answered, reported once at the end
|
|
277
|
+
// rather than as a line each above the timeline.
|
|
278
|
+
let unreachable = [];
|
|
154
279
|
if (wantRemote) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
});
|
|
168
|
-
events = mergeActivityEvents(events, remote.items);
|
|
169
|
-
}
|
|
280
|
+
const remoteArgs = ['activity', '--json', '-n', String(opts.limit)];
|
|
281
|
+
if (typeof opts.since === 'number' && opts.since > 0)
|
|
282
|
+
remoteArgs.push('--since', String(opts.since));
|
|
283
|
+
const remote = await gatherRemoteAgentsJson({
|
|
284
|
+
args: remoteArgs,
|
|
285
|
+
noFanoutEnv: ACTIVITY_NO_FANOUT_ENV,
|
|
286
|
+
hosts: remoteHosts?.length ? remoteHosts : undefined,
|
|
287
|
+
parse: parseActivityPayload,
|
|
288
|
+
quiet: true,
|
|
289
|
+
});
|
|
290
|
+
events = mergeActivityEvents(events, remote.items);
|
|
291
|
+
unreachable = remote.skipped;
|
|
170
292
|
}
|
|
171
293
|
// Enrich by joining to live LOCAL sessions (ticket/project/execution host).
|
|
172
294
|
// Remote events arrive already enriched by their own peer, so the join only
|
|
173
295
|
// fills local ones; the pure fallbacks (cwd->project, host->device) cover
|
|
174
296
|
// everything either way. Skip the ps/lsof scan when there's nothing to show.
|
|
297
|
+
// Project labels are canonical: a defined project's name wins over the
|
|
298
|
+
// repo-level key — including over a stale or def-skewed pre-baked stamp
|
|
299
|
+
// (the def match is a pure prefix compare; foreign paths just don't match),
|
|
300
|
+
// so a multi-repo project buckets as one even across version skew.
|
|
301
|
+
const projectDefs = listProjectDefs();
|
|
302
|
+
const canonicalProject = (cwd) => projectNameForCwd(cwd ?? undefined, projectDefs);
|
|
303
|
+
const resolveProject = (cwd) => resolveProjectNameForCwd(cwd, projectDefs);
|
|
175
304
|
const sessions = includeLocal && events.length > 0 ? await getActiveSessions() : [];
|
|
176
|
-
let enriched = enrichActivityEvents(events, activityHintsFromSessions(sessions));
|
|
305
|
+
let enriched = enrichActivityEvents(events, activityHintsFromSessions(sessions, resolveProject), resolveProject, canonicalProject);
|
|
177
306
|
if (opts.milestones)
|
|
178
307
|
enriched = enriched.filter((e) => tierForEvent(e.event) === 'milestone');
|
|
308
|
+
if (opts.project) {
|
|
309
|
+
const unfiltered = enriched;
|
|
310
|
+
enriched = filterActivityByProject(enriched, opts.project);
|
|
311
|
+
// An exact-name flag that matches nothing deserves a useful answer, not
|
|
312
|
+
// a generic "no activity" that lies about the fleet.
|
|
313
|
+
if (enriched.length === 0 && unfiltered.length > 0) {
|
|
314
|
+
const seen = [...new Set(unfiltered.map((e) => e.project).filter((p) => !!p))].sort();
|
|
315
|
+
const defined = projectDefs.map((d) => d.name);
|
|
316
|
+
const parts = [
|
|
317
|
+
seen.length ? `seen: ${seen.join(', ')}` : '',
|
|
318
|
+
defined.length ? `defined: ${defined.join(', ')}` : '',
|
|
319
|
+
].filter(Boolean);
|
|
320
|
+
process.stderr.write(chalk.gray(` · no events for project "${opts.project}" (exact match)${parts.length ? ` — ${parts.join(' · ')}` : ''}\n`));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
179
323
|
if (opts.filter)
|
|
180
324
|
enriched = filterActivityEvents(enriched, opts.filter);
|
|
181
|
-
enriched = enriched.
|
|
325
|
+
enriched = capActivityEvents(enriched, opts.limit, { all: opts.all || opts.milestones });
|
|
182
326
|
if (opts.json) {
|
|
327
|
+
// stdout stays clean JSON, but an unanswered peer is never a silent drop.
|
|
328
|
+
if (unreachable.length > 0)
|
|
329
|
+
process.stderr.write(formatUnreachableNote(unreachable));
|
|
183
330
|
process.stdout.write(`${JSON.stringify(enriched, null, 2)}\n`);
|
|
184
331
|
return;
|
|
185
332
|
}
|
|
@@ -187,11 +334,13 @@ Linear ticket) -- never by re-parsing transcripts. --devices-all runs the same
|
|
|
187
334
|
process.stderr.write(chalk.yellow(` ! ${w}\n`));
|
|
188
335
|
if (enriched.length === 0) {
|
|
189
336
|
process.stdout.write(chalk.gray('No recent agent activity. Events appear here as agents create plans, open PRs, and spawn sub-agents.\n'));
|
|
337
|
+
process.stdout.write(formatUnreachableNote(unreachable));
|
|
190
338
|
return;
|
|
191
339
|
}
|
|
192
340
|
if (groupBy)
|
|
193
341
|
renderGrouped(enriched, groupBy, opts);
|
|
194
342
|
else
|
|
195
343
|
renderFlat(enriched, opts);
|
|
344
|
+
process.stdout.write(formatUnreachableNote(unreachable));
|
|
196
345
|
});
|
|
197
346
|
}
|
package/dist/commands/beta.js
CHANGED
|
@@ -4,6 +4,7 @@ const BETA_DESCRIPTIONS = {
|
|
|
4
4
|
drive: 'Google Drive integration for reading and writing files',
|
|
5
5
|
factory: 'Cloud-based agent dispatch via Rush Factory',
|
|
6
6
|
'session-sync': 'Cross-machine session transcript sync via R2 (daemon push/pull)',
|
|
7
|
+
projects: 'Named multi-repo projects with a progress rollup (agents projects)',
|
|
7
8
|
};
|
|
8
9
|
function parseFeatures(values) {
|
|
9
10
|
const valid = new Set(ALL_BETA_FEATURES);
|