From d6abc01a6ccbe8053fb513ba6e9d156037b19d20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 15:09:52 +0000 Subject: [PATCH 1/4] fix(news-translate): cap languages per run to stay under Copilot token limit (run #26641603577) Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> --- .github/workflows/news-translate.lock.yml | 44 ++++---- .github/workflows/news-translate.md | 103 +++++++++++++++--- .../news-translate-worklist-contract.test.ts | 39 +++++++ 3 files changed, 149 insertions(+), 37 deletions(-) diff --git a/.github/workflows/news-translate.lock.yml b/.github/workflows/news-translate.lock.yml index e5873dd0ef3..a89da576c07 100644 --- a/.github/workflows/news-translate.lock.yml +++ b/.github/workflows/news-translate.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d4dcb77c134c3c4f5395d15d3a79b59421f27d351d3f7cbde8375c8b2d37f333","body_hash":"9bc2f1054591ea50824bcdac29a7351539aceffb77e009bc114bbb7da3b74d52","compiler_version":"v0.77.1","agent_id":"copilot","agent_model":"claude-sonnet-4.6"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"9eb1fa2f605d7a7f1eec852a7ca3594fde52d133e9fc2eca2a31e031da7d2f65","body_hash":"efa0d180bf270b3f8f429471a15fc329c53d9813f1dc65dabd69268a5cc603c5","compiler_version":"v0.77.1","agent_id":"copilot","agent_model":"claude-sonnet-4.6"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN","IMF_SDMX_SUBSCRIPTION_KEY"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"2d6db59a9f9fdfb957a7753d83270a91e84ecf5c","version":"v0.77.1"},{"repo":"github/gh-aw/actions/setup-cli","sha":"d1aa364b4f4814cd64f0e97fcbf659cc16849230","version":"v0.77.1"},{"repo":"ruby/setup-ruby","sha":"97ecb7b512899eb71ab1bf2310a624c6f1589ac6","version":"v1.308.0"}],"containers":[{"image":"alpine:latest","digest":"sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11","pinned_image":"alpine:latest@sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11"},{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.56","digest":"sha256:83501aa3848cda3f02aa300b90a7444f5a72c23629936c1d78d5c4518e6ccba5","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.56@sha256:83501aa3848cda3f02aa300b90a7444f5a72c23629936c1d78d5c4518e6ccba5"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.56","digest":"sha256:3832c91e64b7dc20ab99024791a7436548275d1c8d6cd0b329b070c9924113c8","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.56@sha256:3832c91e64b7dc20ab99024791a7436548275d1c8d6cd0b329b070c9924113c8"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.56","digest":"sha256:c6db2c30e76b2a30290c6c243567bdb68c43ba6ed7b0bc763e543d7eb3e5b909","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.56@sha256:c6db2c30e76b2a30290c6c243567bdb68c43ba6ed7b0bc763e543d7eb3e5b909"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.20","digest":"sha256:6d79c7b356d7f504229ee693914b46f3920982966b58d96cc15eaf66327faee9","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.20@sha256:6d79c7b356d7f504229ee693914b46f3920982966b58d96cc15eaf66327faee9"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4","digest":"sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4"},{"image":"node:26-alpine","digest":"sha256:30f5a66e7265ef70aac56b4753ffa7905e54eca1084bc25503893ad8e9273f05","pinned_image":"node:26-alpine@sha256:30f5a66e7265ef70aac56b4753ffa7905e54eca1084bc25503893ad8e9273f05"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -22,7 +22,7 @@ # # For more information: https://github.github.com/gh-aw/introduction/overview/ # -# Translates `analysis/daily/**/executive-brief.md` (English source produced by per-type news workflows) into the 13 non-English target languages as sibling `executive-brief_.md` files. Runs three times daily and picks up at most `max_briefs` (default 1) sources per run, **greenfield-first** — sources with one or more missing target files always win batch slots over drift-only sources. Pass 2 is **validator-first**: the bash validator does all structural parity at near-zero model-token cost and the agent only reads back the files it flags, keeping effective-token usage under the Copilot per-session cap after run #26633644372 aborted with a 429 effective-token error before completing the second source — see `07-commit-and-pr.md §Deadline enforcement`. See `TRANSLATION_GUIDE.md §Executive Brief Markdown Translations` for the authoritative content contract. +# Translates `analysis/daily/**/executive-brief.md` (English source produced by per-type news workflows) into the 13 non-English target languages as sibling `executive-brief_.md` files. Runs three times daily and picks up at most `max_briefs` (default 1) sources × `max_langs` (default 7) languages per run, **greenfield-first** — sources with one or more missing target files always win batch slots over drift-only sources, and within a source the still-needed languages are translated missing-first then drift up to `max_langs` (remaining languages drain in later runs). Pass 2 is **validator-first**: the bash validator does all structural parity at near-zero model-token cost and the agent only reads back the files it flags. The per-run `max_langs` language cap plus validator-first Pass 2 keep the session's weighted effective-token usage under the Copilot per-session cap after run #26641603577 aborted with a 429 (27.0M / 25M weighted) before shipping its PR — see `07-commit-and-pr.md §Deadline enforcement`. See `TRANSLATION_GUIDE.md §Executive Brief Markdown Translations` for the authoritative content contract. # # Resolved workflow manifest: # Imports: @@ -94,6 +94,10 @@ on: default: "1" description: "Maximum number of source executive-brief.md files to translate this run (range 1–7; default 1). Hard cap: 7 (keeps under safe-outputs 100-file ceiling with 13 languages = 91 files). Lowered from 3 to 1 after run #26633644372 exceeded the Copilot effective-token cap before completing source #2." required: false + max_langs: + default: "7" + description: "Maximum number of target languages to translate per run (range 1–13; default 7). The agent writes one full file per language in a single Copilot session, so the session's weighted effective-token total scales with the language count. Translating all 13 languages of one source in a single run pushed run #26641603577 to 27.0M weighted effective tokens (> the 25M Copilot per-session cap) and the 429 fired before the PR safe-output — shipping nothing. The worklist builder selects the missing/drifted languages **greenfield-first** and caps them at this value; remaining languages are picked up by the next scheduled run because the source stays classified MISSING/DRIFT until every requested language is present and current. Raise toward 13 only for small briefs or when catching up; lower it if large briefs still approach the cap." + required: false subfolder: description: Restrict the scan to a single document type (propositions, motions, committee-reports, interpellations, evening-analysis, week-ahead, month-ahead, quarter-ahead, year-ahead, election-cycle, weekly-review, monthly-review, realtime-pulse, …). Leave empty to scan all. required: false @@ -227,25 +231,25 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_88a484858af72c9d_EOF' + cat << 'GH_AW_PROMPT_e1a720a761a3238c_EOF' - GH_AW_PROMPT_88a484858af72c9d_EOF + GH_AW_PROMPT_e1a720a761a3238c_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/agentic_workflows_guide.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_88a484858af72c9d_EOF' + cat << 'GH_AW_PROMPT_e1a720a761a3238c_EOF' Tools: add_comment, create_pull_request, missing_tool, missing_data, noop - GH_AW_PROMPT_88a484858af72c9d_EOF + GH_AW_PROMPT_e1a720a761a3238c_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_88a484858af72c9d_EOF' + cat << 'GH_AW_PROMPT_e1a720a761a3238c_EOF' - GH_AW_PROMPT_88a484858af72c9d_EOF + GH_AW_PROMPT_e1a720a761a3238c_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_88a484858af72c9d_EOF' + cat << 'GH_AW_PROMPT_e1a720a761a3238c_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -277,16 +281,16 @@ jobs: - **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it without proper authentication. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches). - GH_AW_PROMPT_88a484858af72c9d_EOF + GH_AW_PROMPT_e1a720a761a3238c_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_88a484858af72c9d_EOF' + cat << 'GH_AW_PROMPT_e1a720a761a3238c_EOF' {{#runtime-import .github/prompts/00-base-contract.md}} {{#runtime-import .github/prompts/01-bash-and-shell-safety.md}} {{#runtime-import .github/prompts/02-mcp-access.md}} {{#runtime-import .github/prompts/07-commit-and-pr.md}} {{#runtime-import .github/workflows/news-translate.md}} - GH_AW_PROMPT_88a484858af72c9d_EOF + GH_AW_PROMPT_e1a720a761a3238c_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -379,9 +383,6 @@ jobs: issues: read pull-requests: read security-events: read - concurrency: - group: "gh-aw-copilot-${{ github.workflow }}-${{ inputs.article_date || 'batch' }}-${{ inputs.subfolder || 'all' }}" - queue: max env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" @@ -467,10 +468,11 @@ jobs: FORCE_RETRANSLATE: ${{ github.event.inputs.force_retranslate }} LANGUAGES_INPUT: ${{ github.event.inputs.languages }} MAX_BRIEFS_INPUT: ${{ github.event.inputs.max_briefs }} + MAX_LANGS_INPUT: ${{ github.event.inputs.max_langs }} SUBFOLDER_INPUT: ${{ github.event.inputs.subfolder }} id: worklist name: Build executive-brief translation work list - run: "set -euo pipefail\n# Resolve target languages.\nLANGUAGES_RAW=\"${LANGUAGES_INPUT:-all-extra}\"\ncase \"$LANGUAGES_RAW\" in\n all-extra) LANGS=\"sv,da,no,fi,de,fr,es,nl,ar,he,ja,ko,zh\" ;;\n nordic-extra) LANGS=\"sv,da,no,fi\" ;;\n eu-extra) LANGS=\"de,fr,es,nl\" ;;\n cjk) LANGS=\"ja,ko,zh\" ;;\n rtl) LANGS=\"ar,he\" ;;\n *) LANGS=\"$LANGUAGES_RAW\" ;;\nesac\necho \"Target languages: $LANGS\"\n\n# Resolve batch cap (1–7 hard range).\nMAX_BRIEFS=\"${MAX_BRIEFS_INPUT:-1}\"\nif ! [[ \"$MAX_BRIEFS\" =~ ^[1-7]$ ]]; then\n echo \"::warning::max_briefs '$MAX_BRIEFS' out of range; clamping to 1\"\n MAX_BRIEFS=1\nfi\necho \"Max source briefs this run: $MAX_BRIEFS\"\n\n# Discover candidate sources.\nROOT=\"analysis/daily\"\n# Worklist file written to GITHUB_WORKSPACE so it is visible inside the\n# AWF container (`--add-dir \"${GITHUB_WORKSPACE}\"`); /tmp on the runner\n# is NOT mounted into the container and the agent cannot read it.\nWORKLIST_FILE=\"${GITHUB_WORKSPACE}/.exec-brief-worklist.txt\"\n\n# Emit a single canonical bundle to BOTH $GITHUB_OUTPUT (for downstream\n# GHA steps) AND $GITHUB_ENV (so `awf --env-all` forwards the values\n# to the agent's bash sandbox — agents have no access to step outputs).\nemit_bundle() {\n local worklist=\"$1\" langs=\"$2\" max=\"$3\" missing=\"$4\" drift=\"$5\"\n {\n echo \"TRANSLATION_WORKLIST=$worklist\"\n echo \"TRANSLATION_LANGS=$langs\"\n echo \"MAX_BRIEFS=$max\"\n echo \"MAX_BRIEFS_RESOLVED=$max\"\n echo \"MISSING_COUNT=$missing\"\n echo \"DRIFT_COUNT=$drift\"\n echo \"EXEC_BRIEF_WORKLIST_FILE=$WORKLIST_FILE\"\n } >> \"$GITHUB_OUTPUT\"\n {\n echo \"TRANSLATION_WORKLIST=$worklist\"\n echo \"TRANSLATION_LANGS=$langs\"\n echo \"MAX_BRIEFS=$max\"\n echo \"MAX_BRIEFS_RESOLVED=$max\"\n echo \"MISSING_COUNT=$missing\"\n echo \"DRIFT_COUNT=$drift\"\n echo \"EXEC_BRIEF_WORKLIST_FILE=$WORKLIST_FILE\"\n } >> \"$GITHUB_ENV\"\n # Always write the file (empty when worklist is empty) so the agent\n # can rely on a stable path.\n : > \"$WORKLIST_FILE\"\n if [ -n \"$worklist\" ]; then\n printf '%s\\n' \"$worklist\" | tr ',' '\\n' > \"$WORKLIST_FILE\"\n fi\n}\n\nif [ ! -d \"$ROOT\" ]; then\n emit_bundle \"\" \"$LANGS\" \"$MAX_BRIEFS\" \"0\" \"0\"\n echo \"No $ROOT directory — nothing to translate.\"\n exit 0\nfi\n\nif [ -n \"${ARTICLE_DATE_INPUT:-}\" ] && [ -n \"${SUBFOLDER_INPUT:-}\" ]; then\n SCAN_PATTERN=\"$ROOT/$ARTICLE_DATE_INPUT/$SUBFOLDER_INPUT\"\nelif [ -n \"${ARTICLE_DATE_INPUT:-}\" ]; then\n SCAN_PATTERN=\"$ROOT/$ARTICLE_DATE_INPUT\"\nelse\n SCAN_PATTERN=\"$ROOT\"\nfi\n\n# Build list of (date, source-path) tuples, oldest date first.\nCANDIDATES=$(find \"$SCAN_PATTERN\" -type f -name 'executive-brief.md' 2>/dev/null \\\n | sort)\n\nif [ -z \"$CANDIDATES\" ]; then\n emit_bundle \"\" \"$LANGS\" \"$MAX_BRIEFS\" \"0\" \"0\"\n echo \"No executive-brief.md sources found under $SCAN_PATTERN.\"\n exit 0\nfi\n\n# For each source, classify it as MISSING (≥1 requested language file absent\n# or empty) or DRIFT (every requested language file present but ≥1 has a\n# stale `` trailer relative to the current source\n# commit SHA). When `force_retranslate=true`, every source with at least\n# one existing target file is reclassified as DRIFT (forces a re-pass);\n# sources missing any target stay in MISSING.\n#\n# Selection policy (greenfield-first):\n# 1. Fill `MAX_BRIEFS` batch slots with MISSING sources, oldest date first\n# (159-source backlog is drained before any drift-fix work).\n# 2. Only if MISSING is empty after the scan, fill remaining slots with\n# DRIFT sources, oldest date first.\n# This guarantees runs never \"redo already-translated briefs\" while the\n# untranslated backlog still has work — see PR description for context.\nMISSING=()\nDRIFT=()\nIFS=',' read -ra LANG_LIST <<< \"$LANGS\"\nwhile IFS= read -r SRC; do\n [ -n \"$SRC\" ] || continue\n DIR=$(dirname \"$SRC\")\n SRC_SHA=$(git log -1 --format=%H -- \"$SRC\" 2>/dev/null || echo \"\")\n HAS_MISSING=0\n HAS_DRIFT=0\n for L in \"${LANG_LIST[@]}\"; do\n TGT=\"$DIR/executive-brief_${L}.md\"\n if [ ! -s \"$TGT\" ]; then\n HAS_MISSING=1\n continue\n fi\n if [ -n \"$SRC_SHA\" ] && ! grep -q \"\" \"$TGT\" 2>/dev/null; then\n HAS_DRIFT=1\n fi\n done\n if [ \"$HAS_MISSING\" -eq 1 ]; then\n MISSING+=(\"$SRC\")\n elif [ \"${FORCE_RETRANSLATE:-false}\" = \"true\" ] || [ \"$HAS_DRIFT\" -eq 1 ]; then\n DRIFT+=(\"$SRC\")\n fi\ndone <<< \"$CANDIDATES\"\n\nMISSING_COUNT=\"${#MISSING[@]}\"\nDRIFT_COUNT=\"${#DRIFT[@]}\"\necho \"Backlog classification: MISSING=$MISSING_COUNT DRIFT=$DRIFT_COUNT\"\n\n# Fill batch: MISSING first (greenfield), DRIFT only if capacity remains.\nWORK=()\nfor SRC in \"${MISSING[@]}\"; do\n WORK+=(\"$SRC\")\n if [ \"${#WORK[@]}\" -ge \"$MAX_BRIEFS\" ]; then break; fi\ndone\nif [ \"${#WORK[@]}\" -lt \"$MAX_BRIEFS\" ]; then\n for SRC in \"${DRIFT[@]}\"; do\n WORK+=(\"$SRC\")\n if [ \"${#WORK[@]}\" -ge \"$MAX_BRIEFS\" ]; then break; fi\n done\nfi\n\nif [ \"${#WORK[@]}\" -eq 0 ]; then\n emit_bundle \"\" \"$LANGS\" \"$MAX_BRIEFS\" \"$MISSING_COUNT\" \"$DRIFT_COUNT\"\n echo \"All discovered sources are up to date for the requested languages.\"\n exit 0\nfi\n\n# Categorise the chosen batch for the log line: how many slots were filled\n# with greenfield (MISSING) vs drift-fix (DRIFT) work.\nWORK_MISSING=0\nWORK_DRIFT=0\nfor SRC in \"${WORK[@]}\"; do\n for M in \"${MISSING[@]:-}\"; do\n if [ \"$M\" = \"$SRC\" ]; then WORK_MISSING=$((WORK_MISSING+1)); continue 2; fi\n done\n WORK_DRIFT=$((WORK_DRIFT+1))\ndone\n\nWORKLIST_JOINED=$(printf '%s\\n' \"${WORK[@]}\" | paste -sd ',' -)\nemit_bundle \"$WORKLIST_JOINED\" \"$LANGS\" \"$MAX_BRIEFS\" \"$MISSING_COUNT\" \"$DRIFT_COUNT\"\necho \"✅ Selected ${#WORK[@]} source brief(s) for translation into [$LANGS]\"\necho \" (greenfield=$WORK_MISSING / drift-fix=$WORK_DRIFT; total backlog: MISSING=$MISSING_COUNT DRIFT=$DRIFT_COUNT)\"\necho \" Worklist file (agent reads this): $WORKLIST_FILE\"\nprintf ' %s\\n' \"${WORK[@]}\"\n" + run: "set -euo pipefail\n# Resolve target languages.\nLANGUAGES_RAW=\"${LANGUAGES_INPUT:-all-extra}\"\ncase \"$LANGUAGES_RAW\" in\n all-extra) LANGS=\"sv,da,no,fi,de,fr,es,nl,ar,he,ja,ko,zh\" ;;\n nordic-extra) LANGS=\"sv,da,no,fi\" ;;\n eu-extra) LANGS=\"de,fr,es,nl\" ;;\n cjk) LANGS=\"ja,ko,zh\" ;;\n rtl) LANGS=\"ar,he\" ;;\n *) LANGS=\"$LANGUAGES_RAW\" ;;\nesac\necho \"Target languages: $LANGS\"\n\n# Resolve batch cap (1–7 hard range).\nMAX_BRIEFS=\"${MAX_BRIEFS_INPUT:-1}\"\nif ! [[ \"$MAX_BRIEFS\" =~ ^[1-7]$ ]]; then\n echo \"::warning::max_briefs '$MAX_BRIEFS' out of range; clamping to 1\"\n MAX_BRIEFS=1\nfi\necho \"Max source briefs this run: $MAX_BRIEFS\"\n\n# Resolve per-run language cap (1–13 hard range; default 7). The agent\n# writes one full file per language in a single Copilot session, so the\n# session's weighted effective-token total scales with this count.\n# Translating all 13 languages of one source in a single run pushed run\n# #26641603577 to 27.0M weighted effective tokens (> the 25M cap) and the\n# 429 fired before the PR safe-output. The batching block below caps the\n# languages handled per run to this value, greenfield-first.\nMAX_LANGS=\"${MAX_LANGS_INPUT:-7}\"\nif ! [[ \"$MAX_LANGS\" =~ ^([1-9]|1[0-3])$ ]]; then\n echo \"::warning::max_langs '$MAX_LANGS' out of range; clamping to 7\"\n MAX_LANGS=7\nfi\necho \"Max target languages this run: $MAX_LANGS\"\n\n# Discover candidate sources.\nROOT=\"analysis/daily\"\n# Worklist file written to GITHUB_WORKSPACE so it is visible inside the\n# AWF container (`--add-dir \"${GITHUB_WORKSPACE}\"`); /tmp on the runner\n# is NOT mounted into the container and the agent cannot read it.\nWORKLIST_FILE=\"${GITHUB_WORKSPACE}/.exec-brief-worklist.txt\"\n\n# Emit a single canonical bundle to BOTH $GITHUB_OUTPUT (for downstream\n# GHA steps) AND $GITHUB_ENV (so `awf --env-all` forwards the values\n# to the agent's bash sandbox — agents have no access to step outputs).\nemit_bundle() {\n local worklist=\"$1\" langs=\"$2\" max=\"$3\" missing=\"$4\" drift=\"$5\"\n {\n echo \"TRANSLATION_WORKLIST=$worklist\"\n echo \"TRANSLATION_LANGS=$langs\"\n echo \"MAX_BRIEFS=$max\"\n echo \"MAX_BRIEFS_RESOLVED=$max\"\n echo \"MISSING_COUNT=$missing\"\n echo \"DRIFT_COUNT=$drift\"\n echo \"EXEC_BRIEF_WORKLIST_FILE=$WORKLIST_FILE\"\n } >> \"$GITHUB_OUTPUT\"\n {\n echo \"TRANSLATION_WORKLIST=$worklist\"\n echo \"TRANSLATION_LANGS=$langs\"\n echo \"MAX_BRIEFS=$max\"\n echo \"MAX_BRIEFS_RESOLVED=$max\"\n echo \"MISSING_COUNT=$missing\"\n echo \"DRIFT_COUNT=$drift\"\n echo \"EXEC_BRIEF_WORKLIST_FILE=$WORKLIST_FILE\"\n } >> \"$GITHUB_ENV\"\n # Always write the file (empty when worklist is empty) so the agent\n # can rely on a stable path.\n : > \"$WORKLIST_FILE\"\n if [ -n \"$worklist\" ]; then\n printf '%s\\n' \"$worklist\" | tr ',' '\\n' > \"$WORKLIST_FILE\"\n fi\n}\n\nif [ ! -d \"$ROOT\" ]; then\n emit_bundle \"\" \"$LANGS\" \"$MAX_BRIEFS\" \"0\" \"0\"\n echo \"No $ROOT directory — nothing to translate.\"\n exit 0\nfi\n\nif [ -n \"${ARTICLE_DATE_INPUT:-}\" ] && [ -n \"${SUBFOLDER_INPUT:-}\" ]; then\n SCAN_PATTERN=\"$ROOT/$ARTICLE_DATE_INPUT/$SUBFOLDER_INPUT\"\nelif [ -n \"${ARTICLE_DATE_INPUT:-}\" ]; then\n SCAN_PATTERN=\"$ROOT/$ARTICLE_DATE_INPUT\"\nelse\n SCAN_PATTERN=\"$ROOT\"\nfi\n\n# Build list of (date, source-path) tuples, oldest date first.\nCANDIDATES=$(find \"$SCAN_PATTERN\" -type f -name 'executive-brief.md' 2>/dev/null \\\n | sort)\n\nif [ -z \"$CANDIDATES\" ]; then\n emit_bundle \"\" \"$LANGS\" \"$MAX_BRIEFS\" \"0\" \"0\"\n echo \"No executive-brief.md sources found under $SCAN_PATTERN.\"\n exit 0\nfi\n\n# For each source, classify it as MISSING (≥1 requested language file absent\n# or empty) or DRIFT (every requested language file present but ≥1 has a\n# stale `` trailer relative to the current source\n# commit SHA). When `force_retranslate=true`, every source with at least\n# one existing target file is reclassified as DRIFT (forces a re-pass);\n# sources missing any target stay in MISSING.\n#\n# Selection policy (greenfield-first):\n# 1. Fill `MAX_BRIEFS` batch slots with MISSING sources, oldest date first\n# (159-source backlog is drained before any drift-fix work).\n# 2. Only if MISSING is empty after the scan, fill remaining slots with\n# DRIFT sources, oldest date first.\n# This guarantees runs never \"redo already-translated briefs\" while the\n# untranslated backlog still has work — see PR description for context.\nMISSING=()\nDRIFT=()\nIFS=',' read -ra LANG_LIST <<< \"$LANGS\"\nwhile IFS= read -r SRC; do\n [ -n \"$SRC\" ] || continue\n DIR=$(dirname \"$SRC\")\n SRC_SHA=$(git log -1 --format=%H -- \"$SRC\" 2>/dev/null || echo \"\")\n HAS_MISSING=0\n HAS_DRIFT=0\n for L in \"${LANG_LIST[@]}\"; do\n TGT=\"$DIR/executive-brief_${L}.md\"\n if [ ! -s \"$TGT\" ]; then\n HAS_MISSING=1\n continue\n fi\n if [ -n \"$SRC_SHA\" ] && ! grep -q \"\" \"$TGT\" 2>/dev/null; then\n HAS_DRIFT=1\n fi\n done\n if [ \"$HAS_MISSING\" -eq 1 ]; then\n MISSING+=(\"$SRC\")\n elif [ \"${FORCE_RETRANSLATE:-false}\" = \"true\" ] || [ \"$HAS_DRIFT\" -eq 1 ]; then\n DRIFT+=(\"$SRC\")\n fi\ndone <<< \"$CANDIDATES\"\n\nMISSING_COUNT=\"${#MISSING[@]}\"\nDRIFT_COUNT=\"${#DRIFT[@]}\"\necho \"Backlog classification: MISSING=$MISSING_COUNT DRIFT=$DRIFT_COUNT\"\n\n# Fill batch: MISSING first (greenfield), DRIFT only if capacity remains.\nWORK=()\nfor SRC in \"${MISSING[@]}\"; do\n WORK+=(\"$SRC\")\n if [ \"${#WORK[@]}\" -ge \"$MAX_BRIEFS\" ]; then break; fi\ndone\nif [ \"${#WORK[@]}\" -lt \"$MAX_BRIEFS\" ]; then\n for SRC in \"${DRIFT[@]}\"; do\n WORK+=(\"$SRC\")\n if [ \"${#WORK[@]}\" -ge \"$MAX_BRIEFS\" ]; then break; fi\n done\nfi\n\nif [ \"${#WORK[@]}\" -eq 0 ]; then\n emit_bundle \"\" \"$LANGS\" \"$MAX_BRIEFS\" \"$MISSING_COUNT\" \"$DRIFT_COUNT\"\n echo \"All discovered sources are up to date for the requested languages.\"\n exit 0\nfi\n\n# Categorise the chosen batch for the log line: how many slots were filled\n# with greenfield (MISSING) vs drift-fix (DRIFT) work.\nWORK_MISSING=0\nWORK_DRIFT=0\nfor SRC in \"${WORK[@]}\"; do\n for M in \"${MISSING[@]:-}\"; do\n if [ \"$M\" = \"$SRC\" ]; then WORK_MISSING=$((WORK_MISSING+1)); continue 2; fi\n done\n WORK_DRIFT=$((WORK_DRIFT+1))\ndone\n\nWORKLIST_JOINED=$(printf '%s\\n' \"${WORK[@]}\" | paste -sd ',' -)\n\n# --- Per-run language batching (effective-token control) ---\n# Across the selected batch, collect the requested languages that still\n# NEED work — missing target file first (greenfield), then drift — in the\n# canonical order of $LANGS, and translate at most $MAX_LANGS of them this\n# run. This bounds the number of full files the agent writes in one\n# Copilot session (the dominant driver of the weighted effective-token\n# total that aborted run #26641603577 with a 429 before the PR shipped).\n# The remaining languages are picked up by the next scheduled run because\n# every selected source stays classified MISSING/DRIFT until all requested\n# languages are present and current.\nNEED_MISSING=()\nNEED_DRIFT=()\nfor L in \"${LANG_LIST[@]}\"; do\n L_MISSING=0\n L_DRIFT=0\n for SRC in \"${WORK[@]}\"; do\n DIR=$(dirname \"$SRC\")\n SRC_SHA=$(git log -1 --format=%H -- \"$SRC\" 2>/dev/null || echo \"\")\n TGT=\"$DIR/executive-brief_${L}.md\"\n if [ ! -s \"$TGT\" ]; then\n L_MISSING=1\n continue\n fi\n if [ \"${FORCE_RETRANSLATE:-false}\" = \"true\" ]; then\n L_DRIFT=1\n elif [ -n \"$SRC_SHA\" ] && ! grep -q \"\" \"$TGT\" 2>/dev/null; then\n L_DRIFT=1\n fi\n done\n if [ \"$L_MISSING\" -eq 1 ]; then\n NEED_MISSING+=(\"$L\")\n elif [ \"$L_DRIFT\" -eq 1 ]; then\n NEED_DRIFT+=(\"$L\")\n fi\ndone\nNEED_LANGS=(\"${NEED_MISSING[@]}\" \"${NEED_DRIFT[@]}\")\n# Fallback: if nothing was flagged as needing work (edge case), fall back\n# to the full requested list so the run never emits an empty language set.\nif [ \"${#NEED_LANGS[@]}\" -eq 0 ]; then\n NEED_LANGS=(\"${LANG_LIST[@]}\")\nfi\nRUN_LANG_LIST=()\nfor L in \"${NEED_LANGS[@]}\"; do\n RUN_LANG_LIST+=(\"$L\")\n if [ \"${#RUN_LANG_LIST[@]}\" -ge \"$MAX_LANGS\" ]; then break; fi\ndone\nRUN_LANGS=$(IFS=','; echo \"${RUN_LANG_LIST[*]}\")\necho \"Per-run language batch (cap $MAX_LANGS): [$RUN_LANGS] of requested [$LANGS]\"\n\nemit_bundle \"$WORKLIST_JOINED\" \"$RUN_LANGS\" \"$MAX_BRIEFS\" \"$MISSING_COUNT\" \"$DRIFT_COUNT\"\necho \"✅ Selected ${#WORK[@]} source brief(s) for translation into [$RUN_LANGS]\"\necho \" (greenfield=$WORK_MISSING / drift-fix=$WORK_DRIFT; total backlog: MISSING=$MISSING_COUNT DRIFT=$DRIFT_COUNT)\"\necho \" Worklist file (agent reads this): $WORKLIST_FILE\"\nprintf ' %s\\n' \"${WORK[@]}\"\n" # Cache memory file share configuration from frontmatter processed below - name: Create cache-memory directory @@ -586,9 +588,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_b9357cabb8a4b854_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_219bf7abbb8a117b_EOF' {"add_comment":{"max":1},"create_pull_request":{"draft":false,"expires":336,"fallback_as_issue":true,"if_no_changes":"warn","labels":["agentic-news","translation","executive-brief"],"max":1,"max_patch_files":100,"max_patch_size":10240,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"allowed"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_b9357cabb8a4b854_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_219bf7abbb8a117b_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -825,7 +827,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_e35b51558562da79_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_57d4c8261daa562d_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "agenticworkflows": { @@ -941,7 +943,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_e35b51558562da79_EOF + GH_AW_MCP_CONFIG_57d4c8261daa562d_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -1438,7 +1440,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: WORKFLOW_NAME: "News: Translate Executive Briefs" - WORKFLOW_DESCRIPTION: "Translates `analysis/daily/**/executive-brief.md` (English source produced by per-type news workflows) into the 13 non-English target languages as sibling `executive-brief_.md` files. Runs three times daily and picks up at most `max_briefs` (default 1) sources per run, **greenfield-first** — sources with one or more missing target files always win batch slots over drift-only sources. Pass 2 is **validator-first**: the bash validator does all structural parity at near-zero model-token cost and the agent only reads back the files it flags, keeping effective-token usage under the Copilot per-session cap after run #26633644372 aborted with a 429 effective-token error before completing the second source — see `07-commit-and-pr.md §Deadline enforcement`. See `TRANSLATION_GUIDE.md §Executive Brief Markdown Translations` for the authoritative content contract." + WORKFLOW_DESCRIPTION: "Translates `analysis/daily/**/executive-brief.md` (English source produced by per-type news workflows) into the 13 non-English target languages as sibling `executive-brief_.md` files. Runs three times daily and picks up at most `max_briefs` (default 1) sources × `max_langs` (default 7) languages per run, **greenfield-first** — sources with one or more missing target files always win batch slots over drift-only sources, and within a source the still-needed languages are translated missing-first then drift up to `max_langs` (remaining languages drain in later runs). Pass 2 is **validator-first**: the bash validator does all structural parity at near-zero model-token cost and the agent only reads back the files it flags. The per-run `max_langs` language cap plus validator-first Pass 2 keep the session's weighted effective-token usage under the Copilot per-session cap after run #26641603577 aborted with a 429 (27.0M / 25M weighted) before shipping its PR — see `07-commit-and-pr.md §Deadline enforcement`. See `TRANSLATION_GUIDE.md §Executive Brief Markdown Translations` for the authoritative content contract." HAS_PATCH: ${{ needs.agent.outputs.has_patch }} with: script: | diff --git a/.github/workflows/news-translate.md b/.github/workflows/news-translate.md index 7ba5d41f0b7..9c8d75664b3 100644 --- a/.github/workflows/news-translate.md +++ b/.github/workflows/news-translate.md @@ -1,6 +1,6 @@ --- name: "News: Translate Executive Briefs" -description: "Translates `analysis/daily/**/executive-brief.md` (English source produced by per-type news workflows) into the 13 non-English target languages as sibling `executive-brief_.md` files. Runs three times daily and picks up at most `max_briefs` (default 1) sources per run, **greenfield-first** — sources with one or more missing target files always win batch slots over drift-only sources. Pass 2 is **validator-first**: the bash validator does all structural parity at near-zero model-token cost and the agent only reads back the files it flags, keeping effective-token usage under the Copilot per-session cap after run #26633644372 aborted with a 429 effective-token error before completing the second source — see `07-commit-and-pr.md §Deadline enforcement`. See `TRANSLATION_GUIDE.md §Executive Brief Markdown Translations` for the authoritative content contract." +description: "Translates `analysis/daily/**/executive-brief.md` (English source produced by per-type news workflows) into the 13 non-English target languages as sibling `executive-brief_.md` files. Runs three times daily and picks up at most `max_briefs` (default 1) sources × `max_langs` (default 7) languages per run, **greenfield-first** — sources with one or more missing target files always win batch slots over drift-only sources, and within a source the still-needed languages are translated missing-first then drift up to `max_langs` (remaining languages drain in later runs). Pass 2 is **validator-first**: the bash validator does all structural parity at near-zero model-token cost and the agent only reads back the files it flags. The per-run `max_langs` language cap plus validator-first Pass 2 keep the session's weighted effective-token usage under the Copilot per-session cap after run #26641603577 aborted with a 429 (27.0M / 25M weighted) before shipping its PR — see `07-commit-and-pr.md §Deadline enforcement`. See `TRANSLATION_GUIDE.md §Executive Brief Markdown Translations` for the authoritative content contract." strict: false imports: - ../prompts/00-base-contract.md @@ -32,6 +32,10 @@ on: description: 'Maximum number of source executive-brief.md files to translate this run (range 1–7; default 1). Hard cap: 7 (keeps under safe-outputs 100-file ceiling with 13 languages = 91 files). Lowered from 3 to 1 after run #26633644372 exceeded the Copilot effective-token cap before completing source #2.' required: false default: '1' + max_langs: + description: 'Maximum number of target languages to translate per run (range 1–13; default 7). The agent writes one full file per language in a single Copilot session, so the session''s weighted effective-token total scales with the language count. Translating all 13 languages of one source in a single run pushed run #26641603577 to 27.0M weighted effective tokens (> the 25M Copilot per-session cap) and the 429 fired before the PR safe-output — shipping nothing. The worklist builder selects the missing/drifted languages **greenfield-first** and caps them at this value; remaining languages are picked up by the next scheduled run because the source stays classified MISSING/DRIFT until every requested language is present and current. Raise toward 13 only for small briefs or when catching up; lower it if large briefs still approach the cap.' + required: false + default: '7' force_retranslate: description: 'When true, ignore the `` drift check and re-translate every matching source. Use sparingly — drains the safe-outputs file budget fast.' required: false @@ -305,6 +309,7 @@ steps: SUBFOLDER_INPUT: ${{ github.event.inputs.subfolder }} LANGUAGES_INPUT: ${{ github.event.inputs.languages }} MAX_BRIEFS_INPUT: ${{ github.event.inputs.max_briefs }} + MAX_LANGS_INPUT: ${{ github.event.inputs.max_langs }} FORCE_RETRANSLATE: ${{ github.event.inputs.force_retranslate }} run: | set -euo pipefail @@ -328,6 +333,20 @@ steps: fi echo "Max source briefs this run: $MAX_BRIEFS" + # Resolve per-run language cap (1–13 hard range; default 7). The agent + # writes one full file per language in a single Copilot session, so the + # session's weighted effective-token total scales with this count. + # Translating all 13 languages of one source in a single run pushed run + # #26641603577 to 27.0M weighted effective tokens (> the 25M cap) and the + # 429 fired before the PR safe-output. The batching block below caps the + # languages handled per run to this value, greenfield-first. + MAX_LANGS="${MAX_LANGS_INPUT:-7}" + if ! [[ "$MAX_LANGS" =~ ^([1-9]|1[0-3])$ ]]; then + echo "::warning::max_langs '$MAX_LANGS' out of range; clamping to 7" + MAX_LANGS=7 + fi + echo "Max target languages this run: $MAX_LANGS" + # Discover candidate sources. ROOT="analysis/daily" # Worklist file written to GITHUB_WORKSPACE so it is visible inside the @@ -465,8 +484,58 @@ steps: done WORKLIST_JOINED=$(printf '%s\n' "${WORK[@]}" | paste -sd ',' -) - emit_bundle "$WORKLIST_JOINED" "$LANGS" "$MAX_BRIEFS" "$MISSING_COUNT" "$DRIFT_COUNT" - echo "✅ Selected ${#WORK[@]} source brief(s) for translation into [$LANGS]" + + # --- Per-run language batching (effective-token control) --- + # Across the selected batch, collect the requested languages that still + # NEED work — missing target file first (greenfield), then drift — in the + # canonical order of $LANGS, and translate at most $MAX_LANGS of them this + # run. This bounds the number of full files the agent writes in one + # Copilot session (the dominant driver of the weighted effective-token + # total that aborted run #26641603577 with a 429 before the PR shipped). + # The remaining languages are picked up by the next scheduled run because + # every selected source stays classified MISSING/DRIFT until all requested + # languages are present and current. + NEED_MISSING=() + NEED_DRIFT=() + for L in "${LANG_LIST[@]}"; do + L_MISSING=0 + L_DRIFT=0 + for SRC in "${WORK[@]}"; do + DIR=$(dirname "$SRC") + SRC_SHA=$(git log -1 --format=%H -- "$SRC" 2>/dev/null || echo "") + TGT="$DIR/executive-brief_${L}.md" + if [ ! -s "$TGT" ]; then + L_MISSING=1 + continue + fi + if [ "${FORCE_RETRANSLATE:-false}" = "true" ]; then + L_DRIFT=1 + elif [ -n "$SRC_SHA" ] && ! grep -q "" "$TGT" 2>/dev/null; then + L_DRIFT=1 + fi + done + if [ "$L_MISSING" -eq 1 ]; then + NEED_MISSING+=("$L") + elif [ "$L_DRIFT" -eq 1 ]; then + NEED_DRIFT+=("$L") + fi + done + NEED_LANGS=("${NEED_MISSING[@]}" "${NEED_DRIFT[@]}") + # Fallback: if nothing was flagged as needing work (edge case), fall back + # to the full requested list so the run never emits an empty language set. + if [ "${#NEED_LANGS[@]}" -eq 0 ]; then + NEED_LANGS=("${LANG_LIST[@]}") + fi + RUN_LANG_LIST=() + for L in "${NEED_LANGS[@]}"; do + RUN_LANG_LIST+=("$L") + if [ "${#RUN_LANG_LIST[@]}" -ge "$MAX_LANGS" ]; then break; fi + done + RUN_LANGS=$(IFS=','; echo "${RUN_LANG_LIST[*]}") + echo "Per-run language batch (cap $MAX_LANGS): [$RUN_LANGS] of requested [$LANGS]" + + emit_bundle "$WORKLIST_JOINED" "$RUN_LANGS" "$MAX_BRIEFS" "$MISSING_COUNT" "$DRIFT_COUNT" + echo "✅ Selected ${#WORK[@]} source brief(s) for translation into [$RUN_LANGS]" echo " (greenfield=$WORK_MISSING / drift-fix=$WORK_DRIFT; total backlog: MISSING=$MISSING_COUNT DRIFT=$DRIFT_COUNT)" echo " Worklist file (agent reads this): $WORKLIST_FILE" printf ' %s\n' "${WORK[@]}" @@ -485,13 +554,13 @@ This workflow is the **sole writer** of `analysis/daily/$DATE/$SUB/executive-bri ## Pipeline 1. **Wall-clock checkpoint (mandatory)** — as the **very first bash call** in this run, record `JOB_START=$(date +%s)` and export it. After every source completes Pass 2, re-read `NOW=$(date +%s)` and compute `ELAPSED_MIN=$(( (NOW - JOB_START) / 60 ))`; print it. If `ELAPSED_MIN >= 35`, stop selecting new sources and proceed directly to the validate → stage → commit → `safeoutputs___create_pull_request` flow regardless of remaining `$TRANSLATION_WORKLIST` entries. If `ELAPSED_MIN >= 42`, **halt all translation work immediately** and execute the [`07-commit-and-pr.md §Emergency deadline order of operations`](../prompts/07-commit-and-pr.md) bash block to ship a partial PR before Timer A (60-min job `timeout-minutes`) and Timer B (~60-min Copilot API session) fire. A partial PR is always better than zero output — see run [#26633644372](https://github.com/Hack23/riksdagsmonitor/actions/runs/26633644372), where source #2 failed with `CAPIError: 429 Maximum effective tokens exceeded`. -2. The `Build executive-brief translation work list` pre-flight step has already exported four env vars into the agent sandbox: **`$TRANSLATION_WORKLIST`** (comma-separated repo-relative paths — read this verbatim, or parse the same content line-by-line from the file at **`$EXEC_BRIEF_WORKLIST_FILE`** at `${GITHUB_WORKSPACE}/.exec-brief-worklist.txt`), **`$TRANSLATION_LANGS`** (comma-separated language codes — honours operator-supplied `inputs.languages` after preset expansion), **`$MAX_BRIEFS`** / **`$MAX_BRIEFS_RESOLVED`** (1–7, clamp-applied), and the audit counters **`$MISSING_COUNT`** / **`$DRIFT_COUNT`**. **Do not re-scan the filesystem** to rebuild this list — the pre-flight step has already honoured `inputs.article_date`, `inputs.subfolder`, `inputs.languages`, `inputs.max_briefs`, and `inputs.force_retranslate`. The selector is **greenfield-first**: sources with one or more missing target files (`MISSING`) always win the `max_briefs` batch slots over sources that only need drift-fixes (`DRIFT`); `DRIFT` is only consulted when `MISSING` is empty. If `$TRANSLATION_WORKLIST` is empty (so `$MISSING_COUNT=0` **and** `$DRIFT_COUNT=0`), nothing is pending — proceed to improvement-mode (re-validate every existing translation against the current `` trailer and fix any drift the validator flags). If still nothing changes after improvement-mode, follow `07-commit-and-pr.md §No-op policy`. +2. The `Build executive-brief translation work list` pre-flight step has already exported four env vars into the agent sandbox: **`$TRANSLATION_WORKLIST`** (comma-separated repo-relative paths — read this verbatim, or parse the same content line-by-line from the file at **`$EXEC_BRIEF_WORKLIST_FILE`** at `${GITHUB_WORKSPACE}/.exec-brief-worklist.txt`), **`$TRANSLATION_LANGS`** (comma-separated language codes — this is the **per-run language batch**, already capped at `inputs.max_langs` (default 7) and pre-selected **greenfield-first** from the languages that still need work; translate **exactly and only** these codes — do not expand to the full 13-language set), **`$MAX_BRIEFS`** / **`$MAX_BRIEFS_RESOLVED`** (1–7, clamp-applied), and the audit counters **`$MISSING_COUNT`** / **`$DRIFT_COUNT`**. **Do not re-scan the filesystem** to rebuild this list — the pre-flight step has already honoured `inputs.article_date`, `inputs.subfolder`, `inputs.languages`, `inputs.max_briefs`, `inputs.max_langs`, and `inputs.force_retranslate`. Languages omitted from `$TRANSLATION_LANGS` this run are intentionally deferred to the next scheduled run (the source stays MISSING/DRIFT until all requested languages are present and current) — this per-run cap is what keeps the session under the Copilot weighted effective-token cap that aborted run [#26641603577](https://github.com/Hack23/riksdagsmonitor/actions/runs/26641603577). The selector is **greenfield-first**: sources with one or more missing target files (`MISSING`) always win the `max_briefs` batch slots over sources that only need drift-fixes (`DRIFT`); `DRIFT` is only consulted when `MISSING` is empty. If `$TRANSLATION_WORKLIST` is empty (so `$MISSING_COUNT=0` **and** `$DRIFT_COUNT=0`), nothing is pending — proceed to improvement-mode (re-validate every existing translation against the current `` trailer and fix any drift the validator flags). If still nothing changes after improvement-mode, follow `07-commit-and-pr.md §No-op policy`. 3. For each source path in `$TRANSLATION_WORKLIST` (or each line of `$EXEC_BRIEF_WORKLIST_FILE`): 1. **Pass 1 — translate**: Read the source `executive-brief.md` in full **once**. For every language in `TRANSLATION_LANGS`, produce `analysis/daily/$DATE/$SUB/executive-brief_.md` following the TRANSLATION_GUIDE rules — **write each file with one `edit` tool call per language** (never via `python3`, `bash` heredocs, or shell redirection — see `01-bash-and-shell-safety.md §File creation & overwrite strategy`). Apply the per-language tone register from TRANSLATION_GUIDE **at write time** (so Pass 2 never needs a full re-read just to adjust register), and for `ar`/`he` start the file with ``. Preserve every verbatim block (YAML, HTML comments except `source-sha`, `dok_id` codes, Mermaid DSL bodies, code fences, URLs, file paths, evidence-anchor canonical column values). Translate every always-translate block (prose, headings, list items, table cell text, image alt-text, BLUF, decisions, link text). **Do not** read a freshly written translation back into model context to "confirm" it — the validator in Pass 2 is the authoritative gate. - 2. **Pass 2 — validate-first & targeted refine**: This is the **primary token-efficiency control**. Run `npx tsx scripts/validate-executive-brief-translations.ts --source ` in the runtime shell. That validator already performs every structural-parity check at near-zero model-token cost: heading / table-row / code-fence / Mermaid-block count parity, `dok_id` and URL set equality, banned-English-phrase detection (`Executive Brief`, `Decisions`, `Confidence`, `BLUF`, …), the `` marker for `ar`/`he`, and the `` trailer. **Do not read passing translations back into model context** — reading all 13 files back in full is exactly what drove run [#26633644372](https://github.com/Hack23/riksdagsmonitor/actions/runs/26633644372) to `7.6M` effective tokens and a `429 Maximum effective tokens exceeded` abort. Only read back (and re-translate / fix) the specific `executive-brief_.md` files the validator reports as failing, then re-run the validator until it is clean for the requested languages. + 2. **Pass 2 — validate-first & targeted refine**: This is the **primary token-efficiency control**. Run `npx tsx scripts/validate-executive-brief-translations.ts --source --lang "$TRANSLATION_LANGS"` in the runtime shell (the `--lang` scope restricts validation to this run's language batch so deferred languages are not reported as missing). That validator already performs every structural-parity check at near-zero model-token cost: heading / table-row / code-fence / Mermaid-block count parity, `dok_id` and URL set equality, banned-English-phrase detection (`Executive Brief`, `Decisions`, `Confidence`, `BLUF`, …), the `` marker for `ar`/`he`, and the `` trailer. **Do not read passing translations back into model context** — reading every target file back in full is exactly what drove run [#26633644372](https://github.com/Hack23/riksdagsmonitor/actions/runs/26633644372) to `7.6M` effective tokens and a `429 Maximum effective tokens exceeded` abort. Only read back (and re-translate / fix) the specific `executive-brief_.md` files the validator reports as failing, then re-run the validator until it is clean for the requested languages. 3. **Append the source-revision marker**: compute `SRC_SHA=$(git log -1 --format=%H -- )` in the runtime shell. Use the `edit` tool to add or replace the final `` line at the end of every translation file (consistent with the file-write contract — no `>>` or `echo`). This is the drift signal future runs use to decide whether to retranslate. 4. **Title post-processing**: run `npx tsx scripts/postprocess-translated-brief.ts analysis/daily/$DATE/$SUB/executive-brief_*.md` to re-apply the renderer's `cleanArticleTitle` pipeline on every translated H1. This strips locale-specific boilerplate prefixes (`Exekutiv sammanfattning — `, `Zusammenfassung — `, `执行摘要:…`) and trailing date suffixes that the Pass 1 translation may have left in place. The helper only rewrites when the cleaned title differs from the source; it never falls back to a BLUF synthesis. Verify the script reports `✓` or `✏️` for every file (no `✗`). - 5. **Final validate**: re-run `npx tsx scripts/validate-executive-brief-translations.ts --source ` (when available) to confirm the title post-processing in sub-step 4 did not break parity, or fall back to the structural sanity checks listed in TRANSLATION_GUIDE §Acceptance checklist. Fix any failures by re-translating only the offending language (read back only that file). Do not commit a file that fails validation. + 5. **Final validate**: re-run `npx tsx scripts/validate-executive-brief-translations.ts --source --lang "$TRANSLATION_LANGS"` (when available) to confirm the title post-processing in sub-step 4 did not break parity, or fall back to the structural sanity checks listed in TRANSLATION_GUIDE §Acceptance checklist. Fix any failures by re-translating only the offending language (read back only that file). Do not commit a file that fails validation. 6. **Re-evaluate the wall-clock checkpoint** (from step 1) before starting the next source. Never start a new source after `ELAPSED_MIN >= 35`. 4. Stage every newly written / refreshed `executive-brief_.md` file. **Do not** touch `executive-brief.md` itself, any HTML under `news/`, any file under `analysis/daily/*/article.md`, or any non-translation file. 5. Call `safeoutputs___create_pull_request` **exactly once** covering the whole batch, **by agent minute 42 at the latest** (hard deadline 45 per `07-commit-and-pr.md §Deadline enforcement`). The branch prefix `news/translate/briefs/` is enforced by the safe-outputs config (`07-commit-and-pr.md`). @@ -501,7 +570,8 @@ This workflow is the **sole writer** of `analysis/daily/$DATE/$SUB/executive-bri - `article_date` — optional. Restrict scanning to a single date folder. - `subfolder` — optional. Restrict scanning to a single document type folder. - `languages` — default `all-extra` (= `sv,da,no,fi,de,fr,es,nl,ar,he,ja,ko,zh`). Aliases: `nordic-extra`, `eu-extra`, `cjk`, `rtl`, `all-extra`. Comma list also accepted. -- `max_briefs` — default `1`, range `1–7`. Caps the number of **source** files processed in this run; total file output = `max_briefs × |languages|` (≤ 91 for the default 13-language target, safely under the 100-file safe-outputs cap). The default was lowered from 3 to 1 after run #26633644372 hit the Copilot effective-token limit before finishing source #2. +- `max_briefs` — default `1`, range `1–7`. Caps the number of **source** files processed in this run; total file output = `max_briefs × |TRANSLATION_LANGS|` (≤ `7 × 7 = 49` at the defaults, safely under the 100-file safe-outputs cap). The default was lowered from 3 to 1 after run #26633644372 hit the Copilot effective-token limit before finishing source #2. +- `max_langs` — default `7`, range `1–13`. Caps the number of **target languages** translated this run. The builder pre-selects the still-needed languages **greenfield-first** (missing files first, then drift) and truncates to this many. Translating all 13 in one Copilot session pushed run #26641603577 to 27.0M weighted effective tokens (> the 25M cap), aborting before the PR shipped; deferred languages are filled by the next scheduled run. Raise toward 13 for small briefs / catch-up; lower it if large briefs still approach the cap. - `force_retranslate` — default `false`. When `true`, every requested language is rewritten even if the `` marker matches. - `analysis_depth` — default `standard`. Echoed into the validator output for parity with content workflows; does not change translation behaviour. @@ -511,15 +581,16 @@ This workflow is the **sole writer** of `analysis/daily/$DATE/$SUB/executive-bri |----------|-------|--------| | Average source size | ~650 words (range 232–2040) | `wc -w` over `analysis/daily/**/executive-brief.md` | | Per-language Pass 1 write + validator gate | ~45–75 s | Sonnet-class translation budget; Pass 2 is bash-validated, not re-read into context | -| Per-source wall time (13 languages, sequential) | ~13–20 min | derived | -| Default `max_briefs` | 1 source | keeps effective-token usage under Copilot's per-session cap while preserving PR headroom | -| Per-source token sink (pre-fix) | ~3.8M effective tokens | run #26633644372 burned 7.6M on ~1.5 sources, dominated by Pass 2 reading all 13 files back into context | -| Primary token lever | validator-first Pass 2 | structural parity moved to the bash validator; only validator-flagged files are read back | -| Per-run file output | 1 × 13 = **13 files** | well under safe-outputs `max-patch-files: 100` | -| Daily throughput (3 runs) | up to 3 sources / **39 files** | raise `max_briefs` once the validator-first token footprint is measured below the cap | -| Current backlog drain (≥159 untranslated sources) | ~27 days at default settings, greenfield-first | linear | - -If a run is behind schedule at agent minute 30 with translations still pending, the agent MUST trim `max_briefs` downward (drop the last selected source) rather than skip the Pass 2 validator gate — quality over completeness. A partial PR is always better than missing Timer A (job `timeout-minutes: 60`). The default `max_briefs=1` was set after run [#26633644372](https://github.com/Hack23/riksdagsmonitor/actions/runs/26633644372) repeatedly hit `CAPIError: 429 Maximum effective tokens exceeded` while attempting source #2; the validator-first Pass 2 above removes the read-back token sink that caused it. +| Per-source wall time (≤`max_langs` languages, sequential) | ~8–15 min at `max_langs=7` | derived | +| Default `max_briefs` / `max_langs` | 1 source / 7 languages | keeps the weighted effective-token total under Copilot's per-session cap while preserving PR headroom | +| Per-session token driver | turn count ≈ languages translated | each language adds Pass-1/validator turns; the full per-turn tool-schema context (github + data MCP servers) is re-sent every turn, so cumulative weighted tokens scale ~linearly with the language count | +| Run #26641603577 (pre-fix) | 13 langs → 27.0M weighted / 25M cap → 429 before PR | gh-aw raw 7.67M; the weighted metric is ~3.5× raw and exceeded the cap; **no PR shipped** | +| Primary token levers | validator-first Pass 2 **+ `max_langs` per-run language cap** | structural parity stays in the bash validator; the language cap bounds the per-session turn count / output | +| Per-run file output | `max_briefs × max_langs` = `1 × 7` = **7 files** (default) | well under safe-outputs `max-patch-files: 100` | +| Daily throughput (3 runs) | up to **21 language-files / day** at defaults | raise `max_briefs` / `max_langs` once the per-session footprint is measured comfortably below the cap | +| Current backlog drain (≥159 untranslated sources) | greenfield-first, oldest source + missing languages first | linear | + +If a run is behind schedule at agent minute 30 with translations still pending, the agent MUST trim `max_briefs` downward (drop the last selected source) rather than skip the Pass 2 validator gate — quality over completeness. A partial PR is always better than missing Timer A (job `timeout-minutes: 60`). The default `max_briefs=1` was set after run [#26633644372](https://github.com/Hack23/riksdagsmonitor/actions/runs/26633644372) repeatedly hit `CAPIError: 429 Maximum effective tokens exceeded` while attempting source #2; the validator-first Pass 2 above removes the read-back token sink, and the `max_langs=7` per-run language cap (added after run [#26641603577](https://github.com/Hack23/riksdagsmonitor/actions/runs/26641603577)) bounds the per-session turn count so the weighted effective-token total stays under the 25M cap and the PR safe-output is reached. ## Rules specific to this workflow diff --git a/tests/news-translate-worklist-contract.test.ts b/tests/news-translate-worklist-contract.test.ts index ada8d5d3b24..2beac61216f 100644 --- a/tests/news-translate-worklist-contract.test.ts +++ b/tests/news-translate-worklist-contract.test.ts @@ -118,4 +118,43 @@ describe("news-translate worklist propagation to agent sandbox", () => { ); expect(legacyEmptyMatches).toBeNull(); }); + + it("caps the per-run language batch via max_langs (effective-token control)", () => { + // Run #26641603577 aborted with a 429 weighted-effective-token error + // because all 13 languages of one source were translated in a single + // Copilot session. The builder must expose a max_langs input, resolve + // it (default 7), and truncate the still-needed languages to it before + // emitting TRANSLATION_LANGS. + // 1. workflow_dispatch input exists with default '7'. + expect(TRANSLATE_MD).toMatch(/max_langs:/); + expect(TRANSLATE_LOCK).toMatch(/max_langs:/); + // 2. The builder forwards the input into the step env and resolves it. + expect(TRANSLATE_MD).toMatch( + /MAX_LANGS_INPUT:\s*\$\{\{\s*github\.event\.inputs\.max_langs\s*\}\}/, + ); + expect(TRANSLATE_MD).toMatch(/MAX_LANGS="\$\{MAX_LANGS_INPUT:-7\}"/); + // 3. The capped batch (RUN_LANGS) — not the full requested $LANGS — is + // what gets emitted as the run's TRANSLATION_LANGS. + expect(TRANSLATE_MD).toMatch(/RUN_LANG_LIST\b/); + expect(TRANSLATE_MD).toMatch(/-ge "\$MAX_LANGS"/); + expect(TRANSLATE_MD).toMatch(/emit_bundle "\$WORKLIST_JOINED" "\$RUN_LANGS"/); + }); + + it("scopes the agent's validator calls to the per-run language batch", () => { + // When languages are capped, the validator must only check the run's + // batch (--lang "$TRANSLATION_LANGS"), otherwise deferred languages are + // reported as missing and the agent re-reads them, defeating the cap. + const promptBodyStart = TRANSLATE_MD.indexOf("# 🌐 News Translate"); + const promptBody = TRANSLATE_MD.slice(promptBodyStart); + const validatorCalls = promptBody.match( + /validate-executive-brief-translations\.ts --source /g, + ); + expect(validatorCalls).not.toBeNull(); + // Every `--source ` validator call in the agent body must carry + // the --lang "$TRANSLATION_LANGS" scope. + const scopedCalls = promptBody.match( + /validate-executive-brief-translations\.ts --source --lang "\$TRANSLATION_LANGS"/g, + ); + expect(scopedCalls?.length).toBe(validatorCalls?.length); + }); }); From af09b1141f0fbecdb21f51a86a7e5c9db2e24475 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 16:37:17 +0000 Subject: [PATCH 2/4] test(render-lib): tolerate intended rm-article--with-toc modifier in e2e article-class assertion Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> --- tests/render-lib.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/render-lib.test.ts b/tests/render-lib.test.ts index 4ebcda2828e..a1ba1cdd2b0 100644 --- a/tests/render-lib.test.ts +++ b/tests/render-lib.test.ts @@ -1487,7 +1487,15 @@ describe('render-lib — renderArticleHtml (end-to-end)', () => { artifactsUsed: ['executive-brief.md', 'risk-assessment.md'], }); expect(html.startsWith('')).toBe(true); - expect(html).toContain('
` + // class. A two-section article (Executive Brief + Risk Assessment) + // clears the ≥2-heading TOC threshold (see article-scannability.test.ts), + // so the wrapper also gains the `rm-article--with-toc` modifier. Match + // the type class while tolerating that optional, intended modifier + // instead of pinning the closing quote directly after the type slug. + expect(html).toMatch( + /
Propositions

'); expect(html).toContain('

Propositions 2099-01-01

'); From 7e8661e72e70a079053c4ceff67a6f50ebddec9d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 17:11:24 +0000 Subject: [PATCH 3/4] Trim news-translate per-turn MCP/tool schema; re-baseline max_langs to 13 Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> --- .github/workflows/news-translate.lock.yml | 146 ++++-------------- .github/workflows/news-translate.md | 75 +++++---- tests/news-translate-mcp-surface.test.ts | 95 ++++++++++++ .../news-translate-worklist-contract.test.ts | 15 +- 4 files changed, 167 insertions(+), 164 deletions(-) create mode 100644 tests/news-translate-mcp-surface.test.ts diff --git a/.github/workflows/news-translate.lock.yml b/.github/workflows/news-translate.lock.yml index a89da576c07..d82a91b29fa 100644 --- a/.github/workflows/news-translate.lock.yml +++ b/.github/workflows/news-translate.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"9eb1fa2f605d7a7f1eec852a7ca3594fde52d133e9fc2eca2a31e031da7d2f65","body_hash":"efa0d180bf270b3f8f429471a15fc329c53d9813f1dc65dabd69268a5cc603c5","compiler_version":"v0.77.1","agent_id":"copilot","agent_model":"claude-sonnet-4.6"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN","IMF_SDMX_SUBSCRIPTION_KEY"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"2d6db59a9f9fdfb957a7753d83270a91e84ecf5c","version":"v0.77.1"},{"repo":"github/gh-aw/actions/setup-cli","sha":"d1aa364b4f4814cd64f0e97fcbf659cc16849230","version":"v0.77.1"},{"repo":"ruby/setup-ruby","sha":"97ecb7b512899eb71ab1bf2310a624c6f1589ac6","version":"v1.308.0"}],"containers":[{"image":"alpine:latest","digest":"sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11","pinned_image":"alpine:latest@sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11"},{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.56","digest":"sha256:83501aa3848cda3f02aa300b90a7444f5a72c23629936c1d78d5c4518e6ccba5","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.56@sha256:83501aa3848cda3f02aa300b90a7444f5a72c23629936c1d78d5c4518e6ccba5"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.56","digest":"sha256:3832c91e64b7dc20ab99024791a7436548275d1c8d6cd0b329b070c9924113c8","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.56@sha256:3832c91e64b7dc20ab99024791a7436548275d1c8d6cd0b329b070c9924113c8"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.56","digest":"sha256:c6db2c30e76b2a30290c6c243567bdb68c43ba6ed7b0bc763e543d7eb3e5b909","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.56@sha256:c6db2c30e76b2a30290c6c243567bdb68c43ba6ed7b0bc763e543d7eb3e5b909"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.20","digest":"sha256:6d79c7b356d7f504229ee693914b46f3920982966b58d96cc15eaf66327faee9","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.20@sha256:6d79c7b356d7f504229ee693914b46f3920982966b58d96cc15eaf66327faee9"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4","digest":"sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4"},{"image":"node:26-alpine","digest":"sha256:30f5a66e7265ef70aac56b4753ffa7905e54eca1084bc25503893ad8e9273f05","pinned_image":"node:26-alpine@sha256:30f5a66e7265ef70aac56b4753ffa7905e54eca1084bc25503893ad8e9273f05"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d5205966d1ed5a0d7c821cd30be8245e29441f166d043a36022fa0143a96ce57","body_hash":"6839e5236b2f07ba014b136db6aae94c5c393e71c609eb032719aeb0fbf94beb","compiler_version":"v0.77.1","agent_id":"copilot","agent_model":"claude-sonnet-4.6"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN","IMF_SDMX_SUBSCRIPTION_KEY"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"2d6db59a9f9fdfb957a7753d83270a91e84ecf5c","version":"v0.77.1"},{"repo":"ruby/setup-ruby","sha":"97ecb7b512899eb71ab1bf2310a624c6f1589ac6","version":"v1.308.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.56","digest":"sha256:83501aa3848cda3f02aa300b90a7444f5a72c23629936c1d78d5c4518e6ccba5","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.56@sha256:83501aa3848cda3f02aa300b90a7444f5a72c23629936c1d78d5c4518e6ccba5"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.56","digest":"sha256:3832c91e64b7dc20ab99024791a7436548275d1c8d6cd0b329b070c9924113c8","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.56@sha256:3832c91e64b7dc20ab99024791a7436548275d1c8d6cd0b329b070c9924113c8"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.56","digest":"sha256:c6db2c30e76b2a30290c6c243567bdb68c43ba6ed7b0bc763e543d7eb3e5b909","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.56@sha256:c6db2c30e76b2a30290c6c243567bdb68c43ba6ed7b0bc763e543d7eb3e5b909"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.20","digest":"sha256:6d79c7b356d7f504229ee693914b46f3920982966b58d96cc15eaf66327faee9","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.20@sha256:6d79c7b356d7f504229ee693914b46f3920982966b58d96cc15eaf66327faee9"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4","digest":"sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -22,7 +22,7 @@ # # For more information: https://github.github.com/gh-aw/introduction/overview/ # -# Translates `analysis/daily/**/executive-brief.md` (English source produced by per-type news workflows) into the 13 non-English target languages as sibling `executive-brief_.md` files. Runs three times daily and picks up at most `max_briefs` (default 1) sources × `max_langs` (default 7) languages per run, **greenfield-first** — sources with one or more missing target files always win batch slots over drift-only sources, and within a source the still-needed languages are translated missing-first then drift up to `max_langs` (remaining languages drain in later runs). Pass 2 is **validator-first**: the bash validator does all structural parity at near-zero model-token cost and the agent only reads back the files it flags. The per-run `max_langs` language cap plus validator-first Pass 2 keep the session's weighted effective-token usage under the Copilot per-session cap after run #26641603577 aborted with a 429 (27.0M / 25M weighted) before shipping its PR — see `07-commit-and-pr.md §Deadline enforcement`. See `TRANSLATION_GUIDE.md §Executive Brief Markdown Translations` for the authoritative content contract. +# Translates `analysis/daily/**/executive-brief.md` (English source produced by per-type news workflows) into the 13 non-English target languages as sibling `executive-brief_.md` files. Runs three times daily and picks up at most `max_briefs` (default 1) sources × `max_langs` (default 13) languages per run, **greenfield-first** — sources with one or more missing target files always win batch slots over drift-only sources, and within a source the still-needed languages are translated missing-first then drift up to `max_langs` (remaining languages drain in later runs). Pass 2 is **validator-first**: the bash validator does all structural parity at near-zero model-token cost and the agent only reads back the files it flags. This workflow only uses bash/edit/safe-outputs — its MCP/tool surface is deliberately minimal so the per-turn tool-schema context (the ~3.5× weighting driver that aborted run #26641603577 at 27.0M / 25M weighted) no longer dominates; wall-clock (Timer A/B) and the 100-file safe-outputs cap, not the token budget, are now the governing limits — see `07-commit-and-pr.md §Deadline enforcement`. See `TRANSLATION_GUIDE.md §Executive Brief Markdown Translations` for the authoritative content contract. # # Resolved workflow manifest: # Imports: @@ -49,17 +49,14 @@ # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 # - github/gh-aw-actions/setup@2d6db59a9f9fdfb957a7753d83270a91e84ecf5c # v0.77.1 -# - github/gh-aw/actions/setup-cli@d1aa364b4f4814cd64f0e97fcbf659cc16849230 # v0.77.1 # - ruby/setup-ruby@97ecb7b512899eb71ab1bf2310a624c6f1589ac6 # v1.308.0 # # Container images used: -# - alpine:latest@sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11 # - ghcr.io/github/gh-aw-firewall/agent:0.25.56@sha256:83501aa3848cda3f02aa300b90a7444f5a72c23629936c1d78d5c4518e6ccba5 # - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.56@sha256:3832c91e64b7dc20ab99024791a7436548275d1c8d6cd0b329b070c9924113c8 # - ghcr.io/github/gh-aw-firewall/squid:0.25.56@sha256:c6db2c30e76b2a30290c6c243567bdb68c43ba6ed7b0bc763e543d7eb3e5b909 # - ghcr.io/github/gh-aw-mcpg:v0.3.20@sha256:6d79c7b356d7f504229ee693914b46f3920982966b58d96cc15eaf66327faee9 # - ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 -# - node:26-alpine@sha256:30f5a66e7265ef70aac56b4753ffa7905e54eca1084bc25503893ad8e9273f05 # - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 name: "News: Translate Executive Briefs" @@ -95,8 +92,8 @@ on: description: "Maximum number of source executive-brief.md files to translate this run (range 1–7; default 1). Hard cap: 7 (keeps under safe-outputs 100-file ceiling with 13 languages = 91 files). Lowered from 3 to 1 after run #26633644372 exceeded the Copilot effective-token cap before completing source #2." required: false max_langs: - default: "7" - description: "Maximum number of target languages to translate per run (range 1–13; default 7). The agent writes one full file per language in a single Copilot session, so the session's weighted effective-token total scales with the language count. Translating all 13 languages of one source in a single run pushed run #26641603577 to 27.0M weighted effective tokens (> the 25M Copilot per-session cap) and the 429 fired before the PR safe-output — shipping nothing. The worklist builder selects the missing/drifted languages **greenfield-first** and caps them at this value; remaining languages are picked up by the next scheduled run because the source stays classified MISSING/DRIFT until every requested language is present and current. Raise toward 13 only for small briefs or when catching up; lower it if large briefs still approach the cap." + default: "13" + description: "Maximum number of target languages to translate per run (range 1–13; default 13 = a full source in one run). The agent writes one full file per language in a single Copilot session, so the session's weighted effective-token total scales with the language count **times the per-turn tool-schema surface**. The dominant cost was the latter: run #26641603577 hit 27.0M weighted (> the 25M Copilot per-session cap) at 13 languages while the workflow still loaded the full github `toolsets:[all]` + three data-MCP servers on every turn. That dead-weight schema was removed (the agent only uses bash/edit/safe-outputs), collapsing the per-turn footprint, so a full 13-language source now fits in one run. The worklist builder selects the missing/drifted languages **greenfield-first** and caps them at this value; remaining languages are picked up by the next scheduled run because the source stays classified MISSING/DRIFT until every requested language is present and current. Lower this first if an unusually large brief still approaches the cap." required: false subfolder: description: Restrict the scan to a single document type (propositions, motions, committee-reports, interpellations, evening-analysis, week-ahead, month-ahead, quarter-ahead, year-ahead, election-cycle, weekly-review, monthly-review, realtime-pulse, …). Leave empty to scan all. @@ -231,25 +228,24 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_e1a720a761a3238c_EOF' + cat << 'GH_AW_PROMPT_67776bb4e21a0135_EOF' - GH_AW_PROMPT_e1a720a761a3238c_EOF + GH_AW_PROMPT_67776bb4e21a0135_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/agentic_workflows_guide.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_e1a720a761a3238c_EOF' + cat << 'GH_AW_PROMPT_67776bb4e21a0135_EOF' Tools: add_comment, create_pull_request, missing_tool, missing_data, noop - GH_AW_PROMPT_e1a720a761a3238c_EOF + GH_AW_PROMPT_67776bb4e21a0135_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_e1a720a761a3238c_EOF' + cat << 'GH_AW_PROMPT_67776bb4e21a0135_EOF' - GH_AW_PROMPT_e1a720a761a3238c_EOF + GH_AW_PROMPT_67776bb4e21a0135_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_e1a720a761a3238c_EOF' + cat << 'GH_AW_PROMPT_67776bb4e21a0135_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -281,16 +277,16 @@ jobs: - **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it without proper authentication. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches). - GH_AW_PROMPT_e1a720a761a3238c_EOF + GH_AW_PROMPT_67776bb4e21a0135_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_e1a720a761a3238c_EOF' + cat << 'GH_AW_PROMPT_67776bb4e21a0135_EOF' {{#runtime-import .github/prompts/00-base-contract.md}} {{#runtime-import .github/prompts/01-bash-and-shell-safety.md}} {{#runtime-import .github/prompts/02-mcp-access.md}} {{#runtime-import .github/prompts/07-commit-and-pr.md}} {{#runtime-import .github/workflows/news-translate.md}} - GH_AW_PROMPT_e1a720a761a3238c_EOF + GH_AW_PROMPT_67776bb4e21a0135_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -383,6 +379,9 @@ jobs: issues: read pull-requests: read security-events: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}-${{ inputs.article_date || 'batch' }}-${{ inputs.subfolder || 'all' }}" + queue: max env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" @@ -472,7 +471,7 @@ jobs: SUBFOLDER_INPUT: ${{ github.event.inputs.subfolder }} id: worklist name: Build executive-brief translation work list - run: "set -euo pipefail\n# Resolve target languages.\nLANGUAGES_RAW=\"${LANGUAGES_INPUT:-all-extra}\"\ncase \"$LANGUAGES_RAW\" in\n all-extra) LANGS=\"sv,da,no,fi,de,fr,es,nl,ar,he,ja,ko,zh\" ;;\n nordic-extra) LANGS=\"sv,da,no,fi\" ;;\n eu-extra) LANGS=\"de,fr,es,nl\" ;;\n cjk) LANGS=\"ja,ko,zh\" ;;\n rtl) LANGS=\"ar,he\" ;;\n *) LANGS=\"$LANGUAGES_RAW\" ;;\nesac\necho \"Target languages: $LANGS\"\n\n# Resolve batch cap (1–7 hard range).\nMAX_BRIEFS=\"${MAX_BRIEFS_INPUT:-1}\"\nif ! [[ \"$MAX_BRIEFS\" =~ ^[1-7]$ ]]; then\n echo \"::warning::max_briefs '$MAX_BRIEFS' out of range; clamping to 1\"\n MAX_BRIEFS=1\nfi\necho \"Max source briefs this run: $MAX_BRIEFS\"\n\n# Resolve per-run language cap (1–13 hard range; default 7). The agent\n# writes one full file per language in a single Copilot session, so the\n# session's weighted effective-token total scales with this count.\n# Translating all 13 languages of one source in a single run pushed run\n# #26641603577 to 27.0M weighted effective tokens (> the 25M cap) and the\n# 429 fired before the PR safe-output. The batching block below caps the\n# languages handled per run to this value, greenfield-first.\nMAX_LANGS=\"${MAX_LANGS_INPUT:-7}\"\nif ! [[ \"$MAX_LANGS\" =~ ^([1-9]|1[0-3])$ ]]; then\n echo \"::warning::max_langs '$MAX_LANGS' out of range; clamping to 7\"\n MAX_LANGS=7\nfi\necho \"Max target languages this run: $MAX_LANGS\"\n\n# Discover candidate sources.\nROOT=\"analysis/daily\"\n# Worklist file written to GITHUB_WORKSPACE so it is visible inside the\n# AWF container (`--add-dir \"${GITHUB_WORKSPACE}\"`); /tmp on the runner\n# is NOT mounted into the container and the agent cannot read it.\nWORKLIST_FILE=\"${GITHUB_WORKSPACE}/.exec-brief-worklist.txt\"\n\n# Emit a single canonical bundle to BOTH $GITHUB_OUTPUT (for downstream\n# GHA steps) AND $GITHUB_ENV (so `awf --env-all` forwards the values\n# to the agent's bash sandbox — agents have no access to step outputs).\nemit_bundle() {\n local worklist=\"$1\" langs=\"$2\" max=\"$3\" missing=\"$4\" drift=\"$5\"\n {\n echo \"TRANSLATION_WORKLIST=$worklist\"\n echo \"TRANSLATION_LANGS=$langs\"\n echo \"MAX_BRIEFS=$max\"\n echo \"MAX_BRIEFS_RESOLVED=$max\"\n echo \"MISSING_COUNT=$missing\"\n echo \"DRIFT_COUNT=$drift\"\n echo \"EXEC_BRIEF_WORKLIST_FILE=$WORKLIST_FILE\"\n } >> \"$GITHUB_OUTPUT\"\n {\n echo \"TRANSLATION_WORKLIST=$worklist\"\n echo \"TRANSLATION_LANGS=$langs\"\n echo \"MAX_BRIEFS=$max\"\n echo \"MAX_BRIEFS_RESOLVED=$max\"\n echo \"MISSING_COUNT=$missing\"\n echo \"DRIFT_COUNT=$drift\"\n echo \"EXEC_BRIEF_WORKLIST_FILE=$WORKLIST_FILE\"\n } >> \"$GITHUB_ENV\"\n # Always write the file (empty when worklist is empty) so the agent\n # can rely on a stable path.\n : > \"$WORKLIST_FILE\"\n if [ -n \"$worklist\" ]; then\n printf '%s\\n' \"$worklist\" | tr ',' '\\n' > \"$WORKLIST_FILE\"\n fi\n}\n\nif [ ! -d \"$ROOT\" ]; then\n emit_bundle \"\" \"$LANGS\" \"$MAX_BRIEFS\" \"0\" \"0\"\n echo \"No $ROOT directory — nothing to translate.\"\n exit 0\nfi\n\nif [ -n \"${ARTICLE_DATE_INPUT:-}\" ] && [ -n \"${SUBFOLDER_INPUT:-}\" ]; then\n SCAN_PATTERN=\"$ROOT/$ARTICLE_DATE_INPUT/$SUBFOLDER_INPUT\"\nelif [ -n \"${ARTICLE_DATE_INPUT:-}\" ]; then\n SCAN_PATTERN=\"$ROOT/$ARTICLE_DATE_INPUT\"\nelse\n SCAN_PATTERN=\"$ROOT\"\nfi\n\n# Build list of (date, source-path) tuples, oldest date first.\nCANDIDATES=$(find \"$SCAN_PATTERN\" -type f -name 'executive-brief.md' 2>/dev/null \\\n | sort)\n\nif [ -z \"$CANDIDATES\" ]; then\n emit_bundle \"\" \"$LANGS\" \"$MAX_BRIEFS\" \"0\" \"0\"\n echo \"No executive-brief.md sources found under $SCAN_PATTERN.\"\n exit 0\nfi\n\n# For each source, classify it as MISSING (≥1 requested language file absent\n# or empty) or DRIFT (every requested language file present but ≥1 has a\n# stale `` trailer relative to the current source\n# commit SHA). When `force_retranslate=true`, every source with at least\n# one existing target file is reclassified as DRIFT (forces a re-pass);\n# sources missing any target stay in MISSING.\n#\n# Selection policy (greenfield-first):\n# 1. Fill `MAX_BRIEFS` batch slots with MISSING sources, oldest date first\n# (159-source backlog is drained before any drift-fix work).\n# 2. Only if MISSING is empty after the scan, fill remaining slots with\n# DRIFT sources, oldest date first.\n# This guarantees runs never \"redo already-translated briefs\" while the\n# untranslated backlog still has work — see PR description for context.\nMISSING=()\nDRIFT=()\nIFS=',' read -ra LANG_LIST <<< \"$LANGS\"\nwhile IFS= read -r SRC; do\n [ -n \"$SRC\" ] || continue\n DIR=$(dirname \"$SRC\")\n SRC_SHA=$(git log -1 --format=%H -- \"$SRC\" 2>/dev/null || echo \"\")\n HAS_MISSING=0\n HAS_DRIFT=0\n for L in \"${LANG_LIST[@]}\"; do\n TGT=\"$DIR/executive-brief_${L}.md\"\n if [ ! -s \"$TGT\" ]; then\n HAS_MISSING=1\n continue\n fi\n if [ -n \"$SRC_SHA\" ] && ! grep -q \"\" \"$TGT\" 2>/dev/null; then\n HAS_DRIFT=1\n fi\n done\n if [ \"$HAS_MISSING\" -eq 1 ]; then\n MISSING+=(\"$SRC\")\n elif [ \"${FORCE_RETRANSLATE:-false}\" = \"true\" ] || [ \"$HAS_DRIFT\" -eq 1 ]; then\n DRIFT+=(\"$SRC\")\n fi\ndone <<< \"$CANDIDATES\"\n\nMISSING_COUNT=\"${#MISSING[@]}\"\nDRIFT_COUNT=\"${#DRIFT[@]}\"\necho \"Backlog classification: MISSING=$MISSING_COUNT DRIFT=$DRIFT_COUNT\"\n\n# Fill batch: MISSING first (greenfield), DRIFT only if capacity remains.\nWORK=()\nfor SRC in \"${MISSING[@]}\"; do\n WORK+=(\"$SRC\")\n if [ \"${#WORK[@]}\" -ge \"$MAX_BRIEFS\" ]; then break; fi\ndone\nif [ \"${#WORK[@]}\" -lt \"$MAX_BRIEFS\" ]; then\n for SRC in \"${DRIFT[@]}\"; do\n WORK+=(\"$SRC\")\n if [ \"${#WORK[@]}\" -ge \"$MAX_BRIEFS\" ]; then break; fi\n done\nfi\n\nif [ \"${#WORK[@]}\" -eq 0 ]; then\n emit_bundle \"\" \"$LANGS\" \"$MAX_BRIEFS\" \"$MISSING_COUNT\" \"$DRIFT_COUNT\"\n echo \"All discovered sources are up to date for the requested languages.\"\n exit 0\nfi\n\n# Categorise the chosen batch for the log line: how many slots were filled\n# with greenfield (MISSING) vs drift-fix (DRIFT) work.\nWORK_MISSING=0\nWORK_DRIFT=0\nfor SRC in \"${WORK[@]}\"; do\n for M in \"${MISSING[@]:-}\"; do\n if [ \"$M\" = \"$SRC\" ]; then WORK_MISSING=$((WORK_MISSING+1)); continue 2; fi\n done\n WORK_DRIFT=$((WORK_DRIFT+1))\ndone\n\nWORKLIST_JOINED=$(printf '%s\\n' \"${WORK[@]}\" | paste -sd ',' -)\n\n# --- Per-run language batching (effective-token control) ---\n# Across the selected batch, collect the requested languages that still\n# NEED work — missing target file first (greenfield), then drift — in the\n# canonical order of $LANGS, and translate at most $MAX_LANGS of them this\n# run. This bounds the number of full files the agent writes in one\n# Copilot session (the dominant driver of the weighted effective-token\n# total that aborted run #26641603577 with a 429 before the PR shipped).\n# The remaining languages are picked up by the next scheduled run because\n# every selected source stays classified MISSING/DRIFT until all requested\n# languages are present and current.\nNEED_MISSING=()\nNEED_DRIFT=()\nfor L in \"${LANG_LIST[@]}\"; do\n L_MISSING=0\n L_DRIFT=0\n for SRC in \"${WORK[@]}\"; do\n DIR=$(dirname \"$SRC\")\n SRC_SHA=$(git log -1 --format=%H -- \"$SRC\" 2>/dev/null || echo \"\")\n TGT=\"$DIR/executive-brief_${L}.md\"\n if [ ! -s \"$TGT\" ]; then\n L_MISSING=1\n continue\n fi\n if [ \"${FORCE_RETRANSLATE:-false}\" = \"true\" ]; then\n L_DRIFT=1\n elif [ -n \"$SRC_SHA\" ] && ! grep -q \"\" \"$TGT\" 2>/dev/null; then\n L_DRIFT=1\n fi\n done\n if [ \"$L_MISSING\" -eq 1 ]; then\n NEED_MISSING+=(\"$L\")\n elif [ \"$L_DRIFT\" -eq 1 ]; then\n NEED_DRIFT+=(\"$L\")\n fi\ndone\nNEED_LANGS=(\"${NEED_MISSING[@]}\" \"${NEED_DRIFT[@]}\")\n# Fallback: if nothing was flagged as needing work (edge case), fall back\n# to the full requested list so the run never emits an empty language set.\nif [ \"${#NEED_LANGS[@]}\" -eq 0 ]; then\n NEED_LANGS=(\"${LANG_LIST[@]}\")\nfi\nRUN_LANG_LIST=()\nfor L in \"${NEED_LANGS[@]}\"; do\n RUN_LANG_LIST+=(\"$L\")\n if [ \"${#RUN_LANG_LIST[@]}\" -ge \"$MAX_LANGS\" ]; then break; fi\ndone\nRUN_LANGS=$(IFS=','; echo \"${RUN_LANG_LIST[*]}\")\necho \"Per-run language batch (cap $MAX_LANGS): [$RUN_LANGS] of requested [$LANGS]\"\n\nemit_bundle \"$WORKLIST_JOINED\" \"$RUN_LANGS\" \"$MAX_BRIEFS\" \"$MISSING_COUNT\" \"$DRIFT_COUNT\"\necho \"✅ Selected ${#WORK[@]} source brief(s) for translation into [$RUN_LANGS]\"\necho \" (greenfield=$WORK_MISSING / drift-fix=$WORK_DRIFT; total backlog: MISSING=$MISSING_COUNT DRIFT=$DRIFT_COUNT)\"\necho \" Worklist file (agent reads this): $WORKLIST_FILE\"\nprintf ' %s\\n' \"${WORK[@]}\"\n" + run: "set -euo pipefail\n# Resolve target languages.\nLANGUAGES_RAW=\"${LANGUAGES_INPUT:-all-extra}\"\ncase \"$LANGUAGES_RAW\" in\n all-extra) LANGS=\"sv,da,no,fi,de,fr,es,nl,ar,he,ja,ko,zh\" ;;\n nordic-extra) LANGS=\"sv,da,no,fi\" ;;\n eu-extra) LANGS=\"de,fr,es,nl\" ;;\n cjk) LANGS=\"ja,ko,zh\" ;;\n rtl) LANGS=\"ar,he\" ;;\n *) LANGS=\"$LANGUAGES_RAW\" ;;\nesac\necho \"Target languages: $LANGS\"\n\n# Resolve batch cap (1–7 hard range).\nMAX_BRIEFS=\"${MAX_BRIEFS_INPUT:-1}\"\nif ! [[ \"$MAX_BRIEFS\" =~ ^[1-7]$ ]]; then\n echo \"::warning::max_briefs '$MAX_BRIEFS' out of range; clamping to 1\"\n MAX_BRIEFS=1\nfi\necho \"Max source briefs this run: $MAX_BRIEFS\"\n\n# Resolve per-run language cap (1–13 hard range; default 13 = a full source\n# in one run). The agent writes one full file per language in a single Copilot\n# session, so the weighted effective-token total scales with the language count\n# times the per-turn tool-schema surface. The latter was the dominant cost: run\n# #26641603577 hit 27.0M weighted (> the 25M cap) at 13 languages while the heavy\n# github toolsets:[all] + three data-MCP server schemas were still re-sent every\n# turn. That dead-weight surface was removed (this workflow only uses bash/edit/\n# safe-outputs), so a full 13-language source now fits under the cap. The batching\n# block below caps the languages handled per run to this value, greenfield-first.\nMAX_LANGS=\"${MAX_LANGS_INPUT:-13}\"\nif ! [[ \"$MAX_LANGS\" =~ ^([1-9]|1[0-3])$ ]]; then\n echo \"::warning::max_langs '$MAX_LANGS' out of range; clamping to 13\"\n MAX_LANGS=13\nfi\necho \"Max target languages this run: $MAX_LANGS\"\n\n# Discover candidate sources.\nROOT=\"analysis/daily\"\n# Worklist file written to GITHUB_WORKSPACE so it is visible inside the\n# AWF container (`--add-dir \"${GITHUB_WORKSPACE}\"`); /tmp on the runner\n# is NOT mounted into the container and the agent cannot read it.\nWORKLIST_FILE=\"${GITHUB_WORKSPACE}/.exec-brief-worklist.txt\"\n\n# Emit a single canonical bundle to BOTH $GITHUB_OUTPUT (for downstream\n# GHA steps) AND $GITHUB_ENV (so `awf --env-all` forwards the values\n# to the agent's bash sandbox — agents have no access to step outputs).\nemit_bundle() {\n local worklist=\"$1\" langs=\"$2\" max=\"$3\" missing=\"$4\" drift=\"$5\"\n {\n echo \"TRANSLATION_WORKLIST=$worklist\"\n echo \"TRANSLATION_LANGS=$langs\"\n echo \"MAX_BRIEFS=$max\"\n echo \"MAX_BRIEFS_RESOLVED=$max\"\n echo \"MISSING_COUNT=$missing\"\n echo \"DRIFT_COUNT=$drift\"\n echo \"EXEC_BRIEF_WORKLIST_FILE=$WORKLIST_FILE\"\n } >> \"$GITHUB_OUTPUT\"\n {\n echo \"TRANSLATION_WORKLIST=$worklist\"\n echo \"TRANSLATION_LANGS=$langs\"\n echo \"MAX_BRIEFS=$max\"\n echo \"MAX_BRIEFS_RESOLVED=$max\"\n echo \"MISSING_COUNT=$missing\"\n echo \"DRIFT_COUNT=$drift\"\n echo \"EXEC_BRIEF_WORKLIST_FILE=$WORKLIST_FILE\"\n } >> \"$GITHUB_ENV\"\n # Always write the file (empty when worklist is empty) so the agent\n # can rely on a stable path.\n : > \"$WORKLIST_FILE\"\n if [ -n \"$worklist\" ]; then\n printf '%s\\n' \"$worklist\" | tr ',' '\\n' > \"$WORKLIST_FILE\"\n fi\n}\n\nif [ ! -d \"$ROOT\" ]; then\n emit_bundle \"\" \"$LANGS\" \"$MAX_BRIEFS\" \"0\" \"0\"\n echo \"No $ROOT directory — nothing to translate.\"\n exit 0\nfi\n\nif [ -n \"${ARTICLE_DATE_INPUT:-}\" ] && [ -n \"${SUBFOLDER_INPUT:-}\" ]; then\n SCAN_PATTERN=\"$ROOT/$ARTICLE_DATE_INPUT/$SUBFOLDER_INPUT\"\nelif [ -n \"${ARTICLE_DATE_INPUT:-}\" ]; then\n SCAN_PATTERN=\"$ROOT/$ARTICLE_DATE_INPUT\"\nelse\n SCAN_PATTERN=\"$ROOT\"\nfi\n\n# Build list of (date, source-path) tuples, oldest date first.\nCANDIDATES=$(find \"$SCAN_PATTERN\" -type f -name 'executive-brief.md' 2>/dev/null \\\n | sort)\n\nif [ -z \"$CANDIDATES\" ]; then\n emit_bundle \"\" \"$LANGS\" \"$MAX_BRIEFS\" \"0\" \"0\"\n echo \"No executive-brief.md sources found under $SCAN_PATTERN.\"\n exit 0\nfi\n\n# For each source, classify it as MISSING (≥1 requested language file absent\n# or empty) or DRIFT (every requested language file present but ≥1 has a\n# stale `` trailer relative to the current source\n# commit SHA). When `force_retranslate=true`, every source with at least\n# one existing target file is reclassified as DRIFT (forces a re-pass);\n# sources missing any target stay in MISSING.\n#\n# Selection policy (greenfield-first):\n# 1. Fill `MAX_BRIEFS` batch slots with MISSING sources, oldest date first\n# (159-source backlog is drained before any drift-fix work).\n# 2. Only if MISSING is empty after the scan, fill remaining slots with\n# DRIFT sources, oldest date first.\n# This guarantees runs never \"redo already-translated briefs\" while the\n# untranslated backlog still has work — see PR description for context.\nMISSING=()\nDRIFT=()\nIFS=',' read -ra LANG_LIST <<< \"$LANGS\"\nwhile IFS= read -r SRC; do\n [ -n \"$SRC\" ] || continue\n DIR=$(dirname \"$SRC\")\n SRC_SHA=$(git log -1 --format=%H -- \"$SRC\" 2>/dev/null || echo \"\")\n HAS_MISSING=0\n HAS_DRIFT=0\n for L in \"${LANG_LIST[@]}\"; do\n TGT=\"$DIR/executive-brief_${L}.md\"\n if [ ! -s \"$TGT\" ]; then\n HAS_MISSING=1\n continue\n fi\n if [ -n \"$SRC_SHA\" ] && ! grep -q \"\" \"$TGT\" 2>/dev/null; then\n HAS_DRIFT=1\n fi\n done\n if [ \"$HAS_MISSING\" -eq 1 ]; then\n MISSING+=(\"$SRC\")\n elif [ \"${FORCE_RETRANSLATE:-false}\" = \"true\" ] || [ \"$HAS_DRIFT\" -eq 1 ]; then\n DRIFT+=(\"$SRC\")\n fi\ndone <<< \"$CANDIDATES\"\n\nMISSING_COUNT=\"${#MISSING[@]}\"\nDRIFT_COUNT=\"${#DRIFT[@]}\"\necho \"Backlog classification: MISSING=$MISSING_COUNT DRIFT=$DRIFT_COUNT\"\n\n# Fill batch: MISSING first (greenfield), DRIFT only if capacity remains.\nWORK=()\nfor SRC in \"${MISSING[@]}\"; do\n WORK+=(\"$SRC\")\n if [ \"${#WORK[@]}\" -ge \"$MAX_BRIEFS\" ]; then break; fi\ndone\nif [ \"${#WORK[@]}\" -lt \"$MAX_BRIEFS\" ]; then\n for SRC in \"${DRIFT[@]}\"; do\n WORK+=(\"$SRC\")\n if [ \"${#WORK[@]}\" -ge \"$MAX_BRIEFS\" ]; then break; fi\n done\nfi\n\nif [ \"${#WORK[@]}\" -eq 0 ]; then\n emit_bundle \"\" \"$LANGS\" \"$MAX_BRIEFS\" \"$MISSING_COUNT\" \"$DRIFT_COUNT\"\n echo \"All discovered sources are up to date for the requested languages.\"\n exit 0\nfi\n\n# Categorise the chosen batch for the log line: how many slots were filled\n# with greenfield (MISSING) vs drift-fix (DRIFT) work.\nWORK_MISSING=0\nWORK_DRIFT=0\nfor SRC in \"${WORK[@]}\"; do\n for M in \"${MISSING[@]:-}\"; do\n if [ \"$M\" = \"$SRC\" ]; then WORK_MISSING=$((WORK_MISSING+1)); continue 2; fi\n done\n WORK_DRIFT=$((WORK_DRIFT+1))\ndone\n\nWORKLIST_JOINED=$(printf '%s\\n' \"${WORK[@]}\" | paste -sd ',' -)\n\n# --- Per-run language batching (effective-token control) ---\n# Across the selected batch, collect the requested languages that still\n# NEED work — missing target file first (greenfield), then drift — in the\n# canonical order of $LANGS, and translate at most $MAX_LANGS of them this\n# run. This bounds the number of full files the agent writes in one\n# Copilot session (the dominant driver of the weighted effective-token\n# total that aborted run #26641603577 with a 429 before the PR shipped).\n# The remaining languages are picked up by the next scheduled run because\n# every selected source stays classified MISSING/DRIFT until all requested\n# languages are present and current.\nNEED_MISSING=()\nNEED_DRIFT=()\nfor L in \"${LANG_LIST[@]}\"; do\n L_MISSING=0\n L_DRIFT=0\n for SRC in \"${WORK[@]}\"; do\n DIR=$(dirname \"$SRC\")\n SRC_SHA=$(git log -1 --format=%H -- \"$SRC\" 2>/dev/null || echo \"\")\n TGT=\"$DIR/executive-brief_${L}.md\"\n if [ ! -s \"$TGT\" ]; then\n L_MISSING=1\n continue\n fi\n if [ \"${FORCE_RETRANSLATE:-false}\" = \"true\" ]; then\n L_DRIFT=1\n elif [ -n \"$SRC_SHA\" ] && ! grep -q \"\" \"$TGT\" 2>/dev/null; then\n L_DRIFT=1\n fi\n done\n if [ \"$L_MISSING\" -eq 1 ]; then\n NEED_MISSING+=(\"$L\")\n elif [ \"$L_DRIFT\" -eq 1 ]; then\n NEED_DRIFT+=(\"$L\")\n fi\ndone\nNEED_LANGS=(\"${NEED_MISSING[@]}\" \"${NEED_DRIFT[@]}\")\n# Fallback: if nothing was flagged as needing work (edge case), fall back\n# to the full requested list so the run never emits an empty language set.\nif [ \"${#NEED_LANGS[@]}\" -eq 0 ]; then\n NEED_LANGS=(\"${LANG_LIST[@]}\")\nfi\nRUN_LANG_LIST=()\nfor L in \"${NEED_LANGS[@]}\"; do\n RUN_LANG_LIST+=(\"$L\")\n if [ \"${#RUN_LANG_LIST[@]}\" -ge \"$MAX_LANGS\" ]; then break; fi\ndone\nRUN_LANGS=$(IFS=','; echo \"${RUN_LANG_LIST[*]}\")\necho \"Per-run language batch (cap $MAX_LANGS): [$RUN_LANGS] of requested [$LANGS]\"\n\nemit_bundle \"$WORKLIST_JOINED\" \"$RUN_LANGS\" \"$MAX_BRIEFS\" \"$MISSING_COUNT\" \"$DRIFT_COUNT\"\necho \"✅ Selected ${#WORK[@]} source brief(s) for translation into [$RUN_LANGS]\"\necho \" (greenfield=$WORK_MISSING / drift-fix=$WORK_DRIFT; total backlog: MISSING=$MISSING_COUNT DRIFT=$DRIFT_COUNT)\"\necho \" Worklist file (agent reads this): $WORKLIST_FILE\"\nprintf ' %s\\n' \"${WORK[@]}\"\n" # Cache memory file share configuration from frontmatter processed below - name: Create cache-memory directory @@ -553,44 +552,15 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" alpine:latest@sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11 ghcr.io/github/gh-aw-firewall/agent:0.25.56@sha256:83501aa3848cda3f02aa300b90a7444f5a72c23629936c1d78d5c4518e6ccba5 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.56@sha256:3832c91e64b7dc20ab99024791a7436548275d1c8d6cd0b329b070c9924113c8 ghcr.io/github/gh-aw-firewall/squid:0.25.56@sha256:c6db2c30e76b2a30290c6c243567bdb68c43ba6ed7b0bc763e543d7eb3e5b909 ghcr.io/github/gh-aw-mcpg:v0.3.20@sha256:6d79c7b356d7f504229ee693914b46f3920982966b58d96cc15eaf66327faee9 ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 node:26-alpine@sha256:30f5a66e7265ef70aac56b4753ffa7905e54eca1084bc25503893ad8e9273f05 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 - - name: Install gh-aw extension - uses: github/gh-aw/actions/setup-cli@d1aa364b4f4814cd64f0e97fcbf659cc16849230 # v0.77.1 - with: - version: 'v0.77.1' - github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - - name: Copy gh-aw binary for MCP server - run: | - gh aw --version - # Copy the gh-aw binary to ${RUNNER_TEMP}/gh-aw for MCP server containerization - mkdir -p "${RUNNER_TEMP}/gh-aw" - GH_AW_BIN="" - GH_AW_BIN=$(command -v gh-aw 2>/dev/null) || true - if [ -z "$GH_AW_BIN" ]; then - GH_AW_BIN=$(find "${HOME}/.local/share/gh/extensions/gh-aw" -name 'gh-aw' -type f 2>/dev/null | head -1) || true - fi - if [ -z "$GH_AW_BIN" ] && [ -n "${GH_CONFIG_DIR:-}" ]; then - GH_AW_BIN=$(find "${GH_CONFIG_DIR}/extensions/gh-aw" -name 'gh-aw' -type f 2>/dev/null | head -1) || true - fi - if [ -z "$GH_AW_BIN" ] && [ -f "${GITHUB_WORKSPACE}/gh-aw" ]; then - GH_AW_BIN="${GITHUB_WORKSPACE}/gh-aw" - fi - if [ -n "$GH_AW_BIN" ] && [ -f "$GH_AW_BIN" ]; then - cp "$GH_AW_BIN" "${RUNNER_TEMP}/gh-aw/gh-aw" - chmod +x "${RUNNER_TEMP}/gh-aw/gh-aw" - echo "Copied gh-aw binary to ${RUNNER_TEMP}/gh-aw/gh-aw" - else - echo "::error::Failed to find gh-aw binary for MCP server" - exit 1 - fi + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.56@sha256:83501aa3848cda3f02aa300b90a7444f5a72c23629936c1d78d5c4518e6ccba5 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.56@sha256:3832c91e64b7dc20ab99024791a7436548275d1c8d6cd0b329b070c9924113c8 ghcr.io/github/gh-aw-firewall/squid:0.25.56@sha256:c6db2c30e76b2a30290c6c243567bdb68c43ba6ed7b0bc763e543d7eb3e5b909 ghcr.io/github/gh-aw-mcpg:v0.3.20@sha256:6d79c7b356d7f504229ee693914b46f3920982966b58d96cc15eaf66327faee9 ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_219bf7abbb8a117b_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_f14081b8c8ff0305_EOF' {"add_comment":{"max":1},"create_pull_request":{"draft":false,"expires":336,"fallback_as_issue":true,"if_no_changes":"warn","labels":["agentic-news","translation","executive-brief"],"max":1,"max_patch_files":100,"max_patch_size":10240,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"allowed"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_219bf7abbb8a117b_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_f14081b8c8ff0305_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -797,7 +767,6 @@ jobs: GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" @@ -827,30 +796,9 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_57d4c8261daa562d_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_f36613f12657f4ff_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { - "agenticworkflows": { - "type": "stdio", - "container": "alpine:latest", - "entrypoint": "${RUNNER_TEMP}/gh-aw/gh-aw", - "entrypointArgs": ["mcp-server", "--validate-actor"], - "mounts": ["${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro", "/usr/bin/gh:/usr/bin/gh:ro", "\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"], - "args": ["--network", "host", "-w", "\${GITHUB_WORKSPACE}"], - "env": { - "DEBUG": "*", - "GITHUB_TOKEN": "\${GITHUB_TOKEN}", - "GITHUB_ACTOR": "\${GITHUB_ACTOR}", - "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}" - }, - "guard-policies": { - "write-sink": { - "accept": [ - "*" - ] - } - } - }, "github": { "type": "stdio", "container": "ghcr.io/github/github-mcp-server:v1.0.4", @@ -858,7 +806,7 @@ jobs: "GITHUB_HOST": "\${GITHUB_SERVER_URL}", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "all" + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" }, "guard-policies": { "allow-only": { @@ -871,7 +819,7 @@ jobs: "type": "http", "url": "https://riksdag-regering-ai.onrender.com/mcp", "tools": [ - "*" + "get_sync_status" ], "guard-policies": { "write-sink": { @@ -894,46 +842,6 @@ jobs: ] } } - }, - "scb": { - "type": "stdio", - "container": "node:26-alpine", - "entrypoint": "npx", - "entrypointArgs": [ - "-y", - "@jarib/pxweb-mcp@2.0.0", - "--url", - "https://api.scb.se/OV0104/v2beta" - ], - "tools": [ - "*" - ], - "guard-policies": { - "write-sink": { - "accept": [ - "*" - ] - } - } - }, - "world-bank": { - "type": "stdio", - "container": "node:26-alpine", - "entrypoint": "npx", - "entrypointArgs": [ - "-y", - "worldbank-mcp@1.0.1" - ], - "tools": [ - "*" - ], - "guard-policies": { - "write-sink": { - "accept": [ - "*" - ] - } - } } }, "gateway": { @@ -943,7 +851,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_57d4c8261daa562d_EOF + GH_AW_MCP_CONFIG_f36613f12657f4ff_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -1440,7 +1348,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: WORKFLOW_NAME: "News: Translate Executive Briefs" - WORKFLOW_DESCRIPTION: "Translates `analysis/daily/**/executive-brief.md` (English source produced by per-type news workflows) into the 13 non-English target languages as sibling `executive-brief_.md` files. Runs three times daily and picks up at most `max_briefs` (default 1) sources × `max_langs` (default 7) languages per run, **greenfield-first** — sources with one or more missing target files always win batch slots over drift-only sources, and within a source the still-needed languages are translated missing-first then drift up to `max_langs` (remaining languages drain in later runs). Pass 2 is **validator-first**: the bash validator does all structural parity at near-zero model-token cost and the agent only reads back the files it flags. The per-run `max_langs` language cap plus validator-first Pass 2 keep the session's weighted effective-token usage under the Copilot per-session cap after run #26641603577 aborted with a 429 (27.0M / 25M weighted) before shipping its PR — see `07-commit-and-pr.md §Deadline enforcement`. See `TRANSLATION_GUIDE.md §Executive Brief Markdown Translations` for the authoritative content contract." + WORKFLOW_DESCRIPTION: "Translates `analysis/daily/**/executive-brief.md` (English source produced by per-type news workflows) into the 13 non-English target languages as sibling `executive-brief_.md` files. Runs three times daily and picks up at most `max_briefs` (default 1) sources × `max_langs` (default 13) languages per run, **greenfield-first** — sources with one or more missing target files always win batch slots over drift-only sources, and within a source the still-needed languages are translated missing-first then drift up to `max_langs` (remaining languages drain in later runs). Pass 2 is **validator-first**: the bash validator does all structural parity at near-zero model-token cost and the agent only reads back the files it flags. This workflow only uses bash/edit/safe-outputs — its MCP/tool surface is deliberately minimal so the per-turn tool-schema context (the ~3.5× weighting driver that aborted run #26641603577 at 27.0M / 25M weighted) no longer dominates; wall-clock (Timer A/B) and the 100-file safe-outputs cap, not the token budget, are now the governing limits — see `07-commit-and-pr.md §Deadline enforcement`. See `TRANSLATION_GUIDE.md §Executive Brief Markdown Translations` for the authoritative content contract." HAS_PATCH: ${{ needs.agent.outputs.has_patch }} with: script: | diff --git a/.github/workflows/news-translate.md b/.github/workflows/news-translate.md index 9c8d75664b3..ac56d3c185d 100644 --- a/.github/workflows/news-translate.md +++ b/.github/workflows/news-translate.md @@ -1,6 +1,6 @@ --- name: "News: Translate Executive Briefs" -description: "Translates `analysis/daily/**/executive-brief.md` (English source produced by per-type news workflows) into the 13 non-English target languages as sibling `executive-brief_.md` files. Runs three times daily and picks up at most `max_briefs` (default 1) sources × `max_langs` (default 7) languages per run, **greenfield-first** — sources with one or more missing target files always win batch slots over drift-only sources, and within a source the still-needed languages are translated missing-first then drift up to `max_langs` (remaining languages drain in later runs). Pass 2 is **validator-first**: the bash validator does all structural parity at near-zero model-token cost and the agent only reads back the files it flags. The per-run `max_langs` language cap plus validator-first Pass 2 keep the session's weighted effective-token usage under the Copilot per-session cap after run #26641603577 aborted with a 429 (27.0M / 25M weighted) before shipping its PR — see `07-commit-and-pr.md §Deadline enforcement`. See `TRANSLATION_GUIDE.md §Executive Brief Markdown Translations` for the authoritative content contract." +description: "Translates `analysis/daily/**/executive-brief.md` (English source produced by per-type news workflows) into the 13 non-English target languages as sibling `executive-brief_.md` files. Runs three times daily and picks up at most `max_briefs` (default 1) sources × `max_langs` (default 13) languages per run, **greenfield-first** — sources with one or more missing target files always win batch slots over drift-only sources, and within a source the still-needed languages are translated missing-first then drift up to `max_langs` (remaining languages drain in later runs). Pass 2 is **validator-first**: the bash validator does all structural parity at near-zero model-token cost and the agent only reads back the files it flags. This workflow only uses bash/edit/safe-outputs — its MCP/tool surface is deliberately minimal so the per-turn tool-schema context (the ~3.5× weighting driver that aborted run #26641603577 at 27.0M / 25M weighted) no longer dominates; wall-clock (Timer A/B) and the 100-file safe-outputs cap, not the token budget, are now the governing limits — see `07-commit-and-pr.md §Deadline enforcement`. See `TRANSLATION_GUIDE.md §Executive Brief Markdown Translations` for the authoritative content contract." strict: false imports: - ../prompts/00-base-contract.md @@ -33,9 +33,9 @@ on: required: false default: '1' max_langs: - description: 'Maximum number of target languages to translate per run (range 1–13; default 7). The agent writes one full file per language in a single Copilot session, so the session''s weighted effective-token total scales with the language count. Translating all 13 languages of one source in a single run pushed run #26641603577 to 27.0M weighted effective tokens (> the 25M Copilot per-session cap) and the 429 fired before the PR safe-output — shipping nothing. The worklist builder selects the missing/drifted languages **greenfield-first** and caps them at this value; remaining languages are picked up by the next scheduled run because the source stays classified MISSING/DRIFT until every requested language is present and current. Raise toward 13 only for small briefs or when catching up; lower it if large briefs still approach the cap.' + description: 'Maximum number of target languages to translate per run (range 1–13; default 13 = a full source in one run). The agent writes one full file per language in a single Copilot session, so the session''s weighted effective-token total scales with the language count **times the per-turn tool-schema surface**. The dominant cost was the latter: run #26641603577 hit 27.0M weighted (> the 25M Copilot per-session cap) at 13 languages while the workflow still loaded the full github `toolsets:[all]` + three data-MCP servers on every turn. That dead-weight schema was removed (the agent only uses bash/edit/safe-outputs), collapsing the per-turn footprint, so a full 13-language source now fits in one run. The worklist builder selects the missing/drifted languages **greenfield-first** and caps them at this value; remaining languages are picked up by the next scheduled run because the source stays classified MISSING/DRIFT until every requested language is present and current. Lower this first if an unusually large brief still approaches the cap.' required: false - default: '7' + default: '13' force_retranslate: description: 'When true, ignore the `` drift check and re-translate every matching source. Use sparingly — drains the safe-outputs file budget fast.' required: false @@ -162,31 +162,24 @@ network: # ── GitHub raw content is covered by the `github` ecosystem identifier above. # Pinned FQDN remains in `safe-outputs.allowed-domains` below (only FQDNs allowed there). +# MCP surface is intentionally minimal: translation reads/writes files via bash + edit +# and ships the PR via safe-outputs, so it calls NO data-MCP, github-MCP, web-fetch or +# agentic-workflows tool. Every declared tool's JSON schema is re-sent on every model turn, +# so the cumulative weighted effective-token total scales with the schema surface × turns — +# this was the dominant driver of the 3.5× weighting that aborted run #26641603577 at 27.0M. +# Only riksdag-regering is kept (its URL is asserted by tests/network-diagnostics.test.ts) and +# narrowed to the single health-gate tool documented in 02-mcp-access.md. The scb / world-bank +# data servers are dropped here (their egress domains stay in network.allowed for uniformity). mcp-servers: riksdag-regering: url: https://riksdag-regering-ai.onrender.com/mcp - allowed: ["*"] - scb: - container: "node:26-alpine" - entrypoint: "npx" - entrypointArgs: ["-y", "@jarib/pxweb-mcp@2.0.0", "--url", "https://api.scb.se/OV0104/v2beta"] - allowed: ["*"] - world-bank: - container: "node:26-alpine" - entrypoint: "npx" - entrypointArgs: ["-y", "worldbank-mcp@1.0.1"] - allowed: ["*"] + allowed: ["get_sync_status"] tools: startup-timeout: 180 timeout: 120 - github: - toolsets: - - all - agentic-workflows: true bash: true edit: - web-fetch: cache-memory: key: news-${{ github.workflow }}-${{ inputs.article_date || 'today' }} retention-days: 14 @@ -333,17 +326,19 @@ steps: fi echo "Max source briefs this run: $MAX_BRIEFS" - # Resolve per-run language cap (1–13 hard range; default 7). The agent - # writes one full file per language in a single Copilot session, so the - # session's weighted effective-token total scales with this count. - # Translating all 13 languages of one source in a single run pushed run - # #26641603577 to 27.0M weighted effective tokens (> the 25M cap) and the - # 429 fired before the PR safe-output. The batching block below caps the - # languages handled per run to this value, greenfield-first. - MAX_LANGS="${MAX_LANGS_INPUT:-7}" + # Resolve per-run language cap (1–13 hard range; default 13 = a full source + # in one run). The agent writes one full file per language in a single Copilot + # session, so the weighted effective-token total scales with the language count + # times the per-turn tool-schema surface. The latter was the dominant cost: run + # #26641603577 hit 27.0M weighted (> the 25M cap) at 13 languages while the heavy + # github toolsets:[all] + three data-MCP server schemas were still re-sent every + # turn. That dead-weight surface was removed (this workflow only uses bash/edit/ + # safe-outputs), so a full 13-language source now fits under the cap. The batching + # block below caps the languages handled per run to this value, greenfield-first. + MAX_LANGS="${MAX_LANGS_INPUT:-13}" if ! [[ "$MAX_LANGS" =~ ^([1-9]|1[0-3])$ ]]; then - echo "::warning::max_langs '$MAX_LANGS' out of range; clamping to 7" - MAX_LANGS=7 + echo "::warning::max_langs '$MAX_LANGS' out of range; clamping to 13" + MAX_LANGS=13 fi echo "Max target languages this run: $MAX_LANGS" @@ -554,7 +549,7 @@ This workflow is the **sole writer** of `analysis/daily/$DATE/$SUB/executive-bri ## Pipeline 1. **Wall-clock checkpoint (mandatory)** — as the **very first bash call** in this run, record `JOB_START=$(date +%s)` and export it. After every source completes Pass 2, re-read `NOW=$(date +%s)` and compute `ELAPSED_MIN=$(( (NOW - JOB_START) / 60 ))`; print it. If `ELAPSED_MIN >= 35`, stop selecting new sources and proceed directly to the validate → stage → commit → `safeoutputs___create_pull_request` flow regardless of remaining `$TRANSLATION_WORKLIST` entries. If `ELAPSED_MIN >= 42`, **halt all translation work immediately** and execute the [`07-commit-and-pr.md §Emergency deadline order of operations`](../prompts/07-commit-and-pr.md) bash block to ship a partial PR before Timer A (60-min job `timeout-minutes`) and Timer B (~60-min Copilot API session) fire. A partial PR is always better than zero output — see run [#26633644372](https://github.com/Hack23/riksdagsmonitor/actions/runs/26633644372), where source #2 failed with `CAPIError: 429 Maximum effective tokens exceeded`. -2. The `Build executive-brief translation work list` pre-flight step has already exported four env vars into the agent sandbox: **`$TRANSLATION_WORKLIST`** (comma-separated repo-relative paths — read this verbatim, or parse the same content line-by-line from the file at **`$EXEC_BRIEF_WORKLIST_FILE`** at `${GITHUB_WORKSPACE}/.exec-brief-worklist.txt`), **`$TRANSLATION_LANGS`** (comma-separated language codes — this is the **per-run language batch**, already capped at `inputs.max_langs` (default 7) and pre-selected **greenfield-first** from the languages that still need work; translate **exactly and only** these codes — do not expand to the full 13-language set), **`$MAX_BRIEFS`** / **`$MAX_BRIEFS_RESOLVED`** (1–7, clamp-applied), and the audit counters **`$MISSING_COUNT`** / **`$DRIFT_COUNT`**. **Do not re-scan the filesystem** to rebuild this list — the pre-flight step has already honoured `inputs.article_date`, `inputs.subfolder`, `inputs.languages`, `inputs.max_briefs`, `inputs.max_langs`, and `inputs.force_retranslate`. Languages omitted from `$TRANSLATION_LANGS` this run are intentionally deferred to the next scheduled run (the source stays MISSING/DRIFT until all requested languages are present and current) — this per-run cap is what keeps the session under the Copilot weighted effective-token cap that aborted run [#26641603577](https://github.com/Hack23/riksdagsmonitor/actions/runs/26641603577). The selector is **greenfield-first**: sources with one or more missing target files (`MISSING`) always win the `max_briefs` batch slots over sources that only need drift-fixes (`DRIFT`); `DRIFT` is only consulted when `MISSING` is empty. If `$TRANSLATION_WORKLIST` is empty (so `$MISSING_COUNT=0` **and** `$DRIFT_COUNT=0`), nothing is pending — proceed to improvement-mode (re-validate every existing translation against the current `` trailer and fix any drift the validator flags). If still nothing changes after improvement-mode, follow `07-commit-and-pr.md §No-op policy`. +2. The `Build executive-brief translation work list` pre-flight step has already exported four env vars into the agent sandbox: **`$TRANSLATION_WORKLIST`** (comma-separated repo-relative paths — read this verbatim, or parse the same content line-by-line from the file at **`$EXEC_BRIEF_WORKLIST_FILE`** at `${GITHUB_WORKSPACE}/.exec-brief-worklist.txt`), **`$TRANSLATION_LANGS`** (comma-separated language codes — this is the **per-run language batch**, already capped at `inputs.max_langs` (default 13) and pre-selected **greenfield-first** from the languages that still need work; translate **exactly and only** these codes — do not expand to the full 13-language set), **`$MAX_BRIEFS`** / **`$MAX_BRIEFS_RESOLVED`** (1–7, clamp-applied), and the audit counters **`$MISSING_COUNT`** / **`$DRIFT_COUNT`**. **Do not re-scan the filesystem** to rebuild this list — the pre-flight step has already honoured `inputs.article_date`, `inputs.subfolder`, `inputs.languages`, `inputs.max_briefs`, `inputs.max_langs`, and `inputs.force_retranslate`. Languages omitted from `$TRANSLATION_LANGS` this run are intentionally deferred to the next scheduled run (the source stays MISSING/DRIFT until all requested languages are present and current) — this per-run cap is what keeps the session under the Copilot weighted effective-token cap that aborted run [#26641603577](https://github.com/Hack23/riksdagsmonitor/actions/runs/26641603577). The selector is **greenfield-first**: sources with one or more missing target files (`MISSING`) always win the `max_briefs` batch slots over sources that only need drift-fixes (`DRIFT`); `DRIFT` is only consulted when `MISSING` is empty. If `$TRANSLATION_WORKLIST` is empty (so `$MISSING_COUNT=0` **and** `$DRIFT_COUNT=0`), nothing is pending — proceed to improvement-mode (re-validate every existing translation against the current `` trailer and fix any drift the validator flags). If still nothing changes after improvement-mode, follow `07-commit-and-pr.md §No-op policy`. 3. For each source path in `$TRANSLATION_WORKLIST` (or each line of `$EXEC_BRIEF_WORKLIST_FILE`): 1. **Pass 1 — translate**: Read the source `executive-brief.md` in full **once**. For every language in `TRANSLATION_LANGS`, produce `analysis/daily/$DATE/$SUB/executive-brief_.md` following the TRANSLATION_GUIDE rules — **write each file with one `edit` tool call per language** (never via `python3`, `bash` heredocs, or shell redirection — see `01-bash-and-shell-safety.md §File creation & overwrite strategy`). Apply the per-language tone register from TRANSLATION_GUIDE **at write time** (so Pass 2 never needs a full re-read just to adjust register), and for `ar`/`he` start the file with ``. Preserve every verbatim block (YAML, HTML comments except `source-sha`, `dok_id` codes, Mermaid DSL bodies, code fences, URLs, file paths, evidence-anchor canonical column values). Translate every always-translate block (prose, headings, list items, table cell text, image alt-text, BLUF, decisions, link text). **Do not** read a freshly written translation back into model context to "confirm" it — the validator in Pass 2 is the authoritative gate. 2. **Pass 2 — validate-first & targeted refine**: This is the **primary token-efficiency control**. Run `npx tsx scripts/validate-executive-brief-translations.ts --source --lang "$TRANSLATION_LANGS"` in the runtime shell (the `--lang` scope restricts validation to this run's language batch so deferred languages are not reported as missing). That validator already performs every structural-parity check at near-zero model-token cost: heading / table-row / code-fence / Mermaid-block count parity, `dok_id` and URL set equality, banned-English-phrase detection (`Executive Brief`, `Decisions`, `Confidence`, `BLUF`, …), the `` marker for `ar`/`he`, and the `` trailer. **Do not read passing translations back into model context** — reading every target file back in full is exactly what drove run [#26633644372](https://github.com/Hack23/riksdagsmonitor/actions/runs/26633644372) to `7.6M` effective tokens and a `429 Maximum effective tokens exceeded` abort. Only read back (and re-translate / fix) the specific `executive-brief_.md` files the validator reports as failing, then re-run the validator until it is clean for the requested languages. @@ -570,8 +565,8 @@ This workflow is the **sole writer** of `analysis/daily/$DATE/$SUB/executive-bri - `article_date` — optional. Restrict scanning to a single date folder. - `subfolder` — optional. Restrict scanning to a single document type folder. - `languages` — default `all-extra` (= `sv,da,no,fi,de,fr,es,nl,ar,he,ja,ko,zh`). Aliases: `nordic-extra`, `eu-extra`, `cjk`, `rtl`, `all-extra`. Comma list also accepted. -- `max_briefs` — default `1`, range `1–7`. Caps the number of **source** files processed in this run; total file output = `max_briefs × |TRANSLATION_LANGS|` (≤ `7 × 7 = 49` at the defaults, safely under the 100-file safe-outputs cap). The default was lowered from 3 to 1 after run #26633644372 hit the Copilot effective-token limit before finishing source #2. -- `max_langs` — default `7`, range `1–13`. Caps the number of **target languages** translated this run. The builder pre-selects the still-needed languages **greenfield-first** (missing files first, then drift) and truncates to this many. Translating all 13 in one Copilot session pushed run #26641603577 to 27.0M weighted effective tokens (> the 25M cap), aborting before the PR shipped; deferred languages are filled by the next scheduled run. Raise toward 13 for small briefs / catch-up; lower it if large briefs still approach the cap. +- `max_briefs` — default `1`, range `1–7`. Caps the number of **source** files processed in this run; total file output = `max_briefs × |TRANSLATION_LANGS|` (= `1 × 13 = 13` at the defaults; hard-cap worst case `7 × 13 = 91`, safely under the 100-file safe-outputs cap). The default was lowered from 3 to 1 after run #26633644372 hit the Copilot effective-token limit before finishing source #2. +- `max_langs` — default `13` (= a full source in one run), range `1–13`. Caps the number of **target languages** translated this run. The builder pre-selects the still-needed languages **greenfield-first** (missing files first, then drift) and truncates to this many. Run #26641603577 hit 27.0M weighted effective tokens (> the 25M cap) at 13 languages, but that was while the heavy `github toolsets:[all]` + three data-MCP server schemas were re-sent every turn; that dead-weight surface has been removed (this workflow only uses bash/edit/safe-outputs), collapsing the per-turn footprint so a full 13-language source now fits in one run. Lower this first if an unusually large brief approaches the cap. - `force_retranslate` — default `false`. When `true`, every requested language is rewritten even if the `` marker matches. - `analysis_depth` — default `standard`. Echoed into the validator output for parity with content workflows; does not change translation behaviour. @@ -581,16 +576,16 @@ This workflow is the **sole writer** of `analysis/daily/$DATE/$SUB/executive-bri |----------|-------|--------| | Average source size | ~650 words (range 232–2040) | `wc -w` over `analysis/daily/**/executive-brief.md` | | Per-language Pass 1 write + validator gate | ~45–75 s | Sonnet-class translation budget; Pass 2 is bash-validated, not re-read into context | -| Per-source wall time (≤`max_langs` languages, sequential) | ~8–15 min at `max_langs=7` | derived | -| Default `max_briefs` / `max_langs` | 1 source / 7 languages | keeps the weighted effective-token total under Copilot's per-session cap while preserving PR headroom | -| Per-session token driver | turn count ≈ languages translated | each language adds Pass-1/validator turns; the full per-turn tool-schema context (github + data MCP servers) is re-sent every turn, so cumulative weighted tokens scale ~linearly with the language count | -| Run #26641603577 (pre-fix) | 13 langs → 27.0M weighted / 25M cap → 429 before PR | gh-aw raw 7.67M; the weighted metric is ~3.5× raw and exceeded the cap; **no PR shipped** | -| Primary token levers | validator-first Pass 2 **+ `max_langs` per-run language cap** | structural parity stays in the bash validator; the language cap bounds the per-session turn count / output | -| Per-run file output | `max_briefs × max_langs` = `1 × 7` = **7 files** (default) | well under safe-outputs `max-patch-files: 100` | -| Daily throughput (3 runs) | up to **21 language-files / day** at defaults | raise `max_briefs` / `max_langs` once the per-session footprint is measured comfortably below the cap | +| Per-source wall time (≤`max_langs` languages, sequential) | ~15–25 min at `max_langs=13` | derived | +| Default `max_briefs` / `max_langs` | 1 source / 13 languages | a slim per-turn MCP/tool surface keeps the weighted effective-token total under Copilot's per-session cap even at a full 13-language source | +| Per-session token driver | per-turn tool-schema surface × turn count | **before the fix** the dominant cost was the fixed per-turn schema (github `toolsets:[all]` + three data-MCP servers) re-sent every turn — the ~3.5× weighting multiplier. The translate agent calls none of those tools, so the surface was stripped to bash/edit/safe-outputs (+ one riksdag health-gate tool); the per-turn footprint now collapses toward the raw token count | +| Run #26641603577 (pre-fix) | 13 langs → 27.0M weighted / 25M cap → 429 before PR | gh-aw raw 7.67M; the weighted metric was ~3.5× raw and exceeded the cap — driven by the dead-weight per-turn schema, now removed; **no PR shipped** | +| Primary token levers | **strip unused per-turn MCP/tool schema** + validator-first Pass 2 | removing the data-MCP servers + github `toolsets:[all]` cuts the fixed per-turn cost so the 25M cap stops being the limiter; structural parity stays in the bash validator | +| Per-run file output | `max_briefs × max_langs` = `1 × 13` = **13 files** (default) | well under safe-outputs `max-patch-files: 100` | +| Daily throughput (3 runs) | up to **39 language-files / day** at defaults | wall-clock (Timer A/B) and the 100-file cap, not the token budget, are now the governing limits | | Current backlog drain (≥159 untranslated sources) | greenfield-first, oldest source + missing languages first | linear | -If a run is behind schedule at agent minute 30 with translations still pending, the agent MUST trim `max_briefs` downward (drop the last selected source) rather than skip the Pass 2 validator gate — quality over completeness. A partial PR is always better than missing Timer A (job `timeout-minutes: 60`). The default `max_briefs=1` was set after run [#26633644372](https://github.com/Hack23/riksdagsmonitor/actions/runs/26633644372) repeatedly hit `CAPIError: 429 Maximum effective tokens exceeded` while attempting source #2; the validator-first Pass 2 above removes the read-back token sink, and the `max_langs=7` per-run language cap (added after run [#26641603577](https://github.com/Hack23/riksdagsmonitor/actions/runs/26641603577)) bounds the per-session turn count so the weighted effective-token total stays under the 25M cap and the PR safe-output is reached. +If a run is behind schedule at agent minute 30 with translations still pending, the agent MUST trim `max_briefs` downward (drop the last selected source) rather than skip the Pass 2 validator gate — quality over completeness. A partial PR is always better than missing Timer A (job `timeout-minutes: 60`). The default `max_briefs=1` was set after run [#26633644372](https://github.com/Hack23/riksdagsmonitor/actions/runs/26633644372) repeatedly hit `CAPIError: 429 Maximum effective tokens exceeded` while attempting source #2; the validator-first Pass 2 above removes the read-back token sink, and stripping the unused per-turn MCP/tool schema (github `toolsets:[all]` + the scb/world-bank data servers — none of which this workflow ever calls) collapses the fixed per-turn cost that drove the ~3.5× weighting, so the weighted effective-token total stays under the 25M cap and the PR safe-output is reached even at the default 13-language batch. ## Rules specific to this workflow diff --git a/tests/news-translate-mcp-surface.test.ts b/tests/news-translate-mcp-surface.test.ts new file mode 100644 index 00000000000..284782fa45c --- /dev/null +++ b/tests/news-translate-mcp-surface.test.ts @@ -0,0 +1,95 @@ +/** + * Guard test for the `news-translate` per-turn MCP / tool-schema surface. + * + * Why this exists: + * - The compiled lock injects every declared MCP server + tool JSON schema into + * *every* model turn. The translate agent only uses bash/edit/safe-outputs — + * it never calls a data-MCP, a broad github toolset, web-fetch, or + * agentic-workflows tool. That dead-weight schema was the dominant driver of + * the ~3.5× weighted-token multiplier that aborted run #26641603577 + * (27.0M weighted / 25M Copilot per-session cap) before the PR shipped. + * - This test pins the trimmed surface so a future edit to a shared module or a + * copy-paste of a content-workflow frontmatter cannot silently re-introduce + * the heavy per-turn schema and re-break the token budget. + * + * Contract-locked config that is intentionally KEPT (enforced by + * tests/network-diagnostics.test.ts and carries NO per-turn token cost — + * network.allowed is firewall egress, safe-outputs.allowed-domains is the + * output sanitiser, neither is re-sent per model turn): + * - the riksdag-regering MCP server URL, + * - the uniform network.allowed / safe-outputs.allowed-domains lists, + * - the 02-mcp-access.md import (get_sync_status health-gate prose). + */ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = join(__dirname, ".."); +const TRANSLATE_MD = readFileSync( + join(REPO_ROOT, ".github/workflows/news-translate.md"), + "utf8", +); +const TRANSLATE_LOCK = readFileSync( + join(REPO_ROOT, ".github/workflows/news-translate.lock.yml"), + "utf8", +); + +describe("news-translate trimmed per-turn MCP/tool surface", () => { + it("declares no scb (pxweb) data MCP server", () => { + // The frontmatter must not declare the scb server, and the compiled lock + // must not spawn the pxweb-mcp container in the agent's mcpServers block. + expect(TRANSLATE_MD).not.toMatch(/^\s*scb:/m); + expect(TRANSLATE_LOCK).not.toMatch(/pxweb-mcp/); + }); + + it("declares no world-bank data MCP server", () => { + expect(TRANSLATE_MD).not.toMatch(/^\s*world-bank:/m); + expect(TRANSLATE_LOCK).not.toMatch(/worldbank-mcp/); + }); + + it("does not load the full github toolset (`toolsets: [all]`)", () => { + // The agent ships its PR via safe-outputs, independent of the github MCP + // toolset. `toolsets: [all]` re-bills dozens of tool schemas every turn. + expect(TRANSLATE_MD).not.toMatch(/toolsets:\s*\n\s*-\s*all/); + // The compiler's read-only safe-outputs default must not be widened to all. + expect(TRANSLATE_LOCK).not.toMatch(/"GITHUB_TOOLSETS":\s*"all"/); + }); + + it("keeps the injected github MCP read-only and minimal for safe-outputs", () => { + // gh-aw auto-provisions a read-only github MCP for the create_pull_request + // safe-output. That is fine; just assert it stays the minimal read-only set + // and never regains write access or the `all` toolset. + expect(TRANSLATE_LOCK).toMatch(/"GITHUB_READ_ONLY":\s*"1"/); + }); + + it("drops web-fetch and agentic-workflows tools", () => { + expect(TRANSLATE_MD).not.toMatch(/^\s*web-fetch:/m); + expect(TRANSLATE_MD).not.toMatch(/^\s*agentic-workflows:/m); + }); + + it("narrows the kept riksdag-regering server to the health-gate tool only", () => { + // riksdag-regering MUST stay declared (its URL is asserted by + // network-diagnostics.test.ts) but its per-turn tool schema is reduced from + // the full ~32-tool surface (`allowed: ["*"]`) to a single tool. + expect(TRANSLATE_MD).toMatch( + /riksdag-regering:[\s\S]*?allowed:\s*\["get_sync_status"\]/, + ); + // Broad data tools must not appear in the compiled riksdag tool list. + expect(TRANSLATE_LOCK).not.toMatch(/"search_dokument"/); + expect(TRANSLATE_LOCK).not.toMatch(/"get_voting_group"/); + }); + + it("keeps the contract-required riksdag-regering MCP URL", () => { + expect(TRANSLATE_LOCK).toMatch( + /https:\/\/riksdag-regering-ai\.onrender\.com\/mcp/, + ); + }); + + it("re-baselines max_langs default to 13 (a full source in one run)", () => { + // The throughput cap is no longer the limiter once the per-turn surface is + // slim, so the default returns to translating all 13 languages per run. + expect(TRANSLATE_MD).toMatch(/MAX_LANGS="\$\{MAX_LANGS_INPUT:-13\}"/); + }); +}); diff --git a/tests/news-translate-worklist-contract.test.ts b/tests/news-translate-worklist-contract.test.ts index 2beac61216f..a80f1a2da47 100644 --- a/tests/news-translate-worklist-contract.test.ts +++ b/tests/news-translate-worklist-contract.test.ts @@ -122,17 +122,22 @@ describe("news-translate worklist propagation to agent sandbox", () => { it("caps the per-run language batch via max_langs (effective-token control)", () => { // Run #26641603577 aborted with a 429 weighted-effective-token error // because all 13 languages of one source were translated in a single - // Copilot session. The builder must expose a max_langs input, resolve - // it (default 7), and truncate the still-needed languages to it before - // emitting TRANSLATION_LANGS. - // 1. workflow_dispatch input exists with default '7'. + // Copilot session — but the dominant cost was the fixed per-turn tool + // schema (github toolsets:[all] + three data-MCP servers) re-sent every + // turn, not the language count itself. With that dead-weight surface + // stripped (the agent only uses bash/edit/safe-outputs), a full + // 13-language source fits under the cap, so the default returns to 13. + // The builder must still expose a max_langs input, resolve it, and + // truncate the still-needed languages to it before emitting + // TRANSLATION_LANGS. + // 1. workflow_dispatch input exists. expect(TRANSLATE_MD).toMatch(/max_langs:/); expect(TRANSLATE_LOCK).toMatch(/max_langs:/); // 2. The builder forwards the input into the step env and resolves it. expect(TRANSLATE_MD).toMatch( /MAX_LANGS_INPUT:\s*\$\{\{\s*github\.event\.inputs\.max_langs\s*\}\}/, ); - expect(TRANSLATE_MD).toMatch(/MAX_LANGS="\$\{MAX_LANGS_INPUT:-7\}"/); + expect(TRANSLATE_MD).toMatch(/MAX_LANGS="\$\{MAX_LANGS_INPUT:-13\}"/); // 3. The capped batch (RUN_LANGS) — not the full requested $LANGS — is // what gets emitted as the run's TRANSLATION_LANGS. expect(TRANSLATE_MD).toMatch(/RUN_LANG_LIST\b/); From bbac367b022a4da298ab97beeb58daa67ba58327 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 17:42:14 +0000 Subject: [PATCH 4/4] fix: scope source-sha marker and title post-processing to $TRANSLATION_LANGS batch Addresses review feedback: - source-sha trailer is now only written to files whose language is in $TRANSLATION_LANGS, preventing deferred languages from being marked as current without actual retranslation. - Title post-processing now iterates per-language instead of using executive-brief_*.md glob, avoiding rewriting deferred-language files without subsequent validation. Co-authored-by: pethers <1726836+pethers@users.noreply.github.com> --- .github/workflows/news-translate.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/news-translate.md b/.github/workflows/news-translate.md index ac56d3c185d..7aa252280f2 100644 --- a/.github/workflows/news-translate.md +++ b/.github/workflows/news-translate.md @@ -553,8 +553,8 @@ This workflow is the **sole writer** of `analysis/daily/$DATE/$SUB/executive-bri 3. For each source path in `$TRANSLATION_WORKLIST` (or each line of `$EXEC_BRIEF_WORKLIST_FILE`): 1. **Pass 1 — translate**: Read the source `executive-brief.md` in full **once**. For every language in `TRANSLATION_LANGS`, produce `analysis/daily/$DATE/$SUB/executive-brief_.md` following the TRANSLATION_GUIDE rules — **write each file with one `edit` tool call per language** (never via `python3`, `bash` heredocs, or shell redirection — see `01-bash-and-shell-safety.md §File creation & overwrite strategy`). Apply the per-language tone register from TRANSLATION_GUIDE **at write time** (so Pass 2 never needs a full re-read just to adjust register), and for `ar`/`he` start the file with ``. Preserve every verbatim block (YAML, HTML comments except `source-sha`, `dok_id` codes, Mermaid DSL bodies, code fences, URLs, file paths, evidence-anchor canonical column values). Translate every always-translate block (prose, headings, list items, table cell text, image alt-text, BLUF, decisions, link text). **Do not** read a freshly written translation back into model context to "confirm" it — the validator in Pass 2 is the authoritative gate. 2. **Pass 2 — validate-first & targeted refine**: This is the **primary token-efficiency control**. Run `npx tsx scripts/validate-executive-brief-translations.ts --source --lang "$TRANSLATION_LANGS"` in the runtime shell (the `--lang` scope restricts validation to this run's language batch so deferred languages are not reported as missing). That validator already performs every structural-parity check at near-zero model-token cost: heading / table-row / code-fence / Mermaid-block count parity, `dok_id` and URL set equality, banned-English-phrase detection (`Executive Brief`, `Decisions`, `Confidence`, `BLUF`, …), the `` marker for `ar`/`he`, and the `` trailer. **Do not read passing translations back into model context** — reading every target file back in full is exactly what drove run [#26633644372](https://github.com/Hack23/riksdagsmonitor/actions/runs/26633644372) to `7.6M` effective tokens and a `429 Maximum effective tokens exceeded` abort. Only read back (and re-translate / fix) the specific `executive-brief_.md` files the validator reports as failing, then re-run the validator until it is clean for the requested languages. - 3. **Append the source-revision marker**: compute `SRC_SHA=$(git log -1 --format=%H -- )` in the runtime shell. Use the `edit` tool to add or replace the final `` line at the end of every translation file (consistent with the file-write contract — no `>>` or `echo`). This is the drift signal future runs use to decide whether to retranslate. - 4. **Title post-processing**: run `npx tsx scripts/postprocess-translated-brief.ts analysis/daily/$DATE/$SUB/executive-brief_*.md` to re-apply the renderer's `cleanArticleTitle` pipeline on every translated H1. This strips locale-specific boilerplate prefixes (`Exekutiv sammanfattning — `, `Zusammenfassung — `, `执行摘要:…`) and trailing date suffixes that the Pass 1 translation may have left in place. The helper only rewrites when the cleaned title differs from the source; it never falls back to a BLUF synthesis. Verify the script reports `✓` or `✏️` for every file (no `✗`). + 3. **Append the source-revision marker**: compute `SRC_SHA=$(git log -1 --format=%H -- )` in the runtime shell. Use the `edit` tool to add or replace the final `` line at the end of every translation file **whose language is in `$TRANSLATION_LANGS`** (consistent with the file-write contract — no `>>` or `echo`). Do not touch translation files for languages outside the current batch — their marker must remain stale (or absent) so the next run correctly classifies them as DRIFT/MISSING. This is the drift signal future runs use to decide whether to retranslate. + 4. **Title post-processing**: for each language code `$L` in `$TRANSLATION_LANGS`, run `npx tsx scripts/postprocess-translated-brief.ts analysis/daily/$DATE/$SUB/executive-brief_$L.md` to re-apply the renderer's `cleanArticleTitle` pipeline on the translated H1. This strips locale-specific boilerplate prefixes (`Exekutiv sammanfattning — `, `Zusammenfassung — `, `执行摘要:…`) and trailing date suffixes that the Pass 1 translation may have left in place. The helper only rewrites when the cleaned title differs from the source; it never falls back to a BLUF synthesis. Verify the script reports `✓` or `✏️` for every file (no `✗`). Do not pass `executive-brief_*.md` as a glob — that would rewrite deferred-language files outside `$TRANSLATION_LANGS` without subsequent validation, defeating the per-run language cap. 5. **Final validate**: re-run `npx tsx scripts/validate-executive-brief-translations.ts --source --lang "$TRANSLATION_LANGS"` (when available) to confirm the title post-processing in sub-step 4 did not break parity, or fall back to the structural sanity checks listed in TRANSLATION_GUIDE §Acceptance checklist. Fix any failures by re-translating only the offending language (read back only that file). Do not commit a file that fails validation. 6. **Re-evaluate the wall-clock checkpoint** (from step 1) before starting the next source. Never start a new source after `ELAPSED_MIN >= 35`. 4. Stage every newly written / refreshed `executive-brief_.md` file. **Do not** touch `executive-brief.md` itself, any HTML under `news/`, any file under `analysis/daily/*/article.md`, or any non-translation file.