@phnx-labs/agents-cli 1.22.24 → 1.22.26

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.
Files changed (143) hide show
  1. package/CHANGELOG.md +350 -1
  2. package/README.md +31 -6
  3. package/dist/bin/agents +0 -0
  4. package/dist/browser.js +14 -4
  5. package/dist/commands/apply.js +52 -8
  6. package/dist/commands/browser.js +35 -0
  7. package/dist/commands/cloud.js +9 -5
  8. package/dist/commands/doctor.d.ts +24 -0
  9. package/dist/commands/doctor.js +108 -9
  10. package/dist/commands/exec.js +19 -16
  11. package/dist/commands/feed.d.ts +5 -0
  12. package/dist/commands/feed.js +21 -1
  13. package/dist/commands/focus.js +2 -2
  14. package/dist/commands/insights.d.ts +25 -19
  15. package/dist/commands/insights.js +107 -33
  16. package/dist/commands/menubar.js +8 -0
  17. package/dist/commands/reconnect.d.ts +46 -0
  18. package/dist/commands/reconnect.js +109 -0
  19. package/dist/commands/routines.d.ts +3 -0
  20. package/dist/commands/routines.js +72 -49
  21. package/dist/commands/run-cloud.js +1 -1
  22. package/dist/commands/secrets.d.ts +2 -8
  23. package/dist/commands/secrets.js +29 -105
  24. package/dist/commands/sessions-browser.d.ts +1 -1
  25. package/dist/commands/sessions-browser.js +27 -7
  26. package/dist/commands/sessions-resume.js +3 -2
  27. package/dist/commands/sessions.d.ts +13 -1
  28. package/dist/commands/sessions.js +28 -2
  29. package/dist/commands/setup-secrets.d.ts +1 -0
  30. package/dist/commands/setup-secrets.js +1 -1
  31. package/dist/commands/setup-watchdog.js +5 -10
  32. package/dist/commands/setup.d.ts +26 -3
  33. package/dist/commands/setup.js +106 -47
  34. package/dist/commands/teams.d.ts +6 -0
  35. package/dist/commands/teams.js +46 -2
  36. package/dist/commands/trends.d.ts +8 -0
  37. package/dist/commands/trends.js +10 -156
  38. package/dist/commands/watchdog.d.ts +3 -4
  39. package/dist/commands/watchdog.js +26 -66
  40. package/dist/index.js +37 -2
  41. package/dist/lib/agents.d.ts +11 -0
  42. package/dist/lib/agents.js +155 -23
  43. package/dist/lib/analytics/dashboard.d.ts +10 -6
  44. package/dist/lib/analytics/dashboard.js +6 -4
  45. package/dist/lib/analytics/mix-commands.d.ts +53 -0
  46. package/dist/lib/analytics/mix-commands.js +229 -0
  47. package/dist/lib/analytics/recipes.d.ts +19 -14
  48. package/dist/lib/analytics/recipes.js +4 -2
  49. package/dist/lib/browser/ipc.d.ts +26 -0
  50. package/dist/lib/browser/ipc.js +139 -24
  51. package/dist/lib/browser/profiles.d.ts +11 -0
  52. package/dist/lib/browser/profiles.js +1 -1
  53. package/dist/lib/browser/stream.d.ts +14 -0
  54. package/dist/lib/browser/stream.js +71 -0
  55. package/dist/lib/channels/owner-sink.d.ts +27 -0
  56. package/dist/lib/channels/owner-sink.js +93 -0
  57. package/dist/lib/cloud/cursor.d.ts +79 -0
  58. package/dist/lib/cloud/cursor.js +228 -0
  59. package/dist/lib/cloud/registry.js +2 -0
  60. package/dist/lib/cloud/types.d.ts +7 -2
  61. package/dist/lib/cloud/types.js +14 -0
  62. package/dist/lib/crabbox/cli.d.ts +2 -2
  63. package/dist/lib/crabbox/config.d.ts +7 -8
  64. package/dist/lib/crabbox/config.js +14 -14
  65. package/dist/lib/crabbox/lease.d.ts +11 -4
  66. package/dist/lib/crabbox/lease.js +40 -8
  67. package/dist/lib/crabbox/setup-copy.d.ts +5 -0
  68. package/dist/lib/crabbox/setup-copy.js +17 -1
  69. package/dist/lib/daemon.js +27 -1
  70. package/dist/lib/device-config.js +7 -0
  71. package/dist/lib/devices/doctor-findings.d.ts +13 -1
  72. package/dist/lib/devices/doctor-findings.js +73 -2
  73. package/dist/lib/events.d.ts +9 -0
  74. package/dist/lib/events.js +58 -0
  75. package/dist/lib/exec.d.ts +3 -3
  76. package/dist/lib/exec.js +24 -10
  77. package/dist/lib/feed-outcome.d.ts +3 -0
  78. package/dist/lib/feed-outcome.js +18 -10
  79. package/dist/lib/feed.d.ts +4 -0
  80. package/dist/lib/fleet/apply.d.ts +59 -3
  81. package/dist/lib/fleet/apply.js +183 -6
  82. package/dist/lib/fleet/types.d.ts +21 -2
  83. package/dist/lib/hooks/cache.js +15 -0
  84. package/dist/lib/hosts/passthrough.d.ts +44 -0
  85. package/dist/lib/hosts/passthrough.js +84 -12
  86. package/dist/lib/hosts/ready.d.ts +2 -0
  87. package/dist/lib/hosts/ready.js +10 -1
  88. package/dist/lib/hosts/reconnect.d.ts +14 -12
  89. package/dist/lib/hosts/reconnect.js +41 -40
  90. package/dist/lib/mcp.js +5 -1
  91. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  92. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  93. package/dist/lib/menubar/snapshot.d.ts +15 -0
  94. package/dist/lib/menubar/snapshot.js +40 -0
  95. package/dist/lib/plugins.js +13 -1
  96. package/dist/lib/resources/mcp.js +3 -0
  97. package/dist/lib/routine-process-cleanup.d.ts +9 -0
  98. package/dist/lib/routine-process-cleanup.js +73 -0
  99. package/dist/lib/routines.js +14 -2
  100. package/dist/lib/runner.d.ts +0 -3
  101. package/dist/lib/runner.js +6 -19
  102. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  103. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  104. package/dist/lib/secrets/bundles.js +1 -20
  105. package/dist/lib/secrets/filestore.d.ts +2 -0
  106. package/dist/lib/secrets/filestore.js +13 -0
  107. package/dist/lib/secrets/push.d.ts +94 -0
  108. package/dist/lib/secrets/push.js +145 -0
  109. package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
  110. package/dist/lib/secrets/rc-hygiene.js +14 -1
  111. package/dist/lib/secrets/reaper.d.ts +15 -1
  112. package/dist/lib/secrets/reaper.js +30 -3
  113. package/dist/lib/session/active.d.ts +4 -0
  114. package/dist/lib/session/db.d.ts +21 -3
  115. package/dist/lib/session/db.js +229 -15
  116. package/dist/lib/session/discover.d.ts +1 -0
  117. package/dist/lib/session/discover.js +115 -19
  118. package/dist/lib/session/insights.d.ts +18 -0
  119. package/dist/lib/session/insights.js +143 -1
  120. package/dist/lib/session/remote-list.d.ts +2 -0
  121. package/dist/lib/session/remote-list.js +1 -0
  122. package/dist/lib/session/session-cache.d.ts +4 -4
  123. package/dist/lib/session/session-cache.js +4 -4
  124. package/dist/lib/session/tool-index.js +133 -22
  125. package/dist/lib/session/tool-store.d.ts +26 -2
  126. package/dist/lib/session/tool-store.js +36 -17
  127. package/dist/lib/shims.js +21 -1
  128. package/dist/lib/signin-badge.js +2 -0
  129. package/dist/lib/ssh-exec.js +8 -2
  130. package/dist/lib/startup/command-registry.d.ts +16 -0
  131. package/dist/lib/startup/command-registry.js +46 -0
  132. package/dist/lib/teams/agents.d.ts +13 -0
  133. package/dist/lib/teams/agents.js +76 -8
  134. package/dist/lib/teams/parsers.d.ts +1 -1
  135. package/dist/lib/teams/placement-probe.d.ts +21 -0
  136. package/dist/lib/teams/placement-probe.js +135 -0
  137. package/dist/lib/teams/scheduler.d.ts +74 -1
  138. package/dist/lib/teams/scheduler.js +187 -10
  139. package/dist/lib/types.d.ts +1 -1
  140. package/dist/lib/versions.js +16 -1
  141. package/dist/lib/watchdog/service.d.ts +17 -0
  142. package/dist/lib/watchdog/service.js +39 -0
  143. package/package.json +1 -1
