Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ import { normalizeContributorBlacklist } from "../settings/contributor-blacklist
import { normalizeAutonomyPolicy, normalizeAutoMaintainPolicy, DEFAULT_AUTO_MAINTAIN_POLICY } from "../settings/autonomy";
import { decryptSecret, encryptSecret, sha256Hex } from "../utils/crypto";
import { jsonString, nowIso, parseJson, repoParts } from "../utils/json";
import { PUBLIC_LOCAL_PATH_SCRUB_PATTERN } from "../signals/redaction";

const MAX_STORED_BODY_CHARS = 4000;
const SIGNAL_FRESHNESS_LOOKBACK_MS = 14 * 24 * 60 * 60 * 1000;
Expand Down Expand Up @@ -5312,7 +5313,9 @@ const PRODUCT_USAGE_SENSITIVE_KEY =
/authorization|cookie|token|secret|password|private[_-]?key|source|body|diff|patch|prompt|raw[_-]?trust|trust[_-]?score|wallet|hotkey|coldkey|seed|mnemonic|local[_-]?path|repo[_-]?root|cwd|scoreability|reviewability|farming/i;
const PRODUCT_USAGE_SENSITIVE_VALUE =
/\b(seed phrase|mnemonic|private key|raw trust|trust score|wallet|hotkey|coldkey|scoreability|reviewability|farming|reward estimate|payout)\b/i;
const PRODUCT_USAGE_LOCAL_PATH = /(?:\/Users|\/home|\/root|\/var|\/tmp)\/[^\s"',;)]*|[A-Za-z]:\\Users\\[^\s"',;)]*/g;
// Compose from the canonical scrubber in redaction.ts so this surface cannot drift from the boundary;
// it already covered /root/ and /var/, and now unifies the Windows form (also accepts `C:/Users/`).
const PRODUCT_USAGE_LOCAL_PATH = PUBLIC_LOCAL_PATH_SCRUB_PATTERN;
const PRODUCT_USAGE_TOKEN_VALUE = /\b(?:ghp_|github_pat_|gts_|glpat-|sk-)[A-Za-z0-9_=-]{8,}/g;
const PRODUCT_USAGE_BEARER_VALUE = /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi;

Expand Down
5 changes: 4 additions & 1 deletion src/services/agent-action-explanation-card.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AgentActionBlockerCategory, AgentActionExplanationCard, AgentActionRecord } from "../types";
import { PUBLIC_LOCAL_PATH_INLINE } from "../signals/redaction";

