@bluewombat/runtime 0.9.0 → 0.11.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/README.md +200 -99
- package/SPECS.md +121 -19
- package/dist/cli.js +35 -4
- package/dist/cli.js.map +1 -1
- package/dist/config/agent-shorthand.d.ts +25 -5
- package/dist/config/agent-shorthand.d.ts.map +1 -1
- package/dist/config/agent-shorthand.js +65 -21
- package/dist/config/agent-shorthand.js.map +1 -1
- package/dist/config/fixture-yaml.d.ts +8 -0
- package/dist/config/fixture-yaml.d.ts.map +1 -0
- package/dist/config/fixture-yaml.js +30 -0
- package/dist/config/fixture-yaml.js.map +1 -0
- package/dist/config/home.d.ts +2 -0
- package/dist/config/home.d.ts.map +1 -1
- package/dist/config/home.js +2 -0
- package/dist/config/home.js.map +1 -1
- package/dist/config/load-config.d.ts.map +1 -1
- package/dist/config/load-config.js +301 -62
- package/dist/config/load-config.js.map +1 -1
- package/dist/config/parse-args.d.ts +1 -1
- package/dist/config/parse-args.d.ts.map +1 -1
- package/dist/config/parse-args.js +84 -58
- package/dist/config/parse-args.js.map +1 -1
- package/dist/config/types.d.ts +96 -16
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +13 -1
- package/dist/config/types.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/loop/authority.d.ts +38 -1
- package/dist/loop/authority.d.ts.map +1 -1
- package/dist/loop/authority.js +41 -2
- package/dist/loop/authority.js.map +1 -1
- package/dist/loop/context.d.ts +16 -1
- package/dist/loop/context.d.ts.map +1 -1
- package/dist/loop/drive.d.ts +9 -3
- package/dist/loop/drive.d.ts.map +1 -1
- package/dist/loop/drive.js +17 -7
- package/dist/loop/drive.js.map +1 -1
- package/dist/loop/features.d.ts +16 -0
- package/dist/loop/features.d.ts.map +1 -1
- package/dist/loop/features.js +404 -62
- package/dist/loop/features.js.map +1 -1
- package/dist/loop/open-host.d.ts +10 -0
- package/dist/loop/open-host.d.ts.map +1 -1
- package/dist/loop/open-host.js +111 -9
- package/dist/loop/open-host.js.map +1 -1
- package/dist/loop/report.d.ts.map +1 -1
- package/dist/loop/report.js +21 -9
- package/dist/loop/report.js.map +1 -1
- package/dist/loop/streams.d.ts +3 -0
- package/dist/loop/streams.d.ts.map +1 -1
- package/dist/loop/streams.js +2 -1
- package/dist/loop/streams.js.map +1 -1
- package/dist/loop/tick.d.ts +3 -0
- package/dist/loop/tick.d.ts.map +1 -1
- package/dist/loop/tick.js.map +1 -1
- package/dist/loop/trace.d.ts +1 -0
- package/dist/loop/trace.d.ts.map +1 -1
- package/dist/loop/trace.js +2 -2
- package/dist/loop/trace.js.map +1 -1
- package/dist/loop/transformers.d.ts +16 -2
- package/dist/loop/transformers.d.ts.map +1 -1
- package/dist/loop/transformers.js +126 -22
- package/dist/loop/transformers.js.map +1 -1
- package/dist/loop/warm.js +1 -1
- package/dist/loop/warm.js.map +1 -1
- package/dist/operator/agent-plugins.d.ts.map +1 -1
- package/dist/operator/agent-plugins.js +14 -8
- package/dist/operator/agent-plugins.js.map +1 -1
- package/dist/operator/board.d.ts +43 -4
- package/dist/operator/board.d.ts.map +1 -1
- package/dist/operator/board.js +153 -10
- package/dist/operator/board.js.map +1 -1
- package/dist/operator/doctor.d.ts +1 -1
- package/dist/operator/doctor.d.ts.map +1 -1
- package/dist/operator/doctor.js +76 -29
- package/dist/operator/doctor.js.map +1 -1
- package/dist/operator/explain.d.ts.map +1 -1
- package/dist/operator/explain.js +36 -12
- package/dist/operator/explain.js.map +1 -1
- package/dist/operator/film.js +0 -3
- package/dist/operator/film.js.map +1 -1
- package/dist/operator/incident-dossier.d.ts +154 -0
- package/dist/operator/incident-dossier.d.ts.map +1 -0
- package/dist/operator/incident-dossier.js +696 -0
- package/dist/operator/incident-dossier.js.map +1 -0
- package/dist/operator/incident-system.d.ts +48 -0
- package/dist/operator/incident-system.d.ts.map +1 -0
- package/dist/operator/incident-system.js +152 -0
- package/dist/operator/incident-system.js.map +1 -0
- package/dist/operator/incident.d.ts +27 -0
- package/dist/operator/incident.d.ts.map +1 -0
- package/dist/operator/incident.js +220 -0
- package/dist/operator/incident.js.map +1 -0
- package/dist/operator/init.js +27 -22
- package/dist/operator/init.js.map +1 -1
- package/dist/operator/live.js +4 -4
- package/dist/operator/live.js.map +1 -1
- package/dist/operator/log.d.ts +2 -0
- package/dist/operator/log.d.ts.map +1 -1
- package/dist/operator/log.js +7 -3
- package/dist/operator/log.js.map +1 -1
- package/dist/operator/restart.d.ts +20 -0
- package/dist/operator/restart.d.ts.map +1 -0
- package/dist/operator/restart.js +63 -0
- package/dist/operator/restart.js.map +1 -0
- package/dist/operator/setup.d.ts.map +1 -1
- package/dist/operator/setup.js +2 -0
- package/dist/operator/setup.js.map +1 -1
- package/package.json +14 -14
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { slug } from "../loop/streams.js";
|
|
4
|
+
/**
|
|
5
|
+
* What `mason incident` hands to whoever has to understand a stop.
|
|
6
|
+
*
|
|
7
|
+
* The WorkLedger says that something stopped and, in a sentence, why. The
|
|
8
|
+
* journal says who answered what, in order. The streams and transcripts hold
|
|
9
|
+
* what the checks and the agents said. Reading all of them to find one stop is
|
|
10
|
+
* the work this does once, with no judgement: it never decides why the work
|
|
11
|
+
* stopped, it puts the facts side by side and says which ones it could not find.
|
|
12
|
+
*/
|
|
13
|
+
export const INCIDENT_SCHEMA = "bluewombat.incident/1";
|
|
14
|
+
/** Longest text kept inline; the rest stays in the file it came from. */
|
|
15
|
+
const REPORT_LIMIT = 4000;
|
|
16
|
+
const EXCERPT_LIMIT = 3000;
|
|
17
|
+
const TAIL_BYTES = 64_000;
|
|
18
|
+
export function isFeature(subject) {
|
|
19
|
+
return "kind" in subject && subject.kind === "feature";
|
|
20
|
+
}
|
|
21
|
+
export function buildDossier(input) {
|
|
22
|
+
const gaps = [];
|
|
23
|
+
const subject = input.subject;
|
|
24
|
+
const key = subject.intention.key;
|
|
25
|
+
const feature = isFeature(subject);
|
|
26
|
+
const mine = linesOf(input.journal, key);
|
|
27
|
+
const stopIndex = lastIndex(mine, (line) => line.event === "status" && line.phase === "escalated");
|
|
28
|
+
const previous = stopIndex < 0
|
|
29
|
+
? -1
|
|
30
|
+
: lastIndex(mine.slice(0, stopIndex), (line) => line.event === "status" && line.phase === "escalated");
|
|
31
|
+
const escalations = mine.filter((line) => line.event === "status" && line.phase === "escalated").length;
|
|
32
|
+
const end = stopIndex < 0 ? mine.length : stopIndex + 1;
|
|
33
|
+
const round = mine.slice(previous + 1, end);
|
|
34
|
+
if (input.journal.length === 0) {
|
|
35
|
+
gaps.push("No journal: who answered what, and when, is not known.");
|
|
36
|
+
}
|
|
37
|
+
else if (mine.length === 0) {
|
|
38
|
+
gaps.push(`The journal holds no line for ${key}.`);
|
|
39
|
+
}
|
|
40
|
+
else if (stopIndex < 0) {
|
|
41
|
+
gaps.push("The journal has no line saying the work escalated; the moment comes from the ledger only.");
|
|
42
|
+
}
|
|
43
|
+
const escalation = subject.escalation;
|
|
44
|
+
const stopLine = stopIndex < 0 ? undefined : mine[stopIndex];
|
|
45
|
+
const gate = failedGate(round);
|
|
46
|
+
const agent = failedAgent(round);
|
|
47
|
+
const sources = [];
|
|
48
|
+
if (escalation !== undefined) {
|
|
49
|
+
sources.push("ledger");
|
|
50
|
+
}
|
|
51
|
+
else if (subject.state === "escalated") {
|
|
52
|
+
gaps.push("The ledger holds no escalation record for this subject.");
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
gaps.push(`The subject is ${subject.state}, not escalated: the stop shown is the last refusal.`);
|
|
56
|
+
}
|
|
57
|
+
if (gate !== undefined || agent !== undefined || stopLine !== undefined) {
|
|
58
|
+
sources.push("journal");
|
|
59
|
+
}
|
|
60
|
+
const report = clip(escalation?.report ?? subject.parked_refusal?.report, REPORT_LIMIT);
|
|
61
|
+
const at = escalatedAt(escalation?.at, stopLine);
|
|
62
|
+
const stop = { layer: layerOf(subject), sources };
|
|
63
|
+
if (report !== undefined) {
|
|
64
|
+
stop.report = report;
|
|
65
|
+
}
|
|
66
|
+
const refusedBy = refusedByOf(escalation, gate, report);
|
|
67
|
+
if (refusedBy !== undefined) {
|
|
68
|
+
stop.refused_by = refusedBy;
|
|
69
|
+
}
|
|
70
|
+
if (at !== undefined) {
|
|
71
|
+
stop.at = at;
|
|
72
|
+
}
|
|
73
|
+
if (stopLine !== undefined) {
|
|
74
|
+
setText(stop, "task_id", stopLine.task_id);
|
|
75
|
+
setNumber(stop, "attempt", stopLine.attempt);
|
|
76
|
+
}
|
|
77
|
+
if (gate !== undefined) {
|
|
78
|
+
stop.gate = {
|
|
79
|
+
id: gate.gate_id,
|
|
80
|
+
verdict: gate.verdict,
|
|
81
|
+
report: clip(gate.report, REPORT_LIMIT) ?? "",
|
|
82
|
+
at: gate.at,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (agent !== undefined) {
|
|
86
|
+
stop.agent = agent;
|
|
87
|
+
}
|
|
88
|
+
const tickets = rowsOf(input.tickets, input.journal);
|
|
89
|
+
const dossier = {
|
|
90
|
+
schema: INCIDENT_SCHEMA,
|
|
91
|
+
generated_at: input.now.toISOString(),
|
|
92
|
+
subject: {
|
|
93
|
+
key,
|
|
94
|
+
kind: feature ? "feature" : "ticket",
|
|
95
|
+
project: subject.intention.project,
|
|
96
|
+
state: subject.state,
|
|
97
|
+
intention: head(subject.intention.intention, 12),
|
|
98
|
+
},
|
|
99
|
+
stop,
|
|
100
|
+
history: { escalations, resumes: subject.resumes },
|
|
101
|
+
budgets: budgetsOf(subject),
|
|
102
|
+
timeline: round.map(timelineRow).filter((row) => row !== undefined),
|
|
103
|
+
artifacts: { streams: [], transcripts: [] },
|
|
104
|
+
gaps,
|
|
105
|
+
};
|
|
106
|
+
if (subject.intention.title !== undefined) {
|
|
107
|
+
dossier.subject.title = subject.intention.title;
|
|
108
|
+
}
|
|
109
|
+
const source = subject.intention.source?.ref;
|
|
110
|
+
if (source !== undefined) {
|
|
111
|
+
dossier.subject.source = source;
|
|
112
|
+
}
|
|
113
|
+
const blockedBy = subject.intention.blocked_by;
|
|
114
|
+
if (blockedBy !== undefined && blockedBy.length > 0) {
|
|
115
|
+
dossier.subject.blocked_by = blockedBy;
|
|
116
|
+
}
|
|
117
|
+
if (feature) {
|
|
118
|
+
dossier.tickets = tickets;
|
|
119
|
+
}
|
|
120
|
+
else if (input.parent !== undefined) {
|
|
121
|
+
dossier.parent = {
|
|
122
|
+
key: input.parent.intention.key,
|
|
123
|
+
kind: "feature",
|
|
124
|
+
state: input.parent.state,
|
|
125
|
+
tickets,
|
|
126
|
+
...(input.parent.intention.title === undefined
|
|
127
|
+
? {}
|
|
128
|
+
: { title: input.parent.intention.title }),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (input.invocation !== undefined && gate !== undefined) {
|
|
132
|
+
const configured = gateConfigOf(input.invocation, gate.gate_id);
|
|
133
|
+
if (configured.length > 0) {
|
|
134
|
+
dossier.gate_config = configured;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
gaps.push(`Gate ${gate.gate_id} is not in the current configuration.`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (input.system !== undefined) {
|
|
141
|
+
dossier.system = input.system;
|
|
142
|
+
}
|
|
143
|
+
collectStreams(dossier, round, input.streamsDir, gate, gaps);
|
|
144
|
+
collectTranscripts(dossier, key, input.transcriptsDir, gaps);
|
|
145
|
+
return dossier;
|
|
146
|
+
}
|
|
147
|
+
/** Lines that belong to a key: its own, and those of the Tasks filed under it. */
|
|
148
|
+
function linesOf(journal, key) {
|
|
149
|
+
return journal.filter((line) => {
|
|
150
|
+
if (line.key === key) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
return (typeof line.task_id === "string" &&
|
|
154
|
+
(line.task_id === key || line.task_id.startsWith(`${key}:`)));
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function lastIndex(items, test) {
|
|
158
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
159
|
+
if (test(items[i])) {
|
|
160
|
+
return i;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return -1;
|
|
164
|
+
}
|
|
165
|
+
/** The last check that did not pass, before the stop. */
|
|
166
|
+
function failedGate(round) {
|
|
167
|
+
const index = lastIndex(round, (line) => line.event === "gate-finished" &&
|
|
168
|
+
typeof line.gate_id === "string" &&
|
|
169
|
+
typeof line.verdict === "string" &&
|
|
170
|
+
line.verdict !== "pass");
|
|
171
|
+
const line = index < 0 ? undefined : round[index];
|
|
172
|
+
if (line === undefined) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
const found = {
|
|
176
|
+
gate_id: String(line.gate_id),
|
|
177
|
+
verdict: String(line.verdict),
|
|
178
|
+
report: typeof line.report === "string" ? line.report : "",
|
|
179
|
+
at: typeof line.at === "string" ? line.at : "",
|
|
180
|
+
};
|
|
181
|
+
if (typeof line.task_id === "string") {
|
|
182
|
+
found.task_id = line.task_id;
|
|
183
|
+
}
|
|
184
|
+
return found;
|
|
185
|
+
}
|
|
186
|
+
/** The last agent that did not complete, before the stop. */
|
|
187
|
+
function failedAgent(round) {
|
|
188
|
+
const index = lastIndex(round, (line) => line.event === "builder-finished" && failureOf(line) !== undefined);
|
|
189
|
+
const line = index < 0 ? undefined : round[index];
|
|
190
|
+
if (line === undefined || typeof line.task_id !== "string") {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
const agent = {
|
|
194
|
+
task_id: line.task_id,
|
|
195
|
+
failure: failureOf(line) ?? "",
|
|
196
|
+
at: typeof line.at === "string" ? line.at : "",
|
|
197
|
+
};
|
|
198
|
+
if (typeof line.attempt === "number") {
|
|
199
|
+
agent.attempt = line.attempt;
|
|
200
|
+
}
|
|
201
|
+
return agent;
|
|
202
|
+
}
|
|
203
|
+
function failureOf(line) {
|
|
204
|
+
const result = line.result;
|
|
205
|
+
if (result === null || typeof result !== "object") {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
const kind = result.kind;
|
|
209
|
+
if (typeof kind !== "string" || kind === "completed" || kind === "skipped") {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
const reason = result.reason;
|
|
213
|
+
return typeof reason === "string" && reason.length > 0 ? `${kind}: ${clip(reason, 500)}` : kind;
|
|
214
|
+
}
|
|
215
|
+
function layerOf(subject) {
|
|
216
|
+
if (subject.escalation === undefined) {
|
|
217
|
+
return subject.state === "escalated" ? "unknown" : "none";
|
|
218
|
+
}
|
|
219
|
+
return isFeature(subject) ? "feature" : subject.escalation.kind;
|
|
220
|
+
}
|
|
221
|
+
function refusedByOf(escalation, gate, report) {
|
|
222
|
+
if (escalation?.refused_by !== undefined) {
|
|
223
|
+
return escalation.refused_by;
|
|
224
|
+
}
|
|
225
|
+
// An older record names nobody: take the check whose words the ledger repeats.
|
|
226
|
+
if (gate !== undefined && gate.report.length > 0 && report?.includes(gate.report.slice(0, 80))) {
|
|
227
|
+
return gate.gate_id;
|
|
228
|
+
}
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
function escalatedAt(ledgerAt, line) {
|
|
232
|
+
if (typeof line?.at === "string") {
|
|
233
|
+
return line.at;
|
|
234
|
+
}
|
|
235
|
+
return ledgerAt === undefined ? undefined : new Date(ledgerAt).toISOString();
|
|
236
|
+
}
|
|
237
|
+
function budgetsOf(subject) {
|
|
238
|
+
const budgets = {};
|
|
239
|
+
if (!isFeature(subject)) {
|
|
240
|
+
budgets.attempts_used = subject.attempts_used;
|
|
241
|
+
}
|
|
242
|
+
const submission = subject.submission;
|
|
243
|
+
if (submission?.refusals !== undefined) {
|
|
244
|
+
budgets.refusals = submission.refusals;
|
|
245
|
+
}
|
|
246
|
+
if (subject.parked_refusals !== undefined) {
|
|
247
|
+
budgets.parked_refusals = subject.parked_refusals;
|
|
248
|
+
}
|
|
249
|
+
if (submission?.realigns !== undefined) {
|
|
250
|
+
budgets.realigns = submission.realigns;
|
|
251
|
+
}
|
|
252
|
+
return budgets;
|
|
253
|
+
}
|
|
254
|
+
function rowsOf(tickets, journal) {
|
|
255
|
+
return tickets.map((ticket) => {
|
|
256
|
+
const intention = ticket.intention;
|
|
257
|
+
const row = {
|
|
258
|
+
key: intention.key,
|
|
259
|
+
state: ticket.state,
|
|
260
|
+
attempts_used: ticket.attempts_used,
|
|
261
|
+
};
|
|
262
|
+
if (intention.title !== undefined) {
|
|
263
|
+
row.title = intention.title;
|
|
264
|
+
}
|
|
265
|
+
const reference = ticket.submission?.reference;
|
|
266
|
+
if (reference !== undefined) {
|
|
267
|
+
row.reference = reference;
|
|
268
|
+
}
|
|
269
|
+
if (intention.featureTest === true) {
|
|
270
|
+
row.featureTest = true;
|
|
271
|
+
}
|
|
272
|
+
if (intention.blocked_by !== undefined && intention.blocked_by.length > 0) {
|
|
273
|
+
row.blocked_by = intention.blocked_by;
|
|
274
|
+
}
|
|
275
|
+
const doneAt = journal.find((line) => line.event === "reported" && line.reported === "done" && line.key === intention.key)?.at;
|
|
276
|
+
if (typeof doneAt === "string") {
|
|
277
|
+
row.done_at = doneAt;
|
|
278
|
+
}
|
|
279
|
+
if (ticket.escalation !== undefined) {
|
|
280
|
+
row.escalation = { kind: ticket.escalation.kind };
|
|
281
|
+
if (ticket.escalation.report !== undefined) {
|
|
282
|
+
row.escalation.report = clip(ticket.escalation.report, 500) ?? "";
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return row;
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
const TIMELINE_EVENTS = new Set([
|
|
289
|
+
"isolation-finished",
|
|
290
|
+
"integration-finished",
|
|
291
|
+
"attempt-finished",
|
|
292
|
+
"builder-finished",
|
|
293
|
+
"gate-finished",
|
|
294
|
+
"submitted",
|
|
295
|
+
"submit-refused",
|
|
296
|
+
"invalid",
|
|
297
|
+
"skipped",
|
|
298
|
+
"run-finished",
|
|
299
|
+
]);
|
|
300
|
+
function timelineRow(line) {
|
|
301
|
+
const event = typeof line.event === "string" ? line.event : undefined;
|
|
302
|
+
if (event === undefined || typeof line.at !== "string") {
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
// The phases of a status are said again by the event that ends them; only the stop is new.
|
|
306
|
+
const stopStatus = event === "status" && line.phase === "escalated";
|
|
307
|
+
if (!TIMELINE_EVENTS.has(event) && !stopStatus) {
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
const row = { at: line.at, event };
|
|
311
|
+
setText(row, "task_id", line.task_id);
|
|
312
|
+
setNumber(row, "attempt", line.attempt);
|
|
313
|
+
setText(row, "gate_id", line.gate_id);
|
|
314
|
+
setText(row, "verdict", line.verdict);
|
|
315
|
+
const detail = detailOf(line);
|
|
316
|
+
if (detail !== undefined) {
|
|
317
|
+
row.detail = detail;
|
|
318
|
+
}
|
|
319
|
+
return row;
|
|
320
|
+
}
|
|
321
|
+
function detailOf(line) {
|
|
322
|
+
for (const name of ["report", "label", "outcome", "ended", "reason", "reference"]) {
|
|
323
|
+
const value = line[name];
|
|
324
|
+
if (typeof value === "string" && value.length > 0) {
|
|
325
|
+
return clip(value, 300);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return failureOf(line);
|
|
329
|
+
}
|
|
330
|
+
function gateConfigOf(invocation, id) {
|
|
331
|
+
const found = [];
|
|
332
|
+
const sections = [
|
|
333
|
+
["ticket.build.gates", invocation.builder.producer.gates],
|
|
334
|
+
["ticket.build.repair.gates", invocation.builder.repair.gates],
|
|
335
|
+
["feature.test.gates", invocation.builder.featureTest?.gates],
|
|
336
|
+
["ticket.fix.gates", invocation.ticket.fix?.gates],
|
|
337
|
+
["ticket.validate.gates", invocation.ticket.validate?.gates],
|
|
338
|
+
["feature.fix.gates", invocation.feature?.fix.gates],
|
|
339
|
+
["feature.validate.gates", invocation.feature?.validate.gates],
|
|
340
|
+
["feature.gates", invocation.feature?.gates],
|
|
341
|
+
];
|
|
342
|
+
for (const [section, gates] of sections) {
|
|
343
|
+
for (const gate of gates ?? []) {
|
|
344
|
+
if (gate.id === id) {
|
|
345
|
+
found.push({
|
|
346
|
+
section,
|
|
347
|
+
id,
|
|
348
|
+
argv: gate.argv,
|
|
349
|
+
...(gate.timeoutMs === undefined ? {} : { timeoutMs: gate.timeoutMs }),
|
|
350
|
+
...(gate.skipWhen === undefined ? {} : { skipWhen: gate.skipWhen }),
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return found;
|
|
356
|
+
}
|
|
357
|
+
function collectStreams(dossier, round, streamsDir, gate, gaps) {
|
|
358
|
+
const opened = round.filter((line) => line.event === "stream-opened" && typeof line.path === "string");
|
|
359
|
+
if (opened.length === 0) {
|
|
360
|
+
gaps.push("No stream recorded for this stop (streams are off unless the Project turns them on, or they were discarded).");
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const closed = new Map();
|
|
364
|
+
for (const line of round) {
|
|
365
|
+
if (line.event === "stream-closed" && typeof line.path === "string") {
|
|
366
|
+
closed.set(line.path, line);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
for (const line of opened) {
|
|
370
|
+
const path = String(line.path);
|
|
371
|
+
const absolute = streamsDir === undefined ? path : join(streamsDir, path);
|
|
372
|
+
const exists = streamsDir !== undefined && existsSync(absolute);
|
|
373
|
+
const row = { path: absolute, exists };
|
|
374
|
+
setText(row, "task_id", line.task_id);
|
|
375
|
+
setText(row, "gate_id", line.gate_id);
|
|
376
|
+
setNumber(row, "attempt", line.attempt);
|
|
377
|
+
setNumber(row, "bytes", closed.get(path)?.bytes);
|
|
378
|
+
if (exists &&
|
|
379
|
+
gate !== undefined &&
|
|
380
|
+
line.gate_id === gate.gate_id &&
|
|
381
|
+
line.task_id === gate.task_id) {
|
|
382
|
+
const text = streamText(absolute);
|
|
383
|
+
if (text !== undefined) {
|
|
384
|
+
row.excerpt = tail(text, EXCERPT_LIMIT);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
dossier.artifacts.streams.push(row);
|
|
388
|
+
}
|
|
389
|
+
if (streamsDir === undefined) {
|
|
390
|
+
gaps.push("The streams directory is not known, so no stream file could be read.");
|
|
391
|
+
}
|
|
392
|
+
else if (dossier.artifacts.streams.every((row) => !row.exists)) {
|
|
393
|
+
gaps.push("The streams recorded for this stop are no longer on disk.");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/** The `text` of each chunk of a stream file (NDJSON), in order. */
|
|
397
|
+
function streamText(path) {
|
|
398
|
+
const raw = readTail(path);
|
|
399
|
+
if (raw === undefined) {
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
let text = "";
|
|
403
|
+
for (const row of raw.split("\n")) {
|
|
404
|
+
if (row.length === 0) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
const chunk = JSON.parse(row).text;
|
|
409
|
+
text += typeof chunk === "string" ? chunk : "";
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// The first row of a tail can be cut in half.
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return text;
|
|
416
|
+
}
|
|
417
|
+
function collectTranscripts(dossier, key, transcriptsDir, gaps) {
|
|
418
|
+
if (transcriptsDir === undefined) {
|
|
419
|
+
gaps.push("The transcripts directory is not known.");
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const dir = join(transcriptsDir, slug(key));
|
|
423
|
+
let names;
|
|
424
|
+
try {
|
|
425
|
+
names = readdirSync(dir).filter((name) => name.endsWith(".md"));
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
gaps.push(`No transcripts under ${dir}: the agents of this subject, if any, left none.`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
names.sort();
|
|
432
|
+
for (const name of names) {
|
|
433
|
+
dossier.artifacts.transcripts.push(transcriptRow(join(dir, name)));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function transcriptRow(path) {
|
|
437
|
+
const row = { path };
|
|
438
|
+
const head = readHead(path);
|
|
439
|
+
if (head !== undefined) {
|
|
440
|
+
const title = /^# (.+)$/m.exec(head)?.[1];
|
|
441
|
+
if (title !== undefined) {
|
|
442
|
+
row.title = title.trim();
|
|
443
|
+
}
|
|
444
|
+
for (const name of ["slot", "started", "ended", "duration", "exit"]) {
|
|
445
|
+
const value = new RegExp(`^- ${name}: (.+)$`, "m").exec(head)?.[1];
|
|
446
|
+
if (value !== undefined) {
|
|
447
|
+
row[name] = value.trim();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const end = readTail(path);
|
|
452
|
+
if (end !== undefined) {
|
|
453
|
+
const marker = [...end.matchAll(/MASON_(?:VERDICT|BLOCKED):[^\n"\\]*/g)].pop()?.[0];
|
|
454
|
+
if (marker !== undefined) {
|
|
455
|
+
row.marker = marker.trim();
|
|
456
|
+
}
|
|
457
|
+
const message = finalMessage(end);
|
|
458
|
+
if (message !== undefined) {
|
|
459
|
+
row.final_message = tail(message, EXCERPT_LIMIT);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return row;
|
|
463
|
+
}
|
|
464
|
+
/** The agent's closing words: the `result` of a stream-json run, or nothing. */
|
|
465
|
+
function finalMessage(text) {
|
|
466
|
+
for (const row of text.split("\n").reverse()) {
|
|
467
|
+
if (!row.startsWith('{"type":"result"')) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
const result = JSON.parse(row).result;
|
|
472
|
+
return typeof result === "string" ? result : undefined;
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
return undefined;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return undefined;
|
|
479
|
+
}
|
|
480
|
+
function readHead(path) {
|
|
481
|
+
try {
|
|
482
|
+
return readFileSync(path, "utf8").slice(0, 3000);
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
return undefined;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function readTail(path) {
|
|
489
|
+
try {
|
|
490
|
+
const size = statSync(path).size;
|
|
491
|
+
const text = readFileSync(path, "utf8");
|
|
492
|
+
return size <= TAIL_BYTES ? text : text.slice(-TAIL_BYTES);
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return undefined;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function setText(target, name, value) {
|
|
499
|
+
if (typeof value === "string" && value.length > 0) {
|
|
500
|
+
target[name] = value;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function setNumber(target, name, value) {
|
|
504
|
+
if (typeof value === "number") {
|
|
505
|
+
target[name] = value;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function clip(text, limit) {
|
|
509
|
+
if (text === undefined) {
|
|
510
|
+
return undefined;
|
|
511
|
+
}
|
|
512
|
+
return text.length <= limit
|
|
513
|
+
? text
|
|
514
|
+
: `${text.slice(0, limit)}… (${text.length - limit} more characters)`;
|
|
515
|
+
}
|
|
516
|
+
function tail(text, limit) {
|
|
517
|
+
return text.length <= limit ? text : `… ${text.slice(-limit)}`;
|
|
518
|
+
}
|
|
519
|
+
function head(text, lines) {
|
|
520
|
+
const all = text.split("\n");
|
|
521
|
+
return all.length <= lines
|
|
522
|
+
? text
|
|
523
|
+
: `${all.slice(0, lines).join("\n")}\n… (${all.length - lines} more lines)`;
|
|
524
|
+
}
|
|
525
|
+
/** The dossier as a page to read: the facts first, the detail after. */
|
|
526
|
+
export function renderDossier(dossier) {
|
|
527
|
+
const out = [];
|
|
528
|
+
const subject = dossier.subject;
|
|
529
|
+
const stop = dossier.stop;
|
|
530
|
+
out.push(`# Incident: ${subject.key}${subject.title === undefined ? "" : ` — ${subject.title}`}`);
|
|
531
|
+
out.push("");
|
|
532
|
+
out.push(`${subject.kind} · ${subject.state} · project ${subject.project} · generated ${dossier.generated_at}`);
|
|
533
|
+
if (subject.source !== undefined) {
|
|
534
|
+
out.push(`source: ${subject.source}`);
|
|
535
|
+
}
|
|
536
|
+
out.push("");
|
|
537
|
+
out.push("## The stop");
|
|
538
|
+
out.push("");
|
|
539
|
+
out.push(`- layer: ${stop.layer}`);
|
|
540
|
+
if (stop.refused_by !== undefined) {
|
|
541
|
+
out.push(`- refused by: ${stop.refused_by}`);
|
|
542
|
+
}
|
|
543
|
+
if (stop.gate !== undefined) {
|
|
544
|
+
out.push(`- gate: ${stop.gate.id} → ${stop.gate.verdict} (${stop.gate.at})`);
|
|
545
|
+
}
|
|
546
|
+
if (stop.agent !== undefined) {
|
|
547
|
+
out.push(`- agent: ${stop.agent.task_id} ended on ${stop.agent.failure}`);
|
|
548
|
+
}
|
|
549
|
+
if (stop.task_id !== undefined) {
|
|
550
|
+
out.push(`- task: ${stop.task_id}${stop.attempt === undefined ? "" : `, attempt ${stop.attempt}`}`);
|
|
551
|
+
}
|
|
552
|
+
if (stop.at !== undefined) {
|
|
553
|
+
out.push(`- at: ${stop.at}`);
|
|
554
|
+
}
|
|
555
|
+
out.push(`- stops of this subject in the journal: ${dossier.history.escalations}; resumes: ${dossier.history.resumes}`);
|
|
556
|
+
const budgets = Object.entries(dossier.budgets).map(([name, value]) => `${name} ${value}`);
|
|
557
|
+
if (budgets.length > 0) {
|
|
558
|
+
out.push(`- budgets: ${budgets.join(", ")}`);
|
|
559
|
+
}
|
|
560
|
+
out.push(`- read from: ${stop.sources.join(", ") || "nothing"}`);
|
|
561
|
+
if (stop.report !== undefined) {
|
|
562
|
+
out.push("");
|
|
563
|
+
out.push("What the ledger says, verbatim:");
|
|
564
|
+
out.push("");
|
|
565
|
+
out.push(fence(stop.report));
|
|
566
|
+
}
|
|
567
|
+
if (stop.gate !== undefined && stop.gate.report !== stop.report) {
|
|
568
|
+
out.push("");
|
|
569
|
+
out.push(`What gate ${stop.gate.id} said, verbatim:`);
|
|
570
|
+
out.push("");
|
|
571
|
+
out.push(fence(stop.gate.report));
|
|
572
|
+
}
|
|
573
|
+
out.push("");
|
|
574
|
+
out.push("## What was asked");
|
|
575
|
+
out.push("");
|
|
576
|
+
out.push(fence(subject.intention));
|
|
577
|
+
if (subject.blocked_by !== undefined) {
|
|
578
|
+
out.push("");
|
|
579
|
+
out.push(`blocked by: ${subject.blocked_by.join(", ")}`);
|
|
580
|
+
}
|
|
581
|
+
const rows = dossier.tickets ?? dossier.parent?.tickets;
|
|
582
|
+
if (rows !== undefined && rows.length > 0) {
|
|
583
|
+
out.push("");
|
|
584
|
+
out.push(dossier.parent === undefined
|
|
585
|
+
? "## Tickets of this feature"
|
|
586
|
+
: `## Tickets of ${dossier.parent.key} (${dossier.parent.state})`);
|
|
587
|
+
out.push("");
|
|
588
|
+
for (const row of rows) {
|
|
589
|
+
const bits = [row.state, `${row.attempts_used} attempt(s)`];
|
|
590
|
+
if (row.reference !== undefined) {
|
|
591
|
+
bits.push(row.reference);
|
|
592
|
+
}
|
|
593
|
+
if (row.done_at !== undefined) {
|
|
594
|
+
bits.push(`done ${row.done_at}`);
|
|
595
|
+
}
|
|
596
|
+
if (row.featureTest === true) {
|
|
597
|
+
bits.push("feature-test");
|
|
598
|
+
}
|
|
599
|
+
out.push(`- ${row.key}${row.title === undefined ? "" : ` ${row.title}`} — ${bits.join(", ")}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (dossier.gate_config !== undefined) {
|
|
603
|
+
out.push("");
|
|
604
|
+
out.push("## Configuration of the gate");
|
|
605
|
+
out.push("");
|
|
606
|
+
for (const gate of dossier.gate_config) {
|
|
607
|
+
out.push(`- ${gate.section}: \`${gate.argv.join(" ")}\`${gate.timeoutMs === undefined ? "" : ` (timeout ${gate.timeoutMs} ms)`}${gate.skipWhen === undefined ? "" : ` skipWhen ${gate.skipWhen}`}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (dossier.system !== undefined) {
|
|
611
|
+
out.push(...renderSystem(dossier.system));
|
|
612
|
+
}
|
|
613
|
+
if (dossier.timeline.length > 0) {
|
|
614
|
+
out.push("");
|
|
615
|
+
out.push("## Timeline of the last round");
|
|
616
|
+
out.push("");
|
|
617
|
+
for (const row of dossier.timeline) {
|
|
618
|
+
const who = row.gate_id ?? row.task_id ?? "";
|
|
619
|
+
const verdict = row.verdict === undefined ? "" : ` ${row.verdict}`;
|
|
620
|
+
const detail = row.detail === undefined ? "" : ` ${row.detail.replace(/\s+/g, " ")}`;
|
|
621
|
+
out.push(`- ${row.at.slice(11, 23)} ${row.event} ${who}${verdict}${detail}`.trimEnd());
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
const { streams, transcripts } = dossier.artifacts;
|
|
625
|
+
if (streams.length > 0 || transcripts.length > 0) {
|
|
626
|
+
out.push("");
|
|
627
|
+
out.push("## Files");
|
|
628
|
+
out.push("");
|
|
629
|
+
for (const stream of streams) {
|
|
630
|
+
out.push(`- stream${stream.exists ? "" : " (gone)"}: ${stream.path}`);
|
|
631
|
+
}
|
|
632
|
+
for (const transcript of transcripts) {
|
|
633
|
+
const bits = [
|
|
634
|
+
transcript.exit === undefined ? undefined : `exit ${transcript.exit}`,
|
|
635
|
+
transcript.duration,
|
|
636
|
+
transcript.marker,
|
|
637
|
+
].filter((bit) => bit !== undefined);
|
|
638
|
+
out.push(`- transcript: ${transcript.path}${bits.length === 0 ? "" : ` (${bits.join(", ")})`}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
for (const stream of streams) {
|
|
642
|
+
if (stream.excerpt !== undefined) {
|
|
643
|
+
out.push("");
|
|
644
|
+
out.push(`## What ${stream.gate_id ?? "the check"} printed (end of its stream)`);
|
|
645
|
+
out.push("");
|
|
646
|
+
out.push(fence(stream.excerpt));
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (dossier.gaps.length > 0) {
|
|
650
|
+
out.push("");
|
|
651
|
+
out.push("## Not found");
|
|
652
|
+
out.push("");
|
|
653
|
+
for (const gap of dossier.gaps) {
|
|
654
|
+
out.push(`- ${gap}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
out.push("");
|
|
658
|
+
return out.join("\n");
|
|
659
|
+
}
|
|
660
|
+
function renderSystem(system) {
|
|
661
|
+
const { bluewombat, config } = system;
|
|
662
|
+
const out = ["", "## The system", ""];
|
|
663
|
+
out.push(`- bluewombat ${bluewombat.host}, node ${bluewombat.node}, ${bluewombat.platform}`);
|
|
664
|
+
if (system.packages.length > 0) {
|
|
665
|
+
out.push(`- installed: ${system.packages.map((pkg) => `${pkg.name.replace("@bluewombat/", "")} ${pkg.version}`).join(", ")}`);
|
|
666
|
+
}
|
|
667
|
+
out.push(`- trackers: features ${config.featureTracker}, tickets ${config.ticketTracker}`);
|
|
668
|
+
out.push(`- ledger: ${config.persist}; work line isolation: ${config.isolation}`);
|
|
669
|
+
out.push(`- authority: ${config.authority}; tickets in parallel: ${config.maxParallel}; streams: ${config.streams}`);
|
|
670
|
+
const limits = Object.entries(config.limits).map(([name, value]) => `${name} ${value}`);
|
|
671
|
+
if (limits.length > 0) {
|
|
672
|
+
out.push(`- limits: ${limits.join(", ")}`);
|
|
673
|
+
}
|
|
674
|
+
out.push(`- passes: ${Object.entries(config.reviews)
|
|
675
|
+
.map(([name, value]) => `${name} ${value}`)
|
|
676
|
+
.join("; ")}`);
|
|
677
|
+
if (Object.keys(config.agents).length > 0) {
|
|
678
|
+
out.push(`- agents: ${Object.entries(config.agents)
|
|
679
|
+
.map(([role, program]) => `${role} ${program}`)
|
|
680
|
+
.join("; ")}`);
|
|
681
|
+
}
|
|
682
|
+
for (const [name, ids] of Object.entries(config.gates)) {
|
|
683
|
+
out.push(`- ${name}: ${ids.join(", ")}`);
|
|
684
|
+
}
|
|
685
|
+
out.push(`- config: ${config.path ?? config.dir}`);
|
|
686
|
+
if (config.raw !== undefined) {
|
|
687
|
+
out.push("", "The configuration file, values of secret-looking keys blanked:", "", fence(config.raw));
|
|
688
|
+
}
|
|
689
|
+
return out;
|
|
690
|
+
}
|
|
691
|
+
function fence(text) {
|
|
692
|
+
const longest = Math.max(0, ...[...text.matchAll(/`+/g)].map((match) => match[0].length));
|
|
693
|
+
const mark = "`".repeat(Math.max(3, longest + 1));
|
|
694
|
+
return `${mark}text\n${text}\n${mark}`;
|
|
695
|
+
}
|
|
696
|
+
//# sourceMappingURL=incident-dossier.js.map
|