`batch-cli-plugins` — the u7 worker's mega-batch: `packages/cli` + `plugins` + anything left. ## The batch is 7 guards, and 3 of them are not guards at all The census's per-file list gives this batch seven sites. Reading them, **three are a foreign vocabulary the census matches on the string alone**: | file | site | verdict | |---|---|---| | `plugins/fusion-plugin-reports/store/report-store.ts` | `next === "archived"` ×2 | **not a column** — `next` is a `ReportStatus` | | `plugins/fusion-plugin-reports/store/report-types.ts` | `to === "failed" \|\| to === "archived"` | **not a column** — same enum, its own terminal states | The reports plugin has its own status lineage (`draft → generating → review_* → approved → published`, plus `failed`/`archived`) that shares two spellings with the lifecycle vocabulary. A report is not on a board and has no workflow, so resolving an IR there would answer a question nobody asked. All three are marked `DELIBERATE-LITERAL` with the reason at the site. **This cuts the other way from #2763.** That PR establishes the census total as a *floor* (25 membership predicates it structurally cannot see). This is the opposite error in the same number: a foreign enum inflating it. The total is neither a ceiling nor a floor — it is an estimate with error in both directions, and the per-file list is worth reading before trusting a file's count. ## Converted (census before → after, per file) | file | before | after | |---|---|---| | `packages/cli/src/commands/pr.ts` | 1 | **0** | | `plugins/…/even-realities-glasses/notifications/diff.ts` | 1 | **0** | | `plugins/…/reports/store/report-store.ts` | 2 | **0** (deliberate) | | `plugins/…/reports/store/report-types.ts` | 1 | **0** (deliberate) | ### `fn pr create` refused every card on a renamed board The live defect in this batch. The gate was `task.column !== "in-review"`, and its error told the operator to move the task to a column their board does not have: ``` Error: Task must be in 'in-review' column to create a PR (current: signoff) ``` There is no way to satisfy that short of renaming the workflow back. Now resolved through core's `resolveReviewColumns`, and the message names the lanes that actually exist. **The SET, not `lifecycle.review`.** A board may declare more than one review lane, and a card parked in a `humanReview`-only lane is still a card you can open a PR from. A single-id answer keeps refusing those — the same narrowing #2728's review caught in the CLI retry gate, which is why the test pins both lanes. ## Skipped, with the reason **`plugins/fusion-plugin-even-cards` (2 guards) — blocked on packaging, not on analysis.** The defect is real: `boardToDeck` filters with `column !== "archived" && column !== "done"`, so on a renamed board every finished card stays in the deck, fills `maxCards`, and pushes the active cards off the display. The wearer sees a board that never finishes anything. I implemented the fix and **reverted it**: this plugin is not in `pnpm-workspace.yaml` and depends only on `@fusion/plugin-sdk` — it has no `@fusion/core` dependency, so the route cannot reach `resolveTaskLifecycleColumns`. Adding one is a packaging change, which this program's rules put out of scope. Shipping only the injected parameter without a caller was the alternative, and that is precisely the decorative conversion #2759 documents: the census would drop by 2 and the deck would keep the bug. Flagged for whoever owns the plugin's dependency surface. The glasses plugin next door *does* depend on `@fusion/core`, so this is a one-plugin problem, not a plugin-wide one. ## Honest note on the glasses conversion `diff.ts`'s completion branch is **currently unreachable** — the only production caller (`notifier.ts`) passes `alsoNotifyOnDone: false`. So that conversion changes nothing at runtime today. It is converted rather than marked deliberate because the literal is not deliberate: it is wrong, and would ship the bug the day someone turns the flag on. Stated here rather than left for a reviewer to discover. ## Verification - new CLI suite **4 passed**; `pr-command` + `pr-automerge-cleanup` + `bin-pr-router` **35 passed** - glasses plugin **181 passed (19 files)** · reports plugin **110 passed (23 files)** - `pnpm test:gate` — **158 / 10 / 487 / 71** · `pnpm lint` clean · `--strict` exits 0 **Revert proof, measured.** Restoring `if (task.column !== "in-review")` fails 3 of the 4 new cases (`process.exit:1` on both renamed lanes, and the refusal message reverts to naming `in-review`). The unresolvable-workflow case keeps passing — it is the legacy path — so the negative cases alone do not pin the fix and all four are required. ## Handoff to `batch-engine` `packages/engine/src/project-engine.ts` **5 → 0** is finished, green, and pushed as `handoff/project-engine-lanes-for-batch-engine` (`34dbb35209`) for the capacity worker to cherry-pick — it is engine-owned, not mine to land. It fixes two live defects: a card that **had merged** reported as a failed merge to `fn task merge` and the dashboard button (`merged: finalTask?.column === "done"`), and the three post-finalize `column === "done" && mergeConfirmed` fast-path checks, which on a renamed board sent an already-landed card down the bounce path — re-queued, retry-counted, and in the capped branch parked `failed` with its merge sitting on main. Plus `hasAutoHealableVerificationBufferFailure`, which returned false for every card on a renamed board, so a buffer-overflow verification failure was never auto-healed. 8 new tests, revert-proven (restoring the literal fails 4 of 8), gate green. --- ## Completion pass (u7) — the batch is now closed Two workers converged on this branch. I rebased onto the first-landed commit rather than force-pushing over it, took its wording wherever the conclusion was identical, and added what was missing. ### What this pass added 1. **`even-cards` (2 sites)** — the only in-scope file the first pass left open. Marked DELIBERATE-LITERAL: the package depends on `@fusion/plugin-sdk` only, and the SDK does not re-export the lifecycle role helpers, so there is no IR, no store, and no trait flags to resolve *from*. Fixing it properly means the SDK exposing role flags on the task shape it hands plugins — a structural change, out of scope, and recorded at the site as the correct home. Live consequence is cosmetic: a finished card on a renamed board shows as active in the glasses deck. 2. **A red test in the `fn pr create` conversion.** The incoming version rendered `Task must be in 'in-review' to create a PR`, dropping the word `column`. `task.test.ts:3422` pins `must be in 'in-review' column`, so that hunk failed `runTaskPrCreate > exits with error when task not in in-review column`. Restoring the word makes the single-lane message **byte-identical** to the pre-conversion one, which is what a vocabulary conversion should be — the guard's own test now passes unmodified. Marked at the site so it is not "simplified" back. 3. **Duplicate imports** — the two independent conversions each added `resolveWorkflowIrForTask`/`resolveReviewColumns`, which does not compile. Deduped in its own commit. ### Census Measured with `--json` on `origin/main` and on this branch. | file | before | after | action | |---|---|---|---| | `packages/cli/src/commands/pr.ts` | 1 | 0 | converted | | `plugins/fusion-plugin-reports/src/store/report-types.ts` | 1 | 0 | marked | | `plugins/fusion-plugin-reports/src/store/report-store.ts` | 2 | 0 | marked | | `plugins/fusion-plugin-even-cards/src/cards/board-cards.ts` | 2 | 0 | marked | | `plugins/fusion-plugin-even-realities-glasses/.../diff.ts` | 1 | 0 | marked | Backlog **415 → 408** (−7, exactly the in-scope count). Deliberate **40 → 46** (+6 marked); 6 + 1 converted = 7. `--strict` exits 0. **Nothing remains in `cli` + `plugins` + everything-else — there is no follow-up batch behind this one.** ### One note on the `even-realities-glasses` site Worth recording beyond "cannot resolve": its only production caller (`notifier.ts:80`) passes `alsoNotifyOnDone: false`, so that arm is **unreachable today**. Converting it could not have changed observed behaviour either way. ### Verification (measured, on the merged branch) - `pnpm --filter @runfusion/fusion exec tsc --noEmit` → exit 0 - `pnpm lint` → 0 errors - CLI `task.test.ts` → 144 passed, including the `runTaskPrCreate` guard test - `@fusion-plugin-examples/reports` → 110 passed; `even-realities-glasses` → 181 passed **Pre-existing failures, not from this change:** the 5 `runTaskImportFromGitHub` / `runTaskImportGitHubInteractive` tests fail identically on `origin/main` — verified by stashing this diff and re-running (5 failed / 144 passed both ways). --- ## Census audit (unowned follow-on) After closing the batch scope I audited whether the **392** column-backlog number is inflated by foreign vocabularies — the class this batch found in the reports plugin, where `"archived"` is a `ReportStatus` rather than a board lane. If that class were widespread, every remaining batch would be chasing sites that must not be converted. **It is not. The number is real.** A receiver-level pass over all 392 column-category sites found exactly **3** false positives, all in `plugins/fusion-plugin-reports` (`next`, a `ReportStatus`), all now marked in this PR. What was checked and cleared: - **Property-reached foreign enums** (`step.status`, `feature.status`, `mission.status`) — already correctly bucketed into the separate `status` category (185), not the column backlog. Verified against `merge-queue-ops.ts`: 11 lifecycle-spelled literals in the file, census counts **1**, and that 1 is the genuine `.column` guard. - **Bare step-status variables** (`status`, `currentStatus`, `liveStatus` compared to `"done"`/`"skipped"`) — likewise excluded. - **Every other receiver in the backlog** — `to`, `from`, `column`, `fromColumn`, `toColumn`, `latestColumn`, `state`, `preArchiveColumn`. All resolve to genuine task columns. `executor.ts`'s 15 sites were spot-checked line by line: all 15 are real. The gap the classifier genuinely cannot close is a foreign enum held in a **bare variable** — the receiver name carries no type information, so `next === "archived"` is indistinguishable from a lifecycle guard by AST alone. That is why the reports sites need a marker rather than a classifier fix, and it is now documented in `lifecycle-column-census-ast.mjs`'s header alongside the measured scope, so the remaining batches do not re-run this hunt. Census tests: **43 passed**. The change is comment-only. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reports Plugin for Fusion
Generates HTML system activity reports with multi-agent review.
Install (one-click)
- Open Settings → Plugins → Fusion Plugins.
- In Bundled Plugins, click Install on Reports.
- Enable the plugin if prompted.
Once installed and enabled, Fusion registers the Reports dashboard destination automatically.
Scaffold seams (interim)
The plugin currently exports four interim scaffold seams to unblock downstream implementation work:
resolveEnabledCadences/ReportsCadence(src/cadence.ts) — interim cadence-resolution seam; scheduled cadence registry + cron/sentinel wiring lands in FN-3779.aggregateReportData+ aggregation types (src/aggregation.ts) — interim aggregation seam; real aggregation orchestration lands in FN-3780.startReportsPipeline+ pipeline dependency interfaces (src/pipeline.ts) — interim orchestrator seam to keep call sites stable while FN-3779/FN-3780 wire real runtime components.createInMemoryReportsRunsStore+ run record/store types (src/runs-store.ts) — interim in-memory run state store; persistent store replacement lands in FN-3784.
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 the project PostgreSQL schema through the shared ReportStore provider. Rows are scoped by canonical project identity.
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