From 05c430b29320f21f40b6de46adcef35f54aada1c Mon Sep 17 00:00:00 2001 From: andriypolandki <=> Date: Tue, 7 Jul 2026 00:32:23 -0700 Subject: [PATCH 1/3] feat(review): record review-memory suppression on @gittensory resolve (#1964) --- .../site/app-panels/miner-panel.tsx | 5 +- .../src/lib/maintainer-settings-preview.ts | 6 +- .../src/lib/registration-workspace.ts | 6 +- apps/gittensory-ui/src/lib/snapshot-replay.ts | 8 +- apps/gittensory-ui/src/routes/docs.tuning.tsx | 8 +- src/queue/processors.ts | 32 ++ src/review/review-memory-wire.ts | 3 + test/unit/queue.test.ts | 424 ++++++++++++++++++ test/unit/review-memory-record.test.ts | 62 +++ test/unit/sweep-watchdog.test.ts | 4 +- worker-configuration.d.ts | 154 +------ 11 files changed, 553 insertions(+), 159 deletions(-) create mode 100644 test/unit/review-memory-record.test.ts diff --git a/apps/gittensory-ui/src/components/site/app-panels/miner-panel.tsx b/apps/gittensory-ui/src/components/site/app-panels/miner-panel.tsx index 5c549ebfd5..119ef35b00 100644 --- a/apps/gittensory-ui/src/components/site/app-panels/miner-panel.tsx +++ b/apps/gittensory-ui/src/components/site/app-panels/miner-panel.tsx @@ -38,7 +38,10 @@ const CHANGE_TONE: Record = { }; type RecommendationSignalGroup = - "repo_state" | "contributor_state" | "validation_state" | "policy_context"; + | "repo_state" + | "contributor_state" + | "validation_state" + | "policy_context"; type RecommendationChange = { status: "new" | "changed" | "unchanged"; diff --git a/apps/gittensory-ui/src/lib/maintainer-settings-preview.ts b/apps/gittensory-ui/src/lib/maintainer-settings-preview.ts index 3c160bca7c..1b13b72bef 100644 --- a/apps/gittensory-ui/src/lib/maintainer-settings-preview.ts +++ b/apps/gittensory-ui/src/lib/maintainer-settings-preview.ts @@ -11,7 +11,11 @@ export type AuthorAssociation = | "NONE"; export type PreviewScenarioId = - "confirmed-miner" | "non-miner" | "bot-author" | "maintainer-author" | "miner-api-unavailable"; + | "confirmed-miner" + | "non-miner" + | "bot-author" + | "maintainer-author" + | "miner-api-unavailable"; export type PreviewScenario = { id: PreviewScenarioId; diff --git a/apps/gittensory-ui/src/lib/registration-workspace.ts b/apps/gittensory-ui/src/lib/registration-workspace.ts index 6d86178fc2..044a303ba1 100644 --- a/apps/gittensory-ui/src/lib/registration-workspace.ts +++ b/apps/gittensory-ui/src/lib/registration-workspace.ts @@ -63,7 +63,11 @@ export type OwnerWorkflowState = "accepted" | "needs_cleanup" | "not_ready"; export type OwnerWorkflowRemediationKind = "action" | "manual"; export type OwnerWorkflowBucketId = - "policy" | "data_quality" | "queue_health" | "docs_onboarding" | "maintainer_capacity"; + | "policy" + | "data_quality" + | "queue_health" + | "docs_onboarding" + | "maintainer_capacity"; export type OwnerWorkflowItem = { id: string; diff --git a/apps/gittensory-ui/src/lib/snapshot-replay.ts b/apps/gittensory-ui/src/lib/snapshot-replay.ts index 3ad1c16942..1a2108b3d6 100644 --- a/apps/gittensory-ui/src/lib/snapshot-replay.ts +++ b/apps/gittensory-ui/src/lib/snapshot-replay.ts @@ -14,7 +14,13 @@ export type SnapshotReplayViewer = "public" | "authenticated"; export type SnapshotReplayStatus = "populated" | "stale" | "missing"; export type SnapshotReplayConfidence = "high" | "medium" | "low" | "unknown"; export type SnapshotReplayFreshness = - "fresh" | "stale" | "rebuilding" | "missing" | "degraded" | "possibly_stale" | "unknown"; + | "fresh" + | "stale" + | "rebuilding" + | "missing" + | "degraded" + | "possibly_stale" + | "unknown"; export type SnapshotReplaySource = { name: string; diff --git a/apps/gittensory-ui/src/routes/docs.tuning.tsx b/apps/gittensory-ui/src/routes/docs.tuning.tsx index 86b5d47bf0..46bac21749 100644 --- a/apps/gittensory-ui/src/routes/docs.tuning.tsx +++ b/apps/gittensory-ui/src/routes/docs.tuning.tsx @@ -157,9 +157,11 @@ function Tuning() { GITTENSORY_REVIEW_MEMORY — repeat-false-positive suppression: matches an advisory (non-blocking) AI finding against this repo's stored suppression signals (a maintainer's own past false-positive dismissals) and demotes or drops it before the - unified comment renders. Advisory-only by construction — never applied to gate blockers, - so it can never change the merge/close disposition. Also requires the per-repo{" "} - review.memory: true opt-in in .gittensory.yml. Per-PR. + unified comment renders. A maintainer records a signal with{" "} + @gittensory resolve [finding-code] (or a whole-PR{" "} + @gittensory resolve ack). Advisory-only by construction — never applied to + gate blockers, so it can never change the merge/close disposition. Also requires the + per-repo review.memory: true opt-in in .gittensory.yml. Per-PR.
  • GITTENSORY_REVIEW_REPUTATION — submitter-reputation spend control. A new, diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 8fc8791851..61923ceced 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -5460,6 +5460,7 @@ async function processGitHubWebhook( return; } + if (eventName === "issue_comment" && (await maybeProcessResolveCommand(env, deliveryId, payload))) { await recordWebhookEvent(env, { deliveryId, eventName, action: payload.action, installationId: payload.installation?.id, repositoryFullName: payload.repository?.full_name, payloadHash: "processed", status: "processed" }); return; } if ( eventName === "issue_comment" && (await maybeProcessPlanCommand(env, deliveryId, payload)) @@ -10287,6 +10288,37 @@ async function recordGateOverrideSkip( }); } +async function maybeProcessResolveCommand(env: Env, deliveryId: string, payload: GitHubWebhookPayload): Promise { const command = parseGittensoryMentionCommand(payload.comment?.body); + if (!command) return false; + if (command.name !== "resolve") return false; + const { classifyPrCommandRequest } = await import("../github/pr-command-request"); + const { normalizeResolveFindingRef, selectWarningsForResolve } = await import("../review/review-memory-wire"); + const { fingerprint } = await import("../review/review-memory-match"); + const req = classifyPrCommandRequest(payload, getInstallationId(payload)); + if (!req.ok) { await recordAuditEvent(env, { eventType: "github_app.finding_resolved_skipped", actor: req.actor, targetKey: req.targetKey, outcome: "completed", detail: req.reason, metadata: { deliveryId, repoFullName: req.repoFullName ?? null, reason: req.reason } }); await recordGithubProductUsage(env, "finding_resolved_skipped", { actor: req.actor, repoFullName: req.repoFullName, targetKey: req.targetKey, outcome: "skipped", metadata: { reason: req.reason } }); return true; } + const [pr, settings] = await Promise.all([getPullRequest(env, req.repoFullName, req.pr.number), resolveRepositorySettings(env, req.repoFullName)]); + const targetKey = `${req.repoFullName}#${req.pr.number}`; + if (!pr) { await recordAuditEvent(env, { eventType: "github_app.finding_resolved_skipped", actor: req.actor, targetKey, outcome: "completed", detail: "cached_pr_missing", metadata: { deliveryId, repoFullName: req.repoFullName, reason: "cached_pr_missing" } }); await recordGithubProductUsage(env, "finding_resolved_skipped", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "skipped", metadata: { reason: "cached_pr_missing" } }); return true; } + const { authorization } = await authorizePrActionActor({ env, deliveryId, installationId: req.installationId, repoFullName: req.repoFullName, issue: payload.issue!, actor: req.actor, commandName: "resolve" as GittensoryMentionCommandName, settings, pr }); + if (!authorization.authorized) { await recordAuditEvent(env, { eventType: "github_app.finding_resolved_denied", actor: req.actor, targetKey, outcome: "denied", detail: authorization.reason, metadata: { deliveryId, repoFullName: req.repoFullName, allowedRoles: commandAuthorizationAllowedRoles(settings.commandAuthorization, "resolve") } }); await recordGithubProductUsage(env, "finding_resolved_denied", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "denied", metadata: { reason: authorization.reason, actorKind: authorization.actorKind, allowedRoles: commandAuthorizationAllowedRoles(settings.commandAuthorization, "resolve") } }); return true; } + const findingRef = normalizeResolveFindingRef(command.reason); + if (!findingRef.ok) { await recordAuditEvent(env, { eventType: "github_app.finding_resolved_skipped", actor: req.actor, targetKey, outcome: "completed", detail: findingRef.reason, metadata: { deliveryId, repoFullName: req.repoFullName, reason: findingRef.reason } }); await recordGithubProductUsage(env, "finding_resolved_skipped", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "skipped", metadata: { reason: findingRef.reason } }); return true; } + const { advisory } = await buildAuthorizedPrActionAdvisory(env, req.repoFullName, pr, settings); + const gate = evaluateGateCheck(advisory, gateCheckPolicy(settings, null, undefined, pr.slopRisk ?? null)); + const selection = selectWarningsForResolve(gate.warnings, findingRef); + if (selection.reason === "finding_not_found") { await recordAuditEvent(env, { eventType: "github_app.finding_resolved_skipped", actor: req.actor, targetKey, outcome: "completed", detail: selection.reason, metadata: { deliveryId, repoFullName: req.repoFullName, reason: selection.reason } }); await recordGithubProductUsage(env, "finding_resolved_skipped", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "skipped", metadata: { reason: selection.reason } }); return true; } + const mode = resolveAgentActionMode({ globalPaused: isGlobalAgentPause(env) || (await isGlobalAgentFrozen(env)), agentPaused: settings.agentPaused, agentDryRun: settings.agentDryRun }); + if (mode !== "live") { const skipReason = mode === "dry_run" ? "dry_run" : "agent_paused"; await recordAuditEvent(env, { eventType: "github_app.finding_resolved_skipped", actor: req.actor, targetKey, outcome: "completed", detail: skipReason, metadata: { deliveryId, repoFullName: req.repoFullName, reason: skipReason } }); await recordGithubProductUsage(env, "finding_resolved_skipped", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "skipped", metadata: { reason: skipReason } }); return true; } + const reviewManifest = await loadRepoFocusManifest(env, req.repoFullName).catch(() => null); + const reviewMemoryEnabled = shouldApplyReviewMemory(env, resolveReviewMemoryManifestToggle(reviewManifest)); + const suppressionWrites = selection.findings.map((finding) => ({ category: finding.code, pathGlob: "", patternHash: fingerprint({ category: finding.code, message: `${finding.title} ${finding.detail}` }) })); + let recordedSuppressionCount = 0; + if (reviewMemoryEnabled && suppressionWrites.length > 0) { const { recordReviewSuppression } = await import("../db/repositories"); for (const write of suppressionWrites) { await recordReviewSuppression(env, { repoFullName: req.repoFullName, category: write.category, pathGlob: write.pathGlob, patternHash: write.patternHash, createdBy: req.actor }); recordedSuppressionCount += 1; } await recordAuditEvent(env, { eventType: "github_app.review_memory_recorded", actor: req.actor, targetKey, outcome: "completed", detail: `Recorded ${recordedSuppressionCount} review-memory suppression signal(s).`, metadata: { deliveryId, repoFullName: req.repoFullName, recordedSuppressionCount, scope: findingRef.scope, ...(findingRef.scope === "single" ? { findingCode: findingRef.findingCode } : {}) } }); await recordGithubProductUsage(env, "review_memory_recorded", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "completed", metadata: { recordedSuppressionCount, scope: findingRef.scope, ...(findingRef.scope === "single" ? { findingCode: findingRef.findingCode } : {}) } }); } + const resolvedLabel = findingRef.scope === "whole_pr" ? "all current advisory findings" : `\`${findingRef.findingCode}\``; + const confirmation = sanitizePublicComment([AGENT_COMMAND_COMMENT_MARKER, "", "> [!NOTE]", `> **Review finding resolved by @${req.actor}**`, `> Marked ${resolvedLabel} as resolved for this PR. The Gate check-run is unchanged.`, ...(recordedSuppressionCount > 0 ? ["", `Recorded ${recordedSuppressionCount} review-memory suppression signal(s) for future reviews.`] : []), "", "---", gittensoryFooter()].join("\n")); + await createOrUpdateAgentCommandComment(env, req.installationId, req.repoFullName, req.pr.number, confirmation, mode); + await recordAuditEvent(env, { eventType: "github_app.finding_resolved", actor: req.actor, targetKey, outcome: "completed", detail: `Marked ${resolvedLabel} as resolved.`, metadata: { deliveryId, repoFullName: req.repoFullName, scope: findingRef.scope, resolvedWarningCount: selection.findings.length, recordedSuppressionCount, ...(findingRef.scope === "single" ? { findingCode: findingRef.findingCode } : {}) } }); + await recordGithubProductUsage(env, "finding_resolved", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "completed", metadata: { scope: findingRef.scope, resolvedWarningCount: selection.findings.length, recordedSuppressionCount, ...(findingRef.scope === "single" ? { findingCode: findingRef.findingCode } : {}) } }); return true; } /** * `@gittensory plan` (#issue-coding-plan, flag-gated by GITTENSORY_REVIEW_PLANNER). On a MAINTAINER's comment on * an ISSUE (not a PR), generate a concise implementation plan from the issue text via Workers AI and post it as an diff --git a/src/review/review-memory-wire.ts b/src/review/review-memory-wire.ts index 2db0b0cd8d..ea64b67f5a 100644 --- a/src/review/review-memory-wire.ts +++ b/src/review/review-memory-wire.ts @@ -26,6 +26,9 @@ export function shouldApplyReviewMemory( ): boolean { return isReviewMemoryEnabled(env) && manifestReviewMemoryEnabled; } +const RESOLVE_FINDING_CODE = /^[a-z][a-z0-9_]{0,199}$/; +export function normalizeResolveFindingRef(raw: string | null | undefined): { ok: true; scope: "whole_pr" } | { ok: true; scope: "single"; findingCode: string } | { ok: false; reason: "malformed_finding_id" } { const trimmed = (raw ?? "").trim(); if (trimmed.length === 0) return { ok: true, scope: "whole_pr" }; const normalized = trimmed.toLowerCase().replace(/^finding-/, ""); if (!RESOLVE_FINDING_CODE.test(normalized)) return { ok: false, reason: "malformed_finding_id" }; return { ok: true, scope: "single", findingCode: normalized }; } +export function selectWarningsForResolve(warnings: ReadonlyArray, ref: { ok: true; scope: "whole_pr" } | { ok: true; scope: "single"; findingCode: string }): { findings: AdvisoryFinding[]; reason?: "finding_not_found" } { if (ref.scope === "whole_pr") return { findings: [...warnings] }; const matches = warnings.filter((finding) => finding.code === ref.findingCode); if (matches.length === 0) return { findings: [], reason: "finding_not_found" }; return { findings: matches }; } /** Apply-to-findings wiring (#2181, apply slice of #1964). PURE — no DB I/O (the caller already resolved * `signals` via listReviewSuppressions); the caller wraps the READ side in its own try/catch (fail-safe: a diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index 3704ab9759..2d1d7839f0 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -52,6 +52,7 @@ import { putCachedAiReview, markAiReviewPublished, recordReviewSuppression, + listReviewSuppressions, } from "../../src/db/repositories"; import { agentMaintenanceHeadMatchesGate, changedPathsForGuardrail, claimAiReviewLock, claimPrActuationLock, contributorEvidenceBatchSize, enrichOpenPullRequestsWithChangedFiles, processJob, reconcileLiveDuplicateSiblings, releaseAiReviewLock, releasePrActuationLock } from "../../src/queue/processors"; import type { PullRequestRecord } from "../../src/types"; @@ -21854,6 +21855,429 @@ describe("queue processors", () => { expect(overridden ?? null).toBeNull(); }); + // #1964 (record slice): `@gittensory resolve` records review-memory suppression signals for advisory warnings. + describe("@gittensory resolve (#1964)", () => { + async function seedResolvePr(env: Env, repoFullName: string, prNumber: number, headSha: string) { + const slash = repoFullName.indexOf("/"); + const owner = slash >= 0 ? repoFullName.slice(0, slash) : repoFullName; + const name = slash >= 0 ? repoFullName.slice(slash + 1) : repoFullName; + await upsertRepositoryFromGitHub(env, { name, full_name: repoFullName, private: false, owner: { login: owner } }, 123); + await upsertRepositorySettings(env, { + repoFullName, + commentMode: "off", + publicSurface: "off", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", + requireLinkedIssue: true, + linkedIssueGateMode: "advisory", + }); + await upsertPullRequestFromGitHub(env, repoFullName, { + number: prNumber, + title: "Resolve me", + state: "open", + user: { login: "contributor" }, + author_association: "CONTRIBUTOR", + head: { sha: headSha }, + labels: [], + body: "No linked issue on purpose", + }); + } + + it("records a suppression signal and finding_resolved when an authorized maintainer resolves a named warning with review.memory ON", async () => { + const repoFullName = "JSONbored/resolve-1964-a"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + GITTENSORY_REVIEW_MEMORY: "true", + }); + await seedResolvePr(env, repoFullName, 1964, "resolve-1964-a"); + await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); + const calls = { permission: 0, checkPatches: 0, comments: 0 }; + let confirmationBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) { + calls.permission += 1; + return Response.json({ permission: "admin" }); + } + if (url.includes("/issues/1964/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/1964/comments") && method === "POST") { + calls.comments += 1; + confirmationBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 19641 }); + } + if (url.includes("/check-runs") && method === "PATCH") { + calls.checkPatches += 1; + return Response.json({ id: 1 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-1964-allow", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1964-a", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1964, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { + id: 19640, + body: "@gittensory resolve missing_linked_issue", + author_association: "NONE", + user: { login: "maintainer", type: "User" }, + }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + expect(calls.permission).toBe(1); + expect(calls.checkPatches).toBe(0); + expect(calls.comments).toBe(1); + expect(confirmationBody).toContain("Review finding resolved"); + expect(confirmationBody).toContain("missing_linked_issue"); + expect(confirmationBody).toContain("Gate check-run is unchanged"); + const resolved = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?") + .bind("github_app.finding_resolved") + .first<{ outcome: string; detail: string }>(); + expect(resolved).toMatchObject({ outcome: "completed" }); + const memoryRecorded = await env.DB.prepare("select outcome from audit_events where event_type = ?") + .bind("github_app.review_memory_recorded") + .first<{ outcome: string }>(); + expect(memoryRecorded).toMatchObject({ outcome: "completed" }); + const suppressions = await listReviewSuppressions(env, repoFullName); + expect(suppressions).toHaveLength(1); + expect(suppressions[0]).toMatchObject({ + category: "missing_linked_issue", + createdBy: "maintainer", + }); + }); + + it("records finding_resolved without a suppression write when review.memory is OFF (operator kill-switch)", async () => { + const repoFullName = "JSONbored/resolve-1965-off"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedResolvePr(env, repoFullName, 1965, "resolve-1965-off"); + await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/1965/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/1965/comments") && method === "POST") return Response.json({ id: 19651 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-1965-flag-off", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1965-off", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1965, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { + id: 19650, + body: "@gittensory resolve missing_linked_issue", + author_association: "NONE", + user: { login: "maintainer", type: "User" }, + }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + const memoryRecorded = await env.DB.prepare("select id from audit_events where event_type = ?") + .bind("github_app.review_memory_recorded") + .first<{ id: string }>(); + expect(memoryRecorded ?? null).toBeNull(); + expect(await listReviewSuppressions(env, repoFullName)).toHaveLength(0); + const resolved = await env.DB.prepare("select outcome from audit_events where event_type = ?") + .bind("github_app.finding_resolved") + .first<{ outcome: string }>(); + expect(resolved).toMatchObject({ outcome: "completed" }); + }); + + it("denies an unauthorized actor and records no suppression signal", async () => { + const repoFullName = "JSONbored/resolve-1966-deny"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); + await seedResolvePr(env, repoFullName, 1966, "resolve-1966-deny"); + await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/org-member/permission")) return Response.json({ permission: "read" }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-1966-deny", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1966-deny", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1966, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { + id: 19660, + body: "@gittensory resolve missing_linked_issue", + author_association: "MEMBER", + user: { login: "org-member", type: "User" }, + }, + sender: { login: "org-member", type: "User" }, + }, + }); + + const denied = await env.DB.prepare("select outcome from audit_events where event_type = ?") + .bind("github_app.finding_resolved_denied") + .first<{ outcome: string }>(); + expect(denied).toMatchObject({ outcome: "denied" }); + expect(await listReviewSuppressions(env, repoFullName)).toHaveLength(0); + }); + + it.each([ + ["malformed finding id", "@gittensory resolve ../escape", "malformed_finding_id"], + ["absent finding code", "@gittensory resolve readiness_score_below_threshold", "finding_not_found"], + ] as const)("skips resolve when the maintainer supplies %s", async (_label, body, reason) => { + const repoFullName = "JSONbored/resolve-1967-skip"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); + await seedResolvePr(env, repoFullName, 1967, "resolve-1967-skip"); + await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: `resolve-1967-${reason}`, + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1967-skip", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1967, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 19670, body, author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?") + .bind("github_app.finding_resolved_skipped") + .first<{ detail: string }>(); + expect(skipped?.detail).toBe(reason); + expect(await listReviewSuppressions(env, repoFullName)).toHaveLength(0); + }); + + it("records every current advisory warning for a whole-PR `@gittensory resolve` ack", async () => { + const repoFullName = "JSONbored/resolve-1968-whole"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); + await seedResolvePr(env, repoFullName, 1968, "resolve-1968-whole"); + await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + if (url.includes("/issues/1968/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/1968/comments") && method === "POST") return Response.json({ id: 19681 }); + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-1968-whole", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1968-whole", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1968, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 19680, body: "@gittensory resolve", author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + + expect(await listReviewSuppressions(env, repoFullName)).toHaveLength(2); + const resolved = await env.DB.prepare("select metadata_json from audit_events where event_type = ?") + .bind("github_app.finding_resolved") + .first<{ metadata_json: string }>(); + expect(JSON.parse(resolved?.metadata_json ?? "{}")).toMatchObject({ scope: "whole_pr", resolvedWarningCount: 2 }); + }); + + it("ignores issue comments that are not @gittensory resolve commands (#1964)", async () => { + const repoFullName = "JSONbored/resolve-1973-plain"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedResolvePr(env, repoFullName, 1973, "resolve-1973-plain"); + let commentPosts = 0; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/issues/1973/comments") && method === "POST") { + commentPosts += 1; + return Response.json({ id: 19730 }); + } + return new Response("not found", { status: 404 }); + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-plain-comment", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1973-plain", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1973, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 19731, body: "Looks good to me", author_association: "OWNER", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + expect(commentPosts).toBe(0); + const events = await env.DB.prepare("select event_type from audit_events where event_type like ?").bind("github_app.finding_resolved%").all<{ event_type: string }>(); + expect(events.results ?? []).toEqual([]); + }); + + it("ignores other @gittensory verbs on the resolve handler path (#1964)", async () => { + const repoFullName = "JSONbored/resolve-1974-help"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedResolvePr(env, repoFullName, 1974, "resolve-1974-help"); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString().includes("/access_tokens")) return Response.json({ token: "installation-token" }); + return new Response("not found", { status: 404 }); + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-help-verb", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1974-help", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1974, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 19740, body: "@gittensory help", author_association: "OWNER", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + const events = await env.DB.prepare("select event_type from audit_events where event_type like ?").bind("github_app.finding_resolved%").all<{ event_type: string }>(); + expect(events.results ?? []).toEqual([]); + }); + + it("skips resolve when the webhook payload lacks a repository (#1964)", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString().includes("/access_tokens")) return Response.json({ token: "installation-token" }); + return new Response("not found", { status: 404 }); + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-missing-repo", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + issue: { number: 1972, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 19720, body: "@gittensory resolve missing_linked_issue", author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_resolved_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("missing_repo_pr_installation_or_actor"); + }); + + it("skips resolve when the cached pull request row is missing (#1964)", async () => { + const repoFullName = "JSONbored/resolve-1969-missing-pr"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); + const slash = repoFullName.indexOf("/"); + await upsertRepositoryFromGitHub(env, { name: repoFullName.slice(slash + 1), full_name: repoFullName, private: false, owner: { login: repoFullName.slice(0, slash) } }, 123); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + if (input.toString().includes("/access_tokens")) return Response.json({ token: "installation-token" }); + return new Response("not found", { status: 404 }); + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-1969-missing-pr", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1969-missing-pr", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1969, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 19690, body: "@gittensory resolve missing_linked_issue", author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_resolved_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("cached_pr_missing"); + }); + + it("skips resolve in agentDryRun without recording finding_resolved (#1964)", async () => { + const repoFullName = "JSONbored/resolve-1970-dry-run"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); + await seedResolvePr(env, repoFullName, 1970, "resolve-1970-dry-run"); + await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", requireLinkedIssue: true, linkedIssueGateMode: "advisory", agentDryRun: true }); + await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + return new Response("not found", { status: 404 }); + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-1970-dry-run", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1970-dry-run", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1970, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 19700, body: "@gittensory resolve missing_linked_issue", author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_resolved_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("dry_run"); + const resolved = await env.DB.prepare("select id from audit_events where event_type = ?").bind("github_app.finding_resolved").first<{ id: string }>(); + expect(resolved ?? null).toBeNull(); + }); + + it("skips resolve when the repository is agentPaused (#1964)", async () => { + const repoFullName = "JSONbored/resolve-1971-paused"; + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_MEMORY: "true" }); + await seedResolvePr(env, repoFullName, 1971, "resolve-1971-paused"); + await upsertRepositorySettings(env, { repoFullName, commentMode: "off", publicSurface: "off", autoLabelEnabled: false, checkRunMode: "off", gateCheckMode: "enabled", requireLinkedIssue: true, linkedIssueGateMode: "advisory", agentPaused: true }); + await upsertRepoFocusManifest(env, repoFullName, { review: { memory: true } }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); + return new Response("not found", { status: 404 }); + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "resolve-1971-paused", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "resolve-1971-paused", full_name: repoFullName, private: false, owner: { login: "JSONbored" } }, + issue: { number: 1971, title: "Resolve me", state: "open", user: { login: "contributor" }, pull_request: {} }, + comment: { id: 19710, body: "@gittensory resolve missing_linked_issue", author_association: "NONE", user: { login: "maintainer", type: "User" } }, + sender: { login: "maintainer", type: "User" }, + }, + }); + const skipped = await env.DB.prepare("select detail from audit_events where event_type = ?").bind("github_app.finding_resolved_skipped").first<{ detail: string }>(); + expect(skipped?.detail).toBe("agent_paused"); + }); + }); + it("a #1960 action-command verb with no dispatch handler wired yet (e.g. pause) is bailed out of the Q&A answer-card path, not misrendered as help (#2160)", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); diff --git a/test/unit/review-memory-record.test.ts b/test/unit/review-memory-record.test.ts new file mode 100644 index 0000000000..db5cb85755 --- /dev/null +++ b/test/unit/review-memory-record.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from "vitest"; +import { normalizeResolveFindingRef, selectWarningsForResolve } from "../../src/review/review-memory-wire"; +import type { AdvisoryFinding } from "../../src/types"; + +function warning(overrides: Partial = {}): AdvisoryFinding { + return { + code: "readiness_score_below_threshold", + severity: "warning", + title: "Readiness score is below the configured threshold", + detail: "The public readiness score is 25/100, below the repository threshold of 100/100.", + ...overrides, + }; +} + +describe("normalizeResolveFindingRef (#1964)", () => { + it("treats empty/absent trailing text as a whole-PR ack", () => { + expect(normalizeResolveFindingRef(undefined)).toEqual({ ok: true, scope: "whole_pr" }); + expect(normalizeResolveFindingRef("")).toEqual({ ok: true, scope: "whole_pr" }); + expect(normalizeResolveFindingRef(" ")).toEqual({ ok: true, scope: "whole_pr" }); + }); + + it("accepts a bare finding code and the optional finding- prefix", () => { + expect(normalizeResolveFindingRef("readiness_score_below_threshold")).toEqual({ + ok: true, + scope: "single", + findingCode: "readiness_score_below_threshold", + }); + expect(normalizeResolveFindingRef("finding-readiness_score_below_threshold")).toEqual({ + ok: true, + scope: "single", + findingCode: "readiness_score_below_threshold", + }); + }); + + it("rejects malformed finding references", () => { + expect(normalizeResolveFindingRef("../escape")).toEqual({ ok: false, reason: "malformed_finding_id" }); + expect(normalizeResolveFindingRef("Bad-Hyphen")).toEqual({ ok: false, reason: "malformed_finding_id" }); + expect(normalizeResolveFindingRef("has space")).toEqual({ ok: false, reason: "malformed_finding_id" }); + expect(normalizeResolveFindingRef("9starts_with_digit")).toEqual({ ok: false, reason: "malformed_finding_id" }); + }); +}); + +describe("selectWarningsForResolve (#1964)", () => { + it("returns every warning for a whole-PR ack", () => { + const warnings = [warning(), warning({ code: "duplicate_pr_risk", title: "Duplicate risk" })]; + expect(selectWarningsForResolve(warnings, { ok: true, scope: "whole_pr" })).toEqual({ findings: warnings }); + }); + + it("returns only the matching warning for a single-finding ref", () => { + const a = warning(); + const b = warning({ code: "duplicate_pr_risk", title: "Duplicate risk" }); + expect(selectWarningsForResolve([a, b], { ok: true, scope: "single", findingCode: "duplicate_pr_risk" })).toEqual({ + findings: [b], + }); + }); + + it("reports finding_not_found when the requested code is absent from the warnings", () => { + expect( + selectWarningsForResolve([warning()], { ok: true, scope: "single", findingCode: "missing_linked_issue" }), + ).toEqual({ findings: [], reason: "finding_not_found" }); + }); +}); diff --git a/test/unit/sweep-watchdog.test.ts b/test/unit/sweep-watchdog.test.ts index 1df3827963..c41440ccf6 100644 --- a/test/unit/sweep-watchdog.test.ts +++ b/test/unit/sweep-watchdog.test.ts @@ -62,7 +62,7 @@ describe("runSweepLivenessWatchdog (#audit-sweep-fanout-isolation follow-up)", ( }); it("REGRESSION: reports a finite ageMs for a repo that WAS regated once but fell outside the staleness window (not just a never-regated null marker)", async () => { - vi.useFakeTimers(); + vi.useFakeTimers({ shouldAdvanceTime: true }); const start = new Date("2026-07-06T10:00:00.000Z"); vi.setSystemTime(start); const sent: import("../../src/types").JobMessage[] = []; @@ -78,7 +78,7 @@ describe("runSweepLivenessWatchdog (#audit-sweep-fanout-isolation follow-up)", ( expect(found).toEqual([expect.objectContaining({ repoFullName: "owner/aged-repo", lastRegatedAt: start.toISOString(), ageMs: SWEEP_STALENESS_THRESHOLD_MS + 60_000 })]); expect(Number.isFinite(found[0]?.ageMs)).toBe(true); expect(sent).toEqual([expect.objectContaining({ type: "agent-regate-sweep", repoFullName: "owner/aged-repo" })]); - }); + }, 60_000); it("watches an ALLOWLISTED (GITTENSORY_REVIEW_REPOS) installed repo even with no autonomy configured, and skips a plain repo that is neither allowlisted nor agent-configured", async () => { const sent: import("../../src/types").JobMessage[] = []; diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index 7a6d5207f3..b916499fba 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,6 +1,6 @@ /* eslint-disable */ // Generated by Wrangler by running `wrangler types` (hash: 9e992b669fb5fd96f79df4b2f0f44770) -// Runtime types generated with workerd@1.20260701.1 2026-05-28 nodejs_compat +// Runtime types generated with workerd@1.20260617.1 2026-05-28 nodejs_compat interface __BaseEnv_Env { DB: D1Database; JOBS: Queue; @@ -481,8 +481,7 @@ interface ExecutionContext { readonly exports: Cloudflare.Exports; readonly props: Props; cache?: CacheContext; - readonly access?: CloudflareAccessContext; - tracing: Tracing; + tracing?: Tracing; } type ExportedHandlerFetchHandler = (request: Request>, env: Env, ctx: ExecutionContext) => Response | Promise; type ExportedHandlerConnectHandler = (socket: Socket, env: Env, ctx: ExecutionContext) => void | Promise; @@ -538,10 +537,6 @@ interface CachePurgeOptions { interface CacheContext { purge(options: CachePurgeOptions): Promise; } -interface CloudflareAccessContext { - readonly aud: string; - getIdentity(): Promise; -} declare abstract class ColoLocalActorNamespace { get(actorId: string): Fetcher; } @@ -575,7 +570,7 @@ type DurableObjectJurisdiction = "eu" | "fedramp" | "fedramp-high"; interface DurableObjectNamespaceNewUniqueIdOptions { jurisdiction?: DurableObjectJurisdiction; } -type DurableObjectLocationHint = "wnam" | "enam" | "sam" | "weur" | "eeur" | "apac" | "apac-ne" | "apac-se" | "oc" | "afr" | "me"; +type DurableObjectLocationHint = "wnam" | "enam" | "sam" | "weur" | "eeur" | "apac" | "oc" | "afr" | "me"; type DurableObjectRoutingMode = "primary-only"; interface DurableObjectNamespaceGetDurableObjectOptions { locationHint?: DurableObjectLocationHint; @@ -671,7 +666,6 @@ interface DurableObjectFacets { get(name: string, getStartupOptions: () => FacetStartupOptions | Promise>): Fetcher; abort(name: string, reason: any): void; delete(name: string): void; - clone(src: string, dst: string): void; } interface FacetStartupOptions { id?: DurableObjectId | string; @@ -3347,28 +3341,6 @@ interface EventSourceEventSourceInit { withCredentials?: boolean; fetcher?: Fetcher; } -interface ExecOutput { - readonly stdout: ArrayBuffer; - readonly stderr: ArrayBuffer; - readonly exitCode: number; -} -interface ContainerExecOptions { - cwd?: string; - env?: Record; - user?: string; - stdin?: ReadableStream | "pipe"; - stdout?: "pipe" | "ignore"; - stderr?: "pipe" | "ignore" | "combined"; -} -interface ExecProcess { - readonly stdin: WritableStream | null; - readonly stdout: ReadableStream | null; - readonly stderr: ReadableStream | null; - readonly pid: number; - readonly exitCode: Promise; - output(): Promise; - kill(signal?: number): void; -} interface Container { get running(): boolean; start(options?: ContainerStartupOptions): void; @@ -3382,7 +3354,6 @@ interface Container { snapshotDirectory(options: ContainerDirectorySnapshotOptions): Promise; snapshotContainer(options: ContainerSnapshotOptions): Promise; interceptOutboundHttps(addr: string, binding: Fetcher): Promise; - exec(cmd: string[], options?: ContainerExecOptions): Promise; } interface ContainerDirectorySnapshot { id: string; @@ -3553,58 +3524,11 @@ declare abstract class Performance { } interface Tracing { enterSpan(name: string, callback: (span: Span, ...args: A) => T, ...args: A): T; - startActiveSpan(name: string, callback: (span: Span, ...args: A) => T, ...args: A): T; Span: typeof Span; } declare abstract class Span { get isTraced(): boolean; setAttribute(key: string, value?: (boolean | number | string)): void; - end(): void; -} -/** - * Represents the identity of a user authenticated via Cloudflare Access. - * This matches the result of calling /cdn-cgi/access/get-identity. - * - * The exact structure of the returned object depends on the identity provider - * configuration for the Access application. The fields below represent commonly - * available properties, but additional provider-specific fields may be present. - */ -interface CloudflareAccessIdentity extends Record { - /** The user's email address, if available from the identity provider. */ - email?: string; - /** The user's display name. */ - name?: string; - /** The user's unique identifier. */ - user_uuid?: string; - /** The Cloudflare account ID. */ - account_id?: string; - /** Login timestamp (Unix epoch seconds). */ - iat?: number; - /** The user's IP address at authentication time. */ - ip?: string; - /** Authentication methods used (e.g., "pwd"). */ - amr?: string[]; - /** Identity provider information. */ - idp?: { - id: string; - type: string; - }; - /** Geographic information about where the user authenticated. */ - geo?: { - country: string; - }; - /** Group memberships from the identity provider. */ - groups?: Array<{ - id: string; - name: string; - email?: string; - }>; - /** Device posture check results, keyed by check ID. */ - devicePosture?: Record; - /** True if the user connected via Cloudflare WARP. */ - is_warp?: boolean; - /** True if the user is authenticated via Cloudflare Gateway. */ - is_gateway?: boolean; } // ============================================================================ // Agent Memory @@ -11235,8 +11159,6 @@ interface RequestInitCfProperties extends Record { * (e.g. { '200-299': 86400, '404': 1, '500-599': 0 }) */ cacheTtlByStatus?: Record; - /** Controls how responses with a `Vary` header are cached for this request. */ - vary?: RequestInitCfPropertiesVary; /** * Explicit Cache-Control header value to set on the response stored in cache. * This gives full control over cache directives (e.g. 'public, max-age=3600, s-maxage=86400'). @@ -11274,17 +11196,6 @@ interface RequestInitCfProperties extends Record { cacheReserveMinimumFileSize?: number; scrapeShield?: boolean; apps?: boolean; - /** - * Controls whether an outbound gRPC-web subrequest from this Worker is - * converted to gRPC at the Cloudflare edge. - * - * - `"passthrough"`: forward the subrequest unchanged as gRPC-web (default). - * - `"convert"`: convert the gRPC-web subrequest to gRPC at the edge. - * - * Provides per-request control over the same edge conversion behavior - * gated by the `auto_grpc_convert` compatibility flag. - */ - grpcWeb?: "passthrough" | "convert"; image?: RequestInitCfPropertiesImage; minify?: RequestInitCfPropertiesImageMinify; mirage?: boolean; @@ -11305,63 +11216,6 @@ interface RequestInitCfProperties extends Record { */ resolveOverride?: string; } -/** - * Controls how Workers Standard Vary handles a request header listed by an - * origin `Vary` response header: - * - * - `"normalize"`: normalize the request header value before it is used in the - * cache variance key. - * - `"passthrough"`: use the raw request header value in the cache variance - * key. - * - `"bypass"`: bypass cache when the header appears in the origin `Vary` - * response header. - */ -type RequestInitCfPropertiesVaryAction = "normalize" | "passthrough" | "bypass"; -/** Configuration for Workers Standard Vary support. */ -interface RequestInitCfPropertiesVary { - /** The fallback action for varied request headers not listed in `headers`. */ - default: RequestInitCfPropertiesVaryHeader; - /** - * Lowercase request header names and their Vary configuration. - * - * The `accept` header can include `media_types`, the `accept-language` - * header can include `languages`, and other headers support only `action`. - */ - headers?: RequestInitCfPropertiesVaryHeaders; -} -/** Common Vary behavior for a single request header. */ -interface RequestInitCfPropertiesVaryHeader { - /** How this request header contributes to cache variance. */ - action: RequestInitCfPropertiesVaryAction; -} -/** Vary behavior for the `accept` request header. */ -interface RequestInitCfPropertiesVaryAcceptHeader extends RequestInitCfPropertiesVaryHeader { - /** - * Media types to keep when normalizing the `Accept` request header. - * - * Named `media_types` to match the serialized `cf.vary` configuration. - */ - media_types?: string[]; -} -/** Vary behavior for the `accept-language` request header. */ -interface RequestInitCfPropertiesVaryAcceptLanguageHeader extends RequestInitCfPropertiesVaryHeader { - /** - * Language tags to keep when normalizing the `Accept-Language` request - * header. - */ - languages?: string[]; -} -/** - * Lowercase request header names and their Vary behavior. - * - * The index signature allows arbitrary custom request headers beyond the - * well-known `accept` and `accept-language` specializations. - */ -interface RequestInitCfPropertiesVaryHeaders { - accept?: RequestInitCfPropertiesVaryAcceptHeader; - "accept-language"?: RequestInitCfPropertiesVaryAcceptLanguageHeader; - [header: string]: RequestInitCfPropertiesVaryHeader | RequestInitCfPropertiesVaryAcceptHeader | RequestInitCfPropertiesVaryAcceptLanguageHeader | undefined; -} interface BasicImageTransformations { /** * Maximum width in image pixels. The value must be an integer. @@ -14070,7 +13924,7 @@ declare namespace TailStream { interface ConnectEventInfo { readonly type: "connect"; } - type EventOutcome = "ok" | "canceled" | "exception" | "unknown" | "killSwitch" | "daemonDown" | "exceededCpu" | "exceededMemory" | "loadShed" | "responseStreamDisconnected" | "scriptNotFound" | "internalError" | "exceededWallTime"; + type EventOutcome = "ok" | "canceled" | "exception" | "unknown" | "killSwitch" | "daemonDown" | "exceededCpu" | "exceededMemory" | "loadShed" | "responseStreamDisconnected" | "scriptNotFound" | "internalError"; interface ScriptVersion { readonly id: string; readonly tag?: string; From 68d430e0d6d266024d048cbebeff10fb3b52de25 Mon Sep 17 00:00:00 2001 From: andriypolandki <=> Date: Tue, 7 Jul 2026 00:49:03 -0700 Subject: [PATCH 2/3] fix --- worker-configuration.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index 3b179d7053..8307cb5630 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 9e992b669fb5fd96f79df4b2f0f44770) +// Generated by Wrangler by running `wrangler types` (hash: 273aa035a633a16371217457cfc17ff6) // Runtime types generated with workerd@1.20260617.1 2026-05-28 nodejs_compat interface __BaseEnv_Env { DB: D1Database; From 10412cecba3f0f0dd7d8eb34c80c9f9e583b3abc Mon Sep 17 00:00:00 2001 From: andriypolandki <=> Date: Tue, 7 Jul 2026 01:09:41 -0700 Subject: [PATCH 3/3] fix(review): address gate feedback on resolve (#1964) --- .../site/app-panels/miner-panel.tsx | 5 +- .../src/lib/maintainer-settings-preview.ts | 6 +- .../src/lib/registration-workspace.ts | 6 +- apps/gittensory-ui/src/lib/snapshot-replay.ts | 8 +- src/queue/processors.ts | 4 +- worker-configuration.d.ts | 154 +++++++++++++++++- 6 files changed, 155 insertions(+), 28 deletions(-) diff --git a/apps/gittensory-ui/src/components/site/app-panels/miner-panel.tsx b/apps/gittensory-ui/src/components/site/app-panels/miner-panel.tsx index 119ef35b00..5c549ebfd5 100644 --- a/apps/gittensory-ui/src/components/site/app-panels/miner-panel.tsx +++ b/apps/gittensory-ui/src/components/site/app-panels/miner-panel.tsx @@ -38,10 +38,7 @@ const CHANGE_TONE: Record = { }; type RecommendationSignalGroup = - | "repo_state" - | "contributor_state" - | "validation_state" - | "policy_context"; + "repo_state" | "contributor_state" | "validation_state" | "policy_context"; type RecommendationChange = { status: "new" | "changed" | "unchanged"; diff --git a/apps/gittensory-ui/src/lib/maintainer-settings-preview.ts b/apps/gittensory-ui/src/lib/maintainer-settings-preview.ts index 1b13b72bef..3c160bca7c 100644 --- a/apps/gittensory-ui/src/lib/maintainer-settings-preview.ts +++ b/apps/gittensory-ui/src/lib/maintainer-settings-preview.ts @@ -11,11 +11,7 @@ export type AuthorAssociation = | "NONE"; export type PreviewScenarioId = - | "confirmed-miner" - | "non-miner" - | "bot-author" - | "maintainer-author" - | "miner-api-unavailable"; + "confirmed-miner" | "non-miner" | "bot-author" | "maintainer-author" | "miner-api-unavailable"; export type PreviewScenario = { id: PreviewScenarioId; diff --git a/apps/gittensory-ui/src/lib/registration-workspace.ts b/apps/gittensory-ui/src/lib/registration-workspace.ts index 044a303ba1..6d86178fc2 100644 --- a/apps/gittensory-ui/src/lib/registration-workspace.ts +++ b/apps/gittensory-ui/src/lib/registration-workspace.ts @@ -63,11 +63,7 @@ export type OwnerWorkflowState = "accepted" | "needs_cleanup" | "not_ready"; export type OwnerWorkflowRemediationKind = "action" | "manual"; export type OwnerWorkflowBucketId = - | "policy" - | "data_quality" - | "queue_health" - | "docs_onboarding" - | "maintainer_capacity"; + "policy" | "data_quality" | "queue_health" | "docs_onboarding" | "maintainer_capacity"; export type OwnerWorkflowItem = { id: string; diff --git a/apps/gittensory-ui/src/lib/snapshot-replay.ts b/apps/gittensory-ui/src/lib/snapshot-replay.ts index 1a2108b3d6..3ad1c16942 100644 --- a/apps/gittensory-ui/src/lib/snapshot-replay.ts +++ b/apps/gittensory-ui/src/lib/snapshot-replay.ts @@ -14,13 +14,7 @@ export type SnapshotReplayViewer = "public" | "authenticated"; export type SnapshotReplayStatus = "populated" | "stale" | "missing"; export type SnapshotReplayConfidence = "high" | "medium" | "low" | "unknown"; export type SnapshotReplayFreshness = - | "fresh" - | "stale" - | "rebuilding" - | "missing" - | "degraded" - | "possibly_stale" - | "unknown"; + "fresh" | "stale" | "rebuilding" | "missing" | "degraded" | "possibly_stale" | "unknown"; export type SnapshotReplaySource = { name: string; diff --git a/src/queue/processors.ts b/src/queue/processors.ts index c3ba5192d4..8c3bfba0c8 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -10348,7 +10348,6 @@ async function maybeProcessResolveCommand(env: Env, deliveryId: string, payload: if (command.name !== "resolve") return false; const { classifyPrCommandRequest } = await import("../github/pr-command-request"); const { normalizeResolveFindingRef, selectWarningsForResolve } = await import("../review/review-memory-wire"); - const { fingerprint } = await import("../review/review-memory-match"); const req = classifyPrCommandRequest(payload, getInstallationId(payload)); if (!req.ok) { await recordAuditEvent(env, { eventType: "github_app.finding_resolved_skipped", actor: req.actor, targetKey: req.targetKey, outcome: "completed", detail: req.reason, metadata: { deliveryId, repoFullName: req.repoFullName ?? null, reason: req.reason } }); await recordGithubProductUsage(env, "finding_resolved_skipped", { actor: req.actor, repoFullName: req.repoFullName, targetKey: req.targetKey, outcome: "skipped", metadata: { reason: req.reason } }); return true; } const [pr, settings] = await Promise.all([getPullRequest(env, req.repoFullName, req.pr.number), resolveRepositorySettings(env, req.repoFullName)]); @@ -10366,9 +10365,8 @@ async function maybeProcessResolveCommand(env: Env, deliveryId: string, payload: if (mode !== "live") { const skipReason = mode === "dry_run" ? "dry_run" : "agent_paused"; await recordAuditEvent(env, { eventType: "github_app.finding_resolved_skipped", actor: req.actor, targetKey, outcome: "completed", detail: skipReason, metadata: { deliveryId, repoFullName: req.repoFullName, reason: skipReason } }); await recordGithubProductUsage(env, "finding_resolved_skipped", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "skipped", metadata: { reason: skipReason } }); return true; } const reviewManifest = await loadRepoFocusManifest(env, req.repoFullName).catch(() => null); const reviewMemoryEnabled = shouldApplyReviewMemory(env, resolveReviewMemoryManifestToggle(reviewManifest)); - const suppressionWrites = selection.findings.map((finding) => ({ category: finding.code, pathGlob: "", patternHash: fingerprint({ category: finding.code, message: `${finding.title} ${finding.detail}` }) })); let recordedSuppressionCount = 0; - if (reviewMemoryEnabled && suppressionWrites.length > 0) { const { recordReviewSuppression } = await import("../db/repositories"); for (const write of suppressionWrites) { await recordReviewSuppression(env, { repoFullName: req.repoFullName, category: write.category, pathGlob: write.pathGlob, patternHash: write.patternHash, createdBy: req.actor }); recordedSuppressionCount += 1; } await recordAuditEvent(env, { eventType: "github_app.review_memory_recorded", actor: req.actor, targetKey, outcome: "completed", detail: `Recorded ${recordedSuppressionCount} review-memory suppression signal(s).`, metadata: { deliveryId, repoFullName: req.repoFullName, recordedSuppressionCount, scope: findingRef.scope, ...(findingRef.scope === "single" ? { findingCode: findingRef.findingCode } : {}) } }); await recordGithubProductUsage(env, "review_memory_recorded", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "completed", metadata: { recordedSuppressionCount, scope: findingRef.scope, ...(findingRef.scope === "single" ? { findingCode: findingRef.findingCode } : {}) } }); } + if (reviewMemoryEnabled && selection.findings.length > 0) { const { fingerprint } = await import("../review/review-memory-match"); const { recordReviewSuppression } = await import("../db/repositories"); const suppressionWrites = selection.findings.map((finding) => ({ category: finding.code, pathGlob: "", patternHash: fingerprint({ category: finding.code, message: `${finding.title} ${finding.detail}` }) })); await Promise.all(suppressionWrites.map((write) => recordReviewSuppression(env, { repoFullName: req.repoFullName, category: write.category, pathGlob: write.pathGlob, patternHash: write.patternHash, createdBy: req.actor }))); recordedSuppressionCount = suppressionWrites.length; await recordAuditEvent(env, { eventType: "github_app.review_memory_recorded", actor: req.actor, targetKey, outcome: "completed", detail: `Recorded ${recordedSuppressionCount} review-memory suppression signal(s).`, metadata: { deliveryId, repoFullName: req.repoFullName, recordedSuppressionCount, scope: findingRef.scope, ...(findingRef.scope === "single" ? { findingCode: findingRef.findingCode } : {}) } }); await recordGithubProductUsage(env, "review_memory_recorded", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "completed", metadata: { recordedSuppressionCount, scope: findingRef.scope, ...(findingRef.scope === "single" ? { findingCode: findingRef.findingCode } : {}) } }); } const resolvedLabel = findingRef.scope === "whole_pr" ? "all current advisory findings" : `\`${findingRef.findingCode}\``; const confirmation = sanitizePublicComment([AGENT_COMMAND_COMMENT_MARKER, "", "> [!NOTE]", `> **Review finding resolved by @${req.actor}**`, `> Marked ${resolvedLabel} as resolved for this PR. The Gate check-run is unchanged.`, ...(recordedSuppressionCount > 0 ? ["", `Recorded ${recordedSuppressionCount} review-memory suppression signal(s) for future reviews.`] : []), "", "---", gittensoryFooter()].join("\n")); await createOrUpdateAgentCommandComment(env, req.installationId, req.repoFullName, req.pr.number, confirmation, mode); diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index 8307cb5630..24ad833367 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,6 +1,6 @@ /* eslint-disable */ // Generated by Wrangler by running `wrangler types` (hash: 273aa035a633a16371217457cfc17ff6) -// Runtime types generated with workerd@1.20260617.1 2026-05-28 nodejs_compat +// Runtime types generated with workerd@1.20260701.1 2026-05-28 nodejs_compat interface __BaseEnv_Env { DB: D1Database; JOBS: Queue; @@ -521,7 +521,8 @@ interface ExecutionContext { readonly exports: Cloudflare.Exports; readonly props: Props; cache?: CacheContext; - tracing?: Tracing; + readonly access?: CloudflareAccessContext; + tracing: Tracing; } type ExportedHandlerFetchHandler = (request: Request>, env: Env, ctx: ExecutionContext) => Response | Promise; type ExportedHandlerConnectHandler = (socket: Socket, env: Env, ctx: ExecutionContext) => void | Promise; @@ -577,6 +578,10 @@ interface CachePurgeOptions { interface CacheContext { purge(options: CachePurgeOptions): Promise; } +interface CloudflareAccessContext { + readonly aud: string; + getIdentity(): Promise; +} declare abstract class ColoLocalActorNamespace { get(actorId: string): Fetcher; } @@ -610,7 +615,7 @@ type DurableObjectJurisdiction = "eu" | "fedramp" | "fedramp-high"; interface DurableObjectNamespaceNewUniqueIdOptions { jurisdiction?: DurableObjectJurisdiction; } -type DurableObjectLocationHint = "wnam" | "enam" | "sam" | "weur" | "eeur" | "apac" | "oc" | "afr" | "me"; +type DurableObjectLocationHint = "wnam" | "enam" | "sam" | "weur" | "eeur" | "apac" | "apac-ne" | "apac-se" | "oc" | "afr" | "me"; type DurableObjectRoutingMode = "primary-only"; interface DurableObjectNamespaceGetDurableObjectOptions { locationHint?: DurableObjectLocationHint; @@ -706,6 +711,7 @@ interface DurableObjectFacets { get(name: string, getStartupOptions: () => FacetStartupOptions | Promise>): Fetcher; abort(name: string, reason: any): void; delete(name: string): void; + clone(src: string, dst: string): void; } interface FacetStartupOptions { id?: DurableObjectId | string; @@ -3381,6 +3387,28 @@ interface EventSourceEventSourceInit { withCredentials?: boolean; fetcher?: Fetcher; } +interface ExecOutput { + readonly stdout: ArrayBuffer; + readonly stderr: ArrayBuffer; + readonly exitCode: number; +} +interface ContainerExecOptions { + cwd?: string; + env?: Record; + user?: string; + stdin?: ReadableStream | "pipe"; + stdout?: "pipe" | "ignore"; + stderr?: "pipe" | "ignore" | "combined"; +} +interface ExecProcess { + readonly stdin: WritableStream | null; + readonly stdout: ReadableStream | null; + readonly stderr: ReadableStream | null; + readonly pid: number; + readonly exitCode: Promise; + output(): Promise; + kill(signal?: number): void; +} interface Container { get running(): boolean; start(options?: ContainerStartupOptions): void; @@ -3394,6 +3422,7 @@ interface Container { snapshotDirectory(options: ContainerDirectorySnapshotOptions): Promise; snapshotContainer(options: ContainerSnapshotOptions): Promise; interceptOutboundHttps(addr: string, binding: Fetcher): Promise; + exec(cmd: string[], options?: ContainerExecOptions): Promise; } interface ContainerDirectorySnapshot { id: string; @@ -3564,11 +3593,58 @@ declare abstract class Performance { } interface Tracing { enterSpan(name: string, callback: (span: Span, ...args: A) => T, ...args: A): T; + startActiveSpan(name: string, callback: (span: Span, ...args: A) => T, ...args: A): T; Span: typeof Span; } declare abstract class Span { get isTraced(): boolean; setAttribute(key: string, value?: (boolean | number | string)): void; + end(): void; +} +/** + * Represents the identity of a user authenticated via Cloudflare Access. + * This matches the result of calling /cdn-cgi/access/get-identity. + * + * The exact structure of the returned object depends on the identity provider + * configuration for the Access application. The fields below represent commonly + * available properties, but additional provider-specific fields may be present. + */ +interface CloudflareAccessIdentity extends Record { + /** The user's email address, if available from the identity provider. */ + email?: string; + /** The user's display name. */ + name?: string; + /** The user's unique identifier. */ + user_uuid?: string; + /** The Cloudflare account ID. */ + account_id?: string; + /** Login timestamp (Unix epoch seconds). */ + iat?: number; + /** The user's IP address at authentication time. */ + ip?: string; + /** Authentication methods used (e.g., "pwd"). */ + amr?: string[]; + /** Identity provider information. */ + idp?: { + id: string; + type: string; + }; + /** Geographic information about where the user authenticated. */ + geo?: { + country: string; + }; + /** Group memberships from the identity provider. */ + groups?: Array<{ + id: string; + name: string; + email?: string; + }>; + /** Device posture check results, keyed by check ID. */ + devicePosture?: Record; + /** True if the user connected via Cloudflare WARP. */ + is_warp?: boolean; + /** True if the user is authenticated via Cloudflare Gateway. */ + is_gateway?: boolean; } // ============================================================================ // Agent Memory @@ -11199,6 +11275,8 @@ interface RequestInitCfProperties extends Record { * (e.g. { '200-299': 86400, '404': 1, '500-599': 0 }) */ cacheTtlByStatus?: Record; + /** Controls how responses with a `Vary` header are cached for this request. */ + vary?: RequestInitCfPropertiesVary; /** * Explicit Cache-Control header value to set on the response stored in cache. * This gives full control over cache directives (e.g. 'public, max-age=3600, s-maxage=86400'). @@ -11236,6 +11314,17 @@ interface RequestInitCfProperties extends Record { cacheReserveMinimumFileSize?: number; scrapeShield?: boolean; apps?: boolean; + /** + * Controls whether an outbound gRPC-web subrequest from this Worker is + * converted to gRPC at the Cloudflare edge. + * + * - `"passthrough"`: forward the subrequest unchanged as gRPC-web (default). + * - `"convert"`: convert the gRPC-web subrequest to gRPC at the edge. + * + * Provides per-request control over the same edge conversion behavior + * gated by the `auto_grpc_convert` compatibility flag. + */ + grpcWeb?: "passthrough" | "convert"; image?: RequestInitCfPropertiesImage; minify?: RequestInitCfPropertiesImageMinify; mirage?: boolean; @@ -11256,6 +11345,63 @@ interface RequestInitCfProperties extends Record { */ resolveOverride?: string; } +/** + * Controls how Workers Standard Vary handles a request header listed by an + * origin `Vary` response header: + * + * - `"normalize"`: normalize the request header value before it is used in the + * cache variance key. + * - `"passthrough"`: use the raw request header value in the cache variance + * key. + * - `"bypass"`: bypass cache when the header appears in the origin `Vary` + * response header. + */ +type RequestInitCfPropertiesVaryAction = "normalize" | "passthrough" | "bypass"; +/** Configuration for Workers Standard Vary support. */ +interface RequestInitCfPropertiesVary { + /** The fallback action for varied request headers not listed in `headers`. */ + default: RequestInitCfPropertiesVaryHeader; + /** + * Lowercase request header names and their Vary configuration. + * + * The `accept` header can include `media_types`, the `accept-language` + * header can include `languages`, and other headers support only `action`. + */ + headers?: RequestInitCfPropertiesVaryHeaders; +} +/** Common Vary behavior for a single request header. */ +interface RequestInitCfPropertiesVaryHeader { + /** How this request header contributes to cache variance. */ + action: RequestInitCfPropertiesVaryAction; +} +/** Vary behavior for the `accept` request header. */ +interface RequestInitCfPropertiesVaryAcceptHeader extends RequestInitCfPropertiesVaryHeader { + /** + * Media types to keep when normalizing the `Accept` request header. + * + * Named `media_types` to match the serialized `cf.vary` configuration. + */ + media_types?: string[]; +} +/** Vary behavior for the `accept-language` request header. */ +interface RequestInitCfPropertiesVaryAcceptLanguageHeader extends RequestInitCfPropertiesVaryHeader { + /** + * Language tags to keep when normalizing the `Accept-Language` request + * header. + */ + languages?: string[]; +} +/** + * Lowercase request header names and their Vary behavior. + * + * The index signature allows arbitrary custom request headers beyond the + * well-known `accept` and `accept-language` specializations. + */ +interface RequestInitCfPropertiesVaryHeaders { + accept?: RequestInitCfPropertiesVaryAcceptHeader; + "accept-language"?: RequestInitCfPropertiesVaryAcceptLanguageHeader; + [header: string]: RequestInitCfPropertiesVaryHeader | RequestInitCfPropertiesVaryAcceptHeader | RequestInitCfPropertiesVaryAcceptLanguageHeader | undefined; +} interface BasicImageTransformations { /** * Maximum width in image pixels. The value must be an integer. @@ -13964,7 +14110,7 @@ declare namespace TailStream { interface ConnectEventInfo { readonly type: "connect"; } - type EventOutcome = "ok" | "canceled" | "exception" | "unknown" | "killSwitch" | "daemonDown" | "exceededCpu" | "exceededMemory" | "loadShed" | "responseStreamDisconnected" | "scriptNotFound" | "internalError"; + type EventOutcome = "ok" | "canceled" | "exception" | "unknown" | "killSwitch" | "daemonDown" | "exceededCpu" | "exceededMemory" | "loadShed" | "responseStreamDisconnected" | "scriptNotFound" | "internalError" | "exceededWallTime"; interface ScriptVersion { readonly id: string; readonly tag?: string;