@phnx-labs/agents-cli 1.20.78 → 1.20.82

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 (176) hide show
  1. package/CHANGELOG.md +487 -0
  2. package/README.md +25 -25
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +9 -0
  5. package/dist/commands/activity.js +153 -24
  6. package/dist/commands/apply.js +11 -4
  7. package/dist/commands/cli.js +1 -1
  8. package/dist/commands/commands.d.ts +1 -1
  9. package/dist/commands/commands.js +7 -6
  10. package/dist/commands/defaults.js +3 -11
  11. package/dist/commands/doctor.d.ts +65 -0
  12. package/dist/commands/doctor.js +575 -31
  13. package/dist/commands/events.js +1 -1
  14. package/dist/commands/exec.js +24 -5
  15. package/dist/commands/feed.d.ts +4 -0
  16. package/dist/commands/feed.js +87 -11
  17. package/dist/commands/harness.js +2 -2
  18. package/dist/commands/hooks.js +1 -1
  19. package/dist/commands/import.js +8 -4
  20. package/dist/commands/logs.js +6 -0
  21. package/dist/commands/models.js +1 -1
  22. package/dist/commands/packages.js +6 -6
  23. package/dist/commands/permissions.js +1 -1
  24. package/dist/commands/profiles.js +1 -1
  25. package/dist/commands/pull.d.ts +1 -1
  26. package/dist/commands/pull.js +4 -4
  27. package/dist/commands/repo.d.ts +78 -0
  28. package/dist/commands/repo.js +187 -11
  29. package/dist/commands/routines.js +12 -2
  30. package/dist/commands/rules.js +13 -12
  31. package/dist/commands/secrets.d.ts +3 -1
  32. package/dist/commands/secrets.js +100 -32
  33. package/dist/commands/sessions-inject.d.ts +7 -4
  34. package/dist/commands/sessions-inject.js +18 -18
  35. package/dist/commands/sessions-picker.js +32 -3
  36. package/dist/commands/sessions-resume.d.ts +16 -0
  37. package/dist/commands/sessions-resume.js +14 -13
  38. package/dist/commands/sessions.d.ts +31 -0
  39. package/dist/commands/sessions.js +141 -16
  40. package/dist/commands/set.d.ts +15 -0
  41. package/dist/commands/set.js +79 -0
  42. package/dist/commands/setup-fleet.d.ts +21 -0
  43. package/dist/commands/setup-fleet.js +201 -0
  44. package/dist/commands/setup-secrets.d.ts +20 -0
  45. package/dist/commands/setup-secrets.js +223 -0
  46. package/dist/commands/setup.js +17 -3
  47. package/dist/commands/skills.js +1 -1
  48. package/dist/commands/sync.js +7 -2
  49. package/dist/commands/teams-picker.js +0 -1
  50. package/dist/commands/teams.d.ts +33 -1
  51. package/dist/commands/teams.js +208 -44
  52. package/dist/commands/versions.js +10 -5
  53. package/dist/commands/view.d.ts +1 -0
  54. package/dist/commands/view.js +11 -0
  55. package/dist/index.js +5 -8
  56. package/dist/lib/acp/harnesses.js +0 -7
  57. package/dist/lib/activity.d.ts +156 -0
  58. package/dist/lib/activity.js +282 -0
  59. package/dist/lib/agents.d.ts +9 -2
  60. package/dist/lib/agents.js +72 -63
  61. package/dist/lib/browser/service.js +3 -0
  62. package/dist/lib/browser/types.d.ts +7 -0
  63. package/dist/lib/capabilities.js +6 -2
  64. package/dist/lib/crabbox/lease.js +2 -2
  65. package/dist/lib/crabbox/setup-copy.d.ts +4 -4
  66. package/dist/lib/crabbox/setup-copy.js +4 -4
  67. package/dist/lib/daemon.d.ts +18 -0
  68. package/dist/lib/daemon.js +47 -6
  69. package/dist/lib/devices/fleet.d.ts +1 -1
  70. package/dist/lib/devices/fleet.js +1 -1
  71. package/dist/lib/doctor-diff.d.ts +20 -0
  72. package/dist/lib/doctor-diff.js +6 -1
  73. package/dist/lib/drift.d.ts +31 -11
  74. package/dist/lib/drift.js +58 -7
  75. package/dist/lib/events.d.ts +8 -1
  76. package/dist/lib/events.js +10 -1
  77. package/dist/lib/exec.js +35 -8
  78. package/dist/lib/feed-post.d.ts +28 -1
  79. package/dist/lib/feed-post.js +110 -2
  80. package/dist/lib/fleet/apply.js +8 -1
  81. package/dist/lib/fleet/auth-sync.d.ts +18 -1
  82. package/dist/lib/fleet/auth-sync.js +25 -11
  83. package/dist/lib/fleet/remote-login.js +5 -0
  84. package/dist/lib/git.d.ts +14 -14
  85. package/dist/lib/git.js +41 -44
  86. package/dist/lib/hooks.d.ts +42 -0
  87. package/dist/lib/hooks.js +137 -93
  88. package/dist/lib/hosts/dispatch.d.ts +28 -1
  89. package/dist/lib/hosts/dispatch.js +33 -5
  90. package/dist/lib/hosts/option.js +2 -2
  91. package/dist/lib/hosts/passthrough.d.ts +22 -1
  92. package/dist/lib/hosts/passthrough.js +238 -13
  93. package/dist/lib/hosts/run-target.d.ts +2 -0
  94. package/dist/lib/hosts/run-target.js +1 -0
  95. package/dist/lib/hq/floor.d.ts +1 -1
  96. package/dist/lib/hq/floor.js +6 -1
  97. package/dist/lib/mcp.js +3 -71
  98. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  99. package/dist/lib/menubar/notify-desktop.d.ts +15 -0
  100. package/dist/lib/menubar/notify-desktop.js +41 -5
  101. package/dist/lib/merged-resources.d.ts +11 -0
  102. package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
  103. package/dist/lib/permissions.d.ts +0 -32
  104. package/dist/lib/permissions.js +4 -157
  105. package/dist/lib/picker.js +1 -1
  106. package/dist/lib/platform/process.d.ts +16 -5
  107. package/dist/lib/platform/process.js +54 -0
  108. package/dist/lib/plugins.d.ts +0 -8
  109. package/dist/lib/plugins.js +4 -110
  110. package/dist/lib/project-resources.js +4 -1
  111. package/dist/lib/refresh.d.ts +5 -3
  112. package/dist/lib/refresh.js +7 -5
  113. package/dist/lib/resources/commands.js +4 -1
  114. package/dist/lib/resources/mcp.js +0 -4
  115. package/dist/lib/resources/permissions.d.ts +1 -1
  116. package/dist/lib/resources/permissions.js +1 -5
  117. package/dist/lib/resources/rules.js +4 -1
  118. package/dist/lib/resources/skills.js +4 -1
  119. package/dist/lib/resources/subagents.js +4 -0
  120. package/dist/lib/resources/types.d.ts +1 -1
  121. package/dist/lib/routines.d.ts +20 -0
  122. package/dist/lib/routines.js +6 -0
  123. package/dist/lib/run-defaults.d.ts +1 -0
  124. package/dist/lib/run-defaults.js +9 -0
  125. package/dist/lib/runner.d.ts +0 -12
  126. package/dist/lib/runner.js +49 -10
  127. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  128. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  129. package/dist/lib/secrets/agent.d.ts +16 -0
  130. package/dist/lib/secrets/agent.js +105 -3
  131. package/dist/lib/secrets/audit.d.ts +46 -0
  132. package/dist/lib/secrets/audit.js +56 -0
  133. package/dist/lib/secrets/bundles.d.ts +11 -8
  134. package/dist/lib/secrets/bundles.js +31 -23
  135. package/dist/lib/secrets/index.d.ts +2 -1
  136. package/dist/lib/secrets/index.js +8 -2
  137. package/dist/lib/secrets/remote.d.ts +75 -2
  138. package/dist/lib/secrets/remote.js +139 -5
  139. package/dist/lib/secrets/sync.js +3 -3
  140. package/dist/lib/session/active.d.ts +110 -26
  141. package/dist/lib/session/active.js +161 -50
  142. package/dist/lib/session/actor-sidecar.d.ts +23 -0
  143. package/dist/lib/session/actor-sidecar.js +101 -0
  144. package/dist/lib/session/db.d.ts +4 -1
  145. package/dist/lib/session/db.js +65 -8
  146. package/dist/lib/session/discover.d.ts +4 -0
  147. package/dist/lib/session/discover.js +13 -0
  148. package/dist/lib/session/hook-sessions.d.ts +9 -0
  149. package/dist/lib/session/hook-sessions.js +57 -8
  150. package/dist/lib/session/pid-registry.d.ts +13 -0
  151. package/dist/lib/session/render.d.ts +2 -0
  152. package/dist/lib/session/render.js +1 -1
  153. package/dist/lib/session/types.d.ts +16 -0
  154. package/dist/lib/staleness/detectors/commands.js +2 -2
  155. package/dist/lib/staleness/detectors/permissions.js +1 -45
  156. package/dist/lib/staleness/writers/commands.js +4 -5
  157. package/dist/lib/staleness/writers/hooks.js +1 -1
  158. package/dist/lib/startup/command-registry.d.ts +1 -2
  159. package/dist/lib/startup/command-registry.js +2 -4
  160. package/dist/lib/subagents-registry.js +5 -12
  161. package/dist/lib/subagents.d.ts +0 -10
  162. package/dist/lib/subagents.js +0 -12
  163. package/dist/lib/teams/agents.d.ts +0 -2
  164. package/dist/lib/teams/agents.js +10 -27
  165. package/dist/lib/teams/index.d.ts +1 -1
  166. package/dist/lib/teams/index.js +1 -1
  167. package/dist/lib/types.d.ts +17 -9
  168. package/dist/lib/usage.d.ts +28 -5
  169. package/dist/lib/usage.js +271 -8
  170. package/dist/lib/versions.js +7 -1
  171. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  172. package/dist/lib/watchdog/watchdog.js +25 -13
  173. package/package.json +1 -2
  174. package/dist/commands/check.d.ts +0 -15
  175. package/dist/commands/check.js +0 -180
  176. package/dist/commands/resources.d.ts +0 -5
