Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ce1a3ae
Plan deterministic trace graders
Copilot Aug 22, 2026
de6658f
Add initial deterministic trace graders
Copilot Aug 22, 2026
4b4de8a
docs(adr): add draft ADR-54678 for deterministic trace grading framework
github-actions[bot] Aug 22, 2026
9eb6028
Merge branch 'main' into copilot/implement-deterministic-trace-graders
github-actions[bot] Aug 22, 2026
0d6a86e
Harden trace grader runtime isolation and manifest transport
Copilot Aug 22, 2026
53938a6
Merge branch 'main' into copilot/implement-deterministic-trace-graders
github-actions[bot] Aug 22, 2026
1a1b44d
Merge remote-tracking branch 'origin/main' into copilot/implement-det…
Copilot Aug 22, 2026
ed01ef4
Refresh branch and fix trace graders JS type casts
Copilot Aug 22, 2026
ef94444
Merge remote-tracking branch 'origin/main' into copilot/implement-det…
Copilot Aug 22, 2026
dce434c
Integrate experiment context into graders and evals
Copilot Aug 22, 2026
7a02562
Allow experiments metrics to reference grader results
Copilot Aug 22, 2026
b0ad3c7
Mark trace graders as experimental
Copilot Aug 22, 2026
673bba9
Merge branch 'main' into copilot/implement-deterministic-trace-graders
github-actions[bot] Aug 22, 2026
1781737
Merge branch 'main' into copilot/implement-deterministic-trace-graders
github-actions[bot] Aug 22, 2026
2906c4a
fix trace grader sandbox embedding and script-size contract
Copilot Aug 22, 2026
803ce40
test: add graders integration workflow coverage
Copilot Aug 22, 2026
ba7aae8
Merge branch 'main' into copilot/implement-deterministic-trace-graders
github-actions[bot] Aug 22, 2026
9baed09
Merge branch 'main' into copilot/implement-deterministic-trace-graders
github-actions[bot] Aug 22, 2026
f377fa2
Merge branch 'main' into copilot/implement-deterministic-trace-graders
github-actions[bot] Aug 22, 2026
00ca65f
Harden trace grader log parsing and summary output
Copilot Aug 22, 2026
9fff955
Harden trace grader fallback parsing and summary safety
Copilot Aug 22, 2026
096aa3e
chore: start triage for unresolved review feedback
Copilot Aug 22, 2026
455639f
fix grader sandbox clone fallback and parser edge cases
Copilot Aug 22, 2026
1d3542e
fix: rename grader clone helper to explicit clone-or-undefined
Copilot Aug 22, 2026
4e303c5
Merge branch 'main' into copilot/implement-deterministic-trace-graders
github-actions[bot] Aug 22, 2026
6c61d48
refactor: rename trace graders wording to graders
Copilot Aug 22, 2026
1b3d870
Merge remote-tracking branch 'origin/main' into copilot/implement-det…
Copilot Aug 22, 2026
5131cd9
refactor: rename grader implementation id
Copilot Aug 22, 2026
782f56d
docs: add graders specification
Copilot Aug 22, 2026
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
19 changes: 18 additions & 1 deletion actions/setup/js/experiment_helpers.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,25 @@ function readExperimentAssignments() {
}
return null;
} catch {
return null;
// Fall through to environment-variable fallback.
}

/** @type {Record<string, string>} */
const fromEnv = {};
for (const [key, value] of Object.entries(process.env)) {
if (!key.startsWith("GH_AW_EXPERIMENTS_")) {
continue;
}
const name = key.substring("GH_AW_EXPERIMENTS_".length).toLowerCase();
if (!name || typeof value !== "string" || value.length === 0) {
continue;
}
fromEnv[name] = value;
}
if (Object.keys(fromEnv).length > 0) {
return fromEnv;
}
return null;
}

module.exports = { readExperimentAssignments, EXPERIMENT_ASSIGNMENTS_PATH };
8 changes: 8 additions & 0 deletions actions/setup/js/experiment_helpers.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ describe("readExperimentAssignments", () => {

afterEach(() => {
readFileSpy.mockRestore();
delete process.env.GH_AW_EXPERIMENTS_PROMPT_STYLE;
delete process.env.GH_AW_EXPERIMENTS_REASONING_DEPTH;
if (savedStateDir !== undefined) {
process.env.GH_AW_EXPERIMENT_STATE_DIR = savedStateDir;
} else {
Expand Down Expand Up @@ -67,4 +69,10 @@ describe("readExperimentAssignments", () => {
});
expect(readExperimentAssignments()).toEqual({ mode: "fast" });
});

it("falls back to GH_AW_EXPERIMENTS_* environment variables when assignments file is unavailable", () => {
process.env.GH_AW_EXPERIMENTS_PROMPT_STYLE = "concise";
process.env.GH_AW_EXPERIMENTS_REASONING_DEPTH = "deep";
expect(readExperimentAssignments()).toEqual({ prompt_style: "concise", reasoning_depth: "deep" });
});
});
38 changes: 37 additions & 1 deletion actions/setup/js/redact_secrets.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -343,4 +343,40 @@ async function main() {
}
}

