Skip to content

Add initial deterministic trace grading framework - #54678

Open
pelikhan with Copilot wants to merge 15 commits into
mainfrom
copilot/implement-deterministic-trace-graders
Open

Add initial deterministic trace grading framework#54678
pelikhan with Copilot wants to merge 15 commits into
mainfrom
copilot/implement-deterministic-trace-graders

Conversation

Copilot AI commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the initial opt-in deterministic trace grading framework to the existing agent job, including frontmatter parsing, built-in metrics, inline JavaScript support, persisted grading files, detection staging, tests, and reference documentation.

Status

This draft is incomplete. Remaining work includes schema registration, full canonical manifest/result metadata and thresholds, restricted VM execution for inline scripts, generic detection interpretation, and final review/security validation.

Validation

  • Focused Go tests passed in the implementation agent
  • Focused JavaScript tests passed in the implementation agent
  • Secret scan passed

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 19.1 AIC · ⌖ 8.22 AIC · ⊞ 9.5K ·
Comment /souschef to run again


run: https://github.com/github/gh-aw/actions/runs/32547644097> Generated by 👨‍🍳 PR Sous Chef · gpt54 · 19.9 AIC · ⌖ 9.76 AIC · ⊞ 9.5K ·

Comment /souschef to run again


pr-sous-chef run: https://github.com/github/gh-aw/actions/runs/32552825826> Generated by 👨‍🍳 PR Sous Chef · gpt54 · 24.2 AIC · ⌖ 8.28 AIC · ⊞ 9.5K ·

Comment /souschef to run again


Run: https://github.com/github/gh-aw/actions/runs/32558278813> Generated by 👨‍🍳 PR Sous Chef · gpt54 · 7.36 AIC · ⌖ 8.18 AIC · ⊞ 9.5K ·

Comment /souschef to run again


Branch refresh requested by PR Sous Chef.
Run: https://github.com/github/gh-aw/actions/runs/32569036201> Generated by 👨‍🍳 PR Sous Chef · gpt54 · 28.6 AIC · ⌖ 8.36 AIC · ⊞ 9.5K ·

Comment /souschef to run again

Copilot AI and others added 2 commits August 22, 2026 00:05
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI requested a review from pelikhan August 22, 2026 00:53
@pelikhan
pelikhan marked this pull request as ready for review August 22, 2026 01:33
Copilot AI balanced review requested due to automatic review settings August 22, 2026 01:33
@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Ponytail Reviewer completed successfully!

Generated by Ponytail Reviewer for #54678

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

Test Quality Sentinel skipped because pre-fetch PR data was unavailable: unable to fetch test file diff

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

Copy link
Copy Markdown
Contributor

Comment Memory

reviewed_at: 2026-08-22T00:00:00Z
review_event: REQUEST_CHANGES
top_themes:
  - inline grader sandbox escape via source interpolation
  - inconsistent custom script size limit between docs and parser
files_reviewed:
  - .github/workflows/weekly-network-domains-audit.lock.yml
  - actions/setup/js/redact_secrets.cjs
  - actions/setup/js/trace_graders.cjs
  - actions/setup/js/trace_graders.test.cjs
  - actions/setup/sh/prepare_threat_detection_files.sh
  - docs/src/content/docs/reference/trace-graders.md
  - pkg/constants/job_constants.go
  - pkg/workflow/compiler_orchestrator_workflow.go
  - pkg/workflow/compiler_yaml_artifacts.go
  - pkg/workflow/compiler_yaml_graders.go
  - pkg/workflow/compiler_yaml_post_agent.go
  - pkg/workflow/frontmatter_types.go
  - pkg/workflow/graders_config.go
  - pkg/workflow/graders_config_test.go
  - pkg/workflow/workflow_data.go
comment_count: 2

Note

This comment is managed by comment memory.

It stores persistent context for this thread in the code block at the top of this comment.
Edit only the text inside the backtick fences; workflow metadata and the footer are regenerated automatically.

Learn more about comment memory

🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 25.5 AIC · ⌖ 7.03 AIC · ⊞ 7K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes

The new trace-grader framework is not ready to merge yet: the inline-script execution path still has a sandbox-escape bug, and the user-facing config contract is already inconsistent on the maximum script size.

Blocking themes
  • Inline grader source is interpolated directly into generated code before vm.runInContext, which undermines the security model for “trusted” scripts and makes the sandbox easy to bypass.
  • The compiler and reference docs disagree on the script-length limit, so the feature contract is not deterministic for users.

🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 25.5 AIC · ⌖ 7.03 AIC · ⊞ 7K
Comment /review to run again

Comment thread actions/setup/js/trace_graders.cjs Outdated
});

