@pellux/goodvibes-agent 1.2.0 → 1.4.1
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 +194 -0
- package/dist/package/{ast-grep-napi.linux-x64-gnu-swtppvy9.node → ast-grep-napi.linux-x64-gnu-mkk8xwww.node} +0 -0
- package/dist/package/{ast-grep-napi.linux-x64-musl-ttfcdtap.node → ast-grep-napi.linux-x64-musl-ryqtgdv6.node} +0 -0
- package/dist/package/main.js +65740 -57156
- package/docs/README.md +3 -3
- package/docs/connected-host.md +2 -2
- package/docs/getting-started.md +11 -9
- package/docs/tools-and-commands.md +12 -10
- package/docs/voice-and-live-tts.md +2 -2
- package/package.json +1 -1
- package/release/release-notes.md +15 -144
- package/release/release-readiness.json +36 -36
- package/src/agent/competitive-feature-inventory.ts +42 -44
- package/src/agent/setup-wizard-artifact-receipts.ts +366 -0
- package/src/agent/setup-wizard.ts +267 -24
- package/src/cli/tui-startup.ts +2 -2
- package/src/input/agent-workspace-activation.ts +2 -2
- package/src/input/agent-workspace-categories.ts +2 -2
- package/src/input/agent-workspace-onboarding-categories.ts +4 -4
- package/src/input/agent-workspace-onboarding-finish.ts +21 -0
- package/src/input/agent-workspace-settings.ts +47 -1
- package/src/input/agent-workspace-setup-snapshot.ts +17 -6
- package/src/input/agent-workspace-setup.ts +20 -26
- package/src/input/agent-workspace-snapshot.ts +16 -3
- package/src/input/agent-workspace-types.ts +5 -2
- package/src/input/agent-workspace.ts +21 -6
- package/src/input/handler.ts +67 -5
- package/src/input/setup-wizard-live-receipts.ts +76 -0
- package/src/renderer/agent-workspace-context-lines.ts +17 -33
- package/src/renderer/agent-workspace.ts +434 -21
- package/src/runtime/tool-permission-safety.ts +1 -1
- package/src/tools/agent-harness-agent-orchestration-policy.ts +75 -0
- package/src/tools/agent-harness-agent-orchestration.ts +216 -128
- package/src/tools/agent-harness-autonomy-live-records.ts +154 -0
- package/src/tools/agent-harness-autonomy-queue-types.ts +1 -1
- package/src/tools/agent-harness-autonomy-queue.ts +19 -8
- package/src/tools/agent-harness-autonomy-watcher-read-models.ts +509 -0
- package/src/tools/agent-harness-background-processes.ts +8 -4
- package/src/tools/agent-harness-browser-cockpit-route.ts +188 -34
- package/src/tools/agent-harness-browser-control.ts +12 -4
- package/src/tools/agent-harness-browser-pwa-read-models.ts +622 -0
- package/src/tools/agent-harness-device-live-read-models.ts +366 -0
- package/src/tools/agent-harness-execution-posture.ts +3 -0
- package/src/tools/agent-harness-interactive-runtime-records.ts +421 -0
- package/src/tools/agent-harness-local-model-benchmarks.ts +71 -1
- package/src/tools/agent-harness-local-model-endpoints.ts +469 -354
- package/src/tools/agent-harness-local-model-smoke.ts +277 -0
- package/src/tools/agent-harness-local-model-url.ts +78 -0
- package/src/tools/agent-harness-media-posture.ts +27 -12
- package/src/tools/agent-harness-memory-external-providers.ts +796 -0
- package/src/tools/agent-harness-memory-posture.ts +253 -137
- package/src/tools/agent-harness-memory-provider-certification.ts +219 -0
- package/src/tools/agent-harness-memory-refinement.ts +340 -0
- package/src/tools/agent-harness-mode-catalog.ts +4 -2
- package/src/tools/agent-harness-model-provider-health.ts +139 -42
- package/src/tools/agent-harness-model-readiness.ts +31 -5
- package/src/tools/agent-harness-model-routing-types.ts +61 -0
- package/src/tools/agent-harness-model-routing.ts +31 -6
- package/src/tools/agent-harness-pairing-posture.ts +30 -9
- package/src/tools/agent-harness-personal-ops-certification.ts +116 -0
- package/src/tools/agent-harness-personal-ops-lanes.ts +81 -15
- package/src/tools/agent-harness-personal-ops-operations.ts +225 -0
- package/src/tools/agent-harness-personal-ops-provider-records.ts +358 -0
- package/src/tools/agent-harness-personal-ops-provider-task-records.ts +321 -0
- package/src/tools/agent-harness-personal-ops-records.ts +176 -224
- package/src/tools/agent-harness-personal-ops-types.ts +19 -1
- package/src/tools/agent-harness-personal-ops.ts +18 -11
- package/src/tools/agent-harness-remote-read-models.ts +541 -0
- package/src/tools/agent-harness-research-briefing.ts +26 -7
- package/src/tools/agent-harness-research-live-read-models.ts +500 -0
- package/src/tools/agent-harness-research-runs.ts +15 -3
- package/src/tools/agent-harness-research-workflow.ts +92 -19
- package/src/tools/agent-harness-setup-model-helpers.ts +1 -0
- package/src/tools/agent-harness-setup-posture.ts +7 -7
- package/src/tools/agent-harness-setup-smoke.ts +26 -1
- package/src/tools/agent-harness-tool-schema.ts +23 -1
- package/src/tools/agent-harness-tool-types.ts +5 -0
- package/src/tools/agent-harness-tool.ts +9 -1
- package/src/tools/agent-harness-workspace-actions.ts +1 -1
- package/src/tools/agent-memory-tool.ts +40 -1
- package/src/tools/agent-model-compare-run.ts +13 -0
- package/src/tools/agent-research-runner.ts +367 -0
- package/src/tools/agent-research-tool.ts +7 -179
- package/src/tools/agent-route-planner-candidates-surfaces.ts +1 -1
- package/src/version.ts +1 -1
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import type { CommandContext } from '../input/command-registry.ts';
|
|
2
|
+
import { previewHarnessText } from './agent-harness-text.ts';
|
|
3
|
+
import { readRecord, readString } from './agent-harness-model-routing-utils.ts';
|
|
4
|
+
import { redactedPersonalOpsText } from './agent-harness-personal-ops-runner.ts';
|
|
5
|
+
import { personalOpsRecordCertification } from './agent-harness-personal-ops-certification.ts';
|
|
6
|
+
import { safeRecordIdPart } from './agent-harness-personal-ops-records.ts';
|
|
7
|
+
import type { PersonalOpsFollowUpRoute, PersonalOpsLiveRecord, PersonalOpsRecordFreshness } from './agent-harness-personal-ops-types.ts';
|
|
8
|
+
|
|
9
|
+
type ProviderTaskLane = 'tasks' | 'reminders';
|
|
10
|
+
type ProviderReadModelSourceKind = Extract<PersonalOpsRecordFreshness['source'], 'daemon-read-model' | 'sdk-read-model'>;
|
|
11
|
+
|
|
12
|
+
interface ProviderTaskSource {
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly source: unknown;
|
|
15
|
+
readonly kind: ProviderReadModelSourceKind;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const TASK_READ_KEYS = ['readRoute', 'inspectRoute', 'refreshRoute', 'getTaskRoute'];
|
|
19
|
+
const TASK_EFFECT_KEYS = {
|
|
20
|
+
update: ['updateRoute', 'editRoute', 'patchRoute'],
|
|
21
|
+
complete: ['completeRoute', 'doneRoute', 'finishRoute'],
|
|
22
|
+
defer: ['deferRoute', 'snoozeRoute'],
|
|
23
|
+
delete: ['deleteRoute', 'archiveRoute'],
|
|
24
|
+
} as const;
|
|
25
|
+
const REMINDER_READ_KEYS = ['readRoute', 'inspectRoute', 'refreshRoute', 'getReminderRoute', 'getScheduleRoute'];
|
|
26
|
+
const REMINDER_EFFECT_KEYS = {
|
|
27
|
+
edit: ['editRoute', 'updateRoute', 'rescheduleRoute'],
|
|
28
|
+
complete: ['completeRoute', 'doneRoute', 'dismissRoute'],
|
|
29
|
+
snooze: ['snoozeRoute', 'deferRoute'],
|
|
30
|
+
delete: ['deleteRoute', 'cancelRoute'],
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
function firstString(record: Record<string, unknown>, keys: readonly string[]): string {
|
|
34
|
+
for (const key of keys) {
|
|
35
|
+
const value = readString(record[key]);
|
|
36
|
+
if (value) return value;
|
|
37
|
+
}
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readStringArray(value: unknown): readonly string[] {
|
|
42
|
+
if (Array.isArray(value)) return [...new Set(value.map((entry) => readString(entry)).filter(Boolean))].slice(0, 12);
|
|
43
|
+
const text = readString(value);
|
|
44
|
+
return text ? text.split(',').map((entry) => entry.trim()).filter(Boolean).slice(0, 12) : [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readIso(value: unknown): string {
|
|
48
|
+
if (typeof value === 'number' && Number.isFinite(value)) return new Date(value).toISOString();
|
|
49
|
+
const text = readString(value);
|
|
50
|
+
if (!text) return '';
|
|
51
|
+
const timestamp = Date.parse(text);
|
|
52
|
+
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : previewHarnessText(redactedPersonalOpsText(text), 80);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readRoute(value: unknown): string {
|
|
56
|
+
const direct = readString(value);
|
|
57
|
+
if (direct) return direct;
|
|
58
|
+
const record = readRecord(value);
|
|
59
|
+
return firstString(record, ['modelRoute', 'route', 'command', 'action', 'href']);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function routeFromKeys(record: Record<string, unknown>, keys: readonly string[]): string {
|
|
63
|
+
for (const key of keys) {
|
|
64
|
+
const route = readRoute(record[key]);
|
|
65
|
+
if (route) return route;
|
|
66
|
+
}
|
|
67
|
+
for (const containerKey of ['routes', 'effectRoutes', 'actions']) {
|
|
68
|
+
const routes = readRecord(record[containerKey]);
|
|
69
|
+
for (const key of keys) {
|
|
70
|
+
const route = readRoute(routes[key]);
|
|
71
|
+
if (route) return route;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function sources(context: CommandContext, lane: ProviderTaskLane): readonly ProviderTaskSource[] {
|
|
78
|
+
const platform = context.platform as unknown as Record<string, unknown>;
|
|
79
|
+
const clients = readRecord(context.clients);
|
|
80
|
+
const readModels = readRecord(platform.readModels);
|
|
81
|
+
const personalOps = readRecord(readModels.personalOps);
|
|
82
|
+
const platformPersonalOps = readRecord(platform.personalOps);
|
|
83
|
+
const operatorPersonalOps = readRecord(readRecord(clients.operator).personalOps);
|
|
84
|
+
if (lane === 'tasks') {
|
|
85
|
+
return [
|
|
86
|
+
{ path: 'context.platform.readModels.personalOps.tasks', source: personalOps.tasks, kind: 'daemon-read-model' },
|
|
87
|
+
{ path: 'context.platform.readModels.personalOps.taskRecords', source: personalOps.taskRecords, kind: 'daemon-read-model' },
|
|
88
|
+
{ path: 'context.platform.readModels.providerTasks', source: readModels.providerTasks, kind: 'daemon-read-model' },
|
|
89
|
+
{ path: 'context.platform.readModels.taskProviders.tasks', source: readRecord(readModels.taskProviders).tasks, kind: 'daemon-read-model' },
|
|
90
|
+
{ path: 'context.platform.personalOps.tasks', source: platformPersonalOps.tasks, kind: 'daemon-read-model' },
|
|
91
|
+
{ path: 'context.clients.operator.personalOps.tasks', source: operatorPersonalOps.tasks, kind: 'sdk-read-model' },
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
return [
|
|
95
|
+
{ path: 'context.platform.readModels.personalOps.reminders', source: personalOps.reminders, kind: 'daemon-read-model' },
|
|
96
|
+
{ path: 'context.platform.readModels.personalOps.reminderRecords', source: personalOps.reminderRecords, kind: 'daemon-read-model' },
|
|
97
|
+
{ path: 'context.platform.readModels.providerReminders', source: readModels.providerReminders, kind: 'daemon-read-model' },
|
|
98
|
+
{ path: 'context.platform.readModels.reminders', source: readModels.reminders, kind: 'daemon-read-model' },
|
|
99
|
+
{ path: 'context.platform.readModels.schedules.reminders', source: readRecord(readModels.schedules).reminders, kind: 'daemon-read-model' },
|
|
100
|
+
{ path: 'context.platform.personalOps.reminders', source: platformPersonalOps.reminders, kind: 'daemon-read-model' },
|
|
101
|
+
{ path: 'context.clients.operator.personalOps.reminders', source: operatorPersonalOps.reminders, kind: 'sdk-read-model' },
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readSnapshot(source: unknown, lane: ProviderTaskLane): unknown {
|
|
106
|
+
if (typeof source === 'function') {
|
|
107
|
+
try {
|
|
108
|
+
const result = (source as () => unknown)();
|
|
109
|
+
return result instanceof Promise ? undefined : result;
|
|
110
|
+
} catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const record = readRecord(source);
|
|
115
|
+
const methods = lane === 'tasks'
|
|
116
|
+
? ['getSnapshot', 'snapshot', 'listTasks', 'getTasks', 'list']
|
|
117
|
+
: ['getSnapshot', 'snapshot', 'listReminders', 'getReminders', 'listSchedules', 'list'];
|
|
118
|
+
for (const methodName of methods) {
|
|
119
|
+
const method = record[methodName];
|
|
120
|
+
if (typeof method !== 'function') continue;
|
|
121
|
+
try {
|
|
122
|
+
const result = (method as () => unknown).call(source);
|
|
123
|
+
return result instanceof Promise ? undefined : result;
|
|
124
|
+
} catch {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return source;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function recordsFromSnapshot(snapshot: unknown, lane: ProviderTaskLane, depth = 0): readonly unknown[] {
|
|
132
|
+
if (Array.isArray(snapshot)) return snapshot;
|
|
133
|
+
const record = readRecord(snapshot);
|
|
134
|
+
const keys = lane === 'tasks'
|
|
135
|
+
? ['tasks', 'taskRecords', 'items', 'records', 'entries', 'queue']
|
|
136
|
+
: ['reminders', 'reminderRecords', 'schedules', 'items', 'records', 'entries', 'queue'];
|
|
137
|
+
for (const key of keys) {
|
|
138
|
+
const value = record[key];
|
|
139
|
+
if (Array.isArray(value)) return value;
|
|
140
|
+
const valueRecord = readRecord(value);
|
|
141
|
+
if (Object.keys(valueRecord).length > 0) {
|
|
142
|
+
return Object.entries(valueRecord).map(([providerRecordId, entry]) => {
|
|
143
|
+
const entryRecord = readRecord(entry);
|
|
144
|
+
return Object.keys(entryRecord).length > 0 && !firstString(entryRecord, ['id', 'taskId', 'reminderId', 'scheduleId', 'providerRecordId'])
|
|
145
|
+
? { ...entryRecord, providerRecordId }
|
|
146
|
+
: entry;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (depth < 2) {
|
|
151
|
+
for (const key of ['snapshot', 'data', 'current', 'payload', 'result', 'state']) {
|
|
152
|
+
const nested = recordsFromSnapshot(record[key], lane, depth + 1);
|
|
153
|
+
if (nested.length > 0) return nested;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return firstString(record, lane === 'tasks'
|
|
157
|
+
? ['id', 'taskId', 'providerRecordId']
|
|
158
|
+
: ['id', 'reminderId', 'scheduleId', 'providerRecordId'])
|
|
159
|
+
? [record]
|
|
160
|
+
: [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function followUp(id: string, label: string, route: string, policy: string): PersonalOpsFollowUpRoute | null {
|
|
164
|
+
if (!route) return null;
|
|
165
|
+
return {
|
|
166
|
+
id,
|
|
167
|
+
label,
|
|
168
|
+
effect: 'confirmed-effect',
|
|
169
|
+
modelRoute: route,
|
|
170
|
+
requiresConfirmation: true,
|
|
171
|
+
policy,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function freshness(source: ProviderTaskSource, readRoute: string, currentAt: string, sampleInput: Readonly<Record<string, unknown>>): PersonalOpsRecordFreshness {
|
|
176
|
+
return {
|
|
177
|
+
status: readRoute ? 'fresh-provider-route-ready' : 'fresh-provider-record-current',
|
|
178
|
+
source: source.kind,
|
|
179
|
+
sourceTool: source.path,
|
|
180
|
+
...(currentAt ? { lastReviewedAt: currentAt } : {}),
|
|
181
|
+
...(readRoute ? { refreshRoute: readRoute } : {}),
|
|
182
|
+
...(Object.keys(sampleInput).length > 0 ? { sampleInput } : {}),
|
|
183
|
+
policy: readRoute
|
|
184
|
+
? 'The provider published a current task/reminder record plus a bounded read route. Run it only for a user-requested refresh; mutations remain separate confirmed follow-up routes.'
|
|
185
|
+
: 'The provider published current task/reminder state through a daemon or SDK read model. Inspect it as read-only; mutations require explicit published routes.',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function normalizeTask(source: ProviderTaskSource, raw: unknown, index: number): PersonalOpsLiveRecord | null {
|
|
190
|
+
const record = readRecord(raw);
|
|
191
|
+
const providerId = firstString(record, ['providerId', 'provider', 'accountId', 'service', 'source']) || 'provider';
|
|
192
|
+
const taskId = firstString(record, ['taskId', 'id', 'uid', 'providerRecordId']) || `${index + 1}`;
|
|
193
|
+
const title = firstString(record, ['title', 'name', 'summary', 'subject']) || `Provider task ${index + 1}`;
|
|
194
|
+
const status = firstString(record, ['status', 'state', 'reviewState']) || 'current';
|
|
195
|
+
const dueAt = readIso(record.dueAt ?? record.due ?? record.deadline ?? record.scheduledAt);
|
|
196
|
+
const updatedAt = readIso(record.currentAt ?? record.syncedAt ?? record.updatedAt ?? record.createdAt);
|
|
197
|
+
const priority = firstString(record, ['priority', 'urgency', 'importance']);
|
|
198
|
+
const list = firstString(record, ['list', 'listId', 'project', 'projectId', 'workspace']);
|
|
199
|
+
const assignee = firstString(record, ['assignee', 'owner', 'assignedTo']);
|
|
200
|
+
const notes = firstString(record, ['snippet', 'preview', 'notes', 'descriptionSummary', 'description']);
|
|
201
|
+
const labels = readStringArray(record.labels ?? record.tags).slice(0, 6);
|
|
202
|
+
const readRoute = routeFromKeys(record, TASK_READ_KEYS);
|
|
203
|
+
const sampleInput: Record<string, unknown> = { taskId };
|
|
204
|
+
const followUpRoutes = [
|
|
205
|
+
followUp('update-provider-task', 'Update task', routeFromKeys(record, TASK_EFFECT_KEYS.update), 'Updating a provider task mutates provider state and requires exact field review plus explicit confirmation.'),
|
|
206
|
+
followUp('complete-provider-task', 'Complete task', routeFromKeys(record, TASK_EFFECT_KEYS.complete), 'Completing a provider task mutates provider state and requires explicit confirmation.'),
|
|
207
|
+
followUp('defer-provider-task', 'Defer task', routeFromKeys(record, TASK_EFFECT_KEYS.defer), 'Deferring or snoozing a provider task changes timing and requires explicit confirmation.'),
|
|
208
|
+
followUp('delete-provider-task', 'Delete or archive task', routeFromKeys(record, TASK_EFFECT_KEYS.delete), 'Deleting or archiving a provider task requires explicit confirmation and an exact provider route.'),
|
|
209
|
+
].filter((route): route is PersonalOpsFollowUpRoute => route !== null);
|
|
210
|
+
const hasConfirmedEffectRoute = followUpRoutes.some((route) => route.effect === 'confirmed-effect');
|
|
211
|
+
return {
|
|
212
|
+
id: `provider-task:${safeRecordIdPart(source.path)}:${safeRecordIdPart(taskId)}`,
|
|
213
|
+
label: `Fresh task: ${previewHarnessText(redactedPersonalOpsText(title), 96)}`,
|
|
214
|
+
status: previewHarnessText(redactedPersonalOpsText(status), 48),
|
|
215
|
+
summary: [
|
|
216
|
+
`Provider ${previewHarnessText(redactedPersonalOpsText(providerId), 64)} published current task ${previewHarnessText(redactedPersonalOpsText(taskId), 80)} from ${source.path}.`,
|
|
217
|
+
notes ? `Preview ${previewHarnessText(redactedPersonalOpsText(notes), 140)}.` : '',
|
|
218
|
+
list ? `List ${previewHarnessText(redactedPersonalOpsText(list), 64)}.` : '',
|
|
219
|
+
dueAt ? `Due ${dueAt}.` : '',
|
|
220
|
+
priority ? `Priority ${previewHarnessText(redactedPersonalOpsText(priority), 40)}.` : '',
|
|
221
|
+
assignee ? `Owner ${previewHarnessText(redactedPersonalOpsText(assignee), 64)}.` : '',
|
|
222
|
+
labels.length > 0 ? `Labels ${labels.map((label) => previewHarnessText(redactedPersonalOpsText(label), 40)).join(', ')}.` : '',
|
|
223
|
+
updatedAt ? `Updated ${updatedAt}.` : '',
|
|
224
|
+
followUpRoutes.length > 0 ? 'Published provider effects are separated into confirmed follow-up routes.' : 'No provider mutation route is published for this task.',
|
|
225
|
+
].filter(Boolean).join(' '),
|
|
226
|
+
userRoute: 'Agent Workspace -> Personal Ops -> Provider task queue',
|
|
227
|
+
modelRoute: readRoute || `personal_ops action:"lane" laneId:"tasks" query:"${safeRecordIdPart(taskId)}" includeParameters:true`,
|
|
228
|
+
tags: ['provider-backed', 'fresh-provider', 'task-record', providerId, status, priority, ...labels].filter(Boolean),
|
|
229
|
+
effect: 'read-only',
|
|
230
|
+
capability: 'task-provider-record',
|
|
231
|
+
confirmationRequired: false,
|
|
232
|
+
sourceTool: source.path,
|
|
233
|
+
certification: personalOpsRecordCertification({
|
|
234
|
+
record,
|
|
235
|
+
sourcePath: source.path,
|
|
236
|
+
durableId: taskId,
|
|
237
|
+
recordKind: 'task',
|
|
238
|
+
hasConfirmedEffectRoute,
|
|
239
|
+
}),
|
|
240
|
+
freshness: freshness(source, readRoute, updatedAt || dueAt, sampleInput),
|
|
241
|
+
followUpRoutes,
|
|
242
|
+
sampleInput,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function normalizeReminder(source: ProviderTaskSource, raw: unknown, index: number): PersonalOpsLiveRecord | null {
|
|
247
|
+
const record = readRecord(raw);
|
|
248
|
+
const providerId = firstString(record, ['providerId', 'provider', 'accountId', 'service', 'source']) || 'provider';
|
|
249
|
+
const reminderId = firstString(record, ['reminderId', 'scheduleId', 'id', 'uid', 'providerRecordId']) || `${index + 1}`;
|
|
250
|
+
const title = firstString(record, ['title', 'message', 'name', 'summary', 'subject']) || `Provider reminder ${index + 1}`;
|
|
251
|
+
const status = firstString(record, ['status', 'state', 'reviewState']) || 'current';
|
|
252
|
+
const dueAt = readIso(record.dueAt ?? record.remindAt ?? record.scheduledAt ?? record.nextRunAt ?? record.time);
|
|
253
|
+
const updatedAt = readIso(record.currentAt ?? record.syncedAt ?? record.updatedAt ?? record.createdAt);
|
|
254
|
+
const cadence = firstString(record, ['cadence', 'schedule', 'scheduleValue', 'rrule', 'repeat']);
|
|
255
|
+
const delivery = firstString(record, ['deliveryTarget', 'deliveryTargetId', 'channel', 'target']);
|
|
256
|
+
const notes = firstString(record, ['snippet', 'preview', 'notes', 'descriptionSummary', 'description']);
|
|
257
|
+
const readRoute = routeFromKeys(record, REMINDER_READ_KEYS);
|
|
258
|
+
const sampleInput: Record<string, unknown> = { reminderId };
|
|
259
|
+
const followUpRoutes = [
|
|
260
|
+
followUp('edit-provider-reminder', 'Edit reminder', routeFromKeys(record, REMINDER_EFFECT_KEYS.edit), 'Editing a provider reminder changes schedule or message state and requires exact field review plus explicit confirmation.'),
|
|
261
|
+
followUp('complete-provider-reminder', 'Complete reminder', routeFromKeys(record, REMINDER_EFFECT_KEYS.complete), 'Completing or dismissing a provider reminder mutates provider state and requires explicit confirmation.'),
|
|
262
|
+
followUp('snooze-provider-reminder', 'Snooze reminder', routeFromKeys(record, REMINDER_EFFECT_KEYS.snooze), 'Snoozing a provider reminder changes timing and requires explicit confirmation.'),
|
|
263
|
+
followUp('delete-provider-reminder', 'Delete or cancel reminder', routeFromKeys(record, REMINDER_EFFECT_KEYS.delete), 'Deleting or cancelling a provider reminder requires explicit confirmation and an exact provider route.'),
|
|
264
|
+
].filter((route): route is PersonalOpsFollowUpRoute => route !== null);
|
|
265
|
+
const hasConfirmedEffectRoute = followUpRoutes.some((route) => route.effect === 'confirmed-effect');
|
|
266
|
+
return {
|
|
267
|
+
id: `provider-reminder:${safeRecordIdPart(source.path)}:${safeRecordIdPart(reminderId)}`,
|
|
268
|
+
label: `Fresh reminder: ${previewHarnessText(redactedPersonalOpsText(title), 96)}`,
|
|
269
|
+
status: previewHarnessText(redactedPersonalOpsText(status), 48),
|
|
270
|
+
summary: [
|
|
271
|
+
`Provider ${previewHarnessText(redactedPersonalOpsText(providerId), 64)} published current reminder ${previewHarnessText(redactedPersonalOpsText(reminderId), 80)} from ${source.path}.`,
|
|
272
|
+
notes ? `Preview ${previewHarnessText(redactedPersonalOpsText(notes), 140)}.` : '',
|
|
273
|
+
dueAt ? `Due ${dueAt}.` : '',
|
|
274
|
+
cadence ? `Cadence ${previewHarnessText(redactedPersonalOpsText(cadence), 64)}.` : '',
|
|
275
|
+
delivery ? `Delivery ${previewHarnessText(redactedPersonalOpsText(delivery), 64)}.` : '',
|
|
276
|
+
updatedAt ? `Updated ${updatedAt}.` : '',
|
|
277
|
+
followUpRoutes.length > 0 ? 'Published provider effects are separated into confirmed follow-up routes.' : 'No provider mutation route is published for this reminder.',
|
|
278
|
+
].filter(Boolean).join(' '),
|
|
279
|
+
userRoute: 'Agent Workspace -> Personal Ops -> Provider reminder queue',
|
|
280
|
+
modelRoute: readRoute || `personal_ops action:"lane" laneId:"reminders" query:"${safeRecordIdPart(reminderId)}" includeParameters:true`,
|
|
281
|
+
tags: ['provider-backed', 'fresh-provider', 'reminder-record', providerId, status, cadence].filter(Boolean),
|
|
282
|
+
effect: 'read-only',
|
|
283
|
+
capability: 'reminder-provider-record',
|
|
284
|
+
confirmationRequired: false,
|
|
285
|
+
sourceTool: source.path,
|
|
286
|
+
certification: personalOpsRecordCertification({
|
|
287
|
+
record,
|
|
288
|
+
sourcePath: source.path,
|
|
289
|
+
durableId: reminderId,
|
|
290
|
+
recordKind: 'reminder',
|
|
291
|
+
hasConfirmedEffectRoute,
|
|
292
|
+
}),
|
|
293
|
+
freshness: freshness(source, readRoute, updatedAt || dueAt, sampleInput),
|
|
294
|
+
followUpRoutes,
|
|
295
|
+
sampleInput,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function providerBackedTaskRecords(context: CommandContext): readonly PersonalOpsLiveRecord[] {
|
|
300
|
+
return providerBackedRecords(context, 'tasks');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function providerBackedReminderRecords(context: CommandContext): readonly PersonalOpsLiveRecord[] {
|
|
304
|
+
return providerBackedRecords(context, 'reminders');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function providerBackedRecords(context: CommandContext, lane: ProviderTaskLane): readonly PersonalOpsLiveRecord[] {
|
|
308
|
+
const records: PersonalOpsLiveRecord[] = [];
|
|
309
|
+
const seen = new Set<string>();
|
|
310
|
+
for (const source of sources(context, lane)) {
|
|
311
|
+
const snapshot = readSnapshot(source.source, lane);
|
|
312
|
+
for (const [index, raw] of recordsFromSnapshot(snapshot, lane).entries()) {
|
|
313
|
+
const record = lane === 'tasks' ? normalizeTask(source, raw, index) : normalizeReminder(source, raw, index);
|
|
314
|
+
if (!record || seen.has(record.id)) continue;
|
|
315
|
+
seen.add(record.id);
|
|
316
|
+
records.push(record);
|
|
317
|
+
if (records.length >= 10) return records;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return records;
|
|
321
|
+
}
|