@pellux/goodvibes-agent 0.1.37 → 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 +9 -0
- package/README.md +3 -1
- package/docs/getting-started.md +3 -1
- package/docs/operator-capability-benchmark.md +2 -1
- package/package.json +1 -1
- package/src/agent/routine-schedule-promotion.ts +586 -2
- package/src/cli/help.ts +6 -1
- package/src/cli/routines-command.ts +61 -3
- package/src/input/agent-workspace.ts +3 -1
- package/src/input/commands/routines-runtime.ts +34 -3
- package/src/input/commands/schedule-runtime.ts +36 -3
- package/src/operator/capability-benchmark.ts +6 -6
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GoodVibes Agent will be recorded here.
|
|
4
4
|
|
|
5
|
+
## 0.1.39 - 2026-05-31
|
|
6
|
+
|
|
7
|
+
- c98de19 Add routine schedule reconciliation
|
|
8
|
+
|
|
9
|
+
## 0.1.38 - 2026-05-31
|
|
10
|
+
|
|
11
|
+
- 072503c Add routine schedule promotion receipts
|
|
12
|
+
- 5bb9801 Update automation capability benchmark
|
|
13
|
+
|
|
5
14
|
## 0.1.37 - 2026-05-31
|
|
6
15
|
|
|
7
16
|
- 656b6f4 Add Agent routine daemon schedule promotion
|
package/README.md
CHANGED
|
@@ -69,11 +69,13 @@ 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
|
|
73
|
+
/schedule reconcile
|
|
72
74
|
/agent-skills create --name "Morning Brief" --description "Daily briefing flow" --procedure "Check tasks, approvals, calendar, and unread state before summarizing." --enabled true
|
|
73
75
|
/skills local list
|
|
74
76
|
```
|
|
75
77
|
|
|
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.
|
|
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`.
|
|
77
79
|
|
|
78
80
|
## Daemon Prerequisite
|
|
79
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:
|
|
@@ -76,7 +78,7 @@ Personas, routines, and reusable Agent skills are local to GoodVibes Agent. They
|
|
|
76
78
|
/skills local list
|
|
77
79
|
```
|
|
78
80
|
|
|
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.
|
|
81
|
+
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
82
|
|
|
81
83
|
## External Daemon
|
|
82
84
|
|
|
@@ -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 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 |
|
|
@@ -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.
|
|
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",
|
|
@@ -1,17 +1,22 @@
|
|
|
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
|
+
import { formatEveryInterval } from '@pellux/goodvibes-sdk/platform/automation';
|
|
5
6
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
7
|
+
import type { ShellPathService } from '@/runtime/index.ts';
|
|
6
8
|
import { getModelIdFromProviderModel, getProviderIdFromModel } from '../config/provider-model.ts';
|
|
9
|
+
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
7
10
|
import { SDK_VERSION } from '../version.ts';
|
|
8
11
|
import type { AgentRoutineRecord } from './routine-registry.ts';
|
|
9
12
|
|
|
10
13
|
export const ROUTINE_SCHEDULE_ROUTE = '/api/automation/schedules';
|
|
11
14
|
export const ROUTINE_SCHEDULE_METHOD = 'schedules.create';
|
|
15
|
+
export const ROUTINE_SCHEDULE_LIST_METHOD = 'schedules.list';
|
|
12
16
|
|
|
13
17
|
type ScheduleCreateInput = OperatorMethodInput<'schedules.create'>;
|
|
14
18
|
type ScheduleCreateOutput = OperatorMethodOutput<'schedules.create'>;
|
|
19
|
+
type ScheduleListOutput = OperatorMethodOutput<'schedules.list'>;
|
|
15
20
|
|
|
16
21
|
export interface AgentDaemonConfigReader {
|
|
17
22
|
get(key: string): unknown;
|
|
@@ -80,6 +85,98 @@ export type RoutineSchedulePromotionResult =
|
|
|
80
85
|
| RoutineSchedulePromotionSuccess
|
|
81
86
|
| RoutineSchedulePromotionFailure;
|
|
82
87
|
|
|
88
|
+
export interface RoutineScheduleReceipt {
|
|
89
|
+
readonly id: string;
|
|
90
|
+
readonly createdAt: string;
|
|
91
|
+
readonly routineId: string;
|
|
92
|
+
readonly routineName: string;
|
|
93
|
+
readonly route: typeof ROUTINE_SCHEDULE_ROUTE;
|
|
94
|
+
readonly method: typeof ROUTINE_SCHEDULE_METHOD;
|
|
95
|
+
readonly status: 'created' | 'failed';
|
|
96
|
+
readonly daemonBaseUrl: string;
|
|
97
|
+
readonly scheduleId?: string;
|
|
98
|
+
readonly scheduleStatus?: string;
|
|
99
|
+
readonly scheduleName: string;
|
|
100
|
+
readonly scheduleKind: RoutineScheduleKind;
|
|
101
|
+
readonly scheduleValue: string;
|
|
102
|
+
readonly timezone?: string;
|
|
103
|
+
readonly provider?: string;
|
|
104
|
+
readonly model?: string;
|
|
105
|
+
readonly enabled: boolean;
|
|
106
|
+
readonly target: {
|
|
107
|
+
readonly kind?: string;
|
|
108
|
+
readonly surfaceKind?: string;
|
|
109
|
+
readonly preserveThread?: boolean;
|
|
110
|
+
readonly createIfMissing?: boolean;
|
|
111
|
+
};
|
|
112
|
+
readonly deliveryMode?: string;
|
|
113
|
+
readonly failureKind?: RoutineSchedulePromotionFailure['kind'];
|
|
114
|
+
readonly failureError?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface RoutineScheduleReceiptSnapshot {
|
|
118
|
+
readonly path: string;
|
|
119
|
+
readonly receipts: readonly RoutineScheduleReceipt[];
|
|
120
|
+
}
|
|
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
|
+
|
|
173
|
+
interface RoutineScheduleReceiptStoreFile {
|
|
174
|
+
readonly version: 1;
|
|
175
|
+
readonly receipts: readonly RoutineScheduleReceipt[];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const RECEIPT_STORE_VERSION = 1;
|
|
179
|
+
|
|
83
180
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
84
181
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
85
182
|
}
|
|
@@ -89,6 +186,20 @@ function readString(record: Record<string, unknown>, key: string): string | null
|
|
|
89
186
|
return typeof value === 'string' ? value : null;
|
|
90
187
|
}
|
|
91
188
|
|
|
189
|
+
function readBoolean(record: Record<string, unknown>, key: string): boolean | undefined {
|
|
190
|
+
const value = record[key];
|
|
191
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
192
|
+
}
|
|
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
|
+
|
|
199
|
+
function nowIso(): string {
|
|
200
|
+
return new Date().toISOString();
|
|
201
|
+
}
|
|
202
|
+
|
|
92
203
|
function optionValue(args: readonly string[], index: number, inlineValue: string | undefined): {
|
|
93
204
|
readonly value: string | undefined;
|
|
94
205
|
readonly nextIndex: number;
|
|
@@ -111,6 +222,320 @@ function normalizeProviderModel(provider: string | undefined, model: string | un
|
|
|
111
222
|
};
|
|
112
223
|
}
|
|
113
224
|
|
|
225
|
+
function readReceipt(value: unknown): RoutineScheduleReceipt | null {
|
|
226
|
+
if (!isRecord(value)) return null;
|
|
227
|
+
const id = readString(value, 'id')?.trim();
|
|
228
|
+
const createdAt = readString(value, 'createdAt')?.trim();
|
|
229
|
+
const routineId = readString(value, 'routineId')?.trim();
|
|
230
|
+
const routineName = readString(value, 'routineName')?.trim();
|
|
231
|
+
const status = value.status === 'created' || value.status === 'failed' ? value.status : null;
|
|
232
|
+
const scheduleKind = value.scheduleKind === 'cron' || value.scheduleKind === 'every' || value.scheduleKind === 'at'
|
|
233
|
+
? value.scheduleKind
|
|
234
|
+
: null;
|
|
235
|
+
const scheduleValue = readString(value, 'scheduleValue')?.trim();
|
|
236
|
+
const target = isRecord(value.target) ? value.target : {};
|
|
237
|
+
if (!id || !createdAt || !routineId || !routineName || !status || !scheduleKind || !scheduleValue) return null;
|
|
238
|
+
return {
|
|
239
|
+
id,
|
|
240
|
+
createdAt,
|
|
241
|
+
routineId,
|
|
242
|
+
routineName,
|
|
243
|
+
route: ROUTINE_SCHEDULE_ROUTE,
|
|
244
|
+
method: ROUTINE_SCHEDULE_METHOD,
|
|
245
|
+
status,
|
|
246
|
+
daemonBaseUrl: readString(value, 'daemonBaseUrl') ?? '',
|
|
247
|
+
scheduleId: readString(value, 'scheduleId') ?? undefined,
|
|
248
|
+
scheduleStatus: readString(value, 'scheduleStatus') ?? undefined,
|
|
249
|
+
scheduleName: readString(value, 'scheduleName') ?? routineName,
|
|
250
|
+
scheduleKind,
|
|
251
|
+
scheduleValue,
|
|
252
|
+
timezone: readString(value, 'timezone') ?? undefined,
|
|
253
|
+
provider: readString(value, 'provider') ?? undefined,
|
|
254
|
+
model: readString(value, 'model') ?? undefined,
|
|
255
|
+
enabled: value.enabled !== false,
|
|
256
|
+
target: {
|
|
257
|
+
kind: readString(target, 'kind') ?? undefined,
|
|
258
|
+
surfaceKind: readString(target, 'surfaceKind') ?? undefined,
|
|
259
|
+
preserveThread: readBoolean(target, 'preserveThread'),
|
|
260
|
+
createIfMissing: readBoolean(target, 'createIfMissing'),
|
|
261
|
+
},
|
|
262
|
+
deliveryMode: readString(value, 'deliveryMode') ?? undefined,
|
|
263
|
+
failureKind: value.failureKind === 'confirmation_required'
|
|
264
|
+
|| value.failureKind === 'auth_required'
|
|
265
|
+
|| value.failureKind === 'daemon_unavailable'
|
|
266
|
+
|| value.failureKind === 'version_mismatch'
|
|
267
|
+
|| value.failureKind === 'daemon_route_unavailable'
|
|
268
|
+
|| value.failureKind === 'daemon_error'
|
|
269
|
+
? value.failureKind
|
|
270
|
+
: undefined,
|
|
271
|
+
failureError: readString(value, 'failureError') ?? undefined,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function parseReceiptStore(raw: string): RoutineScheduleReceiptStoreFile {
|
|
276
|
+
const parsed: unknown = JSON.parse(raw);
|
|
277
|
+
if (!isRecord(parsed)) return { version: RECEIPT_STORE_VERSION, receipts: [] };
|
|
278
|
+
return {
|
|
279
|
+
version: RECEIPT_STORE_VERSION,
|
|
280
|
+
receipts: Array.isArray(parsed.receipts)
|
|
281
|
+
? parsed.receipts.map(readReceipt).filter((receipt): receipt is RoutineScheduleReceipt => receipt !== null)
|
|
282
|
+
: [],
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function formatReceiptStore(store: RoutineScheduleReceiptStoreFile): string {
|
|
287
|
+
return `${JSON.stringify(store, null, 2)}\n`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function receiptId(createdAt: string, routineId: string, existing: readonly RoutineScheduleReceipt[]): string {
|
|
291
|
+
const dayStamp = createdAt.slice(0, 10).replace(/-/g, '');
|
|
292
|
+
const base = `routine-schedule-${routineId}-${dayStamp}`.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
293
|
+
const ids = new Set(existing.map((receipt) => receipt.id));
|
|
294
|
+
if (!ids.has(base)) return base;
|
|
295
|
+
for (let index = 2; index < 1000; index += 1) {
|
|
296
|
+
const candidate = `${base}-${index}`;
|
|
297
|
+
if (!ids.has(candidate)) return candidate;
|
|
298
|
+
}
|
|
299
|
+
return `${base}-${existing.length + 1}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function scheduleValue(payload: ScheduleCreateInput): string {
|
|
303
|
+
if (payload.kind === 'cron') return String(payload.cron ?? '');
|
|
304
|
+
if (payload.kind === 'every') return String(payload.every ?? '');
|
|
305
|
+
return String(payload.at ?? '');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function scheduleKind(payload: ScheduleCreateInput): RoutineScheduleKind {
|
|
309
|
+
if (payload.kind === 'cron' || payload.kind === 'every' || payload.kind === 'at') return payload.kind;
|
|
310
|
+
throw new Error('Routine schedule payload is missing a schedule kind.');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function targetSummary(payload: ScheduleCreateInput): RoutineScheduleReceipt['target'] {
|
|
314
|
+
return isRecord(payload.target)
|
|
315
|
+
? {
|
|
316
|
+
kind: typeof payload.target.kind === 'string' ? payload.target.kind : undefined,
|
|
317
|
+
surfaceKind: typeof payload.target.surfaceKind === 'string' ? payload.target.surfaceKind : undefined,
|
|
318
|
+
preserveThread: typeof payload.target.preserveThread === 'boolean' ? payload.target.preserveThread : undefined,
|
|
319
|
+
createIfMissing: typeof payload.target.createIfMissing === 'boolean' ? payload.target.createIfMissing : undefined,
|
|
320
|
+
}
|
|
321
|
+
: {};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function deliveryMode(payload: ScheduleCreateInput): string | undefined {
|
|
325
|
+
return isRecord(payload.delivery) && typeof payload.delivery.mode === 'string' ? payload.delivery.mode : undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function resultScheduleRecord(result: RoutineSchedulePromotionResult): Record<string, unknown> {
|
|
329
|
+
return result.ok && isRecord(result.schedule) ? result.schedule : {};
|
|
330
|
+
}
|
|
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
|
+
|
|
454
|
+
function buildReceipt(
|
|
455
|
+
existing: readonly RoutineScheduleReceipt[],
|
|
456
|
+
connection: AgentDaemonConnection,
|
|
457
|
+
preview: RoutineSchedulePromotionPreview,
|
|
458
|
+
result: RoutineSchedulePromotionResult,
|
|
459
|
+
): RoutineScheduleReceipt {
|
|
460
|
+
const createdAt = nowIso();
|
|
461
|
+
const kind = scheduleKind(preview.payload);
|
|
462
|
+
const schedule = resultScheduleRecord(result);
|
|
463
|
+
const scheduleId = readString(schedule, 'id') ?? undefined;
|
|
464
|
+
const scheduleStatus = readString(schedule, 'status') ?? (schedule.enabled === false ? 'paused' : schedule.enabled === true ? 'enabled' : undefined);
|
|
465
|
+
return {
|
|
466
|
+
id: receiptId(createdAt, preview.routineId, existing),
|
|
467
|
+
createdAt,
|
|
468
|
+
routineId: preview.routineId,
|
|
469
|
+
routineName: preview.routineName,
|
|
470
|
+
route: ROUTINE_SCHEDULE_ROUTE,
|
|
471
|
+
method: ROUTINE_SCHEDULE_METHOD,
|
|
472
|
+
status: result.ok ? 'created' : 'failed',
|
|
473
|
+
daemonBaseUrl: connection.baseUrl,
|
|
474
|
+
scheduleId,
|
|
475
|
+
scheduleStatus,
|
|
476
|
+
scheduleName: String(preview.payload.name ?? `Agent routine: ${preview.routineName}`),
|
|
477
|
+
scheduleKind: kind,
|
|
478
|
+
scheduleValue: scheduleValue(preview.payload),
|
|
479
|
+
timezone: kind === 'cron' ? preview.payload.timezone : undefined,
|
|
480
|
+
provider: preview.payload.provider,
|
|
481
|
+
model: preview.payload.model,
|
|
482
|
+
enabled: preview.payload.enabled !== false,
|
|
483
|
+
target: targetSummary(preview.payload),
|
|
484
|
+
deliveryMode: deliveryMode(preview.payload),
|
|
485
|
+
failureKind: result.ok ? undefined : result.kind,
|
|
486
|
+
failureError: result.ok ? undefined : result.error,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function routineScheduleReceiptStorePath(shellPaths: ShellPathService): string {
|
|
491
|
+
return shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'routines', 'schedule-receipts.json');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export class RoutineScheduleReceiptStore {
|
|
495
|
+
public constructor(private readonly storePath: string) {}
|
|
496
|
+
|
|
497
|
+
public static fromShellPaths(shellPaths: ShellPathService): RoutineScheduleReceiptStore {
|
|
498
|
+
return new RoutineScheduleReceiptStore(routineScheduleReceiptStorePath(shellPaths));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
public snapshot(): RoutineScheduleReceiptSnapshot {
|
|
502
|
+
const store = this.readStore();
|
|
503
|
+
return {
|
|
504
|
+
path: this.storePath,
|
|
505
|
+
receipts: [...store.receipts].sort((left, right) => right.createdAt.localeCompare(left.createdAt)),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
public get(id: string): RoutineScheduleReceipt | null {
|
|
510
|
+
const normalized = id.trim().toLowerCase();
|
|
511
|
+
if (!normalized) return null;
|
|
512
|
+
return this.snapshot().receipts.find((receipt) => receipt.id.toLowerCase() === normalized) ?? null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
public append(connection: AgentDaemonConnection, preview: RoutineSchedulePromotionPreview, result: RoutineSchedulePromotionResult): RoutineScheduleReceipt {
|
|
516
|
+
const store = this.readStore();
|
|
517
|
+
const receipt = buildReceipt(store.receipts, connection, preview, result);
|
|
518
|
+
this.writeStore({ ...store, receipts: [...store.receipts, receipt] });
|
|
519
|
+
return receipt;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private readStore(): RoutineScheduleReceiptStoreFile {
|
|
523
|
+
if (!existsSync(this.storePath)) return { version: RECEIPT_STORE_VERSION, receipts: [] };
|
|
524
|
+
try {
|
|
525
|
+
return parseReceiptStore(readFileSync(this.storePath, 'utf-8'));
|
|
526
|
+
} catch (error) {
|
|
527
|
+
throw new Error(`Could not read Agent routine schedule receipt store: ${summarizeError(error)}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private writeStore(store: RoutineScheduleReceiptStoreFile): void {
|
|
532
|
+
mkdirSync(dirname(this.storePath), { recursive: true });
|
|
533
|
+
const tmpPath = `${this.storePath}.tmp`;
|
|
534
|
+
writeFileSync(tmpPath, formatReceiptStore(store), 'utf-8');
|
|
535
|
+
renameSync(tmpPath, this.storePath);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
114
539
|
export function parseRoutineSchedulePromotionArgs(args: readonly string[]): ParsedRoutineSchedulePromotionArgs {
|
|
115
540
|
let routineId: string | null = null;
|
|
116
541
|
let schedule: RoutineScheduleSpec | null = null;
|
|
@@ -349,6 +774,38 @@ async function classifyScheduleError(
|
|
|
349
774
|
return { ok: false, kind: 'daemon_error', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
|
|
350
775
|
}
|
|
351
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
|
+
|
|
352
809
|
export async function promoteRoutineToDaemonSchedule(
|
|
353
810
|
connection: AgentDaemonConnection,
|
|
354
811
|
preview: RoutineSchedulePromotionPreview,
|
|
@@ -379,6 +836,37 @@ export async function promoteRoutineToDaemonSchedule(
|
|
|
379
836
|
}
|
|
380
837
|
}
|
|
381
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
|
+
|
|
382
870
|
export function formatRoutineSchedulePreview(preview: RoutineSchedulePromotionPreview): string {
|
|
383
871
|
const schedule = preview.payload.kind === 'cron'
|
|
384
872
|
? `${preview.payload.cron}${preview.payload.timezone ? ` [${preview.payload.timezone}]` : ''}`
|
|
@@ -412,6 +900,102 @@ export function formatRoutineScheduleSuccess(result: RoutineSchedulePromotionSuc
|
|
|
412
900
|
].join('\n');
|
|
413
901
|
}
|
|
414
902
|
|
|
903
|
+
export function formatRoutineScheduleReceipts(snapshot: RoutineScheduleReceiptSnapshot, limit = 10): string {
|
|
904
|
+
const receipts = snapshot.receipts.slice(0, Math.max(1, limit));
|
|
905
|
+
if (snapshot.receipts.length === 0) {
|
|
906
|
+
return [
|
|
907
|
+
'Agent routine schedule receipts',
|
|
908
|
+
` store: ${snapshot.path}`,
|
|
909
|
+
' No routine schedule promotions have been recorded yet.',
|
|
910
|
+
' Create one with /schedule promote-routine <routine-id> --cron <expr> --yes.',
|
|
911
|
+
].join('\n');
|
|
912
|
+
}
|
|
913
|
+
return [
|
|
914
|
+
`Agent routine schedule receipts (${snapshot.receipts.length})`,
|
|
915
|
+
` store: ${snapshot.path}`,
|
|
916
|
+
...receipts.map((receipt) => {
|
|
917
|
+
const schedule = receipt.scheduleId ? ` schedule=${receipt.scheduleId}` : '';
|
|
918
|
+
const failure = receipt.status === 'failed' && receipt.failureKind ? ` failure=${receipt.failureKind}` : '';
|
|
919
|
+
return ` ${receipt.id} ${receipt.status} ${receipt.scheduleKind} ${receipt.scheduleValue} routine=${receipt.routineId}${schedule}${failure}`;
|
|
920
|
+
}),
|
|
921
|
+
snapshot.receipts.length > receipts.length ? ` ...${snapshot.receipts.length - receipts.length} more` : '',
|
|
922
|
+
].filter((line): line is string => Boolean(line)).join('\n');
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
export function formatRoutineScheduleReceipt(receipt: RoutineScheduleReceipt): string {
|
|
926
|
+
return [
|
|
927
|
+
`Agent routine schedule receipt ${receipt.id}`,
|
|
928
|
+
` created: ${receipt.createdAt}`,
|
|
929
|
+
` status: ${receipt.status}`,
|
|
930
|
+
` routine: ${receipt.routineName} (${receipt.routineId})`,
|
|
931
|
+
` route: ${receipt.method} ${receipt.route}`,
|
|
932
|
+
` daemon: ${receipt.daemonBaseUrl}`,
|
|
933
|
+
` schedule: ${receipt.scheduleName}${receipt.scheduleId ? ` (${receipt.scheduleId})` : ''}`,
|
|
934
|
+
receipt.scheduleStatus ? ` schedule status: ${receipt.scheduleStatus}` : '',
|
|
935
|
+
` cadence: ${receipt.scheduleKind} ${receipt.scheduleValue}${receipt.timezone ? ` [${receipt.timezone}]` : ''}`,
|
|
936
|
+
` enabled: ${receipt.enabled ? 'yes' : 'no'}`,
|
|
937
|
+
receipt.provider ? ` provider: ${receipt.provider}` : '',
|
|
938
|
+
receipt.model ? ` model: ${receipt.model}` : '',
|
|
939
|
+
` target: ${receipt.target.kind ?? 'unknown'}${receipt.target.surfaceKind ? `/${receipt.target.surfaceKind}` : ''}`,
|
|
940
|
+
receipt.deliveryMode ? ` delivery: ${receipt.deliveryMode}` : '',
|
|
941
|
+
receipt.failureKind ? ` failure: ${receipt.failureKind}` : '',
|
|
942
|
+
receipt.failureError ? ` error: ${receipt.failureError}` : '',
|
|
943
|
+
].filter((line): line is string => Boolean(line)).join('\n');
|
|
944
|
+
}
|
|
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
|
+
|
|
415
999
|
export function formatRoutineScheduleFailure(failure: RoutineSchedulePromotionFailure): string {
|
|
416
1000
|
return [
|
|
417
1001
|
`Daemon schedule error: ${failure.kind}`,
|
package/src/cli/help.ts
CHANGED
|
@@ -165,12 +165,17 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
165
165
|
'routines list',
|
|
166
166
|
'routines enabled',
|
|
167
167
|
'routines show <id>',
|
|
168
|
+
'routines receipts',
|
|
169
|
+
'routines reconcile',
|
|
170
|
+
'routines receipt <receipt-id>',
|
|
168
171
|
'routines promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) [--timezone <tz>] [--name <schedule-name>] [--provider <id>] [--model <model>] [--disabled] --yes',
|
|
169
172
|
],
|
|
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.',
|
|
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.',
|
|
171
174
|
examples: [
|
|
172
175
|
'routines list',
|
|
173
176
|
'routines show daily-operations-sweep',
|
|
177
|
+
'routines receipts',
|
|
178
|
+
'routines reconcile',
|
|
174
179
|
'routines promote daily-operations-sweep --cron "0 9 * * *" --timezone America/Chicago --yes',
|
|
175
180
|
'routines promote weekly-review --every 7d --disabled',
|
|
176
181
|
],
|
|
@@ -2,12 +2,17 @@ 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,
|
|
6
|
+
formatRoutineScheduleReceipt,
|
|
7
|
+
formatRoutineScheduleReceipts,
|
|
5
8
|
formatRoutineScheduleFailure,
|
|
6
9
|
formatRoutineSchedulePreview,
|
|
7
10
|
formatRoutineScheduleSuccess,
|
|
8
11
|
parseRoutineSchedulePromotionArgs,
|
|
9
12
|
promoteRoutineToDaemonSchedule,
|
|
13
|
+
reconcileRoutineScheduleReceipts,
|
|
10
14
|
resolveAgentDaemonConnection,
|
|
15
|
+
RoutineScheduleReceiptStore,
|
|
11
16
|
} from '../agent/routine-schedule-promotion.ts';
|
|
12
17
|
import type { CliCommandOutput } from './types.ts';
|
|
13
18
|
import type { CliCommandRuntime } from './management.ts';
|
|
@@ -35,6 +40,13 @@ function routineRegistry(runtime: CliCommandRuntime): AgentRoutineRegistry {
|
|
|
35
40
|
}));
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
function routineReceiptStore(runtime: CliCommandRuntime): RoutineScheduleReceiptStore {
|
|
44
|
+
return RoutineScheduleReceiptStore.fromShellPaths(createShellPathService({
|
|
45
|
+
workingDirectory: runtime.workingDirectory,
|
|
46
|
+
homeDirectory: runtime.homeDirectory,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
function summarizeRoutine(routine: AgentRoutineRecord): string {
|
|
39
51
|
const enabled = routine.enabled ? 'enabled' : 'disabled';
|
|
40
52
|
const tags = routine.tags.length > 0 ? ` tags=${routine.tags.join(',')}` : '';
|
|
@@ -121,14 +133,16 @@ async function handleRoutinePromotion(runtime: CliCommandRuntime, args: readonly
|
|
|
121
133
|
}
|
|
122
134
|
const connection = resolveAgentDaemonConnection(runtime.configManager, runtime.homeDirectory);
|
|
123
135
|
const result = await promoteRoutineToDaemonSchedule(connection, preview);
|
|
136
|
+
const receipt = routineReceiptStore(runtime).append(connection, preview, result);
|
|
124
137
|
if (!result.ok) {
|
|
125
138
|
return {
|
|
126
|
-
output: json ? JSON.stringify(result, null, 2) : formatRoutineScheduleFailure(result)
|
|
139
|
+
output: json ? JSON.stringify({ ...result, receipt }, null, 2) : `${formatRoutineScheduleFailure(result)}\n receipt: ${receipt.id}`,
|
|
127
140
|
exitCode: 1,
|
|
128
141
|
};
|
|
129
142
|
}
|
|
143
|
+
const value = { ...result, receipt };
|
|
130
144
|
return {
|
|
131
|
-
output: jsonOrText(runtime,
|
|
145
|
+
output: jsonOrText(runtime, value, `${formatRoutineScheduleSuccess(result)}\n receipt: ${receipt.id}`),
|
|
132
146
|
exitCode: 0,
|
|
133
147
|
};
|
|
134
148
|
}
|
|
@@ -175,11 +189,55 @@ export async function handleRoutinesCommand(runtime: CliCommandRuntime): Promise
|
|
|
175
189
|
exitCode: 0,
|
|
176
190
|
};
|
|
177
191
|
}
|
|
192
|
+
if (normalized === 'receipts' || normalized === 'history') {
|
|
193
|
+
const snapshot = routineReceiptStore(runtime).snapshot();
|
|
194
|
+
const value: RoutinesCommandSuccess<typeof snapshot> = {
|
|
195
|
+
ok: true,
|
|
196
|
+
kind: 'agent.routines.scheduleReceipts.list',
|
|
197
|
+
data: snapshot,
|
|
198
|
+
};
|
|
199
|
+
return {
|
|
200
|
+
output: jsonOrText(runtime, value, formatRoutineScheduleReceipts(snapshot)),
|
|
201
|
+
exitCode: 0,
|
|
202
|
+
};
|
|
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
|
+
}
|
|
215
|
+
if (normalized === 'receipt') {
|
|
216
|
+
const id = rest[0];
|
|
217
|
+
if (!id) return { output: 'Usage: goodvibes-agent routines receipt <receipt-id>', exitCode: 2 };
|
|
218
|
+
const receipt = routineReceiptStore(runtime).get(id);
|
|
219
|
+
if (!receipt) {
|
|
220
|
+
const failure: RoutinesCommandFailure = { ok: false, kind: 'routine_schedule_receipt_not_found', error: `Unknown routine schedule receipt: ${id}` };
|
|
221
|
+
return {
|
|
222
|
+
output: runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(failure, null, 2) : failure.error,
|
|
223
|
+
exitCode: 1,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const value: RoutinesCommandSuccess<typeof receipt> = {
|
|
227
|
+
ok: true,
|
|
228
|
+
kind: 'agent.routines.scheduleReceipts.get',
|
|
229
|
+
data: receipt,
|
|
230
|
+
};
|
|
231
|
+
return {
|
|
232
|
+
output: jsonOrText(runtime, value, formatRoutineScheduleReceipt(receipt)),
|
|
233
|
+
exitCode: 0,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
178
236
|
if (normalized === 'promote' || normalized === 'schedule' || normalized === 'promote-schedule') {
|
|
179
237
|
return handleRoutinePromotion(runtime, rest);
|
|
180
238
|
}
|
|
181
239
|
return {
|
|
182
|
-
output: 'Usage: goodvibes-agent routines [list|enabled|show <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]',
|
|
183
241
|
exitCode: 2,
|
|
184
242
|
};
|
|
185
243
|
}
|
|
@@ -573,10 +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
|
+
{ 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' },
|
|
580
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' },
|
|
581
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' },
|
|
582
584
|
],
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { AgentRoutineRegistry, type AgentRoutineRecord } from '../../agent/routine-registry.ts';
|
|
2
2
|
import {
|
|
3
3
|
buildRoutineSchedulePreview,
|
|
4
|
+
formatRoutineScheduleCorrelation,
|
|
5
|
+
formatRoutineScheduleReceipt,
|
|
6
|
+
formatRoutineScheduleReceipts,
|
|
4
7
|
formatRoutineScheduleFailure,
|
|
5
8
|
formatRoutineSchedulePreview,
|
|
6
9
|
formatRoutineScheduleSuccess,
|
|
7
10
|
parseRoutineSchedulePromotionArgs,
|
|
8
11
|
promoteRoutineToDaemonSchedule,
|
|
12
|
+
reconcileRoutineScheduleReceipts,
|
|
9
13
|
resolveAgentDaemonConnection,
|
|
14
|
+
RoutineScheduleReceiptStore,
|
|
10
15
|
} from '../../agent/routine-schedule-promotion.ts';
|
|
11
16
|
import type { CommandContext, CommandRegistry } from '../command-registry.ts';
|
|
12
17
|
import { requireShellPaths } from './runtime-services.ts';
|
|
@@ -52,6 +57,10 @@ function registryFromContext(ctx: CommandContext): AgentRoutineRegistry {
|
|
|
52
57
|
return AgentRoutineRegistry.fromShellPaths(requireShellPaths(ctx));
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
function receiptStoreFromContext(ctx: CommandContext): RoutineScheduleReceiptStore {
|
|
61
|
+
return RoutineScheduleReceiptStore.fromShellPaths(requireShellPaths(ctx));
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
function requiredFlag(flags: ReadonlyMap<string, string>, key: string): string {
|
|
56
65
|
const value = flags.get(key)?.trim();
|
|
57
66
|
if (!value) throw new Error(`Missing --${key}.`);
|
|
@@ -124,7 +133,8 @@ async function promoteRoutine(args: readonly string[], routineRegistry: AgentRou
|
|
|
124
133
|
const shellPaths = requireShellPaths(ctx);
|
|
125
134
|
const connection = resolveAgentDaemonConnection(ctx.platform.configManager, shellPaths.homeDirectory);
|
|
126
135
|
const result = await promoteRoutineToDaemonSchedule(connection, preview);
|
|
127
|
-
|
|
136
|
+
const receipt = receiptStoreFromContext(ctx).append(connection, preview, result);
|
|
137
|
+
ctx.print(result.ok ? `${formatRoutineScheduleSuccess(result)}\n receipt: ${receipt.id}` : `${formatRoutineScheduleFailure(result)}\n receipt: ${receipt.id}`);
|
|
128
138
|
}
|
|
129
139
|
|
|
130
140
|
export async function runRoutinesRuntimeCommand(args: readonly string[], ctx: CommandContext): Promise<void> {
|
|
@@ -155,6 +165,27 @@ export async function runRoutinesRuntimeCommand(args: readonly string[], ctx: Co
|
|
|
155
165
|
ctx.print(routine ? renderRoutine(routine) : `Unknown Agent routine: ${id}`);
|
|
156
166
|
return;
|
|
157
167
|
}
|
|
168
|
+
if (sub === 'receipts' || sub === 'history') {
|
|
169
|
+
ctx.print(formatRoutineScheduleReceipts(receiptStoreFromContext(ctx).snapshot()));
|
|
170
|
+
return;
|
|
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
|
+
}
|
|
179
|
+
if (sub === 'receipt') {
|
|
180
|
+
const id = args[1];
|
|
181
|
+
if (!id) {
|
|
182
|
+
ctx.print('Usage: /routines receipt <receipt-id>');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const receipt = receiptStoreFromContext(ctx).get(id);
|
|
186
|
+
ctx.print(receipt ? formatRoutineScheduleReceipt(receipt) : `Unknown routine schedule receipt: ${id}`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
158
189
|
if (sub === 'create') {
|
|
159
190
|
const parsed = parseRoutineArgs(args.slice(1));
|
|
160
191
|
const steps = parsed.flags.get('steps')?.trim() || parsed.rest.join(' ').trim();
|
|
@@ -253,7 +284,7 @@ export async function runRoutinesRuntimeCommand(args: readonly string[], ctx: Co
|
|
|
253
284
|
ctx.print(`Deleted Agent routine ${removed.id}: ${removed.name}`);
|
|
254
285
|
return;
|
|
255
286
|
}
|
|
256
|
-
ctx.print('Usage: /routines [list|enabled|search|show|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]');
|
|
257
288
|
} catch (error) {
|
|
258
289
|
printError(ctx, error);
|
|
259
290
|
}
|
|
@@ -264,7 +295,7 @@ export function registerRoutinesRuntimeCommands(registry: CommandRegistry): void
|
|
|
264
295
|
name: 'routines',
|
|
265
296
|
aliases: ['routine'],
|
|
266
297
|
description: 'Manage local GoodVibes Agent routines',
|
|
267
|
-
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]',
|
|
268
299
|
handler: runRoutinesRuntimeCommand,
|
|
269
300
|
});
|
|
270
301
|
}
|
|
@@ -7,12 +7,17 @@ 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,
|
|
11
|
+
formatRoutineScheduleReceipt,
|
|
12
|
+
formatRoutineScheduleReceipts,
|
|
10
13
|
formatRoutineScheduleFailure,
|
|
11
14
|
formatRoutineSchedulePreview,
|
|
12
15
|
formatRoutineScheduleSuccess,
|
|
13
16
|
parseRoutineSchedulePromotionArgs,
|
|
14
17
|
promoteRoutineToDaemonSchedule,
|
|
18
|
+
reconcileRoutineScheduleReceipts,
|
|
15
19
|
resolveAgentDaemonConnection,
|
|
20
|
+
RoutineScheduleReceiptStore,
|
|
16
21
|
} from '../../agent/routine-schedule-promotion.ts';
|
|
17
22
|
import type { CommandContext } from '../command-registry.ts';
|
|
18
23
|
import { requireShellPaths } from './runtime-services.ts';
|
|
@@ -73,7 +78,8 @@ async function promoteRoutineSchedule(args: readonly string[], ctx: CommandConte
|
|
|
73
78
|
}
|
|
74
79
|
const connection = resolveAgentDaemonConnection(ctx.platform.configManager, shellPaths.homeDirectory);
|
|
75
80
|
const result = await promoteRoutineToDaemonSchedule(connection, preview);
|
|
76
|
-
|
|
81
|
+
const receipt = RoutineScheduleReceiptStore.fromShellPaths(shellPaths).append(connection, preview, result);
|
|
82
|
+
ctx.print(result.ok ? `${formatRoutineScheduleSuccess(result)}\n receipt: ${receipt.id}` : `${formatRoutineScheduleFailure(result)}\n receipt: ${receipt.id}`);
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
export function registerScheduleRuntimeCommands(registry: CommandRegistry): void {
|
|
@@ -81,8 +87,8 @@ export function registerScheduleRuntimeCommands(registry: CommandRegistry): void
|
|
|
81
87
|
name: 'schedule',
|
|
82
88
|
aliases: ['sched'],
|
|
83
89
|
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',
|
|
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',
|
|
86
92
|
async handler(args, ctx) {
|
|
87
93
|
const sub = args[0];
|
|
88
94
|
|
|
@@ -91,6 +97,30 @@ export function registerScheduleRuntimeCommands(registry: CommandRegistry): void
|
|
|
91
97
|
return;
|
|
92
98
|
}
|
|
93
99
|
|
|
100
|
+
if (sub === 'receipts' || sub === 'history') {
|
|
101
|
+
ctx.print(formatRoutineScheduleReceipts(RoutineScheduleReceiptStore.fromShellPaths(requireShellPaths(ctx)).snapshot()));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
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
|
+
|
|
113
|
+
if (sub === 'receipt') {
|
|
114
|
+
const id = args[1];
|
|
115
|
+
if (!id) {
|
|
116
|
+
ctx.print('Usage: /schedule receipt <receipt-id>');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const receipt = RoutineScheduleReceiptStore.fromShellPaths(requireShellPaths(ctx)).get(id);
|
|
120
|
+
ctx.print(receipt ? formatRoutineScheduleReceipt(receipt) : `Unknown routine schedule receipt: ${id}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
94
124
|
const manager = ctx.ops.automationManager;
|
|
95
125
|
if (!manager) {
|
|
96
126
|
ctx.print('Automation manager is not available in this runtime.');
|
|
@@ -127,6 +157,9 @@ export function registerScheduleRuntimeCommands(registry: CommandRegistry): void
|
|
|
127
157
|
ctx.print(
|
|
128
158
|
'Usage:\n'
|
|
129
159
|
+ ' /schedule list\n'
|
|
160
|
+
+ ' /schedule receipts\n'
|
|
161
|
+
+ ' /schedule reconcile\n'
|
|
162
|
+
+ ' /schedule receipt <receipt-id>\n'
|
|
130
163
|
+ ' /schedule promote-routine <routine-id> (--cron <expr>|--every <interval>|--at <iso-time>) --yes\n'
|
|
131
164
|
+ ' Local schedule mutations and runs remain blocked.'
|
|
132
165
|
);
|
|
@@ -109,14 +109,14 @@ export const OPERATOR_CAPABILITY_BENCHMARKS: readonly OperatorCapabilityBenchmar
|
|
|
109
109
|
{
|
|
110
110
|
id: 'automation-schedules',
|
|
111
111
|
title: 'Automation, Schedules, And Routines',
|
|
112
|
-
posture: '
|
|
112
|
+
posture: 'configurable',
|
|
113
113
|
competitors: ['openclaw', 'hermes'],
|
|
114
114
|
competitorBaseline: 'Cron/scheduler can create, pause, resume, run, remove, and deliver recurring tasks from natural language.',
|
|
115
|
-
goodvibesAgent: 'Observes public automation/schedule routes and
|
|
116
|
-
configure: ['/schedule list', '/routines create ...', 'goodvibes-agent
|
|
117
|
-
use: ['/schedule list', '/routines start <id>'],
|
|
118
|
-
exceedsBy: ['No recursive hidden scheduler creation from model tools', 'explicit confirmation for side effects', 'local routines separate from daemon jobs'],
|
|
119
|
-
next: ['
|
|
115
|
+
goodvibesAgent: 'Observes public automation/schedule routes, keeps local routines separate from daemon jobs, and promotes a local routine into an external daemon schedules.create record only through an exact user command with --yes.',
|
|
116
|
+
configure: ['/schedule list', '/routines create ...', '/schedule promote-routine <id> --cron "0 8 * * *" --yes', 'goodvibes-agent routines promote <id> --every 1d --yes'],
|
|
117
|
+
use: ['/schedule list', '/routines start <id>', '/schedule promote-routine <id> --cron <expr> --yes'],
|
|
118
|
+
exceedsBy: ['No recursive hidden scheduler creation from model tools', 'explicit confirmation for side effects', 'local routines separate from daemon jobs', 'scheduled prompts preserve isolated Agent Knowledge and forbid default wiki/HomeGraph fallback'],
|
|
119
|
+
next: ['Add delivery target selection and 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 {
|