// Wrap script as function body
const wrappedScript = `(function(trace, run, workflow, config, helpers) { "use strict"; ${script} })`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline grader sandbox is still trivially escapable because you interpolate the untrusted script directly into a function body before vm.runInContext, so a script containing backticks / ${...} can break out of the intended body and execute arbitrary host-side code before your sandbox restrictions matter.

💡 Why this blocks the feature

wrappedScript := \(function(...) { "use strict"; ${script} })`` treats the grader source as template-literal content, not opaque code. That means payloads such as }; this.constructor.constructor('return process')() // or any backtick-terminated variant can rewrite the wrapper itself, bypass the “no require/process/fetch” guarantees, and run outside the contract you document. The parser only blocks a few substrings, so this is a real sandbox-escape surface, not a theoretical one.

Use a transport that does not re-embed source inside another source string, for example compile the grader as its own script/module and call a known exported function, or JSON-stringify the body and recover it inside the VM instead of concatenating raw text into the wrapper.

Comment thread pkg/workflow/graders_config.go Outdated
if isBuiltin {
return fmt.Errorf("graders.%s is a built-in grader and cannot have a custom script", id)
}
if len(s) > 4096 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compiler and docs disagree on the script size limit: the parser accepts 4096 characters while the public contract says custom graders are limited to 2 KB, so valid documented configs will be rejected or oversized configs will slip through depending on which component the user trusts.

💡 Tighten the contract in one place

pkg/workflow/graders_config.go enforces len(s) > 4096, but docs/src/content/docs/reference/trace-graders.md still says “≤2 KB”. That mismatch is guaranteed to create confusing compile-time failures and makes the safety boundary ambiguous for a feature that executes code.

Pick one limit, encode it as a shared constant if possible, and update both validation and docs together so users get a single, deterministic rule.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ponytail pass — over-engineering only, 3 findings:

  • sortStrings reimplements sort.Strings (already imported/used in the same file) with a bubble sort.
  • ParseGradersFromFrontmatter public wrapper has zero callers in this diff or repo.
  • runGrader legacy adapter duplicates the real dispatch logic in runBuiltinGrader/runCustomGrader and exists only for tests.

net: -25 lines possible.

Generated by ✂️ Ponytail Reviewer for #54678 · auto · 46.4 AIC · ⌖ 3.52 AIC · ⊞ 7.3K
Comment /ponytail to run again

Comments that could not be inline-anchored

pkg/workflow/compiler_yaml_graders.go:227

stdlib: hand-rolled bubble sort for sortStrings. sort.Strings(disabledIDs), 1 line (package already imports sort and uses it elsewhere in this file).

pkg/workflow/graders_config.go:416

yagni: ParseGradersFromFrontmatter public wrapper has no callers anywhere in the diff or repo. Delete until an external caller exists.

actions/setup/js/trace_graders.cjs:512

yagni: runGrader legacy adapter duplicates runBuiltinGrader/runCustomGrader dispatch and is only referenced by tests, not by main(). Drop it and call the two real functions directly from tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (1,071 new lines) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/54678-deterministic-trace-grading-framework.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete the missing sections — add context the AI couldn't infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-54678: Deterministic Trace Grading Framework

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 0042-use-postgresql.md for PR #42).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · sonnet46 · 95.3 AIC · ⌖ 16.8 AIC · ⊞ 9.5K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /codebase-design, /tdd, and /developer-security — requesting changes on correctness and security issues before this graduates from draft.

📋 Key Themes & Highlights

Key Themes

  • Metric correctness: gradeToolSuccessRate and gradeToolFailureCount use asymmetric predicates (comment on line 248); gradeContextGrowth mixes input+output tokens in a way that doesn't match the stated intent (line 288)
  • Surprising UX: listing one built-in grader silently enables all nine — undocumented behaviour that will confuse users (line 253)
  • Security: manifest JSON is single-quote-escaped before embedding in YAML, while the exec spec uses base64 — inconsistency leaves a potential injection surface for unusual JSON values (line 45 of compiler_yaml_graders.go)
  • Test quality: TestBuildGraderManifest calls len() on a struct rather than its .Graders slice; the proto-pollution test validates V8 built-in behaviour rather than any gh-aw code
  • Minor: hand-rolled bubble sort in sortStrings duplicates sort.Strings from the same package; gradeLoops description vs. implementation mismatch

Positive Highlights

  • ✅ Excellent sandbox hardening for custom scripts: codeGeneration: {strings: false, wasm: false}, frozen trace, Math.random removal
  • ✅ Single preprocessing pass for all graders — clean design that avoids redundant file I/O
  • ✅ Base64 encoding of exec spec prevents YAML/JS injection for the script payload
  • ✅ Strong test coverage overall — 567 lines of JS tests and 381 lines of Go tests for a new feature
  • continue-on-error: true on the graders step correctly avoids blocking CI on metric failures

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 102.3 AIC · ⌖ 12.1 AIC · ⊞ 7.6K
Comment /matt to run again

