@pellux/goodvibes-agent 0.1.37 → 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 CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  All notable changes to GoodVibes Agent will be recorded here.
4
4
 
5
+ ## 0.1.38 - 2026-05-31
6
+
7
+ - 072503c Add routine schedule promotion receipts
8
+ - 5bb9801 Update automation capability benchmark
9
+
5
10
  ## 0.1.37 - 2026-05-31
6
11
 
7
12
  - 656b6f4 Add Agent routine daemon schedule promotion
package/README.md CHANGED
@@ -69,11 +69,12 @@ Local Agent behavior is editable from the TUI:
69
69
  /routines create --name "Evening Review" --description "Review open work before shutdown" --steps "Check work plan, approvals, and Agent Knowledge status before summarizing." --enabled true
70
70
  /routines start evening-review
71
71
  /schedule promote-routine evening-review --cron "0 17 * * 1-5" --timezone America/Chicago --yes
72
+ /schedule receipts
72
73
  /agent-skills create --name "Morning Brief" --description "Daily briefing flow" --procedure "Check tasks, approvals, calendar, and unread state before summarizing." --enabled true
73
74
  /skills local list
74
75
  ```
75
76
 
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
+ Starting a routine records local usage and prints its steps; it does not spawn background agents or daemon automation jobs. Promotion to a daemon schedule is separate and explicit: it calls the public `schedules.create` route on the externally managed daemon only after `--yes`, records a redacted local receipt, and the generated scheduled prompt keeps Agent Knowledge isolated from default Knowledge/Wiki and HomeGraph.
77
78
 
78
79
  ## Daemon Prerequisite
79
80
 
@@ -76,7 +76,7 @@ Personas, routines, and reusable Agent skills are local to GoodVibes Agent. They
76
76
  /skills local list
77
77
  ```