@@ -43,12 +43,17 @@ export declare function isDangerousRemoteEnvKey(name: string): boolean;
43
43
  */
44
44
  export declare function resolveHostSshTarget(nameOrAlias: string): Promise<string>;
45
45
  /**
46
- * Merge `--host <single>` and `--hosts <a,b,c>` into an ordered, de-duplicated
47
- * list. Both flags compose; either alone works. Empty when neither is set.
46
+ * Merge `--host <single>` / `--hosts <a,b,c>` (and their `--device` / `--devices`
47
+ * aliases) into an ordered, de-duplicated list. All four flags compose; any alone
48
+ * works. `--device`/`--devices` resolve identically to `--host`/`--hosts` so the
49
+ * fleet-wide `--device` vocabulary (see `agents activity`, `agents run --device`)
50
+ * works on the secrets remote commands too. Empty when none is set.
48
51
  */
49
52
  export declare function parseHostsOption(opts: {
50
53
  host?: string;
51
54
  hosts?: string;
55
+ device?: string;
56
+ devices?: string;
52
57
  }): string[];
53
58
  /**
54
59
  * Split a `bundle@host` reference. No `@` → a local bundle (host undefined).
@@ -104,3 +109,71 @@ export declare function remoteSecretsStream(target: string, args: string[], opts
104
109
  export declare function remoteResolveEnv(target: string, bundle: string, opts?: {
105
110
  osLookupName?: string;
106
111
  }): Promise<Record<string, string>>;
112
+ /**
113
+ * Outcome of a post-push read-back verification (see verifyRemoteKeychainPush).
114
+ * - `ok` — the pushed keys materialized readably on the remote.
115
+ * - `locked-keychain` — the read-back gave the SPECIFIC signal of a keychain
116
+ * that didn't persist (the remote's headless "not unlocked
117
+ * in the secrets agent" guard, or a "stored item … not
118
+ * found" on read-back, or pushed keys simply absent). Only
119
+ * this verdict earns the locked-login-keychain diagnosis +
120
+ * `--remote-backend file` steer.
121
+ * - `error` — a DIFFERENT failure (flaky SSH, timeout, unparseable
122
+ * payload). The raw error is re-surfaced verbatim, never
123
+ * mislabeled as a locked keychain.
124
+ */
125
+ export type RemoteKeychainWriteVerification = {
126
+ ok: true;
127
+ } | {
128
+ ok: false;
129
+ kind: 'locked-keychain';
130
+ reason: string;
131
+ } | {
132
+ ok: false;
133
+ kind: 'error';
134
+ reason: string;
135
+ };
136
+ /**
137
+ * Decide whether a keychain-backed push to a remote actually PERSISTED its secret
138
+ * value items, given the read-back of that bundle from the remote's own store.
139
+ *
140
+ * The silent-failure this guards: pushing `--remote-backend keychain` (default) to
141
+ * a macOS host over headless SSH lands the bundle METADATA but not readable value
142
+ * items — the remote login keychain is locked in the non-interactive SSH context,
143
+ * so Security accepts the item WRITE at the DB level but the biometry-ACL'd item is
144
+ * unreadable, and the remote `import` still reports success (values written first,
145
+ * metadata `noAcl` last — bundles.ts writeBundleWithItems). The metadata-only bundle
146
+ * then fails every later read with the confusing `Bundle '<b>' key '<k>': stored
147
+ * item '<item>' not found` (bundles.ts resolveBundleEnv). We catch it by reading
148
+ * the bundle back the same way a release will (`secrets export --plaintext --format
149
+ * json`, driven headlessly on the remote so its `agentOnly` guard FAILS FAST before
150
+ * any keychain read — no Touch ID prompt) and confirming every pushed key returned.
151
+ *
152
+ * Pure so both branches are unit-testable without a real locked keychain: inject the
153
+ * "read-back failed / key absent" condition through `readBack`.
154
+ */
155
+ export declare function evaluateKeychainWriteVerification(pushedKeys: string[], readBack: {
156
+ ok: true;
157
+ keys: string[];
158
+ } | {
159
+ ok: false;
160
+ stderr: string;
161
+ }): RemoteKeychainWriteVerification;
162
+ /**
163
+ * The actionable error message for a failed keychain-over-SSH push verification.
164
+ * Names the cause (locked remote login keychain) and steers to the two real fixes:
165
+ * re-run with the headless-readable file backend, or unlock the remote keychain.
166
+ * Pure + exported so the exact guidance is asserted in tests.
167
+ */
168
+ export declare function keychainWriteFailureMessage(host: string, bundle: string, reason: string): string;
169
+ /**
170
+ * Read a bundle back from a remote over SSH (headlessly, so it fails fast rather
171
+ * than prompting Touch ID) and confirm the pushed keys materialized. Drives the
172
+ * remote's own `secrets export <bundle> --plaintext --format json` — the same read
173
+ * a headless release performs — but keeps only the KEY NAMES; the plaintext values
174
+ * are dropped immediately and never retained or logged. Returns a verification
175
+ * verdict; the caller renders `keychainWriteFailureMessage` on failure.
176
+ */
177
+ export declare function verifyRemoteKeychainPush(target: string, bundle: string, pushedKeys: string[], opts?: {
178
+ osLookupName?: string;
179
+ }): RemoteKeychainWriteVerification;
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import { sshExec, sshStream, assertValidSshTarget } from '../ssh-exec.js';
19
19
  import { resolveHost } from '../hosts/registry.js';