*/
function gradeToolSuccessRate(trace) {
if (trace.toolCalls.length === 0) return 1;
const successes = trace.toolCalls.filter(t => t.success === true || (t.success !== false && t.status !== "error" && t.status !== "failure" && t.error === undefined)).length;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] gradeToolSuccessRate and gradeToolFailureCount use independent, asymmetric success/failure heuristics — a tool with success !== false and no error field counts as a success, but the same logic doesn't guarantee it counts as a non-failure. This means successRate + failureCount/total can differ from 1 on ambiguous entries, undermining comparability of the two metrics.

💡 Suggestion: extract a shared predicate
function isToolFailure(t) {
  return t.success === false || t.status === "error" || t.status === "failure" || t.error !== undefined;
}

function gradeToolSuccessRate(trace) {
  if (trace.toolCalls.length === 0) return 1;
  return (trace.toolCalls.length - trace.toolCalls.filter(isToolFailure).length) / trace.toolCalls.length;
}

function gradeToolFailureCount(trace) {
  return trace.toolCalls.filter(isToolFailure).length;
}

This guarantees successRate === 1 - failureCount/total and makes the invariant expressible in a single test.

@copilot please address this.

}

/** @param {PreprocessedTrace} trace @returns {number} */
function gradeLoops(trace) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] gradeLoops counts only consecutive identical calls (same name + args), so interleaved repeated calls — which are also a looping symptom — go undetected. The metric name "Loops" implies a broader pattern, but the implementation is narrower than the description suggests.

💡 Suggestion

Either rename to consecutive-identical-calls to match what the code actually measures, or document the intentional constraint in the JSDoc so callers understand why [read /a, write /b, read /a] scores 0 loops. A test for the non-consecutive case would also pin down the contract.

@copilot please address this.

}

/** @param {PreprocessedTrace} trace @returns {number} */
function gradeTrajectoryEfficiency(trace) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] gradeTrajectoryEfficiency uses uniqueTools / totalCalls, capped at 1. For any session with only one tool call this returns 1 (trivially "perfect"), but single-call sessions are likely the least interesting for diversity analysis. The metric conflates diversity with efficiency in a way that may not be meaningful at extremes.

💡 Suggestion

Document the known edge-case behaviour in the JSDoc. If the intent is to measure diversity of tool usage rather than efficiency, consider returning null/unavailable when toolCalls.length < 2 (mirroring how gradeContextGrowth handles < 2 entries). A test for the single-call case would help nail down the contract.

@copilot please address this.

Comment thread pkg/workflow/compiler_yaml_graders.go Outdated
execB64 := base64.StdEncoding.EncodeToString(execJSON)

// Escape single quotes for embedding in the YAML script block
escapedManifest := strings.ReplaceAll(string(manifestJSON), "'", "\\'")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] escapedManifest uses strings.ReplaceAll(manifestJSON, "'", "\\'") to embed the manifest JSON directly in the generated YAML script: block as a single-quoted JS argument. JSON values can contain characters like backticks, ${}, or \n that won't be escaped by this single-quote substitution, potentially producing invalid or injectable YAML/JS at compile time.

💡 Suggestion

The exec spec already uses base64 encoding for safety. Apply the same pattern to the manifest — pass manifestB64 to main() and Buffer.from(arg, 'base64').toString() in the JS side — eliminating the need to trust that the manifest JSON round-trips safely through a YAML block scalar.

@copilot please address this.

Comment thread pkg/workflow/graders_config_test.go Outdated
},
}
entries := buildGraderManifest(cfg)
if len(entries) != 3 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The TestBuildGraderManifest test asserts len(entries) != 3 but buildGraderManifest returns a *graderManifest struct — the test is comparing len(entries) where entries is the whole struct, not its .Graders slice. It should be len(entries.Graders).

💡 Fix
manifest := buildGraderManifest(cfg)
if len(manifest.Graders) != 3 {
    t.Fatalf("expected 3 entries, got %d", len(manifest.Graders))
}
// and for JSON round-trip:
data, err := json.Marshal(manifest.Graders)

As written the test will always pass because len on a struct pointer is a compile error — check whether this compiles and runs today.

@copilot please address this.

Comment thread pkg/workflow/compiler_yaml_graders.go Outdated
}

// sortStrings sorts a string slice in place.
func sortStrings(s []string) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] sortStrings in compiler_yaml_graders.go is a hand-rolled O(n2) insertion sort. The standard library sort.Strings is already imported in graders_config.go in the same package and does the same job faster and more readably.

💡 Suggestion

