@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.
- package/CHANGELOG.md +9 -0
- package/README.md +4 -0
- package/docs/getting-started.md +2 -1
- package/docs/operator-capability-benchmark.md +3 -1
- package/package.json +1 -1
- package/src/agent/routine-schedule-promotion.ts +721 -0
- package/src/cli/completion.ts +1 -0
- package/src/cli/help.ts +20 -0
- package/src/cli/management.ts +6 -0
- package/src/cli/parser.ts +2 -0
- package/src/cli/routines-command.ts +230 -0
- package/src/cli/types.ts +1 -0
- package/src/input/agent-workspace.ts +5 -3
- package/src/input/commands/routines-runtime.ts +62 -2
- package/src/input/commands/schedule-runtime.ts +74 -8
- package/src/operator/capability-benchmark.ts +6 -6
- package/src/version.ts +1 -1
package/src/cli/completion.ts
CHANGED
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.',
|
package/src/cli/management.ts
CHANGED
|
@@ -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
|
@@ -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: '
|
|
576
|
-
detail: 'Agent does not create
|
|
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-
|
|
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
|
-
'
|
|
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
|
|
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
|
-
+ '
|
|
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
|
-
+ '
|
|
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: '
|
|
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
|
|
116
|
-
configure: ['/schedule list', '/routines create ...', 'goodvibes-agent
|
|
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: ['
|
|
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.
|
|
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 {
|