Merge origin/main with local UI refactor integration

This commit is contained in:
2026-02-25 00:38:19 -05:00
42 changed files with 4886 additions and 188 deletions

View File

@@ -1,13 +1,20 @@
import { randomUUID } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { JSONFilePreset } from "lowdb/node";
import { SchemaDrivenExecutionEngine } from "../agents/orchestration.js";
import { parseAgentManifest, type AgentManifest } from "../agents/manifest.js";
import { FileSystemProjectContextStore } from "../agents/project-context.js";
import type {
ActorExecutionResult,
ActorExecutor,
PipelineAggregateStatus,
} from "../agents/pipeline.js";
import {
FileSystemSessionMetadataStore,
SessionWorktreeManager,
type SessionMetadata,
} from "../agents/session-lifecycle.js";
import { loadConfig, type AppConfig } from "../config.js";
import { parseEnvFile } from "../store/env-store.js";
import {
@@ -240,7 +247,18 @@ async function loadRuntimeConfig(envPath: string): Promise<Readonly<AppConfig>>
});
}
import { JSONFilePreset } from "lowdb/node";
function resolveRuntimePaths(input: {
workspaceRoot: string;
config: Readonly<AppConfig>;
}): {
stateRoot: string;
worktreeRoot: string;
} {
return {
stateRoot: resolve(input.workspaceRoot, input.config.orchestration.stateRoot),
worktreeRoot: resolve(input.workspaceRoot, input.config.provisioning.gitWorktree.rootDirectory),
};
}
async function writeRunMeta(input: {
stateRoot: string;
@@ -323,6 +341,103 @@ export class UiRunService {
this.envFilePath = resolve(this.workspaceRoot, input.envFilePath ?? ".env");
}
private async loadRuntime(): Promise<{
config: Readonly<AppConfig>;
stateRoot: string;
sessionStore: FileSystemSessionMetadataStore;
worktreeManager: SessionWorktreeManager;
}> {
const config = await loadRuntimeConfig(this.envFilePath);
const paths = resolveRuntimePaths({
workspaceRoot: this.workspaceRoot,
config,
});
return {
config,
stateRoot: paths.stateRoot,
sessionStore: new FileSystemSessionMetadataStore({
stateRoot: paths.stateRoot,
}),
worktreeManager: new SessionWorktreeManager({
worktreeRoot: paths.worktreeRoot,
baseRef: config.provisioning.gitWorktree.baseRef,
targetPath: config.provisioning.gitWorktree.targetPath,
}),
};
}
async createSession(input: {
projectPath: string;
sessionId?: string;
}): Promise<SessionMetadata> {
const runtime = await this.loadRuntime();
const sessionId = input.sessionId?.trim() || toSessionId();
const baseWorkspacePath = runtime.worktreeManager.resolveBaseWorkspacePath(sessionId);
const session = await runtime.sessionStore.createSession({
sessionId,
projectPath: resolve(input.projectPath),
baseWorkspacePath,
});
await runtime.worktreeManager.initializeSessionBaseWorkspace({
sessionId: session.sessionId,
projectPath: session.projectPath,
baseWorkspacePath: session.baseWorkspacePath,
});
return session;
}
async listSessions(): Promise<SessionMetadata[]> {
const runtime = await this.loadRuntime();
return runtime.sessionStore.listSessions();
}
async readSession(sessionId: string): Promise<SessionMetadata | undefined> {
const runtime = await this.loadRuntime();
return runtime.sessionStore.readSession(sessionId);
}
async closeSession(input: {
sessionId: string;
mergeToProject?: boolean;
}): Promise<SessionMetadata> {
const runtime = await this.loadRuntime();
const session = await runtime.sessionStore.readSession(input.sessionId);
if (!session) {
throw new Error(`Session \"${input.sessionId}\" does not exist.`);
}
const sessionProjectContextStore = new FileSystemProjectContextStore({
filePath: runtime.sessionStore.getSessionProjectContextPath(session.sessionId),
});
const projectContext = await sessionProjectContextStore.readState();
const taskWorktreePaths = projectContext.taskQueue
.map((task) => task.worktreePath)
.filter((path): path is string => typeof path === "string" && path.trim().length > 0);
const outcome = await runtime.worktreeManager.closeSession({
session,
taskWorktreePaths,
mergeBaseIntoProject: input.mergeToProject === true,
});
if (outcome.kind === "fatal_error") {
throw new Error(`Session close failed: ${outcome.error}`);
}
if (outcome.kind === "conflict") {
return runtime.sessionStore.updateSession(session.sessionId, {
sessionStatus: "closed_with_conflicts",
});
}
return runtime.sessionStore.updateSession(session.sessionId, {
sessionStatus: "closed",
});
}
listRuns(): RunRecord[] {
const output = [...this.runHistory.values()].sort((left, right) => {
return right.startedAt.localeCompare(left.startedAt);
@@ -335,11 +450,24 @@ export class UiRunService {
}
async startRun(input: StartRunInput): Promise<RunRecord> {
const config = await loadRuntimeConfig(this.envFilePath);
const runtime = await this.loadRuntime();
const config = runtime.config;
const manifest = parseAgentManifest(input.manifest);
const executionMode = input.executionMode ?? "mock";
const provider = input.provider ?? "codex";
const sessionId = input.sessionId?.trim() || toSessionId();
const session = input.sessionId?.trim()
? await runtime.sessionStore.readSession(sessionId)
: undefined;
if (input.sessionId?.trim() && !session) {
throw new Error(`Session \"${sessionId}\" does not exist.`);
}
if (
session &&
(session.sessionStatus === "closed" || session.sessionStatus === "closed_with_conflicts")
) {
throw new Error(`Session \"${sessionId}\" is closed and cannot run new tasks.`);
}
const runId = randomUUID();
const controller = new AbortController();
@@ -361,8 +489,9 @@ export class UiRunService {
if (executionMode === "provider") {
providerRuntime = await createProviderRunRuntime({
provider,
initialPrompt: input.prompt,
config,
observabilityRootPath: this.workspaceRoot,
baseEnv: process.env,
});
}
@@ -380,11 +509,20 @@ export class UiRunService {
actorExecutors,
settings: {
workspaceRoot: this.workspaceRoot,
stateRoot: config.orchestration.stateRoot,
projectContextPath: config.orchestration.projectContextPath,
stateRoot: runtime.stateRoot,
projectContextPath: session
? runtime.sessionStore.getSessionProjectContextPath(sessionId)
: resolve(this.workspaceRoot, config.orchestration.projectContextPath),
runtimeContext: {
ui_mode: executionMode,
run_provider: provider,
...(session
? {
session_id: sessionId,
project_path: session.projectPath,
base_workspace_path: session.baseWorkspacePath,
}
: {}),
...(input.runtimeContextOverrides ?? {}),
},
},
@@ -392,7 +530,7 @@ export class UiRunService {
});
await writeRunMeta({
stateRoot: config.orchestration.stateRoot,
stateRoot: runtime.stateRoot,
sessionId,
run: record,
});
@@ -408,6 +546,7 @@ export class UiRunService {
},
},
signal: controller.signal,
...(session ? { sessionMetadata: session } : {}),
});
const completedRecord = this.runHistory.get(runId);
@@ -423,7 +562,7 @@ export class UiRunService {
this.runHistory.set(runId, next);
await writeRunMeta({
stateRoot: config.orchestration.stateRoot,
stateRoot: runtime.stateRoot,
sessionId,
run: next,
});
@@ -443,7 +582,7 @@ export class UiRunService {
this.runHistory.set(runId, next);
await writeRunMeta({
stateRoot: config.orchestration.stateRoot,
stateRoot: runtime.stateRoot,
sessionId,
run: next,
});