@phnx-labs/agents-cli 1.22.14 → 1.22.16

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 (76) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +7 -5
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/cloud.js +4 -6
  5. package/dist/commands/exec.js +96 -11
  6. package/dist/commands/humans.d.ts +10 -0
  7. package/dist/commands/humans.js +91 -0
  8. package/dist/commands/packages.js +90 -22
  9. package/dist/commands/projects.d.ts +2 -5
  10. package/dist/commands/projects.js +92 -65
  11. package/dist/commands/resume.d.ts +17 -0
  12. package/dist/commands/resume.js +80 -0
  13. package/dist/commands/routines.js +53 -71
  14. package/dist/commands/sessions.d.ts +13 -4
  15. package/dist/commands/sessions.js +37 -19
  16. package/dist/commands/setup-watchdog.d.ts +3 -0
  17. package/dist/commands/setup-watchdog.js +54 -0
  18. package/dist/commands/setup.js +8 -1
  19. package/dist/commands/ssh.d.ts +11 -0
  20. package/dist/commands/ssh.js +18 -4
  21. package/dist/commands/watchdog.d.ts +3 -4
  22. package/dist/commands/watchdog.js +40 -28
  23. package/dist/index.js +3 -2
  24. package/dist/lib/auto-dispatch.d.ts +8 -10
  25. package/dist/lib/auto-dispatch.js +20 -42
  26. package/dist/lib/channels/send.d.ts +4 -1
  27. package/dist/lib/channels/send.js +15 -7
  28. package/dist/lib/devices/doctor-findings.d.ts +14 -0
  29. package/dist/lib/devices/doctor-findings.js +92 -51
  30. package/dist/lib/exec.d.ts +6 -2
  31. package/dist/lib/exec.js +22 -4
  32. package/dist/lib/hooks.d.ts +4 -2
  33. package/dist/lib/hooks.js +173 -23
  34. package/dist/lib/humans.d.ts +25 -0
  35. package/dist/lib/humans.js +65 -0
  36. package/dist/lib/memory.js +2 -1
  37. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  38. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  39. package/dist/lib/migrate.d.ts +1 -1
  40. package/dist/lib/migrate.js +116 -7
  41. package/dist/lib/notify.js +6 -2
  42. package/dist/lib/permissions.js +12 -10
  43. package/dist/lib/project-import.d.ts +7 -46
  44. package/dist/lib/project-import.js +9 -108
  45. package/dist/lib/projects.d.ts +20 -3
  46. package/dist/lib/projects.js +36 -18
  47. package/dist/lib/registry.d.ts +1 -1
  48. package/dist/lib/registry.js +11 -0
  49. package/dist/lib/routine-activation.d.ts +14 -0
  50. package/dist/lib/routine-activation.js +78 -0
  51. package/dist/lib/routines-project.d.ts +1 -1
  52. package/dist/lib/routines-project.js +3 -9
  53. package/dist/lib/routines.d.ts +3 -1
  54. package/dist/lib/routines.js +32 -19
  55. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  56. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  57. package/dist/lib/secrets/bundles.d.ts +11 -0
  58. package/dist/lib/secrets/bundles.js +122 -5
  59. package/dist/lib/session/actor-sidecar.d.ts +5 -2
  60. package/dist/lib/session/actor-sidecar.js +4 -2
  61. package/dist/lib/session/db.d.ts +2 -1
  62. package/dist/lib/session/db.js +19 -3
  63. package/dist/lib/session/types.d.ts +4 -0
  64. package/dist/lib/staleness/writers/sources.d.ts +6 -1
  65. package/dist/lib/staleness/writers/sources.js +93 -7
  66. package/dist/lib/startup/command-registry.d.ts +2 -0
  67. package/dist/lib/startup/command-registry.js +5 -0
  68. package/dist/lib/state.d.ts +2 -0
  69. package/dist/lib/state.js +23 -10
  70. package/dist/lib/types.d.ts +64 -1
  71. package/dist/lib/versions.js +48 -14
  72. package/dist/lib/watchdog/rotate.d.ts +1 -1
  73. package/dist/lib/watchdog/rotate.js +1 -1
  74. package/package.json +1 -1
  75. package/dist/lib/watchdog/routine.d.ts +0 -44
  76. package/dist/lib/watchdog/routine.js +0 -69
@@ -1,3 +1,4 @@
1
+ import { getOwnerNotifyFromHumans } from '../humans.js';
1
2
  import { registerBuiltinProviders } from './providers/index.js';
