From 4178b58fa4f5b6ef48c040b022fbd3b1949b7573 Mon Sep 17 00:00:00 2001 From: Geoff Johnson Date: Fri, 20 Mar 2026 21:47:31 -0700 Subject: [PATCH 1/2] design: pipeline status card and human gates UI for v0.10.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds frontend scaffolding for the Autonomous Development Pipeline (milestone #20) ahead of the conductor agent implementation. New files: ui/src/types/pipeline.ts PipelineStatus, PipelineQueueItem, PipelineGate data types. PIPELINE_GATES constant documents all 4 always-human and 3 configurable gates from the v0.10.0 spec (issue #611). ui/src/hooks/usePipelineStatus.ts Stub hook returning null/not-loading. Includes a TODO comment pointing to the memory service query pattern that will replace it once the conductor starts writing pipeline digests (#603/#604). ui/src/components/dashboard/PipelineStatusCard.tsx Dashboard widget with three states: empty — "Conductor not yet active" with deploy hint loading — CardSkeleton placeholder populated — merge queue (stack-indented, approval dot, CI icon) + active stack count / stale count stats row ui/src/components/settings/PipelineGates.tsx Read-only reference section showing all 7 human interaction gates grouped by kind (always-human / configurable). Uses Lock + Settings2 icons and semantic colour coding (red = always, green/gray = default). Wired into: DashboardPage — PipelineStatusCard replaces the first Coming Soon stub SettingsPage — Pipeline Gates section inserted before About Co-Authored-By: Claude Sonnet 4.6 --- .../dashboard/PipelineStatusCard.tsx | 265 ++++++++++++++++++ ui/src/components/dashboard/index.ts | 2 + ui/src/components/settings/PipelineGates.tsx | 144 ++++++++++ ui/src/hooks/usePipelineStatus.ts | 49 ++++ ui/src/pages/DashboardPage.tsx | 15 +- ui/src/pages/SettingsPage.tsx | 12 + ui/src/types/pipeline.ts | 116 ++++++++ 7 files changed, 602 insertions(+), 1 deletion(-) create mode 100644 ui/src/components/dashboard/PipelineStatusCard.tsx create mode 100644 ui/src/components/settings/PipelineGates.tsx create mode 100644 ui/src/hooks/usePipelineStatus.ts create mode 100644 ui/src/types/pipeline.ts diff --git a/ui/src/components/dashboard/PipelineStatusCard.tsx b/ui/src/components/dashboard/PipelineStatusCard.tsx new file mode 100644 index 00000000..628cadda --- /dev/null +++ b/ui/src/components/dashboard/PipelineStatusCard.tsx @@ -0,0 +1,265 @@ +/** + * PipelineStatusCard — dashboard widget for the autonomous pipeline (v0.10.0). + * + * Shows: + * • Merge queue — PRs labeled merge-queue, ordered bottom-of-stack first + * • Stack stats — active stack count, stale PR count + * • Last sync — timestamp of the conductor's most recent git-spice repo sync + * + * States: + * loading — skeleton placeholder while fetching + * empty — conductor not yet active; shown when status is null + * populated — live merge queue and stats + * + * The card is intentionally narrow in scope: it surfaces actionable pipeline + * state at a glance. Deep stack inspection (dependency graph, per-PR diffs) + * belongs on a future /pipeline detail page. + * + * Accessibility: section landmark with labelled heading; all status + * indicators carry aria-labels; reduced-motion respected via CSS. + */ + +import { CheckCircle2, Circle, GitBranch, Loader2, RefreshCw, Workflow, XCircle } from 'lucide-react' +import { CardSkeleton } from '@/components/common/LoadingSkeleton' +import type { PipelineQueueItem, PipelineStatus } from '@/types/pipeline' + +// --------------------------------------------------------------------------- +// Sub-components +// --------------------------------------------------------------------------- + +/** Indented stack-depth prefix for queue items */ +function StackPrefix({ depth }: { depth: number }) { + if (depth === 0) return null + return ( + + ) +} + +/** CI status indicator */ +function CiIndicator({ passing }: { passing: boolean | null }) { + if (passing === null) { + return ( + + ) + } + if (passing) { + return + } + return +} + +/** Single row in the merge queue */ +function QueueRow({ item }: { item: PipelineQueueItem }) { + return ( +
  • + {/* Stack depth prefix */} + + + #{item.prNumber} + + + {/* Title */} + + {item.title} + + + {/* Approval dot */} + + + {/* CI status */} + + + +
  • + ) +} + +/** Empty state shown when no conductor has run yet */ +function EmptyState() { + return ( +
    +
    +
    +
    +

    + Conductor not yet active +

    +

    + Deploy{' '} + + conductor.yml + {' '} + to activate the autonomous pipeline. +

    +
    +
    + ) +} + +/** Populated state with merge queue and stats */ +function PipelineContent({ status }: { status: PipelineStatus }) { + const { mergeQueue, activeStackCount, staleCount } = status + + return ( + <> + {/* Merge queue */} +
    +
    +
    + + {mergeQueue.length === 0 ? ( +

    + Queue is empty +

    + ) : ( +
      + {mergeQueue.slice(0, 6).map((item) => ( + + ))} + {mergeQueue.length > 6 && ( +
    • + +{mergeQueue.length - 6} more +
    • + )} +
    + )} +
    + + {/* Stats row */} +
    + + + {staleCount > 0 && ( + + + )} +
    + + ) +} + +// --------------------------------------------------------------------------- +// PipelineStatusCard +// --------------------------------------------------------------------------- + +export interface PipelineStatusCardProps { + status: PipelineStatus | null + loading: boolean + error?: string + onRefetch?: () => void +} + +export function PipelineStatusCard({ + status, + loading, + error, + onRefetch, +}: PipelineStatusCardProps) { + const lastSync = status?.lastSyncAt ? formatRelativeTime(new Date(status.lastSyncAt)) : null + + return ( +
    + {/* Header */} +
    +

    + Pipeline +

    + +
    + {lastSync && ( + + Synced {lastSync} + + )} + {onRefetch && ( + + )} +
    +
    + + {/* Error */} + {error && ( +

    + {error} +

    + )} + + {/* Loading */} + {loading && !error && ( +
    + +
    + )} + + {/* Content */} + {!loading && !error && ( + status ? : + )} +
    + ) +} + +export default PipelineStatusCard + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function formatRelativeTime(date: Date): string { + const diffMs = Date.now() - date.getTime() + const diffSec = Math.floor(diffMs / 1000) + if (diffSec < 60) return 'just now' + const diffMin = Math.floor(diffSec / 60) + if (diffMin < 60) return `${diffMin} min ago` + const diffHr = Math.floor(diffMin / 60) + if (diffHr < 24) return `${diffHr}h ago` + return `${Math.floor(diffHr / 24)}d ago` +} diff --git a/ui/src/components/dashboard/index.ts b/ui/src/components/dashboard/index.ts index b2cf918c..dabb2dad 100644 --- a/ui/src/components/dashboard/index.ts +++ b/ui/src/components/dashboard/index.ts @@ -3,3 +3,5 @@ export { AgentSummary } from './AgentSummary' export { NotificationSummary } from './NotificationSummary' export { ActivityTimeline } from './ActivityTimeline' export type { ActivityEvent, ActivityEventType } from './ActivityTimeline' +export { PipelineStatusCard } from './PipelineStatusCard' +export type { PipelineStatusCardProps } from './PipelineStatusCard' diff --git a/ui/src/components/settings/PipelineGates.tsx b/ui/src/components/settings/PipelineGates.tsx new file mode 100644 index 00000000..7ee4beac --- /dev/null +++ b/ui/src/components/settings/PipelineGates.tsx @@ -0,0 +1,144 @@ +/** + * PipelineGates — read-only reference section for the autonomous pipeline + * human interaction gates (v0.10.0). + * + * Gates define exactly where humans remain in the loop. This section + * surfaces them visibly in Settings so operators know what the system + * will and will not do autonomously. + * + * Two categories: + * Always-human — hardcoded in the conductor system prompt; cannot be + * changed through the UI or config. + * Configurable — autonomous by default but can be set per-project in + * the agent YAML. The UI shows the default only; actual + * runtime value comes from the conductor YAML. + * + * This is intentionally read-only for now. Once the conductor exposes a + * runtime settings endpoint, the configurable gates can be made interactive. + */ + +import { Lock, Settings2 } from 'lucide-react' +import { PIPELINE_GATES } from '@/types/pipeline' +import type { PipelineGate } from '@/types/pipeline' + +// --------------------------------------------------------------------------- +// Gate row +// --------------------------------------------------------------------------- + +function GateRow({ gate }: { gate: PipelineGate }) { + const isAlways = gate.kind === 'always' + + return ( +
    + {/* Icon */} +
    + {isAlways ? ( +
    + + {/* Text */} +
    +
    +

    + {gate.label} +

    + + {isAlways + ? 'Always human' + : gate.defaultAutonomous + ? 'Autonomous by default' + : 'Human by default'} + +
    +

    + {gate.description} +

    +
    +
    + ) +} + +// --------------------------------------------------------------------------- +// PipelineGates +// --------------------------------------------------------------------------- + +export function PipelineGates() { + const alwaysGates = PIPELINE_GATES.filter((g) => g.kind === 'always') + const configurableGates = PIPELINE_GATES.filter((g) => g.kind === 'configurable') + + return ( +
    + {/* Explainer */} +

    + The autonomous pipeline (v0.10.0) defines explicit gates where human + approval is required. Always-human gates are hardcoded and cannot be + overridden. Configurable gates show their default; the actual runtime + value is set per-project in{' '} + + .agentd/agents/conductor.yml + + . +

    + + {/* Always-human gates */} +
    +

    +

    +
    + {alwaysGates.map((gate) => ( + + ))} +
    +
    + + {/* Configurable gates */} +
    +

    +

    +
    + {configurableGates.map((gate) => ( + + ))} +
    +

    + To change a configurable gate, update the{' '} + gates: section in conductor.yml and + redeploy the agent. +

    +
    +
    + ) +} + +export default PipelineGates diff --git a/ui/src/hooks/usePipelineStatus.ts b/ui/src/hooks/usePipelineStatus.ts new file mode 100644 index 00000000..505659b4 --- /dev/null +++ b/ui/src/hooks/usePipelineStatus.ts @@ -0,0 +1,49 @@ +/** + * usePipelineStatus — fetches the current autonomous pipeline state. + * + * Status: stub / not yet wired. + * + * The Conductor agent (v0.10.0, issue #603) will expose pipeline state by + * writing structured memories tagged conductor+pipeline after each run. + * This hook will be updated to query the memory service (or a dedicated + * orchestrator endpoint) once that work lands. + * + * Until then it returns { status: null, loading: false } so the + * PipelineStatusCard renders its "not yet active" empty state instead + * of a perpetual spinner. + */ + +import { useEffect, useState } from 'react' +import type { PipelineStatus } from '@/types/pipeline' + +export interface UsePipelineStatusResult { + status: PipelineStatus | null + loading: boolean + error?: string + /** Manually re-fetch */ + refetch: () => void +} + +export function usePipelineStatus(): UsePipelineStatusResult { + const [status] = useState(null) + const [loading] = useState(false) + const [error] = useState(undefined) + const [, setTick] = useState(0) + + // Placeholder effect — replace with real fetch when conductor endpoint exists. + useEffect(() => { + // TODO(v0.10.0): query memory service for latest conductor pipeline digest. + // Example query: + // memoryClient.search('pipeline state merge queue', { + // tags: ['conductor', 'pipeline'], + // limit: 1, + // }) + }, []) + + return { + status, + loading, + error, + refetch: () => setTick((t) => t + 1), + } +} diff --git a/ui/src/pages/DashboardPage.tsx b/ui/src/pages/DashboardPage.tsx index 5357f9cb..08aa569a 100644 --- a/ui/src/pages/DashboardPage.tsx +++ b/ui/src/pages/DashboardPage.tsx @@ -12,10 +12,12 @@ import { import { AgentSummary } from '@/components/dashboard/AgentSummary' import { NotificationSummary } from '@/components/dashboard/NotificationSummary' import { ActivityTimeline } from '@/components/dashboard/ActivityTimeline' +import { PipelineStatusCard } from '@/components/dashboard/PipelineStatusCard' import type { ActivityEvent } from '@/components/dashboard/ActivityTimeline' import { useServiceHealth } from '@/hooks/useServiceHealth' import { useAgentSummary } from '@/hooks/useAgentSummary' import { useNotificationSummary } from '@/hooks/useNotificationSummary' +import { usePipelineStatus } from '@/hooks/usePipelineStatus' // --------------------------------------------------------------------------- // Stub "Coming Soon" card @@ -48,6 +50,7 @@ export function DashboardPage() { const { services, loading: healthLoading, initializing: healthInit, refresh } = useServiceHealth() const agentSummary = useAgentSummary() const notifSummary = useNotificationSummary() + const { status: pipelineStatus, loading: pipelineLoading, error: pipelineError, refetch: pipelineRefetch } = usePipelineStatus() // Build a synthetic activity feed from available data const activityEvents: ActivityEvent[] = useMemo(() => { @@ -117,12 +120,22 @@ export function DashboardPage() { error={agentSummary.error} /> - {/* Stub sections */} + {/* Pipeline status + monitoring stub */}
    + } /> +
    + + {/* Remaining stubs */} +
    } />
    diff --git a/ui/src/pages/SettingsPage.tsx b/ui/src/pages/SettingsPage.tsx index 92075ea1..5b7f34fb 100644 --- a/ui/src/pages/SettingsPage.tsx +++ b/ui/src/pages/SettingsPage.tsx @@ -7,6 +7,7 @@ import { useRef, useState } from 'react' import { ServiceConfig } from '@/components/settings/ServiceConfig' import { UIPreferences } from '@/components/settings/UIPreferences' import { AboutSection } from '@/components/settings/AboutSection' +import { PipelineGates } from '@/components/settings/PipelineGates' import { useSettings } from '@/hooks/useSettings' import { resetSettings } from '@/stores/settingsStore' import type { Settings } from '@/stores/settingsStore' @@ -83,6 +84,17 @@ export function SettingsPage() { + {/* Pipeline Gates */} +
    +

    + Autonomous Pipeline Gates +

    +

    + v0.10.0 — defines where human approval is required in the autonomous pipeline. +

    + +
    + {/* About */}

    About

    diff --git a/ui/src/types/pipeline.ts b/ui/src/types/pipeline.ts new file mode 100644 index 00000000..ce409bce --- /dev/null +++ b/ui/src/types/pipeline.ts @@ -0,0 +1,116 @@ +/** + * Pipeline types for the v0.10.0 Autonomous Development Pipeline. + * + * The Conductor agent manages the merge queue and git-spice stack state. + * These types represent the data shape exposed to the UI; the backend + * endpoint that produces this data is defined in the v0.10.0 conductor + * implementation (issues #603 / #604). + * + * Until the endpoint exists, usePipelineStatus() returns null and the + * PipelineStatusCard renders its "not yet active" empty state. + */ + +/** A single PR waiting in the merge queue */ +export interface PipelineQueueItem { + /** GitHub PR number */ + prNumber: number + /** PR title (truncated for display) */ + title: string + /** Head branch name */ + branch: string + /** Base branch (trunk = "main", or parent stack branch) */ + baseBranch: string + /** + * Stack depth: 0 = branches directly from trunk (main), + * N = Nth level in a git-spice stack. + */ + stackDepth: number + /** Approved by the reviewer agent */ + approved: boolean + /** CI status. null = checks still running */ + ciPassing: boolean | null +} + +/** Aggregate pipeline state posted by the Conductor every 4 hours */ +export interface PipelineStatus { + /** PRs in the merge queue, ordered bottom-of-stack first */ + mergeQueue: PipelineQueueItem[] + /** Number of distinct open stacks (groups of stacked branches) */ + activeStackCount: number + /** PRs with no activity (commits or review comments) for > 7 days */ + staleCount: number + /** ISO timestamp of the last `git-spice repo sync` run */ + lastSyncAt: string | null + /** ISO timestamp of the last full conductor run */ + conductorLastRunAt: string | null +} + +/** + * Human interaction gate definitions. + * + * Sourced from the v0.10.0 milestone spec (issue #611). + * Always-human gates are hardcoded in the conductor system prompt and cannot + * be changed via the UI. Configurable gates default to autonomous but can + * be flipped per-project in the agent YAML. + */ +export interface PipelineGate { + id: string + label: string + description: string + /** 'always' = hardcoded human required; 'configurable' = default autonomous */ + kind: 'always' | 'configurable' + /** Default state for configurable gates */ + defaultAutonomous?: boolean +} + +export const PIPELINE_GATES: PipelineGate[] = [ + // Always-human gates + { + id: 'git-spice-auth', + label: 'git-spice authentication', + description: 'One-time GitHub auth for git-spice must be performed by a human operator.', + kind: 'always', + }, + { + id: 'production-deploy', + label: 'Production deployments', + description: 'Any deployment to a production environment requires human sign-off.', + kind: 'always', + }, + { + id: 'security-changes', + label: 'Security-sensitive changes', + description: + 'Changes to authentication, secrets handling, or cryptography require human review.', + kind: 'always', + }, + { + id: 'conflict-escalation', + label: 'Merge conflict escalation', + description: + 'When the Conductor cannot automatically restack a conflict, it escalates to a human.', + kind: 'always', + }, + // Configurable gates + { + id: 'pr-auto-merge', + label: 'PR auto-merge', + description: 'Conductor merges approved, CI-passing PRs without human confirmation.', + kind: 'configurable', + defaultAutonomous: true, + }, + { + id: 'issue-auto-close', + label: 'Issue auto-close', + description: 'Issues are automatically closed when their implementation PR merges.', + kind: 'configurable', + defaultAutonomous: true, + }, + { + id: 'new-dependency', + label: 'New dependency additions', + description: 'Adding a new crate or package dependency requires human approval.', + kind: 'configurable', + defaultAutonomous: false, + }, +] From c5989961893db52f0aed3d29f233bf794efd163d Mon Sep 17 00:00:00 2001 From: Geoff Johnson Date: Fri, 20 Mar 2026 22:00:03 -0700 Subject: [PATCH 2/2] design: correct pipeline types to milestone #18 and add PipelineStage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The conductor agent (issue #620) and pipeline infrastructure (issues #640, #643) are all in milestone #18 (v0.9.0), not v0.10.0. Update the module docstring and gate issue references accordingly. Add PipelineStage union type modelling the 7-stage label-driven state machine from issue #640: issue-created → triage → enrich → implement → review → merge → document. Exports PIPELINE_STAGE_LABELS (display strings) and PIPELINE_STAGE_ORDER (canonical progression array) for use in future PipelineStatusCard enhancements. Co-Authored-By: Claude Sonnet 4.6 --- ui/src/types/pipeline.ts | 66 ++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/ui/src/types/pipeline.ts b/ui/src/types/pipeline.ts index ce409bce..eb66aa8b 100644 --- a/ui/src/types/pipeline.ts +++ b/ui/src/types/pipeline.ts @@ -1,15 +1,60 @@ /** - * Pipeline types for the v0.10.0 Autonomous Development Pipeline. + * Pipeline types for the v0.9.0 Autonomous Development Pipeline. * - * The Conductor agent manages the merge queue and git-spice stack state. - * These types represent the data shape exposed to the UI; the backend - * endpoint that produces this data is defined in the v0.10.0 conductor - * implementation (issues #603 / #604). + * The Conductor agent (`.agentd/agents/conductor.yml`, issue #620) manages + * the merge queue and git-spice stack state. The label-driven state machine + * is defined in issue #640; human approval gates are defined in issue #643. * - * Until the endpoint exists, usePipelineStatus() returns null and the - * PipelineStatusCard renders its "not yet active" empty state. + * These types represent the data shape exposed to the UI. Until the conductor + * endpoint exists, usePipelineStatus() returns null and PipelineStatusCard + * renders its "not yet active" empty state. */ +// --------------------------------------------------------------------------- +// Pipeline stage state machine +// --------------------------------------------------------------------------- + +/** + * The 7-stage label-driven pipeline state machine (issue #640). + * + * Each issue advances through these stages as agents act on it: + * issue-created → triage → enrich → implement → review → merge → document + * + * The Conductor monitors stage transitions via GitHub label changes. + */ +export type PipelineStage = + | 'issue-created' + | 'triage' + | 'enrich' + | 'implement' + | 'review' + | 'merge' + | 'document' + +export const PIPELINE_STAGE_LABELS: Record = { + 'issue-created': 'Issue Created', + triage: 'Triage', + enrich: 'Enrich', + implement: 'Implement', + review: 'Review', + merge: 'Merge', + document: 'Document', +} + +export const PIPELINE_STAGE_ORDER: PipelineStage[] = [ + 'issue-created', + 'triage', + 'enrich', + 'implement', + 'review', + 'merge', + 'document', +] + +// --------------------------------------------------------------------------- +// Merge queue +// --------------------------------------------------------------------------- + /** A single PR waiting in the merge queue */ export interface PipelineQueueItem { /** GitHub PR number */ @@ -45,10 +90,13 @@ export interface PipelineStatus { conductorLastRunAt: string | null } +// --------------------------------------------------------------------------- +// Human interaction gates +// --------------------------------------------------------------------------- + /** - * Human interaction gate definitions. + * Human interaction gate definitions (issue #643). * - * Sourced from the v0.10.0 milestone spec (issue #611). * Always-human gates are hardcoded in the conductor system prompt and cannot * be changed via the UI. Configurable gates default to autonomous but can * be flipped per-project in the agent YAML.