From 41cfd85bd9a4c4cf7ee670a1e9642ffccd67a66e Mon Sep 17 00:00:00 2001 From: "SURFACEBOOKPRO9\\dan" Date: Thu, 26 Feb 2026 13:44:43 -0800 Subject: [PATCH 1/2] engine/codergen: fallback home discovery for codex seeding --- .../engine/codergen_cli_invocation_test.go | 45 ++++++++++++++++++ internal/attractor/engine/codergen_router.go | 47 +++++++++++++++++-- 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/internal/attractor/engine/codergen_cli_invocation_test.go b/internal/attractor/engine/codergen_cli_invocation_test.go index 8a9bed1a..9fc15207 100644 --- a/internal/attractor/engine/codergen_cli_invocation_test.go +++ b/internal/attractor/engine/codergen_cli_invocation_test.go @@ -187,6 +187,51 @@ func TestBuildCodexIsolatedEnv_ConfiguresCodexScopedOverrides(t *testing.T) { } } +func TestBuildCodexIsolatedEnv_SeedsFromUserProfileWhenHomeUnset(t *testing.T) { + home := t.TempDir() + if err := os.MkdirAll(filepath.Join(home, ".codex"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(home, ".codex", "auth.json"), []byte(`{"token":"x"}`), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(home, ".codex", "config.toml"), []byte(`model = "gpt-5"`), 0o644); err != nil { + t.Fatal(err) + } + + t.Setenv("HOME", "") + t.Setenv("USERPROFILE", home) + t.Setenv("HOMEDRIVE", "") + t.Setenv("HOMEPATH", "") + t.Setenv("KILROY_CODEX_STATE_BASE", filepath.Join(t.TempDir(), "codex-state-base")) + + stageDir := t.TempDir() + _, meta, err := buildCodexIsolatedEnv(stageDir, os.Environ()) + if err != nil { + t.Fatalf("buildCodexIsolatedEnv: %v", err) + } + + stateRoot := strings.TrimSpace(anyToString(meta["state_root"])) + assertExists(t, filepath.Join(stateRoot, "auth.json")) + assertExists(t, filepath.Join(stateRoot, "config.toml")) +} + +func TestCodexStateBaseRoot_FallsBackToUserProfileWhenHomeUnset(t *testing.T) { + userProfile := t.TempDir() + t.Setenv("KILROY_CODEX_STATE_BASE", "") + t.Setenv("XDG_STATE_HOME", "") + t.Setenv("HOME", "") + t.Setenv("USERPROFILE", userProfile) + t.Setenv("HOMEDRIVE", "") + t.Setenv("HOMEPATH", "") + + got := codexStateBaseRoot() + want := filepath.Join(userProfile, ".local", "state", "kilroy", "attractor", "codex-state") + if got != want { + t.Fatalf("codexStateBaseRoot: got %q want %q", got, want) + } +} + func TestEnvHasKey(t *testing.T) { env := []string{"HOME=/tmp", "PATH=/usr/bin", "CARGO_TARGET_DIR=/foo/bar"} if !envHasKey(env, "CARGO_TARGET_DIR") { diff --git a/internal/attractor/engine/codergen_router.go b/internal/attractor/engine/codergen_router.go index 95ac2595..08007839 100644 --- a/internal/attractor/engine/codergen_router.go +++ b/internal/attractor/engine/codergen_router.go @@ -1436,9 +1436,8 @@ func buildCodexIsolatedEnvWithName(stageDir string, homeDirName string, baseEnv seeded := []string{} seedErrors := []string{} // Seed codex config from the ORIGINAL home (before isolation). - // Use os.Getenv("HOME") since baseEnv may already have HOME pinned - // to the original value by buildBaseNodeEnv. - srcHome := strings.TrimSpace(os.Getenv("HOME")) + // Prefer HOME, then Windows home vars, then os.UserHomeDir(). + srcHome := codexSourceHome(baseEnv) if srcHome != "" { for _, name := range []string{"auth.json", "config.toml"} { src := filepath.Join(srcHome, ".codex", name) @@ -1498,7 +1497,7 @@ func codexStateBaseRoot() string { } base := strings.TrimSpace(os.Getenv("XDG_STATE_HOME")) if base == "" { - home := strings.TrimSpace(os.Getenv("HOME")) + home := codexSourceHome(nil) if home == "" { base = "." } else { @@ -1512,6 +1511,46 @@ func codexStateBaseRoot() string { return root } +func codexSourceHome(baseEnv []string) string { + candidates := []string{ + envSliceValue(baseEnv, "HOME"), + os.Getenv("HOME"), + envSliceValue(baseEnv, "USERPROFILE"), + os.Getenv("USERPROFILE"), + windowsHomeFromParts(envSliceValue(baseEnv, "HOMEDRIVE"), envSliceValue(baseEnv, "HOMEPATH")), + windowsHomeFromParts(os.Getenv("HOMEDRIVE"), os.Getenv("HOMEPATH")), + } + for _, candidate := range candidates { + if home := strings.TrimSpace(candidate); home != "" { + return home + } + } + home, err := os.UserHomeDir() + if err != nil { + return "" + } + return strings.TrimSpace(home) +} + +func envSliceValue(env []string, key string) string { + prefix := key + "=" + for _, entry := range env { + if strings.HasPrefix(entry, prefix) { + return strings.TrimSpace(strings.TrimPrefix(entry, prefix)) + } + } + return "" +} + +func windowsHomeFromParts(homeDrive string, homePath string) string { + drive := strings.TrimSpace(homeDrive) + path := strings.TrimSpace(homePath) + if drive == "" || path == "" { + return "" + } + return filepath.Clean(drive + path) +} + func copyIfExists(src string, dst string) (bool, error) { info, err := os.Stat(src) if err != nil { From 90e75fb8ee1b055ab092bfee7a3b5aa06b487473 Mon Sep 17 00:00:00 2001 From: "SURFACEBOOKPRO9\\dan" Date: Thu, 26 Feb 2026 23:25:00 -0800 Subject: [PATCH 2/2] engine: harden tool shell resolution and rogue-fast status contract --- demo/rogue/rogue-fast-changes.md | 46 +++++ demo/rogue/rogue-fast.dot | 189 ++++++++++++++++++ internal/attractor/engine/handlers.go | 70 ++++++- .../engine/tool_shell_resolution_test.go | 55 +++++ 4 files changed, 357 insertions(+), 3 deletions(-) create mode 100644 demo/rogue/rogue-fast-changes.md create mode 100644 demo/rogue/rogue-fast.dot create mode 100644 internal/attractor/engine/tool_shell_resolution_test.go diff --git a/demo/rogue/rogue-fast-changes.md b/demo/rogue/rogue-fast-changes.md new file mode 100644 index 00000000..7b0fdb78 --- /dev/null +++ b/demo/rogue/rogue-fast-changes.md @@ -0,0 +1,46 @@ +# Rogue Fast Change Log + +## 2026-02-26 + +- Switched `check_toolchain` routing to explicit success/fail edges and added `toolchain_fail` hard-stop so failed prerequisites abort immediately instead of continuing or reporting false success. +- Removed the ambiguous `check_dod` branch split and forced linear DoD/planning flow: `check_dod -> consolidate_dod -> debate_consolidate`. +- Updated `check_dod` to an audit-only step (no `has_dod/needs_dod` branching contract) so the prompt matches linear fast-path routing. +- Added `prepare_ai_inputs` tool stage to deterministically scaffold missing `.ai` artifacts (`spec`, `definition_of_done`, `plan_final`, and baseline review/log files) before `implement`. +- Added `fix_fmt` auto-format stage before `verify_fmt` to avoid postmortem cycles from trivial formatting-only failures. +- Enabled `auto_status=true` on codergen stages that were repeatedly failing with `missing status.json (auto_status=false)` (`check_dod`, `consolidate_dod`, `debate_consolidate`, `implement`, `verify_fidelity`, `review_consensus`, `postmortem`). +- Simplified implement/verify flow by removing diamond check nodes that depended on status files and routing directly with condition gates between tool stages. +- Kept postmortem recovery but changed non-transient failures to re-enter planning (`debate_consolidate`) instead of hard-exiting the run. +- Removed graph-level `retry_target`/`fallback_retry_target` so stage failures do not silently jump to unrelated nodes (for example, `toolchain_fail -> implement`). +- Minimal rollback: removed `toolchain_fail` hard-stop routing and restored `check_toolchain -> expand_spec` unconditional flow so runs do not die at startup when host toolchain preconditions are missing. +- Minimal stabilization pass: restored explicit toolchain gate routing (`check_toolchain -> expand_spec` on success, fail to `postmortem`), added unconditional fallback edges for conditional-only routing nodes, and removed explicit `$KILROY_STAGE_STATUS_PATH` write instructions from `auto_status=true` codergen prompts. +- Toolchain hardening pass: made `check_toolchain` install-method agnostic by removing `cargo install --list` dependency for `wasm-bindgen-cli`, adding explicit `rustup` presence check, and prepending both `$HOME/.cargo/bin` and `$USERPROFILE/.cargo/bin` to `PATH` in all Rust/WASM tool stages (`check_toolchain`, `fix_fmt`, `verify_fmt`, `verify_build`, `verify_test`) so Windows runs can find user-local Rust tools without shell profile assumptions. + +### Run Ops Log (same day) + +- Reproduced/fixed codex prompt-probe auth seeding bug in engine (PR #43); prompt probe now passes with seeded `auth.json`/`config.toml`. +- Launched detached real run `rogue-fast-20260226T214843Z`; preflight passed; run failed early due missing CXDB service (`127.0.0.1:9010` unreachable). +- Relaunched with `--no-cxdb` as approved: run `rogue-fast-20260226T215515Z`; failed at `check_toolchain` (`FAIL: cargo not found`) and entered `postmortem`. +- Installed Rustup (`Rustlang.Rustup` 1.28.2), added `wasm32-unknown-unknown` target; `cargo install wasm-pack wasm-bindgen-cli` failed because MSVC linker (`link.exe`) is not present. +- Installed prebuilt `wasm-pack` binary (`v0.14.0`) to `C:\Users\dan\.cargo\bin\wasm-pack.exe` and verified `wasm-pack --version`. +- Relaunched with current dotfile + `--no-cxdb`: run `rogue-fast-20260226T231941Z`; run is currently blocked in `check_toolchain` with `bash` process behavior indicating Windows shell-resolution issues (system `bash.exe`/WSL pathing), not codex auth/CXDB. + +### Dependency-order fix (same day) + +- Root cause identified: `debate_consolidate` required `.ai/spec.md` and `.ai/definition_of_done.md` before `prepare_ai_inputs` created them. +- Fixed flow ordering in `demo/rogue/rogue-fast.dot`: + - `consolidate_dod -> prepare_ai_inputs -> debate_consolidate -> implement` + - removed the incorrect `debate_consolidate -> prepare_ai_inputs` dependency inversion. +- Relaunch attempt `rogue-fast-20260226T235930Z` failed immediately due config mismatch (`missing llm.providers.openai.backend` from `demo/rogue/run.yaml` in this environment). +- Relaunched with prior known-good real/no-CXDB config: `rogue-fast-20260227T002621Z`. +- Polled every 5 minutes (9 polls from `2026-02-26 16:27` to `17:07` PT): + - Run progressed through `expand_spec`, `check_dod`, `consolidate_dod`, `prepare_ai_inputs`, `debate_consolidate`, and `implement`. + - This confirms the ordering fix executed as intended (the run no longer fails at `debate_consolidate` due missing `.ai` scaffolding). + - Run ultimately failed later at `fix_fmt` with deterministic cycle breaker: `/usr/bin/bash: line 1: cd: demo/rogue/rogue-wasm: No such file or directory`. + +### Root-cause correction (same day) + +- Root cause isolated: `implement` was set to `auto_status=true`, so the engine marked success when the stage finished without writing status, even when the model output explicitly reported `outcome: fail`. +- Corrected `implement` status contract in `demo/rogue/rogue-fast.dot`: + - set `auto_status=false` + - restored explicit instruction to write status to `$KILROY_STAGE_STATUS_PATH` with `outcome=success|fail` and failure fields. +- This prevents false-positive routing into `fix_fmt` after a failed implement stage. diff --git a/demo/rogue/rogue-fast.dot b/demo/rogue/rogue-fast.dot new file mode 100644 index 00000000..ff7dd993 --- /dev/null +++ b/demo/rogue/rogue-fast.dot @@ -0,0 +1,189 @@ +digraph rogue_pipeline { + graph [ + goal="Port Rogue 5.4.4 from C to Rust/WebAssembly: exact 1:1 mechanical translation of all game systems (dungeon generation, monster AI, combat math, item tables, RNG formula), playable in browser at demo/rogue/rogue-wasm/www/index.html with classic 80x24 ASCII terminal rendering. Original C source at demo/rogue/original-rogue/ (~16,800 lines, 33 files). Replace ncurses I/O with WASM bridge to JS terminal renderer; save/load uses localStorage.", + rankdir=LR, + default_max_retry=3, + provenance_version="1", + model_stylesheet=" + * { llm_model: gpt-5.3-codex-spark; llm_provider: openai; } + .hard { llm_model: gpt-5.3-codex-spark; llm_provider: openai; } + .verify { llm_model: gpt-5.3-codex-spark; llm_provider: openai; } + " + ] + + exit [shape=Msquare, label="Exit"] + + subgraph cluster_bootstrap { + label="Bootstrap" + start [shape=Mdiamond, label="Start"] + + check_toolchain [ + shape=parallelogram, + max_retries=0, + tool_command="set -e; for d in \"$HOME/.cargo/bin\" \"$USERPROFILE/.cargo/bin\"; do [ -d \"$d\" ] && PATH=\"$d:$PATH\"; done; export PATH; command -v cargo >/dev/null 2>&1 || { echo 'FAIL: cargo not found' >&2; exit 1; }; command -v rustup >/dev/null 2>&1 || { echo 'FAIL: rustup not found' >&2; exit 1; }; command -v wasm-pack >/dev/null 2>&1 || { echo 'FAIL: wasm-pack not found' >&2; exit 1; }; rustup target list --installed | grep -qx wasm32-unknown-unknown || { echo 'FAIL: wasm32-unknown-unknown target not installed' >&2; exit 1; }; echo 'All toolchain checks passed: cargo, rustup, wasm-pack, wasm32-unknown-unknown target'" + ] + + expand_spec [ + shape=box, + auto_status=true, + prompt="Goal: $goal\n\nExpand the requirements into a detailed spec for the Rogue 5.4.4 C-to-Rust/WASM port.\n\nRead the original C source listing at demo/rogue/original-rogue/ to understand every game system:\n- Dungeon generation (rooms, corridors, mazes, doors, stairs)\n- Monster definitions, AI, chase logic, special abilities (A-Z)\n- Combat math (attack/defense, hit tables, damage dice)\n- Item tables (potions, scrolls, rings, weapons, armor, food, wands, amulet)\n- RNG formula and seed handling\n- Player stats, experience, leveling, hunger\n- Save/load serialization format\n\nRead any existing Rust source at demo/rogue/rogue-wasm/src/ to understand what has already been ported.\n\nThe spec must cover:\n1. Module decomposition mapping each C file to a Rust module.\n2. Data structure translations (C structs -> Rust structs/enums).\n3. ncurses replacement strategy (WASM bridge to JS canvas/DOM terminal renderer).\n4. localStorage save/load replacing filesystem I/O.\n5. Build pipeline: wasm-pack build -> www/index.html integration.\n6. Fidelity contract: same algorithms, same constants, same behavior.\n\nWrite the spec to .ai/spec.md." + ] + + check_dod [ + shape=box, + auto_status=true, + label="DoD exists?", + prompt="Goal: $goal\n\nAudit whether .ai/definition_of_done.md exists and is credible, then summarize findings in .ai/check_dod_notes.md.\n\nDoD rubric (must be specific, verifiable, and not over-prescriptive):\n- Scope: defines what is in-scope and what is out-of-scope.\n- Deliverables: names concrete outputs/outcomes (artifacts, behaviors, docs), not implementation steps.\n- Acceptance criteria: includes observable pass/fail criteria a reviewer can verify.\n- Verification: includes how to verify (commands or steps) appropriate for a Rust/WASM project.\n- Quality/safety: includes project-appropriate quality expectations (build/tests/lint) stated as outcomes/evidence.\n- Non-goals/deferrals: explicitly calls out what is intentionally not being done in this iteration." + ] + } + + subgraph cluster_dod { + label="Definition Of Done" + + consolidate_dod [ + shape=box, + auto_status=true, + prompt="Goal: $goal\n\nPropose a project Definition of Done (DoD) for the Rogue C-to-Rust/WASM port. Read .ai/spec.md.\n\nRequirements:\n- DoD must be a checklist of outcomes/evidence, not a plan.\n- Each item must be verifiable (someone can check it and say pass/fail).\n- Avoid prescribing the implementation approach unless the spec explicitly requires it.\n- Include scope, deliverables, acceptance criteria, verification approach, and explicit non-goals/deferrals.\n- Verification must include: cargo build --target wasm32-unknown-unknown succeeds, cargo test passes, wasm-pack build --target web succeeds, index.html loads and renders 80x24 grid.\n- Fidelity criteria: dungeon generation, monster behavior, combat math, item effects must match original C.\n\nWrite the final DoD to .ai/definition_of_done.md." + ] + } + + subgraph cluster_planning { + label="Planning" + + debate_consolidate [ + shape=box, + auto_status=true, + prompt="Goal: $goal\n\nCreate a final implementation plan for the Rogue C-to-Rust/WASM port.\nRead .ai/spec.md, .ai/definition_of_done.md.\nIf .ai/postmortem_latest.md exists, incorporate its lessons.\n\nThe plan must address:\n1. Module-by-module porting order (dependency-aware).\n2. Data structure translations with Rust idioms.\n3. ncurses -> WASM bridge architecture.\n4. localStorage save/load strategy.\n5. Progressive compilation milestones.\n6. www/index.html integration with wasm-pack output.\n\nRead existing Rust source at demo/rogue/rogue-wasm/src/ to build on what exists.\nRead original C source at demo/rogue/original-rogue/ for reference.\nResolve conflicts. Ensure dependency order.\nIf .ai/postmortem_latest.md exists, verify the plan addresses every issue.\nWrite to .ai/plan_final.md." + ] + + prepare_ai_inputs [ + shape=parallelogram, + max_retries=0, + tool_command="set -e; mkdir -p .ai; [ -f .ai/spec.md ] || { [ -f demo/rogue/spec.md ] && cp demo/rogue/spec.md .ai/spec.md || true; }; [ -f .ai/definition_of_done.md ] || { [ -f demo/rogue/DoD.md ] && cp demo/rogue/DoD.md .ai/definition_of_done.md || true; }; if [ ! -f .ai/plan_final.md ]; then printf '%s\n' '# Rogue fast fallback plan' '' '- Preserve existing work; do not restart from scratch.' '- Implement highest-impact missing pieces first.' '- Run fmt, build, and tests and capture exact failures.' '- Update .ai/implementation_log.md and .ai/verify_fidelity.md with evidence.' > .ai/plan_final.md; fi; [ -f .ai/implementation_log.md ] || printf '%s\n' '# Implementation Log' '' '- Initialized by prepare_ai_inputs stage.' > .ai/implementation_log.md; [ -f .ai/review_consensus.md ] || printf '%s\n' '# Review Consensus' '' '- Initialized by prepare_ai_inputs stage.' > .ai/review_consensus.md; [ -f .ai/verify_fidelity.md ] || printf '%s\n' '# Fidelity Review' '' '- Initialized by prepare_ai_inputs stage.' > .ai/verify_fidelity.md; echo 'Prepared .ai inputs for implement stage'" + ] + } + + subgraph cluster_implement_verify { + label="Implement And Verify" + + implement [ + shape=box, + class="hard", + auto_status=false, + max_retries=2, + prompt="Goal: $goal\n\nExecute .ai/plan_final.md. Read .ai/definition_of_done.md for acceptance criteria.\nIf .ai/postmortem_latest.md exists, prioritize fixing those issues.\n\nOriginal C source: demo/rogue/original-rogue/\nRust target: demo/rogue/rogue-wasm/\nHTML deliverable: demo/rogue/rogue-wasm/www/index.html\n\nThis is an exact mechanical port. Every C function must have a Rust equivalent with identical behavior:\n- Same dungeon generation algorithms (rooms, corridors, mazes).\n- Same monster stats table, AI chase logic, special abilities.\n- Same combat math (hit tables, damage dice, armor class).\n- Same item tables (potions, scrolls, rings, weapons, armor, food, wands, amulet of Yendor).\n- Same RNG formula and seed handling.\n- Same player progression (XP, leveling, hunger).\n\nReplace ncurses with a WASM bridge to a JS terminal renderer.\nReplace filesystem save/load with localStorage.\n\nUse progressive compilation: get each module compiling before starting the next.\nLog progress to .ai/implementation_log.md.\n\nWrite status JSON to $KILROY_STAGE_STATUS_PATH.\noutcome=success if build passes, outcome=fail with failure_reason, failure_class, and failure_signature otherwise." + ] + + fix_fmt [ + shape=parallelogram, + tool_command="set -e; for d in \"$HOME/.cargo/bin\" \"$USERPROFILE/.cargo/bin\"; do [ -d \"$d\" ] && PATH=\"$d:$PATH\"; done; export PATH; cd demo/rogue/rogue-wasm && cargo fmt 2>&1" + ] + + verify_fmt [ + shape=parallelogram, + tool_command="set -e; for d in \"$HOME/.cargo/bin\" \"$USERPROFILE/.cargo/bin\"; do [ -d \"$d\" ] && PATH=\"$d:$PATH\"; done; export PATH; cd demo/rogue/rogue-wasm && cargo fmt --check 2>&1" + ] + + verify_build [ + shape=parallelogram, + tool_command="set -e; for d in \"$HOME/.cargo/bin\" \"$USERPROFILE/.cargo/bin\"; do [ -d \"$d\" ] && PATH=\"$d:$PATH\"; done; export PATH; cd demo/rogue/rogue-wasm && wasm-pack build --target web 2>&1" + ] + + verify_test [ + shape=parallelogram, + tool_command="set -e; for d in \"$HOME/.cargo/bin\" \"$USERPROFILE/.cargo/bin\"; do [ -d \"$d\" ] && PATH=\"$d:$PATH\"; done; export PATH; cd demo/rogue/rogue-wasm && cargo test 2>&1" + ] + + verify_artifacts [ + shape=parallelogram, + tool_command="set -e; DIRTY=$(git diff --name-only HEAD -- demo/rogue/rogue-wasm/ | grep -E '\\.(o|a|so|dylib|wasm|d|rmeta|rlib|fingerprint)$|/target/|/pkg/\\.gitignore' || true); if [ -n \"$DIRTY\" ]; then echo \"FAIL: build artifacts in diff: $DIRTY\" >&2; exit 1; fi; echo 'No build artifacts in diff'" + ] + + verify_fidelity [ + shape=box, + class="verify", + auto_status=true, + prompt="Perform semantic fidelity review after deterministic checks pass.\n\nThis is the critical gate for a faithful 1:1 port. Verify:\n1. Read demo/rogue/original-rogue/*.c and compare against demo/rogue/rogue-wasm/src/*.rs.\n2. Dungeon generation: room placement, corridor carving, maze generation, door/stair placement use same algorithms.\n3. Monster table: all 26 monster types (A-Z) have correct stats, flags, and special abilities.\n4. Combat math: attack rolls, hit tables, damage dice, armor class calculations match original.\n5. Item tables: all potion/scroll/ring/weapon/armor/food/wand types and effects match.\n6. RNG: same formula and seed propagation as original.\n7. Player systems: XP thresholds, level-up stats, hunger ticks match.\n8. I/O bridge: ncurses calls map to WASM-exported functions.\n9. Save/load: serialization covers all game state, localStorage integration works.\n10. HTML: demo/rogue/rogue-wasm/www/index.html exists with 80x24 ASCII grid, monospace font, dark background.\n\nIf semantic gaps exist, outcome=fail with stable failure_reason code (e.g., fidelity_gap_monster_stats) and details listing each discrepancy.\n\nWrite results to .ai/verify_fidelity.md.\n\nReport outcome=success if semantic review passes; otherwise report outcome=fail with failure_reason and details." + ] + } + + subgraph cluster_review { + label="Review" + + review_consensus [ + shape=box, + goal_gate=true, + auto_status=true, + prompt="Goal: $goal\n\nReview the implementation against .ai/definition_of_done.md.\nCheck: wasm-pack build succeeds, cargo test passes, completeness of port (all 33 C files accounted for), correctness of game logic, www/index.html renders properly.\nVerify fidelity: spot-check monster stats, combat math constants, RNG formula against original C source.\n\nConsensus policy:\n- If criteria are met and no critical gaps: outcome=success.\n- Otherwise: outcome=retry with failure_reason listing specific issues.\n\nWrite review to .ai/review_consensus.md with APPROVED or REJECTED verdict.\n\nReport outcome=success or outcome=retry with failure_reason." + ] + } + + subgraph cluster_postmortem { + label="Postmortem" + + postmortem [ + shape=box, + auto_status=true, + prompt="Goal: $goal\n\nAnalyze why the implementation failed.\nRead .ai/review_consensus.md.\nRead .ai/implementation_log.md.\nRead .ai/verify_fidelity.md if it exists.\n\nProduce actionable guidance: root causes, what worked, what failed, specific fixes.\nFor fidelity issues, reference exact C source lines and expected Rust equivalents.\nThe next iteration must NOT start from scratch - preserve working code and fix gaps.\n\nWrite to .ai/postmortem_latest.md (overwrite previous)." + ] + } + + // ========================================================================= + // Flow + // ========================================================================= + + start -> check_toolchain + check_toolchain -> expand_spec [condition="outcome=success"] + check_toolchain -> postmortem [condition="outcome=fail"] + check_toolchain -> postmortem + expand_spec -> check_dod + + // keep DoD/planning linear in fast mode + check_dod -> consolidate_dod + consolidate_dod -> prepare_ai_inputs + prepare_ai_inputs -> debate_consolidate + + debate_consolidate -> implement + + prepare_ai_inputs -> implement [condition="outcome=success"] + prepare_ai_inputs -> postmortem [condition="outcome=fail"] + prepare_ai_inputs -> postmortem + + implement -> fix_fmt [condition="outcome=success"] + implement -> implement [condition="outcome=fail && context.failure_class=transient_infra", loop_restart=true] + implement -> postmortem [condition="outcome=fail && context.failure_class!=transient_infra"] + implement -> postmortem + + fix_fmt -> verify_fmt [condition="outcome=success"] + fix_fmt -> postmortem [condition="outcome=fail"] + fix_fmt -> postmortem + + verify_fmt -> verify_build [condition="outcome=success"] + verify_fmt -> postmortem [condition="outcome=fail"] + verify_fmt -> postmortem + + verify_build -> verify_test [condition="outcome=success"] + verify_build -> postmortem [condition="outcome=fail"] + verify_build -> postmortem + + verify_test -> verify_artifacts [condition="outcome=success"] + verify_test -> postmortem [condition="outcome=fail"] + verify_test -> postmortem + + verify_artifacts -> verify_fidelity [condition="outcome=success"] + verify_artifacts -> postmortem [condition="outcome=fail"] + verify_artifacts -> postmortem + + verify_fidelity -> review_consensus [condition="outcome=success"] + verify_fidelity -> postmortem [condition="outcome=fail"] + verify_fidelity -> postmortem + + review_consensus -> exit [condition="outcome=success"] + review_consensus -> postmortem [condition="outcome=retry"] + review_consensus -> postmortem [condition="outcome=fail"] + review_consensus -> postmortem + + postmortem -> debate_consolidate [condition="context.failure_class=transient_infra", loop_restart=true] + postmortem -> debate_consolidate [condition="context.failure_class!=transient_infra"] + postmortem -> debate_consolidate +} diff --git a/internal/attractor/engine/handlers.go b/internal/attractor/engine/handlers.go index 8643c5dd..3eec52c3 100644 --- a/internal/attractor/engine/handlers.go +++ b/internal/attractor/engine/handlers.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "path/filepath" + goruntime "runtime" "strings" "time" @@ -572,10 +573,11 @@ func (h *ToolHandler) Execute(ctx context.Context, execCtx *Execution, node *mod } } + shellPath := resolveToolShellPath() if err := writeJSON(filepath.Join(stageDir, toolInvocationFileName), map[string]any{ - "tool": "bash", + "tool": filepath.Base(shellPath), // Use a non-login, non-interactive shell to avoid sourcing user dotfiles. - "argv": []string{"bash", "-c", cmdStr}, + "argv": []string{shellPath, "-c", cmdStr}, "command": cmdStr, "working_dir": execCtx.WorktreeDir, "timeout_ms": timeout.Milliseconds(), @@ -586,7 +588,7 @@ func (h *ToolHandler) Execute(ctx context.Context, execCtx *Execution, node *mod cctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - cmd := exec.CommandContext(cctx, "bash", "-c", cmdStr) + cmd := exec.CommandContext(cctx, shellPath, "-c", cmdStr) cmd.Dir = execCtx.WorktreeDir cmd.Env = buildBaseNodeEnv(artifactPolicyFromExecution(execCtx)) // Avoid hanging on interactive reads; tool_command doesn't provide a way to supply stdin. @@ -691,6 +693,68 @@ func (h *ToolHandler) Execute(ctx context.Context, execCtx *Execution, node *mod }, nil } +func resolveToolShellPath() string { + return resolveToolShellPathWith(goruntime.GOOS, exec.LookPath, pathExists) +} + +func resolveToolShellPathWith(goos string, lookPath func(string) (string, error), exists func(string) bool) string { + if lookPath == nil { + lookPath = exec.LookPath + } + if exists == nil { + exists = pathExists + } + if path, err := lookPath("bash"); err == nil && strings.TrimSpace(path) != "" { + if strings.EqualFold(goos, "windows") && isWindowsBashShim(path) { + if preferred := preferredWindowsBashPath(lookPath, exists); preferred != "" { + return preferred + } + } + return path + } + if strings.EqualFold(goos, "windows") { + if preferred := preferredWindowsBashPath(lookPath, exists); preferred != "" { + return preferred + } + } + return "bash" +} + +func preferredWindowsBashPath(lookPath func(string) (string, error), exists func(string) bool) string { + candidates := []string{ + filepath.Clean(`C:\Program Files\Git\bin\bash.exe`), + filepath.Clean(`C:\Program Files\Git\usr\bin\bash.exe`), + } + if lookPath != nil { + if gitPath, err := lookPath("git"); err == nil && strings.TrimSpace(gitPath) != "" { + gitDir := filepath.Dir(gitPath) + candidates = append(candidates, + filepath.Clean(filepath.Join(gitDir, "bash.exe")), + filepath.Clean(filepath.Join(gitDir, "..", "usr", "bin", "bash.exe")), + ) + } + } + for _, candidate := range candidates { + if exists != nil && exists(candidate) { + return candidate + } + } + return "" +} + +func isWindowsBashShim(path string) bool { + clean := strings.ToLower(filepath.Clean(strings.TrimSpace(path))) + return strings.HasSuffix(clean, `\windows\system32\bash.exe`) || strings.HasSuffix(clean, `\windows\sysnative\bash.exe`) +} + +func pathExists(path string) bool { + if strings.TrimSpace(path) == "" { + return false + } + info, err := os.Stat(path) + return err == nil && !info.IsDir() +} + func truncate(s string, n int) string { if n <= 0 || len(s) <= n { return s diff --git a/internal/attractor/engine/tool_shell_resolution_test.go b/internal/attractor/engine/tool_shell_resolution_test.go new file mode 100644 index 00000000..af3f478e --- /dev/null +++ b/internal/attractor/engine/tool_shell_resolution_test.go @@ -0,0 +1,55 @@ +package engine + +import ( + "errors" + "path/filepath" + "testing" +) + +func TestResolveToolShellPathWith_NonWindowsUsesLookPath(t *testing.T) { + lookPath := func(name string) (string, error) { + if name == "bash" { + return "/usr/bin/bash", nil + } + return "", errors.New("not found") + } + got := resolveToolShellPathWith("linux", lookPath, func(string) bool { return false }) + if got != "/usr/bin/bash" { + t.Fatalf("shell path: got %q want %q", got, "/usr/bin/bash") + } +} + +func TestResolveToolShellPathWith_WindowsPrefersGitBashWhenBashIsWSLShim(t *testing.T) { + lookPath := func(name string) (string, error) { + switch name { + case "bash": + return `C:\Windows\System32\bash.exe`, nil + case "git": + return `D:\Tools\Git\cmd\git.exe`, nil + default: + return "", errors.New("not found") + } + } + expected := filepath.Clean(`D:\Tools\Git\usr\bin\bash.exe`) + exists := func(path string) bool { + return filepath.Clean(path) == expected + } + got := resolveToolShellPathWith("windows", lookPath, exists) + if got != expected { + t.Fatalf("shell path: got %q want %q", got, expected) + } +} + +func TestResolveToolShellPathWith_WindowsFallsBackToCommonGitBashWhenBashMissing(t *testing.T) { + lookPath := func(name string) (string, error) { + return "", errors.New("not found") + } + expected := filepath.Clean(`C:\Program Files\Git\bin\bash.exe`) + exists := func(path string) bool { + return filepath.Clean(path) == expected + } + got := resolveToolShellPathWith("windows", lookPath, exists) + if got != expected { + t.Fatalf("shell path: got %q want %q", got, expected) + } +}