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
4 changes: 3 additions & 1 deletion core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,10 +462,12 @@ export interface ReviewPolicyMet extends BaseEvent {
// Completion verification
// ---------------------------------------------------------------------------

export type ArtifactKind = "journal" | "code" | "pr";
export type ArtifactKind = "journal" | "code" | "pr" | "file";

export interface ArtifactEvidence {
kind: ArtifactKind;
path?: string;
sizeBytes?: number;
repo?: string;
branch?: string;
baseRef?: string | null;
Expand Down
37 changes: 36 additions & 1 deletion middle/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { loadConfig } from "./config.js";
import { createHttpServer, cleanupPendingDecompositions } from "./http.js";
import { exportState } from "./state-export.js";
import { initJournalRepo } from "./journal.js";
import { cleanupStaleWorktrees } from "./worktree.js";
import { cleanupStaleWorktrees, getWorktreePath, removeWorktree } from "./worktree.js";

// ---------------------------------------------------------------------------
// Lock file
Expand Down Expand Up @@ -102,6 +102,40 @@ function reconcileOrphanedTasks(core: OrchestrationCore): void {
}
}

function cleanupClosedTaskWorktrees(
core: OrchestrationCore,
config: ReturnType<typeof loadConfig>,
): void {
if (!fs.existsSync(config.worktreeBaseDir)) return;

let cleaned = 0;
const state = core.getState();
for (const entry of fs.readdirSync(config.worktreeBaseDir)) {
const match = /^(journal|code)-T(.+)$/.exec(entry);
if (!match) continue;

const kind = match[1] as "journal" | "code";
const taskId = match[2]!;
const task = state.tasks[taskId];
if (!task || task.terminal === null) continue;

if (kind === "journal") {
removeWorktree(config.journalRepoPath, getWorktreePath(config.worktreeBaseDir, taskId, "journal"));
cleaned++;
continue;
}

const targetRepo = (task.metadata["repo"] as string | undefined) || config.defaultCodeRepo || undefined;
if (!targetRepo) continue;
removeWorktree(targetRepo, getWorktreePath(config.worktreeBaseDir, taskId, "code"));
cleaned++;
}

if (cleaned > 0) {
console.log(`[daemon] Cleaned up ${cleaned} closed worktree(s)`);
}
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -182,6 +216,7 @@ async function main(): Promise<void> {
}
// Sweep pending decomposition sessions for tasks that went terminal or changed phase
cleanupPendingDecompositions(core);
cleanupClosedTaskWorktrees(core, config);
} catch (err) {
console.error("[daemon] Tick exception:", err);
}
Expand Down
63 changes: 50 additions & 13 deletions middle/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import type { Config } from "./config.js";
import { buildPrompt } from "./prompt.js";
import { commitJournal, createTaskBranch, getFailureSummaries, getJournalContent, mergeTaskBranch, taskBranch } from "./journal.js";
import { agentRole, loadRegistry, validateMetadataRoles, type Registry } from "./registry.js";
import { createWorktree, getWorktreePath } from "./worktree.js";
import { ensureWorktree, getWorktreePath, removeWorktree, writeTaskContext } from "./worktree.js";
import { verifyArtifacts } from "./finalize.js";
import { createOrFindPr } from "./github.js";

Expand Down Expand Up @@ -452,9 +452,7 @@ function ensureTaskWorkspaces(config: Config, task: Task): {
const jBranch = taskBranch(task.id);
const jPath = getWorktreePath(config.worktreeBaseDir, task.id, "journal");
createTaskBranch(config.journalRepoPath, task.id, task.parentId);
if (!fs.existsSync(jPath)) {
createWorktree(config.journalRepoPath, jPath, jBranch);
}
ensureWorktree(config.journalRepoPath, jPath, jBranch);
journalWorktree = jPath;
journalPath = `${path.join(jPath, "tasks", `T${task.id}`)}${path.sep}`;
} catch (err) {
Expand All @@ -471,9 +469,7 @@ function ensureTaskWorkspaces(config: Config, task: Task): {
const baseBranch = parentCodeBranch
?? (task.metadata["base_branch"] as string | undefined)
?? "main";
if (!fs.existsSync(cPath)) {
createWorktree(targetRepo, cPath, codeBranch, baseBranch);
}
ensureWorktree(targetRepo, cPath, codeBranch, baseBranch);
codeWorktree = cPath;
} catch (err) {
warnings.push(`code worktree setup failed: ${String(err)}`);
Expand All @@ -483,6 +479,41 @@ function ensureTaskWorkspaces(config: Config, task: Task): {
return { journalWorktree, journalPath, codeWorktree, warnings };
}

function writeWorkspaceTaskContext(
task: Task,
workspaces: {
journalWorktree: string | null;
journalPath: string | null;
codeWorktree: string | null;
},
sessionId: string,
fenceToken: number,
claimedAt: number,
): void {
const roots = [workspaces.journalWorktree, workspaces.codeWorktree].filter((root): root is string => !!root);
if (roots.length === 0 || !workspaces.journalPath) return;

writeTaskContext({
taskId: task.id,
phase: task.phase,
fenceToken,
sessionId,
journalPath: workspaces.journalPath,
codeWorktree: workspaces.codeWorktree,
claimedAt,
reviewNotes: [],
}, roots);
}

function cleanupTaskWorkspaces(config: Config, task: Task): void {
removeWorktree(config.journalRepoPath, getWorktreePath(config.worktreeBaseDir, task.id, "journal"));

const targetRepo = (task.metadata["repo"] as string | undefined) || config.defaultCodeRepo || undefined;
if (!targetRepo) return;

removeWorktree(targetRepo, getWorktreePath(config.worktreeBaseDir, task.id, "code"));
}

function ensureJournalWorktreeForTask(config: Config, task: Task): {
journalWorktree: string;
taskDir: string;
Expand Down Expand Up @@ -810,6 +841,7 @@ function handleClaimTask(

// The daemon owns worktree creation so the CLI can stay a pure HTTP client.
const workspace = ensureTaskWorkspaces(config, updated);
writeWorkspaceTaskContext(updated, workspace, sessionId, fenceToken, now);
const parentJournal = updated.parentId
? getJournalContent(config.journalRepoPath, updated.parentId)
: null;
Expand Down Expand Up @@ -1418,10 +1450,10 @@ function handleStatusUpdate(
return applyDoneTransition(core, config, task, fenceToken, ctx, now, b.evidence, b.stateRef);

case "reject":
return applyRejectTransition(core, task, fenceToken, ctx, now, b.evidence);
return applyRejectTransition(core, config, task, fenceToken, ctx, now, b.evidence);

case "blocked":
return applyBlockedTransition(core, task, now, b.blocker ?? b.evidence ?? "No reason provided", ctx);
return applyBlockedTransition(core, config, task, now, b.blocker ?? b.evidence ?? "No reason provided", ctx);

case "pending":
return applyChangesRequestedTransition(core, task, fenceToken, ctx, now, b.evidence);
Expand All @@ -1433,7 +1465,7 @@ function handleStatusUpdate(
return applyDecomposeTransition(core, task, fenceToken, ctx, now);

case "cancel":
return applyCancelTransition(core, task, now, b.evidence, ctx);
return applyCancelTransition(core, config, task, now, b.evidence, ctx);

default:
return { status: 400, body: { error: "unknown_status", message: `Unknown status: ${b.status}` } };
Expand Down Expand Up @@ -1811,14 +1843,14 @@ function applyDoneTransition(
taskId: task.id,
ts: ts + 1,
stateRef: stateRef ?? defaultStateRef(),
source: { type: "agent", id: ctx.agentId },
};
const err = submitOrError(core, completed);
if (err) return err;

notifyInformed(task, "✅ Done");
mergeCodeBranch(config, task);
maybeCreatePr(config, task, core);
cleanupTaskWorkspaces(config, task);

return {
status: 200,
Expand Down Expand Up @@ -1893,14 +1925,14 @@ function applyDoneTransition(
taskId: task.id,
ts: ts + 2,
stateRef: stateRef ?? defaultStateRef(),
source: { type: "agent", id: ctx.agentId },
};
err = submitOrError(core, completed);
if (err) return err;

notifyInformed(task, "✅ Done");
mergeCodeBranch(config, task);
maybeCreatePr(config, task, core);
cleanupTaskWorkspaces(config, task);

return {
status: 200,
Expand All @@ -1911,6 +1943,7 @@ function applyDoneTransition(
/** reject: review.active → failed */
function applyRejectTransition(
core: Core,
config: Config,
task: Task,
fenceToken: number,
ctx: AgentContext,
Expand Down Expand Up @@ -2007,12 +2040,12 @@ function applyRejectTransition(
whatWasLearned: "Reviewer rejected the submission. All review attempts exhausted.",
artifactRef: null,
},
source: { type: "agent", id: ctx.agentId },
};
err = submitOrError(core, failed);
if (err) return err;

notifyInformed(task, "❌ Rejected (final)", evidence ?? "Rejected by reviewer — all attempts exhausted");
cleanupTaskWorkspaces(config, task);

return {
status: 200,
Expand All @@ -2023,6 +2056,7 @@ function applyRejectTransition(
/** blocked: any active state → TaskBlocked */
function applyBlockedTransition(
core: Core,
config: Config,
task: Task,
ts: number,
blocker: string,
Expand Down Expand Up @@ -2051,6 +2085,7 @@ function applyBlockedTransition(
if (err) return err;

notifyInformed(task, "🚫 Blocked", blocker);
cleanupTaskWorkspaces(config, task);

return {
status: 200,
Expand All @@ -2063,6 +2098,7 @@ function applyBlockedTransition(
/** cancel: any non-terminal state -> TaskCanceled */
function applyCancelTransition(
core: Core,
config: Config,
task: Task,
ts: number,
evidence?: string,
Expand All @@ -2079,6 +2115,7 @@ function applyCancelTransition(

const err = submitOrError(core, canceled);
if (err) return err;
cleanupTaskWorkspaces(config, task);

return {
status: 200,
Expand Down
3 changes: 2 additions & 1 deletion middle/journal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ export function taskBranch(taskId: string): string {

function branchExists(repoPath: string, branch: string): boolean {
try {
git(repoPath, ["rev-parse", "--verify", `refs/heads/${branch}`]);
git(repoPath, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]);
return true;
} catch {
return false;
Expand All @@ -255,6 +255,7 @@ function git(cwd: string, args: string[]): string {
cwd,
encoding: "utf-8",
timeout: 10_000,
stdio: ["ignore", "pipe", "pipe"],
env: {
...process.env,
GIT_TERMINAL_PROMPT: "0",
Expand Down
Loading
Loading