Skip to content
Open
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
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ docker run -p 5200:5200 -v ./config.json:/app/config.json orchestration
| `backend/services/git_service.py` | Stateless git operations via subprocess + asyncio.to_thread |
| `backend/services/resource_monitor.py` | Health checks (Ollama, ComfyUI, Claude) |
| `backend/services/progress.py` | SSE broadcast, event persistence |
| `backend/utils/xml_utils.py` | XML plan extraction and parsing (extract_xml_plan, parse_plan_xml) |
| `backend/tools/registry.py` | Injectable `ToolRegistry` class |
| `backend/tools/` | Tool implementations (RAG, Ollama, ComfyUI, file) |
| `frontend/` | React 19 + TypeScript + Vite UI (ErrorBoundary, 404 page) |
Expand All @@ -93,6 +94,7 @@ docker run -p 5200:5200 -v ./config.json:/app/config.json orchestration
- **Auth**: JWT Bearer tokens for REST, API keys (`orch_` prefix) for MCP/external executors, short-lived SSE tokens for EventSource. First registered user becomes admin.
- **Ownership**: projects have `owner_id`. Users see/modify only their own projects. Admins can access all.
- **Budget**: every API call recorded in `usage_log`, checked against limits before execution. Budget endpoints are admin-only.
- **Plans**: XML format (source of truth in `plan_xml` column). Dual-column: `plan_xml` + `plan_json` for backward compat. Decomposer/routes prefer XML with JSON fallback. Planner has JSON fallback if Claude returns JSON despite XML prompt.
- **Models**: Ollama (free) for simple tasks, Haiku ($) for medium, Sonnet ($$) for complex
- **Tools**: registered in `ToolRegistry` class, injected via DI container
- **SSE**: short-lived token via `POST /api/events/{project_id}/token`, then stream via `GET /api/events/{project_id}?token=...`
Expand All @@ -108,7 +110,7 @@ docker run -p 5200:5200 -v ./config.json:/app/config.json orchestration
- **Traceability**: requirements numbered [R1], [R2], mapped to tasks; coverage endpoint shows gaps
- **External execution**: MCP server (`backend/mcp/server.py`) for Claude Code integration. Execution modes: auto (engine-only), hybrid (Ollama internal, Claude external), external (all external). Tasks claimed atomically via CAS, results submitted with cost tracking.
- **Git integration**: optional per-project (`repo_path` nullable). `GitService` wraps subprocess via `asyncio.to_thread()`. Config in `git.*` section. Phase 1 (foundation) complete; execution wiring (Phase 2+) pending.
- **Tests**: Backend: pytest-asyncio (auto mode), 731 tests. Frontend: vitest + @testing-library/react, 137 tests. Load tests: 7 (excluded from CI via `slow` marker)
- **Tests**: Backend: pytest-asyncio (auto mode), 797 tests. Frontend: vitest + @testing-library/react, 137 tests. Load tests: 7 (excluded from CI via `slow` marker)

## Git Workflow

Expand Down
1 change: 1 addition & 0 deletions backend/db/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
completion_tokens INTEGER NOT NULL DEFAULT 0,
cost_usd REAL NOT NULL DEFAULT 0.0,
plan_json TEXT NOT NULL,
plan_xml TEXT,
status TEXT NOT NULL DEFAULT 'draft',
created_at REAL NOT NULL
);
Expand Down
1 change: 1 addition & 0 deletions backend/db/models_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
Column("completion_tokens", Integer, nullable=False, server_default="0"),
Column("cost_usd", Float, nullable=False, server_default="0.0"),
Column("plan_json", Text, nullable=False),
Column("plan_xml", Text, nullable=True),
Column("status", Text, nullable=False, server_default="draft"),
Column("created_at", Float, nullable=False),
)
Expand Down
32 changes: 32 additions & 0 deletions backend/migrations/versions/015_add_plan_xml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Orchestration Engine - Migration 015
#
# Add plan_xml column to plans table for XML plan storage.
# Existing plans remain in plan_json; new plans write both columns.
#
# Depends on: 014_add_api_keys_and_claim_tracking
# Used by: services/planner.py, services/decomposer.py

"""Add plan_xml column to plans table.

Revision ID: 015
Revises: 014
Create Date: 2026-03-06
"""

from alembic import op
import sqlalchemy as sa

revision = "015"
down_revision = "014"
branch_labels = None
depends_on = None


