@phnx-labs/agents-cli 1.22.6 → 1.22.8

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 (104) hide show
  1. package/CHANGELOG.md +131 -1
  2. package/README.md +7 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/browser.js +61 -0
  5. package/dist/commands/exec.js +83 -16
  6. package/dist/commands/feed.d.ts +2 -1
  7. package/dist/commands/feed.js +47 -20
  8. package/dist/commands/focus.js +22 -1
  9. package/dist/commands/harness.d.ts +0 -1
  10. package/dist/commands/harness.js +60 -4
  11. package/dist/commands/models.js +2 -2
  12. package/dist/commands/monitors.js +2 -2
  13. package/dist/commands/projects.js +122 -107
  14. package/dist/commands/routines.js +2 -2
  15. package/dist/commands/run-account-picker.js +2 -0
  16. package/dist/commands/secrets.js +1 -1
  17. package/dist/commands/sessions-backfill.d.ts +33 -0
  18. package/dist/commands/sessions-backfill.js +83 -1
  19. package/dist/commands/sessions-stats.d.ts +36 -0
  20. package/dist/commands/sessions-stats.js +263 -0
  21. package/dist/commands/sessions.d.ts +1 -1
  22. package/dist/commands/sessions.js +21 -1
  23. package/dist/commands/snapshot.d.ts +11 -0
  24. package/dist/commands/snapshot.js +107 -0
  25. package/dist/commands/teams.js +2 -1
  26. package/dist/commands/view.d.ts +9 -1
  27. package/dist/commands/view.js +63 -12
  28. package/dist/index.js +4 -2
  29. package/dist/lib/activity.js +2 -2
  30. package/dist/lib/agents.js +68 -11
  31. package/dist/lib/analytics/recipes.js +11 -5
  32. package/dist/lib/browser/ipc.js +2 -0
  33. package/dist/lib/browser/profiles.d.ts +15 -7
  34. package/dist/lib/browser/profiles.js +53 -12
  35. package/dist/lib/browser/remote-control.d.ts +35 -0
  36. package/dist/lib/browser/remote-control.js +48 -0
  37. package/dist/lib/browser/service.d.ts +19 -0
  38. package/dist/lib/browser/service.js +19 -1
  39. package/dist/lib/browser/types.d.ts +14 -2
  40. package/dist/lib/byok-usage.d.ts +38 -0
  41. package/dist/lib/byok-usage.js +117 -0
  42. package/dist/lib/capabilities.js +1 -1
  43. package/dist/lib/device-config.js +8 -0
  44. package/dist/lib/exec.d.ts +20 -0
  45. package/dist/lib/exec.js +73 -8
  46. package/dist/lib/feed-outcome.d.ts +1 -0
  47. package/dist/lib/feed-outcome.js +2 -0
  48. package/dist/lib/feed-post.js +1 -1
  49. package/dist/lib/feed-ranking.d.ts +1 -0
  50. package/dist/lib/feed-ranking.js +4 -0
  51. package/dist/lib/feed.d.ts +4 -1
  52. package/dist/lib/feed.js +25 -0
  53. package/dist/lib/hosts/passthrough.d.ts +10 -1
  54. package/dist/lib/hosts/passthrough.js +23 -3
  55. package/dist/lib/hosts/remote-cmd.js +1 -0
  56. package/dist/lib/mcp.js +6 -1
  57. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  58. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  59. package/dist/lib/model-tiers.js +4 -1
  60. package/dist/lib/models.js +63 -0
  61. package/dist/lib/placement.d.ts +82 -0
  62. package/dist/lib/placement.js +188 -0
  63. package/dist/lib/profiles.d.ts +73 -10
  64. package/dist/lib/profiles.js +133 -15
  65. package/dist/lib/project-focus.d.ts +9 -0
  66. package/dist/lib/project-focus.js +23 -0
  67. package/dist/lib/project-key.d.ts +1 -1
  68. package/dist/lib/project-key.js +1 -1
  69. package/dist/lib/project-probe.d.ts +18 -0
  70. package/dist/lib/project-probe.js +46 -0
  71. package/dist/lib/project-status.d.ts +56 -0
  72. package/dist/lib/project-status.js +125 -18
  73. package/dist/lib/resources/mcp.js +3 -0
  74. package/dist/lib/resources/types.d.ts +1 -1
  75. package/dist/lib/rotate.d.ts +19 -4
  76. package/dist/lib/rotate.js +24 -1
  77. package/dist/lib/routines.d.ts +2 -0
  78. package/dist/lib/runner.d.ts +3 -0
  79. package/dist/lib/runner.js +90 -7
  80. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  81. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  82. package/dist/lib/secrets/audit.js +1 -1
  83. package/dist/lib/secrets/index.d.ts +1 -0
  84. package/dist/lib/secrets/index.js +29 -15
  85. package/dist/lib/secrets/remote.d.ts +1 -1
  86. package/dist/lib/secrets/remote.js +1 -1
  87. package/dist/lib/session/active.d.ts +2 -0
  88. package/dist/lib/session/bash-command.d.ts +2 -3
  89. package/dist/lib/session/db.d.ts +92 -1
  90. package/dist/lib/session/db.js +230 -1
  91. package/dist/lib/session/digest.d.ts +1 -1
  92. package/dist/lib/session/digest.js +1 -1
  93. package/dist/lib/share/publish.js +24 -0
  94. package/dist/lib/snapshot.d.ts +103 -0
  95. package/dist/lib/snapshot.js +99 -0
  96. package/dist/lib/startup/command-registry.d.ts +1 -1
  97. package/dist/lib/startup/command-registry.js +2 -2
  98. package/dist/lib/subagents-registry.js +3 -0
  99. package/dist/lib/types.d.ts +11 -2
  100. package/dist/lib/usage.d.ts +5 -0
  101. package/dist/lib/usage.js +3 -3
  102. package/package.json +1 -1
  103. package/dist/commands/activity.d.ts +0 -87
  104. package/dist/commands/activity.js +0 -346
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { ensureFeedPublishHook, listAskStats, listBlocks, recordNotified, buildDeclaredBlock, publishBlock, } from '../lib/feed.js';
3
3
  import { ensureActivityLogHook, readRecentActivity, formatActivityLine, formatProgressUpdate, mergeActivityEvents, parseActivityPayload, } from '../lib/activity.js';
