feat(FN-1404): add run-audit instrumentation layer for agent run mutations

- Add shared RunAuditService in packages/engine/src/run-audit.ts for tracking agent run state transitions
- Instrument HeartbeatMonitor to log run mutations (create, complete, error, skip) with timestamps and context
- Instrument TaskExecutor to log run mutations during task execution lifecycle
- Instrument Merger to log run mutations during merge workflow
- Add run-audit pattern documentation to project memory
- Fix: remove audit calls from non-run recovery methods to avoid false positives
This commit is contained in:
gsxdsm
2026-04-10 02:56:57 -07:00
parent 2c06d72203
commit e8dd0f2416
5 changed files with 360 additions and 3 deletions

View File

@@ -25,6 +25,7 @@ import { isContextLimitError } from "./context-limit-detector.js";
import { StepSessionExecutor, type StepSessionExecutorOptions, type StepResult } from "./step-session-executor.js";
import { resolveAgentInstructions, buildSystemPromptWithInstructions } from "./agent-instructions.js";
import type { AgentReflectionService } from "./agent-reflection.js";
import { createRunAuditor, generateSyntheticRunId, type EngineRunContext } from "./run-audit.js";
import {
createReflectOnPerformanceTool,
createTaskCreateTool as sharedCreateTaskCreateTool,
@@ -803,11 +804,23 @@ export class TaskExecutor {
// Construct run context for mutation correlation
// Use a synthetic correlation ID: task ID + timestamp + random suffix
const syntheticRunId = generateSyntheticRunId("exec", task.id);
this.currentRunContext = {
runId: `exec-${task.id}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
runId: syntheticRunId,
agentId: task.assignedAgentId ?? "executor",
};
// Build engine run context for audit instrumentation (FN-1404)
const engineRunContext: EngineRunContext = {
runId: syntheticRunId,
agentId: task.assignedAgentId ?? "executor",
taskId: task.id,
phase: "execute",
};
// Create run auditor for TaskStore-backed audit emission (no-ops if store doesn't support it)
const audit = createRunAuditor(this.store, engineRunContext);
// Hoist worktreePath so it's accessible in the catch block for dep-abort cleanup
// Determine worktree name based on settings
let worktreePath: string;
@@ -873,6 +886,8 @@ export class TaskExecutor {
acquiredFromPool = true;
executorLog.log(`Acquired worktree from pool: ${pooled}`);
await this.store.updateTask(task.id, { worktree: worktreePath, branch: actualBranch });
// Audit trail: record worktree reuse (FN-1404)
await audit.git({ type: "worktree:reuse", target: worktreePath, metadata: { branch: actualBranch } });
if (actualBranch !== branchName) {
executorLog.log(`Branch conflict resolved: using ${actualBranch} instead of ${branchName}`);
await this.store.logEntry(task.id, `Acquired worktree from pool: ${worktreePath} (branch conflict: using ${actualBranch})`, undefined, this.currentRunContext);
@@ -899,6 +914,9 @@ export class TaskExecutor {
const created = await this.createWorktree(branchName, worktreePath, task.id, baseBranch ?? undefined);
worktreePath = created.path;
await this.store.updateTask(task.id, { worktree: created.path, branch: created.branch });
// Audit trail: record worktree creation and branch creation (FN-1404)
await audit.git({ type: "worktree:create", target: created.path, metadata: { branch: created.branch } });
await audit.git({ type: "branch:create", target: created.branch });
if (created.branch !== branchName) {
executorLog.log(`Branch conflict resolved: using ${created.branch} instead of ${branchName}`);
await this.store.logEntry(task.id, `Worktree created at ${worktreePath} (branch conflict: using ${created.branch})`, undefined, this.currentRunContext);
@@ -951,6 +969,9 @@ export class TaskExecutor {
const created = await this.createWorktree(branchName, worktreePath, task.id);
worktreePath = created.path;
await this.store.updateTask(task.id, { worktree: created.path, branch: created.branch });
// Audit trail: record worktree creation and branch creation (FN-1404)
await audit.git({ type: "worktree:create", target: created.path, metadata: { branch: created.branch } });
await audit.git({ type: "branch:create", target: created.branch });
}
// Capture the base commit SHA for diff computation whenever a task
@@ -965,6 +986,8 @@ export class TaskExecutor {
}).trim();
await this.store.updateTask(task.id, { baseCommitSha });
executorLog.log(`${task.id}: captured baseCommitSha ${baseCommitSha.slice(0, 7)}`);
// Audit trail: record base commit capture for later diff computation (FN-1404)
await audit.git({ type: "commit:create", target: baseCommitSha, metadata: { purpose: "base" } });
} catch (err: any) {
executorLog.log(`Failed to capture baseCommitSha for ${task.id}: ${err.message}`);
// Non-fatal: task can continue without baseCommitSha
@@ -1055,18 +1078,24 @@ export class TaskExecutor {
if (modifiedFiles.length > 0) {
await this.store.updateTask(task.id, { modifiedFiles });
executorLog.log(`${task.id}: captured ${modifiedFiles.length} modified files`);
// Audit trail: record filesystem mutation (FN-1404)
await audit.filesystem({ type: "file:capture-modified", target: task.id, metadata: { files: modifiedFiles } });
}
const workflowSuccess = await this.runWorkflowSteps(task, worktreePath, settings);
if (!workflowSuccess) {
await this.store.updateTask(task.id, { status: "failed", error: "Workflow step failed" });
await this.store.moveTask(task.id, "in-review");
// Audit trail: record task move (FN-1404)
await audit.database({ type: "task:move", target: task.id, metadata: { to: "in-review" } });
executorLog.log(`${task.id} workflow step failed → in-review`);
this.options.onError?.(task, new Error("Workflow step failed"));
return;
}
await this.store.moveTask(task.id, "in-review");
// Audit trail: record task move (FN-1404)
await audit.database({ type: "task:move", target: task.id, metadata: { to: "in-review" } });
executorLog.log(`${task.id} completed (step-session) → in-review`);
this.options.onComplete?.(task);
} else {
@@ -1122,6 +1151,8 @@ export class TaskExecutor {
if (worktreePath && existsSync(worktreePath)) {
try {
execSync(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir, stdio: "pipe" });
// Audit trail: record worktree removal (FN-1404)
await audit.git({ type: "worktree:remove", target: worktreePath });
} catch {}
}
await this.store.updateTask(task.id, {
@@ -1643,6 +1674,8 @@ export class TaskExecutor {
try {
execSync(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir, stdio: "pipe" });
executorLog.log(`Removed old worktree for paused task: ${worktreePath}`);
// Audit trail: record worktree removal (FN-1404)
await audit.git({ type: "worktree:remove", target: worktreePath });
} catch (cleanupErr: any) {
executorLog.warn(`Failed to remove old worktree ${worktreePath}: ${cleanupErr.message}`);
}
@@ -1725,6 +1758,8 @@ export class TaskExecutor {
try {
execSync(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir, stdio: "pipe" });
executorLog.log(`Removed old worktree for transient retry: ${worktreePath}`);
// Audit trail: record worktree removal (FN-1404)
await audit.git({ type: "worktree:remove", target: worktreePath });
} catch (cleanupErr: any) {
executorLog.warn(`Failed to remove old worktree ${worktreePath}: ${cleanupErr.message}`);
}
@@ -1785,6 +1820,8 @@ export class TaskExecutor {
try {
execSync(`git worktree remove "${worktreePath}" --force`, { cwd: this.rootDir, stdio: "pipe" });
executorLog.log(`Removed old worktree for stuck-killed retry: ${worktreePath}`);
// Audit trail: record worktree removal (FN-1404)
await audit.git({ type: "worktree:remove", target: worktreePath });
} catch (cleanupErr: any) {
executorLog.warn(`Failed to remove old worktree ${worktreePath}: ${cleanupErr.message}`);
}
@@ -1795,6 +1832,8 @@ export class TaskExecutor {
// when execute() started (e.g., resumed orphan), we skip the redundant move.
if (task.column !== "todo") {
await this.store.moveTask(task.id, "todo");
// Audit trail: record task move (FN-1404)
await audit.database({ type: "task:move", target: task.id, metadata: { to: "todo" } });
executorLog.log(`${task.id} moved to todo for retry after stuck kill`);
} else {
executorLog.log(`${task.id} already in todo — skipping redundant move`);