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
16 changes: 10 additions & 6 deletions .cursor/rules/project-conventions.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ There is no bare `python` or `python3` in `$PATH` that has project dependencies.

## Architecture
- `frontend/` — React SPA (Vite + TypeScript + Tailwind + shadcn/ui)
- `infra/` — SST IaC: DynamoDB table, Hono API Lambda (TypeScript), Watchdog Lambda, Digest Lambda (daily cron → Discord), Autopilot Lambda (daily plan proposal)
- `infra/` — SST IaC: DynamoDB table, Hono API Lambda (TypeScript), Watchdog Lambda, Digest Lambda (daily cron → Discord), Autopilot Lambda (hourly tick; daily-mode projects filtered to 07 UTC)
- `src/web.py` — FastAPI backend: JSON API + SPA serving (EC2, being replaced by Lambda API)
- `src/bot.py` — Discord bot with slash commands (discord.py)
- `src/task_store.py` — Shared task types (`Task`, enums, `Comment`)
Expand All @@ -35,7 +35,7 @@ There is no bare `python` or `python3` in `$PATH` that has project dependencies.
- `run_task.py` — Runner entry point (runs a single task or reply)
- `run_task.py --directive <project_id> <directive_sk>` — Directive decomposition entry point (called by SSM)
- `run_task.py --daily-cycle <project_id>` — Daily observe/reflect/propose cycle entry point (called by Metrics Lambda via SSM)
- `run_task.py --propose-plan <project_id> [--regenerate]` — Autopilot plan proposal entry point (called by Autopilot Lambda via SSM)
- `run_task.py --propose-plan <project_id> [--regenerate] [--plan-suffix <id>]` — Autopilot plan proposal entry point (called by Autopilot Lambda via SSM; `plan-suffix` for regenerating a specific `PLAN#` row in continuous mode)
- `run_poller.py` — Polling daemon: checks DynamoDB every `POLL_INTERVAL` seconds for pending tasks and reply-pending comments, spawns `run_task.py` subprocesses (systemd service)
- `run_heartbeat.py` — Heartbeat dispatcher entry point (called by cron)

Expand Down Expand Up @@ -141,7 +141,7 @@ Projects are long-lived entities (`pk=PROJECT#<id>`, `sk=PROJECT`) with a title,
- `project-list-index` (`proj_status`, `project_updated`) — list projects by status

### Hono project routes (`infra/packages/api/src/routes/projects.ts`)
`GET /api/projects`, `GET /api/projects/:id`, `POST /api/projects`, `PATCH /api/projects/:id`, `DELETE /api/projects/:id`, `POST /api/projects/:id/directive`, `GET /api/projects/:id/snapshots`, `GET /api/projects/:id/proposals`, `PATCH /api/projects/:id/proposals/:propSk`, `GET /api/projects/:id/requests`, `PATCH /api/projects/:id/requests/:reqSk`, `GET /api/projects/:id/plans`, `GET /api/projects/:id/plans/:date`, `PATCH /api/projects/:id/plans/:date`, `POST /api/projects/:id/plans/:date/approve`, `POST /api/projects/:id/plans/:date/regenerate`
`GET /api/projects`, `GET /api/projects/:id`, `POST /api/projects`, `PATCH /api/projects/:id`, `DELETE /api/projects/:id`, `POST /api/projects/:id/directive`, `GET /api/projects/:id/snapshots`, `GET /api/projects/:id/proposals`, `PATCH /api/projects/:id/proposals/:propSk`, `GET /api/projects/:id/requests`, `PATCH /api/projects/:id/requests/:reqSk`, `GET /api/projects/:id/plans`, `GET /api/projects/:id/plans/:planId`, `PATCH /api/projects/:id/plans/:planId`, `POST /api/projects/:id/plans/:planId/approve`, `POST /api/projects/:id/plans/:planId/regenerate`, `POST /api/projects/:id/cycle/start`, `POST /api/projects/:id/cycle/stop`, `POST /api/projects/:id/cycle/review`

