@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +353 -0
  2. package/README.md +3 -3
  3. package/dist/cli/args.d.ts +7 -5
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +31 -17
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/commands/runs.d.ts +26 -0
  8. package/dist/cli/commands/runs.d.ts.map +1 -0
  9. package/dist/cli/commands/runs.js +726 -0
  10. package/dist/cli/commands/runs.js.map +1 -0
  11. package/dist/cli/log-cleanup.d.ts.map +1 -1
  12. package/dist/cli/log-cleanup.js +4 -0
  13. package/dist/cli/log-cleanup.js.map +1 -1
  14. package/dist/cli/main.d.ts +15 -0
  15. package/dist/cli/main.d.ts.map +1 -1
  16. package/dist/cli/main.js +428 -128
  17. package/dist/cli/main.js.map +1 -1
  18. package/dist/cli/output/index.d.ts +1 -1
  19. package/dist/cli/output/index.d.ts.map +1 -1
  20. package/dist/cli/output/index.js +1 -1
  21. package/dist/cli/output/index.js.map +1 -1
  22. package/dist/cli/output/ink-runner.d.ts.map +1 -1
  23. package/dist/cli/output/ink-runner.js +50 -14
  24. package/dist/cli/output/ink-runner.js.map +1 -1
  25. package/dist/cli/output/jsonl-schema-gen.d.ts +16 -0
  26. package/dist/cli/output/jsonl-schema-gen.d.ts.map +1 -0
  27. package/dist/cli/output/jsonl-schema-gen.js +65 -0
  28. package/dist/cli/output/jsonl-schema-gen.js.map +1 -0
  29. package/dist/cli/output/jsonl.d.ts +266 -26
  30. package/dist/cli/output/jsonl.d.ts.map +1 -1
  31. package/dist/cli/output/jsonl.js +377 -139
  32. package/dist/cli/output/jsonl.js.map +1 -1
  33. package/dist/cli/output/tasks.d.ts +6 -1
  34. package/dist/cli/output/tasks.d.ts.map +1 -1
  35. package/dist/cli/output/tasks.js +183 -27
  36. package/dist/cli/output/tasks.js.map +1 -1
  37. package/dist/cli/terminal.d.ts.map +1 -1
  38. package/dist/cli/terminal.js +20 -0
  39. package/dist/cli/terminal.js.map +1 -1
  40. package/dist/index.d.ts +2 -2
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +2 -0
  43. package/dist/index.js.map +1 -1
  44. package/dist/sdk/analyze.d.ts.map +1 -1
  45. package/dist/sdk/analyze.js +103 -10
  46. package/dist/sdk/analyze.js.map +1 -1
  47. package/dist/sdk/errors.d.ts +11 -0
  48. package/dist/sdk/errors.d.ts.map +1 -1
  49. package/dist/sdk/errors.js +50 -0
  50. package/dist/sdk/errors.js.map +1 -1
  51. package/dist/sdk/runner.d.ts +1 -1
  52. package/dist/sdk/runner.d.ts.map +1 -1
  53. package/dist/sdk/types.d.ts +28 -1
  54. package/dist/sdk/types.d.ts.map +1 -1
  55. package/dist/types/index.d.ts +130 -6
  56. package/dist/types/index.d.ts.map +1 -1
  57. package/dist/types/index.js +72 -3
  58. package/dist/types/index.js.map +1 -1
  59. package/package.json +2 -1
  60. package/skills/warden/SKILL.md +5 -5
  61. package/skills/warden/SOURCES.md +39 -0
  62. package/skills/warden/references/cli-reference.md +11 -4
  63. package/skills/warden/references/config-schema.md +9 -0
  64. package/skills/warden/references/configuration.md +9 -0
  65. package/skills/warden-sweep/SKILL.md +43 -365
  66. package/skills/warden-sweep/SOURCES.md +43 -0
  67. package/skills/warden-sweep/references/issue-phase.md +24 -0
  68. package/skills/warden-sweep/references/organize-phase.md +31 -0
  69. package/skills/warden-sweep/references/patch-phase.md +142 -0
  70. package/skills/warden-sweep/references/resume-and-artifacts.md +51 -0
  71. package/skills/warden-sweep/references/scan-phase.md +46 -0
  72. package/skills/warden-sweep/references/script-interfaces.md +127 -0
  73. package/skills/warden-sweep/references/verify-phase.md +69 -0
  74. package/dist/cli/commands/logs.d.ts +0 -19
  75. package/dist/cli/commands/logs.d.ts.map +0 -1
  76. package/dist/cli/commands/logs.js +0 -412
  77. package/dist/cli/commands/logs.js.map +0 -1
