@phnx-labs/agents-cli 1.22.16 → 1.22.18

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 (39) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +10 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/cloud.d.ts +0 -1
  5. package/dist/commands/cloud.js +19 -185
  6. package/dist/commands/doctor.js +16 -14
  7. package/dist/commands/exec.js +57 -5
  8. package/dist/commands/feed.d.ts +1 -0
  9. package/dist/commands/feed.js +36 -32
  10. package/dist/commands/run-cloud.d.ts +26 -0
  11. package/dist/commands/run-cloud.js +162 -0
  12. package/dist/commands/versions.js +4 -2
  13. package/dist/commands/view.js +1 -54
  14. package/dist/lib/agents.js +4 -2
  15. package/dist/lib/channels/providers/rush.d.ts +2 -0
  16. package/dist/lib/channels/providers/rush.js +20 -5
  17. package/dist/lib/channels/registry.d.ts +2 -0
  18. package/dist/lib/channels/send.d.ts +4 -3
  19. package/dist/lib/channels/send.js +15 -18
  20. package/dist/lib/cloud/dispatch.d.ts +27 -0
  21. package/dist/lib/cloud/dispatch.js +214 -0
  22. package/dist/lib/feed-post.d.ts +2 -2
  23. package/dist/lib/feed-post.js +15 -10
  24. package/dist/lib/hosts/remote-cmd.js +8 -0
  25. package/dist/lib/humans.d.ts +3 -2
  26. package/dist/lib/humans.js +16 -8
  27. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  28. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  29. package/dist/lib/notify.d.ts +4 -4
  30. package/dist/lib/notify.js +8 -7
  31. package/dist/lib/placement.d.ts +8 -4
  32. package/dist/lib/placement.js +14 -8
  33. package/dist/lib/resources.js +124 -0
  34. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  35. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  36. package/dist/lib/session/active.js +7 -3
  37. package/dist/lib/settings-manifest.js +7 -1
  38. package/dist/lib/types.d.ts +2 -0
  39. package/package.json +1 -1
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Shared cloud dispatch core — the ONE path every cloud dispatch goes through.
3
+ *
4
+ * Both `agents cloud run` (commands/cloud.ts) and `agents run <agent> --cloud`
5
+ * (commands/run-cloud.ts) build a DispatchOptions + resolve a provider, then
6
+ * call executeCloudDispatch here. Behavior — capability checks, the missing-
7
+ * target picker, local persistence, event emission, streaming, and the live
8
+ * budget kill-switch — must not diverge between the two surfaces.
9
+ */
10
+ import chalk from 'chalk';
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import ora from 'ora';
14
+ import { die } from '../format.js';
15
+ import { insertTask, updateTaskStatus } from './store.js';
16
+ import { renderStream } from './stream.js';
17
+ import { MissingTargetError, MAX_IMAGES_PER_DISPATCH } from './types.js';
18
+ import { emit } from '../events.js';
19
+ import { shareRuntimeEnv } from '../share/config.js';
20
+ /** Map a supported image file extension to its wire mimeType. Rejects anything else. */
21
+ function imageMimeFromPath(file) {
22
+ const ext = path.extname(file).toLowerCase();
23
+ if (ext === '.png')
24
+ return 'image/png';
25
+ if (ext === '.jpg' || ext === '.jpeg')
26
+ return 'image/jpeg';
27
+ if (ext === '.webp')
28
+ return 'image/webp';
29
+ die(`Unsupported image type ${JSON.stringify(ext || file)}. Use .png, .jpg/.jpeg, or .webp.`);
30
+ }
31
+ /** Read one image file into a base64 ImageAttachment, dying with a clear error if it's missing. */
32
+ function readImageAttachment(file) {
33
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
34
+ die(`Image not found: ${file}`);
35
+ }
36
+ const mimeType = imageMimeFromPath(file);
37
+ return { data: fs.readFileSync(file).toString('base64'), mimeType };
38
+ }
39
+ /** Parse a `--skill <id>` value (`id` or `id@version`) into a SkillRef. */
40
+ function parseSkillRef(raw) {
41
+ const at = raw.lastIndexOf('@');
42
+ if (at > 0) {
43
+ return { id: raw.slice(0, at), version: raw.slice(at + 1) };
44
+ }
45
+ return { id: raw };
46
+ }
47
+ /**
48
+ * Resolve the prompt for a cloud dispatch: the raw value, or the contents of
49
+ * the file it points at (with a dim note on TTY). Dies when empty.
50
+ */
51
+ export function resolveCloudPrompt(raw, opts) {
52
+ let prompt = raw;
53
+ if (!prompt)
54
+ die('Prompt is required. Pass it as an argument or with --prompt.', 1, { json: opts.json, hint: opts.hint });
55
+ // If prompt is a file path, read it and tell the user
56
+ if (fs.existsSync(prompt) && fs.statSync(prompt).isFile()) {
57
+ const filePath = prompt;
58
+ const stat = fs.statSync(filePath);
59
+ const sizeKB = (stat.size / 1024).toFixed(1);
60
+ prompt = fs.readFileSync(filePath, 'utf-8').trim();
61
+ if (process.stderr.isTTY) {
62
+ process.stderr.write(chalk.dim(`Reading prompt from ${filePath} (${sizeKB} KB)\n`));
63
+ }
64
+ }
65
+ return prompt;
66
+ }
67
+ /**
68
+ * After a `MissingTargetError`, try to resolve the target interactively.
69
+ * Returns the chosen id, or undefined when no interactive resolution is
70
+ * possible (non-TTY/JSON, provider can't enumerate, or user cancels) — the
71
+ * caller then prints the error's guidance.
72
+ *
73
+ * Codex has no `listTargets` (no list-environments CLI), so it always returns
74
+ * undefined here and the user sees the `codex cloud` guidance. Factory lists
75
+ * Droid Computers; if listing fails (not signed in) or parses to nothing, we
76
+ * fall back to a free-text prompt so a dispatch is never hard-blocked.
77
+ */
78
+ async function pickMissingTarget(provider, err, json) {
79
+ if (json || !process.stdout.isTTY)
80
+ return undefined;
81
+ if (!provider.listTargets)
82
+ return undefined;
83
+ const { select, input } = await import('@inquirer/prompts');
84
+ const promptName = err.kind === 'env' ? 'environment' : 'computer';
85
+ let targets;
86
+ try {
87
+ targets = await provider.listTargets();
88
+ }
89
+ catch (listErr) {
90
+ process.stderr.write(chalk.dim(`Could not list ${promptName}s: ${listErr.message}\n`));
91
+ targets = [];
92
+ }
93
+ try {
94
+ if (targets.length > 0) {
95
+ return await select({
96
+ message: `Select a ${promptName}`,
97
+ choices: targets.map((t) => ({ value: t.id, name: t.label ? `${t.id} ${chalk.dim(t.label)}` : t.id })),
98
+ });
99
+ }
100
+ const typed = (await input({ message: `No ${promptName}s found. Enter a ${promptName} name (blank to cancel):` })).trim();
101
+ return typed || undefined;
102
+ }
103
+ catch {
104
+ // User hit Ctrl-C / Esc on the prompt.
105
+ return undefined;
106
+ }
107
+ }
108
+ /**
109
+ * Dispatch a cloud task and (unless follow=false) stream it to completion.
110
+ * Owns: share-env injection, capability checks, the dispatch spinner +
111
+ * missing-target picker, local persistence, event emission, and the budget
112
+ * kill-switch. Dies on any dispatch failure — callers never see a partial.
113
+ */
114
+ export async function executeCloudDispatch(params) {
115
+ const { provider, dispatchOptions, follow, json } = params;
116
+ const imagePaths = params.imagePaths ?? [];
117
+ const skillIds = params.skillIds ?? [];
118
+ const shareEnv = shareRuntimeEnv();
119
+ if (shareEnv)
120
+ dispatchOptions.env = shareEnv;
121
+ // Vision attachments + ride-along skills. Only wire them when the resolved
122
+ // provider advertises support — otherwise fail loud rather than silently
123
+ // drop the flags the user passed.
124
+ const caps = provider.capabilities();
125
+ if (imagePaths.length > 0) {
126
+ if (!caps.images)
127
+ die(`${provider.name} does not support image attachments.`, 1, { json });
128
+ if (imagePaths.length > MAX_IMAGES_PER_DISPATCH) {
129
+ die(`Too many images: ${imagePaths.length}. Max is ${MAX_IMAGES_PER_DISPATCH} per dispatch.`, 1, { json });
130
+ }
131
+ dispatchOptions.images = imagePaths.map(readImageAttachment);
132
+ }
133
+ if (skillIds.length > 0) {
134
+ if (!caps.skills)
135
+ die(`${provider.name} does not support ride-along skills.`, 1, { json });
136
+ dispatchOptions.skills = skillIds.map(parseSkillRef);
137
+ }
138
+ // Dispatch. On a missing pre-provisioned target (Codex env / Factory
139
+ // computer), offer an interactive picker instead of a raw error.
140
+ const dispatchOnce = async () => {
141
+ const spinner = ora({ text: `Dispatching to ${provider.name}...`, stream: process.stderr }).start();
142
+ try {
143
+ const t = await provider.dispatch(dispatchOptions);
144
+ spinner.succeed(`Task ${t.id} dispatched to ${provider.name}`);
145
+ return t;
146
+ }
147
+ catch (err) {
148
+ spinner.fail('Dispatch failed');
149
+ throw err;
150
+ }
151
+ };
152
+ let task;
153
+ try {
154
+ task = await dispatchOnce();
155
+ }
156
+ catch (err) {
157
+ if (err instanceof MissingTargetError) {
158
+ const picked = await pickMissingTarget(provider, err, json);
159
+ if (!picked) {
160
+ die(err.guidance ? `${err.message}\n\n${err.guidance}` : err.message, 1, { json });
161
+ }
162
+ dispatchOptions.providerOptions[err.kind] = picked;
163
+ try {
164
+ task = await dispatchOnce();
165
+ }
166
+ catch (err2) {
167
+ die(err2.message, 1, { json });
168
+ }
169
+ }
170
+ else {
171
+ die(err.message, 1, { json });
172
+ }
173
+ }
174
+ // Persist locally
175
+ insertTask(task);
176
+ emit('cloud.dispatch', { module: 'cloud', taskId: task.id, agent: task.agent, provider: task.provider, status: task.status });
177
+ if (json) {
178
+ process.stdout.write(JSON.stringify(task) + '\n');
179
+ }
180
+ // Stream output unless --no-follow
181
+ if (!follow)
182
+ return;
183
+ try {
184
+ // Live budget kill-switch (issue #399). Reuses makeLiveSpendWatcher to
185
+ // feed the provider's `usage` events into a shared watcher; on a cap
186
+ // breach we call provider.cancel(task.id) mid-stream. Dormant (returns
187
+ // null) when no caps are configured, so the raw stream flows unchanged.
188
+ const { wrapStreamWithBudgetGate } = await import('../budget/live-cloud.js');
189
+ const gated = wrapStreamWithBudgetGate({
190
+ provider,
191
+ taskId: task.id,
192
+ project: task.repo ?? task.repos?.[0] ?? process.cwd(),
193
+ agent: task.agent ?? 'cloud',
194
+ cwd: process.cwd(),
195
+ });
196
+ const eventSource = gated ? gated.wrap(provider.stream(task.id)) : provider.stream(task.id);
197
+ const result = await renderStream(eventSource, { json });
198
+ updateTaskStatus(task.id, result.status, {
199
+ summary: result.summary,
200
+ prUrl: result.prUrl,
201
+ });
202
+ emit('cloud.complete', { module: 'cloud', taskId: task.id, status: result.status, prUrl: result.prUrl });
203
+ if (gated?.gate.breached()) {
204
+ const b = gated.gate.breach();
205
+ process.stderr.write(`[budget] cap ${b?.cap} exceeded — cancelled cloud task ${task.id}\n`);
206
+ process.exitCode = 7; // Mirrors BUDGET_KILL_EXIT_CODE for CI/headless.
207
+ }
208
+ }
209
+ catch (err) {
210
+ // Stream disconnect is OK — task keeps running
211
+ process.stderr.write(chalk.dim(`\nStream disconnected. Task ${task.id} continues running.\n`));
212
+ process.stderr.write(chalk.dim(`Check status: agents cloud status ${task.id}\n`));
213
+ }
214
+ }
@@ -72,10 +72,10 @@ export interface PostIdentity {
72
72
  * Resolve who is posting. Order:
73
73
  * 1. Explicit --session flag
74
74
  * 2. Env: AGENT_SESSION_ID / AGENTS_SESSION_ID / basename(AGENTS_MAILBOX_DIR)
75
- * 3. Env AGENT_LAUNCH_ID → match in pid registry
75
+ * 3. Env AGENT_LAUNCH_ID → match in pid registry or activity index
76
76
  * 4. Walk parent PIDs from startPid (default process.ppid) through by-pid registry
77
77
  */
78
- export declare function resolvePostIdentity(input: Pick<FeedPostInput, 'sessionId' | 'env' | 'cwd' | 'startPid' | 'getParentPid' | 'readEntry' | 'listEntries'>): PostIdentity | undefined;
78
+ export declare function resolvePostIdentity(input: Pick<FeedPostInput, 'sessionId' | 'env' | 'cwd' | 'activityRoot' | 'startPid' | 'getParentPid' | 'readEntry' | 'listEntries'>): PostIdentity | undefined;
79
79
  /** Walk up to 16 ancestors looking for a by-pid registry entry with a session. */
80
80
  export declare function walkPidRegistry(startPid: number, getParent: (pid: number) => number | undefined, readEntry: (pid: number) => PidSessionEntry | undefined): PidSessionEntry | undefined;
81
81
  /** Best-effort parent pid of `pid` (Linux /proc, else `ps`). */
@@ -16,7 +16,7 @@
16
16
  import * as fs from 'fs';
17
17
  import * as path from 'path';
18
18
  import { spawnSync } from 'child_process';
19
- import { appendActivityEvent, } from './activity.js';
19
+ import { appendActivityEvent, readRecentActivity, } from './activity.js';
20
20
  import { resolveProjectNameForCwd, listProjectDefs } from './projects.js';
21
21
  import { getHistoryDir } from './state.js';
22
22
  import { machineId } from './machine-id.js';
@@ -30,7 +30,7 @@ export const STATUS_TITLE_MAX_CHARS = 60;
30
30
  * Resolve who is posting. Order:
31
31
  * 1. Explicit --session flag
32
32
  * 2. Env: AGENT_SESSION_ID / AGENTS_SESSION_ID / basename(AGENTS_MAILBOX_DIR)
33
- * 3. Env AGENT_LAUNCH_ID → match in pid registry
33
+ * 3. Env AGENT_LAUNCH_ID → match in pid registry or activity index
34
34
  * 4. Walk parent PIDs from startPid (default process.ppid) through by-pid registry
35
35
  */
36
36
  export function resolvePostIdentity(input) {
@@ -55,8 +55,12 @@ export function resolvePostIdentity(input) {
55
55
  registry = walkPidRegistry(start, getParent, readEntry);
56
56
  }
57
57
  }
58
+ const activity = launchId && !envSession && !registry?.sessionId
59
+ ? readRecentActivity({ root: input.activityRoot, maxBytesPerSession: 64 * 1024 })
60
+ .find((event) => event.launchId === launchId)
61
+ : undefined;
58
62
  // Prefer env session (explicit + managed run), fill gaps from registry.
59
- const sessionId = envSession ?? registry?.sessionId;
63
+ const sessionId = envSession ?? registry?.sessionId ?? activity?.sessionId;
60
64
  if (!sessionId || !isValidMailboxId(sessionId))
61
65
  return undefined;
62
66
  const mailboxFromEnv = mailboxIdFromEnv(env);
@@ -66,17 +70,18 @@ export function resolvePostIdentity(input) {
66
70
  return {
67
71
  sessionId,
68
72
  mailboxId,
69
- host: machineIdFromEnv(env),
70
- runtime: env.AGENTS_RUNTIME?.trim() || 'headless',
73
+ host: activity?.host ?? machineIdFromEnv(env),
74
+ runtime: env.AGENTS_RUNTIME?.trim() || activity?.runtime || 'headless',
71
75
  agent: env.AGENTS_AGENT_NAME?.trim()
72
76
  || registry?.agent
77
+ || activity?.agent
73
78
  || detectAgentKind(env),
74
79
  cwd: input.cwd
75
- ?? (env.AGENTS_CWD?.trim() || registry?.cwd || process.cwd()),
76
- pid: registry?.pid,
77
- launchId: launchId || registry?.launchId,
78
- terminalId: env.AGENT_TERMINAL_ID?.trim() || registry?.terminalId,
79
- tmuxPane: env.TMUX_PANE?.trim() || registry?.tmuxPane,
80
+ ?? (env.AGENTS_CWD?.trim() || registry?.cwd || activity?.cwd || process.cwd()),
81
+ pid: registry?.pid ?? activity?.pid,
82
+ launchId: launchId || registry?.launchId || activity?.launchId,
83
+ terminalId: env.AGENT_TERMINAL_ID?.trim() || registry?.terminalId || activity?.terminalId,
84
+ tmuxPane: env.TMUX_PANE?.trim() || registry?.tmuxPane || activity?.tmuxPane,
80
85
  };
81
86
  }
82
87
  function firstValidId(candidates) {
@@ -118,6 +118,14 @@ export const RUN_OPTION_FORWARDING = {
118
118
  bare: 'local-only', // skips the local setup-copy push; lease-only concern
119
119
  tailscale: 'local-only', // --tailscale/--no-tailscale gate the lease net mode; never forwarded
120
120
  copyCreds: 'local-only', // copies creds TO the host before dispatch — local concern only
121
+ // Cloud placement: chosen and dispatched from THIS machine via the provider
122
+ // registry; mutually exclusive with --host (placement conflict dies before
123
+ // dispatch), so these never have a remote argv to ride.
124
+ cloud: 'local-only',
125
+ provider: 'local-only',
126
+ repo: 'local-only',
127
+ branch: 'local-only',
128
+ cloudEnv: 'local-only',
121
129
  authCheck: 'local-only', // --no-auth-check gates the local interactive login preflight; --host runs skip that preflight entirely
122
130
  // The notification must land on the box the PERSON is at — the one that
123
131
  // dispatched — not on a headless worker with no desktop to post to. The local
@@ -12,8 +12,9 @@ export declare function readHumans(): HumansConfig | null;
12
12
  */
13
13
  export declare function writeHumans(config: HumansConfig): void;
14
14
  /**
15
- * Return the effective owner notification config from humans.yaml if
16
- * available, otherwise return null (caller falls back to agents.yaml).
15
+ * Return the effective owner notification destination from humans.yaml.
16
+ * The owner's normal-severity policy selects the channel; when no normal
17
+ * policy is declared, the first addressable channel is the default.
17
18
  */
18
19
  export declare function getOwnerNotifyFromHumans(): {
19
20
  channel: string;
@@ -5,8 +5,7 @@
5
5
  * Schema version: 1
6
6
  *
7
7
  * This module is the single read/write seam for humans.yaml. All channel/
8
- * notify consumers should read owner config from here (with a fallback to
9
- * the legacy notify.owner in agents.yaml during the migration window).
8
+ * notify consumers should read owner config from here.
10
9
  */
11
10
  import * as fs from 'fs';
12
11
  import * as yaml from 'yaml';
@@ -47,14 +46,23 @@ export function writeHumans(config) {
47
46
  fs.writeFileSync(filePath, HUMANS_HEADER + body, { encoding: 'utf-8', mode: 0o600 });
48
47
  }
49
48
  /**
50
- * Return the effective owner notification config from humans.yaml if
51
- * available, otherwise return null (caller falls back to agents.yaml).
49
+ * Return the effective owner notification destination from humans.yaml.
50
+ * The owner's normal-severity policy selects the channel; when no normal
51
+ * policy is declared, the first addressable channel is the default.
52
52
  */
53
53
  export function getOwnerNotifyFromHumans() {
54
- const humans = readHumans();
55
- const notify = humans?.owner?.notify;
56
- if (notify?.channel && notify?.to)
57
- return notify;
54
+ const owner = readHumans()?.owner;
55
+ const channels = owner?.channels ?? [];
56
+ const preferredIds = owner?.policy?.normal ?? [];
57
+ const preferred = preferredIds
58
+ .map((id) => channels.find((entry) => entry.id === id))
59
+ .find((entry) => entry?.to);
60
+ const selected = preferred ?? channels.find((entry) => entry.to);
61
+ if (selected?.id && selected.to)
62
+ return { channel: selected.id, to: selected.to };
63
+ const migrated = owner?.notify;
64
+ if (migrated?.channel && migrated.to)
65
+ return migrated;
58
66
  return null;
59
67
  }
60
68
  /**
@@ -4,7 +4,7 @@
4
4
  * Every human-facing owner notification (feed urgent-block dispatch, monitor
5
5
  * `notify` action, `agents notify`) funnels through the single channel seam:
6
6
  * `lookupTransport(channel, meta).provider.send(text, opts)`. The recipient comes
7
- * from `notify.owner` in agents.yaml — never a hardcoded chat id — so changing the
7
+ * from `humans.yaml` — never a hardcoded chat id — so changing the
8
8
  * owner is honoured by every path at once. `notify.transports` picks the actual
9
9
  * provider per host (rush telegram on zion, openclaw-telegram on mac-mini).
10
10
  * Best-effort: a delivery failure is returned to the caller, never thrown, so a
@@ -19,9 +19,9 @@ import type { SendResult } from './channels/registry.js';
19
19
  export interface OwnerNotifyOptions {
20
20
  /** Config source (defaults to `readMeta()`); lets callers/tests inject it. */
21
21
  meta?: Meta;
22
- /** Override the owner channel from `notify.owner.channel`. */
22
+ /** Override the owner channel resolved from humans.yaml. */
23
23
  channel?: string;
24
- /** Override the owner target from `notify.owner.to`. */
24
+ /** Override the owner target resolved from humans.yaml. */
25
25
  target?: string;
26
26
  /** Resolve + build the delivery but do not actually send. */
27
27
  dryRun?: boolean;
@@ -44,7 +44,7 @@ export declare function buildOpenClawNotifyArgs(text: string, opts: {
44
44
  }): string[];
45
45
  /**
46
46
  * Deliver a message to the configured owner through the one channel seam.
47
- * `channel`/`target` default to `notify.owner.{channel,to}`; `notify.transports`
47
+ * `channel`/`target` default to the normal owner channel in humans.yaml; `notify.transports`
48
48
  * selects the provider per host. A missing owner config or a delivery failure
49
49
  * (e.g. openclaw not on PATH) returns a clean `SendResult` error — never a raw
50
50
  * ENOENT — so callers surface a consistent, best-effort failure.
@@ -34,17 +34,14 @@ export function buildOpenClawNotifyArgs(text, opts) {
34
34
  }
35
35
  /**
36
36
  * Deliver a message to the configured owner through the one channel seam.
37
- * `channel`/`target` default to `notify.owner.{channel,to}`; `notify.transports`
37
+ * `channel`/`target` default to the normal owner channel in humans.yaml; `notify.transports`
38
38
  * selects the provider per host. A missing owner config or a delivery failure
39
39
  * (e.g. openclaw not on PATH) returns a clean `SendResult` error — never a raw
40
40
  * ENOENT — so callers surface a consistent, best-effort failure.
41
41
  */
42
42
  export async function sendToOwner(text, options = {}) {
43
43
  const meta = options.meta ?? readMeta();
44
- // humans.yaml is the primary source; agents.yaml notify.owner is the fallback.
45
- const humansOwner = getOwnerNotifyFromHumans();
46
- const legacyOwner = meta.notify?.owner;
47
- const owner = humansOwner ?? legacyOwner;
44
+ const owner = getOwnerNotifyFromHumans() ?? meta.notify?.owner;
48
45
  const channel = options.channel ?? owner?.channel;
49
46
  const target = options.target ?? owner?.to;
50
47
  if (!channel || !target) {
@@ -52,7 +49,7 @@ export async function sendToOwner(text, options = {}) {
52
49
  ok: false,
53
50
  channel: channel ?? 'unknown',
54
51
  id: target ?? '',
55
- error: 'notify.owner.{channel,to} not set in humans.yaml or agents.yaml',
52
+ error: 'No addressable owner channel configured in humans.yaml or legacy notify.owner',
56
53
  };
57
54
  }
58
55
  registerBuiltinProviders();
@@ -60,7 +57,11 @@ export async function sendToOwner(text, options = {}) {
60
57
  if (!provider) {
61
58
  return { ok: false, channel, id: target, error };
62
59
  }
63
- return provider.send(text, { target, dryRun: options.dryRun });
60
+ return provider.send(text, {
61
+ target,
62
+ ownerScoped: options.target === undefined,
63
+ dryRun: options.dryRun,
64
+ });
64
65
  }
65
66
  export async function notifyUrgentBlock(block, options = {}) {
66
67
  if (block.notifiedAt) {
@@ -2,7 +2,7 @@
2
2
  * Placement — one model for "where does the body run?"
3
3
  *
4
4
  * The CLI grew several doors that all mean execution target:
5
- * run --host / --device / --lease / --box
5
+ * run --host / --device / --lease / --box / --cloud
6
6
  * routines --placement / --run-on / hostStrategy
7
7
  * monitors --run-on (body) vs --device (owner — NOT placement)
8
8
  * teams --device (teammate pin)
@@ -41,6 +41,10 @@ export interface RunPlacementFlags {
41
41
  computer?: string;
42
42
  lease?: string | boolean;
43
43
  box?: string;
44
+ /** --cloud: vendor cloud placement (the agent's native cloud provider). */
45
+ cloud?: boolean;
46
+ /** --provider: refines the cloud placement; not a placement on its own. */
47
+ provider?: string;
44
48
  }
45
49
  export declare class PlacementError extends Error {
46
50
  constructor(message: string);
@@ -68,10 +72,10 @@ export declare function placementFromRunFlags(flags: RunPlacementFlags): Placeme
68
72
  * Expand a resolved placement into the concrete run option fields the
69
73
  * existing dispatch paths already understand. Pure — does not mutate input.
70
74
  *
71
- * `cloud` and `fleet` are not valid for a bare `agents run` (use `cloud run`
72
- * / routines); they throw so callers fail loud.
75
+ * `fleet` is not valid for a bare `agents run` (it is a routines placement);
76
+ * it throws so callers fail loud.
73
77
  */
74
- export declare function expandPlacementToRunFlags(placement: Placement): Pick<RunPlacementFlags, 'host' | 'device' | 'lease' | 'box'>;
78
+ export declare function expandPlacementToRunFlags(placement: Placement): Pick<RunPlacementFlags, 'host' | 'device' | 'lease' | 'box' | 'cloud' | 'provider'>;
75
79
  /** Map routines hostStrategy (+ optional host) onto the shared Placement. */
76
80
  export declare function placementFromHostStrategy(strategy: 'local' | 'host' | 'fleet' | 'cloud', host?: string): Placement;
77
81
  /** One-line human form for logs / help. */
@@ -2,7 +2,7 @@
2
2
  * Placement — one model for "where does the body run?"
3
3
  *
4
4
  * The CLI grew several doors that all mean execution target:
5
- * run --host / --device / --lease / --box
5
+ * run --host / --device / --lease / --box / --cloud
6
6
  * routines --placement / --run-on / hostStrategy
7
7
  * monitors --run-on (body) vs --device (owner — NOT placement)
8
8
  * teams --device (teammate pin)
@@ -93,6 +93,7 @@ export function placementFromRunFlags(flags) {
93
93
  const hostT = hostFamilyTarget(flags);
94
94
  const hasLease = flags.lease !== undefined && flags.lease !== false;
95
95
  const hasBox = !!flags.box;
96
+ const hasCloud = flags.cloud === true;
96
97
  const placementFlags = [];
97
98
  if (where)
98
99
  placementFlags.push('--where');
@@ -102,12 +103,16 @@ export function placementFromRunFlags(flags) {
102
103
  placementFlags.push('--lease');
103
104
  if (hasBox)
104
105
  placementFlags.push('--box');
106
+ if (hasCloud)
107
+ placementFlags.push('--cloud');
105
108
  if (placementFlags.length > 1) {
106
109
  throw new PlacementError(`Conflicting placement flags: ${placementFlags.join(' + ')}. ` +
107
- `Use one door — prefer --where (device:<name> | auto | lease | local).`);
110
+ `Use one door — prefer --where (device:<name> | auto | lease | cloud | local).`);
108
111
  }
109
112
  if (where)
110
113
  return parseWhereSpec(where, '--where');
114
+ if (hasCloud)
115
+ return { kind: 'cloud', target: flags.provider, source: '--cloud' };
111
116
  if (hasBox)
112
117
  return { kind: 'lease', target: flags.box, source: '--box' };
113
118
  if (hasLease) {
@@ -122,8 +127,8 @@ export function placementFromRunFlags(flags) {
122
127
  * Expand a resolved placement into the concrete run option fields the
123
128
  * existing dispatch paths already understand. Pure — does not mutate input.
124
129
  *
125
- * `cloud` and `fleet` are not valid for a bare `agents run` (use `cloud run`
126
- * / routines); they throw so callers fail loud.
130
+ * `fleet` is not valid for a bare `agents run` (it is a routines placement);
131
+ * it throws so callers fail loud.
127
132
  */
128
133
  export function expandPlacementToRunFlags(placement) {
129
134
  switch (placement.kind) {
@@ -140,12 +145,13 @@ export function expandPlacementToRunFlags(placement) {
140
145
  if (placement.source === '--box')
141
146
  return { box: placement.target };
142
147
  return placement.target ? { lease: placement.target } : { lease: true };
148
+ case 'cloud':
149
+ // Vendor cloud placement — `--where cloud[:provider]` expands to the
150
+ // --cloud flag (+ --provider refinement) the run action dispatches on.
151
+ return placement.target ? { cloud: true, provider: placement.target } : { cloud: true };
143
152
  case 'fleet':
144
153
  throw new PlacementError(`fleet placement is for routines (agents routines add … --placement fleet), not agents run. ` +
145
154
  `Use --where device:auto for an affinity pick, or --where device:<name>.`);
146
- case 'cloud':
147
- throw new PlacementError(`cloud placement is agents cloud run (vendor cloud), not agents run. ` +
148
- `For a disposable box use --where lease; for your fleet use --where device:<name>.`);
149
155
  }
150
156
  }
151
157
  /** Map routines hostStrategy (+ optional host) onto the shared Placement. */
@@ -180,9 +186,9 @@ export const PLACEMENT_MATRIX = `
180
186
  Affinity pick (14d usage) --where auto (= --device auto)
181
187
  Disposable cloud box --where lease (= --lease)
182
188
  Reuse warm crabbox --box <slug>
189
+ Vendor cloud task --cloud (= --where cloud[:provider])
183
190
  Routines: body on one box --run-on <name> / --placement host
184
191
  Routines: pick any online --placement fleet
185
- Vendor cloud task agents cloud run …
186
192
  Monitors: who evaluates --device <owner> (NOT body placement)
187
193
  Monitors: where action runs --run-on <host>
188
194
  `.trim();