@pellux/goodvibes-agent 0.1.35 → 0.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to GoodVibes Agent will be recorded here.
4
4
 
5
+ ## 0.1.37 - 2026-05-31
6
+
7
+ - 656b6f4 Add Agent routine daemon schedule promotion
8
+
9
+ ## 0.1.36 - 2026-05-31
10
+
11
+ - 9de2f5e Add guided Agent starter authoring
12
+
5
13
  ## 0.1.35 - 2026-05-31
6
14
 
7
15
  - 2c25d5e Add local starter profile import export
package/README.md CHANGED
@@ -46,6 +46,8 @@ Inside the Agent TUI, use `/agent`, `/home`, or `/operator` to open the operator
46
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
48
 
49
+ 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
+
49
51
  Use isolated Agent runtime profiles when one machine needs separate operator identities or local state:
50
52
 
51
53
  ```sh
@@ -66,10 +68,13 @@ Local Agent behavior is editable from the TUI:
66
68
  /personas use research
67
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
68
70
  /routines start evening-review
71
+ /schedule promote-routine evening-review --cron "0 17 * * 1-5" --timezone America/Chicago --yes
69
72
  /agent-skills create --name "Morning Brief" --description "Daily briefing flow" --procedure "Check tasks, approvals, calendar, and unread state before summarizing." --enabled true
70
73
  /skills local list
71
74
  ```
72
75
 
76
+ 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`, and the generated scheduled prompt keeps Agent Knowledge isolated from default Knowledge/Wiki and HomeGraph.
77
+
73
78
  ## Daemon Prerequisite
74
79
 
75
80
  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`.
package/docs/README.md CHANGED
@@ -21,7 +21,7 @@ Important baseline constraints:
21
21
  - Agent Knowledge/Wiki uses only `/api/goodvibes-agent/knowledge/*`; there is no default Knowledge/Wiki, HomeGraph, or Home Assistant fallback.
22
22
  - Agent exposes `goodvibes-agent capabilities` and `/capabilities` to compare OpenClaw/Hermes capability targets against current Agent readiness and configuration paths.
23
23
  - Agent supports isolated runtime homes with `GOODVIBES_AGENT_HOME=<path>` and named profile homes with `goodvibes-agent profiles create <name> --template <starter> --yes` plus `--agent-profile <name>`.
24
- - Agent ships starter profile templates for household, research, travel, operations, and personal productivity local state; `profiles templates export/import` supports local custom starters.
24
+ - Agent ships starter profile templates for household, research, travel, operations, and personal productivity local state; `profiles templates export/import` and `/agent-profile guide` support local custom starters.
25
25
  - Local personas, routines, and Agent skills are stored under the Agent surface root and are injected only into the serial Agent conversation.
26
26
  - Normal assistant chat is not coding-session delegation.
27
27
  - Build/fix/review delegation to GoodVibes TUI must be explicit; WRFC is not the default Agent behavior.
@@ -37,6 +37,8 @@ bun run dev
37
37
 
38
38
  Once the TUI opens, run `/agent`, `/home`, or `/operator` to open the Agent operator workspace. That fullscreen workspace is the current front door for setup/config, knowledge status, local memory and skills, read-only work/approval/automation views, and explicit GoodVibes TUI build delegation.
39
39
 
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
+
40
42
  ## Isolated Agent Profiles
41
43
 
42
44
  Use a separate Agent home when you want isolated local state:
@@ -68,12 +70,13 @@ Personas, routines, and reusable Agent skills are local to GoodVibes Agent. They
68
70
  /personas use research
69
71
  /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
72
  /routines start evening-review
73
+ /schedule promote-routine evening-review --cron "0 17 * * 1-5" --timezone America/Chicago --yes
71
74
  /agent-skills create --name "Morning Brief" --description "Daily briefing flow" --procedure "Check tasks, approvals, calendar, and unread state before summarizing." --enabled true
72
75
  /agent-skills enabled
73
76
  /skills local list