@@ -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, FindingSchema, SkippedFileSchema, AuxiliaryUsageMapSchema, FixStatusSchema, } from '../../types/index.js';
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 = z.object({
51
- filename: z.string(),
52
- findings: z.number().int().nonnegative(),
53
- durationMs: z.number().nonnegative().optional(),
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
- durationMs: z.number().nonnegative().optional(),
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
- files: z.array(JsonlFileRecordSchema).optional(),
79
+ error: SkillErrorSchema.optional(),
68
80
  skippedFiles: z.array(SkippedFileSchema).optional(),
69
- failedHunks: z.number().int().nonnegative().optional(),
70
- failedExtractions: z.number().int().nonnegative().optional(),
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
- * Uses a transform to normalize legacy keys ('critical' → 'high', 'info' → 'low')
75
- * so old JSONL logs parse correctly. Accepts any string keys to handle old 5-level logs.
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.record(z.string(), z.number().int().nonnegative()).transform((obj) => {
78
- const result = {};
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] = (result[normalized] ?? 0) + value;
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
- * Render skill reports as a JSONL string.
136
- * Each line contains one skill report with run metadata.
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 renderJsonlString(reports, durationMs, options) {
140
- const timestamp = (options?.timestamp ?? new Date()).toISOString();
141
- const cwd = options?.cwd ?? process.cwd();
142
- const runMetadata = {
143
- timestamp,
144
- durationMs,
145
- cwd,
146
- runId: options?.runId ?? generateRunId(),
147
- traceId: options?.traceId,
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
- const lines = [];
152
- for (const report of reports) {
153
- const record = {
154
- run: runMetadata,
155
- skill: report.skill,
156
- summary: report.summary,
157
- findings: report.findings,
158
- metadata: report.metadata,
159
- model: report.model,
160
- durationMs: report.durationMs,
161
- usage: report.usage,
162
- auxiliaryUsage: report.auxiliaryUsage,
163
- files: report.files?.map((f) => ({
164
- filename: f.filename,
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 summaryRecord = {
179
- run: runMetadata,
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
- lines.push(JSON.stringify(summaryRecord));
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
- // Parse skill record and convert to SkillReport
229
- const record = JsonlRecordSchema.parse(parsed);
230
- reports.push({
231
- skill: record.skill,
232
- summary: record.summary,
233
- findings: record.findings,
234
- metadata: record.metadata,
235
- model: record.model,
236
- durationMs: record.durationMs,
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 = record.run;
252
- totalDurationMs = record.run.durationMs;
435
+ runMetadata = run;
436
+ totalDurationMs = run.durationMs;
253
437
  }
254
438
  }
255
- catch {
256
- // Skip invalid lines
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 for its summary and skill names.
263
- * Reads all lines but only fully parses the summary; extracts skill names
264
- * from non-summary lines with minimal parsing.
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
- const content = readFileSync(filePath, 'utf-8');
269
- const lines = content.trim().split('\n');
270
- let summary;
271
- const skills = [];
272
- let model;
273
- let headSha;
274
- const uniqueFiles = new Set();
275
- for (const line of lines) {
276
- if (!line.trim())
277
- continue;
278
- try {
279
- const parsed = JSON.parse(line);
280
- if (parsed.type === 'summary') {
281
- summary = JsonlSummaryRecordSchema.parse(parsed);
282
- // Fall back to summary's run metadata for model/headSha (empty runs have no skill records)
283
- if (!model && parsed.run?.model && typeof parsed.run.model === 'string') {
284
- model = parsed.run.model;
285
- }
286
- if (!headSha && parsed.run?.headSha && typeof parsed.run.headSha === 'string') {
287
- headSha = parsed.run.headSha;
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
- else if (parsed.skill && typeof parsed.skill === 'string') {
291
- if (!skills.includes(parsed.skill)) {
292
- skills.push(parsed.skill);
293
- }
294
- // Extract model and headSha from first record's run metadata
295
- if (!model && parsed.run?.model && typeof parsed.run.model === 'string') {
296
- model = parsed.run.model;
297
- }
298
- if (!headSha && parsed.run?.headSha && typeof parsed.run.headSha === 'string') {
299
- headSha = parsed.run.headSha;
300
- }
301
- // Count unique filenames across skill records' files arrays
302
- if (Array.isArray(parsed.files)) {
303
- for (const f of parsed.files) {
304
- if (f && typeof f.filename === 'string') {
305
- uniqueFiles.add(f.filename);
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
- if (!summary)
316
- return undefined;
317
- return { summary, skills, model, headSha, totalFiles: uniqueFiles.size };
532
+ catch (err) {
533
+ logger.warn('Skipping malformed JSONL line', {
534
+ error: err instanceof Error ? err.message : String(err),
535
+ });
536
+ }
318
537
  }
319
- catch {
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