@monster-orange/den 0.4.8 → 0.4.10
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 +105 -0
- package/README.md +7 -7
- package/docs/distributions.md +128 -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 +13 -1
- package/src/shared/agents/rotation.ts +371 -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 +44 -0
- package/src/shared/usage/report.ts +117 -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 +95 -11
- 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,44 @@
|
|
|
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 number,state`: the state and the
|
|
10
|
+
* number (a bare ref is the branch's PR, and the number names it). Rejects
|
|
11
|
+
* on any error, or an answer that isn't that JSON. Only the ref and repo
|
|
12
|
+
* go out, to GitHub, by gh.
|
|
13
|
+
*/
|
|
14
|
+
export function ghPrState(env: NodeJS.ProcessEnv) {
|
|
15
|
+
return (ref: string, o: { repo?: string; cwd: string }) =>
|
|
16
|
+
new Promise<{ state: string; number?: number }>((res, rej) => {
|
|
17
|
+
const args = [
|
|
18
|
+
"pr",
|
|
19
|
+
"view",
|
|
20
|
+
...(ref ? [ref] : []),
|
|
21
|
+
...(o.repo ? ["-R", o.repo] : []),
|
|
22
|
+
"--json",
|
|
23
|
+
"number,state",
|
|
24
|
+
];
|
|
25
|
+
execFile(
|
|
26
|
+
"gh",
|
|
27
|
+
args,
|
|
28
|
+
{ cwd: o.cwd, env, timeout: 15_000, maxBuffer: 64 * 1024 },
|
|
29
|
+
(e, stdout) => {
|
|
30
|
+
if (e) return rej(e);
|
|
31
|
+
try {
|
|
32
|
+
const o = JSON.parse(String(stdout)) as Record<string, unknown>;
|
|
33
|
+
if (typeof o.state !== "string") throw new Error("no state");
|
|
34
|
+
res({
|
|
35
|
+
state: o.state,
|
|
36
|
+
...(typeof o.number === "number" ? { number: o.number } : {}),
|
|
37
|
+
});
|
|
38
|
+
} catch (err) {
|
|
39
|
+
rej(err);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -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,55 @@ export function outcomeOf(
|
|
|
88
102
|
return out;
|
|
89
103
|
}
|
|
90
104
|
|
|
105
|
+
/**
|
|
106
|
+
* What a pull request check answers: its state ("MERGED"), and its number
|
|
107
|
+
* when gh gave one (a bare `gh pr merge` is the branch's PR: the number is
|
|
108
|
+
* how it is kept).
|
|
109
|
+
*/
|
|
110
|
+
export type PrStateAnswer = string | { state: string; number?: number };
|
|
111
|
+
|
|
112
|
+
/** Checks one pull request's state (gh pr view), in the agent's folder. */
|
|
113
|
+
export type PrStateCheck = (
|
|
114
|
+
ref: string,
|
|
115
|
+
o: { repo?: string; cwd: string },
|
|
116
|
+
) => Promise<PrStateAnswer | undefined>;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Whether one of the transcripts' `gh pr merge` claims is a pull request
|
|
120
|
+
* gh says is MERGED. A bare `gh pr merge` (the branch's PR) is checked by
|
|
121
|
+
* the PR the agent opened, else in its folder.
|
|
122
|
+
*/
|
|
123
|
+
async function mergeVerified(
|
|
124
|
+
rec: AgentRecord,
|
|
125
|
+
stats: readonly TranscriptStats[],
|
|
126
|
+
outcome: AgentOutcome,
|
|
127
|
+
prState?: PrStateCheck,
|
|
128
|
+
): Promise<string | undefined> {
|
|
129
|
+
if (!prState) return undefined;
|
|
130
|
+
const seen = new Set<string>();
|
|
131
|
+
for (const c of stats.flatMap((s) => s.prMerges)) {
|
|
132
|
+
const ref = c.ref || outcome.prOpened || "";
|
|
133
|
+
const key = `${c.repo ?? ""} ${ref}`;
|
|
134
|
+
if (seen.has(key)) continue;
|
|
135
|
+
seen.add(key);
|
|
136
|
+
try {
|
|
137
|
+
const st = await prState(ref, {
|
|
138
|
+
...(c.repo ? { repo: c.repo } : {}),
|
|
139
|
+
cwd: rec.cwd,
|
|
140
|
+
});
|
|
141
|
+
const state = typeof st === "string" ? st : st?.state;
|
|
142
|
+
if (state?.trim().toUpperCase() !== "MERGED") continue;
|
|
143
|
+
// Kept by its ref, or by the number gh gave; never by "".
|
|
144
|
+
const n = typeof st === "object" ? st.number : undefined;
|
|
145
|
+
const key = ref || (Number.isInteger(n) && n! > 0 ? String(n) : "");
|
|
146
|
+
if (key) return key;
|
|
147
|
+
} catch {
|
|
148
|
+
// Not checked: not marked.
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
91
154
|
/**
|
|
92
155
|
* Each record's usage and outcome, from its runtime's transcripts (by its
|
|
93
156
|
* resumeToken). `source` picks the reader (default: the one for its runtime).
|
|
@@ -97,6 +160,12 @@ export async function agentUsage(
|
|
|
97
160
|
opts: {
|
|
98
161
|
env?: NodeJS.ProcessEnv;
|
|
99
162
|
source?: (runtime: string) => UsageSource | undefined;
|
|
163
|
+
/**
|
|
164
|
+
* A pull request's state (`gh pr view <ref> --json number,state`), run in the
|
|
165
|
+
* agent's folder: "MERGED" marks the outcome merged. Without it, or on
|
|
166
|
+
* any error, nothing is marked: a merge is never taken from text.
|
|
167
|
+
*/
|
|
168
|
+
prState?: PrStateCheck;
|
|
100
169
|
} = {},
|
|
101
170
|
): Promise<AgentUsageRow[]> {
|
|
102
171
|
const pick = opts.source ?? sourceFor;
|
|
@@ -107,6 +176,7 @@ export async function agentUsage(
|
|
|
107
176
|
workspace: rec.workspace,
|
|
108
177
|
persona: rec.persona,
|
|
109
178
|
runtime: rec.runtime,
|
|
179
|
+
rotations: rec.rotations?.length ?? 0,
|
|
110
180
|
};
|
|
111
181
|
const src = pick(rec.runtime);
|
|
112
182
|
if (!src) {
|
|
@@ -140,11 +210,21 @@ export async function agentUsage(
|
|
|
140
210
|
}
|
|
141
211
|
const usage = sumUsage(stats);
|
|
142
212
|
const outcome = outcomeOf(rec, stats);
|
|
213
|
+
// Confirmed once, kept: GitHub is not asked again.
|
|
214
|
+
const confirmed =
|
|
215
|
+
rec.outcome?.prMerged && rec.outcome.prMergeConfirmed
|
|
216
|
+
? rec.outcome.prMergeConfirmed
|
|
217
|
+
: await mergeVerified(rec, stats, outcome, opts.prState);
|
|
218
|
+
if (confirmed) {
|
|
219
|
+
outcome.prMerged = true;
|
|
220
|
+
outcome.prMergeConfirmed = confirmed;
|
|
221
|
+
}
|
|
143
222
|
rows.push({
|
|
144
223
|
...base,
|
|
145
224
|
transcripts: files.length,
|
|
146
225
|
usage,
|
|
147
226
|
outcome,
|
|
227
|
+
compactions: usage.compactions,
|
|
148
228
|
attributed: usage.calls > 0 && outcome.wallClockMs !== undefined,
|
|
149
229
|
});
|
|
150
230
|
}
|
|
@@ -176,6 +256,23 @@ export interface UsageMetrics {
|
|
|
176
256
|
subagent_opus_pct: number;
|
|
177
257
|
/** den#6: the share of agent sessions with attributed tokens and outcome. */
|
|
178
258
|
attributed_pct?: number;
|
|
259
|
+
/** den#4: the highest average context per call of a main session (k tokens). */
|
|
260
|
+
long_session_context_k: number;
|
|
261
|
+
/** den#4: the share of cache reads from never-ending main sessions. */
|
|
262
|
+
never_ending_reread_pct: number;
|
|
263
|
+
/** den#4: relaunches from a handoff, over the agents listed. */
|
|
264
|
+
rotations?: number;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** A main session's average context per call, in tokens. */
|
|
268
|
+
export function contextPerCall(s: TranscriptStats): number {
|
|
269
|
+
const t = s.tokens.input + s.tokens.cacheRead + s.tokens.cacheWrite;
|
|
270
|
+
return s.calls ? t / s.calls : 0;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Never-ending: a main session that compacted more often than the limit. */
|
|
274
|
+
export function isNeverEnding(s: TranscriptStats, after: number): boolean {
|
|
275
|
+
return !s.subagent && s.compactions > after;
|
|
179
276
|
}
|
|
180
277
|
|
|
181
278
|
const round = (n: number, d = 0) => {
|
|
@@ -186,6 +283,8 @@ const round = (n: number, d = 0) => {
|
|
|
186
283
|
export function weeklyMetrics(
|
|
187
284
|
stats: readonly TranscriptStats[],
|
|
188
285
|
agents?: readonly AgentUsageRow[],
|
|
286
|
+
/** The rotation limit (agents.rotate.after_compactions, default 3). */
|
|
287
|
+
after = 3,
|
|
189
288
|
): UsageMetrics {
|
|
190
289
|
const sum = (
|
|
191
290
|
xs: readonly TranscriptStats[],
|
|
@@ -239,14 +338,28 @@ export function weeklyMetrics(
|
|
|
239
338
|
1,
|
|
240
339
|
),
|
|
241
340
|
subagent_opus_pct: round((100 * subOpus) / Math.max(1, subTotal)),
|
|
341
|
+
long_session_context_k: round(
|
|
342
|
+
Math.max(0, ...main.map(contextPerCall)) / 1e3,
|
|
343
|
+
),
|
|
344
|
+
never_ending_reread_pct: round(
|
|
345
|
+
(100 *
|
|
346
|
+
sum(
|
|
347
|
+
main.filter((s) => isNeverEnding(s, after)),
|
|
348
|
+
(s) => s.tokens.cacheRead,
|
|
349
|
+
)) /
|
|
350
|
+
Math.max(1, reads),
|
|
351
|
+
),
|
|
242
352
|
};
|
|
243
|
-
if (agents)
|
|
353
|
+
if (agents) {
|
|
354
|
+
m.attributed_pct = attributedPct(agents);
|
|
355
|
+
m.rotations = agents.reduce((a, r) => a + r.rotations, 0);
|
|
356
|
+
}
|
|
244
357
|
return m;
|
|
245
358
|
}
|
|
246
359
|
|
|
247
360
|
/** Higher is better for these; context only for calls, output and prompts. */
|
|
248
361
|
const BETTER_HIGH = new Set(["attributed_pct"]);
|
|
249
|
-
const CONTEXT = new Set(["calls", "output_M", "prompts"]);
|
|
362
|
+
const CONTEXT = new Set(["calls", "output_M", "prompts", "rotations"]);
|
|
250
363
|
|
|
251
364
|
/** The report as markdown: this week, last week, and the change. */
|
|
252
365
|
export function renderReport(
|
|
@@ -277,7 +390,7 @@ export function renderReport(
|
|
|
277
390
|
}
|
|
278
391
|
lines.push(
|
|
279
392
|
"",
|
|
280
|
-
"Read from this machine's transcripts;
|
|
393
|
+
"Read from this machine's transcripts; none of this was sent anywhere.",
|
|
281
394
|
);
|
|
282
395
|
return lines.join("\n") + "\n";
|
|
283
396
|
}
|
|
@@ -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];
|