@pellux/goodvibes-agent 0.1.38 → 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 +8 -0
- package/README.md +3 -2
- package/docs/getting-started.md +2 -0
- package/docs/operator-capability-benchmark.md +15 -7
- package/package.json +1 -1
- package/src/agent/routine-schedule-promotion.ts +529 -3
- package/src/cli/help.ts +5 -3
- package/src/cli/routines-command.ts +15 -2
- package/src/input/agent-workspace.ts +3 -2
- package/src/input/commands/routines-runtime.ts +11 -2
- package/src/input/commands/schedule-runtime.ts +15 -4
- package/src/operator/capability-benchmark.ts +16 -5
- 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.40 - 2026-05-31
|
|
6
|
+
|
|
7
|
+
- 329dc13 Add routine schedule delivery targets
|
|
8
|
+
|
|
9
|
+
## 0.1.39 - 2026-05-31
|
|
10
|
+
|
|
11
|
+
- c98de19 Add routine schedule reconciliation
|
|
12
|
+
|
|
5
13
|
## 0.1.38 - 2026-05-31
|
|
6
14
|
|
|
7
15
|
- 072503c Add routine schedule promotion receipts
|
package/README.md
CHANGED
|
@@ -68,13 +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
|
+
/schedule reconcile
|
|
73
74
|
/agent-skills create --name "Morning Brief" --description "Daily briefing flow" --procedure "Check tasks, approvals, calendar, and unread state before summarizing." --enabled true
|
|
74
75
|
/skills local list
|
|
75
76
|
```
|
|
76
77
|
|
|
77
|
-
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.
|
|
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`.
|
|
78
79
|
|
|
79
80
|
## Daemon Prerequisite
|
|
80
81
|
|
package/docs/getting-started.md
CHANGED
|
@@ -39,6 +39,8 @@ Once the TUI opens, run `/agent`, `/home`, or `/operator` to open the Agent oper
|
|
|
39
39
|
|
|
40
40
|
Use `/agent-profile guide` inside that workspace to walk through starter-profile authoring. It lists built-in and local starters, exports a JSON starter for editing, imports the edited starter back into this Agent home, and creates isolated profiles from the result.
|
|
41
41
|
|
|
42
|
+
Use `/schedule receipts` to review redacted local routine promotion history and `/schedule reconcile` to compare those receipts with live externally owned daemon schedules through public `schedules.list`.
|
|
43
|
+
|
|
42
44
|
## Isolated Agent Profiles
|
|
43
45
|
|
|
44
46
|
Use a separate Agent home when you want isolated local state:
|
|
@@ -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,
|
|
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
|
|
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
|
|
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/
|
|
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
|
|
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
|
-
-
|
|
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.
|
|
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",
|
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from '
|
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { createBrowserGoodVibesSdk } from '@pellux/goodvibes-sdk/browser';
|
|
4
4
|
import type { OperatorMethodInput, OperatorMethodOutput } from '@pellux/goodvibes-sdk/contracts';
|
|
5
|
+
import { formatEveryInterval } from '@pellux/goodvibes-sdk/platform/automation';
|
|
5
6
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
6
7
|
import type { ShellPathService } from '@/runtime/index.ts';
|
|
7
8
|
import { getModelIdFromProviderModel, getProviderIdFromModel } from '../config/provider-model.ts';
|
|
@@ -11,9 +12,13 @@ import type { AgentRoutineRecord } from './routine-registry.ts';
|
|
|
11
12
|
|
|
12
13
|
export const ROUTINE_SCHEDULE_ROUTE = '/api/automation/schedules';
|
|
13
14
|
export const ROUTINE_SCHEDULE_METHOD = 'schedules.create';
|
|
15
|
+
export const ROUTINE_SCHEDULE_LIST_METHOD = 'schedules.list';
|
|
14
16
|
|
|
15
17
|
type ScheduleCreateInput = OperatorMethodInput<'schedules.create'>;
|
|
16
18
|
type ScheduleCreateOutput = OperatorMethodOutput<'schedules.create'>;
|
|
19
|
+
type ScheduleListOutput = OperatorMethodOutput<'schedules.list'>;
|
|
20
|
+
type ScheduleDeliveryInput = NonNullable<ScheduleCreateInput['delivery']>;
|
|
21
|
+
type ScheduleDeliveryTargetInput = ScheduleDeliveryInput['targets'] extends readonly (infer T)[] ? T : never;
|
|
17
22
|
|
|
18
23
|
export interface AgentDaemonConfigReader {
|
|
19
24
|
get(key: string): unknown;
|
|
@@ -32,9 +37,46 @@ export interface RoutineScheduleSpec {
|
|
|
32
37
|
readonly value: string;
|
|
33
38
|
}
|
|
34
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
|
+
|
|
35
76
|
export interface ParsedRoutineSchedulePromotionArgs {
|
|
36
77
|
readonly routineId: string | null;
|
|
37
78
|
readonly schedule: RoutineScheduleSpec | null;
|
|
79
|
+
readonly deliveryTargets: readonly RoutineScheduleDeliveryTargetSpec[];
|
|
38
80
|
readonly name?: string;
|
|
39
81
|
readonly timezone?: string;
|
|
40
82
|
readonly provider?: string;
|
|
@@ -107,6 +149,7 @@ export interface RoutineScheduleReceipt {
|
|
|
107
149
|
readonly createIfMissing?: boolean;
|
|
108
150
|
};
|
|
109
151
|
readonly deliveryMode?: string;
|
|
152
|
+
readonly deliveryTargets?: readonly RoutineScheduleReceiptDeliveryTarget[];
|
|
110
153
|
readonly failureKind?: RoutineSchedulePromotionFailure['kind'];
|
|
111
154
|
readonly failureError?: string;
|
|
112
155
|
}
|
|
@@ -116,12 +159,81 @@ export interface RoutineScheduleReceiptSnapshot {
|
|
|
116
159
|
readonly receipts: readonly RoutineScheduleReceipt[];
|
|
117
160
|
}
|
|
118
161
|
|
|
162
|
+
export interface RoutineScheduleLiveRecord {
|
|
163
|
+
readonly id: string;
|
|
164
|
+
readonly name: string;
|
|
165
|
+
readonly status?: string;
|
|
166
|
+
readonly enabled?: boolean;
|
|
167
|
+
readonly scheduleKind?: RoutineScheduleKind;
|
|
168
|
+
readonly scheduleValue?: string;
|
|
169
|
+
readonly timezone?: string;
|
|
170
|
+
readonly nextRunAt?: number;
|
|
171
|
+
readonly lastRunAt?: number;
|
|
172
|
+
readonly runCount?: number;
|
|
173
|
+
readonly successCount?: number;
|
|
174
|
+
readonly failureCount?: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface RoutineScheduleCorrelation {
|
|
178
|
+
readonly receipt: RoutineScheduleReceipt;
|
|
179
|
+
readonly liveStatus: 'matched' | 'missing' | 'failed-receipt';
|
|
180
|
+
readonly matchReason: 'schedule-id' | 'name-and-cadence' | 'failed-receipt' | 'not-found';
|
|
181
|
+
readonly schedule?: RoutineScheduleLiveRecord;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface RoutineScheduleCorrelationSuccess {
|
|
185
|
+
readonly ok: true;
|
|
186
|
+
readonly kind: typeof ROUTINE_SCHEDULE_LIST_METHOD;
|
|
187
|
+
readonly route: typeof ROUTINE_SCHEDULE_ROUTE;
|
|
188
|
+
readonly baseUrl: string;
|
|
189
|
+
readonly scheduleCount: number;
|
|
190
|
+
readonly receiptCount: number;
|
|
191
|
+
readonly correlations: readonly RoutineScheduleCorrelation[];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface RoutineScheduleCorrelationFailure {
|
|
195
|
+
readonly ok: false;
|
|
196
|
+
readonly kind:
|
|
197
|
+
| 'auth_required'
|
|
198
|
+
| 'daemon_unavailable'
|
|
199
|
+
| 'version_mismatch'
|
|
200
|
+
| 'daemon_route_unavailable'
|
|
201
|
+
| 'daemon_error';
|
|
202
|
+
readonly error: string;
|
|
203
|
+
readonly route: typeof ROUTINE_SCHEDULE_ROUTE;
|
|
204
|
+
readonly baseUrl?: string;
|
|
205
|
+
readonly daemonVersion?: string;
|
|
206
|
+
readonly expectedSdkVersion?: string;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export type RoutineScheduleCorrelationResult =
|
|
210
|
+
| RoutineScheduleCorrelationSuccess
|
|
211
|
+
| RoutineScheduleCorrelationFailure;
|
|
212
|
+
|
|
119
213
|
interface RoutineScheduleReceiptStoreFile {
|
|
120
214
|
readonly version: 1;
|
|
121
215
|
readonly receipts: readonly RoutineScheduleReceipt[];
|
|
122
216
|
}
|
|
123
217
|
|
|
124
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
|
+
];
|
|
125
237
|
|
|
126
238
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
127
239
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -137,6 +249,11 @@ function readBoolean(record: Record<string, unknown>, key: string): boolean | un
|
|
|
137
249
|
return typeof value === 'boolean' ? value : undefined;
|
|
138
250
|
}
|
|
139
251
|
|
|
252
|
+
function readNumber(record: Record<string, unknown>, key: string): number | undefined {
|
|
253
|
+
const value = record[key];
|
|
254
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
140
257
|
function nowIso(): string {
|
|
141
258
|
return new Date().toISOString();
|
|
142
259
|
}
|
|
@@ -163,6 +280,99 @@ function normalizeProviderModel(provider: string | undefined, model: string | un
|
|
|
163
280
|
};
|
|
164
281
|
}
|
|
165
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
|
+
|
|
166
376
|
function readReceipt(value: unknown): RoutineScheduleReceipt | null {
|
|
167
377
|
if (!isRecord(value)) return null;
|
|
168
378
|
const id = readString(value, 'id')?.trim();
|
|
@@ -175,6 +385,23 @@ function readReceipt(value: unknown): RoutineScheduleReceipt | null {
|
|
|
175
385
|
: null;
|
|
176
386
|
const scheduleValue = readString(value, 'scheduleValue')?.trim();
|
|
177
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;
|
|
178
405
|
if (!id || !createdAt || !routineId || !routineName || !status || !scheduleKind || !scheduleValue) return null;
|
|
179
406
|
return {
|
|
180
407
|
id,
|
|
@@ -201,6 +428,7 @@ function readReceipt(value: unknown): RoutineScheduleReceipt | null {
|
|
|
201
428
|
createIfMissing: readBoolean(target, 'createIfMissing'),
|
|
202
429
|
},
|
|
203
430
|
deliveryMode: readString(value, 'deliveryMode') ?? undefined,
|
|
431
|
+
deliveryTargets,
|
|
204
432
|
failureKind: value.failureKind === 'confirmation_required'
|
|
205
433
|
|| value.failureKind === 'auth_required'
|
|
206
434
|
|| value.failureKind === 'daemon_unavailable'
|
|
@@ -270,6 +498,128 @@ function resultScheduleRecord(result: RoutineSchedulePromotionResult): Record<st
|
|
|
270
498
|
return result.ok && isRecord(result.schedule) ? result.schedule : {};
|
|
271
499
|
}
|
|
272
500
|
|
|
501
|
+
function normalizeForMatch(value: string | undefined): string {
|
|
502
|
+
return (value ?? '').trim().toLowerCase();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function isoTime(value: string): string | null {
|
|
506
|
+
const time = new Date(value).getTime();
|
|
507
|
+
return Number.isFinite(time) ? new Date(time).toISOString() : null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function readLiveScheduleDefinition(value: unknown): {
|
|
511
|
+
readonly kind?: RoutineScheduleKind;
|
|
512
|
+
readonly value?: string;
|
|
513
|
+
readonly timezone?: string;
|
|
514
|
+
} {
|
|
515
|
+
if (!isRecord(value)) return {};
|
|
516
|
+
if (value.kind === 'cron') {
|
|
517
|
+
return {
|
|
518
|
+
kind: 'cron',
|
|
519
|
+
value: readString(value, 'expression') ?? undefined,
|
|
520
|
+
timezone: readString(value, 'timezone') ?? undefined,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
if (value.kind === 'every') {
|
|
524
|
+
const intervalMs = readNumber(value, 'intervalMs');
|
|
525
|
+
return {
|
|
526
|
+
kind: 'every',
|
|
527
|
+
value: intervalMs === undefined ? undefined : formatEveryInterval(intervalMs),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
if (value.kind === 'at') {
|
|
531
|
+
const at = readNumber(value, 'at');
|
|
532
|
+
return {
|
|
533
|
+
kind: 'at',
|
|
534
|
+
value: at === undefined ? undefined : new Date(at).toISOString(),
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
return {};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function readLiveScheduleRecord(value: unknown): RoutineScheduleLiveRecord | null {
|
|
541
|
+
if (!isRecord(value)) return null;
|
|
542
|
+
const id = readString(value, 'id')?.trim();
|
|
543
|
+
const name = readString(value, 'name')?.trim();
|
|
544
|
+
if (!id || !name) return null;
|
|
545
|
+
const schedule = readLiveScheduleDefinition(value.schedule);
|
|
546
|
+
return {
|
|
547
|
+
id,
|
|
548
|
+
name,
|
|
549
|
+
status: readString(value, 'status') ?? undefined,
|
|
550
|
+
enabled: readBoolean(value, 'enabled'),
|
|
551
|
+
scheduleKind: schedule.kind,
|
|
552
|
+
scheduleValue: schedule.value,
|
|
553
|
+
timezone: schedule.timezone,
|
|
554
|
+
nextRunAt: readNumber(value, 'nextRunAt'),
|
|
555
|
+
lastRunAt: readNumber(value, 'lastRunAt'),
|
|
556
|
+
runCount: readNumber(value, 'runCount'),
|
|
557
|
+
successCount: readNumber(value, 'successCount'),
|
|
558
|
+
failureCount: readNumber(value, 'failureCount'),
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function readLiveSchedules(output: ScheduleListOutput): readonly RoutineScheduleLiveRecord[] {
|
|
563
|
+
const record: Record<string, unknown> = isRecord(output) ? output : {};
|
|
564
|
+
const jobs: readonly unknown[] = Array.isArray(record.jobs) ? record.jobs : [];
|
|
565
|
+
return jobs.map(readLiveScheduleRecord).filter((schedule): schedule is RoutineScheduleLiveRecord => schedule !== null);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function cadenceMatches(receipt: RoutineScheduleReceipt, schedule: RoutineScheduleLiveRecord): boolean {
|
|
569
|
+
if (receipt.scheduleKind !== schedule.scheduleKind) return false;
|
|
570
|
+
if (receipt.scheduleKind === 'at') {
|
|
571
|
+
const left = isoTime(receipt.scheduleValue);
|
|
572
|
+
const right = schedule.scheduleValue ? isoTime(schedule.scheduleValue) : null;
|
|
573
|
+
return Boolean(left && right && left === right);
|
|
574
|
+
}
|
|
575
|
+
return normalizeForMatch(receipt.scheduleValue) === normalizeForMatch(schedule.scheduleValue);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function findScheduleForReceipt(
|
|
579
|
+
receipt: RoutineScheduleReceipt,
|
|
580
|
+
schedules: readonly RoutineScheduleLiveRecord[],
|
|
581
|
+
): { readonly reason: RoutineScheduleCorrelation['matchReason']; readonly schedule?: RoutineScheduleLiveRecord } {
|
|
582
|
+
if (receipt.scheduleId) {
|
|
583
|
+
const byId = schedules.find((schedule) => schedule.id === receipt.scheduleId);
|
|
584
|
+
if (byId) return { reason: 'schedule-id', schedule: byId };
|
|
585
|
+
}
|
|
586
|
+
const byNameAndCadence = schedules.find((schedule) => (
|
|
587
|
+
normalizeForMatch(schedule.name) === normalizeForMatch(receipt.scheduleName)
|
|
588
|
+
&& cadenceMatches(receipt, schedule)
|
|
589
|
+
));
|
|
590
|
+
return byNameAndCadence
|
|
591
|
+
? { reason: 'name-and-cadence', schedule: byNameAndCadence }
|
|
592
|
+
: { reason: 'not-found' };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function correlateReceipts(
|
|
596
|
+
receipts: readonly RoutineScheduleReceipt[],
|
|
597
|
+
schedules: readonly RoutineScheduleLiveRecord[],
|
|
598
|
+
): readonly RoutineScheduleCorrelation[] {
|
|
599
|
+
return receipts.map((receipt) => {
|
|
600
|
+
if (receipt.status === 'failed') {
|
|
601
|
+
return {
|
|
602
|
+
receipt,
|
|
603
|
+
liveStatus: 'failed-receipt',
|
|
604
|
+
matchReason: 'failed-receipt',
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
const match = findScheduleForReceipt(receipt, schedules);
|
|
608
|
+
return match.schedule
|
|
609
|
+
? {
|
|
610
|
+
receipt,
|
|
611
|
+
liveStatus: 'matched',
|
|
612
|
+
matchReason: match.reason,
|
|
613
|
+
schedule: match.schedule,
|
|
614
|
+
}
|
|
615
|
+
: {
|
|
616
|
+
receipt,
|
|
617
|
+
liveStatus: 'missing',
|
|
618
|
+
matchReason: 'not-found',
|
|
619
|
+
};
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
273
623
|
function buildReceipt(
|
|
274
624
|
existing: readonly RoutineScheduleReceipt[],
|
|
275
625
|
connection: AgentDaemonConnection,
|
|
@@ -301,6 +651,7 @@ function buildReceipt(
|
|
|
301
651
|
enabled: preview.payload.enabled !== false,
|
|
302
652
|
target: targetSummary(preview.payload),
|
|
303
653
|
deliveryMode: deliveryMode(preview.payload),
|
|
654
|
+
deliveryTargets: redactedDeliveryTargets(preview.payload.delivery),
|
|
304
655
|
failureKind: result.ok ? undefined : result.kind,
|
|
305
656
|
failureError: result.ok ? undefined : result.error,
|
|
306
657
|
};
|
|
@@ -358,6 +709,7 @@ export class RoutineScheduleReceiptStore {
|
|
|
358
709
|
export function parseRoutineSchedulePromotionArgs(args: readonly string[]): ParsedRoutineSchedulePromotionArgs {
|
|
359
710
|
let routineId: string | null = null;
|
|
360
711
|
let schedule: RoutineScheduleSpec | null = null;
|
|
712
|
+
const deliveryTargets: RoutineScheduleDeliveryTargetSpec[] = [];
|
|
361
713
|
let name: string | undefined;
|
|
362
714
|
let timezone: string | undefined;
|
|
363
715
|
let provider: string | undefined;
|
|
@@ -412,6 +764,58 @@ export function parseRoutineSchedulePromotionArgs(args: readonly string[]): Pars
|
|
|
412
764
|
};
|
|
413
765
|
continue;
|
|
414
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
|
+
}
|
|
415
819
|
if (raw.startsWith('--')) {
|
|
416
820
|
errors.push(`Unknown option: ${raw}`);
|
|
417
821
|
continue;
|
|
@@ -425,7 +829,9 @@ export function parseRoutineSchedulePromotionArgs(args: readonly string[]): Pars
|
|
|
425
829
|
|
|
426
830
|
if (!routineId) errors.push('Routine id or name is required.');
|
|
427
831
|
if (!schedule) errors.push('Schedule is required: use --cron <expr>, --every <interval>, or --at <iso-time>.');
|
|
428
|
-
|
|
832
|
+
const deliveryError = validateDeliveryTargets(deliveryTargets);
|
|
833
|
+
if (deliveryError) errors.push(deliveryError);
|
|
834
|
+
return { routineId, schedule, deliveryTargets, name, timezone, provider, model, enabled, yes, errors };
|
|
429
835
|
}
|
|
430
836
|
|
|
431
837
|
export function resolveAgentDaemonConnection(
|
|
@@ -488,8 +894,8 @@ export function buildRoutineSchedulePayload(
|
|
|
488
894
|
createIfMissing: true,
|
|
489
895
|
},
|
|
490
896
|
delivery: {
|
|
491
|
-
mode:
|
|
492
|
-
targets:
|
|
897
|
+
mode: deliveryModeFromTargets(parsed.deliveryTargets),
|
|
898
|
+
targets: parsed.deliveryTargets.map(toDeliveryTargetInput),
|
|
493
899
|
fallbackTargets: [],
|
|
494
900
|
includeSummary: true,
|
|
495
901
|
includeTranscript: false,
|
|
@@ -593,6 +999,38 @@ async function classifyScheduleError(
|
|
|
593
999
|
return { ok: false, kind: 'daemon_error', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
|
|
594
1000
|
}
|
|
595
1001
|
|
|
1002
|
+
async function classifyScheduleListError(
|
|
1003
|
+
error: unknown,
|
|
1004
|
+
connection: AgentDaemonConnection,
|
|
1005
|
+
): Promise<RoutineScheduleCorrelationFailure> {
|
|
1006
|
+
const message = summarizeError(error);
|
|
1007
|
+
const lower = message.toLowerCase();
|
|
1008
|
+
if (lower.includes('401') || lower.includes('unauthorized') || lower.includes('auth')) {
|
|
1009
|
+
return { ok: false, kind: 'auth_required', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
|
|
1010
|
+
}
|
|
1011
|
+
if (lower.includes('404') || lower.includes('not found')) {
|
|
1012
|
+
const daemon = await fetchDaemonStatus(connection);
|
|
1013
|
+
const record = isRecord(daemon.body) ? daemon.body : {};
|
|
1014
|
+
const daemonVersion = readString(record, 'version') ?? 'unknown';
|
|
1015
|
+
if (daemon.ok && daemonVersion !== SDK_VERSION) {
|
|
1016
|
+
return {
|
|
1017
|
+
ok: false,
|
|
1018
|
+
kind: 'version_mismatch',
|
|
1019
|
+
error: `External daemon SDK version ${daemonVersion} does not match Agent SDK pin ${SDK_VERSION}; schedules.list is unavailable.`,
|
|
1020
|
+
route: ROUTINE_SCHEDULE_ROUTE,
|
|
1021
|
+
baseUrl: connection.baseUrl,
|
|
1022
|
+
daemonVersion,
|
|
1023
|
+
expectedSdkVersion: SDK_VERSION,
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
return { ok: false, kind: 'daemon_route_unavailable', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
|
|
1027
|
+
}
|
|
1028
|
+
if (lower.includes('fetch') || lower.includes('connect') || lower.includes('econnrefused')) {
|
|
1029
|
+
return { ok: false, kind: 'daemon_unavailable', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
|
|
1030
|
+
}
|
|
1031
|
+
return { ok: false, kind: 'daemon_error', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
|
|
1032
|
+
}
|
|
1033
|
+
|
|
596
1034
|
export async function promoteRoutineToDaemonSchedule(
|
|
597
1035
|
connection: AgentDaemonConnection,
|
|
598
1036
|
preview: RoutineSchedulePromotionPreview,
|
|
@@ -623,12 +1061,45 @@ export async function promoteRoutineToDaemonSchedule(
|
|
|
623
1061
|
}
|
|
624
1062
|
}
|
|
625
1063
|
|
|
1064
|
+
export async function reconcileRoutineScheduleReceipts(
|
|
1065
|
+
connection: AgentDaemonConnection,
|
|
1066
|
+
snapshot: RoutineScheduleReceiptSnapshot,
|
|
1067
|
+
): Promise<RoutineScheduleCorrelationResult> {
|
|
1068
|
+
if (!connection.token) {
|
|
1069
|
+
return {
|
|
1070
|
+
ok: false,
|
|
1071
|
+
kind: 'auth_required',
|
|
1072
|
+
error: `No daemon operator token found at ${connection.tokenPath}`,
|
|
1073
|
+
route: ROUTINE_SCHEDULE_ROUTE,
|
|
1074
|
+
baseUrl: connection.baseUrl,
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
try {
|
|
1078
|
+
const sdk = createBrowserGoodVibesSdk({ baseUrl: connection.baseUrl, authToken: connection.token });
|
|
1079
|
+
const output = await sdk.operator.invoke(ROUTINE_SCHEDULE_LIST_METHOD, {});
|
|
1080
|
+
const schedules = readLiveSchedules(output);
|
|
1081
|
+
return {
|
|
1082
|
+
ok: true,
|
|
1083
|
+
kind: ROUTINE_SCHEDULE_LIST_METHOD,
|
|
1084
|
+
route: ROUTINE_SCHEDULE_ROUTE,
|
|
1085
|
+
baseUrl: connection.baseUrl,
|
|
1086
|
+
scheduleCount: schedules.length,
|
|
1087
|
+
receiptCount: snapshot.receipts.length,
|
|
1088
|
+
correlations: correlateReceipts(snapshot.receipts, schedules),
|
|
1089
|
+
};
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
return classifyScheduleListError(error, connection);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
626
1095
|
export function formatRoutineSchedulePreview(preview: RoutineSchedulePromotionPreview): string {
|
|
627
1096
|
const schedule = preview.payload.kind === 'cron'
|
|
628
1097
|
? `${preview.payload.cron}${preview.payload.timezone ? ` [${preview.payload.timezone}]` : ''}`
|
|
629
1098
|
: preview.payload.kind === 'every'
|
|
630
1099
|
? String(preview.payload.every)
|
|
631
1100
|
: String(preview.payload.at);
|
|
1101
|
+
const delivery = preview.payload.delivery;
|
|
1102
|
+
const deliveryTargetCount = delivery?.targets.length ?? 0;
|
|
632
1103
|
return [
|
|
633
1104
|
'Daemon schedule preview for Agent routine',
|
|
634
1105
|
` routine: ${preview.routineName} (${preview.routineId})`,
|
|
@@ -636,6 +1107,7 @@ export function formatRoutineSchedulePreview(preview: RoutineSchedulePromotionPr
|
|
|
636
1107
|
` name: ${String(preview.payload.name ?? '(daemon default)')}`,
|
|
637
1108
|
` schedule: ${preview.payload.kind} ${schedule}`,
|
|
638
1109
|
` enabled: ${preview.payload.enabled === false ? 'no' : 'yes'}`,
|
|
1110
|
+
` delivery: ${delivery?.mode ?? 'none'}${deliveryTargetCount > 0 ? ` (${deliveryTargetCount} target${deliveryTargetCount === 1 ? '' : 's'})` : ''}`,
|
|
639
1111
|
' target: external daemon service/main conversation route',
|
|
640
1112
|
' policy: isolated Agent Knowledge only; no default wiki/HomeGraph fallback; no WRFC unless explicitly delegated',
|
|
641
1113
|
' next: rerun with --yes to create this daemon schedule',
|
|
@@ -694,11 +1166,65 @@ export function formatRoutineScheduleReceipt(receipt: RoutineScheduleReceipt): s
|
|
|
694
1166
|
receipt.model ? ` model: ${receipt.model}` : '',
|
|
695
1167
|
` target: ${receipt.target.kind ?? 'unknown'}${receipt.target.surfaceKind ? `/${receipt.target.surfaceKind}` : ''}`,
|
|
696
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}` : ''}`),
|
|
697
1170
|
receipt.failureKind ? ` failure: ${receipt.failureKind}` : '',
|
|
698
1171
|
receipt.failureError ? ` error: ${receipt.failureError}` : '',
|
|
699
1172
|
].filter((line): line is string => Boolean(line)).join('\n');
|
|
700
1173
|
}
|
|
701
1174
|
|
|
1175
|
+
export function formatRoutineScheduleCorrelation(result: RoutineScheduleCorrelationResult, limit = 10): string {
|
|
1176
|
+
if (!result.ok) {
|
|
1177
|
+
return [
|
|
1178
|
+
`Daemon schedule reconciliation error: ${result.kind}`,
|
|
1179
|
+
` ${result.error}`,
|
|
1180
|
+
result.baseUrl ? ` daemon: ${result.baseUrl}` : null,
|
|
1181
|
+
` route: ${ROUTINE_SCHEDULE_LIST_METHOD} ${result.route}`,
|
|
1182
|
+
result.kind === 'auth_required'
|
|
1183
|
+
? ' next: pair/authenticate with the externally managed GoodVibes daemon, then retry.'
|
|
1184
|
+
: null,
|
|
1185
|
+
result.kind === 'daemon_unavailable'
|
|
1186
|
+
? ' next: start/restart the external GoodVibes daemon from TUI or daemon host tooling; Agent does not own daemon lifecycle.'
|
|
1187
|
+
: null,
|
|
1188
|
+
result.kind === 'version_mismatch' || result.kind === 'daemon_route_unavailable'
|
|
1189
|
+
? ' next: update/restart the external GoodVibes daemon so public schedules.list is available.'
|
|
1190
|
+
: null,
|
|
1191
|
+
].filter((line): line is string => Boolean(line)).join('\n');
|
|
1192
|
+
}
|
|
1193
|
+
const correlations = result.correlations.slice(0, Math.max(1, limit));
|
|
1194
|
+
if (result.receiptCount === 0) {
|
|
1195
|
+
return [
|
|
1196
|
+
'Agent routine schedule reconciliation',
|
|
1197
|
+
` daemon: ${result.baseUrl}`,
|
|
1198
|
+
` route: ${result.kind} ${result.route}`,
|
|
1199
|
+
` live schedules: ${result.scheduleCount}`,
|
|
1200
|
+
' No local routine promotion receipts exist yet.',
|
|
1201
|
+
' Create one with /schedule promote-routine <routine-id> --cron <expr> --yes.',
|
|
1202
|
+
].join('\n');
|
|
1203
|
+
}
|
|
1204
|
+
const matched = result.correlations.filter((entry) => entry.liveStatus === 'matched').length;
|
|
1205
|
+
const missing = result.correlations.filter((entry) => entry.liveStatus === 'missing').length;
|
|
1206
|
+
const failed = result.correlations.filter((entry) => entry.liveStatus === 'failed-receipt').length;
|
|
1207
|
+
return [
|
|
1208
|
+
'Agent routine schedule reconciliation',
|
|
1209
|
+
` daemon: ${result.baseUrl}`,
|
|
1210
|
+
` route: ${result.kind} ${result.route}`,
|
|
1211
|
+
` receipts: ${result.receiptCount}; live schedules: ${result.scheduleCount}; matched: ${matched}; missing: ${missing}; failed receipts: ${failed}`,
|
|
1212
|
+
...correlations.map((entry) => {
|
|
1213
|
+
const receipt = entry.receipt;
|
|
1214
|
+
const schedule = entry.schedule;
|
|
1215
|
+
const live = schedule
|
|
1216
|
+
? ` live=${schedule.id} status=${schedule.status ?? (schedule.enabled === false ? 'paused' : 'enabled')}`
|
|
1217
|
+
: '';
|
|
1218
|
+
const runs = schedule && schedule.runCount !== undefined
|
|
1219
|
+
? ` runs=${schedule.runCount}/${schedule.successCount ?? 0}/${schedule.failureCount ?? 0}`
|
|
1220
|
+
: '';
|
|
1221
|
+
const next = schedule?.nextRunAt ? ` next=${new Date(schedule.nextRunAt).toISOString()}` : '';
|
|
1222
|
+
return ` ${receipt.id} ${entry.liveStatus} reason=${entry.matchReason} routine=${receipt.routineId} receiptSchedule=${receipt.scheduleId ?? '(none)'}${live}${runs}${next}`;
|
|
1223
|
+
}),
|
|
1224
|
+
result.correlations.length > correlations.length ? ` ...${result.correlations.length - correlations.length} more` : '',
|
|
1225
|
+
].filter((line): line is string => Boolean(line)).join('\n');
|
|
1226
|
+
}
|
|
1227
|
+
|
|
702
1228
|
export function formatRoutineScheduleFailure(failure: RoutineSchedulePromotionFailure): string {
|
|
703
1229
|
return [
|
|
704
1230
|
`Daemon schedule error: ${failure.kind}`,
|
package/src/cli/help.ts
CHANGED
|
@@ -166,15 +166,17 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
166
166
|
'routines enabled',
|
|
167
167
|
'routines show <id>',
|
|
168
168
|
'routines receipts',
|
|
169
|
+
'routines reconcile',
|
|
169
170
|
'routines receipt <receipt-id>',
|
|
170
|
-
'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',
|
|
171
172
|
],
|
|
172
|
-
summary: 'Inspect Agent-local routines, review local promotion receipts, and explicitly promote a reviewed routine into an external daemon schedule. Without --yes, promote only prints the schedules.create preview.',
|
|
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.',
|
|
173
174
|
examples: [
|
|
174
175
|
'routines list',
|
|
175
176
|
'routines show daily-operations-sweep',
|
|
176
177
|
'routines receipts',
|
|
177
|
-
'routines
|
|
178
|
+
'routines reconcile',
|
|
179
|
+
'routines promote daily-operations-sweep --cron "0 9 * * *" --timezone America/Chicago --delivery-surface slack --yes',
|
|
178
180
|
'routines promote weekly-review --every 7d --disabled',
|
|
179
181
|
],
|
|
180
182
|
},
|
|
@@ -2,6 +2,7 @@ import { createShellPathService } from '@/runtime/index.ts';
|
|
|
2
2
|
import { AgentRoutineRegistry, type AgentRoutineRecord } from '../agent/routine-registry.ts';
|
|
3
3
|
import {
|
|
4
4
|
buildRoutineSchedulePreview,
|
|
5
|
+
formatRoutineScheduleCorrelation,
|
|
5
6
|
formatRoutineScheduleReceipt,
|
|
6
7
|
formatRoutineScheduleReceipts,
|
|
7
8
|
formatRoutineScheduleFailure,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
formatRoutineScheduleSuccess,
|
|
10
11
|
parseRoutineSchedulePromotionArgs,
|
|
11
12
|
promoteRoutineToDaemonSchedule,
|
|
13
|
+
reconcileRoutineScheduleReceipts,
|
|
12
14
|
resolveAgentDaemonConnection,
|
|
13
15
|
RoutineScheduleReceiptStore,
|
|
14
16
|
} from '../agent/routine-schedule-promotion.ts';
|
|
@@ -98,7 +100,7 @@ async function handleRoutinePromotion(runtime: CliCommandRuntime, args: readonly
|
|
|
98
100
|
};
|
|
99
101
|
return {
|
|
100
102
|
output: json ? JSON.stringify(failure, null, 2) : [
|
|
101
|
-
'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',
|
|
102
104
|
...parsed.errors.map((error) => ` ${error}`),
|
|
103
105
|
].join('\n'),
|
|
104
106
|
exitCode: 2,
|
|
@@ -199,6 +201,17 @@ export async function handleRoutinesCommand(runtime: CliCommandRuntime): Promise
|
|
|
199
201
|
exitCode: 0,
|
|
200
202
|
};
|
|
201
203
|
}
|
|
204
|
+
if (normalized === 'reconcile' || normalized === 'sync' || normalized === 'status') {
|
|
205
|
+
const store = routineReceiptStore(runtime);
|
|
206
|
+
const result = await reconcileRoutineScheduleReceipts(
|
|
207
|
+
resolveAgentDaemonConnection(runtime.configManager, runtime.homeDirectory),
|
|
208
|
+
store.snapshot(),
|
|
209
|
+
);
|
|
210
|
+
return {
|
|
211
|
+
output: jsonOrText(runtime, result, formatRoutineScheduleCorrelation(result)),
|
|
212
|
+
exitCode: result.ok ? 0 : 1,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
202
215
|
if (normalized === 'receipt') {
|
|
203
216
|
const id = rest[0];
|
|
204
217
|
if (!id) return { output: 'Usage: goodvibes-agent routines receipt <receipt-id>', exitCode: 2 };
|
|
@@ -224,7 +237,7 @@ export async function handleRoutinesCommand(runtime: CliCommandRuntime): Promise
|
|
|
224
237
|
return handleRoutinePromotion(runtime, rest);
|
|
225
238
|
}
|
|
226
239
|
return {
|
|
227
|
-
output: 'Usage: goodvibes-agent routines [list|enabled|show <id>|receipts|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]',
|
|
228
241
|
exitCode: 2,
|
|
229
242
|
};
|
|
230
243
|
}
|
|
@@ -573,11 +573,12 @@ 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 and 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
|
+
{ 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' },
|
|
581
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' },
|
|
582
583
|
{ id: 'health-services', label: 'Service health', detail: 'Inspect service readiness without starting, stopping, or restarting daemon services.', command: '/health services', kind: 'command', safety: 'read-only' },
|
|
583
584
|
],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AgentRoutineRegistry, type AgentRoutineRecord } from '../../agent/routine-registry.ts';
|
|
2
2
|
import {
|
|
3
3
|
buildRoutineSchedulePreview,
|
|
4
|
+
formatRoutineScheduleCorrelation,
|
|
4
5
|
formatRoutineScheduleReceipt,
|
|
5
6
|
formatRoutineScheduleReceipts,
|
|
6
7
|
formatRoutineScheduleFailure,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
formatRoutineScheduleSuccess,
|
|
9
10
|
parseRoutineSchedulePromotionArgs,
|
|
10
11
|
promoteRoutineToDaemonSchedule,
|
|
12
|
+
reconcileRoutineScheduleReceipts,
|
|
11
13
|
resolveAgentDaemonConnection,
|
|
12
14
|
RoutineScheduleReceiptStore,
|
|
13
15
|
} from '../../agent/routine-schedule-promotion.ts';
|
|
@@ -167,6 +169,13 @@ export async function runRoutinesRuntimeCommand(args: readonly string[], ctx: Co
|
|
|
167
169
|
ctx.print(formatRoutineScheduleReceipts(receiptStoreFromContext(ctx).snapshot()));
|
|
168
170
|
return;
|
|
169
171
|
}
|
|
172
|
+
if (sub === 'reconcile' || sub === 'sync' || sub === 'status') {
|
|
173
|
+
const shellPaths = requireShellPaths(ctx);
|
|
174
|
+
const connection = resolveAgentDaemonConnection(ctx.platform.configManager, shellPaths.homeDirectory);
|
|
175
|
+
const result = await reconcileRoutineScheduleReceipts(connection, receiptStoreFromContext(ctx).snapshot());
|
|
176
|
+
ctx.print(formatRoutineScheduleCorrelation(result));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
170
179
|
if (sub === 'receipt') {
|
|
171
180
|
const id = args[1];
|
|
172
181
|
if (!id) {
|
|
@@ -275,7 +284,7 @@ export async function runRoutinesRuntimeCommand(args: readonly string[], ctx: Co
|
|
|
275
284
|
ctx.print(`Deleted Agent routine ${removed.id}: ${removed.name}`);
|
|
276
285
|
return;
|
|
277
286
|
}
|
|
278
|
-
ctx.print('Usage: /routines [list|enabled|search|show|receipts|receipt|create|update|enable|disable|start|review|stale|promote|delete]');
|
|
287
|
+
ctx.print('Usage: /routines [list|enabled|search|show|receipts|reconcile|receipt|create|update|enable|disable|start|review|stale|promote|delete]');
|
|
279
288
|
} catch (error) {
|
|
280
289
|
printError(ctx, error);
|
|
281
290
|
}
|
|
@@ -286,7 +295,7 @@ export function registerRoutinesRuntimeCommands(registry: CommandRegistry): void
|
|
|
286
295
|
name: 'routines',
|
|
287
296
|
aliases: ['routine'],
|
|
288
297
|
description: 'Manage local GoodVibes Agent routines',
|
|
289
|
-
usage: '[list|enabled|search <query>|show <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]',
|
|
290
299
|
handler: runRoutinesRuntimeCommand,
|
|
291
300
|
});
|
|
292
301
|
}
|
|
@@ -7,6 +7,7 @@ import type { AutomationScheduleDefinition } from '@pellux/goodvibes-sdk/platfor
|
|
|
7
7
|
import { AgentRoutineRegistry } from '../../agent/routine-registry.ts';
|
|
8
8
|
import {
|
|
9
9
|
buildRoutineSchedulePreview,
|
|
10
|
+
formatRoutineScheduleCorrelation,
|
|
10
11
|
formatRoutineScheduleReceipt,
|
|
11
12
|
formatRoutineScheduleReceipts,
|
|
12
13
|
formatRoutineScheduleFailure,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
formatRoutineScheduleSuccess,
|
|
15
16
|
parseRoutineSchedulePromotionArgs,
|
|
16
17
|
promoteRoutineToDaemonSchedule,
|
|
18
|
+
reconcileRoutineScheduleReceipts,
|
|
17
19
|
resolveAgentDaemonConnection,
|
|
18
20
|
RoutineScheduleReceiptStore,
|
|
19
21
|
} from '../../agent/routine-schedule-promotion.ts';
|
|
@@ -58,7 +60,7 @@ async function promoteRoutineSchedule(args: readonly string[], ctx: CommandConte
|
|
|
58
60
|
const parsed = parseRoutineSchedulePromotionArgs(args);
|
|
59
61
|
if (parsed.errors.length > 0) {
|
|
60
62
|
ctx.print([
|
|
61
|
-
'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',
|
|
62
64
|
...parsed.errors.map((error) => ` ${error}`),
|
|
63
65
|
].join('\n'));
|
|
64
66
|
return;
|
|
@@ -85,8 +87,8 @@ export function registerScheduleRuntimeCommands(registry: CommandRegistry): void
|
|
|
85
87
|
name: 'schedule',
|
|
86
88
|
aliases: ['sched'],
|
|
87
89
|
description: 'Inspect schedules and explicitly promote local Agent routines to daemon schedules',
|
|
88
|
-
usage: 'list | receipts | receipt <id> | promote-routine <routine-id> --cron <expr> --yes',
|
|
89
|
-
argsHint: 'list | receipts | 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',
|
|
90
92
|
async handler(args, ctx) {
|
|
91
93
|
const sub = args[0];
|
|
92
94
|
|
|
@@ -100,6 +102,14 @@ export function registerScheduleRuntimeCommands(registry: CommandRegistry): void
|
|
|
100
102
|
return;
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
if (sub === 'reconcile' || sub === 'sync' || sub === 'status') {
|
|
106
|
+
const shellPaths = requireShellPaths(ctx);
|
|
107
|
+
const connection = resolveAgentDaemonConnection(ctx.platform.configManager, shellPaths.homeDirectory);
|
|
108
|
+
const result = await reconcileRoutineScheduleReceipts(connection, RoutineScheduleReceiptStore.fromShellPaths(shellPaths).snapshot());
|
|
109
|
+
ctx.print(formatRoutineScheduleCorrelation(result));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
103
113
|
if (sub === 'receipt') {
|
|
104
114
|
const id = args[1];
|
|
105
115
|
if (!id) {
|
|
@@ -148,8 +158,9 @@ export function registerScheduleRuntimeCommands(registry: CommandRegistry): void
|
|
|
148
158
|
'Usage:\n'
|
|
149
159
|
+ ' /schedule list\n'
|
|
150
160
|
+ ' /schedule receipts\n'
|
|
161
|
+
+ ' /schedule reconcile\n'
|
|
151
162
|
+ ' /schedule receipt <receipt-id>\n'
|
|
152
|
-
+ ' /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'
|
|
153
164
|
+ ' Local schedule mutations and runs remain blocked.'
|
|
154
165
|
);
|
|
155
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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.
|
|
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 {
|