type AgentActionExplanationInput = Pick<
AgentActionRecord,
Expand All @@ -9,7 +10,9 @@ const BLOCKER_CATEGORY_ORDER: AgentActionBlockerCategory[] = ["branch", "account
const PUBLIC_FORBIDDEN_PATTERN =
/\b(wallets?|hotkeys?|coldkeys?|seed phrases?|mnemonics?|raw[-_\s]?trust scores?|trust scores?|private reviewability|reviewability internals?|private scoreability|scoreability|projected scores?|score(?:d|s|ability)?|public score estimates?|estimated scores?|score estimates?|score previews?|reward estimates?|payouts?|farming|reward optimization|private rankings?)\b/gi;
const PUBLIC_SCORE_DELTA_PATTERN = /\b(?:projected\s+)?score\w*(?:\s+\w+){0,4}\s+[-+]?\d+(?:\.\d+)?\s*->\s*[-+]?\d+(?:\.\d+)?\b/gi;
const TOKEN_OR_PATH_PATTERN = /\bgithub_pat_[A-Za-z0-9_]+|\bgh[pousr]_[A-Za-z0-9_]+|\/Users\/\S+|\/home\/\S+|\/tmp\/\S+|[A-Z]:\\Users\\\S+/gi;
// Token alternatives stay local; the local-path alternatives compose from the canonical PUBLIC_LOCAL_PATH_INLINE
// in redaction.ts (adds the previously-missed /root/ and /var/, plus the forward-slash Windows form C:/Users/).
const TOKEN_OR_PATH_PATTERN = new RegExp(`\\bgithub_pat_[A-Za-z0-9_]+|\\bgh[pousr]_[A-Za-z0-9_]+|(?:${PUBLIC_LOCAL_PATH_INLINE})\\S+`, "gi");

export function withAgentActionExplanationCard(action: AgentActionRecord): AgentActionRecord {
return { ...action, explanationCard: buildAgentActionExplanationCard(action) };
Expand Down
3 changes: 2 additions & 1 deletion src/services/control-panel-roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { isAuthorizedGitHubSessionLogin } from "../auth/security";
import { getFreshOfficialMinerDetection, getRepository, listAllPullRequests, listInstallations, listRepositories } from "../db/repositories";
import type { ControlPanelRoleCard, ControlPanelRoleName, ControlPanelRoleSummary, InstallationRecord, PullRequestRecord, RepositoryRecord } from "../types";
import { nowIso } from "../utils/json";
import { PUBLIC_LOCAL_PATH_SCRUB_PATTERN } from "../signals/redaction";

export type RoleSummaryInputs = {
login: string;
Expand Down Expand Up @@ -290,7 +291,7 @@ function isMaintainerAssociation(value: string | null | undefined): boolean {

export function sanitizeRoleText(value: string): string {
const redacted = value
.replace(/(?:\/Users|\/home|\/tmp)\/[^\s"',;)]*|[A-Za-z]:\\Users\\[^\s"',;)]*/g, "<redacted-path>")
.replace(PUBLIC_LOCAL_PATH_SCRUB_PATTERN, "<redacted-path>")
.replace(/\b(?:ghp_|github_pat_|gts_|glpat-|sk-)[A-Za-z0-9_=-]{8,}/g, "<redacted-token>")
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, "Bearer <redacted-token>");
if (/\b(seed phrase|mnemonic|private key|raw trust|trust score|wallet|hotkey|coldkey|payout|reward estimate|farming|private reviewability|public score estimate)\b/i.test(redacted)) return "<redacted>";
Expand Down
5 changes: 4 additions & 1 deletion src/services/miner-dashboard-recommendations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ContributorDecisionPack } from "./decision-pack";
import type { SignalSnapshotRecord } from "../types";
import { PUBLIC_LOCAL_PATH_INLINE } from "../signals/redaction";

export type MinerDashboardSignalGroup = "repo_state" | "contributor_state" | "validation_state" | "policy_context";
export type MinerDashboardChangeStatus = "new" | "changed" | "unchanged";
Expand Down Expand Up @@ -42,7 +43,9 @@ const CHANGE_LABEL_LIMIT = 6;
const REASON_LIMIT = 3;
const FORBIDDEN_PUBLIC_TEXT =
/\b(wallets?|hotkeys?|coldkeys?|seed phrases?|mnemonics?|private keys?|raw[-_\s]?trust(?: scores?)?|trust[-_\s]?scores?|reward(?:[-_\s]?(?:estimate|prediction|claim|score))?s?|payouts?|farming(?:[-_\s]?language)?|private[-_\s]?reviewability|private[-_\s]?scoreability|scoreability|public[-_\s]?score[-_\s]?(?:estimate|prediction)|estimated[-_\s]?score|score[-_\s]?estimate)\b/gi;
const LOCAL_PATH = /(?:\/(?:Users|home|root|tmp|var)\/[^\s,;:)]+|[A-Za-z]:\\Users\\[^\s,;:)]+)/g;
// Compose the roots from the canonical PUBLIC_LOCAL_PATH_INLINE in redaction.ts (so this surface cannot drift)
// while preserving this surface's own trailing class and its case-sensitive `/g` (Windows form via `[A-Z]`).
const LOCAL_PATH = new RegExp(`(?:${PUBLIC_LOCAL_PATH_INLINE})[^\\s,;:)]+`, "g");
const FORBIDDEN_TOKEN = /\b(?:ghp_|github_pat_|gts_|glpat-|sk-)[A-Za-z0-9_=-]{8,}/g;

export function previousDecisionPackFromSnapshots(currentPack: ContributorDecisionPack, snapshots: SignalSnapshotRecord[]): ContributorDecisionPack | undefined {
Expand Down
3 changes: 2 additions & 1 deletion src/services/weekly-value-report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
WeeklyValueReportVariant,
} from "../types";
import { nowIso } from "../utils/json";
import { PUBLIC_LOCAL_PATH_SCRUB_PATTERN } from "../signals/redaction";

type WeeklyValueReportInputs = {
generatedAt: string;
Expand Down Expand Up @@ -409,7 +410,7 @@ function normalizeReportDays(value: number | null | undefined): number {

function sanitizeReportText(value: string): string {
const redacted = value
.replace(/(?:\/Users|\/home|\/tmp)\/[^\s"',;)]*|[A-Za-z]:\\Users\\[^\s"',;)]*/g, "<redacted-path>")
.replace(PUBLIC_LOCAL_PATH_SCRUB_PATTERN, "<redacted-path>")
.replace(/\b(?:ghp_|github_pat_|gts_|glpat-|sk-)[A-Za-z0-9_=-]{8,}/g, "<redacted-token>")
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, "Bearer <redacted-token>");
if (
Expand Down
15 changes: 10 additions & 5 deletions src/signals/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { parse as parseYaml } from "yaml";
import type { GatePolicyPack, GateRuleMode, JsonValue, RepositorySettings } from "../types";
import { normalizeAutonomyPolicy, normalizeAutoMaintainPolicy } from "../settings/autonomy";
import { mergeContributorBlacklists, normalizeContributorBlacklist } from "../settings/contributor-blacklist";
import { PUBLIC_LOCAL_PATH_INLINE } from "./redaction";

export type FocusManifestSource = "repo_file" | "api_record" | "none";
export type FocusManifestLinkedIssuePolicy = "required" | "preferred" | "optional";
Expand Down Expand Up @@ -284,16 +285,20 @@ const EMPTY_MANIFEST: FocusManifest = {
warnings: [],
};

// This surface's economic/identity term vocabulary is intentionally richer than the canonical
// PUBLIC_UNSAFE_TERMS (extra phrases like "public score estimate"), so it stays a local literal. The local
// filesystem paths, however, compose from the canonical PUBLIC_LOCAL_PATH_INLINE in redaction.ts (which also
// covers `/var/`, previously missed here, plus `/root/` and the forward-slash Windows form `C:/Users/`) so this
// guard cannot drift from the canonical boundary on a leaking root.
const FOCUS_MANIFEST_TERMS = /\b(reward\w*|score\w*|wallets?|hotkeys?|coldkeys?|seed[-\s]?phrases?|mnemonics?|private[-\s]?keys?|farming|payouts?|rankings?|raw[-\s]?trust(?:[-\s]?scores?)?|trust[-\s]?scores?|private[-\s]?reviewability|reviewability(?:[-\s]?internals?)?|private[-\s]?scoreability|scoreability|public[-\s]?score[-\s]?(?:estimate|prediction|claim)s?|estimated[-\s]?scores?|score[-\s]?(?:estimate|prediction|preview)s?)\b/i;
const FOCUS_MANIFEST_LOCAL_PATH_PATTERN = new RegExp(PUBLIC_LOCAL_PATH_INLINE, "i");

/**
* Public-safe redaction guard shared with the local-branch packet renderer. Public manifest
* text must not leak reward, wallet/key, ranking, or local filesystem path material.
*/
export function isFocusManifestPublicSafe(text: string): boolean {
// Local filesystem path alternatives mirror the canonical PUBLIC_UNSAFE_PATTERN in redaction.ts: the full
// set is `/Users/`, `/home/`, `/root/` (container/CI home), `/var/` (logs, temp, CI workspaces), `/tmp/`,
// and the Windows `[A-Z]:[\/]Users[\/]` form (both slash directions). Any omission leaks that path prefix
// through this public-safe guard.
return !/\b(reward\w*|score\w*|wallets?|hotkeys?|coldkeys?|seed[-\s]?phrases?|mnemonics?|private[-\s]?keys?|farming|payouts?|rankings?|raw[-\s]?trust(?:[-\s]?scores?)?|trust[-\s]?scores?|private[-\s]?reviewability|reviewability(?:[-\s]?internals?)?|private[-\s]?scoreability|scoreability|public[-\s]?score[-\s]?(?:estimate|prediction|claim)s?|estimated[-\s]?scores?|score[-\s]?(?:estimate|prediction|preview)s?)\b|\/Users\/|\/home\/|\/root\/|\/var\/|\/tmp\/|[A-Z]:[\\/]Users[\\/]/i.test(text);
return !FOCUS_MANIFEST_TERMS.test(text) && !FOCUS_MANIFEST_LOCAL_PATH_PATTERN.test(text);
}

function emptyManifest(source: FocusManifestSource, warnings: string[] = []): FocusManifest {
Expand Down
4 changes: 2 additions & 2 deletions src/signals/local-branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { buildRepoRewardRisk, type RepoRewardRisk, type RewardRiskAction } from
import { buildLocalWorkspaceIntelligence, type LocalWorkspaceIntelligence } from "./local-workspace-intelligence";
import { buildFocusManifestGuidance, parseFocusManifest, type FocusManifestGuidance } from "./focus-manifest";
import { sanitizeLocalScorerWarnings } from "./local-scorer-diagnostics";
import { isPublicSafeText } from "./redaction";
import { isPublicSafeText, PUBLIC_LOCAL_PATH_PREFIX_PATTERN } from "./redaction";
import { deriveEligibilityPlan } from "../services/eligibility-plan";
import { scenarioInputFromLocalBranchMetadata } from "../scenarios/input-model";
import { renderPublicScenarioSummary, type PublicScenarioSummary, type ScenarioSummaryInput } from "../scenarios/scenario-summary";
Expand Down Expand Up @@ -1233,7 +1233,7 @@ function firstCommitTitle(messages: string[] | undefined): string | undefined {

function safeRepoPath(path: string): string {
/* v8 ignore next -- Empty path fallback protects malformed local-git adapters; path redaction is covered by local branch tests. */
return /^(\/Users\/|\/home\/|\/root\/|\/tmp\/|[A-Z]:\/Users\/)/i.test(String(path).replace(/\\/g, "/")) ? "[local path hidden]" : String(path || "(unknown path)").replace(/\\/g, "/");
return PUBLIC_LOCAL_PATH_PREFIX_PATTERN.test(String(path).replace(/\\/g, "/")) ? "[local path hidden]" : String(path || "(unknown path)").replace(/\\/g, "/");
}

export function isTestFile(file: string): boolean {
Expand Down
22 changes: 21 additions & 1 deletion src/signals/redaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,27 @@
// intentionally NOT collapsed onto `PUBLIC_UNSAFE_TERMS`.
export const PUBLIC_UNSAFE_TERMS = String.raw`(?:reward|score|wallet|hotkey|coldkey|mnemonic|payout|ranking)\w*|farming|raw[-_\s]?trust|trust[-_\s]?score|private[-_\s]?reviewability|reviewability`;

export const PUBLIC_UNSAFE_PATTERN = new RegExp(String.raw`\b(${PUBLIC_UNSAFE_TERMS})\b|/Users/|/home/|/root/|/var/|/tmp/|[A-Z]:[\\/]Users[\\/]`, "i");
// `PUBLIC_LOCAL_PATH_INLINE` is the canonical local-filesystem-root vocabulary (alternation source only —
// no flags, no anchors), the path analogue of `PUBLIC_UNSAFE_TERMS`. Public surfaces that detect or scrub
// absolute local paths compose from this one source instead of re-typing the root list, so a surface cannot
// drift and miss a root (e.g. `/root/` for container/CI homes, `/var/` for service paths) the canonical
// boundary blocks. It accepts both the back- and forward-slash Windows form (`C:\Users\`, `C:/Users/`). The
// drive letter is matched case-insensitively at the source (`[A-Za-z]`, not `[A-Z]`) so a consumer that omits
Comment thread
GildardoDev marked this conversation as resolved.
// the `i` flag (e.g. the case-sensitive `/g` scrubber in miner-dashboard-recommendations.ts) still redacts a
// lower-case drive like `c:\Users\...`; the unix roots stay literal so case-sensitivity there is the caller's.
export const PUBLIC_LOCAL_PATH_INLINE = String.raw`/Users/|/home/|/root/|/var/|/tmp/|[A-Za-z]:[\\/]Users[\\/]`;

// Global scrubber for `.replace()` surfaces that swap an absolute local path for a placeholder: matches a
// root from `PUBLIC_LOCAL_PATH_INLINE` plus the rest of the path segment (stopping at whitespace or a common
// delimiter). Sharing one `/g` constant across modules is safe because `String.prototype.replace` resets
// `lastIndex` after each call (unlike `.test()`, which is why the boundary patterns below stay non-global).
export const PUBLIC_LOCAL_PATH_SCRUB_PATTERN = new RegExp(String.raw`(?:${PUBLIC_LOCAL_PATH_INLINE})[^\s"',;)]*`, "gi");

// Anchored, non-global guard for surfaces that test whether a single path STARTS at a local root (e.g. the
// local-branch repo-path renderer). Non-global so `.test()` stays stateless across calls.
export const PUBLIC_LOCAL_PATH_PREFIX_PATTERN = new RegExp(String.raw`^(?:${PUBLIC_LOCAL_PATH_INLINE})`, "i");

export const PUBLIC_UNSAFE_PATTERN = new RegExp(String.raw`\b(${PUBLIC_UNSAFE_TERMS})\b|${PUBLIC_LOCAL_PATH_INLINE}`, "i");

/** True iff `text` contains nothing that must stay private — i.e. it is safe to surface on a public GitHub surface. */
export function isPublicSafeText(text: string): boolean {
Expand Down
15 changes: 15 additions & 0 deletions test/unit/agent-orchestrator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,21 @@ describe("agent orchestrator", () => {
expect(publicPacket.publicSafe.rerunWhen).toMatch(/private context/);
});

it("redacts /root/, /var/, and forward-slash Windows local paths from the public-safe card (#1418)", () => {
const card = buildAgentActionExplanationCard({
actionType: "prepare_pr_packet",
status: "ready",
why: ["A concise packet keeps public context focused on linked work."],
blockedBy: [],
// /root/ and /var/ were previously missed by this card's local path regex; C:/Users/ (forward-slash) is also now covered.
publicSafeSummary: "Built at /root/work/repo and /var/log/app.log on C:/Users/alice/repo.",
safetyClass: "public_safe",
});

expect(card.publicSafe.summary).toContain("<redacted>");
expect(card.publicSafe.summary).not.toMatch(/\/root\/work|\/var\/log|C:\/Users\/alice/);
});

it("covers local action ready and blocker-free branches from prepared metadata", () => {
const run = __agentOrchestratorInternals.buildRunRecord({
objective: "local ready branch",
Expand Down
13 changes: 12 additions & 1 deletion test/unit/control-panel-roles.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { buildControlPanelAccessScope, buildControlPanelRoleSummary, loadControlPanelRoleSummary } from "../../src/services/control-panel-roles";
import { __controlPanelRolesInternals, buildControlPanelAccessScope, buildControlPanelRoleSummary, loadControlPanelRoleSummary } from "../../src/services/control-panel-roles";
import type { InstallationRecord, PullRequestRecord, RepositoryRecord } from "../../src/types";
import { createTestEnv } from "../helpers/d1";

Expand Down Expand Up @@ -69,6 +69,17 @@ describe("control panel role summaries", () => {
expect(JSON.stringify(summary)).not.toMatch(/\/Users|github_pat|1234567890abcdef|wallet|hotkey/);
});

it("redacts all local-path roots including /root/, /var/, and forward-slash Windows paths (#1418)", () => {
const { sanitizeRoleText } = __controlPanelRolesInternals;
// /root/ and /var/ were previously missed by this surface's local copy of the path regex.
expect(sanitizeRoleText("clone at /root/work/repo/src")).toBe("clone at <redacted-path>");
expect(sanitizeRoleText("log at /var/log/app/run.log")).toBe("log at <redacted-path>");
expect(sanitizeRoleText("checkout C:/Users/alice/repo")).toBe("checkout <redacted-path>");
// Already-covered roots stay redacted (no regression).
expect(sanitizeRoleText("see /Users/me/repo")).toBe("see <redacted-path>");
expect(sanitizeRoleText("see C:\\Users\\me\\repo")).toBe("see <redacted-path>");
});

it("recognizes account installations even before an owned repo is cached", () => {
const summary = buildControlPanelRoleSummary({
login: "repo-owner",
Expand Down
2 changes: 2 additions & 0 deletions test/unit/focus-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,8 @@ describe("public-safe invariant", () => {
expect(isFocusManifestPublicSafe("see /Users/me/repo/src")).toBe(false);
expect(isFocusManifestPublicSafe("see /home/dev/repo/src")).toBe(false);
expect(isFocusManifestPublicSafe("see /root/repo/src")).toBe(false);
// #1418: `/var/` was previously missed by this guard's local copy; it now composes from the canonical source.
expect(isFocusManifestPublicSafe("see /var/folders/me/work/repo")).toBe(false);
expect(isFocusManifestPublicSafe("see /var/log/build.log")).toBe(false);
expect(isFocusManifestPublicSafe("see /tmp/build/out")).toBe(false);
// Windows, both backslash and forward-slash forms.
Expand Down
26 changes: 26 additions & 0 deletions test/unit/local-branch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,32 @@ describe("local branch analysis", () => {
expect(analysis.prPacket.markdown).not.toContain("/root/work");
});

it("hides /var/ service paths and forward-slash Windows paths from public PR packet changed paths (#1418)", () => {
const analysis = buildLocalBranchAnalysis({
input: {
login: "oktofeesh1",
repoFullName: repo.fullName,
body: "Fixes #7",
changedFiles: [
{ path: "/var/folders/work/src/cache.ts", additions: 12, deletions: 2, status: "modified" },
{ path: "C:/Users/alice/work/src/util.ts", additions: 3, deletions: 1, status: "modified" },
],
validation: [{ command: "npm test -- cache", status: "passed" }],
},
repo,
issues: [{ repoFullName: repo.fullName, number: 7, title: "Cache refresh fails", state: "open", labels: ["bug"], linkedPrs: [] }],
pullRequests: [],
profile,
outcomeHistory,
scoringSnapshot,
scoringProfile,
});

expect(analysis.prPacket.markdown).toContain("[local path hidden]");
expect(analysis.prPacket.markdown).not.toContain("/var/folders");
expect(analysis.prPacket.markdown).not.toContain("C:/Users/alice");
});

it("removes snake_case private signals from public PR packet markdown", () => {
const analysis = buildLocalBranchAnalysis({
input: {
Expand Down
24 changes: 24 additions & 0 deletions test/unit/miner-dashboard-recommendations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,30 @@ describe("miner dashboard recommendation metadata", () => {
expect(priorityChange).not.toHaveProperty("after");
});

it("redacts /root/, /var/, and forward-slash Windows local paths from rerun reasons (#1418)", () => {
const current = decisionPack({
generatedAt: "2026-06-02T00:00:00.000Z",
topActions: [action()],
actionPortfolio: {
topActions: [
{
repoFullName: "JSONbored/gittensory",
actionKind: "open_new_direct_pr",
// /root/ and /var/ were already covered here; C:/Users/ (forward-slash) and the lower-case drive
// form (c:\Users\...) are now covered via the shared source, which matches the drive letter
// case-insensitively even though this surface's scrubber omits the `i` flag.
rerunWhen: "Rerun when PRs change at /root/work/repo, /var/log/app.log, C:/Users/alice/repo, and c:\\Users\\bob\\secret.",
},
],
},
});

const [enriched] = buildMinerDashboardNextActions(current);
const repoStateReasons = enriched?.rerunReasons.find((group) => group.group === "repo_state")?.reasons.join(" ") ?? "";
expect(repoStateReasons).toContain("[local path]");
expect(JSON.stringify(enriched?.rerunReasons)).not.toMatch(/\/root\/work|\/var\/log|C:\/Users\/alice|c:\\Users\\bob/);
});

it("selects the previous ready decision-pack snapshot", () => {
const current = decisionPack({ generatedAt: "2026-06-02T00:00:00.000Z" });
const previous = decisionPack({ generatedAt: "2026-06-01T00:00:00.000Z" });
Expand Down
Loading
Loading