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
2 changes: 1 addition & 1 deletion .github/workflows/pr-sous-chef.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/pr-sous-chef.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ When this workflow is triggered by the `/souschef` slash command on a PR comment
1. Read `/tmp/gh-aw/agent/pr-sous-chef-candidates-compact.json` first.
2. If `prs` is empty, create the run-report issue (see **Run summary** below) and stop. If `create_issue` is unavailable, fall back to `noop` with the message `"processed=0; nudged=0; no eligible PRs"` and stop.
3. Process PRs in `updatedAt` descending order.
4. Process at most **5 PRs** per run.
4. Process all eligible PRs per run.
5. Use the `pr-processor` sub-agent for each PR; pass only the PR number and compact context.
6. If a `pr-processor` call returns non-JSON or an error, record `{pr_number: <N>, skip_reason: "sub_agent_error"}` in the `skipped` array of the run-summary issue payload and move to the next PR without retrying.
7. Do not fetch full PR diffs or large file lists unless absolutely required for a skip decision.
Expand Down
7 changes: 7 additions & 0 deletions pkg/cli/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,13 @@ func processedRunFromSummary(summary *RunSummary, runOutputDir string) Processed
GitHubRateLimitUsage: summary.GitHubRateLimitUsage,
JobDetails: summary.JobDetails,
}
// Run.Turns may be zero on cached-summary paths where the RunSummary was
// serialised before the run completed. Metrics.Turns is populated from log
// parsing and is authoritative; backfill here so that audit comparison deltas
// are computed from an accurate value.
if processedRun.Run.Turns == 0 && summary.Metrics.Turns > 0 {

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.

[/diagnosing-bugs] The fix is correct, but the why is missing — future readers will not know that Run.Turns can be stale/zero specifically in the cached-summary rendering path.

💡 Suggested comment
// Run.Turns may be zero on cached-summary paths where the RunSummary was
// serialised before the run completed. Metrics.Turns is populated from log
// parsing and is authoritative; backfill here so that audit comparison deltas
// are computed from an accurate value.
if processedRun.Run.Turns == 0 && summary.Metrics.Turns > 0 {
    processedRun.Run.Turns = summary.Metrics.Turns
}

Without this context, the next developer may remove it as apparently defensive dead code.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added the explanatory comment in 3d0db57.

processedRun.Run.Turns = summary.Metrics.Turns
}
processedRun.Run.LogsPath = runOutputDir
return processedRun
}
Expand Down
8 changes: 8 additions & 0 deletions pkg/cli/audit_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -515,15 +515,23 @@ func computeRunMetricsDiff(summary1, summary2 *RunSummary) *RunMetricsDiff {
if summary1 != nil {
run1Tokens = summary1.Run.TokenUsage
run1Duration = summary1.Run.Duration
// Run.Turns may be zero on cached-summary paths; Metrics.Turns is authoritative.
run1Turns = summary1.Run.Turns
if run1Turns == 0 && summary1.Metrics.Turns > 0 {
run1Turns = summary1.Metrics.Turns
}
tu1 = summary1.TokenUsage
rl1 = summary1.GitHubRateLimitUsage
m1 = &summary1.Metrics
}
if summary2 != nil {
run2Tokens = summary2.Run.TokenUsage
run2Duration = summary2.Run.Duration
// Run.Turns may be zero on cached-summary paths; Metrics.Turns is authoritative.
run2Turns = summary2.Run.Turns
if run2Turns == 0 && summary2.Metrics.Turns > 0 {
run2Turns = summary2.Metrics.Turns
}
tu2 = summary2.TokenUsage
rl2 = summary2.GitHubRateLimitUsage
m2 = &summary2.Metrics
Expand Down
34 changes: 34 additions & 0 deletions pkg/cli/audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,40 @@ func TestIsPermissionErrorStr(t *testing.T) {
}
}

func TestProcessedRunFromSummaryBackfillsTurnsFromMetrics(t *testing.T) {

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] Consider also testing the edge case where both Run.Turns and Metrics.Turns are zero — the current fix silently leaves turns as 0 in that scenario, which would still produce a false turns_decrease in comparison. A dedicated test makes the invariant explicit.

💡 Suggested test
func TestProcessedRunFromSummaryBothTurnsZero(t *testing.T) {
    summary := &RunSummary{
        Run:     WorkflowRun{DatabaseID: 789, Turns: 0},
        Metrics: LogMetrics{Turns: 0},
    }

    processed := processedRunFromSummary(summary, "/tmp/run-output")

    // Both are zero — no backfill occurs, and turns should remain 0.
    assert.Equal(t, 0, processed.Run.Turns, "turns should remain zero when neither Run.Turns nor Metrics.Turns is available")
}

Documents the known limitation: if a run genuinely has zero turns and Metrics.Turns is also zero, the delta will still reflect 0.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added TestProcessedRunFromSummaryBothTurnsZero in 3d0db57.

summary := &RunSummary{
Run: WorkflowRun{DatabaseID: 123, Turns: 0},
Metrics: LogMetrics{Turns: 34},
}

processed := processedRunFromSummary(summary, "/tmp/run-output")

assert.Equal(t, 34, processed.Run.Turns, "run turns should backfill from summary metrics when run turns are missing")
assert.Equal(t, "/tmp/run-output", processed.Run.LogsPath, "logs path should be set from the current run output directory")
}

func TestProcessedRunFromSummaryPreservesExistingTurns(t *testing.T) {
summary := &RunSummary{
Run: WorkflowRun{DatabaseID: 456, Turns: 7},
Metrics: LogMetrics{Turns: 34},
}

processed := processedRunFromSummary(summary, "/tmp/run-output")

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.

Missing test for the both-zeros edge case (Run.Turns == 0, Metrics.Turns == 0). The two new tests cover backfill (zero→non-zero) and preservation (non-zero→non-zero). Neither exercises the path where Metrics.Turns == 0 too — meaning the guard summary.Metrics.Turns > 0 is never exercised as false. A future regression that drops the > 0 check and unconditionally writes summary.Metrics.Turns back to processedRun.Run.Turns would not be caught by the current suite.

💡 Suggested test
func TestProcessedRunFromSummaryBothTurnsZero(t *testing.T) {
    summary := &RunSummary{
        Run:     WorkflowRun{DatabaseID: 789, Turns: 0},
        Metrics: LogMetrics{Turns: 0},
    }

    processed := processedRunFromSummary(summary, "/tmp/run-output")

    // Neither field has data; turns should remain 0, not be set to Metrics.Turns.
    assert.Equal(t, 0, processed.Run.Turns, "run turns should remain 0 when both Run.Turns and Metrics.Turns are 0")
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added TestProcessedRunFromSummaryBothTurnsZero and also applied the same Run.Turns backfill from Metrics.Turns inside computeRunMetricsDiff (audit_diff.go) so audit diff is fixed for old cached summaries too — see 3d0db57.


assert.Equal(t, 7, processed.Run.Turns, "existing run turns should not be overwritten by summary metrics")
}

func TestProcessedRunFromSummaryBothTurnsZero(t *testing.T) {
summary := &RunSummary{
Run: WorkflowRun{DatabaseID: 789, Turns: 0},
Metrics: LogMetrics{Turns: 0},
}

processed := processedRunFromSummary(summary, "/tmp/run-output")

assert.Equal(t, 0, processed.Run.Turns, "run turns should remain zero when neither Run.Turns nor Metrics.Turns is available")
}

func TestBuildAuditData(t *testing.T) {
// Create test data
run := WorkflowRun{
Expand Down
Loading