Skip to content

fix(devices): resolve the operator platform before choosing a probe's shell family - #3711

Open
taylorg009 wants to merge 4 commits into
mainfrom
fix/fleet-probe-resolved-profile
Open

taylorg009 wants to merge 4 commits into
mainfrom
fix/fleet-probe-resolved-profile

Conversation

@taylorg009

@taylorg009 taylorg009 commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Several fleet paths chose the PowerShell vs POSIX remote shell from the registry's discovered platform/shell, while buildSshInvocation / sshTargetFor dial with the resolved profile (per-device config: platform overlay from resolve-profile.ts).

A Windows-discovered box whose sshd lands in WSL, configured with agents devices config <name> platform linux, was dialed as a bash login shell but handed a PowerShell snippet or wrapper. Empty stdout became reachable: false in agents devices list --refresh (offline on every refresh) and "unreachable or no agents CLI — skipped" in agents sessions --active, even though agents ssh <name> worked. Because every tailnet sync re-stamps the discovered platform, re-adding the device manually as Linux only lasted until the next sync.

Fix

Resolve the operator profile before choosing the shell family, at every site that read the raw registry record:

  • devices/health.ts: extract buildProbeInvocation (resolve first, then snippet + budget); probeDeviceStats consumes it.
  • teams/placement-probe.ts: readiness probe command.
  • accounting/usage-sync.ts: peer exchange command.
  • session/remote/remote-list.ts: fan-out targets and the single-machine target.
  • remote-agents-json.ts: fleet JSON gather targets.
  • commands/doctor.ts: fleet probe targets.

hosts/providers/devices.ts already resolved and is unchanged.

Tests

  • health.test.ts: two new cases on the real config read path (temp HOME + per-device agents.yaml, fresh modules, no mocks). A Windows-discovered profile with no override dials the PowerShell wrapper on the Windows budget; with platform: linux in the device doc it dials PROBE_SNIPPET on the relayed POSIX budget and targets the configured user.
  • vitest run on devices/health, devices/resolve-profile, teams/placement-probe, accounting/usage-sync, session/remote/remote-list, remote-agents-json, commands/doctor: all green. tsc --noEmit: clean.

End-to-end

Real fleet, WSL-backed Windows host jupiter (discovered windows, configured platform: linux, sshd DefaultShell routed into WSL):

command installed 1.22.117 this tree (tsx src/index.ts)
devices list --refresh jupiter linux offline jupiter linux 20c 15.5G 1007G 0% 7% 5% idle
sessions --active jupiter: unreachable or no agents CLI — skipped jupiter (2) with both live codex sessions

🤖 Generated with Claude Code

taylor009 and others added 2 commits September 24, 2026 18:49
… shell family

probeDeviceStats picked the PowerShell vs POSIX stats snippet (and the
probe budget) from the registry's DISCOVERED shell, while buildSshInvocation
dialed with the RESOLVED profile (per-device `config: platform` overlay). A
Windows-discovered box whose sshd lands in WSL, configured
`agents devices config <name> platform linux`, was therefore dialed as a bash
login shell but handed the PowerShell snippet: empty stdout, `reachable:
false`, and an "offline" row on every `agents devices list --refresh` even
though `agents ssh <name>` worked. Every tailnet sync re-stamps the discovered
platform, so the manual-add workaround did not survive either.

Extract buildProbeInvocation (resolves first, then picks snippet + budget)
and cover it against the real config read path (temp HOME + device doc,
no mocks). Apply the same resolve-before-shell-check to the two sibling
probes that inspected `device.shell` directly: the teams readiness probe and
the usage-sync peer exchange.

Verified end to end on the fleet: with the discovered-windows / configured-
linux profile the installed CLI renders the box offline and the patched tree
renders it idle with live load/mem/disk.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… JSON gather and doctor probes

Same defect class as the stats probe: the session fan-out (remote-list),
the fleet agents-json gather and the doctor fleet probe all handed
`remoteShellFor` the registry's DISCOVERED platform while dialing with the
resolved profile. A WSL-routed Windows box configured `platform: linux` was
sent a PowerShell wrapper into a bash login shell and skipped as
"unreachable or no agents CLI" — `agents sessions --active` hid its
sessions even though `agents ssh <name>` worked.

Verified on the fleet: the installed CLI skips the box; this tree lists it
with its two live sessions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@taylorg009

Copy link
Copy Markdown
Collaborator Author

Review verdict: REQUEST CHANGES

