From 7bc5e6cde63a903d983609fc5483a7d1b696c85e Mon Sep 17 00:00:00 2001 From: Marcus Kok Date: Tue, 16 Jun 2026 14:27:07 -0400 Subject: [PATCH 01/12] fix(#2345): resolve TARGET_BRANCH dynamically in code agent The code agent hardcodes TARGET_BRANCH to "main", which breaks repos whose default branch has a different name (e.g. redhat-3.17, master, develop). The post-code script uses this value for git merge-base and gh pr create --base, so PR creation fails silently on non-main repos. Add a "Resolve target branch" step that: 1. Parses --branch from the /fs-code comment (explicit override) 2. Falls back to the repo's default branch via the GitHub API 3. Falls back to "main" if the API call fails This follows the same pattern the fix agent already uses to resolve the PR base branch dynamically. Closes #2345 Signed-off-by: Marcus Kok --- .github/workflows/reusable-code.yml | 44 ++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reusable-code.yml b/.github/workflows/reusable-code.yml index 2eb72770b0..136a762399 100644 --- a/.github/workflows/reusable-code.yml +++ b/.github/workflows/reusable-code.yml @@ -129,6 +129,48 @@ jobs: fetch-depth: 0 persist-credentials: false + - name: Resolve target branch + id: resolve-branch + env: + EVENT_PAYLOAD: ${{ inputs.event_payload }} + SOURCE_REPO: ${{ inputs.source_repo }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + set -euo pipefail + + # 1. Check for explicit --branch override in comment + COMMENT_BODY="$(echo "${EVENT_PAYLOAD}" | jq -r '.comment.body // empty')" + EXPLICIT_BRANCH="" + if [[ -n "${COMMENT_BODY}" ]]; then + EXPLICIT_BRANCH="$(printf '%s\n' "${COMMENT_BODY}" \ + | head -1 | tr -d '\r' \ + | grep -oP '(?<=--branch\s)\S+' || true)" + fi + + # 2. Auto-detect repo default branch via API + DEFAULT_BRANCH="$(gh api "repos/${SOURCE_REPO}" \ + --jq '.default_branch' 2>/dev/null || echo '')" + + # 3. Fallback chain: explicit > repo default > "main" + if [[ -n "${EXPLICIT_BRANCH}" ]]; then + TARGET="${EXPLICIT_BRANCH}" + echo "Using explicit branch override: ${TARGET}" + elif [[ -n "${DEFAULT_BRANCH}" ]]; then + TARGET="${DEFAULT_BRANCH}" + echo "Using repo default branch: ${TARGET}" + else + TARGET="main" + echo "::warning::Could not detect default branch — falling back to 'main'" + fi + + # 4. Validate branch name + if [[ ! "${TARGET}" =~ ^[a-zA-Z0-9._/-]+$ ]]; then + echo "::error::Invalid branch name: '${TARGET}'" + exit 1 + fi + + echo "target_branch=${TARGET}" >> "${GITHUB_OUTPUT}" + - name: Validate inputs id: validate env: @@ -179,7 +221,7 @@ jobs: REPO_FULL_NAME: ${{ inputs.source_repo }} PUSH_TOKEN: ${{ steps.app-token.outputs.token }} PUSH_TOKEN_SOURCE: github-app - TARGET_BRANCH: main + TARGET_BRANCH: ${{ steps.resolve-branch.outputs.target_branch }} with: agent: code version: ${{ inputs.fullsend_version }} From 355afa804fdfd8b1718cee4a46a2157e5b5c4f71 Mon Sep 17 00:00:00 2001 From: Marcus Kok Date: Wed, 17 Jun 2026 09:38:40 -0400 Subject: [PATCH 02/12] docs(#2345): add ADR 0047 for agent-driven branch targeting Propose moving branch-targeting logic from the hardcoded TARGET_BRANCH workflow env to the agent (via structured output) with post-script policy enforcement. This supersedes the workflow-level fix in the previous commit. The agent determines the target branch from issue context, writes it to code-result.json, and the post-script validates it against CODE_ALLOWED_TARGET_BRANCHES before creating the PR. See ADR 0047 for the full decision record. Signed-off-by: Marcus Kok --- .github/workflows/reusable-code.yml | 44 +---- .../0047-agent-driven-branch-targeting.md | 171 ++++++++++++++++++ 2 files changed, 172 insertions(+), 43 deletions(-) create mode 100644 docs/ADRs/0047-agent-driven-branch-targeting.md diff --git a/.github/workflows/reusable-code.yml b/.github/workflows/reusable-code.yml index 136a762399..2eb72770b0 100644 --- a/.github/workflows/reusable-code.yml +++ b/.github/workflows/reusable-code.yml @@ -129,48 +129,6 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Resolve target branch - id: resolve-branch - env: - EVENT_PAYLOAD: ${{ inputs.event_payload }} - SOURCE_REPO: ${{ inputs.source_repo }} - GH_TOKEN: ${{ steps.app-token.outputs.token }} - run: | - set -euo pipefail - - # 1. Check for explicit --branch override in comment - COMMENT_BODY="$(echo "${EVENT_PAYLOAD}" | jq -r '.comment.body // empty')" - EXPLICIT_BRANCH="" - if [[ -n "${COMMENT_BODY}" ]]; then - EXPLICIT_BRANCH="$(printf '%s\n' "${COMMENT_BODY}" \ - | head -1 | tr -d '\r' \ - | grep -oP '(?<=--branch\s)\S+' || true)" - fi - - # 2. Auto-detect repo default branch via API - DEFAULT_BRANCH="$(gh api "repos/${SOURCE_REPO}" \ - --jq '.default_branch' 2>/dev/null || echo '')" - - # 3. Fallback chain: explicit > repo default > "main" - if [[ -n "${EXPLICIT_BRANCH}" ]]; then - TARGET="${EXPLICIT_BRANCH}" - echo "Using explicit branch override: ${TARGET}" - elif [[ -n "${DEFAULT_BRANCH}" ]]; then - TARGET="${DEFAULT_BRANCH}" - echo "Using repo default branch: ${TARGET}" - else - TARGET="main" - echo "::warning::Could not detect default branch — falling back to 'main'" - fi - - # 4. Validate branch name - if [[ ! "${TARGET}" =~ ^[a-zA-Z0-9._/-]+$ ]]; then - echo "::error::Invalid branch name: '${TARGET}'" - exit 1 - fi - - echo "target_branch=${TARGET}" >> "${GITHUB_OUTPUT}" - - name: Validate inputs id: validate env: @@ -221,7 +179,7 @@ jobs: REPO_FULL_NAME: ${{ inputs.source_repo }} PUSH_TOKEN: ${{ steps.app-token.outputs.token }} PUSH_TOKEN_SOURCE: github-app - TARGET_BRANCH: ${{ steps.resolve-branch.outputs.target_branch }} + TARGET_BRANCH: main with: agent: code version: ${{ inputs.fullsend_version }} diff --git a/docs/ADRs/0047-agent-driven-branch-targeting.md b/docs/ADRs/0047-agent-driven-branch-targeting.md new file mode 100644 index 0000000000..927871e4ef --- /dev/null +++ b/docs/ADRs/0047-agent-driven-branch-targeting.md @@ -0,0 +1,171 @@ +--- +title: "47. Agent-driven branch targeting for the code agent" +status: Proposed +relates_to: + - agent-architecture +topics: + - code-agent + - post-script + - branch-targeting + - structured-output +--- + +# 47. Agent-driven branch targeting for the code agent + +Date: 2026-06-17 + +## Status + +Proposed + +## Context + +The code agent's `reusable-code.yml` hardcodes `TARGET_BRANCH: main` in the +workflow step env. The post-script (`post-code.sh`) uses this value for +`git merge-base` and `gh pr create --base`. For any repository whose default +branch is not `main`, PR creation fails silently. + +The fix agent already handles this correctly by resolving the PR's base +branch dynamically from the existing PR metadata. The code agent has no +equivalent — it creates new PRs from issues, so there is no existing PR to +query. + +Beyond the immediate bug, the current design has two deeper problems: + +1. **The agent understands the target better than the workflow.** When an + issue says "set up builds on the 3.18 branch," the agent reads and + understands that context. Hardcoding the branch in an env var forces the + workflow to guess what the agent already knows. + +2. **Business logic in GitHub Actions workflows is not portable.** Fullsend + aims to run agents in environments beyond GitHub Actions (e.g., Kubernetes + pods). Branch-targeting logic embedded in workflow YAML becomes technical + debt that must be disentangled during that migration. + +The post-script is the right place for branch policy enforcement. It already +serves as the security boundary — it runs on the runner (not in the sandbox), +holds the write token, and performs secret scanning and pre-commit validation +before any push. Adding branch validation here follows the established +security model. + +## Options + +### Option A: Workflow-level auto-detection + +Add a step in `reusable-code.yml` that queries the GitHub API for the repo's +default branch and replaces the hardcoded `main`. Optionally parse +`--branch ` from the `/fs-code` comment. + +**Pros:** Smallest change (one file). Fixes the immediate bug. +**Cons:** Business logic in the workflow. Not portable. The agent still +cannot choose a branch different from the default without comment-level +syntax. + +### Option B: Agent-driven targeting with post-script policy gate (recommended) + +The agent writes its chosen `target_branch` to a structured output file +(`code-result.json`). The post-script reads the agent's choice, validates it +against a `CODE_ALLOWED_TARGET_BRANCHES` env var, and falls back to the +auto-detected repo default branch when no output is provided. + +**Pros:** Agent decides intent based on issue context. Post-script enforces +policy. No business logic in the workflow. Follows the existing structured +output pattern used by fix, triage, and review agents. Backward compatible. +**Cons:** Requires a new output schema for the code agent. + +### Option C: Git commit trailer convention + +The agent adds a `Target-Branch: ` trailer to its commit message. The +post-script parses it from `git log`. + +**Pros:** No new schema. +**Cons:** Fragile parsing. No validation tooling. Agent may forget the +trailer. Does not follow established structured output patterns. + +## Decision + +Adopt Option B: agent-driven targeting with post-script policy enforcement. + +### Code agent output schema + +Add `schemas/code-result.schema.json`: + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Code Agent Result", + "type": "object", + "required": ["target_branch"], + "additionalProperties": false, + "properties": { + "target_branch": { + "type": "string", + "description": "Branch the PR should target.", + "pattern": "^[a-zA-Z0-9._/-]+$" + } + } +} +``` + +The agent determines the target branch from the issue context and writes +`code-result.json` to `$FULLSEND_OUTPUT_DIR`. + +### Post-script policy gate + +Replace the `TARGET_BRANCH="${TARGET_BRANCH:-main}"` line in `post-code.sh` +with branch resolution logic: + +1. Read `target_branch` from `code-result.json` (agent's choice). +2. Auto-detect the repo's default branch via `gh api`. +3. If the agent specified a branch, validate it against + `CODE_ALLOWED_TARGET_BRANCHES` (comma-separated list, or `*` for any). + When unset, only the auto-detected default branch is allowed. +4. If the agent did not specify a branch, use the auto-detected default. +5. Fall back to `main` if the API call fails. + +### Harness changes + +Update `harness/code.yaml`: + +- Add `FULLSEND_OUTPUT_SCHEMA` and `FULLSEND_OUTPUT_FILE` to `runner_env` + (wiring up structured output for the code agent). +- Add `CODE_ALLOWED_TARGET_BRANCHES` to `runner_env` (policy enforcement). +- Remove `TARGET_BRANCH` from `runner_env` (replaced by post-script logic). + +### Workflow changes + +Remove `TARGET_BRANCH: main` from the "Run code agent" step env in +`reusable-code.yml`. No replacement env var is needed in the workflow — the +post-script handles all branch logic. Repos that want to restrict allowed +branches configure `CODE_ALLOWED_TARGET_BRANCHES` via their harness override +(`runner_env`), not in the workflow YAML. + +## Consequences + +**What becomes easier:** + +- Repos with non-`main` default branches work out of the box. No + configuration required — the post-script auto-detects the default branch. +- Agents can target the correct branch based on issue context (e.g., "set up + builds on redhat-3.18" results in a PR targeting `redhat-3.18`). +- Repos can restrict which branches agents may target by setting + `CODE_ALLOWED_TARGET_BRANCHES` in their harness override. +- Branch-targeting logic lives in the portable post-script, not in + GitHub-Actions-specific YAML. + +**What becomes harder or changes:** + +- The code agent now has a structured output contract. Agent definitions must + be updated to instruct the agent to write `code-result.json`. +- Repos that override `harness/code.yaml` via `.fullsend/customized/` must + update their override to include the new `runner_env` fields. +- The `TARGET_BRANCH` env var is removed. Any tooling that reads it directly + (outside the post-script) must be updated. + +**Backward compatibility:** + +- If the agent does not write `code-result.json` (e.g., older agent + definitions), the post-script falls back to the auto-detected default + branch. This is strictly better than the current `main` hardcode. +- If `CODE_ALLOWED_TARGET_BRANCHES` is not set, only the auto-detected + default branch is allowed. Safe by default. From 023b9cddffd1d4b92182e2da11988ee4576132b5 Mon Sep 17 00:00:00 2001 From: Marcus Kok Date: Wed, 17 Jun 2026 09:57:54 -0400 Subject: [PATCH 03/12] feat(#2345): wire up structured output and branch policy in code harness Signed-off-by: Marcus Kok --- internal/scaffold/fullsend-repo/harness/code.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/scaffold/fullsend-repo/harness/code.yaml b/internal/scaffold/fullsend-repo/harness/code.yaml index ac6bd9f16f..1c40b820be 100644 --- a/internal/scaffold/fullsend-repo/harness/code.yaml +++ b/internal/scaffold/fullsend-repo/harness/code.yaml @@ -42,7 +42,9 @@ plugins: # Environment variables available to pre/post scripts on the runner. # These are expanded from the runner environment and NEVER enter the sandbox. runner_env: - TARGET_BRANCH: "${TARGET_BRANCH}" + CODE_ALLOWED_TARGET_BRANCHES: "${CODE_ALLOWED_TARGET_BRANCHES}" + FULLSEND_OUTPUT_SCHEMA: ${FULLSEND_DIR}/schemas/code-result.schema.json + FULLSEND_OUTPUT_FILE: code-result.json timeout_minutes: 35 From 4e1e7a62219238b408be6d5dfc29dc82975ce595 Mon Sep 17 00:00:00 2001 From: Marcus Kok Date: Wed, 17 Jun 2026 10:02:49 -0400 Subject: [PATCH 04/12] feat(#2345): add code-result.schema.json for agent branch targeting Signed-off-by: Marcus Kok --- .../schemas/code-result.schema.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 internal/scaffold/fullsend-repo/schemas/code-result.schema.json diff --git a/internal/scaffold/fullsend-repo/schemas/code-result.schema.json b/internal/scaffold/fullsend-repo/schemas/code-result.schema.json new file mode 100644 index 0000000000..f4b5fbf268 --- /dev/null +++ b/internal/scaffold/fullsend-repo/schemas/code-result.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "code-result.schema.json", + "title": "Code Agent Result", + "description": "Structured output from the code agent documenting the target branch for PR creation.", + "type": "object", + "required": ["target_branch"], + "additionalProperties": false, + "properties": { + "target_branch": { + "type": "string", + "description": "Branch the PR should target. Determined by the agent from issue context.", + "pattern": "^[a-zA-Z0-9._/-]+$" + } + } +} From ed7980c5e55ed016c92a958e36b72e135b69a391 Mon Sep 17 00:00:00 2001 From: Marcus Kok Date: Wed, 17 Jun 2026 10:05:05 -0400 Subject: [PATCH 05/12] feat(#2345): add agent-driven branch resolution to post-code.sh Signed-off-by: Marcus Kok --- .../fullsend-repo/scripts/post-code.sh | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/internal/scaffold/fullsend-repo/scripts/post-code.sh b/internal/scaffold/fullsend-repo/scripts/post-code.sh index 87c9dcafc8..9970872a60 100755 --- a/internal/scaffold/fullsend-repo/scripts/post-code.sh +++ b/internal/scaffold/fullsend-repo/scripts/post-code.sh @@ -24,6 +24,10 @@ # # Optional environment variables: # PUSH_TOKEN_SOURCE — "github-app" (for logging; default: unknown) +# CODE_ALLOWED_TARGET_BRANCHES +# — comma-separated list of branches the agent may target, +# or "*" for any. When unset, only the repo's default +# branch is allowed. (default: auto-detected) # # Exit codes: # 0 — branch pushed and PR created, OR agent determined nothing to do @@ -45,6 +49,7 @@ UV_SHA256="f3b623eb0e6141a7053d571d59a0bdc341e0f238ea8f5f0b4815ddbec9a2a296" # Setup # --------------------------------------------------------------------------- REPO_DIR="${REPO_DIR:-repo}" +RUN_DIR="$(pwd)" if [ "${REPO_DIR}" != "." ]; then if [ ! -d "${REPO_DIR}" ]; then @@ -57,7 +62,41 @@ fi : "${PUSH_TOKEN:?PUSH_TOKEN is required}" : "${REPO_FULL_NAME:?REPO_FULL_NAME is required}" : "${ISSUE_NUMBER:?ISSUE_NUMBER is required}" -TARGET_BRANCH="${TARGET_BRANCH:-main}" +# --------------------------------------------------------------------------- +# Resolve target branch (ADR 0047) +# +# Priority: agent output > allowed-list validation > auto-detect default +# The agent writes its chosen branch to code-result.json. The post-script +# validates it against CODE_ALLOWED_TARGET_BRANCHES (comma-separated list +# or "*" for any). When unset, only the auto-detected default branch is +# allowed. Falls back to "main" if the API call fails. +# --------------------------------------------------------------------------- +AGENT_TARGET="" +RESULT_FILE="" +for dir in "${RUN_DIR}"/iteration-*/output; do + if [ -f "${dir}/code-result.json" ]; then + RESULT_FILE="${dir}/code-result.json" + fi +done +if [ -n "${RESULT_FILE}" ]; then + AGENT_TARGET="$(jq -r '.target_branch // empty' "${RESULT_FILE}" 2>/dev/null || true)" +fi + +DEFAULT_BRANCH="$(gh api "repos/${REPO_FULL_NAME}" --jq '.default_branch' 2>/dev/null || echo 'main')" + +if [ -n "${AGENT_TARGET}" ]; then + ALLOWED="${CODE_ALLOWED_TARGET_BRANCHES:-${DEFAULT_BRANCH}}" + if [ "${ALLOWED}" = "*" ] || echo ",${ALLOWED}," | grep -qF ",${AGENT_TARGET},"; then + TARGET_BRANCH="${AGENT_TARGET}" + echo "Agent requested branch '${TARGET_BRANCH}' — allowed" + else + echo "::error::Agent requested branch '${AGENT_TARGET}' but allowed branches are: ${ALLOWED}" + exit 1 + fi +else + TARGET_BRANCH="${DEFAULT_BRANCH}" + echo "No agent branch preference — using repo default: ${TARGET_BRANCH}" +fi echo "::add-mask::${PUSH_TOKEN}" From 70c1caff7813123df168e9b681f2c7af8dc7f2ff Mon Sep 17 00:00:00 2001 From: Marcus Kok Date: Wed, 17 Jun 2026 10:09:41 -0400 Subject: [PATCH 06/12] refactor(#2345): remove hardcoded TARGET_BRANCH from code workflow Signed-off-by: Marcus Kok --- .github/workflows/reusable-code.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/reusable-code.yml b/.github/workflows/reusable-code.yml index 2eb72770b0..a815379af0 100644 --- a/.github/workflows/reusable-code.yml +++ b/.github/workflows/reusable-code.yml @@ -179,7 +179,6 @@ jobs: REPO_FULL_NAME: ${{ inputs.source_repo }} PUSH_TOKEN: ${{ steps.app-token.outputs.token }} PUSH_TOKEN_SOURCE: github-app - TARGET_BRANCH: main with: agent: code version: ${{ inputs.fullsend_version }} From de85fea10f27861416039f80b6a01a5eb867adef Mon Sep 17 00:00:00 2001 From: Marcus Kok Date: Wed, 17 Jun 2026 10:13:31 -0400 Subject: [PATCH 07/12] docs(#2345): instruct code agent to write target branch to structured output Signed-off-by: Marcus Kok --- .../skills/code-implementation/SKILL.md | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/internal/scaffold/fullsend-repo/skills/code-implementation/SKILL.md b/internal/scaffold/fullsend-repo/skills/code-implementation/SKILL.md index dfc18fb5e4..cad0a766b9 100644 --- a/internal/scaffold/fullsend-repo/skills/code-implementation/SKILL.md +++ b/internal/scaffold/fullsend-repo/skills/code-implementation/SKILL.md @@ -178,13 +178,30 @@ From these files, determine: missing, but matching the repo's expected format directly is preferred. - **Branch conventions** — naming patterns, target branch -If a `TARGET_BRANCH` environment variable is set, use it. Otherwise, determine -the default branch: +Determine the correct target branch from the issue context. If the issue +references a specific branch (e.g., "set up builds on the 3.18 branch"), +use that branch. Otherwise, determine the repo's default branch: ```bash git rev-parse --abbrev-ref origin/HEAD | cut -d/ -f2 ``` +Write your chosen branch to the structured output file so the post-script +knows which branch to target the PR against: + +```bash +mkdir -p "${FULLSEND_OUTPUT_DIR}" +cat > "${FULLSEND_OUTPUT_DIR}/${FULLSEND_OUTPUT_FILE}" <" +} +RESULT +``` + +Write this output early (during planning, after determining the target +branch) so it is available even if the agent hits a timeout or error later. +The post-script validates this against the repo's allowed branches. + ### 4. Check for existing branch Before creating a new branch, check whether a branch already exists for this From 2a9c398163048dfd5b526a04b87f8467750451e0 Mon Sep 17 00:00:00 2001 From: Marcus Kok Date: Thu, 18 Jun 2026 09:59:52 -0400 Subject: [PATCH 08/12] fix(#2345): address PR review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Renumber ADR 0047 → 0049 (collision with existing ADR) - Change ADR status from Proposed to Accepted - Clarify fallback semantics (file-absent, not field-absent) - Fix gh api auth: use GH_TOKEN="${PUSH_TOKEN}" for private repos - Replace ::error:: with plain echo to prevent injection - Update scaffold and integration tests for new runner_env keys - Update running-agents-locally.md (TARGET_BRANCH → CODE_ALLOWED_TARGET_BRANCHES) - Add ADR 0049 to docs/architecture.md Decided section Signed-off-by: Marcus Kok --- ...geting.md => 0051-agent-driven-branch-targeting.md} | 10 +++++----- docs/architecture.md | 5 +++++ docs/guides/user/running-agents-locally.md | 2 +- internal/harness/scaffold_integration_test.go | 2 +- internal/scaffold/fullsend-repo/scripts/post-code.sh | 4 ++-- internal/scaffold/scaffold_test.go | 2 +- 6 files changed, 15 insertions(+), 10 deletions(-) rename docs/ADRs/{0047-agent-driven-branch-targeting.md => 0051-agent-driven-branch-targeting.md} (96%) diff --git a/docs/ADRs/0047-agent-driven-branch-targeting.md b/docs/ADRs/0051-agent-driven-branch-targeting.md similarity index 96% rename from docs/ADRs/0047-agent-driven-branch-targeting.md rename to docs/ADRs/0051-agent-driven-branch-targeting.md index 927871e4ef..4b0618a9a4 100644 --- a/docs/ADRs/0047-agent-driven-branch-targeting.md +++ b/docs/ADRs/0051-agent-driven-branch-targeting.md @@ -1,6 +1,6 @@ --- -title: "47. Agent-driven branch targeting for the code agent" -status: Proposed +title: "51. Agent-driven branch targeting for the code agent" +status: Accepted relates_to: - agent-architecture topics: @@ -10,13 +10,13 @@ topics: - structured-output --- -# 47. Agent-driven branch targeting for the code agent +# 51. Agent-driven branch targeting for the code agent Date: 2026-06-17 ## Status -Proposed +Accepted ## Context @@ -120,7 +120,7 @@ with branch resolution logic: 3. If the agent specified a branch, validate it against `CODE_ALLOWED_TARGET_BRANCHES` (comma-separated list, or `*` for any). When unset, only the auto-detected default branch is allowed. -4. If the agent did not specify a branch, use the auto-detected default. +4. If the agent did not write `code-result.json`, use the auto-detected default. 5. Fall back to `main` if the API call fails. ### Harness changes diff --git a/docs/architecture.md b/docs/architecture.md index 5549cf9799..b3067d264e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -96,6 +96,11 @@ The harness draws its configuration from the adopting organization's **`.fullsen mechanisms (`.env` files, `runner_env`). Each agent documents its config vars in `docs/agents/.md` ([ADR 0049](ADRs/0049-agent-configuration-env-var-convention.md)). +- Agent-driven branch targeting: the code agent writes its chosen target + branch to structured output. The post-script validates the choice against + an allowlist and falls back to the repo's auto-detected default branch. + Branch-targeting logic lives in the portable post-script, not in workflow + YAML ([ADR 0051](ADRs/0051-agent-driven-branch-targeting.md)). **Open questions:** diff --git a/docs/guides/user/running-agents-locally.md b/docs/guides/user/running-agents-locally.md index e8f1ec5575..98c384187e 100644 --- a/docs/guides/user/running-agents-locally.md +++ b/docs/guides/user/running-agents-locally.md @@ -172,7 +172,7 @@ PUSH_TOKEN_SOURCE=github-app GITHUB_ISSUE_URL=https://github.com/{org}/{repo}/issues/{issue_num} REPO_FULL_NAME={org}/{repo} ISSUE_NUMBER={issue_num} -TARGET_BRANCH=main +CODE_ALLOWED_TARGET_BRANCHES=main REPO_DIR=/tmp/repo-dir GITHUB_WORKSPACE=/tmp/ ``` diff --git a/internal/harness/scaffold_integration_test.go b/internal/harness/scaffold_integration_test.go index 519355f036..795b08df16 100644 --- a/internal/harness/scaffold_integration_test.go +++ b/internal/harness/scaffold_integration_test.go @@ -301,7 +301,7 @@ func TestResolveForge_ScaffoldRunnerEnvMerge(t *testing.T) { }, { file: "code.yaml", - topLevelKeys: []string{"TARGET_BRANCH"}, + topLevelKeys: []string{"CODE_ALLOWED_TARGET_BRANCHES", "FULLSEND_OUTPUT_SCHEMA", "FULLSEND_OUTPUT_FILE"}, forgeGithubKeys: []string{"PUSH_TOKEN", "PUSH_TOKEN_SOURCE", "REPO_FULL_NAME", "ISSUE_NUMBER", "REPO_DIR"}, }, { diff --git a/internal/scaffold/fullsend-repo/scripts/post-code.sh b/internal/scaffold/fullsend-repo/scripts/post-code.sh index 9970872a60..a4b4ad7796 100755 --- a/internal/scaffold/fullsend-repo/scripts/post-code.sh +++ b/internal/scaffold/fullsend-repo/scripts/post-code.sh @@ -82,7 +82,7 @@ if [ -n "${RESULT_FILE}" ]; then AGENT_TARGET="$(jq -r '.target_branch // empty' "${RESULT_FILE}" 2>/dev/null || true)" fi -DEFAULT_BRANCH="$(gh api "repos/${REPO_FULL_NAME}" --jq '.default_branch' 2>/dev/null || echo 'main')" +DEFAULT_BRANCH="$(GH_TOKEN="${PUSH_TOKEN}" gh api "repos/${REPO_FULL_NAME}" --jq '.default_branch' 2>/dev/null || echo 'main')" if [ -n "${AGENT_TARGET}" ]; then ALLOWED="${CODE_ALLOWED_TARGET_BRANCHES:-${DEFAULT_BRANCH}}" @@ -90,7 +90,7 @@ if [ -n "${AGENT_TARGET}" ]; then TARGET_BRANCH="${AGENT_TARGET}" echo "Agent requested branch '${TARGET_BRANCH}' — allowed" else - echo "::error::Agent requested branch '${AGENT_TARGET}' but allowed branches are: ${ALLOWED}" + echo "Error: agent requested branch '${AGENT_TARGET}' but allowed branches are: ${ALLOWED}" exit 1 fi else diff --git a/internal/scaffold/scaffold_test.go b/internal/scaffold/scaffold_test.go index 0ca8f6c0df..95ab7fbd96 100644 --- a/internal/scaffold/scaffold_test.go +++ b/internal/scaffold/scaffold_test.go @@ -650,7 +650,7 @@ func TestHarnessForgeRunnerEnvMerge(t *testing.T) { }, { file: "code.yaml", - topLevelKeys: []string{"TARGET_BRANCH"}, + topLevelKeys: []string{"CODE_ALLOWED_TARGET_BRANCHES", "FULLSEND_OUTPUT_SCHEMA", "FULLSEND_OUTPUT_FILE"}, forgeGithubKeys: []string{"PUSH_TOKEN", "PUSH_TOKEN_SOURCE", "REPO_FULL_NAME", "ISSUE_NUMBER", "REPO_DIR"}, }, { From 32f7efdf6ded46f50c401c5018e4f5b492ea0f57 Mon Sep 17 00:00:00 2001 From: Marcus Kok Date: Mon, 22 Jun 2026 10:28:10 -0400 Subject: [PATCH 09/12] fix(#2345): address second round of review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add CODE_ALLOWED_TARGET_BRANCHES: '' to workflow env so harness validation passes (ValidateRunnerEnvWith requires all ${VAR} refs to be defined in the host environment) - Fix ADR reference in post-code.sh comment (0047 → 0051) - Add branch name regex validation after reading agent output (defense-in-depth: schema regex not enforced at runtime without validation_loop) - Add migration note to ADR 0051 with concrete before/after for repos with customized harness/code.yaml Signed-off-by: Marcus Kok --- .github/workflows/reusable-code.yml | 1 + docs/ADRs/0051-agent-driven-branch-targeting.md | 11 ++++++++++- internal/scaffold/fullsend-repo/scripts/post-code.sh | 6 +++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/reusable-code.yml b/.github/workflows/reusable-code.yml index a815379af0..c9c30841e8 100644 --- a/.github/workflows/reusable-code.yml +++ b/.github/workflows/reusable-code.yml @@ -179,6 +179,7 @@ jobs: REPO_FULL_NAME: ${{ inputs.source_repo }} PUSH_TOKEN: ${{ steps.app-token.outputs.token }} PUSH_TOKEN_SOURCE: github-app + CODE_ALLOWED_TARGET_BRANCHES: '' with: agent: code version: ${{ inputs.fullsend_version }} diff --git a/docs/ADRs/0051-agent-driven-branch-targeting.md b/docs/ADRs/0051-agent-driven-branch-targeting.md index 4b0618a9a4..ca847265e0 100644 --- a/docs/ADRs/0051-agent-driven-branch-targeting.md +++ b/docs/ADRs/0051-agent-driven-branch-targeting.md @@ -158,7 +158,16 @@ branches configure `CODE_ALLOWED_TARGET_BRANCHES` via their harness override - The code agent now has a structured output contract. Agent definitions must be updated to instruct the agent to write `code-result.json`. - Repos that override `harness/code.yaml` via `.fullsend/customized/` must - update their override to include the new `runner_env` fields. + update their override to include the new `runner_env` fields. Specifically, + replace `TARGET_BRANCH: "${TARGET_BRANCH}"` with the new keys: + ```yaml + runner_env: + CODE_ALLOWED_TARGET_BRANCHES: "${CODE_ALLOWED_TARGET_BRANCHES}" + FULLSEND_OUTPUT_SCHEMA: ${FULLSEND_DIR}/schemas/code-result.schema.json + FULLSEND_OUTPUT_FILE: code-result.json + ``` + Customized harnesses that still reference `${TARGET_BRANCH}` will fail + harness validation because the workflow no longer provides the variable. - The `TARGET_BRANCH` env var is removed. Any tooling that reads it directly (outside the post-script) must be updated. diff --git a/internal/scaffold/fullsend-repo/scripts/post-code.sh b/internal/scaffold/fullsend-repo/scripts/post-code.sh index a4b4ad7796..4c6a3ad710 100755 --- a/internal/scaffold/fullsend-repo/scripts/post-code.sh +++ b/internal/scaffold/fullsend-repo/scripts/post-code.sh @@ -63,7 +63,7 @@ fi : "${REPO_FULL_NAME:?REPO_FULL_NAME is required}" : "${ISSUE_NUMBER:?ISSUE_NUMBER is required}" # --------------------------------------------------------------------------- -# Resolve target branch (ADR 0047) +# Resolve target branch (ADR 0051) # # Priority: agent output > allowed-list validation > auto-detect default # The agent writes its chosen branch to code-result.json. The post-script @@ -81,6 +81,10 @@ done if [ -n "${RESULT_FILE}" ]; then AGENT_TARGET="$(jq -r '.target_branch // empty' "${RESULT_FILE}" 2>/dev/null || true)" fi +if [[ -n "${AGENT_TARGET}" && ! "${AGENT_TARGET}" =~ ^[a-zA-Z0-9._/-]+$ ]]; then + echo "Error: invalid branch name from agent output: '${AGENT_TARGET}'" + exit 1 +fi DEFAULT_BRANCH="$(GH_TOKEN="${PUSH_TOKEN}" gh api "repos/${REPO_FULL_NAME}" --jq '.default_branch' 2>/dev/null || echo 'main')" From 60b3ab8a3db12461301dcbe38b640c9e8a326308 Mon Sep 17 00:00:00 2001 From: Marcus Kok Date: Mon, 22 Jun 2026 15:30:00 -0400 Subject: [PATCH 10/12] =?UTF-8?q?fix(#2345):=20renumber=20ADR=200051=20?= =?UTF-8?q?=E2=86=92=200052=20(collision=20in=20merge=20queue)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcus Kok --- ...nch-targeting.md => 0052-agent-driven-branch-targeting.md} | 4 ++-- docs/architecture.md | 2 +- internal/scaffold/fullsend-repo/scripts/post-code.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename docs/ADRs/{0051-agent-driven-branch-targeting.md => 0052-agent-driven-branch-targeting.md} (98%) diff --git a/docs/ADRs/0051-agent-driven-branch-targeting.md b/docs/ADRs/0052-agent-driven-branch-targeting.md similarity index 98% rename from docs/ADRs/0051-agent-driven-branch-targeting.md rename to docs/ADRs/0052-agent-driven-branch-targeting.md index ca847265e0..3ea0ca03e0 100644 --- a/docs/ADRs/0051-agent-driven-branch-targeting.md +++ b/docs/ADRs/0052-agent-driven-branch-targeting.md @@ -1,5 +1,5 @@ --- -title: "51. Agent-driven branch targeting for the code agent" +title: "52. Agent-driven branch targeting for the code agent" status: Accepted relates_to: - agent-architecture @@ -10,7 +10,7 @@ topics: - structured-output --- -# 51. Agent-driven branch targeting for the code agent +# 52. Agent-driven branch targeting for the code agent Date: 2026-06-17 diff --git a/docs/architecture.md b/docs/architecture.md index b3067d264e..ee8516b7f9 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -100,7 +100,7 @@ The harness draws its configuration from the adopting organization's **`.fullsen branch to structured output. The post-script validates the choice against an allowlist and falls back to the repo's auto-detected default branch. Branch-targeting logic lives in the portable post-script, not in workflow - YAML ([ADR 0051](ADRs/0051-agent-driven-branch-targeting.md)). + YAML ([ADR 0052](ADRs/0052-agent-driven-branch-targeting.md)). **Open questions:** diff --git a/internal/scaffold/fullsend-repo/scripts/post-code.sh b/internal/scaffold/fullsend-repo/scripts/post-code.sh index 4c6a3ad710..0fcbaa5740 100755 --- a/internal/scaffold/fullsend-repo/scripts/post-code.sh +++ b/internal/scaffold/fullsend-repo/scripts/post-code.sh @@ -63,7 +63,7 @@ fi : "${REPO_FULL_NAME:?REPO_FULL_NAME is required}" : "${ISSUE_NUMBER:?ISSUE_NUMBER is required}" # --------------------------------------------------------------------------- -# Resolve target branch (ADR 0051) +# Resolve target branch (ADR 0052) # # Priority: agent output > allowed-list validation > auto-detect default # The agent writes its chosen branch to code-result.json. The post-script From e6ecc125ad2857150399f954367c5e9bfcd4edc7 Mon Sep 17 00:00:00 2001 From: Marcus Kok Date: Mon, 22 Jun 2026 15:38:48 -0400 Subject: [PATCH 11/12] =?UTF-8?q?fix(#2345):=20renumber=20ADR=200052=20?= =?UTF-8?q?=E2=86=92=200053=20(0051-0052=20merged=20upstream)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcus Kok --- ...nch-targeting.md => 0053-agent-driven-branch-targeting.md} | 4 ++-- docs/architecture.md | 2 +- internal/scaffold/fullsend-repo/scripts/post-code.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename docs/ADRs/{0052-agent-driven-branch-targeting.md => 0053-agent-driven-branch-targeting.md} (98%) diff --git a/docs/ADRs/0052-agent-driven-branch-targeting.md b/docs/ADRs/0053-agent-driven-branch-targeting.md similarity index 98% rename from docs/ADRs/0052-agent-driven-branch-targeting.md rename to docs/ADRs/0053-agent-driven-branch-targeting.md index 3ea0ca03e0..206355c1d7 100644 --- a/docs/ADRs/0052-agent-driven-branch-targeting.md +++ b/docs/ADRs/0053-agent-driven-branch-targeting.md @@ -1,5 +1,5 @@ --- -title: "52. Agent-driven branch targeting for the code agent" +title: "53. Agent-driven branch targeting for the code agent" status: Accepted relates_to: - agent-architecture @@ -10,7 +10,7 @@ topics: - structured-output --- -# 52. Agent-driven branch targeting for the code agent +# 53. Agent-driven branch targeting for the code agent Date: 2026-06-17 diff --git a/docs/architecture.md b/docs/architecture.md index ee8516b7f9..bc1148c1b3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -100,7 +100,7 @@ The harness draws its configuration from the adopting organization's **`.fullsen branch to structured output. The post-script validates the choice against an allowlist and falls back to the repo's auto-detected default branch. Branch-targeting logic lives in the portable post-script, not in workflow - YAML ([ADR 0052](ADRs/0052-agent-driven-branch-targeting.md)). + YAML ([ADR 0053](ADRs/0053-agent-driven-branch-targeting.md)). **Open questions:** diff --git a/internal/scaffold/fullsend-repo/scripts/post-code.sh b/internal/scaffold/fullsend-repo/scripts/post-code.sh index 0fcbaa5740..09de04297f 100755 --- a/internal/scaffold/fullsend-repo/scripts/post-code.sh +++ b/internal/scaffold/fullsend-repo/scripts/post-code.sh @@ -63,7 +63,7 @@ fi : "${REPO_FULL_NAME:?REPO_FULL_NAME is required}" : "${ISSUE_NUMBER:?ISSUE_NUMBER is required}" # --------------------------------------------------------------------------- -# Resolve target branch (ADR 0052) +# Resolve target branch (ADR 0053) # # Priority: agent output > allowed-list validation > auto-detect default # The agent writes its chosen branch to code-result.json. The post-script From 22fe71f4718e1bebe468bee0f2b588d2593f515d Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Mon, 22 Jun 2026 16:30:32 -0400 Subject: [PATCH 12/12] ci: retrigger checks after merge queue dequeue Assisted-by: Claude claude-opus-4-6 Signed-off-by: Ralph Bean