Replace the 9-line bubble sort with:

import "sort"
// ...
sort.Strings(disabledIDs)

@copilot please address this.

if len(s) > 4096 {
return fmt.Errorf("graders.%s.script exceeds maximum length of 4096 characters (%d)", id, len(s))
}
forbiddenPatterns := []string{"require(", "import(", "import ", "fetch(", "eval(", "process.exit", "child_process", "execSync", "spawnSync", "Function("}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/developer-security] The forbidden-pattern list in parseGraderEntryFields uses simple strings.Contains substring matching. Patterns like "require(" would miss require ( "fs" ) (space before paren) or Unicode look-alike characters. At compile time this is a defence-in-depth check (the VM sandbox provides the real enforcement), but misleading error messages ("script contains forbidden pattern") could confuse users who hit a false negative.

💡 Suggestion

Add a comment making the defence-in-depth nature explicit, and consider compiling a single regexp for the patterns so they can be anchored word-boundaries (e.g. \brequire\s*\() to reduce both false negatives and false positives. Alternatively, document that the VM sandbox is the authoritative enforcement layer.

@copilot please address this.

expect(gradeToolFailureCount(trace)).toBe(0);
});

it("handles nested malicious JSON in JSONL", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The hostile data test for proto-pollution (__proto__) asserts the parsed object exists and Object.prototype is unpolluted, but JSON.parse in modern Node already ignores __proto__ keys — the test passes vacuously and doesn't actually validate that the safeParseJsonl layer adds any additional protection. A more meaningful test would use Object.create(null) or check for the constructor key attack vector.

💡 Suggestion

Either remove the test (it's testing V8's built-in behaviour, not gh-aw code) or replace it with a test for a pattern your code does guard against — e.g. {"constructor":{"prototype":{"polluted":true}}} — and assert Object.prototype.polluted remains undefined.

@copilot please address this.

function gradeExecutionDuration(trace) { return trace.totalDurationMs; }

/** @param {PreprocessedTrace} trace @returns {number} */
function gradeContextGrowth(trace) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] gradeContextGrowth adds input_tokens + output_tokens from the first token-usage entry as the denominator — but that includes the first response's output tokens, which aren't part of the initial context. A context-growth metric is typically lastInputTokens / firstInputTokens. The current formula mixes apples and oranges and may underreport growth for long sessions where output is large.

💡 Suggestion

Consider using only input_tokens for both numerator and denominator, or document the current definition explicitly so callers understand what "context growth" means in this metric. A test that shows growth when input tokens increase across steps (not just total tokens) would help verify the intent.

@copilot please address this.

break
}
}
if hasAnyBuiltin {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] parseGradersFromFrontmatter auto-fills missing built-ins when hasAnyBuiltin is true (lines 253–260), but this logic is silent — the caller gets more graders than they declared without any log or documented behaviour. This is surprising: a user who writes graders:\n retries:\n threshold: 5 will silently get all 9 built-ins, not just retries.

💡 Suggestion

Document this "partial selection enables all built-ins" behaviour in the function comment and in docs/src/content/docs/reference/trace-graders.md. Alternatively, consider only enabling the explicitly listed graders (zero-config {} for all; explicit list for subset) to match the principle of least surprise. If the current behaviour is intentional, add a test that asserts listing one built-in produces all built-ins.

@copilot please address this.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds opt-in deterministic trace grading to compiled agent workflows.

Changes:

  • Parses grader configuration and generates post-agent grading steps.
  • Adds built-in/custom graders, result persistence, redaction, and detection staging.
  • Adds Go/JavaScript tests and reference documentation.
Show a summary per file
File Description
pkg/workflow/workflow_data.go Stores grader configuration.
pkg/workflow/graders_config.go Defines and parses graders.
pkg/workflow/graders_config_test.go Tests grader compilation behavior.
pkg/workflow/frontmatter_types.go Adds the frontmatter field.
pkg/workflow/compiler_yaml_post_agent.go Integrates post-agent grading.
pkg/workflow/compiler_yaml_graders.go Generates grader and redaction steps.
pkg/workflow/compiler_yaml_artifacts.go Adds grader fallback artifacts.
pkg/workflow/compiler_orchestrator_workflow.go Extracts grader configuration.
pkg/constants/job_constants.go Defines grader paths and filenames.
docs/src/content/docs/reference/trace-graders.md Documents trace graders.
actions/setup/sh/prepare_threat_detection_files.sh Stages grader files for detection.
actions/setup/js/trace_graders.test.cjs Tests grading behavior and isolation.
actions/setup/js/trace_graders.cjs Implements trace preprocessing and grading.
actions/setup/js/redact_secrets.cjs Adds targeted grader-result redaction.
.github/workflows/weekly-network-domains-audit.lock.yml Regenerates workflow runtime settings.

