@pellux/goodvibes-agent 0.1.38 → 0.1.39
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 +4 -0
- package/README.md +2 -1
- package/docs/getting-started.md +2 -0
- package/docs/operator-capability-benchmark.md +1 -1
- package/package.json +1 -1
- package/src/agent/routine-schedule-promotion.ts +297 -0
- package/src/cli/help.ts +3 -1
- package/src/cli/routines-command.ts +14 -1
- package/src/input/agent-workspace.ts +2 -1
- package/src/input/commands/routines-runtime.ts +11 -2
- package/src/input/commands/schedule-runtime.ts +13 -2
- package/src/operator/capability-benchmark.ts +1 -1
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -70,11 +70,12 @@ Local Agent behavior is editable from the TUI:
|
|
|
70
70
|
/routines start evening-review
|
|
71
71
|
/schedule promote-routine evening-review --cron "0 17 * * 1-5" --timezone America/Chicago --yes
|
|
72
72
|
/schedule receipts
|
|
73
|
+
/schedule reconcile
|
|
73
74
|
/agent-skills create --name "Morning Brief" --description "Daily briefing flow" --procedure "Check tasks, approvals, calendar, and unread state before summarizing." --enabled true
|
|
74
75
|
/skills local list
|
|
75
76
|
```
|
|
76
77
|
|
|
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.
|
|
78
|
+
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. Use `/schedule reconcile` to compare those local receipts against live externally owned daemon schedules through public `schedules.list`.
|
|
78
79
|
|
|
79
80
|
## Daemon Prerequisite
|
|
80
81
|
|
package/docs/getting-started.md
CHANGED
|
@@ -39,6 +39,8 @@ Once the TUI opens, run `/agent`, `/home`, or `/operator` to open the Agent oper
|
|
|
39
39
|
|
|
40
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
41
|
|
|
42
|
+
Use `/schedule receipts` to review redacted local routine promotion history and `/schedule reconcile` to compare those receipts with live externally owned daemon schedules through public `schedules.list`.
|
|
43
|
+
|
|
42
44
|
## Isolated Agent Profiles
|
|
43
45
|
|
|
44
46
|
Use a separate Agent home when you want isolated local state:
|
|
@@ -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`; redacted local promotion receipts are reviewable
|
|
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 and can be reconciled with live `schedules.list`; 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 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.39",
|
|
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",
|
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from '
|
|
|
2
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
|
+
import { formatEveryInterval } from '@pellux/goodvibes-sdk/platform/automation';
|
|
5
6
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
6
7
|
import type { ShellPathService } from '@/runtime/index.ts';
|
|
7
8
|
import { getModelIdFromProviderModel, getProviderIdFromModel } from '../config/provider-model.ts';
|
|
@@ -11,9 +12,11 @@ import type { AgentRoutineRecord } from './routine-registry.ts';
|
|
|
11
12
|
|
|
12
13
|
export const ROUTINE_SCHEDULE_ROUTE = '/api/automation/schedules';
|
|
13
14
|
export const ROUTINE_SCHEDULE_METHOD = 'schedules.create';
|
|
15
|
+
export const ROUTINE_SCHEDULE_LIST_METHOD = 'schedules.list';
|
|
14
16
|
|
|
15
17
|
type ScheduleCreateInput = OperatorMethodInput<'schedules.create'>;
|
|
16
18
|
type ScheduleCreateOutput = OperatorMethodOutput<'schedules.create'>;
|
|
19
|
+
type ScheduleListOutput = OperatorMethodOutput<'schedules.list'>;
|
|
17
20
|
|
|
18
21
|
export interface AgentDaemonConfigReader {
|
|
19
22
|
get(key: string): unknown;
|
|
@@ -116,6 +119,57 @@ export interface RoutineScheduleReceiptSnapshot {
|
|
|
116
119
|
readonly receipts: readonly RoutineScheduleReceipt[];
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
export interface RoutineScheduleLiveRecord {
|
|
123
|
+
readonly id: string;
|
|
124
|
+
readonly name: string;
|
|
125
|
+
readonly status?: string;
|
|
126
|
+
readonly enabled?: boolean;
|
|
127
|
+
readonly scheduleKind?: RoutineScheduleKind;
|
|
128
|
+
readonly scheduleValue?: string;
|
|
129
|
+
readonly timezone?: string;
|
|
130
|
+
readonly nextRunAt?: number;
|
|
131
|
+
readonly lastRunAt?: number;
|
|
132
|
+
readonly runCount?: number;
|
|
133
|
+
readonly successCount?: number;
|
|
134
|
+
readonly failureCount?: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface RoutineScheduleCorrelation {
|
|
138
|
+
readonly receipt: RoutineScheduleReceipt;
|
|
139
|
+
readonly liveStatus: 'matched' | 'missing' | 'failed-receipt';
|
|
140
|
+
readonly matchReason: 'schedule-id' | 'name-and-cadence' | 'failed-receipt' | 'not-found';
|
|
141
|
+
readonly schedule?: RoutineScheduleLiveRecord;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface RoutineScheduleCorrelationSuccess {
|
|
145
|
+
readonly ok: true;
|
|
146
|
+
readonly kind: typeof ROUTINE_SCHEDULE_LIST_METHOD;
|
|
147
|
+
readonly route: typeof ROUTINE_SCHEDULE_ROUTE;
|
|
148
|
+
readonly baseUrl: string;
|
|
149
|
+
readonly scheduleCount: number;
|
|
150
|
+
readonly receiptCount: number;
|
|
151
|
+
readonly correlations: readonly RoutineScheduleCorrelation[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface RoutineScheduleCorrelationFailure {
|
|
155
|
+
readonly ok: false;
|
|
156
|
+
readonly kind:
|
|
157
|
+
| 'auth_required'
|
|
158
|
+
| 'daemon_unavailable'
|
|
159
|
+
| 'version_mismatch'
|
|
160
|
+
| 'daemon_route_unavailable'
|
|
161
|
+
| 'daemon_error';
|
|
162
|
+
readonly error: string;
|
|
163
|
+
readonly route: typeof ROUTINE_SCHEDULE_ROUTE;
|
|
164
|
+
readonly baseUrl?: string;
|
|
165
|
+
readonly daemonVersion?: string;
|
|
166
|
+
readonly expectedSdkVersion?: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type RoutineScheduleCorrelationResult =
|
|
170
|
+
| RoutineScheduleCorrelationSuccess
|
|
171
|
+
| RoutineScheduleCorrelationFailure;
|
|
172
|
+
|
|
119
173
|
interface RoutineScheduleReceiptStoreFile {
|
|
120
174
|
readonly version: 1;
|
|
121
175
|
readonly receipts: readonly RoutineScheduleReceipt[];
|
|
@@ -137,6 +191,11 @@ function readBoolean(record: Record<string, unknown>, key: string): boolean | un
|
|
|
137
191
|
return typeof value === 'boolean' ? value : undefined;
|
|
138
192
|
}
|
|
139
193
|
|
|
194
|
+
function readNumber(record: Record<string, unknown>, key: string): number | undefined {
|
|
195
|
+
const value = record[key];
|
|
196
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
140
199
|
function nowIso(): string {
|
|
141
200
|
return new Date().toISOString();
|
|
142
201
|
}
|
|
@@ -270,6 +329,128 @@ function resultScheduleRecord(result: RoutineSchedulePromotionResult): Record<st
|
|
|
270
329
|
return result.ok && isRecord(result.schedule) ? result.schedule : {};
|
|
271
330
|
}
|
|
272
331
|
|
|
332
|
+
function normalizeForMatch(value: string | undefined): string {
|
|
333
|
+
return (value ?? '').trim().toLowerCase();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function isoTime(value: string): string | null {
|
|
337
|
+
const time = new Date(value).getTime();
|
|
338
|
+
return Number.isFinite(time) ? new Date(time).toISOString() : null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function readLiveScheduleDefinition(value: unknown): {
|
|
342
|
+
readonly kind?: RoutineScheduleKind;
|
|
343
|
+
readonly value?: string;
|
|
344
|
+
readonly timezone?: string;
|
|
345
|
+
} {
|
|
346
|
+
if (!isRecord(value)) return {};
|
|
347
|
+
if (value.kind === 'cron') {
|
|
348
|
+
return {
|
|
349
|
+
kind: 'cron',
|
|
350
|
+
value: readString(value, 'expression') ?? undefined,
|
|
351
|
+
timezone: readString(value, 'timezone') ?? undefined,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
if (value.kind === 'every') {
|
|
355
|
+
const intervalMs = readNumber(value, 'intervalMs');
|
|
356
|
+
return {
|
|
357
|
+
kind: 'every',
|
|
358
|
+
value: intervalMs === undefined ? undefined : formatEveryInterval(intervalMs),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
if (value.kind === 'at') {
|
|
362
|
+
const at = readNumber(value, 'at');
|
|
363
|
+
return {
|
|
364
|
+
kind: 'at',
|
|
365
|
+
value: at === undefined ? undefined : new Date(at).toISOString(),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
return {};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function readLiveScheduleRecord(value: unknown): RoutineScheduleLiveRecord | null {
|
|
372
|
+
if (!isRecord(value)) return null;
|
|
373
|
+
const id = readString(value, 'id')?.trim();
|
|
374
|
+
const name = readString(value, 'name')?.trim();
|
|
375
|
+
if (!id || !name) return null;
|
|
376
|
+
const schedule = readLiveScheduleDefinition(value.schedule);
|
|
377
|
+
return {
|
|
378
|
+
id,
|
|
379
|
+
name,
|
|
380
|
+
status: readString(value, 'status') ?? undefined,
|
|
381
|
+
enabled: readBoolean(value, 'enabled'),
|
|
382
|
+
scheduleKind: schedule.kind,
|
|
383
|
+
scheduleValue: schedule.value,
|
|
384
|
+
timezone: schedule.timezone,
|
|
385
|
+
nextRunAt: readNumber(value, 'nextRunAt'),
|
|
386
|
+
lastRunAt: readNumber(value, 'lastRunAt'),
|
|
387
|
+
runCount: readNumber(value, 'runCount'),
|
|
388
|
+
successCount: readNumber(value, 'successCount'),
|
|
389
|
+
failureCount: readNumber(value, 'failureCount'),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function readLiveSchedules(output: ScheduleListOutput): readonly RoutineScheduleLiveRecord[] {
|
|
394
|
+
const record: Record<string, unknown> = isRecord(output) ? output : {};
|
|
395
|
+
const jobs: readonly unknown[] = Array.isArray(record.jobs) ? record.jobs : [];
|
|
396
|
+
return jobs.map(readLiveScheduleRecord).filter((schedule): schedule is RoutineScheduleLiveRecord => schedule !== null);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function cadenceMatches(receipt: RoutineScheduleReceipt, schedule: RoutineScheduleLiveRecord): boolean {
|
|
400
|
+
if (receipt.scheduleKind !== schedule.scheduleKind) return false;
|
|
401
|
+
if (receipt.scheduleKind === 'at') {
|
|
402
|
+
const left = isoTime(receipt.scheduleValue);
|
|
403
|
+
const right = schedule.scheduleValue ? isoTime(schedule.scheduleValue) : null;
|
|
404
|
+
return Boolean(left && right && left === right);
|
|
405
|
+
}
|
|
406
|
+
return normalizeForMatch(receipt.scheduleValue) === normalizeForMatch(schedule.scheduleValue);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function findScheduleForReceipt(
|
|
410
|
+
receipt: RoutineScheduleReceipt,
|
|
411
|
+
schedules: readonly RoutineScheduleLiveRecord[],
|
|
412
|
+
): { readonly reason: RoutineScheduleCorrelation['matchReason']; readonly schedule?: RoutineScheduleLiveRecord } {
|
|
413
|
+
if (receipt.scheduleId) {
|
|
414
|
+
const byId = schedules.find((schedule) => schedule.id === receipt.scheduleId);
|
|
415
|
+
if (byId) return { reason: 'schedule-id', schedule: byId };
|
|
416
|
+
}
|
|
417
|
+
const byNameAndCadence = schedules.find((schedule) => (
|
|
418
|
+
normalizeForMatch(schedule.name) === normalizeForMatch(receipt.scheduleName)
|
|
419
|
+
&& cadenceMatches(receipt, schedule)
|
|
420
|
+
));
|
|
421
|
+
return byNameAndCadence
|
|
422
|
+
? { reason: 'name-and-cadence', schedule: byNameAndCadence }
|
|
423
|
+
: { reason: 'not-found' };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function correlateReceipts(
|
|
427
|
+
receipts: readonly RoutineScheduleReceipt[],
|
|
428
|
+
schedules: readonly RoutineScheduleLiveRecord[],
|
|
429
|
+
): readonly RoutineScheduleCorrelation[] {
|
|
430
|
+
return receipts.map((receipt) => {
|
|
431
|
+
if (receipt.status === 'failed') {
|
|
432
|
+
return {
|
|
433
|
+
receipt,
|
|
434
|
+
liveStatus: 'failed-receipt',
|
|
435
|
+
matchReason: 'failed-receipt',
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
const match = findScheduleForReceipt(receipt, schedules);
|
|
439
|
+
return match.schedule
|
|
440
|
+
? {
|
|
441
|
+
receipt,
|
|
442
|
+
liveStatus: 'matched',
|
|
443
|
+
matchReason: match.reason,
|
|
444
|
+
schedule: match.schedule,
|
|
445
|
+
}
|
|
446
|
+
: {
|
|
447
|
+
receipt,
|
|
448
|
+
liveStatus: 'missing',
|
|
449
|
+
matchReason: 'not-found',
|
|
450
|
+
};
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
273
454
|
function buildReceipt(
|
|
274
455
|
existing: readonly RoutineScheduleReceipt[],
|
|
275
456
|
connection: AgentDaemonConnection,
|
|
@@ -593,6 +774,38 @@ async function classifyScheduleError(
|
|
|
593
774
|
return { ok: false, kind: 'daemon_error', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
|
|
594
775
|
}
|
|
595
776
|
|
|
777
|
+
async function classifyScheduleListError(
|
|
778
|
+
error: unknown,
|
|
779
|
+
connection: AgentDaemonConnection,
|
|
780
|
+
): Promise<RoutineScheduleCorrelationFailure> {
|
|
781
|
+
const message = summarizeError(error);
|
|
782
|
+
const lower = message.toLowerCase();
|
|
783
|
+
if (lower.includes('401') || lower.includes('unauthorized') || lower.includes('auth')) {
|
|
784
|
+
return { ok: false, kind: 'auth_required', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
|
|
785
|
+
}
|
|
786
|
+
if (lower.includes('404') || lower.includes('not found')) {
|
|
787
|
+
const daemon = await fetchDaemonStatus(connection);
|
|
788
|
+
const record = isRecord(daemon.body) ? daemon.body : {};
|
|
789
|
+
const daemonVersion = readString(record, 'version') ?? 'unknown';
|
|
790
|
+
if (daemon.ok && daemonVersion !== SDK_VERSION) {
|
|
791
|
+
return {
|
|
792
|
+
ok: false,
|
|
793
|
+
kind: 'version_mismatch',
|
|
794
|
+
error: `External daemon SDK version ${daemonVersion} does not match Agent SDK pin ${SDK_VERSION}; schedules.list is unavailable.`,
|
|
795
|
+
route: ROUTINE_SCHEDULE_ROUTE,
|
|
796
|
+
baseUrl: connection.baseUrl,
|
|
797
|
+
daemonVersion,
|
|
798
|
+
expectedSdkVersion: SDK_VERSION,
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
return { ok: false, kind: 'daemon_route_unavailable', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
|
|
802
|
+
}
|
|
803
|
+
if (lower.includes('fetch') || lower.includes('connect') || lower.includes('econnrefused')) {
|
|
804
|
+
return { ok: false, kind: 'daemon_unavailable', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
|
|
805
|
+
}
|
|
806
|
+
return { ok: false, kind: 'daemon_error', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
|
|
807
|
+
}
|
|
808
|
+
|
|
596
809
|
export async function promoteRoutineToDaemonSchedule(
|
|
597
810
|
connection: AgentDaemonConnection,
|
|
598
811
|
preview: RoutineSchedulePromotionPreview,
|
|
@@ -623,6 +836,37 @@ export async function promoteRoutineToDaemonSchedule(
|
|
|
623
836
|
}
|
|
624
837
|
}
|
|
625
838
|
|
|
839
|
+
export async function reconcileRoutineScheduleReceipts(
|
|
840
|
+
connection: AgentDaemonConnection,
|
|
841
|
+
snapshot: RoutineScheduleReceiptSnapshot,
|
|
842
|
+
): Promise<RoutineScheduleCorrelationResult> {
|
|
843
|
+
if (!connection.token) {
|
|
844
|
+
return {
|
|
845
|
+
ok: false,
|
|
846
|
+
kind: 'auth_required',
|
|
847
|
+
error: `No daemon operator token found at ${connection.tokenPath}`,
|
|
848
|
+
route: ROUTINE_SCHEDULE_ROUTE,
|
|
849
|
+
baseUrl: connection.baseUrl,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
try {
|
|
853
|
+
const sdk = createBrowserGoodVibesSdk({ baseUrl: connection.baseUrl, authToken: connection.token });
|
|
854
|
+
const output = await sdk.operator.invoke(ROUTINE_SCHEDULE_LIST_METHOD, {});
|
|
855
|
+
const schedules = readLiveSchedules(output);
|
|
856
|
+
return {
|
|
857
|
+
ok: true,
|
|
858
|
+
kind: ROUTINE_SCHEDULE_LIST_METHOD,
|
|
859
|
+
route: ROUTINE_SCHEDULE_ROUTE,
|
|
860
|
+
baseUrl: connection.baseUrl,
|
|
861
|
+
scheduleCount: schedules.length,
|
|
862
|
+
receiptCount: snapshot.receipts.length,
|
|
863
|
+
correlations: correlateReceipts(snapshot.receipts, schedules),
|
|
864
|
+
};
|
|
865
|
+
} catch (error) {
|
|
866
|
+
return classifyScheduleListError(error, connection);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
626
870
|
export function formatRoutineSchedulePreview(preview: RoutineSchedulePromotionPreview): string {
|
|
627
871
|
const schedule = preview.payload.kind === 'cron'
|
|
628
872
|
? `${preview.payload.cron}${preview.payload.timezone ? ` [${preview.payload.timezone}]` : ''}`
|
|
@@ -699,6 +943,59 @@ export function formatRoutineScheduleReceipt(receipt: RoutineScheduleReceipt): s
|
|
|
699
943
|
].filter((line): line is string => Boolean(line)).join('\n');
|
|
700
944
|
}
|
|
701
945
|
|
|
946
|
+
export function formatRoutineScheduleCorrelation(result: RoutineScheduleCorrelationResult, limit = 10): string {
|
|
947
|
+
if (!result.ok) {
|
|
948
|
+
return [
|
|
949
|
+
`Daemon schedule reconciliation error: ${result.kind}`,
|
|
950
|
+
` ${result.error}`,
|
|
951
|
+
result.baseUrl ? ` daemon: ${result.baseUrl}` : null,
|
|
952
|
+
` route: ${ROUTINE_SCHEDULE_LIST_METHOD} ${result.route}`,
|
|
953
|
+
result.kind === 'auth_required'
|
|
954
|
+
? ' next: pair/authenticate with the externally managed GoodVibes daemon, then retry.'
|
|
955
|
+
: null,
|
|
956
|
+
result.kind === 'daemon_unavailable'
|
|
957
|
+
? ' next: start/restart the external GoodVibes daemon from TUI or daemon host tooling; Agent does not own daemon lifecycle.'
|
|
958
|
+
: null,
|
|
959
|
+
result.kind === 'version_mismatch' || result.kind === 'daemon_route_unavailable'
|
|
960
|
+
? ' next: update/restart the external GoodVibes daemon so public schedules.list is available.'
|
|
961
|
+
: null,
|
|
962
|
+
].filter((line): line is string => Boolean(line)).join('\n');
|
|
963
|
+
}
|
|
964
|
+
const correlations = result.correlations.slice(0, Math.max(1, limit));
|
|
965
|
+
if (result.receiptCount === 0) {
|
|
966
|
+
return [
|
|
967
|
+
'Agent routine schedule reconciliation',
|
|
968
|
+
` daemon: ${result.baseUrl}`,
|
|
969
|
+
` route: ${result.kind} ${result.route}`,
|
|
970
|
+
` live schedules: ${result.scheduleCount}`,
|
|
971
|
+
' No local routine promotion receipts exist yet.',
|
|
972
|
+
' Create one with /schedule promote-routine <routine-id> --cron <expr> --yes.',
|
|
973
|
+
].join('\n');
|
|
974
|
+
}
|
|
975
|
+
const matched = result.correlations.filter((entry) => entry.liveStatus === 'matched').length;
|
|
976
|
+
const missing = result.correlations.filter((entry) => entry.liveStatus === 'missing').length;
|
|
977
|
+
const failed = result.correlations.filter((entry) => entry.liveStatus === 'failed-receipt').length;
|
|
978
|
+
return [
|
|
979
|
+
'Agent routine schedule reconciliation',
|
|
980
|
+
` daemon: ${result.baseUrl}`,
|
|
981
|
+
` route: ${result.kind} ${result.route}`,
|
|
982
|
+
` receipts: ${result.receiptCount}; live schedules: ${result.scheduleCount}; matched: ${matched}; missing: ${missing}; failed receipts: ${failed}`,
|
|
983
|
+
...correlations.map((entry) => {
|
|
984
|
+
const receipt = entry.receipt;
|
|
985
|
+
const schedule = entry.schedule;
|
|
986
|
+
const live = schedule
|
|
987
|
+
? ` live=${schedule.id} status=${schedule.status ?? (schedule.enabled === false ? 'paused' : 'enabled')}`
|
|
988
|
+
: '';
|
|
989
|
+
const runs = schedule && schedule.runCount !== undefined
|
|
990
|
+
? ` runs=${schedule.runCount}/${schedule.successCount ?? 0}/${schedule.failureCount ?? 0}`
|
|
991
|
+
: '';
|
|
992
|
+
const next = schedule?.nextRunAt ? ` next=${new Date(schedule.nextRunAt).toISOString()}` : '';
|
|
993
|
+
return ` ${receipt.id} ${entry.liveStatus} reason=${entry.matchReason} routine=${receipt.routineId} receiptSchedule=${receipt.scheduleId ?? '(none)'}${live}${runs}${next}`;
|
|
994
|
+
}),
|
|
995
|
+
result.correlations.length > correlations.length ? ` ...${result.correlations.length - correlations.length} more` : '',
|
|
996
|
+
].filter((line): line is string => Boolean(line)).join('\n');
|
|
997
|
+
}
|
|
998
|
+
|
|
702
999
|
export function formatRoutineScheduleFailure(failure: RoutineSchedulePromotionFailure): string {
|
|
703
1000
|
return [
|
|
704
1001
|
`Daemon schedule error: ${failure.kind}`,
|
package/src/cli/help.ts
CHANGED
|
@@ -166,14 +166,16 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
166
166
|
'routines enabled',
|
|
167
167
|
'routines show <id>',
|
|
168
168
|
'routines receipts',
|
|
169
|
+
'routines reconcile',
|
|
169
170
|
'routines receipt <receipt-id>',
|
|
170
171
|
'routines promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) [--timezone <tz>] [--name <schedule-name>] [--provider <id>] [--model <model>] [--disabled] --yes',
|
|
171
172
|
],
|
|
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
|
+
summary: 'Inspect Agent-local routines, review local promotion receipts, reconcile receipts against live daemon schedules, and explicitly promote a reviewed routine into an external daemon schedule. Without --yes, promote only prints the schedules.create preview.',
|
|
173
174
|
examples: [
|
|
174
175
|
'routines list',
|
|
175
176
|
'routines show daily-operations-sweep',
|
|
176
177
|
'routines receipts',
|
|
178
|
+
'routines reconcile',
|
|
177
179
|
'routines promote daily-operations-sweep --cron "0 9 * * *" --timezone America/Chicago --yes',
|
|
178
180
|
'routines promote weekly-review --every 7d --disabled',
|
|
179
181
|
],
|
|
@@ -2,6 +2,7 @@ import { createShellPathService } from '@/runtime/index.ts';
|
|
|
2
2
|
import { AgentRoutineRegistry, type AgentRoutineRecord } from '../agent/routine-registry.ts';
|
|
3
3
|
import {
|
|
4
4
|
buildRoutineSchedulePreview,
|
|
5
|
+
formatRoutineScheduleCorrelation,
|
|
5
6
|
formatRoutineScheduleReceipt,
|
|
6
7
|
formatRoutineScheduleReceipts,
|
|
7
8
|
formatRoutineScheduleFailure,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
formatRoutineScheduleSuccess,
|
|
10
11
|
parseRoutineSchedulePromotionArgs,
|
|
11
12
|
promoteRoutineToDaemonSchedule,
|
|
13
|
+
reconcileRoutineScheduleReceipts,
|
|
12
14
|
resolveAgentDaemonConnection,
|
|
13
15
|
RoutineScheduleReceiptStore,
|
|
14
16
|
} from '../agent/routine-schedule-promotion.ts';
|
|
@@ -199,6 +201,17 @@ export async function handleRoutinesCommand(runtime: CliCommandRuntime): Promise
|
|
|
199
201
|
exitCode: 0,
|
|
200
202
|
};
|
|
201
203
|
}
|
|
204
|
+
if (normalized === 'reconcile' || normalized === 'sync' || normalized === 'status') {
|
|
205
|
+
const store = routineReceiptStore(runtime);
|
|
206
|
+
const result = await reconcileRoutineScheduleReceipts(
|
|
207
|
+
resolveAgentDaemonConnection(runtime.configManager, runtime.homeDirectory),
|
|
208
|
+
store.snapshot(),
|
|
209
|
+
);
|
|
210
|
+
return {
|
|
211
|
+
output: jsonOrText(runtime, result, formatRoutineScheduleCorrelation(result)),
|
|
212
|
+
exitCode: result.ok ? 0 : 1,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
202
215
|
if (normalized === 'receipt') {
|
|
203
216
|
const id = rest[0];
|
|
204
217
|
if (!id) return { output: 'Usage: goodvibes-agent routines receipt <receipt-id>', exitCode: 2 };
|
|
@@ -224,7 +237,7 @@ export async function handleRoutinesCommand(runtime: CliCommandRuntime): Promise
|
|
|
224
237
|
return handleRoutinePromotion(runtime, rest);
|
|
225
238
|
}
|
|
226
239
|
return {
|
|
227
|
-
output: 'Usage: goodvibes-agent routines [list|enabled|show <id>|receipts|receipt <id>|promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) --yes]',
|
|
240
|
+
output: 'Usage: goodvibes-agent routines [list|enabled|show <id>|receipts|reconcile|receipt <id>|promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) --yes]',
|
|
228
241
|
exitCode: 2,
|
|
229
242
|
};
|
|
230
243
|
}
|
|
@@ -573,11 +573,12 @@ 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, then reconciled against live daemon schedules from 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
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-reconcile', label: 'Reconcile schedules', detail: 'Compare local promotion receipts with live externally owned daemon schedules using schedules.list.', command: '/schedule reconcile', kind: 'command', safety: 'read-only' },
|
|
581
582
|
{ 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' },
|
|
582
583
|
{ 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' },
|
|
583
584
|
],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AgentRoutineRegistry, type AgentRoutineRecord } from '../../agent/routine-registry.ts';
|
|
2
2
|
import {
|
|
3
3
|
buildRoutineSchedulePreview,
|
|
4
|
+
formatRoutineScheduleCorrelation,
|
|
4
5
|
formatRoutineScheduleReceipt,
|
|
5
6
|
formatRoutineScheduleReceipts,
|
|
6
7
|
formatRoutineScheduleFailure,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
formatRoutineScheduleSuccess,
|
|
9
10
|
parseRoutineSchedulePromotionArgs,
|
|
10
11
|
promoteRoutineToDaemonSchedule,
|
|
12
|
+
reconcileRoutineScheduleReceipts,
|
|
11
13
|
resolveAgentDaemonConnection,
|
|
12
14
|
RoutineScheduleReceiptStore,
|
|
13
15
|
} from '../../agent/routine-schedule-promotion.ts';
|
|
@@ -167,6 +169,13 @@ export async function runRoutinesRuntimeCommand(args: readonly string[], ctx: Co
|
|
|
167
169
|
ctx.print(formatRoutineScheduleReceipts(receiptStoreFromContext(ctx).snapshot()));
|
|
168
170
|
return;
|
|
169
171
|
}
|
|
172
|
+
if (sub === 'reconcile' || sub === 'sync' || sub === 'status') {
|
|
173
|
+
const shellPaths = requireShellPaths(ctx);
|
|
174
|
+
const connection = resolveAgentDaemonConnection(ctx.platform.configManager, shellPaths.homeDirectory);
|
|
175
|
+
const result = await reconcileRoutineScheduleReceipts(connection, receiptStoreFromContext(ctx).snapshot());
|
|
176
|
+
ctx.print(formatRoutineScheduleCorrelation(result));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
170
179
|
if (sub === 'receipt') {
|
|
171
180
|
const id = args[1];
|
|
172
181
|
if (!id) {
|
|
@@ -275,7 +284,7 @@ export async function runRoutinesRuntimeCommand(args: readonly string[], ctx: Co
|
|
|
275
284
|
ctx.print(`Deleted Agent routine ${removed.id}: ${removed.name}`);
|
|
276
285
|
return;
|
|
277
286
|
}
|
|
278
|
-
ctx.print('Usage: /routines [list|enabled|search|show|receipts|receipt|create|update|enable|disable|start|review|stale|promote|delete]');
|
|
287
|
+
ctx.print('Usage: /routines [list|enabled|search|show|receipts|reconcile|receipt|create|update|enable|disable|start|review|stale|promote|delete]');
|
|
279
288
|
} catch (error) {
|
|
280
289
|
printError(ctx, error);
|
|
281
290
|
}
|
|
@@ -286,7 +295,7 @@ export function registerRoutinesRuntimeCommands(registry: CommandRegistry): void
|
|
|
286
295
|
name: 'routines',
|
|
287
296
|
aliases: ['routine'],
|
|
288
297
|
description: 'Manage local GoodVibes Agent routines',
|
|
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]',
|
|
298
|
+
usage: '[list|enabled|search <query>|show <id>|receipts|reconcile|receipt <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]',
|
|
290
299
|
handler: runRoutinesRuntimeCommand,
|
|
291
300
|
});
|
|
292
301
|
}
|
|
@@ -7,6 +7,7 @@ import type { AutomationScheduleDefinition } from '@pellux/goodvibes-sdk/platfor
|
|
|
7
7
|
import { AgentRoutineRegistry } from '../../agent/routine-registry.ts';
|
|
8
8
|
import {
|
|
9
9
|
buildRoutineSchedulePreview,
|
|
10
|
+
formatRoutineScheduleCorrelation,
|
|
10
11
|
formatRoutineScheduleReceipt,
|
|
11
12
|
formatRoutineScheduleReceipts,
|
|
12
13
|
formatRoutineScheduleFailure,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
formatRoutineScheduleSuccess,
|
|
15
16
|
parseRoutineSchedulePromotionArgs,
|
|
16
17
|
promoteRoutineToDaemonSchedule,
|
|
18
|
+
reconcileRoutineScheduleReceipts,
|
|
17
19
|
resolveAgentDaemonConnection,
|
|
18
20
|
RoutineScheduleReceiptStore,
|
|
19
21
|
} from '../../agent/routine-schedule-promotion.ts';
|
|
@@ -85,8 +87,8 @@ export function registerScheduleRuntimeCommands(registry: CommandRegistry): void
|
|
|
85
87
|
name: 'schedule',
|
|
86
88
|
aliases: ['sched'],
|
|
87
89
|
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',
|
|
90
|
+
usage: 'list | receipts | reconcile | receipt <id> | promote-routine <routine-id> --cron <expr> --yes',
|
|
91
|
+
argsHint: 'list | receipts | reconcile | receipt <id> | promote-routine <routine-id> --cron <expr> --yes',
|
|
90
92
|
async handler(args, ctx) {
|
|
91
93
|
const sub = args[0];
|
|
92
94
|
|
|
@@ -100,6 +102,14 @@ export function registerScheduleRuntimeCommands(registry: CommandRegistry): void
|
|
|
100
102
|
return;
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
if (sub === 'reconcile' || sub === 'sync' || sub === 'status') {
|
|
106
|
+
const shellPaths = requireShellPaths(ctx);
|
|
107
|
+
const connection = resolveAgentDaemonConnection(ctx.platform.configManager, shellPaths.homeDirectory);
|
|
108
|
+
const result = await reconcileRoutineScheduleReceipts(connection, RoutineScheduleReceiptStore.fromShellPaths(shellPaths).snapshot());
|
|
109
|
+
ctx.print(formatRoutineScheduleCorrelation(result));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
103
113
|
if (sub === 'receipt') {
|
|
104
114
|
const id = args[1];
|
|
105
115
|
if (!id) {
|
|
@@ -148,6 +158,7 @@ export function registerScheduleRuntimeCommands(registry: CommandRegistry): void
|
|
|
148
158
|
'Usage:\n'
|
|
149
159
|
+ ' /schedule list\n'
|
|
150
160
|
+ ' /schedule receipts\n'
|
|
161
|
+
+ ' /schedule reconcile\n'
|
|
151
162
|
+ ' /schedule receipt <receipt-id>\n'
|
|
152
163
|
+ ' /schedule promote-routine <routine-id> (--cron <expr>|--every <interval>|--at <iso-time>) --yes\n'
|
|
153
164
|
+ ' Local schedule mutations and runs remain blocked.'
|
|
@@ -116,7 +116,7 @@ export const OPERATOR_CAPABILITY_BENCHMARKS: readonly OperatorCapabilityBenchmar
|
|
|
116
116
|
configure: ['/schedule list', '/routines create ...', '/schedule promote-routine <id> --cron "0 8 * * *" --yes', 'goodvibes-agent routines promote <id> --every 1d --yes'],
|
|
117
117
|
use: ['/schedule list', '/routines start <id>', '/schedule promote-routine <id> --cron <expr> --yes'],
|
|
118
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
|
|
119
|
+
next: ['Add delivery target selection and deeper live run/delivery history 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.39';
|
|
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 {
|