20
- import { emit } from '../events.js';
20
+ import { emitSecretAudit } from './audit.js';
21
21
  import { sshTargetFor } from '../hosts/types.js';
22
22
  import { buildRemoteAgentsInvocation } from '../hosts/remote-cmd.js';
23
23
  import { resolveRemoteOsSync } from '../hosts/remote-os.js';
@@ -74,8 +74,11 @@ export async function resolveHostSshTarget(nameOrAlias) {
74
74
  return nameOrAlias;
75
75
  }
76
76
  /**
77
- * Merge `--host <single>` and `--hosts <a,b,c>` into an ordered, de-duplicated
78
- * list. Both flags compose; either alone works. Empty when neither is set.
77
+ * Merge `--host <single>` / `--hosts <a,b,c>` (and their `--device` / `--devices`
78
+ * aliases) into an ordered, de-duplicated list. All four flags compose; any alone
79
+ * works. `--device`/`--devices` resolve identically to `--host`/`--hosts` so the
80
+ * fleet-wide `--device` vocabulary (see `agents activity`, `agents run --device`)
81
+ * works on the secrets remote commands too. Empty when none is set.
79
82
  */
80
83
  export function parseHostsOption(opts) {
81
84
  const out = [];
@@ -89,9 +92,14 @@ export function parseHostsOption(opts) {
89
92
  };
90
93
  if (opts.host)
91
94
  push(opts.host);
95
+ if (opts.device)
96
+ push(opts.device);
92
97
  if (opts.hosts)
93
98
  for (const h of opts.hosts.split(','))
94
99
  push(h);
100
+ if (opts.devices)
101
+ for (const h of opts.devices.split(','))
102
+ push(h);
95
103
  return out;
96
104
  }
