diff --git a/Cargo.lock b/Cargo.lock index eeb5f38..0061694 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,53 +20,37 @@ dependencies = [ "libc", ] -[[package]] -name = "animus-log-storage-protocol" -version = "0.1.13" -source = "git+https://github.com/launchapp-dev/animus-protocol?tag=v0.1.13#1dc37a0db952e3805a28a904b9f6120837584752" -dependencies = [ - "animus-plugin-protocol", - "anyhow", - "async-trait", - "chrono", - "futures-core", - "serde", - "serde_json", - "thiserror", -] - [[package]] name = "animus-plugin-protocol" -version = "0.1.13" -source = "git+https://github.com/launchapp-dev/animus-protocol?tag=v0.1.13#1dc37a0db952e3805a28a904b9f6120837584752" +version = "0.1.14" +source = "git+https://github.com/launchapp-dev/animus-protocol?tag=v0.5.12#40f52f3bd20ab28dc3218b186c2d8b208d4b2f27" dependencies = [ + "schemars", "serde", "serde_json", ] [[package]] name = "animus-plugin-runtime" -version = "0.1.13" -source = "git+https://github.com/launchapp-dev/animus-protocol?tag=v0.1.13#1dc37a0db952e3805a28a904b9f6120837584752" +version = "0.2.1" +source = "git+https://github.com/launchapp-dev/animus-protocol?tag=v0.5.12#40f52f3bd20ab28dc3218b186c2d8b208d4b2f27" dependencies = [ - "animus-log-storage-protocol", "animus-plugin-protocol", - "animus-provider-protocol", + "animus-session-backend", "animus-subject-protocol", "animus-transport-protocol", - "animus-trigger-protocol", "anyhow", "async-trait", - "futures-core", "serde", "serde_json", "tokio", + "tracing", ] [[package]] -name = "animus-provider-protocol" -version = "0.1.13" -source = "git+https://github.com/launchapp-dev/animus-protocol?tag=v0.1.13#1dc37a0db952e3805a28a904b9f6120837584752" +name = "animus-session-backend" +version = "0.1.15" +source = "git+https://github.com/launchapp-dev/animus-protocol?tag=v0.5.12#40f52f3bd20ab28dc3218b186c2d8b208d4b2f27" dependencies = [ "animus-plugin-protocol", "anyhow", @@ -74,18 +58,22 @@ dependencies = [ "serde", "serde_json", "thiserror", + "tokio", + "tracing", + "uuid", ] [[package]] name = "animus-subject-protocol" -version = "0.1.13" -source = "git+https://github.com/launchapp-dev/animus-protocol?tag=v0.1.13#1dc37a0db952e3805a28a904b9f6120837584752" +version = "0.1.15" +source = "git+https://github.com/launchapp-dev/animus-protocol?tag=v0.5.12#40f52f3bd20ab28dc3218b186c2d8b208d4b2f27" dependencies = [ "animus-plugin-protocol", "anyhow", "async-trait", "chrono", "futures-core", + "schemars", "serde", "serde_json", "thiserror", @@ -93,28 +81,14 @@ dependencies = [ [[package]] name = "animus-transport-protocol" -version = "0.1.13" -source = "git+https://github.com/launchapp-dev/animus-protocol?tag=v0.1.13#1dc37a0db952e3805a28a904b9f6120837584752" +version = "0.1.15" +source = "git+https://github.com/launchapp-dev/animus-protocol?tag=v0.5.12#40f52f3bd20ab28dc3218b186c2d8b208d4b2f27" dependencies = [ "animus-plugin-protocol", "anyhow", "async-trait", "chrono", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "animus-trigger-protocol" -version = "0.1.13" -source = "git+https://github.com/launchapp-dev/animus-protocol?tag=v0.1.13#1dc37a0db952e3805a28a904b9f6120837584752" -dependencies = [ - "animus-plugin-protocol", - "anyhow", - "async-trait", - "chrono", - "futures-core", + "schemars", "serde", "serde_json", "thiserror", @@ -280,6 +254,18 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -296,6 +282,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -338,6 +330,40 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "http" version = "1.4.0" @@ -442,6 +468,12 @@ dependencies = [ "cc", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "include_dir" version = "0.7.4" @@ -461,6 +493,18 @@ dependencies = [ "quote", ] +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + [[package]] name = "itoa" version = "1.0.18" @@ -485,6 +529,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.186" @@ -613,6 +663,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -631,6 +691,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -640,6 +706,26 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -669,12 +755,44 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "chrono", + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -705,6 +823,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_json" version = "1.0.150" @@ -964,6 +1093,24 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "getrandom", + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -976,6 +1123,24 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + [[package]] name = "wasm-bindgen" version = "0.2.122" @@ -1021,6 +1186,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -1089,6 +1288,100 @@ dependencies = [ "windows-link", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/src/app/agent-page.tsx b/src/app/agent-page.tsx index aa59a5f..db8f0d8 100644 --- a/src/app/agent-page.tsx +++ b/src/app/agent-page.tsx @@ -3,11 +3,15 @@ import { Link } from "react-router-dom"; import { useQuery } from "@/lib/graphql/client"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; import { DaemonDocument } from "@/lib/graphql/generated/graphql"; +import type { DaemonQuery } from "@/lib/graphql/generated/graphql"; import { StatusDot, PageLoading, PageError, SectionHeading } from "./shared"; -const PHASE_OUTPUT_QUERY = `query PhaseOutput($workflowId: ID!, $phaseId: String, $tail: Int) { phaseOutput(workflowId: $workflowId, phaseId: $phaseId, tail: $tail) { lines phaseId hasMore } }`; +// NOTE(E-followup): the kernel schema exposes no per-agent phase-output stream +// (only `daemonAgents` session metadata + the `workflowEvents`/`daemonEvents` +// subscriptions), so agent cards show session metadata only. A richer per-agent +// output view requires a transport-graphql schema extension. +type AgentInfo = NonNullable[number]; function useElapsedTime(startedAt: string | null | undefined): string { const [, setTick] = useState(0); @@ -22,7 +26,7 @@ function useElapsedTime(startedAt: string | null | undefined): string { } function formatDuration(ms: number): string { - if (ms < 0) return "0s"; + if (ms < 0 || Number.isNaN(ms)) return "0s"; const s = Math.floor(ms / 1000); if (s < 60) return `${s}s`; const m = Math.floor(s / 60); @@ -31,70 +35,25 @@ function formatDuration(ms: number): string { return `${h}h ${m % 60}m`; } -function AgentOutputPreview({ workflowId, phaseId }: { workflowId: string; phaseId: string | null | undefined }) { - const [result] = useQuery<{ phaseOutput: { lines: string[]; phaseId: string; hasMore: boolean } }>({ - query: PHASE_OUTPUT_QUERY, - variables: { workflowId, phaseId: phaseId ?? undefined, tail: 10 }, - pause: !workflowId, - }); - - const lines = result.data?.phaseOutput?.lines ?? []; - - if (result.fetching && lines.length === 0) { - return ( -
-

Loading output...

-
- ); - } - - if (lines.length === 0) { - return ( -
-

No output yet

-
- ); - } - - return ( -
-
- {lines.map((line, i) => ( -
- {line} -
- ))} - -
-
- ); -} - -function AgentCard({ agent }: { agent: { runId: string; taskId?: string | null; taskTitle?: string | null; workflowId?: string | null; phaseId?: string | null; status: string } }) { - const elapsed = useElapsedTime(new Date().toISOString()); +function AgentCard({ agent }: { agent: AgentInfo }) { + const elapsed = useElapsedTime(agent.startedAt); return (
- - agent-{agent.runId} + + {agent.sessionId}
- {agent.status} + {agent.provider}
- Task: - {agent.taskId ? ( - - {agent.taskId} {agent.taskTitle ? `"${agent.taskTitle}"` : ""} - - ) : ( - - - )} + Model: + {agent.model}
Workflow: @@ -115,43 +74,20 @@ function AgentCard({ agent }: { agent: { runId: string; taskId?: string | null; {elapsed || "-"}
- - {agent.workflowId && ( -
- Output (last 10 lines) - -
- )} - -
- {agent.workflowId && ( - <> - - - - )} -
); } export function AgentManagementPage() { - const [result] = useQuery({ query: DaemonDocument }); + const [result] = useQuery({ query: DaemonDocument }); const { data, fetching, error } = result; if (fetching) return ; if (error) return ; - const status = data?.daemonStatus; const health = data?.daemonHealth; - const agents = data?.agentRuns ?? []; - const activeCount = agents.filter((a: { status: string }) => a.status.toLowerCase() === "running").length; - const maxAgents = status?.maxAgents ?? health?.activeAgents ?? 0; + const agents = data?.daemonAgents ?? []; const overallHealth = agents.length > 0 ? "running" : health?.healthy ? "healthy" : "error"; @@ -160,7 +96,7 @@ export function AgentManagementPage() {

Agents

- {activeCount} active / {maxAgents} capacity + {agents.length} active
@@ -185,24 +121,24 @@ export function AgentManagementPage() { Active Agents
{agents.map((a) => ( - + ))}
)}
- System Capacity + Daemon
- Max Agents - {status?.maxAgents ?? "-"} + Status + {health?.status ?? "-"}
- Runner Connected + Healthy - {health?.runnerConnected ? ( + {health?.healthy ? ( yes ) : ( "no" @@ -210,12 +146,12 @@ export function AgentManagementPage() {
- Runner PID - {health?.runnerPid ?? "-"} + Running + {data?.daemon?.running ? "yes" : "no"}
- Daemon PID - {health?.daemonPid ?? "-"} + PID + {data?.daemon?.pid ?? "-"}
diff --git a/src/app/builder-pages.tsx b/src/app/builder-pages.tsx deleted file mode 100644 index e5b1792..0000000 --- a/src/app/builder-pages.tsx +++ /dev/null @@ -1,2343 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom"; -import { useQuery, useMutation } from "@/lib/graphql/client"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; -import { Separator } from "@/components/ui/separator"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { WorkflowDefinitionsDocument, WorkflowConfigDocument } from "@/lib/graphql/generated/graphql"; -import { PageLoading, PageError } from "./shared"; -import { - Plus, - ChevronRight, - ChevronLeft, - ArrowLeft, - X, - Pencil, - Copy, - Trash2, - Eye, - CheckCircle2, - AlertCircle, - Save, - Layers, - PaintBucket, - FileText, - Settings, - Server, - Clock, - Users, - Terminal, - Hand, - Bot, - ChevronDown, - ChevronUp, -} from "lucide-react"; - -interface PhaseEntry { - id: string; - maxReworkAttempts: number; - skipIf: string[]; - onVerdict: { - advance: { target: string | null }; - rework: { target: string | null; allowAgentTarget: boolean }; - fail: { target: string | null }; - }; -} - -interface VariableEntry { - name: string; - description: string; - required: boolean; - default: string; -} - -interface PostSuccessConfig { - mergeStrategy: string; - targetBranch: string; - createPr: boolean; - autoMerge: boolean; - cleanupWorktree: boolean; -} - -interface WorkflowDef { - id: string; - name: string; - description: string; - phases: PhaseEntry[]; - variables: VariableEntry[]; - postSuccess: PostSuccessConfig; -} - -interface AgentProfileEntry { - name: string; - description: string; - role: string; - systemPrompt: string; - model: string; - tool: string; - mcpServers: string[]; - skills: string[]; -} - -interface ContractField { - name: string; - type: "string" | "array" | "integer" | "boolean"; - description: string; - itemsType: string; - enumValues: string[]; -} - -interface PhaseDefinitionEntry { - id: string; - mode: "agent" | "command" | "manual"; - agent: string; - directive: string; - decisionContract: { - minConfidence: number; - maxRisk: string; - allowMissingDecision: boolean; - requiredEvidence: string[]; - fields: ContractField[]; - }; - outputContract: { - kind: string; - requiredFields: string[]; - fields: ContractField[]; - }; - command: { - program: string; - args: string[]; - cwdMode: string; - timeoutSecs: number; - }; - manualInstructions: string; - approvalNoteRequired: boolean; -} - -interface McpServerEntry { - name: string; - command: string; - args: string[]; - transport: string; - tools: string[]; - env: Array<{ key: string; value: string }>; -} - -interface ScheduleEntry { - id: string; - cron: string; - workflowRef: string; - enabled: boolean; -} - -function makePhaseEntry(id: string): PhaseEntry { - return { - id, - maxReworkAttempts: 3, - skipIf: [], - onVerdict: { - advance: { target: null }, - rework: { target: null, allowAgentTarget: false }, - fail: { target: null }, - }, - }; -} - -function makeVariableEntry(): VariableEntry { - return { name: "", description: "", required: false, default: "" }; -} - -function makePostSuccess(): PostSuccessConfig { - return { mergeStrategy: "squash", targetBranch: "main", createPr: true, autoMerge: false, cleanupWorktree: true }; -} - -function makeAgentProfile(): AgentProfileEntry { - return { name: "", description: "", role: "", systemPrompt: "", model: "", tool: "", mcpServers: [], skills: [] }; -} - -function makePhaseDefinition(id?: string): PhaseDefinitionEntry { - return { - id: id ?? "", - mode: "agent", - agent: "", - directive: "", - decisionContract: { minConfidence: 0.7, maxRisk: "medium", allowMissingDecision: false, requiredEvidence: [], fields: [] }, - outputContract: { kind: "", requiredFields: [], fields: [] }, - command: { program: "", args: [], cwdMode: "project_root", timeoutSecs: 300 }, - manualInstructions: "", - approvalNoteRequired: false, - }; -} - -function makeMcpServer(): McpServerEntry { - return { name: "", command: "", args: [], transport: "stdio", tools: [], env: [] }; -} - -function makeSchedule(): ScheduleEntry { - return { id: "", cron: "", workflowRef: "", enabled: true }; -} - -const TEMPLATES: Record = { - standard: { - name: "Standard", - description: "A typical development workflow with requirements analysis, implementation, code review, and testing phases.", - phases: ["requirements", "implementation", "code-review", "testing"], - }, - "ui-ux": { - name: "UI/UX", - description: "Extended workflow for user interface work including research, wireframing, and mockup review before implementation.", - phases: ["requirements", "ux-research", "wireframe", "mockup-review", "implementation", "code-review", "testing"], - }, - blank: { - name: "Blank", - description: "Start from scratch with an empty workflow definition. Add phases as needed.", - phases: [], - }, -}; - -const ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/; - -interface ValidationResult { - valid: boolean; - errors: { message: string; phaseId?: string }[]; - warnings: { message: string }[]; -} - -function validateDef(def: WorkflowDef): ValidationResult { - const errors: { message: string; phaseId?: string }[] = []; - const warnings: { message: string }[] = []; - - if (!def.id.trim()) { - errors.push({ message: "Workflow ID is required" }); - } else if (!ID_PATTERN.test(def.id)) { - errors.push({ message: "ID must start with a lowercase letter or digit and contain only lowercase letters, digits, and hyphens" }); - } - - if (!def.name.trim()) { - errors.push({ message: "Workflow name is required" }); - } - - if (def.phases.length === 0) { - errors.push({ message: "At least one phase is required" }); - } - - const seen = new Set(); - const phaseIds = new Set(def.phases.map((p) => p.id)); - for (const phase of def.phases) { - if (!phase.id.trim()) { - errors.push({ message: "Phase ID cannot be empty", phaseId: phase.id }); - } else if (!ID_PATTERN.test(phase.id)) { - errors.push({ message: `Phase "${phase.id}" has an invalid ID format`, phaseId: phase.id }); - } - if (seen.has(phase.id)) { - errors.push({ message: `Duplicate phase ID "${phase.id}"`, phaseId: phase.id }); - } - seen.add(phase.id); - - if (phase.maxReworkAttempts < 1) { - errors.push({ message: `Phase "${phase.id}" must have max rework attempts > 0`, phaseId: phase.id }); - } - - for (const [verdict, cfg] of Object.entries(phase.onVerdict)) { - const target = (cfg as { target: string | null }).target; - if (target && !phaseIds.has(target)) { - errors.push({ message: `Phase "${phase.id}" ${verdict} target "${target}" does not exist`, phaseId: phase.id }); - } - } - } - - if (def.phases.length > 0 && !errors.some((e) => e.phaseId)) { - warnings.push({ message: `${def.phases.length} phase(s) configured` }); - } - - return { valid: errors.length === 0, errors, warnings }; -} - -function defToPreview(def: WorkflowDef): string { - const obj: Record = { - id: def.id, - name: def.name, - }; - if (def.description) obj.description = def.description; - obj.phases = def.phases.map((p) => { - const phase: Record = { id: p.id }; - if (p.maxReworkAttempts !== 3) phase.max_rework_attempts = p.maxReworkAttempts; - if (p.skipIf.length > 0) phase.skip_if = p.skipIf; - const onVerdict: Record = {}; - if (p.onVerdict.advance.target) onVerdict.advance = { target: p.onVerdict.advance.target }; - if (p.onVerdict.rework.target || p.onVerdict.rework.allowAgentTarget) { - const rw: Record = {}; - if (p.onVerdict.rework.target) rw.target = p.onVerdict.rework.target; - if (p.onVerdict.rework.allowAgentTarget) rw.allow_agent_target = true; - onVerdict.rework = rw; - } - if (p.onVerdict.fail.target) onVerdict.fail = { target: p.onVerdict.fail.target }; - if (Object.keys(onVerdict).length > 0) phase.on_verdict = onVerdict; - return phase; - }); - if (def.variables.length > 0) { - obj.variables = def.variables.map((v) => { - const ve: Record = { name: v.name }; - if (v.description) ve.description = v.description; - if (v.required) ve.required = true; - if (v.default) ve.default = v.default; - return ve; - }); - } - return JSON.stringify(obj, null, 2); -} - -function PhaseNode({ - phase, - index, - total, - selected, - hasError, - onSelect, - onMoveLeft, - onMoveRight, - onRemove, -}: { - phase: PhaseEntry; - index: number; - total: number; - selected: boolean; - hasError: boolean; - onSelect: () => void; - onMoveLeft: () => void; - onMoveRight: () => void; - onRemove: () => void; -}) { - return ( -
- - )} - {index < total - 1 && ( - - )} - -
- - {index < total - 1 && } -
- ); -} - -function PhaseDetailPanel({ - phase, - allPhaseIds, - onChange, -}: { - phase: PhaseEntry; - allPhaseIds: string[]; - onChange: (updated: PhaseEntry) => void; -}) { - const otherPhases = allPhaseIds.filter((id) => id !== phase.id); - const [newSkipGuard, setNewSkipGuard] = useState(""); - - const updateField = (key: K, value: PhaseEntry[K]) => { - onChange({ ...phase, [key]: value }); - }; - - const updateVerdict = ( - verdict: "advance" | "rework" | "fail", - field: string, - value: unknown, - ) => { - onChange({ - ...phase, - onVerdict: { - ...phase.onVerdict, - [verdict]: { ...phase.onVerdict[verdict], [field]: value }, - }, - }); - }; - - return ( -
- - - Phase Config - - -
- - updateField("id", e.target.value)} - className="mt-1 font-mono text-xs h-8" - /> -
- -
- - updateField("maxReworkAttempts", Math.max(1, parseInt(e.target.value) || 1))} - className="mt-1 text-xs h-8" - /> -
- -
- -
- {phase.skipIf.map((guard, i) => ( -
- {guard} - -
- ))} -
- setNewSkipGuard(e.target.value)} - placeholder="Guard condition" - className="text-xs h-7 flex-1" - onKeyDown={(e) => { - if (e.key === "Enter" && newSkipGuard.trim()) { - updateField("skipIf", [...phase.skipIf, newSkipGuard.trim()]); - setNewSkipGuard(""); - } - }} - /> - -
-
-
- - - -
- - -
- -
- - - -
- -
- - -
-
-
-
- ); -} - -function VariableCard({ - variable, - onChange, - onRemove, -}: { - variable: VariableEntry; - onChange: (v: VariableEntry) => void; - onRemove: () => void; -}) { - return ( - - -
-
-
- - onChange({ ...variable, name: e.target.value })} - className="mt-1 font-mono text-xs h-8" - placeholder="variable_name" - /> -
-
- - onChange({ ...variable, default: e.target.value })} - className="mt-1 text-xs h-8" - placeholder="Default value" - /> -
-
- -
-
- - onChange({ ...variable, description: e.target.value })} - className="mt-1 text-xs h-8" - placeholder="What this variable controls" - /> -
- -
-
- ); -} - -function TransitionsTable({ phases }: { phases: PhaseEntry[] }) { - if (phases.length === 0) { - return

No phases configured

; - } - - return ( -
- - - - - - - - - - - {phases.map((p, i) => ( - - - - - - - ))} - -
PhaseAdvanceReworkFail
{p.id} - {p.onVerdict.advance.target ?? (i < phases.length - 1 ? `${phases[i + 1].id} (auto)` : "end")} - - {p.onVerdict.rework.target ?? `${p.id} (self)`} - {p.onVerdict.rework.allowAgentTarget && agent} - - {p.onVerdict.fail.target ?? "stop"} -
-
- ); -} - -function TagInput({ - values, - onChange, - placeholder, -}: { - values: string[]; - onChange: (v: string[]) => void; - placeholder?: string; -}) { - const [input, setInput] = useState(""); - return ( -
-
- {values.map((v, i) => ( - - {v} - - - ))} -
- setInput(e.target.value)} - placeholder={placeholder ?? "Type and press Enter"} - className="text-xs h-7 font-mono" - onKeyDown={(e) => { - if (e.key === "Enter" && input.trim()) { - e.preventDefault(); - onChange([...values, input.trim()]); - setInput(""); - } - }} - /> -
- ); -} - -function ListInput({ - values, - onChange, - placeholder, -}: { - values: string[]; - onChange: (v: string[]) => void; - placeholder?: string; -}) { - const [input, setInput] = useState(""); - return ( -
- {values.map((v, i) => ( -
- {v} - -
- ))} -
- setInput(e.target.value)} - placeholder={placeholder ?? "Add item"} - className="text-xs h-7 font-mono flex-1" - onKeyDown={(e) => { - if (e.key === "Enter" && input.trim()) { - e.preventDefault(); - onChange([...values, input.trim()]); - setInput(""); - } - }} - /> - -
-
- ); -} - -function KeyValueEditor({ - entries, - onChange, -}: { - entries: Array<{ key: string; value: string }>; - onChange: (v: Array<{ key: string; value: string }>) => void; -}) { - return ( -
- {entries.map((entry, i) => ( -
- onChange(entries.map((en, j) => j === i ? { ...en, key: e.target.value } : en))} - placeholder="KEY" - className="text-xs h-7 font-mono flex-1" - /> - = - onChange(entries.map((en, j) => j === i ? { ...en, value: e.target.value } : en))} - placeholder="value" - className="text-xs h-7 font-mono flex-1" - /> - -
- ))} - -
- ); -} - -function AgentProfileCard({ - profile, - onEdit, - onDelete, -}: { - profile: AgentProfileEntry; - onEdit: () => void; - onDelete: () => void; -}) { - return ( - - -
-
-

{profile.name || "Unnamed"}

- {profile.description &&

{profile.description}

} -
- {profile.role && {profile.role}} - {profile.model && {profile.model}} - {profile.tool && {profile.tool}} -
-
-
- - -
-
-
-
- ); -} - -function AgentProfileEditor({ - profile, - onSave, - onCancel, -}: { - profile: AgentProfileEntry; - onSave: (p: AgentProfileEntry) => void; - onCancel: () => void; -}) { - const [draft, setDraft] = useState({ ...profile, mcpServers: [...profile.mcpServers], skills: [...profile.skills] }); - return ( - - -
-
- - setDraft({ ...draft, name: e.target.value })} className="mt-1 font-mono text-xs h-8" placeholder="agent-name" disabled={!!profile.name} /> -
-
- - setDraft({ ...draft, role: e.target.value })} className="mt-1 text-xs h-8" placeholder="implementer" /> -
-
-
- - setDraft({ ...draft, description: e.target.value })} className="mt-1 text-xs h-8" placeholder="What this agent does" /> -
-
- -