### Digest Lambda
`infra/packages/digest/` — scheduled Lambda (cron `0 14 * * ? *`) posts a daily summary of task activity and project progress to Discord via webhook. Includes KPI briefings, reflections, and pending proposals for projects with KPIs.
Expand All @@ -150,7 +150,7 @@ Projects are long-lived entities (`pk=PROJECT#<id>`, `sk=PROJECT`) with a title,
`infra/packages/metrics/` — scheduled Lambda (cron `0 6 * * ? *`) fetches metrics for active projects with KPIs. Adapters: PageSpeed Insights (free, no auth), GitHub (existing token). Writes `SNAPSHOT#<date>` records to DynamoDB, updates KPI current values, triggers `run_daily_cycle` on EC2 via SSM.

### Autopilot Lambda
`infra/packages/autopilot/` — scheduled Lambda (cron `0 7 * * ? *`, 7 AM UTC — after metrics/daily cycle) queries active projects with `autopilot: true` and triggers `run_task.py --propose-plan` for each on EC2 via SSM.
`infra/packages/autopilot/` — scheduled Lambda (cron hourly) queries active projects with `autopilot: true`. **Continuous** mode (`autopilot_mode: continuous`, not `cycle_paused`): triggers `run_task.py --propose-plan` on EC2. **Daily** mode: triggers only at **07:00 UTC** (same hourly cron; filtered in Lambda). EC2 `src/autopilot.py` enforces the same rules.

### Autonomous Daily Cycle
Projects can define KPIs (`kpis` array on the PROJECT record). When KPIs are present, the Metrics Lambda collects data daily and triggers `run_daily_cycle()` in `src/objectives.py` on EC2. The cycle: loads 14 days of snapshots + proposals + recent tasks + human tasks in review → calls the agent (opus) with a reflection prompt → parses structured JSON → creates PROP records (proposals) and `assignee: "human"` tasks. The agent also reviews human tasks marked `in_review`, either completing them or returning them to `pending` with a comment.
Expand All @@ -162,9 +162,13 @@ DynamoDB record types under `PROJECT#<id>`: `PROJECT`, `DIR#<iso>`, `SNAPSHOT#<d
See `docs/autonomous-objectives.md` for the full design vision and `docs/autonomous-objectives-setup.md` for human setup steps.

### Project Autopilot
Projects with `autopilot: true` get a daily plan proposal at 7 AM UTC (after metrics/daily cycle at 6 AM). The Autopilot Lambda (`infra/packages/autopilot/`) queries active autopilot projects and triggers `run_task.py --propose-plan <project_id>` on EC2 via SSM. `src/autopilot.py` loads project context (spec, recent tasks, approved proposals, prior plan outcomes) and calls the agent (opus) to propose 3–6 work items. Plans are stored as `PLAN#<YYYY-MM-DD>` records with `status=proposed`.
Projects with `autopilot: true` use `autopilot_mode`: **`daily`** (default) or **`continuous`**.

**Plan lifecycle**: proposed → human reviews/edits in UI → approved (creates tasks with `directive_sk=PLAN#<date>`) → batch runs via poller → completed (outcome summary tracked). If a human sends a directive mid-day, pending plan tasks are auto-cancelled. Stale proposed plans from prior days block new proposals until approved or regenerated. The `--regenerate` flag allows re-proposing today's plan.
- **Daily**: Plan proposal at **07:00 UTC** (Lambda + EC2 guard). One `PLAN#YYYY-MM-DD` per day; human approves in UI; tasks use `directive_sk=PLAN#…`. Stale proposed plans from prior days block new proposals until handled. `--regenerate` re-proposes today’s plan (`--plan-suffix` for a specific id in continuous).

- **Continuous**: Human starts a **cycle** via API (`cycle_started_at`, `cycle_max_hours`). Hourly Lambda tick; EC2 may auto-approve plans, create tasks immediately, and use `PLAN#YYYY-MM-DDTHH:MM:SS` keys. Pauses when the time window expires, failure streak triggers, fully blocked on human tasks, or manual stop — Discord webhook + UI. `next_check_hours` / `next_check_at` defer planner calls when the agent returns empty items.

