Skip to content

fix(#847): preserve child pre_script during base forge composition - #866

Open
fullsend-ai-coder[bot] wants to merge 2 commits into
mainfrom
agent/847-triage-pre-script-forge
Open

fix(#847): preserve child pre_script during base forge composition#866
fullsend-ai-coder[bot] wants to merge 2 commits into
mainfrom
agent/847-triage-pre-script-forge

Conversation

@fullsend-ai-coder

Copy link
Copy Markdown

Summary

  • Fix base-composition bug where a child harness's explicit pre_script was silently overwritten by the base's forge.<platform>.pre_script during ResolveForge
  • After mergeForgeBlocks, clear forge-level pre_script/post_script entries inherited from the base when the child explicitly declared the corresponding top-level field
  • Preserve child-authored forge-level scripts (only base-inherited values are cleared)
  • Add triage-specific runAgent tests verifying pre-script abort on exit 1 and proceed on exit 0

Related Issue

Closes #847

Changes

  • internal/harness/compose.go: Added clearInheritedForgeScripts, snapshotChildForgeScripts, and forgeScriptSnapshot to detect and clear base-inherited forge-level scripts that would shadow the child's explicit top-level pre_script/post_script
  • internal/harness/compose_test.go: Four new tests covering merge behavior and end-to-end LoadWithBase scenarios
  • internal/cli/prescript_run_test.go: Two new runAgent tests for triage agent pre-script execution; refactored newSkipHarnessDir to support arbitrary agent names via newSkipHarnessDirForAgent
  • docs/ADRs/0045-forge-portable-harness-schema.md: Documented the inherited forge-level scripts exception in the merge rules

Root cause

mergeBaseIntoChild correctly preserves the child's top-level pre_script during scalar merge. However, it also merges the base's forge section via mergeForgeBlocks. When the child has no forge section, it inherits the base's forge configs entirely — including forge.github.pre_script. Later, ResolveForge unconditionally overwrites the top-level pre_script with the non-empty forge-level value, silently dropping the child's explicit declaration.

Testing

  • TestMergeBaseIntoChild_ChildPreScriptSurvivesBaseForge — child top-level pre_script survives when base has forge.github.pre_script
  • TestMergeBaseIntoChild_ChildForgePreScriptPreserved — child-authored forge-level pre_script is preserved (not cleared)
  • TestLoadWithBase_ChildPreScriptSurvivesBaseForgeResolution — end-to-end: child pre_script survives full LoadWithBase + ResolveForge pipeline
  • TestLoadWithBase_BaseForgePreScriptInheritedWhenChildOmits — existing behavior unchanged: base forge pre_script inherited when child omits
  • TestRunAgent_TriagePreScriptExitsNonZero — triage agent aborts on pre-script exit 1
  • TestRunAgent_TriagePreScriptExitsZero_ProceedsToSandbox — triage agent proceeds past pre-script on exit 0
  • All existing harness and CLI tests pass

Checklist

  • PR title follows Conventional Commits (correct type, ! for breaking changes)
  • Tests added/updated for new or modified logic

Note: pre-commit hooks could not run (network unavailable in sandbox).
Manual verification of pre-commit is required.


Closes #847

Post-script verification

  • Branch is not main/master (agent/847-triage-pre-script-forge)
  • Secret scan passed (gitleaks — c887fc47c59d8b87b5282ca44044d2933d30df41..HEAD)
  • PR body secret scan passed (gitleaks — no-git)
  • Pre-commit hooks passed (authoritative run on runner)
  • Tests ran inside sandbox

During base composition, mergeForgeBlocks inherits the base's
forge.github.pre_script into the child. ResolveForge then
unconditionally overwrites the child's top-level pre_script with
the non-empty forge-level value, silently dropping the child's
explicit declaration. This caused triage harnesses with custom
pre_scripts to ignore their pre-script entirely.

Fix: after mergeForgeBlocks, snapshot which forge-level scripts
the child authored itself and clear any base-inherited forge-level
pre_script/post_script entries when the child explicitly declared
the corresponding top-level field. Child-authored forge-level
scripts are preserved.

Tests added:
- mergeBaseIntoChild: child pre_script survives base forge
- mergeBaseIntoChild: child forge pre_script is preserved
- LoadWithBase: end-to-end with forge resolution
- LoadWithBase: base forge pre_script inherited when child omits
- runAgent: triage pre-script exits non-zero aborts
- runAgent: triage pre-script exits zero proceeds to sandbox

Closes #847
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 2, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 9:44 AM UTC · Completed 10:00 AM UTC
Commit: cab62da · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 2, 2026

Copy link
Copy Markdown

Looks good to me

Previous run

Looks good to me

Previous run (2)

Review

Findings

Medium

  • [stale-doc] docs/guides/user/bring-your-own-agent.md:299 — The "Field merge rules (for base and forge)" table describes scalar fields (including pre_script) as "Child wins if non-empty" and does not mention the new exception introduced by this PR: inherited forge-level pre_script/post_script from a base harness are cleared when they would shadow the child's own top-level scripts. The ADR was updated but the user-facing guide was not.
    Remediation: Add a footnote or row to the merge rules table noting the exception for inherited forge-level scripts.

Low

  • [shared-pointer-mutation] internal/harness/compose.go — In clearInheritedForgeScripts, when a platform key was not present in the child's original forge map, mergeForgeBlocks assigns child[key] = baseFC (a direct pointer copy). Subsequent fc.PreScript = "" mutates the base's ForgeConfig struct through the shared pointer. Currently safe because all callers discard the base after mergeBaseIntoChild returns, but this is a latent correctness hazard if a future caller reuses the base struct.
  • [declaration-ordering] internal/harness/compose.go — The forgeScriptSnapshot type is declared after clearInheritedForgeScripts which uses it. The codebase convention (e.g., Dependency, ComposeOpts) is to declare types before the functions that reference them.
  • [naming-consistency] internal/harness/compose.go — Parameter name childForgeScripts in clearInheritedForgeScripts differs from the local variable childForgeSnap at the call site in mergeBaseIntoChild.
  • [comment-style] internal/harness/compose.go — The doc comment on clearInheritedForgeScripts is ~17 lines, substantially longer than comparable package-internal helpers in the same file (2–5 lines). The motivation section duplicates information already in the ADR update and the inline comment in mergeBaseIntoChild.

Labels: PR fixes a bug in harness base composition (pre_script/post_script override), matching the type/bug label

@fullsend-ai-review fullsend-ai-review Bot added requires-manual-review Review requires human judgment type/bug Confirmed defect in existing behavior labels Aug 2, 2026
@guyoron1

guyoron1 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

/fs-review

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 2, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:31 AM UTC · Completed 10:45 AM UTC
Commit: cab62da · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added ready-for-merge All reviewers approved — ready to merge and removed requires-manual-review Review requires human judgment labels Aug 2, 2026
@guyoron1

guyoron1 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

/fs-fix

@fullsend-ai-coder

fullsend-ai-coder Bot commented Aug 2, 2026

Copy link
Copy Markdown
Author

🤖 Finished Fix · ✅ Success · Started 10:51 AM UTC · Completed 11:00 AM UTC
Commit: cab62da · View workflow run →

- Add footnote to user guide merge rules table documenting the inherited
  forge-level scripts exception (#847)
- Clone ForgeConfig before clearing inherited scripts to avoid mutating
  the base struct through a shared pointer from mergeForgeBlocks
- Move forgeScriptSnapshot type before clearInheritedForgeScripts to
  follow codebase declaration ordering convention
- Rename clearInheritedForgeScripts parameter to childForgeSnap for
  consistency with the call site
- Trim clearInheritedForgeScripts doc comment to match package-internal
  helper style (4 lines vs 17)

Addresses review feedback on #866
@fullsend-ai-coder

Copy link
Copy Markdown
Author

🔧 Fix agent — iteration 1 (human-triggered)

Addressed all 5 review findings (1 medium, 4 low). Updated user guide merge table with footnote for forge script exception. Fixed shared-pointer mutation by cloning ForgeConfig before clearing inherited fields. Reordered type declaration, aligned parameter naming, and trimmed doc comment to match codebase conventions.

Fixed (5):

  1. stale-doc: merge rules table missing forge script exception (docs/guides/user/bring-your-own-agent.md): Added footnote to the scalars row in the merge rules table documenting that inherited forge-level pre_script/post_script are cleared when the child explicitly declares the corresponding top-level field
  2. shared-pointer-mutation: clearInheritedForgeScripts mutates base ForgeConfig (internal/harness/compose.go): Clone ForgeConfig before clearing inherited script fields to avoid mutating the base struct through the shared pointer assigned by mergeForgeBlocks
  3. declaration-ordering: forgeScriptSnapshot type declared after function that uses it (internal/harness/compose.go): Moved forgeScriptSnapshot type declaration before clearInheritedForgeScripts to follow codebase convention (types before functions that reference them)
  4. naming-consistency: parameter name differs from call-site variable (internal/harness/compose.go): Renamed clearInheritedForgeScripts parameter from childForgeScripts to childForgeSnap to match the call-site variable name
  5. comment-style: doc comment on clearInheritedForgeScripts too long (internal/harness/compose.go): Trimmed the doc comment from ~17 lines to 4 lines, removing the motivation section that duplicated the ADR and inline comment

Tests: passed

Next steps:

  • /fs-review — request a re-review of the changes
  • /fs-fix <your instruction> — run another fix pass with specific guidance
  • Push commits directly — review re-runs automatically on push
    Updated by fullsend fix agent

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 2, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 11:01 AM UTC · Completed 11:10 AM UTC
Commit: 5c5875d · View workflow run →

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

This pull request has been automatically marked as stale because it has not had any activity in the last month. It will be closed in 2 weeks if no further activity occurs. Remove the stale label to reset the inactivity timer.

@github-actions github-actions Bot added the stale label Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-merge All reviewers approved — ready to merge stale type/bug Confirmed defect in existing behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Triage runner does not execute pre_script from harness configuration

1 participant