@phnx-labs/agents-cli 1.22.52 → 1.22.53
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 +146 -0
- package/README.md +1 -1
- package/dist/commands/accounts.js +1 -1
- package/dist/commands/exec.js +16 -9
- package/dist/commands/fleet-capture.js +7 -0
- package/dist/commands/focus.js +2 -0
- package/dist/commands/go.js +2 -1
- package/dist/commands/sessions-inject.js +8 -3
- package/dist/commands/sessions-picker.js +2 -1
- package/dist/commands/sessions.js +30 -20
- package/dist/commands/ssh.js +35 -12
- package/dist/commands/sync.js +44 -0
- package/dist/lib/account-registry.d.ts +15 -5
- package/dist/lib/account-registry.js +150 -50
- package/dist/lib/answer-router.js +2 -1
- package/dist/lib/browser/profiles.d.ts +18 -0
- package/dist/lib/browser/profiles.js +26 -1
- package/dist/lib/browser/registry.d.ts +44 -14
- package/dist/lib/browser/registry.js +141 -45
- package/dist/lib/daemon/runner.js +10 -2
- package/dist/lib/device-config.js +3 -2
- package/dist/lib/devices/config-migration.js +147 -1
- package/dist/lib/devices/device-docs.d.ts +35 -0
- package/dist/lib/devices/device-docs.js +163 -0
- package/dist/lib/devices/discovery-policy.d.ts +14 -2
- package/dist/lib/devices/discovery-policy.js +31 -21
- package/dist/lib/devices/registry.d.ts +11 -5
- package/dist/lib/devices/registry.js +46 -18
- package/dist/lib/exec.d.ts +60 -30
- package/dist/lib/exec.js +65 -27
- package/dist/lib/feed/feed.d.ts +10 -2
- package/dist/lib/feed/feed.js +12 -1
- package/dist/lib/hosts/dispatch.d.ts +4 -3
- package/dist/lib/hosts/dispatch.js +12 -8
- package/dist/lib/hosts/providers/local.d.ts +9 -3
- package/dist/lib/hosts/providers/local.js +23 -12
- package/dist/lib/hosts/reconnect.d.ts +7 -4
- package/dist/lib/hosts/reconnect.js +29 -25
- package/dist/lib/hosts/registry.js +4 -1
- package/dist/lib/hosts/remote-os.js +3 -1
- package/dist/lib/session/active.d.ts +10 -1
- package/dist/lib/session/active.js +7 -1
- package/dist/lib/session/actor-sidecar.d.ts +7 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +39 -3
- package/dist/lib/session/discover.js +7 -12
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/pid-registry.d.ts +7 -0
- package/dist/lib/session/prompt.d.ts +15 -0
- package/dist/lib/session/prompt.js +21 -0
- package/dist/lib/session/types.d.ts +17 -0
- package/dist/lib/session/types.js +10 -0
- package/dist/lib/share/worker-template.js +12 -7
- package/dist/lib/state.d.ts +8 -0
- package/dist/lib/state.js +143 -11
- package/dist/lib/terminal/resolve.d.ts +7 -0
- package/dist/lib/terminal/resolve.js +41 -2
- package/dist/lib/traces/insights.d.ts +67 -0
- package/dist/lib/traces/insights.js +178 -0
- package/dist/lib/traces/phenotype.d.ts +67 -0
- package/dist/lib/traces/phenotype.js +437 -0
- package/dist/lib/traces/segments.d.ts +133 -0
- package/dist/lib/traces/segments.js +301 -0
- package/dist/lib/traces/sync.d.ts +33 -0
- package/dist/lib/traces/sync.js +11 -2
- package/dist/lib/types.d.ts +47 -1
- package/dist/lib/watchdog/runner.js +18 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,151 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.53
|
|
4
|
+
|
|
5
|
+
- **Custom-harness runs launched via a profile now show their real harness name in agents sessions instead of 'claude' (PHNX-2935).**
|
|
6
|
+
|
|
7
|
+
- **Watchdog un-addressable stalls now include the same recovery command as `agents focus` (PHNX-3070).**
|
|
8
|
+
Rotate, needs-human declared-block, and nudge-refuse paths append `addressabilityRecoveryHint` so an un-addressable session prints `agents sessions resume <id>` / tmux wrap instead of a reason-only skip. Source: `cli/src/lib/watchdog/runner.ts`.
|
|
9
|
+
|
|
10
|
+
- **Un-addressable live sessions now surface a clear recovery hint at the point of use** — `agents focus`, `sessions inject`, and the session picker report when a bare interactive session has no addressable terminal rail (not tmux/iTerm/an IDE terminal/a pty sidecar) instead of failing silently, and the hint now names the session's real id in `agents sessions resume <id>` rather than a placeholder (PHNX-3070).
|
|
11
|
+
|
|
12
|
+
- **Vitest can no longer launch the developer's browser (PHNX-3072).** `tests/setup.ts`
|
|
13
|
+
sandboxed `HOME` but not desktop-opener binaries, so a test that reached a real
|
|
14
|
+
`spawn('open' | 'xdg-open')` opened the URL on the developer's machine — green on
|
|
15
|
+
Linux CI, where `xdg-open` is absent, and disruptive locally. The harness now
|
|
16
|
+
prepends stub `open` / `xdg-open` / `gnome-open` binaries to `PATH` so a spawn
|
|
17
|
+
cannot reach the real handler; an unauthorized spawn fails the file. Source:
|
|
18
|
+
`cli/tests/opener-sandbox.ts`.
|
|
19
|
+
|
|
20
|
+
- **Feed attention stamps `sourceCursor` at block-write time (PHNX-3073).**
|
|
21
|
+
`coveredByResolution` conservatively suppresses a candidate with no comparable
|
|
22
|
+
cursor when a tombstone exists, which is correct for a stale lifecycle re-read
|
|
23
|
+
but buried a genuinely new open block whenever `session.lastActivityMs` was
|
|
24
|
+
unresolvable (cloud / remote / index-lag) because neither writer stamped a
|
|
25
|
+
cursor. `buildDeclaredBlock` and the feed-publish hook now stamp
|
|
26
|
+
`sourceCursor.lastActivityMs` from the write instant, so a fresh generation
|
|
27
|
+
compares strictly newer than any prior tombstone. Source:
|
|
28
|
+
`cli/src/lib/feed/feed.ts`.
|
|
29
|
+
|
|
30
|
+
- **Cursor auto-titles go through the same skill-scaffolding cleaner as Claude (PHNX-3123).**
|
|
31
|
+
`readCursorMeta` took `chatMeta.title` verbatim, so a Cursor session whose
|
|
32
|
+
server-generated title echoed the injected `Base directory for this skill: …`
|
|
33
|
+
line kept that path as `SessionMeta.label` — the field that wins on every
|
|
34
|
+
surface for the session's whole life. Claude's `ai-title` was already collapsed
|
|
35
|
+
to `/<skill>` at label composition (PR #2995); that collapse now lives in one
|
|
36
|
+
shared helper (`cleanGeneratedSessionLabel`) and Cursor's title takes the same
|
|
37
|
+
path. An ordinary Cursor title, and one that merely names a `skills/…` path,
|
|
38
|
+
is unchanged. A Claude `/rename` (`custom-title`) is still never rewritten.
|
|
39
|
+
Codex / OpenCode / Kimi / Droid put auto-titles on `topic`, not `label`, so
|
|
40
|
+
they are out of this change. **Not retroactive:** already-indexed labels stay
|
|
41
|
+
until the transcript next changes (RUSH-3122). Source:
|
|
42
|
+
`cli/src/lib/session/prompt.ts`, `cli/src/lib/session/discover.ts`.
|
|
43
|
+
|
|
44
|
+
- **Traces insight engine — cross-session failure clustering + wasted time (PHNX-3141).** `agents traces sync`'s `index.json` now carries `failurePatterns[]` (top-K, ranked by wasted time, not raw occurrence count — a rare 8-hour retry loop still outranks a frequent 2-minute one), `wastedMsTotal`, and `latency` (time-to-first-tool p50/p90/p99/max). Failed `tool_calls` are grouped by `(tool, cause, normalized error)` so near-identical failures (a GitHub rate-limit message with a different countdown each time) fold into one pattern instead of a wall of near-duplicates. Per-session `SessionDetail` also gains `surfacedToolFailures` — every failed step, listed regardless of the run's overall outcome, so a session that ultimately succeeded still shows the tool failures it hit along the way. Source: `cli/src/lib/traces/insights.ts`, `cli/src/lib/traces/sync.ts`.
|
|
45
|
+
|
|
46
|
+
- **Traces insight group-by dimensions (PHNX-3141).** `cli/src/lib/traces/segments.ts` derives the console bar axes — agent (model × harness), task type, failure timing — and the time-to-first-tool latency insight (`firstToolMs` p50/p90/p99/max). Pure functions; the sync integrator wires them into the shard. Source: `cli/src/lib/traces/segments.ts`.
|
|
47
|
+
|
|
48
|
+
- **Native-account labels live on the central `accounts.native` rows in agents.yaml (already fleet-synced via `agents repo push/pull`). `remove`/`rename`/`label` operate on every row matching `(agent, identityKey)`, so a post-merge duplicate cannot silently survive (PHNX-3206).**
|
|
49
|
+
|
|
50
|
+
- **The top-level `agents.yaml` header heals itself instead of freezing (PHNX-3315).**
|
|
51
|
+
`serializeCentral` parses the committed file to preserve its hand-written body
|
|
52
|
+
comments, but that also preserved the leading metadata header verbatim — so a
|
|
53
|
+
top-level `agents.yaml` written before the `agi-cli` rename kept its stale
|
|
54
|
+
3-line header (repo `agents-cli`, no `$schema:` line) forever while every
|
|
55
|
+
freshly-written device doc got the current header. The header is now rewritten
|
|
56
|
+
to the current `META_HEADER` on the next central write, textually (the `yaml`
|
|
57
|
+
library folds the leading comment onto the first key when that key already
|
|
58
|
+
carries a comment, so `doc.commentBefore` is unreliable), leaving body comments
|
|
59
|
+
and every config key intact, and byte-stable once healed so it never re-churns.
|
|
60
|
+
Source: `cli/src/lib/state.ts`.
|
|
61
|
+
- **The legacy central `browser:` tombstone drains itself on `agents sync` (PHNX-3315).**
|
|
62
|
+
New browser profiles write the per-device doc, but profiles created before the
|
|
63
|
+
device-scoped store lingered in the shared top-level `agents.yaml` and churned
|
|
64
|
+
every fleet pull until someone ran `agents browser profiles claim` by hand.
|
|
65
|
+
`agents sync` now auto-evicts a lingering central profile into this device's
|
|
66
|
+
doc and clears it from central — but ONLY for remote (`ssh://`) profiles. An
|
|
67
|
+
`ssh://` endpoint names a specific host, so the profile is fungible by design:
|
|
68
|
+
any box resolves it to the same browser, so a concurrent cross-machine
|
|
69
|
+
double-claim is harmless. A local/`cdp://` profile has no per-machine ownership
|
|
70
|
+
signal ("that browser is installed here" is not "I hold this profile's
|
|
71
|
+
credentialed session"), so auto-claiming it would let two boxes with the same
|
|
72
|
+
common browser each claim the same tombstone and flip a credentialed profile
|
|
73
|
+
from identity-bearing to fungible fleet-wide — the exact logged-out-browser
|
|
74
|
+
failure this module exists to prevent. Local/cdp tombstones are left central for
|
|
75
|
+
an explicit `agents browser profiles claim`. The claim is selected and committed
|
|
76
|
+
inside a single meta-lock acquisition, and non-throwing (a config conflict or
|
|
77
|
+
unhostable profile is left central rather than wedging the sync). `profileRegistry` is left as the single source of truth: a
|
|
78
|
+
claimed profile lives in the device doc alone, never double-counted across two
|
|
79
|
+
stores. Source: `cli/src/lib/browser/registry.ts`, `cli/src/commands/sync.ts`,
|
|
80
|
+
`cli/src/lib/state.ts`.
|
|
81
|
+
|
|
82
|
+
- **`fleet.discovery` / `fleet.ignored` are device-scoped with a union-on-read view (PHNX-3315).**
|
|
83
|
+
Every box used to rewrite one shared `fleet.discovery`/`fleet.ignored` map in the
|
|
84
|
+
top-level `agents.yaml` (each dismissal stamped `ignoredOn:<host>`), so two boxes
|
|
85
|
+
dismissing different peers produced guaranteed git line-conflicts on the next
|
|
86
|
+
pull. Each box now records ONLY its own discovery decisions and dismissals in its
|
|
87
|
+
tracked device doc (`devices/<host>/agents.yaml` `fleet:` block), and the
|
|
88
|
+
effective fleet view is recomputed as a deterministic, order-independent UNION
|
|
89
|
+
across every device doc at read time — `ignored` beats `approved` for a
|
|
90
|
+
conflicting name, and the newest `ignoredAt` wins a shared dismissal — so every
|
|
91
|
+
box computes the identical view and no box ever rewrites another's file. A
|
|
92
|
+
fold-then-delete migration moves the existing central maps into the owning box's
|
|
93
|
+
device doc before the central keys are dropped (the device-doc write lands before
|
|
94
|
+
the central strip, so a crash re-folds idempotently). `agents devices
|
|
95
|
+
register/ignore/unignore/ignored`, discovery reconcile, the sync pending-diff, and
|
|
96
|
+
`agents fleet capture` all read the union. Source:
|
|
97
|
+
`cli/src/lib/devices/{discovery-policy,registry,device-docs,config-migration}.ts`,
|
|
98
|
+
`cli/src/commands/{ssh,fleet-capture}.ts`, `cli/src/lib/state.ts`.
|
|
99
|
+
- **Agent-host registrations are device-scoped (`deviceHosts`) with a merged read (PHNX-3315).**
|
|
100
|
+
Locally-discovered SSH hosts and inline registrations landed in one shared
|
|
101
|
+
`hosts:` map in the top-level `agents.yaml`, so every box that enrolled a host
|
|
102
|
+
rewrote the fleet-shared file. Registrations now write this box's device doc
|
|
103
|
+
`hosts:` block; the effective host directory is the cross-box union (newest
|
|
104
|
+
`addedAt` wins a name collision), and `remove` only edits this box's own doc. A
|
|
105
|
+
fold-then-delete migration relocates the existing central `hosts:` map into this
|
|
106
|
+
box's device doc and drops the central key. Source:
|
|
107
|
+
`cli/src/lib/hosts/{providers/local,registry,remote-os}.ts`,
|
|
108
|
+
`cli/src/lib/devices/{device-docs,config-migration}.ts`, `cli/src/lib/state.ts`.
|
|
109
|
+
- **`scope:'device'` native accounts move off the shared file into the device doc (PHNX-3315).**
|
|
110
|
+
`addNativeAccount` accepted `scope:'device'` but always wrote the central
|
|
111
|
+
`accounts.native` map, so a per-box login (and its email / `identityKey` PII)
|
|
112
|
+
landed in the git-tracked, fleet-synced `agents.yaml`. A native login is
|
|
113
|
+
machine-local, so its home now follows its scope: a `scope:'device'` identity
|
|
114
|
+
(and any binding that targets it) writes this box's device doc `accounts:` block,
|
|
115
|
+
keeping its identity PII off the shared file; genuinely fleet-shared account
|
|
116
|
+
`defaults` and `scope:'version'` identities stay central. Every account reader
|
|
117
|
+
(`listNativeAccounts`, `findUnifiedAccount`, binding resolution, rename/remove)
|
|
118
|
+
merges the fleet-shared central store with this box's device slice. A
|
|
119
|
+
fold-then-delete migration relocates existing `scope:'device'` natives and their
|
|
120
|
+
bindings out of central (forward-only; no history rewrite). Source:
|
|
121
|
+
`cli/src/lib/account-registry.ts`, `cli/src/lib/devices/config-migration.ts`,
|
|
122
|
+
`cli/src/lib/state.ts`.
|
|
123
|
+
|
|
124
|
+
- **`tmux.enabled` now gates remote `--device` runs too — no more forced tmux wrap (PHNX-3316).**
|
|
125
|
+
`resolveTmuxWrap` (`cli/src/lib/exec.ts`) no longer lets `remoteDispatch`
|
|
126
|
+
override the device's `tmux.enabled`: off (the default) means a followed
|
|
127
|
+
`agents run --device <host>` spawns the agent directly, with no tmux session
|
|
128
|
+
on the peer. Durability for a bare remote run comes from the existing
|
|
129
|
+
auto-reconnect in `cli/src/lib/hosts/reconnect.ts`, whose target
|
|
130
|
+
(`agents sessions focus <id> --local`) already resumes the harness session in
|
|
131
|
+
place when no pane exists — a dropped link costs the in-flight turn, not the
|
|
132
|
+
conversation. One case still wraps regardless: a followed remote run whose
|
|
133
|
+
launcher has no TTY (CI, scripts, another agent driving the CLI), where the
|
|
134
|
+
detached pane is the run's only interface.
|
|
135
|
+
`undurable` is now reached only when a wrap was actually wanted (opted-in
|
|
136
|
+
device, or the no-TTY case) but the peer has no tmux; its refusal message
|
|
137
|
+
names the `agents ssh <host> 'agents devices config <host> tmux.enabled off'`
|
|
138
|
+
alternative (the key is machine-local, so it must be set on the peer)
|
|
139
|
+
alongside `--raw`.
|
|
140
|
+
RUSH-3125's forced wrap conflated durability with an ergonomics preference
|
|
141
|
+
and wrapped boxes whose operator had explicitly left tmux off.
|
|
142
|
+
`hosts/reconnect.ts`'s header premise, the `hosts/dispatch.ts`
|
|
143
|
+
REMOTE_INTERACTIVE_ENV comments, and the `tmux.enabled` help text
|
|
144
|
+
(`device-config.ts`) are updated to match; wrap-matrix tests cover the new
|
|
145
|
+
gate.
|
|
146
|
+
|
|
147
|
+
- **Share attribution bar: full-width chrome, not a floating box.** The bar was injected `position:sticky` inside `<body>`, so on a page whose body is a narrow centered column (`max-width` + `margin:auto`) it inherited that width and rendered as a floating black rectangle. It's now `position:fixed`, full viewport width, with an injected `html{padding-top}` pushing the page content down beneath it — clean top chrome on any page layout. `flex-wrap:nowrap` + left-side ellipsis keep it one line so the push height stays correct. Source: `cli/src/lib/share/worker-template.ts`.
|
|
148
|
+
|
|
3
149
|
## 1.22.52
|
|
4
150
|
|
|
5
151
|
- **`agents artifacts share --visibility me|org` — identity-gated share pages (PHNX-3260).**
|
package/README.md
CHANGED
|
@@ -1085,7 +1085,7 @@ agents harness add deepinfra --account deepinfra
|
|
|
1085
1085
|
|
|
1086
1086
|
One provider account **is** one `agents secrets` bundle -- `agents accounts add` creates it with secrets policy `never`, so a background agent launch on that account never raises Touch ID. `agents accounts` (no subcommand) lists provider bundles next to harness-native signed-in identities so you see both kinds of credential together; `accounts list` / `inspect <name>` / `set-key <name>` (rotate) / `rename` / `remove` manage a bundle by its stable id, independent of its current label. `accounts switch <harness>` is the fast picker over that default -- it writes the same binding as `set-default`, and balanced rotation already honors it.
|
|
1087
1087
|
|
|
1088
|
-
Harness-native OAuth logins (Claude Code's own `/login`, `codex login`, and so on) stay exactly where the harness put them -- agi-cli discovers and displays them but never copies, renames, or converts them into a provider bundle. Native `accounts name` / `attach` is only for harnesses agents-cli can isolate today (claude, codex, grok; muse when an email is present). A device-scoped login (kimi, droid, …) is refused with a named reason; provider `accounts add --provider` is unrestricted. `accounts sync <name> --device <device>` is the only way a provider account crosses machines, and it's explicit: nothing
|
|
1088
|
+
Harness-native OAuth logins (Claude Code's own `/login`, `codex login`, and so on) stay exactly where the harness put them -- agi-cli discovers and displays them but never copies, renames, or converts them into a provider bundle. Native `accounts name` / `attach` is only for harnesses agents-cli can isolate today (claude, codex, grok; muse when an email is present). A device-scoped login (kimi, droid, …) is refused with a named reason; provider `accounts add --provider` is unrestricted. Native **labels** (`agents accounts label codex personal`) bind to a stable identityKey on the central account row in `agents.yaml`, which `agents repo push/pull` already syncs fleet-wide, so `codex#personal` selects the same login on every box. `accounts sync <name> --device <device>` is the only way a **provider** account credential crosses machines, and it's explicit: nothing copies OAuth automatically. Selection order for a run is explicit `--account`, then `accounts set-default` / `switch` for that harness, then the harness's native/balanced account behavior.
|
|
1089
1089
|
|
|
1090
1090
|
---
|
|
1091
1091
|
|
|
@@ -464,7 +464,7 @@ export function registerAccountsCommand(program) {
|
|
|
464
464
|
agents accounts label codex@0.146.0 personal
|
|
465
465
|
agents accounts label codex work --account you@example.com
|
|
466
466
|
agents run codex#work`,
|
|
467
|
-
notes: 'The label binds to the signed-in account identity, not the version — codex#work keeps selecting that account after it moves to a newer install. One signed-in login needs no selector; with several, an interactive terminal opens a picker, while scripts pass --account or <harness>@<version>.',
|
|
467
|
+
notes: 'The label binds to the signed-in account identity, not the version — codex#work keeps selecting that account after it moves to a newer install. Labels live on the central account rows in agents.yaml, which `agents repo push/pull` already syncs fleet-wide, keyed by (agent, identityKey). One signed-in login needs no selector; with several, an interactive terminal opens a picker, while scripts pass --account or <harness>@<version>.',
|
|
468
468
|
});
|
|
469
469
|
accounts.command('attach <account> <target>')
|
|
470
470
|
.description('Attach a named account to a native installation or custom harness')
|
package/dist/commands/exec.js
CHANGED
|
@@ -1569,15 +1569,19 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1569
1569
|
});
|
|
1570
1570
|
}
|
|
1571
1571
|
}
|
|
1572
|
-
// A network drop kills the local ssh client (exit 255)
|
|
1573
|
-
// agent
|
|
1574
|
-
// (
|
|
1575
|
-
//
|
|
1576
|
-
//
|
|
1577
|
-
//
|
|
1578
|
-
//
|
|
1579
|
-
//
|
|
1580
|
-
//
|
|
1572
|
+
// A network drop kills the local ssh client (exit 255). What the
|
|
1573
|
+
// remote agent does next depends on the peer's tmux.enabled
|
|
1574
|
+
// (PHNX-3316): wrapped, it survives in its detached pane and the
|
|
1575
|
+
// reconnect rejoins it; bare (the default), the drop SIGHUPs it and
|
|
1576
|
+
// the reconnect resumes the session in place from disk. Either way,
|
|
1577
|
+
// with a known session id (Claude's forced id, a resumed run, or a
|
|
1578
|
+
// non-Claude id we just resolved from the remote hook record) we
|
|
1579
|
+
// reconnect automatically instead of exiting — the user never has
|
|
1580
|
+
// to notice the drop and `agents sessions focus` by hand.
|
|
1581
|
+
// `raw` runs opted out of all of this, so there is nothing to
|
|
1582
|
+
// reconnect to. For `run auto` prefer the join-resolved id (the
|
|
1583
|
+
// harness the remote ACTUALLY picked) over the explicit
|
|
1584
|
+
// --session-id only claude adopts.
|
|
1581
1585
|
const { pickReconnectTarget, reconnectInteractiveSession, afterInteractiveRemoteExit, SSH_CONN_FAILURE } = await import('../lib/hosts/reconnect.js');
|
|
1582
1586
|
const reconnectTarget = pickReconnectTarget({
|
|
1583
1587
|
agent: runAgent,
|
|
@@ -1814,6 +1818,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1814
1818
|
let accountConfigVersion;
|
|
1815
1819
|
let profileProvider;
|
|
1816
1820
|
let fromProfile = false;
|
|
1821
|
+
let profileName;
|
|
1817
1822
|
let profileFallbackModel;
|
|
1818
1823
|
let workflowModel;
|
|
1819
1824
|
// WORKFLOW.md capability scoping, translated to Claude headless flags below.
|
|
@@ -1897,6 +1902,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1897
1902
|
profileProvider = readProfile(rawAgent).provider;
|
|
1898
1903
|
profileFallbackModel = resolved.fallbackModel;
|
|
1899
1904
|
fromProfile = true;
|
|
1905
|
+
profileName = resolved.profileName;
|
|
1900
1906
|
process.stderr.write(chalk.gray(`Resolved custom harness '${resolved.profileName}' -> ${agent}${version ? `@${version}` : ''}\n`));
|
|
1901
1907
|
if (resolved.tierNote) {
|
|
1902
1908
|
process.stderr.write(chalk.gray(`[agents] ${resolved.tierNote}\n`));
|
|
@@ -2745,6 +2751,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2745
2751
|
}
|
|
2746
2752
|
const execOptions = {
|
|
2747
2753
|
agent,
|
|
2754
|
+
harnessName: profileName,
|
|
2748
2755
|
version,
|
|
2749
2756
|
configVersion: accountConfigVersion,
|
|
2750
2757
|
prompt,
|
|
@@ -71,6 +71,13 @@ async function runCapture(opts) {
|
|
|
71
71
|
secretsBundles: listBundles().map((b) => b.name),
|
|
72
72
|
routines: listJobs().map((j) => j.name),
|
|
73
73
|
};
|
|
74
|
+
// Discovery decisions and dismissals are device-scoped now (PHNX-3315): they
|
|
75
|
+
// live in each box's device doc and sync via the repo, so `capture` must NOT
|
|
76
|
+
// hoist the cross-box union back into the shared `agents.yaml` `fleet:` block
|
|
77
|
+
// (that would recreate the N-boxes-rewrite-one-map conflict Task A removed and
|
|
78
|
+
// re-arm the fold-then-delete migration on every capture). `meta.fleet` carries
|
|
79
|
+
// only the roster/defaults; any lingering central-legacy discovery/ignored is
|
|
80
|
+
// carried forward verbatim and drained by the migration.
|
|
74
81
|
const next = captureFleet(meta.fleet, inputs);
|
|
75
82
|
if (opts.dryRun) {
|
|
76
83
|
console.log(chalk.gray('# agents.yaml fleet: block (dry run — not written)'));
|
package/dist/commands/focus.js
CHANGED
|
@@ -31,6 +31,7 @@ import { multiItemPicker } from '../lib/picker.js';
|
|
|
31
31
|
import { sessionRecoveryRunArgs } from '../lib/session/recovery.js';
|
|
32
32
|
import { shellQuote, assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
33
33
|
import { openSurfaces, currentContext, availableBackends, detectCurrentBackend, } from '../lib/terminal/index.js';
|
|
34
|
+
import { addressabilityRecoveryHint } from '../lib/terminal/resolve.js';
|
|
34
35
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
35
36
|
import { setHelpSections } from '../lib/help.js';
|
|
36
37
|
const INHERITED_FOCUS_OPTIONS = [
|
|
@@ -534,6 +535,7 @@ export async function focusSelectedSession(meta, active, self) {
|
|
|
534
535
|
return;
|
|
535
536
|
}
|
|
536
537
|
console.log(chalk.yellow('This live session has no session id or living attach rail to focus.'));
|
|
538
|
+
console.log(chalk.gray(addressabilityRecoveryHint(active, meta.id)));
|
|
537
539
|
process.exitCode = 1;
|
|
538
540
|
return;
|
|
539
541
|
}
|
package/dist/commands/go.js
CHANGED
|
@@ -34,6 +34,7 @@ import { getDefaultSocketPath } from '../lib/tmux/paths.js';
|
|
|
34
34
|
import { sshExec, sshStream, assertValidSshTarget, shellQuote, SSH_CONN_FAILURE_CODE } from '../lib/ssh-exec.js';
|
|
35
35
|
import { connectionEndedNotice } from '../lib/hosts/reconnect.js';
|
|
36
36
|
import { enumerateGhosttyTabs, assignGhosttyTabs } from '../lib/session/ghostty-tabs.js';
|
|
37
|
+
import { addressabilityRecoveryHint } from '../lib/terminal/resolve.js';
|
|
37
38
|
const execFileAsync = promisify(execFile);
|
|
38
39
|
export function registerGoCommand(program) {
|
|
39
40
|
program
|
|
@@ -318,7 +319,7 @@ export async function refuseFallback(s, remote) {
|
|
|
318
319
|
return;
|
|
319
320
|
}
|
|
320
321
|
console.log(chalk.yellow(`Can't jump to ${shortId(s)} — it's in ${s.host ?? 'an unknown terminal'} with no attach rail (not tmux/Ghostty).`) +
|
|
321
|
-
chalk.gray(`\
|
|
322
|
+
chalk.gray(`\n${addressabilityRecoveryHint(s)}`));
|
|
322
323
|
process.exitCode = 1;
|
|
323
324
|
}
|
|
324
325
|
export async function jumpTo(s, self, fallback = refuseFallback) {
|
|
@@ -26,7 +26,11 @@ async function resolveTarget(sessionId) {
|
|
|
26
26
|
const resolution = resolveInjectTargetForSession(match);
|
|
27
27
|
if (!resolution.addressable) {
|
|
28
28
|
// Surface the resolver's precise reason (host/rail), not a generic "not tmux".
|
|
29
|
-
return {
|
|
29
|
+
return {
|
|
30
|
+
target: null,
|
|
31
|
+
reason: `Session "${sessionId}": ${resolution.reason}`,
|
|
32
|
+
hint: resolution.hint,
|
|
33
|
+
};
|
|
30
34
|
}
|
|
31
35
|
return { target: resolution.target };
|
|
32
36
|
}
|
|
@@ -43,10 +47,11 @@ async function runInject(sessionId, text, options) {
|
|
|
43
47
|
else {
|
|
44
48
|
const resolved = await resolveTarget(sessionId);
|
|
45
49
|
if (!resolved.target) {
|
|
50
|
+
const message = resolved.hint ? `${resolved.reason}\n${resolved.hint}` : resolved.reason;
|
|
46
51
|
if (options.json)
|
|
47
|
-
console.log(JSON.stringify({ ok: false, error:
|
|
52
|
+
console.log(JSON.stringify({ ok: false, error: message }));
|
|
48
53
|
else
|
|
49
|
-
console.error(chalk.red(
|
|
54
|
+
console.error(chalk.red(message));
|
|
50
55
|
process.exit(1);
|
|
51
56
|
}
|
|
52
57
|
target = resolved.target;
|
|
@@ -9,6 +9,7 @@ import fs from 'node:fs';
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
import { truncate, humanDuration } from '../lib/format.js';
|
|
12
|
+
import { sessionDisplayAgent } from '../lib/session/types.js';
|
|
12
13
|
import { fetchPeerPreviewDigest } from '../lib/session/remote-list.js';
|
|
13
14
|
import { parseSession, sanitizeForTerminal, SNAPSHOT_TODO_TOOLS } from '../lib/session/parse.js';
|
|
14
15
|
import { safeTeamText } from '../lib/session/team-filter.js';
|
|
@@ -404,7 +405,7 @@ function formatHeader(session, events) {
|
|
|
404
405
|
const { createdAgo, lastActiveAgo, duration } = extractTiming(session, events);
|
|
405
406
|
// Line 1: Agent v version · shortId · model · account
|
|
406
407
|
const line1 = [];
|
|
407
|
-
line1.push(chalk.gray(`${displayAgent(session
|
|
408
|
+
line1.push(chalk.gray(`${displayAgent(sessionDisplayAgent(session))}${session.version ? ` v${session.version}` : ''}`));
|
|
408
409
|
if (session.shortId)
|
|
409
410
|
line1.push(chalk.dim(session.shortId));
|
|
410
411
|
if (model)
|
|
@@ -18,7 +18,7 @@ import { sanitizeForTerminal, redactSecrets } from '../lib/redact.js';
|
|
|
18
18
|
import { resolveProjectKey } from '../lib/project-key.js';
|
|
19
19
|
import { listProjectDefs, resolveProjectNameForCwd } from '../lib/projects.js';
|
|
20
20
|
import ora from 'ora';
|
|
21
|
-
import { SESSION_AGENTS, isAgentTmuxAlias } from '../lib/session/types.js';
|
|
21
|
+
import { SESSION_AGENTS, isAgentTmuxAlias, sessionDisplayAgent } from '../lib/session/types.js';
|
|
22
22
|
import { discoverArtifacts, readArtifact, resolveArtifact } from '../lib/session/artifacts.js';
|
|
23
23
|
import { looksLikePath, toComparablePath, homeDir, needsWindowsShell, composeWin32CommandLine } from '../lib/platform/index.js';
|
|
24
24
|
import { getActiveSessions, describeActiveDiscoveryHealth, sessionProcessIsLocal, backfillActiveRowsFromIndex, isRunningLiveSession, serializeActiveSessionsForJson, serializeSessionsJson, shortIdFromName } from '../lib/session/active.js';
|
|
@@ -233,9 +233,10 @@ async function renderArtifactsForSession(session, listAll, name) {
|
|
|
233
233
|
console.log(chalk.gray('No file-write artifacts found in this session.'));
|
|
234
234
|
return;
|
|
235
235
|
}
|
|
236
|
-
const
|
|
236
|
+
const shown = sessionDisplayAgent(session);
|
|
237
|
+
const agentColor = colorAgent(shown);
|
|
237
238
|
console.log('');
|
|
238
|
-
console.log(agentColor(
|
|
239
|
+
console.log(agentColor(shown) +
|
|
239
240
|
chalk.gray(` · ${session.shortId} · ${formatRelativeTime(session.timestamp)}`));
|
|
240
241
|
console.log(chalk.gray('─'.repeat(72)));
|
|
241
242
|
for (const a of artifacts) {
|
|
@@ -726,7 +727,8 @@ function fitCell(content, room) {
|
|
|
726
727
|
*/
|
|
727
728
|
export function renderActiveRowLines(s, indent, termW) {
|
|
728
729
|
const idCol = chalk.dim(padToWidth((s.sessionId?.slice(0, 8)) ?? '-', ROW_ID_W));
|
|
729
|
-
const
|
|
730
|
+
const shownKind = sessionDisplayAgent({ agent: s.kind, harness: s.harness });
|
|
731
|
+
const kindCol = colorAgent(shownKind)(padToWidth(truncateToWidth(shownKind, ROW_AGENT_W), ROW_AGENT_W + 1));
|
|
730
732
|
const versionCol = chalk.gray(padToWidth(truncateToWidth(s.version ?? '', ROW_VERSION_W), ROW_VERSION_W + 1));
|
|
731
733
|
const statusCol = statusColor(s.status)(padToWidth(truncateToWidth(activityLabel(s), ROW_STATUS_W - 1), ROW_STATUS_W));
|
|
732
734
|
const ownerCol = chalk.cyan(padToWidth(truncateToWidth(ownerLabel(s), ROW_OWNER_W - 1), ROW_OWNER_W));
|
|
@@ -1010,6 +1012,7 @@ export function serializeResolvedSessionsJson(sessions) {
|
|
|
1010
1012
|
id: session.id,
|
|
1011
1013
|
shortId: session.shortId,
|
|
1012
1014
|
agent: session.agent,
|
|
1015
|
+
harness: session.harness,
|
|
1013
1016
|
origin: session.origin,
|
|
1014
1017
|
timestamp: session.timestamp,
|
|
1015
1018
|
lastActivity: session.lastActivity,
|
|
@@ -1805,7 +1808,8 @@ function runOutcomeDetail(meta) {
|
|
|
1805
1808
|
/** Compact per-session metadata line under a linked session row. */
|
|
1806
1809
|
function linkedSessionMeta(session) {
|
|
1807
1810
|
const parts = [];
|
|
1808
|
-
|
|
1811
|
+
const shown = sessionDisplayAgent(session);
|
|
1812
|
+
parts.push(session.version ? `${shown} v${session.version}` : shown);
|
|
1809
1813
|
if (session.account)
|
|
1810
1814
|
parts.push(session.account);
|
|
1811
1815
|
if (session.model)
|
|
@@ -2996,7 +3000,8 @@ function timeCell(age, topicSlack) {
|
|
|
2996
3000
|
* when `showTicket` — otherwise a listing with no refs would waste a column of
|
|
2997
3001
|
* dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
|
|
2998
3002
|
export function flatSessionRow(session, live, showTicket = false, cols = {}, bookmarked = false) {
|
|
2999
|
-
const
|
|
3003
|
+
const shown = sessionDisplayAgent(session);
|
|
3004
|
+
const agentColor = colorAgent(shown);
|
|
3000
3005
|
const age = sessionAgeParts(session.timestamp, session.lastActivity);
|
|
3001
3006
|
const project = session.project || '-';
|
|
3002
3007
|
const tag = originTag(session) || teamTag(session);
|
|
@@ -3045,7 +3050,7 @@ export function flatSessionRow(session, live, showTicket = false, cols = {}, boo
|
|
|
3045
3050
|
const topicW = Math.max(MIN_TOPIC_W, width - fixedW - modelW - when.extraW);
|
|
3046
3051
|
return (bookmarkCell +
|
|
3047
3052
|
chalk.white(padToWidth(truncateToWidth(session.shortId, 9), 10)) +
|
|
3048
|
-
agentColor(padToWidth(truncateToWidth(
|
|
3053
|
+
agentColor(padToWidth(truncateToWidth(shown, 8), 9)) +
|
|
3049
3054
|
chalk.yellow(padToWidth(truncateToWidth(session.version || '-', 7), 8)) +
|
|
3050
3055
|
(modelW ? chalk.yellow(padToWidth(truncateToWidth(modelLabel(session.model), modelW - 1), modelW)) : '') +
|
|
3051
3056
|
machineCell +
|
|
@@ -3060,7 +3065,8 @@ export function flatSessionRow(session, live, showTicket = false, cols = {}, boo
|
|
|
3060
3065
|
}
|
|
3061
3066
|
/** One tree-mode row (grouped under a dir header): id · agent · badges · topic · time. No version/project column. */
|
|
3062
3067
|
function treeSessionRow(session, live) {
|
|
3063
|
-
const
|
|
3068
|
+
const shown = sessionDisplayAgent(session);
|
|
3069
|
+
const agentColor = colorAgent(shown);
|
|
3064
3070
|
const age = sessionAgeParts(session.timestamp, session.lastActivity);
|
|
3065
3071
|
const tag = originTag(session) || teamTag(session);
|
|
3066
3072
|
const label = session.label;
|
|
@@ -3082,7 +3088,7 @@ function treeSessionRow(session, live) {
|
|
|
3082
3088
|
const topicW = Math.max(12, baseTopicW - when.extraW);
|
|
3083
3089
|
return (' ' +
|
|
3084
3090
|
chalk.dim(padToWidth(session.shortId, 9)) +
|
|
3085
|
-
agentColor(padToWidth(truncateToWidth(
|
|
3091
|
+
agentColor(padToWidth(truncateToWidth(shown, 7), 8)) +
|
|
3086
3092
|
(badges ? badges + ' ' : '') +
|
|
3087
3093
|
(glyph ? glyph + ' ' : '') +
|
|
3088
3094
|
statusCell +
|
|
@@ -3246,7 +3252,8 @@ const TEAM_HANDLE_W = 16;
|
|
|
3246
3252
|
* identity the `--teams` view exists to show (RUSH-1997).
|
|
3247
3253
|
*/
|
|
3248
3254
|
function teamMemberRow(session, live) {
|
|
3249
|
-
const
|
|
3255
|
+
const shown = sessionDisplayAgent(session);
|
|
3256
|
+
const agentColor = colorAgent(shown);
|
|
3250
3257
|
const origin = session.teamOrigin;
|
|
3251
3258
|
const age = sessionAgeParts(session.timestamp, session.lastActivity);
|
|
3252
3259
|
const handle = safeTeamText(origin?.handle) ?? session.shortId;
|
|
@@ -3267,7 +3274,7 @@ function teamMemberRow(session, live) {
|
|
|
3267
3274
|
const topicW = Math.max(12, baseTopicW - when.extraW);
|
|
3268
3275
|
return (' ' +
|
|
3269
3276
|
chalk.dim(padToWidth(session.shortId, 9)) +
|
|
3270
|
-
agentColor(padToWidth(truncateToWidth(
|
|
3277
|
+
agentColor(padToWidth(truncateToWidth(shown, 7), 8)) +
|
|
3271
3278
|
chalk.yellow(padToWidth(truncateToWidth(mode || '-', TEAM_MODE_W), TEAM_MODE_W + 1)) +
|
|
3272
3279
|
(badges ? badges + ' ' : '') +
|
|
3273
3280
|
(glyph ? glyph + ' ' : '') +
|
|
@@ -3444,13 +3451,14 @@ function renderArchivedSession(session, mode, options = {}) {
|
|
|
3444
3451
|
}, null, 2));
|
|
3445
3452
|
return;
|
|
3446
3453
|
}
|
|
3447
|
-
const
|
|
3454
|
+
const shown = sessionDisplayAgent(session);
|
|
3455
|
+
const agentColor = colorAgent(shown);
|
|
3448
3456
|
const absTime = formatAbsoluteTime(session.timestamp);
|
|
3449
3457
|
const title = session.label || session.topic;
|
|
3450
3458
|
console.log('');
|
|
3451
3459
|
if (title)
|
|
3452
3460
|
console.log(chalk.bold.white(title));
|
|
3453
|
-
console.log(agentColor(
|
|
3461
|
+
console.log(agentColor(shown) +
|
|
3454
3462
|
(session.version ? chalk.yellow(` ${session.version}`) : '') +
|
|
3455
3463
|
(session.project ? chalk.cyan(` ${session.project}`) : '') +
|
|
3456
3464
|
chalk.gray(` ${absTime} (${formatRelativeTime(session.timestamp)})`) +
|
|
@@ -3481,7 +3489,7 @@ async function renderSession(session, mode, filters, options = {}) {
|
|
|
3481
3489
|
console.log(chalk.yellow('Session transcript not available (file no longer exists).'));
|
|
3482
3490
|
console.log(chalk.gray(`Path: ${session.filePath}`));
|
|
3483
3491
|
if (session.version)
|
|
3484
|
-
console.log(chalk.gray(`Version: ${session
|
|
3492
|
+
console.log(chalk.gray(`Version: ${sessionDisplayAgent(session)} ${session.version}`));
|
|
3485
3493
|
if (session.project)
|
|
3486
3494
|
console.log(chalk.gray(`Project: ${session.project}`));
|
|
3487
3495
|
if (session.account)
|
|
@@ -3489,11 +3497,12 @@ async function renderSession(session, mode, filters, options = {}) {
|
|
|
3489
3497
|
console.log(chalk.gray(`Time: ${session.timestamp}`));
|
|
3490
3498
|
return;
|
|
3491
3499
|
}
|
|
3492
|
-
const spinner = ora(`Parsing ${session
|
|
3500
|
+
const spinner = ora(`Parsing ${sessionDisplayAgent(session)} session...`).start();
|
|
3493
3501
|
const parsedEvents = parseSession(session.filePath, session.agent);
|
|
3494
3502
|
spinner.stop();
|
|
3495
3503
|
let events = filterEvents(parsedEvents, filters);
|
|
3496
|
-
const
|
|
3504
|
+
const shown = sessionDisplayAgent(session);
|
|
3505
|
+
const agentColor = colorAgent(shown);
|
|
3497
3506
|
console.log('');
|
|
3498
3507
|
if (mode === 'summary') {
|
|
3499
3508
|
const stats = computeSummaryStats(events);
|
|
@@ -3507,7 +3516,7 @@ async function renderSession(session, mode, filters, options = {}) {
|
|
|
3507
3516
|
const badges = signalBadges(metaSignals(session));
|
|
3508
3517
|
console.log(chalk.bold.white(title) + (badges ? ' ' + badges : ''));
|
|
3509
3518
|
}
|
|
3510
|
-
console.log(agentColor(
|
|
3519
|
+
console.log(agentColor(shown) +
|
|
3511
3520
|
(session.version ? chalk.yellow(` ${session.version}`) : '') +
|
|
3512
3521
|
modelStr +
|
|
3513
3522
|
(session.project ? chalk.cyan(` ${session.project}`) + branchStr : branchStr) +
|
|
@@ -3521,7 +3530,7 @@ async function renderSession(session, mode, filters, options = {}) {
|
|
|
3521
3530
|
return;
|
|
3522
3531
|
}
|
|
3523
3532
|
if (mode === 'markdown') {
|
|
3524
|
-
console.log(agentColor(
|
|
3533
|
+
console.log(agentColor(shown) +
|
|
3525
3534
|
(session.version ? chalk.yellow(` ${session.version}`) : '') +
|
|
3526
3535
|
(session.project ? chalk.cyan(` ${session.project}`) : '') +
|
|
3527
3536
|
chalk.gray(` ${formatRelativeTime(session.timestamp)}`) +
|
|
@@ -3662,7 +3671,8 @@ export function liveHostLabel(a) {
|
|
|
3662
3671
|
return viewer && viewer !== a.host ? `${a.host}→${viewer}` : a.host;
|
|
3663
3672
|
}
|
|
3664
3673
|
export function formatPickerLabel(s, query, cols = {}, ssh, host = '', bookmarked = false, live) {
|
|
3665
|
-
const
|
|
3674
|
+
const shown = sessionDisplayAgent(s);
|
|
3675
|
+
const agentColor = colorAgent(shown);
|
|
3666
3676
|
const age = sessionAgeParts(s.timestamp, s.lastActivity);
|
|
3667
3677
|
const project = s.project || '-';
|
|
3668
3678
|
// SSH-launch origin (live rows only): mirrors the flat listing's `ssh←<device>`
|
|
@@ -3727,7 +3737,7 @@ export function formatPickerLabel(s, query, cols = {}, ssh, host = '', bookmarke
|
|
|
3727
3737
|
// live row with no session id is named by its pid or cloud task, which can
|
|
3728
3738
|
// run past the column and shunt every later column out of alignment.
|
|
3729
3739
|
chalk.white(padRight(truncate(s.shortId, 9), 10)) +
|
|
3730
|
-
agentColor(padRight(truncate(
|
|
3740
|
+
agentColor(padRight(truncate(shown, 8), 9)) +
|
|
3731
3741
|
chalk.yellow(padRight(truncate(versionStr, 7), 8)) +
|
|
3732
3742
|
machineCell +
|
|
3733
3743
|
hostCell +
|
package/dist/commands/ssh.js
CHANGED
|
@@ -1136,7 +1136,7 @@ function registerDevicesCommands(program) {
|
|
|
1136
1136
|
registerFleetApplyAlias(devicesCmd);
|
|
1137
1137
|
devicesCmd
|
|
1138
1138
|
.command('register <name>')
|
|
1139
|
-
.description(
|
|
1139
|
+
.description("Register a discovered node and record the approval in this box's device doc (fleet.discovery), unioned fleet-wide.")
|
|
1140
1140
|
.action(async (name) => {
|
|
1141
1141
|
try {
|
|
1142
1142
|
const nodes = parseTailscaleStatus(tailscaleStatusJson());
|
|
@@ -1145,11 +1145,20 @@ function registerDevicesCommands(program) {
|
|
|
1145
1145
|
console.error(chalk.red(`'${name}' is not a current tailscale node. See 'agents devices sync'.`));
|
|
1146
1146
|
process.exit(1);
|
|
1147
1147
|
}
|
|
1148
|
-
await removeIgnored(name); //
|
|
1148
|
+
await removeIgnored(name); // clear THIS box's dismissal, if any
|
|
1149
1149
|
const d = await upsertDevice(name, nodeToDeviceInput(node));
|
|
1150
1150
|
setDeviceDiscoveryStatus(name, 'approved');
|
|
1151
1151
|
clearPendingSentinel(name); // drop the notification immediately
|
|
1152
1152
|
console.log(chalk.green(`Registered '${name}'`) + chalk.gray(` (${d.platform})`));
|
|
1153
|
+
// A dismissal on another box still wins the union (ignored beats
|
|
1154
|
+
// approved), so approving here does not un-dismiss it fleet-wide — say so
|
|
1155
|
+
// rather than imply the node is now discoverable everywhere (PHNX-3315).
|
|
1156
|
+
const otherDismissals = loadIgnoredEntries().filter((e) => e.name === name);
|
|
1157
|
+
if (otherDismissals.length > 0 || getDeviceDiscoveryStatus(name) === 'ignored') {
|
|
1158
|
+
const boxes = [...new Set(otherDismissals.map((e) => e.ignoredOn))].sort().join(', ') || 'another box';
|
|
1159
|
+
console.error(chalk.yellow(`Note: '${name}' is still dismissed on: ${boxes}, so it stays ignored fleet-wide`) +
|
|
1160
|
+
chalk.gray(` until you run \`agents devices unignore ${name}\` there.`));
|
|
1161
|
+
}
|
|
1153
1162
|
}
|
|
1154
1163
|
catch (err) {
|
|
1155
1164
|
console.error(chalk.red(err.message));
|
|
@@ -1158,7 +1167,7 @@ function registerDevicesCommands(program) {
|
|
|
1158
1167
|
});
|
|
1159
1168
|
devicesCmd
|
|
1160
1169
|
.command('ignore <name>')
|
|
1161
|
-
.description(
|
|
1170
|
+
.description("Dismiss a node and record the decision in this box's device doc (fleet.ignored), unioned fleet-wide (also removes it locally).")
|
|
1162
1171
|
.action(async (name) => {
|
|
1163
1172
|
try {
|
|
1164
1173
|
await removeDevice(name);
|
|
@@ -1176,13 +1185,25 @@ function registerDevicesCommands(program) {
|
|
|
1176
1185
|
.command('unignore <name>')
|
|
1177
1186
|
.description('Undo `ignore`: allow a node to be discovered and registered again.')
|
|
1178
1187
|
.action(async (name) => {
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1188
|
+
const wasIgnored = getDeviceDiscoveryStatus(name) === 'ignored' || loadIgnoredEntries().some((e) => e.name === name);
|
|
1189
|
+
// A box can edit only its OWN device doc (PHNX-3315), so this clears just
|
|
1190
|
+
// this box's dismissal/approval decision.
|
|
1191
|
+
await removeIgnored(name);
|
|
1192
|
+
setDeviceDiscoveryStatus(name, undefined);
|
|
1193
|
+
// Re-evaluate the cross-box union AFTER clearing our slice: another box's
|
|
1194
|
+
// dismissal keeps the node ignored fleet-wide (ignored beats approved), so
|
|
1195
|
+
// never report a false "no longer ignoring" — name the box(es) to clear.
|
|
1196
|
+
const remaining = loadIgnoredEntries().filter((e) => e.name === name);
|
|
1197
|
+
if (remaining.length > 0 || getDeviceDiscoveryStatus(name) === 'ignored') {
|
|
1198
|
+
const boxes = [...new Set(remaining.map((e) => e.ignoredOn))].sort().join(', ') || 'another box';
|
|
1199
|
+
console.error(chalk.yellow(`Cleared this box's decision, but '${name}' is still dismissed on: ${boxes}.`) +
|
|
1200
|
+
chalk.gray(`\nRun \`agents devices unignore ${name}\` there too to clear it fleet-wide.`));
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
if (!wasIgnored) {
|
|
1182
1204
|
console.error(chalk.gray(`'${name}' was not ignored.`));
|
|
1183
1205
|
return;
|
|
1184
1206
|
}
|
|
1185
|
-
setDeviceDiscoveryStatus(name, undefined);
|
|
1186
1207
|
console.log(chalk.green(`No longer ignoring '${name}'`) + chalk.gray(' — run `agents devices sync` to register it.'));
|
|
1187
1208
|
});
|
|
1188
1209
|
const ignoredCmd = devicesCmd
|
|
@@ -1219,11 +1240,13 @@ function registerDevicesCommands(program) {
|
|
|
1219
1240
|
agents devices unignore old-laptop # undo a dismissal
|
|
1220
1241
|
`,
|
|
1221
1242
|
notes: `
|
|
1222
|
-
Dismissals live in
|
|
1223
|
-
sync with 'agents repo
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1243
|
+
Dismissals live in each box's tracked device doc
|
|
1244
|
+
(devices/<host>/agents.yaml fleet.ignored) and sync with 'agents repo
|
|
1245
|
+
push/pull'; the effective list is their cross-box union, so a node
|
|
1246
|
+
dismissed on one box stays dismissed everywhere without the boxes ever
|
|
1247
|
+
rewriting one shared file. An ignored node is not a device — it never
|
|
1248
|
+
enters the registry, so 'agents devices list' never shows it; this command
|
|
1249
|
+
is where dismissals are visible.
|
|
1227
1250
|
`,
|
|
1228
1251
|
});
|
|
1229
1252
|
// ─── devices config (unified settings surface) ────────────────────────────
|