@pellux/goodvibes-agent 0.1.36 → 0.1.38

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.
@@ -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,24 @@ 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 receipts',
169
+ '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
+ ],
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
+ examples: [
174
+ 'routines list',
175
+ 'routines show daily-operations-sweep',
176
+ 'routines receipts',
177
+ 'routines promote daily-operations-sweep --cron "0 9 * * *" --timezone America/Chicago --yes',
178
+ 'routines promote weekly-review --every 7d --disabled',
179
+ ],
180
+ },
161
181
  models: {
162
182
  usage: ['models [provider]', 'models current', 'models use <registryKey>', 'models pin <registryKey>', 'models recent'],
163
183
  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',
@@ -0,0 +1,230 @@
1
+ import { createShellPathService } from '@/runtime/index.ts';
2
+ import { AgentRoutineRegistry, type AgentRoutineRecord } from '../agent/routine-registry.ts';
3
+ import {
4
+ buildRoutineSchedulePreview,
5
+ formatRoutineScheduleReceipt,
6
+ formatRoutineScheduleReceipts,
7
+ formatRoutineScheduleFailure,
8
+ formatRoutineSchedulePreview,
9
+ formatRoutineScheduleSuccess,
10
+ parseRoutineSchedulePromotionArgs,
11
+ promoteRoutineToDaemonSchedule,
12
+ resolveAgentDaemonConnection,
13
+ RoutineScheduleReceiptStore,
14
+ } from '../agent/routine-schedule-promotion.ts';
15
+ import type { CliCommandOutput } from './types.ts';
16
+ import type { CliCommandRuntime } from './management.ts';
17
+
18
+ interface RoutinesCommandSuccess<TData> {
19
+ readonly ok: true;
20
+ readonly kind: string;
21
+ readonly data: TData;
22
+ }
23
+
24
+ interface RoutinesCommandFailure {
25
+ readonly ok: false;
26
+ readonly kind: string;
27
+ readonly error: string;
28
+ }
29
+
30
+ function jsonOrText(runtime: CliCommandRuntime, value: unknown, text: string): string {
31
+ return runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : text;
32
+ }
33
+
34
+ function routineRegistry(runtime: CliCommandRuntime): AgentRoutineRegistry {
35
+ return AgentRoutineRegistry.fromShellPaths(createShellPathService({
36
+ workingDirectory: runtime.workingDirectory,
37
+ homeDirectory: runtime.homeDirectory,
38
+ }));
39
+ }
40
+
41
+ function routineReceiptStore(runtime: CliCommandRuntime): RoutineScheduleReceiptStore {
42
+ return RoutineScheduleReceiptStore.fromShellPaths(createShellPathService({
43
+ workingDirectory: runtime.workingDirectory,
44
+ homeDirectory: runtime.homeDirectory,
45
+ }));
46
+ }
47
+
48
+ function summarizeRoutine(routine: AgentRoutineRecord): string {
49
+ const enabled = routine.enabled ? 'enabled' : 'disabled';
50
+ const tags = routine.tags.length > 0 ? ` tags=${routine.tags.join(',')}` : '';
51
+ return ` ${routine.id} ${enabled} ${routine.reviewState} starts=${routine.startCount} ${routine.name} - ${routine.description}${tags}`;
52
+ }
53
+
54
+ function renderRoutineList(title: string, path: string, routines: readonly AgentRoutineRecord[]): string {
55
+ if (routines.length === 0) {
56
+ return [
57
+ title,
58
+ ' No local Agent routines yet.',
59
+ ' Create routines inside the Agent TUI with /routines create, or create a runtime profile from a starter template.',
60
+ ].join('\n');
61
+ }
62
+ return [
63
+ `${title} (${routines.length})`,
64
+ ` store: ${path}`,
65
+ ...routines.map(summarizeRoutine),
66
+ ].join('\n');
67
+ }
68
+
69
+ function renderRoutine(routine: AgentRoutineRecord): string {
70
+ return [
71
+ `Routine ${routine.name}`,
72
+ ` id: ${routine.id}`,
73
+ ` enabled: ${routine.enabled ? 'yes' : 'no'}`,
74
+ ` review: ${routine.reviewState}`,
75
+ ` source: ${routine.source}`,
76
+ ` provenance: ${routine.provenance}`,
77
+ ` tags: ${routine.tags.join(', ') || '(none)'}`,
78
+ ` triggers: ${routine.triggers.join(', ') || '(manual)'}`,
79
+ ` started: ${routine.startCount}${routine.lastStartedAt ? `; last ${routine.lastStartedAt}` : ''}`,
80
+ ` created: ${routine.createdAt}`,
81
+ ` updated: ${routine.updatedAt}`,
82
+ routine.staleReason ? ` stale reason: ${routine.staleReason}` : '',
83
+ '',
84
+ routine.description,
85
+ '',
86
+ routine.steps,
87
+ ].filter((line): line is string => Boolean(line)).join('\n');
88
+ }
89
+
90
+ async function handleRoutinePromotion(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
91
+ const parsed = parseRoutineSchedulePromotionArgs(args);
92
+ const json = runtime.cli.flags.outputFormat === 'json';
93
+ if (parsed.errors.length > 0) {
94
+ const failure: RoutinesCommandFailure = {
95
+ ok: false,
96
+ kind: 'invalid_routine_schedule_promotion',
97
+ error: parsed.errors.join(' '),
98
+ };
99
+ return {
100
+ 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',
102
+ ...parsed.errors.map((error) => ` ${error}`),
103
+ ].join('\n'),
104
+ exitCode: 2,
105
+ };
106
+ }
107
+ const registry = routineRegistry(runtime);
108
+ const routine = registry.get(parsed.routineId ?? '');
109
+ if (!routine) {
110
+ const failure: RoutinesCommandFailure = {
111
+ ok: false,
112
+ kind: 'routine_not_found',
113
+ error: `Unknown Agent routine: ${parsed.routineId ?? ''}`,
114
+ };
115
+ return {
116
+ output: json ? JSON.stringify(failure, null, 2) : failure.error,
117
+ exitCode: 1,
118
+ };
119
+ }
120
+ const preview = buildRoutineSchedulePreview(routine, parsed);
121
+ if (!parsed.yes) {
122
+ const value: RoutinesCommandSuccess<typeof preview> = {
123
+ ok: true,
124
+ kind: 'schedules.create.preview',
125
+ data: preview,
126
+ };
127
+ return {
128
+ output: jsonOrText(runtime, value, formatRoutineSchedulePreview(preview)),
129
+ exitCode: 0,
130
+ };
131
+ }
132
+ const connection = resolveAgentDaemonConnection(runtime.configManager, runtime.homeDirectory);
133
+ const result = await promoteRoutineToDaemonSchedule(connection, preview);
134
+ const receipt = routineReceiptStore(runtime).append(connection, preview, result);
135
+ if (!result.ok) {
136
+ return {
137
+ output: json ? JSON.stringify({ ...result, receipt }, null, 2) : `${formatRoutineScheduleFailure(result)}\n receipt: ${receipt.id}`,
138
+ exitCode: 1,
139
+ };
140
+ }
141
+ const value = { ...result, receipt };
142
+ return {
143
+ output: jsonOrText(runtime, value, `${formatRoutineScheduleSuccess(result)}\n receipt: ${receipt.id}`),
144
+ exitCode: 0,
145
+ };
146
+ }
147
+
148
+ export async function handleRoutinesCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
149
+ const [sub = 'list', ...rest] = runtime.cli.commandArgs;
150
+ const registry = routineRegistry(runtime);
151
+ const snapshot = registry.snapshot();
152
+ const normalized = sub.toLowerCase();
153
+ if (normalized === 'list' || normalized === 'enabled') {
154
+ const routines = normalized === 'enabled' ? snapshot.enabledRoutines : snapshot.routines;
155
+ const value: RoutinesCommandSuccess<{
156
+ readonly path: string;
157
+ readonly routines: readonly AgentRoutineRecord[];
158
+ readonly enabledCount: number;
159
+ }> = {
160
+ ok: true,
161
+ kind: normalized === 'enabled' ? 'agent.routines.enabled' : 'agent.routines.list',
162
+ data: {
163
+ path: snapshot.path,
164
+ routines,
165
+ enabledCount: snapshot.enabledRoutines.length,
166
+ },
167
+ };
168
+ return {
169
+ output: jsonOrText(runtime, value, renderRoutineList(normalized === 'enabled' ? 'Enabled Agent routines' : 'Agent routines', snapshot.path, routines)),
170
+ exitCode: 0,
171
+ };
172
+ }
173
+ if (normalized === 'show') {
174
+ const id = rest[0];
175
+ if (!id) return { output: 'Usage: goodvibes-agent routines show <id>', exitCode: 2 };
176
+ const routine = registry.get(id);
177
+ if (!routine) {
178
+ const failure: RoutinesCommandFailure = { ok: false, kind: 'routine_not_found', error: `Unknown Agent routine: ${id}` };
179
+ return {
180
+ output: runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(failure, null, 2) : failure.error,
181
+ exitCode: 1,
182
+ };
183
+ }
184
+ const value: RoutinesCommandSuccess<AgentRoutineRecord> = { ok: true, kind: 'agent.routines.show', data: routine };
185
+ return {
186
+ output: jsonOrText(runtime, value, renderRoutine(routine)),
187
+ exitCode: 0,
188
+ };
189
+ }
190
+ if (normalized === 'receipts' || normalized === 'history') {
191
+ const snapshot = routineReceiptStore(runtime).snapshot();
192
+ const value: RoutinesCommandSuccess<typeof snapshot> = {
193
+ ok: true,
194
+ kind: 'agent.routines.scheduleReceipts.list',
195
+ data: snapshot,
196
+ };
197
+ return {
198
+ output: jsonOrText(runtime, value, formatRoutineScheduleReceipts(snapshot)),
199
+ exitCode: 0,
200
+ };
201
+ }
202
+ if (normalized === 'receipt') {
203
+ const id = rest[0];
204
+ if (!id) return { output: 'Usage: goodvibes-agent routines receipt <receipt-id>', exitCode: 2 };
205
+ const receipt = routineReceiptStore(runtime).get(id);
206
+ if (!receipt) {
207
+ const failure: RoutinesCommandFailure = { ok: false, kind: 'routine_schedule_receipt_not_found', error: `Unknown routine schedule receipt: ${id}` };
208
+ return {
209
+ output: runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(failure, null, 2) : failure.error,
210
+ exitCode: 1,
211
+ };
212
+ }
213
+ const value: RoutinesCommandSuccess<typeof receipt> = {
214
+ ok: true,
215
+ kind: 'agent.routines.scheduleReceipts.get',
216
+ data: receipt,
217
+ };
218
+ return {
219
+ output: jsonOrText(runtime, value, formatRoutineScheduleReceipt(receipt)),
220
+ exitCode: 0,
221
+ };
222
+ }
223
+ if (normalized === 'promote' || normalized === 'schedule' || normalized === 'promote-schedule') {
224
+ return handleRoutinePromotion(runtime, rest);
225
+ }
226
+ return {
227
+ output: 'Usage: goodvibes-agent routines [list|enabled|show <id>|receipts|receipt <id>|promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) --yes]',
228
+ exitCode: 2,
229
+ };
230
+ }
package/src/cli/types.ts CHANGED
@@ -10,6 +10,7 @@ export type GoodVibesCliCommand =
10
10
  | 'models'
