@feigi/fleet-ctl 3.95.3 → 3.96.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/agents/fleet-implementer-slow-high.agent.md +1 -1
- package/agents/fleet-implementer-slow-medium.agent.md +1 -1
- package/agents/fleet-implementer-smol-high.agent.md +1 -1
- package/agents/fleet-implementer-task-high.agent.md +1 -1
- package/agents/fleet-implementer-task-max.agent.md +1 -1
- package/package.json +1 -1
- package/scripts/ledger.mjs +3 -1
- package/scripts/member-outcomes.mjs +106 -15
- package/scripts/member-record.mjs +28 -5
- package/scripts/report-schema.mjs +12 -5
|
@@ -240,7 +240,7 @@ including if the `Sizing:` line was written before the run and corrected after;
|
|
|
240
240
|
`sizing.verdict` and `sizing.signal` are that line's verdict and signal.
|
|
241
241
|
Nothing in the PR body separates a measured verdict from one typed in early;
|
|
242
242
|
that field is the only thing that does, and an unreported ordering costs the
|
|
243
|
-
covariate. A bail is `outcome` `bailed`, its cause in `bailCause`: the
|
|
243
|
+
covariate. A bail is `outcome` `bailed`, its cause in `bailCause`, with `pr` and `head` `null`: the
|
|
244
244
|
pre-code outcome, before you touch code. A block or ambiguity you find after
|
|
245
245
|
implementation began — the work cannot finish without a ruling or human hands
|
|
246
246
|
you do not have — is `outcome` `blocked` instead, with `pr`, `head` and `bailCause` `null` and
|
|
@@ -240,7 +240,7 @@ including if the `Sizing:` line was written before the run and corrected after;
|
|
|
240
240
|
`sizing.verdict` and `sizing.signal` are that line's verdict and signal.
|
|
241
241
|
Nothing in the PR body separates a measured verdict from one typed in early;
|
|
242
242
|
that field is the only thing that does, and an unreported ordering costs the
|
|
243
|
-
covariate. A bail is `outcome` `bailed`, its cause in `bailCause`: the
|
|
243
|
+
covariate. A bail is `outcome` `bailed`, its cause in `bailCause`, with `pr` and `head` `null`: the
|
|
244
244
|
pre-code outcome, before you touch code. A block or ambiguity you find after
|
|
245
245
|
implementation began — the work cannot finish without a ruling or human hands
|
|
246
246
|
you do not have — is `outcome` `blocked` instead, with `pr`, `head` and `bailCause` `null` and
|
|
@@ -240,7 +240,7 @@ including if the `Sizing:` line was written before the run and corrected after;
|
|
|
240
240
|
`sizing.verdict` and `sizing.signal` are that line's verdict and signal.
|
|
241
241
|
Nothing in the PR body separates a measured verdict from one typed in early;
|
|
242
242
|
that field is the only thing that does, and an unreported ordering costs the
|
|
243
|
-
covariate. A bail is `outcome` `bailed`, its cause in `bailCause`: the
|
|
243
|
+
covariate. A bail is `outcome` `bailed`, its cause in `bailCause`, with `pr` and `head` `null`: the
|
|
244
244
|
pre-code outcome, before you touch code. A block or ambiguity you find after
|
|
245
245
|
implementation began — the work cannot finish without a ruling or human hands
|
|
246
246
|
you do not have — is `outcome` `blocked` instead, with `pr`, `head` and `bailCause` `null` and
|
|
@@ -240,7 +240,7 @@ including if the `Sizing:` line was written before the run and corrected after;
|
|
|
240
240
|
`sizing.verdict` and `sizing.signal` are that line's verdict and signal.
|
|
241
241
|
Nothing in the PR body separates a measured verdict from one typed in early;
|
|
242
242
|
that field is the only thing that does, and an unreported ordering costs the
|
|
243
|
-
covariate. A bail is `outcome` `bailed`, its cause in `bailCause`: the
|
|
243
|
+
covariate. A bail is `outcome` `bailed`, its cause in `bailCause`, with `pr` and `head` `null`: the
|
|
244
244
|
pre-code outcome, before you touch code. A block or ambiguity you find after
|
|
245
245
|
implementation began — the work cannot finish without a ruling or human hands
|
|
246
246
|
you do not have — is `outcome` `blocked` instead, with `pr`, `head` and `bailCause` `null` and
|
|
@@ -240,7 +240,7 @@ including if the `Sizing:` line was written before the run and corrected after;
|
|
|
240
240
|
`sizing.verdict` and `sizing.signal` are that line's verdict and signal.
|
|
241
241
|
Nothing in the PR body separates a measured verdict from one typed in early;
|
|
242
242
|
that field is the only thing that does, and an unreported ordering costs the
|
|
243
|
-
covariate. A bail is `outcome` `bailed`, its cause in `bailCause`: the
|
|
243
|
+
covariate. A bail is `outcome` `bailed`, its cause in `bailCause`, with `pr` and `head` `null`: the
|
|
244
244
|
pre-code outcome, before you touch code. A block or ambiguity you find after
|
|
245
245
|
implementation began — the work cannot finish without a ruling or human hands
|
|
246
246
|
you do not have — is `outcome` `blocked` instead, with `pr`, `head` and `bailCause` `null` and
|
package/package.json
CHANGED
package/scripts/ledger.mjs
CHANGED
|
@@ -2507,7 +2507,9 @@ function recordFiled(id, subject) {
|
|
|
2507
2507
|
// reads OPEN and its closingIssuesReferences in its
|
|
2508
2508
|
// own repository name N;
|
|
2509
2509
|
// the Check-step record (below) holds for <head>
|
|
2510
|
-
// impl-N=bailed bailCause is not null; the receipt names it
|
|
2510
|
+
// impl-N=bailed bailCause is not null; the receipt names it.
|
|
2511
|
+
// report-schema.mjs already refuses a bail that
|
|
2512
|
+
// names a pr or a head
|
|
2511
2513
|
// impl-N=blocked nothing to check: report-schema.mjs already
|
|
2512
2514
|
// refuses a block that names a bailCause, a pr or
|
|
2513
2515
|
// a head, or gives no reason in bailDetail
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
// Scraper for per-member model/effort facts. Pure over omp's own
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Scraper for per-member model/effort facts. Pure over omp's own transcripts:
|
|
2
|
+
// no clock, no network, no gh. Two columns look beside the subagent ones:
|
|
3
|
+
// `work_s` also reads the controller's own `<session>.jsonl` next to the
|
|
4
|
+
// session directory, and `report` asks whether the member's report file is in
|
|
5
|
+
// the scratch root (`--scratch <dir>`, default the root scratch-root.mjs
|
|
6
|
+
// prints).
|
|
5
7
|
//
|
|
6
8
|
// The transcript parsing (fold, model/effort extraction, ticket/pr naming,
|
|
7
9
|
// and the `gh pr create` result an implementer's `pr` comes from)
|
|
@@ -10,7 +12,7 @@
|
|
|
10
12
|
// them from this file needs to change.
|
|
11
13
|
|
|
12
14
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
13
|
-
import { join, basename } from "node:path";
|
|
15
|
+
import { join, basename, dirname } from "node:path";
|
|
14
16
|
|
|
15
17
|
import { readOmpSession, normalizeModel, parseMemberName, isOmpSessionDirName } from "./member-record.mjs";
|
|
16
18
|
import { stripLevel } from "./tier-roles.mjs";
|
|
@@ -75,7 +77,13 @@ export function rowsForSession(sessionDir, stats = {}, { verifyMain = verifyMain
|
|
|
75
77
|
for (const f of jsonlNames) {
|
|
76
78
|
try { newest = Math.max(newest, statSync(join(sessionDir, f)).mtimeMs); } catch { /* raced away */ }
|
|
77
79
|
}
|
|
78
|
-
const
|
|
80
|
+
const activity = { stamps: [], unread: 0 };
|
|
81
|
+
const recs = readOmpSession(sessionDir, activity); // may throw — a wrong-root refusal, not a per-file fault
|
|
82
|
+
const controller = controllerStamps(sessionDir);
|
|
83
|
+
const workUnknown = typeof controller === "string" ? controller
|
|
84
|
+
: activity.unread ? `${activity.unread} unreadable transcript${activity.unread === 1 ? "" : "s"}` : null;
|
|
85
|
+
const halts = workUnknown ? [] : haltsOf(activity.stamps.concat(controller));
|
|
86
|
+
stats.workSUnknown = workUnknown;
|
|
79
87
|
stats.seen = jsonlNames.length;
|
|
80
88
|
stats.dropped = jsonlNames.length - recs.length;
|
|
81
89
|
stats.torn = 0;
|
|
@@ -93,9 +101,78 @@ export function rowsForSession(sessionDir, stats = {}, { verifyMain = verifyMain
|
|
|
93
101
|
tokensCacheWrite1h: r.tokens_cache_write_1h ?? "", cost: r.cost == null ? "" : Number(r.cost.toFixed(6)),
|
|
94
102
|
ending: endings[i],
|
|
95
103
|
dispatchModel: dispatchModel(r.resolvedModelIdentity),
|
|
104
|
+
workS: workUnknown ? null : r.transcript.timed ? workSeconds(r.transcript.workParts, halts) : "",
|
|
96
105
|
}));
|
|
97
106
|
}
|
|
98
107
|
|
|
108
|
+
// The `work_s` column: a member's working time in whole seconds. Its parts
|
|
109
|
+
// are the fold's (member-record.mjs): each run of entries between two
|
|
110
|
+
// `session_exit` entries, from its first entry to its last, so omp's idle
|
|
111
|
+
// wait before each exit and the wait until a wake are in no part. Every halt
|
|
112
|
+
// is then subtracted where it falls inside a part. `wall_s` stays the raw
|
|
113
|
+
// span, teardown included.
|
|
114
|
+
//
|
|
115
|
+
// A halt is a stretch over HALT_MS in which no transcript of the run records
|
|
116
|
+
// an entry: every transcript in the session directory, nested ones and those
|
|
117
|
+
// that book no row included, and the controller's own `<session>.jsonl` beside
|
|
118
|
+
// the directory, the one that keeps writing while no member runs. Halts are
|
|
119
|
+
// the gaps between consecutive instants of that one sorted list, so no two
|
|
120
|
+
// overlap, and a part never shares time with another part of the same member:
|
|
121
|
+
// no overlap is subtracted twice.
|
|
122
|
+
//
|
|
123
|
+
// Without the controller transcript no halt can be told from a member's own
|
|
124
|
+
// quiet stretch, and with any transcript of the run unreadable, or a line of
|
|
125
|
+
// the controller's lost, entries are missing from the instants, so a stretch
|
|
126
|
+
// they wrote through reads as a halt. Each way every row's `work_s` is `null`:
|
|
127
|
+
// unknown, written blank, and never written over a recorded value
|
|
128
|
+
// (mergeRows); `stats.workSUnknown` names the cause. A transcript with no
|
|
129
|
+
// timestamped entry reads `""`, blank because there is nothing to time.
|
|
130
|
+
export const HALT_MS = 60 * 60 * 1000;
|
|
131
|
+
|
|
132
|
+
// The controller transcript's entry instants, in epoch ms, or a string naming
|
|
133
|
+
// why they cannot be had. It is not a member transcript, so a line is read for
|
|
134
|
+
// its timestamp alone. An unparseable LAST line is a write in progress and is
|
|
135
|
+
// skipped, as the member fold skips it; an unparseable line before it is lost
|
|
136
|
+
// data that may have been the entry breaking a stretch, so a halt there could
|
|
137
|
+
// not be told from the run writing, and the instants are unknown.
|
|
138
|
+
function controllerStamps(sessionDir) {
|
|
139
|
+
let text;
|
|
140
|
+
try { text = readFileSync(join(dirname(sessionDir), `${basename(sessionDir)}.jsonl`), "utf8"); }
|
|
141
|
+
catch (e) { return e?.code === "ENOENT" ? "no controller transcript" : `unreadable controller transcript: ${e?.code ?? e}`; }
|
|
142
|
+
const stamps = [];
|
|
143
|
+
const lines = text.split("\n");
|
|
144
|
+
for (let i = 0; i < lines.length; i++) {
|
|
145
|
+
if (!lines[i].trim()) continue;
|
|
146
|
+
let d;
|
|
147
|
+
try { d = JSON.parse(lines[i]); }
|
|
148
|
+
catch { if (i !== lines.length - 1) return "an unparseable controller transcript line"; continue; }
|
|
149
|
+
const ms = typeof d?.timestamp === "string" ? Date.parse(d.timestamp) : NaN;
|
|
150
|
+
if (Number.isFinite(ms)) stamps.push(ms);
|
|
151
|
+
}
|
|
152
|
+
return stamps;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Every gap over HALT_MS between consecutive instants, as [from, to].
|
|
156
|
+
export function haltsOf(stamps) {
|
|
157
|
+
const sorted = [...stamps].sort((a, b) => a - b);
|
|
158
|
+
const halts = [];
|
|
159
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
160
|
+
if (sorted[i] - sorted[i - 1] > HALT_MS) halts.push([sorted[i - 1], sorted[i]]);
|
|
161
|
+
}
|
|
162
|
+
return halts;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// The parts' summed length less each halt's share inside them, in whole seconds.
|
|
166
|
+
export function workSeconds(parts, halts) {
|
|
167
|
+
let ms = 0;
|
|
168
|
+
for (const [from, to] of parts) {
|
|
169
|
+
if (!(to > from)) continue;
|
|
170
|
+
ms += to - from;
|
|
171
|
+
for (const [h0, h1] of halts) ms -= Math.max(0, Math.min(to, h1) - Math.max(from, h0));
|
|
172
|
+
}
|
|
173
|
+
return Math.round(ms / 1000);
|
|
174
|
+
}
|
|
175
|
+
|
|
99
176
|
// The `dispatch_model` column: the model fixed at dispatch, `session_init`'s
|
|
100
177
|
// own model (the record's `resolvedModelIdentity`), with its provider prefix
|
|
101
178
|
// and `:<level>` suffix stripped. `model` is the member's last turn, which a
|
|
@@ -120,7 +197,7 @@ export const COLUMNS = [
|
|
|
120
197
|
"session", "run_date", "role", "member", "model", "effort", "ticket", "pr",
|
|
121
198
|
"tokens_cache_create", "tokens_out", "wall_s", "turns", "agent", "harness",
|
|
122
199
|
"subagent_type", "report", "tokens_in", "tokens_cache_read", "tokens_cache_write_1h", "cost",
|
|
123
|
-
"ending", "dispatch_model",
|
|
200
|
+
"ending", "dispatch_model", "work_s",
|
|
124
201
|
];
|
|
125
202
|
|
|
126
203
|
// `report`: what reportStatus() says of the row's member under `scratch` — a
|
|
@@ -235,7 +312,7 @@ function verifyMainWithGit(sha) {
|
|
|
235
312
|
const FIELD = {
|
|
236
313
|
tokens_cache_create: "tokensCacheCreate", tokens_out: "tokensOut", wall_s: "wallS",
|
|
237
314
|
subagent_type: "subagentType", tokens_in: "tokensIn", tokens_cache_read: "tokensCacheRead",
|
|
238
|
-
tokens_cache_write_1h: "tokensCacheWrite1h", dispatch_model: "dispatchModel",
|
|
315
|
+
tokens_cache_write_1h: "tokensCacheWrite1h", dispatch_model: "dispatchModel", work_s: "workS",
|
|
239
316
|
};
|
|
240
317
|
const field = (c) => FIELD[c] ?? c;
|
|
241
318
|
// Keyed on the transcript's path-relative stem, not the member name: `member`
|
|
@@ -249,20 +326,30 @@ const key = (r) => `${r.session}\0${r.agent}`;
|
|
|
249
326
|
// regeneration both safe, and it is the reason no backfill-only code path is
|
|
250
327
|
// needed: the idempotency backfill wants is the idempotency a re-run wants.
|
|
251
328
|
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
329
|
+
// Two cells are the exception, each keeping what the replaced row recorded
|
|
330
|
+
// where this scrape could not say. An incoming `unknown` ending keeps the
|
|
331
|
+
// recorded ending: `unknown` is this scrape failing to say (a verifier that is
|
|
332
|
+
// offline, a transcript gone unreadable), not a reading, so it never
|
|
255
333
|
// overwrites one: a recorded `bail:fixed` would otherwise flip to `unknown`
|
|
256
|
-
// on any re-scrape run without network. `stats.endingsKept` counts them.
|
|
334
|
+
// on any re-scrape run without network. `stats.endingsKept` counts them. An
|
|
335
|
+
// incoming `null` work_s (halts that could not be found) keeps a
|
|
336
|
+
// recorded `work_s` the same way; `stats.workSKept` counts those.
|
|
257
337
|
export function mergeRows(existing, incoming, stats = {}) {
|
|
258
338
|
const by = new Map((existing ?? []).map((r) => [key(r), r]));
|
|
259
339
|
stats.endingsKept = 0;
|
|
340
|
+
stats.workSKept = 0;
|
|
260
341
|
for (const r of incoming ?? []) {
|
|
261
342
|
const old = by.get(key(r));
|
|
343
|
+
let row = r;
|
|
262
344
|
if (r.ending === "unknown" && old?.ending && old.ending !== "unknown") {
|
|
263
|
-
|
|
345
|
+
row = { ...row, ending: old.ending };
|
|
264
346
|
stats.endingsKept++;
|
|
265
|
-
}
|
|
347
|
+
}
|
|
348
|
+
if (r.workS === null && (old?.workS ?? "") !== "") {
|
|
349
|
+
row = { ...row, workS: old.workS };
|
|
350
|
+
stats.workSKept++;
|
|
351
|
+
}
|
|
352
|
+
by.set(key(r), row);
|
|
266
353
|
}
|
|
267
354
|
return [...by.values()].sort((a, b) => key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0);
|
|
268
355
|
}
|
|
@@ -406,8 +493,12 @@ if (isCLI(import.meta.url)) {
|
|
|
406
493
|
// a healthy one — 0-of-2 was indistinguishable from full success.
|
|
407
494
|
const torn = stats.torn ? `, ${stats.torn} torn mid-write` : "";
|
|
408
495
|
const kept = stats.endingsKept ? `, ${stats.endingsKept} read unknown and kept the ending already recorded` : "";
|
|
496
|
+
// Whatever left every row's `work_s` unknown, and how many rows kept the
|
|
497
|
+
// value already recorded, after the yield rather than inside it.
|
|
498
|
+
const workKept = stats.workSKept ? `, ${stats.workSKept} kept the work_s already recorded` : "";
|
|
499
|
+
const work = stats.workSUnknown ? `; work_s unknown (${stats.workSUnknown})${workKept}` : "";
|
|
409
500
|
process.stderr.write(
|
|
410
501
|
`${NAME}: scraped ${stats.seen - stats.dropped} of ${stats.seen} members ` +
|
|
411
|
-
`(${stats.dropped} dropped${torn}${kept}); ${file} now holds ${merged.length} rows\n`,
|
|
502
|
+
`(${stats.dropped} dropped${torn}${kept}); ${file} now holds ${merged.length} rows${work}\n`,
|
|
412
503
|
);
|
|
413
504
|
}
|
|
@@ -358,6 +358,13 @@ function createdPrNumbers(text) {
|
|
|
358
358
|
export function foldOmpTranscript(jsonlText, filePath) {
|
|
359
359
|
let model = null, thinking = null, task = null, resolvedModelIdentity = null, agent = null;
|
|
360
360
|
let firstTs = null, lastTs = null;
|
|
361
|
+
// For the `work_s` column (member-outcomes.mjs): every timestamped entry's
|
|
362
|
+
// instant, and the member's working parts — each run of timestamped entries
|
|
363
|
+
// between two `session_exit` entries, as [first, last] in epoch ms. The
|
|
364
|
+
// `session_exit` entry itself closes a part and never extends one, so the
|
|
365
|
+
// idle wait before it and the wait until the next entry are in no part.
|
|
366
|
+
const stamps = [], workParts = [];
|
|
367
|
+
let partFirst = null, partLast = null;
|
|
361
368
|
let input = 0, cacheWrite = 0, cacheWrite1h = 0, cacheRead = 0, output = 0, cost = 0, turns = 0;
|
|
362
369
|
let sawCost = false, unsplit = false, unpriced = false;
|
|
363
370
|
let malformedNonLastLines = 0;
|
|
@@ -383,10 +390,16 @@ export function foldOmpTranscript(jsonlText, filePath) {
|
|
|
383
390
|
try { d = JSON.parse(raw); }
|
|
384
391
|
catch { if (i !== lines.length - 1) malformedNonLastLines++; continue; }
|
|
385
392
|
assertOmpShaped(d, filePath);
|
|
386
|
-
|
|
393
|
+
let ms = NaN;
|
|
394
|
+
if (typeof d.timestamp === "string") { firstTs ??= d.timestamp; lastTs = d.timestamp; ms = Date.parse(d.timestamp); }
|
|
395
|
+
if (Number.isFinite(ms)) stamps.push(ms);
|
|
396
|
+
if (d.type === "custom" && d.customType === "session_exit") {
|
|
397
|
+
sessionExit = true;
|
|
398
|
+
if (partFirst !== null) workParts.push([partFirst, partLast]);
|
|
399
|
+
partFirst = partLast = null;
|
|
400
|
+
} else if (Number.isFinite(ms)) { partFirst ??= ms; partLast = ms; }
|
|
387
401
|
if (d.type === "thinking_level_change" && typeof d.thinkingLevel === "string") thinking = d.thinkingLevel;
|
|
388
402
|
if (d.type === "model_change" && d.resolvedModelIsFallback === true) providerFallback = true;
|
|
389
|
-
if (d.type === "custom" && d.customType === "session_exit") sessionExit = true;
|
|
390
403
|
if (d.type === "session_init") {
|
|
391
404
|
if (typeof d.task === "string") task = d.task;
|
|
392
405
|
if (typeof d.resolvedModelIdentity === "string") resolvedModelIdentity = d.resolvedModelIdentity;
|
|
@@ -433,6 +446,7 @@ export function foldOmpTranscript(jsonlText, filePath) {
|
|
|
433
446
|
}
|
|
434
447
|
if (d.type === "message" && m?.role === "user" && messageText(m).trimStart().startsWith("[budget notice]")) budgetNotice = true;
|
|
435
448
|
}
|
|
449
|
+
if (partFirst !== null) workParts.push([partFirst, partLast]);
|
|
436
450
|
const span = firstTs && lastTs ? (Date.parse(lastTs) - Date.parse(firstTs)) / 1000 : 0;
|
|
437
451
|
return {
|
|
438
452
|
model, thinking, task, resolvedModelIdentity, agent,
|
|
@@ -453,6 +467,7 @@ export function foldOmpTranscript(jsonlText, filePath) {
|
|
|
453
467
|
malformedNonLastLines,
|
|
454
468
|
openedPrs: [...openedPrs],
|
|
455
469
|
startedAt: firstTs,
|
|
470
|
+
stamps, workParts,
|
|
456
471
|
reportWrites,
|
|
457
472
|
lastStopReason, providerFallback, sessionExit, budgetNotice,
|
|
458
473
|
};
|
|
@@ -867,7 +882,13 @@ export function ompSessionTranscripts(sessionDir) {
|
|
|
867
882
|
// wrong-shape refusal (assertOmpShaped, inside foldOmpTranscript) must
|
|
868
883
|
// propagate all the way out of readMembers, uncaught. Only the directory
|
|
869
884
|
// listing and the raw file read are given the ordinary per-member tolerance.
|
|
870
|
-
|
|
885
|
+
//
|
|
886
|
+
// `activity`, when given, is `{ stamps: [], unread: 0 }`: `stamps` collects the
|
|
887
|
+
// entry instants of EVERY transcript read here, including one that yields no
|
|
888
|
+
// record, and `unread` counts the transcripts that could not be read at all,
|
|
889
|
+
// whose instants are missing from `stamps`. It is the session-wide view
|
|
890
|
+
// member-outcomes.mjs reads halts off.
|
|
891
|
+
export function readOmpSession(sessionDir, activity = null) {
|
|
871
892
|
let transcripts;
|
|
872
893
|
try { transcripts = ompSessionTranscripts(sessionDir); }
|
|
873
894
|
catch { return []; }
|
|
@@ -876,19 +897,21 @@ export function readOmpSession(sessionDir) {
|
|
|
876
897
|
for (const { file, agent, spawnDepth } of transcripts) {
|
|
877
898
|
let text;
|
|
878
899
|
try { text = readFileSync(file, "utf8"); }
|
|
879
|
-
catch { continue; }
|
|
900
|
+
catch { if (activity) activity.unread++; continue; }
|
|
880
901
|
const folded = foldOmpTranscript(text, file); // may throw — see comment above
|
|
902
|
+
if (activity) for (const ms of folded.stamps) activity.stamps.push(ms);
|
|
881
903
|
const rec = ompMemberRecord(folded, agent, spawnDepth);
|
|
882
904
|
if (!rec) continue;
|
|
883
905
|
// How the transcript ENDED, for the `ending` column (member-outcomes.mjs):
|
|
884
906
|
// the fold's end-of-transcript signals, and whether the member wrote its
|
|
885
|
-
// yield sidecar beside the transcript
|
|
907
|
+
// yield sidecar beside the transcript; and its working parts, for `work_s`.
|
|
886
908
|
rows.push({ ...rec, session, agent, transcript: {
|
|
887
909
|
startedAt: folded.startedAt, reportWrites: folded.reportWrites, openedPrs: folded.openedPrs,
|
|
888
910
|
lastStopReason: folded.lastStopReason, providerFallback: folded.providerFallback,
|
|
889
911
|
sessionExit: folded.sessionExit, budgetNotice: folded.budgetNotice,
|
|
890
912
|
malformedNonLastLines: folded.malformedNonLastLines,
|
|
891
913
|
hasYield: existsSync(file.replace(/\.jsonl$/, ".md")),
|
|
914
|
+
timed: folded.stamps.length > 0, workParts: folded.workParts,
|
|
892
915
|
} });
|
|
893
916
|
}
|
|
894
917
|
return rows;
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
// `$.bailCause: "<cause>" is not null, with outcome "blocked" — a block
|
|
26
26
|
// is no bail`, gives no reason in `bailDetail`, or names a `pr` or a
|
|
27
27
|
// `head`, as `$.pr: <pr> is not null, with outcome "blocked" — a block
|
|
28
|
-
// reports no PR`
|
|
28
|
+
// reports no PR`, or whose `bailed` outcome names a `pr` or a `head`, as
|
|
29
|
+
// `$.pr: <pr> is not null, with outcome "bailed" — a bail reports no PR`
|
|
29
30
|
// 2 usage: no or unknown subcommand, a wrong argument count, an unknown
|
|
30
31
|
// family, any flag; or <file> could not be read; or print could not
|
|
31
32
|
// write the schema to stdout. Refused on stderr, nothing on stdout
|
|
@@ -185,10 +186,11 @@ export function mergeBotVerdicts(report) {
|
|
|
185
186
|
// no schema keyword states. For an impl report: a `blocked` outcome names no
|
|
186
187
|
// `bailCause`, since a block is no bail, gives its reason in `bailDetail`,
|
|
187
188
|
// text that is not only whitespace, and names no `pr` or `head`, since a block
|
|
188
|
-
// reports no PR
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
// or
|
|
189
|
+
// reports no PR; a `bailed` outcome names no `pr` or `head` either, since a
|
|
190
|
+
// bail is the pre-code outcome — a PR the member opened is outcome `pr`, which
|
|
191
|
+
// settle checks. For a merge-bot report: each PR gets one verdict — merged,
|
|
192
|
+
// held, conflict-held or refused — the last it reached in the pass, so a PR
|
|
193
|
+
// named twice, in one list or in two, is a violation at its second entry.
|
|
192
194
|
export function reportViolation(family, value) {
|
|
193
195
|
const found = validate(SCHEMAS[family], value);
|
|
194
196
|
if (found !== null) return found;
|
|
@@ -199,6 +201,11 @@ export function reportViolation(family, value) {
|
|
|
199
201
|
if (value[field] !== null) return `$.${field}: ${show(value[field])} is not null, with outcome "blocked" — a block reports no PR`;
|
|
200
202
|
}
|
|
201
203
|
}
|
|
204
|
+
if (family === "impl" && value.outcome === "bailed") {
|
|
205
|
+
for (const field of ["pr", "head"]) {
|
|
206
|
+
if (value[field] !== null) return `$.${field}: ${show(value[field])} is not null, with outcome "bailed" — a bail reports no PR`;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
202
209
|
if (family !== "merge-bot") return null;
|
|
203
210
|
const first = new Map();
|
|
204
211
|
for (const { pr, path } of mergeBotVerdicts(value)) {
|