Single-container AI coach with agent API endpoints and UI polish

- Merge opencode-serve into the web container via entrypoint script
- Add /api/agent/* JSON endpoints for workouts, sets, checkins
- Rewrite fitness-trainer.md to use API instead of markdown files
- Pass recent workouts and check-ins as chat context to the coach
- Show current training phase on dashboard
- Clarify check-ins as morning check-ins (calories/steps = yesterday)
- Add NixOS deployment section to README
- Make all check-in fields explicitly optional in UI
This commit is contained in:
Jacob Hinkle 2026-06-29 10:50:01 -04:00
parent bfab3e8f01
commit 5584022a23
16 changed files with 419 additions and 114 deletions

View File

@ -12,7 +12,7 @@ Replaces the fitness-agent markdown-based training repo.
- **FastAPI** — async Python web framework
- **Jinja2 + Pico.css** — server-rendered templates with minimal CSS framework (CDN)
- **SQLite + SQLAlchemy 2.0 async** — database with aiosqlite driver
- **opencode serve** — AI coach service (Big Pickle model, free). Runs as a separate process/container.
- **opencode serve** — AI coach service (Big Pickle model, free). Runs alongside uvicorn in the same container, managed by a shell entrypoint.
## Key Files
- `app/main.py` — App factory, route registration, lifespan
@ -20,6 +20,7 @@ Replaces the fitness-agent markdown-based training repo.
- `app/auth.py` — Password hashing, session management, `get_current_user` dependency
- `app/models/` — SQLAlchemy ORM models
- `app/routers/` — Route handlers (one per feature)
- `app/routers/agent_api.py` — JSON API for the AI coach to create workouts/check-ins
- `scripts/schema.py` — DB initialization
- `scripts/seed.py` — Seed data (exercises, phases)
- `opencode/fitness-trainer.md` — Agent config for AI coach (copied from fitness-agent)
@ -38,13 +39,14 @@ uv run uvicorn app.main:app --reload # Dev server on :8000
- SSE streaming for chat responses
- Pico.css from CDN (no build step)
- Chat messages stored in DB per session for history
- Docker Compose for deployment (opencode serve service commented out until ready)
- Docker Compose for deployment (opencode serve runs as background process in same container)
## Next Steps / TODOs
1. Seed exercises and phases (done via `python scripts/seed.py`)
2. Add exercise progress chart (matplotlib or chart.js)
3. Enable opencode serve integration (uncomment docker-compose service)
4. Migrate existing markdown logs from fitness-agent repo into DB
5. Migrate Juggernaut training xlsx data into DB
6. Add calendar view for training history
7. PWA manifest + service worker for offline-capable mobile use
3. Enable opencode serve integration (Dockerfile + entrypoint done, single-container approach)
4. Add agent API for DB writes (endpoints + agent config done)
5. Migrate existing markdown logs from fitness-agent repo into DB
6. Migrate Juggernaut training xlsx data into DB
7. Add calendar view for training history
8. Update `fitness-trainer.md` agent config to work with DB-backed context instead of markdown files (done)

View File

@ -1,12 +1,20 @@
FROM node:22-alpine AS opencode-build
RUN apk add --no-cache curl && \
curl -fsSL https://opencode.ai/install | sh
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir uv
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN uv pip install --system --no-cache-dir -r requirements.txt
COPY --from=opencode-build /usr/local/bin/opencode /usr/local/bin/opencode
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
ENV OPENCODE_SERVE_URL=http://127.0.0.1:4096
ENV AGENT_API_KEY=dev-agent-key-change-in-production
RUN mkdir -p /root/.config && \
ln -s /app/opencode /root/.config/opencode && \
printf '#!/bin/sh\nopencode serve --host 127.0.0.1 --port 4096 &\nsleep 1\nexec uvicorn app.main:app --host 0.0.0.0 --port 8000\n' > /entrypoint.sh && chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

View File

@ -12,6 +12,7 @@ Track workouts, log daily check-ins, explore exercise history, and chat with an
- **AI Coach** — Chat interface backed by opencode (Big Pickle model, free)
- **Multi-user** — Login-based, each user has independent data
- **Calendar view** — See your training history at a glance
- **NixOS-ready** — Docker-based deploy with provided NixOS module
## Quick Start
@ -35,6 +36,72 @@ Open http://localhost:8000, register a user, and you're ready.
docker compose up -d
```
## NixOS Deployment
A single container runs both the web app and the AI coach (opencode-serve) together, sharing the SQLite database on the same filesystem.
### 1. Build image
```bash
docker build -t fitness-web:latest .
```
### 2. NixOS module
Add `machines/cj/fitness-web.nix` to your nix_config:
```nix
{ serverIP, serverIP6 }: {
fitness-web = {
image = "fitness-web:latest";
ports = [ "8688:8000" ];
environment = {
TZ = "America/New_York";
SESSION_SECRET = "change-me-in-production";
};
volumes = [
"/serverdata/fitness-web/data:/app/data"
"/serverdata/fitness-web/opencode:/root/.config/opencode"
];
};
}
```
Wire it into `configuration.nix`:
```nix
virtualisation.oci-containers.containers = let
ips = { ... };
in {
# ... existing containers ...
fitness-web = (import ./fitness-web.nix ips).fitness-web;
};
```
### 3. Nginx reverse proxy
In `nginx.nix`, add a vhost:
```nix
"fitness.jhink.org" = simpleProxy 8688;
```
### 4. Firewall
Add to `allowedTCPPorts` in `firewall.nix`:
```nix
8688 # fitness-web
```
### 5. Deploy
```bash
nixos-rebuild switch --flake .#cj
```
Then access at https://fitness.jhink.org (or your chosen domain).
## Architecture
```

View File

@ -6,3 +6,4 @@ DATA_DIR = Path(os.environ.get("DATA_DIR", PROJECT_ROOT / "data"))
DATABASE_PATH = str(DATA_DIR / "fitness.db")
SESSION_SECRET = os.environ.get("SESSION_SECRET", "dev-secret-change-in-production")
OPENCODE_SERVE_URL = os.environ.get("OPENCODE_SERVE_URL", "http://localhost:4096")
AGENT_API_KEY = os.environ.get("AGENT_API_KEY", "")

View File

@ -4,7 +4,7 @@ from fastapi.staticfiles import StaticFiles
from contextlib import asynccontextmanager
from app.config import DATA_DIR
from app.routers import auth, dashboard, workouts, exercises, checkins, profile, chat
from app.routers import auth, dashboard, workouts, exercises, checkins, profile, chat, agent_api
from scripts.schema import init_db
@ -27,3 +27,4 @@ app.include_router(exercises.router)
app.include_router(checkins.router)
app.include_router(profile.router)
app.include_router(chat.router)
app.include_router(agent_api.router)

143
app/routers/agent_api.py Normal file
View File

@ -0,0 +1,143 @@
from fastapi import APIRouter, Header, HTTPException, Depends
from pydantic import BaseModel
from sqlalchemy import select
from app.config import AGENT_API_KEY
from app.models.base import async_session
from app.models.user import User
from app.models.workout import Workout, WorkoutSet
from app.models.checkin import Checkin
router = APIRouter(prefix="/api/agent", tags=["agent"])
async def verify_agent(x_api_key: str = Header("")):
if AGENT_API_KEY and x_api_key != AGENT_API_KEY:
raise HTTPException(status_code=403, detail="invalid api key")
class CreateWorkoutRequest(BaseModel):
username: str
name: str
date: str
phase_id: int | None = None
notes: str = ""
class CreateSetRequest(BaseModel):
exercise: str
set_number: int
reps: int | None = None
weight: float | None = None
rpe: float | None = None
notes: str = ""
class CreateCheckinRequest(BaseModel):
username: str
date: str
feeling: str | None = None
weight_lb: float | None = None
calories: int | None = None
steps: int | None = None
sleep_hours: float | None = None
notes: str = ""
@router.post("/workouts")
async def agent_create_workout(
body: CreateWorkoutRequest,
_=Depends(verify_agent),
):
async with async_session() as session:
result = await session.execute(
select(User).where(User.username == body.username)
)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="user not found")
workout = Workout(
user_id=user.id,
phase_id=body.phase_id,
name=body.name,
date=body.date,
notes=body.notes,
status="plan",
)
session.add(workout)
await session.commit()
await session.refresh(workout)
return {"id": workout.id, "name": workout.name, "date": workout.date, "status": workout.status}
@router.post("/workouts/{workout_id}/sets")
async def agent_add_set(
workout_id: int,
body: CreateSetRequest,
_=Depends(verify_agent),
):
async with async_session() as session:
ws = WorkoutSet(
workout_id=workout_id,
exercise=body.exercise,
set_number=body.set_number,
reps=body.reps,
weight=body.weight,
rpe=body.rpe,
notes=body.notes,
)
session.add(ws)
await session.commit()
await session.refresh(ws)
return {"id": ws.id, "exercise": ws.exercise, "set_number": ws.set_number}
@router.put("/workouts/{workout_id}/complete")
async def agent_complete_workout(
workout_id: int,
_=Depends(verify_agent),
):
async with async_session() as session:
result = await session.execute(
select(Workout).where(Workout.id == workout_id)
)
workout = result.scalar_one_or_none()
if not workout:
raise HTTPException(status_code=404, detail="workout not found")
workout.status = "complete"
await session.commit()
return {"id": workout.id, "status": "complete"}
@router.post("/checkins")
async def agent_create_checkin(
body: CreateCheckinRequest,
_=Depends(verify_agent),
):
async with async_session() as session:
result = await session.execute(
select(User).where(User.username == body.username)
)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="user not found")
checkin = Checkin(
user_id=user.id,
date=body.date,
feeling=body.feeling,
weight_lb=body.weight_lb,
calories=body.calories,
steps=body.steps,
sleep_hours=body.sleep_hours,
notes=body.notes,
)
session.add(checkin)
await session.commit()
await session.refresh(checkin)
return {"id": checkin.id, "date": checkin.date}

View File

@ -8,6 +8,8 @@ from sqlalchemy import select, desc
from app.models.base import async_session
from app.models.user import User
from app.models.chat import ChatMessage
from app.models.workout import Workout
from app.models.checkin import Checkin
from app.auth import get_current_user
from app.services.opencode_proxy import query_opencode
@ -48,12 +50,55 @@ async def chat_send(
message: str = Form(),
):
session_id = request.cookies.get("chat_session_id") or str(uuid.uuid4())
async with async_session() as session:
result = await session.execute(
select(Workout)
.where(Workout.user_id == user.id)
.order_by(desc(Workout.date))
.limit(5)
)
recent_workouts = result.scalars().all()
result = await session.execute(
select(Checkin)
.where(Checkin.user_id == user.id)
.order_by(desc(Checkin.date))
.limit(5)
)
recent_checkins = result.scalars().all()
workout_lines = []
for w in recent_workouts:
workout_lines.append(f" {w.date}{w.name} ({w.status})")
checkin_lines = []
for c in recent_checkins:
parts = []
if c.feeling:
parts.append(f"feeling={c.feeling}")
if c.weight_lb:
parts.append(f"weight={c.weight_lb}lb")
if c.calories:
parts.append(f"cal(yesterday)={c.calories}")
if c.steps:
parts.append(f"steps(yesterday)={c.steps}")
if c.sleep_hours:
parts.append(f"sleep={c.sleep_hours}h")
checkin_lines.append(f" {c.date}{' | '.join(parts)}")
user_context = (
f"Username: {user.username}. "
f"Weight: {user.weight_lb} lb. "
f"Goals: {user.goals or 'Not specified'}. "
f"Medical: {user.medical_notes or 'None'}"
f"Equipment: {user.equipment or 'Not specified'}. "
f"Medical: {user.medical_notes or 'None'}. "
f"Calorie goal: {user.calorie_goal or 'Not set'}. "
f"Step goal: {user.step_goal or 'Not set'}. "
)
if recent_workouts:
user_context += "Recent workouts:\n" + "\n".join(workout_lines) + ". "
if recent_checkins:
user_context += "Recent check-ins:\n" + "\n".join(checkin_lines) + ". "
async def stream():
async with async_session() as session:

View File

@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import date, datetime, timezone
from fastapi import APIRouter, Request, Depends, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
@ -32,6 +32,7 @@ async def checkin_list(request: Request, user: User = Depends(get_current_user))
async def new_checkin_page(request: Request, user: User = Depends(get_current_user)):
return templates.TemplateResponse(request, "checkin_new.html", {
"user": user,
"today": date.today().isoformat(),
})

View File

@ -5,7 +5,7 @@ from sqlalchemy import select, desc
from app.models.base import async_session
from app.models.user import User
from app.models.workout import Workout
from app.models.workout import Phase, Workout
from app.models.checkin import Checkin
from app.auth import get_current_user
@ -38,8 +38,14 @@ async def dashboard(request: Request, user: User = Depends(get_current_user)):
)
latest_checkin = result.scalar_one_or_none()
result = await session.execute(
select(Phase).order_by(desc(Phase.start_date)).limit(1)
)
current_phase = result.scalar_one_or_none()
return templates.TemplateResponse(request, "dashboard.html", {
"user": user,
"recent_workouts": recent_workouts,
"latest_checkin": latest_checkin,
"current_phase": current_phase,
})

View File

@ -89,10 +89,16 @@ async def workout_detail(
)
sets = result.scalars().all()
phase = None
if workout.phase_id:
result = await session.execute(select(Phase).where(Phase.id == workout.phase_id))
phase = result.scalar_one_or_none()
return templates.TemplateResponse(request, "workout_detail.html", {
"user": user,
"workout": workout,
"sets": sets,
"phase": phase,
})

View File

@ -1,37 +1,45 @@
{% extends "base.html" %}
{% block title %}New Check-in{% endblock %}
{% block title %}Morning Check-in{% endblock %}
{% block content %}
<h1>New Check-in</h1>
<h1>Morning Check-in</h1>
<p><small>All fields are optional — fill in whatever you have each morning.</small></p>
<form method="post" action="/checkins/new">
<label>
Date
<input type="date" name="date" required>
<input type="date" name="date" value="{{ today }}" required>
<small>Morning of this check-in</small>
</label>
<label>
Feeling
<input type="text" name="feeling" placeholder="e.g., Good, Tired, Sore">
<small>How are you feeling this morning?</small>
</label>
<div class="grid">
<label>
Weight (lb)
<input type="number" name="weight_lb" step="0.1">
<small>Morning weight</small>
</label>
<label>
Calories
<input type="number" name="calories">
<small>Yesterday's total</small>
</label>
<label>
Steps
<input type="number" name="steps">
<small>Yesterday's total</small>
</label>
<label>
Sleep (hours)
<input type="number" name="sleep_hours" step="0.5">
<small>Last night</small>
</label>
</div>
<label>
Notes
<textarea name="notes" rows="3"></textarea>
<textarea name="notes" rows="3" placeholder="Anything else to note?"></textarea>
<small>Optional notes about yesterday or today</small>
</label>
<button type="submit">Save Check-in</button>
</form>

View File

@ -1,7 +1,8 @@
{% extends "base.html" %}
{% block title %}Check-ins{% endblock %}
{% block content %}
<h1>Check-ins</h1>
<h1>Morning Check-ins</h1>
<p><small>Log what's on your mind and what happened yesterday — all fields optional.</small></p>
<a href="/checkins/new" role="button">New Check-in</a>
{% if checkins %}
@ -11,8 +12,8 @@
<th>Date</th>
<th>Feeling</th>
<th>Weight</th>
<th>Calories</th>
<th>Steps</th>
<th>Calories <small>(yesterday)</small></th>
<th>Steps <small>(yesterday)</small></th>
<th>Sleep</th>
</tr>
</thead>
@ -30,6 +31,6 @@
</tbody>
</table>
{% else %}
<p>No check-ins yet.</p>
<p>No check-ins yet. <a href="/checkins/new">Start your first morning check-in</a>.</p>
{% endif %}
{% endblock %}

View File

@ -18,11 +18,20 @@
</article>
</div>
{% if current_phase %}
<article>
<h3>Current Phase: {{ current_phase.name }}</h3>
<p>{{ current_phase.description }}</p>
{% if current_phase.notes %}<p><em>{{ current_phase.notes }}</em></p>{% endif %}
{% if current_phase.start_date %}<small>Started {{ current_phase.start_date }}</small>{% endif %}
</article>
{% endif %}
{% if latest_checkin %}
<article>
<h3>Latest Check-in</h3>
<h3>Latest Morning Check-in</h3>
<p>{{ latest_checkin.date }} — {{ latest_checkin.feeling or 'No feeling recorded' }}</p>
<p>Weight: {{ latest_checkin.weight_lb or '—' }} lb | Calories: {{ latest_checkin.calories or '—' }} | Steps: {{ latest_checkin.steps or '—' }} | Sleep: {{ latest_checkin.sleep_hours or '—' }}h</p>
<p>Weight: {{ latest_checkin.weight_lb or '—' }} lb | Calories <small>(yesterday)</small>: {{ latest_checkin.calories or '—' }} | Steps <small>(yesterday)</small>: {{ latest_checkin.steps or '—' }} | Sleep: {{ latest_checkin.sleep_hours or '—' }}h</p>
</article>
{% endif %}
@ -49,12 +58,12 @@
</tbody>
</table>
{% else %}
<p>No workouts yet. <a href="/workouts/new">Plan one now</a>.</p>
<p>No workouts yet. Ask the <a href="/chat">AI Coach</a> to plan one.</p>
{% endif %}
<div class="grid">
<a href="/workouts/new" role="button" class="secondary">New Workout</a>
<a href="/checkins/new" role="button" class="secondary">New Check-in</a>
<a href="/chat" role="button" class="secondary">AI Coach</a>
<a href="/workouts" role="button" class="secondary">All Workouts</a>
<a href="/checkins" role="button" class="secondary">All Check-ins</a>
</div>
{% endblock %}

View File

@ -3,6 +3,9 @@
{% block content %}
<h1>{{ workout.name }}</h1>
<p><strong>Date:</strong> {{ workout.date }} | <strong>Status:</strong> {{ workout.status }}</p>
{% if phase %}
<p><strong>Phase:</strong> {{ phase.name }}</p>
{% endif %}
{% if workout.notes %}
<p>{{ workout.notes }}</p>
{% endif %}

View File

@ -1,21 +1,11 @@
services:
web:
fitness-web:
build: .
ports:
- "8000:8000"
volumes:
- ./data:/app/data
- ./opencode:/root/.config/opencode
environment:
- DATABASE_PATH=/app/data/fitness.db
- SESSION_SECRET=change-me-in-production
- OPENCODE_SERVE_URL=http://opencode-serve:4096
# opencode-serve:
# image: opencode:latest
# entrypoint: ["opencode", "serve", "--host", "0.0.0.0", "--port", "4096"]
# ports:
# - "4096:4096"
# volumes:
# - ./opencode:/root/.config/opencode
# environment:
# - OPENCODE_SERVER_PASSWORD=your-password-here
- AGENT_API_KEY=dev-agent-key-change-in-production

View File

@ -1,23 +1,28 @@
---
description: >
Your personal fitness trainer. Plans workouts, tracks progress, adapts to how
you're feeling, and logs everything to markdown. Use this agent for daily
check-ins, workout reviews, and programming discussions.
you're feeling, and logs everything via the web app API. Use this agent for
daily check-ins, workout reviews, and programming discussions.
mode: primary
color: "#4ade80"
---
You are an experienced, adaptable personal trainer. Your client (the user) has
provided their equipment, goals, and medical history in `inputs/`. Their
historical lifting data is in `inputs/Juggernaut training.xlsx`.
provided their equipment, goals, and medical history through the fitness web
app. Their training data is stored in the app's database — past workouts,
daily check-ins, exercise history, and current stats are all available from
there.
Your job is to guide them through their fitness journey. Be encouraging but
honest. You are their single point of contact for training chat.
## Guidelines
- Always consider their medical history (especially the distal radius fracture)
and available equipment when programming
- Always consider their medical history (especially the distal radius
fracture) and available equipment when programming
- The web app passes your client's current stats (weight, goals, medical
notes, recent workouts, recent check-ins) alongside each message. Use this
context to understand their situation.
- **Periodic research check:** Roughly once per week (or every few check-ins),
do a brief web search on current best practices relevant to their situation —
e.g., distal radius fracture return-to-training, tendinopathy prevention,
@ -30,9 +35,14 @@ honest. You are their single point of contact for training chat.
- If they're interested in a specific program methodology (Juggernaut,
Stronglifts 5x5, etc.), use their training history to pick up where they left
off or start a new cycle
- If they want something new, design intelligently using the programming
principles in the `fitness-workout` skill
- **Left hand grip limitation:** Client is doing grip/rehab exercises 3x/day with their PT. In our workouts, minimize left hand grip demand (use straps for any pulling, avoid goblet squats, keep DB loads light). Check in early next week (Mon/Tue) about whether they feel ready to add more grip work back into sessions — they have weekly PT appointments each Wednesday for the next 3 weeks and will update accordingly.
- If they want something new, design intelligently using sound programming
principles
- **Left hand grip limitation:** Client is doing grip/rehab exercises 3x/day
with their PT. In our workouts, minimize left hand grip demand (use straps
for any pulling, avoid goblet squats, keep DB loads light). Check in early
next week (Mon/Tue) about whether they feel ready to add more grip work back
into sessions — they have weekly PT appointments each Wednesday for the next
3 weeks and will update accordingly.
- **During the reintroduction period (weeks 1-4 after a layoff or injury),**
always program movements with **limited range of motion** — avoid end-range
positions (stretch at the bottom/top of any lift) for all exercises. This
@ -40,70 +50,74 @@ honest. You are their single point of contact for training chat.
Mid-range movements only (e.g., Bulgarian split squats instead of deep
squats, landmine press instead of full ROM OHP, step-ups instead of deep
lunges). Apply this to squat, hinge, push, pull, and core movements alike.
- High-level training context lives in `plans/`. Read the relevant plan before
designing individual workouts and keep it up to date as context evolves
(progressing phases, new constraints, notable decisions). The plan describes
current phase, timeline, constraints, and progression criteria. Workout
Analysis sections should briefly reference the broader plan to show how the
session fits.
- Logs are written to `logs/workouts/` and `logs/checkins/`. Use
`grep`/`read`/`glob` to search past logs when they ask questions like "when
was the last time I did farmer's carry?"
## Check-in Workflow
## API — Creating Workouts and Check-ins
When they want to check in, follow this structure:
You write workout plans and check-in logs directly to the database via the
web app's internal API. The API key is in the environment variable
`AGENT_API_KEY`. All endpoints are at `http://localhost:8000/api/agent/`.
Use `curl` with the API key header:
```bash
curl -s http://localhost:8000/api/agent/workouts \
-H "X-API-Key: $AGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"username": "jacob", "name": "Upper Body A", "date": "2026-06-30", "notes": "..."}'
```
### Endpoints
**Create a workout plan:**
```
POST /api/agent/workouts
Body: { username, name, date, phase_id?, notes? }
```
**Add a set to a workout:**
```
POST /api/agent/workouts/{id}/sets
Body: { exercise, set_number, reps?, weight?, rpe?, notes? }
```
**Mark a workout complete:**
```
PUT /api/agent/workouts/{id}/complete
```
**Create a check-in log:**
```
POST /api/agent/checkins
Body: { username, date, feeling?, weight_lb?, calories?, steps?, sleep_hours?, notes? }
```
Always use the username from the context provided with each message.
## Check-in Flow
When the user wants to check in or discuss their training:
1. **Status check** — Ask how they're feeling: soreness, energy, injuries,
sleep, weight, motivation
2. **Nutrition & Steps** — Ask if they'd like to review/adjust their LoseIt!
calorie goal, and if they can report their average daily steps from their
Google Pixel phone. Log these numbers in the check-in entry. If they're
comfortable, suggest a small calorie goal adjustment based on their weight
trend and activity level.
3. **Review** — Check the last planned workout log. Did they complete it? How
did each exercise feel? Update the log with results if needed
4. **Adjust** — Based on feedback + programming guidelines + history, adjust
the next session (weight, volume, exercise selection, or rest day)
5. **Plan** — Write a new workout plan to `logs/workouts/<YYYY-MM-DD>-<slug>.md`
with exercises, sets, reps, weights, and any notes. Present it to them
6. **Log** — Write a brief check-in entry to `logs/checkins/<YYYY-MM-DD>-checkin.md`
summarizing the conversation and any decisions made. This includes calorie
goal, steps, weight, and workout results.
7. **Commit** — After logging, use `git add logs/` and `git commit` with a
descriptive message summarizing the check-in, any plan changes, and key
metrics (weight, calories, steps). Do NOT push unless asked.
sleep, weight, motivation. Reference trends from the context.
2. **Nutrition & Steps** — Ask if they'd like to review or adjust their
calorie goal and daily steps. Suggest adjustments based on weight trend
and activity level.
3. **Review** — Look at their recent workouts (from context). Did they
complete them? How did each exercise feel?
4. **Adjust** — Based on feedback + programming guidelines + history, suggest
adjustments for the next session (weight, volume, exercise selection, or
rest day)
5. **Plan & Save** — Design the next workout with exercises, sets, reps,
weights, and notes. **Create it in the database** using the API
(`POST /api/agent/workouts` followed by `POST /api/agent/workouts/{id}/sets`
for each exercise). Present the plan to the user.
6. **Log the check-in** — After the conversation wraps, **create a check-in
entry** via the API (`POST /api/agent/checkins`) summarizing the key
decisions, metrics, and any adjustments made.
## Log format
## Session Analysis
Workout plan log:
```markdown
# Workout: <Name>
**Date:** <YYYY-MM-DD>
**Program:** <program name or "Custom">
**Status:** Plan
## Analysis
<brief section listing muscles targeted, the overall goal for the session, and how it fits into the broader plan (reference `plans/`). Keep it concise 3-5 sentences max. Update if the plan changes mid-session due to pain or feedback.>
## Exercises
- <Exercise>: <sets>x<reps> @ <weight> <notes>
- ...
## Notes
<context for the session>
```
After completion, update the **Status** to "Complete" and add results inline:
```markdown
- Bench Press: 3x5 @ 185lb — completed (RPE 8)
```
Check-in log:
```markdown
# Check-in: <YYYY-MM-DD>
**Feeling:** <summary>
**Review:** <what was reviewed>
**Adjustments:** <changes made>
**Next session:** <reference to workout log>
```
When discussing a specific workout, briefly note muscles targeted, the
overall goal for the session, and how it fits into their broader training
context. Use the workout history from the context to reference progression
and past performance.