@phnx-labs/agents-cli 1.22.23 → 1.22.25

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 +359 -0
  2. package/README.md +19 -8
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/attach.d.ts +2 -0
  5. package/dist/commands/attach.js +32 -7
  6. package/dist/commands/cloud.js +9 -5
  7. package/dist/commands/defaults.js +2 -0
  8. package/dist/commands/doctor.d.ts +24 -0
  9. package/dist/commands/doctor.js +105 -9
  10. package/dist/commands/exec.js +99 -44
  11. package/dist/commands/feed.d.ts +23 -0
  12. package/dist/commands/feed.js +65 -2
  13. package/dist/commands/focus.d.ts +34 -3
  14. package/dist/commands/focus.js +301 -57
  15. package/dist/commands/go.d.ts +14 -1
  16. package/dist/commands/go.js +49 -5
  17. package/dist/commands/insights.js +6 -2
  18. package/dist/commands/inspect.js +39 -5
  19. package/dist/commands/menubar.js +14 -1
  20. package/dist/commands/models.js +1 -0
  21. package/dist/commands/modes.d.ts +12 -0
  22. package/dist/commands/modes.js +147 -0
  23. package/dist/commands/routines.d.ts +3 -0
  24. package/dist/commands/routines.js +70 -47
  25. package/dist/commands/run-cloud.js +1 -1
  26. package/dist/commands/secrets-sync.js +11 -13
  27. package/dist/commands/secrets.d.ts +2 -0
  28. package/dist/commands/secrets.js +54 -17
  29. package/dist/commands/sessions-browser.d.ts +36 -1
  30. package/dist/commands/sessions-browser.js +165 -19
  31. package/dist/commands/sessions-resume.d.ts +2 -0
  32. package/dist/commands/sessions-resume.js +28 -12
  33. package/dist/commands/sessions.d.ts +23 -1
  34. package/dist/commands/sessions.js +58 -61
  35. package/dist/commands/setup-secrets.js +1 -1
  36. package/dist/commands/setup-watchdog.js +5 -10
  37. package/dist/commands/setup.js +1 -1
  38. package/dist/commands/sync.js +246 -42
  39. package/dist/commands/teams.js +3 -2
  40. package/dist/commands/view.js +2 -0
  41. package/dist/commands/watchdog.d.ts +3 -4
  42. package/dist/commands/watchdog.js +26 -66
  43. package/dist/index.js +38 -2
  44. package/dist/lib/agent-modes.d.ts +49 -0
  45. package/dist/lib/agent-modes.js +70 -0
  46. package/dist/lib/agents.js +126 -21
  47. package/dist/lib/cloud/cursor.d.ts +79 -0
  48. package/dist/lib/cloud/cursor.js +228 -0
  49. package/dist/lib/cloud/registry.js +2 -0
  50. package/dist/lib/cloud/types.d.ts +7 -2
  51. package/dist/lib/cloud/types.js +14 -0
  52. package/dist/lib/crabbox/cli.d.ts +2 -2
  53. package/dist/lib/crabbox/config.d.ts +7 -8
  54. package/dist/lib/crabbox/config.js +14 -14
  55. package/dist/lib/crabbox/lease.d.ts +11 -4
  56. package/dist/lib/crabbox/lease.js +40 -8
  57. package/dist/lib/crabbox/setup-copy.d.ts +5 -0
  58. package/dist/lib/crabbox/setup-copy.js +17 -1
  59. package/dist/lib/daemon.js +27 -1
  60. package/dist/lib/device-config.js +7 -0
  61. package/dist/lib/devices/doctor-findings.d.ts +7 -1
  62. package/dist/lib/devices/doctor-findings.js +40 -1
  63. package/dist/lib/doctor-diff.d.ts +3 -0
  64. package/dist/lib/doctor-diff.js +15 -13
  65. package/dist/lib/event-stream.d.ts +3 -1
  66. package/dist/lib/event-stream.js +14 -1
  67. package/dist/lib/events.d.ts +9 -0
  68. package/dist/lib/events.js +58 -0
  69. package/dist/lib/exec.d.ts +15 -3
  70. package/dist/lib/exec.js +112 -19
  71. package/dist/lib/feed-outcome.d.ts +3 -0
  72. package/dist/lib/feed-outcome.js +18 -10
  73. package/dist/lib/feed.d.ts +4 -0
  74. package/dist/lib/hooks/cache.js +36 -3
  75. package/dist/lib/hooks.d.ts +19 -7
  76. package/dist/lib/hooks.js +88 -38
  77. package/dist/lib/hosts/passthrough.d.ts +21 -0
  78. package/dist/lib/hosts/passthrough.js +39 -12
  79. package/dist/lib/hosts/session-index.d.ts +4 -0
  80. package/dist/lib/hosts/session-index.js +7 -0
  81. package/dist/lib/manifest.d.ts +12 -2
  82. package/dist/lib/manifest.js +60 -5
  83. package/dist/lib/mcp.js +5 -1
  84. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  85. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +5 -1
  86. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  87. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  88. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
  89. package/dist/lib/menubar/install-menubar.d.ts +52 -2
  90. package/dist/lib/menubar/install-menubar.js +128 -6
  91. package/dist/lib/menubar/snapshot.d.ts +15 -0
  92. package/dist/lib/menubar/snapshot.js +40 -0
  93. package/dist/lib/plugins.js +13 -1
  94. package/dist/lib/refresh.d.ts +5 -0
  95. package/dist/lib/refresh.js +37 -33
  96. package/dist/lib/resource-inventory.d.ts +79 -0
  97. package/dist/lib/resource-inventory.js +122 -0
  98. package/dist/lib/resources/mcp.js +3 -0
  99. package/dist/lib/resources.js +8 -5
  100. package/dist/lib/routine-process-cleanup.d.ts +9 -0
  101. package/dist/lib/routine-process-cleanup.js +73 -0
  102. package/dist/lib/run-defaults.d.ts +2 -0
  103. package/dist/lib/run-defaults.js +23 -2
  104. package/dist/lib/runner.js +27 -22
  105. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  106. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
  107. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  108. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  109. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
  110. package/dist/lib/secrets/bundles.js +158 -85
  111. package/dist/lib/secrets/filestore.d.ts +7 -3
  112. package/dist/lib/secrets/filestore.js +25 -8
  113. package/dist/lib/secrets/index.js +34 -8
  114. package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
  115. package/dist/lib/secrets/rc-hygiene.js +14 -1
  116. package/dist/lib/secrets/reaper.d.ts +10 -0
  117. package/dist/lib/secrets/reaper.js +50 -15
  118. package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
  119. package/dist/lib/secrets/sync-passphrase.js +78 -0
  120. package/dist/lib/session/active.d.ts +4 -0
  121. package/dist/lib/session/db.js +8 -2
  122. package/dist/lib/session/recovery.d.ts +37 -0
  123. package/dist/lib/session/recovery.js +95 -0
  124. package/dist/lib/session/remote-list.d.ts +2 -0
  125. package/dist/lib/session/remote-list.js +1 -0
  126. package/dist/lib/session/session-cache.d.ts +4 -4
  127. package/dist/lib/session/session-cache.js +4 -4
  128. package/dist/lib/shims.d.ts +2 -2
  129. package/dist/lib/shims.js +45 -6
  130. package/dist/lib/signin-badge.js +2 -0
  131. package/dist/lib/startup/command-registry.d.ts +16 -0
  132. package/dist/lib/startup/command-registry.js +44 -0
  133. package/dist/lib/sync-umbrella.d.ts +5 -0
  134. package/dist/lib/sync-umbrella.js +5 -4
  135. package/dist/lib/teams/agents.js +1 -1
  136. package/dist/lib/teams/parsers.d.ts +1 -1
  137. package/dist/lib/tmux/session.d.ts +4 -2
  138. package/dist/lib/tmux/session.js +5 -5
  139. package/dist/lib/types.d.ts +3 -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