**Plan lifecycle**: proposed → (daily: human approve | continuous: auto-approve) → tasks with `directive_sk=PLAN#…` → poller → batch completed. Directives cancel pending `PLAN#` tasks. Stale daily proposed plans block as before.

Autopilot coexists with directives and the autonomous objectives (KPI) cycle. The daily cycle's proposals and human task outcomes feed into the autopilot's context. Directives take priority — issuing a directive cancels any pending plan tasks.

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Tasks flow through: **create worktree → plan subtasks → execute → update d

**Autonomous objectives pipeline**: Projects with KPIs get a daily autonomous cycle. The Metrics Lambda (`infra/packages/metrics/`) runs at 6 AM UTC, fetches metrics (PageSpeed Insights, GitHub, optionally GA4/Search Console), writes SNAPSHOT records, and triggers `run_task.py --daily-cycle <project_id>` on EC2 via SSM. The daily cycle (`src/objectives.py`) loads metric history, proposals, and recent tasks, then calls the agent (opus) with a reflection prompt. The agent returns structured proposals and human requests. Proposals queue for human approval in the web UI; approved proposals become tasks in the existing pipeline. See `docs/autonomous-objectives.md`.

**Autopilot pipeline**: Projects with `autopilot: true` get a daily plan proposal. The Autopilot Lambda (`infra/packages/autopilot/`) runs at 7 AM UTC (after the metrics/daily cycle), queries active autopilot projects, and triggers `run_task.py --propose-plan <project_id>` on EC2 via SSM. `src/autopilot.py` loads the project spec, recent tasks, approved proposals, human tasks, and prior plan outcomes, then calls the agent (opus) to propose 3–6 work items for the day. The plan is stored as a `PLAN#<date>` record with `status=proposed`. The human reviews and approves the plan in the web UI, which creates tasks that flow through the existing poller/pipeline. Plan tasks use `directive_sk=PLAN#<date>` so the existing batch finalization detects completion. If a human sends a directive mid-day, pending plan tasks are auto-cancelled.
**Autopilot pipeline**: Projects with `autopilot: true` use `autopilot_mode` **`daily`** (default) or **`continuous`**. The Autopilot Lambda (`infra/packages/autopilot/`) runs **hourly**; **daily** projects are only dispatched at **07:00 UTC** (filtered in Lambda; EC2 enforces the same). **Continuous** projects run while a cycle is active (`cycle_started_at`, `cycle_max_hours`, not `cycle_paused`). `run_task.py --propose-plan <project_id> [--regenerate] [--plan-suffix]` on EC2 calls `src/autopilot.py`, which uses the agent (opus) and `./ctx`. **Daily**: `PLAN#YYYY-MM-DD`, human approves in the UI. **Continuous**: `PLAN#YYYY-MM-DDTHH:MM:SS` UTC, auto-approved tasks, pauses + Discord on time window, failures, or human-only backlog. Plan tasks use `directive_sk=PLAN#…`. Directives cancel pending plan tasks.

Every pipeline event is logged to `pipeline.log` (structured JSONL) with timestamps, task IDs, stages, runtimes, and model info. The Activity page (`/activity`) exposes this in the UI.

Expand Down
17 changes: 12 additions & 5 deletions docs/dynamo-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,14 @@ Project metadata. `id` is 8-char hex.
| awaiting_next_directive | bool? | All tasks in current batch are terminal |
| active_directive_sk | string? | Currently executing `DIR#` or `PLAN#` sk |
| kpis | list\<KPI\>? | KPI definitions (id, name, source, metric, target, direction, current) |
| autopilot | bool? | Enable daily plan proposals |
| autopilot | bool? | Enable autopilot plan proposals |
| autopilot_mode | string? | `daily` (human approve, 07 UTC) or `continuous` (auto-approve, hourly tick while cycle active) |
| cycle_started_at | string? | ISO start of current continuous cycle |
| cycle_max_hours | number? | Wall-clock hours per cycle (default 24) |
| cycle_paused | bool? | Waiting for human review / manual stop |
| cycle_pause_reason | string? | `time_expired` · `blocked` · `failures` · `manual` |
| cycle_feedback | string? | Human notes for next planner pass |
| next_check_at | string? | Agent-requested deferral (ISO); empty if none |