module.exports = { main, redactSecrets, redactBuiltInPatterns, redactStepSummaryContent, extractMCPGatewayTokens, BUILT_IN_PATTERNS, MCP_GATEWAY_CONFIG_PATHS };
/**
* Targeted redaction pass for files in a specific directory. Used by the
* post-graders redaction step to scan grader output files that were written
* after the initial full-workspace redaction pass.
*
* @param {string} dir - Absolute directory path to scan
*/
async function redactFilesInDir(dir) {
try {
const secretNames = (process.env.GH_AW_SECRET_NAMES || "").split(",").filter(n => n.trim());
/** @type {string[]} */
const secretValues = [];
for (const secretName of secretNames) {
const value = process.env[`SECRET_${secretName.trim()}`];
if (typeof value === "string" && value.trim() !== "") {
secretValues.push(value.trim());
}
}
secretValues.push(...extractMCPGatewayTokens(MCP_GATEWAY_CONFIG_PATHS));

const targetExtensions = [".json"];
const files = findFiles(dir, targetExtensions);
if (files.length === 0) return;

let totalRedactions = 0;
for (const file of files) {
totalRedactions += processFile(file, secretValues);
}
if (totalRedactions > 0) {
core.info(`Grader output redaction: ${totalRedactions} redaction(s) in ${dir}`);
}
} catch (error) {
core.warning(`Grader output redaction failed: ${getErrorMessage(error)}`);
}
}

module.exports = { main, redactFilesInDir, redactSecrets, redactBuiltInPatterns, redactStepSummaryContent, extractMCPGatewayTokens, BUILT_IN_PATTERNS, MCP_GATEWAY_CONFIG_PATHS };
3 changes: 3 additions & 0 deletions actions/setup/js/run_evals.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const path = require("path");
const { ERR_VALIDATION, ERR_SYSTEM } = require("./error_codes.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");
const { EVALS_OUTPUT_PATH } = require("./evals_constants.cjs");
const { readExperimentAssignments } = require("./experiment_helpers.cjs");
const { resolveModelWithFallback } = require("./model_fallback.cjs");

const EVALS_DIR = "/tmp/gh-aw/evals";
Expand Down Expand Up @@ -127,6 +128,7 @@ async function parseMain() {
const questionsRaw = process.env.GH_AW_EVALS_QUESTIONS;
const model = resolveModelWithFallback(process.env, "GH_AW_EVALS_MODEL") || "";
const runID = process.env.GITHUB_RUN_ID || "unknown";
const experimentAssignments = readExperimentAssignments();

/** @type {Array<{id: string, question: string}>} */
let questions = [];
Expand Down Expand Up @@ -185,6 +187,7 @@ async function parseMain() {
model,
timestamp,
runid: runID,
experiments: experimentAssignments || undefined,
};
results.push(record);
core.info(`Q[${q.id}]: ${answer}`);
Expand Down
21 changes: 21 additions & 0 deletions actions/setup/js/run_evals.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { createRequire } from "module";
const EVALS_DIR = "/tmp/gh-aw/evals";
const EVALS_LOG_PATH = `${EVALS_DIR}/evals.log`;
const EVALS_OUTPUT_PATH = "/tmp/gh-aw/evals.jsonl";
const EXPERIMENT_ASSIGNMENTS_PATH = "/tmp/gh-aw/experiments/assignments.json";
const require = createRequire(import.meta.url);
const { MODEL_FALLBACK_ENV_VAR } = require("./model_fallback.cjs");
const { setupMain, parseMain, extractAssistantTextFromJsonlLog } = require("./run_evals.cjs");
Expand Down Expand Up @@ -32,6 +33,9 @@ describe("run_evals.cjs", () => {
if (fs.existsSync(EVALS_OUTPUT_PATH)) {
fs.unlinkSync(EVALS_OUTPUT_PATH);
}
if (fs.existsSync(EXPERIMENT_ASSIGNMENTS_PATH)) {
fs.unlinkSync(EXPERIMENT_ASSIGNMENTS_PATH);
}
});

afterEach(() => {
Expand All @@ -42,6 +46,9 @@ describe("run_evals.cjs", () => {
if (fs.existsSync(EVALS_OUTPUT_PATH)) {
fs.unlinkSync(EVALS_OUTPUT_PATH);
}
if (fs.existsSync(EXPERIMENT_ASSIGNMENTS_PATH)) {
fs.unlinkSync(EXPERIMENT_ASSIGNMENTS_PATH);
}
});

it("stores the workflow run id when writing eval records", async () => {
Expand All @@ -64,6 +71,20 @@ describe("run_evals.cjs", () => {
});
});

it("includes experiment assignments in eval records when available", async () => {
vi.stubEnv("GH_AW_EVALS_QUESTIONS", JSON.stringify([{ id: "labels-applied", question: "Did labels get applied?" }]));
vi.stubEnv("GH_AW_EVALS_MODEL", "small");
vi.stubEnv("GITHUB_RUN_ID", "123456789");
fs.mkdirSync("/tmp/gh-aw/experiments", { recursive: true });
fs.writeFileSync("/tmp/gh-aw/experiments/assignments.json", JSON.stringify({ prompt_style: "concise" }) + "\n", "utf8");
fs.writeFileSync(EVALS_LOG_PATH, "labels-applied: YES\n", "utf8");

await parseMain();

const [line] = fs.readFileSync(EVALS_OUTPUT_PATH, "utf8").trim().split("\n");
expect(JSON.parse(line).experiments).toEqual({ prompt_style: "concise" });
});

it('falls back to "unknown" when the workflow run id is absent', async () => {
vi.stubEnv("GH_AW_EVALS_QUESTIONS", JSON.stringify([{ id: "labels-applied", question: "Did labels get applied?" }]));
vi.stubEnv("GH_AW_EVALS_MODEL", "small");
Expand Down
Loading
Loading