@@ -24,7 +24,9 @@ import { BrowserService } from './browser/service.js';
24
24
  import { BrowserIPCServer } from './browser/ipc.js';
25
25
  import { redactSecrets } from './redact.js';
26
26
  import { getAgentsBinPath, getCliLaunch, BUN_VIRTUAL_ROOT } from './cli-entry.js';
27
- import { isSchedulerEnabled, assertSchedulerEnabled } from './device-config.js';
27
+ import { getConfigValue, isSchedulerEnabled, assertSchedulerEnabled } from './device-config.js';
28
+ import { reapTerminalRoutineProcesses } from './routine-process-cleanup.js';
29
+ import { runWatchdogPass } from './watchdog/service.js';
28
30
  const PID_FILE = 'daemon.pid';
29
31
  const LOCK_FILE = 'daemon.lock';
30
32
  const LOG_FILE = 'logs.jsonl';
@@ -34,6 +36,7 @@ const LOG_ROTATE_COUNT = 3;
34
36
  const PLIST_NAME = 'com.phnx-labs.agents-daemon';
35
37
  const SYSTEMD_UNIT = 'agents-daemon.service';
36
38
  const MONITOR_TICK_MS = 60_000;
39
+ const WATCHDOG_TICK_MS = 3 * 60_000;
37
40
  /**
38
41
  * How often to re-scan for missed fires. Deliberately slower than the monitor
39
42
  * tick: detection walks a week of cron occurrences per routine
@@ -572,6 +575,24 @@ export async function runDaemon() {
572
575
  }
573
576
  if (schedulerEnabledAtBoot)
574
577
  bootScheduler();
578
+ let watchdogInFlight = false;
579
+ const runDaemonWatchdog = async () => {
580
+ if (watchdogInFlight || getConfigValue('watchdog.enabled').value !== true)
581
+ return;
582
+ watchdogInFlight = true;
583
+ try {
584
+ const result = await runWatchdogPass({ nudge: true });
585
+ log('INFO', `watchdog: ${result.counts.total} live, ${result.counts.stalled} stalled, ${result.counts.nudged} nudged`);
586
+ }
587
+ catch (err) {
588
+ log('ERROR', `watchdog tick failed: ${err.message}`);
589
+ }
590
+ finally {
591
+ watchdogInFlight = false;
592
+ }
593
+ };
594
+ const watchdogInterval = setInterval(() => { void runDaemonWatchdog(); }, WATCHDOG_TICK_MS);
595
+ const watchdogKickoff = setTimeout(() => { void runDaemonWatchdog(); }, 30_000);
575
596
  // Monitor engine: event-triggered watchers, beside the cron scheduler. Same
576
597
  // daemon, same dispatch seam — a monitor is a routine whose trigger is a
577
598
  // watched source instead of a clock. Reloads on SIGHUP alongside the scheduler.
@@ -682,6 +703,9 @@ export async function runDaemon() {
682
703
  const monitorInterval = setInterval(() => {
683
704
  writeHeartbeat();
684
705
  monitorRunningJobs();
706
+ const reaped = reapTerminalRoutineProcesses();
707
+ if (reaped.length > 0)
708
+ log('WARN', `Reaped ${reaped.length} terminal routine process group(s): ${reaped.join(', ')}`);
685
709
  }, MONITOR_TICK_MS);
686
710
  // Resource safety check: heal gaps between what DotAgents repos define and
687
711
  // what's actually installed in each agent home — the slow rot that nothing
@@ -1066,6 +1090,8 @@ export async function runDaemon() {
1066
1090
  monitorEngine.stop();
1067
1091
  await browserIPC.stop();
1068
1092
  clearInterval(monitorInterval);
1093
+ clearInterval(watchdogInterval);
1094
+ clearTimeout(watchdogKickoff);
1069
1095
  clearInterval(healInterval);
1070
1096
  clearTimeout(healKickoff);
1071
1097
  clearInterval(autoDispatchInterval);
@@ -67,6 +67,13 @@ export const CONFIG_KEYS = [
67
67
  type: 'bool',
68
68
  description: 'Whether the routines scheduler (daemon) may fire on this device.',
69
69
  },
70
+ {
71
+ name: 'watchdog.enabled',
72
+ yamlKey: 'watchdogEnabled',
73
+ scope: 'device',
74
+ type: 'bool',
75
+ description: 'Whether the daemon runs the watchdog pass on this device.',
76
+ },
70
77
  {
71
78
  name: 'browser.remote-control',
72
79
  yamlKey: 'browserRemoteControl',
@@ -10,7 +10,7 @@ export type FindingSeverity = 'critical' | 'warning';
10
10
  * the JSON consumer group by kind. */
