Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,25 @@ jobs:
- name: Test
run: PYTHONPATH=cli:apps/agent python -m unittest discover -s tests -t .

windows_state:
name: State recovery and locking (Windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Exercise revision, lock and recovery paths
env:
PYTHONPATH: cli
run: >-
python -m unittest -v
tests.cli.test_milestone1.Milestone1Tests.test_stale_expected_revision_does_not_write
tests.cli.test_milestone1.Milestone1Tests.test_live_project_lock_returns_conflict_without_writing
tests.cli.test_milestone1.Milestone1Tests.test_stale_snapshot_requires_reconcile
tests.cli.test_milestone1.Milestone1Tests.test_reconcile_refuses_to_hide_a_missing_evidence_artifact
tests.cli.test_milestone1.Milestone1Tests.test_torn_final_event_is_reported_not_repaired

integration:
name: Agent against a real Kura daemon
# Every wire-level bug in this stack -- a rejected URL shape, a route that
Expand Down Expand Up @@ -55,10 +74,12 @@ jobs:
env:
LOOPFORGE_KURA_BIN: ${{ github.workspace }}/apps/workbench/vendor/kura/crates/target/debug/kura
run: PYTHONPATH=cli:apps/agent python -c "import sys; from tests.support.kura_daemon import kura_binary; sys.exit(0 if kura_binary() else 'no Kura binary was discoverable')"
- name: Require the pinned approval-wait source
run: test -f apps/workbench/vendor/kura/crates/domains/mcp/src/agent_tool.rs
- name: Integration tests
env:
LOOPFORGE_KURA_BIN: ${{ github.workspace }}/apps/workbench/vendor/kura/crates/target/debug/kura
run: PYTHONPATH=cli:apps/agent python -m unittest tests.agent.test_integration -v
run: PYTHONPATH=cli:apps/agent python -m unittest tests.agent.test_integration tests.agent.test_approval_wait -v

engine:
name: Engine adapter against a real Godot
Expand All @@ -83,11 +104,17 @@ jobs:
mv Godot_v4.3-stable_linux.x86_64 "$HOME/.local/bin/godot4"
chmod +x "$HOME/.local/bin/godot4"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Install an offscreen display for runtime capture
run: |
sudo apt-get update
sudo apt-get install -y xvfb libgl1-mesa-dri libxcursor1 libxinerama1 libxi6
# requires_godot skips when no usable binary is found, and a skipped suite
# still reports success. Fail here instead, so a broken download cannot
# quietly turn this job into a no-op that reports green.
- name: Require a discoverable engine
run: PYTHONPATH=cli:apps/agent python -c "import sys; from tests.support.godot import godot_binary, godot_major_version; b = godot_binary(); sys.exit('no Godot binary was discoverable') if b is None else None; v = godot_major_version(b); sys.exit(f'Godot major version {v} is not the supported 4') if v != 4 else None"
- name: Require a runtime capture display
run: command -v xvfb-run
- name: Engine integration tests
run: PYTHONPATH=cli:apps/agent python -m unittest tests.cli.test_engine_integration -v

Expand Down
40 changes: 38 additions & 2 deletions apps/agent/loopforge_agent/__main__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
from loopforge_agent.server import main
"""The Agent, and -- under one flag -- the CLI it needs to spawn.

raise SystemExit(main())
A packaged build ships a single binary. The agent registers Loopforge's own
commands with the daemon as an MCP tool server, and the daemon spawns that
server as a subprocess, so the CLI has to be reachable as a process. Resolving
it from PATH found a `loopforge` installed from the git remote months earlier,
with no `mcp` subcommand: it exited before the handshake and the only thing
reported was `mcp transport is closed`.

So the binary answers to both. There is no interpreter to hand `-m loopforge`
to in a frozen build, and shipping a second executable would only move the
question of which copy is which -- one file cannot disagree with itself.

Kept as an argument rather than a second entry point name so it cannot be
reached by accident: `sys.argv[0]` is whatever the daemon was told to run, and
dispatching on it would make a rename change what the program does.
"""

import sys

#: Must match `loopforge.agent.supervisor.CLI_DISPATCH_FLAG`.
CLI_DISPATCH_FLAG = "--loopforge-cli"


def _run() -> int:
if len(sys.argv) > 1 and sys.argv[1] == CLI_DISPATCH_FLAG:
from loopforge.cli import main as cli_main

# Dropped so the CLI sees its own arguments: it parses `--project`
# before a subcommand and would reject the flag that got it here.
sys.argv = [sys.argv[0], *sys.argv[2:]]
return cli_main()

from loopforge_agent.server import main

return main()


raise SystemExit(_run())
Loading
Loading