@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.
Files changed (85) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/dist/package/{ast-grep-napi.linux-x64-gnu-swtppvy9.node → ast-grep-napi.linux-x64-gnu-mkk8xwww.node} +0 -0
  3. package/dist/package/{ast-grep-napi.linux-x64-musl-ttfcdtap.node → ast-grep-napi.linux-x64-musl-ryqtgdv6.node} +0 -0
  4. package/dist/package/main.js +65740 -57156
  5. package/docs/README.md +3 -3
  6. package/docs/connected-host.md +2 -2
  7. package/docs/getting-started.md +11 -9
  8. package/docs/tools-and-commands.md +12 -10
  9. package/docs/voice-and-live-tts.md +2 -2
  10. package/package.json +1 -1
  11. package/release/release-notes.md +15 -144
  12. package/release/release-readiness.json +36 -36
  13. package/src/agent/competitive-feature-inventory.ts +42 -44
  14. package/src/agent/setup-wizard-artifact-receipts.ts +366 -0
  15. package/src/agent/setup-wizard.ts +267 -24
  16. package/src/cli/tui-startup.ts +2 -2
  17. package/src/input/agent-workspace-activation.ts +2 -2
  18. package/src/input/agent-workspace-categories.ts +2 -2
  19. package/src/input/agent-workspace-onboarding-categories.ts +4 -4
  20. package/src/input/agent-workspace-onboarding-finish.ts +21 -0
  21. package/src/input/agent-workspace-settings.ts +47 -1
  22. package/src/input/agent-workspace-setup-snapshot.ts +17 -6
  23. package/src/input/agent-workspace-setup.ts +20 -26
  24. package/src/input/agent-workspace-snapshot.ts +16 -3
  25. package/src/input/agent-workspace-types.ts +5 -2
  26. package/src/input/agent-workspace.ts +21 -6
  27. package/src/input/handler.ts +67 -5
  28. package/src/input/setup-wizard-live-receipts.ts +76 -0
  29. package/src/renderer/agent-workspace-context-lines.ts +17 -33
  30. package/src/renderer/agent-workspace.ts +434 -21
  31. package/src/runtime/tool-permission-safety.ts +1 -1
  32. package/src/tools/agent-harness-agent-orchestration-policy.ts +75 -0
  33. package/src/tools/agent-harness-agent-orchestration.ts +216 -128
  34. package/src/tools/agent-harness-autonomy-live-records.ts +154 -0
  35. package/src/tools/agent-harness-autonomy-queue-types.ts +1 -1
  36. package/src/tools/agent-harness-autonomy-queue.ts +19 -8
  37. package/src/tools/agent-harness-autonomy-watcher-read-models.ts +509 -0
  38. package/src/tools/agent-harness-background-processes.ts +8 -4
  39. package/src/tools/agent-harness-browser-cockpit-route.ts +188 -34
  40. package/src/tools/agent-harness-browser-control.ts +12 -4
  41. package/src/tools/agent-harness-browser-pwa-read-models.ts +622 -0
  42. package/src/tools/agent-harness-device-live-read-models.ts +366 -0
  43. package/src/tools/agent-harness-execution-posture.ts +3 -0
  44. package/src/tools/agent-harness-interactive-runtime-records.ts +421 -0
  45. package/src/tools/agent-harness-local-model-benchmarks.ts +71 -1
  46. package/src/tools/agent-harness-local-model-endpoints.ts +469 -354
  47. package/src/tools/agent-harness-local-model-smoke.ts +277 -0
  48. package/src/tools/agent-harness-local-model-url.ts +78 -0
  49. package/src/tools/agent-harness-media-posture.ts +27 -12
  50. package/src/tools/agent-harness-memory-external-providers.ts +796 -0
  51. package/src/tools/agent-harness-memory-posture.ts +253 -137
  52. package/src/tools/agent-harness-memory-provider-certification.ts +219 -0
  53. package/src/tools/agent-harness-memory-refinement.ts +340 -0
  54. package/src/tools/agent-harness-mode-catalog.ts +4 -2
  55. package/src/tools/agent-harness-model-provider-health.ts +139 -42
  56. package/src/tools/agent-harness-model-readiness.ts +31 -5
  57. package/src/tools/agent-harness-model-routing-types.ts +61 -0
  58. package/src/tools/agent-harness-model-routing.ts +31 -6
  59. package/src/tools/agent-harness-pairing-posture.ts +30 -9
  60. package/src/tools/agent-harness-personal-ops-certification.ts +116 -0
  61. package/src/tools/agent-harness-personal-ops-lanes.ts +81 -15
  62. package/src/tools/agent-harness-personal-ops-operations.ts +225 -0
  63. package/src/tools/agent-harness-personal-ops-provider-records.ts +358 -0
  64. package/src/tools/agent-harness-personal-ops-provider-task-records.ts +321 -0
  65. package/src/tools/agent-harness-personal-ops-records.ts +176 -224
  66. package/src/tools/agent-harness-personal-ops-types.ts +19 -1
  67. package/src/tools/agent-harness-personal-ops.ts +18 -11
  68. package/src/tools/agent-harness-remote-read-models.ts +541 -0
  69. package/src/tools/agent-harness-research-briefing.ts +26 -7
  70. package/src/tools/agent-harness-research-live-read-models.ts +500 -0
  71. package/src/tools/agent-harness-research-runs.ts +15 -3
  72. package/src/tools/agent-harness-research-workflow.ts +92 -19
  73. package/src/tools/agent-harness-setup-model-helpers.ts +1 -0
  74. package/src/tools/agent-harness-setup-posture.ts +7 -7
  75. package/src/tools/agent-harness-setup-smoke.ts +26 -1
  76. package/src/tools/agent-harness-tool-schema.ts +23 -1
  77. package/src/tools/agent-harness-tool-types.ts +5 -0
  78. package/src/tools/agent-harness-tool.ts +9 -1
  79. package/src/tools/agent-harness-workspace-actions.ts +1 -1
  80. package/src/tools/agent-memory-tool.ts +40 -1
  81. package/src/tools/agent-model-compare-run.ts +13 -0
  82. package/src/tools/agent-research-runner.ts +367 -0
  83. package/src/tools/agent-research-tool.ts +7 -179
  84. package/src/tools/agent-route-planner-candidates-surfaces.ts +1 -1
  85. package/src/version.ts +1 -1
