feat: complete Aider Page Data review workflow
This commit is contained in:
+103
-2
@@ -1,5 +1,7 @@
|
||||
import { spawn as nodeSpawn } from 'node:child_process';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const CODE_EXECUTORS = new Set(['aider', 'openhands']);
|
||||
const DEFAULT_STDIO_LIMIT = 64 * 1024;
|
||||
@@ -55,6 +57,92 @@ export function extractToolInstruction(userMessage) {
|
||||
.trim();
|
||||
}
|
||||
|
||||
function runValidation(userMessage) {
|
||||
const metadata = userMessage?.metadata ?? {};
|
||||
const runMetadata = metadata.memindRun ?? metadata.agentRun ?? {};
|
||||
const validation = runMetadata.validation ?? metadata.toolGatewayValidation;
|
||||
return validation && typeof validation === 'object' && !Array.isArray(validation)
|
||||
? validation
|
||||
: null;
|
||||
}
|
||||
|
||||
function validationFilePaths(userMessage) {
|
||||
const validation = runValidation(userMessage);
|
||||
if (!validation) return [];
|
||||
const candidates = [
|
||||
validation.expectedFile ?? validation.expectedPath,
|
||||
...(Array.isArray(validation.expectedFiles) ? validation.expectedFiles : []),
|
||||
];
|
||||
return candidates
|
||||
.map((item) => {
|
||||
if (typeof item === 'string') return item.trim();
|
||||
if (!item || typeof item !== 'object' || Array.isArray(item)) return '';
|
||||
return String(item.path ?? item.file ?? item.relativePath ?? '').trim();
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function resolveAiderReceiptPath(userMessage, requestId) {
|
||||
const expected = `.memind/agent-runs/${String(requestId ?? '').trim()}.json`;
|
||||
return validationFilePaths(userMessage).find((item) => item === expected) ?? null;
|
||||
}
|
||||
|
||||
export async function prepareAiderReceiptFile(cwd, relativePath) {
|
||||
if (!cwd || !relativePath) return null;
|
||||
const root = path.resolve(String(cwd));
|
||||
const target = path.resolve(root, relativePath);
|
||||
if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
|
||||
throw new Error(`Aider receipt path escapes working directory: ${relativePath}`);
|
||||
}
|
||||
await fs.mkdir(path.dirname(target), { recursive: true });
|
||||
try {
|
||||
await fs.access(target);
|
||||
} catch {
|
||||
await fs.writeFile(
|
||||
target,
|
||||
`${JSON.stringify({ status: 'pending', executor: 'aider' }, null, 2)}\n`,
|
||||
{ flag: 'wx' },
|
||||
);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
async function resolveAiderContextFiles(userMessage, cwd) {
|
||||
const metadata = userMessage?.metadata ?? {};
|
||||
const runMetadata = metadata.memindRun ?? metadata.agentRun ?? {};
|
||||
const candidates = Array.isArray(runMetadata.aiderContextFiles)
|
||||
? runMetadata.aiderContextFiles
|
||||
: [];
|
||||
if (!cwd || candidates.length === 0) return [];
|
||||
const root = path.resolve(String(cwd));
|
||||
const resolved = [];
|
||||
for (const relativePath of candidates.slice(0, 40)) {
|
||||
const target = path.resolve(root, String(relativePath ?? ''));
|
||||
if (target === root || !target.startsWith(`${root}${path.sep}`)) continue;
|
||||
try {
|
||||
const stat = await fs.stat(target);
|
||||
if (stat.isFile()) resolved.push(target);
|
||||
} catch {
|
||||
// Review context is best-effort; delivery validation remains authoritative.
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function hardenAiderLaunchPlan(plan, receiptPath = null, contextFiles = []) {
|
||||
const args = [...(plan?.args ?? [])];
|
||||
for (const flag of ['--no-git', '--no-auto-commits', '--no-dirty-commits']) {
|
||||
if (!args.includes(flag)) args.push(flag);
|
||||
}
|
||||
for (const contextFile of contextFiles) {
|
||||
if (!args.includes(contextFile)) args.push('--file', contextFile);
|
||||
}
|
||||
if (receiptPath && !args.includes(receiptPath)) {
|
||||
args.push('--file', receiptPath);
|
||||
}
|
||||
return { ...plan, args };
|
||||
}
|
||||
|
||||
export function createToolGateway({
|
||||
llmProviderService,
|
||||
env = process.env,
|
||||
@@ -110,16 +198,29 @@ export function createToolGateway({
|
||||
throw new Error('Tool Gateway job missing instruction');
|
||||
}
|
||||
const executor = selectExecutor({ userMessage, taskType });
|
||||
const plan = await llmProviderService.getExecutorLaunchPlan(executor, {
|
||||
const receiptPath = executor === 'aider'
|
||||
? resolveAiderReceiptPath(userMessage, requestId)
|
||||
: null;
|
||||
const executorInstruction = receiptPath
|
||||
? `${instruction}\n\nThe validation receipt is already included in the Aider chat. Edit it directly; do not ask the user to add it.`
|
||||
: instruction;
|
||||
let plan = await llmProviderService.getExecutorLaunchPlan(executor, {
|
||||
cwd,
|
||||
mode: 'headless',
|
||||
instruction,
|
||||
instruction: executorInstruction,
|
||||
purpose: 'default',
|
||||
includeSecret: true,
|
||||
});
|
||||
if (!plan?.ok) {
|
||||
throw new Error(plan?.message ?? `Tool Gateway launch plan unavailable for ${executor}`);
|
||||
}
|
||||
if (executor === 'aider') {
|
||||
const preparedReceipt = dryRun
|
||||
? (receiptPath ? path.resolve(String(cwd), receiptPath) : null)
|
||||
: await prepareAiderReceiptFile(cwd, receiptPath);
|
||||
const contextFiles = await resolveAiderContextFiles(userMessage, cwd);
|
||||
plan = hardenAiderLaunchPlan(plan, preparedReceipt, contextFiles);
|
||||
}
|
||||
if (dryRun) {
|
||||
return {
|
||||
ok: true,
|
||||
|
||||
Reference in New Issue
Block a user