@pellux/goodvibes-agent 0.1.56 → 0.1.57
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/.goodvibes/GOODVIBES.md +1 -1
- package/CHANGELOG.md +14 -9
- package/README.md +3 -3
- package/docs/README.md +1 -1
- package/docs/getting-started.md +3 -3
- package/docs/release-and-publishing.md +2 -2
- package/package.json +1 -3
- package/src/agent/routine-schedule-args.ts +219 -0
- package/src/agent/routine-schedule-format.ts +173 -0
- package/src/agent/routine-schedule-promotion.ts +3 -811
- package/src/agent/routine-schedule-receipts.ts +502 -0
- package/src/cli/agent-knowledge-command.ts +6 -6
- package/src/cli/help.ts +3 -25
- package/src/cli/package-verification.ts +23 -16
- package/src/cli/redaction.ts +4 -1
- package/src/cli/routines-command.ts +10 -6
- package/src/cli/service-posture.ts +47 -280
- package/src/cli/status.ts +0 -1
- package/src/config/secret-config.ts +0 -2
- package/src/input/agent-workspace-categories.ts +219 -0
- package/src/input/agent-workspace-editors.ts +143 -0
- package/src/input/agent-workspace-snapshot.ts +265 -0
- package/src/input/agent-workspace-types.ts +142 -0
- package/src/input/agent-workspace.ts +22 -766
- package/src/input/commands/agent-runtime-profile-runtime.ts +1 -1
- package/src/input/commands/delegation-runtime.ts +1 -1
- package/src/input/commands/experience-runtime.ts +3 -4
- package/src/input/commands/guidance-runtime.ts +1 -2
- package/src/input/commands/health-runtime.ts +3 -65
- package/src/input/commands/knowledge.ts +7 -7
- package/src/input/commands/local-setup-review.ts +0 -61
- package/src/input/commands/local-setup-transfer.ts +0 -3
- package/src/input/commands/local-setup.ts +2 -15
- package/src/input/commands/planning-runtime.ts +4 -1
- package/src/input/commands/platform-access-runtime.ts +1 -10
- package/src/input/commands/platform-services-runtime.ts +0 -1
- package/src/input/commands/recall-query.ts +1 -1
- package/src/input/commands/routines-runtime.ts +10 -6
- package/src/input/commands/schedule-runtime.ts +10 -6
- package/src/input/commands/session-workflow.ts +1 -1
- package/src/input/commands/tasks-runtime.ts +1 -14
- package/src/input/commands.ts +0 -4
- package/src/input/handler-onboarding.ts +10 -120
- package/src/input/onboarding/onboarding-wizard-apply.ts +5 -196
- package/src/input/onboarding/onboarding-wizard-constants.ts +8 -119
- package/src/input/onboarding/onboarding-wizard-helpers.ts +2 -53
- package/src/input/onboarding/onboarding-wizard-rules.ts +2 -236
- package/src/input/onboarding/onboarding-wizard-state.ts +1 -69
- package/src/input/onboarding/onboarding-wizard-steps.ts +584 -737
- package/src/input/onboarding/onboarding-wizard-types.ts +8 -26
- package/src/input/onboarding/onboarding-wizard.ts +4 -109
- package/src/input/settings-modal-agent-policy.ts +10 -0
- package/src/input/settings-modal-types.ts +2 -4
- package/src/input/settings-modal.ts +3 -1
- package/src/input/submission-router.ts +0 -1
- package/src/panels/approval-panel.ts +1 -2
- package/src/panels/builtin/operations.ts +1 -2
- package/src/panels/knowledge-panel.ts +2 -2
- package/src/panels/project-planning-panel.ts +4 -1
- package/src/panels/provider-health-domains.ts +0 -22
- package/src/panels/provider-health-panel.ts +1 -5
- package/src/panels/session-browser-panel.ts +0 -5
- package/src/panels/tasks-panel.ts +2 -64
- package/src/renderer/agent-workspace.ts +1 -1
- package/src/renderer/help-overlay.ts +1 -2
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/settings-modal-helpers.ts +0 -16
- package/src/renderer/settings-modal.ts +3 -5
- package/src/runtime/bootstrap-hook-bridge.ts +0 -3
- package/src/runtime/bootstrap-shell.ts +2 -1
- package/src/runtime/bootstrap.ts +1 -1
- package/src/runtime/index.ts +0 -1
- package/src/runtime/onboarding/derivation.ts +1 -28
- package/src/runtime/onboarding/snapshot.ts +0 -1
- package/src/runtime/onboarding/types.ts +1 -4
- package/src/runtime/services.ts +4 -23
- package/src/runtime/ui-read-models.ts +4 -3
- package/src/shell/service-settings-sync.ts +15 -244
- package/src/tools/agent-context-policy.ts +1 -1
- package/src/tools/wrfc-agent-guard.ts +3 -3
- package/src/verification/live-verifier.ts +11 -5
- package/src/verification/verification-ledger.ts +3 -6
- package/src/version.ts +1 -1
- package/src/input/commands/agent-externalized-tui.ts +0 -73
- package/src/input/commands/cloudflare-runtime.ts +0 -385
- package/src/input/handler-onboarding-cloudflare.ts +0 -322
- package/src/input/onboarding/onboarding-runtime-status.ts +0 -87
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +0 -494
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +0 -199
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +0 -130
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +0 -762
- package/src/runtime/cloudflare-control-plane.ts +0 -350
- package/src/runtime/sandbox-public-gaps.ts +0 -358
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { 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';
|
|
6
5
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
7
|
-
import type { ShellPathService } from '@/runtime/index.ts';
|
|
8
6
|
import { getModelIdFromProviderModel, getProviderIdFromModel } from '../config/provider-model.ts';
|
|
9
|
-
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
10
7
|
import { SDK_VERSION } from '../version.ts';
|
|
11
8
|
import type { AgentRoutineRecord } from './routine-registry.ts';
|
|
12
9
|
|
|
@@ -16,7 +13,6 @@ export const ROUTINE_SCHEDULE_LIST_METHOD = 'schedules.list';
|
|
|
16
13
|
|
|
17
14
|
type ScheduleCreateInput = OperatorMethodInput<'schedules.create'>;
|
|
18
15
|
type ScheduleCreateOutput = OperatorMethodOutput<'schedules.create'>;
|
|
19
|
-
type ScheduleListOutput = OperatorMethodOutput<'schedules.list'>;
|
|
20
16
|
type ScheduleDeliveryInput = NonNullable<ScheduleCreateInput['delivery']>;
|
|
21
17
|
type ScheduleDeliveryTargetInput = ScheduleDeliveryInput['targets'] extends readonly (infer T)[] ? T : never;
|
|
22
18
|
|
|
@@ -210,30 +206,6 @@ export type RoutineScheduleCorrelationResult =
|
|
|
210
206
|
| RoutineScheduleCorrelationSuccess
|
|
211
207
|
| RoutineScheduleCorrelationFailure;
|
|
212
208
|
|
|
213
|
-
interface RoutineScheduleReceiptStoreFile {
|
|
214
|
-
readonly version: 1;
|
|
215
|
-
readonly receipts: readonly RoutineScheduleReceipt[];
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const RECEIPT_STORE_VERSION = 1;
|
|
219
|
-
const DELIVERY_SURFACE_KINDS: readonly RoutineScheduleDeliverySurfaceKind[] = [
|
|
220
|
-
'tui',
|
|
221
|
-
'web',
|
|
222
|
-
'slack',
|
|
223
|
-
'discord',
|
|
224
|
-
'ntfy',
|
|
225
|
-
'webhook',
|
|
226
|
-
'telegram',
|
|
227
|
-
'google-chat',
|
|
228
|
-
'signal',
|
|
229
|
-
'whatsapp',
|
|
230
|
-
'imessage',
|
|
231
|
-
'msteams',
|
|
232
|
-
'bluebubbles',
|
|
233
|
-
'mattermost',
|
|
234
|
-
'matrix',
|
|
235
|
-
'service',
|
|
236
|
-
];
|
|
237
209
|
|
|
238
210
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
239
211
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -244,30 +216,6 @@ function readString(record: Record<string, unknown>, key: string): string | null
|
|
|
244
216
|
return typeof value === 'string' ? value : null;
|
|
245
217
|
}
|
|
246
218
|
|
|
247
|
-
function readBoolean(record: Record<string, unknown>, key: string): boolean | undefined {
|
|
248
|
-
const value = record[key];
|
|
249
|
-
return typeof value === 'boolean' ? value : undefined;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function readNumber(record: Record<string, unknown>, key: string): number | undefined {
|
|
253
|
-
const value = record[key];
|
|
254
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function nowIso(): string {
|
|
258
|
-
return new Date().toISOString();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function optionValue(args: readonly string[], index: number, inlineValue: string | undefined): {
|
|
262
|
-
readonly value: string | undefined;
|
|
263
|
-
readonly nextIndex: number;
|
|
264
|
-
} {
|
|
265
|
-
if (inlineValue !== undefined) return { value: inlineValue, nextIndex: index };
|
|
266
|
-
const next = args[index + 1];
|
|
267
|
-
if (next === undefined || next.startsWith('--')) return { value: undefined, nextIndex: index };
|
|
268
|
-
return { value: next, nextIndex: index + 1 };
|
|
269
|
-
}
|
|
270
|
-
|
|
271
219
|
function normalizeProviderModel(provider: string | undefined, model: string | undefined): {
|
|
272
220
|
readonly provider?: string;
|
|
273
221
|
readonly model?: string;
|
|
@@ -280,63 +228,6 @@ function normalizeProviderModel(provider: string | undefined, model: string | un
|
|
|
280
228
|
};
|
|
281
229
|
}
|
|
282
230
|
|
|
283
|
-
function isDeliverySurfaceKind(value: string): value is RoutineScheduleDeliverySurfaceKind {
|
|
284
|
-
return DELIVERY_SURFACE_KINDS.includes(value as RoutineScheduleDeliverySurfaceKind);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function parseSurfaceDeliveryTarget(raw: string): RoutineScheduleDeliveryTargetSpec | string {
|
|
288
|
-
const [surfaceKind = '', routeId, label] = raw.split(':');
|
|
289
|
-
if (!isDeliverySurfaceKind(surfaceKind)) {
|
|
290
|
-
return `Unsupported delivery surface "${surfaceKind}".`;
|
|
291
|
-
}
|
|
292
|
-
return {
|
|
293
|
-
kind: 'surface',
|
|
294
|
-
surfaceKind,
|
|
295
|
-
routeId: routeId?.trim() || undefined,
|
|
296
|
-
label: label?.trim() || undefined,
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function parseRouteDeliveryTarget(raw: string): RoutineScheduleDeliveryTargetSpec | string {
|
|
301
|
-
const [routeId = '', label] = raw.split(':');
|
|
302
|
-
const normalizedRouteId = routeId.trim();
|
|
303
|
-
if (!normalizedRouteId) return '--delivery-route requires a route id.';
|
|
304
|
-
return {
|
|
305
|
-
kind: 'surface',
|
|
306
|
-
routeId: normalizedRouteId,
|
|
307
|
-
label: label?.trim() || undefined,
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function parseWebhookDeliveryTarget(raw: string): RoutineScheduleDeliveryTargetSpec | string {
|
|
312
|
-
const normalized = raw.trim();
|
|
313
|
-
if (!normalized) return '--delivery-webhook requires a URL.';
|
|
314
|
-
try {
|
|
315
|
-
const url = new URL(normalized);
|
|
316
|
-
if (url.protocol !== 'https:' && url.protocol !== 'http:') return '--delivery-webhook must be an http(s) URL.';
|
|
317
|
-
} catch {
|
|
318
|
-
return '--delivery-webhook must be a valid URL.';
|
|
319
|
-
}
|
|
320
|
-
return {
|
|
321
|
-
kind: 'webhook',
|
|
322
|
-
address: normalized,
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function parseLinkDeliveryTarget(raw: string): RoutineScheduleDeliveryTargetSpec | string {
|
|
327
|
-
const normalized = raw.trim();
|
|
328
|
-
if (!normalized) return '--delivery-link requires a URL or label.';
|
|
329
|
-
return {
|
|
330
|
-
kind: 'link',
|
|
331
|
-
address: normalized,
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function validateDeliveryTargets(targets: readonly RoutineScheduleDeliveryTargetSpec[]): string | null {
|
|
336
|
-
const kinds = new Set(targets.map((target) => target.kind));
|
|
337
|
-
return kinds.size > 1 ? 'Use one delivery target kind per routine promotion command.' : null;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
231
|
function deliveryModeFromTargets(targets: readonly RoutineScheduleDeliveryTargetSpec[]): ScheduleDeliveryInput['mode'] {
|
|
341
232
|
const first = targets[0];
|
|
342
233
|
return first ? first.kind : 'none';
|
|
@@ -352,488 +243,6 @@ function toDeliveryTargetInput(target: RoutineScheduleDeliveryTargetSpec): Sched
|
|
|
352
243
|
};
|
|
353
244
|
}
|
|
354
245
|
|
|
355
|
-
function redactedDeliveryAddress(address: string | undefined): string | undefined {
|
|
356
|
-
if (!address) return undefined;
|
|
357
|
-
try {
|
|
358
|
-
const url = new URL(address);
|
|
359
|
-
return `${url.protocol}//${url.host}/...`;
|
|
360
|
-
} catch {
|
|
361
|
-
return '[redacted]';
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function redactedDeliveryTargets(delivery: ScheduleDeliveryInput | undefined): readonly RoutineScheduleReceiptDeliveryTarget[] | undefined {
|
|
366
|
-
if (!delivery) return undefined;
|
|
367
|
-
return delivery.targets.map((target) => ({
|
|
368
|
-
kind: target.kind as RoutineScheduleDeliveryKind,
|
|
369
|
-
surfaceKind: typeof target.surfaceKind === 'string' && isDeliverySurfaceKind(target.surfaceKind) ? target.surfaceKind : undefined,
|
|
370
|
-
address: redactedDeliveryAddress(typeof target.address === 'string' ? target.address : undefined),
|
|
371
|
-
routeId: typeof target.routeId === 'string' ? target.routeId : undefined,
|
|
372
|
-
label: typeof target.label === 'string' ? target.label : undefined,
|
|
373
|
-
}));
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function readReceipt(value: unknown): RoutineScheduleReceipt | null {
|
|
377
|
-
if (!isRecord(value)) return null;
|
|
378
|
-
const id = readString(value, 'id')?.trim();
|
|
379
|
-
const createdAt = readString(value, 'createdAt')?.trim();
|
|
380
|
-
const routineId = readString(value, 'routineId')?.trim();
|
|
381
|
-
const routineName = readString(value, 'routineName')?.trim();
|
|
382
|
-
const status = value.status === 'created' || value.status === 'failed' ? value.status : null;
|
|
383
|
-
const scheduleKind = value.scheduleKind === 'cron' || value.scheduleKind === 'every' || value.scheduleKind === 'at'
|
|
384
|
-
? value.scheduleKind
|
|
385
|
-
: null;
|
|
386
|
-
const scheduleValue = readString(value, 'scheduleValue')?.trim();
|
|
387
|
-
const target = isRecord(value.target) ? value.target : {};
|
|
388
|
-
const deliveryTargets = Array.isArray(value.deliveryTargets)
|
|
389
|
-
? value.deliveryTargets.map((target): RoutineScheduleReceiptDeliveryTarget | null => {
|
|
390
|
-
if (!isRecord(target)) return null;
|
|
391
|
-
const kind = target.kind === 'webhook' || target.kind === 'surface' || target.kind === 'integration' || target.kind === 'link'
|
|
392
|
-
? target.kind
|
|
393
|
-
: null;
|
|
394
|
-
if (!kind) return null;
|
|
395
|
-
const surfaceKind = readString(target, 'surfaceKind') ?? undefined;
|
|
396
|
-
return {
|
|
397
|
-
kind,
|
|
398
|
-
surfaceKind: surfaceKind && isDeliverySurfaceKind(surfaceKind) ? surfaceKind : undefined,
|
|
399
|
-
address: readString(target, 'address') ?? undefined,
|
|
400
|
-
routeId: readString(target, 'routeId') ?? undefined,
|
|
401
|
-
label: readString(target, 'label') ?? undefined,
|
|
402
|
-
};
|
|
403
|
-
}).filter((target): target is RoutineScheduleReceiptDeliveryTarget => target !== null)
|
|
404
|
-
: undefined;
|
|
405
|
-
if (!id || !createdAt || !routineId || !routineName || !status || !scheduleKind || !scheduleValue) return null;
|
|
406
|
-
return {
|
|
407
|
-
id,
|
|
408
|
-
createdAt,
|
|
409
|
-
routineId,
|
|
410
|
-
routineName,
|
|
411
|
-
route: ROUTINE_SCHEDULE_ROUTE,
|
|
412
|
-
method: ROUTINE_SCHEDULE_METHOD,
|
|
413
|
-
status,
|
|
414
|
-
daemonBaseUrl: readString(value, 'daemonBaseUrl') ?? '',
|
|
415
|
-
scheduleId: readString(value, 'scheduleId') ?? undefined,
|
|
416
|
-
scheduleStatus: readString(value, 'scheduleStatus') ?? undefined,
|
|
417
|
-
scheduleName: readString(value, 'scheduleName') ?? routineName,
|
|
418
|
-
scheduleKind,
|
|
419
|
-
scheduleValue,
|
|
420
|
-
timezone: readString(value, 'timezone') ?? undefined,
|
|
421
|
-
provider: readString(value, 'provider') ?? undefined,
|
|
422
|
-
model: readString(value, 'model') ?? undefined,
|
|
423
|
-
enabled: value.enabled !== false,
|
|
424
|
-
target: {
|
|
425
|
-
kind: readString(target, 'kind') ?? undefined,
|
|
426
|
-
surfaceKind: readString(target, 'surfaceKind') ?? undefined,
|
|
427
|
-
preserveThread: readBoolean(target, 'preserveThread'),
|
|
428
|
-
createIfMissing: readBoolean(target, 'createIfMissing'),
|
|
429
|
-
},
|
|
430
|
-
deliveryMode: readString(value, 'deliveryMode') ?? undefined,
|
|
431
|
-
deliveryTargets,
|
|
432
|
-
failureKind: value.failureKind === 'confirmation_required'
|
|
433
|
-
|| value.failureKind === 'auth_required'
|
|
434
|
-
|| value.failureKind === 'daemon_unavailable'
|
|
435
|
-
|| value.failureKind === 'version_mismatch'
|
|
436
|
-
|| value.failureKind === 'daemon_route_unavailable'
|
|
437
|
-
|| value.failureKind === 'daemon_error'
|
|
438
|
-
? value.failureKind
|
|
439
|
-
: undefined,
|
|
440
|
-
failureError: readString(value, 'failureError') ?? undefined,
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function parseReceiptStore(raw: string): RoutineScheduleReceiptStoreFile {
|
|
445
|
-
const parsed: unknown = JSON.parse(raw);
|
|
446
|
-
if (!isRecord(parsed)) return { version: RECEIPT_STORE_VERSION, receipts: [] };
|
|
447
|
-
return {
|
|
448
|
-
version: RECEIPT_STORE_VERSION,
|
|
449
|
-
receipts: Array.isArray(parsed.receipts)
|
|
450
|
-
? parsed.receipts.map(readReceipt).filter((receipt): receipt is RoutineScheduleReceipt => receipt !== null)
|
|
451
|
-
: [],
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function formatReceiptStore(store: RoutineScheduleReceiptStoreFile): string {
|
|
456
|
-
return `${JSON.stringify(store, null, 2)}\n`;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function receiptId(createdAt: string, routineId: string, existing: readonly RoutineScheduleReceipt[]): string {
|
|
460
|
-
const dayStamp = createdAt.slice(0, 10).replace(/-/g, '');
|
|
461
|
-
const base = `routine-schedule-${routineId}-${dayStamp}`.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
462
|
-
const ids = new Set(existing.map((receipt) => receipt.id));
|
|
463
|
-
if (!ids.has(base)) return base;
|
|
464
|
-
for (let index = 2; index < 1000; index += 1) {
|
|
465
|
-
const candidate = `${base}-${index}`;
|
|
466
|
-
if (!ids.has(candidate)) return candidate;
|
|
467
|
-
}
|
|
468
|
-
return `${base}-${existing.length + 1}`;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function scheduleValue(payload: ScheduleCreateInput): string {
|
|
472
|
-
if (payload.kind === 'cron') return String(payload.cron ?? '');
|
|
473
|
-
if (payload.kind === 'every') return String(payload.every ?? '');
|
|
474
|
-
return String(payload.at ?? '');
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function scheduleKind(payload: ScheduleCreateInput): RoutineScheduleKind {
|
|
478
|
-
if (payload.kind === 'cron' || payload.kind === 'every' || payload.kind === 'at') return payload.kind;
|
|
479
|
-
throw new Error('Routine schedule payload is missing a schedule kind.');
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function targetSummary(payload: ScheduleCreateInput): RoutineScheduleReceipt['target'] {
|
|
483
|
-
return isRecord(payload.target)
|
|
484
|
-
? {
|
|
485
|
-
kind: typeof payload.target.kind === 'string' ? payload.target.kind : undefined,
|
|
486
|
-
surfaceKind: typeof payload.target.surfaceKind === 'string' ? payload.target.surfaceKind : undefined,
|
|
487
|
-
preserveThread: typeof payload.target.preserveThread === 'boolean' ? payload.target.preserveThread : undefined,
|
|
488
|
-
createIfMissing: typeof payload.target.createIfMissing === 'boolean' ? payload.target.createIfMissing : undefined,
|
|
489
|
-
}
|
|
490
|
-
: {};
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function deliveryMode(payload: ScheduleCreateInput): string | undefined {
|
|
494
|
-
return isRecord(payload.delivery) && typeof payload.delivery.mode === 'string' ? payload.delivery.mode : undefined;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function resultScheduleRecord(result: RoutineSchedulePromotionResult): Record<string, unknown> {
|
|
498
|
-
return result.ok && isRecord(result.schedule) ? result.schedule : {};
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function normalizeForMatch(value: string | undefined): string {
|
|
502
|
-
return (value ?? '').trim().toLowerCase();
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function isoTime(value: string): string | null {
|
|
506
|
-
const time = new Date(value).getTime();
|
|
507
|
-
return Number.isFinite(time) ? new Date(time).toISOString() : null;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function readLiveScheduleDefinition(value: unknown): {
|
|
511
|
-
readonly kind?: RoutineScheduleKind;
|
|
512
|
-
readonly value?: string;
|
|
513
|
-
readonly timezone?: string;
|
|
514
|
-
} {
|
|
515
|
-
if (!isRecord(value)) return {};
|
|
516
|
-
if (value.kind === 'cron') {
|
|
517
|
-
return {
|
|
518
|
-
kind: 'cron',
|
|
519
|
-
value: readString(value, 'expression') ?? undefined,
|
|
520
|
-
timezone: readString(value, 'timezone') ?? undefined,
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
if (value.kind === 'every') {
|
|
524
|
-
const intervalMs = readNumber(value, 'intervalMs');
|
|
525
|
-
return {
|
|
526
|
-
kind: 'every',
|
|
527
|
-
value: intervalMs === undefined ? undefined : formatEveryInterval(intervalMs),
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
if (value.kind === 'at') {
|
|
531
|
-
const at = readNumber(value, 'at');
|
|
532
|
-
return {
|
|
533
|
-
kind: 'at',
|
|
534
|
-
value: at === undefined ? undefined : new Date(at).toISOString(),
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
return {};
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
function readLiveScheduleRecord(value: unknown): RoutineScheduleLiveRecord | null {
|
|
541
|
-
if (!isRecord(value)) return null;
|
|
542
|
-
const id = readString(value, 'id')?.trim();
|
|
543
|
-
const name = readString(value, 'name')?.trim();
|
|
544
|
-
if (!id || !name) return null;
|
|
545
|
-
const schedule = readLiveScheduleDefinition(value.schedule);
|
|
546
|
-
return {
|
|
547
|
-
id,
|
|
548
|
-
name,
|
|
549
|
-
status: readString(value, 'status') ?? undefined,
|
|
550
|
-
enabled: readBoolean(value, 'enabled'),
|
|
551
|
-
scheduleKind: schedule.kind,
|
|
552
|
-
scheduleValue: schedule.value,
|
|
553
|
-
timezone: schedule.timezone,
|
|
554
|
-
nextRunAt: readNumber(value, 'nextRunAt'),
|
|
555
|
-
lastRunAt: readNumber(value, 'lastRunAt'),
|
|
556
|
-
runCount: readNumber(value, 'runCount'),
|
|
557
|
-
successCount: readNumber(value, 'successCount'),
|
|
558
|
-
failureCount: readNumber(value, 'failureCount'),
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function readLiveSchedules(output: ScheduleListOutput): readonly RoutineScheduleLiveRecord[] {
|
|
563
|
-
const record: Record<string, unknown> = isRecord(output) ? output : {};
|
|
564
|
-
const jobs: readonly unknown[] = Array.isArray(record.jobs) ? record.jobs : [];
|
|
565
|
-
return jobs.map(readLiveScheduleRecord).filter((schedule): schedule is RoutineScheduleLiveRecord => schedule !== null);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
function cadenceMatches(receipt: RoutineScheduleReceipt, schedule: RoutineScheduleLiveRecord): boolean {
|
|
569
|
-
if (receipt.scheduleKind !== schedule.scheduleKind) return false;
|
|
570
|
-
if (receipt.scheduleKind === 'at') {
|
|
571
|
-
const left = isoTime(receipt.scheduleValue);
|
|
572
|
-
const right = schedule.scheduleValue ? isoTime(schedule.scheduleValue) : null;
|
|
573
|
-
return Boolean(left && right && left === right);
|
|
574
|
-
}
|
|
575
|
-
return normalizeForMatch(receipt.scheduleValue) === normalizeForMatch(schedule.scheduleValue);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function findScheduleForReceipt(
|
|
579
|
-
receipt: RoutineScheduleReceipt,
|
|
580
|
-
schedules: readonly RoutineScheduleLiveRecord[],
|
|
581
|
-
): { readonly reason: RoutineScheduleCorrelation['matchReason']; readonly schedule?: RoutineScheduleLiveRecord } {
|
|
582
|
-
if (receipt.scheduleId) {
|
|
583
|
-
const byId = schedules.find((schedule) => schedule.id === receipt.scheduleId);
|
|
584
|
-
if (byId) return { reason: 'schedule-id', schedule: byId };
|
|
585
|
-
}
|
|
586
|
-
const byNameAndCadence = schedules.find((schedule) => (
|
|
587
|
-
normalizeForMatch(schedule.name) === normalizeForMatch(receipt.scheduleName)
|
|
588
|
-
&& cadenceMatches(receipt, schedule)
|
|
589
|
-
));
|
|
590
|
-
return byNameAndCadence
|
|
591
|
-
? { reason: 'name-and-cadence', schedule: byNameAndCadence }
|
|
592
|
-
: { reason: 'not-found' };
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
function correlateReceipts(
|
|
596
|
-
receipts: readonly RoutineScheduleReceipt[],
|
|
597
|
-
schedules: readonly RoutineScheduleLiveRecord[],
|
|
598
|
-
): readonly RoutineScheduleCorrelation[] {
|
|
599
|
-
return receipts.map((receipt) => {
|
|
600
|
-
if (receipt.status === 'failed') {
|
|
601
|
-
return {
|
|
602
|
-
receipt,
|
|
603
|
-
liveStatus: 'failed-receipt',
|
|
604
|
-
matchReason: 'failed-receipt',
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
const match = findScheduleForReceipt(receipt, schedules);
|
|
608
|
-
return match.schedule
|
|
609
|
-
? {
|
|
610
|
-
receipt,
|
|
611
|
-
liveStatus: 'matched',
|
|
612
|
-
matchReason: match.reason,
|
|
613
|
-
schedule: match.schedule,
|
|
614
|
-
}
|
|
615
|
-
: {
|
|
616
|
-
receipt,
|
|
617
|
-
liveStatus: 'missing',
|
|
618
|
-
matchReason: 'not-found',
|
|
619
|
-
};
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function buildReceipt(
|
|
624
|
-
existing: readonly RoutineScheduleReceipt[],
|
|
625
|
-
connection: AgentDaemonConnection,
|
|
626
|
-
preview: RoutineSchedulePromotionPreview,
|
|
627
|
-
result: RoutineSchedulePromotionResult,
|
|
628
|
-
): RoutineScheduleReceipt {
|
|
629
|
-
const createdAt = nowIso();
|
|
630
|
-
const kind = scheduleKind(preview.payload);
|
|
631
|
-
const schedule = resultScheduleRecord(result);
|
|
632
|
-
const scheduleId = readString(schedule, 'id') ?? undefined;
|
|
633
|
-
const scheduleStatus = readString(schedule, 'status') ?? (schedule.enabled === false ? 'paused' : schedule.enabled === true ? 'enabled' : undefined);
|
|
634
|
-
return {
|
|
635
|
-
id: receiptId(createdAt, preview.routineId, existing),
|
|
636
|
-
createdAt,
|
|
637
|
-
routineId: preview.routineId,
|
|
638
|
-
routineName: preview.routineName,
|
|
639
|
-
route: ROUTINE_SCHEDULE_ROUTE,
|
|
640
|
-
method: ROUTINE_SCHEDULE_METHOD,
|
|
641
|
-
status: result.ok ? 'created' : 'failed',
|
|
642
|
-
daemonBaseUrl: connection.baseUrl,
|
|
643
|
-
scheduleId,
|
|
644
|
-
scheduleStatus,
|
|
645
|
-
scheduleName: String(preview.payload.name ?? `Agent routine: ${preview.routineName}`),
|
|
646
|
-
scheduleKind: kind,
|
|
647
|
-
scheduleValue: scheduleValue(preview.payload),
|
|
648
|
-
timezone: kind === 'cron' ? preview.payload.timezone : undefined,
|
|
649
|
-
provider: preview.payload.provider,
|
|
650
|
-
model: preview.payload.model,
|
|
651
|
-
enabled: preview.payload.enabled !== false,
|
|
652
|
-
target: targetSummary(preview.payload),
|
|
653
|
-
deliveryMode: deliveryMode(preview.payload),
|
|
654
|
-
deliveryTargets: redactedDeliveryTargets(preview.payload.delivery),
|
|
655
|
-
failureKind: result.ok ? undefined : result.kind,
|
|
656
|
-
failureError: result.ok ? undefined : result.error,
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
export function routineScheduleReceiptStorePath(shellPaths: ShellPathService): string {
|
|
661
|
-
return shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'routines', 'schedule-receipts.json');
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
export class RoutineScheduleReceiptStore {
|
|
665
|
-
public constructor(private readonly storePath: string) {}
|
|
666
|
-
|
|
667
|
-
public static fromShellPaths(shellPaths: ShellPathService): RoutineScheduleReceiptStore {
|
|
668
|
-
return new RoutineScheduleReceiptStore(routineScheduleReceiptStorePath(shellPaths));
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
public snapshot(): RoutineScheduleReceiptSnapshot {
|
|
672
|
-
const store = this.readStore();
|
|
673
|
-
return {
|
|
674
|
-
path: this.storePath,
|
|
675
|
-
receipts: [...store.receipts].sort((left, right) => right.createdAt.localeCompare(left.createdAt)),
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
public get(id: string): RoutineScheduleReceipt | null {
|
|
680
|
-
const normalized = id.trim().toLowerCase();
|
|
681
|
-
if (!normalized) return null;
|
|
682
|
-
return this.snapshot().receipts.find((receipt) => receipt.id.toLowerCase() === normalized) ?? null;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
public append(connection: AgentDaemonConnection, preview: RoutineSchedulePromotionPreview, result: RoutineSchedulePromotionResult): RoutineScheduleReceipt {
|
|
686
|
-
const store = this.readStore();
|
|
687
|
-
const receipt = buildReceipt(store.receipts, connection, preview, result);
|
|
688
|
-
this.writeStore({ ...store, receipts: [...store.receipts, receipt] });
|
|
689
|
-
return receipt;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
private readStore(): RoutineScheduleReceiptStoreFile {
|
|
693
|
-
if (!existsSync(this.storePath)) return { version: RECEIPT_STORE_VERSION, receipts: [] };
|
|
694
|
-
try {
|
|
695
|
-
return parseReceiptStore(readFileSync(this.storePath, 'utf-8'));
|
|
696
|
-
} catch (error) {
|
|
697
|
-
throw new Error(`Could not read Agent routine schedule receipt store: ${summarizeError(error)}`);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
private writeStore(store: RoutineScheduleReceiptStoreFile): void {
|
|
702
|
-
mkdirSync(dirname(this.storePath), { recursive: true });
|
|
703
|
-
const tmpPath = `${this.storePath}.tmp`;
|
|
704
|
-
writeFileSync(tmpPath, formatReceiptStore(store), 'utf-8');
|
|
705
|
-
renameSync(tmpPath, this.storePath);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
export function parseRoutineSchedulePromotionArgs(args: readonly string[]): ParsedRoutineSchedulePromotionArgs {
|
|
710
|
-
let routineId: string | null = null;
|
|
711
|
-
let schedule: RoutineScheduleSpec | null = null;
|
|
712
|
-
const deliveryTargets: RoutineScheduleDeliveryTargetSpec[] = [];
|
|
713
|
-
let name: string | undefined;
|
|
714
|
-
let timezone: string | undefined;
|
|
715
|
-
let provider: string | undefined;
|
|
716
|
-
let model: string | undefined;
|
|
717
|
-
let enabled = true;
|
|
718
|
-
let yes = false;
|
|
719
|
-
const errors: string[] = [];
|
|
720
|
-
|
|
721
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
722
|
-
const raw = args[index] ?? '';
|
|
723
|
-
const equals = raw.indexOf('=');
|
|
724
|
-
const optionName = equals >= 0 ? raw.slice(0, equals) : raw;
|
|
725
|
-
const inlineValue = equals >= 0 ? raw.slice(equals + 1) : undefined;
|
|
726
|
-
|
|
727
|
-
if (raw === '--yes') {
|
|
728
|
-
yes = true;
|
|
729
|
-
continue;
|
|
730
|
-
}
|
|
731
|
-
if (raw === '--disabled') {
|
|
732
|
-
enabled = false;
|
|
733
|
-
continue;
|
|
734
|
-
}
|
|
735
|
-
if (optionName === '--name' || optionName === '--timezone' || optionName === '--provider' || optionName === '--model') {
|
|
736
|
-
const consumed = optionValue(args, index, inlineValue);
|
|
737
|
-
index = consumed.nextIndex;
|
|
738
|
-
const value = consumed.value?.trim();
|
|
739
|
-
if (!value) {
|
|
740
|
-
errors.push(`${optionName} requires a value.`);
|
|
741
|
-
continue;
|
|
742
|
-
}
|
|
743
|
-
if (optionName === '--name') name = value;
|
|
744
|
-
if (optionName === '--timezone') timezone = value;
|
|
745
|
-
if (optionName === '--provider') provider = value;
|
|
746
|
-
if (optionName === '--model') model = value;
|
|
747
|
-
continue;
|
|
748
|
-
}
|
|
749
|
-
if (optionName === '--cron' || optionName === '--every' || optionName === '--at') {
|
|
750
|
-
const consumed = optionValue(args, index, inlineValue);
|
|
751
|
-
index = consumed.nextIndex;
|
|
752
|
-
const value = consumed.value?.trim();
|
|
753
|
-
if (!value) {
|
|
754
|
-
errors.push(`${optionName} requires a value.`);
|
|
755
|
-
continue;
|
|
756
|
-
}
|
|
757
|
-
if (schedule) {
|
|
758
|
-
errors.push('Choose exactly one schedule selector: --cron, --every, or --at.');
|
|
759
|
-
continue;
|
|
760
|
-
}
|
|
761
|
-
schedule = {
|
|
762
|
-
kind: optionName === '--cron' ? 'cron' : optionName === '--every' ? 'every' : 'at',
|
|
763
|
-
value,
|
|
764
|
-
};
|
|
765
|
-
continue;
|
|
766
|
-
}
|
|
767
|
-
if (optionName === '--delivery-surface' || optionName === '--deliver-surface') {
|
|
768
|
-
const consumed = optionValue(args, index, inlineValue);
|
|
769
|
-
index = consumed.nextIndex;
|
|
770
|
-
const value = consumed.value?.trim();
|
|
771
|
-
if (!value) {
|
|
772
|
-
errors.push(`${optionName} requires a value.`);
|
|
773
|
-
continue;
|
|
774
|
-
}
|
|
775
|
-
const target = parseSurfaceDeliveryTarget(value);
|
|
776
|
-
if (typeof target === 'string') errors.push(target);
|
|
777
|
-
else deliveryTargets.push(target);
|
|
778
|
-
continue;
|
|
779
|
-
}
|
|
780
|
-
if (optionName === '--delivery-route' || optionName === '--deliver-route') {
|
|
781
|
-
const consumed = optionValue(args, index, inlineValue);
|
|
782
|
-
index = consumed.nextIndex;
|
|
783
|
-
const value = consumed.value?.trim();
|
|
784
|
-
if (!value) {
|
|
785
|
-
errors.push(`${optionName} requires a value.`);
|
|
786
|
-
continue;
|
|
787
|
-
}
|
|
788
|
-
const target = parseRouteDeliveryTarget(value);
|
|
789
|
-
if (typeof target === 'string') errors.push(target);
|
|
790
|
-
else deliveryTargets.push(target);
|
|
791
|
-
continue;
|
|
792
|
-
}
|
|
793
|
-
if (optionName === '--delivery-webhook' || optionName === '--deliver-webhook') {
|
|
794
|
-
const consumed = optionValue(args, index, inlineValue);
|
|
795
|
-
index = consumed.nextIndex;
|
|
796
|
-
const value = consumed.value?.trim();
|
|
797
|
-
if (!value) {
|
|
798
|
-
errors.push(`${optionName} requires a value.`);
|
|
799
|
-
continue;
|
|
800
|
-
}
|
|
801
|
-
const target = parseWebhookDeliveryTarget(value);
|
|
802
|
-
if (typeof target === 'string') errors.push(target);
|
|
803
|
-
else deliveryTargets.push(target);
|
|
804
|
-
continue;
|
|
805
|
-
}
|
|
806
|
-
if (optionName === '--delivery-link' || optionName === '--deliver-link') {
|
|
807
|
-
const consumed = optionValue(args, index, inlineValue);
|
|
808
|
-
index = consumed.nextIndex;
|
|
809
|
-
const value = consumed.value?.trim();
|
|
810
|
-
if (!value) {
|
|
811
|
-
errors.push(`${optionName} requires a value.`);
|
|
812
|
-
continue;
|
|
813
|
-
}
|
|
814
|
-
const target = parseLinkDeliveryTarget(value);
|
|
815
|
-
if (typeof target === 'string') errors.push(target);
|
|
816
|
-
else deliveryTargets.push(target);
|
|
817
|
-
continue;
|
|
818
|
-
}
|
|
819
|
-
if (raw.startsWith('--')) {
|
|
820
|
-
errors.push(`Unknown option: ${raw}`);
|
|
821
|
-
continue;
|
|
822
|
-
}
|
|
823
|
-
if (!routineId) {
|
|
824
|
-
routineId = raw;
|
|
825
|
-
continue;
|
|
826
|
-
}
|
|
827
|
-
errors.push(`Unexpected argument: ${raw}`);
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
if (!routineId) errors.push('Routine id or name is required.');
|
|
831
|
-
if (!schedule) errors.push('Schedule is required: use --cron <expr>, --every <interval>, or --at <iso-time>.');
|
|
832
|
-
const deliveryError = validateDeliveryTargets(deliveryTargets);
|
|
833
|
-
if (deliveryError) errors.push(deliveryError);
|
|
834
|
-
return { routineId, schedule, deliveryTargets, name, timezone, provider, model, enabled, yes, errors };
|
|
835
|
-
}
|
|
836
|
-
|
|
837
246
|
export function resolveAgentDaemonConnection(
|
|
838
247
|
configManager: AgentDaemonConfigReader,
|
|
839
248
|
homeDirectory: string,
|
|
@@ -863,7 +272,7 @@ export function buildRoutineSchedulePrompt(routine: AgentRoutineRecord): string
|
|
|
863
272
|
'',
|
|
864
273
|
'Operator policy:',
|
|
865
274
|
'- Run this as a serial GoodVibes Agent operator routine.',
|
|
866
|
-
'- Use isolated Agent Knowledge routes only; never use default Knowledge/Wiki or
|
|
275
|
+
'- Use isolated Agent Knowledge routes only; never use default Knowledge/Wiki or non-Agent knowledge spaces as fallback.',
|
|
867
276
|
'- Do not perform destructive, costly, externally visible, or secret-handling actions without explicit approval.',
|
|
868
277
|
'- Do not request WRFC unless this scheduled routine explicitly delegates build/fix/review work to GoodVibes TUI.',
|
|
869
278
|
'- Summarize what was checked, what changed, and what still needs user review.',
|
|
@@ -999,38 +408,6 @@ async function classifyScheduleError(
|
|
|
999
408
|
return { ok: false, kind: 'daemon_error', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
|
|
1000
409
|
}
|
|
1001
410
|
|
|
1002
|
-
async function classifyScheduleListError(
|
|
1003
|
-
error: unknown,
|
|
1004
|
-
connection: AgentDaemonConnection,
|
|
1005
|
-
): Promise<RoutineScheduleCorrelationFailure> {
|
|
1006
|
-
const message = summarizeError(error);
|
|
1007
|
-
const lower = message.toLowerCase();
|
|
1008
|
-
if (lower.includes('401') || lower.includes('unauthorized') || lower.includes('auth')) {
|
|
1009
|
-
return { ok: false, kind: 'auth_required', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
|
|
1010
|
-
}
|
|
1011
|
-
if (lower.includes('404') || lower.includes('not found')) {
|
|
1012
|
-
const daemon = await fetchDaemonStatus(connection);
|
|
1013
|
-
const record = isRecord(daemon.body) ? daemon.body : {};
|
|
1014
|
-
const daemonVersion = readString(record, 'version') ?? 'unknown';
|
|
1015
|
-
if (daemon.ok && daemonVersion !== SDK_VERSION) {
|
|
1016
|
-
return {
|
|
1017
|
-
ok: false,
|
|
1018
|
-
kind: 'version_mismatch',
|
|
1019
|
-
error: `External daemon SDK version ${daemonVersion} does not match Agent SDK pin ${SDK_VERSION}; schedules.list is unavailable.`,
|
|
1020
|
-
route: ROUTINE_SCHEDULE_ROUTE,
|
|
1021
|
-
baseUrl: connection.baseUrl,
|
|
1022
|
-
daemonVersion,
|
|
1023
|
-
expectedSdkVersion: SDK_VERSION,
|
|
1024
|
-
};
|
|
1025
|
-
}
|
|
1026
|
-
return { ok: false, kind: 'daemon_route_unavailable', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
|
|
1027
|
-
}
|
|
1028
|
-
if (lower.includes('fetch') || lower.includes('connect') || lower.includes('econnrefused')) {
|
|
1029
|
-
return { ok: false, kind: 'daemon_unavailable', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
|
|
1030
|
-
}
|
|
1031
|
-
return { ok: false, kind: 'daemon_error', error: message, route: ROUTINE_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
411
|
export async function promoteRoutineToDaemonSchedule(
|
|
1035
412
|
connection: AgentDaemonConnection,
|
|
1036
413
|
preview: RoutineSchedulePromotionPreview,
|
|
@@ -1060,188 +437,3 @@ export async function promoteRoutineToDaemonSchedule(
|
|
|
1060
437
|
return classifyScheduleError(error, connection);
|
|
1061
438
|
}
|
|
1062
439
|
}
|
|
1063
|
-
|
|
1064
|
-
export async function reconcileRoutineScheduleReceipts(
|
|
1065
|
-
connection: AgentDaemonConnection,
|
|
1066
|
-
snapshot: RoutineScheduleReceiptSnapshot,
|
|
1067
|
-
): Promise<RoutineScheduleCorrelationResult> {
|
|
1068
|
-
if (!connection.token) {
|
|
1069
|
-
return {
|
|
1070
|
-
ok: false,
|
|
1071
|
-
kind: 'auth_required',
|
|
1072
|
-
error: `No daemon operator token found at ${connection.tokenPath}`,
|
|
1073
|
-
route: ROUTINE_SCHEDULE_ROUTE,
|
|
1074
|
-
baseUrl: connection.baseUrl,
|
|
1075
|
-
};
|
|
1076
|
-
}
|
|
1077
|
-
try {
|
|
1078
|
-
const sdk = createBrowserGoodVibesSdk({ baseUrl: connection.baseUrl, authToken: connection.token });
|
|
1079
|
-
const output = await sdk.operator.invoke(ROUTINE_SCHEDULE_LIST_METHOD, {});
|
|
1080
|
-
const schedules = readLiveSchedules(output);
|
|
1081
|
-
return {
|
|
1082
|
-
ok: true,
|
|
1083
|
-
kind: ROUTINE_SCHEDULE_LIST_METHOD,
|
|
1084
|
-
route: ROUTINE_SCHEDULE_ROUTE,
|
|
1085
|
-
baseUrl: connection.baseUrl,
|
|
1086
|
-
scheduleCount: schedules.length,
|
|
1087
|
-
receiptCount: snapshot.receipts.length,
|
|
1088
|
-
correlations: correlateReceipts(snapshot.receipts, schedules),
|
|
1089
|
-
};
|
|
1090
|
-
} catch (error) {
|
|
1091
|
-
return classifyScheduleListError(error, connection);
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
export function formatRoutineSchedulePreview(preview: RoutineSchedulePromotionPreview): string {
|
|
1096
|
-
const schedule = preview.payload.kind === 'cron'
|
|
1097
|
-
? `${preview.payload.cron}${preview.payload.timezone ? ` [${preview.payload.timezone}]` : ''}`
|
|
1098
|
-
: preview.payload.kind === 'every'
|
|
1099
|
-
? String(preview.payload.every)
|
|
1100
|
-
: String(preview.payload.at);
|
|
1101
|
-
const delivery = preview.payload.delivery;
|
|
1102
|
-
const deliveryTargetCount = delivery?.targets.length ?? 0;
|
|
1103
|
-
return [
|
|
1104
|
-
'Daemon schedule preview for Agent routine',
|
|
1105
|
-
` routine: ${preview.routineName} (${preview.routineId})`,
|
|
1106
|
-
` route: ${preview.method} ${preview.route}`,
|
|
1107
|
-
` name: ${String(preview.payload.name ?? '(daemon default)')}`,
|
|
1108
|
-
` schedule: ${preview.payload.kind} ${schedule}`,
|
|
1109
|
-
` enabled: ${preview.payload.enabled === false ? 'no' : 'yes'}`,
|
|
1110
|
-
` delivery: ${delivery?.mode ?? 'none'}${deliveryTargetCount > 0 ? ` (${deliveryTargetCount} target${deliveryTargetCount === 1 ? '' : 's'})` : ''}`,
|
|
1111
|
-
' target: external daemon service/main conversation route',
|
|
1112
|
-
' policy: isolated Agent Knowledge only; no default wiki/HomeGraph fallback; no WRFC unless explicitly delegated',
|
|
1113
|
-
' next: rerun with --yes to create this daemon schedule',
|
|
1114
|
-
].join('\n');
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
export function formatRoutineScheduleSuccess(result: RoutineSchedulePromotionSuccess): string {
|
|
1118
|
-
const record: Record<string, unknown> = isRecord(result.schedule) ? result.schedule : {};
|
|
1119
|
-
const id = readString(record, 'id') ?? '(unknown)';
|
|
1120
|
-
const status = readString(record, 'status') ?? (record.enabled === false ? 'paused' : 'enabled');
|
|
1121
|
-
return [
|
|
1122
|
-
'Created daemon schedule for Agent routine',
|
|
1123
|
-
` routine: ${result.routineName} (${result.routineId})`,
|
|
1124
|
-
` schedule: ${id}`,
|
|
1125
|
-
` status: ${status}`,
|
|
1126
|
-
` route: ${result.kind} ${result.route}`,
|
|
1127
|
-
' next: inspect with /schedule list or daemon schedule observability',
|
|
1128
|
-
].join('\n');
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
export function formatRoutineScheduleReceipts(snapshot: RoutineScheduleReceiptSnapshot, limit = 10): string {
|
|
1132
|
-
const receipts = snapshot.receipts.slice(0, Math.max(1, limit));
|
|
1133
|
-
if (snapshot.receipts.length === 0) {
|
|
1134
|
-
return [
|
|
1135
|
-
'Agent routine schedule receipts',
|
|
1136
|
-
` store: ${snapshot.path}`,
|
|
1137
|
-
' No routine schedule promotions have been recorded yet.',
|
|
1138
|
-
' Create one with /schedule promote-routine <routine-id> --cron <expr> --yes.',
|
|
1139
|
-
].join('\n');
|
|
1140
|
-
}
|
|
1141
|
-
return [
|
|
1142
|
-
`Agent routine schedule receipts (${snapshot.receipts.length})`,
|
|
1143
|
-
` store: ${snapshot.path}`,
|
|
1144
|
-
...receipts.map((receipt) => {
|
|
1145
|
-
const schedule = receipt.scheduleId ? ` schedule=${receipt.scheduleId}` : '';
|
|
1146
|
-
const failure = receipt.status === 'failed' && receipt.failureKind ? ` failure=${receipt.failureKind}` : '';
|
|
1147
|
-
return ` ${receipt.id} ${receipt.status} ${receipt.scheduleKind} ${receipt.scheduleValue} routine=${receipt.routineId}${schedule}${failure}`;
|
|
1148
|
-
}),
|
|
1149
|
-
snapshot.receipts.length > receipts.length ? ` ...${snapshot.receipts.length - receipts.length} more` : '',
|
|
1150
|
-
].filter((line): line is string => Boolean(line)).join('\n');
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
export function formatRoutineScheduleReceipt(receipt: RoutineScheduleReceipt): string {
|
|
1154
|
-
return [
|
|
1155
|
-
`Agent routine schedule receipt ${receipt.id}`,
|
|
1156
|
-
` created: ${receipt.createdAt}`,
|
|
1157
|
-
` status: ${receipt.status}`,
|
|
1158
|
-
` routine: ${receipt.routineName} (${receipt.routineId})`,
|
|
1159
|
-
` route: ${receipt.method} ${receipt.route}`,
|
|
1160
|
-
` daemon: ${receipt.daemonBaseUrl}`,
|
|
1161
|
-
` schedule: ${receipt.scheduleName}${receipt.scheduleId ? ` (${receipt.scheduleId})` : ''}`,
|
|
1162
|
-
receipt.scheduleStatus ? ` schedule status: ${receipt.scheduleStatus}` : '',
|
|
1163
|
-
` cadence: ${receipt.scheduleKind} ${receipt.scheduleValue}${receipt.timezone ? ` [${receipt.timezone}]` : ''}`,
|
|
1164
|
-
` enabled: ${receipt.enabled ? 'yes' : 'no'}`,
|
|
1165
|
-
receipt.provider ? ` provider: ${receipt.provider}` : '',
|
|
1166
|
-
receipt.model ? ` model: ${receipt.model}` : '',
|
|
1167
|
-
` target: ${receipt.target.kind ?? 'unknown'}${receipt.target.surfaceKind ? `/${receipt.target.surfaceKind}` : ''}`,
|
|
1168
|
-
receipt.deliveryMode ? ` delivery: ${receipt.deliveryMode}` : '',
|
|
1169
|
-
...(receipt.deliveryTargets ?? []).map((target) => ` delivery target: ${target.kind}${target.surfaceKind ? `/${target.surfaceKind}` : ''}${target.routeId ? ` route=${target.routeId}` : ''}${target.address ? ` address=${target.address}` : ''}${target.label ? ` label=${target.label}` : ''}`),
|
|
1170
|
-
receipt.failureKind ? ` failure: ${receipt.failureKind}` : '',
|
|
1171
|
-
receipt.failureError ? ` error: ${receipt.failureError}` : '',
|
|
1172
|
-
].filter((line): line is string => Boolean(line)).join('\n');
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
export function formatRoutineScheduleCorrelation(result: RoutineScheduleCorrelationResult, limit = 10): string {
|
|
1176
|
-
if (!result.ok) {
|
|
1177
|
-
return [
|
|
1178
|
-
`Daemon schedule reconciliation error: ${result.kind}`,
|
|
1179
|
-
` ${result.error}`,
|
|
1180
|
-
result.baseUrl ? ` daemon: ${result.baseUrl}` : null,
|
|
1181
|
-
` route: ${ROUTINE_SCHEDULE_LIST_METHOD} ${result.route}`,
|
|
1182
|
-
result.kind === 'auth_required'
|
|
1183
|
-
? ' next: pair/authenticate with the externally managed GoodVibes daemon, then retry.'
|
|
1184
|
-
: null,
|
|
1185
|
-
result.kind === 'daemon_unavailable'
|
|
1186
|
-
? ' next: start/restart the external GoodVibes daemon from TUI or daemon host tooling; Agent does not own daemon lifecycle.'
|
|
1187
|
-
: null,
|
|
1188
|
-
result.kind === 'version_mismatch' || result.kind === 'daemon_route_unavailable'
|
|
1189
|
-
? ' next: update/restart the external GoodVibes daemon so public schedules.list is available.'
|
|
1190
|
-
: null,
|
|
1191
|
-
].filter((line): line is string => Boolean(line)).join('\n');
|
|
1192
|
-
}
|
|
1193
|
-
const correlations = result.correlations.slice(0, Math.max(1, limit));
|
|
1194
|
-
if (result.receiptCount === 0) {
|
|
1195
|
-
return [
|
|
1196
|
-
'Agent routine schedule reconciliation',
|
|
1197
|
-
` daemon: ${result.baseUrl}`,
|
|
1198
|
-
` route: ${result.kind} ${result.route}`,
|
|
1199
|
-
` live schedules: ${result.scheduleCount}`,
|
|
1200
|
-
' No local routine promotion receipts exist yet.',
|
|
1201
|
-
' Create one with /schedule promote-routine <routine-id> --cron <expr> --yes.',
|
|
1202
|
-
].join('\n');
|
|
1203
|
-
}
|
|
1204
|
-
const matched = result.correlations.filter((entry) => entry.liveStatus === 'matched').length;
|
|
1205
|
-
const missing = result.correlations.filter((entry) => entry.liveStatus === 'missing').length;
|
|
1206
|
-
const failed = result.correlations.filter((entry) => entry.liveStatus === 'failed-receipt').length;
|
|
1207
|
-
return [
|
|
1208
|
-
'Agent routine schedule reconciliation',
|
|
1209
|
-
` daemon: ${result.baseUrl}`,
|
|
1210
|
-
` route: ${result.kind} ${result.route}`,
|
|
1211
|
-
` receipts: ${result.receiptCount}; live schedules: ${result.scheduleCount}; matched: ${matched}; missing: ${missing}; failed receipts: ${failed}`,
|
|
1212
|
-
...correlations.map((entry) => {
|
|
1213
|
-
const receipt = entry.receipt;
|
|
1214
|
-
const schedule = entry.schedule;
|
|
1215
|
-
const live = schedule
|
|
1216
|
-
? ` live=${schedule.id} status=${schedule.status ?? (schedule.enabled === false ? 'paused' : 'enabled')}`
|
|
1217
|
-
: '';
|
|
1218
|
-
const runs = schedule && schedule.runCount !== undefined
|
|
1219
|
-
? ` runs=${schedule.runCount}/${schedule.successCount ?? 0}/${schedule.failureCount ?? 0}`
|
|
1220
|
-
: '';
|
|
1221
|
-
const next = schedule?.nextRunAt ? ` next=${new Date(schedule.nextRunAt).toISOString()}` : '';
|
|
1222
|
-
return ` ${receipt.id} ${entry.liveStatus} reason=${entry.matchReason} routine=${receipt.routineId} receiptSchedule=${receipt.scheduleId ?? '(none)'}${live}${runs}${next}`;
|
|
1223
|
-
}),
|
|
1224
|
-
result.correlations.length > correlations.length ? ` ...${result.correlations.length - correlations.length} more` : '',
|
|
1225
|
-
].filter((line): line is string => Boolean(line)).join('\n');
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
export function formatRoutineScheduleFailure(failure: RoutineSchedulePromotionFailure): string {
|
|
1229
|
-
return [
|
|
1230
|
-
`Daemon schedule error: ${failure.kind}`,
|
|
1231
|
-
` ${failure.error}`,
|
|
1232
|
-
failure.baseUrl ? ` daemon: ${failure.baseUrl}` : null,
|
|
1233
|
-
` route: ${ROUTINE_SCHEDULE_METHOD} ${failure.route}`,
|
|
1234
|
-
failure.kind === 'version_mismatch' && failure.daemonVersion && failure.expectedSdkVersion
|
|
1235
|
-
? ` versions: daemon=${failure.daemonVersion} expected=${failure.expectedSdkVersion}`
|
|
1236
|
-
: null,
|
|
1237
|
-
failure.kind === 'auth_required'
|
|
1238
|
-
? ' next: pair/authenticate with the externally managed GoodVibes daemon, then retry with --yes.'
|
|
1239
|
-
: null,
|
|
1240
|
-
failure.kind === 'daemon_unavailable'
|
|
1241
|
-
? ' next: start/restart the external GoodVibes daemon from TUI or daemon host tooling; Agent does not own daemon lifecycle.'
|
|
1242
|
-
: null,
|
|
1243
|
-
failure.kind === 'version_mismatch' || failure.kind === 'daemon_route_unavailable'
|
|
1244
|
-
? ' next: update/restart the external GoodVibes daemon so public schedules.create is available.'
|
|
1245
|
-
: null,
|
|
1246
|
-
].filter((line): line is string => Boolean(line)).join('\n');
|
|
1247
|
-
}
|