Non-author review (subagent reviewer — prix-cloud is paused per AGENTS.md #1767). Verified against the "Code review conventions" block in AGENTS.md.

The diagnosis is correct

The claim checks out. The three dial helpers all resolve the operator profile internally:

  • cli/src/lib/devices/connect.ts:48-49 — export function sshTargetFor(device: DeviceProfile): string { / const resolved = resolveDeviceProfile(device);
  • cli/src/lib/devices/connect.ts:362-363 — export function deviceIdentityArgs(device: DeviceProfile): string[] { / const resolved = resolveDeviceProfile(device);
  • cli/src/lib/devices/connect.ts:406 — device = resolveDeviceProfile(device);

and cli/src/lib/devices/resolve-profile.ts:30,49 overlays config.platform onto the record and re-derives the shell (shell: shellForPlatform(platform),), idempotently (cli/src/lib/devices/resolve-profile.ts:36-45 returns device unchanged when nothing overrides). So a site that read device.shell / device.platform raw while dialing through those helpers genuinely mismatched.

Each of the six changed sites is correct as written. The PR's claim that hosts/providers/devices.ts already resolved is also true:

  • cli/src/lib/hosts/providers/devices.ts:41 — const device = resolveDeviceProfile(rawDevice);
  • cli/src/lib/hosts/providers/devices.ts:51 — ...(device.platform !== 'unknown' ? { os: device.platform } : {}),
  • cli/src/lib/hosts/providers/devices.ts:81 — const device = resolveDeviceProfile(raw);

Blocking: the sweep is incomplete — four more sites pick the shell from the raw record

AGENTS.md §Code review conventions, "Surface parity for propagation / cross-cutting features": the data "must be wired through every exec boundary that data is meant to reach — or the PR states which boundaries are out of scope and why." The PR description enumerates six sites plus providers/devices.ts and says nothing about the following. All four dial through resolved helpers and then choose the shell family from the unresolved registry platform — the exact defect this PR fixes.

1. cli/src/commands/doctor.ts:1436 — in the file this PR edits.

cli/src/commands/doctor.ts:1436      platform: t.device.platform,
cli/src/commands/doctor.ts:1438      dialTarget: fleetDialTarget(t.device),
cli/src/commands/doctor.ts:1439      extraSshArgs: deviceIdentityArgs(t.device),

consumed by:

cli/src/commands/doctor.ts:1403  const isWin = /^win/i.test((target.platform ?? '').trim());
cli/src/commands/doctor.ts:1407    isWin ? 'windows' : undefined,
cli/src/commands/doctor.ts:1408    isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' },

fleetDialTarget resolves (cli/src/lib/devices/connect.ts:74-75: } catch { / const resolved = resolveDeviceProfile(device);), and planFleetTargets hands back the raw registry record (cli/src/lib/devices/fleet.ts:61,70: const device = reg[name]; … return { device };). So agents doctor --check --devices dials jupiter as bash and hands it the PowerShell form.

This one is not a judgment call: the sibling fan-outs in cli/src/commands/ssh.ts already do the right thing on origin/main, untouched by this PR —

cli/src/commands/ssh.ts:798       platform: resolveDeviceProfile(t.device).platform,
cli/src/commands/ssh.ts:1017    platform: resolveDeviceProfile(t.device).platform,
cli/src/commands/ssh.ts:1120    platform: resolveDeviceProfile(t.device).platform,

doctor.ts:1436 is the last raw remoteFleetTargets consumer in the tree.

2. cli/src/lib/session/remote/watch.ts:511 — agents sessions watch.

cli/src/lib/session/remote/watch.ts:511      command: remoteWatchCommand(device.platform),
cli/src/lib/session/remote/watch.ts:480 function remoteWatchCommand(os: string): string {
cli/src/lib/session/remote/watch.ts:482   return remoteShellFor(os) === 'powershell'
cli/src/lib/session/remote/watch.ts:483     ? buildWindowsAgentsCommand({ args })

while the dial one frame down is resolved:

cli/src/lib/session/remote/peer-stream.ts:156     try { target = sshTargetFor(options.device); } catch (error) {
cli/src/lib/session/remote/peer-stream.ts:162       ...SSH_OPTS, ...controlOpts(), ...deviceIdentityArgs(options.device), target, options.command,

3. cli/src/lib/feed/watch.ts:438 — agents feed watch (and the daemon feed hub, cli/src/lib/daemon/feed-stream-service.ts:34: const fleet = new FeedHub({ watch: watchFleetFeed });).

cli/src/lib/feed/watch.ts:438      command: remoteFeedWatchCommand(device.platform),
cli/src/lib/feed/watch.ts:349   return remoteShellFor(os) === 'powershell'
cli/src/lib/feed/watch.ts:350     ? buildWindowsAgentsCommand({ args })

4. cli/src/lib/fleet/apply.ts:360,367,379,387 and :476-477 — agents apply (cli/src/commands/apply.ts:262: const probeList = await pool(desired, 6, async (d) => probeDevice(nameToProfile.get(d.device)!, { withVersions, withSecrets }));).

cli/src/lib/fleet/apply.ts:356     target = sshTargetFor(device);
cli/src/lib/fleet/apply.ts:360   const hint = osHint(device.platform);
cli/src/lib/fleet/apply.ts:361   const extraSshArgs = deviceIdentityArgs(device);
cli/src/lib/fleet/apply.ts:367   const remoteCmd = buildRemoteAgentsInvocation(['teams', 'doctor', '--json'], undefined, hint, remoteEnv(device.platform));
cli/src/lib/fleet/apply.ts:379     const viewCmd = buildRemoteAgentsInvocation(['view', '--json'], undefined, hint, remoteEnv(device.platform));
cli/src/lib/fleet/apply.ts:387     const listCmd = buildRemoteAgentsInvocation(['secrets', 'list', '--json'], undefined, hint, remoteEnv(device.platform));
cli/src/lib/fleet/apply.ts:476   const hint = osHint(device.platform);
cli/src/lib/fleet/apply.ts:477   const env = remoteEnv(device.platform);
cli/src/lib/fleet/apply.ts:333   return platform === 'windows' ? 'windows' : undefined;
cli/src/lib/fleet/apply.ts:338   return platform === 'windows' ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };

Same shape: sshTargetFor / deviceIdentityArgs resolved, osHint / remoteEnv raw.

Either fix these four, or state in the PR description which are out of scope and why.

Blocking: no CHANGELOG entry

AGENTS.md §Code review conventions, "CHANGELOG for user-visible changes." The diff touches no CHANGELOG.md (git diff origin/main...HEAD --name-only lists seven files, none of them the changelog), and the behavior change is user-visible by the PR's own end-to-end table (devices list --refresh: jupiter linux offline → real stats; sessions --active: skipped → jupiter (2)). cli/CHANGELOG.md:3-4 shows this class of bugfix does get an entry (## 1.22.117 / - **agents run --local, and --device no longer SSHes to itself.**).

Non-blocking

5. Gate asymmetry the PR introduces. cli/src/lib/remote-agents-json.ts:215 now gates on the resolved platform ( if (!['windows', 'linux', 'macos'].includes(platform)) continue;), but the peer gate it explicitly mirrors ("mirrors session/remote-list.ts", cli/src/lib/remote-agents-json.ts:210) was left raw:

cli/src/lib/session/remote/remote-list.ts:239   return d.platform === 'windows' || d.platform === 'linux' || d.platform === 'macos';

and remote-list.ts:239 is the gate for the very function the PR fixed one line at a time — cli/src/lib/session/remote/remote-list.ts:549 now reads os: resolveDeviceProfile(d).platform while its isAutomaticSessionPeer gate does not. Same raw gate at cli/src/lib/session/remote/watch.ts:503 and cli/src/lib/feed/watch.ts:432. Not the reported bug (a windows-discovered box passes either way), but a device discovered unknown and configured platform: linux is now included in the fleet-JSON gather and excluded from the sessions fan-out.

6. Test coverage for five of the six fixed sites. AGENTS.md: "a bugfix ships with a test that reproduces the bug." Only cli/src/lib/devices/health.test.ts gained tests; placement-probe.ts, usage-sync.ts, remote-list.ts, remote-agents-json.ts and doctor.ts ship the fix with none.

7. Nit — cli/src/lib/accounting/usage-sync.ts:416 uses a dynamic import inside the per-peer Promise.all body ( const { resolveDeviceProfile } = await import('../devices/resolve-profile.js');) where the other five sites use a static import. There is no cycle to avoid (resolve-profile.ts imports only registry.js and device-config.js, neither of which imports usage-sync.js), so a static import at the top would match the rest of the diff.

What is correct

Mocking rule — PASS. AGENTS.md repo-wide: "Real services only — no mocking." The new block in cli/src/lib/devices/health.test.ts:327-385 uses vi exactly once, for module-cache invalidation, not mocking:

cli/src/lib/devices/health.test.ts:352     vi.resetModules();

It drives the real config read path — temp HOME, a real per-device doc, and a real readDeviceConfigValues fold:

cli/src/lib/devices/health.test.ts:369     const dir = path.join(TMP, '.agents', 'devices', 'jupiter');
cli/src/lib/devices/health.test.ts:371     fs.writeFileSync(path.join(dir, 'agents.yaml'), 'config:\n  platform: linux\n  sshUser: caleb\n');
cli/src/lib/devices/health.test.ts:382     expect(inv.args).toContain('caleb@jupiter.example.ts.net');

That last assertion is what makes it a real-path test rather than ceremony: caleb can only reach the argv by way of readDeviceConfigValues → resolveDeviceProfile (cli/src/lib/devices/resolve-profile.ts:32: const user = (config.sshUser as string | undefined) ?? device.user;) → sshTargetFor. Confirmed, not flagged.

Budget bug fixed too. cli/src/lib/devices/health.ts:53 ( if (device.shell === 'powershell') return WIN_PROBE_TIMEOUT_MS;) is now only ever reached with a resolved profile — cli/src/lib/devices/health.ts:317 ( return { args, env, isWin, budgetMs: probeBudgetMs(resolved) };) is its sole non-test caller.

Verification

node ./node_modules/typescript/bin/tsc --noEmit -p . (in cli/):

TSC_EXIT=0

(no diagnostics; 0 lines of output)

TZ=UTC node ./node_modules/vitest/vitest.mjs run src/lib/devices/health.test.ts src/lib/devices/resolve-profile.test.ts src/lib/teams/placement-probe.test.ts src/lib/accounting/usage-sync.test.ts src/lib/session/remote/remote-list.test.ts src/lib/remote-agents-json.test.ts src/commands/doctor.test.ts:

 RUN  v4.1.9 /Users/taylorgagne/dev/agi-cli/.agents/worktrees/fleet-probe-resolved-profile/cli

 Test Files  7 passed (7)
      Tests  158 passed (158)
   Start at  01:56:02
   Duration  20.26s (transform 28.21s, setup 780ms, import 30.65s, tests 17.85s, environment 0ms)

Both green. The verdict is not about the gates — it is items 1-4 (incomplete sweep) and the missing CHANGELOG entry.

…/feed watch, apply, and the peer gates (review round)

Review found four more sites of the same class: the doctor --check fan-out
(platform for the PowerShell branch while fleetDialTarget resolved), the
sessions-watch and feed-watch peer streams (remoteWatchCommand /
remoteFeedWatchCommand from the discovered platform while streamFromPeer
resolved), and fleet apply's probe + reconcile (osHint / remoteEnv from the
discovered platform while sshTargetFor / deviceIdentityArgs resolved). All
now resolve once at the top. The automatic-peer gates in remote-list and
both watchers filter on the resolved platform too, so the gate and the
shell decision agree.

usage-sync now imports the resolver statically like every other site.
Adds the .changelog/next fragment. The remote-list peer-gate fixtures gain
the auth block a DeviceProfile always carries, which the resolver reads.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@taylorg009

Copy link
Copy Markdown
Collaborator Author

Review verdict: APPROVE

Re-review of 3bc37d10bed811e47a08f53f6a45522c74ca77e5 against origin/main...HEAD. Every blocking item from my previous comment is fixed and verified. Two non-blocking follow-ups below; neither holds the merge.

Blocking 1 (incomplete sweep) — RESOLVED, all four sites

cli/src/commands/doctor.ts:1438 — was platform: t.device.platform,:

cli/src/commands/doctor.ts:1438      platform: resolveDeviceProfile(t.device).platform,

Now matches cli/src/commands/doctor.ts:1440 dialTarget: fleetDialTarget(t.device), and the sibling fan-outs at cli/src/commands/ssh.ts:798,1017,1120. The raw record it used to read comes from cli/src/lib/devices/fleet.ts:61 ( const device = reg[name];), so this was the real gap.

cli/src/lib/session/remote/watch.ts:503 — resolved at the map, before both the gate and the command:

cli/src/lib/session/remote/watch.ts:503   const peers = Object.values(devices).map(resolveDeviceProfile).filter((device) =>
cli/src/lib/session/remote/watch.ts:506     && ['windows', 'linux', 'macos'].includes(device.platform),
cli/src/lib/session/remote/watch.ts:514       command: remoteWatchCommand(device.platform),

Resolving at the map rather than at each read is the right shape here: the same device object then flows into streamFromPeer (cli/src/lib/session/remote/watch.ts:512 device,), so the command dialect and the dial at cli/src/lib/session/remote/peer-stream.ts:156,162 are now guaranteed to agree by construction, not by two parallel calls staying in sync.

cli/src/lib/feed/watch.ts:435,441 — same shape:

cli/src/lib/feed/watch.ts:435   const peers = Object.values(devices).map(resolveDeviceProfile).filter((device) => isDialableDevice(device) && normalizeHost(device.name) !== self && ['windows', 'linux', 'macos'].includes(device.platform));
cli/src/lib/feed/watch.ts:441       command: remoteFeedWatchCommand(device.platform),

cli/src/lib/fleet/apply.ts — resolved once at each entry point, so osHint / remoteEnv / the reported platform all read the effective profile:

cli/src/lib/fleet/apply.ts:354 export function probeDevice(rawDevice: DeviceProfile, opts?: ProbeOptions): DeviceProbe {
cli/src/lib/fleet/apply.ts:357   const device = resolveDeviceProfile(rawDevice);
cli/src/lib/fleet/apply.ts:364   const hint = osHint(device.platform);
cli/src/lib/fleet/apply.ts:469 async function reconcileDevice(row: DeviceDiff, rawDevice: DeviceProfile, ctx: ExecContext): Promise<DeviceApplyResult> {
cli/src/lib/fleet/apply.ts:470   const device = resolveDeviceProfile(rawDevice);
cli/src/lib/fleet/apply.ts:481   const hint = osHint(device.platform);
cli/src/lib/fleet/apply.ts:482   const env = remoteEnv(device.platform);

Shadowing the parameter as rawDevice and rebinding device is the right call — it also picks up cli/src/lib/fleet/apply.ts:398 ( platform: device.platform,, the reported row) and :541 ( const backend: RemoteBackend = isReservedBundleName(bundle) || device.platform === 'linux' ? 'file' : 'keychain';) without a second resolve call to keep in sync.

Blocking 2 (CHANGELOG) — RESOLVED, and via the right mechanism

I pointed at cli/CHANGELOG.md; the repo's actual convention is the queue, which is what the PR used:

cli/scripts/gen-changelog.ts:5 //   .changelog/next/<slug>.md one file per merged-but-unreleased PR (the queue)

cli/.changelog/next/fleet-probe-resolved-platform.md:1-12 is a correct entry in that queue, and cli/scripts/release-changelog.ts:35 confirms the queue is what a release folds in. My previous pointer was wrong; the PR's is right.

Non-blocking gate asymmetry — RESOLVED

cli/src/lib/session/remote/remote-list.ts:239   const platform = resolveDeviceProfile(d).platform;
cli/src/lib/session/remote/remote-list.ts:240   return platform === 'windows' || platform === 'linux' || platform === 'macos';

matching cli/src/lib/remote-agents-json.ts:215, plus the two watcher gates above.

Nit — RESOLVED

cli/src/lib/accounting/usage-sync.ts:36 import { resolveDeviceProfile } from '../devices/resolve-profile.js';
cli/src/lib/accounting/usage-sync.ts:417       const remoteCmd = await buildFleetStateExchangeCommand(resolveDeviceProfile(peer).shell === 'powershell' ? 'windows' : undefined);

The test fixture change is correct, not a mask

cli/src/lib/session/remote/remote-list.test.ts:154,161 add auth: { method: 'key' }, to two partial as DeviceProfile casts. Checked: auth is a required field —

cli/src/lib/devices/registry.ts:97   auth: DeviceAuth;

— and the write path always populates it:

cli/src/lib/devices/registry.ts:342       auth: input.auth ?? prev?.auth ?? { method: 'key' },

So the fixtures were type-dishonest casts that resolveDeviceProfile (cli/src/lib/devices/resolve-profile.ts:31: const method = (config.sshAuth as DeviceAuthMethod | undefined) ?? device.auth.method;) exposed. Adding the field makes them match what the registry actually stores. Confirmed, not flagged.

Re-grep: one remaining raw-shell decision, out of this PR's scope

I re-ran the sweep (device.platform / d.platform / device.shell / peer.shell / remoteShellFor( / buildRemoteAgentsInvocation(, non-test). Every remaining read now traces to a resolved profile:

  • cli/src/lib/devices/health.ts:53 ( if (device.shell === 'powershell') return WIN_PROBE_TIMEOUT_MS;) — sole non-test caller is cli/src/lib/devices/health.ts:317 return { args, env, isWin, budgetMs: probeBudgetMs(resolved) };
  • cli/src/lib/devices/connect.ts:118,128,270,285,334 — reached only from buildSshInvocation, which resolves at cli/src/lib/devices/connect.ts:406 ( device = resolveDeviceProfile(device);), except the one case below
  • every remoteShellFor(os) call resolves its os through cli/src/lib/hosts/providers/devices.ts:51, cli/src/lib/hosts/registry.ts:127,136 ( const resolved = resolveDeviceProfile(device); … os: resolved.platform !== 'unknown' ? resolved.platform : overlay?.os,), cli/src/lib/devices/resolve-target.ts:87 ( os: host.os ?? resolveRemoteOsSync(name),), cli/src/lib/session/remote/remote-list.ts:617, or cli/src/lib/hosts/remote-os.ts:28-29 ( const configured = readDeviceConfigValues(name).platform; / if (typeof configured === 'string' && configured !== 'unknown') return configured;)

The one exception — cli/src/lib/hosts/passthrough.ts:519:

cli/src/lib/hosts/passthrough.ts:499   const targets: FleetTargetWithDevice[] = planned.map((t) => ({
cli/src/lib/hosts/passthrough.ts:501     device: t.device,
cli/src/lib/hosts/passthrough.ts:519         !isSelf && command === 'browser' ? markFleetRemote(cmd, target.device) : cmd;

planned is planFleetTargets (raw — cli/src/lib/devices/fleet.ts:61,70), and markFleetRemote picks the dialect from the raw shell:

cli/src/lib/devices/connect.ts:270   if (device.shell === 'powershell') {
cli/src/lib/devices/connect.ts:271     return [
cli/src/lib/devices/connect.ts:272       `$env:AGENTS_FLEET_REMOTE='1';`,

while the dial one line later re-resolves — cli/src/lib/hosts/passthrough.ts:520 const res = isSelf ? localRunner(cmd) : runner(target.device, remoteCmd); → cli/src/lib/devices/fleet.ts:139 const { args, env } = buildSshInvocation(device, cmd, shim); → connect.ts:406. For a Windows-discovered / platform: linux box, agents browser --devices all hands bash a $env:… prelude, and the re-mark guard cannot recover it because it tests the POSIX form against the resolved profile: cli/src/lib/devices/connect.ts:285 return cmd[0] === 'env' && cmd[1] === 'AGENTS_FLEET_REMOTE=1';.

Not blocking, deliberately: this is a different mechanism (the browser consent/provenance marker's dialect, not the probe/fan-out shell family this PR scopes), it is untouched by the diff, and it is pre-existing on main. It deserves its own ticket rather than a third round on a fix that is correct and verified for its stated scope.

It does, however, make one sentence in the queued note literally untrue:

cli/.changelog/next/fleet-probe-resolved-platform.md:7   worked. Every remote-shell decision now resolves the operator profile first, the same profile
cli/.changelog/next/fleet-probe-resolved-platform.md:8   the dial already used. Source: `cli/src/lib/devices/health.ts` (`buildProbeInvocation`),

Narrowing "Every remote-shell decision" to the fleet probe and fan-out paths the note already enumerates would make it accurate. A one-line edit, not a re-review.

Verification

node ./node_modules/typescript/bin/tsc --noEmit -p . (in cli/):

TSC_EXIT=0

(no diagnostics; 0 lines of output)

TZ=UTC node ./node_modules/vitest/vitest.mjs run src/commands/doctor.test.ts src/lib/session/remote/remote-list.test.ts src/lib/session/remote/watch.test.ts src/lib/feed/watch.test.ts src/lib/fleet/apply.test.ts src/lib/accounting/usage-sync.test.ts src/lib/devices/health.test.ts src/lib/devices/resolve-profile.test.ts src/lib/teams/placement-probe.test.ts src/lib/remote-agents-json.test.ts:

 RUN  v4.1.9 /Users/taylorgagne/dev/agi-cli/.agents/worktrees/fleet-probe-resolved-profile/cli

 Test Files  10 passed (10)
      Tests  230 passed (230)
   Start at  02:04:20
   Duration  8.27s (transform 11.76s, setup 327ms, import 17.02s, tests 8.53s, environment 1ms)

Both green (up from 7 files / 158 tests last round). The mocking rule still passes: cli/src/lib/devices/health.test.ts:352 vi.resetModules(); is the only vi use in the new block, and cli/src/lib/devices/health.test.ts:382 expect(inv.args).toContain('caleb@jupiter.example.ts.net'); proves the real config read path runs. Approved.

…view nit)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants