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
27 changes: 7 additions & 20 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,20 @@ permissions:
contents: read

jobs:
core:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
cache-dependency-path: |
package-lock.json
desktop/package-lock.json
- run: npm ci
- run: npm run check
- run: npm audit --omit=dev

desktop:
runs-on: ubuntu-latest
defaults:
run:
working-directory: desktop
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
cache-dependency-path: desktop/package-lock.json
- run: npm ci
working-directory: desktop
- run: npm run typecheck
- run: npm audit --omit=dev
working-directory: desktop
28 changes: 28 additions & 0 deletions .github/workflows/dependency-audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Dependency Audit

on:
schedule:
- cron: "17 9 * * 1"
workflow_dispatch:

permissions:
contents: read

jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
cache-dependency-path: |
package-lock.json
desktop/package-lock.json
- run: npm ci
- run: npm audit --omit=dev
- run: npm ci
working-directory: desktop
- run: npm audit --omit=dev
working-directory: desktop
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Coding agents are effective inside one task and one checkout. The harder problem
- manager-owned commits retained under `refs/codepilot/runs/...`
- collision detection, integration branches, repository validation, and deterministic review
- Git-native task provenance and generated GitHub pull-request descriptions
- an Electron desktop control plane for projects, task queues, knowledge, and live agent events
- an Electron desktop control plane with local/remote Git onboarding, project-scoped PM threads, task queues, knowledge, and live agent events

## Trust boundary

Expand All @@ -30,6 +30,7 @@ codePilot filters parent conversation identifiers and common unrelated secrets f

- Node.js 22+
- Git
- GitHub CLI for repository cloning and pull-request publication
- Codex CLI and/or Claude Code, already installed and authenticated
- Windows, macOS, or Linux for the CLI; the packaged desktop flow is currently Windows-first

Expand Down Expand Up @@ -74,6 +75,10 @@ npm run desktop:dev

Build the Windows package with `npm run desktop:build`; output is written under `desktop/out/`. Automatic task deployment is off by default.

The Projects screen can register an existing local Git checkout or clone an HTTPS/SSH remote into the configured projects directory. Credentials are never accepted in repository URLs; Git's configured credential helper or SSH agent owns authentication. After registration, onboarding asks the operator to review the App Core start command, repository-relative working directory, and local URL before Project Manager begins; this step saves configuration but never launches the app. Each registered repository has its own Project Manager thread, conversation, backlog, knowledge, and run database. Turns are serialized within one project while different projects can think and execute concurrently, even as the desktop switches between them. Removing a project only unregisters it from the workspace; its repository and `.codepilot` data remain on disk so it can be onboarded again later.

Settings includes a GitHub connection panel backed by the official `gh` browser login. GitHub CLI stores the credential in the operating-system credential store; codePilot receives only redacted account/status metadata. GitHub writes require both the global **Allow GitHub write operations** switch and an explicit **Create GitHub pull request** selection for that deployment. Today those writes are limited to pushing the generated integration branch and opening its pull request—codePilot does not merge, delete, change repository settings, or grant access.

The older local Next.js dashboard is deliberately absent from the public tree because it contains a separately licensed UI kit. Electron is the supported public UI.

## Worker configuration
Expand Down
12 changes: 6 additions & 6 deletions desktop/src/active-agents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ChevronDownIcon, FolderOpenIcon, StopCircleIcon } from "@heroicons/reac
import type { ActiveAgent } from "./types";
import { collapseLifecycleEvents, EventView } from "./event-view";

