feat(FN-1416): merge fusion/fn-1416

This commit is contained in:
gsxdsm
2026-04-11 08:31:32 -07:00
parent 98ad63abcb
commit e48e034754
2 changed files with 259 additions and 0 deletions

153
AGENTS.md
View File

@@ -2417,3 +2417,156 @@ When you add a template:
1. The template data is copied to a new workflow step (templates themselves are immutable)
2. The new step is enabled by default
3. You can edit the step after creation to customize the prompt
## Run Audit
The run-audit system provides complete traceability for agent runs by recording every mutation performed by the engine across three domains: git operations, database changes, and filesystem writes. Each event is tied to a specific run ID, enabling operators to map one agent execution to concrete changes.
### Data Model
**`RunAuditEvent`** — A persisted audit record:
| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | UUID for the event |
| `timestamp` | `string` | ISO-8601 when the event occurred |
| `runId` | `string` | Heartbeat run ID (or synthetic ID for executor/merger) |
| `agentId` | `string` | Agent that performed the mutation |
| `taskId` | `string?` | Associated task (inferred from target when it looks like FN-*, KB-*) |
| `domain` | `RunAuditDomain` | `"database"` \| `"git"` \| `"filesystem"` |
| `mutationType` | `string` | What changed (e.g., `task:update`, `git:commit`, `file:write`) |
| `target` | `string` | What was affected (task ID, branch name, file path) |
| `metadata` | `Record<string, unknown>?` | Additional context (phase, source, mutation-specific details) |
### Mutation Domains
**Database mutations** — TaskStore operations:
- `task:create`, `task:update`, `task:move`, `task:log-entry`
- `task:comment:add`, `task:steering-comment:add`
- `task:assign`, `task:checkout`, `task:release`, `task:pause`, `task:unpause`
- `task:dependency:add`, `document:write`, `workflow-step:result`
**Git mutations** — Repository operations:
- `worktree:create`, `worktree:remove`, `worktree:reuse`
- `branch:create`, `branch:delete`, `branch:checkout`
- `commit:create`, `commit:amend`, `reset:hard`
- `merge:start`, `merge:resolve`, `stash:push`, `stash:pop`
**Filesystem mutations** — File system operations:
- `file:write`, `file:delete`, `file:capture-modified`
- `attachment:create`, `attachment:delete`
- `prompt:write`, `prompt:update`, `session:write`, `session:delete`
### Run Context
Every active run has an `EngineRunContext` that enables correlation:
```typescript
interface EngineRunContext {
runId: string; // Stable identifier (heartbeat run ID or synthetic)
agentId: string; // Agent performing mutations
taskId?: string; // Task being operated on
phase?: string; // "heartbeat" | "execute" | "merge" | "merge-attempt-N"
source?: string; // "timer" | "on_demand" | "assignment"
}
```
The engine creates synthetic run IDs for executor and merger operations (e.g., `exec-FN-001-1712345678-a1b2`).
### Ordering Semantics
Events are ordered by `timestamp DESC, rowid DESC`. When multiple events share the same millisecond timestamp, the `rowid` (auto-increment) provides a stable tiebreaker. This ensures deterministic ordering across repeated queries.
### API Endpoints
**`GET /api/agents/:id/runs/:runId/audit`** — Fetch audit events for a run
Query parameters:
- `taskId` — Filter by task ID
- `domain` — Filter by domain (`database`, `git`, `filesystem`)
- `startTime` — Start of time range (ISO-8601, inclusive)
- `endTime` — End of time range (ISO-8601, inclusive)
- `limit` — Maximum events (default 100, max 1000)
Response:
```typescript
interface RunAuditResponse {
runId: string;
events: NormalizedRunAuditEvent[];
filters: { taskId?, domain?, startTime?, endTime? };
totalCount: number;
hasMore: boolean;
}
```
**`GET /api/agents/:id/runs/:runId/timeline`** — Correlated timeline with logs
Combines audit events with agent logs into a unified chronological view. Query parameters same as `/audit`, plus:
- `includeLogs` — Include agent logs (default true)
Response:
```typescript
interface RunTimelineResponse {
run: { id, agentId, startedAt, endedAt, status, taskId? };
auditByDomain: { database: [], git: [], filesystem: [] };
counts: { auditEvents: number; logEntries: number };
timeline: TimelineEntry[];
}
```
### Tracing a Run End-to-End
**Step 1: Identify the run** — Get the run ID from:
- Agent detail modal → Runs tab
- Task activity log → `agent:run:started` event
- Heartbeat log entries
**Step 2: Fetch audit events** — Use the audit endpoint:
```
GET /api/agents/agent-001/runs/run-abc123/audit
```
**Step 3: Map mutations to evidence** — Each event type maps to concrete evidence:
| Domain | Mutation | Evidence |
|--------|----------|----------|
| `git` | `worktree:create` | Directory `.worktrees/{task-id}` exists |
| `git` | `commit:create` | `git log --oneline` shows the commit |
| `git` | `merge:resolve` | PR merged in GitHub, commit in repo |
| `database` | `task:update` | `task.json` reflects the changes |
| `database` | `task:log-entry` | Activity log shows the entry |
| `filesystem` | `file:write` | File exists at the target path |
**Step 4: View full context** — For combined audit + logs:
```
GET /api/agents/agent-001/runs/run-abc123/timeline?includeLogs=true
```
### Troubleshooting
**Missing `contextSnapshot.taskId`**: Legacy runs may not have task context in their snapshot. Use the `taskId` query parameter explicitly when querying audit events:
```
GET /api/agents/:id/runs/:runId/audit?taskId=FN-001
```
**Unknown run ID**: Verify the run exists first:
```
GET /api/agents/:id/runs/:runId # Returns 404 if not found
```
**Empty audit results**: Possible causes:
- Run predates run-audit feature (pre-schema-v29)
- No mutations occurred during the run
- Wrong domain filter — try without `domain` parameter
**Timestamps appear out of order**: Check for millisecond-precision collisions. Events within the same millisecond are ordered by `rowid DESC` (most recently inserted first). Re-query with `?limit=10` to see the latest events first.
**Executor/merger runs have synthetic IDs**: Look for patterns like `exec-{taskId}-{timestamp}-{random}` or `merge-{taskId}-{timestamp}`. These correlate to the original heartbeat run via the `runId` field in the agent's run records.
### Backward Compatibility
The auditor no-ops cleanly when:
- No run context exists (manual/non-run operations)
- TaskStore doesn't have `recordRunAuditEvent` method
This ensures legacy code paths are unaffected. Database operations without an explicit `runContext` parameter skip audit recording but still succeed.