4
+ import { projectKeyFromCwd } from '../lib/project-key.js';
4
5
  import { postFeedStatus } from '../lib/feed-post.js';
5
6
  import { parseFeedPostLevel, planFeedBroadcast, runFeedBroadcast, effectiveBroadcastConfig, blockBroadcastContext, blockDeliveryFailure, } from '../lib/feed-broadcast.js';
6
7
  import { getSessionById } from '../lib/session/db.js';
@@ -215,6 +216,7 @@ export function sessionHintsFromActive(sessions) {
215
216
  prNumber: s.pr?.number,
216
217
  prUrl: s.pr?.url,
217
218
  worktreeSlug: s.worktree?.slug,
219
+ project: s.cwd ? projectKeyFromCwd(s.cwd) : undefined,
218
220
  }));
219
221
  }
220
222
  export function registerFeedCommand(program) {
@@ -228,6 +230,7 @@ export function registerFeedCommand(program) {
228
230
  .option('--local', 'Only this machine -- skip the cross-machine SSH fan-out')
229
231
  .option('-H, --host <target...>', 'Scope to remote machine(s) over SSH; repeatable')
230
232
  .option('--device <target...>', 'Alias for --host; repeatable')
233
+ .option('--project <name>', 'Scope the feed to one project/repo (matches cwd basename, case-insensitive)')
231
234
  .option('--dispatch', 'Run stall suppression + default-on-no-answer policy and urgent notifications')
232
235
  .option('--pause <id>', 'Pause a runaway/needy local process (SIGSTOP) or cancel a cloud task')
233
236
  .option('--kill <id>', 'Kill a runaway/needy local process (SIGTERM) or cancel a cloud task');
@@ -317,7 +320,13 @@ docs/06-observability.md.
317
320
  // it the ask would scroll away like any other update.
318
321
  let outcomes;
319
322
  if (flags.blocked) {
320
- const block = buildDeclaredBlock(event, {
323
+ const block = buildDeclaredBlock({
324
+ sessionId: event.sessionId,
325
+ mailboxId: event.mailboxId,
326
+ host: event.host,
327
+ runtime: event.runtime,
328
+ cwd: event.cwd,
329
+ }, {
321
330
  // Prefer title as the front-loaded ask on the phone; body is detail.
322
331
  text: event.title
323
332
  ? (event.detail ? `${event.title}: ${event.detail}` : event.title)
@@ -399,12 +408,12 @@ docs/06-observability.md.
399
408
  if (filter === 'all') {
400
409
  console.log();
401
410
  renderUpdatesView(await gatherStatusPosts({
402
- limit: UPDATES_VIEW_LIMIT, hosts: opts.host, local: opts.local, includeLocal, self,
403
- }));
411
+ limit: UPDATES_VIEW_LIMIT, hosts: opts.host, local: opts.local, includeLocal, self, project: opts.project,
412
+ }), opts.project);
404
413
  return;
405
414
  }
406
415
  if (includeLocal)
407
- renderActivityLane();
416
+ renderActivityLane(opts.project);
408
417
  };
409
418
  // Updates view: deliberate progress posts only (blocks are decisions, not
410
419
  // announcements). Short-circuits the block pipeline — no dispatch policy —
@@ -425,7 +434,7 @@ docs/06-observability.md.
425
434
  console.log(JSON.stringify(updates, null, 2));
426
435
  return;
427
436
  }
428
- renderUpdatesView(updates);
437
+ renderUpdatesView(updates, opts.project);
429
438
  return;
430
439
  }
431
440
  // Active sessions feed both the GC sweep and outcome enrichment (ticket/PR).
@@ -487,7 +496,10 @@ docs/06-observability.md.
487
496
  blocks = mergeFeedBlocks(visibleLocalBlocks, remote.items);
488
497
  }
