@phnx-labs/agents-cli 1.20.82 → 1.20.84

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 CHANGED
@@ -1,5 +1,143 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.20.84
4
+
5
+ - **Agent onboarding cheat sheet and docs drift guard.** Added
6
+ `apps/cli/docs/AGENT-CHEATSHEET.md` as a one-page on-ramp for agents, wired it
7
+ from `apps/cli/AGENTS.md` and `apps/cli/docs/README.md`, and added
8
+ `scripts/verify-docs.sh` (plus a `verify-docs` npm script and CI job) to catch
9
+ broken relative links and missing entry-point wiring before merge.
10
+
11
+ - **Codex can now build, test, and install without escalating to YOLO.** Codex's
12
+ `workspace-write` sandbox blocks `$HOME`, so `cargo build`, `go build`, `npm/bun install`,
13
+ `pip install` etc. failed on their out-of-workspace cache writes (`~/.cargo`, `GOCACHE`,
14
+ `~/.npm`, `~/.cache`, …) — which is what pushed people to `--mode full`
15
+ (`--dangerously-bypass-approvals-and-sandbox`). agents-cli now writes a platform-resolved
16
+ baseline of **regenerable toolchain caches** into Codex's `config.toml`
17
+ (`[sandbox_workspace_write].writable_roots`) on permission sync — `~/.cargo`, `~/.rustup`,
18
+ `~/.npm`, `~/.bun`, `~/go`, `~/.deno`, `~/.gradle`, `~/.m2`, `~/.gem`, plus `~/Library/Caches`
19
+ + `~/Library/pnpm` on macOS or `~/.cache` + `~/.local/{share,state}` on Linux. Credential dirs
20
+ (`~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.config`, `~/.netrc`) are deliberately excluded, so
21
+ `--mode auto` stays a real sandbox — far narrower than danger-full-access. Any
22
+ `writable_roots` you set yourself are preserved (unioned, never clobbered). Source:
23
+ `apps/cli/src/lib/permissions.ts` (`codexDefaultWritableRoots`, `mergeCodexSandboxWrite`).
24
+
25
+ - **`agents sessions` team rows now show the team's target and teammate, not just
26
+ the slug.** Each teammate row reads `<team> · <teammate> · by <orchestrator> ·
27
+ <live turn | mission>`, where the mission is a one-line summary of the teammate's
28
+ spawn prompt (`assignedTask`, shown even before it has a transcript). Several
29
+ teams from one orchestrator stay legible (distinct team names) and each says what
30
+ it is for. `--active --json` carries `assignedTask`. Source:
31
+ `apps/cli/src/lib/session/active.ts`, `apps/cli/src/commands/sessions.ts`.
32
+
33
+ ## 1.20.83
34
+
35
+ - **Routines now treat date-specific cron schedules as one-shot jobs (RUSH-2074).**
36
+ `agents routines add --schedule "0 14 29 7 *"` now warns, persists
37
+ `runOnce: true`, marks the routine as one-shot in `routines list`, and
38
+ `agents routines cleanup` removes completed expired one-shots that still have
39
+ user-layer YAML. Source: `apps/cli/src/lib/routines.ts`,
40
+ `apps/cli/src/lib/scheduler.ts`, `apps/cli/src/commands/routines.ts`.
41
+
42
+ - **`agents routines list` groups terminal output by device and placement
43
+ (RUSH-2075).** The default table is bucketed under this machine, fleet-wide,
44
+ cloud, named devices, and named hosts with offline/unknown registry hints;
45
+ `--flat` keeps the legacy single table and `--json` remains a flat payload.
46
+ Source: `apps/cli/src/commands/routines.ts`.
47
+
48
+ - **`agents.yaml` no longer churns on every meta write.** `writeMetaUnlocked` wrote
49
+ the central config with `yaml.stringify`, which strips all comments — so the
50
+ freshly-serialized bytes never matched the comment-annotated file on disk,
51
+ `writeIfChanged` rewrote it on every meta write, and the perpetually-dirty tree
52
+ wedged `agents sync` ("Blocked by local changes") across the fleet. It now
53
+ serializes via a `yaml.Document` round-trip (`serializeCentral`) that edits only
54
+ the keys that actually changed, so comments, key ordering, and untouched
55
+ top-level blocks (e.g. `hosts:`) are byte-stable — and a write that changes no
56
+ central field leaves `agents.yaml` untouched. Source: `apps/cli/src/lib/state.ts`.
57
+
58
+ - **`agents run codex` can now reach the fleet from inside its sandbox.** Codex's
59
+ `workspace-write` sandbox blocks `$HOME` (verified against the live CLI and OpenAI's
60
+ sandbox docs), but the model routinely shells out to `agents ...`, whose runtime state
61
+ lives under `~/.agents` — the SSH askpass shim (`~/.agents/.cache/devices/askpass.sh`),
62
+ the device/stats cache, secrets, session writes, config tunings. Those inner writes hit
63
+ `EROFS` (`agents ssh` died before connecting, so a remote `agents run codex` could not
64
+ SSH or self-tune), and the fix was previously left to the caller (teams pass
65
+ `--add-dir ~/.agents` explicitly; a plain `agents run` never did). `buildExecCommand`
66
+ now grants `~/.agents` as an extra writable root whenever Codex runs `workspace-write`
67
+ (`--mode edit`/`auto`) — via `--add-dir` on fresh runs (deduped against user
68
+ `--add-dir`s) and via `-c sandbox_workspace_write.writable_roots` on resume forms (which
69
+ reject `--add-dir`). This is the officially-recommended way to widen scope "without
70
+ removing the sandbox entirely" — far narrower than `--mode skip` (danger-full-access).
71
+ `plan` (read-only) and `skip` (sandbox already dropped) are unaffected. Source:
72
+ `apps/cli/src/lib/exec.ts` (`buildExecCommand`, `codexWritableRootsConfig`).
73
+
74
+ - **Fix headless release signing (`errSecInternalComponent`).** `headless-sign-context.sh` now runs `security set-key-partition-list` right after unlocking `rush-signing.keychain-db`, authorizing `codesign`/`apple-tool` to use the Developer ID key non-interactively. Without it, the key's ACL prompts for UI approval that a headless SSH release session can't answer, so `codesign` fails and the npm publish halts. Idempotent; runs every release. Source: `apps/cli/scripts/headless-sign-context.sh`.
75
+
76
+ - **Cmd-Shift-V clip paste no longer breaks with an "sshd-keygen-wrapper would like
77
+ to control this computer" prompt.** A menu-bar helper started from an ssh session
78
+ registered the global chords but could never service them: macOS attributes its
79
+ Accessibility request to the responsible process, `/usr/libexec/sshd-keygen-wrapper`,
80
+ not to the helper's bundle, so the prompt named a process whose grant does nothing
81
+ for the paste (and, if granted, hands keystroke synthesis to everything any ssh
82
+ session spawns). `RegisterEventHotKey` is first-come, and the prompt naming
83
+ sshd-keygen-wrapper is itself the evidence that this copy — not the trusted
84
+ launchd-managed one — had registered Cmd-Shift-V and was servicing it. The
85
+ interactive mode now refuses to start over a remote shell, and refuses
86
+ unrecognized arguments: an unknown flag used to fall straight through to the
87
+ status-bar app, which is how a stray `MenubarHelper --self-test` from a verify run
88
+ became a permanent second helper. `launchctl bootstrap` (`agents menubar enable`)
89
+ is unaffected, including when run over ssh. Source:
90
+ `apps/cli/menubar/Sources/MenubarHelper/Guards.swift`.
91
+
92
+ - **`agents menubar status` now names a second helper process instead of reporting a
93
+ healthy `running: yes`.** The check was `pgrep -f MenubarHelper`, which matches any
94
+ process with that name, so a stray copy holding the global chords looked identical
95
+ to a working install. Status now identifies the helper by its resolved executable
96
+ (`ps -o comm=`), reports `running` only for the installed bundle, and lists every
97
+ other live copy with its pid under `foreignInstances` (also in `--json`). Source:
98
+ `apps/cli/src/lib/menubar/install-menubar.ts`.
99
+
100
+ - **The menu bar now says so when a hotkey is unavailable or the paste is not
101
+ permitted.** A `RegisterEventHotKey` conflict only wrote a line to a launchd log,
102
+ and a missing Accessibility grant made `Clip.inject` return silently — both looked
103
+ exactly like a dead hotkey. A stolen chord now posts a notification naming it, and a
104
+ denied grant copies the `host:path` reference to the clipboard and says which
105
+ setting to grant, so the clip is never lost. Source:
106
+ `apps/cli/menubar/Sources/MenubarHelper/Hotkey.swift`,
107
+ `apps/cli/menubar/Sources/MenubarHelper/Clip.swift`.
108
+
109
+ - **Fix the release catch-up path aborting on an unbound variable.** When a release PR had already merged and only the tag + publish remained, `release.sh` re-validated CI and then aborted with `line 933: RELEASE_COMMIT: unbound variable`, so the retry never reached npm. The catch-up block that runs when `main` sits exactly at the release merge commit never set `HISTORICAL_CATCHUP`, so phase 4 took the normal-release branch and read `RELEASE_COMMIT`, which only the branch-creating path defines. It now sets the flag, and phase 4 resolves the release commit from the merged PR (`MERGED_RELEASE_SHA` + `CI_TESTED_HEAD`) as intended. This is why 1.20.79, 1.20.80, and 1.20.81 were tagged but never published. Source: `apps/cli/scripts/release.sh`.
110
+
111
+ - **Removed `agents check` / `agents resources` now forward to their replacements
112
+ instead of erroring (RUSH-1234).** After the command consolidation, running the
113
+ removed names produced a bare `unknown command` (their edit-distance to `doctor`
114
+ was too far to even trigger a "did you mean"). They are now hidden tombstone
115
+ commands that print a one-line deprecation notice to stderr and re-run the
116
+ replacement, preserving flags and exit codes: `agents check …` runs
117
+ `agents doctor --check …` (so `--json` / `--quiet` / `--devices` and the CI
118
+ drift-gate exit code carry through), and `agents resources …` runs
119
+ `agents view --merged …` (with `agents inspect <target>` pointed to for
120
+ per-agent/per-repo detail). The notice goes to stderr so a `--json` consumer's
121
+ stdout stays clean. Source: `apps/cli/src/index.ts`.
122
+
123
+ - **`agents sessions --host`/`--device <box>` now opens the interactive fleet
124
+ browser instead of a raw text dump.** A bare remote listing on a TTY folds the
125
+ named box into the same preview-rich, selectable picker as the local view (it
126
+ previously short-circuited to the legacy per-host stream — non-interactive, no
127
+ previews). A `--host` *query*, a render/filter flag, `--json`, or a
128
+ non-interactive caller keep the streamed output. Source:
129
+ `apps/cli/src/commands/sessions.ts`.
130
+
131
+ - **`agents sessions` now shows which orchestrator spawned each team.** A teams
132
+ teammate row was keyed off its orchestrator's session id (captured from
133
+ `AGENTS_SESSION_ID` at spawn), which both hid the lineage and mislabeled the
134
+ teammate with the orchestrator's id/topic. The teammate now keys off its own
135
+ transcript, exposes the orchestrator as `orchestratorSessionId` (+ a resolved
136
+ `orchestratorLabel`) in `--active --json`, and the listing renders
137
+ `<team> · by <orchestrator>` so "which session spun up this team" is answerable
138
+ at a glance. Source: `apps/cli/src/lib/session/active.ts`,
139
+ `apps/cli/src/commands/sessions.ts`.
140
+
3
141
  ## 1.20.82