11
11
  | 'providers'
12
12
  | 'profiles'
13
+ | 'routines'
13
14
  | 'auth'
14
15
  | 'compat'
15
16
  | 'capabilities'
@@ -572,11 +572,13 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
572
572
  id: 'automation',
573
573
  group: 'WATCH',
574
574
  label: 'Automation',
575
- summary: 'Read-only automation and schedule observability.',
576
- detail: 'Agent does not create, run, enable, disable, or remove local automation jobs. Schedule mutations wait for an Agent-safe public route and explicit approval.',
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.',
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-policy', label: 'Mutation blocked', detail: 'Schedule add/run/remove/enable/disable are intentionally blocked in Agent.', kind: 'guidance', safety: 'blocked' },
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' },
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-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' },
580
582
  { 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' },
581
583
  ],
582
584
  },
@@ -1,4 +1,16 @@
1
1
  import { AgentRoutineRegistry, type AgentRoutineRecord } from '../../agent/routine-registry.ts';
2
+ import {
3
+ buildRoutineSchedulePreview,
4
+ formatRoutineScheduleReceipt,
5
+ formatRoutineScheduleReceipts,
6
+ formatRoutineScheduleFailure,
7
+ formatRoutineSchedulePreview,
8
+ formatRoutineScheduleSuccess,
9
+ parseRoutineSchedulePromotionArgs,
10
+ promoteRoutineToDaemonSchedule,
11
+ resolveAgentDaemonConnection,
12
+ RoutineScheduleReceiptStore,
13
+ } from '../../agent/routine-schedule-promotion.ts';
2
14
  import type { CommandContext, CommandRegistry } from '../command-registry.ts';
