From 3483fbc399c9d213358569f5e35e25dae250d3a7 Mon Sep 17 00:00:00 2001 From: Greg Allen Date: Fri, 12 Jun 2026 15:44:38 -0400 Subject: [PATCH] feat(harness): move GitHub-specific fields into forge.github blocks (ADR-0045 Phase 2 PR 2) Restructure all scaffold harness templates so pre_script, post_script, and GitHub-specific runner_env keys live inside forge.github: blocks. Platform-neutral runner_env keys remain at the top level. Pre/post scripts are kept at the top level as defaults for local dev without --forge, and duplicated in forge.github so forge resolution produces the same result. Templates produce identical runtime behavior when loaded with --forge github via the Phase 1 ResolveForge merge logic. Signed-off-by: Claude Opus 4.6 Signed-off-by: Greg Allen --- .../scaffold/fullsend-repo/harness/code.yaml | 24 +++-- .../scaffold/fullsend-repo/harness/fix.yaml | 19 ++-- .../fullsend-repo/harness/prioritize.yaml | 17 +-- .../scaffold/fullsend-repo/harness/retro.yaml | 15 ++- .../fullsend-repo/harness/review.yaml | 21 ++-- .../fullsend-repo/harness/triage.yaml | 13 ++- internal/scaffold/scaffold_test.go | 102 ++++++++++++++++-- 7 files changed, 166 insertions(+), 45 deletions(-) diff --git a/internal/scaffold/fullsend-repo/harness/code.yaml b/internal/scaffold/fullsend-repo/harness/code.yaml index 70c035930..ac6bd9f16 100644 --- a/internal/scaffold/fullsend-repo/harness/code.yaml +++ b/internal/scaffold/fullsend-repo/harness/code.yaml @@ -17,9 +17,6 @@ policy: policies/code.yaml role: coder slug: fullsend-ai-coder -pre_script: scripts/pre-code.sh -post_script: scripts/post-code.sh - host_files: - src: env/gcp-vertex.env dest: /sandbox/workspace/.env.d/gcp-vertex.env @@ -33,20 +30,29 @@ host_files: dest: /sandbox/workspace/.gcp-oidc-token optional: true +pre_script: scripts/pre-code.sh +post_script: scripts/post-code.sh + skills: - skills/code-implementation plugins: - plugins/gopls-lsp -# Environment variables available to post_script on the runner. +# Environment variables available to pre/post scripts on the runner. # These are expanded from the runner environment and NEVER enter the sandbox. runner_env: - PUSH_TOKEN: "${PUSH_TOKEN}" - PUSH_TOKEN_SOURCE: "${PUSH_TOKEN_SOURCE}" - REPO_FULL_NAME: "${REPO_FULL_NAME}" - ISSUE_NUMBER: "${ISSUE_NUMBER}" - REPO_DIR: "${GITHUB_WORKSPACE}/target-repo" TARGET_BRANCH: "${TARGET_BRANCH}" timeout_minutes: 35 + +forge: + github: + pre_script: scripts/pre-code.sh + post_script: scripts/post-code.sh + runner_env: + PUSH_TOKEN: "${PUSH_TOKEN}" + PUSH_TOKEN_SOURCE: "${PUSH_TOKEN_SOURCE}" + REPO_FULL_NAME: "${REPO_FULL_NAME}" + ISSUE_NUMBER: "${ISSUE_NUMBER}" + REPO_DIR: "${GITHUB_WORKSPACE}/target-repo" diff --git a/internal/scaffold/fullsend-repo/harness/fix.yaml b/internal/scaffold/fullsend-repo/harness/fix.yaml index 238845275..0561abeeb 100644 --- a/internal/scaffold/fullsend-repo/harness/fix.yaml +++ b/internal/scaffold/fullsend-repo/harness/fix.yaml @@ -18,13 +18,12 @@ role: coder slug: fullsend-ai-coder pre_script: scripts/pre-fix.sh +post_script: scripts/post-fix.sh validation_loop: script: scripts/validate-output-schema.sh max_iterations: 2 -post_script: scripts/post-fix.sh - host_files: - src: env/gcp-vertex.env dest: /sandbox/workspace/.env.d/gcp-vertex.env @@ -44,11 +43,6 @@ skills: - skills/fix-review runner_env: - PUSH_TOKEN: "${PUSH_TOKEN}" - PUSH_TOKEN_SOURCE: "${PUSH_TOKEN_SOURCE}" - REPO_FULL_NAME: "${REPO_FULL_NAME}" - PR_NUMBER: "${PR_NUMBER}" - REPO_DIR: "${GITHUB_WORKSPACE}/target-repo" TARGET_BRANCH: "${TARGET_BRANCH}" TRIGGER_SOURCE: "${TRIGGER_SOURCE}" HUMAN_INSTRUCTION: "${HUMAN_INSTRUCTION}" @@ -59,3 +53,14 @@ runner_env: FULLSEND_OUTPUT_FILE: fix-result.json timeout_minutes: 25 + +forge: + github: + pre_script: scripts/pre-fix.sh + post_script: scripts/post-fix.sh + runner_env: + PUSH_TOKEN: "${PUSH_TOKEN}" + PUSH_TOKEN_SOURCE: "${PUSH_TOKEN_SOURCE}" + REPO_FULL_NAME: "${REPO_FULL_NAME}" + PR_NUMBER: "${PR_NUMBER}" + REPO_DIR: "${GITHUB_WORKSPACE}/target-repo" diff --git a/internal/scaffold/fullsend-repo/harness/prioritize.yaml b/internal/scaffold/fullsend-repo/harness/prioritize.yaml index 9bba26d7b..73b7d9dca 100644 --- a/internal/scaffold/fullsend-repo/harness/prioritize.yaml +++ b/internal/scaffold/fullsend-repo/harness/prioritize.yaml @@ -22,18 +22,23 @@ host_files: expand: true pre_script: scripts/pre-prioritize.sh +post_script: scripts/post-prioritize.sh validation_loop: script: scripts/validate-output-schema.sh max_iterations: 2 -post_script: scripts/post-prioritize.sh - runner_env: - GITHUB_ISSUE_URL: ${GITHUB_ISSUE_URL} - GH_TOKEN: ${GH_TOKEN} - ORG: ${ORG} - PROJECT_NUMBER: ${PROJECT_NUMBER} FULLSEND_OUTPUT_SCHEMA: ${FULLSEND_DIR}/schemas/prioritize-result.schema.json timeout_minutes: 10 + +forge: + github: + pre_script: scripts/pre-prioritize.sh + post_script: scripts/post-prioritize.sh + runner_env: + GITHUB_ISSUE_URL: ${GITHUB_ISSUE_URL} + GH_TOKEN: ${GH_TOKEN} + ORG: ${ORG} + PROJECT_NUMBER: ${PROJECT_NUMBER} diff --git a/internal/scaffold/fullsend-repo/harness/retro.yaml b/internal/scaffold/fullsend-repo/harness/retro.yaml index 2b039a8be..e2bcfdd9e 100644 --- a/internal/scaffold/fullsend-repo/harness/retro.yaml +++ b/internal/scaffold/fullsend-repo/harness/retro.yaml @@ -27,17 +27,22 @@ skills: - skills/agent-scaffolding pre_script: scripts/pre-retro.sh +post_script: scripts/post-retro.sh validation_loop: script: scripts/validate-output-schema.sh max_iterations: 2 -post_script: scripts/post-retro.sh - runner_env: - ORIGINATING_URL: "${ORIGINATING_URL}" - REPO_FULL_NAME: "${REPO_FULL_NAME}" - GH_TOKEN: "${GH_TOKEN}" FULLSEND_OUTPUT_SCHEMA: ${FULLSEND_DIR}/schemas/retro-result.schema.json timeout_minutes: 30 + +forge: + github: + pre_script: scripts/pre-retro.sh + post_script: scripts/post-retro.sh + runner_env: + ORIGINATING_URL: "${ORIGINATING_URL}" + REPO_FULL_NAME: "${REPO_FULL_NAME}" + GH_TOKEN: "${GH_TOKEN}" diff --git a/internal/scaffold/fullsend-repo/harness/review.yaml b/internal/scaffold/fullsend-repo/harness/review.yaml index 248f5cf62..ebfce5a73 100644 --- a/internal/scaffold/fullsend-repo/harness/review.yaml +++ b/internal/scaffold/fullsend-repo/harness/review.yaml @@ -13,8 +13,6 @@ skills: - skills/code-review - skills/docs-review -pre_script: scripts/pre-review.sh - host_files: - src: env/gcp-vertex.env dest: /sandbox/workspace/.env.d/gcp-vertex.env @@ -31,17 +29,24 @@ host_files: dest: /sandbox/workspace/prior-review.txt optional: true +pre_script: scripts/pre-review.sh +post_script: scripts/post-review.sh + validation_loop: script: scripts/validate-output-schema.sh max_iterations: 2 -post_script: scripts/post-review.sh - runner_env: - REVIEW_TOKEN: "${REVIEW_TOKEN}" - REPO_FULL_NAME: "${REPO_FULL_NAME}" - PR_NUMBER: "${PR_NUMBER}" - GITHUB_PR_URL: "${GITHUB_PR_URL}" FULLSEND_OUTPUT_SCHEMA: ${FULLSEND_DIR}/schemas/review-result.schema.json timeout_minutes: 20 + +forge: + github: + pre_script: scripts/pre-review.sh + post_script: scripts/post-review.sh + runner_env: + REVIEW_TOKEN: "${REVIEW_TOKEN}" + REPO_FULL_NAME: "${REPO_FULL_NAME}" + PR_NUMBER: "${PR_NUMBER}" + GITHUB_PR_URL: "${GITHUB_PR_URL}" diff --git a/internal/scaffold/fullsend-repo/harness/triage.yaml b/internal/scaffold/fullsend-repo/harness/triage.yaml index f77ce8561..284d7d5f3 100644 --- a/internal/scaffold/fullsend-repo/harness/triage.yaml +++ b/internal/scaffold/fullsend-repo/harness/triage.yaml @@ -25,16 +25,21 @@ skills: - skills/issue-labels pre_script: scripts/pre-triage.sh +post_script: scripts/post-triage.sh validation_loop: script: scripts/validate-output-schema.sh max_iterations: 2 -post_script: scripts/post-triage.sh - runner_env: - GITHUB_ISSUE_URL: ${GITHUB_ISSUE_URL} - GH_TOKEN: ${GH_TOKEN} FULLSEND_OUTPUT_SCHEMA: ${FULLSEND_DIR}/schemas/triage-result.schema.json timeout_minutes: 10 + +forge: + github: + pre_script: scripts/pre-triage.sh + post_script: scripts/post-triage.sh + runner_env: + GITHUB_ISSUE_URL: ${GITHUB_ISSUE_URL} + GH_TOKEN: ${GH_TOKEN} diff --git a/internal/scaffold/scaffold_test.go b/internal/scaffold/scaffold_test.go index e93eb1bbd..90e6cf599 100644 --- a/internal/scaffold/scaffold_test.go +++ b/internal/scaffold/scaffold_test.go @@ -582,20 +582,110 @@ func TestHarnessesLoadAndValidate(t *testing.T) { } t.Run(e.Name(), func(t *testing.T) { harnessPath := filepath.Join(dir, "harness", e.Name()) - h, err := harness.Load(harnessPath) - require.NoError(t, err, "Load should succeed") - err = h.ResolveRelativeTo(dir) - require.NoError(t, err, "ResolveRelativeTo should succeed") + t.Run("Load", func(t *testing.T) { + h, loadErr := harness.Load(harnessPath) + require.NoError(t, loadErr, "Load should succeed") - err = h.ValidateFilesExist() - require.NoError(t, err, "ValidateFilesExist should succeed") + // Top-level pre/post scripts serve as defaults even + // without forge resolution (local dev without --forge). + assert.NotEmpty(t, h.PreScript, "PreScript should be set at top level as default") + assert.NotEmpty(t, h.PostScript, "PostScript should be set at top level as default") + assert.NotNil(t, h.Forge, "Forge map should be present") + assert.Contains(t, h.Forge, "github", "Forge should have a github key") + + resolveErr := h.ResolveRelativeTo(dir) + require.NoError(t, resolveErr, "ResolveRelativeTo should succeed") + + existErr := h.ValidateFilesExist() + require.NoError(t, existErr, "ValidateFilesExist should succeed") + }) + + t.Run("LoadWithOpts_github", func(t *testing.T) { + h, loadErr := harness.LoadWithOpts(harnessPath, harness.LoadOpts{ForgePlatform: "github"}) + require.NoError(t, loadErr, "LoadWithOpts should succeed") + + assert.Nil(t, h.Forge, "Forge should be nil after resolution") + assert.NotEmpty(t, h.PreScript, "PreScript should be set after forge resolution") + assert.NotEmpty(t, h.PostScript, "PostScript should be set after forge resolution") + assert.NotEmpty(t, h.RunnerEnv, "RunnerEnv should be non-empty after merge") + + resolveErr := h.ResolveRelativeTo(dir) + require.NoError(t, resolveErr, "ResolveRelativeTo should succeed") + + existErr := h.ValidateFilesExist() + require.NoError(t, existErr, "ValidateFilesExist should succeed") + }) }) loaded++ } assert.True(t, loaded >= 2, "expected at least 2 harnesses, got %d", loaded) } +func TestHarnessForgeRunnerEnvMerge(t *testing.T) { + dir := t.TempDir() + err := WalkFullsendRepoAll(func(path string, content []byte) error { + dest := filepath.Join(dir, path) + if mkErr := os.MkdirAll(filepath.Dir(dest), 0o755); mkErr != nil { + return mkErr + } + return os.WriteFile(dest, content, 0o644) + }) + require.NoError(t, err, "extracting scaffold") + + tests := []struct { + file string + topLevelKeys []string + forgeGithubKeys []string + }{ + { + file: "triage.yaml", + topLevelKeys: []string{"FULLSEND_OUTPUT_SCHEMA"}, + forgeGithubKeys: []string{"GITHUB_ISSUE_URL", "GH_TOKEN"}, + }, + { + file: "code.yaml", + topLevelKeys: []string{"TARGET_BRANCH"}, + forgeGithubKeys: []string{"PUSH_TOKEN", "PUSH_TOKEN_SOURCE", "REPO_FULL_NAME", "ISSUE_NUMBER", "REPO_DIR"}, + }, + { + file: "review.yaml", + topLevelKeys: []string{"FULLSEND_OUTPUT_SCHEMA"}, + forgeGithubKeys: []string{"REVIEW_TOKEN", "REPO_FULL_NAME", "PR_NUMBER", "GITHUB_PR_URL"}, + }, + { + file: "fix.yaml", + topLevelKeys: []string{"TARGET_BRANCH", "TRIGGER_SOURCE", "HUMAN_INSTRUCTION", "FIX_ITERATION", "REVIEW_BODY_FILE", "PRE_AGENT_HEAD", "FULLSEND_OUTPUT_SCHEMA", "FULLSEND_OUTPUT_FILE"}, + forgeGithubKeys: []string{"PUSH_TOKEN", "PUSH_TOKEN_SOURCE", "REPO_FULL_NAME", "PR_NUMBER", "REPO_DIR"}, + }, + { + file: "retro.yaml", + topLevelKeys: []string{"FULLSEND_OUTPUT_SCHEMA"}, + forgeGithubKeys: []string{"ORIGINATING_URL", "REPO_FULL_NAME", "GH_TOKEN"}, + }, + { + file: "prioritize.yaml", + topLevelKeys: []string{"FULLSEND_OUTPUT_SCHEMA"}, + forgeGithubKeys: []string{"GITHUB_ISSUE_URL", "GH_TOKEN", "ORG", "PROJECT_NUMBER"}, + }, + } + + for _, tt := range tests { + t.Run(tt.file, func(t *testing.T) { + harnessPath := filepath.Join(dir, "harness", tt.file) + h, loadErr := harness.LoadWithOpts(harnessPath, harness.LoadOpts{ForgePlatform: "github"}) + require.NoError(t, loadErr) + + for _, key := range tt.topLevelKeys { + assert.Contains(t, h.RunnerEnv, key, "merged RunnerEnv should contain top-level key %s", key) + } + for _, key := range tt.forgeGithubKeys { + assert.Contains(t, h.RunnerEnv, key, "merged RunnerEnv should contain forge.github key %s", key) + } + }) + } +} + func TestRepoMaintenanceWorkflowContent(t *testing.T) { content, err := FullsendRepoFile(".github/workflows/repo-maintenance.yml") require.NoError(t, err)