chore: add dev branch policy to AGENTS.md

- Document that dev is the default working branch
- main is reserved for stable/production merges only
This commit is contained in:
Developer
2026-06-03 08:51:34 +00:00
parent b39d6ce5f4
commit 6ec5179408
4 changed files with 434 additions and 414 deletions
+7
View File
@@ -85,6 +85,13 @@ Before completion, report:
Do not claim completion without verification evidence. Do not claim completion without verification evidence.
## Git branch policy
- **Default working branch:** `dev` — all commits and pushes target `dev` unless the user explicitly requests otherwise.
- `main` is the stable/production branch; merge to `main` only when explicitly instructed.
- After committing, push to `origin/dev`.
- If `dev` does not exist locally, create it from `main` or fetch it from origin.
## Git workflow ## Git workflow
### Auto-commit on spec completion ### Auto-commit on spec completion
@@ -10,296 +10,299 @@ import { SyntaxHighlighter } from "./SyntaxHighlighter";
import { detectLanguage } from "../../../utils/language"; import { detectLanguage } from "../../../utils/language";
interface GitFileStatus { interface GitFileStatus {
modified: string[]; modified: string[];
added: string[]; added: string[];
deleted: string[]; deleted: string[];
untracked: string[]; untracked: string[];
} }
interface FileEditorProps { interface FileEditorProps {
projectId: string; projectId: string;
repoId: string; repoId: string;
gitStatus?: GitFileStatus | null; gitStatus?: GitFileStatus | null;
} }
export const FileEditor: React.FC<FileEditorProps> = ({ export const FileEditor: React.FC<FileEditorProps> = ({
projectId, projectId,
repoId, repoId,
gitStatus, gitStatus,
}) => { }) => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { user } = useAuth(); const { user } = useAuth();
const [mode, setMode] = useState<"view" | "edit">("view"); const [mode, setMode] = useState<"view" | "edit">("view");
const [content, setContent] = useState(">"); const [content, setContent] = useState(">");
const [originalContent, setOriginalContent] = useState(">"); const [originalContent, setOriginalContent] = useState(">");
const [language, setLanguage] = useState("plaintext"); const [language, setLanguage] = useState("plaintext");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [showCommitDialog, setShowCommitDialog] = useState(false); const [showCommitDialog, setShowCommitDialog] = useState(false);
const [isBinary, setIsBinary] = useState(false); const [isBinary, setIsBinary] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const handleDiscard = async () => { const handleDiscard = async () => {
if (!filePath) return; if (!filePath) return;
try { try {
const response = await apiClient.get( const response = await apiClient.get(
`/projects/${projectId}/repositories/${repoId}/files/content`, `/projects/${projectId}/repositories/${repoId}/files/content`,
{ {
params: { params: {
branch, branch,
path: filePath, path: filePath,
}, },
} },
); );
const data = response.data; const data = response.data;
if (data.is_binary) { if (data.is_binary) {
setIsBinary(true); setIsBinary(true);
setContent("Binary file - cannot display"); setContent("Binary file - cannot display");
setOriginalContent(""); setOriginalContent("");
} else { } else {
setIsBinary(false); setIsBinary(false);
setContent(data.content); setContent(data.content);
setOriginalContent(data.content); setOriginalContent(data.content);
} }
setMode("view"); setMode("view");
} catch { } catch {
setError("Failed to discard changes"); setError("Failed to discard changes");
} }
}; };
const branch = searchParams.get("branch") || "main"; const branch = searchParams.get("branch") || "main";
const filePath = searchParams.get("file"); const filePath = searchParams.get("file");
const fileStatus = gitStatus const fileStatus = gitStatus
? gitStatus.modified.includes(filePath || "") ? gitStatus.modified.includes(filePath || "")
? "modified" ? "modified"
: gitStatus.added.includes(filePath || "") : gitStatus.added.includes(filePath || "")
? "added" ? "added"
: gitStatus.deleted.includes(filePath || "") : gitStatus.deleted.includes(filePath || "")
? "deleted" ? "deleted"
: gitStatus.untracked.includes(filePath || "") : gitStatus.untracked.includes(filePath || "")
? "untracked" ? "untracked"
: undefined : undefined
: undefined; : undefined;
const loadFile = useCallback(async () => { const loadFile = useCallback(async () => {
if (!filePath) { if (!filePath) {
setContent(""); setContent("");
setOriginalContent(""); setOriginalContent("");
return; return;
} }
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const response = await apiClient.get( const response = await apiClient.get(
`/projects/${projectId}/repositories/${repoId}/files/content`, `/projects/${projectId}/repositories/${repoId}/files/content`,
{ {
params: { params: {
branch, branch,
path: filePath, path: filePath,
}, },
} },
); );
const data = response.data; const data = response.data;
if (data.is_binary) { if (data.is_binary) {
setIsBinary(true); setIsBinary(true);
setContent("Binary file - cannot display"); setContent("Binary file - cannot display");
setOriginalContent(""); setOriginalContent("");
} else { } else {
setIsBinary(false); setIsBinary(false);
setContent(data.content); setContent(data.content);
setOriginalContent(data.content); setOriginalContent(data.content);
setLanguage(detectLanguage(filePath)); setLanguage(detectLanguage(filePath));
} }
} catch { } catch {
setError("Failed to load file"); setError("Failed to load file");
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [projectId, repoId, branch, filePath]); }, [projectId, repoId, branch, filePath]);
useEffect(() => { useEffect(() => {
void loadFile(); void loadFile();
}, [loadFile]); }, [loadFile]);
const handleEdit = () => { const handleEdit = () => {
if (isBinary) return; if (isBinary) return;
setMode("edit"); setMode("edit");
}; };
const handleCancel = () => { const handleCancel = () => {
setContent(originalContent); setContent(originalContent);
setMode("view"); setMode("view");
setShowCommitDialog(false); setShowCommitDialog(false);
}; };
const handleSave = () => { const handleSave = () => {
if (content === originalContent) { if (content === originalContent) {
setMode("view"); setMode("view");
return; return;
} }
setShowCommitDialog(true); setShowCommitDialog(true);
}; };
const handleCommit = async (message: string) => { const handleCommit = async (message: string) => {
if (!filePath || !user) return; if (!filePath || !user) return;
setSaving(true); setSaving(true);
try { try {
await apiClient.post( await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/files/content`, `/projects/${projectId}/repositories/${repoId}/files/content`,
{ {
path: filePath, path: filePath,
branch, branch,
content, content,
commit_message: message, commit_message: message,
author_name: user.name || "User", author_name: user.name || "User",
author_email: user.email || "user@example.com", author_email: user.email || "user@example.com",
} },
); );
setOriginalContent(content); setOriginalContent(content);
setMode("view"); setMode("view");
setShowCommitDialog(false); setShowCommitDialog(false);
} catch { } catch {
setError("Failed to save changes"); setError("Failed to save changes");
} finally { } finally {
setSaving(false); setSaving(false);
} }
}; };
// Keyboard shortcuts // Keyboard shortcuts
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "e") { if ((e.ctrlKey || e.metaKey) && e.key === "e") {
e.preventDefault(); e.preventDefault();
if (mode === "view" && !isBinary) { if (mode === "view" && !isBinary) {
handleEdit(); handleEdit();
} else if (mode === "edit") { } else if (mode === "edit") {
handleCancel(); handleCancel();
} }
} }
if ((e.ctrlKey || e.metaKey) && e.key === "s") { if ((e.ctrlKey || e.metaKey) && e.key === "s") {
e.preventDefault(); e.preventDefault();
if (mode === "edit") { if (mode === "edit") {
handleSave(); handleSave();
} }
} }
}; };
window.addEventListener("keydown", handleKeyDown); window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown);
}, [mode, isBinary, content, originalContent]); }, [mode, isBinary, content, originalContent]);
if (!filePath) { if (!filePath) {
return ( return (
<div className="file-viewer-empty"> <div className="file-viewer-empty">
<p className="muted">Select a file to view its contents</p> <p className="muted">Select a file to view its contents</p>
</div> </div>
); );
} }
if (loading) return <p className="muted">Loading file...</p>; if (loading) return <p className="muted">Loading file...</p>;
if (error) return <p className="error-text">{error}</p>; if (error) return <p className="error-text">{error}</p>;
return ( return (
<div className={styles.fileEditor}> <div className={styles.fileEditor}>
<div className={styles.fileEditorToolbar}> <div className={styles.fileEditorToolbar}>
<div className="file-breadcrumbs"> <div className="file-breadcrumbs">
{filePath?.split("/").map((part, i, arr) => ( {filePath?.split("/").map((part, i, arr) => (
<span key={i}> <span key={i}>
{part} {part}
{i < arr.length - 1 && ( {i < arr.length - 1 && <span className="breadcrumb-sep">/</span>}
<span className="breadcrumb-sep">/</span> </span>
)} ))}
</span> </div>
))} <div className={styles.fileActions}>
</div> {fileStatus && (
<div className={styles.fileActions}> <span
{fileStatus && ( className={`git-status-badge ${fileStatus}`}
<span className={`git-status-badge ${fileStatus}`} title={fileStatus}> title={fileStatus}
{fileStatus === "modified" ? "M" : fileStatus === "added" ? "A" : fileStatus === "deleted" ? "D" : "?"} >
</span> {fileStatus === "modified"
)} ? "M"
{mode === "view" && !isBinary && ( : fileStatus === "added"
<button ? "A"
className="btn-primary" : fileStatus === "deleted"
onClick={handleEdit} ? "D"
type="button" : "?"}
> </span>
<Icon name="edit" size="sm" /> )}
Edit {mode === "view" && !isBinary && (
</button> <button className="btn-primary" onClick={handleEdit} type="button">
)} <Icon name="edit" size="sm" />
{mode === "edit" && ( Edit
<> </button>
<button )}
className="btn-primary" {mode === "edit" && (
onClick={handleSave} <>
disabled={content === originalContent || saving} <button
type="button" className="btn-primary"
> onClick={handleSave}
{saving ? ( disabled={content === originalContent || saving}
<> type="button"
<Icon name="loading" size="sm" /> >
Saving... {saving ? (
</> <>
) : ( <Icon name="loading" size="sm" />
<> Saving...
<Icon name="save" size="sm" /> </>
Save ) : (
</> <>
)} <Icon name="save" size="sm" />
</button> Save
<button </>
className="btn-secondary" )}
onClick={handleDiscard} </button>
type="button" <button
title="Revert to last committed version" className="btn-secondary"
> onClick={handleDiscard}
<Icon name="undo" size="sm" /> type="button"
Discard title="Revert to last committed version"
</button> >
<button <Icon name="undo" size="sm" />
className="btn-secondary" Discard
onClick={handleCancel} </button>
type="button" <button
> className="btn-secondary"
<Icon name="cancel" size="sm" /> onClick={handleCancel}
Cancel type="button"
</button> >
</> <Icon name="cancel" size="sm" />
)} Cancel
</div> </button>
</div> </>
)}
</div>
</div>
<div className={styles.fileEditorContent}> <div className={styles.fileEditorContent}>
{mode === "view" && ( {mode === "view" && (
<SyntaxHighlighter <SyntaxHighlighter
code={content} code={content}
language={language} language={language}
showLineNumbers={!isBinary} showLineNumbers={!isBinary}
/> />
)} )}
{mode === "edit" && ( {mode === "edit" && (
<CodeEditor <CodeEditor
value={content} value={content}
onChange={setContent} onChange={setContent}
language={language} language={language}
/> />
)} )}
</div> </div>
<CommitDialog <CommitDialog
isOpen={showCommitDialog} isOpen={showCommitDialog}
filePath={filePath} filePath={filePath}
originalContent={originalContent} originalContent={originalContent}
newContent={content} newContent={content}
onCommit={handleCommit} onCommit={handleCommit}
onCancel={() => setShowCommitDialog(false)} onCancel={() => setShowCommitDialog(false)}
/> />
</div> </div>
); );
}; };
+154 -148
View File
@@ -1,167 +1,173 @@
import React from "react"; import React from "react";
import { import {
House, House,
Folder, Folder,
GitBranch, GitBranch,
Gear, Gear,
User, User,
SignOut, SignOut,
Plus, Plus,
PencilSimple, PencilSimple,
Trash, Trash,
FloppyDisk, FloppyDisk,
X, X,
ArrowsClockwise, ArrowsClockwise,
Copy, Copy,
MagnifyingGlass, MagnifyingGlass,
List, List,
Check, Check,
Warning, Warning,
Info, Info,
Spinner, Spinner,
GitCommit, GitCommit,
GitMerge, GitMerge,
ClockCounterClockwise, ClockCounterClockwise,
ArrowDown, ArrowDown,
ArrowUp, ArrowUp,
File, File,
FileText, FileText,
Image, Image,
Binary, Binary,
Code, Code,
ArrowSquareOut, ArrowSquareOut,
Play, Play,
Stop, Stop,
Terminal, Terminal,
ArrowLeft, ArrowLeft,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
export type IconName = export type IconName =
| "dashboard" | "dashboard"
| "projects" | "projects"
| "repositories" | "repositories"
| "settings" | "settings"
| "profile" | "profile"
| "logout" | "logout"
| "add" | "add"
| "edit" | "edit"
| "delete" | "delete"
| "save" | "save"
| "cancel" | "cancel"
| "refresh" | "refresh"
| "copy" | "copy"
| "search" | "search"
| "menu" | "menu"
| "close" | "close"
| "success" | "success"
| "error" | "error"
| "warning" | "warning"
| "info" | "info"
| "loading" | "loading"
| "branch" | "branch"
| "commit" | "commit"
| "merge" | "merge"
| "history" | "history"
| "pull" | "pull"
| "push" | "push"
| "fetch" | "fetch"
| "file" | "file"
| "folder" | "folder"
| "code" | "code"
| "document" | "document"
| "image" | "image"
| "binary" | "binary"
| "external" | "external"
| "play" | "play"
| "stop" | "stop"
| "terminal" | "terminal"
| "arrow-left" | "arrow-left"
| "undo"; | "undo";
const iconMap: Record<IconName, React.ComponentType<{ size?: number | string; weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone" }>> = { const iconMap: Record<
dashboard: House, IconName,
projects: Folder, React.ComponentType<{
repositories: GitBranch, size?: number | string;
settings: Gear, weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone";
profile: User, }>
logout: SignOut, > = {
add: Plus, dashboard: House,
edit: PencilSimple, projects: Folder,
delete: Trash, repositories: GitBranch,
save: FloppyDisk, settings: Gear,
cancel: X, profile: User,
refresh: ArrowsClockwise, logout: SignOut,
copy: Copy, add: Plus,
search: MagnifyingGlass, edit: PencilSimple,
menu: List, delete: Trash,
close: X, save: FloppyDisk,
success: Check, cancel: X,
error: X, refresh: ArrowsClockwise,
warning: Warning, copy: Copy,
info: Info, search: MagnifyingGlass,
loading: Spinner, menu: List,
branch: GitBranch, close: X,
commit: GitCommit, success: Check,
merge: GitMerge, error: X,
history: ClockCounterClockwise, warning: Warning,
pull: ArrowDown, info: Info,
push: ArrowUp, loading: Spinner,
fetch: ArrowsClockwise, branch: GitBranch,
file: File, commit: GitCommit,
folder: Folder, merge: GitMerge,
code: Code, history: ClockCounterClockwise,
document: FileText, pull: ArrowDown,
image: Image, push: ArrowUp,
binary: Binary, fetch: ArrowsClockwise,
external: ArrowSquareOut, file: File,
play: Play, folder: Folder,
stop: Stop, code: Code,
terminal: Terminal, document: FileText,
"arrow-left": ArrowLeft, image: Image,
undo: ClockCounterClockwise, binary: Binary,
external: ArrowSquareOut,
play: Play,
stop: Stop,
terminal: Terminal,
"arrow-left": ArrowLeft,
undo: ClockCounterClockwise,
}; };
export interface IconProps { export interface IconProps {
name: IconName; name: IconName;
size?: "sm" | "md" | "lg" | "xl"; size?: "sm" | "md" | "lg" | "xl";
color?: string; color?: string;
weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone"; weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone";
className?: string; className?: string;
ariaLabel?: string; ariaLabel?: string;
} }
const sizeMap: Record<NonNullable<IconProps["size"]>, number> = { const sizeMap: Record<NonNullable<IconProps["size"]>, number> = {
sm: 16, sm: 16,
md: 20, md: 20,
lg: 24, lg: 24,
xl: 32, xl: 32,
}; };
export const Icon: React.FC<IconProps> = ({ export const Icon: React.FC<IconProps> = ({
name, name,
size = "md", size = "md",
color, color,
weight = "regular", weight = "regular",
className, className,
ariaLabel, ariaLabel,
}) => { }) => {
const IconComponent = iconMap[name]; const IconComponent = iconMap[name];
const sizeValue = sizeMap[size]; const sizeValue = sizeMap[size];
if (!IconComponent) { if (!IconComponent) {
console.warn(`Icon "${name}" not found`); console.warn(`Icon "${name}" not found`);
return null; return null;
} }
return ( return (
<span <span
className={`icon icon-${size}${className ? ` ${className}` : ""}`} className={`icon icon-${size}${className ? ` ${className}` : ""}`}
style={{ color }} style={{ color }}
aria-label={ariaLabel} aria-label={ariaLabel}
aria-hidden={!ariaLabel} aria-hidden={!ariaLabel}
role="img" role="img"
> >
<IconComponent size={sizeValue} weight={weight} /> <IconComponent size={sizeValue} weight={weight} />
</span> </span>
); );
}; };
+5 -1
View File
@@ -198,7 +198,11 @@ export const RepoWorkspace = () => {
)} )}
<main className="workspace-main"> <main className="workspace-main">
{selectedRepoId && ( {selectedRepoId && (
<FileEditor projectId={projectId!} repoId={selectedRepoId} gitStatus={gitStatus} /> <FileEditor
projectId={projectId!}
repoId={selectedRepoId}
gitStatus={gitStatus}
/>
)} )}
</main> </main>
</div> </div>