def upgrade():
with op.batch_alter_table("plans") as batch_op:
batch_op.add_column(sa.Column("plan_xml", sa.Text(), nullable=True))


def downgrade():
with op.batch_alter_table("plans") as batch_op:
batch_op.drop_column("plan_xml")
3 changes: 2 additions & 1 deletion backend/models/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ class PlanOut(BaseModel):
prompt_tokens: int
completion_tokens: int
cost_usd: float
plan: dict # The structured plan JSON
plan: dict # The structured plan data (parsed from XML or JSON)
plan_xml: str | None = None # Raw XML plan (if available)
status: PlanStatus
created_at: float

Expand Down
23 changes: 17 additions & 6 deletions backend/routes/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@
# Helpers
# ---------------------------------------------------------------------------

def _parse_plan_from_row(row) -> dict:
"""Parse plan data from a DB row, preferring plan_xml over plan_json."""
plan_xml_raw = row["plan_xml"]
if plan_xml_raw:
from backend.utils.xml_utils import parse_plan_xml
return parse_plan_xml(plan_xml_raw)
return json.loads(row["plan_json"])


async def _row_to_project(
row, db: Database,
include_task_summary: bool = False,
Expand Down Expand Up @@ -298,7 +307,8 @@ async def list_plans(
prompt_tokens=r["prompt_tokens"],
completion_tokens=r["completion_tokens"],
cost_usd=r["cost_usd"],
plan=json.loads(r["plan_json"]),
plan=_parse_plan_from_row(r),
plan_xml=r["plan_xml"],
status=r["status"],
created_at=r["created_at"],
)
Expand Down Expand Up @@ -438,9 +448,10 @@ async def clone_project(
new_plan_id = uuid.uuid4().hex[:12]
await db.execute_write(
"INSERT INTO plans (id, project_id, version, model_used, prompt_tokens, "
"completion_tokens, cost_usd, plan_json, status, created_at) "
"VALUES (?, ?, 1, ?, 0, 0, 0.0, ?, 'draft', ?)",
(new_plan_id, new_project_id, plan_row["model_used"], plan_row["plan_json"], now),
"completion_tokens, cost_usd, plan_json, plan_xml, status, created_at) "
"VALUES (?, ?, 1, ?, 0, 0, 0.0, ?, ?, 'draft', ?)",
(new_plan_id, new_project_id, plan_row["model_used"],
plan_row["plan_json"], plan_row["plan_xml"], now),
)

# 3. Clone tasks (reset status, clear output/cost/retry)
Expand Down Expand Up @@ -512,8 +523,8 @@ async def export_project(
{
"id": p["id"], "version": p["version"], "model_used": p["model_used"],
"prompt_tokens": p["prompt_tokens"], "completion_tokens": p["completion_tokens"],
"cost_usd": p["cost_usd"], "plan": json.loads(p["plan_json"]),
"status": p["status"], "created_at": p["created_at"],
"cost_usd": p["cost_usd"], "plan": _parse_plan_from_row(p),
"plan_xml": p["plan_xml"], "status": p["status"], "created_at": p["created_at"],
}
for p in plan_rows
]
Expand Down
12 changes: 9 additions & 3 deletions backend/services/decomposer.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Orchestration Engine - Plan Decomposer
#
# Converts an approved plan JSON into task rows with dependency edges.
# Converts an approved plan (XML or JSON) into task rows with dependency edges.
#
# Depends on: backend/config.py, services/model_router.py
# Depends on: backend/config.py, services/model_router.py, utils/xml_utils.py
# Used by: routes/projects.py, container.py

import json
Expand Down Expand Up @@ -66,7 +66,13 @@ async def decompose(self, project_id: str, plan_id: str) -> dict:
if plan_row["project_id"] != project_id:
raise NotFoundError(f"Plan {plan_id} does not belong to project {project_id}")

plan_data = json.loads(plan_row["plan_json"])
# Prefer XML plan (source of truth) with JSON fallback
plan_xml_raw = plan_row["plan_xml"]
if plan_xml_raw:
from backend.utils.xml_utils import parse_plan_xml
plan_data = parse_plan_xml(plan_xml_raw)
else:
plan_data = json.loads(plan_row["plan_json"])
tasks_data, phase_names = _flatten_plan_tasks(plan_data)

if not tasks_data:
Expand Down
Loading