diff --git a/nanobot/runtime/coordinator.py b/nanobot/runtime/coordinator.py index dd6d03e..4facd86 100644 --- a/nanobot/runtime/coordinator.py +++ b/nanobot/runtime/coordinator.py @@ -412,6 +412,24 @@ def _git_output(args: list[str], cwd: Path) -> str | None: return None +def _observed_product_head_source_fingerprint(workspace: Path) -> dict[str, Any] | None: + current_state_path = workspace / "state" / "self_evolution" / "current_state.json" + payload = _safe_read_json(current_state_path) + if not isinstance(payload, dict): + return None + observed = payload.get("observed_product_head") if isinstance(payload.get("observed_product_head"), dict) else {} + commit = observed.get("commit") or payload.get("product_head") + if not commit: + return None + return { + "source_repo_root": observed.get("repo_root") or str(workspace), + "source_commit": commit, + "source_branch": observed.get("branch"), + "source_tree": observed.get("tree"), + "source_authority": "observed_product_head", + } + + def _runtime_source_fingerprint(workspace: Path) -> dict[str, Any]: env_commit = os.environ.get('NANOBOT_SOURCE_COMMIT') or os.environ.get('SOURCE_COMMIT') if env_commit: @@ -431,12 +449,17 @@ def _runtime_source_fingerprint(workspace: Path) -> dict[str, Any]: commit = _git_output(['git', 'rev-parse', 'HEAD'], repo_root) branch = _git_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], repo_root) tree = _git_output(['git', 'rev-parse', 'HEAD^{tree}'], repo_root) - return { - 'source_repo_root': str(repo_root), - 'source_commit': commit, - 'source_branch': branch, - 'source_tree': tree, - } + if commit: + return { + 'source_repo_root': str(repo_root), + 'source_commit': commit, + 'source_branch': branch, + 'source_tree': tree, + 'source_authority': 'git', + } + observed_fingerprint = _observed_product_head_source_fingerprint(workspace) + if observed_fingerprint: + return observed_fingerprint return { 'source_repo_root': str(workspace), 'source_commit': None, diff --git a/tests/test_runtime_coordinator.py b/tests/test_runtime_coordinator.py index 10ff599..b8a6b15 100644 --- a/tests/test_runtime_coordinator.py +++ b/tests/test_runtime_coordinator.py @@ -35,6 +35,28 @@ def test_runtime_source_fingerprint_prefers_explicit_release_env_for_archived_ru } +def test_runtime_source_fingerprint_falls_back_to_observed_product_head_when_git_is_unavailable(tmp_path, monkeypatch): + state_dir = tmp_path / "state" / "self_evolution" + state_dir.mkdir(parents=True) + (state_dir / "current_state.json").write_text(json.dumps({ + "observed_product_head": { + "commit": "observed-product-head-123", + "source": "git_rev_parse_head", + "repo_root": "/opt/eeepc-agent/runtimes/self-evolving-agent/current", + }, + "product_head": "observed-product-head-123", + }), encoding="utf-8") + monkeypatch.delenv("NANOBOT_SOURCE_COMMIT", raising=False) + monkeypatch.delenv("SOURCE_COMMIT", raising=False) + monkeypatch.setattr("nanobot.runtime.coordinator._git_output", lambda args, cwd: None) + + fingerprint = _runtime_source_fingerprint(tmp_path) + + assert fingerprint["source_commit"] == "observed-product-head-123" + assert fingerprint["source_repo_root"] == "/opt/eeepc-agent/runtimes/self-evolving-agent/current" + assert fingerprint["source_authority"] == "observed_product_head" + + def test_cycle_writes_block_report_when_gate_missing(tmp_path): execute = AsyncMock(return_value="should not run") now = datetime(2026, 4, 15, 12, 0, tzinfo=timezone.utc)