@phnx-labs/agents-cli 1.22.61 → 1.22.62
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.
- package/CHANGELOG.md +4 -0
- package/README.md +6 -0
- package/dist/commands/feed.js +3 -1
- package/dist/commands/monitors.js +3 -3
- package/dist/commands/send.js +4 -1
- package/dist/lib/channels/owner-forward.d.ts +14 -8
- package/dist/lib/channels/owner-forward.js +9 -5
- package/dist/lib/channels/registry.d.ts +2 -0
- package/dist/lib/channels/send.js +13 -1
- package/dist/lib/feed-broadcast.js +5 -12
- package/dist/lib/humans.d.ts +9 -0
- package/dist/lib/humans.js +29 -8
- package/dist/lib/notify.d.ts +3 -0
- package/dist/lib/notify.js +63 -21
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.62
|
|
4
|
+
|
|
5
|
+
- **Owner notifications fan out across the configured normal-severity channels (PHNX-3567).** `agents send --to owner`, deprecated `agents notify`, monitor notifications, and an important feed's owner sink now attempt every addressable entry named by `owner.policy.normal` in `humans.yaml`, instead of silently selecting only the first. Each Rush-backed destination that cannot deliver on a Linux worker forwards its explicit channel and target to a capable Mac, avoiding both shell quoting and policy re-expansion/duplicate sends. Partial failures stay visible while successful channels still deliver; legacy single-channel configs retain their old behavior. Source: `cli/src/lib/humans.ts`, `cli/src/lib/notify.ts`, `cli/src/lib/channels/owner-forward.ts`, `cli/src/lib/feed-broadcast.ts`.
|
|
6
|
+
|
|
3
7
|
## 1.22.61
|
|
4
8
|
|
|
5
9
|
- **`agents harness` wizard: model catalog, connection test, and edit matrix (PHNX-2218/2220/2221/2222).** The create/edit wizard now picks the model from the host's own catalog (`getModelCatalog`) with a free-text escape hatch, gates the endpoint step to hosts that actually carry one, and runs a real pre-save connection test — `agents run <name> "say alive in one word" --headless --timeout 60s` classified into pass / auth / endpoint / model — behind a confirm with `--test`/`--no-test`, offering keep / edit / delete on failure rather than saving a broken harness silently. A resolver-sourced `harnessEditable` matrix disables (with a reason) any param the host's API format can't carry. Source: `apps/cli/src/commands/harness.ts`, `apps/cli/src/commands/harness-wizard.ts`, `apps/cli/src/commands/harness-hooks.ts`, `apps/cli/src/lib/harness-connection-test.ts`.
|
package/README.md
CHANGED
|
@@ -461,6 +461,12 @@ agents feed answer <key> --choice 0 # first answer wins; route over the recorded
|
|
|
461
461
|
agents feed post --title "Halfway done" "CI green, watching merge" # title + body
|
|
462
462
|
```
|
|
463
463
|
|
|
464
|
+
Important posts can reach every owner channel named in
|
|
465
|
+
`~/.agents/humans.yaml` under `owner.policy.normal` (for example iMessage and
|
|
466
|
+
Slack). Owner delivery attempts every selected channel, reports partial
|
|
467
|
+
failures, and safely forwards Rush-backed channels from headless workers to a
|
|
468
|
+
capable Mac; ordinary milestone posts remain record-only unless configured.
|
|
469
|
+
|
|
464
470
|
Top-level questions and waiting notifications publish one atomic open-block record per session, including the mailbox id, host, runtime, and every answer option. The default view collapses agents under the **outcome** they serve (Linear ticket, PR, worktree slug, or Unassigned) so a 1,100-agent fleet reads as dozens of deliverables. Answered, resumed, and stopped blocks clear automatically; Task subagents are excluded. The rendered reply command uses the same mailbox id with `agents message`, so the decision routes back to the agent that asked it.
|
|
465
471
|
|
|
466
472
|
### Auto-nudge stalls
|
package/dist/commands/feed.js
CHANGED
|
@@ -726,7 +726,9 @@ async function broadcastBlock(block, extras, meta, notify = false) {
|
|
|
726
726
|
/** One line per sink that ran. Silent when nothing is configured. */
|
|
727
727
|
function reportBroadcast(outcomes) {
|
|
728
728
|
for (const o of outcomes) {
|
|
729
|
-
if (o.ok)
|
|
729
|
+
if (o.ok && o.error)
|
|
730
|
+
console.error(chalk.yellow(` → ${o.name} partial: ${o.error}`));
|
|
731
|
+
else if (o.ok)
|
|
730
732
|
console.log(chalk.gray(` → ${o.name}`));
|
|
731
733
|
else
|
|
732
734
|
console.error(chalk.yellow(` → ${o.name} failed: ${o.error}`));
|
|
@@ -425,8 +425,8 @@ function buildAction(options) {
|
|
|
425
425
|
}
|
|
426
426
|
if (options.notify !== undefined) {
|
|
427
427
|
// --notify may be a bare flag (notify the owner) or carry a channel that
|
|
428
|
-
//
|
|
429
|
-
//
|
|
428
|
+
// selects one channel. Left unset, the send fans out every addressable
|
|
429
|
+
// owner.policy.normal destination from humans.yaml (one source of truth).
|
|
430
430
|
const channel = typeof options.notify === 'string' ? options.notify : undefined;
|
|
431
431
|
chosen.push({ type: 'notify', ...(channel ? { notifyChannel: channel } : {}) });
|
|
432
432
|
}
|
|
@@ -620,7 +620,7 @@ export function registerMonitorsCommands(program) {
|
|
|
620
620
|
.option('--action-timeout <t>', 'Kill the --run action if it runs longer than this (e.g. 10m)')
|
|
621
621
|
.option('--postcondition <cmd>', 'Shell command that must exit 0 after a --run/--routine action settles; otherwise the fire records as no-effect, not ok. {event} is replaced with the fired event summary')
|
|
622
622
|
.option('--routine <name>', 'Fire an existing routine on change')
|
|
623
|
-
.option('--notify [channel]', 'Notify
|
|
623
|
+
.option('--notify [channel]', 'Notify every normal-policy owner channel; [channel] selects one channel')
|
|
624
624
|
.option('--webhook-out <url>', 'POST the event to this URL')
|
|
625
625
|
// PLACEMENT / hygiene
|
|
626
626
|
.option('--device <name>', 'OWNER (not body placement) — the single machine that evaluates + fires (exactly-once). See docs/concepts.md#placement.')
|
package/dist/commands/send.js
CHANGED
|
@@ -44,6 +44,8 @@ async function runSend(positionalText, opts, ownerMode) {
|
|
|
44
44
|
const suffix = result.msgId ? chalk.dim(` (${result.msgId})`) : '';
|
|
45
45
|
const dry = envelope.dryRun ? chalk.dim(' [dry-run]') : '';
|
|
46
46
|
console.log(chalk.green(`Sent via ${result.channel} → ${result.id}`) + suffix + dry);
|
|
47
|
+
if (result.error)
|
|
48
|
+
console.error(chalk.yellow(`Partial delivery failure: ${result.error}`));
|
|
47
49
|
}
|
|
48
50
|
const SHARED_NOTES = `
|
|
49
51
|
Planes (do not mix them up):
|
|
@@ -122,7 +124,8 @@ export function registerSendCommand(program) {
|
|
|
122
124
|
notes: `
|
|
123
125
|
DEPRECATED. notify ≡ send --to owner and still works, but new callers
|
|
124
126
|
should use "agents feed post" (record + optional broadcast) instead.
|
|
125
|
-
Set
|
|
127
|
+
Set owner.channels + owner.policy.normal in humans.yaml once per fleet.
|
|
128
|
+
Every channel listed in the normal policy receives an owner-addressed send.
|
|
126
129
|
|
|
127
130
|
${SHARED_NOTES}
|
|
128
131
|
`,
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
* This mirrors the SSH reroute `agents message` (decideHostTaskRoute →
|
|
15
15
|
* runOnPeer) and the sessions fan-out already use for work that lives on another
|
|
16
16
|
* box: pick a reachable peer from the device registry and run the same `agents`
|
|
17
|
-
* verb there. Here the verb is `agents send --
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* verb there. Here the verb is `agents send --channel <channel> --to <target>`.
|
|
18
|
+
* Keeping the destination explicit and delivering through the peer's local rush
|
|
19
|
+
* destination explicit prevents a multi-channel owner policy from expanding a
|
|
20
|
+
* second time on the peer and duplicating already-successful channels.
|
|
20
21
|
*
|
|
21
22
|
* Best-effort seam: it never throws and never blocks the post. When no capable
|
|
22
23
|
* peer is reachable it resolves `undefined` and the caller keeps its original
|
|
@@ -63,13 +64,17 @@ export declare function planOwnerForward(channel: string, meta: Meta, devices: D
|
|
|
63
64
|
}): OwnerForwardPlan;
|
|
64
65
|
/**
|
|
65
66
|
* Deliver `text` to the owner FROM one peer over SSH. Runs the peer's own
|
|
66
|
-
* `agents send --to
|
|
67
|
-
*
|
|
67
|
+
* `agents send --channel <channel> --to <target> --text <text> --json`, which
|
|
68
|
+
* delivers the already-resolved destination through its local provider.
|
|
68
69
|
* Resolves the parsed `SendResult`, or `undefined` when the peer is
|
|
69
70
|
* unreachable / not a dialable device / answered with unparseable output —
|
|
70
71
|
* every one of which means "try the next peer".
|
|
71
72
|
*/
|
|
72
|
-
export
|
|
73
|
+
export interface PeerOwnerEnvelope {
|
|
74
|
+
thread?: string;
|
|
75
|
+
from?: string;
|
|
76
|
+
}
|
|
77
|
+
export type PeerOwnerSender = (machine: string, text: string, channel: string, target: string, envelope?: PeerOwnerEnvelope) => Promise<SendResult | undefined>;
|
|
73
78
|
/**
|
|
74
79
|
* Try each capable peer in order and return the first successful delivery. A
|
|
75
80
|
* peer that is unreachable or reports its own delivery failure is skipped and
|
|
@@ -79,10 +84,11 @@ export type PeerOwnerSender = (machine: string, text: string) => Promise<SendRes
|
|
|
79
84
|
*
|
|
80
85
|
* The transport (`send`) is injectable so the try-order / first-success / stop
|
|
81
86
|
* orchestration is testable without a live SSH host; the default runs the real
|
|
82
|
-
* `agents send
|
|
87
|
+
* explicit-destination `agents send` over SSH.
|
|
83
88
|
*/
|
|
84
|
-
export declare function forwardOwnerNotifyToPeer(text: string, channel: string, meta: Meta, opts?: {
|
|
89
|
+
export declare function forwardOwnerNotifyToPeer(text: string, channel: string, target: string, meta: Meta, opts?: {
|
|
85
90
|
self?: string;
|
|
86
91
|
devices?: DeviceProfile[];
|
|
87
92
|
send?: PeerOwnerSender;
|
|
93
|
+
envelope?: PeerOwnerEnvelope;
|
|
88
94
|
}): Promise<SendResult | undefined>;
|
|
@@ -51,11 +51,15 @@ export function planOwnerForward(channel, meta, devices, self, opts = {}) {
|
|
|
51
51
|
return { candidates: [], skip: 'no-capable-peer' };
|
|
52
52
|
return { candidates };
|
|
53
53
|
}
|
|
54
|
-
async function sendOnPeer(machine, text) {
|
|
54
|
+
async function sendOnPeer(machine, text, channel, target, envelope = {}) {
|
|
55
55
|
const peer = await resolvePeerTarget(machine);
|
|
56
56
|
if (!peer)
|
|
57
57
|
return undefined;
|
|
58
|
-
const args = ['send', '--
|
|
58
|
+
const args = ['send', '--channel', channel, '--to', target, '--text', text, '--json'];
|
|
59
|
+
if (envelope.thread)
|
|
60
|
+
args.push('--thread', envelope.thread);
|
|
61
|
+
if (envelope.from)
|
|
62
|
+
args.push('--from', envelope.from);
|
|
59
63
|
// Reuse the one injection-tested remote-command builder every `--device`
|
|
60
64
|
// dispatch uses (posix `bash -lc` / Windows `-EncodedCommand`), rather than a
|
|
61
65
|
// second hand-rolled quoting path on a security-sensitive seam. The env map is
|
|
@@ -83,9 +87,9 @@ async function sendOnPeer(machine, text) {
|
|
|
83
87
|
*
|
|
84
88
|
* The transport (`send`) is injectable so the try-order / first-success / stop
|
|
85
89
|
* orchestration is testable without a live SSH host; the default runs the real
|
|
86
|
-
* `agents send
|
|
90
|
+
* explicit-destination `agents send` over SSH.
|
|
87
91
|
*/
|
|
88
|
-
export async function forwardOwnerNotifyToPeer(text, channel, meta, opts = {}) {
|
|
92
|
+
export async function forwardOwnerNotifyToPeer(text, channel, target, meta, opts = {}) {
|
|
89
93
|
// Cheap, I/O-free gate first: a box that already received a forward, or an
|
|
90
94
|
// owner channel that isn't the macOS-only rush family, can never forward — so
|
|
91
95
|
// a normal local success/failure never pays a device-registry disk read.
|
|
@@ -108,7 +112,7 @@ export async function forwardOwnerNotifyToPeer(text, channel, meta, opts = {}) {
|
|
|
108
112
|
return undefined;
|
|
109
113
|
const send = opts.send ?? sendOnPeer;
|
|
110
114
|
for (const machine of plan.candidates) {
|
|
111
|
-
const result = await send(machine, text);
|
|
115
|
+
const result = await send(machine, text, channel, target, opts.envelope);
|
|
112
116
|
if (result?.ok)
|
|
113
117
|
return result;
|
|
114
118
|
}
|
|
@@ -33,6 +33,8 @@ export interface SendResult {
|
|
|
33
33
|
attachments?: string[];
|
|
34
34
|
/** Mailbox provider returns the enqueued message id. */
|
|
35
35
|
msgId?: string;
|
|
36
|
+
/** Per-destination results when the owner policy selects multiple channels. */
|
|
37
|
+
deliveries?: SendResult[];
|
|
36
38
|
}
|
|
37
39
|
export interface ChannelProvider {
|
|
38
40
|
/** Stable name used in `--channel` and as a `notify.transports` value. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getOwnerNotifyFromHumans } from '../humans.js';
|
|
2
2
|
import { registerBuiltinProviders } from './providers/index.js';
|
|
3
3
|
import { resolveTransport } from './resolve.js';
|
|
4
|
+
import { sendToOwner } from '../notify.js';
|
|
4
5
|
const OWNER_ALIAS = 'owner';
|
|
5
6
|
/** True when the destination token means “the configured owner”. */
|
|
6
7
|
export function isOwnerAlias(to) {
|
|
@@ -112,6 +113,17 @@ export async function sendMessage(input, meta) {
|
|
|
112
113
|
const resolved = resolveSendEnvelope(input, meta);
|
|
113
114
|
if (!resolved.ok)
|
|
114
115
|
return { error: resolved.error };
|
|
115
|
-
const
|
|
116
|
+
const ownerPolicyRequest = resolved.envelope.ownerScoped === true
|
|
117
|
+
&& !input.channel?.trim()
|
|
118
|
+
&& (!input.to?.trim() || isOwnerAlias(input.to));
|
|
119
|
+
const result = ownerPolicyRequest
|
|
120
|
+
? await sendToOwner(resolved.envelope.text, {
|
|
121
|
+
meta,
|
|
122
|
+
dryRun: resolved.envelope.dryRun,
|
|
123
|
+
thread: resolved.envelope.thread,
|
|
124
|
+
attachments: resolved.envelope.attachments,
|
|
125
|
+
from: resolved.envelope.from,
|
|
126
|
+
})
|
|
127
|
+
: await deliverEnvelope(resolved.envelope, meta);
|
|
116
128
|
return { result, envelope: resolved.envelope };
|
|
117
129
|
}
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
import { spawnSync } from 'child_process';
|
|
37
37
|
import { isOwnerAlias, readOwnerDest, resolveSendEnvelope, deliverEnvelope } from './channels/send.js';
|
|
38
38
|
import { lookupTransport } from './channels/resolve.js';
|
|
39
|
-
import { forwardOwnerNotifyToPeer } from './channels/owner-forward.js';
|
|
40
39
|
import { registerBuiltinProviders } from './channels/providers/index.js';
|
|
40
|
+
import { sendToOwner } from './notify.js';
|
|
41
41
|
const LEVEL_RANK = { milestone: 0, important: 1 };
|
|
42
42
|
/** Parse a `--level` value; anything unrecognized is a usage error, not a default. */
|
|
43
43
|
export function parseFeedPostLevel(raw) {
|
|
@@ -462,6 +462,10 @@ async function runChannelSink(sink, meta) {
|
|
|
462
462
|
// for a name that is, in fact, registered.
|
|
463
463
|
registerBuiltinProviders();
|
|
464
464
|
const owner = isOwnerAlias(sink.channel);
|
|
465
|
+
if (owner) {
|
|
466
|
+
const result = await sendToOwner(sink.text ?? '', { meta });
|
|
467
|
+
return { name, ok: result.ok, ...(result.error ? { error: result.error } : {}) };
|
|
468
|
+
}
|
|
465
469
|
const resolved = resolveSendEnvelope({
|
|
466
470
|
text: sink.text ?? '',
|
|
467
471
|
channel: owner ? undefined : sink.channel,
|
|
@@ -476,17 +480,6 @@ async function runChannelSink(sink, meta) {
|
|
|
476
480
|
const result = await deliverEnvelope(resolved.envelope, meta);
|
|
477
481
|
if (result.ok)
|
|
478
482
|
return { name, ok: true };
|
|
479
|
-
// The owner sink failed locally. When this box structurally cannot reach the
|
|
480
|
-
// owner (the rush-backed channel is macOS-only, so a headless Linux worker can
|
|
481
|
-
// never ring the phone — PHNX-3303), hand the delivery to a capable fleet peer
|
|
482
|
-
// over SSH rather than stranding the important post. Only the `owner` alias
|
|
483
|
-
// forwards: it resolves the peer's own fleet-synced owner destination, so a
|
|
484
|
-
// non-owner channel sink with an explicit recipient stays local.
|
|
485
|
-
if (owner) {
|
|
486
|
-
const forwarded = await forwardOwnerNotifyToPeer(sink.text ?? '', resolved.envelope.channel, meta);
|
|
487
|
-
if (forwarded?.ok)
|
|
488
|
-
return { name, ok: true };
|
|
489
|
-
}
|
|
490
483
|
return { name, ok: false, error: result.error };
|
|
491
484
|
}
|
|
492
485
|
/**
|
package/dist/lib/humans.d.ts
CHANGED
|
@@ -20,6 +20,15 @@ export declare function getOwnerNotifyFromHumans(): {
|
|
|
20
20
|
channel: string;
|
|
21
21
|
to: string;
|
|
22
22
|
} | null;
|
|
23
|
+
/**
|
|
24
|
+
* Return every addressable owner destination selected by the normal-severity
|
|
25
|
+
* policy, in policy order. A config without a policy keeps the historical
|
|
26
|
+
* single-channel behavior by selecting the first addressable channel.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getOwnerNotifyDestinationsFromHumans(): Array<{
|
|
29
|
+
channel: string;
|
|
30
|
+
to: string;
|
|
31
|
+
}>;
|
|
23
32
|
/**
|
|
24
33
|
* Read the owner block from humans.yaml. Returns null if missing.
|
|
25
34
|
*/
|
package/dist/lib/humans.js
CHANGED
|
@@ -51,19 +51,40 @@ export function writeHumans(config) {
|
|
|
51
51
|
* policy is declared, the first addressable channel is the default.
|
|
52
52
|
*/
|
|
53
53
|
export function getOwnerNotifyFromHumans() {
|
|
54
|
+
return getOwnerNotifyDestinationsFromHumans()[0] ?? null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Return every addressable owner destination selected by the normal-severity
|
|
58
|
+
* policy, in policy order. A config without a policy keeps the historical
|
|
59
|
+
* single-channel behavior by selecting the first addressable channel.
|
|
60
|
+
*/
|
|
61
|
+
export function getOwnerNotifyDestinationsFromHumans() {
|
|
54
62
|
const owner = readHumans()?.owner;
|
|
55
63
|
const channels = owner?.channels ?? [];
|
|
56
64
|
const preferredIds = owner?.policy?.normal ?? [];
|
|
57
|
-
const
|
|
58
|
-
.map((id) => channels.find((entry) => entry.id === id))
|
|
59
|
-
.find((entry) => entry
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
const selected = preferredIds.length > 0
|
|
66
|
+
? preferredIds.map((id) => channels.find((entry) => entry.id === id))
|
|
67
|
+
: [channels.find((entry) => entry.to)];
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
const destinations = selected
|
|
70
|
+
.filter((entry) => Boolean(entry?.id && entry.to))
|
|
71
|
+
.map((entry) => ({ channel: entry.id, to: entry.to }))
|
|
72
|
+
.filter((entry) => {
|
|
73
|
+
const key = `${entry.channel}\0${entry.to}`;
|
|
74
|
+
if (seen.has(key))
|
|
75
|
+
return false;
|
|
76
|
+
seen.add(key);
|
|
77
|
+
return true;
|
|
78
|
+
});
|
|
79
|
+
if (destinations.length > 0)
|
|
80
|
+
return destinations;
|
|
81
|
+
const firstAddressable = channels.find((entry) => entry.id && entry.to);
|
|
82
|
+
if (firstAddressable?.to)
|
|
83
|
+
return [{ channel: firstAddressable.id, to: firstAddressable.to }];
|
|
63
84
|
const migrated = owner?.notify;
|
|
64
85
|
if (migrated?.channel && migrated.to)
|
|
65
|
-
return migrated;
|
|
66
|
-
return
|
|
86
|
+
return [migrated];
|
|
87
|
+
return [];
|
|
67
88
|
}
|
|
68
89
|
/**
|
|
69
90
|
* Read the owner block from humans.yaml. Returns null if missing.
|
package/dist/lib/notify.d.ts
CHANGED
|
@@ -25,6 +25,9 @@ export interface OwnerNotifyOptions {
|
|
|
25
25
|
target?: string;
|
|
26
26
|
/** Resolve + build the delivery but do not actually send. */
|
|
27
27
|
dryRun?: boolean;
|
|
28
|
+
thread?: string;
|
|
29
|
+
attachments?: string[];
|
|
30
|
+
from?: string;
|
|
28
31
|
}
|
|
29
32
|
export interface NotifyResult {
|
|
30
33
|
ok: boolean;
|
package/dist/lib/notify.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readMeta } from './state.js';
|
|
2
|
-
import {
|
|
2
|
+
import { getOwnerNotifyDestinationsFromHumans } from './humans.js';
|
|
3
3
|
import { registerBuiltinProviders } from './channels/providers/index.js';
|
|
4
4
|
import { lookupTransport } from './channels/resolve.js';
|
|
5
5
|
import { forwardOwnerNotifyToPeer } from './channels/owner-forward.js';
|
|
@@ -49,33 +49,75 @@ export function buildOpenClawNotifyArgs(text, opts) {
|
|
|
49
49
|
*/
|
|
50
50
|
export async function sendToOwner(text, options = {}) {
|
|
51
51
|
const meta = options.meta ?? readMeta();
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
52
|
+
const canonical = getOwnerNotifyDestinationsFromHumans();
|
|
53
|
+
const legacy = meta.notify?.owner ? [meta.notify.owner] : [];
|
|
54
|
+
const configured = canonical.length > 0 ? canonical : legacy;
|
|
55
|
+
const destinations = options.target
|
|
56
|
+
? [{ channel: options.channel ?? configured[0]?.channel, to: options.target }]
|
|
57
|
+
: options.channel
|
|
58
|
+
? [configured.find((dest) => dest.channel === options.channel) ?? {
|
|
59
|
+
channel: options.channel,
|
|
60
|
+
to: configured[0]?.to,
|
|
61
|
+
}]
|
|
62
|
+
: configured;
|
|
63
|
+
const addressable = destinations.filter((dest) => Boolean(dest.channel && dest.to));
|
|
64
|
+
if (addressable.length === 0) {
|
|
56
65
|
return {
|
|
57
66
|
ok: false,
|
|
58
|
-
channel: channel ?? 'unknown',
|
|
59
|
-
id: target ?? '',
|
|
67
|
+
channel: options.channel ?? 'unknown',
|
|
68
|
+
id: options.target ?? '',
|
|
60
69
|
error: 'No addressable owner channel configured in humans.yaml or legacy notify.owner',
|
|
61
70
|
};
|
|
62
71
|
}
|
|
63
72
|
registerBuiltinProviders();
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
const deliveries = [];
|
|
74
|
+
for (const { channel, to: target } of addressable) {
|
|
75
|
+
const { provider, error } = lookupTransport(channel, meta);
|
|
76
|
+
let result;
|
|
77
|
+
try {
|
|
78
|
+
result = provider
|
|
79
|
+
? await provider.send(text, {
|
|
80
|
+
target,
|
|
81
|
+
ownerScoped: options.target === undefined,
|
|
82
|
+
dryRun: options.dryRun,
|
|
83
|
+
thread: options.thread,
|
|
84
|
+
attachments: options.attachments,
|
|
85
|
+
from: options.from,
|
|
86
|
+
})
|
|
87
|
+
: { ok: false, channel, id: target, error };
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
result = { ok: false, channel, id: target, error: err.message };
|
|
91
|
+
}
|
|
92
|
+
// A dry-run never delivers, and an override target is an explicit recipient
|
|
93
|
+
// (not the fleet-wide owner) — neither should hop to a peer.
|
|
94
|
+
if (!result.ok && !options.dryRun && options.target === undefined) {
|
|
95
|
+
if (options.attachments?.length) {
|
|
96
|
+
result = {
|
|
97
|
+
...result,
|
|
98
|
+
error: `${result.error ?? 'local delivery failed'}; owner attachments cannot be forwarded to another device`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
result = await forwardOwnerNotifyToPeer(text, channel, target, meta, {
|
|
103
|
+
envelope: { thread: options.thread, from: options.from },
|
|
104
|
+
}) ?? result;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
deliveries.push(result);
|
|
67
108
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
109
|
+
if (deliveries.length === 1)
|
|
110
|
+
return deliveries[0];
|
|
111
|
+
const failures = deliveries.filter((result) => !result.ok);
|
|
112
|
+
return {
|
|
113
|
+
ok: deliveries.some((result) => result.ok),
|
|
114
|
+
channel: 'owner',
|
|
115
|
+
id: deliveries.map((result) => `${result.channel}:${result.id}`).join(','),
|
|
116
|
+
...(failures.length > 0
|
|
117
|
+
? { error: failures.map((result) => `${result.channel}: ${result.error ?? 'failed'}`).join('; ') }
|
|
118
|
+
: {}),
|
|
119
|
+
deliveries,
|
|
120
|
+
};
|
|
79
121
|
}
|
|
80
122
|
export async function notifyUrgentBlock(block, options = {}) {
|
|
81
123
|
if (block.notifiedAt) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.62",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|