export function ActiveAgents() {
export function ActiveAgents({ projectId }: { projectId: string }) {
const [agents, setAgents] = useState<ActiveAgent[]>([]),
[expanded, setExpanded] = useState<Set<string>>(new Set()),
[messages, setMessages] = useState<Record<string, string>>({}),
Expand All @@ -15,7 +15,7 @@ export function ActiveAgents() {
logs = useRef(new Map<string, HTMLDivElement>());
async function refresh() {
try {
const next = await window.codepilot.getActiveAgents();
const next = await window.codepilot.getActiveAgents(projectId);
setAgents(next);
setExpanded((current) => {
const copy = new Set(current);
Expand All @@ -34,7 +34,7 @@ export function ActiveAgents() {
void refresh();
const timer = setInterval(() => void refresh(), 1200);
return () => clearInterval(timer);
}, []);
}, [projectId]);
useEffect(() => {
const frame = requestAnimationFrame(() => {
for (const [taskId, element] of logs.current)
Expand All @@ -55,7 +55,7 @@ export function ActiveAgents() {
setSending(agent.taskId);
setError("");
try {
setAgents(await window.codepilot.guideAgent(agent.taskId, message));
setAgents(await window.codepilot.guideAgent(projectId, agent.taskId, message));
setMessages((current) => ({ ...current, [agent.taskId]: "" }));
} catch (cause) {
setError(cause instanceof Error ? cause.message : String(cause));
Expand All @@ -67,7 +67,7 @@ export function ActiveAgents() {
setInterrupting(agent.taskId);
setError("");
try {
const result = await window.codepilot.interruptAgent(agent.taskId);
const result = await window.codepilot.interruptAgent(projectId, agent.taskId);
setFeedback(result.feedback);
setAgents(result.agents);
} catch (cause) {
Expand All @@ -90,7 +90,7 @@ export function ActiveAgents() {
</p>
</div>
<div className="flex gap-2">
<button onClick={() => void window.codepilot.openProjectDirectory()} className="flex items-center gap-2 rounded-lg border border-white/10 px-3 py-2 text-xs text-zinc-300 hover:bg-white/5"><FolderOpenIcon className="size-4" />Open project folder</button>
<button onClick={() => void window.codepilot.openProjectDirectory(projectId)} className="flex items-center gap-2 rounded-lg border border-white/10 px-3 py-2 text-xs text-zinc-300 hover:bg-white/5"><FolderOpenIcon className="size-4" />Open project folder</button>
{agents.length > 1 && <>
<button
onClick={() =>
Expand Down
19 changes: 12 additions & 7 deletions desktop/src/agents.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import { PlayIcon } from "@heroicons/react/24/outline";
import type { AgentRunResult, DesktopState, PMState, PMTask } from "./types";
export function Agents({ onState }: { onState(state: DesktopState): void }) {
export function Agents({ projectId, onState }: { projectId: string; onState(state: DesktopState): void }) {
const [objective, setObjective] = useState(""),
[codex, setCodex] = useState(true),
[claude, setClaude] = useState(false),
Expand All @@ -13,11 +13,12 @@ export function Agents({ onState }: { onState(state: DesktopState): void }) {
[error, setError] = useState(""),
[results, setResults] = useState<AgentRunResult[]>([]);
const [pmState, setPMState] = useState<PMState>();
const [githubReady, setGitHubReady] = useState(false);
const [showAllTasks, setShowAllTasks] = useState(false);
const [poofing, setPoofing] = useState<Set<string>>(() => new Set());
useEffect(() => { void window.codepilot.getGlobalSettings().then((settings) => { setCodex(settings.defaultProvider === "codex"); setClaude(settings.defaultProvider === "claude"); }); }, []);
useEffect(() => { let active = true; const refresh = () => void window.codepilot.getPM().then((next) => active && setPMState(next)); refresh(); const timer = window.setInterval(refresh, 2000); return () => { active = false; window.clearInterval(timer); }; }, []);
async function deployTask(task: PMTask) { setError(""); try { setPMState(await window.codepilot.setPMTaskStatus(task.id, "launched")); setPoofing((items) => new Set(items).add(task.id)); await new Promise((resolve) => window.setTimeout(resolve, 900)); const result = await window.codepilot.runAgents({ objective: `${task.title}\n\ncodePilot PM task: ${task.id}\n\n${task.description}\n\nAcceptance criteria:\n${task.acceptanceCriteria.map((item) => `- ${item}`).join("\n")}`, providers: [task.recommendedProvider === "claude" ? "claude" : "codex"], retries: 0, timeoutMinutes: 30, validationCommands: [], integrate, createPullRequest, dryRun }); setResults((current) => [result, ...current].slice(0, 5)); const nextStatus = result.status === "completed" ? "done" : "ready"; const summary = result.review.findings.length ? result.review.findings.join(" ") : `Task did not advance because the agent run returned ${result.status} with review verdict ${result.review.verdict}.`; setPMState(await window.codepilot.setPMTaskStatus(task.id, nextStatus, summary)); } catch (cause) { const reason = cause instanceof Error ? cause.message : String(cause); setError(reason); try { setPMState(await window.codepilot.setPMTaskStatus(task.id, "ready", `Task did not advance because deployment failed: ${reason}`)); } catch {} } finally { setPoofing((items) => { const next = new Set(items); next.delete(task.id); return next; }); } }
useEffect(() => { void Promise.all([window.codepilot.getGlobalSettings(), window.codepilot.getGitHubStatus()]).then(([settings, status]) => { setCodex(settings.defaultProvider === "codex"); setClaude(settings.defaultProvider === "claude"); setGitHubReady(status.authenticated && settings.github.allowWrites); }); }, []);
useEffect(() => { let active = true; const refresh = () => void window.codepilot.getPM(projectId).then((next) => active && setPMState(next)); refresh(); const timer = window.setInterval(refresh, 2000); return () => { active = false; window.clearInterval(timer); }; }, [projectId]);
async function deployTask(task: PMTask) { setError(""); try { setPMState(await window.codepilot.setPMTaskStatus(projectId, task.id, "launched")); setPoofing((items) => new Set(items).add(task.id)); await new Promise((resolve) => window.setTimeout(resolve, 900)); const result = await window.codepilot.runAgents({ projectId, objective: `${task.title}\n\ncodePilot PM task: ${task.id}\n\n${task.description}\n\nAcceptance criteria:\n${task.acceptanceCriteria.map((item) => `- ${item}`).join("\n")}`, providers: [task.recommendedProvider === "claude" ? "claude" : "codex"], retries: 0, timeoutMinutes: 30, validationCommands: [], integrate, createPullRequest, dryRun }); setResults((current) => [result, ...current].slice(0, 5)); const nextStatus = result.status === "completed" ? "done" : "ready"; const summary = result.review.findings.length ? result.review.findings.join(" ") : `Task did not advance because the agent run returned ${result.status} with review verdict ${result.review.verdict}.`; setPMState(await window.codepilot.setPMTaskStatus(projectId, task.id, nextStatus, summary)); } catch (cause) { const reason = cause instanceof Error ? cause.message : String(cause); setError(reason); try { setPMState(await window.codepilot.setPMTaskStatus(projectId, task.id, "ready", `Task did not advance because deployment failed: ${reason}`)); } catch {} } finally { setPoofing((items) => { const next = new Set(items); next.delete(task.id); return next; }); } }
async function launch() {
const requestObjective = objective.trim();
if (!requestObjective) return;
Expand All @@ -26,7 +27,7 @@ export function Agents({ onState }: { onState(state: DesktopState): void }) {
setValidation("");
try {
const [title, ...rest] = requestObjective.split(/\r?\n/);
const nextPM = await window.codepilot.createPMTask({ title: title.slice(0, 160), description: `${rest.join("\n").trim() || title}${validation.trim() ? `\n\nVerification criteria:\n${validation.trim()}` : ""}`, acceptanceCriteria: validation.split("\n").map((item) => item.trim()).filter(Boolean), recommendedProvider: codex ? "codex" : "claude" });
const nextPM = await window.codepilot.createPMTask(projectId, { title: title.slice(0, 160), description: `${rest.join("\n").trim() || title}${validation.trim() ? `\n\nVerification criteria:\n${validation.trim()}` : ""}`, acceptanceCriteria: validation.split("\n").map((item) => item.trim()).filter(Boolean), recommendedProvider: codex ? "codex" : "claude" });
setPMState(nextPM);
const created = [...nextPM.tasks].reverse().find((task) => task.title === title.slice(0, 160));
if (created) void deployTask(created);
Expand Down Expand Up @@ -118,8 +119,9 @@ export function Agents({ onState }: { onState(state: DesktopState): void }) {
set={setIntegrate}
/>
<Check
label="Create GitHub pull request"
label={githubReady ? "Create GitHub pull request" : "Connect GitHub in Settings to publish"}
checked={createPullRequest}
disabled={!githubReady}
set={(value) => { setCreatePullRequest(value); if (value) { setIntegrate(true); setDryRun(false); } }}
/>
<Check label="Dry run only" checked={dryRun} set={setDryRun} />
Expand Down Expand Up @@ -154,16 +156,19 @@ function Check({
label,
checked,
set,
disabled = false,
}: {
label: string;
checked: boolean;
set(value: boolean): void;
disabled?: boolean;
}) {
return (
<label className="mb-3 flex cursor-pointer items-center justify-between text-sm text-zinc-300">
<label className={`mb-3 flex items-center justify-between text-sm text-zinc-300 ${disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"}`}>
<span>{label}</span>
<input
type="checkbox"
disabled={disabled}
checked={checked}
onChange={(e) => set(e.target.checked)}
className="size-4 accent-indigo-500"
Expand Down
Loading