78
78
 
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.
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`, writes a local redacted promotion receipt, and preserves the rule that Agent Knowledge never falls back to default Knowledge/Wiki or HomeGraph.
80
80
 
81
81
  ## External Daemon
82
82
 
@@ -53,7 +53,7 @@ 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 | Local routines can be explicitly promoted to external daemon `schedules.create` with `--yes`; hidden model scheduling and local scheduler spawns are blocked |
56
+ | Scheduling | Natural-language cron, run/pause/resume/edit/remove, delivery | Local routines can be explicitly promoted to external daemon `schedules.create` with `--yes`; redacted local promotion receipts are reviewable; 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 |
@@ -78,6 +78,7 @@ GoodVibes Agent should exceed OpenClaw/Hermes by making these properties true fr
78
78
  - Artifact and multimodal Agent Knowledge ingest affordances once Agent-specific routes are stable.
79
79
  - Visual starter-template editing inside the fullscreen Agent workspace after the command-guided authoring path.
80
80
  - Artifact and multimodal Agent Knowledge ingestion when the isolated Agent route accepts artifact-backed media.
81
+ - Live schedule recovery/status correlation for promoted routines.
81
82
  - Delegation receipts and artifact review inside the operator workspace.
82
83
  - Approval center with route risk labels and saved policy presets.
83
84
  - Intent-gated tool exposure so the model sees fewer irrelevant tools per turn while retaining broad capability coverage.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-agent",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
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",
@@ -1,9 +1,11 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
3
  import { createBrowserGoodVibesSdk } from '@pellux/goodvibes-sdk/browser';
4
4
  import type { OperatorMethodInput, OperatorMethodOutput } from '@pellux/goodvibes-sdk/contracts';
5
5
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
6
+ import type { ShellPathService } from '@/runtime/index.ts';
6
7
  import { getModelIdFromProviderModel, getProviderIdFromModel } from '../config/provider-model.ts';
8
+ import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
7
9
  import { SDK_VERSION } from '../version.ts';
8
10
  import type { AgentRoutineRecord } from './routine-registry.ts';
9
11
 
@@ -80,6 +82,47 @@ export type RoutineSchedulePromotionResult =
80
82
  | RoutineSchedulePromotionSuccess
81
83
  | RoutineSchedulePromotionFailure;
82
84
 
85
+ export interface RoutineScheduleReceipt {
86
+ readonly id: string;
87
+ readonly createdAt: string;
88
+ readonly routineId: string;
89
+ readonly routineName: string;
90
+ readonly route: typeof ROUTINE_SCHEDULE_ROUTE;
91
+ readonly method: typeof ROUTINE_SCHEDULE_METHOD;
92
+ readonly status: 'created' | 'failed';
93
+ readonly daemonBaseUrl: string;
94
+ readonly scheduleId?: string;
95
+ readonly scheduleStatus?: string;
96
+ readonly scheduleName: string;
97
+ readonly scheduleKind: RoutineScheduleKind;
98
+ readonly scheduleValue: string;
99
+ readonly timezone?: string;
100
+ readonly provider?: string;
101
+ readonly model?: string;
102
+ readonly enabled: boolean;
103
+ readonly target: {
104
+ readonly kind?: string;
105
+ readonly surfaceKind?: string;
106
+ readonly preserveThread?: boolean;
107
+ readonly createIfMissing?: boolean;
108
+ };
109
+ readonly deliveryMode?: string;
110
+ readonly failureKind?: RoutineSchedulePromotionFailure['kind'];
111
+ readonly failureError?: string;
112
+ }
113
+
114
+ export interface RoutineScheduleReceiptSnapshot {
115
+ readonly path: string;
116
+ readonly receipts: readonly RoutineScheduleReceipt[];
117
+ }
118
+
119
+ interface RoutineScheduleReceiptStoreFile {
120
+ readonly version: 1;
121
+ readonly receipts: readonly RoutineScheduleReceipt[];
122
+ }
123
+
124
+ const RECEIPT_STORE_VERSION = 1;
125
+
83
126
  function isRecord(value: unknown): value is Record<string, unknown> {
84
127
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
85
128
  }
@@ -89,6 +132,15 @@ function readString(record: Record<string, unknown>, key: string): string | null
89
132
  return typeof value === 'string' ? value : null;
90
133
  }
91
134
 
135
+ function readBoolean(record: Record<string, unknown>, key: string): boolean | undefined {
136
+ const value = record[key];
137
+ return typeof value === 'boolean' ? value : undefined;
138
+ }
139
+
140
+ function nowIso(): string {
141
+ return new Date().toISOString();
142
+ }
143
+
92
144
  function optionValue(args: readonly string[], index: number, inlineValue: string | undefined): {
93
145
  readonly value: string | undefined;
94
146
  readonly nextIndex: number;
@@ -111,6 +163,198 @@ function normalizeProviderModel(provider: string | undefined, model: string | un
111
163
  };
112
164
  }
113
165
 
166
+ function readReceipt(value: unknown): RoutineScheduleReceipt | null {
167
+ if (!isRecord(value)) return null;
168
+ const id = readString(value, 'id')?.trim();
169
+ const createdAt = readString(value, 'createdAt')?.trim();
170
+ const routineId = readString(value, 'routineId')?.trim();
171
+ const routineName = readString(value, 'routineName')?.trim();
172
+ const status = value.status === 'created' || value.status === 'failed' ? value.status : null;
173
+ const scheduleKind = value.scheduleKind === 'cron' || value.scheduleKind === 'every' || value.scheduleKind === 'at'
174
+ ? value.scheduleKind
175
+ : null;
176
+ const scheduleValue = readString(value, 'scheduleValue')?.trim();
177
+ const target = isRecord(value.target) ? value.target : {};
178
+ if (!id || !createdAt || !routineId || !routineName || !status || !scheduleKind || !scheduleValue) return null;
179
+ return {
180
+ id,
181
+ createdAt,
182
+ routineId,
183
+ routineName,
184
+ route: ROUTINE_SCHEDULE_ROUTE,
185
+ method: ROUTINE_SCHEDULE_METHOD,
186
+ status,
187
+ daemonBaseUrl: readString(value, 'daemonBaseUrl') ?? '',
188
+ scheduleId: readString(value, 'scheduleId') ?? undefined,
189
+ scheduleStatus: readString(value, 'scheduleStatus') ?? undefined,
190
+ scheduleName: readString(value, 'scheduleName') ?? routineName,
191
+ scheduleKind,
192
+ scheduleValue,
193
+ timezone: readString(value, 'timezone') ?? undefined,
194
+ provider: readString(value, 'provider') ?? undefined,
195
+ model: readString(value, 'model') ?? undefined,
196
+ enabled: value.enabled !== false,
197
+ target: {
198
+ kind: readString(target, 'kind') ?? undefined,
199
+ surfaceKind: readString(target, 'surfaceKind') ?? undefined,
200
+ preserveThread: readBoolean(target, 'preserveThread'),
201
+ createIfMissing: readBoolean(target, 'createIfMissing'),
202
+ },
203
+ deliveryMode: readString(value, 'deliveryMode') ?? undefined,
204
+ failureKind: value.failureKind === 'confirmation_required'
205
+ || value.failureKind === 'auth_required'
206
+ || value.failureKind === 'daemon_unavailable'
207
+ || value.failureKind === 'version_mismatch'
208
+ || value.failureKind === 'daemon_route_unavailable'
209
+ || value.failureKind === 'daemon_error'
210
+ ? value.failureKind
211
+ : undefined,
212
+ failureError: readString(value, 'failureError') ?? undefined,
213
+ };
214
+ }
215
+
216
+ function parseReceiptStore(raw: string): RoutineScheduleReceiptStoreFile {
217
+ const parsed: unknown = JSON.parse(raw);
218
+ if (!isRecord(parsed)) return { version: RECEIPT_STORE_VERSION, receipts: [] };
219
+ return {
220
+ version: RECEIPT_STORE_VERSION,
221
+ receipts: Array.isArray(parsed.receipts)
222
+ ? parsed.receipts.map(readReceipt).filter((receipt): receipt is RoutineScheduleReceipt => receipt !== null)
223
+ : [],
224
+ };
225
+ }
226
+
227
+ function formatReceiptStore(store: RoutineScheduleReceiptStoreFile): string {
228
+ return `${JSON.stringify(store, null, 2)}\n`;
229
+ }
230
+
231
+ function receiptId(createdAt: string, routineId: string, existing: readonly RoutineScheduleReceipt[]): string {
232
+ const dayStamp = createdAt.slice(0, 10).replace(/-/g, '');
233
+ const base = `routine-schedule-${routineId}-${dayStamp}`.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
234
+ const ids = new Set(existing.map((receipt) => receipt.id));
235
+ if (!ids.has(base)) return base;
236
+ for (let index = 2; index < 1000; index += 1) {
237
+ const candidate = `${base}-${index}`;
238
+ if (!ids.has(candidate)) return candidate;
239
+ }
240
+ return `${base}-${existing.length + 1}`;
241
+ }
242
+
243
+ function scheduleValue(payload: ScheduleCreateInput): string {
244
+ if (payload.kind === 'cron') return String(payload.cron ?? '');
245
+ if (payload.kind === 'every') return String(payload.every ?? '');
246
+ return String(payload.at ?? '');
247
+ }
248
+
249
+ function scheduleKind(payload: ScheduleCreateInput): RoutineScheduleKind {
250
+ if (payload.kind === 'cron' || payload.kind === 'every' || payload.kind === 'at') return payload.kind;
251
+ throw new Error('Routine schedule payload is missing a schedule kind.');
252
+ }
253
+
254
+ function targetSummary(payload: ScheduleCreateInput): RoutineScheduleReceipt['target'] {
255
+ return isRecord(payload.target)
256
+ ? {
257
+ kind: typeof payload.target.kind === 'string' ? payload.target.kind : undefined,
258
+ surfaceKind: typeof payload.target.surfaceKind === 'string' ? payload.target.surfaceKind : undefined,
259
+ preserveThread: typeof payload.target.preserveThread === 'boolean' ? payload.target.preserveThread : undefined,
260
+ createIfMissing: typeof payload.target.createIfMissing === 'boolean' ? payload.target.createIfMissing : undefined,
261
+ }
262
+ : {};
263
+ }
264
+
265
+ function deliveryMode(payload: ScheduleCreateInput): string | undefined {
266
+ return isRecord(payload.delivery) && typeof payload.delivery.mode === 'string' ? payload.delivery.mode : undefined;
267
+ }
268
+
269
+ function resultScheduleRecord(result: RoutineSchedulePromotionResult): Record<string, unknown> {
270
+ return result.ok && isRecord(result.schedule) ? result.schedule : {};
271
+ }
272
+
273
+ function buildReceipt(
274
+ existing: readonly RoutineScheduleReceipt[],
275
+ connection: AgentDaemonConnection,
276
+ preview: RoutineSchedulePromotionPreview,
277
+ result: RoutineSchedulePromotionResult,
278
+ ): RoutineScheduleReceipt {
279
+ const createdAt = nowIso();
280
+ const kind = scheduleKind(preview.payload);
281
+ const schedule = resultScheduleRecord(result);
282
+ const scheduleId = readString(schedule, 'id') ?? undefined;
283
+ const scheduleStatus = readString(schedule, 'status') ?? (schedule.enabled === false ? 'paused' : schedule.enabled === true ? 'enabled' : undefined);
284
+ return {
285
+ id: receiptId(createdAt, preview.routineId, existing),
286
+ createdAt,
287
+ routineId: preview.routineId,
288
+ routineName: preview.routineName,
289
+ route: ROUTINE_SCHEDULE_ROUTE,
290
+ method: ROUTINE_SCHEDULE_METHOD,
291
+ status: result.ok ? 'created' : 'failed',
292
+ daemonBaseUrl: connection.baseUrl,
293
+ scheduleId,
294
+ scheduleStatus,
295
+ scheduleName: String(preview.payload.name ?? `Agent routine: ${preview.routineName}`),
296
+ scheduleKind: kind,
297
+ scheduleValue: scheduleValue(preview.payload),
298
+ timezone: kind === 'cron' ? preview.payload.timezone : undefined,
299
+ provider: preview.payload.provider,
300
+ model: preview.payload.model,
301
+ enabled: preview.payload.enabled !== false,
302
+ target: targetSummary(preview.payload),
303
+ deliveryMode: deliveryMode(preview.payload),
304
+ failureKind: result.ok ? undefined : result.kind,
305
+ failureError: result.ok ? undefined : result.error,
306
+ };
307
+ }
308
+
309
+ export function routineScheduleReceiptStorePath(shellPaths: ShellPathService): string {
310
+ return shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'routines', 'schedule-receipts.json');
311
+ }
312
+
313
+ export class RoutineScheduleReceiptStore {
314
+ public constructor(private readonly storePath: string) {}
315
+
316
+ public static fromShellPaths(shellPaths: ShellPathService): RoutineScheduleReceiptStore {
317
+ return new RoutineScheduleReceiptStore(routineScheduleReceiptStorePath(shellPaths));
318
+ }
319
+
320
+ public snapshot(): RoutineScheduleReceiptSnapshot {
321
+ const store = this.readStore();
322
+ return {
323
+ path: this.storePath,
324
+ receipts: [...store.receipts].sort((left, right) => right.createdAt.localeCompare(left.createdAt)),
325
+ };
326
+ }
327
+
328
+ public get(id: string): RoutineScheduleReceipt | null {
329
+ const normalized = id.trim().toLowerCase();
330
+ if (!normalized) return null;
331
+ return this.snapshot().receipts.find((receipt) => receipt.id.toLowerCase() === normalized) ?? null;
332
+ }
333
+
334
+ public append(connection: AgentDaemonConnection, preview: RoutineSchedulePromotionPreview, result: RoutineSchedulePromotionResult): RoutineScheduleReceipt {
335
+ const store = this.readStore();
336
+ const receipt = buildReceipt(store.receipts, connection, preview, result);
337
+ this.writeStore({ ...store, receipts: [...store.receipts, receipt] });
338
+ return receipt;
339
+ }
340
+
341
+ private readStore(): RoutineScheduleReceiptStoreFile {
342
+ if (!existsSync(this.storePath)) return { version: RECEIPT_STORE_VERSION, receipts: [] };
343
+ try {
344
+ return parseReceiptStore(readFileSync(this.storePath, 'utf-8'));
345
+ } catch (error) {
346
+ throw new Error(`Could not read Agent routine schedule receipt store: ${summarizeError(error)}`);
347
+ }
348
+ }
349
+
350
+ private writeStore(store: RoutineScheduleReceiptStoreFile): void {
351
+ mkdirSync(dirname(this.storePath), { recursive: true });
352
+ const tmpPath = `${this.storePath}.tmp`;
353
+ writeFileSync(tmpPath, formatReceiptStore(store), 'utf-8');
354
+ renameSync(tmpPath, this.storePath);
355
+ }
356
+ }
357
+
114
358
  export function parseRoutineSchedulePromotionArgs(args: readonly string[]): ParsedRoutineSchedulePromotionArgs {
115
359
  let routineId: string | null = null;
116
360
  let schedule: RoutineScheduleSpec | null = null;
@@ -412,6 +656,49 @@ export function formatRoutineScheduleSuccess(result: RoutineSchedulePromotionSuc
412
656
  ].join('\n');
413
657
  }
414
658
 
659
+ export function formatRoutineScheduleReceipts(snapshot: RoutineScheduleReceiptSnapshot, limit = 10): string {
660
+ const receipts = snapshot.receipts.slice(0, Math.max(1, limit));
661
+ if (snapshot.receipts.length === 0) {
662
+ return [
663
+ 'Agent routine schedule receipts',
664
+ ` store: ${snapshot.path}`,
665
+ ' No routine schedule promotions have been recorded yet.',
666
+ ' Create one with /schedule promote-routine <routine-id> --cron <expr> --yes.',
667
+ ].join('\n');
668
+ }
669
+ return [
670
+ `Agent routine schedule receipts (${snapshot.receipts.length})`,
671
+ ` store: ${snapshot.path}`,
672
+ ...receipts.map((receipt) => {
673
+ const schedule = receipt.scheduleId ? ` schedule=${receipt.scheduleId}` : '';
674
+ const failure = receipt.status === 'failed' && receipt.failureKind ? ` failure=${receipt.failureKind}` : '';
675
+ return ` ${receipt.id} ${receipt.status} ${receipt.scheduleKind} ${receipt.scheduleValue} routine=${receipt.routineId}${schedule}${failure}`;
676
+ }),
677
+ snapshot.receipts.length > receipts.length ? ` ...${snapshot.receipts.length - receipts.length} more` : '',
678
+ ].filter((line): line is string => Boolean(line)).join('\n');
679
+ }
680
+
681
+ export function formatRoutineScheduleReceipt(receipt: RoutineScheduleReceipt): string {
682
+ return [
683
+ `Agent routine schedule receipt ${receipt.id}`,
684
+ ` created: ${receipt.createdAt}`,
685
+ ` status: ${receipt.status}`,
686
+ ` routine: ${receipt.routineName} (${receipt.routineId})`,
687
+ ` route: ${receipt.method} ${receipt.route}`,
688
+ ` daemon: ${receipt.daemonBaseUrl}`,
689
+ ` schedule: ${receipt.scheduleName}${receipt.scheduleId ? ` (${receipt.scheduleId})` : ''}`,
690
+ receipt.scheduleStatus ? ` schedule status: ${receipt.scheduleStatus}` : '',
691
+ ` cadence: ${receipt.scheduleKind} ${receipt.scheduleValue}${receipt.timezone ? ` [${receipt.timezone}]` : ''}`,
692
+ ` enabled: ${receipt.enabled ? 'yes' : 'no'}`,
693
+ receipt.provider ? ` provider: ${receipt.provider}` : '',
694
+ receipt.model ? ` model: ${receipt.model}` : '',
695
+ ` target: ${receipt.target.kind ?? 'unknown'}${receipt.target.surfaceKind ? `/${receipt.target.surfaceKind}` : ''}`,
696
+ receipt.deliveryMode ? ` delivery: ${receipt.deliveryMode}` : '',
697
+ receipt.failureKind ? ` failure: ${receipt.failureKind}` : '',
698
+ receipt.failureError ? ` error: ${receipt.failureError}` : '',
699
+ ].filter((line): line is string => Boolean(line)).join('\n');
700
+ }
701
+
415
702
  export function formatRoutineScheduleFailure(failure: RoutineSchedulePromotionFailure): string {
416
703
  return [
417
704
  `Daemon schedule error: ${failure.kind}`,
package/src/cli/help.ts CHANGED
@@ -165,12 +165,15 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
165
165
  'routines list',
166
166
  'routines enabled',
167
167
  'routines show <id>',
168
+ 'routines receipts',
169
+ 'routines receipt <receipt-id>',
168
170
  'routines promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) [--timezone <tz>] [--name <schedule-name>] [--provider <id>] [--model <model>] [--disabled] --yes',
169
171
  ],
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.',
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.',
171
173
  examples: [
172
174
  'routines list',
173
175
  'routines show daily-operations-sweep',
176
+ 'routines receipts',
174
177
  'routines promote daily-operations-sweep --cron "0 9 * * *" --timezone America/Chicago --yes',
175
178
  'routines promote weekly-review --every 7d --disabled',
176
179
  ],
@@ -2,12 +2,15 @@ import { createShellPathService } from '@/runtime/index.ts';
2
2
  import { AgentRoutineRegistry, type AgentRoutineRecord } from '../agent/routine-registry.ts';
3
3
  import {
4
4
  buildRoutineSchedulePreview,
5
+ formatRoutineScheduleReceipt,
6
+ formatRoutineScheduleReceipts,
5
7
  formatRoutineScheduleFailure,
6
8
  formatRoutineSchedulePreview,
7
9
  formatRoutineScheduleSuccess,
8
10
  parseRoutineSchedulePromotionArgs,
9
11
  promoteRoutineToDaemonSchedule,
10
12
  resolveAgentDaemonConnection,
13
+ RoutineScheduleReceiptStore,
11
14
  } from '../agent/routine-schedule-promotion.ts';
12
15
  import type { CliCommandOutput } from './types.ts';
13
16
  import type { CliCommandRuntime } from './management.ts';
@@ -35,6 +38,13 @@ function routineRegistry(runtime: CliCommandRuntime): AgentRoutineRegistry {
35
38
  }));
36
39
  }
37
40
 
41
+ function routineReceiptStore(runtime: CliCommandRuntime): RoutineScheduleReceiptStore {
42
+ return RoutineScheduleReceiptStore.fromShellPaths(createShellPathService({
43
+ workingDirectory: runtime.workingDirectory,
44
+ homeDirectory: runtime.homeDirectory,
45
+ }));
46
+ }
47
+
38
48
  function summarizeRoutine(routine: AgentRoutineRecord): string {
39
49
  const enabled = routine.enabled ? 'enabled' : 'disabled';
40
50
  const tags = routine.tags.length > 0 ? ` tags=${routine.tags.join(',')}` : '';
@@ -121,14 +131,16 @@ async function handleRoutinePromotion(runtime: CliCommandRuntime, args: readonly
121
131
  }
122
132
  const connection = resolveAgentDaemonConnection(runtime.configManager, runtime.homeDirectory);
123
133
  const result = await promoteRoutineToDaemonSchedule(connection, preview);
134
+ const receipt = routineReceiptStore(runtime).append(connection, preview, result);
124
135
  if (!result.ok) {
125
136
  return {
126
- output: json ? JSON.stringify(result, null, 2) : formatRoutineScheduleFailure(result),
137
+ output: json ? JSON.stringify({ ...result, receipt }, null, 2) : `${formatRoutineScheduleFailure(result)}\n receipt: ${receipt.id}`,
127
138
  exitCode: 1,
128
139
  };
129
140
  }
141
+ const value = { ...result, receipt };
130
142
  return {
131
- output: jsonOrText(runtime, result, formatRoutineScheduleSuccess(result)),
143
+ output: jsonOrText(runtime, value, `${formatRoutineScheduleSuccess(result)}\n receipt: ${receipt.id}`),
132
144
  exitCode: 0,
133
145
  };
134
146
  }
@@ -175,11 +187,44 @@ export async function handleRoutinesCommand(runtime: CliCommandRuntime): Promise
175
187
  exitCode: 0,
176
188
  };
177
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
+ }
178
223
  if (normalized === 'promote' || normalized === 'schedule' || normalized === 'promote-schedule') {
179
224
  return handleRoutinePromotion(runtime, rest);
180
225
  }
181
226
  return {
182
- output: 'Usage: goodvibes-agent routines [list|enabled|show <id>|promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) --yes]',
227
+ output: 'Usage: goodvibes-agent routines [list|enabled|show <id>|receipts|receipt <id>|promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) --yes]',
183
228
  exitCode: 2,
184
229
  };
185
230
  }
@@ -573,10 +573,11 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
573
573
  group: 'WATCH',
574
574
  label: 'Automation',
575
575
  summary: 'Automation and schedule observability with explicit routine promotion.',
576
- detail: 'Agent does not create local automation jobs or hidden scheduler spawns. Reviewed local routines can be promoted into externally owned daemon schedules only through an explicit schedules.create command with --yes.',
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
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' },
580
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' },
581
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' },
582
583
  ],
@@ -1,12 +1,15 @@
1
1
  import { AgentRoutineRegistry, type AgentRoutineRecord } from '../../agent/routine-registry.ts';
2
2
  import {
3
3
  buildRoutineSchedulePreview,
4
+ formatRoutineScheduleReceipt,
5
+ formatRoutineScheduleReceipts,
4
6
  formatRoutineScheduleFailure,
5
7
  formatRoutineSchedulePreview,
6
8
  formatRoutineScheduleSuccess,
7
9
  parseRoutineSchedulePromotionArgs,
8
10
  promoteRoutineToDaemonSchedule,
9
11
  resolveAgentDaemonConnection,
12
+ RoutineScheduleReceiptStore,
10
13
  } from '../../agent/routine-schedule-promotion.ts';
11
14
  import type { CommandContext, CommandRegistry } from '../command-registry.ts';
12
15
  import { requireShellPaths } from './runtime-services.ts';
@@ -52,6 +55,10 @@ function registryFromContext(ctx: CommandContext): AgentRoutineRegistry {
52
55
  return AgentRoutineRegistry.fromShellPaths(requireShellPaths(ctx));
53
56
  }
54
57
 
58
+ function receiptStoreFromContext(ctx: CommandContext): RoutineScheduleReceiptStore {
59
+ return RoutineScheduleReceiptStore.fromShellPaths(requireShellPaths(ctx));
60
+ }
61
+
55
62
  function requiredFlag(flags: ReadonlyMap<string, string>, key: string): string {
56
63
  const value = flags.get(key)?.trim();
57
64
  if (!value) throw new Error(`Missing --${key}.`);
@@ -124,7 +131,8 @@ async function promoteRoutine(args: readonly string[], routineRegistry: AgentRou
124
131
  const shellPaths = requireShellPaths(ctx);
125
132
  const connection = resolveAgentDaemonConnection(ctx.platform.configManager, shellPaths.homeDirectory);
126
133
  const result = await promoteRoutineToDaemonSchedule(connection, preview);
127
- ctx.print(result.ok ? formatRoutineScheduleSuccess(result) : formatRoutineScheduleFailure(result));
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}`);
128
136
  }
