@valescoagency/runway 0.9.0 → 0.10.1
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/README.md +1 -1
- package/dist/cli.js +1 -0
- package/dist/commands/run.js +64 -2
- package/dist/config.js +8 -0
- package/dist/dashboard/otlp.js +16 -2
- package/dist/dashboard/projector.js +12 -0
- package/dist/dashboard/server.js +60 -4
- package/dist/dashboard/storage.js +233 -17
- package/dist/dashboard/views.js +18 -1
- package/dist/finalize.js +34 -2
- package/dist/git.js +192 -22
- package/dist/implement.js +6 -0
- package/dist/linear.js +75 -16
- package/dist/orchestrator.js +99 -18
- package/dist/prompts.js +40 -0
- package/dist/review.js +32 -18
- package/package.json +1 -1
- package/prompts/implement.md +11 -0
- package/prompts/review.md +48 -6
|
@@ -21,9 +21,13 @@ const SCHEMA = `
|
|
|
21
21
|
parent_span_id TEXT,
|
|
22
22
|
issue_identifier TEXT NOT NULL,
|
|
23
23
|
issue_id TEXT,
|
|
24
|
+
issue_title TEXT,
|
|
25
|
+
issue_labels TEXT,
|
|
24
26
|
branch TEXT,
|
|
25
27
|
outcome_kind TEXT,
|
|
26
28
|
outcome_detail TEXT,
|
|
29
|
+
pr_url TEXT,
|
|
30
|
+
hitl_reason TEXT,
|
|
27
31
|
start_time_unix_nano TEXT NOT NULL,
|
|
28
32
|
end_time_unix_nano TEXT NOT NULL,
|
|
29
33
|
status_code INTEGER,
|
|
@@ -38,6 +42,9 @@ const SCHEMA = `
|
|
|
38
42
|
CREATE INDEX IF NOT EXISTS idx_issue_processes_trace_id
|
|
39
43
|
ON issue_processes(trace_id);
|
|
40
44
|
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_issue_processes_span_id
|
|
46
|
+
ON issue_processes(span_id);
|
|
47
|
+
|
|
41
48
|
CREATE TABLE IF NOT EXISTS raw_spans (
|
|
42
49
|
trace_id TEXT NOT NULL,
|
|
43
50
|
span_id TEXT NOT NULL,
|
|
@@ -63,6 +70,128 @@ const SCHEMA = `
|
|
|
63
70
|
CREATE INDEX IF NOT EXISTS idx_agent_iterations_issue_process
|
|
64
71
|
ON agent_iterations(trace_id, issue_process_id, iteration_index);
|
|
65
72
|
`;
|
|
73
|
+
const DEFAULT_AGGREGATE_WINDOW = 30;
|
|
74
|
+
/**
|
|
75
|
+
* VA-399: SQL VIEW that computes the evaluator-facing aggregates over
|
|
76
|
+
* the last N drains. N is interpolated at view-creation time because
|
|
77
|
+
* SQLite views can't take parameters — when the dashboard process
|
|
78
|
+
* starts with a different `DASHBOARD_AGGREGATE_WINDOW`, the view is
|
|
79
|
+
* dropped and recreated with the new LIMIT.
|
|
80
|
+
*
|
|
81
|
+
* Median uses the "average of the two middle values when N is even,
|
|
82
|
+
* the middle value when N is odd" convention; p95 uses the
|
|
83
|
+
* nearest-rank method (smallest observed value whose rank meets-or-
|
|
84
|
+
* exceeds 95%). See `read-model.md` for the field-by-field contract.
|
|
85
|
+
*
|
|
86
|
+
* `reviewer_rejection_rate` keys on the detail prefix emitted by
|
|
87
|
+
* `src/review.ts` ("Sub-agent review rejected: ..."). It's a subset
|
|
88
|
+
* of `hitl_escape_rate` — a review rejection routes to HITL, so both
|
|
89
|
+
* rates count the same row.
|
|
90
|
+
*/
|
|
91
|
+
function aggregatesViewDdl(windowDrains) {
|
|
92
|
+
// windowDrains is the only spot we interpolate rather than
|
|
93
|
+
// parameter-bind (CREATE VIEW can't take params). Coerce to a
|
|
94
|
+
// positive integer so a hostile env var can't smuggle SQL through.
|
|
95
|
+
const n = Math.max(1, Math.floor(windowDrains));
|
|
96
|
+
return `
|
|
97
|
+
DROP VIEW IF EXISTS evaluator_aggregates_v1;
|
|
98
|
+
CREATE VIEW evaluator_aggregates_v1 AS
|
|
99
|
+
WITH recent_drains AS (
|
|
100
|
+
SELECT trace_id
|
|
101
|
+
FROM drains
|
|
102
|
+
ORDER BY CAST(start_time_unix_nano AS INTEGER) DESC
|
|
103
|
+
LIMIT ${n}
|
|
104
|
+
),
|
|
105
|
+
process_rows AS (
|
|
106
|
+
SELECT
|
|
107
|
+
ip.trace_id,
|
|
108
|
+
ip.span_id,
|
|
109
|
+
ip.outcome_kind,
|
|
110
|
+
COALESCE(ip.outcome_detail, '') AS outcome_detail,
|
|
111
|
+
CASE
|
|
112
|
+
WHEN instr(ip.issue_identifier, '-') > 0
|
|
113
|
+
THEN substr(ip.issue_identifier, 1, instr(ip.issue_identifier, '-') - 1)
|
|
114
|
+
ELSE ip.issue_identifier
|
|
115
|
+
END AS category,
|
|
116
|
+
(CAST(ip.end_time_unix_nano AS INTEGER) - CAST(ip.start_time_unix_nano AS INTEGER)) / 1000000 AS wall_time_ms,
|
|
117
|
+
(
|
|
118
|
+
SELECT COUNT(*) FROM agent_iterations a
|
|
119
|
+
WHERE a.trace_id = ip.trace_id AND a.issue_process_id = ip.span_id
|
|
120
|
+
) AS iteration_count
|
|
121
|
+
FROM issue_processes ip
|
|
122
|
+
WHERE ip.trace_id IN (SELECT trace_id FROM recent_drains)
|
|
123
|
+
),
|
|
124
|
+
wt_ranked AS (
|
|
125
|
+
SELECT
|
|
126
|
+
category,
|
|
127
|
+
wall_time_ms,
|
|
128
|
+
ROW_NUMBER() OVER (PARTITION BY category ORDER BY wall_time_ms) AS rn,
|
|
129
|
+
COUNT(*) OVER (PARTITION BY category) AS cnt
|
|
130
|
+
FROM process_rows
|
|
131
|
+
),
|
|
132
|
+
it_ranked AS (
|
|
133
|
+
SELECT
|
|
134
|
+
category,
|
|
135
|
+
iteration_count,
|
|
136
|
+
ROW_NUMBER() OVER (PARTITION BY category ORDER BY iteration_count) AS rn,
|
|
137
|
+
COUNT(*) OVER (PARTITION BY category) AS cnt
|
|
138
|
+
FROM process_rows
|
|
139
|
+
),
|
|
140
|
+
wt_median AS (
|
|
141
|
+
SELECT category, AVG(wall_time_ms * 1.0) AS value
|
|
142
|
+
FROM wt_ranked
|
|
143
|
+
WHERE rn IN ((cnt + 1) / 2, (cnt / 2) + 1)
|
|
144
|
+
GROUP BY category
|
|
145
|
+
),
|
|
146
|
+
wt_p95 AS (
|
|
147
|
+
SELECT category, MIN(wall_time_ms) AS value
|
|
148
|
+
FROM wt_ranked
|
|
149
|
+
WHERE rn >= (cnt * 95 + 99) / 100
|
|
150
|
+
GROUP BY category
|
|
151
|
+
),
|
|
152
|
+
it_median AS (
|
|
153
|
+
SELECT category, AVG(iteration_count * 1.0) AS value
|
|
154
|
+
FROM it_ranked
|
|
155
|
+
WHERE rn IN ((cnt + 1) / 2, (cnt / 2) + 1)
|
|
156
|
+
GROUP BY category
|
|
157
|
+
),
|
|
158
|
+
it_p95 AS (
|
|
159
|
+
SELECT category, MIN(iteration_count) AS value
|
|
160
|
+
FROM it_ranked
|
|
161
|
+
WHERE rn >= (cnt * 95 + 99) / 100
|
|
162
|
+
GROUP BY category
|
|
163
|
+
),
|
|
164
|
+
rates AS (
|
|
165
|
+
SELECT
|
|
166
|
+
category,
|
|
167
|
+
COUNT(*) AS sample_size,
|
|
168
|
+
AVG(CASE WHEN outcome_kind = 'hitl' AND outcome_detail LIKE 'Sub-agent review rejected%'
|
|
169
|
+
THEN 1.0 ELSE 0.0 END) AS reviewer_rejection_rate,
|
|
170
|
+
AVG(CASE WHEN outcome_kind = 'reverted' THEN 1.0 ELSE 0.0 END) AS revert_rate,
|
|
171
|
+
AVG(CASE WHEN outcome_kind = 'hitl' THEN 1.0 ELSE 0.0 END) AS hitl_escape_rate,
|
|
172
|
+
AVG(CASE WHEN outcome_kind = 'errored' THEN 1.0 ELSE 0.0 END) AS infra_error_rate
|
|
173
|
+
FROM process_rows
|
|
174
|
+
GROUP BY category
|
|
175
|
+
)
|
|
176
|
+
SELECT
|
|
177
|
+
r.category AS category,
|
|
178
|
+
r.sample_size AS sample_size,
|
|
179
|
+
itm.value AS median_iteration_count,
|
|
180
|
+
itp.value AS p95_iteration_count,
|
|
181
|
+
wtm.value AS median_wall_time_ms,
|
|
182
|
+
wtp.value AS p95_wall_time_ms,
|
|
183
|
+
r.reviewer_rejection_rate AS reviewer_rejection_rate,
|
|
184
|
+
r.revert_rate AS revert_rate,
|
|
185
|
+
r.hitl_escape_rate AS hitl_escape_rate,
|
|
186
|
+
r.infra_error_rate AS infra_error_rate
|
|
187
|
+
FROM rates r
|
|
188
|
+
LEFT JOIN wt_median wtm ON wtm.category = r.category
|
|
189
|
+
LEFT JOIN wt_p95 wtp ON wtp.category = r.category
|
|
190
|
+
LEFT JOIN it_median itm ON itm.category = r.category
|
|
191
|
+
LEFT JOIN it_p95 itp ON itp.category = r.category
|
|
192
|
+
ORDER BY r.category;
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
66
195
|
/**
|
|
67
196
|
* Open (or create) a SQLite database at `path` and return a typed
|
|
68
197
|
* `Storage` handle. Pass `:memory:` for tests — the in-memory db
|
|
@@ -72,9 +201,35 @@ const SCHEMA = `
|
|
|
72
201
|
* OTel SDK retrying a flush) don't blow up the receiver — last writer
|
|
73
202
|
* wins on (trace_id, span_id).
|
|
74
203
|
*/
|
|
75
|
-
export function createStorage(path) {
|
|
204
|
+
export function createStorage(path, opts = {}) {
|
|
76
205
|
const db = new DatabaseSync(path);
|
|
77
206
|
db.exec(SCHEMA);
|
|
207
|
+
// VA-387: idempotent column adds for DBs created against an older
|
|
208
|
+
// schema. `CREATE TABLE IF NOT EXISTS` won't migrate an existing
|
|
209
|
+
// table; SQLite has no `ADD COLUMN IF NOT EXISTS`, so we swallow
|
|
210
|
+
// the duplicate-column error individually. Runs BEFORE VA-399's
|
|
211
|
+
// view install — `evaluator_aggregates_v1` reads from
|
|
212
|
+
// `issue_processes`, so the columns it may query must exist first.
|
|
213
|
+
for (const sql of [
|
|
214
|
+
`ALTER TABLE issue_processes ADD COLUMN issue_title TEXT`,
|
|
215
|
+
`ALTER TABLE issue_processes ADD COLUMN issue_labels TEXT`,
|
|
216
|
+
`ALTER TABLE issue_processes ADD COLUMN pr_url TEXT`,
|
|
217
|
+
`ALTER TABLE issue_processes ADD COLUMN hitl_reason TEXT`,
|
|
218
|
+
]) {
|
|
219
|
+
try {
|
|
220
|
+
db.exec(sql);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Column already present — fresh CREATE TABLE path, or a prior
|
|
224
|
+
// dashboard boot ran the same migration.
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// VA-399: install the evaluator-facing read-model view after the
|
|
228
|
+
// base tables exist (and after VA-387's column migrations above),
|
|
229
|
+
// but before any prepared statement is created — a
|
|
230
|
+
// `SELECT FROM evaluator_aggregates_v1` would otherwise race the
|
|
231
|
+
// DDL on first use.
|
|
232
|
+
db.exec(aggregatesViewDdl(opts.aggregateWindowDrains ?? DEFAULT_AGGREGATE_WINDOW));
|
|
78
233
|
const insertDrain = db.prepare(`
|
|
79
234
|
INSERT INTO drains (
|
|
80
235
|
trace_id, span_id, start_time_unix_nano, end_time_unix_nano,
|
|
@@ -93,16 +248,21 @@ export function createStorage(path) {
|
|
|
93
248
|
const insertIssueProcess = db.prepare(`
|
|
94
249
|
INSERT INTO issue_processes (
|
|
95
250
|
trace_id, span_id, parent_span_id, issue_identifier, issue_id,
|
|
96
|
-
branch, outcome_kind, outcome_detail,
|
|
251
|
+
issue_title, issue_labels, branch, outcome_kind, outcome_detail,
|
|
252
|
+
pr_url, hitl_reason,
|
|
97
253
|
start_time_unix_nano, end_time_unix_nano, status_code, status_message
|
|
98
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
254
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
99
255
|
ON CONFLICT (trace_id, span_id) DO UPDATE SET
|
|
100
256
|
parent_span_id = excluded.parent_span_id,
|
|
101
257
|
issue_identifier = excluded.issue_identifier,
|
|
102
258
|
issue_id = excluded.issue_id,
|
|
259
|
+
issue_title = excluded.issue_title,
|
|
260
|
+
issue_labels = excluded.issue_labels,
|
|
103
261
|
branch = excluded.branch,
|
|
104
262
|
outcome_kind = excluded.outcome_kind,
|
|
105
263
|
outcome_detail = excluded.outcome_detail,
|
|
264
|
+
pr_url = excluded.pr_url,
|
|
265
|
+
hitl_reason = excluded.hitl_reason,
|
|
106
266
|
start_time_unix_nano = excluded.start_time_unix_nano,
|
|
107
267
|
end_time_unix_nano = excluded.end_time_unix_nano,
|
|
108
268
|
status_code = excluded.status_code,
|
|
@@ -130,35 +290,40 @@ export function createStorage(path) {
|
|
|
130
290
|
`);
|
|
131
291
|
// Two list variants instead of one with conditional SQL — keeps
|
|
132
292
|
// each prepared statement static.
|
|
133
|
-
const
|
|
134
|
-
SELECT
|
|
293
|
+
const ISSUE_PROCESS_COLUMNS = `
|
|
135
294
|
trace_id, span_id, parent_span_id, issue_identifier, issue_id,
|
|
136
|
-
branch, outcome_kind, outcome_detail,
|
|
295
|
+
issue_title, issue_labels, branch, outcome_kind, outcome_detail,
|
|
296
|
+
pr_url, hitl_reason,
|
|
137
297
|
start_time_unix_nano, end_time_unix_nano, status_code, status_message,
|
|
138
298
|
inserted_at
|
|
299
|
+
`;
|
|
300
|
+
const listAll = db.prepare(`
|
|
301
|
+
SELECT ${ISSUE_PROCESS_COLUMNS}
|
|
139
302
|
FROM issue_processes
|
|
140
303
|
ORDER BY inserted_at DESC, span_id DESC
|
|
141
304
|
LIMIT ?
|
|
142
305
|
`);
|
|
143
306
|
const listByTrace = db.prepare(`
|
|
144
|
-
SELECT
|
|
145
|
-
trace_id, span_id, parent_span_id, issue_identifier, issue_id,
|
|
146
|
-
branch, outcome_kind, outcome_detail,
|
|
147
|
-
start_time_unix_nano, end_time_unix_nano, status_code, status_message,
|
|
148
|
-
inserted_at
|
|
307
|
+
SELECT ${ISSUE_PROCESS_COLUMNS}
|
|
149
308
|
FROM issue_processes
|
|
150
309
|
WHERE trace_id = ?
|
|
151
310
|
ORDER BY inserted_at DESC, span_id DESC
|
|
152
311
|
LIMIT ?
|
|
153
312
|
`);
|
|
154
313
|
const getProcessStmt = db.prepare(`
|
|
155
|
-
SELECT
|
|
156
|
-
trace_id, span_id, parent_span_id, issue_identifier, issue_id,
|
|
157
|
-
branch, outcome_kind, outcome_detail,
|
|
158
|
-
start_time_unix_nano, end_time_unix_nano, status_code, status_message,
|
|
159
|
-
inserted_at
|
|
314
|
+
SELECT ${ISSUE_PROCESS_COLUMNS}
|
|
160
315
|
FROM issue_processes
|
|
161
316
|
WHERE trace_id = ? AND span_id = ?
|
|
317
|
+
`);
|
|
318
|
+
// VA-387: span_id is unique in practice (random 64-bit ids); the
|
|
319
|
+
// detail route at `/issue-processes/:id` keys on span_id alone so
|
|
320
|
+
// operators don't have to type the trace_id in URLs.
|
|
321
|
+
const getProcessBySpanStmt = db.prepare(`
|
|
322
|
+
SELECT ${ISSUE_PROCESS_COLUMNS}
|
|
323
|
+
FROM issue_processes
|
|
324
|
+
WHERE span_id = ?
|
|
325
|
+
ORDER BY inserted_at DESC
|
|
326
|
+
LIMIT 1
|
|
162
327
|
`);
|
|
163
328
|
const listIterations = db.prepare(`
|
|
164
329
|
SELECT
|
|
@@ -169,11 +334,16 @@ export function createStorage(path) {
|
|
|
169
334
|
WHERE trace_id = ? AND issue_process_id = ?
|
|
170
335
|
ORDER BY iteration_index ASC
|
|
171
336
|
`);
|
|
337
|
+
const selectAggregates = db.prepare(`SELECT * FROM evaluator_aggregates_v1`);
|
|
172
338
|
const saveDrain = (d) => {
|
|
173
339
|
insertDrain.run(d.traceId, d.spanId, d.startTimeUnixNano, d.endTimeUnixNano, asInt(d.attempts), asInt(d.opened), asInt(d.hitl), asInt(d.errored), asInt(d.statusCode), d.statusMessage);
|
|
174
340
|
};
|
|
175
341
|
const saveIssueProcess = (p) => {
|
|
176
|
-
insertIssueProcess.run(p.traceId, p.spanId, p.parentSpanId, p.issueIdentifier, p.issueId, p.
|
|
342
|
+
insertIssueProcess.run(p.traceId, p.spanId, p.parentSpanId, p.issueIdentifier, p.issueId, p.issueTitle,
|
|
343
|
+
// VA-387: labels round-trip as a JSON array string. Keeping them
|
|
344
|
+
// in one column avoids a label-many-to-many table for a feature
|
|
345
|
+
// that's read-only on the dashboard side.
|
|
346
|
+
p.issueLabels.length === 0 ? null : JSON.stringify(p.issueLabels), p.branch, p.outcomeKind, p.outcomeDetail, p.prUrl, p.hitlReason, p.startTimeUnixNano, p.endTimeUnixNano, asInt(p.statusCode), p.statusMessage);
|
|
177
347
|
};
|
|
178
348
|
const saveAgentIteration = (a) => {
|
|
179
349
|
insertAgentIteration.run(a.traceId, a.spanId, a.issueProcessSpanId, asInt(a.iterationIndex), a.startTimeUnixNano, a.endTimeUnixNano, a.sandcastleRunId, a.exitStatus);
|
|
@@ -192,6 +362,10 @@ export function createStorage(path) {
|
|
|
192
362
|
const row = getProcessStmt.get(traceId, spanId);
|
|
193
363
|
return row ? rowToIssueProcess(row) : undefined;
|
|
194
364
|
};
|
|
365
|
+
const getIssueProcessBySpanId = (spanId) => {
|
|
366
|
+
const row = getProcessBySpanStmt.get(spanId);
|
|
367
|
+
return row ? rowToIssueProcess(row) : undefined;
|
|
368
|
+
};
|
|
195
369
|
const listAgentIterations = (traceId, issueProcessSpanId) => {
|
|
196
370
|
const rows = listIterations.all(traceId, issueProcessSpanId);
|
|
197
371
|
return rows.map(rowToAgentIteration);
|
|
@@ -227,6 +401,7 @@ export function createStorage(path) {
|
|
|
227
401
|
.all(traceId, issueProcessSpanId, ...names);
|
|
228
402
|
return rows.map(rowToPhaseSpan);
|
|
229
403
|
};
|
|
404
|
+
const listAggregates = () => selectAggregates.all().map(rowToAggregate);
|
|
230
405
|
const close = () => {
|
|
231
406
|
db.close();
|
|
232
407
|
};
|
|
@@ -237,8 +412,10 @@ export function createStorage(path) {
|
|
|
237
412
|
saveRawSpan,
|
|
238
413
|
listIssueProcesses,
|
|
239
414
|
getIssueProcess,
|
|
415
|
+
getIssueProcessBySpanId,
|
|
240
416
|
listAgentIterations,
|
|
241
417
|
listPhaseSpans,
|
|
418
|
+
listAggregates,
|
|
242
419
|
close,
|
|
243
420
|
};
|
|
244
421
|
}
|
|
@@ -256,9 +433,13 @@ function rowToIssueProcess(row) {
|
|
|
256
433
|
parentSpanId: nullableStr(r.parent_span_id),
|
|
257
434
|
issueIdentifier: String(r.issue_identifier ?? ""),
|
|
258
435
|
issueId: nullableStr(r.issue_id),
|
|
436
|
+
issueTitle: nullableStr(r.issue_title),
|
|
437
|
+
issueLabels: parseLabels(r.issue_labels),
|
|
259
438
|
branch: nullableStr(r.branch),
|
|
260
439
|
outcomeKind: nullableStr(r.outcome_kind),
|
|
261
440
|
outcomeDetail: nullableStr(r.outcome_detail),
|
|
441
|
+
prUrl: nullableStr(r.pr_url),
|
|
442
|
+
hitlReason: nullableStr(r.hitl_reason),
|
|
262
443
|
startTimeUnixNano: String(r.start_time_unix_nano ?? ""),
|
|
263
444
|
endTimeUnixNano: String(r.end_time_unix_nano ?? ""),
|
|
264
445
|
statusCode: nullableNum(r.status_code),
|
|
@@ -266,6 +447,26 @@ function rowToIssueProcess(row) {
|
|
|
266
447
|
insertedAt: String(r.inserted_at ?? ""),
|
|
267
448
|
};
|
|
268
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* VA-387: decode the JSON-encoded `issue_labels` column back into a
|
|
452
|
+
* string array. A row stored before the column existed (or one with
|
|
453
|
+
* NULL / malformed JSON) collapses to an empty list.
|
|
454
|
+
*/
|
|
455
|
+
function parseLabels(v) {
|
|
456
|
+
if (v === null || v === undefined)
|
|
457
|
+
return [];
|
|
458
|
+
if (typeof v !== "string")
|
|
459
|
+
return [];
|
|
460
|
+
try {
|
|
461
|
+
const parsed = JSON.parse(v);
|
|
462
|
+
if (!Array.isArray(parsed))
|
|
463
|
+
return [];
|
|
464
|
+
return parsed.filter((x) => typeof x === "string");
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
return [];
|
|
468
|
+
}
|
|
469
|
+
}
|
|
269
470
|
function rowToAgentIteration(row) {
|
|
270
471
|
const r = row;
|
|
271
472
|
return {
|
|
@@ -302,3 +503,18 @@ function nullableNum(v) {
|
|
|
302
503
|
const n = Number(v);
|
|
303
504
|
return Number.isFinite(n) ? n : null;
|
|
304
505
|
}
|
|
506
|
+
function rowToAggregate(row) {
|
|
507
|
+
const r = row;
|
|
508
|
+
return {
|
|
509
|
+
category: String(r.category ?? ""),
|
|
510
|
+
sampleSize: Number(r.sample_size ?? 0),
|
|
511
|
+
medianIterationCount: nullableNum(r.median_iteration_count),
|
|
512
|
+
p95IterationCount: nullableNum(r.p95_iteration_count),
|
|
513
|
+
medianWallTimeMs: nullableNum(r.median_wall_time_ms),
|
|
514
|
+
p95WallTimeMs: nullableNum(r.p95_wall_time_ms),
|
|
515
|
+
reviewerRejectionRate: Number(r.reviewer_rejection_rate ?? 0),
|
|
516
|
+
revertRate: Number(r.revert_rate ?? 0),
|
|
517
|
+
hitlEscapeRate: Number(r.hitl_escape_rate ?? 0),
|
|
518
|
+
infraErrorRate: Number(r.infra_error_rate ?? 0),
|
|
519
|
+
};
|
|
520
|
+
}
|
package/dist/dashboard/views.js
CHANGED
|
@@ -60,7 +60,8 @@ export function renderListView(rows) {
|
|
|
60
60
|
function renderRow(r) {
|
|
61
61
|
const kind = r.outcomeKind ?? "pending";
|
|
62
62
|
const outcomeCls = `outcome outcome-${escapeHtml(kind)}`;
|
|
63
|
-
|
|
63
|
+
// VA-387: canonical detail link uses the span_id alone.
|
|
64
|
+
const href = `/issue-processes/${encodeURIComponent(r.spanId)}`;
|
|
64
65
|
return `<tr>
|
|
65
66
|
<td class="id"><a href="${escapeHtml(href)}">${escapeHtml(r.issueIdentifier)}</a></td>
|
|
66
67
|
<td class="${outcomeCls}">${escapeHtml(kind)}</td>
|
|
@@ -83,6 +84,18 @@ export function renderDetailView(vm) {
|
|
|
83
84
|
const ip = vm.issueProcess;
|
|
84
85
|
const kind = ip.outcomeKind ?? "pending";
|
|
85
86
|
const outcomeCls = `outcome outcome-${escapeHtml(kind)}`;
|
|
87
|
+
const titleLine = ip.issueTitle
|
|
88
|
+
? `<div class="title">${escapeHtml(ip.issueTitle)}</div>`
|
|
89
|
+
: "";
|
|
90
|
+
const prLine = ip.prUrl
|
|
91
|
+
? `<div><span class="label">PR:</span><a href="${escapeHtml(ip.prUrl)}" rel="noopener noreferrer" target="_blank">${escapeHtml(ip.prUrl)}</a></div>`
|
|
92
|
+
: "";
|
|
93
|
+
// HITL reason is only rendered when present (HITL or errored runs).
|
|
94
|
+
// For opened/reverted outcomes we omit the row entirely rather than
|
|
95
|
+
// showing an empty label.
|
|
96
|
+
const hitlLine = ip.hitlReason
|
|
97
|
+
? `<div><span class="label">HITL reason:</span><span class="detail">${escapeHtml(ip.hitlReason)}</span></div>`
|
|
98
|
+
: "";
|
|
86
99
|
return `<!doctype html>
|
|
87
100
|
<html lang="en">
|
|
88
101
|
<head>
|
|
@@ -90,6 +103,7 @@ export function renderDetailView(vm) {
|
|
|
90
103
|
<title>${escapeHtml(ip.issueIdentifier)} · runway dashboard</title>
|
|
91
104
|
<style>${SHARED_STYLE}
|
|
92
105
|
.breadcrumb { color: #9ca3af; margin-bottom: 16px; font-size: 12px; }
|
|
106
|
+
.title { font-size: 14px; color: #d4d4d8; margin: -8px 0 12px; }
|
|
93
107
|
.meta { margin: 4px 0 16px; }
|
|
94
108
|
.meta .label { color: #9ca3af; margin-right: 4px; }
|
|
95
109
|
.timeline { position: relative; height: 28px; background: #18181b;
|
|
@@ -113,8 +127,11 @@ export function renderDetailView(vm) {
|
|
|
113
127
|
<body>
|
|
114
128
|
<div class="breadcrumb"><a href="/">← all issue processes</a></div>
|
|
115
129
|
<h1>${escapeHtml(ip.issueIdentifier)} · <span class="${outcomeCls}">${escapeHtml(kind)}</span></h1>
|
|
130
|
+
${titleLine}
|
|
116
131
|
<div class="meta">
|
|
117
132
|
<div><span class="label">branch:</span><code>${escapeHtml(ip.branch ?? "—")}</code></div>
|
|
133
|
+
${prLine}
|
|
134
|
+
${hitlLine}
|
|
118
135
|
<div><span class="label">detail:</span><span class="detail">${escapeHtml(ip.outcomeDetail ?? "")}</span></div>
|
|
119
136
|
<div><span class="label">seen at:</span>${escapeHtml(ip.insertedAt)}</div>
|
|
120
137
|
</div>
|
package/dist/finalize.js
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
+
import { rebaseOntoBase } from "./git.js";
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
4
|
+
* VA-419: rebase the agent branch onto the latest `origin/<baseBranch>`,
|
|
5
|
+
* then push and open the PR. If the rebase hits a conflict, restore
|
|
6
|
+
* pre-rebase state and surface a `rebase-conflict` outcome — the
|
|
7
|
+
* orchestrator routes it to HITL so the operator can reconcile
|
|
8
|
+
* manually (or let VA-417 reset the branch on the next drain).
|
|
5
9
|
*/
|
|
6
10
|
export const finalize = (issue, deps, branch) => Effect.gen(function* () {
|
|
7
11
|
const { config, cwd, baseBranch, linear, github } = deps;
|
|
12
|
+
const rebase = yield* rebaseOntoBase(cwd, baseBranch, branch).pipe(Effect.withSpan("rebaseOntoBase"));
|
|
13
|
+
if (rebase.kind === "conflict") {
|
|
14
|
+
return {
|
|
15
|
+
kind: "rebase-conflict",
|
|
16
|
+
baseBranch,
|
|
17
|
+
conflictedFiles: rebase.conflictedFiles,
|
|
18
|
+
reviewVerdict: "REVIEW: APPROVED",
|
|
19
|
+
};
|
|
20
|
+
}
|
|
8
21
|
yield* github.pushBranch(cwd, branch).pipe(Effect.withSpan("pushBranch"));
|
|
9
22
|
const prBody = buildPrBody(issue);
|
|
10
23
|
const prUrl = yield* github
|
|
@@ -20,6 +33,25 @@ export const finalize = (issue, deps, branch) => Effect.gen(function* () {
|
|
|
20
33
|
yield* linear.comment(issue.id, `Runway opened a PR for review: ${prUrl}`);
|
|
21
34
|
return { kind: "opened", detail: prUrl };
|
|
22
35
|
});
|
|
36
|
+
/**
|
|
37
|
+
* VA-419: format the HITL message body for a rebase-conflict outcome.
|
|
38
|
+
* Names the base branch and the conflicted file list, and quotes the
|
|
39
|
+
* reviewer's APPROVED verdict so the operator knows the agent's diff
|
|
40
|
+
* was good before the conflict surfaced.
|
|
41
|
+
*/
|
|
42
|
+
export function formatRebaseConflictReason(args) {
|
|
43
|
+
const fileLines = args.conflictedFiles.length
|
|
44
|
+
? args.conflictedFiles.map((f) => ` - ${f}`).join("\n")
|
|
45
|
+
: " (no conflicted files reported by git — inspect manually)";
|
|
46
|
+
return [
|
|
47
|
+
`Upstream base \`${args.baseBranch}\` advanced during this drain; the rebase`,
|
|
48
|
+
"onto the latest base produced conflicts in:",
|
|
49
|
+
fileLines,
|
|
50
|
+
`Review was APPROVED before the rebase (\`${args.reviewVerdict}\`); the agent's diff`,
|
|
51
|
+
"is good but needs operator reconciliation against the new base. Re-run runway",
|
|
52
|
+
"after rebasing manually, or let VA-417 handle it on the next drain.",
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
|
23
55
|
// VA-412: `Closes` (not `Refs`) is the Linear GitHub-integration magic
|
|
24
56
|
// word that auto-transitions the issue to Done on PR merge. `Refs`
|
|
25
57
|
// only attaches the PR to the issue and leaves it stuck In Progress.
|