job-forge 2.14.36 → 2.14.38

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.
@@ -1,29 +1,58 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawnSync } from 'child_process';
4
3
  import { existsSync, readdirSync, statSync } from 'fs';
5
4
  import { join, resolve } from 'path';
6
- import { defaultOpenCodeDbPath, findSessionById, parseSinceCutoff } from '@razroo/iso-trace';
5
+ import {
6
+ buildSessionGraph,
7
+ clean,
8
+ collectDispatchCalls,
9
+ descendantIds,
10
+ discoverProjectSessions,
11
+ duplicateDispatchUrlCount,
12
+ findObservedSession,
13
+ hasOutcome,
14
+ hasProxyLeak,
15
+ inspectionForSession,
16
+ isFreeModelRoute,
17
+ mentionsLimitedCandidatePool,
18
+ mergeModelUsage,
19
+ messageEvents,
20
+ modelLabel,
21
+ modelUsageFromSession,
22
+ outcomeFromText,
23
+ pad,
24
+ providerErrorsForSession,
25
+ providerErrorCategory,
26
+ redactSecrets,
27
+ relativeToProject,
28
+ requestedJobCount,
29
+ shorten,
30
+ statusCodeFromText,
31
+ stringValue,
32
+ summarizeChildSession,
33
+ userRequestSummaries,
34
+ loadObservedSession,
35
+ } from '../lib/jobforge-observability.mjs';
7
36
  import { jobForgeLedgerSummary } from '../lib/jobforge-ledger.mjs';
8
37
 
9
38
  const PROJECT_DIR = process.env.JOB_FORGE_PROJECT || process.cwd();
10
39
  const DEFAULT_SINCE = '24h';
11
40
 
12
- const USAGE = `job-forge telemetry — JobForge pipeline view over local OpenCode traces
41
+ const USAGE = `job-forge telemetry — JobForge pipeline view over local traces
13
42
 
14
43
  Usage:
15
- job-forge telemetry:list [--since 24h] [--cwd <dir>] [--json]
16
- job-forge telemetry:status [--since 24h] [--cwd <dir>] [--json]
17
- job-forge telemetry:show <id-or-prefix> [--cwd <dir>] [--json]
18
- job-forge telemetry:watch [--since 24h] [--cwd <dir>] [--interval 5]
44
+ job-forge telemetry:list [--since 24h] [--cwd <dir>] [--harness <name>] [--json]
45
+ job-forge telemetry:status [--since 24h] [--cwd <dir>] [--harness <name>] [--json]
46
+ job-forge telemetry:show <id-or-prefix> [--cwd <dir>] [--harness <name>] [--json]
47
+ job-forge telemetry:watch [--since 24h] [--cwd <dir>] [--harness <name>] [--interval 5]
19
48
 
20
- Telemetry is local-only and passive. It derives status from OpenCode's SQLite DB
21
- plus JobForge tracker files; agents do not need to emit custom events.`;
49
+ Telemetry is local-only and passive. It derives status from normalized local
50
+ traces plus JobForge tracker files; agents do not need to emit custom events.`;
22
51
 
23
52
  const [cmd = 'help', ...args] = process.argv.slice(2);
24
53
 
