@wix/pathgrade 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/pathgrade.js +2 -0
- package/dist/affected/anchor.d.ts +18 -0
- package/dist/affected/anchor.js +42 -0
- package/dist/affected/config.d.ts +25 -0
- package/dist/affected/config.js +98 -0
- package/dist/affected/format.d.ts +45 -0
- package/dist/affected/format.js +87 -0
- package/dist/affected/git.d.ts +33 -0
- package/dist/affected/git.js +68 -0
- package/dist/affected/glob.d.ts +17 -0
- package/dist/affected/glob.js +25 -0
- package/dist/affected/meta.d.ts +26 -0
- package/dist/affected/meta.js +158 -0
- package/dist/affected/select.d.ts +30 -0
- package/dist/affected/select.js +101 -0
- package/dist/affected/sidecar.d.ts +32 -0
- package/dist/affected/sidecar.js +86 -0
- package/dist/affected/types.d.ts +43 -0
- package/dist/affected/types.js +7 -0
- package/dist/agents/claude/ask-user-answer-store.d.ts +28 -0
- package/dist/agents/claude/ask-user-answer-store.js +29 -0
- package/dist/agents/claude/ask-user-bridge.d.ts +56 -0
- package/dist/agents/claude/ask-user-bridge.js +162 -0
- package/dist/agents/claude/sdk-message-projector.d.ts +43 -0
- package/dist/agents/claude/sdk-message-projector.js +208 -0
- package/dist/agents/claude/sdk-options.d.ts +52 -0
- package/dist/agents/claude/sdk-options.js +79 -0
- package/dist/agents/claude.d.ts +56 -0
- package/dist/agents/claude.js +143 -0
- package/dist/agents/codex-app-server/agent.d.ts +35 -0
- package/dist/agents/codex-app-server/agent.js +388 -0
- package/dist/agents/codex-app-server/fixtures/app-server-client.d.ts +19 -0
- package/dist/agents/codex-app-server/fixtures/app-server-client.js +104 -0
- package/dist/agents/codex-app-server/fixtures/run-gate.d.ts +25 -0
- package/dist/agents/codex-app-server/fixtures/run-gate.js +48 -0
- package/dist/agents/codex-app-server/protocol/ClientRequest.d.ts +27 -0
- package/dist/agents/codex-app-server/protocol/ClientRequest.js +7 -0
- package/dist/agents/codex-app-server/protocol/DynamicToolCallParams.d.ts +9 -0
- package/dist/agents/codex-app-server/protocol/DynamicToolCallParams.js +4 -0
- package/dist/agents/codex-app-server/protocol/GrantedPermissionProfile.d.ts +6 -0
- package/dist/agents/codex-app-server/protocol/GrantedPermissionProfile.js +7 -0
- package/dist/agents/codex-app-server/protocol/McpElicitationRequestParams.d.ts +23 -0
- package/dist/agents/codex-app-server/protocol/McpElicitationRequestParams.js +8 -0
- package/dist/agents/codex-app-server/protocol/Op.d.ts +11 -0
- package/dist/agents/codex-app-server/protocol/Op.js +5 -0
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalParams.d.ts +10 -0
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalParams.js +6 -0
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalResponse.d.ts +15 -0
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalResponse.js +4 -0
- package/dist/agents/codex-app-server/protocol/SandboxMode.d.ts +1 -0
- package/dist/agents/codex-app-server/protocol/SandboxMode.js +4 -0
- package/dist/agents/codex-app-server/protocol/ServerRequest.d.ts +42 -0
- package/dist/agents/codex-app-server/protocol/ServerRequest.js +7 -0
- package/dist/agents/codex-app-server/protocol/ThreadStartParams.d.ts +45 -0
- package/dist/agents/codex-app-server/protocol/ThreadStartParams.js +10 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputAnswer.d.ts +6 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputAnswer.js +4 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputOption.d.ts +7 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputOption.js +4 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputParams.d.ts +10 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputParams.js +4 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputQuestion.d.ts +12 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputQuestion.js +4 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputResponse.d.ts +9 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputResponse.js +4 -0
- package/dist/agents/codex-app-server/protocol/TurnCompletedNotification.d.ts +5 -0
- package/dist/agents/codex-app-server/protocol/TurnCompletedNotification.js +4 -0
- package/dist/agents/codex-app-server/protocol/index.d.ts +16 -0
- package/dist/agents/codex-app-server/protocol/index.js +11 -0
- package/dist/agents/codex-app-server/transport.d.ts +84 -0
- package/dist/agents/codex-app-server/transport.js +246 -0
- package/dist/agents/codex-app-server/wire-translators.d.ts +19 -0
- package/dist/agents/codex-app-server/wire-translators.js +52 -0
- package/dist/agents/codex.d.ts +7 -0
- package/dist/agents/codex.js +164 -0
- package/dist/agents/cursor.d.ts +39 -0
- package/dist/agents/cursor.js +233 -0
- package/dist/agents/registry.d.ts +17 -0
- package/dist/agents/registry.js +27 -0
- package/dist/agents/transcript-agent.d.ts +14 -0
- package/dist/agents/transcript-agent.js +59 -0
- package/dist/analytics/engine.d.ts +18 -0
- package/dist/analytics/engine.js +66 -0
- package/dist/commands/affected.d.ts +31 -0
- package/dist/commands/affected.js +150 -0
- package/dist/commands/analyze.d.ts +21 -0
- package/dist/commands/analyze.js +167 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +242 -0
- package/dist/commands/preview-reactions.d.ts +10 -0
- package/dist/commands/preview-reactions.js +88 -0
- package/dist/commands/preview.d.ts +5 -0
- package/dist/commands/preview.js +17 -0
- package/dist/commands/report.d.ts +39 -0
- package/dist/commands/report.js +108 -0
- package/dist/commands/run-args.d.ts +22 -0
- package/dist/commands/run-args.js +69 -0
- package/dist/commands/run-changed.d.ts +26 -0
- package/dist/commands/run-changed.js +138 -0
- package/dist/commands/validate.d.ts +13 -0
- package/dist/commands/validate.js +233 -0
- package/dist/core/mcp-mock.d.ts +3 -0
- package/dist/core/mcp-mock.js +23 -0
- package/dist/core/mcp-mock.types.d.ts +15 -0
- package/dist/core/mcp-mock.types.js +1 -0
- package/dist/core/skills.d.ts +15 -0
- package/dist/core/skills.js +76 -0
- package/dist/evals/discovery.d.ts +7 -0
- package/dist/evals/discovery.js +69 -0
- package/dist/mcp-mock-server.d.ts +1 -0
- package/dist/mcp-mock-server.js +116 -0
- package/dist/pathgrade.d.ts +10 -0
- package/dist/pathgrade.js +243 -0
- package/dist/plugin/index.d.ts +9 -0
- package/dist/plugin/index.js +81 -0
- package/dist/plugin/lifecycle.d.ts +45 -0
- package/dist/plugin/lifecycle.js +146 -0
- package/dist/plugin/reporter.d.ts +36 -0
- package/dist/plugin/reporter.js +275 -0
- package/dist/plugin/setup.d.ts +1 -0
- package/dist/plugin/setup.js +3 -0
- package/dist/providers/copy-filter.d.ts +18 -0
- package/dist/providers/copy-filter.js +66 -0
- package/dist/providers/credentials.d.ts +36 -0
- package/dist/providers/credentials.js +202 -0
- package/dist/providers/mcp-config.d.ts +34 -0
- package/dist/providers/mcp-config.js +55 -0
- package/dist/providers/sandbox-exec.d.ts +8 -0
- package/dist/providers/sandbox-exec.js +82 -0
- package/dist/providers/sandbox.d.ts +22 -0
- package/dist/providers/sandbox.js +158 -0
- package/dist/providers/sandboxed-claude-spawn.d.ts +24 -0
- package/dist/providers/sandboxed-claude-spawn.js +57 -0
- package/dist/providers/workspace.d.ts +15 -0
- package/dist/providers/workspace.js +73 -0
- package/dist/reporters/browser.d.ts +2 -0
- package/dist/reporters/browser.js +85 -0
- package/dist/reporters/cli.d.ts +2 -0
- package/dist/reporters/cli.js +131 -0
- package/dist/reporters/diagnostics.d.ts +46 -0
- package/dist/reporters/diagnostics.js +147 -0
- package/dist/reporters/github-comment.d.ts +67 -0
- package/dist/reporters/github-comment.js +252 -0
- package/dist/reporters/loader.d.ts +8 -0
- package/dist/reporters/loader.js +36 -0
- package/dist/reporters/results-path.d.ts +2 -0
- package/dist/reporters/results-path.js +10 -0
- package/dist/reporters/verbose-emitter.d.ts +69 -0
- package/dist/reporters/verbose-emitter.js +103 -0
- package/dist/sdk/agent-crash.d.ts +19 -0
- package/dist/sdk/agent-crash.js +20 -0
- package/dist/sdk/agent-resolution.d.ts +12 -0
- package/dist/sdk/agent-resolution.js +21 -0
- package/dist/sdk/agent-result-log.d.ts +19 -0
- package/dist/sdk/agent-result-log.js +45 -0
- package/dist/sdk/agent.d.ts +4 -0
- package/dist/sdk/agent.js +354 -0
- package/dist/sdk/ask-bus/bus.d.ts +21 -0
- package/dist/sdk/ask-bus/bus.js +129 -0
- package/dist/sdk/ask-bus/handler.d.ts +31 -0
- package/dist/sdk/ask-bus/handler.js +244 -0
- package/dist/sdk/ask-bus/parsers.d.ts +9 -0
- package/dist/sdk/ask-bus/parsers.js +30 -0
- package/dist/sdk/ask-bus/projection.d.ts +26 -0
- package/dist/sdk/ask-bus/projection.js +45 -0
- package/dist/sdk/ask-bus/types.d.ts +69 -0
- package/dist/sdk/ask-bus/types.js +1 -0
- package/dist/sdk/chat.d.ts +29 -0
- package/dist/sdk/chat.js +93 -0
- package/dist/sdk/conversation-window.d.ts +14 -0
- package/dist/sdk/conversation-window.js +38 -0
- package/dist/sdk/converse.d.ts +42 -0
- package/dist/sdk/converse.js +329 -0
- package/dist/sdk/eval-runtime.d.ts +10 -0
- package/dist/sdk/eval-runtime.js +17 -0
- package/dist/sdk/evaluate.d.ts +13 -0
- package/dist/sdk/evaluate.js +325 -0
- package/dist/sdk/index.d.ts +31 -0
- package/dist/sdk/index.js +20 -0
- package/dist/sdk/judge-pipeline.d.ts +7 -0
- package/dist/sdk/judge-pipeline.js +178 -0
- package/dist/sdk/judge-prompt-builder.d.ts +7 -0
- package/dist/sdk/judge-prompt-builder.js +76 -0
- package/dist/sdk/judge-tool-runner.d.ts +19 -0
- package/dist/sdk/judge-tool-runner.js +51 -0
- package/dist/sdk/judge-tool-session.d.ts +47 -0
- package/dist/sdk/judge-tool-session.js +213 -0
- package/dist/sdk/judge-tools.d.ts +29 -0
- package/dist/sdk/judge-tools.js +283 -0
- package/dist/sdk/managed-session.d.ts +50 -0
- package/dist/sdk/managed-session.js +110 -0
- package/dist/sdk/persona.d.ts +5 -0
- package/dist/sdk/persona.js +33 -0
- package/dist/sdk/reaction-loader.d.ts +4 -0
- package/dist/sdk/reaction-loader.js +119 -0
- package/dist/sdk/reaction-preview.d.ts +6 -0
- package/dist/sdk/reaction-preview.js +94 -0
- package/dist/sdk/run-scorer.d.ts +10 -0
- package/dist/sdk/run-scorer.js +124 -0
- package/dist/sdk/runtime-policy.d.ts +21 -0
- package/dist/sdk/runtime-policy.js +89 -0
- package/dist/sdk/scorer-utils.d.ts +6 -0
- package/dist/sdk/scorer-utils.js +39 -0
- package/dist/sdk/scorers.d.ts +64 -0
- package/dist/sdk/scorers.js +80 -0
- package/dist/sdk/snapshots.d.ts +41 -0
- package/dist/sdk/snapshots.js +148 -0
- package/dist/sdk/types.d.ts +416 -0
- package/dist/sdk/types.js +17 -0
- package/dist/sdk/visible-turn.d.ts +4 -0
- package/dist/sdk/visible-turn.js +23 -0
- package/dist/tool-events.d.ts +45 -0
- package/dist/tool-events.js +167 -0
- package/dist/types.d.ts +372 -0
- package/dist/types.js +37 -0
- package/dist/utils/cli.d.ts +55 -0
- package/dist/utils/cli.js +131 -0
- package/dist/utils/env.d.ts +8 -0
- package/dist/utils/env.js +26 -0
- package/dist/utils/llm-mocks.d.ts +43 -0
- package/dist/utils/llm-mocks.js +108 -0
- package/dist/utils/llm-providers/anthropic.d.ts +2 -0
- package/dist/utils/llm-providers/anthropic.js +132 -0
- package/dist/utils/llm-providers/cli.d.ts +20 -0
- package/dist/utils/llm-providers/cli.js +162 -0
- package/dist/utils/llm-providers/openai.d.ts +2 -0
- package/dist/utils/llm-providers/openai.js +56 -0
- package/dist/utils/llm-types.d.ts +107 -0
- package/dist/utils/llm-types.js +1 -0
- package/dist/utils/llm.d.ts +36 -0
- package/dist/utils/llm.js +209 -0
- package/dist/utils/shutdown.d.ts +15 -0
- package/dist/utils/shutdown.js +41 -0
- package/dist/utils/timeout.d.ts +8 -0
- package/dist/utils/timeout.js +32 -0
- package/dist/viewer.html +1178 -0
- package/package.json +88 -0
- package/templates/eval.ts.template +28 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { fmt, header } from '../utils/cli.js';
|
|
3
|
+
import { loadReports } from './loader.js';
|
|
4
|
+
// ─── Main ──────────────────────────────────────────────────
|
|
5
|
+
export async function runCliPreview(resultsDir, opts) {
|
|
6
|
+
const resolved = path.resolve(resultsDir);
|
|
7
|
+
let entries = await loadReports(resolved);
|
|
8
|
+
if (opts?.filter) {
|
|
9
|
+
const pattern = opts.filter.toLowerCase();
|
|
10
|
+
entries = entries.filter(e => e.task?.toLowerCase().includes(pattern));
|
|
11
|
+
}
|
|
12
|
+
if (opts?.last && opts.last > 0) {
|
|
13
|
+
entries = entries.slice(0, opts.last);
|
|
14
|
+
}
|
|
15
|
+
if (!entries.length) {
|
|
16
|
+
console.log(`\n ${fmt.dim('No reports found in')} ${resolved}\n`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
console.log(`\n${fmt.bold('pathgrade preview')} ${fmt.dim(`${entries.length} reports from ${resolved}`)}\n`);
|
|
20
|
+
for (const { file, ...report } of entries) {
|
|
21
|
+
const passRate = report.pass_rate ?? 0;
|
|
22
|
+
const isPass = passRate >= 0.5;
|
|
23
|
+
const trials = report.trials || [];
|
|
24
|
+
const avgDur = trials.reduce((s, t) => s + (t.duration_ms || 0), 0) / (trials.length || 1);
|
|
25
|
+
const totalTokens = trials.reduce((s, t) => s + (t.input_tokens || 0) + (t.output_tokens || 0) + (t.conversation_input_tokens || 0) + (t.conversation_output_tokens || 0), 0);
|
|
26
|
+
// ── Report header
|
|
27
|
+
const status = isPass ? fmt.pass('PASS') : fmt.fail('FAIL');
|
|
28
|
+
header(`${status} ${report.task}`);
|
|
29
|
+
// Timestamp from filename
|
|
30
|
+
const ts = file.match(/\d{4}-\d{2}-\d{2}T[\d-]+/)?.[0]?.replace(/-(\d{2})-(\d{2})-/g, ':$1:$2:') || '';
|
|
31
|
+
if (ts)
|
|
32
|
+
console.log(` ${fmt.dim(ts)}`);
|
|
33
|
+
console.log();
|
|
34
|
+
// ── Summary metrics
|
|
35
|
+
const metrics = [
|
|
36
|
+
['Pass Rate', `${(passRate * 100).toFixed(1)}%`],
|
|
37
|
+
['pass@k', report.pass_at_k != null ? `${(report.pass_at_k * 100).toFixed(1)}%` : '—'],
|
|
38
|
+
['pass^k', report.pass_pow_k != null ? `${(report.pass_pow_k * 100).toFixed(1)}%` : '—'],
|
|
39
|
+
['Avg Duration', `${(avgDur / 1000).toFixed(1)}s`],
|
|
40
|
+
['Total Tokens', `~${totalTokens}`],
|
|
41
|
+
['Skills', report.skills_used?.join(', ') || 'none'],
|
|
42
|
+
];
|
|
43
|
+
for (const [label, value] of metrics) {
|
|
44
|
+
console.log(` ${fmt.dim(label.padEnd(14))} ${fmt.bold(value)}`);
|
|
45
|
+
}
|
|
46
|
+
console.log();
|
|
47
|
+
// ── Trials
|
|
48
|
+
for (const trial of trials) {
|
|
49
|
+
const tp = trial.reward >= 0.5;
|
|
50
|
+
const trialStatus = tp ? fmt.pass('PASS') : fmt.fail('FAIL');
|
|
51
|
+
const reward = fmt.bold(trial.reward.toFixed(2));
|
|
52
|
+
const dur = `${((trial.duration_ms || 0) / 1000).toFixed(1)}s`;
|
|
53
|
+
const cmds = `${trial.n_commands || 0} cmds`;
|
|
54
|
+
const scorers = (trial.scorer_results || []).map((g) => {
|
|
55
|
+
const scoreStr = g.score.toFixed(1);
|
|
56
|
+
const colored = g.score >= 0.5 ? fmt.green(scoreStr) : fmt.red(scoreStr);
|
|
57
|
+
const statusLabel = formatScorerStatus(g.status);
|
|
58
|
+
return `${fmt.dim(g.scorer_type)}${statusLabel ? ` ${statusLabel}` : ''} ${colored}`;
|
|
59
|
+
}).join(' ');
|
|
60
|
+
const toolEvents = (trial.session_log || []).filter((entry) => entry.type === 'tool_event');
|
|
61
|
+
const toolSuffix = toolEvents.length ? ` ${fmt.dim(toolEvents.length + ' tool events')}` : '';
|
|
62
|
+
const judgeToolCalls = (trial.session_log || []).filter((entry) => entry.type === 'judge_tool_call');
|
|
63
|
+
const judgeToolNames = judgeToolCalls
|
|
64
|
+
.map((e) => e.judge_tool_call?.name)
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
const uniqueJudgeTools = Array.from(new Set(judgeToolNames));
|
|
67
|
+
const judgeToolSuffix = judgeToolCalls.length
|
|
68
|
+
? ` ${fmt.dim(`${judgeToolCalls.length} judge tool calls (${uniqueJudgeTools.join(', ')})`)}`
|
|
69
|
+
: '';
|
|
70
|
+
const convSuffix = trial.conversation
|
|
71
|
+
? ` ${fmt.dim(`${trial.conversation.total_turns} turns`)} ${fmt.dim(trial.conversation.completion_reason)}`
|
|
72
|
+
: '';
|
|
73
|
+
const trialLabel = trial.name || `${trial.trial_id}`;
|
|
74
|
+
console.log(` ${fmt.dim(trialLabel.padEnd(4))} ${trialStatus} ${reward} ${fmt.dim(dur.padEnd(7))} ${fmt.dim(cmds.padEnd(7))} ${scorers}${convSuffix}${toolSuffix}${judgeToolSuffix}`);
|
|
75
|
+
}
|
|
76
|
+
console.log();
|
|
77
|
+
const nonOkScorers = trials.flatMap((trial) => (trial.scorer_results || [])
|
|
78
|
+
.filter((g) => g.status && g.status !== 'ok')
|
|
79
|
+
.map((g) => ({ trial, scorer: g })));
|
|
80
|
+
if (nonOkScorers.length > 0) {
|
|
81
|
+
console.log(` ${fmt.bold('Scorer Statuses')}`);
|
|
82
|
+
for (const entry of nonOkScorers) {
|
|
83
|
+
console.log(` ${fmt.dim(`trial ${entry.trial.trial_id}`)} ${formatScorerStatus(entry.scorer.status)} ${fmt.dim(entry.scorer.scorer_type)} ${fmt.dim(entry.scorer.details.substring(0, 100))}`);
|
|
84
|
+
}
|
|
85
|
+
console.log();
|
|
86
|
+
}
|
|
87
|
+
// ── LLM scorer details
|
|
88
|
+
const hasLlm = trials.some((t) => t.scorer_results?.some((g) => g.scorer_type === 'llm_rubric'));
|
|
89
|
+
if (hasLlm) {
|
|
90
|
+
for (const trial of trials) {
|
|
91
|
+
const llmScorers = (trial.scorer_results || []).filter((g) => g.scorer_type === 'llm_rubric');
|
|
92
|
+
for (const g of llmScorers) {
|
|
93
|
+
const scoreStr = g.score >= 0.5 ? fmt.green(g.score.toFixed(2)) : fmt.red(g.score.toFixed(2));
|
|
94
|
+
console.log(` ${fmt.dim(`trial ${trial.trial_id}`)} ${scoreStr} ${fmt.dim(g.details.substring(0, 100))}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
console.log();
|
|
98
|
+
}
|
|
99
|
+
// ── Step scorer details (conversation trials)
|
|
100
|
+
const hasStepScorers = trials.some((t) => t.conversation?.turns?.some((turn) => (turn.step_scorer_results?.length ?? 0) > 0));
|
|
101
|
+
if (hasStepScorers) {
|
|
102
|
+
console.log(` ${fmt.bold('Step Scorers')}`);
|
|
103
|
+
for (const trial of trials) {
|
|
104
|
+
if (!trial.conversation?.turns)
|
|
105
|
+
continue;
|
|
106
|
+
for (const turn of trial.conversation.turns) {
|
|
107
|
+
if (!turn.step_scorer_results?.length)
|
|
108
|
+
continue;
|
|
109
|
+
for (const g of turn.step_scorer_results) {
|
|
110
|
+
const scoreStr = g.score >= 0.5 ? fmt.green(g.score.toFixed(2)) : fmt.red(g.score.toFixed(2));
|
|
111
|
+
const statusLabel = formatScorerStatus(g.status);
|
|
112
|
+
console.log(` ${fmt.dim(`trial ${trial.trial_id} turn ${turn.turn_number}`)} ${scoreStr} ${fmt.dim(g.scorer_type)}${statusLabel ? ` ${statusLabel}` : ''} ${fmt.dim(g.details.substring(0, 80))}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
console.log();
|
|
117
|
+
}
|
|
118
|
+
console.log(` ${fmt.dim(file)}`);
|
|
119
|
+
console.log();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function formatScorerStatus(status) {
|
|
123
|
+
switch (status) {
|
|
124
|
+
case 'error':
|
|
125
|
+
return '[ERROR]';
|
|
126
|
+
case 'skipped':
|
|
127
|
+
return '[SKIPPED]';
|
|
128
|
+
default:
|
|
129
|
+
return '';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { LogEntry } from '../types.js';
|
|
2
|
+
import type { ReactionFiredEntry, TurnDetail } from '../sdk/types.js';
|
|
3
|
+
import { type RuntimePolicyAuditEntry } from '../sdk/runtime-policy.js';
|
|
4
|
+
export interface DiagnosticsScorer {
|
|
5
|
+
name: string;
|
|
6
|
+
type: string;
|
|
7
|
+
score: number;
|
|
8
|
+
weight: number;
|
|
9
|
+
details?: string;
|
|
10
|
+
status?: 'ok' | 'error' | 'skipped';
|
|
11
|
+
}
|
|
12
|
+
export interface DiagnosticsTurnDetail extends TurnDetail {
|
|
13
|
+
apiRetries: number;
|
|
14
|
+
}
|
|
15
|
+
export type DiagnosticsRuntimePolicyEntry = RuntimePolicyAuditEntry;
|
|
16
|
+
export interface DiagnosticsReport {
|
|
17
|
+
turns: number;
|
|
18
|
+
totalDurationMs: number;
|
|
19
|
+
completionReason?: string;
|
|
20
|
+
completionDetail?: string;
|
|
21
|
+
score?: number;
|
|
22
|
+
turnDetails: DiagnosticsTurnDetail[];
|
|
23
|
+
reactionsFired: ReactionFiredEntry[];
|
|
24
|
+
runtimePoliciesApplied: DiagnosticsRuntimePolicyEntry[];
|
|
25
|
+
recommendedTimeoutMs: number;
|
|
26
|
+
warnings: string[];
|
|
27
|
+
scorers: Array<DiagnosticsScorer & {
|
|
28
|
+
status: 'ok' | 'error' | 'skipped';
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
export interface BuildDiagnosticsReportInput {
|
|
32
|
+
completionReason?: string;
|
|
33
|
+
completionDetail?: string;
|
|
34
|
+
score?: number;
|
|
35
|
+
turnDetails?: TurnDetail[];
|
|
36
|
+
reactionsFired?: ReactionFiredEntry[];
|
|
37
|
+
scorers?: DiagnosticsScorer[];
|
|
38
|
+
log: LogEntry[];
|
|
39
|
+
}
|
|
40
|
+
export interface FormatDiagnosticsOptions {
|
|
41
|
+
verbose?: boolean;
|
|
42
|
+
currentTimeoutMs?: number;
|
|
43
|
+
}
|
|
44
|
+
export declare function buildDiagnosticsReport(input: BuildDiagnosticsReportInput): DiagnosticsReport;
|
|
45
|
+
export declare function formatDiagnosticsSummary(report: DiagnosticsReport): string;
|
|
46
|
+
export declare function formatDiagnostics(report: DiagnosticsReport, opts?: FormatDiagnosticsOptions): string;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { extractRuntimePolicyAuditEntries } from '../sdk/runtime-policy.js';
|
|
2
|
+
export function buildDiagnosticsReport(input) {
|
|
3
|
+
const baseTurnDetails = (input.turnDetails && input.turnDetails.length > 0)
|
|
4
|
+
? [...input.turnDetails]
|
|
5
|
+
: deriveTurnDetailsFromLog(input.log);
|
|
6
|
+
const retriesByTurn = countRetriesByTurn(input.log);
|
|
7
|
+
const turnDetails = baseTurnDetails
|
|
8
|
+
.sort((a, b) => a.turn - b.turn)
|
|
9
|
+
.map((detail) => ({
|
|
10
|
+
...detail,
|
|
11
|
+
apiRetries: retriesByTurn.get(detail.turn) ?? 0,
|
|
12
|
+
}));
|
|
13
|
+
const warnings = turnDetails
|
|
14
|
+
.filter((detail) => detail.outputLines > 500)
|
|
15
|
+
.map((detail) => `Turn ${detail.turn}: output exceeded 500 lines (${detail.outputLines} lines)`);
|
|
16
|
+
return {
|
|
17
|
+
turns: turnDetails.length,
|
|
18
|
+
totalDurationMs: turnDetails.reduce((sum, detail) => sum + detail.durationMs, 0),
|
|
19
|
+
completionReason: input.completionReason,
|
|
20
|
+
completionDetail: input.completionDetail,
|
|
21
|
+
score: input.score,
|
|
22
|
+
turnDetails,
|
|
23
|
+
reactionsFired: input.reactionsFired ?? [],
|
|
24
|
+
runtimePoliciesApplied: extractRuntimePolicyAuditEntries(input.log),
|
|
25
|
+
recommendedTimeoutMs: recommendTimeoutMs(turnDetails),
|
|
26
|
+
warnings,
|
|
27
|
+
scorers: (input.scorers ?? []).map((scorer) => ({
|
|
28
|
+
...scorer,
|
|
29
|
+
status: scorer.status ?? 'ok',
|
|
30
|
+
})),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function formatDiagnosticsSummary(report) {
|
|
34
|
+
const turnsLabel = report.turns === 1 ? '1 turn' : `${report.turns} turns`;
|
|
35
|
+
const durationLabel = `${(report.totalDurationMs / 1000).toFixed(1)}s`;
|
|
36
|
+
const completion = report.completionReason ?? 'unknown';
|
|
37
|
+
const scoreLabel = report.score == null ? 'n/a' : report.score.toFixed(2);
|
|
38
|
+
return `${turnsLabel} ${durationLabel} ${completion} score ${scoreLabel}`;
|
|
39
|
+
}
|
|
40
|
+
export function formatDiagnostics(report, opts = {}) {
|
|
41
|
+
if (opts.verbose === false) {
|
|
42
|
+
return formatDiagnosticsSummary(report);
|
|
43
|
+
}
|
|
44
|
+
const lines = [
|
|
45
|
+
'Unified Diagnostics',
|
|
46
|
+
`Summary: ${formatDiagnosticsSummary(report)}`,
|
|
47
|
+
];
|
|
48
|
+
if (report.completionDetail && report.completionReason === 'agent_crashed') {
|
|
49
|
+
lines.push(`Agent crashed: ${report.completionDetail}`);
|
|
50
|
+
}
|
|
51
|
+
else if (report.completionDetail
|
|
52
|
+
&& (report.completionReason === 'error' || report.completionReason === 'timeout')) {
|
|
53
|
+
lines.push(`Error: ${report.completionDetail}`);
|
|
54
|
+
}
|
|
55
|
+
lines.push('', 'Turns:');
|
|
56
|
+
for (const detail of report.turnDetails) {
|
|
57
|
+
lines.push(` Turn ${detail.turn}: ${(detail.durationMs / 1000).toFixed(1)}s, ${detail.outputLines} lines, ${detail.outputChars} chars, api retries: ${detail.apiRetries}`);
|
|
58
|
+
}
|
|
59
|
+
if (report.reactionsFired.length > 0) {
|
|
60
|
+
lines.push('', 'Reactions:');
|
|
61
|
+
for (const reaction of report.reactionsFired) {
|
|
62
|
+
lines.push(` Turn ${reaction.turn}: reaction ${reaction.reactionIndex} ${reaction.pattern} -> ${reaction.reply}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (report.runtimePoliciesApplied.length > 0) {
|
|
66
|
+
lines.push('', 'Runtime policies:');
|
|
67
|
+
for (const policy of report.runtimePoliciesApplied) {
|
|
68
|
+
lines.push(` ${policy.id}@${policy.version} applied on turns ${policy.turns.join(', ')}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (report.scorers.length > 0) {
|
|
72
|
+
lines.push('', 'Scorers:');
|
|
73
|
+
for (const scorer of report.scorers) {
|
|
74
|
+
const detailSuffix = scorer.details ? ` ${scorer.details}` : '';
|
|
75
|
+
lines.push(` [${scorer.status}] ${scorer.name} (${scorer.type}) ${scorer.score.toFixed(2)}${detailSuffix}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (report.warnings.length > 0) {
|
|
79
|
+
lines.push('', 'Warnings:');
|
|
80
|
+
for (const warning of report.warnings) {
|
|
81
|
+
lines.push(` ${warning}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
lines.push('', formatTimeoutRecommendation(report.recommendedTimeoutMs, opts.currentTimeoutMs));
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
}
|
|
87
|
+
function deriveTurnDetailsFromLog(log) {
|
|
88
|
+
return log
|
|
89
|
+
.filter((entry) => entry.type === 'agent_result'
|
|
90
|
+
&& entry.synthetic_blocked_prompt !== true
|
|
91
|
+
&& typeof entry.turn_number === 'number'
|
|
92
|
+
&& typeof entry.duration_ms === 'number'
|
|
93
|
+
&& typeof entry.output_lines === 'number'
|
|
94
|
+
&& typeof entry.output_chars === 'number')
|
|
95
|
+
.map((entry) => ({
|
|
96
|
+
turn: entry.turn_number,
|
|
97
|
+
durationMs: entry.duration_ms,
|
|
98
|
+
outputLines: entry.output_lines,
|
|
99
|
+
outputChars: entry.output_chars,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
function countRetriesByTurn(log) {
|
|
103
|
+
const retriesByTurn = new Map();
|
|
104
|
+
let pendingRetries = 0;
|
|
105
|
+
for (const entry of log) {
|
|
106
|
+
if (isRetryEntry(entry)) {
|
|
107
|
+
pendingRetries++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (entry.type === 'agent_result' && typeof entry.turn_number === 'number') {
|
|
111
|
+
if (pendingRetries > 0) {
|
|
112
|
+
retriesByTurn.set(entry.turn_number, (retriesByTurn.get(entry.turn_number) ?? 0) + pendingRetries);
|
|
113
|
+
pendingRetries = 0;
|
|
114
|
+
}
|
|
115
|
+
if (!retriesByTurn.has(entry.turn_number)) {
|
|
116
|
+
retriesByTurn.set(entry.turn_number, 0);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return retriesByTurn;
|
|
121
|
+
}
|
|
122
|
+
function isRetryEntry(entry) {
|
|
123
|
+
if (entry.type !== 'agent_result')
|
|
124
|
+
return false;
|
|
125
|
+
const text = `${entry.assistant_message ?? ''} ${entry.output ?? ''}`.toLowerCase();
|
|
126
|
+
return text.includes('retry') || text.includes('api_retry');
|
|
127
|
+
}
|
|
128
|
+
function recommendTimeoutMs(turnDetails) {
|
|
129
|
+
if (turnDetails.length <= 1) {
|
|
130
|
+
return (turnDetails[0]?.durationMs ?? 0) + 30_000;
|
|
131
|
+
}
|
|
132
|
+
const totalDurationMs = turnDetails.reduce((sum, detail) => sum + detail.durationMs, 0);
|
|
133
|
+
return Math.max(totalDurationMs + 200_000, 30_000);
|
|
134
|
+
}
|
|
135
|
+
function formatTimeoutRecommendation(recommendedTimeoutMs, currentTimeoutMs) {
|
|
136
|
+
const recommendedSeconds = Math.round(recommendedTimeoutMs / 1000);
|
|
137
|
+
if (currentTimeoutMs == null) {
|
|
138
|
+
return `Recommended timeout: ~${recommendedSeconds}s`;
|
|
139
|
+
}
|
|
140
|
+
if (currentTimeoutMs > recommendedTimeoutMs) {
|
|
141
|
+
return `Timeout could be reduced to ~${recommendedSeconds}s`;
|
|
142
|
+
}
|
|
143
|
+
if (currentTimeoutMs < recommendedTimeoutMs) {
|
|
144
|
+
return `Timeout should be increased to ~${recommendedSeconds}s`;
|
|
145
|
+
}
|
|
146
|
+
return `Timeout looks right at ~${recommendedSeconds}s`;
|
|
147
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure markdown formatting and GitHub PR comment posting for pathgrade.
|
|
3
|
+
*
|
|
4
|
+
* The pure formatter (`formatReportMarkdown`) has no side effects. The
|
|
5
|
+
* GitHub API layer (`resolvePrContext`, `postOrUpdateComment`) talks to
|
|
6
|
+
* the GitHub issues-comments API via `fetch`, with graceful fallback on
|
|
7
|
+
* missing context or HTTP errors — the `pathgrade report` command is
|
|
8
|
+
* display-only and must never fail CI.
|
|
9
|
+
*/
|
|
10
|
+
import type { PathgradeReport, PathgradeSelectionReport } from '../types.js';
|
|
11
|
+
export interface FormatOptions {
|
|
12
|
+
commentId: string;
|
|
13
|
+
}
|
|
14
|
+
/** Minimal body posted when `.pathgrade/results.json` is missing. */
|
|
15
|
+
export declare const MISSING_RESULTS_BODY = "Pathgrade evals did not produce results. Check the workflow logs.";
|
|
16
|
+
export declare function commentMarker(commentId: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Format a pathgrade PR comment as markdown.
|
|
19
|
+
*
|
|
20
|
+
* The string starts with the dedup comment marker and is intended to be
|
|
21
|
+
* posted as a GitHub issue comment body.
|
|
22
|
+
*/
|
|
23
|
+
export declare function formatReportMarkdown(report: PathgradeReport, opts: FormatOptions): string;
|
|
24
|
+
/**
|
|
25
|
+
* Render the `### Selection` section for the PR comment. Pure: no fs,
|
|
26
|
+
* no network, no reporter side effects. Three variants:
|
|
27
|
+
*
|
|
28
|
+
* - `global_match` set → "global trigger fired" one-liner, no collapsible.
|
|
29
|
+
* - `skipped` empty + `selected` non-empty → "every eval had a matching
|
|
30
|
+
* change" one-liner.
|
|
31
|
+
* - otherwise → "Ran **X of Y**" line + `<details>` block listing
|
|
32
|
+
* skipped eval paths as inline code.
|
|
33
|
+
*/
|
|
34
|
+
export declare function formatSelectionSection(selection: PathgradeSelectionReport): string;
|
|
35
|
+
export interface PrContext {
|
|
36
|
+
owner: string;
|
|
37
|
+
repo: string;
|
|
38
|
+
prNumber: number;
|
|
39
|
+
token: string;
|
|
40
|
+
}
|
|
41
|
+
export interface PrEnv {
|
|
42
|
+
GITHUB_TOKEN?: string;
|
|
43
|
+
GITHUB_REPOSITORY?: string;
|
|
44
|
+
GITHUB_REF?: string;
|
|
45
|
+
GITHUB_EVENT_PATH?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the GitHub PR context from CI env vars. Returns `null` when any
|
|
49
|
+
* required field is missing — callers should skip posting and fall back
|
|
50
|
+
* to stdout.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolvePrContext(env: PrEnv): PrContext | null;
|
|
53
|
+
export interface PostOptions {
|
|
54
|
+
commentId: string;
|
|
55
|
+
/** Body WITHOUT the marker — the marker is prepended if not already present. */
|
|
56
|
+
body: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Find an existing PR comment carrying `<!-- pathgrade:${commentId} -->`
|
|
60
|
+
* and update it; otherwise create a new one. Swallows all errors (logs
|
|
61
|
+
* to stderr) — `pathgrade report` must never fail CI.
|
|
62
|
+
*
|
|
63
|
+
* Bodies longer than GitHub's 65,536-character limit are truncated with a
|
|
64
|
+
* sentinel pointing to workflow artifacts, preserving the leading dedup
|
|
65
|
+
* marker so subsequent runs still find and update this comment.
|
|
66
|
+
*/
|
|
67
|
+
export declare function postOrUpdateComment(ctx: PrContext, opts: PostOptions): Promise<void>;
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure markdown formatting and GitHub PR comment posting for pathgrade.
|
|
3
|
+
*
|
|
4
|
+
* The pure formatter (`formatReportMarkdown`) has no side effects. The
|
|
5
|
+
* GitHub API layer (`resolvePrContext`, `postOrUpdateComment`) talks to
|
|
6
|
+
* the GitHub issues-comments API via `fetch`, with graceful fallback on
|
|
7
|
+
* missing context or HTTP errors — the `pathgrade report` command is
|
|
8
|
+
* display-only and must never fail CI.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
/** Minimal body posted when `.pathgrade/results.json` is missing. */
|
|
12
|
+
export const MISSING_RESULTS_BODY = 'Pathgrade evals did not produce results. Check the workflow logs.';
|
|
13
|
+
export function commentMarker(commentId) {
|
|
14
|
+
return `<!-- pathgrade:${commentId} -->`;
|
|
15
|
+
}
|
|
16
|
+
function pct(n) {
|
|
17
|
+
return `${(n * 100).toFixed(1)}%`;
|
|
18
|
+
}
|
|
19
|
+
function durationSeconds(ms) {
|
|
20
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
21
|
+
}
|
|
22
|
+
function computeAvgDuration(trials) {
|
|
23
|
+
if (trials.length === 0)
|
|
24
|
+
return 0;
|
|
25
|
+
return trials.reduce((sum, t) => sum + (t.duration_ms ?? 0), 0) / trials.length;
|
|
26
|
+
}
|
|
27
|
+
function escapeTableCell(value) {
|
|
28
|
+
return value.replace(/\|/g, '\\|').replace(/\n/g, ' ');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Format a pathgrade PR comment as markdown.
|
|
32
|
+
*
|
|
33
|
+
* The string starts with the dedup comment marker and is intended to be
|
|
34
|
+
* posted as a GitHub issue comment body.
|
|
35
|
+
*/
|
|
36
|
+
export function formatReportMarkdown(report, opts) {
|
|
37
|
+
const totalTrials = report.groups.reduce((n, g) => n + g.trials.length, 0);
|
|
38
|
+
const p = report.overall_pass_rate;
|
|
39
|
+
const overallPassAtK = totalTrials > 0 ? 1 - Math.pow(1 - p, totalTrials) : 0;
|
|
40
|
+
const overallPassPowK = totalTrials > 0 ? Math.pow(p, totalTrials) : 0;
|
|
41
|
+
const icon = report.status === 'pass' ? '✅' : '❌';
|
|
42
|
+
const lines = [];
|
|
43
|
+
lines.push(commentMarker(opts.commentId));
|
|
44
|
+
lines.push('');
|
|
45
|
+
lines.push(`### ${icon} Pathgrade report`);
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push(`**Pass rate:** ${pct(p)} | ` +
|
|
48
|
+
`**pass@${totalTrials}:** ${pct(overallPassAtK)} | ` +
|
|
49
|
+
`**pass^${totalTrials}:** ${pct(overallPassPowK)}`);
|
|
50
|
+
if (report.threshold != null) {
|
|
51
|
+
lines.push('');
|
|
52
|
+
lines.push(`Threshold: ${pct(report.threshold)} — ${report.status.toUpperCase()}`);
|
|
53
|
+
}
|
|
54
|
+
lines.push('');
|
|
55
|
+
lines.push('| Group | Pass rate | pass@k | pass^k | Skills | Avg duration |');
|
|
56
|
+
lines.push('|---|---|---|---|---|---|');
|
|
57
|
+
for (const group of report.groups) {
|
|
58
|
+
const skills = group.skills_used.length > 0 ? group.skills_used.join(', ') : '—';
|
|
59
|
+
const avg = computeAvgDuration(group.trials);
|
|
60
|
+
lines.push(`| ${escapeTableCell(group.task)} | ${pct(group.pass_rate)} | ${pct(group.pass_at_k)} | ${pct(group.pass_pow_k)} | ${escapeTableCell(skills)} | ${durationSeconds(avg)} |`);
|
|
61
|
+
}
|
|
62
|
+
if (report.selection) {
|
|
63
|
+
lines.push('');
|
|
64
|
+
lines.push(formatSelectionSection(report.selection));
|
|
65
|
+
}
|
|
66
|
+
for (const group of report.groups) {
|
|
67
|
+
lines.push('');
|
|
68
|
+
lines.push(formatGroupDetails(group));
|
|
69
|
+
}
|
|
70
|
+
lines.push('');
|
|
71
|
+
return lines.join('\n');
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Render the `### Selection` section for the PR comment. Pure: no fs,
|
|
75
|
+
* no network, no reporter side effects. Three variants:
|
|
76
|
+
*
|
|
77
|
+
* - `global_match` set → "global trigger fired" one-liner, no collapsible.
|
|
78
|
+
* - `skipped` empty + `selected` non-empty → "every eval had a matching
|
|
79
|
+
* change" one-liner.
|
|
80
|
+
* - otherwise → "Ran **X of Y**" line + `<details>` block listing
|
|
81
|
+
* skipped eval paths as inline code.
|
|
82
|
+
*/
|
|
83
|
+
export function formatSelectionSection(selection) {
|
|
84
|
+
const out = [];
|
|
85
|
+
out.push('### Selection');
|
|
86
|
+
const total = selection.selected.length + selection.skipped.length;
|
|
87
|
+
if (selection.global_match) {
|
|
88
|
+
out.push(`Ran **all ${total}** evals — global trigger \`${selection.global_match}\` matched.`);
|
|
89
|
+
return out.join('\n');
|
|
90
|
+
}
|
|
91
|
+
if (selection.skipped.length === 0 && selection.selected.length > 0) {
|
|
92
|
+
out.push(`Ran **all ${total}** evals — every eval had a matching change.`);
|
|
93
|
+
return out.join('\n');
|
|
94
|
+
}
|
|
95
|
+
out.push(`Ran **${selection.selected.length} of ${total}** evals based on changes vs \`${selection.base_ref}\`.`);
|
|
96
|
+
if (selection.skipped.length > 0) {
|
|
97
|
+
out.push('');
|
|
98
|
+
out.push(`<details><summary>${selection.skipped.length} skipped (unaffected)</summary>`);
|
|
99
|
+
out.push('');
|
|
100
|
+
for (const s of selection.skipped) {
|
|
101
|
+
out.push(`- \`${s.file}\``);
|
|
102
|
+
}
|
|
103
|
+
out.push('');
|
|
104
|
+
out.push('</details>');
|
|
105
|
+
}
|
|
106
|
+
return out.join('\n');
|
|
107
|
+
}
|
|
108
|
+
function parsePrNumberFromRef(ref) {
|
|
109
|
+
if (!ref)
|
|
110
|
+
return null;
|
|
111
|
+
// refs/pull/<n>/merge or refs/pull/<n>/head
|
|
112
|
+
const m = ref.match(/^refs\/pull\/(\d+)\//);
|
|
113
|
+
if (!m)
|
|
114
|
+
return null;
|
|
115
|
+
const n = parseInt(m[1], 10);
|
|
116
|
+
return Number.isFinite(n) ? n : null;
|
|
117
|
+
}
|
|
118
|
+
function readPrNumberFromEvent(eventPath) {
|
|
119
|
+
if (!eventPath)
|
|
120
|
+
return null;
|
|
121
|
+
try {
|
|
122
|
+
const raw = fs.readFileSync(eventPath, 'utf-8');
|
|
123
|
+
const payload = JSON.parse(raw);
|
|
124
|
+
const n = payload?.pull_request?.number;
|
|
125
|
+
return typeof n === 'number' && Number.isFinite(n) ? n : null;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Resolve the GitHub PR context from CI env vars. Returns `null` when any
|
|
133
|
+
* required field is missing — callers should skip posting and fall back
|
|
134
|
+
* to stdout.
|
|
135
|
+
*/
|
|
136
|
+
export function resolvePrContext(env) {
|
|
137
|
+
const token = env.GITHUB_TOKEN;
|
|
138
|
+
const repo = env.GITHUB_REPOSITORY;
|
|
139
|
+
if (!token || !repo)
|
|
140
|
+
return null;
|
|
141
|
+
const [owner, repoName] = repo.split('/', 2);
|
|
142
|
+
if (!owner || !repoName)
|
|
143
|
+
return null;
|
|
144
|
+
const prNumber = readPrNumberFromEvent(env.GITHUB_EVENT_PATH) ??
|
|
145
|
+
parsePrNumberFromRef(env.GITHUB_REF);
|
|
146
|
+
if (prNumber == null)
|
|
147
|
+
return null;
|
|
148
|
+
return { owner, repo: repoName, prNumber, token };
|
|
149
|
+
}
|
|
150
|
+
function apiHeaders(token) {
|
|
151
|
+
return {
|
|
152
|
+
Authorization: `Bearer ${token}`,
|
|
153
|
+
Accept: 'application/vnd.github+json',
|
|
154
|
+
'Content-Type': 'application/json',
|
|
155
|
+
'User-Agent': 'pathgrade-report',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async function listPrComments(ctx) {
|
|
159
|
+
const url = `https://api.github.com/repos/${ctx.owner}/${ctx.repo}/issues/${ctx.prNumber}/comments?per_page=100`;
|
|
160
|
+
const res = await fetch(url, { headers: apiHeaders(ctx.token) });
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
throw new Error(`GET ${url} returned ${res.status}`);
|
|
163
|
+
}
|
|
164
|
+
const data = (await res.json());
|
|
165
|
+
return Array.isArray(data) ? data : [];
|
|
166
|
+
}
|
|
167
|
+
async function createPrComment(ctx, body) {
|
|
168
|
+
const url = `https://api.github.com/repos/${ctx.owner}/${ctx.repo}/issues/${ctx.prNumber}/comments`;
|
|
169
|
+
const res = await fetch(url, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: apiHeaders(ctx.token),
|
|
172
|
+
body: JSON.stringify({ body }),
|
|
173
|
+
});
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
throw new Error(`POST ${url} returned ${res.status}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function updatePrComment(ctx, commentId, body) {
|
|
179
|
+
const url = `https://api.github.com/repos/${ctx.owner}/${ctx.repo}/issues/comments/${commentId}`;
|
|
180
|
+
const res = await fetch(url, {
|
|
181
|
+
method: 'PATCH',
|
|
182
|
+
headers: apiHeaders(ctx.token),
|
|
183
|
+
body: JSON.stringify({ body }),
|
|
184
|
+
});
|
|
185
|
+
if (!res.ok) {
|
|
186
|
+
throw new Error(`PATCH ${url} returned ${res.status}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** GitHub's documented hard limit for issue-comment bodies. */
|
|
190
|
+
const GITHUB_COMMENT_MAX = 65536;
|
|
191
|
+
const TRUNCATION_SENTINEL = '\n\n_…output truncated to fit GitHub\'s comment size limit — see the workflow artifacts for the full report._';
|
|
192
|
+
/**
|
|
193
|
+
* Find an existing PR comment carrying `<!-- pathgrade:${commentId} -->`
|
|
194
|
+
* and update it; otherwise create a new one. Swallows all errors (logs
|
|
195
|
+
* to stderr) — `pathgrade report` must never fail CI.
|
|
196
|
+
*
|
|
197
|
+
* Bodies longer than GitHub's 65,536-character limit are truncated with a
|
|
198
|
+
* sentinel pointing to workflow artifacts, preserving the leading dedup
|
|
199
|
+
* marker so subsequent runs still find and update this comment.
|
|
200
|
+
*/
|
|
201
|
+
export async function postOrUpdateComment(ctx, opts) {
|
|
202
|
+
const marker = commentMarker(opts.commentId);
|
|
203
|
+
const withMarker = opts.body.startsWith(marker) ? opts.body : `${marker}\n${opts.body}`;
|
|
204
|
+
const body = withMarker.length <= GITHUB_COMMENT_MAX
|
|
205
|
+
? withMarker
|
|
206
|
+
: withMarker.slice(0, GITHUB_COMMENT_MAX - TRUNCATION_SENTINEL.length) +
|
|
207
|
+
TRUNCATION_SENTINEL;
|
|
208
|
+
try {
|
|
209
|
+
const existing = await listPrComments(ctx);
|
|
210
|
+
const match = existing.find((c) => typeof c.body === 'string' && c.body.includes(marker));
|
|
211
|
+
if (match) {
|
|
212
|
+
await updatePrComment(ctx, match.id, body);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
await createPrComment(ctx, body);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
220
|
+
console.error(`pathgrade report: failed to post PR comment — ${message}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function formatGroupDetails(group) {
|
|
224
|
+
const out = [];
|
|
225
|
+
out.push('<details>');
|
|
226
|
+
out.push(`<summary>${escapeTableCell(group.task)} — per-trial breakdown</summary>`);
|
|
227
|
+
out.push('');
|
|
228
|
+
for (const trial of group.trials) {
|
|
229
|
+
out.push(`#### ${trial.name ?? `trial ${trial.trial_id}`}`);
|
|
230
|
+
const reason = trial.diagnostics?.completionReason ?? '—';
|
|
231
|
+
out.push(`reward: **${trial.reward.toFixed(2)}** | duration: ${durationSeconds(trial.duration_ms)} | completion: \`${reason}\``);
|
|
232
|
+
if (trial.scorer_results.length > 0) {
|
|
233
|
+
out.push('');
|
|
234
|
+
out.push('| Scorer | Score | Weight | Details |');
|
|
235
|
+
out.push('|---|---|---|---|');
|
|
236
|
+
for (const s of trial.scorer_results) {
|
|
237
|
+
out.push(`| ${escapeTableCell(s.scorer_type)} | ${s.score.toFixed(2)} | ${s.weight.toFixed(2)} | ${escapeTableCell(s.details ?? '')} |`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const warnings = trial.diagnostics?.warnings ?? [];
|
|
241
|
+
if (warnings.length > 0) {
|
|
242
|
+
out.push('');
|
|
243
|
+
out.push('Warnings:');
|
|
244
|
+
for (const w of warnings) {
|
|
245
|
+
out.push(`- ${w}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
out.push('');
|
|
249
|
+
}
|
|
250
|
+
out.push('</details>');
|
|
251
|
+
return out.join('\n');
|
|
252
|
+
}
|