@@ -15,6 +15,7 @@ import chalk from 'chalk';
15
15
  import { setHelpSections } from '../lib/help.js';
16
16
  import { machineId } from '../lib/session/sync/config.js';
17
17
  import { loadDevices, isControlDevice } from '../lib/devices/registry.js';
18
+ import { isHostPinned, managedKnownHostsPath } from '../lib/devices/known-hosts.js';
18
19
  import { ensureDevicesRegistered } from '../lib/devices/sync.js';
19
20
  import { readFleetFile, resolveDesired } from '../lib/fleet/manifest.js';
20
21
  import { snapshotAuth, materializeAuth, parseAuthBundle, KEYCHAIN_BOUND_ON_MAC, isCredentialSafeToPropagate } from '../lib/fleet/auth-sync.js';
@@ -82,7 +83,10 @@ function renderPlan(plan) {
82
83
  }
83
84
  return chalk.cyan('↑ ' + acts.map((a) => a.agent ?? a.kind.replace('-cli', '')).join(','));
84
85
  };
85
- const header = ` ${'device'.padEnd(nameWidth)} ${'agents-cli'.padEnd(12)}${'agents'.padEnd(20)}${'config'.padEnd(10)}login`;
86
+ // Only show the secrets column when the manifest declares any — an all-`-`
87
+ // column on every fleet that uses no bundles is noise.
88
+ const anySecrets = rows.some((r) => r.actions.some((a) => a.kind === 'push-secret' || a.kind === 'needs-secret'));
89
+ const header = ` ${'device'.padEnd(nameWidth)} ${'agents-cli'.padEnd(12)}${'agents'.padEnd(20)}${'config'.padEnd(10)}${anySecrets ? 'login'.padEnd(18) + 'secrets' : 'login'}`;
86
90
  console.log(chalk.gray(header));
