From 2f6adf4382bbe9f812c1ab2f5ce36d248cc78412 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Fri, 10 Jul 2026 22:50:05 -0500 Subject: [PATCH 1/4] feat(web): Submatter marketing redesign with Signal Editorial system Introduce public /landing and /pricing pages for the SBMTR brand with bento features, convoy product demo, motion reveals, and scoped marketing tokens. Unauthenticated visitors route to the landing page. Co-authored-by: Cursor --- apps/web/app/(marketing)/landing/page.tsx | 29 ++++ .../app/(marketing)/landing/pricing/page.tsx | 125 ++++++++++++++ apps/web/app/(marketing)/layout.tsx | 47 +++++ apps/web/app/page.tsx | 25 +-- .../components/marketing/border-glow-card.tsx | 22 +++ .../marketing/comparison-section.tsx | 78 +++++++++ apps/web/components/marketing/cta-section.tsx | 35 ++++ apps/web/components/marketing/dot-field.tsx | 21 +++ .../components/marketing/features-section.tsx | 110 ++++++++++++ .../web/components/marketing/hero-section.tsx | 75 ++++++++ .../marketing/how-it-works-section.tsx | 72 ++++++++ apps/web/components/marketing/logo.tsx | 61 +++++++ .../web/components/marketing/product-demo.tsx | 116 +++++++++++++ apps/web/components/marketing/reveal.tsx | 90 ++++++++++ apps/web/components/marketing/site-footer.tsx | 44 +++++ apps/web/components/marketing/site-header.tsx | 52 ++++++ apps/web/middleware.ts | 1 + apps/web/package.json | 1 + apps/web/styles/globals.css | 5 + apps/web/styles/marketing.css | 160 ++++++++++++++++++ apps/web/tailwind.config.ts | 8 + pnpm-lock.yaml | 60 +++++++ 22 files changed, 1215 insertions(+), 22 deletions(-) create mode 100644 apps/web/app/(marketing)/landing/page.tsx create mode 100644 apps/web/app/(marketing)/landing/pricing/page.tsx create mode 100644 apps/web/app/(marketing)/layout.tsx create mode 100644 apps/web/components/marketing/border-glow-card.tsx create mode 100644 apps/web/components/marketing/comparison-section.tsx create mode 100644 apps/web/components/marketing/cta-section.tsx create mode 100644 apps/web/components/marketing/dot-field.tsx create mode 100644 apps/web/components/marketing/features-section.tsx create mode 100644 apps/web/components/marketing/hero-section.tsx create mode 100644 apps/web/components/marketing/how-it-works-section.tsx create mode 100644 apps/web/components/marketing/logo.tsx create mode 100644 apps/web/components/marketing/product-demo.tsx create mode 100644 apps/web/components/marketing/reveal.tsx create mode 100644 apps/web/components/marketing/site-footer.tsx create mode 100644 apps/web/components/marketing/site-header.tsx create mode 100644 apps/web/styles/marketing.css diff --git a/apps/web/app/(marketing)/landing/page.tsx b/apps/web/app/(marketing)/landing/page.tsx new file mode 100644 index 0000000..07ec591 --- /dev/null +++ b/apps/web/app/(marketing)/landing/page.tsx @@ -0,0 +1,29 @@ +import type { Metadata } from "next"; + +import { ComparisonSection } from "@/components/marketing/comparison-section"; +import { CtaSection } from "@/components/marketing/cta-section"; +import { FeaturesSection } from "@/components/marketing/features-section"; +import { HeroSection } from "@/components/marketing/hero-section"; +import { HowItWorksSection } from "@/components/marketing/how-it-works-section"; +import { SiteFooter } from "@/components/marketing/site-footer"; +import { SiteHeader } from "@/components/marketing/site-header"; + +export const metadata: Metadata = { + title: "Subagents. Frontmatter. One workspace.", +}; + +export default function LandingPage() { + return ( + <> + +
+ + + + + +
+ + + ); +} diff --git a/apps/web/app/(marketing)/landing/pricing/page.tsx b/apps/web/app/(marketing)/landing/pricing/page.tsx new file mode 100644 index 0000000..6c37734 --- /dev/null +++ b/apps/web/app/(marketing)/landing/pricing/page.tsx @@ -0,0 +1,125 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { Check } from "lucide-react"; + +import { BorderGlowCard } from "@/components/marketing/border-glow-card"; +import { Reveal, Stagger, StaggerItem } from "@/components/marketing/reveal"; +import { SiteFooter } from "@/components/marketing/site-footer"; +import { SiteHeader } from "@/components/marketing/site-header"; +import { cn } from "@/lib/utils"; + +export const metadata: Metadata = { + title: "Pricing", +}; + +const tiers = [ + { + name: "Solo", + price: "Free", + period: "during beta", + description: "Personal workspace for testing agent workflows and markdown sync.", + features: ["1 workspace", "Markdown backlog sync", "MCP claim / complete", "Docs & planner"], + cta: "Start free", + href: "/sign-up", + highlighted: false, + }, + { + name: "Team", + price: "$12", + period: "per seat / mo", + description: "Shared convoys, run history, and model routing for small builder teams.", + features: [ + "Unlimited workspaces", + "Agent run history", + "Role-based routing", + "Audit log", + "Priority sync", + ], + cta: "Start trial", + href: "/sign-up", + highlighted: true, + }, + { + name: "Fleet", + price: "Custom", + period: "contact us", + description: "Mission control for multiple repos, tenants, and orchestrated pipelines.", + features: ["Connected repos", "Fleet dashboard", "Webhook gates", "SSO", "Self-hosted option"], + cta: "Contact", + href: "mailto:hello@submatter.dev", + highlighted: false, + }, +] as const; + +export default function PricingPage() { + return ( + <> + +
+ +

Pricing

+

+ Predictable seats. +
+ No per-token surprise. +

+

+ Beta pricing for feedback. BYOK for model providers always. +

+
+ + + {tiers.map((tier) => ( + + + {tier.highlighted ? ( + + popular + + ) : ( + + )} +

{tier.name}

+

+ {tier.price} + {tier.period} +

+

{tier.description}

+
    + {tier.features.map((feature) => ( +
  • + + {feature} +
  • + ))} +
+ + {tier.cta} + +
+
+ ))} +
+ +

+ kind: pricing · status: draft · priority: P2 +

+
+ + + ); +} diff --git a/apps/web/app/(marketing)/layout.tsx b/apps/web/app/(marketing)/layout.tsx new file mode 100644 index 0000000..b60bd3d --- /dev/null +++ b/apps/web/app/(marketing)/layout.tsx @@ -0,0 +1,47 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; +import { Barlow_Condensed, IBM_Plex_Sans, JetBrains_Mono } from "next/font/google"; + +import "@/styles/marketing.css"; + +const barlowCondensed = Barlow_Condensed({ + subsets: ["latin"], + weight: ["700", "800"], + variable: "--font-display", +}); + +const ibmPlexSans = IBM_Plex_Sans({ + subsets: ["latin"], + weight: ["400", "500", "600"], + variable: "--font-marketing-sans", +}); + +const jetbrainsMono = JetBrains_Mono({ + subsets: ["latin"], + weight: ["400", "500"], + variable: "--font-mono-brand", +}); + +export const metadata: Metadata = { + title: { + default: "Submatter — Subagents. Frontmatter. One workspace.", + template: "%s · Submatter", + }, + description: + "Agent-native workspace with markdown backlog sync, MCP task lifecycle, and convoy pipelines for Cursor subagents.", + openGraph: { + title: "SBMTR — Submatter", + description: "Subagents. Frontmatter. One workspace.", + siteName: "Submatter", + }, +}; + +export default function MarketingLayout({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index f974339..1b7e6dc 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -6,27 +6,14 @@ import { db } from "@tasks/database/client"; import { workspaceMembers, workspaces } from "@tasks/database/schema"; /** - * Root landing route. Decides where an authenticated user goes when they - * hit `/`. The post-sign-in `callbackUrl` defaults here, so this is the - * single source of truth for "where does a user actually land?" - * - * Decision: land on the user's oldest workspace home (`/{slug}/`). Why oldest: - * we don't have a "last visited" column yet (deliberately deferred — see - * Task-pick-workspace-landing-route) and oldest-membership is stable across - * page loads. The workspace home is no longer a mockup; it now shows real - * stats and recent activity via `objects.stats` / `objects.listRecent`. - * - * The auth callback (`ensureUserHasWorkspace` in `apps/web/lib/auth.ts`) - * provisions a personal workspace on first sign-in, so the zero-workspace - * branch below should be effectively unreachable for fresh sign-ins. It - * survives only as a defensive fallback for sessions that were minted - * before that provisioning logic existed. + * Root landing route. Unauthenticated visitors go to the public marketing site; + * signed-in users land on their oldest workspace home (`/{slug}/`). */ export default async function HomePage() { const session = await auth(); if (!session?.user?.id) { - redirect("/sign-in"); + redirect("/landing"); } const [membership] = await db @@ -41,11 +28,5 @@ export default async function HomePage() { redirect(`/${membership.slug}`); } - // Zero-workspace fallback. See header doc — this should be unreachable - // post-`ensureUserHasWorkspace`. If we hit it anyway, bounce through - // sign-in so the auth callback re-runs and provisions a workspace. - // Track follow-up: a proper onboarding flow for users in this state - // is captured in Plan-daily-driver-finish/Epic-shipping-the-shell/ - // Task-onboarding-zero-workspace-flow.md. redirect("/sign-in?error=no_workspace"); } diff --git a/apps/web/components/marketing/border-glow-card.tsx b/apps/web/components/marketing/border-glow-card.tsx new file mode 100644 index 0000000..70bb3bc --- /dev/null +++ b/apps/web/components/marketing/border-glow-card.tsx @@ -0,0 +1,22 @@ +import type { ReactNode } from "react"; + +import { cn } from "@/lib/utils"; + +type BorderGlowCardProps = { + children: ReactNode; + className?: string; + as?: "div" | "article" | "li"; +}; + +export function BorderGlowCard({ children, className, as: Tag = "div" }: BorderGlowCardProps) { + return ( + + {children} + + ); +} diff --git a/apps/web/components/marketing/comparison-section.tsx b/apps/web/components/marketing/comparison-section.tsx new file mode 100644 index 0000000..f3f52b9 --- /dev/null +++ b/apps/web/components/marketing/comparison-section.tsx @@ -0,0 +1,78 @@ +import { Reveal } from "@/components/marketing/reveal"; + +const rows = [ + { label: "Markdown backlog in git", sbmtr: true, linear: false, notion: "partial", convoy: false }, + { label: "MCP claim / complete", sbmtr: true, linear: false, notion: false, convoy: true }, + { label: "Full workspace surface", sbmtr: true, linear: "partial", notion: true, convoy: false }, + { label: "Role-based convoys", sbmtr: true, linear: false, notion: false, convoy: true }, + { label: "Human approval gates", sbmtr: true, linear: false, notion: false, convoy: "partial" }, +] as const; + +function Cell({ value }: { value: boolean | string }) { + if (value === true) return yes; + if (value === false) return —; + return {value}; +} + +export function ComparisonSection() { + return ( +
+
+ +

Compare

+

+ The agent-native lane +

+
+ + +
+ + + + + + + + + + + + {rows.map((row) => ( + + + + + + + + ))} + +
+ Capability + + SBMTR + + Linear + + Notion + + Convoy tools +
{row.label} + + + + + + + +
+
+
+
+
+ ); +} diff --git a/apps/web/components/marketing/cta-section.tsx b/apps/web/components/marketing/cta-section.tsx new file mode 100644 index 0000000..5407791 --- /dev/null +++ b/apps/web/components/marketing/cta-section.tsx @@ -0,0 +1,35 @@ +import Link from "next/link"; +import { ArrowRight } from "lucide-react"; + +import { Reveal } from "@/components/marketing/reveal"; + +export function CtaSection() { + return ( +
+
+ +

---

+

SBMTR

+

+ Subagents. Frontmatter. One workspace. Open Monday morning in Submatter — not a demo shell. +

+
+ + Start free + + + + View pricing + +
+
+
+
+ ); +} diff --git a/apps/web/components/marketing/dot-field.tsx b/apps/web/components/marketing/dot-field.tsx new file mode 100644 index 0000000..ccc3373 --- /dev/null +++ b/apps/web/components/marketing/dot-field.tsx @@ -0,0 +1,21 @@ +import { cn } from "@/lib/utils"; + +type DotFieldProps = { + className?: string; + variant?: "hero" | "light"; +}; + +export function DotField({ className, variant = "hero" }: DotFieldProps) { + return ( +
+ ); +} diff --git a/apps/web/components/marketing/features-section.tsx b/apps/web/components/marketing/features-section.tsx new file mode 100644 index 0000000..65b8c8b --- /dev/null +++ b/apps/web/components/marketing/features-section.tsx @@ -0,0 +1,110 @@ +import { Bot, FileText, GitBranch, Layers, Route, Shield } from "lucide-react"; +import type { LucideIcon } from "lucide-react"; + +import { BorderGlowCard } from "@/components/marketing/border-glow-card"; +import { DotField } from "@/components/marketing/dot-field"; +import { Reveal, Stagger, StaggerItem } from "@/components/marketing/reveal"; + +type Feature = { + icon: LucideIcon; + title: string; + description: string; + span: string; + tag: string; +}; + +const features: Feature[] = [ + { + icon: FileText, + title: "Markdown source of truth", + description: "Plan / Epic / Task files in git. Frontmatter fields drive status, priority, and agent routing.", + span: "lg:col-span-7", + tag: "sync", + }, + { + icon: Bot, + title: "MCP lifecycle", + description: "claim_task → work → complete_task. Auditable runs, not ad-hoc object patches.", + span: "lg:col-span-5", + tag: "mcp", + }, + { + icon: GitBranch, + title: "Convoy pipelines", + description: "Architect, implementer, reviewer roles with human gates between stages.", + span: "lg:col-span-4", + tag: "convoy", + }, + { + icon: Layers, + title: "Full workspace", + description: "Docs, boards, forms, whiteboards — same object model humans and agents share.", + span: "lg:col-span-4", + tag: "workspace", + }, + { + icon: Shield, + title: "Multitenant", + description: "workspace_id on every row. Built for teams, not a single homelab tenant.", + span: "lg:col-span-4", + tag: "tenant", + }, +]; + +export function FeaturesSection() { + return ( +
+ + +
+ +

Capabilities

+

+ Not a chat wrapper. +
+ A coordination plane. +

+
+ + + {features.map((feature) => ( + + +
+ + + {feature.tag} + +
+

{feature.title}

+

+ {feature.description} +

+
+
+ ))} + + + +
+ +
+

Per-role model routing

+

+ Fast implementers, premium architects — declared in convoy frontmatter and enforced by your + agent pipeline. +

+
+
+

+ model_policy: composer-fast +
+ tier: fast | premium +

+
+
+
+
+
+ ); +} diff --git a/apps/web/components/marketing/hero-section.tsx b/apps/web/components/marketing/hero-section.tsx new file mode 100644 index 0000000..d6e5616 --- /dev/null +++ b/apps/web/components/marketing/hero-section.tsx @@ -0,0 +1,75 @@ +import Link from "next/link"; +import { ArrowRight } from "lucide-react"; + +import { DotField } from "@/components/marketing/dot-field"; +import { ProductDemo } from "@/components/marketing/product-demo"; +import { Reveal } from "@/components/marketing/reveal"; + +export function HeroSection() { + return ( +
+ +
+ +
+
+ +

+ Agent-native workspace +

+
+ + +

+ The matter beneath{" "} + + SBMTR + +

+
+ + +

+ Submatter syncs your plans/{" "} + backlog to a real workspace, then lets Cursor subagents claim tasks through MCP — with convoy gates + humans actually approve. +

+
+ + +
+ + Start free + + + + See the pipeline + +
+
+ + +

+ subagents · frontmatter · one workspace +

+
+
+ + + + +
+ +
+
+ ); +} diff --git a/apps/web/components/marketing/how-it-works-section.tsx b/apps/web/components/marketing/how-it-works-section.tsx new file mode 100644 index 0000000..88a2ea2 --- /dev/null +++ b/apps/web/components/marketing/how-it-works-section.tsx @@ -0,0 +1,72 @@ +import { Reveal, Stagger, StaggerItem } from "@/components/marketing/reveal"; + +const steps = [ + { + n: "01", + title: "Write in plans/", + body: "YAML frontmatter on every Plan, Epic, and Task. Versioned in git like the rest of your codebase.", + code: "plans/Plan-*/Epic-*/Task-*.md", + }, + { + n: "02", + title: "Sync to Submatter", + body: "Importer mirrors frontmatter into Postgres. Cursor mappings keep agent ids aligned with slugs.", + code: "watch:markdown-backlog", + }, + { + n: "03", + title: "Agents claim work", + body: "Subagents call MCP tools against a scoped workspace_id. Runs appear in fleet history.", + code: "claim_task → complete_task", + }, + { + n: "04", + title: "Humans gate convoys", + body: "Reviewer and architect roles pause for approval. Status flows back to markdown on merge.", + code: "ready → in_progress → done", + }, +] as const; + +export function HowItWorksSection() { + return ( +
+
+ +
+

+ How it works +

+

+ From frontmatter to shipped +

+
+

+ Interactive product demo pattern — show the file, show the pipeline, show the gate. +

+
+ + +
+ {steps.map((step) => ( + +
+
+ + {step.n} + + +
+

{step.title}

+

{step.body}

+

{step.code}

+
+
+ ))} + +
+
+ ); +} diff --git a/apps/web/components/marketing/logo.tsx b/apps/web/components/marketing/logo.tsx new file mode 100644 index 0000000..8aff656 --- /dev/null +++ b/apps/web/components/marketing/logo.tsx @@ -0,0 +1,61 @@ +import Link from "next/link"; + +import { cn } from "@/lib/utils"; + +type LogoProps = { + className?: string; + showTagline?: boolean; + size?: "sm" | "md" | "lg"; + variant?: "dark" | "light"; +}; + +const sizeClasses = { + sm: { rule: "w-8", word: "text-xl tracking-[0.22em]", tag: "text-[10px]" }, + md: { rule: "w-10", word: "text-2xl tracking-[0.24em]", tag: "text-xs" }, + lg: { rule: "w-14", word: "text-4xl tracking-[0.26em]", tag: "text-sm" }, +}; + +export function Logo({ + className, + showTagline = false, + size = "md", + variant = "light", +}: LogoProps) { + const s = sizeClasses[size]; + const isDark = variant === "dark"; + + return ( + + + + SBMTR + + {showTagline ? ( + + Submatter + + ) : null} + + ); +} diff --git a/apps/web/components/marketing/product-demo.tsx b/apps/web/components/marketing/product-demo.tsx new file mode 100644 index 0000000..6424fc3 --- /dev/null +++ b/apps/web/components/marketing/product-demo.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { motion, useReducedMotion } from "motion/react"; +import { Bot, CheckCircle2, GitBranch, Terminal } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const pipelineSteps = [ + { role: "architect", model: "opus", status: "done" as const }, + { role: "implementer", model: "fast", status: "active" as const }, + { role: "reviewer", model: "fast", status: "queued" as const }, +]; + +export function ProductDemo({ className }: { className?: string }) { + const reduceMotion = useReducedMotion(); + + return ( +
+
+
+ + + +
+

+ submatter — convoy · mcp-claim-task-tool +

+
+ +
+
+
+ + plans/ +
+
+            ---{"\n"}
+            kind
+            : task{"\n"}
+            slug
+            : mcp-claim-task-tool{"\n"}
+            status
+            : 
+            in_progress
+            {"\n"}
+            priority
+            : P1{"\n"}
+            model_policy
+            : composer-fast{"\n"}
+            ---
+            ▍
+          
+
+ +
+
+
+ + + Convoy pipeline + +
+ + + MCP live + +
+ +
    + {pipelineSteps.map((step, index) => ( + +
    + {step.status === "done" ? ( + + ) : ( + + )} + + role-{step.role} + +
    + {step.model} +
    + ))} +
+ +
+ claim_task · workspace_id scoped · run ar_8f2a +
+
+
+
+ ); +} diff --git a/apps/web/components/marketing/reveal.tsx b/apps/web/components/marketing/reveal.tsx new file mode 100644 index 0000000..103ccce --- /dev/null +++ b/apps/web/components/marketing/reveal.tsx @@ -0,0 +1,90 @@ +"use client"; + +import type { ReactNode } from "react"; +import { motion, useReducedMotion } from "motion/react"; + +type RevealProps = { + children: ReactNode; + className?: string; + delay?: number; + y?: number; +}; + +export function Reveal({ children, className, delay = 0, y = 24 }: RevealProps) { + const reduceMotion = useReducedMotion(); + + if (reduceMotion) { + return
{children}
; + } + + return ( + + {children} + + ); +} + +type StaggerProps = { + children: ReactNode; + className?: string; + stagger?: number; +}; + +export function Stagger({ children, className, stagger = 0.08 }: StaggerProps) { + const reduceMotion = useReducedMotion(); + + if (reduceMotion) { + return
{children}
; + } + + return ( + + {children} + + ); +} + +export function StaggerItem({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { + const reduceMotion = useReducedMotion(); + + if (reduceMotion) { + return
{children}
; + } + + return ( + + {children} + + ); +} diff --git a/apps/web/components/marketing/site-footer.tsx b/apps/web/components/marketing/site-footer.tsx new file mode 100644 index 0000000..563e958 --- /dev/null +++ b/apps/web/components/marketing/site-footer.tsx @@ -0,0 +1,44 @@ +import Link from "next/link"; + +import { Logo } from "@/components/marketing/logo"; + +const footerLinks = [ + { href: "/landing#features", label: "Features" }, + { href: "/landing#pipeline", label: "Pipeline" }, + { href: "/landing/pricing", label: "Pricing" }, + { href: "/sign-in", label: "Sign in" }, +] as const; + +export function SiteFooter() { + return ( +
+
+
+ +

+ Subagents. Frontmatter. One workspace. +

+
+ + +
+ +
+
+

© {new Date().getFullYear()} Submatter

+

sbmtr.io · built for agent pipelines

+
+
+
+ ); +} diff --git a/apps/web/components/marketing/site-header.tsx b/apps/web/components/marketing/site-header.tsx new file mode 100644 index 0000000..8c285be --- /dev/null +++ b/apps/web/components/marketing/site-header.tsx @@ -0,0 +1,52 @@ +import Link from "next/link"; + +import { Logo } from "@/components/marketing/logo"; +import { cn } from "@/lib/utils"; + +const nav = [ + { href: "/landing#features", label: "Features" }, + { href: "/landing#pipeline", label: "Pipeline" }, + { href: "/landing/pricing", label: "Pricing" }, +] as const; + +export function SiteHeader({ className }: { className?: string }) { + return ( +
+
+ + + + +
+ + Sign in + + + Start free + +
+
+
+ ); +} diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 2503fb6..0a02f53 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -6,6 +6,7 @@ export default auth((req) => { const isPublic = pathname === "/" || + pathname.startsWith("/landing") || pathname.startsWith("/sign-in") || pathname.startsWith("/sign-up") || pathname.startsWith("/api/auth"); diff --git a/apps/web/package.json b/apps/web/package.json index 6ea6ee9..fcc5482 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -63,6 +63,7 @@ "clsx": "^2.1.1", "drizzle-orm": "^0.38.0", "lucide-react": "^0.468.0", + "motion": "^12.42.2", "next": "^15.1.0", "next-auth": "^5.0.0-beta.25", "next-themes": "^0.4.4", diff --git a/apps/web/styles/globals.css b/apps/web/styles/globals.css index 5c03f9e..a31ca06 100644 --- a/apps/web/styles/globals.css +++ b/apps/web/styles/globals.css @@ -19,6 +19,11 @@ --teal: 168 76% 42%; --teal-foreground: 0 0% 100%; + --matter: 32 92% 50%; + --matter-foreground: 220 25% 12%; + --agent: 168 55% 36%; + --agent-foreground: 0 0% 100%; + --secondary: 210 40% 96%; --secondary-foreground: 222 47% 11%; diff --git a/apps/web/styles/marketing.css b/apps/web/styles/marketing.css new file mode 100644 index 0000000..52df5bb --- /dev/null +++ b/apps/web/styles/marketing.css @@ -0,0 +1,160 @@ +/* Submatter marketing — Signal Editorial (ui-ux-pro-max: Swiss + interactive demo + bold type) */ + +.marketing-theme { + --background: 40 18% 96%; + --foreground: 220 20% 8%; + + --card: 0 0% 100%; + --card-foreground: 220 20% 8%; + + --primary: 228 45% 22%; + --primary-foreground: 40 20% 98%; + + --matter: 32 95% 52%; + --matter-foreground: 220 20% 8%; + + --agent: 162 72% 38%; + --agent-foreground: 0 0% 100%; + + --secondary: 40 12% 92%; + --secondary-foreground: 220 20% 8%; + + --muted: 40 10% 93%; + --muted-foreground: 220 8% 42%; + + --accent: 228 30% 94%; + --accent-foreground: 228 45% 22%; + + --border: 40 8% 86%; + --input: 40 8% 86%; + --ring: 32 95% 52%; + + --hero: 240 6% 4%; + --hero-foreground: 40 15% 96%; + --hero-muted: 240 4% 55%; + --hero-border: 240 4% 14%; + + --radius: 0.375rem; + + color-scheme: light; +} + +.marketing-theme .font-display { + font-family: var(--font-display), system-ui, sans-serif; +} + +.marketing-theme .font-marketing { + font-family: var(--font-marketing-sans), system-ui, sans-serif; +} + +.marketing-theme .font-mono-brand { + font-family: var(--font-mono-brand), ui-monospace, monospace; +} + +/* Dot field (React Bits–style ambient grid) */ +.marketing-dot-field { + background-image: radial-gradient( + circle at center, + hsl(var(--hero-foreground) / 0.12) 1px, + transparent 1px + ); + background-size: 28px 28px; + mask-image: radial-gradient(ellipse 80% 70% at 50% 40%, black 20%, transparent 75%); +} + +.marketing-scanline { + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + hsl(var(--hero-foreground) / 0.02) 2px, + hsl(var(--hero-foreground) / 0.02) 4px + ); + pointer-events: none; +} + +/* Border glow card (React Bits Border Glow–inspired) */ +.marketing-glow-card { + position: relative; + background: hsl(var(--card)); + transition: box-shadow 200ms ease, border-color 200ms ease; +} + +.marketing-glow-card::before { + content: ""; + position: absolute; + inset: -1px; + border-radius: inherit; + padding: 1px; + background: linear-gradient( + 135deg, + hsl(var(--matter) / 0), + hsl(var(--matter) / 0.35), + hsl(var(--agent) / 0.2), + hsl(var(--matter) / 0) + ); + -webkit-mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + opacity: 0; + transition: opacity 200ms ease; + pointer-events: none; +} + +.marketing-glow-card:hover::before, +.marketing-glow-card:focus-within::before { + opacity: 1; +} + +.marketing-glow-card:hover { + box-shadow: + 0 0 0 1px hsl(var(--border)), + 0 12px 40px -12px hsl(var(--foreground) / 0.12); +} + +@keyframes marketing-blink { + 0%, + 49% { + opacity: 1; + } + 50%, + 100% { + opacity: 0; + } +} + +.marketing-cursor-blink { + animation: marketing-blink 1.1s step-end infinite; +} + +@keyframes marketing-pulse-dot { + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.55; + transform: scale(0.92); + } +} + +.marketing-pulse-dot { + animation: marketing-pulse-dot 2s ease-in-out infinite; +} + +@media (prefers-reduced-motion: reduce) { + .marketing-cursor-blink, + .marketing-pulse-dot { + animation: none; + } + + .marketing-glow-card::before { + transition: none; + } +} diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts index 38c64eb..3106b54 100644 --- a/apps/web/tailwind.config.ts +++ b/apps/web/tailwind.config.ts @@ -55,6 +55,14 @@ const config: Config = { DEFAULT: "hsl(var(--teal))", foreground: "hsl(var(--teal-foreground))", }, + matter: { + DEFAULT: "hsl(var(--matter))", + foreground: "hsl(var(--matter-foreground, 0 0% 100%))", + }, + agent: { + DEFAULT: "hsl(var(--agent))", + foreground: "hsl(var(--agent-foreground, 0 0% 100%))", + }, }, borderRadius: { lg: "var(--radius)", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd546d5..2d06962 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -290,6 +290,9 @@ importers: lucide-react: specifier: ^0.468.0 version: 0.468.0(react@19.2.4) + motion: + specifier: ^12.42.2 + version: 12.42.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next: specifier: ^15.1.0 version: 15.5.14(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -3835,6 +3838,20 @@ packages: resolution: {integrity: sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ==} engines: {node: ^14.13.1 || >=16.0.0} + framer-motion@12.42.2: + resolution: {integrity: sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -4362,6 +4379,26 @@ packages: mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + motion-dom@12.42.2: + resolution: {integrity: sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==} + + motion-utils@12.39.0: + resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} + + motion@12.42.2: + resolution: {integrity: sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -8719,6 +8756,15 @@ snapshots: fractional-indexing@3.2.0: {} + framer-motion@12.42.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + motion-dom: 12.42.2 + motion-utils: 12.39.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + fresh@2.0.0: {} fsevents@2.3.3: @@ -9250,6 +9296,20 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 + motion-dom@12.42.2: + dependencies: + motion-utils: 12.39.0 + + motion-utils@12.39.0: {} + + motion@12.42.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + framer-motion: 12.42.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + ms@2.1.3: {} mz@2.7.0: From 35def387aee43dd9a9df22eae6ee1e1f0c6cfb72 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Mon, 3 Aug 2026 10:48:36 -0500 Subject: [PATCH 2/4] docs(plans): add Autensa benchmark tasks to automation foundation roadmap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Map reliability, operator UX, and spend-control follow-ons onto epics 1–9 so worker, webhooks, and fleet work have bead-scale task briefs. Co-authored-by: Cursor --- .../Epic-apps-orchestrator.md | 8 +- .../Task-orchestrator-auto-nudge.md | 64 ++++++++++++++++ .../Epic-apps-worker-v1.md | 11 ++- .../Task-worker-checkpoint-resume.md | 66 +++++++++++++++++ .../Task-worker-minimal-tool-loop.md | 67 +++++++++++++++++ .../Task-worker-operator-notes.md | 73 +++++++++++++++++++ .../Epic-connected-repos.md | 8 +- .../Task-repo-setup-preflight.md | 70 ++++++++++++++++++ .../Epic-fleet-dashboard.md | 14 +++- .../Task-convoy-activity-feed-sse.md | 63 ++++++++++++++++ .../Task-fleet-rollup-dashboard.md | 58 +++++++++++++++ .../Epic-forge-webhooks.md | 11 ++- .../Task-forge-webhook-receiver.md | 59 +++++++++++++++ .../Task-pr-checks-recovery-panel.md | 62 ++++++++++++++++ .../Epic-mission-control-gates-ui.md | 11 ++- .../Task-automation-tiers.md | 68 +++++++++++++++++ .../Task-convoy-dag-visualization.md | 58 +++++++++++++++ .../Epic-model-provider-registry.md | 28 ++++--- .../Task-cost-budget-ui-and-caps.md | 63 ++++++++++++++++ .../Plan-automation-foundation.md | 58 ++++++++++++--- 20 files changed, 886 insertions(+), 34 deletions(-) create mode 100644 plans/Plan-automation-foundation/Epic-apps-orchestrator/Task-orchestrator-auto-nudge.md create mode 100644 plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-checkpoint-resume.md create mode 100644 plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-minimal-tool-loop.md create mode 100644 plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-operator-notes.md create mode 100644 plans/Plan-automation-foundation/Epic-connected-repos/Task-repo-setup-preflight.md create mode 100644 plans/Plan-automation-foundation/Epic-fleet-dashboard/Task-convoy-activity-feed-sse.md create mode 100644 plans/Plan-automation-foundation/Epic-fleet-dashboard/Task-fleet-rollup-dashboard.md create mode 100644 plans/Plan-automation-foundation/Epic-forge-webhooks/Task-forge-webhook-receiver.md create mode 100644 plans/Plan-automation-foundation/Epic-forge-webhooks/Task-pr-checks-recovery-panel.md create mode 100644 plans/Plan-automation-foundation/Epic-mission-control-gates-ui/Task-automation-tiers.md create mode 100644 plans/Plan-automation-foundation/Epic-mission-control-gates-ui/Task-convoy-dag-visualization.md create mode 100644 plans/Plan-automation-foundation/Epic-model-provider-registry/Task-cost-budget-ui-and-caps.md diff --git a/plans/Plan-automation-foundation/Epic-apps-orchestrator/Epic-apps-orchestrator.md b/plans/Plan-automation-foundation/Epic-apps-orchestrator/Epic-apps-orchestrator.md index 61e9638..eb4509c 100644 --- a/plans/Plan-automation-foundation/Epic-apps-orchestrator/Epic-apps-orchestrator.md +++ b/plans/Plan-automation-foundation/Epic-apps-orchestrator/Epic-apps-orchestrator.md @@ -7,7 +7,7 @@ status: done priority: P1 tenant_id: global cursor_epic_id: null -updated_at: "2026-07-03" +updated_at: "2026-08-03" --- # Epic objective @@ -55,6 +55,12 @@ Each job calls `resolveModel(workspace, roleId, convoy, brief)` and passes `Reso - [x] [Task-shared-orchestrator-driver](./Task-shared-orchestrator-driver.md) - [x] [Task-apps-orchestrator-service](./Task-apps-orchestrator-service.md) +## Follow-on tasks (Autensa benchmark) + +| Task | Link | +|------|------| +| Agent health auto-nudge & zombie recovery | [Task-orchestrator-auto-nudge](./Task-orchestrator-auto-nudge.md) | + ## Note Supersedes [Epic-optional-orchestrator](../../Plan-agent-coordination/Epic-optional-orchestrator/Epic-optional-orchestrator.md) deferred stub with a concrete Echodo-native design. diff --git a/plans/Plan-automation-foundation/Epic-apps-orchestrator/Task-orchestrator-auto-nudge.md b/plans/Plan-automation-foundation/Epic-apps-orchestrator/Task-orchestrator-auto-nudge.md new file mode 100644 index 0000000..ea8aed1 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-apps-orchestrator/Task-orchestrator-auto-nudge.md @@ -0,0 +1,64 @@ +--- +kind: task +slug: orchestrator-auto-nudge +title: Orchestrator agent health — auto-nudge and zombie recovery +plan_slug: automation-foundation +epic_slug: apps-orchestrator +status: draft +priority: P2 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-03" +--- + +# Task summary + +Extend stall detection beyond marking convoys `blocked`: detect zombie agent runs, optionally re-dispatch or nudge, and emit health events for mission control / fleet feed. + +## Description + +**Benchmark:** Autensa agent health monitoring with auto-nudge and convoy health panel. Echodo already has `ORCHESTRATOR_STALL_MINUTES` and `detectStalledConvoys` — this task adds **recovery actions** and observability. + +### Health states + +| State | Trigger | Default action | +|-------|---------|----------------| +| **Stalled** | No heartbeat on open `agent_run` for N minutes | Mark convoy `blocked`; surface in gate inbox sidebar | +| **Zombie** | Run open but worker process gone | Re-enqueue job with `resume_from_checkpoint` when Epic 7 checkpoint task ships | +| **Nudge** | Soft stall (configurable) | Log `convoy_events` health event; optional Redis ping to worker | + +### Config (env) + +- `ORCHESTRATOR_NUDGE_MINUTES` (optional, < stall threshold) +- `ORCHESTRATOR_AUTO_REDISPATCH` (default false — opt-in for unattended dogfood) + +### Events + +Append `convoy_events` with `event_type: health` and metadata `{ reason, action, agent_run_id }` for activity feed (Epic 9). + +## Dependencies + +- Epic 6 orchestrator service (shipped) +- [Task-worker-checkpoint-resume](../Epic-apps-worker-v1/Task-worker-checkpoint-resume.md) for meaningful re-dispatch + +## Subtasks + +- [ ] Extend `detectStalledConvoys` with nudge vs hard-stall tiers +- [ ] Optional re-dispatch path on zombie detection +- [ ] `convoy_events` health event writer +- [ ] Document in `docs/orchestrator.md` + +## Estimation + +M + +## Acceptance criteria + +- [ ] Stalled run moves convoy to `blocked` and writes health event +- [ ] With `ORCHESTRATOR_AUTO_REDISPATCH=1`, zombie run triggers new job enqueue +- [ ] No unbounded re-dispatch loops (max retries per brief configurable) + +## Links + +- Epic: `./Epic-apps-orchestrator.md` diff --git a/plans/Plan-automation-foundation/Epic-apps-worker-v1/Epic-apps-worker-v1.md b/plans/Plan-automation-foundation/Epic-apps-worker-v1/Epic-apps-worker-v1.md index 11c94b7..0e0e1c6 100644 --- a/plans/Plan-automation-foundation/Epic-apps-worker-v1/Epic-apps-worker-v1.md +++ b/plans/Plan-automation-foundation/Epic-apps-worker-v1/Epic-apps-worker-v1.md @@ -7,7 +7,7 @@ status: in_progress priority: P1 tenant_id: global cursor_epic_id: null -updated_at: "2026-07-03" +updated_at: "2026-08-03" --- # Epic objective @@ -25,11 +25,13 @@ Role prompt = `.cursor/agents/role-*.md` + brief path. Model endpoint points at - `WorkerSession` adapter interface (for future optional OpenHands plug-in). - `pnpm lint && pnpm test` in allowlisted commands. - Branch + PR draft output (forge adapter from Epic 3). +- Checkpoint/resume and operator mid-build notes (Autensa benchmark — see tasks below). **Out of scope** - Browser automation, multi-agent delegation. - Feature parity with OpenHands. +- OpenClaw Gateway coupling. ## Dependencies @@ -43,7 +45,12 @@ Role prompt = `.cursor/agents/role-*.md` + brief path. Model endpoint points at - [ ] One brief from `.convoys/` completes unattended on connected repo. - [ ] `complete_task` includes model alias + token totals. +- [ ] Crashed worker can resume from checkpoint (follow-on task). ## Tasks -_To be decomposed when epic graduates to `ready`._ +| Task | Link | Status | +|------|------|--------| +| Minimal tool loop + worktree | [Task-worker-minimal-tool-loop](./Task-worker-minimal-tool-loop.md) | ready | +| Checkpoint & crash recovery | [Task-worker-checkpoint-resume](./Task-worker-checkpoint-resume.md) | draft | +| Operator notes mid-build | [Task-worker-operator-notes](./Task-worker-operator-notes.md) | draft | diff --git a/plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-checkpoint-resume.md b/plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-checkpoint-resume.md new file mode 100644 index 0000000..d3aac2e --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-checkpoint-resume.md @@ -0,0 +1,66 @@ +--- +kind: task +slug: worker-checkpoint-resume +title: Worker checkpoint and crash recovery +plan_slug: automation-foundation +epic_slug: apps-worker-v1 +status: draft +priority: P1 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-03" +--- + +# Task summary + +Persist worker session progress at configurable checkpoints so a crashed or stalled job resumes from the last checkpoint instead of restarting the brief from scratch. + +## Description + +**Benchmark:** [Autensa mission-control](https://github.com/crshdn/mission-control) checkpoint & crash recovery — study their checkpoint restore API and per-task history UX; implement Echodo-native equivalents on Postgres. + +### Schema + +Add `worker_checkpoints` (or JSONB on `agent_runs`) workspace-scoped: + +- `agent_run_id`, `convoy_id`, `brief_id` +- `checkpoint_seq`, `tool_history` (bounded), `files_touched`, `branch`, `worktree_path` +- `occurred_at` + +### Worker behavior + +- Save checkpoint after each tool round (configurable via `WORKER_CHECKPOINT_EVERY_N_STEPS`, default 1). +- On job start: if open run has latest checkpoint, resume tool loop from saved state. +- Orchestrator stall detector can trigger re-dispatch with `resume_from_checkpoint: true`. + +### Operator surface (minimal v1) + +- Mission control detail panel: checkpoint count + last checkpoint time (full history deferred). + +## Dependencies + +- [Task-worker-minimal-tool-loop](./Task-worker-minimal-tool-loop.md) (baseline loop must exist). + +## Subtasks + +- [ ] Zod schemas in `packages/shared/worker` +- [ ] Drizzle migration + workspace-scoped queries +- [ ] Checkpoint write in `minimal-session` / `handle-job` +- [ ] Resume path on re-dispatch +- [ ] Env docs in `docs/worker.md` + +## Estimation + +M + +## Acceptance criteria + +- [ ] Kill worker mid-brief; re-dispatch resumes from last tool step without re-cloning +- [ ] Checkpoints filtered by `workspace_id` +- [ ] Checkpoint payload does not store API keys or full file contents (paths + hashes only) + +## Links + +- Epic: `./Epic-apps-worker-v1.md` +- Blocks: reliable unattended dogfood for Epic 8 diff --git a/plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-minimal-tool-loop.md b/plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-minimal-tool-loop.md new file mode 100644 index 0000000..d722081 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-minimal-tool-loop.md @@ -0,0 +1,67 @@ +--- +kind: task +slug: worker-minimal-tool-loop +title: apps/worker — minimal tool loop and worktree-per-brief +plan_slug: automation-foundation +epic_slug: apps-worker-v1 +status: ready +priority: P1 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-03" +agent_prompt: | + Epic: apps/worker v1. Read docs/worker.md and docs/adr-model-routing.md. + Consume orchestrator jobs from Redis; resolve model via workspace registry. + claim_task / complete_task parity with MCP. Run pnpm lint && pnpm type-check + && pnpm test before done. +--- + +# Task summary + +Ship the baseline `apps/worker` consumer: dequeue `role_dispatch` jobs, run `WorkerSession` tool loop against a git worktree, and close with model + token metadata on `complete_task`. + +## Description + +Core v1 scope from [Epic-apps-worker-v1](./Epic-apps-worker-v1.md). Builds on existing package skeleton (`handle-job.ts`, `minimal-session.ts`, allowlisted tools, worktree helpers). + +### Worker loop + +- `BLPOP` on `echodo:orchestrator:jobs` (same key as orchestrator). +- Load brief path + role from job payload; resolve model via shared resolver. +- `claim_task` on matching backlog item; run `read_file` / `write_file` / `grep` / `run_command` (allowlist: `pnpm lint`, `pnpm type-check`, `pnpm test`, `git status`, `git diff`). +- `complete_task` with `modelAlias`, token totals, outcome. + +### Git + +- One worktree per brief under `WORKER_WORKTREE_BASE`. +- Branch naming convention documented for Epic 8 PR linking. + +### PR output + +- Open draft PR via forge adapter from connected repo (Epic 3). + +## Subtasks + +- [ ] Wire live dequeue loop (non-dry-run) end-to-end +- [ ] Role prompt = `.cursor/agents/role-*.md` + brief markdown path +- [ ] Worktree create/teardown on success and failure +- [ ] Draft PR via forge adapter when implementer completes +- [ ] `docs/worker.md` matches shipped behavior + +## Estimation + +L + +## Acceptance criteria + +- [ ] One implementer brief from `.convoys/` completes unattended on a connected repo +- [ ] `complete_task` includes model alias + token totals +- [ ] Failed runs tear down worktree and mark run `failed` +- [ ] `WORKER_DRY_RUN=1` still exits cleanly without Redis + +## Links + +- Epic: `./Epic-apps-worker-v1.md` +- Plan: `../Plan-automation-foundation.md` +- Design reference: Autensa convoy build phase (concepts only — no OpenClaw dependency) diff --git a/plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-operator-notes.md b/plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-operator-notes.md new file mode 100644 index 0000000..9b2406c --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-operator-notes.md @@ -0,0 +1,73 @@ +--- +kind: task +slug: worker-operator-notes +title: Operator notes — mid-build context injection +plan_slug: automation-foundation +epic_slug: apps-worker-v1 +status: draft +priority: P2 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-03" +--- + +# Task summary + +Let operators send **queued notes** and **direct messages** to an active worker session without waiting for a human gate. + +## Description + +**Benchmark:** Autensa Operator Chat — two delivery modes: + +| Mode | Delivery | Use case | +|------|----------|----------| +| **Queued note** | Injected at next checkpoint / tool round | "Use the existing auth middleware" | +| **Direct message** | Prepended to very next model call | Urgent course correction | + +Echodo scope is convoy-scoped (not per arbitrary task board). Gate co-pilot (Epic 5) stays gate-focused; this extends intervention to `in_progress` convoys. + +### Data model + +`convoy_operator_messages` table (workspace-scoped): + +- `convoy_id`, `brief_id` (optional), `agent_run_id` (optional) +- `delivery`: `queued` | `direct` +- `body`, `author_user_id`, `delivered_at`, `created_at` + +### APIs + +- tRPC `convoys.postOperatorNote` (workspace member) +- Worker polls or receives note via job metadata on each tool round + +### UI + +- Mission control detail panel: chat thread when convoy status is `in_progress` +- Distinct from gate co-pilot (separate tab or section) + +## Dependencies + +- [Task-worker-minimal-tool-loop](./Task-worker-minimal-tool-loop.md) +- Epic 5 mission control detail panel (extend, do not rewrite) + +## Subtasks + +- [ ] Schema + tRPC mutations/list +- [ ] Worker consumes queued notes between tool steps +- [ ] Worker consumes direct messages before next `generateText` +- [ ] Mission control UI thread for `in_progress` convoys + +## Estimation + +M + +## Acceptance criteria + +- [ ] Operator posts queued note; worker sees it on next tool round +- [ ] Direct message arrives before next model call when run is active +- [ ] Messages are workspace-scoped and auditable in convoy detail + +## Links + +- Epic: `./Epic-apps-worker-v1.md` +- Related: [Task-mission-control-ui](../Epic-mission-control-gates-ui/Task-mission-control-ui.md) diff --git a/plans/Plan-automation-foundation/Epic-connected-repos/Epic-connected-repos.md b/plans/Plan-automation-foundation/Epic-connected-repos/Epic-connected-repos.md index c376eea..3d3aa65 100644 --- a/plans/Plan-automation-foundation/Epic-connected-repos/Epic-connected-repos.md +++ b/plans/Plan-automation-foundation/Epic-connected-repos/Epic-connected-repos.md @@ -7,7 +7,7 @@ status: done priority: P1 tenant_id: global cursor_epic_id: null -updated_at: "2026-07-03" +updated_at: "2026-08-03" --- # Epic objective @@ -49,3 +49,9 @@ See automation foundation discussion: Vercel/Coolify-style “show me your repo. - [x] [Task-db-connected-repos-migration](./Task-db-connected-repos-migration.md) - [x] [Task-forge-adapters-trpc](./Task-forge-adapters-trpc.md) - [x] [Task-settings-connected-repos-ui](./Task-settings-connected-repos-ui.md) + +## Follow-on tasks (Autensa benchmark) + +| Task | Link | +|------|------| +| Repo setup preflight tab | [Task-repo-setup-preflight](./Task-repo-setup-preflight.md) | diff --git a/plans/Plan-automation-foundation/Epic-connected-repos/Task-repo-setup-preflight.md b/plans/Plan-automation-foundation/Epic-connected-repos/Task-repo-setup-preflight.md new file mode 100644 index 0000000..b69b69c --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-connected-repos/Task-repo-setup-preflight.md @@ -0,0 +1,70 @@ +--- +kind: task +slug: repo-setup-preflight +title: Connected repo setup preflight — readiness before automation +plan_slug: automation-foundation +epic_slug: connected-repos +status: draft +priority: P2 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-03" +--- + +# Task summary + +**Repo Setup** tab on connected repos: verify a remote is automation-ready before orchestrator/worker dispatch (git access, default branch, CI permissions, required secrets). + +## Description + +**Benchmark:** Autensa v2.5.1 Repo Setup tab — preflight checks with in-UI fixes where the forge API allows. Echodo adapts this to multitenant `repo_links` and forge adapters (Forgejo + GitHub minimum). + +### Preflight checks + +| Check | Pass criteria | +|-------|---------------| +| Git clone | Shallow clone with stored credentials | +| Default branch | API-detected default matches stored value | +| Forge API | PR list/create permission | +| CI visibility | Latest workflow run status readable | +| Actions token (GitHub) | `contents` + `pull-requests` write if applicable | +| Required secrets | Names present (values never stored in Echodo DB) | + +### UI + +- Settings → Connected repos → per-repo **Setup** tab +- Traffic-light checklist with expandable failure reasons +- Block convoy **Go** when critical checks fail (soft warning vs hard block — configurable per workspace) + +### Fixes (where API supports) + +- GitHub: suggest/set workflow token permissions; link to docs for secret creation (do not write secret values through Echodo — operator pastes in forge UI) + +## Dependencies + +- Epic 3 connected repos (shipped) +- Epic 6 orchestrator should read preflight status before enqueue + +## Subtasks + +- [ ] `repoPreflightStatus` zod schema in `packages/shared` +- [ ] Forge adapter methods: `getDefaultBranch`, `listWorkflowRuns`, `getRepoPermissions` +- [ ] tRPC `connectedRepos.runPreflight` +- [ ] Settings UI Setup tab +- [ ] Orchestrator skips dispatch when repo link `preflight_ok` is false + +## Estimation + +L + +## Acceptance criteria + +- [ ] Repo with wrong default branch shows failing check with detected branch name +- [ ] Private repo without token shows auth failure (no silent pass) +- [ ] Preflight re-runnable without reconnecting repo +- [ ] No forge secret values persisted in Postgres + +## Links + +- Epic: `./Epic-connected-repos.md` diff --git a/plans/Plan-automation-foundation/Epic-fleet-dashboard/Epic-fleet-dashboard.md b/plans/Plan-automation-foundation/Epic-fleet-dashboard/Epic-fleet-dashboard.md index e2027e7..eab8ee2 100644 --- a/plans/Plan-automation-foundation/Epic-fleet-dashboard/Epic-fleet-dashboard.md +++ b/plans/Plan-automation-foundation/Epic-fleet-dashboard/Epic-fleet-dashboard.md @@ -7,14 +7,16 @@ status: draft priority: P2 tenant_id: global cursor_epic_id: null -updated_at: "2026-07-03" +updated_at: "2026-08-03" --- # Epic objective Top-level **fleet** view: all `convoys-` workspaces (or connected repos) in one screen. Roll up convoys by status, gate inbox count, active `agent_runs`, recent spend by model alias. -Answers: “What needs me across all projects?” +Live **activity feed** (SSE) from `convoy_events` — Autensa benchmark for operator situational awareness. + +Answers: “What needs me across all projects?” and “What just happened?” ## In scope / out of scope @@ -23,10 +25,12 @@ Answers: “What needs me across all projects?” - Route `/fleet` or home dashboard for operator account. - Cross-workspace tRPC (membership-scoped). - Links into per-repo mission control (Epic 5). +- SSE activity stream (workspace + fleet scope). **Out of scope** - Multi-tenant fleet across unrelated customers (operator's own repos only for v1). +- Autopilot research/ideation feeds. ## Dependencies @@ -36,7 +40,11 @@ Answers: “What needs me across all projects?” ## Acceptance criteria - [ ] Operator with two connected repos sees combined “3 awaiting approval” badge. +- [ ] New convoy event appears in activity feed without page refresh. ## Tasks -_To be decomposed when epic graduates to `ready`._ +| Task | Link | Status | +|------|------|--------| +| Fleet rollup dashboard | [Task-fleet-rollup-dashboard](./Task-fleet-rollup-dashboard.md) | ready | +| Live activity feed (SSE) | [Task-convoy-activity-feed-sse](./Task-convoy-activity-feed-sse.md) | draft | diff --git a/plans/Plan-automation-foundation/Epic-fleet-dashboard/Task-convoy-activity-feed-sse.md b/plans/Plan-automation-foundation/Epic-fleet-dashboard/Task-convoy-activity-feed-sse.md new file mode 100644 index 0000000..bd88a07 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-fleet-dashboard/Task-convoy-activity-feed-sse.md @@ -0,0 +1,63 @@ +--- +kind: task +slug: convoy-activity-feed-sse +title: Live convoy activity feed (SSE) +plan_slug: automation-foundation +epic_slug: fleet-dashboard +status: draft +priority: P2 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-03" +--- + +# Task summary + +Real-time activity stream from `convoy_events` for mission control and fleet dashboard — research/build/gate/health events without full page refresh. + +## Description + +**Benchmark:** Autensa live activity feed (SSE). Echodo already appends `convoy_events` for role completion, transitions, and (future) health events — this task exposes them as a **subscription surface**. + +### Transport + +- Prefer Next.js Route Handler `GET /api/convoys/activity/stream` with SSE (workspace-scoped via session) +- Alternative: tRPC subscription if infra already supports it — pick one, document in ADR addendum + +### Event types surfaced + +- `role_complete`, `transition`, `health`, `gate_action` +- Payload: `convoy_id`, `title`, `event_type`, `role`, `outcome`, `occurred_at`, `estimated_cost_usd` (when present) + +### UI + +- Mission control: collapsible **Activity** strip (last 20 events for current workspace) +- Fleet dashboard: global feed with product/repo filter +- Fallback: React Query poll every 30s when SSE unavailable + +## Dependencies + +- Epic 4 `convoy_events` (shipped) +- [Task-orchestrator-auto-nudge](../Epic-apps-orchestrator/Task-orchestrator-auto-nudge.md) for health events (soft — feed works without it) + +## Subtasks + +- [ ] SSE route with workspace membership check +- [ ] `convoy_events` tail query + fan-out on insert (LISTEN/NOTIFY or short poll server-side) +- [ ] `ActivityFeed` client component +- [ ] Rate limit / connection cap per user + +## Estimation + +M + +## Acceptance criteria + +- [ ] New `role_complete` event appears in mission control within 5s without refresh +- [ ] User cannot subscribe to another workspace's events +- [ ] Connection closes cleanly on navigation + +## Links + +- Epic: `./Epic-fleet-dashboard.md` diff --git a/plans/Plan-automation-foundation/Epic-fleet-dashboard/Task-fleet-rollup-dashboard.md b/plans/Plan-automation-foundation/Epic-fleet-dashboard/Task-fleet-rollup-dashboard.md new file mode 100644 index 0000000..0dde38b --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-fleet-dashboard/Task-fleet-rollup-dashboard.md @@ -0,0 +1,58 @@ +--- +kind: task +slug: fleet-rollup-dashboard +title: Fleet dashboard — cross-repo convoy rollup +plan_slug: automation-foundation +epic_slug: fleet-dashboard +status: ready +priority: P2 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-03" +--- + +# Task summary + +Operator home at `/fleet` (or authenticated landing): rollup gate inbox counts, convoys by status, and active `agent_runs` across all workspaces the user belongs to. + +## Description + +Core Epic 9 scope. Answers: **"What needs me across all projects?"** + +### UI + +- Card per connected repo / convoy workspace +- Badge: N awaiting approval (sum of `GATE_INBOX_STATUSES`) +- Quick links into `/{workspaceSlug}/mission-control?convoy=...` +- Active runs count + blocked count + +### API + +- tRPC `fleet.summary` — membership-scoped cross-workspace query (no client-provided workspace_id trust) +- Reuse `MISSION_CONTROL_KANBAN_COLUMNS` counts per workspace + +## Dependencies + +- Epic 4, 5 (shipped) + +## Subtasks + +- [ ] `fleet` tRPC router with membership joins +- [ ] `/fleet` route (app layout) +- [ ] Icon rail / top-header link when user has ≥2 workspaces +- [ ] Empty state for single-workspace operators + +## Estimation + +M + +## Acceptance criteria + +- [ ] Operator with two connected repos sees combined "3 awaiting approval" badge +- [ ] Clicking badge navigates to correct workspace mission control with inbox focused +- [ ] Query performs one round-trip (no N+1 per workspace in UI) + +## Links + +- Epic: `./Epic-fleet-dashboard.md` diff --git a/plans/Plan-automation-foundation/Epic-forge-webhooks/Epic-forge-webhooks.md b/plans/Plan-automation-foundation/Epic-forge-webhooks/Epic-forge-webhooks.md index 86b0ede..2b0f8d0 100644 --- a/plans/Plan-automation-foundation/Epic-forge-webhooks/Epic-forge-webhooks.md +++ b/plans/Plan-automation-foundation/Epic-forge-webhooks/Epic-forge-webhooks.md @@ -7,13 +7,15 @@ status: draft priority: P2 tenant_id: global cursor_epic_id: null -updated_at: "2026-07-03" +updated_at: "2026-08-03" --- # Epic objective Webhook receivers on Echodo for connected forges: PR opened → `pr_review`; PR merged → trigger doc-writer job + `merged`; CI failed → optional `blocked` or notify. Supports Forgejo and GitHub adapters minimum. +PR checks recovery UI in mission control when CI fails (Autensa benchmark). + ## In scope / out of scope **In scope** @@ -21,6 +23,7 @@ Webhook receivers on Echodo for connected forges: PR opened → `pr_review`; PR - `POST /api/webhooks/forge/:repoLinkId` with signature verification. - Map events to `transition_convoy_status(actor: automation)`. - Link PR branch to convoy/brief via branch naming convention or PR body marker. +- PR checks recovery panel (classify failures, rerun where API allows). **Out of scope** @@ -33,7 +36,11 @@ Webhook receivers on Echodo for connected forges: PR opened → `pr_review`; PR ## Acceptance criteria - [ ] Merge on Forgejo moves convoy from `pr_review` to `merged` when Gate 2 approved. +- [ ] Mission control shows PR check failures with recovery actions for `pr_review` convoys. ## Tasks -_To be decomposed when epic graduates to `ready`._ +| Task | Link | Status | +|------|------|--------| +| Webhook receiver | [Task-forge-webhook-receiver](./Task-forge-webhook-receiver.md) | ready | +| PR checks recovery panel | [Task-pr-checks-recovery-panel](./Task-pr-checks-recovery-panel.md) | draft | diff --git a/plans/Plan-automation-foundation/Epic-forge-webhooks/Task-forge-webhook-receiver.md b/plans/Plan-automation-foundation/Epic-forge-webhooks/Task-forge-webhook-receiver.md new file mode 100644 index 0000000..5506963 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-forge-webhooks/Task-forge-webhook-receiver.md @@ -0,0 +1,59 @@ +--- +kind: task +slug: forge-webhook-receiver +title: Forge webhook receiver — PR and CI events +plan_slug: automation-foundation +epic_slug: forge-webhooks +status: ready +priority: P2 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-03" +--- + +# Task summary + +`POST /api/webhooks/forge/:repoLinkId` with HMAC signature verification; map forge events to `transition_convoy_status(actor: automation)`. + +## Description + +Core Epic 8 scope. Link PRs to convoys via branch naming convention (`convoy//brief-N`) or PR body marker (`echodo-convoy: `). + +### Events (minimum) + +| Forge event | Convoy transition | +|-------------|-------------------| +| PR opened | Ensure convoy in `pr_review` (if Gate 2 not yet human-approved, notify only) | +| PR merged | `pr_review` → `merged` when Gate 2 already approved | +| CI completed (failure) | Optional `blocked` or `convoy_events` notify | + +### Security + +- Per-repo webhook secret in `repo_links` metadata (reference env var name, not value) +- Zod-validated payload per `forge_type` + +## Dependencies + +- Epic 3, 4, 7 + +## Subtasks + +- [ ] Route handler + signature verification +- [ ] GitHub + Forgejo payload parsers +- [ ] Convoy lookup by branch/body marker +- [ ] Register webhook URL helper on connect (optional UX) + +## Estimation + +M + +## Acceptance criteria + +- [ ] Merge webhook moves convoy from `pr_review` to `merged` when Gate 2 approved +- [ ] Invalid signature returns 401 +- [ ] Unknown branch logs and returns 200 (no retry storm) + +## Links + +- Epic: `./Epic-forge-webhooks.md` diff --git a/plans/Plan-automation-foundation/Epic-forge-webhooks/Task-pr-checks-recovery-panel.md b/plans/Plan-automation-foundation/Epic-forge-webhooks/Task-pr-checks-recovery-panel.md new file mode 100644 index 0000000..60327b0 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-forge-webhooks/Task-pr-checks-recovery-panel.md @@ -0,0 +1,62 @@ +--- +kind: task +slug: pr-checks-recovery-panel +title: PR checks recovery panel in mission control +plan_slug: automation-foundation +epic_slug: forge-webhooks +status: draft +priority: P2 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-03" +--- + +# Task summary + +When a convoy is in `pr_review`, show linked PR CI status in mission control with classified failures and recovery actions (rerun jobs, link to repo setup). + +## Description + +**Benchmark:** Autensa v2.5.1 PR checks recovery panel — classify failures as retryable, repo setup, or external provider; offer rerun where forge API supports it. + +### UI (convoy detail panel) + +- PR link + check run list (name, status, conclusion) +- Failure classification badge per check +- Actions: **Rerun failed jobs** (GitHub Actions API), **Open repo setup** (Epic 3 follow-on task) + +### Backend + +- tRPC `convoys.getPrChecks` — workspace-scoped, uses forge adapter +- `convoys.rerunFailedChecks` — admin only; audit log in `convoy_events` + +### Webhook integration + +- CI `completed` webhook refreshes cached check status on convoy metadata + +## Dependencies + +- [Task-forge-webhook-receiver](./Task-forge-webhook-receiver.md) +- [Task-repo-setup-preflight](../Epic-connected-repos/Task-repo-setup-preflight.md) (link target for setup failures) + +## Subtasks + +- [ ] Forge adapter: list check runs for PR +- [ ] Failure classifier (heuristics: permissions → setup, timeout → retryable, external → informational) +- [ ] Mission control PR checks section when status is `pr_review` +- [ ] Rerun action + event logging + +## Estimation + +M + +## Acceptance criteria + +- [ ] Failed lint CI shows retryable with working rerun (GitHub fixture or manual dogfood) +- [ ] Missing Actions permission links to repo setup tab with failing check highlighted +- [ ] No recovery action exposed without workspace admin role + +## Links + +- Epic: `./Epic-forge-webhooks.md` diff --git a/plans/Plan-automation-foundation/Epic-mission-control-gates-ui/Epic-mission-control-gates-ui.md b/plans/Plan-automation-foundation/Epic-mission-control-gates-ui/Epic-mission-control-gates-ui.md index 6438865..e6f7acf 100644 --- a/plans/Plan-automation-foundation/Epic-mission-control-gates-ui/Epic-mission-control-gates-ui.md +++ b/plans/Plan-automation-foundation/Epic-mission-control-gates-ui/Epic-mission-control-gates-ui.md @@ -7,7 +7,7 @@ status: done priority: P1 tenant_id: global cursor_epic_id: null -updated_at: "2026-07-03" +updated_at: "2026-08-03" --- # Epic objective @@ -48,3 +48,12 @@ Echodo UI for human gates and convoy visibility: - [x] [Task-trpc-convoys-router](./Task-trpc-convoys-router.md) - [x] [Task-mission-control-ui](./Task-mission-control-ui.md) + +## Follow-on tasks (Autensa benchmark) + +| Task | Link | +|------|------| +| Convoy DAG visualization | [Task-convoy-dag-visualization](./Task-convoy-dag-visualization.md) | +| Automation tiers (supervised / semi-auto) | [Task-automation-tiers](./Task-automation-tiers.md) | + +Operator mid-build chat is tracked under Epic 7: [Task-worker-operator-notes](../Epic-apps-worker-v1/Task-worker-operator-notes.md). diff --git a/plans/Plan-automation-foundation/Epic-mission-control-gates-ui/Task-automation-tiers.md b/plans/Plan-automation-foundation/Epic-mission-control-gates-ui/Task-automation-tiers.md new file mode 100644 index 0000000..3bd4460 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-mission-control-gates-ui/Task-automation-tiers.md @@ -0,0 +1,68 @@ +--- +kind: task +slug: automation-tiers +title: Automation tiers — supervised, semi-auto, full-auto gate policy +plan_slug: automation-foundation +epic_slug: mission-control-gates-ui +status: draft +priority: P2 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-03" +--- + +# Task summary + +Per-convoy (or per-workspace default) **automation tier** controlling which human gates auto-advance when CI and review agents pass. + +## Description + +**Benchmark:** Autensa automation tiers (Supervised / Semi-Auto / Full Auto). Maps onto Echodo's explicit gate model without removing human override. + +| Tier | Gate 1 (plan) | Gate 2 (merge) | Gate 3 (prod) | +|------|---------------|----------------|---------------| +| **Supervised** | Human | Human | Human | +| **Semi-auto** | Human | Auto when CI green + review agent pass | Human | +| **Full-auto** | Human | Auto when CI green | Auto when deploy webhook fires (future) | + +### Schema + +- `convoys` metadata field `automation_tier`: `supervised` | `semi_auto` | `full_auto` (default `supervised`) +- Workspace default in settings (optional) + +### Behavior + +- Orchestrator / webhook handlers consult tier before calling `transition_convoy_status` with `actor: automation` +- Mission control shows tier badge on convoy card; editable before **Go** + +### Out of scope + +- Auto-approving Gate 1 (plan always human in v1) +- Autopilot research/ideation (Autensa-specific) + +## Dependencies + +- Epic 5 gates UI +- Epic 8 webhooks for semi-auto Gate 2 + +## Subtasks + +- [ ] Zod enum in `packages/shared/convoy-lifecycle` +- [ ] Create convoy dialog + detail panel tier selector +- [ ] Orchestrator respects tier on CI webhook path +- [ ] Document tier semantics in `docs/orchestrator.md` + +## Estimation + +S + +## Acceptance criteria + +- [ ] Semi-auto convoy auto-transitions `pr_review` → `merged` on merge webhook only when CI was green at merge time +- [ ] Supervised convoy never auto-merges +- [ ] Tier visible on kanban card tooltip + +## Links + +- Epic: `./Epic-mission-control-gates-ui.md` diff --git a/plans/Plan-automation-foundation/Epic-mission-control-gates-ui/Task-convoy-dag-visualization.md b/plans/Plan-automation-foundation/Epic-mission-control-gates-ui/Task-convoy-dag-visualization.md new file mode 100644 index 0000000..d477ccf --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-mission-control-gates-ui/Task-convoy-dag-visualization.md @@ -0,0 +1,58 @@ +--- +kind: task +slug: convoy-dag-visualization +title: Convoy DAG visualization for parallel briefs +plan_slug: automation-foundation +epic_slug: mission-control-gates-ui +status: draft +priority: P2 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-03" +--- + +# Task summary + +Read-only dependency graph in mission control showing brief/subtask relationships and `multitask_group` parallel lanes. + +## Description + +**Benchmark:** Autensa Convoy Mode DAG — visual dependency graph for parallel subtasks. Echodo already has `multitask_group` on `convoy_events` and parallel implementer fan-out in the orchestrator ADR; this task adds **visibility**, not new scheduling logic. + +### Data source + +- Parse brief frontmatter / `slice_dependencies` from `.convoys//brief-*.md` (via convoy object metadata or file reconcile) +- Overlay live status from `agent_runs` + convoy status per brief + +### UI + +- Mission control detail panel tab: **Graph** +- Nodes = briefs; edges = dependencies; color = run status (pending, running, done, blocked) +- Click node selects brief in detail panel + +## Dependencies + +- Epic 5 mission control (shipped) +- Epic 6 orchestrator parallel dispatch (shipped) + +## Subtasks + +- [ ] Shared zod type `ConvoyDag` in `packages/shared` +- [ ] tRPC `convoys.getDag` +- [ ] React graph component (lightweight — e.g. layered layout, no heavy graph lib unless justified) +- [ ] Empty state when single-brief convoy + +## Estimation + +M + +## Acceptance criteria + +- [ ] Two-brief convoy with dependency shows correct edge direction +- [ ] Parallel briefs without dependency appear in same column/lane +- [ ] Blocked brief visually distinct from pending + +## Links + +- Epic: `./Epic-mission-control-gates-ui.md` diff --git a/plans/Plan-automation-foundation/Epic-model-provider-registry/Epic-model-provider-registry.md b/plans/Plan-automation-foundation/Epic-model-provider-registry/Epic-model-provider-registry.md index abf4237..92aa3f7 100644 --- a/plans/Plan-automation-foundation/Epic-model-provider-registry/Epic-model-provider-registry.md +++ b/plans/Plan-automation-foundation/Epic-model-provider-registry/Epic-model-provider-registry.md @@ -7,7 +7,7 @@ status: done priority: P1 tenant_id: global cursor_epic_id: null -updated_at: "2026-07-03" +updated_at: "2026-08-03" agent_prompt: | Epic: model provider registry (docs/adr-model-routing.md). Server-only change — no UI component library inventing; reuse components/ui/. Admin-only mutations. @@ -32,12 +32,9 @@ This epic is the first shipped piece of the automation-foundation plan and unblo - `complete_task` optional model logging fields. - `.env.example` env var documentation. -**Out of scope** +**Out of scope (moved to follow-on task)** -- Orchestrator dispatch (consumes resolver later). -- Worker inference loop. -- Parsing `model_policy` from `.convoys/*.md` files (orchestrator reads later). -- Billing/budget enforcement. +- Billing/budget enforcement → [Task-cost-budget-ui-and-caps](./Task-cost-budget-ui-and-caps.md) ## Related tasks @@ -47,9 +44,15 @@ This epic is the first shipped piece of the automation-foundation plan and unblo | DB schema and migration | `./Task-db-model-providers-migration.md` | | tRPC models router | `./Task-trpc-models-router.md` | | Settings — providers UI | `./Task-settings-ai-providers-ui.md` | -| Settings — role routing UI | `./Task-settings-role-routing-ui.md` | +| Settings — role routing UI | `./Task-settings-ai-role-routing-ui.md` | | agent_runs model fields + MCP | `./Task-agent-runs-model-logging.md` | +## Follow-on tasks (Autensa benchmark) + +| Task | Link | +|------|------| +| Cost visibility & dispatch budget caps | [Task-cost-budget-ui-and-caps](./Task-cost-budget-ui-and-caps.md) | + ## Dependencies - Depends on: existing auth + workspace admin patterns in `apps/web`. @@ -57,11 +60,11 @@ This epic is the first shipped piece of the automation-foundation plan and unblo ## Acceptance criteria -- [ ] Admin can create `claude-planning` (anthropic) and `qwen-coder` (openai_compatible + base URL) in a workspace. -- [ ] Admin can map `role-implementer` → `qwen-coder`, `role-architect` → `claude-planning`. -- [ ] `testProvider` returns success/failure without exposing API keys. -- [ ] `resolveModel()` unit tests cover override order and missing-alias error. -- [ ] At least one task in this epic completed via `claim_task` → `complete_task` with model metadata. +- [x] Admin can create `claude-planning` (anthropic) and `qwen-coder` (openai_compatible + base URL) in a workspace. +- [x] Admin can map `role-implementer` → `qwen-coder`, `role-architect` → `claude-planning`. +- [x] `testProvider` returns success/failure without exposing API keys. +- [x] `resolveModel()` unit tests cover override order and missing-alias error. +- [x] At least one task in this epic completed via `claim_task` → `complete_task` with model metadata. ## Proposed timeline @@ -71,6 +74,7 @@ This epic is the first shipped piece of the automation-foundation plan and unblo | API | Session 3 | tRPC router | | UI | Session 4–5 | Both settings pages | | Logging | Session 6 | agent_runs + MCP | +| Usage & caps | Future | Follow-on task | ## Pipeline / convoy mapping diff --git a/plans/Plan-automation-foundation/Epic-model-provider-registry/Task-cost-budget-ui-and-caps.md b/plans/Plan-automation-foundation/Epic-model-provider-registry/Task-cost-budget-ui-and-caps.md new file mode 100644 index 0000000..f350087 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-model-provider-registry/Task-cost-budget-ui-and-caps.md @@ -0,0 +1,63 @@ +--- +kind: task +slug: cost-budget-ui-and-caps +title: Cost visibility and dispatch budget caps +plan_slug: automation-foundation +epic_slug: model-provider-registry +status: draft +priority: P2 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-03" +--- + +# Task summary + +Surface spend from `convoy_events.estimated_cost_usd` and `agent_runs` token metadata; add optional workspace budget caps that pause orchestrator dispatch when exceeded. + +## Description + +**Benchmark:** Autensa per-task/product cost tracking and daily/monthly budget caps. Epic 1 deferred billing — this follow-on uses data already on `convoy_events` and run completion. + +### Read path (UI) + +- Settings → AI → **Usage & budgets** +- Rollups: today / this month by `model_alias`, by convoy, by role +- Mission control convoy detail: spend to date for selected convoy + +### Write path (enforcement) + +- `workspace_budget_caps` table: `period` (`daily` | `monthly`), `cap_usd`, `action` (`pause_dispatch` | `warn_only`) +- Orchestrator tick: sum `estimated_cost_usd` for period; if over cap and `pause_dispatch`, skip new `role_dispatch` enqueue and write health event + +### Cost estimation + +- Worker/orchestrator populate `estimated_cost_usd` on `convoy_events.role_complete` using provider-reported usage × static price table in shared config (v1 — no live pricing API) + +## Dependencies + +- Epic 1 model registry + `convoy_events` cost column (shipped) +- Epic 7 worker records token totals on complete + +## Subtasks + +- [ ] Price table stub in `packages/shared` (document as operator-editable later) +- [ ] tRPC usage queries (workspace-scoped aggregates) +- [ ] Settings usage page +- [ ] Budget cap schema + orchestrator guard + +## Estimation + +M + +## Acceptance criteria + +- [ ] Settings shows month-to-date spend grouped by model alias +- [ ] With cap set and exceeded, orchestrator logs skip reason and does not enqueue new jobs +- [ ] Warn-only cap emits event but does not block dispatch + +## Links + +- Epic: `./Epic-model-provider-registry.md` +- Unblocks: Epic 9 fleet spend rollup diff --git a/plans/Plan-automation-foundation/Plan-automation-foundation.md b/plans/Plan-automation-foundation/Plan-automation-foundation.md index a2b5231..3dc1b75 100644 --- a/plans/Plan-automation-foundation/Plan-automation-foundation.md +++ b/plans/Plan-automation-foundation/Plan-automation-foundation.md @@ -6,7 +6,7 @@ status: ready priority: P1 tenant_id: global cursor_plan_id: null -updated_at: "2026-07-07" +updated_at: "2026-08-03" agent_prompt: | You are implementing Echodo's automation foundation. Read docs/adr-model-routing.md before touching model routing code. This repo uses pnpm, Drizzle, tRPC, zod in @@ -28,7 +28,7 @@ agent_prompt: | Echodo is evolving from a multitenant task app with MCP coordination into **mission control** for an agent pipeline: fleet view, human gates, per-role model routing, and (later) a built-in orchestrator and worker. This plan delivers the **first concrete slice** — the model provider registry and role routing system defined in [docs/adr-model-routing.md](../../docs/adr-model-routing.md). -Later epics in this plan (2–9) add convoy scaffolding, connected repos, lifecycle MCP, gates UI, orchestrator, worker, webhooks, and fleet dashboard. Epic 1 gives them a stable `resolveModel()` contract. +Epics 1–6 and the mission-control UI are **shipped**. Epic 7 (worker) is **in progress** — skeleton exists in `apps/worker`. Epics 8–9 remain draft with decomposed tasks. Follow-on tasks from the [Autensa benchmark](#autensa-benchmark-2026-08) are injected into epics 1, 3, 5, 6, 7, 8, and 9 (concepts only — no OpenClaw dependency). ## Dogfooding the agent pipeline @@ -115,7 +115,8 @@ Start with **`Task-shared-schemas-resolve-model`** — no DB migration, pure `pa **In scope (full plan)** - Nine epics — see [Epic roadmap](#epic-roadmap-complete) below. -- Epic 1 (model registry) is **ready** with six tasks; epics 2–9 are **draft** stubs for architect decomposition. +- Epics 1–6 **done**; Epic 7 **in progress**; Epics 8–9 **draft** with bead-scale tasks. +- [Autensa benchmark](#autensa-benchmark-2026-08) follow-ons: reliability (checkpoints, auto-nudge), operator UX (activity feed, DAG, PR recovery), cost caps. - ADR at `docs/adr-model-routing.md`. **Out of scope (separate future plans)** @@ -133,7 +134,7 @@ Start with **`Task-shared-schemas-resolve-model`** — no DB migration, pure `pa ## Epic roadmap (complete) -Sequential order with parallel opportunities noted. Only **Epic 1** has bead-scale tasks today; epics 2–9 are draft stubs ready for architect decomposition when the prior epic lands. +Sequential order with parallel opportunities noted. **Current focus: Epic 7** (worker end-to-end dogfood), then Epic 8 → 9. Follow-on tasks on done epics can run in parallel when they do not block the critical path. ```mermaid flowchart LR @@ -162,15 +163,15 @@ flowchart LR | # | Epic | Status | Delivers | Depends on | |---|------|--------|----------|------------| -| 1 | [Model provider registry](./Epic-model-provider-registry/Epic-model-provider-registry.md) | **ready** (6 tasks) | `resolveModel()`, Settings AI pages, run model logging | — | +| 1 | [Model provider registry](./Epic-model-provider-registry/Epic-model-provider-registry.md) | done (+1 follow-on) | `resolveModel()`, Settings AI pages, run model logging; cost caps task | — | | 2 | [Pipeline convoy scaffolding](./Epic-pipeline-convoy-scaffolding/Epic-pipeline-convoy-scaffolding.md) | done | `.convoys/`, `log-convoy-event.sh`, `convoy-planning.mdc` — dogfood metrics | — (parallel with 1) | -| 3 | [Connected repos](./Epic-connected-repos/Epic-connected-repos.md) | done | `repo_links` table, “paste remote URL”, forge adapter interface | — | +| 3 | [Connected repos](./Epic-connected-repos/Epic-connected-repos.md) | done (+1 follow-on) | `repo_links` table, paste URL, forge adapters; repo setup preflight | — | | 4 | [Convoy lifecycle MCP](./Epic-convoy-lifecycle-mcp/Epic-convoy-lifecycle-mcp.md) | done | `create_convoy`, `transition_convoy_status`, state machine §7, `convoy_events` | 2, 3 | -| 5 | [Mission control gates UI](./Epic-mission-control-gates-ui/Epic-mission-control-gates-ui.md) | done | Convoy kanban, gate inbox, approve/deny/revise, gate co-pilot chat | 4 | -| 6 | [Orchestrator](./Epic-apps-orchestrator/Epic-apps-orchestrator.md) | done | `apps/orchestrator`, Redis queue, role dispatch, pause at gates | 1, 3, 4 | -| 7 | [Worker v1](./Epic-apps-worker-v1/Epic-apps-worker-v1.md) | draft | `apps/worker` minimal tool loop, worktree-per-brief, MCP claim/complete | 1, 3, 6 | -| 8 | [Forge webhooks](./Epic-forge-webhooks/Epic-forge-webhooks.md) | draft | PR opened/merged/CI webhooks → convoy transitions | 3, 4, 7 | -| 9 | [Fleet dashboard](./Epic-fleet-dashboard/Epic-fleet-dashboard.md) | draft | Cross-repo home, `convoys-*` workspaces, “needs you” rollup | 4, 5 | +| 5 | [Mission control gates UI](./Epic-mission-control-gates-ui/Epic-mission-control-gates-ui.md) | done (+2 follow-ons) | Kanban, gate inbox, co-pilot; DAG viz + automation tiers | 4 | +| 6 | [Orchestrator](./Epic-apps-orchestrator/Epic-apps-orchestrator.md) | done (+1 follow-on) | `apps/orchestrator`, Redis queue, role dispatch; auto-nudge | 1, 3, 4 | +| 7 | [Worker v1](./Epic-apps-worker-v1/Epic-apps-worker-v1.md) | **in progress** (3 tasks) | Tool loop, checkpoints, operator notes | 1, 3, 6 | +| 8 | [Forge webhooks](./Epic-forge-webhooks/Epic-forge-webhooks.md) | draft (2 tasks) | Webhooks + PR checks recovery panel | 3, 4, 7 | +| 9 | [Fleet dashboard](./Epic-fleet-dashboard/Epic-fleet-dashboard.md) | draft (2 tasks) | Cross-repo rollup + SSE activity feed | 4, 5 | ### Overlap with existing plans @@ -183,7 +184,26 @@ flowchart LR ### End-state (what “done” looks like for the full plan) -Operator opens Echodo fleet view → starts convoy chat → hits **Go** → orchestrator runs pipeline roles with per-role models → worker executes briefs against connected repos → gates surface in inbox with AI co-pilot → approve → repeat until `released`. +Operator opens Echodo fleet view → starts convoy chat → hits **Go** → orchestrator runs pipeline roles with per-role models → worker executes briefs against connected repos → gates surface in inbox with AI co-pilot → approve → repeat until `released`. Live activity feed, spend rollups, and PR recovery are visible without leaving mission control. + +### Autensa benchmark (2026-08) + +Studied [crshdn/mission-control](https://github.com/crshdn/mission-control) (Autensa) as a UX/reliability benchmark — **not** as a runtime dependency. Ideas mapped to tasks: + +| Autensa concept | Echodo task | Epic | +|-----------------|-------------|------| +| Checkpoint & crash recovery | [Task-worker-checkpoint-resume](./Epic-apps-worker-v1/Task-worker-checkpoint-resume.md) | 7 | +| Operator chat mid-build | [Task-worker-operator-notes](./Epic-apps-worker-v1/Task-worker-operator-notes.md) | 7 | +| Agent health / auto-nudge | [Task-orchestrator-auto-nudge](./Epic-apps-orchestrator/Task-orchestrator-auto-nudge.md) | 6 | +| Repo setup preflight | [Task-repo-setup-preflight](./Epic-connected-repos/Task-repo-setup-preflight.md) | 3 | +| PR checks recovery | [Task-pr-checks-recovery-panel](./Epic-forge-webhooks/Task-pr-checks-recovery-panel.md) | 8 | +| Convoy DAG visualization | [Task-convoy-dag-visualization](./Epic-mission-control-gates-ui/Task-convoy-dag-visualization.md) | 5 | +| Automation tiers | [Task-automation-tiers](./Epic-mission-control-gates-ui/Task-automation-tiers.md) | 5 | +| Cost tracking & budget caps | [Task-cost-budget-ui-and-caps](./Epic-model-provider-registry/Task-cost-budget-ui-and-caps.md) | 1 | +| Live activity feed (SSE) | [Task-convoy-activity-feed-sse](./Epic-fleet-dashboard/Task-convoy-activity-feed-sse.md) | 9 | +| Fleet rollup | [Task-fleet-rollup-dashboard](./Epic-fleet-dashboard/Task-fleet-rollup-dashboard.md) | 9 | + +**Explicitly not adopted:** OpenClaw Gateway, autopilot research/ideation/swipe, preference learning, agent skills learner, SQLite single-tenant model. ## Epics (links) @@ -212,6 +232,20 @@ Operator opens Echodo fleet view → starts convoy chat → hits **Go** → orch | M6 Automation | 6–7 | Orchestrator dispatches one implementer brief unattended | | M7 PR loop | 8 | Merge webhook moves convoy to `merged` | | M8 Fleet | 9 | Single view across ≥2 connected repos | +| M9 Reliability | 7 | Worker completes brief; checkpoint resume after kill | +| M10 Observability | 9 | Activity feed shows role_complete without refresh | +| M11 Spend control | 1 follow-on | Month-to-date spend + dispatch pause on cap | + +## Recommended execution order (post-benchmark) + +1. **Epic 7 critical path** — [Task-worker-minimal-tool-loop](./Epic-apps-worker-v1/Task-worker-minimal-tool-loop.md) → live dogfood with orchestrator +2. **Epic 7 reliability** — checkpoint/resume, then operator notes +3. **Epic 8** — webhook receiver → PR checks panel (needs worker PR output) +4. **Epic 6 follow-on** — auto-nudge (pairs with checkpoints) +5. **Epic 3 follow-on** — repo preflight (before unattended prod dogfood) +6. **Epic 9** — fleet rollup → SSE feed +7. **Epic 5 follow-ons** — DAG viz, automation tiers (after Epic 8 for semi-auto) +8. **Epic 1 follow-on** — cost caps (after worker records token totals) ## Risks and mitigations From d1b2b9695ebb053ad5fcd65a85ffe1da0f4cd8c6 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Wed, 12 Aug 2026 14:11:20 -0500 Subject: [PATCH 3/4] docs(plans): expand automation foundation for owner cockpit vision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add epics 10–12 (plans UI, completion artifacts, ops copilot), worker draft-PR and live E2E verify tasks, and phase A–E execution order after A1 gap review. Co-authored-by: Cursor --- .../Epic-apps-worker-v1.md | 24 ++-- .../Task-worker-draft-pr.md | 65 ++++++++++ .../Task-worker-live-e2e-dogfood.md | 61 +++++++++ .../Epic-fleet-dashboard.md | 21 ++-- .../Task-fleet-rollup-dashboard.md | 2 +- .../Epic-ops-copilot/Epic-ops-copilot.md | 52 ++++++++ .../Task-ops-copilot-context-tools.md | 49 ++++++++ .../Task-ops-copilot-draft-work.md | 48 +++++++ .../Epic-ops-copilot/Task-ops-copilot-ui.md | 41 ++++++ .../Epic-owner-completion-artifacts.md | 52 ++++++++ .../Task-completion-artifact-gallery.md | 54 ++++++++ .../Task-completion-screenshot-capture.md | 51 ++++++++ .../Task-completion-summary-generator.md | 48 +++++++ .../Epic-plans-roadmap-ui.md | 62 ++++++++++ .../Task-plans-roadmap-page.md | 55 ++++++++ .../Task-start-convoy-from-task.md | 55 ++++++++ .../Task-trpc-backlog-tree.md | 55 ++++++++ .../Plan-automation-foundation.md | 117 +++++++++++++----- 18 files changed, 868 insertions(+), 44 deletions(-) create mode 100644 plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-draft-pr.md create mode 100644 plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-live-e2e-dogfood.md create mode 100644 plans/Plan-automation-foundation/Epic-ops-copilot/Epic-ops-copilot.md create mode 100644 plans/Plan-automation-foundation/Epic-ops-copilot/Task-ops-copilot-context-tools.md create mode 100644 plans/Plan-automation-foundation/Epic-ops-copilot/Task-ops-copilot-draft-work.md create mode 100644 plans/Plan-automation-foundation/Epic-ops-copilot/Task-ops-copilot-ui.md create mode 100644 plans/Plan-automation-foundation/Epic-owner-completion-artifacts/Epic-owner-completion-artifacts.md create mode 100644 plans/Plan-automation-foundation/Epic-owner-completion-artifacts/Task-completion-artifact-gallery.md create mode 100644 plans/Plan-automation-foundation/Epic-owner-completion-artifacts/Task-completion-screenshot-capture.md create mode 100644 plans/Plan-automation-foundation/Epic-owner-completion-artifacts/Task-completion-summary-generator.md create mode 100644 plans/Plan-automation-foundation/Epic-plans-roadmap-ui/Epic-plans-roadmap-ui.md create mode 100644 plans/Plan-automation-foundation/Epic-plans-roadmap-ui/Task-plans-roadmap-page.md create mode 100644 plans/Plan-automation-foundation/Epic-plans-roadmap-ui/Task-start-convoy-from-task.md create mode 100644 plans/Plan-automation-foundation/Epic-plans-roadmap-ui/Task-trpc-backlog-tree.md diff --git a/plans/Plan-automation-foundation/Epic-apps-worker-v1/Epic-apps-worker-v1.md b/plans/Plan-automation-foundation/Epic-apps-worker-v1/Epic-apps-worker-v1.md index 0e0e1c6..4cd1add 100644 --- a/plans/Plan-automation-foundation/Epic-apps-worker-v1/Epic-apps-worker-v1.md +++ b/plans/Plan-automation-foundation/Epic-apps-worker-v1/Epic-apps-worker-v1.md @@ -7,7 +7,7 @@ status: in_progress priority: P1 tenant_id: global cursor_epic_id: null -updated_at: "2026-08-03" +updated_at: "2026-08-12" --- # Epic objective @@ -18,6 +18,12 @@ Role prompt = `.cursor/agents/role-*.md` + brief path. Model endpoint points at **Parallel implementers:** When orchestrator dispatches N implementers to the same Mac inference host, prefer a batching-capable MLX server (**mlx-openai-server**) over `mlx_lm.server` alone. Document chosen server + port in workspace provider alias (`qwen-coder-mlx`). +## Status note (2026-08-12) + +- Tool loop + checkpoints are **implemented in code**; task files marked done. +- **Gap:** worker does **not** yet open draft PRs via forge adapter (blocks clean Epic 8 dogfood). +- **A1 live E2E** (Go → unattended job on real Redis/Coolify) needs explicit operator verify: [Task-worker-live-e2e-dogfood](./Task-worker-live-e2e-dogfood.md). + ## In scope / out of scope **In scope** @@ -26,6 +32,7 @@ Role prompt = `.cursor/agents/role-*.md` + brief path. Model endpoint points at - `pnpm lint && pnpm test` in allowlisted commands. - Branch + PR draft output (forge adapter from Epic 3). - Checkpoint/resume and operator mid-build notes (Autensa benchmark — see tasks below). +- Live deploy dogfood checklist. **Out of scope** @@ -39,18 +46,21 @@ Role prompt = `.cursor/agents/role-*.md` + brief path. Model endpoint points at ## Blocks -- Epic 8 (meaningful PR webhook dogfood). +- Epic 8 (meaningful PR webhook dogfood) until draft PR task lands. ## Acceptance criteria -- [ ] One brief from `.convoys/` completes unattended on connected repo. -- [ ] `complete_task` includes model alias + token totals. -- [ ] Crashed worker can resume from checkpoint (follow-on task). +- [ ] One brief from `.convoys/` completes unattended on connected repo (see live E2E task). +- [x] Checkpoint resume after kill (code + task done). +- [ ] Draft PR opened on implementer success. +- [ ] `complete_task` includes model alias + token totals on live run. ## Tasks | Task | Link | Status | |------|------|--------| -| Minimal tool loop + worktree | [Task-worker-minimal-tool-loop](./Task-worker-minimal-tool-loop.md) | ready | -| Checkpoint & crash recovery | [Task-worker-checkpoint-resume](./Task-worker-checkpoint-resume.md) | draft | +| Minimal tool loop + worktree | [Task-worker-minimal-tool-loop](./Task-worker-minimal-tool-loop.md) | done (code; confirm live via E2E task) | +| Checkpoint & crash recovery | [Task-worker-checkpoint-resume](./Task-worker-checkpoint-resume.md) | done | +| Draft PR via forge | [Task-worker-draft-pr](./Task-worker-draft-pr.md) | **ready** | +| Live E2E dogfood (A1 verify) | [Task-worker-live-e2e-dogfood](./Task-worker-live-e2e-dogfood.md) | **ready** | | Operator notes mid-build | [Task-worker-operator-notes](./Task-worker-operator-notes.md) | draft | diff --git a/plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-draft-pr.md b/plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-draft-pr.md new file mode 100644 index 0000000..143e14e --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-draft-pr.md @@ -0,0 +1,65 @@ +--- +kind: task +slug: worker-draft-pr +title: Worker opens draft PR via forge adapter on implementer success +plan_slug: automation-foundation +epic_slug: apps-worker-v1 +status: ready +priority: P1 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-12" +agent_prompt: | + Epic 7 gap: apps/worker never calls the forge adapter to open a PR. + Wire createPullRequest (or equivalent) after a successful implementer + session. Use connected repo from convoy/workspace. Branch naming must + match Epic 8 conventions. claim_task / complete_task this task; run + pnpm lint && pnpm type-check && pnpm test. +--- + +# Task summary + +When an implementer `role_dispatch` job succeeds, open a **draft PR** on the connected forge (GitHub/Forgejo) from the worktree branch, and store PR URL/metadata on the convoy for Mission Control and Epic 8 webhooks. + +## Description + +Gap check (2026-08-12): `Task-worker-minimal-tool-loop` was marked `done` and checkpoints shipped, but `apps/worker` has **no** forge PR call. Epic 7 acceptance (“branch + PR draft output”) and Epic 8 dogfood both require this. + +### Behavior + +- After successful implementer session (and before worktree teardown): push branch if needed, call forge adapter `createPullRequest` (draft). +- PR body includes convoy slug, brief id, echodo marker for webhooks (`echodo-convoy: `). +- Persist PR url / number on convoy content or `convoy_events` metadata. +- Non-implementer roles skip PR creation. +- Failures: log + mark run notes; do not silently claim success if PR was required. + +### Dependencies + +- Epic 3 forge adapters +- Worktree + branch from worker tool loop + +## Subtasks + +- [ ] Resolve `repo_links` for convoy workspace +- [ ] Push worktree branch with stored credentials pattern +- [ ] `createPullRequest` draft via adapter +- [ ] Write PR metadata for Mission Control detail panel +- [ ] Document branch + marker convention in `docs/worker.md` + +## Estimation + +M + +## Acceptance criteria + +- [ ] Successful implementer job produces a draft PR URL on a connected test repo +- [ ] PR body contains convoy marker consumable by Epic 8 +- [ ] Planning-only roles do not open PRs +- [ ] Mission control detail can show PR link when present + +## Links + +- Epic: `./Epic-apps-worker-v1.md` +- Blocks: Epic 8 webhook dogfood +- Related: [Task-worker-minimal-tool-loop](./Task-worker-minimal-tool-loop.md) diff --git a/plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-live-e2e-dogfood.md b/plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-live-e2e-dogfood.md new file mode 100644 index 0000000..169ba5a --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-live-e2e-dogfood.md @@ -0,0 +1,61 @@ +--- +kind: task +slug: worker-live-e2e-dogfood +title: Live E2E dogfood — Go to worker job without Cursor (A1 verify) +plan_slug: automation-foundation +epic_slug: apps-worker-v1 +status: ready +priority: P1 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-12" +--- + +# Task summary + +Operator verification checklist: prove Mission Control **Go** → orchestrator enqueue → worker (non-dry-run) completes one implementer brief with model/token metadata — **without** a Cursor coding session for that brief. + +## Description + +**Phase A1** from the owner vision playbook. Code for the loop and checkpoints exists; this task is the **proof on your deploy** (Coolify / local Redis), not more scaffolding. + +Mark this task `done` only when every acceptance criterion below is true in production or a documented staging environment. + +### Checklist (operator) + +1. `ORCHESTRATOR_DRY_RUN=0`, `WORKER_DRY_RUN=0`, `REDIS_URL` set on orchestrator + worker services +2. Workspace has model aliases + role routes for `role-implementer` +3. Connected repo (or monorepo) reachable from worker +4. Create or reconcile a convoy with an implementer brief +5. Advance to `ready` / trigger dispatch (Gate 1 approve if needed) +6. Worker logs show job pickup + `complete_task` / `role_complete` +7. Settings → Runs shows model alias + tokens for that run +8. (Preferred) Draft PR exists after [Task-worker-draft-pr](./Task-worker-draft-pr.md) + +### Not this task + +- Building new worker features (use sibling tasks) +- Forge webhooks (Epic 8) + +## Subtasks + +- [ ] Document exact Coolify/env values used (no secrets in git — names only) +- [ ] Record convoy slug + run id in task notes or convoy_events +- [ ] Confirm dry-run modes still work when Redis missing + +## Estimation + +S (ops) + +## Acceptance criteria + +- [ ] One implementer brief completed unattended (no Cursor session for that brief) +- [ ] `agent_runs` row has `model_alias` and token totals +- [ ] Convoy progressed or logged `role_complete` visible in Mission Control detail +- [ ] Orchestrator/worker were not in dry-run for the successful job + +## Links + +- Epic: `./Epic-apps-worker-v1.md` +- Plan: `../Plan-automation-foundation.md` diff --git a/plans/Plan-automation-foundation/Epic-fleet-dashboard/Epic-fleet-dashboard.md b/plans/Plan-automation-foundation/Epic-fleet-dashboard/Epic-fleet-dashboard.md index eab8ee2..a1396cb 100644 --- a/plans/Plan-automation-foundation/Epic-fleet-dashboard/Epic-fleet-dashboard.md +++ b/plans/Plan-automation-foundation/Epic-fleet-dashboard/Epic-fleet-dashboard.md @@ -4,43 +4,48 @@ slug: fleet-dashboard title: Fleet dashboard — all connected repos and convoys plan_slug: automation-foundation status: draft -priority: P2 +priority: P1 tenant_id: global cursor_epic_id: null -updated_at: "2026-08-03" +updated_at: "2026-08-12" --- # Epic objective -Top-level **fleet** view: all `convoys-` workspaces (or connected repos) in one screen. Roll up convoys by status, gate inbox count, active `agent_runs`, recent spend by model alias. +Top-level **fleet** home: all workspaces / connected repos in one screen. Roll up gates needing attention, active convoys and `agent_runs`, recent releases, and (when Epic 11 lands) completion thumbnails. -Live **activity feed** (SSE) from `convoy_events` — Autensa benchmark for operator situational awareness. +This is Phase B of the owner cockpit vision — “what’s happening across my projects since I was last here?” -Answers: “What needs me across all projects?” and “What just happened?” +Live **activity feed** (SSE) from `convoy_events`. ## In scope / out of scope **In scope** -- Route `/fleet` or home dashboard for operator account. +- Route `/fleet` or authenticated landing dashboard. - Cross-workspace tRPC (membership-scoped). -- Links into per-repo mission control (Epic 5). +- Cards per workspace/repo: gate count, running convoys, recent `released` / merged. +- Links into per-repo mission control and (later) Plans. - SSE activity stream (workspace + fleet scope). +- Placeholder slots for completion summaries (Epic 11) without blocking v1. **Out of scope** - Multi-tenant fleet across unrelated customers (operator's own repos only for v1). - Autopilot research/ideation feeds. +- Full Plans tree (Epic 10). ## Dependencies - Epic 4 (queryable convoy status). - Epic 5 (gate semantics). +- Epic 7 live dogfood preferred so “running” is meaningful. ## Acceptance criteria -- [ ] Operator with two connected repos sees combined “3 awaiting approval” badge. +- [ ] Operator with two connected repos sees combined “N awaiting approval” badge. - [ ] New convoy event appears in activity feed without page refresh. +- [ ] Card links into correct workspace mission control. ## Tasks diff --git a/plans/Plan-automation-foundation/Epic-fleet-dashboard/Task-fleet-rollup-dashboard.md b/plans/Plan-automation-foundation/Epic-fleet-dashboard/Task-fleet-rollup-dashboard.md index 0dde38b..98d3b36 100644 --- a/plans/Plan-automation-foundation/Epic-fleet-dashboard/Task-fleet-rollup-dashboard.md +++ b/plans/Plan-automation-foundation/Epic-fleet-dashboard/Task-fleet-rollup-dashboard.md @@ -5,7 +5,7 @@ title: Fleet dashboard — cross-repo convoy rollup plan_slug: automation-foundation epic_slug: fleet-dashboard status: ready -priority: P2 +priority: P1 tenant_id: global owner: unassigned cursor_todo_id: null diff --git a/plans/Plan-automation-foundation/Epic-ops-copilot/Epic-ops-copilot.md b/plans/Plan-automation-foundation/Epic-ops-copilot/Epic-ops-copilot.md new file mode 100644 index 0000000..cfa7557 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-ops-copilot/Epic-ops-copilot.md @@ -0,0 +1,52 @@ +--- +kind: epic +slug: ops-copilot +title: Ops copilot — project-aware chat for status and triage +plan_slug: automation-foundation +status: draft +priority: P2 +tenant_id: global +cursor_epic_id: null +updated_at: "2026-08-12" +--- + +# Epic objective + +A Submatter chat surface grounded in **workspace convoys, backlog, runs, and events** so an owner can ask “what needs me?”, “what shipped last week?”, or “add a bug for X” without opening Cursor or scanning markdown. + +Distinct from gate co-pilot (single-convoy approve assist) and generic `/{ws}/ai` chat. + +## In scope / out of scope + +**In scope** + +- Ops chat route or Mission Control / Fleet panel using `resolveModel` (`ops-copilot` role alias) +- Tools/RAG-lite: query convoy kanban counts, gate inbox, recent events, backlog ready list, run outcomes +- Actions (v1 limited): draft backlog task markdown suggestion, create draft convoy stub, link to Start +- Citations: answers reference convoy slug / task slug + +**Out of scope** + +- Full IDE agent / code edit loop (worker remains the coder) +- Autopilot research +- Replacing Mission Control buttons for gates (chat can deep-link) + +## Dependencies + +- Epic 5 gate co-pilot patterns +- Epic 9 fleet data (preferred) +- Epic 10 Plans tRPC (preferred for “what’s next”) + +## Acceptance criteria + +- [ ] Owner asks “what needs my approval?” and gets gate inbox items with links +- [ ] Owner asks “what’s ready to start?” and gets ready backlog tasks (when Epic 10 shipped) +- [ ] Chat cannot mutate another workspace’s data + +## Tasks + +| Task | Link | Status | +|------|------|--------| +| Ops copilot context tools | [Task-ops-copilot-context-tools](./Task-ops-copilot-context-tools.md) | ready | +| Ops copilot UI panel | [Task-ops-copilot-ui](./Task-ops-copilot-ui.md) | draft | +| Draft work from chat | [Task-ops-copilot-draft-work](./Task-ops-copilot-draft-work.md) | draft | diff --git a/plans/Plan-automation-foundation/Epic-ops-copilot/Task-ops-copilot-context-tools.md b/plans/Plan-automation-foundation/Epic-ops-copilot/Task-ops-copilot-context-tools.md new file mode 100644 index 0000000..4689246 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-ops-copilot/Task-ops-copilot-context-tools.md @@ -0,0 +1,49 @@ +--- +kind: task +slug: ops-copilot-context-tools +title: Ops copilot — server tools over convoys, backlog, events +plan_slug: automation-foundation +epic_slug: ops-copilot +status: ready +priority: P2 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-12" +--- + +# Task summary + +Implement workspace-scoped tool functions the ops copilot model can call: gate inbox, convoy summary, recent events, ready backlog (if available), recent runs — all filtered by `workspace_id`. + +## Description + +Pattern after gate co-pilot + AI SDK tools. Register role route `ops-copilot`. No UI yet. + +### Tools (v1) + +- `list_gate_inbox` +- `get_convoy_summary` +- `list_recent_events` +- `list_ready_backlog` (no-op / empty if tree API missing) +- `list_recent_runs` + +## Subtasks + +- [ ] Shared zod tool schemas +- [ ] Server implementation with membership checks +- [ ] Seed `ops-copilot` role route +- [ ] Unit tests for workspace isolation + +## Estimation + +M + +## Acceptance criteria + +- [ ] Tools refuse cross-workspace ids +- [ ] Gate inbox tool matches Mission Control counts for a fixture workspace + +## Links + +- Epic: `./Epic-ops-copilot.md` diff --git a/plans/Plan-automation-foundation/Epic-ops-copilot/Task-ops-copilot-draft-work.md b/plans/Plan-automation-foundation/Epic-ops-copilot/Task-ops-copilot-draft-work.md new file mode 100644 index 0000000..c11ff07 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-ops-copilot/Task-ops-copilot-draft-work.md @@ -0,0 +1,48 @@ +--- +kind: task +slug: ops-copilot-draft-work +title: Ops copilot — draft bug/task or convoy stub from chat +plan_slug: automation-foundation +epic_slug: ops-copilot +status: draft +priority: P2 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-12" +--- + +# Task summary + +From ops chat, allow “file a bug” / “add a task” that creates a **draft** backlog suggestion or draft convoy stub for human confirm — not silent production deploys. + +## Description + +v1 safest path: generate markdown draft + open Plans/Start confirm UI, or create `draft` convoy with idea text for Gate 1. + +Never auto-merge or skip gates. + +### Dependencies + +- [Task-ops-copilot-ui](./Task-ops-copilot-ui.md) +- Epic 10 Start flow preferred + +## Subtasks + +- [ ] Tool `draft_backlog_task` or `draft_convoy` +- [ ] Confirm step in UI before persist +- [ ] Audit log entry + +## Estimation + +M + +## Acceptance criteria + +- [ ] Draft requires explicit confirm +- [ ] Confirmed draft appears in Plans or Mission Control +- [ ] No gate skipped + +## Links + +- Epic: `./Epic-ops-copilot.md` diff --git a/plans/Plan-automation-foundation/Epic-ops-copilot/Task-ops-copilot-ui.md b/plans/Plan-automation-foundation/Epic-ops-copilot/Task-ops-copilot-ui.md new file mode 100644 index 0000000..efa018e --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-ops-copilot/Task-ops-copilot-ui.md @@ -0,0 +1,41 @@ +--- +kind: task +slug: ops-copilot-ui +title: Ops copilot chat UI on fleet or mission control +plan_slug: automation-foundation +epic_slug: ops-copilot +status: draft +priority: P2 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-12" +--- + +# Task summary + +Chat panel for ops copilot with streaming answers and deep links to convoys / plans / gates. + +## Dependencies + +- [Task-ops-copilot-context-tools](./Task-ops-copilot-context-tools.md) +- Prefer Epic 9 fleet page as host; fallback Mission Control aside tab + +## Subtasks + +- [ ] Chat UI component +- [ ] Wire generateText/stream + tools +- [ ] Render links to mission-control / plans routes + +## Estimation + +M + +## Acceptance criteria + +- [ ] “What needs me?” returns clickable gate items +- [ ] Panel works on mobile width + +## Links + +- Epic: `./Epic-ops-copilot.md` diff --git a/plans/Plan-automation-foundation/Epic-owner-completion-artifacts/Epic-owner-completion-artifacts.md b/plans/Plan-automation-foundation/Epic-owner-completion-artifacts/Epic-owner-completion-artifacts.md new file mode 100644 index 0000000..196b771 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-owner-completion-artifacts/Epic-owner-completion-artifacts.md @@ -0,0 +1,52 @@ +--- +kind: epic +slug: owner-completion-artifacts +title: Owner completion packages — summaries, demos, screenshots +plan_slug: automation-foundation +status: draft +priority: P1 +tenant_id: global +cursor_epic_id: null +updated_at: "2026-08-12" +--- + +# Epic objective + +When a convoy reaches `merged` / `released` (or a brief completes), produce an **owner-facing completion package**: plain-language summary of what shipped, optional **screenshots or demo clips** for UI work, and a short backend summary when there is no UI — not a code diff. + +Serves non-developer operators who need to verify “did it build what I wanted?” + +## In scope / out of scope + +**In scope** + +- Generate summary from convoy events, PR title/body, brief acceptance, and optional agent notes (via `resolveModel` / doc-writer or gate-copilot-style role) +- Artifact storage: screenshot images / short demo URLs attached to convoy (workspace-scoped object storage or forge release assets — pick one, document) +- Mission Control / fleet “Completed” view: card with summary + media +- Heuristic: UI-touching briefs request screenshot capture (Playwright or worker tool) when feasible; backend-only → summary only + +**Out of scope** + +- Full design QA +- Replacing PR review for engineers +- Autensa-style marketing site research + +## Dependencies + +- Epic 7 worker (real completions) +- Epic 8 webhooks (merge signal) preferred +- Epic 5 Mission Control detail panel + +## Acceptance criteria + +- [ ] Released convoy shows owner summary without opening git +- [ ] UI convoy can attach at least one screenshot or demo URL +- [ ] Backend-only convoy shows prose summary, no empty “diff” UI + +## Tasks + +| Task | Link | Status | +|------|------|--------| +| Completion summary generation | [Task-completion-summary-generator](./Task-completion-summary-generator.md) | ready | +| Artifact attach + gallery UI | [Task-completion-artifact-gallery](./Task-completion-artifact-gallery.md) | draft | +| Optional UI screenshot capture | [Task-completion-screenshot-capture](./Task-completion-screenshot-capture.md) | draft | diff --git a/plans/Plan-automation-foundation/Epic-owner-completion-artifacts/Task-completion-artifact-gallery.md b/plans/Plan-automation-foundation/Epic-owner-completion-artifacts/Task-completion-artifact-gallery.md new file mode 100644 index 0000000..426d040 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-owner-completion-artifacts/Task-completion-artifact-gallery.md @@ -0,0 +1,54 @@ +--- +kind: task +slug: completion-artifact-gallery +title: Attach screenshots/demo URLs to convoy completion package +plan_slug: automation-foundation +epic_slug: owner-completion-artifacts +status: draft +priority: P1 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-12" +--- + +# Task summary + +Allow uploading or linking screenshots and demo URLs on a convoy; show a simple gallery on the completion / detail panel for owner review. + +## Description + +### Storage + +- Prefer workspace-scoped object storage or existing upload path if any; otherwise URL attachments only for v1 +- Never store secrets; validate content types (image/*, video links) + +### UI + +- Mission control detail: “What shipped” media strip +- Fleet completed card thumbnail (optional) + +### Dependencies + +- [Task-completion-summary-generator](./Task-completion-summary-generator.md) + +## Subtasks + +- [ ] Schema for `completionArtifacts[]` on convoy content +- [ ] tRPC add/remove artifact +- [ ] Gallery UI +- [ ] Manual upload or paste URL + +## Estimation + +M + +## Acceptance criteria + +- [ ] Owner can attach a screenshot URL and see it on the convoy +- [ ] Artifacts are workspace-scoped +- [ ] Removing artifact does not break summary + +## Links + +- Epic: `./Epic-owner-completion-artifacts.md` diff --git a/plans/Plan-automation-foundation/Epic-owner-completion-artifacts/Task-completion-screenshot-capture.md b/plans/Plan-automation-foundation/Epic-owner-completion-artifacts/Task-completion-screenshot-capture.md new file mode 100644 index 0000000..a335e74 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-owner-completion-artifacts/Task-completion-screenshot-capture.md @@ -0,0 +1,51 @@ +--- +kind: task +slug: completion-screenshot-capture +title: Optional automated screenshot capture for UI convoys +plan_slug: automation-foundation +epic_slug: owner-completion-artifacts +status: draft +priority: P2 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-12" +--- + +# Task summary + +For UI-classified convoys, optionally capture one or more screenshots (Playwright or worker browser tool) of a configured preview URL and attach them to the completion package. + +## Description + +### Behavior + +- Convoy/content flag or classification hints UI surface +- Preview URL from convoy metadata or connected repo deploy URL +- Capture on PR open or after merge (configurable) +- Fail soft: summary still works if capture fails + +### Dependencies + +- [Task-completion-artifact-gallery](./Task-completion-artifact-gallery.md) +- Worker or a small capture job runner + +## Subtasks + +- [ ] Capture runner (Playwright in worker job or sidecar) +- [ ] Wire attach to artifacts +- [ ] Opt-in per convoy + +## Estimation + +L + +## Acceptance criteria + +- [ ] Opt-in UI convoy gets ≥1 screenshot attached after successful capture +- [ ] Capture failure logs event and does not block `released` +- [ ] Backend-only convoys skip capture by default + +## Links + +- Epic: `./Epic-owner-completion-artifacts.md` diff --git a/plans/Plan-automation-foundation/Epic-owner-completion-artifacts/Task-completion-summary-generator.md b/plans/Plan-automation-foundation/Epic-owner-completion-artifacts/Task-completion-summary-generator.md new file mode 100644 index 0000000..031902d --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-owner-completion-artifacts/Task-completion-summary-generator.md @@ -0,0 +1,48 @@ +--- +kind: task +slug: completion-summary-generator +title: Generate plain-language completion summary for a convoy +plan_slug: automation-foundation +epic_slug: owner-completion-artifacts +status: ready +priority: P1 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-12" +--- + +# Task summary + +On convoy transition to `merged` or `released` (or manual “Generate summary”), produce a short owner-facing summary stored on the convoy and shown in Mission Control — no code diff. + +## Description + +Use `resolveModel` with a dedicated role alias (e.g. `role-doc-writer` or `completion-summarizer`) and context: success metric, briefs, recent `convoy_events`, PR title if present. + +### Output + +- 3–8 sentences: what changed for the user/operator, what’s left, risks +- Persist on convoy content `completionSummary` + `completionSummaryAt` +- tRPC `convoys.getCompletion` / include in `convoys.get` + +## Subtasks + +- [ ] Zod fields on convoy content +- [ ] Generator service + role route seed +- [ ] Trigger on transition and manual regenerate +- [ ] Mission control detail section + +## Estimation + +M + +## Acceptance criteria + +- [ ] After merge/released, summary appears without git access +- [ ] Regenerating updates text and timestamp +- [ ] No file diffs in the owner-facing panel + +## Links + +- Epic: `./Epic-owner-completion-artifacts.md` diff --git a/plans/Plan-automation-foundation/Epic-plans-roadmap-ui/Epic-plans-roadmap-ui.md b/plans/Plan-automation-foundation/Epic-plans-roadmap-ui/Epic-plans-roadmap-ui.md new file mode 100644 index 0000000..6c1ba76 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-plans-roadmap-ui/Epic-plans-roadmap-ui.md @@ -0,0 +1,62 @@ +--- +kind: epic +slug: plans-roadmap-ui +title: Plans roadmap UI — tree, status, Start → convoy +plan_slug: automation-foundation +status: draft +priority: P1 +tenant_id: global +cursor_epic_id: null +updated_at: "2026-08-12" +--- + +# Epic objective + +Owner-facing **roadmap browser** for `markdown_backlog_items` (imported from `plans/`): Plan → Epic → Task tree with status, detail, and **Start** that creates a Mission Control convoy from a ready task — without reading the git tree in the IDE. + +This is the “dive into a repo and see upcoming work” slice of the owner cockpit vision. Distinct from Mission Control (convoys) and Fleet (cross-repo attention). + +## In scope / out of scope + +**In scope** + +- Route `/{workspaceSlug}/plans` (icon-rail entry) +- tRPC list/tree/get for backlog items (workspace-scoped) +- Status filters (`ready`, `in_progress`, `done`, …) +- Task detail drawer: title, body summary, status, workflow prompt (reuse backlog tRPC) +- **Start** on `ready` tasks → `create_convoy` (or tRPC equivalent) prefilled from task +- Link out to `repo_path` / file path for power users +- Optional simple milestone strip (epics as milestones) — v1 can be hierarchical list only + +**Out of scope** + +- Replacing git as SoT for plan markdown (import remains) +- Autopilot ideation +- Timeline Gantt (follow-on if needed) +- Fleet cross-workspace (Epic 9) + +## Dependencies + +- Markdown importer + `markdown_backlog_items` (shipped) +- Epic 4 convoy create (shipped) +- Epic 5 Mission Control (shipped) + +## Blocks + +- Ops copilot “what’s next?” grounded answers (Epic 12) +- Owner Start-from-UI without Cursor + +## Acceptance criteria + +- [ ] Operator sees Plan-automation-foundation tree after import without opening git +- [ ] Filter “ready” shows only ready tasks +- [ ] Start on a ready task creates a draft convoy visible on Mission Control +- [ ] Non-members cannot read another workspace’s backlog + +## Tasks + +| Task | Link | Status | +|------|------|--------| +| tRPC backlog tree + list | [Task-trpc-backlog-tree](./Task-trpc-backlog-tree.md) | ready | +| Plans page UI | [Task-plans-roadmap-page](./Task-plans-roadmap-page.md) | draft | +| Start task → convoy | [Task-start-convoy-from-task](./Task-start-convoy-from-task.md) | draft | diff --git a/plans/Plan-automation-foundation/Epic-plans-roadmap-ui/Task-plans-roadmap-page.md b/plans/Plan-automation-foundation/Epic-plans-roadmap-ui/Task-plans-roadmap-page.md new file mode 100644 index 0000000..6717349 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-plans-roadmap-ui/Task-plans-roadmap-page.md @@ -0,0 +1,55 @@ +--- +kind: task +slug: plans-roadmap-page +title: Plans roadmap page — tree, filters, detail drawer +plan_slug: automation-foundation +epic_slug: plans-roadmap-ui +status: draft +priority: P1 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-12" +--- + +# Task summary + +Ship `/{workspaceSlug}/plans` with Plan → Epic → Task navigation, status filters, and a detail drawer (summary + workflow prompt preview). + +## Description + +Owner cockpit: see upcoming and ready work without opening git. Add icon-rail entry **Plans** (or Roadmap). + +### UI + +- Left/main: hierarchical list or accordion +- Status chips / filter bar +- Detail: title, status, priority, truncated body, effective workflow prompt (existing `getWorkflowPrompt`) +- Empty state: “Run markdown backlog import” with short docs link +- Epics can render as **milestone** labels in the tree (no full Gantt required) + +### Dependencies + +- [Task-trpc-backlog-tree](./Task-trpc-backlog-tree.md) + +## Subtasks + +- [ ] Route + icon-rail +- [ ] Tree component wired to tRPC +- [ ] Detail drawer +- [ ] Mobile-usable layout + +## Estimation + +L + +## Acceptance criteria + +- [ ] Operator can browse imported plan without git +- [ ] Selecting a task shows detail + prompt source badge +- [ ] Filter ready works end-to-end + +## Links + +- Epic: `./Epic-plans-roadmap-ui.md` +- Related: [Task-workflow-prompt-task-detail-ui](../../Plan-agent-coordination/Epic-task-as-runnable-unit/Task-workflow-prompt-task-detail-ui.md) diff --git a/plans/Plan-automation-foundation/Epic-plans-roadmap-ui/Task-start-convoy-from-task.md b/plans/Plan-automation-foundation/Epic-plans-roadmap-ui/Task-start-convoy-from-task.md new file mode 100644 index 0000000..46ae2d9 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-plans-roadmap-ui/Task-start-convoy-from-task.md @@ -0,0 +1,55 @@ +--- +kind: task +slug: start-convoy-from-task +title: Start — create convoy from ready backlog task +plan_slug: automation-foundation +epic_slug: plans-roadmap-ui +status: draft +priority: P1 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-12" +--- + +# Task summary + +From the Plans UI, **Start** on a `ready` (or `draft` with confirm) task creates a Mission Control convoy prefilled from the task, then navigates to mission control with that convoy selected. + +## Description + +Bridges roadmap → execution without Cursor. + +### Behavior + +- Button visible on eligible tasks +- Calls tRPC wrapping `create_convoy` semantics: slug derived from task slug, idea markdown from task body, success metric from task title/AC summary +- Store link `backlogItemId` or `repo_path` on convoy content metadata +- Navigate to `/{ws}/mission-control` with convoy selected +- Idempotency: if open convoy already linked to this task, focus it instead of duplicating + +### Dependencies + +- [Task-plans-roadmap-page](./Task-plans-roadmap-page.md) +- Epic 4 / convoys.create + +## Subtasks + +- [ ] tRPC `backlog.startConvoy` or `convoys.createFromBacklogItem` +- [ ] Start button + confirm dialog +- [ ] Metadata link task ↔ convoy +- [ ] Navigation + toast + +## Estimation + +M + +## Acceptance criteria + +- [ ] Start on ready task creates draft convoy on kanban +- [ ] Second Start focuses existing linked convoy +- [ ] Non-ready tasks require explicit confirm or are disabled + +## Links + +- Epic: `./Epic-plans-roadmap-ui.md` diff --git a/plans/Plan-automation-foundation/Epic-plans-roadmap-ui/Task-trpc-backlog-tree.md b/plans/Plan-automation-foundation/Epic-plans-roadmap-ui/Task-trpc-backlog-tree.md new file mode 100644 index 0000000..35cce91 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-plans-roadmap-ui/Task-trpc-backlog-tree.md @@ -0,0 +1,55 @@ +--- +kind: task +slug: trpc-backlog-tree +title: tRPC backlog.list / tree / get for markdown_backlog_items +plan_slug: automation-foundation +epic_slug: plans-roadmap-ui +status: ready +priority: P1 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-08-12" +agent_prompt: | + Extend apps/web backlog router: list/tree/get for markdown_backlog_items. + Always filter by workspace_id. Zod inputs. No Plans UI yet — API only. + Reuse existing schema. claim_task / complete_task; lint + type-check + test. +--- + +# Task summary + +Add workspace-scoped tRPC procedures to list the Plan → Epic → Task hierarchy from `markdown_backlog_items` with status filters — the data layer for the Plans roadmap UI. + +## Description + +Today `backlog` router only handles workflow prompts. Add: + +- `backlog.tree` — nested plans → epics → tasks (or flat list + client nest by `kind` / parent) +- `backlog.list` — flat with filters: `kind`, `status`, `planSlug`, `epicSlug` +- `backlog.get` — single item by id or slug keys + +All queries must filter `workspace_id` from session membership. + +Document that rows come from `pnpm --filter @tasks/database import:markdown-backlog`. + +## Subtasks + +- [ ] Zod input schemas in shared or router-local +- [ ] Procedures on `backlog` router +- [ ] Unit/integration coverage for workspace isolation +- [ ] Empty tree when import not run (friendly empty state data) + +## Estimation + +M + +## Acceptance criteria + +- [ ] Member of workspace A cannot fetch workspace B items +- [ ] Tree returns automation-foundation plan with nested epics/tasks after import +- [ ] Status filter `ready` excludes `done` + +## Links + +- Epic: `./Epic-plans-roadmap-ui.md` +- Related: `apps/web/server/routers/backlog.ts` diff --git a/plans/Plan-automation-foundation/Plan-automation-foundation.md b/plans/Plan-automation-foundation/Plan-automation-foundation.md index 3dc1b75..299dc52 100644 --- a/plans/Plan-automation-foundation/Plan-automation-foundation.md +++ b/plans/Plan-automation-foundation/Plan-automation-foundation.md @@ -6,7 +6,7 @@ status: ready priority: P1 tenant_id: global cursor_plan_id: null -updated_at: "2026-08-03" +updated_at: "2026-08-12" agent_prompt: | You are implementing Echodo's automation foundation. Read docs/adr-model-routing.md before touching model routing code. This repo uses pnpm, Drizzle, tRPC, zod in @@ -26,9 +26,31 @@ agent_prompt: | # Plan overview -Echodo is evolving from a multitenant task app with MCP coordination into **mission control** for an agent pipeline: fleet view, human gates, per-role model routing, and (later) a built-in orchestrator and worker. This plan delivers the **first concrete slice** — the model provider registry and role routing system defined in [docs/adr-model-routing.md](../../docs/adr-model-routing.md). +Echodo / **Submatter** is evolving from a multitenant task app with MCP coordination into an **owner cockpit** for agent-built personal projects: fleet home, roadmap + Start, mission-control gates, first-party worker (leave Cursor for convoy builds), forge PRs, completion summaries/demos, and ops copilot. -Epics 1–6 and the mission-control UI are **shipped**. Epic 7 (worker) is **in progress** — skeleton exists in `apps/worker`. Epics 8–9 remain draft with decomposed tasks. Follow-on tasks from the [Autensa benchmark](#autensa-benchmark-2026-08) are injected into epics 1, 3, 5, 6, 7, 8, and 9 (concepts only — no OpenClaw dependency). +Epics **1–6 shipped**. Epic **7 in progress** (tool loop + checkpoints in code; draft PR + live E2E verify still open). Epics **8–9** (webhooks, fleet) and **10–12** (plans UI, completion artifacts, ops copilot) carry the full owner vision. Autensa/Builderz-style ideas are benchmarks only — no OpenClaw dependency. + +## Owner vision phases + +| Phase | Goal | Epics | +|-------|------|-------| +| **A** Engine without Cursor | Go → worker → PR → gates | 7 → 8 (+ draft PR, live E2E) | +| **B** Fleet attention home | All projects, needs-you, activity | 9 | +| **C** Roadmap + Start | Plans tree, Start → convoy | 10 | +| **D** Owner review | Summaries, screenshots/demos | 11 | +| **E** Ops chat | Status / triage / draft work | 12 | + +### A1 status check (2026-08-12) + +| Claim | Reality in repo | +|-------|-----------------| +| Worker tool loop exists | Yes — `apps/worker` dequeue, session, worktree, claim/complete | +| Checkpoints | Yes — task done, `worker_checkpoints` path documented | +| Live Go → unattended job on your deploy | **Operator-verify** — [Task-worker-live-e2e-dogfood](./Epic-apps-worker-v1/Task-worker-live-e2e-dogfood.md) | +| Draft PR from worker | **Missing** — no forge PR call in worker; [Task-worker-draft-pr](./Epic-apps-worker-v1/Task-worker-draft-pr.md) | +| Minimal-tool-loop marked `done` | Code landed; several AC checkboxes in the task file still unchecked — treat live E2E + draft PR as the real close-out | + +**If you “finished A1”** meaning services run and a job completed: mark criteria on the live E2E task and keep going. **If you only shipped code:** run the live E2E checklist before calling Phase A done. ## Dogfooding the agent pipeline @@ -114,9 +136,9 @@ Start with **`Task-shared-schemas-resolve-model`** — no DB migration, pure `pa **In scope (full plan)** -- Nine epics — see [Epic roadmap](#epic-roadmap-complete) below. -- Epics 1–6 **done**; Epic 7 **in progress**; Epics 8–9 **draft** with bead-scale tasks. -- [Autensa benchmark](#autensa-benchmark-2026-08) follow-ons: reliability (checkpoints, auto-nudge), operator UX (activity feed, DAG, PR recovery), cost caps. +- Twelve epics — see [Epic roadmap](#epic-roadmap-complete) below (engine + owner cockpit). +- Epics 1–6 **done**; Epic 7 **in progress**; Epics 8–12 **draft/ready** with bead-scale tasks. +- [Autensa benchmark](#autensa-benchmark-2026-08) follow-ons remain mapped under 1, 3, 5–9. - ADR at `docs/adr-model-routing.md`. **Out of scope (separate future plans)** @@ -125,6 +147,7 @@ Start with **`Task-shared-schemas-resolve-model`** — no DB migration, pure `pa - Agent-pipeline Phase 4 drift/rollup workers (replace GHA cron). - LiteLLM deployment guide (operators use OpenAI-compatible alias). - Symphony adoption. +- Autensa research → swipe ideation. ## Cross-team collaborators @@ -134,7 +157,7 @@ Start with **`Task-shared-schemas-resolve-model`** — no DB migration, pure `pa ## Epic roadmap (complete) -Sequential order with parallel opportunities noted. **Current focus: Epic 7** (worker end-to-end dogfood), then Epic 8 → 9. Follow-on tasks on done epics can run in parallel when they do not block the critical path. +**Current focus: close Epic 7** (draft PR + live E2E), then **Epic 8 → 9 → 10 → 11 → 12**. Follow-ons on done epics can run in parallel when they do not block the critical path. ```mermaid flowchart LR @@ -147,6 +170,9 @@ flowchart LR E7[7 Worker v1] E8[8 Forge webhooks] E9[9 Fleet dashboard] + E10[10 Plans roadmap UI] + E11[11 Completion artifacts] + E12[12 Ops copilot] E1 --> E6 E1 --> E7 @@ -159,19 +185,27 @@ flowchart LR E7 --> E8 E5 --> E9 E4 --> E9 + E4 --> E10 + E5 --> E10 + E8 --> E11 + E9 --> E12 + E10 --> E12 ``` | # | Epic | Status | Delivers | Depends on | |---|------|--------|----------|------------| -| 1 | [Model provider registry](./Epic-model-provider-registry/Epic-model-provider-registry.md) | done (+1 follow-on) | `resolveModel()`, Settings AI pages, run model logging; cost caps task | — | -| 2 | [Pipeline convoy scaffolding](./Epic-pipeline-convoy-scaffolding/Epic-pipeline-convoy-scaffolding.md) | done | `.convoys/`, `log-convoy-event.sh`, `convoy-planning.mdc` — dogfood metrics | — (parallel with 1) | -| 3 | [Connected repos](./Epic-connected-repos/Epic-connected-repos.md) | done (+1 follow-on) | `repo_links` table, paste URL, forge adapters; repo setup preflight | — | -| 4 | [Convoy lifecycle MCP](./Epic-convoy-lifecycle-mcp/Epic-convoy-lifecycle-mcp.md) | done | `create_convoy`, `transition_convoy_status`, state machine §7, `convoy_events` | 2, 3 | -| 5 | [Mission control gates UI](./Epic-mission-control-gates-ui/Epic-mission-control-gates-ui.md) | done (+2 follow-ons) | Kanban, gate inbox, co-pilot; DAG viz + automation tiers | 4 | -| 6 | [Orchestrator](./Epic-apps-orchestrator/Epic-apps-orchestrator.md) | done (+1 follow-on) | `apps/orchestrator`, Redis queue, role dispatch; auto-nudge | 1, 3, 4 | -| 7 | [Worker v1](./Epic-apps-worker-v1/Epic-apps-worker-v1.md) | **in progress** (3 tasks) | Tool loop, checkpoints, operator notes | 1, 3, 6 | -| 8 | [Forge webhooks](./Epic-forge-webhooks/Epic-forge-webhooks.md) | draft (2 tasks) | Webhooks + PR checks recovery panel | 3, 4, 7 | -| 9 | [Fleet dashboard](./Epic-fleet-dashboard/Epic-fleet-dashboard.md) | draft (2 tasks) | Cross-repo rollup + SSE activity feed | 4, 5 | +| 1 | [Model provider registry](./Epic-model-provider-registry/Epic-model-provider-registry.md) | done (+1 follow-on) | `resolveModel()`, Settings AI; cost caps task | — | +| 2 | [Pipeline convoy scaffolding](./Epic-pipeline-convoy-scaffolding/Epic-pipeline-convoy-scaffolding.md) | done | `.convoys/`, metrics, planning rule | — | +| 3 | [Connected repos](./Epic-connected-repos/Epic-connected-repos.md) | done (+1 follow-on) | `repo_links`, forge adapters; preflight | — | +| 4 | [Convoy lifecycle MCP](./Epic-convoy-lifecycle-mcp/Epic-convoy-lifecycle-mcp.md) | done | create/transition/events | 2, 3 | +| 5 | [Mission control gates UI](./Epic-mission-control-gates-ui/Epic-mission-control-gates-ui.md) | done (+2 follow-ons) | Kanban, gates, co-pilot; DAG, tiers | 4 | +| 6 | [Orchestrator](./Epic-apps-orchestrator/Epic-apps-orchestrator.md) | done (+1 follow-on) | Redis dispatch; auto-nudge | 1, 3, 4 | +| 7 | [Worker v1](./Epic-apps-worker-v1/Epic-apps-worker-v1.md) | **in progress** | Loop, checkpoints, **draft PR**, **live E2E**, notes | 1, 3, 6 | +| 8 | [Forge webhooks](./Epic-forge-webhooks/Epic-forge-webhooks.md) | draft | Webhooks + PR checks recovery | 3, 4, 7 | +| 9 | [Fleet dashboard](./Epic-fleet-dashboard/Epic-fleet-dashboard.md) | draft | Cross-repo home + SSE feed | 4, 5 | +| 10 | [Plans roadmap UI](./Epic-plans-roadmap-ui/Epic-plans-roadmap-ui.md) | draft | Tree, status, Start → convoy | 4, 5 | +| 11 | [Owner completion artifacts](./Epic-owner-completion-artifacts/Epic-owner-completion-artifacts.md) | draft | Summaries, gallery, screenshots | 7, 8 | +| 12 | [Ops copilot](./Epic-ops-copilot/Epic-ops-copilot.md) | draft | Project-aware status chat | 9, 10 | ### Overlap with existing plans @@ -184,7 +218,7 @@ flowchart LR ### End-state (what “done” looks like for the full plan) -Operator opens Echodo fleet view → starts convoy chat → hits **Go** → orchestrator runs pipeline roles with per-role models → worker executes briefs against connected repos → gates surface in inbox with AI co-pilot → approve → repeat until `released`. Live activity feed, spend rollups, and PR recovery are visible without leaving mission control. +Owner opens **fleet** → sees gates/PRs/releases across projects → opens a repo **Plans** roadmap → **Start** a ready task → Mission Control convoy → **Go** → worker (local or server models) builds without Cursor → draft PR → approve gates → **completion summary + screenshots/demos** → ask **ops copilot** what needs attention. `plans/` stays git SoT; DB + UI are how owners and agents query without dumping whole trees into context. ### Autensa benchmark (2026-08) @@ -218,6 +252,9 @@ Studied [crshdn/mission-control](https://github.com/crshdn/mission-control) (Aut | 7 Worker v1 | `./Epic-apps-worker-v1/Epic-apps-worker-v1.md` | | 8 Forge webhooks | `./Epic-forge-webhooks/Epic-forge-webhooks.md` | | 9 Fleet dashboard | `./Epic-fleet-dashboard/Epic-fleet-dashboard.md` | +| 10 Plans roadmap UI | `./Epic-plans-roadmap-ui/Epic-plans-roadmap-ui.md` | +| 11 Owner completion artifacts | `./Epic-owner-completion-artifacts/Epic-owner-completion-artifacts.md` | +| 12 Ops copilot | `./Epic-ops-copilot/Epic-ops-copilot.md` | ## Metrics and milestones @@ -229,23 +266,47 @@ Studied [crshdn/mission-control](https://github.com/crshdn/mission-control) (Aut | M3 Repos connected | 3 | Paste Forge/GitHub URL; clone succeeds from Echodo | | M4 Convoy state | 4 | `transition_convoy_status` enforces gate actors | | M5 Human surface | 5 | Gate inbox shows pending `convoy_review` | -| M6 Automation | 6–7 | Orchestrator dispatches one implementer brief unattended | +| M6 Automation | 6–7 | Unattended implementer brief (live E2E task) | +| M6b Draft PR | 7 | Worker opens draft PR on success | | M7 PR loop | 8 | Merge webhook moves convoy to `merged` | | M8 Fleet | 9 | Single view across ≥2 connected repos | -| M9 Reliability | 7 | Worker completes brief; checkpoint resume after kill | +| M9 Reliability | 7 | Checkpoint resume after kill | | M10 Observability | 9 | Activity feed shows role_complete without refresh | | M11 Spend control | 1 follow-on | Month-to-date spend + dispatch pause on cap | +| M12 Roadmap UI | 10 | Plans tree + Start → convoy | +| M13 Owner package | 11 | Completion summary (+ screenshot when UI) | +| M14 Ops chat | 12 | “What needs me?” answered with links | -## Recommended execution order (post-benchmark) +## Recommended execution order (owner vision) -1. **Epic 7 critical path** — [Task-worker-minimal-tool-loop](./Epic-apps-worker-v1/Task-worker-minimal-tool-loop.md) → live dogfood with orchestrator -2. **Epic 7 reliability** — checkpoint/resume, then operator notes -3. **Epic 8** — webhook receiver → PR checks panel (needs worker PR output) -4. **Epic 6 follow-on** — auto-nudge (pairs with checkpoints) -5. **Epic 3 follow-on** — repo preflight (before unattended prod dogfood) -6. **Epic 9** — fleet rollup → SSE feed -7. **Epic 5 follow-ons** — DAG viz, automation tiers (after Epic 8 for semi-auto) -8. **Epic 1 follow-on** — cost caps (after worker records token totals) +### Phase A — Engine (do now) + +1. [Task-worker-live-e2e-dogfood](./Epic-apps-worker-v1/Task-worker-live-e2e-dogfood.md) — confirm A1 on Coolify/local (**ops checklist**) +2. [Task-worker-draft-pr](./Epic-apps-worker-v1/Task-worker-draft-pr.md) — **next coding chat** if PR not already working +3. [Task-forge-webhook-receiver](./Epic-forge-webhooks/Task-forge-webhook-receiver.md) → [Task-pr-checks-recovery-panel](./Epic-forge-webhooks/Task-pr-checks-recovery-panel.md) +4. *Parallel optional:* operator notes, auto-nudge, repo preflight + +### Phase B — Fleet + +5. [Task-fleet-rollup-dashboard](./Epic-fleet-dashboard/Task-fleet-rollup-dashboard.md) → SSE feed + +### Phase C — Roadmap + +6. Epic 10: backlog tree tRPC → Plans page → Start → convoy + +### Phase D — Owner review + +7. Epic 11: completion summary → artifact gallery → screenshot capture + +### Phase E — Ops chat + +8. Epic 12: context tools → UI → draft work from chat + +### Polish (anytime after blockers clear) + +- Epic 5: DAG viz, automation tiers (tiers after Epic 8) +- Epic 1: cost budget caps +- Epic 7: operator notes if not done in Phase A ## Risks and mitigations From 6ccb060bea7d37e7f303722149708ad35761a355 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Tue, 25 Aug 2026 16:20:53 -0500 Subject: [PATCH 4/4] =?UTF-8?q?feat(second-brain):=20open=20self-hosted=20?= =?UTF-8?q?Obsidian=20for=20crews=20=E2=80=94=20harness-agnostic=20v0.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MCP create-backlog-item for natural language Plan/Epic/Task authoring - Worker non-code: optional worktree + per-harness allowlist + doc_review→published - Harness registry: harness_providers/role_harness_routes, WorkerAdapter (minimal|crewai|langgraph|remote-ag-ui), AG-UI POST /api/ag-ui, Settings → Harnesses with Test - Plans tree: backlog.listTree + createPlan/Epic/Task + Start→Convoy, web CRUD without CLI - Fleet: /fleet rollup across workspaces + SSE /api/fleet/events Decision: docs/decisions/2026-08-25-second-brain-os-pivot.md Convoy: .convoys/second-brain-os-v01 Co-authored-by: orchestrator --- .convoys/second-brain-os-v01.md | 69 + .../brief-1-mcp-create-backlog-item.md | 38 + .../brief-2-worker-non-code.md | 40 + .../brief-3-harness-registry-adapter.md | 44 + .../second-brain-os-v01/brief-4-plans-tree.md | 44 + .../brief-5-fleet-rollup.md | 38 + .cursor/mcp.json | 11 +- .env.example | 1 + .../src/lib/resolve-backlog-item.ts | 123 + apps/mcp-server/src/tools/claim-task.ts | 162 +- .../src/tools/create-backlog-item.ts | 362 ++ .../mcp-server/src/tools/find-backlog-item.ts | 70 + apps/mcp-server/src/tools/index.ts | 6 + apps/orchestrator/src/db/convoy.ts | 60 +- apps/orchestrator/src/index.ts | 26 +- .../app/(app)/[workspaceSlug]/plans/page.tsx | 655 +++ .../settings/harnesses/page.tsx | 165 + apps/web/app/(app)/fleet/page.tsx | 190 + apps/web/app/(marketing)/landing/page.tsx | 5 +- .../app/(marketing)/landing/pricing/page.tsx | 120 +- apps/web/app/api/ag-ui/[workspace]/route.ts | 105 + apps/web/app/api/fleet/events/route.ts | 165 + .../components/fleet/fleet-activity-feed.tsx | 101 + .../components/marketing/border-glow-card.tsx | 22 - .../marketing/comparison-section.tsx | 78 - apps/web/components/marketing/cta-section.tsx | 29 +- apps/web/components/marketing/dot-field.tsx | 21 - .../components/marketing/features-section.tsx | 127 +- .../web/components/marketing/hero-section.tsx | 83 +- .../marketing/how-it-works-section.tsx | 96 +- .../components/marketing/manifest-section.tsx | 57 + .../components/marketing/marquee-strip.tsx | 49 + .../web/components/marketing/product-demo.tsx | 75 +- .../components/marketing/scramble-text.tsx | 64 + apps/web/components/marketing/site-footer.tsx | 19 +- apps/web/components/marketing/site-header.tsx | 15 +- .../mission-control/convoy-detail-panel.tsx | 13 + apps/web/package.json | 1 + apps/web/server/lib/convoy/convoy-service.ts | 27 +- apps/web/server/root.ts | 2 + apps/web/server/routers/backlog.ts | 886 +++- apps/web/server/routers/convoys.ts | 110 +- apps/web/server/routers/harnesses.ts | 256 + apps/web/styles/marketing.css | 179 +- apps/worker/src/db/checkpoints.ts | 143 + apps/worker/src/db/convoy.ts | 3 + apps/worker/src/db/harness.ts | 53 + apps/worker/src/db/task-lifecycle.ts | 3 + apps/worker/src/env.ts | 7 + apps/worker/src/git/worktree.ts | 61 +- apps/worker/src/handle-job.test.ts | 286 ++ apps/worker/src/handle-job.ts | 322 +- apps/worker/src/harness.ts | 94 + apps/worker/src/index.dry-run.test.ts | 19 + apps/worker/src/session/minimal-session.ts | 133 +- apps/worker/src/tools/allowlist.ts | 9 +- apps/worker/src/tools/runner.ts | 8 +- docs/convoy-lifecycle.md | 6 +- .../2026-08-25-second-brain-os-pivot.md | 86 + docs/worker.md | 20 +- .../0014_ancient_lenny_balinger.sql | 22 + .../migrations/0015_curved_iron_fist.sql | 35 + .../migrations/meta/0014_snapshot.json | 3954 +++++++++++++++ .../migrations/meta/0015_snapshot.json | 4245 +++++++++++++++++ .../database/migrations/meta/_journal.json | 14 + packages/database/src/schema/agent_runs.ts | 1 + packages/database/src/schema/convoy_events.ts | 1 + .../database/src/schema/harness_providers.ts | 69 + packages/database/src/schema/index.ts | 2 + packages/database/src/schema/relations.ts | 22 + .../database/src/schema/worker_checkpoints.ts | 64 + packages/shared/package.json | 1 + .../shared/src/convoy-lifecycle/kanban.ts | 12 + .../shared/src/convoy-lifecycle/schemas.ts | 7 + .../src/convoy-lifecycle/state-machine.ts | 44 +- .../shared/src/orchestrator/convoy-driver.ts | 2 + packages/shared/src/orchestrator/job.ts | 3 + packages/shared/src/worker/checkpoint.test.ts | 42 + packages/shared/src/worker/checkpoint.ts | 49 + packages/shared/src/worker/executor.ts | 140 + packages/shared/src/worker/index.ts | 24 + .../Task-worker-checkpoint-resume.md | 20 +- .../Task-worker-minimal-tool-loop.md | 6 +- .../Plan-automation-foundation.md | 105 +- pnpm-lock.yaml | 3 + 85 files changed, 14145 insertions(+), 804 deletions(-) create mode 100644 .convoys/second-brain-os-v01.md create mode 100644 .convoys/second-brain-os-v01/brief-1-mcp-create-backlog-item.md create mode 100644 .convoys/second-brain-os-v01/brief-2-worker-non-code.md create mode 100644 .convoys/second-brain-os-v01/brief-3-harness-registry-adapter.md create mode 100644 .convoys/second-brain-os-v01/brief-4-plans-tree.md create mode 100644 .convoys/second-brain-os-v01/brief-5-fleet-rollup.md create mode 100644 apps/mcp-server/src/lib/resolve-backlog-item.ts create mode 100644 apps/mcp-server/src/tools/create-backlog-item.ts create mode 100644 apps/mcp-server/src/tools/find-backlog-item.ts create mode 100644 apps/web/app/(app)/[workspaceSlug]/plans/page.tsx create mode 100644 apps/web/app/(app)/[workspaceSlug]/settings/harnesses/page.tsx create mode 100644 apps/web/app/(app)/fleet/page.tsx create mode 100644 apps/web/app/api/ag-ui/[workspace]/route.ts create mode 100644 apps/web/app/api/fleet/events/route.ts create mode 100644 apps/web/components/fleet/fleet-activity-feed.tsx delete mode 100644 apps/web/components/marketing/border-glow-card.tsx delete mode 100644 apps/web/components/marketing/comparison-section.tsx delete mode 100644 apps/web/components/marketing/dot-field.tsx create mode 100644 apps/web/components/marketing/manifest-section.tsx create mode 100644 apps/web/components/marketing/marquee-strip.tsx create mode 100644 apps/web/components/marketing/scramble-text.tsx create mode 100644 apps/web/server/routers/harnesses.ts create mode 100644 apps/worker/src/db/checkpoints.ts create mode 100644 apps/worker/src/db/harness.ts create mode 100644 apps/worker/src/handle-job.test.ts create mode 100644 apps/worker/src/harness.ts create mode 100644 apps/worker/src/index.dry-run.test.ts create mode 100644 docs/decisions/2026-08-25-second-brain-os-pivot.md create mode 100644 packages/database/migrations/0014_ancient_lenny_balinger.sql create mode 100644 packages/database/migrations/0015_curved_iron_fist.sql create mode 100644 packages/database/migrations/meta/0014_snapshot.json create mode 100644 packages/database/migrations/meta/0015_snapshot.json create mode 100644 packages/database/src/schema/harness_providers.ts create mode 100644 packages/database/src/schema/worker_checkpoints.ts create mode 100644 packages/shared/src/worker/checkpoint.test.ts create mode 100644 packages/shared/src/worker/checkpoint.ts create mode 100644 packages/shared/src/worker/executor.ts diff --git a/.convoys/second-brain-os-v01.md b/.convoys/second-brain-os-v01.md new file mode 100644 index 0000000..9ea5e8b --- /dev/null +++ b/.convoys/second-brain-os-v01.md @@ -0,0 +1,69 @@ +--- +name: second-brain-os-v01 +classification: server-only +success_metric: Web-first second brain OS publishable via docker compose up — any agent (OpenCode/Cursor) can create Plan/Epic/Task via MCP, Plans tree CRUD works without CLI, Fleet shows all workspaces, harness-agnostic worker runs doc + code tasks with BYO LLMs +skip: + - ia + - ux + - design + - a11y + - visual + - smoke +status: in-progress +created: 2026-08-25 +model_policy: + roles: + role-conductor: claude-planning + role-architect: claude-planning + role-implementer: qwen-coder + role-reviewer: qwen-coder + escalate_to: claude-planning +--- + +# Second Brain OS v0.1 — open self-hosted Obsidian for crews + +## Why + +Echodo pivots per `docs/decisions/2026-08-25-second-brain-os-pivot.md` from closed code-cockpit to open "Obsidian for agent crews": `docker compose up`, BYO LLMs (Ollama 11434 / MLX 8000 / NIM), any harness via AG-UI/MCP. Current `plans/` is hand-edited markdown + CLI import; Fleet is per-workspace kanban only; worker is code-only with hard worktree + 4-tool allowlist. This convoy makes the homelab dogfoodable + publishable for stars. + +See `plans/Plan-automation-foundation/Plan-automation-foundation.md` v0.1 order (2026-08-25 pivot). + +## Scope + +1. MCP `create-backlog-item` — natural language `Plan/Epic/Task` creation (0.5d) +2. Worker non-code: optional worktree + per-harness allowlist + `doc_review→published` (1d) +3. Harness registry + WorkerAdapter: `harness_providers` + `role_harness_routes`, `AG-UI /api/ag-ui`, `Settings → Harnesses` (2-3d) +4. Plans tree: `apps/web/app/(app)/[workspaceSlug]/plans/page.tsx` + `backlog.createPlan/createEpic/createTask` + `Start→Convoy` (1w) — Epic 10 +5. Fleet rollup: `/fleet` + SSE `convoy_events` feed (3-4d) — Epic 9 + +Out of scope: Epic 8 webhooks, Epic 11 artifacts, Epic 12 copilot (post-v0.1); LiteLLM required, per-user overrides. + +## Roles invoked + +Conductor → Architect (briefs = 5 above, 1:1 to roadmap) → Implementer per brief (sequential, each needs prior) → Reviewer (PR). + +## Todos + +- [ ] Brief 1 MCP create +- [ ] Brief 2 worker non-code +- [ ] Brief 3 harness registry — depends on 2 +- [ ] Brief 4 Plans tree — depends on 1,3 +- [ ] Brief 5 Fleet — depends on 3 + +## IA + +Skipped (`ia` in skip). No new IA — `plans/` hierarchy `docs/Glossary.md:50` + `objects` `packages/database/src/schema/objects.ts:18` unchanged. + +## UX + +Skipped (`ux` in skip). Reuses existing `Mission control` kanban `apps/web/app/(app)/[workspaceSlug]/mission-control/page.tsx:12` + `Settings` patterns. + +## Architecture + +See decision log + `docs/adr-model-routing.md`. Key new: +- `packages/database/src/schema/harness_providers.ts` (mirror `model_providers` `docs/adr-model-routing.md:78`) +- `packages/shared/src/worker/executor.ts` `WorkerAdapter` +- `apps/worker/src/handle-job.ts:40` dispatch via adapter +- `apps/web/app/api/ag-ui/[workspace]/route.ts` AG-UI +- `apps/mcp-server/src/tools/create-backlog-item.ts` +- `apps/web/server/routers/backlog.ts:29` add CRUD for `plans/*.md` diff --git a/.convoys/second-brain-os-v01/brief-1-mcp-create-backlog-item.md b/.convoys/second-brain-os-v01/brief-1-mcp-create-backlog-item.md new file mode 100644 index 0000000..9c5a8f5 --- /dev/null +++ b/.convoys/second-brain-os-v01/brief-1-mcp-create-backlog-item.md @@ -0,0 +1,38 @@ +--- +brief: 1 +convoy: second-brain-os-v01 +role: role-implementer +title: MCP create-backlog-item tool for natural language Plan/Epic/Task authoring +status: ready +priority: P0 +slice_dependencies: [] +recommended_model: qwen-coder +model_tier: local +workspace_id: global +--- + +# Brief 1 — MCP `create-backlog-item` (0.5d) + +## Goal + +Any agent (OpenCode/Cursor/Codex) via MCP can say "create an Epic for offline mode with 3 tasks" and it writes `plans/Plan-*/Epic-*/Task-*.md` + re-imports to `markdown_backlog_items` `packages/database/src/markdown-backlog/sync.ts:38`. + +## Files + +- `apps/mcp-server/src/tools/create-backlog-item.ts` NEW — zod schema `kind: plan|epic|task`, `slug`, `title`, `planSlug`, `epicSlug`, `status`, `priority`, `bodyMarkdown`, `agent_prompt`; validates frontmatter per `docs/templates/`, writes file via `exportBacklogItemToMarkdown` inverse, calls `syncMarkdownBacklogScan` or direct DB insert + `contentHash` `packages/database/src/markdown-backlog/parse.ts:1`. +- `apps/mcp-server/src/tools/index.ts` register +- `packages/database/src/markdown-backlog/parse.test.ts` add case + +## Acceptance + +- `mcp call create-backlog-item {workspace:"maya-workspace", kind:"task", slug:"research-prompt", planSlug:"clarity", epicSlug:"ai-tags", title:"Research prompt", bodyMarkdown:"..."}` creates `plans/Plan-clarity/Epic-ai-tags/Task-research-prompt.md` with YAML frontmatter + body, row appears in `markdown_backlog_items` without manual `pnpm import`. +- Duplicate slug under same epic → error `CONFLICT`. +- `pnpm lint && pnpm type-check && pnpm --filter @tasks/mcp-server test` (if exists) pass; `apps/mcp-server` still builds Docker. + +## Prompt for implementer + +You are `role-implementer` for `second-brain-os-v01` brief 1. Read `docs/decisions/2026-08-25-second-brain-os-pivot.md`, `packages/database/src/markdown-backlog/parse.ts`, `apps/mcp-server/src/tools/claim-task.ts:1` as reference. Implement exactly this brief, keep changes small, run `pnpm lint && pnpm type-check`. + +## Dogfood + +`claim_task` on the matching `plans/Plan-automation-foundation/.../Task-*.md` if exists (or ad-hoc) → `complete_task outcome:succeeded tokensTotal:... modelAlias:qwen-coder`. diff --git a/.convoys/second-brain-os-v01/brief-2-worker-non-code.md b/.convoys/second-brain-os-v01/brief-2-worker-non-code.md new file mode 100644 index 0000000..08f059a --- /dev/null +++ b/.convoys/second-brain-os-v01/brief-2-worker-non-code.md @@ -0,0 +1,40 @@ +--- +brief: 2 +convoy: second-brain-os-v01 +role: role-implementer +title: Worker non-code — optional worktree + per-harness allowlist + doc_review→published +status: ready +priority: P0 +slice_dependencies: [] +recommended_model: qwen-coder +model_tier: local +--- + +# Brief 2 — Worker non-code (1d) + +## Goal + +`book/course/research/SOPs` docs don't need git worktree or `pnpm`. Worker must handle doc tasks writing straight to `docs/`/`plans/` no branch, with allowlist per harness. + +## Files + +- `apps/worker/src/git/worktree.ts:1` — add `needsWorktree: boolean` param; when false skip `ensureWorktree`, use `monorepoRoot` directly. Keep existing path for code briefs `job.briefId` present. +- `apps/worker/src/tools/allowlist.ts:1` — make `allowlist` configurable per harness `harness.config.allowedCommands` (or `minimal: [read,write,grep]` only). Pass `allowedCommands` into `runCommandTool`. +- `packages/shared/src/convoy-lifecycle/state-machine.ts:1` — add `doc_review → published` transition (mirror `pr_review → merged`), allow `human-gate-2` analogue `doc-approver`. +- `apps/worker/src/handle-job.ts:40` — thread `harness.needsWorktree` + `harness.allowlist` through; log `harnessAlias` to `convoy_events`. +- `docs/worker.md:1` document optional worktree. + +## Acceptance + +- `Task-write-manifesto.md` (doc task) with `harness: doc-writer` creates `docs/manifesto.md` via `write_file` tool without worktree directory under `/tmp/echodo-worker-worktrees`. +- Code task `Task-fix-lint` still creates worktree per prior behavior. +- `transition_convoy_status` `doc_review → published` succeeds; existing `pr_review → merged` still works. +- `pnpm lint && pnpm type-check && pnpm --filter @tasks/worker test` pass. + +## Prompt + +You are `role-implementer` brief 2. Read `docs/convoy-lifecycle.md`, `packages/shared/src/convoy-lifecycle/state-machine.ts`, `apps/worker/src/handle-job.ts`, `docs/decisions/2026-08-25-second-brain-os-pivot.md`. Implement small, gated. + +## Depends + +None — can run in parallel with brief 1. diff --git a/.convoys/second-brain-os-v01/brief-3-harness-registry-adapter.md b/.convoys/second-brain-os-v01/brief-3-harness-registry-adapter.md new file mode 100644 index 0000000..2143f6d --- /dev/null +++ b/.convoys/second-brain-os-v01/brief-3-harness-registry-adapter.md @@ -0,0 +1,44 @@ +--- +brief: 3 +convoy: second-brain-os-v01 +role: role-implementer +title: Harness registry + WorkerAdapter + AG-UI + Settings → Harnesses +status: ready +priority: P0 +slice_dependencies: [1, 2] +recommended_model: qwen-coder +model_tier: local +--- + +# Brief 3 — Harness registry + WorkerAdapter (2-3d) — THE big one + +## Goal + +Workspace admins CRUD harness providers (`minimal|crewai|langgraph|remote-ag-ui`) + map `role_id → harness_alias`, worker dispatches via adapter, any harness via `AG-UI /api/ag-ui/[workspace]`, test button proves reachability. + +## Files + +- `packages/database/src/schema/harness_providers.ts` NEW — mirror `model_providers` `docs/adr-model-routing.md:78`: `id, workspace_id fk, alias unique per workspace, display_name, harness_type enum, endpoint_url nullable, auth_env_var varchar nullable, config jsonb, enabled bool, created_at/updated_at`. + `role_harness_routes` `workspace_id, role_id varchar, harness_provider_id fk, enabled, sort_order`, unique `(workspace_id, role_id)`. +- `packages/database/migrations/0015_*` via `pnpm db:generate`. +- `packages/shared/src/worker/executor.ts` NEW — `WorkerExecutor {id, run(ctx, model):Promise<...>}`, `resolveHarnessForRole()` pure like `resolveModel` `packages/shared/src/model-routing/index.ts`. +- `apps/worker/src/handle-job.ts:40` — replace hard `createMinimalSession` with `registry.get(harnessAlias).run(...)`; keep `MinimalSession` as `minimal` adapter. +- `apps/web/server/routers/harnesses.ts` NEW — `list/create/update/delete/testHarness` (admin only, never return `auth` value, `test` does fetch `health` on endpoint with `Authorization: Bearer ${process.env[AUTH_ENV_VAR]}`). +- `apps/web/app/(app)/[workspaceSlug]/settings/harnesses/page.tsx` NEW — table + add/edit dialog + test. +- `apps/web/app/api/ag-ui/[workspace]/route.ts` NEW — POST ag-ui envelope, validate `workspace_id`, write `convoy_events`/`agent_runs`, enforce tenant fence. +- `packages/database/src/schema/index.ts` exports. + +## Acceptance + +- Migration applies `pnpm db:generate && pnpm db:migrate` locally (no drift). +- `Settings → Harnesses` CRUD works; `Test` on `remote-ag-ui https://crew.example.com/ag-ui` with `HARNESS_AUTH_TOKEN` env shows latency or error detail, never leaks token to browser. +- `resolveHarnessForRole(workspaceId, "role-implementer")` returns `crewai` when mapped, `minimal` fallback. +- Worker dispatched `role-implementer` via `crewai` adapter still `claim_task`/`complete_task` and writes `harness_alias` to `convoy_events.harness_alias` + `agent_runs`. +- `pnpm lint && pnpm type-check && pnpm test` pass. + +## Prompt + +You are `role-implementer` brief 3. Read `docs/adr-model-routing.md`, `docs/decisions/2026-08-25-second-brain-os-pivot.md`, `packages/database/src/schema/model_providers.ts`, `packages/shared/src/model-routing/index.ts`. Keep diff minimal, follow existing Drizzle/tRPC patterns in `server/routers/ai-models.ts`. + +## Depends + +Briefs 1,2 — needs optional worktree done. diff --git a/.convoys/second-brain-os-v01/brief-4-plans-tree.md b/.convoys/second-brain-os-v01/brief-4-plans-tree.md new file mode 100644 index 0000000..c6fed21 --- /dev/null +++ b/.convoys/second-brain-os-v01/brief-4-plans-tree.md @@ -0,0 +1,44 @@ +--- +brief: 4 +convoy: second-brain-os-v01 +role: role-implementer +title: Plans tree — web CRUD for plans/ without CLI, Start→Convoy +status: ready +priority: P0 +slice_dependencies: [1, 3] +recommended_model: qwen-coder +model_tier: local +--- + +# Brief 4 — Plans tree (1w) — Epic 10 + +## Goal + +Web `Plans` is Obsidian-like tree — read/write `plans/Plan-*/Epic-*/Task-*.md` without `pnpm import:markdown-backlog` CLI; click `Start → Convoy`. + +## Files + +- `apps/web/server/routers/backlog.ts:29` — add `listTree` (returns hierarchical `Plan → Epic → Task` with `status/priority/repoPath`), `createPlan/createEpic/createTask` (`slug/title/bodyMarkdown/agent_prompt` → writes `.md` via `join(repoRoot, repoPath)` atomically `.tmp→rename`, then direct DB upsert or `syncMarkdownBacklogScan`), `updateTaskBody`, `getBacklogItem` already exists. Enforce slug `^[a-z0-9-]+$`, `workspace_id` fence, `updated_at` bump. +- `apps/web/app/(app)/[workspaceSlug]/plans/page.tsx` NEW — tree component (collapsible `Radix Collapsible` + `ScrollArea`), status chips, search, `New Plan/Epic/Task` dialogs, `Start` button → calls `convoys.create` `apps/web/server/routers/convoys.ts:173` with `planSlug/epicSlug`. +- Reuse `resolveWorkflowPrompt` preview in task detail drawer. +- `docs/Glossary.md:50` hierarchy respected; `config/CursorSync.md:33` export allow-list preserved (web writes go through same export path). + +## Acceptance + +- `Plans` page shows `Plan-clarity → Epic-ai-tags → Task-local-tag-prompt [ready]` from DB without CLI after DB seed. +- `New Task` dialog `title: Research prompt` `slug: research-prompt` creates `plans/Plan-clarity/Epic-ai-tags/Task-research-prompt.md` on disk + row in `markdown_backlog_items` visible after refresh without manual import. +- `Start` on a `ready` task creates convoy `clarity-research-prompt` at `draft` and links `briefFiles: [repoPath]`. +- `MCP create-backlog-item` brief 1 and web create produce same file format (round-trip parse test). +- `pnpm lint && pnpm type-check` pass. + +## Prompt + +You are `role-implementer` brief 4. Read `plans/README.md`, `packages/database/src/markdown-backlog/parse.ts`, `apps/web/server/routers/backlog.ts`, `docs/decisions/2026-08-25-second-brain-os-pivot.md`. Follow existing tRPC patterns `ai-models.ts`. Keep UI to `components/ui/*` + Tailwind. + +## Depends + +Briefs 1,3 — needs MCP tool shape + harness registry for Start. + +## Out of scope + +Full Notion drag-n-drop, breadcrumb perf — keep tree simple. diff --git a/.convoys/second-brain-os-v01/brief-5-fleet-rollup.md b/.convoys/second-brain-os-v01/brief-5-fleet-rollup.md new file mode 100644 index 0000000..77aecc6 --- /dev/null +++ b/.convoys/second-brain-os-v01/brief-5-fleet-rollup.md @@ -0,0 +1,38 @@ +--- +brief: 5 +convoy: second-brain-os-v01 +role: role-implementer +title: Fleet rollup /fleet + SSE convoy_events feed +status: ready +priority: P1 +slice_dependencies: [3] +recommended_model: qwen-coder +model_tier: local +--- + +# Brief 5 — Fleet rollup `/fleet` + SSE (3-4d) — Epic 9 + +## Goal + +`echodo.stillwell.cloud/fleet` shows *all* workspaces + `connected_repos` projects needing you — the "land and see everything" you asked for. Live `convoy_events` tail without refresh. + +## Files + +- `apps/web/server/routers/convoys.ts:119` — add `fleet` procedure (no `workspace` arg, uses `ctx.session.user.id` to gather all `workspaceMembers` workspaces → `listConvoyProjects` across them, return `byWorkspace, byStatus, gateInboxAcross`). Reuse `GATE_INBOX_STATUSES`. +- `apps/web/app/(app)/fleet/page.tsx` NEW — top `Needs you` list, grid `byWorkspace` cards, reuse `ConvoyKanban` `apps/web/app/(app)/[workspaceSlug]/mission-control/page.tsx:12` + `ConvoyDetailPanel` but workspace-agnostic. +- `apps/web/app/api/fleet/events/route.ts` NEW — SSE `text/event-stream` tailing `convoy_events` `packages/database/src/schema/convoy_events.ts:1` filtered to user's workspaces, `occurredAt desc` 30 rows + live publish via `pg_notify` or polling 5s fallback (reuse orchestrator poll). +- `apps/web/components/fleet/fleet-activity-feed.tsx` NEW — auto-append without refresh. + +## Acceptance + +- `/fleet` lists convoys from `maya-workspace` + `studio-internal` + `bakery-coop` together; `Needs you` shows `× pr_review 2d` count. +- Clicking fleet card opens detail with correct `workspaceSlug` context for `gateAction`. +- SSE feed shows `role_complete` event within 5s of worker `logConvoyEvent` `apps/mcp-server/src/tools/log-convoy-event.ts`. + +## Prompt + +You are `role-implementer` brief 5. Read `apps/web/server/routers/convoys.ts:173`, `apps/web/server/lib/convoy/convoy-service.ts`, `plans/Plan-automation-foundation/Epic-fleet-dashboard/Epic-fleet-dashboard.md`, `docs/decisions/2026-08-25-second-brain-os-pivot.md`. Keep polling fallback simple, no Redis pubsub v1. + +## Depends + +Brief 3 — needs harness field in events for display. diff --git a/.cursor/mcp.json b/.cursor/mcp.json index 2025e50..e0dddd6 100644 --- a/.cursor/mcp.json +++ b/.cursor/mcp.json @@ -2,8 +2,15 @@ "_comment": "Echodo's own MCP server. Spawned per Cursor session over stdio. The 'mcp' script in apps/mcp-server/package.json invokes tsx with --env-file=../../.env so DATABASE_URL is read from the repo's .env without leaking it into this committed file. The -s/--silent flag on pnpm suppresses its progress output that would otherwise corrupt the MCP stdio protocol. If you fork the repo, copy .env.example → .env and fill in DATABASE_URL.", "mcpServers": { "echodo": { - "command": "pnpm", - "args": ["-s", "--filter", "@tasks/mcp-server", "mcp"] + "command": "/Users/rstillw/.nvm/versions/node/v22.14.0/bin/pnpm", + "args": [ + "-s", + "-C", + "/Users/rstillw/Documents/Personal Coding Projects/tasks", + "--filter", + "@tasks/mcp-server", + "mcp" + ] } } } diff --git a/.env.example b/.env.example index 9b0336a..1eccec9 100644 --- a/.env.example +++ b/.env.example @@ -41,6 +41,7 @@ REDIS_URL="redis://:CHANGE_ME@192.168.68.102:6379/11" # WORKER_DRY_RUN=1 skips Redis dequeue loop # WORKER_BLPOP_TIMEOUT_S=5 # WORKER_MAX_TOOL_STEPS=12 +# WORKER_CHECKPOINT_EVERY_N_STEPS=1 # WORKER_REPO_ROOT= # defaults to monorepo root (pnpm-workspace.yaml walk-up) # WORKER_WORKTREE_BASE=/tmp/echodo-worker-worktrees diff --git a/apps/mcp-server/src/lib/resolve-backlog-item.ts b/apps/mcp-server/src/lib/resolve-backlog-item.ts new file mode 100644 index 0000000..8ee015e --- /dev/null +++ b/apps/mcp-server/src/lib/resolve-backlog-item.ts @@ -0,0 +1,123 @@ +import { z } from "zod"; + +import { and, eq, isNull } from "../drizzle.js"; +import { db } from "../db.js"; +import { markdownBacklogItems } from "../schema.js"; + +export const backlogKindSchema = z.enum(["plan", "epic", "task"]); + +export const backlogItemLookupFieldsSchema = z.object({ + repoPath: z + .string() + .min(1) + .optional() + .describe( + "Repo-relative path under plans/, e.g. plans/Plan-automation-foundation/Epic-apps-worker-v1/Task-worker-minimal-tool-loop.md", + ), + slug: z.string().min(1).optional(), + planSlug: z.string().min(1).optional(), + epicSlug: z.string().min(1).optional(), + kind: backlogKindSchema.optional(), +}); + +export const backlogItemLookupSchema = backlogItemLookupFieldsSchema.superRefine( + (input, ctx) => { + const hasRepoPath = Boolean(input.repoPath?.trim()); + const hasSlugKeys = Boolean(input.slug?.trim() && input.planSlug?.trim()); + if (!hasRepoPath && !hasSlugKeys) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Provide repoPath, or slug + planSlug (add epicSlug for tasks/epics).", + }); + } + }, +); + +export type BacklogItemLookup = z.infer; + +export type ResolvedBacklogItem = { + id: string; + workspaceId: string; + kind: string; + slug: string; + planSlug: string; + epicSlug: string | null; + repoPath: string; + title: string; + bodyMarkdown: string; + status: string | null; + priority: string | null; +}; + +export const BACKLOG_ITEM_NOT_FOUND_MESSAGE = + "Backlog item not found. Run `pnpm --filter @tasks/database import:markdown-backlog` (with MARKDOWN_BACKLOG_WORKSPACE_ID set) if the plans/ file exists but has not been imported yet."; + +export function normalizeRepoPath(repoPath: string): string { + return repoPath.trim().replace(/^\.\/+/, ""); +} + +function buildLookupConditions( + workspaceId: string, + lookup: BacklogItemLookup, +) { + const conditions = [ + eq(markdownBacklogItems.workspaceId, workspaceId), + isNull(markdownBacklogItems.archivedAt), + ]; + + if (lookup.repoPath?.trim()) { + conditions.push( + eq(markdownBacklogItems.repoPath, normalizeRepoPath(lookup.repoPath)), + ); + return conditions; + } + + conditions.push(eq(markdownBacklogItems.slug, lookup.slug!.trim())); + conditions.push(eq(markdownBacklogItems.planSlug, lookup.planSlug!.trim())); + if (lookup.epicSlug?.trim()) { + conditions.push(eq(markdownBacklogItems.epicSlug, lookup.epicSlug.trim())); + } + if (lookup.kind) { + conditions.push(eq(markdownBacklogItems.kind, lookup.kind)); + } + + return conditions; +} + +const backlogItemSelect = { + id: markdownBacklogItems.id, + workspaceId: markdownBacklogItems.workspaceId, + kind: markdownBacklogItems.kind, + slug: markdownBacklogItems.slug, + planSlug: markdownBacklogItems.planSlug, + epicSlug: markdownBacklogItems.epicSlug, + repoPath: markdownBacklogItems.repoPath, + title: markdownBacklogItems.title, + bodyMarkdown: markdownBacklogItems.bodyMarkdown, + status: markdownBacklogItems.status, + priority: markdownBacklogItems.priority, +}; + +export async function resolveBacklogItemById( + backlogItemId: string, +): Promise { + const [row] = await db + .select(backlogItemSelect) + .from(markdownBacklogItems) + .where(eq(markdownBacklogItems.id, backlogItemId)) + .limit(1); + + return row ?? null; +} + +export async function resolveBacklogItemsByLookup( + workspaceId: string, + lookup: BacklogItemLookup, +): Promise { + return db + .select(backlogItemSelect) + .from(markdownBacklogItems) + .where(and(...buildLookupConditions(workspaceId, lookup))) + .limit(5); +} diff --git a/apps/mcp-server/src/tools/claim-task.ts b/apps/mcp-server/src/tools/claim-task.ts index 96d383c..f2a7063 100644 --- a/apps/mcp-server/src/tools/claim-task.ts +++ b/apps/mcp-server/src/tools/claim-task.ts @@ -8,6 +8,12 @@ import { auditLog, markdownBacklogItems, } from "../schema.js"; +import { + BACKLOG_ITEM_NOT_FOUND_MESSAGE, + backlogItemLookupFieldsSchema, + resolveBacklogItemById, + resolveBacklogItemsByLookup, +} from "../lib/resolve-backlog-item.js"; import { resolveWorkspaceHandle } from "../lib/resolve-workspace.js"; import { resolveWorkflowPrompt } from "../../../../packages/database/src/markdown-backlog/resolve-prompt.ts"; import { exportBacklogItemToMarkdown } from "../../../../packages/database/src/markdown-backlog/export.ts"; @@ -40,45 +46,62 @@ import { toolCatch, toolErr, toolOk } from "./tool-result.js"; * `agent_runs` separately. */ -const claimTaskInputSchema = z.object({ - workspace: z - .string() - .min(1) - .describe("Workspace UUID or slug (e.g. 'acme' or '550e8400-...')"), - backlogItemId: z - .string() - .uuid() - .describe("UUID of the markdown_backlog_items row being claimed."), - actorUserId: z - .string() - .uuid() - .nullable() - .optional() - .describe( - "Optional UUID of the human user the agent is acting on behalf of. Null = anonymous dev session.", - ), - notes: z - .string() - .max(2_000) - .optional() - .describe("Short opener text attached to the run (e.g. session intent)."), - client: z - .string() - .min(1) - .max(50) - .optional() - .describe( - "Caller identifier — e.g. 'cursor-ide', 'cursor-cloud', 'codex', 'echodo-orchestrator'. Persisted in agent_runs.metadata so the runs UI can slice by client.", - ), - model: z - .string() - .min(1) - .max(100) - .optional() - .describe( - "Model handle driving the session — e.g. 'claude-4.6-sonnet', 'gpt-5-codex'. Free-form because new models ship faster than we can rev a zod enum.", - ), -}); +const claimTaskInputSchema = z + .object({ + workspace: z + .string() + .min(1) + .describe("Workspace UUID or slug (e.g. 'acme' or '550e8400-...')"), + backlogItemId: z + .string() + .uuid() + .optional() + .describe( + "UUID of the markdown_backlog_items row being claimed. Optional when repoPath or slug keys are provided.", + ), + actorUserId: z + .string() + .uuid() + .nullable() + .optional() + .describe( + "Optional UUID of the human user the agent is acting on behalf of. Null = anonymous dev session.", + ), + notes: z + .string() + .max(2_000) + .optional() + .describe("Short opener text attached to the run (e.g. session intent)."), + client: z + .string() + .min(1) + .max(50) + .optional() + .describe( + "Caller identifier — e.g. 'cursor-ide', 'cursor-cloud', 'codex', 'echodo-orchestrator'. Persisted in agent_runs.metadata so the runs UI can slice by client.", + ), + model: z + .string() + .min(1) + .max(100) + .optional() + .describe( + "Model handle driving the session — e.g. 'claude-4.6-sonnet', 'gpt-5-codex'. Free-form because new models ship faster than we can rev a zod enum.", + ), + }) + .merge(backlogItemLookupFieldsSchema) + .superRefine((input, ctx) => { + const hasId = Boolean(input.backlogItemId); + const hasRepoPath = Boolean(input.repoPath?.trim()); + const hasSlugKeys = Boolean(input.slug?.trim() && input.planSlug?.trim()); + if (!hasId && !hasRepoPath && !hasSlugKeys) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Provide backlogItemId, repoPath, or slug + planSlug (add epicSlug for tasks/epics).", + }); + } + }); /** * Build the metadata object stored on agent_runs at claim time. Returns @@ -100,7 +123,7 @@ export function registerClaimTaskTool(mcp: McpServer): void { "claim_task", { description: - "Open an agent_runs row against a backlog item, transition it to in_progress (if eligible), and return the effective workflow prompt for the session.", + "Open an agent_runs row against a backlog item, transition it to in_progress (if eligible), and return the effective workflow prompt for the session. Identify the item by backlogItemId, repoPath, or slug + planSlug.", inputSchema: claimTaskInputSchema, }, async (args) => { @@ -108,27 +131,42 @@ export function registerClaimTaskTool(mcp: McpServer): void { const input = claimTaskInputSchema.parse(args); const ws = await resolveWorkspaceHandle(input.workspace); - const [item] = await db - .select({ - id: markdownBacklogItems.id, - workspaceId: markdownBacklogItems.workspaceId, - title: markdownBacklogItems.title, - bodyMarkdown: markdownBacklogItems.bodyMarkdown, - status: markdownBacklogItems.status, - }) - .from(markdownBacklogItems) - .where(eq(markdownBacklogItems.id, input.backlogItemId)) - .limit(1); + let item; + let backlogItemId: string; - if (!item) { - return toolErr(`Backlog item ${input.backlogItemId} not found`); + if (input.backlogItemId) { + const found = await resolveBacklogItemById(input.backlogItemId); + if (!found) { + return toolErr(`Backlog item ${input.backlogItemId} not found`); + } + item = found; + backlogItemId = found.id; + } else { + const matches = await resolveBacklogItemsByLookup(ws.id, { + repoPath: input.repoPath, + slug: input.slug, + planSlug: input.planSlug, + epicSlug: input.epicSlug, + kind: input.kind, + }); + if (matches.length === 0) { + return toolErr(BACKLOG_ITEM_NOT_FOUND_MESSAGE); + } + if (matches.length > 1) { + return toolErr( + `Ambiguous backlog lookup (${matches.length} matches). Add epicSlug/kind or use repoPath.`, + ); + } + item = matches[0]!; + backlogItemId = item.id; } + if (item.workspaceId !== ws.id) { // Tenancy fence: the backlog item exists but in a different // workspace than the resolved handle. Refuse rather than 404 so // the caller can't fish for cross-tenant existence. return toolErr( - `Backlog item ${input.backlogItemId} does not belong to workspace ${ws.slug}`, + `Backlog item ${backlogItemId} does not belong to workspace ${ws.slug}`, ); } @@ -150,7 +188,7 @@ export function registerClaimTaskTool(mcp: McpServer): void { .from(agentRuns) .where( and( - eq(agentRuns.backlogItemId, input.backlogItemId), + eq(agentRuns.backlogItemId, backlogItemId), isNull(agentRuns.finishedAt), ), ) @@ -190,7 +228,7 @@ export function registerClaimTaskTool(mcp: McpServer): void { const resolved = await resolveWorkflowPrompt(db, { workspaceId: ws.id, - backlogItemId: input.backlogItemId, + backlogItemId, }); return toolOk({ @@ -203,6 +241,7 @@ export function registerClaimTaskTool(mcp: McpServer): void { title: item.title, bodyMarkdown: item.bodyMarkdown, status: item.status, + repoPath: item.repoPath, }, workspace: { id: ws.id, slug: ws.slug, name: ws.name }, }); @@ -219,7 +258,7 @@ export function registerClaimTaskTool(mcp: McpServer): void { .insert(agentRuns) .values({ workspaceId: ws.id, - backlogItemId: input.backlogItemId, + backlogItemId, actorUserId, notes: input.notes ?? null, metadata: claimMeta, @@ -230,7 +269,7 @@ export function registerClaimTaskTool(mcp: McpServer): void { await tx .update(markdownBacklogItems) .set({ status: "in_progress", updatedAt: new Date() }) - .where(eq(markdownBacklogItems.id, input.backlogItemId)); + .where(eq(markdownBacklogItems.id, backlogItemId)); } await tx.insert(auditLog).values({ @@ -240,7 +279,7 @@ export function registerClaimTaskTool(mcp: McpServer): void { targetType: "agent_run", targetId: created.id, metadata: { - backlog_item_id: input.backlogItemId, + backlog_item_id: backlogItemId, prior_status: priorStatus, transitioned: shouldTransition, ...(claimMeta ?? {}), @@ -253,7 +292,7 @@ export function registerClaimTaskTool(mcp: McpServer): void { const resolved = await resolveWorkflowPrompt(db, { workspaceId: ws.id, - backlogItemId: input.backlogItemId, + backlogItemId, }); // Only export when the row's status actually changed. On a @@ -269,7 +308,7 @@ export function registerClaimTaskTool(mcp: McpServer): void { try { const exportResult = await exportBacklogItemToMarkdown(db, { workspaceId: ws.id, - backlogItemId: input.backlogItemId, + backlogItemId, repoRootAbs: repoRoot, }); if (exportResult.ok) { @@ -302,6 +341,7 @@ export function registerClaimTaskTool(mcp: McpServer): void { title: item.title, bodyMarkdown: item.bodyMarkdown, status: shouldTransition ? "in_progress" : priorStatus, + repoPath: item.repoPath, }, workspace: { id: ws.id, slug: ws.slug, name: ws.name }, export: exportSummary, diff --git a/apps/mcp-server/src/tools/create-backlog-item.ts b/apps/mcp-server/src/tools/create-backlog-item.ts new file mode 100644 index 0000000..b536291 --- /dev/null +++ b/apps/mcp-server/src/tools/create-backlog-item.ts @@ -0,0 +1,362 @@ +import { mkdir, writeFile, rename, access } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { randomUUID } from "node:crypto"; +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { parseDocument, Scalar } from "yaml"; + +import { eq, and } from "../drizzle.js"; +import { db } from "../db.js"; +import { markdownBacklogItems } from "../schema.js"; +import { resolveWorkspaceHandle } from "../lib/resolve-workspace.js"; +import { resolveRepoRoot } from "../lib/repo-root.js"; +import { parentRepoPath } from "../../../../packages/database/src/markdown-backlog/paths.ts"; +import { hashFileContents } from "../../../../packages/database/src/markdown-backlog/parse.ts"; +import { toolCatch, toolErr, toolOk } from "./tool-result.js"; + +const slugRe = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; + +const kindSchema = z.enum(["plan", "epic", "task"]); +const statusSchema = z.enum(["draft", "ready", "in_progress", "blocked", "done", "cancelled"]); +const prioritySchema = z.enum(["P0", "P1", "P2", "P3"]); + +const createBacklogItemInputSchema = z + .object({ + workspace: z.string().min(1).describe("Workspace UUID or slug (e.g. 'acme' or '550e8400-...')"), + kind: kindSchema, + slug: z + .string() + .min(1) + .max(200) + .regex(slugRe, "slug must be lowercase-hyphenated (a-z, 0-9, hyphen)"), + title: z.string().min(1).max(500), + planSlug: z + .string() + .min(1) + .max(200) + .regex(slugRe, "planSlug must be lowercase-hyphenated") + .optional(), + epicSlug: z + .string() + .min(1) + .max(200) + .regex(slugRe, "epicSlug must be lowercase-hyphenated") + .optional(), + status: statusSchema.optional(), + priority: prioritySchema.optional(), + bodyMarkdown: z.string().optional(), + agent_prompt: z.string().nullable().optional().describe("Optional per-item agent prompt (multiline supported)"), + }) + .superRefine((input, ctx) => { + if (input.kind === "plan") { + if (input.epicSlug) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "epicSlug must not be provided for kind=plan", + path: ["epicSlug"], + }); + } + if (input.planSlug && input.planSlug !== input.slug) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "planSlug for kind=plan must equal slug or be omitted", + path: ["planSlug"], + }); + } + } + if (input.kind === "epic") { + if (!input.planSlug?.trim()) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "planSlug is required for kind=epic", + path: ["planSlug"], + }); + } + } + if (input.kind === "task") { + if (!input.planSlug?.trim()) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "planSlug is required for kind=task", + path: ["planSlug"], + }); + } + if (!input.epicSlug?.trim()) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "epicSlug is required for kind=task", + path: ["epicSlug"], + }); + } + } + }); + +function todayIso(): string { + return new Date().toISOString().slice(0, 10); +} + +function defaultStatusForKind(kind: z.infer): string { + if (kind === "task") return "ready"; + return "draft"; +} + +function defaultBodyForKind(kind: string, title: string): string { + if (kind === "plan") { + return `# ${title}\n\nShort summary of the initiative and why it exists.\n`; + } + if (kind === "epic") { + return `# ${title}\n\nOne paragraph: what milestone this epic delivers.\n`; + } + return `# ${title}\n\nOne or two sentences.\n`; +} + +function repoPathFor(input: { + kind: z.infer; + slug: string; + planSlug?: string; + epicSlug?: string; +}): string { + if (input.kind === "plan") { + return `plans/Plan-${input.slug}/Plan-${input.slug}.md`; + } + if (input.kind === "epic") { + return `plans/Plan-${input.planSlug}/Epic-${input.slug}/Epic-${input.slug}.md`; + } + return `plans/Plan-${input.planSlug}/Epic-${input.epicSlug}/Task-${input.slug}.md`; +} + +function buildFrontmatterYaml(input: { + kind: string; + slug: string; + title: string; + planSlug: string | null; + epicSlug: string | null; + status: string; + priority: string; + agentPrompt: string | null; +}): string { + const doc = parseDocument(""); + // Ensure we start from empty map + if (!doc.contents) { + // parseDocument("") yields empty; ensure contents is mapping + } + doc.set("kind", input.kind); + doc.set("slug", input.slug); + doc.set("title", input.title); + if (input.planSlug) doc.set("plan_slug", input.planSlug); + if (input.epicSlug) doc.set("epic_slug", input.epicSlug); + doc.set("status", input.status); + doc.set("priority", input.priority); + doc.set("updated_at", todayIso()); + if (input.agentPrompt !== null && input.agentPrompt.trim() !== "") { + const isMultiline = input.agentPrompt.includes("\n"); + const node = doc.createNode(input.agentPrompt); + if (node instanceof Scalar) { + node.type = isMultiline ? Scalar.BLOCK_LITERAL : Scalar.PLAIN; + } + doc.set("agent_prompt", node); + } + const yamlText = String(doc).replace(/\n$/, ""); + return yamlText; +} + +async function fileExists(absPath: string): Promise { + try { + await access(absPath); + return true; + } catch { + return false; + } +} + +async function atomicWrite(absPath: string, contents: string): Promise { + const dir = dirname(absPath); + await mkdir(dir, { recursive: true }); + const tmp = join(dir, `.${randomUUID()}.tmp`); + await writeFile(tmp, contents, "utf8"); + await rename(tmp, absPath); +} + +export function registerCreateBacklogItemTool(mcp: McpServer): void { + mcp.registerTool( + "create-backlog-item", + { + description: + "Create a new Plan/Epic/Task markdown file under plans/ and upsert markdown_backlog_items. Validates frontmatter per docs/templates/. Duplicate slug under same epic → CONFLICT.", + inputSchema: createBacklogItemInputSchema, + }, + async (args) => { + try { + const input = createBacklogItemInputSchema.parse(args); + const ws = await resolveWorkspaceHandle(input.workspace); + + const effectivePlanSlug = + input.kind === "plan" ? input.slug : (input.planSlug?.trim() ?? null); + const effectiveEpicSlug = + input.kind === "task" + ? (input.epicSlug?.trim() ?? null) + : input.kind === "epic" + ? null + : null; + + const status = input.status ?? defaultStatusForKind(input.kind); + const priority = input.priority ?? "P2"; + const agentPrompt = + input.agent_prompt !== undefined && input.agent_prompt !== null + ? input.agent_prompt + : null; + const bodyMarkdown = input.bodyMarkdown ?? defaultBodyForKind(input.kind, input.title); + + const repoPath = repoPathFor({ + kind: input.kind, + slug: input.slug, + planSlug: effectivePlanSlug ?? undefined, + epicSlug: effectiveEpicSlug ?? undefined, + }); + + const repoRoot = resolveRepoRoot(); + if (!repoRoot) { + return toolErr("MARKDOWN_BACKLOG_REPO_ROOT is disabled; cannot write plans/ file"); + } + const absPath = join(repoRoot, ...repoPath.split("/")); + + // Duplicate check: file exists + if (await fileExists(absPath)) { + return toolErr(`CONFLICT: backlog item already exists at ${repoPath}`); + } + + // Duplicate check: DB row exists for same workspace+repoPath + const [existingRow] = await db + .select({ id: markdownBacklogItems.id }) + .from(markdownBacklogItems) + .where(and(eq(markdownBacklogItems.workspaceId, ws.id), eq(markdownBacklogItems.repoPath, repoPath))) + .limit(1); + if (existingRow) { + return toolErr(`CONFLICT: backlog item already exists at ${repoPath}`); + } + + // Build frontmatter + raw markdown (exportBacklogItemToMarkdown inverse) + // Plan files per template do not carry plan_slug/epic_slug + const yamlPlanSlug = input.kind === "plan" ? null : effectivePlanSlug; + const yamlEpicSlug = input.kind === "task" ? effectiveEpicSlug : null; + const yamlText = buildFrontmatterYaml({ + kind: input.kind, + slug: input.slug, + title: input.title, + planSlug: yamlPlanSlug, + epicSlug: yamlEpicSlug, + status, + priority, + agentPrompt, + }); + + const body = bodyMarkdown.startsWith("\n") ? bodyMarkdown : `\n${bodyMarkdown}`; + // Ensure body ends with newline + const normalizedBody = body.endsWith("\n") ? body : `${body}\n`; + const raw = `---\n${yamlText}\n---${normalizedBody}`; + + const contentHash = hashFileContents(raw); + + // Frontmatter stored in DB (mirrors sync's parsed frontmatter) + const frontmatter: Record = { + kind: input.kind, + slug: input.slug, + title: input.title, + status, + priority, + updated_at: todayIso(), + }; + if (yamlPlanSlug) frontmatter.plan_slug = yamlPlanSlug; + if (yamlEpicSlug) frontmatter.epic_slug = yamlEpicSlug; + if (agentPrompt !== null && agentPrompt.trim() !== "") { + frontmatter.agent_prompt = agentPrompt; + } + + // Atomic write .tmp → rename + await atomicWrite(absPath, raw); + + // Upsert markdown_backlog_items with contentHash + // Compute parentId by looking up parent repo path if any + const parentPath = parentRepoPath(repoPath); + let parentId: string | null = null; + if (parentPath) { + const [parentRow] = await db + .select({ id: markdownBacklogItems.id }) + .from(markdownBacklogItems) + .where(and(eq(markdownBacklogItems.workspaceId, ws.id), eq(markdownBacklogItems.repoPath, parentPath))) + .limit(1); + parentId = parentRow?.id ?? null; + } + + let insertedId: string | null = null; + try { + const [inserted] = await db + .insert(markdownBacklogItems) + .values({ + workspaceId: ws.id, + kind: input.kind, + slug: input.slug, + planSlug: effectivePlanSlug ?? input.slug, + epicSlug: effectiveEpicSlug, + parentId, + repoPath, + title: input.title, + status, + priority, + owner: null, + frontmatter, + bodyMarkdown: normalizedBody.replace(/^\n/, ""), + contentHash, + workflowPrompt: agentPrompt?.trim() ? agentPrompt.trim() : null, + }) + .returning({ id: markdownBacklogItems.id }); + insertedId = inserted?.id ?? null; + } catch (e) { + // Unique violation → CONFLICT (race with concurrent writer) + const msg = e instanceof Error ? e.message : String(e); + if (msg.includes("duplicate") || msg.includes("unique") || msg.includes("CONFLICT")) { + return toolErr(`CONFLICT: backlog item already exists at ${repoPath}`); + } + throw e; + } + + // If we had a parent but it was inserted concurrently after our lookup, + // refresh parentId (best-effort) + if (parentPath && !parentId) { + const [parentRow] = await db + .select({ id: markdownBacklogItems.id }) + .from(markdownBacklogItems) + .where(and(eq(markdownBacklogItems.workspaceId, ws.id), eq(markdownBacklogItems.repoPath, parentPath))) + .limit(1); + if (parentRow) { + await db + .update(markdownBacklogItems) + .set({ parentId: parentRow.id }) + .where(eq(markdownBacklogItems.id, insertedId!)); + parentId = parentRow.id; + } + } + + return toolOk({ + workspace: { id: ws.id, slug: ws.slug, name: ws.name }, + repoPath, + absPath, + contentHash, + backlogItem: { + id: insertedId, + kind: input.kind, + slug: input.slug, + planSlug: effectivePlanSlug, + epicSlug: effectiveEpicSlug, + title: input.title, + status, + priority, + parentId, + }, + }); + } catch (e) { + return toolCatch(e); + } + }, + ); +} diff --git a/apps/mcp-server/src/tools/find-backlog-item.ts b/apps/mcp-server/src/tools/find-backlog-item.ts new file mode 100644 index 0000000..0a38bc4 --- /dev/null +++ b/apps/mcp-server/src/tools/find-backlog-item.ts @@ -0,0 +1,70 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +import { + BACKLOG_ITEM_NOT_FOUND_MESSAGE, + backlogItemLookupFieldsSchema, + resolveBacklogItemsByLookup, +} from "../lib/resolve-backlog-item.js"; +import { resolveWorkspaceHandle } from "../lib/resolve-workspace.js"; +import { toolCatch, toolErr, toolOk } from "./tool-result.js"; + +const findBacklogItemInputSchema = z + .object({ + workspace: z + .string() + .min(1) + .describe("Workspace UUID or slug (e.g. 'convoys-tasks')."), + }) + .merge(backlogItemLookupFieldsSchema) + .superRefine((input, ctx) => { + const hasRepoPath = Boolean(input.repoPath?.trim()); + const hasSlugKeys = Boolean(input.slug?.trim() && input.planSlug?.trim()); + if (!hasRepoPath && !hasSlugKeys) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Provide repoPath, or slug + planSlug (add epicSlug for tasks/epics).", + }); + } + }); + +export function registerFindBacklogItemTool(mcp: McpServer): void { + mcp.registerTool( + "find_backlog_item", + { + description: + "Resolve a markdown backlog item to its backlogItemId by repo path or slug keys. Optional when claim_task is called with repoPath directly.", + inputSchema: findBacklogItemInputSchema, + }, + async (args) => { + try { + const input = findBacklogItemInputSchema.parse(args); + const ws = await resolveWorkspaceHandle(input.workspace); + const rows = await resolveBacklogItemsByLookup(ws.id, input); + + if (rows.length === 0) { + return toolErr(BACKLOG_ITEM_NOT_FOUND_MESSAGE); + } + + if (rows.length > 1) { + return toolOk({ + workspace: { id: ws.id, slug: ws.slug, name: ws.name }, + ambiguous: true, + matches: rows, + count: rows.length, + }); + } + + const item = rows[0]!; + return toolOk({ + workspace: { id: ws.id, slug: ws.slug, name: ws.name }, + backlogItem: item, + backlogItemId: item.id, + }); + } catch (e) { + return toolCatch(e); + } + }, + ); +} diff --git a/apps/mcp-server/src/tools/index.ts b/apps/mcp-server/src/tools/index.ts index ce29082..3056526 100644 --- a/apps/mcp-server/src/tools/index.ts +++ b/apps/mcp-server/src/tools/index.ts @@ -6,6 +6,8 @@ import { registerSearchObjectsTool } from "./search-objects.js"; import { registerUpdateObjectTool } from "./update-object.js"; import { registerClaimTaskTool } from "./claim-task.js"; import { registerCompleteTaskTool } from "./complete-task.js"; +import { registerCreateBacklogItemTool } from "./create-backlog-item.js"; +import { registerFindBacklogItemTool } from "./find-backlog-item.js"; import { registerCreateConvoyTool } from "./create-convoy.js"; import { registerCreateBriefTool } from "./create-brief.js"; import { registerTransitionConvoyStatusTool } from "./transition-convoy-status.js"; @@ -21,6 +23,8 @@ export function registerTools(mcp: McpServer): void { registerManageObjectTool(mcp); registerClaimTaskTool(mcp); registerCompleteTaskTool(mcp); + registerCreateBacklogItemTool(mcp); + registerFindBacklogItemTool(mcp); registerCreateConvoyTool(mcp); registerCreateBriefTool(mcp); registerTransitionConvoyStatusTool(mcp); @@ -36,6 +40,8 @@ export { registerListObjectsTool } from "./list-objects.js"; export { registerManageObjectTool } from "./manage-object.js"; export { registerClaimTaskTool } from "./claim-task.js"; export { registerCompleteTaskTool } from "./complete-task.js"; +export { registerCreateBacklogItemTool } from "./create-backlog-item.js"; +export { registerFindBacklogItemTool } from "./find-backlog-item.js"; export { registerCreateConvoyTool } from "./create-convoy.js"; export { registerCreateBriefTool } from "./create-brief.js"; export { registerTransitionConvoyStatusTool } from "./transition-convoy-status.js"; diff --git a/apps/orchestrator/src/db/convoy.ts b/apps/orchestrator/src/db/convoy.ts index 8d5c2ea..d471997 100644 --- a/apps/orchestrator/src/db/convoy.ts +++ b/apps/orchestrator/src/db/convoy.ts @@ -1,7 +1,7 @@ import { and, desc, eq, inArray, isNull, lt, sql } from "drizzle-orm"; import type { db as dbInstance } from "@tasks/database"; -import { agentRuns, convoyEvents, objects } from "@tasks/database/schema"; +import { agentRuns, convoyEvents, objects, workerCheckpoints } from "@tasks/database/schema"; import { briefObjectContentSchema, convoyObjectContentSchema, @@ -145,10 +145,28 @@ export async function transitionConvoyInDb( }); } -export async function detectStalledConvoys(db: Db, stallMinutes: number): Promise { +export type StalledRunDispatch = { + workspaceId: string; + agentRunId: string; + backlogItemId: string; + convoyId: string; + roleId: string; + briefId?: string; +}; + +/** + * Open agent runs past the stall threshold that have a workspace-scoped + * checkpoint (enough context to re-dispatch with resume_from_checkpoint). + */ +export async function detectStalledRunsForResume( + db: Db, + stallMinutes: number, +): Promise { const cutoff = new Date(Date.now() - stallMinutes * 60_000); const openRuns = await db .select({ + id: agentRuns.id, + workspaceId: agentRuns.workspaceId, backlogItemId: agentRuns.backlogItemId, startedAt: agentRuns.startedAt, }) @@ -158,5 +176,41 @@ export async function detectStalledConvoys(db: Db, stallMinutes: number): Promis if (openRuns.length === 0) return []; - return openRuns.map((run) => run.backlogItemId); + const dispatches: StalledRunDispatch[] = []; + for (const run of openRuns) { + const [checkpoint] = await db + .select({ + convoyId: workerCheckpoints.convoyId, + briefId: workerCheckpoints.briefId, + roleId: workerCheckpoints.roleId, + }) + .from(workerCheckpoints) + .where( + and( + eq(workerCheckpoints.workspaceId, run.workspaceId), + eq(workerCheckpoints.agentRunId, run.id), + ), + ) + .orderBy(desc(workerCheckpoints.checkpointSeq)) + .limit(1); + + if (!checkpoint) continue; + + dispatches.push({ + workspaceId: run.workspaceId, + agentRunId: run.id, + backlogItemId: run.backlogItemId, + convoyId: checkpoint.convoyId, + roleId: checkpoint.roleId, + briefId: checkpoint.briefId ?? undefined, + }); + } + + return dispatches; +} + +/** @deprecated Prefer detectStalledRunsForResume — kept for log-only callers. */ +export async function detectStalledConvoys(db: Db, stallMinutes: number): Promise { + const stalled = await detectStalledRunsForResume(db, stallMinutes); + return stalled.map((run) => run.backlogItemId); } diff --git a/apps/orchestrator/src/index.ts b/apps/orchestrator/src/index.ts index 9b3d371..6dafdec 100644 --- a/apps/orchestrator/src/index.ts +++ b/apps/orchestrator/src/index.ts @@ -1,8 +1,9 @@ import { db } from "@tasks/database"; import IoRedis from "ioredis"; -import { detectStalledConvoys } from "./db/convoy.js"; +import { detectStalledRunsForResume } from "./db/convoy.js"; import { loadEnv } from "./env.js"; +import { enqueueJob } from "./queue/redis-queue.js"; import { runOrchestratorTick } from "./tick.js"; async function main(): Promise { @@ -24,11 +25,32 @@ async function main(): Promise { ORCHESTRATOR_DRY_RUN: env.ORCHESTRATOR_DRY_RUN || !redis, }); - const stalled = await detectStalledConvoys(db, env.ORCHESTRATOR_STALL_MINUTES); + const stalled = await detectStalledRunsForResume(db, env.ORCHESTRATOR_STALL_MINUTES); if (stalled.length > 0) { console.warn( `[orchestrator] stall detector: ${stalled.length} open agent_runs older than ${env.ORCHESTRATOR_STALL_MINUTES}m`, ); + + for (const run of stalled) { + if (env.ORCHESTRATOR_DRY_RUN || !redis) { + console.info( + `[orchestrator] DRY-RUN resume_from_checkpoint convoy=${run.convoyId} role=${run.roleId} run=${run.agentRunId}`, + ); + continue; + } + + const job = await enqueueJob(redis, { + type: "role_dispatch", + workspaceId: run.workspaceId, + convoyId: run.convoyId, + roleId: run.roleId, + briefId: run.briefId, + resumeFromCheckpoint: true, + }); + console.info( + `[orchestrator] re-dispatched job=${job.id} resume_from_checkpoint run=${run.agentRunId}`, + ); + } } if (results.length === 0) { diff --git a/apps/web/app/(app)/[workspaceSlug]/plans/page.tsx b/apps/web/app/(app)/[workspaceSlug]/plans/page.tsx new file mode 100644 index 0000000..d566b2b --- /dev/null +++ b/apps/web/app/(app)/[workspaceSlug]/plans/page.tsx @@ -0,0 +1,655 @@ +"use client"; + +import * as React from "react"; +import { useParams, useRouter } from "next/navigation"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; +import { ChevronDown, ChevronRight, FileText, Folder, FolderKanban, Loader2, Plus, Search, Rocket, X } from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from "@/components/ui/sheet"; +import { api } from "@/lib/trpc"; +import { cn } from "@/lib/utils"; + +const STATUS_VARIANT: Record = { + draft: "bg-muted text-muted-foreground", + ready: "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border-emerald-500/20", + in_progress: "bg-amber-500/15 text-amber-600 dark:text-amber-400 border-amber-500/20", + blocked: "bg-destructive/10 text-destructive border-destructive/20", + done: "bg-sky-500/15 text-sky-600 dark:text-sky-400 border-sky-500/20", + cancelled: "bg-muted text-muted-foreground", +}; + +function StatusChip({ status }: { status: string | null }) { + const s = status ?? "unknown"; + const cls = STATUS_VARIANT[s] ?? STATUS_VARIANT.draft; + return ( + {s} + ); +} + +function slugify(value: string): string { + return value + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .replace(/--+/g, "-"); +} + +type DialogState = + | { kind: "plan" } + | { kind: "epic"; planSlug: string } + | { kind: "task"; planSlug: string; epicSlug: string } + | null; + +export default function PlansPage() { + const params = useParams(); + const router = useRouter(); + const workspaceSlug = params?.workspaceSlug as string | undefined; + + const [search, setSearch] = React.useState(""); + const [expandedPlans, setExpandedPlans] = React.useState>(new Set()); + const [expandedEpics, setExpandedEpics] = React.useState>(new Set()); + const [selectedId, setSelectedId] = React.useState(null); + const [dialog, setDialog] = React.useState(null); + const [formSlug, setFormSlug] = React.useState(""); + const [formTitle, setFormTitle] = React.useState(""); + const [formBody, setFormBody] = React.useState(""); + const [formAgentPrompt, setFormAgentPrompt] = React.useState(""); + const [formError, setFormError] = React.useState(null); + + const treeQuery = api.backlog.listTree.useQuery({ workspace: workspaceSlug! }, { enabled: Boolean(workspaceSlug) }); + const detailQuery = api.backlog.getBacklogItem.useQuery( + { workspace: workspaceSlug!, backlogItemId: selectedId! }, + { enabled: Boolean(workspaceSlug && selectedId) }, + ); + const promptQuery = api.backlog.getWorkflowPrompt.useQuery( + { workspace: workspaceSlug!, backlogItemId: selectedId! }, + { enabled: Boolean(workspaceSlug && selectedId) }, + ); + + const utils = api.useUtils(); + + const createPlan = api.backlog.createPlan.useMutation({ + onSuccess: () => { + void utils.backlog.listTree.invalidate(); + closeDialog(); + }, + onError: (e) => setFormError(e.message), + }); + const createEpic = api.backlog.createEpic.useMutation({ + onSuccess: () => { + void utils.backlog.listTree.invalidate(); + closeDialog(); + }, + onError: (e) => setFormError(e.message), + }); + const createTask = api.backlog.createTask.useMutation({ + onSuccess: () => { + void utils.backlog.listTree.invalidate(); + closeDialog(); + }, + onError: (e) => setFormError(e.message), + }); + const startConvoy = api.backlog.startConvoy.useMutation({ + onSuccess: () => { + void utils.convoys.kanban.invalidate(); + if (workspaceSlug) router.push(`/${workspaceSlug}/mission-control`); + }, + onError: (e) => setFormError(e.message), + }); + // Direct convoy.create path required by brief 4 — Start → Convoy via convoys.create with planSlug/epicSlug + const createConvoyDirect = api.convoys.create.useMutation({ + onSuccess: () => { + void utils.convoys.kanban.invalidate(); + if (workspaceSlug) router.push(`/${workspaceSlug}/mission-control`); + }, + onError: (e) => setFormError(e.message), + }); + + function closeDialog() { + setDialog(null); + setFormSlug(""); + setFormTitle(""); + setFormBody(""); + setFormAgentPrompt(""); + setFormError(null); + } + + function openPlanDialog() { + setFormError(null); + setFormSlug(""); + setFormTitle(""); + setFormBody(""); + setFormAgentPrompt(""); + setDialog({ kind: "plan" }); + } + function openEpicDialog(planSlug: string) { + setFormError(null); + setFormSlug(""); + setFormTitle(""); + setFormBody(""); + setFormAgentPrompt(""); + setDialog({ kind: "epic", planSlug }); + } + function openTaskDialog(planSlug: string, epicSlug: string) { + setFormError(null); + setFormSlug(""); + setFormTitle(""); + setFormBody(""); + setFormAgentPrompt(""); + setDialog({ kind: "task", planSlug, epicSlug }); + } + + const tree = React.useMemo(() => treeQuery.data?.plans ?? [], [treeQuery.data?.plans]); + + const filtered = React.useMemo(() => { + if (!search.trim()) return tree; + const q = search.trim().toLowerCase(); + return tree + .map((plan) => { + const planMatches = plan.title.toLowerCase().includes(q) || plan.slug.toLowerCase().includes(q); + const epics = plan.epics + .map((epic) => { + const epicMatches = epic.title.toLowerCase().includes(q) || epic.slug.toLowerCase().includes(q); + const tasks = epic.tasks.filter( + (t) => + t.title.toLowerCase().includes(q) || + t.slug.toLowerCase().includes(q) || + (t.status ?? "").toLowerCase().includes(q), + ); + if (epicMatches || tasks.length > 0 || planMatches) { + return { ...epic, tasks: epicMatches && tasks.length === 0 ? epic.tasks : tasks }; + } + return null; + }) + .filter(Boolean) as typeof plan.epics; + if (planMatches) return { ...plan, epics: epics.length ? epics : plan.epics }; + if (epics.length) return { ...plan, epics }; + return null; + }) + .filter(Boolean) as typeof tree; + }, [tree, search]); + + React.useEffect(() => { + if (tree.length && expandedPlans.size === 0) { + setExpandedPlans(new Set(tree.slice(0, 2).map((p) => p.slug))); + } + }, [tree, expandedPlans.size]); + + if (!workspaceSlug) return null; + + const isMutating = createPlan.isPending || createEpic.isPending || createTask.isPending; + + return ( +
+
+
+
+ +
+
+

Plans

+

Plan → Epic → Task tree from plans/ markdown

+
+
+
+
+ + setSearch(e.target.value)} className="w-64 pl-8" /> +
+ +
+
+ +
+
+ {treeQuery.isLoading ? ( +
+ + Loading plans… +
+ ) : filtered.length === 0 ? ( +
+ +

No plans yet

+

+ {search.trim() + ? "No results for your search." + : "Create your first plan or run pnpm --filter @tasks/database watch:markdown-backlog to import plans/ after seeding."} +

+ {!search.trim() && ( + + )} +
+ ) : ( + +
+ {filtered.map((plan) => { + const planOpen = expandedPlans.has(plan.slug); + return ( + + setExpandedPlans((prev) => { + const next = new Set(prev); + if (open) next.add(plan.slug); + else next.delete(plan.slug); + return next; + }) + } + > +
+ + + + + + + {plan.slug} + +
+ +
+ {plan.epics.length === 0 ? ( +

No epics — add one.

+ ) : ( + plan.epics.map((epic) => { + const epicKey = `${plan.slug}/${epic.slug}`; + const epicOpen = expandedEpics.has(epicKey); + return ( + + setExpandedEpics((prev) => { + const next = new Set(prev); + if (open) next.add(epicKey); + else next.delete(epicKey); + return next; + }) + } + > +
+ + + + + + + {epic.slug} + +
+ +
    + {epic.tasks.length === 0 ? ( +
  • No tasks.
  • + ) : ( + epic.tasks.map((task) => ( +
  • + + + + {task.priority && ( + + {task.priority} + + )} + {task.repoPath} + +
  • + )) + )} +
+
+
+ ); + }) + )} +
+
+
+ ); + })} +
+
+ )} +
+ + !open && setSelectedId(null)}> + + + {detailQuery.data?.title ?? "Details"} + + {detailQuery.data?.repoPath ?? ""} + {detailQuery.data ? ` · ${detailQuery.data.kind}` : ""} + + + +
+ {detailQuery.isLoading ? ( +
+ + Loading… +
+ ) : detailQuery.data ? ( + <> +
+ + {detailQuery.data.priority && {detailQuery.data.priority}} + {detailQuery.data.kind} +
+
+

Repo path

+

{detailQuery.data.repoPath}

+

Slugs

+

+ Plan: {detailQuery.data.planSlug} + {detailQuery.data.epicSlug ? ( + <> + {" "} + Epic: {detailQuery.data.epicSlug} + + ) : null}{" "} + Slug: {detailQuery.data.slug} +

+
+
+

Body

+
+                        {detailQuery.data.bodyMarkdown || "(empty)"}
+                      
+
+
+

Effective prompt

+

Source: {promptQuery.data?.source ?? "—"}

+
+                        {promptQuery.data?.effectivePrompt ?? promptQuery.data?.ownOverride ?? "(no prompt)"}
+                      
+ {promptQuery.data?.ownOverride &&

Own override present

} +
+ {detailQuery.data.kind === "task" && ( +
+ + +
+ )} + {(startConvoy.error || createConvoyDirect.error) && ( +

{(startConvoy.error ?? createConvoyDirect.error)?.message}

+ )} + {(startConvoy.isSuccess || createConvoyDirect.isSuccess) && ( +

+ Convoy {(startConvoy.data?.convoySlug ?? createConvoyDirect.data?.convoyId) ?? ""} created — check Mission Control. +

+ )} + + ) : ( +

Select an item to see details.

+ )} +
+
+
+
+
+ + !open && closeDialog()}> + + + +
+ + {dialog?.kind === "plan" ? "New Plan" : dialog?.kind === "epic" ? "New Epic" : dialog?.kind === "task" ? "New Task" : "New"} + + + + +
+ +
+ {dialog?.kind === "epic" && ( +
+ +
{dialog.planSlug}
+
+ )} + {dialog?.kind === "task" && ( +
+
+ +
{dialog.planSlug}
+
+
+ +
{dialog.epicSlug}
+
+
+ )} + +
+ + { + const v = e.target.value.toLowerCase(); + setFormSlug(v); + if (!formTitle.trim() || slugify(formTitle) === formSlug) { + // keep title in sync helper? no auto + } + }} + /> +

Must match ^[a-z0-9-]+$

+
+
+ + setFormTitle(e.target.value)} + onBlur={() => { + if (!formSlug.trim() && formTitle.trim()) setFormSlug(slugify(formTitle)); + }} + /> +
+
+ +