@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/main.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { config as dotenvConfig } from 'dotenv';
|
|
4
4
|
import { Sentry, flushSentry, setGlobalAttributes, emitRunMetric, getTraceId } from '../sentry.js';
|
|
5
5
|
import { loadWardenConfig, resolveSkillConfigs } from '../config/loader.js';
|
|
6
6
|
import { verifyAuth } from '../sdk/runner.js';
|
|
7
|
+
import { mapExtractionErrorCode } from '../sdk/errors.js';
|
|
8
|
+
import { mergeAuxiliaryUsage } from '../sdk/usage.js';
|
|
7
9
|
import { resolveSkillAsync } from '../skills/loader.js';
|
|
8
10
|
import { matchTrigger, filterContextByPaths, shouldFail, countFindingsAtOrAbove } from '../triggers/matcher.js';
|
|
9
11
|
import { filterFindings } from '../types/index.js';
|
|
@@ -12,7 +14,7 @@ import { parseCliArgs, showHelp, showVersion, classifyTargets } from './args.js'
|
|
|
12
14
|
import { buildLocalEventContext, buildFileEventContext } from './context.js';
|
|
13
15
|
import { getRepoRoot, getHeadSha, refExists, hasUncommittedChanges } from './git.js';
|
|
14
16
|
import { renderTerminalReport, filterReports } from './terminal.js';
|
|
15
|
-
import { Reporter, detectOutputMode, parseVerbosity, Verbosity, runSkillTasks, runSkillTasksWithInk, pluralize, MODEL_DEFAULT_SENTINEL, writeJsonlContent, renderJsonlString, getRepoLogPath, generateRunId, } from './output/index.js';
|
|
17
|
+
import { Reporter, detectOutputMode, parseVerbosity, Verbosity, runSkillTasks, runSkillTasksWithInk, pluralize, MODEL_DEFAULT_SENTINEL, writeJsonlContent, renderJsonlString, renderJsonlChunkLine, renderJsonlChunkRecords, initJsonlFile, appendJsonlLine, getRepoLogPath, generateRunId, } from './output/index.js';
|
|
16
18
|
import { cleanupArtifacts } from './log-cleanup.js';
|
|
17
19
|
import { collectFixableFindings, applyAllFixes, runInteractiveFixFlow, renderFixSummary, } from './fix.js';
|
|
18
20
|
import { UserAbortError } from './input.js';
|
|
@@ -20,7 +22,7 @@ import { runInit } from './commands/init.js';
|
|
|
20
22
|
import { runAdd } from './commands/add.js';
|
|
21
23
|
import { runSetupApp } from './commands/setup-app.js';
|
|
22
24
|
import { runSync } from './commands/sync.js';
|
|
23
|
-
import {
|
|
25
|
+
import { runRuns } from './commands/runs.js';
|
|
24
26
|
/**
|
|
25
27
|
* Global abort controller for graceful shutdown on SIGINT.
|
|
26
28
|
* Used to cancel in-progress SDK queries.
|
|
@@ -69,15 +71,18 @@ function resolveConfigPath(options, repoPath) {
|
|
|
69
71
|
return options.config ? resolve(cwd, options.config) : resolve(repoPath, 'warden.toml');
|
|
70
72
|
}
|
|
71
73
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
74
|
+
* Emit a minimal JSONL log (summary-only, 0 findings) for early-exit paths.
|
|
75
|
+
*
|
|
76
|
+
* Behavior:
|
|
77
|
+
* - `--json`: writes to stdout (the API contract for piping consumers).
|
|
78
|
+
* - `--output <path>`: writes to that explicit path (CI artifacts).
|
|
79
|
+
* - Default `.warden/logs/`: written only when `error` is set. Real
|
|
80
|
+
* failures (auth, config load, etc.) belong in the on-disk audit trail;
|
|
81
|
+
* pure no-ops (no files, no skills) would just clutter it.
|
|
76
82
|
*/
|
|
77
|
-
function
|
|
83
|
+
function emitEmptyRunLog(repoPath, options, error) {
|
|
78
84
|
const runId = generateRunId();
|
|
79
85
|
const timestamp = new Date();
|
|
80
|
-
const logPath = getRepoLogPath(repoPath, runId, timestamp);
|
|
81
86
|
let headSha;
|
|
82
87
|
try {
|
|
83
88
|
headSha = getHeadSha(repoPath);
|
|
@@ -85,43 +90,319 @@ function writeEmptyRunLog(repoPath, opts) {
|
|
|
85
90
|
catch {
|
|
86
91
|
// Not in a git repo or HEAD is unborn
|
|
87
92
|
}
|
|
88
|
-
const content = renderJsonlString([], 0, {
|
|
93
|
+
const content = renderJsonlString([], 0, {
|
|
94
|
+
runId,
|
|
95
|
+
traceId: getTraceId(),
|
|
96
|
+
timestamp,
|
|
97
|
+
headSha,
|
|
98
|
+
error,
|
|
99
|
+
});
|
|
100
|
+
if (error) {
|
|
101
|
+
const logPath = getRepoLogPath(repoPath, runId, timestamp);
|
|
102
|
+
try {
|
|
103
|
+
writeJsonlContent(logPath, content);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.warn(`Warning: Failed to write run log: ${err instanceof Error ? err.message : String(err)}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (options.output) {
|
|
110
|
+
try {
|
|
111
|
+
writeJsonlContent(options.output, content);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
console.warn(`Warning: Failed to write output file: ${err instanceof Error ? err.message : String(err)}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (options.json) {
|
|
118
|
+
process.stdout.write(content);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function initializeRunLog(args) {
|
|
122
|
+
const { repoPath, runId, timestamp, traceId, headSha, model, outputPath, reporter, startTime } = args;
|
|
123
|
+
const baseRun = {
|
|
124
|
+
timestamp: timestamp.toISOString(),
|
|
125
|
+
cwd: process.cwd(),
|
|
126
|
+
runId,
|
|
127
|
+
traceId,
|
|
128
|
+
model,
|
|
129
|
+
headSha,
|
|
130
|
+
};
|
|
131
|
+
const primaryLogPath = getRepoLogPath(repoPath, runId, timestamp);
|
|
132
|
+
let primaryLogWritten = false;
|
|
89
133
|
try {
|
|
90
|
-
|
|
134
|
+
initJsonlFile(primaryLogPath);
|
|
135
|
+
primaryLogWritten = true;
|
|
91
136
|
}
|
|
92
137
|
catch (err) {
|
|
93
|
-
|
|
138
|
+
reporter.warning(`Failed to write run log: ${err instanceof Error ? err.message : String(err)}`);
|
|
94
139
|
}
|
|
95
|
-
|
|
140
|
+
let resolvedOutputPath;
|
|
141
|
+
if (outputPath) {
|
|
96
142
|
try {
|
|
97
|
-
|
|
143
|
+
initJsonlFile(outputPath);
|
|
144
|
+
resolvedOutputPath = outputPath;
|
|
98
145
|
}
|
|
99
146
|
catch (err) {
|
|
100
|
-
|
|
147
|
+
reporter.warning(`Failed to write output file: ${err instanceof Error ? err.message : String(err)}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const paths = [];
|
|
151
|
+
const seenPaths = new Set();
|
|
152
|
+
const addPath = (path) => {
|
|
153
|
+
const key = resolve(process.cwd(), path);
|
|
154
|
+
if (seenPaths.has(key))
|
|
155
|
+
return;
|
|
156
|
+
seenPaths.add(key);
|
|
157
|
+
paths.push(path);
|
|
158
|
+
};
|
|
159
|
+
if (primaryLogWritten)
|
|
160
|
+
addPath(primaryLogPath);
|
|
161
|
+
if (resolvedOutputPath)
|
|
162
|
+
addPath(resolvedOutputPath);
|
|
163
|
+
return { paths, primaryLogPath, primaryLogWritten, outputPath: resolvedOutputPath, startTime, baseRun, chunks: [] };
|
|
164
|
+
}
|
|
165
|
+
function appendChunkToRunLog(log, skillName, chunk) {
|
|
166
|
+
if (log.paths.length === 0)
|
|
167
|
+
return;
|
|
168
|
+
const auxiliaryUsage = chunk.auxiliaryUsage?.reduce((acc, entry) => mergeAuxiliaryUsage(acc, { [entry.agent]: entry.usage }), undefined);
|
|
169
|
+
const error = chunk.failed
|
|
170
|
+
? {
|
|
171
|
+
code: chunk.failureCode ?? 'unknown',
|
|
172
|
+
message: chunk.failureMessage ?? 'unknown error',
|
|
173
|
+
timestamp: new Date().toISOString(),
|
|
174
|
+
}
|
|
175
|
+
: chunk.extractionFailed
|
|
176
|
+
? {
|
|
177
|
+
code: mapExtractionErrorCode(chunk.extractionError),
|
|
178
|
+
message: chunk.extractionError ?? 'unknown extraction error',
|
|
179
|
+
timestamp: new Date().toISOString(),
|
|
180
|
+
}
|
|
181
|
+
: undefined;
|
|
182
|
+
const record = {
|
|
183
|
+
schemaVersion: 1,
|
|
184
|
+
run: { ...log.baseRun, durationMs: Date.now() - log.startTime },
|
|
185
|
+
skill: skillName,
|
|
186
|
+
model: chunk.model,
|
|
187
|
+
chunk: {
|
|
188
|
+
file: chunk.filename,
|
|
189
|
+
index: chunk.index,
|
|
190
|
+
total: chunk.total,
|
|
191
|
+
lineRange: chunk.lineRange,
|
|
192
|
+
},
|
|
193
|
+
status: error ? 'error' : 'ok',
|
|
194
|
+
findings: chunk.findings,
|
|
195
|
+
usage: chunk.usage,
|
|
196
|
+
durationMs: chunk.durationMs,
|
|
197
|
+
auxiliaryUsage,
|
|
198
|
+
error,
|
|
199
|
+
};
|
|
200
|
+
let line;
|
|
201
|
+
try {
|
|
202
|
+
line = renderJsonlChunkLine(record);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
log.chunks.push(record);
|
|
208
|
+
for (const p of log.paths) {
|
|
209
|
+
try {
|
|
210
|
+
appendJsonlLine(p, line);
|
|
211
|
+
}
|
|
212
|
+
catch { /* best-effort */ }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function buildReportChunkRecord(log, report, runDurationMs, index = 1, total = 1, error) {
|
|
216
|
+
const reportError = report.error ?? error;
|
|
217
|
+
return {
|
|
218
|
+
schemaVersion: 1,
|
|
219
|
+
run: { ...log.baseRun, durationMs: runDurationMs },
|
|
220
|
+
skill: report.skill,
|
|
221
|
+
model: report.model,
|
|
222
|
+
chunk: {
|
|
223
|
+
file: report.skippedFiles?.[0]?.filename ?? '',
|
|
224
|
+
index,
|
|
225
|
+
total,
|
|
226
|
+
lineRange: '',
|
|
227
|
+
},
|
|
228
|
+
status: reportError ? 'error' : report.skippedFiles?.length ? 'skipped' : 'ok',
|
|
229
|
+
findings: report.findings,
|
|
230
|
+
usage: report.usage,
|
|
231
|
+
durationMs: report.durationMs ?? runDurationMs,
|
|
232
|
+
auxiliaryUsage: report.auxiliaryUsage,
|
|
233
|
+
error: reportError,
|
|
234
|
+
skippedFiles: report.skippedFiles,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function buildRunErrorChunkRecord(log, runDurationMs, error) {
|
|
238
|
+
return {
|
|
239
|
+
schemaVersion: 1,
|
|
240
|
+
run: { ...log.baseRun, durationMs: runDurationMs },
|
|
241
|
+
skill: 'run',
|
|
242
|
+
chunk: {
|
|
243
|
+
file: '',
|
|
244
|
+
index: 1,
|
|
245
|
+
total: 1,
|
|
246
|
+
lineRange: '',
|
|
247
|
+
},
|
|
248
|
+
status: 'error',
|
|
249
|
+
findings: [],
|
|
250
|
+
durationMs: runDurationMs,
|
|
251
|
+
error,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function hasReportRecord(log, report) {
|
|
255
|
+
return log.chunks.some((chunk) => {
|
|
256
|
+
if (chunk.skill !== report.skill)
|
|
257
|
+
return false;
|
|
258
|
+
if (report.error) {
|
|
259
|
+
return chunk.error?.code === report.error.code && chunk.error.message === report.error.message;
|
|
260
|
+
}
|
|
261
|
+
if (report.skippedFiles?.length) {
|
|
262
|
+
return (chunk.skippedFiles?.length ?? 0) > 0;
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
function shouldStreamReportRecord(log, report) {
|
|
268
|
+
if (hasReportRecord(log, report))
|
|
269
|
+
return false;
|
|
270
|
+
return Boolean(report.error || report.skippedFiles?.length || !log.chunks.some((chunk) => chunk.skill === report.skill));
|
|
271
|
+
}
|
|
272
|
+
function appendReportToRunLog(log, report) {
|
|
273
|
+
if (!shouldStreamReportRecord(log, report))
|
|
274
|
+
return;
|
|
275
|
+
const record = buildReportChunkRecord(log, report, Date.now() - log.startTime);
|
|
276
|
+
let line;
|
|
277
|
+
try {
|
|
278
|
+
line = renderJsonlChunkLine(record);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
log.chunks.push(record);
|
|
284
|
+
for (const p of log.paths) {
|
|
285
|
+
try {
|
|
286
|
+
appendJsonlLine(p, line);
|
|
287
|
+
}
|
|
288
|
+
catch { /* best-effort */ }
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function lineRangeIncludes(lineRange, line) {
|
|
292
|
+
if (!lineRange)
|
|
293
|
+
return false;
|
|
294
|
+
const [startText, endText] = lineRange.split('-');
|
|
295
|
+
const start = Number(startText);
|
|
296
|
+
const end = endText ? Number(endText) : start;
|
|
297
|
+
return Number.isFinite(start) && Number.isFinite(end) && line >= start && line <= end;
|
|
298
|
+
}
|
|
299
|
+
function findChunkForFinding(chunks, skill, finding) {
|
|
300
|
+
const sameSkill = chunks.filter((chunk) => chunk.skill === skill && chunk.chunk.file);
|
|
301
|
+
const location = finding.location;
|
|
302
|
+
if (!location)
|
|
303
|
+
return sameSkill[0];
|
|
304
|
+
return sameSkill.find((chunk) => chunk.chunk.file === location.path && lineRangeIncludes(chunk.chunk.lineRange, location.startLine)) ?? sameSkill.find((chunk) => chunk.chunk.file === location.path) ?? sameSkill[0];
|
|
305
|
+
}
|
|
306
|
+
function buildFinalChunkRecords(log, reports, totalDurationMs, error) {
|
|
307
|
+
const finalRun = { ...log.baseRun, durationMs: totalDurationMs };
|
|
308
|
+
if (log.chunks.length === 0) {
|
|
309
|
+
if (reports.length === 0 && error) {
|
|
310
|
+
return [buildRunErrorChunkRecord(log, totalDurationMs, error)];
|
|
311
|
+
}
|
|
312
|
+
return reports.map((report) => buildReportChunkRecord(log, report, totalDurationMs, undefined, undefined, error));
|
|
313
|
+
}
|
|
314
|
+
const findingsByChunk = new Map();
|
|
315
|
+
for (const report of reports) {
|
|
316
|
+
for (const finding of report.findings) {
|
|
317
|
+
const chunk = findChunkForFinding(log.chunks, report.skill, finding);
|
|
318
|
+
if (!chunk)
|
|
319
|
+
continue;
|
|
320
|
+
const findings = findingsByChunk.get(chunk) ?? [];
|
|
321
|
+
findings.push(finding);
|
|
322
|
+
findingsByChunk.set(chunk, findings);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const chunkRecords = log.chunks.map((chunk) => ({
|
|
326
|
+
...chunk,
|
|
327
|
+
run: { ...finalRun },
|
|
328
|
+
findings: findingsByChunk.get(chunk) ?? [],
|
|
329
|
+
}));
|
|
330
|
+
const finalLog = { ...log, chunks: chunkRecords };
|
|
331
|
+
const missingReports = reports.filter((report) => shouldStreamReportRecord(finalLog, report));
|
|
332
|
+
return [
|
|
333
|
+
...chunkRecords,
|
|
334
|
+
...missingReports.map((report) => buildReportChunkRecord(log, report, totalDurationMs, undefined, undefined, error)),
|
|
335
|
+
];
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Rewrite the run log with final chunk records. Returns the set of paths that
|
|
339
|
+
* accepted the write, so the caller can decide whether to claim
|
|
340
|
+
* "wrote JSONL output to X" (only true when the final log actually landed).
|
|
341
|
+
*/
|
|
342
|
+
function finalizeRunLog(log, reports, totalDurationMs, error) {
|
|
343
|
+
const wrote = new Set();
|
|
344
|
+
if (log.paths.length === 0)
|
|
345
|
+
return wrote;
|
|
346
|
+
let content;
|
|
347
|
+
try {
|
|
348
|
+
const records = buildFinalChunkRecords(log, reports, totalDurationMs, error);
|
|
349
|
+
content = renderJsonlChunkRecords(records);
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return wrote;
|
|
353
|
+
}
|
|
354
|
+
for (const p of log.paths) {
|
|
355
|
+
const targetPath = resolve(process.cwd(), p);
|
|
356
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
|
|
357
|
+
const tempDonePath = `${tempPath}.done`;
|
|
358
|
+
try {
|
|
359
|
+
writeJsonlContent(tempPath, content);
|
|
360
|
+
writeFileSync(tempDonePath, '');
|
|
361
|
+
renameSync(tempPath, targetPath);
|
|
362
|
+
renameSync(tempDonePath, `${targetPath}.done`);
|
|
363
|
+
wrote.add(p);
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
try {
|
|
367
|
+
unlinkSync(tempPath);
|
|
368
|
+
}
|
|
369
|
+
catch { /* ignore */ }
|
|
370
|
+
try {
|
|
371
|
+
unlinkSync(tempDonePath);
|
|
372
|
+
}
|
|
373
|
+
catch { /* ignore */ }
|
|
374
|
+
// best-effort
|
|
101
375
|
}
|
|
102
376
|
}
|
|
103
|
-
return
|
|
377
|
+
return wrote;
|
|
104
378
|
}
|
|
105
379
|
/**
|
|
106
380
|
* Process skill task results into reports and check for failures.
|
|
381
|
+
* Exported for testing; callers inside main.ts use it directly.
|
|
107
382
|
*/
|
|
108
|
-
function processTaskResults(results, reportOn, minConfidence) {
|
|
383
|
+
export function processTaskResults(results, reportOn, minConfidence) {
|
|
109
384
|
const reports = [];
|
|
110
385
|
let hasFailure = false;
|
|
111
386
|
const failureReasons = [];
|
|
112
387
|
for (const result of results) {
|
|
113
|
-
if (result.report)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
388
|
+
if (!result.report)
|
|
389
|
+
continue;
|
|
390
|
+
reports.push(result.report);
|
|
391
|
+
// Skill-level errors always fail the run, independent of failOn thresholds.
|
|
392
|
+
if (result.report.error) {
|
|
393
|
+
hasFailure = true;
|
|
394
|
+
failureReasons.push(`${result.name}: ${result.report.error.code}: ${result.report.error.message}`);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
// Apply confidence filtering before failOn evaluation so low-confidence findings
|
|
398
|
+
// don't cause exit code 1. Per-result minConfidence (from trigger config) takes
|
|
399
|
+
// precedence over the global default.
|
|
400
|
+
const effectiveConfidence = result.minConfidence ?? minConfidence;
|
|
401
|
+
const reportForFail = { ...result.report, findings: filterFindings(result.report.findings, undefined, effectiveConfidence) };
|
|
402
|
+
if (result.failOn && shouldFail(reportForFail, result.failOn)) {
|
|
403
|
+
hasFailure = true;
|
|
404
|
+
const count = countFindingsAtOrAbove(reportForFail, result.failOn);
|
|
405
|
+
failureReasons.push(`${result.name}: ${count} ${result.failOn}+ severity ${pluralize(count, 'issue')}`);
|
|
125
406
|
}
|
|
126
407
|
}
|
|
127
408
|
const filteredReports = filterReports(reports, reportOn, minConfidence);
|
|
@@ -130,39 +411,17 @@ function processTaskResults(results, reportOn, minConfidence) {
|
|
|
130
411
|
/**
|
|
131
412
|
* Output results and handle fixes. Returns exit code.
|
|
132
413
|
*/
|
|
133
|
-
async function outputResultsAndHandleFixes(processed, options, reporter,
|
|
414
|
+
async function outputResultsAndHandleFixes(processed, options, reporter, runLog, totalDuration, failFastAborted) {
|
|
134
415
|
const { reports, filteredReports, hasFailure, failureReasons } = processed;
|
|
135
|
-
const traceId =
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
headSha = getHeadSha(repoPath);
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
// Not in a git repo or HEAD is unborn
|
|
145
|
-
}
|
|
146
|
-
// Render JSONL content once so repo log and --output have identical timestamps
|
|
147
|
-
const jsonlContent = renderJsonlString(reports, totalDuration, { runId, traceId, timestamp, model: resolvedModel, headSha });
|
|
148
|
-
// Always write repo-local JSONL log (non-fatal — don't lose analysis output)
|
|
149
|
-
const logPath = getRepoLogPath(repoPath, runId, timestamp);
|
|
150
|
-
let logWritten = false;
|
|
151
|
-
try {
|
|
152
|
-
writeJsonlContent(logPath, jsonlContent);
|
|
153
|
-
logWritten = true;
|
|
154
|
-
}
|
|
155
|
-
catch (err) {
|
|
156
|
-
reporter.warning(`Failed to write run log: ${err instanceof Error ? err.message : String(err)}`);
|
|
157
|
-
}
|
|
158
|
-
// Write additional copy to --output path if specified
|
|
159
|
-
if (options.output) {
|
|
160
|
-
try {
|
|
161
|
-
writeJsonlContent(options.output, jsonlContent);
|
|
162
|
-
reporter.success(`Wrote JSONL output to ${options.output}`);
|
|
416
|
+
const traceId = runLog.baseRun.traceId;
|
|
417
|
+
const finalizedPaths = finalizeRunLog(runLog, reports, totalDuration);
|
|
418
|
+
// Only claim --output succeeded if the summary actually landed there.
|
|
419
|
+
if (runLog.outputPath) {
|
|
420
|
+
if (finalizedPaths.has(runLog.outputPath)) {
|
|
421
|
+
reporter.success(`Wrote JSONL output to ${runLog.outputPath}`);
|
|
163
422
|
}
|
|
164
|
-
|
|
165
|
-
reporter.warning(`Failed to write output file: ${
|
|
423
|
+
else {
|
|
424
|
+
reporter.warning(`Failed to write output file: ${runLog.outputPath}`);
|
|
166
425
|
}
|
|
167
426
|
}
|
|
168
427
|
// Collect fixable findings early so we know whether to suppress diffs in the report
|
|
@@ -175,14 +434,31 @@ async function outputResultsAndHandleFixes(processed, options, reporter, repoPat
|
|
|
175
434
|
&& reporter.verbosity !== Verbosity.Quiet
|
|
176
435
|
&& reporter.mode.isTTY
|
|
177
436
|
&& process.stdin.isTTY;
|
|
178
|
-
// Output results
|
|
179
437
|
reporter.blank();
|
|
180
438
|
if (options.json) {
|
|
181
|
-
//
|
|
439
|
+
// Prefer reading the on-disk log (per-skill durationMs is a snapshot).
|
|
440
|
+
// Only read it back if finalize actually landed the summary there;
|
|
441
|
+
// a half-written file should fall through to the in-memory render.
|
|
442
|
+
// The fallback renders the same chunk-record shape in memory.
|
|
443
|
+
let jsonlContent;
|
|
444
|
+
if (finalizedPaths.has(runLog.primaryLogPath)) {
|
|
445
|
+
try {
|
|
446
|
+
jsonlContent = readFileSync(runLog.primaryLogPath, 'utf-8');
|
|
447
|
+
}
|
|
448
|
+
catch { /* fall through */ }
|
|
449
|
+
}
|
|
450
|
+
if (!jsonlContent) {
|
|
451
|
+
try {
|
|
452
|
+
jsonlContent = renderJsonlChunkRecords(buildFinalChunkRecords(runLog, reports, totalDuration));
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
reporter.error(`Failed to render JSONL output: ${err instanceof Error ? err.message : String(err)}`);
|
|
456
|
+
return 1;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
182
459
|
process.stdout.write(jsonlContent);
|
|
183
460
|
}
|
|
184
461
|
else {
|
|
185
|
-
// Suppress fix diffs in report when interactive step-through will show them
|
|
186
462
|
console.log(renderTerminalReport(filteredReports, reporter.mode, { suppressFixDiffs: willStepThrough, verbosity: reporter.verbosity }));
|
|
187
463
|
}
|
|
188
464
|
// Show interrupted / fail-fast banner before summary
|
|
@@ -198,8 +474,8 @@ async function outputResultsAndHandleFixes(processed, options, reporter, repoPat
|
|
|
198
474
|
reporter.blank();
|
|
199
475
|
reporter.renderSummary(filteredReports, totalDuration, { traceId });
|
|
200
476
|
// Show log file path after summary (only if write succeeded)
|
|
201
|
-
if (!options.json &&
|
|
202
|
-
reporter.dim(`Log: ${
|
|
477
|
+
if (!options.json && runLog.primaryLogWritten) {
|
|
478
|
+
reporter.dim(`Log: ${runLog.primaryLogPath}`);
|
|
203
479
|
}
|
|
204
480
|
// Handle fixes: --fix (automatic) always runs, interactive step-through in TTY mode
|
|
205
481
|
if (fixableFindings.length > 0) {
|
|
@@ -250,15 +526,13 @@ async function runSkills(context, options, reporter) {
|
|
|
250
526
|
verifyAuth({ apiKey });
|
|
251
527
|
}
|
|
252
528
|
catch (error) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
writeEmptyRunLog(effectiveRepo, { traceId: getTraceId(), outputPath: options.output });
|
|
261
|
-
}
|
|
529
|
+
const message = error.message;
|
|
530
|
+
reporter.error(message);
|
|
531
|
+
emitEmptyRunLog(repoPath ?? cwd, options, {
|
|
532
|
+
code: 'auth_failed',
|
|
533
|
+
message,
|
|
534
|
+
timestamp: new Date().toISOString(),
|
|
535
|
+
});
|
|
262
536
|
return 1;
|
|
263
537
|
}
|
|
264
538
|
// Resolve config path
|
|
@@ -310,13 +584,8 @@ async function runSkills(context, options, reporter) {
|
|
|
310
584
|
emitRunMetric();
|
|
311
585
|
// Handle case where no skills to run
|
|
312
586
|
if (skillsToRun.length === 0) {
|
|
313
|
-
|
|
314
|
-
if (options.json) {
|
|
315
|
-
const { content } = writeEmptyRunLog(effectiveRepo, { traceId: getTraceId(), outputPath: options.output });
|
|
316
|
-
process.stdout.write(content);
|
|
317
|
-
}
|
|
318
|
-
else {
|
|
319
|
-
writeEmptyRunLog(effectiveRepo, { traceId: getTraceId(), outputPath: options.output });
|
|
587
|
+
emitEmptyRunLog(repoPath ?? cwd, options);
|
|
588
|
+
if (!options.json) {
|
|
320
589
|
reporter.warning('No triggers matched for the changed files');
|
|
321
590
|
reporter.tip('Specify a skill explicitly: warden <target> --skill <name>');
|
|
322
591
|
}
|
|
@@ -347,6 +616,29 @@ async function runSkills(context, options, reporter) {
|
|
|
347
616
|
context: filterContextByPaths(context, filters),
|
|
348
617
|
runnerOptions,
|
|
349
618
|
}));
|
|
619
|
+
// Open the run's JSONL log before launching skills so `warden runs
|
|
620
|
+
// follow <runId>` works from a second terminal while the run is live.
|
|
621
|
+
const runId = generateRunId();
|
|
622
|
+
const timestamp = new Date();
|
|
623
|
+
const traceId = getTraceId();
|
|
624
|
+
let headSha;
|
|
625
|
+
try {
|
|
626
|
+
headSha = getHeadSha(repoPath ?? cwd);
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
// Not a git repo or HEAD is unborn — non-fatal
|
|
630
|
+
}
|
|
631
|
+
const runLog = initializeRunLog({
|
|
632
|
+
repoPath: repoPath ?? cwd,
|
|
633
|
+
runId,
|
|
634
|
+
timestamp,
|
|
635
|
+
traceId,
|
|
636
|
+
headSha,
|
|
637
|
+
model: logModel,
|
|
638
|
+
outputPath: options.output,
|
|
639
|
+
reporter,
|
|
640
|
+
startTime,
|
|
641
|
+
});
|
|
350
642
|
// Run skills with Ink UI (TTY) or simple console output (non-TTY)
|
|
351
643
|
const concurrency = options.parallel ?? DEFAULT_CONCURRENCY;
|
|
352
644
|
failFastController = options.failFast ? new AbortController() : undefined;
|
|
@@ -355,6 +647,8 @@ async function runSkills(context, options, reporter) {
|
|
|
355
647
|
verbosity: reporter.verbosity,
|
|
356
648
|
concurrency,
|
|
357
649
|
failFastController,
|
|
650
|
+
onChunkComplete: (skillName, chunk) => appendChunkToRunLog(runLog, skillName, chunk),
|
|
651
|
+
onSkillComplete: (report) => appendReportToRunLog(runLog, report),
|
|
358
652
|
};
|
|
359
653
|
const results = reporter.mode.isTTY
|
|
360
654
|
? await runSkillTasksWithInk(tasks, taskOptions)
|
|
@@ -363,7 +657,7 @@ async function runSkills(context, options, reporter) {
|
|
|
363
657
|
const totalDuration = Date.now() - startTime;
|
|
364
658
|
const effectiveMinConfidence = options.minConfidence ?? config?.defaults?.minConfidence ?? 'medium';
|
|
365
659
|
const processed = processTaskResults(results, options.reportOn, effectiveMinConfidence);
|
|
366
|
-
return outputResultsAndHandleFixes(processed, options, reporter,
|
|
660
|
+
return outputResultsAndHandleFixes(processed, options, reporter, runLog, totalDuration, failFastController?.signal.aborted);
|
|
367
661
|
}
|
|
368
662
|
/**
|
|
369
663
|
* Run in file mode: analyze specific files.
|
|
@@ -382,12 +676,8 @@ async function runFileMode(filePatterns, options, reporter) {
|
|
|
382
676
|
return 1;
|
|
383
677
|
}
|
|
384
678
|
if (pullRequest.files.length === 0) {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
process.stdout.write(content);
|
|
388
|
-
}
|
|
389
|
-
else {
|
|
390
|
-
writeEmptyRunLog(cwd, { traceId: getTraceId(), outputPath: options.output });
|
|
679
|
+
emitEmptyRunLog(cwd, options);
|
|
680
|
+
if (!options.json) {
|
|
391
681
|
reporter.blank();
|
|
392
682
|
reporter.warning('No files matched the given patterns');
|
|
393
683
|
}
|
|
@@ -456,12 +746,8 @@ async function runGitRefMode(gitRef, options, reporter) {
|
|
|
456
746
|
return 1;
|
|
457
747
|
}
|
|
458
748
|
if (pullRequest.files.length === 0) {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
process.stdout.write(content);
|
|
462
|
-
}
|
|
463
|
-
else {
|
|
464
|
-
writeEmptyRunLog(repoPath, { traceId: getTraceId(), outputPath: options.output });
|
|
749
|
+
emitEmptyRunLog(repoPath, options);
|
|
750
|
+
if (!options.json) {
|
|
465
751
|
reporter.renderEmptyState('No changes found');
|
|
466
752
|
reporter.blank();
|
|
467
753
|
}
|
|
@@ -509,12 +795,8 @@ async function runConfigMode(options, reporter) {
|
|
|
509
795
|
return 1;
|
|
510
796
|
}
|
|
511
797
|
if (pullRequest.files.length === 0) {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
process.stdout.write(content);
|
|
515
|
-
}
|
|
516
|
-
else {
|
|
517
|
-
writeEmptyRunLog(repoPath, { traceId: getTraceId(), outputPath: options.output });
|
|
798
|
+
emitEmptyRunLog(repoPath, options);
|
|
799
|
+
if (!options.json) {
|
|
518
800
|
if (options.staged) {
|
|
519
801
|
reporter.renderEmptyState('No staged changes found');
|
|
520
802
|
}
|
|
@@ -549,12 +831,8 @@ async function runConfigMode(options, reporter) {
|
|
|
549
831
|
}
|
|
550
832
|
const triggersToRun = [...seen.values()];
|
|
551
833
|
if (triggersToRun.length === 0) {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
process.stdout.write(content);
|
|
555
|
-
}
|
|
556
|
-
else {
|
|
557
|
-
writeEmptyRunLog(repoPath, { traceId: getTraceId(), outputPath: options.output });
|
|
834
|
+
emitEmptyRunLog(repoPath, options);
|
|
835
|
+
if (!options.json) {
|
|
558
836
|
reporter.blank();
|
|
559
837
|
if (options.skill) {
|
|
560
838
|
reporter.warning(`No triggers matched for skill: ${options.skill}`);
|
|
@@ -577,14 +855,13 @@ async function runConfigMode(options, reporter) {
|
|
|
577
855
|
verifyAuth({ apiKey });
|
|
578
856
|
}
|
|
579
857
|
catch (error) {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}
|
|
858
|
+
const message = error.message;
|
|
859
|
+
reporter.error(message);
|
|
860
|
+
emitEmptyRunLog(repoPath, options, {
|
|
861
|
+
code: 'auth_failed',
|
|
862
|
+
message,
|
|
863
|
+
timestamp: new Date().toISOString(),
|
|
864
|
+
});
|
|
588
865
|
return 1;
|
|
589
866
|
}
|
|
590
867
|
// Build trigger tasks
|
|
@@ -608,6 +885,33 @@ async function runConfigMode(options, reporter) {
|
|
|
608
885
|
auxiliaryMaxRetries: config.defaults?.auxiliaryMaxRetries,
|
|
609
886
|
},
|
|
610
887
|
}));
|
|
888
|
+
// Initialize the run's JSONL log up front so a second terminal can
|
|
889
|
+
// `warden runs follow <runId>` while skills are still running.
|
|
890
|
+
// Skill records are appended on each completion; the trailing summary
|
|
891
|
+
// is appended in `outputResultsAndHandleFixes`.
|
|
892
|
+
// Run-level model is the default (ignoring per-trigger overrides); per-skill models are on each report.
|
|
893
|
+
const defaultModel = config.defaults?.model ?? options.model ?? process.env['WARDEN_MODEL'] ?? MODEL_DEFAULT_SENTINEL;
|
|
894
|
+
const runId = generateRunId();
|
|
895
|
+
const timestamp = new Date();
|
|
896
|
+
const traceId = getTraceId();
|
|
897
|
+
let headSha;
|
|
898
|
+
try {
|
|
899
|
+
headSha = getHeadSha(repoPath);
|
|
900
|
+
}
|
|
901
|
+
catch {
|
|
902
|
+
// Not a git repo or HEAD is unborn — non-fatal
|
|
903
|
+
}
|
|
904
|
+
const runLog = initializeRunLog({
|
|
905
|
+
repoPath,
|
|
906
|
+
runId,
|
|
907
|
+
timestamp,
|
|
908
|
+
traceId,
|
|
909
|
+
headSha,
|
|
910
|
+
model: defaultModel,
|
|
911
|
+
outputPath: options.output,
|
|
912
|
+
reporter,
|
|
913
|
+
startTime,
|
|
914
|
+
});
|
|
611
915
|
// Run triggers with Ink UI (TTY) or simple console output (non-TTY)
|
|
612
916
|
const concurrency = options.parallel ?? config.runner?.concurrency ?? DEFAULT_CONCURRENCY;
|
|
613
917
|
failFastController = options.failFast ? new AbortController() : undefined;
|
|
@@ -616,6 +920,8 @@ async function runConfigMode(options, reporter) {
|
|
|
616
920
|
verbosity: reporter.verbosity,
|
|
617
921
|
concurrency,
|
|
618
922
|
failFastController,
|
|
923
|
+
onChunkComplete: (skillName, chunk) => appendChunkToRunLog(runLog, skillName, chunk),
|
|
924
|
+
onSkillComplete: (report) => appendReportToRunLog(runLog, report),
|
|
619
925
|
};
|
|
620
926
|
const results = reporter.mode.isTTY
|
|
621
927
|
? await runSkillTasksWithInk(tasks, taskOptions)
|
|
@@ -623,9 +929,7 @@ async function runConfigMode(options, reporter) {
|
|
|
623
929
|
// Process results and output
|
|
624
930
|
const totalDuration = Date.now() - startTime;
|
|
625
931
|
const processed = processTaskResults(results, options.reportOn, effectiveMinConfidence);
|
|
626
|
-
|
|
627
|
-
const defaultModel = config.defaults?.model ?? options.model ?? process.env['WARDEN_MODEL'] ?? MODEL_DEFAULT_SENTINEL;
|
|
628
|
-
return outputResultsAndHandleFixes(processed, options, reporter, repoPath, totalDuration, failFastController?.signal.aborted, defaultModel);
|
|
932
|
+
return outputResultsAndHandleFixes(processed, options, reporter, runLog, totalDuration, failFastController?.signal.aborted);
|
|
629
933
|
}
|
|
630
934
|
/**
|
|
631
935
|
* Run in direct skill mode: run a specific skill on uncommitted changes.
|
|
@@ -660,12 +964,8 @@ async function runDirectSkillMode(options, reporter) {
|
|
|
660
964
|
return 1;
|
|
661
965
|
}
|
|
662
966
|
if (pullRequest.files.length === 0) {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
process.stdout.write(content);
|
|
666
|
-
}
|
|
667
|
-
else {
|
|
668
|
-
writeEmptyRunLog(repoPath, { traceId: getTraceId(), outputPath: options.output });
|
|
967
|
+
emitEmptyRunLog(repoPath, options);
|
|
968
|
+
if (!options.json) {
|
|
669
969
|
if (options.staged) {
|
|
670
970
|
reporter.renderEmptyState('No staged changes found');
|
|
671
971
|
}
|
|
@@ -717,7 +1017,7 @@ async function runCommand(options, reporter) {
|
|
|
717
1017
|
return runFileMode(filePatterns, options, reporter);
|
|
718
1018
|
}
|
|
719
1019
|
export async function main() {
|
|
720
|
-
const { command, options, setupAppOptions,
|
|
1020
|
+
const { command, options, setupAppOptions, runsOptions } = parseCliArgs();
|
|
721
1021
|
if (command === 'help') {
|
|
722
1022
|
showHelp();
|
|
723
1023
|
process.exit(0);
|
|
@@ -762,12 +1062,12 @@ export async function main() {
|
|
|
762
1062
|
return runSetupApp(setupAppOptions, reporter);
|
|
763
1063
|
case 'sync':
|
|
764
1064
|
return runSync(options, reporter);
|
|
765
|
-
case '
|
|
766
|
-
if (!
|
|
767
|
-
reporter.error('Missing
|
|
1065
|
+
case 'runs':
|
|
1066
|
+
if (!runsOptions) {
|
|
1067
|
+
reporter.error('Missing runs options');
|
|
768
1068
|
process.exit(1);
|
|
769
1069
|
}
|
|
770
|
-
return
|
|
1070
|
+
return runRuns(runsOptions, options, reporter);
|
|
771
1071
|
default:
|
|
772
1072
|
return runCommand(options, reporter);
|
|
773
1073
|
}
|