2
3
  import { resolveTransport } from './resolve.js';
3
4
  const OWNER_ALIAS = 'owner';
@@ -16,8 +17,14 @@ export function composeSendText(text, urls) {
16
17
  return body;
17
18
  return body ? `${body}\n${extra.join('\n')}` : extra.join('\n');
18
19
  }
19
- /** Read notify.owner; null when either field is missing. */
20
+ /**
21
+ * Read the owner destination; humans.yaml is the primary source, agents.yaml
22
+ * notify.owner is the fallback. Returns null when neither is set.
23
+ */
20
24
  export function readOwnerDest(meta) {
25
+ const humansOwner = getOwnerNotifyFromHumans();
26
+ if (humansOwner)
27
+ return humansOwner;
21
28
  const owner = meta.notify?.owner;
22
29
  const channel = owner?.channel?.trim();
23
30
  const to = owner?.to?.trim();
@@ -48,15 +55,16 @@ export function resolveSendEnvelope(input, meta) {
48
55
  };
49
56
  }
50
57
  // Owner defaults fill only missing fields (and expand the bare "owner" alias).
51
- // Explicit --channel/--to always win; a complete notify.owner is NOT required
52
- // when both flags are already set (same merge-then-require shape as main).
53
- const ownerCfg = meta.notify?.owner;
54
- const ownerChannel = ownerCfg?.channel?.trim() || '';
55
- const ownerTo = ownerCfg?.to?.trim() || '';
58
+ // humans.yaml is the primary source; notify.owner in agents.yaml is the
59
+ // fallback for the migration window. Explicit --channel/--to always win.
56
60
  let channel = (input.channel ?? '').trim();
57
61
  let to = (input.to ?? '').trim();
58
62
  const usedOwnerAlias = isOwnerAlias(to);
