From e85edadb246e41d172a0c79fad147752d1df9ea9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 22 Aug 2026 15:57:39 +0000 Subject: [PATCH 01/19] chore: align autofix source pins --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 71a7ae097..c667f88aa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: # Python linting and formatting - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.16.2 # Synced with autofix-versions.env + rev: v0.16.4 # Synced with autofix-versions.env hooks: - id: ruff args: ["--fix"] From 63939632dd67730649574a3ae53e7b4f5598ab98 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 23:22:29 +0000 Subject: [PATCH 02/19] feat(sync): onboard iamkayleb/bukay and repoint consumer templates at this fork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bukay was built against an older Workflows version and had already been wired to iamkayleb/Workflows by hand (38 refs, zero to stranske). Syncing the upstream-owned templates into it would have reverted every one of those refs on the first delivery, so the repoint has to land before the registration does. Consumer templates (33 functional refs, 19 files) Repoint `uses:`, `repository:` sparse-checkouts, `runner_ref:`, and the Renovate fleet preset from stranske/Workflows to iamkayleb/Workflows. The three upstream action/workflow SHAs do not exist in this fork, so they are re-pinned to e85edad, which carries all three paths. Prose and comments are untouched, which keeps the three `template_sync: exact` files byte-identical with their root copies. Registration - REGISTERED_CONSUMER_REPOS += iamkayleb/bukay (15 consumers) - pr-00-gate.yml overwrite_repos += iamkayleb/bukay. bukay's Gate carries no generated-delivery-seal job, and a create_only Gate would never receive one, so Maint 71 would never merge its delivery. Its Node CI lives in ci.yml, which stays create_only and repo-owned. - bukay added to the canary set, replacing trip-planner (the validator accepts exactly 2-3 registered repos). A non-canary needs promote plus canary evidence, which this fork cannot produce against stranske repos. Coupled inventories langsmith_fleet.MANAGED_CONSUMER_REPOS, the observability allowlist (not-applicable until bukay produces a conformant artifact), and the generated Renovate ownership preset. Health 74 Eight template fingerprints re-baselined. The checker canonicalizes the pin ref but preserves the action path, so an owner change is real drift. Only the stale fingerprints were rewritten; every hand-written "do not align" rationale is preserved. Tests Count and membership assertions updated where the fleet inventory grew. Three assertions that covered root and template together were made owner-agnostic rather than repointed, since which reusable is called is the contract, not who hosts it. The renovate-ownership test now derives its opt-out set from the manifest instead of a hardcoded repo list, and test_workflow_llm_installs discovery matches `*/Workflows` — it had silently dropped three template workflows. 4375 passed, no new failures. The 9 remaining failures are pre-existing in this sandbox (Python 3.11 vs the repo's >=3.12 pin, and missing langchain deps); verified identical on a clean tree. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Df2tQ9HcgAW8M3VNrFT3gT --- .github/agents/registry.yml | 8 +-- .github/sync-manifest.yml | 1 + .../maint-68-sync-consumer-repos.yml | 1 + config/consumer_sync_canaries.json | 8 +-- config/langsmith_fleet_allowlist.json | 6 +++ config/template-drift-allowlist.txt | 35 +++++++------ docs/ops/CONSUMER_REPO_MAINTENANCE.md | 29 +++++++++++ renovate-presets/consumer-managed-paths.json | 17 +++++++ scripts/langsmith_fleet.py | 1 + .../consumer-repo/.github/agents/registry.yml | 8 +-- templates/consumer-repo/.github/renovate.json | 2 +- .../agents-71-codex-belt-dispatcher.yml | 2 +- .../workflows/agents-72-codex-belt-worker.yml | 4 +- .../workflows/agents-80-pr-event-hub.yml | 8 +-- .../workflows/agents-81-gate-followups.yml | 8 +-- .../.github/workflows/agents-auto-pilot.yml | 2 +- .../.github/workflows/agents-guard.yml | 4 +- .../.github/workflows/agents-issue-intake.yml | 4 +- .../workflows/agents-issue-optimizer.yml | 2 +- .../.github/workflows/agents-pr-health.yml | 2 +- .../.github/workflows/agents-verifier.yml | 2 +- .../workflows/agents-verify-to-new-pr.yml | 2 +- .../.github/workflows/autofix.yml | 2 +- .../workflows/backplane-conformance.yml | 2 +- .../consumer-repo/.github/workflows/ci.yml | 2 +- .../.github/workflows/cross-repo-smoke.yml | 2 +- .../.github/workflows/pr-00-gate.yml | 6 +-- .../pr-46-dependency-repair-contract.yml | 2 +- ...st_generate_consumer_renovate_ownership.py | 50 +++++++++++++++---- tests/scripts/test_langsmith_fleet.py | 2 +- .../test_metrics_dashboard_generator.py | 6 +-- tests/workflows/test_bot_comment_handler.py | 11 ++-- ...test_consumer_sync_create_only_evidence.py | 16 +++--- .../test_github_api_retry_standard.py | 6 ++- .../test_langsmith_metrics_dashboard.py | 1 + tests/workflows/test_renovate_fleet_policy.py | 9 +++- .../workflows/test_sync_manifest_delivery.py | 10 +++- tests/workflows/test_workflow_llm_installs.py | 7 ++- 38 files changed, 207 insertions(+), 83 deletions(-) diff --git a/.github/agents/registry.yml b/.github/agents/registry.yml index 6cc23eb91..a44f3f76d 100644 --- a/.github/agents/registry.yml +++ b/.github/agents/registry.yml @@ -21,7 +21,7 @@ model_profile_trial_contract: artifact_schema: workflows.model-profile-trial-result/v2 identity_authority: workflows-read-only-trial-artifact/v2 collector_identity_authority: github-actions-api/workflows-read-only-trial-artifact/v2 - runner_ref: stranske/Workflows/.github/workflows/reusable-model-profile-trial.yml@822e323eefba3edb640e0bd9c922caec6fffee65 + runner_ref: iamkayleb/Workflows/.github/workflows/reusable-model-profile-trial.yml@e85edadb246e41d172a0c79fad147752d1df9ea9 cli_version: 0.144.1 runtime_fallback_allowed: false auxiliary_evaluator_allowed: false @@ -52,7 +52,7 @@ execution_profiles: model: gpt-5.6-sol fallback_model: gpt-5.5 runner: reusable-model-profile-trial - runner_ref: stranske/Workflows/.github/workflows/reusable-model-profile-trial.yml@822e323eefba3edb640e0bd9c922caec6fffee65 + runner_ref: iamkayleb/Workflows/.github/workflows/reusable-model-profile-trial.yml@e85edadb246e41d172a0c79fad147752d1df9ea9 capacity_pool: codex-standard safety: read-only lifecycle: trial @@ -63,7 +63,7 @@ execution_profiles: model: gpt-5.6-terra fallback_model: gpt-5.5 runner: reusable-model-profile-trial - runner_ref: stranske/Workflows/.github/workflows/reusable-model-profile-trial.yml@822e323eefba3edb640e0bd9c922caec6fffee65 + runner_ref: iamkayleb/Workflows/.github/workflows/reusable-model-profile-trial.yml@e85edadb246e41d172a0c79fad147752d1df9ea9 capacity_pool: codex-standard safety: read-only lifecycle: trial @@ -74,7 +74,7 @@ execution_profiles: model: gpt-5.6-luna fallback_model: gpt-5.5 runner: reusable-model-profile-trial - runner_ref: stranske/Workflows/.github/workflows/reusable-model-profile-trial.yml@822e323eefba3edb640e0bd9c922caec6fffee65 + runner_ref: iamkayleb/Workflows/.github/workflows/reusable-model-profile-trial.yml@e85edadb246e41d172a0c79fad147752d1df9ea9 capacity_pool: codex-standard safety: read-only lifecycle: trial diff --git a/.github/sync-manifest.yml b/.github/sync-manifest.yml index 786eba03c..411ccfcad 100644 --- a/.github/sync-manifest.yml +++ b/.github/sync-manifest.yml @@ -35,6 +35,7 @@ workflows: sync_mode: create_only # Don't overwrite existing consumer Gate files; fresh repos can still receive this file, so follow #2158 before first seeding. overwrite_repos: - stranske/Template + - iamkayleb/bukay skip_repos: - repo: stranske/Manager-Database reason: "Maintains a fully custom Gate workflow; never overwrite (replaces the hard-coded custom_gate_repos list in maint-68)." diff --git a/.github/workflows/maint-68-sync-consumer-repos.yml b/.github/workflows/maint-68-sync-consumer-repos.yml index 15a1c1c6b..128a5df4d 100644 --- a/.github/workflows/maint-68-sync-consumer-repos.yml +++ b/.github/workflows/maint-68-sync-consumer-repos.yml @@ -80,6 +80,7 @@ env: stranske/learning-management-system stranske/Fine-Art-Archive stranske/Orchestrator + iamkayleb/bukay concurrency: group: sync-consumer-repos-${{ github.repository }}-${{ github.ref }} diff --git a/config/consumer_sync_canaries.json b/config/consumer_sync_canaries.json index 2378ca4be..202c8f768 100644 --- a/config/consumer_sync_canaries.json +++ b/config/consumer_sync_canaries.json @@ -3,12 +3,12 @@ "version": 1, "canaries": [ { - "repo": "stranske/Travel-Plan-Permission", - "capabilities": ["standard", "custom-gate"] + "repo": "iamkayleb/bukay", + "capabilities": ["node-tooling", "python-consumer", "lock-heavy"] }, { - "repo": "stranske/trip-planner", - "capabilities": ["lock-heavy", "node-tooling"] + "repo": "stranske/Travel-Plan-Permission", + "capabilities": ["standard", "custom-gate"] }, { "repo": "stranske/Portable-Alpha-Extension-Model", diff --git a/config/langsmith_fleet_allowlist.json b/config/langsmith_fleet_allowlist.json index bc385c3cb..9a53fb18b 100644 --- a/config/langsmith_fleet_allowlist.json +++ b/config/langsmith_fleet_allowlist.json @@ -24,6 +24,12 @@ "status": "not-applicable", "reason": "Local agent-orchestration tool repo; receives consumer sync for agent machinery but has no substantive LangSmith-traced runtime surface (offload-only Brain runs).", "registry_activation_condition": "Register when Orchestrator gains an LLM-backed runtime or agent-observability surface that should participate in fleet LangSmith metrics." + }, + { + "repo": "iamkayleb/bukay", + "status": "not-applicable", + "reason": "Newly registered consumer; receives fleet updates and runs the agent workflows but has no conformant langsmith-fleet.ndjson producer yet, so registry membership would add a zero-signal artifact expectation.", + "registry_activation_condition": "Register once bukay uploads a conformant langsmith-fleet.ndjson artifact from a trusted workflow path." } ] } diff --git a/config/template-drift-allowlist.txt b/config/template-drift-allowlist.txt index 250401002..2de3abfdb 100644 --- a/config/template-drift-allowlist.txt +++ b/config/template-drift-allowlist.txt @@ -44,22 +44,22 @@ main = .github/workflows/agents-63-issue-intake.yml template = templates/consumer-repo/.github/workflows/agents-issue-intake.yml main_sha256 = 200757cbb8d1801434ac3acb9e517a90ea37aa06e57ce925e8ac4480d829fc7f -template_sha256 = 9176b7cffc68dba50fa7ff9c6a2386383c237433ac5a656053eccdd202628c6d -reason = Intentional divergence re-reviewed 2026-08-16: root and consumer intake surfaces both remove the operator draft toggle and always hand off ready-for-review automation PRs; root retains its richer failure summary while the consumer remains a pinned, minimal bridge contract. +template_sha256 = 9674a8cbba932c5af668fb50f8d90362df4510447efb0b376b9433dece046a9a +reason = Intentional divergence re-reviewed 2026-08-16: root and consumer intake surfaces both remove the operator draft toggle and always hand off ready-for-review automation PRs; root retains its richer failure summary while the consumer remains a pinned, minimal bridge contract. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. Root workflows were not repointed; the owner divergence is the intended contract. [pair.2] main = .github/workflows/agents-71-codex-belt-dispatcher.yml template = templates/consumer-repo/.github/workflows/agents-71-codex-belt-dispatcher.yml main_sha256 = 0e9a4c7e7b120985fd9684a9e4f89049601b2bbc0b9da9bee5510d3ec1c3c175 -template_sha256 = 6a7b6e203f6ac2837bc09dc2dc6620faa37b72cb78068af1bb77779e246cb1dd -reason = Intentional divergence re-baselined 2026-08-16: workflow_dispatch inputs (force_issue, agent_key) are now passed to the github-script step via step-level env: and read through process.env instead of being interpolated into the script body, removing a script-injection surface that caused GitHub to block the workflow as possibly malicious in consumer repos. Applied identically to both surfaces; consumer action pinning and Codex-specific wording preserved. +template_sha256 = a797257d9e1145c41615ab12872d2d040edf663cdc72b16e99a96c7e03b59c81 +reason = Intentional divergence re-baselined 2026-08-16: workflow_dispatch inputs (force_issue, agent_key) are now passed to the github-script step via step-level env: and read through process.env instead of being interpolated into the script body, removing a script-injection surface that caused GitHub to block the workflow as possibly malicious in consumer repos. Applied identically to both surfaces; consumer action pinning and Codex-specific wording preserved. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. Root workflows were not repointed; the owner divergence is the intended contract. [pair.3] main = .github/workflows/agents-72-codex-belt-worker.yml template = templates/consumer-repo/.github/workflows/agents-72-codex-belt-worker.yml main_sha256 = 905e1a6487c44f0705b4c83294fa5cc9e2693cfdf2927994f6dc28912516b758 -template_sha256 = f8ce0688837030094e20af4aee01d25274dbffcb3eeec3fa5b34da16318f3993 -reason = Existing reviewed baseline drift re-baselined 2026-06-20: exported Orchestrator skill inputs were added to both root and consumer worker workflow_call surfaces while preserving consumer action pinning and guarded merge wording. +template_sha256 = 4b4ed9f19b25bfd7fa7afb75967f5fc3374cf30af755700ae87290530b1619aa +reason = Existing reviewed baseline drift re-baselined 2026-06-20: exported Orchestrator skill inputs were added to both root and consumer worker workflow_call surfaces while preserving consumer action pinning and guarded merge wording. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. Root workflows were not repointed; the owner divergence is the intended contract. [pair.4] main = .github/workflows/agents-73-codex-belt-conveyor.yml @@ -107,15 +107,15 @@ reason = Intentional divergence (re-baselined 2026-06-14): consumer template SHA main = .github/workflows/agents-guard.yml template = templates/consumer-repo/.github/workflows/agents-guard.yml main_sha256 = eb5a10b5246ab1aa94afa481ed40e6a330a44be935f581827f6a49527b73e19f -template_sha256 = a8bc6224681c3fbb3314a393135188bca1b1362439cfd509b81eceb1acc0e8cd -reason = Intentional divergence re-baselined 2026-06-30: root and consumer guard workflows differ for pinned consumer actions/App-token setup; stranske/Workflows digest pins were refreshed together in root and consumer guard surfaces after Renovate moved the Workflows digest to ebef44a. +template_sha256 = e56f95bb658f17060c8aa6985c8e3b8e4fc402df5571fe3279a785fd42b21745 +reason = Intentional divergence re-baselined 2026-06-30: root and consumer guard workflows differ for pinned consumer actions/App-token setup; stranske/Workflows digest pins were refreshed together in root and consumer guard surfaces after Renovate moved the Workflows digest to ebef44a. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. Root workflows were not repointed; the owner divergence is the intended contract. [pair.11] main = .github/workflows/agents-issue-optimizer.yml template = templates/consumer-repo/.github/workflows/agents-issue-optimizer.yml main_sha256 = 32a4dd4b3b2c744fe11abf0cfe60e47464fe4ca63836af2560ac5002f3912441 -template_sha256 = 145e47655f8153d50970ea51e99aa833e74698fce915a2c124e1463f1b194e85 -reason = Intentional divergence re-baselined 2026-08-11: root remains in-tree (scripts/langchain + .github/scripts/issue_format.py); consumer vendors those via Workflows sparse-checkout under workflows-scripts/. Shared behavioral contract includes live format eligibility checks, format-lease release when those checks skip work, checkout-aware path validation, explicit guard retry dispatch, and quoted lowercase identifier acceptance. Do not align wholesale — that would strip consumer action pins/token setup. +template_sha256 = ab3a44f201da0bd1dc21bb4d00f0ff54fd8892e1250a72eff2d32116fe5f6f4d +reason = Intentional divergence re-baselined 2026-08-11: root remains in-tree (scripts/langchain + .github/scripts/issue_format.py); consumer vendors those via Workflows sparse-checkout under workflows-scripts/. Shared behavioral contract includes live format eligibility checks, format-lease release when those checks skip work, checkout-aware path validation, explicit guard retry dispatch, and quoted lowercase identifier acceptance. Do not align wholesale — that would strip consumer action pins/token setup. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. Root workflows were not repointed; the owner divergence is the intended contract. [pair.12] main = .github/workflows/agents-keepalive-loop-reporter.yml @@ -135,8 +135,8 @@ reason = Intentional divergence re-reviewed 2026-08-13: root and consumer sweeps main = .github/workflows/agents-verifier.yml template = templates/consumer-repo/.github/workflows/agents-verifier.yml main_sha256 = 5e1cb04f45d27ccd30395eb804203677db130c7f1bcabf8054d80cb22c189e97 -template_sha256 = 9c803d40ce8b26f4450a60d6f821eb2cadbd59c42b47259d3394ad1d932fee0f -reason = Existing reviewed baseline drift; align the template or update this fingerprint deliberately. +template_sha256 = e288ad90d1b243af7b60f850f143f23a13ae4d874a7a632d927a6acefbbac01b +reason = Existing reviewed baseline drift; align the template or update this fingerprint deliberately. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. Root workflows were not repointed; the owner divergence is the intended contract. [pair.15] main = .github/workflows/agents-weekly-metrics.yml @@ -149,5 +149,12 @@ reason = Intentional divergence (re-baselined 2026-07-14): consumer template SHA main = .github/workflows/agents-auto-pilot.yml template = templates/consumer-repo/.github/workflows/agents-auto-pilot.yml main_sha256 = 7d3c491c9f45d1666a08e6b69c3aea487568b1eeeb9bcbe48dac3f5d3de69b93 -template_sha256 = 9a3d67230d348407a4cc7c98ff2d8709a77b065c2fa6b5401922518ceef808e1 -reason = Intentional divergence reviewed 2026-08-21: the Workflows-local auto-pilot retains Workflows-only PR-meta and keepalive fallbacks, while the consumer template dispatches only the consolidated Agents 80 and Agents 81 entry points. The sync manifest delivers the consumer-specific template so retired Workflows-local targets cannot be restored in consumer repositories. +template_sha256 = 877d440d20eb31964a0c387d5213cce65ed1bbd7df44a5e7bc1061fae367f5e8 +reason = Intentional divergence reviewed 2026-08-21: the Workflows-local auto-pilot retains Workflows-only PR-meta and keepalive fallbacks, while the consumer template dispatches only the consolidated Agents 80 and Agents 81 entry points. The sync manifest delivers the consumer-specific template so retired Workflows-local targets cannot be restored in consumer repositories. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. Root workflows were not repointed; the owner divergence is the intended contract. +[pair.17] +main = .github/workflows/agents-verify-to-new-pr.yml +template = templates/consumer-repo/.github/workflows/agents-verify-to-new-pr.yml +main_sha256 = 68d891295a386a2a0b6f07226eba358263ad5b3be9444fa2c4207264b70ea6b2 +template_sha256 = 02c0d9b92a15fcb97944cfd3c6b68ee79d645e05d7abcedad802f3e7b5015630 +reason = Consumer template resolves the control plane from iamkayleb/Workflows (this fork) while the root workflow keeps its own wiring. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. Root workflows were not repointed; the owner divergence is the intended contract. + diff --git a/docs/ops/CONSUMER_REPO_MAINTENANCE.md b/docs/ops/CONSUMER_REPO_MAINTENANCE.md index d84a124d3..9c921aeaf 100644 --- a/docs/ops/CONSUMER_REPO_MAINTENANCE.md +++ b/docs/ops/CONSUMER_REPO_MAINTENANCE.md @@ -561,6 +561,35 @@ of following the first-party default. If a reusable workflow fix must ship immediately, trigger: - `Maint 68 Sync Consumer Repos` only if template files changed +#### Control-plane owner in the consumer templates + +The consumer templates resolve reusable workflows, composite actions, and script +sparse-checkouts from **`iamkayleb/Workflows`** — this fork — not from +`stranske/Workflows`. `iamkayleb/bukay` was already wired that way by hand, so +syncing upstream-owned templates into it would have silently reverted 38 refs +back to upstream on the first delivery. + +Three consequences worth knowing before you touch a template workflow: + +- **Root workflows were deliberately not repointed.** The owner divergence + between a root workflow and its template twin is the intended contract, and + the affected pairs are re-baselined in `config/template-drift-allowlist.txt`. + Health 74 canonicalizes the `@` pin but *preserves the action path*, so + changing the owner registers as drift and needs a deliberate re-baseline. +- **Pinned refs must exist in this fork.** The upstream SHAs for + `generated-delivery-seal`, `setup-api-client`, and + `reusable-model-profile-trial.yml` are not in this fork's history; they are + pinned to `e85edad` here. Bumping a pin means picking a commit that exists in + `iamkayleb/Workflows`, not copying an upstream SHA. +- **Discovery predicates must stay owner-agnostic.** Tests that find template + callers by matching `stranske/Workflows` silently stop covering them after a + repoint. Match on the repository name (`*/Workflows`) instead. This already + dropped three template workflows out of `test_workflow_llm_installs` coverage. + +Prose references to `stranske/Workflows` in `README.md`, `docs/USAGE.md`, and +`docs/INTEGRATION_GUIDE.md` were left alone; they document the upstream project, +not this fork's delivery wiring. + ### Sync PR Branch Cleanup `maint-71-merge-sync-prs.yml` owns routine cleanup for `sync/workflows-*` diff --git a/renovate-presets/consumer-managed-paths.json b/renovate-presets/consumer-managed-paths.json index e32b783d8..7987c1037 100644 --- a/renovate-presets/consumer-managed-paths.json +++ b/renovate-presets/consumer-managed-paths.json @@ -5,6 +5,7 @@ { "description": "Maint 68 overwrites these 214 manifest-managed paths in every registered consumer; Renovate edits there are reverted on the next sync.", "matchRepositories": [ + "iamkayleb/bukay", "stranske/Collab-Admin", "stranske/Counter_Risk", "stranske/Fine-Art-Archive", @@ -238,6 +239,22 @@ ], "enabled": false }, + { + "description": "iamkayleb/bukay additionally has 7 manifest-managed path(s) that at least one other consumer owns via skip_repos, create_only, or an overwrite_repos opt-in.", + "matchRepositories": [ + "iamkayleb/bukay" + ], + "matchFileNames": [ + ".github/scripts/node_modules/balanced-match/**", + ".github/scripts/node_modules/brace-expansion/**", + ".github/scripts/node_modules/minimatch/**", + ".github/scripts/package.json", + ".github/workflows/pr-00-gate.yml", + "AGENTS.md", + "CLAUDE.md" + ], + "enabled": false + }, { "description": "stranske/Collab-Admin, stranske/Counter_Risk, stranske/Fine-Art-Archive, stranske/Inv-Man-Intake, stranske/Manager-Database, stranske/Pension-Data, stranske/Portable-Alpha-Extension-Model, stranske/Ready, stranske/Travel-Plan-Permission, stranske/learning-management-system additionally has 6 manifest-managed path(s) that at least one other consumer owns via skip_repos, create_only, or an overwrite_repos opt-in.", "matchRepositories": [ diff --git a/scripts/langsmith_fleet.py b/scripts/langsmith_fleet.py index ee54671ae..976193082 100644 --- a/scripts/langsmith_fleet.py +++ b/scripts/langsmith_fleet.py @@ -44,6 +44,7 @@ "stranske/learning-management-system", "stranske/Fine-Art-Archive", "stranske/Orchestrator", + "iamkayleb/bukay", } REQUIRED_ACTIVE_REPO_ISSUES = { "stranske/trip-planner": 1208, diff --git a/templates/consumer-repo/.github/agents/registry.yml b/templates/consumer-repo/.github/agents/registry.yml index 6cc23eb91..a44f3f76d 100644 --- a/templates/consumer-repo/.github/agents/registry.yml +++ b/templates/consumer-repo/.github/agents/registry.yml @@ -21,7 +21,7 @@ model_profile_trial_contract: artifact_schema: workflows.model-profile-trial-result/v2 identity_authority: workflows-read-only-trial-artifact/v2 collector_identity_authority: github-actions-api/workflows-read-only-trial-artifact/v2 - runner_ref: stranske/Workflows/.github/workflows/reusable-model-profile-trial.yml@822e323eefba3edb640e0bd9c922caec6fffee65 + runner_ref: iamkayleb/Workflows/.github/workflows/reusable-model-profile-trial.yml@e85edadb246e41d172a0c79fad147752d1df9ea9 cli_version: 0.144.1 runtime_fallback_allowed: false auxiliary_evaluator_allowed: false @@ -52,7 +52,7 @@ execution_profiles: model: gpt-5.6-sol fallback_model: gpt-5.5 runner: reusable-model-profile-trial - runner_ref: stranske/Workflows/.github/workflows/reusable-model-profile-trial.yml@822e323eefba3edb640e0bd9c922caec6fffee65 + runner_ref: iamkayleb/Workflows/.github/workflows/reusable-model-profile-trial.yml@e85edadb246e41d172a0c79fad147752d1df9ea9 capacity_pool: codex-standard safety: read-only lifecycle: trial @@ -63,7 +63,7 @@ execution_profiles: model: gpt-5.6-terra fallback_model: gpt-5.5 runner: reusable-model-profile-trial - runner_ref: stranske/Workflows/.github/workflows/reusable-model-profile-trial.yml@822e323eefba3edb640e0bd9c922caec6fffee65 + runner_ref: iamkayleb/Workflows/.github/workflows/reusable-model-profile-trial.yml@e85edadb246e41d172a0c79fad147752d1df9ea9 capacity_pool: codex-standard safety: read-only lifecycle: trial @@ -74,7 +74,7 @@ execution_profiles: model: gpt-5.6-luna fallback_model: gpt-5.5 runner: reusable-model-profile-trial - runner_ref: stranske/Workflows/.github/workflows/reusable-model-profile-trial.yml@822e323eefba3edb640e0bd9c922caec6fffee65 + runner_ref: iamkayleb/Workflows/.github/workflows/reusable-model-profile-trial.yml@e85edadb246e41d172a0c79fad147752d1df9ea9 capacity_pool: codex-standard safety: read-only lifecycle: trial diff --git a/templates/consumer-repo/.github/renovate.json b/templates/consumer-repo/.github/renovate.json index f63e2fb99..24ba43cee 100644 --- a/templates/consumer-repo/.github/renovate.json +++ b/templates/consumer-repo/.github/renovate.json @@ -1,4 +1,4 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["github>stranske/Workflows//renovate-presets/fleet"] + "extends": ["github>iamkayleb/Workflows//renovate-presets/fleet"] } diff --git a/templates/consumer-repo/.github/workflows/agents-71-codex-belt-dispatcher.yml b/templates/consumer-repo/.github/workflows/agents-71-codex-belt-dispatcher.yml index b9dfd0444..298d9b5d4 100644 --- a/templates/consumer-repo/.github/workflows/agents-71-codex-belt-dispatcher.yml +++ b/templates/consumer-repo/.github/workflows/agents-71-codex-belt-dispatcher.yml @@ -211,7 +211,7 @@ jobs: - name: Checkout (for retry helpers) uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: ${{ steps.workflows_ref.outputs.ref }} sparse-checkout: | .github/actions/setup-api-client diff --git a/templates/consumer-repo/.github/workflows/agents-72-codex-belt-worker.yml b/templates/consumer-repo/.github/workflows/agents-72-codex-belt-worker.yml index 80cc3d449..724f1af6b 100644 --- a/templates/consumer-repo/.github/workflows/agents-72-codex-belt-worker.yml +++ b/templates/consumer-repo/.github/workflows/agents-72-codex-belt-worker.yml @@ -321,7 +321,7 @@ jobs: - name: Checkout Workflows scripts uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: ${{ steps.workflows_ref.outputs.ref }} sparse-checkout: | .github/actions/setup-api-client @@ -592,7 +592,7 @@ jobs: if: ${{ steps.parallel.outputs.allowed == 'true' && (inputs.keepalive != true || steps.keepalive_gate.outputs.action != 'skip') }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: ${{ steps.workflows_ref.outputs.ref }} token: ${{ env.GH_BELT_TOKEN }} fetch-depth: 1 diff --git a/templates/consumer-repo/.github/workflows/agents-80-pr-event-hub.yml b/templates/consumer-repo/.github/workflows/agents-80-pr-event-hub.yml index 1a3ce1e85..b97c6beb0 100644 --- a/templates/consumer-repo/.github/workflows/agents-80-pr-event-hub.yml +++ b/templates/consumer-repo/.github/workflows/agents-80-pr-event-hub.yml @@ -194,7 +194,7 @@ jobs: needs.resolve.outputs.pr_number != '' && (needs.resolve.outputs.run_pr_meta == 'true' || needs.resolve.outputs.run_bot_comments == 'true') - uses: stranske/Workflows/.github/workflows/reusable-pr-context.yml@main + uses: iamkayleb/Workflows/.github/workflows/reusable-pr-context.yml@main with: pr_number: ${{ fromJSON(needs.resolve.outputs.pr_number) }} secrets: inherit @@ -206,7 +206,7 @@ jobs: needs.resolve.outputs.should_run == 'true' && needs.resolve.outputs.pr_number != '' && needs.resolve.outputs.run_pr_meta == 'true' - uses: stranske/Workflows/.github/workflows/reusable-20-pr-meta.yml@main + uses: iamkayleb/Workflows/.github/workflows/reusable-20-pr-meta.yml@main with: pr_number: ${{ fromJSON(needs.resolve.outputs.pr_number) }} comment_id: ${{ needs.resolve.outputs.comment_id }} @@ -230,7 +230,7 @@ jobs: (needs.resolve.outputs.gate_conclusion == 'success' && needs.pr_context.outputs.has_agent_label == 'true') ) - uses: stranske/Workflows/.github/workflows/reusable-bot-comment-handler.yml@main + uses: iamkayleb/Workflows/.github/workflows/reusable-bot-comment-handler.yml@main with: pr_number: ${{ fromJSON(needs.resolve.outputs.pr_number) }} dry_run: ${{ needs.resolve.outputs.dry_run == 'true' }} @@ -317,7 +317,7 @@ jobs: if: steps.check-merged.outputs.merged == 'true' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows token: ${{ secrets.SERVICE_BOT_PAT || github.token }} sparse-checkout: | config diff --git a/templates/consumer-repo/.github/workflows/agents-81-gate-followups.yml b/templates/consumer-repo/.github/workflows/agents-81-gate-followups.yml index ca885518b..e49491399 100644 --- a/templates/consumer-repo/.github/workflows/agents-81-gate-followups.yml +++ b/templates/consumer-repo/.github/workflows/agents-81-gate-followups.yml @@ -667,7 +667,7 @@ jobs: if: >- needs.evaluate.outputs.agent_type == 'codex' && needs.evaluate.outputs.dispatch_should_run == 'true' - uses: stranske/Workflows/.github/workflows/reusable-codex-run.yml@main + uses: iamkayleb/Workflows/.github/workflows/reusable-codex-run.yml@main secrets: CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }} # Use dedicated KEEPALIVE_APP for isolated rate limit pool (5000/hr) @@ -701,7 +701,7 @@ jobs: (needs.evaluate.outputs.action == 'run' || needs.evaluate.outputs.action == 'fix' || needs.evaluate.outputs.action == 'conflict') - uses: stranske/Workflows/.github/workflows/reusable-claude-run.yml@main + uses: iamkayleb/Workflows/.github/workflows/reusable-claude-run.yml@main secrets: inherit with: skip: >- @@ -1621,7 +1621,7 @@ jobs: needs.prepare.outputs.dispatch_should_run == 'true' && needs.prepare.outputs.agent_type == 'codex' name: Run Codex autofix - uses: stranske/Workflows/.github/workflows/reusable-codex-run.yml@main + uses: iamkayleb/Workflows/.github/workflows/reusable-codex-run.yml@main with: prompt_file: .github/codex/prompts/autofix_from_ci_failure.md mode: autofix @@ -1640,7 +1640,7 @@ jobs: needs.prepare.outputs.dispatch_should_run == 'true' && needs.prepare.outputs.agent_type == 'claude' name: Run Claude autofix - uses: stranske/Workflows/.github/workflows/reusable-claude-run.yml@main + uses: iamkayleb/Workflows/.github/workflows/reusable-claude-run.yml@main with: prompt_file: .github/codex/prompts/autofix_from_ci_failure.md mode: autofix diff --git a/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml b/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml index e620eec1a..cc30e0824 100644 --- a/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml +++ b/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml @@ -184,7 +184,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: token: ${{ steps.app_token.outputs.token || github.token }} - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: ${{ steps.workflows_ref.outputs.ref }} sparse-checkout: | .github/actions/setup-api-client diff --git a/templates/consumer-repo/.github/workflows/agents-guard.yml b/templates/consumer-repo/.github/workflows/agents-guard.yml index 038e8ec27..5afa47402 100644 --- a/templates/consumer-repo/.github/workflows/agents-guard.yml +++ b/templates/consumer-repo/.github/workflows/agents-guard.yml @@ -111,7 +111,7 @@ jobs: github.event_name == 'pull_request_target' && steps.eligibility.outputs.should-run == 'true' && steps.api_client_base.outputs.available != 'true' - uses: "stranske/Workflows/.github/actions/setup-api-client@ebef44a616c1da319b1dc96658978fd8f621a00c" # v1 + uses: "iamkayleb/Workflows/.github/actions/setup-api-client@e85edadb246e41d172a0c79fad147752d1df9ea9" # v1 with: secrets: ${{ toJSON(secrets) }} github_token: ${{ github.token }} @@ -180,7 +180,7 @@ jobs: steps.eligibility.outputs.should-run == 'true' && github.event_name == 'pull_request' && steps.api_client_head.outputs.available != 'true' - uses: "stranske/Workflows/.github/actions/setup-api-client@ebef44a616c1da319b1dc96658978fd8f621a00c" # v1 + uses: "iamkayleb/Workflows/.github/actions/setup-api-client@e85edadb246e41d172a0c79fad147752d1df9ea9" # v1 with: secrets: ${{ toJSON(secrets) }} github_token: ${{ github.token }} diff --git a/templates/consumer-repo/.github/workflows/agents-issue-intake.yml b/templates/consumer-repo/.github/workflows/agents-issue-intake.yml index 6fc3bbcf2..00181bfcc 100644 --- a/templates/consumer-repo/.github/workflows/agents-issue-intake.yml +++ b/templates/consumer-repo/.github/workflows/agents-issue-intake.yml @@ -176,7 +176,7 @@ jobs: if: | needs.route.outputs.should_run_bridge == 'true' && needs.check_labels.outputs.should_run == 'true' - uses: stranske/Workflows/.github/workflows/reusable-agents-issue-bridge.yml@main + uses: iamkayleb/Workflows/.github/workflows/reusable-agents-issue-bridge.yml@main with: agent: ${{ needs.check_labels.outputs.agent }} issue_number: ${{ needs.check_labels.outputs.issue_number }} @@ -204,7 +204,7 @@ jobs: id-token: write models: read pull-requests: write - uses: stranske/Workflows/.github/workflows/agents-63-issue-intake.yml@main + uses: iamkayleb/Workflows/.github/workflows/agents-63-issue-intake.yml@main with: intake_mode: "chatgpt_sync" source: ${{ inputs.topic_files }} diff --git a/templates/consumer-repo/.github/workflows/agents-issue-optimizer.yml b/templates/consumer-repo/.github/workflows/agents-issue-optimizer.yml index eec484a4c..cf3255ad4 100644 --- a/templates/consumer-repo/.github/workflows/agents-issue-optimizer.yml +++ b/templates/consumer-repo/.github/workflows/agents-issue-optimizer.yml @@ -198,7 +198,7 @@ jobs: if: steps.check.outputs.should_run == 'true' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows path: workflows-scripts sparse-checkout: | .github/scripts/issue_format.py diff --git a/templates/consumer-repo/.github/workflows/agents-pr-health.yml b/templates/consumer-repo/.github/workflows/agents-pr-health.yml index b4e8eb6ec..5c2fe37a4 100644 --- a/templates/consumer-repo/.github/workflows/agents-pr-health.yml +++ b/templates/consumer-repo/.github/workflows/agents-pr-health.yml @@ -52,7 +52,7 @@ concurrency: jobs: health: - uses: stranske/Workflows/.github/workflows/reusable-agents-pr-health.yml@main + uses: iamkayleb/Workflows/.github/workflows/reusable-agents-pr-health.yml@main with: dry_run: ${{ inputs.dry_run && 'true' || 'false' }} max_prs: ${{ inputs.max_prs || '10' }} diff --git a/templates/consumer-repo/.github/workflows/agents-verifier.yml b/templates/consumer-repo/.github/workflows/agents-verifier.yml index 45a64a449..f5ae3ad60 100644 --- a/templates/consumer-repo/.github/workflows/agents-verifier.yml +++ b/templates/consumer-repo/.github/workflows/agents-verifier.yml @@ -300,7 +300,7 @@ jobs: if: >- needs.check.outputs.should_run == 'true' && needs.check.outputs.fingerprint_should_run == 'true' - uses: stranske/Workflows/.github/workflows/reusable-agents-verifier.yml@main + uses: iamkayleb/Workflows/.github/workflows/reusable-agents-verifier.yml@main with: # CI workflows to wait for before running verifier ci_workflows: '["ci.yml", "pr-00-gate.yml"]' diff --git a/templates/consumer-repo/.github/workflows/agents-verify-to-new-pr.yml b/templates/consumer-repo/.github/workflows/agents-verify-to-new-pr.yml index d0682a3f4..86bae748b 100644 --- a/templates/consumer-repo/.github/workflows/agents-verify-to-new-pr.yml +++ b/templates/consumer-repo/.github/workflows/agents-verify-to-new-pr.yml @@ -68,7 +68,7 @@ jobs: if: steps.check-merged.outputs.merged == 'true' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows token: ${{ steps.select-token.outputs.token }} sparse-checkout: | .github/actions/setup-api-client diff --git a/templates/consumer-repo/.github/workflows/autofix.yml b/templates/consumer-repo/.github/workflows/autofix.yml index 47c2d3bf9..f343d5998 100644 --- a/templates/consumer-repo/.github/workflows/autofix.yml +++ b/templates/consumer-repo/.github/workflows/autofix.yml @@ -519,7 +519,7 @@ jobs: if: >- needs.resolve.outputs.should_run == 'true' && needs.resolve.outputs.dispatch_should_run == 'true' - uses: stranske/Workflows/.github/workflows/reusable-18-autofix.yml@main + uses: iamkayleb/Workflows/.github/workflows/reusable-18-autofix.yml@main with: pr_number: ${{ fromJson(needs.resolve.outputs.pr_number) }} pr_head_ref: ${{ needs.resolve.outputs.pr_head_ref }} diff --git a/templates/consumer-repo/.github/workflows/backplane-conformance.yml b/templates/consumer-repo/.github/workflows/backplane-conformance.yml index 3898ec1e0..7e2e21c98 100644 --- a/templates/consumer-repo/.github/workflows/backplane-conformance.yml +++ b/templates/consumer-repo/.github/workflows/backplane-conformance.yml @@ -52,7 +52,7 @@ jobs: conformance: needs: emit-reference-run - uses: stranske/Workflows/.github/workflows/reusable-backplane-conformance.yml@main + uses: iamkayleb/Workflows/.github/workflows/reusable-backplane-conformance.yml@main with: run_json_path: artifacts/reference/run.json manifest_path: artifacts/reference/manifest.json diff --git a/templates/consumer-repo/.github/workflows/ci.yml b/templates/consumer-repo/.github/workflows/ci.yml index 2715b8691..4f74073b1 100644 --- a/templates/consumer-repo/.github/workflows/ci.yml +++ b/templates/consumer-repo/.github/workflows/ci.yml @@ -31,7 +31,7 @@ on: jobs: python: name: Python CI - uses: stranske/Workflows/.github/workflows/reusable-10-ci-python.yml@main + uses: iamkayleb/Workflows/.github/workflows/reusable-10-ci-python.yml@main with: python-versions: '["3.12", "3.13"]' typecheck: true diff --git a/templates/consumer-repo/.github/workflows/cross-repo-smoke.yml b/templates/consumer-repo/.github/workflows/cross-repo-smoke.yml index 9742880ed..672f7d9f2 100644 --- a/templates/consumer-repo/.github/workflows/cross-repo-smoke.yml +++ b/templates/consumer-repo/.github/workflows/cross-repo-smoke.yml @@ -32,7 +32,7 @@ jobs: vars.CROSS_REPO_SMOKE_DEPENDENCY_REPO != '' && vars.CROSS_REPO_SMOKE_RUN_COMMAND != '' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) - uses: stranske/Workflows/.github/workflows/reusable-13-cross-repo-smoke.yml@main + uses: iamkayleb/Workflows/.github/workflows/reusable-13-cross-repo-smoke.yml@main with: dependency_repo: ${{ vars.CROSS_REPO_SMOKE_DEPENDENCY_REPO }} dependency_ref: ${{ vars.CROSS_REPO_SMOKE_DEPENDENCY_REF || 'main' }} diff --git a/templates/consumer-repo/.github/workflows/pr-00-gate.yml b/templates/consumer-repo/.github/workflows/pr-00-gate.yml index 8f5f0defe..87a4cc75d 100644 --- a/templates/consumer-repo/.github/workflows/pr-00-gate.yml +++ b/templates/consumer-repo/.github/workflows/pr-00-gate.yml @@ -171,7 +171,7 @@ jobs: contents: read steps: - name: Require an exact-head delivery seal - uses: stranske/Workflows/.github/actions/generated-delivery-seal@632eb20586f8403219d101e8a982b62efeb94104 + uses: iamkayleb/Workflows/.github/actions/generated-delivery-seal@e85edadb246e41d172a0c79fad147752d1df9ea9 python-ci: name: python ci @@ -184,7 +184,7 @@ jobs: needs.detect.outputs.is_python_code == 'true' && needs.detect.outputs.run_core == 'true' }} - uses: stranske/Workflows/.github/workflows/reusable-10-ci-python.yml@main + uses: iamkayleb/Workflows/.github/workflows/reusable-10-ci-python.yml@main secrets: inherit permissions: contents: read @@ -406,7 +406,7 @@ jobs: fromJSON(needs.detect.outputs.run_core || 'true') && fromJSON(needs.detect.outputs.docker_changed || 'false') }} - uses: stranske/Workflows/.github/workflows/reusable-12-ci-docker.yml@main + uses: iamkayleb/Workflows/.github/workflows/reusable-12-ci-docker.yml@main ledger-validation: name: ledger validation diff --git a/templates/consumer-repo/.github/workflows/pr-46-dependency-repair-contract.yml b/templates/consumer-repo/.github/workflows/pr-46-dependency-repair-contract.yml index a7ad4ca22..0fcb3c638 100644 --- a/templates/consumer-repo/.github/workflows/pr-46-dependency-repair-contract.yml +++ b/templates/consumer-repo/.github/workflows/pr-46-dependency-repair-contract.yml @@ -31,7 +31,7 @@ jobs: contains(github.event.pull_request.labels.*.name, 'dependency:repair-promotion') && (contains(github.event.pull_request.labels.*.name, 'workflow:source-dependabot') || contains(github.event.pull_request.labels.*.name, 'workflow_source_dependabot'))) - uses: stranske/Workflows/.github/workflows/reusable-19-dependency-repair-contract.yml@main + uses: iamkayleb/Workflows/.github/workflows/reusable-19-dependency-repair-contract.yml@main # `contract` is gated on the two trust labels, and a skipped job reports # success. Without this guard a same-repository promotion marker whose labels diff --git a/tests/scripts/test_generate_consumer_renovate_ownership.py b/tests/scripts/test_generate_consumer_renovate_ownership.py index fedf317d6..bf84f2f51 100644 --- a/tests/scripts/test_generate_consumer_renovate_ownership.py +++ b/tests/scripts/test_generate_consumer_renovate_ownership.py @@ -125,18 +125,50 @@ def test_manifest_managed_workflows_are_disabled_for_consumers(): ) +def _manifest_overwrite_repos(path: str) -> set[str]: + """Repos that opted a create_only path back into Maint 68 ownership.""" + manifest = yaml.safe_load(MANIFEST.read_text(encoding="utf-8")) or {} + for section in manifest.values(): + if not isinstance(section, list): + continue + for item in section: + if isinstance(item, dict) and item.get("target", item.get("source")) == path: + return set(item.get("overwrite_repos") or []) + raise AssertionError(f"{path} is not manifested") + + def test_create_only_targets_stay_visible_to_consumer_renovate(): - """`.github/workflows/ci.yml` is create_only, so consumers own their copy.""" + """create_only paths stay consumer-owned unless that repo opted into overwrite. + + The opt-out set is derived from each entry's `overwrite_repos` in the live + manifest rather than hardcoded here: a repo that opts in (stranske/Template for + byte-alignment, iamkayleb/bukay so the Gate receives the delivery-seal job) is + genuinely Maint-68-owned for that path, so its own Renovate must stay disabled + there. Hardcoding the list made this test fail the next time a repo opted in. + """ preset = committed_preset() - consumers = [ - repo for repo in registered_consumers() if repo not in {SOURCE_REPO, "stranske/Template"} - ] - for repo in consumers: - effective = disabled_paths(preset, repo) - assert ".github/workflows/ci.yml" not in effective - assert ".github/workflows/pr-00-gate.yml" not in effective - assert ".github/renovate.json" not in effective + for path in ( + ".github/workflows/ci.yml", + ".github/workflows/pr-00-gate.yml", + ".github/renovate.json", + ): + opted_in = _manifest_overwrite_repos(path) + for repo in registered_consumers(): + if repo == SOURCE_REPO or repo in opted_in: + continue + assert path not in disabled_paths(preset, repo), ( + f"{path} is create_only for {repo}; its own Renovate must still see it" + ) + + +def test_gate_overwrite_optin_is_managed_for_that_repo(): + """iamkayleb/bukay opted pr-00-gate.yml into overwrite, so Maint 68 owns it there.""" + effective = disabled_paths(committed_preset(), "iamkayleb/bukay") + + assert ".github/workflows/pr-00-gate.yml" in effective + # ci.yml stayed create_only for bukay, so the repo keeps owning its Node CI. + assert ".github/workflows/ci.yml" not in effective def test_overwrite_repos_opt_back_into_managed_paths(): diff --git a/tests/scripts/test_langsmith_fleet.py b/tests/scripts/test_langsmith_fleet.py index 660ce0387..a7adb631f 100644 --- a/tests/scripts/test_langsmith_fleet.py +++ b/tests/scripts/test_langsmith_fleet.py @@ -205,7 +205,7 @@ def test_markdown_summary_renders_mixed_valid_invalid_missing_rows() -> None: assert "- Invalid: 1" in markdown assert "- Missing: 1" in markdown assert "- Direct evidence: 3" in markdown - assert "- Not applicable: 4" in markdown + assert "- Not applicable: 5" in markdown # Per-repo status rows, one per status flavor. assert ( diff --git a/tests/scripts/test_metrics_dashboard_generator.py b/tests/scripts/test_metrics_dashboard_generator.py index 7ea44ce33..d7ce01777 100644 --- a/tests/scripts/test_metrics_dashboard_generator.py +++ b/tests/scripts/test_metrics_dashboard_generator.py @@ -257,7 +257,7 @@ def test_build_dashboard_from_path_includes_langsmith_fleet_status(tmp_path: Pat assert "- Stale: 0" in dashboard assert "- Invalid: 0" in dashboard assert "- Direct evidence: 3" in dashboard - assert "- Not applicable: 4" in dashboard + assert "- Not applicable: 5" in dashboard assert ( "| stranske/trip-planner | planner-runtime | artifact | " "stranske/trip-planner#1208 | valid |" in dashboard @@ -337,7 +337,7 @@ def summarize_with_fixed_now(*args: object, **kwargs: object) -> dict[str, objec assert "## LangSmith Fleet Artifact Status" in content assert "- Valid: 8" in content assert "- Direct evidence: 3" in content - assert "- Not applicable: 4" in content + assert "- Not applicable: 5" in content def test_build_dashboard_from_path_mixed_fleet_status(tmp_path: Path) -> None: @@ -366,7 +366,7 @@ def test_build_dashboard_from_path_mixed_fleet_status(tmp_path: Path) -> None: assert "- Invalid: 1" in dashboard assert "- Missing: 6" in dashboard assert "- Direct evidence: 3" in dashboard - assert "- Not applicable: 4" in dashboard + assert "- Not applicable: 5" in dashboard assert ( "| stranske/Workflows | agent-automation | artifact | " "stranske/Workflows#2150 | valid |" in dashboard diff --git a/tests/workflows/test_bot_comment_handler.py b/tests/workflows/test_bot_comment_handler.py index a4deaf367..59d7f67ad 100644 --- a/tests/workflows/test_bot_comment_handler.py +++ b/tests/workflows/test_bot_comment_handler.py @@ -180,8 +180,13 @@ def test_bot_comment_handler_callers_pass_app_client_id() -> None: reusable_jobs = [ job for job in workflow.get("jobs", {}).values() - if job.get("uses") - == "stranske/Workflows/.github/workflows/reusable-bot-comment-handler.yml@main" + # Owner-agnostic: the root caller resolves the reusable from the + # upstream control plane while the consumer template resolves it from + # this fork. Which reusable is called, and with which secrets, is what + # this test is about — not who hosts it. + if str(job.get("uses") or "").endswith( + "/Workflows/.github/workflows/reusable-bot-comment-handler.yml@main" + ) ] assert reusable_jobs, f"{caller_path} must call reusable-bot-comment-handler" @@ -322,7 +327,7 @@ def test_template_event_hub_uses_reusable_bot_comment_handler_defaults() -> None assert ( bot_comments_job.get("uses") - == "stranske/Workflows/.github/workflows/reusable-bot-comment-handler.yml@main" + == "iamkayleb/Workflows/.github/workflows/reusable-bot-comment-handler.yml@main" ) assert "ignored_paths" not in inputs diff --git a/tests/workflows/test_consumer_sync_create_only_evidence.py b/tests/workflows/test_consumer_sync_create_only_evidence.py index 72c5f51c8..3950d791d 100644 --- a/tests/workflows/test_consumer_sync_create_only_evidence.py +++ b/tests/workflows/test_consumer_sync_create_only_evidence.py @@ -32,11 +32,15 @@ def test_consumer_create_only_files_are_manifested() -> None: assert source in entries assert entries[source]["sync_mode"] == "create_only" - for source in ( - ".github/workflows/pr-00-gate.yml", - ".github/workflows/ci.yml", - ): - assert entries[source]["overwrite_repos"] == ["stranske/Template"] + # Gate overwrite is per-repo: stranske/Template stays byte-aligned with the + # canonical template, and iamkayleb/bukay opted in so the delivery-seal job + # reaches its Gate (a create_only Gate would never receive that job, and + # Maint 71 will not merge a delivery whose head is unsealed). + assert entries[".github/workflows/pr-00-gate.yml"]["overwrite_repos"] == [ + "stranske/Template", + "iamkayleb/bukay", + ] + assert entries[".github/workflows/ci.yml"]["overwrite_repos"] == ["stranske/Template"] # .github/dependabot.yml was intentionally dropped from the template and the # manifest in #2401 (P3b of the Renovate fleet migration): create_only sync @@ -50,7 +54,7 @@ def test_consumer_renovate_template_extends_fleet_preset() -> None: (REPO_ROOT / "templates/consumer-repo/.github/renovate.json").read_text(encoding="utf-8") ) - assert template["extends"] == ["github>stranske/Workflows//renovate-presets/fleet"] + assert template["extends"] == ["github>iamkayleb/Workflows//renovate-presets/fleet"] def test_fine_art_archive_jsonschema_renovate_exception_is_repo_scoped() -> None: diff --git a/tests/workflows/test_github_api_retry_standard.py b/tests/workflows/test_github_api_retry_standard.py index 1ef5a3f3a..920728004 100644 --- a/tests/workflows/test_github_api_retry_standard.py +++ b/tests/workflows/test_github_api_retry_standard.py @@ -159,7 +159,9 @@ def test_agents_verifier_callers_pass_checked_pr_number() -> None: for relative_path in caller_paths: workflow = _load_workflow(REPO_ROOT / relative_path) verifier_job = workflow["jobs"]["verifier"] - assert verifier_job["uses"] == ( - "stranske/Workflows/.github/workflows/reusable-agents-verifier.yml@main" + # Owner-agnostic: root resolves the reusable upstream, the consumer + # template resolves it from this fork. + assert str(verifier_job["uses"]).endswith( + "/Workflows/.github/workflows/reusable-agents-verifier.yml@main" ) assert verifier_job["with"]["pr_number"] == "${{ needs.check.outputs.pr_number }}" diff --git a/tests/workflows/test_langsmith_metrics_dashboard.py b/tests/workflows/test_langsmith_metrics_dashboard.py index 8d7824a85..7ab621013 100644 --- a/tests/workflows/test_langsmith_metrics_dashboard.py +++ b/tests/workflows/test_langsmith_metrics_dashboard.py @@ -43,6 +43,7 @@ def test_every_maintained_consumer_has_an_observability_state() -> None: "stranske/Ready", "stranske/Collab-Admin", "stranske/Orchestrator", + "iamkayleb/bukay", } assert {entry["status"] for entry in allowlist["repos"]} == {"not-applicable"} diff --git a/tests/workflows/test_renovate_fleet_policy.py b/tests/workflows/test_renovate_fleet_policy.py index 8e108f2d2..4c2686fb6 100644 --- a/tests/workflows/test_renovate_fleet_policy.py +++ b/tests/workflows/test_renovate_fleet_policy.py @@ -85,7 +85,10 @@ def test_lock_file_maintenance_has_the_same_explicit_weekly_cadence() -> None: def test_workflows_and_consumer_entrypoints_share_the_bounded_fleet_policy() -> None: - expected_preset = "github>stranske/Workflows//renovate-presets/fleet" + # Owner-agnostic: both entrypoints must extend exactly one fleet preset from a + # repository named Workflows. Root points at the upstream control plane; the + # consumer template points at this fork. + expected_preset_suffix = "/Workflows//renovate-presets/fleet" entrypoints = ( REPO_ROOT / "renovate.json", REPO_ROOT / "templates" / "consumer-repo" / ".github" / "renovate.json", @@ -93,7 +96,9 @@ def test_workflows_and_consumer_entrypoints_share_the_bounded_fleet_policy() -> for entrypoint in entrypoints: config = json.loads(entrypoint.read_text(encoding="utf-8")) - assert config["extends"] == [expected_preset] + assert len(config["extends"]) == 1 + assert config["extends"][0].startswith("github>") + assert config["extends"][0].endswith(expected_preset_suffix) preset = _preset() assert preset["prConcurrentLimit"] == preset["branchConcurrentLimit"] == 3 diff --git a/tests/workflows/test_sync_manifest_delivery.py b/tests/workflows/test_sync_manifest_delivery.py index f662f4971..f5abacabd 100644 --- a/tests/workflows/test_sync_manifest_delivery.py +++ b/tests/workflows/test_sync_manifest_delivery.py @@ -660,7 +660,15 @@ def test_gate_and_shared_mergers_hold_mutable_stable_deliveries() -> None: "@632eb20586f8403219d101e8a982b62efeb94104" ) assert action_ref in gate - assert action_ref in template_gate + # The consumer template resolves the seal from this fork, so it carries the + # same commit-pinned action under iamkayleb/Workflows. The pin is what matters: + # the seal must be evaluated from an immutable ref outside the consumer's own + # (sync-mutable) checkout, whichever owner hosts the control plane. + template_action_ref = ( + "iamkayleb/Workflows/.github/actions/generated-delivery-seal" + "@e85edadb246e41d172a0c79fad147752d1df9ea9" + ) + assert template_action_ref in template_gate assert ( "uses: ./.github/actions/path-classifier" not in gate.split("generated-delivery-seal:", 1)[1].split("\n python-ci:", 1)[0] diff --git a/tests/workflows/test_workflow_llm_installs.py b/tests/workflows/test_workflow_llm_installs.py index bc7b07fa7..6505beaf0 100644 --- a/tests/workflows/test_workflow_llm_installs.py +++ b/tests/workflows/test_workflow_llm_installs.py @@ -118,7 +118,12 @@ def _workflows_library_checkout_steps(workflow: dict) -> list[dict]: steps = [] for step in _iter_steps(workflow): with_block = step.get("with") or {} - if with_block.get("repository") == "stranske/Workflows" and "sparse-checkout" in with_block: + # Owner-agnostic on purpose: the consumer template resolves the control + # plane from whichever owner hosts this fork, so discovery must key on the + # repository NAME. Pinning the owner here silently dropped three template + # workflows out of coverage when the templates were repointed at the fork. + repository = str(with_block.get("repository") or "") + if repository.endswith("/Workflows") and "sparse-checkout" in with_block: steps.append(step) return steps From e0c0aa0f1b15bc55c7776eeff5c7b1756dfc846a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 00:17:05 +0000 Subject: [PATCH 03/19] fix(reusables): vendor the helper layer from this fork, not upstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A reusable runs in the CALLER's context. Repointing the consumer templates at iamkayleb/Workflows got consumers the fork's reusable *logic*, but each reusable then ran `actions/checkout` against `repository: stranske/Workflows` to vendor its helper scripts — so bukay would have run fork reusables over upstream scripts, and a script change in this fork would never have reached it. Helper layer (27 checkouts, 13 reusables) reusable-10-ci-python (5), reusable-20-pr-meta (5), reusable-18-autofix (3), reusable-agents-pr-health (3), reusable-16-agents (2), reusable-model-profile-trial (2), and one each in reusable-19-dependency-repair-contract, reusable-agents-verifier, reusable-backplane-conformance, reusable-{claude,codex,cursor,gemini}-run. 22 of the 27 are reachable from the reusables bukay calls. The upstream @main-only rationale in these files ("callers ride @main, hence main IS the pinned ref") holds when the control plane and the helper source are the same repo. In a fork they are not. Owner comparisons that were silently dead reusable-18-autofix derived the owner from github.workflow_ref and compared it to a literal `stranske/Workflows`. In this fork that branch never fired, so a SHA-pinned self-test silently degraded to upstream main. Now matches the repository name. Same fix in two test discovery predicates that had quietly stopped inspecting anything -- test_reusable_ci_no_hardcoded_ref_main had fallen to zero inspected checkouts and only its `checked >= 4` floor revealed it; test_reusable_run_shared_base asserted the literal owner string. Model-profile-trial is one unit Its caller `uses:`, its `runner_sha` input, the registry `runner_ref`, and the runner's own immutable-identity assertion (plus its remote-main freshness probe) must all name the same repo and commit, or the identity check fails closed against a fork checkout. Repointed together to e85edad, which carries the runner. Its assertions stay exact rather than owner-agnostic: for a read-only pinned trial arm, the owner is part of the guarantee. Not changed The 14 refs across 8 root agent workflows that call upstream reusables and actions. That is a fork-self-CI concern and does not reach consumers; holding it until the first bukay delivery lands. Documented in docs/ops/CONSUMER_REPO_MAINTENANCE.md along with the corrected claim that "root workflows were not repointed", which this commit makes partly false. 4371 passed, no new failures. Template drift 0 unallowlisted, actionlint clean. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Df2tQ9HcgAW8M3VNrFT3gT --- .../workflows/agents-model-profile-trial.yml | 4 +- .github/workflows/reusable-10-ci-python.yml | 10 ++--- .github/workflows/reusable-16-agents.yml | 4 +- .github/workflows/reusable-18-autofix.yml | 12 +++-- ...reusable-19-dependency-repair-contract.yml | 2 +- .github/workflows/reusable-20-pr-meta.yml | 12 ++--- .../workflows/reusable-agents-pr-health.yml | 6 +-- .../workflows/reusable-agents-verifier.yml | 2 +- .../reusable-backplane-conformance.yml | 4 +- .github/workflows/reusable-claude-run.yml | 2 +- .github/workflows/reusable-codex-run.yml | 2 +- .github/workflows/reusable-cursor-run.yml | 2 +- .github/workflows/reusable-gemini-run.yml | 2 +- .../reusable-model-profile-trial.yml | 8 ++-- config/template-drift-allowlist.txt | 16 +++---- docs/ops/CONSUMER_REPO_MAINTENANCE.md | 45 ++++++++++++++----- .../test_model_profile_trial_workflows.py | 6 +-- .../test_reusable_ci_no_hardcoded_ref_main.py | 8 +++- .../test_reusable_run_shared_base.py | 5 ++- 19 files changed, 93 insertions(+), 59 deletions(-) diff --git a/.github/workflows/agents-model-profile-trial.yml b/.github/workflows/agents-model-profile-trial.yml index 49f09a92a..563bef27b 100644 --- a/.github/workflows/agents-model-profile-trial.yml +++ b/.github/workflows/agents-model-profile-trial.yml @@ -49,7 +49,7 @@ permissions: jobs: trial: - uses: stranske/Workflows/.github/workflows/reusable-model-profile-trial.yml@822e323eefba3edb640e0bd9c922caec6fffee65 + uses: iamkayleb/Workflows/.github/workflows/reusable-model-profile-trial.yml@e85edadb246e41d172a0c79fad147752d1df9ea9 permissions: contents: read with: @@ -61,6 +61,6 @@ jobs: packet_hash: ${{ inputs.packet_hash }} launch_ordinal: ${{ fromJSON(inputs.launch_ordinal) }} expected_source_sha: ${{ inputs.expected_source_sha }} - runner_sha: 822e323eefba3edb640e0bd9c922caec6fffee65 + runner_sha: e85edadb246e41d172a0c79fad147752d1df9ea9 secrets: CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }} diff --git a/.github/workflows/reusable-10-ci-python.yml b/.github/workflows/reusable-10-ci-python.yml index a576dfe19..15a37e168 100644 --- a/.github/workflows/reusable-10-ci-python.yml +++ b/.github/workflows/reusable-10-ci-python.yml @@ -402,7 +402,7 @@ jobs: - name: Checkout Workflows repo for actions uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows # @main-only policy (issue #2346): `@main` is the single supported pin, # so checking out the helper layer at `main` is intentional and correct # — callers ride `@main`, hence main IS the pinned ref. The @@ -721,7 +721,7 @@ jobs: - name: Checkout Workflows helper uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows # @main-only policy (issue #2346): `@main` is the single supported pin, # so checking out the helper layer at `main` is intentional and correct # — callers ride `@main`, hence main IS the pinned ref. The @@ -1585,7 +1585,7 @@ jobs: if: ${{ inputs.cache }} uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows # @main-only policy (issue #2346): `@main` is the single supported pin, # so checking out the helper layer at `main` is intentional and correct # — callers ride `@main`, hence main IS the pinned ref. The @@ -2532,7 +2532,7 @@ jobs: }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows # @main-only policy (issue #2346): `@main` is the single supported pin, # so checking out the helper layer at `main` is intentional and correct # - callers ride `@main`, hence main IS the pinned ref. The @@ -2638,7 +2638,7 @@ jobs: - name: Checkout Workflows repo for actions uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows # @main-only policy (issue #2346): `@main` is the single supported pin, # so checking out the helper layer at `main` is intentional and correct # — callers ride `@main`, hence main IS the pinned ref. The diff --git a/.github/workflows/reusable-16-agents.yml b/.github/workflows/reusable-16-agents.yml index 617e58d0e..f4ea186d9 100644 --- a/.github/workflows/reusable-16-agents.yml +++ b/.github/workflows/reusable-16-agents.yml @@ -1018,7 +1018,7 @@ jobs: - name: Checkout resolver action uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: ${{ steps.workflows_workflow_ref.outputs.ref }} sparse-checkout: | .github/actions/resolve-default-branch @@ -1043,7 +1043,7 @@ jobs: - name: Checkout Workflows scripts uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: ${{ steps.workflows_ref.outputs.ref }} sparse-checkout: | scripts diff --git a/.github/workflows/reusable-18-autofix.yml b/.github/workflows/reusable-18-autofix.yml index c0757bddb..5e99e9967 100644 --- a/.github/workflows/reusable-18-autofix.yml +++ b/.github/workflows/reusable-18-autofix.yml @@ -253,7 +253,11 @@ jobs: set -euo pipefail workflow_repo="${WORKFLOW_REF%%/.github/workflows/*}" ref="main" - if [ "$workflow_repo" = "stranske/Workflows" ]; then + # Owner-agnostic on purpose: this asks "did the run start inside the + # control-plane repo itself?", so only the repository NAME is the signal. + # Comparing against a literal `stranske/Workflows` made the branch dead in + # a fork, silently degrading a self-test's pinned ref to `main`. + if [ "${workflow_repo##*/}" = "Workflows" ]; then ref="${WORKFLOW_REF##*@}" fi if [ -z "$ref" ] || [ "$ref" = "$WORKFLOW_REF" ]; then @@ -264,7 +268,7 @@ jobs: - name: Checkout resolver action uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: ${{ steps.workflows_workflow_ref.outputs.ref }} sparse-checkout: | .github/actions/resolve-default-branch @@ -284,7 +288,7 @@ jobs: - name: Checkout Workflows scripts (for autofix utilities) uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: ${{ steps.workflows_ref.outputs.ref }} sparse-checkout: | scripts @@ -1469,7 +1473,7 @@ jobs: if: steps.guard.outputs.skip != 'true' uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: ${{ steps.workflows_ref.outputs.ref }} sparse-checkout: | .github/actions/build-pr-comment diff --git a/.github/workflows/reusable-19-dependency-repair-contract.yml b/.github/workflows/reusable-19-dependency-repair-contract.yml index 7904ae751..8fcffb1ea 100644 --- a/.github/workflows/reusable-19-dependency-repair-contract.yml +++ b/.github/workflows/reusable-19-dependency-repair-contract.yml @@ -43,7 +43,7 @@ jobs: - name: Checkout trusted contract implementation uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: main sparse-checkout: | .github/scripts/dependency-repair-contract.js diff --git a/.github/workflows/reusable-20-pr-meta.yml b/.github/workflows/reusable-20-pr-meta.yml index e6d388930..679da9f9e 100644 --- a/.github/workflows/reusable-20-pr-meta.yml +++ b/.github/workflows/reusable-20-pr-meta.yml @@ -1,6 +1,6 @@ # Reusable PR Meta workflow for consumer repos # Provides keepalive detection and dispatch functionality using dual checkout pattern -# Scripts are fetched from stranske/Workflows - no local scripts needed in consumer repo +# Scripts are fetched from iamkayleb/Workflows - no local scripts needed in consumer repo name: Reusable 20 PR Meta on: @@ -149,7 +149,7 @@ jobs: - name: Checkout resolver action uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: ${{ steps.workflows_workflow_ref.outputs.ref }} sparse-checkout: | .github/actions/resolve-default-branch @@ -175,7 +175,7 @@ jobs: - name: Checkout Workflows scripts uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: ${{ steps.workflows_ref.outputs.ref }} sparse-checkout: | scripts @@ -291,7 +291,7 @@ jobs: - name: Checkout Workflows scripts uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: main sparse-checkout: | .github/scripts @@ -368,7 +368,7 @@ jobs: - name: Checkout Workflows scripts uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: main sparse-checkout: | .github/scripts @@ -525,7 +525,7 @@ jobs: - name: Checkout Workflows scripts uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: main sparse-checkout: | .github/scripts diff --git a/.github/workflows/reusable-agents-pr-health.yml b/.github/workflows/reusable-agents-pr-health.yml index 65c28c87a..37128e58a 100644 --- a/.github/workflows/reusable-agents-pr-health.yml +++ b/.github/workflows/reusable-agents-pr-health.yml @@ -216,7 +216,7 @@ jobs: - name: Checkout Workflows helpers uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows sparse-checkout: | .github/actions/setup-api-client .github/scripts/error_classifier.js @@ -593,7 +593,7 @@ jobs: - name: Checkout Workflows helpers uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows sparse-checkout: | .github/scripts/error_classifier.js .github/scripts/github-api-with-retry.js @@ -807,7 +807,7 @@ jobs: - name: Checkout Workflows helpers uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows sparse-checkout: | .github/scripts/error_classifier.js .github/scripts/github-api-with-retry.js diff --git a/.github/workflows/reusable-agents-verifier.yml b/.github/workflows/reusable-agents-verifier.yml index 3d5c0d3de..b1f94d296 100644 --- a/.github/workflows/reusable-agents-verifier.yml +++ b/.github/workflows/reusable-agents-verifier.yml @@ -118,7 +118,7 @@ jobs: uses: actions/checkout@v7 with: token: ${{ steps.app_token.outputs.token || github.token }} - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: ${{ steps.workflows_ref.outputs.ref }} sparse-checkout: | .github/actions/setup-api-client diff --git a/.github/workflows/reusable-backplane-conformance.yml b/.github/workflows/reusable-backplane-conformance.yml index 25e928dbf..81926210f 100644 --- a/.github/workflows/reusable-backplane-conformance.yml +++ b/.github/workflows/reusable-backplane-conformance.yml @@ -12,7 +12,7 @@ name: Reusable Backplane Conformance # evidence-object/v1); a consumer is never failed for not emitting a run.json. # # The reusable workflow reads the CANONICAL registry + schemas + validator from -# stranske/Workflows@ (default @main) -- never from the +# iamkayleb/Workflows@ (default @main) -- never from the # participant's copy -- so there is exactly one registry. on: @@ -94,7 +94,7 @@ jobs: - name: Checkout Workflows contract uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: ${{ inputs.contract_ref }} path: .backplane-contract diff --git a/.github/workflows/reusable-claude-run.yml b/.github/workflows/reusable-claude-run.yml index 73486908d..60dcde7c5 100644 --- a/.github/workflows/reusable-claude-run.yml +++ b/.github/workflows/reusable-claude-run.yml @@ -288,7 +288,7 @@ jobs: - name: Checkout Workflows run-base action uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: ${{ inputs.workflows_ref }} path: .workflows-actions sparse-checkout: | diff --git a/.github/workflows/reusable-codex-run.yml b/.github/workflows/reusable-codex-run.yml index fd36375b2..a393f9815 100644 --- a/.github/workflows/reusable-codex-run.yml +++ b/.github/workflows/reusable-codex-run.yml @@ -341,7 +341,7 @@ jobs: - name: Checkout Workflows run-base action uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: ${{ inputs.workflows_ref }} path: .workflows-actions sparse-checkout: | diff --git a/.github/workflows/reusable-cursor-run.yml b/.github/workflows/reusable-cursor-run.yml index 513eab677..bb2ef0e59 100644 --- a/.github/workflows/reusable-cursor-run.yml +++ b/.github/workflows/reusable-cursor-run.yml @@ -309,7 +309,7 @@ jobs: - name: Checkout Workflows scripts uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: ${{ inputs.workflows_ref }} path: .workflows-lib sparse-checkout: | diff --git a/.github/workflows/reusable-gemini-run.yml b/.github/workflows/reusable-gemini-run.yml index b6c8ca707..7023d03c5 100644 --- a/.github/workflows/reusable-gemini-run.yml +++ b/.github/workflows/reusable-gemini-run.yml @@ -317,7 +317,7 @@ jobs: - name: Checkout Workflows scripts uses: actions/checkout@v7 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: ${{ inputs.workflows_ref }} path: .workflows-lib sparse-checkout: | diff --git a/.github/workflows/reusable-model-profile-trial.yml b/.github/workflows/reusable-model-profile-trial.yml index 5f6dd8709..3df564048 100644 --- a/.github/workflows/reusable-model-profile-trial.yml +++ b/.github/workflows/reusable-model-profile-trial.yml @@ -61,13 +61,13 @@ jobs: env: PINNED_RUNNER_SHA: ${{ inputs.runner_sha }} PINNED_RUNNER_REF: >- - stranske/Workflows/.github/workflows/reusable-model-profile-trial.yml@${{ inputs.runner_sha }} + iamkayleb/Workflows/.github/workflows/reusable-model-profile-trial.yml@${{ inputs.runner_sha }} ZERO_SOURCE_MANIFEST: sha256:0000000000000000000000000000000000000000000000000000000000000000 steps: - name: Checkout immutable trial runner uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: ${{ inputs.runner_sha }} path: runner-src persist-credentials: false @@ -91,7 +91,7 @@ jobs: exit 1 fi remote_main="$( - git ls-remote https://github.com/stranske/Workflows.git refs/heads/main | + git ls-remote https://github.com/iamkayleb/Workflows.git refs/heads/main | awk 'NR == 1 { print $1 }' )" if [ -z "$remote_main" ] || [ "$remote_main" != "$EXPECTED_SOURCE_SHA" ]; then @@ -102,7 +102,7 @@ jobs: - name: Checkout exact trial source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - repository: stranske/Workflows + repository: iamkayleb/Workflows ref: ${{ inputs.expected_source_sha }} path: target-src persist-credentials: false diff --git a/config/template-drift-allowlist.txt b/config/template-drift-allowlist.txt index 2de3abfdb..070cd9998 100644 --- a/config/template-drift-allowlist.txt +++ b/config/template-drift-allowlist.txt @@ -45,21 +45,21 @@ main = .github/workflows/agents-63-issue-intake.yml template = templates/consumer-repo/.github/workflows/agents-issue-intake.yml main_sha256 = 200757cbb8d1801434ac3acb9e517a90ea37aa06e57ce925e8ac4480d829fc7f template_sha256 = 9674a8cbba932c5af668fb50f8d90362df4510447efb0b376b9433dece046a9a -reason = Intentional divergence re-reviewed 2026-08-16: root and consumer intake surfaces both remove the operator draft toggle and always hand off ready-for-review automation PRs; root retains its richer failure summary while the consumer remains a pinned, minimal bridge contract. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. Root workflows were not repointed; the owner divergence is the intended contract. +reason = Intentional divergence re-reviewed 2026-08-16: root and consumer intake surfaces both remove the operator draft toggle and always hand off ready-for-review automation PRs; root retains its richer failure summary while the consumer remains a pinned, minimal bridge contract. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. The paired root agent workflows were not repointed (only the reusables' vendored helper layer was), so the owner divergence in these pairs is the intended contract. [pair.2] main = .github/workflows/agents-71-codex-belt-dispatcher.yml template = templates/consumer-repo/.github/workflows/agents-71-codex-belt-dispatcher.yml main_sha256 = 0e9a4c7e7b120985fd9684a9e4f89049601b2bbc0b9da9bee5510d3ec1c3c175 template_sha256 = a797257d9e1145c41615ab12872d2d040edf663cdc72b16e99a96c7e03b59c81 -reason = Intentional divergence re-baselined 2026-08-16: workflow_dispatch inputs (force_issue, agent_key) are now passed to the github-script step via step-level env: and read through process.env instead of being interpolated into the script body, removing a script-injection surface that caused GitHub to block the workflow as possibly malicious in consumer repos. Applied identically to both surfaces; consumer action pinning and Codex-specific wording preserved. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. Root workflows were not repointed; the owner divergence is the intended contract. +reason = Intentional divergence re-baselined 2026-08-16: workflow_dispatch inputs (force_issue, agent_key) are now passed to the github-script step via step-level env: and read through process.env instead of being interpolated into the script body, removing a script-injection surface that caused GitHub to block the workflow as possibly malicious in consumer repos. Applied identically to both surfaces; consumer action pinning and Codex-specific wording preserved. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. The paired root agent workflows were not repointed (only the reusables' vendored helper layer was), so the owner divergence in these pairs is the intended contract. [pair.3] main = .github/workflows/agents-72-codex-belt-worker.yml template = templates/consumer-repo/.github/workflows/agents-72-codex-belt-worker.yml main_sha256 = 905e1a6487c44f0705b4c83294fa5cc9e2693cfdf2927994f6dc28912516b758 template_sha256 = 4b4ed9f19b25bfd7fa7afb75967f5fc3374cf30af755700ae87290530b1619aa -reason = Existing reviewed baseline drift re-baselined 2026-06-20: exported Orchestrator skill inputs were added to both root and consumer worker workflow_call surfaces while preserving consumer action pinning and guarded merge wording. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. Root workflows were not repointed; the owner divergence is the intended contract. +reason = Existing reviewed baseline drift re-baselined 2026-06-20: exported Orchestrator skill inputs were added to both root and consumer worker workflow_call surfaces while preserving consumer action pinning and guarded merge wording. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. The paired root agent workflows were not repointed (only the reusables' vendored helper layer was), so the owner divergence in these pairs is the intended contract. [pair.4] main = .github/workflows/agents-73-codex-belt-conveyor.yml @@ -108,14 +108,14 @@ main = .github/workflows/agents-guard.yml template = templates/consumer-repo/.github/workflows/agents-guard.yml main_sha256 = eb5a10b5246ab1aa94afa481ed40e6a330a44be935f581827f6a49527b73e19f template_sha256 = e56f95bb658f17060c8aa6985c8e3b8e4fc402df5571fe3279a785fd42b21745 -reason = Intentional divergence re-baselined 2026-06-30: root and consumer guard workflows differ for pinned consumer actions/App-token setup; stranske/Workflows digest pins were refreshed together in root and consumer guard surfaces after Renovate moved the Workflows digest to ebef44a. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. Root workflows were not repointed; the owner divergence is the intended contract. +reason = Intentional divergence re-baselined 2026-06-30: root and consumer guard workflows differ for pinned consumer actions/App-token setup; stranske/Workflows digest pins were refreshed together in root and consumer guard surfaces after Renovate moved the Workflows digest to ebef44a. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. The paired root agent workflows were not repointed (only the reusables' vendored helper layer was), so the owner divergence in these pairs is the intended contract. [pair.11] main = .github/workflows/agents-issue-optimizer.yml template = templates/consumer-repo/.github/workflows/agents-issue-optimizer.yml main_sha256 = 32a4dd4b3b2c744fe11abf0cfe60e47464fe4ca63836af2560ac5002f3912441 template_sha256 = ab3a44f201da0bd1dc21bb4d00f0ff54fd8892e1250a72eff2d32116fe5f6f4d -reason = Intentional divergence re-baselined 2026-08-11: root remains in-tree (scripts/langchain + .github/scripts/issue_format.py); consumer vendors those via Workflows sparse-checkout under workflows-scripts/. Shared behavioral contract includes live format eligibility checks, format-lease release when those checks skip work, checkout-aware path validation, explicit guard retry dispatch, and quoted lowercase identifier acceptance. Do not align wholesale — that would strip consumer action pins/token setup. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. Root workflows were not repointed; the owner divergence is the intended contract. +reason = Intentional divergence re-baselined 2026-08-11: root remains in-tree (scripts/langchain + .github/scripts/issue_format.py); consumer vendors those via Workflows sparse-checkout under workflows-scripts/. Shared behavioral contract includes live format eligibility checks, format-lease release when those checks skip work, checkout-aware path validation, explicit guard retry dispatch, and quoted lowercase identifier acceptance. Do not align wholesale — that would strip consumer action pins/token setup. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. The paired root agent workflows were not repointed (only the reusables' vendored helper layer was), so the owner divergence in these pairs is the intended contract. [pair.12] main = .github/workflows/agents-keepalive-loop-reporter.yml @@ -136,7 +136,7 @@ main = .github/workflows/agents-verifier.yml template = templates/consumer-repo/.github/workflows/agents-verifier.yml main_sha256 = 5e1cb04f45d27ccd30395eb804203677db130c7f1bcabf8054d80cb22c189e97 template_sha256 = e288ad90d1b243af7b60f850f143f23a13ae4d874a7a632d927a6acefbbac01b -reason = Existing reviewed baseline drift; align the template or update this fingerprint deliberately. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. Root workflows were not repointed; the owner divergence is the intended contract. +reason = Existing reviewed baseline drift; align the template or update this fingerprint deliberately. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. The paired root agent workflows were not repointed (only the reusables' vendored helper layer was), so the owner divergence in these pairs is the intended contract. [pair.15] main = .github/workflows/agents-weekly-metrics.yml @@ -150,11 +150,11 @@ main = .github/workflows/agents-auto-pilot.yml template = templates/consumer-repo/.github/workflows/agents-auto-pilot.yml main_sha256 = 7d3c491c9f45d1666a08e6b69c3aea487568b1eeeb9bcbe48dac3f5d3de69b93 template_sha256 = 877d440d20eb31964a0c387d5213cce65ed1bbd7df44a5e7bc1061fae367f5e8 -reason = Intentional divergence reviewed 2026-08-21: the Workflows-local auto-pilot retains Workflows-only PR-meta and keepalive fallbacks, while the consumer template dispatches only the consolidated Agents 80 and Agents 81 entry points. The sync manifest delivers the consumer-specific template so retired Workflows-local targets cannot be restored in consumer repositories. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. Root workflows were not repointed; the owner divergence is the intended contract. +reason = Intentional divergence reviewed 2026-08-21: the Workflows-local auto-pilot retains Workflows-only PR-meta and keepalive fallbacks, while the consumer template dispatches only the consolidated Agents 80 and Agents 81 entry points. The sync manifest delivers the consumer-specific template so retired Workflows-local targets cannot be restored in consumer repositories. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. The paired root agent workflows were not repointed (only the reusables' vendored helper layer was), so the owner divergence in these pairs is the intended contract. [pair.17] main = .github/workflows/agents-verify-to-new-pr.yml template = templates/consumer-repo/.github/workflows/agents-verify-to-new-pr.yml main_sha256 = 68d891295a386a2a0b6f07226eba358263ad5b3be9444fa2c4207264b70ea6b2 template_sha256 = 02c0d9b92a15fcb97944cfd3c6b68ee79d645e05d7abcedad802f3e7b5015630 -reason = Consumer template resolves the control plane from iamkayleb/Workflows (this fork) while the root workflow keeps its own wiring. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. Root workflows were not repointed; the owner divergence is the intended contract. +reason = Consumer template resolves the control plane from iamkayleb/Workflows (this fork) while the root workflow keeps its own wiring. Template fingerprint re-baselined 2026-08-22: the consumer templates now resolve reusable workflows, composite actions, and script sparse-checkouts from iamkayleb/Workflows (this fork) instead of stranske/Workflows, so every template copy's normalized content changed. The paired root agent workflows were not repointed (only the reusables' vendored helper layer was), so the owner divergence in these pairs is the intended contract. diff --git a/docs/ops/CONSUMER_REPO_MAINTENANCE.md b/docs/ops/CONSUMER_REPO_MAINTENANCE.md index 9c921aeaf..71b7fa1bc 100644 --- a/docs/ops/CONSUMER_REPO_MAINTENANCE.md +++ b/docs/ops/CONSUMER_REPO_MAINTENANCE.md @@ -569,22 +569,45 @@ sparse-checkouts from **`iamkayleb/Workflows`** — this fork — not from syncing upstream-owned templates into it would have silently reverted 38 refs back to upstream on the first delivery. -Three consequences worth knowing before you touch a template workflow: - -- **Root workflows were deliberately not repointed.** The owner divergence - between a root workflow and its template twin is the intended contract, and - the affected pairs are re-baselined in `config/template-drift-allowlist.txt`. - Health 74 canonicalizes the `@` pin but *preserves the action path*, so - changing the owner registers as drift and needs a deliberate re-baseline. +The reusables' **vendored helper layer** is repointed too, and that part is easy +to miss. A reusable runs in the *caller's* context: before this change, a consumer +calling `iamkayleb/Workflows/.../reusable-10-ci-python.yml@main` got fork reusable +logic and then a `repository: stranske/Workflows` checkout that vendored its helper +scripts from upstream. All 27 of those checkouts across the 13 reusables now name +this fork, so a script change here actually reaches consumers. + +Four consequences worth knowing before you touch a template or reusable workflow: + +- **The paired root agent workflows were deliberately not repointed.** Only the + reusables' helper layer was, plus `agents-model-profile-trial.yml` (its caller, + registry `runner_ref`, and the runner's own immutability assertion are one unit + and must name the same commit). The owner divergence between a root agent + workflow and its template twin is the intended contract, and those pairs are + re-baselined in `config/template-drift-allowlist.txt`. Health 74 canonicalizes + the `@` pin but *preserves the action path*, so an owner change registers + as drift and needs a deliberate re-baseline. - **Pinned refs must exist in this fork.** The upstream SHAs for `generated-delivery-seal`, `setup-api-client`, and `reusable-model-profile-trial.yml` are not in this fork's history; they are pinned to `e85edad` here. Bumping a pin means picking a commit that exists in `iamkayleb/Workflows`, not copying an upstream SHA. -- **Discovery predicates must stay owner-agnostic.** Tests that find template - callers by matching `stranske/Workflows` silently stop covering them after a - repoint. Match on the repository name (`*/Workflows`) instead. This already - dropped three template workflows out of `test_workflow_llm_installs` coverage. +- **Discovery predicates and owner comparisons must be owner-agnostic.** Anything + that matches a literal `stranske/Workflows` becomes a silent no-op after a + repoint — it does not fail, it just stops seeing anything. Match on the + repository *name* (`*/Workflows`) instead. Three cases have already bitten: + `test_workflow_llm_installs` dropped three template workflows from coverage, + `test_reusable_ci_no_hardcoded_ref_main` fell to zero inspected checkouts (its + `checked >= 4` floor is the only reason anyone noticed), and + `reusable-18-autofix.yml` had a dead `if` branch that silently degraded a + SHA-pinned call to `main`. Keep a count floor on any such discovery loop. +- **Still pointing upstream, on purpose, for now.** 14 refs across 8 root agent + workflows (`agents-keepalive-loop.yml`, `agents-autofix-loop.yml`, + `agents-guard.yml`, `agents-verifier.yml`, `agents-bot-comment-handler.yml`, + `pr-00-gate.yml`, `pr-46-dependency-repair-contract.yml`) still call upstream + reusables and actions, so this fork's own CI exercises upstream code. That is a + fork-self-CI concern only — it does not affect consumers, which reach the fork + through the templates. `maint-69/70/71-auto-fix-integration` also target + `stranske/Workflows-Integration-Tests`, which this fork cannot write to. Prose references to `stranske/Workflows` in `README.md`, `docs/USAGE.md`, and `docs/INTEGRATION_GUIDE.md` were left alone; they document the upstream project, diff --git a/tests/workflows/test_model_profile_trial_workflows.py b/tests/workflows/test_model_profile_trial_workflows.py index f2bad5027..c818d7ffd 100644 --- a/tests/workflows/test_model_profile_trial_workflows.py +++ b/tests/workflows/test_model_profile_trial_workflows.py @@ -42,7 +42,7 @@ def test_dispatch_shim_is_single_arm_and_calls_only_pinned_reusable_runner(): assert list(workflow["jobs"]) == ["trial"] runner_ref = workflow["jobs"]["trial"]["uses"] assert re.fullmatch( - r"stranske/Workflows/\.github/workflows/" r"reusable-model-profile-trial\.yml@[0-9a-f]{40}", + r"iamkayleb/Workflows/\.github/workflows/" r"reusable-model-profile-trial\.yml@[0-9a-f]{40}", runner_ref, ) runner_sha = workflow["jobs"]["trial"]["with"]["runner_sha"] @@ -65,7 +65,7 @@ def test_reusable_runner_is_read_only_exact_cli_and_has_no_write_lane(): assert "--ignore-user-config" in source assert "persist-credentials: false" in source assert "expected_source_sha must equal current remote main before auth" in source - assert "git ls-remote https://github.com/stranske/Workflows.git refs/heads/main" in source + assert "git ls-remote https://github.com/iamkayleb/Workflows.git refs/heads/main" in source assert "target-src/scripts/" not in source assert "provider_resolved" not in source forbidden = ( @@ -109,7 +109,7 @@ def test_runner_uses_separate_pinned_helper_checkout_and_full_action_shas(): ] assert len(checkouts) == 2 assert checkouts[0]["with"] == { - "repository": "stranske/Workflows", + "repository": "iamkayleb/Workflows", "ref": "${{ inputs.runner_sha }}", "path": "runner-src", "persist-credentials": False, diff --git a/tests/workflows/test_reusable_ci_no_hardcoded_ref_main.py b/tests/workflows/test_reusable_ci_no_hardcoded_ref_main.py index b861857ae..5356af6ac 100644 --- a/tests/workflows/test_reusable_ci_no_hardcoded_ref_main.py +++ b/tests/workflows/test_reusable_ci_no_hardcoded_ref_main.py @@ -88,7 +88,7 @@ def test_workflows_ref_input_declared() -> None: def test_workflows_checkouts_use_workflows_ref_input() -> None: - """Every ``stranske/Workflows`` helper checkout resolves via the input.""" + """Every Workflows helper checkout resolves via the input.""" data = _load_workflow() jobs = data.get("jobs", {}) checked = 0 @@ -98,7 +98,11 @@ def test_workflows_checkouts_use_workflows_ref_input() -> None: if not uses.startswith("actions/checkout"): continue with_block = step.get("with", {}) or {} - if with_block.get("repository") != "stranske/Workflows": + # Owner-agnostic: match the repository NAME, not the owner. This guard + # is about the ref, and pinning the owner here turned it into a silent + # no-op the moment the helper layer was repointed at a fork — the + # `checked >= 4` floor below is what caught it. + if not str(with_block.get("repository") or "").endswith("/Workflows"): continue ref = str(with_block.get("ref", "")) checked += 1 diff --git a/tests/workflows/test_reusable_run_shared_base.py b/tests/workflows/test_reusable_run_shared_base.py index 8fbe68e6f..5c9fd0c05 100644 --- a/tests/workflows/test_reusable_run_shared_base.py +++ b/tests/workflows/test_reusable_run_shared_base.py @@ -26,6 +26,7 @@ from __future__ import annotations import os +import re import subprocess from pathlib import Path @@ -210,7 +211,9 @@ def test_extracted_setup_steps_not_duplicated_in_runners(workflow_rel: str) -> N # Runtime auth selection and the Workflows scripts checkout remain shared in # the composite; the target checkout is intentionally caller-owned. - assert "repository: stranske/Workflows" in src + # Owner-agnostic: the helper layer is vendored from whichever owner hosts the + # control plane, so assert on the repository name. + assert re.search(r"repository: \S+/Workflows\b", src) assert sum(_uses_base(step) == "actions/create-github-app-token" for step in steps) == 1, ( f"{workflow_rel}: only the run-base bootstrap checkout may mint an App " "token in the runner; shared runtime auth still belongs in " From 565d6f9db5d93899e86aa57fb9555154665f0f4f Mon Sep 17 00:00:00 2001 From: Caleb Ogbike Date: Mon, 24 Aug 2026 07:07:11 +0100 Subject: [PATCH 04/19] corpus: grow verifier eval corpus from realized PR outcomes (#82) --- config/model_eval_corpus_staging.json | 270 ++++++++++++++++++++++++++ 1 file changed, 270 insertions(+) diff --git a/config/model_eval_corpus_staging.json b/config/model_eval_corpus_staging.json index c3871ccf4..d25f44841 100644 --- a/config/model_eval_corpus_staging.json +++ b/config/model_eval_corpus_staging.json @@ -298,6 +298,276 @@ "category": "clean-pass", "provenance": "harvested", "harvested_at": "2026-08-17" + }, + { + "case_id": "workflows-2833", + "repo": "stranske/Workflows", + "pr": 2833, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "workflows-2832", + "repo": "stranske/Workflows", + "pr": 2832, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "workflows-2831", + "repo": "stranske/Workflows", + "pr": 2831, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "travel-plan-permission-1350", + "repo": "stranske/Travel-Plan-Permission", + "pr": 1350, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "travel-plan-permission-1349", + "repo": "stranske/Travel-Plan-Permission", + "pr": 1349, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "trend_model_project-5757", + "repo": "stranske/Trend_Model_Project", + "pr": 5757, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "portable-alpha-extension-model-2163", + "repo": "stranske/Portable-Alpha-Extension-Model", + "pr": 2163, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "portable-alpha-extension-model-2162", + "repo": "stranske/Portable-Alpha-Extension-Model", + "pr": 2162, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "counter_risk-887", + "repo": "stranske/Counter_Risk", + "pr": 887, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "counter_risk-886", + "repo": "stranske/Counter_Risk", + "pr": 886, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "manager-database-1482", + "repo": "stranske/Manager-Database", + "pr": 1482, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "inv-man-intake-853", + "repo": "stranske/Inv-Man-Intake", + "pr": 853, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "inv-man-intake-844", + "repo": "stranske/Inv-Man-Intake", + "pr": 844, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "pension-data-780", + "repo": "stranske/Pension-Data", + "pr": 780, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "pension-data-779", + "repo": "stranske/Pension-Data", + "pr": 779, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "pension-data-777", + "repo": "stranske/Pension-Data", + "pr": 777, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "pension-data-776", + "repo": "stranske/Pension-Data", + "pr": 776, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "pension-data-775", + "repo": "stranske/Pension-Data", + "pr": 775, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "pension-data-774", + "repo": "stranske/Pension-Data", + "pr": 774, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "pension-data-773", + "repo": "stranske/Pension-Data", + "pr": 773, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "ready-474", + "repo": "stranske/Ready", + "pr": 474, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "ready-473", + "repo": "stranske/Ready", + "pr": 473, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "trip-planner-1582", + "repo": "stranske/trip-planner", + "pr": 1582, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "trip-planner-1581", + "repo": "stranske/trip-planner", + "pr": 1581, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "learning-management-system-476", + "repo": "stranske/learning-management-system", + "pr": 476, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "learning-management-system-475", + "repo": "stranske/learning-management-system", + "pr": 475, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "fine-art-archive-344", + "repo": "stranske/Fine-Art-Archive", + "pr": 344, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "fine-art-archive-343", + "repo": "stranske/Fine-Art-Archive", + "pr": 343, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "fine-art-archive-342", + "repo": "stranske/Fine-Art-Archive", + "pr": 342, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" + }, + { + "case_id": "fine-art-archive-341", + "repo": "stranske/Fine-Art-Archive", + "pr": 341, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-08-24" } ] } From 1cca24f798d4ffaaf7ca08480b265514cab607ea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Aug 2026 08:53:14 +0000 Subject: [PATCH 05/19] maint: prune closed-issue agent stubs [skip ci] --- agents/auto-pilot-755.md | 1 - agents/auto-pilot-763.md | 1 - agents/auto-pilot-768.md | 1 - agents/auto-pilot-771.md | 1 - agents/auto-pilot-785.md | 1 - agents/auto-pilot-821.md | 1 - agents/codex-10.md | 1 - agents/codex-1001.md | 1 - agents/codex-1003.md | 1 - agents/codex-1005.md | 1 - agents/codex-1006.md | 1 - agents/codex-101.md | 5 --- agents/codex-1011.md | 66 ---------------------------------------- agents/codex-1015.md | 1 - agents/codex-1021.md | 1 - agents/codex-1025.md | 1 - agents/codex-1026.md | 1 - agents/codex-1038.md | 1 - agents/codex-1039.md | 2 -- agents/codex-1063.md | 1 - agents/codex-11.md | 1 - agents/codex-12.md | 1 - agents/codex-123.md | 1 - agents/codex-1267.md | 1 - agents/codex-1296.md | 1 - agents/codex-13.md | 1 - agents/codex-1322.md | 2 -- agents/codex-1331.md | 1 - agents/codex-1342.md | 1 - agents/codex-1353.md | 1 - agents/codex-1355.md | 1 - agents/codex-1365.md | 1 - agents/codex-1366.md | 1 - agents/codex-1371.md | 1 - agents/codex-1385.md | 1 - agents/codex-1395.md | 1 - agents/codex-14.md | 1 - agents/codex-1404.md | 1 - agents/codex-1406.md | 1 - agents/codex-1407.md | 1 - agents/codex-1416.md | 1 - agents/codex-1425.md | 1 - agents/codex-144.md | 1 - agents/codex-15.md | 1 - agents/codex-153.md | 1 - agents/codex-157.md | 1 - agents/codex-16.md | 1 - agents/codex-160.md | 1 - agents/codex-164.md | 1 - agents/codex-167.md | 1 - agents/codex-17.md | 1 - agents/codex-170.md | 1 - agents/codex-2.md | 1 - agents/codex-213.md | 1 - agents/codex-23.md | 1 - agents/codex-271.md | 1 - agents/codex-287.md | 1 - agents/codex-288.md | 1 - agents/codex-297.md | 1 - agents/codex-299.md | 1 - agents/codex-302.md | 1 - agents/codex-356.md | 1 - agents/codex-359.md | 1 - agents/codex-362.md | 1 - agents/codex-365.md | 1 - agents/codex-368.md | 1 - agents/codex-371.md | 1 - agents/codex-377.md | 1 - agents/codex-380.md | 1 - agents/codex-387.md | 1 - agents/codex-392.md | 1 - agents/codex-397.md | 1 - agents/codex-399.md | 1 - agents/codex-4.md | 1 - agents/codex-405.md | 1 - agents/codex-439.md | 1 - agents/codex-453.md | 1 - agents/codex-454.md | 1 - agents/codex-455.md | 1 - agents/codex-456.md | 1 - agents/codex-457.md | 1 - agents/codex-477.md | 1 - agents/codex-478.md | 1 - agents/codex-480.md | 1 - agents/codex-481.md | 1 - agents/codex-482.md | 1 - agents/codex-483.md | 1 - agents/codex-518.md | 1 - agents/codex-540.md | 1 - agents/codex-545.md | 1 - agents/codex-567.md | 1 - agents/codex-581.md | 1 - agents/codex-582.md | 1 - agents/codex-690.md | 1 - agents/codex-692.md | 1 - agents/codex-693.md | 1 - agents/codex-719.md | 1 - agents/codex-723.md | 1 - agents/codex-725.md | 1 - agents/codex-729.md | 1 - agents/codex-73.md | 1 - agents/codex-74.md | 1 - agents/codex-75.md | 1 - agents/codex-76.md | 2 -- agents/codex-77.md | 1 - agents/codex-78.md | 1 - agents/codex-79.md | 1 - agents/codex-8.md | 2 -- agents/codex-875.md | 1 - agents/codex-893.md | 1 - agents/codex-895.md | 1 - agents/codex-9.md | 1 - agents/codex-92.md | 1 - agents/codex-922.md | 1 - agents/codex-93.md | 1 - agents/codex-935.md | 1 - agents/codex-936.md | 1 - agents/codex-942.md | 1 - agents/codex-970.md | 1 - agents/codex-982.md | 1 - agents/codex-983.md | 1 - agents/codex-988.md | 1 - agents/codex-989.md | 1 - agents/codex-992.md | 1 - agents/codex-994.md | 1 - agents/format-580.md | 1 - 126 files changed, 199 deletions(-) delete mode 100644 agents/auto-pilot-755.md delete mode 100644 agents/auto-pilot-763.md delete mode 100644 agents/auto-pilot-768.md delete mode 100644 agents/auto-pilot-771.md delete mode 100644 agents/auto-pilot-785.md delete mode 100644 agents/auto-pilot-821.md delete mode 100644 agents/codex-10.md delete mode 100644 agents/codex-1001.md delete mode 100644 agents/codex-1003.md delete mode 100644 agents/codex-1005.md delete mode 100644 agents/codex-1006.md delete mode 100644 agents/codex-101.md delete mode 100644 agents/codex-1011.md delete mode 100644 agents/codex-1015.md delete mode 100644 agents/codex-1021.md delete mode 100644 agents/codex-1025.md delete mode 100644 agents/codex-1026.md delete mode 100644 agents/codex-1038.md delete mode 100644 agents/codex-1039.md delete mode 100644 agents/codex-1063.md delete mode 100644 agents/codex-11.md delete mode 100644 agents/codex-12.md delete mode 100644 agents/codex-123.md delete mode 100644 agents/codex-1267.md delete mode 100644 agents/codex-1296.md delete mode 100644 agents/codex-13.md delete mode 100644 agents/codex-1322.md delete mode 100644 agents/codex-1331.md delete mode 100644 agents/codex-1342.md delete mode 100644 agents/codex-1353.md delete mode 100644 agents/codex-1355.md delete mode 100644 agents/codex-1365.md delete mode 100644 agents/codex-1366.md delete mode 100644 agents/codex-1371.md delete mode 100644 agents/codex-1385.md delete mode 100644 agents/codex-1395.md delete mode 100644 agents/codex-14.md delete mode 100644 agents/codex-1404.md delete mode 100644 agents/codex-1406.md delete mode 100644 agents/codex-1407.md delete mode 100644 agents/codex-1416.md delete mode 100644 agents/codex-1425.md delete mode 100644 agents/codex-144.md delete mode 100644 agents/codex-15.md delete mode 100644 agents/codex-153.md delete mode 100644 agents/codex-157.md delete mode 100644 agents/codex-16.md delete mode 100644 agents/codex-160.md delete mode 100644 agents/codex-164.md delete mode 100644 agents/codex-167.md delete mode 100644 agents/codex-17.md delete mode 100644 agents/codex-170.md delete mode 100644 agents/codex-2.md delete mode 100644 agents/codex-213.md delete mode 100644 agents/codex-23.md delete mode 100644 agents/codex-271.md delete mode 100644 agents/codex-287.md delete mode 100644 agents/codex-288.md delete mode 100644 agents/codex-297.md delete mode 100644 agents/codex-299.md delete mode 100644 agents/codex-302.md delete mode 100644 agents/codex-356.md delete mode 100644 agents/codex-359.md delete mode 100644 agents/codex-362.md delete mode 100644 agents/codex-365.md delete mode 100644 agents/codex-368.md delete mode 100644 agents/codex-371.md delete mode 100644 agents/codex-377.md delete mode 100644 agents/codex-380.md delete mode 100644 agents/codex-387.md delete mode 100644 agents/codex-392.md delete mode 100644 agents/codex-397.md delete mode 100644 agents/codex-399.md delete mode 100644 agents/codex-4.md delete mode 100644 agents/codex-405.md delete mode 100644 agents/codex-439.md delete mode 100644 agents/codex-453.md delete mode 100644 agents/codex-454.md delete mode 100644 agents/codex-455.md delete mode 100644 agents/codex-456.md delete mode 100644 agents/codex-457.md delete mode 100644 agents/codex-477.md delete mode 100644 agents/codex-478.md delete mode 100644 agents/codex-480.md delete mode 100644 agents/codex-481.md delete mode 100644 agents/codex-482.md delete mode 100644 agents/codex-483.md delete mode 100644 agents/codex-518.md delete mode 100644 agents/codex-540.md delete mode 100644 agents/codex-545.md delete mode 100644 agents/codex-567.md delete mode 100644 agents/codex-581.md delete mode 100644 agents/codex-582.md delete mode 100644 agents/codex-690.md delete mode 100644 agents/codex-692.md delete mode 100644 agents/codex-693.md delete mode 100644 agents/codex-719.md delete mode 100644 agents/codex-723.md delete mode 100644 agents/codex-725.md delete mode 100644 agents/codex-729.md delete mode 100644 agents/codex-73.md delete mode 100644 agents/codex-74.md delete mode 100644 agents/codex-75.md delete mode 100644 agents/codex-76.md delete mode 100644 agents/codex-77.md delete mode 100644 agents/codex-78.md delete mode 100644 agents/codex-79.md delete mode 100644 agents/codex-8.md delete mode 100644 agents/codex-875.md delete mode 100644 agents/codex-893.md delete mode 100644 agents/codex-895.md delete mode 100644 agents/codex-9.md delete mode 100644 agents/codex-92.md delete mode 100644 agents/codex-922.md delete mode 100644 agents/codex-93.md delete mode 100644 agents/codex-935.md delete mode 100644 agents/codex-936.md delete mode 100644 agents/codex-942.md delete mode 100644 agents/codex-970.md delete mode 100644 agents/codex-982.md delete mode 100644 agents/codex-983.md delete mode 100644 agents/codex-988.md delete mode 100644 agents/codex-989.md delete mode 100644 agents/codex-992.md delete mode 100644 agents/codex-994.md delete mode 100644 agents/format-580.md diff --git a/agents/auto-pilot-755.md b/agents/auto-pilot-755.md deleted file mode 100644 index 341779a2d..000000000 --- a/agents/auto-pilot-755.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/auto-pilot-763.md b/agents/auto-pilot-763.md deleted file mode 100644 index fdb46dfb5..000000000 --- a/agents/auto-pilot-763.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/auto-pilot-768.md b/agents/auto-pilot-768.md deleted file mode 100644 index d743ee523..000000000 --- a/agents/auto-pilot-768.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/auto-pilot-771.md b/agents/auto-pilot-771.md deleted file mode 100644 index 8e4adfa4f..000000000 --- a/agents/auto-pilot-771.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/auto-pilot-785.md b/agents/auto-pilot-785.md deleted file mode 100644 index 9b35c58f9..000000000 --- a/agents/auto-pilot-785.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/auto-pilot-821.md b/agents/auto-pilot-821.md deleted file mode 100644 index b6bd8780c..000000000 --- a/agents/auto-pilot-821.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-10.md b/agents/codex-10.md deleted file mode 100644 index 3bd38eda6..000000000 --- a/agents/codex-10.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1001.md b/agents/codex-1001.md deleted file mode 100644 index c2cfb5042..000000000 --- a/agents/codex-1001.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1003.md b/agents/codex-1003.md deleted file mode 100644 index f8423cc30..000000000 --- a/agents/codex-1003.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1005.md b/agents/codex-1005.md deleted file mode 100644 index eb5af58bc..000000000 --- a/agents/codex-1005.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1006.md b/agents/codex-1006.md deleted file mode 100644 index a0311d73a..000000000 --- a/agents/codex-1006.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-101.md b/agents/codex-101.md deleted file mode 100644 index c38e04466..000000000 --- a/agents/codex-101.md +++ /dev/null @@ -1,5 +0,0 @@ - -# Trigger keepalive test 2025-12-24T14:43:06+00:00 - -# Test Codex CLI with ChatGPT auth 2025-12-24T16:57:45+00:00 -# Test with Codex CLI install fix 2025-12-24T17:16:00+00:00 diff --git a/agents/codex-1011.md b/agents/codex-1011.md deleted file mode 100644 index 2e35c793b..000000000 --- a/agents/codex-1011.md +++ /dev/null @@ -1,66 +0,0 @@ -# Issue 1011: Add rebalancer coverage and sanitize blank ranks - -**Source**: https://github.com/stranske/Trend_Model_Project/issues/1011 - -## Why - -The Rebalancer helper in the multi-period workflow lacks sufficient unit test coverage for critical entry, exit, and weighting paths. Additionally, blank or whitespace-only fund names can flow through the ranking system unchanged, leading to empty string selections that cause unexpected downstream behavior. Empty multi-period demo exports also need placeholder rows to prevent parsing errors. - -## Scope - -- Add comprehensive unit tests for the multi-period Rebalancer helper covering entry triggers, exit triggers, and weighting strategies -- Normalize blank or whitespace-only column labels in rank_selection before processing to avoid empty fund name selections -- Ensure empty multi-period period exports include placeholder rows with appropriate messages -- Update lockfile dependencies (hypothesis 6.138.16→6.138.17, xlsxwriter 3.2.8→3.2.9) - -## Non-Goals - -- Changing the core Rebalancer algorithm or trigger logic -- Refactoring existing multi-period workflow structure -- Adding new weighting strategies beyond existing score_prop_bayes -- Modifying the export file format structure - -## Tasks - -- [x] Add unit tests for Rebalancer consecutive soft strikes (exit path) -- [x] Add unit tests for Rebalancer hard exit threshold override -- [x] Add unit tests for hard entry candidates filling capacity first -- [x] Add unit tests for eligible candidates accumulating strikes -- [x] Add unit tests for score-proportional weighting behavior -- [x] Add unit tests for score-proportional fallback to equal weights -- [x] Add unit tests for empty holdings edge case -- [x] Sanitize blank/whitespace column names in rank_select_funds before processing -- [x] Add _ensure_periods_placeholder helper for empty period exports -- [x] Apply placeholder logic to phase1_multi and multi_period exports in empty demo -- [x] Update requirements.lock with hypothesis 6.138.17 and xlsxwriter 3.2.9 -- [x] Verify all tests pass with ./scripts/run_tests.sh - -## Acceptance Criteria - -- [x] All unit tests in test_multi_period_rebalancer.py pass -- [x] Rebalancer correctly removes funds after consecutive soft strikes -- [x] Rebalancer immediately drops funds below hard exit threshold -- [x] Hard entry candidates consume capacity before auto entries -- [x] Eligible candidates join after accumulating required strikes -- [x] Score-proportional weighting produces normalized weights favoring higher scores -- [x] Score-proportional weighting falls back to equal weights when zscore column is missing -- [x] Empty holdings return empty Series without errors -- [x] rank_select_funds strips whitespace from column names and renames blank columns to Unnamed_N -- [x] Empty multi-period exports include placeholder rows with descriptive messages -- [x] All existing tests continue to pass after changes -- [x] Requirements lockfile reflects updated dependency versions - -## Implementation Notes - -Files modified: -- `tests/test_multi_period_rebalancer.py` - New comprehensive unit test suite (130 lines) -- `src/trend_analysis/core/rank_selection.py` - Column name sanitization logic in rank_select_funds -- `scripts/run_multi_demo.py` - Added _ensure_periods_placeholder helper and applied to empty exports -- `requirements.lock` - Updated hypothesis and xlsxwriter versions - -The column sanitization in rank_select_funds ensures uniqueness after stripping by appending numeric suffixes when needed. The placeholder helper populates CSV, JSON, and TXT period exports with {"period": "N/A", "note": ""} when empty. - -Testing command: `./scripts/run_tests.sh` - -**Status**: ✅ Completed -**ChatGPT Task**: https://chatgpt.com/codex/tasks/task_e_68c8c29578f08331969f58373bf6896a diff --git a/agents/codex-1015.md b/agents/codex-1015.md deleted file mode 100644 index 002befa18..000000000 --- a/agents/codex-1015.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1021.md b/agents/codex-1021.md deleted file mode 100644 index 7ca2bda9f..000000000 --- a/agents/codex-1021.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1025.md b/agents/codex-1025.md deleted file mode 100644 index e486510ee..000000000 --- a/agents/codex-1025.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1026.md b/agents/codex-1026.md deleted file mode 100644 index c6f862b6a..000000000 --- a/agents/codex-1026.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1038.md b/agents/codex-1038.md deleted file mode 100644 index 2499286cc..000000000 --- a/agents/codex-1038.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1039.md b/agents/codex-1039.md deleted file mode 100644 index aa3b59e0d..000000000 --- a/agents/codex-1039.md +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/agents/codex-1063.md b/agents/codex-1063.md deleted file mode 100644 index d4b432c59..000000000 --- a/agents/codex-1063.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-11.md b/agents/codex-11.md deleted file mode 100644 index ab374e167..000000000 --- a/agents/codex-11.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-12.md b/agents/codex-12.md deleted file mode 100644 index eeb3113dd..000000000 --- a/agents/codex-12.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-123.md b/agents/codex-123.md deleted file mode 100644 index 3c65ee8dd..000000000 --- a/agents/codex-123.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1267.md b/agents/codex-1267.md deleted file mode 100644 index b8364c037..000000000 --- a/agents/codex-1267.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1296.md b/agents/codex-1296.md deleted file mode 100644 index 224c2354d..000000000 --- a/agents/codex-1296.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-13.md b/agents/codex-13.md deleted file mode 100644 index d2004b66c..000000000 --- a/agents/codex-13.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1322.md b/agents/codex-1322.md deleted file mode 100644 index cc912d996..000000000 --- a/agents/codex-1322.md +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/agents/codex-1331.md b/agents/codex-1331.md deleted file mode 100644 index d2a3cfd19..000000000 --- a/agents/codex-1331.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1342.md b/agents/codex-1342.md deleted file mode 100644 index a3747dbcd..000000000 --- a/agents/codex-1342.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1353.md b/agents/codex-1353.md deleted file mode 100644 index 7bd63d480..000000000 --- a/agents/codex-1353.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1355.md b/agents/codex-1355.md deleted file mode 100644 index 21c90b90f..000000000 --- a/agents/codex-1355.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1365.md b/agents/codex-1365.md deleted file mode 100644 index 08b835d4d..000000000 --- a/agents/codex-1365.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1366.md b/agents/codex-1366.md deleted file mode 100644 index 10df3e2e2..000000000 --- a/agents/codex-1366.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1371.md b/agents/codex-1371.md deleted file mode 100644 index 74f692f6c..000000000 --- a/agents/codex-1371.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1385.md b/agents/codex-1385.md deleted file mode 100644 index f0e95aec3..000000000 --- a/agents/codex-1385.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1395.md b/agents/codex-1395.md deleted file mode 100644 index 89e7100ad..000000000 --- a/agents/codex-1395.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-14.md b/agents/codex-14.md deleted file mode 100644 index 7847c03d7..000000000 --- a/agents/codex-14.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1404.md b/agents/codex-1404.md deleted file mode 100644 index 6f66e1748..000000000 --- a/agents/codex-1404.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1406.md b/agents/codex-1406.md deleted file mode 100644 index b0b5fb112..000000000 --- a/agents/codex-1406.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1407.md b/agents/codex-1407.md deleted file mode 100644 index 218ff6637..000000000 --- a/agents/codex-1407.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1416.md b/agents/codex-1416.md deleted file mode 100644 index 3ceb8437a..000000000 --- a/agents/codex-1416.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-1425.md b/agents/codex-1425.md deleted file mode 100644 index fd0208bd5..000000000 --- a/agents/codex-1425.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-144.md b/agents/codex-144.md deleted file mode 100644 index 668c9cc10..000000000 --- a/agents/codex-144.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-15.md b/agents/codex-15.md deleted file mode 100644 index e9c95e321..000000000 --- a/agents/codex-15.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-153.md b/agents/codex-153.md deleted file mode 100644 index f226439a1..000000000 --- a/agents/codex-153.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-157.md b/agents/codex-157.md deleted file mode 100644 index 35f6cb5c6..000000000 --- a/agents/codex-157.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-16.md b/agents/codex-16.md deleted file mode 100644 index a608de089..000000000 --- a/agents/codex-16.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-160.md b/agents/codex-160.md deleted file mode 100644 index c76310f0e..000000000 --- a/agents/codex-160.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-164.md b/agents/codex-164.md deleted file mode 100644 index 408c743f2..000000000 --- a/agents/codex-164.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-167.md b/agents/codex-167.md deleted file mode 100644 index 0a14b7024..000000000 --- a/agents/codex-167.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-17.md b/agents/codex-17.md deleted file mode 100644 index 0ae41736c..000000000 --- a/agents/codex-17.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-170.md b/agents/codex-170.md deleted file mode 100644 index 73eccd8b3..000000000 --- a/agents/codex-170.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-2.md b/agents/codex-2.md deleted file mode 100644 index ce6f5204c..000000000 --- a/agents/codex-2.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-213.md b/agents/codex-213.md deleted file mode 100644 index aa9069319..000000000 --- a/agents/codex-213.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-23.md b/agents/codex-23.md deleted file mode 100644 index e81c2e0e3..000000000 --- a/agents/codex-23.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-271.md b/agents/codex-271.md deleted file mode 100644 index 1a8fd2c43..000000000 --- a/agents/codex-271.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-287.md b/agents/codex-287.md deleted file mode 100644 index 4dfdaca5c..000000000 --- a/agents/codex-287.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-288.md b/agents/codex-288.md deleted file mode 100644 index 6c3fb74a0..000000000 --- a/agents/codex-288.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-297.md b/agents/codex-297.md deleted file mode 100644 index 0a0a3251c..000000000 --- a/agents/codex-297.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-299.md b/agents/codex-299.md deleted file mode 100644 index b59b82b5f..000000000 --- a/agents/codex-299.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-302.md b/agents/codex-302.md deleted file mode 100644 index 83da77f4d..000000000 --- a/agents/codex-302.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-356.md b/agents/codex-356.md deleted file mode 100644 index a941b8976..000000000 --- a/agents/codex-356.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-359.md b/agents/codex-359.md deleted file mode 100644 index 3129eaab1..000000000 --- a/agents/codex-359.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-362.md b/agents/codex-362.md deleted file mode 100644 index 9bab13e41..000000000 --- a/agents/codex-362.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-365.md b/agents/codex-365.md deleted file mode 100644 index 2df543cc2..000000000 --- a/agents/codex-365.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-368.md b/agents/codex-368.md deleted file mode 100644 index 8e5a53196..000000000 --- a/agents/codex-368.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-371.md b/agents/codex-371.md deleted file mode 100644 index 33b4a5ed1..000000000 --- a/agents/codex-371.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-377.md b/agents/codex-377.md deleted file mode 100644 index 9d8a03585..000000000 --- a/agents/codex-377.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-380.md b/agents/codex-380.md deleted file mode 100644 index c0446edc9..000000000 --- a/agents/codex-380.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-387.md b/agents/codex-387.md deleted file mode 100644 index 083ff9f5c..000000000 --- a/agents/codex-387.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-392.md b/agents/codex-392.md deleted file mode 100644 index 700236e7f..000000000 --- a/agents/codex-392.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-397.md b/agents/codex-397.md deleted file mode 100644 index e111db33a..000000000 --- a/agents/codex-397.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-399.md b/agents/codex-399.md deleted file mode 100644 index e66cbd18d..000000000 --- a/agents/codex-399.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-4.md b/agents/codex-4.md deleted file mode 100644 index a1c08ef27..000000000 --- a/agents/codex-4.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-405.md b/agents/codex-405.md deleted file mode 100644 index 005f3eee3..000000000 --- a/agents/codex-405.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-439.md b/agents/codex-439.md deleted file mode 100644 index 0aa6b51a5..000000000 --- a/agents/codex-439.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-453.md b/agents/codex-453.md deleted file mode 100644 index 6f1270ce0..000000000 --- a/agents/codex-453.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-454.md b/agents/codex-454.md deleted file mode 100644 index 07c336f6a..000000000 --- a/agents/codex-454.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-455.md b/agents/codex-455.md deleted file mode 100644 index faf570b08..000000000 --- a/agents/codex-455.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-456.md b/agents/codex-456.md deleted file mode 100644 index a6496e555..000000000 --- a/agents/codex-456.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-457.md b/agents/codex-457.md deleted file mode 100644 index 3b8220174..000000000 --- a/agents/codex-457.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-477.md b/agents/codex-477.md deleted file mode 100644 index 2ca34ee29..000000000 --- a/agents/codex-477.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-478.md b/agents/codex-478.md deleted file mode 100644 index e5d13c4c1..000000000 --- a/agents/codex-478.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-480.md b/agents/codex-480.md deleted file mode 100644 index 5b477d03b..000000000 --- a/agents/codex-480.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-481.md b/agents/codex-481.md deleted file mode 100644 index db39c0761..000000000 --- a/agents/codex-481.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-482.md b/agents/codex-482.md deleted file mode 100644 index 7b028d735..000000000 --- a/agents/codex-482.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-483.md b/agents/codex-483.md deleted file mode 100644 index df8589d4a..000000000 --- a/agents/codex-483.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-518.md b/agents/codex-518.md deleted file mode 100644 index c1f757f5e..000000000 --- a/agents/codex-518.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-540.md b/agents/codex-540.md deleted file mode 100644 index f623f883e..000000000 --- a/agents/codex-540.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-545.md b/agents/codex-545.md deleted file mode 100644 index 223c675b4..000000000 --- a/agents/codex-545.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-567.md b/agents/codex-567.md deleted file mode 100644 index 55d642a4c..000000000 --- a/agents/codex-567.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-581.md b/agents/codex-581.md deleted file mode 100644 index 8f1a08cdb..000000000 --- a/agents/codex-581.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-582.md b/agents/codex-582.md deleted file mode 100644 index 7f9af6f7c..000000000 --- a/agents/codex-582.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-690.md b/agents/codex-690.md deleted file mode 100644 index db791e0dd..000000000 --- a/agents/codex-690.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-692.md b/agents/codex-692.md deleted file mode 100644 index 212362919..000000000 --- a/agents/codex-692.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-693.md b/agents/codex-693.md deleted file mode 100644 index fe9b356e2..000000000 --- a/agents/codex-693.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-719.md b/agents/codex-719.md deleted file mode 100644 index 16a3ac036..000000000 --- a/agents/codex-719.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-723.md b/agents/codex-723.md deleted file mode 100644 index 72061212c..000000000 --- a/agents/codex-723.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-725.md b/agents/codex-725.md deleted file mode 100644 index 0243369be..000000000 --- a/agents/codex-725.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-729.md b/agents/codex-729.md deleted file mode 100644 index 2924fffd5..000000000 --- a/agents/codex-729.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-73.md b/agents/codex-73.md deleted file mode 100644 index 0749583e0..000000000 --- a/agents/codex-73.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-74.md b/agents/codex-74.md deleted file mode 100644 index ad9046a89..000000000 --- a/agents/codex-74.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-75.md b/agents/codex-75.md deleted file mode 100644 index 1d683792d..000000000 --- a/agents/codex-75.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-76.md b/agents/codex-76.md deleted file mode 100644 index a7bb5c990..000000000 --- a/agents/codex-76.md +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/agents/codex-77.md b/agents/codex-77.md deleted file mode 100644 index 182a27acb..000000000 --- a/agents/codex-77.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-78.md b/agents/codex-78.md deleted file mode 100644 index d6d39899e..000000000 --- a/agents/codex-78.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-79.md b/agents/codex-79.md deleted file mode 100644 index 1b847734c..000000000 --- a/agents/codex-79.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-8.md b/agents/codex-8.md deleted file mode 100644 index f9215a45d..000000000 --- a/agents/codex-8.md +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/agents/codex-875.md b/agents/codex-875.md deleted file mode 100644 index 3d2ee565a..000000000 --- a/agents/codex-875.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-893.md b/agents/codex-893.md deleted file mode 100644 index 90bcb4356..000000000 --- a/agents/codex-893.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-895.md b/agents/codex-895.md deleted file mode 100644 index 29dfd25a5..000000000 --- a/agents/codex-895.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-9.md b/agents/codex-9.md deleted file mode 100644 index 3fafe21b7..000000000 --- a/agents/codex-9.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-92.md b/agents/codex-92.md deleted file mode 100644 index fc886374f..000000000 --- a/agents/codex-92.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-922.md b/agents/codex-922.md deleted file mode 100644 index 29a63355e..000000000 --- a/agents/codex-922.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-93.md b/agents/codex-93.md deleted file mode 100644 index 7b554e0ae..000000000 --- a/agents/codex-93.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-935.md b/agents/codex-935.md deleted file mode 100644 index ee1f0addb..000000000 --- a/agents/codex-935.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-936.md b/agents/codex-936.md deleted file mode 100644 index c055295bc..000000000 --- a/agents/codex-936.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-942.md b/agents/codex-942.md deleted file mode 100644 index 3b2902501..000000000 --- a/agents/codex-942.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-970.md b/agents/codex-970.md deleted file mode 100644 index 6dc8cc2bd..000000000 --- a/agents/codex-970.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-982.md b/agents/codex-982.md deleted file mode 100644 index 34add8512..000000000 --- a/agents/codex-982.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-983.md b/agents/codex-983.md deleted file mode 100644 index f6d1f93f0..000000000 --- a/agents/codex-983.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-988.md b/agents/codex-988.md deleted file mode 100644 index 8a8754136..000000000 --- a/agents/codex-988.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-989.md b/agents/codex-989.md deleted file mode 100644 index 81bd9345f..000000000 --- a/agents/codex-989.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-992.md b/agents/codex-992.md deleted file mode 100644 index 24ebcb818..000000000 --- a/agents/codex-992.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/codex-994.md b/agents/codex-994.md deleted file mode 100644 index e122e3758..000000000 --- a/agents/codex-994.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agents/format-580.md b/agents/format-580.md deleted file mode 100644 index 134dd6147..000000000 --- a/agents/format-580.md +++ /dev/null @@ -1 +0,0 @@ - From 833be49e3aafd58da8e7b69341739a0401e8176b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 11:30:25 +0000 Subject: [PATCH 06/19] Support non-default base branches for agent PRs (evaluation lanes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent bootstrap PRs always targeted the repository default branch: the issue bridge hardcoded `base = data.default_branch` with no override. That makes per-agent evaluation lanes (eval/claude, eval/codex) impossible — every agent PR lands on main. - reusable-agents-issue-bridge: resolve the PR base as base_branch input > `` issue marker > repo default, with an existence guard that falls back to the default when a marker names a missing branch (a typo can no longer wedge dispatch). The input is passed via env, not inline interpolation. - agents-verify-to-new-pr: carry a non-default PR base into the generated follow-up issue as a base-branch marker (both the LLM and fallback paths), so remediation PRs stay in the lane instead of escaping to main. Resolution order verified against all four cases; workflow YAML validator passes. Two pre-existing long-line lint failures in the bridge are upstream and untouched. --- .github/workflows/agents-verify-to-new-pr.yml | 26 +++++++ .../reusable-agents-issue-bridge.yml | 67 ++++++++++++++++++- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/.github/workflows/agents-verify-to-new-pr.yml b/.github/workflows/agents-verify-to-new-pr.yml index d0682a3f4..7f5cbd0e4 100644 --- a/.github/workflows/agents-verify-to-new-pr.yml +++ b/.github/workflows/agents-verify-to-new-pr.yml @@ -211,6 +211,23 @@ jobs: fs.writeFileSync('original_issue.txt', originalIssueBody); + // --- Base-branch propagation --- + // Carry the merged PR's base forward when it is NOT the default + // branch (evaluation lanes such as eval/claude). Without this the + // follow-up PR targets the default branch and the fix escapes the + // lane it belongs to. The bridge reads this marker back. + const prBaseRef = context.payload.pull_request.base?.ref || ''; + const repoDefaultBranch = + context.payload.repository?.default_branch || ''; + const nonDefaultBase = + prBaseRef && prBaseRef !== repoDefaultBranch ? prBaseRef : ''; + if (nonDefaultBase) { + core.info( + `PR base '${nonDefaultBase}' is non-default; follow-up will target it.`, + ); + } + core.setOutput('pr_base_ref', nonDefaultBase); + // --- Chain depth tracking (P0) --- // Extract follow-up-depth from original issue body or PR body // Format: @@ -572,6 +589,7 @@ jobs: ORIGINAL_ISSUE_TITLE: ${{ steps.collect.outputs.original_issue_title }} PR_NUMBER: ${{ steps.check-merged.outputs.pr_number }} FOLLOW_UP_DEPTH: ${{ steps.chain-check.outputs.next_depth }} + PR_BASE_REF: ${{ steps.collect.outputs.pr_base_ref }} run: | # Generate using Python script python scripts/langchain/followup_issue_generator.py \ @@ -586,6 +604,11 @@ jobs: # Inject chain depth marker into generated body depth="${FOLLOW_UP_DEPTH:-1}" marker="" + # Preserve a non-default base (evaluation lanes) so the follow-up PR + # targets the same branch the originating PR merged into. + if [ -n "${PR_BASE_REF:-}" ]; then + marker="$(printf '%s\n%s' "$marker" "")" + fi jq --arg marker "$marker" \ '.body = $marker + "\n" + .body' \ followup_issue.json > followup_issue_tmp.json @@ -613,11 +636,13 @@ jobs: env: FOLLOW_UP_DEPTH: ${{ steps.chain-check.outputs.next_depth }} EXTRACTED_VERDICT: ${{ steps.extract-verdict.outputs.verdict }} + PR_BASE_REF: ${{ steps.collect.outputs.pr_base_ref }} with: github-token: ${{ steps.select-token.outputs.token }} script: | // Fallback to structured extraction if Python script fails const fs = require('fs'); + const prBaseRef = process.env.PR_BASE_REF || ''; const prNumber = context.payload.pull_request.number; const prUrl = context.payload.pull_request.html_url; const depth = process.env.FOLLOW_UP_DEPTH || '1'; @@ -738,6 +763,7 @@ jobs: const issueBody = [ ``, + ...(prBaseRef ? [``] : []), '## Why', '', `PR #${prNumber} was verified with verdict **${verdict}**. ` + diff --git a/.github/workflows/reusable-agents-issue-bridge.yml b/.github/workflows/reusable-agents-issue-bridge.yml index 6ac523aea..0e0602a3d 100644 --- a/.github/workflows/reusable-agents-issue-bridge.yml +++ b/.github/workflows/reusable-agents-issue-bridge.yml @@ -50,6 +50,14 @@ on: required: false type: string default: "false" + base_branch: + description: >- + Optional base branch for the automation PR. Overrides the + `` issue marker and the repository default. + Used for evaluation lanes that must not target the default branch. + required: false + type: string + default: "" secrets: service_bot_pat: description: "PAT for service bot operations" @@ -279,6 +287,10 @@ jobs: - name: Resolve base and head refs id: refs uses: actions/github-script@v9 + env: + # Passed via env (not inline interpolation) so a caller-supplied + # value can never be injected into the script body. + INPUT_BASE_BRANCH: ${{ inputs.base_branch }} with: script: | const { owner, repo } = context.repo; @@ -300,13 +312,64 @@ jobs: repo, }), ); - const base = data.default_branch; - if (!base) { + const defaultBranch = data.default_branch; + if (!defaultBranch) { core.setFailed('Repository default branch not available'); return; } const issue = Number('${{ steps.ctx.outputs.issue }}'); + // Base-branch resolution, highest priority first: + // 1. explicit `base_branch` input (caller override) + // 2. `` marker in the issue body + // 3. repository default branch + // Evaluation lanes rely on (1)/(2) so agent PRs land on + // eval/ instead of the default branch. + let base = String(process.env.INPUT_BASE_BRANCH || '').trim(); + let baseSource = base ? 'input' : ''; + + if (!base && issue) { + try { + const { data: issueData } = await withRetry(() => + api.rest.issues.get({ owner, repo, issue_number: issue }), + ); + const marker = String(issueData.body || '').match( + //, + ); + if (marker) { + base = marker[1].trim(); + baseSource = 'issue-marker'; + } + } catch (error) { + core.warning( + `Could not read issue #${issue} for a base-branch marker: ${error.message}`, + ); + } + } + + if (!base) { + base = defaultBranch; + baseSource = 'default'; + } + + // Never target a branch that does not exist: a stale or mistyped + // marker would otherwise wedge every dispatch for that issue. + if (base !== defaultBranch) { + try { + await withRetry(() => + api.rest.repos.getBranch({ owner, repo, branch: base }), + ); + } catch (error) { + core.warning( + `Base branch '${base}' (from ${baseSource}) not found; ` + + `falling back to '${defaultBranch}'.`, + ); + base = defaultBranch; + baseSource = 'default-fallback'; + } + } + core.info(`Resolved PR base: ${base} (source: ${baseSource})`); + let branchPrefix = 'codex/issue-'; if (agentKey) { try { From 6d15c0a2f34ab4ff5c4806ce8eb2f783bfce397f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 12:52:20 +0000 Subject: [PATCH 07/19] Wire Cursor into the consolidated keepalive/autofix loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cursor was integrated against the previous loop architecture: the runner (reusable-cursor-run.yml) and registry entry exist, and agents-autofix-loop had an autofix-cursor job. But both agents-keepalive-loop.yml and agents-autofix-loop.yml were retired in favour of agents-81-gate-followups, and the consolidation did not carry Cursor across — agents-81 dispatched only run-codex and run-claude. Cursor therefore had a runner that nothing called. - agents-81 template: add run-cursor (keepalive) and autofix-cursor, mirroring the Claude jobs and calling reusable-cursor-run.yml. - Add both to the needs lists of record-keepalive-completion, summary and metrics so completion accounting and reporting wait on them. - registry: capabilities.belt true (the loop now dispatches cursor) and raise the capacity placeholder off 1/day, flagged for the owner's real plan quota. Routing verified (agent:cursor -> reusable-cursor-run.yml, cursor/issue-), dependency graph checked, registry tests pass, templates in sync. The one pre-existing long-line lint failure in agents-81 is upstream and untouched. --- .github/agents/registry.yml | 4 +- .../consumer-repo/.github/agents/registry.yml | 4 +- .../workflows/agents-81-gate-followups.yml | 48 +++++++++++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/.github/agents/registry.yml b/.github/agents/registry.yml index a44f3f76d..6e5f1b2a8 100644 --- a/.github/agents/registry.yml +++ b/.github/agents/registry.yml @@ -137,7 +137,7 @@ agents: branch_prefix: cursor/issue- capacity: window: daily - limit: 1 # TODO: confirm owner-supplied public plan limit + limit: 10 # SET THIS to your actual Cursor plan quota ui_mentions_allowed: false # Reuses stranske-automation-bot for branch pushes/attribution until a # dedicated stranske-cursor-bot service account is provisioned. @@ -152,7 +152,7 @@ agents: capabilities: pr_keepalive: true pr_autofix: true # wired into agents-autofix-loop.yml (autofix-cursor job) - belt: false # belt routing deferred to a later phase + belt: true # dispatched by agents-81 run-cursor verifier_checkbox: false # verification stays on the existing judge (config/llm_slots.json) gemini: diff --git a/templates/consumer-repo/.github/agents/registry.yml b/templates/consumer-repo/.github/agents/registry.yml index a44f3f76d..6e5f1b2a8 100644 --- a/templates/consumer-repo/.github/agents/registry.yml +++ b/templates/consumer-repo/.github/agents/registry.yml @@ -137,7 +137,7 @@ agents: branch_prefix: cursor/issue- capacity: window: daily - limit: 1 # TODO: confirm owner-supplied public plan limit + limit: 10 # SET THIS to your actual Cursor plan quota ui_mentions_allowed: false # Reuses stranske-automation-bot for branch pushes/attribution until a # dedicated stranske-cursor-bot service account is provisioned. @@ -152,7 +152,7 @@ agents: capabilities: pr_keepalive: true pr_autofix: true # wired into agents-autofix-loop.yml (autofix-cursor job) - belt: false # belt routing deferred to a later phase + belt: true # dispatched by agents-81 run-cursor verifier_checkbox: false # verification stays on the existing judge (config/llm_slots.json) gemini: diff --git a/templates/consumer-repo/.github/workflows/agents-81-gate-followups.yml b/templates/consumer-repo/.github/workflows/agents-81-gate-followups.yml index e49491399..f0defcf3f 100644 --- a/templates/consumer-repo/.github/workflows/agents-81-gate-followups.yml +++ b/templates/consumer-repo/.github/workflows/agents-81-gate-followups.yml @@ -715,12 +715,39 @@ jobs: appendix: ${{ needs.evaluate.outputs.task_appendix }} iteration: ${{ needs.evaluate.outputs.iteration }} + run-cursor: + name: Keepalive next task (Cursor) + needs: + - evaluate + - preflight + - mark-running + if: | + needs.evaluate.outputs.agent_type == 'cursor' && + needs.evaluate.outputs.dispatch_should_run == 'true' && + (needs.evaluate.outputs.action == 'run' || + needs.evaluate.outputs.action == 'fix' || + needs.evaluate.outputs.action == 'conflict') + uses: iamkayleb/Workflows/.github/workflows/reusable-cursor-run.yml@main + secrets: inherit + with: + skip: >- + ${{ needs.evaluate.outputs.action != 'run' && + needs.evaluate.outputs.action != 'fix' && + needs.evaluate.outputs.action != 'conflict' }} + prompt_file: ${{ needs.evaluate.outputs.prompt_file }} + mode: keepalive + pr_number: ${{ needs.evaluate.outputs.pr_number }} + pr_ref: ${{ needs.evaluate.outputs.pr_ref }} + appendix: ${{ needs.evaluate.outputs.task_appendix }} + iteration: ${{ needs.evaluate.outputs.iteration }} + record-keepalive-completion: name: Record keepalive dispatch completion needs: - evaluate - run-codex - run-claude + - run-cursor if: >- always() && needs.evaluate.outputs.dispatch_should_run == 'true' && @@ -780,6 +807,7 @@ jobs: - preflight - run-codex - run-claude + - run-cursor # Run always if PR exists, handle skipped agent jobs gracefully if: | always() && @@ -1653,6 +1681,25 @@ jobs: WORKFLOWS_APP_ID: ${{ secrets.WORKFLOWS_APP_ID }} WORKFLOWS_APP_PRIVATE_KEY: ${{ secrets.WORKFLOWS_APP_PRIVATE_KEY }} + autofix-cursor: + needs: prepare + if: >- + needs.prepare.outputs.should_run == 'true' && + needs.prepare.outputs.dispatch_should_run == 'true' && + needs.prepare.outputs.agent_type == 'cursor' + name: Run Cursor autofix + uses: iamkayleb/Workflows/.github/workflows/reusable-cursor-run.yml@main + with: + prompt_file: .github/codex/prompts/autofix_from_ci_failure.md + mode: autofix + pr_number: ${{ needs.prepare.outputs.pr_number }} + pr_ref: ${{ needs.prepare.outputs.head_ref }} + appendix: ${{ needs.prepare.outputs.appendix }} + secrets: + CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} + WORKFLOWS_APP_ID: ${{ secrets.WORKFLOWS_APP_ID }} + WORKFLOWS_APP_PRIVATE_KEY: ${{ secrets.WORKFLOWS_APP_PRIVATE_KEY }} + needs-human: needs: prepare if: needs.prepare.outputs.stop_reason == 'max_attempts' @@ -1727,6 +1774,7 @@ jobs: - prepare - autofix - autofix-claude + - autofix-cursor if: >- ${{ always() && From d1ca332e64c316fc2b6d05054841373b50f30e14 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 13:20:00 +0000 Subject: [PATCH 08/19] Close two agent allowlists that silently excluded Cursor Both were hardcoded two-agent enumerations that the registry never fed, so adding Cursor to the registry was not enough to make its lane behave like the other two. - agents-capability-check gated on ["agent:codex","agent:claude","agent:auto"], so Cursor issues skipped the pre-flight capability gate entirely. Claude and Codex issues were screened for impossible tasks; Cursor's were not, which is an unequal starting condition for a three-way comparison. - agents-auto-label's skip-list omitted agent:cursor, so an issue already routed to Cursor was not recognised as already-routed and could be relabelled to a different agent. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FC8XoyssN5v6hQCTtcjoB5 --- templates/consumer-repo/.github/workflows/agents-auto-label.yml | 1 + .../consumer-repo/.github/workflows/agents-capability-check.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/consumer-repo/.github/workflows/agents-auto-label.yml b/templates/consumer-repo/.github/workflows/agents-auto-label.yml index b671c8c4e..337b675be 100644 --- a/templates/consumer-repo/.github/workflows/agents-auto-label.yml +++ b/templates/consumer-repo/.github/workflows/agents-auto-label.yml @@ -35,6 +35,7 @@ jobs: !contains(github.event.issue.labels.*.name, 'agents:autofix') && !contains(github.event.issue.labels.*.name, 'agent:codex') && !contains(github.event.issue.labels.*.name, 'agent:claude') && + !contains(github.event.issue.labels.*.name, 'agent:cursor') && !contains(github.event.issue.labels.*.name, 'agent:auto') && !contains(join(github.event.issue.labels.*.name, ','), 'campaign:') && !contains(github.event.issue.labels.*.name, 'automated') diff --git a/templates/consumer-repo/.github/workflows/agents-capability-check.yml b/templates/consumer-repo/.github/workflows/agents-capability-check.yml index 2e5a3fd59..93b85c389 100644 --- a/templates/consumer-repo/.github/workflows/agents-capability-check.yml +++ b/templates/consumer-repo/.github/workflows/agents-capability-check.yml @@ -21,7 +21,7 @@ jobs: capability-check: runs-on: ubuntu-latest # Trigger when an agent assignment label is added (pre-agent gate) - if: contains(fromJSON('["agent:codex","agent:claude","agent:auto"]'), github.event.label.name) + if: contains(fromJSON('["agent:codex","agent:claude","agent:cursor","agent:auto"]'), github.event.label.name) steps: - name: Checkout repository From 58e28fd6d9cb08e5929542e7bb17d886ffa3fe07 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 12:25:36 +0000 Subject: [PATCH 09/19] Limit the canary list to repos this fork can write to The canary registry still carried stranske/Travel-Plan-Permission and stranske/Portable-Alpha-Extension-Model from upstream. This fork has no write access to either, so every consumer sync run fails on both and the failure issue it opens is noise that hides real sync problems. Leaves iamkayleb/bukay as the sole canary. Restore the upstream entries only if this fork is ever given write access to them. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FC8XoyssN5v6hQCTtcjoB5 --- config/consumer_sync_canaries.json | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/config/consumer_sync_canaries.json b/config/consumer_sync_canaries.json index 202c8f768..798ab48e5 100644 --- a/config/consumer_sync_canaries.json +++ b/config/consumer_sync_canaries.json @@ -1,18 +1,14 @@ { "schema": "workflows.consumer-sync-canaries/v1", - "version": 1, + "version": 2, "canaries": [ { "repo": "iamkayleb/bukay", - "capabilities": ["node-tooling", "python-consumer", "lock-heavy"] - }, - { - "repo": "stranske/Travel-Plan-Permission", - "capabilities": ["standard", "custom-gate"] - }, - { - "repo": "stranske/Portable-Alpha-Extension-Model", - "capabilities": ["python-consumer", "codex-review", "legacy-precommit"] + "capabilities": [ + "node-tooling", + "python-consumer", + "lock-heavy" + ] } ] } From e3550ec8ae53a93d73a208afffbaa4548ab0ef42 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 01:33:11 +0000 Subject: [PATCH 10/19] Optimizer recursion guard: stop counting no-op runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guard counted every workflow run whose run-name ended with the issue number, with no filter on whether the run did anything. This workflow triggers on every `issues: labeled` event, but only agents:format, agents:optimize, agents:apply-suggestions and workflow_dispatch get past the trigger check — every other label spawns a run that exits immediately and still consumed one of the four slots. Applying three labels to a new issue therefore burned the whole budget before any optimizer work started, and the guard tripped on legitimate bulk seeding: "Too many optimizer runs (5 > 3) in last hour" on issues that had run the optimizer at most twice. run-name now marks a run [work] or [noop] from the triggering label, which is known at trigger time, and the guard excludes [noop]. Genuine recursion is still caught: four real runs in the window trips as before. Verified against the observed failure — the same history counts 5 (trips) under the old expression and 2 (passes) under the new one, while four work runs still trip. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FC8XoyssN5v6hQCTtcjoB5 --- .github/workflows/agents-issue-optimizer.yml | 19 +++++++++++++++++-- docs/WORKFLOW_GUIDE.md | 2 +- .../workflows/agents-issue-optimizer.yml | 19 +++++++++++++++++-- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/.github/workflows/agents-issue-optimizer.yml b/.github/workflows/agents-issue-optimizer.yml index 0883f3fec..bf46c7016 100644 --- a/.github/workflows/agents-issue-optimizer.yml +++ b/.github/workflows/agents-issue-optimizer.yml @@ -5,7 +5,13 @@ name: Agents Issue Optimizer # nothing and reported 0 for an issue it was re-running every minute. Pin the issue # number into the run name so both trigger types are correlatable. run-name: >- - Agents Issue Optimizer #${{ github.event.issue.number || github.event.inputs.issue_number }} + Agents Issue Optimizer + ${{ (github.event_name == 'workflow_dispatch' || + github.event.label.name == 'agents:format' || + github.event.label.name == 'agents:optimize' || + github.event.label.name == 'agents:apply-suggestions') + && '[work]' || '[noop]' }} + #${{ github.event.issue.number || github.event.inputs.issue_number }} on: issues: @@ -210,6 +216,14 @@ jobs: # `gh run list` defaults to 20 runs, which a tight loop exhausts inside the # window; ask for enough history to actually see the recursion. + # + # Count only runs that could do work. This workflow triggers on EVERY + # `labeled` event, but only agents:format / agents:optimize / + # agents:apply-suggestions (and workflow_dispatch) reach the optimizer; + # every other label spawns a run that exits at the trigger check. Those + # no-ops used to count here, so applying three labels to a new issue + # burned the whole budget before any real work started and the guard + # tripped on legitimate bulk seeding. run-name marks them [noop]. # shellcheck disable=SC2016 count=$(gh run list \ --workflow=agents-issue-optimizer.yml \ @@ -218,7 +232,8 @@ jobs: | jq --arg cutoff "$one_hour_ago" \ --arg issue "#$ISSUE_NUMBER" \ '[.[] | select(.createdAt > $cutoff - and (.displayTitle | endswith($issue))) + and (.displayTitle | endswith($issue)) + and (.displayTitle | contains("[noop]") | not)) ] | length') echo "Optimizer runs for issue #$ISSUE_NUMBER in last hour: $count" diff --git a/docs/WORKFLOW_GUIDE.md b/docs/WORKFLOW_GUIDE.md index 77af69e11..4c41b38d7 100644 --- a/docs/WORKFLOW_GUIDE.md +++ b/docs/WORKFLOW_GUIDE.md @@ -113,7 +113,7 @@ _Inline Gate helper_ - **`agents-bot-comment-handler.yml`** — Runs the existing inline-comment application logic. In addition to the prior label/Gate/manual triggers it now auto-runs when trusted bot review comments land, so inline suggestions on non-agent PRs are applied without manual relabeling. Override the trusted logins via `BOT_COMMENT_LOGINS` when repositories add more bots. - **`agents-guard.yml`** (aka Health 45 Agents Guard) — PR workflow that validates agent-related labels and permissions for both `pull_request` and `pull_request_target`. It now relies entirely on the shared API client/token balancer, so there’s no bespoke App-token mint ahead of safety checks. - **`pr-46-dependency-repair-contract.yml`** — Thin PR check that calls `reusable-19-dependency-repair-contract.yml` for dependency-bot and marked promotion PRs. It rejects unclassified agent commits on bot branches while permitting later repair commits on an agent-owned promotion branch after verifying that its first commit reproduces the selected bot delta. See [`docs/ops/DEPENDENCY_REPAIR_PROMOTION.md`](ops/DEPENDENCY_REPAIR_PROMOTION.md). -- **`agents-issue-optimizer.yml`** — Powers the analyzer/apply/format stages for legacy agent issues. It fires on the `agents:*` optimizer labels or via manual dispatch, re-reads live issue eligibility before format work, skips held, exempt, closed, bot-authored, and `agents:auto-pilot` work, releases the `agents:format` lease when that recheck makes formatting ineligible, validates formatted path evidence against the current checkout, enforces recursion guards via the GitHub CLI, and runs the LangChain optimizer before posting suggestions or applying changes. It now uses only the shared API client (no manual App-token mint) for GH CLI/auth flows. +- **`agents-issue-optimizer.yml`** — Powers the analyzer/apply/format stages for legacy agent issues. It fires on the `agents:*` optimizer labels or via manual dispatch, re-reads live issue eligibility before format work, skips held, exempt, closed, bot-authored, and `agents:auto-pilot` work, releases the `agents:format` lease when that recheck makes formatting ineligible, validates formatted path evidence against the current checkout, enforces a per-issue recursion guard via the GitHub CLI (counting only runs that could reach the optimizer — `run-name` marks non-triggering label events `[noop]` so bulk labelling cannot exhaust the budget), and runs the LangChain optimizer before posting suggestions or applying changes. It now uses only the shared API client (no manual App-token mint) for GH CLI/auth flows. - **`agents-80-pr-event-hub.yml`** — Consumer-template consolidated PR event hub that fans out keepalive metadata, bot-comment handling, and verification follow-ups after a single PR context fetch. - **`agents-81-gate-followups.yml`** — Consumer-template consolidated Gate follow-up hub that coordinates keepalive, autofix, and post-CI recovery. - **`agents-pr-meta-v4.yml`** — Workflows-repo PR metadata/keepalive front door: listens to issue comments, structural PR updates (open, synchronize, and reopen), and Gate completions to detect `@agent` activations, enforce gate/run-cap rules, dispatch the orchestrator, and write dispatch summaries. It deliberately ignores PR-body edit events because it writes that body itself; observing its own edits can create an unbounded metadata-check loop. It leaves release-please PR bodies untouched so release-please can parse merged release PRs and publish tags/releases. This remains a Workflows-local service workflow; the current consumer default is the `agents-80-pr-event-hub.yml` / `agents-81-gate-followups.yml` pair distributed from `templates/consumer-repo/`. diff --git a/templates/consumer-repo/.github/workflows/agents-issue-optimizer.yml b/templates/consumer-repo/.github/workflows/agents-issue-optimizer.yml index cf3255ad4..76c2c9d00 100644 --- a/templates/consumer-repo/.github/workflows/agents-issue-optimizer.yml +++ b/templates/consumer-repo/.github/workflows/agents-issue-optimizer.yml @@ -5,7 +5,13 @@ name: Agents Issue Optimizer # nothing and reported 0 for an issue it was re-running every minute. Pin the issue # number into the run name so both trigger types are correlatable. run-name: >- - Agents Issue Optimizer #${{ github.event.issue.number || github.event.inputs.issue_number }} + Agents Issue Optimizer + ${{ (github.event_name == 'workflow_dispatch' || + github.event.label.name == 'agents:format' || + github.event.label.name == 'agents:optimize' || + github.event.label.name == 'agents:apply-suggestions') + && '[work]' || '[noop]' }} + #${{ github.event.issue.number || github.event.inputs.issue_number }} on: issues: @@ -241,6 +247,14 @@ jobs: # `gh run list` defaults to 20 runs, which a tight loop exhausts inside the # window; ask for enough history to actually see the recursion. + # + # Count only runs that could do work. This workflow triggers on EVERY + # `labeled` event, but only agents:format / agents:optimize / + # agents:apply-suggestions (and workflow_dispatch) reach the optimizer; + # every other label spawns a run that exits at the trigger check. Those + # no-ops used to count here, so applying three labels to a new issue + # burned the whole budget before any real work started and the guard + # tripped on legitimate bulk seeding. run-name marks them [noop]. # shellcheck disable=SC2016 count=$(gh run list \ --workflow=agents-issue-optimizer.yml \ @@ -249,7 +263,8 @@ jobs: | jq --arg cutoff "$one_hour_ago" \ --arg issue "#$ISSUE_NUMBER" \ '[.[] | select(.createdAt > $cutoff - and (.displayTitle | endswith($issue))) + and (.displayTitle | endswith($issue)) + and (.displayTitle | contains("[noop]") | not)) ] | length') echo "Optimizer runs for issue #$ISSUE_NUMBER in last hour: $count" From 8bf0b11571c3d78371f8d4d9b6a519da81a81160 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 01:48:00 +0000 Subject: [PATCH 11/19] Restore the full canary registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trimming the registry to iamkayleb/bukay alone broke every sync run: select_consumer_sync_phase rejects a config outside 2..3 canaries with canary_config_requires_two_or_three_repos, so Maint 68 failed in its first job before reaching any repository. The trim was aimed at two consistently red matrix legs for repositories this fork cannot write to. Those legs were noise, not the blocker — the run that prompted the change failed on all three legs, including bukay, for an unrelated missing read:org PAT scope. Removing them bought nothing and cost the whole sync. Relaxing the lower bound was considered and rejected: the checked-in config is also asserted to cover six distinct consumer capability shapes, which a single-consumer fork cannot satisfy without inventing capabilities. The two unowned legs stay red until this fork either gains access to them or registers a second consumer it owns. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FC8XoyssN5v6hQCTtcjoB5 --- config/consumer_sync_canaries.json | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/config/consumer_sync_canaries.json b/config/consumer_sync_canaries.json index 798ab48e5..621edb582 100644 --- a/config/consumer_sync_canaries.json +++ b/config/consumer_sync_canaries.json @@ -1,6 +1,6 @@ { "schema": "workflows.consumer-sync-canaries/v1", - "version": 2, + "version": 3, "canaries": [ { "repo": "iamkayleb/bukay", @@ -9,6 +9,21 @@ "python-consumer", "lock-heavy" ] + }, + { + "repo": "stranske/Travel-Plan-Permission", + "capabilities": [ + "standard", + "custom-gate" + ] + }, + { + "repo": "stranske/Portable-Alpha-Extension-Model", + "capabilities": [ + "python-consumer", + "codex-review", + "legacy-precommit" + ] } ] } From 7a75f6b6979cf8ce9094a7bb07e5aca01319d912 Mon Sep 17 00:00:00 2001 From: Caleb Ogbike Date: Mon, 7 Sep 2026 11:42:19 +0100 Subject: [PATCH 12/19] corpus: grow verifier eval corpus from realized PR outcomes (#89) --- config/model_eval_corpus_staging.json | 108 ++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/config/model_eval_corpus_staging.json b/config/model_eval_corpus_staging.json index d25f44841..2619ca1a8 100644 --- a/config/model_eval_corpus_staging.json +++ b/config/model_eval_corpus_staging.json @@ -568,6 +568,114 @@ "category": "clean-pass", "provenance": "harvested", "harvested_at": "2026-08-24" + }, + { + "case_id": "workflows-2993", + "repo": "stranske/Workflows", + "pr": 2993, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-09-07" + }, + { + "case_id": "workflows-2988", + "repo": "stranske/Workflows", + "pr": 2988, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-09-07" + }, + { + "case_id": "workflows-2987", + "repo": "stranske/Workflows", + "pr": 2987, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-09-07" + }, + { + "case_id": "workflows-2986", + "repo": "stranske/Workflows", + "pr": 2986, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-09-07" + }, + { + "case_id": "workflows-2985", + "repo": "stranske/Workflows", + "pr": 2985, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-09-07" + }, + { + "case_id": "workflows-2984", + "repo": "stranske/Workflows", + "pr": 2984, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-09-07" + }, + { + "case_id": "workflows-2981", + "repo": "stranske/Workflows", + "pr": 2981, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-09-07" + }, + { + "case_id": "trend_model_project-5799", + "repo": "stranske/Trend_Model_Project", + "pr": 5799, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-09-07" + }, + { + "case_id": "fine-art-archive-474", + "repo": "stranske/Fine-Art-Archive", + "pr": 474, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-09-07" + }, + { + "case_id": "fine-art-archive-473", + "repo": "stranske/Fine-Art-Archive", + "pr": 473, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-09-07" + }, + { + "case_id": "fine-art-archive-472", + "repo": "stranske/Fine-Art-Archive", + "pr": 472, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-09-07" + }, + { + "case_id": "fine-art-archive-470", + "repo": "stranske/Fine-Art-Archive", + "pr": 470, + "expected_verdict": "PASS", + "category": "clean-pass", + "provenance": "harvested", + "harvested_at": "2026-09-07" } ] } From 5d4930a38e97361a45bd6e099f648922363a2bd4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 11:56:06 +0000 Subject: [PATCH 13/19] Put the issue number in the auto-pilot run title MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agents-auto-pilot set no run-name, so every run appeared in `gh run list` as the bare workflow name. With no issue number in displayTitle there is no way to ask "what has auto-pilot done for issue #241" — a hundred runs are indistinguishable, and per-issue history cannot be reconstructed even though auto-pilot is the workflow that owns the whole issue pipeline. agents-issue-optimizer already does this, and its recursion guard depends on it. Adopt the same shape here, covering the issue, pull_request and workflow_dispatch triggers. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FC8XoyssN5v6hQCTtcjoB5 --- .github/workflows/agents-auto-pilot.yml | 8 ++++++++ .../consumer-repo/.github/workflows/agents-auto-pilot.yml | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/.github/workflows/agents-auto-pilot.yml b/.github/workflows/agents-auto-pilot.yml index cc24e38ad..eddc39b63 100644 --- a/.github/workflows/agents-auto-pilot.yml +++ b/.github/workflows/agents-auto-pilot.yml @@ -1,6 +1,14 @@ # See docs/ci/AGENTS_POLICY.md for guardrails and override process. name: Agents Auto-Pilot +# Correlating a run to its issue needs the issue number in the run title; +# without it `gh run list` shows only the workflow name and no per-issue +# history can be reconstructed. +run-name: >- + Agents Auto-Pilot + #${{ github.event.issue.number || github.event.pull_request.number || + inputs.issue_number }} + # End-to-end automation: Issue → Format → Optimize → Apply → Agent → Keepalive → Merge # Triggered by: # 1. agents:auto-pilot label (initial trigger) diff --git a/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml b/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml index cc30e0824..18543f9f8 100644 --- a/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml +++ b/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml @@ -1,6 +1,14 @@ # See docs/ci/AGENTS_POLICY.md for guardrails and override process. name: Agents Auto-Pilot +# Correlating a run to its issue needs the issue number in the run title; +# without it `gh run list` shows only the workflow name and no per-issue +# history can be reconstructed. +run-name: >- + Agents Auto-Pilot + #${{ github.event.issue.number || github.event.pull_request.number || + inputs.issue_number }} + # End-to-end automation: Issue → Format → Optimize → Apply → Agent → Keepalive → Merge # Triggered by: # 1. agents:auto-pilot label (initial trigger) From 446e70e215821b18dee03abeaf3d12e3de2a3ded Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 12:17:38 +0000 Subject: [PATCH 14/19] Belt dispatcher: give each dispatched issue its own concurrency lane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The consumer template still carried `group: codex-belt-dispatcher`, a single fixed lane for the whole repository, while the Workflows-local copy had already moved to a per-agent group. Consumers therefore serialised every belt dispatch. Dispatching several issues at once puts them all in that one lane. GitHub keeps one running plus one waiting and cancels the rest while they are still pending, so they surface as failed runs with no jobs and no log — which reads as the dispatcher being broken rather than as contention. Six evaluation issues produced one running, one waiting and four cancelled, and every agent sat in "Waiting for agent to create branch" backoff indefinitely because no branch was ever created. Key the group by agent and forced issue. Auto-select runs (no force_issue) still share one lane per agent, so the "pick the next issue" path cannot race itself into double-assignment; explicitly forced per-issue dispatches now run in parallel. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FC8XoyssN5v6hQCTtcjoB5 --- .../workflows/agents-71-codex-belt-dispatcher.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/templates/consumer-repo/.github/workflows/agents-71-codex-belt-dispatcher.yml b/templates/consumer-repo/.github/workflows/agents-71-codex-belt-dispatcher.yml index 298d9b5d4..441ef05c4 100644 --- a/templates/consumer-repo/.github/workflows/agents-71-codex-belt-dispatcher.yml +++ b/templates/consumer-repo/.github/workflows/agents-71-codex-belt-dispatcher.yml @@ -96,7 +96,15 @@ permissions: actions: write concurrency: - group: codex-belt-dispatcher + # Keyed by the forced issue so per-issue dispatches run in parallel. A fixed + # group serialises the whole belt: with several issues dispatched at once, + # GitHub keeps one running plus one waiting and CANCELS the rest while they + # are still pending, which surfaces as a failed run with no jobs and no log. + # Auto-select runs (no force_issue) still share one lane, so the "pick the + # next issue" path cannot race itself into double-assignment. + group: >- + belt-dispatcher-${{ inputs.agent_key || 'codex' }}-${{ + inputs.force_issue || 'auto-select' }} cancel-in-progress: false jobs: From 77ae451f6a9f026268441c6c3426953384a14955 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 12:57:23 +0000 Subject: [PATCH 15/19] Belt dispatcher: honour the base-branch marker reusable-agents-issue-bridge.yml learned to resolve a base branch from an explicit input, then the issue's `` marker, then the repository default. agents-71-codex-belt-dispatcher kept `const base = repoInfo.default_branch` and was never updated. That is the path auto-pilot actually dispatches, so in practice the marker was ignored: an evaluation issue carrying `` produced "Selected issue #246 -> branch cursor/issue-246 on base main", and every agent branch was cut from the default branch regardless of the lane it was meant for. Adds the same resolution order and existence guard, plus a `base_branch` input so callers can override explicitly, and logs the chosen base and its source. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FC8XoyssN5v6hQCTtcjoB5 --- .../agents-71-codex-belt-dispatcher.yml | 48 ++++++++++++++++++- .../agents-71-codex-belt-dispatcher.yml | 48 ++++++++++++++++++- 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/.github/workflows/agents-71-codex-belt-dispatcher.yml b/.github/workflows/agents-71-codex-belt-dispatcher.yml index 025a512a9..f5e2c9271 100644 --- a/.github/workflows/agents-71-codex-belt-dispatcher.yml +++ b/.github/workflows/agents-71-codex-belt-dispatcher.yml @@ -20,6 +20,13 @@ on: required: false default: false type: boolean + base_branch: + description: >- + Optional base branch for the agent branch. Overrides the issue's + `` marker and the repository default. + required: false + default: '' + type: string orchestrator_skill_pack: description: >- Optional reference-pack name override for exported Orchestrator skill context on @@ -76,6 +83,13 @@ on: required: false default: false type: boolean + base_branch: + description: >- + Optional base branch for the agent branch. Overrides the issue's + `` marker and the repository default. + required: false + default: '' + type: string orchestrator_skill_pack: description: >- Optional reference-pack name override for exported Orchestrator skill context on @@ -292,12 +306,42 @@ jobs: ]); const { data: repoInfo } = await withRetry((client) => client.rest.repos.get({ owner, repo })); - const base = repoInfo.default_branch; - if (!base) { + const defaultBranch = repoInfo.default_branch; + if (!defaultBranch) { core.setFailed('Repository default branch not available'); return; } + // Base resolution mirrors reusable-agents-issue-bridge.yml: an + // explicit input wins, then the issue's `` + // marker, then the repository default. Without the marker every + // agent branch is cut from the default branch, which silently + // defeats any per-lane workflow (evaluation lanes, release trains, + // long-lived feature bases). + let base = String(process.env.INPUT_BASE_BRANCH || '').trim(); + let baseSource = base ? 'input' : ''; + if (!base) { + try { + const { data: issueData } = await withRetry((client) => + client.rest.issues.get({ owner, repo, issue_number: issueNumber })); + const marker = String(issueData.body || '') + .match(//); + if (marker) { base = marker[1].trim(); baseSource = 'issue-marker'; } + } catch (error) { + core.warning(`Could not read issue body for a base-branch marker: ${error.message}`); + } + } + if (!base) { base = defaultBranch; baseSource = 'default'; } + if (base !== defaultBranch) { + try { + await withRetry((client) => client.rest.repos.getBranch({ owner, repo, branch: base })); + } catch (error) { + core.warning(`Base branch '${base}' not found; falling back to '${defaultBranch}'.`); + base = defaultBranch; baseSource = 'default-fallback'; + } + } + core.info(`Base branch: ${base} (source: ${baseSource})`); + let branchPrefix = 'codex/issue-'; try { const { getAgentConfig } = require('./.github/scripts/agent_registry.js'); diff --git a/templates/consumer-repo/.github/workflows/agents-71-codex-belt-dispatcher.yml b/templates/consumer-repo/.github/workflows/agents-71-codex-belt-dispatcher.yml index 298d9b5d4..9d560b665 100644 --- a/templates/consumer-repo/.github/workflows/agents-71-codex-belt-dispatcher.yml +++ b/templates/consumer-repo/.github/workflows/agents-71-codex-belt-dispatcher.yml @@ -20,6 +20,13 @@ on: required: false default: false type: boolean + base_branch: + description: >- + Optional base branch for the agent branch. Overrides the issue's + `` marker and the repository default. + required: false + default: '' + type: string orchestrator_skill_pack: description: >- Optional reference-pack name override for exported Orchestrator skill context on @@ -76,6 +83,13 @@ on: required: false default: false type: boolean + base_branch: + description: >- + Optional base branch for the agent branch. Overrides the issue's + `` marker and the repository default. + required: false + default: '' + type: string orchestrator_skill_pack: description: >- Optional reference-pack name override for exported Orchestrator skill context on @@ -292,12 +306,42 @@ jobs: ]); const { data: repoInfo } = await withRetry((client) => client.rest.repos.get({ owner, repo })); - const base = repoInfo.default_branch; - if (!base) { + const defaultBranch = repoInfo.default_branch; + if (!defaultBranch) { core.setFailed('Repository default branch not available'); return; } + // Base resolution mirrors reusable-agents-issue-bridge.yml: an + // explicit input wins, then the issue's `` + // marker, then the repository default. Without the marker every + // agent branch is cut from the default branch, which silently + // defeats any per-lane workflow (evaluation lanes, release trains, + // long-lived feature bases). + let base = String(process.env.INPUT_BASE_BRANCH || '').trim(); + let baseSource = base ? 'input' : ''; + if (!base) { + try { + const { data: issueData } = await withRetry((client) => + client.rest.issues.get({ owner, repo, issue_number: issueNumber })); + const marker = String(issueData.body || '') + .match(//); + if (marker) { base = marker[1].trim(); baseSource = 'issue-marker'; } + } catch (error) { + core.warning(`Could not read issue body for a base-branch marker: ${error.message}`); + } + } + if (!base) { base = defaultBranch; baseSource = 'default'; } + if (base !== defaultBranch) { + try { + await withRetry((client) => client.rest.repos.getBranch({ owner, repo, branch: base })); + } catch (error) { + core.warning(`Base branch '${base}' not found; falling back to '${defaultBranch}'.`); + base = defaultBranch; baseSource = 'default-fallback'; + } + } + core.info(`Base branch: ${base} (source: ${baseSource})`); + let branchPrefix = 'codex/issue-'; try { const { getAgentConfig } = require('./.github/scripts/agent_registry.js'); From 6adf8d4603f45035764c2625b9d233eec4ab9241 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 10:35:29 +0000 Subject: [PATCH 16/19] Ship the Codex auth expiry check to consumer repos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit health-codex-auth-check runs twice daily, decodes CODEX_AUTH_JSON, and opens an `auth-expiring` issue before the token lapses. It existed only in this repository, so consumers — where the agents actually run and where the secret actually matters — had no auth monitoring at all. That gap has a cost on record: a consumer's CODEX_AUTH_JSON expired and every dispatched agent produced no commits, with no error naming the cause; the same failure is documented upstream as having gone unnoticed for 34 days. The workflow is already portable. It sparse-checks-out error_classifier.js, github-api-with-retry.js and token_load_balancer.js and uses the local setup-api-client action — all four are already present in consumer repos via this manifest — and needs only CODEX_AUTH_JSON, which consumers already set for the agent runners. Copied verbatim; no changes required. Detection only. Refreshing the token needs `codex login --device-auth` at a browser, so the most any automation can do is warn early. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FC8XoyssN5v6hQCTtcjoB5 --- .github/sync-manifest.yml | 3 + .../workflows/health-codex-auth-check.yml | 305 ++++++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 templates/consumer-repo/.github/workflows/health-codex-auth-check.yml diff --git a/.github/sync-manifest.yml b/.github/sync-manifest.yml index 411ccfcad..a9fad3327 100644 --- a/.github/sync-manifest.yml +++ b/.github/sync-manifest.yml @@ -74,6 +74,9 @@ workflows: - source: .github/workflows/agents-72-codex-belt-worker.yml description: "Codex belt worker - executes agent on issues with full prompt and context" + - source: .github/workflows/health-codex-auth-check.yml + description: "Twice-daily CODEX_AUTH_JSON expiry check - opens an auth-expiring issue before the token lapses" + - source: .github/workflows/agents-72-codex-belt-worker-dispatch.yml description: "Codex belt worker dispatch wrapper - allows workflow_dispatch for the worker" diff --git a/templates/consumer-repo/.github/workflows/health-codex-auth-check.yml b/templates/consumer-repo/.github/workflows/health-codex-auth-check.yml new file mode 100644 index 000000000..be94e8f7e --- /dev/null +++ b/templates/consumer-repo/.github/workflows/health-codex-auth-check.yml @@ -0,0 +1,305 @@ +# Scheduled check for Codex auth token expiration +# Creates an issue when token is close to expiring to prompt manual refresh +name: Health 46 Codex Auth Check + +on: + schedule: + # Run twice daily at 8am and 8pm UTC + - cron: '0 8,20 * * *' + workflow_dispatch: + inputs: + force_check: + description: 'Force check even if issue exists' + type: boolean + default: false + +permissions: + contents: read + issues: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-expiration: + runs-on: ubuntu-latest + steps: + - name: Checkout retry helpers + uses: actions/checkout@v7 + with: + sparse-checkout: | + .github/actions/setup-api-client + .github/scripts/error_classifier.js + .github/scripts/github-api-with-retry.js + .github/scripts/token_load_balancer.js + sparse-checkout-cone-mode: false + + - name: Setup API client + uses: ./.github/actions/setup-api-client + with: + secrets: ${{ toJSON(secrets) }} + github_token: ${{ github.token }} + + + + - name: Check for existing open issue + id: existing + uses: actions/github-script@v9 + with: + github-token: ${{ github.token }} + script: | + const fs = require('fs'); + const retryHelperPath = './.github/scripts/github-api-with-retry.js'; + const retryHelpers = fs.existsSync(retryHelperPath) + ? require(retryHelperPath) + : { + withRetry: (fn) => fn(), + paginateWithRetry: (githubInstance, method, params) => + githubInstance.paginate(method, params), + }; + const { withRetry } = retryHelpers; + + const issues = await withRetry(() => github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'auth-expiring', + per_page: 1 + })); + const exists = issues.data.length > 0; + console.log(`Existing auth-expiring issue: ${exists}`); + if (exists) { + console.log(`Issue #${issues.data[0].number}: ${issues.data[0].title}`); + } + return exists; + result-encoding: string + + - name: Check token expiration + id: check + env: + CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }} + run: | + set -euo pipefail + + if [ -z "$CODEX_AUTH_JSON" ]; then + echo "status=missing" >> "$GITHUB_OUTPUT" + echo "::error::CODEX_AUTH_JSON secret is not set" + exit 0 + fi + + # Write auth.json temporarily + mkdir -p ~/.codex + echo "$CODEX_AUTH_JSON" > ~/.codex/auth.json + + # Check expiration with Python + python3 << 'PYEOF' + import json, base64, datetime, sys, os + + auth_path = os.path.expanduser("~/.codex/auth.json") + gh_output = os.environ.get("GITHUB_OUTPUT", "/dev/null") + + try: + with open(auth_path) as f: + auth = json.load(f) + token = auth.get("tokens", {}).get("access_token", "") + if not token: + with open(gh_output, "a") as out: + out.write("status=invalid\n") + print("No access token found") + sys.exit(0) + + parts = token.split(".") + if len(parts) != 3: + with open(gh_output, "a") as out: + out.write("status=invalid\n") + print("Invalid JWT format") + sys.exit(0) + + payload = parts[1] + "=" * (4 - len(parts[1]) % 4) + data = json.loads(base64.urlsafe_b64decode(payload)) + exp_time = datetime.datetime.fromtimestamp(data["exp"], tz=datetime.timezone.utc) + now = datetime.datetime.now(tz=datetime.timezone.utc) + diff = exp_time - now + days_left = diff.days + hours_left = diff.total_seconds() / 3600 + + print(f"Token expires: {exp_time.isoformat()}") + print(f"Days until expiration: {days_left}") + print(f"Hours until expiration: {hours_left:.1f}") + + with open(gh_output, "a") as out: + out.write(f"expires_at={exp_time.strftime('%Y-%m-%d %H:%M UTC')}\n") + out.write(f"days_left={days_left}\n") + out.write(f"hours_left={hours_left:.0f}\n") + + if days_left < 0: + out.write("status=expired\n") + elif days_left < 2: + out.write("status=expiring-soon\n") + elif days_left < 5: + out.write("status=expiring\n") + else: + out.write("status=ok\n") + + except Exception as e: + print(f"Error checking token: {e}") + with open(gh_output, "a") as out: + out.write("status=error\n") + PYEOF + + # Cleanup + rm -f ~/.codex/auth.json + + - name: Create expiration warning issue + if: >- + steps.existing.outputs.result != 'true' && + (steps.check.outputs.status == 'expiring-soon' || + steps.check.outputs.status == 'expired') + uses: actions/github-script@v9 + with: + github-token: ${{ github.token }} + script: | + const fs = require('fs'); + const retryHelperPath = './.github/scripts/github-api-with-retry.js'; + const retryHelpers = fs.existsSync(retryHelperPath) + ? require(retryHelperPath) + : { + withRetry: (fn) => fn(), + paginateWithRetry: (githubInstance, method, params) => + githubInstance.paginate(method, params), + }; + const { withRetry } = retryHelpers; + const status = '${{ steps.check.outputs.status }}'; + const expiresAt = '${{ steps.check.outputs.expires_at }}'; + const hoursLeft = '${{ steps.check.outputs.hours_left }}'; + const daysLeft = '${{ steps.check.outputs.days_left }}'; + + const isExpired = status === 'expired'; + const title = isExpired + ? '🚨 CODEX_AUTH_JSON has expired - CI agents broken' + : `⚠️ CODEX_AUTH_JSON expires in ${hoursLeft} hours`; + + const body = `## Codex Authentication Token ${isExpired ? 'Expired' : 'Expiring Soon'} + + ${isExpired + ? '**The token has already expired.** CI workflows using Codex are currently broken.' + : `**Token expires:** ${expiresAt} (${hoursLeft} hours / ${daysLeft} days remaining)` + } + + ### Action Required + + Run the device authentication flow to refresh the token: + + \`\`\`bash + # 1. Authenticate with device code flow + codex login --device-auth + + # 2. Follow the prompts: + # - Go to https://auth.openai.com/codex/device + # - Enter the code displayed in terminal + # - Complete authentication + + # 3. Copy the new auth.json content + cat ~/.codex/auth.json + \`\`\` + + ### Update GitHub Secret + + 1. Go to Repository Secrets: + + https://github.com/${context.repo.owner}/${context.repo.repo}/settings/secrets/actions + 2. Click on \`CODEX_AUTH_JSON\` → **Update** + 3. Paste the JSON content from step 3 above + 4. Click **Update secret** + + ### Affected Workflows + + - \`agents-keepalive-loop.yml\` - Codex agent task processing + - \`agents-autofix-loop.yml\` - Automated CI failure fixes + - \`agents-verifier.yml\` - PR verification + - \`reusable-codex-run.yml\` - Reusable Codex runner + + ### Documentation + + Documentation link: + + /${context.repo.owner}/${context.repo.repo}/blob/main/docs/ci/CHATGPT_SUBSCRIPTION_CI.md + + See the linked doc for full details on ChatGPT subscription authentication in CI. + + --- + *This issue was automatically created by the \`health-codex-auth-check\` workflow.* + `; + + await withRetry(() => + github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: title, + body: body, + labels: ['auth-expiring', 'ci', 'priority:high'], + }) + ); + + console.log(`Created issue: ${title}`); + + - name: Summary + env: + STATUS: ${{ steps.check.outputs.status }} + EXISTING: ${{ steps.existing.outputs.result }} + EXPIRES_AT: ${{ steps.check.outputs.expires_at }} + DAYS_LEFT: ${{ steps.check.outputs.days_left }} + HOURS_LEFT: ${{ steps.check.outputs.hours_left }} + run: | + { + echo "## Codex Auth Check Results" + echo "" + + if [ -z "$STATUS" ]; then + echo "⏭️ **Skipped** - Check was not run" + elif [ "$STATUS" = "ok" ]; then + echo "✅ **Token OK** - Expires $EXPIRES_AT ($DAYS_LEFT days)" + elif [ "$STATUS" = "expiring" ]; then + echo "ℹ️ **Token expiring** - Expires $EXPIRES_AT ($DAYS_LEFT days)" + elif [ "$STATUS" = "expiring-soon" ]; then + echo "⚠️ **Token expiring soon** - Expires $EXPIRES_AT ($HOURS_LEFT hours)" + echo "" + if [ "$EXISTING" = "true" ]; then + echo "🎫 Existing auth-expiring issue remains open" + else + echo "🎫 Issue created to prompt token refresh" + fi + elif [ "$STATUS" = "expired" ]; then + echo "🚨 **Token EXPIRED** - Was due $EXPIRES_AT" + echo "" + if [ "$EXISTING" = "true" ]; then + echo "🎫 Existing auth-expiring issue remains open" + else + echo "🎫 Issue created - CI agents are broken" + fi + elif [ "$STATUS" = "missing" ]; then + echo "❌ **Secret missing** - CODEX_AUTH_JSON is not set" + elif [ "$STATUS" = "invalid" ]; then + echo "❌ **Secret invalid** - CODEX_AUTH_JSON has no usable access token" + elif [ "$STATUS" = "error" ]; then + echo "❌ **Auth check failed** - CODEX_AUTH_JSON could not be decoded" + else + echo "⚠️ **Unknown status**: $STATUS" + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Fail on unusable auth + if: always() + env: + STATUS: ${{ steps.check.outputs.status }} + run: | + case "$STATUS" in + missing|invalid|error) + echo "::error::CODEX_AUTH_JSON is ${STATUS}; Codex workflows cannot authenticate." + exit 1 + ;; + *) + exit 0 + ;; + esac From cb15ccf79641121bfbab2a021b60823aa2494c90 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 15:40:40 +0000 Subject: [PATCH 17/19] Auto-pilot: honour the base-branch marker when opening the pull request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The issue bridge and the belt dispatcher both resolve a pull request base from the issue's `` marker. Auto-pilot did not: it took `repoInfo.default_branch` at both of its PR sites and never read the marker at all. Auto-pilot is the component that actually calls pulls.create, so the marker was honoured when the branch was cut and then ignored when the PR was opened. An issue marked `eval/cursor` produced branch `cursor/issue-285` and a pull request into `main` — the per-lane separation collapsed silently, and merging any such PR would put one lane's work on the shared branch. Both sites now resolve marker over default, verify the branch exists before using it, fall back with a warning when it does not, and log the chosen base and its source. This is the third and last place that computed a PR base: the bridge, the belt dispatcher and auto-pilot are now consistent, and no `const baseBranch = repoInfo.default_branch` remains in any of them. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FC8XoyssN5v6hQCTtcjoB5 --- .github/workflows/agents-auto-pilot.yml | 90 ++++++++++++++++++- .../.github/workflows/agents-auto-pilot.yml | 90 ++++++++++++++++++- 2 files changed, 172 insertions(+), 8 deletions(-) diff --git a/.github/workflows/agents-auto-pilot.yml b/.github/workflows/agents-auto-pilot.yml index eddc39b63..01259d1ed 100644 --- a/.github/workflows/agents-auto-pilot.yml +++ b/.github/workflows/agents-auto-pilot.yml @@ -2888,12 +2888,53 @@ jobs: owner: context.repo.owner, repo: context.repo.repo })); - const baseBranch = repoInfo.default_branch; - if (!baseBranch) { + const defaultBranch = repoInfo.default_branch; + if (!defaultBranch) { core.setFailed('Repository default branch not available'); return; } + // Resolve the pull request base the same way the issue bridge and the + // belt dispatcher do: the issue's `` marker + // wins over the repository default. Auto-pilot opens the pull request + // itself, so without this the marker is honoured when the branch is + // cut and then ignored when the PR is opened — the PR lands on the + // default branch and any per-lane workflow silently collapses. + let baseBranch = defaultBranch; + let baseSource = 'default'; + try { + const { data: baseIssue } = await withRetry((client) => + client.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + })); + const marker = String(baseIssue.body || '') + .match(//); + if (marker) { + const candidate = marker[1].trim(); + if (candidate && candidate !== defaultBranch) { + try { + await withRetry((client) => client.rest.repos.getBranch({ + owner: context.repo.owner, + repo: context.repo.repo, + branch: candidate, + })); + baseBranch = candidate; + baseSource = 'issue-marker'; + } catch (branchError) { + core.warning( + `Base branch '${candidate}' from the issue marker was not found; ` + + `falling back to '${defaultBranch}'.`); + baseSource = 'default-fallback'; + } + } + } + } catch (markerError) { + core.warning(`Could not read issue #${issueNumber} for a base-branch marker: ${markerError.message}`); + } + core.info(`Base branch: ${baseBranch} (source: ${baseSource})`); + // If a PR already exists for this branch, stop create-pr loop try { const headRef = `${context.repo.owner}:${branchName}`; @@ -3762,12 +3803,53 @@ jobs: owner: context.repo.owner, repo: context.repo.repo })); - const baseBranch = repoInfo.default_branch; - if (!baseBranch) { + const defaultBranch = repoInfo.default_branch; + if (!defaultBranch) { core.setFailed('Repository default branch not available'); return; } + // Resolve the pull request base the same way the issue bridge and the + // belt dispatcher do: the issue's `` marker + // wins over the repository default. Auto-pilot opens the pull request + // itself, so without this the marker is honoured when the branch is + // cut and then ignored when the PR is opened — the PR lands on the + // default branch and any per-lane workflow silently collapses. + let baseBranch = defaultBranch; + let baseSource = 'default'; + try { + const { data: baseIssue } = await withRetry((client) => + client.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + })); + const marker = String(baseIssue.body || '') + .match(//); + if (marker) { + const candidate = marker[1].trim(); + if (candidate && candidate !== defaultBranch) { + try { + await withRetry((client) => client.rest.repos.getBranch({ + owner: context.repo.owner, + repo: context.repo.repo, + branch: candidate, + })); + baseBranch = candidate; + baseSource = 'issue-marker'; + } catch (branchError) { + core.warning( + `Base branch '${candidate}' from the issue marker was not found; ` + + `falling back to '${defaultBranch}'.`); + baseSource = 'default-fallback'; + } + } + } + } catch (markerError) { + core.warning(`Could not read issue #${issueNumber} for a base-branch marker: ${markerError.message}`); + } + core.info(`Base branch: ${baseBranch} (source: ${baseSource})`); + const scriptsPath = process.env.WORKFLOWS_SCRIPTS_PATH || process.env.GITHUB_WORKSPACE; const { redispatchForceStep } = require( `${scriptsPath}/.github/scripts/auto_pilot_transitions.js` diff --git a/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml b/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml index 18543f9f8..f5c347096 100644 --- a/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml +++ b/templates/consumer-repo/.github/workflows/agents-auto-pilot.yml @@ -2888,12 +2888,53 @@ jobs: owner: context.repo.owner, repo: context.repo.repo })); - const baseBranch = repoInfo.default_branch; - if (!baseBranch) { + const defaultBranch = repoInfo.default_branch; + if (!defaultBranch) { core.setFailed('Repository default branch not available'); return; } + // Resolve the pull request base the same way the issue bridge and the + // belt dispatcher do: the issue's `` marker + // wins over the repository default. Auto-pilot opens the pull request + // itself, so without this the marker is honoured when the branch is + // cut and then ignored when the PR is opened — the PR lands on the + // default branch and any per-lane workflow silently collapses. + let baseBranch = defaultBranch; + let baseSource = 'default'; + try { + const { data: baseIssue } = await withRetry((client) => + client.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + })); + const marker = String(baseIssue.body || '') + .match(//); + if (marker) { + const candidate = marker[1].trim(); + if (candidate && candidate !== defaultBranch) { + try { + await withRetry((client) => client.rest.repos.getBranch({ + owner: context.repo.owner, + repo: context.repo.repo, + branch: candidate, + })); + baseBranch = candidate; + baseSource = 'issue-marker'; + } catch (branchError) { + core.warning( + `Base branch '${candidate}' from the issue marker was not found; ` + + `falling back to '${defaultBranch}'.`); + baseSource = 'default-fallback'; + } + } + } + } catch (markerError) { + core.warning(`Could not read issue #${issueNumber} for a base-branch marker: ${markerError.message}`); + } + core.info(`Base branch: ${baseBranch} (source: ${baseSource})`); + // If a PR already exists for this branch, stop create-pr loop try { const headRef = `${context.repo.owner}:${branchName}`; @@ -3759,12 +3800,53 @@ jobs: owner: context.repo.owner, repo: context.repo.repo })); - const baseBranch = repoInfo.default_branch; - if (!baseBranch) { + const defaultBranch = repoInfo.default_branch; + if (!defaultBranch) { core.setFailed('Repository default branch not available'); return; } + // Resolve the pull request base the same way the issue bridge and the + // belt dispatcher do: the issue's `` marker + // wins over the repository default. Auto-pilot opens the pull request + // itself, so without this the marker is honoured when the branch is + // cut and then ignored when the PR is opened — the PR lands on the + // default branch and any per-lane workflow silently collapses. + let baseBranch = defaultBranch; + let baseSource = 'default'; + try { + const { data: baseIssue } = await withRetry((client) => + client.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + })); + const marker = String(baseIssue.body || '') + .match(//); + if (marker) { + const candidate = marker[1].trim(); + if (candidate && candidate !== defaultBranch) { + try { + await withRetry((client) => client.rest.repos.getBranch({ + owner: context.repo.owner, + repo: context.repo.repo, + branch: candidate, + })); + baseBranch = candidate; + baseSource = 'issue-marker'; + } catch (branchError) { + core.warning( + `Base branch '${candidate}' from the issue marker was not found; ` + + `falling back to '${defaultBranch}'.`); + baseSource = 'default-fallback'; + } + } + } + } catch (markerError) { + core.warning(`Could not read issue #${issueNumber} for a base-branch marker: ${markerError.message}`); + } + core.info(`Base branch: ${baseBranch} (source: ${baseSource})`); + const scriptsPath = process.env.WORKFLOWS_SCRIPTS_PATH || process.env.GITHUB_WORKSPACE; const { redispatchForceStep } = require( `${scriptsPath}/.github/scripts/auto_pilot_transitions.js` From 43cbb88f07c3ed6b99269695c1651ad4de954cf1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 11 Sep 2026 12:56:28 +0000 Subject: [PATCH 18/19] Give Codex a usable sandbox in the consolidated loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex defaults to `workspace-write`, which makes it sandbox with bubblewrap. bwrap cannot create a loopback interface on a GitHub-hosted runner: bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted Every shell command then fails before it runs. Codex reported this exactly — "the workspace shell fails before any command runs ... I therefore can't inspect the existing availability API, run tests, or create the required verified commit safely" — and its session recorded 0 commands executed and 0 files changed. From outside that reads as the agent producing nothing: the job is green, the ledger closes the task, and the only commit is a telemetry timestamp. Two evaluation rounds were scored against Codex on this basis before the session transcript was read. The runner is an ephemeral single-tenant VM and is already the isolation boundary, so Codex's inner sandbox buys nothing here. Pass `danger-full-access` on both Codex paths in the consolidated loop, matching what the runner already documents for CI use. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FC8XoyssN5v6hQCTtcjoB5 --- .../.github/workflows/agents-81-gate-followups.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/templates/consumer-repo/.github/workflows/agents-81-gate-followups.yml b/templates/consumer-repo/.github/workflows/agents-81-gate-followups.yml index f0defcf3f..003623985 100644 --- a/templates/consumer-repo/.github/workflows/agents-81-gate-followups.yml +++ b/templates/consumer-repo/.github/workflows/agents-81-gate-followups.yml @@ -684,6 +684,15 @@ jobs: needs.evaluate.outputs.action != 'conflict' }} prompt_file: ${{ needs.evaluate.outputs.prompt_file }} mode: keepalive + # Codex sandboxes with bubblewrap under `workspace-write`, and bwrap cannot + # create a loopback interface on a GitHub-hosted runner: + # bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted + # Every shell command then fails before it runs, so Codex correctly refuses + # the task and reports "Blocked" having executed 0 commands and written 0 + # files — which reads as the agent producing nothing. The runner is an + # ephemeral, single-tenant VM that is already the isolation boundary, so + # dropping Codex's inner sandbox costs nothing here. + sandbox: danger-full-access pr_number: ${{ needs.evaluate.outputs.pr_number }} pr_ref: ${{ needs.evaluate.outputs.pr_ref }} appendix: ${{ needs.evaluate.outputs.task_appendix }} @@ -1653,6 +1662,8 @@ jobs: with: prompt_file: .github/codex/prompts/autofix_from_ci_failure.md mode: autofix + # Same bubblewrap failure as run-codex; see the note there. + sandbox: danger-full-access pr_number: ${{ needs.prepare.outputs.pr_number }} pr_ref: ${{ needs.prepare.outputs.head_ref }} appendix: ${{ needs.prepare.outputs.appendix }} From c2d357868f29a45ae7c9f198e23a8d5f32539c06 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 12:57:38 +0000 Subject: [PATCH 19/19] Follow-up creator: prefer a GitHub App token agents-verify-to-new-pr chose OWNER_PR_PAT, then SERVICE_BOT_PAT, then GITHUB_TOKEN. It never tried a GitHub App installation token, unlike the rest of the fleet, so a consumer with neither PAT set created every follow-up issue with GITHUB_TOKEN. Issues and labels created that way raise no events for other workflows (GitHub's loop guard), so the follow-up issue was created correctly, carried the right labels, and then sat there: auto-pilot never saw it and a human had to remove and re-apply `agents:auto-pilot` by hand to start the work. The automatic remediation loop silently required a manual step on every iteration. Mint an App token when WORKFLOWS_APP_ID and WORKFLOWS_APP_PRIVATE_KEY are present and prefer it, keeping the PAT chain as fallback. When nothing better than GITHUB_TOKEN is available the step now warns that the issue will not trigger auto-pilot, instead of failing silently. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FC8XoyssN5v6hQCTtcjoB5 --- .github/workflows/agents-verify-to-new-pr.yml | 28 ++++++++++++++++++- .../workflows/agents-verify-to-new-pr.yml | 28 ++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/.github/workflows/agents-verify-to-new-pr.yml b/.github/workflows/agents-verify-to-new-pr.yml index 7f5cbd0e4..25c1daf1a 100644 --- a/.github/workflows/agents-verify-to-new-pr.yml +++ b/.github/workflows/agents-verify-to-new-pr.yml @@ -29,6 +29,9 @@ jobs: create-new-pr: if: github.event.label.name == 'verify:create-new-pr' runs-on: ubuntu-latest + env: + WORKFLOWS_APP_ID: ${{ secrets.WORKFLOWS_APP_ID || '' }} + WORKFLOWS_APP_PRIVATE_KEY: ${{ secrets.WORKFLOWS_APP_PRIVATE_KEY || '' }} steps: - name: Check PR is merged id: check-merged @@ -45,21 +48,44 @@ jobs: core.setOutput('pr_number', pr.number); core.setOutput('pr_title', pr.title); + - name: Mint GitHub App token + id: app_token + if: >- + steps.check-merged.outputs.merged == 'true' && + env.WORKFLOWS_APP_ID != '' && env.WORKFLOWS_APP_PRIVATE_KEY != '' + continue-on-error: true + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ env.WORKFLOWS_APP_ID }} + private-key: ${{ env.WORKFLOWS_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + - name: Select GitHub token id: select-token if: steps.check-merged.outputs.merged == 'true' env: + APP_TOKEN: ${{ steps.app_token.outputs.token }} OWNER_PR_PAT: ${{ secrets.OWNER_PR_PAT }} SERVICE_BOT_PAT: ${{ secrets.SERVICE_BOT_PAT }} GITHUB_TOKEN: ${{ github.token }} run: | - if [ -n "$OWNER_PR_PAT" ]; then + # Prefer an App installation token. Issues and labels created with + # GITHUB_TOKEN raise no events for other workflows (GitHub's loop + # guard), so a follow-up issue created that way is never picked up by + # auto-pilot and waits until a human re-applies its label by hand. + if [ -n "$APP_TOKEN" ]; then + echo "token=$APP_TOKEN" >> "$GITHUB_OUTPUT" + echo "source=workflows-app" >> "$GITHUB_OUTPUT" + elif [ -n "$OWNER_PR_PAT" ]; then echo "token=$OWNER_PR_PAT" >> "$GITHUB_OUTPUT" echo "source=owner-pat" >> "$GITHUB_OUTPUT" elif [ -n "$SERVICE_BOT_PAT" ]; then echo "token=$SERVICE_BOT_PAT" >> "$GITHUB_OUTPUT" echo "source=service-bot" >> "$GITHUB_OUTPUT" else + echo "::warning::No App token or PAT; using GITHUB_TOKEN." + echo "::warning::The follow-up issue will NOT trigger auto-pilot." + echo "::warning::Set WORKFLOWS_APP_ID + _PRIVATE_KEY, or SERVICE_BOT_PAT." echo "token=$GITHUB_TOKEN" >> "$GITHUB_OUTPUT" echo "source=github-token" >> "$GITHUB_OUTPUT" fi diff --git a/templates/consumer-repo/.github/workflows/agents-verify-to-new-pr.yml b/templates/consumer-repo/.github/workflows/agents-verify-to-new-pr.yml index 86bae748b..a0918af0f 100644 --- a/templates/consumer-repo/.github/workflows/agents-verify-to-new-pr.yml +++ b/templates/consumer-repo/.github/workflows/agents-verify-to-new-pr.yml @@ -29,6 +29,9 @@ jobs: create-new-pr: if: github.event.label.name == 'verify:create-new-pr' runs-on: ubuntu-latest + env: + WORKFLOWS_APP_ID: ${{ secrets.WORKFLOWS_APP_ID || '' }} + WORKFLOWS_APP_PRIVATE_KEY: ${{ secrets.WORKFLOWS_APP_PRIVATE_KEY || '' }} steps: - name: Check PR is merged id: check-merged @@ -45,21 +48,44 @@ jobs: core.setOutput('pr_number', pr.number); core.setOutput('pr_title', pr.title); + - name: Mint GitHub App token + id: app_token + if: >- + steps.check-merged.outputs.merged == 'true' && + env.WORKFLOWS_APP_ID != '' && env.WORKFLOWS_APP_PRIVATE_KEY != '' + continue-on-error: true + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ env.WORKFLOWS_APP_ID }} + private-key: ${{ env.WORKFLOWS_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + - name: Select GitHub token id: select-token if: steps.check-merged.outputs.merged == 'true' env: + APP_TOKEN: ${{ steps.app_token.outputs.token }} OWNER_PR_PAT: ${{ secrets.OWNER_PR_PAT }} SERVICE_BOT_PAT: ${{ secrets.SERVICE_BOT_PAT }} GITHUB_TOKEN: ${{ github.token }} run: | - if [ -n "$OWNER_PR_PAT" ]; then + # Prefer an App installation token. Issues and labels created with + # GITHUB_TOKEN raise no events for other workflows (GitHub's loop + # guard), so a follow-up issue created that way is never picked up by + # auto-pilot and waits until a human re-applies its label by hand. + if [ -n "$APP_TOKEN" ]; then + echo "token=$APP_TOKEN" >> "$GITHUB_OUTPUT" + echo "source=workflows-app" >> "$GITHUB_OUTPUT" + elif [ -n "$OWNER_PR_PAT" ]; then echo "token=$OWNER_PR_PAT" >> "$GITHUB_OUTPUT" echo "source=owner-pat" >> "$GITHUB_OUTPUT" elif [ -n "$SERVICE_BOT_PAT" ]; then echo "token=$SERVICE_BOT_PAT" >> "$GITHUB_OUTPUT" echo "source=service-bot" >> "$GITHUB_OUTPUT" else + echo "::warning::No App token or PAT; using GITHUB_TOKEN." + echo "::warning::The follow-up issue will NOT trigger auto-pilot." + echo "::warning::Set WORKFLOWS_APP_ID + _PRIVATE_KEY, or SERVICE_BOT_PAT." echo "token=$GITHUB_TOKEN" >> "$GITHUB_OUTPUT" echo "source=github-token" >> "$GITHUB_OUTPUT" fi