Files
fusion/plugins/fusion-plugin-reports
gsxdsm 4184fde08d batch-cli-plugins: 7 guards — 3 were a foreign enum, and fn pr create refused every card on a renamed board (#2775)
`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>
2026-07-30 10:38:39 -07:00
..
2026-07-26 18:11:47 -07:00

Reports Plugin for Fusion

Generates HTML system activity reports with multi-agent review.

Install (one-click)

  1. Open Settings → Plugins → Fusion Plugins.
  2. In Bundled Plugins, click Install on Reports.
  3. 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;
}
  • perspective is appended to the reviewer system prompt.
  • promptTemplateId selects a template from settings.reviewPromptTemplates[templateId] when present.
  • provider + modelId optionally override model selection per reviewer.

Prompt template contract

runReviewPanel resolves reviewer templates in this order:

  1. settings.reviewPromptTemplates[promptTemplateId ?? id]
  2. settings.reviewPrompt
  3. 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 reject with 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:

  • idxReportsCadenceCreated on (cadence, createdAt DESC, id)
  • idxReportsStatusUpdated on (status, updatedAt DESC, id)
  • idxReportsPeriod on (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=approved
  • approvalRequired=false, autoPublishOnApproval=true → approvalState=published, status=published
  • approvalRequired=true → approvalState=awaiting_approval, status=review_complete

Decision transitions:

  • awaiting_approval --approve--> approved (or directly published when autoPublishOnApproval=true)
  • awaiting_approval --reject--> rejected
  • approved --publish--> published

Backfilled legacy rows use approvalState=not_required and are non-actionable.

Authorization rules:

  • When approvalRequired=true and approverAgentIds is non-empty, only listed approver agent IDs may approve/reject/publish.
  • When approvalRequired=true and approverAgentIds=[], any human dashboard user is allowed; agents are not.
  • publishTargets records publish intent metadata when a report reaches published.

Share-ready summary blocks (FN-3787)

Approved/published reports can produce deterministic share artifacts via GET /reports/:id/share-blocks:

  • plainText: compact paste-ready summary
  • markdown: heading/bullets + report link
  • slack: mrkdwn-friendly summary
  • emailHtml: inline-styled HTML snippet for email clients

share-blocks is intentionally locked (409) until approvalState is approved or published.

Email HTML styling exemption: emailHtml deliberately 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:created
  • report:updated
  • report:status-changed
  • report:review-attached
  • report: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-section markers
  • Side-by-side comparison drawer for two reports with section-level diff summary
  • Standalone HTML download action wired to the export endpoint