Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ node_modules
dist
.next
.turbo
.codegraph/
.DS_Store
.env
.env.local
coverage
artifacts
sandboxes
data/*.json
data/codegraph/
*.log
*.tsbuildinfo
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The idea is simple: humans write the intent, AI handles the code path. Not as a

- **Issue to PRD to PR**: convert GitHub Issues into structured PRDs, implementation plans, verified diffs, and draft PRs.
- **Zero-code operator flow**: product or engineering leads describe what should change; agents handle the coding loop.
- **Repository intelligence**: build a Repo Navigation Graph and evidence-backed ContextPack before editing.
- **Repository intelligence**: initialize or refresh the upstream CodeGraph index, then build an evidence-backed ContextPack before editing.
- **Isolated execution**: each Issue runs in its own sandbox, branch, artifact set, and quality gate trail.
- **Human control**: PRD approval, policy gates, review subagents, and memory update proposals keep the system inspectable.
- **Local verification built in**: generated PRs include checkout, install, test, and run instructions.
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Code零 是一个面向 GitHub 的工程 Agent 平台,用来把产品意图自

- **Issue 到 PRD 到 PR**:把 GitHub Issue 转成结构化 PRD、实现计划、验证后的 diff 和 draft PR。
- **零代码操作流**:产品或工程负责人描述“要改什么”,Agent 处理代码实现闭环。
- **仓库智能理解**:修改前构建 Repo Navigation Graph 和带证据链的 ContextPack。
- **仓库智能理解**:修改前初始化或刷新上游 CodeGraph 索引,并构建带证据链的 ContextPack。
- **隔离执行**:每个 Issue 拥有独立沙箱、独立分支、独立产物和独立质量门禁记录。
- **人可控**:PRD 审批、Policy 门禁、Review subagent 和 memory update proposal 让每一步可检查。
- **PR 可本地验证**:生成的 PR 自动包含 checkout、安装、测试和启动验证指令。
Expand Down
81 changes: 81 additions & 0 deletions apps/api/src/routes/task-queue-summary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { RepositoryConfig } from "@agent/config";
import { computeRepositoryQueueState } from "@agent/orchestrator";
import type { Task } from "@agent/shared";

export type RepositoryQueueSummary = {
id: string;
owner: string;
repo: string;
fullName: string;
configured: boolean;
maxConcurrentIssues: number;
runningCount: number;
queuedCount: number;
reviewCount: number;
blockedCount: number;
completedCount: number;
totalCount: number;
availableSlots: number;
tasks: Task[];
};

export function buildRepositoryQueueSummaries(tasks: Task[], repositories: RepositoryConfig[]): RepositoryQueueSummary[] {
const summaries = new Map<string, RepositoryQueueSummary>();

for (const repository of repositories) {
const key = repositoryKey(repository.github_owner, repository.github_repo);
const state = computeRepositoryQueueState(tasks, repository);
summaries.set(key, {
id: repository.id,
owner: repository.github_owner,
repo: repository.github_repo,
fullName: `${repository.github_owner}/${repository.github_repo}`,
configured: true,
...state,
tasks: []
});
}

for (const task of tasks) {
const key = repositoryKey(task.issue.owner, task.issue.repo);
let summary = summaries.get(key);

if (!summary) {
const inferredRepository = {
id: key,
github_owner: task.issue.owner,
github_repo: task.issue.repo,
queue: {
max_concurrent_issues: 1
}
};
const state = computeRepositoryQueueState(tasks, inferredRepository);
summary = {
id: key,
owner: task.issue.owner,
repo: task.issue.repo,
fullName: `${task.issue.owner}/${task.issue.repo}`,
configured: false,
...state,
tasks: []
};
summaries.set(key, summary);
}

summary.tasks.push(task);
}

return [...summaries.values()].sort((left, right) => {
const activityDelta = right.runningCount + right.queuedCount - (left.runningCount + left.queuedCount);

if (activityDelta !== 0) {
return activityDelta;
}

return left.fullName.localeCompare(right.fullName);
});
}

function repositoryKey(owner: string, repo: string): string {
return `${owner}/${repo}`;
}
83 changes: 2 additions & 81 deletions apps/api/src/routes/tasks.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type { FastifyInstance } from "fastify";
import { z } from "zod";
import type { RepositoryConfig } from "@agent/config";
import { buildTaskTrace } from "@agent/observability";
import { computeRepositoryQueueState, transitionTask } from "@agent/orchestrator";
import { transitionTask } from "@agent/orchestrator";
import { createTaskEvent } from "@agent/persistence";
import type { Task } from "@agent/shared";
import { createAndEnqueueTask, enqueueIssueWorkflow, getServices } from "../services/task-services.js";
import { buildRepositoryQueueSummaries } from "./task-queue-summary.js";

const importIssueSchema = z.object({
owner: z.string().min(1),
Expand Down Expand Up @@ -101,81 +100,3 @@ export async function registerTaskRoutes(app: FastifyInstance): Promise<void> {
return { task: updated };
});
}

type RepositoryQueueSummary = {
id: string;
owner: string;
repo: string;
fullName: string;
configured: boolean;
maxConcurrentIssues: number;
runningCount: number;
queuedCount: number;
reviewCount: number;
blockedCount: number;
completedCount: number;
totalCount: number;
availableSlots: number;
tasks: Task[];
};

function buildRepositoryQueueSummaries(tasks: Task[], repositories: RepositoryConfig[]): RepositoryQueueSummary[] {
const summaries = new Map<string, RepositoryQueueSummary>();

for (const repository of repositories) {
const key = repositoryKey(repository.github_owner, repository.github_repo);
const state = computeRepositoryQueueState(tasks, repository);
summaries.set(key, {
id: repository.id,
owner: repository.github_owner,
repo: repository.github_repo,
fullName: `${repository.github_owner}/${repository.github_repo}`,
configured: true,
...state,
tasks: []
});
}

for (const task of tasks) {
const key = repositoryKey(task.issue.owner, task.issue.repo);
let summary = summaries.get(key);

if (!summary) {
const inferredRepository = {
id: key,
github_owner: task.issue.owner,
github_repo: task.issue.repo,
queue: {
max_concurrent_issues: 1
}
};
const state = computeRepositoryQueueState(tasks, inferredRepository);
summary = {
id: key,
owner: task.issue.owner,
repo: task.issue.repo,
fullName: `${task.issue.owner}/${task.issue.repo}`,
configured: false,
...state,
tasks: []
};
summaries.set(key, summary);
}

summary.tasks.push(task);
}

return [...summaries.values()].sort((left, right) => {
const activityDelta = right.runningCount + right.queuedCount - (left.runningCount + left.queuedCount);

if (activityDelta !== 0) {
return activityDelta;
}

return left.fullName.localeCompare(right.fullName);
});
}

function repositoryKey(owner: string, repo: string): string {
return `${owner}/${repo}`;
}
90 changes: 90 additions & 0 deletions apps/web/src/features/settings/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type {
ConfigResponse,
ConfigSection,
ConfigSectionName,
ProviderValidationResponse,
RepositoryRuntimeSettingsInput,
ValidationResponse
} from "./types";

export const apiBaseUrl = () => process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";

export async function fetchConfig(): Promise<ConfigResponse> {
const response = await fetch(`${apiBaseUrl()}/settings/config`, { cache: "no-store" });

if (!response.ok) {
throw new Error("Failed to load settings");
}

return (await response.json()) as ConfigResponse;
}

export async function validateConfig(input: { section: ConfigSectionName; content: string }): Promise<ValidationResponse> {
const response = await fetch(`${apiBaseUrl()}/settings/config/${input.section}/validate`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ content: input.content })
});
const body = (await response.json()) as ValidationResponse;

if (!response.ok) {
return { ...body, section: input.section, valid: false };
}

return body;
}

export async function saveConfig(input: { section: ConfigSectionName; content: string }): Promise<ConfigSection> {
const response = await fetch(`${apiBaseUrl()}/settings/config/${input.section}`, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ content: input.content })
});

if (!response.ok) {
const body = (await response.json().catch(() => ({}))) as { message?: string };
throw new Error(body.message ?? "Failed to save config");
}

return (await response.json()) as ConfigSection;
}

export async function validateProviderConnection(input: { content: string; providerId: string; apiKey?: string }): Promise<ProviderValidationResponse> {
const response = await fetch(`${apiBaseUrl()}/settings/providers/validate`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(input)
});
const body = (await response.json().catch(() => ({}))) as Partial<ProviderValidationResponse> & { message?: string };

if (!response.ok) {
return {
providerId: input.providerId,
valid: false,
message: body.message ?? "Provider validation failed"
};
}

return body as ProviderValidationResponse;
}

export async function updateRepositoryRuntimeSettings(input: RepositoryRuntimeSettingsInput): Promise<ConfigSection> {
const response = await fetch(`${apiBaseUrl()}/settings/repositories/${encodeURIComponent(input.repositoryId)}/runtime`, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({
triggerMode: input.triggerMode,
mention: input.mention,
maxConcurrentIssues: input.maxConcurrentIssues,
allowedPermissions: input.allowedPermissions,
blockedPermissions: input.blockedPermissions
})
});

if (!response.ok) {
const body = (await response.json().catch(() => ({}))) as { message?: string };
throw new Error(body.message ?? "Failed to update repository settings");
}

return (await response.json()) as ConfigSection;
}
5 changes: 5 additions & 0 deletions apps/web/src/features/settings/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { ConfigSectionName, ToolPermissionLevel, TriggerMode } from "./types";

export const triggerModes: TriggerMode[] = ["auto", "mention", "label", "manual", "disabled"];
export const permissionLevels: ToolPermissionLevel[] = ["read", "safe_write", "repo_write", "external_write", "dangerous"];
export const orderedSections: ConfigSectionName[] = ["agents", "repositories", "tools", "policies", "sandbox"];
30 changes: 30 additions & 0 deletions apps/web/src/features/settings/section-meta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Bot, GitBranch, Server, Shield, Wrench } from "lucide-react";
import type { ConfigSectionName } from "./types";

export const sectionMeta: Record<ConfigSectionName, { title: string; icon: React.ReactNode; description: string }> = {
agents: {
title: "Model Providers & Agents",
icon: <Bot size={18} aria-hidden />,
description: "Configure DeepSeek, Qwen, OpenAI-compatible providers and choose which provider each workflow step uses."
},
repositories: {
title: "GitHub Repositories",
icon: <GitBranch size={18} aria-hidden />,
description: "Configure repository trigger mode, quality gates, frontend screenshots and PR behavior."
},
tools: {
title: "Tool Permissions",
icon: <Wrench size={18} aria-hidden />,
description: "Configure tool permissions and timeout boundaries used by Tool Gateway."
},
policies: {
title: "Policy Guardrails",
icon: <Shield size={18} aria-hidden />,
description: "Configure path, command, tool and permission policies for block or approval decisions."
},
sandbox: {
title: "Sandbox Runtime",
icon: <Server size={18} aria-hidden />,
description: "Configure Docker/worktree sandbox mode, image, network allowlist and runtime limits."
}
};
Loading
Loading