489
498
  }
490
- blocks = rankFeedBlocks(blocks, localSignals);
499
+ blocks = rankFeedBlocks(blocks, localSignals).filter((b) => blockMatchesProject(b, opts.project));
500
+ const dispatchBlocksProject = opts.project
501
+ ? dispatchBlocks.filter((b) => blockMatchesProject(b, opts.project))
502
+ : dispatchBlocks;
491
503
  const digest = suppressionDigest(preparedLocal.filter);
492
504
  if (digest && !opts.json) {
493
505
  console.log(chalk.dim(digest));
@@ -495,7 +507,7 @@ docs/06-observability.md.
495
507
  if (opts.dispatch) {
496
508
  const policy = loadPolicy();
497
509
  const now = new Date();
498
- for (const b of dispatchBlocks) {
510
+ for (const b of dispatchBlocksProject) {
499
511
  // Wrap per-block policy so one malformed block (e.g. a crafted
500
512
  // mailboxId that throws in mailboxDir) can't abort the whole loop and
501
513
  // strand every remaining block's dispatch.
@@ -540,7 +552,7 @@ docs/06-observability.md.
540
552
  }
541
553
  // Shared fleet-comms masthead (same family as `agents mailboxes`).
542
554
  console.log(masthead({
543
- title: 'they need you',
555
+ title: opts.project ? `${opts.project} needs you` : 'they need you',
544
556
  accent: 'amber',
545
557
  host: self,
546
558
  right: formatFeedMastheadRight(blocks),
@@ -630,6 +642,18 @@ export function resolveFeedFilter(raw) {
630
642
  return 'all';
631
643
  return 'needs';
632
644
  }
645
+ /** True when a block belongs to the requested project (case-insensitive). */
646
+ function blockMatchesProject(block, project) {
647
+ if (!project)
648
+ return true;
649
+ return (block.project ?? '').toLowerCase() === project.toLowerCase();
650
+ }
651
+ /** True when an activity event belongs to the requested project (case-insensitive). */
652
+ function eventMatchesProject(ev, project) {
653
+ if (!project)
654
+ return true;
655
+ return ((ev.project ?? projectKeyFromCwd(ev.cwd) ?? '')).toLowerCase() === project.toLowerCase();
656
+ }
633
657
  /**
634
658
  * Render one activity event in the lane: deliberate progress posts
635
659
  * (`status.posted`) use the rich multi-line {@link formatProgressUpdate}; hook
@@ -668,30 +692,33 @@ function readStatusPosts(limit) {
668
692
  * uses; `--local` (or the no-fanout env guard on a peer) keeps it to this box.
669
693
  */
670
694
  async function gatherStatusPosts(opts) {
671
- const local = opts.includeLocal ? readStatusPosts(opts.limit) : [];
695
+ const local = opts.includeLocal
696
+ ? readStatusPosts(opts.limit).filter((ev) => eventMatchesProject(ev, opts.project))
697
+ : [];
672
698
  const forceLocal = opts.local === true || process.env[FEED_NO_FANOUT_ENV] === '1';
673
699
  if (forceLocal)
674
- return local;
700
+ return local.slice(0, opts.limit);
675
701
  const remoteHosts = opts.hosts?.length ? remoteFeedHostsToDial(opts.hosts, opts.self) : undefined;
676
702
  if (opts.hosts?.length && (!remoteHosts || remoteHosts.length === 0))
677
- return local;
703
+ return local.slice(0, opts.limit);
678
704
  const remote = await gatherRemoteAgentsJson({
679
705
  args: ['feed', '--filter', 'updates', '--json'],
680
706
  noFanoutEnv: FEED_NO_FANOUT_ENV,
681
707
  hosts: remoteHosts,
682
708
  parse: parseActivityPayload,
683
709
  });
684
- return mergeActivityEvents(local, remote.items).slice(0, opts.limit);
710
+ const merged = mergeActivityEvents(local, remote.items).filter((ev) => eventMatchesProject(ev, opts.project));
711
+ return merged.slice(0, opts.limit);
685
712
  }
686
713
  /**
687
714
  * Render the **Updates** view: deliberate progress posts only (`status.posted`),
688
715
  * recency-ordered, with rich identity chips. Pure `file.edited` / git-hook noise
689
716
  * is excluded so operators see announcements, not tool churn.
690
717
  */
691
- function renderUpdatesView(updates) {
718
+ function renderUpdatesView(updates, project) {
692
719
  const hosts = new Set(updates.map((e) => e.host).filter(Boolean));
693
720
  console.log(masthead({
694
- title: 'updates',
721
+ title: project ? `${project} updates` : 'updates',
695
722
  accent: 'cyan',
696
723
  host: hosts.size > 1 ? `${hosts.size} machines` : (updates[0]?.host ?? machineId()),
697
724
  right: `${updates.length} post${updates.length === 1 ? '' : 's'}`,
@@ -712,16 +739,16 @@ function renderUpdatesView(updates) {
712
739
  * activity logs. Read-only tail of the logs -- no transcript re-parsing. Silent
713
740
  * when empty.
714
741
  */
715
- function renderActivityLane(limit = 6) {
742
+ function renderActivityLane(project, limit = 6) {
716
743
  const events = readRecentActivity({
717
744
  sinceMs: Date.now() - 24 * 60 * 60 * 1000,
718
- limit,
745
+ limit: limit * (project ? 4 : 1),
719
746
  tier: 'milestone',
720
- });
747
+ }).filter((ev) => eventMatchesProject(ev, project));
721
748
  if (events.length === 0)
722
749
  return;
723
- console.log(chalk.bold('\n recent activity'));
724
- for (const ev of events)
750
+ console.log(chalk.bold(project ? `\n recent activity · ${project}` : '\n recent activity'));
751
+ for (const ev of events.slice(0, limit))
725
752
  renderActivityEntry(ev);
726
- console.log(chalk.gray(' → agents activity for the full stream'));
753
+ console.log(chalk.gray(' → agents feed --project ' + (project ?? '<project>') + ' for the full stream'));
727
754
  }
@@ -20,8 +20,9 @@ import { runOnPeer } from '../lib/session/remote-list.js';
20
20
  import { discoverSessions } from '../lib/session/discover.js';
21
21
  import { openSurfaces, currentContext, availableBackends, detectCurrentBackend, } from '../lib/terminal/index.js';
22
22
  import { isInteractiveTerminal } from './utils.js';
23
+ import { setHelpSections } from '../lib/help.js';
23
24
  export function registerFocusCommand(program) {
24
- program
25
+ const cmd = program
25
26
  .command('focus')
26
27
  .argument('[id]', 'Short/full session id to focus; omit for an interactive picker')
27
28
  .option('--local', 'Only this machine (skip the cross-host sweep)')
@@ -30,6 +31,26 @@ export function registerFocusCommand(program) {
30
31
  .action(async (id, opts) => {
31
32
  await focusAction(id, opts);
32
33
  });
34
+ setHelpSections(cmd, {
35
+ examples: `
36
+ # Jump to a live session (attach pane/tab, or open a new tab and resume)
37
+ agents sessions focus a1b2c3d4
38
+
39
+ # Attach only — refuse if nothing is joinable (old sessions go)
40
+ agents sessions focus a1b2c3d4 --attach-only
41
+
42
+ # Pick from live sessions on this machine only
43
+ agents sessions focus --local
44
+ `,
45
+ notes: `
46
+ Lifecycle siblings (not synonyms):
47
+ focus live jump (default "take me there")
48
+ focus --attach-only attach only; never fork (replaces go)
49
+ detach / attach interactive ↔ headless presence
50
+ resume multi-select history → tabs
51
+ run --resume single scripted continue
52
+ `,
53
+ });
33
54
  }
34
55
  /**
35
56
  * Which fallback fires when a session has no attach rail. `--attach-only` (the old
@@ -24,7 +24,6 @@ export interface ForkOptions {
24
24
  baseUrl?: string;
25
25
  authProvider?: string;
26
26
  version?: string;
27
- label?: string;
28
27
  description?: string;
29
28
  keyStdin?: boolean;
30
29
  force?: boolean;
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import chalk from 'chalk';
14
14
  import { addProfile, ensureProviderToken } from './profiles.js';
15
- import { listProfiles, readProfile, writeProfile, deleteProfile, profileExists, profileHostLabel, profileProviderLabel, profileModelLabel, profileAuthLabel, profileLabel, forkProfile, profileFromHostModel, authEnvKeyForHost, getProfilePath, validateProfileName, } from '../lib/profiles.js';
15
+ import { listProfiles, readProfile, writeProfile, deleteProfile, profileExists, profileHostLabel, profileProviderLabel, profileModelLabel, profileAuthLabel, profileLabel, forkProfile, profileFromHostModel, authEnvKeyForHost, getProfilePath, validateProfileName, editProfile, renameProfile, } from '../lib/profiles.js';
16
16
  import { listPresets } from '../lib/profiles-presets.js';
17
17
  import { AGENTS, ALL_AGENT_IDS, resolveAgentName } from '../lib/agents.js';
18
18
  /** Short capability summary for a native harness — its supported run modes. */
@@ -58,7 +58,6 @@ export function buildFork(source, name, opts) {
58
58
  baseUrl: opts.baseUrl,
59
59
  provider: opts.authProvider,
60
60
  version: opts.version,
61
- label: opts.label,
62
61
  description: opts.description,
63
62
  });
64
63
  }
@@ -75,7 +74,6 @@ export function buildFork(source, name, opts) {
75
74
  baseUrl: opts.baseUrl,
76
75
  provider: opts.authProvider,
77
76
  authEnvVar: opts.authProvider ? authEnvKeyForHostOrThrow(host) : undefined,
78
- label: opts.label,
79
77
  description: opts.description ?? `Forked from ${host}: ${opts.model}`,
80
78
  });
81
79
  }
@@ -147,7 +145,6 @@ Examples:
147
145
  .option('--base-url <url>', 'Custom endpoint base URL (claude/codex hosts)')
148
146
  .option('--auth-provider <provider>', 'Attach a keychain-backed API key under this provider')
149
147
  .option('--version <version>', 'Pin the host CLI version (e.g., 1.16.0)')
150
- .option('--label <text>', 'Human-facing name shown by `agents view` (defaults to <name>)')
151
148
  .option('--description <text>', 'One-line description')
152
149
  .option('--key-stdin', 'Read the API key from stdin instead of prompting (for scripts/CI)')
153
150
  .option('--force', 'Overwrite an existing harness with the same name')
@@ -249,4 +246,63 @@ Examples:
249
246
  }
250
247
  console.log(chalk.green(`Harness '${name}' removed.`));
251
248
  });
249
+ cmd
250
+ .command('edit <name>')
251
+ .description('Edit a custom harness in place (model, endpoint, host version, description).')
252
+ // Flags mirror ForkProfileOptions exactly — `editProfile` reuses
253
+ // `forkProfile`'s override logic, so anything it does not accept would be a
254
+ // flag that parses and does nothing. There is deliberately no --label: the
255
+ // header `agents view` prints is derived from the harness name.
256
+ .option('--model <id>', 'Swap the pinned model (e.g., meta/muse-spark-2.0)')
257
+ .option('--base-url <url>', 'Set or replace the endpoint base URL (claude/codex hosts)')
258
+ .option('--version <ver>', 'Re-pin the host CLI version. Empty string unpins (tracks latest).')
259
+ .option('--description <text>', 'Set the one-line description')
260
+ .addHelpText('after', `
261
+ Examples:
262
+ # Upgrade the pinned model
263
+ agents harness edit spark --model meta/muse-spark-2.0
264
+
265
+ # Point to a new private endpoint
266
+ agents harness edit corp --base-url https://gw.corp/v2
267
+
268
+ # Track the host's latest instead of a pinned version
269
+ agents harness edit spark --version ""
270
+
271
+ # The display name is derived from the harness name, so to change it, rename:
272
+ agents harness rename spark muse-spark
273
+ `)
274
+ .action((name, opts) => {
275
+ try {
276
+ const edited = editProfile(readProfile(name), opts);
277
+ writeProfile(edited);
278
+ console.log(chalk.green(`Harness '${name}' updated.`));
279
+ console.log(chalk.gray(`Model: ${profileModelLabel(edited)}`));
280
+ }
281
+ catch (err) {
282
+ console.error(chalk.red(err.message));
283
+ process.exit(1);
284
+ }
285
+ });
286
+ cmd
287
+ .command('rename <name> <new-name>')
288
+ .description('Rename a custom harness — updates the file name and its internal name field.')
289
+ .addHelpText('after', `
290
+ Examples:
291
+ # Rename 'spark' to 'muse'
292
+ agents harness rename spark muse
293
+
294
+ # Rename then run under the new name
295
+ agents harness rename deepseek ds && agents run ds "hello"
296
+ `)
297
+ .action((name, newName) => {
298
+ try {
299
+ renameProfile(name, newName);
300
+ console.log(chalk.green(`Harness '${name}' renamed to '${newName}'.`));
301
+ console.log(chalk.gray(`Run: agents run ${newName} "hello"`));
302
+ }
303
+ catch (err) {
304
+ console.error(chalk.red(err.message));
305
+ process.exit(1);
306
+ }
307
+ });
252
308
  }
@@ -16,13 +16,13 @@ import { setTierOverride, clearTierOverride, listTierOverrides } from '../lib/mo
16
16
  import { getModelPricing } from '../lib/pricing/index.js';
17
17
  import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
18
18
  import { wrapJoined } from './inspect.js';
19
- const MODEL_CAPABLE_AGENTS = ['claude', 'codex', 'opencode', 'cursor', 'openclaw', 'antigravity', 'kimi', 'grok', 'droid'];
19
+ const MODEL_CAPABLE_AGENTS = ['claude', 'codex', 'opencode', 'cursor', 'openclaw', 'antigravity', 'kimi', 'grok', 'droid', 'pi'];
20
20
  /**
21
21
  * Agents that don't necessarily install under ~/.agents/versions (cursor ships
22
22
  * via a curl script). For these, fall back to the PATH binary and synthesize
23
23
  * a version label from the install path so cache keys stay stable.
24
24
  */
25
- const PATH_ONLY_AGENTS = new Set(['cursor']);
25
+ const PATH_ONLY_AGENTS = new Set(['cursor', 'pi']);
26
26
  /** Derive a version label from the PATH-installed binary location for agents without managed versions. */
27
27
  function fallbackPathVersion(agent) {
28
28
  const src = locateModelSource(agent, 'unresolved');
@@ -315,9 +315,9 @@ export function registerMonitorsCommands(program) {
315
315
  .option('--notify [channel]', 'Notify the owner (notify.owner); [channel] overrides the owner channel')
316
316
  .option('--webhook-out <url>', 'POST the event to this URL')
317
317
  // PLACEMENT / hygiene
318
- .option('--device <name>', 'OWNER device — the single machine that evaluates + fires (exactly-once)')
318
+ .option('--device <name>', 'OWNER (not body placement) — the single machine that evaluates + fires (exactly-once). See docs/00-concepts.md#placement.')
319
319
  .option('--devices <list>', 'Allowlist (comma-separated): each device fires independently')
320
- .option('--run-on <host>', 'Execute the ACTION on this machine over SSH (placement)')
320
+ .option('--run-on <host>', 'BODY placement — execute the ACTION on this machine over SSH (same idea as run --where device:<host>)')
321
321
  .option('--rate-limit <spec>', 'Auto-pause if it fires more than N/<interval> (e.g. 5/1m)')
322
322
  .option('--disabled', 'Create the monitor paused (enable later with resume)')
323
323
  .action(async (nameOrPath, options) => {