From bb2053ec339300742f34015979fa1f315097f93c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 27 Apr 2026 05:23:50 +0000 Subject: [PATCH] chore: sync workflow templates from Workflows repo Automated sync from stranske/Workflows Template hash: 20d0e241bca2 Changes synced from sync-manifest.yml --- .github/scripts/agents_pr_meta_keepalive.js | 9 + .github/scripts/agents_pr_meta_update_body.js | 64 +++++++- .github/scripts/coverage_monitor_summary.js | 8 +- .github/scripts/source_context.js | 103 +++++++++--- .github/scripts/weekly_metrics_artifacts.js | 154 +++++++++++++++++- scripts/aggregate_agent_metrics.py | 64 ++++---- 6 files changed, 348 insertions(+), 54 deletions(-) diff --git a/.github/scripts/agents_pr_meta_keepalive.js b/.github/scripts/agents_pr_meta_keepalive.js index 39eaec8a..2e241bf4 100644 --- a/.github/scripts/agents_pr_meta_keepalive.js +++ b/.github/scripts/agents_pr_meta_keepalive.js @@ -755,6 +755,15 @@ async function detectKeepalive({ core, github, context, env = process.env }) { return finalise(); } + if (sourceContext.noAutomation) { + outputs.reason = 'no-automation-source-context'; + outputs.dispatch = 'false'; + core.info( + `Keepalive dispatch skipped: PR source context opts out of automation (${formatSourceContextForLog(sourceContext)}).`, + ); + return finalise(); + } + if (!issueNumber) { if (sourceContext.isValid && !sourceContext.requiresIssue) { core.info( diff --git a/.github/scripts/agents_pr_meta_update_body.js b/.github/scripts/agents_pr_meta_update_body.js index 89c24c55..47e6c4df 100644 --- a/.github/scripts/agents_pr_meta_update_body.js +++ b/.github/scripts/agents_pr_meta_update_body.js @@ -980,13 +980,16 @@ async function createIssueCommentWithRetry({ github, owner, repo, issueNumber, b } function buildSourceContextResolvedCommentBody(prNumber, sourceContext) { + const isIssueBacked = sourceContext?.requiresIssue || sourceContext?.issueNumber || sourceContext?.sourceType === SOURCE_TYPES.GITHUB_ISSUE; return [ '', '### Workflow source detected', '', `PR #${prNumber} now has valid workflow source context (${formatSourceContextForLog(sourceContext)}).`, '', - 'No linked GitHub issue is required for this PR.', + isIssueBacked + ? 'A linked GitHub issue is present for this PR.' + : 'No linked GitHub issue is required for this PR.', ].join('\n'); } @@ -1023,6 +1026,37 @@ function resolveExplicitNonIssueWorkflowSourceContext(pr = {}) { }; } +function extractExplicitIssueSyncNumbers(pr = {}) { + const text = `${pr.title || ''}\n${pr.body || ''}`; + const issueNumbers = new Set(); + const patterns = [ + /\b(?:close[sd]?|closing|fix(?:e[sd])?|fixing|resolve[sd]?|resolving|address(?:e[sd])?|addressing)\s*[:#-]?\s*#([0-9]+)\b/gi, + /\b(?:(?:relate[sd]?\s+to|refs?|references?)\s+(?:issue\s+)?|(?:source|github|linked)\s+issue\s*)[:#-]?\s*#([0-9]+)\b/gi, + ]; + for (const pattern of patterns) { + for (const match of text.matchAll(pattern)) { + issueNumbers.add(Number(match[1])); + } + } + return issueNumbers; +} + +function hasExplicitIssueSyncReference(pr = {}) { + return extractExplicitIssueSyncNumbers(pr).size > 0; +} + +function resolveNonIssueWorkflowSourceContextForBodySync(pr = {}, issueNumber = null) { + const explicitNonIssueSourceContext = resolveExplicitNonIssueWorkflowSourceContext(pr); + if (!explicitNonIssueSourceContext) { + return null; + } + const explicitIssueNumbers = extractExplicitIssueSyncNumbers(pr); + if (issueNumber && explicitIssueNumbers.has(Number(issueNumber))) { + return null; + } + return explicitNonIssueSourceContext; +} + async function resolveSourceContextRepairComment({ github, owner, @@ -1356,7 +1390,9 @@ async function run({github: rawGithub, context, core, inputs}) { return; } - const explicitNonIssueSourceContext = resolveExplicitNonIssueWorkflowSourceContext(pr); + const issueNumber = extractIssueNumberFromPull(pr); + const sourceContext = resolvePrSourceContext(pr); + const explicitNonIssueSourceContext = resolveNonIssueWorkflowSourceContextForBodySync(pr, issueNumber); if (explicitNonIssueSourceContext) { core.info( `PR #${pr.number} has explicit non-issue workflow source context (${formatSourceContextForLog(explicitNonIssueSourceContext)}); skipping issue-sourced body sync.`, @@ -1382,8 +1418,6 @@ async function run({github: rawGithub, context, core, inputs}) { return; } - const issueNumber = extractIssueNumberFromPull(pr); - const sourceContext = resolvePrSourceContext(pr); if (!issueNumber) { if (sourceContext.isValid && !sourceContext.requiresIssue) { core.info( @@ -1461,6 +1495,25 @@ async function run({github: rawGithub, context, core, inputs}) { ); const issueBody = issueResponse.data.body || ''; + try { + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number: pr.number, + }); + await resolveSourceContextRepairComment({ + github, + owner, + repo, + prNumber: pr.number, + comments, + sourceContext, + core, + }); + } catch (error) { + core.warning(`Failed to resolve workflow source repair comment: ${error.message}`); + } + if (!issueBody) { core.warning(`Issue #${issueNumber} has no body content`); } else { @@ -1629,6 +1682,9 @@ module.exports = { buildSourceContextRepairCommentBody, buildSourceContextResolvedCommentBody, resolveExplicitNonIssueWorkflowSourceContext, + extractExplicitIssueSyncNumbers, + hasExplicitIssueSyncReference, + resolveNonIssueWorkflowSourceContextForBodySync, resolveSourceContextRepairComment, isCampaignIssue, buildStatusBlock, diff --git a/.github/scripts/coverage_monitor_summary.js b/.github/scripts/coverage_monitor_summary.js index 0a2fa756..67ef84d7 100644 --- a/.github/scripts/coverage_monitor_summary.js +++ b/.github/scripts/coverage_monitor_summary.js @@ -140,8 +140,12 @@ function optionalExistingReportPath(filePath) { const cleanedPath = cleanString(filePath); if (!cleanedPath) return ''; if (!fs.existsSync(cleanedPath)) return ''; - if (!fs.statSync(cleanedPath).isFile()) return ''; - return cleanedPath; + try { + if (!fs.statSync(cleanedPath).isFile()) return ''; + return cleanedPath; + } catch (_error) { + return ''; + } } function buildCoverageMonitorSummary(options = {}) { diff --git a/.github/scripts/source_context.js b/.github/scripts/source_context.js index b7899253..49c23f87 100644 --- a/.github/scripts/source_context.js +++ b/.github/scripts/source_context.js @@ -34,19 +34,23 @@ const SOURCE_LABELS = Object.freeze({ workflow_no_automation: SOURCE_TYPES.MANUAL_REMOTE, }); +const NO_AUTOMATION_LABELS = new Set(['workflow:no-automation', 'workflow_no_automation']); + const CHECKBOX_SOURCE_PATTERNS = Object.freeze([ [SOURCE_TYPES.GITHUB_ISSUE, /\bgithub\s+issue\b|\bsource\s+issue\b/i], [ SOURCE_TYPES.MANUAL_REMOTE, /\bdirect\s+pr\b|\bremote\s+github\s+work\b|\bstarted\s+directly\b|\bdo\s+not\s+automate\b|\bhuman[- ]only\b/i, ], - [SOURCE_TYPES.LOCAL_REQUEST, /\blocal\s+(?:codex|user)\s+request\b|\blocal\s+request\b/i], + [SOURCE_TYPES.LOCAL_REQUEST, /\blocal\s+(?:codex(?:\s*\/\s*|\s+)?user|codex|user)\s+request\b|\blocal\s+request\b/i], [SOURCE_TYPES.AUTOMATION_RUN, /\bautomation\s+run\b|\bworkflow\s+run\b/i], [SOURCE_TYPES.REVIEW_FOLLOWUP, /\breview\s+follow[- ]?up\b|\bfollow[- ]?up\s+from\s+pr\b/i], [SOURCE_TYPES.SYNC_CAMPAIGN, /\bsync\b|\bmaintenance\s+campaign\b|\bmaintenance\b/i], [SOURCE_TYPES.DEPENDABOT, /\bdependabot\b|\bdependency\s+update\b/i], ]); +const NO_AUTOMATION_CHECKBOX_PATTERN = /\bdo\s+not\s+automate\b/i; + function cleanString(value) { return String(value || '').trim(); } @@ -106,6 +110,54 @@ function labelNames(pull = {}) { : []; } +function checkedLabels(lines) { + return lines + .map((line) => line.match(/^\s*[-*]\s+\[[xX]\]\s+(.+?)\s*$/)) + .filter(Boolean) + .map((match) => match[1]); +} + +function workflowSourceSectionLines(body) { + const lines = String(body || '').split(/\r?\n/); + const start = lines.findIndex((line) => /^#{1,6}\s+Workflow Source\s*$/i.test(line)); + if (start < 0) { + return []; + } + + const sectionLines = []; + for (const line of lines.slice(start + 1)) { + if (/^#{1,6}\s+\S/.test(line)) { + break; + } + sectionLines.push(line); + } + return sectionLines; +} + +function startedFromLines(sectionLines) { + const start = sectionLines.findIndex((line) => /^\s*Started from:\s*$/i.test(line)); + if (start < 0) { + return sectionLines; + } + + const result = []; + for (const line of sectionLines.slice(start + 1)) { + if (/^\s*(Automation intent|Notes):\s*$/i.test(line)) { + break; + } + result.push(line); + } + return result; +} + +function hasCheckedNoAutomationTemplate(body) { + const sectionLines = workflowSourceSectionLines(body); + if (!sectionLines.length) { + return false; + } + return checkedLabels(startedFromLines(sectionLines)).some((label) => NO_AUTOMATION_CHECKBOX_PATTERN.test(label)); +} + function hasExplicitIssueReferencePrefix(value) { const prefix = cleanString(value) .replace(/[>_[\]()`*~]/g, ' ') @@ -200,26 +252,12 @@ function parseWorkflowSourceBlock(body) { } function sourceTypeFromCheckedTemplate(body) { - const lines = String(body || '').split(/\r?\n/); - const start = lines.findIndex((line) => /^#{1,6}\s+Workflow Source\s*$/i.test(line)); - if (start < 0) { + const sectionLines = workflowSourceSectionLines(body); + if (!sectionLines.length) { return SOURCE_TYPES.UNKNOWN; } - const sectionLines = []; - for (const line of lines.slice(start + 1)) { - if (/^#{1,6}\s+\S/.test(line)) { - break; - } - sectionLines.push(line); - } - const text = sectionLines.join('\n'); const checkedTypes = new Set(); - for (const line of text.split(/\r?\n/)) { - const checkbox = line.match(/^\s*[-*]\s+\[[xX]\]\s+(.+?)\s*$/); - if (!checkbox) { - continue; - } - const label = checkbox[1]; + for (const label of checkedLabels(startedFromLines(sectionLines))) { for (const [sourceType, pattern] of CHECKBOX_SOURCE_PATTERNS) { if (pattern.test(label)) { checkedTypes.add(sourceType); @@ -230,6 +268,27 @@ function sourceTypeFromCheckedTemplate(body) { return checkedTypes.size === 1 ? Array.from(checkedTypes)[0] : SOURCE_TYPES.UNKNOWN; } +function hasNoAutomationWorkflowContext(pull = {}) { + const body = String(pull?.body || ''); + const markerToken = normalizeToken(parseHtmlMarker(body, 'workflow-source')); + const block = parseWorkflowSourceBlock(body); + const blockTokens = [ + block.origin, + block.source, + block.type, + block.automation, + block.automation_intent, + ].map(normalizeToken); + const labels = labelNames(pull).map((label) => label.toLowerCase()); + + return ( + markerToken === 'no_automation' + || blockTokens.includes('no_automation') + || labels.some((label) => NO_AUTOMATION_LABELS.has(label) || NO_AUTOMATION_LABELS.has(normalizeToken(label))) + || hasCheckedNoAutomationTemplate(body) + ); +} + function sourceTypeFromLabels(pull = {}) { for (const label of labelNames(pull)) { const sourceType = SOURCE_LABELS[label.toLowerCase()] || SOURCE_LABELS[normalizeToken(label)]; @@ -267,6 +326,7 @@ function resolvePrSourceContext(pull = {}) { const body = String(pull?.body || ''); const block = parseWorkflowSourceBlock(body); const issueNumber = extractIssueNumberFromPull(pull); + const noAutomation = hasNoAutomationWorkflowContext(pull); const markerType = normalizeSourceType(parseHtmlMarker(body, 'workflow-source')); const blockType = normalizeSourceType(block.origin || block.source || block.type); @@ -305,12 +365,13 @@ function resolvePrSourceContext(pull = {}) { labelType !== SOURCE_TYPES.UNKNOWN ), requiresIssue: sourceType === SOURCE_TYPES.GITHUB_ISSUE, + noAutomation, }; } function hasValidNonIssueSourceContext(pull = {}) { const context = resolvePrSourceContext(pull); - return context.isValid && !context.requiresIssue; + return context.isValid && !context.requiresIssue && !context.noAutomation; } function formatSourceContextForLog(context = {}) { @@ -324,6 +385,9 @@ function formatSourceContextForLog(context = {}) { if (context.automation) { parts.push(`automation=${context.automation}`); } + if (context.noAutomation) { + parts.push('no_automation=true'); + } return parts.join(' '); } @@ -335,6 +399,7 @@ module.exports = { parseWorkflowSourceBlock, sourceTypeFromCheckedTemplate, sourceTypeFromLabels, + hasNoAutomationWorkflowContext, resolvePrSourceContext, hasValidNonIssueSourceContext, formatSourceContextForLog, diff --git a/.github/scripts/weekly_metrics_artifacts.js b/.github/scripts/weekly_metrics_artifacts.js index e9d24179..adc46b29 100644 --- a/.github/scripts/weekly_metrics_artifacts.js +++ b/.github/scripts/weekly_metrics_artifacts.js @@ -6,6 +6,7 @@ const DEFAULT_MAX_TOTAL = 80; const DEFAULT_MAX_PER_FAMILY = 20; const DEFAULT_MAX_SCAN_PAGES = 5; const DEFAULT_PER_PAGE = 100; +const DEFAULT_PRIORITY_WORKFLOW_RUNS_PER_SOURCE = 10; const EXACT_METRICS_ARTIFACTS = new Set([ 'keepalive-metrics', @@ -45,6 +46,37 @@ const PRIORITY_METRICS_FAMILIES = [ 'pr-source-context', ]; +const PRIORITY_WORKFLOW_ARTIFACT_SOURCES = [ + { + workflow_id: 'health-76-codex-cli-freshness.yml', + families: ['codex-cli-freshness'], + }, + { + workflow_id: 'reusable-agents-verifier.yml', + families: ['verifier-terminal-disposition'], + }, + { + workflow_id: 'agents-verify-to-new-pr.yml', + families: ['verifier-terminal-disposition'], + }, + { + workflow_id: 'agents-verify-to-issue-v2.yml', + families: ['verifier-terminal-disposition'], + }, + { + workflow_id: 'agents-bot-comment-handler.yml', + families: ['review-thread-terminal-disposition', 'bot-comment-auth-coverage-wrapper'], + }, + { + workflow_id: 'reusable-bot-comment-handler.yml', + families: ['review-thread-terminal-disposition', 'bot-comment-auth-coverage-reusable'], + }, + { + workflow_id: 'pr-11-ci-smoke.yml', + families: ['pr-source-context'], + }, +]; + function cleanString(value) { if (value === null || value === undefined) return ''; return String(value).trim(); @@ -102,6 +134,12 @@ function normalizeSelectionOptions(options = {}) { options.per_page ?? options.perPage ?? process.env.METRICS_ARTIFACTS_PER_PAGE, DEFAULT_PER_PAGE ); + const priorityWorkflowRunsPerSource = parsePositiveInt( + options.priority_workflow_runs_per_source ?? + options.priorityWorkflowRunsPerSource ?? + process.env.METRICS_PRIORITY_WORKFLOW_RUNS_PER_SOURCE, + DEFAULT_PRIORITY_WORKFLOW_RUNS_PER_SOURCE + ); const cutoffMs = nowMs - lookbackDays * 24 * 60 * 60 * 1000; return { now_ms: nowMs, @@ -110,6 +148,7 @@ function normalizeSelectionOptions(options = {}) { max_per_family: maxPerFamily, max_scan_pages: maxScanPages, per_page: perPage, + priority_workflow_runs_per_source: priorityWorkflowRunsPerSource, cutoff_ms: cutoffMs, }; } @@ -296,6 +335,7 @@ function selectMetricsArtifacts(artifacts = [], options = {}) { max_per_family: config.max_per_family, max_scan_pages: config.max_scan_pages, per_page: config.per_page, + priority_workflow_runs_per_source: config.priority_workflow_runs_per_source, cutoff_iso: new Date(config.cutoff_ms).toISOString(), }, ...stats, @@ -331,6 +371,7 @@ function buildSelectionErrorReport(options = {}, error = {}) { max_per_family: config.max_per_family, max_scan_pages: config.max_scan_pages, per_page: config.per_page, + priority_workflow_runs_per_source: config.priority_workflow_runs_per_source, cutoff_iso: new Date(config.cutoff_ms).toISOString(), }, scanned_count: 0, @@ -368,6 +409,7 @@ function formatSelectionMarkdown(report) { `- Status: ${report.status || 'pass'}`, `- Lookback days: ${report.config.lookback_days}`, `- Scan cap: ${report.config.max_scan_pages} pages x ${report.config.per_page} artifacts`, + `- Priority producer scan cap: ${report.config.priority_workflow_runs_per_source} runs per source workflow`, `- Download cap: ${report.config.max_total} total, ${report.config.max_per_family} per family`, `- Scanned artifacts: ${report.scanned_count}`, `- Candidate artifacts: ${report.candidate_count}`, @@ -405,6 +447,101 @@ function formatSelectionMarkdown(report) { return `${lines.join('\n')}\n`; } +function dedupeArtifacts(artifacts = []) { + const seen = new Set(); + const deduped = []; + for (const artifact of artifacts) { + const id = artifact?.id ?? artifact?.artifact_id ?? artifact?.artifactId; + const key = id + ? `id:${id}` + : `name:${cleanString(artifact?.name)}:${cleanString(artifact?.created_at ?? artifact?.createdAt)}`; + if (!key || seen.has(key)) continue; + seen.add(key); + deduped.push(artifact); + } + return deduped; +} + +function familiesSatisfied(artifacts = [], families = [], config = normalizeSelectionOptions()) { + const found = new Set(); + for (const raw of artifacts) { + const artifact = normalizeArtifact(raw); + if (!artifact.id || artifact.expired || !artifact.family || !families.includes(artifact.family)) { + continue; + } + if (artifact.timestamp_ms > 0 && artifact.timestamp_ms < config.cutoff_ms) { + continue; + } + found.add(artifact.family); + } + return families.every((family) => found.has(family)); +} + +function isNotFoundError(error) { + return Number(error?.status) === 404 || Number(error?.response?.status) === 404; +} + +async function collectPriorityWorkflowArtifacts({ + github, + owner, + repo, + withRetry, + options, + sources = PRIORITY_WORKFLOW_ARTIFACT_SOURCES, +}) { + const config = normalizeSelectionOptions(options); + const artifacts = []; + for (const source of sources) { + const workflowId = cleanString(source.workflow_id ?? source.workflowId); + const families = (source.families || []).map(cleanString).filter(Boolean); + if (!workflowId || families.length === 0) continue; + let runsResponse; + try { + runsResponse = await withRetry((client) => client.rest.actions.listWorkflowRuns({ + owner, + repo, + workflow_id: workflowId, + per_page: config.priority_workflow_runs_per_source, + })); + } catch (error) { + if (isNotFoundError(error)) continue; + throw error; + } + const runs = runsResponse?.data?.workflow_runs || []; + for (const run of runs) { + const runTimestamp = Math.max( + parseDateMs(run.created_at ?? run.createdAt), + parseDateMs(run.updated_at ?? run.updatedAt) + ); + if (runTimestamp > 0 && runTimestamp < config.cutoff_ms) { + continue; + } + let artifactResponse; + try { + artifactResponse = await withRetry((client) => + client.rest.actions.listWorkflowRunArtifacts({ + owner, + repo, + run_id: run.id, + per_page: config.per_page, + }) + ); + } catch (error) { + if (isNotFoundError(error)) continue; + throw error; + } + const matchingArtifacts = (artifactResponse?.data?.artifacts || []).filter((artifact) => + families.includes(artifactFamily(artifact.name)) + ); + artifacts.push(...matchingArtifacts); + if (familiesSatisfied(artifacts, families, config)) { + break; + } + } + } + return dedupeArtifacts(artifacts); +} + async function collectRepoArtifacts({ github, owner, repo, withRetry, options }) { const artifacts = []; const config = normalizeSelectionOptions(options); @@ -428,7 +565,14 @@ async function collectRepoArtifacts({ github, owner, repo, withRetry, options }) break; } } - return artifacts; + const priorityArtifacts = await collectPriorityWorkflowArtifacts({ + github, + owner, + repo, + withRetry, + options, + }); + return dedupeArtifacts([...artifacts, ...priorityArtifacts]); } function parseArgs(argv = process.argv.slice(2)) { @@ -462,6 +606,9 @@ function parseArgs(argv = process.argv.slice(2)) { } else if (arg === '--max-scan-pages') { options.max_scan_pages = next; index += 1; + } else if (arg === '--priority-workflow-runs-per-source') { + options.priority_workflow_runs_per_source = next; + index += 1; } } @@ -524,11 +671,16 @@ module.exports = { DEFAULT_MAX_PER_FAMILY, DEFAULT_MAX_SCAN_PAGES, DEFAULT_MAX_TOTAL, + DEFAULT_PRIORITY_WORKFLOW_RUNS_PER_SOURCE, PRIORITY_METRICS_FAMILIES, + PRIORITY_WORKFLOW_ARTIFACT_SOURCES, SELECTION_SCHEMA, artifactFamily, buildSelectionErrorReport, + collectPriorityWorkflowArtifacts, collectRepoArtifacts, + dedupeArtifacts, + familiesSatisfied, formatArtifactTsv, formatSelectionMarkdown, latestCandidateByFamily, diff --git a/scripts/aggregate_agent_metrics.py b/scripts/aggregate_agent_metrics.py index 7ebe4ae8..855495c1 100755 --- a/scripts/aggregate_agent_metrics.py +++ b/scripts/aggregate_agent_metrics.py @@ -468,9 +468,9 @@ def _verifier_mode_requires_model_metadata(entry: dict[str, Any]) -> bool: def _summarise_keepalive(entries: list[dict[str, Any]]) -> dict[str, Any]: - stop_reasons = Counter() - actions = Counter() - gate_results = Counter() + stop_reasons: Counter[str] = Counter() + actions: Counter[str] = Counter() + gate_results: Counter[str] = Counter() iterations: list[int] = [] prs: set[int] = set() tasks_complete = 0 @@ -513,8 +513,8 @@ def _summarise_keepalive(entries: list[dict[str, Any]]) -> dict[str, Any]: def _summarise_autofix(entries: list[dict[str, Any]]) -> dict[str, Any]: - triggers = Counter() - gate_results = Counter() + triggers: Counter[str] = Counter() + gate_results: Counter[str] = Counter() prs: set[int] = set() fixes_applied = 0 for entry in entries: @@ -542,28 +542,28 @@ def _summarise_verifier( entries: list[dict[str, Any]], ledger_entries: list[dict[str, Any]] | None = None, ) -> dict[str, Any]: - verdicts = Counter() - terminal_dispositions = Counter() - terminal_sources = Counter() - verifier_models = Counter() - model_selection_reasons = Counter() - verifier_cli_versions = Counter() - unsupported_verifier_models = Counter() - unsupported_model_dispositions = Counter() - missing_verifier_model_metadata = Counter() + verdicts: Counter[str] = Counter() + terminal_dispositions: Counter[str] = Counter() + terminal_sources: Counter[str] = Counter() + verifier_models: Counter[str] = Counter() + model_selection_reasons: Counter[str] = Counter() + verifier_cli_versions: Counter[str] = Counter() + unsupported_verifier_models: Counter[str] = Counter() + unsupported_model_dispositions: Counter[str] = Counter() + missing_verifier_model_metadata: Counter[str] = Counter() unsupported_models = _unsupported_verifier_models() model_metadata_required = _verifier_model_metadata_required() model_metadata_required_after = _verifier_model_metadata_required_after() - legacy_missing_verifier_model_metadata = Counter() - verifier_modes = Counter() - ledger_dispositions = Counter() + legacy_missing_verifier_model_metadata: Counter[str] = Counter() + verifier_modes: Counter[str] = Counter() + ledger_dispositions: Counter[str] = Counter() ledger_followup_issues: set[int] = set() ledger_prs: set[int] = set() ledger_needs_human = 0 ledger_chain_depths: list[int] = [] ledger_policy_records = 0 - ledger_policy_actions = Counter() - ledger_policy_triggers = Counter() + ledger_policy_actions: Counter[str] = Counter() + ledger_policy_triggers: Counter[str] = Counter() ledger_policy_depth_limit_exceeded = 0 verifier_run_keys: set[str] = set() prs: set[int] = set() @@ -702,9 +702,9 @@ def _summarise_autopilot(entries: list[dict[str, Any]]) -> dict[str, Any]: step_durations: dict[str, list[float]] = {} step_successes: dict[str, int] = {} step_failures: dict[str, int] = {} - cycle_counts = Counter() - failure_reasons = Counter() - escalation_reasons = Counter() + cycle_counts: Counter[str] = Counter() + failure_reasons: Counter[str] = Counter() + escalation_reasons: Counter[str] = Counter() cycle_records = 0 cycle_steps_attempted = 0 cycle_steps_completed = 0 @@ -782,14 +782,14 @@ def _summarise_autopilot(entries: list[dict[str, Any]]) -> dict[str, Any]: def _summarise_codex_cli_freshness(entries: list[dict[str, Any]]) -> dict[str, Any]: - statuses = Counter() - packages = Counter() - pinned_versions = Counter() - latest_versions = Counter() + statuses: Counter[str] = Counter() + packages: Counter[str] = Counter() + pinned_versions: Counter[str] = Counter() + latest_versions: Counter[str] = Counter() max_major_delta = 0 max_minor_delta = 0 max_patch_delta = 0 - update_targets = Counter() + update_targets: Counter[str] = Counter() for entry in entries: status = _normalize_counter_token(entry.get("status")) statuses[status] += 1 @@ -1088,13 +1088,21 @@ def _artifact_selection_contract(selection: dict[str, Any], selection_path: Path for item in statuses if item["status"] == "missing" or item["selected_count"] <= 0 ] + missing_priority_families = selection.get("missing_priority_families") + if isinstance(missing_priority_families, (list, tuple)): + missing_priority_families = [ + str(family) for family in missing_priority_families if isinstance(family, str) + ] + else: + missing_priority_families = [] + return { "schema": selection.get("schema") or "unknown", "path": selection_path.as_posix(), "status": selection.get("status") or "unknown", "selected_count": _safe_int(selection.get("selected_count")) or 0, "candidate_count": _safe_int(selection.get("candidate_count")) or 0, - "missing_priority_families": list(selection.get("missing_priority_families") or []), + "missing_priority_families": missing_priority_families, "terminal_artifact_families": statuses, "missing_terminal_artifact_families": missing_terminal, }