From 4e5d976197c9485b4cebf50448a66b346dd929f1 Mon Sep 17 00:00:00 2001 From: Tim Stranske Date: Sun, 23 Aug 2026 17:44:27 -0500 Subject: [PATCH 1/2] fix(tokens): statuses:write aliased to write-repo, so declaring it filtered nothing A fully green Gate run left a RED `Gate / gate` status on a sealed sync PR in stranske/Orchestrator, and nothing could clear it: Token registry initialized with 7 tokens Selected token: WORKFLOWS_APP (4645 remaining, 92.9% capacity) POST /repos/stranske/Orchestrator/statuses/ - 403 ##[warning]Gate commit status update blocked by permissions; leaving existing status untouched. `POST /statuses/{sha}` needs the `statuses` scope. GITHUB_TOKEN had it (the job declares `statuses: write`; the runner printed `Statuses: write`). The App installation does not. The declaration that should have prevented this could not: `CAPABILITY_ALIASES` mapped `statuses:write` -> `write-repo`, and GITHUB_TOKEN, PAT *and* APP all claim `write-repo`, so `capabilities: ['statuses:write']` was decorative. An alias must name what the endpoint actually requires; collapsing a narrow scope into a broad one makes the filter unable to filter. `statuses` is now its own capability, held by GITHUB_TOKEN and PAT, not by APP. Three changes, and they do different jobs: 1. token_load_balancer.js -- `statuses` becomes a real capability. Makes the declaration honest for any caller that needs it. 2. pr-00-gate.yml (root AND template) -- the status post passes `env: {}`, pinning it to the workflow token. This is the operative fix for the Gate, and it is deliberately NOT the capability declaration: this file ships `create_only`, so a consumer's Gate can sit at an old revision while token_load_balancer.js syncs forward independently. The pin needs no agreement between the two files; a declaration would. One API call per run, so losing rate-limit spreading costs nothing. 3. github-api-with-retry.js -- the swallow now NAMES the refused token and says the previous status survives. It said only "blocked by permissions", which reads as a repo misconfiguration and sends a reader to check `permissions:` blocks that are already correct. The swallow itself stays: a status post must not fail the Gate. Why this is a latched gate: Maint 71 will not merge a sync PR without `Gate / gate = success`, the only writer of that status is this step, and the step was refused the write -- so the stale failure outlived its evidence and waiting could not clear it. It failed toward SILENCE, a warning inside a run whose 18 jobs were all green. WHAT IS NOT ESTABLISHED: the exact scoring that picked the App among 7 tokens is not reproduced here. Selection scores `percentRemaining + priority*10 + typeBonus + taskBonus`, but a multi-token test seeding the App with 25x the headroom passed even with the alias deliberately broken -- getOptimalToken refreshes rate limits and appears to discard seeded capacities. That test was REMOVED rather than kept: it passed for a reason I could not establish, which is a false comfort, not coverage. The note in the test file says so. Tests: 4 added, 3 kept. Both guards fail when reverted -- alias back to ['write-repo'] and 'statuses' re-added to APP each turn the suite red, then green again on revert (demonstrated). Full node suite 1472 passed / 0 failed; drift+template pytest 375 passed. Template drift: config/template-drift-allowlist.txt fingerprints refreshed for pr-00-gate.yml. The change is applied identically to both surfaces, so the divergence is unchanged in nature; check_template_drift.py exits 0, matching the clean-tree control it failed against before. Co-Authored-By: Claude Opus 5 --- .../__tests__/token-load-balancer.test.js | 95 +++++++++++++++++++ .github/scripts/github-api-with-retry.js | 12 ++- .github/scripts/token_load_balancer.js | 19 +++- .github/workflows/pr-00-gate.yml | 23 +++++ config/template-drift-allowlist.txt | 6 +- .../.github/scripts/github-api-with-retry.js | 12 ++- .../.github/scripts/token_load_balancer.js | 19 +++- .../.github/workflows/pr-00-gate.yml | 23 +++++ 8 files changed, 198 insertions(+), 11 deletions(-) diff --git a/.github/scripts/__tests__/token-load-balancer.test.js b/.github/scripts/__tests__/token-load-balancer.test.js index 06fcf2b1d..1fa77b099 100644 --- a/.github/scripts/__tests__/token-load-balancer.test.js +++ b/.github/scripts/__tests__/token-load-balancer.test.js @@ -170,3 +170,98 @@ test('hasHealthyTokens: returns true when mixed critical and healthy tokens exis ]); assert.equal(balancer.hasHealthyTokens(), true); }); + +// --------------------------------------------------------------------------- +// statuses:write capability filtering +// +// Regression guard for the defect these cover: `statuses:write` aliased to the generic +// `write-repo`, which GITHUB_TOKEN, PAT *and* APP all claim, so declaring the capability +// filtered nothing and the balancer could hand out an App installation without the Commit +// statuses scope. Observed in stranske/Orchestrator on 2026-08-23: the Gate's own status post +// selected WORKFLOWS_APP and got a 403, the swallow left the previous status in place, and a +// fully green run kept a red `Gate / gate` that nothing could clear. +// --------------------------------------------------------------------------- + +test('TOKEN_CAPABILITIES: statuses is held by GITHUB_TOKEN and PAT but NOT by APP', () => { + assert.ok(balancer.TOKEN_CAPABILITIES.GITHUB_TOKEN.includes('statuses')); + assert.ok(balancer.TOKEN_CAPABILITIES.PAT.includes('statuses')); + assert.equal( + balancer.TOKEN_CAPABILITIES.APP.includes('statuses'), + false, + 'APP must not claim `statuses`: an App installation only has Commit statuses if it was ' + + 'granted them, and the installations in use were not. A wrong entry here is silent -- the ' + + 'balancer hands out a token that 403s on POST /statuses/{sha}.' + ); +}); + +// A multi-token "the App must not win on capacity" test was written and then REMOVED. Selection +// scores `percentRemaining + priority*10 + typeBonus + taskBonus`, so an ineligible App with more +// headroom should out-score a statuses-capable token -- but the test passed even with the alias +// deliberately broken, i.e. for a reason not established (getOptimalToken refreshes rate limits, +// which appears to discard seeded capacities). A test that passes for an unknown reason is a false +// comfort, not coverage, so the guard here is the two assertions below, both of which DO fail when +// the alias is reverted to ['write-repo']: the table itself, and the App-only selection. + +/** Register a single token of one type, healthy, so eligibility alone decides the answer. */ +function seedOnly(type) { + balancer.tokenRegistry.tokens.clear(); + balancer.tokenRegistry.lastRefresh = 0; + balancer.registerToken({ + id: type, + token: `fake-token-${type}`, + type, + source: type, + capabilities: balancer.TOKEN_CAPABILITIES[type], + priority: 5, + }); + const info = balancer.tokenRegistry.tokens.get(type); + info.rateLimit.remaining = 5000; + info.rateLimit.limit = 5000; + info.rateLimit.used = 0; + info.rateLimit.percentUsed = 0; + info.rateLimit.percentRemaining = 100; +} + +test('a broad write-repo request still accepts APP, so the fix narrowed nothing else', async () => { + // Asserted on ELIGIBILITY, not on who wins: selection is deterministic given equal capacity, + // so "APP shows up eventually" would never hold regardless of the capability tables. + seedOnly('APP'); + const selection = await balancer.getOptimalToken({ + capabilities: ['contents:write'], + minRemaining: 1, + }); + assert.equal( + selection?.source ?? null, + 'APP', + 'APP should still satisfy a generic write-repo request; if it does not, the capability change ' + + 'over-narrowed and every App-backed caller just lost its token' + ); +}); + +test('statuses:write with only an APP registered returns no token rather than a doomed one', async () => { + balancer.tokenRegistry.tokens.clear(); + balancer.tokenRegistry.lastRefresh = 0; + balancer.registerToken({ + id: 'APP', + token: 'fake-token-APP', + type: 'APP', + source: 'APP', + capabilities: balancer.TOKEN_CAPABILITIES.APP, + priority: 5, + }); + const info = balancer.tokenRegistry.tokens.get('APP'); + info.rateLimit.remaining = 5000; + info.rateLimit.limit = 5000; + info.rateLimit.percentRemaining = 100; + + const selection = await balancer.getOptimalToken({ + capabilities: ['statuses:write'], + minRemaining: 1, + }); + assert.equal( + selection?.source ?? null, + null, + 'handing back an APP that cannot write statuses is worse than handing back nothing: the ' + + 'caller falls through to its own github client, which is the token that actually has the scope' + ); +}); diff --git a/.github/scripts/github-api-with-retry.js b/.github/scripts/github-api-with-retry.js index 90b72cbe4..1cc677fa9 100755 --- a/.github/scripts/github-api-with-retry.js +++ b/.github/scripts/github-api-with-retry.js @@ -352,10 +352,20 @@ async function withRetry(fn, options = {}) { } if (integrationPermissionError && task === 'gate-commit-status') { + // NAME THE TOKEN. This swallow is deliberate -- a status post must not fail the Gate -- + // but until 2026-08-23 it said only "blocked by permissions", which reads as a repo + // misconfiguration and sent a reader to check `permissions:` blocks that were already + // correct. The real cause is WHICH token was selected, so the message has to carry it: + // a green run leaving a red status is otherwise indistinguishable from a settings problem. + const refusedBy = currentTokenSource || 'the workflow token'; logWithCore( core, 'warning', - 'Gate commit status update blocked by permissions; leaving existing status untouched.' + `Gate commit status update blocked by permissions (token: ${refusedBy}); ` + + 'leaving the EXISTING status in place, so a stale one can outlive this run. ' + + 'That token lacks the `statuses` scope: declare ' + + "`capabilities: ['statuses:write']` or pin the call to the workflow token with " + + '`env: {}`.' ); return null; } diff --git a/.github/scripts/token_load_balancer.js b/.github/scripts/token_load_balancer.js index 970a93aba..af6aa4935 100644 --- a/.github/scripts/token_load_balancer.js +++ b/.github/scripts/token_load_balancer.js @@ -65,8 +65,16 @@ const invalidAuthWarningMemory = new Set(); * Based on analysis of actual usage across workflows */ const TOKEN_CAPABILITIES = { - GITHUB_TOKEN: ['read-repo', 'write-repo', 'pr-update', 'labels', 'comments'], - PAT: ['read-repo', 'write-repo', 'pr-update', 'labels', 'comments', 'cross-repo', 'workflow-dispatch'], + GITHUB_TOKEN: ['read-repo', 'write-repo', 'pr-update', 'labels', 'comments', 'statuses'], + PAT: ['read-repo', 'write-repo', 'pr-update', 'labels', 'comments', 'cross-repo', 'workflow-dispatch', 'statuses'], + // NO `statuses` FOR APP, and that omission is measured, not assumed. A GitHub App only holds the + // scopes its INSTALLATION was granted, and the installations in use here do not include Commit + // statuses. Observed 2026-08-23 in stranske/Orchestrator: the Gate's own status post selected + // WORKFLOWS_APP and got `POST /repos/.../statuses/ - 403`, while the same job's + // GITHUB_TOKEN was granted `Statuses: write` and posted fine minutes earlier. + // If an App installation is later granted Commit statuses, add 'statuses' back here -- but + // verify against the installation, because a wrong entry here is silent: the balancer hands out + // a token that cannot do the job and the caller sees a 403 it did not ask for. APP: ['read-repo', 'write-repo', 'pr-update', 'labels', 'comments', 'workflow-dispatch'], }; @@ -140,7 +148,12 @@ const CAPABILITY_ALIASES = { 'rate_limit:read': ['read-repo'], 'deployments:write': ['write-repo'], 'checks:read': ['read-repo'], - 'statuses:write': ['write-repo'], + // `statuses:write` maps to its OWN capability, not to the generic `write-repo`. It aliased to + // `write-repo` until 2026-08-23, which all three token types claim -- so declaring + // `capabilities: ['statuses:write']` selected an App that cannot write statuses and the + // declaration was decorative. A capability alias must name what the API endpoint actually + // requires; collapsing a narrow scope into a broad one makes the filter unable to filter. + 'statuses:write': ['statuses'], }; function normalizeCapabilities(capabilities = []) { diff --git a/.github/workflows/pr-00-gate.yml b/.github/workflows/pr-00-gate.yml index 28b5792d1..b7c9245aa 100644 --- a/.github/workflows/pr-00-gate.yml +++ b/.github/workflows/pr-00-gate.yml @@ -1091,6 +1091,29 @@ jobs: github, core, task: 'gate-commit-status', + // `env: {}` PINS THIS CALL TO THE WORKFLOW TOKEN, and it is load-bearing. + // With the default `env: process.env` the token load balancer collects every App + // and PAT secret this job exposes and scores them + // (`percentRemaining + priority*10 + typeBonus + taskBonus`), so which token posts + // the Gate's status depends on rate-limit state and varies run to run. `POST /statuses/{sha}` needs the + // `statuses` scope: GITHUB_TOKEN has it here (this job declares `statuses: write`), + // an App installation only has it if it was granted Commit statuses -- and the + // installations in use are not. When the balancer picked the App the post 403'd and + // the swallow left the PREVIOUS status in place, so a green run kept a red + // `Gate / gate` that nothing could clear. Measured in stranske/Orchestrator on + // 2026-08-23: `Selected token: WORKFLOWS_APP` then `POST .../statuses/... - 403`, + // one line after `STATE: success`; a run minutes earlier posted fine with identical + // declared permissions because the balancer chose differently. + // + // WHY THE PIN AND NOT `capabilities: ['statuses:write']`: the declaration is now + // honest (that alias maps to its own `statuses` capability, which APP does not + // claim), but this file is distributed `create_only`, so a consumer's Gate can sit + // at an old revision while token_load_balancer.js is synced forward independently. + // The pin needs no agreement between the two files; the declaration would. + // + // This is one API call per run, so losing rate-limit spreading costs nothing. + // Retries still apply -- only the token source is fixed. + env: {}, }); const owner = context.repo.owner; const repo = context.repo.repo; diff --git a/config/template-drift-allowlist.txt b/config/template-drift-allowlist.txt index 2dc7c2701..01434675c 100644 --- a/config/template-drift-allowlist.txt +++ b/config/template-drift-allowlist.txt @@ -205,9 +205,9 @@ fingerprint_refreshed = 2026-08-23 [pair.19] main = .github/workflows/pr-00-gate.yml template = templates/consumer-repo/.github/workflows/pr-00-gate.yml -main_sha256 = e65e060fd26897215b04311a9b030460c5b95758ec83e68bb970b458f1264541 -template_sha256 = fb63d85eec60e6822b1c0b35466f5677653051b7db436bd74383fdbff154f1b9 -divergence = Named-secrets rollout 2026-08-23: both surfaces now pass named setup-api-client secret inputs instead of the whole-secrets-context blob, applied identically to root and consumer, and scoped to each workflow's DECLARED workflow_call secrets where that context is a closed set. Removing that handoff is the demonstrated remedy for GitHub's suspicious-workflow hold (agents-dedup then ran at run_attempt 1 with nothing approved after 22 days held). Prior divergence unchanged: Intentional divergence reviewed 2026-08-23: the source Gate runs Workflows-only package, ledger, diff-quality, and live sync-manifest issue-state checks with GH_TOKEN and GITHUB_TOKEN exported for the pytest guard; the consumer Gate uses published reusable workflows, pinned actions, and skips unavailable Workflows-local deliberate-break helpers. The consumer template must remain a bootstrap-safe deployment surface. +main_sha256 = 6ac00cf1b2ec2a4e5bac5f7ad280c404810ca69e256900360446e909fed349af +template_sha256 = a5780f26a137f1d0889e9ebc26deaaa40644b7cdedd3a01dccdb3e83ec9a057e +divergence = Gate commit-status token pinned 2026-08-23: both surfaces now pass `env: {}` to createTokenAwareRetry for the `gate-commit-status` call, applied IDENTICALLY to root and consumer, so the divergence between them is unchanged by it. Without the pin the balancer could select a token lacking the `statuses` scope; the post then 403s and the swallow leaves the PREVIOUS status in place, so a fully green Gate keeps a red `Gate / gate` that nothing clears and a sealed sync PR cannot merge (observed in stranske/Orchestrator, PR #54). Prior divergence unchanged: Named-secrets rollout 2026-08-23: both surfaces now pass named setup-api-client secret inputs instead of the whole-secrets-context blob, applied identically to root and consumer, and scoped to each workflow's DECLARED workflow_call secrets where that context is a closed set. Removing that handoff is the demonstrated remedy for GitHub's suspicious-workflow hold (agents-dedup then ran at run_attempt 1 with nothing approved after 22 days held). Prior divergence unchanged: Intentional divergence reviewed 2026-08-23: the source Gate runs Workflows-only package, ledger, diff-quality, and live sync-manifest issue-state checks with GH_TOKEN and GITHUB_TOKEN exported for the pytest guard; the consumer Gate uses published reusable workflows, pinned actions, and skips unavailable Workflows-local deliberate-break helpers. The consumer template must remain a bootstrap-safe deployment surface. divergence_reviewed = 2026-08-23 fingerprint_refreshed = 2026-08-23 diff --git a/templates/consumer-repo/.github/scripts/github-api-with-retry.js b/templates/consumer-repo/.github/scripts/github-api-with-retry.js index 90b72cbe4..1cc677fa9 100755 --- a/templates/consumer-repo/.github/scripts/github-api-with-retry.js +++ b/templates/consumer-repo/.github/scripts/github-api-with-retry.js @@ -352,10 +352,20 @@ async function withRetry(fn, options = {}) { } if (integrationPermissionError && task === 'gate-commit-status') { + // NAME THE TOKEN. This swallow is deliberate -- a status post must not fail the Gate -- + // but until 2026-08-23 it said only "blocked by permissions", which reads as a repo + // misconfiguration and sent a reader to check `permissions:` blocks that were already + // correct. The real cause is WHICH token was selected, so the message has to carry it: + // a green run leaving a red status is otherwise indistinguishable from a settings problem. + const refusedBy = currentTokenSource || 'the workflow token'; logWithCore( core, 'warning', - 'Gate commit status update blocked by permissions; leaving existing status untouched.' + `Gate commit status update blocked by permissions (token: ${refusedBy}); ` + + 'leaving the EXISTING status in place, so a stale one can outlive this run. ' + + 'That token lacks the `statuses` scope: declare ' + + "`capabilities: ['statuses:write']` or pin the call to the workflow token with " + + '`env: {}`.' ); return null; } diff --git a/templates/consumer-repo/.github/scripts/token_load_balancer.js b/templates/consumer-repo/.github/scripts/token_load_balancer.js index 970a93aba..af6aa4935 100644 --- a/templates/consumer-repo/.github/scripts/token_load_balancer.js +++ b/templates/consumer-repo/.github/scripts/token_load_balancer.js @@ -65,8 +65,16 @@ const invalidAuthWarningMemory = new Set(); * Based on analysis of actual usage across workflows */ const TOKEN_CAPABILITIES = { - GITHUB_TOKEN: ['read-repo', 'write-repo', 'pr-update', 'labels', 'comments'], - PAT: ['read-repo', 'write-repo', 'pr-update', 'labels', 'comments', 'cross-repo', 'workflow-dispatch'], + GITHUB_TOKEN: ['read-repo', 'write-repo', 'pr-update', 'labels', 'comments', 'statuses'], + PAT: ['read-repo', 'write-repo', 'pr-update', 'labels', 'comments', 'cross-repo', 'workflow-dispatch', 'statuses'], + // NO `statuses` FOR APP, and that omission is measured, not assumed. A GitHub App only holds the + // scopes its INSTALLATION was granted, and the installations in use here do not include Commit + // statuses. Observed 2026-08-23 in stranske/Orchestrator: the Gate's own status post selected + // WORKFLOWS_APP and got `POST /repos/.../statuses/ - 403`, while the same job's + // GITHUB_TOKEN was granted `Statuses: write` and posted fine minutes earlier. + // If an App installation is later granted Commit statuses, add 'statuses' back here -- but + // verify against the installation, because a wrong entry here is silent: the balancer hands out + // a token that cannot do the job and the caller sees a 403 it did not ask for. APP: ['read-repo', 'write-repo', 'pr-update', 'labels', 'comments', 'workflow-dispatch'], }; @@ -140,7 +148,12 @@ const CAPABILITY_ALIASES = { 'rate_limit:read': ['read-repo'], 'deployments:write': ['write-repo'], 'checks:read': ['read-repo'], - 'statuses:write': ['write-repo'], + // `statuses:write` maps to its OWN capability, not to the generic `write-repo`. It aliased to + // `write-repo` until 2026-08-23, which all three token types claim -- so declaring + // `capabilities: ['statuses:write']` selected an App that cannot write statuses and the + // declaration was decorative. A capability alias must name what the API endpoint actually + // requires; collapsing a narrow scope into a broad one makes the filter unable to filter. + 'statuses:write': ['statuses'], }; function normalizeCapabilities(capabilities = []) { diff --git a/templates/consumer-repo/.github/workflows/pr-00-gate.yml b/templates/consumer-repo/.github/workflows/pr-00-gate.yml index 351795909..4854916b7 100644 --- a/templates/consumer-repo/.github/workflows/pr-00-gate.yml +++ b/templates/consumer-repo/.github/workflows/pr-00-gate.yml @@ -1066,6 +1066,29 @@ jobs: github, core, task: 'gate-commit-status', + // `env: {}` PINS THIS CALL TO THE WORKFLOW TOKEN, and it is load-bearing. + // With the default `env: process.env` the token load balancer collects every App + // and PAT secret this job exposes and scores them + // (`percentRemaining + priority*10 + typeBonus + taskBonus`), so which token posts + // the Gate's status depends on rate-limit state and varies run to run. `POST /statuses/{sha}` needs the + // `statuses` scope: GITHUB_TOKEN has it here (this job declares `statuses: write`), + // an App installation only has it if it was granted Commit statuses -- and the + // installations in use are not. When the balancer picked the App the post 403'd and + // the swallow left the PREVIOUS status in place, so a green run kept a red + // `Gate / gate` that nothing could clear. Measured in stranske/Orchestrator on + // 2026-08-23: `Selected token: WORKFLOWS_APP` then `POST .../statuses/... - 403`, + // one line after `STATE: success`; a run minutes earlier posted fine with identical + // declared permissions because the balancer chose differently. + // + // WHY THE PIN AND NOT `capabilities: ['statuses:write']`: the declaration is now + // honest (that alias maps to its own `statuses` capability, which APP does not + // claim), but this file is distributed `create_only`, so a consumer's Gate can sit + // at an old revision while token_load_balancer.js is synced forward independently. + // The pin needs no agreement between the two files; the declaration would. + // + // This is one API call per run, so losing rate-limit spreading costs nothing. + // Retries still apply -- only the token source is fixed. + env: {}, }); const owner = context.repo.owner; const repo = context.repo.repo; From 263418b6239800d2eec21d55b4dcef1ccfc28e38 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 23 Aug 2026 22:45:25 +0000 Subject: [PATCH 2/2] chore: sync consumer templates --- .../scripts/sync_status_file_ignores.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/templates/consumer-repo/scripts/sync_status_file_ignores.py b/templates/consumer-repo/scripts/sync_status_file_ignores.py index 5c5f965d9..d6f714888 100755 --- a/templates/consumer-repo/scripts/sync_status_file_ignores.py +++ b/templates/consumer-repo/scripts/sync_status_file_ignores.py @@ -68,6 +68,18 @@ "workloop-state.md", # Test/coverage artifacts "coverage.xml", + # Per-run agent execution telemetry (HIGH conflict risk). reusable-codex-run.yml rewrites + # this into the checkout root every agent round to stage its upload-artifact step; while + # tracked, codex-autofix committed the diff onto whatever PR was open and the next PR + # collided with main's copy. Patterns, not the literal name, because the file is named after + # the role recorded; bounded by extension so langsmith_*.py sources stay committable. + # ROOT-ANCHORED, and that leading slash is load-bearing. Unanchored, a gitignore pattern + # matches at EVERY depth, so `langsmith-fleet*.json` also swallowed this repo's own tracked + # docs/contracts/schemas/langsmith-fleet-v1.schema.json -- verified with check-ignore, not + # inferred. Same near-miss as the node_modules work: the debris lands in the checkout ROOT, + # so that is the only place the pattern should reach. + "/langsmith-fleet*.json", + "/langsmith-fleet*.ndjson", # Wrong package manager artifacts (defense-in-depth) "Pipfile.lock", "poetry.lock", @@ -85,7 +97,7 @@ # Sync from: stranske/Workflows templates/consumer-repo/.gitignore # Validate: python scripts/sync_status_file_ignores.py --check # ============================================================================= -# Template-Version: 5 +# Template-Version: 6 # BEGIN WORKFLOWS STATUS FILES """