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
63 changes: 63 additions & 0 deletions backend/engine/document.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Render documented-run artifacts from a run_pipeline result (no live deps)."""

from __future__ import annotations

import json
from pathlib import Path

_PHASE_ORDER = ["clarify", "design", "code", "test", "deploy", "iterate"]


def _dag_mermaid() -> str:
edges = "\n".join(
f" {a} --> {b}" for a, b in zip(_PHASE_ORDER, _PHASE_ORDER[1:], strict=False)
)
return f"```mermaid\nflowchart LR\n{edges}\n```\n"


def write_run_docs(result: dict, out_dir: str) -> list[str]:
out = Path(out_dir)
out.mkdir(parents=True, exist_ok=True)
snap = result["snapshot"]
pids = result.get("worker_pids", [])

def pid_for(owner: str | None) -> str:
if owner and owner.startswith("w"):
try:
return f"pid {pids[int(owner[1:])]}"
except (ValueError, IndexError):
return owner
return owner or "-"

lines = [
f"# AppForge run `{result['run_id']}`",
"",
f"**Status:** {snap['status']} • **Workers (PIDs):** {pids}",
"",
"## Phases",
"",
"| phase | status | gate |",
"|---|---|---|",
]
lines += [f"| {p['name']} | {p['status']} | {p['gate']} |" for p in snap["phases"]]
lines += [
"",
"## Tasks (agent → worker PID → model)",
"",
"| agent | phase | status | worker | model |",
"|---|---|---|---|---|",
]
for t in snap["tasks"]:
lines.append(
f"| {t['agent_id']} | {t['phase']} | {t['status']} | "
f"{pid_for(t.get('owner'))} | {t.get('model')} |"
)
lines += ["", "## Phase dependency graph", "", _dag_mermaid()]

summary_p = out / "run-summary.md"
summary_p.write_text("\n".join(lines) + "\n", encoding="utf-8")
snap_p = out / "snapshot.json"
snap_p.write_text(json.dumps(snap, indent=2), encoding="utf-8")
dag_p = out / "dag.md"
dag_p.write_text(_dag_mermaid(), encoding="utf-8")
return [str(summary_p), str(snap_p), str(dag_p)]
4 changes: 3 additions & 1 deletion backend/engine/phases.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import os
from dataclasses import dataclass
from pathlib import Path

Expand Down Expand Up @@ -31,7 +32,8 @@ def __init__(self, phases: list[PhaseSpec]):
self._by_name = {p.name: p for p in self._phases}

@classmethod
def load(cls, path: str = "config/phases.yaml") -> PhasesConfig:
def load(cls, path: str | None = None) -> PhasesConfig:
path = path or os.getenv("APPFORGE_PHASES", "config/phases.yaml")
raw = yaml.safe_load(Path(path).read_text(encoding="utf-8"))
phases: list[PhaseSpec] = []
for p in raw["phases"]:
Expand Down
10 changes: 8 additions & 2 deletions backend/engine/state_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from mcp.server.fastmcp import FastMCP

from backend.engine.mcp_tools import register_tools
from backend.engine.phases import PhasesConfig
from backend.engine.phases import PhasesConfig, load_downgrade_paths
from backend.engine.store import Store


Expand All @@ -33,7 +33,13 @@ def base_models_from_config(path: str = "config/agents.yaml") -> dict[str, str]:
def build_server(db_path, cfg=None, base_models=None, lease_s: float = 120.0):
cfg = cfg or PhasesConfig.load()
base_models = base_models if base_models is not None else base_models_from_config()
store = Store(db_path, cfg, base_models, lease_s=lease_s)
store = Store(
db_path,
cfg,
base_models,
lease_s=lease_s,
downgrade_paths=load_downgrade_paths(),
)
mcp = FastMCP("appforge-state", stateless_http=True)
register_tools(mcp, store)
return mcp, store
Expand Down
11 changes: 7 additions & 4 deletions backend/engine/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ def __init__(
cfg: PhasesConfig,
base_models: dict[str, str],
lease_s: float = 120.0,
downgrade_paths: dict[str, str] | None = None,
):
self.db_path = db_path
self.cfg = cfg
self.base_models = base_models
self.lease_s = lease_s
self.downgrade_paths = downgrade_paths or {}
self._db: aiosqlite.Connection | None = None
self._db_lock = asyncio.Lock()

Expand Down Expand Up @@ -58,6 +60,7 @@ async def _txn(self):
raise

async def create_run(self, run_id: str, idea: str, budget_limit: float) -> None:
entry = self.cfg.phase_names[0] # order-0 phase (phase_names is order-sorted)
async with self._db_lock, self._txn():
now = self._now()
await self._db.execute(
Expand All @@ -66,12 +69,12 @@ async def create_run(self, run_id: str, idea: str, budget_limit: float) -> None:
)
for name in self.cfg.phase_names:
order = self.cfg.order_of(name)
status = "open" if name == "clarify" else "blocked"
status = "open" if name == entry else "blocked"
await self._db.execute(
"INSERT INTO phases (run_id, name, phase_order, status, gate, seeded) VALUES (?,?,?,?,?,0)",
(run_id, name, order, status, self.cfg.gate_of(name)),
)
await self._seed_phase_locked(run_id, "clarify", now)
await self._seed_phase_locked(run_id, entry, now)
await self._recompute_ready_locked(run_id)

async def _seed_phase_locked(
Expand Down Expand Up @@ -173,8 +176,7 @@ async def _spend_ratio_locked(self, run_id: str) -> float:
return (spent / limit) if limit > 0 else 1.0

async def _downgrade_config(self) -> tuple[dict[str, str], set[str]]:
# Overridden/injected in Plan C to read budget.yaml; Plan A uses no downgrades.
return ({}, {"clarifying_pm", "solution_architect"})
return (self.downgrade_paths, {"clarifying_pm", "solution_architect"})

async def claim_next_task(self, run_id: str, worker_id: str) -> ClaimResult | None:
async with self._db_lock:
Expand Down Expand Up @@ -410,6 +412,7 @@ async def snapshot(self, run_id: str) -> dict:
"phase": t["phase"],
"status": t["status"],
"owner": t["owner"],
"model": t["model"],
}
for t in tasks
],
Expand Down
8 changes: 8 additions & 0 deletions docs/runs/2026-07-24/dag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
```mermaid
flowchart LR
clarify --> design
design --> code
code --> test
test --> deploy
deploy --> iterate
```
44 changes: 44 additions & 0 deletions docs/runs/2026-07-24/run-summary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# AppForge run `4a9b6b809e4b4cad8b03a04e687c3b25`

**Status:** done • **Workers (PIDs):** [41556, 5908, 35144, 34280]

## Phases

| phase | status | gate |
|---|---|---|
| clarify | complete | approved |
| code | complete | none |
| deploy | complete | none |
| design | complete | approved |
| iterate | complete | none |
| test | complete | none |

## Tasks (agent → worker PID → model)

| agent | phase | status | worker | model |
|---|---|---|---|---|
| clarifying_pm | clarify | done | pid 41556 | claude-3-5-sonnet-20241022 |
| solution_architect | design | done | pid 41556 | claude-3-5-sonnet-20241022 |
| tech_lead | design | done | pid 41556 | gpt-4o |
| uiux_designer | design | done | pid 41556 | claude-3-5-sonnet-20241022 |
| database | code | done | pid 41556 | gpt-4o |
| backend | code | done | pid 41556 | claude-3-5-sonnet-20241022 |
| frontend | code | done | pid 41556 | claude-3-5-sonnet-20241022 |
| ai_ml | code | done | pid 35144 | claude-3-5-sonnet-20241022 |
| qa_test | test | done | pid 41556 | gpt-4o-mini |
| security | test | done | pid 41556 | claude-3-5-haiku-20241022 |
| devops | deploy | done | pid 41556 | gpt-4o-mini |
| technical_writer | deploy | done | pid 41556 | gpt-4o-mini |
| delivery_summarizer | iterate | done | pid 41556 | gpt-4o-mini |

## Phase dependency graph

```mermaid
flowchart LR
clarify --> design
design --> code
code --> test
test --> deploy
deploy --> iterate
```

129 changes: 129 additions & 0 deletions docs/runs/2026-07-24/snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
{
"run_id": "4a9b6b809e4b4cad8b03a04e687c3b25",
"status": "done",
"phases": [
{
"name": "clarify",
"status": "complete",
"gate": "approved"
},
{
"name": "code",
"status": "complete",
"gate": "none"
},
{
"name": "deploy",
"status": "complete",
"gate": "none"
},
{
"name": "design",
"status": "complete",
"gate": "approved"
},
{
"name": "iterate",
"status": "complete",
"gate": "none"
},
{
"name": "test",
"status": "complete",
"gate": "none"
}
],
"tasks": [
{
"agent_id": "clarifying_pm",
"phase": "clarify",
"status": "done",
"owner": "w0",
"model": "claude-3-5-sonnet-20241022"
},
{
"agent_id": "solution_architect",
"phase": "design",
"status": "done",
"owner": "w0",
"model": "claude-3-5-sonnet-20241022"
},
{
"agent_id": "tech_lead",
"phase": "design",
"status": "done",
"owner": "w0",
"model": "gpt-4o"
},
{
"agent_id": "uiux_designer",
"phase": "design",
"status": "done",
"owner": "w0",
"model": "claude-3-5-sonnet-20241022"
},
{
"agent_id": "database",
"phase": "code",
"status": "done",
"owner": "w0",
"model": "gpt-4o"
},
{
"agent_id": "backend",
"phase": "code",
"status": "done",
"owner": "w0",
"model": "claude-3-5-sonnet-20241022"
},
{
"agent_id": "frontend",
"phase": "code",
"status": "done",
"owner": "w0",
"model": "claude-3-5-sonnet-20241022"
},
{
"agent_id": "ai_ml",
"phase": "code",
"status": "done",
"owner": "w2",
"model": "claude-3-5-sonnet-20241022"
},
{
"agent_id": "qa_test",
"phase": "test",
"status": "done",
"owner": "w0",
"model": "gpt-4o-mini"
},
{
"agent_id": "security",
"phase": "test",
"status": "done",
"owner": "w0",
"model": "claude-3-5-haiku-20241022"
},
{
"agent_id": "devops",
"phase": "deploy",
"status": "done",
"owner": "w0",
"model": "gpt-4o-mini"
},
{
"agent_id": "technical_writer",
"phase": "deploy",
"status": "done",
"owner": "w0",
"model": "gpt-4o-mini"
},
{
"agent_id": "delivery_summarizer",
"phase": "iterate",
"status": "done",
"owner": "w0",
"model": "gpt-4o-mini"
}
]
}
Loading
Loading