**GSI:** project-list-index (`proj_status`, `project_updated`).

Expand Down Expand Up @@ -233,13 +240,13 @@ Durable notes written by the daily-cycle agent via `./ctx memory save` (max 50 p

---

### PLAN (Daily Plan) — `pk=PROJECT#{id} sk=PLAN#{YYYY-MM-DD}`
### PLAN (Autopilot Plan) — `pk=PROJECT#{id} sk=PLAN#`

Autopilot daily plans. One per project per day.
Autopilot plans. Sort key is either legacy `PLAN#YYYY-MM-DD` (one per calendar day) or `PLAN#YYYY-MM-DDTHH:MM:SS` (UTC, multiple per day in continuous mode). `plan_date` is always calendar `YYYY-MM-DD` for grouping.

| Attribute | Type | Description |
|-----------|------|-------------|
| plan_date | string | YYYY-MM-DD |
| plan_date | string | YYYY-MM-DD (calendar day for the plan) |
| status | string | `proposed` · `approved` · `executing` · `completed` |
| reflection | string? | Agent's reasoning |
| human_notes | string? | Human notes on approval |
Expand All @@ -252,7 +259,7 @@ Autopilot daily plans. One per project per day.
| outcome_summary | map? | `{completed: N, in_review: N, failed: N, cancelled: N}` |

**Access:**
- Get: `GetItem(pk=PROJECT#id, sk=PLAN#date)`
- Get: `GetItem(pk=PROJECT#id, sk=PLAN#<suffix>)` — suffix is date or `YYYY-MM-DDTHH:MM:SS`
- List: `Query pk, sk begins_with "PLAN#"` — `ScanIndexForward=false, Limit=14`

---
Expand Down
4 changes: 2 additions & 2 deletions frontend/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ src/
pages/Stats.tsx — Usage & cost (today / all-time tokens, budget bar, 14-day chart via GET /api/stats and /api/budget)
pages/ProjectList.tsx — Project list with status, task progress, last directive, target repo
pages/ProjectCreate.tsx — New project form: title, spec (markdown) with optional “Generate from prompt” (Bedrock via API), priority, target repo, autopilot toggle
pages/ProjectDetail.tsx — Spec editor (Generate while editing), progress bar, autopilot toggle + today’s plan section (proposed/approve/progress/completed), directive timeline, active tasks, directive composer; polls 3s while busy
pages/ProjectDetail.tsx — Spec editor (Generate while editing), progress bar, autopilot Off/Daily/Continuous + plan timeline (continuous), cycle start/stop/review, daily plan approve/regenerate, directive timeline, active tasks; polls 3s while busy
pages/Login.tsx — Sign-in page
lib/api.ts — Fetch client (credentials: "include", auto-reload on 401); exports fetchRoles() → GET /api/roles; project CRUD (fetchProjects, createProject, patchProject, postProjectDirective, etc.); plan APIs (fetchPlans, fetchPlanDetail, approvePlan, regeneratePlan, patchPlanItems)
lib/api.ts — Fetch client; project CRUD; plan APIs (fetchPlans, fetchPlanDetail, approvePlan, regeneratePlan, patchPlanItems); continuous autopilot cycle (startAutopilotCycle, stopAutopilotCycle, reviewAutopilotCycle)
lib/types.ts — TypeScript interfaces matching the /api/* responses; Task includes role: string, project_id, directive_sk; Project (with autopilot boolean), Directive, DailyPlan, PlanItem, ProjectListItem types
lib/time.ts — timeAgo utility
index.css — Tailwind imports, dark theme CSS vars, .prose-custom styles (colors use CSS var tokens, not hardcoded oklch literals)
Expand Down
45 changes: 37 additions & 8 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ export async function createProject(data: {
target_repo?: string
status?: ProjectStatus
autopilot?: boolean
autopilot_mode?: "daily" | "continuous"
}) {
return request<Project>("/projects", {
method: "POST",
Expand Down Expand Up @@ -283,6 +284,8 @@ export async function patchProject(
target_repo: string
kpis: KPI[]
autopilot: boolean
autopilot_mode: "daily" | "continuous"
cycle_max_hours: number
}>,
) {
return request<Project>(`/projects/${id}`, {
Expand Down Expand Up @@ -354,33 +357,59 @@ export async function fetchPlans(projectId: string, limit = 14) {
return request<{ plans: DailyPlan[] }>(`/projects/${projectId}/plans?limit=${limit}`)
}

export async function fetchPlanDetail(projectId: string, dateStr: string) {
export async function fetchPlanDetail(projectId: string, planId: string) {
return request<{ plan: DailyPlan; tasks: Task[] }>(
`/projects/${projectId}/plans/${encodeURIComponent(dateStr)}`,
`/projects/${projectId}/plans/${encodeURIComponent(planId)}`,
)
}

export async function patchPlanItems(
projectId: string,
dateStr: string,
planId: string,
body: { items: PlanItem[]; reflection?: string },
) {
return request<{ plan: DailyPlan }>(`/projects/${projectId}/plans/${encodeURIComponent(dateStr)}`, {
return request<{ plan: DailyPlan }>(`/projects/${projectId}/plans/${encodeURIComponent(planId)}`, {
method: "PATCH",
body: JSON.stringify(body),
})
}

export async function approvePlan(projectId: string, dateStr: string, humanNotes = "") {
export async function approvePlan(projectId: string, planId: string, humanNotes = "") {
return request<{ ok: boolean; plan: DailyPlan; task_ids: string[] }>(
`/projects/${projectId}/plans/${encodeURIComponent(dateStr)}/approve`,
`/projects/${projectId}/plans/${encodeURIComponent(planId)}/approve`,
{ method: "POST", body: JSON.stringify({ human_notes: humanNotes }) },
)
}

export async function regeneratePlan(projectId: string, dateStr: string) {
export async function regeneratePlan(projectId: string, planId: string) {
return request<{ ok: boolean }>(
`/projects/${projectId}/plans/${encodeURIComponent(dateStr)}/regenerate`,
`/projects/${projectId}/plans/${encodeURIComponent(planId)}/regenerate`,
{ method: "POST", body: JSON.stringify({}) },
)
}

export async function startAutopilotCycle(projectId: string, maxHours?: number) {
return request<{ ok: boolean }>(`/projects/${projectId}/cycle/start`, {
method: "POST",
body: JSON.stringify(
maxHours != null && maxHours > 0 ? { max_hours: maxHours } : {},
),
})
}

export async function stopAutopilotCycle(projectId: string) {
return request<{ ok: boolean }>(`/projects/${projectId}/cycle/stop`, {
method: "POST",
body: JSON.stringify({}),
})
}

export async function reviewAutopilotCycle(
projectId: string,
opts: { feedback: string; restart?: boolean; max_hours?: number },
) {
return request<{ ok: boolean }>(`/projects/${projectId}/cycle/review`, {
method: "POST",
body: JSON.stringify(opts),
})
}
13 changes: 12 additions & 1 deletion frontend/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export interface Task {

export type ProjectStatus = "active" | "paused" | "completed"

export type AutopilotMode = "daily" | "continuous"

export type CyclePauseReason = "time_expired" | "blocked" | "failures" | "manual" | ""

export type KPIDirection = "up" | "down" | "maintain"

export interface KPI {
Expand Down Expand Up @@ -76,8 +80,15 @@ export interface Project {
awaiting_next_directive: boolean
active_directive_sk: string
kpis: KPI[]
/** Daily autopilot plan (Lambda 7 AM UTC); omitted on older API responses */
/** Autopilot enabled; omitted on older API responses */
autopilot?: boolean
autopilot_mode?: AutopilotMode
cycle_started_at?: string
cycle_max_hours?: number
cycle_paused?: boolean
cycle_pause_reason?: CyclePauseReason
cycle_feedback?: string
next_check_at?: string
}

export interface ProjectListItem extends Project {
Expand Down
Loading
Loading