@pellux/goodvibes-agent 0.1.39 → 0.1.41
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 +8 -0
- package/README.md +13 -3
- package/docs/operator-capability-benchmark.md +21 -7
- package/package.json +1 -1
- package/src/agent/routine-schedule-promotion.ts +232 -3
- package/src/cli/capabilities-command.ts +39 -4
- package/src/cli/help.ts +7 -6
- package/src/cli/routines-command.ts +2 -2
- package/src/input/agent-workspace.ts +2 -2
- package/src/input/commands/capabilities-runtime.ts +23 -3
- package/src/input/commands/routines-runtime.ts +1 -1
- package/src/input/commands/schedule-runtime.ts +4 -4
- package/src/operator/capability-benchmark.ts +17 -5
- package/src/operator/daemon-capability-audit.ts +554 -0
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GoodVibes Agent will be recorded here.
|
|
4
4
|
|
|
5
|
+
## 0.1.41 - 2026-05-31
|
|
6
|
+
|
|
7
|
+
- c108e13 Add live daemon capability audit
|
|
8
|
+
|
|
9
|
+
## 0.1.40 - 2026-05-31
|
|
10
|
+
|
|
11
|
+
- 329dc13 Add routine schedule delivery targets
|
|
12
|
+
|
|
5
13
|
## 0.1.39 - 2026-05-31
|
|
6
14
|
|
|
7
15
|
- c98de19 Add routine schedule reconciliation
|
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ bun add -g @pellux/goodvibes-agent
|
|
|
16
16
|
goodvibes-agent --help
|
|
17
17
|
goodvibes-agent status
|
|
18
18
|
goodvibes-agent capabilities
|
|
19
|
+
goodvibes-agent capabilities daemon
|
|
19
20
|
```
|
|
20
21
|
|
|
21
22
|
If Bun reports untrusted lifecycle dependencies, trust only the package and dependencies required by this package:
|
|
@@ -44,7 +45,7 @@ bun run publish:check
|
|
|
44
45
|
|
|
45
46
|
Inside the Agent TUI, use `/agent`, `/home`, or `/operator` to open the operator workspace. It is the Agent-first fullscreen surface for setup, status, knowledge, local memory/skills, work-plan/approval review, automation observability, and explicit build delegation to GoodVibes TUI.
|
|
46
47
|
|
|
47
|
-
Use `goodvibes-agent capabilities` or `/capabilities` to inspect the OpenClaw/Hermes benchmark, current Agent posture, configuration commands, usage paths, and remaining gaps.
|
|
48
|
+
Use `goodvibes-agent capabilities` or `/capabilities` to inspect the OpenClaw/Hermes benchmark, current Agent posture, configuration commands, usage paths, and remaining gaps. Use `goodvibes-agent capabilities daemon` or `/capabilities daemon` for a live read-only audit of the GoodVibes daemon method catalog and isolated Agent Knowledge route coverage.
|
|
48
49
|
|
|
49
50
|
Inside the workspace, use `/agent-profile guide` to author custom profile starters without leaving the Agent TUI. The guided flow lists starters, exports starter JSON, imports edited local starters, and creates isolated runtime profiles from them.
|
|
50
51
|
|
|
@@ -68,19 +69,28 @@ Local Agent behavior is editable from the TUI:
|
|
|
68
69
|
/personas use research
|
|
69
70
|
/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
71
|
/routines start evening-review
|
|
71
|
-
/schedule promote-routine evening-review --cron "0 17 * * 1-5" --timezone America/Chicago --yes
|
|
72
|
+
/schedule promote-routine evening-review --cron "0 17 * * 1-5" --timezone America/Chicago --delivery-surface slack --yes
|
|
72
73
|
/schedule receipts
|
|
73
74
|
/schedule reconcile
|
|
74
75
|
/agent-skills create --name "Morning Brief" --description "Daily briefing flow" --procedure "Check tasks, approvals, calendar, and unread state before summarizing." --enabled true
|
|
75
76
|
/skills local list
|
|
76
77
|
```
|
|
77
78
|
|
|
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`.
|
|
79
|
+
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
80
|
|
|
80
81
|
## Daemon Prerequisite
|
|
81
82
|
|
|
82
83
|
Start or restart the daemon from GoodVibes TUI or the daemon host before launching Agent. Agent status and companion/knowledge routes connect to that external daemon, normally on `http://127.0.0.1:3421`.
|
|
83
84
|
|
|
85
|
+
To verify what the running daemon can expose for the Agent/OpenClaw/Hermes capability benchmark:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
goodvibes-agent capabilities daemon
|
|
89
|
+
goodvibes-agent capabilities daemon --json
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This audit checks `/api/control-plane/methods` and `/api/goodvibes-agent/knowledge/status`. It does not query default Knowledge/Wiki or HomeGraph.
|
|
93
|
+
|
|
84
94
|
Agent intentionally blocks daemon lifecycle commands:
|
|
85
95
|
|
|
86
96
|
```sh
|
|
@@ -15,6 +15,9 @@ Use the live benchmark from the package:
|
|
|
15
15
|
goodvibes-agent capabilities
|
|
16
16
|
goodvibes-agent capabilities --json
|
|
17
17
|
goodvibes-agent capabilities hermes
|
|
18
|
+
goodvibes-agent capabilities daemon
|
|
19
|
+
goodvibes-agent capabilities daemon --json
|
|
20
|
+
goodvibes-agent capabilities daemon knowledge
|
|
18
21
|
```
|
|
19
22
|
|
|
20
23
|
Inside the TUI:
|
|
@@ -23,6 +26,7 @@ Inside the TUI:
|
|
|
23
26
|
/capabilities
|
|
24
27
|
/capabilities openclaw
|
|
25
28
|
/capabilities knowledge
|
|
29
|
+
/capabilities daemon
|
|
26
30
|
```
|
|
27
31
|
|
|
28
32
|
## Research Baseline
|
|
@@ -43,19 +47,29 @@ Primary sources used for the benchmark:
|
|
|
43
47
|
- Hermes Voice: https://hermes-agent.nousresearch.com/docs/user-guide/features/voice-mode/
|
|
44
48
|
- Hermes API Server: https://hermes-agent.nousresearch.com/docs/user-guide/features/api-server/
|
|
45
49
|
- Hermes Profiles: https://hermes-agent.nousresearch.com/docs/user-guide/profiles/
|
|
50
|
+
- GoodVibes daemon: `@pellux/goodvibes-sdk@0.33.35` public operator contract plus `/api/goodvibes-agent/knowledge/*`
|
|
51
|
+
|
|
52
|
+
The benchmark measures two different GoodVibes layers:
|
|
53
|
+
|
|
54
|
+
- daemon capability: what the externally owned GoodVibes daemon can already expose through public operator routes;
|
|
55
|
+
- Agent usability: what GoodVibes Agent makes configurable, visible, safe, and usable from day one.
|
|
56
|
+
|
|
57
|
+
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.
|
|
58
|
+
|
|
59
|
+
Use `goodvibes-agent capabilities daemon` for the live read-only daemon audit. It checks the public control-plane method catalog and the isolated Agent Knowledge status route. It intentionally does not call default `/api/knowledge/*`, HomeGraph, or Home Assistant routes.
|
|
46
60
|
|
|
47
61
|
## Capability Targets
|
|
48
62
|
|
|
49
63
|
| Area | OpenClaw/Hermes Baseline | GoodVibes Agent Position |
|
|
50
64
|
| --- | --- | --- |
|
|
51
65
|
| 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,
|
|
66
|
+
| 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
67
|
| 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
|
|
68
|
+
| 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
69
|
| 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
|
|
57
|
-
| Tools/MCP | Broad toolsets, MCP, browser, media, terminal, files | GoodVibes
|
|
58
|
-
| Voice/media/canvas/nodes | Voice, TTS, mobile nodes, live canvas, browser automation | GoodVibes media/
|
|
70
|
+
| 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 |
|
|
71
|
+
| 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 |
|
|
72
|
+
| 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
73
|
| Build/code work | Direct terminal/file/code tools and subagents | Explicit delegation to GoodVibes TUI; local WRFC/spawn fanout blocked |
|
|
60
74
|
| 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
75
|
| Security | DM pairing, approvals, sandboxing, allowlists | Daemon approvals, auth diagnostics, secret refs, confirmation gates, model-tool policy |
|
|
@@ -66,7 +80,7 @@ GoodVibes Agent should exceed OpenClaw/Hermes by making these properties true fr
|
|
|
66
80
|
|
|
67
81
|
- Capability surfaces are discoverable through `goodvibes-agent capabilities`, `/capabilities`, onboarding, and the operator workspace.
|
|
68
82
|
- Agent Knowledge isolation is a release gate, not a convention.
|
|
69
|
-
- Routine-to-schedule promotion preserves Agent Knowledge isolation
|
|
83
|
+
- Routine-to-schedule promotion preserves Agent Knowledge isolation, uses only public external daemon schedule routes, supports explicit delivery targets, and stores redacted receipts.
|
|
70
84
|
- Model-visible tools are policy-gated for serial, non-secret, non-destructive use.
|
|
71
85
|
- Personal assistant state is Agent-local unless an explicit Agent Knowledge ingest route is used.
|
|
72
86
|
- Build work is delegated to the product that owns coding execution instead of turning the personal operator into a second coding TUI.
|
|
@@ -78,7 +92,7 @@ GoodVibes Agent should exceed OpenClaw/Hermes by making these properties true fr
|
|
|
78
92
|
- Artifact and multimodal Agent Knowledge ingest affordances once Agent-specific routes are stable.
|
|
79
93
|
- Visual starter-template editing inside the fullscreen Agent workspace after the command-guided authoring path.
|
|
80
94
|
- Artifact and multimodal Agent Knowledge ingestion when the isolated Agent route accepts artifact-backed media.
|
|
81
|
-
-
|
|
95
|
+
- Deeper live run/delivery history and delivery error surfacing for promoted routines.
|
|
82
96
|
- Delegation receipts and artifact review inside the operator workspace.
|
|
83
97
|
- Approval center with route risk labels and saved policy presets.
|
|
84
98
|
- 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.
|
|
3
|
+
"version": "0.1.41",
|
|
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
|
-
|
|
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:
|
|
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');
|
|
@@ -5,16 +5,51 @@ import {
|
|
|
5
5
|
filterOperatorCapabilities,
|
|
6
6
|
renderOperatorCapabilityBenchmark,
|
|
7
7
|
} from '../operator/capability-benchmark.ts';
|
|
8
|
+
import {
|
|
9
|
+
fetchLiveDaemonCapabilityAudit,
|
|
10
|
+
filterDaemonCapabilityAuditAreas,
|
|
11
|
+
renderDaemonCapabilityAudit,
|
|
12
|
+
renderDaemonCapabilityFailure,
|
|
13
|
+
} from '../operator/daemon-capability-audit.ts';
|
|
14
|
+
import { resolveAgentDaemonConnection } from '../agent/routine-schedule-promotion.ts';
|
|
15
|
+
|
|
16
|
+
interface CapabilityCommandArgs {
|
|
17
|
+
readonly mode: 'benchmark' | 'daemon';
|
|
18
|
+
readonly query: string | undefined;
|
|
19
|
+
}
|
|
8
20
|
|
|
9
|
-
function
|
|
21
|
+
function readCapabilityArgs(args: readonly string[]): CapabilityCommandArgs {
|
|
10
22
|
const values = args.filter((arg) => !arg.startsWith('--'));
|
|
11
|
-
|
|
23
|
+
if (values[0] === 'daemon') {
|
|
24
|
+
const query = values.slice(1).join(' ').trim();
|
|
25
|
+
return { mode: 'daemon', query: query.length > 0 ? query : undefined };
|
|
26
|
+
}
|
|
27
|
+
return { mode: 'benchmark', query: values.length > 0 ? values.join(' ') : undefined };
|
|
12
28
|
}
|
|
13
29
|
|
|
14
30
|
export async function handleCapabilitiesCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
|
|
15
|
-
const
|
|
31
|
+
const args = readCapabilityArgs(runtime.cli.commandArgs);
|
|
32
|
+
if (args.mode === 'daemon') {
|
|
33
|
+
const connection = resolveAgentDaemonConnection(runtime.configManager, runtime.homeDirectory);
|
|
34
|
+
const audit = await fetchLiveDaemonCapabilityAudit(connection);
|
|
35
|
+
if (!audit.ok) {
|
|
36
|
+
return {
|
|
37
|
+
output: runtime.cli.flags.outputFormat === 'json'
|
|
38
|
+
? JSON.stringify(audit, null, 2)
|
|
39
|
+
: renderDaemonCapabilityFailure(audit),
|
|
40
|
+
exitCode: audit.kind === 'auth_required' || audit.kind === 'daemon_unavailable' ? 1 : 2,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const areas = filterDaemonCapabilityAuditAreas(audit.areas, args.query);
|
|
44
|
+
return {
|
|
45
|
+
output: runtime.cli.flags.outputFormat === 'json'
|
|
46
|
+
? JSON.stringify({ ...audit, areas }, null, 2)
|
|
47
|
+
: renderDaemonCapabilityAudit(audit, areas),
|
|
48
|
+
exitCode: 0,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
16
51
|
const report = buildOperatorCapabilityBenchmarkReport();
|
|
17
|
-
const capabilities = filterOperatorCapabilities(report.capabilities, query);
|
|
52
|
+
const capabilities = filterOperatorCapabilities(report.capabilities, args.query);
|
|
18
53
|
if (runtime.cli.flags.outputFormat === 'json') {
|
|
19
54
|
return {
|
|
20
55
|
output: JSON.stringify({ ...report, capabilities }, null, 2),
|
package/src/cli/help.ts
CHANGED
|
@@ -42,7 +42,7 @@ export function renderGoodVibesHelp(binary = 'goodvibes-agent'): string {
|
|
|
42
42
|
' routines Inspect local routines and explicitly promote one to a daemon schedule',
|
|
43
43
|
' auth Inspect and manage local users, sessions, and bootstrap auth',
|
|
44
44
|
' compat Inspect Agent SDK pin, daemon version, and Agent knowledge route readiness',
|
|
45
|
-
' capabilities Show OpenClaw/Hermes
|
|
45
|
+
' capabilities Show OpenClaw/Hermes benchmark and live daemon coverage',
|
|
46
46
|
' knowledge Use isolated Agent Knowledge/Wiki routes',
|
|
47
47
|
' ask|search Shortcuts for isolated Agent Knowledge ask/search',
|
|
48
48
|
' delegate Explicitly delegate build/fix/review work to GoodVibes TUI',
|
|
@@ -102,6 +102,7 @@ export function renderGoodVibesHelp(binary = 'goodvibes-agent'): string {
|
|
|
102
102
|
` ${binary} routines promote daily-operations-sweep --cron "0 9 * * *" --timezone America/Chicago --yes`,
|
|
103
103
|
` ${binary} compat`,
|
|
104
104
|
` ${binary} capabilities`,
|
|
105
|
+
` ${binary} capabilities daemon`,
|
|
105
106
|
` ${binary} knowledge status`,
|
|
106
107
|
` ${binary} knowledge ask "What is GoodVibes Agent?"`,
|
|
107
108
|
` ${binary} ask "What is GoodVibes Agent?"`,
|
|
@@ -168,7 +169,7 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
168
169
|
'routines receipts',
|
|
169
170
|
'routines reconcile',
|
|
170
171
|
'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',
|
|
172
|
+
'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
173
|
],
|
|
173
174
|
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
175
|
examples: [
|
|
@@ -176,7 +177,7 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
176
177
|
'routines show daily-operations-sweep',
|
|
177
178
|
'routines receipts',
|
|
178
179
|
'routines reconcile',
|
|
179
|
-
'routines promote daily-operations-sweep --cron "0 9 * * *" --timezone America/Chicago --yes',
|
|
180
|
+
'routines promote daily-operations-sweep --cron "0 9 * * *" --timezone America/Chicago --delivery-surface slack --yes',
|
|
180
181
|
'routines promote weekly-review --every 7d --disabled',
|
|
181
182
|
],
|
|
182
183
|
},
|
|
@@ -196,9 +197,9 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
196
197
|
examples: ['compat', 'compat --json'],
|
|
197
198
|
},
|
|
198
199
|
capabilities: {
|
|
199
|
-
usage: ['capabilities [openclaw|hermes|query]', 'capabilities --json'],
|
|
200
|
-
summary: 'Show the OpenClaw/Hermes capability benchmark, Agent readiness,
|
|
201
|
-
examples: ['capabilities', 'capabilities hermes', 'capabilities knowledge --json'],
|
|
200
|
+
usage: ['capabilities [openclaw|hermes|query]', 'capabilities daemon [query]', 'capabilities --json'],
|
|
201
|
+
summary: 'Show the OpenClaw/Hermes capability benchmark, Agent readiness, and live GoodVibes daemon method coverage.',
|
|
202
|
+
examples: ['capabilities', 'capabilities hermes', 'capabilities daemon', 'capabilities daemon knowledge --json'],
|
|
202
203
|
},
|
|
203
204
|
knowledge: {
|
|
204
205
|
usage: [
|
|
@@ -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,
|
|
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' },
|