11
11
  /** Every finding class. Severity is NOT annotated here — {@link FINDING_SEVERITY}
12
12
  * below owns it, and a second copy in these comments is a fourth place to drift. */
13
- export declare const ALL_FINDING_KINDS: readonly ["logged-out", "logout-unprovable", "missing-hook", "missing-plugin", "unwired-hook", "cli-missing", "missing-resource", "content-drift", "never-synced", "stale", "repo-behind", "repo-drift", "fleet-resource-gap", "host-cli-missing", "host-cli-invalid", "version-skew", "orphan", "duplicate-hook", "duplicate-hook-drift", "rc-secret-export", "exec-policy", "stale-cli"];
13
+ export declare const ALL_FINDING_KINDS: readonly ["logged-out", "logout-unprovable", "missing-hook", "missing-plugin", "unwired-hook", "cli-missing", "missing-resource", "content-drift", "never-synced", "stale", "repo-behind", "repo-drift", "fleet-resource-gap", "host-cli-missing", "host-cli-invalid", "version-skew", "orphan", "duplicate-hook", "duplicate-hook-drift", "rc-secret-export", "env-secret-export", "exec-policy", "stale-cli"];
14
14
  /**
15
15
  * The severity each kind is emitted with - the SINGLE source of truth, read by
16
16
  * the builders below and asserted against both prose rubrics by
@@ -89,6 +89,12 @@ export interface LocalFindingInputs {
89
89
  duplicateHooks?: DuplicateVersionHook[];
90
90
  /** Credential-shaped exports found in the user's shell rc files (RUSH-1968). */
