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.
- package/.claude/agents/general-free.md +3 -2
- package/.codex/config.toml +1 -1
- package/.cursor/mcp.json +1 -1
- package/.cursor/rules/agent-general-free.mdc +3 -2
- package/.cursor/rules/main.mdc +3 -3
- package/.mcp.json +1 -1
- package/.opencode/agents/general-free.md +3 -2
- package/.opencode/instructions.md +8 -0
- package/.opencode/skills/job-forge.md +1 -1
- package/AGENTS.md +3 -3
- package/CLAUDE.md +3 -3
- package/README.md +2 -2
- package/batch/README.md +7 -6
- package/batch/batch-runner.sh +2 -2
- package/bin/create-job-forge.mjs +6 -3
- package/bin/sync.mjs +35 -1
- package/config/profile.example.yml +8 -5
- package/docs/ARCHITECTURE.md +9 -8
- package/docs/CUSTOMIZATION.md +6 -6
- package/docs/SETUP.md +1 -1
- package/iso/agents/general-free.md +3 -2
- package/iso/commands/job-forge.md +1 -1
- package/iso/instructions.md +3 -3
- package/iso/instructions.opencode.md +8 -0
- package/iso/mcp.json +1 -1
- package/lib/jobforge-observability.mjs +847 -0
- package/modes/apply.md +4 -3
- package/modes/auto-pipeline.md +2 -2
- package/modes/pipeline.md +2 -2
- package/modes/reference-geometra.md +5 -5
- package/modes/reference-portals.md +10 -10
- package/modes/scan.md +3 -3
- package/opencode.json +3 -2
- package/package.json +8 -5
- package/scripts/batch-orchestrator.mjs +158 -9
- package/scripts/check-iso-smoke.mjs +3 -0
- package/scripts/guard.mjs +114 -190
- package/scripts/telemetry.mjs +214 -450
- package/scripts/trace.mjs +103 -232
package/scripts/trace.mjs
CHANGED
|
@@ -1,36 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawnSync } from 'child_process';
|
|
4
|
-
import { existsSync } from 'fs';
|
|
5
4
|
import { createRequire } from 'module';
|
|
6
5
|
import { dirname, join, resolve } from 'path';
|
|
7
|
-
import { fileURLToPath } from 'url';
|
|
8
6
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
clean,
|
|
8
|
+
discoverProjectSessions,
|
|
9
|
+
findObservedSession,
|
|
10
|
+
loadObservedSession,
|
|
11
|
+
pad,
|
|
12
|
+
safeJson,
|
|
13
|
+
shorten,
|
|
14
|
+
statsForSessions,
|
|
15
|
+
} from '../lib/jobforge-observability.mjs';
|
|
14
16
|
|
|
15
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
17
|
const require = createRequire(import.meta.url);
|
|
17
18
|
const PROJECT_DIR = process.env.JOB_FORGE_PROJECT || process.cwd();
|
|
18
19
|
|
|
19
|
-
const USAGE = `job-forge trace — local
|
|
20
|
+
const USAGE = `job-forge trace — local transcript observability across supported harnesses
|
|
20
21
|
|
|
21
22
|
Usage:
|
|
22
|
-
job-forge trace:list [--since 7d] [--cwd <dir>] [--json]
|
|
23
|
-
job-forge trace:stats [<id-or-prefix>...] [--since 7d] [--cwd <dir>] [--json]
|
|
24
|
-
job-forge trace:show <id-or-prefix> [--events <kinds>] [--grep <regex>]
|
|
23
|
+
job-forge trace:list [--since 7d] [--cwd <dir>] [--harness <name>] [--json]
|
|
24
|
+
job-forge trace:stats [<id-or-prefix>...] [--since 7d] [--cwd <dir>] [--harness <name>] [--json]
|
|
25
|
+
job-forge trace:show <id-or-prefix> [--cwd <dir>] [--harness <name>] [--events <kinds>] [--grep <regex>]
|
|
25
26
|
job-forge trace <iso-trace args...>
|
|
26
27
|
|
|
27
|
-
Common aliases default to
|
|
28
|
+
Common aliases default to sessions for the current JobForge project.
|
|
28
29
|
Use "job-forge trace sources" or "job-forge trace where" for raw iso-trace passthrough.`;
|
|
29
30
|
|
|
30
31
|
const [cmd = 'help', ...args] = process.argv.slice(2);
|
|
31
32
|
|
|
32
33
|
function parseFilters(rawArgs) {
|
|
33
|
-
const opts = { since: '7d', cwd: PROJECT_DIR, json: false };
|
|
34
|
+
const opts = { since: '7d', cwd: PROJECT_DIR, harness: '', json: false };
|
|
34
35
|
const positional = [];
|
|
35
36
|
|
|
36
37
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
@@ -43,6 +44,10 @@ function parseFilters(rawArgs) {
|
|
|
43
44
|
opts.cwd = rawArgs[++i];
|
|
44
45
|
} else if (arg.startsWith('--cwd=')) {
|
|
45
46
|
opts.cwd = arg.slice('--cwd='.length);
|
|
47
|
+
} else if (arg === '--harness') {
|
|
48
|
+
opts.harness = rawArgs[++i];
|
|
49
|
+
} else if (arg.startsWith('--harness=')) {
|
|
50
|
+
opts.harness = arg.slice('--harness='.length);
|
|
46
51
|
} else if (arg === '--json') {
|
|
47
52
|
opts.json = true;
|
|
48
53
|
} else if (arg === '--help' || arg === '-h') {
|
|
@@ -59,12 +64,20 @@ function parseFilters(rawArgs) {
|
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
function parseShowArgs(rawArgs) {
|
|
62
|
-
const opts = {};
|
|
67
|
+
const opts = { cwd: PROJECT_DIR, harness: '' };
|
|
63
68
|
const positional = [];
|
|
64
69
|
|
|
65
70
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
66
71
|
const arg = rawArgs[i];
|
|
67
|
-
if (arg === '--
|
|
72
|
+
if (arg === '--cwd') {
|
|
73
|
+
opts.cwd = rawArgs[++i];
|
|
74
|
+
} else if (arg.startsWith('--cwd=')) {
|
|
75
|
+
opts.cwd = arg.slice('--cwd='.length);
|
|
76
|
+
} else if (arg === '--harness') {
|
|
77
|
+
opts.harness = rawArgs[++i];
|
|
78
|
+
} else if (arg.startsWith('--harness=')) {
|
|
79
|
+
opts.harness = arg.slice('--harness='.length);
|
|
80
|
+
} else if (arg === '--events') {
|
|
68
81
|
const raw = rawArgs[++i] || '';
|
|
69
82
|
opts.events = new Set(raw.split(',').map((s) => s.trim()).filter(Boolean));
|
|
70
83
|
} else if (arg.startsWith('--events=')) {
|
|
@@ -83,6 +96,7 @@ function parseShowArgs(rawArgs) {
|
|
|
83
96
|
}
|
|
84
97
|
}
|
|
85
98
|
|
|
99
|
+
opts.cwd = resolve(opts.cwd || PROJECT_DIR);
|
|
86
100
|
return { opts, positional };
|
|
87
101
|
}
|
|
88
102
|
|
|
@@ -95,96 +109,23 @@ function compileRegex(pattern, context) {
|
|
|
95
109
|
}
|
|
96
110
|
}
|
|
97
111
|
|
|
98
|
-
async function discoverOpenCodeRefs(opts) {
|
|
99
|
-
const dbPath = defaultOpenCodeDbPath();
|
|
100
|
-
if (!existsSync(dbPath)) return [];
|
|
101
|
-
|
|
102
|
-
const where = [
|
|
103
|
-
's.time_archived is null',
|
|
104
|
-
`s.directory = ${sqlString(resolve(opts.cwd || PROJECT_DIR))}`,
|
|
105
|
-
];
|
|
106
|
-
const sinceMs = parseSinceCutoff(opts.since);
|
|
107
|
-
if (sinceMs !== undefined) {
|
|
108
|
-
where.push(`s.time_created >= ${Number(sinceMs)}`);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const rows = queryOpenCodeDb(dbPath, [
|
|
112
|
-
'select',
|
|
113
|
-
' s.id,',
|
|
114
|
-
' s.directory,',
|
|
115
|
-
' s.time_created,',
|
|
116
|
-
' s.time_updated,',
|
|
117
|
-
' (select count(*) from message m where m.session_id = s.id) as turn_count,',
|
|
118
|
-
' (',
|
|
119
|
-
' (select coalesce(sum(length(data)), 0) from message m where m.session_id = s.id) +',
|
|
120
|
-
' (select coalesce(sum(length(data)), 0) from part p where p.session_id = s.id)',
|
|
121
|
-
' ) as size_bytes',
|
|
122
|
-
'from session s',
|
|
123
|
-
`where ${where.join(' and ')}`,
|
|
124
|
-
'order by s.time_updated desc',
|
|
125
|
-
].join(' '));
|
|
126
|
-
|
|
127
|
-
return rows.map((row) => ({
|
|
128
|
-
id: row.id,
|
|
129
|
-
source: {
|
|
130
|
-
harness: 'opencode',
|
|
131
|
-
format: 'opencode/sqlite-v1',
|
|
132
|
-
path: openCodeSessionLocator(row.id, dbPath),
|
|
133
|
-
},
|
|
134
|
-
cwd: row.directory,
|
|
135
|
-
startedAt: msToIso(row.time_created),
|
|
136
|
-
endedAt: msToIso(row.time_updated),
|
|
137
|
-
turnCount: row.turn_count ?? 0,
|
|
138
|
-
sizeBytes: row.size_bytes ?? 0,
|
|
139
|
-
}));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function queryOpenCodeDb(dbPath, sql) {
|
|
143
|
-
const result = spawnSync('sqlite3', ['-json', dbPath, sql], {
|
|
144
|
-
encoding: 'utf8',
|
|
145
|
-
maxBuffer: 16 * 1024 * 1024,
|
|
146
|
-
});
|
|
147
|
-
if ((result.status ?? 0) !== 0) {
|
|
148
|
-
const detail = result.stderr?.trim() || result.stdout?.trim() || `exit ${result.status ?? 1}`;
|
|
149
|
-
throw new Error(`job-forge trace: sqlite3 query failed: ${detail}`);
|
|
150
|
-
}
|
|
151
|
-
return JSON.parse(result.stdout || '[]');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function sqlString(value) {
|
|
155
|
-
return `'${String(value).replaceAll("'", "''")}'`;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function msToIso(ms) {
|
|
159
|
-
return new Date(Number(ms)).toISOString();
|
|
160
|
-
}
|
|
161
|
-
|
|
162
112
|
function sizeLabel(bytes) {
|
|
163
113
|
if (bytes < 1024) return `${bytes} B`;
|
|
164
114
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
165
115
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
166
116
|
}
|
|
167
117
|
|
|
168
|
-
function shorten(value, max) {
|
|
169
|
-
const text = String(value ?? '');
|
|
170
|
-
if (text.length <= max) return text;
|
|
171
|
-
return `${text.slice(0, max - 1)}...`;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function pad(value, width) {
|
|
175
|
-
const text = String(value ?? '');
|
|
176
|
-
return text.length >= width ? text : text + ' '.repeat(width - text.length);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
118
|
function printSessionTable(refs) {
|
|
180
119
|
const rows = refs.map((ref) => [
|
|
181
120
|
ref.id,
|
|
121
|
+
ref.source.harness,
|
|
182
122
|
ref.startedAt.replace('T', ' ').replace(/\.\d+Z$/, 'Z'),
|
|
183
|
-
shorten(ref.
|
|
123
|
+
shorten(ref.title || '', 24),
|
|
124
|
+
shorten(ref.cwd, 40),
|
|
184
125
|
String(ref.turnCount),
|
|
185
126
|
sizeLabel(ref.sizeBytes),
|
|
186
127
|
]);
|
|
187
|
-
const header = ['id', 'started', 'cwd', 'turns', 'size'];
|
|
128
|
+
const header = ['id', 'harness', 'started', 'title', 'cwd', 'turns', 'size'];
|
|
188
129
|
const widths = header.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i].length)));
|
|
189
130
|
|
|
190
131
|
console.log(header.map((h, i) => pad(h, widths[i])).join(' '));
|
|
@@ -211,151 +152,51 @@ function printStats(result) {
|
|
|
211
152
|
}
|
|
212
153
|
}
|
|
213
154
|
|
|
214
|
-
function
|
|
215
|
-
const result = {
|
|
216
|
-
sessions: refs.length,
|
|
217
|
-
turns: 0,
|
|
218
|
-
durationMs: 0,
|
|
219
|
-
tokens: { input: 0, output: 0, cacheRead: 0, cacheCreated: 0 },
|
|
220
|
-
toolCalls: {},
|
|
221
|
-
fileOps: {},
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
for (const ref of refs) {
|
|
225
|
-
const rows = loadOpenCodeRows(ref.id);
|
|
226
|
-
result.turns += rows.messages.length;
|
|
227
|
-
result.durationMs += Math.max(0, Date.parse(ref.endedAt || ref.startedAt) - Date.parse(ref.startedAt));
|
|
228
|
-
|
|
229
|
-
for (const row of rows.messages) {
|
|
230
|
-
const data = parseJson(row.data);
|
|
231
|
-
const tokens = data.tokens;
|
|
232
|
-
if (!tokens || typeof tokens !== 'object') continue;
|
|
233
|
-
result.tokens.input += Number(tokens.input || 0);
|
|
234
|
-
result.tokens.output += Number(tokens.output || 0);
|
|
235
|
-
result.tokens.cacheRead += Number(tokens.cache?.read || 0);
|
|
236
|
-
result.tokens.cacheCreated += Number(tokens.cache?.write || 0);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
for (const row of rows.parts) {
|
|
240
|
-
const data = parseJson(row.data);
|
|
241
|
-
if (data.type !== 'tool') continue;
|
|
242
|
-
const toolName = data.tool || 'unknown';
|
|
243
|
-
result.toolCalls[toolName] = (result.toolCalls[toolName] || 0) + 1;
|
|
244
|
-
const op = fileOpForTool(toolName);
|
|
245
|
-
if (op) result.fileOps[op] = (result.fileOps[op] || 0) + 1;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return result;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function printOpenCodeSession(ref, opts) {
|
|
253
|
-
const rows = loadOpenCodeRows(ref.id);
|
|
155
|
+
function printSession(ref, session, opts) {
|
|
254
156
|
console.log(`id: ${ref.id}`);
|
|
255
|
-
console.log(`
|
|
157
|
+
console.log(`harness: ${ref.source.harness}`);
|
|
158
|
+
console.log(`source: ${ref.source.format}`);
|
|
256
159
|
console.log(`path: ${ref.source.path}`);
|
|
257
160
|
console.log(`cwd: ${ref.cwd}`);
|
|
161
|
+
if (ref.title) console.log(`title: ${ref.title}`);
|
|
162
|
+
if (session.model) console.log(`model: ${session.model}`);
|
|
258
163
|
console.log(`started: ${ref.startedAt}`);
|
|
259
164
|
if (ref.endedAt) console.log(`ended: ${ref.endedAt}`);
|
|
260
|
-
console.log(`turns: ${
|
|
165
|
+
console.log(`turns: ${session.turns.length}`);
|
|
261
166
|
console.log('');
|
|
262
167
|
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
168
|
+
for (const turn of session.turns) {
|
|
169
|
+
for (const event of turn.events) {
|
|
170
|
+
if (opts.events && !opts.events.has(event.kind)) continue;
|
|
171
|
+
const line = `${turn.at} ${event.kind}: ${formatEvent(event)}`;
|
|
172
|
+
if (opts.grep && !opts.grep.test(line)) continue;
|
|
173
|
+
console.log(line);
|
|
174
|
+
}
|
|
269
175
|
}
|
|
270
176
|
}
|
|
271
177
|
|
|
272
|
-
function
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
return {
|
|
276
|
-
messages: queryOpenCodeDb(dbPath, `select id, time_created, data from message where session_id = ${id} order by time_created, id`),
|
|
277
|
-
parts: queryOpenCodeDb(dbPath, `select id, message_id, time_created, data from part where session_id = ${id} order by time_created, id`),
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function openCodeEvents(rows) {
|
|
282
|
-
const events = [];
|
|
283
|
-
|
|
284
|
-
for (const row of rows.messages) {
|
|
285
|
-
const data = parseJson(row.data);
|
|
286
|
-
const at = msToIso(row.time_created);
|
|
287
|
-
const model = data.modelID && data.providerID ? `${data.providerID}/${data.modelID}` : undefined;
|
|
288
|
-
const error = data.error?.data?.message || data.error?.message;
|
|
289
|
-
events.push({
|
|
290
|
-
kind: error ? 'error' : 'turn',
|
|
291
|
-
at,
|
|
292
|
-
text: error
|
|
293
|
-
? `${data.role || 'assistant'} ${data.agent || ''} ${model || ''}: ${error}`
|
|
294
|
-
: `${data.role || 'unknown'} ${data.agent || ''} ${model || ''} finish=${data.finish || 'unknown'}`,
|
|
295
|
-
});
|
|
296
|
-
if (data.tokens) {
|
|
297
|
-
events.push({
|
|
298
|
-
kind: 'token_usage',
|
|
299
|
-
at,
|
|
300
|
-
text: `input=${data.tokens.input || 0} output=${data.tokens.output || 0} cache_read=${data.tokens.cache?.read || 0} cache_created=${data.tokens.cache?.write || 0}${model ? ` model=${model}` : ''}`,
|
|
301
|
-
});
|
|
302
|
-
}
|
|
178
|
+
function formatEvent(event) {
|
|
179
|
+
if (event.kind === 'message') {
|
|
180
|
+
return `${event.role}: ${oneLine(event.text, 360)}`;
|
|
303
181
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const data = parseJson(row.data);
|
|
307
|
-
const at = msToIso(row.time_created);
|
|
308
|
-
if (data.type === 'text') {
|
|
309
|
-
events.push({ kind: 'message', at, text: data.text || '' });
|
|
310
|
-
} else if (data.type === 'reasoning') {
|
|
311
|
-
events.push({ kind: 'reasoning', at, text: data.text || '' });
|
|
312
|
-
} else if (data.type === 'tool') {
|
|
313
|
-
const status = data.state?.status ? ` status=${data.state.status}` : '';
|
|
314
|
-
const input = data.state?.input ? ` ${JSON.stringify(data.state.input)}` : '';
|
|
315
|
-
const output = data.state?.output ? ` => ${data.state.output}` : '';
|
|
316
|
-
events.push({ kind: 'tool_call', at, text: `${data.tool || 'unknown'}${status}${input}${output}` });
|
|
317
|
-
const op = fileOpForTool(data.tool);
|
|
318
|
-
if (op) events.push({ kind: 'file_op', at, text: `${op} ${filePathFromTool(data) || ''}`.trim() });
|
|
319
|
-
} else if (data.__parseError) {
|
|
320
|
-
events.push({ kind: 'error', at, text: `unparseable part JSON: ${data.__parseError}` });
|
|
321
|
-
} else {
|
|
322
|
-
events.push({ kind: data.type || 'part', at, text: JSON.stringify(data) });
|
|
323
|
-
}
|
|
182
|
+
if (event.kind === 'tool_call') {
|
|
183
|
+
return `${event.name || 'unknown'} ${oneLine(safeJson(event.input), 360)}`;
|
|
324
184
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function formatOpenCodeEvent(event) {
|
|
330
|
-
return `${event.at} ${event.kind}: ${oneLine(event.text, 360)}`;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function parseJson(raw) {
|
|
334
|
-
try {
|
|
335
|
-
return JSON.parse(raw || '{}');
|
|
336
|
-
} catch (error) {
|
|
337
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
338
|
-
return { __parseError: message, __raw: raw };
|
|
185
|
+
if (event.kind === 'tool_result') {
|
|
186
|
+
const suffix = event.error ? ` error=${oneLine(event.error, 160)}` : '';
|
|
187
|
+
return `${event.toolUseId || '(unknown)'}${suffix}${event.output ? ` => ${oneLine(event.output, 240)}` : ''}`;
|
|
339
188
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (toolName === 'grep') return 'search';
|
|
348
|
-
return undefined;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function filePathFromTool(part) {
|
|
352
|
-
const input = part.state?.input;
|
|
353
|
-
if (!input || typeof input !== 'object') return undefined;
|
|
354
|
-
return input.filePath || input.path || input.pattern;
|
|
189
|
+
if (event.kind === 'file_op') {
|
|
190
|
+
return `${event.op} ${event.path} (${event.tool})`;
|
|
191
|
+
}
|
|
192
|
+
if (event.kind === 'token_usage') {
|
|
193
|
+
return `input=${event.input} output=${event.output} cache_read=${event.cacheRead} cache_created=${event.cacheCreated}${event.model ? ` model=${event.model}` : ''}`;
|
|
194
|
+
}
|
|
195
|
+
return oneLine(safeJson(event), 360);
|
|
355
196
|
}
|
|
356
197
|
|
|
357
198
|
function oneLine(value, max) {
|
|
358
|
-
return shorten(
|
|
199
|
+
return shorten(clean(value), max);
|
|
359
200
|
}
|
|
360
201
|
|
|
361
202
|
function resolveIsoTraceCli() {
|
|
@@ -373,6 +214,18 @@ function passthroughIsoTrace(rawArgs) {
|
|
|
373
214
|
return result.status ?? 1;
|
|
374
215
|
}
|
|
375
216
|
|
|
217
|
+
function tryLoadSession(ref) {
|
|
218
|
+
try {
|
|
219
|
+
return { ref, session: loadObservedSession(ref), error: null };
|
|
220
|
+
} catch (error) {
|
|
221
|
+
return {
|
|
222
|
+
ref,
|
|
223
|
+
session: null,
|
|
224
|
+
error: error instanceof Error ? error.message : String(error),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
376
229
|
async function main() {
|
|
377
230
|
if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
378
231
|
console.log(USAGE);
|
|
@@ -389,13 +242,13 @@ async function main() {
|
|
|
389
242
|
console.error(`job-forge trace:list: ${opts.error}`);
|
|
390
243
|
return 2;
|
|
391
244
|
}
|
|
392
|
-
const refs = await
|
|
245
|
+
const refs = await discoverProjectSessions(opts);
|
|
393
246
|
if (opts.json) {
|
|
394
247
|
console.log(JSON.stringify(refs, null, 2));
|
|
395
248
|
return 0;
|
|
396
249
|
}
|
|
397
250
|
if (refs.length === 0) {
|
|
398
|
-
console.error('job-forge trace:list: no
|
|
251
|
+
console.error('job-forge trace:list: no sessions found for this project');
|
|
399
252
|
return 2;
|
|
400
253
|
}
|
|
401
254
|
printSessionTable(refs);
|
|
@@ -412,18 +265,30 @@ async function main() {
|
|
|
412
265
|
console.error(`job-forge trace:stats: ${opts.error}`);
|
|
413
266
|
return 2;
|
|
414
267
|
}
|
|
415
|
-
const refs = await
|
|
268
|
+
const refs = await discoverProjectSessions(opts);
|
|
416
269
|
const selected = positional.length === 0
|
|
417
270
|
? refs
|
|
418
271
|
: positional.map((id) => {
|
|
419
|
-
const ref =
|
|
420
|
-
if (!ref) throw new Error(`job-forge trace:stats: no
|
|
272
|
+
const ref = findObservedSession(refs, id);
|
|
273
|
+
if (!ref) throw new Error(`job-forge trace:stats: no session matches "${id}"`);
|
|
421
274
|
return ref;
|
|
422
275
|
});
|
|
423
|
-
const
|
|
276
|
+
const loaded = selected.map(tryLoadSession);
|
|
277
|
+
const failures = loaded.filter((item) => item.error);
|
|
278
|
+
if (positional.length > 0 && failures.length > 0) {
|
|
279
|
+
throw new Error(`job-forge trace:stats: could not load session "${failures[0].ref.id}": ${failures[0].error}`);
|
|
280
|
+
}
|
|
281
|
+
const sessions = loaded.filter((item) => item.session).map((item) => item.session);
|
|
282
|
+
if (sessions.length === 0) {
|
|
283
|
+
throw new Error('job-forge trace:stats: no readable sessions found for this selection');
|
|
284
|
+
}
|
|
285
|
+
const result = statsForSessions(sessions);
|
|
424
286
|
if (opts.json) {
|
|
425
287
|
console.log(JSON.stringify(result, null, 2));
|
|
426
288
|
} else {
|
|
289
|
+
if (failures.length > 0) {
|
|
290
|
+
console.error(`job-forge trace:stats: skipped ${failures.length} unreadable session(s)`);
|
|
291
|
+
}
|
|
427
292
|
printStats(result);
|
|
428
293
|
}
|
|
429
294
|
return 0;
|
|
@@ -447,13 +312,19 @@ async function main() {
|
|
|
447
312
|
console.error('job-forge trace:show: missing <id-or-prefix>');
|
|
448
313
|
return 2;
|
|
449
314
|
}
|
|
450
|
-
const refs = await
|
|
451
|
-
const ref =
|
|
315
|
+
const refs = await discoverProjectSessions({ cwd: opts.cwd, harness: opts.harness, since: undefined });
|
|
316
|
+
const ref = findObservedSession(refs, positional[0]);
|
|
452
317
|
if (!ref) {
|
|
453
|
-
console.error(`job-forge trace:show: no
|
|
318
|
+
console.error(`job-forge trace:show: no session matches "${positional[0]}"`);
|
|
319
|
+
return 2;
|
|
320
|
+
}
|
|
321
|
+
const loaded = tryLoadSession(ref);
|
|
322
|
+
if (!loaded.session) {
|
|
323
|
+
console.error(`job-forge trace:show: could not load session "${ref.id}": ${loaded.error}`);
|
|
454
324
|
return 2;
|
|
455
325
|
}
|
|
456
|
-
|
|
326
|
+
const session = loaded.session;
|
|
327
|
+
printSession(ref, session, opts);
|
|
457
328
|
return 0;
|
|
458
329
|
}
|
|
459
330
|
|