@sentry/warden 0.20.0 → 0.22.0
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 +353 -0
- package/README.md +3 -3
- package/dist/cli/args.d.ts +7 -5
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +31 -17
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/commands/runs.d.ts +26 -0
- package/dist/cli/commands/runs.d.ts.map +1 -0
- package/dist/cli/commands/runs.js +726 -0
- package/dist/cli/commands/runs.js.map +1 -0
- package/dist/cli/log-cleanup.d.ts.map +1 -1
- package/dist/cli/log-cleanup.js +4 -0
- package/dist/cli/log-cleanup.js.map +1 -1
- package/dist/cli/main.d.ts +15 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +428 -128
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/output/index.d.ts +1 -1
- package/dist/cli/output/index.d.ts.map +1 -1
- package/dist/cli/output/index.js +1 -1
- package/dist/cli/output/index.js.map +1 -1
- package/dist/cli/output/ink-runner.d.ts.map +1 -1
- package/dist/cli/output/ink-runner.js +50 -14
- package/dist/cli/output/ink-runner.js.map +1 -1
- package/dist/cli/output/jsonl-schema-gen.d.ts +16 -0
- package/dist/cli/output/jsonl-schema-gen.d.ts.map +1 -0
- package/dist/cli/output/jsonl-schema-gen.js +65 -0
- package/dist/cli/output/jsonl-schema-gen.js.map +1 -0
- package/dist/cli/output/jsonl.d.ts +266 -26
- package/dist/cli/output/jsonl.d.ts.map +1 -1
- package/dist/cli/output/jsonl.js +377 -139
- package/dist/cli/output/jsonl.js.map +1 -1
- package/dist/cli/output/tasks.d.ts +6 -1
- package/dist/cli/output/tasks.d.ts.map +1 -1
- package/dist/cli/output/tasks.js +183 -27
- package/dist/cli/output/tasks.js.map +1 -1
- package/dist/cli/terminal.d.ts.map +1 -1
- package/dist/cli/terminal.js +20 -0
- package/dist/cli/terminal.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/sdk/analyze.d.ts.map +1 -1
- package/dist/sdk/analyze.js +103 -10
- package/dist/sdk/analyze.js.map +1 -1
- package/dist/sdk/errors.d.ts +11 -0
- package/dist/sdk/errors.d.ts.map +1 -1
- package/dist/sdk/errors.js +50 -0
- package/dist/sdk/errors.js.map +1 -1
- package/dist/sdk/runner.d.ts +1 -1
- package/dist/sdk/runner.d.ts.map +1 -1
- package/dist/sdk/types.d.ts +28 -1
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/types/index.d.ts +130 -6
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +72 -3
- package/dist/types/index.js.map +1 -1
- package/package.json +2 -1
- package/skills/warden/SKILL.md +5 -5
- package/skills/warden/SOURCES.md +39 -0
- package/skills/warden/references/cli-reference.md +11 -4
- package/skills/warden/references/config-schema.md +9 -0
- package/skills/warden/references/configuration.md +9 -0
- package/skills/warden-sweep/SKILL.md +43 -365
- package/skills/warden-sweep/SOURCES.md +43 -0
- package/skills/warden-sweep/references/issue-phase.md +24 -0
- package/skills/warden-sweep/references/organize-phase.md +31 -0
- package/skills/warden-sweep/references/patch-phase.md +142 -0
- package/skills/warden-sweep/references/resume-and-artifacts.md +51 -0
- package/skills/warden-sweep/references/scan-phase.md +46 -0
- package/skills/warden-sweep/references/script-interfaces.md +127 -0
- package/skills/warden-sweep/references/verify-phase.md +69 -0
- package/dist/cli/commands/logs.d.ts +0 -19
- package/dist/cli/commands/logs.d.ts.map +0 -1
- package/dist/cli/commands/logs.js +0 -412
- package/dist/cli/commands/logs.js.map +0 -1
package/dist/cli/output/jsonl.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { UsageStatsSchema,
|
|
5
|
+
import { UsageStatsSchema, SkillReportSchema, FileReportSchema, FindingSchema, AuxiliaryUsageMapSchema, FixStatusSchema, isExtractionErrorCode, SkillErrorSchema, SkippedFileSchema, } from '../../types/index.js';
|
|
6
6
|
import { mergeAuxiliaryUsage } from '../../sdk/usage.js';
|
|
7
|
+
import { logger } from '../../sentry.js';
|
|
7
8
|
import { countBySeverity } from './formatters.js';
|
|
8
9
|
/**
|
|
9
10
|
* Sentinel value recorded in JSONL metadata when no model is explicitly configured.
|
|
@@ -35,6 +36,16 @@ export function getRepoLogPath(repoRoot, runId, timestamp = new Date()) {
|
|
|
35
36
|
* Formal JSON Schema: specs/jsonl-schema.json
|
|
36
37
|
* Example payloads: specs/jsonl-examples.jsonl
|
|
37
38
|
* Reporter spec: specs/reporters.md Section 3 "JSONL Specification"
|
|
39
|
+
*
|
|
40
|
+
* BACKWARD COMPATIBILITY: breaking on-disk JSONL log formats is NEVER
|
|
41
|
+
* ALLOWED. Users keep .warden/logs/*.jsonl across versions. The schema
|
|
42
|
+
* may evolve — new optional fields, additive enum values, normalization
|
|
43
|
+
* — but every historical shape must continue to parse cleanly. Field
|
|
44
|
+
* renames require a preprocess that maps the old name to the new one
|
|
45
|
+
* (see FileReportSchema's `findingCount → findings` preprocess in
|
|
46
|
+
* src/types/index.ts). Removing a field is fine; making it optional in
|
|
47
|
+
* the schema preserves old logs. If you can't reconcile an old shape
|
|
48
|
+
* with a preprocess, the change is wrong — find a different path.
|
|
38
49
|
*/
|
|
39
50
|
/** Metadata common to every JSONL record. */
|
|
40
51
|
export const JsonlRunMetadataSchema = z.object({
|
|
@@ -46,44 +57,64 @@ export const JsonlRunMetadataSchema = z.object({
|
|
|
46
57
|
model: z.string().optional(),
|
|
47
58
|
headSha: z.string().optional(),
|
|
48
59
|
});
|
|
49
|
-
/** Per-file breakdown within a skill record. */
|
|
50
|
-
export const JsonlFileRecordSchema =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
usage: UsageStatsSchema.optional(),
|
|
55
|
-
});
|
|
56
|
-
/** One skill's analysis results. */
|
|
57
|
-
export const JsonlRecordSchema = z.object({
|
|
60
|
+
/** Per-file breakdown within a skill record (re-exported from shared types). */
|
|
61
|
+
export const JsonlFileRecordSchema = FileReportSchema;
|
|
62
|
+
/** Unit of work scanned by Warden. New run logs contain only this record type. */
|
|
63
|
+
export const JsonlChunkRecordSchema = z.object({
|
|
64
|
+
schemaVersion: z.literal(1),
|
|
58
65
|
run: JsonlRunMetadataSchema,
|
|
59
66
|
skill: z.string(),
|
|
60
|
-
summary: z.string(),
|
|
61
|
-
findings: z.array(FindingSchema),
|
|
62
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
63
67
|
model: z.string().optional(),
|
|
64
|
-
|
|
68
|
+
chunk: z.object({
|
|
69
|
+
file: z.string(),
|
|
70
|
+
index: z.number().int().positive(),
|
|
71
|
+
total: z.number().int().positive(),
|
|
72
|
+
lineRange: z.string(),
|
|
73
|
+
}),
|
|
74
|
+
status: z.enum(['ok', 'error', 'skipped']),
|
|
75
|
+
findings: z.array(FindingSchema),
|
|
65
76
|
usage: UsageStatsSchema.optional(),
|
|
77
|
+
durationMs: z.number().nonnegative(),
|
|
66
78
|
auxiliaryUsage: AuxiliaryUsageMapSchema.optional(),
|
|
67
|
-
|
|
79
|
+
error: SkillErrorSchema.optional(),
|
|
68
80
|
skippedFiles: z.array(SkippedFileSchema).optional(),
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
});
|
|
82
|
+
/**
|
|
83
|
+
* One skill's analysis results. This is the shared SkillReport plus a `run`
|
|
84
|
+
* block of run-wide metadata, so any new SkillReport field is automatically
|
|
85
|
+
* part of the JSONL contract without a parallel schema.
|
|
86
|
+
*/
|
|
87
|
+
export const JsonlRecordSchema = SkillReportSchema.extend({
|
|
88
|
+
run: JsonlRunMetadataSchema,
|
|
89
|
+
});
|
|
90
|
+
/** Normalized output shape — what we emit. */
|
|
91
|
+
const BySeverityOutputSchema = z.object({
|
|
92
|
+
high: z.number().int().nonnegative(),
|
|
93
|
+
medium: z.number().int().nonnegative(),
|
|
94
|
+
low: z.number().int().nonnegative(),
|
|
71
95
|
});
|
|
72
96
|
/**
|
|
73
97
|
* Severity breakdown in the summary record.
|
|
74
|
-
*
|
|
75
|
-
*
|
|
98
|
+
*
|
|
99
|
+
* Parse-time accepts any string keys (legacy logs may emit 5-level severities
|
|
100
|
+
* like 'critical'/'info'); a transform normalizes 'critical' → 'high' and
|
|
101
|
+
* 'info' → 'low' and drops unknown keys. The piped output shape is the
|
|
102
|
+
* strict `{ high, medium, low }` triple we emit going forward, so
|
|
103
|
+
* JSON-Schema derivation describes the output contract (not the lax input).
|
|
76
104
|
*/
|
|
77
|
-
const BySeveritySchema = z
|
|
78
|
-
|
|
105
|
+
const BySeveritySchema = z
|
|
106
|
+
.record(z.string(), z.number().int().nonnegative())
|
|
107
|
+
.transform((obj) => {
|
|
108
|
+
const result = { high: 0, medium: 0, low: 0 };
|
|
79
109
|
for (const [key, value] of Object.entries(obj)) {
|
|
80
110
|
const normalized = key === 'critical' ? 'high' : key === 'info' ? 'low' : key;
|
|
81
111
|
if (normalized === 'high' || normalized === 'medium' || normalized === 'low') {
|
|
82
|
-
result[normalized]
|
|
112
|
+
result[normalized] += value;
|
|
83
113
|
}
|
|
84
114
|
}
|
|
85
115
|
return result;
|
|
86
|
-
})
|
|
116
|
+
})
|
|
117
|
+
.pipe(BySeverityOutputSchema);
|
|
87
118
|
/** Aggregate summary across all skills (always the last JSONL line). */
|
|
88
119
|
export const JsonlSummaryRecordSchema = z.object({
|
|
89
120
|
run: JsonlRunMetadataSchema,
|
|
@@ -93,6 +124,15 @@ export const JsonlSummaryRecordSchema = z.object({
|
|
|
93
124
|
usage: UsageStatsSchema.optional(),
|
|
94
125
|
totalSkippedFiles: z.number().int().nonnegative().optional(),
|
|
95
126
|
auxiliaryUsage: AuxiliaryUsageMapSchema.optional(),
|
|
127
|
+
failedSkills: z.array(z.string()).optional(),
|
|
128
|
+
totalFailedHunks: z.number().int().nonnegative().optional(),
|
|
129
|
+
totalFailedExtractions: z.number().int().nonnegative().optional(),
|
|
130
|
+
/**
|
|
131
|
+
* Top-level run error captured before any skill ran (e.g. auth failure,
|
|
132
|
+
* config load error). Skill-level errors live on the SkillRecord; this
|
|
133
|
+
* is for failures that prevent the per-skill loop from starting.
|
|
134
|
+
*/
|
|
135
|
+
error: SkillErrorSchema.optional(),
|
|
96
136
|
});
|
|
97
137
|
/** Per-evaluation detail for fix evaluation records. */
|
|
98
138
|
export const JsonlFixEvalDetailSchema = z.object({
|
|
@@ -132,59 +172,104 @@ function aggregateUsage(reports) {
|
|
|
132
172
|
}));
|
|
133
173
|
}
|
|
134
174
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* A final summary line is appended at the end.
|
|
175
|
+
* Build a JSONL run metadata block. `durationMs` is a snapshot at write
|
|
176
|
+
* time for skill records, the run total on the trailing summary record.
|
|
138
177
|
*/
|
|
139
|
-
export function
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
model: options?.model,
|
|
149
|
-
headSha: options?.headSha,
|
|
178
|
+
export function buildRunMetadata(options) {
|
|
179
|
+
return {
|
|
180
|
+
timestamp: (options.timestamp ?? new Date()).toISOString(),
|
|
181
|
+
durationMs: options.durationMs,
|
|
182
|
+
cwd: options.cwd ?? process.cwd(),
|
|
183
|
+
runId: options.runId,
|
|
184
|
+
traceId: options.traceId,
|
|
185
|
+
model: options.model,
|
|
186
|
+
headSha: options.headSha,
|
|
150
187
|
};
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
findings: f.findingCount,
|
|
166
|
-
durationMs: f.durationMs,
|
|
167
|
-
usage: f.usage,
|
|
168
|
-
})),
|
|
169
|
-
skippedFiles: report.skippedFiles?.length ? report.skippedFiles : undefined,
|
|
170
|
-
failedHunks: report.failedHunks || undefined,
|
|
171
|
-
failedExtractions: report.failedExtractions || undefined,
|
|
172
|
-
};
|
|
173
|
-
lines.push(JSON.stringify(record));
|
|
174
|
-
}
|
|
188
|
+
}
|
|
189
|
+
/** Build a skill JSONL record, dropping zero-valued optional fields. */
|
|
190
|
+
export function buildSkillJsonlRecord(report, run) {
|
|
191
|
+
const trimmed = {
|
|
192
|
+
...report,
|
|
193
|
+
skippedFiles: report.skippedFiles?.length ? report.skippedFiles : undefined,
|
|
194
|
+
failedHunks: report.failedHunks || undefined,
|
|
195
|
+
failedExtractions: report.failedExtractions || undefined,
|
|
196
|
+
hunkFailures: report.hunkFailures?.length ? report.hunkFailures : undefined,
|
|
197
|
+
};
|
|
198
|
+
return { ...trimmed, run };
|
|
199
|
+
}
|
|
200
|
+
/** Build the aggregate summary JSONL record. */
|
|
201
|
+
export function buildSummaryJsonlRecord(reports, run, error) {
|
|
175
202
|
const allFindings = reports.flatMap((r) => r.findings);
|
|
176
203
|
const totalSkippedFiles = reports.reduce((n, r) => n + (r.skippedFiles?.length ?? 0), 0);
|
|
177
204
|
const totalAuxiliaryUsage = reports.reduce((acc, r) => mergeAuxiliaryUsage(acc, r.auxiliaryUsage), undefined);
|
|
178
|
-
const
|
|
179
|
-
|
|
205
|
+
const failedSkills = reports.filter((r) => r.error).map((r) => r.skill);
|
|
206
|
+
const totalFailedHunks = reports.reduce((n, r) => n + (r.failedHunks ?? 0), 0);
|
|
207
|
+
const totalFailedExtractions = reports.reduce((n, r) => n + (r.failedExtractions ?? 0), 0);
|
|
208
|
+
return {
|
|
209
|
+
run,
|
|
180
210
|
type: 'summary',
|
|
181
211
|
totalFindings: allFindings.length,
|
|
182
212
|
bySeverity: countBySeverity(allFindings),
|
|
183
213
|
usage: aggregateUsage(reports),
|
|
184
214
|
totalSkippedFiles: totalSkippedFiles > 0 ? totalSkippedFiles : undefined,
|
|
185
215
|
auxiliaryUsage: totalAuxiliaryUsage,
|
|
216
|
+
failedSkills: failedSkills.length > 0 ? failedSkills : undefined,
|
|
217
|
+
totalFailedHunks: totalFailedHunks > 0 ? totalFailedHunks : undefined,
|
|
218
|
+
totalFailedExtractions: totalFailedExtractions > 0 ? totalFailedExtractions : undefined,
|
|
219
|
+
error,
|
|
186
220
|
};
|
|
187
|
-
|
|
221
|
+
}
|
|
222
|
+
/** Render a single skill JSONL record as one line including trailing newline. */
|
|
223
|
+
export function renderJsonlSkillLine(report, run) {
|
|
224
|
+
return JSON.stringify(buildSkillJsonlRecord(report, run)) + '\n';
|
|
225
|
+
}
|
|
226
|
+
/** Render the summary JSONL record as one line including trailing newline. */
|
|
227
|
+
export function renderJsonlSummaryLine(reports, run, error) {
|
|
228
|
+
return JSON.stringify(buildSummaryJsonlRecord(reports, run, error)) + '\n';
|
|
229
|
+
}
|
|
230
|
+
/** Render one chunk result record as one JSONL line. */
|
|
231
|
+
export function renderJsonlChunkLine(record) {
|
|
232
|
+
return JSON.stringify(JsonlChunkRecordSchema.parse(record)) + '\n';
|
|
233
|
+
}
|
|
234
|
+
export function renderJsonlChunkRecords(records) {
|
|
235
|
+
return records.map((record) => renderJsonlChunkLine(record)).join('');
|
|
236
|
+
}
|
|
237
|
+
/** Create parent dirs and truncate the file to empty. */
|
|
238
|
+
export function initJsonlFile(outputPath) {
|
|
239
|
+
const resolvedPath = resolve(process.cwd(), outputPath);
|
|
240
|
+
mkdirSync(dirname(resolvedPath), { recursive: true });
|
|
241
|
+
writeFileSync(resolvedPath, '');
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Append a pre-rendered line (must include its trailing newline).
|
|
245
|
+
* This uses one synchronous append call so parallel skill callbacks in this
|
|
246
|
+
* process cannot interleave partial JSON records.
|
|
247
|
+
*/
|
|
248
|
+
export function appendJsonlLine(outputPath, line) {
|
|
249
|
+
const resolvedPath = resolve(process.cwd(), outputPath);
|
|
250
|
+
mkdirSync(dirname(resolvedPath), { recursive: true });
|
|
251
|
+
appendFileSync(resolvedPath, line);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Render skill reports as a JSONL string.
|
|
255
|
+
* Each line contains one skill report with run metadata.
|
|
256
|
+
* A final summary line is appended at the end.
|
|
257
|
+
*/
|
|
258
|
+
export function renderJsonlString(reports, durationMs, options) {
|
|
259
|
+
const runMetadata = buildRunMetadata({
|
|
260
|
+
runId: options?.runId ?? generateRunId(),
|
|
261
|
+
durationMs,
|
|
262
|
+
timestamp: options?.timestamp,
|
|
263
|
+
traceId: options?.traceId,
|
|
264
|
+
model: options?.model,
|
|
265
|
+
headSha: options?.headSha,
|
|
266
|
+
cwd: options?.cwd,
|
|
267
|
+
});
|
|
268
|
+
const lines = [];
|
|
269
|
+
for (const report of reports) {
|
|
270
|
+
lines.push(JSON.stringify(buildSkillJsonlRecord(report, runMetadata)));
|
|
271
|
+
}
|
|
272
|
+
lines.push(JSON.stringify(buildSummaryJsonlRecord(reports, runMetadata, options?.error)));
|
|
188
273
|
return lines.join('\n') + '\n';
|
|
189
274
|
}
|
|
190
275
|
/**
|
|
@@ -210,14 +295,125 @@ export function writeJsonlContent(outputPath, content) {
|
|
|
210
295
|
export function readJsonlLog(logPath) {
|
|
211
296
|
return readFileSync(logPath, 'utf-8');
|
|
212
297
|
}
|
|
298
|
+
function summarizeFindings(skill, findings) {
|
|
299
|
+
if (findings.length === 0)
|
|
300
|
+
return `${skill}: No issues found`;
|
|
301
|
+
const counts = countBySeverity(findings);
|
|
302
|
+
const parts = [
|
|
303
|
+
counts.high ? `${counts.high} high` : undefined,
|
|
304
|
+
counts.medium ? `${counts.medium} medium` : undefined,
|
|
305
|
+
counts.low ? `${counts.low} low` : undefined,
|
|
306
|
+
].filter(Boolean);
|
|
307
|
+
return `${skill}: Found ${findings.length} ${findings.length === 1 ? 'issue' : 'issues'} (${parts.join(', ')})`;
|
|
308
|
+
}
|
|
309
|
+
function addUsage(a, b) {
|
|
310
|
+
if (!a)
|
|
311
|
+
return b;
|
|
312
|
+
if (!b)
|
|
313
|
+
return a;
|
|
314
|
+
return {
|
|
315
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
316
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
317
|
+
cacheReadInputTokens: (a.cacheReadInputTokens ?? 0) + (b.cacheReadInputTokens ?? 0),
|
|
318
|
+
cacheCreationInputTokens: (a.cacheCreationInputTokens ?? 0) + (b.cacheCreationInputTokens ?? 0),
|
|
319
|
+
costUSD: a.costUSD + b.costUSD,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function reportsFromChunks(chunks) {
|
|
323
|
+
const bySkill = new Map();
|
|
324
|
+
for (const chunk of chunks) {
|
|
325
|
+
const records = bySkill.get(chunk.skill) ?? [];
|
|
326
|
+
records.push(chunk);
|
|
327
|
+
bySkill.set(chunk.skill, records);
|
|
328
|
+
}
|
|
329
|
+
const reports = [];
|
|
330
|
+
for (const [skill, records] of bySkill) {
|
|
331
|
+
const reportLevelError = records.find(isReportLevelErrorRecord)?.error;
|
|
332
|
+
const chunkRecords = records.filter((record) => !isReportLevelErrorRecord(record));
|
|
333
|
+
const aggregateRecords = chunkRecords.length > 0 ? chunkRecords : records;
|
|
334
|
+
const findings = aggregateRecords.flatMap((r) => r.findings);
|
|
335
|
+
const usage = aggregateRecords.reduce((acc, r) => addUsage(acc, r.usage), undefined);
|
|
336
|
+
const auxiliaryUsage = aggregateRecords.reduce((acc, r) => mergeAuxiliaryUsage(acc, r.auxiliaryUsage), undefined);
|
|
337
|
+
const filesByName = new Map();
|
|
338
|
+
const hunkFailures = [];
|
|
339
|
+
const skippedFiles = records.flatMap((r) => r.skippedFiles ?? []);
|
|
340
|
+
for (const record of aggregateRecords) {
|
|
341
|
+
const existing = filesByName.get(record.chunk.file);
|
|
342
|
+
if (record.chunk.file) {
|
|
343
|
+
filesByName.set(record.chunk.file, {
|
|
344
|
+
filename: record.chunk.file,
|
|
345
|
+
findings: (existing?.findings ?? 0) + record.findings.length,
|
|
346
|
+
durationMs: (existing?.durationMs ?? 0) + record.durationMs,
|
|
347
|
+
usage: addUsage(existing?.usage, record.usage),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
if (record.status === 'error' && record.error && !isReportLevelErrorRecord(record)) {
|
|
351
|
+
hunkFailures.push({
|
|
352
|
+
type: isExtractionErrorCode(record.error.code) ? 'extraction' : 'analysis',
|
|
353
|
+
filename: record.chunk.file,
|
|
354
|
+
lineRange: record.chunk.lineRange,
|
|
355
|
+
code: record.error.code,
|
|
356
|
+
message: record.error.message,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const failedHunks = chunkRecords.filter((r) => r.status === 'error' && r.error && !isExtractionErrorCode(r.error.code)).length;
|
|
361
|
+
const failedExtractions = chunkRecords.filter((r) => r.status === 'error' && r.error && isExtractionErrorCode(r.error.code)).length;
|
|
362
|
+
const allChunksFailed = chunkRecords.length > 0 &&
|
|
363
|
+
findings.length === 0 &&
|
|
364
|
+
chunkRecords.every((record) => record.status === 'error');
|
|
365
|
+
const report = {
|
|
366
|
+
skill,
|
|
367
|
+
summary: summarizeFindings(skill, findings),
|
|
368
|
+
findings,
|
|
369
|
+
durationMs: aggregateRecords.reduce((sum, r) => sum + r.durationMs, 0),
|
|
370
|
+
usage,
|
|
371
|
+
files: [...filesByName.values()],
|
|
372
|
+
model: aggregateRecords.find((r) => r.model)?.model,
|
|
373
|
+
};
|
|
374
|
+
if (reportLevelError) {
|
|
375
|
+
report.error = reportLevelError;
|
|
376
|
+
}
|
|
377
|
+
else if (allChunksFailed) {
|
|
378
|
+
report.error = {
|
|
379
|
+
code: 'all_hunks_failed',
|
|
380
|
+
message: `All ${chunkRecords.length} ${chunkRecords.length === 1 ? 'chunk' : 'chunks'} failed to analyze.`,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
if (auxiliaryUsage)
|
|
384
|
+
report.auxiliaryUsage = auxiliaryUsage;
|
|
385
|
+
if (failedHunks > 0)
|
|
386
|
+
report.failedHunks = failedHunks;
|
|
387
|
+
if (failedExtractions > 0)
|
|
388
|
+
report.failedExtractions = failedExtractions;
|
|
389
|
+
if (hunkFailures.length > 0)
|
|
390
|
+
report.hunkFailures = hunkFailures;
|
|
391
|
+
if (skippedFiles.length > 0)
|
|
392
|
+
report.skippedFiles = skippedFiles;
|
|
393
|
+
reports.push(report);
|
|
394
|
+
}
|
|
395
|
+
return reports;
|
|
396
|
+
}
|
|
397
|
+
function isReportLevelErrorRecord(record) {
|
|
398
|
+
return record.status === 'error' && record.chunk.file === '' && Boolean(record.error);
|
|
399
|
+
}
|
|
213
400
|
export function parseJsonlReports(content) {
|
|
214
401
|
const lines = content.trim().split('\n').filter((line) => line.trim());
|
|
215
402
|
const reports = [];
|
|
403
|
+
const chunks = [];
|
|
216
404
|
let runMetadata;
|
|
217
405
|
let totalDurationMs = 0;
|
|
218
406
|
for (const line of lines) {
|
|
219
407
|
try {
|
|
220
408
|
const parsed = JSON.parse(line);
|
|
409
|
+
const chunk = JsonlChunkRecordSchema.safeParse(parsed);
|
|
410
|
+
if (chunk.success) {
|
|
411
|
+
chunks.push(chunk.data);
|
|
412
|
+
if (!runMetadata)
|
|
413
|
+
runMetadata = chunk.data.run;
|
|
414
|
+
totalDurationMs = Math.max(totalDurationMs, chunk.data.run.durationMs);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
221
417
|
// Skip summary record (but capture metadata from it)
|
|
222
418
|
if (parsed.type === 'summary') {
|
|
223
419
|
const summary = JsonlSummaryRecordSchema.parse(parsed);
|
|
@@ -225,99 +421,141 @@ export function parseJsonlReports(content) {
|
|
|
225
421
|
totalDurationMs = summary.run.durationMs;
|
|
226
422
|
continue;
|
|
227
423
|
}
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
usage: record.usage,
|
|
238
|
-
auxiliaryUsage: record.auxiliaryUsage,
|
|
239
|
-
skippedFiles: record.skippedFiles,
|
|
240
|
-
failedHunks: record.failedHunks,
|
|
241
|
-
failedExtractions: record.failedExtractions,
|
|
242
|
-
files: record.files?.map((f) => ({
|
|
243
|
-
filename: f.filename,
|
|
244
|
-
findingCount: f.findings,
|
|
245
|
-
durationMs: f.durationMs,
|
|
246
|
-
usage: f.usage,
|
|
247
|
-
})),
|
|
248
|
-
});
|
|
424
|
+
// Fix-evaluation records are valid JSONL but not SkillReports; let
|
|
425
|
+
// them pass through silently so we don't warn on every line of a log
|
|
426
|
+
// that contains them.
|
|
427
|
+
if (parsed.type === 'fix-evaluation')
|
|
428
|
+
continue;
|
|
429
|
+
// A JsonlRecord is a SkillReport + { run }. Strip `run` to get the
|
|
430
|
+
// SkillReport without rebuilding it field-by-field.
|
|
431
|
+
const { run, ...report } = JsonlRecordSchema.parse(parsed);
|
|
432
|
+
reports.push(report);
|
|
249
433
|
// Capture run metadata from first record if no summary yet
|
|
250
434
|
if (!runMetadata) {
|
|
251
|
-
runMetadata =
|
|
252
|
-
totalDurationMs =
|
|
435
|
+
runMetadata = run;
|
|
436
|
+
totalDurationMs = run.durationMs;
|
|
253
437
|
}
|
|
254
438
|
}
|
|
255
|
-
catch {
|
|
256
|
-
|
|
439
|
+
catch (err) {
|
|
440
|
+
logger.warn('Skipping malformed JSONL line', {
|
|
441
|
+
error: err instanceof Error ? err.message : String(err),
|
|
442
|
+
});
|
|
257
443
|
}
|
|
258
444
|
}
|
|
259
|
-
return { reports, runMetadata, totalDurationMs };
|
|
445
|
+
return { reports: [...reports, ...reportsFromChunks(chunks)], runMetadata, totalDurationMs };
|
|
260
446
|
}
|
|
261
447
|
/**
|
|
262
|
-
* Parse a JSONL log file
|
|
263
|
-
*
|
|
264
|
-
*
|
|
448
|
+
* Parse a JSONL log file's summary, skill names, and high-level metadata.
|
|
449
|
+
* Returns undefined when the file can't be read or contains no parseable
|
|
450
|
+
* records; in-progress files (valid records but no summary yet) return
|
|
451
|
+
* metadata with `inProgress: true`.
|
|
265
452
|
*/
|
|
266
453
|
export function parseLogMetadata(filePath) {
|
|
454
|
+
let content;
|
|
267
455
|
try {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
456
|
+
content = readFileSync(filePath, 'utf-8');
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
const lines = content.trim().split('\n').filter((l) => l.trim());
|
|
462
|
+
let summary;
|
|
463
|
+
let firstRun;
|
|
464
|
+
const skills = [];
|
|
465
|
+
let model;
|
|
466
|
+
let headSha;
|
|
467
|
+
const uniqueFiles = new Set();
|
|
468
|
+
const chunks = [];
|
|
469
|
+
let recognizedRecords = 0;
|
|
470
|
+
for (const line of lines) {
|
|
471
|
+
try {
|
|
472
|
+
const parsed = JSON.parse(line);
|
|
473
|
+
const chunk = JsonlChunkRecordSchema.safeParse(parsed);
|
|
474
|
+
if (chunk.success) {
|
|
475
|
+
chunks.push(chunk.data);
|
|
476
|
+
recognizedRecords++;
|
|
477
|
+
if (!skills.includes(chunk.data.skill)) {
|
|
478
|
+
skills.push(chunk.data.skill);
|
|
289
479
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
480
|
+
if (!model && chunk.data.model) {
|
|
481
|
+
model = chunk.data.model;
|
|
482
|
+
}
|
|
483
|
+
if (!model && chunk.data.run.model) {
|
|
484
|
+
model = chunk.data.run.model;
|
|
485
|
+
}
|
|
486
|
+
if (!headSha && chunk.data.run.headSha) {
|
|
487
|
+
headSha = chunk.data.run.headSha;
|
|
488
|
+
}
|
|
489
|
+
if (!firstRun)
|
|
490
|
+
firstRun = chunk.data.run;
|
|
491
|
+
if (chunk.data.chunk.file) {
|
|
492
|
+
uniqueFiles.add(chunk.data.chunk.file);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
else if (parsed.type === 'summary') {
|
|
496
|
+
summary = JsonlSummaryRecordSchema.parse(parsed);
|
|
497
|
+
recognizedRecords++;
|
|
498
|
+
if (!model && parsed.run?.model && typeof parsed.run.model === 'string') {
|
|
499
|
+
model = parsed.run.model;
|
|
500
|
+
}
|
|
501
|
+
if (!headSha && parsed.run?.headSha && typeof parsed.run.headSha === 'string') {
|
|
502
|
+
headSha = parsed.run.headSha;
|
|
503
|
+
}
|
|
504
|
+
if (!firstRun)
|
|
505
|
+
firstRun = summary.run;
|
|
506
|
+
}
|
|
507
|
+
else if (parsed.skill && typeof parsed.skill === 'string') {
|
|
508
|
+
recognizedRecords++;
|
|
509
|
+
if (!skills.includes(parsed.skill)) {
|
|
510
|
+
skills.push(parsed.skill);
|
|
511
|
+
}
|
|
512
|
+
if (!model && parsed.run?.model && typeof parsed.run.model === 'string') {
|
|
513
|
+
model = parsed.run.model;
|
|
514
|
+
}
|
|
515
|
+
if (!headSha && parsed.run?.headSha && typeof parsed.run.headSha === 'string') {
|
|
516
|
+
headSha = parsed.run.headSha;
|
|
517
|
+
}
|
|
518
|
+
if (!firstRun && parsed.run) {
|
|
519
|
+
const runResult = JsonlRunMetadataSchema.safeParse(parsed.run);
|
|
520
|
+
if (runResult.success)
|
|
521
|
+
firstRun = runResult.data;
|
|
522
|
+
}
|
|
523
|
+
if (Array.isArray(parsed.files)) {
|
|
524
|
+
for (const f of parsed.files) {
|
|
525
|
+
if (f && typeof f.filename === 'string') {
|
|
526
|
+
uniqueFiles.add(f.filename);
|
|
307
527
|
}
|
|
308
528
|
}
|
|
309
529
|
}
|
|
310
530
|
}
|
|
311
|
-
catch {
|
|
312
|
-
// Skip unparseable lines
|
|
313
|
-
}
|
|
314
531
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
532
|
+
catch (err) {
|
|
533
|
+
logger.warn('Skipping malformed JSONL line', {
|
|
534
|
+
error: err instanceof Error ? err.message : String(err),
|
|
535
|
+
});
|
|
536
|
+
}
|
|
318
537
|
}
|
|
319
|
-
|
|
538
|
+
// Empty or fully corrupt files (no parseable records) surface as
|
|
539
|
+
// "parse error" in the list, not as in-progress runs.
|
|
540
|
+
if (recognizedRecords === 0 && lines.length > 0)
|
|
320
541
|
return undefined;
|
|
542
|
+
if (!summary && chunks.length > 0) {
|
|
543
|
+
const reports = reportsFromChunks(chunks);
|
|
544
|
+
const lastDuration = chunks.reduce((max, chunk) => Math.max(max, chunk.run.durationMs), 0);
|
|
545
|
+
const firstChunk = chunks[0];
|
|
546
|
+
if (!firstChunk)
|
|
547
|
+
return undefined;
|
|
548
|
+
const run = { ...(firstRun ?? firstChunk.run), durationMs: lastDuration };
|
|
549
|
+
summary = buildSummaryJsonlRecord(reports, run);
|
|
321
550
|
}
|
|
551
|
+
return {
|
|
552
|
+
summary,
|
|
553
|
+
inProgress: chunks.length > 0 ? !existsSync(`${filePath}.done`) : !summary && !existsSync(`${filePath}.done`),
|
|
554
|
+
runMetadata: summary?.run ?? firstRun,
|
|
555
|
+
skills,
|
|
556
|
+
model,
|
|
557
|
+
headSha,
|
|
558
|
+
totalFiles: uniqueFiles.size,
|
|
559
|
+
};
|
|
322
560
|
}
|
|
323
561
|
//# sourceMappingURL=jsonl.js.map
|