From b07b846790c029ffb75a856eb50a186741d95c69 Mon Sep 17 00:00:00 2001 From: Gregory Fong Date: Sun, 22 Mar 2026 01:19:16 -0700 Subject: [PATCH] feat: add project docs (DOC#) for non-repo knowledge Add a DOC# record type under PROJECT# for storing non-repo knowledge like credentials inventory, infra references, and business context. Agents access docs via `./ctx docs [slug]`. - Python: CRUD helpers in projects_dynamo.py with slug validation - CLI: `./ctx docs` (list) and `./ctx docs ` (get) in context_cli.py - API: GET/PUT/DELETE /projects/:id/docs/:slug in Hono Lambda - UI: Docs tab on ProjectDetail with create, edit, delete, markdown preview - Schema: DOC# record type documented in dynamo-schema.md Made-with: Cursor --- .cursor/rules/project-conventions.mdc | 6 +- docs/dynamo-schema.md | 17 + frontend/AGENTS.md | 4 +- frontend/src/lib/api.ts | 33 ++ frontend/src/lib/types.ts | 8 + frontend/src/pages/ProjectDetail.tsx | 408 +++++++++++++++++++--- infra/packages/api/src/lib/dynamo.ts | 88 +++++ infra/packages/api/src/lib/types.ts | 8 + infra/packages/api/src/routes/projects.ts | 56 +++ src/AGENTS.md | 4 +- src/context_cli.py | 44 +++ src/projects_dynamo.py | 69 ++++ 12 files changed, 683 insertions(+), 62 deletions(-) diff --git a/.cursor/rules/project-conventions.mdc b/.cursor/rules/project-conventions.mdc index 56cdb73..85fc1b0 100644 --- a/.cursor/rules/project-conventions.mdc +++ b/.cursor/rules/project-conventions.mdc @@ -24,7 +24,7 @@ There is no bare `python` or `python3` in `$PATH` that has project dependencies. - `src/bot.py` — Discord bot with slash commands (discord.py) - `src/task_store.py` — Shared task types (`Task`, enums, `Comment`) - `src/dynamo_store.py` — DynamoDB-backed task store (`DynamoTaskStore`) -- `src/projects_dynamo.py` — DynamoDB helpers for project records (get/update project, directives, snapshots, proposals, daily plans) +- `src/projects_dynamo.py` — DynamoDB helpers for project records (get/update project, directives, snapshots, proposals, daily plans, docs) - `src/autopilot.py` — Daily autopilot plan proposal (`propose_daily_plan`); called via `run_task.py --propose-plan` - `src/pipeline.py` — Task orchestration: plan, execute, PR, cleanup; `run_directive()` for project directives - `src/objectives.py` — Daily autonomous cycle: observe metrics → reflect → propose actions; `run_daily_cycle()` for projects with KPIs @@ -152,7 +152,7 @@ Projects are long-lived entities (`pk=PROJECT#`, `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/: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` +`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/docs`, `GET /api/projects/:id/docs/:slug`, `PUT /api/projects/:id/docs/:slug`, `DELETE /api/projects/:id/docs/:slug`, `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. @@ -168,7 +168,7 @@ Projects can define KPIs (`kpis` array on the PROJECT record). When KPIs are pre Proposals go through a human approval queue in the web UI. Approved proposals create tasks that flow through the existing pipeline. Rejected proposals carry feedback the agent sees in the next cycle. Proposal outcomes are tracked — when a task completes, the PROP record is updated so the agent can learn what worked. -DynamoDB record types under `PROJECT#`: `PROJECT`, `DIR#`, `SNAPSHOT#`, `PROP##`, `PLAN#`. Task records use `TASK#` as pk with sk prefixes `META`, `OUTPUT#`, `COMMENT#`, `PLAN#`, `LOG#`. See `docs/dynamo-schema.md` for the full single-table schema (all record types, GSIs, attributes, access patterns). +DynamoDB record types under `PROJECT#`: `PROJECT`, `DIR#`, `SNAPSHOT#`, `PROP##`, `PLAN#`, `DOC#`. Task records use `TASK#` as pk with sk prefixes `META`, `OUTPUT#`, `COMMENT#`, `PLAN#`, `LOG#`. See `docs/dynamo-schema.md` for the full single-table schema (all record types, GSIs, attributes, access patterns). See `docs/autonomous-objectives.md` for the full design vision and `docs/autonomous-objectives-setup.md` for human setup steps. diff --git a/docs/dynamo-schema.md b/docs/dynamo-schema.md index ddd65c7..4b5637a 100644 --- a/docs/dynamo-schema.md +++ b/docs/dynamo-schema.md @@ -255,6 +255,23 @@ Durable notes written by the daily-cycle agent via `./ctx memory save` (max 50 p --- +### DOC — `pk=PROJECT#{id} sk=DOC#` + +Human-managed project documents for non-repo knowledge (infra references, credentials inventory, business context). Agents read via `./ctx docs [slug]`. No TTL; no max count. + +| Attribute | Type | Description | +|-----------|------|-------------| +| title | string | Human-readable document title | +| content | string | Markdown body (max 50000 chars) | +| created_at | string | ISO 8601 | +| updated_at | string | ISO 8601 | + +**Slug format:** lowercase alphanumeric, hyphens, underscores; 1–63 chars (`/^[a-z0-9][a-z0-9_-]{0,62}$/`). + +**Access:** `Query pk, sk begins_with "DOC#"` — `ScanIndexForward=true` for alphabetical. `GetItem(pk, DOC#)` for single doc. + +--- + ### PLAN (Autopilot Plan) — `pk=PROJECT#{id} sk=PLAN#…` 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. diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index c0fe579..fe4901f 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -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, **Project manager** chat (GET/POST `/projects/:id/chat`, polls while `reply_pending`), autopilot Off/Daily/Continuous + plan timeline (continuous), cycle start/stop/review, daily plan approve/regenerate, directive timeline, active tasks; polls 3s while busy + pages/ProjectDetail.tsx — Spec editor (Generate while editing), progress bar, **Project manager** chat (GET/POST `/projects/:id/chat`, polls while `reply_pending`), autopilot Off/Daily/Continuous + plan timeline (continuous), cycle start/stop/review, daily plan approve/regenerate, **Docs** tab (CRUD for DOC# records via `/projects/:id/docs`), directive timeline, active tasks; polls 3s while busy pages/Login.tsx — Sign-in page - lib/api.ts — Fetch client; project CRUD; `fetchProjectChat` / `postProjectChat`; plan APIs (fetchPlans, fetchPlanDetail, approvePlan, regeneratePlan, patchPlanItems); continuous autopilot cycle (startAutopilotCycle, stopAutopilotCycle, reviewAutopilotCycle) + lib/api.ts — Fetch client; project CRUD; `fetchProjectChat` / `postProjectChat`; plan APIs (fetchPlans, fetchPlanDetail, approvePlan, regeneratePlan, patchPlanItems); continuous autopilot cycle (startAutopilotCycle, stopAutopilotCycle, reviewAutopilotCycle); project docs (fetchProjectDocs, putProjectDoc, deleteProjectDoc) 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) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 149b65d..2251570 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -11,6 +11,7 @@ import type { DailyPlan, PlanItem, ProjectChatMessage, + ProjectDoc, } from "./types" const BASE = (import.meta.env.VITE_API_BASE_URL ?? "") + "/api" @@ -436,3 +437,35 @@ export async function reviewAutopilotCycle( body: JSON.stringify(opts), }) } + +// --------------------------------------------------------------------------- +// Project Docs +// --------------------------------------------------------------------------- + +export async function fetchProjectDocs(projectId: string) { + return request<{ docs: ProjectDoc[] }>(`/projects/${projectId}/docs`) +} + +export async function fetchProjectDoc(projectId: string, slug: string) { + return request<{ doc: ProjectDoc }>( + `/projects/${projectId}/docs/${encodeURIComponent(slug)}`, + ) +} + +export async function putProjectDoc( + projectId: string, + slug: string, + data: { title: string; content: string }, +) { + return request<{ doc: ProjectDoc }>( + `/projects/${projectId}/docs/${encodeURIComponent(slug)}`, + { method: "PUT", body: JSON.stringify(data) }, + ) +} + +export async function deleteProjectDoc(projectId: string, slug: string) { + return request<{ ok: boolean }>( + `/projects/${projectId}/docs/${encodeURIComponent(slug)}`, + { method: "DELETE" }, + ) +} diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index b782bbf..66134e4 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -141,6 +141,14 @@ export interface Comment { created_at: string } +export interface ProjectDoc { + slug: string + title: string + content: string + created_at: string + updated_at: string +} + export interface Counts { all: number pending: number diff --git a/frontend/src/pages/ProjectDetail.tsx b/frontend/src/pages/ProjectDetail.tsx index 1683e38..989a15e 100644 --- a/frontend/src/pages/ProjectDetail.tsx +++ b/frontend/src/pages/ProjectDetail.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback, useMemo, useRef } from "react" -import { useParams, Link } from "react-router-dom" +import { useParams, Link, useSearchParams } from "react-router-dom" import toast from "react-hot-toast" import { ArrowLeft, @@ -15,6 +15,9 @@ import { AlertTriangle, Sparkles, RotateCcw, + FileText, + Plus, + Trash2, } from "lucide-react" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" @@ -48,6 +51,9 @@ import { fetchProjectChat, postProjectChat, dismissProjectDirective, + fetchProjectDocs, + putProjectDoc, + deleteProjectDoc, } from "@/lib/api" import type { Snapshot } from "@/lib/api" import type { @@ -58,6 +64,7 @@ import type { KPI, DailyPlan, ProjectChatMessage, + ProjectDoc, } from "@/lib/types" import { timeAgo } from "@/lib/time" @@ -179,8 +186,27 @@ function EditableTitle({ ) } +const PROJECT_TABS = ["overview", "autopilot", "spec", "docs", "directives"] as const +type ProjectTab = (typeof PROJECT_TABS)[number] + +const TAB_LABELS: Record = { + overview: "Overview", + autopilot: "Autopilot", + spec: "Spec", + docs: "Docs", + directives: "Directives", +} + export default function ProjectDetail() { const { projectId } = useParams<{ projectId: string }>() + const [searchParams, setSearchParams] = useSearchParams() + const rawTab = searchParams.get("tab") ?? "overview" + const activeTab: ProjectTab = PROJECT_TABS.includes(rawTab as ProjectTab) + ? (rawTab as ProjectTab) + : "overview" + const setActiveTab = (tab: ProjectTab) => + setSearchParams({ tab }, { replace: true }) + const [project, setProject] = useState(null) const [directives, setDirectives] = useState([]) const [tasks, setTasks] = useState([]) @@ -211,6 +237,13 @@ export default function ProjectDetail() { const [pmChatMessages, setPmChatMessages] = useState([]) const [pmChatBody, setPmChatBody] = useState("") const [pmChatSending, setPmChatSending] = useState(false) + const [projectDocs, setProjectDocs] = useState([]) + const [docEditing, setDocEditing] = useState(null) + const [docDraftTitle, setDocDraftTitle] = useState("") + const [docDraftContent, setDocDraftContent] = useState("") + const [docDraftSlug, setDocDraftSlug] = useState("") + const [docSaving, setDocSaving] = useState(false) + const [docCreating, setDocCreating] = useState(false) /** When true, polling must not overwrite the spec textarea (draft). */ const specEditingRef = useRef(false) useEffect(() => { @@ -227,10 +260,12 @@ export default function ProjectDetail() { messages: [] as ProjectChatMessage[], reply_pending: false, })), + fetchProjectDocs(projectId).catch(() => ({ docs: [] as ProjectDoc[] })), ]) - .then(async ([d, s, chat]) => { + .then(async ([d, s, chat, docsResult]) => { setProject(d.project) setPmChatMessages(chat.messages) + setProjectDocs(docsResult.docs) setDirectives(d.directives) setTasks(d.tasks) setProgress(d.progress) @@ -440,6 +475,27 @@ export default function ProjectDetail() { + {/* Tab bar */} + + + {/* ── Overview tab ── */} + {activeTab === "overview" && ( + <> {/* Progress */}
@@ -562,6 +618,65 @@ export default function ProjectDetail() { + {/* Your tasks (human-assigned) */} + {humanTasks.length > 0 && ( +
+

+ + Your tasks ({humanTasks.length}) +

+
    + {humanTasks.map((t) => ( +
  • + + {t.title} + + {t.status.replace("_", " ")} + + +
  • + ))} +
+
+ )} + + {/* Active tasks */} +
+

+ Active tasks +

+ {activeTasks.length === 0 ? ( +

No active tasks for this project.

+ ) : ( +
    + {activeTasks.map((t) => ( +
  • + + {t.title} + + + + {t.status.replace("_", " ")} + + + +
  • + ))} +
+ )} +
+ + )} + + {/* ── Autopilot tab ── */} + {activeTab === "autopilot" && ( + <> {/* KPI Dashboard */}
@@ -1067,7 +1182,12 @@ export default function ProjectDetail() {
)}
+ + )} + {/* ── Spec tab ── */} + {activeTab === "spec" && ( + <> {/* Spec */}
@@ -1163,7 +1283,237 @@ export default function ProjectDetail() {
)}
+ + )} + {/* ── Docs tab ── */} + {activeTab === "docs" && ( +
+
+

+ + Project docs +

+ {!docCreating && !docEditing && ( + + )} +
+

+ Non-repo knowledge: credentials inventory, infra references, business context. Agents access these via ./ctx docs. +

+ + {/* Create new doc form */} + {docCreating && ( +
+

New document

+
+
+ + setDocDraftSlug( + e.target.value + .toLowerCase() + .replace(/[^a-z0-9_-]/g, "") + .replace(/^[^a-z0-9]+/, "") + .slice(0, 63) + )} + placeholder="e.g. aws-setup" + maxLength={63} + className="h-8 border-zinc-800 bg-zinc-950/50 text-[12px] font-mono" + /> +
+
+ + setDocDraftTitle(e.target.value)} + placeholder="AWS Setup Notes" + className="h-8 border-zinc-800 bg-zinc-950/50 text-[12px]" + /> +
+
+