91
91
  rcSecrets?: RcSecretFinding[];
92
+ /** True when the file-store master key is live in THIS process's environment.
93
+ * Distinct from `rcSecrets`, which scans FILES: a value inherited by a
94
+ * long-lived process survives deleting the rc line that set it, so the rc
95
+ * scan reports clean while the leak is still in flight (RUSH-1968). Never the
96
+ * value — only whether it is set. */
97
+ masterPassphraseInEnv?: boolean;
92
98
  /** The effective PowerShell execution policy and the platform it was read on.
93
99
  * Only `win32` yields a finding — the `agents.ps1` launcher is Windows-only. */
94
100
  execPolicy?: {
@@ -23,7 +23,7 @@
23
23
  * never-synced · stale · repo-behind · repo-drift · version-skew ·
24
24
  * fleet-resource-gap · orphan · duplicate-hook ·
25
25
  * duplicate-hook-drift · host-cli-missing · host-cli-invalid ·
26
- * rc-secret-export · exec-policy · stale-cli.
26
+ * rc-secret-export · env-secret-export · exec-policy · stale-cli.
27
27
  * (RUSH-2162 moved never-synced and duplicate-hook-drift to WARNING: both are
28
28
  * stale-sync states one `agents sync` resolves, not "needs you now".)
29
29
  *
@@ -94,6 +94,7 @@ export const ALL_FINDING_KINDS = [
94
94
  'duplicate-hook', // one hook materialized in several version homes, byte-identical
95
95
  'duplicate-hook-drift', // …with differing content, so a stale copy can disagree
96
96
  'rc-secret-export', // credential-shaped export in a shell rc file
97
+ 'env-secret-export', // the file-store master key live in THIS process's env
97
98
  'exec-policy', // Windows execution policy blocks agents.ps1
98
99
  'stale-cli',
99
100
  ];
@@ -133,6 +134,7 @@ export const FINDING_SEVERITY = {
133
134
  'host-cli-missing': 'warning',
134
135
  'host-cli-invalid': 'warning',
135
136
  'rc-secret-export': 'warning',
137
+ 'env-secret-export': 'warning',
136
138
  'exec-policy': 'warning',
137
139
  'stale-cli': 'warning',
138
140
  };
@@ -240,6 +242,11 @@ export function remediationFor(finding) {
240
242
  return 'fix the manifest';
241
243
  case 'rc-secret-export':
242
244
  return 'agents secrets add';
245
+ case 'env-secret-export':
246
+ // Not just "restart the shell": the value is in every long-lived parent
247
+ // that inherited it — an editor, a tmux server, the agents daemon — and
248
+ // each keeps handing it to new children until IT restarts.
249
+ return 'unset at the source, then restart every process that inherited it (shells, editor, tmux, agents daemon)';
243
250
  case 'exec-policy':
244
251
  return 'Set-ExecutionPolicy -Scope CurrentUser RemoteSigned';
245
252
  case 'stale-cli':
@@ -450,6 +457,10 @@ export function buildLocalFindings(input) {
450
457
  // into `agents secrets`.
451
458
  for (const f of rcSecretFindings(device, input.rcSecrets ?? []))
452
459
  out.push(f);
460
+ // The same key, live in this process's environment rather than in a file.
461
+ const envFinding = envSecretFinding(device, input.masterPassphraseInEnv ?? false);
462
+ if (envFinding)
463
+ out.push(envFinding);
453
464
  // Windows execution policy blocking the generated agents.ps1 launcher.
454
465
  const policyFinding = execPolicyFinding(device, input.execPolicy);
455
466
  if (policyFinding)
@@ -572,6 +583,32 @@ function rcSecretFindings(device, rc) {
572
583
  * policy itself. Returns null off Windows or on a permissive policy — pure, so
573
584
  * both branches are testable without invoking PowerShell.
574
585
  */
586
+ /**
587
+ * The file-store master key sitting in THIS process's environment.
588
+ *
589
+ * `rc-hygiene.ts` scans FILES, which leaves a real hole: a value inherited by a
590
+ * long-lived process outlives the rc line that set it, so an operator who
591
+ * deletes `~/.zshenv:8` gets a clean `rc-secret-export` while every shell,
592
+ * editor and agent started before the edit still carries the key — and hands it
593
+ * to everything they spawn. Verified on a box with no rc export whose live
594
+ * environment still held the value, hashing identical to its key file.
595
+ *
596
+ * Warning, not critical: on the release home base the export is deliberate and
597
+ * scoped (`headless-sign-context.sh`), so the message names that exception
598
+ * rather than the check trying to detect it.
599
+ */
600
+ function envSecretFinding(device, present) {
601
+ if (!present)
602
+ return null;
603
+ return finding({
604
+ severity: FINDING_SEVERITY['env-secret-export'], kind: 'env-secret-export', device,
605
+ // Never the value — only that it is set.
606
+ message: 'AGENTS_SECRETS_PASSPHRASE is set in this process environment — every '
607
+ + 'child inherits it and any same-user process can read it from /proc/<pid>/environ. '
608
+ + 'It outlives the shell rc line that set it, so deleting that line is not enough. '
609
+ + '(Expected inside a release sign context, which sets it deliberately.)',
610
+ });
611
+ }
575
612
  function execPolicyFinding(device, execPolicy) {
576
613
  if (!execPolicy || execPolicy.platform !== 'win32')
577
614
  return null;
@@ -881,6 +918,8 @@ function warningSubject(f) {
881
918
  return 'orphans';
882
919
  if (f.kind === 'rc-secret-export')
883
920
  return 'shell rc';
921
+ if (f.kind === 'env-secret-export')
922
+ return 'environment';
884
923
  if (f.kind === 'exec-policy')
885
924
  return 'PowerShell';
886
925
  if (f.kind === 'fleet-resource-gap')
@@ -22,6 +22,7 @@
22
22
  import type { AgentId } from './types.js';
23
23
  import type { DiscoveredPlugin } from './types.js';
24
24
  import { type HookWiringReport } from './hooks.js';
25
+ import { type ResourceInventory } from './resource-inventory.js';
25
26
  export type DoctorKind = 'commands' | 'skills' | 'hooks' | 'rules' | 'mcp' | 'permissions' | 'subagents' | 'plugins' | 'promptcuts';
26
27
  export type DiffStatus = 'ok' | 'diff' | 'missing' | 'extra';
27
28
  export type SourceLayer = 'project' | 'user' | 'system' | 'extra';
@@ -75,6 +76,8 @@ export interface VersionResourceReport {
75
76
  /** Wiring inspection for the version's native settings.json (claude/droid).
76
77
  * Populated only when the `hooks` kind is in scope. */
77
78
  hookWiring?: HookWiringReport;
79
+ /** Harness-scoped hook state from the shared inventory engine. */
80
+ hookInventory?: ResourceInventory;
78
81
  /** Source layers behind their upstream. Filled by the doctor command (a git
79
82
  * probe), not the pure diff — undefined when uncomputed. */
80
83
  sourceBehind?: SourceLayerBehind[];
@@ -33,7 +33,8 @@ import { shouldInstallCommandAsSkill, commandSkillMatches, commandSkillName } fr
33
33
  import { gooseCommandMatches, gooseCommandsDir } from './goose-commands.js';
34
34
  import { supports } from './capabilities.js';
35
35
  import { listSkillsInVersionHome, getVersionSkillsDir } from './skills.js';
36
- import { listHooksInVersionHome, listHookEntriesFromDir, checkVersionHookWiring } from './hooks.js';
36
+ import { listHookEntriesFromDir } from './hooks.js';
37
+ import { getResourceInventory } from './resource-inventory.js';
37
38
  const RULES_DOC_FILENAME = 'README.md';
38
39
  const ALL_KINDS = [
39
40
  'commands',
@@ -300,11 +301,10 @@ function diffSkills(agent, version, cwd, excludeProject = false) {
300
301
  return rows.sort((a, b) => a.name.localeCompare(b.name));
301
302
  }
302
303
  // ─── hooks ────────────────────────────────────────────────────────────────────
303
- function diffHooks(agent, version, cwd) {
304
+ function diffHooks(agent, version, cwd, inventory) {
304
305
  if (!AGENTS[agent].supportsHooks)
305
306
  return [];
306
- const installedEntries = listHooksInVersionHome(agent, version);
307
- const installedByName = new Map(installedEntries.map((e) => [e.name, e]));
307
+ const installedByName = new Map(inventory.onDisk.map((e) => [e.name, e]));
308
308
  // Sync intentionally excludes project/.agents/hooks/ — mirror that.
309
309
  const layerBases = buildLayerBases(cwd, 'hooks', { excludeProject: true });
310
310
  // Group source files the same way the hook installer does (basename across
@@ -329,14 +329,14 @@ function diffHooks(agent, version, cwd) {
329
329
  continue;
330
330
  }
331
331
  const a = readSafe(src.entry.scriptPath);
332
- const b = readSafe(installed.scriptPath);
332
+ const b = readSafe(installed.path);
333
333
  let matches = a != null && b != null && normalize(a) === normalize(b);
334
- if (matches && src.entry.dataFile && installed.dataFile) {
334
+ if (matches && src.entry.dataFile && installed.detail) {
335
335
  const ad = readSafe(src.entry.dataFile);
336
- const bd = readSafe(installed.dataFile);
336
+ const bd = readSafe(installed.detail);
337
337
  matches = ad != null && bd != null && normalize(ad) === normalize(bd);
338
338
  }
339
- else if (matches && (!!src.entry.dataFile !== !!installed.dataFile)) {
339
+ else if (matches && (!!src.entry.dataFile !== !!installed.detail)) {
340
340
  matches = false;
341
341
  }
342
342
  rows.push({
@@ -345,13 +345,13 @@ function diffHooks(agent, version, cwd) {
345
345
  status: matches ? 'ok' : 'diff',
346
346
  source: src.layer,
347
347
  sourcePath: src.entry.scriptPath,
348
- homePath: installed.scriptPath,
348
+ homePath: installed.path,
349
349
  });
350
350
  }
351
351
  for (const [name, installed] of installedByName) {
352
352
  if (seen.has(name))
353
353
  continue;
354
- rows.push({ kind: 'hooks', name, status: 'extra', homePath: installed.scriptPath });
354
+ rows.push({ kind: 'hooks', name, status: 'extra', homePath: installed.path });
355
355
  }
356
356
  return rows.sort((a, b) => a.name.localeCompare(b.name));
357
357
  }
@@ -609,12 +609,13 @@ export function diffVersionResources(agent, version, options = {}) {
609
609
  empty.commands = diffCommands(agent, version, cwd, excludeProject);
610
610
  if (requested.has('skills'))
611
611
  empty.skills = diffSkills(agent, version, cwd, excludeProject);
612
- if (requested.has('hooks'))
613
- empty.hooks = diffHooks(agent, version, cwd);
612
+ const hookInventory = requested.has('hooks') ? getResourceInventory(agent, version, 'hooks', { cwd }) : undefined;
613
+ if (hookInventory)
614
+ empty.hooks = diffHooks(agent, version, cwd, hookInventory);
614
615
  // Wiring check: a hook FILE can be present and byte-identical to source (ok
615
616
  // above) yet never referenced in settings.json, so it never fires. Only
616
617
  // meaningful when hooks are in scope.
617
- const hookWiring = requested.has('hooks') ? checkVersionHookWiring(agent, version) : undefined;
618
+ const hookWiring = hookInventory?.wiring;
618
619
  if (requested.has('rules'))
619
620
  empty.rules = diffRules(agent, version, cwd, excludeProject);
620
621
  if (requested.has('mcp'))
@@ -654,6 +655,7 @@ export function diffVersionResources(agent, version, options = {}) {
654
655
  kinds: empty,
655
656
  summary: { ok, diff, missing, extra },
656
657
  ...(hookWiring ? { hookWiring } : {}),
658
+ ...(hookInventory ? { hookInventory } : {}),
657
659
  };
658
660
  }
659
661
  export const DOCTOR_ALL_KINDS = ALL_KINDS;
@@ -36,6 +36,8 @@ export interface UnifiedQuery {
36
36
  * Read a unified, newest-first event stream. Operational events come from
37
37
  * events.ts `query()`; agent-semantic events from the activity logs, normalized
38
38
  * to the same record shape and filtered identically. `limit` caps the merged
39
- * result (each source is fetched up to `limit`, so the top-N is exact).
39
+ * result (each source is fetched up to `limit` *after* its primary filters
40
+ * eventTypes for activity, eventTypes/module/bundle for ops — so the top-N is
41
+ * exact for those filters).
40
42
  */
41
43
  export declare function readUnifiedEvents(q?: UnifiedQuery): EventRecord[];
@@ -46,7 +46,9 @@ function matches(r, q) {
46
46
  * Read a unified, newest-first event stream. Operational events come from
47
47
  * events.ts `query()`; agent-semantic events from the activity logs, normalized
48
48
  * to the same record shape and filtered identically. `limit` caps the merged
49
- * result (each source is fetched up to `limit`, so the top-N is exact).
49
+ * result (each source is fetched up to `limit` *after* its primary filters
50
+ * eventTypes for activity, eventTypes/module/bundle for ops — so the top-N is
51
+ * exact for those filters).
50
52
  */
51
53
  export function readUnifiedEvents(q = {}) {
52
54
  // `bundle` is filtered inside query()'s scan (before its limit cutoff) so a
@@ -66,10 +68,21 @@ export function readUnifiedEvents(q = {}) {
66
68
  });
67
69
  if (q.includeActivity === false)
68
70
  return ops;
71
+ // Activity events always stamp module: 'activity'. A non-activity module
72
+ // filter can never match them — skip the activity scan entirely.
73
+ if (q.module != null && q.module !== 'activity')
74
+ return ops;
75
+ // Push eventTypes into the activity reader so `limit` is applied AFTER the
76
+ // event-type filter (readRecentActivity already does this for `events`).
77
+ // Without this, a rare match older than the newest-`limit` window of routine
78
+ // churn is silently dropped — the same class of bug as the ops-side bundle
79
+ // pre-filter above (RUSH-2093). Remaining filters (agent, sessionId, …) still
80
+ // run via matches() for fields activity.ts does not pre-filter.
69
81
  const acts = readActivityAsEventRecords({
70
82
  sinceMs: q.startDate?.getTime(),
71
83
  limit: q.limit,
72
84
  root: q.activityRoot,
85
+ events: q.eventTypes,
73
86
  }).filter((r) => matches(r, q));
74
87
  const merged = [...ops, ...acts].sort((a, b) => Date.parse(b.ts) - Date.parse(a.ts));
75
88
  return typeof q.limit === 'number' ? merged.slice(0, q.limit) : merged;
@@ -226,6 +226,15 @@ export declare function query(options: {
226
226
  bundle?: string;
227
227
  limit?: number;
228
228
  }): EventRecord[];
229
+ /**
230
+ * Scan event logs once for a set of session IDs and return browser/computer
231
+ * usage flags for each. O(files) instead of O(N × files) — safe to call
232
+ * outside a SQLite write transaction.
233
+ */
234
+ export declare function queryToolUsageForSessions(sessionIds: ReadonlySet<string>): Map<string, {
235
+ usedBrowser: boolean;
236
+ usedComputer: boolean;
237
+ }>;
229
238
  /**
230
239
  * Get performance stats for a specific label.
231
240
  */
@@ -1052,6 +1052,64 @@ export function query(options) {
1052
1052
  }
1053
1053
  return results;
1054
1054
  }
1055
+ /**
1056
+ * Scan event logs once for a set of session IDs and return browser/computer
1057
+ * usage flags for each. O(files) instead of O(N × files) — safe to call
1058
+ * outside a SQLite write transaction.
1059
+ */
1060
+ export function queryToolUsageForSessions(sessionIds) {
1061
+ const result = new Map();
1062
+ if (sessionIds.size === 0)
1063
+ return result;
1064
+ for (const id of sessionIds) {
1065
+ result.set(id, { usedBrowser: false, usedComputer: false });
1066
+ }
1067
+ const BROWSER_EVENTS = new Set(['browser.navigate', 'browser.screenshot']);
1068
+ const COMPUTER_EVENTS = new Set(['computer.action']);
1069
+ eventsPath();
1070
+ migrateLegacyEventLogs();
1071
+ const files = listEventLogFiles().sort((a, b) => Number(b.currentActive) - Number(a.currentActive) || b.mtimeMs - a.mtimeMs || b.path.localeCompare(a.path));
1072
+ let remaining = sessionIds.size * 2; // each session needs up to 2 flags set
1073
+ for (const file of files) {
1074
+ if (remaining <= 0)
1075
+ break;
1076
+ let content;
1077
+ if (file.gzip) {
1078
+ try {
1079
+ content = gunzipSync(fs.readFileSync(file.path)).toString('utf-8');
1080
+ }
1081
+ catch {
1082
+ continue;
1083
+ }
1084
+ }
1085
+ else {
1086
+ content = fs.readFileSync(file.path, 'utf-8');
1087
+ }
1088
+ for (const line of content.trim().split('\n')) {
1089
+ if (!line)
1090
+ continue;
1091
+ try {
1092
+ const record = JSON.parse(line);
1093
+ const sid = record.sessionId;
1094
+ if (!sid || !result.has(sid))
1095
+ continue;
1096
+ const entry = result.get(sid);
1097
+ if (BROWSER_EVENTS.has(record.event) && !entry.usedBrowser) {
1098
+ entry.usedBrowser = true;
1099
+ remaining--;
1100
+ }
1101
+ else if (COMPUTER_EVENTS.has(record.event) && !entry.usedComputer) {
1102
+ entry.usedComputer = true;
1103
+ remaining--;
1104
+ }
1105
+ }
1106
+ catch {
1107
+ // skip malformed lines
1108
+ }
1109
+ }
1110
+ }
1111
+ return result;
1112
+ }
1055
1113
  // ─── Stats ────────────────────────────────────────────────────────────────────
1056
1114
  /**
1057
1115
  * Get performance stats for a specific label.
@@ -42,7 +42,7 @@ export declare function headlessPlanStallCommand(args: {
42
42
  * (every agent supports edit-like behavior as its default).
43
43
  * - `plan` on an agent without a read-only mode degrades to the agent's
44
44
  * safest native mode (`capabilities.modes[0]`, typically `edit`). Agents
45
- * like antigravity/cursor/kiro have no plan flag; hard-failing made
45
+ * like antigravity/kiro have no plan flag; hard-failing made
46
46
  * multi-agent scripts (`--mode plan` for everyone) unusable and diverged
47
47
  * from `agents teams add`, which already defaults to `edit`. Callers that
48
48
  * care (the `agents run` CLI) must surface a warning when requested ≠
@@ -61,8 +61,8 @@ export declare function resolveMode(agent: AgentId, requested: Mode): Mode;
61
61
  * `--prompt` + `--plan`, and grok's `--permission-mode plan` silently stalls at
62
62
  * its ExitPlanMode gate. For those agents, a headless plan request degrades to
63
63
  * `auto` (kimi -p auto-runs; grok maps auto→edit via resolveMode) with a visible
64
- * one-line stderr warning, mirroring the graceful plan→edit degrade cursor and
65
- * antigravity get for having no plan flag at all. Interactive runs are never
64
+ * one-line stderr warning, mirroring the graceful plan→edit degrade antigravity
65
+ * and kiro get for having no plan flag at all. Interactive runs are never
66
66
  * downgraded. This is the single source of truth shared by buildExecCommand
67
67
  * (agents run / teams) and the routine runner.
68
68
  */
@@ -389,7 +389,19 @@ export declare function shouldWrapInTmux(ctx: TmuxWrapContext): boolean;
389
389
  */
390
390
  export declare function buildTmuxAgentCommand(executable: string, args: string[], env: NodeJS.ProcessEnv, opts?: {
391
391
  redactEnvValues?: boolean;
392
+ envFile?: string;
392
393
  }): string;
394
+ /**
395
+ * Serialize the pane env to a shell-sourceable file, created 0600 and exclusively
396
+ * (`wx`) so it can never adopt a pre-existing file's mode or content.
397
+ *
398
+ * EVERY key goes in the file, not a curated "secret-bearing" subset: a denylist
399
+ * has to be updated for each new credential and is wrong the moment someone
400
+ * forgets, whereas routing all of it through the file makes the guarantee hold by
401
+ * construction. Keys are filtered to valid shell identifiers for the same reason
402
+ * `env` needed it — an exported function (`BASH_FUNC_x%%`) is not assignable.
403
+ */
404
+ export declare function writeTmuxEnvFile(env: NodeJS.ProcessEnv, filePath: string): void;
393
405
  /**
394
406
  * Trim a raw `tmux capture-pane` dump to its last `maxLines` non-empty lines
395
407
  * (right-stripping each). Used by runInTmux to recap a fast-failed agent's