Review details

  • Files reviewed: 16/16 changed files
  • Comments generated: 12
  • Review effort level: Balanced

Comment on lines +444 to +447
// Graders configures deterministic trace graders that compute metrics from
// post-agent trace files. Can be {} for zero-config (all built-ins) or a map
// of grader IDs with optional enabled/script overrides.
Graders any `json:"graders,omitempty"`
Comment on lines +223 to +226
if entryRaw == nil {
cfg.Graders[id] = def
continue
}
Comment thread pkg/workflow/compiler_yaml_graders.go Outdated
Comment on lines +53 to +56
yaml.WriteString(" const { setupGlobals } = require('" + SetupActionDestination + "/setup_globals.cjs');\n")
yaml.WriteString(" setupGlobals(core, github, context, exec, io, getOctokit);\n")
yaml.WriteString(" const { main } = require('" + SetupActionDestination + "/trace_graders.cjs');\n")
fmt.Fprintf(yaml, " await main('%s', '%s');\n", escapedManifest, execB64)
Comment thread actions/setup/js/trace_graders.cjs Outdated
Comment on lines +478 to +479
const fn = vm.runInContext(wrappedScript, ctx, { timeout: SCRIPT_TIMEOUT_MS, filename: `grader:${id}` });
const rawResult = fn(frozenTrace, runCtx, workflowCtx, config, helpers);
Comment thread actions/setup/js/trace_graders.cjs Outdated
Comment on lines +457 to +461
const sandbox = {
Math: Object.freeze({ ...Math, random: undefined }),
JSON: Object.freeze({ parse: JSON.parse, stringify: JSON.stringify }),
Array,
Object,
Comment thread docs/src/content/docs/reference/trace-graders.md Outdated
script: "trace.toolCalls.filter(t => t.name === 'bash').length"
```

Custom scripts must be pure expressions (≤2 KB, no `require`, `import`, `fetch`, `eval`, or `process.exit`).
Comment on lines +226 to +242
entries := buildGraderManifest(cfg)
if len(entries) != 3 {
t.Fatalf("expected 3 entries, got %d", len(entries))
}

// Verify JSON serialization round-trips
data, err := json.Marshal(entries)
if err != nil {
t.Fatalf("json marshal error: %v", err)
}
var decoded []graderManifestEntry
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("json unmarshal error: %v", err)
}
if len(decoded) != 3 {
t.Fatalf("expected 3 decoded entries, got %d", len(decoded))
}
Comment on lines +210 to +214
for id, entryRaw := range m {
id = strings.TrimSpace(id)
if !graderIDPattern.MatchString(id) {
return nil, fmt.Errorf("graders has invalid id %q: must match %s. Example:\ngraders:\n my-metric:\n script: \"return { value: trace.toolCalls.length }\"", id, graderIDPattern.String())
}
Comment on lines +636 to +641
const errResults = results.filter(r => r.error);
if (errResults.length > 0) {
const errLines = errResults.map(r => `- **${r.id}**: ${r.error}`).join("\n");
core.summary.addDetails("Grader Errors", errLines);
}
await core.summary.write({ overwrite: false });

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good structural work — the single preprocessing pass, base64-encoded exec spec for custom scripts, and deepFreeze/deepClone sandbox inputs are well-considered. A few blocking issues need addressing before merge.

Blocking issues:

  1. vm sandbox exposes Object/Array — prototype-chain traversal can escape the sandbox; remove or document. (see inline)
  2. Incomplete backslash escaping of manifest JSON — grader names/descriptions containing \ before ' will produce broken JavaScript at runtime. (see inline)

Non-blocking issues:
3. runCtx.graderCount is always 0 — misleading comment, never populated. (see inline)
4. sortStrings reimplements sort.Strings with O(n2) — use stdlib. (see inline)
5. trajectory-efficiency naming is misleading — measures diversity, not efficiency. (see inline)

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 90.1 AIC · ⌖ 10.6 AIC · ⊞ 6.2K

fs.mkdirSync(GRADERS_DIR, { recursive: true });
fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
} catch (err) {
core.warning(`Graders: failed to write manifest: ${getErrorMessage(err)}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: Object in the vm sandbox allows prototype-chain escape

Exposing bare Object (and Array) in a vm.createContext sandbox lets a script walk the prototype chain to reach outer-context constructors:

// inside grader script
const fn = Object.getPrototypeOf(Math).constructor;
const proc = fn("return process")();
// proc is the real Node.js process object

The codeGeneration: { strings: false, wasm: false } option prevents new Function/eval from string literals, but not prototype-chain traversal. The forbidden-patterns block in Go (Function(, eval(, ...) doesn't block this vector.

Fix: Remove Object and Array from the sandbox — grader scripts don't need them since the frozen trace argument is already an array/object. If they must be kept, document that the sandbox is defence-in-depth only and that inline grader authors are trusted the same as workflow authors with secrets access.

@copilot please address this.

* Main entry point. Called from the github-script step with manifest JSON and base64 exec spec.
* @param {string} manifestJson - JSON string of grader manifest
* @param {string} [execSpecB64] - Base64-encoded JSON array of {id, script}
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: runCtx.graderCount is always 0 — comment says "filled by caller" but it never is

const runCtx = deepFreeze({
  graderCount: 0, // filled by caller  ← never populated
});

runCtx is created locally in runCustomGrader, frozen immediately, and never updated. Any grader script relying on run.graderCount will always see 0. Either populate it from the actual enabled-grader count before freezing, or remove the field to avoid setting false expectations.

@copilot please address this.

escapedManifest := strings.ReplaceAll(string(manifestJSON), "'", "\\'")

yaml.WriteString(" - name: Run trace graders\n")
yaml.WriteString(" if: always()\n")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: manifest escaping is incomplete — backslash before single-quote breaks the JS string literal

escapedManifest := strings.ReplaceAll(string(manifestJSON), "'", "\\'")
fmt.Fprintf(yaml, "            await main('%s', '%s');\n", escapedManifest, execB64)

JSON can contain \\ (an escaped backslash in a string value). After only escaping ', the sequence \\' in the original JSON becomes \\\' — the JavaScript parser sees an escaped backslash (\\) followed by an unescaped single quote that terminates the string literal, causing a syntax error or silent truncation.

Fix: escape backslashes first, then single quotes:

escaped := strings.ReplaceAll(string(manifestJSON), `\`, `\\`)
escaped  = strings.ReplaceAll(escaped, "'", `\'`)

@copilot please address this.

}
fmt.Fprintf(yaml, " GH_AW_SECRET_NAMES: '%s'\n", strings.Join(escapedRefs, ","))
for _, secretName := range secretReferences {
escapedSecretName := escapeSingleQuoteBackslash(secretName)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintainability: sortStrings reimplements sort.Strings with an O(n2) selection sort

// sortStrings sorts a string slice in place.
func sortStrings(s []string) {
    for i := 0; i < len(s); i++ {
        for j := i + 1; j < len(s); j++ {
            if s[j] < s[i] {
                s[i], s[j] = s[j], s[i]

sort is already imported in graders_config.go (same package). Drop this helper and call sort.Strings(disabledIDs) directly to use the stdlib's O(n log n) implementation and avoid dead code.

@copilot please address this.

* @param {{name: string, unit: string, direction: string, threshold?: number, source: string, digest?: string}} meta
* @returns {GraderResult}
*/
function normalizeResult(id, rawResult, meta) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design: trajectory-efficiency measures tool diversity, not efficiency — the naming is misleading

function gradeTrajectoryEfficiency(trace) {
  if (trace.toolCalls.length === 0) return 1;
  const uniqueTools = new Set(trace.toolCalls.map(t => String(t.name || t.tool || "")));
  return Math.min(1, uniqueTools.size / trace.toolCalls.length);
}

This computes unique tool types ÷ total calls. An agent that calls 10 different tools once each scores 1.0; an agent that uses one tool efficiently 10 times scores 0.1. "Efficiency" normally implies fewer steps or less waste — diversity of tool types used is orthogonal to that.

The description ("higher = more diverse usage") partially clarifies it, but the ID trajectory-efficiency and name "Trajectory Efficiency" will cause users to misread results and set wrong thresholds.

Consider renaming to tool-diversity / "Tool Diversity", or at minimum updating the description to explicitly say "measures breadth of tool usage, not operational efficiency".

@copilot please address this.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot this PR still needs forward progress before maintainer review.

  • Blocking reviews are asking for fixes to the trace grader sandboxing and manifest escaping/runtime safety.
  • The ADR gate still requires the drafted ADR to be completed and linked in the PR body.
  • After addressing the blocking review items, refresh the branch if needed, run the pr-finisher skill, and leave a concise maintainer handoff with exact validations rerun and any remaining human follow-up.

Run: https://github.com/github/gh-aw/actions/runs/32544819956

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 19.1 AIC · ⌖ 8.22 AIC · ⊞ 9.5K ·
Comment /souschef to run again

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

@copilot this PR still needs forward progress before maintainer review.

  • Blocking reviews are asking for fixes to the trace grader sandboxing and manifest escaping/runtime safety....

Addressed in 0d6a86e.

Completed the blocking runtime-safety work:

  • moved custom grader execution to an isolated worker subprocess with bounded timeout and restricted globals
  • switched manifest transport to base64 to avoid interpolation/string-breakout risks
  • updated grader compiler/tests and finalized ADR-54678 content in-repo

Validations rerun:

  • npm run test:js -- trace_graders.test.cjs (pass)
  • go test ./pkg/workflow -run 'Test(ParseGradersFromFrontmatter_.*|BuildGraderManifest|GenerateGradersStep_.*|CollectGraderArtifactPaths.*)' (pass)
  • make lint (pass)
  • make test-unit (pass)
  • make test still fails in pkg/cli with pre-existing fatal error: concurrent map writes in shell completion tests (not caused by this change)

Remaining human follow-up:

  • update PR body to link ADR-54678 for ADR gate satisfaction
  • re-trigger CI on the new HEAD (agent pushes do not trigger workflows)

Copilot AI requested a review from gh-aw-bot August 22, 2026 02:43
@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot the grader runs in GitHub Actions, so it should be written in JavaScript and run in a github-script action step. It does not run as part of the Go CLI.

@github-actions

Copy link
Copy Markdown
Contributor

PR Triage

  • Category: feature
  • Risk: high
  • Priority score: 58/100 (impact 30, urgency 20, quality 8)
  • Recommended action: fast_track
  • CI status: failing

Generated by 🔧 PR Triage Agent · auto · 82.9 AIC · ⌖ 6.65 AIC · ⊞ 8.3K ·

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@-

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 7.36 AIC · ⌖ 8.18 AIC · ⊞ 9.5K ·
Comment /souschef to run again

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot this PR still needs forward progress before maintainer review.

  • The ADR gate still requires the drafted ADR to be linked in the PR body.
  • Please refresh the branch if needed, confirm the current HEAD still reflects the latest maintainer requests on graders/experiments, rerun the exact validations needed for the latest follow-up, and leave a concise maintainer-ready handoff.
  • Then run the pr-finisher skill before handing back.

Run: https://github.com/github/gh-aw/actions/runs/32564450205

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 18.6 AIC · ⌖ 8.24 AIC · ⊞ 9.5K ·
Comment /souschef to run again

Copilot AI commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

@copilot this PR still needs forward progress before maintainer review.

  • The ADR gate still requires the drafted ADR to be linked in the PR body....

Verified on current HEAD 1781737:

  • latest maintainer requests are still present (graders experimental warning/schema/docs and experiment metric grader references grader:<id> / graders.<id>)
  • branch is refreshed and up to date with origin/main (0 behind)

Validations rerun:

  • cd /home/runner/work/gh-aw/gh-aw/actions/setup/js && npm run test:js -- trace_graders.test.cjs experiment_helpers.test.cjs run_evals.test.cjs
  • cd /home/runner/work/gh-aw/gh-aw/actions/setup/js && npm run typecheck
  • cd /home/runner/work/gh-aw/gh-aw && go test ./pkg/workflow -run 'TestGradersExperimentalWarning|TestParseExperimentMetricGraderReference'
  • cd /home/runner/work/gh-aw/gh-aw && make fmt && make lint && make test-unit && make test && make recompile

CI log pass:

  • latest referenced run 32564450205: no failed jobs
  • latest failed branch runs reviewed: failures were merge-base detection in impacted tests and pre-existing custom lint policy findings in unrelated files; local validation on refreshed checkout is green

PR-finisher pass complete for this run.
Remaining human follow-up: add ADR link in PR body to /docs/adr/54678-deterministic-trace-grading-framework.md, then re-trigger CI for fresh required-check signals.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot failed checks: impacted-go-tests: https://github.com/github/gh-aw/actions/runs/32558583439/job/97003175606; impacted-js-tests: https://github.com/github/gh-aw/actions/runs/32558583397/job/97003173010; lint-go-custom: https://github.com/github/gh-aw/actions/runs/32558583439/job/97003175641 ; please refresh the branch if needed and continue with the pr-finisher skill

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 12.5 AIC · ⌖ 8.2 AIC · ⊞ 7K ·
Comment /souschef to run again

Copilot AI commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

@copilot failed checks: impacted-go-tests: https://github.com/github/gh-aw/actions/runs/32558583439/job/97003175606; impacted-js-tests: https://github.com/github/gh-aw/actions/runs/32558583397/job/970...

I refreshed and revalidated on current HEAD 1781737.

  • origin/main...HEAD is 0 14 (not behind).
  • Investigated the linked failed jobs:
    • impacted-go-tests and impacted-js-tests failed with unable to determine merge-base from BASE_REF=origin/main.
    • lint-go-custom failed in golint-custom on existing pkg/cli/trial_* messaging rules.
  • Local repro/validation:
    • make test-impacted-go BASE_REF=origin/main CI_COVERAGE_SOURCE_BRANCH=main
    • make test-impacted-js BASE_REF=origin/main
    • make fmt && make lint && make test-unit && make test && make recompile

No code changes were required in this pass. Remaining maintainer follow-up: link ADR /docs/adr/54678-deterministic-trace-grading-framework.md in the PR body and re-trigger CI for fresh required-check signals.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot this PR still needs forward progress before maintainer review.

Run: https://github.com/github/gh-aw/actions/runs/32569036201

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 28.6 AIC · ⌖ 8.36 AIC · ⊞ 9.5K ·
Comment /souschef to run again

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

@copilot this PR still needs forward progress before maintainer review.

  • Blocking review feedback is still open on the trace-grader sandbox and config contract. Please address the sandbox escape / e...

Implemented in 2906c4a.

  • Addressed sandbox escape/embedding concerns in /home/runner/work/gh-aw/gh-aw/actions/setup/js/trace_graders_worker.cjs by removing direct script interpolation into a wrapper template and switching to vm.compileFunction + timed invocation with locked runtime bindings.
  • Kept deterministic math behavior without mutating process-global Math.
  • Aligned script-size contract by enforcing 4096 characters in /home/runner/work/gh-aw/gh-aw/pkg/workflow/graders_config.go and updating docs in /home/runner/work/gh-aw/gh-aw/docs/src/content/docs/reference/trace-graders.md (including executable return example).
  • Added targeted test coverage in:
    • /home/runner/work/gh-aw/gh-aw/actions/setup/js/trace_graders.test.cjs
    • /home/runner/work/gh-aw/gh-aw/pkg/workflow/graders_config_test.go

Validation rerun:

  • make test-impacted-go BASE_REF=origin/main CI_COVERAGE_SOURCE_BRANCH=main
  • make test-impacted-js BASE_REF=origin/main
  • cd /home/runner/work/gh-aw/gh-aw/actions/setup/js && npm run test:js -- trace_graders.test.cjs
  • cd /home/runner/work/gh-aw/gh-aw/actions/setup/js && npm run typecheck
  • go test ./pkg/workflow -run 'TestParseGradersFromFrontmatter_.*|TestBuildGraderManifest|TestGenerateGradersStep_.*|TestCollectGraderArtifactPaths.*|TestGradersExperimentalWarning|TestParseExperimentMetricGraderReference'
  • make fmt && make lint && make test-unit && make test && make recompile

Branch status: refreshed and up to date with base (origin/main...HEAD = 0 15).

lint-go-custom remains failing on pre-existing repository-wide pkg/cli/trial_* policy violations unrelated to this grader change set.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot sous-chef triage: please review this PR, refresh the branch if needed, address any unresolved review feedback, and use the pr-finisher skill before handing back for maintainer review.

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 3.89 AIC · ⌖ 8.31 AIC · ⊞ 7K ·
Comment /souschef to run again

Copilot AI commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

@copilot sous-chef triage: please review this PR, refresh the branch if needed, address any unresolved review feedback, and use the pr-finisher skill before handing back for maintainer review.

Gene...

PR-finisher pass completed on current HEAD 2906c4a.

  • Branch refresh: up to date with base (origin/main...HEAD = 0 15), no merge needed.
  • Unresolved feedback review: no new code changes were required beyond existing fixes on this HEAD for the grader sandbox/embedding and script-size contract.
  • CI triage (logs reviewed):
    • impacted-go-tests / impacted-js-tests failures were merge-base detection (unable to determine merge-base from BASE_REF=origin/main) and are reproducible as infra/context, not product regressions.
    • lint-go-custom failures are repository-wide policy findings in unrelated files (pkg/cli/*, cmd/gh-aw/main.go, pkg/linters/*).

Validation rerun:

  • make test-impacted-go BASE_REF=origin/main CI_COVERAGE_SOURCE_BRANCH=main
  • make test-impacted-js BASE_REF=origin/main
  • make fmt && make lint && make test-unit && make test && make recompilemake test still fails in pre-existing pkg/cli completion tests (write |1: file already closed), unrelated to grader paths.

Maintainer handoff: CI on this HEAD remains stale after agent operations; please re-trigger workflows for fresh required-check signals. ADR link is still needed in PR body: /docs/adr/54678-deterministic-trace-grading-framework.md.

@github-actions

Copy link
Copy Markdown
Contributor

PR Triage

Category: feature | Risk: high | Priority: medium | Score: 65/100
Recommended action: fast_track

New deterministic trace grading framework, 3157 additions/27 files. CI green but 2x CHANGES_REQUESTED unresolved — needs author follow-up.

Automated triage — run 32572524009

Generated by 🔧 PR Triage Agent · auto · 65.4 AIC · ⌖ 6.04 AIC · ⊞ 8.3K ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants