@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 +8 -0
- package/README.md +5 -0
- package/docs/README.md +1 -1
- package/docs/getting-started.md +4 -1
- package/docs/operator-capability-benchmark.md +4 -3
- package/package.json +1 -1
- package/src/agent/routine-schedule-promotion.ts +434 -0
- package/src/agent/runtime-profile.ts +4 -0
- package/src/cli/completion.ts +1 -0
- package/src/cli/help.ts +17 -0
- package/src/cli/management.ts +6 -0
- package/src/cli/parser.ts +2 -0
- package/src/cli/routines-command.ts +185 -0
- package/src/cli/types.ts +1 -0
- package/src/input/agent-workspace.ts +20 -6
- package/src/input/commands/agent-runtime-profile-runtime.ts +223 -0
- package/src/input/commands/routines-runtime.ts +40 -2
- package/src/input/commands/schedule-runtime.ts +52 -8
- package/src/input/commands.ts +2 -0
- package/src/operator/capability-benchmark.ts +5 -5
- package/src/renderer/agent-workspace.ts +2 -1
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GoodVibes Agent will be recorded here.
|
|
4
4
|
|
|
5
|
+
## 0.1.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`
|
|
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.
|
package/docs/getting-started.md
CHANGED
|
@@ -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 |
|
|
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;
|
|
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
|
-
-
|
|
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.
|
|
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,
|
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,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.',
|
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',
|