@pellux/goodvibes-agent 0.1.39 → 0.1.40

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 CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  All notable changes to GoodVibes Agent will be recorded here.
4
4
 
5
+ ## 0.1.40 - 2026-05-31
6
+
7
+ - 329dc13 Add routine schedule delivery targets
8
+
5
9
  ## 0.1.39 - 2026-05-31
6
10
 
7
11
  - c98de19 Add routine schedule reconciliation
package/README.md CHANGED
@@ -68,14 +68,14 @@ Local Agent behavior is editable from the TUI:
68
68
  /personas use research
69
69
  /routines create --name "Evening Review" --description "Review open work before shutdown" --steps "Check work plan, approvals, and Agent Knowledge status before summarizing." --enabled true
70
70
  /routines start evening-review
71
- /schedule promote-routine evening-review --cron "0 17 * * 1-5" --timezone America/Chicago --yes
71
+ /schedule promote-routine evening-review --cron "0 17 * * 1-5" --timezone America/Chicago --delivery-surface slack --yes
72
72
  /schedule receipts
73
73
  /schedule reconcile
74
74
  /agent-skills create --name "Morning Brief" --description "Daily briefing flow" --procedure "Check tasks, approvals, calendar, and unread state before summarizing." --enabled true
75
75
  /skills local list
