Added an approval workflow to the reports plugin comprising a state machine (`approval.ts`), share blocks logic (`share-blocks.ts`), API routes for approvals, and two new dashboard panels (ReportApprovalPanel and ShareBlocksPanel), with corresponding tests; also updated the plugin README and added a Fusion-Task-Id: FN-3787
5.9 KiB
Reports Plugin for Fusion
Generates HTML system activity reports with multi-agent review.
Review Panel
The plugin exposes runReviewPanel() / runGeneratedReportReview() to fan out a generated report draft to multiple reviewer agents in parallel.
Panel member settings shape
Each reviewer uses this contract:
{
id: string;
name: string;
perspective: string;
promptTemplateId?: string;
provider?: string;
modelId?: string;
}
perspectiveis appended to the reviewer system prompt.promptTemplateIdselects a template fromsettings.reviewPromptTemplates[templateId]when present.provider+modelIdoptionally override model selection per reviewer.
Prompt template contract
runReviewPanel resolves reviewer templates in this order:
settings.reviewPromptTemplates[promptTemplateId ?? id]settings.reviewPrompt- Built-in fallback (
DEFAULT_REVIEW_PROMPT)
This is the temporary compatibility contract until FN-3782 lands shared review-template helpers.
Individual review shape
{
memberId: string;
memberName: string;
perspective: string;
verdict: "approve" | "revise" | "reject";
summary: string;
highlights: string[];
lowlights: string[];
suggestions: string[];
rawText: string;
durationMs: number;
}
Combined review shape
{
overallVerdict: "approve" | "revise" | "reject";
consensusSummary: string;
mergedHighlights: string[];
mergedLowlights: string[];
mergedSuggestions: string[];
individual: IndividualReview[];
failures: ReviewFailure[];
}
Aggregation is deterministic:
- verdict precedence:
approve < revise < reject - merged arrays are case-insensitive de-duped, first-seen order, max 25 items each
- consensus summary is generated locally from reviewer summaries (no second AI call)
Timeout and failure semantics
- Each reviewer has a hard timeout (
120_000ms). - A single reviewer failure never aborts the full panel.
- Failures are returned as:
{
memberId: string;
reason: "timeout" | "parse_error" | "session_unavailable" | "exception";
message: string;
}
- If all reviewers fail, combined verdict is
rejectwith an explicit consensus summary describing panel failure.
Report Archive
The plugin persists generated reports in SQLite via ensureReportSchema(db) and ReportStore.
Schema
Table: reports
- identity/metadata:
id,cadence,title,metadataJson - period window:
periodStart,periodEnd - lifecycle/status:
status,failureReason - payload references:
draftMarkdown,renderedHtmlPath - review payload:
combinedReviewJson - timestamps:
generationStartedAt,generationCompletedAt,reviewStartedAt,reviewCompletedAt,approvedAt,publishedAt,archivedAt,createdAt,updatedAt - approval actor:
approvedBy
Indexes:
idxReportsCadenceCreatedon(cadence, createdAt DESC, id)idxReportsStatusUpdatedon(status, updatedAt DESC, id)idxReportsPeriodon(periodStart, periodEnd, id)
Status lifecycle
generating → review_pending → review_in_progress → review_complete → approved → published
failed and archived are allowed from any non-terminal state. Idempotent transitions (from === to) are no-ops.
Approval + publish lifecycle (FN-3787)
A parallel approvalState gate now controls human/approver decisions before distribution:
review_complete entry:
approvalRequired=false, autoPublishOnApproval=false→approvalState=approved,status=approvedapprovalRequired=false, autoPublishOnApproval=true→approvalState=published,status=publishedapprovalRequired=true→approvalState=awaiting_approval,status=review_complete
Decision transitions:
awaiting_approval --approve--> approved(or directlypublishedwhenautoPublishOnApproval=true)awaiting_approval --reject--> rejectedapproved --publish--> published
Backfilled legacy rows use approvalState=not_required and are non-actionable.
Authorization rules:
- When
approvalRequired=trueandapproverAgentIdsis non-empty, only listed approver agent IDs may approve/reject/publish. - When
approvalRequired=trueandapproverAgentIds=[], any human dashboard user is allowed; agents are not. publishTargetsrecords publish intent metadata when a report reachespublished.
Share-ready summary blocks (FN-3787)
Approved/published reports can produce deterministic share artifacts via GET /reports/:id/share-blocks:
plainText: compact paste-ready summarymarkdown: heading/bullets + report linkslack: mrkdwn-friendly summaryemailHtml: inline-styled HTML snippet for email clients
share-blocks is intentionally locked (409) until approvalState is approved or published.
Email HTML styling exemption:
emailHtmldeliberately uses inline style attributes and hardcoded hex colors for email-client compatibility; dashboard design-token CSS rules do not apply to this serialized output format.
ReportStore API
createReport(input)getReport(id)listReports(filter?)updateReport(id, patch)setStatus(id, next, opts?)attachReview(id, combinedReview)attachRenderedHtml(id, htmlPath)deleteReport(id)
Emitted events:
report:createdreport:updatedreport:status-changedreport:review-attachedreport:deleted
This archive is the source of truth for downstream report HTML rendering (FN-3785) and dashboard report list/detail flows (FN-3786).
Dashboard view
The plugin registers a primary dashboard view (Reports) via dashboardViews with componentPath: "./dashboard-view".
The view provides:
- History list of reports with filters (cadence, status, period date range, title search, agent filter)
- Embedded detail preview using sandboxed iframe + preview HTML endpoint
- Section quick-jump navigation by stable
data-sectionmarkers - Side-by-side comparison drawer for two reports with section-level diff summary
- Standalone HTML download action wired to the export endpoint