@mnemom/mnemom 0.17.38-next.1 → 0.17.38-next.3
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/dist/commands/agent-run.d.ts +10 -2
- package/dist/commands/agent-run.js +18 -7
- package/dist/commands/scan.d.ts +1 -1
- package/dist/commands/scan.js +7 -14
- package/dist/index.js +1 -0
- package/dist/lib/runner/executor-client.d.ts +6 -0
- package/dist/lib/runner/executor-client.js +6 -0
- package/dist/lib/runner/job.d.ts +12 -3
- package/dist/lib/runner/job.js +30 -2
- package/dist/lib/runner/loop.d.ts +7 -2
- package/dist/lib/runner/loop.js +15 -3
- package/dist/lib/runner/options.d.ts +7 -0
- package/dist/lib/runner/options.js +11 -0
- package/dist/lib/scan/caps.d.ts +1 -35
- package/dist/lib/scan/caps.js +2 -67
- package/dist/lib/scan/cost-per-pr.d.ts +1 -93
- package/dist/lib/scan/cost-per-pr.js +2 -101
- package/dist/lib/scan/credential.d.ts +2 -26
- package/dist/lib/scan/credential.js +5 -58
- package/dist/lib/scan/failure.d.ts +1 -10
- package/dist/lib/scan/failure.js +2 -43
- package/dist/lib/scan/pr-source.d.ts +1 -57
- package/dist/lib/scan/pr-source.js +2 -132
- package/dist/lib/scan/report-email.d.ts +7 -104
- package/dist/lib/scan/report-email.js +25 -97
- package/dist/lib/scan/scan.d.ts +1 -69
- package/dist/lib/scan/scan.js +2 -195
- package/dist/lib/scan/share.d.ts +3 -1
- package/dist/lib/scan/spend/ai-tool-detect.d.ts +1 -6
- package/dist/lib/scan/spend/ai-tool-detect.js +2 -18
- package/dist/lib/scan/spend/anthropic-usage.d.ts +1 -76
- package/dist/lib/scan/spend/anthropic-usage.js +2 -269
- package/dist/lib/scan/spend/prices.d.ts +1 -33
- package/dist/lib/scan/spend/prices.js +2 -57
- package/dist/lib/scan/spend/spend-estimate.d.ts +1 -30
- package/dist/lib/scan/spend/spend-estimate.js +2 -70
- package/dist/lib/scan/spend/spend-tiers.d.ts +1 -16
- package/dist/lib/scan/spend/spend-tiers.js +6 -26
- package/dist/lib/scan/spend/token-rate.d.ts +1 -30
- package/dist/lib/scan/spend/token-rate.js +2 -54
- package/dist/vendor/durable-contract/index.js +19 -2
- package/dist/vendor/shared/durable-contract/cli-surface.js +2 -1
- package/dist/vendor/shared/durable-contract/codes.d.ts +12 -0
- package/dist/vendor/shared/durable-contract/codes.js +11 -0
- package/dist/vendor/shared/durable-contract/constants.d.ts +7 -1
- package/dist/vendor/shared/durable-contract/constants.js +7 -1
- package/dist/vendor/shared/durable-contract/doors.d.ts +9 -1
- package/dist/vendor/shared/durable-contract/doors.js +13 -0
- package/dist/vendor/shared/durable-contract/freeze-record.d.ts +1 -1
- package/dist/vendor/shared/durable-contract/schema.d.ts +9 -0
- package/dist/vendor/shared/durable-contract/schema.js +11 -0
- package/dist/vendor/shared/org-scan-core/caps.d.ts +35 -0
- package/dist/vendor/shared/org-scan-core/caps.js +68 -0
- package/dist/vendor/shared/org-scan-core/cost-per-pr.d.ts +93 -0
- package/dist/vendor/shared/org-scan-core/cost-per-pr.js +101 -0
- package/dist/vendor/shared/org-scan-core/credential.d.ts +43 -0
- package/dist/vendor/shared/org-scan-core/credential.js +74 -0
- package/dist/vendor/shared/org-scan-core/failure.d.ts +10 -0
- package/dist/vendor/shared/org-scan-core/failure.js +43 -0
- package/dist/vendor/shared/org-scan-core/index.d.ts +18 -0
- package/dist/vendor/shared/org-scan-core/index.js +19 -0
- package/dist/vendor/shared/org-scan-core/pr-source.d.ts +61 -0
- package/dist/vendor/shared/org-scan-core/pr-source.js +138 -0
- package/dist/vendor/shared/org-scan-core/report-build.d.ts +120 -0
- package/dist/vendor/shared/org-scan-core/report-build.js +119 -0
- package/dist/vendor/shared/org-scan-core/scan.d.ts +75 -0
- package/dist/vendor/shared/org-scan-core/scan.js +202 -0
- package/dist/vendor/shared/org-scan-core/spend/ai-tool-detect.d.ts +6 -0
- package/dist/vendor/shared/org-scan-core/spend/ai-tool-detect.js +18 -0
- package/dist/vendor/shared/org-scan-core/spend/anthropic-usage.d.ts +76 -0
- package/dist/vendor/shared/org-scan-core/spend/anthropic-usage.js +270 -0
- package/dist/{lib/scan → vendor/shared/org-scan-core}/spend/cost-corpus.v1.js +1 -0
- package/dist/vendor/shared/org-scan-core/spend/prices.d.ts +33 -0
- package/dist/vendor/shared/org-scan-core/spend/prices.js +58 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-estimate.d.ts +30 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-estimate.js +71 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-select.d.ts +16 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-select.js +26 -0
- package/dist/vendor/shared/org-scan-core/spend/token-rate.d.ts +30 -0
- package/dist/vendor/shared/org-scan-core/spend/token-rate.js +55 -0
- package/package.json +1 -1
- /package/dist/{lib/scan → vendor/shared/org-scan-core}/spend/cost-corpus.v1.d.ts +0 -0
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* errors exit 2 with no outcome line; every outcome line goes to stdout, every log to stderr.
|
|
9
9
|
*
|
|
10
10
|
* The GitHub token (GH_TOKEN, GITHUB_TOKEN, else `gh auth token`) is the runner's own: it reaches
|
|
11
|
-
* git only through GIT_CONFIG_* env (never argv) and the phases never see it (phaseEnv).
|
|
11
|
+
* git only through GIT_CONFIG_* env (never argv) and the phases never see it (phaseEnv). Under
|
|
12
|
+
* --oidc the PR is pushed and opened with the job's PR token instead (ruling 34.34): the api's
|
|
13
|
+
* Record App token for the seal's repo, fetched with the lease key, carried the same way.
|
|
12
14
|
*
|
|
13
15
|
* So is the Anthropic key (ruling 34.30.1): the phases' model calls are billed to the runner's own
|
|
14
16
|
* ANTHROPIC_API_KEY, which only the model proxy holds and sets on each upstream request. A runner
|
|
@@ -35,7 +37,9 @@ export interface RunDeps {
|
|
|
35
37
|
stop: AbortSignal;
|
|
36
38
|
/** The text of this host's executor-state.json, or null when there is none. */
|
|
37
39
|
readExecutorState?(): Promise<string | null>;
|
|
38
|
-
jobDeps(api: RunnerApi, executorId: string | null, github: GithubBases, modelKey: string
|
|
40
|
+
jobDeps(api: RunnerApi, executorId: string | null, github: GithubBases, modelKey: string,
|
|
41
|
+
/** True under --oidc: the job opens its PR with the api's PR token (ruling 34.34). */
|
|
42
|
+
prTokenFromLease: boolean): JobDeps;
|
|
39
43
|
}
|
|
40
44
|
/** The refusal of a runner with no ANTHROPIC_API_KEY (34.30.1); a config error, exit 2. */
|
|
41
45
|
export declare const RUNNER_MODEL_KEY_MISSING = "runner_model_key_missing";
|
|
@@ -78,6 +82,10 @@ export declare function liveJobDeps(api: RunnerApi, opts: {
|
|
|
78
82
|
executorId: string | null;
|
|
79
83
|
github: GithubBases;
|
|
80
84
|
modelKey: string;
|
|
85
|
+
/** The credential is the --oidc exchange: the PR uses the job's PR token (ruling 34.34). */
|
|
86
|
+
prTokenFromLease?: boolean;
|
|
87
|
+
/** The git runner for an env; tests inject one to see which credentials a push carries. */
|
|
88
|
+
makeGit?: (env: NodeJS.ProcessEnv) => Git;
|
|
81
89
|
}): JobDeps;
|
|
82
90
|
/** The process wiring: signals, stdout and stderr. Resolves the exit code. */
|
|
83
91
|
export declare function runAgentRun(flags: RunFlags): Promise<number>;
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* errors exit 2 with no outcome line; every outcome line goes to stdout, every log to stderr.
|
|
9
9
|
*
|
|
10
10
|
* The GitHub token (GH_TOKEN, GITHUB_TOKEN, else `gh auth token`) is the runner's own: it reaches
|
|
11
|
-
* git only through GIT_CONFIG_* env (never argv) and the phases never see it (phaseEnv).
|
|
11
|
+
* git only through GIT_CONFIG_* env (never argv) and the phases never see it (phaseEnv). Under
|
|
12
|
+
* --oidc the PR is pushed and opened with the job's PR token instead (ruling 34.34): the api's
|
|
13
|
+
* Record App token for the seal's repo, fetched with the lease key, carried the same way.
|
|
12
14
|
*
|
|
13
15
|
* So is the Anthropic key (ruling 34.30.1): the phases' model calls are billed to the runner's own
|
|
14
16
|
* ANTHROPIC_API_KEY, which only the model proxy holds and sets on each upstream request. A runner
|
|
@@ -90,8 +92,13 @@ export async function agentRunCommand(flags, deps) {
|
|
|
90
92
|
executorId = executorIdFromState(await deps.readExecutorState(), executorKey) ?? executorId;
|
|
91
93
|
}
|
|
92
94
|
const api = runnerApi({ apiUrl: deps.apiUrl, executorKey, fetch: deps.fetch });
|
|
93
|
-
const loop = {
|
|
94
|
-
|
|
95
|
+
const loop = {
|
|
96
|
+
failMode: plan.failMode,
|
|
97
|
+
stop: deps.stop,
|
|
98
|
+
emit: deps.emit,
|
|
99
|
+
...(plan.sessionId !== undefined ? { sessionId: plan.sessionId } : {}),
|
|
100
|
+
};
|
|
101
|
+
const job = deps.jobDeps(api, executorId, gh.bases, modelKey, plan.credential.kind === "oidc");
|
|
95
102
|
return plan.form === "once" ? runOnce(job, loop) : runWatch(job, loop);
|
|
96
103
|
}
|
|
97
104
|
// ── GitHub and git, for the live driver ──────────────────────────────────────
|
|
@@ -259,7 +266,10 @@ export function runPhaseProcess(inv, ws, modelUrl, signal, parentEnv = process.e
|
|
|
259
266
|
export function liveJobDeps(api, opts) {
|
|
260
267
|
const gh = opts.github;
|
|
261
268
|
const token = resolveGithubToken(opts.env);
|
|
262
|
-
const
|
|
269
|
+
const makeGit = opts.makeGit ?? liveGit;
|
|
270
|
+
const git = makeGit(gitAuthEnv(opts.env, token, gh.web));
|
|
271
|
+
// The PR token's git: a fresh env, so its header replaces the runner token's, never adds to it.
|
|
272
|
+
const gitFor = (t) => t === null ? git : makeGit(gitAuthEnv(opts.env, t, gh.web));
|
|
263
273
|
return {
|
|
264
274
|
api,
|
|
265
275
|
executorId: opts.executorId,
|
|
@@ -272,11 +282,12 @@ export function liveJobDeps(api, opts) {
|
|
|
272
282
|
startProxy: (state) => startModelProxy(`${getGatewayUrl()}/anthropic`, state, opts.modelKey),
|
|
273
283
|
prepareWorkspace: (repo, sessionId) => prepareWorkspace(git, `${gh.web}/${repo}.git`, sessionId),
|
|
274
284
|
runPhase: (inv, ws, modelUrl, signal) => runPhaseProcess(inv, ws, modelUrl, signal, opts.env),
|
|
275
|
-
openPr: (repo, ws, title, body) => openPr(
|
|
285
|
+
openPr: (repo, ws, title, body, prToken) => openPr(gitFor(prToken), opts.fetch, prToken ?? token, repo, ws, title, body, gh.api),
|
|
276
286
|
commitsAhead: (ws) => commitsAhead(git, ws),
|
|
277
287
|
headSha: (ws) => headSha(git, ws),
|
|
278
288
|
commitBodies: (ws, since, lineageKey, repo) => commitBodies(git, ws, since, lineageKey, repo),
|
|
279
|
-
enableAutoMerge: (prUrl) => enableAutoMerge(opts.fetch, token, prUrl, gh),
|
|
289
|
+
enableAutoMerge: (prUrl, prToken) => enableAutoMerge(opts.fetch, prToken ?? token, prUrl, gh),
|
|
290
|
+
prTokenFromLease: opts.prTokenFromLease === true,
|
|
280
291
|
cleanup: cleanupWorkspace,
|
|
281
292
|
};
|
|
282
293
|
}
|
|
@@ -296,7 +307,7 @@ export function runAgentRun(flags) {
|
|
|
296
307
|
err: (line) => process.stderr.write(`${line}\n`),
|
|
297
308
|
stop: stop.signal,
|
|
298
309
|
readExecutorState: () => readFile(executorStatePath(homedir()), "utf8").catch(() => null),
|
|
299
|
-
jobDeps: (api, executorId, github, modelKey) => liveJobDeps(api, { fetch, env: process.env, executorId, github, modelKey }),
|
|
310
|
+
jobDeps: (api, executorId, github, modelKey, prTokenFromLease) => liveJobDeps(api, { fetch, env: process.env, executorId, github, modelKey, prTokenFromLease }),
|
|
300
311
|
}).finally(() => {
|
|
301
312
|
process.off("SIGTERM", onSignal);
|
|
302
313
|
process.off("SIGINT", onSignal);
|
package/dist/commands/scan.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { selectSpend } from "../lib/scan/spend/spend-tiers.js";
|
|
|
10
10
|
import { type AnthropicUsageDeps, type MeasuredUsage } from "../lib/scan/spend/anthropic-usage.js";
|
|
11
11
|
import { type WifDeps } from "../lib/scan/spend/anthropic-wif.js";
|
|
12
12
|
import { type RecordDeps, type RecordOutcome } from "../lib/scan/record.js";
|
|
13
|
-
import {
|
|
13
|
+
import type { CostSpend } from "../lib/scan/cost-per-pr.js";
|
|
14
14
|
import { type ReportEmailDeps } from "../lib/scan/report-email.js";
|
|
15
15
|
import { type RepoListDeps } from "../lib/scan/repo-picker.js";
|
|
16
16
|
export interface ScanCommandOptions {
|
package/dist/commands/scan.js
CHANGED
|
@@ -15,7 +15,7 @@ import { getAuthInfo } from "../lib/auth.js";
|
|
|
15
15
|
import { listMyOrgs } from "../lib/api.js";
|
|
16
16
|
import { getActiveOrg } from "../lib/cli-config.js";
|
|
17
17
|
import { fetchRecordTotals, isOrgId, } from "../lib/scan/record.js";
|
|
18
|
-
import {
|
|
18
|
+
import { costSpendFor, sendsSpendRange } from "../vendor/shared/org-scan-core/report-build.js";
|
|
19
19
|
import { RECORD_NO_ORG_NOTE, renderRecordSection } from "../lib/scan/report.js";
|
|
20
20
|
import { buildEmailBody, buildEmailReport, DRY_RUN_MSG, UNAVAILABLE_MSG, failureMessage, ReportEmailError, sendReportEmail, } from "../lib/scan/report-email.js";
|
|
21
21
|
import { pickRepos, REPO_PICKER_NO_TERMINAL_HINT, } from "../lib/scan/repo-picker.js";
|
|
@@ -110,15 +110,8 @@ export async function resolveSelfReportedSpend(o) {
|
|
|
110
110
|
* tiers off) the modelled range. Sources are never mixed.
|
|
111
111
|
*/
|
|
112
112
|
export function resolveCostSpend(o, result, spend, measured) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (spend.tier === 1) {
|
|
116
|
-
const usd = measured?.usd ?? o.anthropicUsageUsd;
|
|
117
|
-
return typeof usd === "number" ? { source: "anthropic_usage", usd } : null;
|
|
118
|
-
}
|
|
119
|
-
if (spend.tier === 2)
|
|
120
|
-
return null;
|
|
121
|
-
return spend.tier === 3 ? modelledSpend(result.spend) : null;
|
|
113
|
+
// Shared with the Mnemom-side run (MNE-9752): same spend source, same cost figures.
|
|
114
|
+
return costSpendFor(result, spend, measured?.usd ?? o.anthropicUsageUsd);
|
|
122
115
|
}
|
|
123
116
|
export const NO_ADMIN_KEY_TERMINAL_MSG = "Set ANTHROPIC_ADMIN_KEY (there is no terminal to ask on).";
|
|
124
117
|
export const TIER1_FALLBACK_NOTE = "Tier 1 skipped: falling back to the next tier.";
|
|
@@ -353,21 +346,21 @@ export async function scanCommand(o) {
|
|
|
353
346
|
* returns it so the caller exits non-zero after the local report is shown.
|
|
354
347
|
*/
|
|
355
348
|
async function emailReport(o, org, result, level, spend, period, costSpend, extraEmails) {
|
|
356
|
-
const fail = (f) => {
|
|
357
|
-
console.log(failureMessage(f));
|
|
349
|
+
const fail = (f, retryAfterMs) => {
|
|
350
|
+
console.log(failureMessage(f, retryAfterMs));
|
|
358
351
|
return f;
|
|
359
352
|
};
|
|
360
353
|
const report = buildEmailReport(result, {
|
|
361
354
|
orgId: org,
|
|
362
355
|
now: o.now ?? Date.now,
|
|
363
|
-
sendSpendRange: spend
|
|
356
|
+
sendSpendRange: sendsSpendRange(spend),
|
|
364
357
|
period,
|
|
365
358
|
costSpend,
|
|
366
359
|
});
|
|
367
360
|
const body = buildEmailBody(report, level, result.coverage.complete, extraEmails);
|
|
368
361
|
const outcome = await sendReportEmail(org, body, o.emailDeps);
|
|
369
362
|
if (outcome.status === "failed")
|
|
370
|
-
return fail(outcome.reason);
|
|
363
|
+
return fail(outcome.reason, outcome.retryAfterMs);
|
|
371
364
|
if (outcome.status === "unavailable") {
|
|
372
365
|
// Org Scan is off by default: the gate's 404 is not a failure and must not break the scan.
|
|
373
366
|
console.log(UNAVAILABLE_MSG);
|
package/dist/index.js
CHANGED
|
@@ -1220,6 +1220,7 @@ agentCmd
|
|
|
1220
1220
|
.option("--fail-mode <mode>", "closed (default) or open; also MNEMOM_RUNNER_FAIL_MODE")
|
|
1221
1221
|
.option("--json", "Print only outcome lines on stdout; logs go to stderr")
|
|
1222
1222
|
.option("--oidc", "With --once: exchange the GitHub Actions ID token for a one-job key")
|
|
1223
|
+
.option("--session <uuid>", "With --once: claim only this session (a wake-up's own); never take another from the pool")
|
|
1223
1224
|
.hook("preAction", () => requireExperimental("`mnemom agent run`", "dispatch"))
|
|
1224
1225
|
.action(async (opts) => {
|
|
1225
1226
|
process.exitCode = await runAgentRun(opts);
|
|
@@ -34,6 +34,11 @@ export declare function errorDetails(body: unknown): Record<string, unknown>;
|
|
|
34
34
|
export declare const JOB_READ_PATH: (sessionId: string) => string;
|
|
35
35
|
/** The served bundle fetch, per claim (W-L12). */
|
|
36
36
|
export declare const BUNDLE_FETCH_PATH: (sessionId: string) => string;
|
|
37
|
+
/**
|
|
38
|
+
* The job's PR token (ruling 34.34): a Record App installation token narrowed to the seal's repo,
|
|
39
|
+
* contents and pull_requests write, fetched with the lease key. Never cached or logged.
|
|
40
|
+
*/
|
|
41
|
+
export declare const GITHUB_TOKEN_PATH: (sessionId: string) => string;
|
|
37
42
|
export declare const JWKS_PATH = "/v1/.well-known/jwks.json";
|
|
38
43
|
/** Record's commit index (ruling 34.05 (a)): the lease key may write the session's own commits. */
|
|
39
44
|
export declare const RECORD_COMMITS_PATH: (orgId: string) => string;
|
|
@@ -65,6 +70,7 @@ export interface RunnerApi {
|
|
|
65
70
|
fail(sessionId: string, body: Record<string, unknown>): Promise<HttpResult>;
|
|
66
71
|
job(sessionId: string): Promise<HttpResult>;
|
|
67
72
|
bundle(sessionId: string, leaseKey: string): Promise<HttpResult>;
|
|
73
|
+
githubToken(sessionId: string, leaseKey: string): Promise<HttpResult>;
|
|
68
74
|
jwks(): Promise<HttpResult>;
|
|
69
75
|
/** One RecordCommit/v1 body, with the lease key; the api pins it to the session's lineage. */
|
|
70
76
|
recordCommit(orgId: string, body: RecordCommitBody, leaseKey: string): Promise<HttpResult>;
|
|
@@ -33,6 +33,11 @@ export function errorDetails(body) {
|
|
|
33
33
|
export const JOB_READ_PATH = (sessionId) => `/v1/executor/sessions/${sessionId}/job`;
|
|
34
34
|
/** The served bundle fetch, per claim (W-L12). */
|
|
35
35
|
export const BUNDLE_FETCH_PATH = (sessionId) => `/v1/executor/sessions/${sessionId}/bundle`;
|
|
36
|
+
/**
|
|
37
|
+
* The job's PR token (ruling 34.34): a Record App installation token narrowed to the seal's repo,
|
|
38
|
+
* contents and pull_requests write, fetched with the lease key. Never cached or logged.
|
|
39
|
+
*/
|
|
40
|
+
export const GITHUB_TOKEN_PATH = (sessionId) => `/v1/executor/sessions/${sessionId}/github-token`;
|
|
36
41
|
export const JWKS_PATH = "/v1/.well-known/jwks.json";
|
|
37
42
|
/** Record's commit index (ruling 34.05 (a)): the lease key may write the session's own commits. */
|
|
38
43
|
export const RECORD_COMMITS_PATH = (orgId) => `/v1/orgs/${encodeURIComponent(orgId)}/record/commits`;
|
|
@@ -95,6 +100,7 @@ export function runnerApi(opts) {
|
|
|
95
100
|
fail: (id, body) => call(routePath("exec_fail", scoped(id)), "POST", ex, body),
|
|
96
101
|
job: (id) => call(JOB_READ_PATH(scoped(id)), "GET", ex),
|
|
97
102
|
bundle: (id, leaseKey) => call(BUNDLE_FETCH_PATH(scoped(id)), "GET", leaseKey),
|
|
103
|
+
githubToken: (id, leaseKey) => call(GITHUB_TOKEN_PATH(scoped(id)), "POST", leaseKey),
|
|
98
104
|
jwks: () => call(JWKS_PATH, "GET", null),
|
|
99
105
|
recordCommit: (orgId, body, leaseKey) => {
|
|
100
106
|
if (!ORG_ID_RE.test(orgId))
|
package/dist/lib/runner/job.d.ts
CHANGED
|
@@ -42,8 +42,11 @@ export interface JobDeps {
|
|
|
42
42
|
prepareWorkspace(repo: string, sessionId: string): Promise<Workspace>;
|
|
43
43
|
/** Run one phase's process; resolves its exit code. Rejects only when it could not start. */
|
|
44
44
|
runPhase(inv: PhaseInvocation, ws: Workspace, modelUrl: string, signal: AbortSignal): Promise<number>;
|
|
45
|
-
/**
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Push the job branch and open its PR; resolves the PR URL. `token` is the job's PR token when
|
|
47
|
+
* `prTokenFromLease` is set; null means the runner's own GitHub token.
|
|
48
|
+
*/
|
|
49
|
+
openPr(repo: string, ws: Workspace, title: string, body: string, token: string | null): Promise<string>;
|
|
47
50
|
/** Commits on the job branch that the base does not have; 0 means there is nothing to open. */
|
|
48
51
|
commitsAhead(ws: Workspace): Promise<number>;
|
|
49
52
|
/** The working copy's HEAD sha before a phase runs; null only for a clone with no commit. Throws otherwise. */
|
|
@@ -53,7 +56,13 @@ export interface JobDeps {
|
|
|
53
56
|
* first, under `lineageKey` and the job's repo. A commit that cannot be read is left out.
|
|
54
57
|
*/
|
|
55
58
|
commitBodies(ws: Workspace, since: string | null, lineageKey: string, repo: string): Promise<RecordCommitBody[]>;
|
|
56
|
-
enableAutoMerge(prUrl: string): Promise<void>;
|
|
59
|
+
enableAutoMerge(prUrl: string, token: string | null): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* True when the credential is the --oidc exchange: the PR is pushed and opened with the job's PR
|
|
62
|
+
* token from the api (ruling 34.34), never the runner's own GitHub token, which in a sealed repo's
|
|
63
|
+
* Action is a GITHUB_TOKEN the org forbids from opening PRs. A refused token fails the job.
|
|
64
|
+
*/
|
|
65
|
+
prTokenFromLease?: boolean;
|
|
57
66
|
cleanup(ws: Workspace): Promise<void>;
|
|
58
67
|
}
|
|
59
68
|
export interface JobOptions {
|
package/dist/lib/runner/job.js
CHANGED
|
@@ -379,6 +379,7 @@ export async function runJob(sessionId, opts, deps) {
|
|
|
379
379
|
});
|
|
380
380
|
}
|
|
381
381
|
if (opensPr) {
|
|
382
|
+
let prToken = null;
|
|
382
383
|
try {
|
|
383
384
|
// Checked before the push, so an empty diff never reaches GitHub's 422 (amendment 59).
|
|
384
385
|
if ((await deps.commitsAhead(ws)) === 0) {
|
|
@@ -386,7 +387,13 @@ export async function runJob(sessionId, opts, deps) {
|
|
|
386
387
|
await reportFail(deps, sessionId, claim.epoch, "runner_failed", "pr_no_changes");
|
|
387
388
|
return { kind: "outcome", outcome: failedOutcome(facts, "pr_no_changes") };
|
|
388
389
|
}
|
|
389
|
-
|
|
390
|
+
if (deps.prTokenFromLease) {
|
|
391
|
+
const t = await fetchPrToken(deps, sessionId, claim.leaseKey, job.repo);
|
|
392
|
+
if (!t.ok)
|
|
393
|
+
throw new Error(t.detail);
|
|
394
|
+
prToken = t.token;
|
|
395
|
+
}
|
|
396
|
+
facts.pr_url = await deps.openPr(job.repo, ws, `mnemom: ${bundle.workflow.id} job ${sessionId.slice(0, 8)}`, prBody(job), prToken);
|
|
390
397
|
}
|
|
391
398
|
catch (e) {
|
|
392
399
|
deps.log(`opening the PR failed: ${e.message}`);
|
|
@@ -395,7 +402,7 @@ export async function runJob(sessionId, opts, deps) {
|
|
|
395
402
|
}
|
|
396
403
|
if (facts.merge_strategy === "merge_on_green") {
|
|
397
404
|
try {
|
|
398
|
-
await deps.enableAutoMerge(facts.pr_url);
|
|
405
|
+
await deps.enableAutoMerge(facts.pr_url, prToken);
|
|
399
406
|
}
|
|
400
407
|
catch (e) {
|
|
401
408
|
deps.log(`enabling auto-merge failed (the PR stays open): ${e.message}`);
|
|
@@ -469,6 +476,27 @@ export async function reportPhaseCommits(deps, report, p) {
|
|
|
469
476
|
deps.log(`commit report failed: ${e.message}`);
|
|
470
477
|
}
|
|
471
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* The job's PR token (ruling 34.34), fetched once per claim with the lease key. It must name the
|
|
481
|
+
* job's repo; the detail never carries the token.
|
|
482
|
+
*/
|
|
483
|
+
async function fetchPrToken(deps, sessionId, leaseKey, repo) {
|
|
484
|
+
const res = await deps.api.githubToken(sessionId, leaseKey);
|
|
485
|
+
if (res.status !== 200) {
|
|
486
|
+
return {
|
|
487
|
+
ok: false,
|
|
488
|
+
detail: `the PR token fetch answered ${res.code ?? res.networkError ?? res.status}`,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const b = (res.body ?? {});
|
|
492
|
+
if (typeof b.token !== "string" || b.token === "") {
|
|
493
|
+
return { ok: false, detail: "the PR token fetch answered 200 with no token" };
|
|
494
|
+
}
|
|
495
|
+
if (typeof b.repository !== "string" || b.repository.toLowerCase() !== repo.toLowerCase()) {
|
|
496
|
+
return { ok: false, detail: `the PR token names another repo than the job's ${repo}` };
|
|
497
|
+
}
|
|
498
|
+
return { ok: true, token: b.token };
|
|
499
|
+
}
|
|
472
500
|
async function fetchBundle(deps, sessionId, claim) {
|
|
473
501
|
const [jwksRes, res] = await Promise.all([
|
|
474
502
|
deps.api.jwks(),
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* --once: list the ready pool sessions (exec_ready_list, oldest first), try each in order, and run
|
|
5
5
|
* at most one job. A lost race, or an ineligible waiting / not_pool_member, moves to the next
|
|
6
|
-
* candidate; an empty list, or every candidate lost, ends no_job. --
|
|
7
|
-
*
|
|
6
|
+
* candidate; an empty list, or every candidate lost, ends no_job. --once --session <id> (the
|
|
7
|
+
* wake-up's own session, #1329): try exactly that session and never list the pool; a 404 attest, a
|
|
8
|
+
* lost race or a skippable ineligible ends no_job, and no other session is tried, so a wake-up
|
|
9
|
+
* dispatched for one repo's session never works another's. --watch: the same as plain --once, again
|
|
10
|
+
* and again, until SIGTERM or SIGINT. Every job prints exactly one outcome line (none when stopped
|
|
8
11
|
* mid-job), and the process exit code is the --once outcome's, or 0 for --watch stopped.
|
|
9
12
|
*
|
|
10
13
|
* Every side effect is in `JobDeps` (plus `emit`), so each arm is tested; `liveJobDeps`
|
|
@@ -21,6 +24,8 @@ export interface LoopOptions {
|
|
|
21
24
|
emit(line: string): void;
|
|
22
25
|
/** --watch: wait between empty polls, ms. */
|
|
23
26
|
idlePollMs?: number;
|
|
27
|
+
/** --once --session: the one session to try; the ready list is never read. */
|
|
28
|
+
sessionId?: string;
|
|
24
29
|
}
|
|
25
30
|
export declare const WATCH_IDLE_POLL_MS = 15000;
|
|
26
31
|
/** --watch backs off on a ready-list the api cannot answer, up to this, ms. */
|
package/dist/lib/runner/loop.js
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* --once: list the ready pool sessions (exec_ready_list, oldest first), try each in order, and run
|
|
5
5
|
* at most one job. A lost race, or an ineligible waiting / not_pool_member, moves to the next
|
|
6
|
-
* candidate; an empty list, or every candidate lost, ends no_job. --
|
|
7
|
-
*
|
|
6
|
+
* candidate; an empty list, or every candidate lost, ends no_job. --once --session <id> (the
|
|
7
|
+
* wake-up's own session, #1329): try exactly that session and never list the pool; a 404 attest, a
|
|
8
|
+
* lost race or a skippable ineligible ends no_job, and no other session is tried, so a wake-up
|
|
9
|
+
* dispatched for one repo's session never works another's. --watch: the same as plain --once, again
|
|
10
|
+
* and again, until SIGTERM or SIGINT. Every job prints exactly one outcome line (none when stopped
|
|
8
11
|
* mid-job), and the process exit code is the --once outcome's, or 0 for --watch stopped.
|
|
9
12
|
*
|
|
10
13
|
* Every side effect is in `JobDeps` (plus `emit`), so each arm is tested; `liveJobDeps`
|
|
@@ -58,9 +61,18 @@ async function onePass(deps, opts) {
|
|
|
58
61
|
}
|
|
59
62
|
return { kind: "none" };
|
|
60
63
|
}
|
|
64
|
+
/** --once --session: that session's outcome, or none; never another session. */
|
|
65
|
+
async function sessionPass(deps, opts, id) {
|
|
66
|
+
if (opts.stop.aborted)
|
|
67
|
+
return { kind: "stopped" };
|
|
68
|
+
const r = await runJob(id, { failMode: opts.failMode, stop: opts.stop }, deps);
|
|
69
|
+
return r.kind === "next" ? { kind: "none" } : r;
|
|
70
|
+
}
|
|
61
71
|
/** --once: at most one job. Resolves the process exit code. */
|
|
62
72
|
export async function runOnce(deps, opts) {
|
|
63
|
-
const p =
|
|
73
|
+
const p = opts.sessionId === undefined
|
|
74
|
+
? await onePass(deps, opts)
|
|
75
|
+
: await sessionPass(deps, opts, opts.sessionId);
|
|
64
76
|
if (p.kind === "stopped")
|
|
65
77
|
return 0;
|
|
66
78
|
const outcome = p.kind === "outcome" ? p.outcome : noJobOutcome();
|
|
@@ -16,6 +16,8 @@ export interface RunFlags {
|
|
|
16
16
|
failMode?: string;
|
|
17
17
|
json?: boolean;
|
|
18
18
|
oidc?: boolean;
|
|
19
|
+
/** --session <uuid>: with --once, claim exactly this session and never list the pool. */
|
|
20
|
+
session?: string;
|
|
19
21
|
}
|
|
20
22
|
export type RunCredential = {
|
|
21
23
|
kind: "executor_key";
|
|
@@ -34,6 +36,11 @@ export interface RunPlan {
|
|
|
34
36
|
executorId: string | null;
|
|
35
37
|
/** Warnings to print on stderr (an unknown fail mode reads as closed). */
|
|
36
38
|
warnings: string[];
|
|
39
|
+
/**
|
|
40
|
+
* --session: the one session this --once run may claim (the wake-up's own, #1329). Absent: the
|
|
41
|
+
* pool's ready list, oldest first, as before.
|
|
42
|
+
*/
|
|
43
|
+
sessionId?: string;
|
|
37
44
|
}
|
|
38
45
|
export type RunPlanResult = {
|
|
39
46
|
ok: true;
|
|
@@ -22,6 +22,16 @@ export function resolveRunPlan(flags, env) {
|
|
|
22
22
|
if (flags.oidc && form === "watch") {
|
|
23
23
|
return usage("mnemom agent run: --oidc is valid only with --once");
|
|
24
24
|
}
|
|
25
|
+
let sessionId;
|
|
26
|
+
if (flags.session !== undefined) {
|
|
27
|
+
if (form === "watch") {
|
|
28
|
+
return usage("mnemom agent run: --session is valid only with --once");
|
|
29
|
+
}
|
|
30
|
+
if (!UUID_RE.test(flags.session)) {
|
|
31
|
+
return usage("mnemom agent run: --session is not a lowercase uuid");
|
|
32
|
+
}
|
|
33
|
+
sessionId = flags.session;
|
|
34
|
+
}
|
|
25
35
|
const fail = resolveFailMode(flags.failMode, env[RUNNER_FAIL_MODE_ENV]);
|
|
26
36
|
const warnings = fail.warning ? [`mnemom agent run: ${fail.warning}`] : [];
|
|
27
37
|
let credential;
|
|
@@ -60,6 +70,7 @@ export function resolveRunPlan(flags, env) {
|
|
|
60
70
|
credential,
|
|
61
71
|
executorId,
|
|
62
72
|
warnings,
|
|
73
|
+
...(sessionId !== undefined ? { sessionId } : {}),
|
|
63
74
|
},
|
|
64
75
|
};
|
|
65
76
|
}
|
package/dist/lib/scan/caps.d.ts
CHANGED
|
@@ -1,35 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Coverage caps and sampling. Port of mnemom-api src/org-scan/scan/capped-scan.ts (PR #4208).
|
|
3
|
-
*
|
|
4
|
-
* Coverage decision (Shraddha, 2026-10-28): each scan covers the last 3 months OR the last 2,000 merged
|
|
5
|
-
* PRs, whichever is MORE; above 2,000 in the window, an even deterministic sample of 2,000.
|
|
6
|
-
*/
|
|
7
|
-
export declare const COVERAGE_MONTHS = 3;
|
|
8
|
-
export interface ScanCaps {
|
|
9
|
-
maxPrs: number;
|
|
10
|
-
/** A PR with more additions+deletions than this is skipped, never read in full. */
|
|
11
|
-
maxPrSizeLines: number;
|
|
12
|
-
/** Upstream read calls. */
|
|
13
|
-
maxReads: number;
|
|
14
|
-
/** Wall-clock budget. */
|
|
15
|
-
maxMillis: number;
|
|
16
|
-
}
|
|
17
|
-
export declare const DEFAULT_SCAN_CAPS: ScanCaps;
|
|
18
|
-
export type StopReason = "pr-cap" | "read-cap" | "time-cap";
|
|
19
|
-
/** The day `months` calendar months before `end` (YYYY-MM-DD, UTC), clamped to the month's last day. */
|
|
20
|
-
export declare function monthsBefore(end: string, months: number): string;
|
|
21
|
-
/** `k` items spread evenly across `items` (order kept): index floor(i*n/k). Deterministic; n <= k returns all. */
|
|
22
|
-
export declare function evenSample<T>(items: readonly T[], k: number): T[];
|
|
23
|
-
export declare function sanitiseCaps(c: Partial<ScanCaps>): ScanCaps;
|
|
24
|
-
/** Running read budget. Every call is told how many reads it may spend (`room`). */
|
|
25
|
-
export declare class Budget {
|
|
26
|
-
private caps;
|
|
27
|
-
private now;
|
|
28
|
-
private startedAt;
|
|
29
|
-
reads: number;
|
|
30
|
-
constructor(caps: ScanCaps, now: () => number, startedAt: number);
|
|
31
|
-
block(n?: number): StopReason | null;
|
|
32
|
-
room(): number;
|
|
33
|
-
overrun(): StopReason | null;
|
|
34
|
-
charge(n: number): void;
|
|
35
|
-
}
|
|
1
|
+
export * from "../../vendor/shared/org-scan-core/caps.js";
|
package/dist/lib/scan/caps.js
CHANGED
|
@@ -1,67 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Coverage decision (Shraddha, 2026-10-28): each scan covers the last 3 months OR the last 2,000 merged
|
|
5
|
-
* PRs, whichever is MORE; above 2,000 in the window, an even deterministic sample of 2,000.
|
|
6
|
-
*/
|
|
7
|
-
export const COVERAGE_MONTHS = 3;
|
|
8
|
-
export const DEFAULT_SCAN_CAPS = {
|
|
9
|
-
maxPrs: 2_000,
|
|
10
|
-
maxPrSizeLines: 20_000,
|
|
11
|
-
maxReads: 4_500,
|
|
12
|
-
maxMillis: 15 * 60_000,
|
|
13
|
-
};
|
|
14
|
-
/** The day `months` calendar months before `end` (YYYY-MM-DD, UTC), clamped to the month's last day. */
|
|
15
|
-
export function monthsBefore(end, months) {
|
|
16
|
-
const [y, m, d] = end.split("-").map(Number);
|
|
17
|
-
const target = new Date(Date.UTC(y, m - 1 - months, 1));
|
|
18
|
-
const last = new Date(Date.UTC(target.getUTCFullYear(), target.getUTCMonth() + 1, 0)).getUTCDate();
|
|
19
|
-
target.setUTCDate(Math.min(d, last));
|
|
20
|
-
return target.toISOString().slice(0, 10);
|
|
21
|
-
}
|
|
22
|
-
/** `k` items spread evenly across `items` (order kept): index floor(i*n/k). Deterministic; n <= k returns all. */
|
|
23
|
-
export function evenSample(items, k) {
|
|
24
|
-
const n = items.length;
|
|
25
|
-
if (n <= k)
|
|
26
|
-
return [...items];
|
|
27
|
-
return Array.from({ length: k }, (_, i) => items[Math.floor((i * n) / k)]);
|
|
28
|
-
}
|
|
29
|
-
export function sanitiseCaps(c) {
|
|
30
|
-
const pos = (n, d) => typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.floor(n) : d;
|
|
31
|
-
return {
|
|
32
|
-
maxPrs: pos(c.maxPrs, DEFAULT_SCAN_CAPS.maxPrs),
|
|
33
|
-
maxPrSizeLines: pos(c.maxPrSizeLines, DEFAULT_SCAN_CAPS.maxPrSizeLines),
|
|
34
|
-
maxReads: pos(c.maxReads, DEFAULT_SCAN_CAPS.maxReads),
|
|
35
|
-
maxMillis: pos(c.maxMillis, DEFAULT_SCAN_CAPS.maxMillis),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
/** Running read budget. Every call is told how many reads it may spend (`room`). */
|
|
39
|
-
export class Budget {
|
|
40
|
-
caps;
|
|
41
|
-
now;
|
|
42
|
-
startedAt;
|
|
43
|
-
reads = 0;
|
|
44
|
-
constructor(caps, now, startedAt) {
|
|
45
|
-
this.caps = caps;
|
|
46
|
-
this.now = now;
|
|
47
|
-
this.startedAt = startedAt;
|
|
48
|
-
}
|
|
49
|
-
block(n = 1) {
|
|
50
|
-
if (this.now() - this.startedAt >= this.caps.maxMillis)
|
|
51
|
-
return "time-cap";
|
|
52
|
-
if (this.reads + n > this.caps.maxReads)
|
|
53
|
-
return "read-cap";
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
room() {
|
|
57
|
-
if (this.now() - this.startedAt >= this.caps.maxMillis)
|
|
58
|
-
return 0;
|
|
59
|
-
return Math.max(0, this.caps.maxReads - this.reads);
|
|
60
|
-
}
|
|
61
|
-
overrun() {
|
|
62
|
-
return this.reads > this.caps.maxReads ? "read-cap" : null;
|
|
63
|
-
}
|
|
64
|
-
charge(n) {
|
|
65
|
-
this.reads += n;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
1
|
+
// Moved to shared/org-scan-core (MNE-9752); vendored at cli/src/vendor/shared/org-scan-core. Re-exported so CLI imports stay put.
|
|
2
|
+
export * from "../../vendor/shared/org-scan-core/caps.js";
|
|
@@ -1,93 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Cost per merged PR and cost by PR size, for the Org Scan report. Pure: numbers in, numbers out.
|
|
3
|
-
*
|
|
4
|
-
* Source of the spend. Measured per-request cost cannot be tied to a PR with the data the scan has:
|
|
5
|
-
* the Anthropic usage reads return one whole-org total (no per-request or per-PR rows) and the Mnemom
|
|
6
|
-
* Record totals are one window sum (model cost and a request count, no PR key). So every figure here is
|
|
7
|
-
* `label: "estimate"`: the period's spend split across the merged PRs in proportion to lines changed.
|
|
8
|
-
* Each figure names the spend it was split from (`spend_source`); sources are never mixed in one report.
|
|
9
|
-
*
|
|
10
|
-
* anthropic_usage the whole-org Anthropic cost for the period (the CTO's admin key)
|
|
11
|
-
* modelled_range our modelled estimate (a low to high range, so the figures are ranges)
|
|
12
|
-
*
|
|
13
|
-
* Size is per PR, in lines changed = additions + deletions, exactly as GitHub reports them. Lockfiles and
|
|
14
|
-
* generated files are NOT excluded. small < 50; medium 50..500 inclusive; large > 500.
|
|
15
|
-
*
|
|
16
|
-
* Each size figure is the average cost of ONE PR of that size: spend * (mean lines of the bucket) /
|
|
17
|
-
* (total lines of the PRs in scope). Scope: the modelled estimate prices AI-assisted PRs only, so it is
|
|
18
|
-
* split across those only (`pr_scope: "ai_assisted"`, buckets too); the others use every merged PR
|
|
19
|
-
* (`pr_scope: "all_merged"`). `pr_count` counts PRs in that scope. Usage-key figures also carry
|
|
20
|
-
* `spend_scope: "whole_org"` (the key covers the whole organization, not just the scanned repos). The cost per merged PR is the spend over the count of merged PRs.
|
|
21
|
-
*
|
|
22
|
-
* Missing data is an absent field, never 0 or NaN: no usable spend, zero merged PRs in the period, an
|
|
23
|
-
* empty bucket, a scan that did not read every merged PR in the period, or a result past safe-integer
|
|
24
|
-
* range. Reverts are listed without sizes, so they are not counted as merged PRs here.
|
|
25
|
-
*/
|
|
26
|
-
import type { SpendEstimate } from "./spend/spend-estimate.js";
|
|
27
|
-
/** Strictly below this is small. 50 is medium. */
|
|
28
|
-
export declare const SMALL_MAX_EXCLUSIVE = 50;
|
|
29
|
-
/** At or below this (and not small) is medium. 500 is medium; 501 is large. */
|
|
30
|
-
export declare const MEDIUM_MAX_INCLUSIVE = 500;
|
|
31
|
-
export type SizeBucket = "small" | "medium" | "large";
|
|
32
|
-
export type CostSpendSourceName = "anthropic_usage" | "modelled_range";
|
|
33
|
-
export interface PrSize {
|
|
34
|
-
/** YYYY-MM-DD, UTC. */
|
|
35
|
-
mergedDay: string;
|
|
36
|
-
/** additions + deletions. */
|
|
37
|
-
lines: number;
|
|
38
|
-
/** The PR carries an AI-tool mark (the same rule the modelled spend uses to count AI-assisted PRs). */
|
|
39
|
-
aiAssisted: boolean;
|
|
40
|
-
}
|
|
41
|
-
/** Where the spend comes from. USD, never cents. */
|
|
42
|
-
export type CostSpend = {
|
|
43
|
-
source: "anthropic_usage";
|
|
44
|
-
usd: number;
|
|
45
|
-
} | {
|
|
46
|
-
source: "modelled_range";
|
|
47
|
-
lowUsd: number;
|
|
48
|
-
highUsd: number;
|
|
49
|
-
};
|
|
50
|
-
/** One figure: a single value, or a low to high range for the modelled source. Integer cents. */
|
|
51
|
-
export type PrScope = "ai_assisted" | "all_merged";
|
|
52
|
-
export type CostFigure = {
|
|
53
|
-
/** "measured" is not possible with current data (see header); every figure is an estimate. */
|
|
54
|
-
label: "estimate";
|
|
55
|
-
spend_source: CostSpendSourceName;
|
|
56
|
-
/**
|
|
57
|
-
* Which PRs the average is over. The modelled estimate prices AI-assisted PRs only, so it is split
|
|
58
|
-
* across those only ("ai_assisted"); the usage key covers all spend, so they use "all_merged".
|
|
59
|
-
*/
|
|
60
|
-
pr_scope: PrScope;
|
|
61
|
-
/** PRs in that scope. */
|
|
62
|
-
pr_count: number;
|
|
63
|
-
/** Present on anthropic_usage only: the key's usage is the whole organization's, not just these PRs'. */
|
|
64
|
-
spend_scope?: "whole_org";
|
|
65
|
-
} & ({
|
|
66
|
-
value_cents: number;
|
|
67
|
-
} | {
|
|
68
|
-
low_cents: number;
|
|
69
|
-
high_cents: number;
|
|
70
|
-
});
|
|
71
|
-
export interface CostPerPr {
|
|
72
|
-
cost_per_merged_pr?: CostFigure;
|
|
73
|
-
cost_by_pr_size?: Partial<Record<SizeBucket, CostFigure>>;
|
|
74
|
-
}
|
|
75
|
-
export declare const sizeBucket: (lines: number) => SizeBucket;
|
|
76
|
-
/** Inclusive day count of YYYY-MM-DD..YYYY-MM-DD, or null if unparsable or reversed. */
|
|
77
|
-
export declare function periodDays(p: {
|
|
78
|
-
start: string;
|
|
79
|
-
end: string;
|
|
80
|
-
}): number | null;
|
|
81
|
-
/** The period's spend in USD as [low, high]; null when there is no usable spend. */
|
|
82
|
-
export declare function spendForPeriod(spend: CostSpend): [number, number] | null;
|
|
83
|
-
/** The modelled estimate as a spend source, or null when the estimate is "none". */
|
|
84
|
-
export declare const modelledSpend: (e: SpendEstimate) => CostSpend | null;
|
|
85
|
-
/**
|
|
86
|
-
* @param prs every merged PR the scan read, with merge day and size
|
|
87
|
-
* @param exhaustive true only if the scan read EVERY merged PR in the period (no sample, no skipped
|
|
88
|
-
* large PR, none unreadable, no cap hit). Otherwise the average is not defined.
|
|
89
|
-
*/
|
|
90
|
-
export declare function buildCostPerPr(prs: readonly PrSize[] | undefined, period: {
|
|
91
|
-
start: string;
|
|
92
|
-
end: string;
|
|
93
|
-
}, spend: CostSpend | null, exhaustive: boolean): CostPerPr;
|
|
1
|
+
export * from "../../vendor/shared/org-scan-core/cost-per-pr.js";
|