@phnx-labs/agents-cli 1.22.0 → 1.22.3
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 +115 -0
- package/README.md +2 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/doctor.js +34 -2
- package/dist/commands/exec.js +1 -1
- package/dist/commands/projects.js +128 -66
- package/dist/commands/secrets.js +9 -5
- package/dist/commands/teams.js +2 -2
- package/dist/commands/watchdog.js +64 -5
- package/dist/lib/app-bundle-install.d.ts +17 -0
- package/dist/lib/app-bundle-install.js +94 -0
- package/dist/lib/devices/doctor-findings.js +12 -4
- package/dist/lib/devices/doctor-overview-cache.d.ts +45 -0
- package/dist/lib/devices/doctor-overview-cache.js +168 -0
- package/dist/lib/devices/fleet.js +7 -2
- package/dist/lib/devices/self-host.d.ts +9 -0
- package/dist/lib/devices/self-host.js +61 -0
- package/dist/lib/hosts/passthrough.js +8 -6
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.js +27 -23
- package/dist/lib/project-status.d.ts +7 -0
- package/dist/lib/project-status.js +9 -0
- package/dist/lib/projects.d.ts +26 -2
- package/dist/lib/projects.js +69 -9
- package/dist/lib/rotate.d.ts +16 -1
- package/dist/lib/rotate.js +33 -7
- 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/secrets/bundles.d.ts +20 -0
- package/dist/lib/secrets/bundles.js +50 -0
- package/dist/lib/secrets/install-helper.js +28 -31
- package/dist/lib/share/config.d.ts +14 -6
- package/dist/lib/share/config.js +23 -5
- package/dist/lib/types.d.ts +10 -0
- package/dist/lib/versions.js +69 -22
- package/dist/lib/watchdog/rotate.d.ts +218 -0
- package/dist/lib/watchdog/rotate.js +378 -0
- package/dist/lib/watchdog/runner.d.ts +33 -1
- package/dist/lib/watchdog/runner.js +303 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,120 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.3
|
|
4
|
+
|
|
5
|
+
- **Menubar home-base self-test accepts `MenubarHelper-universal`.** The
|
|
6
|
+
signed-helper gate required `executablePath` to end in exactly `MenubarHelper`,
|
|
7
|
+
but lipo production builds name the binary `MenubarHelper-universal`, so every
|
|
8
|
+
1.22.2 publish on mac-mini failed after the release PR had already merged and
|
|
9
|
+
tagged. Source: `menubar/Sources/MenubarHelper/ChildProcessSelfTest.swift`.
|
|
10
|
+
|
|
11
|
+
## 1.22.2
|
|
12
|
+
|
|
13
|
+
- **Menu bar recovers from a stale single-instance lock instead of staying dead.**
|
|
14
|
+
The helper's lock fd is now opened `O_CLOEXEC`, so a spawned `doctor` child can
|
|
15
|
+
never inherit it and hold the `menubar.lock` flock after the helper crashes; and
|
|
16
|
+
`SingleInstance.acquire` now self-heals — when the flock is held but no live
|
|
17
|
+
`MenubarHelper` owns it (a leaked orphan / dead pid), it reaps the orphan and
|
|
18
|
+
retries rather than exiting as "already running". Previously a leaked orphan
|
|
19
|
+
bricked the menu bar until reboot. The headless Swift self-tests (single-instance
|
|
20
|
+
+ child-process) now run as a build gate (`menubar/scripts/test-menubar.sh`),
|
|
21
|
+
which nothing invoked before. Source: `apps/cli/menubar/Sources/MenubarHelper/SingleInstance.swift`.
|
|
22
|
+
|
|
23
|
+
- **`agents projects` definitions can now carry goals — the OKR-shaped "why".**
|
|
24
|
+
A project serves one or more `goals[]`, each an `objective` (the outcome) plus an
|
|
25
|
+
optional `measure` (the key result). Set them at scaffold time with
|
|
26
|
+
`agents projects add <name> --goal "objective:measure"` (repeatable), replace them
|
|
27
|
+
later with `agents projects set <name> --goal …`, or hand-edit the YAML. Goals show
|
|
28
|
+
on the `status` card (compact) and in `projects view` (in full), and survive a
|
|
29
|
+
`--from-linear` re-import like every other hand-set field. Milestones (pulled from
|
|
30
|
+
Linear) remain the dated checkpoints toward these goals. Source:
|
|
31
|
+
`apps/cli/src/lib/projects.ts`, `apps/cli/src/commands/projects.ts`.
|
|
32
|
+
|
|
33
|
+
- **Balanced rotation no longer picks version homes that only inherit the active login.**
|
|
34
|
+
`getAccountInfo` falls back to the active/global HOME credential so `agents view`
|
|
35
|
+
still shows who is signed in when a version home has no auth file of its own.
|
|
36
|
+
Launch paths isolate config (`GROK_HOME`, `CODEX_HOME`, …) to the per-version home,
|
|
37
|
+
so those empty homes died at spawn with "Not signed in" after balanced picked them
|
|
38
|
+
(observed: `grok@0.2.118` with no `auth.json` looking signed-in via `~/.grok` →
|
|
39
|
+
`0.2.32`). Rotation now requires a real per-version credential when we know where
|
|
40
|
+
it lives (`credentialPresence.perVersion`). Source: `src/lib/rotate.ts`
|
|
41
|
+
(`isLaunchableSignedIn`, `collectRunCandidates`).
|
|
42
|
+
|
|
43
|
+
- **A `never`-policy secrets bundle now actually stays silent — no more Touch ID for a bundle you set to silent, and no more double prompt.** Two bugs made `agents secrets policy <b> never` a lie. (1) The command rewrote only the bundle *metadata*, never the value items — but macOS gates each read on the item's own ACL, not the tier label, so a bundle created under `hold`/`always` kept its biometry ACL and kept popping Touch ID forever after the switch. `policy` now reconciles the value items to the new tier (`reAclBundleItems`): tightening to `never` re-stores them no-ACL (a single last prompt to read them once), and loosening back re-attaches the gate. (2) The signed keychain helper's just-in-time migration (`migrateInline`/`rehomeOrphan`) re-stamped a biometry ACL onto *every* `agents-cli.*` item it touched on read — including bundle metadata and the HMAC key, which are supposed to be silent — so a metadata/hmackey read in its own helper process raised a *second* Touch ID sheet on top of the value read. The migration now re-adds silent items (metadata, hmackey) without an ACL, so metadata enumeration and the pre-value hmackey read never prompt. Net: unlock/read a `never` bundle once and it stays silent through sleep, reboot, 30+ days, an agents-cli upgrade, and a macOS upgrade — with no Touch ID and no passphrase. The `never` tier's durability and attribution guarantees are now written into the `§Secrets` spec (SEC-19, SEC-27, SEC-28). Source: `apps/cli/src/commands/secrets.ts`, `apps/cli/src/lib/secrets/bundles.ts`, `apps/cli/src/lib/secrets/keychain-helper.swift`, `apps/cli/docs/specifications.md`.
|
|
44
|
+
|
|
45
|
+
- **The per-run Touch ID storm is fixed: `agents run` no longer pops a sheet to auto-inject the share token.** On every `agents run`, `shareRuntimeEnv` auto-reads the `share` bundle's R2 write token to hand it to the spawned agent — and because `share` is a keychain bundle that is rarely broker-held, an interactive read spawned the helper and raised Touch ID on EVERY launch. Auto-injecting a token on an agent launch is a background convenience, not a user-initiated secret access, so it must never raise a sheet (SEC-13): the read is now always `agentOnly` — it resolves the token from the injected env or an already-held / no-ACL bundle and silently returns nothing otherwise (the agent can still publish via its own explicit `agents share`). And a NEW `share` bundle now defaults to the `never` tier (no biometry ACL) — the write token is low-sensitivity automation infra — so auto-share is silent with no unlock at all; an existing bundle keeps its tier (change it with `agents secrets policy share never`, which now actually strips the ACL). Source: `apps/cli/src/lib/share/config.ts`, `apps/cli/src/commands/exec.ts`.
|
|
46
|
+
|
|
47
|
+
- **The daemon watchdog now rotates rate-limited sessions in place.** A stalled session whose transcript tail shows a hard account limit ("You've hit your weekly limit · resets …", "usage limit reached", "out of credits") is rotated instead of nudged: the tick gates on the same first-party healthy-account selection `agents run auto` makes (`collectHarnessCandidates` + `pickHarnessWeighted` — zero healthy logs one `rotate` skip event per cooldown window to `watchdog.log` and leaves the terminal untouched), injects the harness's exit sequence (claude: Esc, Ctrl+C, Ctrl+C; codex/gemini/cursor/opencode: Ctrl+C twice), relaunches `agents run auto --interactive --session-id <uuid>` in the SAME tab via the inject rail, then — once the new session's TUI is live — injects the resume replay for the old session. Readiness is the new session's transcript (primary) or a fresh active session correlated by cwd + machine, never an unrelated one; the wait is bounded (60s), and on timeout the session is flagged with a bare-shell message pointing at a manual `agents run auto` and suppressed for 15m before retry — never blind-typed into. The state machine (`exiting → launching → awaiting-tui → replaying → done | failed`) persists at `~/.agents/.cache/state/watchdog/rotate/<sessionId>.json` and spans ticks via a post-loop sweep. Config: **`agents watchdog rotate on|off`** writes `watchdog.rotate` in `~/.agents/agents.yaml` (default on; rotate-only, nudging is unaffected), honored per tick; `agents watchdog status` / `--json` report the rotate config and every persisted rotate state. This replaces the Factory extension's own watchdog rotate loop, which is being deleted in the companion change. Source: `apps/cli/src/lib/watchdog/rotate.ts`, `apps/cli/src/lib/watchdog/runner.ts`, `apps/cli/src/commands/watchdog.ts`.
|
|
48
|
+
|
|
49
|
+
## 1.22.1
|
|
50
|
+
|
|
51
|
+
- **`agents doctor` de-noise: never-synced and cross-version hook drift are warnings, not criticals (RUSH-2162).** The CRITICAL section now holds only "needs you now" problems — a logged-out account, or a hook/plugin missing from a version you keep synced. A version that was never synced (an old/unused install with nothing installed) and a hook that merely *differs* across versions (installed but stale) are surfaced as WARNINGs instead, cutting the critical count on a busy machine from ~11 to the handful that actually need action. Source: `apps/cli/src/lib/devices/doctor-findings.ts`.
|
|
52
|
+
|
|
53
|
+
- **The "… is damaged and can't be opened" dialog stops — both helper `.app` bundles now install atomically and serialized.** The secrets keychain helper (`Agents CLI.app`) and the menu-bar helper (`MenubarHelper.app`) are each (re)installed on the hot path of ordinary `agents` invocations, and both did a non-atomic `rm -rf dest` + `cp -R src dest` straight onto the live bundle. On a busy box dozens of concurrent invocations raced that path, so a reader (Gatekeeper, or an exec of the bundle) could see a half-written `.app` — a truncated Mach-O / mismatched code signature — which macOS reports as damaged. A new shared installer (`lib/app-bundle-install.ts`, replacing the two duplicated copy functions) stages the copy in a sibling dir and swaps it in with renames (the live bundle is only ever a complete, signed `.app`, and a failed copy never touches it), and serializes concurrent installers behind the shared `withFileLock` with a double-checked skip so a burst copies once instead of stampeding. Source: `apps/cli/src/lib/app-bundle-install.ts`, `apps/cli/src/lib/secrets/install-helper.ts`, `apps/cli/src/lib/menubar/install-menubar.ts`.
|
|
54
|
+
|
|
55
|
+
- **`agents doctor --json` no longer stampedes into dozens of concurrent runs — the overview is singleflighted and cached, with a new `--refresh` to force a live recompute.** The bare `doctor --json` overview probes every host CLI, every agent's sign-in, and every agent×version diff — seconds on an idle box, minutes on a loaded one. The menu-bar helper polls it on a timer with only a per-*process* in-flight guard, so a helper relaunch (or any second poller) each launched its own live compute, and a helper killed mid-run orphaned a `doctor --json` that kept spinning — stacking to dozens of concurrent runs pinning the CPU. Now a fresh snapshot (< 90s) serves instantly from a disk cache, and when a live compute IS needed exactly one runs while every other caller serves its result (a lock-directory singleflight that self-heals if the computer dies). `agents doctor --json --refresh` bypasses the cache. Source: `apps/cli/src/lib/devices/doctor-overview-cache.ts`, `apps/cli/src/commands/doctor.ts`.
|
|
56
|
+
|
|
57
|
+
- **`doctor --json` releases its singleflight lock before it returns.** The overview gate
|
|
58
|
+
fired the lock release without awaiting it on the path where a waiter serves the winner's
|
|
59
|
+
fresh snapshot, so the call returned with the lockfile still on disk. The next caller then
|
|
60
|
+
retried against a lock that was already logically free — the pile-up the gate exists to
|
|
61
|
+
prevent, narrowed to the window between return and unlink. The release is now awaited.
|
|
62
|
+
The existing coalescing test failed 5 times in 15 runs before this and 0 in 15 after.
|
|
63
|
+
Source: `apps/cli/src/lib/devices/doctor-overview-cache.ts`.
|
|
64
|
+
|
|
65
|
+
- **`agents add grok@latest` no longer strands the freshly-downloaded binary in the old version's home.** When the post-install version probe (`<cli> --version`) transiently failed right after grok's self-updating installer exited, `installVersion` silently fell back to the literal string `'latest'` as the resolved version — creating a bogus `versions/grok/latest/` directory and defeating `relocateGrokBinaryToVersionHome`'s exact-filename match (its regex could never match `grok-latest-...`, since the real file is named `grok-<semver>-<platform>`). The real multi-hundred-MB binary was left behind in the PREVIOUS default's downloads dir, and `agents view grok` never listed the new version as installed even though `agents add` reported success. The probe now retries briefly instead of silently falling back, and fails loudly if it still can't resolve a version rather than corrupting the version bookkeeping. Relocation also now self-heals: if the current `~/.grok` symlink target has nothing matching, it sweeps every other installed grok version home for a binary stranded by a past occurrence of this bug. Source: `apps/cli/src/lib/versions.ts`.
|
|
66
|
+
|
|
67
|
+
- **A blocked menu-bar row now takes you to the session (RUSH-2110).** A NEEDS-YOU row
|
|
68
|
+
exists because an agent is waiting on you, but its only action was "Reveal working
|
|
69
|
+
dir", which unblocks nothing — you still had to go find the session by hand. Blocked
|
|
70
|
+
rows now lead with **Focus session**, which runs `agents focus <id>`: attach the live
|
|
71
|
+
terminal, or open a new tab and resume, cross-host. Reveal stays underneath. Both
|
|
72
|
+
render paths are covered — the single inline row and each entry inside a collapsed
|
|
73
|
+
multi-waiter group. A row the engine could not identify (a cloud task, a stale
|
|
74
|
+
sentinel) simply omits the item rather than offering an action that would do nothing.
|
|
75
|
+
Source: `apps/cli/menubar/Sources/MenubarHelper/StatusItemController.swift`,
|
|
76
|
+
`AgentsCLI.swift`.
|
|
77
|
+
|
|
78
|
+
- **Two projects sharing one monorepo checkout are no longer indistinguishable.** Session,
|
|
79
|
+
activity, and feed attribution anchored a project on `root ?? defaultPath`, so a subproject
|
|
80
|
+
whose `root` is the monorepo and whose `defaultPath` is a subdir collapsed onto the same
|
|
81
|
+
path as its umbrella — the longest-match tiebreak had nothing to separate them, and work in
|
|
82
|
+
`rush/apps/cli` counted toward whichever definition happened to be listed first. A
|
|
83
|
+
`defaultPath` nested under `root` now takes precedence over that `root` (the root says where
|
|
84
|
+
the checkout is; `defaultPath` says which work is this project's), and each bound repo's
|
|
85
|
+
checkout and subpath anchor too. A narrowed `root` still covers the rest of its checkout as
|
|
86
|
+
a fallback, so a lone project defined with `--path` keeps attributing work across its own
|
|
87
|
+
repo instead of only inside the subdir. Source: `apps/cli/src/lib/projects.ts`.
|
|
88
|
+
|
|
89
|
+
- **`agents projects view <name>` now shows more than `status`, not less.** The command you
|
|
90
|
+
open to learn everything about one project built its own short list — root, repos, a raw
|
|
91
|
+
Linear project id, an issue count, milestones — and never called the card renderer, so it
|
|
92
|
+
omitted the agents roster, merged PRs and release, focus areas, the schedule verdict,
|
|
93
|
+
tickets, and artifacts that `status` had shown all along. `view` and `status` now gather
|
|
94
|
+
through one function and render through one card; `view` adds every milestone (instead of
|
|
95
|
+
just the next) and the stored definition in full underneath — each repo with its subpath and
|
|
96
|
+
checkout, each context with its purpose, each integration with its URL. It also takes
|
|
97
|
+
`--window <days>` to match `status`. Source: `apps/cli/src/commands/projects.ts`.
|
|
98
|
+
- **The `agents` roster on the card lists live sessions only.** It included every matched
|
|
99
|
+
session, so a card headed `23 live` went on to print `claude · crashed ×25` — the corpses the
|
|
100
|
+
`dead` row already reports, counted twice and contradicting the headline. Both now derive
|
|
101
|
+
from one `isDeadStatus` predicate, pinned by a test across every `ActiveStatus`. Source:
|
|
102
|
+
`apps/cli/src/lib/project-status.ts`.
|
|
103
|
+
|
|
104
|
+
- **`--host <self>` and the fleet-health fan-out now short-circuit ALL of the
|
|
105
|
+
local machine's names, not just its short hostname (RUSH-2114).** A `--host`
|
|
106
|
+
target or fleet probe that referenced this box by its **tailscale dnsName**
|
|
107
|
+
(`zion.tail1a85a1.ts.net`) slipped past a `=== machineId()` check and SSH'd to
|
|
108
|
+
the local box over its own name; on a loaded machine that self-SSH'd `doctor
|
|
109
|
+
--json` orphaned on timeout and piled up until the host was crushed. A new
|
|
110
|
+
`isSelfHost()` matches every identity the box answers to (short id, loopback,
|
|
111
|
+
tailscale dnsName + its short form) and gates all four self-checks — the
|
|
112
|
+
generic `--host` passthrough (`maybeRunOnHost`), the `--devices`-all fan-out
|
|
113
|
+
(`runFleetPassthrough`), `remoteFleetTargets`, and `runFleet` — so a
|
|
114
|
+
self-reference runs locally instead of self-SSHing. Source:
|
|
115
|
+
`apps/cli/src/lib/devices/self-host.ts`, `apps/cli/src/lib/hosts/passthrough.ts`,
|
|
116
|
+
`apps/cli/src/lib/devices/fleet.ts`.
|
|
117
|
+
|
|
3
118
|
## 1.22.0
|
|
4
119
|
|
|
5
120
|
- **`agents run auto` — full-auto dispatch (RUSH-2132).** `run auto` composes all three routing layers: host (14d launch affinity, unless `--host` is given), harness (installed CLIs weighted by best-account headroom), and account (the configured strategy). `balanced`/`available` now exit nonzero when every installed account is unhealthy — naming each excluded account, the earliest window reset, and the `--strategy pinned` escape hatch — instead of warning "falling back to defaults" and launching the exhausted pinned default. The error text is a machine-readable contract (`no healthy` + `resets <iso-time>`) the Factory watchdog tail-detects for rotate cooldowns. Source: `apps/cli/src/lib/rotate.ts`, `apps/cli/src/commands/exec.ts`, `apps/cli/src/lib/runner.ts`.
|
package/README.md
CHANGED
|
@@ -382,6 +382,8 @@ agents watchdog --watch # daemon loop: a tick every --interval
|
|
|
382
382
|
|
|
383
383
|
`agents watchdog` detects a stalled session, resolves the *exact* terminal split it lives in (tmux, iTerm, VSCodium, or a raw pty), and injects a nudge -- `Continue.` by default, or set `--text`. It's dry by default; `--nudge` acts on a single tick, and `agents watchdog enable` flips global auto-nudge on so `--watch` injects on its own. Steer a single run with `agents watchdog policy <id> off | keep | handsoff`.
|
|
384
384
|
|
|
385
|
+
A stalled session whose tail shows a hard account limit ("You've hit your weekly limit · resets …") is **rotated in place** instead of nudged: the watchdog gates on the same healthy-account selection `agents run auto` makes (zero healthy → one skip event per cooldown window, terminal untouched), injects the harness's exit sequence, relaunches `agents run auto --interactive --session-id <uuid>` in the *same* tab, then replays the old session's resume once the new TUI is live. Default on; `agents watchdog rotate off` disables it (nudging stays on).
|
|
386
|
+
|
|
385
387
|
---
|
|
386
388
|
|
|
387
389
|
## Sync the fleet
|
package/dist/bin/agents
CHANGED
|
Binary file
|
package/dist/commands/cloud.js
CHANGED
package/dist/commands/doctor.js
CHANGED
|
@@ -4,6 +4,7 @@ import { addHostOption } from '../lib/hosts/option.js';
|
|
|
4
4
|
import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
|
|
5
5
|
import { loadDevices, isControlDevice } from '../lib/devices/registry.js';
|
|
6
6
|
import { fanOutDevices, planFleetTargets, remoteFleetTargets } from '../lib/devices/fleet.js';
|
|
7
|
+
import { enterDoctorOverviewGate, writeDoctorOverviewCache } from '../lib/devices/doctor-overview-cache.js';
|
|
7
8
|
import { fleetDialTarget } from '../lib/devices/connect.js';
|
|
8
9
|
import { compareFleetInventories } from '../lib/devices/fleet-divergence.js';
|
|
9
10
|
import { collectLocalFleetInventory } from '../lib/devices/fleet-inventory.js';
|
|
@@ -1155,12 +1156,18 @@ export function registerDoctorCommand(program) {
|
|
|
1155
1156
|
.option('--devices', 'Check agent readiness AND cross-device harness divergence (missing resources/versions, repo drift) on every registered device (alias --hosts)')
|
|
1156
1157
|
.option('--hosts', 'Alias of --devices')
|
|
1157
1158
|
.option('--check', 'CI drift gate: exit non-zero when any installed version is out of sync (stale or never-synced), zero when clean. Combine with --devices to gate the whole fleet.')
|
|
1159
|
+
.option('--refresh', 'Bypass the cached overview snapshot: recompute the bare `doctor --json` overview live and refresh the shared cache that the menu-bar and other pollers read')
|
|
1158
1160
|
.option('-q, --quiet', 'With --check, suppress per-version lines; print only the one-line verdict');
|
|
1159
1161
|
setHelpSections(doctorCmd, {
|
|
1160
1162
|
examples: `
|
|
1161
1163
|
# Overview: CLI availability + sync status + orphans across all defaults
|
|
1162
1164
|
agents doctor
|
|
1163
1165
|
|
|
1166
|
+
# Machine-readable overview (served from a ~90s cache for pollers like the
|
|
1167
|
+
# menu-bar helper); --refresh recomputes live and refreshes that cache
|
|
1168
|
+
agents doctor --json
|
|
1169
|
+
agents doctor --json --refresh
|
|
1170
|
+
|
|
1164
1171
|
# Full per-resource report for the active default
|
|
1165
1172
|
agents doctor claude@default
|
|
1166
1173
|
|
|
@@ -1288,6 +1295,26 @@ export function registerDoctorCommand(program) {
|
|
|
1288
1295
|
return;
|
|
1289
1296
|
}
|
|
1290
1297
|
if (!target) {
|
|
1298
|
+
// Singleflight + short-TTL cache for the bare `doctor --json` overview.
|
|
1299
|
+
// This overview probes every host CLI, every agent's sign-in, and every
|
|
1300
|
+
// agent×version diff — seconds on an idle box, minutes on a loaded one.
|
|
1301
|
+
// The menu-bar helper polls it with only a per-*process* in-flight guard,
|
|
1302
|
+
// so a helper relaunch (or any second poller) each launched its own live
|
|
1303
|
+
// compute, and a helper killed mid-run orphaned a `doctor --json` that
|
|
1304
|
+
// kept spinning — stacking to dozens of concurrent runs pinning the CPU
|
|
1305
|
+
// (RUSH-2153). Now: a fresh snapshot serves instantly, and when a compute
|
|
1306
|
+
// IS needed exactly one runs while every other caller serves its result.
|
|
1307
|
+
// A crashed computer never wedges the gate — the lock is stolen once its
|
|
1308
|
+
// directory mtime goes stale (see enterDoctorOverviewGate).
|
|
1309
|
+
let releaseOverviewGate;
|
|
1310
|
+
if (opts.json) {
|
|
1311
|
+
const gate = await enterDoctorOverviewGate({ forceRefresh: !!opts.refresh });
|
|
1312
|
+
if (gate.cached !== null) {
|
|
1313
|
+
console.log(gate.cached);
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
releaseOverviewGate = gate.release;
|
|
1317
|
+
}
|
|
1291
1318
|
const clis = checkAllClis();
|
|
1292
1319
|
const syncRows = checkSyncStatus(cwd);
|
|
1293
1320
|
const orphanRows = countOrphans();
|
|
@@ -1355,7 +1382,7 @@ export function registerDoctorCommand(program) {
|
|
|
1355
1382
|
isolatedVersions,
|
|
1356
1383
|
});
|
|
1357
1384
|
if (opts.json) {
|
|
1358
|
-
|
|
1385
|
+
const overviewPayload = {
|
|
1359
1386
|
clis,
|
|
1360
1387
|
signIn,
|
|
1361
1388
|
// Cached auth-health rollup for THIS host — lets `agents fleet status`
|
|
@@ -1398,7 +1425,12 @@ export function registerDoctorCommand(program) {
|
|
|
1398
1425
|
branch: m.branch,
|
|
1399
1426
|
fetchedAt: m.fetchedAt,
|
|
1400
1427
|
})),
|
|
1401
|
-
}
|
|
1428
|
+
};
|
|
1429
|
+
// Persist for the next poller and release the singleflight lock BEFORE
|
|
1430
|
+
// printing, so a concurrent caller picks up the fresh snapshot at once.
|
|
1431
|
+
writeDoctorOverviewCache(overviewPayload);
|
|
1432
|
+
releaseOverviewGate?.();
|
|
1433
|
+
console.log(JSON.stringify(overviewPayload, null, 2));
|
|
1402
1434
|
return;
|
|
1403
1435
|
}
|
|
1404
1436
|
// Single-machine hybrid: CRITICAL section + one `▸ <machine>` block.
|
package/dist/commands/exec.js
CHANGED
|
@@ -2261,7 +2261,7 @@ export function registerRunCommand(program) {
|
|
|
2261
2261
|
}
|
|
2262
2262
|
}
|
|
2263
2263
|
const autoShareEnv = options.autoSecrets !== false
|
|
2264
|
-
? shareRuntimeEnv(
|
|
2264
|
+
? shareRuntimeEnv()
|
|
2265
2265
|
: undefined;
|
|
2266
2266
|
// Merge order (later wins): profile env < auto share token < secrets bundles < --env K=V.
|
|
2267
2267
|
// Profile carries provider auth; secrets bundles carry user-defined
|
|
@@ -25,7 +25,7 @@ import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
|
|
|
25
25
|
import { factoryProjectsPath } from '../lib/auto-dispatch.js';
|
|
26
26
|
import { formatFleetWorkspaces, parseRemoteProbe, probeProjectWorkspaces, workspaceTargetsForDef, } from '../lib/project-probe.js';
|
|
27
27
|
import { listProjectDefs, loadProjectDef, writeProjectDef, removeProjectDef, projectDefPath, isSafeProjectName, } from '../lib/projects.js';
|
|
28
|
-
import { rollupSessionsByProject, liveDeadSplit, enrichProjectSignals, formatProjectMembers, } from '../lib/project-status.js';
|
|
28
|
+
import { rollupSessionsByProject, isDeadStatus, liveDeadSplit, enrichProjectSignals, formatProjectMembers, } from '../lib/project-status.js';
|
|
29
29
|
import { fetchLinearProjectCounts } from '../lib/linear-project-counts.js';
|
|
30
30
|
import { listLinearProjects, pickLinearProject } from '../lib/linear-projects.js';
|
|
31
31
|
import { checkRepoSlug } from '../lib/project-doctor.js';
|
|
@@ -59,6 +59,17 @@ function parseContextFlag(raw) {
|
|
|
59
59
|
return { path: raw.trim(), purpose: '' };
|
|
60
60
|
return { path: raw.slice(0, i).trim(), purpose: raw.slice(i + 1).trim() };
|
|
61
61
|
}
|
|
62
|
+
/** `objective:measure` → a goal. The measure is optional; the objective may contain colons only after the first is claimed. */
|
|
63
|
+
function parseGoalFlag(raw) {
|
|
64
|
+
const i = raw.indexOf(':');
|
|
65
|
+
if (i === -1)
|
|
66
|
+
return { objective: raw.trim() };
|
|
67
|
+
const measure = raw.slice(i + 1).trim();
|
|
68
|
+
const goal = { objective: raw.slice(0, i).trim() };
|
|
69
|
+
if (measure)
|
|
70
|
+
goal.measure = measure;
|
|
71
|
+
return goal;
|
|
72
|
+
}
|
|
62
73
|
/**
|
|
63
74
|
* Best-effort `owner/repo` from a repo's origin remote.
|
|
64
75
|
*
|
|
@@ -268,11 +279,45 @@ function statusBar(r) {
|
|
|
268
279
|
parts.push(chalk.gray(`+${live - shown} other`));
|
|
269
280
|
return parts.join(' · ') || chalk.gray('no live agents');
|
|
270
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Gather the live/remote/Linear/focus signals for the projects about to be
|
|
284
|
+
* rendered. `status` and `view` share this deliberately: `view` used to build
|
|
285
|
+
* its own thinner picture, so the command you open to learn everything about
|
|
286
|
+
* ONE project showed strictly less than the roll-up across all of them — no
|
|
287
|
+
* agents, no ships, no focus, no schedule verdict. One gatherer means a signal
|
|
288
|
+
* added for either surface appears on both.
|
|
289
|
+
*
|
|
290
|
+
* Only the shown projects are enriched. `skipRemote` still reads the local
|
|
291
|
+
* artifact log and git focus, and skips just the network calls (`gh`, Linear).
|
|
292
|
+
*/
|
|
293
|
+
async function enrichProjectsForRender(defs, all, opts) {
|
|
294
|
+
const roll = rollupSessionsByProject(all, [...(await getActiveSessions()), ...(opts.extraSessions ?? [])]);
|
|
295
|
+
const remote = new Map();
|
|
296
|
+
const linear = new Map();
|
|
297
|
+
// Local git, no API, no rate limit — measured 0.23s over a 897-commit week.
|
|
298
|
+
const focus = new Map();
|
|
299
|
+
await Promise.all(defs.map(async (d) => {
|
|
300
|
+
const [sig, counts] = await Promise.all([
|
|
301
|
+
enrichProjectSignals(d, opts.windowDays, opts.nowMs, { skipRemote: opts.skipRemote }),
|
|
302
|
+
!opts.skipRemote && d.linear?.projectId
|
|
303
|
+
? fetchLinearProjectCounts(d.linear.projectId)
|
|
304
|
+
: Promise.resolve(undefined),
|
|
305
|
+
]);
|
|
306
|
+
remote.set(d.name, sig);
|
|
307
|
+
if (counts)
|
|
308
|
+
linear.set(d.name, counts);
|
|
309
|
+
if (d.root)
|
|
310
|
+
focus.set(d.name, await readFocusAreas(expandLocalHome(d.root), opts.windowDays));
|
|
311
|
+
}));
|
|
312
|
+
return { roll, remote, linear, focus };
|
|
313
|
+
}
|
|
271
314
|
function renderCard(def, r, remote, fleet, linear, nowMs = Date.now(),
|
|
272
315
|
/** How many milestones to print. `status` shows the next one; `view` shows all. */
|
|
273
316
|
milestoneLimit = 1,
|
|
274
317
|
/** Directories the window's work landed in, from local git. */
|
|
275
|
-
focus = []
|
|
318
|
+
focus = [],
|
|
319
|
+
/** `view` mode: the caller prints the stored definition in full afterwards. */
|
|
320
|
+
detail = false) {
|
|
276
321
|
// The headline counts LIVE agents. It used to be every matched session, which
|
|
277
322
|
// read `39 agents` on a project where 19 had crashed. `planPct` used to sit
|
|
278
323
|
// here too and is gone: it summed each session's latest checklist snapshot,
|
|
@@ -288,8 +333,12 @@ focus = []) {
|
|
|
288
333
|
const detail = split.deadByStatus.map((d) => `${d.n} ${d.status}`).join(', ');
|
|
289
334
|
console.log(` ${chalk.dim('dead')} ${chalk.yellow(`${split.dead} finished or lost`)} ${chalk.dim(`(${detail})`)}`);
|
|
290
335
|
}
|
|
291
|
-
|
|
292
|
-
|
|
336
|
+
// Live only. The roster used to list every matched session, so a card headed
|
|
337
|
+
// `23 live` went on to show `crashed ×25` — the corpses the `dead` row above
|
|
338
|
+
// already accounts for, counted twice and contradicting the headline.
|
|
339
|
+
const liveMembers = r?.members.filter((m) => !isDeadStatus(m.status)) ?? [];
|
|
340
|
+
if (liveMembers.length)
|
|
341
|
+
console.log(` ${chalk.dim('agents')} ${formatProjectMembers(liveMembers)}`);
|
|
293
342
|
const ships = [];
|
|
294
343
|
if (remote?.mergedPrs) {
|
|
295
344
|
ships.push(chalk.green(`${remote.mergedPrs}${remote.mergedPrsTruncated ? '+' : ''} merged (${remote.windowDays}d)`));
|
|
@@ -343,13 +392,19 @@ focus = []) {
|
|
|
343
392
|
console.log(` ${chalk.yellow('!')} ${chalk.yellow(mismatch.message)}`);
|
|
344
393
|
console.log(` ${' '.repeat(8)}${chalk.gray(mismatch.remediation)}`);
|
|
345
394
|
}
|
|
346
|
-
|
|
395
|
+
// `view` prints these in full (path + purpose, label + URL) right below, so
|
|
396
|
+
// the compact one-line summaries would just say the same thing twice.
|
|
397
|
+
if (!detail && def.goals?.length) {
|
|
398
|
+
console.log(` ${chalk.dim('goal')} ${def.goals.map((g) => g.objective).join(' · ')}`);
|
|
399
|
+
}
|
|
400
|
+
if (!detail && def.contexts?.length) {
|
|
347
401
|
console.log(` ${chalk.dim('context')} ${def.contexts.map((c) => c.path).join(' · ')}`);
|
|
348
402
|
}
|
|
349
|
-
if (def.integrations?.length) {
|
|
403
|
+
if (!detail && def.integrations?.length) {
|
|
350
404
|
console.log(` ${chalk.dim('links')} ${def.integrations.map((i) => i.label ?? i.kind).join(' · ')}`);
|
|
351
405
|
}
|
|
352
|
-
|
|
406
|
+
if (!detail)
|
|
407
|
+
console.log('');
|
|
353
408
|
}
|
|
354
409
|
export function registerProjectsCommands(program) {
|
|
355
410
|
const enabled = isBetaEnabled('projects');
|
|
@@ -426,6 +481,7 @@ export function registerProjectsCommands(program) {
|
|
|
426
481
|
.option('--path <subdir>', 'Default cwd for agents (a monorepo subdir)')
|
|
427
482
|
.option('--repo <owner/repo>', 'Primary GitHub slug (defaults to the origin remote)')
|
|
428
483
|
.option('--context <path:purpose...>', 'A described starting point; repeatable')
|
|
484
|
+
.option('--goal <objective:measure...>', 'An outcome the project serves; repeatable')
|
|
429
485
|
.option('--linear <url-or-id>', 'Linear project URL or id')
|
|
430
486
|
.option('--force', 'Overwrite an existing definition')
|
|
431
487
|
.action(async (name, opts) => {
|
|
@@ -456,6 +512,8 @@ export function registerProjectsCommands(program) {
|
|
|
456
512
|
def.repo = repo;
|
|
457
513
|
if (opts.context?.length)
|
|
458
514
|
def.contexts = opts.context.map(parseContextFlag);
|
|
515
|
+
if (opts.goal?.length)
|
|
516
|
+
def.goals = opts.goal.map(parseGoalFlag);
|
|
459
517
|
if (opts.linear) {
|
|
460
518
|
def.linear = /^https?:/.test(opts.linear) ? { url: opts.linear } : { projectId: opts.linear };
|
|
461
519
|
}
|
|
@@ -468,58 +526,72 @@ export function registerProjectsCommands(program) {
|
|
|
468
526
|
projects
|
|
469
527
|
.command('view <name>')
|
|
470
528
|
.alias('show')
|
|
471
|
-
.description('
|
|
529
|
+
.description('Everything about one project: the full status card, every milestone, and the stored definition.')
|
|
472
530
|
.option('--json', 'Machine-readable output')
|
|
473
|
-
.option('--
|
|
531
|
+
.option('--window <days>', 'Window for merged PRs, artifacts, and focus areas', '7')
|
|
532
|
+
.option('--no-remote', 'Skip the GitHub and Linear lookups; definition only')
|
|
474
533
|
.action(async (name, opts) => {
|
|
475
534
|
const def = loadProjectDef(name);
|
|
476
535
|
if (!def) {
|
|
477
536
|
console.error(chalk.red(`No project named "${name}". List them: agents projects list`));
|
|
478
537
|
process.exit(1);
|
|
479
538
|
}
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
539
|
+
// `view` is `status` for one project PLUS the stored definition and every
|
|
540
|
+
// milestone. It used to render its own short list — root, repos, a raw
|
|
541
|
+
// Linear id, issues — so opening a single project told you LESS than the
|
|
542
|
+
// roll-up did: no agents, no ships, no focus, no schedule verdict.
|
|
543
|
+
const windowDays = Math.max(1, Number.parseInt(opts.window ?? '7', 10) || 7);
|
|
544
|
+
const nowMs = Date.now();
|
|
545
|
+
const all = listProjectDefs();
|
|
546
|
+
const { roll, remote, linear, focus } = await enrichProjectsForRender([def], all, {
|
|
547
|
+
windowDays,
|
|
548
|
+
nowMs,
|
|
549
|
+
skipRemote: opts.remote === false,
|
|
550
|
+
});
|
|
551
|
+
const r = roll.get(def.name);
|
|
552
|
+
const sig = remote.get(def.name);
|
|
553
|
+
const counts = linear.get(def.name);
|
|
485
554
|
if (opts.json) {
|
|
486
|
-
console.log(JSON.stringify({
|
|
555
|
+
console.log(JSON.stringify({
|
|
556
|
+
...def,
|
|
557
|
+
linear: { ...def.linear, ...(counts ?? {}) },
|
|
558
|
+
schedule: counts?.milestones?.length ? scheduleVerdict(counts.milestones, nowMs) : null,
|
|
559
|
+
live: r ? liveDeadSplit(r.byStatus).live : 0,
|
|
560
|
+
dead: r ? liveDeadSplit(r.byStatus).dead : 0,
|
|
561
|
+
byStatus: r?.byStatus ?? {},
|
|
562
|
+
members: r?.members ?? [],
|
|
563
|
+
openPrs: r?.openPrs ?? [],
|
|
564
|
+
tickets: r?.tickets ?? [],
|
|
565
|
+
mergedPrs: sig?.mergedPrs ?? 0,
|
|
566
|
+
latestRelease: sig?.latestRelease ?? null,
|
|
567
|
+
artifacts: sig?.artifacts ?? 0,
|
|
568
|
+
focus: focus.get(def.name) ?? [],
|
|
569
|
+
}, null, 2));
|
|
487
570
|
return;
|
|
488
571
|
}
|
|
489
|
-
|
|
572
|
+
// The same card `status` prints, with every milestone instead of the next.
|
|
573
|
+
renderCard(def, r, sig, undefined, counts, nowMs, Number.POSITIVE_INFINITY, focus.get(def.name) ?? [], true);
|
|
574
|
+
// Then what only `view` shows: the stored definition, in full.
|
|
575
|
+
console.log();
|
|
490
576
|
if (def.root)
|
|
491
|
-
console.log(` root
|
|
577
|
+
console.log(` ${chalk.dim('root')} ${def.root}`);
|
|
492
578
|
if (def.defaultPath)
|
|
493
|
-
console.log(`
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
console.log(`
|
|
579
|
+
console.log(` ${chalk.dim('path')} ${def.defaultPath}`);
|
|
580
|
+
for (const rp of def.repos ?? []) {
|
|
581
|
+
const where = [rp.subpath ? `subpath ${rp.subpath}` : undefined, rp.path].filter(Boolean).join(' · ');
|
|
582
|
+
console.log(` ${chalk.dim('repo')} ${rp.slug}${where ? chalk.dim(` (${where})`) : ''}`);
|
|
583
|
+
}
|
|
584
|
+
for (const g of def.goals ?? [])
|
|
585
|
+
console.log(` ${chalk.dim('goal')} ${g.objective}${g.measure ? chalk.dim(` · ${g.measure}`) : ''}`);
|
|
497
586
|
for (const c of def.contexts ?? [])
|
|
498
|
-
console.log(` context
|
|
499
|
-
for (const
|
|
500
|
-
console.log(` ${
|
|
587
|
+
console.log(` ${chalk.dim('context')} ${chalk.cyan(c.path)} ${chalk.dim('—')} ${c.purpose}`);
|
|
588
|
+
for (const ig of def.integrations ?? []) {
|
|
589
|
+
console.log(` ${chalk.dim(ig.kind.padEnd(8))} ${ig.url}${ig.label ? chalk.dim(` (${ig.label})`) : ''}`);
|
|
590
|
+
}
|
|
501
591
|
if (def.linear?.url || def.linear?.projectId)
|
|
502
|
-
console.log(` linear
|
|
592
|
+
console.log(` ${chalk.dim('linear')} ${def.linear.url ?? def.linear.projectId}`);
|
|
503
593
|
for (const d of def.docs ?? [])
|
|
504
|
-
console.log(` doc
|
|
505
|
-
if (counts) {
|
|
506
|
-
const staleNote = counts.stale ? chalk.dim(' (cached)') : '';
|
|
507
|
-
console.log(` issues ${counts.done}/${counts.total}${counts.truncated ? '+' : ''} done · ${counts.inProgress} in progress${staleNote}`);
|
|
508
|
-
}
|
|
509
|
-
const ms = counts?.milestones ?? [];
|
|
510
|
-
if (ms.length) {
|
|
511
|
-
console.log(` ${chalk.bold('milestones')}`);
|
|
512
|
-
for (const m of ms) {
|
|
513
|
-
const mark = m.name === counts?.nextMilestone?.name ? chalk.cyan('next') : '';
|
|
514
|
-
console.log(` ${mark.padEnd(6)} ${formatNextMilestone(m, Date.now())}`);
|
|
515
|
-
}
|
|
516
|
-
// A milestone nothing is filed against cannot report progress. Say that
|
|
517
|
-
// once, plainly, rather than printing a row of silent 0%s.
|
|
518
|
-
const unfiled = ms.filter((m) => m.total === 0).length;
|
|
519
|
-
if (unfiled === ms.length) {
|
|
520
|
-
console.log(` ${chalk.yellow('!')} ${chalk.yellow(`no issues are assigned to any milestone — progress against them cannot be measured`)}`);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
594
|
+
console.log(` ${chalk.dim('doc')} ${d}`);
|
|
523
595
|
console.log(chalk.gray(` ${projectDefPath(name)}`));
|
|
524
596
|
});
|
|
525
597
|
// ---- edit ----
|
|
@@ -590,27 +662,12 @@ export function registerProjectsCommands(program) {
|
|
|
590
662
|
fleetSkipped = probeRes.skipped;
|
|
591
663
|
fleetSessions = activeRes.sessions;
|
|
592
664
|
}
|
|
593
|
-
const roll
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
const focus = new Map();
|
|
600
|
-
await Promise.all(defs.map(async (d) => {
|
|
601
|
-
const skipRemote = opts.remote === false;
|
|
602
|
-
const [sig, counts] = await Promise.all([
|
|
603
|
-
enrichProjectSignals(d, windowDays, nowMs, { skipRemote }),
|
|
604
|
-
!skipRemote && d.linear?.projectId
|
|
605
|
-
? fetchLinearProjectCounts(d.linear.projectId)
|
|
606
|
-
: Promise.resolve(undefined),
|
|
607
|
-
]);
|
|
608
|
-
remote.set(d.name, sig);
|
|
609
|
-
if (counts)
|
|
610
|
-
linear.set(d.name, counts);
|
|
611
|
-
if (d.root)
|
|
612
|
-
focus.set(d.name, await readFocusAreas(expandLocalHome(d.root), windowDays));
|
|
613
|
-
}));
|
|
665
|
+
const { roll, remote, linear, focus } = await enrichProjectsForRender(defs, all, {
|
|
666
|
+
windowDays,
|
|
667
|
+
nowMs,
|
|
668
|
+
skipRemote: opts.remote === false,
|
|
669
|
+
extraSessions: fleetSessions,
|
|
670
|
+
});
|
|
614
671
|
/** This def's slice of the fleet probe, in its own target order. */
|
|
615
672
|
const fleetFor = (d) => {
|
|
616
673
|
const targets = new Set(workspaceTargetsForDef(d));
|
|
@@ -704,6 +761,7 @@ export function registerProjectsCommands(program) {
|
|
|
704
761
|
.option('--root <path>', 'Repo / monorepo root')
|
|
705
762
|
.option('--path <subdir>', 'Default cwd for agents (a monorepo subdir)')
|
|
706
763
|
.option('--description <text>', 'One-line description shown on the card')
|
|
764
|
+
.option('--goal <objective:measure...>', 'Replace the goals this project serves; repeatable')
|
|
707
765
|
.action((name, opts) => {
|
|
708
766
|
const def = loadProjectDef(name);
|
|
709
767
|
if (!def) {
|
|
@@ -711,7 +769,7 @@ export function registerProjectsCommands(program) {
|
|
|
711
769
|
process.exit(1);
|
|
712
770
|
}
|
|
713
771
|
const fields = ['repo', 'root', 'path', 'description'].filter((k) => opts[k] !== undefined);
|
|
714
|
-
if (fields.length === 0) {
|
|
772
|
+
if (fields.length === 0 && !opts.goal?.length) {
|
|
715
773
|
console.error(chalk.red('Nothing to set. Pass a field, e.g. --repo <owner/repo>.'));
|
|
716
774
|
process.exit(1);
|
|
717
775
|
}
|
|
@@ -724,6 +782,8 @@ export function registerProjectsCommands(program) {
|
|
|
724
782
|
def.root = opts.root;
|
|
725
783
|
if (opts.description !== undefined)
|
|
726
784
|
def.description = opts.description;
|
|
785
|
+
if (opts.goal?.length)
|
|
786
|
+
def.goals = opts.goal.map(parseGoalFlag);
|
|
727
787
|
if (opts.path !== undefined) {
|
|
728
788
|
// `--path` is a subdir OF the root, so without one there is nothing to
|
|
729
789
|
// hang it off. A def imported with `--from-linear` that found no local
|
|
@@ -742,6 +802,8 @@ export function registerProjectsCommands(program) {
|
|
|
742
802
|
console.log(chalk.green(`Updated ${def.name}`));
|
|
743
803
|
for (const f of fields)
|
|
744
804
|
console.log(chalk.gray(` ${f} ${f === 'path' ? def.defaultPath : def[f]}`));
|
|
805
|
+
if (opts.goal?.length)
|
|
806
|
+
console.log(chalk.gray(` goals ${def.goals?.map((g) => g.objective).join(' · ')}`));
|
|
745
807
|
});
|
|
746
808
|
// ---- link ----
|
|
747
809
|
projects
|
package/dist/commands/secrets.js
CHANGED
|
@@ -18,7 +18,7 @@ import { ensureDaemonStarted, isDaemonRunning } from '../lib/daemon.js';
|
|
|
18
18
|
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget, verifyRemoteKeychainPush, keychainWriteFailureMessage, } from '../lib/secrets/remote.js';
|
|
19
19
|
import { remoteShellFor, buildWindowsStdinImportCommand } from '../lib/hosts/remote-cmd.js';
|
|
20
20
|
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
21
|
-
import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, isHeadlessSecretsContext, readBundle, readBundleIfDecryptable, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, SECRET_TYPES, } from '../lib/secrets/bundles.js';
|
|
21
|
+
import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, isHeadlessSecretsContext, readBundle, readBundleIfDecryptable, reAclBundleItems, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, SECRET_TYPES, } from '../lib/secrets/bundles.js';
|
|
22
22
|
import { parseListFilters, bundleMatchesFilter, bundleExpiry, filterIsActive, describeFilter, parseSortField, sortBundles, SORT_FIELDS, REF_KINDS, DEFAULT_EXPIRING_DAYS, } from '../lib/secrets/list-filter.js';
|
|
23
23
|
import { encryptForFallback, decryptForFallback } from '../lib/secrets/filestore.js';
|
|
24
24
|
import { getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
|
|
@@ -2738,10 +2738,14 @@ Examples:
|
|
|
2738
2738
|
return;
|
|
2739
2739
|
}
|
|
2740
2740
|
bundle.policy = next;
|
|
2741
|
-
//
|
|
2742
|
-
//
|
|
2743
|
-
//
|
|
2744
|
-
|
|
2741
|
+
// Re-store the VALUE items so their keychain ACL matches the new tier —
|
|
2742
|
+
// NOT just the metadata. macOS gates each read on the item's own ACL
|
|
2743
|
+
// (SEC-19); a metadata-only write would leave a hold/always bundle's
|
|
2744
|
+
// biometry ACL in place and keep popping Touch ID forever after a switch
|
|
2745
|
+
// to `never`. reAclBundleItems reads once (the last prompt this bundle
|
|
2746
|
+
// raises when tightening to `never`), rewrites no-ACL, and evicts any
|
|
2747
|
+
// broker-held copy so the change takes effect on the very next read.
|
|
2748
|
+
reAclBundleItems(bundle);
|
|
2745
2749
|
console.log(chalk.green(`${bundle.name} policy set to ${next}.`));
|
|
2746
2750
|
if (next === 'hold') {
|
|
2747
2751
|
console.log(chalk.gray('Held by the secrets-agent for ~7 days after one unlock (auto-cache is on by default; disable with `secrets.agent.auto: false` in agents.yaml).'));
|
package/dist/commands/teams.js
CHANGED
|
@@ -1539,7 +1539,7 @@ export function registerTeamsCommands(program) {
|
|
|
1539
1539
|
repo: opts.repo,
|
|
1540
1540
|
branch: opts.branch,
|
|
1541
1541
|
model: a.model ?? undefined,
|
|
1542
|
-
env: shareRuntimeEnv(
|
|
1542
|
+
env: shareRuntimeEnv(),
|
|
1543
1543
|
};
|
|
1544
1544
|
const cloudTask = await prov.dispatch(dispatchOpts);
|
|
1545
1545
|
return { cloudSessionId: cloudTask.id };
|
|
@@ -1557,7 +1557,7 @@ export function registerTeamsCommands(program) {
|
|
|
1557
1557
|
repo: opts.repo,
|
|
1558
1558
|
branch: opts.branch,
|
|
1559
1559
|
model: opts.model,
|
|
1560
|
-
env: shareRuntimeEnv(
|
|
1560
|
+
env: shareRuntimeEnv(),
|
|
1561
1561
|
};
|
|
1562
1562
|
try {
|
|
1563
1563
|
const cloudTask = await prov.dispatch(dispatchOpts);
|