129
137
 
130
138
  export async function runRoutinesRuntimeCommand(args: readonly string[], ctx: CommandContext): Promise<void> {
@@ -155,6 +163,20 @@ export async function runRoutinesRuntimeCommand(args: readonly string[], ctx: Co
155
163
  ctx.print(routine ? renderRoutine(routine) : `Unknown Agent routine: ${id}`);
156
164
  return;
157
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
+ }
158
180
  if (sub === 'create') {
159
181
  const parsed = parseRoutineArgs(args.slice(1));
160
182
  const steps = parsed.flags.get('steps')?.trim() || parsed.rest.join(' ').trim();
@@ -253,7 +275,7 @@ export async function runRoutinesRuntimeCommand(args: readonly string[], ctx: Co
253
275
  ctx.print(`Deleted Agent routine ${removed.id}: ${removed.name}`);
254
276
  return;
255
277
  }
256
- ctx.print('Usage: /routines [list|enabled|search|show|create|update|enable|disable|start|review|stale|promote|delete]');
278
+ ctx.print('Usage: /routines [list|enabled|search|show|receipts|receipt|create|update|enable|disable|start|review|stale|promote|delete]');
257
279
  } catch (error) {
258
280
  printError(ctx, error);
259
281
  }
@@ -7,12 +7,15 @@ import type { AutomationScheduleDefinition } from '@pellux/goodvibes-sdk/platfor
7
7
  import { AgentRoutineRegistry } from '../../agent/routine-registry.ts';
8
8
  import {
9
9
  buildRoutineSchedulePreview,
10
+ formatRoutineScheduleReceipt,
11
+ formatRoutineScheduleReceipts,
10
12
  formatRoutineScheduleFailure,
11
13
  formatRoutineSchedulePreview,
12
14
  formatRoutineScheduleSuccess,
13
15
  parseRoutineSchedulePromotionArgs,
14
16
  promoteRoutineToDaemonSchedule,
15
17
  resolveAgentDaemonConnection,
18
+ RoutineScheduleReceiptStore,
16
19
  } from '../../agent/routine-schedule-promotion.ts';
17
20
  import type { CommandContext } from '../command-registry.ts';
18
21
  import { requireShellPaths } from './runtime-services.ts';
@@ -73,7 +76,8 @@ async function promoteRoutineSchedule(args: readonly string[], ctx: CommandConte
73
76
  }
74
77
  const connection = resolveAgentDaemonConnection(ctx.platform.configManager, shellPaths.homeDirectory);
75
78
  const result = await promoteRoutineToDaemonSchedule(connection, preview);
76
- ctx.print(result.ok ? formatRoutineScheduleSuccess(result) : formatRoutineScheduleFailure(result));
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}`);
77
81
  }
78
82
 
79
83
  export function registerScheduleRuntimeCommands(registry: CommandRegistry): void {
@@ -81,8 +85,8 @@ export function registerScheduleRuntimeCommands(registry: CommandRegistry): void
81
85
  name: 'schedule',
82
86
  aliases: ['sched'],
83
87
  description: 'Inspect schedules and explicitly promote local Agent routines to daemon schedules',
84
- usage: 'list | promote-routine <routine-id> --cron <expr> --yes',
85
- argsHint: 'list | promote-routine <routine-id> --cron <expr> --yes',
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',
86
90
  async handler(args, ctx) {
87
91
  const sub = args[0];
88
92
 
@@ -91,6 +95,22 @@ export function registerScheduleRuntimeCommands(registry: CommandRegistry): void
91
95
  return;
92
96
  }
93
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
+
94
114
  const manager = ctx.ops.automationManager;
95
115
  if (!manager) {
96
116
  ctx.print('Automation manager is not available in this runtime.');
@@ -127,6 +147,8 @@ export function registerScheduleRuntimeCommands(registry: CommandRegistry): void
127
147
  ctx.print(
128
148
  'Usage:\n'
129
149
  + ' /schedule list\n'
150
+ + ' /schedule receipts\n'
151
+ + ' /schedule receipt <receipt-id>\n'
130
152
  + ' /schedule promote-routine <routine-id> (--cron <expr>|--every <interval>|--at <iso-time>) --yes\n'
131
153
  + ' Local schedule mutations and runs remain blocked.'
132
154
  );
@@ -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.37';
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 {