@@ -0,0 +1,541 @@
1
+ import type { CommandContext } from '../input/command-registry.ts';
2
+ import { previewHarnessText } from './agent-harness-text.ts';
3
+
4
+ export interface RemoteCaptureOutcomeRecord {
5
+ readonly id: string;
6
+ readonly runnerId: string;
7
+ readonly status: string;
8
+ readonly kind: string;
9
+ readonly task: string | null;
10
+ readonly summary: string | null;
11
+ readonly captureId: string | null;
12
+ readonly exportId: string | null;
13
+ readonly artifactId: string | null;
14
+ readonly sourcePath: string;
15
+ readonly source: 'daemon-read-model' | 'sdk-read-model';
16
+ readonly redaction: string;
17
+ readonly startedAt: string | null;
18
+ readonly completedAt: string | null;
19
+ readonly modelRoute: string;
20
+ readonly certification: RemoteRuntimeRecordCertification;
21
+ }
22
+
23
+ export interface RemoteWorkspaceEvidenceRecord {
24
+ readonly id: string;
25
+ readonly runnerId: string;
26
+ readonly status: string;
27
+ readonly workspaceId: string | null;
28
+ readonly isolationKind: string;
29
+ readonly label: string | null;
30
+ readonly worktreeRef: string | null;
31
+ readonly branch: string | null;
32
+ readonly sourcePath: string;
33
+ readonly source: 'daemon-read-model' | 'sdk-read-model';
34
+ readonly redaction: string;
35
+ readonly modelRoute: string;
36
+ readonly certification: RemoteRuntimeRecordCertification;
37
+ }
38
+
39
+ export interface RemoteReadModelSnapshot {
40
+ readonly outcomes: readonly RemoteCaptureOutcomeRecord[];
41
+ readonly workspaces: readonly RemoteWorkspaceEvidenceRecord[];
42
+ readonly sourceCounts: Readonly<Record<string, number>>;
43
+ }
44
+
45
+ export interface RemoteRuntimeRecordCertification {
46
+ readonly schemaStatus: 'certified' | 'legacy';
47
+ readonly schemaVersion?: string;
48
+ readonly publicationGuarantee?: string;
49
+ readonly publisher?: string;
50
+ readonly provenance?: readonly string[];
51
+ readonly receiptId?: string;
52
+ readonly cursor?: string;
53
+ readonly missingSignals: readonly string[];
54
+ readonly policy: string;
55
+ }
56
+
57
+ interface SourceCandidate {
58
+ readonly path: string;
59
+ readonly source: unknown;
60
+ readonly kind: 'daemon-read-model' | 'sdk-read-model';
61
+ }
62
+
63
+ interface CollectedRecord {
64
+ readonly path: string;
65
+ readonly kind: 'daemon-read-model' | 'sdk-read-model';
66
+ readonly record: Record<string, unknown>;
67
+ }
68
+
69
+ const OUTCOME_WRAPPER_KEYS = [
70
+ 'records',
71
+ 'items',
72
+ 'outcomes',
73
+ 'captureOutcomes',
74
+ 'captures',
75
+ 'captureRecords',
76
+ 'exports',
77
+ 'exportOutcomes',
78
+ 'closeouts',
79
+ 'closeoutOutcomes',
80
+ 'artifacts',
81
+ ] as const;
82
+
83
+ const WORKSPACE_WRAPPER_KEYS = [
84
+ 'records',
85
+ 'items',
86
+ 'workspaces',
87
+ 'workspaceRecords',
88
+ 'workspaceIsolation',
89
+ 'workspaceEvidence',
90
+ 'worktrees',
91
+ 'worktreeRecords',
92
+ ] as const;
93
+
94
+ const SNAPSHOT_METHODS = [
95
+ 'getSnapshot',
96
+ 'snapshot',
97
+ 'toJSON',
98
+ ] as const;
99
+
100
+ const OUTCOME_METHODS = [
101
+ 'listCaptureOutcomes',
102
+ 'listOutcomes',
103
+ 'listCaptures',
104
+ 'listExports',
105
+ 'listCloseouts',
106
+ 'listArtifacts',
107
+ 'list',
108
+ ] as const;
109
+
110
+ const WORKSPACE_METHODS = [
111
+ 'listWorkspaceEvidence',
112
+ 'listWorkspaceIsolation',
113
+ 'listWorkspaces',
114
+ 'listWorktrees',
115
+ 'list',
116
+ ] as const;
117
+
118
+ function readObject(value: unknown): Record<string, unknown> {
119
+ return value && typeof value === 'object' ? value as Record<string, unknown> : {};
120
+ }
121
+
122
+ function readString(value: unknown): string {
123
+ return typeof value === 'string' ? value.trim() : '';
124
+ }
125
+
126
+ function readNumber(value: unknown): number {
127
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
128
+ }
129
+
130
+ function readMetadata(record: Record<string, unknown>): Record<string, unknown> {
131
+ return readObject(record.metadata);
132
+ }
133
+
134
+ function certificationRecords(record: Record<string, unknown>): readonly Record<string, unknown>[] {
135
+ return [
136
+ record,
137
+ readMetadata(record),
138
+ readObject(record.schema),
139
+ readObject(record.contract),
140
+ readObject(record.publication),
141
+ readObject(record.receipt),
142
+ readObject(record.certification),
143
+ ];
144
+ }
145
+
146
+ function nestedString(record: Record<string, unknown>, key: string): string {
147
+ return readString(record[key])
148
+ || readString(readMetadata(record)[key])
149
+ || readString(readObject(record.task)[key])
150
+ || readString(readObject(record.evidence)[key])
151
+ || readString(readObject(record.workspace)[key]);
152
+ }
153
+
154
+ function firstAcross(records: readonly Record<string, unknown>[], keys: readonly string[]): string {
155
+ for (const record of records) {
156
+ for (const key of keys) {
157
+ const value = readString(record[key]);
158
+ if (value) return value;
159
+ }
160
+ }
161
+ return '';
162
+ }
163
+
164
+ function stringArray(value: unknown): readonly string[] {
165
+ if (Array.isArray(value)) return [...new Set(value.map((entry) => readString(entry)).filter(Boolean))].slice(0, 10);
166
+ const text = readString(value);
167
+ return text ? text.split(',').map((entry) => entry.trim()).filter(Boolean).slice(0, 10) : [];
168
+ }
169
+
170
+ function redactText(value: string): string {
171
+ return value
172
+ .replace(/\b(bearer)\s+[a-z0-9._~+/=-]+/gi, '$1 <redacted>')
173
+ .replace(/\b(token|secret|password|api[_-]?key|authorization)\s*[:=]\s*[^,\s;/]+/gi, '$1=<redacted>');
174
+ }
175
+
176
+ function safePreview(value: string, limit: number): string {
177
+ return previewHarnessText(redactText(value), limit);
178
+ }
179
+
180
+ function safeNullablePreview(value: string, limit: number): string | null {
181
+ return value ? safePreview(value, limit) : null;
182
+ }
183
+
184
+ function certificationSchemaStatus(records: readonly Record<string, unknown>[]): RemoteRuntimeRecordCertification['schemaStatus'] {
185
+ const explicit = firstAcross(records, ['schemaStatus', 'receiptSchemaStatus', 'certificationStatus']).toLowerCase().replace(/[_\s]+/g, '-');
186
+ if (['certified', 'valid', 'verified', 'schema-certified'].includes(explicit)) return 'certified';
187
+ return firstAcross(records, ['schemaVersion', 'receiptSchemaVersion', 'contractVersion'])
188
+ && firstAcross(records, ['publicationGuarantee', 'hostPublicationGuarantee', 'remoteRuntimePublicationGuarantee'])
189
+ && firstAcross(records, ['publisher', 'publisherId', 'daemonId', 'hostId', 'methodId', 'sourceTool'])
190
+ ? 'certified'
191
+ : 'legacy';
192
+ }
193
+
194
+ function remoteCertification(input: {
195
+ readonly record: Record<string, unknown>;
196
+ readonly sourcePath: string;
197
+ readonly recordKind: 'remote outcome' | 'remote workspace';
198
+ readonly durableId: string;
199
+ readonly runnerId: string;
200
+ readonly modelRoute: string;
201
+ readonly hasOutcomeEvidence?: boolean;
202
+ readonly hasWorkspaceEvidence?: boolean;
203
+ readonly status: string;
204
+ }): RemoteRuntimeRecordCertification {
205
+ const records = certificationRecords(input.record);
206
+ const schemaStatus = certificationSchemaStatus(records);
207
+ const schemaVersion = firstAcross(records, ['schemaVersion', 'receiptSchemaVersion', 'contractVersion']);
208
+ const publicationGuarantee = firstAcross(records, ['publicationGuarantee', 'hostPublicationGuarantee', 'remoteRuntimePublicationGuarantee']);
209
+ const publisher = firstAcross(records, ['publisher', 'publisherId', 'daemonId', 'hostId']);
210
+ const receiptId = firstAcross(records, ['receiptId', 'operationReceiptId', 'captureReceiptId', 'exportReceiptId', 'closeoutReceiptId', 'workspaceReceiptId']);
211
+ const cursor = firstAcross(records, ['cursor', 'freshnessCursor', 'sequence', 'checkpoint']);
212
+ const provenance = [...new Set([
213
+ ...stringArray(input.record.provenance),
214
+ input.sourcePath ? `source ${input.sourcePath}` : '',
215
+ firstAcross(records, ['methodId']) ? `method ${firstAcross(records, ['methodId'])}` : '',
216
+ firstAcross(records, ['sourceTool']) ? `sourceTool ${firstAcross(records, ['sourceTool'])}` : '',
217
+ ].map((entry) => safePreview(entry, 180)).filter(Boolean))].slice(0, 8);
218
+ const missingSignals = [
219
+ ...(schemaStatus === 'certified' ? [] : [`Certified ${input.recordKind} schema is not published.`]),
220
+ ...(input.durableId ? [] : [`Durable ${input.recordKind} id is not published.`]),
221
+ ...(input.runnerId ? [] : [`${input.recordKind} runner id is not published.`]),
222
+ ...(publicationGuarantee ? [] : [`${input.recordKind} publication guarantee is not published.`]),
223
+ ...(publisher ? [] : [`${input.recordKind} publisher is not published.`]),
224
+ ...(provenance.length > 0 ? [] : [`${input.recordKind} provenance is not published.`]),
225
+ ...(cursor ? [] : [`${input.recordKind} freshness cursor is not published.`]),
226
+ ...(input.modelRoute ? [] : [`${input.recordKind} inspect route is not published.`]),
227
+ ...(input.status ? [] : [`${input.recordKind} status is not published.`]),
228
+ ...(input.hasOutcomeEvidence === false ? ['Remote capture/export/closeout/artifact evidence id is not published.'] : []),
229
+ ...(input.hasWorkspaceEvidence === false ? ['Remote workspace/worktree isolation evidence is not published.'] : []),
230
+ ];
231
+ return {
232
+ schemaStatus,
233
+ ...(schemaVersion ? { schemaVersion: safePreview(schemaVersion, 80) } : {}),
234
+ ...(publicationGuarantee ? { publicationGuarantee: safePreview(publicationGuarantee, 220) } : {}),
235
+ ...(publisher ? { publisher: safePreview(publisher, 80) } : {}),
236
+ ...(provenance.length > 0 ? { provenance } : {}),
237
+ ...(receiptId ? { receiptId: safePreview(receiptId, 96) } : {}),
238
+ ...(cursor ? { cursor: safePreview(cursor, 96) } : {}),
239
+ missingSignals,
240
+ policy: 'Remote runtime read models certify release readiness only when the SDK or daemon publishes schema, durable runner/evidence ids, publication guarantee, publisher/provenance, freshness cursor, exact inspect route, and redacted outcome or workspace isolation evidence.',
241
+ };
242
+ }
243
+
244
+ function safePathRef(value: string): string | null {
245
+ const redacted = redactText(value);
246
+ if (!redacted) return null;
247
+ const parts = redacted.split(/[\\/]+/).filter(Boolean);
248
+ if (parts.length <= 2) return safePreview(redacted, 120);
249
+ return safePreview(`.../${parts.slice(-2).join('/')}`, 120);
250
+ }
251
+
252
+ function timestampString(value: unknown): string | null {
253
+ const raw = readString(value);
254
+ if (raw) return safePreview(raw, 80);
255
+ const numeric = readNumber(value);
256
+ if (!numeric) return null;
257
+ const date = new Date(numeric);
258
+ return Number.isNaN(date.getTime()) ? String(numeric) : date.toISOString();
259
+ }
260
+
261
+ function callMethod(source: Record<string, unknown>, method: string): unknown {
262
+ const fn = source[method];
263
+ if (typeof fn !== 'function') return undefined;
264
+ try {
265
+ return (fn as () => unknown).call(source);
266
+ } catch {
267
+ return undefined;
268
+ }
269
+ }
270
+
271
+ function collectFromSource(
272
+ source: unknown,
273
+ path: string,
274
+ kind: 'daemon-read-model' | 'sdk-read-model',
275
+ wrapperKeys: readonly string[],
276
+ methods: readonly string[],
277
+ visited = new WeakSet<object>(),
278
+ ): readonly CollectedRecord[] {
279
+ if (!source) return [];
280
+ if (Array.isArray(source)) {
281
+ return source
282
+ .flatMap((entry, index) => collectFromSource(entry, `${path}[${index}]`, kind, wrapperKeys, methods, visited));
283
+ }
284
+ if (source instanceof Map) {
285
+ return Array.from(source.entries())
286
+ .flatMap(([key, value]) => collectFromSource(value, `${path}.${String(key)}`, kind, wrapperKeys, methods, visited));
287
+ }
288
+ if (typeof source !== 'object') return [];
289
+ if (visited.has(source)) return [];
290
+ visited.add(source);
291
+
292
+ const record = source as Record<string, unknown>;
293
+ const fromSnapshots = SNAPSHOT_METHODS.flatMap((method) => {
294
+ const snapshot = callMethod(record, method);
295
+ return snapshot === undefined ? [] : collectFromSource(snapshot, `${path}.${method}()`, kind, wrapperKeys, methods, visited);
296
+ });
297
+ const fromMethods = methods.flatMap((method) => {
298
+ const snapshot = callMethod(record, method);
299
+ return snapshot === undefined ? [] : collectFromSource(snapshot, `${path}.${method}()`, kind, wrapperKeys, methods, visited);
300
+ });
301
+ const fromWrappers = wrapperKeys.flatMap((key) => {
302
+ if (!(key in record)) return [];
303
+ return collectFromSource(record[key], `${path}.${key}`, kind, wrapperKeys, methods, visited);
304
+ });
305
+ if (fromSnapshots.length > 0 || fromMethods.length > 0 || fromWrappers.length > 0) {
306
+ return [...fromSnapshots, ...fromMethods, ...fromWrappers];
307
+ }
308
+ return [{ path, kind, record }];
309
+ }
310
+
311
+ function runnerId(record: Record<string, unknown>): string {
312
+ return nestedString(record, 'runnerId')
313
+ || nestedString(record, 'agentId')
314
+ || nestedString(record, 'linkedAgentId')
315
+ || nestedString(record, 'remoteRunnerId')
316
+ || nestedString(record, 'runner');
317
+ }
318
+
319
+ function routeForRunner(record: Record<string, unknown>, runner: string): string {
320
+ return nestedString(record, 'modelRoute')
321
+ || nestedString(record, 'inspectRoute')
322
+ || nestedString(record, 'reviewRoute')
323
+ || (runner ? `agent_harness mode:"agent_orchestration_agent" agentId:"${runner}" includeParameters:true` : 'agent_harness mode:"agent_orchestration" includeParameters:true');
324
+ }
325
+
326
+ function outcomeKind(record: Record<string, unknown>): string {
327
+ const raw = nestedString(record, 'kind')
328
+ || nestedString(record, 'type')
329
+ || nestedString(record, 'purpose')
330
+ || nestedString(record, 'operation');
331
+ const normalized = raw.toLowerCase();
332
+ if (normalized.includes('export')) return 'export';
333
+ if (normalized.includes('closeout')) return 'closeout';
334
+ if (normalized.includes('artifact')) return 'artifact';
335
+ if (normalized.includes('capture')) return 'capture';
336
+ return raw || 'remote-outcome';
337
+ }
338
+
339
+ function outcomeStatus(record: Record<string, unknown>): string {
340
+ return nestedString(record, 'status')
341
+ || nestedString(record, 'outcome')
342
+ || nestedString(record, 'result')
343
+ || nestedString(record, 'state')
344
+ || 'unknown';
345
+ }
346
+
347
+ function outcomeId(record: Record<string, unknown>, runner: string, kind: string, index: number): string {
348
+ return nestedString(record, 'id')
349
+ || nestedString(record, 'outcomeId')
350
+ || nestedString(record, 'captureId')
351
+ || nestedString(record, 'exportId')
352
+ || nestedString(record, 'artifactId')
353
+ || `remote-outcome:${runner || 'unknown'}:${kind}:${index}`;
354
+ }
355
+
356
+ function normalizeOutcome(entry: CollectedRecord, index: number): RemoteCaptureOutcomeRecord | null {
357
+ const runner = runnerId(entry.record);
358
+ if (!runner) return null;
359
+ const kind = outcomeKind(entry.record);
360
+ const task = nestedString(entry.record, 'task') || nestedString(entry.record, 'title') || nestedString(entry.record, 'request');
361
+ const summary = nestedString(entry.record, 'summary')
362
+ || nestedString(entry.record, 'description')
363
+ || nestedString(entry.record, 'logTail')
364
+ || nestedString(entry.record, 'message');
365
+ const id = outcomeId(entry.record, runner, kind, index);
366
+ const status = outcomeStatus(entry.record);
367
+ const captureId = nestedString(entry.record, 'captureId') || null;
368
+ const exportId = nestedString(entry.record, 'exportId') || null;
369
+ const artifactId = nestedString(entry.record, 'artifactId') || nestedString(entry.record, 'sourceArtifactId') || null;
370
+ const modelRoute = routeForRunner(entry.record, runner);
371
+ return {
372
+ id,
373
+ runnerId: runner,
374
+ status,
375
+ kind,
376
+ task: safeNullablePreview(task, 140),
377
+ summary: safeNullablePreview(summary, 220),
378
+ captureId,
379
+ exportId,
380
+ artifactId,
381
+ sourcePath: entry.path,
382
+ source: entry.kind,
383
+ redaction: nestedString(entry.record, 'redaction') || nestedString(entry.record, 'redactionPolicy') || 'bounded-redacted-read-model',
384
+ startedAt: timestampString(entry.record.startedAt ?? readMetadata(entry.record).startedAt),
385
+ completedAt: timestampString(entry.record.completedAt ?? entry.record.finishedAt ?? readMetadata(entry.record).completedAt),
386
+ modelRoute,
387
+ certification: remoteCertification({
388
+ record: entry.record,
389
+ sourcePath: entry.path,
390
+ recordKind: 'remote outcome',
391
+ durableId: id,
392
+ runnerId: runner,
393
+ modelRoute,
394
+ hasOutcomeEvidence: Boolean(captureId || exportId || artifactId || kind === 'closeout'),
395
+ status,
396
+ }),
397
+ };
398
+ }
399
+
400
+ function workspaceId(record: Record<string, unknown>): string {
401
+ return nestedString(record, 'workspaceId')
402
+ || nestedString(record, 'worktreeId')
403
+ || nestedString(record, 'id');
404
+ }
405
+
406
+ function workspaceStatus(record: Record<string, unknown>): string {
407
+ return nestedString(record, 'status')
408
+ || nestedString(record, 'state')
409
+ || nestedString(record, 'outcome')
410
+ || 'unknown';
411
+ }
412
+
413
+ function normalizeWorkspace(entry: CollectedRecord, index: number): RemoteWorkspaceEvidenceRecord | null {
414
+ const runner = runnerId(entry.record);
415
+ if (!runner) return null;
416
+ const id = workspaceId(entry.record) || `remote-workspace:${runner}:${index}`;
417
+ const path = nestedString(entry.record, 'worktreePath')
418
+ || nestedString(entry.record, 'workspacePath')
419
+ || nestedString(entry.record, 'path')
420
+ || nestedString(entry.record, 'root')
421
+ || nestedString(entry.record, 'directory');
422
+ const status = workspaceStatus(entry.record);
423
+ const modelRoute = routeForRunner(entry.record, runner);
424
+ const workspace = workspaceId(entry.record) || null;
425
+ const worktreeRef = safePathRef(path);
426
+ const branch = safeNullablePreview(nestedString(entry.record, 'branch') || nestedString(entry.record, 'ref') || nestedString(entry.record, 'baseRef'), 100);
427
+ return {
428
+ id,
429
+ runnerId: runner,
430
+ status,
431
+ workspaceId: workspace,
432
+ isolationKind: nestedString(entry.record, 'isolationKind')
433
+ || nestedString(entry.record, 'workspaceKind')
434
+ || nestedString(entry.record, 'kind')
435
+ || 'workspace',
436
+ label: safeNullablePreview(nestedString(entry.record, 'label') || nestedString(entry.record, 'name') || nestedString(entry.record, 'summary'), 120),
437
+ worktreeRef,
438
+ branch,
439
+ sourcePath: entry.path,
440
+ source: entry.kind,
441
+ redaction: nestedString(entry.record, 'redaction') || nestedString(entry.record, 'redactionPolicy') || 'path-bounded-read-model',
442
+ modelRoute,
443
+ certification: remoteCertification({
444
+ record: entry.record,
445
+ sourcePath: entry.path,
446
+ recordKind: 'remote workspace',
447
+ durableId: id,
448
+ runnerId: runner,
449
+ modelRoute,
450
+ hasWorkspaceEvidence: Boolean(workspace || worktreeRef || branch),
451
+ status,
452
+ }),
453
+ };
454
+ }
455
+
456
+ function dedupeById<T extends { readonly id: string; readonly runnerId: string }>(records: readonly T[]): readonly T[] {
457
+ const seen = new Set<string>();
458
+ const output: T[] = [];
459
+ for (const record of records) {
460
+ const key = `${record.runnerId}:${record.id}`;
461
+ if (seen.has(key)) continue;
462
+ seen.add(key);
463
+ output.push(record);
464
+ }
465
+ return output;
466
+ }
467
+
468
+ function sourceCounts(entries: readonly CollectedRecord[]): Readonly<Record<string, number>> {
469
+ const counts: Record<string, number> = {};
470
+ for (const entry of entries) counts[entry.path] = (counts[entry.path] ?? 0) + 1;
471
+ return counts;
472
+ }
473
+
474
+ function outcomeSources(context: CommandContext): readonly SourceCandidate[] {
475
+ const platform = readObject(context.platform);
476
+ const readModels = readObject(platform.readModels);
477
+ const remoteRuntime = readObject(readModels.remoteRuntime);
478
+ const remoteRunners = readObject(readModels.remoteRunners);
479
+ const remote = readObject(readModels.remote);
480
+ const opsRemote = readObject(readObject(context.ops).remoteRuntime);
481
+ const opsReadModels = readObject(opsRemote.readModels);
482
+ const clients = readObject(context.clients);
483
+ const operator = readObject(clients.operator);
484
+ const operatorRemote = readObject(operator.remoteRuntime);
485
+ return [
486
+ { path: 'context.platform.readModels.remoteRuntime.captureOutcomes', source: remoteRuntime.captureOutcomes, kind: 'daemon-read-model' },
487
+ { path: 'context.platform.readModels.remoteRuntime.outcomes', source: remoteRuntime.outcomes, kind: 'daemon-read-model' },
488
+ { path: 'context.platform.readModels.remoteRuntime.captures', source: remoteRuntime.captures, kind: 'daemon-read-model' },
489
+ { path: 'context.platform.readModels.remoteRuntime.exports', source: remoteRuntime.exports, kind: 'daemon-read-model' },
490
+ { path: 'context.platform.readModels.remoteRuntime.closeouts', source: remoteRuntime.closeouts, kind: 'daemon-read-model' },
491
+ { path: 'context.platform.readModels.remoteRunners.captureOutcomes', source: remoteRunners.captureOutcomes, kind: 'daemon-read-model' },
492
+ { path: 'context.platform.readModels.remote.captureOutcomes', source: remote.captureOutcomes, kind: 'daemon-read-model' },
493
+ { path: 'context.platform.readModels.remoteCaptureOutcomes', source: readModels.remoteCaptureOutcomes, kind: 'daemon-read-model' },
494
+ { path: 'context.platform.readModels.remoteRunnerOutcomes', source: readModels.remoteRunnerOutcomes, kind: 'daemon-read-model' },
495
+ { path: 'context.ops.remoteRuntime.readModels.captureOutcomes', source: opsReadModels.captureOutcomes, kind: 'sdk-read-model' },
496
+ { path: 'context.clients.operator.remoteRuntime.captureOutcomes', source: operatorRemote.captureOutcomes, kind: 'sdk-read-model' },
497
+ ];
498
+ }
499
+
500
+ function workspaceSources(context: CommandContext): readonly SourceCandidate[] {
501
+ const platform = readObject(context.platform);
502
+ const readModels = readObject(platform.readModels);
503
+ const remoteRuntime = readObject(readModels.remoteRuntime);
504
+ const remoteRunners = readObject(readModels.remoteRunners);
505
+ const remote = readObject(readModels.remote);
506
+ const worktrees = readObject(readModels.worktrees);
507
+ const opsRemote = readObject(readObject(context.ops).remoteRuntime);
508
+ const opsReadModels = readObject(opsRemote.readModels);
509
+ const clients = readObject(context.clients);
510
+ const operator = readObject(clients.operator);
511
+ const operatorRemote = readObject(operator.remoteRuntime);
512
+ return [
513
+ { path: 'context.platform.readModels.remoteRuntime.workspaces', source: remoteRuntime.workspaces, kind: 'daemon-read-model' },
514
+ { path: 'context.platform.readModels.remoteRuntime.workspaceIsolation', source: remoteRuntime.workspaceIsolation, kind: 'daemon-read-model' },
515
+ { path: 'context.platform.readModels.remoteRuntime.worktrees', source: remoteRuntime.worktrees, kind: 'daemon-read-model' },
516
+ { path: 'context.platform.readModels.remoteRunners.workspaces', source: remoteRunners.workspaces, kind: 'daemon-read-model' },
517
+ { path: 'context.platform.readModels.remote.workspaceIsolation', source: remote.workspaceIsolation, kind: 'daemon-read-model' },
518
+ { path: 'context.platform.readModels.remoteWorkspaces', source: readModels.remoteWorkspaces, kind: 'daemon-read-model' },
519
+ { path: 'context.platform.readModels.remoteWorkspaceIsolation', source: readModels.remoteWorkspaceIsolation, kind: 'daemon-read-model' },
520
+ { path: 'context.platform.readModels.worktrees.remote', source: worktrees.remote, kind: 'daemon-read-model' },
521
+ { path: 'context.ops.remoteRuntime.readModels.workspaces', source: opsReadModels.workspaces, kind: 'sdk-read-model' },
522
+ { path: 'context.clients.operator.remoteRuntime.workspaces', source: operatorRemote.workspaces, kind: 'sdk-read-model' },
523
+ ];
524
+ }
525
+
526
+ export function remoteReadModelSnapshot(context: CommandContext): RemoteReadModelSnapshot {
527
+ const outcomeEntries = outcomeSources(context).flatMap((candidate) =>
528
+ collectFromSource(candidate.source, candidate.path, candidate.kind, OUTCOME_WRAPPER_KEYS, OUTCOME_METHODS)
529
+ );
530
+ const workspaceEntries = workspaceSources(context).flatMap((candidate) =>
531
+ collectFromSource(candidate.source, candidate.path, candidate.kind, WORKSPACE_WRAPPER_KEYS, WORKSPACE_METHODS)
532
+ );
533
+ return {
534
+ outcomes: dedupeById(outcomeEntries.map(normalizeOutcome).filter((entry): entry is RemoteCaptureOutcomeRecord => entry !== null)),
535
+ workspaces: dedupeById(workspaceEntries.map(normalizeWorkspace).filter((entry): entry is RemoteWorkspaceEvidenceRecord => entry !== null)),
536
+ sourceCounts: {
537
+ ...sourceCounts(outcomeEntries),
538
+ ...sourceCounts(workspaceEntries),
539
+ },
540
+ };
541
+ }
@@ -12,6 +12,7 @@ import {
12
12
  type AgentResearchSourceRecord,
13
13
  } from '../agent/research-source-registry.ts';