87
91
  for (const row of rows) {
88
92
  const cli = row.probe.reachable
@@ -100,10 +104,33 @@ function renderPlan(plan) {
100
104
  ? (row.actions.some((a) => a.kind === 'sync-config') ? chalk.cyan('↑ sync') : chalk.green('ok'))
101
105
  : chalk.gray('-');
102
106
  const loginCell = cell(row, ['push-login', 'needs-login'], `${row.desired.agents.length}/${row.desired.agents.length}`);
103
- console.log(` ${row.device.padEnd(nameWidth)} ${stripPad(cli, 12)}${stripPad(agentsCell, 20)}${stripPad(configCell, 10)}${loginCell}`);
107
+ const secretsCell = (() => {
108
+ if (!anySecrets)
109
+ return '';
110
+ if (!row.probe.reachable)
111
+ return chalk.gray('- offline');
112
+ const push = row.actions.filter((a) => a.kind === 'push-secret').length;
113
+ const blocked = row.actions.filter((a) => a.kind === 'needs-secret').length;
114
+ if (push === 0 && blocked === 0)
115
+ return chalk.green('ok');
116
+ if (push > 0 && blocked === 0)
117
+ return chalk.cyan(`↑ push ${push}`);
118
+ if (push === 0)
119
+ return chalk.yellow(`blocked ${blocked}`);
120
+ return chalk.yellow(`↑ push ${push} · blocked ${blocked}`);
121
+ })();
122
+ const loginPart = anySecrets ? stripPad(loginCell, 18) + secretsCell : loginCell;
123
+ console.log(` ${row.device.padEnd(nameWidth)} ${stripPad(cli, 12)}${stripPad(agentsCell, 20)}${stripPad(configCell, 10)}${loginPart}`);
104
124
  }
105
125
  console.log();
106
126
  console.log(chalk.gray(` ${plan.actions.length} action(s) across ${rows.filter((r) => r.probe.reachable).length} reachable device(s)`));
127
+ // The capability is opt-in, so when it is OFF and the manifest declares
128
+ // bundles, say that it exists. Otherwise an operator reads "manual recreate"
129
+ // and concludes there is no supported path — which is exactly the conclusion
130
+ // that led to a master key being hand-exported across the fleet (RUSH-1968).
131
+ if (anySecrets && !rows.some((r) => r.actions.some((a) => a.kind === 'push-secret'))) {
132
+ console.log(chalk.gray(' secrets: not pushed. `--provision-secrets` pushes declared bundles to devices whose host key is pinned.'));
133
+ }
107
134
  // Distinguish *why* a login can't be propagated: macOS keychain-bound,
108
135
  // single-use rotating refresh token (never copied), or the source simply not
109
136
  // being signed in to that agent (no portable file).
@@ -130,14 +157,15 @@ function renderPlan(plan) {
130
157
  if (noToken.length > 0) {
131
158
  console.log(chalk.yellow(` manual login needed (no portable token on source): ${noToken.join(', ')}`));
132
159
  }
133
- // Secrets bundles are declared once for the fleet; surface the distinct set to
134
- // recreate on any device missing them (values are keychain-local, never pushed).
160
+ // Secrets bundles are declared once for the fleet; surface the distinct set the
161
+ // gate did NOT push, so a refusal is never silent. "never pushed" used to be
162
+ // literally true here and no longer is — `--provision-secrets` pushes them.
135
163
  const bundles = [...new Set(rows.flatMap((r) => r.secretsNeeded))];
136
164
  if (bundles.length > 0) {
137
165
  const shown = bundles.slice(0, 12);
138
166
  const more = bundles.length - shown.length;
139
167
  const list = shown.join(', ') + (more > 0 ? `, +${more} more` : '');
140
- console.log(chalk.yellow(` ${bundles.length} secrets bundle(s) to recreate where missing (keychain-local, never pushed): ${list}`));
168
+ console.log(chalk.yellow(` ${bundles.length} secrets bundle(s) not pushed — recreate where missing: ${list}`));
141
169
  }
142
170
  }
143
171
  /** padEnd on the visible width, ignoring chalk color codes. Exported for tests. */
@@ -218,11 +246,24 @@ async function runApply(opts) {
218
246
  // Probe every target device in parallel.
219
247
  const nameToProfile = new Map(desired.map((d) => [d.device, registry[d.device]]));
220
248
  const withVersions = rosterNeedsVersions(desired);
249
+ // One extra `secrets list --json` per device, and only when it can change the
250
+ // plan: the manifest declares bundles AND provisioning is on. Same cost
251
+ // discipline as `withVersions` — a fleet that uses no bundles never pays it.
252
+ const withSecrets = opts.provisionSecrets === true && (manifest.secrets?.bundles?.length ?? 0) > 0;
221
253
  console.log(chalk.gray(`Probing ${desired.length} device(s)…`));
222
- const probeList = await pool(desired, 6, async (d) => probeDevice(nameToProfile.get(d.device), { withVersions }));
254
+ const probeList = await pool(desired, 6, async (d) => probeDevice(nameToProfile.get(d.device), { withVersions, withSecrets }));
223
255
  const probes = new Map(probeList.map((p) => [p.device, p]));
224
256
  const targetCliVersion = localCliVersion();
225
- let plan = diffFleet(desired, probes, { targetCliVersion, sourceAuth, secretsBundles: manifest.secrets?.bundles });
257
+ let plan = diffFleet(desired, probes, {
258
+ targetCliVersion,
259
+ sourceAuth,
260
+ secretsBundles: manifest.secrets?.bundles,
261
+ provisionSecrets: opts.provisionSecrets === true,
262
+ forceSecrets: opts.force === true,
263
+ // Same bar as `exec --copy-creds` (EXEC-34): credential values only ever go
264
+ // to a host whose key we already pinned.
265
+ isHostPinned: (device) => isHostPinned(device, managedKnownHostsPath()),
266
+ });
226
267
  // --only filter.
227
268
  if (opts.only) {
228
269
  const keep = new Set();
@@ -242,7 +283,8 @@ async function runApply(opts) {
242
283
  return;
243
284
  // `needs-login`/`needs-secret` are surfaced manual reminders, not executable
244
285
  // mutations — exclude them so an otherwise-converged fleet still says "nothing
245
- // to do" instead of looping forever on un-actionable surfacing.
286
+ // to do" instead of looping forever on un-actionable surfacing. `push-secret`
287
+ // IS executable and deliberately stays counted.
246
288
  if (plan.actions.filter((a) => a.kind !== 'needs-login' && a.kind !== 'needs-secret').length === 0) {
247
289
  console.log(chalk.green('\nNothing to do — fleet already matches the profile.'));
248
290
  return;
@@ -301,6 +343,8 @@ export function configureApplyCommand(cmd) {
301
343
  .option('--agent <specs...>', 'Override the roster for targeted device(s): install these specs instead of the manifest\'s. Use `claude@all` to replicate every version installed on this machine.')
302
344
  .option('--only <dims>', 'Limit to dimensions: comma list of agents,config,login')
303
345
  .option('--no-login', 'Do not propagate logins')
346
+ .option('--provision-secrets', "Push the manifest's declared secrets bundles to each device (OFF by default; moves credential values over SSH, and only to a device whose host key is already pinned)")
347
+ .option('--force', 'With --provision-secrets: re-push a bundle the device already has')
304
348
  .addOption(new Option('--recv-auth', 'internal: receive an auth bundle on stdin').hideHelp())
305
349
  .action(async (opts) => {
306
350
  try {
@@ -20,6 +20,7 @@ import { isInteractiveTerminal } from './utils.js';
20
20
  import { registerCommandGroups, setHelpSections } from '../lib/help.js';
21
21
  import { buildHar } from '../lib/browser/har.js';
22
22
  import { getCliVersion } from '../lib/version.js';
23
+ import { runBrowserIPCStream } from '../lib/browser/stream.js';
23
24
  /**
24
25
  * Resolve which browser task a command targets. Order:
25
26
  * 1. `--task <name>` flag (explicit per-command override)
@@ -48,6 +49,7 @@ const TASK_OPTION_DESC = 'Task name (defaults to $AGENTS_BROWSER_TASK)';
48
49
  // trailing "Other commands" section automatically.
49
50
  const BROWSER_HELP_GROUPS = [
50
51
  { title: 'Session lifecycle', names: ['start', 'done', 'status'] },
52
+ { title: 'Fast action loop', names: ['stream'] },
51
53
  {
52
54
  title: 'Drive the page',
53
55
  names: ['navigate', 'tabs', 'screenshot', 'evaluate', 'click', 'type', 'press', 'wait'],
@@ -79,6 +81,9 @@ export function registerBrowserCommand(program) {
79
81
  agents browser navigate https://example.com
80
82
  agents browser screenshot
81
83
 
84
+ # Keep one process and daemon socket warm for repeated actions
85
+ agents browser stream --task "$AGENTS_BROWSER_TASK"
86
+
82
87
  # Drive another machine's browser (needs its consent — see remote-control)
83
88
  agents browser start --host zion
84
89
 
@@ -654,6 +659,36 @@ function registerTaskCommands(browser) {
654
659
  ? 'Other fleet machines can now drive this browser via `browser --host <this-device>`.'
655
660
  : 'Cross-machine `browser --host` drives to this machine are refused.');
656
661
  });
662
+ const stream = browser
663
+ .command('stream')
664
+ .description('Keep one process and daemon IPC socket open; read NDJSON requests from stdin and write NDJSON responses')
665
+ .option(TASK_OPTION_FLAG, 'Default task for requests that omit `task` (defaults to $AGENTS_BROWSER_TASK)')
666
+ .action(async (opts) => {
667
+ await runBrowserIPCStream({
668
+ input: process.stdin,
669
+ output: process.stdout,
670
+ task: opts.task ?? process.env.AGENTS_BROWSER_TASK,
671
+ actor: resolveActor().id,
672
+ launchId: process.env.AGENT_LAUNCH_ID,
673
+ });
674
+ });
675
+ setHelpSections(stream, {
676
+ examples: `
677
+ # Batch two warm actions through one process and one daemon connection
678
+ printf '%s\\n' \\
679
+ '{"action":"screenshot","path":"/tmp/page.jpg"}' \\
680
+ '{"action":"click","atX":320,"atY":540}' \\
681
+ | agents browser stream --task "$AGENTS_BROWSER_TASK"
682
+
683
+ # Keep the command open and send one JSON object per line from a long-lived shell
684
+ agents browser stream --task "$AGENTS_BROWSER_TASK"
685
+ `,
686
+ notes: `
687
+ stdout is protocol-only: one compact JSON response for each non-empty input line.
688
+ Malformed JSON returns an error response without closing the stream.
689
+ The first start response becomes the default task for later lines in the same stream.
690
+ `,
691
+ });
657
692
  browser
658
693
  .command('start')
659
694
  .description('Start a browser task. Pass --profile <name>; omit to use your configured default (`agents browser profiles set-default`), else auto-pick an installed Chromium-family browser.')
@@ -25,14 +25,15 @@ export function statusColor(status) {
25
25
  export function registerCloudCommands(program) {
26
26
  const cloud = program
27
27
  .command('cloud', { hidden: true })
28
- .description('Dispatch and manage cloud agent tasks across providers (Rush, Codex, Factory, Antigravity).')
28
+ .description('Dispatch and manage cloud agent tasks across providers (Rush, Codex, Cursor, Factory, Antigravity, Host).')
29
29
  .addHelpText('after', `
30
30
  Each agent runs in its own cloud. Pass --agent and the provider is auto-selected
31
- (claude→rush, codex→codex, droid→factory, antigravity→antigravity); --provider overrides.
31
+ (claude→rush, codex→codex, cursor→cursor, droid→factory, antigravity→antigravity); --provider overrides.
32
32
 
33
33
  Providers:
34
34
  rush Rush Cloud — Claude against a GitHub repo + branch → PR
35
35
  codex Codex Cloud — runs in a pre-built Codex environment (--env)
36
+ cursor Cursor Cloud Agents — v1 REST API, with optional GitHub repos
36
37
  factory Factory Droid Computer — droid exec on a cloud VM (--computer)
37
38
  antigravity Gemini Managed Agents — Antigravity harness in a remote sandbox
38
39
 
@@ -49,6 +50,9 @@ Examples:
49
50
  # Codex Cloud against a saved environment
50
51
  agents cloud run "add pytest fixtures for the new billing module" --provider codex --env env_a1b2c3 --agent codex --timeout 30m
51
52
 
53
+ # Cursor Cloud Agents against a GitHub repository
54
+ agents cloud run "fix the flaky parser test" --agent cursor --repo acme/example
55
+
52
56
  # Factory pod targeting a specific computer (Droid)
53
57
  agents cloud run "QA the new onboarding flow end-to-end" --provider factory --computer linux-vm-1 --agent droid
54
58
 
@@ -74,9 +78,9 @@ Examples:
74
78
  cloud
75
79
  .command('run [prompt]')
76
80
  .description('Dispatch a task to a cloud agent.')
77
- .option('--provider <id>', 'Cloud backend: rush, codex, factory, antigravity, host (overrides agent auto-routing)')
78
- .option('--agent <name>', 'Agent to run: claude, codex, droid, antigravity (auto-routes to its native cloud)')
79
- .option('--repo <owner/repo>', 'GitHub repository. Repeatable for multi-repo dispatch (Rush Cloud only).', (value, previous) => {
81
+ .option('--provider <id>', 'Cloud backend: rush, codex, cursor, factory, antigravity, host (overrides agent auto-routing)')
82
+ .option('--agent <name>', 'Agent to run: claude, codex, cursor, droid, antigravity (auto-routes to its native cloud)')
83
+ .option('--repo <owner/repo>', 'GitHub repository. Repeatable for multi-repo dispatch (Rush or Cursor Cloud).', (value, previous) => {
80
84
  const acc = Array.isArray(previous) ? previous : [];
81
85
  acc.push(value);
82
86
  return acc;
@@ -22,11 +22,35 @@
22
22
  * apply pending sync.
23
23
  */
24
24
  import type { Command } from 'commander';
25
+ import { type DoctorFinding } from '../lib/devices/doctor-findings.js';
25
26
  import { type VersionResourceReport } from '../lib/doctor-diff.js';
26
27
  import { type DuplicateVersionHook } from '../lib/hooks.js';
27
28
  import { type SyncStatusRow, type OrphanRow } from '../lib/drift.js';
28
29
  import { type FetchStatusMarker } from '../lib/auto-pull.js';
29
30
  export declare function wrapLine(prefix: string, text: string, width?: number): string[];
31
+ export declare function asRemoteSecretFindings(raw: unknown, device: string): DoctorFinding[];
32
+ /**
33
+ * Fetch a device's harness inventory by running its top-level `agents doctor
34
+ * --json` (which carries the `fleet` field, RUSH-2027). Separate from
35
+ * {@link probeFleetTarget} — that forwards `teams doctor --json` for per-agent
36
+ * readiness, a different payload. Returns the parsed {@link FleetInventory} or
37
+ * null (unreachable, non-zero exit, unparseable, or an older CLI with no
38
+ * `fleet` field); a null is skipped by the comparator, never a false gap.
39
+ */
40
+ /**
41
+ * How long one remote `doctor --json` probe may take.
42
+ *
43
+ * 180s, matching `ChildProcess.doctorTimeout`, which was set to 180 for this
44
+ * same command for this same reason. The previous 30s sat BELOW the command's
45
+ * real cost — 56s measured on `yosemite-m0`, 136s on an idle box — so every slow
46
+ * device silently contributed nothing at all: no inventory, no sign-in, no
47
+ * divergence, no secret findings. A ceiling under the true cost does not make a
48
+ * probe cheap, it makes it always fail.
49
+ *
50
+ * This does not multiply by device count: `fanOutDevices` is `Promise.all`, so
51
+ * whole-fleet wall time is bounded near the slowest box, not 180s x N.
52
+ */
53
+ export declare const FLEET_INVENTORY_TIMEOUT_MS = 180000;
30
54
  export type IssueSeverity = 'critical' | 'warning' | 'info';
31
55
  /**
32
56
  * One triaged health finding. `severity`/`category`/`subject`/`impact`/`fix` are
@@ -8,7 +8,7 @@ import { enterDoctorOverviewGate, writeDoctorOverviewCache } from '../lib/device
8
8
  import { fleetDialTarget } from '../lib/devices/connect.js';
9
9
  import { compareFleetInventories } from '../lib/devices/fleet-divergence.js';
10
10
  import { collectLocalFleetInventory } from '../lib/devices/fleet-inventory.js';
11
- import { buildLocalFindings, fleetDivergenceToFindings, signInToFindings, renderFindings, } from '../lib/devices/doctor-findings.js';
11
+ import { buildLocalFindings, fleetDivergenceToFindings, signInToFindings, renderFindings, remediationFor, FINDING_SEVERITY, } from '../lib/devices/doctor-findings.js';
12
12
  import { getCliVersion } from '../lib/version.js';
13
13
  import { resolveHost } from '../lib/hosts/registry.js';
14
14
  import { sshExecAsync } from '../lib/ssh-exec.js';
@@ -26,12 +26,14 @@ import { checkVersionHookWiring, inspectDuplicateVersionHooks, registerHooksToSe
26
26
  import { isVersionIsolated } from '../lib/versions.js';
27
27
  import { computeDrift, checkSyncStatus, countOrphans, computeSourceBehind } from '../lib/drift.js';
28
28
  import { readAuthHealthCache, summarizeHostAuth } from '../lib/auth-health.js';
29
+ import { readMeta } from '../lib/state.js';
30
+ import { probeOwnerSink } from '../lib/channels/owner-sink.js';
29
31
  import { unifiedDiff, colorizeUnifiedDiff } from '../lib/diff-text.js';
30
32
  import { listCliStatus, listCliStatusAsync } from '../lib/cli-resources.js';
31
33
  import { setHelpSections } from '../lib/help.js';
32
34
  import { heal, healChangedAnything } from '../lib/heal.js';
33
35
  import { getEffectiveExecutionPolicy } from '../lib/platform/winpath.js';
34
- import { scanUserRcFiles } from '../lib/secrets/rc-hygiene.js';
36
+ import { scanUserRcFiles, masterPassphraseInEnv } from '../lib/secrets/rc-hygiene.js';
35
37
  import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
36
38
  import { readRepoBehindMarkers } from '../lib/auto-pull.js';
37
39
  import * as fs from 'fs';
@@ -80,6 +82,70 @@ function printWrappedLine(prefix, text) {
80
82
  for (const line of wrapLine(prefix, text))
81
83
  console.log(chalk.gray(line));
82
84
  }
85
+ /**
86
+ * Narrow a remote `findings` array to the secret-hygiene rows, or `[]`.
87
+ *
88
+ * Deliberately NOT "forward every remote finding". The aggregator already
89
+ * rebuilds a remote's sign-in rows from its inventory and its divergence rows
90
+ * from the comparator, so forwarding wholesale would double them; and pulling a
91
+ * remote's orphan/drift rows into a fleet readout is a much larger UX change
92
+ * than this fix. The two secret kinds are the ones that are BOTH unrecomputable
93
+ * centrally and security-relevant, which is exactly why they were being lost.
94
+ *
95
+ * **The remote contributes exactly one thing: the KIND.** Severity, message and
96
+ * remediation are all generated HERE. That is not defensiveness for its own
97
+ * sake — the remote runs its own agents-cli, whose version and integrity we do
98
+ * not control, and each forwarded string is load-bearing in a different way:
99
+ *
100
+ * - `remediation` is a command a human copies and runs. Accepting it from the
101
+ * wire is an injection channel, full stop.
102
+ * - `message` is the one place a secret VALUE could re-enter a readout that
103
+ * otherwise guarantees it never prints one. A local builder cannot leak what
104
+ * it never receives.
105
+ * - `severity` decides the CRITICAL section, so a remote could otherwise
106
+ * promote its own warning and bury real criticals under it.
107
+ * - `device` is overwritten with the name we dialled, so no box can pin a
108
+ * finding on another.
109
+ *
110
+ * The cost is detail: the fleet row says which box and which kind, not which
111
+ * file and line. Run `agents doctor` on that box for the specifics — the
112
+ * message says so.
113
+ */
114
+ const REMOTE_FORWARDED_KINDS = ['rc-secret-export', 'env-secret-export'];
115
+ /** Canonical, locally-authored text for a forwarded kind. Never the remote's. */
116
+ const REMOTE_SECRET_MESSAGE = {
117
+ 'rc-secret-export': 'a credential-shaped export was found in this box\'s shell rc files'
118
+ + ' — run `agents doctor` there for the file and line',
119
+ 'env-secret-export': 'AGENTS_SECRETS_PASSPHRASE is set in this box\'s process environment'
120
+ + ' — run `agents doctor` there for detail',
121
+ };
122
+ export function asRemoteSecretFindings(raw, device) {
123
+ if (!Array.isArray(raw))
124
+ return [];
125
+ const seen = new Set();
126
+ const out = [];
127
+ for (const item of raw) {
128
+ if (!item || typeof item !== 'object' || Array.isArray(item))
129
+ continue;
130
+ const kind = item.kind;
131
+ if (typeof kind !== 'string')
132
+ continue;
133
+ if (!REMOTE_FORWARDED_KINDS.includes(kind))
134
+ continue;
135
+ if (seen.has(kind))
136
+ continue; // one row per kind per box
137
+ seen.add(kind);
138
+ const k = kind;
139
+ const base = {
140
+ severity: FINDING_SEVERITY[k],
141
+ kind: k,
142
+ device,
143
+ message: REMOTE_SECRET_MESSAGE[k],
144
+ };
145
+ out.push({ ...base, remediation: remediationFor({ ...base, remediation: '' }) });
146
+ }
147
+ return out;
148
+ }
83
149
  async function resolveFleetTargets(opts) {
84
150
  const singleName = opts.host || opts.device;
85
151
  if (singleName) {
@@ -155,15 +221,32 @@ async function probeFleetTarget(target) {
155
221
  * null (unreachable, non-zero exit, unparseable, or an older CLI with no
156
222
  * `fleet` field); a null is skipped by the comparator, never a false gap.
157
223
  */
224
+ /**
225
+ * How long one remote `doctor --json` probe may take.
226
+ *
227
+ * 180s, matching `ChildProcess.doctorTimeout`, which was set to 180 for this
228
+ * same command for this same reason. The previous 30s sat BELOW the command's
229
+ * real cost — 56s measured on `yosemite-m0`, 136s on an idle box — so every slow
230
+ * device silently contributed nothing at all: no inventory, no sign-in, no
231
+ * divergence, no secret findings. A ceiling under the true cost does not make a
232
+ * probe cheap, it makes it always fail.
233
+ *
234
+ * This does not multiply by device count: `fanOutDevices` is `Promise.all`, so
235
+ * whole-fleet wall time is bounded near the slowest box, not 180s x N.
236
+ */
237
+ export const FLEET_INVENTORY_TIMEOUT_MS = 180_000;
158
238
  async function probeFleetInventory(target) {
159
239
  const isWin = /^win/i.test((target.os ?? '').trim());
160
240
  const remoteCmd = buildRemoteAgentsInvocation(['doctor', '--json'], undefined, isWin ? 'windows' : undefined, isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' });
161
- const res = await sshExecAsync(target.sshTarget, remoteCmd, { timeoutMs: 30000, multiplex: true });
241
+ const res = await sshExecAsync(target.sshTarget, remoteCmd, { timeoutMs: FLEET_INVENTORY_TIMEOUT_MS, multiplex: true });
162
242
  if (res.code !== 0)
163
243
  return null;
164
244
  try {
165
245
  const parsed = JSON.parse(res.stdout);
166
- return asFleetInventory(parsed.fleet);
246
+ return {
247
+ inventory: asFleetInventory(parsed.fleet),
248
+ secretFindings: asRemoteSecretFindings(parsed.findings, target.name),
249
+ };
167
250
  }
168
251
  catch {
169
252
  return null;
@@ -252,19 +335,22 @@ async function runDevicesDoctor(opts) {
252
335
  fanOutDevices(targets, probeFleetTarget),
253
336
  fanOutDevices(targets, probeFleetInventory),
254
337
  ]);
255
- const inventoryByName = new Map();
338
+ const payloadByName = new Map();
256
339
  for (const r of inventoryResults)
257
- inventoryByName.set(r.name, r.status === 'ok' ? (r.value ?? null) : null);
340
+ payloadByName.set(r.name, r.status === 'ok' ? (r.value ?? null) : null);
258
341
  results.push(...remoteResults.map((r) => {
259
- const inventory = inventoryByName.get(r.name) ?? undefined;
342
+ const payload = payloadByName.get(r.name) ?? null;
343
+ const inventory = payload?.inventory ?? undefined;
344
+ const secretFindings = payload?.secretFindings;
260
345
  if (r.status === 'ok' && r.value)
261
- return { ...r.value, inventory: inventory ?? undefined };
346
+ return { ...r.value, inventory, secretFindings };
262
347
  return {
263
348
  name: r.name,
264
349
  online: false,
265
350
  error: r.error ?? String(r.reason ?? 'skipped'),
266
351
  agents: {},
267
- inventory: inventory ?? undefined,
352
+ inventory,
353
+ secretFindings,
268
354
  };
269
355
  }));
270
356
  // Cross-device divergence: compare every device's inventory against the local
@@ -314,12 +400,16 @@ async function runDevicesDoctor(opts) {
314
400
  duplicateHooks: inspectDuplicateVersionHooks(cwd),
315
401
  hostClis: toHostCliInput(listCliStatus(cwd)),
316
402
  rcSecrets: scanUserRcFiles(),
403
+ masterPassphraseInEnv: masterPassphraseInEnv(),
317
404
  execPolicy: process.platform === 'win32'
318
405
  ? { platform: process.platform, policy: getEffectiveExecutionPolicy() }
319
406
  : undefined,
320
407
  isolatedVersions: localReports
321
408
  .filter((rep) => isVersionIsolated(rep.agent, rep.version))
322
409
  .map((rep) => `${rep.agent}@${rep.version}`),
410
+ // Can the owner-delivery lane escalate a block from THIS box? Local only —
411
+ // remote boxes self-report it in their own `agents doctor --json`.
412
+ ownerSink: await probeOwnerSink(readMeta()),
323
413
  }));
324
414
  accounts[localName] = r.inventory?.signIn ?? {};
325
415
  continue;
@@ -335,6 +425,11 @@ async function runDevicesDoctor(opts) {
335
425
  });
336
426
  continue;
337
427
  }
428
+ // Secret hygiene the aggregator cannot see for itself — the remote's shell
429
+ // rc files and its process environment. Without this the fleet readout
430
+ // reports a leaking box as clean (RUSH-1968).
431
+ if (r.secretFindings?.length)
432
+ findings.push(...r.secretFindings);
338
433
  if (r.inventory?.signIn) {
339
434
  findings.push(...signInToFindings(r.name, r.inventory.signIn));
340
435
  accounts[r.name] = r.inventory.signIn;
@@ -1389,12 +1484,16 @@ export function registerDoctorCommand(program) {
1389
1484
  duplicateHooks,
1390
1485
  hostClis: toHostCliInput(hostClis),
1391
1486
  rcSecrets: scanUserRcFiles(),
1487
+ masterPassphraseInEnv: masterPassphraseInEnv(),
1392
1488
  // getEffectiveExecutionPolicy spawns powershell — a doomed process on
1393
1489
  // POSIX, where the advisory never applies. Probe only on Windows.
1394
1490
  execPolicy: process.platform === 'win32'
1395
1491
  ? { platform: process.platform, policy: getEffectiveExecutionPolicy() }
1396
1492
  : undefined,
1397
1493
  isolatedVersions,
1494
+ // Can the owner-delivery lane (feed/notify) escalate a block from this
1495
+ // box? A factory that cannot escalate is not healthy (RUSH-2262).
1496
+ ownerSink: await probeOwnerSink(readMeta()),
1398
1497
  });
1399
1498
  if (opts.json) {
1400
1499
  const overviewPayload = {
@@ -568,8 +568,8 @@ export function registerRunCommand(program) {
568
568
  .option('--bare', 'With --lease, skip copying your local ~/.agents setup (skills/hooks/commands/MCP) onto the box.')
569
569
  .option('--tailscale', 'Lease the box onto your tailnet (reachable only over Tailscale) rather than a public IP.')
570
570
  .option('--no-tailscale', 'Force a public-IP lease even when a reuse context would default to Tailscale.')
571
- .option('--cloud', 'Vendor cloud placement: dispatch to the agent\'s native cloud (claude→rush, codex→codex, droid→factory, antigravity→antigravity) and stream the result. Same dispatch as `agents cloud run --agent <agent>`; tracked by `agents cloud list/status/logs`. Same as --where cloud. Mutually exclusive with --host/--lease and local-run flags.')
572
- .option('--provider <id>', 'With --cloud: override the agent\'s native cloud provider (rush | codex | factory | antigravity | host).')
571
+ .option('--cloud', 'Vendor cloud placement: dispatch to the agent\'s native cloud (claude→rush, codex→codex, cursor→cursor, droid→factory, antigravity→antigravity) and stream the result. Same dispatch as `agents cloud run --agent <agent>`; tracked by `agents cloud list/status/logs`. Same as --where cloud. Mutually exclusive with --host/--lease and local-run flags.')
572
+ .option('--provider <id>', 'With --cloud: override the agent\'s native cloud provider (rush | codex | cursor | factory | antigravity | host).')
573
573
  .option('--repo <owner/repo>', 'With --cloud: GitHub repository. Repeatable for multi-repo dispatch (Rush Cloud only).', (val, prev) => [...prev, val], [])
574
574
  .option('--branch <name>', 'With --cloud: target git branch.')
575
575
  .option('--cloud-env <id>', 'With --cloud: Codex Cloud environment ID (run\'s --env is the KEY=VAL passthrough, so the cloud env id gets its own flag).');
@@ -657,8 +657,8 @@ export function registerRunCommand(program) {
657
657
  Models (cheap|default|best|ultra): agents models <agent[@version]>
658
658
 
659
659
  Headless plan support (a prompt makes the run headless):
660
- plan works headless on claude, codex, droid, opencode.
661
- kimi, grok, cursor, antigravity have no headless plan mode — a headless
660
+ plan works headless on claude, codex, cursor, droid, opencode.
661
+ kimi, grok, antigravity have no headless plan mode — a headless
662
662
  --mode plan auto-downgrades to --mode auto (with a stderr warning).
663
663
  Interactive plan (omit the prompt) works everywhere it is listed.
664
664
 
@@ -1019,19 +1019,21 @@ export function registerRunCommand(program) {
1019
1019
  const leaseSecretsBundle = process.env.AGENTS_LEASE_SECRETS_BUNDLE;
1020
1020
  const nowSecs = Math.floor(Date.now() / 1000);
1021
1021
  let reuseSlug = options.box;
1022
- // The profile this run's pool/box carries: the repo's .crabbox.yaml
1023
- // `profile:` when declared (what crabbox warmup would label a fresh box
1024
- // with), else crabbox's default. Passing it to leaseAndRun makes the
1025
- // pool-reuse match interchangeable with a fresh warmup.
1022
+ // Lease runs share one default warm pool across repositories. The generic
1023
+ // `.crabbox.yaml profile:` remains available to repo sandbox/CI scripts;
1024
+ // `leaseProfile:` is the explicit opt-in for a dedicated lease hot box.
1026
1025
  const repoRoot = gitToplevel(leaseCwd);
1027
- const { readCrabboxRepoProfile } = await import('../lib/crabbox/config.js');
1028
- const poolProfile = repoRoot ? readCrabboxRepoProfile(repoRoot) : undefined;
1026
+ const { readCrabboxLeaseProfile } = await import('../lib/crabbox/config.js');
1027
+ const poolProfile = repoRoot ? readCrabboxLeaseProfile(repoRoot) : 'default';
1029
1028
  if (options.lease && !reuseSlug && !options.fresh) {
1030
- const { crabboxList } = await import('../lib/crabbox/cli.js');
1031
- const { reusableBoxes, formatBoxRow } = await import('./lease.js');
1029
+ const { crabboxList, poolReusableBoxes } = await import('../lib/crabbox/cli.js');
1030
+ const { formatBoxRow } = await import('./lease.js');
1032
1031
  let warm = [];
1033
1032
  try {
1034
- warm = reusableBoxes(crabboxList({ secretsBundle: leaseSecretsBundle }), nowSecs);
1033
+ warm = poolReusableBoxes(crabboxList({ secretsBundle: leaseSecretsBundle }), {
1034
+ profile: poolProfile,
1035
+ nowSecs,
1036
+ });
1035
1037
  }
1036
1038
  catch {
1037
1039
  warm = []; // crabbox unavailable / no creds → the pool check in leaseAndRun decides
@@ -1102,7 +1104,7 @@ export function registerRunCommand(program) {
1102
1104
  }
1103
1105
  }
1104
1106
  const { detectSignedInRuntimes, resolveClaudeCredentialsBlob, inferLeaseRuntime, profileNeedsBaseRuntimeCredentials } = await import('../lib/crabbox/runtimes.js');
1105
- const { leaseAndRun } = await import('../lib/crabbox/lease.js');
1107
+ const { leaseAndRun, leaseWorkspaceId } = await import('../lib/crabbox/lease.js');
1106
1108
  const { boxAddress } = await import('./lease.js');
1107
1109
  const { getConfiguredRunStrategy, resolveRunVersion } = await import('../lib/rotate.js');
1108
1110
  const { profileExists, readProfile, resolveProfileEnv } = await import('../lib/profiles.js');
@@ -1197,7 +1199,7 @@ export function registerRunCommand(program) {
1197
1199
  ? 'the box is kept after the run'
1198
1200
  : options.fresh
1199
1201
  ? 'the box is destroyed after the run'
1200
- : 'a fresh box is destroyed after the run; a reused pool box is kept';
1202
+ : 'the shared-pool box is kept after the run';
1201
1203
  console.error(chalk.gray(`${boxLifecycle} · shipping ${whatShips}${credentialRuntimes.length > 0 && leaseEmail ? ` (${leaseEmail})` : ''}; ${boxAfterRun}.`));
1202
1204
  // Read the Claude OAuth token from the local Keychain (silent) so it can be
1203
1205
  // written to ~/.claude/.credentials.json on the box — otherwise Claude boots
@@ -1279,6 +1281,7 @@ export function registerRunCommand(program) {
1279
1281
  reuseBox: reuseSlug,
1280
1282
  fresh: options.fresh,
1281
1283
  profile: poolProfile,
1284
+ workspaceId: leaseWorkspaceId(repoRoot ?? leaseCwd),
1282
1285
  copySetup,
1283
1286
  netMode,
1284
1287
  onData: (chunk) => router.push(chunk),
@@ -2455,7 +2458,7 @@ export function registerRunCommand(program) {
2455
2458
  process.exit(1);
2456
2459
  }
2457
2460
  // Default CLI mode is the generic 'plan'. Agents without a read-only
2458
- // mode (antigravity, cursor, kiro, …) degrade via resolveMode to their
2461
+ // mode (antigravity, kiro, …) degrade via resolveMode to their
2459
2462
  // safest native mode (modes[0], typically edit). That covers both the
2460
2463
  // implicit default and an explicit `--mode plan`, so multi-agent
2461
2464
  // scripts can pass a uniform plan flag without per-agent branching.
@@ -42,6 +42,8 @@ export declare function prepareLocalFeedBlocks(localBlocks: OpenBlock[], opts: {
42
42
  dispatch: OpenBlock[];
43
43
  filter: ReturnType<typeof filterBlocksForFeed>;
44
44
  };
45
+ /** Specific host app + routine provenance for one feed row. */
46
+ export declare function formatFeedRuntime(block: Pick<OpenBlock, 'runtime' | 'origin' | 'routineName'>): string;
45
47
  /** Human summary line for one outcome rollup. */
46
48
  export declare function formatOutcomeHeader(group: OutcomeGroup): string;
47
49
  /** Map active sessions into the lightweight hints outcome enrichment needs. */
@@ -59,6 +61,9 @@ export declare function sessionHintsFromActive(sessions: Array<{
59
61
  worktree?: {
60
62
  slug?: string;
61
63
  };
64
+ host?: string;
65
+ origin?: 'cli' | 'routine';
66
+ routineName?: string;
62
67
  }>): SessionOutcomeHint[];
63
68
  /**
64
69
  * True when a SQLite open/query failed because another writer holds the lock.
@@ -16,6 +16,7 @@ import { notifyUrgentBlock } from '../lib/notify.js';
16
16
  import { gcMailbox } from '../lib/mailbox-gc.js';
17
17
  import { isValidMailboxId } from '../lib/mailbox.js';
18
18
  import { getActiveSessions } from '../lib/session/active.js';
19
+ import { backfillActiveRowsFromMeta } from './sessions.js';
19
20
  import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
20
21
  import { GLYPH, masthead } from '../lib/comms-render.js';
21
22
  import { discoverSessions } from '../lib/session/discover.js';
@@ -154,7 +155,7 @@ export function prepareLocalFeedBlocks(localBlocks, opts) {
154
155
  }
155
156
  function renderBlock(b, localHost, indent = '') {
156
157
  const host = b.host !== localHost ? chalk.yellow(` [${b.host}]`) : '';
157
- const runtime = chalk.gray(b.runtime);
158
+ const runtime = chalk.gray(formatFeedRuntime(b));
158
159
  const age = chalk.gray(relTime(b.ts));
159
160
  const cls = b.blockClass ? chalk.gray(`(${b.blockClass})`) : '';
160
161
  const consequence = b.consequence && b.consequence !== 'normal' ? chalk.red(`[${b.consequence}]`) : '';
@@ -218,6 +219,21 @@ function renderBlock(b, localHost, indent = '') {
218
219
  }
219
220
  console.log();
220
221
  }
222
+ /** Specific host app + routine provenance for one feed row. */
223
+ export function formatFeedRuntime(block) {
224
+ const host = {
225
+ ghostty: 'Ghostty',
226
+ iterm: 'iTerm',
227
+ terminal: 'terminal',
228
+ kitty: 'Kitty',
229
+ codium: 'VSCodium',
230
+ code: 'VS Code',
231
+ cursor: 'Cursor',
232
+ }[block.runtime.toLowerCase()] ?? block.runtime;
233
+ if (block.origin !== 'routine')
234
+ return host;
235
+ return `${host} · routine${block.routineName ? `:${block.routineName}` : ''}`;
236
+ }
221
237
  /** Human summary line for one outcome rollup. */
222
238
  export function formatOutcomeHeader(group) {
223
239
  const { agents, open, answered, parked } = group.counts;
@@ -251,6 +267,9 @@ export function sessionHintsFromActive(sessions) {
251
267
  prUrl: s.pr?.url,
252
268
  worktreeSlug: s.worktree?.slug,
253
269
  project: s.cwd ? projectKeyFromCwd(s.cwd) : undefined,
270
+ host: s.host,
271
+ origin: s.origin,
272
+ routineName: s.routineName,
254
273
  }));
255
274
  }
256
275
  /**
@@ -489,6 +508,7 @@ export function registerFeedCommand(program) {
489
508
  console.error(chalk.yellow('Feed: session index is locked; skipping local outcome enrichment'));
490
509
  }
491
510
  sessionMetas = loaded.metas;
511
+ backfillActiveRowsFromMeta(sessions, new Map(sessionMetas.map((meta) => [meta.id, meta])));
492
512
  }
493
513
  const localSignals = buildSessionSignals(sessions, sessionMetas);
494
514
  if (opts.pause || opts.kill) {