diff --git a/apps/server/git/branch-manager.ts b/apps/server/git/branch-manager.ts index d61b4dd..477e593 100644 --- a/apps/server/git/branch-manager.ts +++ b/apps/server/git/branch-manager.ts @@ -6,7 +6,7 @@ * a worktree to be checked out. */ -import type { MergeResult } from './types.js'; +import type { MergeResult, BranchCommit } from './types.js'; export interface BranchManager { /** @@ -45,4 +45,15 @@ export interface BranchManager { * since local branches may not include all remote branches. */ remoteBranchExists(repoPath: string, branch: string): Promise; + + /** + * List commits that headBranch has but baseBranch doesn't. + * Used for commit-level navigation in code review. + */ + listCommits(repoPath: string, baseBranch: string, headBranch: string): Promise; + + /** + * Get the raw unified diff for a single commit. + */ + diffCommit(repoPath: string, commitHash: string): Promise; } diff --git a/apps/server/git/simple-git-branch-manager.ts b/apps/server/git/simple-git-branch-manager.ts index a6b2be8..ce39b54 100644 --- a/apps/server/git/simple-git-branch-manager.ts +++ b/apps/server/git/simple-git-branch-manager.ts @@ -11,7 +11,7 @@ import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { simpleGit } from 'simple-git'; import type { BranchManager } from './branch-manager.js'; -import type { MergeResult } from './types.js'; +import type { MergeResult, BranchCommit } from './types.js'; import { createModuleLogger } from '../logger/index.js'; const log = createModuleLogger('branch-manager'); @@ -116,4 +116,28 @@ export class SimpleGitBranchManager implements BranchManager { return false; } } + + async listCommits(repoPath: string, baseBranch: string, headBranch: string): Promise { + const git = simpleGit(repoPath); + const logResult = await git.log({ from: baseBranch, to: headBranch, '--stat': null }); + + return logResult.all.map((entry) => { + const diffStat = entry.diff; + return { + hash: entry.hash, + shortHash: entry.hash.slice(0, 7), + message: entry.message, + author: entry.author_name, + date: entry.date, + filesChanged: diffStat?.files?.length ?? 0, + insertions: diffStat?.insertions ?? 0, + deletions: diffStat?.deletions ?? 0, + }; + }); + } + + async diffCommit(repoPath: string, commitHash: string): Promise { + const git = simpleGit(repoPath); + return git.diff([`${commitHash}~1`, commitHash]); + } } diff --git a/apps/server/git/types.ts b/apps/server/git/types.ts index 3a94117..17d56ae 100644 --- a/apps/server/git/types.ts +++ b/apps/server/git/types.ts @@ -58,6 +58,33 @@ export interface MergeResult { message: string; } +// ============================================================================= +// Branch Commit Info +// ============================================================================= + +/** + * Represents a single commit in a branch's history. + * Used for commit-level navigation in code review. + */ +export interface BranchCommit { + /** Full commit hash */ + hash: string; + /** Short commit hash (7 chars) */ + shortHash: string; + /** First line of commit message */ + message: string; + /** Author name */ + author: string; + /** ISO date string */ + date: string; + /** Number of files changed */ + filesChanged: number; + /** Number of insertions */ + insertions: number; + /** Number of deletions */ + deletions: number; +} + // ============================================================================= // WorktreeManager Port Interface // ============================================================================= diff --git a/apps/server/trpc/routers/phase.ts b/apps/server/trpc/routers/phase.ts index e505837..876140b 100644 --- a/apps/server/trpc/routers/phase.ts +++ b/apps/server/trpc/routers/phase.ts @@ -234,5 +234,69 @@ export function phaseProcedures(publicProcedure: ProcedureBuilder) { await orchestrator.approveAndMergePhase(input.phaseId); return { success: true }; }), + + getPhaseReviewCommits: publicProcedure + .input(z.object({ phaseId: z.string().min(1) })) + .query(async ({ ctx, input }) => { + const phaseRepo = requirePhaseRepository(ctx); + const initiativeRepo = requireInitiativeRepository(ctx); + const projectRepo = requireProjectRepository(ctx); + const branchManager = requireBranchManager(ctx); + + const phase = await phaseRepo.findById(input.phaseId); + if (!phase) { + throw new TRPCError({ code: 'NOT_FOUND', message: `Phase '${input.phaseId}' not found` }); + } + if (phase.status !== 'pending_review') { + throw new TRPCError({ code: 'BAD_REQUEST', message: `Phase is not pending review (status: ${phase.status})` }); + } + + const initiative = await initiativeRepo.findById(phase.initiativeId); + if (!initiative?.branch) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Initiative has no branch configured' }); + } + + const initBranch = initiative.branch; + const phBranch = phaseBranchName(initBranch, phase.name); + const projects = await projectRepo.findProjectsByInitiativeId(phase.initiativeId); + + const allCommits: Array<{ hash: string; shortHash: string; message: string; author: string; date: string; filesChanged: number; insertions: number; deletions: number }> = []; + + for (const project of projects) { + const clonePath = await ensureProjectClone(project, ctx.workspaceRoot!); + const commits = await branchManager.listCommits(clonePath, initBranch, phBranch); + allCommits.push(...commits); + } + + return { commits: allCommits, sourceBranch: phBranch, targetBranch: initBranch }; + }), + + getCommitDiff: publicProcedure + .input(z.object({ phaseId: z.string().min(1), commitHash: z.string().min(1) })) + .query(async ({ ctx, input }) => { + const phaseRepo = requirePhaseRepository(ctx); + const projectRepo = requireProjectRepository(ctx); + const branchManager = requireBranchManager(ctx); + + const phase = await phaseRepo.findById(input.phaseId); + if (!phase) { + throw new TRPCError({ code: 'NOT_FOUND', message: `Phase '${input.phaseId}' not found` }); + } + + const projects = await projectRepo.findProjectsByInitiativeId(phase.initiativeId); + let rawDiff = ''; + + for (const project of projects) { + const clonePath = await ensureProjectClone(project, ctx.workspaceRoot!); + try { + const diff = await branchManager.diffCommit(clonePath, input.commitHash); + if (diff) rawDiff += diff + '\n'; + } catch { + // commit not in this project clone + } + } + + return { rawDiff }; + }), }; } diff --git a/apps/web/src/components/review/CommitNav.tsx b/apps/web/src/components/review/CommitNav.tsx new file mode 100644 index 0000000..4d2712a --- /dev/null +++ b/apps/web/src/components/review/CommitNav.tsx @@ -0,0 +1,116 @@ +import { GitCommitHorizontal, Plus, Minus, FileCode } from "lucide-react"; +import type { CommitInfo } from "./types"; + +interface CommitNavProps { + commits: CommitInfo[]; + selectedCommit: string | null; // null = "all changes" + onSelectCommit: (hash: string | null) => void; + isLoading: boolean; +} + +export function CommitNav({ + commits, + selectedCommit, + onSelectCommit, + isLoading, +}: CommitNavProps) { + if (isLoading || commits.length === 0) return null; + + // Don't show navigator for single-commit phases + if (commits.length === 1) return null; + + return ( +
+
+ {/* "All changes" pill */} + onSelectCommit(null)} + /> + +
+ + {/* Individual commit pills - most recent first */} + {commits.map((commit) => ( + onSelectCommit(commit.hash)} + isMono + /> + ))} +
+
+ ); +} + +interface CommitPillProps { + label: string; + sublabel: string; + stats?: { files: number; add: number; del: number }; + isActive: boolean; + onClick: () => void; + isMono?: boolean; +} + +function CommitPill({ + label, + sublabel, + stats, + isActive, + onClick, + isMono, +}: CommitPillProps) { + return ( + + ); +} + +function truncateMessage(msg: string): string { + const firstLine = msg.split("\n")[0]; + return firstLine.length > 50 ? firstLine.slice(0, 47) + "..." : firstLine; +} diff --git a/apps/web/src/components/review/ReviewHeader.tsx b/apps/web/src/components/review/ReviewHeader.tsx new file mode 100644 index 0000000..b259aac --- /dev/null +++ b/apps/web/src/components/review/ReviewHeader.tsx @@ -0,0 +1,255 @@ +import { + Check, + X, + GitBranch, + FileCode, + Plus, + Minus, + ExternalLink, + Loader2, + Square, + CircleDot, + RotateCcw, + ArrowRight, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import type { FileDiff, ReviewStatus } from "./types"; + +interface PhaseOption { + id: string; + name: string; +} + +interface PreviewState { + status: "idle" | "building" | "running" | "failed"; + url?: string; + onStart: () => void; + onStop: () => void; + isStarting: boolean; + isStopping: boolean; +} + +interface ReviewHeaderProps { + phases: PhaseOption[]; + activePhaseId: string | null; + onPhaseSelect: (id: string) => void; + phaseName: string; + sourceBranch: string; + targetBranch: string; + files: FileDiff[]; + status: ReviewStatus; + unresolvedCount: number; + onApprove: () => void; + onRequestChanges: () => void; + preview: PreviewState | null; +} + +export function ReviewHeader({ + phases, + activePhaseId, + onPhaseSelect, + phaseName, + sourceBranch, + targetBranch, + files, + status, + unresolvedCount, + onApprove, + onRequestChanges, + preview, +}: ReviewHeaderProps) { + const totalAdditions = files.reduce((s, f) => s + f.additions, 0); + const totalDeletions = files.reduce((s, f) => s + f.deletions, 0); + + return ( +
+ {/* Phase selector row */} + {phases.length > 1 && ( +
+ + Phases + +
+ {phases.map((phase) => { + const isActive = phase.id === activePhaseId; + return ( + + ); + })} +
+
+ )} + + {/* Main toolbar row */} +
+ {/* Left: branch info + stats */} +
+

+ {phaseName} +

+ + {sourceBranch && ( +
+ + + {truncateBranch(sourceBranch)} + + + + {truncateBranch(targetBranch)} + +
+ )} + +
+ + + {files.length} + + + + {totalAdditions} + + + + {totalDeletions} + +
+
+ + {/* Right: preview + actions */} +
+ {/* Preview controls */} + {preview && } + + {/* Review status / actions */} + {status === "pending" && ( + <> + + + + )} + {status === "approved" && ( + + + Approved + + )} + {status === "changes_requested" && ( + + + Changes Requested + + )} +
+
+
+ ); +} + +function PreviewControls({ preview }: { preview: PreviewState }) { + if (preview.status === "building" || preview.isStarting) { + return ( +
+ + Building... +
+ ); + } + + if (preview.status === "running") { + return ( + + ); + } + + if (preview.status === "failed") { + return ( + + ); + } + + return ( + + ); +} + +function truncateBranch(branch: string): string { + const parts = branch.split("/"); + if (parts.length <= 2) return branch; + return parts.slice(-2).join("/"); +} diff --git a/apps/web/src/components/review/ReviewSidebar.tsx b/apps/web/src/components/review/ReviewSidebar.tsx index 3829d5e..378e6a3 100644 --- a/apps/web/src/components/review/ReviewSidebar.tsx +++ b/apps/web/src/components/review/ReviewSidebar.tsx @@ -1,186 +1,109 @@ import { - Check, - X, MessageSquare, - GitBranch, FileCode, Plus, Minus, Circle, CheckCircle2, } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import type { FileDiff, ReviewComment, ReviewStatus } from "./types"; +import type { FileDiff, ReviewComment } from "./types"; interface ReviewSidebarProps { - title: string; - description: string; - author: string; - status: ReviewStatus; - sourceBranch: string; - targetBranch: string; files: FileDiff[]; comments: ReviewComment[]; - onApprove: () => void; - onRequestChanges: () => void; onFileClick: (filePath: string) => void; + selectedCommit: string | null; + activeFiles: FileDiff[]; } export function ReviewSidebar({ - title, - description, - author, - status, - sourceBranch, - targetBranch, files, comments, - onApprove, - onRequestChanges, onFileClick, + selectedCommit, + activeFiles, }: ReviewSidebarProps) { const unresolvedCount = comments.filter((c) => !c.resolved).length; const resolvedCount = comments.filter((c) => c.resolved).length; - const totalAdditions = files.reduce((s, f) => s + f.additions, 0); - const totalDeletions = files.reduce((s, f) => s + f.deletions, 0); + + // Build a set of files visible in the current diff view + const activeFilePaths = new Set(activeFiles.map((f) => f.newPath)); return ( -
- {/* Review info */} -
-
-

{title}

- -
-

- {description} -

-
- {author} -
-
- - {sourceBranch} - - {targetBranch} -
-
- - {/* Actions */} -
- {status === "pending" && ( - <> - - - - )} - {status === "approved" && ( -
- - Approved -
- )} - {status === "changes_requested" && ( -
- - Changes Requested -
- )} -
- +
{/* Comment summary */} -
-

- Discussions -

-
- - - {comments.length} comment{comments.length !== 1 ? "s" : ""} - - {resolvedCount > 0 && ( - - - {resolvedCount} resolved + {comments.length > 0 && ( +
+

+ Discussions +

+
+ + + {comments.length} - )} - {unresolvedCount > 0 && ( - - - {unresolvedCount} open - - )} + {resolvedCount > 0 && ( + + + {resolvedCount} + + )} + {unresolvedCount > 0 && ( + + + {unresolvedCount} + + )} +
-
- - {/* Stats */} -
-

- Changes -

-
- - - {files.length} file{files.length !== 1 ? "s" : ""} - - - - {totalAdditions} - - - - {totalDeletions} - -
-
+ )} {/* File list */} -
-

+
+

Files + {selectedCommit && ( + + ({activeFiles.length} in commit) + + )}

{files.map((file) => { const fileCommentCount = comments.filter( - (c) => c.filePath === file.newPath + (c) => c.filePath === file.newPath, ).length; + const isInView = activeFilePaths.has(file.newPath); + const dimmed = selectedCommit && !isInView; + return ( ); @@ -190,24 +113,9 @@ export function ReviewSidebar({ ); } -function ReviewStatusBadge({ status }: { status: ReviewStatus }) { - if (status === "approved") { - return ( - - Approved - - ); - } - if (status === "changes_requested") { - return ( - - Changes Requested - - ); - } - return ( - - Pending Review - - ); +/** Show filename with parent directory for context */ +function formatFilePath(path: string): string { + const parts = path.split("/"); + if (parts.length <= 2) return path; + return parts.slice(-2).join("/"); } diff --git a/apps/web/src/components/review/ReviewTab.tsx b/apps/web/src/components/review/ReviewTab.tsx index daac08c..852e81d 100644 --- a/apps/web/src/components/review/ReviewTab.tsx +++ b/apps/web/src/components/review/ReviewTab.tsx @@ -1,10 +1,12 @@ import { useState, useCallback, useMemo, useRef } from "react"; import { toast } from "sonner"; +import { Loader2 } from "lucide-react"; import { trpc } from "@/lib/trpc"; import { parseUnifiedDiff } from "./parse-diff"; import { DiffViewer } from "./DiffViewer"; import { ReviewSidebar } from "./ReviewSidebar"; -import { PreviewPanel } from "./PreviewPanel"; +import { ReviewHeader } from "./ReviewHeader"; +import { CommitNav } from "./CommitNav"; import type { ReviewComment, ReviewStatus, DiffLine } from "./types"; interface ReviewTabProps { @@ -14,6 +16,7 @@ interface ReviewTabProps { export function ReviewTab({ initiativeId }: ReviewTabProps) { const [comments, setComments] = useState([]); const [status, setStatus] = useState("pending"); + const [selectedCommit, setSelectedCommit] = useState(null); const fileRefs = useRef>(new Map()); // Fetch phases for this initiative @@ -31,27 +34,111 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) { const projectsQuery = trpc.getInitiativeProjects.useQuery({ initiativeId }); const firstProjectId = projectsQuery.data?.[0]?.id ?? null; - // Fetch diff for active phase + // Fetch full branch diff for active phase const diffQuery = trpc.getPhaseReviewDiff.useQuery( { phaseId: activePhaseId! }, { enabled: !!activePhaseId }, ); + // Fetch commits for active phase + const commitsQuery = trpc.getPhaseReviewCommits.useQuery( + { phaseId: activePhaseId! }, + { enabled: !!activePhaseId }, + ); + const commits = commitsQuery.data?.commits ?? []; + + // Fetch single-commit diff when a commit is selected + const commitDiffQuery = trpc.getCommitDiff.useQuery( + { phaseId: activePhaseId!, commitHash: selectedCommit! }, + { enabled: !!activePhaseId && !!selectedCommit }, + ); + + // Preview state + const previewsQuery = trpc.listPreviews.useQuery( + { initiativeId }, + { refetchInterval: 3000 }, + ); + const existingPreview = previewsQuery.data?.find( + (p) => p.phaseId === activePhaseId || p.initiativeId === initiativeId, + ); + const [activePreviewId, setActivePreviewId] = useState(null); + const previewStatusQuery = trpc.getPreviewStatus.useQuery( + { previewId: activePreviewId ?? existingPreview?.id ?? "" }, + { + enabled: !!(activePreviewId ?? existingPreview?.id), + refetchInterval: 3000, + }, + ); + const preview = previewStatusQuery.data ?? existingPreview; + const sourceBranch = diffQuery.data?.sourceBranch ?? commitsQuery.data?.sourceBranch ?? ""; + + const startPreview = trpc.startPreview.useMutation({ + onSuccess: (data) => { + setActivePreviewId(data.id); + toast.success(`Preview running at http://localhost:${data.port}`); + }, + onError: (err) => toast.error(`Preview failed: ${err.message}`), + }); + + const stopPreview = trpc.stopPreview.useMutation({ + onSuccess: () => { + setActivePreviewId(null); + toast.success("Preview stopped"); + previewsQuery.refetch(); + }, + onError: (err) => toast.error(`Failed to stop: ${err.message}`), + }); + + const previewState = firstProjectId && sourceBranch + ? { + status: startPreview.isPending + ? ("building" as const) + : preview?.status === "running" + ? ("running" as const) + : preview?.status === "building" + ? ("building" as const) + : preview?.status === "failed" + ? ("failed" as const) + : ("idle" as const), + url: preview?.port ? `http://localhost:${preview.port}` : undefined, + onStart: () => + startPreview.mutate({ + initiativeId, + phaseId: activePhaseId ?? undefined, + projectId: firstProjectId, + branch: sourceBranch, + }), + onStop: () => { + const id = activePreviewId ?? existingPreview?.id; + if (id) stopPreview.mutate({ previewId: id }); + }, + isStarting: startPreview.isPending, + isStopping: stopPreview.isPending, + } + : null; + const approveMutation = trpc.approvePhaseReview.useMutation({ onSuccess: () => { setStatus("approved"); toast.success("Phase approved and merged"); phasesQuery.refetch(); }, - onError: (err) => { - toast.error(err.message); - }, + onError: (err) => toast.error(err.message), }); + // Determine which diff to display + const activeDiffRaw = selectedCommit + ? commitDiffQuery.data?.rawDiff + : diffQuery.data?.rawDiff; + const files = useMemo(() => { - if (!diffQuery.data?.rawDiff) return []; - return parseUnifiedDiff(diffQuery.data.rawDiff); - }, [diffQuery.data?.rawDiff]); + if (!activeDiffRaw) return []; + return parseUnifiedDiff(activeDiffRaw); + }, [activeDiffRaw]); + + const isDiffLoading = selectedCommit + ? commitDiffQuery.isLoading + : diffQuery.isLoading; const handleAddComment = useCallback( (filePath: string, lineNumber: number, lineType: DiffLine["type"], body: string) => { @@ -68,18 +155,18 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) { setComments((prev) => [...prev, newComment]); toast.success("Comment added"); }, - [] + [], ); const handleResolveComment = useCallback((commentId: string) => { setComments((prev) => - prev.map((c) => (c.id === commentId ? { ...c, resolved: true } : c)) + prev.map((c) => (c.id === commentId ? { ...c, resolved: true } : c)), ); }, []); const handleUnresolveComment = useCallback((commentId: string) => { setComments((prev) => - prev.map((c) => (c.id === commentId ? { ...c, resolved: false } : c)) + prev.map((c) => (c.id === commentId ? { ...c, resolved: false } : c)), ); }, []); @@ -102,6 +189,15 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) { } }, []); + const handlePhaseSelect = useCallback((id: string) => { + setSelectedPhaseId(id); + setSelectedCommit(null); + setStatus("pending"); + setComments([]); + }, []); + + const unresolvedCount = comments.filter((c) => !c.resolved).length; + if (pendingReviewPhases.length === 0) { return (
@@ -110,58 +206,58 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) { ); } - const activePhaseName = diffQuery.data?.phaseName ?? pendingReviewPhases.find(p => p.id === activePhaseId)?.name ?? "Phase"; - const sourceBranch = diffQuery.data?.sourceBranch ?? ""; + const activePhaseName = + diffQuery.data?.phaseName ?? + pendingReviewPhases.find((p) => p.id === activePhaseId)?.name ?? + "Phase"; + + // All files from the full branch diff (for sidebar file list) + const allFiles = useMemo(() => { + if (!diffQuery.data?.rawDiff) return []; + return parseUnifiedDiff(diffQuery.data.rawDiff); + }, [diffQuery.data?.rawDiff]); return ( -
- {/* Phase selector if multiple pending */} - {pendingReviewPhases.length > 1 && ( -
- {pendingReviewPhases.map((phase) => ( - - ))} -
- )} +
+ {/* Header: phase selector + toolbar */} + ({ id: p.id, name: p.name }))} + activePhaseId={activePhaseId} + onPhaseSelect={handlePhaseSelect} + phaseName={activePhaseName} + sourceBranch={sourceBranch} + targetBranch={diffQuery.data?.targetBranch ?? commitsQuery.data?.targetBranch ?? ""} + files={allFiles} + status={status} + unresolvedCount={unresolvedCount} + onApprove={handleApprove} + onRequestChanges={handleRequestChanges} + preview={previewState} + /> - {/* Preview panel */} - {firstProjectId && sourceBranch && ( - - )} + {/* Commit navigation strip */} + - {diffQuery.isLoading ? ( -
+ {/* Main content area */} + {isDiffLoading ? ( +
+ Loading diff...
) : ( -
+
{/* Left: Diff */} -
-
-

Review: {activePhaseName}

- - {comments.filter((c) => !c.resolved).length} unresolved thread - {comments.filter((c) => !c.resolved).length !== 1 ? "s" : ""} - -
+
{files.length === 0 ? ( -
- No changes in this phase +
+ {selectedCommit + ? "No changes in this commit" + : "No changes in this phase"}
) : ( {/* Right: Sidebar */} -
+
diff --git a/apps/web/src/components/review/types.ts b/apps/web/src/components/review/types.ts index a9adde3..a450d90 100644 --- a/apps/web/src/components/review/types.ts +++ b/apps/web/src/components/review/types.ts @@ -35,6 +35,17 @@ export interface ReviewComment { export type ReviewStatus = "pending" | "approved" | "changes_requested"; +export interface CommitInfo { + hash: string; + shortHash: string; + message: string; + author: string; + date: string; + filesChanged: number; + insertions: number; + deletions: number; +} + export interface ReviewSummary { id: string; title: string; diff --git a/docs/frontend.md b/docs/frontend.md index 6828b53..9d952f7 100644 --- a/docs/frontend.md +++ b/docs/frontend.md @@ -111,10 +111,12 @@ The initiative detail page has three tabs managed via local state (not URL param ### Review Components (`src/components/review/`) | Component | Purpose | |-----------|---------| -| `ReviewTab` | Review tab container with diff viewer and preview integration | -| `ReviewSidebar` | Review info, actions, file list, comment summary | +| `ReviewTab` | Review tab container — orchestrates header, commit nav, diff, sidebar, and preview | +| `ReviewHeader` | Consolidated toolbar: phase selector pills, branch info, stats, preview controls, approve/reject actions | +| `CommitNav` | Horizontal commit navigation strip — "All changes" + individual commit pills with stats | +| `ReviewSidebar` | File list with comment counts, dimmed files when viewing single commit | | `DiffViewer` | Unified diff renderer with inline comments | -| `PreviewPanel` | Docker preview status: building/running/failed with start/stop | +| `PreviewPanel` | Docker preview status: building/running/failed with start/stop (legacy, now integrated into ReviewHeader) | | `ProposalCard` | Individual proposal display | ### UI Primitives (`src/components/ui/`) diff --git a/docs/git-process-logging.md b/docs/git-process-logging.md index b1a7b20..659a192 100644 --- a/docs/git-process-logging.md +++ b/docs/git-process-logging.md @@ -43,6 +43,8 @@ Worktrees stored in `.cw-worktrees/` subdirectory of the repo. Each agent gets a | `deleteBranch(repoPath, branch)` | Delete local branch (no-op if missing) | | `branchExists(repoPath, branch)` | Check local branches | | `remoteBranchExists(repoPath, branch)` | Check remote tracking branches (`origin/`) | +| `listCommits(repoPath, base, head)` | List commits head has that base doesn't (with stats) | +| `diffCommit(repoPath, commitHash)` | Get unified diff for a single commit | `remoteBranchExists` is used by `registerProject` and `updateProject` to validate that a project's default branch actually exists in the cloned repository before saving. diff --git a/docs/server-api.md b/docs/server-api.md index b1fb9a5..295d386 100644 --- a/docs/server-api.md +++ b/docs/server-api.md @@ -109,6 +109,10 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE | listInitiativePhaseDependencies | query | All dependency edges | | getPhaseDependencies | query | What this phase depends on | | getPhaseDependents | query | What depends on this phase | +| getPhaseReviewDiff | query | Full branch diff for pending_review phase | +| getPhaseReviewCommits | query | List commits between initiative and phase branch | +| getCommitDiff | query | Diff for a single commit (by hash) in a phase | +| approvePhaseReview | mutation | Approve and merge phase branch | ### Phase Dispatch | Procedure | Type | Description |