fix: enforce build verification gate + retry empty OpenRouter responses#3
Open
DeSennd wants to merge 6 commits into
Open
fix: enforce build verification gate + retry empty OpenRouter responses#3DeSennd wants to merge 6 commits into
DeSennd wants to merge 6 commits into
Conversation
Five fixes for compiled-language projects (C++/CMake):
1. config.py: Add VerifyConfig ([verify] section) with build_command,
test_command, timeout_s, max_fail_lines. Wired into ForgeGodConfig.
2. loop.py: Add _run_verify_command helper + build/test gate in
_finalize_story_result. Runs build_command as subprocess before
marking story DONE; non-zero exit routes story back to TODO with
compiler output in error_log. FileNotFoundError skips gate.
3. agent.py: Track bash exit codes via [exit code: N] regex parse.
New _last_verification_exit_code state field. Block completion when
last verification command failed (non-zero exit). Reset on new writes.
4. agent.py: Add C++ build tools (cmake, make, ninja, ctest, g++,
clang++, gcc, clang, meson, nasm) to VERIFICATION_COMMAND_MARKERS.
5. router.py: Rewrite _call_openrouter with retry loop (3 retries,
4s delay) for empty/null content responses. Normalize
msg.get('content') or '' to fix NoneType crash at agent.py:373.
Handles: no choices, null content, empty string, tool-call-only.
- agent.py: Skip auto-research when task prompt contains 'Previous attempt errors' — indicates a retry where research already ran on the first attempt. Saves ~30s of wasted research per retry. - loop.py: Bump error_log truncation in story prompt from 500 to 2000 chars so the agent sees actual compiler errors, not just build progress. - loop.py: Extract error lines (error:, fatal, undefined reference, etc.) from build gate output instead of raw tail. The 2000-char budget now contains the useful errors, not '[ 3%] Built target...' noise.
Previous change skipped research on every retry. Now only skips when
the last error_log entry is a build/test gate failure ('Build failed'
or 'Test failed'). Other failures (reviewer rejection, timeout, crash)
still trigger research — a wrong approach may benefit from fresh
domain research.
Loop now tags the error section header differently:
- Build/test failure: 'Build/Test failure — fix compiler errors'
- Other failure: 'Previous attempt errors (FIX THESE)'
Agent checks for 'Build/Test failure' to decide whether to skip.
The smoke test runs during the verify gate BEFORE the story is marked done, so the phase check couldn't see the last story as completed. Moved the phase completion logic into loop.py's _finalize_story_result after the story status is set to DONE and the PRD is saved. This runs after commit, checks if all stories in the phase are done, generates the XLSX checklist, and drops the killswitch to stop the loop for manual testing.
When the model returns empty responses repeatedly (no content, no tool calls), the context gets poisoned with repeated closure-gate blocker messages, making the model less likely to respond on subsequent turns. After 4 consecutive empty responses, abort the story so it gets a fresh agent with clean context on the next retry attempt. Also added a _consecutive_empty_responses counter to Agent state, reset to 0 whenever a non-empty response is received.
The 6000-char limit was too small for multi-file changes. The reviewer was seeing truncated diffs and rejecting valid code because it couldn't see the full implementation across split files.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
ForgeGod has no enforced build/compile gate for compiled languages (C++/CMake, Rust, Go, etc.):
Closure gate checks execution, not success —
agent.pytracks verification commands by substring-matching the command string, but never checks the exit code. A failingcmake --build(exit 2) satisfies the gate the same as a passing one.No build gate in story finalization —
loop.py_finalize_story_resultruns lint (Python/TS only) → reviewer (LLM text, no subprocess) → taste →status = DONE. Nothing ever invokes a compiler.C++ tools invisible to closure gate —
VERIFICATION_COMMAND_MARKERShad nocmake,make,ctest,g++,clang++. Build commands were unrecognized.OpenRouter null content crash —
msg.get("content", "")returnsNonewhen JSON has"content": null(key exists with null value). This propagates toagent.py:373whereresponse_text.strip()crashes withAttributeError: 'NoneType' object has no attribute 'strip'. No retry — the router treats null as success.Fix
config.py: AddVerifyConfig([verify]section) withbuild_command,test_command,timeout_s,max_fail_lines.loop.py: Add_run_verify_commandhelper + build/test gate in_finalize_story_result. Runs build_command as subprocess before marking DONE; non-zero exit routes story back to TODO with compiler output inerror_log.agent.py: Track bash exit codes via[exit code: N]regex. Block completion when last verification command failed. Reset on new writes.agent.py: Add C++ build tools toVERIFICATION_COMMAND_MARKERS.router.py: Rewrite_call_openrouterwith retry loop (3 retries, 4s delay) for empty/null content. Normalizemsg.get("content") or ""to fix NoneType crash.Evidence
Observed on a 65-story C++/CMake project: 16 stories marked
donewith zero successful builds after P0-01. Everycmake --buildreturned exit 2 incommands.logbut stories still closed as done. The NoneType crash burned 3+ story retry slots across P0-01, P0-04, P2-02.Backward compatibility
VerifyConfigdefaults tobuild_command = ""(empty) — gate is skipped when no command configured, so Python/JS projects are unaffected.FileNotFoundError(toolchain missing) skips the gate rather than blocking.