Progi is an MCP-native workflow engine. Key terms:
- workflow: a reusable template of ordered steps; defines a repeatable, reused across many tasks
- step: one unit of work in a workflow; has a playbook, an input spec, and an output spec
- playbook: markdown attached to a step; the agent reads it via
start_or_continue_taskand follows it - task: a single execution of a workflow; progresses through steps one at a time with lifecycle
todo→in_progress→done
Two interfaces over one SQLite DB:
- Progi MCP server (FastMCP, stdio) — the work loop runs here, inside the user's harness.
- Progi Monitoring (FastAPI + Jinja + Alpine/AlpineAJAX) — a web app for tracking tasks and reviewing workflows.
All database access goes through progi/db.py. MCP tools and web routes
are thin adapters that call named functions there — they never write SQL. This is
what keeps LLM-driven and human-driven edits behaviorally identical.
| File | Role |
|---|---|
progi/db.py |
Schema (SQLAlchemy Core) + all queries, mutations, and state-transition logic |
progi/mcp_server.py |
@mcp.tool wrappers (work loop + workflow authoring) |
progi/web/app.py |
FastAPI routes → Jinja partials |
progi/prompts/ |
Pass 1 / Pass 2 authoring system prompts (served by tools) |
progi/seed.py |
"Blog Post" workflow + sample task (idempotent) |
tests/test_db.py |
DB roundtrip, full work loop, authoring |
Templates live in progi/web/templates/. The UI stack is:
- Tailwind CSS v4 (compiled by the standalone CLI via
just build) - Alpine.js v3 + Alpine AJAX — vendored to
static/vendor/byjust vendorize - PenguinUI — a copy-paste component library; no install needed
Browse components at https://www.penguinui.com/components, pick what you need, and paste the HTML directly into a template. There is nothing to download or import — PenguinUI components are just Tailwind + Alpine markup.
Key conventions when adding components:
- Use
<article>as the root for card-style components (PenguinUI's convention). - Use the existing design tokens from
frontend/input.css(bg-surface-*,text-primary,text-accent,border-subtle, etc.) instead of raw colors. - Interactive components that require Alpine plugins (Collapse, Focus, Mask) must
vendorize those scripts via
just vendorizeand load them before Alpine core inbase.html(plugins must register before Alpine initializes). - Web routes return HTML partials for AJAX; the swapped element's
idmust match thex-targetattribute on the trigger element.
Pages live in progi/web/templates/pages/. Each page template extends a
variable base template so the same file works for both full page loads and
Alpine AJAX partial swaps — no separate *_content.html partial needed.
1. Template (pages/mypage.html):
{% extends base_template %}
{% block content %}
<!-- page content here -->
{% endblock %}If the page needs full-bleed layout (no max-width wrapper), also override
outer_class as an empty block (see pages/workflows.html).
2. Router (routers/mypage.py):
from . import base_template
@router.get("/mypage", response_class=HTMLResponse)
def mypage(request: Request):
ctx = {"base_template": base_template(request), ...}
return _templates(request).TemplateResponse(request, "pages/mypage.html", ctx)Always pass "base_template": base_template(request) in the context.
base_template() returns "base_partial.html" for Alpine AJAX requests and
"base.html" for direct browser navigation. base_partial.html wraps the
content in <div id="page-content"> so Alpine AJAX can find its swap target.
3. Nav link (base.html):
<a href="/mypage"
x-target.push="page-content"
:class="{ 'text-primary': page === '/mypage', 'text-faint hover:text-muted': page !== '/mypage' }"
class="text-xs font-medium transition-colors duration-150"
x-cloak>
My Page
</a>4. Register the router (web/app.py):
from .routers import board, mypage, workflows
app.include_router(mypage.router)- No SQL outside
db.py. Add a named function; wrap writes inwith engine.begin(). - Never
print()/ write to stdout anywhere reachable from the MCP process — stdout is the MCP protocol channel. Log to stderr vialogging_setup. - Web returns HTML partials for AJAX (not JSON); the swapped element's
idmust match the trigger'sx-target. input_spec/output_spec/input_data/outputaresa.JSONcolumns — pass and receive plain dicts, no manualjson.dumps.- Keep the web UI localhost-only (unauthenticated DB viewer).
Use plain fetch + Alpine reactive state (in app.js) when:
- The backend returns no meaningful body (e.g. 204 on DELETE)
- The UI update is a local state mutation — remove an item, clear a field, reset a flag
- No new HTML needs to come from the server
Use Alpine AJAX / HTML partials when:
- The server is the source of truth for what to render (e.g. a detail panel, a refreshed list)
- The response is the UI update — swapping in rendered HTML is simpler than rebuilding it in JS
Alpine AJAX intercepts clicks on <a href> elements and form submits — not bare button clicks. For any clickable element that should trigger an AJAX swap, wrap it in an <a> tag with href pointing to the endpoint and x-target naming the element ID to replace:
<a href="/board" x-target="board" class="...">
Refresh
</a>If you also need to send a payload to backend, use <FORM> instead of <a>.
Omit .push on x-target when you don't want the click to update the browser history (e.g. a refresh action vs. navigation).
Example of the fetch pattern (workflow delete in app.js):
const resp = await fetch(`/workflows/${id}`, { method: 'DELETE' });
if (resp.ok) {
this.workflows = this.workflows.filter(wf => wf.id !== id);
// clear canvas, reset active state, update URL as needed
}just ships in the dev group, so prefix recipes with uv run if you don't have a system just.
uv sync --extra dev # deps + just
uv run just install # + vendored JS, Tailwind CLI
uv run just build # compile web/static/style.css
uv run just dev # MCP server over SSE (connect via http://127.0.0.1:8001/sse)
uv run just migrate "msg" && uv run just upgrade # Alembic migration
uv run python -m pytest # tests (do NOT use `uv run pytest` — a system pytest may shadow it)
uv run ruff check progi # lintprogi (MCP + web), progi --no-web (MCP only), progi-web (web only).
Config via env: PROGI_DB_PATH, PROGI_WEB_HOST, PROGI_WEB_PORT, PROGI_NO_WEB.
Commit messages must follow Conventional Commits: feat:, fix:, docs:, chore:, refactor:, test:, ci:. Use feat!: for breaking changes. Release Please reads these to generate the changelog and determine the version bump.
Keep the commit messages short.