74
77
  ```
75
78
 
76
- The active persona plus enabled Agent routines and skills are injected into the main serial assistant conversation. Starting a routine records local usage and prints its steps; it does not spawn background agents or daemon automation jobs.
79
+ The active persona plus enabled Agent routines and skills are injected into the main serial assistant conversation. Starting a routine records local usage and prints its steps; it does not spawn background agents or daemon automation jobs. Promoting a routine to a schedule is an explicit `schedules.create` call to the external daemon, requires `--yes`, and preserves the rule that Agent Knowledge never falls back to default Knowledge/Wiki or HomeGraph.
77
80
 
78
81
  ## External Daemon
79
82
 
@@ -53,11 +53,11 @@ Primary sources used for the benchmark:
53
53
  | 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
54
  | Knowledge/memory | Durable memory, semantic search, wiki/claim layers | Isolated Agent Knowledge routes with workspace ask/search/ingest/review flows plus local memory/skills/personas/routines |
55
55
  | 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 | Guarded automation/schedule routes plus local routines; hidden model scheduling blocked |
56
+ | Scheduling | Natural-language cron, run/pause/resume/edit/remove, delivery | Local routines can be explicitly promoted to external daemon `schedules.create` with `--yes`; hidden model scheduling and local scheduler spawns are blocked |
57
57
  | Tools/MCP | Broad toolsets, MCP, browser, media, terminal, files | GoodVibes SDK tools with Agent policy guards and MCP/provider integrations |
58
58
  | Voice/media/canvas/nodes | Voice, TTS, mobile nodes, live canvas, browser automation | GoodVibes media/voice/browser/node primitives with an Agent workspace for setup, image input, browser posture, MCP, and remote/node inspection |
59
59
  | Build/code work | Direct terminal/file/code tools and subagents | Explicit delegation to GoodVibes TUI; local WRFC/spawn fanout blocked |
60
- | 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; the Agent workspace exposes profile and portability flows; daemon remains external |
60
+ | 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
61
  | Security | DM pairing, approvals, sandboxing, allowlists | Daemon approvals, auth diagnostics, secret refs, confirmation gates, model-tool policy |
62
62
 
63
63
  ## Exceed Targets
@@ -66,6 +66,7 @@ GoodVibes Agent should exceed OpenClaw/Hermes by making these properties true fr
66
66
 
67
67
  - Capability surfaces are discoverable through `goodvibes-agent capabilities`, `/capabilities`, onboarding, and the operator workspace.
68
68
  - Agent Knowledge isolation is a release gate, not a convention.
69
+ - Routine-to-schedule promotion preserves Agent Knowledge isolation and uses only public external daemon schedule routes.
69
70
  - Model-visible tools are policy-gated for serial, non-secret, non-destructive use.
70
71
  - Personal assistant state is Agent-local unless an explicit Agent Knowledge ingest route is used.
71
72
  - Build work is delegated to the product that owns coding execution instead of turning the personal operator into a second coding TUI.
@@ -75,7 +76,7 @@ GoodVibes Agent should exceed OpenClaw/Hermes by making these properties true fr
75
76
 
76
77
  - Live daemon account health and last delivery errors in the Channels workspace once a stable read-only route is available.
77
78
  - Artifact and multimodal Agent Knowledge ingest affordances once Agent-specific routes are stable.
78
- - Guided starter profile authoring inside the fullscreen Agent workspace.
79
+ - Visual starter-template editing inside the fullscreen Agent workspace after the command-guided authoring path.
79
80
  - Artifact and multimodal Agent Knowledge ingestion when the isolated Agent route accepts artifact-backed media.
80
81
  - Delegation receipts and artifact review inside the operator workspace.
81
82
  - Approval center with route risk labels and saved policy presets.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-agent",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
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",
@@ -0,0 +1,434 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { createBrowserGoodVibesSdk } from '@pellux/goodvibes-sdk/browser';
4
+ import type { OperatorMethodInput, OperatorMethodOutput } from '@pellux/goodvibes-sdk/contracts';
5
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
6
+ import { getModelIdFromProviderModel, getProviderIdFromModel } from '../config/provider-model.ts';
7
+ import { SDK_VERSION } from '../version.ts';
8
+ import type { AgentRoutineRecord } from './routine-registry.ts';
9
+
10
+ export const ROUTINE_SCHEDULE_ROUTE = '/api/automation/schedules';
11
+ export const ROUTINE_SCHEDULE_METHOD = 'schedules.create';
12
+
13
+ type ScheduleCreateInput = OperatorMethodInput<'schedules.create'>;
14
+ type ScheduleCreateOutput = OperatorMethodOutput<'schedules.create'>;
15
+
16
+ export interface AgentDaemonConfigReader {
17
+ get(key: string): unknown;
18
+ }
19
+
20
+ export interface AgentDaemonConnection {
21
+ readonly baseUrl: string;
22
+ readonly token: string | null;
23
+ readonly tokenPath: string;
24
+ }
25
+
26
+ export type RoutineScheduleKind = 'cron' | 'every' | 'at';
27
+
28
+ export interface RoutineScheduleSpec {
29
+ readonly kind: RoutineScheduleKind;
30
+ readonly value: string;
31
+ }
32
+
33
+ export interface ParsedRoutineSchedulePromotionArgs {
34
+ readonly routineId: string | null;
35
+ readonly schedule: RoutineScheduleSpec | null;
36
+ readonly name?: string;
37
+ readonly timezone?: string;
38
+ readonly provider?: string;
39
+ readonly model?: string;
40
+ readonly enabled: boolean;
41
+ readonly yes: boolean;
42
+ readonly errors: readonly string[];
43
+ }
44
+
45
+ export interface RoutineSchedulePromotionPreview {
46
+ readonly routineId: string;
47
+ readonly routineName: string;
48
+ readonly route: typeof ROUTINE_SCHEDULE_ROUTE;
49
+ readonly method: typeof ROUTINE_SCHEDULE_METHOD;
50
+ readonly payload: ScheduleCreateInput;
51
+ }
52
+
53
+ export interface RoutineSchedulePromotionSuccess {
54
+ readonly ok: true;
55
+ readonly kind: typeof ROUTINE_SCHEDULE_METHOD;
56
+ readonly route: typeof ROUTINE_SCHEDULE_ROUTE;
57
+ readonly routineId: string;
58
+ readonly routineName: string;
59
+ readonly schedule: ScheduleCreateOutput;
60
+ readonly request: ScheduleCreateInput;
61
+ }
62
+
63
+ export interface RoutineSchedulePromotionFailure {
64
+ readonly ok: false;
65
+ readonly kind:
66
+ | 'confirmation_required'
67
+ | 'auth_required'
68
+ | 'daemon_unavailable'
69
+ | 'version_mismatch'
70
+ | 'daemon_route_unavailable'
71
+ | 'daemon_error';
72
+ readonly error: string;
73
+ readonly route: typeof ROUTINE_SCHEDULE_ROUTE;
74
+ readonly baseUrl?: string;
75
+ readonly daemonVersion?: string;
76
+ readonly expectedSdkVersion?: string;
77
+ }
78
+
79
+ export type RoutineSchedulePromotionResult =
80
+ | RoutineSchedulePromotionSuccess
81
+ | RoutineSchedulePromotionFailure;
82
+
83
+ function isRecord(value: unknown): value is Record<string, unknown> {
84
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
85
+ }
86
+
87
+ function readString(record: Record<string, unknown>, key: string): string | null {
88
+ const value = record[key];
89
+ return typeof value === 'string' ? value : null;
90
+ }
91
+
92
+ function optionValue(args: readonly string[], index: number, inlineValue: string | undefined): {
93
+ readonly value: string | undefined;
94
+ readonly nextIndex: number;
95
+ } {
96
+ if (inlineValue !== undefined) return { value: inlineValue, nextIndex: index };
97
+ const next = args[index + 1];
98
+ if (next === undefined || next.startsWith('--')) return { value: undefined, nextIndex: index };
99
+ return { value: next, nextIndex: index + 1 };
100
+ }
101
+
102
+ function normalizeProviderModel(provider: string | undefined, model: string | undefined): {
103
+ readonly provider?: string;
104
+ readonly model?: string;
105
+ } {
106
+ if (!model) return provider ? { provider } : {};
107
+ const normalizedProvider = provider ?? getProviderIdFromModel(model);
108
+ return {
109
+ provider: normalizedProvider,
110
+ model: getModelIdFromProviderModel(model),
111
+ };
112
+ }
113
+
114
+ export function parseRoutineSchedulePromotionArgs(args: readonly string[]): ParsedRoutineSchedulePromotionArgs {
115
+ let routineId: string | null = null;
116
+ let schedule: RoutineScheduleSpec | null = null;
117
+ let name: string | undefined;
118
+ let timezone: string | undefined;
119
+ let provider: string | undefined;
120
+ let model: string | undefined;
121
+ let enabled = true;
122
+ let yes = false;
123
+ const errors: string[] = [];
124
+
125
+ for (let index = 0; index < args.length; index += 1) {
126
+ const raw = args[index] ?? '';
127
+ const equals = raw.indexOf('=');
128
+ const optionName = equals >= 0 ? raw.slice(0, equals) : raw;
129
+ const inlineValue = equals >= 0 ? raw.slice(equals + 1) : undefined;
130
+
131
+ if (raw === '--yes') {
132
+ yes = true;
133
+ continue;
134
+ }
135
+ if (raw === '--disabled') {
136
+ enabled = false;
137
+ continue;
138
+ }
139
+ if (optionName === '--name' || optionName === '--timezone' || optionName === '--provider' || optionName === '--model') {
140
+ const consumed = optionValue(args, index, inlineValue);
141
+ index = consumed.nextIndex;
142
+ const value = consumed.value?.trim();
143
+ if (!value) {
144
+ errors.push(`${optionName} requires a value.`);
145
+ continue;
146
+ }
147
+ if (optionName === '--name') name = value;
148
+ if (optionName === '--timezone') timezone = value;
149
+ if (optionName === '--provider') provider = value;
150
+ if (optionName === '--model') model = value;
151
+ continue;
152
+ }
153
+ if (optionName === '--cron' || optionName === '--every' || optionName === '--at') {
154
+ const consumed = optionValue(args, index, inlineValue);
155
+ index = consumed.nextIndex;
156
+ const value = consumed.value?.trim();
157
+ if (!value) {
158
+ errors.push(`${optionName} requires a value.`);
159
+ continue;
160
+ }
161
+ if (schedule) {
162
+ errors.push('Choose exactly one schedule selector: --cron, --every, or --at.');
163
+ continue;
164
+ }
165
+ schedule = {
166
+ kind: optionName === '--cron' ? 'cron' : optionName === '--every' ? 'every' : 'at',
167
+ value,
168
+ };
169
+ continue;
170
+ }
171
+ if (raw.startsWith('--')) {
172
+ errors.push(`Unknown option: ${raw}`);
173
+ continue;
174
+ }
175
+ if (!routineId) {
176
+ routineId = raw;
177
+ continue;
178
+ }
179
+ errors.push(`Unexpected argument: ${raw}`);
180
+ }
181
+
182
+ if (!routineId) errors.push('Routine id or name is required.');
183
+ if (!schedule) errors.push('Schedule is required: use --cron <expr>, --every <interval>, or --at <iso-time>.');
184
+ return { routineId, schedule, name, timezone, provider, model, enabled, yes, errors };
185
+ }
186
+
187
+ export function resolveAgentDaemonConnection(
188
+ configManager: AgentDaemonConfigReader,
189
+ homeDirectory: string,
190
+ ): AgentDaemonConnection {
191
+ const host = String(configManager.get('controlPlane.host') ?? '127.0.0.1');
192
+ const port = Number(configManager.get('controlPlane.port') ?? 3421);
193
+ const tokenPath = join(homeDirectory, '.goodvibes', 'daemon', 'operator-tokens.json');
194
+ if (!existsSync(tokenPath)) return { baseUrl: `http://${host}:${Number.isFinite(port) ? port : 3421}`, token: null, tokenPath };
195
+ try {
196
+ const parsed = JSON.parse(readFileSync(tokenPath, 'utf-8')) as unknown;
197
+ const token = isRecord(parsed) && typeof parsed.token === 'string' ? parsed.token : null;
198
+ return { baseUrl: `http://${host}:${Number.isFinite(port) ? port : 3421}`, token, tokenPath };
199
+ } catch {
200
+ return { baseUrl: `http://${host}:${Number.isFinite(port) ? port : 3421}`, token: null, tokenPath };
201
+ }
202
+ }
203
+
204
+ export function buildRoutineSchedulePrompt(routine: AgentRoutineRecord): string {
205
+ return [
206
+ 'GoodVibes Agent scheduled routine.',
207
+ '',
208
+ `Routine: ${routine.name}`,
209
+ `Routine id: ${routine.id}`,
210
+ `Review state: ${routine.reviewState}`,
211
+ `Tags: ${routine.tags.join(', ') || '(none)'}`,
212
+ `Triggers: ${routine.triggers.join(', ') || '(manual)'}`,
213
+ '',
214
+ 'Operator policy:',
215
+ '- Run this as a serial GoodVibes Agent operator routine.',
216
+ '- Use isolated Agent Knowledge routes only; never use default Knowledge/Wiki or HomeGraph as fallback.',
217
+ '- Do not perform destructive, costly, externally visible, or secret-handling actions without explicit approval.',
218
+ '- Do not request WRFC unless this scheduled routine explicitly delegates build/fix/review work to GoodVibes TUI.',
219
+ '- Summarize what was checked, what changed, and what still needs user review.',
220
+ '',
221
+ 'Routine description:',
222
+ routine.description,
223
+ '',
224
+ 'Routine steps:',
225
+ routine.steps,
226
+ ].join('\n');
227
+ }
228
+
229
+ export function buildRoutineSchedulePayload(
230
+ routine: AgentRoutineRecord,
231
+ parsed: ParsedRoutineSchedulePromotionArgs,
232
+ ): ScheduleCreateInput {
233
+ if (!parsed.schedule) throw new Error('Schedule is required.');
234
+ const modelRoute = normalizeProviderModel(parsed.provider, parsed.model);
235
+ const payload: ScheduleCreateInput = {
236
+ name: parsed.name ?? `Agent routine: ${routine.name}`,
237
+ prompt: buildRoutineSchedulePrompt(routine),
238
+ kind: parsed.schedule.kind,
239
+ enabled: parsed.enabled,
240
+ target: {
241
+ kind: 'main',
242
+ surfaceKind: 'service',
243
+ preserveThread: true,
244
+ createIfMissing: true,
245
+ },
246
+ delivery: {
247
+ mode: 'none',
248
+ targets: [],
249
+ fallbackTargets: [],
250
+ includeSummary: true,
251
+ includeTranscript: false,
252
+ includeLinks: true,
253
+ },
254
+ failure: {
255
+ action: 'retry',
256
+ maxConsecutiveFailures: 3,
257
+ cooldownMs: 3_600_000,
258
+ retryPolicy: {
259
+ maxAttempts: 2,
260
+ delayMs: 60_000,
261
+ strategy: 'exponential',
262
+ maxDelayMs: 900_000,
263
+ jitterMs: 30_000,
264
+ },
265
+ disableAfterFailures: false,
266
+ },
267
+ lightContext: true,
268
+ autoApprove: false,
269
+ allowUnsafeExternalContent: false,
270
+ ...modelRoute,
271
+ };
272
+ if (parsed.schedule.kind === 'cron') {
273
+ return {
274
+ ...payload,
275
+ cron: parsed.schedule.value,
276
+ timezone: parsed.timezone,
277
+ };
278
+ }
279
+ if (parsed.schedule.kind === 'every') {
280
+ return { ...payload, every: parsed.schedule.value };
281
+ }
282
+ return { ...payload, at: parsed.schedule.value };
283
+ }
284
+
285
+ export function buildRoutineSchedulePreview(
286
+ routine: AgentRoutineRecord,
287
+ parsed: ParsedRoutineSchedulePromotionArgs,
288
+ ): RoutineSchedulePromotionPreview {
289
+ return {
290
+ routineId: routine.id,
291
+ routineName: routine.name,
292
+ route: ROUTINE_SCHEDULE_ROUTE,
293
+ method: ROUTINE_SCHEDULE_METHOD,
294
+ payload: buildRoutineSchedulePayload(routine, parsed),
295
+ };
296
+ }
297
+
298
+ async function fetchDaemonStatus(connection: AgentDaemonConnection): Promise<{
299
+ readonly ok: boolean;
300
+ readonly status: number;
301
+ readonly body: unknown;
302
+ }> {
303
+ try {
304
+ const response = await fetch(`${connection.baseUrl}/status`, {
305
+ headers: connection.token ? { authorization: `Bearer ${connection.token}` } : undefined,
306
+ });
307
+ const text = await response.text();
308
+ let body: unknown = text;
309
+ try {
310
+ body = text.trim() ? JSON.parse(text) as unknown : {};
311
+ } catch {
312
+ body = text;
313
+ }
314
+ return { ok: response.ok, status: response.status, body };
315
+ } catch (error) {
316
+ return { ok: false, status: 0, body: summarizeError(error) };
317
+ }
318
+ }
319
+
320
+ async function classifyScheduleError(
321
+ error: unknown,
322
+ connection: AgentDaemonConnection,
323
+ ): Promise<RoutineSchedulePromotionFailure> {
324
+ const message = summarizeError(error);
325
+ const lower = message.toLowerCase();
326
+ if (lower.includes('401') || lower.includes('unauthorized') || lower.includes('auth')) {
327
+ return { ok: false, kind: 'auth_required', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
328
+ }
329
+ if (lower.includes('404') || lower.includes('not found')) {
330
+ const daemon = await fetchDaemonStatus(connection);
331
+ const record = isRecord(daemon.body) ? daemon.body : {};
332
+ const daemonVersion = readString(record, 'version') ?? 'unknown';
333
+ if (daemon.ok && daemonVersion !== SDK_VERSION) {
334
+ return {
335
+ ok: false,
336
+ kind: 'version_mismatch',
337
+ error: `External daemon SDK version ${daemonVersion} does not match Agent SDK pin ${SDK_VERSION}; schedules.create is unavailable.`,
338
+ route: ROUTINE_SCHEDULE_ROUTE,
339
+ baseUrl: connection.baseUrl,
340
+ daemonVersion,
341
+ expectedSdkVersion: SDK_VERSION,
342
+ };
343
+ }
344
+ return { ok: false, kind: 'daemon_route_unavailable', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
345
+ }
346
+ if (lower.includes('fetch') || lower.includes('connect') || lower.includes('econnrefused')) {
347
+ return { ok: false, kind: 'daemon_unavailable', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
348
+ }
349
+ return { ok: false, kind: 'daemon_error', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
350
+ }
351
+
352
+ export async function promoteRoutineToDaemonSchedule(
353
+ connection: AgentDaemonConnection,
354
+ preview: RoutineSchedulePromotionPreview,
355
+ ): Promise<RoutineSchedulePromotionResult> {
356
+ if (!connection.token) {
357
+ return {
358
+ ok: false,
359
+ kind: 'auth_required',
360
+ error: `No daemon operator token found at ${connection.tokenPath}`,
361
+ route: ROUTINE_SCHEDULE_ROUTE,
362
+ baseUrl: connection.baseUrl,
363
+ };
364
+ }
365
+ try {
366
+ const sdk = createBrowserGoodVibesSdk({ baseUrl: connection.baseUrl, authToken: connection.token });
367
+ const schedule = await sdk.operator.invoke(ROUTINE_SCHEDULE_METHOD, preview.payload);
368
+ return {
369
+ ok: true,
370
+ kind: ROUTINE_SCHEDULE_METHOD,
371
+ route: ROUTINE_SCHEDULE_ROUTE,
372
+ routineId: preview.routineId,
373
+ routineName: preview.routineName,
374
+ schedule,
375
+ request: preview.payload,
376
+ };
377
+ } catch (error) {
378
+ return classifyScheduleError(error, connection);
379
+ }
380
+ }
381
+
382
+ export function formatRoutineSchedulePreview(preview: RoutineSchedulePromotionPreview): string {
383
+ const schedule = preview.payload.kind === 'cron'
384
+ ? `${preview.payload.cron}${preview.payload.timezone ? ` [${preview.payload.timezone}]` : ''}`
385
+ : preview.payload.kind === 'every'
386
+ ? String(preview.payload.every)
387
+ : String(preview.payload.at);
388
+ return [
389
+ 'Daemon schedule preview for Agent routine',
390
+ ` routine: ${preview.routineName} (${preview.routineId})`,
391
+ ` route: ${preview.method} ${preview.route}`,
392
+ ` name: ${String(preview.payload.name ?? '(daemon default)')}`,
393
+ ` schedule: ${preview.payload.kind} ${schedule}`,
394
+ ` enabled: ${preview.payload.enabled === false ? 'no' : 'yes'}`,
395
+ ' target: external daemon service/main conversation route',
396
+ ' policy: isolated Agent Knowledge only; no default wiki/HomeGraph fallback; no WRFC unless explicitly delegated',
397
+ ' next: rerun with --yes to create this daemon schedule',
398
+ ].join('\n');
399
+ }
400
+
401
+ export function formatRoutineScheduleSuccess(result: RoutineSchedulePromotionSuccess): string {
402
+ const record: Record<string, unknown> = isRecord(result.schedule) ? result.schedule : {};
403
+ const id = readString(record, 'id') ?? '(unknown)';
404
+ const status = readString(record, 'status') ?? (record.enabled === false ? 'paused' : 'enabled');
405
+ return [
406
+ 'Created daemon schedule for Agent routine',
407
+ ` routine: ${result.routineName} (${result.routineId})`,
408
+ ` schedule: ${id}`,
409
+ ` status: ${status}`,
410
+ ` route: ${result.kind} ${result.route}`,
411
+ ' next: inspect with /schedule list or daemon schedule observability',
412
+ ].join('\n');
413
+ }
414
+
415
+ export function formatRoutineScheduleFailure(failure: RoutineSchedulePromotionFailure): string {
416
+ return [
417
+ `Daemon schedule error: ${failure.kind}`,
418
+ ` ${failure.error}`,
419
+ failure.baseUrl ? ` daemon: ${failure.baseUrl}` : null,
420
+ ` route: ${ROUTINE_SCHEDULE_METHOD} ${failure.route}`,
421
+ failure.kind === 'version_mismatch' && failure.daemonVersion && failure.expectedSdkVersion
422
+ ? ` versions: daemon=${failure.daemonVersion} expected=${failure.expectedSdkVersion}`
423
+ : null,
424
+ failure.kind === 'auth_required'
425
+ ? ' next: pair/authenticate with the externally managed GoodVibes daemon, then retry with --yes.'
426
+ : null,
427
+ failure.kind === 'daemon_unavailable'
428
+ ? ' next: start/restart the external GoodVibes daemon from TUI or daemon host tooling; Agent does not own daemon lifecycle.'
429
+ : null,
430
+ failure.kind === 'version_mismatch' || failure.kind === 'daemon_route_unavailable'
431
+ ? ' next: update/restart the external GoodVibes daemon so public schedules.create is available.'
432
+ : null,
433
+ ].filter((line): line is string => Boolean(line)).join('\n');
434
+ }
@@ -615,6 +615,10 @@ export function getAgentRuntimeProfileTemplate(templateId: AgentRuntimeProfileTe
615
615
  return summarizeTemplate(resolveAgentRuntimeProfileTemplate(templateId, baseHomeDirectory));
616
616
  }
617
617
 
618
+ export function getAgentRuntimeProfileTemplateFile(templateId: AgentRuntimeProfileTemplateId, baseHomeDirectory?: string): AgentRuntimeProfileStarterTemplateFile {
619
+ return templateFilePayload(resolveAgentRuntimeProfileTemplate(templateId, baseHomeDirectory));
620
+ }
621
+
618
622
  function templateFilePayload(template: AgentRuntimeProfileStarterTemplate): AgentRuntimeProfileStarterTemplateFile {
619
623
  return {
620
624
  version: 1,
@@ -12,6 +12,7 @@ const COMMANDS = [
12
12
  'models',
13
13
  'providers',
14
14
  'profiles',
15
+ 'routines',
15
16
  'auth',
16
17
  'compat',
17
18
  'capabilities',
package/src/cli/help.ts CHANGED
@@ -39,6 +39,7 @@ export function renderGoodVibesHelp(binary = 'goodvibes-agent'): string {
39
39
  ' models [provider] List/use/pin selectable models and recent model history',
40
40
  ' providers List/inspect/use provider config/auth posture',
41
41
  ' profiles Manage isolated Agent runtime profile homes',
42
+ ' routines Inspect local routines and explicitly promote one to a daemon schedule',
42
43
  ' auth Inspect and manage local users, sessions, and bootstrap auth',
43
44
  ' compat Inspect Agent SDK pin, daemon version, and Agent knowledge route readiness',
44
45
  ' capabilities Show OpenClaw/Hermes capability parity and Agent readiness',
@@ -98,6 +99,7 @@ export function renderGoodVibesHelp(binary = 'goodvibes-agent'): string {
98
99
  ` ${binary} providers inspect openai`,
99
100
  ` ${binary} profiles create household --template household --yes`,
100
101
  ` ${binary} --agent-profile household`,
102
+ ` ${binary} routines promote daily-operations-sweep --cron "0 9 * * *" --timezone America/Chicago --yes`,
101
103
  ` ${binary} compat`,
102
104
  ` ${binary} capabilities`,
103
105
  ` ${binary} knowledge status`,
@@ -158,6 +160,21 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
158
160
  summary: 'Create and inspect isolated Agent runtime profile homes, with starter templates for household, research, travel, operations, personal productivity, and local imported starters. A profile changes Agent-local config, sessions, memory, personas, skills, routines, and setup paths without changing the externally owned daemon.',
159
161
  examples: ['profiles templates', 'profiles templates export research ./research-starter.json --yes', 'profiles templates import ./research-starter.json --yes', 'profiles create household --template household --yes', '--agent-profile household status'],
160
162
  },
163
+ routines: {
164
+ usage: [
165
+ 'routines list',
166
+ 'routines enabled',
167
+ 'routines show <id>',
168
+ 'routines promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) [--timezone <tz>] [--name <schedule-name>] [--provider <id>] [--model <model>] [--disabled] --yes',
169
+ ],
170
+ summary: 'Inspect Agent-local routines and explicitly promote a reviewed routine into an external daemon schedule. Without --yes, promote only prints the schedules.create preview.',
171
+ examples: [
172
+ 'routines list',
173
+ 'routines show daily-operations-sweep',
174
+ 'routines promote daily-operations-sweep --cron "0 9 * * *" --timezone America/Chicago --yes',
175
+ 'routines promote weekly-review --every 7d --disabled',
176
+ ],
177
+ },
161
178
  models: {
162
179
  usage: ['models [provider]', 'models current', 'models use <registryKey>', 'models pin <registryKey>', 'models recent'],
163
180
  summary: 'List, inspect, select, pin, and review model choices.',
@@ -35,6 +35,7 @@ import { buildControlPlaneStatusResult, formatControlPlaneStatus, handleSecrets,
35
35
  import { handleAgentKnowledgeCommand, handleAgentKnowledgeShortcutCommand, handleCompatCommand, handleDelegateCommand } from './agent-knowledge-command.ts';
36
36
  import { handleCapabilitiesCommand } from './capabilities-command.ts';
37
37
  import { handleProfilesCommand } from './profiles-command.ts';
38
+ import { handleRoutinesCommand } from './routines-command.ts';
38
39
  import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
39
40
 
40
41
  export interface CliCommandRuntime {
@@ -710,6 +711,11 @@ export async function handleGoodVibesCliCommand(runtime: CliCommandRuntime): Pro
710
711
  console.log(result.output);
711
712
  return { handled: true, exitCode: result.exitCode };
712
713
  }
714
+ case 'routines': {
715
+ const result = await handleRoutinesCommand(runtime);
716
+ console.log(result.output);
717
+ return { handled: true, exitCode: result.exitCode };
718
+ }
713
719
  case 'knowledge': {
714
720
  const result = await handleAgentKnowledgeCommand(runtime);
715
721
  console.log(result.output);
package/src/cli/parser.ts CHANGED
@@ -27,6 +27,8 @@ const COMMAND_ALIASES: Readonly<Record<string, GoodVibesCliCommand>> = {
27
27
  provider: 'providers',
28
28
  profiles: 'profiles',
29
29
  profile: 'profiles',
30
+ routines: 'routines',
31
+ routine: 'routines',
30
32
  auth: 'auth',
31
33
  compat: 'compat',
32
34
  compatibility: 'compat',