4
142
 
5
143
  - **Codex hook sync no longer leaves startup warnings after upgrades.** The Codex
package/README.md CHANGED
@@ -306,7 +306,7 @@ agents sessions detach a1b2c3d4 # go headless in the background, keep workin
306
306
  agents sessions attach a1b2c3d4 # resume it interactively, right here
307
307
  ```
308
308
 
309
- Both are agent-agnostic -- they route through the same `agents run --resume` path (native resume for Claude/Codex, `/continue` replay for the rest). `agents sessions --active` marks each session's `presence` -- `attached` (you're watching it), `background` (running headless), or `parked` (its background run finished) -- so the menu bar and Factory show where every agent is. In the Factory extension, **Agents: Detach** (`Cmd/Ctrl+K B`) and **Agents: Attach** (`Cmd/Ctrl+K A`) do the same over the focused terminal.
309
+ Both are agent-agnostic -- they route through the same `agents run --resume` path (native resume for Claude/Codex, `/continue` replay for the rest). `agents sessions --active` shows each session's **owner** (the human who launched it, resolved from the tailnet identity, or `-` for an unresolved local run) and its `presence` -- `attached` (you're watching it), `background` (running headless), or `parked` (its background run finished) -- so the menu bar and Factory show who is running what, and where. In the Factory extension, **Agents: Detach** (`Cmd/Ctrl+K B`) and **Agents: Attach** (`Cmd/Ctrl+K A`) do the same over the focused terminal.
310
310
 
311
311
  ---
312
312
 
package/dist/bin/agents CHANGED
Binary file
@@ -24,6 +24,7 @@
24
24
  import type { Command } from 'commander';
25
25
  import { type FleetDivergenceReport } from '../lib/devices/fleet-divergence.js';
26
26
  import { type VersionResourceReport } from '../lib/doctor-diff.js';
27
+ import { type DuplicateVersionHook } from '../lib/hooks.js';
27
28
  import { type SyncStatusRow, type OrphanRow } from '../lib/drift.js';
28
29
  import { type FetchStatusMarker } from '../lib/auto-pull.js';
29
30
  export declare function wrapLine(prefix: string, text: string, width?: number): string[];
@@ -106,5 +107,5 @@ export declare function healthBlockLines(verdict: DoctorVerdict, opts: {
106
107
  * and orphan resources into severity-tagged findings. Agent-agnostic: every
107
108
  * installed version is classified the same way. Pure, so it is unit-testable.
108
109
  */
109
- export declare function computeOverviewHealth(syncRows: SyncStatusRow[], orphanRows: OrphanRow[], repoBehindMarkers: FetchStatusMarker[]): DoctorVerdict;
110
+ export declare function computeOverviewHealth(syncRows: SyncStatusRow[], orphanRows: OrphanRow[], repoBehindMarkers: FetchStatusMarker[], duplicateHooks?: DuplicateVersionHook[]): DoctorVerdict;
110
111
  export declare function registerDoctorCommand(program: Command): void;
@@ -18,7 +18,7 @@ import { formatSignInBadge } from '../lib/signin-badge.js';
18
18
  import { getGlobalDefault, getVersionHomePath, isVersionInstalled, listInstalledVersions, parseAgentSpec, } from '../lib/versions.js';
19
19
  import { loadManifest, isStale } from '../lib/staleness/index.js';
20
20
  import { diffVersionResources, DOCTOR_ALL_KINDS, } from '../lib/doctor-diff.js';
21
- import { checkVersionHookWiring, registerHooksToSettings } from '../lib/hooks.js';
21
+ import { checkVersionHookWiring, inspectDuplicateVersionHooks, registerHooksToSettings } from '../lib/hooks.js';
22
22
  import { isVersionIsolated } from '../lib/versions.js';
23
23
  import { computeDrift, checkSyncStatus, countOrphans, computeSourceBehind } from '../lib/drift.js';
24
24
  import { readAuthHealthCache, summarizeHostAuth } from '../lib/auth-health.js';
@@ -67,11 +67,11 @@ function printWrappedLine(prefix, text) {
67
67
  for (const line of wrapLine(prefix, text))
68
68
  console.log(chalk.gray(line));
69
69
  }
70
- function renderOverviewText(clis, syncRows, orphanRows, hostClis, signIn, repoBehindMarkers) {
70
+ function renderOverviewText(clis, syncRows, orphanRows, hostClis, signIn, repoBehindMarkers, duplicateHooks) {
71
71
  // Triaged health banner FIRST, so a user running bare `agents doctor` sees what
72
72
  // is unhealthy, why it matters, and the exact fix before scrolling the detail
73
73
  // sections below. Same triage model as target mode, aggregated across versions.
74
- const overviewHealth = computeOverviewHealth(syncRows, orphanRows, repoBehindMarkers);
74
+ const overviewHealth = computeOverviewHealth(syncRows, orphanRows, repoBehindMarkers, duplicateHooks);
75
75
  console.log(chalk.bold('Health'));
76
76
  renderHealthBlock(overviewHealth, {
77
77
  healthySummary: syncRows.length
@@ -827,9 +827,26 @@ function renderHealthBlock(verdict, opts) {
827
827
  * and orphan resources into severity-tagged findings. Agent-agnostic: every
828
828
  * installed version is classified the same way. Pure, so it is unit-testable.
829
829
  */
830
- export function computeOverviewHealth(syncRows, orphanRows, repoBehindMarkers) {
830
+ export function computeOverviewHealth(syncRows, orphanRows, repoBehindMarkers, duplicateHooks = []) {
831
831
  const issues = [];
832
832
  const pretty = (agent, version) => `${AGENT_NAMES[agent] || agent}@${version}`;
833
+ // critical/warning: same hook resource materialized in several version homes.
834
+ // Different content is more severe because a stale copy can disagree with
835
+ // the active gate; byte-identical copies are noise and duplicate runtime cost.
836
+ for (const finding of duplicateHooks) {
837
+ const versions = finding.copies.map((copy) => copy.version).join(', ');
838
+ const active = finding.authoritative.version;
839
+ const drift = finding.kind === 'drift';
840
+ issues.push({
841
+ severity: drift ? 'critical' : 'warning',
842
+ category: drift ? 'duplicate-hook-drift' : 'duplicate-hook',
843
+ subject: `${finding.agent}/${finding.name}`,
844
+ impact: `${drift ? 'different content' : 'identical content'} across versions ${versions}; ${active} is authoritative`,
845
+ fix: `agents sync ${finding.agent}@${active} --yes`,
846
+ text: `${finding.name} ${drift ? 'drift' : 'duplicated'} across ${versions}`,
847
+ color: drift ? 'red' : 'yellow',
848
+ });
849
+ }
833
850
  // critical: unwired hooks / broken settings.json per version
834
851
  for (const row of syncRows) {
835
852
  const n = row.unwiredHooks ?? 0;
@@ -1406,6 +1423,7 @@ export function registerDoctorCommand(program) {
1406
1423
  const orphanRows = countOrphans();
1407
1424
  const hostClis = listCliStatus(cwd);
1408
1425
  const repoBehindMarkers = readRepoBehindMarkers();
1426
+ const duplicateHooks = inspectDuplicateVersionHooks(cwd);
1409
1427
  // Advisory login state per installed agent (file-based getAccountInfo,
1410
1428
  // no home → the account-global/active credential). Best-effort: a probe
1411
1429
  // failure just leaves that agent's badge as "logged out".
@@ -1433,7 +1451,8 @@ export function registerDoctorCommand(program) {
1433
1451
  // Triaged overview health — severity/category/subject/impact/fix per
1434
1452
  // finding, aggregated across versions. Additive; existing consumers
1435
1453
  // reading `sync`/`orphans`/`repos` are unaffected.
1436
- health: computeOverviewHealth(syncRows, orphanRows, repoBehindMarkers),
1454
+ health: computeOverviewHealth(syncRows, orphanRows, repoBehindMarkers, duplicateHooks),
1455
+ duplicateHooks,
1437
1456
  // This host's harness inventory — installed resources per kind,
1438
1457
  // installed version ids per agent, and `.agents`/`.system` repo
1439
1458
  // state — so `agents doctor --devices` can compare presence across
@@ -1461,7 +1480,7 @@ export function registerDoctorCommand(program) {
1461
1480
  }, null, 2));
1462
1481
  return;
1463
1482
  }
1464
- renderOverviewText(clis, syncRows, orphanRows, hostClis, signIn, repoBehindMarkers);
1483
+ renderOverviewText(clis, syncRows, orphanRows, hostClis, signIn, repoBehindMarkers, duplicateHooks);
1465
1484
  // Point at the interactive reconcile when anything is out of sync — the
1466
1485
  // report shouldn't be a dead end. `agents status` runs the unified
1467
1486
  // home-reading engine and offers to sync (opt-in, never auto-fires here).
@@ -65,6 +65,18 @@ export function registerMenubarCommands(program) {
65
65
  console.log(` current version ${chalk.gray(s.currentVersion)}`);
66
66
  console.log(` bundle source ${s.source ? chalk.gray(s.source) : chalk.red('missing (cannot enable)')}`);
67
67
  console.log(` disabled by user ${yn(s.disabledByUser)}`);
68
+ if (s.foreignInstances.length > 0) {
69
+ // RegisterEventHotKey is first-come, so the helper that registered the
70
+ // chord first owns Cmd-Shift-V/O. A process list cannot say which that
71
+ // was — only that a rival exists — so report the conflict, not a
72
+ // winner. The loser
73
+ // has no other symptom: its chords simply never fire.
74
+ const n = s.foreignInstances.length;
75
+ console.log(chalk.yellow(`\n ${n} other helper process${n === 1 ? '' : 'es'} running — ${n === 1 ? 'it' : 'they'} may hold Cmd-Shift-V/O instead of the installed one:`));
76
+ for (const p of s.foreignInstances)
77
+ console.log(chalk.gray(` ${p.pid} ${p.executable}`));
78
+ console.log(chalk.gray(' End it, then `agents menubar enable` to restart the installed helper.'));
79
+ }
68
80
  if (s.stale) {
69
81
  console.log(chalk.yellow('\n Installed helper is stale — runs on next `agents` startup, or `agents menubar enable` now.'));
70
82
  }
@@ -6,13 +6,20 @@
6
6
  * Also exposes scheduler lifecycle controls (start/stop/status/logs).
7
7
  */
8
8
  import type { Command } from 'commander';
9
- import type { RunMeta } from '../lib/routines.js';
9
+ import type { JobConfig, RunMeta } from '../lib/routines.js';
10
+ import type { DeviceRegistry } from '../lib/devices/registry.js';
10
11
  /**
11
12
  * Human-friendly wall-clock a run took (e.g. " · 3 min", " · 45 sec"), or ""
12
13
  * when it hasn't completed or timestamps are unparseable. Leading separator lets
13
14
  * callers drop it straight into a status line.
14
15
  */
15
16
  export declare function formatRunDuration(startedAt: string, completedAt: string | null): string;
17
+ export interface RoutineListGroup {
18
+ key: string;
19
+ title: string;
20
+ jobs: JobConfig[];
21
+ }
22
+ export declare function groupRoutineJobsByDevice(jobs: JobConfig[], registry: DeviceRegistry, self?: string): RoutineListGroup[];
16
23
  export declare function buildRunsJson(runs: RunMeta[]): Record<string, unknown>[];
17
24
  /** Register the `agents routines` command tree. */
18
25
  export declare function registerRoutinesCommands(program: Command): void;