59
63
  if (input.ownerMode || usedOwnerAlias) {
64
+ const humansOwner = getOwnerNotifyFromHumans();
65
+ const fallbackOwner = meta.notify?.owner;
66
+ const ownerChannel = humansOwner?.channel ?? fallbackOwner?.channel?.trim() ?? '';
67
+ const ownerTo = humansOwner?.to ?? fallbackOwner?.to?.trim() ?? '';
60
68
  if (!channel)
61
69
  channel = ownerChannel;
62
70
  if (!to || usedOwnerAlias)
@@ -64,7 +72,7 @@ export function resolveSendEnvelope(input, meta) {
64
72
  }
65
73
  if (!channel || !to) {
66
74
  const hint = input.ownerMode || usedOwnerAlias
67
- ? 'Set notify.owner.{channel,to} in agents.yaml, or pass --channel and --to explicitly.'
75
+ ? 'Set notify.{channel,to} in humans.yaml (or notify.owner in agents.yaml), or pass --channel and --to explicitly.'
68
76
  : 'Need --channel and --to (or --to owner with notify.owner configured). ' +
69
77
  'Example: agents send --channel desktop --to local --text "hi"';
70
78
  return { ok: false, error: hint };
@@ -8,7 +8,21 @@ import type { FleetDivergence, FleetVersionSignIn } from './fleet-divergence.js'
8
8
  export type FindingSeverity = 'critical' | 'warning';
9
9
  /** A machine-stable class for a finding — drives {@link remediationFor} and lets
10
10
  * the JSON consumer group by kind. */
11
+ /** Every finding class. Severity is NOT annotated here — {@link FINDING_SEVERITY}
12
+ * below owns it, and a second copy in these comments is a fourth place to drift. */
11
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"];
14
+ /**
15
+ * The severity each kind is emitted with - the SINGLE source of truth, read by
16
+ * the builders below and asserted against both prose rubrics by
17
+ * `doctor-findings.test.ts`.
18
+ *
19
+ * It exists because severity drifted: the rubrics claimed `never-synced` and
20
+ * `duplicate-hook-drift` were critical for three days after RUSH-2162 downgraded
21
+ * them, and the exhaustiveness test missed it because it only checked that a kind
22
+ * was *named* in the rubric, not which bucket it sat in. Change a severity HERE
23
+ * and the test names the docs that must move with it.
24
+ */
25
+ export declare const FINDING_SEVERITY: Record<FindingKind, FindingSeverity>;
12
26
  /** A machine-stable class for a finding. Derived from the runtime list above so
13
27
  * the rubric test can enumerate every kind. */
14
28
  export type FindingKind = typeof ALL_FINDING_KINDS[number];
@@ -18,13 +18,14 @@
18
18
  * with. Keep this list exhaustive; a kind missing from it is a doc that lies.
19
19
  * CRITICAL — logged-out (provable) · missing-hook · missing-plugin ·
20
20
  * unwired-hook (a hook on disk that settings.json never fires) ·
21
- * never-synced WHEN the version's declared resources are absent ·
22
- * duplicate-hook-drift (copies that DIFFER) · cli-missing.
21
+ * cli-missing.
23
22
  * WARNING — logout-unprovable (hedged) · missing-resource · content-drift ·
24
- * stale · never-synced when the version declares nothing to miss ·
25
- * repo-behind · repo-drift · version-skew · fleet-resource-gap ·
26
- * orphan · duplicate-hook (identical copies) · host-cli-missing ·
27
- * host-cli-invalid · rc-secret-export · exec-policy · stale-cli.
23
+ * never-synced · stale · repo-behind · repo-drift · version-skew ·
24
+ * fleet-resource-gap · orphan · duplicate-hook ·
25
+ * duplicate-hook-drift · host-cli-missing · host-cli-invalid ·
26
+ * rc-secret-export · exec-policy · stale-cli.
27
+ * (RUSH-2162 moved never-synced and duplicate-hook-drift to WARNING: both are
28
+ * stale-sync states one `agents sync` resolves, not "needs you now".)
28
29
  *
29
30
  * This module is pure: it maps already-collected signals (drift rows, orphan
30
31
  * rows, repo-behind markers, per-version resource diffs, cross-device divergence,
@@ -70,31 +71,71 @@ function loginShape(agent) {
70
71
  }
71
72
  /** A machine-stable class for a finding — drives {@link remediationFor} and lets
72
73
  * the JSON consumer group by kind. */
74
+ /** Every finding class. Severity is NOT annotated here — {@link FINDING_SEVERITY}
75
+ * below owns it, and a second copy in these comments is a fourth place to drift. */
73
76
  export const ALL_FINDING_KINDS = [
74
- 'logged-out', // provable per-version logout (CRITICAL)
75
- 'logout-unprovable', // credential absent but not provable (WARNING)
76
- 'missing-hook', // a declared hook absent from a version home (CRITICAL)
77
- 'missing-plugin', // a declared plugin absent from a version home (CRITICAL)
78
- 'unwired-hook', // hook present on disk but not wired into settings.json (CRITICAL)
79
- 'cli-missing', // a managed agent whose binary won't resolve (CRITICAL)
80
- 'missing-resource', // a missing command/skill/rule/mcp/permission/subagent (WARNING)
81
- 'content-drift', // a resource diverged from source (WARNING)
82
- 'never-synced', // installed but never synced — CRITICAL when its declared
83
- // resources are therefore absent, WARNING when it declares none
84
- 'stale', // sources changed since last sync (WARNING)
85
- 'repo-behind', // a config repo behind origin (WARNING)
86
- 'repo-drift', // a config repo diverged from the fleet baseline (WARNING)
87
- 'fleet-resource-gap', // a resource in another box's central repos, absent here (WARNING)
88
- 'host-cli-missing', // a declared host CLI not installed on this box (WARNING)
89
- 'host-cli-invalid', // a host-CLI manifest that failed to parse (WARNING)
90
- 'version-skew', // an agent version present elsewhere, absent here (WARNING)
91
- 'orphan', // orphan resources in a version home (WARNING)
92
- 'duplicate-hook', // one hook materialized in several version homes, byte-identical (WARNING)
93
- 'duplicate-hook-drift', // …with differing content, so a stale copy can disagree (CRITICAL)
94
- 'rc-secret-export', // credential-shaped export in a shell rc file (WARNING)
95
- 'exec-policy', // Windows execution policy blocks agents.ps1 (WARNING)
77
+ 'logged-out', // provable per-version logout
78
+ 'logout-unprovable', // credential absent but not provable
79
+ 'missing-hook', // a declared hook absent from a version home
80
+ 'missing-plugin', // a declared plugin absent from a version home
81
+ 'unwired-hook', // hook present on disk but not wired into settings.json
82
+ 'cli-missing', // a managed agent whose binary won't resolve
83
+ 'missing-resource', // a missing command/skill/rule/mcp/permission/subagent
84
+ 'content-drift', // a resource diverged from source
85
+ 'never-synced', // installed but never synced
86
+ 'stale', // sources changed since last sync
87
+ 'repo-behind', // a config repo behind origin
88
+ 'repo-drift', // a config repo diverged from the fleet baseline
89
+ 'fleet-resource-gap', // a resource in another box's central repos, absent here
90
+ 'host-cli-missing', // a declared host CLI not installed on this box
91
+ 'host-cli-invalid', // a host-CLI manifest that failed to parse
92
+ 'version-skew', // an agent version present elsewhere, absent here
93
+ 'orphan', // orphan resources in a version home
94
+ 'duplicate-hook', // one hook materialized in several version homes, byte-identical
95
+ 'duplicate-hook-drift', // …with differing content, so a stale copy can disagree
96
+ 'rc-secret-export', // credential-shaped export in a shell rc file
97
+ 'exec-policy', // Windows execution policy blocks agents.ps1
96
98
  'stale-cli',
97
99
  ];
100
+ /**
101
+ * The severity each kind is emitted with - the SINGLE source of truth, read by
102
+ * the builders below and asserted against both prose rubrics by
103
+ * `doctor-findings.test.ts`.
104
+ *
105
+ * It exists because severity drifted: the rubrics claimed `never-synced` and
106
+ * `duplicate-hook-drift` were critical for three days after RUSH-2162 downgraded
107
+ * them, and the exhaustiveness test missed it because it only checked that a kind
108
+ * was *named* in the rubric, not which bucket it sat in. Change a severity HERE
109
+ * and the test names the docs that must move with it.
110
+ */
111
+ export const FINDING_SEVERITY = {
112
+ // Needs you now: the harness cannot do its job until this is fixed.
113
+ 'logged-out': 'critical',
114
+ 'missing-hook': 'critical',
115
+ 'missing-plugin': 'critical',
116
+ 'unwired-hook': 'critical',
117
+ 'cli-missing': 'critical',
118
+ // Everything else is resolvable by a routine sync/cleanup and does not block
119
+ // the harness right now. RUSH-2162 moved never-synced and duplicate-hook-drift
120
+ // here: both are stale-sync states that one `agents sync` resolves.
121
+ 'logout-unprovable': 'warning',
122
+ 'missing-resource': 'warning',
123
+ 'content-drift': 'warning',
124
+ 'never-synced': 'warning',
125
+ 'stale': 'warning',
126
+ 'repo-behind': 'warning',
127
+ 'repo-drift': 'warning',
128
+ 'fleet-resource-gap': 'warning',
129
+ 'version-skew': 'warning',
130
+ 'orphan': 'warning',
131
+ 'duplicate-hook': 'warning',
132
+ 'duplicate-hook-drift': 'warning',
133
+ 'host-cli-missing': 'warning',
134
+ 'host-cli-invalid': 'warning',
135
+ 'rc-secret-export': 'warning',
136
+ 'exec-policy': 'warning',
137
+ 'stale-cli': 'warning',
138
+ };
98
139
  function agentName(agent) {
99
140
  return AGENT_NAMES[agent] || agent;
100
141
  }
@@ -241,7 +282,7 @@ export function buildLocalFindings(input) {
241
282
  // cli-missing (managed agent, binary broken) — critical.
242
283
  for (const agent of input.cliMissing ?? []) {
243
284
  out.push(finding({
244
- severity: 'critical', kind: 'cli-missing', device, agent,
285
+ severity: FINDING_SEVERITY['cli-missing'], kind: 'cli-missing', device, agent,
245
286
  message: `${agentName(agent)} binary not found`,
246
287
  }));
247
288
  }
@@ -254,20 +295,20 @@ export function buildLocalFindings(input) {
254
295
  if (w?.supported) {
255
296
  if (w.settingsMissing) {
256
297
  out.push(finding({
257
- severity: 'critical', kind: 'unwired-hook', device, agent, version,
298
+ severity: FINDING_SEVERITY['unwired-hook'], kind: 'unwired-hook', device, agent, version,
258
299
  message: `settings.json missing — ${w.expected ?? 0} declared hook${(w.expected ?? 0) === 1 ? '' : 's'} never fire`,
259
300
  }));
260
301
  }
261
302
  else if (w.settingsUnparseable) {
262
303
  out.push(finding({
263
- severity: 'critical', kind: 'unwired-hook', device, agent, version,
304
+ severity: FINDING_SEVERITY['unwired-hook'], kind: 'unwired-hook', device, agent, version,
264
305
  message: `settings.json unparseable — hook wiring can't be verified`,
265
306
  }));
266
307
  }
267
308
  else {
268
309
  for (const u of w.unwired) {
269
310
  out.push(finding({
270
- severity: 'critical', kind: 'unwired-hook', device, agent, version,
311
+ severity: FINDING_SEVERITY['unwired-hook'], kind: 'unwired-hook', device, agent, version,
271
312
  message: `hook '${u.name}' present on disk but not wired into settings.json`,
272
313
  }));
273
314
  }
@@ -317,17 +358,17 @@ export function buildLocalFindings(input) {
317
358
  missingPlugins.length ? `${missingPlugins.length} plugin${missingPlugins.length === 1 ? '' : 's'}` : '',
318
359
  ].filter(Boolean).join(', ');
319
360
  out.push(finding({
320
- severity: 'warning', kind: 'never-synced', device, agent, version,
361
+ severity: FINDING_SEVERITY['never-synced'], kind: 'never-synced', device, agent, version,
321
362
  message: `never synced — ${total} resource${total === 1 ? '' : 's'}${breakdown ? ` (incl. ${breakdown})` : ''} not installed`,
322
363
  }));
323
364
  }
324
365
  }
325
366
  else {
326
367
  // Synced-but-drifted: one line per kind of gap on this version.
327
- emitGroup(out, missingHooks, 'critical', 'missing-hook', device, agent, version, 'hook', 'missing');
328
- emitGroup(out, missingPlugins, 'critical', 'missing-plugin', device, agent, version, 'plugin', 'missing');
329
- emitGroup(out, missingOther, 'warning', 'missing-resource', device, agent, version, 'resource', 'missing');
330
- emitGroup(out, drifted, 'warning', 'content-drift', device, agent, version, 'resource', 'drifted');
368
+ emitGroup(out, missingHooks, FINDING_SEVERITY['missing-hook'], 'missing-hook', device, agent, version, 'hook', 'missing');
369
+ emitGroup(out, missingPlugins, FINDING_SEVERITY['missing-plugin'], 'missing-plugin', device, agent, version, 'plugin', 'missing');
370
+ emitGroup(out, missingOther, FINDING_SEVERITY['missing-resource'], 'missing-resource', device, agent, version, 'resource', 'missing');
371
+ emitGroup(out, drifted, FINDING_SEVERITY['content-drift'], 'content-drift', device, agent, version, 'resource', 'drifted');
331
372
  if (missingHooks.length + missingPlugins.length + missingOther.length + drifted.length > 0) {
332
373
  detailedVersions.add(`${agent}@${version}`);
333
374
  }
@@ -343,7 +384,7 @@ export function buildLocalFindings(input) {
343
384
  if (detailedVersions.has(`${row.agent}@${row.version}`))
344
385
  continue;
345
386
  out.push(finding({
346
- severity: 'warning', kind: 'stale', device, agent: row.agent, version: row.version,
387
+ severity: FINDING_SEVERITY['stale'], kind: 'stale', device, agent: row.agent, version: row.version,
347
388
  message: 'sources changed since last sync',
348
389
  }));
349
390
  }
@@ -355,7 +396,7 @@ export function buildLocalFindings(input) {
355
396
  Object.values(rep.kinds).some((rows) => rows.some((r) => r.status === 'missing')));
356
397
  if (!hadCritical) {
357
398
  out.push(finding({
358
- severity: 'warning', kind: 'never-synced', device, agent: row.agent, version: row.version,
399
+ severity: FINDING_SEVERITY['never-synced'], kind: 'never-synced', device, agent: row.agent, version: row.version,
359
400
  message: 'installed but never synced',
360
401
  }));
361
402
  }
@@ -369,7 +410,7 @@ export function buildLocalFindings(input) {
369
410
  const stales = input.syncRows.filter((r) => r.status === 'stale').length;
370
411
  const staleNote = stales > 0 ? ` → stales ${stales} version${stales === 1 ? '' : 's'}` : '';
371
412
  out.push(finding({
372
- severity: 'warning', kind: 'repo-behind', device, version: m.alias,
413
+ severity: FINDING_SEVERITY['repo-behind'], kind: 'repo-behind', device, version: m.alias,
373
414
  message: `${m.behind} behind ${m.branch}${staleNote}`,
374
415
  }));
375
416
  }
@@ -383,7 +424,7 @@ export function buildLocalFindings(input) {
383
424
  const missingClis = (input.hostClis?.statuses ?? []).filter((c) => !c.installed).map((c) => c.name);
384
425
  if (missingClis.length > 0) {
385
426
  out.push({
386
- severity: 'warning', kind: 'host-cli-missing', device,
427
+ severity: FINDING_SEVERITY['host-cli-missing'], kind: 'host-cli-missing', device,
387
428
  message: missingClis.length === 1
388
429
  ? `host CLI '${missingClis[0]}' declared but not installed`
389
430
  : `${missingClis.length} declared host CLIs not installed (${missingClis.slice(0, 2).join(', ')}${missingClis.length > 2 ? ', …' : ''})`,
@@ -399,7 +440,7 @@ export function buildLocalFindings(input) {
399
440
  // row per bad file, since each needs its own edit.
400
441
  for (const e of input.hostClis?.errors ?? []) {
401
442
  out.push(finding({
402
- severity: 'warning', kind: 'host-cli-invalid', device,
443
+ severity: FINDING_SEVERITY['host-cli-invalid'], kind: 'host-cli-invalid', device,
403
444
  message: `host-CLI manifest ${e.file} could not be read: ${e.reason}`,
404
445
  }));
405
446
  }
@@ -432,7 +473,7 @@ function orphanFinding(device, rows) {
432
473
  ? `${affected[0].agent}@${affected[0].version}`
433
474
  : `${affected.length} versions`;
434
475
  return [finding({
435
- severity: 'warning', kind: 'orphan', device,
476
+ severity: FINDING_SEVERITY['orphan'], kind: 'orphan', device,
436
477
  message: `${total} orphaned resource${total === 1 ? '' : 's'} on ${where} (cleanup only)`,
437
478
  })];
438
479
  }
@@ -472,7 +513,7 @@ function duplicateHookFindings(device, dups) {
472
513
  // stale/sync drift, not a missing hook. Resolvable by one sync; a WARNING,
473
514
  // not a "needs you now" critical. (A genuinely MISSING hook stays critical
474
515
  // via the missing-hook path.)
475
- severity: 'warning',
516
+ severity: FINDING_SEVERITY[drift ? 'duplicate-hook-drift' : 'duplicate-hook'],
476
517
  kind: drift ? 'duplicate-hook-drift' : 'duplicate-hook',
477
518
  device, agent, versions, message,
478
519
  remediation: `agents sync ${agent}@all --yes`,
@@ -516,7 +557,7 @@ function rcSecretFindings(device, rc) {
516
557
  ? 'agents secrets add, then delete the rc line'
517
558
  : `agents secrets add once per export (${n}), then delete each rc line`;
518
559
  out.push({
519
- severity: 'warning', kind: 'rc-secret-export', device,
560
+ severity: FINDING_SEVERITY['rc-secret-export'], kind: 'rc-secret-export', device,
520
561
  message: `${what} (${examples}) — readable by any same-user process`,
521
562
  remediation,
522
563
  });
@@ -537,7 +578,7 @@ function execPolicyFinding(device, execPolicy) {
537
578
  if (!blocksLocalScripts(execPolicy.policy))
538
579
  return null;
539
580
  return finding({
540
- severity: 'warning', kind: 'exec-policy', device,
581
+ severity: FINDING_SEVERITY['exec-policy'], kind: 'exec-policy', device,
541
582
  message: `PowerShell execution policy is ${execPolicy.policy} — it blocks the generated agents.ps1 launcher (agents.cmd still works)`,
542
583
  });
543
584
  }
@@ -630,14 +671,14 @@ export function signInToFindings(device, signIn) {
630
671
  continue;
631
672
  if (row.provable) {
632
673
  out.push(finding({
633
- severity: 'critical', kind: 'logged-out', device, agent, version: row.version,
674
+ severity: FINDING_SEVERITY['logged-out'], kind: 'logged-out', device, agent, version: row.version,
634
675
  account: row.account ?? null,
635
676
  message: 'logged out — no account signed in',
636
677
  }));
637
678
  }
638
679
  else {
639
680
  out.push(finding({
640
- severity: 'warning', kind: 'logout-unprovable', device, agent, version: row.version,
681
+ severity: FINDING_SEVERITY['logout-unprovable'], kind: 'logout-unprovable', device, agent, version: row.version,
641
682
  account: row.account ?? null,
642
683
  message: 'could not verify sign-in',
643
684
  }));
@@ -662,14 +703,14 @@ export function fleetDivergenceToFindings(divergences, baseline) {
662
703
  case 'agent-version-missing-remote':
663
704
  case 'agent-version-missing-local':
664
705
  out.push(finding({
665
- severity: 'warning', kind: 'version-skew', device: laggingDevice,
706
+ severity: FINDING_SEVERITY['version-skew'], kind: 'version-skew', device: laggingDevice,
666
707
  agent: d.category, version: d.name,
667
708
  message: 'not installed (present elsewhere in the fleet)',
668
709
  }));
669
710
  break;
670
711
  case 'repo-drift':
671
712
  out.push(finding({
672
- severity: 'warning', kind: 'repo-drift', device: laggingDevice,
713
+ severity: FINDING_SEVERITY['repo-drift'], kind: 'repo-drift', device: laggingDevice,
673
714
  // `category` is the repo ('agents' | 'system'); carry it as the alias
674
715
  // `agents repo pull` expects — ~/.agents is the `user` repo.
675
716
  version: d.category === 'system' ? 'system' : 'user',
@@ -679,7 +720,7 @@ export function fleetDivergenceToFindings(divergences, baseline) {
679
720
  case 'resource-missing-remote':
680
721
  case 'resource-missing-local':
681
722
  out.push(finding({
682
- severity: 'warning', kind: 'fleet-resource-gap', device: laggingDevice,
723
+ severity: FINDING_SEVERITY['fleet-resource-gap'], kind: 'fleet-resource-gap', device: laggingDevice,
683
724
  message: `${d.category.replace(/s$/, '')} '${d.name}' missing (present elsewhere)`,
684
725
  }));
685
726
  break;
@@ -254,10 +254,14 @@ export interface AgentCommandTemplate {
254
254
  * { subcommand } — replace the headless base subcommand with `<subcommand> <id>`
255
255
  * (codex: `codex exec` -> `codex exec resume <id>`)
256
256
  */
257
- resume?: {
257
+ resume?: ({
258
258
  flag: string;
259
+ interactiveFlag?: string;
260
+ headlessFlag?: string;
259
261
  } | {
260
262
  subcommand: string;
263
+ }) & {
264
+ since?: string;
261
265
  };
262
266
  }
263
267
  /**
@@ -272,7 +276,7 @@ export declare const AGENT_COMMANDS: Record<AgentId, AgentCommandTemplate>;
272
276
  * command template's `resume` field — the single source of truth. Agents that
273
277
  * return false resume via the universal Tier-2 `/continue` replay instead.
274
278
  */
275
- export declare function nativeResume(agent: AgentId): boolean;
279
+ export declare function nativeResume(agent: AgentId, version?: string): boolean;
276
280
  /**
277
281
  * Build the `-c` value that adds `dir` to codex's workspace-write writable
278
282
  * roots. Codex parses the value as TOML, so it's a single-element TOML array of
package/dist/lib/exec.js CHANGED
@@ -11,7 +11,7 @@ import * as path from 'path';
11
11
  import { ALL_MODES } from './types.js';
12
12
  import { AGENTS } from './agents.js';
13
13
  import { parseTimeout } from './routines.js';
14
- import { getBinaryPath, getVersionHomePath, isVersionInstalled, resolveVersion } from './versions.js';
14
+ import { compareVersions, getBinaryPath, getVersionHomePath, isVersionInstalled, resolveVersion } from './versions.js';
15
15
  import { resolveModel, buildReasoningFlags } from './models.js';
16
16
  import { isTierToken, resolveTier } from './model-tiers.js';
17
17
  import { createTimer, redactPrompt, redactArgs } from './events.js';
@@ -385,6 +385,12 @@ export function buildExecEnv(options) {
385
385
  result.AGENTS_PARENT_SESSION_ID = spawnerSessionId;
386
386
  }
387
387
  result.AGENTS_RUNTIME = resolveInteractive(options) ? 'terminal' : 'headless';
388
+ // Durable SessionStart metadata. The hook joins these launch facts to the
389
+ // harness-provided real session id and writes them under the shared history
390
+ // directory, so a later resume can restore the permission boundary without
391
+ // re-parsing harness-specific transcripts.
392
+ result.AGENTS_RUN_MODE = resolveHeadlessMode(options.agent, normalizeMode(options.mode), resolveInteractive(options), options.modeWarningContext, options.modeWarningState);
393
+ result.AGENTS_HISTORY_DIR = getHistoryDir();
388
394
  // So activity / feed posts stamp the right harness without re-detecting.
389
395
  if (options.agent) {
390
396
  result.AGENTS_AGENT_NAME = options.agent;
@@ -466,6 +472,7 @@ export const AGENT_COMMANDS = {
466
472
  },
467
473
  jsonFlags: ['--output-format', 'stream-json'],
468
474
  modelFlag: '--model',
475
+ resume: { flag: '--resume', since: '2026.7.23' },
469
476
  },
470
477
  opencode: {
471
478
  base: ['opencode', 'run'],
@@ -589,6 +596,7 @@ export const AGENT_COMMANDS = {
589
596
  },
590
597
  jsonFlags: ['--output-format', 'streaming-json'],
591
598
  modelFlag: '--model',
599
+ resume: { flag: '--resume', since: '0.2.91' },
592
600
  },
593
601
  kimi: {
594
602
  base: ['kimi'],
@@ -601,6 +609,7 @@ export const AGENT_COMMANDS = {
601
609
  },
602
610
  jsonFlags: ['--output-format', 'stream-json'],
603
611
  modelFlag: '--model',
612
+ resume: { flag: '--session', since: '0.19.2' },
604
613
  },
605
614
  // Factory AI Droid (`droid exec` for headless, `droid` for TUI). Flags from
606
615
  // docs.factory.ai CLI reference: prompt is positional; --auto low|medium|high
@@ -618,6 +627,7 @@ export const AGENT_COMMANDS = {
618
627
  },
619
628
  jsonFlags: ['-o', 'stream-json'],
620
629
  modelFlag: '-m',
630
+ resume: { flag: '--resume', headlessFlag: '--session-id', since: '0.186.0' },
621
631
  },
622
632
  hermes: {
623
633
  base: ['hermes', 'chat'],
@@ -633,8 +643,13 @@ export const AGENT_COMMANDS = {
633
643
  * command template's `resume` field — the single source of truth. Agents that
634
644
  * return false resume via the universal Tier-2 `/continue` replay instead.
635
645
  */
636
- export function nativeResume(agent) {
637
- return AGENT_COMMANDS[agent]?.resume !== undefined;
646
+ export function nativeResume(agent, version) {
647
+ const resume = AGENT_COMMANDS[agent]?.resume;
648
+ if (!resume)
649
+ return false;
650
+ if (!resume.since)
651
+ return true;
652
+ return !!version && compareVersions(version, resume.since) >= 0;
638
653
  }
639
654
  /**
640
655
  * Build the `-c` value that adds `dir` to codex's workspace-write writable
@@ -822,7 +837,10 @@ export function buildExecCommand(options) {
822
837
  // `resume`, the legacy claude-only `--session-id` CREATES a session with that id.
823
838
  if (options.resume && options.sessionId && resumeSpec) {
824
839
  if ('flag' in resumeSpec) {
825
- cmd.push(resumeSpec.flag, options.sessionId);
840
+ const flag = interactive
841
+ ? (resumeSpec.interactiveFlag ?? resumeSpec.flag)
842
+ : (resumeSpec.headlessFlag ?? resumeSpec.flag);
843
+ cmd.push(flag, options.sessionId);
826
844
  }
827
845
  else {
828
846
  cmd.push(options.sessionId);
@@ -58,8 +58,10 @@ export interface DuplicateVersionHook {
58
58
  export declare function inspectDuplicateVersionHooks(cwd?: string): DuplicateVersionHook[];
59
59
  /**
60
60
  * List hook entries in a single directory, grouping script + data files by
61
- * basename. Exported so doctor-diff can reuse the same grouping the sync path
62
- * applies; without this, doctor would double-count `foo.sh` and `foo.yaml`.
61
+ * basename. Also discovers scripts in one-level group subdirs
62
+ * (hooks/session-starts/). Exported so doctor-diff can reuse the same grouping
63
+ * the sync path applies; without this, doctor would double-count `foo.sh` and
64
+ * `foo.yaml`. On basename collision, the top-level file wins over a nested one.
63
65
  */
64
66
  export declare function listHookEntriesFromDir(dir: string): HookEntry[];
65
67
  /**