Skip to content
Merged
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
9 changes: 9 additions & 0 deletions .agent/rules/graphify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## graphify

This project has a graphify knowledge graph at graphify-out/.

Rules:
- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure
- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
- If the graphify MCP server is active, utilize tools like `query_graph`, `get_node`, and `shortest_path` for precise architecture navigation instead of falling back to `grep`
- After modifying code files in this session, run `python3 -c "from graphify.watch import _rebuild_code; from pathlib import Path; _rebuild_code(Path('.'))"` to keep the graph current
8 changes: 8 additions & 0 deletions .agent/workflows/graphify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Workflow: graphify
**Command:** /graphify
**Description:** Turn any folder of files into a navigable knowledge graph

## Steps
Follow the graphify skill installed at ~/.agent/skills/graphify/SKILL.md to run the full pipeline.

If no path argument is given, use `.` (current directory).
10 changes: 10 additions & 0 deletions .cursor/rules/graphify.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
description: graphify knowledge graph context
alwaysApply: true
---

This project has a graphify knowledge graph at graphify-out/.

- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure
- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
- After modifying code files in this session, run `python3 -c "from graphify.watch import _rebuild_code; from pathlib import Path; _rebuild_code(Path('.'))"` to keep the graph current
22 changes: 22 additions & 0 deletions .opencode/plugins/graphify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// graphify OpenCode plugin
// Injects a knowledge graph reminder before bash tool calls when the graph exists.
import { existsSync } from "fs";
import { join } from "path";

export const GraphifyPlugin = async ({ directory }) => {
let reminded = false;

return {
"tool.execute.before": async (input, output) => {
if (reminded) return;
if (!existsSync(join(directory, "graphify-out", "graph.json"))) return;

if (input.tool === "bash") {
output.args.command =
'echo "[graphify] Knowledge graph available. Read graphify-out/GRAPH_REPORT.md for god nodes and architecture context before searching files." && ' +
output.args.command;
reminded = true;
}
},
};
};
10 changes: 5 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ ENV PYTHONPATH="/app"
ENV PYTHONUNBUFFERED=1

# Expose port
EXPOSE 8000
EXPOSE 7860

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Health check - generous timeouts for Hugging Face Spaces
HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=10 \
CMD curl -f http://localhost:7860/health || exit 1