View File

@@ -863,3 +863,109 @@ await network.initialize();
### Error handling model
All managers use defensive async APIs and silent fallback handling for unsupported environments (for example, browser development/test runs without native Capacitor bindings). This keeps startup resilient across web, simulator, and device contexts.
## Run Audit
The dashboard provides run-audit API clients for tracing agent runs across git, database, and filesystem mutations.
### API Client Functions
**`fetchAgentRunAudit(agentId, runId, filters?, projectId?)`** — Fetch audit events for a run
```typescript
import { fetchAgentRunAudit } from "./api";
const response = await fetchAgentRunAudit("agent-001", "run-abc123", {
domain: "git", // Optional: filter by domain
startTime: "2025-01-01T00:00:00Z", // Optional: time range
limit: 100, // Optional: max events
}, "project-xyz");
```
**`fetchAgentRunTimeline(agentId, runId, options?, projectId?)`** — Fetch correlated timeline with logs
```typescript
import { fetchAgentRunTimeline } from "./api";
const response = await fetchAgentRunTimeline("agent-001", "run-abc123", {
domain: "filesystem", // Optional: filter by domain
includeLogs: true, // Include agent log entries
limit: 50, // Max audit events
}, "project-xyz");
```
### Response Shapes
**`RunAuditResponse`** — From `fetchAgentRunAudit`:
```typescript
interface RunAuditResponse {
runId: string;
events: NormalizedRunAuditEvent[];
filters: { taskId?, domain?, startTime?, endTime? };
totalCount: number;
hasMore: boolean;
}
```
**`RunTimelineResponse`** — From `fetchAgentRunTimeline`:
```typescript
interface RunTimelineResponse {
run: { id, agentId, startedAt, endedAt?, status, taskId? };
auditByDomain: { database: [], git: [], filesystem: [] };
counts: { auditEvents: number; logEntries: number };
timeline: TimelineEntry[];
}
```
### Debugging Recipe: Map Run ID to Mutations
**Problem**: An agent run completed but you need to verify what changed.
1. **Get the run ID** from the Runs tab in the agent detail modal
2. **Fetch audit events** to see all mutations:
```typescript
const audit = await fetchAgentRunAudit(agentId, runId);
console.log(audit.events.map(e => `${e.domain}:${e.mutationType} → ${e.target}`));
```
3. **Check git mutations** for code changes:
```typescript
const timeline = await fetchAgentRunTimeline(agentId, runId, { domain: "git" });
timeline.auditByDomain.git.forEach(e => {
console.log(`${e.mutationType}: ${e.target}`, e.metadata);
});
```
4. **Verify database changes**:
```typescript
const dbEvents = audit.events.filter(e => e.domain === "database");
dbEvents.forEach(e => {
// e.summary contains a human-readable description
console.log(`[${e.timestamp}] ${e.summary}`);
});
```
5. **Trace filesystem changes**:
```typescript
const fsEvents = timeline.auditByDomain.filesystem;
fsEvents.forEach(e => {
if (e.mutationType === "file:write") {
// File was written at e.target
console.log(`Wrote: ${e.target} (${e.metadata?.size} bytes)`);
}
});
```
### TypeScript Types
Import types from `api.ts`:
```typescript
import type {
RunAuditFilters,
RunAuditResponse,
RunTimelineResponse,
NormalizedRunAuditEvent,
TimelineEntry,
} from "./api";
```