76
76
  ```
77
77
 
78
- Starting a routine records local usage and prints its steps; it does not spawn background agents or daemon automation jobs. Promotion to a daemon schedule is separate and explicit: it calls the public `schedules.create` route on the externally managed daemon only after `--yes`, records a redacted local receipt, and the generated scheduled prompt keeps Agent Knowledge isolated from default Knowledge/Wiki and HomeGraph. Use `/schedule reconcile` to compare those local receipts against live externally owned daemon schedules through public `schedules.list`.
78
+ Starting a routine records local usage and prints its steps; it does not spawn background agents or daemon automation jobs. Promotion to a daemon schedule is separate and explicit: it calls the public `schedules.create` route on the externally managed daemon only after `--yes`, can include explicit delivery targets such as `--delivery-surface slack`, records a redacted local receipt, and the generated scheduled prompt keeps Agent Knowledge isolated from default Knowledge/Wiki and HomeGraph. Use `/schedule reconcile` to compare those local receipts against live externally owned daemon schedules through public `schedules.list`.
79
79
 
80
80
  ## Daemon Prerequisite
81
81
 
@@ -43,19 +43,27 @@ Primary sources used for the benchmark:
43
43
  - Hermes Voice: https://hermes-agent.nousresearch.com/docs/user-guide/features/voice-mode/
44
44
  - Hermes API Server: https://hermes-agent.nousresearch.com/docs/user-guide/features/api-server/
45
45
  - Hermes Profiles: https://hermes-agent.nousresearch.com/docs/user-guide/profiles/
46
+ - GoodVibes daemon: `@pellux/goodvibes-sdk@0.33.35` public operator contract plus `/api/goodvibes-agent/knowledge/*`
47
+
48
+ The benchmark measures two different GoodVibes layers:
49
+
50
+ - daemon capability: what the externally owned GoodVibes daemon can already expose through public operator routes;
51
+ - Agent usability: what GoodVibes Agent makes configurable, visible, safe, and usable from day one.
52
+
53
+ If the daemon already has a route but Agent lacks a good setup/workspace/CLI surface, the gap is treated as an Agent product gap rather than a missing platform capability.
46
54
 
47
55
  ## Capability Targets
48
56
 
49
57
  | Area | OpenClaw/Hermes Baseline | GoodVibes Agent Position |
50
58
  | --- | --- | --- |
51
59
  | Terminal operator UI | Interactive CLI/TUI, commands, sessions | Near-fork GoodVibes TUI compositor/input/fullscreen foundation |
52
- | Always-on gateway | Gateway/service owns channels, sessions, tools, events | External GoodVibes daemon, never Agent-owned lifecycle |
60
+ | Always-on gateway | Gateway/service owns channels, sessions, tools, events | External GoodVibes daemon exposes sessions, companion chat, channels, remote peers, approvals, automation, schedules, artifacts, MCP, providers, voice, media, web search, and isolated Agent Knowledge; Agent never owns daemon lifecycle |
53
61
  | Channels | WhatsApp, Telegram, Slack, Discord, Signal, iMessage, web chat | GoodVibes daemon channel and companion surfaces with Agent-side policy, a Channels operator workspace, and per-channel readiness/risk labels |
54
- | Knowledge/memory | Durable memory, semantic search, wiki/claim layers | Isolated Agent Knowledge routes with workspace ask/search/ingest/review flows plus local memory/skills/personas/routines |
62
+ | Knowledge/memory | Durable memory, semantic search, wiki/claim layers | Isolated `/api/goodvibes-agent/knowledge/*` routes with workspace ask/search/ingest/review flows plus local memory/skills/personas/routines |
55
63
  | Skills/procedural memory | Skills directories, registries, skill lifecycle | Local Agent skills with review/stale/source/provenance fields |
56
- | Scheduling | Natural-language cron, run/pause/resume/edit/remove, delivery | Local routines can be explicitly promoted to external daemon `schedules.create` with `--yes`; redacted local promotion receipts are reviewable and can be reconciled with live `schedules.list`; hidden model scheduling and local scheduler spawns are blocked |
57
- | Tools/MCP | Broad toolsets, MCP, browser, media, terminal, files | GoodVibes SDK tools with Agent policy guards and MCP/provider integrations |
58
- | Voice/media/canvas/nodes | Voice, TTS, mobile nodes, live canvas, browser automation | GoodVibes media/voice/browser/node primitives with an Agent workspace for setup, image input, browser posture, MCP, and remote/node inspection |
64
+ | Scheduling | Natural-language cron, run/pause/resume/edit/remove, delivery | Local routines can be explicitly promoted to external daemon `schedules.create` with `--yes` and optional explicit delivery targets; redacted local promotion receipts are reviewable and can be reconciled with live `schedules.list`; hidden model scheduling and local scheduler spawns are blocked |
65
+ | Tools/MCP | Broad toolsets, MCP, browser, media, terminal, files | GoodVibes daemon exposes MCP, artifacts, web search, providers, media, multimodal, and channel tool routes; Agent adds policy guards and operator setup surfaces |
66
+ | Voice/media/canvas/nodes | Voice, TTS, mobile nodes, live canvas, browser automation | GoodVibes daemon exposes voice, media, multimodal, artifacts, and remote/node routes; Agent workspace makes setup and posture visible without daemon ownership |
59
67
  | Build/code work | Direct terminal/file/code tools and subagents | Explicit delegation to GoodVibes TUI; local WRFC/spawn fanout blocked |
60
68
  | Profiles | Independent profiles with own config/memory/skills/gateway | `GOODVIBES_AGENT_HOME` and named `--agent-profile` homes isolate Agent-local state; starter templates seed local personas/skills/routines; starter JSON can be exported/imported for local custom lanes; `/agent-profile guide` brings starter authoring into the Agent workspace; daemon remains external |
61
69
  | Security | DM pairing, approvals, sandboxing, allowlists | Daemon approvals, auth diagnostics, secret refs, confirmation gates, model-tool policy |
@@ -66,7 +74,7 @@ GoodVibes Agent should exceed OpenClaw/Hermes by making these properties true fr
66
74
 
67
75
  - Capability surfaces are discoverable through `goodvibes-agent capabilities`, `/capabilities`, onboarding, and the operator workspace.
68
76
  - Agent Knowledge isolation is a release gate, not a convention.
69
- - Routine-to-schedule promotion preserves Agent Knowledge isolation and uses only public external daemon schedule routes.
77
+ - Routine-to-schedule promotion preserves Agent Knowledge isolation, uses only public external daemon schedule routes, supports explicit delivery targets, and stores redacted receipts.
70
78
  - Model-visible tools are policy-gated for serial, non-secret, non-destructive use.
71
79
  - Personal assistant state is Agent-local unless an explicit Agent Knowledge ingest route is used.
72
80
  - Build work is delegated to the product that owns coding execution instead of turning the personal operator into a second coding TUI.
@@ -78,7 +86,7 @@ GoodVibes Agent should exceed OpenClaw/Hermes by making these properties true fr
78
86
  - Artifact and multimodal Agent Knowledge ingest affordances once Agent-specific routes are stable.
79
87
  - Visual starter-template editing inside the fullscreen Agent workspace after the command-guided authoring path.
80
88
  - Artifact and multimodal Agent Knowledge ingestion when the isolated Agent route accepts artifact-backed media.
81
- - Live schedule recovery/status correlation for promoted routines.
89
+ - Deeper live run/delivery history and delivery error surfacing for promoted routines.
82
90
  - Delegation receipts and artifact review inside the operator workspace.
83
91
  - Approval center with route risk labels and saved policy presets.
84
92
  - Intent-gated tool exposure so the model sees fewer irrelevant tools per turn while retaining broad capability coverage.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-agent",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "private": false,
5
5
  "description": "Near-fork GoodVibes operator assistant with the GoodVibes TUI shell, renderer, input, fullscreen workspace, and daemon-connected Agent product brain.",
6
6
  "type": "module",
@@ -17,6 +17,8 @@ export const ROUTINE_SCHEDULE_LIST_METHOD = 'schedules.list';
17
17
  type ScheduleCreateInput = OperatorMethodInput<'schedules.create'>;
18
18
  type ScheduleCreateOutput = OperatorMethodOutput<'schedules.create'>;
19
19
  type ScheduleListOutput = OperatorMethodOutput<'schedules.list'>;
20
+ type ScheduleDeliveryInput = NonNullable<ScheduleCreateInput['delivery']>;
21
+ type ScheduleDeliveryTargetInput = ScheduleDeliveryInput['targets'] extends readonly (infer T)[] ? T : never;
20
22
 
21
23
  export interface AgentDaemonConfigReader {
22
24
  get(key: string): unknown;
@@ -35,9 +37,46 @@ export interface RoutineScheduleSpec {
35
37
  readonly value: string;
36
38
  }
37
39
 
40
+ export type RoutineScheduleDeliveryKind = 'webhook' | 'surface' | 'integration' | 'link';
41
+
42
+ export type RoutineScheduleDeliverySurfaceKind =
43
+ | 'tui'
44
+ | 'web'
45
+ | 'slack'
46
+ | 'discord'
47
+ | 'ntfy'
48
+ | 'webhook'
49
+ | 'telegram'
50
+ | 'google-chat'
51
+ | 'signal'
52
+ | 'whatsapp'
53
+ | 'imessage'
54
+ | 'msteams'
55
+ | 'bluebubbles'
56
+ | 'mattermost'
57
+ | 'matrix'
58
+ | 'service';
59
+
60
+ export interface RoutineScheduleDeliveryTargetSpec {
61
+ readonly kind: RoutineScheduleDeliveryKind;
62
+ readonly surfaceKind?: RoutineScheduleDeliverySurfaceKind;
63
+ readonly address?: string;
64
+ readonly routeId?: string;
65
+ readonly label?: string;
66
+ }
67
+
68
+ export interface RoutineScheduleReceiptDeliveryTarget {
69
+ readonly kind: RoutineScheduleDeliveryKind;
70
+ readonly surfaceKind?: RoutineScheduleDeliverySurfaceKind;
71
+ readonly address?: string;
72
+ readonly routeId?: string;
73
+ readonly label?: string;
74
+ }
75
+
38
76
  export interface ParsedRoutineSchedulePromotionArgs {
39
77
  readonly routineId: string | null;
40
78
  readonly schedule: RoutineScheduleSpec | null;
79
+ readonly deliveryTargets: readonly RoutineScheduleDeliveryTargetSpec[];
41
80
  readonly name?: string;
42
81
  readonly timezone?: string;
43
82
  readonly provider?: string;
@@ -110,6 +149,7 @@ export interface RoutineScheduleReceipt {
110
149
  readonly createIfMissing?: boolean;
111
150
  };
112
151
  readonly deliveryMode?: string;
152
+ readonly deliveryTargets?: readonly RoutineScheduleReceiptDeliveryTarget[];
113
153
  readonly failureKind?: RoutineSchedulePromotionFailure['kind'];
114
154
  readonly failureError?: string;
115
155
  }
@@ -176,6 +216,24 @@ interface RoutineScheduleReceiptStoreFile {
176
216
  }
177
217
 
178
218
  const RECEIPT_STORE_VERSION = 1;
219
+ const DELIVERY_SURFACE_KINDS: readonly RoutineScheduleDeliverySurfaceKind[] = [
220
+ 'tui',
221
+ 'web',
222
+ 'slack',
223
+ 'discord',
224
+ 'ntfy',
225
+ 'webhook',
226
+ 'telegram',
227
+ 'google-chat',
228
+ 'signal',
229
+ 'whatsapp',
230
+ 'imessage',
231
+ 'msteams',
232
+ 'bluebubbles',
233
+ 'mattermost',
234
+ 'matrix',
235
+ 'service',
236
+ ];
179
237
 
180
238
  function isRecord(value: unknown): value is Record<string, unknown> {
181
239
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
@@ -222,6 +280,99 @@ function normalizeProviderModel(provider: string | undefined, model: string | un
222
280
  };
223
281
  }
224
282
 
283
+ function isDeliverySurfaceKind(value: string): value is RoutineScheduleDeliverySurfaceKind {
284
+ return DELIVERY_SURFACE_KINDS.includes(value as RoutineScheduleDeliverySurfaceKind);
285
+ }
286
+
287
+ function parseSurfaceDeliveryTarget(raw: string): RoutineScheduleDeliveryTargetSpec | string {
288
+ const [surfaceKind = '', routeId, label] = raw.split(':');
289
+ if (!isDeliverySurfaceKind(surfaceKind)) {
290
+ return `Unsupported delivery surface "${surfaceKind}".`;
291
+ }
292
+ return {
293
+ kind: 'surface',
294
+ surfaceKind,
295
+ routeId: routeId?.trim() || undefined,
296
+ label: label?.trim() || undefined,
297
+ };
298
+ }
299
+
300
+ function parseRouteDeliveryTarget(raw: string): RoutineScheduleDeliveryTargetSpec | string {
301
+ const [routeId = '', label] = raw.split(':');
302
+ const normalizedRouteId = routeId.trim();
303
+ if (!normalizedRouteId) return '--delivery-route requires a route id.';
304
+ return {
305
+ kind: 'surface',
306
+ routeId: normalizedRouteId,
307
+ label: label?.trim() || undefined,
308
+ };
309
+ }
310
+
311
+ function parseWebhookDeliveryTarget(raw: string): RoutineScheduleDeliveryTargetSpec | string {
312
+ const normalized = raw.trim();
313
+ if (!normalized) return '--delivery-webhook requires a URL.';
314
+ try {
315
+ const url = new URL(normalized);
316
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') return '--delivery-webhook must be an http(s) URL.';
317
+ } catch {
318
+ return '--delivery-webhook must be a valid URL.';
319
+ }
320
+ return {
321
+ kind: 'webhook',
322
+ address: normalized,
323
+ };
324
+ }
325
+
326
+ function parseLinkDeliveryTarget(raw: string): RoutineScheduleDeliveryTargetSpec | string {
327
+ const normalized = raw.trim();
328
+ if (!normalized) return '--delivery-link requires a URL or label.';
329
+ return {
330
+ kind: 'link',
331
+ address: normalized,
332
+ };
333
+ }
334
+
335
+ function validateDeliveryTargets(targets: readonly RoutineScheduleDeliveryTargetSpec[]): string | null {
336
+ const kinds = new Set(targets.map((target) => target.kind));
337
+ return kinds.size > 1 ? 'Use one delivery target kind per routine promotion command.' : null;
338
+ }
339
+
340
+ function deliveryModeFromTargets(targets: readonly RoutineScheduleDeliveryTargetSpec[]): ScheduleDeliveryInput['mode'] {
341
+ const first = targets[0];
342
+ return first ? first.kind : 'none';
343
+ }
344
+
345
+ function toDeliveryTargetInput(target: RoutineScheduleDeliveryTargetSpec): ScheduleDeliveryTargetInput {
346
+ return {
347
+ kind: target.kind,
348
+ surfaceKind: target.surfaceKind,
349
+ address: target.address,
350
+ routeId: target.routeId,
351
+ label: target.label,
352
+ };
353
+ }
354
+
355
+ function redactedDeliveryAddress(address: string | undefined): string | undefined {
356
+ if (!address) return undefined;
357
+ try {
358
+ const url = new URL(address);
359
+ return `${url.protocol}//${url.host}/...`;
360
+ } catch {
361
+ return '[redacted]';
362
+ }
363
+ }
364
+
365
+ function redactedDeliveryTargets(delivery: ScheduleDeliveryInput | undefined): readonly RoutineScheduleReceiptDeliveryTarget[] | undefined {
366
+ if (!delivery) return undefined;
367
+ return delivery.targets.map((target) => ({
368
+ kind: target.kind as RoutineScheduleDeliveryKind,
369
+ surfaceKind: typeof target.surfaceKind === 'string' && isDeliverySurfaceKind(target.surfaceKind) ? target.surfaceKind : undefined,
370
+ address: redactedDeliveryAddress(typeof target.address === 'string' ? target.address : undefined),
371
+ routeId: typeof target.routeId === 'string' ? target.routeId : undefined,
372
+ label: typeof target.label === 'string' ? target.label : undefined,
373
+ }));
374
+ }
375
+
225
376
  function readReceipt(value: unknown): RoutineScheduleReceipt | null {
226
377
  if (!isRecord(value)) return null;
227
378
  const id = readString(value, 'id')?.trim();
@@ -234,6 +385,23 @@ function readReceipt(value: unknown): RoutineScheduleReceipt | null {
234
385
  : null;
235
386
  const scheduleValue = readString(value, 'scheduleValue')?.trim();
236
387
  const target = isRecord(value.target) ? value.target : {};
388
+ const deliveryTargets = Array.isArray(value.deliveryTargets)
389
+ ? value.deliveryTargets.map((target): RoutineScheduleReceiptDeliveryTarget | null => {
390
+ if (!isRecord(target)) return null;
391
+ const kind = target.kind === 'webhook' || target.kind === 'surface' || target.kind === 'integration' || target.kind === 'link'
392
+ ? target.kind
393
+ : null;
394
+ if (!kind) return null;
395
+ const surfaceKind = readString(target, 'surfaceKind') ?? undefined;
396
+ return {
397
+ kind,
398
+ surfaceKind: surfaceKind && isDeliverySurfaceKind(surfaceKind) ? surfaceKind : undefined,
399
+ address: readString(target, 'address') ?? undefined,
400
+ routeId: readString(target, 'routeId') ?? undefined,
401
+ label: readString(target, 'label') ?? undefined,
402
+ };
403
+ }).filter((target): target is RoutineScheduleReceiptDeliveryTarget => target !== null)
404
+ : undefined;
237
405
  if (!id || !createdAt || !routineId || !routineName || !status || !scheduleKind || !scheduleValue) return null;
238
406
  return {
239
407
  id,
@@ -260,6 +428,7 @@ function readReceipt(value: unknown): RoutineScheduleReceipt | null {
260
428
  createIfMissing: readBoolean(target, 'createIfMissing'),
261
429
  },
262
430
  deliveryMode: readString(value, 'deliveryMode') ?? undefined,
431
+ deliveryTargets,
263
432
  failureKind: value.failureKind === 'confirmation_required'
264
433
  || value.failureKind === 'auth_required'
265
434
  || value.failureKind === 'daemon_unavailable'
@@ -482,6 +651,7 @@ function buildReceipt(
482
651
  enabled: preview.payload.enabled !== false,
483
652
  target: targetSummary(preview.payload),
484
653
  deliveryMode: deliveryMode(preview.payload),
654
+ deliveryTargets: redactedDeliveryTargets(preview.payload.delivery),
485
655
  failureKind: result.ok ? undefined : result.kind,
486
656
  failureError: result.ok ? undefined : result.error,
487
657
  };
@@ -539,6 +709,7 @@ export class RoutineScheduleReceiptStore {
539
709
  export function parseRoutineSchedulePromotionArgs(args: readonly string[]): ParsedRoutineSchedulePromotionArgs {
540
710
  let routineId: string | null = null;
541
711
  let schedule: RoutineScheduleSpec | null = null;
712
+ const deliveryTargets: RoutineScheduleDeliveryTargetSpec[] = [];
542
713
  let name: string | undefined;
543
714
  let timezone: string | undefined;
544
715
  let provider: string | undefined;
@@ -593,6 +764,58 @@ export function parseRoutineSchedulePromotionArgs(args: readonly string[]): Pars
593
764
  };
594
765
  continue;
595
766
  }
767
+ if (optionName === '--delivery-surface' || optionName === '--deliver-surface') {
768
+ const consumed = optionValue(args, index, inlineValue);
769
+ index = consumed.nextIndex;
770
+ const value = consumed.value?.trim();
771
+ if (!value) {
772
+ errors.push(`${optionName} requires a value.`);
773
+ continue;
774
+ }
775
+ const target = parseSurfaceDeliveryTarget(value);
776
+ if (typeof target === 'string') errors.push(target);
777
+ else deliveryTargets.push(target);
778
+ continue;
779
+ }
780
+ if (optionName === '--delivery-route' || optionName === '--deliver-route') {
781
+ const consumed = optionValue(args, index, inlineValue);
782
+ index = consumed.nextIndex;
783
+ const value = consumed.value?.trim();
784
+ if (!value) {
785
+ errors.push(`${optionName} requires a value.`);
786
+ continue;
787
+ }
788
+ const target = parseRouteDeliveryTarget(value);
789
+ if (typeof target === 'string') errors.push(target);
790
+ else deliveryTargets.push(target);
791
+ continue;
792
+ }
793
+ if (optionName === '--delivery-webhook' || optionName === '--deliver-webhook') {
794
+ const consumed = optionValue(args, index, inlineValue);
795
+ index = consumed.nextIndex;
796
+ const value = consumed.value?.trim();
797
+ if (!value) {
798
+ errors.push(`${optionName} requires a value.`);
799
+ continue;
800
+ }
801
+ const target = parseWebhookDeliveryTarget(value);
802
+ if (typeof target === 'string') errors.push(target);
803
+ else deliveryTargets.push(target);
804
+ continue;
805
+ }
806
+ if (optionName === '--delivery-link' || optionName === '--deliver-link') {
807
+ const consumed = optionValue(args, index, inlineValue);
808
+ index = consumed.nextIndex;
809
+ const value = consumed.value?.trim();
810
+ if (!value) {
811
+ errors.push(`${optionName} requires a value.`);
812
+ continue;
813
+ }
814
+ const target = parseLinkDeliveryTarget(value);
815
+ if (typeof target === 'string') errors.push(target);
816
+ else deliveryTargets.push(target);
817
+ continue;
818
+ }
596
819
  if (raw.startsWith('--')) {
597
820
  errors.push(`Unknown option: ${raw}`);
598
821
  continue;
@@ -606,7 +829,9 @@ export function parseRoutineSchedulePromotionArgs(args: readonly string[]): Pars
606
829
 
607
830
  if (!routineId) errors.push('Routine id or name is required.');
608
831
  if (!schedule) errors.push('Schedule is required: use --cron <expr>, --every <interval>, or --at <iso-time>.');
609
- return { routineId, schedule, name, timezone, provider, model, enabled, yes, errors };
832
+ const deliveryError = validateDeliveryTargets(deliveryTargets);
833
+ if (deliveryError) errors.push(deliveryError);
834
+ return { routineId, schedule, deliveryTargets, name, timezone, provider, model, enabled, yes, errors };
610
835
  }
611
836
 
612
837
  export function resolveAgentDaemonConnection(
@@ -669,8 +894,8 @@ export function buildRoutineSchedulePayload(
669
894
  createIfMissing: true,
670
895
  },
671
896
  delivery: {
672
- mode: 'none',
673
- targets: [],
897
+ mode: deliveryModeFromTargets(parsed.deliveryTargets),
898
+ targets: parsed.deliveryTargets.map(toDeliveryTargetInput),
674
899
  fallbackTargets: [],
675
900
  includeSummary: true,
676
901
  includeTranscript: false,
@@ -873,6 +1098,8 @@ export function formatRoutineSchedulePreview(preview: RoutineSchedulePromotionPr
873
1098
  : preview.payload.kind === 'every'
874
1099
  ? String(preview.payload.every)
875
1100
  : String(preview.payload.at);
1101
+ const delivery = preview.payload.delivery;
1102
+ const deliveryTargetCount = delivery?.targets.length ?? 0;
876
1103
  return [
877
1104
  'Daemon schedule preview for Agent routine',
878
1105
  ` routine: ${preview.routineName} (${preview.routineId})`,
@@ -880,6 +1107,7 @@ export function formatRoutineSchedulePreview(preview: RoutineSchedulePromotionPr
880
1107
  ` name: ${String(preview.payload.name ?? '(daemon default)')}`,
881
1108
  ` schedule: ${preview.payload.kind} ${schedule}`,
882
1109
  ` enabled: ${preview.payload.enabled === false ? 'no' : 'yes'}`,
1110
+ ` delivery: ${delivery?.mode ?? 'none'}${deliveryTargetCount > 0 ? ` (${deliveryTargetCount} target${deliveryTargetCount === 1 ? '' : 's'})` : ''}`,
883
1111
  ' target: external daemon service/main conversation route',
884
1112
  ' policy: isolated Agent Knowledge only; no default wiki/HomeGraph fallback; no WRFC unless explicitly delegated',
885
1113
  ' next: rerun with --yes to create this daemon schedule',
@@ -938,6 +1166,7 @@ export function formatRoutineScheduleReceipt(receipt: RoutineScheduleReceipt): s
938
1166
  receipt.model ? ` model: ${receipt.model}` : '',
939
1167
  ` target: ${receipt.target.kind ?? 'unknown'}${receipt.target.surfaceKind ? `/${receipt.target.surfaceKind}` : ''}`,
940
1168
  receipt.deliveryMode ? ` delivery: ${receipt.deliveryMode}` : '',
1169
+ ...(receipt.deliveryTargets ?? []).map((target) => ` delivery target: ${target.kind}${target.surfaceKind ? `/${target.surfaceKind}` : ''}${target.routeId ? ` route=${target.routeId}` : ''}${target.address ? ` address=${target.address}` : ''}${target.label ? ` label=${target.label}` : ''}`),
941
1170
  receipt.failureKind ? ` failure: ${receipt.failureKind}` : '',
942
1171
  receipt.failureError ? ` error: ${receipt.failureError}` : '',
943
1172
  ].filter((line): line is string => Boolean(line)).join('\n');
package/src/cli/help.ts CHANGED
@@ -168,7 +168,7 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
168
168
  'routines receipts',
169
169
  'routines reconcile',
170
170
  'routines receipt <receipt-id>',
171
- 'routines promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) [--timezone <tz>] [--name <schedule-name>] [--provider <id>] [--model <model>] [--disabled] --yes',
171
+ 'routines promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) [--timezone <tz>] [--name <schedule-name>] [--provider <id>] [--model <model>] [--delivery-surface <surface[:route[:label]]>|--delivery-route <route[:label]>|--delivery-webhook <url>|--delivery-link <url>] [--disabled] --yes',
172
172
  ],
173
173
  summary: 'Inspect Agent-local routines, review local promotion receipts, reconcile receipts against live daemon schedules, and explicitly promote a reviewed routine into an external daemon schedule. Without --yes, promote only prints the schedules.create preview.',
174
174
  examples: [
@@ -176,7 +176,7 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
176
176
  'routines show daily-operations-sweep',
177
177
  'routines receipts',
178
178
  'routines reconcile',
179
- 'routines promote daily-operations-sweep --cron "0 9 * * *" --timezone America/Chicago --yes',
179
+ 'routines promote daily-operations-sweep --cron "0 9 * * *" --timezone America/Chicago --delivery-surface slack --yes',
180
180
  'routines promote weekly-review --every 7d --disabled',
181
181
  ],
182
182
  },
@@ -100,7 +100,7 @@ async function handleRoutinePromotion(runtime: CliCommandRuntime, args: readonly
100
100
  };
101
101
  return {
102
102
  output: json ? JSON.stringify(failure, null, 2) : [
103
- 'Usage: goodvibes-agent routines promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) [--timezone <tz>] [--name <schedule-name>] [--provider <id>] [--model <model>] [--disabled] --yes',
103
+ 'Usage: goodvibes-agent routines promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) [--timezone <tz>] [--name <schedule-name>] [--provider <id>] [--model <model>] [--delivery-surface <surface[:route[:label]]>|--delivery-route <route[:label]>|--delivery-webhook <url>|--delivery-link <url>] [--disabled] --yes',
104
104
  ...parsed.errors.map((error) => ` ${error}`),
105
105
  ].join('\n'),
106
106
  exitCode: 2,
@@ -237,7 +237,7 @@ export async function handleRoutinesCommand(runtime: CliCommandRuntime): Promise
237
237
  return handleRoutinePromotion(runtime, rest);
238
238
  }
239
239
  return {
240
- output: 'Usage: goodvibes-agent routines [list|enabled|show <id>|receipts|reconcile|receipt <id>|promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) --yes]',
240
+ output: 'Usage: goodvibes-agent routines [list|enabled|show <id>|receipts|reconcile|receipt <id>|promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) [--delivery-surface <surface>|--delivery-route <route>|--delivery-webhook <url>] --yes]',
241
241
  exitCode: 2,
242
242
  };
243
243
  }
@@ -573,10 +573,10 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
573
573
  group: 'WATCH',
574
574
  label: 'Automation',
575
575
  summary: 'Automation and schedule observability with explicit routine promotion.',
576
- detail: 'Agent does not create local automation jobs or hidden scheduler spawns. Reviewed local routines can be promoted into externally owned daemon schedules only through an explicit schedules.create command with --yes, then reconciled against live daemon schedules from a redacted local receipt.',
576
+ detail: 'Agent does not create local automation jobs or hidden scheduler spawns. Reviewed local routines can be promoted into externally owned daemon schedules only through an explicit schedules.create command with --yes, optional delivery targets, and a redacted local receipt.',
577
577
  actions: [
578
578
  { id: 'schedule-list', label: 'List schedules', detail: 'Inspect configured jobs and history without running or mutating them.', command: '/schedule list', kind: 'command', safety: 'read-only' },
579
- { id: 'schedule-promote-routine', label: 'Promote routine', detail: 'Create an external daemon schedule from a local Agent routine. Requires a real routine id, schedule expression, and explicit --yes.', command: '/schedule promote-routine <routine-id> --cron <expr> --yes', kind: 'command', safety: 'safe' },
579
+ { id: 'schedule-promote-routine', label: 'Promote routine', detail: 'Create an external daemon schedule from a local Agent routine. Requires a real routine id, schedule expression, optional delivery target, and explicit --yes.', command: '/schedule promote-routine <routine-id> --cron <expr> [--delivery-surface slack] --yes', kind: 'command', safety: 'safe' },
580
580
  { id: 'schedule-receipts', label: 'Promotion receipts', detail: 'Review local redacted receipt history for routine-to-daemon schedule promotion attempts.', command: '/schedule receipts', kind: 'command', safety: 'read-only' },
581
581
  { id: 'schedule-reconcile', label: 'Reconcile schedules', detail: 'Compare local promotion receipts with live externally owned daemon schedules using schedules.list.', command: '/schedule reconcile', kind: 'command', safety: 'read-only' },
582
582
  { id: 'schedule-policy', label: 'Local scheduler blocked', detail: 'Local schedule add/run/remove/enable/disable remain blocked; only explicit external daemon schedule promotion is allowed here.', kind: 'guidance', safety: 'blocked' },
@@ -295,7 +295,7 @@ export function registerRoutinesRuntimeCommands(registry: CommandRegistry): void
295
295
  name: 'routines',
296
296
  aliases: ['routine'],
297
297
  description: 'Manage local GoodVibes Agent routines',
298
- usage: '[list|enabled|search <query>|show <id>|receipts|reconcile|receipt <id>|create --name <name> --description <summary> --steps <steps>|update <id> [--name ...] [--description ...] [--steps ...]|enable <id>|disable <id>|start <id>|review <id>|stale <id> <reason...>|promote <id> --cron <expr> --yes|delete <id> --yes]',
298
+ usage: '[list|enabled|search <query>|show <id>|receipts|reconcile|receipt <id>|create --name <name> --description <summary> --steps <steps>|update <id> [--name ...] [--description ...] [--steps ...]|enable <id>|disable <id>|start <id>|review <id>|stale <id> <reason...>|promote <id> --cron <expr> [--delivery-surface slack] --yes|delete <id> --yes]',
299
299
  handler: runRoutinesRuntimeCommand,
300
300
  });
301
301
  }
@@ -60,7 +60,7 @@ async function promoteRoutineSchedule(args: readonly string[], ctx: CommandConte
60
60
  const parsed = parseRoutineSchedulePromotionArgs(args);
61
61
  if (parsed.errors.length > 0) {
62
62
  ctx.print([
63
- 'Usage: /schedule promote-routine <routine-id> (--cron <expr>|--every <interval>|--at <iso-time>) [--timezone <tz>] [--name <schedule-name>] [--provider <id>] [--model <model>] [--disabled] --yes',
63
+ 'Usage: /schedule promote-routine <routine-id> (--cron <expr>|--every <interval>|--at <iso-time>) [--timezone <tz>] [--name <schedule-name>] [--provider <id>] [--model <model>] [--delivery-surface <surface[:route[:label]]>|--delivery-route <route[:label]>|--delivery-webhook <url>|--delivery-link <url>] [--disabled] --yes',
64
64
  ...parsed.errors.map((error) => ` ${error}`),
65
65
  ].join('\n'));
66
66
  return;
@@ -87,8 +87,8 @@ export function registerScheduleRuntimeCommands(registry: CommandRegistry): void
87
87
  name: 'schedule',
88
88
  aliases: ['sched'],
89
89
  description: 'Inspect schedules and explicitly promote local Agent routines to daemon schedules',
90
- usage: 'list | receipts | reconcile | receipt <id> | promote-routine <routine-id> --cron <expr> --yes',
91
- argsHint: 'list | receipts | reconcile | receipt <id> | promote-routine <routine-id> --cron <expr> --yes',
90
+ usage: 'list | receipts | reconcile | receipt <id> | promote-routine <routine-id> --cron <expr> [--delivery-surface slack] --yes',
91
+ argsHint: 'list | receipts | reconcile | receipt <id> | promote-routine <routine-id> --cron <expr> [--delivery-surface slack] --yes',
92
92
  async handler(args, ctx) {
93
93
  const sub = args[0];
94
94
 
@@ -160,7 +160,7 @@ export function registerScheduleRuntimeCommands(registry: CommandRegistry): void
160
160
  + ' /schedule receipts\n'
161
161
  + ' /schedule reconcile\n'
162
162
  + ' /schedule receipt <receipt-id>\n'
163
- + ' /schedule promote-routine <routine-id> (--cron <expr>|--every <interval>|--at <iso-time>) --yes\n'
163
+ + ' /schedule promote-routine <routine-id> (--cron <expr>|--every <interval>|--at <iso-time>) [--delivery-surface <surface>|--delivery-route <route>|--delivery-webhook <url>] --yes\n'
164
164
  + ' Local schedule mutations and runs remain blocked.'
165
165
  );
166
166
  },
@@ -14,6 +14,7 @@ export interface OperatorCapabilityBenchmark {
14
14
  readonly posture: CapabilityPosture;
15
15
  readonly competitors: readonly CompetitorProduct[];
16
16
  readonly competitorBaseline: string;
17
+ readonly goodvibesDaemon?: string;
17
18
  readonly goodvibesAgent: string;
18
19
  readonly configure: readonly string[];
19
20
  readonly use: readonly string[];
@@ -43,6 +44,7 @@ export const OPERATOR_CAPABILITY_BENCHMARK_SOURCES = [
43
44
  'https://hermes-agent.nousresearch.com/docs/user-guide/features/voice-mode/',
44
45
  'https://hermes-agent.nousresearch.com/docs/user-guide/features/api-server/',
45
46
  'https://hermes-agent.nousresearch.com/docs/user-guide/profiles/',
47
+ '@pellux/goodvibes-sdk@0.33.35 public operator contract and /api/goodvibes-agent/knowledge routes',
46
48
  ] as const;
47
49
 
48
50
  export const OPERATOR_CAPABILITY_BENCHMARKS: readonly OperatorCapabilityBenchmark[] = [
@@ -64,6 +66,7 @@ export const OPERATOR_CAPABILITY_BENCHMARKS: readonly OperatorCapabilityBenchmar
64
66
  posture: 'external-daemon',
65
67
  competitors: ['openclaw', 'hermes'],
66
68
  competitorBaseline: 'Always-on gateway/service provides channel ingress, sessions, tools, events, and scheduled execution.',
69
+ goodvibesDaemon: 'Daemon exposes status/auth/control, sessions, companion chat, channels, remote peers, approvals, automation, schedules, artifacts, MCP, providers, voice, media, web search, and isolated Agent Knowledge routes.',
67
70
  goodvibesAgent: 'Connects to the GoodVibes daemon owned by GoodVibes TUI/daemon tooling; Agent never starts, stops, or owns daemon lifecycle.',
68
71
  configure: ['goodvibes-agent compat', 'goodvibes-agent service check', 'goodvibes-agent control-plane status'],
69
72
  use: ['goodvibes-agent status', 'goodvibes-agent doctor'],
@@ -76,6 +79,7 @@ export const OPERATOR_CAPABILITY_BENCHMARKS: readonly OperatorCapabilityBenchmar
76
79
  posture: 'configurable',
77
80
  competitors: ['openclaw', 'hermes'],
78
81
  competitorBaseline: 'Messaging gateway for WhatsApp, Telegram, Slack, Discord, Signal, iMessage, web chat, and related platforms.',
82
+ goodvibesDaemon: 'Public channel routes include channels.status, channels.capabilities.*, channels.accounts.*, channels.setup.*, channels.directory.*, channels.actions.*, channels.tools.*, channels.targets.resolve, pairing, and companion chat routes.',
79
83
  goodvibesAgent: 'Uses GoodVibes daemon channel, companion, pairing, QR, communication, and session surfaces while keeping side effects behind explicit user action. The Agent workspace exposes channel setup, per-channel readiness, default-target posture, and risk labels as a first-class operator area.',
80
84
  configure: ['goodvibes-agent pair', 'goodvibes-agent qrcode', 'goodvibes-agent surfaces check', '/agent → Channels'],
81
85
  use: ['/agent → Channels', '/communication', '/pair'],
@@ -88,6 +92,7 @@ export const OPERATOR_CAPABILITY_BENCHMARKS: readonly OperatorCapabilityBenchmar
88
92
  posture: 'ready',
89
93
  competitors: ['openclaw', 'hermes'],
90
94
  competitorBaseline: 'Persistent memory and knowledge/wiki layers with search, recall, provenance, and freshness checks.',
95
+ goodvibesDaemon: 'Agent-specific daemon routes cover /api/goodvibes-agent/knowledge/status, ask, search, ingest, source/node/issue/candidate/refinement/report/job/schedule, projection, GraphQL, and usage surfaces.',
91
96
  goodvibesAgent: 'Uses only /api/goodvibes-agent/knowledge/*; never falls back to default Knowledge/Wiki, HomeGraph, or Home Assistant routes. The Agent workspace exposes isolated ask/search/status, URL/bookmark ingestion, review queue, and consolidation workflows.',
92
97
  configure: ['goodvibes-agent compat', 'goodvibes-agent knowledge status', '/agent → Knowledge'],
93
98
  use: ['goodvibes-agent ask <question>', 'goodvibes-agent search <query>', '/knowledge ask <question>', '/knowledge ingest-url <url> --yes', '/knowledge queue'],
@@ -112,11 +117,12 @@ export const OPERATOR_CAPABILITY_BENCHMARKS: readonly OperatorCapabilityBenchmar
112
117
  posture: 'configurable',
113
118
  competitors: ['openclaw', 'hermes'],
114
119
  competitorBaseline: 'Cron/scheduler can create, pause, resume, run, remove, and deliver recurring tasks from natural language.',
115
- goodvibesAgent: 'Observes public automation/schedule routes, keeps local routines separate from daemon jobs, and promotes a local routine into an external daemon schedules.create record only through an exact user command with --yes.',
116
- configure: ['/schedule list', '/routines create ...', '/schedule promote-routine <id> --cron "0 8 * * *" --yes', 'goodvibes-agent routines promote <id> --every 1d --yes'],
117
- use: ['/schedule list', '/routines start <id>', '/schedule promote-routine <id> --cron <expr> --yes'],
118
- exceedsBy: ['No recursive hidden scheduler creation from model tools', 'explicit confirmation for side effects', 'local routines separate from daemon jobs', 'scheduled prompts preserve isolated Agent Knowledge and forbid default wiki/HomeGraph fallback'],
119
- next: ['Add delivery target selection and deeper live run/delivery history for promoted routines.'],
120
+ goodvibesDaemon: 'Public daemon routes cover automation.integration.snapshot, automation.jobs.*, automation.runs.*, automation.heartbeat.*, scheduler.capacity, schedules.create/list/run/enable/disable/delete, and delivery policy fields.',
121
+ goodvibesAgent: 'Observes public automation/schedule routes, keeps local routines separate from daemon jobs, and promotes a local routine into an external daemon schedules.create record only through an exact user command with --yes and optional explicit delivery targets.',
122
+ configure: ['/schedule list', '/routines create ...', '/schedule promote-routine <id> --cron "0 8 * * *" --delivery-surface slack --yes', 'goodvibes-agent routines promote <id> --every 1d --delivery-webhook https://example.test/hook --yes'],
123
+ use: ['/schedule list', '/routines start <id>', '/schedule promote-routine <id> --cron <expr> [--delivery-surface slack] --yes', '/schedule receipts', '/schedule reconcile'],
124
+ exceedsBy: ['No recursive hidden scheduler creation from model tools', 'explicit confirmation for side effects', 'local routines separate from daemon jobs', 'explicit delivery target selection', 'redacted promotion receipts', 'scheduled prompts preserve isolated Agent Knowledge and forbid default wiki/HomeGraph fallback'],
125
+ next: ['Add deeper live run/delivery history for promoted routines and expose delivery attempt errors in the operator workspace.'],
120
126
  },
121
127
  {
122
128
  id: 'tool-gateway-mcp',
@@ -124,6 +130,7 @@ export const OPERATOR_CAPABILITY_BENCHMARKS: readonly OperatorCapabilityBenchmar
124
130
  posture: 'configurable',
125
131
  competitors: ['openclaw', 'hermes'],
126
132
  competitorBaseline: 'Broad toolsets, MCP integration, browser/web/media tools, and configurable platform-specific tool availability.',
133
+ goodvibesDaemon: 'Daemon public routes include mcp.servers/tools/config, artifacts.create/get/list/content, web_search.providers/query, providers/model surfaces, media.analyze/generate/transform, multimodal providers, and channel tool/action registries.',
127
134
  goodvibesAgent: 'Uses GoodVibes SDK tool registry, MCP inspection, provider tools, web search, media, plugins, and policy-gated model-visible tools.',
128
135
  configure: ['/mcp servers', '/plugin list', 'goodvibes-agent providers', 'goodvibes-agent models'],
129
136
  use: ['/mcp tools', '/provider current', 'goodvibes-agent search <query>'],
@@ -136,6 +143,7 @@ export const OPERATOR_CAPABILITY_BENCHMARKS: readonly OperatorCapabilityBenchmar
136
143
  posture: 'configurable',
137
144
  competitors: ['openclaw', 'hermes'],
138
145
  competitorBaseline: 'Voice/TTS, mobile nodes, live canvas, browser automation, image/video generation, and multimodal analysis.',
146
+ goodvibesDaemon: 'Daemon public routes include voice.status/providers/voices/tts/stt/realtime, media providers/analyze/generate/transform, multimodal providers, artifacts, remote.snapshot/peers/work, and channel media-capable surfaces.',
139
147
  goodvibesAgent: 'Uses GoodVibes voice/media/browser/node primitives and exposes an Agent workspace for TTS setup, image input, browser/web posture, MCP browser tools, and node/remote inspection.',
140
148
  configure: ['/agent → Voice, Media & Nodes', '/config tts', '/voice review', '/mcp servers'],
141
149
  use: ['/tts <prompt>', '/image <path> <prompt>', '/remote list'],
@@ -148,6 +156,7 @@ export const OPERATOR_CAPABILITY_BENCHMARKS: readonly OperatorCapabilityBenchmar
148
156
  posture: 'explicit-delegation',
149
157
  competitors: ['openclaw', 'hermes'],
150
158
  competitorBaseline: 'Terminal/file/code tools and subagents can execute software tasks directly.',
159
+ goodvibesDaemon: 'Daemon shared-session routes cover sessions.create/list/get/messages/followUp/steer/close/reopen and task/workflow visibility used by GoodVibes TUI-owned execution.',
151
160
  goodvibesAgent: 'Main assistant stays serial. Explicit build/fix/review/code work is delegated to GoodVibes TUI/shared-session contracts; WRFC is opt-in only.',
152
161
  configure: ['goodvibes-agent delegate --help', 'goodvibes-agent compat'],
153
162
  use: ['goodvibes-agent delegate "fix the failing tests"', 'goodvibes-agent delegate --wrfc "implement and review the feature"'],
@@ -172,6 +181,7 @@ export const OPERATOR_CAPABILITY_BENCHMARKS: readonly OperatorCapabilityBenchmar
172
181
  posture: 'ready',
173
182
  competitors: ['openclaw', 'hermes'],
174
183
  competitorBaseline: 'Command approval, DM pairing, sandboxing, allowlists, and safety defaults for exposed channels.',
184
+ goodvibesDaemon: 'Daemon public routes include approvals.list/claim/approve/deny/cancel, channel policies/allowlists, local auth/session controls, secrets, and route danger metadata in the operator contract.',
175
185
  goodvibesAgent: 'Uses daemon approvals, local auth diagnostics, secret refs, explicit confirmation gates, and Agent model-tool policy guards.',
176
186
  configure: ['goodvibes-agent auth status', 'goodvibes-agent secrets providers', '/approvals list'],
177
187
  use: ['/policy status', '/approvals list', 'goodvibes-agent doctor'],
@@ -218,6 +228,7 @@ export function renderOperatorCapabilityBenchmark(
218
228
  lines.push(`${capability.title} [${capability.posture}]`);
219
229
  lines.push(` competitors: ${capability.competitors.join(', ')}`);
220
230
  lines.push(` baseline: ${capability.competitorBaseline}`);
231
+ if (capability.goodvibesDaemon) lines.push(` daemon: ${capability.goodvibesDaemon}`);
221
232
  lines.push(` Agent: ${capability.goodvibesAgent}`);
222
233
  lines.push(` configure: ${capability.configure.join(' | ')}`);
223
234
  lines.push(` use: ${capability.use.join(' | ')}`);
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.1.39';
9
+ let _version = '0.1.40';
10
10
  let _sdkVersion = '0.33.35';
11
11
  try {
12
12
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8')) as {