# Run server (the app.py lifespan prints the full banner)
CMD ["python", "-m", "uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["python", "-m", "uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860"]
65 changes: 59 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
---
title: Mission Control
emoji: "☁️"
colorFrom: blue
colorTo: indigo
sdk: docker
tags:
- finops
- openenv
- fastapi
- observability
- dashboard
---

# 🛡️ MissionCtrl — AI Oversight Fleet Environment

> *Every LLM agent fleet will hallucinate. MissionCtrl trains the overseer to catch them.*
Expand Down Expand Up @@ -204,7 +218,7 @@ docker build -t missionctrl .
docker run -p 8000:8000 --name missionctrl missionctrl

# 3. Run the baseline agent (in another terminal)
docker exec -it missionctrl python inference.py
docker exec -it missionctrl python client.py

# 4. Watch the dashboard
open http://localhost:8000/dashboard
Expand All @@ -222,8 +236,8 @@ python -m uvicorn server.app:app --host 0.0.0.0 --port 8000
# Configure API keys
cp .env.example .env # fill in your LLM provider keys

# Run inference
python inference.py
# Run inference (OpenEnv canonical entrypoint)
python client.py

# Run tests
pytest tests/ -v
Expand Down Expand Up @@ -301,7 +315,8 @@ missionctrl/
├── openenv.yaml # OpenEnv manifest
├── pyproject.toml # Python project config
├── Dockerfile # Single-container deployment
├── inference.py # Baseline LLM agent + cross-episode memory
├── client.py # OpenEnv-required baseline evaluator entrypoint
├── inference.py # Backward-compatible wrapper to client.main()
├── .env.example # API key template
├── server/
│ ├── app.py # FastAPI server (6 endpoints + dashboard)
Expand Down Expand Up @@ -343,15 +358,53 @@ missionctrl/
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/` | Status heartbeat |
| `GET` | `/health` | Readiness check (`{"status": "ok"}`) |
| `GET` | `/health` | Readiness check (`{"healthy": true, "env": "missionctrl"}`) |
| `POST` | `/reset` | `{"task_id": "easy"}` → Reset environment for tier |
| `POST` | `/step` | `{"action": "FLAG(task_01, \"evidence\")"}` → Execute action |
| `GET` | `/state` | Current observation + live hallucination stats |
| `GET` | `/state` | Runtime-aware observation payload + build/container metadata |
| `GET` | `/logs` | Structured logs summary (status/path counters + recent requests) |
| `GET` | `/history` | Full action/reward timeline (JSON array) |
| `GET` | `/dashboard` | Live visualization UI |

---

## HF Spaces Health and Logs

The Space now exposes two `200 OK` observability endpoints intended for build/runtime diagnostics:

- `GET /state` returns:
- `status`
- `build` metadata (`container_id`, `build_id`, `git_sha`, `started_at`)
- current environment `observation`
- `GET /logs` returns:
- `status`
- `build` metadata
- aggregate `totals`, `statuses`, and `paths`
- recent request `entries` with `method`, `path`, `status_code`, and `duration_ms`

Quick check:

```bash
python scripts.py
```

---

## OpenEnv Required Files

OpenEnv validation expects a root-level `client.py`. This repository now provides:

- `client.py` as the canonical OpenEnv evaluator script
- `inference.py` as a compatibility wrapper for legacy commands

Preferred command:

```bash
python client.py
```

---

## 🧪 Testing

```bash
Expand Down
154 changes: 154 additions & 0 deletions client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""Client library for MissionCtrl environment.

Provides HTTP client functions for interacting with the MissionCtrl environment API.
Contains environment payload structures and example scenarios.
"""

import os
from typing import Any, Dict, Optional

import httpx
from dotenv import load_dotenv

# ---------------------------------------------------------------------------
# Load .env file automatically
# ---------------------------------------------------------------------------
load_dotenv()

# ---------------------------------------------------------------------------
# Environment configuration
# ---------------------------------------------------------------------------
ENV_BASE_URL: str = os.environ.get("ENV_BASE_URL", "http://localhost:7860")

# Known agent types in the environment
KNOWN_AGENTS = (
"PlannerAgent",
"ResearchAgent",
"CoderAgent",
"TesterAgent",
"CommAgent",
)

# Available task tiers
TASKS = ["easy", "medium", "hard", "special"]

# Default max steps per episode
MAX_STEPS = 5


# ---------------------------------------------------------------------------
# HTTP Client
# ---------------------------------------------------------------------------
http = httpx.Client(timeout=60.0)


# ---------------------------------------------------------------------------
# Environment API Functions
# ---------------------------------------------------------------------------
def reset_env(task_id: str, seed: Optional[int] = None) -> Dict[str, Any]:
"""Reset the environment for a specific task.

Args:
task_id: The task tier to run (easy, medium, hard, special)
seed: Optional random seed for reproducibility

Returns:
Dictionary containing the initial observation
"""
payload = {"task_id": task_id}
if seed is not None:
payload["seed"] = seed

resp = http.post(f"{ENV_BASE_URL}/reset", json=payload)
resp.raise_for_status()
return resp.json()


def step_env(action: str) -> Dict[str, Any]:
"""Execute one action in the environment.

Args:
action: The action string to execute (e.g., "APPROVE(task_1)")

Returns:
Dictionary containing the new observation, reward, done flag, and info
"""
resp = http.post(f"{ENV_BASE_URL}/step", json={"action": action})
resp.raise_for_status()
return resp.json()


def get_state() -> Dict[str, Any]:
"""Get the current environment state (read-only).

Returns:
Dictionary containing the current observation
"""
resp = http.get(f"{ENV_BASE_URL}/state")
resp.raise_for_status()
return resp.json()


def get_history() -> list:
"""Get the action history for the current episode.

Returns:
List of past actions and their results
"""
resp = http.get(f"{ENV_BASE_URL}/history")
resp.raise_for_status()
return resp.json()


def record_result(tier: str, score: float, steps: int, history: list,
score_breakdown: Optional[Dict] = None,
hallucination_stats: Optional[Dict] = None) -> Dict[str, str]:
"""Push a completed episode result to the dashboard.

Args:
tier: Task tier (easy, medium, hard, special)
score: Final score for the episode
steps: Number of steps taken
history: Action history for the episode
score_breakdown: Optional detailed score breakdown
hallucination_stats: Optional hallucination detection statistics

Returns:
Confirmation response
"""
payload = {
"tier": tier,
"score": score,
"steps": steps,
"history": history,
"score_breakdown": score_breakdown or {},
"hallucination_stats": hallucination_stats or {},
}
resp = http.post(f"{ENV_BASE_URL}/record", json=payload)
resp.raise_for_status()
return resp.json()


# ---------------------------------------------------------------------------
# Example Usage
# ---------------------------------------------------------------------------
def example_basic_usage():
"""Example showing basic environment interaction."""
# Reset environment for easy task
result = reset_env("easy")
obs = result["observation"]
print(f"Started task with {len(obs['tasks'])} tasks")

# Take a step
step_result = step_env("NOOP")
print(f"Reward: {step_result['reward']}, Done: {step_result['done']}")

# Get current state
state = get_state()
print(f"Current step: {state['time_step']}")


if __name__ == "__main__":
# For backward compatibility, delegate to inference script
from inference import main
main()
3 changes: 1 addition & 2 deletions inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
MODEL_NAME: str = os.environ.get("MODEL_NAME", "openai/gpt-oss-120b")
HF_TOKEN: str = os.environ.get("HF_TOKEN", "")

ENV_BASE_URL: str = os.environ.get("ENV_BASE_URL", "http://localhost:8000")
ENV_BASE_URL: str = os.environ.get("ENV_BASE_URL", "http://localhost:7860")

MAX_STEPS: int = int(os.environ.get("MAX_STEPS", "5"))
TASKS: List[str] = ["easy", "medium", "hard", "special"]
Expand Down Expand Up @@ -100,7 +100,6 @@ class PromptTooLargeError(RuntimeError):
"""Raised when provider rejects a request as permanently oversized."""



def _append_bounded_unique(bucket: List[str], value: str, limit: int) -> None:
value = value.strip()
if not value:
Expand Down
Loading
Loading