25
54
  function parseArgs(rawArgs, { allowSession = false, allowInterval = false } = {}) {
26
- const opts = { since: DEFAULT_SINCE, cwd: PROJECT_DIR, json: false, interval: 5 };
55
+ const opts = { since: DEFAULT_SINCE, cwd: PROJECT_DIR, harness: '', json: false, interval: 5 };
27
56
  const positional = [];
28
57
 
29
58
  for (let i = 0; i < rawArgs.length; i++) {
@@ -36,6 +65,10 @@ function parseArgs(rawArgs, { allowSession = false, allowInterval = false } = {}
36
65
  opts.cwd = rawArgs[++i];
37
66
  } else if (arg.startsWith('--cwd=')) {
38
67
  opts.cwd = arg.slice('--cwd='.length);
68
+ } else if (arg === '--harness') {
69
+ opts.harness = rawArgs[++i];
70
+ } else if (arg.startsWith('--harness=')) {
71
+ opts.harness = arg.slice('--harness='.length);
39
72
  } else if (arg === '--json') {
40
73
  opts.json = true;
41
74
  } else if (allowInterval && arg === '--interval') {
@@ -58,146 +91,122 @@ function parseArgs(rawArgs, { allowSession = false, allowInterval = false } = {}
58
91
  return { opts, positional };
59
92
  }
60
93
 
61
- function queryOpenCodeDb(dbPath, sql) {
62
- const result = spawnSync('sqlite3', ['-json', dbPath, sql], {
63
- encoding: 'utf8',
64
- maxBuffer: 24 * 1024 * 1024,
65
- });
66
- if ((result.status ?? 0) !== 0) {
67
- const detail = result.stderr?.trim() || result.stdout?.trim() || `exit ${result.status ?? 1}`;
68
- throw new Error(`job-forge telemetry: sqlite3 query failed: ${detail}`);
69
- }
70
- return JSON.parse(result.stdout || '[]');
71
- }
72
-
73
- function sqlString(value) {
74
- return `'${String(value).replaceAll("'", "''")}'`;
94
+ function rootRefs(refs, graph) {
95
+ return graph.roots.length > 0 ? graph.roots : refs;
75
96
  }
76
97
 
77
- function msToIso(ms) {
78
- return new Date(Number(ms)).toISOString();
79
- }
80
-
81
- function discoverSessions(opts, { includeAllForShow = false } = {}) {
82
- const dbPath = defaultOpenCodeDbPath();
83
- if (!existsSync(dbPath)) return [];
84
-
85
- const where = [
86
- 's.time_archived is null',
87
- `s.directory = ${sqlString(opts.cwd)}`,
88
- ];
89
- const sinceMs = includeAllForShow ? undefined : parseSinceCutoff(opts.since);
90
- if (sinceMs !== undefined) where.push(`s.time_created >= ${Number(sinceMs)}`);
91
-
92
- const rows = queryOpenCodeDb(dbPath, [
93
- 'select',
94
- ' s.id,',
95
- ' s.parent_id,',
96
- ' s.title,',
97
- ' s.directory,',
98
- ' s.time_created,',
99
- ' s.time_updated,',
100
- ' (select count(*) from message m where m.session_id = s.id) as turn_count,',
101
- ' (',
102
- ' (select coalesce(sum(length(data)), 0) from message m where m.session_id = s.id) +',
103
- ' (select coalesce(sum(length(data)), 0) from part p where p.session_id = s.id)',
104
- ' ) as size_bytes',
105
- 'from session s',
106
- `where ${where.join(' and ')}`,
107
- 'order by s.time_updated desc',
108
- ].join(' '));
109
-
110
- return rows.map((row) => ({
111
- id: row.id,
112
- parentId: row.parent_id || null,
113
- title: row.title || '',
114
- cwd: row.directory,
115
- startedAt: msToIso(row.time_created),
116
- endedAt: msToIso(row.time_updated),
117
- turnCount: row.turn_count ?? 0,
118
- sizeBytes: row.size_bytes ?? 0,
119
- }));
120
- }
121
-
122
- function loadRows(sessionId) {
123
- const dbPath = defaultOpenCodeDbPath();
124
- const id = sqlString(sessionId);
125
- return {
126
- messages: queryOpenCodeDb(dbPath, `select id, time_created, data from message where session_id = ${id} order by time_created, id`),
127
- parts: queryOpenCodeDb(dbPath, `select id, message_id, time_created, data from part where session_id = ${id} order by time_created, id`),
128
- };
129
- }
130
-
131
- function parseJson(raw) {
132
- try {
133
- return JSON.parse(raw || '{}');
134
- } catch (error) {
135
- const message = error instanceof Error ? error.message : String(error);
136
- return { __parseError: message, __raw: raw || '' };
98
+ function loadContext(refs) {
99
+ const sessionsById = new Map();
100
+ const loadErrors = new Map();
101
+ for (const ref of refs) {
102
+ try {
103
+ sessionsById.set(ref.id, loadObservedSession(ref));
104
+ } catch (error) {
105
+ loadErrors.set(ref.id, error instanceof Error ? error.message : String(error));
106
+ }
137
107
  }
108
+ const loadedRefs = refs.filter((ref) => sessionsById.has(ref.id));
109
+ const graph = buildSessionGraph(loadedRefs, sessionsById);
110
+ const refsById = new Map(refs.map((ref) => [ref.id, ref]));
111
+ return { refsById, sessionsById, graph, loadedRefs, loadErrors };
138
112
  }
139
113
 
140
- function analyzeSession(session, allSessions, opts) {
141
- const rows = loadRows(session.id);
142
- const messages = rows.messages.map((row) => ({ row, data: parseJson(row.data) }));
143
- const parts = rows.parts.map((row) => ({ row, data: parseJson(row.data) }));
144
- const messageById = new Map(messages.map((m) => [m.row.id, m.data]));
145
- const textParts = parts.filter((p) => p.data.type === 'text');
146
- const taskCalls = parts.filter((p) => p.data.type === 'tool' && p.data.tool === 'task').map(taskCallSummary);
147
- const userRequests = userRequestSummaries(textParts, messageById);
114
+ function analyzeSession(ref, context, opts) {
115
+ const session = context.sessionsById.get(ref.id);
116
+ const inspection = inspectionForSession(session);
117
+ const userRequests = userRequestSummaries(session);
148
118
  const activeRequest = userRequests.at(-1) || null;
149
- const userPrompt = activeRequest?.prompt || userRequests[0]?.prompt || '';
150
- const latestTaskCalls = activeRequest
151
- ? taskCalls.filter((task) => task.atMs >= activeRequest.atMs)
152
- : taskCalls;
153
- const providerErrors = messages.map(providerErrorSummary).filter(Boolean);
154
- const rootModels = modelUsageFromMessages(messages);
119
+ const prompt = activeRequest?.prompt || userRequests[0]?.prompt || inspection.preview.firstUser || '';
120
+ const dispatchCalls = collectDispatchCalls(session);
121
+ const latestDispatchCalls = activeRequest
122
+ ? dispatchCalls.filter((call) => call.atMs >= activeRequest.atMs)
123
+ : dispatchCalls;
124
+ const providerErrors = providerErrorsForSession(session);
125
+ const rootModels = modelUsageFromSession(session);
155
126
  const tracker = trackerStatus(opts.cwd);
156
- const children = allSessions
157
- .filter((candidate) => candidate.parentId === session.id)
158
- .sort((a, b) => a.startedAt.localeCompare(b.startedAt))
159
- .map((child) => childSummary(child));
127
+
128
+ const childRefs = (context.graph.childrenBySession.get(ref.id) || [])
129
+ .map((id) => context.refsById.get(id))
130
+ .filter(Boolean)
131
+ .sort((a, b) => a.startedAt.localeCompare(b.startedAt));
132
+ const children = childRefs
133
+ .map((childRef) => summarizeChildSession(context.sessionsById.get(childRef.id)))
134
+ .filter(Boolean);
160
135
  const latestChildren = activeRequest
161
136
  ? children.filter((child) => child.startedAtMs >= activeRequest.atMs)
162
137
  : children;
163
138
  const models = mergeModelUsage([rootModels, ...children.map((child) => child.models)]);
164
- const policyIssues = detectPolicyIssues(session, parts, textParts, messageById, providerErrors, {
165
- taskCalls,
166
- latestTaskCalls,
139
+ const unresolvedChildren = dispatchCalls.filter((call) => call.sessionId && !children.find((child) => child.id === call.sessionId));
140
+ const policyIssues = detectPolicyIssues(ref, session, providerErrors, {
141
+ dispatchCalls,
142
+ latestDispatchCalls,
167
143
  children,
168
144
  latestChildren,
145
+ unresolvedChildren,
169
146
  activeRequest,
170
147
  models,
148
+ inspection,
149
+ isChildSession: context.graph.childIds.has(ref.id),
171
150
  });
151
+
172
152
  const childOutcomes = children.filter((child) => child.outcome !== 'unknown').length;
173
153
  const childProviderErrors = children.reduce((sum, child) => sum + child.providerErrors, 0);
174
- const status = sessionStatus({ session, taskCalls, children, childOutcomes, childProviderErrors, policyIssues, providerErrors });
175
- const recommendations = nextActions({ tracker, policyIssues, providerErrors, taskCalls, children });
154
+ const status = sessionStatus({
155
+ dispatchCalls,
156
+ children,
157
+ unresolvedChildren,
158
+ childOutcomes,
159
+ childProviderErrors,
160
+ policyIssues,
161
+ providerErrors,
162
+ });
163
+ const recommendations = nextActions({ tracker, policyIssues, providerErrors, dispatchCalls, children });
176
164
 
177
165
  return {
178
- session,
166
+ session: {
167
+ id: ref.id,
168
+ harness: ref.source.harness,
169
+ title: ref.title || session.title || '',
170
+ startedAt: ref.startedAt,
171
+ endedAt: ref.endedAt,
172
+ },
179
173
  projectDir: opts.cwd,
180
174
  status,
181
- prompt: userPrompt,
175
+ prompt,
182
176
  userRequests,
183
177
  latestRequest: activeRequest ? {
184
178
  ...activeRequest,
185
- taskDispatches: latestTaskCalls.filter((task) => !task.isStatusPoll).length,
179
+ taskDispatches: latestDispatchCalls.filter((call) => !call.isStatusPoll).length,
186
180
  children: latestChildren.length,
187
181
  childOutcomes: latestChildren.filter((child) => child.outcome !== 'unknown').length,
188
182
  } : null,
189
183
  tasks: {
190
- total: taskCalls.length,
191
- statusPolls: taskCalls.filter((task) => task.isStatusPoll).length,
192
- running: taskCalls.filter((task) => task.status && task.status !== 'completed').length,
193
- calls: taskCalls,
184
+ total: dispatchCalls.length,
185
+ statusPolls: dispatchCalls.filter((call) => call.isStatusPoll).length,
186
+ running: children.filter((child) => child.outcome === 'unknown').length + unresolvedChildren.length,
187
+ calls: dispatchCalls,
194
188
  },
195
189
  children: {
196
- total: children.length,
190
+ total: children.length + unresolvedChildren.length,
197
191
  withOutcomes: childOutcomes,
198
192
  providerErrors: childProviderErrors,
199
193
  toolErrors: children.reduce((sum, child) => sum + child.toolErrors, 0),
200
- sessions: children,
194
+ sessions: [
195
+ ...children,
196
+ ...unresolvedChildren.map((call) => ({
197
+ id: call.sessionId,
198
+ title: call.description || '',
199
+ startedAt: call.at,
200
+ outcome: 'unknown',
201
+ providerErrors: 0,
202
+ taskCalls: 0,
203
+ dispatchCalls: 0,
204
+ toolErrors: 0,
205
+ dedupeMiss: false,
206
+ trackerWrites: 0,
207
+ models: [],
208
+ })),
209
+ ],
201
210
  },
202
211
  models,
203
212
  providerErrors,
@@ -207,80 +216,32 @@ function analyzeSession(session, allSessions, opts) {
207
216
  };
208
217
  }
209
218
 
210
- function userRequestSummaries(textParts, messageById) {
211
- return textParts
212
- .filter((part) => messageById.get(part.row.message_id)?.role === 'user')
213
- .map((part) => {
214
- const prompt = clean(redactSecrets(part.data.text || ''));
215
- return {
216
- at: msToIso(part.row.time_created),
217
- atMs: Number(part.row.time_created),
218
- prompt,
219
- requestedJobs: requestedJobCount(prompt),
220
- };
221
- })
222
- .filter((request) => request.prompt.length > 0);
223
- }
224
-
225
- function taskCallSummary(part) {
226
- const input = objectOrEmpty(part.data.state?.input);
227
- const metadata = objectOrEmpty(part.data.state?.metadata);
228
- const prompt = typeof input.prompt === 'string' ? input.prompt : '';
229
- const description = stringValue(input.description || metadata.description || part.data.state?.title);
230
- const sessionId = stringValue(input.task_id || metadata.sessionId);
231
- const subagentType = stringValue(input.subagent_type || metadata.subagent_type || metadata.agent);
232
- const isStatusPoll = Boolean(input.task_id) ||
233
- /\b(check|poll|status|force|abort|progress|result)\b/i.test(description) ||
234
- /\b(return your final outcome now|if still working|current status|report your current status|still running)\b/i.test(prompt);
235
-
236
- return {
237
- at: msToIso(part.row.time_created),
238
- atMs: Number(part.row.time_created),
239
- description,
240
- subagentType,
241
- sessionId,
242
- status: stringValue(part.data.state?.status),
243
- isStatusPoll,
244
- promptBytes: Buffer.byteLength(prompt, 'utf8'),
245
- proxyLeak: hasProxyLeak(prompt),
246
- url: firstUrl(prompt),
247
- };
248
- }
249
-
250
- function providerErrorSummary(message) {
251
- const error = message.data.error;
252
- if (!error) return null;
253
- const rawMessage = stringValue(error.data?.message || error.message || error.name || 'unknown provider error');
254
- const statusCode = error.data?.statusCode ?? statusCodeFromText(rawMessage);
255
- return {
256
- at: msToIso(message.row.time_created),
257
- provider: stringValue(message.data.providerID),
258
- model: stringValue(message.data.modelID),
259
- statusCode,
260
- category: providerErrorCategory(rawMessage, statusCode),
261
- message: redactSecrets(rawMessage),
262
- };
263
- }
264
-
265
- function detectPolicyIssues(session, parts, textParts, messageById, providerErrors, context = {}) {
219
+ function detectPolicyIssues(ref, session, providerErrors, context = {}) {
266
220
  const issues = [];
267
- const taskParts = parts.filter((p) => p.data.type === 'tool' && p.data.tool === 'task');
268
- const taskCalls = context.taskCalls || taskParts.map(taskCallSummary);
269
- const latestTaskCalls = context.latestTaskCalls || taskCalls;
221
+ const dispatchCalls = context.dispatchCalls || [];
222
+ const latestDispatchCalls = context.latestDispatchCalls || dispatchCalls;
270
223
  const children = context.children || [];
271
224
  const latestChildren = context.latestChildren || children;
225
+ const unresolvedChildren = context.unresolvedChildren || [];
272
226
  const activeRequest = context.activeRequest || null;
273
- const statusPolls = taskCalls.filter((task) => task.isStatusPoll);
227
+ const assistantTexts = messageEvents(session, 'assistant');
228
+ const latestAssistantText = assistantTexts
229
+ .filter((event) => !activeRequest || event.atMs >= activeRequest.atMs)
230
+ .map((event) => event.text || '')
231
+ .join('\n');
232
+ const finalText = assistantTexts.slice(-5).map((event) => event.text || '').join('\n');
233
+
234
+ const statusPolls = dispatchCalls.filter((call) => call.isStatusPoll);
274
235
  if (statusPolls.length > 0) {
275
236
  issues.push({
276
237
  type: 'task_status_poll',
277
238
  severity: 'high',
278
239
  count: statusPolls.length,
279
- detail: 'A task call tried to poll/check an existing task session.',
240
+ detail: 'A dispatch call tried to poll/check an existing subagent session.',
280
241
  });
281
242
  }
282
243
 
283
- const proxyLeakCount = parts.reduce((count, part) => count + (partHasProxyLeak(part) ? 1 : 0), 0);
244
+ const proxyLeakCount = dispatchCalls.filter((call) => call.proxyLeak).length;
284
245
  if (proxyLeakCount > 0) {
285
246
  issues.push({
286
247
  type: 'proxy_prompt_leak',
@@ -290,13 +251,12 @@ function detectPolicyIssues(session, parts, textParts, messageById, providerErro
290
251
  });
291
252
  }
292
253
 
293
- const childTaskCalls = session.parentId ? taskParts.length : 0;
294
- if (childTaskCalls > 0) {
254
+ if (context.isChildSession && dispatchCalls.length > 0) {
295
255
  issues.push({
296
256
  type: 'subagent_spawned_task',
297
257
  severity: 'high',
298
- count: childTaskCalls,
299
- detail: 'A child/subagent session used the task tool.',
258
+ count: dispatchCalls.length,
259
+ detail: 'A child/subagent session spawned more child work.',
300
260
  });
301
261
  }
302
262
 
@@ -330,7 +290,7 @@ function detectPolicyIssues(session, parts, textParts, messageById, providerErro
330
290
  });
331
291
  }
332
292
 
333
- const duplicateUrlCount = duplicateTaskUrlCount(taskCalls);
293
+ const duplicateUrlCount = duplicateDispatchUrlCount(dispatchCalls);
334
294
  if (duplicateUrlCount > 0) {
335
295
  issues.push({
336
296
  type: 'duplicate_task_url',
@@ -340,35 +300,22 @@ function detectPolicyIssues(session, parts, textParts, messageById, providerErro
340
300
  });
341
301
  }
342
302
 
343
- const runningTasks = taskCalls.filter((task) => task.status && task.status !== 'completed');
344
- if (runningTasks.length > 0) {
345
- const consumed = runningTasks.filter((task) => {
346
- if (!task.sessionId) return false;
347
- const child = children.find((candidate) => candidate.id === task.sessionId);
348
- return child && child.outcome !== 'unknown';
349
- }).length;
303
+ if (unresolvedChildren.length > 0) {
350
304
  issues.push({
351
- type: consumed === runningTasks.length ? 'task_result_not_consumed' : 'task_still_running',
352
- severity: consumed === runningTasks.length ? 'medium' : 'high',
353
- count: runningTasks.length,
354
- detail: consumed === runningTasks.length
355
- ? 'One or more task calls still show running even though child sessions have terminal-looking outcomes; root did not consume the final task result.'
356
- : 'One or more task calls still show running and do not have terminal child outcomes.',
305
+ type: 'task_still_running',
306
+ severity: 'high',
307
+ count: unresolvedChildren.length,
308
+ detail: 'One or more dispatches reference child sessions that do not yet have a visible terminal outcome.',
357
309
  });
358
310
  }
359
311
 
360
- const latestAssistantText = textParts
361
- .filter((part) => messageById.get(part.row.message_id)?.role === 'assistant')
362
- .filter((part) => !activeRequest || Number(part.row.time_created) >= activeRequest.atMs)
363
- .map((part) => part.data.text || '')
364
- .join('\n');
365
- const latestDispatches = latestTaskCalls.filter((task) => !task.isStatusPoll).length;
312
+ const latestDispatches = latestDispatchCalls.filter((call) => !call.isStatusPoll).length;
366
313
  if (activeRequest?.requestedJobs && latestDispatches > 0 && latestDispatches < activeRequest.requestedJobs && !mentionsLimitedCandidatePool(latestAssistantText)) {
367
314
  issues.push({
368
315
  type: 'requested_count_not_met',
369
316
  severity: 'high',
370
317
  count: activeRequest.requestedJobs - latestDispatches,
371
- detail: `Latest request asked for ${activeRequest.requestedJobs} jobs, but only ${latestDispatches} task dispatches are visible after that prompt.`,
318
+ detail: `Latest request asked for ${activeRequest.requestedJobs} jobs, but only ${latestDispatches} dispatches are visible after that prompt.`,
372
319
  });
373
320
  }
374
321
 
@@ -381,105 +328,32 @@ function detectPolicyIssues(session, parts, textParts, messageById, providerErro
381
328
  });
382
329
  }
383
330
 
384
- const finalText = textParts
385
- .filter((part) => messageById.get(part.row.message_id)?.role === 'assistant')
386
- .slice(-5)
387
- .map((part) => part.data.text || '')
388
- .join('\n');
389
331
  if (latestDispatches > 0 && !hasOutcome(latestAssistantText) && !/round .*in flight|still running|waiting/i.test(latestAssistantText)) {
390
332
  issues.push({
391
333
  type: 'latest_request_no_visible_final_outcome',
392
334
  severity: 'high',
393
335
  count: 1,
394
- detail: 'Latest request dispatched task work but assistant text after that request has no final outcome or in-flight notice.',
336
+ detail: 'Latest request dispatched child work but assistant text after that request has no final outcome or in-flight notice.',
395
337
  });
396
- } else if (taskParts.length > 0 && !hasOutcome(finalText) && !/round .*in flight|still running|waiting/i.test(finalText)) {
338
+ } else if (dispatchCalls.length > 0 && !hasOutcome(finalText) && !/round .*in flight|still running|waiting/i.test(finalText)) {
397
339
  issues.push({
398
340
  type: 'no_visible_final_outcome',
399
341
  severity: 'medium',
400
342
  count: 1,
401
- detail: 'Session dispatched task work but recent assistant text has no final outcome or in-flight notice.',
343
+ detail: 'Session dispatched child work but recent assistant text has no final outcome or in-flight notice.',
402
344
  });
403
345
  }
404
346
 
405
347
  return issues;
406
348
  }
407
349
 
408
- function partHasProxyLeak(part) {
409
- const data = part.data;
410
- if (data.type === 'text' || data.type === 'reasoning') return hasProxyLeak(data.text || '');
411
- if (data.type === 'tool') return hasProxyLeak(JSON.stringify(data.state?.input || {}));
412
- return false;
413
- }
414
-
415
- function childSummary(session) {
416
- const rows = loadRows(session.id);
417
- const messages = rows.messages.map((row) => ({ row, data: parseJson(row.data) }));
418
- const parts = rows.parts.map((row) => ({ row, data: parseJson(row.data) }));
419
- const messageById = new Map(messages.map((m) => [m.row.id, m.data]));
420
- const assistantTexts = parts
421
- .filter((p) => p.data.type === 'text' && messageById.get(p.row.message_id)?.role === 'assistant')
422
- .map((p) => p.data.text || '');
423
- const finalText = assistantTexts.slice(-5).join('\n');
424
- const providerErrors = messages.map(providerErrorSummary).filter(Boolean);
425
- const taskCalls = parts.filter((p) => p.data.type === 'tool' && p.data.tool === 'task').length;
426
- const trackerWrites = parts.filter((p) => p.data.type === 'tool' && /batch\/tracker-additions\/.*\.tsv/.test(JSON.stringify(p.data.state?.input || {}))).length;
427
- const toolErrors = parts.filter((p) => p.data.type === 'tool' && (p.data.state?.status === 'error' || p.data.state?.error)).length;
428
- const dedupeMiss = /\b(DUPLICATE|already\s+\*{0,2}Applied|already applied|per \[H2\]|Hard Limit #2|No re-dispatch needed)\b/i.test(finalText) ||
429
- /\bpreviously applied (on|as|under)\b/i.test(finalText);
430
-
431
- return {
432
- id: session.id,
433
- title: session.title,
434
- startedAt: session.startedAt,
435
- startedAtMs: Date.parse(session.startedAt),
436
- endedAt: session.endedAt,
437
- outcome: outcomeFromText(finalText, trackerWrites),
438
- providerErrors: providerErrors.length,
439
- taskCalls,
440
- toolErrors,
441
- dedupeMiss,
442
- trackerWrites,
443
- models: modelUsageFromMessages(messages),
444
- };
445
- }
446
-
447
- function outcomeFromText(text, trackerWrites = 0) {
448
- const explicitFailed = /\b(APPLICATION OUTCOME|RESULT|STATUS)(?:\*\*)?\s*[:|-]\s*\*{0,2}\s*(FAILED|APPLY FAILED)\b/i.test(text) ||
449
- /\|\s*\*\*?Status\*\*?\s*\|\s*\*\*?Failed\*\*?/i.test(text);
450
- const explicitSkipped = /\b(APPLICATION OUTCOME|RESULT|STATUS)(?:\*\*)?\s*[:|-]\s*\*{0,2}\s*(SKIP|SKIPPED|DISCARDED|DISCARD)\b/i.test(text) ||
451
- /\|\s*\*\*?Status\*\*?\s*\|\s*\*\*?(SKIP|SKIPPED|Discarded|DISCARDED)\*\*?/i.test(text);
452
- const explicitApplied = /\b(APPLICATION OUTCOME|RESULT|STATUS)(?:\*\*)?\s*[:|-]\s*\*{0,2}\s*APPLIED\b/i.test(text) ||
453
- /\|\s*\*\*?Status\*\*?\s*\|\s*\*\*?Applied\*\*?/i.test(text);
454
-
455
- if (explicitFailed) return 'Failed';
456
- if (explicitSkipped) return 'Discarded';
457
- if (explicitApplied) return 'Applied';
458
-
459
- if (/\bAPPLY FAILED\b/i.test(text) || /^\s*(FAILED|Failed)\b/m.test(text)) return 'Failed';
460
- if (/^\s*(SKIP|SKIPPED|DISCARDED|Discarded)\b/m.test(text) ||
461
- /\b(DUPLICATE|job posting closed|role no longer available)\b/i.test(text)) return 'Discarded';
462
- if (/\bwith\s+\*\*?Applied\*\*?\s+status\b/i.test(text) ||
463
- /\bAPPLIED\s+https?:\/\//i.test(text) ||
464
- /\b(successfully submitted|Applied via|Thank you for applying|confirmation page)\b/i.test(text)) return 'Applied';
465
- if (trackerWrites > 0) return 'TSV written';
466
- return 'unknown';
467
- }
468
-
469
- function hasOutcome(text) {
470
- return outcomeFromText(text) !== 'unknown' ||
471
- /tracker-additions\/.*\.tsv/i.test(text) ||
472
- /\bAll\s+\d+\s+jobs?\s+dispatched\b/i.test(text) ||
473
- /\*\*(Applied|Skipped|Failed|Discarded)\s*\(\d+\):\*\*/i.test(text);
474
- }
475
-
476
- function sessionStatus({ taskCalls, children, childOutcomes, childProviderErrors, policyIssues, providerErrors }) {
350
+ function sessionStatus({ dispatchCalls, children, unresolvedChildren, childOutcomes, childProviderErrors, policyIssues, providerErrors }) {
477
351
  if (policyIssues.some((issue) => issue.severity === 'high')) return 'attention';
478
352
  if (providerErrors.length > 0) return 'attention';
479
353
  if (childProviderErrors > 0) return 'attention';
480
- if (taskCalls.some((task) => task.status && task.status !== 'completed')) return 'in-flight-or-incomplete';
481
- if (taskCalls.length > 0 && children.length > childOutcomes) return 'in-flight-or-incomplete';
482
- if (taskCalls.length > 0 && children.length === childOutcomes) return 'complete';
354
+ if (unresolvedChildren.length > 0) return 'in-flight-or-incomplete';
355
+ if (dispatchCalls.length > 0 && children.length > childOutcomes) return 'in-flight-or-incomplete';
356
+ if (dispatchCalls.length > 0 && children.length === childOutcomes) return 'complete';
483
357
  return 'observed';
484
358
  }
485
359
 
@@ -519,12 +393,11 @@ function listTsv(dir) {
519
393
  function nextActions({ tracker, policyIssues, providerErrors, children }) {
520
394
  const actions = [];
521
395
  if (tracker.pending.length > 0) actions.push('Run `npm run merge && npm run verify` when you are ready to fold pending TSV outcomes into day files.');
522
- if (policyIssues.some((issue) => issue.type === 'task_status_poll')) actions.push('Avoid resuming by spawning "check task status" tasks; inspect telemetry/trace and tracker files instead.');
523
- if (policyIssues.some((issue) => issue.type === 'proxy_prompt_leak')) actions.push('Restart OpenCode after updating the harness so new sessions load the proxy prompt hygiene rule.');
524
- if (policyIssues.some((issue) => issue.type === 'free_model_usage')) actions.push('Restart OpenCode and rerun `npm run update-harness` so application tiers use the bundled DeepSeek V4 Flash route.');
396
+ if (policyIssues.some((issue) => issue.type === 'task_status_poll')) actions.push('Avoid resuming by spawning "check status" child sessions; inspect telemetry/trace and tracker files instead.');
397
+ if (policyIssues.some((issue) => issue.type === 'proxy_prompt_leak')) actions.push('Refresh the harness instructions so new sessions inherit the proxy prompt hygiene rule.');
398
+ if (policyIssues.some((issue) => issue.type === 'free_model_usage')) actions.push('Refresh the harness config so application tiers use the intended paid/default route.');
525
399
  if (policyIssues.some((issue) => issue.type === 'requested_count_not_met')) actions.push('Resume the latest apply request or start a new run for the remaining requested jobs; telemetry did not see enough dispatches after the latest prompt.');
526
- if (policyIssues.some((issue) => issue.type === 'latest_request_no_visible_final_outcome')) actions.push('Inspect the latest child sessions before treating the current OpenCode run as complete.');
527
- if (policyIssues.some((issue) => issue.type === 'task_result_not_consumed')) actions.push('Resume the root session only to collect final task results and summarize; do not dispatch new applications until it reconciles current children.');
400
+ if (policyIssues.some((issue) => issue.type === 'latest_request_no_visible_final_outcome')) actions.push('Inspect the latest child sessions before treating the current run as complete.');
528
401
  if (policyIssues.some((issue) => issue.type === 'duplicate_task_url')) actions.push('Do not re-dispatch duplicate URLs automatically; inspect the prior child result and tracker TSV before retrying.');
529
402
  if (policyIssues.some((issue) => issue.type === 'dedupe_preflight_missed')) actions.push('Tighten candidate preflight: grep all application day files plus pending/merged TSVs before dispatching replacements.');
530
403
  if (providerErrors.some((err) => err.statusCode === 402)) actions.push('Provider balance errors occurred; use a non-402 fallback or add provider credits before retrying paid routes.');
@@ -535,6 +408,7 @@ function nextActions({ tracker, policyIssues, providerErrors, children }) {
535
408
  function summaryForList(telemetry) {
536
409
  return {
537
410
  id: telemetry.session.id,
411
+ harness: telemetry.session.harness,
538
412
  startedAt: telemetry.session.startedAt,
539
413
  updatedAt: telemetry.session.endedAt,
540
414
  status: telemetry.status,
@@ -547,112 +421,43 @@ function summaryForList(telemetry) {
547
421
  };
548
422
  }
549
423
 
550
- function modelUsageFromMessages(messages) {
551
- const counts = new Map();
552
- for (const message of messages) {
553
- const provider = stringValue(message.data.providerID);
554
- const model = stringValue(message.data.modelID);
555
- if (!provider && !model) continue;
556
- const key = `${provider}\u0000${model}`;
557
- const current = counts.get(key) || { provider, model, count: 0 };
558
- current.count += 1;
559
- counts.set(key, current);
560
- }
561
- return [...counts.values()].sort((a, b) => b.count - a.count || modelLabel(a).localeCompare(modelLabel(b)));
562
- }
563
-
564
- function mergeModelUsage(groups) {
565
- const counts = new Map();
566
- for (const group of groups) {
567
- for (const item of group || []) {
568
- const provider = stringValue(item.provider);
569
- const model = stringValue(item.model);
570
- const key = `${provider}\u0000${model}`;
571
- const current = counts.get(key) || { provider, model, count: 0 };
572
- current.count += Number(item.count || 0);
573
- counts.set(key, current);
574
- }
575
- }
576
- return [...counts.values()].sort((a, b) => b.count - a.count || modelLabel(a).localeCompare(modelLabel(b)));
577
- }
578
-
579
- function modelLabel(model) {
580
- return `${model.provider || '(unknown)'}/${model.model || '(unknown)'} x${model.count}`;
581
- }
582
-
583
- function isFreeModelRoute(provider, model) {
584
- const route = `${provider}/${model}`.toLowerCase();
585
- return route.includes(':free') ||
586
- route.includes('/big-pickle') ||
587
- route.includes('minimax-m2.5-free') ||
588
- route.includes('glm-4.5-air') ||
589
- route.includes('gpt-oss-20b') ||
590
- route.includes('qwen3-next-80b-a3b-instruct:free');
591
- }
592
-
593
- function requestedJobCount(prompt) {
594
- const text = String(prompt || '').toLowerCase();
595
- if (!/\b(job|jobs|application|applications)\b/.test(text)) return null;
596
- if (!/\b(apply|applt|another|nother|more|process)\b/.test(text)) return null;
597
- const match = text.match(/\b(\d{1,3})\b/);
598
- return match ? Number(match[1]) : null;
599
- }
600
-
601
- function firstUrl(text) {
602
- const match = String(text || '').match(/https?:\/\/[^\s)>\]]+/i);
603
- return match ? match[0].replace(/[.,;]+$/, '') : '';
604
- }
605
-
606
- function duplicateTaskUrlCount(taskCalls) {
607
- const seen = new Set();
608
- const duplicates = new Set();
609
- for (const task of taskCalls) {
610
- if (!task.url || task.isStatusPoll) continue;
611
- if (seen.has(task.url)) duplicates.add(task.url);
612
- seen.add(task.url);
613
- }
614
- return duplicates.size;
615
- }
616
-
617
- function mentionsLimitedCandidatePool(text) {
618
- return /\b(only|just)\s+\d+\s+(candidate|candidates|jobs?|applications?)\b/i.test(text) ||
619
- /\b(no more|not enough|ran out of|exhausted)\s+(candidate|candidates|jobs?|applications?|pipeline)\b/i.test(text);
620
- }
621
-
622
424
  function printList(items) {
623
425
  const rows = items.map((item) => [
624
426
  item.id,
427
+ item.harness,
625
428
  item.startedAt.replace('T', ' ').replace(/\.\d+Z$/, 'Z'),
626
429
  item.status,
627
430
  String(item.tasks),
628
431
  `${item.outcomes}/${item.children}`,
629
432
  String(item.issues + item.providerErrors),
630
- shorten(item.prompt || '', 42),
433
+ shorten(item.prompt || '', 36),
631
434
  ]);
632
- const header = ['session', 'started', 'status', 'tasks', 'outcomes', 'alerts', 'prompt'];
435
+ const header = ['session', 'harness', 'started', 'status', 'dispatches', 'outcomes', 'alerts', 'prompt'];
633
436
  printTable(header, rows);
634
437
  }
635
438
 
636
439
  function printStatus(telemetry) {
637
- console.log(`project: ${telemetry.projectDir}`);
638
- console.log(`session: ${telemetry.session.id}`);
639
- console.log(`status: ${telemetry.status}`);
640
- console.log(`started: ${telemetry.session.startedAt}`);
641
- console.log(`prompt: ${shorten(telemetry.prompt || '', 100)}`);
440
+ console.log(`project: ${telemetry.projectDir}`);
441
+ console.log(`session: ${telemetry.session.id}`);
442
+ console.log(`harness: ${telemetry.session.harness}`);
443
+ console.log(`status: ${telemetry.status}`);
444
+ console.log(`started: ${telemetry.session.startedAt}`);
445
+ if (telemetry.session.title) console.log(`title: ${telemetry.session.title}`);
446
+ console.log(`prompt: ${shorten(telemetry.prompt || '', 100)}`);
642
447
  if (telemetry.userRequests.length > 1 || telemetry.latestRequest?.requestedJobs) {
643
448
  const latest = telemetry.latestRequest;
644
449
  const requestDetail = latest?.requestedJobs
645
450
  ? `latest ${latest.taskDispatches}/${latest.requestedJobs} dispatches`
646
451
  : `latest ${latest?.taskDispatches ?? 0} dispatches`;
647
- console.log(`requests: ${telemetry.userRequests.length} user prompt${telemetry.userRequests.length === 1 ? '' : 's'} (${requestDetail})`);
452
+ console.log(`requests: ${telemetry.userRequests.length} user prompt${telemetry.userRequests.length === 1 ? '' : 's'} (${requestDetail})`);
648
453
  }
649
- console.log(`tasks: ${telemetry.tasks.total} (${telemetry.tasks.statusPolls} status-poll, ${telemetry.tasks.running} running)`);
650
- console.log(`children: ${telemetry.children.withOutcomes}/${telemetry.children.total} with outcomes`);
651
- console.log(`tracker: ${telemetry.tracker.pending.length} pending TSVs, ${telemetry.tracker.mergedCount} merged TSVs`);
652
- console.log(`ledger: ${telemetry.tracker.ledger.error ? `error: ${telemetry.tracker.ledger.error}` : telemetry.tracker.ledger.exists ? `${telemetry.tracker.ledger.events} events` : 'missing'}`);
653
- console.log(`models: ${telemetry.models.slice(0, 3).map(modelLabel).join(', ') || 'none'}`);
654
- console.log(`errors: ${telemetry.providerErrors.length} root, ${telemetry.children.providerErrors} child provider errors, ${telemetry.children.toolErrors} child tool errors`);
655
- console.log(`issues: ${telemetry.policyIssues.length}`);
454
+ console.log(`dispatches: ${telemetry.tasks.total} (${telemetry.tasks.statusPolls} status-poll, ${telemetry.tasks.running} unresolved)`);
455
+ console.log(`children: ${telemetry.children.withOutcomes}/${telemetry.children.total} with outcomes`);
456
+ console.log(`tracker: ${telemetry.tracker.pending.length} pending TSVs, ${telemetry.tracker.mergedCount} merged TSVs`);
457
+ console.log(`ledger: ${telemetry.tracker.ledger.error ? `error: ${telemetry.tracker.ledger.error}` : telemetry.tracker.ledger.exists ? `${telemetry.tracker.ledger.events} events` : 'missing'}`);
458
+ console.log(`models: ${telemetry.models.slice(0, 3).map(modelLabel).join(', ') || 'none'}`);
459
+ console.log(`errors: ${telemetry.providerErrors.length} root, ${telemetry.children.providerErrors} child provider errors, ${telemetry.children.toolErrors} child tool errors`);
460
+ console.log(`issues: ${telemetry.policyIssues.length}`);
656
461
 
657
462
  if (telemetry.policyIssues.length > 0) {
658
463
  console.log('\nissues:');
@@ -690,20 +495,20 @@ function printStatus(telemetry) {
690
495
  function printShow(telemetry) {
691
496
  printStatus(telemetry);
692
497
  if (telemetry.tasks.calls.length > 0) {
693
- console.log('\ntask dispatches:');
498
+ console.log('\ndispatches:');
694
499
  for (const task of telemetry.tasks.calls) {
695
500
  const flags = [
696
501
  task.isStatusPoll ? 'status-poll' : '',
697
502
  task.status && task.status !== 'completed' ? task.status : '',
698
503
  task.proxyLeak ? 'proxy-values-detected' : '',
699
504
  ].filter(Boolean).join(', ');
700
- console.log(` - ${task.at} ${task.description || '(no description)'} ${task.sessionId || ''} ${task.subagentType || ''}${flags ? ` [${flags}]` : ''}`);
505
+ console.log(` - ${task.at} ${task.name} ${task.description || '(no description)'} ${task.sessionId || ''} ${task.subagentType || ''}${flags ? ` [${flags}]` : ''}`);
701
506
  }
702
507
  }
703
508
  if (telemetry.providerErrors.length > 0) {
704
509
  console.log('\nprovider errors:');
705
510
  for (const err of telemetry.providerErrors) {
706
- console.log(` - ${err.at} ${err.provider}/${err.model} ${err.statusCode || ''} ${err.category}: ${err.message}`);
511
+ console.log(` - ${err.at} ${err.provider || '(unknown)'}/${err.model || '(unknown)'} ${err.statusCode || ''} ${err.category}: ${err.message}`);
707
512
  }
708
513
  }
709
514
  }
@@ -715,72 +520,26 @@ function printTable(header, rows) {
715
520
  for (const row of rows) console.log(row.map((cell, i) => pad(cell, widths[i])).join(' '));
716
521
  }
717
522
 
718
- function latestRootTelemetry(opts) {
719
- const sessions = discoverSessions(opts);
720
- const roots = sessions.filter((session) => !session.parentId);
721
- if (roots.length === 0) return { sessions, telemetry: null };
722
- return { sessions, telemetry: analyzeSession(roots[0], sessions, opts) };
723
- }
724
-
725
- function objectOrEmpty(value) {
726
- return value && typeof value === 'object' ? value : {};
727
- }
728
-
729
- function stringValue(value) {
730
- return typeof value === 'string' ? value : value == null ? '' : String(value);
731
- }
732
-
733
- function statusCodeFromText(text) {
734
- const match = String(text).match(/\b(40[0-9]|42[0-9]|50[0-9])\b/);
735
- return match ? Number(match[1]) : undefined;
736
- }
737
-
738
- function providerErrorCategory(text, statusCode) {
739
- if (statusCode === 402 || /insufficient|balance|credits|diem/i.test(text)) return 'balance';
740
- if (statusCode === 429 || /rate.?limit|quota/i.test(text)) return 'rate-limit';
741
- if (/overload|temporarily unavailable|timeout/i.test(text)) return 'transient';
742
- return 'provider-error';
743
- }
744
-
745
- function hasProxyLeak(text) {
746
- const raw = String(text || '');
747
- if (!/proxy/i.test(raw)) return false;
748
- return /\b(server|username|password|bypass)["']?\s*[:=]\s*["']?[^"',\s)}]+/i.test(raw) ||
749
- /brd-customer|superproxy|oxylabs|smartproxy|soax/i.test(raw);
750
- }
751
-
752
- function redactSecrets(text) {
753
- return String(text || '')
754
- .replace(/\b(password|username|server|bypass)["']?\s*[:=]\s*["']?[^"',\s)}]+/gi, '$1=<redacted>')
755
- .replace(/brd-customer-[A-Za-z0-9_.-]+/g, '<redacted-proxy-user>');
756
- }
757
-
758
- function relativeToProject(file, projectDir = PROJECT_DIR) {
759
- return file.startsWith(`${projectDir}/`) ? file.slice(projectDir.length + 1) : file;
760
- }
761
-
762
- function clean(text) {
763
- return String(text || '').replace(/\s+/g, ' ').trim();
764
- }
765
-
766
- function shorten(value, max) {
767
- const text = clean(value);
768
- if (text.length <= max) return text;
769
- return `${text.slice(0, max - 1)}...`;
770
- }
771
-
772
- function pad(value, width) {
773
- const text = String(value ?? '');
774
- return text.length >= width ? text : text + ' '.repeat(width - text.length);
523
+ async function recentTelemetry(opts) {
524
+ const refs = await discoverProjectSessions(opts);
525
+ if (refs.length === 0) return { refs, context: null, telemetry: null };
526
+ const context = loadContext(refs);
527
+ const roots = rootRefs(context.loadedRefs, context.graph);
528
+ const selected = roots[0] || refs[0] || null;
529
+ return {
530
+ refs,
531
+ context,
532
+ telemetry: selected ? analyzeSession(selected, context, opts) : null,
533
+ };
775
534
  }
776
535
 
777
536
  async function runWatch(opts) {
778
537
  while (true) {
779
538
  console.clear();
780
539
  console.log(new Date().toISOString());
781
- const { telemetry } = latestRootTelemetry(opts);
540
+ const { telemetry } = await recentTelemetry(opts);
782
541
  if (!telemetry) {
783
- console.log('No recent JobForge OpenCode sessions found.');
542
+ console.log('No recent JobForge sessions found.');
784
543
  } else {
785
544
  printStatus(telemetry);
786
545
  }
@@ -804,15 +563,15 @@ async function main() {
804
563
  console.error(`job-forge telemetry:list: ${opts.error}`);
805
564
  return 2;
806
565
  }
807
- const sessions = discoverSessions(opts);
808
- const items = sessions
809
- .filter((session) => !session.parentId)
810
- .map((session) => summaryForList(analyzeSession(session, sessions, opts)));
566
+ const refs = await discoverProjectSessions(opts);
567
+ if (refs.length === 0) {
568
+ console.error('job-forge telemetry:list: no recent JobForge sessions found');
569
+ return 2;
570
+ }
571
+ const context = loadContext(refs);
572
+ const items = rootRefs(context.loadedRefs, context.graph).map((ref) => summaryForList(analyzeSession(ref, context, opts)));
811
573
  if (opts.json) {
812
574
  console.log(JSON.stringify(items, null, 2));
813
- } else if (items.length === 0) {
814
- console.error('job-forge telemetry:list: no recent JobForge OpenCode sessions found');
815
- return 2;
816
575
  } else {
817
576
  printList(items);
818
577
  }
@@ -829,9 +588,9 @@ async function main() {
829
588
  console.error(`job-forge telemetry:status: ${opts.error}`);
830
589
  return 2;
831
590
  }
832
- const { telemetry } = latestRootTelemetry(opts);
591
+ const { telemetry } = await recentTelemetry(opts);
833
592
  if (!telemetry) {
834
- console.error('job-forge telemetry:status: no recent JobForge OpenCode sessions found');
593
+ console.error('job-forge telemetry:status: no recent JobForge sessions found');
835
594
  return 2;
836
595
  }
837
596
  if (opts.json) console.log(JSON.stringify(telemetry, null, 2));
@@ -853,13 +612,18 @@ async function main() {
853
612
  console.error('job-forge telemetry:show: missing <id-or-prefix>');
854
613
  return 2;
855
614
  }
856
- const sessions = discoverSessions(opts, { includeAllForShow: true });
857
- const session = findSessionById(sessions, positional[0]);
858
- if (!session) {
615
+ const refs = await discoverProjectSessions({ ...opts, since: undefined });
616
+ const sessionRef = findObservedSession(refs, positional[0]);
617
+ if (!sessionRef) {
859
618
  console.error(`job-forge telemetry:show: no session matches "${positional[0]}"`);
860
619
  return 2;
861
620
  }
862
- const telemetry = analyzeSession(session, sessions, opts);
621
+ const context = loadContext(refs);
622
+ if (!context.sessionsById.has(sessionRef.id)) {
623
+ console.error(`job-forge telemetry:show: could not load session "${sessionRef.id}": ${context.loadErrors.get(sessionRef.id) || 'unknown parse error'}`);
624
+ return 2;
625
+ }
626
+ const telemetry = analyzeSession(sessionRef, context, opts);
863
627
  if (opts.json) console.log(JSON.stringify(telemetry, null, 2));
864
628
  else printShow(telemetry);
865
629
  return 0;