@monster-orange/den 0.4.8 → 0.4.9
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 +80 -0
- package/README.md +7 -7
- package/docs/distributions.md +124 -1
- package/package.json +1 -1
- package/src/shared/agents/den-claude.ts +38 -3
- package/src/shared/agents/guardrails/hooks.json +23 -0
- package/src/shared/agents/guardrails/instructions.md +23 -0
- package/src/shared/agents/guardrails/rotation-hook.ts +127 -0
- package/src/shared/agents/guardrails/stop-hook.ts +48 -2
- package/src/shared/agents/guardrails.ts +38 -7
- package/src/shared/agents/handoff.ts +118 -0
- package/src/shared/agents/records.ts +8 -1
- package/src/shared/agents/rotation.ts +362 -0
- package/src/shared/agents/runtime.ts +15 -0
- package/src/shared/den-config.ts +42 -1
- package/src/shared/git/gh-pr-state.ts +32 -0
- package/src/shared/usage/report.ts +98 -4
- package/src/shared/usage/transcript.ts +62 -5
- package/src/ui/cli/agent-ops.ts +196 -7
- package/src/ui/cli/agents-command.ts +75 -11
- package/src/ui/cli/usage-command.ts +77 -10
- package/src/ui/server/agent-routes.ts +19 -2
- package/src/ui/server/public/agents-page.js +78 -13
- package/src/ui/server/public/i18n/es.js +12 -0
- package/src/ui/server/public/i18n/ja.js +11 -0
- package/src/ui/server/public/shell.js +13 -0
- package/src/ui/server/route-classes.ts +6 -0
- package/src/ui/server/session-routes.ts +8 -1
package/src/shared/den-config.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { validateOrgSources, type WorkOrgsConfig } from "./org-sources.js";
|
|
|
20
20
|
import { ConfigError, isConfigError } from "./config-error.js";
|
|
21
21
|
import { validateCatalog, type CatalogEntry } from "./agents/catalog.js";
|
|
22
22
|
import type { Guardrails } from "./agents/guardrails.js";
|
|
23
|
+
import { DEFAULT_ROTATE, type RotateSettings } from "./agents/rotation.js";
|
|
23
24
|
import type { TestAccountsConfig } from "./auth/test-accounts.js";
|
|
24
25
|
|
|
25
26
|
import { product } from "./product.js";
|
|
@@ -879,6 +880,13 @@ export interface AgentsConfig {
|
|
|
879
880
|
* Den doesn't ship, with the same fields and checks as a built-in one.
|
|
880
881
|
*/
|
|
881
882
|
runtime_catalog?: unknown[];
|
|
883
|
+
/**
|
|
884
|
+
* Session rotation (den#4), `{ after_compactions, standing }`: a session
|
|
885
|
+
* ends after `after_compactions` compactions (default 3) or once its pull
|
|
886
|
+
* request merged, and its role relaunches from its handoff. `standing:
|
|
887
|
+
* false` keeps standing personas from rotating (default true).
|
|
888
|
+
*/
|
|
889
|
+
rotate?: { after_compactions?: unknown; standing?: unknown };
|
|
882
890
|
/**
|
|
883
891
|
* Any other key is an agent id: `agents.<id>.grants` widens that agent's
|
|
884
892
|
* MCP scope to other workspaces (AG spec §5b). Config only: an agent
|
|
@@ -902,6 +910,7 @@ export const AGENTS_RESERVED_KEYS = [
|
|
|
902
910
|
"persona_packs",
|
|
903
911
|
"runtime_catalog",
|
|
904
912
|
"default_pack",
|
|
913
|
+
"rotate",
|
|
905
914
|
];
|
|
906
915
|
|
|
907
916
|
/** One agent's own block, `agents.<id>`. */
|
|
@@ -923,6 +932,37 @@ export interface ResolvedAgentsConfig {
|
|
|
923
932
|
agents: Record<string, AgentGrantsConfig>;
|
|
924
933
|
/** The distribution's runtime catalog entries (AG-0.5), checked. */
|
|
925
934
|
runtimeCatalog: CatalogEntry[];
|
|
935
|
+
/** agents.rotate with its defaults (den#4); unset: DEFAULT_ROTATE. */
|
|
936
|
+
rotate?: RotateSettings;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/** `agents.rotate` checked; any other key, or a bad value, throws naming it. */
|
|
940
|
+
function rotateBlock(v: unknown): RotateSettings {
|
|
941
|
+
const out: RotateSettings = { ...DEFAULT_ROTATE };
|
|
942
|
+
if (v === undefined || v === null) return out;
|
|
943
|
+
if (!isMapping(v))
|
|
944
|
+
throw new Error(
|
|
945
|
+
"agents.rotate: must be a mapping ({ after_compactions: <n>, standing: true|false })",
|
|
946
|
+
);
|
|
947
|
+
for (const [k, x] of Object.entries(v)) {
|
|
948
|
+
if (k === "after_compactions") {
|
|
949
|
+
if (typeof x !== "number" || !Number.isInteger(x) || x < 1 || x > 100)
|
|
950
|
+
throw new Error(
|
|
951
|
+
`agents.rotate.after_compactions: must be a whole number from 1 to 100 (got ${JSON.stringify(x)})`,
|
|
952
|
+
);
|
|
953
|
+
out.afterCompactions = x;
|
|
954
|
+
} else if (k === "standing") {
|
|
955
|
+
if (typeof x !== "boolean")
|
|
956
|
+
throw new Error(
|
|
957
|
+
`agents.rotate.standing: must be true or false (got ${JSON.stringify(x)})`,
|
|
958
|
+
);
|
|
959
|
+
out.standing = x;
|
|
960
|
+
} else
|
|
961
|
+
throw new Error(
|
|
962
|
+
`agents.rotate.${k}: unknown key (takes after_compactions, standing)`,
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
return out;
|
|
926
966
|
}
|
|
927
967
|
|
|
928
968
|
/** An agent id or workspace name: what WORKSPACE_NAME_RE accepts. */
|
|
@@ -985,6 +1025,7 @@ export function readAgentsConfig(
|
|
|
985
1025
|
raw.runtime_catalog,
|
|
986
1026
|
"agents.runtime_catalog",
|
|
987
1027
|
),
|
|
1028
|
+
rotate: rotateBlock(raw.rotate),
|
|
988
1029
|
};
|
|
989
1030
|
const max = raw.max_concurrent;
|
|
990
1031
|
if (max !== undefined) {
|
|
@@ -1018,7 +1059,7 @@ export function readAgentsConfig(
|
|
|
1018
1059
|
const where = `agents.${id}`;
|
|
1019
1060
|
if (!AGENT_KEY_RE.test(id) || !isMapping(block))
|
|
1020
1061
|
throw new Error(
|
|
1021
|
-
`${where}: unknown key (agents: takes max_concurrent, persona_packs, runtime_catalog, default_pack, ` +
|
|
1062
|
+
`${where}: unknown key (agents: takes max_concurrent, persona_packs, runtime_catalog, default_pack, rotate, ` +
|
|
1022
1063
|
`or an agent id mapping to { grants: [...] })`,
|
|
1023
1064
|
);
|
|
1024
1065
|
for (const k of Object.keys(block))
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A pull request's state from gh, to confirm a merge an agent ran
|
|
3
|
+
* (session rotation's "PR merged" trigger, den#4). It runs gh, which asks
|
|
4
|
+
* GitHub; usage itself stays local (src/shared/usage never sends).
|
|
5
|
+
*/
|
|
6
|
+
import { execFile } from "child_process";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `gh pr view [<ref>] [-R <repo>] --json state -q .state`;
|
|
10
|
+
* rejects on any error. Only the ref and repo go out, to GitHub, by gh.
|
|
11
|
+
*/
|
|
12
|
+
export function ghPrState(env: NodeJS.ProcessEnv) {
|
|
13
|
+
return (ref: string, o: { repo?: string; cwd: string }) =>
|
|
14
|
+
new Promise<string | undefined>((res, rej) => {
|
|
15
|
+
const args = [
|
|
16
|
+
"pr",
|
|
17
|
+
"view",
|
|
18
|
+
...(ref ? [ref] : []),
|
|
19
|
+
...(o.repo ? ["-R", o.repo] : []),
|
|
20
|
+
"--json",
|
|
21
|
+
"state",
|
|
22
|
+
"-q",
|
|
23
|
+
".state",
|
|
24
|
+
];
|
|
25
|
+
execFile(
|
|
26
|
+
"gh",
|
|
27
|
+
args,
|
|
28
|
+
{ cwd: o.cwd, env, timeout: 15_000, maxBuffer: 64 * 1024 },
|
|
29
|
+
(e, stdout) => (e ? rej(e) : res(String(stdout).trim())),
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -8,6 +8,17 @@
|
|
|
8
8
|
* share of attributed sessions is the scorecard metric: before this, no
|
|
9
9
|
* agent's tokens or outcome were joined to it (~0).
|
|
10
10
|
*
|
|
11
|
+
* Session rotation (den#4) adds, per agent, its compactions (this
|
|
12
|
+
* conversation's) and its rotations (how many times Den relaunched it from
|
|
13
|
+
* a handoff); and per week, two numbers to move:
|
|
14
|
+
* - long_session_context_k: the highest average context per call (input,
|
|
15
|
+
* cache reads and cache writes, over calls) of any main session in the
|
|
16
|
+
* window, in thousands of tokens;
|
|
17
|
+
* - never_ending_reread_pct: the share of context re-reads (cache reads)
|
|
18
|
+
* that came from never-ending sessions: main sessions that compacted
|
|
19
|
+
* more often than the rotation limit (agents.rotate.after_compactions,
|
|
20
|
+
* default 3).
|
|
21
|
+
*
|
|
11
22
|
* Everything is read and written on this machine; nothing is sent.
|
|
12
23
|
*/
|
|
13
24
|
import type {
|
|
@@ -30,6 +41,10 @@ export interface AgentUsageRow {
|
|
|
30
41
|
usage?: AgentUsage;
|
|
31
42
|
outcome?: AgentOutcome;
|
|
32
43
|
attributed: boolean;
|
|
44
|
+
/** Its current conversation's compactions (den#4), when counted. */
|
|
45
|
+
compactions?: number;
|
|
46
|
+
/** How many times Den relaunched it from a handoff (den#4). */
|
|
47
|
+
rotations: number;
|
|
33
48
|
}
|
|
34
49
|
|
|
35
50
|
/** The sum of transcripts' counts, as the record keeps them. */
|
|
@@ -79,7 +94,6 @@ export function outcomeOf(
|
|
|
79
94
|
}
|
|
80
95
|
const pr = [...all].reverse().find((s) => s.prOpened)?.prOpened;
|
|
81
96
|
if (pr) out.prOpened = pr;
|
|
82
|
-
if (all.some((s) => s.prMerged)) out.prMerged = true;
|
|
83
97
|
// The last test run: of the transcript that ended last and ran tests.
|
|
84
98
|
const tested = all
|
|
85
99
|
.filter((s) => s.testsGreen !== undefined)
|
|
@@ -88,6 +102,40 @@ export function outcomeOf(
|
|
|
88
102
|
return out;
|
|
89
103
|
}
|
|
90
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Whether one of the transcripts' `gh pr merge` claims is a pull request
|
|
107
|
+
* gh says is MERGED. A bare `gh pr merge` (the branch's PR) is checked by
|
|
108
|
+
* the PR the agent opened, else in its folder.
|
|
109
|
+
*/
|
|
110
|
+
async function mergeVerified(
|
|
111
|
+
rec: AgentRecord,
|
|
112
|
+
stats: readonly TranscriptStats[],
|
|
113
|
+
outcome: AgentOutcome,
|
|
114
|
+
prState?: (
|
|
115
|
+
ref: string,
|
|
116
|
+
o: { repo?: string; cwd: string },
|
|
117
|
+
) => Promise<string | undefined>,
|
|
118
|
+
): Promise<boolean> {
|
|
119
|
+
if (!prState) return false;
|
|
120
|
+
const seen = new Set<string>();
|
|
121
|
+
for (const c of stats.flatMap((s) => s.prMerges)) {
|
|
122
|
+
const ref = c.ref || outcome.prOpened || "";
|
|
123
|
+
const key = `${c.repo ?? ""} ${ref}`;
|
|
124
|
+
if (seen.has(key)) continue;
|
|
125
|
+
seen.add(key);
|
|
126
|
+
try {
|
|
127
|
+
const st = await prState(ref, {
|
|
128
|
+
...(c.repo ? { repo: c.repo } : {}),
|
|
129
|
+
cwd: rec.cwd,
|
|
130
|
+
});
|
|
131
|
+
if (st?.trim().toUpperCase() === "MERGED") return true;
|
|
132
|
+
} catch {
|
|
133
|
+
// Not checked: not marked.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
91
139
|
/**
|
|
92
140
|
* Each record's usage and outcome, from its runtime's transcripts (by its
|
|
93
141
|
* resumeToken). `source` picks the reader (default: the one for its runtime).
|
|
@@ -97,6 +145,15 @@ export async function agentUsage(
|
|
|
97
145
|
opts: {
|
|
98
146
|
env?: NodeJS.ProcessEnv;
|
|
99
147
|
source?: (runtime: string) => UsageSource | undefined;
|
|
148
|
+
/**
|
|
149
|
+
* A pull request's state (`gh pr view <ref> --json state`), run in the
|
|
150
|
+
* agent's folder: "MERGED" marks the outcome merged. Without it, or on
|
|
151
|
+
* any error, nothing is marked: a merge is never taken from text.
|
|
152
|
+
*/
|
|
153
|
+
prState?: (
|
|
154
|
+
ref: string,
|
|
155
|
+
o: { repo?: string; cwd: string },
|
|
156
|
+
) => Promise<string | undefined>;
|
|
100
157
|
} = {},
|
|
101
158
|
): Promise<AgentUsageRow[]> {
|
|
102
159
|
const pick = opts.source ?? sourceFor;
|
|
@@ -107,6 +164,7 @@ export async function agentUsage(
|
|
|
107
164
|
workspace: rec.workspace,
|
|
108
165
|
persona: rec.persona,
|
|
109
166
|
runtime: rec.runtime,
|
|
167
|
+
rotations: rec.rotations?.length ?? 0,
|
|
110
168
|
};
|
|
111
169
|
const src = pick(rec.runtime);
|
|
112
170
|
if (!src) {
|
|
@@ -140,11 +198,14 @@ export async function agentUsage(
|
|
|
140
198
|
}
|
|
141
199
|
const usage = sumUsage(stats);
|
|
142
200
|
const outcome = outcomeOf(rec, stats);
|
|
201
|
+
if (await mergeVerified(rec, stats, outcome, opts.prState))
|
|
202
|
+
outcome.prMerged = true;
|
|
143
203
|
rows.push({
|
|
144
204
|
...base,
|
|
145
205
|
transcripts: files.length,
|
|
146
206
|
usage,
|
|
147
207
|
outcome,
|
|
208
|
+
compactions: usage.compactions,
|
|
148
209
|
attributed: usage.calls > 0 && outcome.wallClockMs !== undefined,
|
|
149
210
|
});
|
|
150
211
|
}
|
|
@@ -176,6 +237,23 @@ export interface UsageMetrics {
|
|
|
176
237
|
subagent_opus_pct: number;
|
|
177
238
|
/** den#6: the share of agent sessions with attributed tokens and outcome. */
|
|
178
239
|
attributed_pct?: number;
|
|
240
|
+
/** den#4: the highest average context per call of a main session (k tokens). */
|
|
241
|
+
long_session_context_k: number;
|
|
242
|
+
/** den#4: the share of cache reads from never-ending main sessions. */
|
|
243
|
+
never_ending_reread_pct: number;
|
|
244
|
+
/** den#4: relaunches from a handoff, over the agents listed. */
|
|
245
|
+
rotations?: number;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** A main session's average context per call, in tokens. */
|
|
249
|
+
export function contextPerCall(s: TranscriptStats): number {
|
|
250
|
+
const t = s.tokens.input + s.tokens.cacheRead + s.tokens.cacheWrite;
|
|
251
|
+
return s.calls ? t / s.calls : 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Never-ending: a main session that compacted more often than the limit. */
|
|
255
|
+
export function isNeverEnding(s: TranscriptStats, after: number): boolean {
|
|
256
|
+
return !s.subagent && s.compactions > after;
|
|
179
257
|
}
|
|
180
258
|
|
|
181
259
|
const round = (n: number, d = 0) => {
|
|
@@ -186,6 +264,8 @@ const round = (n: number, d = 0) => {
|
|
|
186
264
|
export function weeklyMetrics(
|
|
187
265
|
stats: readonly TranscriptStats[],
|
|
188
266
|
agents?: readonly AgentUsageRow[],
|
|
267
|
+
/** The rotation limit (agents.rotate.after_compactions, default 3). */
|
|
268
|
+
after = 3,
|
|
189
269
|
): UsageMetrics {
|
|
190
270
|
const sum = (
|
|
191
271
|
xs: readonly TranscriptStats[],
|
|
@@ -239,14 +319,28 @@ export function weeklyMetrics(
|
|
|
239
319
|
1,
|
|
240
320
|
),
|
|
241
321
|
subagent_opus_pct: round((100 * subOpus) / Math.max(1, subTotal)),
|
|
322
|
+
long_session_context_k: round(
|
|
323
|
+
Math.max(0, ...main.map(contextPerCall)) / 1e3,
|
|
324
|
+
),
|
|
325
|
+
never_ending_reread_pct: round(
|
|
326
|
+
(100 *
|
|
327
|
+
sum(
|
|
328
|
+
main.filter((s) => isNeverEnding(s, after)),
|
|
329
|
+
(s) => s.tokens.cacheRead,
|
|
330
|
+
)) /
|
|
331
|
+
Math.max(1, reads),
|
|
332
|
+
),
|
|
242
333
|
};
|
|
243
|
-
if (agents)
|
|
334
|
+
if (agents) {
|
|
335
|
+
m.attributed_pct = attributedPct(agents);
|
|
336
|
+
m.rotations = agents.reduce((a, r) => a + r.rotations, 0);
|
|
337
|
+
}
|
|
244
338
|
return m;
|
|
245
339
|
}
|
|
246
340
|
|
|
247
341
|
/** Higher is better for these; context only for calls, output and prompts. */
|
|
248
342
|
const BETTER_HIGH = new Set(["attributed_pct"]);
|
|
249
|
-
const CONTEXT = new Set(["calls", "output_M", "prompts"]);
|
|
343
|
+
const CONTEXT = new Set(["calls", "output_M", "prompts", "rotations"]);
|
|
250
344
|
|
|
251
345
|
/** The report as markdown: this week, last week, and the change. */
|
|
252
346
|
export function renderReport(
|
|
@@ -277,7 +371,7 @@ export function renderReport(
|
|
|
277
371
|
}
|
|
278
372
|
lines.push(
|
|
279
373
|
"",
|
|
280
|
-
"Read from this machine's transcripts;
|
|
374
|
+
"Read from this machine's transcripts; none of this was sent anywhere.",
|
|
281
375
|
);
|
|
282
376
|
return lines.join("\n") + "\n";
|
|
283
377
|
}
|
|
@@ -49,11 +49,66 @@ export interface TranscriptStats {
|
|
|
49
49
|
activeSeconds: number;
|
|
50
50
|
/** The last pull request URL it opened. */
|
|
51
51
|
prOpened?: string;
|
|
52
|
-
|
|
52
|
+
/**
|
|
53
|
+
* `gh pr merge` commands that exited 0: claims, which `usage` checks
|
|
54
|
+
* with `gh pr view` before it marks the pull request merged.
|
|
55
|
+
*/
|
|
56
|
+
prMerges: PrMergeClaim[];
|
|
53
57
|
/** The last test run's result; undefined when it ran none. */
|
|
54
58
|
testsGreen?: boolean;
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
/** A `gh pr merge` that ran: its PR (number, URL, or "" for the branch's). */
|
|
62
|
+
export interface PrMergeClaim {
|
|
63
|
+
ref: string;
|
|
64
|
+
repo?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Words of one simple command, with quotes removed (no expansion). */
|
|
68
|
+
function words(part: string): string[] {
|
|
69
|
+
const out: string[] = [];
|
|
70
|
+
for (const m of part.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g))
|
|
71
|
+
out.push(m[1] ?? m[2] ?? m[3]);
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The merge a shell command runs: one of its parts (split on `&&`, `||`,
|
|
77
|
+
* `;`, `|`, newlines), after `VAR=value` words, `env` or `command`, starts
|
|
78
|
+
* with `gh pr merge`, and is not asking for help. Null otherwise:
|
|
79
|
+
* `echo gh pr merge 1`, a grep for it, or a comment are not merges.
|
|
80
|
+
*/
|
|
81
|
+
export function ghPrMergeRef(cmd: string): PrMergeClaim | null {
|
|
82
|
+
for (const part of cmd.split(/&&|\|\||;|\||\n/)) {
|
|
83
|
+
const w = words(part.trim());
|
|
84
|
+
let i = 0;
|
|
85
|
+
while (
|
|
86
|
+
i < w.length &&
|
|
87
|
+
(/^[A-Za-z_][A-Za-z0-9_]*=/.test(w[i]) ||
|
|
88
|
+
w[i] === "env" ||
|
|
89
|
+
w[i] === "command")
|
|
90
|
+
)
|
|
91
|
+
i++;
|
|
92
|
+
if (w[i] !== "gh" || w[i + 1] !== "pr" || w[i + 2] !== "merge") continue;
|
|
93
|
+
const rest = w.slice(i + 3);
|
|
94
|
+
if (rest.includes("--help") || rest.includes("-h")) return null;
|
|
95
|
+
const claim: PrMergeClaim = { ref: "" };
|
|
96
|
+
for (let j = 0; j < rest.length; j++) {
|
|
97
|
+
const a = rest[j];
|
|
98
|
+
if (a === "-R" || a === "--repo") {
|
|
99
|
+
if (rest[j + 1]) claim.repo = rest[++j];
|
|
100
|
+
} else if (a.startsWith("--repo=")) claim.repo = a.slice(7);
|
|
101
|
+
else if (
|
|
102
|
+
!claim.ref &&
|
|
103
|
+
/^(\d+|https:\/\/github\.com\/[^\s/]+\/[^\s/]+\/pull\/\d+)$/.test(a)
|
|
104
|
+
)
|
|
105
|
+
claim.ref = a;
|
|
106
|
+
}
|
|
107
|
+
return claim;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
57
112
|
/** A correction, as the miner reads one (emphatic ones; polite ones slip by). */
|
|
58
113
|
const CORRECTION =
|
|
59
114
|
/\b(wtf|fuck|shit|damn|not what i (said|asked)|that('s| is) wrong|you (missed|forgot|broke)|again\??$|still (not|broken|failing|404)|where (is|the)|why (did|didn't|are|is)|i told you|stop|no,|nope|ugh|boo)\b/i;
|
|
@@ -130,13 +185,13 @@ export async function mineTranscript(
|
|
|
130
185
|
interrupts: 0,
|
|
131
186
|
agentSpawns: 0,
|
|
132
187
|
activeSeconds: 0,
|
|
133
|
-
|
|
188
|
+
prMerges: [],
|
|
134
189
|
};
|
|
135
190
|
const seen = new Set<string>();
|
|
136
191
|
const toolNames = new Map<string, string>();
|
|
137
192
|
/** Tool-use ids of the commands whose result decides an outcome. */
|
|
138
193
|
const tests = new Map<string, number>();
|
|
139
|
-
const merges = new
|
|
194
|
+
const merges = new Map<string, PrMergeClaim>();
|
|
140
195
|
const creates = new Set<string>();
|
|
141
196
|
let order = 0;
|
|
142
197
|
let lastTest: { at: number; green: boolean } | undefined;
|
|
@@ -217,7 +272,8 @@ export async function mineTranscript(
|
|
|
217
272
|
for (const s of cmd.matchAll(/\bsleep\s+(\d+)/g))
|
|
218
273
|
r.sleepSeconds += Number(s[1]);
|
|
219
274
|
if (TEST_RUN.test(cmd)) tests.set(useId, order++);
|
|
220
|
-
|
|
275
|
+
const merge = ghPrMergeRef(cmd);
|
|
276
|
+
if (merge) merges.set(useId, merge);
|
|
221
277
|
if (/\bgh pr create\b/.test(cmd)) creates.add(useId);
|
|
222
278
|
}
|
|
223
279
|
continue;
|
|
@@ -240,7 +296,8 @@ export async function mineTranscript(
|
|
|
240
296
|
const t = tests.get(useId);
|
|
241
297
|
if (t !== undefined && (!lastTest || t >= lastTest.at))
|
|
242
298
|
lastTest = { at: t, green: !err };
|
|
243
|
-
|
|
299
|
+
const merge = merges.get(useId);
|
|
300
|
+
if (merge && !err) r.prMerges.push(merge);
|
|
244
301
|
if (creates.has(useId) && !err) {
|
|
245
302
|
const u = PR_URL.exec(textOf(tr.content));
|
|
246
303
|
if (u) r.prOpened = u[0];
|