14
14
  import { browserControlPosture } from './agent-harness-browser-control.ts';
15
+ import { isCertifiedResearchLiveRecord, researchLiveReadModelSnapshot } from './agent-harness-research-live-read-models.ts';
15
16
  import { previewHarnessText } from './agent-harness-text.ts';
16
17
 
17
18
  interface AgentHarnessResearchBriefingArgs {
@@ -313,30 +314,41 @@ function workflowItem(query: string, includeParameters: boolean): ResearchBriefi
313
314
 
314
315
  function browserItem(context: CommandContext, includeParameters: boolean): ResearchBriefingItem {
315
316
  const browser = browserControlPosture(context);
317
+ const liveResearch = researchLiveReadModelSnapshot(context);
318
+ const certifiedRunnerRecords = liveResearch.browserRunnerRecords.filter(isCertifiedResearchLiveRecord);
319
+ const certifiedVisualReports = liveResearch.visualReportRecords.filter(isCertifiedResearchLiveRecord);
320
+ const hasCertifiedRunner = certifiedRunnerRecords.length > 0;
316
321
  return {
317
322
  id: 'browser:research-runner',
318
323
  kind: 'browser',
319
- priority: browser.configured ? 60 : 28,
324
+ priority: hasCertifiedRunner ? 86 : browser.configured ? 60 : 28,
320
325
  label: 'Browser-backed research runner',
321
- status: browser.configured ? 'ready-with-confirmation' : 'setup-needed',
322
- summary: browser.configured
326
+ status: hasCertifiedRunner ? 'certified-live-runner' : browser.configured ? 'ready-with-confirmation' : 'setup-needed',
327
+ summary: hasCertifiedRunner
328
+ ? 'The SDK/daemon published certified live browser-backed research runner evidence.'
329
+ : browser.configured
323
330
  ? 'Browser or desktop control is configured; use it only when public web/fetch is insufficient.'
324
331
  : 'Browser-backed research needs setup before live browser execution should be promised.',
325
- next: browser.configured
332
+ next: hasCertifiedRunner
333
+ ? 'Inspect certified run, source receipt, and visual-render evidence before saving reports or promoting Knowledge.'
334
+ : browser.configured
326
335
  ? 'Use browser-backed research only after the user confirms authenticated or interactive scope.'
327
336
  : 'Use bounded public search now, or inspect browser setup if live browser state is required.',
328
337
  routes: {
329
338
  runner: 'research action:"runner"',
330
339
  setup: browser.setupRoute,
331
- recommended: browser.recommendedRoute,
340
+ recommended: certifiedRunnerRecords[0]?.modelRoute ?? browser.recommendedRoute,
332
341
  fallback: browser.fallbackRoutes[0] ?? 'research action:"search"',
342
+ ...(certifiedVisualReports[0] ? { visualReport: certifiedVisualReports[0].modelRoute } : {}),
333
343
  },
334
344
  confirmationBoundary: 'This briefing never opens the browser; browser handoff stays on confirmed computer/browser routes.',
335
345
  ...(includeParameters ? {
336
346
  detail: {
337
- configured: browser.configured,
347
+ configured: browser.configured || hasCertifiedRunner,
338
348
  needsReview: browser.needsReview,
339
349
  fallbackRoutes: browser.fallbackRoutes,
350
+ liveRunnerRecords: liveResearch.browserRunnerRecords.slice(0, 8),
351
+ liveVisualReportRecords: liveResearch.visualReportRecords.slice(0, 8),
340
352
  },
341
353
  } : {}),
342
354
  };
@@ -347,6 +359,7 @@ export function researchBriefingCatalogStatus(context: CommandContext): Record<s
347
359
  const runs = shellPaths ? AgentResearchRunRegistry.fromShellPaths(shellPaths).snapshot() : null;
348
360
  const sources = shellPaths ? AgentResearchSourceRegistry.fromShellPaths(shellPaths).snapshot() : null;
349
361
  const reports = reportArtifacts(context, '');
362
+ const liveResearch = researchLiveReadModelSnapshot(context);
350
363
  return {
351
364
  modes: ['research_briefing'],
352
365
  status: shellPaths ? 'ready' : 'unavailable',
@@ -354,6 +367,8 @@ export function researchBriefingCatalogStatus(context: CommandContext): Record<s
354
367
  candidateSources: sources?.candidates.length ?? 0,
355
368
  reviewedSources: sources?.reviewed.length ?? 0,
356
369
  savedReports: reports.length,
370
+ liveBrowserRuns: liveResearch.browserRunnerRecords.length,
371
+ liveVisualReports: liveResearch.visualReportRecords.length,
357
372
  readOnly: true,
358
373
  };
359
374
  }
@@ -368,6 +383,8 @@ export function researchBriefingSummary(context: CommandContext, args: AgentHarn
368
383
  const matchedRuns = runs.filter((run) => !normalized || runSearchText(run).includes(normalized));
369
384
  const matchedSources = matchingSources(sources, matchedRuns, query);
370
385
  const reports = reportArtifacts(context, query);
386
+ const liveResearch = researchLiveReadModelSnapshot(context);
387
+ const certifiedRunnerRecords = liveResearch.browserRunnerRecords.filter(isCertifiedResearchLiveRecord);
371
388
  const activeRuns = matchedRuns.filter((run) => isMutableRun(run.status));
372
389
  const candidateSources = matchedSources.filter((source) => source.status === 'candidate');
373
390
  const reviewedSources = matchedSources.filter((source) => source.status === 'reviewed');
@@ -404,7 +421,9 @@ export function researchBriefingSummary(context: CommandContext, args: AgentHarn
404
421
  candidateSources: candidateSources.length,
405
422
  reviewedSources: reviewedSources.length,
406
423
  savedReports: reports.length,
407
- browserReady: browserControlPosture(context).configured,
424
+ browserReady: browserControlPosture(context).configured || certifiedRunnerRecords.length > 0,
425
+ liveBrowserRuns: liveResearch.browserRunnerRecords.length,
426
+ liveVisualReports: liveResearch.visualReportRecords.length,
408
427
  },
409
428
  queue: items.slice(0, limit),
410
429
  returned: Math.min(items.length, limit),