3
15
  import { requireShellPaths } from './runtime-services.ts';
4
16
 
@@ -43,6 +55,10 @@ function registryFromContext(ctx: CommandContext): AgentRoutineRegistry {
43
55
  return AgentRoutineRegistry.fromShellPaths(requireShellPaths(ctx));
44
56
  }
45
57
 
58
+ function receiptStoreFromContext(ctx: CommandContext): RoutineScheduleReceiptStore {
59
+ return RoutineScheduleReceiptStore.fromShellPaths(requireShellPaths(ctx));
60
+ }
61
+
46
62
  function requiredFlag(flags: ReadonlyMap<string, string>, key: string): string {
47
63
  const value = flags.get(key)?.trim();
48
64
  if (!value) throw new Error(`Missing --${key}.`);
@@ -93,6 +109,32 @@ function printError(ctx: CommandContext, error: unknown): void {
93
109
  ctx.print(`Error: ${error instanceof Error ? error.message : String(error)}`);
94
110
  }
95
111
 
112
+ async function promoteRoutine(args: readonly string[], routineRegistry: AgentRoutineRegistry, ctx: CommandContext): Promise<void> {
113
+ const parsed = parseRoutineSchedulePromotionArgs(args);
114
+ if (parsed.errors.length > 0) {
115
+ ctx.print([
116
+ 'Usage: /routines promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) [--timezone <tz>] [--name <schedule-name>] [--provider <id>] [--model <model>] [--disabled] --yes',
117
+ ...parsed.errors.map((error) => ` ${error}`),
118
+ ].join('\n'));
119
+ return;
120
+ }
121
+ const routine = routineRegistry.get(parsed.routineId ?? '');
122
+ if (!routine) {
123
+ ctx.print(`Unknown Agent routine: ${parsed.routineId ?? ''}`);
124
+ return;
125
+ }
126
+ const preview = buildRoutineSchedulePreview(routine, parsed);
127
+ if (!parsed.yes) {
128
+ ctx.print(formatRoutineSchedulePreview(preview));
129
+ return;
130
+ }
131
+ const shellPaths = requireShellPaths(ctx);
132
+ const connection = resolveAgentDaemonConnection(ctx.platform.configManager, shellPaths.homeDirectory);
133
+ const result = await promoteRoutineToDaemonSchedule(connection, preview);
134
+ const receipt = receiptStoreFromContext(ctx).append(connection, preview, result);
135
+ ctx.print(result.ok ? `${formatRoutineScheduleSuccess(result)}\n receipt: ${receipt.id}` : `${formatRoutineScheduleFailure(result)}\n receipt: ${receipt.id}`);
136
+ }
137
+
96
138
  export async function runRoutinesRuntimeCommand(args: readonly string[], ctx: CommandContext): Promise<void> {
97
139
  const sub = (args[0] ?? 'list').toLowerCase();
98
140
  const routineRegistry = registryFromContext(ctx);
@@ -121,6 +163,20 @@ export async function runRoutinesRuntimeCommand(args: readonly string[], ctx: Co
121
163
  ctx.print(routine ? renderRoutine(routine) : `Unknown Agent routine: ${id}`);
122
164
  return;
123
165
  }
166
+ if (sub === 'receipts' || sub === 'history') {
167
+ ctx.print(formatRoutineScheduleReceipts(receiptStoreFromContext(ctx).snapshot()));
168
+ return;
169
+ }
170
+ if (sub === 'receipt') {
171
+ const id = args[1];
172
+ if (!id) {
173
+ ctx.print('Usage: /routines receipt <receipt-id>');
174
+ return;
175
+ }
176
+ const receipt = receiptStoreFromContext(ctx).get(id);
177
+ ctx.print(receipt ? formatRoutineScheduleReceipt(receipt) : `Unknown routine schedule receipt: ${id}`);
178
+ return;
179
+ }
124
180
  if (sub === 'create') {
125
181
  const parsed = parseRoutineArgs(args.slice(1));
126
182
  const steps = parsed.flags.get('steps')?.trim() || parsed.rest.join(' ').trim();
@@ -200,6 +256,10 @@ export async function runRoutinesRuntimeCommand(args: readonly string[], ctx: Co
200
256
  ctx.print(`Marked Agent routine ${routine.id} stale.`);
201
257
  return;
202
258
  }
259
+ if (sub === 'promote' || sub === 'schedule' || sub === 'promote-schedule') {
260
+ await promoteRoutine(args.slice(1), routineRegistry, ctx);
261
+ return;
262
+ }
203
263
  if (sub === 'delete' || sub === 'remove') {
204
264
  const parsed = parseRoutineArgs(args.slice(1));
205
265
  const id = parsed.rest[0];
@@ -215,7 +275,7 @@ export async function runRoutinesRuntimeCommand(args: readonly string[], ctx: Co
215
275
  ctx.print(`Deleted Agent routine ${removed.id}: ${removed.name}`);
216
276
  return;
217
277
  }
218
- ctx.print('Usage: /routines [list|enabled|search|show|create|update|enable|disable|start|review|stale|delete]');
278
+ ctx.print('Usage: /routines [list|enabled|search|show|receipts|receipt|create|update|enable|disable|start|review|stale|promote|delete]');
219
279
  } catch (error) {
220
280
  printError(ctx, error);
221
281
  }
@@ -226,7 +286,7 @@ export function registerRoutinesRuntimeCommands(registry: CommandRegistry): void
226
286
  name: 'routines',
227
287
  aliases: ['routine'],
228
288
  description: 'Manage local GoodVibes Agent routines',
229
- 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...>|delete <id> --yes]',
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]',
230
290
  handler: runRoutinesRuntimeCommand,
231
291
  });
