feat: apply reviewed goal patches

This commit is contained in:
thanhnv
2026-07-12 00:21:50 +09:00
parent 159022c73f
commit a2cca645dc
9 changed files with 378 additions and 68 deletions
@@ -59,6 +59,8 @@ export interface GoalJob {
context_manifest?: { files: number; characters: number; truncated: boolean; path?: string };
approval?: { id: string; status: string; action: string };
reviewer_attempts?: GoalReviewerAttempt[];
patch_artifact?: { path: string; sha256: string; files: string[]; bytes: number; status: string; preview?: string; approval_id?: string; applied_at?: string; applied_by?: string };
verification?: Array<{ command: string; exit_code: number; output: string }>;
}
export interface GoalReviewerAttempt {
@@ -95,6 +97,7 @@ interface AccountProviderStatus {
const HARNESS_BIN = join(APP_ROOT, 'packages', 'casan-harness', 'scripts', 'bash');
const CONNECTIONS_CLI = join(HARNESS_BIN, 'model-connections.py');
const ORCHESTRATOR_CLI = join(HARNESS_BIN, 'goal-orchestrator.py');
const PATCH_EXECUTOR_CLI = join(HARNESS_BIN, 'goal-patch-executor.py');
const RBAC_CLI = join(HARNESS_BIN, 'rbac-check.py');
const PROJECT_REGISTRY = join(APP_ROOT, 'packages', 'casan-harness', 'level5', 'project-registry.json');
@@ -132,7 +135,7 @@ export class GoalsService {
const account = await this.accountReviewer();
const localModel = local?.defaultModel || local?.models[0] || 'ornith:9b';
const cloudModel = cloud?.defaultModel || cloud?.models[0] || gateway?.defaultModel || gateway?.models[0] || '';
const localRuntime = local ? this.runtime(local.id, localModel, actor) : {
const localRuntime = local ? this.localRuntime(this.runtime(local.id, localModel, actor)) : {
CASAN_CHAT_SELECTED_MODEL: `ollama:${localModel}`,
CASAN_OLLAMA_HOST: process.env.CASAN_OLLAMA_HOST || 'host.docker.internal:11434',
OLLAMA_HOST: process.env.OLLAMA_HOST || 'host.docker.internal:11434',
@@ -284,6 +287,31 @@ export class GoalsService {
return job;
}
apply(id: string, actor: SettingsActor): GoalJob {
const job = this.get(id, actor);
if (!job.patch_artifact || job.status !== 'requires_approval') {
throw new BadRequestException('GOAL_PATCH_NOT_READY');
}
try {
const output = execFileSync('python3', [PATCH_EXECUTOR_CLI, '--job-file', this.jobPath(actor.tenant, id), '--actor', actor.actor], {
cwd: APP_ROOT,
env: { ...process.env, CASAN_TENANT_ID: actor.tenant || 'default' },
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 10 * 60_000,
});
const applied = parseJson<GoalJob>(output);
if (!applied) throw new InternalServerErrorException('GOAL_APPLY_RESPONSE_INVALID');
return applied;
} catch (error: unknown) {
if (error instanceof HttpException) throw error;
const detail = error as { status?: number; stderr?: string | Buffer; stdout?: string | Buffer };
const message = String(detail.stderr || detail.stdout || 'GOAL_APPLY_FAILED').trim();
if (Number(detail.status) === 3) throw new ForbiddenException(message);
throw new InternalServerErrorException(message);
}
}
list(actor: SettingsActor, limit = 20): { count: number; goals: GoalJob[] } {
this.requireRead(actor, actor.project);
const directory = join(APP_ROOT, '.specify', 'state', 'goals', safeTenant(actor.tenant));
@@ -351,6 +379,15 @@ export class GoalsService {
return parsed.env;
}
private localRuntime(runtime: Record<string, string>): Record<string, string> {
if (existsSync('/.dockerenv')) return runtime;
const normalize = (value: string): string => value
.replace('http://host.docker.internal:', 'http://127.0.0.1:')
.replace('https://host.docker.internal:', 'https://127.0.0.1:')
.replace(/^host\.docker\.internal:/, '127.0.0.1:');
return Object.fromEntries(Object.entries(runtime).map(([key, value]) => [key, normalize(value)]));
}
private async accountReviewer(): Promise<'claude' | 'codex' | ''> {
if (process.env.CASAN_PROVIDER_ACCOUNT_AUTH_ENABLED !== '1') return '';
const bridgeUrl = (process.env.CASAN_AUTH_BRIDGE_URL || '').replace(/\/$/, '');