97
105
  /**
@@ -203,8 +211,8 @@ export async function remoteResolveEnv(target, bundle, opts = {}) {
203
211
  // event on the INITIATING host too (values were pulled into this process and
204
212
  // injected locally). Covers `secrets exec --host` and `run --secrets b@host`.
205
213
  // Values never enter the payload — only the bundle, target host, and count.
206
- emit('secrets.get', {
207
- module: 'secrets',
214
+ emitSecretAudit({
215
+ event: 'secrets.get',
208
216
  bundle,
209
217
  operation: 'remote resolve',
210
218
  source: 'remote',
@@ -214,3 +222,129 @@ export async function remoteResolveEnv(target, bundle, opts = {}) {
214
222
  });
215
223
  return env;
216
224
  }
225
+ /**
226
+ * The remote's headless read-back raises one of these when a keychain-backed bundle
227
+ * has metadata but no readable value items — the exact locked-login-keychain
228
+ * signature. Anything else (connection refused, timeout, host key error) is a
229
+ * transient/unrelated failure and must NOT be mislabeled as a locked keychain.
230
+ */
231
+ function isLockedKeychainReadBackError(stderr) {
232
+ const s = stderr.toLowerCase();
233
+ return (
234
+ // bundles.ts agentOnly guard: "…is not unlocked in the secrets agent…"
235
+ s.includes('not unlocked') ||
236
+ s.includes('secrets agent') ||
237
+ // resolveBundleEnv: "Bundle '<b>' key '<k>': stored item '<item>' not found."
238
+ s.includes('stored item') ||
239
+ s.includes('not found'));
240
+ }
241
+ /**
242
+ * Decide whether a keychain-backed push to a remote actually PERSISTED its secret
243
+ * value items, given the read-back of that bundle from the remote's own store.
244
+ *
245
+ * The silent-failure this guards: pushing `--remote-backend keychain` (default) to
246
+ * a macOS host over headless SSH lands the bundle METADATA but not readable value
247
+ * items — the remote login keychain is locked in the non-interactive SSH context,
248
+ * so Security accepts the item WRITE at the DB level but the biometry-ACL'd item is
249
+ * unreadable, and the remote `import` still reports success (values written first,
250
+ * metadata `noAcl` last — bundles.ts writeBundleWithItems). The metadata-only bundle
251
+ * then fails every later read with the confusing `Bundle '<b>' key '<k>': stored
252
+ * item '<item>' not found` (bundles.ts resolveBundleEnv). We catch it by reading
253
+ * the bundle back the same way a release will (`secrets export --plaintext --format
254
+ * json`, driven headlessly on the remote so its `agentOnly` guard FAILS FAST before
255
+ * any keychain read — no Touch ID prompt) and confirming every pushed key returned.
256
+ *
257
+ * Pure so both branches are unit-testable without a real locked keychain: inject the
258
+ * "read-back failed / key absent" condition through `readBack`.
259
+ */
260
+ export function evaluateKeychainWriteVerification(pushedKeys, readBack) {
261
+ if (!readBack.ok) {
262
+ const stderr = readBack.stderr.trim();
263
+ if (isLockedKeychainReadBackError(stderr)) {
264
+ // The remote's own headless read raised the not-unlocked / not-found signal —
265
+ // exactly the confusing error the user hits later. Surface it now, at push
266
+ // time, with the fix.
267
+ return {
268
+ ok: false,
269
+ kind: 'locked-keychain',
270
+ reason: `the remote could not read it back${stderr ? ` (${stderr})` : ''}`,
271
+ };
272
+ }
273
+ // A transient / unrelated failure (flaky SSH, timeout, bad payload). Re-surface
274
+ // verbatim — do NOT diagnose a locked keychain from a connection error.
275
+ return {
276
+ ok: false,
277
+ kind: 'error',
278
+ reason: stderr || 'read-back failed',
279
+ };
280
+ }
281
+ const present = new Set(readBack.keys);
282
+ const missing = pushedKeys.filter((k) => !present.has(k));
283
+ if (missing.length > 0) {
284
+ // Read-back succeeded but some pushed keys are absent — the value items didn't
285
+ // persist. Same locked-keychain cause and fix.
286
+ return {
287
+ ok: false,
288
+ kind: 'locked-keychain',
289
+ reason: `${missing.length} of ${pushedKeys.length} key(s) did not persist on the remote ` +
290
+ `(missing: ${missing.slice(0, 5).join(', ')}${missing.length > 5 ? ', …' : ''})`,
291
+ };
292
+ }
293
+ return { ok: true };
294
+ }
295
+ /**
296
+ * The actionable error message for a failed keychain-over-SSH push verification.
297
+ * Names the cause (locked remote login keychain) and steers to the two real fixes:
298
+ * re-run with the headless-readable file backend, or unlock the remote keychain.
299
+ * Pure + exported so the exact guidance is asserted in tests.
300
+ */
301
+ export function keychainWriteFailureMessage(host, bundle, reason) {
302
+ return (`${host}: pushed '${bundle}' but the keychain items did not persist — ${reason}. ` +
303
+ `A macOS login keychain is LOCKED under headless SSH, so a keychain-backed write ` +
304
+ `lands the bundle metadata but no readable secret items, and later reads fail with ` +
305
+ `"stored item '…' not found". Re-run with a headless-readable backend:\n` +
306
+ ` agents secrets export ${bundle} --host ${host} --remote-backend file\n` +
307
+ `(needs AGENTS_SECRETS_PASSPHRASE set locally), or unlock the remote keychain first ` +
308
+ `(e.g. an interactive login / \`agents secrets unlock\` on ${host}) and retry.`);
309
+ }
310
+ /**
311
+ * Read a bundle back from a remote over SSH (headlessly, so it fails fast rather
312
+ * than prompting Touch ID) and confirm the pushed keys materialized. Drives the
313
+ * remote's own `secrets export <bundle> --plaintext --format json` — the same read
314
+ * a headless release performs — but keeps only the KEY NAMES; the plaintext values
315
+ * are dropped immediately and never retained or logged. Returns a verification
316
+ * verdict; the caller renders `keychainWriteFailureMessage` on failure.
317
+ */
318
+ export function verifyRemoteKeychainPush(target, bundle, pushedKeys, opts = {}) {
319
+ const remoteCmd = buildRemoteAgentsInvocation(['secrets', 'export', bundle, '--plaintext', '--format', 'json'], undefined, osForTarget(target, opts.osLookupName));
320
+ const res = sshExec(target, remoteCmd, { timeoutMs: REMOTE_TIMEOUT_MS });
321
+ if (res.code !== 0) {
322
+ const why = res.timedOut ? 'timed out' : res.code === null ? 'ssh failed' : `exit ${res.code}`;
323
+ const stderr = `${why}${(res.stderr || res.stdout || '').trim() ? `: ${(res.stderr || res.stdout).trim()}` : ''}`;
324
+ return evaluateKeychainWriteVerification(pushedKeys, { ok: false, stderr });
325
+ }
326
+ // Take the outer { … } object (tolerate login-shell banner noise), read the key
327
+ // names, and immediately discard the values — we only need presence here.
328
+ const raw = res.stdout;
329
+ const start = raw.indexOf('{');
330
+ const end = raw.lastIndexOf('}');
331
+ const jsonText = start >= 0 && end >= start ? raw.slice(start, end + 1) : raw.trim();
332
+ let keys;
333
+ try {
334
+ const parsed = JSON.parse(jsonText);
335
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
336
+ return evaluateKeychainWriteVerification(pushedKeys, {
337
+ ok: false,
338
+ stderr: 'unexpected read-back payload',
339
+ });
340
+ }
341
+ keys = Object.keys(parsed);
342
+ }
343
+ catch {
344
+ return evaluateKeychainWriteVerification(pushedKeys, {
345
+ ok: false,
346
+ stderr: 'could not parse read-back JSON',
347
+ });
348
+ }
349
+ return evaluateKeychainWriteVerification(pushedKeys, { ok: true, keys });
350
+ }
@@ -14,7 +14,7 @@ import * as crypto from 'crypto';
14
14
  import { deleteKeychainToken, getKeychainToken, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from './index.js';
15
15
  import { readBundle, writeBundle, keychainItemsForBundle, validateBundleName, } from './bundles.js';
16
16
  import { rushSyncBackend } from './drivers/rush.js';
17
- import { emit } from '../events.js';
17
+ import { emitSecretAudit } from './audit.js';
18
18
  // PBKDF2 cost. 600k SHA-256 iters matches OWASP 2023+ guidance and keeps a
19
19
  // passphrase prompt under a second on the hardware the CLI targets.
20
20
  const PBKDF2_ITER = 600_000;
@@ -190,8 +190,8 @@ export async function pushBundle(name, opts) {
190
190
  // bundle off-machine — the most sensitive read there is. It bypasses
191
191
  // readAndResolveBundleEnv, so audit it explicitly. Values never enter the
192
192
  // payload; only the bundle name and how many keys were read.
193
- emit('secrets.get', {
194
- module: 'secrets',
193
+ emitSecretAudit({
194
+ event: 'secrets.get',
195
195
  bundle: name,
196
196
  operation: 'sync push',
197
197
  source: 'sync-push',
@@ -1,11 +1,20 @@
1
1
  import type { CloudTaskStatus } from '../cloud/types.js';
2
2
  import { type PidSessionEntry } from './pid-registry.js';
3
3
  import { type HookSessionIndex } from './hook-sessions.js';
4
- import { type SessionActivity, type AwaitingReason, type StructuredQuestion, type TodoProgress, type DetectedPr, type DetectedWorktree, type DetectedTicket } from './state.js';
5
- import type { SessionAttachment } from './types.js';
4
+ import { type SessionState, type SessionActivity, type AwaitingReason, type StructuredQuestion, type TodoProgress, type DetectedPr, type DetectedWorktree, type DetectedTicket } from './state.js';
5
+ import { type SessionAttachment } from './types.js';
6
6
  import { type SessionProvenance } from './provenance.js';
7
7
  import { type DeviceRegistry } from '../devices/registry.js';
8
8
  import { type Presence } from './detached.js';
9
+ /**
10
+ * The owner (actor id) to show for a session in `--active`. Prefers the actor
11
+ * recorded on the live-attribution source (the pid registry / teammate record),
12
+ * but falls back to the durable per-session actor sidecar — written at spawn and,
13
+ * unlike the pid entry, NOT overwritten by the SessionStart hook's own by-pid
14
+ * write. Without this fallback a real `agents run` shows no owner whenever the
15
+ * hook's actor-less entry wins the by-pid file (RUSH-2018 fix).
16
+ */
17
+ export declare function resolveOwner(pidActor: string | null | undefined, sessionId: string | undefined): string | undefined;
9
18
  /**
10
19
  * Per-PID `lsof` probes run bounded and staggered rather than as one parallel
11
20
  * fan-out: a simultaneous system-wide `lsof` burst reads to behavioral EDR
@@ -15,14 +24,27 @@ import { type Presence } from './detached.js';
15
24
  export declare const LSOF_CONCURRENCY = 4;
16
25
  export type ActiveContext = 'terminal' | 'teams' | 'cloud' | 'headless';
17
26
  /**
18
- * `unknown` = the process is alive but we cannot introspect what it is doing —
19
- * a live harness whose transcript format we do not parse (everything but
20
- * claude/codex), or a resolvable transcript whose `stat` momentarily failed. It
21
- * is NOT a synonym for idle: idle is a positive "not mid-turn, not waiting on
22
- * you" conclusion drawn from a readable transcript; unknown is the honest "we
23
- * can't tell", which we refuse to fake as idle.
27
+ * Every status here is COMPUTED by the framework from observable signals PID
28
+ * liveness and transcript last-write (mtime) never self-reported by the agent.
29
+ *
30
+ * - `running` / `idle` / `input_required` a live process's working / stopped
31
+ * / waiting-on-you activity, from its transcript (see {@link computeLiveSignals}).
32
+ * - `queued` — dispatched, work in the pipeline (cloud/headless launch).
33
+ * - `closed` — the PID is dead. The process has exited; report that, don't
34
+ * fabricate `idle` ("done, waiting for you") for a process that is simply gone.
35
+ * - `abandoned` — no transcript write in {@link ABANDONED_STALE_MS} (days): the
36
+ * session is dangling, whether its PID is dead (long gone) or still alive but
37
+ * stuck making no progress. A soft signal — it clears the moment it writes again.
38
+ * - `unknown` — the residual: genuinely NO signal (no PID info AND no file).
39
+ *
40
+ * A LIVE, fresh process is never `unknown` — "the process is alive" is itself a
41
+ * positive signal, so an opaque/unparseable live harness resolves to `running`
42
+ * (its honest floor), and every tracked harness with a locatable, parseable
43
+ * transcript (claude, codex, grok, droid, rush, gemini, kimi, hermes, opencode,
44
+ * antigravity) gets a real working/waiting/idle from its own parser — see
45
+ * {@link computeLiveSignals}, {@link lifecycleStatus} and {@link resolveFallbackStatus}.
24
46
  */
25
- export type ActiveStatus = 'running' | 'idle' | 'queued' | 'input_required' | 'unknown';
47
+ export type ActiveStatus = 'running' | 'idle' | 'queued' | 'input_required' | 'closed' | 'abandoned' | 'unknown';
26
48
  export interface ActiveSession {
27
49
  context: ActiveContext;
28
50
  kind: string;
@@ -109,6 +131,14 @@ export interface ActiveSession {
109
131
  * Absent for cloud sessions (no local pid) and any pid whose env is unreadable.
110
132
  */
111
133
  provenance?: SessionProvenance;
134
+ /**
135
+ * Who initiated this session — the resolved actor id stamped at spawn
136
+ * (`resolveActor().id`, read back from the pid registry / teammate record).
137
+ * A tailnet login/email for a resolved human, `UNRESOLVED@<host>` when it
138
+ * couldn't be determined, absent when the launch predates actor stamping.
139
+ * Surfaced as the owner column in `--active` (RUSH-2018).
140
+ */
141
+ owner?: string;
112
142
  /**
113
143
  * The machine this session runs on, as a normalized device id (machineId()
114
144
  * form). Set when merging cross-machine results so the grouped `--active`
@@ -172,6 +202,16 @@ export interface ActiveQueryOptions {
172
202
  * transcript just because a GUI app service with the same basename is alive.
173
203
  */
174
204
  export declare const ACTIVE_SESSION_STALE_MS: number;
205
+ /**
206
+ * A session whose transcript hasn't been written in this long is ABANDONED /
207
+ * dangling — the framework can no longer treat it as live work, whether its PID
208
+ * is dead (long gone) or still alive but making no progress (a hung agent).
209
+ * Two days: past a normal work gap, well short of a session legitimately kept
210
+ * open across a long weekend. Deliberately far wider than
211
+ * {@link ACTIVE_SESSION_STALE_MS} (which bounds transcript-borrowing, a different
212
+ * concern) — this is the lifecycle threshold, not the freshness window.
213
+ */
214
+ export declare const ABANDONED_STALE_MS: number;
175
215
  /**
176
216
  * Resolve an agent kind from a process's reported executable. `comm` may be an
177
217
  * absolute path (shim-launched agents), and Windows image names carry an
@@ -219,30 +259,74 @@ export declare function sessionFileTimes(sessionFile: string | undefined): {
219
259
  birthtimeMs?: number;
220
260
  mtimeMs?: number;
221
261
  };
262
+ /**
263
+ * The lifecycle status computed purely from the two hard signals the framework
264
+ * always has — PID liveness and transcript last-write (mtime) — independent of
265
+ * anything the agent self-reports or what its last transcript turn happened to
266
+ * look like. Returns the definitive lifecycle label when one applies, or
267
+ * `undefined` when the process is live and fresh (so the caller uses the richer
268
+ * activity-derived status instead).
269
+ *
270
+ * - No write in {@link ABANDONED_STALE_MS} ⇒ `abandoned` (dangling): a
271
+ * days-stale session is not live work, whether its PID is dead (long gone)
272
+ * or still alive but stuck. Checked first — it outranks a bare `closed`.
273
+ * - Otherwise, PID dead ⇒ `closed`: the process has exited. This is the fix
274
+ * for the old "dead PID reports idle" lie — `idle` reads as "done, waiting
275
+ * for you", but a dead process is done, period.
276
+ * - Otherwise (alive + fresh) ⇒ `undefined`: defer to the activity engine.
277
+ */
278
+ export declare function lifecycleStatus(pidAlive: boolean, mtimeMs: number | undefined, nowMs?: number): ActiveStatus | undefined;
222
279
  /**
223
280
  * The ONE place a fallback status is decided when no rich transcript state is
224
- * available — a non-Claude/Codex kind we cannot parse, or a Claude/Codex tail
225
- * that was empty or unreadable. Honest by construction: it never asserts a status
226
- * it cannot justify from a measured signal.
281
+ * available — an opaque kind we cannot parse (cursor, openclaw), or a transcript
282
+ * whose parse/tail was empty or unreadable. Honest by construction: computed from
283
+ * PID + mtime, never a fabricated `idle`.
227
284
  *
228
- * - Resolvable transcript, readable mtime the MEASURED freshness signal:
229
- * written within ACTIVE_MTIME_WINDOW_MS `running`, else `idle`.
230
- * - Resolvable transcript whose `stat` throws (file vanished / permission) we
231
- * genuinely cannot tell `unknown`. (This branch previously returned
232
- * `running`, which contradicted the `idle` default one branch up.)
233
- * - No resolvable transcript but the process is alive → alive-but-opaque
234
- * `unknown`. This is the truthful answer for a live gemini / droid / cursor /
235
- * opencode whose format we don't parse NOT a fabricated `idle` (which the
236
- * UI reads as "done and waiting"), and it never lies as `running` either.
237
- * - No transcript and the process is not known alive → nothing to report ⇒ `idle`.
285
+ * - Dead PID, or a days-stale transcript {@link lifecycleStatus} (`closed` /
286
+ * `abandoned`). A dead process is `closed`, not the old fabricated `idle`
287
+ * (which the UI reads as "done, waiting for you"); a dead process whose file
288
+ * also vanished is still `closed` (death is a definitive answer, not `unknown`).
289
+ * - Live + fresh ⇒ `running`. A live process is, at minimum, running: we may
290
+ * not see WHAT an opaque harness (or an empty tail) is doing, but the process
291
+ * being alive is a positive signal never a blank `unknown` (the old blanket
292
+ * bug for every non-claude/codex live agent) nor a downgraded `idle`.
238
293
  */
239
- export declare function resolveFallbackStatus(sessionFile: string | undefined, pidAlive: boolean): ActiveStatus;
294
+ export declare function resolveFallbackStatus(sessionFile: string | undefined, pidAlive: boolean, nowMs?: number): ActiveStatus;
240
295
  /**
241
- * Locate the live transcript for an agent process. Claude files are keyed by
242
- * cwd (+ optional session uuid); Codex files are date-partitioned, so we resolve
243
- * the newest indexed Codex session for the cwd instead.
296
+ * Locate the live transcript for an agent process. Claude files are keyed by cwd
297
+ * (+ optional session uuid), so they resolve straight off disk. Every OTHER
298
+ * tracked harness Codex (date-partitioned), plus grok / droid / rush / gemini /
299
+ * kimi / hermes / opencode / antigravity (per-session dirs, SQLite, single-JSON)
300
+ * — is resolved through the session index by cwd: the newest indexed transcript
301
+ * for that cwd, bounded by ACTIVE_SESSION_STALE_MS so a live pid never borrows a
302
+ * weeks-old transcript. This is what lets a live NON-claude/codex agent get a real
303
+ * status instead of falling through to `unknown` (the file feeds
304
+ * {@link computeLiveSignals}). An opaque kind we don't track (cursor) still yields
305
+ * undefined here and degrades honestly to a live `running`.
244
306
  */
245
307
  export declare function findSessionFileForKind(kind: string, cwd?: string, sessionId?: string): string | undefined;
308
+ /** Live per-session signals derived from one transcript-tail read. */
309
+ interface LiveSignals {
310
+ /** Rich inferred state (activity/preview/badges). Undefined when unreadable. */
311
+ state?: SessionState;
312
+ /** Rolling output-token throughput (tokens/sec). Undefined when no usage in the tail. */
313
+ tokPerSec?: number;
314
+ }
315
+ /**
316
+ * Derive the inferred state (working / waiting / idle + preview/badges) and, for
317
+ * the two harnesses whose raw lines carry it, the output-token throughput.
318
+ *
319
+ * Claude/Codex take the fast bounded byte-tail ({@link readSessionTailWithRaw}) —
320
+ * the hot path, and the only two that also yield throughput (their raw lines
321
+ * carry usage the event model drops). EVERY OTHER tracked harness (grok, droid,
322
+ * rush, gemini, kimi, hermes, opencode, antigravity) is parsed with its own
323
+ * parser and run through the SAME {@link inferSessionState}, so a live
324
+ * non-claude/codex agent gets a real working/waiting/idle instead of the blanket
325
+ * `unknown` it used to fall through to. An opaque/untracked kind (cursor) or an
326
+ * unreadable/empty transcript yields an empty signal set, and the caller's
327
+ * {@link resolveFallbackStatus} reports the honest live floor (`running`).
328
+ */
329
+ export declare function computeLiveSignals(kind: string, sessionFile: string | undefined, cwd: string | undefined, pidAlive: boolean): LiveSignals;
246
330
  /** Live teams teammates. Reuses AgentManager which already polls PIDs via `kill -0`. */
247
331
  export declare function listTeamsActive(): Promise<ActiveSession[]>;
248
332
  /** Live editor-terminal agents across every IDE window. */