232
292
  }
@@ -4,6 +4,21 @@ import {
4
4
  } from '@pellux/goodvibes-sdk/platform/automation';
5
5
  import type { AutomationJob } from '@pellux/goodvibes-sdk/platform/automation';
6
6
  import type { AutomationScheduleDefinition } from '@pellux/goodvibes-sdk/platform/automation';
7
+ import { AgentRoutineRegistry } from '../../agent/routine-registry.ts';
8
+ import {
9
+ buildRoutineSchedulePreview,
10
+ formatRoutineScheduleReceipt,
11
+ formatRoutineScheduleReceipts,
12
+ formatRoutineScheduleFailure,
13
+ formatRoutineSchedulePreview,
14
+ formatRoutineScheduleSuccess,
15
+ parseRoutineSchedulePromotionArgs,
16
+ promoteRoutineToDaemonSchedule,
17
+ resolveAgentDaemonConnection,
18
+ RoutineScheduleReceiptStore,
19
+ } from '../../agent/routine-schedule-promotion.ts';
20
+ import type { CommandContext } from '../command-registry.ts';
21
+ import { requireShellPaths } from './runtime-services.ts';
7
22
 
8
23
  function formatSchedule(schedule: AutomationScheduleDefinition): string {
9
24
  switch (schedule.kind) {
@@ -31,35 +46,83 @@ function formatPrompt(job: AutomationJob): string {
31
46
 
32
47
  function printReadOnlyScheduleBoundary(print: (text: string) => void, requestedAction: string): void {
33
48
  print([
34
- 'GoodVibes Agent schedule commands are read-only in this runtime.',
49
+ 'GoodVibes Agent local schedule commands are read-only in this runtime.',
35
50
  ` requested: ${requestedAction}`,
36
51
  ' policy: no local Agent automation jobs, scheduled spawns, or immediate automation runs',
37
52
  ' use: /schedule list',
38
- ' future: mutate schedules through an Agent-safe public daemon route after explicit approval',
53
+ ' daemon route: use /schedule promote-routine <routine> --cron <expr> --yes to create an external daemon schedule explicitly',
39
54
  ].join('\n'));
40
55
  }
41
56
 
57
+ async function promoteRoutineSchedule(args: readonly string[], ctx: CommandContext): Promise<void> {
58
+ const parsed = parseRoutineSchedulePromotionArgs(args);
59
+ if (parsed.errors.length > 0) {
60
+ 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',
62
+ ...parsed.errors.map((error) => ` ${error}`),
63
+ ].join('\n'));
64
+ return;
65
+ }
66
+ const shellPaths = requireShellPaths(ctx);
67
+ const routine = AgentRoutineRegistry.fromShellPaths(shellPaths).get(parsed.routineId ?? '');
68
+ if (!routine) {
69
+ ctx.print(`Unknown Agent routine: ${parsed.routineId ?? ''}`);
70
+ return;
71
+ }
72
+ const preview = buildRoutineSchedulePreview(routine, parsed);
73
+ if (!parsed.yes) {
74
+ ctx.print(formatRoutineSchedulePreview(preview));
75
+ return;
76
+ }
77
+ const connection = resolveAgentDaemonConnection(ctx.platform.configManager, shellPaths.homeDirectory);
78
+ const result = await promoteRoutineToDaemonSchedule(connection, preview);
79
+ const receipt = RoutineScheduleReceiptStore.fromShellPaths(shellPaths).append(connection, preview, result);
80
+ ctx.print(result.ok ? `${formatRoutineScheduleSuccess(result)}\n receipt: ${receipt.id}` : `${formatRoutineScheduleFailure(result)}\n receipt: ${receipt.id}`);
81
+ }
82
+
42
83
  export function registerScheduleRuntimeCommands(registry: CommandRegistry): void {
43
84
  registry.register({
44
85
  name: 'schedule',
45
86
  aliases: ['sched'],
46
- description: 'Inspect automation jobs and scheduled runs',
47
- usage: 'list',
48
- argsHint: 'list',
87
+ 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',
49
90
  async handler(args, ctx) {
91
+ const sub = args[0];
92
+
93
+ if (sub === 'promote-routine' || sub === 'promote' || sub === 'create-routine-schedule') {
94
+ await promoteRoutineSchedule(args.slice(1), ctx);
95
+ return;
96
+ }
97
+
98
+ if (sub === 'receipts' || sub === 'history') {
99
+ ctx.print(formatRoutineScheduleReceipts(RoutineScheduleReceiptStore.fromShellPaths(requireShellPaths(ctx)).snapshot()));
100
+ return;
101
+ }
102
+
103
+ if (sub === 'receipt') {
104
+ const id = args[1];
105
+ if (!id) {
106
+ ctx.print('Usage: /schedule receipt <receipt-id>');
107
+ return;
108
+ }
109
+ const receipt = RoutineScheduleReceiptStore.fromShellPaths(requireShellPaths(ctx)).get(id);
110
+ ctx.print(receipt ? formatRoutineScheduleReceipt(receipt) : `Unknown routine schedule receipt: ${id}`);
111
+ return;
112
+ }
113
+
50
114
  const manager = ctx.ops.automationManager;
51
115
  if (!manager) {
52
116
  ctx.print('Automation manager is not available in this runtime.');
53
117
  return;
54
118
  }
55
- const sub = args[0];
56
119
 
57
120
  if (!sub || sub === 'list') {
58
121
  const jobs = manager.listJobs();
59
122
  if (jobs.length === 0) {
60
123
  ctx.print(
61
124
  'No automation jobs.\n'
62
- + 'Agent schedule commands are read-only here; create/run/enable/disable/remove are blocked until an Agent-safe route exists.'
125
+ + 'Local add/run/enable/disable/remove are blocked. Use /schedule promote-routine <routine> --cron <expr> --yes for an explicit external daemon schedule.'
63
126
  );
64
127
  return;
65
128
  }
@@ -84,7 +147,10 @@ export function registerScheduleRuntimeCommands(registry: CommandRegistry): void
84
147
  ctx.print(
85
148
  'Usage:\n'
86
149
  + ' /schedule list\n'
87
- + ' Agent schedule mutations and runs are blocked until an Agent-safe route exists.'
150
+ + ' /schedule receipts\n'
151
+ + ' /schedule receipt <receipt-id>\n'
152
+ + ' /schedule promote-routine <routine-id> (--cron <expr>|--every <interval>|--at <iso-time>) --yes\n'
153
+ + ' Local schedule mutations and runs remain blocked.'
88
154
  );
89
155
  },
90
156
  });
@@ -109,14 +109,14 @@ export const OPERATOR_CAPABILITY_BENCHMARKS: readonly OperatorCapabilityBenchmar
109
109
  {
110
110
  id: 'automation-schedules',
111
111
  title: 'Automation, Schedules, And Routines',
112
- posture: 'guarded',
112
+ posture: 'configurable',
113
113
  competitors: ['openclaw', 'hermes'],
114
114
  competitorBaseline: 'Cron/scheduler can create, pause, resume, run, remove, and deliver recurring tasks from natural language.',
115
- goodvibesAgent: 'Observes public automation/schedule routes and allows only explicitly confirmed side-effecting route calls; hidden model scheduling/spawn paths are blocked.',
116
- configure: ['/schedule list', '/routines create ...', 'goodvibes-agent status'],
117
- use: ['/schedule list', '/routines start <id>'],
118
- exceedsBy: ['No recursive hidden scheduler creation from model tools', 'explicit confirmation for side effects', 'local routines separate from daemon jobs'],
119
- next: ['Build Agent-first routine-to-daemon schedule promotion flow with preview, confirmation, delivery target selection, and audit trail.'],
115
+ goodvibesAgent: 'Observes public automation/schedule routes, keeps local routines separate from daemon jobs, and promotes a local routine into an external daemon schedules.create record only through an exact user command with --yes.',
116
+ configure: ['/schedule list', '/routines create ...', '/schedule promote-routine <id> --cron "0 8 * * *" --yes', 'goodvibes-agent routines promote <id> --every 1d --yes'],
117
+ use: ['/schedule list', '/routines start <id>', '/schedule promote-routine <id> --cron <expr> --yes'],
118
+ exceedsBy: ['No recursive hidden scheduler creation from model tools', 'explicit confirmation for side effects', 'local routines separate from daemon jobs', 'scheduled prompts preserve isolated Agent Knowledge and forbid default wiki/HomeGraph fallback'],
119
+ next: ['Add delivery target selection and live schedule recovery/status correlation for promoted routines.'],
120
120
  },
121
121
  {
122
122
  id: 'tool-gateway-mcp',
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.36';
9
+ let _version = '0.1.38';
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 {