@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,509 @@
|
|
|
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 { safeRecordIdPart } from './agent-harness-personal-ops-records.ts';
|
|
6
|
+
import type { AutonomyQueueLiveRecord, AutonomyQueueRecordControl, AutonomyQueueRecordOutput } from './agent-harness-autonomy-queue-types.ts';
|
|
7
|
+
|
|
8
|
+
type WatcherReadModelLane = 'watcher-run' | 'provider-source';
|
|
9
|
+
type WatcherReadModelSourceKind = 'daemon-read-model' | 'sdk-read-model';
|
|
10
|
+
|
|
11
|
+
interface WatcherReadModelSource {
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly source: unknown;
|
|
14
|
+
readonly kind: WatcherReadModelSourceKind;
|
|
15
|
+
readonly lane: WatcherReadModelLane;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface CollectedWatcherRecord {
|
|
19
|
+
readonly path: string;
|
|
20
|
+
readonly kind: WatcherReadModelSourceKind;
|
|
21
|
+
readonly lane: WatcherReadModelLane;
|
|
22
|
+
readonly record: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SNAPSHOT_METHODS = ['getSnapshot', 'snapshot', 'toJSON'] as const;
|
|
26
|
+
const RUN_METHODS = ['listRuns', 'listRunHistory', 'listWatcherRuns', 'listAutomationRuns', 'listReceipts', 'listEvents', 'list'] as const;
|
|
27
|
+
const SOURCE_METHODS = ['listSources', 'listProviderSources', 'listWatcherSources', 'listWatchers', 'listSubscriptions', 'list'] as const;
|
|
28
|
+
|
|
29
|
+
const RUN_WRAPPER_KEYS = [
|
|
30
|
+
'records',
|
|
31
|
+
'items',
|
|
32
|
+
'runs',
|
|
33
|
+
'runHistory',
|
|
34
|
+
'history',
|
|
35
|
+
'watcherRuns',
|
|
36
|
+
'automationRuns',
|
|
37
|
+
'watcherRunHistory',
|
|
38
|
+
'events',
|
|
39
|
+
'receipts',
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
const SOURCE_WRAPPER_KEYS = [
|
|
43
|
+
'records',
|
|
44
|
+
'items',
|
|
45
|
+
'sources',
|
|
46
|
+
'sourceRecords',
|
|
47
|
+
'providerSources',
|
|
48
|
+
'watcherSources',
|
|
49
|
+
'watchers',
|
|
50
|
+
'subscriptions',
|
|
51
|
+
'accounts',
|
|
52
|
+
'mailboxes',
|
|
53
|
+
] as const;
|
|
54
|
+
|
|
55
|
+
const RUN_READ_KEYS = ['readRoute', 'inspectRoute', 'reviewRoute', 'watcherRunRoute', 'runRoute', 'modelRoute'];
|
|
56
|
+
const RUN_OUTPUT_KEYS = ['outputRoute', 'logsRoute', 'tailRoute', 'chunkRoute', 'streamRoute', 'outputStreamRoute'];
|
|
57
|
+
const RUN_CANCEL_KEYS = ['cancelRoute', 'stopRoute', 'abortRoute'];
|
|
58
|
+
const RUN_RETRY_KEYS = ['retryRoute', 'rerunRoute', 'replayRoute', 'recoveryRoute'];
|
|
59
|
+
const RUN_CHECKPOINT_KEYS = ['checkpointRoute', 'saveCheckpointRoute'];
|
|
60
|
+
const RUN_PAUSE_KEYS = ['pauseRoute'];
|
|
61
|
+
const RUN_RESUME_KEYS = ['resumeRoute'];
|
|
62
|
+
const SOURCE_READ_KEYS = ['readRoute', 'inspectRoute', 'reviewRoute', 'sourceRoute', 'watcherRoute', 'modelRoute'];
|
|
63
|
+
const SOURCE_REFRESH_KEYS = ['refreshRoute', 'syncRoute', 'pollRoute', 'readNowRoute'];
|
|
64
|
+
|
|
65
|
+
function watcherSources(context: CommandContext): readonly WatcherReadModelSource[] {
|
|
66
|
+
const platform = context.platform as unknown as Record<string, unknown>;
|
|
67
|
+
const readModels = readRecord(platform.readModels);
|
|
68
|
+
const clients = readRecord(context.clients);
|
|
69
|
+
const operator = readRecord(clients.operator);
|
|
70
|
+
const automation = readRecord(readModels.automation);
|
|
71
|
+
const watchers = readRecord(readModels.watchers);
|
|
72
|
+
const gmail = readRecord(readModels.gmail);
|
|
73
|
+
const email = readRecord(readModels.email);
|
|
74
|
+
const provider = readRecord(readModels.providers);
|
|
75
|
+
const operatorAutomation = readRecord(operator.automation);
|
|
76
|
+
const operatorWatchers = readRecord(operator.watchers);
|
|
77
|
+
const operatorGmail = readRecord(operator.gmail);
|
|
78
|
+
const operatorEmail = readRecord(operator.email);
|
|
79
|
+
return [
|
|
80
|
+
{ path: 'context.platform.readModels.watchers.runs', source: watchers.runs, kind: 'daemon-read-model', lane: 'watcher-run' },
|
|
81
|
+
{ path: 'context.platform.readModels.watchers.runHistory', source: watchers.runHistory, kind: 'daemon-read-model', lane: 'watcher-run' },
|
|
82
|
+
{ path: 'context.platform.readModels.watchers.watcherRuns', source: watchers.watcherRuns, kind: 'daemon-read-model', lane: 'watcher-run' },
|
|
83
|
+
{ path: 'context.platform.readModels.watcherRuns', source: readModels.watcherRuns, kind: 'daemon-read-model', lane: 'watcher-run' },
|
|
84
|
+
{ path: 'context.platform.readModels.watcherRunHistory', source: readModels.watcherRunHistory, kind: 'daemon-read-model', lane: 'watcher-run' },
|
|
85
|
+
{ path: 'context.platform.readModels.automation.watcherRuns', source: automation.watcherRuns, kind: 'daemon-read-model', lane: 'watcher-run' },
|
|
86
|
+
{ path: 'context.platform.readModels.automation.runHistory', source: automation.runHistory, kind: 'daemon-read-model', lane: 'watcher-run' },
|
|
87
|
+
{ path: 'context.platform.readModels.gmail.watcherRuns', source: gmail.watcherRuns, kind: 'daemon-read-model', lane: 'watcher-run' },
|
|
88
|
+
{ path: 'context.platform.readModels.email.watcherRuns', source: email.watcherRuns, kind: 'daemon-read-model', lane: 'watcher-run' },
|
|
89
|
+
{ path: 'context.clients.operator.watchers.runs', source: operatorWatchers.runs, kind: 'sdk-read-model', lane: 'watcher-run' },
|
|
90
|
+
{ path: 'context.clients.operator.watchers.runHistory', source: operatorWatchers.runHistory, kind: 'sdk-read-model', lane: 'watcher-run' },
|
|
91
|
+
{ path: 'context.clients.operator.automation.watcherRuns', source: operatorAutomation.watcherRuns, kind: 'sdk-read-model', lane: 'watcher-run' },
|
|
92
|
+
|
|
93
|
+
{ path: 'context.platform.readModels.watchers.sources', source: watchers.sources, kind: 'daemon-read-model', lane: 'provider-source' },
|
|
94
|
+
{ path: 'context.platform.readModels.watchers.providerSources', source: watchers.providerSources, kind: 'daemon-read-model', lane: 'provider-source' },
|
|
95
|
+
{ path: 'context.platform.readModels.watcherSources', source: readModels.watcherSources, kind: 'daemon-read-model', lane: 'provider-source' },
|
|
96
|
+
{ path: 'context.platform.readModels.providerSources', source: readModels.providerSources, kind: 'daemon-read-model', lane: 'provider-source' },
|
|
97
|
+
{ path: 'context.platform.readModels.providers.sources', source: provider.sources, kind: 'daemon-read-model', lane: 'provider-source' },
|
|
98
|
+
{ path: 'context.platform.readModels.gmail.sources', source: gmail.sources, kind: 'daemon-read-model', lane: 'provider-source' },
|
|
99
|
+
{ path: 'context.platform.readModels.gmail.providerSources', source: gmail.providerSources, kind: 'daemon-read-model', lane: 'provider-source' },
|
|
100
|
+
{ path: 'context.platform.readModels.gmail.watchers', source: gmail.watchers, kind: 'daemon-read-model', lane: 'provider-source' },
|
|
101
|
+
{ path: 'context.platform.readModels.email.sources', source: email.sources, kind: 'daemon-read-model', lane: 'provider-source' },
|
|
102
|
+
{ path: 'context.platform.readModels.email.providerSources', source: email.providerSources, kind: 'daemon-read-model', lane: 'provider-source' },
|
|
103
|
+
{ path: 'context.platform.readModels.email.watchers', source: email.watchers, kind: 'daemon-read-model', lane: 'provider-source' },
|
|
104
|
+
{ path: 'context.clients.operator.watchers.sources', source: operatorWatchers.sources, kind: 'sdk-read-model', lane: 'provider-source' },
|
|
105
|
+
{ path: 'context.clients.operator.gmail.sources', source: operatorGmail.sources, kind: 'sdk-read-model', lane: 'provider-source' },
|
|
106
|
+
{ path: 'context.clients.operator.email.sources', source: operatorEmail.sources, kind: 'sdk-read-model', lane: 'provider-source' },
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function callMethod(source: Record<string, unknown>, method: string): unknown {
|
|
111
|
+
const fn = source[method];
|
|
112
|
+
if (typeof fn !== 'function') return undefined;
|
|
113
|
+
try {
|
|
114
|
+
const result = (fn as () => unknown).call(source);
|
|
115
|
+
return result instanceof Promise ? undefined : result;
|
|
116
|
+
} catch {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function firstString(record: Record<string, unknown>, keys: readonly string[]): string {
|
|
122
|
+
const containers = [
|
|
123
|
+
record,
|
|
124
|
+
readRecord(record.metadata),
|
|
125
|
+
readRecord(record.evidence),
|
|
126
|
+
readRecord(record.source),
|
|
127
|
+
readRecord(record.providerSource),
|
|
128
|
+
readRecord(record.provider),
|
|
129
|
+
readRecord(record.watcher),
|
|
130
|
+
readRecord(record.run),
|
|
131
|
+
readRecord(record.event),
|
|
132
|
+
readRecord(record.route),
|
|
133
|
+
readRecord(record.routes),
|
|
134
|
+
];
|
|
135
|
+
for (const container of containers) {
|
|
136
|
+
for (const key of keys) {
|
|
137
|
+
const value = readString(container[key]);
|
|
138
|
+
if (value) return value;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return '';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readBoolean(record: Record<string, unknown>, keys: readonly string[]): boolean | undefined {
|
|
145
|
+
for (const key of keys) {
|
|
146
|
+
const value = record[key] ?? readRecord(record.metadata)[key] ?? readRecord(record.source)[key];
|
|
147
|
+
if (typeof value === 'boolean') return value;
|
|
148
|
+
if (typeof value === 'string') {
|
|
149
|
+
const normalized = value.trim().toLowerCase();
|
|
150
|
+
if (normalized === 'true') return true;
|
|
151
|
+
if (normalized === 'false') return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function readStringArray(value: unknown): readonly string[] {
|
|
158
|
+
if (Array.isArray(value)) return [...new Set(value.map((entry) => readString(entry)).filter(Boolean))].slice(0, 12);
|
|
159
|
+
const text = readString(value);
|
|
160
|
+
return text ? text.split(',').map((entry) => entry.trim()).filter(Boolean).slice(0, 12) : [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function redactWatcherText(value: string): string {
|
|
164
|
+
return redactedPersonalOpsText(value)
|
|
165
|
+
.replace(/("?\b(?:api[-_]?key|apikey|token|secret|password|passwd|credential|authorization)\b"?\s*:\s*)("[^"]*"|'[^']*'|[^\s,}]+)/gi, '$1"<redacted>"')
|
|
166
|
+
.replace(/(\b(?:token|secret|password|passwd|api[-_]?key|apikey|authorization|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,}]+)/gi, '$1<redacted>');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function compactUnknown(value: unknown): string {
|
|
170
|
+
if (value === undefined || value === null) return '';
|
|
171
|
+
if (typeof value === 'string') return redactWatcherText(value.replace(/\s+/g, ' ').trim());
|
|
172
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
173
|
+
try {
|
|
174
|
+
return redactWatcherText(JSON.stringify(value).replace(/\s+/g, ' ').trim());
|
|
175
|
+
} catch {
|
|
176
|
+
return '';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function safePreview(value: string, limit: number): string {
|
|
181
|
+
return previewHarnessText(redactWatcherText(value), limit);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function readIso(value: unknown): string {
|
|
185
|
+
if (typeof value === 'number' && Number.isFinite(value)) return new Date(value).toISOString();
|
|
186
|
+
const text = readString(value);
|
|
187
|
+
if (!text) return '';
|
|
188
|
+
const parsed = Date.parse(text);
|
|
189
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : safePreview(text, 80);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function readRoute(value: unknown): string {
|
|
193
|
+
const direct = readString(value);
|
|
194
|
+
if (direct) return direct;
|
|
195
|
+
const route = readRecord(value);
|
|
196
|
+
return firstString(route, ['modelRoute', 'route', 'command', 'action', 'href']);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function routeFromKeys(record: Record<string, unknown>, keys: readonly string[]): string {
|
|
200
|
+
for (const key of keys) {
|
|
201
|
+
const route = readRoute(record[key]);
|
|
202
|
+
if (route) return route;
|
|
203
|
+
}
|
|
204
|
+
for (const containerKey of ['routes', 'effectRoutes', 'actions', 'controls']) {
|
|
205
|
+
const routes = readRecord(record[containerKey]);
|
|
206
|
+
for (const key of keys) {
|
|
207
|
+
const route = readRoute(routes[key]);
|
|
208
|
+
if (route) return route;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return '';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function normalizedStatus(record: Record<string, unknown>, lane: WatcherReadModelLane): string {
|
|
215
|
+
const raw = firstString(record, ['status', 'state', 'outcome', 'result']);
|
|
216
|
+
if (!raw && lane === 'provider-source') {
|
|
217
|
+
const enabled = readBoolean(record, ['enabled', 'active', 'watching']);
|
|
218
|
+
if (enabled === true) return 'ready';
|
|
219
|
+
if (enabled === false) return 'blocked';
|
|
220
|
+
}
|
|
221
|
+
const status = raw.toLowerCase().replace(/[\s_]+/g, '-');
|
|
222
|
+
if (!status) return lane === 'provider-source' ? 'ready' : 'unknown';
|
|
223
|
+
if (['ok', 'ready', 'success', 'succeeded', 'complete', 'completed', 'delivered', 'captured', 'recorded'].includes(status)) return 'succeeded';
|
|
224
|
+
if (['pending', 'scheduled'].includes(status)) return 'queued';
|
|
225
|
+
if (['in-progress', 'active', 'executing', 'processing'].includes(status)) return 'running';
|
|
226
|
+
if (['needs-review', 'needs-setup', 'error-blocked'].includes(status)) return 'blocked';
|
|
227
|
+
if (['fail', 'error', 'errored'].includes(status)) return 'failed';
|
|
228
|
+
if (['cancelled', 'canceled', 'aborted'].includes(status)) return 'cancelled';
|
|
229
|
+
return status;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function looksLikeRecord(record: Record<string, unknown>, lane: WatcherReadModelLane): boolean {
|
|
233
|
+
const keys = lane === 'watcher-run'
|
|
234
|
+
? ['runId', 'watcherRunId', 'automationRunId', 'jobRunId', 'eventId', 'id', 'status', 'outcome']
|
|
235
|
+
: ['sourceId', 'providerSourceId', 'watcherId', 'subscriptionId', 'accountId', 'id', 'scope', 'filter'];
|
|
236
|
+
return firstString(record, keys) !== '';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function collectFromSource(
|
|
240
|
+
source: unknown,
|
|
241
|
+
path: string,
|
|
242
|
+
kind: WatcherReadModelSourceKind,
|
|
243
|
+
lane: WatcherReadModelLane,
|
|
244
|
+
visited = new WeakSet<object>(),
|
|
245
|
+
depth = 0,
|
|
246
|
+
): readonly CollectedWatcherRecord[] {
|
|
247
|
+
if (!source) return [];
|
|
248
|
+
if (Array.isArray(source)) {
|
|
249
|
+
return source.flatMap((entry, index) => collectFromSource(entry, `${path}[${index}]`, kind, lane, visited, depth + 1));
|
|
250
|
+
}
|
|
251
|
+
if (source instanceof Map) {
|
|
252
|
+
return Array.from(source.entries())
|
|
253
|
+
.flatMap(([key, value]) => collectFromSource(value, `${path}.${String(key)}`, kind, lane, visited, depth + 1));
|
|
254
|
+
}
|
|
255
|
+
if (typeof source !== 'object') return [];
|
|
256
|
+
if (visited.has(source)) return [];
|
|
257
|
+
visited.add(source);
|
|
258
|
+
|
|
259
|
+
const record = source as Record<string, unknown>;
|
|
260
|
+
const wrapperKeys = lane === 'watcher-run' ? RUN_WRAPPER_KEYS : SOURCE_WRAPPER_KEYS;
|
|
261
|
+
const methods = lane === 'watcher-run' ? RUN_METHODS : SOURCE_METHODS;
|
|
262
|
+
const fromSnapshots = SNAPSHOT_METHODS.flatMap((method) => {
|
|
263
|
+
const snapshot = callMethod(record, method);
|
|
264
|
+
return snapshot === undefined ? [] : collectFromSource(snapshot, `${path}.${method}()`, kind, lane, visited, depth + 1);
|
|
265
|
+
});
|
|
266
|
+
const fromMethods = methods.flatMap((method) => {
|
|
267
|
+
const snapshot = callMethod(record, method);
|
|
268
|
+
return snapshot === undefined ? [] : collectFromSource(snapshot, `${path}.${method}()`, kind, lane, visited, depth + 1);
|
|
269
|
+
});
|
|
270
|
+
const fromWrappers = wrapperKeys.flatMap((key) => {
|
|
271
|
+
if (!(key in record)) return [];
|
|
272
|
+
return collectFromSource(record[key], `${path}.${key}`, kind, lane, visited, depth + 1);
|
|
273
|
+
});
|
|
274
|
+
if (fromSnapshots.length > 0 || fromMethods.length > 0 || fromWrappers.length > 0) {
|
|
275
|
+
return [...fromSnapshots, ...fromMethods, ...fromWrappers];
|
|
276
|
+
}
|
|
277
|
+
if (looksLikeRecord(record, lane)) return [{ path, kind, lane, record }];
|
|
278
|
+
if (depth >= 3) return [];
|
|
279
|
+
return Object.entries(record)
|
|
280
|
+
.flatMap(([key, value]) => collectFromSource(value, `${path}.${key}`, kind, lane, visited, depth + 1));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function availableControl(id: string, label: string, effect: AutonomyQueueRecordControl['effect'], modelRoute: string): AutonomyQueueRecordControl {
|
|
284
|
+
return {
|
|
285
|
+
id,
|
|
286
|
+
label,
|
|
287
|
+
state: 'available',
|
|
288
|
+
effect,
|
|
289
|
+
confirmationRequired: effect === 'confirmed-effect',
|
|
290
|
+
modelRoute,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function unavailableControl(id: string, label: string, reason: string): AutonomyQueueRecordControl {
|
|
295
|
+
return {
|
|
296
|
+
id,
|
|
297
|
+
label,
|
|
298
|
+
state: 'unavailable',
|
|
299
|
+
effect: 'confirmed-effect',
|
|
300
|
+
confirmationRequired: true,
|
|
301
|
+
reason,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function effectControl(
|
|
306
|
+
id: string,
|
|
307
|
+
label: string,
|
|
308
|
+
route: string,
|
|
309
|
+
enabled: boolean,
|
|
310
|
+
disabledReason: string,
|
|
311
|
+
): AutonomyQueueRecordControl {
|
|
312
|
+
return route && enabled
|
|
313
|
+
? availableControl(id, label, 'confirmed-effect', route)
|
|
314
|
+
: unavailableControl(id, label, route ? disabledReason : `The ${label.toLowerCase()} route was not published by this watcher read model.`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function outputPreview(record: Record<string, unknown>): string {
|
|
318
|
+
const direct = firstString(record, ['outputPreview', 'lastOutput', 'lastChunk', 'lastMessage', 'resultPreview']);
|
|
319
|
+
if (direct) return safePreview(direct, 240);
|
|
320
|
+
for (const key of ['outputChunks', 'chunks', 'logChunks', 'messages', 'outputs']) {
|
|
321
|
+
const values = Array.isArray(record[key]) ? record[key] : [];
|
|
322
|
+
const joined = values.slice(-4).map((entry) => compactUnknown(entry)).filter(Boolean).join(' ');
|
|
323
|
+
if (joined) return safePreview(joined, 240);
|
|
324
|
+
}
|
|
325
|
+
const result = compactUnknown(record.result ?? record.output);
|
|
326
|
+
return result ? safePreview(result, 240) : '';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function runOutputDescriptor(record: Record<string, unknown>, route: string, inspectRoute: string): AutonomyQueueRecordOutput | undefined {
|
|
330
|
+
const preview = outputPreview(record);
|
|
331
|
+
if (!preview && !route) return undefined;
|
|
332
|
+
return {
|
|
333
|
+
status: preview ? 'preview' : 'route-only',
|
|
334
|
+
route: route || inspectRoute,
|
|
335
|
+
source: preview ? 'host-output-chunk' : 'not-published',
|
|
336
|
+
...(preview ? { preview } : {}),
|
|
337
|
+
policy: preview
|
|
338
|
+
? 'Bounded live watcher output/chunk preview from the connected host; secret-looking text is redacted. Use the route for the full host-owned stream when available.'
|
|
339
|
+
: 'The watcher record exposes an output route but no inline chunks. Use the route only for a user-requested host-owned output view.',
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function normalizeRun(entry: CollectedWatcherRecord, index: number): AutonomyQueueLiveRecord | null {
|
|
344
|
+
const providerId = firstString(entry.record, ['providerId', 'provider', 'sourceProvider', 'service', 'accountProvider']);
|
|
345
|
+
const watcherId = firstString(entry.record, ['watcherId', 'watchId', 'subscriptionId', 'sourceId', 'providerSourceId']);
|
|
346
|
+
const runId = firstString(entry.record, ['runId', 'watcherRunId', 'automationRunId', 'jobRunId', 'eventId', 'id']) || `${index + 1}`;
|
|
347
|
+
const status = normalizedStatus(entry.record, 'watcher-run');
|
|
348
|
+
const active = status === 'queued' || status === 'running';
|
|
349
|
+
const retryable = status === 'failed' || status === 'blocked' || status === 'cancelled';
|
|
350
|
+
const trigger = firstString(entry.record, ['triggerKind', 'trigger', 'eventKind', 'kind', 'type', 'operation']) || 'watcher-run';
|
|
351
|
+
const sourceId = firstString(entry.record, ['sourceId', 'providerSourceId', 'mailboxId', 'accountId']);
|
|
352
|
+
const correlationId = firstString(entry.record, ['correlationId', 'turnId', 'sessionId', 'requestId']);
|
|
353
|
+
const checkpoint = firstString(entry.record, ['checkpoint', 'lastCheckpoint', 'cursor', 'historyId']);
|
|
354
|
+
const error = firstString(entry.record, ['error', 'lastError', 'failureReason']) || compactUnknown(entry.record.error ?? entry.record.lastError);
|
|
355
|
+
const inspectRoute = routeFromKeys(entry.record, RUN_READ_KEYS)
|
|
356
|
+
|| `autonomy action:"item" queueItemId:"automation-runs" query:"${safeRecordIdPart(runId)}" includeParameters:true`;
|
|
357
|
+
const outputRoute = routeFromKeys(entry.record, RUN_OUTPUT_KEYS);
|
|
358
|
+
const cancelRoute = routeFromKeys(entry.record, RUN_CANCEL_KEYS);
|
|
359
|
+
const retryRoute = routeFromKeys(entry.record, RUN_RETRY_KEYS);
|
|
360
|
+
const checkpointRoute = routeFromKeys(entry.record, RUN_CHECKPOINT_KEYS);
|
|
361
|
+
const pauseRoute = routeFromKeys(entry.record, RUN_PAUSE_KEYS);
|
|
362
|
+
const resumeRoute = routeFromKeys(entry.record, RUN_RESUME_KEYS);
|
|
363
|
+
const output = runOutputDescriptor(entry.record, outputRoute, inspectRoute);
|
|
364
|
+
const controls = [
|
|
365
|
+
availableControl('inspect', 'Inspect watcher run', 'read-only', inspectRoute),
|
|
366
|
+
...(outputRoute ? [availableControl('output', 'Show watcher output', 'read-only', outputRoute)] : []),
|
|
367
|
+
effectControl('cancel', 'Cancel watcher run', cancelRoute, active, `Watcher run is ${status}; cancel is only offered for queued or running runs.`),
|
|
368
|
+
effectControl('retry', 'Retry watcher run', retryRoute, retryable, `Watcher run is ${status}; retry is only offered for failed, blocked, or cancelled runs.`),
|
|
369
|
+
...(checkpointRoute ? [availableControl('checkpoint', 'Checkpoint watcher run', 'confirmed-effect', checkpointRoute)] : []),
|
|
370
|
+
...(pauseRoute ? [effectControl('pause', 'Pause watcher run', pauseRoute, active, `Watcher run is ${status}; pause is only offered for queued or running runs.`)] : []),
|
|
371
|
+
...(resumeRoute ? [effectControl('resume', 'Resume watcher run', resumeRoute, status === 'paused' || status === 'blocked', `Watcher run is ${status}; resume is only offered for paused or blocked runs.`)] : []),
|
|
372
|
+
];
|
|
373
|
+
return {
|
|
374
|
+
id: `watcher-run:${safeRecordIdPart(entry.path)}:${safeRecordIdPart(runId)}`,
|
|
375
|
+
label: `Watcher run: ${safePreview(providerId || trigger, 48)} ${safePreview(runId, 64)}`,
|
|
376
|
+
status,
|
|
377
|
+
phase: trigger,
|
|
378
|
+
updatedAt: readIso(entry.record.updatedAt ?? entry.record.completedAt ?? entry.record.endedAt ?? entry.record.startedAt ?? entry.record.createdAt),
|
|
379
|
+
summary: [
|
|
380
|
+
`Live ${entry.kind} watcher run ${safePreview(runId, 80)} from ${entry.path} is ${status}.`,
|
|
381
|
+
providerId ? `Provider ${safePreview(providerId, 64)}.` : '',
|
|
382
|
+
watcherId ? `Watcher ${safePreview(watcherId, 80)}.` : '',
|
|
383
|
+
sourceId ? `Source ${safePreview(sourceId, 80)}.` : '',
|
|
384
|
+
checkpoint ? `Checkpoint ${safePreview(checkpoint, 120)}.` : '',
|
|
385
|
+
correlationId ? `Correlation ${safePreview(correlationId, 80)}.` : '',
|
|
386
|
+
output ? 'Live output route or chunk preview is published.' : 'No live output chunk route is published for this run.',
|
|
387
|
+
error ? `Failure ${safePreview(error, 160)}.` : '',
|
|
388
|
+
controls.some((control) => control.effect === 'confirmed-effect' && control.state === 'available')
|
|
389
|
+
? 'Published run effects are exact confirmed controls.'
|
|
390
|
+
: 'No confirmed run effect route is currently available.',
|
|
391
|
+
].filter(Boolean).join(' '),
|
|
392
|
+
inspectRoute,
|
|
393
|
+
...(active && cancelRoute ? { cancelRoute } : {}),
|
|
394
|
+
...(checkpointRoute ? { checkpointRoute } : {}),
|
|
395
|
+
...(pauseRoute && active ? { pauseRoute } : {}),
|
|
396
|
+
...(resumeRoute && (status === 'paused' || status === 'blocked') ? { resumeRoute } : {}),
|
|
397
|
+
nextSteps: [
|
|
398
|
+
inspectRoute,
|
|
399
|
+
...(outputRoute ? [outputRoute] : []),
|
|
400
|
+
...(active && cancelRoute ? [cancelRoute] : []),
|
|
401
|
+
...(retryable && retryRoute ? [retryRoute] : []),
|
|
402
|
+
'Use exact watcher run controls only after the user authorizes the specific run id.',
|
|
403
|
+
],
|
|
404
|
+
sourceIds: [
|
|
405
|
+
watcherId,
|
|
406
|
+
runId,
|
|
407
|
+
sourceId,
|
|
408
|
+
providerId,
|
|
409
|
+
correlationId,
|
|
410
|
+
...readStringArray(entry.record.deliveryIds),
|
|
411
|
+
...readStringArray(entry.record.eventIds),
|
|
412
|
+
].filter(Boolean).map((value) => safePreview(value, 96)),
|
|
413
|
+
...(error ? { logTail: [safePreview(error, 220)] } : {}),
|
|
414
|
+
...(output ? { output } : {}),
|
|
415
|
+
diagnostics: [
|
|
416
|
+
`source ${entry.path}`,
|
|
417
|
+
`read model ${entry.kind}`,
|
|
418
|
+
`record path ${entry.path}`,
|
|
419
|
+
checkpoint ? `checkpoint ${safePreview(checkpoint, 120)}` : '',
|
|
420
|
+
routeFromKeys(entry.record, RUN_CANCEL_KEYS) ? 'cancel route published' : 'cancel route not published',
|
|
421
|
+
routeFromKeys(entry.record, RUN_RETRY_KEYS) ? 'retry route published' : 'retry route not published',
|
|
422
|
+
].filter(Boolean),
|
|
423
|
+
controls,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function normalizeProviderSource(entry: CollectedWatcherRecord, index: number): AutonomyQueueLiveRecord | null {
|
|
428
|
+
const providerId = firstString(entry.record, ['providerId', 'provider', 'service', 'kind', 'sourceKind']) || 'provider';
|
|
429
|
+
const sourceId = firstString(entry.record, ['sourceId', 'providerSourceId', 'watcherId', 'subscriptionId', 'accountId', 'id']) || `${index + 1}`;
|
|
430
|
+
const status = normalizedStatus(entry.record, 'provider-source');
|
|
431
|
+
const scope = firstString(entry.record, ['scope', 'sourceScope', 'mailbox', 'folder', 'label']);
|
|
432
|
+
const filter = firstString(entry.record, ['filter', 'query', 'search', 'predicate']);
|
|
433
|
+
const checkpoint = firstString(entry.record, ['checkpoint', 'lastCheckpoint', 'cursor', 'historyId']);
|
|
434
|
+
const lastError = firstString(entry.record, ['lastError', 'error', 'failureReason']) || compactUnknown(entry.record.lastError ?? entry.record.error);
|
|
435
|
+
const inspectRoute = routeFromKeys(entry.record, SOURCE_READ_KEYS)
|
|
436
|
+
|| `autonomy action:"item" queueItemId:"automation-runs" query:"${safeRecordIdPart(sourceId)}" includeParameters:true`;
|
|
437
|
+
const refreshRoute = routeFromKeys(entry.record, SOURCE_REFRESH_KEYS);
|
|
438
|
+
const preview = firstString(entry.record, ['lastEventPreview', 'preview', 'snippet', 'lastMessage']);
|
|
439
|
+
const output: AutonomyQueueRecordOutput | undefined = preview
|
|
440
|
+
? {
|
|
441
|
+
status: 'preview',
|
|
442
|
+
route: inspectRoute,
|
|
443
|
+
source: 'provider-source-preview',
|
|
444
|
+
preview: safePreview(preview, 220),
|
|
445
|
+
policy: 'Bounded provider-source event preview from a daemon or SDK read model; secret-looking text is redacted and provider mutations require separate confirmed routes.',
|
|
446
|
+
}
|
|
447
|
+
: undefined;
|
|
448
|
+
return {
|
|
449
|
+
id: `watcher-source:${safeRecordIdPart(entry.path)}:${safeRecordIdPart(sourceId)}`,
|
|
450
|
+
label: `Provider source: ${safePreview(providerId, 48)} ${safePreview(sourceId, 64)}`,
|
|
451
|
+
status,
|
|
452
|
+
phase: firstString(entry.record, ['sourceKind', 'kind', 'type']) || providerId,
|
|
453
|
+
updatedAt: readIso(entry.record.updatedAt ?? entry.record.syncedAt ?? entry.record.currentAt ?? entry.record.createdAt),
|
|
454
|
+
summary: [
|
|
455
|
+
`Live ${entry.kind} provider source ${safePreview(sourceId, 80)} from ${entry.path} is ${status}.`,
|
|
456
|
+
`Provider ${safePreview(providerId, 64)}.`,
|
|
457
|
+
scope ? `Scope ${safePreview(scope, 96)}.` : '',
|
|
458
|
+
filter ? `Filter ${safePreview(filter, 120)}.` : '',
|
|
459
|
+
checkpoint ? `Checkpoint ${safePreview(checkpoint, 120)}.` : '',
|
|
460
|
+
refreshRoute ? 'A read-only refresh route is published.' : 'No read-only refresh route is published.',
|
|
461
|
+
lastError ? `Last error ${safePreview(lastError, 160)}.` : '',
|
|
462
|
+
].filter(Boolean).join(' '),
|
|
463
|
+
inspectRoute,
|
|
464
|
+
nextSteps: [
|
|
465
|
+
inspectRoute,
|
|
466
|
+
...(refreshRoute ? [refreshRoute] : []),
|
|
467
|
+
'Treat source records as read-only context; provider source mutations require separate confirmed provider routes.',
|
|
468
|
+
],
|
|
469
|
+
sourceIds: [
|
|
470
|
+
sourceId,
|
|
471
|
+
providerId,
|
|
472
|
+
firstString(entry.record, ['watcherId', 'subscriptionId']),
|
|
473
|
+
firstString(entry.record, ['accountId', 'mailboxId']),
|
|
474
|
+
checkpoint,
|
|
475
|
+
].filter(Boolean).map((value) => safePreview(value, 96)),
|
|
476
|
+
...(lastError ? { logTail: [safePreview(lastError, 220)] } : {}),
|
|
477
|
+
...(output ? { output } : {}),
|
|
478
|
+
diagnostics: [
|
|
479
|
+
`source ${entry.path}`,
|
|
480
|
+
`read model ${entry.kind}`,
|
|
481
|
+
`record path ${entry.path}`,
|
|
482
|
+
scope ? `scope ${safePreview(scope, 96)}` : '',
|
|
483
|
+
filter ? `filter ${safePreview(filter, 120)}` : '',
|
|
484
|
+
refreshRoute ? 'refresh route published' : 'refresh route not published',
|
|
485
|
+
].filter(Boolean),
|
|
486
|
+
controls: [
|
|
487
|
+
availableControl('inspect', 'Inspect provider source', 'read-only', inspectRoute),
|
|
488
|
+
...(refreshRoute ? [availableControl('refresh', 'Refresh provider source', 'read-only', refreshRoute)] : []),
|
|
489
|
+
],
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function watcherReadModelLiveRecords(context: CommandContext): readonly AutonomyQueueLiveRecord[] {
|
|
494
|
+
const records = watcherSources(context)
|
|
495
|
+
.flatMap((source) => collectFromSource(
|
|
496
|
+
source.source,
|
|
497
|
+
source.path,
|
|
498
|
+
source.kind,
|
|
499
|
+
source.lane,
|
|
500
|
+
))
|
|
501
|
+
.map((entry, index) => entry.lane === 'watcher-run' ? normalizeRun(entry, index) : normalizeProviderSource(entry, index))
|
|
502
|
+
.filter((record): record is AutonomyQueueLiveRecord => record !== null);
|
|
503
|
+
const seen = new Set<string>();
|
|
504
|
+
return records.filter((record) => {
|
|
505
|
+
if (seen.has(record.id)) return false;
|
|
506
|
+
seen.add(record.id);
|
|
507
|
+
return true;
|
|
508
|
+
}).slice(0, 20);
|
|
509
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { BackgroundProcess, ProcessManager } from '@pellux/goodvibes-sdk/pl
|
|
|
3
3
|
import type { CommandContext } from '../input/command-registry.ts';
|
|
4
4
|
import { sudoExecutionPosture } from './agent-harness-sudo-posture.ts';
|
|
5
5
|
import { previewHarnessText } from './agent-harness-text.ts';
|
|
6
|
+
import { interactiveRuntimeCapabilitySummary, interactiveRuntimeParityStatus } from './agent-harness-interactive-runtime-records.ts';
|
|
6
7
|
import type {
|
|
7
8
|
AgentHarnessBackgroundProcessArgs,
|
|
8
9
|
BackgroundProcessLookupSource,
|
|
@@ -356,17 +357,19 @@ function candidateProcess(entry: BackgroundProcess): Record<string, unknown> {
|
|
|
356
357
|
|
|
357
358
|
function processToolParity(context?: CommandContext): readonly Record<string, unknown>[] {
|
|
358
359
|
const substrate = processSubstrateReport(context);
|
|
360
|
+
const interactive = context ? interactiveRuntimeParityStatus(context) : null;
|
|
359
361
|
const localProcessManager = substrate.localProcessManager as Record<string, unknown>;
|
|
360
362
|
const daemonContract = substrate.daemonOperatorContract as Record<string, unknown>;
|
|
361
363
|
const stdinWrite = localProcessManager.stdinWrite as Record<string, unknown>;
|
|
362
364
|
const pty = localProcessManager.pty as Record<string, unknown>;
|
|
363
365
|
const terminalRoutes = Array.isArray(daemonContract.terminalOrPtyRoutes) ? daemonContract.terminalOrPtyRoutes : [];
|
|
364
|
-
const writeStatus: ProcessCapabilityStatus = stdinWrite.status === 'contract-discovered'
|
|
366
|
+
const writeStatus: ProcessCapabilityStatus = interactive?.stdinWriteContract ? 'contract-discovered' : stdinWrite.status === 'contract-discovered'
|
|
365
367
|
? 'contract-discovered'
|
|
366
368
|
: terminalRoutes.some((route) => String((route as Record<string, unknown>).methodId).toLowerCase().includes('write'))
|
|
367
369
|
? 'contract-discovered'
|
|
368
370
|
: 'blocked-contract-gap';
|
|
369
|
-
const ptyStatus: ProcessCapabilityStatus = pty.status === 'contract-discovered' || terminalRoutes.length > 0 ? 'contract-discovered' : 'blocked-contract-gap';
|
|
371
|
+
const ptyStatus: ProcessCapabilityStatus = interactive?.ptyContract || pty.status === 'contract-discovered' || terminalRoutes.length > 0 ? 'contract-discovered' : 'blocked-contract-gap';
|
|
372
|
+
const sudoStatus: ProcessCapabilityStatus = interactive?.sudoMediationContract ? 'contract-discovered' : 'visible-only';
|
|
370
373
|
return [
|
|
371
374
|
{
|
|
372
375
|
capability: 'terminal(background=true)',
|
|
@@ -422,8 +425,8 @@ function processToolParity(context?: CommandContext): readonly Record<string, un
|
|
|
422
425
|
},
|
|
423
426
|
{
|
|
424
427
|
capability: 'sudo',
|
|
425
|
-
status:
|
|
426
|
-
userOutcome: 'Privilege prompts must stay foreground or use a future safe credential-prompt contract.',
|
|
428
|
+
status: sudoStatus,
|
|
429
|
+
userOutcome: sudoStatus === 'contract-discovered' ? 'A certified daemon mediation route is published for visible credential prompts.' : 'Privilege prompts must stay foreground or use a future safe credential-prompt contract.',
|
|
427
430
|
modelRoute: 'execution action:"route" id:"local-shell-command"',
|
|
428
431
|
},
|
|
429
432
|
];
|
|
@@ -455,6 +458,7 @@ function capabilities(context?: CommandContext): Record<string, unknown> {
|
|
|
455
458
|
...substrate,
|
|
456
459
|
auditedTerms: ['terminal', 'process.write', 'stdin', 'pty', 'sudo', 'sessions.inputs'],
|
|
457
460
|
},
|
|
461
|
+
interactiveRuntime: context ? interactiveRuntimeCapabilitySummary(context) : null,
|
|
458
462
|
pty: {
|
|
459
463
|
status: pty.status === 'contract-discovered' ? 'contract-discovered-but-not-generic-executable' : 'not-yet-supported-in-agent-harness',
|
|
460
464
|
guidance: pty.status === 'contract-discovered'
|