perf(engine): add per-phase timing logs and monorepo test warning
Instrument worktree init, setup script, workflow steps, verification commands, and in-merge fix retries with [timing] log entries so task duration bottlenecks are visible in task logs. Also warn when inferDefaultTestCommand falls back to `pnpm test` inside a pnpm workspace — users should set an explicit scoped testCommand to avoid running the whole monorepo suite on every merge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -451,7 +451,7 @@ describe("TaskExecutor worktreeInitCommand", () => {
|
||||
// Should log success
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"FN-010",
|
||||
"Worktree init command completed",
|
||||
expect.stringMatching(/^\[timing\] Worktree init command completed in \d+ms$/),
|
||||
"pnpm install",
|
||||
expect.objectContaining({ agentId: "executor" }),
|
||||
);
|
||||
|
||||
@@ -1326,13 +1326,15 @@ export class TaskExecutor {
|
||||
// with "@fusion/core entry not found" because monorepo exports point
|
||||
// to dist/. 5-minute timeout accommodates install + build together.
|
||||
if (settings.worktreeInitCommand) {
|
||||
const initStartedAt = Date.now();
|
||||
try {
|
||||
const initResult = await runConfiguredCommand(settings.worktreeInitCommand, worktreePath, 300_000);
|
||||
if (initResult.spawnError || initResult.timedOut || initResult.exitCode !== 0) {
|
||||
throw new Error(configuredCommandErrorMessage(initResult));
|
||||
}
|
||||
await this.store.logEntry(task.id, "Worktree init command completed", settings.worktreeInitCommand, this.currentRunContext);
|
||||
await this.store.logEntry(task.id, `[timing] Worktree init command completed in ${Date.now() - initStartedAt}ms`, settings.worktreeInitCommand, this.currentRunContext);
|
||||
} catch (err: unknown) {
|
||||
await this.store.logEntry(task.id, `[timing] Worktree init command failed after ${Date.now() - initStartedAt}ms`, undefined, this.currentRunContext);
|
||||
const execError = err instanceof Error ? err : new Error(String(err));
|
||||
const message = "stderr" in execError && typeof (execError as Record<string, unknown>).stderr === "string"
|
||||
? String((execError as Record<string, unknown>).stderr)
|
||||
@@ -1351,12 +1353,13 @@ export class TaskExecutor {
|
||||
if (settings.setupScript) {
|
||||
const scriptCommand = settings.scripts?.[settings.setupScript];
|
||||
if (scriptCommand) {
|
||||
const setupStartedAt = Date.now();
|
||||
try {
|
||||
const setupResult = await runConfiguredCommand(scriptCommand, worktreePath, 120_000);
|
||||
if (setupResult.spawnError || setupResult.timedOut || setupResult.exitCode !== 0) {
|
||||
throw new Error(configuredCommandErrorMessage(setupResult));
|
||||
}
|
||||
await this.store.logEntry(task.id, `Setup script '${settings.setupScript}' completed`, scriptCommand, this.currentRunContext);
|
||||
await this.store.logEntry(task.id, `[timing] Setup script '${settings.setupScript}' completed in ${Date.now() - setupStartedAt}ms`, scriptCommand, this.currentRunContext);
|
||||
} catch (err: unknown) {
|
||||
const execError = err instanceof Error ? err : new Error(String(err));
|
||||
const message = "stderr" in execError && typeof (execError as Record<string, unknown>).stderr === "string"
|
||||
@@ -3427,6 +3430,7 @@ ${failureFeedback}
|
||||
executorLog.log(`${task.id} — [pre-merge] running workflow step: ${ws.name} (${stepMode} mode)`);
|
||||
|
||||
const startedAt = new Date().toISOString();
|
||||
const stepStartedAtMs = Date.now();
|
||||
|
||||
// Push pending entry BEFORE execution so dashboard can show live status
|
||||
results.push({
|
||||
@@ -3445,6 +3449,7 @@ ${failureFeedback}
|
||||
const completedAt = new Date().toISOString();
|
||||
|
||||
if (result.success) {
|
||||
await this.store.logEntry(task.id, `[timing] Workflow step '${ws.name}' completed in ${Date.now() - stepStartedAtMs}ms`);
|
||||
await this.store.logEntry(task.id, `[pre-merge] Workflow step completed: ${ws.name}`);
|
||||
executorLog.log(`${task.id} — [pre-merge] workflow step passed: ${ws.name}`);
|
||||
// Update existing pending entry in place
|
||||
@@ -3460,6 +3465,7 @@ ${failureFeedback}
|
||||
await this.store.updateTask(task.id, { workflowStepResults: results });
|
||||
} else if (result.revisionRequested) {
|
||||
// Revision requested — this is a structured outcome that routes back to executor
|
||||
await this.store.logEntry(task.id, `[timing] Workflow step '${ws.name}' requested revision after ${Date.now() - stepStartedAtMs}ms`);
|
||||
await this.store.logEntry(
|
||||
task.id,
|
||||
`[pre-merge] Workflow step requested revision: ${ws.name}`,
|
||||
@@ -3485,6 +3491,7 @@ ${failureFeedback}
|
||||
};
|
||||
} else {
|
||||
// Hard failure
|
||||
await this.store.logEntry(task.id, `[timing] Workflow step '${ws.name}' failed after ${Date.now() - stepStartedAtMs}ms`);
|
||||
await this.store.logEntry(
|
||||
task.id,
|
||||
`[pre-merge] Workflow step failed: ${ws.name}`,
|
||||
|
||||
@@ -3407,7 +3407,7 @@ describe("aiMergeTask — deterministic merge verification", () => {
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done");
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"FN-050",
|
||||
"[verification] test command succeeded (exit 0, output exceeded buffer)",
|
||||
expect.stringMatching(/^\[timing\] \[verification\] test command succeeded \(exit 0, output exceeded buffer\) in \d+ms$/),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -397,6 +397,18 @@ export function inferDefaultTestCommand(
|
||||
|
||||
// Infer test command from lock files
|
||||
if (existsSync(join(rootDir, "pnpm-lock.yaml"))) {
|
||||
// Monorepo heuristic: a pnpm-workspace.yaml means `pnpm test` will fan out
|
||||
// across every workspace package on every merge, which is usually far slower
|
||||
// than necessary. Warn so the user sets an explicit scoped testCommand
|
||||
// (e.g. `pnpm -r --filter "...[main]" test`). We don't auto-scope because
|
||||
// the default branch name isn't guaranteed and git context may be unavailable.
|
||||
if (existsSync(join(rootDir, "pnpm-workspace.yaml"))) {
|
||||
mergerLog.warn(
|
||||
`Inferred test command "pnpm test" in a pnpm workspace (${rootDir}). ` +
|
||||
`This runs the full monorepo suite on every merge. Consider setting an explicit ` +
|
||||
`scoped testCommand in project settings, e.g. \`pnpm -r --filter "...[main]" test\`.`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
command: "pnpm test",
|
||||
testSource: "inferred",
|
||||
@@ -572,6 +584,7 @@ async function runVerificationCommand(
|
||||
success: false,
|
||||
};
|
||||
|
||||
const verificationStartedAt = Date.now();
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(command, {
|
||||
cwd: rootDir,
|
||||
@@ -585,10 +598,12 @@ async function runVerificationCommand(
|
||||
result.exitCode = 0;
|
||||
result.success = true;
|
||||
|
||||
mergerLog.log(`${taskId}: ${type} command succeeded`);
|
||||
await store.logEntry(taskId, `[verification] ${type} command succeeded (exit 0)`);
|
||||
const verificationDurationMs = Date.now() - verificationStartedAt;
|
||||
mergerLog.log(`${taskId}: ${type} command succeeded in ${verificationDurationMs}ms`);
|
||||
await store.logEntry(taskId, `[timing] [verification] ${type} command succeeded (exit 0) in ${verificationDurationMs}ms`);
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
const verificationDurationMs = Date.now() - verificationStartedAt;
|
||||
result.stdout = error?.stdout?.toString?.() || "";
|
||||
result.stderr = error?.stderr?.toString?.() || "";
|
||||
result.exitCode = typeof error?.status === "number"
|
||||
@@ -601,10 +616,10 @@ async function runVerificationCommand(
|
||||
result.success = maxBufferExceeded && result.exitCode === 0;
|
||||
|
||||
if (result.success) {
|
||||
mergerLog.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer)`);
|
||||
mergerLog.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
|
||||
await store.logEntry(
|
||||
taskId,
|
||||
`[verification] ${type} command succeeded (exit 0, output exceeded buffer)`,
|
||||
`[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
@@ -613,10 +628,10 @@ async function runVerificationCommand(
|
||||
// the task for diagnostics without dumping test output to the engine stdout.
|
||||
const output = result.stderr || result.stdout || error?.message || "Unknown error";
|
||||
const summary = summarizeVerificationOutput(output, type);
|
||||
mergerLog.error(`${taskId}: ${type} command failed (exit ${result.exitCode}); output captured in task log`);
|
||||
mergerLog.error(`${taskId}: ${type} command failed (exit ${result.exitCode}) in ${verificationDurationMs}ms; output captured in task log`);
|
||||
await store.logEntry(
|
||||
taskId,
|
||||
`[verification] ${type} command failed (exit ${result.exitCode}):\n${summary}`,
|
||||
`[timing] [verification] ${type} command failed (exit ${result.exitCode}) after ${verificationDurationMs}ms:\n${summary}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2062,6 +2077,7 @@ export async function aiMergeTask(
|
||||
if (failedResult) {
|
||||
let fixSuccess = false;
|
||||
for (let fixAttempt = 1; fixAttempt <= maxFixRetries; fixAttempt++) {
|
||||
const fixAttemptStartedAt = Date.now();
|
||||
mergerLog.log(`${taskId}: in-merge verification fix attempt ${fixAttempt}/${maxFixRetries}`);
|
||||
await store.logEntry(taskId, `In-merge verification fix attempt ${fixAttempt}/${maxFixRetries}`);
|
||||
|
||||
@@ -2076,14 +2092,15 @@ export async function aiMergeTask(
|
||||
settings, options, effectiveTestCommand, effectiveBuildCommand,
|
||||
);
|
||||
|
||||
const fixAttemptDurationMs = Date.now() - fixAttemptStartedAt;
|
||||
if (fixSuccess) {
|
||||
mergerLog.log(`${taskId}: in-merge verification fix succeeded on attempt ${fixAttempt}`);
|
||||
await store.logEntry(taskId, `In-merge verification fix succeeded — verification now passes`);
|
||||
mergerLog.log(`${taskId}: in-merge verification fix succeeded on attempt ${fixAttempt} in ${fixAttemptDurationMs}ms`);
|
||||
await store.logEntry(taskId, `[timing] In-merge verification fix succeeded on attempt ${fixAttempt} in ${fixAttemptDurationMs}ms — verification now passes`);
|
||||
break;
|
||||
}
|
||||
|
||||
mergerLog.warn(`${taskId}: in-merge verification fix attempt ${fixAttempt} — verification still fails`);
|
||||
await store.logEntry(taskId, `In-merge verification fix attempt ${fixAttempt} — verification still fails`);
|
||||
mergerLog.warn(`${taskId}: in-merge verification fix attempt ${fixAttempt} — verification still fails (${fixAttemptDurationMs}ms)`);
|
||||
await store.logEntry(taskId, `[timing] In-merge verification fix attempt ${fixAttempt} — verification still fails (${fixAttemptDurationMs}ms)`);
|
||||
}
|
||||
|
||||
if (fixSuccess) {
|
||||
@@ -2114,6 +2131,7 @@ export async function aiMergeTask(
|
||||
|
||||
let fixSuccess = false;
|
||||
for (let fixAttempt = 1; fixAttempt <= maxFixRetries; fixAttempt++) {
|
||||
const fixAttemptStartedAt = Date.now();
|
||||
mergerLog.log(`${taskId}: in-merge verification fix attempt ${fixAttempt}/${maxFixRetries}`);
|
||||
await store.logEntry(taskId, `In-merge verification fix attempt ${fixAttempt}/${maxFixRetries}`);
|
||||
|
||||
@@ -2128,11 +2146,13 @@ export async function aiMergeTask(
|
||||
settings, options, effectiveTestCommand, effectiveBuildCommand,
|
||||
);
|
||||
|
||||
const fixAttemptDurationMs = Date.now() - fixAttemptStartedAt;
|
||||
if (fixSuccess) {
|
||||
mergerLog.log(`${taskId}: in-merge verification fix succeeded on attempt ${fixAttempt}`);
|
||||
await store.logEntry(taskId, `In-merge verification fix succeeded`);
|
||||
mergerLog.log(`${taskId}: in-merge verification fix succeeded on attempt ${fixAttempt} in ${fixAttemptDurationMs}ms`);
|
||||
await store.logEntry(taskId, `[timing] In-merge verification fix succeeded on attempt ${fixAttempt} in ${fixAttemptDurationMs}ms`);
|
||||
break;
|
||||
}
|
||||
await store.logEntry(taskId, `[timing] In-merge verification fix attempt ${fixAttempt} — verification still fails (${fixAttemptDurationMs}ms)`);
|
||||
}
|
||||
|
||||
if (fixSuccess) {
|
||||
|
||||
Reference in New Issue
Block a user